Commit ebc5c0e2 authored by Eduardo Sanz García's avatar Eduardo Sanz García Committed by Eduardo

Strengthen the types for `Bridge` events

I have created type definitions for all the event names that are sent
across the different frames using various `Bridge`s. It is based on the
previous `bridge-events.js`. I broke down the events in four sections
based on the direction of the messages:

* guest -> sidebar events
* host -> sidebar events
* sidebar -> guest/s events
* sidebar -> host events

For those events that didn't have a description I added one.

This is more stringent and less verbose than the previous lookup system.
parent f6ac11fc
...@@ -104,8 +104,8 @@ ...@@ -104,8 +104,8 @@
"scripts": { "scripts": {
"build": "cross-env NODE_ENV=production gulp build", "build": "cross-env NODE_ENV=production gulp build",
"lint": "eslint .", "lint": "eslint .",
"checkformatting": "prettier --check '**/*.{js,scss}'", "checkformatting": "prettier --check '**/*.{js,scss,d.ts}'",
"format": "prettier --list-different --write '**/*.{js,scss}'", "format": "prettier --list-different --write '**/*.{js,scss,d.ts}'",
"test": "gulp test", "test": "gulp test",
"typecheck": "tsc --build tsconfig.json", "typecheck": "tsc --build tsconfig.json",
"report-coverage": "codecov -f coverage/coverage-final.json", "report-coverage": "codecov -f coverage/coverage-final.json",
......
import events from '../shared/bridge-events';
const ANNOTATION_COUNT_ATTR = 'data-hypothesis-annotation-count'; const ANNOTATION_COUNT_ATTR = 'data-hypothesis-annotation-count';
/** /**
...@@ -11,8 +9,9 @@ const ANNOTATION_COUNT_ATTR = 'data-hypothesis-annotation-count'; ...@@ -11,8 +9,9 @@ const ANNOTATION_COUNT_ATTR = 'data-hypothesis-annotation-count';
* display annotation count. * display annotation count.
* @param {import('../shared/bridge').Bridge} bridge - Channel for host-sidebar communication * @param {import('../shared/bridge').Bridge} bridge - Channel for host-sidebar communication
*/ */
export function annotationCounts(rootEl, bridge) { export function annotationCounts(rootEl, bridge) {
bridge.on(events.PUBLIC_ANNOTATION_COUNT_CHANGED, updateAnnotationCountElems); bridge.on('publicAnnotationCountChanged', updateAnnotationCountElems);
function updateAnnotationCountElems(newCount) { function updateAnnotationCountElems(newCount) {
const elems = rootEl.querySelectorAll('[' + ANNOTATION_COUNT_ATTR + ']'); const elems = rootEl.querySelectorAll('[' + ANNOTATION_COUNT_ATTR + ']');
......
/** /**
* @typedef {import('../shared/bridge').Bridge} Bridge
* @typedef {import('../types/annotator').AnnotationData} AnnotationData * @typedef {import('../types/annotator').AnnotationData} AnnotationData
* @typedef {import('../types/annotator').Destroyable} Destroyable * @typedef {import('../types/annotator').Destroyable} Destroyable
* @typedef {import('../types/bridge-events').GuestToSidebarEvent} GuestToSidebarEvent
* @typedef {import('../types/bridge-events').SidebarToGuestEvent} SidebarToGuestEvent
* @typedef {import('./util/emitter').EventBus} EventBus * @typedef {import('./util/emitter').EventBus} EventBus
*/ */
/**
* @template {GuestToSidebarEvent} T
* @template {SidebarToGuestEvent} U
* @typedef {import('../shared/bridge').Bridge<T,U>} Bridge
*/
/** /**
* Message sent between the sidebar and annotator about an annotation that has * Message sent between the sidebar and annotator about an annotation that has
* changed. * changed.
...@@ -26,11 +33,11 @@ ...@@ -26,11 +33,11 @@
export class AnnotationSync { export class AnnotationSync {
/** /**
* @param {EventBus} eventBus - Event bus for communicating with the annotator code (eg. the Guest) * @param {EventBus} eventBus - Event bus for communicating with the annotator code (eg. the Guest)
* @param {Bridge} bridge - Channel for communicating with the sidebar * @param {Bridge<GuestToSidebarEvent,SidebarToGuestEvent>} bridge - Channel for communicating with the sidebar
*/ */
constructor(eventBus, bridge) { constructor(eventBus, bridge) {
this._emitter = eventBus.createEmitter(); this._emitter = eventBus.createEmitter();
this.bridge = bridge; this._sidebar = bridge;
/** /**
* Mapping from annotation tags to annotation objects for annotations which * Mapping from annotation tags to annotation objects for annotations which
...@@ -43,7 +50,7 @@ export class AnnotationSync { ...@@ -43,7 +50,7 @@ export class AnnotationSync {
this.destroyed = false; this.destroyed = false;
// Relay events from the sidebar to the rest of the annotator. // Relay events from the sidebar to the rest of the annotator.
this.bridge.on('deleteAnnotation', (body, callback) => { this._sidebar.on('deleteAnnotation', (body, callback) => {
if (this.destroyed) { if (this.destroyed) {
callback(null); callback(null);
return; return;
...@@ -55,7 +62,7 @@ export class AnnotationSync { ...@@ -55,7 +62,7 @@ export class AnnotationSync {
callback(null); callback(null);
}); });
this.bridge.on('loadAnnotations', (bodies, callback) => { this._sidebar.on('loadAnnotations', (bodies, callback) => {
if (this.destroyed) { if (this.destroyed) {
callback(null); callback(null);
return; return;
...@@ -70,7 +77,7 @@ export class AnnotationSync { ...@@ -70,7 +77,7 @@ export class AnnotationSync {
if (annotation.$tag) { if (annotation.$tag) {
return; return;
} }
this.bridge.call('beforeCreateAnnotation', this._format(annotation)); this._sidebar.call('beforeCreateAnnotation', this._format(annotation));
}); });
} }
...@@ -87,7 +94,7 @@ export class AnnotationSync { ...@@ -87,7 +94,7 @@ export class AnnotationSync {
return; return;
} }
this.bridge.call( this._sidebar.call(
'sync', 'sync',
annotations.map(ann => this._format(ann)) annotations.map(ann => this._format(ann))
); );
......
import events from '../shared/bridge-events';
import warnOnce from '../shared/warn-once'; import warnOnce from '../shared/warn-once';
let _features = {}; let _features = {};
...@@ -12,7 +11,7 @@ export const features = { ...@@ -12,7 +11,7 @@ export const features = {
* @param {import('../shared/bridge').Bridge} bridge - Channel for host-sidebar communication * @param {import('../shared/bridge').Bridge} bridge - Channel for host-sidebar communication
*/ */
init: function (bridge) { init: function (bridge) {
bridge.on(events.FEATURE_FLAGS_UPDATED, _set); bridge.on('featureFlagsUpdated', _set);
}, },
reset: function () { reset: function () {
......
...@@ -25,6 +25,8 @@ import { normalizeURI } from './util/url'; ...@@ -25,6 +25,8 @@ import { normalizeURI } from './util/url';
* @typedef {import('../types/annotator').Destroyable} Destroyable * @typedef {import('../types/annotator').Destroyable} Destroyable
* @typedef {import('../types/annotator').SidebarLayout} SidebarLayout * @typedef {import('../types/annotator').SidebarLayout} SidebarLayout
* @typedef {import('../types/api').Target} Target * @typedef {import('../types/api').Target} Target
* @typedef {import('../types/bridge-events').GuestToSidebarEvent} GuestToSidebarEvent
* @typedef {import('../types/bridge-events').SidebarToGuestEvent} SidebarToGuestEvent
* @typedef {import('./util/emitter').EventBus} EventBus * @typedef {import('./util/emitter').EventBus} EventBus
*/ */
...@@ -163,7 +165,11 @@ export default class Guest { ...@@ -163,7 +165,11 @@ export default class Guest {
// The "top" guest instance will have this as null since it's in a top frame not a sub frame // The "top" guest instance will have this as null since it's in a top frame not a sub frame
this._frameIdentifier = config.subFrameIdentifier || null; this._frameIdentifier = config.subFrameIdentifier || null;
// Set up listeners for messages coming from the sidebar to this guest. /**
* Channel for sidebar-guest communication.
*
* @type {Bridge<GuestToSidebarEvent,SidebarToGuestEvent>}
*/
this._bridge = new Bridge(); this._bridge = new Bridge();
this._bridge.onConnect(() => { this._bridge.onConnect(() => {
this.setVisibleHighlights(config.showHighlights === 'always'); this.setVisibleHighlights(config.showHighlights === 'always');
......
import Hammer from 'hammerjs'; import Hammer from 'hammerjs';
import { Bridge } from '../shared/bridge'; import { Bridge } from '../shared/bridge';
import events from '../shared/bridge-events';
import { ListenerCollection } from '../shared/listener-collection'; import { ListenerCollection } from '../shared/listener-collection';
import { annotationCounts } from './annotation-counts'; import { annotationCounts } from './annotation-counts';
...@@ -14,6 +13,8 @@ import { createShadowRoot } from './util/shadow-root'; ...@@ -14,6 +13,8 @@ import { createShadowRoot } from './util/shadow-root';
/** /**
* @typedef {import('./guest').default} Guest * @typedef {import('./guest').default} Guest
* @typedef {import('../types/bridge-events').HostToSidebarEvent} HostToSidebarEvent
* @typedef {import('../types/bridge-events').SidebarToHostEvent} SidebarToHostEvent
* @typedef {import('../types/annotator').SidebarLayout} SidebarLayout * @typedef {import('../types/annotator').SidebarLayout} SidebarLayout
* @typedef {import('../types/annotator').Destroyable} Destroyable * @typedef {import('../types/annotator').Destroyable} Destroyable
*/ */
...@@ -64,6 +65,11 @@ export default class Sidebar { ...@@ -64,6 +65,11 @@ export default class Sidebar {
constructor(element, eventBus, guest, config = {}) { constructor(element, eventBus, guest, config = {}) {
this._emitter = eventBus.createEmitter(); this._emitter = eventBus.createEmitter();
/**
* Channel for sidebar-host communication.
*
* @type {Bridge<HostToSidebarEvent,SidebarToHostEvent>}
*/
this._sidebarRPC = new Bridge(); this._sidebarRPC = new Bridge();
/** /**
...@@ -243,12 +249,13 @@ export default class Sidebar { ...@@ -243,12 +249,13 @@ export default class Sidebar {
this.show(); this.show();
}); });
/** @type {Array<[SidebarToHostEvent, function]>} */
const eventHandlers = [ const eventHandlers = [
[events.LOGIN_REQUESTED, this.onLoginRequest], ['loginRequested', this.onLoginRequest],
[events.LOGOUT_REQUESTED, this.onLogoutRequest], ['logoutRequested', this.onLogoutRequest],
[events.SIGNUP_REQUESTED, this.onSignupRequest], ['signupRequested', this.onSignupRequest],
[events.PROFILE_REQUESTED, this.onProfileRequest], ['profileRequested', this.onProfileRequest],
[events.HELP_REQUESTED, this.onHelpRequest], ['helpRequested', this.onHelpRequest],
]; ];
eventHandlers.forEach(([event, handler]) => { eventHandlers.forEach(([event, handler]) => {
if (handler) { if (handler) {
......
import { EventBus } from '../util/emitter';
import { AnnotationSync } from '../annotation-sync'; import { AnnotationSync } from '../annotation-sync';
import { EventBus } from '../util/emitter';
describe('AnnotationSync', () => { describe('AnnotationSync', () => {
let createAnnotationSync; let createAnnotationSync;
...@@ -49,7 +48,7 @@ describe('AnnotationSync', () => { ...@@ -49,7 +48,7 @@ describe('AnnotationSync', () => {
assert.calledWith(eventStub, ann); assert.calledWith(eventStub, ann);
}); });
it("calls the 'deleteAnnotation' event's callback function", done => { it('calls the "deleteAnnotation" event\'s callback function', done => {
const ann = { id: 1, $tag: 'tag1' }; const ann = { id: 1, $tag: 'tag1' };
const callback = function (err, result) { const callback = function (err, result) {
assert.isNull(err); assert.isNull(err);
......
import events from '../../shared/bridge-events';
import { features, $imports } from '../features'; import { features, $imports } from '../features';
describe('features - annotation layer', () => { describe('features - annotation layer', () => {
...@@ -22,7 +21,7 @@ describe('features - annotation layer', () => { ...@@ -22,7 +21,7 @@ describe('features - annotation layer', () => {
features.init({ features.init({
on: function (topic, handler) { on: function (topic, handler) {
if (topic === events.FEATURE_FLAGS_UPDATED) { if (topic === 'featureFlagsUpdated') {
featureFlagsUpdateHandler = handler; featureFlagsUpdateHandler = handler;
} }
}, },
......
import Guest from '../guest'; import Guest, { $imports } from '../guest';
import { EventBus } from '../util/emitter'; import { EventBus } from '../util/emitter';
import { $imports } from '../guest';
class FakeAdder { class FakeAdder {
constructor(container, options) { constructor(container, options) {
...@@ -226,7 +225,7 @@ describe('Guest', () => { ...@@ -226,7 +225,7 @@ describe('Guest', () => {
}); });
describe('events from sidebar', () => { describe('events from sidebar', () => {
const emitGuestEvent = (event, ...args) => { const emitSidebarEvent = (event, ...args) => {
for (let [evt, fn] of fakeBridge.on.args) { for (let [evt, fn] of fakeBridge.on.args) {
if (event === evt) { if (event === evt) {
fn(...args); fn(...args);
...@@ -244,7 +243,7 @@ describe('Guest', () => { ...@@ -244,7 +243,7 @@ describe('Guest', () => {
{ annotation: { $tag: 'tag2' }, highlights: [highlight1] }, { annotation: { $tag: 'tag2' }, highlights: [highlight1] },
]; ];
emitGuestEvent('focusAnnotations', ['tag1']); emitSidebarEvent('focusAnnotations', ['tag1']);
assert.calledWith( assert.calledWith(
highlighter.setHighlightsFocused, highlighter.setHighlightsFocused,
...@@ -262,7 +261,7 @@ describe('Guest', () => { ...@@ -262,7 +261,7 @@ describe('Guest', () => {
{ annotation: { $tag: 'tag2' }, highlights: [highlight1] }, { annotation: { $tag: 'tag2' }, highlights: [highlight1] },
]; ];
emitGuestEvent('focusAnnotations', ['tag1']); emitSidebarEvent('focusAnnotations', ['tag1']);
assert.calledWith( assert.calledWith(
highlighter.setHighlightsFocused, highlighter.setHighlightsFocused,
...@@ -274,8 +273,8 @@ describe('Guest', () => { ...@@ -274,8 +273,8 @@ describe('Guest', () => {
it('updates focused tag set', () => { it('updates focused tag set', () => {
const guest = createGuest(); const guest = createGuest();
emitGuestEvent('focusAnnotations', ['tag1']); emitSidebarEvent('focusAnnotations', ['tag1']);
emitGuestEvent('focusAnnotations', ['tag2', 'tag3']); emitSidebarEvent('focusAnnotations', ['tag2', 'tag3']);
assert.deepEqual([...guest.focusedAnnotationTags], ['tag2', 'tag3']); assert.deepEqual([...guest.focusedAnnotationTags], ['tag2', 'tag3']);
}); });
...@@ -294,7 +293,7 @@ describe('Guest', () => { ...@@ -294,7 +293,7 @@ describe('Guest', () => {
}, },
]; ];
emitGuestEvent('scrollToAnnotation', 'tag1'); emitSidebarEvent('scrollToAnnotation', 'tag1');
assert.called(fakeIntegration.scrollToAnchor); assert.called(fakeIntegration.scrollToAnchor);
assert.calledWith(fakeIntegration.scrollToAnchor, guest.anchors[0]); assert.calledWith(fakeIntegration.scrollToAnchor, guest.anchors[0]);
...@@ -318,7 +317,7 @@ describe('Guest', () => { ...@@ -318,7 +317,7 @@ describe('Guest', () => {
resolve(); resolve();
}); });
emitGuestEvent('scrollToAnnotation', 'tag1'); emitSidebarEvent('scrollToAnnotation', 'tag1');
}); });
}); });
...@@ -337,7 +336,7 @@ describe('Guest', () => { ...@@ -337,7 +336,7 @@ describe('Guest', () => {
event.preventDefault() event.preventDefault()
); );
emitGuestEvent('scrollToAnnotation', 'tag1'); emitSidebarEvent('scrollToAnnotation', 'tag1');
assert.notCalled(fakeIntegration.scrollToAnchor); assert.notCalled(fakeIntegration.scrollToAnchor);
}); });
...@@ -346,7 +345,7 @@ describe('Guest', () => { ...@@ -346,7 +345,7 @@ describe('Guest', () => {
const guest = createGuest(); const guest = createGuest();
guest.anchors = [{ annotation: { $tag: 'tag1' } }]; guest.anchors = [{ annotation: { $tag: 'tag1' } }];
emitGuestEvent('scrollToAnnotation', 'tag1'); emitSidebarEvent('scrollToAnnotation', 'tag1');
assert.notCalled(fakeIntegration.scrollToAnchor); assert.notCalled(fakeIntegration.scrollToAnchor);
}); });
...@@ -366,7 +365,7 @@ describe('Guest', () => { ...@@ -366,7 +365,7 @@ describe('Guest', () => {
const eventEmitted = sandbox.stub(); const eventEmitted = sandbox.stub();
guest.element.addEventListener('scrolltorange', eventEmitted); guest.element.addEventListener('scrolltorange', eventEmitted);
emitGuestEvent('scrollToAnnotation', 'tag1'); emitSidebarEvent('scrollToAnnotation', 'tag1');
assert.notCalled(eventEmitted); assert.notCalled(eventEmitted);
assert.notCalled(fakeIntegration.scrollToAnchor); assert.notCalled(fakeIntegration.scrollToAnchor);
...@@ -400,7 +399,7 @@ describe('Guest', () => { ...@@ -400,7 +399,7 @@ describe('Guest', () => {
fakeIntegration.getMetadata.resolves(metadata); fakeIntegration.getMetadata.resolves(metadata);
emitGuestEvent( emitSidebarEvent(
'getDocumentInfo', 'getDocumentInfo',
createCallback('https://example.com/test.pdf', metadata, done) createCallback('https://example.com/test.pdf', metadata, done)
); );
...@@ -411,14 +410,14 @@ describe('Guest', () => { ...@@ -411,14 +410,14 @@ describe('Guest', () => {
it('sets visibility of highlights in document', () => { it('sets visibility of highlights in document', () => {
const guest = createGuest(); const guest = createGuest();
emitGuestEvent('setVisibleHighlights', true); emitSidebarEvent('setVisibleHighlights', true);
assert.calledWith( assert.calledWith(
highlighter.setHighlightsVisible, highlighter.setHighlightsVisible,
guest.element, guest.element,
true true
); );
emitGuestEvent('setVisibleHighlights', false); emitSidebarEvent('setVisibleHighlights', false);
assert.calledWith( assert.calledWith(
highlighter.setHighlightsVisible, highlighter.setHighlightsVisible,
guest.element, guest.element,
......
import events from '../../shared/bridge-events'; import Sidebar, { MIN_RESIZE, $imports } from '../sidebar';
import Sidebar, { MIN_RESIZE } from '../sidebar';
import { $imports } from '../sidebar';
import { EventBus } from '../util/emitter'; import { EventBus } from '../util/emitter';
const DEFAULT_WIDTH = 350; const DEFAULT_WIDTH = 350;
...@@ -335,12 +332,12 @@ describe('Sidebar', () => { ...@@ -335,12 +332,12 @@ describe('Sidebar', () => {
}); });
}); });
describe('on LOGIN_REQUESTED event', () => { describe('on "loginRequest" event', () => {
it('calls the onLoginRequest callback function if one was provided', () => { it('calls the onLoginRequest callback function if one was provided', () => {
const onLoginRequest = sandbox.stub(); const onLoginRequest = sandbox.stub();
createSidebar({ services: [{ onLoginRequest }] }); createSidebar({ services: [{ onLoginRequest }] });
emitEvent(events.LOGIN_REQUESTED); emitEvent('loginRequested');
assert.called(onLoginRequest); assert.called(onLoginRequest);
}); });
...@@ -360,7 +357,7 @@ describe('Sidebar', () => { ...@@ -360,7 +357,7 @@ describe('Sidebar', () => {
], ],
}); });
emitEvent(events.LOGIN_REQUESTED); emitEvent('loginRequested');
assert.called(firstOnLogin); assert.called(firstOnLogin);
assert.notCalled(secondOnLogin); assert.notCalled(secondOnLogin);
...@@ -380,7 +377,7 @@ describe('Sidebar', () => { ...@@ -380,7 +377,7 @@ describe('Sidebar', () => {
], ],
}); });
emitEvent(events.LOGIN_REQUESTED); emitEvent('loginRequested');
assert.notCalled(secondOnLogin); assert.notCalled(secondOnLogin);
assert.notCalled(thirdOnLogin); assert.notCalled(thirdOnLogin);
...@@ -388,17 +385,17 @@ describe('Sidebar', () => { ...@@ -388,17 +385,17 @@ describe('Sidebar', () => {
it('does not crash if there is no services', () => { it('does not crash if there is no services', () => {
createSidebar(); // No config.services createSidebar(); // No config.services
emitEvent(events.LOGIN_REQUESTED); emitEvent('loginRequested');
}); });
it('does not crash if services is an empty array', () => { it('does not crash if services is an empty array', () => {
createSidebar({ services: [] }); createSidebar({ services: [] });
emitEvent(events.LOGIN_REQUESTED); emitEvent('loginRequested');
}); });
it('does not crash if the first service has no onLoginRequest', () => { it('does not crash if the first service has no onLoginRequest', () => {
createSidebar({ services: [{}] }); createSidebar({ services: [{}] });
emitEvent(events.LOGIN_REQUESTED); emitEvent('loginRequested');
}); });
}); });
...@@ -407,37 +404,37 @@ describe('Sidebar', () => { ...@@ -407,37 +404,37 @@ describe('Sidebar', () => {
const onLogoutRequest = sandbox.stub(); const onLogoutRequest = sandbox.stub();
createSidebar({ services: [{ onLogoutRequest }] }); createSidebar({ services: [{ onLogoutRequest }] });
emitEvent(events.LOGOUT_REQUESTED); emitEvent('logoutRequested');
assert.called(onLogoutRequest); assert.called(onLogoutRequest);
})); }));
describe('on SIGNUP_REQUESTED event', () => describe('on "signupRequest" event', () =>
it('calls the onSignupRequest callback function', () => { it('calls the onSignupRequest callback function', () => {
const onSignupRequest = sandbox.stub(); const onSignupRequest = sandbox.stub();
createSidebar({ services: [{ onSignupRequest }] }); createSidebar({ services: [{ onSignupRequest }] });
emitEvent(events.SIGNUP_REQUESTED); emitEvent('signupRequested');
assert.called(onSignupRequest); assert.called(onSignupRequest);
})); }));
describe('on PROFILE_REQUESTED event', () => describe('on "profileRequest" event', () =>
it('calls the onProfileRequest callback function', () => { it('calls the onProfileRequest callback function', () => {
const onProfileRequest = sandbox.stub(); const onProfileRequest = sandbox.stub();
createSidebar({ services: [{ onProfileRequest }] }); createSidebar({ services: [{ onProfileRequest }] });
emitEvent(events.PROFILE_REQUESTED); emitEvent('profileRequested');
assert.called(onProfileRequest); assert.called(onProfileRequest);
})); }));
describe('on HELP_REQUESTED event', () => describe('on "helpRequested" event', () =>
it('calls the onHelpRequest callback function', () => { it('calls the onHelpRequest callback function', () => {
const onHelpRequest = sandbox.stub(); const onHelpRequest = sandbox.stub();
createSidebar({ services: [{ onHelpRequest }] }); createSidebar({ services: [{ onHelpRequest }] });
emitEvent(events.HELP_REQUESTED); emitEvent('helpRequested');
assert.called(onHelpRequest); assert.called(onHelpRequest);
})); }));
......
/**
* This module defines the set of global events that are dispatched
* across the bridge between the sidebar and annotator
*/
export default {
// Events that the sidebar sends to the annotator
// ----------------------------------------------
/**
* The updated feature flags for the user
*/
FEATURE_FLAGS_UPDATED: 'featureFlagsUpdated',
/**
* The sidebar is asking the annotator to open the partner site help page.
*/
HELP_REQUESTED: 'helpRequested',
/** The sidebar is asking the annotator to do a partner site log in
* (for example, pop up a log in window). This is used when the client is
* embedded in a partner site and a log in button in the client is clicked.
*/
LOGIN_REQUESTED: 'loginRequested',
/** The sidebar is asking the annotator to do a partner site log out.
* This is used when the client is embedded in a partner site and a log out
* button in the client is clicked.
*/
LOGOUT_REQUESTED: 'logoutRequested',
/**
* The sidebar is asking the annotator to open the partner site profile page.
*/
PROFILE_REQUESTED: 'profileRequested',
/**
* The set of annotations was updated.
*/
PUBLIC_ANNOTATION_COUNT_CHANGED: 'publicAnnotationCountChanged',
/**
* The sidebar is asking the annotator to do a partner site sign-up.
*/
SIGNUP_REQUESTED: 'signupRequested',
// Events that the annotator sends to the sidebar
// ----------------------------------------------
};
import { PortRPC } from './port-rpc'; import { PortRPC } from './port-rpc';
/** @typedef {import('../types/annotator').Destroyable} Destroyable */ /**
* @typedef {import('../types/annotator').Destroyable} Destroyable
* @typedef {import('../types/bridge-events').BridgeEvent} BridgeEvent
*/
/** /**
* The Bridge service sets up a channel between frames and provides an events * The Bridge service sets up a channel between frames and provides an events
* API on top of it. * API on top of it.
* *
* @template {BridgeEvent} CallMethods - Names of methods that can be called (via {@link call})
* @template {BridgeEvent} OnMethods - Names of methods that can be handled (via {@link on})
* @implements Destroyable * @implements Destroyable
*/ */
export class Bridge { export class Bridge {
...@@ -66,7 +71,7 @@ export class Bridge { ...@@ -66,7 +71,7 @@ export class Bridge {
* Make a method call on all channels, collect the results and pass them to a * Make a method call on all channels, collect the results and pass them to a
* callback when all results are collected. * callback when all results are collected.
* *
* @param {string} method - Name of remote method to call. * @param {CallMethods} method - Name of remote method to call.
* @param {any[]} args - Arguments to method. Final argument is an optional * @param {any[]} args - Arguments to method. Final argument is an optional
* callback with this type: `(error: string|Error|null, ...result: any[]) => void`. * callback with this type: `(error: string|Error|null, ...result: any[]) => void`.
* This callback, if any, will be triggered once a response (via `postMessage`) * This callback, if any, will be triggered once a response (via `postMessage`)
...@@ -132,7 +137,7 @@ export class Bridge { ...@@ -132,7 +137,7 @@ export class Bridge {
* Register a listener to be invoked when any connected channel sends a * Register a listener to be invoked when any connected channel sends a
* message to this `Bridge`. * message to this `Bridge`.
* *
* @param {string} method * @param {'connect'|OnMethods} method
* @param {(...args: any[]) => void} listener -- Final argument is an optional * @param {(...args: any[]) => void} listener -- Final argument is an optional
* callback of the type: `(error: string|Error|null, ...result: any[]) => void`. * callback of the type: `(error: string|Error|null, ...result: any[]) => void`.
* This callback must be invoked in order to respond (via `postMessage`) * This callback must be invoked in order to respond (via `postMessage`)
......
import classnames from 'classnames'; import classnames from 'classnames';
import { useEffect, useMemo } from 'preact/hooks'; import { useEffect, useMemo } from 'preact/hooks';
import bridgeEvents from '../../shared/bridge-events';
import { confirm } from '../../shared/prompts'; import { confirm } from '../../shared/prompts';
import { serviceConfig } from '../config/service-config'; import { serviceConfig } from '../config/service-config';
import { useStoreProxy } from '../store/use-store';
import { parseAccountID } from '../helpers/account-id'; import { parseAccountID } from '../helpers/account-id';
import { shouldAutoDisplayTutorial } from '../helpers/session'; import { shouldAutoDisplayTutorial } from '../helpers/session';
import { applyTheme } from '../helpers/theme'; import { applyTheme } from '../helpers/theme';
import { withServices } from '../service-context'; import { withServices } from '../service-context';
import { useStoreProxy } from '../store/use-store';
import AnnotationView from './AnnotationView'; import AnnotationView from './AnnotationView';
import SidebarView from './SidebarView'; import SidebarView from './SidebarView';
...@@ -98,7 +97,7 @@ function HypothesisApp({ auth, frameSync, settings, session, toastMessenger }) { ...@@ -98,7 +97,7 @@ function HypothesisApp({ auth, frameSync, settings, session, toastMessenger }) {
const login = async () => { const login = async () => {
if (serviceConfig(settings)) { if (serviceConfig(settings)) {
// Let the host page handle the login request // Let the host page handle the login request
frameSync.notifyHost(bridgeEvents.LOGIN_REQUESTED); frameSync.notifyHost('loginRequested');
return; return;
} }
...@@ -116,7 +115,7 @@ function HypothesisApp({ auth, frameSync, settings, session, toastMessenger }) { ...@@ -116,7 +115,7 @@ function HypothesisApp({ auth, frameSync, settings, session, toastMessenger }) {
const signUp = () => { const signUp = () => {
if (serviceConfig(settings)) { if (serviceConfig(settings)) {
// Let the host page handle the signup request // Let the host page handle the signup request
frameSync.notifyHost(bridgeEvents.SIGNUP_REQUESTED); frameSync.notifyHost('signupRequested');
return; return;
} }
window.open(store.getLink('signup')); window.open(store.getLink('signup'));
...@@ -156,7 +155,7 @@ function HypothesisApp({ auth, frameSync, settings, session, toastMessenger }) { ...@@ -156,7 +155,7 @@ function HypothesisApp({ auth, frameSync, settings, session, toastMessenger }) {
store.discardAllDrafts(); store.discardAllDrafts();
if (serviceConfig(settings)) { if (serviceConfig(settings)) {
frameSync.notifyHost(bridgeEvents.LOGOUT_REQUESTED); frameSync.notifyHost('logoutRequested');
return; return;
} }
......
import { IconButton, LinkButton } from '@hypothesis/frontend-shared'; import { IconButton, LinkButton } from '@hypothesis/frontend-shared';
import bridgeEvents from '../../shared/bridge-events';
import { serviceConfig } from '../config/service-config'; import { serviceConfig } from '../config/service-config';
import { useStoreProxy } from '../store/use-store';
import { isThirdPartyService } from '../helpers/is-third-party-service'; import { isThirdPartyService } from '../helpers/is-third-party-service';
import { withServices } from '../service-context';
import { applyTheme } from '../helpers/theme'; import { applyTheme } from '../helpers/theme';
import { withServices } from '../service-context';
import { useStoreProxy } from '../store/use-store';
import GroupList from './GroupList'; import GroupList from './GroupList';
import SearchInput from './SearchInput'; import SearchInput from './SearchInput';
...@@ -71,7 +70,7 @@ function TopBar({ ...@@ -71,7 +70,7 @@ function TopBar({
const requestHelp = () => { const requestHelp = () => {
const service = serviceConfig(settings); const service = serviceConfig(settings);
if (service && service.onHelpRequestProvided) { if (service && service.onHelpRequestProvided) {
frameSync.notifyHost(bridgeEvents.HELP_REQUESTED); frameSync.notifyHost('helpRequested');
} else { } else {
store.toggleSidebarPanel('help'); store.toggleSidebarPanel('help');
} }
......
import { SvgIcon } from '@hypothesis/frontend-shared'; import { SvgIcon } from '@hypothesis/frontend-shared';
import { useState } from 'preact/hooks'; import { useState } from 'preact/hooks';
import bridgeEvents from '../../shared/bridge-events';
import { serviceConfig } from '../config/service-config'; import { serviceConfig } from '../config/service-config';
import { isThirdPartyUser } from '../helpers/account-id'; import { isThirdPartyUser } from '../helpers/account-id';
import { useStoreProxy } from '../store/use-store';
import { withServices } from '../service-context'; import { withServices } from '../service-context';
import { useStoreProxy } from '../store/use-store';
import Menu from './Menu'; import Menu from './Menu';
import MenuItem from './MenuItem'; import MenuItem from './MenuItem';
...@@ -70,7 +69,7 @@ function UserMenu({ auth, frameSync, onLogout, settings }) { ...@@ -70,7 +69,7 @@ function UserMenu({ auth, frameSync, onLogout, settings }) {
}; };
const onProfileSelected = () => const onProfileSelected = () =>
isThirdParty && frameSync.notifyHost(bridgeEvents.PROFILE_REQUESTED); isThirdParty && frameSync.notifyHost('profileRequested');
// Generate dynamic props for the profile <MenuItem> component // Generate dynamic props for the profile <MenuItem> component
const profileItemProps = (() => { const profileItemProps = (() => {
......
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import bridgeEvents from '../../../shared/bridge-events';
import mockImportedComponents from '../../../test-util/mock-imported-components'; import mockImportedComponents from '../../../test-util/mock-imported-components';
import HypothesisApp, { $imports } from '../HypothesisApp'; import HypothesisApp, { $imports } from '../HypothesisApp';
describe('HypothesisApp', () => { describe('HypothesisApp', () => {
...@@ -225,13 +223,10 @@ describe('HypothesisApp', () => { ...@@ -225,13 +223,10 @@ describe('HypothesisApp', () => {
fakeServiceConfig.returns({}); fakeServiceConfig.returns({});
}); });
it('sends SIGNUP_REQUESTED event', () => { it('sends "signupRequest" event', () => {
const wrapper = createComponent(); const wrapper = createComponent();
clickSignUp(wrapper); clickSignUp(wrapper);
assert.calledWith( assert.calledWith(fakeFrameSync.notifyHost, 'signupRequested');
fakeFrameSync.notifyHost,
bridgeEvents.SIGNUP_REQUESTED
);
}); });
it('does not open a URL directly', () => { it('does not open a URL directly', () => {
...@@ -304,7 +299,7 @@ describe('HypothesisApp', () => { ...@@ -304,7 +299,7 @@ describe('HypothesisApp', () => {
assert.equal(fakeFrameSync.notifyHost.callCount, 1); assert.equal(fakeFrameSync.notifyHost.callCount, 1);
assert.isTrue( assert.isTrue(
fakeFrameSync.notifyHost.calledWithExactly(bridgeEvents.LOGIN_REQUESTED) fakeFrameSync.notifyHost.calledWithExactly('loginRequested')
); );
}); });
}); });
...@@ -412,10 +407,7 @@ describe('HypothesisApp', () => { ...@@ -412,10 +407,7 @@ describe('HypothesisApp', () => {
await clickLogOut(wrapper); await clickLogOut(wrapper);
assert.calledOnce(fakeFrameSync.notifyHost); assert.calledOnce(fakeFrameSync.notifyHost);
assert.calledWithExactly( assert.calledWithExactly(fakeFrameSync.notifyHost, 'logoutRequested');
fakeFrameSync.notifyHost,
bridgeEvents.LOGOUT_REQUESTED
);
}); });
it('does not send LOGOUT_REQUESTED if the user cancels the prompt', async () => { it('does not send LOGOUT_REQUESTED if the user cancels the prompt', async () => {
......
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import bridgeEvents from '../../../shared/bridge-events';
import TopBar from '../TopBar';
import { $imports } from '../TopBar';
import { checkAccessibility } from '../../../test-util/accessibility'; import { checkAccessibility } from '../../../test-util/accessibility';
import mockImportedComponents from '../../../test-util/mock-imported-components'; import mockImportedComponents from '../../../test-util/mock-imported-components';
import TopBar, { $imports } from '../TopBar';
describe('TopBar', () => { describe('TopBar', () => {
const fakeSettings = {}; const fakeSettings = {};
...@@ -123,10 +120,7 @@ describe('TopBar', () => { ...@@ -123,10 +120,7 @@ describe('TopBar', () => {
helpButton.props().onClick(); helpButton.props().onClick();
assert.equal(fakeStore.toggleSidebarPanel.callCount, 0); assert.equal(fakeStore.toggleSidebarPanel.callCount, 0);
assert.calledWith( assert.calledWith(fakeFrameSync.notifyHost, 'helpRequested');
fakeFrameSync.notifyHost,
bridgeEvents.HELP_REQUESTED
);
}); });
}); });
}); });
......
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { act } from 'preact/test-utils'; import { act } from 'preact/test-utils';
import bridgeEvents from '../../../shared/bridge-events';
import UserMenu from '../UserMenu';
import { $imports } from '../UserMenu';
import mockImportedComponents from '../../../test-util/mock-imported-components'; import mockImportedComponents from '../../../test-util/mock-imported-components';
import UserMenu, { $imports } from '../UserMenu';
describe('UserMenu', () => { describe('UserMenu', () => {
let fakeAuth; let fakeAuth;
...@@ -148,10 +145,7 @@ describe('UserMenu', () => { ...@@ -148,10 +145,7 @@ describe('UserMenu', () => {
onProfileSelected(); onProfileSelected();
assert.equal(fakeFrameSync.notifyHost.callCount, 1); assert.equal(fakeFrameSync.notifyHost.callCount, 1);
assert.calledWith( assert.calledWith(fakeFrameSync.notifyHost, 'profileRequested');
fakeFrameSync.notifyHost,
bridgeEvents.PROFILE_REQUESTED
);
}); });
it('should not fire profile event for first-party user', () => { it('should not fire profile event for first-party user', () => {
......
import bridgeEvents from '../../shared/bridge-events';
import { watch } from '../util/watch'; import { watch } from '../util/watch';
/** /**
...@@ -26,10 +25,7 @@ export class FeaturesService { ...@@ -26,10 +25,7 @@ export class FeaturesService {
init() { init() {
const currentFlags = () => this._store.profile().features; const currentFlags = () => this._store.profile().features;
const sendFeatureFlags = () => { const sendFeatureFlags = () => {
this._frameSync.notifyHost( this._frameSync.notifyHost('featureFlagsUpdated', currentFlags() || {});
bridgeEvents.FEATURE_FLAGS_UPDATED,
currentFlags() || {}
);
}; };
// Re-send feature flags to connected frames when flags change or a new // Re-send feature flags to connected frames when flags change or a new
......
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
import bridgeEvents from '../../shared/bridge-events';
import { Bridge } from '../../shared/bridge'; import { Bridge } from '../../shared/bridge';
import { isReply, isPublic } from '../helpers/annotation-metadata'; import { isReply, isPublic } from '../helpers/annotation-metadata';
import { watch } from '../util/watch'; import { watch } from '../util/watch';
/** /**
* @typedef {import('../../types/bridge-events').SidebarToHostEvent} SidebarToHostEvent
* @typedef {import('../../types/bridge-events').HostToSidebarEvent} HostToSidebarEvent
* @typedef {import('../../types/bridge-events').SidebarToGuestEvent} SidebarToGuestEvent
* @typedef {import('../../types/bridge-events').GuestToSidebarEvent} GuestToSidebarEvent
* @typedef {import('../../shared/port-rpc').PortRPC} PortRPC * @typedef {import('../../shared/port-rpc').PortRPC} PortRPC
*/ */
...@@ -55,10 +58,18 @@ export class FrameSyncService { ...@@ -55,10 +58,18 @@ export class FrameSyncService {
* @param {import('../store').SidebarStore} store * @param {import('../store').SidebarStore} store
*/ */
constructor($window, annotationsService, store) { constructor($window, annotationsService, store) {
/** Channel for sidebar <-> host communication. */ /**
* Channel for sidebar-host communication.
*
* @type {Bridge<SidebarToHostEvent,HostToSidebarEvent>}
*/
this._hostRPC = new Bridge(); this._hostRPC = new Bridge();
/** Channel for sidebar <-> guest(s) communication. */ /**
* Channel for sidebar-guest(s) communication.
*
* @type {Bridge<SidebarToGuestEvent,GuestToSidebarEvent>}
*/
this._guestRPC = new Bridge(); this._guestRPC = new Bridge();
this._store = store; this._store = store;
...@@ -118,10 +129,7 @@ export class FrameSyncService { ...@@ -118,10 +129,7 @@ export class FrameSyncService {
if (frames.length > 0) { if (frames.length > 0) {
if (frames.every(frame => frame.isAnnotationFetchComplete)) { if (frames.every(frame => frame.isAnnotationFetchComplete)) {
if (publicAnns === 0 || publicAnns !== prevPublicAnns) { if (publicAnns === 0 || publicAnns !== prevPublicAnns) {
this._hostRPC.call( this._hostRPC.call('publicAnnotationCountChanged', publicAnns);
bridgeEvents.PUBLIC_ANNOTATION_COUNT_CHANGED,
publicAnns
);
prevPublicAnns = publicAnns; prevPublicAnns = publicAnns;
} }
} }
...@@ -288,7 +296,7 @@ export class FrameSyncService { ...@@ -288,7 +296,7 @@ export class FrameSyncService {
/** /**
* Send an RPC message to the host frame. * Send an RPC message to the host frame.
* *
* @param {string} method * @param {SidebarToHostEvent} method
* @param {any[]} args * @param {any[]} args
*/ */
notifyHost(method, ...args) { notifyHost(method, ...args) {
......
import bridgeEvents from '../../../shared/bridge-events';
import { FeaturesService } from '../features'; import { FeaturesService } from '../features';
describe('FeaturesService', () => { describe('FeaturesService', () => {
...@@ -52,7 +51,7 @@ describe('FeaturesService', () => { ...@@ -52,7 +51,7 @@ describe('FeaturesService', () => {
assert.calledWith( assert.calledWith(
fakeFrameSync.notifyHost, fakeFrameSync.notifyHost,
bridgeEvents.FEATURE_FLAGS_UPDATED, 'featureFlagsUpdated',
fakeStore.profile().features fakeStore.profile().features
); );
}); });
...@@ -71,7 +70,7 @@ describe('FeaturesService', () => { ...@@ -71,7 +70,7 @@ describe('FeaturesService', () => {
assert.calledWith( assert.calledWith(
fakeFrameSync.notifyHost, fakeFrameSync.notifyHost,
bridgeEvents.FEATURE_FLAGS_UPDATED, 'featureFlagsUpdated',
fakeStore.profile().features fakeStore.profile().features
); );
}); });
......
/**
* This module defines the set of global events that are dispatched across the
* the bridge(s) between the sidebar-host and sidebar-guest(s).
*/
/**
* Events that the host sends to the sidebar
*/
export type HostToSidebarEvent =
/**
* The host is asking the sidebar to delete a frame.
*/
| 'destroyFrame'
/**
* The host is asking the sidebar to set the annotation highlights on/off.
*/
| 'setVisibleHighlights'
/**
* The host informs the sidebar that the sidebar has been opened.
*/
| 'sidebarOpened';
/**
* Events that the guest sends to the sidebar
*/
export type GuestToSidebarEvent =
/**
* The guest is asking the sidebar to create an annotation.
*/
| 'beforeCreateAnnotation'
/**
* The guest is asking the sidebar to relay the message to open the sidebar.
*/
| 'closeSidebar'
/**
* The guest is asking the sidebar to focus on certain annotations.
*/
| 'focusAnnotations'
/**
* The guest is asking the sidebar to relay the message to open the sidebar.
*/
| 'openSidebar'
/**
* The guest is asking the sidebar to display some annotations.
*/
| 'showAnnotations'
/**
* The guest notifies the sidebar to synchronize about the anchoring status of annotations.
*/
| 'sync'
/**
* The guest is asking the sidebar to toggle some annotations.
*/
| 'toggleAnnotationSelection';
/**
* Events that the sidebar sends to the guest(s)
*/
export type SidebarToGuestEvent =
/**
* The sidebar is asking the guest(s) to delete an annotation.
*/
| 'deleteAnnotation'
/**
* The sidebar is asking the guest(s) to focus on certain annotations.
*/
| 'focusAnnotations'
/**
* The sidebar is asking the guest(s) to get the document metadata.
*/
| 'getDocumentInfo'
/**
* The sidebar is asking the guest(s) to load annotations.
*/
| 'loadAnnotations'
/**
* The sidebar is asking the guest(s) to scroll to certain annotation.
*/
| 'scrollToAnnotation'
/**
* The sidebar relays to the guest(s) to set the annotation highlights on/off.
*/
| 'setVisibleHighlights';
/**
* Events that the sidebar sends to the host
*/
export type SidebarToHostEvent =
/**
* The sidebar relays to the host to open the sidebar.
*/
| 'closeSidebar'
/**
* The updated feature flags for the user
*/
| 'featureFlagsUpdated'
/**
* The sidebar is asking the host to open the partner site help page.
*/
| 'helpRequested'
/**
* The sidebar is asking the host to do a partner site log in
* (for example pop up a log in window). This is used when the client is
* embedded in a partner site and a log in button in the client is clicked.
*/
| 'loginRequested'
/**
* The sidebar is asking the host to do a partner site log out.
* This is used when the client is embedded in a partner site and a log out
* button in the client is clicked.
*/
| 'logoutRequested'
/**
* The sidebar is asking the host to open the notebook.
*/
| 'openNotebook'
/**
* The sidebar is asking the host to open the sidebar (side-effect of
* creating an annotation).
*/
| 'openSidebar'
/**
* The sidebar is asking the host to open the partner site profile page.
*/
| 'profileRequested'
/**
* The sidebar inform the host to update the number of annotations in the partner site.
*/
| 'publicAnnotationCountChanged'
/**
* The sidebar is asking the host to do a partner site sign-up.
*/
| 'signupRequested';
export type BridgeEvent =
| HostToSidebarEvent
| GuestToSidebarEvent
| SidebarToGuestEvent
| SidebarToHostEvent;
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