Skip to content

Commit

Permalink
Show annotation count on the page, so that users don't have to open t…
Browse files Browse the repository at this point in the history
…o sidebar to know how many annotations are in the page.

Fixes: hypothesis/product-backlog#129
  • Loading branch information
Sheetal Umesh Kumar committed Feb 1, 2017
1 parent 4610489 commit c93835e
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 2 deletions.
6 changes: 5 additions & 1 deletion scripts/gulp/live-reload-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ function LiveReloadServer(port, appServer) {
<title>Hypothesis Client Test</title>
</head>
<body>
<pre style="margin: 75px;">${changelogText()}</pre>
<div data-hypothesis-trigger style="margin: 75px 0 0 75px;">
Number of annotations:
<span data-hypothesis-annotation-count>...</span>
</div>
<pre style="margin: 20px 75px 75px 75px;">${changelogText()}</pre>
<script>
var appHost = document.location.hostname;
Expand Down
26 changes: 26 additions & 0 deletions src/annotator/annotation-counts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use strict';

var events = require('../shared/bridge-events');

var ANNOTATION_COUNT_ATTR = 'data-hypothesis-annotation-count';

/**
* Update the elements in the container element with the count data attribute
* with the new annotation count.
*
* @param {Element} rootEl - The DOM element which contains the elements that
* display annotation count.
*/

function annotationCounts(rootEl, crossframe) {
crossframe.on(events.PUBLIC_ANNOTATION_COUNT_CHANGED, updateAnnotationCountElems);

function updateAnnotationCountElems(newCount) {
var elems = rootEl.querySelectorAll('['+ANNOTATION_COUNT_ATTR+']');
Array.from(elems).forEach(function(elem) {
elem.textContent = newCount;
});
}
}

module.exports = annotationCounts;
6 changes: 5 additions & 1 deletion src/annotator/sidebar.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ raf = require('raf')
Hammer = require('hammerjs')

Host = require('./host')
annotationCounts = require('./annotation-counts')
sidebarTrigger = require('./sidebar-trigger')

# Minimum width to which the frame can be resized.
Expand Down Expand Up @@ -37,13 +38,16 @@ module.exports = class Sidebar extends Host
this._setupSidebarEvents()

_setupDocumentEvents: ->
sidebarTrigger(document, @show.bind(this))
sidebarTrigger(document.body, @show.bind(this))

@element.on 'click', (event) =>
if !@selectedTargets?.length
this.hide()
return this

_setupSidebarEvents: ->
annotationCounts(document.body, @crossframe)

@crossframe.on('show', this.show.bind(this))
@crossframe.on('hide', this.hide.bind(this))

Expand Down
67 changes: 67 additions & 0 deletions src/annotator/test/annotation-counts-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use strict';

var annotationCounts = require('../annotation-counts');

describe('annotationCounts', function () {
var countEl1;
var countEl2;
var CrossFrame;
var fakeCrossFrame;
var sandbox;

beforeEach(function () {
CrossFrame = null;
fakeCrossFrame = {};
sandbox = sinon.sandbox.create();

countEl1 = document.createElement('button');
countEl1.setAttribute('data-hypothesis-annotation-count');
document.body.appendChild(countEl1);

countEl2 = document.createElement('button');
countEl2.setAttribute('data-hypothesis-annotation-count');
document.body.appendChild(countEl2);

fakeCrossFrame.on = sandbox.stub().returns(fakeCrossFrame);

CrossFrame = sandbox.stub();
CrossFrame.returns(fakeCrossFrame);
});

afterEach(function () {
sandbox.restore();
countEl1.remove();
countEl2.remove();
});

describe('listen for "publicAnnotationCountChanged" event', function () {
var emitEvent = function () {
var crossFrameArgs;
var evt;
var fn;

var event = arguments[0];
var args = 2 <= arguments.length ? Array.prototype.slice.call(arguments, 1) : [];

crossFrameArgs = fakeCrossFrame.on.args;
for (var i = 0, len = crossFrameArgs.length; i < len; i++) {
evt = crossFrameArgs[i][0];
fn = crossFrameArgs[i][1];

if (event === evt) {
fn.apply(null, args);
}
}
};

it('displays the updated annotation count on the appropriate elements', function () {
var newCount = 10;
annotationCounts(document.body, fakeCrossFrame);

emitEvent('publicAnnotationCountChanged', newCount);

assert.equal(countEl1.textContent, newCount);
assert.equal(countEl2.textContent, newCount);
});
});
});
10 changes: 10 additions & 0 deletions src/shared/bridge-events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';

/**
* This module defines the set of global events that are dispatched
* across the bridge between the sidebar and annotator
*/
module.exports = {
/** The set of annotations was updated. */
PUBLIC_ANNOTATION_COUNT_CHANGED: 'publicAnnotationCountChanged',
};
19 changes: 19 additions & 0 deletions src/sidebar/annotation-metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,24 @@ function isNew(annotation) {
return !annotation.id;
}

/** Return `true` if the given annotation is public, `false` otherwise. */
function isPublic(annotation) {
var isPublic = false;

if (!annotation.permissions) {
return isPublic;
}

annotation.permissions.read.forEach(function(perm) {
var readPermArr = perm.split(':');
if (readPermArr.length === 2 && readPermArr[0] === 'group') {
isPublic = true;
}
});

return isPublic;
}

/**
* Return `true` if `annotation` has a selector.
*
Expand Down Expand Up @@ -169,6 +187,7 @@ module.exports = {
isNew: isNew,
isOrphan: isOrphan,
isPageNote: isPageNote,
isPublic: isPublic,
isReply: isReply,
isWaitingToAnchor: isWaitingToAnchor,
location: location,
Expand Down
12 changes: 12 additions & 0 deletions src/sidebar/frame-sync.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

var events = require('./events');
var bridgeEvents = require('../shared/bridge-events');
var metadata = require('./annotation-metadata');
var uiConstants = require('./ui-constants');

Expand Down Expand Up @@ -50,13 +51,15 @@ function FrameSync($rootScope, $window, Discovery, annotationUI, bridge) {
function setupSyncToFrame() {
// List of loaded annotations in previous state
var prevAnnotations = [];
var prevPublicAnns = 0;

annotationUI.subscribe(function () {
var state = annotationUI.getState();
if (state.annotations === prevAnnotations) {
return;
}

var publicAnns = 0;
var inSidebar = new Set();
var added = [];

Expand All @@ -66,6 +69,10 @@ function FrameSync($rootScope, $window, Discovery, annotationUI, bridge) {
return;
}

if (metadata.isPublic(annot)) {
++publicAnns;
}

inSidebar.add(annot.$tag);
if (!inFrame.has(annot.$tag)) {
added.push(annot);
Expand All @@ -89,6 +96,11 @@ function FrameSync($rootScope, $window, Discovery, annotationUI, bridge) {
bridge.call('deleteAnnotation', formatAnnot(annot));
inFrame.delete(annot.$tag);
});

if (publicAnns !== prevPublicAnns) {
bridge.call(bridgeEvents.PUBLIC_ANNOTATION_COUNT_CHANGED, publicAnns);
prevPublicAnns = publicAnns;
}
});
}

Expand Down
20 changes: 20 additions & 0 deletions src/sidebar/test/annotation-fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ function defaultAnnotation() {
};
}

/**
* Return a fake public annotation with the basic properties filled in.
*/
function publicAnnotation() {
return {
id: 'pubann',
document: {
title: 'A special document',
},
permissions: {
read:['group:__world__'],
},
target: [{source: 'source', 'selector': []}],
uri: 'http://example.com',
user: 'acct:bill@localhost',
updated: '2015-05-10T20:18:56.613388+00:00',
};
}

/** Return an annotation domain model object for a new annotation
* (newly-created client-side, not yet saved to the server).
*/
Expand Down Expand Up @@ -120,6 +139,7 @@ function oldReply() {

module.exports = {
defaultAnnotation: defaultAnnotation,
publicAnnotation: publicAnnotation,
newAnnotation: newAnnotation,
newEmptyAnnotation: newEmptyAnnotation,
newHighlight: newHighlight,
Expand Down
22 changes: 22 additions & 0 deletions src/sidebar/test/annotation-metadata-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
var annotationMetadata = require('../annotation-metadata');
var fixtures = require('./annotation-fixtures');

var unroll = require('../../shared/test/util').unroll;

var documentMetadata = annotationMetadata.documentMetadata;
var domainAndTitle = annotationMetadata.domainAndTitle;

Expand Down Expand Up @@ -247,6 +249,26 @@ describe('annotation-metadata', function () {
});
});

describe('.isPublic', function () {
it('returns true if an annotation is shared within a group', function () {
assert.isTrue(annotationMetadata.isPublic(fixtures.publicAnnotation()));
});

unroll('returns false if an annotation is not publicly readable', function (testCase) {
var annotation = Object.assign(fixtures.defaultAnnotation(), {permissions: testCase});
assert.isFalse(annotationMetadata.isPublic(annotation));
}, [{
read:['acct:someemail@localhost'],
}, {
read:['something invalid'],
}]);

it('returns false if an annotation is missing permissions', function () {
var annotation = Object.assign(fixtures.defaultAnnotation());
assert.isFalse(annotationMetadata.isPublic(annotation));
});
});

describe('.isOrphan', function () {
it('returns true if an annotation failed to anchor', function () {
var annotation = Object.assign(fixtures.defaultAnnotation(), {$orphan: true});
Expand Down
7 changes: 7 additions & 0 deletions src/sidebar/test/frame-sync-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ describe('FrameSync', function () {
});
});

context('when annotation count has changed', function () {
it('sends a "publicAnnotationCountChanged" message to the frame', function () {
fakeAnnotationUI.setState({annotations: [annotationFixtures.publicAnnotation()]});
assert.calledWithMatch(fakeBridge.call, 'publicAnnotationCountChanged', sinon.match(1));
});
});

context('when annotations are removed from the sidebar', function () {
it('sends a "deleteAnnotation" message to the frame', function () {
fakeAnnotationUI.setState({annotations: [fixtures.ann]});
Expand Down

0 comments on commit c93835e

Please sign in to comment.