messenger.ts 53 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466
  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 {ContactService} from '../services/contact';
  20. import {ControllerService} from '../services/controller';
  21. import {ControllerModelService} from '../services/controller_model';
  22. import {ExecuteService} from '../services/execute';
  23. import {FingerPrintService} from '../services/fingerprint';
  24. import {TrustedKeyStoreService} from '../services/keystore';
  25. import {MimeService} from '../services/mime';
  26. import {NotificationService} from '../services/notification';
  27. import {ReceiverService} from '../services/receiver';
  28. import {SettingsService} from '../services/settings';
  29. import {StateService} from '../services/state';
  30. import {VersionService} from '../services/version';
  31. import {WebClientService} from '../services/webclient';
  32. import {ControllerModelMode} from '../types/enums';
  33. class DialogController {
  34. public static $inject = ['$mdDialog'];
  35. public $mdDialog: ng.material.IDialogService;
  36. public activeElement: HTMLElement | null;
  37. constructor($mdDialog: ng.material.IDialogService) {
  38. this.$mdDialog = $mdDialog;
  39. this.activeElement = document.activeElement as HTMLElement;
  40. }
  41. public cancel(): void {
  42. this.$mdDialog.cancel();
  43. this.done();
  44. }
  45. protected hide(data: any): void {
  46. this.$mdDialog.hide(data);
  47. this.done();
  48. }
  49. private done(): void {
  50. if (this.resumeFocusOnClose() === true && this.activeElement !== null) {
  51. // reset focus
  52. this.activeElement.focus();
  53. }
  54. }
  55. /**
  56. * If true, the focus on the active element (before opening the dialog)
  57. * will be restored. Default `true`, override if desired.
  58. */
  59. protected resumeFocusOnClose(): boolean {
  60. return true;
  61. }
  62. }
  63. /**
  64. * Handle sending of files.
  65. */
  66. class SendFileController extends DialogController {
  67. public static $inject = ['$mdDialog', 'preview'];
  68. public caption: string;
  69. public sendAsFile: boolean = false;
  70. public preview: threema.FileMessageData | null = null;
  71. constructor($mdDialog: ng.material.IDialogService, preview: threema.FileMessageData) {
  72. super($mdDialog);
  73. this.preview = preview;
  74. }
  75. public send(): void {
  76. this.hide({
  77. caption: this.caption,
  78. sendAsFile: this.sendAsFile,
  79. });
  80. }
  81. public keypress($event: KeyboardEvent): void {
  82. if ($event.key === 'Enter') { // see https://developer.mozilla.org/de/docs/Web/API/KeyboardEvent/key/Key_Values
  83. this.send();
  84. }
  85. }
  86. public hasPreview(): boolean {
  87. return this.preview !== null && this.preview !== undefined;
  88. }
  89. }
  90. /**
  91. * Handle settings
  92. */
  93. class SettingsController {
  94. public static $inject = ['$mdDialog', '$window', 'SettingsService', 'NotificationService'];
  95. public $mdDialog: ng.material.IDialogService;
  96. public $window: ng.IWindowService;
  97. public settingsService: SettingsService;
  98. private notificationService: NotificationService;
  99. public activeElement: HTMLElement | null;
  100. private desktopNotifications: boolean;
  101. private notificationApiAvailable: boolean;
  102. private notificationPermission: boolean;
  103. private notificationPreview: boolean;
  104. private notificationSound: boolean;
  105. constructor($mdDialog: ng.material.IDialogService,
  106. $window: ng.IWindowService,
  107. settingsService: SettingsService,
  108. notificationService: NotificationService) {
  109. this.$mdDialog = $mdDialog;
  110. this.$window = $window;
  111. this.settingsService = settingsService;
  112. this.notificationService = notificationService;
  113. this.activeElement = document.activeElement as HTMLElement;
  114. this.desktopNotifications = notificationService.getWantsNotifications();
  115. this.notificationApiAvailable = notificationService.isNotificationApiAvailable();
  116. this.notificationPermission = notificationService.getNotificationPermission();
  117. this.notificationPreview = notificationService.getWantsPreview();
  118. this.notificationSound = notificationService.getWantsSound();
  119. }
  120. public cancel(): void {
  121. this.$mdDialog.cancel();
  122. this.done();
  123. }
  124. protected hide(data: any): void {
  125. this.$mdDialog.hide(data);
  126. this.done();
  127. }
  128. private done(): void {
  129. if (this.activeElement !== null) {
  130. // Reset focus
  131. this.activeElement.focus();
  132. }
  133. }
  134. public setWantsNotifications(desktopNotifications: boolean) {
  135. this.notificationService.setWantsNotifications(desktopNotifications);
  136. }
  137. public setWantsPreview(notificationPreview: boolean) {
  138. this.notificationService.setWantsPreview(notificationPreview);
  139. }
  140. public setWantsSound(notificationSound: boolean) {
  141. this.notificationService.setWantsSound(notificationSound);
  142. }
  143. }
  144. class ConversationController {
  145. public name = 'navigation';
  146. private logTag: string = '[ConversationController]';
  147. // Angular services
  148. private $stateParams;
  149. private $timeout: ng.ITimeoutService;
  150. private $state: ng.ui.IStateService;
  151. private $log: ng.ILogService;
  152. private $scope: ng.IScope;
  153. private $rootScope: ng.IRootScopeService;
  154. private $filter: ng.IFilterService;
  155. // Own services
  156. private webClientService: WebClientService;
  157. private receiverService: ReceiverService;
  158. private stateService: StateService;
  159. private mimeService: MimeService;
  160. // Third party services
  161. private $mdDialog: ng.material.IDialogService;
  162. private $mdToast: ng.material.IToastService;
  163. // Controller model
  164. private controllerModel: threema.ControllerModel;
  165. // DOM Elements
  166. private domChatElement: HTMLElement;
  167. // Scrolling
  168. public showScrollJump: boolean = false;
  169. public receiver: threema.Receiver;
  170. public type: threema.ReceiverType;
  171. public message: string = '';
  172. public lastReadMsgId: number = 0;
  173. public msgReadReportPending = false;
  174. private hasMore = true;
  175. private latestRefMsgId: number = null;
  176. private allText: string;
  177. private messages: threema.Message[];
  178. public initialData: threema.InitialConversationData = {
  179. draft: '',
  180. initialText: '',
  181. };
  182. private $translate: ng.translate.ITranslateService;
  183. private locked = false;
  184. public maxTextLength: number;
  185. public isTyping = (): boolean => false;
  186. public allMentions: threema.Mention[] = [];
  187. public currentMentions: threema.Mention[] = [];
  188. public currentMentionFilterWord = null;
  189. public selectedMention: number = null;
  190. private uploading = {
  191. enabled: false,
  192. value1: 0,
  193. value2: 0,
  194. };
  195. public static $inject = [
  196. '$stateParams', '$state', '$timeout', '$log', '$scope', '$rootScope',
  197. '$mdDialog', '$mdToast', '$location', '$translate', '$filter',
  198. 'WebClientService', 'StateService', 'ReceiverService', 'MimeService', 'VersionService',
  199. 'ControllerModelService',
  200. ];
  201. constructor($stateParams: threema.ConversationStateParams,
  202. $state: ng.ui.IStateService,
  203. $timeout: ng.ITimeoutService,
  204. $log: ng.ILogService,
  205. $scope: ng.IScope,
  206. $rootScope: ng.IRootScopeService,
  207. $mdDialog: ng.material.IDialogService,
  208. $mdToast: ng.material.IToastService,
  209. $location,
  210. $translate: ng.translate.ITranslateService,
  211. $filter: ng.IFilterService,
  212. webClientService: WebClientService,
  213. stateService: StateService,
  214. receiverService: ReceiverService,
  215. mimeService: MimeService,
  216. versionService: VersionService,
  217. controllerModelService: ControllerModelService) {
  218. this.$stateParams = $stateParams;
  219. this.$timeout = $timeout;
  220. this.$log = $log;
  221. this.webClientService = webClientService;
  222. this.receiverService = receiverService;
  223. this.stateService = stateService;
  224. this.mimeService = mimeService;
  225. this.$state = $state;
  226. this.$scope = $scope;
  227. this.$filter = $filter;
  228. this.$rootScope = $rootScope;
  229. this.$mdDialog = $mdDialog;
  230. this.$mdToast = $mdToast;
  231. this.$translate = $translate;
  232. // Close any showing dialogs
  233. this.$mdDialog.cancel();
  234. this.maxTextLength = this.webClientService.getMaxTextLength();
  235. this.allText = this.$translate.instant('messenger.ALL');
  236. // On every navigation event, close all dialogs.
  237. // Note: Deprecated. When migrating ui-router ($state),
  238. // replace with transition hooks.
  239. $rootScope.$on('$stateChangeStart', () => this.$mdDialog.cancel());
  240. // Check for version updates
  241. versionService.checkForUpdate();
  242. // Redirect to welcome if necessary
  243. if (stateService.state === 'error') {
  244. $log.debug('ConversationController: WebClient not yet running, redirecting to welcome screen');
  245. $state.go('welcome');
  246. return;
  247. }
  248. if (!this.locked) {
  249. // Get DOM references
  250. this.domChatElement = document.querySelector('#conversation-chat') as HTMLElement;
  251. // Add custom event handlers
  252. this.domChatElement.addEventListener('scroll', throttle(() => {
  253. $rootScope.$apply(() => {
  254. this.updateScrollJump();
  255. });
  256. }, 100, this), supportsPassive() ? {passive: true} as any : false);
  257. }
  258. // Set receiver and type
  259. try {
  260. this.receiver = webClientService.receivers.getData({type: $stateParams.type, id: $stateParams.id});
  261. this.type = $stateParams.type;
  262. if (this.receiver.type === undefined) {
  263. this.receiver.type = this.type;
  264. }
  265. // Initialize controller model
  266. const mode = ControllerModelMode.CHAT;
  267. switch (this.receiver.type) {
  268. case 'me':
  269. $log.warn(this.logTag, 'Cannot chat with own contact');
  270. $state.go('messenger.home');
  271. return;
  272. case 'contact':
  273. this.controllerModel = controllerModelService.contact(
  274. this.receiver as threema.ContactReceiver, mode);
  275. break;
  276. case 'group':
  277. this.controllerModel = controllerModelService.group(
  278. this.receiver as threema.GroupReceiver, mode);
  279. break;
  280. case 'distributionList':
  281. this.controllerModel = controllerModelService.distributionList(
  282. this.receiver as threema.DistributionListReceiver, mode);
  283. break;
  284. default:
  285. $log.error(this.logTag, 'Cannot initialize controller model:',
  286. 'Invalid receiver type "' + this.receiver.type + '"');
  287. $state.go('messenger.home');
  288. return;
  289. }
  290. // Check if this receiver may be viewed
  291. if (this.controllerModel.canView() === false) {
  292. $log.warn(this.logTag, 'Cannot view this receiver, redirecting to home');
  293. $state.go('messenger.home');
  294. return;
  295. }
  296. // initial set locked state
  297. this.locked = this.receiver.locked;
  298. this.receiverService.setActive(this.receiver);
  299. if (!this.receiver.locked) {
  300. let latestHeight = 0;
  301. // update unread count
  302. this.webClientService.messages.updateFirstUnreadMessage(this.receiver);
  303. this.messages = this.webClientService.messages.register(
  304. this.receiver,
  305. this.$scope,
  306. (e, allMessages: threema.Message[], hasMore: boolean) => {
  307. this.messages = allMessages;
  308. this.hasMore = hasMore;
  309. if (this.latestRefMsgId !== null) {
  310. // scroll to div..
  311. this.domChatElement.scrollTop =
  312. this.domChatElement.scrollHeight - latestHeight;
  313. this.latestRefMsgId = null;
  314. }
  315. latestHeight = this.domChatElement.scrollHeight;
  316. },
  317. );
  318. // Enable mentions only in group chats
  319. if (this.type === 'group') {
  320. this.allMentions.push({
  321. identity: null,
  322. query: this.$translate.instant('messenger.ALL').toLowerCase(),
  323. isAll: true,
  324. });
  325. this.controllerModel.getMembers().forEach((identity: string) => {
  326. const contactReceiver = this.webClientService.contacts.get(identity);
  327. if (contactReceiver) {
  328. this.allMentions.push({
  329. identity: identity,
  330. query: (contactReceiver.displayName + ' ' + identity).toLowerCase(),
  331. isAll: false,
  332. });
  333. }
  334. });
  335. }
  336. this.initialData = {
  337. draft: webClientService.getDraft(this.receiver),
  338. initialText: $stateParams.initParams ? $stateParams.initParams.text : '',
  339. };
  340. if (this.receiver.type === 'contact') {
  341. this.isTyping = () => this.webClientService.isTyping(this.receiver as threema.ContactReceiver);
  342. }
  343. }
  344. } catch (error) {
  345. $log.error('Could not set receiver and type');
  346. $log.debug(error.stack);
  347. $state.go('messenger.home');
  348. }
  349. // reload controller if locked state was changed
  350. $scope.$watch(() => {
  351. return this.receiver.locked;
  352. }, () => {
  353. if (this.locked !== this.receiver.locked) {
  354. $state.reload();
  355. }
  356. });
  357. }
  358. public isEnabled(): boolean {
  359. return this.type !== 'group'
  360. || !(this.receiver as threema.GroupReceiver).disabled;
  361. }
  362. public isQuoting(): boolean {
  363. return this.getQuote() !== undefined;
  364. }
  365. public getQuote(): threema.Quote {
  366. return this.webClientService.getQuote(this.receiver);
  367. }
  368. public cancelQuoting(): void {
  369. // Clear current quote
  370. this.webClientService.setQuote(this.receiver);
  371. }
  372. public showError(errorMessage: string, toastLength = 4000) {
  373. if (errorMessage === undefined || errorMessage.length === 0) {
  374. errorMessage = this.$translate.instant('error.ERROR_OCCURRED');
  375. }
  376. this.$mdToast.show(
  377. this.$mdToast.simple()
  378. .textContent(errorMessage)
  379. .position('bottom center'));
  380. }
  381. /**
  382. * Submit function for input field. Can contain text or file data.
  383. * Return whether sending was successful.
  384. */
  385. public submit = (type: threema.MessageContentType, contents: threema.MessageData[]): Promise<any> => {
  386. // Validate whether a connection is available
  387. return new Promise((resolve, reject) => {
  388. if (this.stateService.state !== 'ok') {
  389. // Invalid connection, show toast and abort
  390. this.showError(this.$translate.instant('error.NO_CONNECTION'));
  391. return reject();
  392. }
  393. let success = true;
  394. const nextCallback = (index: number) => {
  395. if (index === contents.length - 1) {
  396. if (success) {
  397. resolve();
  398. } else {
  399. reject();
  400. }
  401. }
  402. };
  403. switch (type) {
  404. case 'file':
  405. // Determine file type
  406. let showSendAsFileCheckbox = false;
  407. let captionSupported = false;
  408. for (const msg of contents as threema.FileMessageData[]) {
  409. if (!msg.fileType) {
  410. msg.fileType = 'application/octet-stream';
  411. }
  412. captionSupported = this.mimeService.isImage(msg.fileType);
  413. if (this.mimeService.isImage(msg.fileType)
  414. || this.mimeService.isAudio(msg.fileType)
  415. || this.mimeService.isVideo(msg.fileType)) {
  416. showSendAsFileCheckbox = true;
  417. break;
  418. }
  419. }
  420. // Prepare preview
  421. let preview: threema.FileMessageData | null = null;
  422. if (contents.length === 1) {
  423. const msg = contents[0] as threema.FileMessageData;
  424. if (this.mimeService.isImage(msg.fileType)) {
  425. preview = msg;
  426. }
  427. }
  428. // Eager translations
  429. const title = this.$translate.instant('messenger.CONFIRM_FILE_SEND', {
  430. senderName: (this.$filter('emojify') as any)
  431. ((this.$filter('emptyToPlaceholder') as any)(this.receiver.displayName, '-')),
  432. });
  433. const placeholder = this.$translate.instant('messenger.CONFIRM_FILE_CAPTION');
  434. const confirmSendAsFile = this.$translate.instant('messenger.CONFIRM_SEND_AS_FILE');
  435. // Show confirmation dialog
  436. this.$mdDialog.show({
  437. clickOutsideToClose: false,
  438. locals: { preview: preview },
  439. controller: 'SendFileController',
  440. controllerAs: 'ctrl',
  441. // tslint:disable:max-line-length
  442. template: `
  443. <md-dialog class="send-file-dialog">
  444. <md-dialog-content class="md-dialog-content">
  445. <h2 class="md-title">${title}</h2>
  446. <img class="preview" ng-if="ctrl.hasPreview()" ng-src="{{ ctrl.preview.data | bufferToUrl: ctrl.preview.fileType }}" />
  447. <md-input-container md-no-float class="input-caption md-prompt-input-container" ng-show="!${showSendAsFileCheckbox} || ctrl.sendAsFile || ${captionSupported}">
  448. <input maxlength="1000" md-autofocus ng-keypress="ctrl.keypress($event)" ng-model="ctrl.caption" placeholder="${placeholder}" aria-label="${placeholder}">
  449. </md-input-container>
  450. <md-input-container md-no-float class="input-send-as-file md-prompt-input-container" ng-show="${showSendAsFileCheckbox}">
  451. <md-checkbox ng-model="ctrl.sendAsFile" aria-label="${confirmSendAsFile}">
  452. ${confirmSendAsFile}
  453. </md-checkbox>
  454. </md-input-container>
  455. </md-dialog-content>
  456. <md-dialog-actions>
  457. <button class="md-primary md-cancel-button md-button" md-ink-ripple type="button" ng-click="ctrl.cancel()">
  458. <span translate>common.CANCEL</span>
  459. </button>
  460. <button class="md-primary md-cancel-button md-button" md-ink-ripple type="button" ng-click="ctrl.send()">
  461. <span translate>common.SEND</span>
  462. </button>
  463. </md-dialog-actions>
  464. </md-dialog>
  465. `,
  466. // tslint:enable:max-line-length
  467. }).then((data) => {
  468. const caption = data.caption;
  469. const sendAsFile = data.sendAsFile;
  470. contents.forEach((msg: threema.FileMessageData, index: number) => {
  471. if (caption !== undefined && caption.length > 0) {
  472. msg.caption = caption;
  473. }
  474. msg.sendAsFile = sendAsFile;
  475. this.webClientService.sendMessage(this.$stateParams, type, msg)
  476. .then(() => {
  477. nextCallback(index);
  478. })
  479. .catch((error) => {
  480. this.$log.error(error);
  481. this.showError(error);
  482. success = false;
  483. nextCallback(index);
  484. });
  485. });
  486. }, angular.noop);
  487. break;
  488. case 'text':
  489. // do not show confirmation, send directly
  490. contents.forEach((msg: threema.MessageData, index: number) => {
  491. msg.quote = this.webClientService.getQuote(this.receiver);
  492. // remove quote
  493. this.webClientService.setQuote(this.receiver);
  494. // send message
  495. this.webClientService.sendMessage(this.$stateParams, type, msg)
  496. .then(() => {
  497. nextCallback(index);
  498. })
  499. .catch((error) => {
  500. this.$log.error(error);
  501. this.showError(error);
  502. success = false;
  503. nextCallback(index);
  504. });
  505. });
  506. return;
  507. default:
  508. this.$log.warn('Invalid message type:', type);
  509. reject();
  510. }
  511. });
  512. }
  513. /**
  514. * Something was typed.
  515. *
  516. * In contrast to startTyping, this method is is always called, not just if
  517. * the text field is non-empty.
  518. */
  519. public onTyping = (text: string, currentWord: string = null) => {
  520. // Update draft
  521. this.webClientService.setDraft(this.receiver, text);
  522. if (currentWord && currentWord.substr(0, 1) === '@') {
  523. this.currentMentionFilterWord = currentWord.substr(1);
  524. const query = this.currentMentionFilterWord.toLowerCase().trim();
  525. const selectedMentionObject = this.getSelectedMention();
  526. this.currentMentions = this.allMentions.filter((i) => {
  527. if (query.length === 0) {
  528. return true;
  529. }
  530. return i.query.indexOf(query) >= 0;
  531. });
  532. // If only one mention is filtered, select them
  533. if (this.currentMentions.length === 1) {
  534. this.selectedMention = 0;
  535. } else if (selectedMentionObject !== null) {
  536. // Get the new position of the latest selected mention object
  537. this.selectedMention = null;
  538. this.selectedMention = this.currentMentions.findIndex((m) => {
  539. return m.identity === selectedMentionObject.identity;
  540. });
  541. }
  542. } else {
  543. this.currentMentionFilterWord = null;
  544. }
  545. }
  546. public getSelectedMention = (): threema.Mention => {
  547. if (this.selectedMention === null
  548. || this.selectedMention < 0
  549. || this.selectedMention > this.currentMentions.length - 1) {
  550. return null;
  551. }
  552. return this.currentMentions[this.selectedMention];
  553. }
  554. public showMentionSelector = (): boolean => {
  555. return this.type === 'group'
  556. && this.currentMentionFilterWord != null
  557. && this.currentMentions.length > 0;
  558. }
  559. /**
  560. * Handle mention selector navigation
  561. */
  562. public onComposeKeyDown = (ev: KeyboardEvent): boolean => {
  563. /* Make mentions readonly for now
  564. if (this.showMentionSelector() && !ev.shiftKey) {
  565. let move = ev.key === 'ArrowDown' ? 1 : (ev.key === 'ArrowUp' ? - 1 : 0);
  566. if (move !== 0) {
  567. // Move cursors position in mention selector
  568. if (this.selectedMention !== null) {
  569. this.selectedMention += move;
  570. // Fix positions
  571. if (this.selectedMention > this.currentMentions.length - 1) {
  572. this.selectedMention = 0;
  573. } else if (this.selectedMention < 0) {
  574. this.selectedMention = this.currentMentions.length - 1;
  575. }
  576. } else {
  577. this.selectedMention = 0;
  578. }
  579. return false;
  580. }
  581. if (ev.key === 'Enter') {
  582. // Enter, select current mention
  583. const selectedMentionObject = this.getSelectedMention();
  584. if (selectedMentionObject === null) {
  585. // If no (or a invalid) mention is selected, select the first mention
  586. this.selectedMention = 0;
  587. } else {
  588. this.onMentionSelected(selectedMentionObject.identity);
  589. }
  590. return false;
  591. }
  592. }
  593. */
  594. return true;
  595. }
  596. public onMentionSelected(identity: string = null): void {
  597. this.$rootScope.$broadcast('onMentionSelected', {
  598. query: '@' + this.currentMentionFilterWord,
  599. mention: '@[' + (identity === null ? '@@@@@@@@' : identity.toUpperCase()) + ']',
  600. });
  601. }
  602. public onUploading = (inProgress: boolean, percentCurrent: number = null, percentFull: number = null) => {
  603. this.uploading.enabled = inProgress;
  604. this.uploading.value1 = Number(percentCurrent);
  605. this.uploading.value2 = Number(percentCurrent);
  606. }
  607. /**
  608. * We started typing.
  609. */
  610. public startTyping = () => {
  611. // Notify app
  612. this.webClientService.sendMeIsTyping(this.$stateParams, true);
  613. }
  614. /**
  615. * We stopped typing.
  616. */
  617. public stopTyping = () => {
  618. // Notify app
  619. this.webClientService.sendMeIsTyping(this.$stateParams, false);
  620. }
  621. /**
  622. * User scrolled to the top of the chat.
  623. */
  624. public topOfChat(): void {
  625. this.requestMessages();
  626. }
  627. public requestMessages(): void {
  628. const refMsgId = this.webClientService.requestMessages(this.$stateParams);
  629. if (refMsgId !== null
  630. && refMsgId !== undefined) {
  631. // new message are requested, scroll to refMsgId
  632. this.latestRefMsgId = refMsgId;
  633. } else {
  634. this.latestRefMsgId = null;
  635. }
  636. }
  637. public showReceiver(ev): void {
  638. this.$state.go('messenger.home.detail', this.receiver);
  639. }
  640. public hasMoreMessages(): boolean {
  641. return this.hasMore;
  642. }
  643. /**
  644. * A message has been seen. Report it to the app, with a small delay to
  645. * avoid sending too many messages at once.
  646. */
  647. public msgRead(msgId: number): void {
  648. if (msgId > this.lastReadMsgId) {
  649. this.lastReadMsgId = msgId;
  650. }
  651. if (!this.msgReadReportPending) {
  652. this.msgReadReportPending = true;
  653. const receiver = angular.copy(this.receiver);
  654. receiver.type = this.type;
  655. this.$timeout(() => {
  656. this.webClientService.requestRead(receiver, this.lastReadMsgId);
  657. this.msgReadReportPending = false;
  658. }, 500);
  659. }
  660. }
  661. public goBack(): void {
  662. this.receiverService.setActive(undefined);
  663. // redirect to messenger home
  664. this.$state.go('messenger.home');
  665. }
  666. /**
  667. * Scroll to bottom of chat.
  668. */
  669. public scrollDown(): void {
  670. this.domChatElement.scrollTop = this.domChatElement.scrollHeight;
  671. }
  672. /**
  673. * Only show the scroll to bottom button if user scrolled more than 10px
  674. * away from bottom.
  675. */
  676. private updateScrollJump(): void {
  677. const chat = this.domChatElement;
  678. this.showScrollJump = chat.scrollHeight - (chat.scrollTop + chat.offsetHeight) > 10;
  679. }
  680. }
  681. class NavigationController {
  682. public name = 'navigation';
  683. private webClientService: WebClientService;
  684. private receiverService: ReceiverService;
  685. private stateService: StateService;
  686. private trustedKeyStoreService: TrustedKeyStoreService;
  687. private activeTab: 'contacts' | 'conversations' = 'conversations';
  688. private searchVisible = false;
  689. private searchText: string = '';
  690. private $mdDialog;
  691. private $translate: ng.translate.ITranslateService;
  692. private $state: ng.ui.IStateService;
  693. public static $inject = [
  694. '$log', '$state', '$mdDialog', '$translate',
  695. 'WebClientService', 'StateService', 'ReceiverService', 'TrustedKeyStore',
  696. ];
  697. constructor($log: ng.ILogService, $state: ng.ui.IStateService,
  698. $mdDialog: ng.material.IDialogService, $translate: ng.translate.ITranslateService,
  699. webClientService: WebClientService, stateService: StateService,
  700. receiverService: ReceiverService,
  701. trustedKeyStoreService: TrustedKeyStoreService) {
  702. // Redirect to welcome if necessary
  703. if (stateService.state === 'error') {
  704. $log.debug('NavigationController: WebClient not yet running, redirecting to welcome screen');
  705. $state.go('welcome');
  706. return;
  707. }
  708. this.webClientService = webClientService;
  709. this.receiverService = receiverService;
  710. this.stateService = stateService;
  711. this.trustedKeyStoreService = trustedKeyStoreService;
  712. this.$mdDialog = $mdDialog;
  713. this.$translate = $translate;
  714. this.$state = $state;
  715. }
  716. public contacts(): threema.ContactReceiver[] {
  717. return Array.from(this.webClientService.contacts.values()) as threema.ContactReceiver[];
  718. }
  719. /**
  720. * Search for `needle` in the `haystack`. The search is case insensitive.
  721. */
  722. private matches(haystack: string, needle: string): boolean {
  723. return haystack.toLowerCase().replace('\n', ' ').indexOf(needle.trim().toLowerCase()) !== -1;
  724. }
  725. /**
  726. * Predicate function used for conversation filtering.
  727. *
  728. * Match by contact name *or* id *or* last message text.
  729. */
  730. private searchConversation = (value: threema.Conversation, index, array): boolean => {
  731. return this.searchText === ''
  732. || this.matches(value.receiver.displayName, this.searchText)
  733. || (value.latestMessage && value.latestMessage.body
  734. && this.matches(value.latestMessage.body, this.searchText))
  735. || (value.receiver.id.length === 8 && this.matches(value.receiver.id, this.searchText));
  736. }
  737. /**
  738. * Predicate function used for contact filtering.
  739. *
  740. * Match by contact name *or* id.
  741. */
  742. private searchContact = (value, index, array): boolean => {
  743. return this.searchText === ''
  744. || value.displayName.toLowerCase().indexOf(this.searchText.toLowerCase()) !== -1
  745. || value.id.toLowerCase().indexOf(this.searchText.toLowerCase()) !== -1;
  746. }
  747. public isVisible(conversation: threema.Conversation) {
  748. return conversation.receiver.visible;
  749. }
  750. public conversations(): threema.Conversation[] {
  751. return this.webClientService.conversations.get();
  752. }
  753. public isActive(value: threema.Conversation): boolean {
  754. return this.receiverService.isConversationActive(value);
  755. }
  756. /**
  757. * Show dialog.
  758. */
  759. public showDialog(name, ev) {
  760. this.$mdDialog.show({
  761. controller: DialogController,
  762. controllerAs: 'ctrl',
  763. templateUrl: 'partials/dialog.' + name + '.html',
  764. parent: angular.element(document.body),
  765. targetEvent: ev,
  766. clickOutsideToClose: true,
  767. fullscreen: true,
  768. });
  769. }
  770. /**
  771. * Show about dialog.
  772. */
  773. public about(ev): void {
  774. this.showDialog('about', ev);
  775. }
  776. /**
  777. * Show settings dialog.
  778. */
  779. public settings(ev): void {
  780. this.$mdDialog.show({
  781. controller: SettingsController,
  782. controllerAs: 'ctrl',
  783. templateUrl: 'partials/dialog.settings.html',
  784. parent: angular.element(document.body),
  785. targetEvent: ev,
  786. clickOutsideToClose: true,
  787. fullscreen: true,
  788. });
  789. }
  790. /**
  791. * Return whether a trusted key is available.
  792. */
  793. public isPersistent(): boolean {
  794. return this.trustedKeyStoreService.hasTrustedKey();
  795. }
  796. /**
  797. * Close the session.
  798. */
  799. public closeSession(ev): void {
  800. const confirm = this.$mdDialog.confirm()
  801. .title(this.$translate.instant('common.SESSION_CLOSE'))
  802. .textContent(this.$translate.instant('common.CONFIRM_CLOSE_BODY'))
  803. .targetEvent(ev)
  804. .ok(this.$translate.instant('common.YES'))
  805. .cancel(this.$translate.instant('common.CANCEL'));
  806. this.$mdDialog.show(confirm).then(() => {
  807. const deleteStoredData = false;
  808. const resetPush = true;
  809. const redirect = true;
  810. this.webClientService.stop(true, deleteStoredData, resetPush, redirect);
  811. }, () => {
  812. // do nothing
  813. });
  814. }
  815. /**
  816. * Close and delete the session.
  817. */
  818. public deleteSession(ev): void {
  819. const confirm = this.$mdDialog.confirm()
  820. .title(this.$translate.instant('common.SESSION_DELETE'))
  821. .textContent(this.$translate.instant('common.CONFIRM_DELETE_CLOSE_BODY'))
  822. .targetEvent(ev)
  823. .ok(this.$translate.instant('common.YES'))
  824. .cancel(this.$translate.instant('common.CANCEL'));
  825. this.$mdDialog.show(confirm).then(() => {
  826. const deleteStoredData = true;
  827. const resetPush = true;
  828. const redirect = true;
  829. this.webClientService.stop(true, deleteStoredData, resetPush, redirect);
  830. }, () => {
  831. // do nothing
  832. });
  833. }
  834. public addContact(ev): void {
  835. this.$state.go('messenger.home.create', {
  836. type: 'contact',
  837. });
  838. }
  839. public createGroup(ev): void {
  840. this.$state.go('messenger.home.create', {
  841. type: 'group',
  842. });
  843. }
  844. public createDistributionList(ev): void {
  845. this.$state.go('messenger.home.create', {
  846. type: 'distributionList',
  847. });
  848. }
  849. /**
  850. * Toggle search bar.
  851. */
  852. public toggleSearch(): void {
  853. this.searchVisible = !this.searchVisible;
  854. }
  855. public getMyIdentity(): threema.Identity {
  856. return this.webClientService.getMyIdentity();
  857. }
  858. public showMyIdentity(): boolean {
  859. return this.getMyIdentity() !== undefined;
  860. }
  861. }
  862. class MessengerController {
  863. public name = 'messenger';
  864. private receiverService: ReceiverService;
  865. private $state;
  866. private webClientService: WebClientService;
  867. public static $inject = [
  868. '$scope', '$state', '$log', '$mdDialog', '$translate',
  869. 'StateService', 'ReceiverService', 'WebClientService', 'ControllerService',
  870. ];
  871. constructor($scope, $state, $log: ng.ILogService, $mdDialog: ng.material.IDialogService,
  872. $translate: ng.translate.ITranslateService,
  873. stateService: StateService, receiverService: ReceiverService,
  874. webClientService: WebClientService, controllerService: ControllerService) {
  875. // Redirect to welcome if necessary
  876. if (stateService.state === 'error') {
  877. $log.debug('MessengerController: WebClient not yet running, redirecting to welcome screen');
  878. $state.go('welcome');
  879. return;
  880. }
  881. controllerService.setControllerName('messenger');
  882. this.receiverService = receiverService;
  883. this.$state = $state;
  884. this.webClientService = webClientService;
  885. // watch for alerts
  886. $scope.$watch(() => webClientService.alerts, (alerts: threema.Alert[]) => {
  887. if (alerts.length > 0) {
  888. angular.forEach(alerts, (alert: threema.Alert) => {
  889. $mdDialog.show(
  890. $mdDialog.alert()
  891. .clickOutsideToClose(true)
  892. .title(alert.type)
  893. .textContent(alert.message)
  894. .ok($translate.instant('common.OK')));
  895. });
  896. // clean array
  897. webClientService.alerts = [];
  898. }
  899. }, true);
  900. this.webClientService.setReceiverListener({
  901. onRemoved(receiver: threema.Receiver) {
  902. switch ($state.current.name) {
  903. case 'messenger.home.conversation':
  904. case 'messenger.home.detail':
  905. case 'messenger.home.edit':
  906. if ($state.params !== undefined
  907. && $state.params.type !== undefined
  908. && $state.params.id !== undefined) {
  909. if ($state.params.type === receiver.type
  910. && $state.params.id === receiver.id) {
  911. // conversation or sub form is open, redirect to home!
  912. $state.go('messenger.home', null, {location: 'replace'});
  913. }
  914. }
  915. break;
  916. default:
  917. $log.warn('Ignored onRemoved event for state', $state.current.name);
  918. }
  919. },
  920. });
  921. }
  922. public showDetail(): boolean {
  923. return !this.$state.is('messenger.home');
  924. }
  925. }
  926. class ReceiverDetailController {
  927. private logTag: string = '[ReceiverDetailController]';
  928. public $mdDialog: any;
  929. public $state: ng.ui.IStateService;
  930. public receiver: threema.Receiver;
  931. public me: threema.MeReceiver;
  932. public title: string;
  933. public fingerPrint?: string;
  934. private fingerPrintService: FingerPrintService;
  935. private contactService: ContactService;
  936. private showGroups = false;
  937. private showDistributionLists = false;
  938. private inGroups: threema.GroupReceiver[] = [];
  939. private inDistributionLists: threema.DistributionListReceiver[] = [];
  940. private hasSystemEmails = false;
  941. private hasSystemPhones = false;
  942. private isWorkReceiver = false;
  943. private showBlocked = () => false;
  944. private controllerModel: threema.ControllerModel;
  945. public static $inject = [
  946. '$log', '$stateParams', '$state', '$mdDialog',
  947. 'WebClientService', 'FingerPrintService', 'ContactService', 'ControllerModelService',
  948. ];
  949. constructor($log: ng.ILogService, $stateParams, $state: ng.ui.IStateService, $mdDialog: ng.material.IDialogService,
  950. webClientService: WebClientService, fingerPrintService: FingerPrintService,
  951. contactService: ContactService, controllerModelService: ControllerModelService) {
  952. this.$mdDialog = $mdDialog;
  953. this.$state = $state;
  954. this.fingerPrintService = fingerPrintService;
  955. this.contactService = contactService;
  956. this.receiver = webClientService.receivers.getData($stateParams);
  957. this.me = webClientService.me;
  958. // Append members
  959. if (this.receiver.type === 'contact') {
  960. const contactReceiver = this.receiver as threema.ContactReceiver;
  961. this.contactService.requiredDetails(contactReceiver)
  962. .then(() => {
  963. this.hasSystemEmails = contactReceiver.systemContact.emails.length > 0;
  964. this.hasSystemPhones = contactReceiver.systemContact.phoneNumbers.length > 0;
  965. })
  966. .catch(() => {
  967. // do nothing
  968. });
  969. this.isWorkReceiver = contactReceiver.identityType === threema.IdentityType.Work;
  970. this.fingerPrint = this.fingerPrintService.generate(contactReceiver.publicKey);
  971. webClientService.groups.forEach((groupReceiver: threema.GroupReceiver) => {
  972. // check if my identity is a member
  973. if (groupReceiver.members.indexOf(contactReceiver.id) !== -1) {
  974. this.inGroups.push(groupReceiver);
  975. this.showGroups = true;
  976. }
  977. });
  978. webClientService.distributionLists.forEach(
  979. (distributionListReceiver: threema.DistributionListReceiver) => {
  980. // check if my identity is a member
  981. if (distributionListReceiver.members.indexOf(contactReceiver.id) !== -1) {
  982. this.inDistributionLists.push(distributionListReceiver);
  983. this.showDistributionLists = true;
  984. }
  985. },
  986. );
  987. this.showBlocked = () => contactReceiver.isBlocked;
  988. }
  989. switch (this.receiver.type) {
  990. case 'me':
  991. $log.warn(this.logTag, 'Cannot view own contact');
  992. $state.go('messenger.home');
  993. return;
  994. case 'contact':
  995. this.controllerModel = controllerModelService
  996. .contact(this.receiver as threema.ContactReceiver, ControllerModelMode.VIEW);
  997. break;
  998. case 'group':
  999. this.controllerModel = controllerModelService
  1000. .group(this.receiver as threema.GroupReceiver, ControllerModelMode.VIEW);
  1001. break;
  1002. case 'distributionList':
  1003. this.controllerModel = controllerModelService
  1004. .distributionList(this.receiver as threema.DistributionListReceiver, ControllerModelMode.VIEW);
  1005. break;
  1006. default:
  1007. $log.error(this.logTag, 'Cannot initialize controller model:',
  1008. 'Invalid receiver type "' + this.receiver.type + '"');
  1009. $state.go('messenger.home');
  1010. return;
  1011. }
  1012. // If this receiver may not be viewed, navigate to "home" view
  1013. if (this.controllerModel.canView() === false) {
  1014. $log.warn(this.logTag, 'Cannot view this receiver, redirecting to home');
  1015. this.$state.go('messenger.home');
  1016. return;
  1017. }
  1018. // If this receiver is removed, navigate to "home" view
  1019. this.controllerModel.setOnRemoved((receiverId: string) => {
  1020. $log.warn(this.logTag, 'Receiver removed, redirecting to home');
  1021. this.$state.go('messenger.home');
  1022. });
  1023. }
  1024. public chat(): void {
  1025. this.$state.go('messenger.home.conversation', {
  1026. type: this.receiver.type,
  1027. id: this.receiver.id,
  1028. initParams: null,
  1029. });
  1030. }
  1031. public edit(): void {
  1032. if (!this.controllerModel.canEdit()) {
  1033. return;
  1034. }
  1035. this.$state.go('messenger.home.edit', {
  1036. type: this.receiver.type,
  1037. id: this.receiver.id,
  1038. initParams: null,
  1039. });
  1040. }
  1041. public goBack(): void {
  1042. window.history.back();
  1043. }
  1044. }
  1045. /**
  1046. * Control edit a group or a contact
  1047. * fields, validate and save routines are implemented in the specific ControllerModel
  1048. */
  1049. class ReceiverEditController {
  1050. private logTag: string = '[ReceiverEditController]';
  1051. public $mdDialog: any;
  1052. public $state: ng.ui.IStateService;
  1053. private $translate: ng.translate.ITranslateService;
  1054. public title: string;
  1055. private $timeout: ng.ITimeoutService;
  1056. private execute: ExecuteService;
  1057. public loading = false;
  1058. private controllerModel: threema.ControllerModel;
  1059. public type: string;
  1060. public static $inject = [
  1061. '$log', '$stateParams', '$state', '$mdDialog',
  1062. '$timeout', '$translate', 'WebClientService', 'ControllerModelService',
  1063. ];
  1064. constructor($log: ng.ILogService, $stateParams, $state: ng.ui.IStateService,
  1065. $mdDialog, $timeout: ng.ITimeoutService, $translate: ng.translate.ITranslateService,
  1066. webClientService: WebClientService, controllerModelService: ControllerModelService) {
  1067. this.$mdDialog = $mdDialog;
  1068. this.$state = $state;
  1069. this.$timeout = $timeout;
  1070. this.$translate = $translate;
  1071. const receiver = webClientService.receivers.getData($stateParams);
  1072. switch (receiver.type) {
  1073. case 'me':
  1074. $log.warn(this.logTag, 'Cannot edit own contact');
  1075. $state.go('messenger.home');
  1076. return;
  1077. case 'contact':
  1078. this.controllerModel = controllerModelService.contact(
  1079. receiver as threema.ContactReceiver,
  1080. ControllerModelMode.EDIT,
  1081. );
  1082. break;
  1083. case 'group':
  1084. this.controllerModel = controllerModelService.group(
  1085. receiver as threema.GroupReceiver,
  1086. ControllerModelMode.EDIT,
  1087. );
  1088. break;
  1089. case 'distributionList':
  1090. this.controllerModel = controllerModelService.distributionList(
  1091. receiver as threema.DistributionListReceiver,
  1092. ControllerModelMode.EDIT,
  1093. );
  1094. break;
  1095. default:
  1096. $log.error(this.logTag, 'Cannot initialize controller model:',
  1097. 'Invalid receiver type "' + receiver.type + '"');
  1098. $state.go('messenger.home');
  1099. return;
  1100. }
  1101. this.type = receiver.type;
  1102. // If this receiver may not be viewed, navigate to "home" view
  1103. if (this.controllerModel.canView() === false) {
  1104. $log.warn(this.logTag, 'Cannot view this receiver, redirecting to home');
  1105. this.$state.go('messenger.home');
  1106. return;
  1107. }
  1108. this.execute = new ExecuteService($log, $timeout, 1000);
  1109. }
  1110. public keypress($event: KeyboardEvent): void {
  1111. if ($event.key === 'Enter' && this.controllerModel.isValid()) {
  1112. this.save();
  1113. }
  1114. }
  1115. public save(): void {
  1116. // show loading
  1117. this.loading = true;
  1118. // validate first
  1119. this.execute.execute(this.controllerModel.save())
  1120. .then((receiver: threema.Receiver) => {
  1121. this.goBack();
  1122. })
  1123. .catch((errorCode) => {
  1124. this.showError(errorCode);
  1125. });
  1126. }
  1127. public isSaving(): boolean {
  1128. return this.execute !== undefined
  1129. && this.execute.isRunning();
  1130. }
  1131. public showError(errorCode): void {
  1132. this.$mdDialog.show(
  1133. this.$mdDialog.alert()
  1134. .clickOutsideToClose(true)
  1135. .title(this.controllerModel.subject)
  1136. .textContent(this.$translate.instant('validationError.editReceiver.' + errorCode))
  1137. .ok(this.$translate.instant('common.OK')));
  1138. }
  1139. public goBack(): void {
  1140. window.history.back();
  1141. }
  1142. }
  1143. /**
  1144. * Control creating a group or adding contact
  1145. * fields, validate and save routines are implemented in the specific ControllerModel
  1146. */
  1147. class ReceiverCreateController {
  1148. private logTag: string = '[ReceiverEditController]';
  1149. public $mdDialog: any;
  1150. private loading = false;
  1151. private $timeout: ng.ITimeoutService;
  1152. private $log: ng.ILogService;
  1153. private $state: ng.ui.IStateService;
  1154. private $mdToast: any;
  1155. public identity = '';
  1156. private $translate: any;
  1157. public type: string;
  1158. private execute: ExecuteService;
  1159. public controllerModel: threema.ControllerModel;
  1160. public static $inject = ['$stateParams', '$mdDialog', '$mdToast', '$translate',
  1161. '$timeout', '$state', '$log', 'ControllerModelService'];
  1162. constructor($stateParams: threema.CreateReceiverStateParams, $mdDialog, $mdToast, $translate,
  1163. $timeout: ng.ITimeoutService, $state: ng.ui.IStateService, $log: ng.ILogService,
  1164. controllerModelService: ControllerModelService) {
  1165. this.$mdDialog = $mdDialog;
  1166. this.$timeout = $timeout;
  1167. this.$state = $state;
  1168. this.$log = $log;
  1169. this.$mdToast = $mdToast;
  1170. this.$translate = $translate;
  1171. this.type = $stateParams.type;
  1172. switch (this.type) {
  1173. case 'me':
  1174. $log.warn(this.logTag, 'Cannot create own contact');
  1175. $state.go('messenger.home');
  1176. return;
  1177. case 'contact':
  1178. this.controllerModel = controllerModelService.contact(null, ControllerModelMode.NEW);
  1179. if ($stateParams.initParams !== null) {
  1180. (this.controllerModel as ContactControllerModel)
  1181. .identity = $stateParams.initParams.identity;
  1182. }
  1183. break;
  1184. case 'group':
  1185. this.controllerModel = controllerModelService.group(null, ControllerModelMode.NEW);
  1186. break;
  1187. case 'distributionList':
  1188. this.controllerModel = controllerModelService.distributionList(null, ControllerModelMode.NEW);
  1189. break;
  1190. default:
  1191. this.$log.error('invalid type', this.type);
  1192. }
  1193. this.execute = new ExecuteService($log, $timeout, 1000);
  1194. }
  1195. public isSaving(): boolean {
  1196. return this.execute.isRunning();
  1197. }
  1198. public goBack(): void {
  1199. if (!this.isSaving()) {
  1200. window.history.back();
  1201. }
  1202. }
  1203. private showAddError(errorCode: string): void {
  1204. if (errorCode === undefined) {
  1205. errorCode = 'invalid_entry';
  1206. }
  1207. this.$mdDialog.show(
  1208. this.$mdDialog.alert()
  1209. .clickOutsideToClose(true)
  1210. .title(this.controllerModel.subject)
  1211. .textContent(this.$translate.instant('validationError.createReceiver.' + errorCode))
  1212. .ok(this.$translate.instant('common.OK')),
  1213. );
  1214. }
  1215. public keypress($event: KeyboardEvent): void {
  1216. if ($event.key === 'Enter' && this.controllerModel.isValid()) {
  1217. this.create();
  1218. }
  1219. }
  1220. public create(): void {
  1221. // show loading
  1222. this.loading = true;
  1223. // validate first
  1224. this.execute.execute(this.controllerModel.save())
  1225. .then((receiver: threema.Receiver) => {
  1226. this.$state.go('messenger.home.detail', receiver, {location: 'replace'});
  1227. })
  1228. .catch((errorCode) => {
  1229. this.showAddError(errorCode);
  1230. });
  1231. }
  1232. }
  1233. angular.module('3ema.messenger', ['ngMaterial'])
  1234. .config(['$stateProvider', function($stateProvider: ng.ui.IStateProvider) {
  1235. $stateProvider
  1236. .state('messenger', {
  1237. abstract: true,
  1238. templateUrl: 'partials/messenger.html',
  1239. controller: 'MessengerController',
  1240. controllerAs: 'ctrl',
  1241. })
  1242. .state('messenger.home', {
  1243. url: '/messenger',
  1244. views: {
  1245. navigation: {
  1246. templateUrl: 'partials/messenger.navigation.html',
  1247. controller: 'NavigationController',
  1248. controllerAs: 'ctrl',
  1249. },
  1250. content: {
  1251. // Required because navigation should not be changed,
  1252. template: '<div ui-view></div>',
  1253. },
  1254. },
  1255. })
  1256. .state('messenger.home.conversation', {
  1257. url: '/conversation/{type}/{id}',
  1258. templateUrl: 'partials/messenger.conversation.html',
  1259. controller: 'ConversationController',
  1260. controllerAs: 'ctrl',
  1261. params: {initParams: null},
  1262. })
  1263. .state('messenger.home.detail', {
  1264. url: '/conversation/{type}/{id}/detail',
  1265. templateUrl: 'partials/messenger.receiver.html',
  1266. controller: 'ReceiverDetailController',
  1267. controllerAs: 'ctrl',
  1268. })
  1269. .state('messenger.home.edit', {
  1270. url: '/conversation/{type}/{id}/detail/edit',
  1271. templateUrl: 'partials/messenger.receiver.edit.html',
  1272. controller: 'ReceiverEditController',
  1273. controllerAs: 'ctrl',
  1274. })
  1275. .state('messenger.home.create', {
  1276. url: '/receiver/create/{type}',
  1277. templateUrl: 'partials/messenger.receiver.create.html',
  1278. controller: 'ReceiverCreateController',
  1279. controllerAs: 'ctrl',
  1280. params: {initParams: null},
  1281. })
  1282. ;
  1283. }])
  1284. .controller('SendFileController', SendFileController)
  1285. .controller('MessengerController', MessengerController)
  1286. .controller('ConversationController', ConversationController)
  1287. .controller('NavigationController', NavigationController)
  1288. .controller('ReceiverDetailController', ReceiverDetailController)
  1289. .controller('ReceiverEditController', ReceiverEditController)
  1290. .controller('ReceiverCreateController', ReceiverCreateController)
  1291. ;