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);
// Preact UI components that are wrapped for use within Angular templates.
import AnnotationViewerContent from './components/annotation-viewer-content';
import FocusedModeHeader from './components/focused-mode-header';
import HelpPanel from './components/help-panel';
import LoggedOutMessage from './components/logged-out-message';
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 SidebarContentError from './components/sidebar-content-error';
import SidebarContent from './components/sidebar-content';
import StreamContent from './components/stream-content';
import ThreadList from './components/thread-list';
import ToastMessages from './components/toast-messages';
......@@ -124,7 +120,6 @@ import TopBar from './components/top-bar';
// Remaining UI components that are still built with Angular.
import hypothesisApp from './components/hypothesis-app';
import sidebarContent from './components/sidebar-content';
// Services.
......@@ -243,12 +238,7 @@ function startAngularApp(config) {
)
.component('helpPanel', wrapComponent(HelpPanel))
.component('loginPromptPanel', wrapComponent(LoginPromptPanel))
.component('loggedOutMessage', wrapComponent(LoggedOutMessage))
.component('searchStatusBar', wrapComponent(SearchStatusBar))
.component('focusedModeHeader', wrapComponent(FocusedModeHeader))
.component('selectionTabs', wrapComponent(SelectionTabs))
.component('sidebarContent', sidebarContent)
.component('sidebarContentError', wrapComponent(SidebarContentError))
.component('sidebarContent', wrapComponent(SidebarContent))
.component('shareAnnotationsPanel', wrapComponent(ShareAnnotationsPanel))
.component('streamContent', wrapComponent(StreamContent))
.component('threadList', wrapComponent(ThreadList))
......
......@@ -3,6 +3,7 @@ import * as queryString from 'query-string';
import warnOnce from '../../shared/warn-once';
import { generateHexString } from '../util/random';
import Socket from '../websocket';
import { watch } from '../util/watch';
/**
* Open a new WebSocket connection to the Hypothesis push notification service.
......@@ -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.
*
......@@ -169,10 +190,10 @@ export default function Streamer(store, auth, groups, session, settings) {
* process has started.
*/
function connect() {
setUpAutoReconnect();
if (socket) {
return Promise.resolve();
}
return _connect();
}
......
import EventEmitter from 'tiny-emitter';
import fakeReduxStore from '../../test/fake-redux-store';
import Streamer from '../streamer';
import { $imports } from '../streamer';
......@@ -43,12 +44,14 @@ const fixtures = {
// the most recently created FakeSocket instance
let fakeWebSocket = null;
let fakeWebSockets = [];
class FakeSocket extends EventEmitter {
constructor(url) {
super();
fakeWebSocket = this; // eslint-disable-line consistent-this
fakeWebSockets.push(this);
this.url = url;
this.messages = [];
......@@ -95,19 +98,22 @@ describe('Streamer', function () {
},
};
fakeStore = {
addAnnotations: sinon.stub(),
annotationExists: sinon.stub().returns(false),
clearPendingUpdates: sinon.stub(),
pendingUpdates: sinon.stub().returns({}),
pendingDeletions: sinon.stub().returns({}),
profile: sinon.stub().returns({
userid: 'jim@hypothes.is',
}),
receiveRealTimeUpdates: sinon.stub(),
removeAnnotations: sinon.stub(),
route: sinon.stub().returns('sidebar'),
};
fakeStore = fakeReduxStore(
{},
{
addAnnotations: sinon.stub(),
annotationExists: sinon.stub().returns(false),
clearPendingUpdates: sinon.stub(),
pendingUpdates: sinon.stub().returns({}),
pendingDeletions: sinon.stub().returns({}),
profile: sinon.stub().returns({
userid: 'jim@hypothes.is',
}),
receiveRealTimeUpdates: sinon.stub(),
removeAnnotations: sinon.stub(),
route: sinon.stub().returns('sidebar'),
}
);
fakeGroups = {
focused: sinon.stub().returns({ id: 'public' }),
......@@ -130,6 +136,7 @@ describe('Streamer', function () {
afterEach(function () {
$imports.$restore();
activeStreamer = null;
fakeWebSockets = [];
});
it('should not create a websocket connection if websocketUrl is not provided', 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 () {
beforeEach(function () {
createDefaultStreamer();
......
......@@ -15,11 +15,7 @@
<main ng-if="vm.route()">
<annotation-viewer-content ng-if="vm.route() == 'annotation'"></annotation-viewer-content>
<stream-content ng-if="vm.route() == 'stream'"></stream-content>
<sidebar-content
ng-if="vm.route() == 'sidebar'"
auth="vm.auth"
on-login="vm.login()"
on-sign-up="vm.signUp()"></sidebar-content>
<sidebar-content ng-if="vm.route() == 'sidebar'" on-login="vm.login()" on-signUp="vm.signUp()"></sidebar-content>
</main>
</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