Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
454 lines
12 KiB
JavaScript
Executable file
454 lines
12 KiB
JavaScript
Executable file
'use strict';
|
|
|
|
(function() {
|
|
// Ideally we should store this value in the configuration file.
|
|
// However, that means that we need to load two more script files and
|
|
// read directly from the config.json file or from the cookie storing
|
|
// the configuration data. This steals us ~150ms of startup time, so
|
|
// we chose to hard code this value instead.
|
|
const CACHE_ENABLED = true;
|
|
const FIRST_CHUNK = 'firstChunk';
|
|
|
|
const l10nStrings = {
|
|
'title': {
|
|
query: 'gaia-header h1#app-title[data-l10n-id="contacts"]',
|
|
container: 'textContent'
|
|
},
|
|
'search': {
|
|
query: 'input[data-l10n-id="search-contact"]',
|
|
container: 'placeholder'
|
|
}
|
|
};
|
|
|
|
var _observers = {};
|
|
|
|
function addObserver(aMessage, aCallback) {
|
|
if (typeof aCallback !== 'function') {
|
|
return;
|
|
}
|
|
|
|
if (!_observers[aMessage]) {
|
|
_observers[aMessage] = [];
|
|
}
|
|
_observers[aMessage].push(aCallback);
|
|
}
|
|
|
|
function notifyObservers(aMessage, aData) {
|
|
var observers = _observers[aMessage];
|
|
if (!observers) {
|
|
return;
|
|
}
|
|
|
|
for (var callback in observers) {
|
|
if (observers[callback]) {
|
|
observers[callback](aData);
|
|
}
|
|
}
|
|
}
|
|
|
|
// So far we only cache the first chunk of contacts, but we may want to
|
|
// extend the cache to a bigger or even the whole list in the future.
|
|
var _caches = new Map();
|
|
_caches.set(FIRST_CHUNK, {
|
|
containerId: 'groups-list',
|
|
active: false
|
|
});
|
|
|
|
var _cachedContacts;
|
|
var _cachedFavorites;
|
|
var _cachedHeaders;
|
|
var _cacheEvictionTimer;
|
|
|
|
function setCache(aCache) {
|
|
if (!CACHE_ENABLED || !aCache.id ||
|
|
!aCache.content || !aCache.lastOrderString ||
|
|
!_caches.has(aCache.id)) {
|
|
return;
|
|
}
|
|
|
|
var l10n = {};
|
|
Object.keys(l10nStrings).forEach(key => {
|
|
var string = l10nStrings[key];
|
|
var selector = document.querySelector(string.query);
|
|
if (!selector || !selector[string.container]) {
|
|
return;
|
|
}
|
|
l10n[key] = selector[string.container];
|
|
});
|
|
|
|
localStorage.setItem(aCache.id, JSON.stringify({
|
|
content: aCache.content,
|
|
languageDirection: navigator.mozL10n.language.direction,
|
|
languageCode: navigator.mozL10n.language.code,
|
|
lastOrderString: aCache.lastOrderString,
|
|
updated: Date.now(),
|
|
// Because we lazy load l10n.js quite late, we need to cache the
|
|
// localized strings to avoid showing the unlocalized text during
|
|
// the start up process.
|
|
l10n: l10n
|
|
}));
|
|
}
|
|
|
|
function deleteCache(aId) {
|
|
if (!CACHE_ENABLED) {
|
|
return;
|
|
}
|
|
localStorage.removeItem(aId);
|
|
}
|
|
|
|
function getCachedContacts(aNodeList) {
|
|
if (!CACHE_ENABLED || !aNodeList) {
|
|
return;
|
|
}
|
|
|
|
if (!_cachedContacts) {
|
|
_cachedContacts = new Map();
|
|
}
|
|
|
|
if (!_cachedFavorites) {
|
|
_cachedFavorites = new Map();
|
|
}
|
|
|
|
for (var i = 0; i < aNodeList.length; i++) {
|
|
var cacheEntry = aNodeList[i];
|
|
if (cacheEntry.parentNode.id == 'contact-list-ice') {
|
|
continue;
|
|
}
|
|
|
|
if (cacheEntry.parentNode.id == 'contacts-list-favorites') {
|
|
_cachedFavorites.set(cacheEntry.dataset.uuid,
|
|
cacheEntry.innerHTML);
|
|
continue;
|
|
}
|
|
|
|
_cachedContacts.set(cacheEntry.dataset.uuid,
|
|
cacheEntry.innerHTML);
|
|
}
|
|
}
|
|
|
|
function appendNodesToContainer(aContainer, aNodeList) {
|
|
if (!CACHE_ENABLED || !aNodeList.length) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
if (!_cachedHeaders) {
|
|
_cachedHeaders = {};
|
|
}
|
|
var resolved = false;
|
|
var fragment = document.createDocumentFragment();
|
|
aNodeList.forEach((node) => {
|
|
if (!node.elementName) {
|
|
return;
|
|
}
|
|
var child = document.createElement(node.elementName);
|
|
node.attributes && node.attributes.forEach((attribute) => {
|
|
if (!attribute.name || !attribute.value) {
|
|
return;
|
|
}
|
|
child.setAttribute(attribute.name, attribute.value);
|
|
});
|
|
if (node.innerHTML) {
|
|
child.innerHTML = node.innerHTML;
|
|
}
|
|
fragment.appendChild(child);
|
|
|
|
var headerName = child.id.split('section-group-')[1];
|
|
_cachedHeaders[headerName] =
|
|
fragment.querySelector('ol#contacts-list-' + headerName);
|
|
|
|
if (!resolved) {
|
|
resolve();
|
|
}
|
|
});
|
|
getCachedContacts(fragment.querySelectorAll('li[data-cache=true]'));
|
|
aContainer.appendChild(fragment);
|
|
});
|
|
}
|
|
|
|
function removeCacheFromContainer(aContainer) {
|
|
if (!CACHE_ENABLED || !_cachedContacts) {
|
|
return;
|
|
}
|
|
for (var id of _cachedContacts.keys()) {
|
|
var items = aContainer.querySelectorAll('li[data-uuid=\"' + id + '\"]');
|
|
Array.prototype.forEach.call(items, (item) => {
|
|
var ol = item.parentNode;
|
|
ol.removeChild(item);
|
|
if (ol.children.length < 1) {
|
|
var header = _cachedHeaders[ol.dataset.group];
|
|
if (header) {
|
|
var groupTitle = header.parentNode.children[0];
|
|
groupTitle.classList.add('hide');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function undoCache(aCacheId) {
|
|
if (!CACHE_ENABLED || !_caches.has(aCacheId)) {
|
|
return;
|
|
}
|
|
|
|
var _cache = _caches.get(aCacheId);
|
|
|
|
if (!_cache.containerId) {
|
|
return;
|
|
}
|
|
|
|
var container = document.getElementById(_cache.containerId);
|
|
if (!container) {
|
|
console.warning('Could not remove cached content from ' +
|
|
_cache.containerId);
|
|
return;
|
|
}
|
|
|
|
removeCacheFromContainer(container);
|
|
}
|
|
|
|
var Cache = {
|
|
get active() {
|
|
return CACHE_ENABLED && _caches.get(FIRST_CHUNK).active;
|
|
},
|
|
|
|
get enabled() {
|
|
return CACHE_ENABLED;
|
|
},
|
|
|
|
get contacts() {
|
|
return (CACHE_ENABLED && _cachedContacts) ? _cachedContacts.keys()
|
|
: null;
|
|
},
|
|
|
|
get favorites() {
|
|
return (CACHE_ENABLED && _cachedFavorites) ? _cachedFavorites.keys()
|
|
: null;
|
|
},
|
|
|
|
get length() {
|
|
return (CACHE_ENABLED && _cachedContacts) ?
|
|
_cachedContacts.size + _cachedFavorites.size : 0;
|
|
},
|
|
|
|
get headers() {
|
|
return (CACHE_ENABLED && _cachedHeaders) ? _cachedHeaders
|
|
: {};
|
|
},
|
|
|
|
// We need to know the order string of the last contact in the cache
|
|
// so we can insert externaly added contacts in the appropriate order.
|
|
get lastOrderString() {
|
|
return (CACHE_ENABLED && _caches.get(FIRST_CHUNK).lastOrderString);
|
|
},
|
|
|
|
get updated() {
|
|
return (CACHE_ENABLED && _caches.get(FIRST_CHUNK).updated);
|
|
},
|
|
|
|
// For testing purposes only.
|
|
get _rawContent() {
|
|
return _caches.get(FIRST_CHUNK).content;
|
|
},
|
|
|
|
set firstChunk(aFirstChunk) {
|
|
if (!CACHE_ENABLED || !aFirstChunk ||
|
|
!aFirstChunk.cache || !aFirstChunk.lastOrderString) {
|
|
return;
|
|
}
|
|
setCache({
|
|
id: FIRST_CHUNK,
|
|
content: aFirstChunk.cache,
|
|
lastOrderString: aFirstChunk.lastOrderString
|
|
});
|
|
},
|
|
|
|
set oneviction(aCallback) {
|
|
if (!CACHE_ENABLED) {
|
|
return;
|
|
}
|
|
addObserver('oneviction', aCallback);
|
|
},
|
|
|
|
hasContact: function(aUuid) {
|
|
if (!CACHE_ENABLED) {
|
|
return;
|
|
}
|
|
return _cachedContacts && _cachedContacts.has(aUuid);
|
|
},
|
|
|
|
hasFavorite: function(aUuid) {
|
|
if (!CACHE_ENABLED) {
|
|
return;
|
|
}
|
|
return _cachedFavorites && _cachedFavorites.has(aUuid);
|
|
},
|
|
|
|
getContact: function(aUuid) {
|
|
if (!CACHE_ENABLED || !this.hasContact(aUuid)) {
|
|
return;
|
|
}
|
|
// We should get each contact once while rendering the contacts list
|
|
// to see if what we have in the cache is different to what we have in
|
|
// the contacts source (most likely mozContacts). Removing the contact
|
|
// entry from the map allow us to easily check if we have any contact in
|
|
// the cache that was deleted from the original source and so it needs
|
|
// to be removed from the view.
|
|
var contact = _cachedContacts.get(aUuid);
|
|
_cachedContacts.delete(aUuid);
|
|
return contact;
|
|
},
|
|
|
|
getFavorite: function(aUuid) {
|
|
if (!CACHE_ENABLED || !this.hasFavorite(aUuid)) {
|
|
return;
|
|
}
|
|
var contact = _cachedFavorites.get(aUuid);
|
|
_cachedFavorites.delete(aUuid);
|
|
return contact;
|
|
},
|
|
|
|
removeContact: function(aUuid) {
|
|
if (!CACHE_ENABLED ||
|
|
!this.hasContact(aUuid) ||
|
|
!this.hasFavorite(aUuid)) {
|
|
return;
|
|
}
|
|
if (_cachedContacts.has(aUuid)) {
|
|
_cachedContacts.delete(aUuid);
|
|
}
|
|
if (_cachedFavorites.has(aUuid)) {
|
|
_cachedFavorites.delete(aUuid);
|
|
}
|
|
},
|
|
|
|
_doEvict: function(aUndo) {
|
|
_cacheEvictionTimer = null;
|
|
if (aUndo) {
|
|
undoCache(FIRST_CHUNK);
|
|
}
|
|
deleteCache(FIRST_CHUNK);
|
|
this.cleanup();
|
|
notifyObservers('oneviction');
|
|
},
|
|
|
|
evict: function(aUndo, aInstant) {
|
|
if (!CACHE_ENABLED) {
|
|
return;
|
|
}
|
|
|
|
// There are cases where we need to evict the cache
|
|
// inmediately, like in web activities, where we can be closed
|
|
// before doing the eviction.
|
|
if (aInstant) {
|
|
this._doEvict(aUndo);
|
|
return;
|
|
}
|
|
|
|
// Evicting the cache means accessing localStorage.
|
|
// Use the following logic to evict the cache in a moderate pattern.
|
|
// 1. When a request to evict the cache is received, start a timer.
|
|
// 2. If another request is received while the timer is
|
|
// running, reset it.
|
|
// 3. Once the timer fires, evict the cache.
|
|
|
|
if (_cacheEvictionTimer) {
|
|
clearTimeout(_cacheEvictionTimer);
|
|
}
|
|
|
|
_cacheEvictionTimer = setTimeout(() => {
|
|
this._doEvict(aUndo);
|
|
}, 1000);
|
|
},
|
|
|
|
maybeEvict: function() {
|
|
if (!CACHE_ENABLED || !this.active) {
|
|
return;
|
|
}
|
|
if ((_caches.get(FIRST_CHUNK).languageDirection !=
|
|
navigator.mozL10n.language.direction) ||
|
|
(_caches.get(FIRST_CHUNK).languageCode !=
|
|
navigator.mozL10n.language.code)) {
|
|
// If the language direction or code changed, we need to evict and also
|
|
// undo the applied cache.
|
|
this.evict(true /* remove applied cache from DOM */);
|
|
}
|
|
},
|
|
|
|
cleanup: function() {
|
|
if (!CACHE_ENABLED) {
|
|
return;
|
|
}
|
|
_caches.get(FIRST_CHUNK).active = false;
|
|
_caches.get(FIRST_CHUNK).content = null;
|
|
_cachedContacts = null;
|
|
_cachedFavorites = null;
|
|
_cachedHeaders = null;
|
|
},
|
|
|
|
apply: function(aCacheId) {
|
|
|
|
if (!CACHE_ENABLED || !_caches.has(aCacheId)) {
|
|
return Promise.reject();
|
|
}
|
|
|
|
var _cache = _caches.get(aCacheId);
|
|
|
|
var cache = localStorage.getItem(aCacheId);
|
|
if (!cache) {
|
|
return Promise.reject();
|
|
}
|
|
|
|
try {
|
|
cache = JSON.parse(cache);
|
|
_cache.content = cache.content;
|
|
_cache.languageDirection = cache.languageDirection;
|
|
_cache.languageCode = cache.languageCode;
|
|
_cache.lastOrderString = cache.lastOrderString;
|
|
_cache.updated = new Date(cache.updated);
|
|
} catch(e) {
|
|
console.error(e);
|
|
return Promise.reject();
|
|
}
|
|
|
|
if (!_cache.containerId) {
|
|
return Promise.reject();
|
|
}
|
|
|
|
if (cache.l10n) {
|
|
Object.keys(cache.l10n).forEach(key => {
|
|
if (!l10nStrings[key] || cache.l10n[key] === 'undefined') {
|
|
return;
|
|
}
|
|
var string = l10nStrings[key];
|
|
var selector = document.querySelector(string.query);
|
|
if (!selector) {
|
|
return;
|
|
}
|
|
selector[string.container] = cache.l10n[key];
|
|
});
|
|
}
|
|
|
|
if (cache.languageDirection) {
|
|
var html = document.querySelector('html');
|
|
html.setAttribute('dir', cache.languageDirection);
|
|
}
|
|
|
|
var container = document.getElementById(_cache.containerId);
|
|
if (!container) {
|
|
console.warning('Could not apply cached content to ' +
|
|
_cache.containerId);
|
|
return Promise.reject();
|
|
}
|
|
|
|
return appendNodesToContainer(container, _cache.content).then(() => {
|
|
_cache.content = null;
|
|
_cache.active = true;
|
|
_caches.set(aCacheId, _cache);
|
|
return Promise.resolve();
|
|
});
|
|
}
|
|
};
|
|
|
|
window.Cache = Cache;
|
|
})();
|