diff --git a/locales/en-US/server.ftl b/locales/en-US/server.ftl
index 4de25c9062..37a0bd5a82 100644
--- a/locales/en-US/server.ftl
+++ b/locales/en-US/server.ftl
@@ -265,6 +265,13 @@ textToolCancelButton = Cancel
textToolInputPlaceholder =
.placeholder = Hello
+## The following are the title and message for an error displayed as a Firefox
+## notification. It is triggered by an action in the shot page and the strings
+## are passed from the shot page to the addon.
+
+copyImageErrorTitle = Something went wrong
+copyImageErrorMessage = Unable to copy your shot to the clipboard.
+
## Settings Page
settingsDisconnectButton = Disconnect
diff --git a/server/src/pages/shot/controller.js b/server/src/pages/shot/controller.js
index 0335a679cf..b82ba16439 100644
--- a/server/src/pages/shot/controller.js
+++ b/server/src/pages/shot/controller.js
@@ -78,6 +78,8 @@ exports.launch = function(data) {
}
model.highlightEditButton = shouldHighlightEditIcon(model);
model.promoDialog = shouldShowPromo(model);
+ document.dispatchEvent(new CustomEvent("request-addon-present"));
+
if (firstSet) {
refreshHash();
}
@@ -281,4 +283,14 @@ function render() {
page.render(model);
}
+document.addEventListener("addon-present", (e) => {
+ if (e.detail) {
+ const capabilities = JSON.parse(e.detail);
+ if (capabilities["copy-to-clipboard"]) {
+ model.canCopy = true;
+ render();
+ }
+ }
+});
+
window.controller = exports;
diff --git a/server/src/pages/shot/model.js b/server/src/pages/shot/model.js
index 6b132918e6..69ee05975c 100644
--- a/server/src/pages/shot/model.js
+++ b/server/src/pages/shot/model.js
@@ -13,6 +13,10 @@ exports.createModel = function(req) {
const title = req.getText("shotPageTitle", {originalTitle: req.shot.title});
const enableAnnotations = req.config.enableAnnotations;
const isFxaAuthenticated = req.accountId && req.accountId === req.shot.accountId;
+ const copyImageErrorMessage = {
+ title: req.getText("copyImageErrorTitle"),
+ message: req.getText("copyImageErrorMessage"),
+ };
const serverPayload = {
title,
staticLink: req.staticLink,
@@ -74,6 +78,7 @@ exports.createModel = function(req) {
downloadUrl,
isMobile,
enableAnnotations,
+ copyImageErrorMessage,
};
if (serverPayload.expireTime !== null && Date.now() > serverPayload.expireTime) {
clientPayload.shot = {
diff --git a/server/src/pages/shot/view.js b/server/src/pages/shot/view.js
index e83a1f51cd..48a4096c99 100644
--- a/server/src/pages/shot/view.js
+++ b/server/src/pages/shot/view.js
@@ -43,10 +43,17 @@ class Clip extends React.Component {
if (!isValidClipImageUrl(clip.image.url)) {
return null;
}
- const node = this.clipImage = clipImage} src={ clip.image.url } alt={ clip.image.text } />;
+
+ const node = this.clipImage = clipImage}
+ src={clip.image.url}
+ alt={clip.image.text} />;
+
const clipUrl = this.props.isMobile
? this.props.downloadUrl
: clip.image.url;
+
return
this.clipContainer = clipContainer} className="clip-container">
{ this.copyTextContextMenu() }
@@ -183,12 +190,16 @@ class Body extends React.Component {
this.state = {
hidden: false,
imageEditing: false,
+ canCopy: false,
};
}
componentDidMount() {
- this.setState({highlightEditButton: this.props.highlightEditButton || this.props.promoDialog});
- this.setState({promoDialog: this.props.promoDialog});
+ this.setState({
+ highlightEditButton: this.props.highlightEditButton || this.props.promoDialog,
+ promoDialog: this.props.promoDialog,
+ canCopy: !!this.props.canCopy,
+ });
}
clickDeleteHandler() {
@@ -227,7 +238,9 @@ class Body extends React.Component {
const clipNames = shot.clipNames();
const clip = shot.getClip(clipNames[0]);
return
-
+
;
}
@@ -343,26 +356,47 @@ class Body extends React.Component {
let favoriteShotButton = null;
let trashOrFlagButton = null;
let editButton = null;
- const highlight = this.state.highlightEditButton ? : null;
+ const highlight = this.state.highlightEditButton
+ ?
+ : null;
const activeFavClass = this.props.expireTime ? "" : "is-fav";
const inactive = this.props.isFxaAuthenticated ? "" : "inactive";
- favoriteShotButton =
-
;
-
- const downloadButton =
-
-
;
+ favoriteShotButton =
+
+
;
+
+ const downloadButton =
+
+
+
;
+
+ const copyButton =
+
+
+
+
;
if (this.props.isOwner) {
trashOrFlagButton =
{ favoriteShotButton }
{ editButton }
+ { copyButton }
{ downloadButton }
{ trashOrFlagButton }
@@ -447,6 +482,27 @@ class Body extends React.Component {
}
}
+ async onClickCopy() {
+ const clipId = this.props.shot.clipNames()[0];
+ const clip = this.props.shot.getClip(clipId);
+ try {
+ const resp = await fetch(clip.image.url);
+
+ if (!resp.ok) {
+ throw new Error(resp.statusText);
+ }
+
+ const blob = await resp.blob();
+ document.dispatchEvent(new CustomEvent("copy-to-clipboard", {detail: blob}));
+ } catch (e) {
+ document.dispatchEvent(new CustomEvent("show-notification", { detail: {
+ type: "basic",
+ title: this.props.copyImageErrorMessage.title,
+ message: this.props.copyImageErrorMessage.message,
+ }}));
+ }
+ }
+
onClickSave(dataUrl, dimensions) {
this.props.controller.saveEdit(this.props.shot, dataUrl, dimensions);
}
@@ -503,6 +559,7 @@ Body.propTypes = {
abTests: PropTypes.object,
backend: PropTypes.string,
blockType: PropTypes.string,
+ canCopy: PropTypes.bool,
controller: PropTypes.object,
defaultExpiration: PropTypes.number,
downloadUrl: PropTypes.string,
diff --git a/static/css/partials/_buttons.scss b/static/css/partials/_buttons.scss
index 289a732607..e64beac9d0 100644
--- a/static/css/partials/_buttons.scss
+++ b/static/css/partials/_buttons.scss
@@ -138,15 +138,4 @@
background-color: $light-active;
}
}
-
- &.flag {
- background-image: url("../img/icon-flag.svg");
- &:hover {
- background-color: $light-hover;
- }
-
- &:active {
- background-color: $light-active;
- }
- }
}
diff --git a/static/css/partials/_header.scss b/static/css/partials/_header.scss
index 361db8206e..f2b97c737f 100644
--- a/static/css/partials/_header.scss
+++ b/static/css/partials/_header.scss
@@ -100,6 +100,10 @@
background-image: url("../img/icon-pen.svg");
}
+ &.icon-copy {
+ background-image: url("../img/icon-copy.svg");
+ }
+
&.icon-download {
background-image: url("../img/icon-download.svg");
}
diff --git a/static/img/icon-flag.svg b/static/img/icon-flag.svg
deleted file mode 100644
index 0a41f19db7..0000000000
--- a/static/img/icon-flag.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/webextension/background/main.js b/webextension/background/main.js
index 1da031e3dd..a14e944e6e 100644
--- a/webextension/background/main.js
+++ b/webextension/background/main.js
@@ -301,5 +301,10 @@ this.main = (function() {
}));
});
+ // This allows the web site show notifications through sitehelper.js
+ communication.register("showNotification", (sender, notification) => {
+ return browser.notifications.create(notification);
+ });
+
return exports;
})();
diff --git a/webextension/sitehelper.js b/webextension/sitehelper.js
index aa9bd0ec79..35a20eb445 100644
--- a/webextension/sitehelper.js
+++ b/webextension/sitehelper.js
@@ -15,6 +15,12 @@ this.sitehelper = (function() {
});
+ const capabilities = {};
+ function registerListener(name, func) {
+ capabilities[name] = name;
+ document.addEventListener(name, func);
+ }
+
function sendCustomEvent(name, detail) {
if (typeof detail === "object") {
// Note sending an object can lead to security problems, while a string
@@ -50,11 +56,11 @@ this.sitehelper = (function() {
};
}
- document.addEventListener("delete-everything", catcher.watchFunction((event) => {
+ registerListener("delete-everything", catcher.watchFunction((event) => {
// FIXME: reset some data in the add-on
}, false));
- document.addEventListener("request-login", catcher.watchFunction((event) => {
+ registerListener("request-login", catcher.watchFunction((event) => {
const shotId = event.detail;
catcher.watchPromise(callBackground("getAuthInfo", shotId || null).then((info) => {
sendBackupCookieRequest(info.authHeaders);
@@ -62,17 +68,25 @@ this.sitehelper = (function() {
}));
}));
- document.addEventListener("request-onboarding", catcher.watchFunction((event) => {
+ registerListener("request-onboarding", catcher.watchFunction((event) => {
callBackground("requestOnboarding");
}));
+ registerListener("copy-to-clipboard", catcher.watchFunction(event => {
+ catcher.watchPromise(callBackground("copyShotToClipboard", event.detail));
+ }));
+
+ registerListener("show-notification", catcher.watchFunction(event => {
+ catcher.watchPromise(callBackground("showNotification", event.detail));
+ }));
+
// Depending on the script loading order, the site might get the addon-present event,
// but probably won't - instead the site will ask for that event after it has loaded
- document.addEventListener("request-addon-present", catcher.watchFunction(() => {
- sendCustomEvent("addon-present");
+ registerListener("request-addon-present", catcher.watchFunction(() => {
+ sendCustomEvent("addon-present", capabilities);
}));
- sendCustomEvent("addon-present");
+ sendCustomEvent("addon-present", capabilities);
})();
null;