feat ui: prefetch mobfiles on hover, use prefetched if exist (#2098)
This commit is contained in:
parent
156e1f94d6
commit
ceb714617e
11 changed files with 440 additions and 204 deletions
|
|
@ -1,86 +1,99 @@
|
||||||
|
import withPermissions from 'HOCs/withPermissions';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import usePageTitle from 'App/hooks/usePageTitle';
|
|
||||||
import { fetchV2, clearCurrentSession } from "Duck/sessions";
|
|
||||||
import { fetchList as fetchSlackList } from 'Duck/integrations/slack';
|
|
||||||
import { Link, NoContent, Loader } from 'UI';
|
|
||||||
import { sessions as sessionsRoute } from 'App/routes';
|
|
||||||
import withPermissions from 'HOCs/withPermissions'
|
|
||||||
import WebPlayer from './WebPlayer';
|
|
||||||
import { useStore } from 'App/mstore';
|
|
||||||
import { clearLogs } from 'App/dev/console';
|
|
||||||
|
|
||||||
import MobilePlayer from "Components/Session/MobilePlayer";
|
import { clearLogs } from 'App/dev/console';
|
||||||
|
import usePageTitle from 'App/hooks/usePageTitle';
|
||||||
|
import { useStore } from 'App/mstore';
|
||||||
|
import { sessions as sessionsRoute } from 'App/routes';
|
||||||
|
import MobilePlayer from 'Components/Session/MobilePlayer';
|
||||||
|
import { fetchList as fetchSlackList } from 'Duck/integrations/slack';
|
||||||
|
import { clearCurrentSession, fetchV2 } from 'Duck/sessions';
|
||||||
|
import { Link, Loader, NoContent } from 'UI';
|
||||||
|
|
||||||
|
import WebPlayer from './WebPlayer';
|
||||||
|
|
||||||
const SESSIONS_ROUTE = sessionsRoute();
|
const SESSIONS_ROUTE = sessionsRoute();
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
hasErrors: boolean;
|
hasErrors: boolean;
|
||||||
fetchV2: (sessionId: string) => void;
|
fetchV2: (sessionId: string) => void;
|
||||||
clearCurrentSession: () => void;
|
clearCurrentSession: () => void;
|
||||||
session: Record<string, any>;
|
session: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Session({
|
function Session({
|
||||||
sessionId,
|
sessionId,
|
||||||
loading,
|
hasErrors,
|
||||||
hasErrors,
|
fetchV2,
|
||||||
fetchV2,
|
clearCurrentSession,
|
||||||
clearCurrentSession,
|
session,
|
||||||
session,
|
}: Props) {
|
||||||
}: Props) {
|
usePageTitle('OpenReplay Session Player');
|
||||||
usePageTitle("OpenReplay Session Player");
|
const { sessionStore } = useStore();
|
||||||
const [ initializing, setInitializing ] = useState(true)
|
useEffect(() => {
|
||||||
const { sessionStore } = useStore();
|
if (sessionId != null) {
|
||||||
useEffect(() => {
|
fetchV2(sessionId);
|
||||||
if (sessionId != null) {
|
} else {
|
||||||
fetchV2(sessionId)
|
console.error('No sessionID in route.');
|
||||||
} else {
|
}
|
||||||
console.error("No sessionID in route.")
|
return () => {
|
||||||
}
|
clearCurrentSession();
|
||||||
setInitializing(false)
|
};
|
||||||
return () => {
|
}, [sessionId]);
|
||||||
clearCurrentSession();
|
|
||||||
}
|
|
||||||
},[ sessionId ]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearLogs()
|
clearLogs();
|
||||||
sessionStore.resetUserFilter();
|
sessionStore.resetUserFilter();
|
||||||
} ,[])
|
}, []);
|
||||||
|
|
||||||
const player = session.isMobileNative ? <MobilePlayer /> : <WebPlayer />
|
const player = session.isMobileNative ? <MobilePlayer /> : <WebPlayer />;
|
||||||
return (
|
return (
|
||||||
<NoContent
|
<NoContent
|
||||||
show={ hasErrors }
|
show={hasErrors}
|
||||||
title="Session not found."
|
title="Session not found."
|
||||||
subtext={
|
subtext={
|
||||||
<span>
|
<span>
|
||||||
{'Please check your data retention plan, or try '}
|
{'Please check your data retention plan, or try '}
|
||||||
<Link to={ SESSIONS_ROUTE } className="link">{'another one'}</Link>
|
<Link to={SESSIONS_ROUTE} className="link">
|
||||||
</span>
|
{'another one'}
|
||||||
}
|
</Link>
|
||||||
>
|
</span>
|
||||||
<Loader className="flex-1" loading={ loading || initializing }>
|
}
|
||||||
{player}
|
>
|
||||||
</Loader>
|
<Loader className="flex-1" loading={!session.sessionId}>
|
||||||
</NoContent>
|
{player}
|
||||||
);
|
</Loader>
|
||||||
|
</NoContent>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withPermissions(['SESSION_REPLAY'], '', true)(connect((state: any, props: any) => {
|
export default withPermissions(
|
||||||
const { match: { params: { sessionId } } } = props;
|
['SESSION_REPLAY'],
|
||||||
return {
|
'',
|
||||||
sessionId,
|
true
|
||||||
loading: state.getIn([ 'sessions', 'loading' ]),
|
)(
|
||||||
hasErrors: !!state.getIn([ 'sessions', 'errors' ]),
|
connect(
|
||||||
session: state.getIn([ 'sessions', 'current' ]),
|
(state: any, props: any) => {
|
||||||
};
|
const {
|
||||||
}, {
|
match: {
|
||||||
fetchSlackList,
|
params: { sessionId },
|
||||||
fetchV2,
|
},
|
||||||
clearCurrentSession,
|
} = props;
|
||||||
})(Session));
|
return {
|
||||||
|
sessionId,
|
||||||
|
loading: state.getIn(['sessions', 'loading']),
|
||||||
|
hasErrors: !!state.getIn(['sessions', 'errors']),
|
||||||
|
session: state.getIn(['sessions', 'current']),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fetchSlackList,
|
||||||
|
fetchV2,
|
||||||
|
clearCurrentSession,
|
||||||
|
}
|
||||||
|
)(Session)
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,28 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import withLocationHandlers from 'HOCs/withLocationHandlers';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Modal, Loader } from 'UI';
|
|
||||||
import { toggleFullscreen, closeBottomBlock } from 'Duck/components/player';
|
|
||||||
import { fetchList } from 'Duck/integrations';
|
|
||||||
import { createWebPlayer } from 'Player';
|
import { createWebPlayer } from 'Player';
|
||||||
import { makeAutoObservable } from 'mobx';
|
import { makeAutoObservable } from 'mobx';
|
||||||
import withLocationHandlers from 'HOCs/withLocationHandlers';
|
|
||||||
import { useStore } from 'App/mstore';
|
|
||||||
import PlayerBlockHeader from './Player/ReplayPlayer/PlayerBlockHeader';
|
|
||||||
import ReadNote from '../Session_/Player/Controls/components/ReadNote';
|
|
||||||
import PlayerContent from './Player/ReplayPlayer/PlayerContent';
|
|
||||||
import { IPlayerContext, PlayerContext, defaultContextValue } from './playerContext';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { Note } from 'App/services/NotesService';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import { useStore } from 'App/mstore';
|
||||||
|
import { Note } from 'App/services/NotesService';
|
||||||
|
import { closeBottomBlock, toggleFullscreen } from 'Duck/components/player';
|
||||||
|
import { fetchList } from 'Duck/integrations';
|
||||||
|
import { Loader, Modal } from 'UI';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import ReadNote from '../Session_/Player/Controls/components/ReadNote';
|
||||||
|
import PlayerBlockHeader from './Player/ReplayPlayer/PlayerBlockHeader';
|
||||||
|
import PlayerContent from './Player/ReplayPlayer/PlayerContent';
|
||||||
|
import { IPlayerContext, PlayerContext, defaultContextValue } from './playerContext';
|
||||||
|
|
||||||
|
|
||||||
const TABS = {
|
const TABS = {
|
||||||
EVENTS: 'Activity',
|
EVENTS: 'Activity',
|
||||||
CLICKMAP: 'Click Map',
|
CLICKMAP: 'Click Map',
|
||||||
|
|
@ -54,13 +61,21 @@ function WebPlayer(props: any) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
playerInst = undefined;
|
playerInst = undefined;
|
||||||
if (!session.sessionId || contextValue.player !== undefined) return;
|
if (!session.sessionId || contextValue.player !== undefined) return;
|
||||||
|
const mobData = sessionStore.prefetchedMobUrls[session.sessionId] as Record<string, any> | undefined;
|
||||||
|
const usePrefetched = props.prefetched && mobData?.data;
|
||||||
fetchList('issues');
|
fetchList('issues');
|
||||||
sessionStore.setUserTimezone(session.timezone);
|
sessionStore.setUserTimezone(session.timezone);
|
||||||
const [WebPlayerInst, PlayerStore] = createWebPlayer(
|
const [WebPlayerInst, PlayerStore] = createWebPlayer(
|
||||||
session,
|
session,
|
||||||
(state) => makeAutoObservable(state),
|
(state) => makeAutoObservable(state),
|
||||||
toast
|
toast,
|
||||||
|
props.prefetched,
|
||||||
);
|
);
|
||||||
|
if (usePrefetched) {
|
||||||
|
if (mobData?.data) {
|
||||||
|
WebPlayerInst.preloadFirstFile(mobData?.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
setContextValue({ player: WebPlayerInst, store: PlayerStore });
|
setContextValue({ player: WebPlayerInst, store: PlayerStore });
|
||||||
playerInst = WebPlayerInst;
|
playerInst = WebPlayerInst;
|
||||||
|
|
||||||
|
|
@ -78,6 +93,12 @@ function WebPlayer(props: any) {
|
||||||
}
|
}
|
||||||
}, [session.sessionId]);
|
}, [session.sessionId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!props.prefetched && session.domURL.length > 0) {
|
||||||
|
playerInst?.reinit(session)
|
||||||
|
}
|
||||||
|
}, [session.domURL.length, props.prefetched])
|
||||||
|
|
||||||
const { firstVisualEvent: visualOffset, messagesProcessed, tabStates, ready } = contextValue.store?.get() || {};
|
const { firstVisualEvent: visualOffset, messagesProcessed, tabStates, ready } = contextValue.store?.get() || {};
|
||||||
const cssLoading = ready && tabStates ? Object.values(tabStates).some(
|
const cssLoading = ready && tabStates ? Object.values(tabStates).some(
|
||||||
({ cssLoading }) => cssLoading
|
({ cssLoading }) => cssLoading
|
||||||
|
|
@ -205,6 +226,7 @@ export default connect(
|
||||||
(state: any) => ({
|
(state: any) => ({
|
||||||
session: state.getIn(['sessions', 'current']),
|
session: state.getIn(['sessions', 'current']),
|
||||||
insights: state.getIn(['sessions', 'insights']),
|
insights: state.getIn(['sessions', 'insights']),
|
||||||
|
prefetched: state.getIn(['sessions', 'prefetched']),
|
||||||
visitedEvents: state.getIn(['sessions', 'visitedEvents']),
|
visitedEvents: state.getIn(['sessions', 'visitedEvents']),
|
||||||
jwt: state.getIn(['user', 'jwt']),
|
jwt: state.getIn(['user', 'jwt']),
|
||||||
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
|
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,74 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Link, Icon } from 'UI';
|
import { useHistory } from 'react-router';
|
||||||
import { session as sessionRoute, liveSession as liveSessionRoute } from 'App/routes';
|
|
||||||
|
import {
|
||||||
|
liveSession as liveSessionRoute,
|
||||||
|
session as sessionRoute,
|
||||||
|
withSiteId,
|
||||||
|
} from 'App/routes';
|
||||||
|
import { Icon, Link } from 'UI';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
const PLAY_ICON_NAMES = {
|
const PLAY_ICON_NAMES = {
|
||||||
notPlayed: 'play-fill',
|
notPlayed: 'play-fill',
|
||||||
played: 'play-circle-light',
|
played: 'play-circle-light',
|
||||||
hovered: 'play-hover',
|
hovered: 'play-hover',
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDefaultIconName = (isViewed: any) => (!isViewed ? PLAY_ICON_NAMES.notPlayed : PLAY_ICON_NAMES.played);
|
const getDefaultIconName = (isViewed: any) =>
|
||||||
|
!isViewed ? PLAY_ICON_NAMES.notPlayed : PLAY_ICON_NAMES.played;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isAssist: boolean;
|
isAssist: boolean;
|
||||||
viewed: boolean;
|
viewed: boolean;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
queryParams?: any;
|
queryParams?: any;
|
||||||
newTab?: boolean;
|
newTab?: boolean;
|
||||||
query?: string
|
query?: string;
|
||||||
|
beforeOpen?: () => void;
|
||||||
|
siteId?: string;
|
||||||
}
|
}
|
||||||
export default function PlayLink(props: Props) {
|
function PlayLink(props: Props) {
|
||||||
const { isAssist, viewed, sessionId, onClick = null, queryParams } = props;
|
const { isAssist, viewed, sessionId, onClick = null, queryParams } = props;
|
||||||
const defaultIconName = getDefaultIconName(viewed);
|
const history = useHistory();
|
||||||
|
const defaultIconName = getDefaultIconName(viewed);
|
||||||
|
|
||||||
const [isHovered, toggleHover] = useState(false);
|
const [isHovered, toggleHover] = useState(false);
|
||||||
const [iconName, setIconName] = useState(defaultIconName);
|
const [iconName, setIconName] = useState(defaultIconName);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isHovered) setIconName(PLAY_ICON_NAMES.hovered);
|
if (isHovered) setIconName(PLAY_ICON_NAMES.hovered);
|
||||||
else setIconName(getDefaultIconName(viewed));
|
else setIconName(getDefaultIconName(viewed));
|
||||||
}, [isHovered, viewed]);
|
}, [isHovered, viewed]);
|
||||||
|
|
||||||
const link = isAssist ? liveSessionRoute(sessionId, queryParams) : sessionRoute(sessionId);
|
const link = isAssist
|
||||||
return (
|
? liveSessionRoute(sessionId, queryParams)
|
||||||
<Link
|
: sessionRoute(sessionId);
|
||||||
onClick={onClick ? onClick : () => {}}
|
|
||||||
to={link + (props.query ? props.query : '')}
|
const handleBeforeOpen = () => {
|
||||||
onMouseEnter={() => toggleHover(true)}
|
if (props.beforeOpen) {
|
||||||
onMouseLeave={() => toggleHover(false)}
|
props.beforeOpen();
|
||||||
target={props.newTab ? "_blank" : undefined} rel={props.newTab ? "noopener noreferrer" : undefined}
|
history.push(withSiteId(link + (props.query ? props.query : ''), props.siteId));
|
||||||
>
|
}
|
||||||
<Icon name={iconName} size={38} color={isAssist ? 'tealx' : 'teal'} />
|
};
|
||||||
</Link>
|
|
||||||
);
|
const onLinkClick = props.beforeOpen ? handleBeforeOpen : onClick;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
onClick={onLinkClick}
|
||||||
|
to={link + (props.query ? props.query : '')}
|
||||||
|
onMouseEnter={() => toggleHover(true)}
|
||||||
|
onMouseLeave={() => toggleHover(false)}
|
||||||
|
target={props.newTab ? '_blank' : undefined}
|
||||||
|
rel={props.newTab ? 'noopener noreferrer' : undefined}
|
||||||
|
>
|
||||||
|
<Icon name={iconName} size={38} color={isAssist ? 'tealx' : 'teal'} />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default connect((state: any, props: Props) => ({
|
||||||
|
siteId: props.siteId || state.getIn([ 'site', 'siteId' ])
|
||||||
|
}))(PlayLink);
|
||||||
|
|
@ -1,26 +1,38 @@
|
||||||
import React, { useMemo } from 'react';
|
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import { CountryFlag, Avatar, TextEllipsis, Label, Icon, Tooltip, ItemMenu } from 'UI';
|
import copy from 'copy-to-clipboard';
|
||||||
import { useStore } from 'App/mstore';
|
import { Duration } from 'luxon';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { durationFormatted, formatTimeOrDate } from 'App/date';
|
import { durationFormatted, formatTimeOrDate } from 'App/date';
|
||||||
import stl from './sessionItem.module.css';
|
import { presetSession } from 'App/duck/sessions';
|
||||||
import Counter from './Counter';
|
import { useStore } from 'App/mstore';
|
||||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
|
||||||
import SessionMetaList from './SessionMetaList';
|
|
||||||
import PlayLink from './PlayLink';
|
|
||||||
import ErrorBars from './ErrorBars';
|
|
||||||
import {
|
import {
|
||||||
assist as assistRoute,
|
assist as assistRoute,
|
||||||
|
isRoute,
|
||||||
liveSession,
|
liveSession,
|
||||||
sessions as sessionsRoute,
|
|
||||||
session as sessionRoute,
|
session as sessionRoute,
|
||||||
isRoute
|
sessions as sessionsRoute,
|
||||||
} from 'App/routes';
|
} from 'App/routes';
|
||||||
import { capitalize } from 'App/utils';
|
import { capitalize } from 'App/utils';
|
||||||
import { Duration } from 'luxon';
|
import {
|
||||||
import copy from 'copy-to-clipboard';
|
Avatar,
|
||||||
import { toast } from 'react-toastify';
|
CountryFlag,
|
||||||
|
Icon,
|
||||||
|
ItemMenu,
|
||||||
|
Label,
|
||||||
|
TextEllipsis,
|
||||||
|
Tooltip,
|
||||||
|
} from 'UI';
|
||||||
|
|
||||||
|
import Counter from './Counter';
|
||||||
|
import ErrorBars from './ErrorBars';
|
||||||
|
import PlayLink from './PlayLink';
|
||||||
|
import SessionMetaList from './SessionMetaList';
|
||||||
|
import stl from './sessionItem.module.css';
|
||||||
|
|
||||||
const ASSIST_ROUTE = assistRoute();
|
const ASSIST_ROUTE = assistRoute();
|
||||||
const ASSIST_LIVE_SESSION = liveSession();
|
const ASSIST_LIVE_SESSION = liveSession();
|
||||||
|
|
@ -69,12 +81,20 @@ interface Props {
|
||||||
ignoreAssist?: boolean;
|
ignoreAssist?: boolean;
|
||||||
bookmarked?: boolean;
|
bookmarked?: boolean;
|
||||||
toggleFavorite?: (sessionId: string) => void;
|
toggleFavorite?: (sessionId: string) => void;
|
||||||
query?: string
|
query?: string;
|
||||||
|
presetSession?: typeof presetSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PREFETCH_STATE = {
|
||||||
|
none: 0,
|
||||||
|
loading: 1,
|
||||||
|
fetched: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
function SessionItem(props: RouteComponentProps & Props) {
|
function SessionItem(props: RouteComponentProps & Props) {
|
||||||
const { settingsStore } = useStore();
|
const { settingsStore, sessionStore } = useStore();
|
||||||
const { timezone, shownTimezone } = settingsStore.sessionSettings;
|
const { timezone, shownTimezone } = settingsStore.sessionSettings;
|
||||||
|
const [prefetchState, setPrefetched] = React.useState(PREFETCH_STATE.none);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
session,
|
session,
|
||||||
|
|
@ -88,6 +108,7 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
ignoreAssist = false,
|
ignoreAssist = false,
|
||||||
bookmarked = false,
|
bookmarked = false,
|
||||||
query,
|
query,
|
||||||
|
presetSession,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -110,7 +131,7 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
metadata,
|
metadata,
|
||||||
issueTypes,
|
issueTypes,
|
||||||
active,
|
active,
|
||||||
timezone: userTimezone
|
timezone: userTimezone,
|
||||||
} = session;
|
} = session;
|
||||||
|
|
||||||
const location = props.location;
|
const location = props.location;
|
||||||
|
|
@ -120,11 +141,11 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
const hasUserId = userId || userAnonymousId;
|
const hasUserId = userId || userAnonymousId;
|
||||||
const isSessions = isRoute(SESSIONS_ROUTE, location.pathname);
|
const isSessions = isRoute(SESSIONS_ROUTE, location.pathname);
|
||||||
const isAssist =
|
const isAssist =
|
||||||
!ignoreAssist &&
|
(!ignoreAssist &&
|
||||||
(isRoute(ASSIST_ROUTE, location.pathname) ||
|
(isRoute(ASSIST_ROUTE, location.pathname) ||
|
||||||
isRoute(ASSIST_LIVE_SESSION, location.pathname) ||
|
isRoute(ASSIST_LIVE_SESSION, location.pathname) ||
|
||||||
location.pathname.includes('multiview')) ||
|
location.pathname.includes('multiview'))) ||
|
||||||
props.live
|
props.live;
|
||||||
|
|
||||||
const isLastPlayed = lastPlayedSessionId === sessionId;
|
const isLastPlayed = lastPlayedSessionId === sessionId;
|
||||||
|
|
||||||
|
|
@ -146,16 +167,32 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
}${sessionRoute(sessionId)}`;
|
}${sessionRoute(sessionId)}`;
|
||||||
copy(sessionPath);
|
copy(sessionPath);
|
||||||
toast.success('Session URL copied to clipboard');
|
toast.success('Session URL copied to clipboard');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'trash',
|
icon: 'trash',
|
||||||
text: 'Remove',
|
text: 'Remove',
|
||||||
onClick: () => (props.toggleFavorite ? props.toggleFavorite(sessionId) : null)
|
onClick: () =>
|
||||||
}
|
props.toggleFavorite ? props.toggleFavorite(sessionId) : null,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleHover = async () => {
|
||||||
|
if (prefetchState !== PREFETCH_STATE.none || props.live || isAssist) return;
|
||||||
|
|
||||||
|
setPrefetched(PREFETCH_STATE.loading);
|
||||||
|
try {
|
||||||
|
await sessionStore.getFirstMob(sessionId);
|
||||||
|
setPrefetched(PREFETCH_STATE.fetched);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error while prefetching first mob', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const openSession = () => {
|
||||||
|
if (props.live || isAssist || prefetchState === PREFETCH_STATE.none) return
|
||||||
|
presetSession?.(session);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
delay={0}
|
delay={0}
|
||||||
|
|
@ -164,23 +201,31 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(stl.sessionItem, 'flex flex-col py-2 px-4')}
|
className={cn(stl.sessionItem, 'flex flex-col py-2 px-4')}
|
||||||
id='session-item'
|
id="session-item"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div className='flex items-start'>
|
<div className="flex items-start">
|
||||||
<div className={cn('flex items-center w-full')}>
|
<div className={cn('flex items-center w-full')}>
|
||||||
{!compact && (
|
{!compact && (
|
||||||
<div className='flex items-center pr-2 shrink-0' style={{ width: '40%' }}>
|
<div
|
||||||
|
className="flex items-center pr-2 shrink-0"
|
||||||
|
style={{ width: '40%' }}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<Avatar isActive={active} seed={userNumericHash} isAssist={isAssist} />
|
<Avatar
|
||||||
|
isActive={active}
|
||||||
|
seed={userNumericHash}
|
||||||
|
isAssist={isAssist}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="flex flex-col overflow-hidden color-gray-medium ml-3 justify-between items-center shrink-0">
|
||||||
className='flex flex-col overflow-hidden color-gray-medium ml-3 justify-between items-center shrink-0'>
|
|
||||||
<div
|
<div
|
||||||
className={cn('text-lg', {
|
className={cn('text-lg', {
|
||||||
'color-teal cursor-pointer': !disableUser && hasUserId && !props.isDisabled,
|
'color-teal cursor-pointer':
|
||||||
[stl.userName]: !disableUser && hasUserId && !props.isDisabled,
|
!disableUser && hasUserId && !props.isDisabled,
|
||||||
'color-gray-medium': disableUser || !hasUserId
|
[stl.userName]:
|
||||||
|
!disableUser && hasUserId && !props.isDisabled,
|
||||||
|
'color-gray-medium': disableUser || !hasUserId,
|
||||||
})}
|
})}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
!disableUser && !hasUserFilter && hasUserId
|
!disableUser && !hasUserFilter && hasUserId
|
||||||
|
|
@ -190,7 +235,7 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
>
|
>
|
||||||
<TextEllipsis
|
<TextEllipsis
|
||||||
text={userDisplayName}
|
text={userDisplayName}
|
||||||
maxWidth='200px'
|
maxWidth="200px"
|
||||||
popupProps={{ inverted: true, size: 'tiny' }}
|
popupProps={{ inverted: true, size: 'tiny' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -199,7 +244,7 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
style={{ width: compact ? '40%' : '20%' }}
|
style={{ width: compact ? '40%' : '20%' }}
|
||||||
className='px-2 flex flex-col justify-between'
|
className="px-2 flex flex-col justify-between"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|
@ -208,7 +253,9 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
title={
|
title={
|
||||||
<div className={'flex flex-col gap-1'}>
|
<div className={'flex flex-col gap-1'}>
|
||||||
<span>
|
<span>
|
||||||
Local Time: {formatTimeOrDate(startedAt, timezone, true)} {timezone.label}
|
Local Time:{' '}
|
||||||
|
{formatTimeOrDate(startedAt, timezone, true)}{' '}
|
||||||
|
{timezone.label}
|
||||||
</span>
|
</span>
|
||||||
{userTimezone ? (
|
{userTimezone ? (
|
||||||
<span>
|
<span>
|
||||||
|
|
@ -217,7 +264,7 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
startedAt,
|
startedAt,
|
||||||
{
|
{
|
||||||
label: userTimezone.split('+').join(' +'),
|
label: userTimezone.split('+').join(' +'),
|
||||||
value: userTimezone.split(':')[0]
|
value: userTimezone.split(':')[0],
|
||||||
},
|
},
|
||||||
true
|
true
|
||||||
)}{' '}
|
)}{' '}
|
||||||
|
|
@ -226,36 +273,49 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className='w-fit !block'
|
className="w-fit !block"
|
||||||
>
|
>
|
||||||
<TextEllipsis
|
<TextEllipsis
|
||||||
text={formatTimeOrDate(
|
text={formatTimeOrDate(
|
||||||
startedAt,
|
startedAt,
|
||||||
shownTimezone === 'user' && userTimezone
|
shownTimezone === 'user' && userTimezone
|
||||||
? {
|
? {
|
||||||
label: userTimezone.split('+').join(' +'),
|
label: userTimezone.split('+').join(' +'),
|
||||||
value: userTimezone.split(':')[0]
|
value: userTimezone.split(':')[0],
|
||||||
}
|
}
|
||||||
: timezone
|
: timezone
|
||||||
)}
|
)}
|
||||||
popupProps={{ inverted: true, size: 'tiny' }}
|
popupProps={{ inverted: true, size: 'tiny' }}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center color-gray-medium py-1'>
|
<div className="flex items-center color-gray-medium py-1">
|
||||||
{!isAssist && (
|
{!isAssist && (
|
||||||
<>
|
<>
|
||||||
<div className='color-gray-medium'>
|
<div className="color-gray-medium">
|
||||||
<span className='mr-1'>{eventsCount}</span>
|
<span className="mr-1">{eventsCount}</span>
|
||||||
<span>{eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event'}</span>
|
<span>
|
||||||
|
{eventsCount === 0 || eventsCount > 1
|
||||||
|
? 'Events'
|
||||||
|
: 'Event'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Icon name='circle-fill' size={3} className='mx-4' />
|
<Icon name="circle-fill" size={3} className="mx-4" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div>{live || props.live ? <Counter startTime={startedAt} /> : formattedDuration}</div>
|
<div>
|
||||||
|
{live || props.live ? (
|
||||||
|
<Counter startTime={startedAt} />
|
||||||
|
) : (
|
||||||
|
formattedDuration
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ width: '30%' }} className='px-2 flex flex-col justify-between'>
|
<div
|
||||||
|
style={{ width: '30%' }}
|
||||||
|
className="px-2 flex flex-col justify-between"
|
||||||
|
>
|
||||||
<div style={{ height: '21px' }}>
|
<div style={{ height: '21px' }}>
|
||||||
<CountryFlag
|
<CountryFlag
|
||||||
userCity={userCity}
|
userCity={userCity}
|
||||||
|
|
@ -276,14 +336,17 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
{userOs && userBrowser ? (
|
{userOs && userBrowser ? (
|
||||||
<Icon name="circle-fill" size={3} className="mx-4" />
|
<Icon name="circle-fill" size={3} className="mx-4" />
|
||||||
) : null}
|
) : null}
|
||||||
<span className={/ios/i.test(userOs) ? '' : 'capitalize'} style={{ maxWidth: '70px' }}>
|
<span
|
||||||
|
className={/ios/i.test(userOs) ? '' : 'capitalize'}
|
||||||
|
style={{ maxWidth: '70px' }}
|
||||||
|
>
|
||||||
<TextEllipsis
|
<TextEllipsis
|
||||||
text={/ios/i.test(userOs) ? 'iOS' : capitalize(userOs)}
|
text={/ios/i.test(userOs) ? 'iOS' : capitalize(userOs)}
|
||||||
popupProps={{ inverted: true, size: 'tiny' }}
|
popupProps={{ inverted: true, size: 'tiny' }}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<Icon name='circle-fill' size={3} className='mx-4' />
|
<Icon name="circle-fill" size={3} className="mx-4" />
|
||||||
<span className='capitalize' style={{ maxWidth: '70px' }}>
|
<span className="capitalize" style={{ maxWidth: '70px' }}>
|
||||||
<TextEllipsis
|
<TextEllipsis
|
||||||
text={capitalize(userDeviceType)}
|
text={capitalize(userDeviceType)}
|
||||||
popupProps={{ inverted: true, size: 'tiny' }}
|
popupProps={{ inverted: true, size: 'tiny' }}
|
||||||
|
|
@ -292,35 +355,44 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isSessions && (
|
{isSessions && (
|
||||||
<div style={{ width: '10%' }} className='self-center px-2 flex items-center'>
|
<div
|
||||||
|
style={{ width: '10%' }}
|
||||||
|
className="self-center px-2 flex items-center"
|
||||||
|
>
|
||||||
<ErrorBars count={issueTypes.length} />
|
<ErrorBars count={issueTypes.length} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex items-center'>
|
<div className="flex items-center">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
stl.playLink,
|
stl.playLink,
|
||||||
props.isDisabled ? 'cursor-not-allowed' : 'cursor-pointer'
|
props.isDisabled ? 'cursor-not-allowed' : 'cursor-pointer'
|
||||||
)}
|
)}
|
||||||
id='play-button'
|
id="play-button"
|
||||||
data-viewed={viewed}
|
data-viewed={viewed}
|
||||||
>
|
>
|
||||||
{live && session.isCallActive && session.agentIds!.length > 0 ? (
|
{live && session.isCallActive && session.agentIds!.length > 0 ? (
|
||||||
<div className='mr-4'>
|
<div className="mr-4">
|
||||||
<Label className='bg-gray-lightest p-1 px-2 rounded-lg'>
|
<Label className="bg-gray-lightest p-1 px-2 rounded-lg">
|
||||||
<span className='color-gray-medium text-xs' style={{ whiteSpace: 'nowrap' }}>
|
<span
|
||||||
|
className="color-gray-medium text-xs"
|
||||||
|
style={{ whiteSpace: 'nowrap' }}
|
||||||
|
>
|
||||||
CALL IN PROGRESS
|
CALL IN PROGRESS
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{isSessions && (
|
{isSessions && (
|
||||||
<div className='mr-4 flex-shrink-0 w-24'>
|
<div className="mr-4 flex-shrink-0 w-24">
|
||||||
{isLastPlayed && (
|
{isLastPlayed && (
|
||||||
<Label className='bg-gray-lightest p-1 px-2 rounded-lg'>
|
<Label className="bg-gray-lightest p-1 px-2 rounded-lg">
|
||||||
<span className='color-gray-medium text-xs' style={{ whiteSpace: 'nowrap' }}>
|
<span
|
||||||
|
className="color-gray-medium text-xs"
|
||||||
|
style={{ whiteSpace: 'nowrap' }}
|
||||||
|
>
|
||||||
LAST PLAYED
|
LAST PLAYED
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
@ -329,15 +401,15 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
)}
|
)}
|
||||||
{props.isAdd ? (
|
{props.isAdd ? (
|
||||||
<div
|
<div
|
||||||
className='rounded-full border-tealx p-2 border'
|
className="rounded-full border-tealx p-2 border"
|
||||||
onClick={() => (props.isDisabled ? null : props.onClick())}
|
onClick={() => (props.isDisabled ? null : props.onClick())}
|
||||||
>
|
>
|
||||||
<div className='bg-tealx rounded-full p-2'>
|
<div className="bg-tealx rounded-full p-2">
|
||||||
<Icon name='plus' size={16} color='white' />
|
<Icon name="plus" size={16} color="white" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div onMouseEnter={handleHover}>
|
||||||
<PlayLink
|
<PlayLink
|
||||||
isAssist={isAssist}
|
isAssist={isAssist}
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
|
|
@ -345,21 +417,24 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
queryParams={queryParams}
|
queryParams={queryParams}
|
||||||
query={query}
|
query={query}
|
||||||
|
beforeOpen={props.live || isAssist ? undefined : openSession}
|
||||||
/>
|
/>
|
||||||
{bookmarked && (
|
{bookmarked && (
|
||||||
<div className='ml-2 cursor-pointer'>
|
<div className="ml-2 cursor-pointer">
|
||||||
<ItemMenu bold items={menuItems} />
|
<ItemMenu bold items={menuItems} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{_metaList.length > 0 && <SessionMetaList className='mt-4' metaList={_metaList} />}
|
{_metaList.length > 0 && (
|
||||||
|
<SessionMetaList className="mt-4" metaList={_metaList} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(observer(SessionItem));
|
export default withRouter(connect(null, { presetSession })(observer(SessionItem)));
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ const SET_ACTIVE_TAB = 'sessions/SET_ACTIVE_TAB';
|
||||||
|
|
||||||
const CLEAR_CURRENT_SESSION = 'sessions/CLEAR_CURRENT_SESSION'
|
const CLEAR_CURRENT_SESSION = 'sessions/CLEAR_CURRENT_SESSION'
|
||||||
|
|
||||||
|
const PREFETCH_SESSION = 'sessions/PREFETCH_SESSION'
|
||||||
|
|
||||||
const range = getDateRangeFromValue(LAST_7_DAYS);
|
const range = getDateRangeFromValue(LAST_7_DAYS);
|
||||||
const defaultDateFilters = {
|
const defaultDateFilters = {
|
||||||
url: '',
|
url: '',
|
||||||
|
|
@ -60,6 +62,7 @@ const initObj = {
|
||||||
list: [],
|
list: [],
|
||||||
sessionIds: [],
|
sessionIds: [],
|
||||||
current: new Session(),
|
current: new Session(),
|
||||||
|
prefetched: false,
|
||||||
eventsAsked: false,
|
eventsAsked: false,
|
||||||
total: 0,
|
total: 0,
|
||||||
keyMap: Map(),
|
keyMap: Map(),
|
||||||
|
|
@ -185,6 +188,13 @@ const reducer = (state = initialState, action: IAction) => {
|
||||||
|
|
||||||
return state
|
return state
|
||||||
.set('current', session)
|
.set('current', session)
|
||||||
|
.set('prefetched', false)
|
||||||
|
}
|
||||||
|
case PREFETCH_SESSION: {
|
||||||
|
const { data } = action;
|
||||||
|
return state
|
||||||
|
.set('current', data)
|
||||||
|
.set('prefetched', true);
|
||||||
}
|
}
|
||||||
case FETCH_EVENTS.SUCCESS: {
|
case FETCH_EVENTS.SUCCESS: {
|
||||||
const {
|
const {
|
||||||
|
|
@ -454,6 +464,13 @@ export const fetchV2 = (sessionId: string) =>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function presetSession(sessionData) {
|
||||||
|
return {
|
||||||
|
type: PREFETCH_SESSION,
|
||||||
|
data: sessionData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function clearCurrentSession() {
|
export function clearCurrentSession() {
|
||||||
return {
|
return {
|
||||||
type: CLEAR_CURRENT_SESSION
|
type: CLEAR_CURRENT_SESSION
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { getDateRangeFromValue } from "App/dateRange";
|
||||||
import { getRE, setSessionFilter, getSessionFilter, compareJsonObjects, cleanSessionFilters } from 'App/utils';
|
import { getRE, setSessionFilter, getSessionFilter, compareJsonObjects, cleanSessionFilters } from 'App/utils';
|
||||||
import store from 'App/store'
|
import store from 'App/store'
|
||||||
import { Note } from "App/services/NotesService";
|
import { Note } from "App/services/NotesService";
|
||||||
|
import { loadFile } from "../player/web/network/loadFiles";
|
||||||
|
|
||||||
class UserFilter {
|
class UserFilter {
|
||||||
endDate: number = new Date().getTime();
|
endDate: number = new Date().getTime();
|
||||||
|
|
@ -125,6 +126,7 @@ export default class SessionStore {
|
||||||
previousId = ''
|
previousId = ''
|
||||||
nextId = ''
|
nextId = ''
|
||||||
userTimezone = ''
|
userTimezone = ''
|
||||||
|
prefetchedMobUrls: Record<string, { data: Uint8Array, entryNum: number }> = {}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this, {
|
makeAutoObservable(this, {
|
||||||
|
|
@ -141,6 +143,28 @@ export default class SessionStore {
|
||||||
this.userFilter = new UserFilter();
|
this.userFilter = new UserFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getFirstMob(sessionId: string) {
|
||||||
|
const { domURL } = await sessionService.getFirstMobUrl(sessionId)
|
||||||
|
await loadFile(
|
||||||
|
domURL[0],
|
||||||
|
(data) => this.setPrefetchedMobUrl(sessionId, data)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setPrefetchedMobUrl(sessionId: string, fileData: Uint8Array) {
|
||||||
|
const keys = Object.keys(this.prefetchedMobUrls)
|
||||||
|
const toLimit = 10 - keys.length
|
||||||
|
if (toLimit < 0) {
|
||||||
|
const oldest = keys.sort(
|
||||||
|
(a, b) => this.prefetchedMobUrls[a].entryNum - this.prefetchedMobUrls[b].entryNum
|
||||||
|
)[0]
|
||||||
|
delete this.prefetchedMobUrls[oldest]
|
||||||
|
}
|
||||||
|
const nextEntryNum = keys.length > 0
|
||||||
|
? Math.max(...keys.map(key => this.prefetchedMobUrls[key].entryNum)) + 1 : 0
|
||||||
|
this.prefetchedMobUrls[sessionId] = { data: fileData, entryNum: nextEntryNum }
|
||||||
|
}
|
||||||
|
|
||||||
getSessions(filter: any): Promise<any> {
|
getSessions(filter: any): Promise<any> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
sessionService
|
sessionService
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,8 @@ export function createIOSPlayer(
|
||||||
export function createWebPlayer(
|
export function createWebPlayer(
|
||||||
session: SessionFilesInfo,
|
session: SessionFilesInfo,
|
||||||
wrapStore?: (s: IWebPlayerStore) => IWebPlayerStore,
|
wrapStore?: (s: IWebPlayerStore) => IWebPlayerStore,
|
||||||
uiErrorHandler?: { error: (msg: string) => void }
|
uiErrorHandler?: { error: (msg: string) => void },
|
||||||
|
prefetched?: boolean,
|
||||||
): [IWebPlayer, IWebPlayerStore] {
|
): [IWebPlayer, IWebPlayerStore] {
|
||||||
let store: WebPlayerStore = new SimpleStore<WebState>({
|
let store: WebPlayerStore = new SimpleStore<WebState>({
|
||||||
...WebPlayer.INITIAL_STATE,
|
...WebPlayer.INITIAL_STATE,
|
||||||
|
|
@ -49,7 +50,7 @@ export function createWebPlayer(
|
||||||
store = wrapStore(store);
|
store = wrapStore(store);
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = new WebPlayer(store, session, false, false, uiErrorHandler);
|
const player = new WebPlayer(store, session, false, false, uiErrorHandler, prefetched);
|
||||||
return [player, store];
|
return [player, store];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,17 @@ export default class MessageLoader {
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly session: SessionFilesInfo,
|
private session: SessionFilesInfo,
|
||||||
private store: Store<State>,
|
private store: Store<State>,
|
||||||
private messageManager: MessageManager | IOSMessageManager,
|
private messageManager: MessageManager | IOSMessageManager,
|
||||||
private isClickmap: boolean,
|
private isClickmap: boolean,
|
||||||
private uiErrorHandler?: { error: (msg: string) => void }
|
private uiErrorHandler?: { error: (msg: string) => void }
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
setSession(session: SessionFilesInfo) {
|
||||||
|
this.session = session
|
||||||
|
}
|
||||||
|
|
||||||
createNewParser(
|
createNewParser(
|
||||||
shouldDecrypt = true,
|
shouldDecrypt = true,
|
||||||
onMessagesDone: (msgs: PlayerMsg[], file?: string) => void,
|
onMessagesDone: (msgs: PlayerMsg[], file?: string) => void,
|
||||||
|
|
@ -145,6 +149,18 @@ export default class MessageLoader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preloaded = false;
|
||||||
|
async preloadFirstFile(data: Uint8Array) {
|
||||||
|
this.mobParser = this.createNewParser(true, this.processMessages, 'p:dom');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.mobParser(data)
|
||||||
|
this.preloaded = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('error parsing msgs', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async loadDomFiles(urls: string[], parser: (b: Uint8Array) => Promise<void>) {
|
async loadDomFiles(urls: string[], parser: (b: Uint8Array) => Promise<void>) {
|
||||||
if (urls.length > 0) {
|
if (urls.length > 0) {
|
||||||
this.store.update({ domLoading: true });
|
this.store.update({ domLoading: true });
|
||||||
|
|
@ -197,21 +213,25 @@ export default class MessageLoader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mobParser: (b: Uint8Array) => Promise<void>
|
||||||
loadMobs = async () => {
|
loadMobs = async () => {
|
||||||
const loadMethod =
|
const loadMethod =
|
||||||
this.session.domURL && this.session.domURL.length > 0
|
this.session.domURL && this.session.domURL.length > 0
|
||||||
? {
|
? {
|
||||||
mobUrls: this.session.domURL,
|
mobUrls: this.session.domURL,
|
||||||
parser: () =>
|
parser: () =>
|
||||||
this.createNewParser(true, this.processMessages, 'dom'),
|
this.createNewParser(true, this.processMessages, 'd:dom'),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
mobUrls: this.session.mobsUrl,
|
mobUrls: this.session.mobsUrl,
|
||||||
parser: () =>
|
parser: () =>
|
||||||
this.createNewParser(false, this.processMessages, 'dom'),
|
this.createNewParser(false, this.processMessages, 'm:dom'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const parser = loadMethod.parser();
|
if (!this.mobParser) {
|
||||||
|
this.mobParser = loadMethod.parser();
|
||||||
|
}
|
||||||
|
const parser = this.mobParser
|
||||||
const devtoolsParser = this.createNewParser(
|
const devtoolsParser = this.createNewParser(
|
||||||
true,
|
true,
|
||||||
this.processMessages,
|
this.processMessages,
|
||||||
|
|
@ -225,7 +245,7 @@ export default class MessageLoader {
|
||||||
* as a tradeoff we have some copy-paste code
|
* as a tradeoff we have some copy-paste code
|
||||||
* for the devtools file
|
* for the devtools file
|
||||||
* */
|
* */
|
||||||
await loadFiles([loadMethod.mobUrls[0]], parser);
|
if (!this.preloaded) await loadFiles([loadMethod.mobUrls[0]], parser);
|
||||||
this.messageManager.onFileReadFinally();
|
this.messageManager.onFileReadFinally();
|
||||||
const restDomFilesPromise = this.loadDomFiles(
|
const restDomFilesPromise = this.loadDomFiles(
|
||||||
[...loadMethod.mobUrls.slice(1)],
|
[...loadMethod.mobUrls.slice(1)],
|
||||||
|
|
|
||||||
|
|
@ -237,9 +237,9 @@ export default class MessageManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.waitingForFiles &&
|
this.waitingForFiles ||
|
||||||
this.lastMessageTime <= t &&
|
(this.lastMessageTime <= t &&
|
||||||
t !== this.session.duration.milliseconds
|
t !== this.session.duration.milliseconds)
|
||||||
) {
|
) {
|
||||||
this.setMessagesLoading(true);
|
this.setMessagesLoading(true);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,10 @@ export default class WebPlayer extends Player {
|
||||||
|
|
||||||
liveTimeTravel: false,
|
liveTimeTravel: false,
|
||||||
inspectorMode: false,
|
inspectorMode: false,
|
||||||
|
mobsFetched: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly inspectorController: InspectorController
|
private inspectorController: InspectorController
|
||||||
protected screen: Screen
|
protected screen: Screen
|
||||||
protected readonly messageManager: MessageManager
|
protected readonly messageManager: MessageManager
|
||||||
protected readonly messageLoader: MessageLoader
|
protected readonly messageLoader: MessageLoader
|
||||||
|
|
@ -34,7 +35,8 @@ export default class WebPlayer extends Player {
|
||||||
session: SessionFilesInfo,
|
session: SessionFilesInfo,
|
||||||
live: boolean,
|
live: boolean,
|
||||||
isClickMap = false,
|
isClickMap = false,
|
||||||
public readonly uiErrorHandler?: { error: (msg: string) => void }
|
public readonly uiErrorHandler?: { error: (msg: string) => void },
|
||||||
|
private readonly prefetched?: boolean,
|
||||||
) {
|
) {
|
||||||
let initialLists = live ? {} : {
|
let initialLists = live ? {} : {
|
||||||
event: session.events || [],
|
event: session.events || [],
|
||||||
|
|
@ -62,8 +64,10 @@ export default class WebPlayer extends Player {
|
||||||
this.screen = screen
|
this.screen = screen
|
||||||
this.messageManager = messageManager
|
this.messageManager = messageManager
|
||||||
this.messageLoader = messageLoader
|
this.messageLoader = messageLoader
|
||||||
if (!live) { // hack. TODO: split OfflinePlayer class
|
|
||||||
|
if (!live && !prefetched) { // hack. TODO: split OfflinePlayer class
|
||||||
void messageLoader.loadFiles()
|
void messageLoader.loadFiles()
|
||||||
|
wpState.update({ mobsFetched: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
this.targetMarker = new TargetMarker(this.screen, wpState)
|
this.targetMarker = new TargetMarker(this.screen, wpState)
|
||||||
|
|
@ -84,6 +88,30 @@ export default class WebPlayer extends Player {
|
||||||
window.playerJumpToTime = this.jump.bind(this)
|
window.playerJumpToTime = this.jump.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preloadFirstFile(data: Uint8Array) {
|
||||||
|
void this.messageLoader.preloadFirstFile(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
reinit(session: SessionFilesInfo) {
|
||||||
|
if (this.wpState.get().mobsFetched) return; // already initialized
|
||||||
|
this.messageLoader.setSession(session)
|
||||||
|
void this.messageLoader.loadFiles();
|
||||||
|
|
||||||
|
this.targetMarker = new TargetMarker(this.screen, this.wpState)
|
||||||
|
this.inspectorController = new InspectorController(this.screen, this.wpState)
|
||||||
|
|
||||||
|
|
||||||
|
const endTime = session.duration?.valueOf() || 0
|
||||||
|
this.wpState.update({
|
||||||
|
//@ts-ignore
|
||||||
|
session,
|
||||||
|
endTime, // : 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
window.playerJumpToTime = this.jump.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
updateLists = (session: any) => {
|
updateLists = (session: any) => {
|
||||||
const lists = {
|
const lists = {
|
||||||
event: session.events || [],
|
event: session.events || [],
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,14 @@ export default class SettingsService {
|
||||||
.catch((e) => Promise.reject(e));
|
.catch((e) => Promise.reject(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFirstMobUrl(sessionId: string): Promise<{ domURL: string[] }> {
|
||||||
|
return this.client
|
||||||
|
.get(`/sessions/${sessionId}/first-mob`)
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((j) => j.data || {})
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
getSessionInfo(sessionId: string, isLive?: boolean): Promise<ISession> {
|
getSessionInfo(sessionId: string, isLive?: boolean): Promise<ISession> {
|
||||||
return this.client
|
return this.client
|
||||||
.get(isLive ? `/assist/sessions/${sessionId}` : `/sessions/${sessionId}`)
|
.get(isLive ? `/assist/sessions/${sessionId}` : `/sessions/${sessionId}`)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue