Commit 8107b000 authored by Robert Knight's avatar Robert Knight

Dim other annotations on standalone reply pages

On the standalone pages for replies, indicate the reply which the page
is for by lightening the usernames and bodies of other annotations in the
thread and darkening the username and body of the annotation whose ID
appears in the URL.
parent af8ca570
......@@ -50,6 +50,9 @@ function initialState(settings) {
// by the user even if they do not match the current search filter
forceVisible: {},
// IDs of annotations that should be highlighted
highlighted: [],
filterQuery: null,
// Key by which annotations are currently sorted.
......@@ -62,6 +65,7 @@ function initialState(settings) {
var types = {
SELECT_ANNOTATIONS: 'SELECT_ANNOTATIONS',
FOCUS_ANNOTATIONS: 'FOCUS_ANNOTATIONS',
HIGHLIGHT_ANNOTATIONS: 'HIGHLIGHT_ANNOTATIONS',
SET_HIGHLIGHTS_VISIBLE: 'SET_HIGHLIGHTS_VISIBLE',
SET_FORCE_VISIBLE: 'SET_FORCE_VISIBLE',
SET_EXPANDED: 'SET_EXPANDED',
......@@ -113,6 +117,8 @@ function reducer(state, action) {
return Object.assign({}, state, {forceVisible: action.forceVisible});
case types.SET_EXPANDED:
return Object.assign({}, state, {expanded: action.expanded});
case types.HIGHLIGHT_ANNOTATIONS:
return Object.assign({}, state, {highlighted: action.highlighted});
case types.SET_FILTER_QUERY:
return Object.assign({}, state, {
filterQuery: action.query,
......@@ -311,5 +317,18 @@ module.exports = function (settings) {
key: key,
});
},
/**
* Highlight annotations with the given `ids`.
*
* This is used to indicate the specific annotation in a thread that was
* linked to for example.
*/
highlightAnnotations: function (ids) {
store.dispatch({
type: types.HIGHLIGHT_ANNOTATIONS,
highlighted: ids,
});
},
};
};
......@@ -75,6 +75,10 @@ function AnnotationViewerController (
annots.forEach(function (annot) {
annotationUI.setCollapsed(annot.id, false);
});
if (topLevelAnnot.id !== id) {
annotationUI.highlightAnnotations([id]);
}
});
}
......
......@@ -28,6 +28,13 @@ var DEFAULT_THREAD_STATE = {
* including any which have been hidden by filters.
*/
totalChildren: 0,
/**
* The highlight state of this annotation:
* undefined - Do not (de-)emphasize this annotation
* 'dim' - De-emphasize this annotation
* 'highlight' - Emphasize this annotation
*/
highlightState: undefined,
};
/**
......@@ -234,6 +241,8 @@ var defaultOpts = {
* Mapping of annotation IDs to expansion states.
*/
expanded: {},
/** List of highlighted annotation IDs */
highlighted: [],
/**
* Less-than comparison function used to compare annotations in order to sort
* the top-level thread.
......@@ -291,10 +300,17 @@ function buildThread(annotations, opts) {
});
}
// Set the visibility of threads based on whether they match
// the current search filter
// Set the visibility and highlight states of threads
thread = mapThread(thread, function (thread) {
var highlightState;
if (opts.highlighted.length > 0) {
var isHighlighted = thread.annotation &&
opts.highlighted.indexOf(thread.id) !== -1;
highlightState = isHighlighted ? 'highlight' : 'dim';
}
return Object.assign({}, thread, {
highlightState: highlightState,
visible: thread.visible &&
thread.annotation &&
shouldShowThread(thread.annotation),
......
......@@ -62,6 +62,8 @@ function AnnotationThreadController() {
annotation: true,
'annotation--reply': this.thread.depth > 0,
'is-collapsed': this.thread.collapsed,
'is-highlighted': this.thread.highlightState === 'highlight',
'is-dimmed': this.thread.highlightState === 'dim',
};
};
......
......@@ -68,6 +68,7 @@ function RootThread($rootScope, annotationUI, searchFilter, viewFilter) {
return buildThread(state.annotations, {
forceVisible: truthyKeys(state.forceVisible),
expanded: state.expanded,
highlighted: state.highlighted,
selected: truthyKeys(state.selectedAnnotationMap || {}),
sortCompareFn: sortFn,
filterFn: filterFn,
......
......@@ -218,4 +218,11 @@ describe('annotationUI', function () {
assert.deepEqual(annotationUI.getState().expanded, {});
});
});
describe('#highlightAnnotations()', function () {
it('sets the highlighted annotations', function () {
annotationUI.highlightAnnotations(['id1', 'id2']);
assert.deepEqual(annotationUI.getState().highlighted, ['id1', 'id2']);
});
});
});
......@@ -58,7 +58,11 @@ describe('AnnotationViewerController', function () {
$scope: {
search: {},
},
annotationUI: {setCollapsed: sinon.stub(), subscribe: sinon.stub()},
annotationUI: {
setCollapsed: sinon.stub(),
highlightAnnotations: sinon.stub(),
subscribe: sinon.stub()
},
rootThread: {thread: sinon.stub()},
streamer: { setConfig: function () {} },
store: opts.store,
......@@ -95,6 +99,17 @@ describe('AnnotationViewerController', function () {
sinon.match(fakeStore.annots));
});
});
it('does not highlight any annotations', function () {
var fakeStore = new FakeStore([
{id: 'test_annotation_id'},
{id: 'test_reply_id', references: ['test_annotation_id']},
]);
var controller = createController({store: fakeStore});
return controller.ctrl.ready.then(function () {
assert.notCalled(controller.annotationUI.highlightAnnotations);
});
});
});
describe('the standalone view for a reply', function () {
......@@ -121,5 +136,17 @@ describe('AnnotationViewerController', function () {
assert.calledWith(controller.annotationUI.setCollapsed, 'test_annotation_id', false);
});
});
it('highlights the reply', function () {
var fakeStore = new FakeStore([
{id: 'parent_id'},
{id: 'test_annotation_id', references: ['parent_id']},
]);
var controller = createController({store: fakeStore});
return controller.ctrl.ready.then(function () {
assert.calledWith(controller.annotationUI.highlightAnnotations,
sinon.match(['test_annotation_id']));
});
});
});
});
......@@ -339,4 +339,23 @@ describe('build-thread', function () {
assert.deepEqual(thread[0].children[0].depth, 1);
});
});
describe('highlighting', function () {
it('does not set highlight state when none are highlighted', function () {
var thread = createThread(SIMPLE_FIXTURE, {}, ['dimmed']);
thread.forEach(function (child) {
assert.equal(child.highlightState, undefined);
});
});
it('highlights annotations', function () {
var thread = createThread(SIMPLE_FIXTURE, {highlighted: ['1']}, ['highlightState']);
assert.equal(thread[0].highlightState, 'highlight');
});
it('dims annotations which are not highlighted', function () {
var thread = createThread(SIMPLE_FIXTURE, {highlighted: ['1']}, ['highlightState']);
assert.equal(thread[1].highlightState, 'dim');
});
});
});
......@@ -31,14 +31,15 @@ describe('rootThread', function () {
fakeAnnotationUI = {
state: {
annotations: [],
visibleHighlights: false,
focusedAnnotationMap: null,
selectedAnnotationMap: null,
expanded: {},
forceVisible: {},
filterQuery: null,
focusedAnnotationMap: null,
forceVisible: {},
highlighted: [],
selectedAnnotationMap: null,
sortKey: 'Location',
sortKeysAvailable: ['Location'],
visibleHighlights: false,
},
getState: function () {
......@@ -120,6 +121,16 @@ describe('rootThread', function () {
forceVisible: ['id1', 'id2'],
}));
});
it('passes the highlighted set to buildThread()', function () {
fakeAnnotationUI.state = Object.assign({}, fakeAnnotationUI.state, {
highlighted: ['id1', 'id2'],
});
rootThread.thread(fakeAnnotationUI.state);
assert.calledWith(fakeBuildThread, [], sinon.match({
highlighted: ['id1', 'id2'],
}));
});
});
describe('when the sort order changes', function () {
......
......@@ -49,6 +49,25 @@ $annotation-card-left-padding: 10px;
position: relative;
}
.annotation.is-dimmed {
// Lighten the username and bodies of dimmed annotations to make other
// annotations which are not dimmed stand out
.annotation-header__user,
.annotation-body {
color: $grey-5;
}
}
.annotation.is-highlighted {
// Slightly darken the username and bodies of highlighted annotations to
// make them stand out next to others, which will have the `is-dimmed` state
// set
.annotation-header__user,
.annotation-body {
color: $grey-7;
}
}
.annotation-link {
@include font-small;
color: $grey-4;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment