Files
tubestation/browser/components/firefoxview/tabs-pickup.mjs

296 lines
8.4 KiB
JavaScript

/* 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/. */
/* eslint-env mozilla/frame-script */
const { switchToTabHavingURI } = window.docShell.chromeEventHandler.ownerGlobal;
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const SYNC_TABS_PREF = "services.sync.engine.tabs";
const RECENT_TABS_SYNC = "services.sync.lastTabFetch";
const tabsSetupFlowManager = new (class {
constructor() {
this.QueryInterface = ChromeUtils.generateQI(["nsIObserver"]);
this.setupState = new Map();
this._currentSetupStateName = "";
this.sync = {};
XPCOMUtils.defineLazyModuleGetters(this, {
Services: "resource://gre/modules/Services.jsm",
SyncedTabs: "resource://services-sync/SyncedTabs.jsm",
});
XPCOMUtils.defineLazyGetter(this, "fxAccounts", () => {
return ChromeUtils.import(
"resource://gre/modules/FxAccounts.jsm"
).getFxAccountsSingleton();
});
ChromeUtils.defineModuleGetter(
this.sync,
"UIState",
"resource://services-sync/UIState.jsm"
);
// this.syncTabsPrefEnabled will track the value of the tabs pref
XPCOMUtils.defineLazyPreferenceGetter(
this,
"syncTabsPrefEnabled",
SYNC_TABS_PREF,
false,
() => {
this.maybeUpdateUI();
}
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"lastTabFetch",
RECENT_TABS_SYNC,
false,
() => {
this.maybeUpdateUI();
}
);
this.registerSetupState({
uiStateIndex: 0,
name: "not-signed-in",
exitConditions: () => {
return this.fxaSignedIn;
},
});
// TODO: handle offline, sync service not ready or available
this.registerSetupState({
uiStateIndex: 1,
name: "connect-mobile-device",
exitConditions: () => {
return this.secondaryDeviceConnected;
},
});
this.registerSetupState({
uiStateIndex: 2,
name: "disabled-tab-sync",
exitConditions: () => {
return this.syncTabsPrefEnabled;
},
});
this.registerSetupState({
uiStateIndex: 3,
name: "synced-tabs-not-ready",
enter: () => {
if (!this.didRecentTabSync) {
this.SyncedTabs.syncTabs();
}
},
exitConditions: () => {
return this.didRecentTabSync;
},
});
this.registerSetupState({
uiStateIndex: 4,
name: "synced-tabs-loaded",
exitConditions: () => {
// This is the end state
return false;
},
});
}
async initialize(elem) {
this.elem = elem;
this.elem.addEventListener("click", this);
this.Services.obs.addObserver(this, this.sync.UIState.ON_UPDATE);
this.Services.obs.addObserver(this, "fxaccounts:device_connected");
this.Services.obs.addObserver(this, "fxaccounts:device_disconnected");
await this.fxAccounts.getSignedInUser();
this.maybeUpdateUI();
}
uninit() {
this.Services.obs.removeObserver(this, this.sync.UIState.ON_UPDATE);
this.Services.obs.removeObserver(this, "fxaccounts:device_connected");
this.Services.obs.removeObserver(this, "fxaccounts:device_disconnected");
}
get fxaSignedIn() {
return (
this.sync.UIState.get().status === this.sync.UIState.STATUS_SIGNED_IN
);
}
get secondaryDeviceConnected() {
let recentDevices = this.fxAccounts.device?.recentDeviceList?.length;
return recentDevices > 1;
}
get didRecentTabSync() {
const nowSeconds = Math.floor(Date.now() / 1000);
return (
nowSeconds - this.lastTabFetch <
this.SyncedTabs.TABS_FRESH_ENOUGH_INTERVAL_SECONDS
);
}
registerSetupState(state) {
this.setupState.set(state.name, state);
}
async observe(subject, topic, data) {
switch (topic) {
case this.sync.UIState.ON_UPDATE:
this.maybeUpdateUI();
break;
case "fxaccounts:device_connected":
case "fxaccounts:device_disconnected":
await this.fxAccounts.device.refreshDeviceList();
this.maybeUpdateUI();
break;
}
}
handleEvent(event) {
if (event.type == "click" && event.target.dataset.action) {
switch (event.target.dataset.action) {
case "view0-primary-action": {
this.openFxASignup(event.target);
break;
}
case "view1-primary-action": {
this.openSyncPreferences(event.target);
break;
}
case "view2-primary-action": {
this.syncOpenTabs(event.target);
break;
}
}
}
}
maybeUpdateUI() {
let nextSetupStateName = this._currentSetupStateName;
// state transition conditions
for (let state of this.setupState.values()) {
nextSetupStateName = state.name;
if (!state.exitConditions()) {
break;
}
}
if (nextSetupStateName !== this._currentSetupStateName) {
const state = this.setupState.get(nextSetupStateName);
this.elem.updateSetupState(state.uiStateIndex);
this._currentSetupStateName = nextSetupStateName;
if ("function" == typeof state.enter) {
state.enter();
}
}
}
async openFxASignup() {
const url = await this.fxAccounts.constructor.config.promiseConnectAccountURI(
"firefoxview"
);
switchToTabHavingURI(url, true);
}
openSyncPreferences(containerElem) {
const url = "about:preferences?action=pair#sync";
switchToTabHavingURI(url, true);
}
syncOpenTabs(containerElem) {
// Flip the pref on.
// The observer should trigger re-evaluating state and advance to next step
this.Services.prefs.setBoolPref(SYNC_TABS_PREF, true);
}
})();
class TabsPickupContainer extends HTMLElement {
constructor() {
super();
this.manager = null;
this._currentSetupStateIndex = -1;
XPCOMUtils.defineLazyModuleGetters(this, {
Services: "resource://gre/modules/Services.jsm",
});
}
get setupContainerElem() {
return this.querySelector(".sync-setup-container");
}
get tabsContainerElem() {
return this.querySelector(".synced-tabs-container");
}
appendTemplatedElement(templateId, elementId) {
const template = document.getElementById(templateId);
const templateContent = template.content;
const cloned = templateContent.cloneNode(true);
if (elementId) {
// populate id-prefixed attributes on elements that need them
for (let elem of cloned.querySelectorAll("[data-prefix]")) {
let [name, value] = elem.dataset.prefix
.split(":")
.map(str => str.trim());
elem.setAttribute(name, elementId + value);
delete elem.dataset.prefix;
}
for (let elem of cloned.querySelectorAll("a[data-support-url]")) {
elem.href =
this.Services.urlFormatter.formatURLPref("app.support.baseURL") +
elem.dataset.supportUrl;
}
}
this.appendChild(cloned);
}
updateSetupState(stateIndex) {
const currStateIndex = this._currentSetupStateIndex;
if (stateIndex === undefined) {
stateIndex = currStateIndex;
}
if (stateIndex === this._currentSetupStateIndex) {
return;
}
this._currentSetupStateIndex = stateIndex;
this.render();
}
render() {
if (!this.isConnected) {
return;
}
let setupElem = this.setupContainerElem;
let tabsElem = this.tabsContainerElem;
const stateIndex = this._currentSetupStateIndex;
// show/hide either the setup or tab list containers, creating each as necessary
if (stateIndex < 3) {
if (!setupElem) {
this.appendTemplatedElement("sync-setup-template", "tabpickup-steps");
setupElem = this.setupContainerElem;
}
if (tabsElem) {
tabsElem.hidden = true;
}
setupElem.hidden = false;
setupElem.selectedViewName = `sync-setup-view${stateIndex}`;
return;
}
if (!tabsElem) {
this.appendTemplatedElement(
"synced-tabs-template",
"tabpickup-tabs-container"
);
tabsElem = this.tabsContainerElem;
tabsElem.classList.toggle("loading", stateIndex == 3);
}
if (setupElem) {
setupElem.hidden = true;
}
tabsElem.hidden = false;
if (stateIndex == 4) {
tabsElem.classList.toggle("loading", false);
}
}
}
customElements.define("tabs-pickup-container", TabsPickupContainer);
export { tabsSetupFlowManager };