Bug 1866197 - Push about:restartrequired buildid mismatch telemetry to release 135...
[gecko.git] / browser / actors / AboutReaderChild.sys.mjs
blobb92078aaecee5aa2f40ddacf775d258b7bf65739
1 /* vim: set ts=2 sw=2 sts=2 et tw=80: */
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   AboutReader: "resource://gre/modules/AboutReader.sys.mjs",
10   ReaderMode: "resource://gre/modules/ReaderMode.sys.mjs",
11   Readerable: "resource://gre/modules/Readerable.sys.mjs",
12 });
14 var gUrlsToDocContentType = new Map();
15 var gUrlsToDocTitle = new Map();
17 export class AboutReaderChild extends JSWindowActorChild {
18   constructor() {
19     super();
21     this._reader = null;
22     this._articlePromise = null;
23     this._isLeavingReaderableReaderMode = false;
24   }
26   didDestroy() {
27     this.cancelPotentialPendingReadabilityCheck();
28     this.readerModeHidden();
29   }
31   readerModeHidden() {
32     if (this._reader) {
33       this._reader.clearActor();
34     }
35     this._reader = null;
36   }
38   async receiveMessage(message) {
39     switch (message.name) {
40       case "Reader:ToggleReaderMode":
41         if (!this.isAboutReader) {
42           gUrlsToDocContentType.set(
43             this.document.URL,
44             this.document.contentType
45           );
46           gUrlsToDocTitle.set(this.document.URL, this.document.title);
47           this._articlePromise = lazy.ReaderMode.parseDocument(
48             this.document
49           ).catch(console.error);
51           // Get the article data and cache it in the parent process. The reader mode
52           // page will retrieve it when it has loaded.
53           let article = await this._articlePromise;
54           this.sendAsyncMessage("Reader:EnterReaderMode", article);
55         } else {
56           this.closeReaderMode();
57         }
58         break;
60       case "Reader:PushState":
61         this.updateReaderButton(!!(message.data && message.data.isArticle));
62         break;
63       case "Reader:EnterReaderMode": {
64         lazy.ReaderMode.enterReaderMode(this.docShell, this.contentWindow);
65         break;
66       }
67       case "Reader:LeaveReaderMode": {
68         lazy.ReaderMode.leaveReaderMode(this.docShell, this.contentWindow);
69         break;
70       }
71     }
73     // Forward the message to the reader if it has been created.
74     if (this._reader) {
75       this._reader.receiveMessage(message);
76     }
77   }
79   get isAboutReader() {
80     if (!this.document) {
81       return false;
82     }
83     return this.document.documentURI.startsWith("about:reader");
84   }
86   get isReaderableAboutReader() {
87     return this.isAboutReader && !this.document.documentElement.dataset.isError;
88   }
90   handleEvent(aEvent) {
91     if (aEvent.originalTarget.defaultView != this.contentWindow) {
92       return;
93     }
95     switch (aEvent.type) {
96       case "DOMContentLoaded":
97         if (!this.isAboutReader) {
98           this.updateReaderButton();
99           return;
100         }
102         if (this.document.body) {
103           let url = this.document.documentURI;
104           if (!this._articlePromise) {
105             url = decodeURIComponent(url.substr("about:reader?url=".length));
106             this._articlePromise = this.sendQuery("Reader:GetCachedArticle", {
107               url,
108             });
109           }
110           // Update the toolbar icon to show the "reader active" icon.
111           this.sendAsyncMessage("Reader:UpdateReaderButton");
112           let docContentType =
113             gUrlsToDocContentType.get(url) === "text/plain"
114               ? "text/plain"
115               : "document";
117           let docTitle = gUrlsToDocTitle.get(url);
118           this._reader = new lazy.AboutReader(
119             this,
120             this._articlePromise,
121             docContentType,
122             docTitle
123           );
124           this._articlePromise = null;
125         }
126         break;
128       case "pagehide":
129         this.cancelPotentialPendingReadabilityCheck();
130         // this._isLeavingReaderableReaderMode is used here to keep the Reader Mode icon
131         // visible in the location bar when transitioning from reader-mode page
132         // back to the readable source page.
133         this.sendAsyncMessage("Reader:UpdateReaderButton", {
134           isArticle: this._isLeavingReaderableReaderMode,
135         });
136         this._isLeavingReaderableReaderMode = false;
137         break;
139       case "pageshow":
140         // If a page is loaded from the bfcache, we won't get a "DOMContentLoaded"
141         // event, so we need to rely on "pageshow" in this case.
142         if (aEvent.persisted && this.canDoReadabilityCheck()) {
143           this.performReadabilityCheckNow();
144         }
145         break;
146     }
147   }
149   /**
150    * NB: this function will update the state of the reader button asynchronously
151    * after the next mozAfterPaint call (assuming reader mode is enabled and
152    * this is a suitable document). Calling it on things which won't be
153    * painted is not going to work.
154    */
155   updateReaderButton(forceNonArticle) {
156     if (!this.canDoReadabilityCheck()) {
157       return;
158     }
160     this.scheduleReadabilityCheckPostPaint(forceNonArticle);
161   }
163   canDoReadabilityCheck() {
164     return (
165       lazy.Readerable.isEnabledForParseOnLoad &&
166       !this.isAboutReader &&
167       this.contentWindow &&
168       this.contentWindow.windowRoot &&
169       this.contentWindow.HTMLDocument.isInstance(this.document) &&
170       !this.document.mozSyntheticDocument
171     );
172   }
174   cancelPotentialPendingReadabilityCheck() {
175     if (this._pendingReadabilityCheck) {
176       if (this._listenerWindow) {
177         this._listenerWindow.removeEventListener(
178           "MozAfterPaint",
179           this._pendingReadabilityCheck
180         );
181       }
182       delete this._pendingReadabilityCheck;
183       delete this._listenerWindow;
184     }
185   }
187   scheduleReadabilityCheckPostPaint(forceNonArticle) {
188     if (this._pendingReadabilityCheck) {
189       // We need to stop this check before we re-add one because we don't know
190       // if forceNonArticle was true or false last time.
191       this.cancelPotentialPendingReadabilityCheck();
192     }
193     this._pendingReadabilityCheck = this.onPaintWhenWaitedFor.bind(
194       this,
195       forceNonArticle
196     );
198     this._listenerWindow = this.contentWindow.windowRoot;
199     this.contentWindow.windowRoot.addEventListener(
200       "MozAfterPaint",
201       this._pendingReadabilityCheck
202     );
203   }
205   onPaintWhenWaitedFor(forceNonArticle, event) {
206     // In non-e10s, we'll get called for paints other than ours, and so it's
207     // possible that this page hasn't been laid out yet, in which case we
208     // should wait until we get an event that does relate to our layout. We
209     // determine whether any of our this.contentWindow got painted by checking
210     // if there are any painted rects.
211     if (!event.clientRects.length) {
212       return;
213     }
215     this.performReadabilityCheckNow(forceNonArticle);
216   }
218   performReadabilityCheckNow(forceNonArticle) {
219     this.cancelPotentialPendingReadabilityCheck();
221     // Ignore errors from actors that have been unloaded before the
222     // paint event timer fires.
223     let document;
224     try {
225       document = this.document;
226     } catch (ex) {
227       return;
228     }
230     // Only send updates when there are articles; there's no point updating with
231     // |false| all the time.
232     if (
233       lazy.Readerable.shouldCheckUri(document.baseURIObject, true) &&
234       lazy.Readerable.isProbablyReaderable(document)
235     ) {
236       this.sendAsyncMessage("Reader:UpdateReaderButton", {
237         isArticle: true,
238       });
239     } else if (forceNonArticle) {
240       this.sendAsyncMessage("Reader:UpdateReaderButton", {
241         isArticle: false,
242       });
243     }
244   }
246   closeReaderMode() {
247     if (this.isAboutReader) {
248       this._isLeavingReaderableReaderMode = this.isReaderableAboutReader;
249       this.sendAsyncMessage("Reader:LeaveReaderMode", {});
250     }
251   }