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.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,75 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<meta charset="utf-8">
<meta name="theme-group" content="theme-communications">
<meta name="theme-color" content="var(--header-background)">
<title> Contacts </title>
<!-- Hack for avoiding 'glitch' when loading the view -->
<style type="text/css">
html, body {
background: #00b7b7 !important;
}
</style>
<!-- Building Blocks -->
<link href="/shared/style/input_areas.css" rel="stylesheet">
<link href="/shared/style/lists.css" rel="stylesheet">
<link href="/shared/style/scrolling.css" rel="stylesheet">
<link href="/shared/style/buttons.css" rel="stylesheet">
<link href="/shared/style/progress_activity.css" rel="stylesheet">
<link href="/shared/style/status.css" rel="stylesheet">
<link href="/shared/style/switches.css" rel="stylesheet">
<link href="/shared/style/confirm.css" rel="stylesheet">
<link href="/shared/style/action_menu.css" rel="stylesheet">
<link href="/shared/style/edit_mode.css" rel="stylesheet">
<link href="/shared/style/animations.css" rel="stylesheet">
<link href="/contacts/style/app.css" rel="stylesheet">
<link href="/contacts/style/contacts.css" rel="stylesheet">
<link href="/contacts/style/overlay.css" rel="stylesheet">
<link href="/shared/pages/import/style/search.css" rel="stylesheet">
<link href="/shared/pages/import/style/curtain_frame.css" rel="stylesheet">
<link href="/contacts/style/status.css" rel="stylesheet">
<link href="/contacts/style/fb_extensions.css" rel="stylesheet">
<link href="/shared/style/contacts.css" rel="stylesheet">
<link href="/shared/style/contacts/contacts_buttons.css" rel="stylesheet">
<link href="/contacts/style/match_service.css" rel="stylesheet">
<link href="/contacts/views/details/style/overwrite.css" rel="stylesheet">
<!-- <link rel="stylesheet" type="text/css" href="/shared/elements/gaia-icons/gaia-icons.css" /> -->
<link rel="stylesheet" type="text/css" href="app://theme.gaiamobile.org/shared/elements/gaia-theme/gaia-theme.css" />
<!-- Web Components -->
<script defer src="/shared/elements/config.js"></script>
<script defer src="/shared/js/component_utils.js"></script>
<script defer src="/shared/elements/gaia_subheader/script.js"></script>
<script defer src="/shared/elements/gaia-header/dist/gaia-header.js"></script>
<link rel="localization" href="/shared/elements/gaia_menu/locales/gaia_menu.{locale}.properties">
<link rel="localization" href="/shared/locales/telephony/telephony.{locale}.properties">
<script defer src="/shared/js/lazy_loader.js"></script>
<script defer src="/contacts/js/utilities/performance_helper.js"></script>
<script defer src="/contacts/js/cache.js"></script>
<script defer src='/contacts/views/details/js/boot.js'></script>
<meta name="defaultLanguage" content="en-US">
<meta name="availableLanguages" content="en-US">
<link rel="localization" href="/contacts/locales/contacts.{locale}.properties">
<link rel="localization" href="/shared/locales/import_contacts/import_contacts.{locale}.properties">
<link rel="localization" href="/shared/locales/date/date.{locale}.properties">
<link rel="localization" href="/shared/locales/phone_types/phone_types.{locale}.properties">
<link rel="import" href="/contacts/elements/details.html">
<link rel="import" href="/shared/elements/confirmation-message.html">
</head>
<body role="application" class="app-contacts skin-comms theme-communications hidden">
<section is="view-contact-details" id='view-contact-details' role="region" class="view view-contact-profile view-noscroll current"></section>
<iframe src="/shared/pages/import/curtain.html" id="iframe_curtain"></iframe>
<form is="confirmation-message" id="confirmation-message" class="hide" role="dialog" data-type="confirm"></form>
</body>
</html>

View file

@ -0,0 +1,63 @@
/* global DetailsUI, DetailsController, LazyLoader, ContactsService */
'use strict';
/*
* This class is the one in charge of loading the minimum set of
* resources needed for the view to load. Any other JS/CSS/Element
* not needed in the critical path *must* be lazy loaded when needed.
*
* Once localization and all the basic JS/CSS/Elements are loaded,
* we will initialize UI and Controller. Both JS classes *must* be
* independent and will communicate through events.
*/
window.addEventListener('DOMContentLoaded', function() {
LazyLoader.load(['/shared/js/l10n.js']).then(function() {
LazyLoader.load([
document.getElementById('view-contact-details')
]).then(function() {
// TODO Add if needed
});
});
});
window.onload = function() {
var dependencies = [
'/contacts/services/contacts.js',
'/shared/js/contact_photo_helper.js',
'/shared/js/contacts/contacts_buttons.js',
'/shared/js/text_normalizer.js',
'/shared/js/contacts/utilities/dom.js',
'/shared/js/contacts/utilities/templates.js',
'/shared/js/contacts/import/utilities/misc.js',
'/contacts/js/match_service.js',
'/contacts/views/details/js/details_ui.js',
'/contacts/views/details/js/details_controller.js',
'/shared/pages/import/js/curtain.js'
];
LazyLoader.load(dependencies).then(function() {
DetailsUI.init();
DetailsController.init();
window.addEventListener('renderdone', function fn() {
window.removeEventListener('renderdone', fn);
document.body.classList.remove('hidden');
});
navigator.mozSetMessageHandler('activity', activity => {
DetailsController.setActivity(activity);
var id = activity.source.data.params.id;
// TODO: Implement handler for open Vcards
ContactsService.get(id, function onSuccess(savedContact) {
ContactsService.getCount(count => {
DetailsUI.render(savedContact, count, true);
});
}, function onError() {
console.error('Error retrieving contact');
});
});
});
};

View file

@ -0,0 +1,165 @@
'use strict';
/* global utils */
/* global MatchService */
/* global ContactsService */
/* global LazyLoader */
/* global ContactToVcardBlob */
/* global VcardFilename */
/* global MozActivity */
/* exported Details */
/*
* Once the deatils view is loaded, we will listen events dispatched
* from the UI. This events will come with the info needed in order
* to execute actions related with the UI (back, toggle favorite, share...).
*
* Controller will *not* contain any code related with the DOM/UI,
* and will rely on the info provided by the event.
*/
(function(exports) {
var _activity = null;
function setActivity(activity) {
_activity = activity;
}
function findDuplicates(evt) {
if (!evt.detail || !evt.detail.contactId) {
console.error('Missing parameters in CustomEvent');
return;
}
var contactId = evt.detail.contactId;
MatchService.match(contactId);
}
function toggleFavorite(evt){
if (!evt.detail || typeof evt.detail.contact === 'undefined' ||
typeof evt.detail.isFavorite === 'undefined') {
console.error('Missing parameters in CustomEvent');
return;
}
var contact = evt.detail.contact;
var isFavorite = evt.detail.isFavorite;
var favorite = !isFavorite;
if (favorite) {
contact.category = contact.category || [];
contact.category.push('favorite');
} else {
if (!contact.category) {
return;
}
var pos = contact.category.indexOf('favorite');
if (pos > -1) {
contact.category.splice(pos, 1);
}
}
function onError(e) {
console.error('Error saving favorite');
// We must restore contact in order to update the UX accordingly
if (favorite) {
var pos = contact.category.indexOf('favorite');
if (pos > -1) {
contact.category.splice(pos, 1);
}
} else {
contact.category = contact.category || [];
contact.category.push('favorite');
}
dispatchEvent('toggleFavoriteDone', {contact: contact});
}
ContactsService.save(
utils.misc.toMozContact(contact),
function(e) {
if (typeof e !== 'undefined') {
onError(e);
return;
}
// TODO: Listening to oncontactchange event, we can save the evt
// and send it via sessionStorage to the main list in order to
// update it accordingly with the changes made by the user in this view.
ContactsService.get(contact.id, function(savedContact) {
dispatchEvent('toggleFavoriteDone', {contact: savedContact});
}, onError);
}
);
}
function init() {
window.addEventListener('backAction', handleBackAction);
window.addEventListener('editAction', handleEditAction);
window.addEventListener('toggleFavoriteAction', toggleFavorite);
window.addEventListener('shareAction', shareContact);
window.addEventListener('findDuplicatesAction', findDuplicates);
// TODO: Need to save the oncontactchange event in order to update
// the main list when the user go back from this view. It could be done
// by saving the event in sessionStorage
}
function shareContact(evt) {
const VCARD_DEPS = [
'/shared/js/text_normalizer.js',
'/shared/js/contact2vcard.js',
'/shared/js/setImmediate.js'
];
if (!evt.detail || typeof evt.detail.contact === 'undefined') {
console.error('Missing parameters in CustomEvent');
return;
}
var contact = evt.detail.contact;
LazyLoader.load(VCARD_DEPS,function vcardLoaded() {
ContactToVcardBlob([contact], function blobReady(vcardBlob) {
VcardFilename(contact).then(filename => {
/* jshint nonew: false */
new MozActivity({
name: 'share',
data: {
type: 'text/vcard',
number: 1,
blobs: [new window.File([vcardBlob], filename, {
type: 'text/x-vcard'
})],
filenames: [filename]
}
});
});
// The MIME of the blob should be this for some MMS gateways
}, { type: 'text/x-vcard'} );
});
}
function handleBackAction(evt) {
if (_activity) {
_activity.postResult({});
} else {
window.history.back();
}
}
function handleEditAction(evt) {
// In the future the navigation will change the URL to navigate
// to #update view: Bug 1169579
}
function dispatchEvent(name, data) {
window.dispatchEvent(new CustomEvent(name, {detail: data}));
}
exports.DetailsController = {
'init': init,
'setActivity': setActivity
};
})(window);

View file

@ -0,0 +1,490 @@
/* global ContactsButtons, LazyLoader, utils, WebrtcClient */
/* global Normalizer, ContactPhotoHelper */
/* exported DetailsUI */
/*
* UI *must* work alone, without any 'Controller' tied. This will
* help us to have a well-separated model, where the UI will dispatch
* events that a Controller could potentially handle.
*
* UI will be in charge of rendering the info provided by 'Boot', which
* is the only part of the code which knows UI & Controller.
*
* Every action performed by the user in the UI will dispatch an event that
* should be handle by a controller
*/
(function(exports) {
'use strict';
const PHOTO_POS = 7;
const INIT_MARGIN = 8;
const MAX_POSITION = 150;
// Scale ratio for different devices
var SCALE_RATIO = window.devicePixelRatio || 1;
var DetailsUI = {
init: function() {
var self = this;
function initContainers() {
self.header = document.querySelector('#details-view-header');
self.contactDetails = document.querySelector('#contact-detail');
self.listContainer = document.querySelector('#details-list');
self.detailsName = document.querySelector('#contact-name-title');
self.detailsNameText =
document.querySelector('#contact-name-title bdi');
self.orgTitle = document.querySelector('#org-title');
self.datesTemplate = document.querySelector('#dates-template-\\#i\\#');
self.addressesTemplate =
document.querySelector('#address-details-template-\\#i\\#');
self.duplicateTemplate =
document.querySelector('#duplicate-contacts-template');
self.editContactButton = document.querySelector('#edit-contact-button');
self.cover = document.querySelector('#cover-img');
self.detailsInner = document.querySelector('#contact-detail-inner');
self.favoriteMessage = document.querySelector('#toggle-favorite');
self.notesTemplate =
document.querySelector('#note-details-template-\\#i\\#');
self.socialTemplate =
document.querySelector('#social-template-\\#i\\#');
}
function addListeners() {
self.initPullEffect(self.cover);
self.favoriteMessage.addEventListener('click',
self.toggleFavorite.bind(self));
self.header.addEventListener('action',
self.handleBackAction.bind(self));
self.editContactButton.addEventListener('click',
self.handleEditAction.bind(self));
ContactsButtons.init(self.listContainer, self.contactDetails, null);
window.addEventListener('toggleFavoriteDone',
self.toggleFavoriteHandler.bind(self));
}
initContainers();
addListeners();
},
handleBackAction: function(evt) {
this.dispatchEvent('backAction');
},
handleEditAction: function(evt) {
this.dispatchEvent('editAction');
},
initPullEffect: function(cover) {
var maxPosition = Math.round(MAX_POSITION * SCALE_RATIO);
var startPosition = 0;
var self = this;
function onTouchStart(e) {
if (self.contactDetails.classList.contains('no-photo')) {
return;
}
e.preventDefault();
startPosition = e.changedTouches[0].clientY;
self.contactDetails.classList.add('up');
cover.classList.add('up');
window.addEventListener('touchmove', onTouchMove, true);
window.addEventListener('touchend', onTouchEnd, true);
}
function onTouchEnd(e) {
e.preventDefault();
self.contactDetails.style.transform = null;
self.contactDetails.classList.add('up');
cover.style.transform = null;
cover.classList.add('up');
window.removeEventListener('touchmove', onTouchMove, true);
window.removeEventListener('touchend', onTouchEnd, true);
}
function onTouchMove(e) {
e.preventDefault();
var deltaY = e.changedTouches[0].clientY - startPosition;
deltaY = Math.min(maxPosition, Math.max(0, deltaY));
var calc = 'calc(' + INIT_MARGIN + 'rem + ' + deltaY + 'px)';
self.contactDetails.style.transform = 'translateY(' + calc + ')';
self.contactDetails.classList.remove('up');
// Divide by 40 (4 times slower and in rems)
var coverPosition = (-PHOTO_POS + (deltaY / 40)) + 'rem';
cover.style.transform = 'translateY(' + coverPosition + ')';
cover.classList.remove('up');
}
cover.addEventListener('touchstart', onTouchStart, true);
},
render: function(currentContact, contactsCount, readOnly) {
if(this.isAFavoriteChange){
this.isAFavoriteChange = false;
return Promise.resolve(this.isAFavoriteChange);
}
this.dispatchEvent('renderinit');
this.contactData = currentContact;
this.contactsCount = contactsCount;
// Initially enabled and only disabled if necessary
this.editContactButton.removeAttribute('disabled');
this.editContactButton.classList.remove('hide');
this.header.setAttribute('action', 'back');
if (readOnly) {
this.editContactButton.classList.add('hide');
this.header.setAttribute('action', 'close');
}
this.doReloadContactDetails(this.contactData);
},
doReloadContactDetails: function(contact) {
this.setupDisplayName(contact);
this.contactDetails.classList.remove('no-photo');
this.contactDetails.classList.remove('up');
utils.dom.removeChildNodes(this.listContainer);
this.renderFavorite(contact);
this.renderOrg(contact);
ContactsButtons.renderPhones(contact);
ContactsButtons.renderEmails(contact);
this.renderWebrtcClient(contact);
this.renderAddresses(contact);
this.renderDates(contact);
this.renderNotes(contact);
this.renderShareButton(contact);
this.renderDuplicate(contact);
this.renderPhoto(contact);
this.dispatchEvent('renderdone');
},
setupDisplayName: function(contact) {
var name = this.getDisplayName(contact);
if (typeof name === 'string') {
this.detailsNameText.setAttribute('data-l10n-id', name);
} else if (name.raw) {
this.detailsNameText.textContent = name.raw;
} else if (name.id) {
navigator.mozL10n.setAttributes(this.detailsNameText,
name.id, name.args);
}
},
getDisplayName: function(contact) {
var l10nId = 'noName';
if (this.hasName(contact)) {
l10nId = {'raw': contact.name[0]};
} else if (this.hasContent(contact.tel)) {
l10nId = {'raw': contact.tel[0].value};
} else if (this.hasContent(contact.email)) {
l10nId = {'raw': contact.email[0].value};
}
return l10nId;
},
hasContent: function(field) {
return (Array.isArray(field) &&
field.length > 0 &&
field[0].value &&
field[0].value.trim());
},
hasName: function(contact) {
return (Array.isArray(contact.name) &&
contact.name[0] &&
contact.name[0].trim());
},
renderFavorite: function(contact) {
var favorite = this.isFavorite();
this.toggleFavoriteMessage(favorite);
this.header.classList.toggle('favorite', !!favorite);
},
toggleFavorite: function() {
var contact = this.contactData;
var favorite = !this.isFavorite();
this.toggleFavoriteMessage(favorite);
// Disabling button while saving the contact
this.favoriteMessage.style.pointerEvents = 'none';
this.dispatchEvent('toggleFavoriteAction',
{contact: contact, isFavorite: !favorite});
},
toggleFavoriteMessage: function(isFav) {
var cList = this.favoriteMessage.classList;
var l10nId = isFav ? 'removeFavorite' : 'addFavorite';
this.favoriteMessage.setAttribute('data-l10n-id', l10nId);
isFav ? cList.add('on') : cList.remove('on');
},
toggleFavoriteHandler: function(evt) {
var savedContact = evt.detail.contact;
this.favoriteMessage.style.pointerEvents = 'auto';
this.setContact(savedContact);
this.renderFavorite(savedContact);
this.isAFavoriteChange = true;
},
setContact: function(currentContact) {
this.contactData = currentContact;
},
renderOrg: function(contact) {
if (contact.org && contact.org.length > 0 && contact.org[0] !== '') {
this.orgTitle.textContent = contact.org[0];
this.orgTitle.classList.remove('hide');
} else {
this.orgTitle.classList.add('hide');
this.orgTitle.textContent = '';
}
},
renderWebrtcClient: function(contact) {
LazyLoader.load([
'/contacts/style/webrtc-client/webrtc_client.css',
'/contacts/js/webrtc-client/webrtc_client.js'
]).then(function() {
WebrtcClient.start(contact);
});
},
renderAddresses: function(contact) {
if (!contact.adr) {
return;
}
// Load what we need
LazyLoader.load('/contacts/js/utilities/mozContact.js', () => {
for (var i = 0; i < contact.adr.length; i++) {
var currentAddress = contact.adr[i];
// Sanity check
if (utils.mozContact.haveEmptyFields(currentAddress,
['streetAddress', 'postalCode', 'locality', 'countryName'])) {
continue;
}
var address = currentAddress.streetAddress || '';
var escapedStreet = Normalizer.escapeHTML(address, true);
var locality = currentAddress.locality;
var escapedLocality = Normalizer.escapeHTML(locality, true);
var country = currentAddress.countryName || '';
var escapedCountry = Normalizer.escapeHTML(country, true);
var postalCode = currentAddress.postalCode || '';
var escapedPostalCode = Normalizer.escapeHTML(postalCode, true);
var addressField = {
streetAddress: escapedStreet,
postalCode: escapedPostalCode,
locality: escapedLocality || '',
countryName: escapedCountry,
'type_l10n_id': currentAddress.type,
i: i
};
var template = utils.templates.render(this.addressesTemplate,
addressField);
this.listContainer.appendChild(template);
}
});
},
renderDates: function(contact) {
if (!contact.bday && !contact.anniversary) {
return;
}
var dateString = '';
var fields = ['bday', 'anniversary'];
var l10nIds = ['birthday', 'anniversary'];
var rendered = 0;
for (var j = 0; j < fields.length; j++) {
var value = contact[fields[j]];
if (!value) {
continue;
}
try {
dateString = utils.misc.formatDate(value);
} catch (err) {
console.error('Error parsing date');
continue;
}
var element = utils.templates.render(this.datesTemplate, {
i: ++rendered,
date: dateString,
'type_l10n_id': l10nIds[j]
});
this.listContainer.appendChild(element);
}
},
renderNotes: function(contact) {
if (!contact.note || contact.note.length === 0) {
return;
}
var container = document.createElement('li');
var title = document.createElement('h2');
title.setAttribute('data-l10n-id', 'comments');
container.appendChild(title);
for (var i = 0; i < contact.note.length; i++) {
var currentNote = contact.note[i];
var noteField = {
note: Normalizer.escapeHTML(currentNote, true) || '',
i: i
};
var template = utils.templates.render(this.notesTemplate, noteField);
container.appendChild(template);
this.listContainer.appendChild(container);
}
},
renderShareButton: function(contact) {
var social = utils.templates.render(this.socialTemplate, {
i: contact.id
});
var shareButton = social.querySelector('#share_button');
shareButton.addEventListener('click', () => {
this.dispatchEvent('shareAction', {contact: this.contactData});
});
shareButton.classList.remove('hide');
this.listContainer.appendChild(social);
},
renderDuplicate: function(contact) {
var dupItem = utils.templates.render(this.duplicateTemplate, {});
var findMergeButton = dupItem.querySelector('#find-merge-button');
findMergeButton.disabled = true;
if (this.contactsCount > 1) {
// Only have this active if contact list has more than one entry
findMergeButton.disabled = false;
findMergeButton.addEventListener('click', () => {
this.dispatchEvent('findDuplicatesAction',
{contactId: this.contactData.id});
});
}
this.listContainer.appendChild(dupItem);
},
renderPhoto: function(contact) {
this.contactDetails.classList.remove('up');
var photo = ContactPhotoHelper.getFullResolution(contact);
if (photo) {
var currentHash = this.cover.dataset.imgHash;
if (!currentHash) {
utils.dom.updatePhoto(photo, this.cover);
this.updateHash(photo, this.cover);
}
else {
// Need to recalculate the hash and see whether the images changed
this.calculateHash(photo, newHash => {
if (currentHash !== newHash) {
utils.dom.updatePhoto(photo, this.cover);
this.cover.dataset.imgHash = newHash;
}
else {
// Only for testing purposes
this.cover.dataset.photoReady = 'true';
}
});
}
this.contactDetails.classList.add('up');
this.cover.classList.add('translated');
this.contactDetails.classList.add('translated');
var clientHeight = this.contactDetails.clientHeight -
(INIT_MARGIN * 10 * SCALE_RATIO);
if (this.detailsInner.offsetHeight < clientHeight) {
this.cover.style.overflow = 'hidden';
} else {
this.cover.style.overflow = 'auto';
}
} else {
this.resetPhoto();
}
},
updateHash: function(photo, cover) {
this.calculateHash(photo, function(hash) {
cover.dataset.imgHash = hash;
});
},
calculateHash: function(photo, cb) {
var START_BYTES = 127;
var BYTES_HASH = 16;
var out = [photo.type, photo.size];
// We skip the first bytes that typically are headers
var chunk = photo.slice(START_BYTES, START_BYTES + BYTES_HASH);
var reader = new FileReader();
reader.onloadend = function() {
out.push(reader.result);
cb(out.join(''));
};
reader.onerror = function() {
window.console.error('Error while calculating the hash: ',
reader.error.name);
cb(out.join(''));
};
reader.readAsDataURL(chunk);
},
resetPhoto: function() {
this.cover.classList.remove('translated');
this.contactDetails.classList.remove('translated');
this.cover.style.backgroundImage = '';
this.cover.style.overflow = 'auto';
this.contactDetails.style.transform = '';
this.contactDetails.classList.add('no-photo');
this.cover.dataset.imgHash = '';
},
isFavorite: function() {
return this.contactData != null && this.contactData.category != null &&
this.contactData.category.indexOf('favorite') != -1;
},
dispatchEvent: function(name, data) {
window.dispatchEvent(new CustomEvent(name, {detail: data}));
}
};
exports.DetailsUI = DetailsUI;
})(window);

View file

@ -0,0 +1,7 @@
body * {
transition: opacity 0.3s ease;
}
body.hidden * {
opacity: 0;
}