diff --git a/.changeset/light-yaks-drive.md b/.changeset/light-yaks-drive.md new file mode 100644 index 0000000000000..b36dcdeb02bb1 --- /dev/null +++ b/.changeset/light-yaks-drive.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Implements a modal to let users know about VoIP calls in direct messages and missing configurations. diff --git a/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useStartCallRoomAction.tsx b/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useStartCallRoomAction.tsx index 6538ff524feac..a4db8c7eddd6c 100644 --- a/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useStartCallRoomAction.tsx +++ b/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useStartCallRoomAction.tsx @@ -11,7 +11,7 @@ export const useStartCallRoomAction = () => { const voipCall = useVoipMenuOptions(); return useMemo((): RoomToolboxActionConfig | undefined => { - if (!videoCall.allowed && !voipCall.allowed) { + if (!videoCall.allowed && !voipCall?.allowed) { return undefined; } @@ -19,10 +19,10 @@ export const useStartCallRoomAction = () => { id: 'start-call', title: 'Call', icon: 'phone', - groups: [...videoCall.groups, ...voipCall.groups], - disabled: videoCall.disabled && voipCall.disabled, + groups: [...videoCall.groups, ...(voipCall?.groups ?? [])], + disabled: videoCall.disabled && (voipCall?.disabled ?? true), full: true, - order: Math.max(voipCall.order, videoCall.order), + order: Math.max(voipCall?.order ?? Number.NEGATIVE_INFINITY, videoCall.order), featured: true, renderToolboxItem: ({ id, icon, title, disabled, className }) => ( { key={id} title={title} disabled={disabled} - items={[...voipCall.items, ...videoCall.items]} + items={[...(voipCall?.allowed ? voipCall.items : []), ...videoCall.items]} className={className} placement='bottom-start' icon={icon} diff --git a/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useVoipMenuOptions.tsx b/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useVoipMenuOptions.tsx index 28b955765c49d..b6f31ff5cedf2 100644 --- a/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useVoipMenuOptions.tsx +++ b/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useVoipMenuOptions.tsx @@ -1,6 +1,7 @@ import { Box } from '@rocket.chat/fuselage'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; -import { useUserId } from '@rocket.chat/ui-contexts'; +import { usePermission, useUserId } from '@rocket.chat/ui-contexts'; import { useVoipAPI, useVoipState } from '@rocket.chat/ui-voip'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,11 +9,14 @@ import { useTranslation } from 'react-i18next'; import { useMediaPermissions } from '../../../views/room/composer/messageBox/hooks/useMediaPermissions'; import { useRoom } from '../../../views/room/contexts/RoomContext'; import { useUserInfoQuery } from '../../useUserInfoQuery'; +import { useVoipWarningModal } from '../../useVoipWarningModal'; const useVoipMenuOptions = () => { const { t } = useTranslation(); const { uids = [] } = useRoom(); const ownUserId = useUserId(); + const canStartVoiceCall = usePermission('view-user-voip-extension'); + const dispatchWarning = useVoipWarningModal(); const [isMicPermissionDenied] = useMediaPermissions('microphone'); @@ -27,7 +31,9 @@ const useVoipMenuOptions = () => { const isRemoteRegistered = !!remoteUser?.freeSwitchExtension; const isDM = members.length === 1; - const disabled = isMicPermissionDenied || !isDM || !isRemoteRegistered || !isRegistered || isInCall || isPending; + const disabled = isMicPermissionDenied || !isDM || isInCall || isPending; + const allowed = isDM && !isInCall && !isPending; + const canMakeVoipCall = allowed && isRemoteRegistered && isRegistered && isEnabled && !isMicPermissionDenied; const title = useMemo(() => { if (isMicPermissionDenied) { @@ -41,13 +47,24 @@ const useVoipMenuOptions = () => { return disabled ? t('Voice_calling_disabled') : ''; }, [disabled, isInCall, isMicPermissionDenied, t]); + const handleOnClick = useEffectEvent(() => { + if (canMakeVoipCall) { + return makeCall(remoteUser?.freeSwitchExtension as string); + } + dispatchWarning(); + }); + return useMemo(() => { + if (!canStartVoiceCall) { + return undefined; + } + const items: GenericMenuItemProps[] = [ { id: 'start-voip-call', icon: 'phone', disabled, - onClick: () => makeCall(remoteUser?.freeSwitchExtension as string), + onClick: handleOnClick, content: ( {t('Voice_call')} @@ -57,13 +74,13 @@ const useVoipMenuOptions = () => { ]; return { - items: isEnabled ? items : [], + items, groups: ['direct'] as const, disabled, - allowed: isEnabled, order: 4, + allowed, }; - }, [disabled, title, t, isEnabled, makeCall, remoteUser?.freeSwitchExtension]); + }, [disabled, title, t, handleOnClick, allowed, canStartVoiceCall]); }; export default useVoipMenuOptions; diff --git a/apps/meteor/client/hooks/useVoipWarningModal.tsx b/apps/meteor/client/hooks/useVoipWarningModal.tsx new file mode 100644 index 0000000000000..4e6cedbd5977d --- /dev/null +++ b/apps/meteor/client/hooks/useVoipWarningModal.tsx @@ -0,0 +1,30 @@ +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useRole, useRoute, useSetModal } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { useHasLicenseModule } from './useHasLicenseModule'; +import TeamsVoipConfigModal from '../views/room/contextualBar/TeamsVoipConfigModal'; + +export const useVoipWarningModal = (): (() => void) => { + const setModal = useSetModal(); + const isAdmin = useRole('admin'); + const hasModule = useHasLicenseModule('teams-voip') === true; + const teamsVoipSettingsRoute = useRoute('admin-settings'); + + const handleClose = useEffectEvent(() => setModal(null)); + + const handleRedirectToConfiguration = useEffectEvent(() => { + handleClose(); + teamsVoipSettingsRoute.push({ + group: 'VoIP_TeamCollab', + }); + }); + + return useMemo( + () => (): void => + setModal( + , + ), + [handleClose, handleRedirectToConfiguration, isAdmin, setModal, hasModule], + ); +}; diff --git a/apps/meteor/client/views/room/contextualBar/TeamsVoipConfigModal.tsx b/apps/meteor/client/views/room/contextualBar/TeamsVoipConfigModal.tsx new file mode 100644 index 0000000000000..1ee7b80b7775b --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/TeamsVoipConfigModal.tsx @@ -0,0 +1,116 @@ +import { + Modal, + Button, + Box, + Callout, + Margins, + ModalHeader, + ModalHeaderText, + ModalTagline, + ModalTitle, + ModalClose, + ModalContent, + ModalHeroImage, + ModalFooter, + ModalFooterAnnotation, + ModalFooterControllers, +} from '@rocket.chat/fuselage'; +import type { ReactElement } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; + +import { useExternalLink } from '../../../hooks/useExternalLink'; +import { GET_ADDONS_LINK } from '../../admin/subscription/utils/links'; + +type TeamsVoipConfigModalProps = { + onClose: () => void; + onConfirm?: () => void; + isAdmin: boolean; + hasModule: boolean; +}; + +const TeamsVoipConfigModal = ({ onClose, onConfirm, isAdmin, hasModule }: TeamsVoipConfigModalProps): ReactElement => { + const { t } = useTranslation(); + const openExternalLink = useExternalLink(); + + const getCalloutWarning = () => { + if (isAdmin && !hasModule) { + return t('Contact_sales_start_using_VoIP'); + } + + if (!isAdmin && !hasModule) { + return t('Contact_your_workspace_admin_to_start_using_VoIP'); + } + + return t('VoIP_available_setup_freeswitch_server_details'); + }; + + return ( + + + + {t('VoIP')} + {t('Team_voice_call')} + + + + + + + {t('Fully_integrated_voip_receive_internal_external_calls_without_switching_between_apps_external_systems')} + + {t('Features')} + + + +
  • + + Direct calling: Instantly start or receive calls with team members within your Rocket.Chat workspace. + +
  • +
  • + + Extension management: Admins can assign unique extensions to users, enabling quick, direct dialing both + from inside and outside your organization. + +
  • +
  • + + Call transfers: Seamlessly transfer active calls to ensure users reach the right team member. + +
  • +
  • + + Availability settings: Users can control their availability to receive calls, enhancing flexibility. + +
  • +
    +
    +
    + + {t('Required_action')} + + + {getCalloutWarning()} + +
    + + {!isAdmin && hasModule && {t('Only_admins_can_perform_this_setup')}} + + + {onConfirm && isAdmin && hasModule && ( + + )} + {isAdmin && !hasModule && ( + + )} + + +
    + ); +}; + +export default TeamsVoipConfigModal; diff --git a/apps/meteor/public/images/teams-voip-config.svg b/apps/meteor/public/images/teams-voip-config.svg new file mode 100644 index 0000000000000..f5282c60815fc --- /dev/null +++ b/apps/meteor/public/images/teams-voip-config.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 2ef172c1b9ffe..65c97d7477348 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1221,6 +1221,8 @@ "Contact_not_found": "Contact not found", "Contact_Profile": "Contact Profile", "Contact_Info": "Contact Information", + "Contact_sales_start_using_VoIP": "Contact sales to start using VoIP.", + "Contact_your_workspace_admin_to_start_using_VoIP": "Contact your workspace admin to start using VoIP.", "Content": "Content", "Continue": "Continue", "Continue_Adding": "Continue Adding?", @@ -2604,6 +2606,7 @@ "Forwarding": "Forwarding", "Free": "Free", "Free_Apps": "Free Apps", + "FreeSwitch_setup_required": "FreeSwitch setup required", "Frequently_Used": "Frequently Used", "Friday": "Friday", "From": "From", @@ -2611,6 +2614,7 @@ "From_email_warning": "Warning: The field From is subject to your mail server settings.", "Full_Name": "Full Name", "Full_Screen": "Full Screen", + "Fully_integrated_voip_receive_internal_external_calls_without_switching_between_apps_external_systems": "Fully-integrated Rocket.Chat VoIP allows your team to make and receive internal and external calls without switching between apps or external systems.", "Gaming": "Gaming", "General": "General", "General_Description": "Configure general workspace settings.", @@ -2647,6 +2651,7 @@ "Glossary_of_simplified_terms": "Glossary of simplified terms", "Go_to_your_workspace": "Go to your workspace", "Go_to_accessibility_and_appearance": "Go to accessibility and appearance", + "Go_to_settings": "Go to settings", "Google_Meet_Premium_only": "Google Meet (Premium only)", "Google_Play": "Google Play", "Hold_Call": "Hold Call", @@ -4191,6 +4196,7 @@ "On_Hold_conversations": "On hold conversations", "online": "online", "Online": "Online", + "Only_admins_can_perform_this_setup": "Only admins can perform this setup", "Only_authorized_users_can_write_new_messages": "Only authorized users can write new messages", "Only_authorized_users_can_react_to_messages": "Only authorized users can react to messages", "Only_from_users": "Only prune content from these users (leave empty to prune everyone's content)", @@ -5362,6 +5368,7 @@ "Team_Name": "Team Name", "Team_Remove_from_team_modal_content": "Would you like to remove this Channel from {{teamName}}? The Channel will be moved back to the workspace.", "Team_Remove_from_team": "Remove from team", + "Team_voice_call": "Team voice calls", "Team_what_is_this_team_about": "What is this team about", "Teams": "Teams", "Teams_about_the_channels": "And about the Channels?", @@ -6102,6 +6109,8 @@ "Voice_calling_enabled": "Voice calling is enabled", "Voice_calling_registration_failed": "Voice calling registration failed", "Voice_Call_Extension": "Voice Call Extension", + "VoIP": "VoIP", + "VoIP_available_setup_freeswitch_server_details": "VoIP is available but the FreeSwitch server details need to be set up from the team voice call settings.", "VoIP_Enable_Keep_Alive_For_Unstable_Networks": "Enable SIP Options Keep Alive", "VoIP_Enable_Keep_Alive_For_Unstable_Networks_Description": "Monitor the status of multiple external SIP gateways by sending periodic SIP OPTIONS messages. Used for unstable networks.", "VoIP_Enabled": "Enable voice channel", @@ -6136,6 +6145,10 @@ "VoIP_TeamCollab": "Team voice calls (VoIP)", "VoIP_TeamCollab_Description": "Set up FreeSwitch for VoIP in Team collaboration", "VoIP_TeamCollab_Enabled": "Enabled", + "VoIP_TeamCollab_Feature1": "<0>Direct calling: Instantly start or receive calls with team members within your Rocket.Chat workspace.", + "VoIP_TeamCollab_Feature2": "<0>Extension management: Admins can assign unique extensions to users, enabling quick, direct dialing both from inside and outside your organization.", + "VoIP_TeamCollab_Feature3": "<0>Call transfers: Seamlessly transfer active calls to ensure users reach the right team member.", + "VoIP_TeamCollab_Feature4": "<0>Availability settings: Users can control their availability to receive calls, enhancing flexibility.", "VoIP_TeamCollab_FreeSwitch_Host": "FreeSwitch Host", "VoIP_TeamCollab_FreeSwitch_Port": "FreeSwitch Port", "VoIP_TeamCollab_FreeSwitch_Password": "FreeSwitch Password",