From 9a136d68a6fc769015b07d68d7e8bde8dbc2bafa Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Wed, 20 Jul 2022 12:24:55 -0300 Subject: [PATCH 01/37] [IMPROVE] Improved voip call identification --- .../hooks/omnichannel/useContactName.ts | 17 ++++ .../omnichannel/useOmnichannelContacts.ts | 77 +++++++++++++++++++ .../client/sidebar/footer/voip/VoipFooter.tsx | 6 +- 3 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 apps/meteor/client/hooks/omnichannel/useContactName.ts create mode 100644 apps/meteor/client/hooks/omnichannel/useOmnichannelContacts.ts diff --git a/apps/meteor/client/hooks/omnichannel/useContactName.ts b/apps/meteor/client/hooks/omnichannel/useContactName.ts new file mode 100644 index 000000000000..95ba504812bc --- /dev/null +++ b/apps/meteor/client/hooks/omnichannel/useContactName.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react'; + +import { useOmnichannelContacts } from './useOmnichannelContacts'; + +export const useContactName = (phone: string): string => { + const safePhone = `+${phone.replace(/\D/g, '')}`; + const { getContactByPhone } = useOmnichannelContacts(); + const [name, setName] = useState(safePhone); + + useEffect(() => { + getContactByPhone(safePhone).then((contact) => { + setName(contact.name || contact.phone); + }); + }, [safePhone, getContactByPhone]); + + return name; +}; diff --git a/apps/meteor/client/hooks/omnichannel/useOmnichannelContacts.ts b/apps/meteor/client/hooks/omnichannel/useOmnichannelContacts.ts new file mode 100644 index 000000000000..26fcd8308fec --- /dev/null +++ b/apps/meteor/client/hooks/omnichannel/useOmnichannelContacts.ts @@ -0,0 +1,77 @@ +import { ILivechatVisitor } from '@rocket.chat/core-typings'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useCallback, useEffect, useState } from 'react'; + +type ContactsHookAPI = { + getContactByPhone(phone: string): Promise; +}; + +type Contact = { + name: string; + phone: string; +}; + +const STORAGE_KEY = 'rcOmnichannelContacts'; + +const createContact = (phone: string, data: ILivechatVisitor | null): Contact => ({ + phone, + name: data?.name || '', +}); + +const storeInCache = (contacts: Record): void => { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(contacts)); +}; + +const retrieveFromCache = (): Record => { + const cache = window.localStorage.getItem(STORAGE_KEY); + + try { + return cache ? JSON.parse(cache) : {}; + } catch (_) { + return {}; + } +}; + +export const useOmnichannelContacts = (): ContactsHookAPI => { + const getContactBy = useEndpoint('GET', '/v1/omnichannel/contact.search'); + const [contacts, setContacts] = useState>(retrieveFromCache); + + useEffect(() => { + storeInCache(contacts); + }, [contacts]); + + const getContactByPhoneFromCache = useCallback((phone: string): Contact | null => contacts[phone] || null, [contacts]); + + const addContactToCache = useCallback( + (contact: Contact): void => { + setContacts({ ...contacts, [contact.phone]: contact }); + }, + [contacts], + ); + + const fetchContactByPhone = useCallback( + (phone: string): Promise> => getContactBy({ phone }), + [getContactBy], + ); + + const getContactByPhone = useCallback( + async (phone: string): Promise => { + const cache = getContactByPhoneFromCache(phone); + if (cache) { + return cache; + } + + const data = await fetchContactByPhone(phone); + const contact = createContact(phone, data.contact); + + addContactToCache(contact); + + return contact; + }, + [addContactToCache, fetchContactByPhone, getContactByPhoneFromCache], + ); + + return { + getContactByPhone, + }; +}; diff --git a/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx b/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx index 8d73671f8436..49c818cdeec0 100644 --- a/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx +++ b/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx @@ -5,8 +5,8 @@ import { Box, Button, ButtonGroup, Icon, SidebarFooter, Menu, IconButton } from import React, { ReactElement, MouseEvent, ReactNode } from 'react'; import type { VoipFooterMenuOptions } from '../../../../ee/client/hooks/useVoipFooterMenu'; -import { parseOutboundPhoneNumber } from '../../../../ee/client/lib/voip/parseOutboundPhoneNumber'; import { CallActionsType } from '../../../contexts/CallContext'; +import { useContactName } from '../../../hooks/omnichannel/useContactName'; type VoipFooterPropsType = { caller: ICallerInfo; @@ -58,6 +58,8 @@ export const VoipFooter = ({ children, options, }: VoipFooterPropsType): ReactElement => { + const contactName = useContactName(caller.callerId); + const cssClickable = callerState === 'IN_CALL' || callerState === 'ON_HOLD' ? css` @@ -117,7 +119,7 @@ export const VoipFooter = ({ - {caller.callerName || parseOutboundPhoneNumber(caller.callerId) || anonymousText} + {caller.callerName || contactName || anonymousText} {subtitle} From 0166bedfa40f6ba8a00d7ace0968652b3b11dd15 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Wed, 6 Jul 2022 14:13:16 -0300 Subject: [PATCH 02/37] Chore: Improved styling logic for the call dial button --- .../views/omnichannel/directory/CallDialpadButton.tsx | 8 ++++++++ .../views/omnichannel/directory/calls/CallTable.tsx | 11 +---------- .../omnichannel/directory/contacts/ContactTable.tsx | 10 +--------- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/apps/meteor/client/views/omnichannel/directory/CallDialpadButton.tsx b/apps/meteor/client/views/omnichannel/directory/CallDialpadButton.tsx index 15ddb34928e5..78bdda45d796 100644 --- a/apps/meteor/client/views/omnichannel/directory/CallDialpadButton.tsx +++ b/apps/meteor/client/views/omnichannel/directory/CallDialpadButton.tsx @@ -1,3 +1,4 @@ +import { css } from '@rocket.chat/css-in-js'; import { IconButton } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { MouseEvent, ReactElement } from 'react'; @@ -5,6 +6,12 @@ import React, { MouseEvent, ReactElement } from 'react'; import { useVoipOutboundStates } from '../../../contexts/CallContext'; import { useDialModal } from '../../../hooks/useDialModal'; +export const rcxCallDialButton = css` + .rcx-show-call-button-on-hover:not(:hover) & { + display: none !important; + } +`; + export const CallDialpadButton = ({ phoneNumber }: { phoneNumber: string }): ReactElement => { const t = useTranslation(); @@ -20,6 +27,7 @@ export const CallDialpadButton = ({ phoneNumber }: { phoneNumber: string }): Rea { return ( onRowClick(_id, v?.token)} diff --git a/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx b/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx index 2cc4127bc20c..cae96653a9d7 100644 --- a/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx +++ b/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx @@ -1,4 +1,3 @@ -import { css } from '@rocket.chat/css-in-js'; import { Icon, Pagination, States, StatesAction, StatesActions, StatesIcon, StatesTitle, Box } from '@rocket.chat/fuselage'; import { useDebouncedState, useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; @@ -27,13 +26,6 @@ type ContactTableProps = { setContactReload(fn: () => void): void; }; -export const rcxCallDialButton = css` - &:not(:hover) { - .rcx-call-dial-button { - display: none !important; - } - } -`; function ContactTable({ setContactReload }: ContactTableProps): ReactElement { const { current, itemsPerPage, setItemsPerPage, setCurrent, ...paginationProps } = usePagination(); const { sortBy, sortDirection, setSort } = useSort<'username' | 'phone' | 'name' | 'visitorEmails.address' | 'lastchat'>('username'); @@ -140,7 +132,7 @@ function ContactTable({ setContactReload }: ContactTableProps): ReactElement { role='link' height='40px' qa-user-id={_id} - className={rcxCallDialButton} + rcx-show-call-button-on-hover onClick={onRowClick(_id)} > {username} From ad5818cd074f1b100403c5f6be52a4b3276e5f62 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Thu, 21 Jul 2022 15:45:58 -0300 Subject: [PATCH 03/37] [FIX] Fixed FilterByText passing invalid props to div --- apps/meteor/client/components/FilterByText.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/components/FilterByText.tsx b/apps/meteor/client/components/FilterByText.tsx index b551ffd72944..9d1d366a80be 100644 --- a/apps/meteor/client/components/FilterByText.tsx +++ b/apps/meteor/client/components/FilterByText.tsx @@ -38,7 +38,7 @@ const FilterByText = ({ placeholder, onChange: setFilter, inputRef, children, .. }, []); return ( - + Date: Thu, 21 Jul 2022 15:46:51 -0300 Subject: [PATCH 04/37] [FIX] Changed useContactName phone parse fn --- apps/meteor/client/hooks/omnichannel/useContactName.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/hooks/omnichannel/useContactName.ts b/apps/meteor/client/hooks/omnichannel/useContactName.ts index 95ba504812bc..7a91c790cc38 100644 --- a/apps/meteor/client/hooks/omnichannel/useContactName.ts +++ b/apps/meteor/client/hooks/omnichannel/useContactName.ts @@ -1,9 +1,10 @@ import { useEffect, useState } from 'react'; +import { parseOutboundPhoneNumber } from '../../../ee/client/lib/voip/parseOutboundPhoneNumber'; import { useOmnichannelContacts } from './useOmnichannelContacts'; export const useContactName = (phone: string): string => { - const safePhone = `+${phone.replace(/\D/g, '')}`; + const safePhone = parseOutboundPhoneNumber(phone); const { getContactByPhone } = useOmnichannelContacts(); const [name, setName] = useState(safePhone); From 6abd4e3345e2cf9148291b93ff4edd76768f0f67 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Thu, 21 Jul 2022 15:47:53 -0300 Subject: [PATCH 05/37] [IMPROVE] Moved call table row to its own component and added contact names --- .../omnichannel/directory/calls/CallTable.tsx | 48 +--------------- .../directory/calls/CallTableRow.tsx | 57 +++++++++++++++++++ 2 files changed, 59 insertions(+), 46 deletions(-) create mode 100644 apps/meteor/client/views/omnichannel/directory/calls/CallTableRow.tsx diff --git a/apps/meteor/client/views/omnichannel/directory/calls/CallTable.tsx b/apps/meteor/client/views/omnichannel/directory/calls/CallTable.tsx index 0f473e622aea..0beb4dd9beb9 100644 --- a/apps/meteor/client/views/omnichannel/directory/calls/CallTable.tsx +++ b/apps/meteor/client/views/omnichannel/directory/calls/CallTable.tsx @@ -1,16 +1,11 @@ -import { IVoipRoom } from '@rocket.chat/core-typings'; -import { Table } from '@rocket.chat/fuselage'; import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; -import moment from 'moment'; import React, { useState, useMemo, useCallback, FC } from 'react'; -import { parseOutboundPhoneNumber } from '../../../../../ee/client/lib/voip/parseOutboundPhoneNumber'; import GenericTable from '../../../../components/GenericTable'; -import { useIsCallReady } from '../../../../contexts/CallContext'; import { useEndpointData } from '../../../../hooks/useEndpointData'; -import { CallDialpadButton } from '../CallDialpadButton'; +import { CallTableRow } from './CallTableRow'; const useQuery = ( { @@ -57,18 +52,6 @@ const CallTable: FC = () => { const userIdLoggedIn = Meteor.userId(); const query = useQuery(debouncedParams, debouncedSort, userIdLoggedIn); const directoryRoute = useRoute('omnichannel-directory'); - const isCallReady = useIsCallReady(); - - const resolveDirectionLabel = useCallback( - (direction: IVoipRoom['direction']) => { - const labels = { - inbound: 'Incoming', - outbound: 'Outgoing', - } as const; - return t(labels[direction] || 'Not_Available'); - }, - [t], - ); const onHeaderClick = useMutableCallback((id) => { const [sortBy, sortDirection] = sort; @@ -147,34 +130,7 @@ const CallTable: FC = () => { [sort, onHeaderClick, t], ); - const renderRow = useCallback( - ({ _id, fname, callStarted, queue, callDuration, v, direction }) => { - const duration = moment.duration(callDuration / 1000, 'seconds'); - const phoneNumber = Array.isArray(v?.phone) ? v?.phone[0]?.phoneNumber : v?.phone; - - return ( - onRowClick(_id, v?.token)} - action - qa-user-id={_id} - height='40px' - > - {parseOutboundPhoneNumber(fname)} - {parseOutboundPhoneNumber(phoneNumber)} - {queue} - {moment(callStarted).format('L LTS')} - {duration.isValid() && duration.humanize()} - {resolveDirectionLabel(direction)} - {isCallReady && } - - ); - }, - [onRowClick, resolveDirectionLabel, isCallReady], - ); + const renderRow = useCallback((room) => , [onRowClick]); return ( { + const t = useTranslation(); + const isCallReady = useIsCallReady(); + + const { _id, fname, callStarted, queue, callDuration = 0, v, direction } = room; + const duration = moment.duration(callDuration / 1000, 'seconds'); + const phoneNumber = Array.isArray(v?.phone) ? v?.phone[0]?.phoneNumber : v?.phone; + const contactName = useContactName(phoneNumber); + + const resolveDirectionLabel = useCallback( + (direction: IVoipRoom['direction']) => { + const labels = { + inbound: 'Incoming', + outbound: 'Outgoing', + } as const; + return t(labels[direction] || 'Not_Available'); + }, + [t], + ); + + return ( + onRowClick(_id, v?.token)} + action + qa-user-id={_id} + height='40px' + > + {contactName || parseOutboundPhoneNumber(fname)} + {parseOutboundPhoneNumber(phoneNumber)} + {queue} + {moment(callStarted).format('L LTS')} + {duration.isValid() && duration.humanize()} + {resolveDirectionLabel(direction)} + {isCallReady && } + + ); +}; From 3eb082e26049db8f9f31ef5a24cd1348e62b9af1 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Thu, 21 Jul 2022 16:46:56 -0300 Subject: [PATCH 06/37] Chore: Adjusted endpoint typings --- packages/rest-typings/src/v1/omnichannel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 674acb0b4f35..3475fe72979a 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -1139,13 +1139,13 @@ export type OmnichannelEndpoints = { }>, ) => PaginatedResult<{ visitors: any[] }>; }; - 'omnichannel/contact': { + '/v1/omnichannel/contact': { POST: (params: POSTOmnichannelContactProps) => { contact: string }; GET: (params: GETOmnichannelContactProps) => { contact: ILivechatVisitor | null }; }; - 'omnichannel/contact.search': { + '/v1/omnichannel/contact.search': { GET: (params: GETOmnichannelContactSearchProps) => { contact: ILivechatVisitor | null }; }; }; From f98a7e3a81b08bbbc9588b83a6860db7877b8fd4 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Thu, 21 Jul 2022 17:09:32 -0300 Subject: [PATCH 07/37] Chore: Adjusted hook typings --- .../client/hooks/omnichannel/useOmnichannelContacts.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/meteor/client/hooks/omnichannel/useOmnichannelContacts.ts b/apps/meteor/client/hooks/omnichannel/useOmnichannelContacts.ts index 26fcd8308fec..c5f159e8556e 100644 --- a/apps/meteor/client/hooks/omnichannel/useOmnichannelContacts.ts +++ b/apps/meteor/client/hooks/omnichannel/useOmnichannelContacts.ts @@ -13,7 +13,7 @@ type Contact = { const STORAGE_KEY = 'rcOmnichannelContacts'; -const createContact = (phone: string, data: ILivechatVisitor | null): Contact => ({ +const createContact = (phone: string, data: Pick | null): Contact => ({ phone, name: data?.name || '', }); @@ -49,10 +49,7 @@ export const useOmnichannelContacts = (): ContactsHookAPI => { [contacts], ); - const fetchContactByPhone = useCallback( - (phone: string): Promise> => getContactBy({ phone }), - [getContactBy], - ); + const fetchContactByPhone = useCallback((phone: string): ReturnType => getContactBy({ phone }), [getContactBy]); const getContactByPhone = useCallback( async (phone: string): Promise => { From ddbd469b94dc2bff0e483b91ca3b5b2f2a0c813d Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 26 Jul 2022 11:13:39 -0600 Subject: [PATCH 08/37] small change on error message --- .../meteor/client/providers/CallProvider/CallProvider.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/providers/CallProvider/CallProvider.tsx b/apps/meteor/client/providers/CallProvider/CallProvider.tsx index b55bf57ddd54..58ad97daf0c6 100644 --- a/apps/meteor/client/providers/CallProvider/CallProvider.tsx +++ b/apps/meteor/client/providers/CallProvider/CallProvider.tsx @@ -389,14 +389,20 @@ export const CallProvider: FC = ({ children }) => { stopAllRingback(); }; - const onCallFailed = (reason: 'Not Found' | 'Address Incomplete' | string): void => { + const onCallFailed = (reason: 'Not Found' | 'Address Incomplete' | 'Request Terminated' | string): void => { switch (reason) { case 'Not Found': + // This happens when the call matches dialplan and goes to the world, but the trunk doesnt find the number. openDialModal({ errorMessage: t('Dialed_number_doesnt_exist') }); break; case 'Address Incomplete': + // This happens when the dialed number doesnt match a valid asterisk dialplan pattern or the number is invalid. openDialModal({ errorMessage: t('Dialed_number_is_incomplete') }); break; + case 'Request Terminated': + // This happens when the user is the one hanging up the call. + openDialModal(); + break; default: openDialModal({ errorMessage: t('Something_went_wrong_try_again_later') }); } From 21eb8bf1fadb55447c81a61c1e99d7ff8da981fa Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Tue, 2 Aug 2022 09:57:14 -0300 Subject: [PATCH 09/37] [FIX] Contact name not being displayed in the contact table --- .../views/omnichannel/directory/contacts/ContactTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx b/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx index cae96653a9d7..256d4b625c5f 100644 --- a/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx +++ b/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx @@ -120,7 +120,7 @@ function ContactTable({ setContactReload }: ContactTableProps): ReactElement { {result.phase === AsyncStatePhase.RESOLVED && ( - {result.value.visitors.map(({ _id, username, fname, visitorEmails, phone, lastChat }) => { + {result.value.visitors.map(({ _id, username, fname, name, visitorEmails, phone, lastChat }) => { const phoneNumber = phone?.length && phone[0].phoneNumber; const visitorEmail = visitorEmails?.length && visitorEmails[0].address; @@ -136,7 +136,7 @@ function ContactTable({ setContactReload }: ContactTableProps): ReactElement { onClick={onRowClick(_id)} > {username} - {parseOutboundPhoneNumber(fname)} + {parseOutboundPhoneNumber(fname || name)} {parseOutboundPhoneNumber(phoneNumber)} {visitorEmail} {lastChat && formatDate(lastChat.ts)} From fc37ab1f5dd19caa6583f362d7cd49b8b172b590 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Tue, 2 Aug 2022 09:58:01 -0300 Subject: [PATCH 10/37] [FIX] Contact name not being displayed in the voip contextual bar --- .../directory/calls/contextualBar/VoipInfo.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx b/apps/meteor/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx index fba024243d7b..629104e5f873 100644 --- a/apps/meteor/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx +++ b/apps/meteor/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx @@ -14,6 +14,7 @@ import AgentInfoDetails from '../../../components/AgentInfoDetails'; import AgentField from '../../chats/contextualBar/AgentField'; import { InfoField } from './InfoField'; import { VoipInfoCallButton } from './VoipInfoCallButton'; +import { useContactName } from '../../../../../hooks/omnichannel/useContactName'; type VoipInfoPropsType = { room: IVoipRoom; @@ -34,6 +35,7 @@ export const VoipInfo = ({ room, onClickClose /* , onClickReport */ }: VoipInfo const shouldShowWrapup = useMemo(() => lastMessage?.t === 'voip-call-wrapup' && lastMessage?.msg, [lastMessage]); const shouldShowTags = useMemo(() => tags && tags.length > 0, [tags]); const _name = fname || name; + const contactName = useContactName(phoneNumber); return ( <> @@ -57,7 +59,11 @@ export const VoipInfo = ({ room, onClickClose /* , onClickReport */ }: VoipInfo {t('Contact')} - } /> + } + /> )} From f6f71f526f54f5b6b882b6b4756450dfde6de2c2 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Tue, 2 Aug 2022 11:32:13 -0300 Subject: [PATCH 11/37] Chore: Adjusting import order --- .../omnichannel/directory/calls/contextualBar/VoipInfo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx b/apps/meteor/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx index 629104e5f873..1be47b06179f 100644 --- a/apps/meteor/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx +++ b/apps/meteor/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx @@ -10,11 +10,11 @@ import { UserStatus } from '../../../../../components/UserStatus'; import VerticalBar from '../../../../../components/VerticalBar'; import UserAvatar from '../../../../../components/avatar/UserAvatar'; import { useIsCallReady } from '../../../../../contexts/CallContext'; +import { useContactName } from '../../../../../hooks/omnichannel/useContactName'; import AgentInfoDetails from '../../../components/AgentInfoDetails'; import AgentField from '../../chats/contextualBar/AgentField'; import { InfoField } from './InfoField'; import { VoipInfoCallButton } from './VoipInfoCallButton'; -import { useContactName } from '../../../../../hooks/omnichannel/useContactName'; type VoipInfoPropsType = { room: IVoipRoom; From 7d3211c0f75afeba892a6d701c0180b08fd54778 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Tue, 2 Aug 2022 17:13:13 -0300 Subject: [PATCH 12/37] [REFACTOR] Moved contacts fetch logic to context to reduce fetch requests --- .../omnichannel/useOmnichannelContacts.ts | 75 +------------ .../client/providers/MeteorProvider.tsx | 5 +- .../providers/OmnichannelContactsProvider.tsx | 106 ++++++++++++++++++ 3 files changed, 113 insertions(+), 73 deletions(-) create mode 100644 apps/meteor/client/providers/OmnichannelContactsProvider.tsx diff --git a/apps/meteor/client/hooks/omnichannel/useOmnichannelContacts.ts b/apps/meteor/client/hooks/omnichannel/useOmnichannelContacts.ts index c5f159e8556e..448e38d4ed93 100644 --- a/apps/meteor/client/hooks/omnichannel/useOmnichannelContacts.ts +++ b/apps/meteor/client/hooks/omnichannel/useOmnichannelContacts.ts @@ -1,74 +1,5 @@ -import { ILivechatVisitor } from '@rocket.chat/core-typings'; -import { useEndpoint } from '@rocket.chat/ui-contexts'; -import { useCallback, useEffect, useState } from 'react'; +import { useContext } from 'react'; -type ContactsHookAPI = { - getContactByPhone(phone: string): Promise; -}; +import { ContactsContext, ContactsContextValue } from '../../providers/OmnichannelContactsProvider'; -type Contact = { - name: string; - phone: string; -}; - -const STORAGE_KEY = 'rcOmnichannelContacts'; - -const createContact = (phone: string, data: Pick | null): Contact => ({ - phone, - name: data?.name || '', -}); - -const storeInCache = (contacts: Record): void => { - window.localStorage.setItem(STORAGE_KEY, JSON.stringify(contacts)); -}; - -const retrieveFromCache = (): Record => { - const cache = window.localStorage.getItem(STORAGE_KEY); - - try { - return cache ? JSON.parse(cache) : {}; - } catch (_) { - return {}; - } -}; - -export const useOmnichannelContacts = (): ContactsHookAPI => { - const getContactBy = useEndpoint('GET', '/v1/omnichannel/contact.search'); - const [contacts, setContacts] = useState>(retrieveFromCache); - - useEffect(() => { - storeInCache(contacts); - }, [contacts]); - - const getContactByPhoneFromCache = useCallback((phone: string): Contact | null => contacts[phone] || null, [contacts]); - - const addContactToCache = useCallback( - (contact: Contact): void => { - setContacts({ ...contacts, [contact.phone]: contact }); - }, - [contacts], - ); - - const fetchContactByPhone = useCallback((phone: string): ReturnType => getContactBy({ phone }), [getContactBy]); - - const getContactByPhone = useCallback( - async (phone: string): Promise => { - const cache = getContactByPhoneFromCache(phone); - if (cache) { - return cache; - } - - const data = await fetchContactByPhone(phone); - const contact = createContact(phone, data.contact); - - addContactToCache(contact); - - return contact; - }, - [addContactToCache, fetchContactByPhone, getContactByPhoneFromCache], - ); - - return { - getContactByPhone, - }; -}; +export const useOmnichannelContacts = (): ContactsContextValue => useContext(ContactsContext); diff --git a/apps/meteor/client/providers/MeteorProvider.tsx b/apps/meteor/client/providers/MeteorProvider.tsx index 5a5454fc68e3..1d115f9c59a9 100644 --- a/apps/meteor/client/providers/MeteorProvider.tsx +++ b/apps/meteor/client/providers/MeteorProvider.tsx @@ -9,6 +9,7 @@ import CustomSoundProvider from './CustomSoundProvider'; import { DeviceProvider } from './DeviceProvider/DeviceProvider'; import LayoutProvider from './LayoutProvider'; import ModalProvider from './ModalProvider'; +import { OmnichannelContactsProvider } from './OmnichannelContactsProvider'; import OmnichannelProvider from './OmnichannelProvider'; import RouterProvider from './RouterProvider'; import ServerProvider from './ServerProvider'; @@ -39,7 +40,9 @@ const MeteorProvider: FC = ({ children }) => ( - {children} + + {children} + diff --git a/apps/meteor/client/providers/OmnichannelContactsProvider.tsx b/apps/meteor/client/providers/OmnichannelContactsProvider.tsx new file mode 100644 index 000000000000..8c9d788d0e4b --- /dev/null +++ b/apps/meteor/client/providers/OmnichannelContactsProvider.tsx @@ -0,0 +1,106 @@ +import { ILivechatVisitor } from '@rocket.chat/core-typings'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import React, { ReactElement, ReactNode, useCallback, useEffect, createContext, useMemo, useRef } from 'react'; + +type Contact = { + name: string; + phone: string; +}; + +type ContactsProviderProps = { + children: ReactNode | undefined; +}; + +export type ContactsContextValue = { + getContactByPhone(phone: string): Promise; +}; + +const STORAGE_KEY = 'rcOmnichannelContacts'; + +const createContact = (phone: string, data: Pick | null): Contact => ({ + phone, + name: data?.name || '', +}); + +const storeInCache = (contacts: Record): void => { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(contacts)); +}; + +const retrieveFromCache = (): Record => { + const cache = window.localStorage.getItem(STORAGE_KEY); + + try { + return cache ? JSON.parse(cache) : {}; + } catch (_) { + return {}; + } +}; + +export const ContactsContext = createContext({ + getContactByPhone: (_: string) => Promise.reject(), +}); + +export const OmnichannelContactsProvider = ({ children }: ContactsProviderProps): ReactElement => { + const getContactBy = useEndpoint('GET', '/v1/omnichannel/contact.search'); + const contacts = useRef>({}); + const pendingRequests = useRef>>(new Map()); + + useEffect(() => { + contacts.current = retrieveFromCache(); + + const handleVisibilityChange = (): void => storeInCache(contacts.current); + document.addEventListener('visibilitychange', handleVisibilityChange); + + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; + }, []); + + const getContactByPhoneFromCache = useCallback((phone: string): Contact | null => contacts.current[phone] || null, []); + + const addContactToCache = useCallback((contact: Contact): void => { + contacts.current[contact.phone] = contact; + }, []); + + const fetchContactByPhone = useCallback((phone: string): ReturnType => getContactBy({ phone }), [getContactBy]); + + const getContactByPhone = useCallback( + (phone: string): Promise => { + const cache = getContactByPhoneFromCache(phone); + const cachedRequest = pendingRequests.current.get(phone); + + if (cache) { + return Promise.resolve(cache); + } + + if (cachedRequest) { + return cachedRequest; + } + + const request = fetchContactByPhone(phone) + .then((data) => { + const contact = createContact(phone, data.contact); + addContactToCache(contact); + + return contact; + }) + .finally(() => { + pendingRequests.current.delete(phone); + }); + + pendingRequests.current.set(phone, request); + + return request; + }, + [addContactToCache, fetchContactByPhone, getContactByPhoneFromCache], + ); + + const contextValue = useMemo( + () => ({ + getContactByPhone, + }), + [getContactByPhone], + ); + + return {children}; +}; From e64a427e0650f2ceebf36d12ba7da16e8670107e Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Sun, 14 Aug 2022 20:21:01 -0300 Subject: [PATCH 13/37] Chore: Removing useContactName from call table and voip info --- .../views/omnichannel/directory/calls/CallTableRow.tsx | 4 +--- .../directory/calls/contextualBar/VoipInfo.tsx | 8 +------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/apps/meteor/client/views/omnichannel/directory/calls/CallTableRow.tsx b/apps/meteor/client/views/omnichannel/directory/calls/CallTableRow.tsx index 41db9b26a1d2..851d3a7ec6c1 100644 --- a/apps/meteor/client/views/omnichannel/directory/calls/CallTableRow.tsx +++ b/apps/meteor/client/views/omnichannel/directory/calls/CallTableRow.tsx @@ -6,7 +6,6 @@ import React, { ReactElement, useCallback } from 'react'; import { parseOutboundPhoneNumber } from '../../../../../ee/client/lib/voip/parseOutboundPhoneNumber'; import { useIsCallReady } from '../../../../contexts/CallContext'; -import { useContactName } from '../../../../hooks/omnichannel/useContactName'; import { CallDialpadButton } from '../CallDialpadButton'; type CallTableRowProps = { @@ -21,7 +20,6 @@ export const CallTableRow = ({ room, onRowClick }: CallTableRowProps): ReactElem const { _id, fname, callStarted, queue, callDuration = 0, v, direction } = room; const duration = moment.duration(callDuration / 1000, 'seconds'); const phoneNumber = Array.isArray(v?.phone) ? v?.phone[0]?.phoneNumber : v?.phone; - const contactName = useContactName(phoneNumber); const resolveDirectionLabel = useCallback( (direction: IVoipRoom['direction']) => { @@ -45,7 +43,7 @@ export const CallTableRow = ({ room, onRowClick }: CallTableRowProps): ReactElem qa-user-id={_id} height='40px' > - {contactName || parseOutboundPhoneNumber(fname)} + {parseOutboundPhoneNumber(fname)} {parseOutboundPhoneNumber(phoneNumber)} {queue} {moment(callStarted).format('L LTS')} diff --git a/apps/meteor/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx b/apps/meteor/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx index 834ab8479de4..100277afae74 100644 --- a/apps/meteor/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx +++ b/apps/meteor/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx @@ -10,7 +10,6 @@ import { UserStatus } from '../../../../../components/UserStatus'; import VerticalBar from '../../../../../components/VerticalBar'; import UserAvatar from '../../../../../components/avatar/UserAvatar'; import { useIsCallReady } from '../../../../../contexts/CallContext'; -import { useContactName } from '../../../../../hooks/omnichannel/useContactName'; import AgentInfoDetails from '../../../components/AgentInfoDetails'; import AgentField from '../../chats/contextualBar/AgentField'; import { InfoField } from './InfoField'; @@ -35,7 +34,6 @@ export const VoipInfo = ({ room, onClickClose /* , onClickReport */ }: VoipInfo const shouldShowWrapup = useMemo(() => lastMessage?.t === 'voip-call-wrapup' && lastMessage?.msg, [lastMessage]); const shouldShowTags = useMemo(() => tags && tags.length > 0, [tags]); const _name = fname || name; - const contactName = useContactName(phoneNumber); return ( <> @@ -59,11 +57,7 @@ export const VoipInfo = ({ room, onClickClose /* , onClickReport */ }: VoipInfo {t('Contact')} - } - /> + } /> )} From 7c89f34d225296c1575d848a99a55afbc30199f3 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Sun, 14 Aug 2022 21:37:56 -0300 Subject: [PATCH 14/37] Chore: Removing contacts provider restoring useOmnichannelContacts hook --- .../omnichannel/useOmnichannelContacts.ts | 73 +++++++++++- .../client/providers/MeteorProvider.tsx | 5 +- .../providers/OmnichannelContactsProvider.tsx | 106 ------------------ 3 files changed, 71 insertions(+), 113 deletions(-) delete mode 100644 apps/meteor/client/providers/OmnichannelContactsProvider.tsx diff --git a/apps/meteor/client/hooks/omnichannel/useOmnichannelContacts.ts b/apps/meteor/client/hooks/omnichannel/useOmnichannelContacts.ts index 448e38d4ed93..c7c8ac41629e 100644 --- a/apps/meteor/client/hooks/omnichannel/useOmnichannelContacts.ts +++ b/apps/meteor/client/hooks/omnichannel/useOmnichannelContacts.ts @@ -1,5 +1,72 @@ -import { useContext } from 'react'; +import { ILivechatVisitor } from '@rocket.chat/core-typings'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useCallback, useEffect, useRef } from 'react'; -import { ContactsContext, ContactsContextValue } from '../../providers/OmnichannelContactsProvider'; +type Contact = { + name: string; + phone: string; +}; -export const useOmnichannelContacts = (): ContactsContextValue => useContext(ContactsContext); +export type ContactsHookValue = { + getContactByPhone(phone: string): Promise; +}; + +const STORAGE_KEY = 'rcOmnichannelContacts'; + +const createContact = (phone: string, data: Pick | null): Contact => ({ + phone, + name: data?.name || '', +}); + +const storeInCache = (contacts: Record): void => { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(contacts)); +}; + +const retrieveFromCache = (): Record => { + const cache = window.localStorage.getItem(STORAGE_KEY); + + try { + return cache ? JSON.parse(cache) : {}; + } catch (_) { + return {}; + } +}; + +export const useOmnichannelContacts = (): ContactsHookValue => { + const getContactBy = useEndpoint('GET', '/v1/omnichannel/contact.search'); + const contacts = useRef>({}); + + useEffect(() => { + contacts.current = retrieveFromCache(); + return () => storeInCache(contacts.current); + }, []); + + const getContactByPhoneFromCache = useCallback((phone: string): Contact | null => contacts.current[phone] || null, []); + + const addContactToCache = useCallback((contact: Contact): void => { + contacts.current[contact.phone] = contact; + }, []); + + const fetchContactByPhone = useCallback((phone: string): ReturnType => getContactBy({ phone }), [getContactBy]); + + const getContactByPhone = useCallback( + async (phone: string): Promise => { + const cache = getContactByPhoneFromCache(phone); + + if (cache) { + return Promise.resolve(cache); + } + + const data = await fetchContactByPhone(phone); + const contact = createContact(phone, data.contact); + addContactToCache(contact); + + return contact; + }, + [addContactToCache, fetchContactByPhone, getContactByPhoneFromCache], + ); + + return { + getContactByPhone, + }; +}; diff --git a/apps/meteor/client/providers/MeteorProvider.tsx b/apps/meteor/client/providers/MeteorProvider.tsx index 1d115f9c59a9..5a5454fc68e3 100644 --- a/apps/meteor/client/providers/MeteorProvider.tsx +++ b/apps/meteor/client/providers/MeteorProvider.tsx @@ -9,7 +9,6 @@ import CustomSoundProvider from './CustomSoundProvider'; import { DeviceProvider } from './DeviceProvider/DeviceProvider'; import LayoutProvider from './LayoutProvider'; import ModalProvider from './ModalProvider'; -import { OmnichannelContactsProvider } from './OmnichannelContactsProvider'; import OmnichannelProvider from './OmnichannelProvider'; import RouterProvider from './RouterProvider'; import ServerProvider from './ServerProvider'; @@ -40,9 +39,7 @@ const MeteorProvider: FC = ({ children }) => ( - - {children} - + {children} diff --git a/apps/meteor/client/providers/OmnichannelContactsProvider.tsx b/apps/meteor/client/providers/OmnichannelContactsProvider.tsx deleted file mode 100644 index 8c9d788d0e4b..000000000000 --- a/apps/meteor/client/providers/OmnichannelContactsProvider.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { ILivechatVisitor } from '@rocket.chat/core-typings'; -import { useEndpoint } from '@rocket.chat/ui-contexts'; -import React, { ReactElement, ReactNode, useCallback, useEffect, createContext, useMemo, useRef } from 'react'; - -type Contact = { - name: string; - phone: string; -}; - -type ContactsProviderProps = { - children: ReactNode | undefined; -}; - -export type ContactsContextValue = { - getContactByPhone(phone: string): Promise; -}; - -const STORAGE_KEY = 'rcOmnichannelContacts'; - -const createContact = (phone: string, data: Pick | null): Contact => ({ - phone, - name: data?.name || '', -}); - -const storeInCache = (contacts: Record): void => { - window.localStorage.setItem(STORAGE_KEY, JSON.stringify(contacts)); -}; - -const retrieveFromCache = (): Record => { - const cache = window.localStorage.getItem(STORAGE_KEY); - - try { - return cache ? JSON.parse(cache) : {}; - } catch (_) { - return {}; - } -}; - -export const ContactsContext = createContext({ - getContactByPhone: (_: string) => Promise.reject(), -}); - -export const OmnichannelContactsProvider = ({ children }: ContactsProviderProps): ReactElement => { - const getContactBy = useEndpoint('GET', '/v1/omnichannel/contact.search'); - const contacts = useRef>({}); - const pendingRequests = useRef>>(new Map()); - - useEffect(() => { - contacts.current = retrieveFromCache(); - - const handleVisibilityChange = (): void => storeInCache(contacts.current); - document.addEventListener('visibilitychange', handleVisibilityChange); - - return () => { - document.removeEventListener('visibilitychange', handleVisibilityChange); - }; - }, []); - - const getContactByPhoneFromCache = useCallback((phone: string): Contact | null => contacts.current[phone] || null, []); - - const addContactToCache = useCallback((contact: Contact): void => { - contacts.current[contact.phone] = contact; - }, []); - - const fetchContactByPhone = useCallback((phone: string): ReturnType => getContactBy({ phone }), [getContactBy]); - - const getContactByPhone = useCallback( - (phone: string): Promise => { - const cache = getContactByPhoneFromCache(phone); - const cachedRequest = pendingRequests.current.get(phone); - - if (cache) { - return Promise.resolve(cache); - } - - if (cachedRequest) { - return cachedRequest; - } - - const request = fetchContactByPhone(phone) - .then((data) => { - const contact = createContact(phone, data.contact); - addContactToCache(contact); - - return contact; - }) - .finally(() => { - pendingRequests.current.delete(phone); - }); - - pendingRequests.current.set(phone, request); - - return request; - }, - [addContactToCache, fetchContactByPhone, getContactByPhoneFromCache], - ); - - const contextValue = useMemo( - () => ({ - getContactByPhone, - }), - [getContactByPhone], - ); - - return {children}; -}; From d3f68de941f335bdf5e5ac53262410c366087cf2 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Sun, 14 Aug 2022 21:39:37 -0300 Subject: [PATCH 15/37] Chore: Added page objects for dialpad modal, voip footer and omnichannel section --- .../client/sidebar/footer/voip/VoipFooter.tsx | 8 ++++--- .../sidebar/sections/OmnichannelSection.tsx | 2 +- .../actions/OmnichannelCallDialPad.tsx | 1 + .../actions/OmnichannelCallToggleError.tsx | 2 +- .../actions/OmnichannelCallToggleLoading.tsx | 11 +++++++++- .../actions/OmnichannelCallToggleReady.tsx | 11 +++++++++- .../DeviceManagementFeatureModal.tsx | 2 +- .../voip/modal/DialPad/DialPadModal.tsx | 3 ++- apps/meteor/tests/e2e/page-objects/index.ts | 3 +++ .../page-objects/omnichannel-dialpad-modal.ts | 21 +++++++++++++++++++ .../e2e/page-objects/omnichannel-section.ts | 21 +++++++++++++++++++ .../page-objects/omnichannel-voip-footer.ts | 21 +++++++++++++++++++ 12 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-dialpad-modal.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-section.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-voip-footer.ts diff --git a/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx b/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx index 49c818cdeec0..0926c62fa1d4 100644 --- a/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx +++ b/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx @@ -78,6 +78,7 @@ export const VoipFooter = ({ { if (callerState === 'IN_CALL' || callerState === 'ON_HOLD') { openRoom(openedRoomInfo.rid); @@ -118,8 +119,8 @@ export const VoipFooter = ({ - - {caller.callerName || contactName || anonymousText} + + {contactName || caller.callerName || anonymousText} {subtitle} @@ -134,6 +135,7 @@ export const VoipFooter = ({ small square danger + data-qa-id='omncVoipRejectButton' onClick={(e): unknown => { e.stopPropagation(); muted && toggleMic(false); @@ -145,7 +147,7 @@ export const VoipFooter = ({ )} {callerState === 'OFFER_RECEIVED' && ( - )} diff --git a/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx b/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx index c0ab4eacb520..f9b363ee10ff 100644 --- a/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx +++ b/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx @@ -51,7 +51,7 @@ const OmnichannelSection = (props: typeof Box): ReactElement => { // The className is a paliative while we make TopBar.ToolBox optional on fuselage return ( - + {t('Omnichannel')} {showOmnichannelQueueLink && handleRoute('queue')} />} diff --git a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallDialPad.tsx b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallDialPad.tsx index 29bbd30eed1d..d0750dc3c519 100644 --- a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallDialPad.tsx +++ b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallDialPad.tsx @@ -18,6 +18,7 @@ export const OmniChannelCallDialPad = ({ ...props }): ReactElement => { icon='dialpad' onClick={(): void => openDialModal()} disabled={!outBoundCallsEnabledForUser} + data-qa-id='omncDialPadButton' {...props} /> ); diff --git a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleError.tsx b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleError.tsx index 0d5d7817f9c4..62c817ee8aea 100644 --- a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleError.tsx +++ b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleError.tsx @@ -4,5 +4,5 @@ import React, { ReactElement } from 'react'; export const OmnichannelCallToggleError = ({ ...props }): ReactElement => { const t = useTranslation(); - return ; + return ; }; diff --git a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleLoading.tsx b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleLoading.tsx index 83e9d26a2e57..695ca7b3166e 100644 --- a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleLoading.tsx +++ b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleLoading.tsx @@ -4,5 +4,14 @@ import React, { ReactElement } from 'react'; export const OmnichannelCallToggleLoading = ({ ...props }): ReactElement => { const t = useTranslation(); - return ; + return ( + + ); }; diff --git a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleReady.tsx b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleReady.tsx index 661413728890..0252fdb7e062 100644 --- a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleReady.tsx +++ b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleReady.tsx @@ -59,5 +59,14 @@ export const OmnichannelCallToggleReady = ({ ...props }): ReactElement => { color: getColor(), }; - return ; + return ( + + ); }; diff --git a/apps/meteor/ee/client/deviceManagement/components/featureModal/DeviceManagementFeatureModal.tsx b/apps/meteor/ee/client/deviceManagement/components/featureModal/DeviceManagementFeatureModal.tsx index 21a616ef3425..afbfb473c329 100644 --- a/apps/meteor/ee/client/deviceManagement/components/featureModal/DeviceManagementFeatureModal.tsx +++ b/apps/meteor/ee/client/deviceManagement/components/featureModal/DeviceManagementFeatureModal.tsx @@ -46,7 +46,7 @@ const DeviceManagementFeatureModal = ({ close }: { close: () => void }): ReactEl }; return ( - + {t('Workspace_now_using_device_management')} diff --git a/apps/meteor/ee/client/voip/modal/DialPad/DialPadModal.tsx b/apps/meteor/ee/client/voip/modal/DialPad/DialPadModal.tsx index 501f3ff9b975..720388c0346d 100644 --- a/apps/meteor/ee/client/voip/modal/DialPad/DialPadModal.tsx +++ b/apps/meteor/ee/client/voip/modal/DialPad/DialPadModal.tsx @@ -28,7 +28,7 @@ const DialPadModal = ({ initialValue, errorMessage, handleClose }: DialPadModalP useEnterKey(handleCallButtonClick, isButtonDisabled); return ( - + @@ -57,6 +57,7 @@ const DialPadModal = ({ initialValue, errorMessage, handleClose }: DialPadModalP secondary info size='64px' + data-qa-id='omncDialpadCallButton' onClick={(): void => { handleCallButtonClick(); handleClose(); diff --git a/apps/meteor/tests/e2e/page-objects/index.ts b/apps/meteor/tests/e2e/page-objects/index.ts index dec118470628..31d5ca4cf044 100644 --- a/apps/meteor/tests/e2e/page-objects/index.ts +++ b/apps/meteor/tests/e2e/page-objects/index.ts @@ -6,3 +6,6 @@ export * from './home-discussion'; export * from './omnichannel-agents'; export * from './omnichannel-departaments'; export * from './omnichannel-livechat'; +export * from './omnichannel-section'; +export * from './omnichannel-voip-footer'; +export * from './omnichannel-dialpad-modal'; diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-dialpad-modal.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-dialpad-modal.ts new file mode 100644 index 000000000000..b98ab2fd4d3c --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-dialpad-modal.ts @@ -0,0 +1,21 @@ +import type { Locator, Page } from '@playwright/test'; + +export class OmnichannelDialpadModal { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + get element(): Locator { + return this.page.locator('dialog[data-qa-id="omncDialpadModal"]'); + } + + get inputPhoneNumber(): Locator { + return this.page.locator('input[type="text"]'); + } + + get btnCall(): Locator { + return this.page.locator('button[data-qa-id="omncDialpadCallButton"]'); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-section.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-section.ts new file mode 100644 index 000000000000..997163f32bbe --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-section.ts @@ -0,0 +1,21 @@ +import type { Locator, Page } from '@playwright/test'; + +export class OmnichannelSection { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + get element(): Locator { + return this.page.locator('div[data-qa-id="omncSection"]'); + } + + get btnVoipToggle(): Locator { + return this.page.locator('button[data-qa-id="omncVoipToggleButton"]'); + } + + get btnDialpad(): Locator { + return this.page.locator('button[data-qa-id="omncDialPadButton"]'); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-voip-footer.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-voip-footer.ts new file mode 100644 index 000000000000..489d3323d2c8 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-voip-footer.ts @@ -0,0 +1,21 @@ +import type { Locator, Page } from '@playwright/test'; + +export class OmnichannelVoipFooter { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + get element(): Locator { + return this.page.locator('[data-qa-id="omncVoipFooter"]'); + } + + get btnReject(): Locator { + return this.page.locator('[data-qa-id="omncVoipRejectButton"]'); + } + + get textTitle(): Locator { + return this.page.locator('[data-qa-id="omncVoipTitle"]'); + } +} From 2711521b9ad2c90d80ace5d084bc25b02bdfa815 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Sun, 14 Aug 2022 21:40:26 -0300 Subject: [PATCH 16/37] [FIX] Fixed voip enabled initial value on useVoipClient --- apps/meteor/ee/client/hooks/useVoipClient.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/meteor/ee/client/hooks/useVoipClient.ts b/apps/meteor/ee/client/hooks/useVoipClient.ts index c9001f3646b3..4512e329f043 100644 --- a/apps/meteor/ee/client/hooks/useVoipClient.ts +++ b/apps/meteor/ee/client/hooks/useVoipClient.ts @@ -22,7 +22,8 @@ const isSignedResponse = (data: any): data is { result: string } => typeof data? // Currently we only support the websocket connection and the SIP proxy connection being from the same host, // we need to add a new setting for SIP proxy if we want to support different hosts for them. export const useVoipClient = (): UseVoipClientResult => { - const [voipEnabled, setVoipEnabled] = useSafely(useState(useSetting('VoIP_Enabled'))); + const settingVoipEnabled = useSetting('VoIP_Enabled'); + const [voipEnabled, setVoipEnabled] = useSafely(useState(settingVoipEnabled)); const voipRetryCount = useSetting('VoIP_Retry_Count'); const enableKeepAlive = useSetting('VoIP_Enable_Keep_Alive_For_Unstable_Networks'); const registrationInfo = useEndpoint('GET', '/v1/connector.extension.getRegistrationInfoByUserId'); @@ -34,6 +35,10 @@ export const useVoipClient = (): UseVoipClientResult => { const isEE = useHasLicenseModule('voip-enterprise'); + useEffect(() => { + setVoipEnabled(settingVoipEnabled); + }, [settingVoipEnabled, setVoipEnabled]); + useEffect(() => { const voipEnableEventHandler = (enabled: boolean): void => { setVoipEnabled(enabled); From 8f2b9ac75a4814b2b699e7deb6286171099e04b4 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Sun, 14 Aug 2022 21:41:07 -0300 Subject: [PATCH 17/37] Chore: Implemented e2e tests for the VoipFooter component --- apps/meteor/playwright.config.ts | 5 +- .../tests/e2e/omnichannel-voip-footer.spec.ts | 172 ++++++++++++++++++ 2 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts diff --git a/apps/meteor/playwright.config.ts b/apps/meteor/playwright.config.ts index 6df6dbbf187b..98e3fa4124aa 100644 --- a/apps/meteor/playwright.config.ts +++ b/apps/meteor/playwright.config.ts @@ -3,7 +3,7 @@ import type { PlaywrightTestConfig } from '@playwright/test'; import * as constants from './tests/e2e/config/constants'; export default { - globalSetup: require.resolve('./tests/e2e/config/global-setup.ts'), + // globalSetup: require.resolve('./tests/e2e/config/global-setup.ts'), use: { headless: true, ignoreHTTPSErrors: true, @@ -14,8 +14,9 @@ export default { launchOptions: { // force GPU hardware acceleration // (even in headless mode) - args: ['--use-gl=egl'], + args: ['--use-gl=egl', '--use-fake-ui-for-media-stream'], }, + permissions: ['microphone'], }, outputDir: 'tests/e2e/.playwright', reporter: process.env.CI ? 'github' : 'list', diff --git a/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts b/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts new file mode 100644 index 000000000000..6898244af54e --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts @@ -0,0 +1,172 @@ +import type { Page } from '@playwright/test'; + +import { test, expect } from './utils/test'; +// import type { OmnichannelCallCenter } from './page-objects'; +import { OmnichannelDialpadModal, OmnichannelVoipFooter, OmnichannelSection } from './page-objects'; +import { createAuxContext } from './utils'; +import { createToken } from '../../client/lib/utils/createToken'; + +type PageObjects = { + page: Page; + omncSection: OmnichannelSection; + dialpadModal: OmnichannelDialpadModal; + voipFooter: OmnichannelVoipFooter; +}; + +const createPageObjects = (page: Page) => ({ + page, + omncSection: new OmnichannelSection(page), + dialpadModal: new OmnichannelDialpadModal(page), + voipFooter: new OmnichannelVoipFooter(page), +}); + +test.use({ storageState: 'admin-session.json' }); + +test.describe('Omnichannel VoIP Footer', () => { + let admin: PageObjects; + let user: PageObjects; + + test.beforeAll(async ({ api }) => { + // Enable Omnichannel + await api.post('/method.call/saveSettings', { + message: JSON.stringify({ + msg: 'method', + id: '67', + method: 'saveSettings', + params: [ + [{ _id: 'Livechat_enabled', value: true }], + { + twoFactorCode: 'b6769a5ae0a6071ecabbe868dbdfa925f856c2bb3d910f93cb39479c64ca221e', + twoFactorMethod: 'password', + }, + ], + }), + }); + + // Configure VoIP + await api.post('/method.call/saveSettings', { + message: JSON.stringify({ + msg: 'method', + id: '84', + method: 'saveSettings', + params: [ + [ + { _id: 'VoIP_Enabled', value: true }, + { _id: 'VoIP_Management_Server_Host', value: 'omni-asterisk.dev.rocket.chat' }, + { _id: 'VoIP_Management_Server_Port', value: 5038 }, + { _id: 'VoIP_Management_Server_Name', value: 'OminiAsterisk' }, + { _id: 'VoIP_Management_Server_Username', value: 'sales.rocket.chat' }, + { _id: 'VoIP_Management_Server_Password', value: 'rocket@123' }, + { _id: 'VoIP_Server_Name', value: 'OmniAsterisk' }, + { _id: 'VoIP_Server_Websocket_Path', value: 'wss://omni-asterisk.dev.rocket.chat/ws' }, + ], + { + twoFactorCode: 'b6769a5ae0a6071ecabbe868dbdfa925f856c2bb3d910f93cb39479c64ca221e', + twoFactorMethod: 'password', + }, + ], + }), + }); + + // Add agent + await Promise.all([ + api.post('/livechat/users/agent', { username: 'rocketchat.internal.admin.test' }), + api.post('/livechat/users/agent', { username: 'user1' }), + ]); + + // Add agent to extension and as a contact + await Promise.all([ + api.post('/omnichannel/agent/extension', { username: 'rocketchat.internal.admin.test', extension: '80018' }), + api.post('/omnichannel/agent/extension', { username: 'user1', extension: '80017' }), + api.post('/omnichannel/contact', { + name: 'Test User One', + phone: '80017', + email: '', + customFields: {}, + token: createToken(), + }), + ]); + }); + + test.beforeAll(async ({ browser }) => { + const { page } = await createAuxContext(browser, 'user1-session.json'); + user = createPageObjects(page); + + await expect(user.omncSection.element).toBeVisible({ timeout: 10000 }); + + if (await page.isVisible('[data-qa-id="deviceManagementFeatureModal"]')) { + await page.locator('[data-qa-id="deviceManagementFeatureModal"] button >> text="Got it"').click(); + } + + await expect(user.omncSection.btnVoipToggle).toBeEnabled(); + await page.waitForTimeout(2000); + await user.omncSection.btnVoipToggle.click(); + await expect(user.omncSection.btnVoipToggle).toHaveAttribute('data-qa-type', 'enabled'); + }); + + test.beforeEach(async ({ page }) => { + admin = createPageObjects(page); + await page.goto('/home'); + }); + + test.beforeEach(async () => { + const { page, omncSection } = admin; + + // Enable voip + await expect(omncSection.element).toBeVisible({ timeout: 10000 }); + + // Close feature modal + if (await page.isVisible('[data-qa-id="deviceManagementFeatureModal"]')) { + await page.locator('[data-qa-id="deviceManagementFeatureModal"] button >> text="Got it"').click(); + } + + await expect(omncSection.btnVoipToggle).toBeEnabled(); + await page.waitForTimeout(2000); + await admin.omncSection.btnVoipToggle.click(); + await expect(omncSection.btnVoipToggle).toHaveAttribute('data-qa-type', 'enabled'); + }); + + test('expect voip footer to identify known contact', async () => { + const { page, omncSection, dialpadModal, voipFooter } = admin; + + // Open dialpad modal + await expect(omncSection.btnDialpad).toBeEnabled(); + await omncSection.btnDialpad.click(); + + // Dial number and call + await expect(dialpadModal.element).toBeVisible(); + await dialpadModal.inputPhoneNumber.type('80017'); + await expect(dialpadModal.btnCall).toBeEnabled(); + await dialpadModal.btnCall.click(); + await page.pause(); + + // Check if contact name is there + await expect(voipFooter.element).toBeVisible({ timeout: 10000 }); + await expect(voipFooter.textTitle).toHaveText('Test User One'); + + // Reject the call + await voipFooter.btnReject.click(); + }); + + test('expect voip footer to fallback to phone number for unknown contact', async () => { + const { omncSection, dialpadModal, voipFooter } = user; + + // Open dialpad modal + await expect(omncSection.btnDialpad).toBeEnabled(); + await omncSection.btnDialpad.click(); + + // Dial number and call + await expect(dialpadModal.element).toBeVisible(); + await dialpadModal.inputPhoneNumber.type('80018'); + await expect(dialpadModal.btnCall).toBeEnabled(); + await dialpadModal.btnCall.click(); + + // Check if contact name is there + await voipFooter.element.waitFor(); + await expect(voipFooter.element).toBeVisible({ timeout: 10000 }); + await expect(voipFooter.textTitle).toHaveText('80018'); + + // Reject the call + await voipFooter.btnReject.click(); + }); +}); From 085fe47b5aea21f1038e896c336ef4e5aafef674 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Mon, 15 Aug 2022 10:22:19 -0300 Subject: [PATCH 18/37] Chore: Uncommenting playwright global setupo --- apps/meteor/playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/playwright.config.ts b/apps/meteor/playwright.config.ts index 98e3fa4124aa..e2a44055ce94 100644 --- a/apps/meteor/playwright.config.ts +++ b/apps/meteor/playwright.config.ts @@ -3,7 +3,7 @@ import type { PlaywrightTestConfig } from '@playwright/test'; import * as constants from './tests/e2e/config/constants'; export default { - // globalSetup: require.resolve('./tests/e2e/config/global-setup.ts'), + globalSetup: require.resolve('./tests/e2e/config/global-setup.ts'), use: { headless: true, ignoreHTTPSErrors: true, From 5f7e86031d0d87fc512f61350b19f020414148f3 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Mon, 15 Aug 2022 13:01:03 -0300 Subject: [PATCH 19/37] Chore: Skip e2e for community edition --- apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts b/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts index 6898244af54e..8846ab93be88 100644 --- a/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts @@ -1,10 +1,10 @@ import type { Page } from '@playwright/test'; import { test, expect } from './utils/test'; -// import type { OmnichannelCallCenter } from './page-objects'; import { OmnichannelDialpadModal, OmnichannelVoipFooter, OmnichannelSection } from './page-objects'; import { createAuxContext } from './utils'; import { createToken } from '../../client/lib/utils/createToken'; +import { IS_EE } from './config/constants'; type PageObjects = { page: Page; @@ -20,6 +20,8 @@ const createPageObjects = (page: Page) => ({ voipFooter: new OmnichannelVoipFooter(page), }); +test.skip(!IS_EE, 'Omnichannel Voip Footer > Enterprise Only'); + test.use({ storageState: 'admin-session.json' }); test.describe('Omnichannel VoIP Footer', () => { From e0a929dd8c60e0c8be1e8998a42d67b7657ea81f Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 15 Aug 2022 14:09:47 -0600 Subject: [PATCH 20/37] Support receiving duplicated dialend events --- .../asterisk/ami/ContinuousMonitor.ts | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/apps/meteor/server/services/voip/connector/asterisk/ami/ContinuousMonitor.ts b/apps/meteor/server/services/voip/connector/asterisk/ami/ContinuousMonitor.ts index 6a89b4ded0bc..91345e0935dd 100644 --- a/apps/meteor/server/services/voip/connector/asterisk/ami/ContinuousMonitor.ts +++ b/apps/meteor/server/services/voip/connector/asterisk/ami/ContinuousMonitor.ts @@ -278,15 +278,23 @@ export class ContinuousMonitor extends Command { * event?.connectedlinenum is the extension/phone number that is being called * and event.calleridnum is the extension that is initiating a call. */ - await PbxEvents.insertOne({ - uniqueId: `${event.event}-${event.calleridnum}-${event.channel}-${event.destchannel}-${event.uniqueid}`, - event: event.event, - ts: new Date(), - phone: event?.connectedlinenum, - callUniqueId: event.uniqueid, - callUniqueIdFallback: event.linkedid, - agentExtension: event.calleridnum, - }); + try { + await PbxEvents.insertOne({ + uniqueId: `${event.event}-${event.calleridnum}-${event.channel}-${event.destchannel}-${event.uniqueid}`, + event: event.event, + ts: new Date(), + phone: event?.connectedlinenum, + callUniqueId: event.uniqueid, + callUniqueIdFallback: event.linkedid, + agentExtension: event.calleridnum, + }); + } catch (e) { + // This could mean we received a duplicate event + // This is quite common since DialEnd event happens "multiple times" at the end of the call + // We receive one for DialEnd in progress and one for DialEnd finished. + this.logger.warn(`Duplicate event ${event.event} received for ${event.uniqueid}`); + this.logger.debug(event); + } } async onEvent(event: IEventBase): Promise { From ddbb78dc7344382409ea3279118a6c1c4f539cb7 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Tue, 16 Aug 2022 14:46:06 -0300 Subject: [PATCH 21/37] Chore: Skipping e2e test until CI license issue is fixed --- apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts b/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts index 8846ab93be88..6503fd792826 100644 --- a/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts @@ -4,7 +4,7 @@ import { test, expect } from './utils/test'; import { OmnichannelDialpadModal, OmnichannelVoipFooter, OmnichannelSection } from './page-objects'; import { createAuxContext } from './utils'; import { createToken } from '../../client/lib/utils/createToken'; -import { IS_EE } from './config/constants'; +// import { IS_EE } from './config/constants'; type PageObjects = { page: Page; @@ -20,7 +20,7 @@ const createPageObjects = (page: Page) => ({ voipFooter: new OmnichannelVoipFooter(page), }); -test.skip(!IS_EE, 'Omnichannel Voip Footer > Enterprise Only'); +test.skip(true /* IS_EE */, 'Omnichannel Voip Footer > Enterprise Only'); test.use({ storageState: 'admin-session.json' }); From 72ae189ffbdc3261ce548ba86844b2d818cf590b Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Thu, 18 Aug 2022 15:46:14 -0300 Subject: [PATCH 22/37] [FIX] Removing asterisk from visitor's numbers --- apps/meteor/client/providers/CallProvider/CallProvider.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/providers/CallProvider/CallProvider.tsx b/apps/meteor/client/providers/CallProvider/CallProvider.tsx index a152c484eb82..a92624f3b7b2 100644 --- a/apps/meteor/client/providers/CallProvider/CallProvider.tsx +++ b/apps/meteor/client/providers/CallProvider/CallProvider.tsx @@ -36,6 +36,7 @@ import { OutgoingByeRequest } from 'sip.js/lib/core'; import { CustomSounds } from '../../../app/custom-sounds/client'; import { getUserPreference } from '../../../app/utils/client'; import { isOutboundClient, useVoipClient } from '../../../ee/client/hooks/useVoipClient'; +import { parseOutboundPhoneNumber } from '../../../ee/client/lib/voip/parseOutboundPhoneNumber'; import { WrapUpCallModal } from '../../../ee/client/voip/components/modals/WrapUpCallModal'; import { CallContext, CallContextValue, useIsVoipEnterprise } from '../../contexts/CallContext'; import { useDialModal } from '../../hooks/useDialModal'; @@ -166,11 +167,12 @@ export const CallProvider: FC = ({ children }) => { return ''; } try { + const phone = parseOutboundPhoneNumber(caller.callerId); const { visitor } = await visitorEndpoint({ visitor: { token: Random.id(), - phone: caller.callerId, - name: caller.callerName || caller.callerId, + phone, + name: caller.callerName || phone, }, }); const voipRoom = await voipEndpoint({ token: visitor.token, agentId: user._id, direction }); From 2d4fdfa95592994abde32d4235914d0276de3c49 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Thu, 18 Aug 2022 15:46:21 -0300 Subject: [PATCH 23/37] Chore: Removing cache and simplifying hook --- .../hooks/omnichannel/useContactName.ts | 18 ----- .../omnichannel/useOmnichannelContact.ts | 35 +++++++++ .../omnichannel/useOmnichannelContacts.ts | 72 ------------------- .../client/sidebar/footer/voip/VoipFooter.tsx | 6 +- 4 files changed, 38 insertions(+), 93 deletions(-) delete mode 100644 apps/meteor/client/hooks/omnichannel/useContactName.ts create mode 100644 apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts delete mode 100644 apps/meteor/client/hooks/omnichannel/useOmnichannelContacts.ts diff --git a/apps/meteor/client/hooks/omnichannel/useContactName.ts b/apps/meteor/client/hooks/omnichannel/useContactName.ts deleted file mode 100644 index 7a91c790cc38..000000000000 --- a/apps/meteor/client/hooks/omnichannel/useContactName.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { parseOutboundPhoneNumber } from '../../../ee/client/lib/voip/parseOutboundPhoneNumber'; -import { useOmnichannelContacts } from './useOmnichannelContacts'; - -export const useContactName = (phone: string): string => { - const safePhone = parseOutboundPhoneNumber(phone); - const { getContactByPhone } = useOmnichannelContacts(); - const [name, setName] = useState(safePhone); - - useEffect(() => { - getContactByPhone(safePhone).then((contact) => { - setName(contact.name || contact.phone); - }); - }, [safePhone, getContactByPhone]); - - return name; -}; diff --git a/apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts b/apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts new file mode 100644 index 000000000000..d787c68221e2 --- /dev/null +++ b/apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts @@ -0,0 +1,35 @@ +import { ILivechatVisitor } from '@rocket.chat/core-typings'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useCallback, useEffect, useState } from 'react'; + +import { parseOutboundPhoneNumber } from '../../../ee/client/lib/voip/parseOutboundPhoneNumber'; + +type Contact = { + name: string; + phone: string; +}; + +const createContact = (phone: string, data: Pick | null): Contact => ({ + phone, + name: data?.name || '', +}); + +export const useOmnichannelContact = (phone: string): Contact => { + const getContactBy = useEndpoint('GET', '/v1/omnichannel/contact.search'); + const safePhone = parseOutboundPhoneNumber(phone); + const [contact, setContact] = useState({ phone: safePhone, name: safePhone }); + + const getContactByPhone = useCallback( + async (phone: string): Promise => { + const data = await getContactBy({ phone }); + return createContact(phone, data.contact); + }, + [getContactBy], + ); + + useEffect(() => { + getContactByPhone(safePhone).then(setContact); + }, [safePhone, getContactByPhone]); + + return contact; +}; diff --git a/apps/meteor/client/hooks/omnichannel/useOmnichannelContacts.ts b/apps/meteor/client/hooks/omnichannel/useOmnichannelContacts.ts deleted file mode 100644 index c7c8ac41629e..000000000000 --- a/apps/meteor/client/hooks/omnichannel/useOmnichannelContacts.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { ILivechatVisitor } from '@rocket.chat/core-typings'; -import { useEndpoint } from '@rocket.chat/ui-contexts'; -import { useCallback, useEffect, useRef } from 'react'; - -type Contact = { - name: string; - phone: string; -}; - -export type ContactsHookValue = { - getContactByPhone(phone: string): Promise; -}; - -const STORAGE_KEY = 'rcOmnichannelContacts'; - -const createContact = (phone: string, data: Pick | null): Contact => ({ - phone, - name: data?.name || '', -}); - -const storeInCache = (contacts: Record): void => { - window.localStorage.setItem(STORAGE_KEY, JSON.stringify(contacts)); -}; - -const retrieveFromCache = (): Record => { - const cache = window.localStorage.getItem(STORAGE_KEY); - - try { - return cache ? JSON.parse(cache) : {}; - } catch (_) { - return {}; - } -}; - -export const useOmnichannelContacts = (): ContactsHookValue => { - const getContactBy = useEndpoint('GET', '/v1/omnichannel/contact.search'); - const contacts = useRef>({}); - - useEffect(() => { - contacts.current = retrieveFromCache(); - return () => storeInCache(contacts.current); - }, []); - - const getContactByPhoneFromCache = useCallback((phone: string): Contact | null => contacts.current[phone] || null, []); - - const addContactToCache = useCallback((contact: Contact): void => { - contacts.current[contact.phone] = contact; - }, []); - - const fetchContactByPhone = useCallback((phone: string): ReturnType => getContactBy({ phone }), [getContactBy]); - - const getContactByPhone = useCallback( - async (phone: string): Promise => { - const cache = getContactByPhoneFromCache(phone); - - if (cache) { - return Promise.resolve(cache); - } - - const data = await fetchContactByPhone(phone); - const contact = createContact(phone, data.contact); - addContactToCache(contact); - - return contact; - }, - [addContactToCache, fetchContactByPhone, getContactByPhoneFromCache], - ); - - return { - getContactByPhone, - }; -}; diff --git a/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx b/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx index 0926c62fa1d4..b52f128dc840 100644 --- a/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx +++ b/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx @@ -6,7 +6,7 @@ import React, { ReactElement, MouseEvent, ReactNode } from 'react'; import type { VoipFooterMenuOptions } from '../../../../ee/client/hooks/useVoipFooterMenu'; import { CallActionsType } from '../../../contexts/CallContext'; -import { useContactName } from '../../../hooks/omnichannel/useContactName'; +import { useOmnichannelContact } from '../../../hooks/omnichannel/useOmnichannelContact'; type VoipFooterPropsType = { caller: ICallerInfo; @@ -58,7 +58,7 @@ export const VoipFooter = ({ children, options, }: VoipFooterPropsType): ReactElement => { - const contactName = useContactName(caller.callerId); + const contact = useOmnichannelContact(caller.callerId); const cssClickable = callerState === 'IN_CALL' || callerState === 'ON_HOLD' @@ -120,7 +120,7 @@ export const VoipFooter = ({ - {contactName || caller.callerName || anonymousText} + {contact.name || caller.callerName || contact.phone || anonymousText} {subtitle} From 6ecba02ad75e9534b6498a26c06b79c02bc4dfff Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Tue, 23 Aug 2022 18:59:03 -0300 Subject: [PATCH 24/37] Chore: Refactored useOmnichannelContact to use useQuery --- .../omnichannel/useOmnichannelContact.ts | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts b/apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts index d787c68221e2..8afae0cef131 100644 --- a/apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts +++ b/apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts @@ -1,6 +1,7 @@ import { ILivechatVisitor } from '@rocket.chat/core-typings'; import { useEndpoint } from '@rocket.chat/ui-contexts'; -import { useCallback, useEffect, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { useState } from 'react'; import { parseOutboundPhoneNumber } from '../../../ee/client/lib/voip/parseOutboundPhoneNumber'; @@ -14,22 +15,19 @@ const createContact = (phone: string, data: Pick | nul name: data?.name || '', }); -export const useOmnichannelContact = (phone: string): Contact => { +export const useOmnichannelContact = (ogPhone: string, name = ''): Contact => { const getContactBy = useEndpoint('GET', '/v1/omnichannel/contact.search'); - const safePhone = parseOutboundPhoneNumber(phone); - const [contact, setContact] = useState({ phone: safePhone, name: safePhone }); + const phone = parseOutboundPhoneNumber(ogPhone); + const [defaultContact] = useState({ phone, name }); - const getContactByPhone = useCallback( - async (phone: string): Promise => { - const data = await getContactBy({ phone }); - return createContact(phone, data.contact); - }, - [getContactBy], - ); + const { + data: contact, + isLoading, + isError, + } = useQuery(['getContactsByPhone', phone], async (): Promise => { + const { contact } = await getContactBy({ phone }); + return createContact(phone, contact); + }); - useEffect(() => { - getContactByPhone(safePhone).then(setContact); - }, [safePhone, getContactByPhone]); - - return contact; + return isLoading || isError ? defaultContact : contact; }; From 03f68ab8cfa74581a12f12c902d2989f3f7b4df1 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Tue, 23 Aug 2022 19:00:01 -0300 Subject: [PATCH 25/37] Chore: Removed duplicated qa ids and refactored to use roles --- .../client/sidebar/footer/voip/VoipFooter.tsx | 6 +- .../actions/OmnichannelCallDialPad.tsx | 2 +- .../actions/OmnichannelCallToggleLoading.tsx | 11 +-- .../actions/OmnichannelCallToggleReady.tsx | 4 +- .../rocketchat-i18n/i18n/en.i18n.json | 3 + .../rocketchat-i18n/i18n/pt-BR.i18n.json | 5 +- .../tests/e2e/omnichannel-voip-footer.spec.ts | 82 +++++++++++-------- .../e2e/page-objects/omnichannel-section.ts | 4 +- .../page-objects/omnichannel-voip-footer.ts | 6 +- 9 files changed, 69 insertions(+), 54 deletions(-) diff --git a/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx b/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx index b52f128dc840..a0ac7e6077f5 100644 --- a/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx +++ b/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx @@ -2,6 +2,7 @@ import type { IVoipRoom } from '@rocket.chat/core-typings'; import { ICallerInfo, VoIpCallerInfo, VoipClientEvents } from '@rocket.chat/core-typings'; import { css } from '@rocket.chat/css-in-js'; import { Box, Button, ButtonGroup, Icon, SidebarFooter, Menu, IconButton } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { ReactElement, MouseEvent, ReactNode } from 'react'; import type { VoipFooterMenuOptions } from '../../../../ee/client/hooks/useVoipFooterMenu'; @@ -59,6 +60,7 @@ export const VoipFooter = ({ options, }: VoipFooterPropsType): ReactElement => { const contact = useOmnichannelContact(caller.callerId); + const t = useTranslation(); const cssClickable = callerState === 'IN_CALL' || callerState === 'ON_HOLD' @@ -135,7 +137,7 @@ export const VoipFooter = ({ small square danger - data-qa-id='omncVoipRejectButton' + aria-label={t('End_call')} onClick={(e): unknown => { e.stopPropagation(); muted && toggleMic(false); @@ -147,7 +149,7 @@ export const VoipFooter = ({ )} {callerState === 'OFFER_RECEIVED' && ( - )} diff --git a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallDialPad.tsx b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallDialPad.tsx index d0750dc3c519..d6146db21a83 100644 --- a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallDialPad.tsx +++ b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallDialPad.tsx @@ -18,7 +18,7 @@ export const OmniChannelCallDialPad = ({ ...props }): ReactElement => { icon='dialpad' onClick={(): void => openDialModal()} disabled={!outBoundCallsEnabledForUser} - data-qa-id='omncDialPadButton' + aria-label={t('Open_Dialpad')} {...props} /> ); diff --git a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleLoading.tsx b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleLoading.tsx index 695ca7b3166e..72242f3789f2 100644 --- a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleLoading.tsx +++ b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleLoading.tsx @@ -4,14 +4,5 @@ import React, { ReactElement } from 'react'; export const OmnichannelCallToggleLoading = ({ ...props }): ReactElement => { const t = useTranslation(); - return ( - - ); + return ; }; diff --git a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleReady.tsx b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleReady.tsx index 0252fdb7e062..7f285da22c5e 100644 --- a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleReady.tsx +++ b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleReady.tsx @@ -61,8 +61,8 @@ export const OmnichannelCallToggleReady = ({ ...props }): ReactElement => { return ( { await expect(user.omncSection.btnVoipToggle).toBeEnabled(); await page.waitForTimeout(2000); await user.omncSection.btnVoipToggle.click(); - await expect(user.omncSection.btnVoipToggle).toHaveAttribute('data-qa-type', 'enabled'); + await expect(user.omncSection.btnVoipToggle).toHaveAttribute('aria-checked', 'true'); + }); + + test.afterAll(async ({ api }) => { + // Remove users from extensions + await Promise.all([ + api.delete(`/omnichannel/agent/extension/user1`), + api.delete(`/omnichannel/agent/extension/rocketchat.internal.admin.test`), + ]); + + // Remove users from extensions + await Promise.all([api.delete('/livechat/users/agent/user1'), api.delete('/livechat/users/agent/rocketchat.internal.admin.test')]); }); test.beforeEach(async ({ page }) => { @@ -125,50 +136,51 @@ test.describe('Omnichannel VoIP Footer', () => { await expect(omncSection.btnVoipToggle).toBeEnabled(); await page.waitForTimeout(2000); await admin.omncSection.btnVoipToggle.click(); - await expect(omncSection.btnVoipToggle).toHaveAttribute('data-qa-type', 'enabled'); + await expect(omncSection.btnVoipToggle).toHaveAttribute('aria-checked', 'true'); }); - test('expect voip footer to identify known contact', async () => { - const { page, omncSection, dialpadModal, voipFooter } = admin; + test('VoIP call identification', async () => { + await test.step('expect voip footer to identify known contact', async () => { + const { omncSection, dialpadModal, voipFooter } = admin; - // Open dialpad modal - await expect(omncSection.btnDialpad).toBeEnabled(); - await omncSection.btnDialpad.click(); + // Open dialpad modal + await expect(omncSection.btnDialpad).toBeEnabled(); + await omncSection.btnDialpad.click(); - // Dial number and call - await expect(dialpadModal.element).toBeVisible(); - await dialpadModal.inputPhoneNumber.type('80017'); - await expect(dialpadModal.btnCall).toBeEnabled(); - await dialpadModal.btnCall.click(); - await page.pause(); + // Dial number and call + await expect(dialpadModal.element).toBeVisible(); + await dialpadModal.inputPhoneNumber.type('80017'); + await expect(dialpadModal.btnCall).toBeEnabled(); + await dialpadModal.btnCall.click(); - // Check if contact name is there - await expect(voipFooter.element).toBeVisible({ timeout: 10000 }); - await expect(voipFooter.textTitle).toHaveText('Test User One'); + // Check if contact name is there + await expect(voipFooter.element).toBeVisible({ timeout: 10000 }); + await expect(voipFooter.textTitle).toHaveText('Test User One'); - // Reject the call - await voipFooter.btnReject.click(); - }); + // Reject the call + await voipFooter.btnEndCall.click(); + }); - test('expect voip footer to fallback to phone number for unknown contact', async () => { - const { omncSection, dialpadModal, voipFooter } = user; + await test.step('expect voip footer to fallback to phone number for unknown contact', async () => { + const { omncSection, dialpadModal, voipFooter } = user; - // Open dialpad modal - await expect(omncSection.btnDialpad).toBeEnabled(); - await omncSection.btnDialpad.click(); + // Open dialpad modal + await expect(omncSection.btnDialpad).toBeEnabled(); + await omncSection.btnDialpad.click(); - // Dial number and call - await expect(dialpadModal.element).toBeVisible(); - await dialpadModal.inputPhoneNumber.type('80018'); - await expect(dialpadModal.btnCall).toBeEnabled(); - await dialpadModal.btnCall.click(); + // Dial number and call + await expect(dialpadModal.element).toBeVisible(); + await dialpadModal.inputPhoneNumber.type('80018'); + await expect(dialpadModal.btnCall).toBeEnabled(); + await dialpadModal.btnCall.click(); - // Check if contact name is there - await voipFooter.element.waitFor(); - await expect(voipFooter.element).toBeVisible({ timeout: 10000 }); - await expect(voipFooter.textTitle).toHaveText('80018'); + // Check if contact name is there + await voipFooter.element.waitFor(); + await expect(voipFooter.element).toBeVisible({ timeout: 10000 }); + await expect(voipFooter.textTitle).toHaveText('80018'); - // Reject the call - await voipFooter.btnReject.click(); + // Reject the call + await voipFooter.btnEndCall.click(); + }); }); }); diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-section.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-section.ts index 997163f32bbe..8d617e25712b 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-section.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-section.ts @@ -12,10 +12,10 @@ export class OmnichannelSection { } get btnVoipToggle(): Locator { - return this.page.locator('button[data-qa-id="omncVoipToggleButton"]'); + return this.page.locator('role=button[name="Enable/Disable VoIP"]'); } get btnDialpad(): Locator { - return this.page.locator('button[data-qa-id="omncDialPadButton"]'); + return this.page.locator('role=button[name="Open Dialpad"]'); } } diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-voip-footer.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-voip-footer.ts index 489d3323d2c8..a2febe220c1c 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-voip-footer.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-voip-footer.ts @@ -12,7 +12,11 @@ export class OmnichannelVoipFooter { } get btnReject(): Locator { - return this.page.locator('[data-qa-id="omncVoipRejectButton"]'); + return this.page.locator('role=button[name="Reject call"]'); + } + + get btnEndCall(): Locator { + return this.page.locator('role=button[name="End call"]'); } get textTitle(): Locator { From dd3b6b2520a8ef52a2a337aa1c5dba73659ac761 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Tue, 23 Aug 2022 19:37:57 -0300 Subject: [PATCH 26/37] Chore: Fixing typing --- apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts | 2 +- .../sidebar/sections/actions/OmnichannelCallToggleLoading.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts b/apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts index 8afae0cef131..3ed4c2e638db 100644 --- a/apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts +++ b/apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts @@ -29,5 +29,5 @@ export const useOmnichannelContact = (ogPhone: string, name = ''): Contact => { return createContact(phone, contact); }); - return isLoading || isError ? defaultContact : contact; + return isLoading || isError || !contact ? defaultContact : contact; }; diff --git a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleLoading.tsx b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleLoading.tsx index 72242f3789f2..89048d577638 100644 --- a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleLoading.tsx +++ b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleLoading.tsx @@ -4,5 +4,5 @@ import React, { ReactElement } from 'react'; export const OmnichannelCallToggleLoading = ({ ...props }): ReactElement => { const t = useTranslation(); - return ; + return ; }; From 3c04aa67cfb6e933b4e94f5defc11b360c7c99d9 Mon Sep 17 00:00:00 2001 From: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com> Date: Wed, 24 Aug 2022 19:39:39 +0530 Subject: [PATCH 27/37] Chore: Suggestions on PR #26334 (#26670) --- .github/workflows/build_and_test.yml | 2 + .../tests/e2e/omnichannel-voip-footer.spec.ts | 38 ++++++++----------- docker-compose-ci.yml | 1 + 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 50138d215a3c..6fd8d3150117 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -268,6 +268,7 @@ jobs: env: MONGO_URL: 'mongodb://host.docker.internal:27017/rocketchat?replicaSet=rs0&directConnection=true' MONGO_OPLOG_URL: 'mongodb://mongodb:27017/local?replicaSet=rs0&directConnection=true' + VOIP_MANAGEMENT_SERVER_PWD: ${{ secrets.VOIP_MANAGEMENT_SERVER_PWD }} run: | export LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") @@ -446,6 +447,7 @@ jobs: DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} TRANSPORTER: nats://nats:4222 ENTERPRISE_LICENSE: ${{ secrets.ENTERPRISE_LICENSE }} + VOIP_MANAGEMENT_SERVER_PWD: ${{ secrets.VOIP_MANAGEMENT_SERVER_PWD }} run: | export LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") diff --git a/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts b/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts index 9834687249f6..37d4f92bf637 100644 --- a/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts @@ -46,29 +46,21 @@ test.describe('Omnichannel VoIP Footer', () => { }); // Configure VoIP - await api.post('/method.call/saveSettings', { - message: JSON.stringify({ - msg: 'method', - id: '84', - method: 'saveSettings', - params: [ - [ - { _id: 'VoIP_Enabled', value: true }, - { _id: 'VoIP_Management_Server_Host', value: 'omni-asterisk.dev.rocket.chat' }, - { _id: 'VoIP_Management_Server_Port', value: 5038 }, - { _id: 'VoIP_Management_Server_Name', value: 'OminiAsterisk' }, - { _id: 'VoIP_Management_Server_Username', value: 'sales.rocket.chat' }, - { _id: 'VoIP_Management_Server_Password', value: 'rocket@123' }, - { _id: 'VoIP_Server_Name', value: 'OmniAsterisk' }, - { _id: 'VoIP_Server_Websocket_Path', value: 'wss://omni-asterisk.dev.rocket.chat/ws' }, - ], - { - twoFactorCode: 'b6769a5ae0a6071ecabbe868dbdfa925f856c2bb3d910f93cb39479c64ca221e', - twoFactorMethod: 'password', - }, - ], - }), - }); + const expectedSettings = { + VoIP_Enabled: true, + VoIP_Management_Server_Host: 'omni-asterisk.dev.rocket.chat', + VoIP_Management_Server_Port: 5038, + VoIP_Management_Server_Name: 'OminiAsterisk', + VoIP_Management_Server_Username: 'sales.rocket.chat', + VoIP_Server_Name: 'OmniAsterisk', + VoIP_Server_Websocket_Path: 'wss://omni-asterisk.dev.rocket.chat/ws', + }; + const promises = []; + for (const [key, value] of Object.entries(expectedSettings)) { + promises.push(api.post(`/settings/${key}`, { value })); + } + const allResults = await Promise.all(promises); + expect(allResults.every(({ status }) => status() === 200)).toBe(true); // Add agent await Promise.all([ diff --git a/docker-compose-ci.yml b/docker-compose-ci.yml index 32b33c3d0a4e..c3c859d7a433 100644 --- a/docker-compose-ci.yml +++ b/docker-compose-ci.yml @@ -14,6 +14,7 @@ services: - 'TRANSPORTER=${TRANSPORTER}' - MOLECULER_LOG_LEVEL=info - 'ROCKETCHAT_LICENSE=${ENTERPRISE_LICENSE}' + - 'OVERWRITE_SETTING_VoIP_Management_Server_Password=${VOIP_MANAGEMENT_SERVER_PWD}' extra_hosts: - 'host.docker.internal:host-gateway' depends_on: From cae6385cf5de614f06b0880c2897c49620b58e44 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Wed, 24 Aug 2022 11:12:03 -0300 Subject: [PATCH 28/37] Chore: Added isLoading and isError to useOmnichannelContact return value --- .../hooks/omnichannel/useOmnichannelContact.ts | 14 ++++++++++++-- .../client/sidebar/footer/voip/VoipFooter.tsx | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts b/apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts index 3ed4c2e638db..4eee21c519a2 100644 --- a/apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts +++ b/apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts @@ -10,12 +10,18 @@ type Contact = { phone: string; }; +type ContactQuery = { + isLoading: boolean; + isError: boolean; + contact: Contact; +}; + const createContact = (phone: string, data: Pick | null): Contact => ({ phone, name: data?.name || '', }); -export const useOmnichannelContact = (ogPhone: string, name = ''): Contact => { +export const useOmnichannelContact = (ogPhone: string, name = ''): ContactQuery => { const getContactBy = useEndpoint('GET', '/v1/omnichannel/contact.search'); const phone = parseOutboundPhoneNumber(ogPhone); const [defaultContact] = useState({ phone, name }); @@ -29,5 +35,9 @@ export const useOmnichannelContact = (ogPhone: string, name = ''): Contact => { return createContact(phone, contact); }); - return isLoading || isError || !contact ? defaultContact : contact; + return { + contact: contact || defaultContact, + isLoading, + isError, + }; }; diff --git a/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx b/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx index a0ac7e6077f5..042158bd7108 100644 --- a/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx +++ b/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx @@ -59,7 +59,7 @@ export const VoipFooter = ({ children, options, }: VoipFooterPropsType): ReactElement => { - const contact = useOmnichannelContact(caller.callerId); + const { contact } = useOmnichannelContact(caller.callerId); const t = useTranslation(); const cssClickable = From 68fee757344103762ba8d5ae3645f6ece8b69638 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Wed, 24 Aug 2022 14:33:07 -0300 Subject: [PATCH 29/37] Chore: Enabling voip footer e2e tests --- apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts b/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts index 37d4f92bf637..7218333e15cd 100644 --- a/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts @@ -4,7 +4,7 @@ import { test, expect } from './utils/test'; import { OmnichannelDialpadModal, OmnichannelVoipFooter, OmnichannelSection } from './page-objects'; import { createAuxContext } from './utils'; import { createToken } from '../../client/lib/utils/createToken'; -// import { IS_EE } from './config/constants'; +import { IS_EE } from './config/constants'; type PageObjects = { page: Page; @@ -20,7 +20,7 @@ const createPageObjects = (page: Page) => ({ voipFooter: new OmnichannelVoipFooter(page), }); -test.skip(true /* IS_EE */, 'Omnichannel Voip Footer > Enterprise Only'); +test.skip(!IS_EE, 'Omnichannel Voip Footer > Enterprise Only'); test.use({ storageState: 'admin-session.json' }); From 976474e7fc431770ee46580c9051f35fea59c521 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Wed, 24 Aug 2022 15:09:15 -0300 Subject: [PATCH 30/37] Chore: ts-ignore for type circular reference --- apps/meteor/server/models/raw/LivechatInquiry.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/meteor/server/models/raw/LivechatInquiry.ts b/apps/meteor/server/models/raw/LivechatInquiry.ts index 703ddeafbaeb..23d1eea9bd98 100644 --- a/apps/meteor/server/models/raw/LivechatInquiry.ts +++ b/apps/meteor/server/models/raw/LivechatInquiry.ts @@ -5,6 +5,7 @@ import { LivechatInquiryStatus } from '@rocket.chat/core-typings'; import { BaseRaw } from './BaseRaw'; +// @ts-ignore Circular reference on field 'attachments' export class LivechatInquiryRaw extends BaseRaw implements ILivechatInquiryModel { constructor(db: Db, trash?: Collection>) { super(db, 'livechat_inquiry', trash); From 7c3f3835ed77bb1582fbc8b96e87b4bac67d081e Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Wed, 24 Aug 2022 16:44:18 -0300 Subject: [PATCH 31/37] Chore: Adjusting config logic --- apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts b/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts index 7218333e15cd..2b6c1017ce2f 100644 --- a/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts @@ -55,12 +55,10 @@ test.describe('Omnichannel VoIP Footer', () => { VoIP_Server_Name: 'OmniAsterisk', VoIP_Server_Websocket_Path: 'wss://omni-asterisk.dev.rocket.chat/ws', }; - const promises = []; - for (const [key, value] of Object.entries(expectedSettings)) { - promises.push(api.post(`/settings/${key}`, { value })); - } + + const promises = Object.entries(expectedSettings).map(([key, value]) => api.post(`/settings/${key}`, { value })); const allResults = await Promise.all(promises); - expect(allResults.every(({ status }) => status() === 200)).toBe(true); + expect(allResults.every((res) => res.status() === 200)).toBe(true); // Add agent await Promise.all([ From 80d79efdc1d42fe58a15bd1607cf258b45086bdc Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Wed, 24 Aug 2022 18:20:19 -0300 Subject: [PATCH 32/37] Chore: Removing e2e tests --- .github/workflows/build_and_test.yml | 2 - .../tests/e2e/omnichannel-voip-footer.spec.ts | 176 ------------------ apps/meteor/tests/e2e/page-objects/index.ts | 3 - .../page-objects/omnichannel-dialpad-modal.ts | 21 --- .../e2e/page-objects/omnichannel-section.ts | 21 --- .../page-objects/omnichannel-voip-footer.ts | 25 --- docker-compose-ci.yml | 1 - 7 files changed, 249 deletions(-) delete mode 100644 apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-dialpad-modal.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-section.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-voip-footer.ts diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 6fd8d3150117..50138d215a3c 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -268,7 +268,6 @@ jobs: env: MONGO_URL: 'mongodb://host.docker.internal:27017/rocketchat?replicaSet=rs0&directConnection=true' MONGO_OPLOG_URL: 'mongodb://mongodb:27017/local?replicaSet=rs0&directConnection=true' - VOIP_MANAGEMENT_SERVER_PWD: ${{ secrets.VOIP_MANAGEMENT_SERVER_PWD }} run: | export LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") @@ -447,7 +446,6 @@ jobs: DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} TRANSPORTER: nats://nats:4222 ENTERPRISE_LICENSE: ${{ secrets.ENTERPRISE_LICENSE }} - VOIP_MANAGEMENT_SERVER_PWD: ${{ secrets.VOIP_MANAGEMENT_SERVER_PWD }} run: | export LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") diff --git a/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts b/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts deleted file mode 100644 index 2b6c1017ce2f..000000000000 --- a/apps/meteor/tests/e2e/omnichannel-voip-footer.spec.ts +++ /dev/null @@ -1,176 +0,0 @@ -import type { Page } from '@playwright/test'; - -import { test, expect } from './utils/test'; -import { OmnichannelDialpadModal, OmnichannelVoipFooter, OmnichannelSection } from './page-objects'; -import { createAuxContext } from './utils'; -import { createToken } from '../../client/lib/utils/createToken'; -import { IS_EE } from './config/constants'; - -type PageObjects = { - page: Page; - omncSection: OmnichannelSection; - dialpadModal: OmnichannelDialpadModal; - voipFooter: OmnichannelVoipFooter; -}; - -const createPageObjects = (page: Page) => ({ - page, - omncSection: new OmnichannelSection(page), - dialpadModal: new OmnichannelDialpadModal(page), - voipFooter: new OmnichannelVoipFooter(page), -}); - -test.skip(!IS_EE, 'Omnichannel Voip Footer > Enterprise Only'); - -test.use({ storageState: 'admin-session.json' }); - -test.describe('Omnichannel VoIP Footer', () => { - let admin: PageObjects; - let user: PageObjects; - - test.beforeAll(async ({ api }) => { - // Enable Omnichannel - await api.post('/method.call/saveSettings', { - message: JSON.stringify({ - msg: 'method', - id: '67', - method: 'saveSettings', - params: [ - [{ _id: 'Livechat_enabled', value: true }], - { - twoFactorCode: 'b6769a5ae0a6071ecabbe868dbdfa925f856c2bb3d910f93cb39479c64ca221e', - twoFactorMethod: 'password', - }, - ], - }), - }); - - // Configure VoIP - const expectedSettings = { - VoIP_Enabled: true, - VoIP_Management_Server_Host: 'omni-asterisk.dev.rocket.chat', - VoIP_Management_Server_Port: 5038, - VoIP_Management_Server_Name: 'OminiAsterisk', - VoIP_Management_Server_Username: 'sales.rocket.chat', - VoIP_Server_Name: 'OmniAsterisk', - VoIP_Server_Websocket_Path: 'wss://omni-asterisk.dev.rocket.chat/ws', - }; - - const promises = Object.entries(expectedSettings).map(([key, value]) => api.post(`/settings/${key}`, { value })); - const allResults = await Promise.all(promises); - expect(allResults.every((res) => res.status() === 200)).toBe(true); - - // Add agent - await Promise.all([ - api.post('/livechat/users/agent', { username: 'rocketchat.internal.admin.test' }), - api.post('/livechat/users/agent', { username: 'user1' }), - ]); - - // Add agent to extension and as a contact - await Promise.all([ - api.post('/omnichannel/agent/extension', { username: 'rocketchat.internal.admin.test', extension: '80018' }), - api.post('/omnichannel/agent/extension', { username: 'user1', extension: '80017' }), - api.post('/omnichannel/contact', { - name: 'Test User One', - phone: '80017', - email: '', - customFields: {}, - token: createToken(), - }), - ]); - }); - - test.beforeAll(async ({ browser }) => { - const { page } = await createAuxContext(browser, 'user1-session.json'); - user = createPageObjects(page); - - await expect(user.omncSection.element).toBeVisible({ timeout: 10000 }); - - if (await page.isVisible('[data-qa-id="deviceManagementFeatureModal"]')) { - await page.locator('[data-qa-id="deviceManagementFeatureModal"] button >> text="Got it"').click(); - } - - await expect(user.omncSection.btnVoipToggle).toBeEnabled(); - await page.waitForTimeout(2000); - await user.omncSection.btnVoipToggle.click(); - await expect(user.omncSection.btnVoipToggle).toHaveAttribute('aria-checked', 'true'); - }); - - test.afterAll(async ({ api }) => { - // Remove users from extensions - await Promise.all([ - api.delete(`/omnichannel/agent/extension/user1`), - api.delete(`/omnichannel/agent/extension/rocketchat.internal.admin.test`), - ]); - - // Remove users from extensions - await Promise.all([api.delete('/livechat/users/agent/user1'), api.delete('/livechat/users/agent/rocketchat.internal.admin.test')]); - }); - - test.beforeEach(async ({ page }) => { - admin = createPageObjects(page); - await page.goto('/home'); - }); - - test.beforeEach(async () => { - const { page, omncSection } = admin; - - // Enable voip - await expect(omncSection.element).toBeVisible({ timeout: 10000 }); - - // Close feature modal - if (await page.isVisible('[data-qa-id="deviceManagementFeatureModal"]')) { - await page.locator('[data-qa-id="deviceManagementFeatureModal"] button >> text="Got it"').click(); - } - - await expect(omncSection.btnVoipToggle).toBeEnabled(); - await page.waitForTimeout(2000); - await admin.omncSection.btnVoipToggle.click(); - await expect(omncSection.btnVoipToggle).toHaveAttribute('aria-checked', 'true'); - }); - - test('VoIP call identification', async () => { - await test.step('expect voip footer to identify known contact', async () => { - const { omncSection, dialpadModal, voipFooter } = admin; - - // Open dialpad modal - await expect(omncSection.btnDialpad).toBeEnabled(); - await omncSection.btnDialpad.click(); - - // Dial number and call - await expect(dialpadModal.element).toBeVisible(); - await dialpadModal.inputPhoneNumber.type('80017'); - await expect(dialpadModal.btnCall).toBeEnabled(); - await dialpadModal.btnCall.click(); - - // Check if contact name is there - await expect(voipFooter.element).toBeVisible({ timeout: 10000 }); - await expect(voipFooter.textTitle).toHaveText('Test User One'); - - // Reject the call - await voipFooter.btnEndCall.click(); - }); - - await test.step('expect voip footer to fallback to phone number for unknown contact', async () => { - const { omncSection, dialpadModal, voipFooter } = user; - - // Open dialpad modal - await expect(omncSection.btnDialpad).toBeEnabled(); - await omncSection.btnDialpad.click(); - - // Dial number and call - await expect(dialpadModal.element).toBeVisible(); - await dialpadModal.inputPhoneNumber.type('80018'); - await expect(dialpadModal.btnCall).toBeEnabled(); - await dialpadModal.btnCall.click(); - - // Check if contact name is there - await voipFooter.element.waitFor(); - await expect(voipFooter.element).toBeVisible({ timeout: 10000 }); - await expect(voipFooter.textTitle).toHaveText('80018'); - - // Reject the call - await voipFooter.btnEndCall.click(); - }); - }); -}); diff --git a/apps/meteor/tests/e2e/page-objects/index.ts b/apps/meteor/tests/e2e/page-objects/index.ts index 828e9c73afd3..47d50e611c42 100644 --- a/apps/meteor/tests/e2e/page-objects/index.ts +++ b/apps/meteor/tests/e2e/page-objects/index.ts @@ -9,6 +9,3 @@ export * from './omnichannel-agents'; export * from './omnichannel-departments'; export * from './omnichannel-current-chats'; export * from './omnichannel-livechat'; -export * from './omnichannel-section'; -export * from './omnichannel-voip-footer'; -export * from './omnichannel-dialpad-modal'; diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-dialpad-modal.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-dialpad-modal.ts deleted file mode 100644 index b98ab2fd4d3c..000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-dialpad-modal.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -export class OmnichannelDialpadModal { - private readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - get element(): Locator { - return this.page.locator('dialog[data-qa-id="omncDialpadModal"]'); - } - - get inputPhoneNumber(): Locator { - return this.page.locator('input[type="text"]'); - } - - get btnCall(): Locator { - return this.page.locator('button[data-qa-id="omncDialpadCallButton"]'); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-section.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-section.ts deleted file mode 100644 index 8d617e25712b..000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-section.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -export class OmnichannelSection { - private readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - get element(): Locator { - return this.page.locator('div[data-qa-id="omncSection"]'); - } - - get btnVoipToggle(): Locator { - return this.page.locator('role=button[name="Enable/Disable VoIP"]'); - } - - get btnDialpad(): Locator { - return this.page.locator('role=button[name="Open Dialpad"]'); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-voip-footer.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-voip-footer.ts deleted file mode 100644 index a2febe220c1c..000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-voip-footer.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -export class OmnichannelVoipFooter { - private readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - get element(): Locator { - return this.page.locator('[data-qa-id="omncVoipFooter"]'); - } - - get btnReject(): Locator { - return this.page.locator('role=button[name="Reject call"]'); - } - - get btnEndCall(): Locator { - return this.page.locator('role=button[name="End call"]'); - } - - get textTitle(): Locator { - return this.page.locator('[data-qa-id="omncVoipTitle"]'); - } -} diff --git a/docker-compose-ci.yml b/docker-compose-ci.yml index c3c859d7a433..32b33c3d0a4e 100644 --- a/docker-compose-ci.yml +++ b/docker-compose-ci.yml @@ -14,7 +14,6 @@ services: - 'TRANSPORTER=${TRANSPORTER}' - MOLECULER_LOG_LEVEL=info - 'ROCKETCHAT_LICENSE=${ENTERPRISE_LICENSE}' - - 'OVERWRITE_SETTING_VoIP_Management_Server_Password=${VOIP_MANAGEMENT_SERVER_PWD}' extra_hosts: - 'host.docker.internal:host-gateway' depends_on: From b3481aebe524a65327277edfebeea1dfae0b0dd4 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 24 Aug 2022 23:53:34 -0300 Subject: [PATCH 33/37] review --- .../omnichannel/useOmnichannelContact.ts | 43 ------------------- .../footer/voip/VoipFooter.stories.tsx | 1 - .../client/sidebar/footer/voip/VoipFooter.tsx | 14 +++--- .../voip/hooks/useOmnichannelContactLabel.ts | 15 +++++++ .../client/sidebar/footer/voip/index.tsx | 1 - .../sidebar/sections/OmnichannelSection.tsx | 2 +- .../actions/OmnichannelCallToggleError.tsx | 2 +- .../DeviceManagementFeatureModal.tsx | 2 +- .../voip/modal/DialPad/DialPadModal.tsx | 3 +- 9 files changed, 26 insertions(+), 57 deletions(-) delete mode 100644 apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts create mode 100644 apps/meteor/client/sidebar/footer/voip/hooks/useOmnichannelContactLabel.ts diff --git a/apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts b/apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts deleted file mode 100644 index 4eee21c519a2..000000000000 --- a/apps/meteor/client/hooks/omnichannel/useOmnichannelContact.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { ILivechatVisitor } from '@rocket.chat/core-typings'; -import { useEndpoint } from '@rocket.chat/ui-contexts'; -import { useQuery } from '@tanstack/react-query'; -import { useState } from 'react'; - -import { parseOutboundPhoneNumber } from '../../../ee/client/lib/voip/parseOutboundPhoneNumber'; - -type Contact = { - name: string; - phone: string; -}; - -type ContactQuery = { - isLoading: boolean; - isError: boolean; - contact: Contact; -}; - -const createContact = (phone: string, data: Pick | null): Contact => ({ - phone, - name: data?.name || '', -}); - -export const useOmnichannelContact = (ogPhone: string, name = ''): ContactQuery => { - const getContactBy = useEndpoint('GET', '/v1/omnichannel/contact.search'); - const phone = parseOutboundPhoneNumber(ogPhone); - const [defaultContact] = useState({ phone, name }); - - const { - data: contact, - isLoading, - isError, - } = useQuery(['getContactsByPhone', phone], async (): Promise => { - const { contact } = await getContactBy({ phone }); - return createContact(phone, contact); - }); - - return { - contact: contact || defaultContact, - isLoading, - isError, - }; -}; diff --git a/apps/meteor/client/sidebar/footer/voip/VoipFooter.stories.tsx b/apps/meteor/client/sidebar/footer/voip/VoipFooter.stories.tsx index 914f0b6f6e97..c8d273305a7c 100644 --- a/apps/meteor/client/sidebar/footer/voip/VoipFooter.stories.tsx +++ b/apps/meteor/client/sidebar/footer/voip/VoipFooter.stories.tsx @@ -90,7 +90,6 @@ const VoipFooterTemplate: ComponentStory = (args) => { callsInQueue='2 Calls In Queue' dispatchEvent={() => null} openedRoomInfo={{ v: { token: '' }, rid: '' }} - anonymousText={'Anonymous'} options={{ deviceSettings: { label: ( diff --git a/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx b/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx index 042158bd7108..45486e8250f3 100644 --- a/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx +++ b/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx @@ -7,7 +7,7 @@ import React, { ReactElement, MouseEvent, ReactNode } from 'react'; import type { VoipFooterMenuOptions } from '../../../../ee/client/hooks/useVoipFooterMenu'; import { CallActionsType } from '../../../contexts/CallContext'; -import { useOmnichannelContact } from '../../../hooks/omnichannel/useOmnichannelContact'; +import { useOmnichannelContactLabel } from './hooks/useOmnichannelContactLabel'; type VoipFooterPropsType = { caller: ICallerInfo; @@ -32,7 +32,6 @@ type VoipFooterPropsType = { openRoom: (rid: IVoipRoom['_id']) => void; dispatchEvent: (params: { event: VoipClientEvents; rid: string; comment?: string }) => void; openedRoomInfo: { v: { token?: string | undefined }; rid: string }; - anonymousText: string; isEnterprise: boolean; children?: ReactNode; options: VoipFooterMenuOptions; @@ -54,12 +53,11 @@ export const VoipFooter = ({ callsInQueue, dispatchEvent, openedRoomInfo, - anonymousText, isEnterprise = false, children, options, }: VoipFooterPropsType): ReactElement => { - const { contact } = useOmnichannelContact(caller.callerId); + const contactLabel = useOmnichannelContactLabel(caller); const t = useTranslation(); const cssClickable = @@ -80,7 +78,6 @@ export const VoipFooter = ({ { if (callerState === 'IN_CALL' || callerState === 'ON_HOLD') { openRoom(openedRoomInfo.rid); @@ -121,8 +118,11 @@ export const VoipFooter = ({ - - {contact.name || caller.callerName || contact.phone || anonymousText} + + {/* TODO: Check what is the point of having Anonymous here, + since callerId and callerName are required and they act as a fallback + */} + {contactLabel || t('Anonymous')} {subtitle} diff --git a/apps/meteor/client/sidebar/footer/voip/hooks/useOmnichannelContactLabel.ts b/apps/meteor/client/sidebar/footer/voip/hooks/useOmnichannelContactLabel.ts new file mode 100644 index 000000000000..d7faa5d621d7 --- /dev/null +++ b/apps/meteor/client/sidebar/footer/voip/hooks/useOmnichannelContactLabel.ts @@ -0,0 +1,15 @@ +import { ICallerInfo } from '@rocket.chat/core-typings'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; + +import { parseOutboundPhoneNumber } from '../../../../../ee/client/lib/voip/parseOutboundPhoneNumber'; + +export const useOmnichannelContactLabel = (caller: ICallerInfo): string => { + const getContactBy = useEndpoint('GET', '/v1/omnichannel/contact.search'); + const phone = parseOutboundPhoneNumber(caller.callerId); + + const { data } = useQuery(['getContactsByPhone', phone], async () => getContactBy({ phone }).then(({ contact }) => contact)); + + // TODO: callerName is typed as required so maybe we should not use phone as fallback + return data?.name || caller.callerName || phone; +}; diff --git a/apps/meteor/client/sidebar/footer/voip/index.tsx b/apps/meteor/client/sidebar/footer/voip/index.tsx index 252f843e22ff..17a327fdd6d9 100644 --- a/apps/meteor/client/sidebar/footer/voip/index.tsx +++ b/apps/meteor/client/sidebar/footer/voip/index.tsx @@ -102,7 +102,6 @@ export const VoipFooter = (): ReactElement | null => { callsInQueue={getCallsInQueueText} dispatchEvent={dispatchEvent} openedRoomInfo={openedRoomInfo} - anonymousText={t('Anonymous')} isEnterprise={isEnterprise} options={options} /> diff --git a/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx b/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx index 14681e3881aa..e411c68bc45a 100644 --- a/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx +++ b/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx @@ -51,7 +51,7 @@ const OmnichannelSection = (props: typeof Box): ReactElement => { // The className is a paliative while we make TopBar.ToolBox optional on fuselage return ( - + {t('Omnichannel')} {showOmnichannelQueueLink && handleRoute('queue')} />} diff --git a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleError.tsx b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleError.tsx index 62c817ee8aea..0d5d7817f9c4 100644 --- a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleError.tsx +++ b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleError.tsx @@ -4,5 +4,5 @@ import React, { ReactElement } from 'react'; export const OmnichannelCallToggleError = ({ ...props }): ReactElement => { const t = useTranslation(); - return ; + return ; }; diff --git a/apps/meteor/ee/client/deviceManagement/components/featureModal/DeviceManagementFeatureModal.tsx b/apps/meteor/ee/client/deviceManagement/components/featureModal/DeviceManagementFeatureModal.tsx index 9747f9ee0ffa..477b6aa4c20b 100644 --- a/apps/meteor/ee/client/deviceManagement/components/featureModal/DeviceManagementFeatureModal.tsx +++ b/apps/meteor/ee/client/deviceManagement/components/featureModal/DeviceManagementFeatureModal.tsx @@ -46,7 +46,7 @@ const DeviceManagementFeatureModal = ({ close }: { close: () => void }): ReactEl }; return ( - + {t('Workspace_now_using_device_management')} diff --git a/apps/meteor/ee/client/voip/modal/DialPad/DialPadModal.tsx b/apps/meteor/ee/client/voip/modal/DialPad/DialPadModal.tsx index 720388c0346d..501f3ff9b975 100644 --- a/apps/meteor/ee/client/voip/modal/DialPad/DialPadModal.tsx +++ b/apps/meteor/ee/client/voip/modal/DialPad/DialPadModal.tsx @@ -28,7 +28,7 @@ const DialPadModal = ({ initialValue, errorMessage, handleClose }: DialPadModalP useEnterKey(handleCallButtonClick, isButtonDisabled); return ( - + @@ -57,7 +57,6 @@ const DialPadModal = ({ initialValue, errorMessage, handleClose }: DialPadModalP secondary info size='64px' - data-qa-id='omncDialpadCallButton' onClick={(): void => { handleCallButtonClick(); handleClose(); From cf82287ce98d26a4533991af654d0abd4d426f6c Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Thu, 25 Aug 2022 10:30:57 -0300 Subject: [PATCH 34/37] Chore: Removing todo's --- apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx | 3 --- .../sidebar/footer/voip/hooks/useOmnichannelContactLabel.ts | 1 - 2 files changed, 4 deletions(-) diff --git a/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx b/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx index 45486e8250f3..1179921a552b 100644 --- a/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx +++ b/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx @@ -119,9 +119,6 @@ export const VoipFooter = ({ - {/* TODO: Check what is the point of having Anonymous here, - since callerId and callerName are required and they act as a fallback - */} {contactLabel || t('Anonymous')} diff --git a/apps/meteor/client/sidebar/footer/voip/hooks/useOmnichannelContactLabel.ts b/apps/meteor/client/sidebar/footer/voip/hooks/useOmnichannelContactLabel.ts index d7faa5d621d7..98ebc0d37d5c 100644 --- a/apps/meteor/client/sidebar/footer/voip/hooks/useOmnichannelContactLabel.ts +++ b/apps/meteor/client/sidebar/footer/voip/hooks/useOmnichannelContactLabel.ts @@ -10,6 +10,5 @@ export const useOmnichannelContactLabel = (caller: ICallerInfo): string => { const { data } = useQuery(['getContactsByPhone', phone], async () => getContactBy({ phone }).then(({ contact }) => contact)); - // TODO: callerName is typed as required so maybe we should not use phone as fallback return data?.name || caller.callerName || phone; }; From bbf288989ffcf1fcb572e9e0cd8e56bb38949540 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Thu, 25 Aug 2022 11:33:18 -0300 Subject: [PATCH 35/37] [FIX] Query adjustment to prevent 400 when there's no phone --- .../sidebar/footer/voip/hooks/useOmnichannelContactLabel.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/sidebar/footer/voip/hooks/useOmnichannelContactLabel.ts b/apps/meteor/client/sidebar/footer/voip/hooks/useOmnichannelContactLabel.ts index 98ebc0d37d5c..b7df8cbcf0d1 100644 --- a/apps/meteor/client/sidebar/footer/voip/hooks/useOmnichannelContactLabel.ts +++ b/apps/meteor/client/sidebar/footer/voip/hooks/useOmnichannelContactLabel.ts @@ -8,7 +8,9 @@ export const useOmnichannelContactLabel = (caller: ICallerInfo): string => { const getContactBy = useEndpoint('GET', '/v1/omnichannel/contact.search'); const phone = parseOutboundPhoneNumber(caller.callerId); - const { data } = useQuery(['getContactsByPhone', phone], async () => getContactBy({ phone }).then(({ contact }) => contact)); + const { data } = useQuery(['getContactsByPhone', phone], async () => getContactBy({ phone }).then(({ contact }) => contact), { + enabled: !!phone, + }); return data?.name || caller.callerName || phone; }; From 971e33240af0dc36db25644e1737205f1a1bcbea Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Thu, 25 Aug 2022 14:07:34 -0300 Subject: [PATCH 36/37] Chore: Improving useVoipClient logic --- apps/meteor/ee/client/hooks/useVoipClient.ts | 26 +++++++++++--------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/apps/meteor/ee/client/hooks/useVoipClient.ts b/apps/meteor/ee/client/hooks/useVoipClient.ts index 4512e329f043..0ccb278350d3 100644 --- a/apps/meteor/ee/client/hooks/useVoipClient.ts +++ b/apps/meteor/ee/client/hooks/useVoipClient.ts @@ -22,8 +22,10 @@ const isSignedResponse = (data: any): data is { result: string } => typeof data? // Currently we only support the websocket connection and the SIP proxy connection being from the same host, // we need to add a new setting for SIP proxy if we want to support different hosts for them. export const useVoipClient = (): UseVoipClientResult => { - const settingVoipEnabled = useSetting('VoIP_Enabled'); - const [voipEnabled, setVoipEnabled] = useSafely(useState(settingVoipEnabled)); + const settingVoipEnabled = Boolean(useSetting('VoIP_Enabled')); + + const [voipConnectorEnabled, setVoipConnectorEnabled] = useSafely(useState(true)); + const voipRetryCount = useSetting('VoIP_Retry_Count'); const enableKeepAlive = useSetting('VoIP_Enable_Keep_Alive_For_Unstable_Networks'); const registrationInfo = useEndpoint('GET', '/v1/connector.extension.getRegistrationInfoByUserId'); @@ -34,17 +36,19 @@ export const useVoipClient = (): UseVoipClientResult => { const [result, setResult] = useSafely(useState({})); const isEE = useHasLicenseModule('voip-enterprise'); + const voipEnabled = settingVoipEnabled && voipConnectorEnabled; useEffect(() => { - setVoipEnabled(settingVoipEnabled); - }, [settingVoipEnabled, setVoipEnabled]); - - useEffect(() => { - const voipEnableEventHandler = (enabled: boolean): void => { - setVoipEnabled(enabled); - }; - return subscribeToNotifyLoggedIn(`voip.statuschanged`, voipEnableEventHandler); - }, [setResult, setVoipEnabled, subscribeToNotifyLoggedIn]); + setVoipConnectorEnabled(settingVoipEnabled); + }, [settingVoipEnabled, setVoipConnectorEnabled]); + + useEffect( + () => + subscribeToNotifyLoggedIn(`voip.statuschanged`, (enabled: boolean): void => { + setVoipConnectorEnabled(enabled); + }), + [setResult, setVoipConnectorEnabled, subscribeToNotifyLoggedIn], + ); useEffect(() => { const uid = user?._id; From dae4b1b7d383ff58df68fabd56d26f7e285bf4bd Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Thu, 25 Aug 2022 14:11:32 -0300 Subject: [PATCH 37/37] Chore: Removed unnecessary effect --- apps/meteor/ee/client/hooks/useVoipClient.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/meteor/ee/client/hooks/useVoipClient.ts b/apps/meteor/ee/client/hooks/useVoipClient.ts index 0ccb278350d3..eab2b66993a8 100644 --- a/apps/meteor/ee/client/hooks/useVoipClient.ts +++ b/apps/meteor/ee/client/hooks/useVoipClient.ts @@ -38,10 +38,6 @@ export const useVoipClient = (): UseVoipClientResult => { const isEE = useHasLicenseModule('voip-enterprise'); const voipEnabled = settingVoipEnabled && voipConnectorEnabled; - useEffect(() => { - setVoipConnectorEnabled(settingVoipEnabled); - }, [settingVoipEnabled, setVoipConnectorEnabled]); - useEffect( () => subscribeToNotifyLoggedIn(`voip.statuschanged`, (enabled: boolean): void => {