Home Reference Source

src/contacts-new/ContactRepository.ts

import {zip} from 'lodash-es';
import {Observable, combineLatest, concat, defer, of} from 'rxjs';
import {debounceTime, map, scan, shareReplay, switchMap, tap} from 'rxjs/operators';
import {v4 as uuidv4} from 'uuid';

import Logs from '../Logs';
import {User} from '../users-new/User';
import {UserRepository} from '../users-new/UserRepository';
import {ContactService} from '../wac-proxy/wac-stack/contact/ContactService';
import {AddressBookType, AddressTypeDto, ContactDto} from '../wac-proxy/wac-stack/contact/types';

import {AddressType} from './AddressType';
import {Contact} from './Contact';
import {ContactType} from './ContactType';

const log = Logs.instance.getLogger('SippoJS/ContactRepository');

const toContactType = (type: AddressBookType): ContactType => {
	switch (type) {
		case AddressBookType.CONTACTS: return ContactType.CONTACTS;
		case AddressBookType.DOMAIN: return ContactType.DOMAIN;
		case AddressBookType.GROUPS: return ContactType.GROUPS;
		case AddressBookType.PHONEBOOKS: return ContactType.PHONEBOOKS;
		case AddressBookType.STATIC: return ContactType.STATIC;
	}
};

const toAddressType = (type: AddressTypeDto): AddressType => {
	switch (type) {
		case AddressTypeDto.HOME: return AddressType.HOME;
		case AddressTypeDto.OTHER: return AddressType.OTHER;
		case AddressTypeDto.WORK: return AddressType.WORK;
	}
};

const toContact = (data: ContactDto & {type: AddressBookType}, user?: User): Contact => ({
	id: data.id,
	name: data.name,
	type: toContactType(data.type),
	phones: data.phones.map(phone => ({...phone, type: toAddressType(phone.type)})),
	emails: data.emails.map(email => ({...email, type: toAddressType(email.type)})),
	favorite: data.favorite || false,
	user,
});

const toAddressTypeDTO = (type: AddressType): AddressTypeDto => {
	switch (type) {
		case AddressType.HOME: return AddressTypeDto.HOME;
		case AddressType.OTHER: return AddressTypeDto.OTHER;
		case AddressType.WORK: return AddressTypeDto.WORK;
	}
};

const toContactDto = (contact: Pick<Contact, 'emails' | 'favorite' | 'id' | 'name' | 'phones'>): ContactDto => ({
	id: contact.id,
	name: contact.name,
	favorite: contact.favorite,
	phones: contact.phones.map(phone => ({type: toAddressTypeDTO(phone.type), value: phone.value, primary: phone.primary})),
	emails: contact.emails.map(email => ({type: toAddressTypeDTO(email.type), value: email.value, primary: email.primary})),
});

type Action = {
	readonly type: 'CREATE' | 'DELETE' | 'INIT' | 'UPDATE';
	readonly contacts: Readonly<Array<ContactDto & {type: AddressBookType}>>;
};
type State = Readonly<Array<ContactDto & {type: AddressBookType}>>;

/**
 * Allows retrieving, updating and creating contacts. An instance of this class
 * must be obtained using {@link Session.getContactRepository}
 */
export class ContactRepository {
	/**
	 * Allows access to the list with every contact
	 */
	readonly contacts$: Observable<readonly Contact[]>;

	constructor(
		private contactService: ContactService,
		private userRepository: UserRepository,
	) {
		this.contacts$ = this.getActions().pipe(
			scan((state: State, action: Action) => {
				log.debug('action', action);
				switch (action.type) {
					case 'INIT':
						return action.contacts;
					case 'CREATE':
						return state.concat(action.contacts);
					case 'DELETE':
						return state.filter(contact => !action.contacts.some(c => c.id === contact.id && c.type === contact.type));
					case 'UPDATE':
						return state.map(contact => action.contacts.find(c => c.id === contact.id && c.type === contact.type) || contact);
				}
			}, []),
			switchMap((contacts) => {
				if (contacts.length === 0) {
					return of([[], []]);
				}
				const users = contacts.map(contact =>
					contact.associatedUserId ? this.userRepository.getUser$(contact.associatedUserId) : of(undefined),
				);
				return combineLatest([
					of(contacts),
					combineLatest(users).pipe(debounceTime(10)),
				]);
			}),
			map(([contacts, users]) => zip(contacts, users)
				.map(([contact, user]) => contact ? toContact(contact, user) : undefined)
				.filter((contact): contact is Contact => contact !== undefined)),
			tap(x => log.debug('contacts', x)),
			shareReplay(1),
		);
	}

	private getActions(): Observable<Action> {
		const initialAction$: Observable<Action> = defer(() => this.contactService.getContacts$()).pipe(
			map(addressBooks => ({
				type: 'INIT',
				contacts: addressBooks.flatMap(
					addressBook => addressBook.contacts.map(contact => ({...contact, type: addressBook.type})),
				),
			})),
		);
		const updatedActions$: Observable<Action> = defer(() => this.contactService.contactEvent$).pipe(
			map((event) => {
				const contacts = [
					{...event.body.contact, type: event.body.addressBookType},
				];
				switch (event.method) {
					case 'POST': return {type: 'CREATE', contacts};
					case 'PUT': return {type: 'UPDATE', contacts};
					case 'DELETE': return {type: 'DELETE', contacts};
				}
			}),
		);
		return concat(initialAction$, updatedActions$);
	}

	/**
	 * Saves a contact
	 * @param contact
	 */
	async saveContact(contact: Pick<Contact, 'emails' | 'favorite' | 'id' | 'name' | 'phones'>): Promise<void> {
		const data = toContactDto(contact);
		return this.contactService.updateContact(data);
	}

	/**
	 * Creates a new contact
	 * @param contact
	 */
	async createContact(contact: Pick<Contact, 'emails' | 'favorite' | 'name' | 'phones'>): Promise<void> {
		const data = toContactDto({id: uuidv4(), ...contact});
		return this.contactService.createContact(data);
	}

	/**
	 * Deletes a contact
	 * @param contact
	 */
	async deleteContact(contact: Pick<Contact, 'id'>): Promise<void> {
		return this.contactService.deleteContact(contact.id);
	}
}