import * as React from 'react';
import {useContext, useEffect} from 'react';
import {BridgeContext} from '../context';
import {TreeDispatcherContext} from '../Components/TreeContext';
import {useScrollToHostInstance} from '../hooks';
import {
SuspenseTreeDispatcherContext,
SuspenseTreeStateContext,
} from './SuspenseTreeContext';
import styles from './SuspenseTimeline.css';
import SuspenseScrubber from './SuspenseScrubber';
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
function SuspenseTimelineInput() {
const bridge = useContext(BridgeContext);
const treeDispatch = useContext(TreeDispatcherContext);
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
const scrollToHostInstance = useScrollToHostInstance();
const {timeline, timelineIndex, hoveredTimelineIndex, playing, autoScroll} =
useContext(SuspenseTreeStateContext);
const min = 0;
const max = timeline.length > 0 ? timeline.length - 1 : 0;
function switchSuspenseNode(nextTimelineIndex: number) {
const nextSelectedSuspenseID = timeline[nextTimelineIndex].id;
treeDispatch({
type: 'SELECT_ELEMENT_BY_ID',
payload: nextSelectedSuspenseID,
});
suspenseTreeDispatch({
type: 'SUSPENSE_SET_TIMELINE_INDEX',
payload: nextTimelineIndex,
});
}
function handleChange(pendingTimelineIndex: number) {
switchSuspenseNode(pendingTimelineIndex);
}
function handleFocus() {
switchSuspenseNode(timelineIndex);
}
function handleHoverSegment(hoveredIndex: number) {
const nextSelectedSuspenseID = timeline[hoveredIndex].id;
suspenseTreeDispatch({
type: 'HOVER_TIMELINE_FOR_ID',
payload: nextSelectedSuspenseID,
});
}
function handleUnhoverSegment() {
suspenseTreeDispatch({
type: 'HOVER_TIMELINE_FOR_ID',
payload: -1,
});
}
function skipPrevious() {
const nextSelectedSuspenseID = timeline[timelineIndex - 1].id;
treeDispatch({
type: 'SELECT_ELEMENT_BY_ID',
payload: nextSelectedSuspenseID,
});
suspenseTreeDispatch({
type: 'SUSPENSE_SKIP_TIMELINE_INDEX',
payload: false,
});
}
function skipForward() {
const nextSelectedSuspenseID = timeline[timelineIndex + 1].id;
treeDispatch({
type: 'SELECT_ELEMENT_BY_ID',
payload: nextSelectedSuspenseID,
});
suspenseTreeDispatch({
type: 'SUSPENSE_SKIP_TIMELINE_INDEX',
payload: true,
});
}
function togglePlaying() {
suspenseTreeDispatch({
type: 'SUSPENSE_PLAY_PAUSE',
payload: 'toggle',
});
}
function changeTimelineIndex(newIndex: number) {
const suspendedSet = timeline.slice(timelineIndex + 1).map(step => step.id);
bridge.send('overrideSuspenseMilestone', {
suspendedSet,
});
}
useEffect(() => {
changeTimelineIndex(timelineIndex);
}, [timelineIndex]);
useEffect(() => {
if (autoScroll.id > 0) {
const scrollToId = autoScroll.id;
autoScroll.id = 0;
scrollToHostInstance(scrollToId);
}
}, [autoScroll]);
useEffect(() => {
if (!playing) {
return undefined;
}
const PLAY_SPEED_INTERVAL = 1000;
const timer = setInterval(() => {
suspenseTreeDispatch({
type: 'SUSPENSE_PLAY_TICK',
});
}, PLAY_SPEED_INTERVAL);
return () => {
clearInterval(timer);
};
}, [playing]);
if (timeline.length === 0) {
return (
<div className={styles.SuspenseTimelineInput}>
Root contains no Suspense nodes.
</div>
);
}
return (
<>
<Button
disabled={timelineIndex === 0}
title={'Previous'}
onClick={skipPrevious}>
<ButtonIcon type={'skip-previous'} />
</Button>
<Button
disabled={max === 0 && !playing}
title={playing ? 'Pause' : 'Play'}
onClick={togglePlaying}>
<ButtonIcon type={playing ? 'pause' : 'play'} />
</Button>
<Button
disabled={timelineIndex === max}
title={'Next'}
onClick={skipForward}>
<ButtonIcon type={'skip-next'} />
</Button>
<div className={styles.SuspenseTimelineInput}>
<SuspenseScrubber
min={min}
max={max}
timeline={timeline}
value={timelineIndex}
highlight={hoveredTimelineIndex}
onChange={handleChange}
onFocus={handleFocus}
onHoverSegment={handleHoverSegment}
onHoverLeave={handleUnhoverSegment}
/>
</div>
</>
);
}
export default function SuspenseTimeline(): React$Node {
return (
<div className={styles.SuspenseTimelineContainer}>
<SuspenseTimelineInput />
</div>
);
}