Merge autoland to mozilla-central. a=merge
[gecko.git] / browser / actors / AboutReaderParent.sys.mjs
blob00126910ae083a4b99dd3cb987105294b1305c27
1 // -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 const lazy = {};
8 ChromeUtils.defineESModuleGetters(lazy, {
9   PageActions: "resource:///modules/PageActions.sys.mjs",
10   PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
11   ReaderMode: "resource://gre/modules/ReaderMode.sys.mjs",
12 });
14 // A set of all of the AboutReaderParent actors that exist.
15 // See bug 1631146 for a request for a less manual way of doing this.
16 let gAllActors = new Set();
18 // A map of message names to listeners that listen to messages
19 // received by the AboutReaderParent actors.
20 let gListeners = new Map();
22 // As a reader mode document could be loaded in a different process than
23 // the source article, temporarily cache the article data here in the
24 // parent while switching to it.
25 let gCachedArticles = new Map();
27 export class AboutReaderParent extends JSWindowActorParent {
28   didDestroy() {
29     gAllActors.delete(this);
31     if (this.isReaderMode()) {
32       let url = this.manager.documentURI.spec;
33       url = decodeURIComponent(url.substr("about:reader?url=".length));
34       gCachedArticles.delete(url);
35     }
36   }
38   isReaderMode() {
39     return this.manager.documentURI.spec.startsWith("about:reader");
40   }
42   static addMessageListener(name, listener) {
43     if (!gListeners.has(name)) {
44       gListeners.set(name, new Set([listener]));
45     } else {
46       gListeners.get(name).add(listener);
47     }
48   }
50   static removeMessageListener(name, listener) {
51     if (!gListeners.has(name)) {
52       return;
53     }
55     gListeners.get(name).delete(listener);
56   }
58   static broadcastAsyncMessage(name, data) {
59     for (let actor of gAllActors) {
60       // Ignore errors for actors that might not be valid yet or anymore.
61       try {
62         actor.sendAsyncMessage(name, data);
63       } catch (ex) {}
64     }
65   }
67   callListeners(message) {
68     let listeners = gListeners.get(message.name);
69     if (!listeners) {
70       return;
71     }
73     message.target = this.browsingContext.embedderElement;
74     for (let listener of listeners.values()) {
75       try {
76         listener.receiveMessage(message);
77       } catch (e) {
78         console.error(e);
79       }
80     }
81   }
83   async receiveMessage(message) {
84     switch (message.name) {
85       case "Reader:EnterReaderMode": {
86         gCachedArticles.set(message.data.url, message.data);
87         this.enterReaderMode(message.data.url);
88         break;
89       }
90       case "Reader:LeaveReaderMode": {
91         this.leaveReaderMode();
92         break;
93       }
94       case "Reader:GetCachedArticle": {
95         let cachedArticle = gCachedArticles.get(message.data.url);
96         gCachedArticles.delete(message.data.url);
97         return cachedArticle;
98       }
99       case "Reader:FaviconRequest": {
100         try {
101           let preferredWidth = message.data.preferredWidth || 0;
102           let uri = Services.io.newURI(message.data.url);
104           let result = await new Promise(resolve => {
105             lazy.PlacesUtils.favicons.getFaviconURLForPage(
106               uri,
107               iconUri => {
108                 if (iconUri) {
109                   resolve({
110                     url: message.data.url,
111                     faviconUrl: iconUri.spec,
112                   });
113                 } else {
114                   resolve(null);
115                 }
116               },
117               preferredWidth
118             );
119           });
121           this.callListeners(message);
122           return result;
123         } catch (ex) {
124           console.error(
125             "Error requesting favicon URL for about:reader content: ",
126             ex
127           );
128         }
130         break;
131       }
133       case "Reader:UpdateReaderButton": {
134         let browser = this.browsingContext.embedderElement;
135         if (!browser) {
136           return undefined;
137         }
139         if (message.data && message.data.isArticle !== undefined) {
140           browser.isArticle = message.data.isArticle;
141         }
142         this.updateReaderButton(browser);
143         this.callListeners(message);
144         break;
145       }
147       case "RedirectTo": {
148         gCachedArticles.set(message.data.newURL, message.data.article);
149         // This is setup as a query so we can navigate the page after we've
150         // cached the relevant info in the parent.
151         return true;
152       }
154       default:
155         this.callListeners(message);
156         break;
157     }
159     return undefined;
160   }
162   static updateReaderButton(browser) {
163     let windowGlobal = browser.browsingContext.currentWindowGlobal;
164     let actor = windowGlobal.getActor("AboutReader");
165     actor.updateReaderButton(browser);
166   }
168   updateReaderButton(browser) {
169     let tabBrowser = browser.getTabBrowser();
170     if (!tabBrowser || browser != tabBrowser.selectedBrowser) {
171       return;
172     }
174     let doc = browser.ownerGlobal.document;
175     let button = doc.getElementById("reader-mode-button");
176     let menuitem = doc.getElementById("menu_readerModeItem");
177     let key = doc.getElementById("key_toggleReaderMode");
178     if (this.isReaderMode()) {
179       gAllActors.add(this);
181       button.setAttribute("readeractive", true);
182       button.hidden = false;
183       doc.l10n.setAttributes(button, "reader-view-close-button");
185       menuitem.hidden = false;
186       doc.l10n.setAttributes(menuitem, "menu-view-close-readerview");
188       key.setAttribute("disabled", false);
190       Services.obs.notifyObservers(null, "reader-mode-available");
191     } else {
192       button.removeAttribute("readeractive");
193       button.hidden = !browser.isArticle;
194       doc.l10n.setAttributes(button, "reader-view-enter-button");
196       menuitem.hidden = !browser.isArticle;
197       doc.l10n.setAttributes(menuitem, "menu-view-enter-readerview");
199       key.setAttribute("disabled", !browser.isArticle);
201       if (browser.isArticle) {
202         Services.obs.notifyObservers(null, "reader-mode-available");
203       }
204     }
206     if (!button.hidden) {
207       lazy.PageActions.sendPlacedInUrlbarTrigger(button);
208     }
209   }
211   static forceShowReaderIcon(browser) {
212     browser.isArticle = true;
213     AboutReaderParent.updateReaderButton(browser);
214   }
216   static buttonClick(event) {
217     if (event.button != 0) {
218       return;
219     }
220     AboutReaderParent.toggleReaderMode(event);
221   }
223   static toggleReaderMode(event) {
224     let win = event.target.ownerGlobal;
225     if (win.gBrowser) {
226       let browser = win.gBrowser.selectedBrowser;
228       let windowGlobal = browser.browsingContext.currentWindowGlobal;
229       let actor = windowGlobal.getActor("AboutReader");
230       if (actor) {
231         if (actor.isReaderMode()) {
232           gAllActors.delete(this);
233         }
234         actor.sendAsyncMessage("Reader:ToggleReaderMode", {});
235       }
236     }
237   }
239   hasReaderModeEntryAtOffset(url, offset) {
240     if (Services.appinfo.sessionHistoryInParent) {
241       let browsingContext = this.browsingContext;
242       if (browsingContext.childSessionHistory.canGo(offset)) {
243         let shistory = browsingContext.sessionHistory;
244         let nextEntry = shistory.getEntryAtIndex(shistory.index + offset);
245         let nextURL = nextEntry.URI.spec;
246         return nextURL && (nextURL == url || !url);
247       }
248     }
250     return false;
251   }
253   enterReaderMode(url) {
254     let readerURL = "about:reader?url=" + encodeURIComponent(url);
255     if (this.hasReaderModeEntryAtOffset(readerURL, +1)) {
256       let browsingContext = this.browsingContext;
257       browsingContext.childSessionHistory.go(+1);
258       return;
259     }
261     this.sendAsyncMessage("Reader:EnterReaderMode", {});
262   }
264   leaveReaderMode() {
265     let browsingContext = this.browsingContext;
266     let url = browsingContext.currentWindowGlobal.documentURI.spec;
267     let originalURL = lazy.ReaderMode.getOriginalUrl(url);
268     if (this.hasReaderModeEntryAtOffset(originalURL, -1)) {
269       browsingContext.childSessionHistory.go(-1);
270       return;
271     }
273     this.sendAsyncMessage("Reader:LeaveReaderMode", {});
274   }
276   /**
277    * Gets an article for a given URL. This method will download and parse a document.
278    *
279    * @param url The article URL.
280    * @param browser The browser where the article is currently loaded.
281    * @return {Promise}
282    * @resolves JS object representing the article, or null if no article is found.
283    */
284   async _getArticle(url) {
285     return lazy.ReaderMode.downloadAndParseDocument(url).catch(e => {
286       if (e && e.newURL) {
287         // Pass up the error so we can navigate the browser in question to the new URL:
288         throw e;
289       }
290       console.error("Error downloading and parsing document: ", e);
291       return null;
292     });
293   }