diff --git a/apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.spec.tsx b/apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.spec.tsx
new file mode 100644
index 0000000000000..78de9bcf37067
--- /dev/null
+++ b/apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.spec.tsx
@@ -0,0 +1,146 @@
+import type { IUser, IRoom } from '@rocket.chat/core-typings';
+import { mockAppRoot } from '@rocket.chat/mock-providers';
+import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
+import { useMediaCallAction } from '@rocket.chat/ui-voip';
+import { act, renderHook } from '@testing-library/react';
+
+import { useMediaCallRoomAction } from './useMediaCallRoomAction';
+import FakeRoomProvider from '../../../tests/mocks/client/FakeRoomProvider';
+import { createFakeRoom, createFakeSubscription, createFakeUser } from '../../../tests/mocks/data';
+
+jest.mock('@rocket.chat/ui-contexts', () => ({
+ ...jest.requireActual('@rocket.chat/ui-contexts'),
+ useUserAvatarPath: jest.fn((_args: any) => 'avatar-url'),
+}));
+
+jest.mock('@rocket.chat/ui-voip', () => ({
+ useMediaCallAction: jest.fn(),
+}));
+
+const getUserInfoMocked = jest.fn().mockResolvedValue({ user: createFakeUser({ _id: 'peer-uid', username: 'peer-username' }) });
+
+const appRoot = (overrides: { user?: IUser | null; room?: IRoom; subscription?: SubscriptionWithRoom } = {}) => {
+ const {
+ user = createFakeUser({ _id: 'own-uid', username: 'own-username' }),
+ room = createFakeRoom({ uids: ['own-uid', 'peer-uid'] }),
+ subscription = createFakeSubscription(),
+ } = overrides;
+
+ const root = mockAppRoot()
+ .withRoom(room)
+ .withEndpoint('GET', '/v1/users.info', getUserInfoMocked)
+ .wrap((children) => (
+
+ {children}
+
+ ));
+
+ if (user !== null) {
+ root.withUser(user);
+ }
+
+ return root.build();
+};
+
+describe('useMediaCallRoomAction', () => {
+ const useMediaCallActionMocked = jest.mocked(useMediaCallAction);
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ useMediaCallActionMocked.mockReturnValue({
+ action: jest.fn(),
+ title: 'Start_call',
+ icon: 'phone',
+ });
+ });
+
+ it('should return undefined if ownUserId is not defined', () => {
+ const { result } = renderHook(() => useMediaCallRoomAction(), {
+ wrapper: appRoot({ user: null }),
+ });
+
+ expect(result.current).toBeUndefined();
+ });
+
+ it('should return undefined if there are no other users in the room', () => {
+ const fakeRoom = createFakeRoom({ uids: ['own-uid'] });
+ const { result } = renderHook(() => useMediaCallRoomAction(), {
+ wrapper: appRoot({ room: fakeRoom }),
+ });
+
+ expect(result.current).toBeUndefined();
+ });
+
+ it('should return undefined if there are more than one other user (Group DM)', () => {
+ const fakeRoom = createFakeRoom({ uids: ['own-uid', 'peer-uid-1', 'peer-uid-2'] });
+ const { result } = renderHook(() => useMediaCallRoomAction(), {
+ wrapper: appRoot({ room: fakeRoom }),
+ });
+
+ expect(result.current).toBeUndefined();
+ });
+
+ it('should return undefined if callAction is undefined', () => {
+ useMediaCallActionMocked.mockReturnValue(undefined);
+
+ const { result } = renderHook(() => useMediaCallRoomAction(), {
+ wrapper: appRoot(),
+ });
+
+ expect(result.current).toBeUndefined();
+ });
+
+ it('should return undefined if subscription is blocked', () => {
+ const fakeBlockedSubscription = createFakeSubscription({ blocker: false, blocked: true });
+ const { result } = renderHook(() => useMediaCallRoomAction(), {
+ wrapper: appRoot({ subscription: fakeBlockedSubscription }),
+ });
+
+ expect(result.current).toBeUndefined();
+ });
+
+ it('should return undefined if subscription is blocker', () => {
+ const fakeBlockedSubscription = createFakeSubscription({ blocked: false, blocker: true });
+ const { result } = renderHook(() => useMediaCallRoomAction(), {
+ wrapper: appRoot({ subscription: fakeBlockedSubscription }),
+ });
+
+ expect(result.current).toBeUndefined();
+ });
+
+ it('should return undefined if room is federated', () => {
+ const fakeFederatedRoom = createFakeRoom({ uids: ['own-uid', 'peer-uid'], federated: true });
+ const { result } = renderHook(() => useMediaCallRoomAction(), {
+ wrapper: appRoot({ room: fakeFederatedRoom }),
+ });
+
+ expect(result.current).toBeUndefined();
+ });
+
+ it('should return the action config if all conditions are met', () => {
+ const actionMock = jest.fn();
+ useMediaCallActionMocked.mockReturnValue({
+ action: actionMock,
+ title: 'Start_call',
+ icon: 'phone',
+ });
+
+ const { result } = renderHook(() => useMediaCallRoomAction(), {
+ wrapper: appRoot(),
+ });
+
+ expect(result.current).toEqual({
+ id: 'start-voice-call',
+ title: 'Start_call',
+ icon: 'phone',
+ featured: true,
+ action: expect.any(Function),
+ groups: ['direct'],
+ });
+
+ // Test the action trigger
+ act(() => result.current?.action?.());
+ expect(actionMock).toHaveBeenCalled();
+ });
+});
diff --git a/apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.ts b/apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.ts
index d0e458662251c..a4652eebf2fdd 100644
--- a/apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.ts
+++ b/apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.ts
@@ -1,3 +1,4 @@
+import { isRoomFederated } from '@rocket.chat/core-typings';
import { useUserAvatarPath, useUserId } from '@rocket.chat/ui-contexts';
import type { TranslationKey, RoomToolboxActionConfig } from '@rocket.chat/ui-contexts';
import type { PeerInfo } from '@rocket.chat/ui-voip';
@@ -23,9 +24,11 @@ const getPeerId = (uids: string[], ownUserId: string | undefined) => {
};
export const useMediaCallRoomAction = () => {
- const { uids = [] } = useRoom();
+ const room = useRoom();
+ const { uids = [] } = room;
const subscription = useRoomSubscription();
const ownUserId = useUserId();
+ const federated = isRoomFederated(room);
const getAvatarUrl = useUserAvatarPath();
@@ -52,7 +55,7 @@ export const useMediaCallRoomAction = () => {
const blocked = subscription?.blocked || subscription?.blocker;
return useMemo((): RoomToolboxActionConfig | undefined => {
- if (!peerId || !callAction || blocked) {
+ if (!peerId || !callAction || blocked || federated) {
return undefined;
}
@@ -66,5 +69,5 @@ export const useMediaCallRoomAction = () => {
action: () => action(),
groups: ['direct'] as const,
};
- }, [peerId, callAction, blocked]);
+ }, [peerId, callAction, blocked, federated]);
};
diff --git a/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useUserMediaCallAction.spec.tsx b/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useUserMediaCallAction.spec.tsx
new file mode 100644
index 0000000000000..68a16d072ee68
--- /dev/null
+++ b/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useUserMediaCallAction.spec.tsx
@@ -0,0 +1,142 @@
+import { mockAppRoot } from '@rocket.chat/mock-providers';
+import { useMediaCallContext } from '@rocket.chat/ui-voip';
+import { act, renderHook } from '@testing-library/react';
+
+import { useUserMediaCallAction } from './useUserMediaCallAction';
+import { createFakeRoom, createFakeSubscription, createFakeUser } from '../../../../../../tests/mocks/data';
+
+jest.mock('@rocket.chat/ui-contexts', () => ({
+ ...jest.requireActual('@rocket.chat/ui-contexts'),
+ useUserAvatarPath: jest.fn().mockReturnValue((_args: any) => 'avatar-url'),
+ useUserCard: jest.fn().mockReturnValue({ closeUserCard: jest.fn() }),
+}));
+
+jest.mock('@rocket.chat/ui-voip', () => ({
+ ...jest.requireActual('@rocket.chat/ui-voip'),
+ useMediaCallContext: jest.fn().mockImplementation(() => ({
+ state: 'closed',
+ onToggleWidget: jest.fn(),
+ })),
+}));
+
+const useMediaCallContextMocked = jest.mocked(useMediaCallContext);
+
+describe('useUserMediaCallAction', () => {
+ const fakeUser = createFakeUser({ _id: 'own-uid' });
+ const mockRid = 'room-id';
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return undefined if room is federated', () => {
+ const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), {
+ wrapper: mockAppRoot()
+ .withJohnDoe()
+ .withRoom(createFakeRoom({ federated: true }))
+ .build(),
+ });
+
+ expect(result.current).toBeUndefined();
+ });
+
+ it('should return undefined if state is unauthorized', () => {
+ useMediaCallContextMocked.mockReturnValueOnce({
+ state: 'unauthorized',
+ onToggleWidget: undefined,
+ onEndCall: undefined,
+ peerInfo: undefined,
+ });
+
+ const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), { wrapper: mockAppRoot().build() });
+ expect(result.current).toBeUndefined();
+ });
+
+ it('should return undefined if subscription is blocked', () => {
+ const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), {
+ wrapper: mockAppRoot()
+ .withJohnDoe()
+ .withRoom(createFakeRoom())
+ .withSubscription(createFakeSubscription({ blocker: false, blocked: true }))
+ .build(),
+ });
+
+ expect(result.current).toBeUndefined();
+ });
+
+ it('should return undefined if subscription is blocker', () => {
+ const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), {
+ wrapper: mockAppRoot()
+ .withJohnDoe()
+ .withRoom(createFakeRoom())
+ .withSubscription(createFakeSubscription({ blocker: true, blocked: false }))
+ .build(),
+ });
+
+ expect(result.current).toBeUndefined();
+ });
+
+ it('should return undefined if user is own user', () => {
+ const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), {
+ wrapper: mockAppRoot().withUser(fakeUser).withRoom(createFakeRoom()).withSubscription(createFakeSubscription()).build(),
+ });
+
+ expect(result.current).toBeUndefined();
+ });
+
+ it('should return action if conditions are met', () => {
+ const fakeUser = createFakeUser();
+ const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), {
+ wrapper: mockAppRoot()
+ .withJohnDoe()
+ .withRoom(createFakeRoom())
+ .withSubscription(createFakeSubscription())
+ .withTranslations('en', 'core', {
+ Voice_call__user_: 'Voice call {{user}}',
+ })
+ .build(),
+ });
+
+ expect(result.current).toEqual(
+ expect.objectContaining({
+ type: 'communication',
+ icon: 'phone',
+ title: `Voice call ${fakeUser.name}`,
+ disabled: false,
+ }),
+ );
+ });
+
+ it('should call onClick handler correctly', () => {
+ const mockOnToggleWidget = jest.fn();
+ useMediaCallContextMocked.mockReturnValueOnce({
+ state: 'closed',
+ onToggleWidget: mockOnToggleWidget,
+ peerInfo: undefined,
+ onEndCall: () => undefined,
+ });
+
+ const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid));
+
+ act(() => result.current?.onClick());
+
+ expect(mockOnToggleWidget).toHaveBeenCalledWith({
+ userId: fakeUser._id,
+ displayName: fakeUser.name,
+ avatarUrl: 'avatar-url',
+ });
+ });
+
+ it('should be disabled if state is not closed, new, or unlicensed', () => {
+ useMediaCallContextMocked.mockReturnValueOnce({
+ state: 'calling',
+ onToggleWidget: jest.fn(),
+ peerInfo: undefined,
+ onEndCall: () => undefined,
+ });
+
+ const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid));
+
+ expect(result.current?.disabled).toBe(true);
+ });
+});
diff --git a/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useUserMediaCallAction.ts b/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useUserMediaCallAction.ts
index cbf6341644743..dfcb2a7a02e2b 100644
--- a/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useUserMediaCallAction.ts
+++ b/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useUserMediaCallAction.ts
@@ -1,5 +1,6 @@
+import { isRoomFederated } from '@rocket.chat/core-typings';
import type { IRoom, IUser } from '@rocket.chat/core-typings';
-import { useUserAvatarPath, useUserId, useUserSubscription, useUserCard } from '@rocket.chat/ui-contexts';
+import { useUserAvatarPath, useUserId, useUserSubscription, useUserCard, useUserRoom } from '@rocket.chat/ui-contexts';
import { useMediaCallContext } from '@rocket.chat/ui-voip';
import { useTranslation } from 'react-i18next';
@@ -13,9 +14,14 @@ export const useUserMediaCallAction = (user: Pick