diff --git a/.changeset/young-avocados-brake.md b/.changeset/young-avocados-brake.md new file mode 100644 index 0000000000000..a20df69c72426 --- /dev/null +++ b/.changeset/young-avocados-brake.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Adds close action to contact unknown callout displayed within Livechat rooms diff --git a/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.spec.tsx b/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.spec.tsx new file mode 100644 index 0000000000000..f246478e3b4b8 --- /dev/null +++ b/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.spec.tsx @@ -0,0 +1,75 @@ +import { faker } from '@faker-js/faker/locale/af_ZA'; +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import ComposerOmnichannelCallout from './ComposerOmnichannelCallout'; +import FakeRoomProvider from '../../../../../tests/mocks/client/FakeRoomProvider'; +import { createFakeContact, createFakeRoom } from '../../../../../tests/mocks/data'; + +jest.mock('../../../omnichannel/contactInfo/tabs/ContactInfoChannels/useBlockChannel', () => ({ + useBlockChannel: () => jest.fn(), +})); + +const fakeVisitor = { + _id: faker.string.uuid(), + token: faker.string.uuid(), + username: faker.internet.userName(), +}; + +const fakeRoom = createFakeRoom({ t: 'l', v: fakeVisitor }); +const fakeContact = createFakeContact(); + +it('should be displayed if contact is unknown', async () => { + const getContactMockFn = jest.fn().mockResolvedValue({ contact: fakeContact }); + const wrapper = mockAppRoot().withEndpoint('GET', '/v1/omnichannel/contacts.get', getContactMockFn).withRoom(fakeRoom); + + render( + + + , + { wrapper: wrapper.build() }, + ); + + await waitFor(() => expect(getContactMockFn).toHaveBeenCalled()); + expect(screen.getByText('Unknown_contact_callout_description')).toBeVisible(); + expect(screen.getByRole('button', { name: 'Add_contact' })).toBeVisible(); + expect(screen.getByRole('button', { name: 'Block' })).toBeVisible(); + expect(screen.getByRole('button', { name: 'Dismiss' })).toBeVisible(); +}); + +it('should not be displayed if contact is known', async () => { + const getContactMockFn = jest.fn().mockResolvedValue({ contact: createFakeContact({ unknown: false }) }); + const wrapper = mockAppRoot().withEndpoint('GET', '/v1/omnichannel/contacts.get', getContactMockFn).withRoom(fakeRoom); + + render( + + + , + { wrapper: wrapper.build() }, + ); + + await waitFor(() => expect(getContactMockFn).toHaveBeenCalled()); + expect(screen.queryByText('Unknown_contact_callout_description')).not.toBeInTheDocument(); +}); + +it('should hide callout on dismiss', async () => { + const getContactMockFn = jest.fn().mockResolvedValue({ contact: fakeContact }); + const wrapper = mockAppRoot().withEndpoint('GET', '/v1/omnichannel/contacts.get', getContactMockFn).withRoom(fakeRoom); + + render( + + + , + { wrapper: wrapper.build() }, + ); + + await waitFor(() => expect(getContactMockFn).toHaveBeenCalled()); + expect(screen.getByText('Unknown_contact_callout_description')).toBeVisible(); + + const btnDismiss = screen.getByRole('button', { name: 'Dismiss' }); + await userEvent.click(btnDismiss); + + expect(screen.queryByText('Unknown_contact_callout_description')).not.toBeInTheDocument(); +}); diff --git a/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.tsx b/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.tsx index b1ce6cc244d3d..23a42b0546ad3 100644 --- a/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.tsx +++ b/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.tsx @@ -1,26 +1,18 @@ -import { Box, Button, ButtonGroup, Callout } from '@rocket.chat/fuselage'; -import { useAtLeastOnePermission, useEndpoint, useRouter, useSetting } from '@rocket.chat/ui-contexts'; +import { Button, ButtonGroup, Callout, IconButton } from '@rocket.chat/fuselage'; +import { useSessionStorage } from '@rocket.chat/fuselage-hooks'; +import { useEndpoint, useRouter } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; -import { Trans, useTranslation } from 'react-i18next'; +import { useId } from 'react'; +import { useTranslation } from 'react-i18next'; import { isSameChannel } from '../../../../../app/livechat/lib/isSameChannel'; -import { useHasLicenseModule } from '../../../../hooks/useHasLicenseModule'; import { useBlockChannel } from '../../../omnichannel/contactInfo/tabs/ContactInfoChannels/useBlockChannel'; import { useOmnichannelRoom } from '../../contexts/RoomContext'; const ComposerOmnichannelCallout = () => { const { t } = useTranslation(); const room = useOmnichannelRoom(); - const { navigate, buildRoutePath } = useRouter(); - const hasLicense = useHasLicenseModule('contact-id-verification'); - const securityPrivacyRoute = buildRoutePath('/omnichannel/security-privacy'); - const shouldShowSecurityRoute = useSetting('Livechat_Require_Contact_Verification') !== 'never' || !hasLicense; - - const canViewSecurityPrivacy = useAtLeastOnePermission([ - 'view-privileged-setting', - 'edit-privileged-setting', - 'manage-selected-settings', - ]); + const { navigate } = useRouter(); const { _id, @@ -29,6 +21,9 @@ const ComposerOmnichannelCallout = () => { contactId, } = room; + const calloutDescriptionId = useId(); + const [dismissed, setDismissed] = useSessionStorage(`contact-unknown-callout-${contactId}`, false); + const getContactById = useEndpoint('GET', '/v1/omnichannel/contacts.get'); const { data } = useQuery({ queryKey: ['getContactById', contactId], queryFn: () => getContactById({ contactId }) }); @@ -37,14 +32,15 @@ const ComposerOmnichannelCallout = () => { const handleBlock = useBlockChannel({ blocked: currentChannel?.blocked || false, association }); - if (!data?.contact?.unknown) { + if (dismissed || !data?.contact?.unknown) { return null; } return ( + setDismissed(true)} /> } > - {shouldShowSecurityRoute ? ( - - Add to contact list manually and - - enable verification - - using multi-factor authentication. - - ) : ( - t('Add_to_contact_list_manually') - )} +

{t('Unknown_contact_callout_description')}

); }; diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-unknown-callout.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-unknown-callout.spec.ts new file mode 100644 index 0000000000000..44335abad6cab --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-unknown-callout.spec.ts @@ -0,0 +1,69 @@ +import type { Page } from '@playwright/test'; + +import { createFakeVisitor } from '../../mocks/data'; +import { IS_EE } from '../config/constants'; +import { createAuxContext } from '../fixtures/createAuxContext'; +import { Users } from '../fixtures/userStates'; +import { OmnichannelLiveChat, HomeChannel } from '../page-objects'; +import { expect, test } from '../utils/test'; + +test.describe('OC - Contact Unknown Callout', () => { + test.skip(!IS_EE, 'Enterprise Only'); + + let poLiveChat: OmnichannelLiveChat; + let newVisitor: { email: string; name: string }; + + let agent: { page: Page; poHomeChannel: HomeChannel }; + + test.beforeAll(async ({ api, browser }) => { + newVisitor = createFakeVisitor(); + + await api.post('/livechat/users/agent', { username: 'user1' }); + await api.post('/livechat/users/manager', { username: 'user1' }); + + const { page } = await createAuxContext(browser, Users.user1); + agent = { page, poHomeChannel: new HomeChannel(page) }; + }); + test.beforeEach(async ({ page, api }) => { + poLiveChat = new OmnichannelLiveChat(page, api); + }); + + test.beforeEach('create livechat conversation', async ({ page }) => { + await page.goto('/livechat'); + await poLiveChat.openLiveChat(); + await poLiveChat.sendMessage(newVisitor, false); + await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor'); + await poLiveChat.btnSendMessageToOnlineAgent.click(); + }); + + test.afterEach('close livechat conversation', async () => { + await poLiveChat.closeChat(); + }); + + test.afterAll(async ({ api }) => { + await api.delete('/livechat/users/agent/user1'); + await api.delete('/livechat/users/manager/user1'); + await agent.page.close(); + }); + + test('OC - Contact Unknown Callout - Dismiss callout', async () => { + await test.step('expect to open conversation', async () => { + await agent.poHomeChannel.sidenav.openChat(newVisitor.name); + }); + + await test.step('expect contact unknown callout to be visible', async () => { + await expect(agent.poHomeChannel.content.contactUnknownCallout).toBeVisible(); + }); + + await test.step('expect to hide callout when dismiss is clicked', async () => { + await agent.poHomeChannel.content.btnDismissContactUnknownCallout.click(); + await expect(agent.poHomeChannel.content.contactUnknownCallout).not.toBeVisible(); + }); + + await test.step('expect keep callout hidden after changing pages', async () => { + await agent.poHomeChannel.sidenav.sidebarHomeAction.click(); + await agent.poHomeChannel.sidenav.openChat(newVisitor.name); + await expect(agent.poHomeChannel.content.contactUnknownCallout).not.toBeVisible(); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 08806275ef42e..cab59e8430693 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -463,4 +463,12 @@ export class HomeContent { get btnJoinChannel() { return this.page.getByRole('button', { name: 'Join channel' }); } + + get contactUnknownCallout() { + return this.page.getByRole('status', { name: 'Unknown contact. This contact is not on the contact list.' }); + } + + get btnDismissContactUnknownCallout() { + return this.contactUnknownCallout.getByRole('button', { name: 'Dismiss' }); + } } diff --git a/apps/meteor/tests/mocks/data.ts b/apps/meteor/tests/mocks/data.ts index d059d941abd6f..a651c8494335b 100644 --- a/apps/meteor/tests/mocks/data.ts +++ b/apps/meteor/tests/mocks/data.ts @@ -1,7 +1,17 @@ import { faker } from '@faker-js/faker'; import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from '@rocket.chat/apps-engine/client/definition'; -import { AppSubscriptionStatus } from '@rocket.chat/core-typings'; -import type { LicenseInfo, App, IMessage, IRoom, ISubscription, IUser } from '@rocket.chat/core-typings'; +import type { ILivechatContact } from '@rocket.chat/apps-engine/definition/livechat'; +import { AppSubscriptionStatus, OmnichannelSourceType } from '@rocket.chat/core-typings'; +import type { + LicenseInfo, + App, + IMessage, + IRoom, + ISubscription, + IUser, + ILivechatContactChannel, + Serialized, +} from '@rocket.chat/core-typings'; import { parse } from '@rocket.chat/message-parser'; import type { MessageWithMdEnforced } from '../../client/lib/parseMessageTextToAstMarkdown'; @@ -21,21 +31,22 @@ export function createFakeUser(overrides?: Partial): IUser { }; } -export const createFakeRoom = (overrides?: Partial): IRoom => ({ - _id: faker.database.mongodbObjectId(), - _updatedAt: faker.date.recent(), - t: faker.helpers.arrayElement(['c', 'p', 'd']), - msgs: faker.number.int({ min: 0 }), - u: { +export const createFakeRoom = (overrides?: Partial): T => + ({ _id: faker.database.mongodbObjectId(), - username: faker.internet.userName(), - name: faker.person.fullName(), - ...overrides?.u, - }, - usersCount: faker.number.int({ min: 0 }), - autoTranslateLanguage: faker.helpers.arrayElement(['en', 'es', 'pt', 'ar', 'it', 'ru', 'fr']), - ...overrides, -}); + _updatedAt: faker.date.recent(), + t: faker.helpers.arrayElement(['c', 'p', 'd']), + msgs: faker.number.int({ min: 0 }), + u: { + _id: faker.database.mongodbObjectId(), + username: faker.internet.userName(), + name: faker.person.fullName(), + ...overrides?.u, + }, + usersCount: faker.number.int({ min: 0 }), + autoTranslateLanguage: faker.helpers.arrayElement(['en', 'es', 'pt', 'ar', 'it', 'ru', 'fr']), + ...overrides, + }) as T; export const createFakeSubscription = (overrides?: Partial): ISubscription => ({ _id: faker.database.mongodbObjectId(), @@ -284,3 +295,38 @@ export function createFakeVisitor() { email: faker.internet.email(), } as const; } + +export function createFakeContactChannel(overrides?: Partial>): Serialized { + return { + name: 'widget', + blocked: false, + verified: false, + ...overrides, + visitor: { + visitorId: faker.string.uuid(), + source: { + type: OmnichannelSourceType.WIDGET, + }, + ...overrides?.visitor, + }, + details: { + type: OmnichannelSourceType.WIDGET, + destination: '', + ...overrides?.details, + }, + }; +} + +export function createFakeContact(overrides?: Partial>): Serialized { + return { + _id: faker.string.uuid(), + _updatedAt: new Date().toISOString(), + name: pullNextVisitorName(), + phones: [{ phoneNumber: faker.phone.number() }], + emails: [{ address: faker.internet.email() }], + unknown: true, + channels: [createFakeContactChannel()], + createdAt: new Date().toISOString(), + ...overrides, + }; +} diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 4f3c109638bc1..fee5d3ae26462 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1717,6 +1717,7 @@ "Daily_Active_Users": "Daily Active Users", "Display_unread_counter": "Display room as unread when there are unread messages", "Displays_action_text": "Displays action text", + "Dismiss": "Dismiss", "Data_modified": "Data Modified", "Do_not_display_unread_counter": "Do not display any counter of this channel", "Do_you_want_to_accept": "Do you want to accept?", @@ -5999,6 +6000,7 @@ "Unique_ID_change_detected": "Unique ID change detected", "Unknown_Import_State": "Unknown Import State", "Unknown_User": "Unknown User", + "Unknown_contact_callout_description": "Unknown contact. This contact is not on the contact list.", "Unlimited": "Unlimited", "Unmute": "Unmute", "unpinning-not-allowed": "Unpinning is not allowed", @@ -6778,11 +6780,8 @@ "Advanced_contact_profile": "Advanced contact profile", "Advanced_contact_profile_description": "Manage multiple emails and phone numbers for a single contact, enabling a comprehensive multi-channel history that keeps you well-informed and improves communication efficiency.", "Add_contact": "Add contact", - "Add_to_contact_list_manually": "Add to contact list manually", - "Add_to_contact_and_enable_verification_description": "Add to contact list manually and <1>enable verification using multi-factor authentication.", "Ask_enable_advanced_contact_profile": "Ask your workspace admin to enable advanced contact profile", "close-blocked-room-comment": "This channel has been blocked", - "Contact_unknown": "Contact unknown", "Review_contact": "Review contact", "See_conflicts": "See conflicts", "Conflicts_found": "Conflicts found", diff --git a/packages/i18n/src/locales/nb.i18n.json b/packages/i18n/src/locales/nb.i18n.json index 6c425ba6327c9..dda09ad8d3dd7 100644 --- a/packages/i18n/src/locales/nb.i18n.json +++ b/packages/i18n/src/locales/nb.i18n.json @@ -6185,11 +6185,8 @@ "Advanced_contact_profile": "Avansert kontaktprofil", "Advanced_contact_profile_description": "Administrer flere e-poster og telefonnumre for en enkelt kontakt, noe som muliggjør en omfattende flerkanalshistorikk som holder deg godt informert og forbedrer kommunikasjonseffektiviteten.", "Add_contact": "Legg til kontakt", - "Add_to_contact_list_manually": "Legg til i kontaktlisten manuelt", - "Add_to_contact_and_enable_verification_description": "Legg til i kontaktlisten manuelt og <1>aktiver verifisering ved hjelp av multifaktorautentisering.", "Ask_enable_advanced_contact_profile": "Be arbeidsområdeadministratoren din om å aktivere avansert kontaktprofil", "close-blocked-room-comment": "Denne kanalen er blokkert", - "Contact_unknown": "Ukjent kontakt", "Review_contact": "Gjennomgå kontakt", "See_conflicts": "Se konflikter", "Conflicts_found": "Konflikter funnet", diff --git a/packages/i18n/src/locales/nn.i18n.json b/packages/i18n/src/locales/nn.i18n.json index 1e73ad4e738d2..14c86c4f6c25c 100644 --- a/packages/i18n/src/locales/nn.i18n.json +++ b/packages/i18n/src/locales/nn.i18n.json @@ -6185,11 +6185,8 @@ "Advanced_contact_profile": "Avansert kontaktprofil", "Advanced_contact_profile_description": "Administrer flere e-poster og telefonnumre for en enkelt kontakt, noe som muliggjør en omfattende flerkanalshistorikk som holder deg godt informert og forbedrer kommunikasjonseffektiviteten.", "Add_contact": "Legg til kontakt", - "Add_to_contact_list_manually": "Legg til i kontaktlisten manuelt", - "Add_to_contact_and_enable_verification_description": "Legg til i kontaktlisten manuelt og <1>aktiver verifisering ved hjelp av multifaktorautentisering.", "Ask_enable_advanced_contact_profile": "Be arbeidsområdeadministratoren din om å aktivere avansert kontaktprofil", "close-blocked-room-comment": "Denne kanalen er blokkert", - "Contact_unknown": "Ukjent kontakt", "Review_contact": "Gjennomgå kontakt", "See_conflicts": "Se konflikter", "Conflicts_found": "Konflikter funnet", diff --git a/packages/i18n/src/locales/pt-BR.i18n.json b/packages/i18n/src/locales/pt-BR.i18n.json index fed94c6851d0f..67f050a51caba 100644 --- a/packages/i18n/src/locales/pt-BR.i18n.json +++ b/packages/i18n/src/locales/pt-BR.i18n.json @@ -324,8 +324,6 @@ "Add_members": "Adicionar membros", "Add_monitor": "Adicionar monitor", "Add_phone": "Adicionar número de telefone", - "Add_to_contact_and_enable_verification_description": "Adicione o contato na lista manualmente e <1>habilite a verificação usando autenticação de múltiplos fatores.", - "Add_to_contact_list_manually": "Adicione o contato na lista manualmente", "Add_user": "Adicionar usuário", "Add_users": "Adicionar usuários", "Added__username__to_team": "@{{user_added}} adicionado a esta equipe", @@ -927,7 +925,6 @@ "Contact_identification": "Identificação de contato", "Contact_not_found": "Contato não encontrado", "Contact_unblocked": "Contato desbloqueado", - "Contact_unknown": "Contato desconhecido", "Contacts": "Contatos", "Contains_Security_Fixes": "Contém correções de segurança", "Content": "Conteúdo", @@ -1402,6 +1399,7 @@ "Display_setting_permissions": "Exibir permissões para alterar configurações", "Display_unread_counter": "Exibir número de mensagens não lidas", "Displays_action_text": "Exibe texto da ação", + "Dismiss": "Dispensar", "Do_It_Later": "Fazer depois", "Do_Nothing": "Não fazer nada", "Do_not_display_unread_counter": "Não exibir nenhum contador desse canal", @@ -3971,6 +3969,7 @@ "Uninstall": "Desinstalar", "Unit_removed": "Unidade removida", "Unknown_Import_State": "Estado de importação desconhecido", + "Unknown_contact_callout_description": "Contato desconhecido. Este contato não está na lista de contatos.", "Unlimited": "Ilimitado", "Unmute": "Ativar o som", "Unmute_microphone": "Ativar o som do microfone",