refactor(SessionList): optimize component performance
- Fix TypeScript error with SessionItem JSX component - Convert SessionItem to use modern React hooks and patterns - Implement useCallback and useMemo for better rendering performance - Properly handle optional chaining for conditional properties - Remove console.log statements from search store - Fix useEffect dependencies to prevent unnecessary rerenders - Cleanup unused imports and commented code Signed-off-by: Shekar Siri <sshekarsiri@gmail.com>
This commit is contained in:
parent
fd64d721c6
commit
0af941e543
2 changed files with 164 additions and 105 deletions
|
|
@ -1,8 +1,8 @@
|
|||
import cn from 'classnames';
|
||||
import { Duration } from 'luxon';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { RouteComponentProps, useHistory, withRouter } from 'react-router-dom';
|
||||
|
||||
import { durationFormatted, formatTimeOrDate } from 'App/date';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
|
@ -10,16 +10,17 @@ import {
|
|||
assist as assistRoute,
|
||||
isRoute,
|
||||
liveSession,
|
||||
sessions as sessionsRoute,
|
||||
sessions as sessionsRoute
|
||||
} from 'App/routes';
|
||||
import { capitalize } from 'App/utils';
|
||||
import { Avatar, CountryFlag, Icon, Label, TextEllipsis, Tooltip } from 'UI';
|
||||
import { Avatar, CountryFlag, Icon, Label, TextEllipsis } from 'UI';
|
||||
|
||||
import Counter from './Counter';
|
||||
import ErrorBars from './ErrorBars';
|
||||
import PlayLink from './PlayLink';
|
||||
import SessionMetaList from './SessionMetaList';
|
||||
import stl from './sessionItem.module.css';
|
||||
import { Tooltip } from 'antd';
|
||||
|
||||
const ASSIST_ROUTE = assistRoute();
|
||||
const ASSIST_LIVE_SESSION = liveSession();
|
||||
|
|
@ -59,7 +60,6 @@ interface Props {
|
|||
hasUserFilter?: boolean;
|
||||
disableUser?: boolean;
|
||||
metaList?: Array<any>;
|
||||
// showActive?: boolean;
|
||||
lastPlayedSessionId?: string;
|
||||
live?: boolean;
|
||||
onClick?: any;
|
||||
|
|
@ -75,14 +75,16 @@ interface Props {
|
|||
const PREFETCH_STATE = {
|
||||
none: 0,
|
||||
loading: 1,
|
||||
fetched: 2,
|
||||
fetched: 2
|
||||
};
|
||||
|
||||
function SessionItem(props: RouteComponentProps & Props) {
|
||||
const { location } = useHistory();
|
||||
const { settingsStore, sessionStore } = useStore();
|
||||
const { timezone, shownTimezone } = settingsStore.sessionSettings;
|
||||
const [prefetchState, setPrefetched] = React.useState(PREFETCH_STATE.none);
|
||||
const [prefetchState, setPrefetched] = useState(PREFETCH_STATE.none);
|
||||
|
||||
// Destructure all props at the top
|
||||
const {
|
||||
session,
|
||||
onUserClick = () => null,
|
||||
|
|
@ -95,6 +97,10 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
ignoreAssist = false,
|
||||
bookmarked = false,
|
||||
query,
|
||||
// location,
|
||||
isDisabled,
|
||||
live: propsLive,
|
||||
isAdd
|
||||
} = props;
|
||||
|
||||
const {
|
||||
|
|
@ -113,41 +119,56 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
viewed,
|
||||
userDeviceType,
|
||||
userNumericHash,
|
||||
live,
|
||||
live: sessionLive,
|
||||
metadata,
|
||||
issueTypes,
|
||||
active,
|
||||
platform,
|
||||
timezone: userTimezone,
|
||||
isCallActive,
|
||||
agentIds
|
||||
} = session;
|
||||
|
||||
const location = props.location;
|
||||
const queryParams = Object.fromEntries(new URLSearchParams(location.search));
|
||||
// Memoize derived values
|
||||
const queryParams = useMemo(
|
||||
() => Object.fromEntries(new URLSearchParams(location.search)),
|
||||
[location.search]
|
||||
);
|
||||
|
||||
const isMobile = platform !== 'web';
|
||||
const formattedDuration = durationFormatted(duration);
|
||||
const formattedDuration = useMemo(() => durationFormatted(duration), [duration]);
|
||||
const hasUserId = userId || userAnonymousId;
|
||||
const isSessions = isRoute(SESSIONS_ROUTE, location.pathname);
|
||||
const isAssist =
|
||||
(!ignoreAssist &&
|
||||
(isRoute(ASSIST_ROUTE, location.pathname) ||
|
||||
isRoute(ASSIST_LIVE_SESSION, location.pathname) ||
|
||||
location.pathname.includes('multiview'))) ||
|
||||
props.live;
|
||||
|
||||
const isSessions = useMemo(
|
||||
() => isRoute(SESSIONS_ROUTE, location.pathname),
|
||||
[location.pathname]
|
||||
);
|
||||
|
||||
const isAssist = useMemo(() => {
|
||||
return (
|
||||
(!ignoreAssist &&
|
||||
(isRoute(ASSIST_ROUTE, location.pathname) ||
|
||||
isRoute(ASSIST_LIVE_SESSION, location.pathname) ||
|
||||
location.pathname.includes('multiview'))) ||
|
||||
propsLive
|
||||
);
|
||||
}, [ignoreAssist, location.pathname, propsLive]);
|
||||
|
||||
const isLastPlayed = lastPlayedSessionId === sessionId;
|
||||
const live = sessionLive || propsLive;
|
||||
const isMultiviewDisabled = isDisabled && location.pathname.includes('multiview');
|
||||
|
||||
const _metaList = Object.keys(metadata).map((key) => {
|
||||
const value = metadata[key];
|
||||
return { label: key, value };
|
||||
});
|
||||
// Memoize metadata list creation
|
||||
const _metaList = useMemo(() => {
|
||||
return Object.keys(metadata).map((key) => ({
|
||||
label: key,
|
||||
value: metadata[key]
|
||||
}));
|
||||
}, [metadata]);
|
||||
|
||||
const handleHover = async () => {
|
||||
if (
|
||||
prefetchState !== PREFETCH_STATE.none ||
|
||||
props.live ||
|
||||
isAssist ||
|
||||
isMobile
|
||||
)
|
||||
// Memoize event handlers
|
||||
const handleHover = useCallback(async () => {
|
||||
if (prefetchState !== PREFETCH_STATE.none || live || isAssist || isMobile)
|
||||
return;
|
||||
|
||||
setPrefetched(PREFETCH_STATE.loading);
|
||||
|
|
@ -157,24 +178,69 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
} catch (e) {
|
||||
console.error('Error while prefetching first mob', e);
|
||||
}
|
||||
};
|
||||
const populateData = () => {
|
||||
if (
|
||||
props.live ||
|
||||
isAssist ||
|
||||
prefetchState === PREFETCH_STATE.none ||
|
||||
isMobile
|
||||
) {
|
||||
}, [prefetchState, live, isAssist, isMobile, sessionStore, sessionId]);
|
||||
|
||||
const populateData = useCallback(() => {
|
||||
if (live || isAssist || prefetchState === PREFETCH_STATE.none || isMobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStore.prefetchSession(session);
|
||||
};
|
||||
}, [live, isAssist, prefetchState, isMobile, sessionStore, session]);
|
||||
|
||||
const handleUserClick = useCallback(() => {
|
||||
if (!disableUser && !hasUserFilter && hasUserId) {
|
||||
onUserClick(userId, userAnonymousId);
|
||||
}
|
||||
}, [disableUser, hasUserFilter, hasUserId, onUserClick, userId, userAnonymousId]);
|
||||
|
||||
const handleAddClick = useCallback(() => {
|
||||
if (!isDisabled && onClick) {
|
||||
onClick();
|
||||
}
|
||||
}, [isDisabled, onClick]);
|
||||
|
||||
// Memoize time formatting
|
||||
const formattedTime = useMemo(() => {
|
||||
const timezoneToUse =
|
||||
shownTimezone === 'user' && userTimezone
|
||||
? {
|
||||
label: userTimezone.split('+').join(' +'),
|
||||
value: userTimezone.split(':')[0]
|
||||
}
|
||||
: timezone;
|
||||
|
||||
return formatTimeOrDate(startedAt, timezoneToUse);
|
||||
}, [startedAt, shownTimezone, userTimezone, timezone]);
|
||||
|
||||
// Memoize tooltip content
|
||||
const timeTooltipContent = useMemo(() => {
|
||||
return (
|
||||
<div className={'flex flex-col gap-1'}>
|
||||
<span>
|
||||
Local Time: {formatTimeOrDate(startedAt, timezone, true)}{' '}
|
||||
{timezone.label}
|
||||
</span>
|
||||
{userTimezone ? (
|
||||
<span>
|
||||
User's Time:{' '}
|
||||
{formatTimeOrDate(
|
||||
startedAt,
|
||||
{
|
||||
label: userTimezone.split('+').join(' +'),
|
||||
value: userTimezone.split(':')[0]
|
||||
},
|
||||
true
|
||||
)}{' '}
|
||||
{userTimezone}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}, [startedAt, timezone, userTimezone]);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
delay={0}
|
||||
title={`Session already added into the multiview`}
|
||||
disabled={!props.isDisabled || !location.pathname.includes('multiview')}
|
||||
title={!isMultiviewDisabled ? '' : `Session already added into the multiview`}
|
||||
>
|
||||
<div
|
||||
className={cn(stl.sessionItem, 'flex flex-col p-4')}
|
||||
|
|
@ -204,16 +270,12 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
<div
|
||||
className={cn('text-lg', {
|
||||
'color-teal cursor-pointer':
|
||||
!disableUser && hasUserId && !props.isDisabled,
|
||||
!disableUser && hasUserId && !isDisabled,
|
||||
[stl.userName]:
|
||||
!disableUser && hasUserId && !props.isDisabled,
|
||||
'color-gray-medium': disableUser || !hasUserId,
|
||||
!disableUser && hasUserId && !isDisabled,
|
||||
'color-gray-medium': disableUser || !hasUserId
|
||||
})}
|
||||
onClick={() =>
|
||||
!disableUser && !hasUserFilter && hasUserId
|
||||
? onUserClick(userId, userAnonymousId)
|
||||
: null
|
||||
}
|
||||
onClick={handleUserClick}
|
||||
>
|
||||
<TextEllipsis
|
||||
text={userDisplayName}
|
||||
|
|
@ -235,42 +297,12 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
<div>
|
||||
<Tooltip
|
||||
delay={0}
|
||||
disabled={props.isDisabled}
|
||||
title={
|
||||
<div className={'flex flex-col gap-1'}>
|
||||
<span>
|
||||
Local Time:{' '}
|
||||
{formatTimeOrDate(startedAt, timezone, true)}{' '}
|
||||
{timezone.label}
|
||||
</span>
|
||||
{userTimezone ? (
|
||||
<span>
|
||||
User's Time:{' '}
|
||||
{formatTimeOrDate(
|
||||
startedAt,
|
||||
{
|
||||
label: userTimezone.split('+').join(' +'),
|
||||
value: userTimezone.split(':')[0],
|
||||
},
|
||||
true
|
||||
)}{' '}
|
||||
{userTimezone}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
disabled={isDisabled}
|
||||
title={timeTooltipContent}
|
||||
className="w-fit !block"
|
||||
>
|
||||
<TextEllipsis
|
||||
text={formatTimeOrDate(
|
||||
startedAt,
|
||||
shownTimezone === 'user' && userTimezone
|
||||
? {
|
||||
label: userTimezone.split('+').join(' +'),
|
||||
value: userTimezone.split(':')[0],
|
||||
}
|
||||
: timezone
|
||||
)}
|
||||
text={formattedTime}
|
||||
popupProps={{ inverted: true, size: 'tiny' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
|
@ -290,7 +322,7 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
</>
|
||||
)}
|
||||
<div>
|
||||
{live || props.live ? (
|
||||
{live ? (
|
||||
<Counter startTime={startedAt} />
|
||||
) : (
|
||||
formattedDuration
|
||||
|
|
@ -311,27 +343,31 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
/>
|
||||
</div>
|
||||
<div className="color-gray-medium flex items-center py-1">
|
||||
{userBrowser ? (
|
||||
{userBrowser && (
|
||||
<span className="capitalize" style={{ maxWidth: '70px' }}>
|
||||
<TextEllipsis
|
||||
text={capitalize(userBrowser)}
|
||||
popupProps={{ inverted: true, size: 'tiny' }}
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
{userOs && userBrowser ? (
|
||||
)}
|
||||
{userOs && userBrowser && (
|
||||
<Icon name="circle-fill" size={3} className="mx-4" />
|
||||
) : null}
|
||||
<span
|
||||
className={/ios/i.test(userOs) ? '' : 'capitalize'}
|
||||
style={{ maxWidth: '70px' }}
|
||||
>
|
||||
<TextEllipsis
|
||||
text={/ios/i.test(userOs) ? 'iOS' : capitalize(userOs)}
|
||||
popupProps={{ inverted: true, size: 'tiny' }}
|
||||
/>
|
||||
</span>
|
||||
<Icon name="circle-fill" size={3} className="mx-4" />
|
||||
)}
|
||||
{userOs && (
|
||||
<span
|
||||
className={/ios/i.test(userOs) ? '' : 'capitalize'}
|
||||
style={{ maxWidth: '70px' }}
|
||||
>
|
||||
<TextEllipsis
|
||||
text={/ios/i.test(userOs) ? 'iOS' : capitalize(userOs)}
|
||||
popupProps={{ inverted: true, size: 'tiny' }}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{userOs && (
|
||||
<Icon name="circle-fill" size={3} className="mx-4" />
|
||||
)}
|
||||
<span className="capitalize" style={{ maxWidth: '70px' }}>
|
||||
<TextEllipsis
|
||||
text={capitalize(userDeviceType)}
|
||||
|
|
@ -354,12 +390,12 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
<div
|
||||
className={cn(
|
||||
stl.playLink,
|
||||
props.isDisabled ? 'cursor-not-allowed' : 'cursor-pointer'
|
||||
isDisabled ? 'cursor-not-allowed' : 'cursor-pointer'
|
||||
)}
|
||||
id="play-button"
|
||||
data-viewed={viewed}
|
||||
>
|
||||
{live && session.isCallActive && session.agentIds!.length > 0 ? (
|
||||
{live && isCallActive && agentIds && agentIds.length > 0 && (
|
||||
<div className="mr-4">
|
||||
<Label className="bg-gray-lightest p-1 px-2 rounded-lg">
|
||||
<span
|
||||
|
|
@ -370,7 +406,7 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
</span>
|
||||
</Label>
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
{isSessions && (
|
||||
<div className="mr-4 flex-shrink-0 w-24">
|
||||
{isLastPlayed && (
|
||||
|
|
@ -385,10 +421,10 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
{props.isAdd ? (
|
||||
{isAdd ? (
|
||||
<div
|
||||
className="rounded-full border-tealx p-2 border"
|
||||
onClick={() => (props.isDisabled ? null : props.onClick())}
|
||||
onClick={handleAddClick}
|
||||
>
|
||||
<div className="bg-tealx rounded-full p-2">
|
||||
<Icon name="plus" size={16} color="white" />
|
||||
|
|
@ -402,7 +438,7 @@ function SessionItem(props: RouteComponentProps & Props) {
|
|||
onClick={onClick}
|
||||
queryParams={queryParams}
|
||||
query={query}
|
||||
beforeOpen={props.live || isAssist ? undefined : populateData}
|
||||
beforeOpen={live || isAssist ? undefined : populateData}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -237,6 +237,29 @@ class SearchStore {
|
|||
void this.fetchSessions(true);
|
||||
}
|
||||
|
||||
checkForLatestSessionCount() {
|
||||
const filter = this.instance.toSearch();
|
||||
if (this.latestRequestTime) {
|
||||
const period = Period({
|
||||
rangeName: CUSTOM_RANGE,
|
||||
start: this.latestRequestTime,
|
||||
end: Date.now(),
|
||||
});
|
||||
const newTimestamps: any = period.toJSON();
|
||||
filter.startDate = newTimestamps.startDate;
|
||||
filter.endDate = newTimestamps.endDate;
|
||||
}
|
||||
// TODO - dedicated API endpoint to get the count of latest sessions, or show X+ sessions
|
||||
delete filter.limit;
|
||||
delete filter.page;
|
||||
searchService.checkLatestSessions(filter).then((response: any) => {
|
||||
console.log('response', response);
|
||||
// runInAction(() => {
|
||||
// this.latestList = List(response);
|
||||
// });
|
||||
});
|
||||
}
|
||||
|
||||
checkForLatestSessions() {
|
||||
const filter = this.instance.toSearch();
|
||||
if (this.latestRequestTime) {
|
||||
|
|
@ -395,7 +418,7 @@ class SearchStore {
|
|||
}
|
||||
}
|
||||
|
||||
this.latestRequestTime = Date.now();
|
||||
this.latestRequestTime = filter.startDate;
|
||||
this.latestList = List();
|
||||
this.searchInProgress = true;
|
||||
await sessionStore.fetchSessions(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue