Source code
Revision control
Copy as Markdown
Other Tools
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
/* 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
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
PageActions: "resource:///modules/PageActions.sys.mjs",
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
ReaderMode: "resource://gre/modules/ReaderMode.sys.mjs",
});
// A set of all of the AboutReaderParent actors that exist.
let gAllActors = new Set();
// A map of message names to listeners that listen to messages
// received by the AboutReaderParent actors.
let gListeners = new Map();
// As a reader mode document could be loaded in a different process than
// the source article, temporarily cache the article data here in the
// parent while switching to it.
let gCachedArticles = new Map();
export class AboutReaderParent extends JSWindowActorParent {
didDestroy() {
gAllActors.delete(this);
if (this.isReaderMode()) {
let url = this.manager.documentURI.spec;
url = decodeURIComponent(url.substr("about:reader?url=".length));
gCachedArticles.delete(url);
}
}
isReaderMode() {
return this.manager.documentURI.spec.startsWith("about:reader");
}
static addMessageListener(name, listener) {
if (!gListeners.has(name)) {
gListeners.set(name, new Set([listener]));
} else {
gListeners.get(name).add(listener);
}
}
static removeMessageListener(name, listener) {
if (!gListeners.has(name)) {
return;
}
gListeners.get(name).delete(listener);
}
static broadcastAsyncMessage(name, data) {
for (let actor of gAllActors) {
// Ignore errors for actors that might not be valid yet or anymore.
try {
actor.sendAsyncMessage(name, data);
} catch (ex) {}
}
}
callListeners(message) {
let listeners = gListeners.get(message.name);
if (!listeners) {
return;
}
message.target = this.browsingContext.embedderElement;
for (let listener of listeners.values()) {
try {
listener.receiveMessage(message);
} catch (e) {
console.error(e);
}
}
}
async receiveMessage(message) {
switch (message.name) {
case "Reader:EnterReaderMode": {
gCachedArticles.set(message.data.url, message.data);
this.enterReaderMode(message.data.url);
break;
}
case "Reader:LeaveReaderMode": {
this.leaveReaderMode();
break;
}
case "Reader:GetCachedArticle": {
let cachedArticle = gCachedArticles.get(message.data.url);
gCachedArticles.delete(message.data.url);
return cachedArticle;
}
case "Reader:FaviconRequest": {
try {
let preferredWidth = message.data.preferredWidth || 0;
let uri = Services.io.newURI(message.data.url);
let result = await new Promise(resolve => {
lazy.PlacesUtils.favicons.getFaviconURLForPage(
uri,
iconUri => {
if (iconUri) {
resolve({
url: message.data.url,
faviconUrl: iconUri.spec,
});
} else {
resolve(null);
}
},
preferredWidth
);
});
this.callListeners(message);
return result;
} catch (ex) {
console.error(
"Error requesting favicon URL for about:reader content: ",
ex
);
}
break;
}
case "Reader:UpdateReaderButton": {
let browser = this.browsingContext.embedderElement;
if (!browser) {
return undefined;
}
if (message.data && message.data.isArticle !== undefined) {
browser.isArticle = message.data.isArticle;
}
this.updateReaderButton(browser);
this.callListeners(message);
break;
}
case "RedirectTo": {
gCachedArticles.set(message.data.newURL, message.data.article);
// This is setup as a query so we can navigate the page after we've
// cached the relevant info in the parent.
return true;
}
default:
this.callListeners(message);
break;
}
return undefined;
}
static updateReaderButton(browser) {
let windowGlobal = browser.browsingContext.currentWindowGlobal;
let actor = windowGlobal.getActor("AboutReader");
actor.updateReaderButton(browser);
}
updateReaderButton(browser) {
let tabBrowser = browser.getTabBrowser();
if (!tabBrowser || browser != tabBrowser.selectedBrowser) {
return;
}
let doc = browser.ownerGlobal.document;
let button = doc.getElementById("reader-mode-button");
let menuitem = doc.getElementById("menu_readerModeItem");
let key = doc.getElementById("key_toggleReaderMode");
if (this.isReaderMode()) {
gAllActors.add(this);
button.setAttribute("readeractive", true);
button.hidden = false;
doc.l10n.setAttributes(button, "reader-view-close-button");
menuitem.hidden = false;
doc.l10n.setAttributes(menuitem, "menu-view-close-readerview");
key.setAttribute("disabled", false);
Services.obs.notifyObservers(null, "reader-mode-available");
} else {
button.removeAttribute("readeractive");
button.hidden = !browser.isArticle;
doc.l10n.setAttributes(button, "reader-view-enter-button");
menuitem.hidden = !browser.isArticle;
doc.l10n.setAttributes(menuitem, "menu-view-enter-readerview");
key.setAttribute("disabled", !browser.isArticle);
if (browser.isArticle) {
Services.obs.notifyObservers(null, "reader-mode-available");
}
}
if (!button.hidden) {
lazy.PageActions.sendPlacedInUrlbarTrigger(button);
}
}
static forceShowReaderIcon(browser) {
browser.isArticle = true;
AboutReaderParent.updateReaderButton(browser);
}
static toggleReaderMode(event) {
let win = event.target.ownerGlobal;
if (win.gBrowser) {
let browser = win.gBrowser.selectedBrowser;
let windowGlobal = browser.browsingContext.currentWindowGlobal;
let actor = windowGlobal.getActor("AboutReader");
if (actor) {
if (actor.isReaderMode()) {
gAllActors.delete(this);
}
actor.sendAsyncMessage("Reader:ToggleReaderMode", {});
}
}
}
hasReaderModeEntryAtOffset(url, offset) {
if (Services.appinfo.sessionHistoryInParent) {
let browsingContext = this.browsingContext;
if (browsingContext.childSessionHistory.canGo(offset)) {
let shistory = browsingContext.sessionHistory;
let nextEntry = shistory.getEntryAtIndex(shistory.index + offset);
let nextURL = nextEntry.URI.spec;
return nextURL && (nextURL == url || !url);
}
}
return false;
}
enterReaderMode(url) {
let readerURL = "about:reader?url=" + encodeURIComponent(url);
if (this.hasReaderModeEntryAtOffset(readerURL, +1)) {
let browsingContext = this.browsingContext;
browsingContext.childSessionHistory.go(+1);
return;
}
this.sendAsyncMessage("Reader:EnterReaderMode", {});
}
leaveReaderMode() {
let browsingContext = this.browsingContext;
let url = browsingContext.currentWindowGlobal.documentURI.spec;
let originalURL = lazy.ReaderMode.getOriginalUrl(url);
if (this.hasReaderModeEntryAtOffset(originalURL, -1)) {
browsingContext.childSessionHistory.go(-1);
return;
}
this.sendAsyncMessage("Reader:LeaveReaderMode", {});
}
/**
* Gets an article for a given URL. This method will download and parse a document.
*
* @param url The article URL.
* @param browser The browser where the article is currently loaded.
* @return {Promise}
* @resolves JS object representing the article, or null if no article is found.
*/
async _getArticle(url) {
return lazy.ReaderMode.downloadAndParseDocument(url).catch(e => {
if (e && e.newURL) {
// Pass up the error so we can navigate the browser in question to the new URL:
throw e;
}
console.error("Error downloading and parsing document: ", e);
return null;
});
}
}