import { getAllConversations, getAllGroupsInvolvingId, removeConversation, saveConversation, } from '../../../ts/data/data'; import { ConversationAttributes, ConversationCollection, ConversationModel, } from '../../models/conversation'; import { BlockedNumberController } from '../../util'; import { getSnodesFor } from '../snode_api/snodePool'; import { PubKey } from '../types'; import { UserUtils } from '../utils'; import { actions as conversationActions } from '../../state/ducks/conversations'; export class ConversationController { private static instance: ConversationController | null; private readonly conversations: any; private _initialFetchComplete: boolean = false; private _initialPromise?: Promise; private constructor() { this.conversations = new ConversationCollection(); } public static getInstance() { if (ConversationController.instance) { return ConversationController.instance; } ConversationController.instance = new ConversationController(); return ConversationController.instance; } public get(id: string): ConversationModel { if (!this._initialFetchComplete) { throw new Error( 'ConversationController.get() needs complete initial fetch' ); } return this.conversations.get(id); } public getOrThrow(id: string): ConversationModel { if (!this._initialFetchComplete) { throw new Error( 'ConversationController.get() needs complete initial fetch' ); } const convo = this.conversations.get(id); if (convo) { return convo; } throw new Error( `Conversation ${id} does not exist on ConversationController.get()` ); } // Needed for some model setup which happens during the initial fetch() call below public getUnsafe(id: string): ConversationModel | undefined { return this.conversations.get(id); } public dangerouslyCreateAndAdd(attributes: ConversationAttributes) { return this.conversations.add(attributes); } public getOrCreate(id: string, type: string) { if (typeof id !== 'string') { throw new TypeError("'id' must be a string"); } if (type !== 'private' && type !== 'group') { throw new TypeError( `'type' must be 'private' or 'group'; got: '${type}'` ); } if (!this._initialFetchComplete) { throw new Error( 'ConversationController.get() needs complete initial fetch' ); } let conversation = this.conversations.get(id); if (conversation) { return conversation; } conversation = this.conversations.add({ id, type, version: 2, } as any); const create = async () => { if (!conversation.isValid()) { const validationError = conversation.validationError || {}; window.log.error( 'Contact is not valid. Not saving, but adding to collection:', conversation.idForLogging(), validationError.stack ); return conversation; } try { await saveConversation(conversation.attributes); } catch (error) { window.log.error( 'Conversation save failed! ', id, type, 'Error:', error && error.stack ? error.stack : error ); throw error; } return conversation; }; conversation.initialPromise = create(); conversation.initialPromise.then(async () => { if (window.inboxStore) { window.inboxStore?.dispatch( conversationActions.conversationAdded( conversation.id, conversation.getProps() ) ); } if (!conversation.isPublic()) { await Promise.all([ conversation.updateProfileAvatar(), // NOTE: we request snodes updating the cache, but ignore the result void getSnodesFor(id), ]); } }); return conversation; } public getContactProfileNameOrShortenedPubKey(pubKey: string): string { const conversation = ConversationController.getInstance().get(pubKey); if (!conversation) { return pubKey; } return conversation.getContactProfileNameOrShortenedPubKey(); } public getContactProfileNameOrFullPubKey(pubKey: string): string { const conversation = this.conversations.get(pubKey); if (!conversation) { return pubKey; } return conversation.getContactProfileNameOrFullPubKey(); } public isMediumGroup(hexEncodedGroupPublicKey: string): boolean { const convo = this.conversations.get(hexEncodedGroupPublicKey); if (convo) { return convo.isMediumGroup(); } return false; } public async getOrCreateAndWait( id: string | PubKey, type: 'private' | 'group' ): Promise { const initialPromise = this._initialPromise !== undefined ? this._initialPromise : Promise.resolve(); return initialPromise.then(() => { if (!id) { return Promise.reject( new Error('getOrCreateAndWait: invalid id passed.') ); } const pubkey = id && (id as any).key ? (id as any).key : id; const conversation = this.getOrCreate(pubkey, type); if (conversation) { return conversation.initialPromise.then(() => conversation); } return Promise.reject( new Error('getOrCreateAndWait: did not get conversation') ); }); } public async getAllGroupsInvolvingId(id: string) { const groups = await getAllGroupsInvolvingId(id); return groups.map((group: any) => this.conversations.add(group)); } public async deleteContact(id: string) { if (typeof id !== 'string') { throw new TypeError("'id' must be a string"); } if (!this._initialFetchComplete) { throw new Error( 'ConversationController.get() needs complete initial fetch' ); } const conversation = this.conversations.get(id); if (!conversation) { return; } // Close group leaving if (conversation.isClosedGroup()) { await conversation.leaveGroup(); } else if (conversation.isPublic()) { const channelAPI = await conversation.getPublicSendData(); if (channelAPI === null) { window.log.warn(`Could not get API for public conversation ${id}`); } else { channelAPI.serverAPI.partChannel(channelAPI.channelId); } } await conversation.destroyMessages(); await removeConversation(id); this.conversations.remove(conversation); if (window.inboxStore) { window.inboxStore?.dispatch( conversationActions.conversationRemoved(conversation.id) ); } } public getConversations(): Array { return Array.from(this.conversations.models); } public async load() { window.log.info('ConversationController: starting initial fetch'); if (this.conversations.length) { throw new Error('ConversationController: Already loaded!'); } const load = async () => { try { const collection = await getAllConversations(); this.conversations.add(collection.models); this._initialFetchComplete = true; const promises: any = []; this.conversations.forEach((conversation: ConversationModel) => { if (!conversation.get('lastMessage')) { // tslint:disable-next-line: no-void-expression promises.push(conversation.updateLastMessage()); } promises.concat([ conversation.updateProfileName(), conversation.updateProfileAvatar(), ]); }); await Promise.all(promises); // Remove any unused images window.profileImages.removeImagesNotInArray( this.conversations.map((c: any) => c.id) ); window.log.info('ConversationController: done with initial fetch'); } catch (error) { window.log.error( 'ConversationController: initial fetch failed', error && error.stack ? error.stack : error ); throw error; } }; await BlockedNumberController.load(); this._initialPromise = load(); return this._initialPromise; } public loadPromise() { return this._initialPromise; } public reset() { this._initialPromise = Promise.resolve(); this._initialFetchComplete = false; if (window.inboxStore) { window.inboxStore?.dispatch(conversationActions.removeAllConversations()); } this.conversations.reset([]); } }