Commit 5df4a9cd authored by Robert Knight's avatar Robert Knight

Move the list of connected frames to the Redux store

Move metadata about the frames that are connected to the sidebar app,
such as the document's URL and fingerprint, to the central app state
store.

This is part of an effort to unify how important application state is
managed.
parent 6460fbc1
...@@ -38,6 +38,7 @@ var thunk = require('redux-thunk').default; ...@@ -38,6 +38,7 @@ var thunk = require('redux-thunk').default;
var reducers = require('./reducers'); var reducers = require('./reducers');
var annotationsReducer = require('./reducers/annotations'); var annotationsReducer = require('./reducers/annotations');
var framesReducer = require('./reducers/frames');
var selectionReducer = require('./reducers/selection'); var selectionReducer = require('./reducers/selection');
var sessionReducer = require('./reducers/session'); var sessionReducer = require('./reducers/session');
var viewerReducer = require('./reducers/viewer'); var viewerReducer = require('./reducers/viewer');
...@@ -93,6 +94,7 @@ module.exports = function ($rootScope, settings) { ...@@ -93,6 +94,7 @@ module.exports = function ($rootScope, settings) {
// //
var actionCreators = redux.bindActionCreators(Object.assign({}, var actionCreators = redux.bindActionCreators(Object.assign({},
annotationsReducer.actions, annotationsReducer.actions,
framesReducer.actions,
selectionReducer.actions, selectionReducer.actions,
sessionReducer.actions, sessionReducer.actions,
viewerReducer.actions viewerReducer.actions
...@@ -113,6 +115,8 @@ module.exports = function ($rootScope, settings) { ...@@ -113,6 +115,8 @@ module.exports = function ($rootScope, settings) {
findIDsForTags: annotationsReducer.findIDsForTags, findIDsForTags: annotationsReducer.findIDsForTags,
savedAnnotations: annotationsReducer.savedAnnotations, savedAnnotations: annotationsReducer.savedAnnotations,
frames: framesReducer.frames,
isSidebar: viewerReducer.isSidebar, isSidebar: viewerReducer.isSidebar,
}, store.getState); }, store.getState);
......
...@@ -11,15 +11,15 @@ module.exports = function () { ...@@ -11,15 +11,15 @@ module.exports = function () {
bindToController: true, bindToController: true,
controllerAs: 'vm', controllerAs: 'vm',
// @ngInject // @ngInject
controller: function ($scope, $window, frameSync, serviceUrl) { controller: function ($scope, $window, annotationUI, serviceUrl) {
this.userAgent = $window.navigator.userAgent; this.userAgent = $window.navigator.userAgent;
this.version = '__VERSION__'; // replaced by versionify this.version = '__VERSION__'; // replaced by versionify
this.dateTime = new Date(); this.dateTime = new Date();
this.serviceUrl = serviceUrl; this.serviceUrl = serviceUrl;
$scope.$watchCollection( $scope.$watch(
function () { function () {
return frameSync.frames; return annotationUI.frames();
}, },
function (frames) { function (frames) {
if (frames.length === 0) { if (frames.length === 0) {
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
var VIA_PREFIX = 'https://via.hypothes.is/'; var VIA_PREFIX = 'https://via.hypothes.is/';
// @ngInject // @ngInject
function ShareDialogController($scope, $element, frameSync) { function ShareDialogController($scope, $element, annotationUI) {
var self = this; var self = this;
function updateViaLink(frames) { function updateViaLink(frames) {
...@@ -24,7 +24,7 @@ function ShareDialogController($scope, $element, frameSync) { ...@@ -24,7 +24,7 @@ function ShareDialogController($scope, $element, frameSync) {
viaInput.focus(); viaInput.focus();
viaInput.select(); viaInput.select();
$scope.$watchCollection(function () { return frameSync.frames; }, $scope.$watch(function () { return annotationUI.frames(); },
updateViaLink); updateViaLink);
} }
......
...@@ -5,28 +5,30 @@ var angular = require('angular'); ...@@ -5,28 +5,30 @@ var angular = require('angular');
var util = require('./util'); var util = require('./util');
describe('shareDialog', function () { describe('shareDialog', function () {
var fakeFrameSync; var fakeAnnotationUI;
beforeEach(function () { beforeEach(function () {
fakeFrameSync = { frames: [] }; fakeAnnotationUI = { frames: sinon.stub().returns([]) };
angular.module('h', []) angular.module('h', [])
.directive('shareDialog', require('../share-dialog')) .directive('shareDialog', require('../share-dialog'))
.value('frameSync', fakeFrameSync) .value('annotationUI', fakeAnnotationUI)
.value('urlEncodeFilter', function (val) { return val; }); .value('urlEncodeFilter', function (val) { return val; });
angular.mock.module('h'); angular.mock.module('h');
}); });
it('generates new via link', function () { it('generates new via link', function () {
var element = util.createDirective(document, 'shareDialog', {}); var element = util.createDirective(document, 'shareDialog', {});
fakeFrameSync.frames.push({ uri: 'http://example.com' }); fakeAnnotationUI.frames.returns([{ uri: 'http://example.com' }]);
element.scope.$digest(); element.scope.$digest();
assert.equal(element.ctrl.viaPageLink, 'https://via.hypothes.is/http://example.com'); assert.equal(element.ctrl.viaPageLink, 'https://via.hypothes.is/http://example.com');
}); });
it('does not generate new via link if already on via', function () { it('does not generate new via link if already on via', function () {
var element = util.createDirective(document, 'shareDialog', {}); var element = util.createDirective(document, 'shareDialog', {});
fakeFrameSync.frames.push({ uri: 'https://via.hypothes.is/http://example.com' }); fakeAnnotationUI.frames.returns([{
uri: 'https://via.hypothes.is/http://example.com',
}]);
element.scope.$digest(); element.scope.$digest();
assert.equal(element.ctrl.viaPageLink, 'https://via.hypothes.is/http://example.com'); assert.equal(element.ctrl.viaPageLink, 'https://via.hypothes.is/http://example.com');
}); });
......
...@@ -40,9 +40,6 @@ function formatAnnot(ann) { ...@@ -40,9 +40,6 @@ function formatAnnot(ann) {
// @ngInject // @ngInject
function FrameSync($rootScope, $window, Discovery, annotationUI, bridge) { function FrameSync($rootScope, $window, Discovery, annotationUI, bridge) {
// List of frames currently connected to the sidebar
var frames = [];
// Set of tags of annotations that are currently loaded into the frame // Set of tags of annotations that are currently loaded into the frame
var inFrame = new Set(); var inFrame = new Set();
...@@ -157,14 +154,10 @@ function FrameSync($rootScope, $window, Discovery, annotationUI, bridge) { ...@@ -157,14 +154,10 @@ function FrameSync($rootScope, $window, Discovery, annotationUI, bridge) {
}); });
} }
// The `frames` list is currently stored by this service but should in annotationUI.connectFrame({
// future be moved to the app state. uri: info.uri,
$rootScope.$apply(function () { searchUris: searchUris,
frames.push({ documentFingerprint: documentFingerprint,
uri: info.uri,
searchUris: searchUris,
documentFingerprint: documentFingerprint,
});
}); });
}); });
} }
...@@ -201,12 +194,6 @@ function FrameSync($rootScope, $window, Discovery, annotationUI, bridge) { ...@@ -201,12 +194,6 @@ function FrameSync($rootScope, $window, Discovery, annotationUI, bridge) {
this.scrollToAnnotation = function (tag) { this.scrollToAnnotation = function (tag) {
bridge.call('scrollToAnnotation', tag); bridge.call('scrollToAnnotation', tag);
}; };
/**
* List of frames that are connected to the app.
* @type {FrameInfo}
*/
this.frames = frames;
} }
module.exports = { module.exports = {
......
'use strict';
var util = require('./util');
function init() {
return {
// The list of frames connected to the sidebar app
frames: [],
};
}
var update = {
CONNECT_FRAME: function (state, action) {
return {frames: state.frames.concat(action.frame)};
},
};
var actions = util.actionTypes(update);
/**
* Add a frame to the list of frames currently connected to the sidebar app.
*/
function connectFrame(frame) {
return {type: actions.CONNECT_FRAME, frame: frame};
}
/**
* Return the list of frames currently connected to the sidebar app.
*/
function frames(state) {
return state.frames;
}
module.exports = {
init: init,
update: update,
actions: {
connectFrame: connectFrame,
},
// Selectors
frames: frames,
};
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
*/ */
var annotations = require('./annotations'); var annotations = require('./annotations');
var frames = require('./frames');
var selection = require('./selection'); var selection = require('./selection');
var session = require('./session'); var session = require('./session');
var viewer = require('./viewer'); var viewer = require('./viewer');
...@@ -27,6 +28,7 @@ function init(settings) { ...@@ -27,6 +28,7 @@ function init(settings) {
return Object.assign( return Object.assign(
{}, {},
annotations.init(), annotations.init(),
frames.init(),
selection.init(settings), selection.init(settings),
session.init(), session.init(),
viewer.init() viewer.init()
...@@ -35,6 +37,7 @@ function init(settings) { ...@@ -35,6 +37,7 @@ function init(settings) {
var update = util.createReducer(Object.assign( var update = util.createReducer(Object.assign(
annotations.update, annotations.update,
frames.update,
selection.update, selection.update,
session.update, session.update,
viewer.update viewer.update
......
'use strict';
var frames = require('../frames');
var util = require('../util');
var init = frames.init;
var actions = frames.actions;
var update = util.createReducer(frames.update);
describe('frames reducer', function () {
describe('#connectFrame', function () {
it('adds the frame to the list of connected frames', function () {
var frame = {uri: 'http://example.com'};
var state = update(init(), actions.connectFrame(frame));
assert.deepEqual(frames.frames(state), [frame]);
});
});
});
...@@ -54,6 +54,7 @@ describe('FrameSync', function () { ...@@ -54,6 +54,7 @@ describe('FrameSync', function () {
beforeEach(function () { beforeEach(function () {
fakeAnnotationUI = fakeStore({annotations: []}, { fakeAnnotationUI = fakeStore({annotations: []}, {
connectFrame: sinon.stub(),
findIDsForTags: sinon.stub(), findIDsForTags: sinon.stub(),
focusAnnotations: sinon.stub(), focusAnnotations: sinon.stub(),
selectAnnotations: sinon.stub(), selectAnnotations: sinon.stub(),
...@@ -174,11 +175,11 @@ describe('FrameSync', function () { ...@@ -174,11 +175,11 @@ describe('FrameSync', function () {
fakeBridge.emit('connect', fakeChannel); fakeBridge.emit('connect', fakeChannel);
assert.deepEqual(frameSync.frames, [{ assert.calledWith(fakeAnnotationUI.connectFrame, {
documentFingerprint: undefined, documentFingerprint: undefined,
searchUris: [frameInfo.uri], searchUris: [frameInfo.uri],
uri: frameInfo.uri, uri: frameInfo.uri,
}]); });
}); });
it('adds the document fingerprint for PDFs', function () { it('adds the document fingerprint for PDFs', function () {
...@@ -186,11 +187,11 @@ describe('FrameSync', function () { ...@@ -186,11 +187,11 @@ describe('FrameSync', function () {
fakeBridge.emit('connect', fakeChannel); fakeBridge.emit('connect', fakeChannel);
assert.deepEqual(frameSync.frames, [{ assert.calledWith(fakeAnnotationUI.connectFrame, {
documentFingerprint: frameInfo.metadata.documentFingerprint, documentFingerprint: frameInfo.metadata.documentFingerprint,
searchUris: [frameInfo.uri, 'urn:1234'], searchUris: [frameInfo.uri, 'urn:1234'],
uri: frameInfo.uri, uri: frameInfo.uri,
}]); });
}); });
}); });
......
...@@ -77,7 +77,6 @@ describe('WidgetController', function () { ...@@ -77,7 +77,6 @@ describe('WidgetController', function () {
fakeFrameSync = { fakeFrameSync = {
focusAnnotations: sinon.stub(), focusAnnotations: sinon.stub(),
scrollToAnnotation: sinon.stub(), scrollToAnnotation: sinon.stub(),
frames: [],
}; };
fakeDrafts = { fakeDrafts = {
...@@ -125,12 +124,16 @@ describe('WidgetController', function () { ...@@ -125,12 +124,16 @@ describe('WidgetController', function () {
$provide.value('settings', fakeSettings); $provide.value('settings', fakeSettings);
})); }));
function setFrames(frames) {
annotationUI.frames.returns(frames);
}
beforeEach(angular.mock.inject(function ($controller, _annotationUI_, _$rootScope_) { beforeEach(angular.mock.inject(function ($controller, _annotationUI_, _$rootScope_) {
$rootScope = _$rootScope_; $rootScope = _$rootScope_;
$scope = $rootScope.$new(); $scope = $rootScope.$new();
$scope.auth = {'status': 'unknown'}; $scope.auth = {'status': 'unknown'};
annotationUI = _annotationUI_; annotationUI = _annotationUI_;
annotationUI.frames = sinon.stub().returns([]);
$controller('WidgetController', {$scope: $scope}); $controller('WidgetController', {$scope: $scope});
})); }));
...@@ -144,11 +147,13 @@ describe('WidgetController', function () { ...@@ -144,11 +147,13 @@ describe('WidgetController', function () {
// before reloading annotations for each currently-connected client // before reloading annotations for each currently-connected client
annotationUI.addAnnotations([{id: '123'}]); annotationUI.addAnnotations([{id: '123'}]);
var uri1 = 'http://example.com/page-a'; var uri1 = 'http://example.com/page-a';
fakeFrameSync.frames.push({uri: uri1, searchUris: [uri1]}); var frames = [{uri: uri1, searchUris: [uri1]}];
setFrames(frames);
$scope.$digest(); $scope.$digest();
fakeAnnotationMapper.unloadAnnotations = sandbox.spy(); fakeAnnotationMapper.unloadAnnotations = sandbox.spy();
var uri2 = 'http://example.com/page-b'; var uri2 = 'http://example.com/page-b';
fakeFrameSync.frames.push({uri: uri2, searchUris: [uri2]}); frames = frames.concat({uri: uri2, searchUris: [uri2]});
setFrames(frames);
$scope.$digest(); $scope.$digest();
assert.calledWith(fakeAnnotationMapper.unloadAnnotations, assert.calledWith(fakeAnnotationMapper.unloadAnnotations,
annotationUI.getState().annotations); annotationUI.getState().annotations);
...@@ -156,7 +161,7 @@ describe('WidgetController', function () { ...@@ -156,7 +161,7 @@ describe('WidgetController', function () {
it('loads all annotations for a frame', function () { it('loads all annotations for a frame', function () {
var uri = 'http://example.com'; var uri = 'http://example.com';
fakeFrameSync.frames.push({uri: uri, searchUris: [uri]}); setFrames([{uri: uri, searchUris: [uri]}]);
$scope.$digest(); $scope.$digest();
var loadSpy = fakeAnnotationMapper.loadAnnotations; var loadSpy = fakeAnnotationMapper.loadAnnotations;
assert.calledWith(loadSpy, [sinon.match({id: uri + '123'})]); assert.calledWith(loadSpy, [sinon.match({id: uri + '123'})]);
...@@ -166,7 +171,7 @@ describe('WidgetController', function () { ...@@ -166,7 +171,7 @@ describe('WidgetController', function () {
it('loads all annotations for a frame with multiple urls', function () { it('loads all annotations for a frame with multiple urls', function () {
var uri = 'http://example.com/test.pdf'; var uri = 'http://example.com/test.pdf';
var fingerprint = 'urn:x-pdf:fingerprint'; var fingerprint = 'urn:x-pdf:fingerprint';
fakeFrameSync.frames.push({uri: uri, searchUris: [uri, fingerprint]}); setFrames([{uri: uri, searchUris: [uri, fingerprint]}]);
$scope.$digest(); $scope.$digest();
var loadSpy = fakeAnnotationMapper.loadAnnotations; var loadSpy = fakeAnnotationMapper.loadAnnotations;
assert.calledWith(loadSpy, [sinon.match({id: uri + '123'})]); assert.calledWith(loadSpy, [sinon.match({id: uri + '123'})]);
...@@ -177,9 +182,9 @@ describe('WidgetController', function () { ...@@ -177,9 +182,9 @@ describe('WidgetController', function () {
it('loads all annotations for all frames', function () { it('loads all annotations for all frames', function () {
var uris = ['http://example.com', 'http://foobar.com']; var uris = ['http://example.com', 'http://foobar.com'];
fakeFrameSync.frames = uris.map(function (uri) { setFrames(uris.map(function (uri) {
return {uri: uri, searchUris: [uri]}; return {uri: uri, searchUris: [uri]};
}); }));
$scope.$digest(); $scope.$digest();
var loadSpy = fakeAnnotationMapper.loadAnnotations; var loadSpy = fakeAnnotationMapper.loadAnnotations;
assert.calledWith(loadSpy, [sinon.match({id: uris[0] + '123'})]); assert.calledWith(loadSpy, [sinon.match({id: uris[0] + '123'})]);
...@@ -193,7 +198,7 @@ describe('WidgetController', function () { ...@@ -193,7 +198,7 @@ describe('WidgetController', function () {
var id = uri + '123'; var id = uri + '123';
beforeEach(function () { beforeEach(function () {
fakeFrameSync.frames = [{uri: uri, searchUris: [uri]}]; setFrames([{uri: uri, searchUris: [uri]}]);
annotationUI.selectAnnotations([id]); annotationUI.selectAnnotations([id]);
$scope.$digest(); $scope.$digest();
}); });
...@@ -223,7 +228,7 @@ describe('WidgetController', function () { ...@@ -223,7 +228,7 @@ describe('WidgetController', function () {
var uri = 'http://example.com'; var uri = 'http://example.com';
beforeEach(function () { beforeEach(function () {
fakeFrameSync.frames = [{uri: uri, searchUris: [uri]}]; setFrames([{uri: uri, searchUris: [uri]}]);
fakeGroups.focused = function () { return { id: 'a-group' }; }; fakeGroups.focused = function () { return { id: 'a-group' }; };
$scope.$digest(); $scope.$digest();
}); });
...@@ -246,7 +251,7 @@ describe('WidgetController', function () { ...@@ -246,7 +251,7 @@ describe('WidgetController', function () {
var id = uri + 'does-not-exist'; var id = uri + 'does-not-exist';
beforeEach(function () { beforeEach(function () {
fakeFrameSync.frames = [{uri: uri, searchUris: [uri]}]; setFrames([{uri: uri, searchUris: [uri]}]);
annotationUI.selectAnnotations([id]); annotationUI.selectAnnotations([id]);
fakeGroups.focused = function () { return { id: 'private-group' }; }; fakeGroups.focused = function () { return { id: 'private-group' }; };
$scope.$digest(); $scope.$digest();
...@@ -264,7 +269,7 @@ describe('WidgetController', function () { ...@@ -264,7 +269,7 @@ describe('WidgetController', function () {
it('focuses and scrolls to the annotation if already selected', function () { it('focuses and scrolls to the annotation if already selected', function () {
var uri = 'http://example.com'; var uri = 'http://example.com';
annotationUI.selectAnnotations(['123']); annotationUI.selectAnnotations(['123']);
fakeFrameSync.frames.push({uri: uri, searchUris: [uri]}); setFrames([{uri: uri, searchUris: [uri]}]);
var annot = { var annot = {
$tag: 'atag', $tag: 'atag',
id: '123', id: '123',
...@@ -283,7 +288,7 @@ describe('WidgetController', function () { ...@@ -283,7 +288,7 @@ describe('WidgetController', function () {
annotationUI.addAnnotations([{id: '123'}]); annotationUI.addAnnotations([{id: '123'}]);
annotationUI.addAnnotations = sinon.stub(); annotationUI.addAnnotations = sinon.stub();
fakeDrafts.unsaved.returns([{id: uri + '123'}, {id: uri + '456'}]); fakeDrafts.unsaved.returns([{id: uri + '123'}, {id: uri + '456'}]);
fakeFrameSync.frames.push({uri: uri, searchUris: [uri]}); setFrames([{uri: uri, searchUris: [uri]}]);
var loadSpy = fakeAnnotationMapper.loadAnnotations; var loadSpy = fakeAnnotationMapper.loadAnnotations;
$scope.$broadcast(events.GROUP_FOCUSED); $scope.$broadcast(events.GROUP_FOCUSED);
...@@ -300,12 +305,12 @@ describe('WidgetController', function () { ...@@ -300,12 +305,12 @@ describe('WidgetController', function () {
beforeEach(function () { beforeEach(function () {
// The document has finished loading. // The document has finished loading.
fakeFrameSync.frames = [ setFrames([
{ {
uri: 'http://www.example.com', uri: 'http://www.example.com',
searchUris: [], searchUris: [],
}, },
]; ]);
// There is a direct-linked annotation // There is a direct-linked annotation
fakeSettings.annotations = 'test'; fakeSettings.annotations = 'test';
...@@ -336,7 +341,7 @@ describe('WidgetController', function () { ...@@ -336,7 +341,7 @@ describe('WidgetController', function () {
// There is a selection but the selected annotation isn't available. // There is a selection but the selected annotation isn't available.
annotationUI.selectAnnotations(['missing']); annotationUI.selectAnnotations(['missing']);
// The document hasn't finished loading. // The document hasn't finished loading.
fakeFrameSync.frames = []; setFrames([]);
$scope.$digest(); $scope.$digest();
assert.isFalse($scope.selectedAnnotationUnavailable()); assert.isFalse($scope.selectedAnnotationUnavailable());
......
...@@ -160,7 +160,7 @@ module.exports = function WidgetController( ...@@ -160,7 +160,7 @@ module.exports = function WidgetController(
} }
function isLoading() { function isLoading() {
if (!frameSync.frames.some(function (frame) { return frame.uri; })) { if (!annotationUI.frames().some(function (frame) { return frame.uri; })) {
// The document's URL isn't known so the document must still be loading. // The document's URL isn't known so the document must still be loading.
return true; return true;
} }
...@@ -262,12 +262,12 @@ module.exports = function WidgetController( ...@@ -262,12 +262,12 @@ module.exports = function WidgetController(
return; return;
} }
annotationUI.clearSelectedAnnotations(); annotationUI.clearSelectedAnnotations();
loadAnnotations(frameSync.frames); loadAnnotations(annotationUI.frames());
}); });
// Watch anything that may require us to reload annotations. // Watch anything that may require us to reload annotations.
$scope.$watchCollection(function () { $scope.$watch(function () {
return frameSync.frames; return annotationUI.frames();
}, loadAnnotations); }, loadAnnotations);
$scope.setCollapsed = function (id, collapsed) { $scope.setCollapsed = function (id, collapsed) {
......
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