Bug 1768681 - Implement a loading state for tabs-pickup while we wait for remote tabs to sync. r=fluent-reviewers,desktop-theme-reviewers,dao,Gijs

* Add an animated svg loading spinner similar to pdf.js' loading.svg
* Add loading state strings and update string from previous steps
* Show a loading/syncing step when the last tab sync was greater than 30s ago
* Change the loading state to hide the setup steps and show the tabs list with a loading spinner
* Expose TABS_FRESH_ENOUGH_INTERVAL_SECONDS from SyncedTabs.jsm so we can define it in a single place

Differential Revision: https://phabricator.services.mozilla.com/D147565
This commit is contained in:
Sam Foster 2022-06-15 01:18:30 +00:00
parent 4b73cd1e20
commit b715ef7498
8 changed files with 169 additions and 48 deletions

View File

@ -14,8 +14,8 @@ firefoxview-page-title = { -firefoxview-brand-name }
firefoxview-just-now-timestamp = Just now
# This is a headline for an area in the product where users can resume and re-open tabs they have previously viewed on other devices.
firefoxview-tabpickup-header = Tab Pickup
firefoxview-tabpickup-description = Pick up where you left off on another device.
firefoxview-tabpickup-header = Tab pickup
firefoxview-tabpickup-description = Open pages from other devices.
firefoxview-tabpickup-recenttabs-description = Recent tabs list would go here
@ -23,23 +23,21 @@ firefoxview-tabpickup-recenttabs-description = Recent tabs list would go here
# $percentValue (Number): the percentage value for setup completion
firefoxview-tabpickup-progress-label = { $percentValue }% complete
firefoxview-tabpickup-step-signin-header = Step 1 of 3: Sign in to { -brand-product-name }
firefoxview-tabpickup-step-signin-description = To get your mobile tabs on this device, sign in to { -brand-product-name } and turn on sync.
firefoxview-tabpickup-step-signin-header = Switch seamlessly between devices
firefoxview-tabpickup-step-signin-description = To grab your phone tabs here, first sign in or create an account.
firefoxview-tabpickup-step-signin-primarybutton = Continue
# These are placeholders for now..
firefoxview-tabpickup-adddevice-header = Step 2 of 3: Sign in on a mobile device
firefoxview-tabpickup-adddevice-description = Step 2 description.
firefoxview-tabpickup-adddevice-header = Sync { -brand-product-name } on your phone or tablet
firefoxview-tabpickup-adddevice-description = Download { -brand-product-name } for mobile and sign in there.
firefoxview-tabpickup-adddevice-learn-how = Learn how
firefoxview-tabpickup-adddevice-primarybutton = Get { -brand-product-name } for mobile
firefoxview-tabpickup-synctabs-header = Step 3 of 3: Sync open tabs
firefoxview-tabpickup-synctabs-description = Step 3 description.
firefoxview-tabpickup-synctabs-header = Turn on tab syncing
firefoxview-tabpickup-synctabs-description = Allow { -brand-short-name } to share tabs between devices.
firefoxview-tabpickup-synctabs-learn-how = Learn how
firefoxview-tabpickup-synctabs-primarybutton = Sync open tabs
firefoxview-tabpickup-setupsuccess-header = Setup Complete!
firefoxview-tabpickup-setupsuccess-description = Step 4 description.
firefoxview-tabpickup-setupsuccess-primarybutton = Get my other tabs
firefoxview-tabpickup-syncing = Sit tight while your tabs sync. Itll be just a moment.
firefoxview-closed-tabs-title = Recently closed
firefoxview-closed-tabs-collapse-button =

View File

@ -272,6 +272,21 @@ body > main > aside {
margin-top: 0;
}
.synced-tabs-container.loading > .card,
.synced-tabs-container:not(.loading) > .loading-content {
display: none;
}
.synced-tabs-container > .loading-content {
text-align: center;
-moz-context-properties: fill;
fill: currentColor;
background: url(chrome://global/skin/icons/loading-dial.svg) no-repeat center top;
background-size: 32px;
margin-top: 32px;
padding: 48px 16px 16px;
}
.closed-tabs-list {
padding-inline-start: 0;
}

View File

@ -46,7 +46,12 @@
<div name="sync-setup-view1" data-prefix="id:-view1" class="card setup-step" data-prefix="aria-labelledby:-view1-header">
<h2 data-prefix="id:-view1-header" data-l10n-id="firefoxview-tabpickup-adddevice-header" class="card-header"></h2>
<section class="step-body">
<p class="step-content" data-l10n-id="firefoxview-tabpickup-adddevice-description"></p>
<p class="step-content">
<span data-l10n-id="firefoxview-tabpickup-adddevice-description"></span>
<br/>
<!-- TODO: Bug 1772278: Replace placeholder URL -->
<a href="https://support.mozilla.org/kb/firefox-accounts-managing-account-data" data-l10n-id="firefoxview-tabpickup-adddevice-learn-how"></a>
</p>
<button class="primary" data-action="view1-primary-action" data-l10n-id="firefoxview-tabpickup-adddevice-primarybutton"></button>
</section>
<footer>
@ -60,7 +65,10 @@
<div name="sync-setup-view2" data-prefix="id:-view2" class="card setup-step" data-prefix="aria-labelledby:-view2-header">
<h2 data-prefix="id:-view2-header" data-l10n-id="firefoxview-tabpickup-synctabs-header" class="card-header"></h2>
<section class="step-body">
<p class="step-content" data-l10n-id="firefoxview-tabpickup-synctabs-description"></p>
<p class="step-content"><span data-l10n-id="firefoxview-tabpickup-synctabs-description">
<br/>
<!-- TODO: Bug 1772278: Replace placeholder URL -->
<a href="https://support.mozilla.org/kb/how-do-i-set-sync-my-computer" data-l10n-id="firefoxview-tabpickup-synctabs-learn-how"></a></p>
<button class="primary" data-action="view2-primary-action" data-l10n-id="firefoxview-tabpickup-synctabs-primarybutton"></button>
</section>
<footer>
@ -71,13 +79,6 @@
data-l10n-args='{"percentValue":"66"}'></label>
</footer>
</div>
<div name="sync-setup-view3" data-prefix="id:-view3" class="card setup-step" data-prefix="aria-labelledby:-view3-header">
<h2 data-prefix="id:-view2-header" data-l10n-id="firefoxview-tabpickup-setupsuccess-header" class="card-header"></h2>
<section class="step-body">
<p class="step-content" data-l10n-id="firefoxview-tabpickup-setupsuccess-description"></p>
<button class="primary" data-action="view3-primary-action" data-l10n-id="firefoxview-tabpickup-setupsuccess-primarybutton"></button>
</section>
</div>
</named-deck>
</template>
<template id="synced-tabs-template">
@ -85,6 +86,7 @@
<div class="card">
<h2 data-l10n-id="firefoxview-tabpickup-recenttabs-description"></h2>
</div>
<p class="loading-content" data-l10n-id="firefoxview-tabpickup-syncing"></p>
</div>
</template>

View File

@ -10,6 +10,7 @@ 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() {
@ -21,6 +22,7 @@ const tabsSetupFlowManager = new (class {
XPCOMUtils.defineLazyModuleGetters(this, {
Services: "resource://gre/modules/Services.jsm",
SyncedTabs: "resource://services-sync/SyncedTabs.jsm",
});
XPCOMUtils.defineLazyGetter(this, "fxAccounts", () => {
return ChromeUtils.import(
@ -43,6 +45,15 @@ const tabsSetupFlowManager = new (class {
this.maybeUpdateUI();
}
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"lastTabFetch",
RECENT_TABS_SYNC,
false,
() => {
this.maybeUpdateUI();
}
);
this.registerSetupState({
uiStateIndex: 0,
@ -69,16 +80,20 @@ const tabsSetupFlowManager = new (class {
this.registerSetupState({
uiStateIndex: 3,
name: "synced-tabs-not-ready",
enter: () => {
if (!this.didRecentTabSync) {
this.SyncedTabs.syncTabs();
}
},
exitConditions: () => {
// Bug 1763139 - Implement the actual logic to advance to next step
return false;
return this.didRecentTabSync;
},
});
this.registerSetupState({
uiStateIndex: 4,
name: "show-synced-tabs-loading",
name: "synced-tabs-loaded",
exitConditions: () => {
// Bug 1763139 - Implement the actual logic to advance to next step
// This is the end state
return false;
},
});
@ -109,6 +124,13 @@ const tabsSetupFlowManager = new (class {
);
return !!mobileDevice;
}
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);
}
@ -141,10 +163,6 @@ const tabsSetupFlowManager = new (class {
this.syncOpenTabs(event.target);
break;
}
case "view3-primary-action": {
this.confirmSetupComplete(event.target);
break;
}
}
}
}
@ -161,10 +179,12 @@ const tabsSetupFlowManager = new (class {
}
if (nextSetupStateName !== this._currentSetupStateName) {
this.elem.updateSetupState(
this.setupState.get(nextSetupStateName).uiStateIndex
);
const state = this.setupState.get(nextSetupStateName);
this.elem.updateSetupState(state.uiStateIndex);
this._currentSetupStateName = nextSetupStateName;
if ("function" == typeof state.enter) {
state.enter();
}
}
}
@ -183,12 +203,6 @@ const tabsSetupFlowManager = new (class {
// The observer should trigger re-evaluating state and advance to next step
this.Services.prefs.setBoolPref(SYNC_TABS_PREF, true);
}
confirmSetupComplete(containerElem) {
// Bug 1763139 - Implement the actual logic to advance to next step
this.elem.updateSetupState(
this.setupState.get("show-synced-tabs-loading").uiStateIndex
);
}
})();
class TabsPickupContainer extends HTMLElement {
@ -239,7 +253,7 @@ class TabsPickupContainer extends HTMLElement {
const stateIndex = this._currentSetupStateIndex;
// show/hide either the setup or tab list containers, creating each as necessary
if (stateIndex < 4) {
if (stateIndex < 3) {
if (!setupElem) {
this.appendTemplatedElement("sync-setup-template", "tabpickup-steps");
setupElem = this.setupContainerElem;
@ -260,6 +274,7 @@ class TabsPickupContainer extends HTMLElement {
if (setupElem) {
setupElem.hidden = true;
}
tabsElem.classList.toggle("loading", stateIndex == 3);
tabsElem.hidden = false;
}
}

View File

@ -62,6 +62,24 @@ async function waitForVisibleStep(browser, expected) {
}
}
async function waitForElementVisible(browser, selector, isVisible = true) {
const { document } = browser.contentWindow;
const elem = document.querySelector(selector);
ok(elem, `Got element with selector: ${selector}`);
await BrowserTestUtils.waitForMutationCondition(
elem,
{
attributeFilter: ["hidden"],
},
() => {
return isVisible
? BrowserTestUtils.is_visible(elem)
: BrowserTestUtils.is_hidden(elem);
}
);
}
add_setup(async function() {
await promiseSyncReady();
// gSync.init() is called in a requestIdleCallback. Force its initialization.
@ -74,6 +92,7 @@ add_setup(async function() {
registerCleanupFunction(async function() {
BrowserTestUtils.removeTab(tab);
Services.prefs.clearUserPref("services.sync.engine.tabs");
Services.prefs.clearUserPref("services.sync.lastTabFetch");
});
// set tab sync false so we don't skip setup states
await SpecialPowers.pushPrefEnv({
@ -220,9 +239,7 @@ add_task(async function test_tab_sync_enabled() {
await SpecialPowers.pushPrefEnv({
set: [["services.sync.engine.tabs", true]],
});
await waitForVisibleStep(browser, {
expectedVisible: "#tabpickup-steps-view3",
});
await waitForElementVisible(browser, "#tabpickup-steps", false);
// reset and test clicking the action button
await SpecialPowers.popPrefEnv();
@ -234,9 +251,9 @@ add_task(async function test_tab_sync_enabled() {
"#tabpickup-steps-view2 button.primary"
);
actionButton.click();
await waitForVisibleStep(browser, {
expectedVisible: "#tabpickup-steps-view3",
});
await waitForElementVisible(browser, "#tabpickup-steps", false);
ok(
Services.prefs.getBoolPref("services.sync.engine.tabs", false),
"tab sync pref should be enabled after button click"
@ -245,3 +262,55 @@ add_task(async function test_tab_sync_enabled() {
sandbox.restore();
Services.prefs.clearUserPref("services.sync.engine.tabs");
});
add_task(async function test_tab_sync_loading() {
const browser = gBrowser.selectedBrowser;
const sandbox = setupMocks({
state: UIState.STATUS_SIGNED_IN,
fxaDevices: [
{
id: 1,
name: "This Device",
isCurrentDevice: true,
type: "desktop",
},
{
id: 2,
name: "Other Device",
type: "mobile",
},
],
});
await SpecialPowers.pushPrefEnv({
set: [["services.sync.engine.tabs", true]],
});
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
await waitForElementVisible(browser, "#tabpickup-steps", false);
await waitForElementVisible(browser, "#tabpickup-tabs-container", true);
const tabsContainer = browser.contentWindow.document.querySelector(
"#tabpickup-tabs-container"
);
ok(
tabsContainer.classList.contains("loading"),
"Tabs container has loading class"
);
const recentFetchTime = Math.floor(Date.now() / 1000);
info("updating lastFetch:" + recentFetchTime);
Services.prefs.setIntPref("services.sync.lastTabFetch", recentFetchTime);
await BrowserTestUtils.waitForMutationCondition(
tabsContainer,
{ attributeFilter: ["class"], attributes: true },
() => {
return !tabsContainer.classList.contains("loading");
}
);
await SpecialPowers.popPrefEnv();
sandbox.restore();
Services.prefs.clearUserPref("services.sync.engine.tabs");
Services.prefs.clearUserPref("services.sync.lastTabFetch");
});

View File

@ -38,7 +38,7 @@ const TOPIC_TABS_CHANGED = "services.sync.tabs.changed";
// The interval, in seconds, before which we consider the existing list
// of tabs "fresh enough" and don't force a new sync.
const TABS_FRESH_ENOUGH_INTERVAL = 30;
const TABS_FRESH_ENOUGH_INTERVAL_SECONDS = 30;
XPCOMUtils.defineLazyGetter(lazy, "log", function() {
let log = Log.repository.getLogger("Sync.RemoteTabs");
@ -143,7 +143,7 @@ let SyncedTabsInternal = {
// Don't bother refetching tabs if we already did so recently
let lastFetch = Preferences.get("services.sync.lastTabFetch", 0);
let now = Math.floor(Date.now() / 1000);
if (now - lastFetch < TABS_FRESH_ENOUGH_INTERVAL) {
if (now - lastFetch < TABS_FRESH_ENOUGH_INTERVAL_SECONDS) {
lazy.log.info("_refetchTabs was done recently, do not doing it again");
return false;
}
@ -229,6 +229,9 @@ var SyncedTabs = {
// We make the topic for the observer notification public.
TOPIC_TABS_CHANGED,
// Expose the interval used to determine if synced tabs data needs a new sync
TABS_FRESH_ENOUGH_INTERVAL_SECONDS,
// Returns true if Sync is configured to Sync tabs, false otherwise
get isConfiguredToSyncTabs() {
return this._internal.isConfiguredToSyncTabs;

View File

@ -77,6 +77,7 @@
skin/classic/global/icons/link.svg (../../shared/icons/link.svg)
skin/classic/global/icons/loading.png (../../shared/icons/loading.png)
skin/classic/global/icons/loading@2x.png (../../shared/icons/loading@2x.png)
skin/classic/global/icons/loading-dial.svg (../../shared/icons/loading-dial.svg)
skin/classic/global/icons/more.svg (../../shared/icons/more.svg)
skin/classic/global/icons/open-in-new.svg (../../shared/icons/open-in-new.svg)
skin/classic/global/icons/page-portrait.svg (../../shared/icons/page-portrait.svg)

View File

@ -0,0 +1,18 @@
<!-- 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/.-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity" style="animation:spinIcon 1.2s steps(12,end) infinite">
<style>@keyframes spinIcon{to{transform:rotate(360deg)}}</style>
<path d="m7 3 0-2s0-1 1-1 1 1 1 1l0 2s0 1-1 1-1-1-1-1z"/>
<path d="m4.634 4.17-1-1.732s-.5-.866.366-1.366 1.366.366 1.366.366l1 1.732s.5.866-.366 1.366-1.366-.366-1.366-.366z" opacity=".93"/>
<path d="m3.17 6.366-1.732-1S.572 4.866 1.072 4s1.366-.366 1.366-.366l1.732 1s.866.5.366 1.366-1.366.366-1.366.366z" opacity=".86"/>
<path d="M3 9 1 9S0 9 0 8s1-1 1-1l2 0s1 0 1 1-1 1-1 1z" opacity=".79"/>
<path d="m4.17 11.366-1.732 1s-.866.5-1.366-.366.366-1.366.366-1.366l1.732-1s.866-.5 1.366.366-.366 1.366-.366 1.366z" opacity=".72"/>
<path d="m6.366 12.83-1 1.732s-.5.866-1.366.366-.366-1.366-.366-1.366l1-1.732s.5-.866 1.366-.366.366 1.366.366 1.366z" opacity=".65"/>
<path d="m9 13 0 2s0 1-1 1-1-1-1-1l0-2s0-1 1-1 1 1 1 1z" opacity=".58"/>
<path d="m11.366 11.83 1 1.732s.5.866-.366 1.366-1.366-.366-1.366-.366l-1-1.732s-.5-.866.366-1.366 1.366.366 1.366.366z" opacity=".51"/>
<path d="m12.83 9.634 1.732 1s.866.5.366 1.366-1.366.366-1.366.366l-1.732-1s-.866-.5-.366-1.366 1.366-.366 1.366-.366z" opacity=".44"/>
<path d="m13 7 2 0s1 0 1 1-1 1-1 1l-2 0s-1 0-1-1 1-1 1-1z" opacity=".37"/>
<path d="m11.83 4.634 1.732-1s.866-.5 1.366.366-.366 1.366-.366 1.366l-1.732 1s-.866.5-1.366-.366.366-1.366.366-1.366z" opacity=".5"/>
<path d="m9.634 3.17 1-1.732s.5-.866 1.366-.366.366 1.366.366 1.366l-1 1.732s-.5.866-1.366.366-.366-1.366-.366-1.366z" opacity=".75"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB