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

Commit

Permalink
add annotation tools: pen and highlighter
Browse files Browse the repository at this point in the history
  • Loading branch information
Niharika Khanna committed Aug 16, 2017
1 parent 1f531dc commit 83d207a
Show file tree
Hide file tree
Showing 16 changed files with 572 additions and 4 deletions.
10 changes: 10 additions & 0 deletions docs/METRICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,16 @@ These are events that an add-on user can encounter on a shot they own

The hashed page ID (`{hash}`) is a simple SHA1(path), with no additional randomness or salt added.

#### Annotation metrics

1. [x] Open annotation tools: `web/start-annotations/navbar`
1. [x] Save Edited shot: `web/save/annotation-toolbar`
2. [x] Cancel Annotations: `web/cancel/annotation-toolbar`
3. [x] Select pen from annotation toolbar: `web/pen-select/annotation-toolbar`
4. [x] Deselect pen from annotation toolbar: `web/pen-deselect/annotation-toolbar`
5. [x] Select highlighter from annotation toolbar: `web/highlighter-select/annotation-toolbar`
6. [x] Deselect highlighter from annotation toolbar: `web/highlighter-deselect/annotation-toolbar`

#### General Google Analytics information

This is stuff we get from including ga.js on Screenshots pages.
Expand Down
7 changes: 7 additions & 0 deletions server/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,13 @@ var conf = convict({
default: false,
env: "USER_SETTINGS",
arg: "user-settings"
},
enableAnnotations: {
doc: "If true, then disable shot annotations",
format: Boolean,
default: true,
env: "ENBALE_ANNOTATIONS",
arg: "enable-annotations"
}
});

Expand Down
29 changes: 29 additions & 0 deletions server/src/pages/shot/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,35 @@ exports.deleteShot = function(shot) {
req.send(`id=${encodeURIComponent(shot.id)}&_csrf=${encodeURIComponent(model.csrfToken)}`);
};

exports.saveEdit = function(shot, shotUrl) {
var url = model.backend + "/api/save-edit";
var body = JSON.stringify({
shotId: shot.id,
_csrf: model.csrfToken,
url: shotUrl
});
var req = new Request(url, {
method: 'POST',
mode: 'cors',
credentials: 'include',
headers: new Headers({
'content-type': 'application/json'
}),
body
});
fetch(req).then((resp) => {
if (!resp.ok) {
var errorMessage = "Error saving edited shot";
window.alert(errorMessage);
} else {
location.reload();
}
}).catch((error) => {
error.popupMessage = "CONNECTION_ERROR";
throw error;
});
}

function refreshHash() {
if (location.hash === "#fullpage") {
let frameOffset = document.getElementById("frame").getBoundingClientRect().top + window.scrollY;
Expand Down
150 changes: 150 additions & 0 deletions server/src/pages/shot/editor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
const React = require("react");
const sendEvent = require("../../browser-send-event.js");

let pos;

exports.Editor = class Editor extends React.Component {
constructor(props) {
super(props);
this.draw = this.draw.bind(this);
this.setPosition = this.setPosition.bind(this);
this.state = {
tool: 'none',
color: '#000000',
size: '5'
};
}

render() {
let penState;
let highlighterState;
let canvasHeight = this.props.clip.image.dimensions.y;
let canvasWidth = this.props.clip.image.dimensions.x;
if (this.state.tool == 'highlighter') {
highlighterState = 'active';
penState = 'inactive';
} else if (this.state.tool == 'pen') {
highlighterState = 'inactive';
penState = 'active';
} else {
penState = 'inactive';
highlighterState = 'inactive';
}
return <div>
<div className="editor-header default-color-scheme">
<div className="shot-main-actions">
<a className={`button pen-button ${penState}`} id="pen" onClick={this.onClickPen.bind(this)} title="pen"></a>
<a className={`button highlight-button ${highlighterState}`} id="highlight" onClick={this.onClickHighlight.bind(this)} title="highlighter"></a>
</div>
<div className="shot-alt-actions">
<a className="button primary save" id="save" onClick={ this.onClickSave.bind(this) }>Save</a>
<a className="button secondary cancel" id="cancel" onClick={this.onClickCancel.bind(this)}>Cancel</a>
</div>
</div>
<div className="main-container">
<div className="canvas-container" id="canvas-container" ref={(canvasContainer) => this.canvasContainer = canvasContainer}>
<canvas className="image-holder centered" id="image-holder" ref={(image) => { this.imageCanvas = image }} height={ 2 * canvasHeight } width={ 2 * canvasWidth } style={{height: canvasHeight, width: canvasWidth}}></canvas>
<canvas className="highlighter centered" id="highlighter" ref={(highlighter) => { this.highlighter = highlighter }} height={canvasHeight} width={canvasWidth}></canvas>
<canvas className="editor centered" id="editor" ref={(editor) => { this.editor = editor }} height={canvasHeight} width={canvasWidth}></canvas>
</div>
</div>
</div>
}

componentDidUpdate() {
this.edit();
}

onClickCancel() {
this.props.onCancelEdit(false);
sendEvent("cancel", "annotation-toolbar");
}

onClickSave() {
sendEvent("save", "annotation-toolbar");
this.imageContext.drawImage(this.editor, 0, 0);
this.imageContext.globalCompositeOperation = 'multiply';
this.imageContext.drawImage(this.highlighter, 0, 0);
let dataUrl = this.imageCanvas.toDataURL();
this.props.onClickSave(dataUrl);
}

onClickHighlight() {
if (this.state.tool != 'highlighter') {
this.setState({tool: 'highlighter'});
sendEvent("highlighter-select", "annotation-toolbar");
} else {
this.setState({tool: 'none'});
sendEvent("highlighter-deselect", "annotation-toolbar");
}
}

onClickPen() {
if (this.state.tool != 'pen') {
this.setState({tool: 'pen'});
sendEvent("pen-select", "annotation-toolbar");
} else {
this.setState({tool: 'none'});
sendEvent("pen-deselect", "annotation-toolbar");
}
}

componentDidMount() {
this.context = this.editor.getContext('2d');
this.highlightContext = this.highlighter.getContext('2d');
let imageContext = this.imageCanvas.getContext('2d');
this.imageContext = imageContext;
// From https://blog.headspin.com/?p=464, we oversample the canvas for improved image quality
let img = new Image();
img.src = this.props.clip.image.url;
let width = this.props.clip.image.dimensions.x;
let height = this.props.clip.image.dimensions.y;
this.imageContext.scale(2, 2);
this.imageContext.drawImage(img, 0, 0, width, height);
}

edit() {
pos = { x: 0, y: 0 };
if (this.state.tool == 'highlighter') {
this.highlightContext.lineWidth = 20;
this.highlightContext.strokeStyle = '#ff0';
this.drawContext = this.highlightContext;
} else if (this.state.tool == 'pen') {
this.context.strokeStyle = this.state.color;
this.context.globalCompositeOperation = 'source-over';
this.drawContext = this.context;
}
this.context.lineWidth = this.state.size;
if (this.state.tool == 'none') {
this.canvasContainer.removeEventListener("mousemove", this.draw);
this.canvasContainer.removeEventListener("mousedown", this.setPosition);
this.canvasContainer.removeEventListener("mouseenter", this.setPosition);
} else {
this.canvasContainer.addEventListener("mousemove", this.draw);
this.canvasContainer.addEventListener("mousedown", this.setPosition);
this.canvasContainer.addEventListener("mouseenter", this.setPosition);
}
}

setPosition(e) {
var rect = this.editor.getBoundingClientRect();
pos.x = e.clientX - rect.left,
pos.y = e.clientY - rect.top
}

draw(e) {
if (e.buttons !== 1) {
return null;
}
this.drawContext.beginPath();

this.drawContext.lineCap = 'square';
this.drawContext.moveTo(pos.x, pos.y);
let rect = this.editor.getBoundingClientRect();
pos.x = e.clientX - rect.left,
pos.y = e.clientY - rect.top
this.drawContext.lineTo(pos.x, pos.y);

this.drawContext.stroke();
}
}
7 changes: 5 additions & 2 deletions server/src/pages/shot/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ exports.createModel = function(req) {
req.shot.favicon = createProxyUrl(req, req.shot.favicon);
}
let title = req.getText("shotPageTitle", {originalTitle: req.shot.title});
let enableAnnotations = req.config.enableAnnotations;
let serverPayload = {
title,
staticLink: req.staticLink,
Expand All @@ -40,7 +41,8 @@ exports.createModel = function(req) {
userAgent: req.headers['user-agent'],
blockType: req.shot.blockType,
downloadUrl,
isMobile
isMobile,
enableAnnotations
};
let clientPayload = {
title: req.shot.title,
Expand Down Expand Up @@ -68,7 +70,8 @@ exports.createModel = function(req) {
userAgent: req.headers['user-agent'],
blockType: req.shot.blockType,
downloadUrl,
isMobile
isMobile,
enableAnnotations
};
if (serverPayload.expireTime !== null && Date.now() > serverPayload.expireTime) {
clientPayload.shot = {
Expand Down
38 changes: 36 additions & 2 deletions server/src/pages/shot/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const sendEvent = require("../../browser-send-event.js");
const { ShareButton } = require("../../share-buttons");
const { TimeDiff } = require("./time-diff");
const reactruntime = require("../../reactruntime");
const { Editor } = require("./editor");

class Clip extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -157,7 +158,8 @@ class Body extends React.Component {
this.state = {
hidden: false,
closeBanner: false,
isChangingExpire: false
isChangingExpire: false,
imageEditing: false
};
}

Expand Down Expand Up @@ -188,9 +190,21 @@ class Body extends React.Component {
if (this.props.expireTime !== null && Date.now() > this.props.expireTime) {
return this.renderExpired();
}
if (this.state.imageEditing) {
return this.renderEditor();
}
return this.renderBody();
}

renderEditor() {
let shot = this.props.shot;
let clipNames = shot.clipNames();
let clip = shot.getClip(clipNames[0]);
return <reactruntime.BodyTemplate {...this.props}>
<Editor clip={clip} onCancelEdit={this.onCancelEdit.bind(this)} onClickSave={this.onClickSave.bind(this)}></Editor>
</reactruntime.BodyTemplate>;
}

renderBlock() {
let message = null;
let moreInfo = null;
Expand Down Expand Up @@ -314,14 +328,17 @@ class Body extends React.Component {
}

let trashOrFlagButton;
let editButton;
if (this.props.isOwner) {
trashOrFlagButton = <Localized id="shotPageDeleteButton">
<button className="button transparent trash" title="Delete this shot permanently" onClick={ this.onClickDelete.bind(this) }></button>
</Localized>;
editButton = <button className="button transparent edit" title="Edit this image" onClick={ this.onClickEdit.bind(this) }></button>
} else {
trashOrFlagButton = <Localized id="shotPageAbuseButton">
<button className="button transparent flag" title="Report this shot for abuse, spam, or other problems" onClick={ this.onClickFlag.bind(this) }></button>
</Localized>;
editButton = null;
}

let myShotsHref = "/shots";
Expand All @@ -339,10 +356,11 @@ class Body extends React.Component {
myShotsHref = "/";
}

let clip;
let clipUrl = null;
if (clipNames.length) {
let clipId = clipNames[0];
let clip = this.props.shot.getClip(clipId);
clip = this.props.shot.getClip(clipId);
clipUrl = clip.image.url;
}

Expand Down Expand Up @@ -378,6 +396,7 @@ class Body extends React.Component {
</div>
</div>
<div className="shot-alt-actions">
{ this.props.enableAnnotations ? editButton : null }
{ trashOrFlagButton }
<ShareButton abTests={this.props.abTests} clipUrl={clipUrl} shot={shot} isOwner={this.props.isOwner} staticLink={this.props.staticLink} renderExtensionNotification={renderExtensionNotification} isExtInstalled={this.props.isExtInstalled} />
<Localized id="shotPageDownloadShot">
Expand Down Expand Up @@ -411,6 +430,21 @@ class Body extends React.Component {
</div>;
}

onClickEdit() {
if (!this.state.imageEditing) {
this.setState({imageEditing: true});
sendEvent("start-annotations", "navbar");
}
}

onClickSave(dataUrl) {
this.props.controller.saveEdit(this.props.shot, dataUrl);
}

onCancelEdit(imageEditing) {
this.setState({imageEditing});
}

clickedInstallExtension() {
sendEvent("click-install-banner", {useBeacon: true});
}
Expand Down
24 changes: 24 additions & 0 deletions server/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,30 @@ app.post("/api/set-title/:id/:domain", csrfProtection, function(req, res) {
});
});

app.post("/api/save-edit", csrfProtection, function(req, res) {
let vars = req.body;
if (!req.deviceId) {
sendRavenMessage(req, "Attempt to set expiration without login");
simpleResponse(res, "Not logged in", 401);
return;
}
let id = vars.shotId;
let url = vars.url;
Shot.get(req.backend, id).then((shot) => {
if (!shot) {
simpleResponse(res, "No such shot", 404);
return;
}
let name = shot.clipNames()[0];
shot.getClip(name).image.url = url;
return shot.update();
}).then((updated) => {
simpleResponse(res, "Updated", 200);
}).catch((err) => {
errorResponse(res, "Error updating image", err);
})
});

app.post("/api/set-expiration", csrfProtection, function(req, res) {
if (!req.deviceId) {
sendRavenMessage(req, "Attempt to set expiration without login");
Expand Down
Loading

0 comments on commit 83d207a

Please sign in to comment.