You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			304 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			304 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			TypeScript
		
	
import {
 | 
						|
  getAllConversations,
 | 
						|
  getAllGroupsInvolvingId,
 | 
						|
  removeConversation,
 | 
						|
  saveConversation,
 | 
						|
} from '../../data/data';
 | 
						|
import {
 | 
						|
  ConversationAttributes,
 | 
						|
  ConversationCollection,
 | 
						|
  ConversationModel,
 | 
						|
  ConversationTypeEnum,
 | 
						|
} from '../../models/conversation';
 | 
						|
import { BlockedNumberController } from '../../util';
 | 
						|
import { getSwarm } from '../snode_api/snodePool';
 | 
						|
import { PubKey } from '../types';
 | 
						|
import { actions as conversationActions } from '../../state/ducks/conversations';
 | 
						|
import { getV2OpenGroupRoom, removeV2OpenGroupRoom } from '../../data/opengroups';
 | 
						|
import _ from 'lodash';
 | 
						|
import { OpenGroupManagerV2 } from '../../opengroup/opengroupV2/OpenGroupManagerV2';
 | 
						|
import { deleteAuthToken } from '../../opengroup/opengroupV2/ApiAuth';
 | 
						|
 | 
						|
export class ConversationController {
 | 
						|
  private static instance: ConversationController | null;
 | 
						|
  private readonly conversations: ConversationCollection;
 | 
						|
  private _initialFetchComplete: boolean = false;
 | 
						|
  private _initialPromise?: Promise<any>;
 | 
						|
 | 
						|
  private constructor() {
 | 
						|
    this.conversations = new ConversationCollection();
 | 
						|
  }
 | 
						|
 | 
						|
  public static getInstance() {
 | 
						|
    if (ConversationController.instance) {
 | 
						|
      return ConversationController.instance;
 | 
						|
    }
 | 
						|
    ConversationController.instance = new ConversationController();
 | 
						|
 | 
						|
    return ConversationController.instance;
 | 
						|
  }
 | 
						|
 | 
						|
  // FIXME this could return | undefined
 | 
						|
  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: ConversationTypeEnum) {
 | 
						|
    if (typeof id !== 'string') {
 | 
						|
      throw new TypeError("'id' must be a string");
 | 
						|
    }
 | 
						|
 | 
						|
    if (type !== ConversationTypeEnum.PRIVATE && type !== ConversationTypeEnum.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 () => {
 | 
						|
      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 getSwarm(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: ConversationTypeEnum
 | 
						|
  ): Promise<ConversationModel> {
 | 
						|
    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 (!this._initialFetchComplete) {
 | 
						|
      throw new Error('ConversationController.get() needs complete initial fetch');
 | 
						|
    }
 | 
						|
 | 
						|
    const conversation = this.conversations.get(id);
 | 
						|
    if (!conversation) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Closed/Medium group leaving
 | 
						|
    if (conversation.isClosedGroup()) {
 | 
						|
      await conversation.leaveClosedGroup();
 | 
						|
      // open group v1
 | 
						|
    } else if (conversation.isPublic() && !conversation.isOpenGroupV2()) {
 | 
						|
      const channelAPI = await conversation.getPublicSendData();
 | 
						|
      if (channelAPI === null) {
 | 
						|
        window.log.warn(`Could not get API for public conversation ${id}`);
 | 
						|
      } else {
 | 
						|
        channelAPI.serverAPI.partChannel((channelAPI as any).channelId);
 | 
						|
      }
 | 
						|
      // open group v2
 | 
						|
    } else if (conversation.isOpenGroupV2()) {
 | 
						|
      window.log.info('leaving open group v2', conversation.id);
 | 
						|
      const roomInfos = await getV2OpenGroupRoom(conversation.id);
 | 
						|
      if (roomInfos) {
 | 
						|
        OpenGroupManagerV2.getInstance().removeRoomFromPolledRooms(roomInfos);
 | 
						|
        // leave the group on the remote server
 | 
						|
        try {
 | 
						|
          await deleteAuthToken(_.pick(roomInfos, 'serverUrl', 'roomId'));
 | 
						|
        } catch (e) {
 | 
						|
          window.log.info('deleteAuthToken failed:', e);
 | 
						|
        }
 | 
						|
        // remove the roomInfos locally for this open group room
 | 
						|
        try {
 | 
						|
          await removeV2OpenGroupRoom(conversation.id);
 | 
						|
        } catch (e) {
 | 
						|
          window.log.info('removeV2OpenGroupRoom failed:', e);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // those are the stuff to do for all contact types
 | 
						|
    await conversation.destroyMessages();
 | 
						|
 | 
						|
    // if this conversation is a private conversation it's in fact a `contact` for desktop.
 | 
						|
    // we just want to remove everything related to it, set the active_at to undefined
 | 
						|
    // so conversation still exists (useful for medium groups members or opengroups) but is not shown on the UI
 | 
						|
    if (conversation.isPrivate()) {
 | 
						|
      conversation.set('active_at', undefined);
 | 
						|
      await conversation.commit();
 | 
						|
    } else {
 | 
						|
      await removeConversation(id);
 | 
						|
      this.conversations.remove(conversation);
 | 
						|
      if (window.inboxStore) {
 | 
						|
        window.inboxStore?.dispatch(conversationActions.conversationRemoved(conversation.id));
 | 
						|
        window.inboxStore?.dispatch(
 | 
						|
          conversationActions.conversationChanged(conversation.id, conversation.getProps())
 | 
						|
        );
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  public getConversations(): Array<ConversationModel> {
 | 
						|
    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([]);
 | 
						|
  }
 | 
						|
}
 |