Bug 1419148 - Export Screenshots 25.0.0 to Firefox (except translations); r=kmag,ianbicking draft
authorJared Hirsch <ohai@6a68.net>
Mon, 20 Nov 2017 11:06:02 -0800
changeset 703945 02820ed87153f9d55f0b4501d85a8fa882846ec5
parent 701678 271297ce098224270e6b1ab829ff47d1e4f155bc
child 723954 2b4b3fa8f078adf205fdddc569a0efd0f4d957c6
push id91003
push userbmo:jhirsch@mozilla.com
push dateMon, 27 Nov 2017 23:21:34 +0000
reviewerskmag, ianbicking
bugs1419148
milestone59.0a1
Bug 1419148 - Export Screenshots 25.0.0 to Firefox (except translations); r=kmag,ianbicking MozReview-Commit-ID: 2InUwTe8oPa
browser/extensions/screenshots/bootstrap.js
browser/extensions/screenshots/install.rdf
browser/extensions/screenshots/moz.build
browser/extensions/screenshots/webextension/background/analytics.js
browser/extensions/screenshots/webextension/background/main.js
browser/extensions/screenshots/webextension/background/selectorLoader.js
browser/extensions/screenshots/webextension/background/senderror.js
browser/extensions/screenshots/webextension/background/startBackground.js
browser/extensions/screenshots/webextension/background/takeshot.js
browser/extensions/screenshots/webextension/build/buildSettings.js
browser/extensions/screenshots/webextension/build/inlineSelectionCss.js
browser/extensions/screenshots/webextension/build/shot.js
browser/extensions/screenshots/webextension/domainFromUrl.js
browser/extensions/screenshots/webextension/icons/copy.svg
browser/extensions/screenshots/webextension/icons/help-16.svg
browser/extensions/screenshots/webextension/icons/icon-starred-32-v2.svg
browser/extensions/screenshots/webextension/manifest.json
browser/extensions/screenshots/webextension/selector/shooter.js
browser/extensions/screenshots/webextension/selector/ui.js
browser/extensions/screenshots/webextension/selector/uicontrol.js
browser/extensions/screenshots/webextension/sitehelper.js
--- a/browser/extensions/screenshots/bootstrap.js
+++ b/browser/extensions/screenshots/bootstrap.js
@@ -1,15 +1,16 @@
 /* globals ADDON_DISABLE */
 const OLD_ADDON_PREF_NAME = "extensions.jid1-NeEaf3sAHdKHPA@jetpack.deviceIdInfo";
 const OLD_ADDON_ID = "jid1-NeEaf3sAHdKHPA@jetpack";
 const ADDON_ID = "screenshots@mozilla.org";
 const TELEMETRY_ENABLED_PREF = "datareporting.healthreport.uploadEnabled";
 const PREF_BRANCH = "extensions.screenshots.";
 const USER_DISABLE_PREF = "extensions.screenshots.disabled";
+const HISTORY_ENABLED_PREF = "places.history.enabled";
 
 const { interfaces: Ci, utils: Cu } = Components;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Console",
@@ -44,16 +45,17 @@ const prefObserver = {
     // aData is the name of the pref that's been changed (relative to aSubject)
     if (aData == USER_DISABLE_PREF) {
       // eslint-disable-next-line promise/catch-or-return
       appStartupPromise = appStartupPromise.then(handleStartup);
     }
   }
 };
 
+
 const appStartupObserver = {
   register() {
     Services.obs.addObserver(this, "sessionstore-windows-restored", false); // eslint-disable-line mozilla/no-useless-parameters
   },
 
   unregister() {
     Services.obs.removeObserver(this, "sessionstore-windows-restored", false); // eslint-disable-line mozilla/no-useless-parameters
   },
@@ -218,16 +220,28 @@ function handleMessage(msg, sender, send
     AddonManager.getAddonByID(OLD_ADDON_ID, (addon) => {
       prefs.clearUserPref(OLD_ADDON_PREF_NAME);
       if (addon) {
         addon.uninstall();
       }
       sendReply({type: "success", value: !!addon});
     });
     return true;
+  } else if (msg.funcName === "getHistoryPref") {
+    let historyEnabled = getBoolPref(HISTORY_ENABLED_PREF);
+    sendReply({type: "success", value: historyEnabled});
+  } else if (msg.funcName === "incrementDownloadCount") {
+    Services.telemetry.scalarAdd('screenshots.download', 1);
+    sendReply({type: "success", value: true});
+  } else if (msg.funcName === "incrementUploadCount") {
+    Services.telemetry.scalarAdd('screenshots.upload', 1);
+    sendReply({type: "success", value: true});
+  } else if (msg.funcName === "incrementCopyCount") {
+    Services.telemetry.scalarAdd('screenshots.copy', 1);
+    sendReply({type: "success", value: true});
   }
 }
 
 let photonPageAction;
 
 // If the current Firefox version supports Photon (57 and later), this sets up
 // a Photon page action and removes the UI for the WebExtension browser action.
 // Does nothing otherwise.  Ideally, in the future, WebExtension page actions
@@ -261,20 +275,20 @@ function initPhotonPageAction(api, webEx
     if (listenerPort.name != "photonPageActionPort") {
       return;
     }
     port = listenerPort;
     port.onMessage.addListener((message) => {
       switch (message.type) {
       case "setProperties":
         if (message.title) {
-          photonPageAction.title = message.title;
+          photonPageAction.setTitle(message.title);
         }
         if (message.iconPath) {
-          photonPageAction.iconURL = webExtension.extension.getURL(message.iconPath);
+          photonPageAction.setIconURL(webExtension.extension.getURL(message.iconPath));
         }
         break;
       default:
         console.error("Unrecognized message:", message);
         break;
       }
     });
   });
--- a/browser/extensions/screenshots/install.rdf
+++ b/browser/extensions/screenshots/install.rdf
@@ -7,14 +7,14 @@
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!--Firefox-->
         <em:minVersion>57.0a1</em:minVersion>
         <em:maxVersion>*</em:maxVersion>
       </Description>
     </em:targetApplication>
     <em:type>2</em:type>
-    <em:version>23.0.0</em:version>
+    <em:version>25.0.0</em:version>
     <em:bootstrap>true</em:bootstrap>
     <em:homepageURL>https://screenshots.firefox.com/</em:homepageURL>
     <em:multiprocessCompatible>true</em:multiprocessCompatible>
   </Description>
 </RDF>
--- a/browser/extensions/screenshots/moz.build
+++ b/browser/extensions/screenshots/moz.build
@@ -375,22 +375,23 @@ FINAL_TARGET_FILES.features['screenshots
 ]
 
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["icons"] += [
   'webextension/icons/back-highlight.svg',
   'webextension/icons/back.svg',
   'webextension/icons/cancel.svg',
   'webextension/icons/cloud.svg',
   'webextension/icons/copy.png',
+  'webextension/icons/copy.svg',
   'webextension/icons/done.svg',
   'webextension/icons/download.svg',
+  'webextension/icons/help-16.svg',
   'webextension/icons/icon-16-v2.svg',
   'webextension/icons/icon-32-v2.svg',
   'webextension/icons/icon-highlight-32-v2.svg',
-  'webextension/icons/icon-starred-32-v2.svg',
   'webextension/icons/icon-welcome-face-without-eyes.svg',
   'webextension/icons/menu-fullpage.svg',
   'webextension/icons/menu-myshot-white.svg',
   'webextension/icons/menu-myshot.svg',
   'webextension/icons/menu-visible.svg',
   'webextension/icons/onboarding-1.png',
   'webextension/icons/onboarding-2.png',
   'webextension/icons/onboarding-3.png',
--- a/browser/extensions/screenshots/webextension/background/analytics.js
+++ b/browser/extensions/screenshots/webextension/background/analytics.js
@@ -3,26 +3,62 @@
 "use strict";
 
 this.analytics = (function() {
   let exports = {};
 
   let telemetryPrefKnown = false;
   let telemetryPref;
 
+  function sendTiming(timingLabel, timingVar, timingValue) {
+    // sendTiming is only called in response to sendEvent, so no need to check
+    // the telemetry pref again here.
+    let timingCategory = "addon";
+    return new Promise((resolve, reject) => {
+      let url = main.getBackend() + "/timing";
+      let req = new XMLHttpRequest();
+      req.open("POST", url);
+      req.setRequestHeader("content-type", "application/json");
+      req.onload = catcher.watchFunction(() => {
+        if (req.status >= 300) {
+          let exc = new Error("Bad response from POST /timing");
+          exc.status = req.status;
+          exc.statusText = req.statusText;
+          reject(exc);
+        } else {
+          resolve();
+        }
+      });
+      log.info(`sendTiming ${timingCategory}/${timingLabel}/${timingVar}: ${timingValue}`);
+      req.send(JSON.stringify({
+        deviceId: auth.getDeviceId(),
+        timingCategory,
+        timingLabel,
+        timingVar,
+        timingValue
+      }));
+    });
+  }
+
   exports.sendEvent = function(action, label, options) {
     let eventCategory = "addon";
     if (!telemetryPrefKnown) {
       log.warn("sendEvent called before we were able to refresh");
       return Promise.resolve();
     }
     if (!telemetryPref) {
       log.info(`Cancelled sendEvent ${eventCategory}/${action}/${label || 'none'} ${JSON.stringify(options)}`);
       return Promise.resolve();
     }
+    measureTiming(action, label);
+    // Internal-only events are used for measuring time between events,
+    // but aren't submitted to GA.
+    if (action === 'internal') {
+      return Promise.resolve();
+    }
     if (typeof label == "object" && (!options)) {
       options = label;
       label = undefined;
     }
     options = options || {};
     let di = deviceInfo();
     return new Promise((resolve, reject) => {
       let url = main.getBackend() + "/event";
@@ -72,10 +108,112 @@ this.analytics = (function() {
     });
   };
 
   exports.getTelemetryPrefSync = function() {
     catcher.watchPromise(exports.refreshTelemetryPref());
     return !!telemetryPref;
   };
 
+  let timingData = {};
+
+  // Configuration for filtering the sendEvent stream on start/end events.
+  // When start or end events occur, the time is recorded.
+  // When end events occur, the elapsed time is calculated and submitted
+  // via `sendEvent`, where action = "perf-response-time", label = name of rule,
+  // and cd1 value is the elapsed time in milliseconds.
+  // If a cancel event happens between the start and end events, the start time
+  // is deleted.
+  let rules = [{
+    name: 'page-action',
+    start: { action: 'start-shot', label: 'toolbar-button' },
+    end: { action: 'internal', label: 'unhide-preselection-frame' },
+    cancel: [{ action: 'cancel-shot' }]
+  }, {
+    name: 'context-menu',
+    start: { action: 'start-shot', label: 'context-menu' },
+    end: { action: 'internal', label: 'unhide-preselection-frame' },
+    cancel: [{ action: 'cancel-shot' }]
+  }, {
+    name: 'capture-full-page',
+    start: { action: 'capture-full-page' },
+    end: { action: 'internal', label: 'unhide-preview-frame' },
+    cancel: [{ action: 'cancel-shot' }]
+  }, {
+    name: 'capture-visible',
+    start: { action: 'capture-visible' },
+    end: { action: 'internal', label: 'unhide-preview-frame' },
+    cancel: [{ action: 'cancel-shot' }]
+  }, {
+    name: 'make-selection',
+    start: { action: 'make-selection' },
+    end: { action: 'internal', label: 'unhide-selection-frame' },
+    cancel: [{ action: 'cancel-shot' }]
+  }, {
+    name: 'save-shot',
+    start: { action: 'save-shot' },
+    end: { action: 'internal', label: 'open-shot-tab' },
+    cancel: [{ action: 'cancel-shot' }, { action: 'upload-failed' }]
+  }, {
+    name: 'save-visible',
+    start: { action: 'save-visible' },
+    end: { action: 'internal', label: 'open-shot-tab' },
+    cancel: [{ action: 'cancel-shot' }, { action: 'upload-failed' }]
+  }, {
+    name: 'save-full-page',
+    start: { action: 'save-full-page' },
+    end: { action: 'internal', label: 'open-shot-tab' },
+    cancel: [{ action: 'cancel-shot' }, { action: 'upload-failed' }]
+  }, {
+    name: 'save-full-page-truncated',
+    start: { action: 'save-full-page-truncated' },
+    end: { action: 'internal', label: 'open-shot-tab' },
+    cancel: [{ action: 'cancel-shot' }, { action: 'upload-failed' }]
+  }, {
+    name: 'download-shot',
+    start: { action: 'download-shot' },
+    end: { action: 'internal', label: 'deactivate' },
+    cancel: [{ action: 'cancel-shot' }]
+  }, {
+    name: 'download-full-page',
+    start: { action: 'download-full-page' },
+    end: { action: 'internal', label: 'deactivate' },
+    cancel: [{ action: 'cancel-shot' }]
+  }, {
+    name: 'download-full-page-truncated',
+    start: { action: 'download-full-page-truncated' },
+    end: { action: 'internal', label: 'deactivate' },
+    cancel: [{ action: 'cancel-shot' }]
+  }, {
+    name: 'download-visible',
+    start: { action: 'download-visible' },
+    end: { action: 'internal', label: 'deactivate' },
+    cancel: [{ action: 'cancel-shot' }]
+  }];
+
+  // Match a filter (action and optional label) against an action and label.
+  function match(filter, action, label) {
+    return filter.label ?
+      filter.action === action && filter.label === label :
+      filter.action === action;
+  }
+
+  function anyMatches(filters, action, label) {
+    return !!filters.find(filter => match(filter, action, label));
+  }
+
+  function measureTiming(action, label) {
+    rules.forEach(r => {
+      if (anyMatches(r.cancel, action, label)) {
+        delete timingData[r.name];
+      } else if (match(r.start, action, label)) {
+        timingData[r.name] = Date.now();
+      } else if (timingData[r.name] && match(r.end, action, label)) {
+        let endTime = Date.now();
+        let elapsed = endTime - timingData[r.name];
+        catcher.watchPromise(sendTiming("perf-response-time", r.name, elapsed), true);
+        delete timingData[r.name];
+      }
+    });
+  }
+
   return exports;
 })();
--- a/browser/extensions/screenshots/webextension/background/main.js
+++ b/browser/extensions/screenshots/webextension/background/main.js
@@ -48,19 +48,16 @@ this.main = (function() {
     if (/^https?:\/\//.test(permission)) {
       exports.setBackend(permission);
       break;
     }
   }
 
   function setIconActive(active, tabId) {
     let path = active ? "icons/icon-highlight-32-v2.svg" : "icons/icon-32-v2.svg";
-    if ((!hasSeenOnboarding) && !active) {
-      path = "icons/icon-starred-32-v2.svg";
-    }
     startBackground.photonPageActionPort.postMessage({
       type: "setProperties",
       iconPath: path
     });
   }
 
   function toggleSelector(tab) {
     return analytics.refreshTelemetryPref()
@@ -91,22 +88,16 @@ this.main = (function() {
   }
 
   function shouldOpenMyShots(url) {
     return /^about:(?:newtab|blank|home)/i.test(url) || /^resource:\/\/activity-streams\//i.test(url);
   }
 
   // This is called by startBackground.js, directly in response to clicks on the Photon page action
   exports.onClicked = catcher.watchFunction((tab) => {
-    if (tab.incognito) {
-      senderror.showError({
-        popupMessage: "PRIVATE_WINDOW"
-      });
-      return;
-    }
     if (shouldOpenMyShots(tab.url)) {
       if (!hasSeenOnboarding) {
         catcher.watchPromise(analytics.refreshTelemetryPref().then(() => {
           sendEvent("goto-onboarding", "selection-button");
           return forceOnboarding();
         }));
         return;
       }
@@ -136,22 +127,16 @@ this.main = (function() {
     return browser.tabs.create({url: getOnboardingUrl()});
   }
 
   exports.onClickedContextMenu = catcher.watchFunction((info, tab) => {
     if (!tab) {
       // Not in a page/tab context, ignore
       return;
     }
-    if (tab.incognito) {
-      senderror.showError({
-        popupMessage: "PRIVATE_WINDOW"
-      });
-      return;
-    }
     if (!urlEnabled(tab.url)) {
       senderror.showError({
         popupMessage: "UNSHOOTABLE_PAGE"
       });
       return;
     }
     catcher.watchPromise(
       toggleSelector(tab)
@@ -215,37 +200,58 @@ this.main = (function() {
         type: "basic",
         iconUrl: "../icons/copy.png",
         title: browser.i18n.getMessage("notificationLinkCopiedTitle"),
         message: browser.i18n.getMessage("notificationLinkCopiedDetails", pasteSymbol)
       });
     }
   });
 
+  communication.register("copyShotToClipboard", (sender, blob) => {
+    return blobConverters.blobToArray(blob).then(buffer => {
+      return browser.clipboard.setImageData(
+        buffer, blob.type.split("/", 2)[1]).then(() => {
+          catcher.watchPromise(communication.sendToBootstrap('incrementCopyCount'));
+          return browser.notifications.create({
+            type: "basic",
+            iconUrl: "../icons/copy.png",
+            title: browser.i18n.getMessage("notificationImageCopiedTitle"),
+            message: browser.i18n.getMessage("notificationImageCopiedDetails", pasteSymbol)
+          });
+        });
+    })
+  });
+
   communication.register("downloadShot", (sender, info) => {
     // 'data:' urls don't work directly, let's use a Blob
     // see http://stackoverflow.com/questions/40269862/save-data-uri-as-file-using-downloads-download-api
     const blob = blobConverters.dataUrlToBlob(info.url);
     let url = URL.createObjectURL(blob);
     let downloadId;
     let onChangedCallback = catcher.watchFunction(function(change) {
       if (!downloadId || downloadId != change.id) {
         return;
       }
       if (change.state && change.state.current != "in_progress") {
         URL.revokeObjectURL(url);
         browser.downloads.onChanged.removeListener(onChangedCallback);
       }
     });
     browser.downloads.onChanged.addListener(onChangedCallback)
-    return browser.downloads.download({
-      url,
-      filename: info.filename
-    }).then((id) => {
-      downloadId = id;
+    catcher.watchPromise(communication.sendToBootstrap("incrementDownloadCount"));
+    return browser.windows.getLastFocused().then(windowInfo => {
+      return windowInfo.incognito;
+    }).then((incognito) => {
+      return browser.downloads.download({
+        url,
+        incognito,
+        filename: info.filename
+      }).then((id) => {
+        downloadId = id;
+      });
     });
   });
 
   communication.register("closeSelector", (sender) => {
     setIconActive(false, sender.tab.id);
   });
 
   catcher.watchPromise(communication.sendToBootstrap("getOldDeviceInfo").then((deviceInfo) => {
@@ -304,10 +310,22 @@ this.main = (function() {
     return catcher.watchPromise(browser.tabs.create({url: "https://www.mozilla.org/privacy/firefox-cloud/"}));
   });
 
   // A Screenshots page wants us to start/force onboarding
   communication.register("requestOnboarding", (sender) => {
     return startSelectionWithOnboarding(sender.tab);
   });
 
+  communication.register("isHistoryEnabled", () => {
+    return catcher.watchPromise(communication.sendToBootstrap("getHistoryPref").then(historyEnabled => {
+      return historyEnabled;
+    }));
+  });
+
+  communication.register("getPlatformOs", () => {
+    return catcher.watchPromise(browser.runtime.getPlatformInfo().then(platformInfo => {
+      return platformInfo.os;
+    }));
+  });
+
   return exports;
 })();
--- a/browser/extensions/screenshots/webextension/background/selectorLoader.js
+++ b/browser/extensions/screenshots/webextension/background/selectorLoader.js
@@ -1,9 +1,9 @@
-/* globals catcher, log */
+/* globals catcher, communication, log */
 
 "use strict";
 
 var global = this;
 
 this.selectorLoader = (function() {
   const exports = {};
 
@@ -60,32 +60,53 @@ this.selectorLoader = (function() {
     }).then(result => {
       return result && result[0];
     });
   };
 
   let loadingTabs = new Set();
 
   exports.loadModules = function(tabId, hasSeenOnboarding) {
-    let promise;
     loadingTabs.add(tabId);
+    let promise = downloadOnlyCheck(tabId);
     if (hasSeenOnboarding) {
-      promise = executeModules(tabId, standardScripts.concat(selectorScripts));
+      promise = promise.then(() => {
+        return executeModules(tabId, standardScripts.concat(selectorScripts));
+      });
     } else {
-      promise = executeModules(tabId, standardScripts.concat(onboardingScripts).concat(selectorScripts));
+      promise = promise.then(() => {
+        return executeModules(tabId, standardScripts.concat(onboardingScripts).concat(selectorScripts));
+      });
     }
     return promise.then((result) => {
       loadingTabs.delete(tabId);
       return result;
     }, (error) => {
       loadingTabs.delete(tabId);
       throw error;
     });
   };
 
+  // TODO: since bootstrap communication is now required, would this function
+  // make more sense inside background/main?
+  function downloadOnlyCheck(tabId) {
+    return communication.sendToBootstrap("getHistoryPref").then((historyEnabled) => {
+      return browser.tabs.get(tabId).then(tab => {
+        let downloadOnly = !historyEnabled || tab.incognito;
+        return browser.tabs.executeScript(tabId, {
+          // Note: `window` here refers to a global accessible to content
+          // scripts, but not the scripts in the underlying page. For more
+          // details, see https://mdn.io/WebExtensions/Content_scripts#Content_script_environment
+          code: `window.downloadOnly = ${downloadOnly}`,
+          runAt: "document_start"
+        });
+      });
+    });
+  }
+
   function executeModules(tabId, scripts) {
     let lastPromise = Promise.resolve(null);
     scripts.forEach((file) => {
       lastPromise = lastPromise.then(() => {
         return browser.tabs.executeScript(tabId, {
           file,
           runAt: "document_start"
         }).catch((error) => {
--- a/browser/extensions/screenshots/webextension/background/senderror.js
+++ b/browser/extensions/screenshots/webextension/background/senderror.js
@@ -1,14 +1,12 @@
-/* globals analytics, communication, makeUuid, Raven, catcher, auth, log */
+/* globals startBackground, analytics, communication, makeUuid, Raven, catcher, auth, log */
 
 "use strict";
 
-const startTime = Date.now();
-
 this.senderror = (function() {
   let exports = {};
 
   let manifest = browser.runtime.getManifest();
 
   // Do not show an error more than every ERROR_TIME_LIMIT milliseconds:
   const ERROR_TIME_LIMIT = 3000;
 
@@ -74,17 +72,17 @@ this.senderror = (function() {
     let showMessage = messages[popupMessage].showMessage;
     if (error.message && showMessage) {
       if (message) {
         message += "\n" + error.message;
       } else {
         message = error.message;
       }
     }
-    if (Date.now() - startTime > 5 * 1000) {
+    if (Date.now() - startBackground.startTime > 5 * 1000) {
       browser.notifications.create(id, {
         type: "basic",
         // FIXME: need iconUrl for an image, see #2239
         title,
         message
       });
     }
   };
--- a/browser/extensions/screenshots/webextension/background/startBackground.js
+++ b/browser/extensions/screenshots/webextension/background/startBackground.js
@@ -1,19 +1,20 @@
 /* globals browser, main, communication */
 /* This file handles:
      clicks on the Photon page action
      browser.contextMenus.onClicked
      browser.runtime.onMessage
    and loads the rest of the background page in response to those events, forwarding
    the events to main.onClicked, main.onClickedContextMenu, or communication.onMessage
 */
+const startTime = Date.now();
 
 this.startBackground = (function() {
-  let exports = {};
+  let exports = {startTime};
 
   const backgroundScripts = [
     "log.js",
     "makeUuid.js",
     "catcher.js",
     "blobConverters.js",
     "background/selectorLoader.js",
     "background/communication.js",
@@ -40,34 +41,16 @@ this.startBackground = (function() {
   browser.contextMenus.onClicked.addListener((info, tab) => {
     loadIfNecessary().then(() => {
       main.onClickedContextMenu(info, tab);
     }).catch((error) => {
       console.error("Error loading Screenshots:", error);
     });
   });
 
-  // Note this duplicates functionality in main.js, but we need to change
-  // the onboarding icon before main.js loads up
-  let iconPath = null;
-  browser.storage.local.get(["hasSeenOnboarding"]).then((result) => {
-    let hasSeenOnboarding = !!result.hasSeenOnboarding;
-    if (!hasSeenOnboarding) {
-      iconPath = "icons/icon-starred-32-v2.svg";
-      if (photonPageActionPort) {
-        photonPageActionPort.postMessage({
-          type: "setProperties",
-          iconPath
-        });
-      }
-    }
-  }).catch((error) => {
-    console.error("Error loading Screenshots onboarding flag:", error);
-  });
-
   browser.runtime.onMessage.addListener((req, sender, sendResponse) => {
     loadIfNecessary().then(() => {
       return communication.onMessage(req, sender, sendResponse);
     }).catch((error) => {
       console.error("Error loading Screenshots:", error);
     });
     return true;
   });
@@ -144,18 +127,17 @@ this.startBackground = (function() {
         break;
       default:
         console.error("Unrecognized message:", message);
         break;
       }
     });
     photonPageActionPort.postMessage({
       type: "setProperties",
-      title: browser.i18n.getMessage("contextMenuLabel"),
-      iconPath
+      title: browser.i18n.getMessage("contextMenuLabel")
     });
 
     // Export these so that main.js can use them.
     Object.defineProperties(exports, {
       "photonPageActionPort": {
         enumerable: true,
         get() {
           return photonPageActionPort;
--- a/browser/extensions/screenshots/webextension/background/takeshot.js
+++ b/browser/extensions/screenshots/webextension/background/takeshot.js
@@ -52,31 +52,33 @@ this.takeshot = (function() {
       shot.abTests = shotAbTests;
     }
     return catcher.watchPromise(capturePromise.then(() => {
       return convertBlobPromise;
     }).then(() => {
       return browser.tabs.create({url: shot.creatingUrl})
     }).then((tab) => {
       openedTab = tab;
+      sendEvent('internal', 'open-shot-tab');
       return uploadShot(shot, imageBlob);
     }).then(() => {
       return browser.tabs.update(openedTab.id, {url: shot.viewUrl}).then(
         null,
         (error) => {
           // FIXME: If https://bugzilla.mozilla.org/show_bug.cgi?id=1365718 is resolved,
           // use the errorCode added as an additional check:
           if ((/invalid tab id/i).test(error)) {
             // This happens if the tab was closed before the upload completed
             return browser.tabs.create({url: shot.viewUrl});
           }
           throw error;
         }
       );
     }).then(() => {
+      catcher.watchPromise(communication.sendToBootstrap('incrementUploadCount'));
       return shot.viewUrl;
     }).catch((error) => {
       browser.tabs.remove(openedTab.id);
       throw error;
     }));
   }));
 
   communication.register("screenshotPage", (sender, selectedPos, scroll) => {
--- a/browser/extensions/screenshots/webextension/build/buildSettings.js
+++ b/browser/extensions/screenshots/webextension/build/buildSettings.js
@@ -1,11 +1,11 @@
 window.buildSettings = {
   defaultSentryDsn: "https://904ccdd4866247c092ae8fc1a4764a63:940d44bdc71d4daea133c19080ccd38d@sentry.prod.mozaws.net/224",
   logLevel: "" || "warn",
   captureText: ("" === "true"),
   uploadBinary: ("" === "true"),
   pngToJpegCutoff: parseInt("" || 2500000, 10),
-  maxImageHeight: parseInt("" || 5000, 10),
-  maxImageWidth: parseInt("" || 5000, 10)
+  maxImageHeight: parseInt("" || 10000, 10),
+  maxImageWidth: parseInt("" || 10000, 10)
 };
 null;
 
--- a/browser/extensions/screenshots/webextension/build/inlineSelectionCss.js
+++ b/browser/extensions/screenshots/webextension/build/inlineSelectionCss.js
@@ -1,11 +1,11 @@
 /* Created from build/server/static/css/inline-selection.css */
 window.inlineSelectionCss = `
-.button, .highlight-button-cancel, .highlight-button-save, .highlight-button-download, .preview-button-save {
+.button, .highlight-button-cancel, .highlight-button-save, .highlight-button-download, .highlight-button-copy, .preview-button-save {
   display: flex;
   align-items: center;
   justify-content: center;
   border: 0;
   border-radius: 3px;
   cursor: pointer;
   font-size: 16px;
   font-weight: 400;
@@ -14,90 +14,90 @@ window.inlineSelectionCss = `
   outline: none;
   padding: 0 10px;
   position: relative;
   text-align: center;
   text-decoration: none;
   transition: background 150ms cubic-bezier(0.07, 0.95, 0, 1), border 150ms cubic-bezier(0.07, 0.95, 0, 1);
   user-select: none;
   white-space: nowrap; }
-  .button.small, .small.highlight-button-cancel, .small.highlight-button-save, .small.highlight-button-download, .small.preview-button-save {
+  .button.small, .small.highlight-button-cancel, .small.highlight-button-save, .small.highlight-button-download, .small.highlight-button-copy, .small.preview-button-save {
     height: 32px;
     line-height: 32px;
     padding: 0 8px; }
-  .button.tiny, .tiny.highlight-button-cancel, .tiny.highlight-button-save, .tiny.highlight-button-download, .tiny.preview-button-save {
+  .button.tiny, .tiny.highlight-button-cancel, .tiny.highlight-button-save, .tiny.highlight-button-download, .tiny.highlight-button-copy, .tiny.preview-button-save {
     font-size: 14px;
     height: 26px;
     border: 1px solid #c7c7c7; }
-    .button.tiny:hover, .tiny.highlight-button-cancel:hover, .tiny.highlight-button-save:hover, .tiny.highlight-button-download:hover, .tiny.preview-button-save:hover, .button.tiny:focus, .tiny.highlight-button-cancel:focus, .tiny.highlight-button-save:focus, .tiny.highlight-button-download:focus, .tiny.preview-button-save:focus {
+    .button.tiny:hover, .tiny.highlight-button-cancel:hover, .tiny.highlight-button-save:hover, .tiny.highlight-button-download:hover, .tiny.highlight-button-copy:hover, .tiny.preview-button-save:hover, .button.tiny:focus, .tiny.highlight-button-cancel:focus, .tiny.highlight-button-save:focus, .tiny.highlight-button-download:focus, .tiny.highlight-button-copy:focus, .tiny.preview-button-save:focus {
       background: #ededf0;
       border-color: #989898; }
-    .button.tiny:active, .tiny.highlight-button-cancel:active, .tiny.highlight-button-save:active, .tiny.highlight-button-download:active, .tiny.preview-button-save:active {
+    .button.tiny:active, .tiny.highlight-button-cancel:active, .tiny.highlight-button-save:active, .tiny.highlight-button-download:active, .tiny.highlight-button-copy:active, .tiny.preview-button-save:active {
       background: #dedede;
       border-color: #989898; }
-  .button.block-button, .block-button.highlight-button-cancel, .block-button.highlight-button-save, .block-button.highlight-button-download, .block-button.preview-button-save {
+  .button.block-button, .block-button.highlight-button-cancel, .block-button.highlight-button-save, .block-button.highlight-button-download, .block-button.highlight-button-copy, .block-button.preview-button-save {
     display: flex;
     align-items: center;
     justify-content: center;
     box-sizing: border-box;
     border: 0;
     border-right: 1px solid #c7c7c7;
     box-shadow: 0;
     border-radius: 0;
     flex-shrink: 0;
     font-size: 20px;
     height: 100px;
     line-height: 100%;
     overflow: hidden; }
     @media (max-width: 719px) {
-      .button.block-button, .block-button.highlight-button-cancel, .block-button.highlight-button-save, .block-button.highlight-button-download, .block-button.preview-button-save {
+      .button.block-button, .block-button.highlight-button-cancel, .block-button.highlight-button-save, .block-button.highlight-button-download, .block-button.highlight-button-copy, .block-button.preview-button-save {
         justify-content: flex-start;
         font-size: 16px;
         height: 72px;
         margin-right: 10px;
         padding: 0 5px; } }
-    .button.block-button:hover, .block-button.highlight-button-cancel:hover, .block-button.highlight-button-save:hover, .block-button.highlight-button-download:hover, .block-button.preview-button-save:hover {
+    .button.block-button:hover, .block-button.highlight-button-cancel:hover, .block-button.highlight-button-save:hover, .block-button.highlight-button-download:hover, .block-button.highlight-button-copy:hover, .block-button.preview-button-save:hover {
       background: #ededf0; }
-    .button.block-button:active, .block-button.highlight-button-cancel:active, .block-button.highlight-button-save:active, .block-button.highlight-button-download:active, .block-button.preview-button-save:active {
+    .button.block-button:active, .block-button.highlight-button-cancel:active, .block-button.highlight-button-save:active, .block-button.highlight-button-download:active, .block-button.highlight-button-copy:active, .block-button.preview-button-save:active {
       background: #dedede; }
-  .button.download, .download.highlight-button-cancel, .download.highlight-button-save, .download.highlight-button-download, .download.preview-button-save, .button.edit, .edit.highlight-button-cancel, .edit.highlight-button-save, .edit.highlight-button-download, .edit.preview-button-save, .button.trash, .trash.highlight-button-cancel, .trash.highlight-button-save, .trash.highlight-button-download, .trash.preview-button-save, .button.share, .share.highlight-button-cancel, .share.highlight-button-save, .share.highlight-button-download, .share.preview-button-save, .button.flag, .flag.highlight-button-cancel, .flag.highlight-button-save, .flag.highlight-button-download, .flag.preview-button-save {
+  .button.download, .download.highlight-button-cancel, .download.highlight-button-save, .download.highlight-button-download, .download.highlight-button-copy, .download.preview-button-save, .button.edit, .edit.highlight-button-cancel, .edit.highlight-button-save, .edit.highlight-button-download, .edit.highlight-button-copy, .edit.preview-button-save, .button.trash, .trash.highlight-button-cancel, .trash.highlight-button-save, .trash.highlight-button-download, .trash.highlight-button-copy, .trash.preview-button-save, .button.share, .share.highlight-button-cancel, .share.highlight-button-save, .share.highlight-button-download, .share.highlight-button-copy, .share.preview-button-save, .button.flag, .flag.highlight-button-cancel, .flag.highlight-button-save, .flag.highlight-button-download, .flag.highlight-button-copy, .flag.preview-button-save {
     background-repeat: no-repeat;
     background-size: 50%;
     background-position: center;
     margin-right: 10px;
     transition: background-color 150ms cubic-bezier(0.07, 0.95, 0, 1); }
-  .button.download, .download.highlight-button-cancel, .download.highlight-button-save, .download.highlight-button-download, .download.preview-button-save {
+  .button.download, .download.highlight-button-cancel, .download.highlight-button-save, .download.highlight-button-download, .download.highlight-button-copy, .download.preview-button-save {
     background-image: url("../img/icon-download.svg"); }
-    .button.download:hover, .download.highlight-button-cancel:hover, .download.highlight-button-save:hover, .download.highlight-button-download:hover, .download.preview-button-save:hover {
+    .button.download:hover, .download.highlight-button-cancel:hover, .download.highlight-button-save:hover, .download.highlight-button-download:hover, .download.highlight-button-copy:hover, .download.preview-button-save:hover {
       background-color: #ededf0; }
-    .button.download:active, .download.highlight-button-cancel:active, .download.highlight-button-save:active, .download.highlight-button-download:active, .download.preview-button-save:active {
+    .button.download:active, .download.highlight-button-cancel:active, .download.highlight-button-save:active, .download.highlight-button-download:active, .download.highlight-button-copy:active, .download.preview-button-save:active {
       background-color: #dedede; }
-  .button.share, .share.highlight-button-cancel, .share.highlight-button-save, .share.highlight-button-download, .share.preview-button-save {
+  .button.share, .share.highlight-button-cancel, .share.highlight-button-save, .share.highlight-button-download, .share.highlight-button-copy, .share.preview-button-save {
     background-image: url("../img/icon-share.svg"); }
-    .button.share:hover, .share.highlight-button-cancel:hover, .share.highlight-button-save:hover, .share.highlight-button-download:hover, .share.preview-button-save:hover {
+    .button.share:hover, .share.highlight-button-cancel:hover, .share.highlight-button-save:hover, .share.highlight-button-download:hover, .share.highlight-button-copy:hover, .share.preview-button-save:hover {
       background-color: #ededf0; }
-    .button.share.active, .share.active.highlight-button-cancel, .share.active.highlight-button-save, .share.active.highlight-button-download, .share.active.preview-button-save, .button.share:active, .share.highlight-button-cancel:active, .share.highlight-button-save:active, .share.highlight-button-download:active, .share.preview-button-save:active {
+    .button.share.active, .share.active.highlight-button-cancel, .share.active.highlight-button-save, .share.active.highlight-button-download, .share.active.highlight-button-copy, .share.active.preview-button-save, .button.share:active, .share.highlight-button-cancel:active, .share.highlight-button-save:active, .share.highlight-button-download:active, .share.highlight-button-copy:active, .share.preview-button-save:active {
       background-color: #dedede; }
-  .button.trash, .trash.highlight-button-cancel, .trash.highlight-button-save, .trash.highlight-button-download, .trash.preview-button-save {
+  .button.trash, .trash.highlight-button-cancel, .trash.highlight-button-save, .trash.highlight-button-download, .trash.highlight-button-copy, .trash.preview-button-save {
     background-image: url("../img/icon-trash.svg"); }
-    .button.trash:hover, .trash.highlight-button-cancel:hover, .trash.highlight-button-save:hover, .trash.highlight-button-download:hover, .trash.preview-button-save:hover {
+    .button.trash:hover, .trash.highlight-button-cancel:hover, .trash.highlight-button-save:hover, .trash.highlight-button-download:hover, .trash.highlight-button-copy:hover, .trash.preview-button-save:hover {
       background-color: #ededf0; }
-    .button.trash:active, .trash.highlight-button-cancel:active, .trash.highlight-button-save:active, .trash.highlight-button-download:active, .trash.preview-button-save:active {
+    .button.trash:active, .trash.highlight-button-cancel:active, .trash.highlight-button-save:active, .trash.highlight-button-download:active, .trash.highlight-button-copy:active, .trash.preview-button-save:active {
       background-color: #dedede; }
-  .button.edit, .edit.highlight-button-cancel, .edit.highlight-button-save, .edit.highlight-button-download, .edit.preview-button-save {
+  .button.edit, .edit.highlight-button-cancel, .edit.highlight-button-save, .edit.highlight-button-download, .edit.highlight-button-copy, .edit.preview-button-save {
     background-image: url("../img/icon-edit.svg"); }
-    .button.edit:hover, .edit.highlight-button-cancel:hover, .edit.highlight-button-save:hover, .edit.highlight-button-download:hover, .edit.preview-button-save:hover {
+    .button.edit:hover, .edit.highlight-button-cancel:hover, .edit.highlight-button-save:hover, .edit.highlight-button-download:hover, .edit.highlight-button-copy:hover, .edit.preview-button-save:hover {
       background-color: #ededf0; }
-    .button.edit:active, .edit.highlight-button-cancel:active, .edit.highlight-button-save:active, .edit.highlight-button-download:active, .edit.preview-button-save:active {
+    .button.edit:active, .edit.highlight-button-cancel:active, .edit.highlight-button-save:active, .edit.highlight-button-download:active, .edit.highlight-button-copy:active, .edit.preview-button-save:active {
       background-color: #dedede; }
-  .button.flag, .flag.highlight-button-cancel, .flag.highlight-button-save, .flag.highlight-button-download, .flag.preview-button-save {
+  .button.flag, .flag.highlight-button-cancel, .flag.highlight-button-save, .flag.highlight-button-download, .flag.highlight-button-copy, .flag.preview-button-save {
     background-image: url("../img/icon-flag.svg"); }
-    .button.flag:hover, .flag.highlight-button-cancel:hover, .flag.highlight-button-save:hover, .flag.highlight-button-download:hover, .flag.preview-button-save:hover {
+    .button.flag:hover, .flag.highlight-button-cancel:hover, .flag.highlight-button-save:hover, .flag.highlight-button-download:hover, .flag.highlight-button-copy:hover, .flag.preview-button-save:hover {
       background-color: #ededf0; }
-    .button.flag:active, .flag.highlight-button-cancel:active, .flag.highlight-button-save:active, .flag.highlight-button-download:active, .flag.preview-button-save:active {
+    .button.flag:active, .flag.highlight-button-cancel:active, .flag.highlight-button-save:active, .flag.highlight-button-download:active, .flag.highlight-button-copy:active, .flag.preview-button-save:active {
       background-color: #dedede; }
 
 .inverse-color-scheme {
   background: #38383d;
   color: #f9f9fa; }
   .inverse-color-scheme a {
     color: #e1e1e6; }
 
@@ -118,46 +118,46 @@ window.inlineSelectionCss = `
   background: #38383d;
   color: #f9f9fa; }
   .alt-color-scheme h1 {
     color: #6f7fb6; }
   .alt-color-scheme a {
     color: #e1e1e6;
     text-decoration: underline; }
 
-.button.primary, .primary.highlight-button-cancel, .highlight-button-save, .primary.highlight-button-download, .preview-button-save {
+.button.primary, .primary.highlight-button-cancel, .highlight-button-save, .primary.highlight-button-download, .primary.highlight-button-copy, .preview-button-save {
   background-color: #009ec0;
   color: #fff; }
-  .button.primary:hover, .primary.highlight-button-cancel:hover, .highlight-button-save:hover, .primary.highlight-button-download:hover, .preview-button-save:hover, .button.primary:focus, .primary.highlight-button-cancel:focus, .highlight-button-save:focus, .primary.highlight-button-download:focus, .preview-button-save:focus {
+  .button.primary:hover, .primary.highlight-button-cancel:hover, .highlight-button-save:hover, .primary.highlight-button-download:hover, .primary.highlight-button-copy:hover, .preview-button-save:hover, .button.primary:focus, .primary.highlight-button-cancel:focus, .highlight-button-save:focus, .primary.highlight-button-download:focus, .primary.highlight-button-copy:focus, .preview-button-save:focus {
     background-color: #00819c; }
-  .button.primary:active, .primary.highlight-button-cancel:active, .highlight-button-save:active, .primary.highlight-button-download:active, .preview-button-save:active {
+  .button.primary:active, .primary.highlight-button-cancel:active, .highlight-button-save:active, .primary.highlight-button-download:active, .primary.highlight-button-copy:active, .preview-button-save:active {
     background-color: #006c83; }
 
-.button.secondary, .highlight-button-cancel, .secondary.highlight-button-save, .highlight-button-download, .secondary.preview-button-save {
+.button.secondary, .highlight-button-cancel, .secondary.highlight-button-save, .highlight-button-download, .highlight-button-copy, .secondary.preview-button-save {
   background-color: #f9f9fa;
   color: #38383d; }
-  .button.secondary:hover, .highlight-button-cancel:hover, .secondary.highlight-button-save:hover, .highlight-button-download:hover, .secondary.preview-button-save:hover {
+  .button.secondary:hover, .highlight-button-cancel:hover, .secondary.highlight-button-save:hover, .highlight-button-download:hover, .highlight-button-copy:hover, .secondary.preview-button-save:hover {
     background-color: #ededf0; }
-  .button.secondary:active, .highlight-button-cancel:active, .secondary.highlight-button-save:active, .highlight-button-download:active, .secondary.preview-button-save:active {
+  .button.secondary:active, .highlight-button-cancel:active, .secondary.highlight-button-save:active, .highlight-button-download:active, .highlight-button-copy:active, .secondary.preview-button-save:active {
     background-color: #dedede; }
 
-.button.transparent, .transparent.highlight-button-cancel, .transparent.highlight-button-save, .transparent.highlight-button-download, .transparent.preview-button-save {
+.button.transparent, .transparent.highlight-button-cancel, .transparent.highlight-button-save, .transparent.highlight-button-download, .transparent.highlight-button-copy, .transparent.preview-button-save {
   background-color: transparent;
   color: #38383d; }
-  .button.transparent:hover, .transparent.highlight-button-cancel:hover, .transparent.highlight-button-save:hover, .transparent.highlight-button-download:hover, .transparent.preview-button-save:hover {
+  .button.transparent:hover, .transparent.highlight-button-cancel:hover, .transparent.highlight-button-save:hover, .transparent.highlight-button-download:hover, .transparent.highlight-button-copy:hover, .transparent.preview-button-save:hover {
     background-color: #ededf0; }
-  .button.transparent:focus, .transparent.highlight-button-cancel:focus, .transparent.highlight-button-save:focus, .transparent.highlight-button-download:focus, .transparent.preview-button-save:focus, .button.transparent:active, .transparent.highlight-button-cancel:active, .transparent.highlight-button-save:active, .transparent.highlight-button-download:active, .transparent.preview-button-save:active {
+  .button.transparent:focus, .transparent.highlight-button-cancel:focus, .transparent.highlight-button-save:focus, .transparent.highlight-button-download:focus, .transparent.highlight-button-copy:focus, .transparent.preview-button-save:focus, .button.transparent:active, .transparent.highlight-button-cancel:active, .transparent.highlight-button-save:active, .transparent.highlight-button-download:active, .transparent.highlight-button-copy:active, .transparent.preview-button-save:active {
     background-color: #dedede; }
 
-.button.warning, .warning.highlight-button-cancel, .warning.highlight-button-save, .warning.highlight-button-download, .warning.preview-button-save {
+.button.warning, .warning.highlight-button-cancel, .warning.highlight-button-save, .warning.highlight-button-download, .warning.highlight-button-copy, .warning.preview-button-save {
   color: #fff;
   background: #d92215; }
-  .button.warning:hover, .warning.highlight-button-cancel:hover, .warning.highlight-button-save:hover, .warning.highlight-button-download:hover, .warning.preview-button-save:hover, .button.warning:focus, .warning.highlight-button-cancel:focus, .warning.highlight-button-save:focus, .warning.highlight-button-download:focus, .warning.preview-button-save:focus {
+  .button.warning:hover, .warning.highlight-button-cancel:hover, .warning.highlight-button-save:hover, .warning.highlight-button-download:hover, .warning.highlight-button-copy:hover, .warning.preview-button-save:hover, .button.warning:focus, .warning.highlight-button-cancel:focus, .warning.highlight-button-save:focus, .warning.highlight-button-download:focus, .warning.highlight-button-copy:focus, .warning.preview-button-save:focus {
     background: #b81d12; }
-  .button.warning:active, .warning.highlight-button-cancel:active, .warning.highlight-button-save:active, .warning.highlight-button-download:active, .warning.preview-button-save:active {
+  .button.warning:active, .warning.highlight-button-cancel:active, .warning.highlight-button-save:active, .warning.highlight-button-download:active, .warning.highlight-button-copy:active, .warning.preview-button-save:active {
     background: #a11910; }
 
 .subtitle-link {
   color: #009ec0; }
 
 .loader {
   background: #2e2d30;
   border-radius: 2px;
@@ -360,16 +360,85 @@ window.inlineSelectionCss = `
 .bghighlight {
   background-color: rgba(0, 0, 0, 0.7);
   position: absolute;
   z-index: 9999999999; }
   body.hcm .bghighlight {
     background-color: black;
     opacity: 0.7; }
 
+.notice {
+  display: flex;
+  height: 41px;
+  left: 50%;
+  position: fixed;
+  transform: translate(-50%, 0);
+  transition: top 125ms ease-out, translate 125ms ease-out;
+  user-select: none;
+  will-change: top, translate;
+  z-index: 10000000000; }
+  .notice .notice-wrapper {
+    align-items: center;
+    background: #737373;
+    border-radius: 100px;
+    display: flex;
+    flex-wrap: nowrap;
+    justify-content: space-between;
+    padding: 10px 15px; }
+  .notice .notice-content {
+    color: #fff;
+    flex: 1;
+    font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif;
+    font-size: 14px;
+    font-weight: bold;
+    white-space: nowrap; }
+  .notice .notice-help {
+    background-image: url("MOZ_EXTENSION/icons/help-16.svg");
+    background-position: center center;
+    background-repeat: no-repeat;
+    height: 16px;
+    width: 16px; }
+
+.notice-tooltip {
+  background: #fff;
+  border-radius: 3px;
+  border: 1px solid #9d9d9e;
+  bottom: 60px;
+  color: #000;
+  cursor: default;
+  display: none;
+  font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif;
+  font-size: 14px;
+  font-weight: normal;
+  line-height: 22px;
+  overflow-wrap: break-word;
+  padding: 15px;
+  position: absolute;
+  right: -14px;
+  white-space: normal;
+  width: 300px;
+  z-index: 10000000000;
+  /* down-arrow for the tooltip */ }
+  .notice:hover .notice-tooltip {
+    display: block; }
+  .notice-tooltip p {
+    margin: 0; }
+  .notice-tooltip ul {
+    margin-bottom: 0; }
+  .notice-tooltip::after {
+    border-left: 10px solid transparent;
+    border-right: 10px solid transparent;
+    border-top: 10px solid #f9f9fa;
+    content: "";
+    height: 0;
+    left: 86%;
+    position: absolute;
+    top: 100%;
+    width: 0; }
+
 .preview-overlay {
   align-items: center;
   background-color: rgba(0, 0, 0, 0.7);
   display: flex;
   height: 100%;
   justify-content: center;
   left: 0;
   margin: 0;
@@ -424,31 +493,45 @@ window.inlineSelectionCss = `
   background-repeat: no-repeat;
   background-size: 20px 18px;
   font-size: 18px;
   margin: 5px;
   min-width: 80px; }
   html[dir="ltr"] .highlight-button-save {
     background-position: 8px center; }
   html[dir="rtl"] .highlight-button-save {
-    background-position: 65px center; }
+    background-position: right 10px center; }
   html[dir="ltr"] .highlight-button-save {
     padding-left: 34px; }
   html[dir="rtl"] .highlight-button-save {
     padding-right: 40px; }
 
 .highlight-button-download {
   background-image: url("MOZ_EXTENSION/icons/download.svg");
   background-position: center center;
   background-repeat: no-repeat;
   background-size: 18px 18px;
   border: 1px solid #dedede;
   display: block;
   margin: 5px;
   width: 40px; }
+  .highlight-button-download.download-only-button {
+    width: auto;
+    background-position: 7px;
+    padding-left: 32px; }
+
+.highlight-button-copy {
+  background-image: url("MOZ_EXTENSION/icons/copy.svg");
+  background-position: center center;
+  background-repeat: no-repeat;
+  background-size: 18px 18px;
+  border: 1px solid #dedede;
+  display: block;
+  margin: 5px;
+  width: 40px; }
 
 .pixel-dimensions {
   position: absolute;
   pointer-events: none;
   font-weight: bold;
   font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif;
   font-size: 70%;
   color: #000;
@@ -547,29 +630,18 @@ window.inlineSelectionCss = `
   justify-content: center;
   animation: pulse 125mm cubic-bezier(0.07, 0.95, 0, 1);
   color: #fff;
   font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif;
   font-size: 24px;
   line-height: 32px;
   text-align: center;
   padding-top: 20px;
-  width: 400px; }
-
-#imageCroppedWarning {
-  position: absolute;
-  background: rgba(0, 0, 0, 0.8);
-  bottom: 0;
-  color: #fff;
-  font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif;
-  font-size: 12px;
-  padding: 10px;
-  text-align: center;
-  width: 100%;
-  z-index: 2; }
+  width: 400px;
+  user-select: none; }
 
 .myshots-all-buttons-container {
   display: flex;
   flex-direction: row-reverse;
   background: #f5f5f5;
   border-radius: 2px;
   box-sizing: border-box;
   height: 80px;
--- a/browser/extensions/screenshots/webextension/build/shot.js
+++ b/browser/extensions/screenshots/webextension/build/shot.js
@@ -24,17 +24,17 @@ function isUrl(url) {
     return true;
   }
   if ((/^chrome:.{0,8000}/i).test(url)) {
     return true;
   }
   if ((/^view-source:/i).test(url)) {
     return isUrl(url.substr("view-source:".length));
   }
-  return (/^https?:\/\/[a-z0-9.-_]{1,8000}[a-z0-9](:[0-9]{1,8000})?\/?/i).test(url);
+  return (/^https?:\/\/[a-z0-9._-]{1,8000}[a-z0-9](:[0-9]{1,8000})?\/?/i).test(url);
 }
 
 function isValidClipImageUrl(url) {
     return isUrl(url) && !(url.indexOf(')') > -1);
 }
 
 function assertUrl(url) {
   if (!url) {
@@ -43,17 +43,17 @@ function assertUrl(url) {
   if (!isUrl(url)) {
     let exc = new Error("Not a URL");
     exc.scheme = url.split(":")[0];
     throw exc;
   }
 }
 
 function isSecureWebUri(url) {
-  return (/^https?:\/\/[a-z0-9.-_]{1,8000}[a-z0-9](:[0-9]{1,8000})?\/?/i).test(url);
+  return (/^https?:\/\/[a-z0-9._-]{1,8000}[a-z0-9](:[0-9]{1,8000})?\/?/i).test(url);
 }
 
 function assertOrigin(url) {
   assertUrl(url);
   if (url.search(/^https?:/i) != -1) {
     let match = (/^https?:\/\/[^/:]{1,4000}\/?$/i).exec(url);
     if (!match) {
       throw new Error("Bad origin, might include path");
@@ -124,17 +124,17 @@ function resolveUrl(base, url) {
     return url;
   }
   if (url.indexOf("//") === 0) {
     // Protocol-relative URL
     return (/^https?:/i).exec(base)[0] + url;
   }
   if (url.indexOf("/") === 0) {
     // Domain-relative URL
-    return (/^https?:\/\/[a-z0-9.-_]{1,4000}/i).exec(base)[0] + url;
+    return (/^https?:\/\/[a-z0-9._-]{1,4000}/i).exec(base)[0] + url;
   }
   // Otherwise, a full relative URL
   while (url.indexOf("./") === 0) {
     url = url.substr(2);
   }
   if (!base) {
     // It's not an absolute URL, and we don't have a base URL, so we have
     // to throw away the URL
@@ -204,17 +204,17 @@ function makeRandomId() {
   }
   return id;
 }
 
 class AbstractShot {
 
   constructor(backend, id, attrs) {
     attrs = attrs || {};
-    assert((/^[a-zA-Z0-9]{1,4000}\/[a-z0-9.-_]{1,4000}$/).test(id), "Bad ID (should be alphanumeric):", JSON.stringify(id));
+    assert((/^[a-zA-Z0-9]{1,4000}\/[a-z0-9._-]{1,4000}$/).test(id), "Bad ID (should be alphanumeric):", JSON.stringify(id));
     this._backend = backend;
     this._id = id;
     this.origin = attrs.origin || null;
     this.fullUrl = attrs.fullUrl || null;
     if ((!attrs.fullUrl) && attrs.url) {
       console.warn("Received deprecated attribute .url");
       this.fullUrl = attrs.url;
     }
@@ -384,17 +384,17 @@ class AbstractShot {
     }
     return clipFilename + extension;
   }
 
   get urlDisplay() {
     if (!this.url) {
       return null;
     }
-    if (this.url.search(/^https?/i) != -1) {
+    if (/^https?:\/\//i.test(this.url)) {
       let txt = this.url;
       txt = txt.replace(/^[a-z]{1,4000}:\/\//i, "");
       txt = txt.replace(/\/.{0,4000}/, "");
       txt = txt.replace(/^www\./i, "");
       return txt;
     } else if (this.url.startsWith("data:")) {
       return "data:url";
     }
--- a/browser/extensions/screenshots/webextension/domainFromUrl.js
+++ b/browser/extensions/screenshots/webextension/domainFromUrl.js
@@ -9,20 +9,20 @@ this.domainFromUrl = (function() {
   return function urlDomainForId(location) { // eslint-disable-line no-unused-vars
     let domain = location.hostname;
     if (!domain) {
       domain = location.origin.split(":")[0];
       if (!domain) {
         domain = "unknown";
       }
     }
-    if (domain.search(/^[a-z0-9.-_]{1,1000}$/i) === -1) {
+    if (domain.search(/^[a-z0-9._-]{1,1000}$/i) === -1) {
       // Probably a unicode domain; we could use punycode but it wouldn't decode
       // well in the URL anyway.  Instead we'll punt.
-      domain = domain.replace(/[^a-z0-9.-_]/ig, "");
+      domain = domain.replace(/[^a-z0-9._-]/ig, "");
       if (!domain) {
         domain = "site";
       }
     }
     return domain;
   };
 
 })();
new file mode 100644
--- /dev/null
+++ b/browser/extensions/screenshots/webextension/icons/copy.svg
@@ -0,0 +1,7 @@
+<!-- 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" width="16" height="16" viewBox="0 0 16 16">
+  <style>.st0{fill:#3e3d40}</style>
+  <path class="st0" fill="context-fill" d="M14.707 8.293l-3-3A1 1 0 0 0 11 5h-1V4a1 1 0 0 0-.293-.707l-3-3A1 1 0 0 0 6 0H3a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h3v3a2 2 0 0 0 2 2h5a2 2 0 0 0 2-2V9a1 1 0 0 0-.293-.707zM12.586 9H11V7.414zm-5-5H6V2.414zM6 7v2H3V2h2v2.5a.5.5 0 0 0 .5.5H8a2 2 0 0 0-2 2zm2 7V7h2v2.5a.5.5 0 0 0 .5.5H13v4z"></path>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/screenshots/webextension/icons/help-16.svg
@@ -0,0 +1,6 @@
+<!-- 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" width="16" height="16" viewBox="0 0 16 16">
+  <path fill="rgba(249, 249, 250, .8)" d="M8 1a7 7 0 1 0 7 7 7.008 7.008 0 0 0-7-7zm0 13a6 6 0 1 1 6-6 6.007 6.007 0 0 1-6 6zM8 3.125A2.7 2.7 0 0 0 5.125 6a.875.875 0 0 0 1.75 0c0-1 .6-1.125 1.125-1.125a1.105 1.105 0 0 1 1.13.744.894.894 0 0 1-.53 1.016A2.738 2.738 0 0 0 7.125 9v.337a.875.875 0 0 0 1.75 0v-.37a1.041 1.041 0 0 1 .609-.824A2.637 2.637 0 0 0 10.82 5.16 2.838 2.838 0 0 0 8 3.125zm0 7.625A1.25 1.25 0 1 0 9.25 12 1.25 1.25 0 0 0 8 10.75z"></path>
+</svg>
deleted file mode 100644
--- a/browser/extensions/screenshots/webextension/icons/icon-starred-32-v2.svg
+++ /dev/null
@@ -1,1 +0,0 @@
-<svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32"><title>icon-starred-32-v2</title><path fill="context-fill" fill-opacity="context-fill-opacity" d="M8 2a4 4 0 0 0-4 4h4zm6 0h-4v4h4zm14 22a4 4 0 0 0 4-4h-4zm-12.2.64l6 4.6a4 4 0 0 0 5.57-1l-8.09-6.04zm6.06-7.2L9.9 26a5 5 0 1 1-2.3-3.27l2.8-2L7.92 19A5 5 0 1 1 4 10.1V8h4v3a5 5 0 0 1 2 4 5 5 0 0 1-.06.58l3.94 2.76 4.46-3.29a9 9 0 0 0 3.52 2.39zM5 17.5A2.5 2.5 0 1 0 2.5 15 2.5 2.5 0 0 0 5 17.5zm0 12A2.5 2.5 0 1 0 2.5 27 2.5 2.5 0 0 0 5 29.5zM16.51 6H16V2h3.34a9 9 0 0 0-2.83 4zM28 17.49V18h4v-3.34a9 9 0 0 1-4 2.83z"/><circle fill="#00feff" fill-opacity="context-fill-opacity" cx="25" cy="9" r="7"/><path fill="#005a71" fill-opacity="context-fill-opacity" d="M25 4a.89.89 0 0 1 .89.69l.4 1.65.07.31.31-.09 1.64-.48a.91.91 0 0 1 1.11.54.87.87 0 0 1-.22 1L28 8.78l-.27.22.23.22 1.24 1.17a.87.87 0 0 1 .22 1 .91.91 0 0 1-1.11.54l-1.64-.48-.31-.09-.07.31-.4 1.65a.92.92 0 0 1-1.78 0l-.4-1.65-.07-.31-.31.09-1.64.48a.91.91 0 0 1-1.11-.54.87.87 0 0 1 .22-1L22 9.22l.27-.22-.27-.22-1.21-1.17a.87.87 0 0 1-.22-1 .91.91 0 0 1 1.11-.54l1.64.48.31.09.07-.31.4-1.65A.89.89 0 0 1 25 4"/></svg>
\ No newline at end of file
--- a/browser/extensions/screenshots/webextension/manifest.json
+++ b/browser/extensions/screenshots/webextension/manifest.json
@@ -1,12 +1,12 @@
 {
   "manifest_version": 2,
   "name": "Firefox Screenshots",
-  "version": "23.0.0",
+  "version": "25.0.0",
   "description": "__MSG_addonDescription__",
   "author": "__MSG_addonAuthorsList__",
   "homepage_url": "https://github.com/mozilla-services/screenshots",
   "applications": {
     "gecko": {
       "id": "screenshots@mozilla.org",
       "strict_min_version": "57.0a1"
     }
@@ -29,19 +29,21 @@
         "sitehelper.js"
       ]
     }
   ],
   "web_accessible_resources": [
     "blank.html",
     "icons/cancel.svg",
     "icons/download.svg",
+    "icons/copy.svg",
     "icons/icon-256.png",
     "icons/back.svg",
     "icons/back-highlight.svg",
+    "icons/help-16.svg",
     "icons/menu-fullpage.svg",
     "icons/menu-visible.svg",
     "icons/menu-myshot.svg",
     "icons/onboarding-1.png",
     "icons/onboarding-2.png",
     "icons/onboarding-3.png",
     "icons/onboarding-4.png",
     "icons/onboarding-5.png",
--- a/browser/extensions/screenshots/webextension/selector/shooter.js
+++ b/browser/extensions/screenshots/webextension/selector/shooter.js
@@ -191,16 +191,24 @@ this.shooter = (function() { // eslint-d
           location: selectedPos
         }
       });
       ui.triggerDownload(dataUrl, shotObject.filename);
       uicontrol.deactivate();
     }));
   };
 
+  exports.copyShot = function(selectedPos) {
+    let dataUrl = screenshotPage(selectedPos);
+    let blob = blobConverters.dataUrlToBlob(dataUrl);
+    catcher.watchPromise(callBackground("copyShotToClipboard", blob).then(() => {
+      uicontrol.deactivate();
+    }));
+  };
+
   exports.sendEvent = function(...args) {
     callBackground("sendEvent", ...args);
   };
 
   catcher.watchFunction(() => {
     shotObject = new AbstractShot(
       backend,
       randomString(RANDOM_STRING_LENGTH) + "/" + domainFromUrl(location),
--- a/browser/extensions/screenshots/webextension/selector/ui.js
+++ b/browser/extensions/screenshots/webextension/selector/ui.js
@@ -86,16 +86,45 @@ this.ui = (function() { // eslint-disabl
     let computed = win.getComputedStyle(el);
     // When Windows is in High Contrast mode, Firefox replaces background
     // image URLs with the string "none".
     result = computed && computed.backgroundImage === "none";
     doc.body.removeChild(el);
     return result;
   }
 
+  let isDownloadOnly = exports.isDownloadOnly = function() {
+    return window.downloadOnly;
+  }
+
+  // the download notice is rendered in iframes that match the document height
+  // or the window height. If parent iframe matches window height, pass in true
+  function renderDownloadNotice(initAtBottom = false) {
+    let notice = makeEl("table", "notice");
+    notice.innerHTML = `
+      <div class="notice-tooltip">
+        <p data-l10n-id="downloadOnlyDetails"></p>
+        <ul>
+          <li data-l10n-id="downloadOnlyDetailsPrivate"></li>
+          <li data-l10n-id="downloadOnlyDetailsNeverRemember"></li>
+        </ul>
+      </div>
+      <tbody>
+        <tr class="notice-wrapper">
+          <td class="notice-content" data-l10n-id="downloadOnlyNotice"></td>
+          <td class="notice-help"></td>
+        </tr>
+      <tbody>`;
+    localizeText(notice);
+    if (initAtBottom) {
+      notice.style.bottom = '10px';
+    }
+    return notice;
+  }
+
   function initializeIframe() {
     let el = document.createElement("iframe");
     el.src = browser.extension.getURL("blank.html");
     el.style.zIndex = "99999999999";
     el.style.border = "none";
     el.style.top = "0";
     el.style.left = "0";
     el.style.margin = "0";
@@ -150,16 +179,17 @@ this.ui = (function() { // eslint-disabl
     hide() {
       this.element.style.display = "none";
       this.stopSizeWatch();
     },
 
     unhide() {
       this.updateElementSize();
       this.element.style.display = "";
+      catcher.watchPromise(callBackground("sendEvent", "internal", "unhide-selection-frame"));
       if (highContrastCheck(this.element.contentWindow)) {
         this.element.contentDocument.body.classList.add("hcm");
       }
       this.initSizeWatch();
       this.element.focus();
     },
 
     updateElementSize(force) {
@@ -169,31 +199,40 @@ this.ui = (function() { // eslint-disabl
       const visible = this.element.style.display !== "none";
       if (force && visible) {
         this.element.style.display = "none";
       }
       let height = Math.max(
         document.documentElement.clientHeight,
         document.body.clientHeight,
         document.documentElement.scrollHeight,
-        document.body.scrollHeight,
-        window.innerHeight);
+        document.body.scrollHeight);
       if (height !== this.sizeTracking.lastHeight) {
         this.sizeTracking.lastHeight = height;
         this.element.style.height = height + "px";
       }
+      // Do not use window.innerWidth since that includes the width of the
+      // scroll bar.
       let width = Math.max(
         document.documentElement.clientWidth,
         document.body.clientWidth,
         document.documentElement.scrollWidth,
-        document.body.scrollWidth,
-        window.innerWidth);
+        document.body.scrollWidth);
       if (width !== this.sizeTracking.lastWidth) {
         this.sizeTracking.lastWidth = width;
         this.element.style.width = width + "px";
+        // Since this frame has an absolute position relative to the parent
+        // document, if the parent document has a max-width that is narrower
+        // than the viewport, then the x of the parent document is not at 0 of
+        // the viewport. That makes the frame shifted to the right. This left
+        // margin negates that.
+        let boundingRect = document.body.getBoundingClientRect();
+        if (boundingRect.x) {
+          this.element.style.marginLeft = `-${boundingRect.x}px`;
+        }
       }
       if (force && visible) {
         this.element.style.display = "";
       }
     },
 
     initSizeWatch() {
       this.stopSizeWatch();
@@ -259,42 +298,45 @@ this.ui = (function() { // eslint-disabl
                    <div class="fixed-container">
                      <div class="face-container">
                        <div class="eye left"><div class="eyeball"></div></div>
                        <div class="eye right"><div class="eyeball"></div></div>
                        <div class="face"></div>
                      </div>
                      <div class="preview-instructions" data-l10n-id="screenshotInstructions"></div>
                      <div class="myshots-all-buttons-container">
-                       <button class="myshots-button myshots-link" tabindex="1" data-l10n-id="myShotsLink"></button>
-                       <div class="spacer"></div>
+                       ${isDownloadOnly() ? '' : `
+                         <button class="myshots-button myshots-link" tabindex="1" data-l10n-id="myShotsLink"></button>
+                         <div class="spacer"></div>
+                       `}
                        <button class="myshots-button visible" tabindex="2" data-l10n-id="saveScreenshotVisibleArea"></button>
                        <button class="myshots-button full-page" tabindex="3" data-l10n-id="saveScreenshotFullPage"></button>
                      </div>
                    </div>
                  </div>
                </body>`;
             installHandlerOnDocument(this.document);
             if (this.addClassName) {
               this.document.body.className = this.addClassName;
             }
             this.document.documentElement.dir = browser.i18n.getMessage("@@bidi_dir");
             this.document.documentElement.lang = browser.i18n.getMessage("@@ui_locale");
             const overlay = this.document.querySelector(".preview-overlay");
             localizeText(this.document);
-            overlay.querySelector(".myshots-button").addEventListener(
-              "click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onOpenMyShots)));
+            if (!(isDownloadOnly())) {
+              overlay.querySelector(".myshots-button").addEventListener(
+                "click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onOpenMyShots)));
+            }
             overlay.querySelector(".visible").addEventListener(
               "click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onClickVisible)));
             overlay.querySelector(".full-page").addEventListener(
               "click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onClickFullPage)));
             resolve();
           }), {once: true});
           document.body.appendChild(this.element);
-          this.unhide();
         } else {
           resolve();
         }
       });
     },
 
     hide() {
       window.removeEventListener("scroll", watchFunction(assertIsTrusted(this.onScroll)));
@@ -303,16 +345,17 @@ this.ui = (function() { // eslint-disabl
         this.element.style.display = "none";
       }
     },
 
     unhide() {
       window.addEventListener("scroll", watchFunction(assertIsTrusted(this.onScroll)));
       window.addEventListener("resize", this.onResize, true);
       this.element.style.display = "";
+      catcher.watchPromise(callBackground("sendEvent", "internal", "unhide-preselection-frame"));
       if (highContrastCheck(this.element.contentWindow)) {
         this.element.contentDocument.body.classList.add("hcm");
       }
       this.element.focus();
     },
 
     onScroll() {
       exports.HoverBox.hide();
@@ -332,16 +375,21 @@ this.ui = (function() { // eslint-disabl
     remove() {
       this.hide();
       util.removeNode(this.element);
       this.element = null;
       this.document = null;
     }
   };
 
+  function getAttributeText(l10nID) {
+    let text = browser.i18n.getMessage(l10nID);
+    return text && text.replace('"', "&quot;");
+  }
+
   let iframePreview = exports.iframePreview = {
     element: null,
     document: null,
     display(installHandlerOnDocument, standardOverlayCallbacks) {
       return new Promise((resolve, reject) => {
         if (!this.element) {
           this.element = initializeIframe();
           this.element.id = "firefox-screenshots-preview-iframe";
@@ -356,32 +404,49 @@ this.ui = (function() { // eslint-disabl
               <head>
                 <style>${substitutedCss}</style>
                 <title></title>
               </head>
               <body>
                 <div class="preview-overlay">
                   <div class="preview-image">
                     <div class="preview-buttons">
-                      <button class="highlight-button-cancel"></button>
-                      <button class="highlight-button-download"></button>
-                      <button class="preview-button-save" data-l10n-id="saveScreenshotSelectedArea"></button>
+                      <button class="highlight-button-cancel"
+                        title="${getAttributeText("cancelScreenshot")}"></button>
+                      <button class="highlight-button-copy"
+                        title="${getAttributeText("copyScreenshot")}"></button>
+                      ${isDownloadOnly() ?
+                        `<button class="highlight-button-download download-only-button"
+                                 title="${getAttributeText("downloadScreenshot")}"
+                                 data-l10n-id="downloadScreenshot"></button>` :
+                        `<button class="highlight-button-download"
+                                 title="${getAttributeText("downloadScreenshot")}"></button>
+                         <button class="preview-button-save"
+                                 title="${getAttributeText("saveScreenshotSelectedArea")}"
+                                 data-l10n-id="saveScreenshotSelectedArea"></button>`
+                      }
                     </div>
                   </div>
                 </div>
               </body>`;
             installHandlerOnDocument(this.document);
             this.document.documentElement.dir = browser.i18n.getMessage("@@bidi_dir");
             this.document.documentElement.lang = browser.i18n.getMessage("@@ui_locale");
             localizeText(this.document);
             const overlay = this.document.querySelector(".preview-overlay");
+            if (isDownloadOnly()) {
+              overlay.appendChild(renderDownloadNotice(true));
+            } else {
+              overlay.querySelector(".preview-button-save").addEventListener(
+                "click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onSavePreview)));
+            }
+            overlay.querySelector(".highlight-button-copy").addEventListener(
+              "click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onCopyPreview)));
             overlay.querySelector(".highlight-button-download").addEventListener(
               "click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onDownloadPreview)));
-            overlay.querySelector(".preview-button-save").addEventListener(
-              "click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onSavePreview)));
             overlay.querySelector(".highlight-button-cancel").addEventListener(
               "click", watchFunction(assertIsTrusted(standardOverlayCallbacks.cancel)));
             resolve();
           });
           document.body.appendChild(this.element);
         } else {
           resolve();
         }
@@ -391,16 +456,17 @@ this.ui = (function() { // eslint-disabl
     hide() {
       if (this.element) {
         this.element.style.display = "none";
       }
     },
 
     unhide() {
       this.element.style.display = "";
+      catcher.watchPromise(callBackground("sendEvent", "internal", "unhide-preview-frame"));
       this.element.focus();
     },
 
     remove() {
       this.hide();
       util.removeNode(this.element);
       this.element = null;
       this.document = null;
@@ -474,44 +540,54 @@ this.ui = (function() { // eslint-disabl
       if (callbacks !== undefined && callbacks.cancel) {
         // We use onclick here because we don't want addEventListener
         // to add multiple event handlers to the same button
         this.cancel.onclick = watchFunction(assertIsTrusted(callbacks.cancel));
         this.cancel.style.display = "";
       } else {
         this.cancel.style.display = "none";
       }
-      if (callbacks !== undefined && callbacks.save) {
+      if (callbacks !== undefined && callbacks.save && this.save) {
         // We use onclick here because we don't want addEventListener
         // to add multiple event handlers to the same button
         this.save.removeAttribute("disabled");
         this.save.onclick = watchFunction(assertIsTrusted((e) => {
           this.save.setAttribute("disabled", "true");
           callbacks.save(e);
         }));
         this.save.style.display = "";
-      } else {
+      } else if (this.save) {
         this.save.style.display = "none";
       }
       if (callbacks !== undefined && callbacks.download) {
         this.download.removeAttribute("disabled");
         this.download.onclick = watchFunction(assertIsTrusted((e) => {
           this.download.setAttribute("disabled", true);
           callbacks.download(e);
           e.preventDefault();
           e.stopPropagation();
           return false;
         }));
         this.download.style.display = "";
       } else {
         this.download.style.display = "none";
       }
+      if (callbacks !== undefined && callbacks.copy) {
+        this.copy.removeAttribute("disabled");
+        this.copy.onclick = watchFunction(assertIsTrusted((e) => {
+          this.copy.setAttribute("disabled", true);
+          callbacks.copy(e);
+          e.preventDefault();
+          e.stopPropagation();
+        }));
+        this.copy.style.display = "";
+      } else {
+        this.copy.style.display = "none";
+      }
       let bodyRect = getBodyRect();
-      // Note, document.documentElement.scrollHeight is zero on some strange pages (such as the page created when you load an image):
-      let docHeight = Math.max(document.documentElement.scrollHeight || 0, document.body.scrollHeight);
 
       let winBottom = window.innerHeight;
       let pageYOffset = window.pageYOffset;
 
       if ((pos.right - pos.left) < 78 || (pos.bottom - pos.top) < 78) {
         this.el.classList.add("small-selection");
       } else {
         this.el.classList.remove("small-selection");
@@ -534,43 +610,86 @@ this.ui = (function() { // eslint-disabl
       this.el.style.left = (pos.left - bodyRect.left) + "px";
       this.el.style.height = (pos.bottom - pos.top - bodyRect.top) + "px";
       this.el.style.width = (pos.right - pos.left - bodyRect.left) + "px";
       this.bgTop.style.top = "0px";
       this.bgTop.style.height = (pos.top - bodyRect.top) + "px";
       this.bgTop.style.left = "0px";
       this.bgTop.style.width = "100%";
       this.bgBottom.style.top = (pos.bottom - bodyRect.top) + "px";
-      this.bgBottom.style.height = docHeight - (pos.bottom - bodyRect.top) + "px";
+      this.bgBottom.style.height = "100vh";
       this.bgBottom.style.left = "0px";
       this.bgBottom.style.width = "100%";
       this.bgLeft.style.top = (pos.top - bodyRect.top) + "px";
       this.bgLeft.style.height = pos.bottom - pos.top + "px";
       this.bgLeft.style.left = "0px";
       this.bgLeft.style.width = (pos.left - bodyRect.left) + "px";
       this.bgRight.style.top = (pos.top - bodyRect.top) + "px";
       this.bgRight.style.height = pos.bottom - pos.top + "px";
       this.bgRight.style.left = (pos.right - bodyRect.left) + "px";
       this.bgRight.style.width = "100%";
+      // the download notice is injected into an iframe that matches the document size
+      // in order to reposition it on scroll we need to bind an updated positioning
+      // function to some window events.
+      this.repositionDownloadNotice = () => {
+        if (this.downloadNotice) {
+          const currentYOffset = window.pageYOffset;
+          const currentWinBottom = window.innerHeight;
+          this.downloadNotice.style.top = (currentYOffset + currentWinBottom - 60) + "px";
+        }
+      };
+
+      if (this.downloadNotice) {
+        this.downloadNotice.style.top = (pageYOffset + winBottom - 60) + "px";
+        // event callbacks are delayed 100ms each to keep from overloading things
+        this.windowChangeStop = this.delayExecution(100, this.repositionDownloadNotice);
+        window.addEventListener('scroll', watchFunction(assertIsTrusted(this.windowChangeStop)));
+        window.addEventListener('resize', watchFunction(assertIsTrusted(this.windowChangeStop)));
+      }
 
       if (!(this.isElementInViewport(this.buttons))) {
-        this.cancel.style.position = this.download.style.position = this.save.style.position = "fixed";
+        this.cancel.style.position = this.download.style.position = "fixed";
         this.cancel.style.left = (pos.left - bodyRect.left - 50) + "px";
         this.download.style.left = ((pos.left - bodyRect.left - 100)) + "px";
-        this.save.style.left = ((pos.left - bodyRect.left) - 190) + "px";
-        this.cancel.style.top = this.download.style.top = this.save.style.top = (pos.top - bodyRect.top) + "px";
+        this.cancel.style.top = this.download.style.top = (pos.top - bodyRect.top) + "px";
+        if (this.save) {
+          this.save.style.position = "fixed";
+          this.save.style.left = ((pos.left - bodyRect.left) - 190) + "px";
+          this.save.style.top = (pos.top - bodyRect.top) + "px";
+        }
       } else {
-        this.cancel.style.position = this.download.style.position = this.save.style.position = "initial";
-        this.cancel.style.top = this.download.style.top = this.save.style.top = 0;
-        this.cancel.style.left = this.download.style.left = this.save.style.left = 0;
+        this.cancel.style.position = this.download.style.position = "initial";
+        this.cancel.style.top = this.download.style.top = 0;
+        this.cancel.style.left = this.download.style.left = 0;
+        if (this.save) {
+          this.save.style.position = "initial";
+          this.save.style.top = 0;
+          this.save.style.left = 0;
+        }
+      }
+    },
+
+    // used to eventually move the download-only warning
+    // when a user ends scrolling or ends resizing a window
+    delayExecution(delay, cb) {
+      let timer;
+      return function() {
+        if (typeof timer !== 'undefined') {
+          clearTimeout(timer);
+        }
+        timer = setTimeout(cb, delay);
       }
     },
 
     remove() {
-      for (let name of ["el", "bgTop", "bgLeft", "bgRight", "bgBottom"]) {
+      if (this.downloadNotice) {
+        window.removeEventListener('scroll', this.windowChangeStop, true);
+        window.removeEventListener('resize', this.windowChangeStop, true);
+      }
+      for (let name of ["el", "bgTop", "bgLeft", "bgRight", "bgBottom", "downloadNotice"]) {
         if (name in this) {
           util.removeNode(this[name]);
           this[name] = null;
         }
       }
     },
 
     _createEl() {
@@ -578,26 +697,42 @@ this.ui = (function() { // eslint-disabl
       if (boxEl) {
         return;
       }
       boxEl = makeEl("div", "highlight");
       let buttons = makeEl("div", "highlight-buttons");
       let cancel = makeEl("button", "highlight-button-cancel");
       cancel.title = browser.i18n.getMessage("cancelScreenshot");
       buttons.appendChild(cancel);
-      let download = makeEl("button", "highlight-button-download");
-      download.title = browser.i18n.getMessage("downloadScreenshot");
+
+      let copy = makeEl("button", "highlight-button-copy");
+      copy.title = browser.i18n.getMessage("copyScreenshot");
+      buttons.appendChild(copy);
+
+      let download, save;
+
+      if (isDownloadOnly()) {
+        download = makeEl("button", "highlight-button-download download-only-button");
+        download.title = browser.i18n.getMessage("downloadScreenshot");
+        download.textContent = browser.i18n.getMessage("downloadScreenshot");
+      } else {
+        download = makeEl("button", "highlight-button-download");
+        download.title = browser.i18n.getMessage("downloadScreenshot");
+        save = makeEl("button", "highlight-button-save");
+        save.textContent = browser.i18n.getMessage("saveScreenshotSelectedArea");
+        save.title = browser.i18n.getMessage("saveScreenshotSelectedArea");
+      }
       buttons.appendChild(download);
-      let save = makeEl("button", "highlight-button-save");
-      save.textContent = browser.i18n.getMessage("saveScreenshotSelectedArea");
-      save.title = browser.i18n.getMessage("saveScreenshotSelectedArea");
-      buttons.appendChild(save);
+      if (save) {
+        buttons.appendChild(save);
+      }
       this.buttons = buttons;
       this.cancel = cancel;
       this.download = download;
+      this.copy = copy;
       this.save = save;
       boxEl.appendChild(buttons);
       for (let name of movements) {
         let elTarget = makeEl("div", "mover-target direction-" + name);
         let elMover = makeEl("div", "mover");
         elTarget.appendChild(elMover);
         boxEl.appendChild(elTarget);
       }
@@ -605,16 +740,20 @@ this.ui = (function() { // eslint-disabl
       iframe.document().body.appendChild(this.bgTop);
       this.bgLeft = makeEl("div", "bghighlight");
       iframe.document().body.appendChild(this.bgLeft);
       this.bgRight = makeEl("div", "bghighlight");
       iframe.document().body.appendChild(this.bgRight);
       this.bgBottom = makeEl("div", "bghighlight");
       iframe.document().body.appendChild(this.bgBottom);
       iframe.document().body.appendChild(boxEl);
+      if (isDownloadOnly()) {
+        this.downloadNotice = renderDownloadNotice();
+        iframe.document().body.appendChild(this.downloadNotice);
+      }
       this.el = boxEl;
     },
 
     draggerDirection(target) {
       while (target) {
         if (target.nodeType == document.ELEMENT_NODE) {
           if (target.classList.contains("mover-target")) {
             for (let name of movements) {
@@ -690,16 +829,19 @@ this.ui = (function() { // eslint-disabl
       this.el.style.width = (rect.right - rect.left + 2) + "px";
       this.el.style.height = (rect.bottom - rect.top + 2) + "px";
     },
 
     hide() {
       if (this.el) {
         this.el.style.display = "none";
       }
+      if (this.downloadNotice) {
+        this.downloadNotice.display = "none";
+      }
     },
 
     remove() {
       util.removeNode(this.el);
       this.el = null;
     }
   };
 
@@ -727,20 +869,26 @@ this.ui = (function() { // eslint-disabl
     }
   };
 
   exports.Preview = {
     display(dataUrl, showCropWarning) {
       let img = makeEl("IMG");
       img.src = dataUrl;
       iframe.document().querySelector(".preview-image").appendChild(img);
-      if (showCropWarning) {
-        let imageCroppedEl = makeEl("DIV");
-        imageCroppedEl.id = "imageCroppedWarning";
-        imageCroppedEl.textContent = browser.i18n.getMessage("imageCroppedWarning", buildSettings.maxImageHeight);
+      if (showCropWarning && !(isDownloadOnly())) {
+        let imageCroppedEl = makeEl("table", "notice");
+        imageCroppedEl.style.bottom = "10px";
+        imageCroppedEl.innerHTML = `<tbody>
+          <tr class="notice-wrapper">
+            <td class="notice-content"></td>
+          </tr>
+        </tbody>`;
+        let contentCell = imageCroppedEl.getElementsByTagName("td");
+        contentCell[0].textContent = browser.i18n.getMessage("imageCroppedWarning", buildSettings.maxImageHeight);
         iframe.document().querySelector(".preview-overlay").appendChild(imageCroppedEl);
       }
     }
   };
 
   /** Removes every UI this module creates */
   exports.remove = function() {
     for (let name in exports) {
--- a/browser/extensions/screenshots/webextension/selector/uicontrol.js
+++ b/browser/extensions/screenshots/webextension/selector/uicontrol.js
@@ -129,16 +129,19 @@ this.uicontrol = (function() {
       sendEvent("cancel-shot", "overlay-cancel-button");
       exports.deactivate();
     }, save: () => {
       sendEvent("save-shot", "overlay-save-button");
       shooter.takeShot("selection", selectedPos);
     }, download: () => {
       sendEvent("download-shot", "overlay-download-button");
       shooter.downloadShot(selectedPos);
+    }, copy: () => {
+      sendEvent("copy-shot", "overlay-copy-button");
+      shooter.copyShot(selectedPos);
     }
   };
 
   let standardOverlayCallbacks = {
     cancel: () => {
       sendEvent("cancel-shot", "cancel-preview-button");
       exports.deactivate();
     },
@@ -150,17 +153,17 @@ this.uicontrol = (function() {
           // Handled in communication.js
         });
     },
     onClickVisible: () => {
       sendEvent("capture-visible", "selection-button");
       selectedPos = new Selection(
         window.scrollX, window.scrollY,
         window.scrollX + window.innerWidth, window.scrollY + window.innerHeight);
-      captureType = 'visible';
+      captureType = "visible";
       setState("previewing");
     },
     onClickFullPage: () => {
       sendEvent("capture-full-page", "selection-button");
       captureType = "fullPage";
       let width = getDocumentWidth();
       if (width > MAX_PAGE_WIDTH) {
         captureType = "fullPageTruncated";
@@ -187,16 +190,20 @@ this.uicontrol = (function() {
       if (captureType === "fullPageTruncated") {
         captureType = "fullPage";
         selectedPos = new Selection(
           0, 0,
           getDocumentWidth(), getDocumentHeight());
       }
 
       shooter.downloadShot(selectedPos);
+    },
+    onCopyPreview: () => {
+      sendEvent(`copy-${captureType.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}`, "copy-preview-button");
+      shooter.copyShot(selectedPos);
     }
   };
 
   /** Holds all the objects that handle events for each state: */
   let stateHandlers = {};
 
   function getState() {
     return getState.state;
@@ -881,16 +888,17 @@ this.uicontrol = (function() {
   }
 
   function isFrameset() {
     return document.body.tagName == "FRAMESET";
   }
 
   exports.deactivate = function() {
     try {
+      sendEvent("internal", "deactivate");
       setState("cancel");
       callBackground('closeSelector');
       selectorLoader.unloadModules();
     } catch (e) {
       log.error('Error in deactivate', e)
       // Sometimes this fires so late that the document isn't available
       // We don't care about the exception, so we swallow it here
     }
@@ -953,26 +961,40 @@ this.uicontrol = (function() {
   }
 
   function beforeunloadHandler() {
     sendEvent("cancel-shot", "tab-load");
     exports.deactivate();
   }
 
   function keyupHandler(event) {
-    if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
-      // Modified
+    if (event.shiftKey || event.altKey) {
+      // unused modifier keys
       return;
     }
+    if (event.code === "KeyC" && (event.ctrlKey || event.metaKey)) {
+      callBackground("getPlatformOs").then(os => {
+        if ((event.ctrlKey && os !== "mac") ||
+            (event.metaKey && os === "mac")) {
+          sendEvent("copy-shot", "keyboard-copy");
+          shooter.copyShot(selectedPos);
+        }
+      }).catch(() => {
+        // handled by catcher.watchPromise
+      });
+    }
     if ((event.key || event.code) === "Escape") {
       sendEvent("cancel-shot", "keyboard-escape");
       exports.deactivate();
     }
-    if ((event.key || event.code) === "Enter") {
-      if (getState.state === "selected") {
+    if ((event.key || event.code) === "Enter" && getState.state === "selected") {
+      if (ui.isDownloadOnly()) {
+        sendEvent("download-shot", "keyboard-enter");
+        shooter.downloadShot(selectedPos);
+      } else {
         sendEvent("save-shot", "keyboard-enter");
         shooter.takeShot("selection", selectedPos);
       }
     }
   }
 
   function removeHandlers() {
     window.removeEventListener("beforeunload", beforeunloadHandler);
--- a/browser/extensions/screenshots/webextension/sitehelper.js
+++ b/browser/extensions/screenshots/webextension/sitehelper.js
@@ -1,26 +1,19 @@
-/* globals catcher, callBackground */
+/* globals catcher, callBackground, content */
 /** This is a content script added to all screenshots.firefox.com pages, and allows the site to
     communicate with the add-on */
 
 "use strict";
 
 this.sitehelper = (function() {
 
-  let ContentXMLHttpRequest = XMLHttpRequest;
   // This gives us the content's copy of XMLHttpRequest, instead of the wrapped
   // copy that this content script gets:
-  if (location.origin === "https://screenshots.firefox.com" ||
-      location.origin === "http://localhost:10080") {
-    // Note http://localhost:10080 is the default development server
-    // This code should always run, unless this content script is
-    // somehow run in a bad/malicious context
-    ContentXMLHttpRequest = window.wrappedJSObject.XMLHttpRequest;
-  }
+  let ContentXMLHttpRequest = content.XMLHttpRequest;
 
   catcher.registerHandler((errorObj) => {
     callBackground("reportError", errorObj);
   });
 
 
   function sendCustomEvent(name, detail) {
     if (typeof detail == "object") {