feat(ui/tracker): display user time in sessions

This commit is contained in:
nick-delirium 2023-08-21 12:01:43 +02:00
parent 4cf85bebda
commit 5ea745c14b
11 changed files with 122 additions and 40 deletions

View file

@ -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),

View file

@ -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 {

View file

@ -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 };

View file

@ -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 }

View file

@ -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">

View file

@ -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

View file

@ -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);

View file

@ -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,
}

View file

@ -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
}

View file

@ -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;

View file

@ -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) => {