mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-27 14:52:16 +00:00
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:
parent
4b73cd1e20
commit
b715ef7498
@ -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. It’ll be just a moment.
|
||||
|
||||
firefoxview-closed-tabs-title = Recently closed
|
||||
firefoxview-closed-tabs-collapse-button =
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
18
toolkit/themes/shared/icons/loading-dial.svg
Normal file
18
toolkit/themes/shared/icons/loading-dial.svg
Normal 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 |
Loading…
Reference in New Issue
Block a user