Skip to content

Commit 911774b

Browse files
gabriellshdougfabris
authored andcommitted
feat: Implement Call History contextual bar inside rooms (RocketChat#37773)
Co-authored-by: Douglas Fabris <devfabris@gmail.com>
1 parent fbefb23 commit 911774b

File tree

9 files changed

+281
-3
lines changed

9 files changed

+281
-3
lines changed

apps/meteor/client/lib/queryKeys.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,8 @@ export const ABACQueryKeys = {
158158
room: (roomId: string) => [...ABACQueryKeys.rooms.all(), roomId] as const,
159159
},
160160
};
161+
162+
export const callHistoryQueryKeys = {
163+
all: ['call-history'] as const,
164+
info: (callId?: string) => [...callHistoryQueryKeys.all, 'info', callId] as const,
165+
};

apps/meteor/client/uikit/hooks/useMessageBlockContextValue.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { IRoom, IMessage } from '@rocket.chat/core-typings';
22
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
33
import type { UiKitContext } from '@rocket.chat/fuselage-ui-kit';
4+
import { useRoomToolbox } from '@rocket.chat/ui-contexts';
45
import {
56
useVideoConfDispatchOutgoing,
67
useVideoConfIsCalling,
@@ -38,6 +39,8 @@ export const useMessageBlockContextValue = (rid: IRoom['_id'], mid: IMessage['_i
3839

3940
const actionManager = useUiKitActionManager();
4041

42+
const { openTab } = useRoomToolbox();
43+
4144
return {
4245
action: ({ appId, actionId, blockId, value }, event) => {
4346
if (appId === 'videoconf-core') {
@@ -52,6 +55,12 @@ export const useMessageBlockContextValue = (rid: IRoom['_id'], mid: IMessage['_i
5255
}
5356
}
5457

58+
if (appId === 'media-call-core') {
59+
if (actionId === 'open-history') {
60+
return openTab('media-call-history', blockId);
61+
}
62+
}
63+
5564
actionManager.emitInteraction(appId, {
5665
type: 'blockAction',
5766
actionId,
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {
2+
ContextualbarHeader,
3+
ContextualbarIcon,
4+
ContextualbarTitle,
5+
ContextualbarClose,
6+
ContextualbarEmptyContent,
7+
ContextualbarDialog,
8+
ContextualbarSkeleton,
9+
} from '@rocket.chat/ui-client';
10+
import { useEndpoint, useRouteParameter, useRoomToolbox } from '@rocket.chat/ui-contexts';
11+
import { useQuery } from '@tanstack/react-query';
12+
import { useTranslation } from 'react-i18next';
13+
14+
import MediaCallHistoryExternal, { isExternalCallHistoryItem } from './MediaCallHistoryExternal';
15+
import MediaCallHistoryInternal, { isInternalCallHistoryItem } from './MediaCallHistoryInternal';
16+
import { callHistoryQueryKeys } from '../../lib/queryKeys';
17+
18+
export const MediaCallHistoryContextualbar = () => {
19+
const context = useRouteParameter('context');
20+
21+
const { closeTab } = useRoomToolbox();
22+
const { t } = useTranslation();
23+
24+
const getCallHistory = useEndpoint('GET', '/v1/call-history.info');
25+
const { data, isPending, isSuccess } = useQuery({
26+
queryKey: callHistoryQueryKeys.info(context),
27+
queryFn: async () => {
28+
if (!context) {
29+
throw new Error('Call ID is required');
30+
}
31+
return getCallHistory({ callId: context } as any); // TODO fix this type
32+
},
33+
staleTime: Infinity, // Call history should never change...
34+
enabled: !!context,
35+
});
36+
37+
if (isPending) {
38+
return <ContextualbarSkeleton />;
39+
}
40+
41+
if (isSuccess && isInternalCallHistoryItem(data)) {
42+
return <MediaCallHistoryInternal onClose={closeTab} data={data} />;
43+
}
44+
45+
if (isSuccess && isExternalCallHistoryItem(data)) {
46+
return <MediaCallHistoryExternal onClose={closeTab} data={data} />;
47+
}
48+
49+
return (
50+
<ContextualbarDialog onClose={closeTab}>
51+
<ContextualbarHeader>
52+
<ContextualbarIcon name='info-circled' />
53+
<ContextualbarTitle>{t('Call_info')}</ContextualbarTitle>
54+
<ContextualbarClose onClick={closeTab} />
55+
</ContextualbarHeader>
56+
<ContextualbarEmptyContent icon='warning' title={t('Call_info_could_not_be_loaded')} subtitle={t('Please_try_again')} />
57+
</ContextualbarDialog>
58+
);
59+
};
60+
61+
export default MediaCallHistoryContextualbar;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { CallHistoryItem, IExternalMediaCallHistoryItem, IMediaCall, Serialized } from '@rocket.chat/core-typings';
2+
import { CallHistoryContextualBar, useMediaCallContext } from '@rocket.chat/ui-voip';
3+
import { useMemo } from 'react';
4+
5+
type ExternalCallEndpointData = Serialized<{
6+
item: IExternalMediaCallHistoryItem;
7+
call: IMediaCall;
8+
}>;
9+
10+
type MediaCallHistoryExternalProps = {
11+
data: ExternalCallEndpointData;
12+
onClose: () => void;
13+
};
14+
15+
const getContact = (item: ExternalCallEndpointData['item']) => {
16+
return {
17+
number: item.contactExtension,
18+
};
19+
};
20+
21+
export const isExternalCallHistoryItem = (data: { item: Serialized<CallHistoryItem> }): data is ExternalCallEndpointData => {
22+
return 'external' in data.item && data.item.external;
23+
};
24+
25+
const MediaCallHistoryExternal = ({ data, onClose }: MediaCallHistoryExternalProps) => {
26+
const contact = useMemo(() => getContact(data.item), [data]);
27+
const historyData = useMemo(() => {
28+
return {
29+
callId: data.call._id,
30+
direction: data.item.direction,
31+
duration: data.item.duration,
32+
startedAt: new Date(data.item.ts),
33+
state: data.item.state,
34+
};
35+
}, [data]);
36+
const { onToggleWidget } = useMediaCallContext();
37+
38+
const actions = useMemo(() => {
39+
if (!onToggleWidget) {
40+
return {};
41+
}
42+
return {
43+
voiceCall: () => onToggleWidget(contact),
44+
};
45+
}, [contact, onToggleWidget]);
46+
47+
return <CallHistoryContextualBar onClose={onClose} actions={actions} contact={contact} data={historyData} />;
48+
};
49+
50+
export default MediaCallHistoryExternal;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { CallHistoryItem, IInternalMediaCallHistoryItem, IMediaCall, Serialized } from '@rocket.chat/core-typings';
2+
import { CallHistoryContextualBar } from '@rocket.chat/ui-voip';
3+
import { useMemo } from 'react';
4+
5+
import { useMediaCallInternalHistoryActions } from './useMediaCallInternalHistoryActions';
6+
7+
type InternalCallEndpointData = Serialized<{
8+
item: IInternalMediaCallHistoryItem;
9+
call: IMediaCall;
10+
}>;
11+
12+
type MediaCallHistoryInternalProps = {
13+
data: InternalCallEndpointData;
14+
onClose: () => void;
15+
};
16+
17+
export const isInternalCallHistoryItem = (data: { item: Serialized<CallHistoryItem> }): data is InternalCallEndpointData => {
18+
return 'external' in data.item && !data.item.external;
19+
};
20+
21+
const getContact = (item: InternalCallEndpointData['item'], call: InternalCallEndpointData['call']) => {
22+
const { caller, callee } = call ?? {};
23+
const contact = caller?.id === item.contactId ? caller : callee;
24+
const { id, sipExtension, username, ...rest } = contact;
25+
return {
26+
...rest,
27+
_id: id,
28+
username: username ?? '',
29+
voiceCallExtension: sipExtension,
30+
};
31+
};
32+
33+
const MediaCallHistoryInternal = ({ data, onClose }: MediaCallHistoryInternalProps) => {
34+
const contact = useMemo(() => getContact(data.item, data.call), [data]);
35+
const historyData = useMemo(() => {
36+
return {
37+
callId: data.call._id,
38+
direction: data.item.direction,
39+
duration: data.item.duration,
40+
startedAt: new Date(data.item.ts),
41+
state: data.item.state,
42+
};
43+
}, [data]);
44+
const actions = useMediaCallInternalHistoryActions(contact, data.item.messageId);
45+
46+
return <CallHistoryContextualBar onClose={onClose} actions={actions} contact={contact} data={historyData} />;
47+
};
48+
49+
export default MediaCallHistoryInternal;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
2+
import type { LocationPathname } from '@rocket.chat/ui-contexts';
3+
import { useCurrentRoutePath, useRoomToolbox, useRouter, useUserAvatarPath } from '@rocket.chat/ui-contexts';
4+
import { useMediaCallContext } from '@rocket.chat/ui-voip';
5+
import { useMemo } from 'react';
6+
7+
import { useRoom } from '../room/contexts/RoomContext';
8+
import { useDirectMessageAction } from '../room/hooks/useUserInfoActions/actions/useDirectMessageAction';
9+
10+
export type InternalCallHistoryContact = {
11+
_id: string;
12+
name?: string;
13+
username: string;
14+
displayName?: string;
15+
voiceCallExtension?: string;
16+
avatarUrl?: string;
17+
};
18+
19+
export const useMediaCallInternalHistoryActions = (contact: InternalCallHistoryContact, messageId?: string) => {
20+
const { onToggleWidget } = useMediaCallContext();
21+
const router = useRouter();
22+
23+
const currentRoutePath = useCurrentRoutePath();
24+
const room = useRoom();
25+
const toolbox = useRoomToolbox();
26+
const getAvatarUrl = useUserAvatarPath();
27+
28+
const voiceCall = useEffectEvent(() => {
29+
if (!onToggleWidget) {
30+
return;
31+
}
32+
33+
onToggleWidget({
34+
userId: contact._id,
35+
displayName: contact.displayName ?? '',
36+
username: contact.username,
37+
avatarUrl: getAvatarUrl({ username: contact.username }),
38+
callerId: contact.voiceCallExtension,
39+
});
40+
});
41+
42+
const directMessage = useDirectMessageAction(contact, room._id);
43+
44+
const jumpToMessage = useEffectEvent(() => {
45+
if (!messageId || !currentRoutePath) {
46+
return;
47+
}
48+
49+
const { msg: _, ...searchParams } = router.getSearchParameters();
50+
51+
router.navigate(
52+
{
53+
pathname: currentRoutePath as LocationPathname,
54+
search: messageId ? { ...searchParams, msg: messageId } : searchParams,
55+
},
56+
{ replace: true },
57+
);
58+
});
59+
60+
const userInfo = useEffectEvent(() => {
61+
toolbox.openTab('user-info', contact._id);
62+
});
63+
64+
return useMemo(
65+
() => ({
66+
voiceCall,
67+
directMessage: directMessage && 'onClick' in directMessage ? directMessage.onClick : undefined,
68+
jumpToMessage,
69+
userInfo,
70+
}),
71+
[voiceCall, directMessage, jumpToMessage, userInfo],
72+
);
73+
};

apps/meteor/client/views/room/providers/RoomToolboxProvider.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useRoom } from '../contexts/RoomContext';
1515
import { getRoomGroup } from '../lib/getRoomGroup';
1616
import { useAppsRoomActions } from './hooks/useAppsRoomActions';
1717
import { useCoreRoomActions } from './hooks/useCoreRoomActions';
18+
import { useCoreRoomRoutes } from './hooks/useCoreRoomRoutes';
1819

1920
type RoomToolboxProviderProps = { children: ReactNode };
2021

@@ -70,6 +71,9 @@ const RoomToolboxProvider = ({ children }: RoomToolboxProviderProps) => {
7071
const coreRoomActions = useCoreRoomActions();
7172
const appsRoomActions = useAppsRoomActions();
7273

74+
// core routes open the contextual bar, but have no button on the header
75+
const coreRoomRoutes = useCoreRoomRoutes();
76+
7377
const allowAnonymousRead = useSetting<boolean>('Accounts_AllowAnonymousRead', false);
7478
const uid = useUserId();
7579

@@ -89,8 +93,13 @@ const RoomToolboxProvider = ({ children }: RoomToolboxProviderProps) => {
8993
return undefined;
9094
}
9195

96+
const coreRouteTab = coreRoomRoutes.find((route) => route.id === tabActionId);
97+
if (coreRouteTab) {
98+
return coreRouteTab;
99+
}
100+
92101
return actions.find((action) => action.id === tabActionId);
93-
}, [actions, tabActionId]);
102+
}, [coreRoomRoutes, actions, tabActionId]);
94103

95104
const contextValue = useMemo(
96105
(): RoomToolboxContextValue => ({
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { RoomToolboxActionConfig } from '@rocket.chat/ui-contexts';
2+
3+
import MediaCallHistoryContextualbar from '../../../mediaCallHistory/MediaCallHistoryContextualbar';
4+
5+
const mediaCallHistoryRoute: RoomToolboxActionConfig = {
6+
id: 'media-call-history',
7+
title: 'Call_Information',
8+
tabComponent: MediaCallHistoryContextualbar,
9+
icon: 'info-circled',
10+
groups: ['direct'],
11+
};
12+
13+
const coreRoomRoutes = [mediaCallHistoryRoute];
14+
15+
// This isn't really a proper hook, but it could be extended in the future
16+
// So we're maitaning the same pattern as `useCoreRoomActions`
17+
export const useCoreRoomRoutes = (): Array<RoomToolboxActionConfig> => {
18+
return coreRoomRoutes;
19+
};

packages/i18n/src/locales/en.i18n.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7205,5 +7205,8 @@
72057205
"timestamps.longDateDescription": "12/31/2020, 12:00 AM",
72067206
"timestamps.fullDateTimeDescription": "December 31, 2020 12:00 AM",
72077207
"timestamps.fullDateTimeLongDescription": "Thursday, December 31, 2020 12:00:00 AM",
7208-
"timestamps.relativeTimeDescription": "1 year ago"
7209-
}
7208+
"timestamps.relativeTimeDescription": "1 year ago",
7209+
"Call_info_could_not_be_loaded": "Call info could not be loaded",
7210+
"Please_try_again": "Please try again."
7211+
7212+
}

0 commit comments

Comments
 (0)