import type {ComponentProps} from 'react'; import {useEffect, useRef, useState} from 'react'; import styled from '@emotion/styled'; import {Button, LinkButton} from 'sentry/components/button'; import ErrorBoundary from 'sentry/components/errorBoundary'; import Panel from 'sentry/components/panels/panel'; import {useReplayContext} from 'sentry/components/replays/replayContext'; import ReplayCurrentScreen from 'sentry/components/replays/replayCurrentScreen'; import ReplayCurrentUrl from 'sentry/components/replays/replayCurrentUrl'; import {ReplayFullscreenButton} from 'sentry/components/replays/replayFullscreenButton'; import ReplayPlayer from 'sentry/components/replays/replayPlayer'; import ReplayPlayPauseButton from 'sentry/components/replays/replayPlayPauseButton'; import {ReplaySidebarToggleButton} from 'sentry/components/replays/replaySidebarToggleButton'; import TimeAndScrubberGrid from 'sentry/components/replays/timeAndScrubberGrid'; import {IconNext, IconPrevious} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import EventView from 'sentry/utils/discover/eventView'; import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes'; import {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab'; import useMarkReplayViewed from 'sentry/utils/replays/hooks/useMarkReplayViewed'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import {useRoutes} from 'sentry/utils/useRoutes'; import useFullscreen from 'sentry/utils/window/useFullscreen'; import useIsFullscreen from 'sentry/utils/window/useIsFullscreen'; import {normalizeUrl} from 'sentry/utils/withDomainRequired'; import Breadcrumbs from 'sentry/views/replays/detail/breadcrumbs'; import BrowserOSIcons from 'sentry/views/replays/detail/browserOSIcons'; import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight'; import {ReplayCell} from 'sentry/views/replays/replayTable/tableCell'; import type {ReplayRecord} from 'sentry/views/replays/types'; function ReplayPreviewPlayer({ replayId, fullReplayButtonProps, replayRecord, handleBackClick, handleForwardClick, overlayContent, showNextAndPrevious, playPausePriority, }: { replayId: string; replayRecord: ReplayRecord; fullReplayButtonProps?: Partial<ComponentProps<typeof LinkButton>>; handleBackClick?: () => void; handleForwardClick?: () => void; overlayContent?: React.ReactNode; playPausePriority?: ComponentProps<typeof ReplayPlayPauseButton>['priority']; showNextAndPrevious?: boolean; }) { const routes = useRoutes(); const location = useLocation(); const organization = useOrganization(); const [isSidebarOpen, setIsSidebarOpen] = useState(true); const {replay, currentTime, isFetching, isFinished, isPlaying, isVideoReplay} = useReplayContext(); const eventView = EventView.fromLocation(location); const fullscreenRef = useRef(null); const {toggle: toggleFullscreen} = useFullscreen({ elementRef: fullscreenRef, }); const isFullscreen = useIsFullscreen(); const startOffsetMs = replay?.getStartOffsetMs() ?? 0; const referrer = getRouteStringFromRoutes(routes); const fromFeedback = referrer === '/feedback/'; const fullReplayUrl = { pathname: normalizeUrl(`/organizations/${organization.slug}/replays/${replayId}/`), query: { referrer: getRouteStringFromRoutes(routes), t_main: fromFeedback ? TabKey.BREADCRUMBS : TabKey.ERRORS, t: (currentTime + startOffsetMs) / 1000, f_b_type: fromFeedback ? 'feedback' : undefined, }, }; const {mutate: markAsViewed} = useMarkReplayViewed(); useEffect(() => { if (replayRecord?.id && !replayRecord.has_viewed && !isFetching && isPlaying) { markAsViewed({projectSlug: replayRecord.project_id, replayId: replayRecord.id}); } }, [isFetching, isPlaying, markAsViewed, organization, replayRecord]); return ( <PlayerPanel> <HeaderWrapper> <StyledReplayCell key="session" replay={replayRecord} eventView={eventView} organization={organization} referrer="issue-details-replay-header" /> <LinkButton size="sm" to={fullReplayUrl} {...fullReplayButtonProps}> {t('See Full Replay')} </LinkButton> </HeaderWrapper> <PreviewPlayerContainer ref={fullscreenRef} isSidebarOpen={isSidebarOpen}> <PlayerBreadcrumbContainer> <PlayerContextContainer> {isFullscreen ? ( <ContextContainer> {isVideoReplay ? <ReplayCurrentScreen /> : <ReplayCurrentUrl />} <BrowserOSIcons /> <ReplaySidebarToggleButton isOpen={isSidebarOpen} setIsOpen={setIsSidebarOpen} /> </ContextContainer> ) : null} <StaticPanel> <ReplayPlayer overlayContent={overlayContent} /> </StaticPanel> </PlayerContextContainer> {isFullscreen && isSidebarOpen ? <Breadcrumbs /> : null} </PlayerBreadcrumbContainer> <ErrorBoundary mini> <ButtonGrid> {showNextAndPrevious && ( <Button size="sm" title={t('Previous Clip')} icon={<IconPrevious />} onClick={() => handleBackClick?.()} aria-label={t('Previous Clip')} disabled={!handleBackClick} analyticsEventName="Replay Preview Player: Clicked Previous Clip" analyticsEventKey="replay_preview_player.clicked_previous_clip" /> )} <ReplayPlayPauseButton analyticsEventName="Replay Preview Player: Clicked Play/Plause Clip" analyticsEventKey="replay_preview_player.clicked_play_pause_clip" priority={ playPausePriority ?? (isFinished || isPlaying ? 'primary' : 'default') } /> {showNextAndPrevious && ( <Button size="sm" title={t('Next Clip')} icon={<IconNext />} onClick={() => handleForwardClick?.()} aria-label={t('Next Clip')} disabled={!handleForwardClick} analyticsEventName="Replay Preview Player: Clicked Next Clip" analyticsEventKey="replay_preview_player.clicked_next_clip" /> )} <Container> <TimeAndScrubberGrid /> </Container> <ReplayFullscreenButton toggleFullscreen={toggleFullscreen} /> </ButtonGrid> </ErrorBoundary> </PreviewPlayerContainer> </PlayerPanel> ); } const PlayerPanel = styled(Panel)` padding: ${space(3)} ${space(3)} ${space(1.5)}; margin: 0; display: flex; gap: ${space(1)}; flex-direction: column; flex-grow: 1; overflow: hidden; height: 100%; `; const PlayerBreadcrumbContainer = styled(FluidHeight)` position: relative; `; const PreviewPlayerContainer = styled(FluidHeight)<{isSidebarOpen: boolean}>` gap: ${space(2)}; background: ${p => p.theme.background}; :fullscreen { padding: ${space(1)}; ${PlayerBreadcrumbContainer} { display: grid; grid-template-columns: ${p => (p.isSidebarOpen ? '1fr 25%' : '1fr')}; height: 100%; gap: ${space(1)}; } } `; const PlayerContextContainer = styled(FluidHeight)` display: flex; flex-direction: column; gap: ${space(1)}; `; const StaticPanel = styled(FluidHeight)` border: 1px solid ${p => p.theme.border}; border-radius: ${p => p.theme.borderRadius}; `; const ButtonGrid = styled('div')` display: flex; align-items: center; gap: 0 ${space(1)}; flex-direction: row; justify-content: space-between; `; const Container = styled('div')` display: flex; flex-direction: column; flex: 1 1; justify-content: center; `; const ContextContainer = styled('div')` display: grid; grid-auto-flow: column; grid-template-columns: 1fr max-content max-content; align-items: center; gap: ${space(1)}; `; const StyledReplayCell = styled(ReplayCell)` padding: 0 0 ${space(1)}; `; const HeaderWrapper = styled('div')` display: flex; justify-content: space-between; align-items: center; margin-bottom: ${space(1)}; `; export default ReplayPreviewPlayer;