summaryrefslogtreecommitdiffstats
path: root/browser/actors/AboutReaderParent.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/actors/AboutReaderParent.sys.mjs')
-rw-r--r--browser/actors/AboutReaderParent.sys.mjs340
1 files changed, 340 insertions, 0 deletions
diff --git a/browser/actors/AboutReaderParent.sys.mjs b/browser/actors/AboutReaderParent.sys.mjs
new file mode 100644
index 0000000000..8ffc2ce0aa
--- /dev/null
+++ b/browser/actors/AboutReaderParent.sys.mjs
@@ -0,0 +1,340 @@
+// -*- 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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+});
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "ReaderMode",
+ "resource://gre/modules/ReaderMode.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "pktApi",
+ "chrome://pocket/content/pktApi.jsm"
+);
+
+// A set of all of the AboutReaderParent actors that exist.
+// See bug 1631146 for a request for a less manual way of doing this.
+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:PocketLoginStatusRequest": {
+ return lazy.pktApi.isUserLoggedIn();
+ }
+ case "Reader:PocketGetArticleInfo": {
+ return new Promise(resolve => {
+ lazy.pktApi.getArticleInfo(message.data.url, {
+ success: data => {
+ resolve(data);
+ },
+ error: error => {
+ resolve(null);
+ },
+ });
+ });
+ }
+ case "Reader:PocketGetArticleRecs": {
+ return new Promise(resolve => {
+ lazy.pktApi.getRecsForItem(message.data.itemID, {
+ success: data => {
+ resolve(data);
+ },
+ error: error => {
+ resolve(null);
+ },
+ });
+ });
+ }
+ case "Reader:PocketSaveArticle": {
+ return new Promise(resolve => {
+ lazy.pktApi.addLink(message.data.url, {
+ success: data => {
+ resolve(data);
+ },
+ error: error => {
+ resolve(null);
+ },
+ });
+ });
+ }
+ 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) {
+ iconUri = lazy.PlacesUtils.favicons.getFaviconLinkForIcon(
+ iconUri
+ );
+ resolve({
+ url: message.data.url,
+ faviconUrl: iconUri.pathQueryRef.replace(/^favicon:/, ""),
+ });
+ } 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");
+ }
+ }
+ }
+
+ static forceShowReaderIcon(browser) {
+ browser.isArticle = true;
+ AboutReaderParent.updateReaderButton(browser);
+ }
+
+ static buttonClick(event) {
+ if (event.button != 0) {
+ return;
+ }
+ AboutReaderParent.toggleReaderMode(event);
+ }
+
+ 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, browser) {
+ 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;
+ });
+ }
+}