container.ts 28 KB

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