/**
* 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 .
*/
import {ContactControllerModel} from '../controller_model/contact';
import {supportsPassive, throttle} from '../helpers';
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';
import {NotificationService} from '../services/notification';
import {ReceiverService} from '../services/receiver';
import {SettingsService} from '../services/settings';
import {StateService} from '../services/state';
import {VersionService} from '../services/version';
import {WebClientService} from '../services/webclient';
import {ControllerModelMode} from '../types/enums';
class DialogController {
public static $inject = ['$mdDialog'];
public $mdDialog: ng.material.IDialogService;
public activeElement: HTMLElement | null;
constructor($mdDialog: ng.material.IDialogService) {
this.$mdDialog = $mdDialog;
this.activeElement = document.activeElement as HTMLElement;
}
public cancel(): void {
this.$mdDialog.cancel();
this.done();
}
protected hide(data: any): void {
this.$mdDialog.hide(data);
this.done();
}
private done(): void {
if (this.resumeFocusOnClose() === true && this.activeElement !== null) {
// reset focus
this.activeElement.focus();
}
}
/**
* If true, the focus on the active element (before opening the dialog)
* will be restored. Default `true`, override if desired.
*/
protected resumeFocusOnClose(): boolean {
return true;
}
}
/**
* Handle sending of files.
*/
class SendFileController extends DialogController {
public static $inject = ['$mdDialog', 'preview'];
public caption: string;
public sendAsFile: boolean = false;
public preview: threema.FileMessageData | null = null;
constructor($mdDialog: ng.material.IDialogService, preview: threema.FileMessageData) {
super($mdDialog);
this.preview = preview;
}
public send(): void {
this.hide({
caption: this.caption,
sendAsFile: this.sendAsFile,
});
}
public keypress($event: KeyboardEvent): void {
if ($event.key === 'Enter') { // see https://developer.mozilla.org/de/docs/Web/API/KeyboardEvent/key/Key_Values
this.send();
}
}
public hasPreview(): boolean {
return this.preview !== null && this.preview !== undefined;
}
}
/**
* Handle settings
*/
class SettingsController {
public static $inject = ['$mdDialog', '$window', 'SettingsService', 'NotificationService'];
public $mdDialog: ng.material.IDialogService;
public $window: ng.IWindowService;
public settingsService: SettingsService;
private notificationService: NotificationService;
public activeElement: HTMLElement | null;
private desktopNotifications: boolean;
private notificationApiAvailable: boolean;
private notificationPermission: boolean;
private notificationPreview: boolean;
private notificationSound: boolean;
constructor($mdDialog: ng.material.IDialogService,
$window: ng.IWindowService,
settingsService: SettingsService,
notificationService: NotificationService) {
this.$mdDialog = $mdDialog;
this.$window = $window;
this.settingsService = settingsService;
this.notificationService = notificationService;
this.activeElement = document.activeElement as HTMLElement;
this.desktopNotifications = notificationService.getWantsNotifications();
this.notificationApiAvailable = notificationService.isNotificationApiAvailable();
this.notificationPermission = notificationService.getNotificationPermission();
this.notificationPreview = notificationService.getWantsPreview();
this.notificationSound = notificationService.getWantsSound();
}
public cancel(): void {
this.$mdDialog.cancel();
this.done();
}
protected hide(data: any): void {
this.$mdDialog.hide(data);
this.done();
}
private done(): void {
if (this.activeElement !== null) {
// Reset focus
this.activeElement.focus();
}
}
public setWantsNotifications(desktopNotifications: boolean) {
this.notificationService.setWantsNotifications(desktopNotifications);
}
public setWantsPreview(notificationPreview: boolean) {
this.notificationService.setWantsPreview(notificationPreview);
}
public setWantsSound(notificationSound: boolean) {
this.notificationService.setWantsSound(notificationSound);
}
}
class ConversationController {
public name = 'navigation';
private logTag: string = '[ConversationController]';
// Angular services
private $stateParams;
private $timeout: ng.ITimeoutService;
private $state: ng.ui.IStateService;
private $log: ng.ILogService;
private $scope: ng.IScope;
private $rootScope: ng.IRootScopeService;
private $filter: ng.IFilterService;
// Own services
private webClientService: WebClientService;
private receiverService: ReceiverService;
private stateService: StateService;
private mimeService: MimeService;
// Third party services
private $mdDialog: ng.material.IDialogService;
private $mdToast: ng.material.IToastService;
// Controller model
private controllerModel: threema.ControllerModel;
// DOM Elements
private domChatElement: HTMLElement;
// Scrolling
public showScrollJump: boolean = false;
public receiver: threema.Receiver;
public type: threema.ReceiverType;
public message: string = '';
public lastReadMsgId: number = 0;
public msgReadReportPending = false;
private hasMore = true;
private latestRefMsgId: number = null;
private allText: string;
private messages: threema.Message[];
public initialData: threema.InitialConversationData = {
draft: '',
initialText: '',
};
private $translate: ng.translate.ITranslateService;
private locked = false;
public maxTextLength: number;
public isTyping = (): boolean => false;
public allMentions: threema.Mention[] = [];
public currentMentions: threema.Mention[] = [];
public currentMentionFilterWord = null;
public selectedMention: number = null;
private uploading = {
enabled: false,
value1: 0,
value2: 0,
};
public static $inject = [
'$stateParams', '$state', '$timeout', '$log', '$scope', '$rootScope',
'$mdDialog', '$mdToast', '$location', '$translate', '$filter',
'WebClientService', 'StateService', 'ReceiverService', 'MimeService', 'VersionService',
'ControllerModelService',
];
constructor($stateParams: threema.ConversationStateParams,
$state: ng.ui.IStateService,
$timeout: ng.ITimeoutService,
$log: ng.ILogService,
$scope: ng.IScope,
$rootScope: ng.IRootScopeService,
$mdDialog: ng.material.IDialogService,
$mdToast: ng.material.IToastService,
$location,
$translate: ng.translate.ITranslateService,
$filter: ng.IFilterService,
webClientService: WebClientService,
stateService: StateService,
receiverService: ReceiverService,
mimeService: MimeService,
versionService: VersionService,
controllerModelService: ControllerModelService) {
this.$stateParams = $stateParams;
this.$timeout = $timeout;
this.$log = $log;
this.webClientService = webClientService;
this.receiverService = receiverService;
this.stateService = stateService;
this.mimeService = mimeService;
this.$state = $state;
this.$scope = $scope;
this.$filter = $filter;
this.$rootScope = $rootScope;
this.$mdDialog = $mdDialog;
this.$mdToast = $mdToast;
this.$translate = $translate;
// Close any showing dialogs
this.$mdDialog.cancel();
this.maxTextLength = this.webClientService.getMaxTextLength();
this.allText = this.$translate.instant('messenger.ALL');
// On every navigation event, close all dialogs.
// Note: Deprecated. When migrating ui-router ($state),
// replace with transition hooks.
$rootScope.$on('$stateChangeStart', () => this.$mdDialog.cancel());
// Check for version updates
versionService.checkForUpdate();
// Redirect to welcome if necessary
if (stateService.state === 'error') {
$log.debug('ConversationController: WebClient not yet running, redirecting to welcome screen');
$state.go('welcome');
return;
}
if (!this.locked) {
// Get DOM references
this.domChatElement = document.querySelector('#conversation-chat') as HTMLElement;
// Add custom event handlers
this.domChatElement.addEventListener('scroll', throttle(() => {
$rootScope.$apply(() => {
this.updateScrollJump();
});
}, 100, this), supportsPassive() ? {passive: true} as any : false);
}
// Set receiver and type
try {
this.receiver = webClientService.receivers.getData({type: $stateParams.type, id: $stateParams.id});
this.type = $stateParams.type;
if (this.receiver.type === undefined) {
this.receiver.type = this.type;
}
// Initialize controller model
const mode = ControllerModelMode.CHAT;
switch (this.receiver.type) {
case 'me':
$log.warn(this.logTag, 'Cannot chat with own contact');
$state.go('messenger.home');
return;
case 'contact':
this.controllerModel = controllerModelService.contact(
this.receiver as threema.ContactReceiver, mode);
break;
case 'group':
this.controllerModel = controllerModelService.group(
this.receiver as threema.GroupReceiver, mode);
break;
case 'distributionList':
this.controllerModel = controllerModelService.distributionList(
this.receiver as threema.DistributionListReceiver, mode);
break;
default:
$log.error(this.logTag, 'Cannot initialize controller model:',
'Invalid receiver type "' + this.receiver.type + '"');
$state.go('messenger.home');
return;
}
// Check if this receiver may be viewed
if (this.controllerModel.canView() === false) {
$log.warn(this.logTag, 'Cannot view this receiver, redirecting to home');
$state.go('messenger.home');
return;
}
// initial set locked state
this.locked = this.receiver.locked;
this.receiverService.setActive(this.receiver);
if (!this.receiver.locked) {
let latestHeight = 0;
// update unread count
this.webClientService.messages.updateFirstUnreadMessage(this.receiver);
this.messages = this.webClientService.messages.register(
this.receiver,
this.$scope,
(e, allMessages: threema.Message[], hasMore: boolean) => {
this.messages = allMessages;
this.hasMore = hasMore;
if (this.latestRefMsgId !== null) {
// scroll to div..
this.domChatElement.scrollTop =
this.domChatElement.scrollHeight - latestHeight;
this.latestRefMsgId = null;
}
latestHeight = this.domChatElement.scrollHeight;
},
);
// Enable mentions only in group chats
if (this.type === 'group') {
this.allMentions.push({
identity: null,
query: this.$translate.instant('messenger.ALL').toLowerCase(),
isAll: true,
});
this.controllerModel.getMembers().forEach((identity: string) => {
const contactReceiver = this.webClientService.contacts.get(identity);
if (contactReceiver) {
this.allMentions.push({
identity: identity,
query: (contactReceiver.displayName + ' ' + identity).toLowerCase(),
isAll: false,
});
}
});
}
this.initialData = {
draft: webClientService.getDraft(this.receiver),
initialText: $stateParams.initParams ? $stateParams.initParams.text : '',
};
if (this.receiver.type === 'contact') {
this.isTyping = () => this.webClientService.isTyping(this.receiver as threema.ContactReceiver);
}
}
} catch (error) {
$log.error('Could not set receiver and type');
$log.debug(error.stack);
$state.go('messenger.home');
}
// reload controller if locked state was changed
$scope.$watch(() => {
return this.receiver.locked;
}, () => {
if (this.locked !== this.receiver.locked) {
$state.reload();
}
});
}
public isEnabled(): boolean {
return this.type !== 'group'
|| !(this.receiver as threema.GroupReceiver).disabled;
}
public isQuoting(): boolean {
return this.getQuote() !== undefined;
}
public getQuote(): threema.Quote {
return this.webClientService.getQuote(this.receiver);
}
public cancelQuoting(): void {
// Clear current quote
this.webClientService.setQuote(this.receiver);
}
public showError(errorMessage: string, toastLength = 4000) {
if (errorMessage === undefined || errorMessage.length === 0) {
errorMessage = this.$translate.instant('error.ERROR_OCCURRED');
}
this.$mdToast.show(
this.$mdToast.simple()
.textContent(errorMessage)
.position('bottom center'));
}
/**
* Submit function for input field. Can contain text or file data.
* Return whether sending was successful.
*/
public submit = (type: threema.MessageContentType, contents: threema.MessageData[]): Promise => {
// Validate whether a connection is available
return new Promise((resolve, reject) => {
if (this.stateService.state !== 'ok') {
// Invalid connection, show toast and abort
this.showError(this.$translate.instant('error.NO_CONNECTION'));
return reject();
}
let success = true;
const nextCallback = (index: number) => {
if (index === contents.length - 1) {
if (success) {
resolve();
} else {
reject();
}
}
};
switch (type) {
case 'file':
// Determine file type
let showSendAsFileCheckbox = false;
let captionSupported = false;
for (const msg of contents as threema.FileMessageData[]) {
if (!msg.fileType) {
msg.fileType = 'application/octet-stream';
}
captionSupported = this.mimeService.isImage(msg.fileType);
if (this.mimeService.isImage(msg.fileType)
|| this.mimeService.isAudio(msg.fileType)
|| this.mimeService.isVideo(msg.fileType)) {
showSendAsFileCheckbox = true;
break;
}
}
// Prepare preview
let preview: threema.FileMessageData | null = null;
if (contents.length === 1) {
const msg = contents[0] as threema.FileMessageData;
if (this.mimeService.isImage(msg.fileType)) {
preview = msg;
}
}
// Eager translations
const title = this.$translate.instant('messenger.CONFIRM_FILE_SEND', {
senderName: (this.$filter('emojify') as any)
((this.$filter('emptyToPlaceholder') as any)(this.receiver.displayName, '-')),
});
const placeholder = this.$translate.instant('messenger.CONFIRM_FILE_CAPTION');
const confirmSendAsFile = this.$translate.instant('messenger.CONFIRM_SEND_AS_FILE');
// Show confirmation dialog
this.$mdDialog.show({
clickOutsideToClose: false,
locals: { preview: preview },
controller: 'SendFileController',
controllerAs: 'ctrl',
// tslint:disable:max-line-length
template: `
${title}
${confirmSendAsFile}
`,
// tslint:enable:max-line-length
}).then((data) => {
const caption = data.caption;
const sendAsFile = data.sendAsFile;
contents.forEach((msg: threema.FileMessageData, index: number) => {
if (caption !== undefined && caption.length > 0) {
msg.caption = caption;
}
msg.sendAsFile = sendAsFile;
this.webClientService.sendMessage(this.$stateParams, type, msg)
.then(() => {
nextCallback(index);
})
.catch((error) => {
this.$log.error(error);
this.showError(error);
success = false;
nextCallback(index);
});
});
}, angular.noop);
break;
case 'text':
// do not show confirmation, send directly
contents.forEach((msg: threema.MessageData, index: number) => {
msg.quote = this.webClientService.getQuote(this.receiver);
// remove quote
this.webClientService.setQuote(this.receiver);
// send message
this.webClientService.sendMessage(this.$stateParams, type, msg)
.then(() => {
nextCallback(index);
})
.catch((error) => {
this.$log.error(error);
this.showError(error);
success = false;
nextCallback(index);
});
});
return;
default:
this.$log.warn('Invalid message type:', type);
reject();
}
});
}
/**
* Something was typed.
*
* In contrast to startTyping, this method is is always called, not just if
* the text field is non-empty.
*/
public onTyping = (text: string, currentWord: string = null) => {
// Update draft
this.webClientService.setDraft(this.receiver, text);
if (currentWord && currentWord.substr(0, 1) === '@') {
this.currentMentionFilterWord = currentWord.substr(1);
const query = this.currentMentionFilterWord.toLowerCase().trim();
const selectedMentionObject = this.getSelectedMention();
this.currentMentions = this.allMentions.filter((i) => {
if (query.length === 0) {
return true;
}
return i.query.indexOf(query) >= 0;
});
// If only one mention is filtered, select them
if (this.currentMentions.length === 1) {
this.selectedMention = 0;
} else if (selectedMentionObject !== null) {
// Get the new position of the latest selected mention object
this.selectedMention = null;
this.selectedMention = this.currentMentions.findIndex((m) => {
return m.identity === selectedMentionObject.identity;
});
}
} else {
this.currentMentionFilterWord = null;
}
}
public getSelectedMention = (): threema.Mention => {
if (this.selectedMention === null
|| this.selectedMention < 0
|| this.selectedMention > this.currentMentions.length - 1) {
return null;
}
return this.currentMentions[this.selectedMention];
}
public showMentionSelector = (): boolean => {
return this.type === 'group'
&& this.currentMentionFilterWord != null
&& this.currentMentions.length > 0;
}
/**
* Handle mention selector navigation
*/
public onComposeKeyDown = (ev: KeyboardEvent): boolean => {
/* Make mentions readonly for now
if (this.showMentionSelector() && !ev.shiftKey) {
let move = ev.key === 'ArrowDown' ? 1 : (ev.key === 'ArrowUp' ? - 1 : 0);
if (move !== 0) {
// Move cursors position in mention selector
if (this.selectedMention !== null) {
this.selectedMention += move;
// Fix positions
if (this.selectedMention > this.currentMentions.length - 1) {
this.selectedMention = 0;
} else if (this.selectedMention < 0) {
this.selectedMention = this.currentMentions.length - 1;
}
} else {
this.selectedMention = 0;
}
return false;
}
if (ev.key === 'Enter') {
// Enter, select current mention
const selectedMentionObject = this.getSelectedMention();
if (selectedMentionObject === null) {
// If no (or a invalid) mention is selected, select the first mention
this.selectedMention = 0;
} else {
this.onMentionSelected(selectedMentionObject.identity);
}
return false;
}
}
*/
return true;
}
public onMentionSelected(identity: string = null): void {
this.$rootScope.$broadcast('onMentionSelected', {
query: '@' + this.currentMentionFilterWord,
mention: '@[' + (identity === null ? '@@@@@@@@' : identity.toUpperCase()) + ']',
});
}
public onUploading = (inProgress: boolean, percentCurrent: number = null, percentFull: number = null) => {
this.uploading.enabled = inProgress;
this.uploading.value1 = Number(percentCurrent);
this.uploading.value2 = Number(percentCurrent);
}
/**
* We started typing.
*/
public startTyping = () => {
// Notify app
this.webClientService.sendMeIsTyping(this.$stateParams, true);
}
/**
* We stopped typing.
*/
public stopTyping = () => {
// Notify app
this.webClientService.sendMeIsTyping(this.$stateParams, false);
}
/**
* User scrolled to the top of the chat.
*/
public topOfChat(): void {
this.requestMessages();
}
public requestMessages(): void {
const refMsgId = this.webClientService.requestMessages(this.$stateParams);
if (refMsgId !== null
&& refMsgId !== undefined) {
// new message are requested, scroll to refMsgId
this.latestRefMsgId = refMsgId;
} else {
this.latestRefMsgId = null;
}
}
public showReceiver(ev): void {
this.$state.go('messenger.home.detail', this.receiver);
}
public hasMoreMessages(): boolean {
return this.hasMore;
}
/**
* A message has been seen. Report it to the app, with a small delay to
* avoid sending too many messages at once.
*/
public msgRead(msgId: number): void {
if (msgId > this.lastReadMsgId) {
this.lastReadMsgId = msgId;
}
if (!this.msgReadReportPending) {
this.msgReadReportPending = true;
const receiver = angular.copy(this.receiver);
receiver.type = this.type;
this.$timeout(() => {
this.webClientService.requestRead(receiver, this.lastReadMsgId);
this.msgReadReportPending = false;
}, 500);
}
}
public goBack(): void {
this.receiverService.setActive(undefined);
// redirect to messenger home
this.$state.go('messenger.home');
}
/**
* Scroll to bottom of chat.
*/
public scrollDown(): void {
this.domChatElement.scrollTop = this.domChatElement.scrollHeight;
}
/**
* Only show the scroll to bottom button if user scrolled more than 10px
* away from bottom.
*/
private updateScrollJump(): void {
const chat = this.domChatElement;
this.showScrollJump = chat.scrollHeight - (chat.scrollTop + chat.offsetHeight) > 10;
}
}
class NavigationController {
public name = 'navigation';
private webClientService: WebClientService;
private receiverService: ReceiverService;
private stateService: StateService;
private trustedKeyStoreService: TrustedKeyStoreService;
private activeTab: 'contacts' | 'conversations' = 'conversations';
private searchVisible = false;
private searchText: string = '';
private $mdDialog;
private $translate: ng.translate.ITranslateService;
private $state: ng.ui.IStateService;
public static $inject = [
'$log', '$state', '$mdDialog', '$translate',
'WebClientService', 'StateService', 'ReceiverService', 'TrustedKeyStore',
];
constructor($log: ng.ILogService, $state: ng.ui.IStateService,
$mdDialog: ng.material.IDialogService, $translate: ng.translate.ITranslateService,
webClientService: WebClientService, stateService: StateService,
receiverService: ReceiverService,
trustedKeyStoreService: TrustedKeyStoreService) {
// Redirect to welcome if necessary
if (stateService.state === 'error') {
$log.debug('NavigationController: WebClient not yet running, redirecting to welcome screen');
$state.go('welcome');
return;
}
this.webClientService = webClientService;
this.receiverService = receiverService;
this.stateService = stateService;
this.trustedKeyStoreService = trustedKeyStoreService;
this.$mdDialog = $mdDialog;
this.$translate = $translate;
this.$state = $state;
}
public contacts(): threema.ContactReceiver[] {
return Array.from(this.webClientService.contacts.values()) as threema.ContactReceiver[];
}
/**
* Search for `needle` in the `haystack`. The search is case insensitive.
*/
private matches(haystack: string, needle: string): boolean {
return haystack.toLowerCase().replace('\n', ' ').indexOf(needle.trim().toLowerCase()) !== -1;
}
/**
* Predicate function used for conversation filtering.
*
* Match by contact name *or* id *or* last message text.
*/
private searchConversation = (value: threema.Conversation, index, array): boolean => {
return this.searchText === ''
|| this.matches(value.receiver.displayName, this.searchText)
|| (value.latestMessage && value.latestMessage.body
&& this.matches(value.latestMessage.body, this.searchText))
|| (value.receiver.id.length === 8 && this.matches(value.receiver.id, this.searchText));
}
/**
* Predicate function used for contact filtering.
*
* Match by contact name *or* id.
*/
private searchContact = (value, index, array): boolean => {
return this.searchText === ''
|| value.displayName.toLowerCase().indexOf(this.searchText.toLowerCase()) !== -1
|| value.id.toLowerCase().indexOf(this.searchText.toLowerCase()) !== -1;
}
public isVisible(conversation: threema.Conversation) {
return conversation.receiver.visible;
}
public conversations(): threema.Conversation[] {
return this.webClientService.conversations.get();
}
public isActive(value: threema.Conversation): boolean {
return this.receiverService.isConversationActive(value);
}
/**
* Show dialog.
*/
public showDialog(name, ev) {
this.$mdDialog.show({
controller: DialogController,
controllerAs: 'ctrl',
templateUrl: 'partials/dialog.' + name + '.html',
parent: angular.element(document.body),
targetEvent: ev,
clickOutsideToClose: true,
fullscreen: true,
});
}
/**
* Show about dialog.
*/
public about(ev): void {
this.showDialog('about', ev);
}
/**
* Show settings dialog.
*/
public settings(ev): void {
this.$mdDialog.show({
controller: SettingsController,
controllerAs: 'ctrl',
templateUrl: 'partials/dialog.settings.html',
parent: angular.element(document.body),
targetEvent: ev,
clickOutsideToClose: true,
fullscreen: true,
});
}
/**
* Return whether a trusted key is available.
*/
public isPersistent(): boolean {
return this.trustedKeyStoreService.hasTrustedKey();
}
/**
* Close the session.
*/
public closeSession(ev): void {
const confirm = this.$mdDialog.confirm()
.title(this.$translate.instant('common.SESSION_CLOSE'))
.textContent(this.$translate.instant('common.CONFIRM_CLOSE_BODY'))
.targetEvent(ev)
.ok(this.$translate.instant('common.YES'))
.cancel(this.$translate.instant('common.CANCEL'));
this.$mdDialog.show(confirm).then(() => {
const deleteStoredData = false;
const resetPush = true;
const redirect = true;
this.webClientService.stop(true, deleteStoredData, resetPush, redirect);
}, () => {
// do nothing
});
}
/**
* Close and delete the session.
*/
public deleteSession(ev): void {
const confirm = this.$mdDialog.confirm()
.title(this.$translate.instant('common.SESSION_DELETE'))
.textContent(this.$translate.instant('common.CONFIRM_DELETE_CLOSE_BODY'))
.targetEvent(ev)
.ok(this.$translate.instant('common.YES'))
.cancel(this.$translate.instant('common.CANCEL'));
this.$mdDialog.show(confirm).then(() => {
const deleteStoredData = true;
const resetPush = true;
const redirect = true;
this.webClientService.stop(true, deleteStoredData, resetPush, redirect);
}, () => {
// do nothing
});
}
public addContact(ev): void {
this.$state.go('messenger.home.create', {
type: 'contact',
});
}
public createGroup(ev): void {
this.$state.go('messenger.home.create', {
type: 'group',
});
}
public createDistributionList(ev): void {
this.$state.go('messenger.home.create', {
type: 'distributionList',
});
}
/**
* Toggle search bar.
*/
public toggleSearch(): void {
this.searchVisible = !this.searchVisible;
}
public getMyIdentity(): threema.Identity {
return this.webClientService.getMyIdentity();
}
public showMyIdentity(): boolean {
return this.getMyIdentity() !== undefined;
}
}
class MessengerController {
public name = 'messenger';
private receiverService: ReceiverService;
private $state;
private webClientService: WebClientService;
public static $inject = [
'$scope', '$state', '$log', '$mdDialog', '$translate',
'StateService', 'ReceiverService', 'WebClientService', 'ControllerService',
];
constructor($scope, $state, $log: ng.ILogService, $mdDialog: ng.material.IDialogService,
$translate: ng.translate.ITranslateService,
stateService: StateService, receiverService: ReceiverService,
webClientService: WebClientService, controllerService: ControllerService) {
// Redirect to welcome if necessary
if (stateService.state === 'error') {
$log.debug('MessengerController: WebClient not yet running, redirecting to welcome screen');
$state.go('welcome');
return;
}
controllerService.setControllerName('messenger');
this.receiverService = receiverService;
this.$state = $state;
this.webClientService = webClientService;
// watch for alerts
$scope.$watch(() => webClientService.alerts, (alerts: threema.Alert[]) => {
if (alerts.length > 0) {
angular.forEach(alerts, (alert: threema.Alert) => {
$mdDialog.show(
$mdDialog.alert()
.clickOutsideToClose(true)
.title(alert.type)
.textContent(alert.message)
.ok($translate.instant('common.OK')));
});
// clean array
webClientService.alerts = [];
}
}, true);
this.webClientService.setReceiverListener({
onRemoved(receiver: threema.Receiver) {
switch ($state.current.name) {
case 'messenger.home.conversation':
case 'messenger.home.detail':
case 'messenger.home.edit':
if ($state.params !== undefined
&& $state.params.type !== undefined
&& $state.params.id !== undefined) {
if ($state.params.type === receiver.type
&& $state.params.id === receiver.id) {
// conversation or sub form is open, redirect to home!
$state.go('messenger.home', null, {location: 'replace'});
}
}
break;
default:
$log.warn('Ignored onRemoved event for state', $state.current.name);
}
},
});
}
public showDetail(): boolean {
return !this.$state.is('messenger.home');
}
}
class ReceiverDetailController {
private logTag: string = '[ReceiverDetailController]';
public $mdDialog: any;
public $state: ng.ui.IStateService;
public receiver: threema.Receiver;
public me: threema.MeReceiver;
public title: string;
public fingerPrint?: string;
private fingerPrintService: FingerPrintService;
private contactService: ContactService;
private showGroups = false;
private showDistributionLists = false;
private inGroups: threema.GroupReceiver[] = [];
private inDistributionLists: threema.DistributionListReceiver[] = [];
private hasSystemEmails = false;
private hasSystemPhones = false;
private isWorkReceiver = false;
private showBlocked = () => false;
private controllerModel: threema.ControllerModel;
public static $inject = [
'$log', '$stateParams', '$state', '$mdDialog',
'WebClientService', 'FingerPrintService', 'ContactService', 'ControllerModelService',
];
constructor($log: ng.ILogService, $stateParams, $state: ng.ui.IStateService, $mdDialog: ng.material.IDialogService,
webClientService: WebClientService, fingerPrintService: FingerPrintService,
contactService: ContactService, controllerModelService: ControllerModelService) {
this.$mdDialog = $mdDialog;
this.$state = $state;
this.fingerPrintService = fingerPrintService;
this.contactService = contactService;
this.receiver = webClientService.receivers.getData($stateParams);
this.me = webClientService.me;
// Append members
if (this.receiver.type === 'contact') {
const contactReceiver = this.receiver as threema.ContactReceiver;
this.contactService.requiredDetails(contactReceiver)
.then(() => {
this.hasSystemEmails = contactReceiver.systemContact.emails.length > 0;
this.hasSystemPhones = contactReceiver.systemContact.phoneNumbers.length > 0;
})
.catch(() => {
// do nothing
});
this.isWorkReceiver = contactReceiver.identityType === threema.IdentityType.Work;
this.fingerPrint = this.fingerPrintService.generate(contactReceiver.publicKey);
webClientService.groups.forEach((groupReceiver: threema.GroupReceiver) => {
// check if my identity is a member
if (groupReceiver.members.indexOf(contactReceiver.id) !== -1) {
this.inGroups.push(groupReceiver);
this.showGroups = true;
}
});
webClientService.distributionLists.forEach(
(distributionListReceiver: threema.DistributionListReceiver) => {
// check if my identity is a member
if (distributionListReceiver.members.indexOf(contactReceiver.id) !== -1) {
this.inDistributionLists.push(distributionListReceiver);
this.showDistributionLists = true;
}
},
);
this.showBlocked = () => contactReceiver.isBlocked;
}
switch (this.receiver.type) {
case 'me':
$log.warn(this.logTag, 'Cannot view own contact');
$state.go('messenger.home');
return;
case 'contact':
this.controllerModel = controllerModelService
.contact(this.receiver as threema.ContactReceiver, ControllerModelMode.VIEW);
break;
case 'group':
this.controllerModel = controllerModelService
.group(this.receiver as threema.GroupReceiver, ControllerModelMode.VIEW);
break;
case 'distributionList':
this.controllerModel = controllerModelService
.distributionList(this.receiver as threema.DistributionListReceiver, ControllerModelMode.VIEW);
break;
default:
$log.error(this.logTag, 'Cannot initialize controller model:',
'Invalid receiver type "' + this.receiver.type + '"');
$state.go('messenger.home');
return;
}
// If this receiver may not be viewed, navigate to "home" view
if (this.controllerModel.canView() === false) {
$log.warn(this.logTag, 'Cannot view this receiver, redirecting to home');
this.$state.go('messenger.home');
return;
}
// If this receiver is removed, navigate to "home" view
this.controllerModel.setOnRemoved((receiverId: string) => {
$log.warn(this.logTag, 'Receiver removed, redirecting to home');
this.$state.go('messenger.home');
});
}
public chat(): void {
this.$state.go('messenger.home.conversation', {
type: this.receiver.type,
id: this.receiver.id,
initParams: null,
});
}
public edit(): void {
if (!this.controllerModel.canEdit()) {
return;
}
this.$state.go('messenger.home.edit', {
type: this.receiver.type,
id: this.receiver.id,
initParams: null,
});
}
public goBack(): void {
window.history.back();
}
}
/**
* Control edit a group or a contact
* fields, validate and save routines are implemented in the specific ControllerModel
*/
class ReceiverEditController {
private logTag: string = '[ReceiverEditController]';
public $mdDialog: any;
public $state: ng.ui.IStateService;
private $translate: ng.translate.ITranslateService;
public title: string;
private $timeout: ng.ITimeoutService;
private execute: ExecuteService;
public loading = false;
private controllerModel: threema.ControllerModel;
public type: string;
public static $inject = [
'$log', '$stateParams', '$state', '$mdDialog',
'$timeout', '$translate', 'WebClientService', 'ControllerModelService',
];
constructor($log: ng.ILogService, $stateParams, $state: ng.ui.IStateService,
$mdDialog, $timeout: ng.ITimeoutService, $translate: ng.translate.ITranslateService,
webClientService: WebClientService, controllerModelService: ControllerModelService) {
this.$mdDialog = $mdDialog;
this.$state = $state;
this.$timeout = $timeout;
this.$translate = $translate;
const receiver = webClientService.receivers.getData($stateParams);
switch (receiver.type) {
case 'me':
$log.warn(this.logTag, 'Cannot edit own contact');
$state.go('messenger.home');
return;
case 'contact':
this.controllerModel = controllerModelService.contact(
receiver as threema.ContactReceiver,
ControllerModelMode.EDIT,
);
break;
case 'group':
this.controllerModel = controllerModelService.group(
receiver as threema.GroupReceiver,
ControllerModelMode.EDIT,
);
break;
case 'distributionList':
this.controllerModel = controllerModelService.distributionList(
receiver as threema.DistributionListReceiver,
ControllerModelMode.EDIT,
);
break;
default:
$log.error(this.logTag, 'Cannot initialize controller model:',
'Invalid receiver type "' + receiver.type + '"');
$state.go('messenger.home');
return;
}
this.type = receiver.type;
// If this receiver may not be viewed, navigate to "home" view
if (this.controllerModel.canView() === false) {
$log.warn(this.logTag, 'Cannot view this receiver, redirecting to home');
this.$state.go('messenger.home');
return;
}
this.execute = new ExecuteService($log, $timeout, 1000);
}
public keypress($event: KeyboardEvent): void {
if ($event.key === 'Enter' && this.controllerModel.isValid()) {
this.save();
}
}
public save(): void {
// show loading
this.loading = true;
// validate first
this.execute.execute(this.controllerModel.save())
.then((receiver: threema.Receiver) => {
this.goBack();
})
.catch((errorCode) => {
this.showError(errorCode);
});
}
public isSaving(): boolean {
return this.execute !== undefined
&& this.execute.isRunning();
}
public showError(errorCode): void {
this.$mdDialog.show(
this.$mdDialog.alert()
.clickOutsideToClose(true)
.title(this.controllerModel.subject)
.textContent(this.$translate.instant('validationError.editReceiver.' + errorCode))
.ok(this.$translate.instant('common.OK')));
}
public goBack(): void {
window.history.back();
}
}
/**
* Control creating a group or adding contact
* fields, validate and save routines are implemented in the specific ControllerModel
*/
class ReceiverCreateController {
private logTag: string = '[ReceiverEditController]';
public $mdDialog: any;
private loading = false;
private $timeout: ng.ITimeoutService;
private $log: ng.ILogService;
private $state: ng.ui.IStateService;
private $mdToast: any;
public identity = '';
private $translate: any;
public type: string;
private execute: ExecuteService;
public controllerModel: threema.ControllerModel;
public static $inject = ['$stateParams', '$mdDialog', '$mdToast', '$translate',
'$timeout', '$state', '$log', 'ControllerModelService'];
constructor($stateParams: threema.CreateReceiverStateParams, $mdDialog, $mdToast, $translate,
$timeout: ng.ITimeoutService, $state: ng.ui.IStateService, $log: ng.ILogService,
controllerModelService: ControllerModelService) {
this.$mdDialog = $mdDialog;
this.$timeout = $timeout;
this.$state = $state;
this.$log = $log;
this.$mdToast = $mdToast;
this.$translate = $translate;
this.type = $stateParams.type;
switch (this.type) {
case 'me':
$log.warn(this.logTag, 'Cannot create own contact');
$state.go('messenger.home');
return;
case 'contact':
this.controllerModel = controllerModelService.contact(null, ControllerModelMode.NEW);
if ($stateParams.initParams !== null) {
(this.controllerModel as ContactControllerModel)
.identity = $stateParams.initParams.identity;
}
break;
case 'group':
this.controllerModel = controllerModelService.group(null, ControllerModelMode.NEW);
break;
case 'distributionList':
this.controllerModel = controllerModelService.distributionList(null, ControllerModelMode.NEW);
break;
default:
this.$log.error('invalid type', this.type);
}
this.execute = new ExecuteService($log, $timeout, 1000);
}
public isSaving(): boolean {
return this.execute.isRunning();
}
public goBack(): void {
if (!this.isSaving()) {
window.history.back();
}
}
private showAddError(errorCode: string): void {
if (errorCode === undefined) {
errorCode = 'invalid_entry';
}
this.$mdDialog.show(
this.$mdDialog.alert()
.clickOutsideToClose(true)
.title(this.controllerModel.subject)
.textContent(this.$translate.instant('validationError.createReceiver.' + errorCode))
.ok(this.$translate.instant('common.OK')),
);
}
public keypress($event: KeyboardEvent): void {
if ($event.key === 'Enter' && this.controllerModel.isValid()) {
this.create();
}
}
public create(): void {
// show loading
this.loading = true;
// validate first
this.execute.execute(this.controllerModel.save())
.then((receiver: threema.Receiver) => {
this.$state.go('messenger.home.detail', receiver, {location: 'replace'});
})
.catch((errorCode) => {
this.showAddError(errorCode);
});
}
}
angular.module('3ema.messenger', ['ngMaterial'])
.config(['$stateProvider', function($stateProvider: ng.ui.IStateProvider) {
$stateProvider
.state('messenger', {
abstract: true,
templateUrl: 'partials/messenger.html',
controller: 'MessengerController',
controllerAs: 'ctrl',
})
.state('messenger.home', {
url: '/messenger',
views: {
navigation: {
templateUrl: 'partials/messenger.navigation.html',
controller: 'NavigationController',
controllerAs: 'ctrl',
},
content: {
// Required because navigation should not be changed,
template: '',
},
},
})
.state('messenger.home.conversation', {
url: '/conversation/{type}/{id}',
templateUrl: 'partials/messenger.conversation.html',
controller: 'ConversationController',
controllerAs: 'ctrl',
params: {initParams: null},
})
.state('messenger.home.detail', {
url: '/conversation/{type}/{id}/detail',
templateUrl: 'partials/messenger.receiver.html',
controller: 'ReceiverDetailController',
controllerAs: 'ctrl',
})
.state('messenger.home.edit', {
url: '/conversation/{type}/{id}/detail/edit',
templateUrl: 'partials/messenger.receiver.edit.html',
controller: 'ReceiverEditController',
controllerAs: 'ctrl',
})
.state('messenger.home.create', {
url: '/receiver/create/{type}',
templateUrl: 'partials/messenger.receiver.create.html',
controller: 'ReceiverCreateController',
controllerAs: 'ctrl',
params: {initParams: null},
})
;
}])
.controller('SendFileController', SendFileController)
.controller('MessengerController', MessengerController)
.controller('ConversationController', ConversationController)
.controller('NavigationController', NavigationController)
.controller('ReceiverDetailController', ReceiverDetailController)
.controller('ReceiverEditController', ReceiverEditController)
.controller('ReceiverCreateController', ReceiverCreateController)
;