container.ts 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808
  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 {ReceiverService} from '../services/receiver';
  18. /**
  19. * A simple HashSet implementation based on a JavaScript object.
  20. *
  21. * Only strings can be stored in this set.
  22. */
  23. class StringHashSet {
  24. private setObj = {};
  25. private val = {};
  26. public add(str: string): void {
  27. this.setObj[str] = this.val;
  28. }
  29. public contains(str: string): boolean {
  30. return this.setObj[str] === this.val;
  31. }
  32. public remove(str: string): void {
  33. delete this.setObj[str];
  34. }
  35. public values() {
  36. const values = [];
  37. for (const i in this.setObj) {
  38. if (this.setObj[i] === this.val) {
  39. values.push(i);
  40. }
  41. }
  42. return values;
  43. }
  44. }
  45. angular.module('3ema.container', [])
  46. .factory('Container', ['$filter', '$log', 'ReceiverService',
  47. function($filter, $log, receiverService: ReceiverService) {
  48. type ContactMap = Map<string, threema.ContactReceiver>;
  49. type GroupMap = Map<string, threema.GroupReceiver>;
  50. type DistributionListMap = Map<string, threema.DistributionListReceiver>;
  51. type MessageMap = Map<threema.ReceiverType, Map<string, ReceiverMessages>>;
  52. type ConversationFilter = (data: threema.Conversation[]) => threema.Conversation[];
  53. type ConversationConverter = (data: threema.Conversation) => threema.Conversation;
  54. type MessageConverter = (data: threema.Message) => threema.Message;
  55. /**
  56. * Collection that manages receivers like contacts, groups or distribution lists.
  57. *
  58. * Think of it like the "address book".
  59. */
  60. class Receivers implements threema.Container.Receivers {
  61. public me: threema.MeReceiver = null;
  62. public contacts: ContactMap = new Map();
  63. public groups: GroupMap = new Map();
  64. public distributionLists: DistributionListMap = new Map();
  65. /**
  66. * Get the receiver map for the specified type.
  67. */
  68. public get(receiverType: threema.ReceiverType): threema.Receiver | Map<string, threema.Receiver> {
  69. switch (receiverType) {
  70. case 'me':
  71. return this.me;
  72. case 'contact':
  73. return this.contacts;
  74. case 'group':
  75. return this.groups;
  76. case 'distributionList':
  77. return this.distributionLists;
  78. default:
  79. throw new Error('Unknown or invalid receiver type: ' + receiverType);
  80. }
  81. }
  82. /**
  83. * Get the receiver matching a certain template.
  84. */
  85. public getData(receiver: threema.BaseReceiver): threema.Receiver | null {
  86. if (receiver.type === 'me') {
  87. return this.me.id === receiver.id ? this.me : undefined;
  88. } else {
  89. const receivers = this.get(receiver.type) as Map<string, threema.Receiver>;
  90. return receivers.get(receiver.id);
  91. }
  92. }
  93. /**
  94. * Set receiver data.
  95. */
  96. public set(data: threema.Container.ReceiverData) {
  97. this.setMe(data['me' as threema.ReceiverType]);
  98. this.setContacts(data['contact' as threema.ReceiverType]);
  99. this.setGroups(data['group' as threema.ReceiverType]);
  100. this.setDistributionLists(data['distributionList' as threema.ReceiverType]);
  101. }
  102. /**
  103. * Set own contact.
  104. */
  105. public setMe(data: threema.MeReceiver): void {
  106. data.type = 'me';
  107. this.me = data;
  108. }
  109. /**
  110. * Set contacts.
  111. */
  112. public setContacts(data: threema.ContactReceiver[]): void {
  113. this.contacts = new Map(data.map((c) => {
  114. c.type = 'contact';
  115. return [c.id, c];
  116. }) as any) as ContactMap;
  117. if (this.me !== undefined) {
  118. this.contacts.set(this.me.id, this.me);
  119. }
  120. }
  121. /**
  122. * Set groups.
  123. */
  124. public setGroups(data: threema.GroupReceiver[]): void {
  125. this.groups = new Map(data.map((g) => {
  126. g.type = 'group';
  127. return [g.id, g];
  128. }) as any) as GroupMap;
  129. }
  130. /**
  131. * Set distribution lists.
  132. */
  133. public setDistributionLists(data: threema.DistributionListReceiver[]): void {
  134. this.distributionLists = new Map(data.map((d) => {
  135. d.type = 'distributionList';
  136. return [d.id, d];
  137. }) as any) as DistributionListMap;
  138. }
  139. public extend(receiverType: threema.ReceiverType, data: threema.Receiver): threema.Receiver {
  140. switch (receiverType) {
  141. case 'me':
  142. return this.extendMe(data as threema.MeReceiver);
  143. case 'contact':
  144. return this.extendContact(data as threema.ContactReceiver);
  145. case 'group':
  146. return this.extendGroup(data as threema.GroupReceiver);
  147. case 'distributionList':
  148. return this.extendDistributionList(data as threema.DistributionListReceiver);
  149. default:
  150. throw new Error('Unknown or invalid receiver type: ' + receiverType);
  151. }
  152. }
  153. public extendDistributionList(data: threema.DistributionListReceiver): threema.DistributionListReceiver {
  154. let distributionListReceiver = this.distributionLists.get(data.id);
  155. if (distributionListReceiver === undefined) {
  156. data.type = 'distributionList';
  157. this.distributionLists.set(data.id, data);
  158. return data;
  159. }
  160. // update existing object
  161. distributionListReceiver = angular.extend(distributionListReceiver, data);
  162. return distributionListReceiver;
  163. }
  164. public extendGroup(data: threema.GroupReceiver): threema.GroupReceiver {
  165. let groupReceiver = this.groups.get(data.id);
  166. if (groupReceiver === undefined) {
  167. data.type = 'group';
  168. this.groups.set(data.id, data);
  169. return data;
  170. }
  171. // update existing object
  172. groupReceiver = angular.extend(groupReceiver, data);
  173. return groupReceiver;
  174. }
  175. public extendMe(data: threema.MeReceiver): threema.MeReceiver {
  176. if (this.me === undefined) {
  177. data.type = 'me';
  178. this.me = data;
  179. return data;
  180. }
  181. // update existing object
  182. this.me = angular.extend(this.me, data);
  183. return this.me;
  184. }
  185. public extendContact(data: threema.ContactReceiver): threema.ContactReceiver {
  186. let contactReceiver = this.contacts.get(data.id);
  187. if (contactReceiver === undefined) {
  188. data.type = 'contact';
  189. this.contacts.set(data.id, data);
  190. return data;
  191. }
  192. // update existing object
  193. contactReceiver = angular.extend(contactReceiver, data);
  194. return contactReceiver;
  195. }
  196. }
  197. class Conversations implements threema.Container.Conversations {
  198. private conversations: threema.Conversation[] = [];
  199. public filter: ConversationFilter = null;
  200. private converter: ConversationConverter = null;
  201. /**
  202. * Get conversations.
  203. */
  204. public get(): threema.Conversation[] {
  205. let conversations = this.conversations;
  206. if (this.filter != null) {
  207. conversations = this.filter(conversations);
  208. }
  209. if (this.converter != null) {
  210. conversations = conversations.map(this.converter);
  211. }
  212. return conversations;
  213. }
  214. /**
  215. * Set conversations.
  216. */
  217. public set(data: threema.Conversation[]): void {
  218. data.forEach((existingConversation: threema.Conversation) => {
  219. this.updateOrAdd(existingConversation);
  220. });
  221. }
  222. /**
  223. * Find a stored conversation matching the given conversation or receiver.
  224. *
  225. * Comparison is done by type and id.
  226. */
  227. public find(pattern: threema.Conversation | threema.Receiver): threema.Conversation | null {
  228. for (const conversation of this.get()) {
  229. const a = pattern;
  230. const b = conversation;
  231. if (a !== undefined && b !== undefined && a.type === b.type && a.id === b.id) {
  232. return conversation;
  233. }
  234. }
  235. return null;
  236. }
  237. public add(conversation: threema.Conversation): void {
  238. this.conversations.splice(conversation.position, 0, conversation);
  239. }
  240. public updateOrAdd(conversation: threema.Conversation): void {
  241. let moveDirection = 0;
  242. let updated = false;
  243. for (const p of this.conversations.keys()) {
  244. if (receiverService.compare(this.conversations[p], conversation)) {
  245. // ok, replace me and break
  246. const old = this.conversations[p];
  247. if (old.position !== conversation.position) {
  248. // position also changed...
  249. moveDirection = old.position > conversation.position ? -1 : 1;
  250. }
  251. this.conversations[p] = conversation;
  252. updated = true;
  253. }
  254. }
  255. // reset the position field to correct the sorting
  256. if (moveDirection !== 0) {
  257. // reindex
  258. let before = true;
  259. for (const p in this.conversations) {
  260. if (receiverService.compare(this.conversations[p], conversation)) {
  261. before = false;
  262. } else if (before && moveDirection < 0) {
  263. this.conversations[p].position++;
  264. } else if (!before && moveDirection > 0) {
  265. this.conversations[p].position++;
  266. }
  267. }
  268. // sort by position field
  269. this.conversations.sort(function(convA, convB) {
  270. return convA.position - convB.position;
  271. });
  272. } else if (!updated) {
  273. this.add(conversation);
  274. }
  275. }
  276. public remove(conversation: threema.Conversation): void {
  277. for (const p of this.conversations.keys()) {
  278. if (receiverService.compare(this.conversations[p], conversation)) {
  279. // remove conversation from array
  280. this.conversations.splice(p, 1);
  281. return;
  282. }
  283. }
  284. }
  285. /**
  286. * Set a filter.
  287. */
  288. public setFilter(filter: ConversationFilter): void {
  289. this.filter = filter;
  290. }
  291. /**
  292. * Set a converter.
  293. */
  294. public setConverter(converter: ConversationConverter): void {
  295. this.converter = converter;
  296. }
  297. }
  298. /**
  299. * Messages between local user and a receiver.
  300. */
  301. class ReceiverMessages {
  302. // The message id used as reference when paging.
  303. public referenceMsgId: number = null;
  304. // Whether a message request has been sent yet.
  305. public requested = false;
  306. // This flag indicates that more (older) messages are available.
  307. public more = true;
  308. // List of messages.
  309. public list: threema.Message[] = [];
  310. }
  311. /**
  312. * This class manages all messages for the current user.
  313. */
  314. class Messages implements threema.Container.Messages {
  315. // The messages are stored in date-ascending order,
  316. // newest messages are appended, older messages are prepended.
  317. private messages: MessageMap = new Map();
  318. // Message converter
  319. public converter: MessageConverter = null;
  320. /**
  321. * Ensure that the receiver exists in the receiver map.
  322. */
  323. private lazyCreate(receiver: threema.Receiver): void {
  324. // If the type is not yet known, create a new type map.
  325. if (!this.messages.has(receiver.type)) {
  326. this.messages.set(receiver.type, new Map());
  327. }
  328. // If the receiver is not yet known, initialize it.
  329. const typeMap = this.messages.get(receiver.type);
  330. if (!typeMap.has(receiver.id)) {
  331. typeMap.set(receiver.id, new ReceiverMessages());
  332. }
  333. }
  334. /**
  335. * Return the `ReceiverMessages` instance for the specified receiver.
  336. *
  337. * If the receiver is not known yet, it is initialized.
  338. */
  339. private getReceiverMessages(receiver: threema.Receiver): ReceiverMessages {
  340. this.lazyCreate(receiver);
  341. return this.messages.get(receiver.type).get(receiver.id);
  342. }
  343. /**
  344. * Return the list of messages for the specified receiver.
  345. *
  346. * If the receiver is not known yet, it is initialized with an empty
  347. * message list.
  348. */
  349. public getList(receiver: threema.Receiver): threema.Message[] {
  350. return this.getReceiverMessages(receiver).list;
  351. }
  352. /**
  353. * Clear and reset all loaded messages but do not remove objects
  354. * @param $scope
  355. */
  356. public clear($scope: ng.IScope): void {
  357. this.messages.forEach((messageMap: Map<string, ReceiverMessages>, receiverType: threema.ReceiverType) => {
  358. messageMap.forEach((messages: ReceiverMessages, id: string) => {
  359. messages.requested = false;
  360. messages.referenceMsgId = null;
  361. messages.more = true;
  362. messages.list = [];
  363. this.notify({
  364. id: id,
  365. type: receiverType,
  366. } as threema.Receiver, $scope);
  367. });
  368. });
  369. }
  370. /**
  371. * Reset the cached messages of a receiver (e.g. the receiver was locked by the mobile)
  372. */
  373. public clearReceiverMessages(receiver: threema.Receiver): number {
  374. let cachedMessageCount = 0;
  375. if (this.messages.has(receiver.type)) {
  376. const typeMessages = this.messages.get(receiver.type);
  377. if (typeMessages.has(receiver.id)) {
  378. cachedMessageCount = typeMessages.get(receiver.id).list.length;
  379. typeMessages.delete(receiver.id);
  380. }
  381. }
  382. return cachedMessageCount;
  383. }
  384. /**
  385. * Return whether messages from/for the specified receiver are available.
  386. */
  387. public contains(receiver: threema.Receiver): boolean {
  388. return this.messages.has(receiver.type) &&
  389. this.messages.get(receiver.type).has(receiver.id);
  390. }
  391. /**
  392. * Return whether there are more (older) messages available to fetch
  393. * for the specified receiver.
  394. */
  395. public hasMore(receiver: threema.Receiver): boolean {
  396. return this.getReceiverMessages(receiver).more;
  397. }
  398. /**
  399. * Set the "more" flag for the specified receiver.
  400. *
  401. * The flag indicates that more (older) messages are available.
  402. */
  403. public setMore(receiver: threema.Receiver, more: boolean): void {
  404. this.getReceiverMessages(receiver).more = more;
  405. }
  406. /**
  407. * Return the reference msg id for the specified receiver.
  408. */
  409. public getReferenceMsgId(receiver: threema.Receiver): number {
  410. return this.getReceiverMessages(receiver).referenceMsgId;
  411. }
  412. /**
  413. * Return whether the messages for the specified receiver are already
  414. * requested.
  415. */
  416. public isRequested(receiver: threema.Receiver): boolean {
  417. return this.getReceiverMessages(receiver).requested;
  418. }
  419. /**
  420. * Set the requested flag for the specified receiver.
  421. */
  422. public setRequested(receiver: threema.Receiver): void {
  423. const messages = this.getReceiverMessages(receiver);
  424. // If a request was already pending, this must be a bug.
  425. if (messages.requested) {
  426. throw new Error('Message request for receiver ' + receiver.id + ' still pending');
  427. }
  428. // Set requested
  429. messages.requested = true;
  430. }
  431. /**
  432. * Clear the "requested" flag for the specified receiver messages.
  433. */
  434. public clearRequested(receiver): void {
  435. const messages = this.getReceiverMessages(receiver);
  436. messages.requested = false;
  437. }
  438. /**
  439. * Append newer messages.
  440. *
  441. * Messages must be sorted ascending by date.
  442. */
  443. public addNewer(receiver: threema.Receiver, messages: threema.Message[]): void {
  444. if (messages.length === 0) {
  445. // do nothing
  446. return;
  447. }
  448. const receiverMessages = this.getReceiverMessages(receiver);
  449. // if the list is empty, add the current message as ref
  450. if (receiverMessages.list.length === 0) {
  451. receiverMessages.referenceMsgId = messages[0].id;
  452. }
  453. receiverMessages.list.push.apply(receiverMessages.list, messages);
  454. }
  455. /**
  456. * Prepend older messages.
  457. *
  458. * Messages must be sorted ascending by date (oldest first).
  459. */
  460. public addOlder(receiver: threema.Receiver, messages: threema.Message[]): void {
  461. if (messages.length === 0) {
  462. // do nothing
  463. return;
  464. }
  465. // Get reference to message list for the specified receiver
  466. const receiverMessages = this.getReceiverMessages(receiver);
  467. // If the first or last message is already contained in the list,
  468. // do nothing.
  469. const firstId = messages[0].id;
  470. const lastId = messages[messages.length - 1].id;
  471. const predicate = (msg: threema.Message) => msg.id === firstId || msg.id === lastId;
  472. if (receiverMessages.list.findIndex(predicate, receiverMessages.list) !== -1) {
  473. $log.warn('Messages to be prepended intersect with existing messages:', messages);
  474. return;
  475. }
  476. // Add the oldest message as ref
  477. receiverMessages.referenceMsgId = messages[0].id;
  478. receiverMessages.list.unshift.apply(receiverMessages.list, messages);
  479. }
  480. /**
  481. * Update/replace a message with a newer version.
  482. *
  483. * Return a boolean indicating whether the message was found and
  484. * replaced, or not.
  485. */
  486. public update(receiver: threema.Receiver, message: threema.Message): boolean {
  487. const list = this.getList(receiver);
  488. for (let i = 0; i < list.length; i++) {
  489. if (list[i].id === message.id) {
  490. if (message.thumbnail === undefined) {
  491. // do not reset the thumbnail
  492. message.thumbnail = list[i].thumbnail;
  493. }
  494. list[i] = message;
  495. return true;
  496. }
  497. }
  498. return false;
  499. }
  500. /**
  501. * Update a thumbnail of a message, if a message was found the method will return true
  502. *
  503. * @param receiver
  504. * @param messageId
  505. * @param thumbnailImage
  506. * @returns {boolean}
  507. */
  508. public setThumbnail(receiver: threema.Receiver, messageId: number, thumbnailImage: string): boolean {
  509. const list = this.getList(receiver);
  510. for (const message of list) {
  511. if (message.id === messageId) {
  512. if (message.thumbnail === undefined) {
  513. message.thumbnail = {img: thumbnailImage} as threema.Thumbnail;
  514. } else {
  515. message.thumbnail.img = thumbnailImage;
  516. }
  517. return true;
  518. }
  519. }
  520. return false;
  521. }
  522. /**
  523. * Remove a message.
  524. *
  525. * Return a boolean indicating whether the message was found and
  526. * removed, or not.
  527. */
  528. public remove(receiver: threema.Receiver, messageId: number): boolean {
  529. const list = this.getList(receiver);
  530. for (let i = 0; i < list.length; i++) {
  531. if (list[i].id === messageId) {
  532. list.splice(i, 1);
  533. return true;
  534. }
  535. }
  536. return false;
  537. }
  538. /**
  539. * Remove a message.
  540. *
  541. * Return a boolean indicating whether the message was found and
  542. * removed, or not.
  543. */
  544. public removeTemporary(receiver: threema.Receiver, temporaryMessageId: string): boolean {
  545. const list = this.getList(receiver);
  546. for (let i = 0; i < list.length; i++) {
  547. if (list[i].temporaryId === temporaryMessageId) {
  548. list.splice(i, 1);
  549. return true;
  550. }
  551. }
  552. return false;
  553. }
  554. public bindTemporaryToMessageId(receiver: threema.Receiver, temporaryId: string, messageId: number): boolean {
  555. const list = this.getList(receiver);
  556. for (const item of list) {
  557. if (item.temporaryId === temporaryId) {
  558. if (item.id !== undefined) {
  559. // do not bind to a new message id
  560. return false;
  561. }
  562. // reset temporary id
  563. item.temporaryId = null;
  564. // assign to "real" message id
  565. item.id = messageId;
  566. return true;
  567. }
  568. }
  569. return false;
  570. }
  571. public notify(receiver: threema.Receiver, $scope: ng.IScope) {
  572. $scope.$broadcast('threema.receiver.' + receiver.type + '.' + receiver.id + '.messages',
  573. this.getList(receiver), this.hasMore(receiver));
  574. }
  575. /**
  576. * register a message change notify on the given scope
  577. * return the CURRENT list of loaded messages
  578. */
  579. public register(receiver: threema.Receiver, $scope: ng.IScope, callback: any): threema.Message[] {
  580. $scope.$on('threema.receiver.' + receiver.type + '.' + receiver.id + '.messages', callback);
  581. return this.getList(receiver);
  582. }
  583. public updateFirstUnreadMessage(receiver: threema.Receiver): void {
  584. const receiverMessages = this.getReceiverMessages(receiver);
  585. if (receiverMessages !== undefined && receiverMessages.list.length > 0) {
  586. // remove unread
  587. let removedElements = 0;
  588. let firstUnreadMessageIndex;
  589. receiverMessages.list = receiverMessages.list.filter((message: threema.Message, index: number) => {
  590. if (message.type === 'status'
  591. && message.statusType === 'firstUnreadMessage') {
  592. removedElements++;
  593. return false;
  594. } else if (firstUnreadMessageIndex === undefined
  595. && !message.isOutbox
  596. && message.unread) {
  597. firstUnreadMessageIndex = index;
  598. }
  599. return true;
  600. });
  601. if (firstUnreadMessageIndex !== undefined) {
  602. firstUnreadMessageIndex -= removedElements;
  603. receiverMessages.list.splice(firstUnreadMessageIndex, 0 , {
  604. type: 'status',
  605. isStatus: true,
  606. statusType: 'firstUnreadMessage',
  607. } as threema.Message);
  608. }
  609. }
  610. }
  611. }
  612. /**
  613. * Converters transform a message or conversation.
  614. */
  615. class Converter {
  616. public static unicodeToEmoji(message: threema.Message) {
  617. if (message.type === 'text') {
  618. message.body = emojione.toShort(message.body);
  619. }
  620. return message;
  621. }
  622. /**
  623. * Retrieve the receiver corresponding to this conversation and set the
  624. * `receiver` attribute.
  625. */
  626. public static addReceiverToConversation(receivers: Receivers) {
  627. return (conversation: threema.Conversation): threema.Conversation => {
  628. conversation.receiver = receivers.getData({
  629. type: conversation.type,
  630. id: conversation.id,
  631. } as threema.Receiver);
  632. return conversation;
  633. };
  634. }
  635. }
  636. class Filters {
  637. public static hasData(receivers) {
  638. return (obj) => $filter('hasData')(obj, receivers);
  639. }
  640. public static hasContact(contacts) {
  641. return (obj) => $filter('hasContact')(obj, contacts);
  642. }
  643. public static isValidMessage(contacts) {
  644. return (obj) => $filter('isValidMessage')(obj, contacts);
  645. }
  646. }
  647. /**
  648. * This class manages the typing flags for receivers.
  649. *
  650. * Internally values are stored in a hash set for efficient lookup.
  651. */
  652. class Typing implements threema.Container.Typing {
  653. private set = new StringHashSet();
  654. private getReceiverUid(receiver: threema.ContactReceiver): string {
  655. return receiver.type + '-' + receiver.id;
  656. }
  657. public setTyping(receiver: threema.ContactReceiver): void {
  658. this.set.add(this.getReceiverUid(receiver));
  659. }
  660. public unsetTyping(receiver: threema.ContactReceiver): void {
  661. this.set.remove(this.getReceiverUid(receiver));
  662. }
  663. public isTyping(receiver: threema.ContactReceiver): boolean {
  664. return this.set.contains(this.getReceiverUid(receiver));
  665. }
  666. }
  667. /**
  668. * Holds message drafts and quotes
  669. */
  670. class Drafts implements threema.Container.Drafts {
  671. private quotes = new Map<string, threema.Quote>();
  672. // Use to implement draft texts!
  673. private texts = new Map<string, string>();
  674. private getReceiverUid(receiver: threema.Receiver): string {
  675. // do not use receiver.type => can be null
  676. return receiver.id;
  677. }
  678. public setQuote(receiver: threema.Receiver, quote: threema.Quote): void {
  679. this.quotes.set(this.getReceiverUid(receiver), quote);
  680. }
  681. public removeQuote(receiver: threema.Receiver): void {
  682. this.quotes.delete(this.getReceiverUid(receiver));
  683. }
  684. public getQuote(receiver: threema.Receiver): threema.Quote {
  685. return this.quotes.get(this.getReceiverUid(receiver));
  686. }
  687. public setText(receiver: threema.Receiver, draftMessage: string): void {
  688. this.texts.set(this.getReceiverUid(receiver), draftMessage);
  689. }
  690. public removeText(receiver: threema.Receiver): void {
  691. this.texts.delete(this.getReceiverUid(receiver));
  692. }
  693. public getText(receiver: threema.Receiver): string {
  694. return this.texts.get(this.getReceiverUid(receiver));
  695. }
  696. }
  697. return {
  698. Converter: Converter as threema.Container.Converter,
  699. Filters: Filters as threema.Container.Filters,
  700. createReceivers: () => new Receivers(),
  701. createConversations: () => new Conversations(),
  702. createMessages: () => new Messages(),
  703. createTyping: () => new Typing(),
  704. createDrafts: () => new Drafts(),
  705. } as threema.Container.Factory;
  706. }]);