Bug 1470555 - Implement ability to send a selection of tabs. r=flod,jaws
authorAbdoulaye O. Ly <ablayelyfondou@gmail.com>
Tue, 04 Sep 2018 14:36:05 +0000
changeset 434588 bc1e9abe3747c5c93ba1d8615785d71da22e6571
parent 434587 c6efa2d9a32006f3d676dca81048ac5e30a4a4a4
child 434589 873f58fff17d0551455eeb36614b91d197c47dbc
push id68702
push userjwein@mozilla.com
push dateTue, 04 Sep 2018 14:36:36 +0000
treeherderautoland@bc1e9abe3747 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersflod, jaws
bugs1470555
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1470555 - Implement ability to send a selection of tabs. r=flod,jaws Differential Revision: https://phabricator.services.mozilla.com/D3126
browser/base/content/browser-context.inc
browser/base/content/browser-pageActions.js
browser/base/content/browser-sync.js
browser/base/content/browser.xul
browser/locales/en-US/chrome/browser/browser.dtd
browser/locales/en-US/chrome/browser/browser.properties
--- a/browser/base/content/browser-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -262,17 +262,17 @@
       <menuseparator id="context-sep-sendpagetodevice" class="sync-ui-item"
                      hidden="true"/>
       <menu id="context-sendpagetodevice"
             class="sync-ui-item"
             label="&sendPageToDevice.label;"
             accesskey="&sendPageToDevice.accesskey;"
             hidden="true">
         <menupopup id="context-sendpagetodevice-popup"
-                   onpopupshowing="(() => { gSync.populateSendTabToDevicesMenu(event.target, gBrowser.currentURI.spec, gBrowser.contentTitle); })()"/>
+                   onpopupshowing="(() => { gSync.populateSendTabToDevicesMenu(event.target, gBrowser.selectedTab); })()"/>
       </menu>
       <menuseparator id="context-sep-viewbgimage"/>
       <menuitem id="context-viewbgimage"
                 label="&viewBGImageCmd.label;"
                 accesskey="&viewBGImageCmd.accesskey;"
                 oncommand="gContextMenu.viewBGImage(event);"
                 onclick="checkForMiddleClick(this, event);"/>
       <menuitem id="context-undo"
--- a/browser/base/content/browser-pageActions.js
+++ b/browser/base/content/browser-pageActions.js
@@ -53,21 +53,32 @@ var BrowserPageActions = {
     this.panelNode.addEventListener("popuphiding", () => {
       this.mainButtonNode.removeAttribute("open");
     });
   },
 
   _onPanelShowing() {
     this.placeLazyActionsInPanel();
     for (let action of PageActions.actionsInPanel(window)) {
+      if (action.id == "sendToDevice") {
+        this.panelNode.removeAttribute(action.getTitle());
+        action.setTitle(this.getSendToDeviceString(), window);
+      }
       let buttonNode = this.panelButtonNodeForActionID(action.id);
       action.onShowingInPanel(buttonNode);
     }
   },
 
+  getSendToDeviceString() {
+    let tabCount = gBrowser.multiSelectedTabsCount || 1;
+    return PluralForm.get(tabCount,
+                          gNavigatorBundle.getString("pageAction.sendTabsToDevice.label"))
+                     .replace("#1", tabCount.toLocaleString());
+  },
+
   placeLazyActionsInPanel() {
     let actions = this._actionsToLazilyPlaceInPanel;
     this._actionsToLazilyPlaceInPanel = [];
     for (let action of actions) {
       this._placeActionInPanelNow(action);
     }
   },
 
@@ -1018,26 +1029,22 @@ BrowserPageActions.sendToDevice = {
   onLocationChange() {
     let action = PageActions.actionForID("sendToDevice");
     let browser = gBrowser.selectedBrowser;
     let url = browser.currentURI.spec;
     action.setDisabled(!gSync.isSendableURI(url), window);
   },
 
   onShowingSubview(panelViewNode) {
-    let browser = gBrowser.selectedBrowser;
-    let url = browser.currentURI.spec;
-    let title = browser.contentTitle;
-
     let bodyNode = panelViewNode.querySelector(".panel-subview-body");
     let panelNode = panelViewNode.closest("panel");
 
     // This is on top because it also clears the device list between state
     // changes.
-    gSync.populateSendTabToDevicesMenu(bodyNode, url, title, (clientId, name, clientType, lastModified) => {
+    gSync.populateSendTabToDevicesMenu(bodyNode, gBrowser.selectedTab, (clientId, name, clientType, lastModified) => {
       if (!name) {
         return document.createXULElement("toolbarseparator");
       }
       let item = document.createXULElement("toolbarbutton");
       item.classList.add("pageAction-sendToDevice-device", "subviewbutton");
       if (clientId) {
         item.classList.add("subviewbutton-iconic");
         item.setAttribute("tooltiptext", gSync.formatLastSyncDate(lastModified));
--- a/browser/base/content/browser-sync.js
+++ b/browser/base/content/browser-sync.js
@@ -356,17 +356,17 @@ var gSync = {
         console.log(`Sending a tab to ${client.name} using Sync.`);
         await Weave.Service.clientsEngine.sendURIToClientForDisplay(url, client.id, title);
       } catch (e) {
         console.error("Could not send tab to device.", e);
       }
     }
   },
 
-  populateSendTabToDevicesMenu(devicesPopup, url, title, createDeviceNodeFn) {
+  populateSendTabToDevicesMenu(devicesPopup, aTab, createDeviceNodeFn) {
     if (!createDeviceNodeFn) {
       createDeviceNodeFn = (clientId, name, clientType, lastModified) => {
         let eltName = name ? "menuitem" : "menuseparator";
         return document.createXULElement(eltName);
       };
     }
 
     // remove existing menu items
@@ -381,40 +381,53 @@ var gSync = {
       // We can only be in this case in the page action menu.
       return;
     }
 
     const fragment = document.createDocumentFragment();
 
     const state = UIState.get();
     if (state.status == UIState.STATUS_SIGNED_IN && this.remoteClients.length > 0) {
-      this._appendSendTabDeviceList(fragment, createDeviceNodeFn, url, title);
+      this._appendSendTabDeviceList(fragment, createDeviceNodeFn, aTab);
     } else if (state.status == UIState.STATUS_SIGNED_IN) {
       this._appendSendTabSingleDevice(fragment, createDeviceNodeFn);
     } else if (state.status == UIState.STATUS_NOT_VERIFIED ||
                state.status == UIState.STATUS_LOGIN_FAILED) {
       this._appendSendTabVerify(fragment, createDeviceNodeFn);
     } else /* status is STATUS_NOT_CONFIGURED */ {
       this._appendSendTabUnconfigured(fragment, createDeviceNodeFn);
     }
 
     devicesPopup.appendChild(fragment);
   },
 
   // TODO: once our transition from the old-send tab world is complete,
   // this list should be built using the FxA device list instead of the client
   // collection.
-  _appendSendTabDeviceList(fragment, createDeviceNodeFn, url, title) {
+  _appendSendTabDeviceList(fragment, createDeviceNodeFn, tab) {
+    let tabsToSend = tab.multiselected ? gBrowser.selectedTabs : [tab];
+
+    function getTabUrl(t) {
+      return t.linkedBrowser.currentURI.spec;
+    }
+    function getTabTitle(t) {
+      return t.linkedBrowser.contentTitle;
+    }
+
     const onSendAllCommand = (event) => {
-      this.sendTabToDevice(url, this.remoteClients, title);
+      for (let t of tabsToSend) {
+        this.sendTabToDevice(getTabUrl(t), this.remoteClients, getTabTitle(t));
+      }
     };
     const onTargetDeviceCommand = (event) => {
       const clientId = event.target.getAttribute("clientId");
       const client = this.remoteClients.find(c => c.id == clientId);
-      this.sendTabToDevice(url, [client], title);
+      for (let t of tabsToSend) {
+        this.sendTabToDevice(getTabUrl(t), [client], getTabTitle(t));
+      }
     };
 
     function addTargetDevice(clientId, name, clientType, lastModified) {
       const targetDevice = createDeviceNodeFn(clientId, name, clientType, lastModified);
       targetDevice.addEventListener("command", clientId ? onTargetDeviceCommand :
                                                           onSendAllCommand, true);
       targetDevice.classList.add("sync-menuitem", "sendtab-target");
       targetDevice.setAttribute("clientId", clientId);
@@ -511,24 +524,41 @@ var gSync = {
     // as a valid URI. We've already logged an error when trying to construct
     // the regexp, and the more problematic case is the length, which we've
     // already addressed.
     return true;
   },
 
   // "Send Tab to Device" menu item
   updateTabContextMenu(aPopupMenu, aTargetTab) {
+    // We may get here before initialisation. This situation
+    // can lead to a empty label for 'Send To Device' Menu.
+    this.init();
+
     if (!this.SYNC_ENABLED) {
       // These items are hidden in onSyncDisabled(). No need to do anything.
       return;
     }
-    const enabled = !this.syncConfiguredAndLoading &&
-                    this.isSendableURI(aTargetTab.linkedBrowser.currentURI.spec);
+    let hasASendableURI = false;
+    for (let tab of aTargetTab.multiselected ? gBrowser.selectedTabs : [aTargetTab]) {
+      if (this.isSendableURI(tab.linkedBrowser.currentURI.spec)) {
+        hasASendableURI = true;
+        break;
+      }
+    }
+    const enabled = !this.syncConfiguredAndLoading && hasASendableURI;
 
-    document.getElementById("context_sendTabToDevice").disabled = !enabled;
+    let sendTabsToDevice = document.getElementById("context_sendTabToDevice");
+    sendTabsToDevice.disabled = !enabled;
+
+    let tabCount = aTargetTab.multiselected ? gBrowser.multiSelectedTabsCount : 1;
+    sendTabsToDevice.label = PluralForm.get(tabCount,
+                                           gNavigatorBundle.getString("sendTabsToDevice.label"))
+                                      .replace("#1", tabCount.toLocaleString());
+    sendTabsToDevice.accessKey = gNavigatorBundle.getString("sendTabsToDevice.accesskey");
   },
 
   // "Send Page to Device" and "Send Link to Device" menu items
   updateContentContextMenu(contextMenu) {
     if (!this.SYNC_ENABLED) {
       // These items are hidden by default. No need to do anything.
       return;
     }
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -137,21 +137,20 @@ xmlns="http://www.w3.org/1999/xhtml"
         <menupopup oncommand="TabContextMenu.reopenInContainer(event);"
                    onpopupshowing="TabContextMenu.createReopenInContainerMenu(event);"/>
       </menu>
       <menuitem id="context_openTabInWindow" label="&moveToNewWindow.label;"
                 accesskey="&moveToNewWindow.accesskey;"
                 tbattr="tabbrowser-multiple"
                 oncommand="gBrowser.replaceTabsWithWindow(TabContextMenu.contextTab);"/>
       <menuseparator id="context_sendTabToDevice_separator" class="sync-ui-item"/>
-      <menu id="context_sendTabToDevice" label="&sendTabToDevice.label;"
-            class="sync-ui-item"
-            accesskey="&sendTabToDevice.accesskey;">
+      <menu id="context_sendTabToDevice"
+            class="sync-ui-item">
         <menupopup id="context_sendTabToDevicePopupMenu"
-                   onpopupshowing="gSync.populateSendTabToDevicesMenu(event.target, TabContextMenu.contextTab.linkedBrowser.currentURI.spec, TabContextMenu.contextTab.linkedBrowser.contentTitle);"/>
+                   onpopupshowing="gSync.populateSendTabToDevicesMenu(event.target, TabContextMenu.contextTab);"/>
       </menu>
       <menuseparator/>
       <menuitem id="context_reloadAllTabs" label="&reloadAllTabs.label;" accesskey="&reloadAllTabs.accesskey;"
                 tbattr="tabbrowser-multiple-visible"
                 oncommand="gBrowser.reloadAllTabs();"/>
        <menuitem id="context_bookmarkSelectedTabs"
                 hidden="true"
                 label="&bookmarkSelectedTabs.label;"
@@ -483,17 +482,17 @@ xmlns="http://www.w3.org/1999/xhtml"
            hidden="true"
            flip="slide"
            photon="true"
            position="bottomcenter topright"
            tabspecific="true"
            noautofocus="true"
            copyURL-title="&pageAction.copyLink.label;"
            emailLink-title="&emailPageCmd.label;"
-           sendToDevice-title="&pageAction.sendTabToDevice.label;"
+           sendToDevice-title=""
            sendToDevice-notReadyTitle="&sendToDevice.syncNotReady.label;"
            shareURL-title="&pageAction.shareUrl.label;"
            shareMore-label="&pageAction.shareMore.label;">
       <panelmultiview id="pageActionPanelMultiView"
                       mainViewId="pageActionPanelMainView"
                       viewCacheId="appMenu-viewCache">
         <panelview id="pageActionPanelMainView"
                    context="pageActionContextMenu"
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -56,18 +56,16 @@ but will never be visible at the same ti
 used as a metaphor for expressing the fact that these tabs are "pinned" to the
 left edge of the tabstrip. Really we just want the string to express the idea
 that this is a lightweight and reversible action that keeps your tab where you
 can reach it easily. -->
 <!ENTITY  pinTab.label                       "Pin Tab">
 <!ENTITY  pinTab.accesskey                   "P">
 <!ENTITY  unpinTab.label                     "Unpin Tab">
 <!ENTITY  unpinTab.accesskey                 "b">
-<!ENTITY  sendTabToDevice.label              "Send Tab to Device">
-<!ENTITY  sendTabToDevice.accesskey          "n">
 <!ENTITY  sendPageToDevice.label             "Send Page to Device">
 <!ENTITY  sendPageToDevice.accesskey         "n">
 <!ENTITY  sendLinkToDevice.label             "Send Link to Device">
 <!ENTITY  sendLinkToDevice.accesskey         "n">
 <!ENTITY  moveToNewWindow.label              "Move to New Window">
 <!ENTITY  moveToNewWindow.accesskey          "W">
 <!ENTITY  reopenInContainer.label            "Reopen in Container">
 <!ENTITY  reopenInContainer.accesskey        "e">
@@ -1083,17 +1081,16 @@ you can use these alternative items. Oth
 
 <!ENTITY pageActionButton.tooltip "Page actions">
 <!ENTITY pageAction.addToUrlbar.label "Add to Address Bar">
 <!ENTITY pageAction.removeFromUrlbar.label "Remove from Address Bar">
 <!ENTITY pageAction.allowInUrlbar.label "Show in Address Bar">
 <!ENTITY pageAction.disallowInUrlbar.label "Don’t Show in Address Bar">
 <!ENTITY pageAction.manageExtension.label "Manage Extension…">
 
-<!ENTITY pageAction.sendTabToDevice.label "Send Tab to Device">
 <!ENTITY sendToDevice.syncNotReady.label "Syncing Devices…">
 
 <!ENTITY pageAction.shareUrl.label "Share">
 <!ENTITY pageAction.shareMore.label "More…">
 
 <!ENTITY libraryButton.tooltip "View history, saved bookmarks, and more">
 
 <!-- LOCALIZATION NOTE: (accessibilityIndicator.tooltip): This is used to
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -900,16 +900,29 @@ playTab.accesskey = l
 
 muteSelectedTabs.label = Mute Tabs
 muteSelectedTabs.accesskey = u
 unmuteSelectedTabs.label = Unmute Tabs
 unmuteSelectedTabs.accesskey = b
 playTabs.label = Play Tabs
 playTabs.accesskey = y
 
+# LOCALIZATION NOTE (sendTabsToDevice.label):
+# Semi-colon list of plural forms.
+# See: https://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 is the number of tabs sent to the device.
+sendTabsToDevice.label = Send Tab to Device;Send #1 Tabs to Device
+sendTabsToDevice.accesskey = n
+
+# LOCALIZATION NOTE (pageAction.sendTabsToDevice.label):
+# Semi-colon list of plural forms.
+# See: https://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 is the number of tabs sent to the device.
+pageAction.sendTabsToDevice.label = Send Tab to Device;Send #1 Tabs to Device
+
 # LOCALIZATION NOTE (certErrorDetails*.label): These are text strings that
 # appear in the about:certerror page, so that the user can copy and send them to
 # the server administrators for troubleshooting.
 certErrorDetailsHSTS.label = HTTP Strict Transport Security: %S
 certErrorDetailsKeyPinning.label = HTTP Public Key Pinning: %S
 certErrorDetailsCertChain.label = Certificate chain:
 
 # LOCALIZATION NOTE (pendingCrashReports2.label): Semi-colon list of plural forms