messenger.ts 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208
  1. /**
  2. * This file is part of Threema Web.
  3. *
  4. * Threema Web is free software: you can redistribute it and/or modify it
  5. * under the terms of the GNU Affero General Public License as published by
  6. * the Free Software Foundation, either version 3 of the License, or (at
  7. * your option) any later version.
  8. *
  9. * This program is distributed in the hope that it will be useful, but
  10. * WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
  12. * General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU Affero General Public License
  15. * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  16. */
  17. import {ContactControllerModel} from '../controller_model/contact';
  18. import {supportsPassive, throttle} from '../helpers';
  19. import {ExecuteService} from '../services/execute';
  20. import {ControllerModelMode} from '../types/enums';
  21. abstract class DialogController {
  22. public static $inject = ['$mdDialog'];
  23. public $mdDialog: ng.material.IDialogService;
  24. public activeElement: HTMLElement | null;
  25. constructor($mdDialog: ng.material.IDialogService) {
  26. this.$mdDialog = $mdDialog;
  27. this.activeElement = document.activeElement as HTMLElement;
  28. }
  29. public cancel(): void {
  30. this.$mdDialog.cancel();
  31. this.done();
  32. }
  33. protected hide(data: any): void {
  34. this.$mdDialog.hide(data);
  35. this.done();
  36. }
  37. private done(): void {
  38. if (this.resumeFocusOnClose() === true && this.activeElement !== null) {
  39. // reset focus
  40. this.activeElement.focus();
  41. }
  42. }
  43. /**
  44. * If true, the focus on the active element (before opening the dialog)
  45. * will be restored.
  46. */
  47. protected abstract resumeFocusOnClose(): boolean;
  48. }
  49. /**
  50. * Handle sending of files.
  51. */
  52. class SendFileController extends DialogController {
  53. public caption: string;
  54. public sendAsFile: boolean = false;
  55. public send(): void {
  56. this.hide({
  57. caption: this.caption,
  58. sendAsFile: this.sendAsFile,
  59. });
  60. }
  61. public keypress($event: KeyboardEvent): void {
  62. if ($event.key === 'Enter') { // see https://developer.mozilla.org/de/docs/Web/API/KeyboardEvent/key/Key_Values
  63. this.send();
  64. }
  65. }
  66. protected resumeFocusOnClose(): boolean {
  67. return true;
  68. }
  69. }
  70. /**
  71. * Handle settings
  72. */
  73. class SettingsController {
  74. public static $inject = ['$mdDialog', '$window', 'SettingsService', 'NotificationService'];
  75. public $mdDialog: ng.material.IDialogService;
  76. public $window: ng.IWindowService;
  77. public settingsService: threema.SettingsService;
  78. private notificationService: threema.NotificationService;
  79. public activeElement: HTMLElement | null;
  80. private desktopNotifications: boolean;
  81. private notificationApiAvailable: boolean;
  82. private notificationPermission: boolean;
  83. private notificationPreview: boolean;
  84. constructor($mdDialog: ng.material.IDialogService,
  85. $window: ng.IWindowService,
  86. settingsService: threema.SettingsService,
  87. notificationService: threema.NotificationService) {
  88. this.$mdDialog = $mdDialog;
  89. this.$window = $window;
  90. this.settingsService = settingsService;
  91. this.notificationService = notificationService;
  92. this.activeElement = document.activeElement as HTMLElement;
  93. this.desktopNotifications = notificationService.getWantsNotifications();
  94. this.notificationApiAvailable = notificationService.isNotificationApiAvailable();
  95. this.notificationPermission = notificationService.getNotificationPermission();
  96. this.notificationPreview = notificationService.getWantsPreview();
  97. }
  98. public cancel(): void {
  99. this.$mdDialog.cancel();
  100. this.done();
  101. }
  102. protected hide(data: any): void {
  103. this.$mdDialog.hide(data);
  104. this.done();
  105. }
  106. private done(): void {
  107. if (this.activeElement !== null) {
  108. // Reset focus
  109. this.activeElement.focus();
  110. }
  111. }
  112. public setWantsNotifications(desktopNotifications: boolean) {
  113. this.notificationService.setWantsNotifications(desktopNotifications);
  114. }
  115. public setWantsPreview(notificationPreview: boolean) {
  116. this.notificationService.setWantsPreview(notificationPreview);
  117. }
  118. }
  119. class ConversationController {
  120. public name = 'navigation';
  121. // Angular services
  122. private $stateParams;
  123. private $timeout: ng.ITimeoutService;
  124. private $state: ng.ui.IStateService;
  125. private $log: ng.ILogService;
  126. private $scope: ng.IScope;
  127. // Own services
  128. private webClientService: threema.WebClientService;
  129. private receiverService: threema.ReceiverService;
  130. private stateService: threema.StateService;
  131. private mimeService: threema.MimeService;
  132. // Third party services
  133. private $mdDialog: ng.material.IDialogService;
  134. private $mdToast: ng.material.IToastService;
  135. // DOM Elements
  136. private domChatElement: HTMLElement;
  137. // Scrolling
  138. public showScrollJump: boolean = false;
  139. private stopTypingTimer: ng.IPromise<void> = null;
  140. public receiver: threema.Receiver;
  141. public type: threema.ReceiverType;
  142. public message: string = '';
  143. public lastReadMsgId: number = 0;
  144. public msgReadReportPending = false;
  145. private hasMore = true;
  146. private latestRefMsgId: number = null;
  147. private messages: threema.Message[];
  148. private draft: string;
  149. private $translate: ng.translate.ITranslateService;
  150. private locked = false;
  151. public maxTextLength: number;
  152. public isTyping = (): boolean => false;
  153. private uploading = {
  154. enabled: false,
  155. value1: 0,
  156. value2: 0,
  157. };
  158. public static $inject = [
  159. '$stateParams', '$state', '$timeout', '$log', '$scope', '$rootScope',
  160. '$mdDialog', '$mdToast', '$location', '$translate',
  161. 'WebClientService', 'StateService', 'ReceiverService', 'MimeService',
  162. ];
  163. constructor($stateParams,
  164. $state: ng.ui.IStateService,
  165. $timeout: ng.ITimeoutService,
  166. $log: ng.ILogService,
  167. $scope: ng.IScope,
  168. $rootScope: ng.IRootScopeService,
  169. $mdDialog: ng.material.IDialogService,
  170. $mdToast: ng.material.IToastService,
  171. $location,
  172. $translate: ng.translate.ITranslateService,
  173. webClientService: threema.WebClientService,
  174. stateService: threema.StateService,
  175. receiverService: threema.ReceiverService,
  176. mimeService: threema.MimeService) {
  177. this.$stateParams = $stateParams;
  178. this.$timeout = $timeout;
  179. this.$log = $log;
  180. this.webClientService = webClientService;
  181. this.receiverService = receiverService;
  182. this.stateService = stateService;
  183. this.mimeService = mimeService;
  184. this.$state = $state;
  185. this.$scope = $scope;
  186. this.$mdDialog = $mdDialog;
  187. this.$mdToast = $mdToast;
  188. this.$translate = $translate;
  189. // Close any showing dialogs
  190. this.$mdDialog.cancel();
  191. this.maxTextLength = this.webClientService.getMaxTextLength();
  192. // On every navigation event, close all dialogs.
  193. // Note: Deprecated. When migrating ui-router ($state),
  194. // replace with transition hooks.
  195. $rootScope.$on('$stateChangeStart', () => this.$mdDialog.cancel());
  196. // Redirect to welcome if necessary
  197. if (stateService.state === 'error') {
  198. $log.debug('ConversationController: WebClient not yet running, redirecting to welcome screen');
  199. $state.go('welcome');
  200. return;
  201. }
  202. if (!this.locked) {
  203. // Get DOM references
  204. this.domChatElement = document.querySelector('#conversation-chat') as HTMLElement;
  205. // Add custom event handlers
  206. this.domChatElement.addEventListener('scroll', throttle(() => {
  207. $rootScope.$apply(() => {
  208. this.updateScrollJump();
  209. });
  210. }, 100, this), supportsPassive() ? {passive: true} as any : false);
  211. }
  212. // Set receiver and type
  213. try {
  214. this.receiver = webClientService.receivers.getData($stateParams);
  215. this.type = $stateParams.type;
  216. if (this.receiver.type === undefined) {
  217. this.receiver.type = this.type;
  218. }
  219. // initial set locked state
  220. this.locked = this.receiver.locked;
  221. this.receiverService.setActive(this.receiver);
  222. if (!this.receiver.locked) {
  223. let latestHeight = 0;
  224. // update unread count
  225. this.webClientService.messages.updateFirstUnreadMessage(this.receiver);
  226. this.messages = this.webClientService.messages.register(
  227. this.receiver,
  228. this.$scope,
  229. (e, allMessages: threema.Message[], hasMore: boolean) => {
  230. this.messages = allMessages;
  231. this.hasMore = hasMore;
  232. if (this.latestRefMsgId !== null) {
  233. // scroll to div..
  234. this.domChatElement.scrollTop =
  235. this.domChatElement.scrollHeight - latestHeight;
  236. this.latestRefMsgId = null;
  237. }
  238. latestHeight = this.domChatElement.scrollHeight;
  239. },
  240. );
  241. this.draft = webClientService.getDraft(this.receiver);
  242. if (this.receiver.type === 'contact') {
  243. this.isTyping = () => this.webClientService.isTyping(this.receiver as threema.ContactReceiver);
  244. }
  245. }
  246. } catch (error) {
  247. $log.error('Could not set receiver and type');
  248. $log.debug(error.stack);
  249. $state.go('messenger.home');
  250. }
  251. // reload controller if locked state was changed
  252. $scope.$watch(() => {
  253. return this.receiver.locked;
  254. }, () => {
  255. if (this.locked !== this.receiver.locked) {
  256. $state.reload();
  257. }
  258. });
  259. }
  260. public isEnabled(): boolean {
  261. return this.type !== 'group'
  262. || !(this.receiver as threema.GroupReceiver).disabled;
  263. }
  264. public isQuoting(): boolean {
  265. return this.getQuote() !== undefined;
  266. }
  267. public getQuote(): threema.Quote {
  268. return this.webClientService.getQuote(this.receiver);
  269. }
  270. public cancelQuoting(): void {
  271. // clear curren quote
  272. this.webClientService.setQuote(this.receiver);
  273. }
  274. public showError(errorMessage: string, toastLength = 4000) {
  275. if (errorMessage === undefined || errorMessage.length === 0) {
  276. errorMessage = this.$translate.instant('error.ERROR_OCCURRED');
  277. }
  278. this.$mdToast.show(
  279. this.$mdToast.simple()
  280. .textContent(errorMessage)
  281. .position('bottom center'));
  282. }
  283. /**
  284. * Submit function for input field. Can contain text or file data.
  285. * Return whether sending was successful.
  286. */
  287. public submit = (type: threema.MessageContentType, contents: threema.MessageData[]): Promise<any> => {
  288. // Validate whether a connection is available
  289. return new Promise((resolve, reject) => {
  290. if (this.stateService.state !== 'ok') {
  291. // Invalid connection, show toast and abort
  292. this.showError(this.$translate.instant('error.NO_CONNECTION'));
  293. return reject();
  294. }
  295. let success = true;
  296. let nextCallback = (index: number) => {
  297. if (index === contents.length - 1) {
  298. if (success) {
  299. resolve();
  300. } else {
  301. reject();
  302. }
  303. }
  304. };
  305. switch (type) {
  306. case 'file':
  307. // Determine file type
  308. let showSendAsFileCheckbox = false;
  309. for (let msg of contents) {
  310. const mime = (msg as threema.FileMessageData).fileType;
  311. if (this.mimeService.isImage(mime)
  312. || this.mimeService.isAudio(mime)
  313. || this.mimeService.isVideo(mime)) {
  314. showSendAsFileCheckbox = true;
  315. break;
  316. }
  317. }
  318. // Eager translations
  319. const title = this.$translate.instant('messenger.CONFIRM_FILE_SEND', {
  320. senderName: this.receiver.displayName,
  321. });
  322. const placeholder = this.$translate.instant('messenger.CONFIRM_FILE_CAPTION');
  323. const confirmSendAsFile = this.$translate.instant('messenger.CONFIRM_SEND_AS_FILE');
  324. // Show confirmation dialog
  325. this.$mdDialog.show({
  326. clickOutsideToClose: false,
  327. controller: 'SendFileController',
  328. controllerAs: 'ctrl',
  329. // tslint:disable:max-line-length
  330. template: `
  331. <md-dialog class="send-file-dialog">
  332. <md-dialog-content class="md-dialog-content">
  333. <h2 class="md-title">${title}</h2>
  334. <md-input-container md-no-float class="input-caption md-prompt-input-container">
  335. <input md-autofocus ng-keypress="ctrl.keypress($event)" ng-model="ctrl.caption" placeholder="${placeholder}" aria-label="${placeholder}">
  336. </md-input-container>
  337. <md-input-container md-no-float class="input-send-as-file md-prompt-input-container" ng-show="${showSendAsFileCheckbox}">
  338. <md-checkbox ng-model="ctrl.sendAsFile" aria-label="${confirmSendAsFile}">
  339. ${confirmSendAsFile}
  340. </md-checkbox>
  341. </md-input-container>
  342. </md-dialog-content>
  343. <md-dialog-actions>
  344. <button class="md-primary md-cancel-button md-button" md-ink-ripple type="button" ng-click="ctrl.cancel()">
  345. <span translate>common.CANCEL</span>
  346. </button>
  347. <button class="md-primary md-cancel-button md-button" md-ink-ripple type="button" ng-click="ctrl.send()">
  348. <span translate>common.SEND</span>
  349. </button>
  350. </md-dialog-actions>
  351. </md-dialog>
  352. `,
  353. // tslint:enable:max-line-length
  354. }).then((data) => {
  355. const caption = data.caption;
  356. const sendAsFile = data.sendAsFile;
  357. contents.forEach((msg: threema.FileMessageData, index: number) => {
  358. if (caption !== undefined && caption.length > 0) {
  359. msg.caption = caption;
  360. }
  361. msg.sendAsFile = sendAsFile;
  362. this.webClientService.sendMessage(this.$stateParams, type, msg)
  363. .then(() => {
  364. nextCallback(index);
  365. })
  366. .catch((error) => {
  367. this.$log.error(error);
  368. this.showError(error);
  369. success = false;
  370. nextCallback(index);
  371. });
  372. });
  373. }, angular.noop);
  374. break;
  375. case 'text':
  376. // do not show confirmation, send directly
  377. contents.forEach((msg: threema.MessageData, index: number) => {
  378. msg.quote = this.webClientService.getQuote(this.receiver);
  379. // remove quote
  380. this.webClientService.setQuote(this.receiver);
  381. // send message
  382. this.webClientService.sendMessage(this.$stateParams, type, msg)
  383. .then(() => {
  384. nextCallback(index);
  385. })
  386. .catch((error) => {
  387. this.$log.error(error);
  388. this.showError(error);
  389. success = false;
  390. nextCallback(index);
  391. });
  392. });
  393. return;
  394. default:
  395. this.$log.warn('Invalid message type:', type);
  396. reject();
  397. }
  398. });
  399. }
  400. /**
  401. * Something was typed.
  402. *
  403. * In contrast to startTyping, this method is is always called, not just if
  404. * the text field is non-empty.
  405. */
  406. public onTyping = (text: string) => {
  407. // Update draft
  408. this.webClientService.setDraft(this.receiver, text);
  409. }
  410. public onUploading = (inProgress: boolean, percentCurrent: number = null, percentFull: number = null) => {
  411. this.uploading.enabled = inProgress;
  412. this.uploading.value1 = Number(percentCurrent);
  413. this.uploading.value2 = Number(percentCurrent);
  414. }
  415. /**
  416. * We started typing.
  417. */
  418. public startTyping = (text: string) => {
  419. if (this.stopTypingTimer === null) {
  420. // Notify app
  421. this.webClientService.sendMeIsTyping(this.$stateParams, true);
  422. } else {
  423. // Stop existing timer
  424. this.$timeout.cancel(this.stopTypingTimer);
  425. }
  426. // Define a timeout to send the stopTyping event
  427. this.stopTypingTimer = this.$timeout(() => {
  428. this.stopTyping();
  429. }, 10000);
  430. }
  431. /**
  432. * We stopped typing.
  433. */
  434. public stopTyping = () => {
  435. // Cancel timer if present
  436. if (this.stopTypingTimer !== null) {
  437. this.$timeout.cancel(this.stopTypingTimer);
  438. this.stopTypingTimer = null;
  439. }
  440. // Notify app
  441. this.webClientService.sendMeIsTyping(this.$stateParams, false);
  442. }
  443. /**
  444. * User scrolled to the top of the chat.
  445. */
  446. public topOfChat(): void {
  447. this.requestMessages();
  448. }
  449. public requestMessages(): void {
  450. let refMsgId = this.webClientService.requestMessages(this.$stateParams);
  451. if (refMsgId !== null
  452. && refMsgId !== undefined) {
  453. // new message are requested, scroll to refMsgId
  454. this.latestRefMsgId = refMsgId;
  455. } else {
  456. this.latestRefMsgId = null;
  457. }
  458. }
  459. public showReceiver(ev): void {
  460. this.$state.go('messenger.home.detail', this.receiver);
  461. }
  462. public hasMoreMessages(): boolean {
  463. return this.hasMore;
  464. }
  465. /**
  466. * A message has been seen. Report it to the app, with a small delay to
  467. * avoid sending too many messages at once.
  468. */
  469. public msgRead(msgId: number): void {
  470. if (msgId > this.lastReadMsgId) {
  471. this.lastReadMsgId = msgId;
  472. }
  473. if (!this.msgReadReportPending) {
  474. this.msgReadReportPending = true;
  475. const receiver = angular.copy(this.receiver);
  476. receiver.type = this.type;
  477. this.$timeout(() => {
  478. this.webClientService.requestRead(receiver, this.lastReadMsgId);
  479. this.msgReadReportPending = false;
  480. }, 500);
  481. }
  482. }
  483. public goBack(): void {
  484. this.receiverService.setActive(undefined);
  485. // redirect to messenger home
  486. this.$state.go('messenger.home');
  487. }
  488. /**
  489. * Scroll to bottom of chat.
  490. */
  491. public scrollDown(): void {
  492. this.domChatElement.scrollTop = this.domChatElement.scrollHeight;
  493. }
  494. /**
  495. * Only show the scroll to bottom button if user scrolled more than 10px
  496. * away from bottom.
  497. */
  498. private updateScrollJump(): void {
  499. const chat = this.domChatElement;
  500. this.showScrollJump = chat.scrollHeight - (chat.scrollTop + chat.offsetHeight) > 10;
  501. }
  502. }
  503. class NavigationController {
  504. public name = 'navigation';
  505. private webClientService: threema.WebClientService;
  506. private receiverService: threema.ReceiverService;
  507. private stateService: threema.StateService;
  508. private trustedKeyStoreService: threema.TrustedKeyStoreService;
  509. private activeTab: 'contacts' | 'conversations' = 'conversations';
  510. private searchVisible = false;
  511. private searchText: string = '';
  512. private $mdDialog;
  513. private $translate: ng.translate.ITranslateService;
  514. private $state: ng.ui.IStateService;
  515. public static $inject = [
  516. '$log', '$state', '$mdDialog', '$translate',
  517. 'WebClientService', 'StateService', 'ReceiverService', 'TrustedKeyStore',
  518. ];
  519. constructor($log: ng.ILogService, $state: ng.ui.IStateService,
  520. $mdDialog: ng.material.IDialogService, $translate: ng.translate.ITranslateService,
  521. webClientService: threema.WebClientService, stateService: threema.StateService,
  522. receiverService: threema.ReceiverService,
  523. trustedKeyStoreService: threema.TrustedKeyStoreService) {
  524. // Redirect to welcome if necessary
  525. if (stateService.state === 'error') {
  526. $log.debug('NavigationController: WebClient not yet running, redirecting to welcome screen');
  527. $state.go('welcome');
  528. return;
  529. }
  530. this.webClientService = webClientService;
  531. this.receiverService = receiverService;
  532. this.stateService = stateService;
  533. this.trustedKeyStoreService = trustedKeyStoreService;
  534. this.$mdDialog = $mdDialog;
  535. this.$translate = $translate;
  536. this.$state = $state;
  537. }
  538. public contacts(): threema.ContactReceiver[] {
  539. return Array.from(this.webClientService.contacts.values()) as threema.ContactReceiver[];
  540. }
  541. /**
  542. * Search for `needle` in the `haystack`. The search is case insensitive.
  543. */
  544. private matches(haystack: string, needle: string): boolean {
  545. return haystack.toLowerCase().replace('\n', ' ').indexOf(needle.trim().toLowerCase()) !== -1;
  546. }
  547. /**
  548. * Predicate function used for conversation filtering.
  549. *
  550. * Match by contact name *or* id *or* last message text.
  551. */
  552. private searchConversation = (value: threema.Conversation, index, array): boolean => {
  553. return this.searchText === ''
  554. || this.matches(value.receiver.displayName, this.searchText)
  555. || (value.latestMessage && value.latestMessage.body
  556. && this.matches(value.latestMessage.body, this.searchText))
  557. || (value.receiver.id.length === 8 && this.matches(value.receiver.id, this.searchText));
  558. }
  559. /**
  560. * Predicate function used for contact filtering.
  561. *
  562. * Match by contact name *or* id.
  563. */
  564. private searchContact = (value, index, array): boolean => {
  565. return this.searchText === ''
  566. || value.displayName.toLowerCase().indexOf(this.searchText.toLowerCase()) !== -1
  567. || value.id.toLowerCase().indexOf(this.searchText.toLowerCase()) !== -1;
  568. }
  569. public isVisible(conversation: threema.Conversation) {
  570. return conversation.receiver.visible;
  571. }
  572. public conversations(): threema.Conversation[] {
  573. return this.webClientService.conversations.get();
  574. }
  575. public isActive(value: threema.Conversation): boolean {
  576. return this.receiverService.isConversationActive(value);
  577. }
  578. /**
  579. * Show dialog.
  580. */
  581. public showDialog(name, ev) {
  582. this.$mdDialog.show({
  583. controller: DialogController,
  584. controllerAs: 'ctrl',
  585. templateUrl: 'partials/dialog.' + name + '.html',
  586. parent: angular.element(document.body),
  587. targetEvent: ev,
  588. clickOutsideToClose: true,
  589. fullscreen: true,
  590. });
  591. }
  592. /**
  593. * Show about dialog.
  594. */
  595. public about(ev): void {
  596. this.showDialog('about', ev);
  597. }
  598. /**
  599. * Show settings dialog.
  600. */
  601. public settings(ev): void {
  602. this.$mdDialog.show({
  603. controller: SettingsController,
  604. controllerAs: 'ctrl',
  605. templateUrl: 'partials/dialog.settings.html',
  606. parent: angular.element(document.body),
  607. targetEvent: ev,
  608. clickOutsideToClose: true,
  609. fullscreen: true,
  610. });
  611. }
  612. /**
  613. * Return whether a trusted key is available.
  614. */
  615. public isPersistent(): boolean {
  616. return this.trustedKeyStoreService.hasTrustedKey();
  617. }
  618. /**
  619. * Close the session.
  620. */
  621. public closeSession(ev): void {
  622. const confirm = this.$mdDialog.confirm()
  623. .title(this.$translate.instant('common.SESSION_CLOSE'))
  624. .textContent(this.$translate.instant('common.CONFIRM_CLOSE_BODY'))
  625. .targetEvent(ev)
  626. .ok(this.$translate.instant('common.YES'))
  627. .cancel(this.$translate.instant('common.CANCEL'));
  628. this.$mdDialog.show(confirm).then(() => {
  629. const deleteStoredData = false;
  630. const resetPush = true;
  631. const redirect = true;
  632. this.webClientService.stop(true, deleteStoredData, resetPush, redirect);
  633. }, () => {
  634. // do nothing
  635. });
  636. }
  637. /**
  638. * Close and delete the session.
  639. */
  640. public deleteSession(ev): void {
  641. const confirm = this.$mdDialog.confirm()
  642. .title(this.$translate.instant('common.SESSION_DELETE'))
  643. .textContent(this.$translate.instant('common.CONFIRM_DELETE_CLOSE_BODY'))
  644. .targetEvent(ev)
  645. .ok(this.$translate.instant('common.YES'))
  646. .cancel(this.$translate.instant('common.CANCEL'));
  647. this.$mdDialog.show(confirm).then(() => {
  648. const deleteStoredData = true;
  649. const resetPush = true;
  650. const redirect = true;
  651. this.webClientService.stop(true, deleteStoredData, resetPush, redirect);
  652. }, () => {
  653. // do nothing
  654. });
  655. }
  656. public addContact(ev): void {
  657. this.$state.go('messenger.home.create', {
  658. type: 'contact',
  659. });
  660. }
  661. public createGroup(ev): void {
  662. this.$state.go('messenger.home.create', {
  663. type: 'group',
  664. });
  665. }
  666. public createDistributionList(ev): void {
  667. this.$state.go('messenger.home.create', {
  668. type: 'distributionList',
  669. });
  670. }
  671. /**
  672. * Toggle search bar.
  673. */
  674. public toggleSearch(): void {
  675. this.searchVisible = !this.searchVisible;
  676. }
  677. public getMyIdentity(): threema.Identity {
  678. return this.webClientService.getMyIdentity();
  679. }
  680. public showMyIdentity(): boolean {
  681. return this.getMyIdentity() !== undefined;
  682. }
  683. }
  684. class MessengerController {
  685. public name = 'messenger';
  686. private receiverService: threema.ReceiverService;
  687. private $state;
  688. private webClientService: threema.WebClientService;
  689. public static $inject = [
  690. '$scope', '$state', '$log', '$mdDialog', '$translate',
  691. 'StateService', 'ReceiverService', 'WebClientService', 'ControllerService',
  692. ];
  693. constructor($scope, $state, $log: ng.ILogService, $mdDialog: ng.material.IDialogService,
  694. $translate: ng.translate.ITranslateService,
  695. stateService: threema.StateService, receiverService: threema.ReceiverService,
  696. webClientService: threema.WebClientService, controllerService: threema.ControllerService) {
  697. // Redirect to welcome if necessary
  698. if (stateService.state === 'error') {
  699. $log.debug('MessengerController: WebClient not yet running, redirecting to welcome screen');
  700. $state.go('welcome');
  701. return;
  702. }
  703. controllerService.setControllerName('messenger');
  704. this.receiverService = receiverService;
  705. this.$state = $state;
  706. this.webClientService = webClientService;
  707. // watch for alerts
  708. $scope.$watch(() => webClientService.alerts, (alerts: threema.Alert[]) => {
  709. if (alerts.length > 0) {
  710. angular.forEach(alerts, (alert: threema.Alert) => {
  711. $mdDialog.show(
  712. $mdDialog.alert()
  713. .clickOutsideToClose(true)
  714. .title(alert.type)
  715. .textContent(alert.message)
  716. .ok($translate.instant('common.OK')));
  717. });
  718. // clean array
  719. webClientService.alerts = [];
  720. }
  721. }, true);
  722. this.webClientService.setReceiverListener({
  723. onRemoved(receiver: threema.Receiver) {
  724. switch ($state.current.name) {
  725. case 'messenger.home.conversation':
  726. case 'messenger.home.detail':
  727. case 'messenger.home.edit':
  728. if ($state.params !== undefined
  729. && $state.params.type !== undefined
  730. && $state.params.id !== undefined) {
  731. if ($state.params.type === receiver.type
  732. && $state.params.id === receiver.id) {
  733. // conversation or sub form is open, redirect to home!
  734. this.$state.go('messenger.home', null, {location: 'replace'});
  735. }
  736. }
  737. break;
  738. default:
  739. $log.warn('Ignored onRemoved event for state', $state.current.name);
  740. }
  741. },
  742. });
  743. }
  744. public showDetail(): boolean {
  745. return !this.$state.is('messenger.home');
  746. }
  747. }
  748. class ReceiverDetailController {
  749. public $mdDialog: any;
  750. public $state: ng.ui.IStateService;
  751. public receiver: threema.Receiver;
  752. public title: string;
  753. public fingerPrint?: string;
  754. private fingerPrintService: threema.FingerPrintService;
  755. private contactService: threema.ContactService;
  756. private showGroups = false;
  757. private showDistributionLists = false;
  758. private inGroups: threema.GroupReceiver[] = [];
  759. private inDistributionLists: threema.DistributionListReceiver[] = [];
  760. private hasSystemEmails = false;
  761. private hasSystemPhones = false;
  762. private controllerModel: threema.ControllerModel;
  763. public static $inject = [
  764. '$log', '$stateParams', '$state', '$mdDialog',
  765. 'WebClientService', 'FingerPrintService', 'ContactService', 'ControllerModelService',
  766. ];
  767. constructor($log: ng.ILogService, $stateParams, $state: ng.ui.IStateService, $mdDialog: ng.material.IDialogService,
  768. webClientService: threema.WebClientService, fingerPrintService: threema.FingerPrintService,
  769. contactService: threema.ContactService, controllerModelService: threema.ControllerModelService) {
  770. this.$mdDialog = $mdDialog;
  771. this.$state = $state;
  772. this.fingerPrintService = fingerPrintService;
  773. this.contactService = contactService;
  774. this.receiver = webClientService.receivers.getData($stateParams);
  775. // append members
  776. if (this.receiver.type === 'contact') {
  777. let contactReceiver = (<threema.ContactReceiver> this.receiver);
  778. this.contactService.requiredDetails(contactReceiver)
  779. .then(() => {
  780. this.hasSystemEmails = contactReceiver.systemContact.emails.length > 0;
  781. this.hasSystemPhones = contactReceiver.systemContact.phoneNumbers.length > 0;
  782. })
  783. .catch(() => {
  784. // do nothing
  785. });
  786. this.fingerPrint = this.fingerPrintService.generate(contactReceiver.publicKey);
  787. webClientService.groups.forEach((groupReceiver: threema.GroupReceiver) => {
  788. // check if my identity is a member
  789. if (groupReceiver.members.indexOf(contactReceiver.id) !== -1) {
  790. this.inGroups.push(groupReceiver);
  791. this.showGroups = true;
  792. }
  793. });
  794. webClientService.distributionLists.forEach(
  795. (distributionListReceiver: threema.DistributionListReceiver) => {
  796. // check if my identity is a member
  797. if (distributionListReceiver.members.indexOf(contactReceiver.id) !== -1) {
  798. this.inDistributionLists.push(distributionListReceiver);
  799. this.showDistributionLists = true;
  800. }
  801. },
  802. );
  803. }
  804. switch (this.receiver.type) {
  805. case 'contact':
  806. this.controllerModel = controllerModelService
  807. .contact(this.receiver as threema.ContactReceiver, ControllerModelMode.VIEW);
  808. break;
  809. case 'group':
  810. this.controllerModel = controllerModelService
  811. .group(this.receiver as threema.GroupReceiver, ControllerModelMode.VIEW);
  812. break;
  813. case 'distributionList':
  814. this.controllerModel = controllerModelService
  815. .distributionList(this.receiver as threema.DistributionListReceiver, ControllerModelMode.VIEW);
  816. break;
  817. default:
  818. $log.warn('Invalid receiver type:', this.receiver.type);
  819. }
  820. // if this receiver was removed, navigation to "home" view
  821. this.controllerModel.setOnRemoved((receiverId: string) => {
  822. // go "home"
  823. this.$state.go('messenger.home', null, {location: 'replace'});
  824. });
  825. }
  826. public chat(): void {
  827. this.$state.go('messenger.home.conversation', this.receiver);
  828. }
  829. public edit(): void {
  830. if (!this.controllerModel.canEdit()) {
  831. return;
  832. }
  833. this.$state.go('messenger.home.edit', this.receiver);
  834. }
  835. public goBack(): void {
  836. window.history.back();
  837. }
  838. }
  839. /**
  840. * Control edit a group or a contact
  841. * fields, validate and save routines are implemented in the specific ControllerModel
  842. */
  843. class ReceiverEditController {
  844. public $mdDialog: any;
  845. public $state: ng.ui.IStateService;
  846. private $translate: ng.translate.ITranslateService;
  847. public title: string;
  848. private $timeout: ng.ITimeoutService;
  849. private execute: ExecuteService;
  850. public loading = false;
  851. private controllerModel: threema.ControllerModel;
  852. public type: string;
  853. public static $inject = [
  854. '$log', '$stateParams', '$state', '$mdDialog',
  855. '$timeout', '$translate', 'WebClientService', 'ControllerModelService',
  856. ];
  857. constructor($log: ng.ILogService, $stateParams, $state: ng.ui.IStateService,
  858. $mdDialog, $timeout: ng.ITimeoutService, $translate: ng.translate.ITranslateService,
  859. webClientService: threema.WebClientService, controllerModelService: threema.ControllerModelService) {
  860. this.$mdDialog = $mdDialog;
  861. this.$state = $state;
  862. this.$timeout = $timeout;
  863. this.$translate = $translate;
  864. const receiver = webClientService.receivers.getData($stateParams);
  865. switch (receiver.type) {
  866. case 'contact':
  867. this.controllerModel = controllerModelService.contact(
  868. receiver as threema.ContactReceiver,
  869. ControllerModelMode.EDIT,
  870. );
  871. break;
  872. case 'group':
  873. this.controllerModel = controllerModelService.group(
  874. receiver as threema.GroupReceiver,
  875. ControllerModelMode.EDIT,
  876. );
  877. break;
  878. case 'distributionList':
  879. this.controllerModel = controllerModelService.distributionList(
  880. receiver as threema.DistributionListReceiver,
  881. ControllerModelMode.EDIT,
  882. );
  883. break;
  884. default:
  885. $log.warn('Invalid receiver type:', receiver.type);
  886. }
  887. this.execute = new ExecuteService($log, $timeout, 1000);
  888. this.type = receiver.type;
  889. }
  890. public save(): void {
  891. // show loading
  892. this.loading = true;
  893. // validate first
  894. this.execute.execute(this.controllerModel.save())
  895. .then((receiver: threema.Receiver) => {
  896. this.goBack();
  897. })
  898. .catch((errorCode) => {
  899. this.showError(errorCode);
  900. });
  901. }
  902. public isSaving(): boolean {
  903. return this.execute.isRunning();
  904. }
  905. public showError(errorCode): void {
  906. this.$mdDialog.show(
  907. this.$mdDialog.alert()
  908. .clickOutsideToClose(true)
  909. .title(this.controllerModel.subject)
  910. .textContent(this.$translate.instant('validationError.editReceiver.' + errorCode))
  911. .ok(this.$translate.instant('common.OK')));
  912. }
  913. public goBack(): void {
  914. window.history.back();
  915. }
  916. }
  917. /**
  918. * Control creating a group or adding contact
  919. * fields, validate and save routines are implemented in the specific ControllerModel
  920. */
  921. class ReceiverCreateController {
  922. public static $inject = ['$stateParams', '$mdDialog', '$mdToast', '$translate',
  923. '$timeout', '$state', '$log', 'ControllerModelService'];
  924. public $mdDialog: any;
  925. private loading = false;
  926. private $timeout: ng.ITimeoutService;
  927. private $log: ng.ILogService;
  928. private $state: ng.ui.IStateService;
  929. private $mdToast: any;
  930. public identity = '';
  931. private $translate: any;
  932. public type: string;
  933. private execute: ExecuteService;
  934. public controllerModel: threema.ControllerModel;
  935. constructor($stateParams: threema.CreateReceiverStateParams, $mdDialog, $mdToast, $translate,
  936. $timeout: ng.ITimeoutService, $state: ng.ui.IStateService, $log: ng.ILogService,
  937. controllerModelService: threema.ControllerModelService) {
  938. this.$mdDialog = $mdDialog;
  939. this.$timeout = $timeout;
  940. this.$state = $state;
  941. this.$log = $log;
  942. this.$mdToast = $mdToast;
  943. this.$translate = $translate;
  944. this.type = $stateParams.type;
  945. switch (this.type) {
  946. case 'contact':
  947. this.controllerModel = controllerModelService.contact(null, ControllerModelMode.NEW);
  948. if ($stateParams.initParams !== null) {
  949. (this.controllerModel as ContactControllerModel)
  950. .identity = $stateParams.initParams.identity;
  951. }
  952. break;
  953. case 'group':
  954. this.controllerModel = controllerModelService.group(null, ControllerModelMode.NEW);
  955. break;
  956. case 'distributionList':
  957. this.controllerModel = controllerModelService.distributionList(null, ControllerModelMode.NEW);
  958. break;
  959. default:
  960. this.$log.error('invalid type', this.type);
  961. }
  962. this.execute = new ExecuteService($log, $timeout, 1000);
  963. }
  964. public isSaving(): boolean {
  965. return this.execute.isRunning();
  966. }
  967. public goBack(): void {
  968. if (!this.isSaving()) {
  969. window.history.back();
  970. }
  971. }
  972. private showAddError(errorCode: String): void {
  973. if (errorCode === undefined) {
  974. errorCode = 'invalid_entry';
  975. }
  976. this.$mdDialog.show(
  977. this.$mdDialog.alert()
  978. .clickOutsideToClose(true)
  979. .title(this.controllerModel.subject)
  980. .textContent(this.$translate.instant('validationError.createReceiver.' + errorCode))
  981. .ok(this.$translate.instant('common.OK')),
  982. );
  983. }
  984. public create(): void {
  985. // show loading
  986. this.loading = true;
  987. // validate first
  988. this.execute.execute(this.controllerModel.save())
  989. .then((receiver: threema.Receiver) => {
  990. this.$state.go('messenger.home.detail', receiver, {location: 'replace'});
  991. })
  992. .catch((errorCode) => {
  993. this.showAddError(errorCode);
  994. });
  995. }
  996. }
  997. angular.module('3ema.messenger', ['ngMaterial'])
  998. .config(['$stateProvider', function($stateProvider: ng.ui.IStateProvider) {
  999. $stateProvider
  1000. .state('messenger', {
  1001. abstract: true,
  1002. templateUrl: 'partials/messenger.html',
  1003. controller: 'MessengerController',
  1004. controllerAs: 'ctrl',
  1005. })
  1006. .state('messenger.home', {
  1007. url: '/messenger',
  1008. views: {
  1009. navigation: {
  1010. templateUrl: 'partials/messenger.navigation.html',
  1011. controller: 'NavigationController',
  1012. controllerAs: 'ctrl',
  1013. },
  1014. content: {
  1015. // Required because navigation should not be changed,
  1016. template: '<div ui-view></div>',
  1017. },
  1018. },
  1019. })
  1020. .state('messenger.home.conversation', {
  1021. url: '/conversation/{type}/{id}',
  1022. templateUrl: 'partials/messenger.conversation.html',
  1023. controller: 'ConversationController',
  1024. controllerAs: 'ctrl',
  1025. })
  1026. .state('messenger.home.detail', {
  1027. url: '/conversation/{type}/{id}/detail',
  1028. templateUrl: 'partials/messenger.receiver.html',
  1029. controller: 'ReceiverDetailController',
  1030. controllerAs: 'ctrl',
  1031. })
  1032. .state('messenger.home.edit', {
  1033. url: '/conversation/{type}/{id}/detail/edit',
  1034. templateUrl: 'partials/messenger.receiver.edit.html',
  1035. controller: 'ReceiverEditController',
  1036. controllerAs: 'ctrl',
  1037. })
  1038. .state('messenger.home.create', {
  1039. url: '/receiver/create/{type}',
  1040. templateUrl: 'partials/messenger.receiver.create.html',
  1041. controller: 'ReceiverCreateController',
  1042. controllerAs: 'ctrl',
  1043. params: {initParams: null},
  1044. })
  1045. ;
  1046. }])
  1047. .controller('SendFileController', SendFileController)
  1048. .controller('MessengerController', MessengerController)
  1049. .controller('ConversationController', ConversationController)
  1050. .controller('NavigationController', NavigationController)
  1051. .controller('ReceiverDetailController', ReceiverDetailController)
  1052. .controller('ReceiverEditController', ReceiverEditController)
  1053. .controller('ReceiverCreateController', ReceiverCreateController)
  1054. ;