diff --git a/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useStartCallRoomAction.tsx b/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useStartCallRoomAction.tsx index ee3117d664d1..18d3efd01053 100644 --- a/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useStartCallRoomAction.tsx +++ b/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useStartCallRoomAction.tsx @@ -7,8 +7,8 @@ import useVideoConfMenuOptions from './useVideoConfMenuOptions'; import useVoipMenuOptions from './useVoipMenuOptions'; export const useStartCallRoomAction = () => { - const voipCall = useVideoConfMenuOptions(); - const videoCall = useVoipMenuOptions(); + const videoCall = useVideoConfMenuOptions(); + const voipCall = useVoipMenuOptions(); return useMemo((): RoomToolboxActionConfig | undefined => { if (!videoCall.allowed && !voipCall.allowed) { diff --git a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx index 14757e3710b0..afe881f64cc9 100644 --- a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx +++ b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx @@ -147,12 +147,12 @@ const AdminUsersPage = (): ReactElement => { diff --git a/apps/meteor/client/views/admin/users/UsersPageHeaderContent.spec.tsx b/apps/meteor/client/views/admin/users/UsersPageHeaderContent.spec.tsx index f4691eb4dd69..6181fe4fed50 100644 --- a/apps/meteor/client/views/admin/users/UsersPageHeaderContent.spec.tsx +++ b/apps/meteor/client/views/admin/users/UsersPageHeaderContent.spec.tsx @@ -5,12 +5,31 @@ import '@testing-library/jest-dom'; import UsersPageHeaderContent from './UsersPageHeaderContent'; -it('should render "Associate Extension" button when VoIP_TeamCollab_Enabled setting is enabled', async () => { +it('should not show "Assign Extension" button if voip setting is enabled but user dont have required permission', async () => { render(, { legacyRoot: true, wrapper: mockAppRoot().withJohnDoe().withSetting('VoIP_TeamCollab_Enabled', true).build(), }); + expect(screen.queryByRole('button', { name: 'Assign_extension' })).not.toBeInTheDocument(); +}); + +it('should not show "Assign Extension" button if user has required permission but voip setting is disabled', async () => { + render(, { + legacyRoot: true, + wrapper: mockAppRoot().withJohnDoe().withSetting('VoIP_TeamCollab_Enabled', true).build(), + }); + + expect(screen.queryByRole('button', { name: 'Assign_extension' })).not.toBeInTheDocument(); +}); + +it('should show "Assign Extension" button if user has required permission and voip setting is enabled', async () => { + render(, { + legacyRoot: true, + wrapper: mockAppRoot().withJohnDoe().withSetting('VoIP_TeamCollab_Enabled', true).withPermission('manage-voip-extensions').build(), + }); + + expect(screen.getByRole('button', { name: 'Assign_extension' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Assign_extension' })).toBeEnabled(); }); diff --git a/apps/meteor/client/views/admin/users/UsersPageHeaderContent.tsx b/apps/meteor/client/views/admin/users/UsersPageHeaderContent.tsx index e6794a9f98f6..89916c3e6f2e 100644 --- a/apps/meteor/client/views/admin/users/UsersPageHeaderContent.tsx +++ b/apps/meteor/client/views/admin/users/UsersPageHeaderContent.tsx @@ -1,5 +1,5 @@ import { Button, ButtonGroup, Margins } from '@rocket.chat/fuselage'; -import { usePermission, useRouter, useSetModal, useSetting } from '@rocket.chat/ui-contexts'; +import { usePermission, useRouter } from '@rocket.chat/ui-contexts'; import React from 'react'; import { useTranslation } from 'react-i18next'; @@ -7,7 +7,8 @@ import { useExternalLink } from '../../../hooks/useExternalLink'; import { useCheckoutUrl } from '../subscription/hooks/useCheckoutUrl'; import SeatsCapUsage from './SeatsCapUsage'; import type { SeatCapProps } from './useSeatsCap'; -import AssignExtensionModal from './voip/AssignExtensionModal'; +import AssignExtensionButton from './voip/AssignExtensionButton'; +import { useVoipExtensionPermission } from './voip/hooks/useVoipExtensionPermission'; type UsersPageHeaderContentProps = { isSeatsCapExceeded: boolean; @@ -17,10 +18,9 @@ type UsersPageHeaderContentProps = { const UsersPageHeaderContent = ({ isSeatsCapExceeded, seatsCap }: UsersPageHeaderContentProps) => { const { t } = useTranslation(); const router = useRouter(); - const setModal = useSetModal(); const canCreateUser = usePermission('create-user'); const canBulkCreateUser = usePermission('bulk-register-user'); - const canRegisterExtension = useSetting('VoIP_TeamCollab_Enabled'); + const canManageVoipExtension = useVoipExtensionPermission(); const manageSubscriptionUrl = useCheckoutUrl()({ target: 'user-page', action: 'buy_more' }); const openExternalLink = useExternalLink(); @@ -41,11 +41,7 @@ const UsersPageHeaderContent = ({ isSeatsCapExceeded, seatsCap }: UsersPageHeade )} - {canRegisterExtension && ( - - )} + {canManageVoipExtension && } {canBulkCreateUser && ( ); }; diff --git a/apps/meteor/client/views/admin/users/voip/hooks/useVoipExtensionAction.tsx b/apps/meteor/client/views/admin/users/voip/hooks/useVoipExtensionAction.tsx new file mode 100644 index 000000000000..e5f25683586a --- /dev/null +++ b/apps/meteor/client/views/admin/users/voip/hooks/useVoipExtensionAction.tsx @@ -0,0 +1,37 @@ +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useSetModal } from '@rocket.chat/ui-contexts'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { Action } from '../../../../hooks/useActionSpread'; +import AssignExtensionModal from '../AssignExtensionModal'; +import RemoveExtensionModal from '../RemoveExtensionModal'; + +type VoipExtensionActionParams = { + name: string; + username: string; + extension?: string; + enabled: boolean; +}; + +export const useVoipExtensionAction = ({ name, username, extension, enabled }: VoipExtensionActionParams): Action | undefined => { + const { t } = useTranslation(); + const setModal = useSetModal(); + + const handleExtensionAssignment = useEffectEvent(() => { + if (extension) { + setModal( setModal(null)} />); + return; + } + + setModal( setModal(null)} />); + }); + + return enabled + ? { + icon: extension ? 'phone-disabled' : 'phone', + label: extension ? t('Unassign_extension') : t('Assign_extension'), + action: handleExtensionAssignment, + } + : undefined; +}; diff --git a/apps/meteor/client/views/admin/users/voip/hooks/useVoipExtensionPermission.tsx b/apps/meteor/client/views/admin/users/voip/hooks/useVoipExtensionPermission.tsx new file mode 100644 index 000000000000..70e9e2ce91af --- /dev/null +++ b/apps/meteor/client/views/admin/users/voip/hooks/useVoipExtensionPermission.tsx @@ -0,0 +1,8 @@ +import { useSetting, usePermission } from '@rocket.chat/ui-contexts'; + +export const useVoipExtensionPermission = () => { + const isVoipSettingEnabled = useSetting('VoIP_TeamCollab_Enabled', false); + const canManageVoipExtensions = usePermission('manage-voip-extensions'); + + return isVoipSettingEnabled && canManageVoipExtensions; +}; diff --git a/packages/ui-voip/src/hooks/useVoipClient.tsx b/packages/ui-voip/src/hooks/useVoipClient.tsx index e4aad0f4919b..26c3c50427ba 100644 --- a/packages/ui-voip/src/hooks/useVoipClient.tsx +++ b/packages/ui-voip/src/hooks/useVoipClient.tsx @@ -6,6 +6,7 @@ import VoipClient from '../lib/VoipClient'; import { useWebRtcServers } from './useWebRtcServers'; type VoipClientParams = { + enabled?: boolean; autoRegister?: boolean; }; @@ -14,7 +15,7 @@ type VoipClientResult = { error: Error | null; }; -export const useVoipClient = ({ autoRegister = true }: VoipClientParams): VoipClientResult => { +export const useVoipClient = ({ enabled = true, autoRegister = true }: VoipClientParams = {}): VoipClientResult => { const { _id: userId } = useUser() || {}; const isVoipEnabled = useSetting('VoIP_TeamCollab_Enabled'); const voipClientRef = useRef(null); @@ -71,7 +72,7 @@ export const useVoipClient = ({ autoRegister = true }: VoipClientParams): VoipCl }, { initialData: null, - enabled: isVoipEnabled, + enabled, }, ); diff --git a/packages/ui-voip/src/hooks/useVoipExtensionDetails.tsx b/packages/ui-voip/src/hooks/useVoipExtensionDetails.tsx index d106ae2842aa..d08d6f851638 100644 --- a/packages/ui-voip/src/hooks/useVoipExtensionDetails.tsx +++ b/packages/ui-voip/src/hooks/useVoipExtensionDetails.tsx @@ -7,10 +7,7 @@ export const useVoipExtensionDetails = ({ extension, enabled = true }: { extensi const { data, ...result } = useQuery( ['voip', 'voip-extension-details', extension, getContactDetails], () => getContactDetails({ extension: extension as string }), - { - enabled: isEnabled, - onError: () => undefined, - }, + { enabled: isEnabled }, ); return { diff --git a/packages/ui-voip/src/providers/VoipProvider.tsx b/packages/ui-voip/src/providers/VoipProvider.tsx index 28133abd8698..d723c4b5ceb6 100644 --- a/packages/ui-voip/src/providers/VoipProvider.tsx +++ b/packages/ui-voip/src/providers/VoipProvider.tsx @@ -1,6 +1,12 @@ import { useEffectEvent, useLocalStorage } from '@rocket.chat/fuselage-hooks'; import type { Device } from '@rocket.chat/ui-contexts'; -import { useSetInputMediaDevice, useSetOutputMediaDevice, useSetting, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { + usePermission, + useSetInputMediaDevice, + useSetOutputMediaDevice, + useSetting, + useToastMessageDispatch, +} from '@rocket.chat/ui-contexts'; import type { ReactNode } from 'react'; import { useEffect, useMemo, useRef } from 'react'; import { createPortal } from 'react-dom'; @@ -15,16 +21,22 @@ import { useVoipSounds } from '../hooks/useVoipSounds'; const VoipProvider = ({ children }: { children: ReactNode }) => { // Settings - const isVoipEnabled = useSetting('VoIP_TeamCollab_Enabled') || false; + const isVoipSettingEnabled = useSetting('VoIP_TeamCollab_Enabled') || false; + const canViewVoipRegistrationInfo = usePermission('view-user-voip-extension'); + const isVoipEnabled = isVoipSettingEnabled && canViewVoipRegistrationInfo; + const [isLocalRegistered, setStorageRegistered] = useLocalStorage('voip-registered', true); // Hooks + const { t } = useTranslation(); const voipSounds = useVoipSounds(); - const { voipClient, error } = useVoipClient({ autoRegister: isLocalRegistered }); + const { voipClient, error } = useVoipClient({ + enabled: isVoipEnabled, + autoRegister: isLocalRegistered, + }); const setOutputMediaDevice = useSetOutputMediaDevice(); const setInputMediaDevice = useSetInputMediaDevice(); const dispatchToastMessage = useToastMessageDispatch(); - const { t } = useTranslation(); // Refs const remoteAudioMediaRef = useRef(null);