src/contacts/ContactSynchronizer.js
import {EventEmitter} from '../eventemitter';
import {ContactRetriever} from './ContactRetriever';
import {
isDevice,
getDeviceId,
} from '../utils';
import * as ContactCache from './ContactCache';
import {isEqual} from 'lodash-es';
import Logs from '../Logs';
let log = Logs.instance.getLogger('SippoJS/ContactSynchronizer');
const NUM_CONTACTS_ALLOW_UPLOAD_ONE_BY_ONE = 30;
/**
* Get an object without some fields
* @param {Object} contact
* @return {Object} New object without some fields
*/
function _getContactToCompare(contact) {
let objContact = Object.assign({}, contact);
delete objContact.user;
delete objContact.favorite;
delete objContact.id;
delete objContact.unnormalizedPhones;
objContact.avatar = null;
objContact.phones = contact.unnormalizedPhones;
return objContact;
}
function _prepareToUpdate(oldContact, newContact) {
return Object.assign(oldContact, newContact);
}
const _isNeededUploadCompleteListAgain = (arrayOfArrays) => {
return arrayOfArrays.reduce((counter, actual) => {
counter += actual.length;
return counter;
}, 0) >= NUM_CONTACTS_ALLOW_UPLOAD_ONE_BY_ONE;
};
const _onErrorSyncContacts = (err) => {
return err.message === 'not-sync-avatars' ?
Promise.resolve() :
Promise.reject(err);
};
/**
* Keeps the wac contacts up to date with device contacts.
*/
export class ContactSynchronizer extends EventEmitter {
constructor(stack, favoriteContact) {
super();
this._stack = stack;
this._retriever = new ContactRetriever(stack);
this._deviceId = getDeviceId();
this._favoriteContacts = favoriteContact;
}
/**
* Init the contact sync at first time
* @param {*} deviceContacts : <W3cContact>[]
*/
syncContacts(deviceContacts) {
if (this._isAllowDeviceContacts()) {
return this._invalidateAndSyncContacts(deviceContacts);
}
this.emit('stop-synchronizing');
}
/**
* Launch the synchronizing to check if a change has occurred
*/
launchSynchronize() {
let devSync = Promise.resolve();
if (this._isAllowDeviceContacts()) {
devSync = this._retriever.getContactsDevice().then((deviceContacts) => {
if (!ContactCache.isCacheUpdated(deviceContacts)) {
this.emit('start-synchronizing');
let deviceContactsStored = ContactCache.getDeviceWacContacts();
ContactCache.invalidateCache();
let contactsToDelete = this._getContactsToDeleted(deviceContactsStored, deviceContacts);
let [contactsToUpdate, contactsToInsert] = this._getContactsToSynchronize(deviceContactsStored, deviceContacts);
return _isNeededUploadCompleteListAgain([contactsToDelete, contactsToUpdate, contactsToInsert]) ?
this._uploadAllContactsAgain(deviceContacts) :
this._sendUpdatesToWac(contactsToDelete, contactsToUpdate, contactsToInsert, deviceContacts);
}
this.emit('stop-synchronizing');
})
.then(() => this.launchSynchronizeAvatars())
.catch(_onErrorSyncContacts);
}
return devSync;
}
/**
* Get an array with contacts to update and to insert
* @param {Array} oldListContacts
* @param {Array} newListContacts
* @return {Array} Two elements. First a list of contacts to update and
* second element with a list of contact to insert
*/
_getContactsToSynchronize(oldListContacts, newListContacts) {
let mapOldDeviceContacts = new Map(oldListContacts.map(contact => [contact.contactDeviceId, contact]));
let contactsToUpdate = [];
let contactsToInsert = [];
newListContacts.forEach((contact) => {
if (mapOldDeviceContacts.has(contact.contactDeviceId)) {
let oContactStored = mapOldDeviceContacts.get(contact.contactDeviceId);
return !isEqual(contact, _getContactToCompare(oContactStored)) ?
contactsToUpdate.push(_prepareToUpdate(oContactStored, contact)) :
undefined;
}
return contactsToInsert.push(contact);
});
return [contactsToUpdate, contactsToInsert];
}
/**
* Get the list of contacts to delete
* @param {Array} oldListContacts
* @param {Array} newListContacts
* @return {Array} A list with contacts to delete
*/
_getContactsToDeleted(oldListContacts, newListContacts) {
let mapNewListContacts = new Map(newListContacts.map(contact => [contact.contactDeviceId, contact]));
return oldListContacts.map((contact) => {
if (!mapNewListContacts.has(contact.contactDeviceId)) {
return contact;
}
}).filter(contact => !!contact);
}
/**
* Update the new changes to the WAC
* @param {Array} contactsToDelete
* @param {Array} contactsToUpdate
* @param {Array} contactsToInsert
* @param {Array} deviceContacts
* @return {Promise} Fulfilled if success
*/
_sendUpdatesToWac(contactsToDelete, contactsToUpdate, contactsToInsert, deviceContacts) {
contactsToUpdate = this._favoriteContacts.updateContactsWithFavoriteFlag(contactsToUpdate);
return Promise.all([
this._removeContacts(contactsToDelete),
this._addContacts(contactsToInsert),
this._updateContacts(contactsToUpdate),
]).then(() => {
ContactCache.setDeviceContacts(deviceContacts);
ContactCache.validateCache();
});
}
/**
* Sync the avatars of the device contacts
*/
launchSynchronizeAvatars() {
if (this._isAllowDeviceContacts()) {
return this._retriever.getAvatarDeviceContacts().then((avatars) => {
ContactCache.setAvatarDeviceContacts(Array.from(avatars));
this.emit('update-avatars-contacts', avatars);
});
}
}
/**
* Invalidate list of contacts for this device and upload all contacts
*/
_invalidateAndSyncContacts(deviceContacts) {
log.info('device contacts out of sync, uploading new list');
this.emit('sync-all-device-contacts');
return this._stack.invalidateContacts(this._deviceId)
.then(() => this._uploadContacts(deviceContacts))
.then(() => {
ContactCache.setDeviceContacts(deviceContacts);
ContactCache.validateCache();
});
}
/**
* Invalidate list of contacts for this device,
* upload all contacts and avoid to sync avatars
*/
_uploadAllContactsAgain(deviceContacts) {
return this._invalidateAndSyncContacts(deviceContacts)
.then(() => {
throw new Error('not-sync-avatars');
});
}
/**
* Upload a list of contacts to the WAC.
* @param {*} contactList
*/
_uploadContacts(contactList) {
return contactList.length ? this._stack.insertMany({
contacts: contactList,
source: 'device',
}) : Promise.resolve();
}
/**
* Add new contacts to the WAC.
* @param {*} contactList
*/
_addContacts(contactList) {
if (contactList.length) {
return Promise.all(contactList.map(contact => this._stack.addContact(contact)));
}
}
/**
* Update a list of contacts to the WAC.
* @param {*} contactList
*/
_updateContacts(contactList) {
if (contactList.length) {
return Promise.all(contactList.map(contact => this._stack.updateContact(contact)));
}
}
/**
* Remove contacts in the WAC.
* @param {*} contactsList
*/
_removeContacts(contactsList) {
if (contactsList.length) {
return Promise.all(contactsList.map(contact => this._stack.removeContact(contact)));
}
}
_isAllowDeviceContacts() {
return isDevice() && this._stack.getCapabilities().includes('w3c-contacts-api');
}
}