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);
}
}