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;