diff --git a/frontend/__snapshots__/replay-player-success--recent-recordings--dark.png b/frontend/__snapshots__/replay-player-success--recent-recordings--dark.png index 000613096ba38..c02be573f0f34 100644 Binary files a/frontend/__snapshots__/replay-player-success--recent-recordings--dark.png and b/frontend/__snapshots__/replay-player-success--recent-recordings--dark.png differ diff --git a/frontend/__snapshots__/replay-player-success--recent-recordings--light.png b/frontend/__snapshots__/replay-player-success--recent-recordings--light.png index e46ba42f61155..ace7d3debd600 100644 Binary files a/frontend/__snapshots__/replay-player-success--recent-recordings--light.png and b/frontend/__snapshots__/replay-player-success--recent-recordings--light.png differ diff --git a/frontend/__snapshots__/replay-player-success--second-recording-in-list--dark.png b/frontend/__snapshots__/replay-player-success--second-recording-in-list--dark.png index a8faf2bcdfdb3..03870e07340f4 100644 Binary files a/frontend/__snapshots__/replay-player-success--second-recording-in-list--dark.png and b/frontend/__snapshots__/replay-player-success--second-recording-in-list--dark.png differ diff --git a/frontend/__snapshots__/replay-player-success--second-recording-in-list--light.png b/frontend/__snapshots__/replay-player-success--second-recording-in-list--light.png index 1979e48fd60a1..576caf4f434a2 100644 Binary files a/frontend/__snapshots__/replay-player-success--second-recording-in-list--light.png and b/frontend/__snapshots__/replay-player-success--second-recording-in-list--light.png differ diff --git a/frontend/src/scenes/session-recordings/player/PlayerFrameOverlay.tsx b/frontend/src/scenes/session-recordings/player/PlayerFrameOverlay.tsx index 6337aafc995af..c1c68eac5cbc8 100644 --- a/frontend/src/scenes/session-recordings/player/PlayerFrameOverlay.tsx +++ b/frontend/src/scenes/session-recordings/player/PlayerFrameOverlay.tsx @@ -5,14 +5,11 @@ import clsx from 'clsx' import { useActions, useValues } from 'kea' import { IconErrorOutline, IconSync } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { useState } from 'react' import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' import { getCurrentExporterData } from '~/exporter/exporterViewLogic' import { SessionPlayerState } from '~/types' -import { PlayerUpNext } from './PlayerUpNext' - const PlayerFrameOverlayContent = (): JSX.Element | null => { const { currentPlayerState, endReached } = useValues(sessionRecordingPlayerLogic) let content = null @@ -80,26 +77,11 @@ const PlayerFrameOverlayContent = (): JSX.Element | null => { } export function PlayerFrameOverlay(): JSX.Element { - const { playlistLogic } = useValues(sessionRecordingPlayerLogic) const { togglePlayPause } = useActions(sessionRecordingPlayerLogic) - const [interrupted, setInterrupted] = useState(false) - return ( -
setInterrupted(true)} - onMouseOut={() => setInterrupted(false)} - > +
- {playlistLogic ? ( - setInterrupted(false)} - /> - ) : undefined}
) } diff --git a/frontend/src/scenes/session-recordings/player/PlayerUpNext.scss b/frontend/src/scenes/session-recordings/player/PlayerUpNext.scss index 1d573607ad8fd..b85d735b176ba 100644 --- a/frontend/src/scenes/session-recordings/player/PlayerUpNext.scss +++ b/frontend/src/scenes/session-recordings/player/PlayerUpNext.scss @@ -1,43 +1,14 @@ .PlayerUpNext { - position: absolute; - right: 1rem; - bottom: 1rem; z-index: 11; transition: 250ms transform ease-out; - - &--enter { - transform: translateY(200%); - } - - &--enter-active, - &--enter-done { - transform: translateY(0%); - } - - &--exit { - transform: translateY(0%); - } - - &--exit-active { - transform: translateY(200%); - } } .PlayerUpNextButton { - position: relative; display: flex; align-items: center; - min-height: 2.5rem; - padding: 0.25rem 0.75rem; overflow: hidden; - font-weight: 600; - line-height: 1.5rem; cursor: pointer; - background-color: rgb(255 255 255 / 75%); backdrop-filter: blur(5px); - border: 1px solid rgb(0 0 0 / 50%); - border-radius: var(--radius); - box-shadow: var(--shadow-elevation-3000); .PlayerUpNextButtonBackground { position: absolute; @@ -46,7 +17,7 @@ left: 0; width: 0; color: var(--primary-alt); - background-color: var(--bg-light); + background-color: var(--border-3000); } &.PlayerUpNextButton--animating { diff --git a/frontend/src/scenes/session-recordings/player/PlayerUpNext.tsx b/frontend/src/scenes/session-recordings/player/PlayerUpNext.tsx index 669dcb5f02b29..b16df4b1722eb 100644 --- a/frontend/src/scenes/session-recordings/player/PlayerUpNext.tsx +++ b/frontend/src/scenes/session-recordings/player/PlayerUpNext.tsx @@ -3,28 +3,39 @@ import './PlayerUpNext.scss' import { IconPlay } from '@posthog/icons' import clsx from 'clsx' import { BuiltLogic, useActions, useValues } from 'kea' +import { useKeyboardHotkeys } from 'lib/hooks/useKeyboardHotkeys' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { useEffect, useRef, useState } from 'react' -import { CSSTransition } from 'react-transition-group' + +import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut' import { sessionRecordingsPlaylistLogicType } from '../playlist/sessionRecordingsPlaylistLogicType' import { sessionRecordingPlayerLogic } from './sessionRecordingPlayerLogic' export interface PlayerUpNextProps { playlistLogic: BuiltLogic - interrupted?: boolean - clearInterrupted?: () => void } -export function PlayerUpNext({ interrupted, clearInterrupted, playlistLogic }: PlayerUpNextProps): JSX.Element | null { +export function PlayerUpNext({ playlistLogic }: PlayerUpNextProps): JSX.Element | null { const timeoutRef = useRef() - const { endReached } = useValues(sessionRecordingPlayerLogic) - const { reportNextRecordingTriggered } = useActions(sessionRecordingPlayerLogic) + const { endReached, playNextAnimationInterrupted } = useValues(sessionRecordingPlayerLogic) + const { reportNextRecordingTriggered, setPlayNextAnimationInterrupted } = useActions(sessionRecordingPlayerLogic) const [animate, setAnimate] = useState(false) const { nextSessionRecording } = useValues(playlistLogic) const { setSelectedRecordingId } = useActions(playlistLogic) + useKeyboardHotkeys({ + n: { + action: () => { + if (nextSessionRecording?.id) { + reportNextRecordingTriggered(false) + setSelectedRecordingId(nextSessionRecording.id) + } + }, + }, + }) + const goToRecording = (automatic: boolean): void => { if (!nextSessionRecording?.id) { return @@ -38,41 +49,45 @@ export function PlayerUpNext({ interrupted, clearInterrupted, playlistLogic }: P if (endReached && nextSessionRecording?.id) { setAnimate(true) - clearInterrupted?.() + setPlayNextAnimationInterrupted(false) timeoutRef.current = setTimeout(() => { goToRecording(true) - }, 3000) // NOTE: Keep in sync with SCSS + }, 30000) // NOTE: Keep in sync with SCSS } return () => clearTimeout(timeoutRef.current) }, [endReached, !!nextSessionRecording]) useEffect(() => { - if (interrupted) { + if (playNextAnimationInterrupted) { clearTimeout(timeoutRef.current) setAnimate(false) } - }, [interrupted]) + }, [playNextAnimationInterrupted]) if (!nextSessionRecording) { return null } return ( - - -
-
goToRecording(false)} - > -
-
- Next recording -
+ + Play the next recording + + } + > +
+
goToRecording(false)} + > +
+
+ Play next
- - +
+ ) } diff --git a/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.tsx b/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.tsx index 5666b718cb5bf..6b28feca120ae 100644 --- a/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.tsx +++ b/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.tsx @@ -92,6 +92,7 @@ export function SessionRecordingPlayer(props: SessionRecordingPlayerProps): JSX. const { isFullScreen, explorerMode, isBuffering, messageTooLargeWarnings } = useValues( sessionRecordingPlayerLogic(logicProps) ) + const { setPlayNextAnimationInterrupted } = useActions(sessionRecordingPlayerLogic(logicProps)) const speedHotkeys = useMemo(() => createPlaybackSpeedKey(setSpeed), [setSpeed]) const { isVerticallyStacked, sidebarOpen, playbackMode } = useValues(playerSettingsLogic) @@ -185,6 +186,8 @@ export function SessionRecordingPlayer(props: SessionRecordingPlayerProps): JSX. `SessionRecordingPlayer--${size}` )} onClick={incrementClickCount} + onMouseMove={() => setPlayNextAnimationInterrupted(true)} + onMouseOut={() => setPlayNextAnimationInterrupted(false)} > {explorerMode ? ( diff --git a/frontend/src/scenes/session-recordings/player/controller/PlayerController.tsx b/frontend/src/scenes/session-recordings/player/controller/PlayerController.tsx index 35b2d1d18651a..9306afef52bbd 100644 --- a/frontend/src/scenes/session-recordings/player/controller/PlayerController.tsx +++ b/frontend/src/scenes/session-recordings/player/controller/PlayerController.tsx @@ -1,4 +1,4 @@ -import { IconCollapse45, IconExpand45, IconPause, IconPlay, IconSearch } from '@posthog/icons' +import { IconClock, IconCollapse45, IconExpand45, IconPause, IconPlay, IconSearch } from '@posthog/icons' import clsx from 'clsx' import { useActions, useValues } from 'kea' import { useKeyboardHotkeys } from 'lib/hooks/useKeyboardHotkeys' @@ -10,11 +10,13 @@ import { SettingsMenu, SettingsToggle, } from 'scenes/session-recordings/components/PanelSettings' -import { playerSettingsLogic } from 'scenes/session-recordings/player/playerSettingsLogic' +import { playerSettingsLogic, TimestampFormat } from 'scenes/session-recordings/player/playerSettingsLogic' +import { PlayerUpNext } from 'scenes/session-recordings/player/PlayerUpNext' import { PLAYBACK_SPEEDS, sessionRecordingPlayerLogic, } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' +import { TimestampFormatToLabel } from 'scenes/session-recordings/utils' import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut' import { SessionPlayerState } from '~/types' @@ -117,12 +119,39 @@ function InspectDOM(): JSX.Element { } function PlayerBottomSettings(): JSX.Element { + const { timestampFormat } = useValues(playerSettingsLogic) + const { setTimestampFormat } = useActions(playerSettingsLogic) + return ( - - - - - + +
+ + + + setTimestampFormat(TimestampFormat.UTC), + active: timestampFormat === TimestampFormat.UTC, + }, + { + label: 'Device', + onClick: () => setTimestampFormat(TimestampFormat.Device), + active: timestampFormat === TimestampFormat.Device, + }, + { + label: 'Relative', + onClick: () => setTimestampFormat(TimestampFormat.Relative), + active: timestampFormat === TimestampFormat.Relative, + }, + ]} + icon={} + label={TimestampFormatToLabel[timestampFormat]} + /> + +
) } @@ -166,31 +195,33 @@ function Maximise(): JSX.Element { size="xsmall" onClick={onChangeMaximise} tooltip={`${isMaximised ? 'Open' : 'Close'} other panels (M)`} - icon={isMaximised ? : } - className="text-2xl" + icon={isMaximised ? : } /> ) } export function PlayerController(): JSX.Element { + const { playlistLogic } = useValues(sessionRecordingPlayerLogic) + return (
-
-
+
+
- -
- - - -
-
+
+ + + +
+
+ {playlistLogic ? : undefined}
+
) diff --git a/frontend/src/scenes/session-recordings/player/controller/PlayerControllerTime.tsx b/frontend/src/scenes/session-recordings/player/controller/PlayerControllerTime.tsx index 0c40085388cd4..53ac102c92028 100644 --- a/frontend/src/scenes/session-recordings/player/controller/PlayerControllerTime.tsx +++ b/frontend/src/scenes/session-recordings/player/controller/PlayerControllerTime.tsx @@ -4,7 +4,6 @@ import { useActions, useValues } from 'kea' import { useKeyHeld } from 'lib/hooks/useKeyHeld' import { IconSkipBackward } from 'lib/lemon-ui/icons' import { capitalizeFirstLetter, colonDelimitedDuration } from 'lib/utils' -import { useCallback } from 'react' import { SimpleTimeLabel } from 'scenes/session-recordings/components/SimpleTimeLabel' import { ONE_FRAME_MS, sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' @@ -16,39 +15,26 @@ export function Timestamp(): JSX.Element { useValues(sessionRecordingPlayerLogic) const { isScrubbing, scrubbingTime } = useValues(seekbarLogic(logicProps)) const { timestampFormat } = useValues(playerSettingsLogic) - const { setTimestampFormat } = useActions(playerSettingsLogic) const startTimeSeconds = ((isScrubbing ? scrubbingTime : currentPlayerTime) ?? 0) / 1000 const endTimeSeconds = Math.floor(sessionPlayerData.durationMs / 1000) const fixedUnits = endTimeSeconds > 3600 ? 3 : 2 - const rotateTimestampFormat = useCallback(() => { - setTimestampFormat( - timestampFormat === 'relative' - ? TimestampFormat.UTC - : timestampFormat === TimestampFormat.UTC - ? TimestampFormat.Device - : TimestampFormat.Relative - ) - }, [timestampFormat]) - return ( - - - {timestampFormat === TimestampFormat.Relative ? ( -
- {colonDelimitedDuration(startTimeSeconds, fixedUnits)} - / - {colonDelimitedDuration(endTimeSeconds, fixedUnits)} -
- ) : currentTimestamp ? ( - - ) : ( - '--/--/----, 00:00:00' - )} -
-
+
+ {timestampFormat === TimestampFormat.Relative ? ( +
+ {colonDelimitedDuration(startTimeSeconds, fixedUnits)} + / + {colonDelimitedDuration(endTimeSeconds, fixedUnits)} +
+ ) : currentTimestamp ? ( + + ) : ( + '--/--/----, 00:00:00' + )} +
) } diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts index 56240e7ec1ee0..b9b06bfedc101 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts @@ -192,8 +192,15 @@ export const sessionRecordingPlayerLogic = kea( reportMessageTooLargeWarningSeen: (sessionRecordingId: string) => ({ sessionRecordingId }), setDebugSnapshotTypes: (types: EventType[]) => ({ types }), setDebugSnapshotIncrementalSources: (incrementalSources: IncrementalSource[]) => ({ incrementalSources }), + setPlayNextAnimationInterrupted: (interrupted: boolean) => ({ interrupted }), }), reducers(() => ({ + playNextAnimationInterrupted: [ + false, + { + setPlayNextAnimationInterrupted: (_, { interrupted }) => interrupted, + }, + ], reportedReplayerErrors: [ new Set(), { diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistSettings.tsx b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistSettings.tsx index 93dfc56d83f0c..da10911c8bf55 100644 --- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistSettings.tsx +++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistSettings.tsx @@ -2,6 +2,7 @@ import { IconEllipsis } from '@posthog/icons' import { IconClock, IconSort } from '@posthog/icons' import { useActions, useValues } from 'kea' import { SettingsBar, SettingsMenu, SettingsToggle } from 'scenes/session-recordings/components/PanelSettings' +import { TimestampFormatToLabel } from 'scenes/session-recordings/utils' import { RecordingUniversalFilters } from '~/types' @@ -19,12 +20,6 @@ const SortingKeyToLabel = { mouse_activity_count: 'Mouse activity', } -const TimestampFormatToLabel = { - relative: 'Relative', - utc: 'UTC', - device: 'Device', -} - function SortedBy({ filters, setFilters, @@ -92,7 +87,7 @@ function SortedBy({ ], }, ]} - icon={} + icon={} label={SortingKeyToLabel[filters.order || 'start_time']} /> ) diff --git a/frontend/src/scenes/session-recordings/utils.ts b/frontend/src/scenes/session-recordings/utils.ts index c7e4f61264ed3..f3e29ae3ea52c 100644 --- a/frontend/src/scenes/session-recordings/utils.ts +++ b/frontend/src/scenes/session-recordings/utils.ts @@ -1,5 +1,11 @@ import { LegacyRecordingFilters, RecordingUniversalFilters, UniversalFiltersGroup, UniversalFilterValue } from '~/types' +export const TimestampFormatToLabel = { + relative: 'Relative', + utc: 'UTC', + device: 'Device', +} + export const isUniversalFilters = ( filters: RecordingUniversalFilters | LegacyRecordingFilters ): filters is RecordingUniversalFilters => {