VulpesOS/gaia/shared/js/contacts/contacts_merger.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

378 lines
12 KiB
JavaScript
Executable file

/* globals Promise, SimplePhoneMatcher, utils, ContactPhotoHelper */
'use strict';
var contacts = window.contacts || {};
contacts.Merger = (function() {
var DEFAULT_ADR_TYPE = 'home';
var DEFAULT_TEL_TYPE = 'other';
var DEFAULT_EMAIL_TYPE = 'other';
// Performs the merge passing the master contact and matching contacts
// The master contact is the one that contains the info with more priority
// The matchingContacts are the contacts which information will merged with
// the master. It is an Array in which the elements should be ordered
// by priority i.e. the data from the first elements might take precedence
// over the data of the last elements according to the merging rules.
//
// Each element in the array is an object with the following keys:
// * matchingContact: the Contact object matching one or more targets.
// * matchings: optional, an object which entries are arrays of
// field-matching objects indexed by the field name.
//
// Each field-matching object has two fields:
// * target: the value that field matched with.
// * matchedValue: the value from the matchingContact field matching the
// target.
//
// Here is an example:
// {
// matchingContact: { ... },
// matchings: {
// 'tel': [{
// target: '600000000',
// matchedValue: '+34600000000'
// }]
// }
// }
//
// The `matchings` field can be not present if the matching contact comes
// from an external source not related with the matching algorithm.
function doMerge(pmasterContact, pmatchingContacts, callbacks) {
window.setTimeout(function contactsMerge() {
mergeAll(pmasterContact, pmatchingContacts, filled, callbacks);
}, 0);
}
function doInMemoryMerge(pmasterContact, pmatchingContacts) {
return new Promise(function (resolve, reject) {
mergeAll(pmasterContact, pmatchingContacts, inMemoryMergeDone, {
success: function(masterContact) {
resolve(masterContact);
},
error: function(err) {
reject(err);
}
});
});
}
function inMemoryMergeDone(callbacks, matchingContacts, masterContact) {
callbacks.success(masterContact);
}
function isSimContact(contact) {
return Array.isArray(contact.category) &&
contact.category.indexOf('sim') !== -1;
}
function mergeAll(masterContact, matchingContacts, onFilled, callbacks) {
var emailsHash;
var categoriesHash;
var telsHash;
var mergedContact = {};
var mergedPhoto;
mergedContact.givenName = [];
copyStringArray(masterContact.givenName, mergedContact.givenName);
mergedContact.familyName = [];
copyStringArray(masterContact.familyName, mergedContact.familyName);
mergedContact.photo = masterContact.photo || [];
mergedPhoto = mergedContact.photo;
mergedContact.bday = masterContact.bday;
mergedContact.anniversary = masterContact.anniversary;
mergedContact.adr = masterContact.adr || [];
telsHash = {};
mergedContact.tel = [];
if (Array.isArray(masterContact.tel)) {
masterContact.tel.forEach(function(aTel) {
if (!telsHash[aTel.value]) {
aTel.type = (Array.isArray(aTel.type) ? aTel.type : [aTel.type]);
aTel.type[0] = aTel.type[0] || DEFAULT_TEL_TYPE;
mergedContact.tel.push(aTel);
}
var variants = SimplePhoneMatcher.generateVariants(aTel.value);
variants.forEach(function(aVariant) {
telsHash[aVariant] = true;
});
});
}
mergedContact.email = [];
emailsHash = {};
populateEmails(masterContact.email, emailsHash, mergedContact.email);
mergedContact.org = masterContact.org || [];
mergedContact.category = [];
categoriesHash = {};
populateNoDuplicates(masterContact.category, categoriesHash,
mergedContact.category);
mergedContact.url = masterContact.url || [];
mergedContact.note = masterContact.note || [];
matchingContacts.forEach(function(aResult) {
var theMatchingContact = aResult.matchingContact;
var givenName = theMatchingContact.givenName;
if (Array.isArray(givenName) && givenName[0]) {
if (mergedContact.givenName.indexOf(givenName[0]) === -1) {
if (mergedContact.givenName[0] &&
mergedContact.givenName[0].trim()) {
mergedContact.givenName.push(givenName[0]);
}
else {
mergedContact.givenName[0] = givenName[0];
}
}
}
var familyName = theMatchingContact.familyName;
if (Array.isArray(familyName) && familyName[0]) {
if (mergedContact.familyName.indexOf(familyName[0]) === -1) {
if (mergedContact.familyName[0] &&
mergedContact.familyName[0].trim()) {
mergedContact.familyName.push(familyName[0]);
}
else {
mergedContact.familyName[0] = familyName[0];
}
}
}
// Avoiding duplicating givenName - familyName for SIM Contacts
if ((!isDefined(masterContact.givenName) ||
!isDefined(masterContact.familyName)) &&
isSimContact(theMatchingContact)) {
if (isDefined(mergedContact.givenName) &&
isDefined(mergedContact.familyName) &&
mergedContact.familyName[0] === mergedContact.givenName[0]) {
if (!isDefined(masterContact.givenName)) {
mergedContact.givenName = [];
mergedContact.name = mergedContact.familyName;
}
if (!isDefined(masterContact.familyName)) {
mergedContact.familyName = [];
mergedContact.name = mergedContact.givenName;
}
}
}
if (!mergedContact.bday && theMatchingContact.bday) {
mergedContact.bday = theMatchingContact.bday;
}
if (!mergedContact.anniversary && theMatchingContact.anniversary) {
mergedContact.anniversary = theMatchingContact.anniversary;
}
if (isDefined(theMatchingContact.org) && mergedContact.org.length === 0) {
mergedContact.org = theMatchingContact.org;
}
if (isDefined(theMatchingContact.category)) {
populateNoDuplicates(theMatchingContact.category, categoriesHash,
mergedContact.category);
}
populateEmails(theMatchingContact.email, emailsHash, mergedContact.email);
if (Array.isArray(theMatchingContact.tel)) {
var theMatchings = aResult.matchings || {};
var telMatchings = theMatchings.tel;
theMatchingContact.tel.forEach(function(aTel) {
var theValue = aTel.value;
var target = theValue, matchedValue = '';
if (telMatchings) {
var matchedFrom = telMatchings.filter(function(x) {
return (x.target === theValue || x.matchedValue === theValue);
});
if (matchedFrom[0]) {
target = matchedFrom[0].target;
matchedValue = matchedFrom[0].matchedValue;
}
}
var variants = SimplePhoneMatcher.generateVariants(theValue);
var alreadyPresent = false;
for (var j = 0; j < variants.length; j++) {
if (telsHash[variants[j]]) {
alreadyPresent = true;
}
}
if (!alreadyPresent) {
theValue = target.length > matchedValue.length ?
target : matchedValue;
mergedContact.tel.push({
type: aTel.type || [DEFAULT_TEL_TYPE],
value: theValue,
carrier: aTel.carrier,
pref: aTel.pref
});
variants.forEach(function(aVariant) {
telsHash[aVariant] = true;
});
}
});
}
if (!Array.isArray(mergedPhoto) || mergedPhoto.length === 0 &&
isDefined(theMatchingContact.photo)) {
var photo = ContactPhotoHelper.getFullResolution(theMatchingContact);
mergedPhoto = [photo];
}
populateField(
theMatchingContact.adr,
mergedContact.adr,
DEFAULT_ADR_TYPE
);
populateField(theMatchingContact.url, mergedContact.url);
populateField(theMatchingContact.note, mergedContact.note);
}); // matchingResults
mergedContact.name = [((mergedContact.givenName[0] ?
mergedContact.givenName[0] : '') + ' ' +
(mergedContact.familyName[0] ?
mergedContact.familyName[0] : '')).trim()];
fillMasterContact(masterContact, mergedContact, mergedPhoto,
onFilled.bind(null, callbacks, matchingContacts));
}
function filled(callbacks, matchingContacts, masterContact) {
// Updating the master contact
var req = navigator.mozContacts.save(
utils.misc.toMozContact(masterContact));
req.onsuccess = function() {
// Now for all the matchingContacts they have to be removed
matchingContacts.forEach(function(aMatchingContact) {
// Only remove those contacts which are already in the DB
if (aMatchingContact.matchingContact.id) {
var contact = aMatchingContact.matchingContact;
navigator.mozContacts.remove(utils.misc.toMozContact(contact));
}
});
if (typeof callbacks.success === 'function') {
callbacks.success(masterContact);
}
};
req.onerror = function() {
window.console.error('Error while saving merged Contact: ',
req.error.name);
typeof callbacks.error === 'function' && callbacks.error(req.error);
};
}
function isDefined(field) {
return (Array.isArray(field) && field[0] &&
((typeof field[0] === 'string' && field[0].trim().length > 0) ||
typeof field[0] === 'object'));
}
function copyStringArray(source, dest) {
if (Array.isArray(source)) {
source.forEach(function(aVal) {
if (aVal && aVal.trim()) {
dest.push(aVal);
}
});
}
}
function populateEmails(sourceEmails, hash, out) {
if (Array.isArray(sourceEmails)) {
sourceEmails.forEach(function(aEmail) {
aEmail.type = Array.isArray(aEmail.type) ? aEmail.type : [aEmail.type];
aEmail.type[0] = aEmail.type[0] || DEFAULT_EMAIL_TYPE;
var value = aEmail.value;
if (value && value.trim()) {
value = value.toLowerCase();
if (!hash[value]) {
aEmail.value = value;
out.push(aEmail);
hash[value] = true;
}
}
});
}
}
function populateNoDuplicates(source, hash, out) {
if (Array.isArray(source)) {
source.forEach(function(aCat) {
if (!hash[aCat]) {
out.push(aCat);
hash[aCat] = true;
}
});
}
}
function populateField(source, destination, defaultType) {
if (Array.isArray(source)) {
// The easiest way to compare two objects is to compare their's
// stringified JSON representations as strings. So we create
// temporary Array with JSONs of the objects to compare.
var stringifiedDestination = destination.map(function(element){
return JSON.stringify(element);
});
source.forEach(function(as, index) {
// If the source value and destination value is the same we
// don't want to merge and will leave contact as it is. This
// prevents duplication of the data like in Bug 935636
if (stringifiedDestination.indexOf(JSON.stringify(as)) === -1) {
if (defaultType && (!as.type || !as.type[0])) {
as.type = [defaultType];
}
destination.push(as);
}
});
}
}
function fillMasterContact(masterContact, mergedContact, mergedPhotos, done) {
var fields = ['familyName', 'givenName', 'name', 'org', 'email', 'tel',
'bday', 'anniversary', 'adr', 'category',
'url', 'note', 'photo'];
fields.forEach(function(aField) {
masterContact[aField] = mergedContact[aField];
});
if (!Array.isArray(mergedPhotos) || !mergedPhotos[0] ||
mergedPhotos.length >= 2) {
done(masterContact);
return;
}
var mergedPhoto = mergedPhotos[0];
utils.thumbnailImage(mergedPhoto, function gotTumbnail(thumbnail) {
if (mergedPhoto !== thumbnail) {
masterContact.photo = [mergedPhoto, thumbnail];
} else {
masterContact.photo = [mergedPhoto];
}
done(masterContact);
});
}
return {
merge: doMerge,
inMemoryMerge: doInMemoryMerge
};
})();