feat(ui/tracker): display user time in sessions
This commit is contained in:
parent
4cf85bebda
commit
5ea745c14b
11 changed files with 122 additions and 40 deletions
|
|
@ -31,7 +31,7 @@ function WebPlayer(props: any) {
|
|||
fullscreen,
|
||||
fetchList,
|
||||
} = props;
|
||||
const { notesStore } = useStore();
|
||||
const { notesStore, sessionStore } = useStore();
|
||||
const [activeTab, setActiveTab] = useState('');
|
||||
const [noteItem, setNoteItem] = useState<Note | undefined>(undefined);
|
||||
const [visuallyAdjusted, setAdjusted] = useState(false);
|
||||
|
|
@ -43,7 +43,7 @@ function WebPlayer(props: any) {
|
|||
playerInst = undefined
|
||||
if (!session.sessionId || contextValue.player !== undefined) return;
|
||||
fetchList('issues');
|
||||
|
||||
sessionStore.setUserTimezone(session.timezone)
|
||||
const [WebPlayerInst, PlayerStore] = createWebPlayer(
|
||||
session,
|
||||
(state) => makeAutoObservable(state),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectStorageType, STORAGE_TYPES, StorageType } from 'Player';
|
||||
import {MarkedTarget, selectStorageType, STORAGE_TYPES, StorageType} from 'Player';
|
||||
import { PlayButton, PlayingState, FullScreenButton } from 'App/player-ui'
|
||||
|
||||
import { Tooltip } from 'UI';
|
||||
|
|
@ -84,7 +84,7 @@ function Controls(props: any) {
|
|||
} = props;
|
||||
|
||||
const disabled = disabledRedux || messagesLoading || inspectorMode || markedTargets;
|
||||
|
||||
const sessionTz = session?.timezone;
|
||||
const onKeyDown = (e: any) => {
|
||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
||||
return;
|
||||
|
|
@ -149,6 +149,7 @@ function Controls(props: any) {
|
|||
<div className="flex items-center">
|
||||
<PlayerControls
|
||||
skip={skip}
|
||||
sessionTz={sessionTz}
|
||||
speed={speed}
|
||||
disabled={disabled}
|
||||
backTenSeconds={backTenSeconds}
|
||||
|
|
@ -192,7 +193,16 @@ function Controls(props: any) {
|
|||
);
|
||||
}
|
||||
|
||||
function DevtoolsButtons({ showStorageRedux, toggleBottomTools, bottomBlock, disabledRedux, messagesLoading, markedTargets}) {
|
||||
interface IDevtoolsButtons {
|
||||
showStorageRedux: boolean;
|
||||
disabledRedux: boolean;
|
||||
toggleBottomTools: (blockName: number) => void;
|
||||
bottomBlock: number;
|
||||
markedTargets: MarkedTarget[] | null;
|
||||
messagesLoading: boolean;
|
||||
}
|
||||
|
||||
function DevtoolsButtons({ showStorageRedux, toggleBottomTools, bottomBlock, disabledRedux, messagesLoading, markedTargets }: IDevtoolsButtons) {
|
||||
const { store } = React.useContext(PlayerContext);
|
||||
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,14 @@ const RealReplayTimeConnected: React.FC<{startedAt: number}> = observer(({ start
|
|||
return <RealPlayTime sessionStart={startedAt} time={time} tz={tz} />
|
||||
})
|
||||
|
||||
const RealUserReplayTimeConnected: React.FC<{startedAt: number, sessionTz?: string}> = observer(({ startedAt, sessionTz }) => {
|
||||
if (!sessionTz) return null;
|
||||
const { store } = React.useContext(PlayerContext)
|
||||
const time = store.get().time || 0
|
||||
|
||||
return <RealPlayTime sessionStart={startedAt} time={time} tz={sessionTz} />
|
||||
})
|
||||
|
||||
ReduxTime.displayName = "ReduxTime";
|
||||
|
||||
export { ReduxTime, RealReplayTimeConnected };
|
||||
export { ReduxTime, RealReplayTimeConnected, RealUserReplayTimeConnected };
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ interface IProps {
|
|||
setTimelineHoverTime: (t: number) => void
|
||||
startedAt: number
|
||||
tooltipVisible: boolean
|
||||
timezone?: string
|
||||
}
|
||||
|
||||
function Timeline(props: IProps) {
|
||||
|
|
@ -36,7 +37,7 @@ function Timeline(props: IProps) {
|
|||
devtoolsLoading,
|
||||
domLoading,
|
||||
} = store.get()
|
||||
const { issues } = props;
|
||||
const { issues, timezone } = props;
|
||||
|
||||
const progressRef = useRef<HTMLDivElement>(null)
|
||||
const timelineRef = useRef<HTMLDivElement>(null)
|
||||
|
|
@ -86,9 +87,12 @@ function Timeline(props: IProps) {
|
|||
if (!time) return;
|
||||
const tz = settingsStore.sessionSettings.timezone.value
|
||||
const timeStr = DateTime.fromMillis(props.startedAt + time).setZone(tz).toFormat(`hh:mm:ss a`)
|
||||
const userTimeStr = timezone ? DateTime.fromMillis(props.startedAt + time).setZone(timezone).toFormat(`hh:mm:ss a`) : undefined
|
||||
|
||||
const timeLineTooltip = {
|
||||
time: Duration.fromMillis(time).toFormat(`mm:ss`),
|
||||
timeStr,
|
||||
localTime: timeStr,
|
||||
userTime: userTimeStr,
|
||||
offset: e.nativeEvent.pageX,
|
||||
isVisible: true,
|
||||
};
|
||||
|
|
@ -173,6 +177,7 @@ export default connect(
|
|||
(state: any) => ({
|
||||
issues: state.getIn(['sessions', 'current']).issues || [],
|
||||
startedAt: state.getIn(['sessions', 'current']).startedAt || 0,
|
||||
timezone: state.getIn(['sessions', 'current']).timezone,
|
||||
tooltipVisible: state.getIn(['sessions', 'timeLineTooltip', 'isVisible']),
|
||||
}),
|
||||
{ setTimelinePointer, setTimelineHoverTime }
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import * as constants from "constants";
|
||||
import React from 'react';
|
||||
import { Icon, Tooltip, Popover } from 'UI';
|
||||
import cn from 'classnames';
|
||||
|
|
@ -21,8 +22,16 @@ interface Props {
|
|||
forthTenSeconds: () => void;
|
||||
toggleSpeed: (speedIndex: number) => void;
|
||||
toggleSkip: () => void;
|
||||
sessionTz?: string;
|
||||
}
|
||||
|
||||
export const TimeMode = {
|
||||
Real: 'real',
|
||||
UserReal: 'user_real',
|
||||
Timestamp: 'current',
|
||||
} as const
|
||||
export type ITimeMode = typeof TimeMode[keyof typeof TimeMode]
|
||||
|
||||
function PlayerControls(props: Props) {
|
||||
const {
|
||||
skip,
|
||||
|
|
@ -37,17 +46,18 @@ function PlayerControls(props: Props) {
|
|||
setSkipInterval,
|
||||
currentInterval,
|
||||
startedAt,
|
||||
sessionTz,
|
||||
} = props;
|
||||
const [showTooltip, setShowTooltip] = React.useState(false);
|
||||
const [isUniTime, setUniTime] = React.useState(localStorage.getItem('__or_player_time_mode') === 'real');
|
||||
const [timeMode, setTimeMode] = React.useState<ITimeMode>(localStorage.getItem('__or_player_time_mode') as ITimeMode);
|
||||
const speedRef = React.useRef<HTMLButtonElement>(null);
|
||||
const arrowBackRef = React.useRef<HTMLButtonElement>(null);
|
||||
const arrowForwardRef = React.useRef<HTMLButtonElement>(null);
|
||||
const skipRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const setIsUniTime = (isUniTime: boolean) => {
|
||||
localStorage.setItem('__or_player_time_mode', isUniTime ? 'real' : 'current');
|
||||
setUniTime(isUniTime);
|
||||
const saveTimeMode = (mode: ITimeMode) => {
|
||||
localStorage.setItem('__or_player_time_mode', mode);
|
||||
setTimeMode(mode);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
|
|
@ -83,7 +93,7 @@ function PlayerControls(props: Props) {
|
|||
|
||||
|
||||
<button className={cn(styles.speedButton, 'focus:border focus:border-blue')}>
|
||||
<PlayingTime isUniTime={isUniTime} setIsUniTime={setIsUniTime} startedAt={startedAt} />
|
||||
<PlayingTime timeMode={timeMode} setTimeMode={saveTimeMode} startedAt={startedAt} sessionTz={sessionTz} />
|
||||
</button>
|
||||
|
||||
<div className="rounded ml-4 bg-active-blue border border-active-blue-border flex items-stretch">
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import {ITimeMode, TimeMode} from "Components/Session_/Player/Controls/components/PlayerControls";
|
||||
import React from 'react'
|
||||
import { Popover } from 'UI'
|
||||
import { RealReplayTimeConnected, ReduxTime } from "Components/Session_/Player/Controls/Time";
|
||||
import { RealReplayTimeConnected, ReduxTime, RealUserReplayTimeConnected } from "Components/Session_/Player/Controls/Time";
|
||||
|
||||
interface Props {
|
||||
isUniTime: boolean;
|
||||
timeMode: ITimeMode;
|
||||
startedAt: number;
|
||||
setIsUniTime: (isUniTime: boolean) => void;
|
||||
setTimeMode: (mode: ITimeMode) => void;
|
||||
sessionTz?: string;
|
||||
}
|
||||
|
||||
function PlayingTime({ isUniTime, setIsUniTime, startedAt }: Props) {
|
||||
function PlayingTime({ timeMode, setTimeMode, startedAt, sessionTz }: Props) {
|
||||
return (
|
||||
<Popover
|
||||
// @ts-ignore
|
||||
|
|
@ -17,33 +19,57 @@ function PlayingTime({ isUniTime, setIsUniTime, startedAt }: Props) {
|
|||
duration={0}
|
||||
className="cursor-pointer select-none"
|
||||
distance={20}
|
||||
render={({close}) => (
|
||||
render={({ close }) => (
|
||||
<div className={'flex flex-col gap-2 bg-white py-2 rounded color-gray-darkest text-left'}>
|
||||
<div className={'font-semibold px-4 cursor-default'}>Playback Time Mode</div>
|
||||
<div className={'flex flex-col cursor-pointer hover:bg-active-blue w-full px-4'}>
|
||||
<div className={'text-sm text-disabled-text text-left'}>Current / Session Duration</div>
|
||||
<div className={'flex items-center text-left'} onClick={() => {
|
||||
setIsUniTime(false);
|
||||
close();
|
||||
}}>
|
||||
<div
|
||||
className={'flex items-center text-left'}
|
||||
onClick={() => {
|
||||
setTimeMode(TimeMode.Timestamp);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<ReduxTime isCustom name="time" format="mm:ss" />
|
||||
<span className="px-1">/</span>
|
||||
<ReduxTime isCustom name="endTime" format="mm:ss" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={'flex flex-col cursor-pointer hover:bg-active-blue w-full px-4'} onClick={() => {
|
||||
setIsUniTime(true);
|
||||
close();
|
||||
}}>
|
||||
{sessionTz ?
|
||||
<div
|
||||
className={'flex flex-col cursor-pointer hover:bg-active-blue w-full px-4'}
|
||||
onClick={() => {
|
||||
setTimeMode(TimeMode.UserReal);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<div className={'text-sm text-disabled-text text-left'}>User's time</div>
|
||||
<div className={'text-left'}>
|
||||
<RealUserReplayTimeConnected startedAt={startedAt} sessionTz={sessionTz}/>
|
||||
</div>
|
||||
</div>
|
||||
: null}
|
||||
<div
|
||||
className={'flex flex-col cursor-pointer hover:bg-active-blue w-full px-4'}
|
||||
onClick={() => {
|
||||
setTimeMode(TimeMode.Real);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<div className={'text-sm text-disabled-text text-left'}>Based on your settings</div>
|
||||
<div className={'text-left'}><RealReplayTimeConnected startedAt={startedAt} /></div>
|
||||
<div className={'text-left'}>
|
||||
<RealReplayTimeConnected startedAt={startedAt} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center font-semibold text-center" style={{ minWidth: 85 }}>
|
||||
{isUniTime ? (
|
||||
{timeMode === TimeMode.Real ? (
|
||||
<RealReplayTimeConnected startedAt={startedAt} />
|
||||
) : timeMode === TimeMode.UserReal ? (
|
||||
<RealUserReplayTimeConnected startedAt={startedAt} sessionTz={sessionTz} />
|
||||
) : (
|
||||
<>
|
||||
<ReduxTime isCustom name="time" format="mm:ss" />
|
||||
|
|
@ -53,7 +79,7 @@ function PlayingTime({ isUniTime, setIsUniTime, startedAt }: Props) {
|
|||
)}
|
||||
</div>
|
||||
</Popover>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default PlayingTime
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import React from 'react';
|
||||
// @ts-ignore
|
||||
import { Duration } from 'luxon';
|
||||
import { connect } from 'react-redux';
|
||||
import stl from './styles.module.css';
|
||||
|
||||
|
|
@ -8,14 +6,16 @@ interface Props {
|
|||
time: number;
|
||||
offset: number;
|
||||
isVisible: boolean;
|
||||
timeStr: string;
|
||||
localTime: string;
|
||||
userTime?: string;
|
||||
}
|
||||
|
||||
function TimeTooltip({
|
||||
time,
|
||||
offset,
|
||||
isVisible,
|
||||
timeStr,
|
||||
localTime,
|
||||
userTime
|
||||
}: Props) {
|
||||
return (
|
||||
<div
|
||||
|
|
@ -30,10 +30,16 @@ function TimeTooltip({
|
|||
}}
|
||||
>
|
||||
{!time ? 'Loading' : time}
|
||||
{timeStr ? (
|
||||
{localTime ? (
|
||||
<>
|
||||
<br />
|
||||
<span className="text-gray-light">({timeStr})</span>
|
||||
<span className="text-gray-light">local: {localTime}</span>
|
||||
</>
|
||||
) : null}
|
||||
{userTime ? (
|
||||
<>
|
||||
<br />
|
||||
<span className="text-gray-light">user: {userTime}</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
@ -41,6 +47,7 @@ function TimeTooltip({
|
|||
}
|
||||
|
||||
export default connect((state) => {
|
||||
const { time = 0, offset = 0, isVisible, timeStr } = state.getIn(['sessions', 'timeLineTooltip']);
|
||||
return { time, offset, isVisible, timeStr };
|
||||
// @ts-ignore
|
||||
const { time = 0, offset = 0, isVisible, localTime, userTime } = state.getIn(['sessions', 'timeLineTooltip']);
|
||||
return { time, offset, isVisible, localTime, userTime };
|
||||
})(TimeTooltip);
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ const initObj = {
|
|||
timelinePointer: null,
|
||||
sessionPath: {},
|
||||
lastPlayedSessionId: null,
|
||||
timeLineTooltip: { time: 0, offset: 0, isVisible: false, timeStr: '' },
|
||||
timeLineTooltip: { time: 0, offset: 0, isVisible: false, localTime: '', userTime: '' },
|
||||
createNoteTooltip: { time: 0, isVisible: false, isEdit: false, note: null },
|
||||
fetchFailed: false,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,10 +120,11 @@ export default class SessionStore {
|
|||
timelinePointer = {}
|
||||
sessionPath = {}
|
||||
lastPlayedSessionId: string
|
||||
timeLineTooltip = { time: 0, offset: 0, isVisible: false, timeStr: '' }
|
||||
timeLineTooltip = { time: 0, offset: 0, isVisible: false, localTime: '', userTime: '' }
|
||||
createNoteTooltip = { time: 0, isVisible: false, isEdit: false, note: null }
|
||||
previousId = ''
|
||||
nextId = ''
|
||||
userTimezone = ''
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
|
|
@ -132,6 +133,10 @@ export default class SessionStore {
|
|||
});
|
||||
}
|
||||
|
||||
setUserTimezone(timezone: string) {
|
||||
this.userTimezone = timezone;
|
||||
}
|
||||
|
||||
resetUserFilter() {
|
||||
this.userFilter = new UserFilter();
|
||||
}
|
||||
|
|
@ -345,7 +350,7 @@ export default class SessionStore {
|
|||
this.timelinePointer = pointer
|
||||
}
|
||||
|
||||
setTimelineTooltip(tp: { time: number, offset: number, isVisible: boolean, timeStr: string }) {
|
||||
setTimelineTooltip(tp: { time: number, offset: number, isVisible: boolean, localTime: string, userTime?: string }) {
|
||||
this.timeLineTooltip = tp
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ export interface ISession {
|
|||
userID: string;
|
||||
userUUID: string;
|
||||
userEvents: any[];
|
||||
timezone?: string;
|
||||
}
|
||||
|
||||
const emptyValues = {
|
||||
|
|
@ -196,6 +197,7 @@ export default class Session {
|
|||
notes: ISession['notes'];
|
||||
notesWithEvents: ISession['notesWithEvents'];
|
||||
frustrations: Array<IIssue | InjectedEvent>
|
||||
timezone?: ISession['timezone'];
|
||||
|
||||
fileKey: ISession['fileKey'];
|
||||
durationSeconds: number;
|
||||
|
|
|
|||
|
|
@ -95,6 +95,14 @@ export type Options = AppOptions & ObserverOptions & SanitizerOptions
|
|||
// TODO: use backendHost only
|
||||
export const DEFAULT_INGEST_POINT = 'https://api.openreplay.com/ingest'
|
||||
|
||||
function getTimezone() {
|
||||
const offset = new Date().getTimezoneOffset() * -1
|
||||
const sign = offset >= 0 ? '+' : '-'
|
||||
const hours = Math.floor(Math.abs(offset) / 60)
|
||||
const minutes = Math.abs(offset) % 60
|
||||
return `UTC${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
export default class App {
|
||||
readonly nodes: Nodes
|
||||
readonly ticker: Ticker
|
||||
|
|
@ -525,6 +533,7 @@ export default class App {
|
|||
token: isNewSession ? undefined : sessionToken,
|
||||
deviceMemory,
|
||||
jsHeapSizeLimit,
|
||||
timezone: getTimezone(),
|
||||
}),
|
||||
})
|
||||
.then((r) => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue