Skip to content
This repository has been archived by the owner on Jan 17, 2023. It is now read-only.

Commit

Permalink
Add copy shot to clipboard. (#4776) (#4917)
Browse files Browse the repository at this point in the history
  • Loading branch information
chenba authored and jaredhirsch committed Sep 26, 2018
1 parent 0d69850 commit 5597b0b
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 39 deletions.
7 changes: 7 additions & 0 deletions locales/en-US/server.ftl
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions server/src/pages/shot/controller.js
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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;
5 changes: 5 additions & 0 deletions server/src/pages/shot/model.js
Expand Up @@ -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,
Expand Down Expand Up @@ -74,6 +78,7 @@ exports.createModel = function(req) {
downloadUrl,
isMobile,
enableAnnotations,
copyImageErrorMessage,
};
if (serverPayload.expireTime !== null && Date.now() > serverPayload.expireTime) {
clientPayload.shot = {
Expand Down
99 changes: 78 additions & 21 deletions server/src/pages/shot/view.js
Expand Up @@ -43,10 +43,17 @@ class Clip extends React.Component {
if (!isValidClipImageUrl(clip.image.url)) {
return null;
}
const node = <img id="clipImage" style={{height: "auto", width: Math.floor(clip.image.dimensions.x) + "px", maxWidth: "100%" }} ref={clipImage => this.clipImage = clipImage} src={ clip.image.url } alt={ clip.image.text } />;

const node = <img id="clipImage"
style={{height: "auto", width: Math.floor(clip.image.dimensions.x) + "px", maxWidth: "100%" }}
ref={clipImage => this.clipImage = clipImage}
src={clip.image.url}
alt={clip.image.text} />;

const clipUrl = this.props.isMobile
? this.props.downloadUrl
: clip.image.url;

return <div ref={clipContainer => this.clipContainer = clipContainer} className="clip-container">
{ this.copyTextContextMenu() }
<a href={ clipUrl } onClick={ this.onClickClip.bind(this) } contextMenu="clip-image-context">
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -227,7 +238,9 @@ class Body extends React.Component {
const clipNames = shot.clipNames();
const clip = shot.getClip(clipNames[0]);
return <reactruntime.BodyTemplate {...this.props}>
<Editor clip={clip} pngToJpegCutoff={this.props.pngToJpegCutoff} onCancelEdit={this.onCancelEdit.bind(this)} onClickSave={this.onClickSave.bind(this)}></Editor>
<Editor clip={clip} pngToJpegCutoff={this.props.pngToJpegCutoff}
onCancelEdit={this.onCancelEdit.bind(this)}
onClickSave={this.onClickSave.bind(this)}></Editor>
</reactruntime.BodyTemplate>;
}

Expand Down Expand Up @@ -343,26 +356,47 @@ class Body extends React.Component {
let favoriteShotButton = null;
let trashOrFlagButton = null;
let editButton = null;
const highlight = this.state.highlightEditButton ? <div className="edit-highlight" onClick={ this.onClickEdit.bind(this) } onMouseOver={ this.onMouseOverHighlight.bind(this) } onMouseOut={ this.onMouseOutHighlight.bind(this) }></div> : null;
const highlight = this.state.highlightEditButton
? <div className="edit-highlight"
onClick={this.onClickEdit.bind(this)}
onMouseOver={this.onMouseOverHighlight.bind(this)}
onMouseOut={this.onMouseOutHighlight.bind(this)}></div>
: null;
const activeFavClass = this.props.expireTime ? "" : "is-fav";
const inactive = this.props.isFxaAuthenticated ? "" : "inactive";

favoriteShotButton = <div className="favorite-shot-button"><Localized id="shotPagefavoriteButton">
<button className={`nav-button ${inactive}`} disabled={!this.props.isFxaAuthenticated} onClick={this.onClickFavorite.bind(this)}>
<span className={`icon-favorite favorite ${activeFavClass}`} ></span>
<Localized id="shotPageFavorite">
<span className={`favorite-text favorite ${activeFavClass} `}>Favorite</span>
</Localized>
</button></Localized></div>;

const downloadButton = <div className="download-shot-button"><Localized id="shotPageDownloadShot">
<button className={`nav-button icon-download`} onClick={this.onClickDownload.bind(this)}
title="Download the shot image">
<Localized id="shotPageDownload">
<span>Download</span>
</Localized>
</button>
</Localized></div>;
favoriteShotButton = <div className="favorite-shot-button">
<Localized id="shotPagefavoriteButton">
<button className={`nav-button ${inactive}`}
disabled={!this.props.isFxaAuthenticated}
onClick={this.onClickFavorite.bind(this)}>
<span className={`icon-favorite favorite ${activeFavClass}`} ></span>
<Localized id="shotPageFavorite">
<span className={`favorite-text favorite ${activeFavClass} `}>Favorite</span>
</Localized>
</button></Localized></div>;

const downloadButton = <div className="download-shot-button">
<Localized id="shotPageDownloadShot">
<button className="nav-button icon-download" onClick={this.onClickDownload.bind(this)}
title="Download the shot image">
<Localized id="shotPageDownload">
<span>Download</span>
</Localized>
</button>
</Localized></div>;

const copyButton = <div className="copy-img-button" hidden={!this.state.canCopy}>
<Localized id="shotPageCopyButton">
<button className="nav-button icon-copy transparent copy"
title="Copy image to clipboard"
onClick={this.onClickCopy.bind(this)}>
<Localized id="shotPageCopy">
<span>Copy Image</span>
</Localized>
</button>
</Localized>
</div>;

if (this.props.isOwner) {
trashOrFlagButton = <DeleteShotButton
Expand Down Expand Up @@ -409,6 +443,7 @@ class Body extends React.Component {
shot={this.props.shot} expireTime={this.props.expireTime} shouldGetFirefox={renderGetFirefox}>
{ favoriteShotButton }
{ editButton }
{ copyButton }
{ downloadButton }
{ trashOrFlagButton }
</ShotPageHeader>
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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,
Expand Down
11 changes: 0 additions & 11 deletions static/css/partials/_buttons.scss
Expand Up @@ -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;
}
}
}
4 changes: 4 additions & 0 deletions static/css/partials/_header.scss
Expand Up @@ -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");
}
Expand Down
1 change: 0 additions & 1 deletion static/img/icon-flag.svg

This file was deleted.

5 changes: 5 additions & 0 deletions webextension/background/main.js
Expand Up @@ -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;
})();
26 changes: 20 additions & 6 deletions webextension/sitehelper.js
Expand Up @@ -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
Expand Down Expand Up @@ -50,29 +56,37 @@ 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);
sendCustomEvent("login-successful", {deviceId: info.deviceId, accountId: info.accountId, isOwner: info.isOwner, backupCookieRequest: true});
}));
}));

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;

0 comments on commit 5597b0b

Please sign in to comment.