AboutReaderParent.sys.mjs (8246B)
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/. */ 5 6 const lazy = {}; 7 8 ChromeUtils.defineESModuleGetters(lazy, { 9 PageActions: "resource:///modules/PageActions.sys.mjs", 10 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 11 ReaderMode: "moz-src:///toolkit/components/reader/ReaderMode.sys.mjs", 12 }); 13 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(); 17 18 // A map of message names to listeners that listen to messages 19 // received by the AboutReaderParent actors. 20 let gListeners = new Map(); 21 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(); 26 27 export class AboutReaderParent extends JSWindowActorParent { 28 didDestroy() { 29 gAllActors.delete(this); 30 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 } 37 38 isReaderMode() { 39 return this.manager.documentURI.spec.startsWith("about:reader"); 40 } 41 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 } 49 50 static removeMessageListener(name, listener) { 51 if (!gListeners.has(name)) { 52 return; 53 } 54 55 gListeners.get(name).delete(listener); 56 } 57 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 } 66 67 callListeners(message) { 68 let listeners = gListeners.get(message.name); 69 if (!listeners) { 70 return; 71 } 72 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 } 82 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); 103 104 let result = await lazy.PlacesUtils.favicons.getFaviconForPage( 105 uri, 106 preferredWidth 107 ); 108 109 this.callListeners(message); 110 return result && { url: uri.spec, faviconUrl: result.uri.spec }; 111 } catch (ex) { 112 console.error( 113 "Error requesting favicon URL for about:reader content: ", 114 ex 115 ); 116 } 117 118 break; 119 } 120 121 case "Reader:UpdateReaderButton": { 122 let browser = this.browsingContext.embedderElement; 123 if (!browser) { 124 return undefined; 125 } 126 127 if (message.data && message.data.isArticle !== undefined) { 128 browser.isArticle = message.data.isArticle; 129 } 130 this.updateReaderButton(browser); 131 this.callListeners(message); 132 break; 133 } 134 135 case "RedirectTo": { 136 gCachedArticles.set(message.data.newURL, message.data.article); 137 // This is setup as a query so we can navigate the page after we've 138 // cached the relevant info in the parent. 139 return true; 140 } 141 142 default: 143 this.callListeners(message); 144 break; 145 } 146 147 return undefined; 148 } 149 150 static updateReaderButton(browser) { 151 let windowGlobal = browser.browsingContext.currentWindowGlobal; 152 let actor = windowGlobal.getActor("AboutReader"); 153 actor.updateReaderButton(browser); 154 } 155 156 updateReaderButton(browser) { 157 let tabBrowser = browser.getTabBrowser(); 158 if (!tabBrowser || browser != tabBrowser.selectedBrowser) { 159 return; 160 } 161 162 let doc = browser.ownerGlobal.document; 163 let button = doc.getElementById("reader-mode-button"); 164 let menuitem = doc.getElementById("menu_readerModeItem"); 165 let key = doc.getElementById("key_toggleReaderMode"); 166 if (this.isReaderMode()) { 167 gAllActors.add(this); 168 169 button.setAttribute("readeractive", true); 170 button.hidden = false; 171 doc.l10n.setAttributes(button, "reader-view-close-button"); 172 173 menuitem.hidden = false; 174 doc.l10n.setAttributes(menuitem, "menu-view-close-readerview"); 175 176 key.removeAttribute("disabled"); 177 178 Services.obs.notifyObservers(null, "reader-mode-available"); 179 } else { 180 button.removeAttribute("readeractive"); 181 button.hidden = !browser.isArticle; 182 doc.l10n.setAttributes(button, "reader-view-enter-button"); 183 184 menuitem.hidden = !browser.isArticle; 185 doc.l10n.setAttributes(menuitem, "menu-view-enter-readerview"); 186 187 key.toggleAttribute("disabled", !browser.isArticle); 188 189 if (browser.isArticle) { 190 Services.obs.notifyObservers(null, "reader-mode-available"); 191 } 192 } 193 194 if (!button.hidden) { 195 lazy.PageActions.sendPlacedInUrlbarTrigger(button); 196 } 197 } 198 199 static forceShowReaderIcon(browser) { 200 browser.isArticle = true; 201 AboutReaderParent.updateReaderButton(browser); 202 } 203 204 static toggleReaderMode(event) { 205 let win = event.target.ownerGlobal; 206 if (win.gBrowser) { 207 let browser = win.gBrowser.selectedBrowser; 208 209 let windowGlobal = browser.browsingContext.currentWindowGlobal; 210 let actor = windowGlobal.getActor("AboutReader"); 211 if (actor) { 212 if (actor.isReaderMode()) { 213 gAllActors.delete(this); 214 } 215 actor.sendAsyncMessage("Reader:ToggleReaderMode", {}); 216 } 217 } 218 } 219 220 hasReaderModeEntryAtOffset(url, offset) { 221 if (Services.appinfo.sessionHistoryInParent) { 222 let browsingContext = this.browsingContext; 223 if (browsingContext.childSessionHistory.canGo(offset)) { 224 let shistory = browsingContext.sessionHistory; 225 let nextEntry = shistory.getEntryAtIndex(shistory.index + offset); 226 let nextURL = nextEntry.URI.spec; 227 return nextURL && (nextURL == url || !url); 228 } 229 } 230 231 return false; 232 } 233 234 enterReaderMode(url) { 235 let readerURL = "about:reader?url=" + encodeURIComponent(url); 236 if (this.hasReaderModeEntryAtOffset(readerURL, +1)) { 237 let browsingContext = this.browsingContext; 238 browsingContext.childSessionHistory.go(+1); 239 return; 240 } 241 242 this.sendAsyncMessage("Reader:EnterReaderMode", {}); 243 } 244 245 leaveReaderMode() { 246 let browsingContext = this.browsingContext; 247 let url = browsingContext.currentWindowGlobal.documentURI.spec; 248 let originalURL = lazy.ReaderMode.getOriginalUrl(url); 249 if (this.hasReaderModeEntryAtOffset(originalURL, -1)) { 250 browsingContext.childSessionHistory.go(-1); 251 return; 252 } 253 254 this.sendAsyncMessage("Reader:LeaveReaderMode", {}); 255 } 256 257 /** 258 * Gets an article for a given URL. This method will download and parse a document. 259 * 260 * @param url The article URL. 261 * @param browser The browser where the article is currently loaded. 262 * @return {Promise<?object>} 263 * Resolves to the JS object representing the article, or null if no article 264 * is found. 265 */ 266 async _getArticle(url) { 267 return lazy.ReaderMode.downloadAndParseDocument(url).catch(e => { 268 if (e && e.newURL) { 269 // Pass up the error so we can navigate the browser in question to the new URL: 270 throw e; 271 } 272 console.error("Error downloading and parsing document: ", e); 273 return null; 274 }); 275 } 276 }