Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IMPROVE] Added identification on calls to/from existing contacts #26334

Merged
merged 53 commits into from
Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
9a136d6
[IMPROVE] Improved voip call identification
aleksandernsilva Jul 20, 2022
0166bed
Chore: Improved styling logic for the call dial button
aleksandernsilva Jul 6, 2022
ad5818c
[FIX] Fixed FilterByText passing invalid props to div
aleksandernsilva Jul 21, 2022
b331a6d
[FIX] Changed useContactName phone parse fn
aleksandernsilva Jul 21, 2022
6abd4e3
[IMPROVE] Moved call table row to its own component and added contact…
aleksandernsilva Jul 21, 2022
3eb082e
Chore: Adjusted endpoint typings
aleksandernsilva Jul 21, 2022
f98a7e3
Chore: Adjusted hook typings
aleksandernsilva Jul 21, 2022
579d637
Merge branch 'develop' into improve/call-contact-id
aleksandernsilva Jul 22, 2022
c35e746
Merge branch 'develop' into improve/call-contact-id
aleksandernsilva Jul 22, 2022
dad97c9
Merge branch 'develop' into improve/call-contact-id
KevLehman Jul 26, 2022
ddbd469
small change on error message
KevLehman Jul 26, 2022
cdaacd5
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into impr…
aleksandernsilva Aug 1, 2022
21eb8bf
[FIX] Contact name not being displayed in the contact table
aleksandernsilva Aug 2, 2022
fc37ab1
[FIX] Contact name not being displayed in the voip contextual bar
aleksandernsilva Aug 2, 2022
f6f71f5
Chore: Adjusting import order
aleksandernsilva Aug 2, 2022
7d3211c
[REFACTOR] Moved contacts fetch logic to context to reduce fetch requ…
aleksandernsilva Aug 2, 2022
6155b8b
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into impr…
aleksandernsilva Aug 8, 2022
6f536ec
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into impr…
aleksandernsilva Aug 10, 2022
e64a427
Chore: Removing useContactName from call table and voip info
aleksandernsilva Aug 14, 2022
7c89f34
Chore: Removing contacts provider restoring useOmnichannelContacts hook
aleksandernsilva Aug 15, 2022
d3f68de
Chore: Added page objects for dialpad modal, voip footer and omnichan…
aleksandernsilva Aug 15, 2022
2711521
[FIX] Fixed voip enabled initial value on useVoipClient
aleksandernsilva Aug 15, 2022
8f2b9ac
Chore: Implemented e2e tests for the VoipFooter component
aleksandernsilva Aug 15, 2022
4321378
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into impr…
aleksandernsilva Aug 15, 2022
085fe47
Chore: Uncommenting playwright global setupo
aleksandernsilva Aug 15, 2022
5f7e860
Chore: Skip e2e for community edition
aleksandernsilva Aug 15, 2022
e0a929d
Support receiving duplicated dialend events
KevLehman Aug 15, 2022
ddbb78d
Chore: Skipping e2e test until CI license issue is fixed
aleksandernsilva Aug 16, 2022
185580d
Merge branch 'develop' into improve/call-contact-id
tiagoevanp Aug 16, 2022
72ae189
[FIX] Removing asterisk from visitor's numbers
aleksandernsilva Aug 18, 2022
2d4fdfa
Chore: Removing cache and simplifying hook
aleksandernsilva Aug 18, 2022
4ecdf0a
Merge branch 'develop' into improve/call-contact-id
tiagoevanp Aug 19, 2022
8786cf0
Merge branch 'develop' into improve/call-contact-id
aleksandernsilva Aug 19, 2022
6ecba02
Chore: Refactored useOmnichannelContact to use useQuery
aleksandernsilva Aug 23, 2022
03f68ab
Chore: Removed duplicated qa ids and refactored to use roles
aleksandernsilva Aug 23, 2022
3076938
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into impr…
aleksandernsilva Aug 23, 2022
dd3b6b2
Chore: Fixing typing
aleksandernsilva Aug 23, 2022
3c04aa6
Chore: Suggestions on PR #26334 (#26670)
murtaza98 Aug 24, 2022
cae6385
Chore: Added isLoading and isError to useOmnichannelContact return value
aleksandernsilva Aug 24, 2022
098ca9c
Merge branch 'improve/call-contact-id' of github.com:RocketChat/Rocke…
aleksandernsilva Aug 24, 2022
2daf6a6
Merge branch 'develop' into improve/call-contact-id
aleksandernsilva Aug 24, 2022
68fee75
Chore: Enabling voip footer e2e tests
aleksandernsilva Aug 24, 2022
976474e
Chore: ts-ignore for type circular reference
aleksandernsilva Aug 24, 2022
7c3f383
Chore: Adjusting config logic
aleksandernsilva Aug 24, 2022
80d79ef
Chore: Removing e2e tests
aleksandernsilva Aug 24, 2022
9b8ae32
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into impr…
ggazzo Aug 25, 2022
26b5ed4
Merge branch 'improve/call-contact-id' of github.com:RocketChat/Rocke…
ggazzo Aug 25, 2022
b3481ae
review
ggazzo Aug 25, 2022
cf82287
Chore: Removing todo's
aleksandernsilva Aug 25, 2022
bbf2889
[FIX] Query adjustment to prevent 400 when there's no phone
aleksandernsilva Aug 25, 2022
971e332
Chore: Improving useVoipClient logic
aleksandernsilva Aug 25, 2022
dae4b1b
Chore: Removed unnecessary effect
aleksandernsilva Aug 25, 2022
6b74f77
Merge branch 'develop' into improve/call-contact-id
kodiakhq[bot] Aug 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/meteor/client/components/FilterByText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const FilterByText = ({
}, []);

return (
<Box mb='x16' is='form' onSubmit={handleFormSubmit} display='flex' flexDirection={shouldFiltersStack ? 'column' : 'row'} {...props}>
aleksandernsilva marked this conversation as resolved.
Show resolved Hide resolved
<Box mb='x16' is='form' onSubmit={handleFormSubmit} display='flex' flexDirection={shouldFiltersStack ? 'column' : 'row'}>
aleksandernsilva marked this conversation as resolved.
Show resolved Hide resolved
<TextInput
placeholder={placeholder ?? t('Search')}
ref={inputRef}
Expand Down
18 changes: 18 additions & 0 deletions apps/meteor/client/hooks/omnichannel/useContactName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { useContext } from 'react';

import { ContactsContext, ContactsContextValue } from '../../providers/OmnichannelContactsProvider';

export const useOmnichannelContacts = (): ContactsContextValue => useContext(ContactsContext);
Original file line number Diff line number Diff line change
Expand Up @@ -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') });
}
Expand Down
5 changes: 4 additions & 1 deletion apps/meteor/client/providers/MeteorProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -39,7 +40,9 @@ const MeteorProvider: FC = ({ children }) => (
<VideoConfProvider>
<CallProvider>
<OmnichannelProvider>
<AttachmentProvider>{children}</AttachmentProvider>
<OmnichannelContactsProvider>
<AttachmentProvider>{children}</AttachmentProvider>
</OmnichannelContactsProvider>
</OmnichannelProvider>
</CallProvider>
</VideoConfProvider>
Expand Down
106 changes: 106 additions & 0 deletions apps/meteor/client/providers/OmnichannelContactsProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<Contact>;
};

const STORAGE_KEY = 'rcOmnichannelContacts';

const createContact = (phone: string, data: Pick<ILivechatVisitor, 'name'> | null): Contact => ({
phone,
name: data?.name || '',
});

const storeInCache = (contacts: Record<string, Contact>): void => {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(contacts));
};

const retrieveFromCache = (): Record<string, Contact> => {
const cache = window.localStorage.getItem(STORAGE_KEY);

try {
return cache ? JSON.parse(cache) : {};
} catch (_) {
return {};
}
};

export const ContactsContext = createContext<ContactsContextValue>({
getContactByPhone: (_: string) => Promise.reject(),
});

export const OmnichannelContactsProvider = ({ children }: ContactsProviderProps): ReactElement => {
const getContactBy = useEndpoint('GET', '/v1/omnichannel/contact.search');
const contacts = useRef<Record<string, Contact>>({});
const pendingRequests = useRef<Map<string, Promise<Contact>>>(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<typeof getContactBy> => getContactBy({ phone }), [getContactBy]);

const getContactByPhone = useCallback(
(phone: string): Promise<Contact> => {
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 <ContactsContext.Provider value={contextValue}>{children}</ContactsContext.Provider>;
};
6 changes: 4 additions & 2 deletions apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -117,7 +119,7 @@ export const VoipFooter = ({
<Box display='flex' flexDirection='row' mi='16px' mbe='12px' justifyContent='space-between' alignItems='center'>
<Box>
<Box color='white' fontScale='p2' withTruncatedText>
{caller.callerName || parseOutboundPhoneNumber(caller.callerId) || anonymousText}
{caller.callerName || contactName || anonymousText}
</Box>
<Box color='hint' fontScale='c1' withTruncatedText>
{subtitle}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
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';

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();

Expand All @@ -20,6 +27,7 @@ export const CallDialpadButton = ({ phoneNumber }: { phoneNumber: string }): Rea
<IconButton
rcx-call-dial-button
title={outBoundCallsAllowed ? t('Call_number') : t('Call_number_enterprise_only')}
className={rcxCallDialButton}
disabled={!outBoundCallsEnabledForUser || !phoneNumber}
tiny
icon='phone'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,11 @@
import { IVoipRoom } from '@rocket.chat/core-typings';
import { css } from '@rocket.chat/css-in-js';
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';

export const rcxCallDialButton = css`
&:not(:hover) {
.rcx-call-dial-button {
display: none !important;
}
}
`;
import { CallTableRow } from './CallTableRow';

const useQuery = (
{
Expand Down Expand Up @@ -66,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;
Expand Down Expand Up @@ -156,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 (
<Table.Row
key={_id}
className={rcxCallDialButton}
tabIndex={0}
role='link'
onClick={(): void => onRowClick(_id, v?.token)}
action
qa-user-id={_id}
height='40px'
>
<Table.Cell withTruncatedText>{parseOutboundPhoneNumber(fname)}</Table.Cell>
<Table.Cell withTruncatedText>{parseOutboundPhoneNumber(phoneNumber)}</Table.Cell>
<Table.Cell withTruncatedText>{queue}</Table.Cell>
<Table.Cell withTruncatedText>{moment(callStarted).format('L LTS')}</Table.Cell>
<Table.Cell withTruncatedText>{duration.isValid() && duration.humanize()}</Table.Cell>
<Table.Cell withTruncatedText>{resolveDirectionLabel(direction)}</Table.Cell>
<Table.Cell>{isCallReady && <CallDialpadButton phoneNumber={phoneNumber} />}</Table.Cell>
</Table.Row>
);
},
[onRowClick, resolveDirectionLabel, isCallReady],
);
const renderRow = useCallback((room) => <CallTableRow room={room} onRowClick={onRowClick} />, [onRowClick]);

return (
<GenericTable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { IVoipRoom } from '@rocket.chat/core-typings';
import { Table } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import moment from 'moment';
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 = {
room: IVoipRoom;
onRowClick(_id: string, token?: string): void;
};

export const CallTableRow = ({ room, onRowClick }: CallTableRowProps): ReactElement => {
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 (
<Table.Row
key={_id}
rcx-show-call-button-on-hover
tabIndex={0}
role='link'
onClick={(): void => onRowClick(_id, v?.token)}
action
qa-user-id={_id}
height='40px'
>
<Table.Cell withTruncatedText>{contactName || parseOutboundPhoneNumber(fname)}</Table.Cell>
<Table.Cell withTruncatedText>{parseOutboundPhoneNumber(phoneNumber)}</Table.Cell>
<Table.Cell withTruncatedText>{queue}</Table.Cell>
<Table.Cell withTruncatedText>{moment(callStarted).format('L LTS')}</Table.Cell>
<Table.Cell withTruncatedText>{duration.isValid() && duration.humanize()}</Table.Cell>
<Table.Cell withTruncatedText>{resolveDirectionLabel(direction)}</Table.Cell>
<Table.Cell>{isCallReady && <CallDialpadButton phoneNumber={phoneNumber} />}</Table.Cell>
</Table.Row>
);
};
Loading