123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793 |
- /**
- * This file is part of Threema Web.
- *
- * Threema Web is free software: you can redistribute it and/or modify it
- * under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or (at
- * your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
- * General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
- */
- import {ReceiverService} from '../services/receiver';
- /**
- * A simple HashSet implementation based on a JavaScript object.
- *
- * Only strings can be stored in this set.
- */
- class StringHashSet {
- private setObj = {};
- private val = {};
- public add(str: string): void {
- this.setObj[str] = this.val;
- }
- public contains(str: string): boolean {
- return this.setObj[str] === this.val;
- }
- public remove(str: string): void {
- delete this.setObj[str];
- }
- public values() {
- let values = [];
- for (let i in this.setObj) {
- if (this.setObj[i] === this.val) {
- values.push(i);
- }
- }
- return values;
- }
- }
- angular.module('3ema.container', [])
- .factory('Container', ['$filter', '$log', 'ReceiverService',
- function($filter, $log, receiverService: ReceiverService) {
- type ContactMap = Map<string, threema.ContactReceiver>;
- type GroupMap = Map<string, threema.GroupReceiver>;
- type DistributionListMap = Map<string, threema.DistributionListReceiver>;
- type MessageMap = Map<threema.ReceiverType, Map<string, ReceiverMessages>>;
- type ConversationFilter = (data: threema.Conversation[]) => threema.Conversation[];
- type ConversationConverter = (data: threema.Conversation) => threema.Conversation;
- type MessageConverter = (data: threema.Message) => threema.Message;
- /**
- * Collection that manages receivers like contacts, groups or distribution lists.
- *
- * Think of it like the "address book".
- */
- class Receivers implements threema.Container.Receivers {
- public me: threema.MeReceiver = null;
- public contacts: ContactMap = new Map();
- public groups: GroupMap = new Map();
- public distributionLists: DistributionListMap = new Map();
- /**
- * Get the receiver map for the specified type.
- */
- public get(receiverType: threema.ReceiverType): threema.Receiver | Map<string, threema.Receiver> {
- switch (receiverType) {
- case 'me':
- return this.me;
- case 'contact':
- return this.contacts;
- case 'group':
- return this.groups;
- case 'distributionList':
- return this.distributionLists;
- default:
- throw new Error('Unknown or invalid receiver type: ' + receiverType);
- }
- }
- /**
- * Get the receiver matching a certain template.
- */
- public getData(receiver: threema.BaseReceiver): threema.Receiver | null {
- if (receiver.type === 'me') {
- return this.me.id === receiver.id ? this.me : undefined;
- } else {
- const receivers = this.get(receiver.type) as Map<string, threema.Receiver>;
- return receivers.get(receiver.id);
- }
- }
- /**
- * Set receiver data.
- */
- public set(data: threema.Container.ReceiverData) {
- this.setMe(data['me' as threema.ReceiverType]);
- this.setContacts(data['contact' as threema.ReceiverType]);
- this.setGroups(data['group' as threema.ReceiverType]);
- this.setDistributionLists(data['distributionList' as threema.ReceiverType]);
- }
- /**
- * Set own contact.
- */
- public setMe(data: threema.MeReceiver): void {
- data.type = 'me';
- this.me = data;
- }
- /**
- * Set contacts.
- */
- public setContacts(data: threema.ContactReceiver[]): void {
- this.contacts = new Map(data.map((c) => {
- c.type = 'contact';
- return [c.id, c];
- }) as any) as ContactMap;
- if (this.me !== undefined) {
- this.contacts.set(this.me.id, this.me);
- }
- }
- /**
- * Set groups.
- */
- public setGroups(data: threema.GroupReceiver[]): void {
- this.groups = new Map(data.map((g) => {
- g.type = 'group';
- return [g.id, g];
- }) as any) as GroupMap;
- }
- /**
- * Set distribution lists.
- */
- public setDistributionLists(data: threema.DistributionListReceiver[]): void {
- this.distributionLists = new Map(data.map((d) => {
- d.type = 'distributionList';
- return [d.id, d];
- }) as any) as DistributionListMap;
- }
- public extend(receiverType: threema.ReceiverType, data: threema.Receiver): threema.Receiver {
- switch (receiverType) {
- case 'me':
- return this.extendMe(data as threema.MeReceiver);
- case 'contact':
- return this.extendContact(data as threema.ContactReceiver);
- case 'group':
- return this.extendGroup(data as threema.GroupReceiver);
- case 'distributionList':
- return this.extendDistributionList(data as threema.DistributionListReceiver);
- default:
- throw new Error('Unknown or invalid receiver type: ' + receiverType);
- }
- }
- public extendDistributionList(data: threema.DistributionListReceiver): threema.DistributionListReceiver {
- let distributionListReceiver = this.distributionLists.get(data.id);
- if (distributionListReceiver === undefined) {
- data.type = 'distributionList';
- this.distributionLists.set(data.id, data);
- return data;
- }
- // update existing object
- distributionListReceiver = angular.extend(distributionListReceiver, data);
- return distributionListReceiver;
- }
- public extendGroup(data: threema.GroupReceiver): threema.GroupReceiver {
- let groupReceiver = this.groups.get(data.id);
- if (groupReceiver === undefined) {
- data.type = 'group';
- this.groups.set(data.id, data);
- return data;
- }
- // update existing object
- groupReceiver = angular.extend(groupReceiver, data);
- return groupReceiver;
- }
- public extendMe(data: threema.MeReceiver): threema.MeReceiver {
- if (this.me === undefined) {
- data.type = 'me';
- this.me = data;
- return data;
- }
- // update existing object
- this.me = angular.extend(this.me, data);
- return this.me;
- }
- public extendContact(data: threema.ContactReceiver): threema.ContactReceiver {
- let contactReceiver = this.contacts.get(data.id);
- if (contactReceiver === undefined) {
- data.type = 'contact';
- this.contacts.set(data.id, data);
- return data;
- }
- // update existing object
- contactReceiver = angular.extend(contactReceiver, data);
- return contactReceiver;
- }
- }
- class Conversations implements threema.Container.Conversations {
- private conversations: threema.Conversation[] = [];
- public filter: ConversationFilter = null;
- private converter: ConversationConverter = null;
- /**
- * Get conversations.
- */
- public get(): threema.Conversation[] {
- let conversations = this.conversations;
- if (this.filter != null) {
- conversations = this.filter(conversations);
- }
- if (this.converter != null) {
- conversations = conversations.map(this.converter);
- }
- return conversations;
- }
- /**
- * Set conversations.
- */
- public set(data: threema.Conversation[]): void {
- data.forEach((existingConversation: threema.Conversation) => {
- this.updateOrAdd(existingConversation);
- });
- }
- public add(conversation: threema.Conversation): void {
- this.conversations.splice(conversation.position, 0, conversation);
- }
- public updateOrAdd(conversation: threema.Conversation): void {
- let moveDirection = 0;
- let updated = false;
- for (let p of this.conversations.keys()) {
- if (receiverService.compare(this.conversations[p], conversation)) {
- // ok, replace me and break
- let old = this.conversations[p];
- if (old.position !== conversation.position) {
- // position also changed...
- moveDirection = old.position > conversation.position ? -1 : 1;
- }
- this.conversations[p] = conversation;
- updated = true;
- }
- }
- // reset the position field to correct the sorting
- if (moveDirection !== 0) {
- // reindex
- let before = true;
- for (let p in this.conversations) {
- if (receiverService.compare(this.conversations[p], conversation)) {
- before = false;
- } else if (before && moveDirection < 0) {
- this.conversations[p].position++;
- } else if (!before && moveDirection > 0) {
- this.conversations[p].position++;
- }
- }
- // sort by position field
- this.conversations.sort(function (convA, convB) {
- return convA.position - convB.position;
- });
- } else if (!updated) {
- this.add(conversation);
- }
- }
- public remove(conversation: threema.Conversation): void {
- for (let p of this.conversations.keys()) {
- if (receiverService.compare(this.conversations[p], conversation)) {
- // remove conversation from array
- this.conversations.splice(p, 1);
- return;
- }
- }
- }
- /**
- * Set a filter.
- */
- public setFilter(filter: ConversationFilter): void {
- this.filter = filter;
- }
- /**
- * Set a converter.
- */
- public setConverter(converter: ConversationConverter): void {
- this.converter = converter;
- }
- }
- /**
- * Messages between local user and a receiver.
- */
- class ReceiverMessages {
- // The message id used as reference when paging.
- public referenceMsgId: number = null;
- // Whether a message request has been sent yet.
- public requested = false;
- // This flag indicates that more (older) messages are available.
- public more = true;
- // List of messages.
- public list: threema.Message[] = [];
- }
- /**
- * This class manages all messages for the current user.
- */
- class Messages implements threema.Container.Messages {
- // The messages are stored in date-ascending order,
- // newest messages are appended, older messages are prepended.
- private messages: MessageMap = new Map();
- // Message converter
- public converter: MessageConverter = null;
- /**
- * Ensure that the receiver exists in the receiver map.
- */
- private lazyCreate(receiver: threema.Receiver): void {
- // If the type is not yet known, create a new type map.
- if (!this.messages.has(receiver.type)) {
- this.messages.set(receiver.type, new Map());
- }
- // If the receiver is not yet known, initialize it.
- const typeMap = this.messages.get(receiver.type);
- if (!typeMap.has(receiver.id)) {
- typeMap.set(receiver.id, new ReceiverMessages());
- }
- }
- /**
- * Return the `ReceiverMessages` instance for the specified receiver.
- *
- * If the receiver is not known yet, it is initialized.
- */
- private getReceiverMessages(receiver: threema.Receiver): ReceiverMessages {
- this.lazyCreate(receiver);
- return this.messages.get(receiver.type).get(receiver.id);
- }
- /**
- * Return the list of messages for the specified receiver.
- *
- * If the receiver is not known yet, it is initialized with an empty
- * message list.
- */
- public getList(receiver: threema.Receiver): threema.Message[] {
- return this.getReceiverMessages(receiver).list;
- }
- /**
- * Clear and reset all loaded messages but do not remove objects
- * @param $scope
- */
- public clear($scope: ng.IScope): void {
- this.messages.forEach ((messageMap: Map<string, ReceiverMessages>,
- receiverType: threema.ReceiverType) => {
- messageMap.forEach ((messages: ReceiverMessages, id: string) => {
- messages.requested = false;
- messages.referenceMsgId = null;
- messages.more = true;
- messages.list = [];
- this.notify({
- id: id,
- type: receiverType,
- } as threema.Receiver, $scope);
- });
- });
- }
- /**
- * Reset the cached messages of a receiver (e.g. the receiver was locked by the mobile)
- */
- public clearReceiverMessages(receiver: threema.Receiver): Number {
- let cachedMessageCount = 0;
- if (this.messages.has(receiver.type)) {
- let typeMessages = this.messages.get(receiver.type);
- if (typeMessages.has(receiver.id)) {
- cachedMessageCount = typeMessages.get(receiver.id).list.length;
- typeMessages.delete(receiver.id);
- }
- }
- return cachedMessageCount;
- }
- /**
- * Return whether messages from/for the specified receiver are available.
- */
- public contains(receiver: threema.Receiver): boolean {
- return this.messages.has(receiver.type) &&
- this.messages.get(receiver.type).has(receiver.id);
- }
- /**
- * Return whether there are more (older) messages available to fetch
- * for the specified receiver.
- */
- public hasMore(receiver: threema.Receiver): boolean {
- return this.getReceiverMessages(receiver).more;
- }
- /**
- * Set the "more" flag for the specified receiver.
- *
- * The flag indicates that more (older) messages are available.
- */
- public setMore(receiver: threema.Receiver, more: boolean): void {
- this.getReceiverMessages(receiver).more = more;
- }
- /**
- * Return the reference msg id for the specified receiver.
- */
- public getReferenceMsgId(receiver: threema.Receiver): number {
- return this.getReceiverMessages(receiver).referenceMsgId;
- }
- /**
- * Return whether the messages for the specified receiver are already
- * requested.
- */
- public isRequested(receiver: threema.Receiver): boolean {
- return this.getReceiverMessages(receiver).requested;
- }
- /**
- * Set the requested flag for the specified receiver.
- */
- public setRequested(receiver: threema.Receiver): void {
- const messages = this.getReceiverMessages(receiver);
- // If a request was already pending, this must be a bug.
- if (messages.requested) {
- throw new Error('Message request for receiver ' + receiver.id + ' still pending');
- }
- // Set requested
- messages.requested = true;
- }
- /**
- * Clear the "requested" flag for the specified receiver messages.
- */
- public clearRequested(receiver): void {
- const messages = this.getReceiverMessages(receiver);
- messages.requested = false;
- }
- /**
- * Append newer messages.
- *
- * Messages must be sorted ascending by date.
- */
- public addNewer(receiver: threema.Receiver, messages: threema.Message[]): void {
- if (messages.length === 0) {
- // do nothing
- return;
- }
- const receiverMessages = this.getReceiverMessages(receiver);
- // if the list is empty, add the current message as ref
- if (receiverMessages.list.length === 0) {
- receiverMessages.referenceMsgId = messages[0].id;
- }
- receiverMessages.list.push.apply(receiverMessages.list, messages);
- }
- /**
- * Prepend older messages.
- *
- * Messages must be sorted ascending by date (oldest first).
- */
- public addOlder(receiver: threema.Receiver, messages: threema.Message[]): void {
- if (messages.length === 0) {
- // do nothing
- return;
- }
- // Get reference to message list for the specified receiver
- const receiverMessages = this.getReceiverMessages(receiver);
- // If the first or last message is already contained in the list,
- // do nothing.
- const firstId = messages[0].id;
- const lastId = messages[messages.length - 1].id;
- const predicate = (msg: threema.Message) => msg.id === firstId || msg.id === lastId;
- if (receiverMessages.list.findIndex(predicate, receiverMessages.list) !== -1) {
- $log.warn('Messages to be prepended intersect with existing messages:', messages);
- return;
- }
- // Add the oldest message as ref
- receiverMessages.referenceMsgId = messages[0].id;
- receiverMessages.list.unshift.apply(receiverMessages.list, messages);
- }
- /**
- * Update/replace a message with a newer version.
- *
- * Return a boolean indicating whether the message was found and
- * replaced, or not.
- */
- public update(receiver: threema.Receiver, message: threema.Message): boolean {
- const list = this.getList(receiver);
- for (let i = 0; i < list.length; i++) {
- if (list[i].id === message.id) {
- if (message.thumbnail === undefined) {
- // do not reset the thumbnail
- message.thumbnail = list[i].thumbnail;
- }
- list[i] = message;
- return true;
- }
- }
- return false;
- }
- /**
- * Update a thumbnail of a message, if a message was found the method will return true
- *
- * @param receiver
- * @param messageId
- * @param thumbnailImage
- * @returns {boolean}
- */
- public setThumbnail(receiver: threema.Receiver, messageId: number, thumbnailImage: string): boolean {
- const list = this.getList(receiver);
- for (let message of list) {
- if (message.id === messageId) {
- if (message.thumbnail === undefined) {
- message.thumbnail = {img: thumbnailImage} as threema.Thumbnail;
- } else {
- message.thumbnail.img = thumbnailImage;
- }
- return true;
- }
- }
- return false;
- }
- /**
- * Remove a message.
- *
- * Return a boolean indicating whether the message was found and
- * removed, or not.
- */
- public remove(receiver: threema.Receiver, messageId: number): boolean {
- const list = this.getList(receiver);
- for (let i = 0; i < list.length; i++) {
- if (list[i].id === messageId) {
- list.splice(i, 1);
- return true;
- }
- }
- return false;
- }
- /**
- * Remove a message.
- *
- * Return a boolean indicating whether the message was found and
- * removed, or not.
- */
- public removeTemporary(receiver: threema.Receiver, temporaryMessageId: string): boolean {
- const list = this.getList(receiver);
- for (let i = 0; i < list.length; i++) {
- if (list[i].temporaryId === temporaryMessageId) {
- list.splice(i, 1);
- return true;
- }
- }
- return false;
- }
- public bindTemporaryToMessageId(receiver: threema.Receiver, temporaryId: string, messageId: number): boolean {
- const list = this.getList(receiver);
- for (let item of list) {
- if (item.temporaryId === temporaryId) {
- if (item.id !== undefined) {
- // do not bind to a new message id
- return false;
- }
- // reset temporary id
- item.temporaryId = null;
- // assign to "real" message id
- item.id = messageId;
- return true;
- }
- }
- return false;
- }
- public notify(receiver: threema.Receiver, $scope: ng.IScope) {
- $scope.$broadcast('threema.receiver.' + receiver.type + '.' + receiver.id + '.messages',
- this.getList(receiver), this.hasMore(receiver));
- }
- /**
- * register a message change notify on the given scope
- * return the CURRENT list of loaded messages
- */
- public register(receiver: threema.Receiver, $scope: ng.IScope, callback: any): threema.Message[] {
- $scope.$on('threema.receiver.' + receiver.type + '.' + receiver.id + '.messages', callback);
- return this.getList(receiver);
- }
- public updateFirstUnreadMessage(receiver: threema.Receiver): void {
- const receiverMessages = this.getReceiverMessages(receiver);
- if (receiverMessages !== undefined && receiverMessages.list.length > 0) {
- // remove unread
- let removedElements = 0;
- let firstUnreadMessageIndex;
- receiverMessages.list = receiverMessages.list.filter((message: threema.Message, index: number) => {
- if (message.type === 'status'
- && message.statusType === 'firstUnreadMessage') {
- removedElements++;
- return false;
- } else if (firstUnreadMessageIndex === undefined
- && !message.isOutbox
- && message.unread) {
- firstUnreadMessageIndex = index;
- }
- return true;
- });
- if (firstUnreadMessageIndex !== undefined) {
- firstUnreadMessageIndex -= removedElements;
- receiverMessages.list.splice(firstUnreadMessageIndex, 0 , {
- type: 'status',
- isStatus: true,
- statusType: 'firstUnreadMessage',
- } as threema.Message);
- }
- }
- }
- }
- /**
- * Converters transform a message or conversation.
- */
- class Converter {
- public static unicodeToEmoji(message: threema.Message) {
- if (message.type === 'text') {
- message.body = emojione.toShort(message.body);
- }
- return message;
- }
- /**
- * Retrieve the receiver corresponding to this conversation and set the
- * `receiver` attribute.
- */
- public static addReceiverToConversation(receivers: Receivers) {
- return (conversation: threema.Conversation): threema.Conversation => {
- conversation.receiver = receivers.getData({
- type: conversation.type,
- id: conversation.id,
- } as threema.Receiver);
- return conversation;
- };
- }
- }
- class Filters {
- public static hasData(receivers) {
- return (obj) => $filter('hasData')(obj, receivers);
- }
- public static hasContact(contacts) {
- return (obj) => $filter('hasContact')(obj, contacts);
- }
- public static isValidMessage(contacts) {
- return (obj) => $filter('isValidMessage')(obj, contacts);
- }
- }
- /**
- * This class manages the typing flags for receivers.
- *
- * Internally values are stored in a hash set for efficient lookup.
- */
- class Typing implements threema.Container.Typing {
- private set = new StringHashSet();
- private getReceiverUid(receiver: threema.ContactReceiver): string {
- return receiver.type + '-' + receiver.id;
- }
- public setTyping(receiver: threema.ContactReceiver): void {
- this.set.add(this.getReceiverUid(receiver));
- }
- public unsetTyping(receiver: threema.ContactReceiver): void {
- this.set.remove(this.getReceiverUid(receiver));
- }
- public isTyping(receiver: threema.ContactReceiver): boolean {
- return this.set.contains(this.getReceiverUid(receiver));
- }
- }
- /**
- * Holds message drafts and quotes
- */
- class Drafts implements threema.Container.Drafts {
- private quotes = new Map<String, threema.Quote>();
- // Use to implement draft texts!
- private texts = new Map<string, string>();
- private getReceiverUid(receiver: threema.Receiver): string {
- // do not use receiver.type => can be null
- return receiver.id;
- }
- public setQuote(receiver: threema.Receiver, quote: threema.Quote): void {
- this.quotes.set(this.getReceiverUid(receiver), quote);
- }
- public removeQuote(receiver: threema.Receiver): void {
- this.quotes.delete(this.getReceiverUid(receiver));
- }
- public getQuote(receiver: threema.Receiver): threema.Quote {
- return this.quotes.get(this.getReceiverUid(receiver));
- }
- public setText(receiver: threema.Receiver, draftMessage: string): void {
- this.texts.set(this.getReceiverUid(receiver), draftMessage);
- }
- public removeText(receiver: threema.Receiver): void {
- this.texts.delete(this.getReceiverUid(receiver));
- }
- public getText(receiver: threema.Receiver): string {
- return this.texts.get(this.getReceiverUid(receiver));
- }
- }
- return {
- Converter: Converter as threema.Container.Converter,
- Filters: Filters as threema.Container.Filters,
- createReceivers: () => new Receivers(),
- createConversations: () => new Conversations(),
- createMessages: () => new Messages(),
- createTyping: () => new Typing(),
- createDrafts: () => new Drafts(),
- } as threema.Container.Factory;
- }]);
|