Skip to content

Commit 7b851e6

Browse files
feat: Call History Table (#37839)
Co-authored-by: Tasso Evangelista <2263066+tassoevan@users.noreply.github.com>
1 parent 626f0d2 commit 7b851e6

33 files changed

+1124
-60
lines changed

apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsMenu.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,17 @@ type NavBarControlsMenuProps = Omit<HTMLAttributes<HTMLElement>, 'is'> & {
1010
omnichannelItems: GenericMenuItemProps[];
1111
isPressed: boolean;
1212
callItem?: GenericMenuItemProps;
13+
callHistoryItem?: GenericMenuItemProps;
1314
};
1415

15-
const NavBarControlsMenu = ({ omnichannelItems, isPressed, callItem, ...props }: NavBarControlsMenuProps) => {
16+
const NavBarControlsMenu = ({ omnichannelItems, isPressed, callItem, callHistoryItem, ...props }: NavBarControlsMenuProps) => {
1617
const { t } = useTranslation();
1718
const showOmnichannel = useOmnichannelEnabled();
1819

1920
const sections = [
2021
{
2122
title: t('Voice_Call'),
22-
items: callItem ? [callItem] : [],
23+
items: callItem || callHistoryItem ? ([callItem, callHistoryItem].filter(Boolean) as GenericMenuItemProps[]) : [],
2324
},
2425
{
2526
title: t('Omnichannel'),

apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsWithCall.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type NavBarControlsMenuProps = Omit<HTMLAttributes<HTMLElement>, 'is'> & {
99
omnichannelItems: GenericMenuItemProps[];
1010
isPressed: boolean;
1111
callItem?: GenericMenuItemProps;
12+
callHistoryItem?: GenericMenuItemProps;
1213
};
1314

1415
const NavBarControlsWithCall = ({ omnichannelItems, isPressed, ...props }: NavBarControlsMenuProps) => {

apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsWithData.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { GenericMenuItemProps } from '@rocket.chat/ui-client';
22
import { useMediaCallAction } from '@rocket.chat/ui-voip';
33
import type { HTMLAttributes } from 'react';
4+
import { useTranslation } from 'react-i18next';
45

56
import NavBarControlsMenu from './NavBarControlsMenu';
67
import NavbarControlsWithCall from './NavBarControlsWithCall';
@@ -12,6 +13,7 @@ import { useOmnichannelQueueAction } from '../NavBarOmnichannelGroup/hooks/useOm
1213
type NavBarControlsMenuProps = Omit<HTMLAttributes<HTMLElement>, 'is'>;
1314

1415
const NavBarControlsWithData = (props: NavBarControlsMenuProps) => {
16+
const { t } = useTranslation();
1517
const isCallEnabled = useIsCallEnabled();
1618

1719
const callAction = useMediaCallAction();
@@ -46,6 +48,14 @@ const NavBarControlsWithData = (props: NavBarControlsMenuProps) => {
4648
}
4749
: undefined;
4850

51+
const callHistoryItem = callAction
52+
? {
53+
id: 'rcx-media-call-history',
54+
icon: 'clock' as const,
55+
content: t('Call_history'),
56+
}
57+
: undefined;
58+
4959
const omnichannelItems = [
5060
queueEnabled && {
5161
id: 'omnichannelQueue',
@@ -70,10 +80,26 @@ const NavBarControlsWithData = (props: NavBarControlsMenuProps) => {
7080
const isPressed = isQueuePressed || isContactPressed;
7181

7282
if (isCallEnabled) {
73-
return <NavbarControlsWithCall callItem={callItem} omnichannelItems={omnichannelItems} isPressed={isPressed} {...props} />;
83+
return (
84+
<NavbarControlsWithCall
85+
callItem={callItem}
86+
callHistoryItem={callHistoryItem}
87+
omnichannelItems={omnichannelItems}
88+
isPressed={isPressed}
89+
{...props}
90+
/>
91+
);
7492
}
7593

76-
return <NavBarControlsMenu callItem={callItem} omnichannelItems={omnichannelItems} isPressed={isPressed} {...props} />;
94+
return (
95+
<NavBarControlsMenu
96+
callHistoryItem={callHistoryItem}
97+
callItem={callItem}
98+
omnichannelItems={omnichannelItems}
99+
isPressed={isPressed}
100+
{...props}
101+
/>
102+
);
77103
};
78104

79105
export default NavBarControlsWithData;

apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarVoipGroup.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
import { NavBarGroup, NavBarItem } from '@rocket.chat/fuselage';
2+
import { useRouter } from '@rocket.chat/ui-contexts';
23
import { useMediaCallAction } from '@rocket.chat/ui-voip';
4+
import { useCallback } from 'react';
35
import { useTranslation } from 'react-i18next';
46

57
const NavBarVoipGroup = () => {
68
const { t } = useTranslation();
79

810
const callAction = useMediaCallAction();
11+
const router = useRouter();
12+
const openCallHistory = useCallback(() => {
13+
router.navigate('/call-history');
14+
}, [router]);
915
if (!callAction) {
1016
return null;
1117
}
1218

1319
return (
1420
<NavBarGroup aria-label={t('Voice_Call')}>
1521
<NavBarItem title={callAction.title} icon={callAction.icon} onClick={() => callAction.action()} />
22+
<NavBarItem title={t('Call_history')} icon='clock' onClick={openCallHistory} />
1623
</NavBarGroup>
1724
);
1825
};

apps/meteor/client/startup/routes.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const ResetPasswordPage = lazy(() =>
2424
const OAuthAuthorizationPage = lazy(() => import('../views/oauth/OAuthAuthorizationPage'));
2525
const OAuthErrorPage = lazy(() => import('../views/oauth/OAuthErrorPage'));
2626
const NotFoundPage = lazy(() => import('../views/notFound/NotFoundPage'));
27+
const CallHistoryPage = lazy(() => import('../views/mediaCallHistory/CallHistoryPage'));
2728

2829
declare module '@rocket.chat/ui-contexts' {
2930
interface IRouterPaths {
@@ -107,6 +108,10 @@ declare module '@rocket.chat/ui-contexts' {
107108
pathname: `/saml/${string}`;
108109
pattern: '/saml/:token';
109110
};
111+
'call-history': {
112+
pathname: `/call-history${`/details/${string}` | ''}`;
113+
pattern: '/call-history/:tab?/:historyId?';
114+
};
110115
}
111116
}
112117

@@ -233,6 +238,15 @@ router.defineRoutes([
233238
id: 'saml',
234239
element: appLayout.wrap(<SAMLLoginRoute />),
235240
},
241+
{
242+
path: '/call-history/:tab?/:historyId?',
243+
id: 'call-history',
244+
element: appLayout.wrap(
245+
<MainLayout>
246+
<CallHistoryPage />
247+
</MainLayout>,
248+
),
249+
},
236250
{
237251
path: '*',
238252
id: 'not-found',
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import { Pagination } from '@rocket.chat/fuselage';
2+
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
3+
import { useSort, Page, PageHeader, PageContent, usePagination, GenericTableLoadingRow } from '@rocket.chat/ui-client';
4+
import { useEndpoint, useRouteParameter, useRouter } from '@rocket.chat/ui-contexts';
5+
import { MediaCallHistoryTable, isCallHistoryUnknownContact, isCallHistoryTableInternalContact } from '@rocket.chat/ui-voip';
6+
import type { CallHistoryTableInternalContact, CallHistoryUnknownContact, CallHistoryTableExternalContact } from '@rocket.chat/ui-voip';
7+
import { useQuery } from '@tanstack/react-query';
8+
import { useCallback, useMemo, useState } from 'react';
9+
import { useTranslation } from 'react-i18next';
10+
11+
import CallHistoryPageFilters, { useCallHistoryPageFilters } from './CallHistoryPageFilters';
12+
import CallHistoryRowExternalUser from './CallHistoryRowExternalUser';
13+
import CallHistoryRowInternalUser from './CallHistoryRowInternalUser';
14+
import CallHistoryRowUnknownUser from './CallHistoryRowUnknownUser';
15+
import MediaCallHistoryContextualbar from './MediaCallHistoryContextualbar';
16+
import GenericNoResults from '../../components/GenericNoResults';
17+
import UserInfoWithData from '../room/contextualBar/UserInfo/UserInfoWithData';
18+
19+
const getSort = (sortBy: 'contact' | 'type' | 'status' | 'timestamp', sortDirection: 'asc' | 'desc') => {
20+
const sortDirectionValue = sortDirection === 'asc' ? 1 : -1;
21+
switch (sortBy) {
22+
case 'type':
23+
return { direction: sortDirectionValue, ts: -1 };
24+
case 'status':
25+
return { state: sortDirectionValue, ts: -1 };
26+
case 'timestamp':
27+
return { ts: sortDirectionValue };
28+
case 'contact':
29+
return { contactName: sortDirectionValue, contactUsername: sortDirectionValue, contactExtension: sortDirectionValue, ts: -1 };
30+
default:
31+
return { ts: -1 };
32+
}
33+
};
34+
35+
const getStateFilter = <T extends string[]>(states: T): T | [...T, 'error'] | undefined => {
36+
if (states.length === 0) {
37+
return undefined;
38+
}
39+
if (states.includes('failed')) {
40+
return [...states, 'error'];
41+
}
42+
return states;
43+
};
44+
45+
type DetailsTab = {
46+
openTab: 'details';
47+
rid: string;
48+
};
49+
50+
type UserInfoTab = {
51+
openTab: 'user-info';
52+
rid: string;
53+
userId: string;
54+
};
55+
56+
type Tab = DetailsTab | UserInfoTab;
57+
58+
const CallHistoryPage = () => {
59+
const { t } = useTranslation();
60+
const [tab, setTab] = useState<Tab | null>(null);
61+
const sortProps = useSort<'contact' | 'type' | 'status' | 'timestamp'>('timestamp', 'desc');
62+
63+
const getCallHistory = useEndpoint('GET', '/v1/call-history.list');
64+
const { setItemsPerPage, setCurrent, ...paginationProps } = usePagination();
65+
66+
const router = useRouter();
67+
const historyId = useRouteParameter('historyId');
68+
69+
const filterProps = useCallHistoryPageFilters();
70+
71+
const { searchText, type, states } = filterProps;
72+
73+
const debouncedSearchText = useDebouncedValue(searchText, 400);
74+
75+
const onClickRow = useCallback(
76+
(rid: string, _id: string) => {
77+
router.navigate(`/call-history/details/${_id}`);
78+
setTab({ openTab: 'details', rid });
79+
},
80+
[router],
81+
);
82+
83+
const openUserInfo = useCallback(
84+
(userId: string, rid: string) => {
85+
setTab({ openTab: 'user-info', rid, userId });
86+
},
87+
[setTab],
88+
);
89+
90+
const closeTab = useCallback(() => {
91+
setTab(null);
92+
router.navigate('/call-history');
93+
}, [router]);
94+
95+
const handleBack = useCallback(() => {
96+
if (historyId && tab?.rid) {
97+
onClickRow(tab.rid, historyId);
98+
return;
99+
}
100+
setTab(null);
101+
}, [setTab, historyId, onClickRow, tab?.rid]);
102+
103+
const { data, isPending, error, refetch } = useQuery({
104+
queryKey: [
105+
'call-history',
106+
'list',
107+
sortProps.sortBy,
108+
sortProps.sortDirection,
109+
paginationProps.current,
110+
paginationProps.itemsPerPage,
111+
type,
112+
states,
113+
debouncedSearchText,
114+
],
115+
queryFn: () => {
116+
const sort = getSort(sortProps.sortBy, sortProps.sortDirection);
117+
const stateFilter = getStateFilter(states);
118+
119+
return getCallHistory({
120+
count: paginationProps.itemsPerPage,
121+
offset: paginationProps.current,
122+
sort: JSON.stringify(sort),
123+
...(type !== 'all' && { direction: type }),
124+
...(stateFilter && { state: stateFilter }),
125+
...(debouncedSearchText && { filter: debouncedSearchText }),
126+
});
127+
},
128+
});
129+
130+
const tableData = useMemo(() => {
131+
return data?.items.map((item) => {
132+
if (item.external) {
133+
return {
134+
_id: item._id,
135+
contact: item.contactExtension
136+
? ({ number: item.contactExtension } as CallHistoryTableExternalContact)
137+
: ({ unknown: true } as CallHistoryUnknownContact),
138+
type: item.direction,
139+
status: item.state,
140+
timestamp: item.ts,
141+
duration: item.duration,
142+
};
143+
}
144+
if (!item.contactUsername || !item.contactName) {
145+
return {
146+
_id: item._id,
147+
contact: { unknown: true } as CallHistoryUnknownContact,
148+
type: item.direction,
149+
status: item.state,
150+
timestamp: item.ts,
151+
duration: item.duration,
152+
};
153+
}
154+
return {
155+
_id: item._id,
156+
rid: item.rid,
157+
contact: { _id: item.contactId, username: item.contactUsername, name: item.contactName } as CallHistoryTableInternalContact,
158+
messageId: item.messageId,
159+
type: item.direction,
160+
status: item.state,
161+
timestamp: item.ts,
162+
duration: item.duration,
163+
};
164+
});
165+
}, [data]);
166+
167+
if (isPending) {
168+
return (
169+
<PageContent>
170+
<MediaCallHistoryTable sort={sortProps}>
171+
<GenericTableLoadingRow cols={5} />
172+
</MediaCallHistoryTable>
173+
</PageContent>
174+
);
175+
}
176+
177+
if (error) {
178+
return (
179+
<Page>
180+
<PageHeader title={t('Call_history')} />
181+
<PageContent>
182+
<GenericNoResults
183+
icon='warning'
184+
title={t('Something_went_wrong')}
185+
description={t('Please_try_again')}
186+
buttonTitle={t('Reload_page')}
187+
buttonAction={() => refetch()}
188+
/>
189+
</PageContent>
190+
</Page>
191+
);
192+
}
193+
194+
return (
195+
<Page flexDirection='row'>
196+
<Page>
197+
<PageHeader title={t('Call_history')} />
198+
<PageContent>
199+
<CallHistoryPageFilters {...filterProps} />
200+
{!tableData || (tableData.length === 0 && <GenericNoResults />)}
201+
{tableData && tableData.length > 0 && (
202+
<MediaCallHistoryTable sort={sortProps}>
203+
{tableData.map((item) => {
204+
if (isCallHistoryUnknownContact(item.contact)) {
205+
return (
206+
<CallHistoryRowUnknownUser key={item._id} {...item} contact={item.contact} onClick={() => onClickRow('', item._id)} />
207+
);
208+
}
209+
if (isCallHistoryTableInternalContact(item.contact)) {
210+
return (
211+
<CallHistoryRowInternalUser
212+
key={item._id}
213+
{...item}
214+
contact={item.contact}
215+
onClick={() => onClickRow(item.rid ?? '', item._id)}
216+
rid={item.rid ?? ''}
217+
onClickUserInfo={item.rid ? openUserInfo : undefined}
218+
/>
219+
);
220+
}
221+
222+
return (
223+
<CallHistoryRowExternalUser key={item._id} {...item} contact={item.contact} onClick={() => onClickRow('', item._id)} />
224+
);
225+
})}
226+
</MediaCallHistoryTable>
227+
)}
228+
<Pagination divider count={data?.total || 0} onSetItemsPerPage={setItemsPerPage} onSetCurrent={setCurrent} {...paginationProps} />
229+
</PageContent>
230+
</Page>
231+
{tab?.openTab === 'user-info' && (
232+
<UserInfoWithData rid={tab.rid} uid={tab.userId} onClose={closeTab} onClickBack={historyId ? handleBack : undefined} />
233+
)}
234+
{tab?.openTab === 'details' && historyId && (
235+
<MediaCallHistoryContextualbar historyId={historyId} onClose={closeTab} messageRoomId={tab.rid} openUserInfo={openUserInfo} />
236+
)}
237+
</Page>
238+
);
239+
};
240+
241+
export default CallHistoryPage;

0 commit comments

Comments
 (0)