VulpesOS/gaia/apps/email/js/html_cache_restore.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

495 lines
18 KiB
JavaScript
Executable file

/*jshint browser: true */
/*global performance, console, Notification */
'use strict';
var _xstart = performance.timing.fetchStart -
performance.timing.navigationStart;
window.plog = function(msg) {
console.log(msg + ' ' + (performance.now() - _xstart));
};
/**
* Apparently the event catching done for the startup events from
* performance_testing_helper record the last event received of that type as
* the official time, instead of using the first. So unfortunately, we need a
* global to make sure we do not emit the same events later.
* @type {Boolean}
*/
window.startupCacheEventsSent = false;
/**
* Version number for cache, allows expiring cache.
* Set by build process. Set as a global because it
* is also used in html_cache.js.
*/
window.HTML_CACHE_VERSION = '2';
/**
* A function callback is registered if the model needs to be loaded before
* final view determination can be done. This should be a rare event, but can
* happen, see localOnModelLoaded for more details. config.js looks for this
* callback.
*/
window.startupOnModelLoaded = null;
/**
* Tracks if a mozSetMessageHandler has been dispatched to code. This only
* exists to help us be efficient on closing the app in the case of notification
* close events. If the notification system improved so that the app could tell
* it not to notify us on close events, this could be removed. It is a global
* so that cronsync-main can set it for alarm operations. This file does not
* track alarm setMozMessageHandler messages directly, since they need to
* acquire wake locks in the event turn the message is received. Since that is a
* bit complicated, the back-end handles it, since it also knows more about the
* sync details.
*/
window.appDispatchedMessage = false;
(function() {
// Holds on to the pending message type that is indicated by
// mozHasPendingMessage. If it has a value, given by mozHasPendingMessage,
// then startup is put on hold until it arrives. Once the mozSetMessageHandler
// for the pending message type is received, this variable is cleared,
// opening the way for other mozSetMessageHandler messages to be processed.
// This is only important to track for messages that have UI impact, like
// inserting cards. Data messages, like alarm are fine to pass through
// unblocked.
var pendingUiMessageType = null;
// There are special tasks, like DOM injection from cache, and startup of the
// main JS scripts, that should only be done during startup, but once the app
// has started up, then this file just handles dispatching of messages
// received via mozSetMessageHandler.
var startingUp = true;
// Holds the determination of the entry point type and view that should be
// used for startup.
var startupData = {};
/**
* startupOnModelLoaded is set to this function if the
* data_has_account localStorage value is not known. Called eventually by
* config.js once the model module has been loaded.
*/
function localOnModelLoaded(model, callback) {
model.latestOnce('acctsSlice', function(acctsSlice) {
// At this point, model will have set up 'data_has_account', so the the
// final view can be set and the rest of the world can start turning.
// Note that alarm can get kicked off before this is all done,
// since the worker will have started up and registered to get those
// messages. It is OK though since not showing any UI. If this case is
// triggered, it will result in a UI display in that case, but that is OK,
// it only happens on an app update situation, from cookies to
// localStorage or some kind of localStorage reset. So a rare event, and
// does not cause harm, just a bit of extra work in those cases once.
console.log('localOnModelLoaded called, hasAccount: ' +
localStorage.getItem('data_has_account'));
setDefaultView();
hydrateHtml(startupData.view);
window.startupOnModelLoaded = null;
callback();
});
}
if (!localStorage.getItem('data_has_account')) {
console.log('data_has_account unknown, asking for model load first');
window.startupOnModelLoaded = localOnModelLoaded;
}
function hasAccount() {
// var _1 = performance.now();
var has = localStorage.getItem('data_has_account') === 'yes';
// console.log('@@@ LOCALSTORAGE GET data_has_account: ' +
// (performance.now() - _1));
return has;
}
function setDefaultView() {
if (hasAccount()) {
if (!startupData.view) {
startupData.view = 'message_list';
}
} else {
startupData.view = 'setup_account_info';
}
}
// Set up the default view, but if that is not possible to know yet, since
// the status of hasAccount is unknown, wait for the callback to set it up.
if (!window.startupOnModelLoaded) {
setDefaultView();
}
startupData.entry = 'default';
/**
* Makes sure the message type is wanted, given pendingUiMessageType concerns,
* and not coming in at a fast rate due to things like double clicks. Only
* necessary to use if the message is something that would insert cards, and
* fast entries could mess up card state.
*/
var lastEntryTime = 0;
function isUiMessageTypeAllowedEntry(type) {
// If startup is pending on a message, and this message type is not what is
// wanted, skip it.
if (pendingUiMessageType && pendingUiMessageType !== type) {
console.log('Ignoring message of type: ' + type);
return false;
}
var entryTime = Date.now();
// This is the right pending startup message, so proceed without checking
// the entryTime as it is the first one allowed through.
if (pendingUiMessageType) {
pendingUiMessageType = null;
} else {
// Check for fast incoming messages, like from activity double taps, and
// ignore them, to avoid messing up the UI startup from double-click taps
// of activities/notifications that would insert new cards. Only one entry
// per second.
if (entryTime < lastEntryTime + 1000) {
console.log('email entry gate blocked fast repeated action: ' + type);
return false;
}
}
lastEntryTime = entryTime;
return true;
}
/**
* Gets the HTML string from cache, as well as language direction.
* This method assumes all cookie keys that have pattern
* /htmlc(\d+)/ are part of the object value. This method could
* throw given vagaries of cookie cookie storage and encodings.
* Be prepared.
*/
function retrieve(id) {
// var _1 = performance.now();
var value = localStorage.getItem('html_cache_' + id) || '';
// console.log('@@@ LOCALSTORAGE GET html_cache: ' +
// (performance.now() - _1));
var index, version, langDir;
// console.log('RETRIEVED: ' + 'html_cache_' + id + ': ' + value);
index = value.indexOf(':');
if (index === -1) {
value = '';
} else {
version = value.substring(0, index);
value = value.substring(index + 1);
// Version is further subdivided to include lang direction. See email's
// l10n.js for logic to reset the dir back to the actual language choice
// if the language direction changes between email invocations.
var versionParts = version.split(',');
version = versionParts[0];
langDir = versionParts[1];
}
if (version !== window.HTML_CACHE_VERSION) {
console.log('Skipping html cache for ' + id + ', out of date. Expected ' +
window.HTML_CACHE_VERSION + ' but found ' + version);
value = '';
}
return {
langDir: langDir,
contents: value
};
}
// The evt module is needed to register for 'notification' events that are
// triggered by other code in the email app (not from the mozSetMessageHandler
// pathway), but it can be done lazily once the rest of the app has started up
// and has asked to listen for app messages.
var evt;
// Tracks the handlers that are registered via globalOnAppMessage. The
// handlers are stored in slots that are named for the message types, like
// 'activity', 'notification'.
var handlers = {};
// Holds on to messages that come in via mozSetMessageHandler until there is a
// handler that has been registred for that message type.
var handlerQueues = {
notification: [],
activity: []
};
/**
* Called by app code. Only expects one listener to be registered for each
* handler type. This function also assumes that a `require` loader is
* available to fetch the 'evt' module. This would not be needed if
* evt.emit('notification') was not triggered by the email code.
* @param {Object} listener Object whose keys are the handler type names and
* values are functions that handle that type.
*/
window.globalOnAppMessage = function(listener) {
Object.keys(listener).forEach(function(key) {
var fn = handlers[key] = listener[key];
var queue = handlerQueues[key];
if (queue.length) {
handlerQueues[key] = [];
queue.forEach(function(argsArray) {
fn.apply(undefined, argsArray);
});
}
});
// Only need to do this wiring once, but globalOnAppMessage could be called
// multiple times.
if (!evt) {
require(['evt'], function(ev) {
evt = ev;
evt.on('notification', onNotification);
});
}
return startupData;
};
// Attach the hasAccount so that other code can use it and always get the
// freshest cached value.
window.globalOnAppMessage.hasAccount = hasAccount;
function dispatch(type, args) {
window.appDispatchedMessage = true;
if (handlers[type]) {
return handlers[type].apply(undefined, args);
} else {
handlerQueues[type].push(args);
}
// On the very first dispatch when app open is triggered by a dispatchable
// event, need to finish bootup now that full startup state is known.
finishStartup();
}
/**
* Perform requested activity.
*
* @param {MozActivityRequestHandler} req activity invocation.
*/
function onActivityRequest(req) {
console.log('mozSetMessageHandler: received an activity');
if (!isUiMessageTypeAllowedEntry('activity')) {
return req.postError('cancelled');
}
// Right now all activity entry points go to compose, but may need to
// revisited if the activity entry points change.
if (startingUp) {
startupData.view = 'compose';
hydrateHtml(startupData.view);
}
dispatch('activity', [req]);
}
function onNotification(msg) {
console.log('mozSetMessageHandler: received a notification');
// Skip notification events that are not from a notification "click". The
// system app will also notify this method of any close events for
// notifications, which are not at all interesting.
if (!msg.clicked) {
// If a alarm is waiting right behind this notification message, that sync
// would will be lost when the application closes. It is an edge case
// though, and recoverable on the next sync, where trying to be
// accommodating to it here would add more code complexity, and it would
// still have a failure window where the app just starts up with a
// notification, but just after that, after startup is finished but before
// this function is called, a sync message is queued up. Activities could
// be dropped too in a similar situation, but we might already drop some
// due to the fast click gate. The long term fix is to just get a
// notification system that allows apps to tell it not to call it if just
// closing a notification.
// Only close if entry was a notification and no other messages, like a
// alarm or a UI-based message, have been dispatched.
if (startupData.entry === 'notification' &&
!window.appDispatchedMessage) {
console.log('App only started for notification close, closing app.');
window.close();
}
return;
}
// Bail early if notification is ignored. Do this before the notification
// close() work, so that user has the opportunity to tap on the notification
// later and still activate that notification flow.
if (!isUiMessageTypeAllowedEntry('notification')) {
return;
}
// Need to manually get all notifications and close the one that triggered
// this event due to fallout from 890440 and 966481.
if (typeof Notification !== 'undefined' && Notification.get) {
Notification.get().then(function(notifications) {
if (notifications) {
notifications.some(function(notification) {
// Compare tags, as the tag is based on the account ID and
// we only have one notification per account. Plus, there
// is no "id" field on the notification.
if (notification.tag === msg.tag && notification.close) {
notification.close();
return true;
}
});
}
});
}
// Adjust the startupData view as desired by the notification. For upgrade
// cases where a previous notification from an older version of email
// used the iconUrl, this just means we will got to the message_list instead
// of the message_reader for the single email notification case, but that is
// OK since it is a temporary upgrade issue, and the email will still be
// seen since it should be top of the list in the message_list.
var view = msg.data && msg.data.type;
if (startingUp && view) {
startupData.view = view;
hydrateHtml(view);
}
// The notification infrastructure does not automatically bring the app to
// the foreground, so if still hidden, show it now. Ideally this would not
// use a setTimeout, but it was getting incorrect document.hidden values on
// startup, where just a bit later the value does seem to be set correctly.
// The other option was to do this work inside the notification handler in
// mail_app, but the delay is long enough waiting for that point that the
// user might be concerned they did not correctly tap the notification.
setTimeout(function() {
if (document.hidden && navigator.mozApps) {
console.log('document was hidden, showing app via mozApps.getSelf');
navigator.mozApps.getSelf().onsuccess = function(event) {
var app = event.target.result;
app.launch();
};
}
}, 300);
dispatch('notification', [msg.data]);
}
var selfNode = document.querySelector('[data-loadsrc]');
function hydrateHtml(id) {
var parsedResults = retrieve(id);
if (parsedResults.langDir) {
document.querySelector('html').setAttribute('dir', parsedResults.langDir);
}
var contents = parsedResults.contents;
// Automatically restore the HTML as soon as module is executed.
// ASSUMES card node is available (DOMContentLoaded or execution of
// module after DOM node is in doc)
var cardsNode = document.getElementById(selfNode.dataset.targetid);
cardsNode.innerHTML = contents;
window.startupCacheEventsSent = !!contents;
if (contents) {
console.log('Using HTML cache for ' + id);
}
if (window.startupCacheEventsSent) {
window.performance.mark('navigationLoaded');
window.performance.mark('visuallyLoaded');
}
}
function finishStartup() {
// This can be called multiple times due to pendingUiMessageType listeners
// so only do the work once.
if (!startingUp) {
return;
}
var scriptNode = document.createElement('script'),
loader = selfNode.dataset.loader,
loadSrc = selfNode.dataset.loadsrc;
if (loader) {
scriptNode.setAttribute('data-main', loadSrc);
scriptNode.src = loader;
} else {
scriptNode.src = loadSrc;
}
document.head.appendChild(scriptNode);
startingUp = false;
}
// mozHasPendingMessage seems like it can only be called once per message
// type, so only asking once. If we have both an activity or a notification
// coming in, the activity should win since it is part of a larger user action
// besides just email, and expects to get some callbacks.
if (navigator.mozHasPendingMessage) {
if (navigator.mozHasPendingMessage('activity')) {
pendingUiMessageType = 'activity';
} else if (navigator.mozHasPendingMessage('notification')) {
pendingUiMessageType = 'notification';
}
if (pendingUiMessageType) {
startupData.entry = pendingUiMessageType;
} else if (navigator.mozHasPendingMessage('alarm')) {
// While alarm is not important for the pendingUiMessageType
// gateway, it still should be indicated that the entry point was not the
// default entry point, so that the UI is not fully started if this is a
// background sync.
startupData.entry = 'alarm';
} else if (navigator.mozHasPendingMessage('request-sync')) {
// Be nice and consume the message so that request-sync feels like it
// completed its mission, but ignore the actual message.
if (navigator.mozSetMessageHandler) {
navigator.mozSetMessageHandler('request-sync', function (e) {
console.log('Received legacy request-sync message, ignoring.');
});
}
// Legacy request-sync trigger. Just clear all request-sync tasks, and
// allow the UI to go to a normal view this time. When back end starts up
// it will ensure the sync alarms are set properly for the next sync, so
// in this legacy case we will just miss one sync pass.
var navSync = navigator.sync;
if (navSync) {
navSync.registrations().then(function(regs) {
regs.forEach(function(reg) {
console.log('Unregistering legacy request sync...');
navSync.unregister(reg.task);
console.log('Unregister done!');
});
}, function(err) {
console.error('navigator.sync.registrations failed: ', err);
});
}
}
}
if ('mozSetMessageHandler' in navigator) {
navigator.mozSetMessageHandler('notification', onNotification);
navigator.mozSetMessageHandler('activity', onActivityRequest);
} else {
console.warn('mozSetMessageHandler not available. No notifications, ' +
'activities or syncs.');
}
if (window.startupOnModelLoaded) {
finishStartup();
} else if (startupData.entry === 'default' ||
startupData.entry === 'alarm') {
hydrateHtml(startupData.view);
finishStartup();
}
}());