Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
60ceee8
WIP
gabriellsh Aug 12, 2025
e227c0d
Partially update impl
gabriellsh Aug 13, 2025
9e0a391
lockfile
gabriellsh Aug 13, 2025
88817a6
Implement sounds
gabriellsh Aug 14, 2025
2b1e4d8
Send signals through stream
gabriellsh Aug 14, 2025
1f4186a
Fix hangup
gabriellsh Aug 14, 2025
91d114d
get correct peerinfo
gabriellsh Aug 14, 2025
effa92c
Fix stream not stopping
gabriellsh Aug 25, 2025
49d7c0e
fix null node
gabriellsh Aug 25, 2025
becdc6f
Remove "old" voip
gabriellsh Aug 25, 2025
c4c9345
call instantly from hoom header
gabriellsh Aug 25, 2025
cb044ad
Update headerv2
gabriellsh Aug 25, 2025
99eea31
Update toggle and peerInfo type. Get peerInfo from call/autocomplete …
gabriellsh Aug 26, 2025
3a8fee6
Update call buttons
gabriellsh Aug 26, 2025
30ddbf2
Update user menu
gabriellsh Aug 26, 2025
3994429
extend webrtc processor and implement mute
gabriellsh Aug 27, 2025
0bbcc5d
Implement hold
gabriellsh Aug 28, 2025
1152833
ForwardRef for devicePickerButton
gabriellsh Aug 28, 2025
5adc96b
WIP permission modals
gabriellsh Aug 28, 2025
eea0ce0
Fix Device change and lingering picker issues
gabriellsh Sep 1, 2025
0d535df
Fix ts
gabriellsh Sep 1, 2025
af42960
implement ice config and fix lingering stream after switching devices
gabriellsh Sep 2, 2025
4ba2dcb
Reflect base changes in client impl
gabriellsh Sep 4, 2025
0fd92b2
Fix device picker disabled in some cases
gabriellsh Sep 4, 2025
a7931e2
Fix device list update & use previously selected device for new calls
gabriellsh Sep 4, 2025
4647b5f
Fix pre-selected device
gabriellsh Sep 4, 2025
508fcc8
add new translation strings
gabriellsh Sep 4, 2025
6851f2a
Improve media call buttons
gabriellsh Sep 4, 2025
2ebedf5
exclude own user from users list
gabriellsh Sep 4, 2025
5818b47
Fix import
gabriellsh Sep 5, 2025
5d7e57e
fix changed signatures
gabriellsh Sep 5, 2025
b7c5ac1
Update header actions
gabriellsh Sep 10, 2025
034b8bc
fix useUserMenu
gabriellsh Sep 10, 2025
fb4f0a3
Update Icon
gabriellsh Sep 11, 2025
72ea4c7
update `onCall`
gabriellsh Sep 11, 2025
5b7c1e0
Remove old `useMediaCallAction`
gabriellsh Sep 11, 2025
a715a11
update MediaCallWidget.stories
gabriellsh Sep 11, 2025
91d4e9c
fix Icon
gabriellsh Sep 11, 2025
4c699d9
move `useMediaSessionInstance` to it's own file
gabriellsh Sep 11, 2025
a6cf548
Implement transfer modal
gabriellsh Sep 11, 2025
f2b1f27
Fix type imports
gabriellsh Sep 11, 2025
4acae93
Fix translations
gabriellsh Sep 15, 2025
8e4cd9d
Fix TS and memoized peerInfo
gabriellsh Sep 15, 2025
5d381e0
Update contact avatar and session events
gabriellsh Sep 15, 2025
f296d79
Update instance implementation
gabriellsh Sep 15, 2025
b55e1b8
Update device switching
gabriellsh Sep 15, 2025
e22f9ec
Fix cbref type
gabriellsh Sep 15, 2025
321239b
Cleanup session instance
gabriellsh Sep 15, 2025
bdecd09
Fix SessionId
gabriellsh Sep 15, 2025
03c3ec8
Finish call transfering
gabriellsh Sep 15, 2025
b318288
Ensure all fns exist before instantiating
gabriellsh Sep 15, 2025
41d9f0e
Add in-between states
gabriellsh Sep 16, 2025
db831c0
add connectionState
gabriellsh Sep 16, 2025
9b48646
Fix connection state
gabriellsh Sep 16, 2025
4525984
Hidden Calls
gabriellsh Sep 16, 2025
91f7318
Reject incoming call when canceling permission request
gabriellsh Sep 17, 2025
00007ac
Fallback to denied permission modal if prompt fails (safari)
gabriellsh Sep 17, 2025
d979f96
Fix device selection constraints
gabriellsh Sep 17, 2025
fc15a18
Fix random id generator
gabriellsh Sep 17, 2025
9ebb798
Add button to user card
gabriellsh Sep 17, 2025
b2789f6
Fix device selection through keyboard
gabriellsh Sep 17, 2025
f546145
Fix cannot change output device if no call in progress
gabriellsh Sep 17, 2025
7002534
change togglebutton tooltip based on pressed state
gabriellsh Sep 19, 2025
7c657eb
update transfer modal with inline errors
gabriellsh Sep 19, 2025
7961225
pass more information for transfer onConfirm
gabriellsh Sep 19, 2025
c77e386
success message on transfer
gabriellsh Sep 19, 2025
8052e8b
Close transfer modal if call ends
gabriellsh Sep 19, 2025
e6ea8ea
Fix `*` size in keypad
gabriellsh Sep 19, 2025
ada4486
remove call action from blocked rooms
gabriellsh Sep 19, 2025
4c1a6da
Exclude current peer from list
gabriellsh Sep 19, 2025
2ef9a88
Permissions and license check
gabriellsh Sep 22, 2025
6948361
remove logger
gabriellsh Sep 22, 2025
8a100b5
Cleanup
gabriellsh Sep 22, 2025
061f552
dtmf
gabriellsh Sep 22, 2025
ba3c306
Update snapshots
gabriellsh Sep 22, 2025
3d0000a
Add changeset
gabriellsh Sep 22, 2025
ccd96fb
typo
gabriellsh Sep 23, 2025
0d22733
memoize external provider values
gabriellsh Sep 23, 2025
8f65055
Merge branch 'develop' into feat/mediaCallClient
gabriellsh Sep 23, 2025
f3fabe2
retrieve missed changes
gabriellsh Sep 23, 2025
3c0a25f
remove duplicate key
gabriellsh Sep 23, 2025
a265e1d
Move files to v2
gabriellsh Sep 23, 2025
2f35b65
remove comments
gabriellsh Sep 23, 2025
699e952
remove from room header when blocked
gabriellsh Sep 23, 2025
aeb4a5d
update lockfile
gabriellsh Sep 23, 2025
8a3e042
review
gabriellsh Sep 23, 2025
f0ff773
change prop naming to large
gabriellsh Sep 24, 2025
f657c2d
Merge branch 'develop' into feat/mediaCallClient
gabriellsh Sep 24, 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
7 changes: 7 additions & 0 deletions .changeset/sweet-ghosts-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/i18n": minor
"@rocket.chat/ui-voip": minor
---

Introduces a new voice call architecture along with a new and improved call widget experience.

This file was deleted.

16 changes: 5 additions & 11 deletions apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarVoipGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
import { NavBarDivider, NavBarGroup } from '@rocket.chat/fuselage';
import { useVoipState } from '@rocket.chat/ui-voip';
import { NavBarDivider, NavBarGroup, NavBarItem } from '@rocket.chat/fuselage';
import { useMediaCallAction } from '@rocket.chat/ui-voip';
import { useTranslation } from 'react-i18next';

import NavBarItemVoipDialer from './NavBarItemVoipDialer';
import NavBarItemVoipToggler from './NavBarItemVoipToggler';
import { useIsCallEnabled } from '../../contexts/CallContext';

const NavBarVoipGroup = () => {
const { t } = useTranslation();
const { isEnabled: showVoip } = useVoipState();
const isCallEnabled = useIsCallEnabled();

if (!showVoip) {
const callAction = useMediaCallAction();
if (!callAction) {
return null;
}

return (
<>
<NavBarGroup aria-label={t('Voice_Call')}>
<NavBarItemVoipDialer primary={isCallEnabled} />
<NavBarItemVoipToggler />
<NavBarItem title={callAction.title} icon={callAction.icon} onClick={() => callAction.action()} />;
</NavBarGroup>
<NavBarDivider />
</>
Expand Down
71 changes: 71 additions & 0 deletions apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import { useUserAvatarPath, useUserId } from '@rocket.chat/ui-contexts';
import type { PeerInfo } from '@rocket.chat/ui-voip';
import { useMediaCallAction } from '@rocket.chat/ui-voip';
import { useMemo } from 'react';

import { useRoom, useRoomSubscription } from '../../views/room/contexts/RoomContext';
import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext';
import { useUserInfoQuery } from '../useUserInfoQuery';

const getPeerId = (uids: string[], ownUserId: string | null) => {
if (!ownUserId) {
return undefined;
}

const otherIds = uids.filter((uid) => uid !== ownUserId);

// If no id, it's an one user DM. If more than one, it's a group dm. Both are not supported as of now.
if (otherIds.length === 0 || otherIds.length > 1) {
return undefined;
}

return otherIds[0];
};

export const useMediaCallRoomAction = () => {
const { uids = [] } = useRoom();
const subscription = useRoomSubscription();
const ownUserId = useUserId();

const getAvatarUrl = useUserAvatarPath();

const peerId = getPeerId(uids, ownUserId);

const { data } = useUserInfoQuery({ userId: peerId as string }, { enabled: !!peerId });

const peerInfo = useMemo<PeerInfo | undefined>(() => {
if (!data?.user?._id) {
return undefined;
}

return {
userId: data.user._id,
displayName: data.user.name || data.user.username || '',
avatarUrl: data.user.username
? getAvatarUrl({ username: data.user.username, etag: data.user.avatarETag })
: getAvatarUrl({ userId: data.user._id }),
};
}, [data, getAvatarUrl]);

const callAction = useMediaCallAction(peerInfo);

const blocked = subscription?.blocked || subscription?.blocker;

return useMemo((): RoomToolboxActionConfig | undefined => {
if (!peerId || !callAction || blocked) {
return undefined;
}

const { action, title, icon } = callAction;

return {
id: 'start-voice-call',
title: title as TranslationKey,
icon,
featured: true,
action: () => action(),
groups: ['direct'] as const,
};
}, [peerId, callAction, blocked]);
};
48 changes: 48 additions & 0 deletions apps/meteor/client/providers/MediaCallProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { usePermission } from '@rocket.chat/ui-contexts';
import { MediaCallProvider as MediaCallProviderBase, MediaCallContext } from '@rocket.chat/ui-voip';
import type { ReactNode } from 'react';
import { useMemo } from 'react';

import { useHasLicenseModule } from '../hooks/useHasLicenseModule';
import { useVoipWarningModal } from '../hooks/useVoipWarningModal';

const MediaCallProvider = ({ children }: { children: ReactNode }) => {
const dispatchWarning = useVoipWarningModal();

const canMakeInternalCall = usePermission('allow-internal-voice-calls');
const canMakeExternalCall = usePermission('allow-external-voice-calls');

const hasModule = useHasLicenseModule('teams-voip');

const unauthorizedContextValue = useMemo(
() => ({
state: 'unauthorized' as const,
onToggleWidget: undefined,
onEndCall: undefined,
peerInfo: undefined,
}),
[],
);

const unlicensedContextValue = useMemo(
() => ({
state: 'unlicensed' as const,
onToggleWidget: dispatchWarning,
onEndCall: undefined,
peerInfo: undefined,
}),
[dispatchWarning],
);

if (!hasModule) {
return <MediaCallContext.Provider value={unlicensedContextValue}>{children}</MediaCallContext.Provider>;
}

if (!canMakeInternalCall && !canMakeExternalCall) {
return <MediaCallContext.Provider value={unauthorizedContextValue}>{children}</MediaCallContext.Provider>;
}

return <MediaCallProviderBase>{children}</MediaCallProviderBase>;
};

export default MediaCallProvider;
6 changes: 3 additions & 3 deletions apps/meteor/client/providers/MeteorProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ModalProvider } from '@rocket.chat/ui-client';
import { VoipProvider } from '@rocket.chat/ui-voip';
import type { ReactNode } from 'react';

import ActionManagerProvider from './ActionManagerProvider';
Expand All @@ -11,6 +10,7 @@ import CustomSoundProvider from './CustomSoundProvider';
import { DeviceProvider } from './DeviceProvider/DeviceProvider';
import EmojiPickerProvider from './EmojiPickerProvider';
import LayoutProvider from './LayoutProvider';
import MediaCallProvider from './MediaCallProvider';
import OmnichannelProvider from './OmnichannelProvider';
import RouterProvider from './RouterProvider';
import ServerProvider from './ServerProvider';
Expand Down Expand Up @@ -49,11 +49,11 @@ const MeteorProvider = ({ children }: MeteorProviderProps) => (
<UserPresenceProvider>
<ActionManagerProvider>
<VideoConfProvider>
<VoipProvider>
<MediaCallProvider>
<OmnichannelCallProvider>
<OmnichannelProvider>{children}</OmnichannelProvider>
</OmnichannelCallProvider>
</VoipProvider>
</MediaCallProvider>
</VideoConfProvider>
</ActionManagerProvider>
</UserPresenceProvider>
Expand Down
20 changes: 17 additions & 3 deletions apps/meteor/client/sidebar/header/hooks/useUserMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@ import type { IUser } from '@rocket.chat/core-typings';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import type { GenericMenuItemProps } from '@rocket.chat/ui-client';
import { useLogout } from '@rocket.chat/ui-contexts';
import { useMediaCallAction } from '@rocket.chat/ui-voip';
import { useTranslation } from 'react-i18next';

import UserMenuHeader from '../UserMenuHeader';
import { useAccountItems } from './useAccountItems';
import { useStatusItems } from './useStatusItems';
import { useVoipItemsSection } from './useVoipItemsSection';

export const useUserMenu = (user: IUser) => {
const { t } = useTranslation();

const statusItems = useStatusItems();
const accountItems = useAccountItems();
const voipItemsSection = useVoipItemsSection();

const mediaCallAction = useMediaCallAction();

const logout = useLogout();
const handleLogout = useEffectEvent(() => {
Expand All @@ -28,6 +29,15 @@ export const useUserMenu = (user: IUser) => {
onClick: handleLogout,
};

const mediaCallItem = mediaCallAction
? {
id: 'voice-call',
icon: mediaCallAction.icon,
content: mediaCallAction.title,
onClick: () => mediaCallAction.action(),
}
: undefined;

return [
{
title: <UserMenuHeader user={user} />,
Expand All @@ -37,7 +47,11 @@ export const useUserMenu = (user: IUser) => {
title: t('Status'),
items: statusItems,
},
voipItemsSection,
mediaCallItem
? {
items: [mediaCallItem],
}
: undefined,
{
title: t('Account'),
items: accountItems,
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/client/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { useE2EERoomAction } from './hooks/roomActions/useE2EERoomAction';
import { useExportMessagesRoomAction } from './hooks/roomActions/useExportMessagesRoomAction';
import { useGameCenterRoomAction } from './hooks/roomActions/useGameCenterRoomAction';
import { useKeyboardShortcutListRoomAction } from './hooks/roomActions/useKeyboardShortcutListRoomAction';
import { useMediaCallRoomAction } from './hooks/roomActions/useMediaCallRoomAction';
import { useMembersListRoomAction } from './hooks/roomActions/useMembersListRoomAction';
import { useMentionsRoomAction } from './hooks/roomActions/useMentionsRoomAction';
import { useOTRRoomAction } from './hooks/roomActions/useOTRRoomAction';
Expand All @@ -33,7 +34,6 @@ import { useUserInfoGroupRoomAction } from './hooks/roomActions/useUserInfoGroup
import { useUserInfoRoomAction } from './hooks/roomActions/useUserInfoRoomAction';
import { useVideoCallRoomAction } from './hooks/roomActions/useVideoCallRoomAction';
import { useVoIPRoomInfoRoomAction } from './hooks/roomActions/useVoIPRoomInfoRoomAction';
import { useVoiceCallRoomAction } from './hooks/roomActions/useVoiceCallRoomAction';
import { useWebRTCVideoRoomAction } from './hooks/roomActions/useWebRTCVideoRoomAction';
import type { RoomToolboxActionConfig } from './views/room/contexts/RoomToolboxContext';
import type { QuickActionsActionConfig } from './views/room/lib/quickActions';
Expand Down Expand Up @@ -70,7 +70,7 @@ export const roomActionHooks = [
useWebRTCVideoRoomAction,
useAppsRoomStarActions,
useVideoCallRoomAction,
useVoiceCallRoomAction,
useMediaCallRoomAction,
] satisfies (() => RoomToolboxActionConfig | undefined)[];

export const quickActionHooks = [
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/client/views/room/UserCard/UserCardWithData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ const UserCardWithData = ({ username, rid, onOpenUserInfo, onClose }: UserCardWi
}, [menuOptions, onClose, t]);

const actions = useMemo(() => {
const mapAction = ([key, { content, title, icon, onClick }]: [string, UserInfoAction]): ReactElement => (
<UserCardAction key={key} label={content || title} aria-label={content || title} onClick={onClick} icon={icon!} />
const mapAction = ([key, { content, title, icon, onClick, disabled }]: [string, UserInfoAction]): ReactElement => (
<UserCardAction key={key} label={content || title} aria-label={content || title} onClick={onClick} icon={icon!} disabled={disabled} />
);

return [...actionsDefinition.map(mapAction), menu].filter(Boolean);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { IRoom, IUser } from '@rocket.chat/core-typings';
import { useUserAvatarPath, useUserId, useUserSubscription } from '@rocket.chat/ui-contexts';
import { useMediaCallContext } from '@rocket.chat/ui-voip';
import { useTranslation } from 'react-i18next';

import { useUserCard } from '../../../contexts/UserCardContext';
import type { UserInfoAction } from '../useUserInfoActions';

export const useUserMediaCallAction = (user: Pick<IUser, '_id' | 'username' | 'name'>, rid: IRoom['_id']): UserInfoAction | undefined => {
const { t } = useTranslation();
const ownUserId = useUserId();
const { closeUserCard } = useUserCard();
const { state, onToggleWidget } = useMediaCallContext();
const getAvatarUrl = useUserAvatarPath();

const currentSubscription = useUserSubscription(rid);

const blocked = currentSubscription?.blocked || currentSubscription?.blocker;

if (state === 'unauthorized') {
return undefined;
}

if (blocked) {
return undefined;
}

const disabled = !['closed', 'new', 'unlicensed'].includes(state);

if (user._id === ownUserId) {
return undefined;
}

const avatarUrl = user.username ? getAvatarUrl({ username: user.username }) : getAvatarUrl({ userId: user._id });

return {
type: 'communication',
title: t('Voice_call__user_', { user: user.name || user.username || '' }),
icon: 'phone',
onClick: () => {
closeUserCard();
onToggleWidget({
userId: user._id,
displayName: user.name || user.username || '',
avatarUrl,
});
},
disabled,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import { useMuteUserAction } from './actions/useMuteUserAction';
import { useRedirectModerationConsole } from './actions/useRedirectModerationConsole';
import { useRemoveUserAction } from './actions/useRemoveUserAction';
import { useReportUser } from './actions/useReportUser';
import { useUserMediaCallAction } from './actions/useUserMediaCallAction';
import { useVideoCallAction } from './actions/useVideoCallAction';
import { useVoipCallAction } from './actions/useVoipCallAction';
import { useEmbeddedLayout } from '../../../../hooks/useEmbeddedLayout';

export type UserInfoActionType = 'communication' | 'privileges' | 'management' | 'moderation';
Expand All @@ -29,6 +29,7 @@ type UserInfoActionWithOnlyIcon = {
title: string;
variant?: 'danger';
onClick: () => void;
disabled?: boolean;
};

type UserInfoActionWithContent = {
Expand All @@ -38,6 +39,7 @@ type UserInfoActionWithContent = {
title?: string;
variant?: 'danger';
onClick: () => void;
disabled?: boolean;
};

export type UserInfoAction = UserInfoActionWithContent | UserInfoActionWithOnlyIcon;
Expand Down Expand Up @@ -74,16 +76,16 @@ export const useUserInfoActions = ({
const muteUser = useMuteUserAction(user, rid);
const removeUser = useRemoveUserAction(user, rid, reload);
const videoCall = useVideoCallAction(user);
const voipCall = useVoipCallAction(user);
const reportUserOption = useReportUser(user);
const isLayoutEmbedded = useEmbeddedLayout();
const { userToolbox: hiddenActions } = useLayoutHiddenActions();
const userMediaCall = useUserMediaCallAction(user, rid);

const userinfoActions = useMemo(
() => ({
...(openDirectMessage && !isLayoutEmbedded && { openDirectMessage }),
...(videoCall && { videoCall }),
...(voipCall && { voipCall }),
...(userMediaCall && { userMediaCall }),
...(!isMember && addUser && { addUser }),
...(isMember && changeOwner && { changeOwner }),
...(isMember && changeLeader && { changeLeader }),
Expand All @@ -99,7 +101,7 @@ export const useUserInfoActions = ({
openDirectMessage,
isLayoutEmbedded,
videoCall,
voipCall,
userMediaCall,
changeOwner,
changeLeader,
changeModerator,
Expand Down
Loading
Loading