Commit c4187a17 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner

Migrate `SidebarContent` component to preact

Make `streamer` auto-reconnect on user change to support this migration
parent f6479483
This diff is collapsed.
...@@ -108,14 +108,10 @@ registerIcons(iconSet); ...@@ -108,14 +108,10 @@ registerIcons(iconSet);
// Preact UI components that are wrapped for use within Angular templates. // Preact UI components that are wrapped for use within Angular templates.
import AnnotationViewerContent from './components/annotation-viewer-content'; import AnnotationViewerContent from './components/annotation-viewer-content';
import FocusedModeHeader from './components/focused-mode-header';
import HelpPanel from './components/help-panel'; import HelpPanel from './components/help-panel';
import LoggedOutMessage from './components/logged-out-message';
import LoginPromptPanel from './components/login-prompt-panel'; import LoginPromptPanel from './components/login-prompt-panel';
import SearchStatusBar from './components/search-status-bar';
import SelectionTabs from './components/selection-tabs';
import ShareAnnotationsPanel from './components/share-annotations-panel'; import ShareAnnotationsPanel from './components/share-annotations-panel';
import SidebarContentError from './components/sidebar-content-error'; import SidebarContent from './components/sidebar-content';
import StreamContent from './components/stream-content'; import StreamContent from './components/stream-content';
import ThreadList from './components/thread-list'; import ThreadList from './components/thread-list';
import ToastMessages from './components/toast-messages'; import ToastMessages from './components/toast-messages';
...@@ -124,7 +120,6 @@ import TopBar from './components/top-bar'; ...@@ -124,7 +120,6 @@ import TopBar from './components/top-bar';
// Remaining UI components that are still built with Angular. // Remaining UI components that are still built with Angular.
import hypothesisApp from './components/hypothesis-app'; import hypothesisApp from './components/hypothesis-app';
import sidebarContent from './components/sidebar-content';
// Services. // Services.
...@@ -243,12 +238,7 @@ function startAngularApp(config) { ...@@ -243,12 +238,7 @@ function startAngularApp(config) {
) )
.component('helpPanel', wrapComponent(HelpPanel)) .component('helpPanel', wrapComponent(HelpPanel))
.component('loginPromptPanel', wrapComponent(LoginPromptPanel)) .component('loginPromptPanel', wrapComponent(LoginPromptPanel))
.component('loggedOutMessage', wrapComponent(LoggedOutMessage)) .component('sidebarContent', wrapComponent(SidebarContent))
.component('searchStatusBar', wrapComponent(SearchStatusBar))
.component('focusedModeHeader', wrapComponent(FocusedModeHeader))
.component('selectionTabs', wrapComponent(SelectionTabs))
.component('sidebarContent', sidebarContent)
.component('sidebarContentError', wrapComponent(SidebarContentError))
.component('shareAnnotationsPanel', wrapComponent(ShareAnnotationsPanel)) .component('shareAnnotationsPanel', wrapComponent(ShareAnnotationsPanel))
.component('streamContent', wrapComponent(StreamContent)) .component('streamContent', wrapComponent(StreamContent))
.component('threadList', wrapComponent(ThreadList)) .component('threadList', wrapComponent(ThreadList))
......
...@@ -3,6 +3,7 @@ import * as queryString from 'query-string'; ...@@ -3,6 +3,7 @@ import * as queryString from 'query-string';
import warnOnce from '../../shared/warn-once'; import warnOnce from '../../shared/warn-once';
import { generateHexString } from '../util/random'; import { generateHexString } from '../util/random';
import Socket from '../websocket'; import Socket from '../websocket';
import { watch } from '../util/watch';
/** /**
* Open a new WebSocket connection to the Hypothesis push notification service. * Open a new WebSocket connection to the Hypothesis push notification service.
...@@ -160,6 +161,26 @@ export default function Streamer(store, auth, groups, session, settings) { ...@@ -160,6 +161,26 @@ export default function Streamer(store, auth, groups, session, settings) {
}); });
}; };
let reconnectSetUp = false;
/**
* Set up automatic reconnecting when user changes.
*/
function setUpAutoReconnect() {
if (reconnectSetUp) {
return;
}
reconnectSetUp = true;
// Reconnect when user changes, as auth token will have changed
watch(
store.subscribe,
() => store.profile().userid,
() => {
reconnect();
}
);
}
/** /**
* Connect to the Hypothesis real time update service. * Connect to the Hypothesis real time update service.
* *
...@@ -169,10 +190,10 @@ export default function Streamer(store, auth, groups, session, settings) { ...@@ -169,10 +190,10 @@ export default function Streamer(store, auth, groups, session, settings) {
* process has started. * process has started.
*/ */
function connect() { function connect() {
setUpAutoReconnect();
if (socket) { if (socket) {
return Promise.resolve(); return Promise.resolve();
} }
return _connect(); return _connect();
} }
......
import EventEmitter from 'tiny-emitter'; import EventEmitter from 'tiny-emitter';
import fakeReduxStore from '../../test/fake-redux-store';
import Streamer from '../streamer'; import Streamer from '../streamer';
import { $imports } from '../streamer'; import { $imports } from '../streamer';
...@@ -43,12 +44,14 @@ const fixtures = { ...@@ -43,12 +44,14 @@ const fixtures = {
// the most recently created FakeSocket instance // the most recently created FakeSocket instance
let fakeWebSocket = null; let fakeWebSocket = null;
let fakeWebSockets = [];
class FakeSocket extends EventEmitter { class FakeSocket extends EventEmitter {
constructor(url) { constructor(url) {
super(); super();
fakeWebSocket = this; // eslint-disable-line consistent-this fakeWebSocket = this; // eslint-disable-line consistent-this
fakeWebSockets.push(this);
this.url = url; this.url = url;
this.messages = []; this.messages = [];
...@@ -95,19 +98,22 @@ describe('Streamer', function () { ...@@ -95,19 +98,22 @@ describe('Streamer', function () {
}, },
}; };
fakeStore = { fakeStore = fakeReduxStore(
addAnnotations: sinon.stub(), {},
annotationExists: sinon.stub().returns(false), {
clearPendingUpdates: sinon.stub(), addAnnotations: sinon.stub(),
pendingUpdates: sinon.stub().returns({}), annotationExists: sinon.stub().returns(false),
pendingDeletions: sinon.stub().returns({}), clearPendingUpdates: sinon.stub(),
profile: sinon.stub().returns({ pendingUpdates: sinon.stub().returns({}),
userid: 'jim@hypothes.is', pendingDeletions: sinon.stub().returns({}),
}), profile: sinon.stub().returns({
receiveRealTimeUpdates: sinon.stub(), userid: 'jim@hypothes.is',
removeAnnotations: sinon.stub(), }),
route: sinon.stub().returns('sidebar'), receiveRealTimeUpdates: sinon.stub(),
}; removeAnnotations: sinon.stub(),
route: sinon.stub().returns('sidebar'),
}
);
fakeGroups = { fakeGroups = {
focused: sinon.stub().returns({ id: 'public' }), focused: sinon.stub().returns({ id: 'public' }),
...@@ -130,6 +136,7 @@ describe('Streamer', function () { ...@@ -130,6 +136,7 @@ describe('Streamer', function () {
afterEach(function () { afterEach(function () {
$imports.$restore(); $imports.$restore();
activeStreamer = null; activeStreamer = null;
fakeWebSockets = [];
}); });
it('should not create a websocket connection if websocketUrl is not provided', function () { it('should not create a websocket connection if websocketUrl is not provided', function () {
...@@ -246,6 +253,47 @@ describe('Streamer', function () { ...@@ -246,6 +253,47 @@ describe('Streamer', function () {
}); });
}); });
describe('Automatic reconnection', function () {
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
it('should reconnect when user changes', function () {
let oldWebSocket;
createDefaultStreamer();
return activeStreamer
.connect()
.then(function () {
oldWebSocket = fakeWebSocket;
fakeStore.profile.returns({ userid: 'somebody' });
return fakeStore.setState({});
})
.then(function () {
assert.ok(oldWebSocket.didClose);
assert.ok(!fakeWebSocket.didClose);
});
});
it('should only set up auto-reconnect once', async () => {
createDefaultStreamer();
// This should register auto-reconnect
await activeStreamer.connect();
// Call connect again: this should not "re-register" auto-reconnect
await activeStreamer.connect();
// This should trigger auto-reconnect, but only once, proving that
// only one registration happened
fakeStore.profile.returns({ userid: 'somebody' });
fakeStore.setState({});
await delay(1);
// Total number of web sockets blown through in this test should be 2
// 3+ would indicate `reconnect` fired more than once
assert.lengthOf(fakeWebSockets, 2);
});
});
describe('annotation notifications', function () { describe('annotation notifications', function () {
beforeEach(function () { beforeEach(function () {
createDefaultStreamer(); createDefaultStreamer();
......
...@@ -15,11 +15,7 @@ ...@@ -15,11 +15,7 @@
<main ng-if="vm.route()"> <main ng-if="vm.route()">
<annotation-viewer-content ng-if="vm.route() == 'annotation'"></annotation-viewer-content> <annotation-viewer-content ng-if="vm.route() == 'annotation'"></annotation-viewer-content>
<stream-content ng-if="vm.route() == 'stream'"></stream-content> <stream-content ng-if="vm.route() == 'stream'"></stream-content>
<sidebar-content <sidebar-content ng-if="vm.route() == 'sidebar'" on-login="vm.login()" on-signUp="vm.signUp()"></sidebar-content>
ng-if="vm.route() == 'sidebar'"
auth="vm.auth"
on-login="vm.login()"
on-sign-up="vm.signUp()"></sidebar-content>
</main> </main>
</div> </div>
</div> </div>
<focused-mode-header
ng-if="vm.showFocusedHeader()">
</focused-mode-header>
<login-prompt-panel on-login="vm.onLogin()" on-sign-up="vm.onSignUp()"></login-prompt-panel>
<!-- Display error message if direct-linked annotation fetch failed. -->
<sidebar-content-error
error-type="'annotation'"
on-login-request="vm.onLogin()"
ng-if="vm.selectedAnnotationUnavailable()"
>
</sidebar-content-error>
<!-- Display error message if direct-linked group fetch failed. -->
<sidebar-content-error
error-type="'group'"
on-login-request="vm.onLogin()"
ng-if="vm.selectedGroupUnavailable()"
>
</sidebar-content-error>
<selection-tabs
ng-if="vm.showSelectedTabs()"
is-loading="vm.isLoading()">
</selection-tabs>
<search-status-bar
ng-if="!vm.isLoading() && !(vm.selectedAnnotationUnavailable() || vm.selectedGroupUnavailable())">
</search-status-bar>
<thread-list thread="vm.rootThread()"></thread-list>
<logged-out-message ng-if="vm.shouldShowLoggedOutMessage()" on-login="vm.onLogin()">
</logged-out-message>
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