Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe('elements/common/annotator-context/withAnnotations', () => {
onAnnotator: jest.fn(),
onError: jest.fn(),
onPreviewDestroy: jest.fn(),
onViewer: jest.fn(),
};
const MockComponent = (props: ComponentProps) => <div {...props} />;
const WrappedComponent = withAnnotations(MockComponent);
Expand Down
13 changes: 13 additions & 0 deletions src/elements/common/annotator-context/types.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ export interface Annotator {
listener: (...args: any[]) => void
) => void;
}
export interface TimelineMarker {
id: string;
timestampMs: number;
type: "comment" | "annotation";
}
export interface TimelineMarkerClickPayload {
id: string;
timestampMs?: number;
type?: string;
}
export type TimelineMarkerClickHandler = (payload: TimelineMarkerClickPayload) => void;
export interface AnnotatorState {
activeAnnotationFileVersionId?: string | null;
activeAnnotationId?: string | null;
Expand All @@ -52,6 +63,7 @@ export interface AnnotatorState {
}
export type GetMatchPath = (location?: Location) => match<MatchParams> | null;
export interface AnnotatorContext {
addTimelineMarkerClickListener?: (handler: TimelineMarkerClickHandler) => () => void;
emitActiveAnnotationChangeEvent?: (id: string) => void;
emitAnnotationRemoveEvent?: (id: string, isStartEvent?: boolean) => void;
emitAnnotationReplyCreateEvent?: (
Expand All @@ -76,6 +88,7 @@ export interface AnnotatorContext {
) => void;
getAnnotationsMatchPath?: GetMatchPath;
getAnnotationsPath?: (fileVersionId?: string, annotationId?: string) => string;
setTimelineMarkers?: (markers: TimelineMarker[]) => void;
state: AnnotatorState;
}
declare export var Status: {|
Expand Down
22 changes: 22 additions & 0 deletions src/elements/common/annotator-context/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ export interface Annotator {
}
/* eslint-enable @typescript-eslint/no-explicit-any */

export interface TimelineMarker {
id: string;
timestampMs: number;
type: 'comment' | 'annotation';
}

export interface TimelineMarkerClickPayload {
id: string;
timestampMs?: number;
type?: string;
}

export type TimelineMarkerClickHandler = (payload: TimelineMarkerClickPayload) => void;

export interface AnnotatorState {
activeAnnotationFileVersionId?: string | null;
activeAnnotationId?: string | null;
Expand All @@ -41,7 +55,14 @@ export interface AnnotatorState {

export type GetMatchPath = (location?: Location) => match<MatchParams> | null;

// Bridges the imperative box-annotations Annotator into the React tree below.
// Also exposes timeline-marker hooks (setTimelineMarkers /
// addTimelineMarkerClickListener) which talk to the box-content-preview viewer
// through window-level CustomEvents — no direct viewer reference is held by
// the host. Both surfaces share this single provider since they share
// ContentPreview's lifecycle.
export interface AnnotatorContext {
addTimelineMarkerClickListener?: (handler: TimelineMarkerClickHandler) => () => void;
emitActiveAnnotationChangeEvent?: (id: string) => void;
emitAnnotationRemoveEvent?: (id: string, isStartEvent?: boolean) => void;
emitAnnotationReplyCreateEvent?: (
Expand All @@ -55,6 +76,7 @@ export interface AnnotatorContext {
emitAnnotationUpdateEvent?: (annotation: Object, isStartEvent?: boolean) => void;
getAnnotationsMatchPath?: GetMatchPath;
getAnnotationsPath?: (fileVersionId?: string, annotationId?: string) => string;
setTimelineMarkers?: (markers: TimelineMarker[]) => void;
state: AnnotatorState;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import {
AnnotatorState,
GetMatchPath,
MatchParams,
Status
Status,
TimelineMarker,
TimelineMarkerClickHandler
} from "./types";
import { SidebarNavigation } from '../types/SidebarNavigation';
import { type FeatureConfig } from '../feature-checking';
Expand All @@ -29,6 +31,7 @@ export type ActiveChangeEvent = {
};
export type ActiveChangeEventHandler = (event: ActiveChangeEvent) => void;
export type ComponentWithAnnotations = {
addTimelineMarkerClickListener: (handler: TimelineMarkerClickHandler) => () => void,
emitActiveAnnotationChangeEvent: (id: string | null) => void,
emitAnnotationRemoveEvent: (id: string, isStartEvent?: boolean) => void,
emitAnnotationReplyCreateEvent: (
Expand Down Expand Up @@ -71,6 +74,7 @@ export type ComponentWithAnnotations = {
handleAnnotationUpdate: (eventData: AnnotationActionEvent) => void,
handleAnnotator: (annotator: Annotator) => void,
handlePreviewDestroy: (shouldReset?: boolean) => void,
setTimelineMarkers: (markers: TimelineMarker[]) => void,
...
};
export type WithAnnotationsProps = {
Expand Down
73 changes: 72 additions & 1 deletion src/elements/common/annotator-context/withAnnotations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@ import { generatePath, match as matchType, matchPath } from 'react-router-dom';
import { Location } from 'history';
import AnnotatorContext from './AnnotatorContext';
import { isFeatureEnabled, type FeatureConfig } from '../feature-checking';
import { Action, Annotator, AnnotationActionEvent, AnnotatorState, GetMatchPath, MatchParams, Status } from './types';
import {
Action,
Annotator,
AnnotationActionEvent,
AnnotatorState,
GetMatchPath,
MatchParams,
Status,
TimelineMarker,
TimelineMarkerClickHandler,
} from './types';
import { FeedEntryType, SidebarNavigation } from '../types/SidebarNavigation';

export type ActiveChangeEvent = {
Expand All @@ -15,6 +25,7 @@ export type ActiveChangeEvent = {
export type ActiveChangeEventHandler = (event: ActiveChangeEvent) => void;

export type ComponentWithAnnotations = {
addTimelineMarkerClickListener: (handler: TimelineMarkerClickHandler) => () => void;
emitActiveAnnotationChangeEvent: (id: string | null) => void;
emitAnnotationRemoveEvent: (id: string, isStartEvent?: boolean) => void;
emitAnnotationReplyCreateEvent: (
Expand All @@ -40,6 +51,7 @@ export type ComponentWithAnnotations = {
handleAnnotationUpdate: (eventData: AnnotationActionEvent) => void;
handleAnnotator: (annotator: Annotator) => void;
handlePreviewDestroy: (shouldReset?: boolean) => void;
setTimelineMarkers: (markers: TimelineMarker[]) => void;
};

export type WithAnnotationsProps = {
Expand Down Expand Up @@ -73,6 +85,12 @@ export default function withAnnotations<P extends object>(

annotator: Annotator | null = null;

// Cached so we can replay markers onto a viewer that arrives after the
// feed has rendered (e.g. file navigation while sidebar stays mounted).
// The SDK dispatches `bp:timeline_markers_ready` when its listener is in
// place; we re-emit the cached list in response.
lastTimelineMarkers: TimelineMarker[] | null = null;

constructor(props: P & WithAnnotationsProps) {
super(props);

Expand Down Expand Up @@ -101,6 +119,27 @@ export default function withAnnotations<P extends object>(
this.state = { ...defaultState, activeAnnotationId };
}

componentDidMount(): void {
if (typeof window !== 'undefined') {
window.addEventListener('bp:timeline_markers_ready', this.handleTimelineMarkersReady);
}
}

componentWillUnmount(): void {
if (typeof window !== 'undefined') {
window.removeEventListener('bp:timeline_markers_ready', this.handleTimelineMarkersReady);
}
}

handleTimelineMarkersReady = (): void => {
// The viewer just attached its listener; replay whatever the host
// last pushed so the scrubber is correct without the host having to
// re-derive on the next feed mutation.
if (this.lastTimelineMarkers) {
this.setTimelineMarkers(this.lastTimelineMarkers);
}
};

emitActiveAnnotationChangeEvent = (id: string | null) => {
const { annotator } = this;

Expand Down Expand Up @@ -338,6 +377,36 @@ export default function withAnnotations<P extends object>(
}

this.annotator = null;
this.lastTimelineMarkers = null;
};

// Pushes markers into the SDK via a window-level CustomEvent. The viewer
// listens on `bp:timeline_markers_update`; we never hold a reference to
// the viewer instance. Cached so the ready handler can replay onto a
// viewer that mounts after the first push.
setTimelineMarkers = (markers: TimelineMarker[]): void => {
this.lastTimelineMarkers = markers;
if (typeof window !== 'undefined' && typeof window.CustomEvent === 'function') {
window.dispatchEvent(new window.CustomEvent('bp:timeline_markers_update', { detail: markers }));
}
};

// Subscribes the host to viewer-emitted marker clicks. The SDK dispatches
// `bp:timeline_marker_click` on the window; we wrap the handler to unbox
// the CustomEvent's detail so consumers see a plain payload object.
addTimelineMarkerClickListener = (handler: TimelineMarkerClickHandler): (() => void) => {
const noopUnsubscribe = (): void => undefined;
if (typeof window === 'undefined') {
return noopUnsubscribe;
}
const wrapped = (event: Event): void => {
const { detail } = event as CustomEvent;
if (detail && typeof detail.id === 'string') {
handler(detail);
}
};
window.addEventListener('bp:timeline_marker_click', wrapped);
return () => window.removeEventListener('bp:timeline_marker_click', wrapped);
};

render(): JSX.Element {
Expand All @@ -352,12 +421,14 @@ export default function withAnnotations<P extends object>(
return (
<AnnotatorContext.Provider
value={{
addTimelineMarkerClickListener: this.addTimelineMarkerClickListener,
emitActiveAnnotationChangeEvent: this.emitActiveAnnotationChangeEvent,
emitAnnotationRemoveEvent: this.emitAnnotationRemoveEvent,
emitAnnotationReplyCreateEvent: this.emitAnnotationReplyCreateEvent,
emitAnnotationReplyDeleteEvent: this.emitAnnotationReplyDeleteEvent,
emitAnnotationReplyUpdateEvent: this.emitAnnotationReplyUpdateEvent,
emitAnnotationUpdateEvent: this.emitAnnotationUpdateEvent,
setTimelineMarkers: this.setTimelineMarkers,
...annotationsRouterProps,
state: this.state,
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@

import * as React from "react";
import AnnotatorContext from "./AnnotatorContext";
import { AnnotatorState, GetMatchPath } from "./types";
import { AnnotatorState, GetMatchPath, TimelineMarker, TimelineMarkerClickHandler } from "./types";

export interface WithAnnotatorContextProps {
addTimelineMarkerClickListener?: (handler: TimelineMarkerClickHandler) => () => void;
annotatorState?: AnnotatorState;
emitActiveAnnotationChangeEvent?: (id: string) => void;
emitAnnotationRemoveEvent?: (id: string, isStartEvent?: boolean) => void;
Expand Down Expand Up @@ -37,6 +38,7 @@ export interface WithAnnotatorContextProps {
fileVersionId?: string,
annotationId?: string
) => string;
setTimelineMarkers?: (markers: TimelineMarker[]) => void;
}
declare export default function withAnnotatorContext<P: { ... }>(
WrappedComponent: React.ComponentType<P>
Expand Down
12 changes: 11 additions & 1 deletion src/elements/common/annotator-context/withAnnotatorContext.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as React from 'react';
import AnnotatorContext from './AnnotatorContext';
import { isFeatureEnabled, type FeatureConfig } from '../feature-checking';
import { AnnotatorState, GetMatchPath } from './types';
import { AnnotatorState, GetMatchPath, TimelineMarker, TimelineMarkerClickHandler } from './types';

export interface WithAnnotatorContextProps {
addTimelineMarkerClickListener?: (handler: TimelineMarkerClickHandler) => () => void;
annotatorState?: AnnotatorState;
emitActiveAnnotationChangeEvent?: (id: string) => void;
emitAnnotationRemoveEvent?: (id: string, isStartEvent?: boolean) => void;
Expand All @@ -18,6 +19,7 @@ export interface WithAnnotatorContextProps {
emitAnnotationUpdateEvent?: (annotation: Object, isStartEvent?: boolean) => void;
getAnnotationsMatchPath?: GetMatchPath;
getAnnotationsPath?: (fileVersionId?: string, annotationId?: string) => string;
setTimelineMarkers?: (markers: TimelineMarker[]) => void;
}

export default function withAnnotatorContext<P extends {}>(WrappedComponent: React.ComponentType<P>) {
Expand All @@ -27,24 +29,28 @@ export default function withAnnotatorContext<P extends {}>(WrappedComponent: Rea
return (
<AnnotatorContext.Consumer>
{({
addTimelineMarkerClickListener,
emitActiveAnnotationChangeEvent,
emitAnnotationRemoveEvent,
emitAnnotationReplyCreateEvent,
emitAnnotationReplyDeleteEvent,
emitAnnotationReplyUpdateEvent,
emitAnnotationUpdateEvent,
setTimelineMarkers,
state,
}) => (
<WrappedComponent
ref={ref}
{...props}
addTimelineMarkerClickListener={addTimelineMarkerClickListener}
annotatorState={state}
emitActiveAnnotationChangeEvent={emitActiveAnnotationChangeEvent}
emitAnnotationRemoveEvent={emitAnnotationRemoveEvent}
emitAnnotationReplyCreateEvent={emitAnnotationReplyCreateEvent}
emitAnnotationReplyDeleteEvent={emitAnnotationReplyDeleteEvent}
emitAnnotationReplyUpdateEvent={emitAnnotationReplyUpdateEvent}
emitAnnotationUpdateEvent={emitAnnotationUpdateEvent}
setTimelineMarkers={setTimelineMarkers}
/>
)}
</AnnotatorContext.Consumer>
Expand All @@ -53,6 +59,7 @@ export default function withAnnotatorContext<P extends {}>(WrappedComponent: Rea
return (
<AnnotatorContext.Consumer>
{({
addTimelineMarkerClickListener,
emitActiveAnnotationChangeEvent,
emitAnnotationRemoveEvent,
emitAnnotationReplyCreateEvent,
Expand All @@ -61,11 +68,13 @@ export default function withAnnotatorContext<P extends {}>(WrappedComponent: Rea
emitAnnotationUpdateEvent,
getAnnotationsMatchPath,
getAnnotationsPath,
setTimelineMarkers,
state,
}) => (
<WrappedComponent
ref={ref}
{...props}
addTimelineMarkerClickListener={addTimelineMarkerClickListener}
annotatorState={state}
emitActiveAnnotationChangeEvent={emitActiveAnnotationChangeEvent}
emitAnnotationRemoveEvent={emitAnnotationRemoveEvent}
Expand All @@ -75,6 +84,7 @@ export default function withAnnotatorContext<P extends {}>(WrappedComponent: Rea
emitAnnotationUpdateEvent={emitAnnotationUpdateEvent}
getAnnotationsMatchPath={getAnnotationsMatchPath}
getAnnotationsPath={getAnnotationsPath}
setTimelineMarkers={setTimelineMarkers}
/>
)}
</AnnotatorContext.Consumer>
Expand Down
Loading
Loading