src/contacts-new/ContactRepository.ts
import Logs from '../Logs';
import {zip} from 'lodash-es';
import {combineLatest, concat, defer, Observable, of} from 'rxjs';
import {debounceTime, map, scan, shareReplay, switchMap, tap} from 'rxjs/operators';
import {User} from '../users-new/User';
import {UserRepository} from '../users-new/UserRepository';
import {ContactService} from '../wac-proxy/wac-stack/contact/ContactService';
import {AddressBookType, AddressTypeDto, ContactEditionDto, ContactDto, ContactCreationDto} 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,
editable: toContactType(data.type) === ContactType.CONTACTS,
user,
});
const toContactEditionAddressName = (type: AddressType): string => {
switch (type) {
case AddressType.HOME: return 'home';
case AddressType.OTHER: return 'other';
case AddressType.WORK: return 'work';
}
};
const toContactEditionDto = (contact: Pick<Contact, 'id'|'name'|'favorite'|'phones'|'emails'>): ContactEditionDto => ({
id: contact.id,
name: contact.name,
favorite: contact.favorite,
phones: contact.phones.map(({type, value}) => ({name: toContactEditionAddressName(type), address: value})),
emails: contact.emails.map(({type, value}) => ({name: toContactEditionAddressName(type), email: value})),
source: 'wac',
});
const toContactCreationDto = (contact: Pick<Contact, 'name'|'favorite'|'phones'|'emails'>): ContactCreationDto => ({
id: '',
name: contact.name,
favorite: contact.favorite,
phones: contact.phones.map(({type, value}) => ({name: toContactEditionAddressName(type), address: value})),
emails: contact.emails.map(({type, value}) => ({name: toContactEditionAddressName(type), email: value})),
source: 'wac',
});
type Action = {
type: 'INIT' | 'CREATE' | 'UPDATE' | 'DELETE';
contacts: Readonly<Array<ContactDto & {type: AddressBookType}>>;
};
type State = Readonly<Array<ContactDto & {type: AddressBookType}>>;
/**
* Allows retrieving, updating and creating contacts. A ContactRepository instance
* is obtained calling the {@link Session#getContactRepository} method and must not
* be directly instantiated.
*/
export class ContactRepository {
readonly contacts$: Observable<readonly Contact[]>;
private contactService: ContactService;
private userRepository: UserRepository;
/** @ignore */
constructor(contactService: ContactService, userRepository: UserRepository) {
/** @private */
this.contactService = contactService;
/** @private */
this.userRepository = userRepository;
/**
* Allows access to the list with every contact
* @type {Observable<Contact[]>}
*/
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));
case 'UPDATE':
return state.map(contact => action.contacts.find(c => c.id === contact.id) || 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 */
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} contact
* @return {Promise<void>}
*/
saveContact(contact: Pick<Contact, 'id'|'name'|'phones'|'emails'|'favorite'>): Promise<void> {
const data = toContactEditionDto(contact);
return this.contactService.updateContact(data);
}
/**
* Creates a new contact
* @param {{name: string, phones: Phone[], emails: Email[], favorite: boolean = false}} contact
* @return {Promise<void>}
*/
createContact(contact: Pick<Contact, 'name'|'phones'|'emails'|'favorite'>): Promise<void> {
const data = toContactCreationDto(contact);
return this.contactService.createContact(data);
}
/**
* Deletes a contact
* @param {{id: string}} param
* @return {Promise<void>}
*/
deleteContact({id}: Pick<Contact, 'id'>): Promise<void> {
return this.contactService.deleteContact(id);
}
}