Bug 1184265 - Make last sync date be relative. r?markh draft
authorEdouard Oger <eoger@fastmail.com>
Tue, 20 Mar 2018 17:08:16 -0400
changeset 770580 e56ea914b7a63861e48abcd135f8125f54829441
parent 770080 3d4f4a6bb6ba36a649f7f9b856b7b42c2127e133
push id103433
push userbmo:eoger@fastmail.com
push dateWed, 21 Mar 2018 14:27:33 +0000
reviewersmarkh
bugs1184265
milestone61.0a1
Bug 1184265 - Make last sync date be relative. r?markh MozReview-Commit-ID: 2hB9F7Elynh
browser/base/content/browser-sets.inc
browser/base/content/browser-sync.js
browser/base/content/test/sync/browser_fxa_badge.js
browser/base/content/test/sync/browser_sync.js
browser/base/content/test/urlbar/browser_page_action_menu.js
browser/components/customizableui/CustomizableWidgets.jsm
browser/components/customizableui/content/panelUI.inc.xul
browser/components/customizableui/test/browser_remote_tabs_button.js
browser/components/customizableui/test/browser_synced_tabs_menu.js
browser/components/syncedtabs/TabListView.js
browser/components/uitour/test/browser_fxa.js
services/sync/locales/en-US/sync.properties
--- 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…