Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c92c48d
WIP
gabriellsh Dec 3, 2025
37886bf
Merge remote-tracking branch 'origin/develop' into feat/callHistoryCo…
gabriellsh Dec 5, 2025
268c7ba
add fuselage-ui-kit
gabriellsh Dec 8, 2025
338c578
Add Internal and External user components
gabriellsh Dec 8, 2025
42b2350
Translations
gabriellsh Dec 8, 2025
f9b9d19
implement useFullStartDate
gabriellsh Dec 8, 2025
b20679b
Wip 2 contextualbar and more stories
gabriellsh Dec 8, 2025
c9d86d3
Merge branch 'develop' into feat/callHistoryContextual
gabriellsh Dec 9, 2025
da55d2a
Add userCard to internal user
gabriellsh Dec 9, 2025
c5b6081
wip blocks
gabriellsh Dec 9, 2025
51b774d
Implement CallHistoryActions
gabriellsh Dec 9, 2025
5dc7765
export
gabriellsh Dec 9, 2025
034b8ee
fix external export
gabriellsh Dec 9, 2025
409b87c
Merge remote-tracking branch 'origin/develop' into feat/callHistoryCo…
gabriellsh Dec 10, 2025
987c190
use `getHistoryMessagePayload` to generate blocks and fix translation
gabriellsh Dec 10, 2025
0137b94
missing translation string
gabriellsh Dec 10, 2025
6b5f5b3
Merge remote-tracking branch 'origin/develop' into feat/callHistoryCo…
gabriellsh Dec 12, 2025
b87e8b6
export types
gabriellsh Dec 12, 2025
1a94c8e
Fix contextualbar size
gabriellsh Dec 12, 2025
2e26b5a
Merge remote-tracking branch 'origin/develop' into feat/callHistoryCo…
gabriellsh Dec 16, 2025
d3022bf
Fix Icon and make text bold
gabriellsh Dec 16, 2025
b925003
fix review
gabriellsh Dec 16, 2025
7fb6a6a
Add tests
gabriellsh Dec 16, 2025
75d8e5b
Merge branch 'develop' into feat/callHistoryContextual
gabriellsh Dec 16, 2025
f11015f
Fix test timezone
gabriellsh Dec 17, 2025
d1f2b32
Merge branch 'develop' into feat/callHistoryContextual
gabriellsh Dec 17, 2025
386cbdd
Merge branch 'develop' into feat/callHistoryContextual
kodiakhq[bot] Dec 17, 2025
86981c9
Merge branch 'develop' into feat/callHistoryContextual
kodiakhq[bot] Dec 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,7 @@
"Calendar_settings": "Calendar settings",
"Call": "Call",
"Call_Already_Ended": "Call Already Ended",
"Call_ID": "Call ID",
"Call_Information": "Call Information",
"Call_again": "Call again",
"Call_back": "Call back",
Expand Down Expand Up @@ -1803,6 +1804,7 @@
"Duplicate_file_name_found": "Duplicate file name found.",
"Duplicate_private_group_name": "A Private Group with name '%s' exists",
"Duplicated_Email_address_will_be_ignored": "Duplicated email address will be ignored.",
"Duration": "Duration",
"E2EE_Composer_Unencrypted_Message": "You're sending an unencrypted message",
"E2EE_password_reset": "E2EE password reset",
"E2EE_alert": "<b>Enabling E2EE affects other functionalities</b> <ul><li> - Encrypted content cannot be found by search</li> <li> - Encrypted content cannot be audited</li> <li> - Bot interactions may not work with encrypted messages</li></ul>",
Expand Down Expand Up @@ -2569,6 +2571,7 @@
"Incoming_call_from": "Incoming call from",
"Incoming_call_from__roomName__": "Incoming call from {{roomName}}",
"Incoming_call_transfer": "Incoming call transfer",
"Incoming_voice_call": "Incoming voice call",
"Incoming_voice_call_canceled_suddenly": "An Incoming Voice Call was canceled suddenly.",
"Incoming_voice_call_canceled_user_not_registered": "An Incoming Voice Call was canceled due to an unexpected error.",
"Industry": "Industry",
Expand Down Expand Up @@ -3964,6 +3967,7 @@
"Out_of_seats": "Out of Seats",
"Outdated": "Outdated",
"Outgoing": "Outgoing",
"Outgoing_voice_call": "Outgoing voice call",
"Outgoing_WebHook": "Outgoing WebHook",
"Outgoing_WebHook_Description": "Get data out of Rocket.Chat in real-time.",
"Outlook_Calendar": "Outlook Calendar",
Expand Down
2 changes: 2 additions & 0 deletions packages/ui-voip/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@rocket.chat/fuselage": "~0.68.1",
"@rocket.chat/fuselage-hooks": "~0.38.1",
"@rocket.chat/fuselage-tokens": "~0.33.2",
"@rocket.chat/fuselage-ui-kit": "workspace:^",
"@rocket.chat/icons": "~0.45.0",
"@rocket.chat/jest-presets": "workspace:~",
"@rocket.chat/mock-providers": "workspace:~",
Expand Down Expand Up @@ -76,6 +77,7 @@
"@rocket.chat/css-in-js": "*",
"@rocket.chat/fuselage": "*",
"@rocket.chat/fuselage-hooks": "*",
"@rocket.chat/fuselage-ui-kit": "workspace:^",
"@rocket.chat/icons": "*",
"@rocket.chat/styled": "*",
"@rocket.chat/ui-avatar": "workspace:^",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import type { Meta, StoryObj } from '@storybook/react';
import type { ReactElement } from 'react';

import CallHistoryContextualbar from './CallHistoryContextualbar';

const noop = () => undefined;

const meta = {
title: 'V2/Views/CallHistoryContextualbar',
component: CallHistoryContextualbar,
decorators: [
mockAppRoot()
.withTranslations('en', 'core', {
Call_info: 'Call info',
Direct_message: 'Direct message',
Call: 'Call',
Call_ended_bold: '*Voice call ended*',
Incoming_voice_call: 'Incoming voice call',
Outgoing_voice_call: 'Outgoing voice call',
Duration: 'Duration',
Voice_Call_Extension: 'Voice call extension',
Call_ID: 'Call ID',
})
.withDefaultLanguage('en-US')
.buildStoryDecorator(),
(Story): ReactElement => <Story />,
],
} satisfies Meta<typeof CallHistoryContextualbar>;

export default meta;

type Story = StoryObj<typeof meta>;

const externalContact = {
number: '1234567890',
};

const internalContact = {
_id: '1234567890',
name: 'John Doe',
username: 'john.doe',
voiceCallExtension: '0000',
};

export const Default: Story = {
args: {
onClose: noop,
actions: {
voiceCall: noop,
videoCall: noop,
jumpToMessage: noop,
directMessage: noop,
userInfo: noop,
},
contact: internalContact,
data: {
callId: '1234567890',
direction: 'inbound',
duration: 100,
startedAt: new Date(),
state: 'ended',
},
},
};

export const ExternalContact: Story = {
args: {
onClose: noop,
actions: {
voiceCall: noop,
videoCall: noop,
jumpToMessage: noop,
directMessage: noop,
userInfo: noop,
},
data: {
callId: '1234567890',
direction: 'inbound',
duration: 100,
startedAt: new Date(),
state: 'ended',
},
contact: externalContact,
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { Box, Button, ButtonGroup, Icon, MessageBlock } from '@rocket.chat/fuselage';
import { UiKitComponent, UiKitMessage as UiKitMessageSurfaceRender, UiKitContext } from '@rocket.chat/fuselage-ui-kit';
import {
Contextualbar,
ContextualbarHeader,
ContextualbarTitle,
ContextualbarClose,
ContextualbarFooter,
ContextualbarIcon,
ContextualbarScrollableContent,
InfoPanel,
InfoPanelSection,
InfoPanelLabel,
InfoPanelText,
} from '@rocket.chat/ui-client';
import type { TFunction } from 'i18next';
import { useTranslation } from 'react-i18next';

import CallHistoryExternalUser from './CallHistoryExternalUser';
import CallHistoryInternalUser from './CallHistoryInternalUser';
import { useFullStartDate } from './useFullStartDate';

type InternalCallHistoryContact = {
_id: string;
name?: string;
username: string;
voiceCallExtension?: string;
};

type ExternalCallHistoryContact = {
number: string;
};

type CallHistoryData = {
callId: string;
direction: 'inbound' | 'outbound';
duration: number;
startedAt: Date;
state: 'ended' | 'not-answered' | 'failed' | 'error' | 'transferred';
messageId?: string;
};

type HistoryActions = 'voiceCall' | 'videoCall' | 'jumpToMessage' | 'directMessage' | 'userInfo';

type HistoryActionCallbacks = Record<HistoryActions, () => void>;

type CallHistoryContextualBarProps = {
onClose: () => void;
actions: HistoryActionCallbacks;
contact: InternalCallHistoryContact | ExternalCallHistoryContact;
data: CallHistoryData;
};

const isInternalCallHistoryContact = (
contact: InternalCallHistoryContact | ExternalCallHistoryContact,
): contact is InternalCallHistoryContact => {
return '_id' in contact;
};

const contextValue = {
action: () => undefined,
rid: '',
values: {},
};

// TODO use the same function that generates the message-block payload on the server. Duration will also be displayed here.
const getBlocks = (t: TFunction) => {
return [
{
type: 'info_card' as const,
rows: [
{
background: 'default' as const,
elements: [
{ type: 'icon', icon: 'phone-off', variant: 'default' },
{ type: 'mrkdwn', i18n: { key: 'Call_ended_bold' }, text: t('Call_ended') },
] as const,
},
],
},
];
};
Comment thread
gabriellsh marked this conversation as resolved.
Outdated

const CallHistoryContextualBar = ({ onClose, actions, contact, data }: CallHistoryContextualBarProps) => {
const { t } = useTranslation();

const { voiceCall, /* videoCall, jumpToMessage, */ directMessage, userInfo /* voiceCallExtension, direction */ } = actions;
const { duration, callId, direction, startedAt } = data;

const date = useFullStartDate(startedAt);
return (
<Contextualbar>
<ContextualbarHeader>
<ContextualbarIcon name='info-circled' />
<ContextualbarTitle>{t('Call_info')}</ContextualbarTitle>
<ContextualbarClose onClick={onClose} />
</ContextualbarHeader>
<ContextualbarScrollableContent>
<InfoPanel>
<InfoPanelSection>
{isInternalCallHistoryContact(contact) ? (
<CallHistoryInternalUser username={contact.username} name={contact.name} _id={contact._id} onUserClick={userInfo} />
) : (
<CallHistoryExternalUser number={contact.number} />
)}
</InfoPanelSection>
<InfoPanelSection>
<Box display='flex' flexDirection='row' alignItems='center' fontScale='p1m'>
{/* TODO use `arrow-up-right` and `arrow-down-left` icons when available */}
<Icon name={direction === 'inbound' ? 'phone-in' : 'phone-out'} size='x20' mie='x4' />
{direction === 'inbound' ? t('Incoming_voice_call') : t('Outgoing_voice_call')}
</Box>
</InfoPanelSection>
<InfoPanelSection>
<MessageBlock fixedWidth>
<UiKitContext.Provider value={contextValue}>
<UiKitComponent render={UiKitMessageSurfaceRender} blocks={getBlocks(t)} />
</UiKitContext.Provider>
</MessageBlock>
<Box mbs={-8}>{date}</Box>
</InfoPanelSection>
<InfoPanelSection>
<InfoPanelLabel>{t('Duration')}</InfoPanelLabel>
<InfoPanelText>{duration}</InfoPanelText>
</InfoPanelSection>
<InfoPanelSection>
<InfoPanelLabel>{t('Call_ID')}</InfoPanelLabel>
<InfoPanelText>{callId}</InfoPanelText>
</InfoPanelSection>
{isInternalCallHistoryContact(contact) && contact.voiceCallExtension && (
<InfoPanelSection>
<InfoPanelLabel>{t('Voice_Call_Extension')}</InfoPanelLabel>
<InfoPanelText>{contact.voiceCallExtension}</InfoPanelText>
</InfoPanelSection>
)}
</InfoPanel>
</ContextualbarScrollableContent>
<ContextualbarFooter>
<ButtonGroup stretch>
{isInternalCallHistoryContact(contact) && (
<Button onClick={directMessage}>
<Icon name='balloon' size='x20' mie='x4' />
{t('Direct_message')}
</Button>
)}
<Button success onClick={voiceCall}>
<Icon name='phone' size='x20' mie='x4' />
{t('Call')}
</Button>
</ButtonGroup>
</ContextualbarFooter>
</Contextualbar>
);
};

export default CallHistoryContextualBar;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Box, Icon, FramedIcon } from '@rocket.chat/fuselage';

type CallHistoryExternalUserProps = {
number: string;
};

const CallHistoryExternalUser = ({ number }: CallHistoryExternalUserProps) => {
return (
<Box display='flex' flexDirection='row' alignItems='center'>
<Box mie={8}>
<FramedIcon icon='user' size={28} />
</Box>
<Box mie={8}>
<Icon name='phone' size={20} />
</Box>
<Box>{number.startsWith('+') ? number : `+${number}`}</Box>
</Box>
);
};

export default CallHistoryExternalUser;
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Box, Icon, Avatar, StatusBullet } from '@rocket.chat/fuselage';
import { useUserDisplayName } from '@rocket.chat/ui-client';
import { useUserAvatarPath, useUserPresence } from '@rocket.chat/ui-contexts';
import { useMemo } from 'react';

type CallHistoryInternalUserProps = {
username: string;
name?: string;
_id: string;
onUserClick: () => void;
};

const CallHistoryInternalUser = ({ username, name, _id, onUserClick }: CallHistoryInternalUserProps) => {
const getUserAvatarPath = useUserAvatarPath();

const avatarUrl = useMemo(() => {
return getUserAvatarPath({ username });
}, [username, getUserAvatarPath]);

const displayName = useUserDisplayName({ username, name });

const userStatus = useUserPresence(_id);

return (
<Box display='flex' flexDirection='row' alignItems='center' role='button' onClick={onUserClick}>
<Box mie={8}>{avatarUrl ? <Avatar url={avatarUrl} size='x28' /> : <Icon name='user' size='x28' />}</Box>
<Box mie={8}>
<StatusBullet status={userStatus?.status || 'loading'} size='small' />
</Box>
<Box>{displayName}</Box>
</Box>
);
Comment thread
gabriellsh marked this conversation as resolved.
};

export default CallHistoryInternalUser;
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useLanguage } from '@rocket.chat/ui-contexts';
import { useMemo } from 'react';

export const useFullStartDate = (startedAt: Date) => {
const locale = useLanguage();

const date = useMemo(() => {
// eslint-disable-next-line new-cap
return Intl.DateTimeFormat(locale, { dateStyle: 'full', timeStyle: 'medium' }).format(startedAt);
Comment thread
gabriellsh marked this conversation as resolved.
Outdated
}, [locale, startedAt]);

return date;
};
2 changes: 2 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10529,6 +10529,7 @@ __metadata:
"@rocket.chat/fuselage": "npm:~0.68.1"
"@rocket.chat/fuselage-hooks": "npm:~0.38.1"
"@rocket.chat/fuselage-tokens": "npm:~0.33.2"
"@rocket.chat/fuselage-ui-kit": "workspace:^"
"@rocket.chat/icons": "npm:~0.45.0"
"@rocket.chat/jest-presets": "workspace:~"
"@rocket.chat/media-signaling": "workspace:~"
Expand Down Expand Up @@ -10574,6 +10575,7 @@ __metadata:
"@rocket.chat/css-in-js": "*"
"@rocket.chat/fuselage": "*"
"@rocket.chat/fuselage-hooks": "*"
"@rocket.chat/fuselage-ui-kit": "workspace:^"
"@rocket.chat/icons": "*"
"@rocket.chat/styled": "*"
"@rocket.chat/ui-avatar": "workspace:^"
Expand Down
Loading