Просмотр исходного кода

feat(replays): Adds restart functionality to replays (#36193)

Changes
- Adds isFinished boolean to replay context
- Adds restart function to replay context
- Updates UI to replace play/pause button with restart button when 'isFinished'
Notes
- This resolves a bug where you can keep hitting play after the video has ended and the current time keeps increasing and breaking the UI.

Closes #36139
Dane Grant 2 лет назад
Родитель
Сommit
fdd371f359

+ 31 - 5
static/app/components/replays/replayContext.tsx

@@ -73,6 +73,11 @@ type ReplayPlayerContextProps = {
    */
   isBuffering: boolean;
 
+  /**
+   * Set to true when the replay finish event is fired
+   */
+  isFinished: boolean;
+
   /**
    * Whether the video is currently playing
    */
@@ -93,6 +98,11 @@ type ReplayPlayerContextProps = {
    */
   replay: ReplayReader | null;
 
+  /**
+   * Restart the replay
+   */
+  restart: () => void;
+
   /**
    * Set the currentHoverTime so collaborating components can highlight related
    * information
@@ -139,10 +149,12 @@ const ReplayPlayerContext = React.createContext<ReplayPlayerContextProps>({
   highlight: () => {},
   initRoot: () => {},
   isBuffering: false,
+  isFinished: false,
   isPlaying: false,
   isSkippingInactive: false,
   removeHighlight: () => {},
   replay: null,
+  restart: () => {},
   setCurrentHoverTime: () => {},
   setCurrentTime: () => {},
   setSpeed: () => {},
@@ -183,18 +195,18 @@ export function Provider({children, replay, initialTimeOffset = 0, value = {}}:
   const [dimensions, setDimensions] = useState<Dimensions>({height: 0, width: 0});
   const [currentHoverTime, setCurrentHoverTime] = useState<undefined | number>();
   const [isPlaying, setIsPlaying] = useState(false);
+  const [finishedAtMS, setFinishedAtMS] = useState<number>(-1);
   const [isSkippingInactive, setIsSkippingInactive] = useState(false);
   const [speed, setSpeedState] = useState(1);
   const [fastForwardSpeed, setFFSpeed] = useState(0);
   const [buffer, setBufferTime] = useState({target: -1, previous: -1});
   const playTimer = useRef<number | undefined>(undefined);
 
+  const isFinished = replayerRef.current?.getCurrentTime() === finishedAtMS;
+
   const forceDimensions = (dimension: Dimensions) => {
     setDimensions(dimension);
   };
-  const setPlayingFalse = () => {
-    setIsPlaying(false);
-  };
   const onFastForwardStart = (e: {speed: number}) => {
     setFFSpeed(e.speed);
   };
@@ -229,6 +241,11 @@ export function Provider({children, replay, initialTimeOffset = 0, value = {}}:
     removeHighlightedNode({replayer, nodeId});
   }, []);
 
+  const setReplayFinished = useCallback(() => {
+    setFinishedAtMS(replayerRef.current?.getCurrentTime() ?? -1);
+    setIsPlaying(false);
+  }, []);
+
   const initRoot = useCallback(
     (root: RootElem) => {
       if (events === undefined) {
@@ -277,7 +294,7 @@ export function Provider({children, replay, initialTimeOffset = 0, value = {}}:
 
       // @ts-expect-error: rrweb types event handlers with `unknown` parameters
       inst.on(ReplayerEvents.Resize, forceDimensions);
-      inst.on(ReplayerEvents.Finish, setPlayingFalse);
+      inst.on(ReplayerEvents.Finish, setReplayFinished);
       // @ts-expect-error: rrweb types event handlers with `unknown` parameters
       inst.on(ReplayerEvents.SkipStart, onFastForwardStart);
       inst.on(ReplayerEvents.SkipEnd, onFastForwardEnd);
@@ -288,7 +305,7 @@ export function Provider({children, replay, initialTimeOffset = 0, value = {}}:
       // @ts-expect-error
       replayerRef.current = inst;
     },
-    [events, theme.purple200, hasNewEvents]
+    [events, theme.purple200, hasNewEvents, setReplayFinished]
   );
 
   useEffect(() => {
@@ -379,6 +396,13 @@ export function Provider({children, replay, initialTimeOffset = 0, value = {}}:
     [getCurrentTime]
   );
 
+  const restart = useCallback(() => {
+    if (replayerRef.current) {
+      replayerRef.current.play(0);
+      setIsPlaying(true);
+    }
+  }, []);
+
   const toggleSkipInactive = useCallback((skip: boolean) => {
     const replayer = replayerRef.current;
     if (!replayer) {
@@ -425,10 +449,12 @@ export function Provider({children, replay, initialTimeOffset = 0, value = {}}:
         highlight,
         initRoot,
         isBuffering,
+        isFinished,
         isPlaying,
         isSkippingInactive,
         removeHighlight,
         replay,
+        restart,
         setCurrentHoverTime,
         setCurrentTime,
         setSpeed,

+ 27 - 9
static/app/components/replays/replayController.tsx

@@ -12,6 +12,7 @@ import {
   IconNext,
   IconPause,
   IconPlay,
+  IconPrevious,
   IconRefresh,
   IconResize,
 } from 'sentry/icons';
@@ -37,8 +38,15 @@ interface Props {
 }
 
 function ReplayPlayPauseBar() {
-  const {currentTime, isPlaying, replay, setCurrentTime, togglePlayPause} =
-    useReplayContext();
+  const {
+    currentTime,
+    isFinished,
+    isPlaying,
+    replay,
+    restart,
+    setCurrentTime,
+    togglePlayPause,
+  } = useReplayContext();
 
   return (
     <ButtonBar merged>
@@ -49,13 +57,23 @@ function ReplayPlayPauseBar() {
         onClick={() => setCurrentTime(currentTime - 10 * SECOND)}
         aria-label={t('Go back 10 seconds')}
       />
-      <Button
-        size="xsmall"
-        title={isPlaying ? t('Pause the Replay') : t('Play the Replay')}
-        icon={isPlaying ? <IconPause size="sm" /> : <IconPlay size="sm" />}
-        onClick={() => togglePlayPause(!isPlaying)}
-        aria-label={isPlaying ? t('Pause the Replay') : t('Play the Replay')}
-      />
+      {isFinished ? (
+        <Button
+          size="xsmall"
+          title={t('Restart Replay')}
+          icon={<IconPrevious size="sm" />}
+          onClick={restart}
+          aria-label={t('Restart the Replay')}
+        />
+      ) : (
+        <Button
+          size="xsmall"
+          title={isPlaying ? t('Pause the Replay') : t('Play the Replay')}
+          icon={isPlaying ? <IconPause size="sm" /> : <IconPlay size="sm" />}
+          onClick={() => togglePlayPause(!isPlaying)}
+          aria-label={isPlaying ? t('Pause the Replay') : t('Play the Replay')}
+        />
+      )}
       <Button
         size="xsmall"
         title={t('Jump to next event')}