messenger.ts 40 KB

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