--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -162,17 +162,17 @@
<broadcaster id="isFrameImage"/>
<broadcaster id="singleFeedMenuitemState" disabled="true"/>
<broadcaster id="multipleFeedsMenuState" hidden="true"/>
<!-- Sync broadcasters -->
<!-- A broadcaster of a number of attributes suitable for "sync now" UI -
A 'syncstatus' attribute is set while actively syncing, and the label
attribute which changes from "sync now" to "syncing" etc. -->
- <broadcaster id="sync-status"/>
+ <broadcaster id="sync-status" onmouseover="gSync.refreshSyncButtonsTooltip();"/>
<!-- broadcasters of the "hidden" attribute to reflect setup state for
menus -->
<broadcaster id="sync-setup-state" hidden="true"/>
<broadcaster id="sync-unverified-state" hidden="true"/>
<broadcaster id="sync-syncnow-state" hidden="true"/>
<broadcaster id="sync-reauth-state" hidden="true"/>
<broadcaster id="viewTabsSidebar" autoCheck="false" sidebartitle="&syncedTabs.sidebar.label;"
type="checkbox" group="sidebar"
--- a/browser/base/content/browser-sync.js
+++ b/browser/base/content/browser-sync.js
@@ -383,17 +383,17 @@ var gSync = {
targetDevice.setAttribute("label", name);
fragment.appendChild(targetDevice);
}
const clients = this.remoteClients;
for (let client of clients) {
const type = client.formfactor && client.formfactor.includes("tablet") ?
"tablet" : client.type;
- addTargetDevice(client.id, client.name, type, client.serverLastModified * 1000);
+ addTargetDevice(client.id, client.name, type, new Date(client.serverLastModified * 1000));
}
// "Send to All Devices" menu item
if (clients.length > 1) {
const separator = createDeviceNodeFn();
separator.classList.add("sync-menuitem");
fragment.appendChild(separator);
const allDevicesLabel = this.fxaStrings.GetStringFromName("sendToAllDevices.menuitem");
@@ -586,16 +586,21 @@ var gSync = {
PanelUI.showSubView("PanelUI-remotetabs", anchor);
}, Cu.reportError);
} else {
// It is placed somewhere else - just try and show it.
PanelUI.showSubView("PanelUI-remotetabs", anchor);
}
},
+ refreshSyncButtonsTooltip() {
+ const state = UIState.get();
+ this.updateSyncButtonsTooltip(state);
+ },
+
/* Update the tooltip for the sync-status broadcaster (which will update the
Sync Toolbar button and the Sync spinner in the FxA hamburger area.)
If Sync is configured, the tooltip is when the last sync occurred,
otherwise the tooltip reflects the fact that Sync needs to be
(re-)configured.
*/
updateSyncButtonsTooltip(state) {
const status = state.status;
@@ -623,42 +628,27 @@ var gSync = {
if (tooltiptext) {
broadcaster.setAttribute("tooltiptext", tooltiptext);
} else {
broadcaster.removeAttribute("tooltiptext");
}
}
},
- get withinLastWeekFormat() {
- delete this.withinLastWeekFormat;
- return this.withinLastWeekFormat = new Intl.DateTimeFormat(undefined,
- {weekday: "long", hour: "numeric", minute: "numeric"});
- },
-
- get oneWeekOrOlderFormat() {
- delete this.oneWeekOrOlderFormat;
- return this.oneWeekOrOlderFormat = new Intl.DateTimeFormat(undefined,
- {month: "long", day: "numeric"});
+ get relativeTimeFormat() {
+ delete this.relativeTimeFormat;
+ return this.relativeTimeFormat = new Services.intl.RelativeTimeFormat(undefined, {style: "short"});
},
formatLastSyncDate(date) {
- let sixDaysAgo = (() => {
- let tempDate = new Date();
- tempDate.setDate(tempDate.getDate() - 6);
- tempDate.setHours(0, 0, 0, 0);
- return tempDate;
- })();
-
- // It may be confusing for the user to see "Last Sync: Monday" when the last
- // sync was indeed a Monday, but 3 weeks ago.
- let dateFormat = date < sixDaysAgo ? this.oneWeekOrOlderFormat : this.withinLastWeekFormat;
-
- let lastSyncDateString = dateFormat.format(date);
- return this.syncStrings.formatStringFromName("lastSync2.label", [lastSyncDateString], 1);
+ if (!date) { // Date can be null before the first sync!
+ return null;
+ }
+ const relativeDateStr = this.relativeTimeFormat.formatBestUnit(date);
+ return this.syncStrings.formatStringFromName("lastSync2.label", [relativeDateStr], 1);
},
onClientsSynced() {
let broadcaster = document.getElementById("sync-syncnow-state");
if (broadcaster) {
if (Weave.Service.clientsEngine.stats.numClients > 1) {
broadcaster.setAttribute("devices-status", "multi");
} else {
--- a/browser/base/content/test/sync/browser_fxa_badge.js
+++ b/browser/base/content/test/sync/browser_fxa_badge.js
@@ -17,16 +17,17 @@ add_task(async function test_unconfigure
UIState.get = oldUIState;
});
add_task(async function test_signedin_no_badge() {
const oldUIState = UIState.get;
UIState.get = () => ({
status: UIState.STATUS_SIGNED_IN,
+ lastSync: new Date(),
email: "foo@bar.com"
});
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
checkFxABadge(false);
UIState.get = oldUIState;
});
--- a/browser/base/content/test/sync/browser_sync.js
+++ b/browser/base/content/test/sync/browser_sync.js
@@ -129,31 +129,16 @@ add_task(async function test_ui_state_lo
avatarURL: null,
syncing: false,
syncNowTooltip: tooltipText
});
checkRemoteTabsPanel("PanelUI-remotetabs-reauthsync", false);
checkMenuBarItem("sync-reauthitem");
});
-add_task(async function test_FormatLastSyncDateNow() {
- let now = new Date();
- let nowString = gSync.formatLastSyncDate(now);
- is(nowString, "Last sync: " + now.toLocaleDateString(undefined, {weekday: "long", hour: "numeric", minute: "numeric"}),
- "The date is correctly formatted");
-});
-
-add_task(async function test_FormatLastSyncDateMonthAgo() {
- let monthAgo = new Date();
- monthAgo.setMonth(monthAgo.getMonth() - 1);
- let monthAgoString = gSync.formatLastSyncDate(monthAgo);
- is(monthAgoString, "Last sync: " + monthAgo.toLocaleDateString(undefined, {month: "long", day: "numeric"}),
- "The date is correctly formatted");
-});
-
function checkPanelUIStatusBar({label, tooltip, fxastatus, avatarURL, syncing, syncNowTooltip}) {
let labelNode = document.getElementById("appMenu-fxa-label");
let tooltipNode = document.getElementById("appMenu-fxa-status");
let statusNode = document.getElementById("appMenu-fxa-container");
let avatar = document.getElementById("appMenu-fxa-avatar");
is(labelNode.getAttribute("label"), label, "fxa label has the right value");
is(tooltipNode.getAttribute("tooltiptext"), tooltip, "fxa tooltip has the right value");
--- a/browser/base/content/test/urlbar/browser_page_action_menu.js
+++ b/browser/base/content/test/urlbar/browser_page_action_menu.js
@@ -323,17 +323,17 @@ add_task(async function sendToDevice_syn
},
];
for (let client of mockRemoteClients) {
expectedItems.push({
attrs: {
clientId: client.id,
label: client.name,
clientType: client.type,
- tooltiptext: gSync.formatLastSyncDate(lastModifiedFixture * 1000)
+ tooltiptext: gSync.formatLastSyncDate(new Date(lastModifiedFixture * 1000))
},
});
}
expectedItems.push(
null,
{
attrs: {
label: "Send to All Devices"
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -560,36 +560,17 @@ if (Services.prefs.getBoolPref("identity
deckIndices: {
DECKINDEX_TABS: 0,
DECKINDEX_TABSDISABLED: 1,
DECKINDEX_FETCHING: 2,
DECKINDEX_NOCLIENTS: 3,
},
TABS_PER_PAGE: 25,
NEXT_PAGE_MIN_TABS: 5, // Minimum number of tabs displayed when we click "Show All"
- onCreated(aNode) {
- this._initialize(aNode);
- },
- _initialize(aNode) {
- if (this._initialized) {
- return;
- }
- // Add an observer to the button so we get the animation during sync.
- // (Note the observer sets many attributes, including label and
- // tooltiptext, but we only want the 'syncstatus' attribute for the
- // animation)
- let doc = aNode.ownerDocument;
- let obnode = doc.createElementNS(kNSXUL, "observes");
- obnode.setAttribute("element", "sync-status");
- obnode.setAttribute("attribute", "syncstatus");
- aNode.appendChild(obnode);
- this._initialized = true;
- },
onViewShowing(aEvent) {
- this._initialize(aEvent.target);
let doc = aEvent.target.ownerDocument;
this._tabsList = doc.getElementById("PanelUI-remotetabs-tabslist");
Services.obs.addObserver(this, SyncedTabs.TOPIC_TABS_CHANGED);
if (SyncedTabs.isConfiguredToSyncTabs) {
if (SyncedTabs.hasSyncedThisSession) {
this.setDeckIndex(this.deckIndices.DECKINDEX_TABS);
} else {
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -194,16 +194,17 @@
</hbox>
<toolbarseparator orient="vertical"/>
<toolbarbutton id="appMenu-fxa-icon"
class="subviewbutton subviewbutton-iconic"
oncommand="gSync.doSync();"
closemenu="none">
<observes element="sync-status" attribute="syncstatus"/>
<observes element="sync-status" attribute="tooltiptext"/>
+ <observes element="sync-status" attribute="onmouseover"/>
</toolbarbutton>
</toolbaritem>
<toolbarseparator class="sync-ui-item"/>
<toolbarbutton id="appMenu-new-window-button"
class="subviewbutton subviewbutton-iconic"
label="&newNavigatorCmd.label;"
key="key_newNavigator"
command="cmd_newNavigator"/>
--- a/browser/components/customizableui/test/browser_remote_tabs_button.js
+++ b/browser/components/customizableui/test/browser_remote_tabs_button.js
@@ -65,16 +65,17 @@ add_task(async function asyncCleanup() {
restoreValues();
});
function mockFunctions() {
// mock UIState.get()
UIState.get = () => ({
status: UIState.STATUS_SIGNED_IN,
+ lastSync: new Date(),
email: "user@mozilla.com"
});
service.sync = mocked_sync;
}
function mocked_sync() {
syncWasCalled = true;
--- a/browser/components/customizableui/test/browser_synced_tabs_menu.js
+++ b/browser/components/customizableui/test/browser_synced_tabs_menu.js
@@ -154,17 +154,18 @@ add_task(asyncCleanup);
// When Sync is configured in a "needs reauthentication" state.
add_task(async function() {
gSync.updateAllUI({ status: UIState.STATUS_LOGIN_FAILED, email: "foo@bar.com" });
await openPrefsFromMenuPanel("PanelUI-remotetabs-reauthsync", "synced-tabs");
});
// Test the Connect Another Device button
add_task(async function() {
- gSync.updateAllUI({ status: UIState.STATUS_SIGNED_IN, email: "foo@bar.com" });
+ gSync.updateAllUI({ status: UIState.STATUS_SIGNED_IN, email: "foo@bar.com",
+ lastSync: new Date() });
let button = document.getElementById("PanelUI-remotetabs-connect-device-button");
ok(button, "found the button");
await document.getElementById("nav-bar").overflowable.show();
let expectedUrl = "https://example.com/connect_another_device?service=sync&context=" +
"fx_desktop_v3&entrypoint=synced-tabs&uid=uid&email=foo%40bar.com";
let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, expectedUrl);
@@ -173,17 +174,18 @@ add_task(async function() {
ok(!isOverflowOpen(), "click closed the panel");
await promiseTabOpened;
gBrowser.removeTab(gBrowser.selectedTab);
});
// Test the "Sync Now" button
add_task(async function() {
- gSync.updateAllUI({ status: UIState.STATUS_SIGNED_IN, email: "foo@bar.com" });
+ gSync.updateAllUI({ status: UIState.STATUS_SIGNED_IN, email: "foo@bar.com",
+ lastSync: new Date() });
await document.getElementById("nav-bar").overflowable.show();
let tabsUpdatedPromise = promiseObserverNotified("synced-tabs-menu:test:tabs-updated");
let syncPanel = document.getElementById("PanelUI-remotetabs");
let viewShownPromise = BrowserTestUtils.waitForEvent(syncPanel, "ViewShown");
let syncButton = document.getElementById("sync-button");
syncButton.click();
await Promise.all([tabsUpdatedPromise, viewShownPromise]);
@@ -336,17 +338,18 @@ add_task(async function() {
allTabsDesktop.push({ title: "Tab #" + i });
}
return allTabsDesktop;
}(),
}
]);
};
- gSync.updateAllUI({ status: UIState.STATUS_SIGNED_IN, email: "foo@bar.com" });
+ gSync.updateAllUI({ status: UIState.STATUS_SIGNED_IN, lastSync: new Date(),
+ email: "foo@bar.com" });
await document.getElementById("nav-bar").overflowable.show();
let tabsUpdatedPromise = promiseObserverNotified("synced-tabs-menu:test:tabs-updated");
let syncPanel = document.getElementById("PanelUI-remotetabs");
let viewShownPromise = BrowserTestUtils.waitForEvent(syncPanel, "ViewShown");
let syncButton = document.getElementById("sync-button");
syncButton.click();
await Promise.all([tabsUpdatedPromise, viewShownPromise]);
--- a/browser/components/syncedtabs/TabListView.js
+++ b/browser/components/syncedtabs/TabListView.js
@@ -126,21 +126,30 @@ TabListView.prototype = {
// Client rows are hidden when the list is filtered
_renderFilteredClient(client, filter) {
client.tabs.forEach((tab, index) => {
let node = this._renderTab(client, tab, index);
this.list.appendChild(node);
});
},
+ _updateLastSyncTitle(lastModified, itemNode) {
+ let lastSync = new Date(lastModified);
+ let lastSyncTitle = getChromeWindow(this._window).gSync.formatLastSyncDate(lastSync);
+ itemNode.setAttribute("title", lastSyncTitle);
+ },
+
_renderClient(client) {
let itemNode = client.tabs.length ?
this._createClient(client) :
this._createEmptyClient(client);
+ itemNode.addEventListener("mouseover", () =>
+ this._updateLastSyncTitle(client.lastModified, itemNode));
+
this._updateClient(client, itemNode);
let tabsList = itemNode.querySelector(".item-tabs-list");
client.tabs.forEach((tab, index) => {
let node = this._renderTab(client, tab, index);
tabsList.appendChild(node);
});
@@ -149,25 +158,25 @@ TabListView.prototype = {
},
_renderTab(client, tab, index) {
let itemNode = this._createTab(tab);
this._updateTab(tab, itemNode, index);
return itemNode;
},
- _createClient(item) {
+ _createClient() {
return this._doc.importNode(this._clientTemplate.content, true).firstElementChild;
},
- _createEmptyClient(item) {
+ _createEmptyClient() {
return this._doc.importNode(this._emptyClientTemplate.content, true).firstElementChild;
},
- _createTab(item) {
+ _createTab() {
return this._doc.importNode(this._tabTemplate.content, true).firstElementChild;
},
_clearChilden(node) {
let parent = node || this.container;
while (parent.firstChild) {
parent.firstChild.remove();
}
@@ -206,19 +215,17 @@ TabListView.prototype = {
/**
* Update the element representing an item, ensuring it's in sync with the
* underlying data.
* @param {client} item - Item to use as a source.
* @param {Element} itemNode - Element to update.
*/
_updateClient(item, itemNode) {
itemNode.setAttribute("id", "item-" + item.id);
- let lastSync = new Date(item.lastModified);
- let lastSyncTitle = getChromeWindow(this._window).gSync.formatLastSyncDate(lastSync);
- itemNode.setAttribute("title", lastSyncTitle);
+ this._updateLastSyncTitle(item.lastModified, itemNode);
if (item.closed) {
itemNode.classList.add("closed");
} else {
itemNode.classList.remove("closed");
}
if (item.selected) {
itemNode.classList.add("selected");
} else {
--- a/browser/components/uitour/test/browser_fxa.js
+++ b/browser/components/uitour/test/browser_fxa.js
@@ -31,17 +31,17 @@ var tests = [
let highlight = document.getElementById("UITourHighlightContainer");
is(highlight.getAttribute("targetName"), "accountStatus", "Correct highlight target");
}),
taskify(async function test_highlight_accountStatus_loggedIn() {
await setSignedInUser();
let userData = await fxAccounts.getSignedInUser();
isnot(userData, null, "Logged in now");
- gSync.updateAllUI({ status: UIState.STATUS_SIGNED_IN, email: "foo@example.com" });
+ gSync.updateAllUI({ status: UIState.STATUS_SIGNED_IN, lastSync: new Date(), email: "foo@example.com" });
await showMenuPromise("appMenu");
await showHighlightPromise("accountStatus");
let highlight = document.getElementById("UITourHighlightContainer");
let expectedTarget = "appMenu-fxa-avatar";
is(highlight.popupBoxObject.anchorNode.id, expectedTarget, "Anchored on avatar");
is(highlight.getAttribute("targetName"), "accountStatus", "Correct highlight target");
}),
];
--- a/services/sync/locales/en-US/sync.properties
+++ b/services/sync/locales/en-US/sync.properties
@@ -1,16 +1,16 @@
# 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/.
# %1: the user name (Ed), %2: the app name (Firefox), %3: the operating system (Android)
client.name2 = %1$S’s %2$S on %3$S
-# %S is the date and time at which the last sync successfully completed
+# %S is the relative time at which the last sync successfully completed (e.g. 5 min. ago)
lastSync2.label = Last sync: %S
# signInToSync.description is the tooltip for the Sync buttons when Sync is
# not configured.
signInToSync.description = Sign In To Sync
syncnow.label = Sync Now
syncingtabs.label = Syncing Tabs…