Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .changeset/young-avocados-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/i18n": minor
---

Adds close action to contact unknown callout displayed within Livechat rooms
Original file line number Diff line number Diff line change
@@ -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<IOmnichannelRoom>({ 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(
<FakeRoomProvider roomOverrides={fakeRoom}>
<ComposerOmnichannelCallout />
</FakeRoomProvider>,
{ 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(
<FakeRoomProvider roomOverrides={fakeRoom}>
<ComposerOmnichannelCallout />
</FakeRoomProvider>,
{ 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(
<FakeRoomProvider roomOverrides={fakeRoom}>
<ComposerOmnichannelCallout />
</FakeRoomProvider>,
{ 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();
});
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 }) });

Expand All @@ -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 (
<Callout
role='status'
aria-labelledby={calloutDescriptionId}
mbe={16}
title={t('Contact_unknown')}
actions={
<ButtonGroup>
<Button onClick={() => navigate(`/live/${_id}/contact-profile/edit`)} small>
Expand All @@ -53,20 +49,11 @@ const ComposerOmnichannelCallout = () => {
<Button danger secondary small onClick={handleBlock}>
{currentChannel?.blocked ? t('Unblock') : t('Block')}
</Button>
<IconButton icon='cross' secondary small title={t('Dismiss')} onClick={() => setDismissed(true)} />
</ButtonGroup>
}
>
{shouldShowSecurityRoute ? (
<Trans i18nKey='Add_to_contact_and_enable_verification_description'>
Add to contact list manually and
<Box is={canViewSecurityPrivacy ? 'a' : 'span'} href={securityPrivacyRoute}>
enable verification
</Box>
using multi-factor authentication.
</Trans>
) : (
t('Add_to_contact_list_manually')
)}
<p id={calloutDescriptionId}>{t('Unknown_contact_callout_description')}</p>
</Callout>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
}
}
78 changes: 62 additions & 16 deletions apps/meteor/tests/mocks/data.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -21,21 +31,22 @@ export function createFakeUser(overrides?: Partial<IUser>): IUser {
};
}

export const createFakeRoom = (overrides?: Partial<IRoom & { retention?: { enabled: boolean } }>): 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 = <T extends IRoom = IRoom>(overrides?: Partial<T & { retention?: { enabled: boolean } }>): 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>): ISubscription => ({
_id: faker.database.mongodbObjectId(),
Expand Down Expand Up @@ -284,3 +295,38 @@ export function createFakeVisitor() {
email: faker.internet.email(),
} as const;
}

export function createFakeContactChannel(overrides?: Partial<Serialized<ILivechatContactChannel>>): Serialized<ILivechatContactChannel> {
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<ILivechatContact>>): Serialized<ILivechatContact> {
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,
};
}
5 changes: 2 additions & 3 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -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?",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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</1> 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",
Expand Down
3 changes: 0 additions & 3 deletions packages/i18n/src/locales/nb.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -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</1> 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",
Expand Down
3 changes: 0 additions & 3 deletions packages/i18n/src/locales/nn.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -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</1> 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",
Expand Down
Loading
Loading