Bug 1944627 - update sidebar button checked state for non-revamped sidebar cases...
[gecko.git] / browser / components / firefoxview / syncedtabs.mjs
blob93af4188190de9104e0c072514c7d931be7876ed
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 const lazy = {};
6 ChromeUtils.defineESModuleGetters(lazy, {
7   SyncedTabsController: "resource:///modules/SyncedTabsController.sys.mjs",
8 });
10 const { TabsSetupFlowManager } = ChromeUtils.importESModule(
11   "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
14 import {
15   html,
16   ifDefined,
17   when,
18 } from "chrome://global/content/vendor/lit.all.mjs";
19 import { ViewPage } from "./viewpage.mjs";
20 import {
21   escapeHtmlEntities,
22   MAX_TABS_FOR_RECENT_BROWSING,
23   navigateToLink,
24 } from "./helpers.mjs";
25 // eslint-disable-next-line import/no-unassigned-import
26 import "chrome://browser/content/firefoxview/syncedtabs-tab-list.mjs";
28 const UI_OPEN_STATE = "browser.tabs.firefox-view.ui-state.tab-pickup.open";
30 class SyncedTabsInView extends ViewPage {
31   controller = new lazy.SyncedTabsController(this, {
32     contextMenu: true,
33     pairDeviceCallback: () =>
34       Glean.firefoxviewNext.fxaMobileSync.record({
35         has_devices: TabsSetupFlowManager.secondaryDeviceConnected,
36       }),
37     signupCallback: () => Glean.firefoxviewNext.fxaContinueSync.record(),
38   });
40   constructor() {
41     super();
42     this._started = false;
43     this._id = Math.floor(Math.random() * 10e6);
44     if (this.recentBrowsing) {
45       this.maxTabsLength = MAX_TABS_FOR_RECENT_BROWSING;
46     } else {
47       // Setting maxTabsLength to -1 for no max
48       this.maxTabsLength = -1;
49     }
50     this.fullyUpdated = false;
51     this.showAll = false;
52     this.cumulativeSearches = 0;
53     this.onSearchQuery = this.onSearchQuery.bind(this);
54   }
56   static properties = {
57     ...ViewPage.properties,
58     showAll: { type: Boolean },
59     cumulativeSearches: { type: Number },
60   };
62   static queries = {
63     cardEls: { all: "card-container" },
64     emptyState: "fxview-empty-state",
65     searchTextbox: "fxview-search-textbox",
66     tabLists: { all: "syncedtabs-tab-list" },
67   };
69   start() {
70     if (this._started) {
71       return;
72     }
73     this._started = true;
74     this.controller.addSyncObservers();
75     this.controller.updateStates();
76     this.onVisibilityChange();
78     if (this.recentBrowsing) {
79       this.recentBrowsingElement.addEventListener(
80         "fxview-search-textbox-query",
81         this.onSearchQuery
82       );
83     }
84   }
86   stop() {
87     if (!this._started) {
88       return;
89     }
90     this._started = false;
91     TabsSetupFlowManager.updateViewVisibility(this._id, "unloaded");
92     this.onVisibilityChange();
93     this.controller.removeSyncObservers();
95     if (this.recentBrowsing) {
96       this.recentBrowsingElement.removeEventListener(
97         "fxview-search-textbox-query",
98         this.onSearchQuery
99       );
100     }
101   }
103   disconnectedCallback() {
104     super.disconnectedCallback();
105     this.stop();
106   }
108   viewVisibleCallback() {
109     this.start();
110   }
112   viewHiddenCallback() {
113     this.stop();
114   }
116   onVisibilityChange() {
117     const isOpen = this.open;
118     const isVisible = this.isVisible;
119     if (isVisible && isOpen) {
120       this.update();
121       TabsSetupFlowManager.updateViewVisibility(this._id, "visible");
122     } else {
123       TabsSetupFlowManager.updateViewVisibility(
124         this._id,
125         isVisible ? "closed" : "hidden"
126       );
127     }
129     this.toggleVisibilityInCardContainer();
130   }
132   generateMessageCard({
133     action,
134     buttonLabel,
135     descriptionArray,
136     descriptionLink,
137     header,
138     mainImageUrl,
139   }) {
140     return html`
141       <fxview-empty-state
142         headerLabel=${header}
143         .descriptionLabels=${descriptionArray}
144         .descriptionLink=${ifDefined(descriptionLink)}
145         class="empty-state synced-tabs error"
146         ?isSelectedTab=${this.selectedTab}
147         ?isInnerCard=${this.recentBrowsing}
148         mainImageUrl="${ifDefined(mainImageUrl)}"
149         id="empty-container"
150       >
151         <button
152           class="primary"
153           slot="primary-action"
154           ?hidden=${!buttonLabel}
155           data-l10n-id="${ifDefined(buttonLabel)}"
156           data-action="${action}"
157           @click=${e => this.controller.handleEvent(e)}
158         ></button>
159       </fxview-empty-state>
160     `;
161   }
163   onOpenLink(event) {
164     navigateToLink(event);
166     Glean.firefoxviewNext.syncedTabsTabs.record({
167       page: this.recentBrowsing ? "recentbrowsing" : "syncedtabs",
168     });
170     if (this.controller.searchQuery) {
171       Services.telemetry
172         .getKeyedHistogramById("FIREFOX_VIEW_CUMULATIVE_SEARCHES")
173         .add(
174           this.recentBrowsing ? "recentbrowsing" : "syncedtabs",
175           this.cumulativeSearches
176         );
177       this.cumulativeSearches = 0;
178     }
179   }
181   onContextMenu(e) {
182     this.triggerNode = e.originalTarget;
183     e.target.querySelector("panel-list").toggle(e.detail.originalEvent);
184   }
186   onCloseTab(e) {
187     const { url, fxaDeviceId, tertiaryActionClass } = e.originalTarget;
188     if (tertiaryActionClass === "dismiss-button") {
189       // Set new pending close tab
190       this.controller.requestCloseRemoteTab(fxaDeviceId, url);
191     } else if (tertiaryActionClass === "undo-button") {
192       // User wants to undo
193       this.controller.removePendingTabToClose(fxaDeviceId, url);
194     }
195     this.requestUpdate();
196   }
198   panelListTemplate() {
199     return html`
200       <panel-list slot="menu" data-tab-type="syncedtabs">
201         <panel-item
202           @click=${this.openInNewWindow}
203           data-l10n-id="fxviewtabrow-open-in-window"
204           data-l10n-attrs="accesskey"
205         ></panel-item>
206         <panel-item
207           @click=${this.openInNewPrivateWindow}
208           data-l10n-id="fxviewtabrow-open-in-private-window"
209           data-l10n-attrs="accesskey"
210         ></panel-item>
211         <hr />
212         <panel-item
213           @click=${this.copyLink}
214           data-l10n-id="fxviewtabrow-copy-link"
215           data-l10n-attrs="accesskey"
216         ></panel-item>
217       </panel-list>
218     `;
219   }
221   noDeviceTabsTemplate(deviceName, deviceType, isSearchResultsEmpty = false) {
222     const template = html`<h3
223         slot=${ifDefined(this.recentBrowsing ? null : "header")}
224         class="device-header"
225       >
226         <span class="icon ${deviceType}" role="presentation"></span>
227         ${deviceName}
228       </h3>
229       ${when(
230         isSearchResultsEmpty,
231         () => html`
232           <div
233             slot=${ifDefined(this.recentBrowsing ? null : "main")}
234             class="blackbox notabs search-results-empty"
235             data-l10n-id="firefoxview-search-results-empty"
236             data-l10n-args=${JSON.stringify({
237               query: escapeHtmlEntities(this.controller.searchQuery),
238             })}
239           ></div>
240         `,
241         () => html`
242           <div
243             slot=${ifDefined(this.recentBrowsing ? null : "main")}
244             class="blackbox notabs"
245             data-l10n-id="firefoxview-syncedtabs-device-notabs"
246           ></div>
247         `
248       )}`;
249     return this.recentBrowsing
250       ? template
251       : html`<card-container
252           shortPageName=${this.recentBrowsing ? "syncedtabs" : null}
253           >${template}</card-container
254         >`;
255   }
257   onSearchQuery(e) {
258     this.controller.searchQuery = e.detail.query;
259     this.cumulativeSearches = e.detail.query ? this.cumulativeSearches + 1 : 0;
260     this.showAll = false;
261   }
263   deviceTemplate(deviceName, deviceType, tabItems) {
264     return html`<h3
265         slot=${!this.recentBrowsing ? "header" : null}
266         class="device-header"
267       >
268         <span class="icon ${deviceType}" role="presentation"></span>
269         ${deviceName}
270       </h3>
271       <syncedtabs-tab-list
272         slot="main"
273         .hasPopup=${"menu"}
274         .tabItems=${ifDefined(tabItems)}
275         .searchQuery=${this.controller.searchQuery}
276         .maxTabsLength=${this.showAll ? -1 : this.maxTabsLength}
277         @fxview-tab-list-primary-action=${this.onOpenLink}
278         @fxview-tab-list-secondary-action=${this.onContextMenu}
279         @fxview-tab-list-tertiary-action=${this.onCloseTab}
280         secondaryActionClass="options-button"
281       >
282         ${this.panelListTemplate()}
283       </syncedtabs-tab-list>`;
284   }
286   generateTabList() {
287     let renderArray = [];
288     let renderInfo = this.controller.getRenderInfo();
289     for (let id in renderInfo) {
290       let tabItems = renderInfo[id].tabItems;
291       if (tabItems.length) {
292         const template = this.recentBrowsing
293           ? this.deviceTemplate(
294               renderInfo[id].name,
295               renderInfo[id].deviceType,
296               tabItems
297             )
298           : html`<card-container
299               shortPageName=${this.recentBrowsing ? "syncedtabs" : null}
300               >${this.deviceTemplate(
301                 renderInfo[id].name,
302                 renderInfo[id].deviceType,
303                 tabItems
304               )}
305             </card-container>`;
306         renderArray.push(template);
307         if (this.isShowAllLinkVisible(tabItems)) {
308           renderArray.push(
309             html` <div class="show-all-link-container">
310               <div
311                 class="show-all-link"
312                 @click=${this.enableShowAll}
313                 @keydown=${this.enableShowAll}
314                 data-l10n-id="firefoxview-show-all"
315                 tabindex="0"
316                 role="link"
317               ></div>
318             </div>`
319           );
320         }
321       } else {
322         // Check renderInfo[id].tabs.length to determine whether to display an
323         // empty tab list message or empty search results message.
324         // If there are no synced tabs, we always display the empty tab list
325         // message, even if there is an active search query.
326         renderArray.push(
327           this.noDeviceTabsTemplate(
328             renderInfo[id].name,
329             renderInfo[id].deviceType,
330             Boolean(renderInfo[id].tabs.length)
331           )
332         );
333       }
334     }
335     return renderArray;
336   }
338   isShowAllLinkVisible(tabItems) {
339     return (
340       this.recentBrowsing &&
341       this.controller.searchQuery &&
342       tabItems.length > this.maxTabsLength &&
343       !this.showAll
344     );
345   }
347   enableShowAll(event) {
348     if (
349       event.type == "click" ||
350       (event.type == "keydown" && event.code == "Enter") ||
351       (event.type == "keydown" && event.code == "Space")
352     ) {
353       event.preventDefault();
354       this.showAll = true;
355       Glean.firefoxviewNext.searchShowAllShowallbutton.record({
356         section: "syncedtabs",
357       });
358     }
359   }
361   generateCardContent() {
362     const cardProperties = this.controller.getMessageCard();
363     return cardProperties
364       ? this.generateMessageCard(cardProperties)
365       : this.generateTabList();
366   }
368   render() {
369     this.open =
370       !TabsSetupFlowManager.isTabSyncSetupComplete ||
371       Services.prefs.getBoolPref(UI_OPEN_STATE, true);
373     let renderArray = [];
374     renderArray.push(
375       html` <link
376         rel="stylesheet"
377         href="chrome://browser/content/firefoxview/view-syncedtabs.css"
378       />`
379     );
380     renderArray.push(
381       html` <link
382         rel="stylesheet"
383         href="chrome://browser/content/firefoxview/firefoxview.css"
384       />`
385     );
387     if (!this.recentBrowsing) {
388       renderArray.push(
389         html`<div class="sticky-container bottom-fade">
390           <h2
391             class="page-header"
392             data-l10n-id="firefoxview-synced-tabs-header"
393           ></h2>
394           <div class="syncedtabs-header">
395             <div>
396               <fxview-search-textbox
397                 data-l10n-id="firefoxview-search-text-box-tabs"
398                 data-l10n-attrs="placeholder"
399                 @fxview-search-textbox-query=${this.onSearchQuery}
400                 .size=${this.searchTextboxSize}
401                 pageName=${this.recentBrowsing
402                   ? "recentbrowsing"
403                   : "syncedtabs"}
404               ></fxview-search-textbox>
405             </div>
406             ${when(
407               this.controller.currentSetupStateIndex === 4,
408               () => html`
409                 <button
410                   class="small-button"
411                   data-action="add-device"
412                   @click=${e => this.controller.handleEvent(e)}
413                 >
414                   <img
415                     class="icon"
416                     role="presentation"
417                     src="chrome://global/skin/icons/plus.svg"
418                     alt="plus sign"
419                   /><span
420                     data-l10n-id="firefoxview-syncedtabs-connect-another-device"
421                     data-action="add-device"
422                   ></span>
423                 </button>
424               `
425             )}
426           </div>
427         </div>`
428       );
429     }
431     if (this.recentBrowsing) {
432       renderArray.push(
433         html`<card-container
434           preserveCollapseState
435           shortPageName="syncedtabs"
436           ?showViewAll=${this.controller.currentSetupStateIndex == 4 &&
437           this.controller.currentSyncedTabs.length}
438           ?isEmptyState=${!this.controller.currentSyncedTabs.length}
439         >
440           >
441           <h3
442             slot="header"
443             data-l10n-id="firefoxview-synced-tabs-header"
444             class="recentbrowsing-header"
445           ></h3>
446           <div slot="main">${this.generateCardContent()}</div>
447         </card-container>`
448       );
449     } else {
450       renderArray.push(
451         html`<div class="cards-container">${this.generateCardContent()}</div>`
452       );
453     }
454     return renderArray;
455   }
457   updated() {
458     this.fullyUpdated = true;
459     this.toggleVisibilityInCardContainer();
460   }
462 customElements.define("view-syncedtabs", SyncedTabsInView);