Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
// Used as storage mechanism: a JSON array of { id, at } for the matches behind the dot,
// newest match first and one entry per monitor.
const PREF_MATCHES = "browser.smartwindow.agent.monitorAttention";
// 7 days in milliseconds.
export const MONITOR_ATTENTION_LIFETIME_MS = 7 * 24 * 60 * 60 * 1000;
/**
* Which monitors have matched their condition since the user last opened the
* panel, and which of those are still recent enough to be worth saying. This is
* one half of what puts the dot on the monitor toolbar button; announcing the
* feature as new is the other, and lives with the button itself.
*
* This owns the pref and nothing else. It has no notion of a window, so
* repainting the button after a change is the caller's job.
*
* @typedef {{id: string, at: number}} MonitorMatch
* A monitor id and the epoch milliseconds at which it matched.
*/
export const MonitorAttention = {
/**
* The monitors worth drawing attention to, dropping matches the user never
* came back for so a stale one stops being advertised.
*
* @returns {string[]} Monitor ids, newest match first.
*/
get matchedIds() {
return this.unexpiredIds(this._read());
},
/**
* @returns {boolean} Whether any monitor has matched since the user last
* opened the panel.
*/
get hasMatches() {
return !!this.matchedIds.length;
},
/**
* @param {string} monitorId - The monitor whose run met its condition.
*/
recordMatch(monitorId) {
if (!monitorId) {
return;
}
Services.prefs.setStringPref(
PREF_MATCHES,
JSON.stringify(this.withMatch(this._read(), monitorId))
);
},
clearMatches() {
Services.prefs.clearUserPref(PREF_MATCHES);
},
/**
* Reads stored matches. Anything unrecognized reads as no matches rather
* than throwing, so a corrupt value cannot take the toolbar button down with
* it, and one bad entry does not discard the good ones around it.
*
* @param {string} stored - The stored JSON, or [] when nothing is stored.
* @returns {MonitorMatch[]} Matches newest first, as stored.
*/
parseMatches(stored) {
if (!stored) {
return [];
}
let parsed;
try {
parsed = JSON.parse(stored);
} catch {
return [];
}
if (!Array.isArray(parsed)) {
return [];
}
return parsed.filter(match => match?.id && typeof match.at == "number");
},
/**
* @param {MonitorMatch[]} matches
* @param {number} [now] - Epoch milliseconds to measure the lifetime against.
* @returns {string[]} Monitor ids, newest match first.
*/
unexpiredIds(matches, now = Date.now()) {
const cutoff = now - MONITOR_ATTENTION_LIFETIME_MS;
return matches.filter(match => match.at > cutoff).map(match => match.id);
},
/**
* Records a match, keeping one entry per monitor. The order is stored rather
* than recovered by comparing timestamps afterwards, because two monitors
* can match in the same millisecond and then cannot be told apart.
*
* @param {MonitorMatch[]} matches
* @param {string} monitorId - The monitor whose run met its condition.
* @param {number} [now] - Epoch milliseconds of the match.
* @returns {MonitorMatch[]} Matches newest first, with this one at the front.
*/
withMatch(matches, monitorId, now = Date.now()) {
const rest = matches.filter(match => match.id !== monitorId);
return [{ id: monitorId, at: now }, ...rest];
},
/**
* @returns {MonitorMatch[]} Matches newest first, as stored.
*/
_read() {
return this.parseMatches(Services.prefs.getStringPref(PREF_MATCHES, ""));
},
};