Commit b005ed0d authored by Robert Knight's avatar Robert Knight Committed by Nick Stenning

Render annotation thread using new threading infrastructure

This replaces the rendering of conversation threads using
the new threading implementation.

 * Add <annotation-thread> component to render Thread objects
   generated by rootThread.

 * Expose the Thread generated by the `rootThread` service on
   the scope so that <annotation-thread> can render it.

 * Remove logic and tests in <annotation> for updating reply
   counts and thread states as these are no longer needed.
parent de829f34
...@@ -4,37 +4,26 @@ var angular = require('angular'); ...@@ -4,37 +4,26 @@ var angular = require('angular');
var events = require('./events'); var events = require('./events');
// Fetch the container object for the passed annotation from the threading function getExistingAnnotation(annotationUI, id) {
// service, but only return it if it has an associated message. return annotationUI.getState().annotations.find(function (annot) {
function getContainer(threading, annotation) { return annot.id === id;
var container = threading.idTable[annotation.id]; });
if (container === null || typeof container === 'undefined') {
return null;
}
// Also return null if the container has no message
if (!container.message) {
return null;
}
return container;
} }
// Wraps the annotation store to trigger events for the CRUD actions // Wraps the annotation store to trigger events for the CRUD actions
// @ngInject // @ngInject
function annotationMapper($rootScope, threading, store) { function annotationMapper($rootScope, annotationUI, store) {
function loadAnnotations(annotations, replies) { function loadAnnotations(annotations, replies) {
annotations = annotations.concat(replies || []); annotations = annotations.concat(replies || []);
var loaded = []; var loaded = [];
annotations.forEach(function (annotation) { annotations.forEach(function (annotation) {
var container = getContainer(threading, annotation); var existing = getExistingAnnotation(annotationUI, annotation.id);
if (container !== null) { if (existing) {
angular.copy(annotation, container.message); angular.copy(annotation, existing);
$rootScope.$emit(events.ANNOTATION_UPDATED, container.message); $rootScope.$emit(events.ANNOTATION_UPDATED, existing);
return; return;
} }
loaded.push(new store.AnnotationResource(annotation)); loaded.push(new store.AnnotationResource(annotation));
}); });
...@@ -43,9 +32,9 @@ function annotationMapper($rootScope, threading, store) { ...@@ -43,9 +32,9 @@ function annotationMapper($rootScope, threading, store) {
function unloadAnnotations(annotations) { function unloadAnnotations(annotations) {
var unloaded = annotations.map(function (annotation) { var unloaded = annotations.map(function (annotation) {
var container = getContainer(threading, annotation); var existing = getExistingAnnotation(annotationUI, annotation.id);
if (container !== null && annotation !== container.message) { if (existing && annotation !== existing) {
annotation = angular.copy(annotation, container.message); annotation = angular.copy(annotation, existing);
} }
return annotation; return annotation;
}); });
......
...@@ -5,7 +5,7 @@ var angular = require('angular'); ...@@ -5,7 +5,7 @@ var angular = require('angular');
// @ngInject // @ngInject
function AnnotationViewerController ( function AnnotationViewerController (
$location, $routeParams, $scope, $location, $routeParams, $scope,
streamer, store, streamFilter, annotationMapper, threading annotationUI, rootThread, streamer, store, streamFilter, annotationMapper
) { ) {
var id = $routeParams.id; var id = $routeParams.id;
...@@ -17,9 +17,16 @@ function AnnotationViewerController ( ...@@ -17,9 +17,16 @@ function AnnotationViewerController (
$location.path('/stream').search('q', query); $location.path('/stream').search('q', query);
}; };
$scope.rootThread = function () {
return rootThread.thread();
};
$scope.setCollapsed = function (id, collapsed) {
annotationUI.setCollapsed(id, collapsed);
};
store.AnnotationResource.get({ id: id }, function (annotation) { store.AnnotationResource.get({ id: id }, function (annotation) {
annotationMapper.loadAnnotations([annotation]); annotationMapper.loadAnnotations([annotation]);
$scope.threadRoot = { children: [threading.idTable[id]] };
}); });
store.SearchResource.get({ references: id }, function (data) { store.SearchResource.get({ references: id }, function (data) {
......
...@@ -25,7 +25,6 @@ if (settings.raven) { ...@@ -25,7 +25,6 @@ if (settings.raven) {
angular.module('ngRaven', []); angular.module('ngRaven', []);
} }
var mail = require('./vendor/jwz');
var streamer = require('./streamer'); var streamer = require('./streamer');
// Fetch external state that the app needs before it can run. This includes the // Fetch external state that the app needs before it can run. This includes the
...@@ -40,20 +39,6 @@ var resolve = { ...@@ -40,20 +39,6 @@ var resolve = {
return store.$promise; return store.$promise;
}, },
streamer: streamer.connect, streamer: streamer.connect,
// @ngInject
threading: function (annotationMapper, drafts, threading) {
// Unload all the annotations
annotationMapper.unloadAnnotations(threading.annotationList());
// Reset the threading root
threading.createIdTable([]);
threading.root = mail.messageContainer();
// Reload all new, unsaved annotations
threading.thread(drafts.unsaved());
return threading;
},
}; };
// @ngInject // @ngInject
...@@ -117,15 +102,6 @@ function processAppOpts() { ...@@ -117,15 +102,6 @@ function processAppOpts() {
} }
} }
// @ngInject
function setupTemplateCache($templateCache) {
// 'thread.html' is used via ng-include so it needs to be added
// to the template cache. Other components just require() templates
// directly as strings.
$templateCache.put('thread.html',
require('../../templates/client/thread.html'));
}
module.exports = angular.module('h', [ module.exports = angular.module('h', [
// Angular addons which export the Angular module name // Angular addons which export the Angular module name
// via module.exports // via module.exports
...@@ -155,7 +131,7 @@ module.exports = angular.module('h', [ ...@@ -155,7 +131,7 @@ module.exports = angular.module('h', [
.directive('aboutThisVersionDialog', require('./directive/about-this-version-dialog')) .directive('aboutThisVersionDialog', require('./directive/about-this-version-dialog'))
.directive('annotation', require('./directive/annotation').directive) .directive('annotation', require('./directive/annotation').directive)
.directive('annotationShareDialog', require('./directive/annotation-share-dialog')) .directive('annotationShareDialog', require('./directive/annotation-share-dialog'))
.directive('deepCount', require('./directive/deep-count')) .directive('annotationThread', require('./directive/annotation-thread'))
.directive('dropdownMenuBtn', require('./directive/dropdown-menu-btn')) .directive('dropdownMenuBtn', require('./directive/dropdown-menu-btn'))
.directive('excerpt', require('./directive/excerpt').directive) .directive('excerpt', require('./directive/excerpt').directive)
.directive('feedbackLink', require('./directive/feedback-link')) .directive('feedbackLink', require('./directive/feedback-link'))
...@@ -176,8 +152,6 @@ module.exports = angular.module('h', [ ...@@ -176,8 +152,6 @@ module.exports = angular.module('h', [
.directive('sortDropdown', require('./directive/sort-dropdown')) .directive('sortDropdown', require('./directive/sort-dropdown'))
.directive('spinner', require('./directive/spinner')) .directive('spinner', require('./directive/spinner'))
.directive('statusButton', require('./directive/status-button')) .directive('statusButton', require('./directive/status-button'))
.directive('thread', require('./directive/thread'))
.directive('threadFilter', require('./directive/thread-filter'))
.directive('topBar', require('./directive/top-bar')) .directive('topBar', require('./directive/top-bar'))
.directive('windowScroll', require('./directive/window-scroll')) .directive('windowScroll', require('./directive/window-scroll'))
...@@ -195,12 +169,11 @@ module.exports = angular.module('h', [ ...@@ -195,12 +169,11 @@ module.exports = angular.module('h', [
.service('localStorage', require('./local-storage')) .service('localStorage', require('./local-storage'))
.service('permissions', require('./permissions')) .service('permissions', require('./permissions'))
.service('queryParser', require('./query-parser')) .service('queryParser', require('./query-parser'))
.service('render', require('./render')) .service('rootThread', require('./root-thread'))
.service('searchFilter', require('./search-filter')) .service('searchFilter', require('./search-filter'))
.service('session', require('./session')) .service('session', require('./session'))
.service('streamFilter', require('./stream-filter')) .service('streamFilter', require('./stream-filter'))
.service('tags', require('./tags')) .service('tags', require('./tags'))
.service('threading', require('./threading'))
.service('unicode', require('./unicode')) .service('unicode', require('./unicode'))
.service('viewFilter', require('./view-filter')) .service('viewFilter', require('./view-filter'))
...@@ -219,7 +192,6 @@ module.exports = angular.module('h', [ ...@@ -219,7 +192,6 @@ module.exports = angular.module('h', [
.config(configureRoutes) .config(configureRoutes)
.run(setupCrossFrame) .run(setupCrossFrame)
.run(setupHttp) .run(setupHttp);
.run(setupTemplateCache);
processAppOpts(); processAppOpts();
'use strict';
function hiddenCount(thread) {
var isHidden = thread.annotation && !thread.visible;
return thread.children.reduce(function (count, reply) {
return count + hiddenCount(reply);
}, isHidden ? 1 : 0);
}
function showAllChildren(thread, showFn) {
thread.children.forEach(function (child) {
showFn({thread: child});
showAllChildren(child, showFn);
});
}
function showAllParents(thread, showFn) {
while (thread.parent && thread.parent.annotation) {
showFn({thread: thread.parent});
thread = thread.parent;
}
}
// @ngInject
function AnnotationThreadController() {
this.toggleCollapsed = function () {
this.onChangeCollapsed({
id: this.thread.id,
collapsed: !this.thread.collapsed,
});
};
/**
* Show this thread and any of its children
*/
this.showThreadAndReplies = function () {
showAllParents(this.thread, this.onForceVisible);
this.onForceVisible({thread: this.thread});
showAllChildren(this.thread, this.onForceVisible);
};
/**
* Return the total number of annotations in the current
* thread which have been hidden because they do not match the current
* search filter.
*/
this.hiddenCount = function () {
return hiddenCount(this.thread);
};
}
/**
* Renders a thread of annotations.
*/
module.exports = function () {
return {
restrict: 'E',
bindToController: true,
controllerAs: 'vm',
controller: AnnotationThreadController,
scope: {
/** The annotation thread to render. */
thread: '<',
/**
* Specify whether document information should be shown
* on annotation cards.
*/
showDocumentInfo: '<',
/** Called when the user clicks on the expand/collapse replies toggle. */
onChangeCollapsed: '&',
/**
* Called when the user clicks the button to show this thread or
* one of its replies.
*/
onForceVisible: '&',
},
template: require('../../../templates/client/annotation_thread.html'),
};
};
...@@ -721,51 +721,7 @@ function AnnotationController( ...@@ -721,51 +721,7 @@ function AnnotationController(
function link(scope, elem, attrs, controllers) { function link(scope, elem, attrs, controllers) {
var ctrl = controllers[0]; var ctrl = controllers[0];
var thread = controllers[1];
var threadFilter = controllers[2];
var counter = controllers[3];
elem.on('keydown', ctrl.onKeydown); elem.on('keydown', ctrl.onKeydown);
// FIXME: Replace this counting code with something more sane.
if (counter !== null) {
scope.$watch((function() {return ctrl.editing();}), function(editing, old) {
if (editing) { // The user has just started editing this annotation.
// Keep track of edits going on in the thread.
// This 'edit' count is for example to uncollapse a thread if one of
// the replies in the thread is currently being edited when the
// annotations are first rendered (this can happen when switching
// focus to a different group then back again, for example).
counter.count('edit', 1);
// Always show an annotation if it is being edited, even if there's an
// active search filter that does not match the annotation.
if ((thread !== null) && (threadFilter !== null)) {
threadFilter.active(false);
threadFilter.freeze(true);
}
} else if (old) { // The user has just finished editing this annotation.
counter.count('edit', -1);
if (threadFilter) {
threadFilter.freeze(false);
}
// Ensure that the just-edited annotation remains visible.
if (thread.parent) {
thread.parent.toggleCollapsed(false);
}
}
});
scope.$on('$destroy', function() {
if (ctrl.editing() && counter) {
counter.count('edit', -1);
}
});
}
} }
/** /**
...@@ -785,12 +741,12 @@ function annotation() { ...@@ -785,12 +741,12 @@ function annotation() {
controller: AnnotationController, controller: AnnotationController,
controllerAs: 'vm', controllerAs: 'vm',
link: link, link: link,
require: ['annotation', '?^thread', '?^threadFilter', '?^deepCount'], require: ['annotation'],
scope: { scope: {
annotation: '<', annotation: '<',
// Indicates whether this is the last reply in a thread. // Indicates whether this is the last reply in a thread.
isLastReply: '<', isLastReply: '<',
isSidebar: '<', showDocumentInfo: '<',
onReplyCountClick: '&', onReplyCountClick: '&',
replyCount: '<', replyCount: '<',
isCollapsed: '<' isCollapsed: '<'
......
...@@ -182,104 +182,6 @@ describe('annotation', function() { ...@@ -182,104 +182,6 @@ describe('annotation', function() {
'keydown', mockAnnotationController.onKeydown) 'keydown', mockAnnotationController.onKeydown)
); );
}); });
it('increments the "edit" count when editing() becomes true', function () {
link(scope, mockElement, mockAttributes, mockControllers);
mockAnnotationController.editing.returns(true);
scope.$digest();
assert.equal(true, mockDeepCountController.count.calledOnce);
assert.equal(
true, mockDeepCountController.count.calledWithExactly('edit', 1)
);
});
it('decrements the "edit" count when editing() turns false', function () {
mockAnnotationController.editing.returns(true);
link(scope, mockElement, mockAttributes, mockControllers);
scope.$digest();
mockAnnotationController.editing.returns(false);
scope.$digest();
assert.equal(
true,
mockDeepCountController.count.lastCall.calledWithExactly('edit', -1)
);
});
it('decrements the edit count when destroyed while editing', function () {
mockAnnotationController.editing.returns(true);
link(scope, mockElement, mockAttributes, mockControllers);
scope.$destroy();
assert.equal(1, mockDeepCountController.count.callCount);
assert.equal(
true, mockDeepCountController.count.calledWithExactly('edit', -1)
);
});
it('does not decrement the edit count when destroyed while not editing',
function () {
mockAnnotationController.editing.returns(false);
link(scope, mockElement, mockAttributes, mockControllers);
scope.$destroy();
assert.equal(0, mockDeepCountController.count.callCount);
}
);
it('deactivates the thread filter when editing() turns true', function () {
mockAnnotationController.editing.returns(false);
link(scope, mockElement, mockAttributes, mockControllers);
mockAnnotationController.editing.returns(true);
scope.$digest();
assert.equal(1, mockThreadFilterController.active.callCount);
assert.equal(
true, mockThreadFilterController.active.calledWithExactly(false));
});
it('freezes the thread filter when editing', function () {
mockAnnotationController.editing.returns(false);
link(scope, mockElement, mockAttributes, mockControllers);
mockAnnotationController.editing.returns(true);
scope.$digest();
assert.equal(1, mockThreadFilterController.freeze.callCount);
assert.equal(
true, mockThreadFilterController.freeze.calledWithExactly(true));
});
it('unfreezes the thread filter when editing becomes false', function () {
mockAnnotationController.editing.returns(true);
link(scope, mockElement, mockAttributes, mockControllers);
scope.$digest();
mockAnnotationController.editing.returns(false);
scope.$digest();
assert.equal(
true,
mockThreadFilterController.freeze.lastCall.calledWithExactly(false));
});
it('does not collapse parent thread after edit', function() {
mockAnnotationController.editing.returns(true);
link(scope, mockElement, mockAttributes, mockControllers);
scope.$digest();
mockAnnotationController.editing.returns(false);
scope.$digest();
assert.calledWith(mockThreadController.parent.toggleCollapsed, false);
});
}); });
describe('AnnotationController', function() { describe('AnnotationController', function() {
......
'use strict';
var angular = require('angular');
var annotationThread = require('../annotation-thread');
var util = require('./util');
function PageObject(element) {
this.annotations = function () {
return Array.from(element[0].querySelectorAll('annotation'));
};
this.replies = function () {
return Array.from(element[0].querySelectorAll('annotation-thread'));
};
this.replyList = function () {
return element[0].querySelector('.thread-replies');
};
this.isHidden = function (element) {
return element.classList.contains('ng-hide');
};
}
describe('annotationThread', function () {
before(function () {
angular.module('app', [])
.directive('annotationThread', annotationThread);
});
beforeEach(function () {
angular.mock.module('app');
});
it('renders the tree structure of parent and child annotations', function () {
var element = util.createDirective(document, 'annotationThread', {
thread: {
id: '1',
annotation: {id: '1', text: 'text'},
children: [{
id: '2',
annotation: {id: '2', text: 'areply'},
children: [],
}],
visible: true,
},
});
var pageObject = new PageObject(element);
assert.equal(pageObject.annotations().length, 2);
assert.equal(pageObject.replies().length, 1);
});
it('does not render hidden threads', function () {
var element = util.createDirective(document, 'annotationThread', {
thread: {
id: '1',
annotation: {id: '1'},
visible: false,
children: []
}
});
var pageObject = new PageObject(element);
assert.equal(pageObject.annotations().length, 1);
assert.isTrue(pageObject.isHidden(pageObject.annotations()[0]));
});
it('shows replies if not collapsed', function () {
var element = util.createDirective(document, 'annotationThread', {
thread: {
id: '1',
annotation: {id: '1'},
visible: true,
children: [{
id: '2',
annotation: {id: '2'},
children: [],
visible: true,
}],
collapsed: false,
}
});
var pageObject = new PageObject(element);
assert.equal(pageObject.replies().length, 1);
});
it('does not show replies if collapsed', function () {
var element = util.createDirective(document, 'annotationThread', {
thread: {
id: '1',
annotation: {id: '1'},
visible: true,
children: [{
id: '2',
annotation: {id: '2'},
children: [],
visible: true,
}],
collapsed: true,
}
});
var pageObject = new PageObject(element);
assert.isTrue(pageObject.isHidden(pageObject.replyList()));
});
describe('#toggleCollapsed', function () {
it('toggles replies', function () {
var onChangeCollapsed = sinon.stub();
var element = util.createDirective(document, 'annotationThread', {
thread: {
id: '123',
annotation: {id: '123'},
children: [],
collapsed: true,
},
onChangeCollapsed: {
args: ['id', 'collapsed'],
callback: onChangeCollapsed,
}
});
element.ctrl.toggleCollapsed();
assert.calledWith(onChangeCollapsed, '123', false);
});
});
describe('#showThreadAndReplies', function () {
it('reveals all parents and replies', function () {
var onForceVisible = sinon.stub();
var thread = {
id: '123',
annotation: {id: '123'},
children: [{
id: 'child-id',
annotation: {id: 'child-id'},
children: [],
}],
parent: {
id: 'parent-id',
annotation: {id: 'parent-id'},
},
};
var element = util.createDirective(document, 'annotationThread', {
thread: thread,
onForceVisible: {
args: ['thread'],
callback: onForceVisible,
},
});
element.ctrl.showThreadAndReplies();
assert.calledWith(onForceVisible, thread.parent);
assert.calledWith(onForceVisible, thread);
assert.calledWith(onForceVisible, thread.children[0]);
});
});
});
angular = require('angular') angular = require('angular')
mail = require('./vendor/jwz')
module.exports = class StreamController module.exports = class StreamController
this.$inject = [ this.$inject = [
'$scope', '$route', '$rootScope', '$routeParams', '$scope', '$route', '$rootScope', '$routeParams',
'queryParser', 'searchFilter', 'store', 'annotationUI',
'streamer', 'streamFilter', 'threading', 'annotationMapper' 'queryParser', 'rootThread', 'searchFilter', 'store',
'streamer', 'streamFilter', 'annotationMapper'
] ]
constructor: ( constructor: (
$scope, $route, $rootScope, $routeParams $scope, $route, $rootScope, $routeParams
queryParser, searchFilter, store, annotationUI,
streamer, streamFilter, threading, annotationMapper queryParser, rootThread, searchFilter, store,
streamer, streamFilter, annotationMapper
) -> ) ->
offset = 0 offset = 0
...@@ -26,16 +26,11 @@ module.exports = class StreamController ...@@ -26,16 +26,11 @@ module.exports = class StreamController
offset += rows.length offset += rows.length
annotationMapper.loadAnnotations(rows, replies) annotationMapper.loadAnnotations(rows, replies)
# Disable the thread filter (client-side search)
$scope.$on '$routeChangeSuccess', ->
if $scope.threadFilter?
$scope.threadFilter.active(false)
$scope.threadFilter.freeze(true)
# Reload on query change (ignore hash change) # Reload on query change (ignore hash change)
lastQuery = $routeParams.q lastQuery = $routeParams.q
$scope.$on '$routeUpdate', -> $scope.$on '$routeUpdate', ->
if $routeParams.q isnt lastQuery if $routeParams.q isnt lastQuery
annotationUI.clearAnnotations()
$route.reload() $route.reload()
# Initialize the base filter # Initialize the base filter
...@@ -51,8 +46,19 @@ module.exports = class StreamController ...@@ -51,8 +46,19 @@ module.exports = class StreamController
# Perform the initial search # Perform the initial search
fetch(20) fetch(20)
$scope.$watch('sort.name', (name) ->
rootThread.sortBy(name)
)
$scope.setCollapsed = (id, collapsed) ->
annotationUI.setCollapsed(id, collapsed)
$scope.forceVisible = (id) ->
annotationUI.setForceVisible(id, true)
$scope.isStream = true $scope.isStream = true
$scope.sortOptions = ['Newest', 'Oldest'] $scope.sortOptions = ['Newest', 'Oldest']
$scope.sort.name = 'Newest' $scope.sort.name = 'Newest'
$scope.threadRoot = threading.root $scope.rootThread = ->
return rootThread.thread()
$scope.loadMore = fetch $scope.loadMore = fetch
...@@ -2,37 +2,32 @@ ...@@ -2,37 +2,32 @@
var angular = require('angular'); var angular = require('angular');
var annotationUIFactory = require('../annotation-ui');
var events = require('../events'); var events = require('../events');
describe('annotationMapper', function() { describe('annotationMapper', function() {
var sandbox = sinon.sandbox.create(); var sandbox = sinon.sandbox.create();
var $rootScope; var $rootScope;
var annotationUI;
var fakeStore; var fakeStore;
var fakeThreading;
var annotationMapper; var annotationMapper;
before(function () { beforeEach(function () {
angular.module('h', [])
.service('annotationMapper', require('../annotation-mapper'));
});
beforeEach(angular.mock.module('h'));
beforeEach(angular.mock.module(function ($provide) {
fakeStore = { fakeStore = {
AnnotationResource: sandbox.stub().returns({}) AnnotationResource: sandbox.stub().returns({}),
}; };
fakeThreading = { annotationUI = annotationUIFactory({});
idTable: {} angular.module('app', [])
}; .service('annotationMapper', require('../annotation-mapper'))
.value('annotationUI', annotationUI)
$provide.value('store', fakeStore); .value('store', fakeStore);
$provide.value('threading', fakeThreading); angular.mock.module('app');
}));
angular.mock.inject(function (_$rootScope_, _annotationMapper_) {
beforeEach(angular.mock.inject(function (_annotationMapper_, _$rootScope_) {
$rootScope = _$rootScope_; $rootScope = _$rootScope_;
annotationMapper = _annotationMapper_; annotationMapper = _annotationMapper_;
})); });
});
afterEach(function () { afterEach(function () {
sandbox.restore(); sandbox.restore();
...@@ -58,24 +53,22 @@ describe('annotationMapper', function() { ...@@ -58,24 +53,22 @@ describe('annotationMapper', function() {
[{}, {}, {}]); [{}, {}, {}]);
}); });
it('triggers the annotationUpdated event for each annotation in the threading cache', function () { it('triggers the annotationUpdated event for each loaded annotation', function () {
sandbox.stub($rootScope, '$emit'); sandbox.stub($rootScope, '$emit');
var annotations = [{id: 1}, {id: 2}, {id: 3}]; var annotations = [{id: 1}, {id: 2}, {id: 3}];
var cached = {message: {id: 1, $$tag: 'tag1'}}; annotationUI.addAnnotations(angular.copy(annotations));
fakeThreading.idTable[1] = cached;
annotationMapper.loadAnnotations(annotations); annotationMapper.loadAnnotations(annotations);
assert.called($rootScope.$emit); assert.called($rootScope.$emit);
assert.calledWith($rootScope.$emit, events.ANNOTATION_UPDATED, assert.calledWith($rootScope.$emit, events.ANNOTATION_UPDATED,
cached.message); annotations[0]);
}); });
it('also triggers annotationUpdated for cached replies', function () { it('also triggers annotationUpdated for cached replies', function () {
sandbox.stub($rootScope, '$emit'); sandbox.stub($rootScope, '$emit');
var annotations = [{id: 1}]; var annotations = [{id: 1}];
var replies = [{id: 2}, {id: 3}, {id: 4}]; var replies = [{id: 2}, {id: 3}, {id: 4}];
var cached = {message: {id: 3, $$tag: 'tag3'}}; annotationUI.addAnnotations([{id:3}]);
fakeThreading.idTable[3] = cached;
annotationMapper.loadAnnotations(annotations, replies); annotationMapper.loadAnnotations(annotations, replies);
assert($rootScope.$emit.calledWith(events.ANNOTATION_UPDATED, assert($rootScope.$emit.calledWith(events.ANNOTATION_UPDATED,
...@@ -85,8 +78,7 @@ describe('annotationMapper', function() { ...@@ -85,8 +78,7 @@ describe('annotationMapper', function() {
it('replaces the properties on the cached annotation with those from the loaded one', function () { it('replaces the properties on the cached annotation with those from the loaded one', function () {
sandbox.stub($rootScope, '$emit'); sandbox.stub($rootScope, '$emit');
var annotations = [{id: 1, url: 'http://example.com'}]; var annotations = [{id: 1, url: 'http://example.com'}];
var cached = {message: {id: 1, $$tag: 'tag1'}}; annotationUI.addAnnotations([{id:1, $$tag: 'tag1'}]);
fakeThreading.idTable[1] = cached;
annotationMapper.loadAnnotations(annotations); annotationMapper.loadAnnotations(annotations);
assert.called($rootScope.$emit); assert.called($rootScope.$emit);
...@@ -99,8 +91,7 @@ describe('annotationMapper', function() { ...@@ -99,8 +91,7 @@ describe('annotationMapper', function() {
it('excludes cached annotations from the annotationLoaded event', function () { it('excludes cached annotations from the annotationLoaded event', function () {
sandbox.stub($rootScope, '$emit'); sandbox.stub($rootScope, '$emit');
var annotations = [{id: 1, url: 'http://example.com'}]; var annotations = [{id: 1, url: 'http://example.com'}];
var cached = {message: {id: 1, $$tag: 'tag1'}}; annotationUI.addAnnotations([{id: 1, $$tag: 'tag1'}]);
fakeThreading.idTable[1] = cached;
annotationMapper.loadAnnotations(annotations); annotationMapper.loadAnnotations(annotations);
assert.called($rootScope.$emit); assert.called($rootScope.$emit);
...@@ -120,8 +111,7 @@ describe('annotationMapper', function() { ...@@ -120,8 +111,7 @@ describe('annotationMapper', function() {
it('replaces the properties on the cached annotation with those from the deleted one', function () { it('replaces the properties on the cached annotation with those from the deleted one', function () {
sandbox.stub($rootScope, '$emit'); sandbox.stub($rootScope, '$emit');
var annotations = [{id: 1, url: 'http://example.com'}]; var annotations = [{id: 1, url: 'http://example.com'}];
var cached = {message: {id: 1, $$tag: 'tag1'}}; annotationUI.addAnnotations([{id: 1, $$tag: 'tag1'}]);
fakeThreading.idTable[1] = cached;
annotationMapper.unloadAnnotations(annotations); annotationMapper.unloadAnnotations(annotations);
assert.calledWith($rootScope.$emit, events.ANNOTATIONS_UNLOADED, [{ assert.calledWith($rootScope.$emit, events.ANNOTATIONS_UNLOADED, [{
......
...@@ -28,9 +28,10 @@ describe('AnnotationViewerController', function () { ...@@ -28,9 +28,10 @@ describe('AnnotationViewerController', function () {
$routeParams: opts.$routeParams || { id: 'test_annotation_id' }, $routeParams: opts.$routeParams || { id: 'test_annotation_id' },
$scope: opts.$scope || { $scope: opts.$scope || {
search: {}, search: {},
threading: { },
getContainer: function () {} annotationUI: {},
} rootThread: {
thread: sinon.stub(),
}, },
streamer: opts.streamer || { setConfig: function () {} }, streamer: opts.streamer || { setConfig: function () {} },
store: opts.store || { store: opts.store || {
...@@ -50,7 +51,6 @@ describe('AnnotationViewerController', function () { ...@@ -50,7 +51,6 @@ describe('AnnotationViewerController', function () {
getFilter: function () {} getFilter: function () {}
}, },
annotationMapper: opts.annotationMapper || { loadAnnotations: sinon.spy() }, annotationMapper: opts.annotationMapper || { loadAnnotations: sinon.spy() },
threading: opts.threading || { idTable: {} }
}; };
locals.ctrl = getControllerService()( locals.ctrl = getControllerService()(
'AnnotationViewerController', locals); 'AnnotationViewerController', locals);
......
...@@ -4,6 +4,7 @@ describe 'StreamController', -> ...@@ -4,6 +4,7 @@ describe 'StreamController', ->
$controller = null $controller = null
$scope = null $scope = null
fakeAnnotationMapper = null fakeAnnotationMapper = null
fakeAnnotationUI = null
fakeParams = null fakeParams = null
fakeQueryParser = null fakeQueryParser = null
fakeRoute = null fakeRoute = null
...@@ -30,6 +31,12 @@ describe 'StreamController', -> ...@@ -30,6 +31,12 @@ describe 'StreamController', ->
loadAnnotations: sandbox.spy() loadAnnotations: sandbox.spy()
} }
fakeAnnotationUI = {
clearAnnotations: sandbox.spy()
setCollapsed: sandbox.spy()
setForceVisible: sandbox.spy()
}
fakeParams = {id: 'test'} fakeParams = {id: 'test'}
fakeQueryParser = { fakeQueryParser = {
...@@ -63,19 +70,20 @@ describe 'StreamController', -> ...@@ -63,19 +70,20 @@ describe 'StreamController', ->
getFilter: sandbox.stub() getFilter: sandbox.stub()
} }
fakeThreading = { fakeRootThread = {
root: {} thread: sandbox.stub()
} }
$provide.value 'annotationMapper', fakeAnnotationMapper $provide.value 'annotationMapper', fakeAnnotationMapper
$provide.value 'annotationUI', fakeAnnotationUI
$provide.value '$route', fakeRoute $provide.value '$route', fakeRoute
$provide.value '$routeParams', fakeParams $provide.value '$routeParams', fakeParams
$provide.value 'queryParser', fakeQueryParser $provide.value 'queryParser', fakeQueryParser
$provide.value 'rootThread', fakeRootThread
$provide.value 'searchFilter', fakeSearchFilter $provide.value 'searchFilter', fakeSearchFilter
$provide.value 'store', fakeStore $provide.value 'store', fakeStore
$provide.value 'streamer', fakeStreamer $provide.value 'streamer', fakeStreamer
$provide.value 'streamFilter', fakeStreamFilter $provide.value 'streamFilter', fakeStreamFilter
$provide.value 'threading', fakeThreading
return return
beforeEach inject (_$controller_, $rootScope) -> beforeEach inject (_$controller_, $rootScope) ->
...@@ -106,17 +114,6 @@ describe 'StreamController', -> ...@@ -106,17 +114,6 @@ describe 'StreamController', ->
) )
describe 'on $routeChangeSuccess', ->
it 'disables page search', ->
createController()
$scope.threadFilter = {active: sinon.stub(), freeze: sinon.stub()}
$scope.$broadcast('$routeChangeSuccess')
assert.calledOnce($scope.threadFilter.active)
assert.calledWith($scope.threadFilter.active, false)
assert.calledOnce($scope.threadFilter.freeze)
assert.calledWith($scope.threadFilter.freeze, true)
describe 'on $routeUpdate', -> describe 'on $routeUpdate', ->
it 'reloads the route when the query changes', -> it 'reloads the route when the query changes', ->
...@@ -124,6 +121,7 @@ describe 'StreamController', -> ...@@ -124,6 +121,7 @@ describe 'StreamController', ->
createController() createController()
fakeParams.q = 'new query' fakeParams.q = 'new query'
$scope.$broadcast('$routeUpdate') $scope.$broadcast('$routeUpdate')
assert.called(fakeAnnotationUI.clearAnnotations)
assert.calledOnce(fakeRoute.reload) assert.calledOnce(fakeRoute.reload)
it 'does not reload the route when the query is the same', -> it 'does not reload the route when the query is the same', ->
......
...@@ -27,20 +27,20 @@ function FakeSearchClient(resource, opts) { ...@@ -27,20 +27,20 @@ function FakeSearchClient(resource, opts) {
inherits(FakeSearchClient, EventEmitter); inherits(FakeSearchClient, EventEmitter);
describe('WidgetController', function () { describe('WidgetController', function () {
var $scope = null; var $rootScope;
var $rootScope = null; var $scope;
var annotationUI = null; var annotationUI;
var fakeAnnotationMapper = null; var fakeAnnotationMapper;
var fakeCrossFrame = null; var fakeCrossFrame;
var fakeDrafts = null; var fakeDrafts;
var fakeStore = null; var fakeGroups;
var fakeStreamer = null; var fakeRootThread;
var fakeStreamFilter = null; var fakeSettings;
var fakeThreading = null; var fakeStore;
var fakeGroups = null; var fakeStreamer;
var sandbox = null; var fakeStreamFilter;
var viewer = null; var sandbox;
var fakeSettings = null; var viewer;
before(function () { before(function () {
angular.module('h', []) angular.module('h', [])
...@@ -64,12 +64,13 @@ describe('WidgetController', function () { ...@@ -64,12 +64,13 @@ describe('WidgetController', function () {
}; };
annotationUI = annotationUIFactory({}); annotationUI = annotationUIFactory({});
fakeCrossFrame = { fakeCrossFrame = {
call: sinon.stub(), call: sinon.stub(),
frames: [], frames: [],
}; };
fakeDrafts = { fakeDrafts = {
unsaved: sandbox.stub() unsaved: sandbox.stub().returns([]),
}; };
fakeStreamer = { fakeStreamer = {
...@@ -82,19 +83,19 @@ describe('WidgetController', function () { ...@@ -82,19 +83,19 @@ describe('WidgetController', function () {
getFilter: sandbox.stub().returns({}) getFilter: sandbox.stub().returns({})
}; };
fakeThreading = {
root: {},
thread: sandbox.stub(),
annotationList: function () {
return [{id: '123'}];
},
};
fakeGroups = { fakeGroups = {
focused: function () { return {id: 'foo'}; }, focused: function () { return {id: 'foo'}; },
focus: sinon.stub(), focus: sinon.stub(),
}; };
fakeRootThread = {
thread: sinon.stub().returns({
totalChildren: 0,
}),
setSearchQuery: sinon.stub(),
sortBy: sinon.stub(),
};
fakeSettings = { fakeSettings = {
annotations: 'test', annotations: 'test',
}; };
...@@ -107,10 +108,10 @@ describe('WidgetController', function () { ...@@ -107,10 +108,10 @@ describe('WidgetController', function () {
$provide.value('annotationUI', annotationUI); $provide.value('annotationUI', annotationUI);
$provide.value('crossframe', fakeCrossFrame); $provide.value('crossframe', fakeCrossFrame);
$provide.value('drafts', fakeDrafts); $provide.value('drafts', fakeDrafts);
$provide.value('rootThread', fakeRootThread);
$provide.value('store', fakeStore); $provide.value('store', fakeStore);
$provide.value('streamer', fakeStreamer); $provide.value('streamer', fakeStreamer);
$provide.value('streamFilter', fakeStreamFilter); $provide.value('streamFilter', fakeStreamFilter);
$provide.value('threading', fakeThreading);
$provide.value('groups', fakeGroups); $provide.value('groups', fakeGroups);
$provide.value('settings', fakeSettings); $provide.value('settings', fakeSettings);
})); }));
...@@ -129,13 +130,14 @@ describe('WidgetController', function () { ...@@ -129,13 +130,14 @@ describe('WidgetController', function () {
it('unloads any existing annotations', function () { it('unloads any existing annotations', function () {
// When new clients connect, all existing annotations should be unloaded // When new clients connect, all existing annotations should be unloaded
// before reloading annotations for each currently-connected client // before reloading annotations for each currently-connected client
annotationUI.addAnnotations([{id: '123'}]);
fakeCrossFrame.frames.push({uri: 'http://example.com/page-a'}); fakeCrossFrame.frames.push({uri: 'http://example.com/page-a'});
$scope.$digest(); $scope.$digest();
fakeAnnotationMapper.unloadAnnotations = sandbox.spy(); fakeAnnotationMapper.unloadAnnotations = sandbox.spy();
fakeCrossFrame.frames.push({uri: 'http://example.com/page-b'}); fakeCrossFrame.frames.push({uri: 'http://example.com/page-b'});
$scope.$digest(); $scope.$digest();
assert.calledWith(fakeAnnotationMapper.unloadAnnotations, assert.calledWith(fakeAnnotationMapper.unloadAnnotations,
fakeThreading.annotationList()); annotationUI.getState().annotations);
}); });
it('loads all annotations for a frame', function () { it('loads all annotations for a frame', function () {
...@@ -170,6 +172,10 @@ describe('WidgetController', function () { ...@@ -170,6 +172,10 @@ describe('WidgetController', function () {
$scope.$digest(); $scope.$digest();
}); });
it('selectedAnnotationCount is > 0', function () {
assert.equal($scope.selectedAnnotationCount(), 1);
});
it('switches to the selected annotation\'s group', function () { it('switches to the selected annotation\'s group', function () {
assert.calledWith(fakeGroups.focus, '__world__'); assert.calledWith(fakeGroups.focus, '__world__');
assert.calledOnce(fakeAnnotationMapper.loadAnnotations); assert.calledOnce(fakeAnnotationMapper.loadAnnotations);
...@@ -196,6 +202,10 @@ describe('WidgetController', function () { ...@@ -196,6 +202,10 @@ describe('WidgetController', function () {
$scope.$digest(); $scope.$digest();
}); });
it('selectedAnnotationCount is 0', function () {
assert.equal($scope.selectedAnnotationCount(), 0);
});
it('fetches annotations for the current group', function () { it('fetches annotations for the current group', function () {
assert.calledWith(searchClients[0].get, {uri: uri, group: 'a-group'}); assert.calledWith(searchClients[0].get, {uri: uri, group: 'a-group'});
}); });
...@@ -233,11 +243,7 @@ describe('WidgetController', function () { ...@@ -233,11 +243,7 @@ describe('WidgetController', function () {
$$tag: 'atag', $$tag: 'atag',
id: '123', id: '123',
}; };
fakeThreading.idTable = { annotationUI.addAnnotations([annot]);
'123': {
message: annot,
},
};
$scope.$digest(); $scope.$digest();
$rootScope.$broadcast(events.ANNOTATIONS_SYNCED, [{tag: 'atag'}]); $rootScope.$broadcast(events.ANNOTATIONS_SYNCED, [{tag: 'atag'}]);
assert.calledWith(fakeCrossFrame.call, 'focusAnnotations', ['atag']); assert.calledWith(fakeCrossFrame.call, 'focusAnnotations', ['atag']);
...@@ -248,12 +254,18 @@ describe('WidgetController', function () { ...@@ -248,12 +254,18 @@ describe('WidgetController', function () {
describe('when the focused group changes', function () { describe('when the focused group changes', function () {
it('should load annotations for the new group', function () { it('should load annotations for the new group', function () {
var uri = 'http://example.com'; var uri = 'http://example.com';
annotationUI.addAnnotations([{id: '123'}]);
annotationUI.addAnnotations = sinon.stub();
fakeDrafts.unsaved.returns([{id: uri + '123'}, {id: uri + '456'}]);
fakeCrossFrame.frames.push({uri: uri}); fakeCrossFrame.frames.push({uri: uri});
var loadSpy = fakeAnnotationMapper.loadAnnotations; var loadSpy = fakeAnnotationMapper.loadAnnotations;
$scope.$broadcast(events.GROUP_FOCUSED); $scope.$broadcast(events.GROUP_FOCUSED);
assert.calledWith(fakeAnnotationMapper.unloadAnnotations, [{id: '123'}]); assert.calledWith(fakeAnnotationMapper.unloadAnnotations, [{id: '123'}]);
assert.calledWith(fakeThreading.thread, fakeDrafts.unsaved()); assert.calledWith(annotationUI.addAnnotations, fakeDrafts.unsaved());
$scope.$digest(); $scope.$digest();
assert.calledWith(loadSpy, [sinon.match({id: uri + '123'})]); assert.calledWith(loadSpy, [sinon.match({id: uri + '123'})]);
assert.calledWith(loadSpy, [sinon.match({id: uri + '456'})]); assert.calledWith(loadSpy, [sinon.match({id: uri + '456'})]);
...@@ -291,14 +303,13 @@ describe('WidgetController', function () { ...@@ -291,14 +303,13 @@ describe('WidgetController', function () {
describe('direct linking messages', function () { describe('direct linking messages', function () {
it('displays a message if the selection is unavailable', function () { it('displays a message if the selection is unavailable', function () {
annotationUI.selectAnnotations(['missing']); annotationUI.selectAnnotations(['missing']);
fakeThreading.idTable = {'123': {}};
$scope.$digest(); $scope.$digest();
assert.isTrue($scope.selectedAnnotationUnavailable()); assert.isTrue($scope.selectedAnnotationUnavailable());
}); });
it('does not show a message if the selection is available', function () { it('does not show a message if the selection is available', function () {
annotationUI.addAnnotations([{id: '123'}]);
annotationUI.selectAnnotations(['123']); annotationUI.selectAnnotations(['123']);
fakeThreading.idTable = {'123': {}};
$scope.$digest(); $scope.$digest();
assert.isFalse($scope.selectedAnnotationUnavailable()); assert.isFalse($scope.selectedAnnotationUnavailable());
}); });
...@@ -313,8 +324,8 @@ describe('WidgetController', function () { ...@@ -313,8 +324,8 @@ describe('WidgetController', function () {
$scope.auth = { $scope.auth = {
status: 'signed-out' status: 'signed-out'
}; };
annotationUI.addAnnotations([{id: '123'}]);
annotationUI.selectAnnotations(['123']); annotationUI.selectAnnotations(['123']);
fakeThreading.idTable = {'123': {}};
$scope.$digest(); $scope.$digest();
assert.isTrue($scope.shouldShowLoggedOutMessage()); assert.isTrue($scope.shouldShowLoggedOutMessage());
}); });
...@@ -324,7 +335,6 @@ describe('WidgetController', function () { ...@@ -324,7 +335,6 @@ describe('WidgetController', function () {
status: 'signed-out' status: 'signed-out'
}; };
annotationUI.selectAnnotations(['missing']); annotationUI.selectAnnotations(['missing']);
fakeThreading.idTable = {'123': {}};
$scope.$digest(); $scope.$digest();
assert.isFalse($scope.shouldShowLoggedOutMessage()); assert.isFalse($scope.shouldShowLoggedOutMessage());
}); });
...@@ -342,8 +352,8 @@ describe('WidgetController', function () { ...@@ -342,8 +352,8 @@ describe('WidgetController', function () {
$scope.auth = { $scope.auth = {
status: 'signed-in' status: 'signed-in'
}; };
annotationUI.addAnnotations([{id: '123'}]);
annotationUI.selectAnnotations(['123']); annotationUI.selectAnnotations(['123']);
fakeThreading.idTable = {'123': {}};
$scope.$digest(); $scope.$digest();
assert.isFalse($scope.shouldShowLoggedOutMessage()); assert.isFalse($scope.shouldShowLoggedOutMessage());
}); });
...@@ -353,10 +363,28 @@ describe('WidgetController', function () { ...@@ -353,10 +363,28 @@ describe('WidgetController', function () {
status: 'signed-out' status: 'signed-out'
}; };
delete fakeSettings.annotations; delete fakeSettings.annotations;
annotationUI.addAnnotations([{id: '123'}]);
annotationUI.selectAnnotations(['123']); annotationUI.selectAnnotations(['123']);
fakeThreading.idTable = {'123': {}};
$scope.$digest(); $scope.$digest();
assert.isFalse($scope.shouldShowLoggedOutMessage()); assert.isFalse($scope.shouldShowLoggedOutMessage());
}); });
}); });
describe('#forceVisible', function () {
it('shows the thread', function () {
var thread = {id: '1'};
$scope.forceVisible(thread);
assert.deepEqual(annotationUI.getState().forceVisible, {1: true});
});
it('uncollapses the parent', function () {
var thread = {
id: '2',
parent: {id: '3'},
};
assert.equal(annotationUI.getState().expanded[thread.parent.id], undefined);
$scope.forceVisible(thread);
assert.equal(annotationUI.getState().expanded[thread.parent.id], true);
});
});
}); });
...@@ -31,11 +31,17 @@ function groupIDFromSelection(selection, results) { ...@@ -31,11 +31,17 @@ function groupIDFromSelection(selection, results) {
// @ngInject // @ngInject
module.exports = function WidgetController( module.exports = function WidgetController(
$scope, $rootScope, annotationUI, crossframe, annotationMapper, $scope, $rootScope, annotationUI, crossframe, annotationMapper,
drafts, groups, settings, streamer, streamFilter, store, threading drafts, groups, rootThread, settings, streamer, streamFilter, store
) { ) {
$scope.threadRoot = threading.root;
$scope.sortOptions = ['Newest', 'Oldest', 'Location']; $scope.sortOptions = ['Newest', 'Oldest', 'Location'];
function annotationExists(id) {
return annotationUI.getState().annotations.some(function (annot) {
return annot.id === id;
});
}
function focusAnnotation(annotation) { function focusAnnotation(annotation) {
var highlights = []; var highlights = [];
if (annotation) { if (annotation) {
...@@ -60,21 +66,23 @@ module.exports = function WidgetController( ...@@ -60,21 +66,23 @@ module.exports = function WidgetController(
function firstSelectedAnnotation() { function firstSelectedAnnotation() {
if (annotationUI.getState().selectedAnnotationMap) { if (annotationUI.getState().selectedAnnotationMap) {
var id = Object.keys(annotationUI.getState().selectedAnnotationMap)[0]; var id = Object.keys(annotationUI.getState().selectedAnnotationMap)[0];
return threading.idTable[id] && threading.idTable[id].message; return annotationUI.getState().annotations.find(function (annot) {
return annot.id === id;
});
} else { } else {
return null; return null;
} }
} }
var searchClients = [];
function _resetAnnotations() { function _resetAnnotations() {
// Unload all the annotations // Unload all the annotations
annotationMapper.unloadAnnotations(threading.annotationList()); annotationMapper.unloadAnnotations(annotationUI.getState().annotations);
// Reload all the drafts // Reload all the drafts
threading.thread(drafts.unsaved()); annotationUI.addAnnotations(drafts.unsaved());
} }
var searchClients = [];
function _loadAnnotationsFor(uri, group) { function _loadAnnotationsFor(uri, group) {
var searchClient = new SearchClient(store.SearchResource, { var searchClient = new SearchClient(store.SearchResource, {
// If no group is specified, we are fetching annotations from // If no group is specified, we are fetching annotations from
...@@ -196,6 +204,30 @@ module.exports = function WidgetController( ...@@ -196,6 +204,30 @@ module.exports = function WidgetController(
return crossframe.frames; return crossframe.frames;
}, loadAnnotations); }, loadAnnotations);
// Watch the inputs that determine which annotations are currently
// visible and how they are sorted and rebuild the thread when they change
$scope.$watch('sort.name', function (mode) {
rootThread.sortBy(mode);
});
$scope.$watch('search.query', function (query) {
rootThread.setSearchQuery(query);
});
$scope.rootThread = function () {
return rootThread.thread();
};
$scope.setCollapsed = function (id, collapsed) {
annotationUI.setCollapsed(id, collapsed);
};
$scope.forceVisible = function (thread) {
annotationUI.setForceVisible(thread.id, true);
if (thread.parent) {
annotationUI.setCollapsed(thread.parent.id, false);
}
};
$scope.focus = focusAnnotation; $scope.focus = focusAnnotation;
$scope.scrollTo = scrollToAnnotation; $scope.scrollTo = scrollToAnnotation;
...@@ -206,14 +238,19 @@ module.exports = function WidgetController( ...@@ -206,14 +238,19 @@ module.exports = function WidgetController(
return annotation.$$tag in annotationUI.getState().focusedAnnotationMap; return annotation.$$tag in annotationUI.getState().focusedAnnotationMap;
}; };
function selectedID() { $scope.selectedAnnotationCount = function () {
return firstKey(annotationUI.getState().selectedAnnotationMap); var selection = annotationUI.getState().selectedAnnotationMap;
if (!selection) {
return 0;
} }
return Object.keys(selection).length;
};
$scope.selectedAnnotationUnavailable = function () { $scope.selectedAnnotationUnavailable = function () {
var selectedID = firstKey(annotationUI.getState().selectedAnnotationMap);
return !isLoading() && return !isLoading() &&
!!selectedID() && !!selectedID &&
!threading.idTable[selectedID()]; !annotationExists(selectedID);
}; };
$scope.shouldShowLoggedOutMessage = function () { $scope.shouldShowLoggedOutMessage = function () {
...@@ -231,21 +268,22 @@ module.exports = function WidgetController( ...@@ -231,21 +268,22 @@ module.exports = function WidgetController(
// The user is logged out and has landed on a direct linked // The user is logged out and has landed on a direct linked
// annotation. If there is an annotation selection and that // annotation. If there is an annotation selection and that
// selection is available to the user, show the CTA. // selection is available to the user, show the CTA.
var selectedID = firstKey(annotationUI.getState().selectedAnnotationMap);
return !isLoading() && return !isLoading() &&
!!selectedID() && !!selectedID &&
!!threading.idTable[selectedID()]; annotationExists(selectedID);
}; };
$scope.isLoading = isLoading; $scope.isLoading = isLoading;
$scope.topLevelThreadCount = function () { $scope.topLevelThreadCount = function () {
return threading.root.children.length; return rootThread.thread().totalChildren;
}; };
$rootScope.$on(events.BEFORE_ANNOTATION_CREATED, function (event, data) { $rootScope.$on(events.BEFORE_ANNOTATION_CREATED, function (event, data) {
if (data.$highlight || (data.references && data.references.length > 0)) { if (data.$highlight || (data.references && data.references.length > 0)) {
return; return;
} }
return $scope.clearSelection(); $scope.clearSelection();
}); });
}; };
...@@ -29,11 +29,11 @@ ...@@ -29,11 +29,11 @@
<i class="h-icon-border-color" ng-show="vm.isHighlight() && !vm.editing()" title="This is a highlight. Click 'edit' to add a note or tag."></i> <i class="h-icon-border-color" ng-show="vm.isHighlight() && !vm.editing()" title="This is a highlight. Click 'edit' to add a note or tag."></i>
<span class="annotation-citation" <span class="annotation-citation"
ng-bind-html="vm.documentTitle" ng-bind-html="vm.documentTitle"
ng-if="::!vm.isSidebar"> ng-if="::vm.showDocumentInfo">
</span> </span>
<span class="annotation-citation-domain" <span class="annotation-citation-domain"
ng-bind-html="vm.documentDomain" ng-bind-html="vm.documentDomain"
ng-if="::!vm.isSidebar"> ng-if="::vm.showDocumentInfo">
</span> </span>
</span> </span>
</span> </span>
......
<a href=""
class="threadexp"
title="{{vm.thread.collapsed && 'Expand' || 'Collapse'}}"
ng-click="vm.toggleCollapsed()"
ng-if="vm.thread.parent">
<span ng-class="{'h-icon-arrow-right': vm.thread.collapsed,
'h-icon-arrow-drop-down': !vm.thread.collapsed}"></span>
</a>
<!-- Annotation -->
<annotation class="annotation thread-message {{vm.thread.collapsed && 'collapsed'}}"
annotation="vm.thread.annotation"
is-collapsed="vm.thread.collapsed"
is-last-reply="$last"
is-sidebar="::vm.isSidebar"
name="annotation"
ng-if="vm.thread.annotation"
ng-show="vm.thread.visible"
show-document-info="vm.showDocumentInfo"
on-reply-count-click="vm.toggleCollapsed()"
reply-count="vm.thread.replyCount">
</annotation>
<div ng-if="!vm.thread.annotation" class="thread-deleted">
<p><em>Message not available.</em></p>
</div>
<div class="thread-load-more" ng-if="vm.hiddenCount() > 0">
<a class="load-more small"
href=""
ng-click="vm.showThreadAndReplies()"
ng-pluralize
count="vm.hiddenCount()"
when="{'0': '',
one: 'View one more in conversation',
other: 'View {} more in conversation'}"
></a>
</div>
<!-- Replies -->
<ul class="thread-replies" ng-show="!vm.thread.collapsed">
<li class="thread" ng-repeat="child in vm.thread.children track by child.id">
<annotation-thread
show-document-info="false"
thread="child"
on-change-collapsed="vm.onChangeCollapsed({id:id, collapsed:collapsed})"
on-force-visible="vm.onForceVisible({thread:thread})">
</annotation-thread>
</li>
</ul>
...@@ -4,16 +4,15 @@ ...@@ -4,16 +4,15 @@
--> -->
<ul class="stream-list ng-hide" <ul class="stream-list ng-hide"
ng-show="true" ng-show="true"
deep-count="count"
thread-filter="search.query"
window-scroll="loadMore(20)"> window-scroll="loadMore(20)">
<search-status-bar <search-status-bar
ng-show="!isLoading()" ng-show="!isLoading()"
filter-active="threadFilter.active()" ng-if="!isStream"
filter-match-count="count('match')" filter-active="search.query"
filter-match-count="rootThread().children.length"
on-clear-selection="clearSelection()" on-clear-selection="clearSelection()"
search-query="search ? search.query : ''" search-query="search ? search.query : ''"
selection-count="selectedAnnotationsCount" selection-count="selectedAnnotationCount()"
total-count="topLevelThreadCount()" total-count="topLevelThreadCount()"
> >
</search-status-bar> </search-status-bar>
...@@ -33,15 +32,17 @@ ...@@ -33,15 +32,17 @@
</li> </li>
<li id="{{vm.id}}" <li id="{{vm.id}}"
class="annotation-card thread" class="annotation-card thread"
ng-class="{'js-hover': hasFocus(child.message)}" ng-class="{'js-hover': hasFocus(child.annotation)}"
deep-count="count" ng-mouseenter="focus(child.annotation)"
thread="child" thread-filter ng-click="scrollTo(child.annotation)"
ng-include="'thread.html'"
ng-mouseenter="focus(child.message)"
ng-click="scrollTo(child.message)"
ng-mouseleave="focus()" ng-mouseleave="focus()"
ng-repeat="child in threadRoot.children | orderBy : sort.predicate" ng-repeat="child in rootThread().children track by child.id">
ng-show="vm.shouldShow()"> <annotation-thread
thread="child"
show-document-info="::!isSidebar"
on-change-collapsed="setCollapsed(id, collapsed)"
on-force-visible="forceVisible(thread)">
</annotation-thread>
</li> </li>
<loggedout-message ng-if="isSidebar && shouldShowLoggedOutMessage()" <loggedout-message ng-if="isSidebar && shouldShowLoggedOutMessage()"
on-login="login()" ng-cloak> on-login="login()" ng-cloak>
......
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