Skip to content

Commit

Permalink
feat: Allow admins to decide if email transcript should be sent always (
Browse files Browse the repository at this point in the history
#32820)

Co-authored-by: Marcos Spessatto Defendi <[email protected]>
Co-authored-by: Guilherme Gazzo <[email protected]>
  • Loading branch information
3 people authored Jul 24, 2024
1 parent f5ea2ff commit 393e613
Show file tree
Hide file tree
Showing 15 changed files with 273 additions and 48 deletions.
5 changes: 5 additions & 0 deletions .changeset/shaggy-hats-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": minor
---

Added a new setting `Livechat_transcript_send_always` that allows admins to decide if email transcript should be sent all the times when a conversation is closed. This setting bypasses agent's preferences. For this setting to work, `Livechat_enable_transcript` should be off, meaning that visitors will no longer receive the option to decide if they want a transcript or not.
49 changes: 15 additions & 34 deletions apps/meteor/app/livechat/server/lib/LivechatTyped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ import { parseAgentCustomFields, updateDepartmentAgents, validateEmail, normaliz
import { QueueManager } from './QueueManager';
import { RoutingManager } from './RoutingManager';
import { isDepartmentCreationAvailable } from './isDepartmentCreationAvailable';
import type { CloseRoomParams, CloseRoomParamsByUser, CloseRoomParamsByVisitor } from './localTypes';
import { parseTranscriptRequest } from './parseTranscriptRequest';
import { sendTranscript as sendTranscriptFunc } from './sendTranscript';

type RegisterGuestType = Partial<Pick<ILivechatVisitor, 'token' | 'name' | 'department' | 'status' | 'username'>> & {
Expand All @@ -80,36 +82,6 @@ type RegisterGuestType = Partial<Pick<ILivechatVisitor, 'token' | 'name' | 'depa
phone?: { number: string };
};

type GenericCloseRoomParams = {
room: IOmnichannelRoom;
comment?: string;
options?: {
clientAction?: boolean;
tags?: string[];
emailTranscript?:
| {
sendToVisitor: false;
}
| {
sendToVisitor: true;
requestData: NonNullable<IOmnichannelRoom['transcriptRequest']>;
};
pdfTranscript?: {
requestedBy: string;
};
};
};

export type CloseRoomParamsByUser = {
user: IUser | null;
} & GenericCloseRoomParams;

export type CloseRoomParamsByVisitor = {
visitor: ILivechatVisitor;
} & GenericCloseRoomParams;

export type CloseRoomParams = CloseRoomParamsByUser | CloseRoomParamsByVisitor;

type OfflineMessageData = {
message: string;
name: string;
Expand Down Expand Up @@ -324,6 +296,9 @@ class LivechatClass {

this.logger.debug(`DB updated for room ${room._id}`);

const transcriptRequested =
!!transcriptRequest || (!settings.get('Livechat_enable_transcript') && settings.get('Livechat_transcript_send_always'));

// Retrieve the closed room
const newRoom = await LivechatRooms.findOneById(rid);

Expand All @@ -338,13 +313,15 @@ class LivechatClass {
t: 'livechat-close',
msg: comment,
groupable: false,
transcriptRequested: !!transcriptRequest,
transcriptRequested,
...(isRoomClosedByVisitorParams(params) && { token: chatCloser.token }),
},
newRoom,
);

await Message.saveSystemMessage('command', rid, 'promptTranscript', closeData.closedBy);
if (settings.get('Livechat_enable_transcript') && !settings.get('Livechat_transcript_send_always')) {
await Message.saveSystemMessage('command', rid, 'promptTranscript', closeData.closedBy);
}

this.logger.debug(`Running callbacks for room ${newRoom._id}`);

Expand All @@ -356,15 +333,18 @@ class LivechatClass {
void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.ILivechatRoomClosedHandler, newRoom);
void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatRoomClosed, newRoom);
});

const visitor = isRoomClosedByVisitorParams(params) ? params.visitor : undefined;
const opts = await parseTranscriptRequest(params.room, options, visitor);
if (process.env.TEST_MODE) {
await callbacks.run('livechat.closeRoom', {
room: newRoom,
options,
options: opts,
});
} else {
callbacks.runAsync('livechat.closeRoom', {
room: newRoom,
options,
options: opts,
});
}

Expand Down Expand Up @@ -1880,3 +1860,4 @@ class LivechatClass {
}

export const Livechat = new LivechatClass();
export * from './localTypes';
31 changes: 31 additions & 0 deletions apps/meteor/app/livechat/server/lib/localTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { IOmnichannelRoom, IUser, ILivechatVisitor } from '@rocket.chat/core-typings';

export type GenericCloseRoomParams = {
room: IOmnichannelRoom;
comment?: string;
options?: {
clientAction?: boolean;
tags?: string[];
emailTranscript?:
| {
sendToVisitor: false;
}
| {
sendToVisitor: true;
requestData: NonNullable<IOmnichannelRoom['transcriptRequest']>;
};
pdfTranscript?: {
requestedBy: string;
};
};
};

export type CloseRoomParamsByUser = {
user: IUser | null;
} & GenericCloseRoomParams;

export type CloseRoomParamsByVisitor = {
visitor: ILivechatVisitor;
} & GenericCloseRoomParams;

export type CloseRoomParams = CloseRoomParamsByUser | CloseRoomParamsByVisitor;
61 changes: 61 additions & 0 deletions apps/meteor/app/livechat/server/lib/parseTranscriptRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { ILivechatVisitor, IOmnichannelRoom, IUser } from '@rocket.chat/core-typings';
import { LivechatVisitors, Users } from '@rocket.chat/models';

import { settings } from '../../../settings/server';
import type { CloseRoomParams } from './localTypes';

export const parseTranscriptRequest = async (
room: IOmnichannelRoom,
options: CloseRoomParams['options'],
visitor?: ILivechatVisitor,
user?: IUser,
): Promise<CloseRoomParams['options']> => {
const visitorDecideTranscript = settings.get<boolean>('Livechat_enable_transcript');
// visitor decides, no changes
if (visitorDecideTranscript) {
return options;
}

// send always is disabled, no changes
const sendAlways = settings.get<boolean>('Livechat_transcript_send_always');
if (!sendAlways) {
return options;
}

const visitorData =
visitor ||
(await LivechatVisitors.findOneById<Pick<ILivechatVisitor, 'visitorEmails'>>(room.v._id, { projection: { visitorEmails: 1 } }));
// no visitor, no changes
if (!visitorData) {
return options;
}
const visitorEmail = visitorData?.visitorEmails?.[0]?.address;
// visitor doesnt have email, no changes
if (!visitorEmail) {
return options;
}

const defOptions = { projection: { _id: 1, username: 1, name: 1 } };
const requestedBy =
user ||
(room.servedBy && (await Users.findOneById(room.servedBy._id, defOptions))) ||
(await Users.findOneById('rocket.cat', defOptions));

// no user available for backing request, no changes
if (!requestedBy) {
return options;
}

return {
...options,
emailTranscript: {
sendToVisitor: true,
requestData: {
email: visitorEmail,
requestedAt: new Date(),
subject: '',
requestedBy,
},
},
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const CloseChatModal = ({
} = useForm();

const commentRequired = useSetting('Livechat_request_comment_when_closing_conversation') as boolean;
const alwaysSendTranscript = useSetting<boolean>('Livechat_transcript_send_always');
const customSubject = useSetting<string>('Livechat_transcript_email_subject');
const [tagRequired, setTagRequired] = useState(false);

Expand All @@ -66,7 +67,7 @@ const CloseChatModal = ({
const transcriptPDFPermission = usePermission('request-pdf-transcript');
const transcriptEmailPermission = usePermission('send-omnichannel-chat-transcript');

const canSendTranscriptEmail = transcriptEmailPermission && visitorEmail;
const canSendTranscriptEmail = transcriptEmailPermission && visitorEmail && !alwaysSendTranscript;
const canSendTranscriptPDF = transcriptPDFPermission && hasLicense;
const canSendTranscript = canSendTranscriptEmail || canSendTranscriptPDF;

Expand All @@ -78,7 +79,7 @@ const CloseChatModal = ({
({ comment, tags, transcriptPDF, transcriptEmail, subject }): void => {
const preferences = {
omnichannelTranscriptPDF: !!transcriptPDF,
omnichannelTranscriptEmail: !!transcriptEmail,
omnichannelTranscriptEmail: alwaysSendTranscript ? true : !!transcriptEmail,
};
const requestData = transcriptEmail && visitorEmail ? { email: visitorEmail, subject } : undefined;

Expand All @@ -98,7 +99,7 @@ const CloseChatModal = ({
onConfirm(comment, tags, preferences, requestData);
}
},
[commentRequired, tagRequired, visitorEmail, errors, setError, t, onConfirm],
[commentRequired, tagRequired, visitorEmail, errors, setError, t, onConfirm, alwaysSendTranscript],
);

const cannotSubmit = useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ButtonGroup, Button, Box, Accordion } from '@rocket.chat/fuselage';
import { useToastMessageDispatch, useTranslation, useEndpoint, useUserPreference } from '@rocket.chat/ui-contexts';
import { useToastMessageDispatch, useTranslation, useEndpoint, useUserPreference, useSetting } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React from 'react';
import { useForm, FormProvider } from 'react-hook-form';
Expand All @@ -17,12 +17,17 @@ const OmnichannelPreferencesPage = (): ReactElement => {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();

const alwaysSendEmailTranscript = useSetting<boolean>('Livechat_transcript_send_always');
const omnichannelTranscriptPDF = useUserPreference<boolean>('omnichannelTranscriptPDF') ?? false;
const omnichannelTranscriptEmail = useUserPreference<boolean>('omnichannelTranscriptEmail') ?? false;
const omnichannelHideConversationAfterClosing = useUserPreference<boolean>('omnichannelHideConversationAfterClosing') ?? true;

const methods = useForm({
defaultValues: { omnichannelTranscriptPDF, omnichannelTranscriptEmail, omnichannelHideConversationAfterClosing },
defaultValues: {
omnichannelTranscriptPDF,
omnichannelTranscriptEmail: alwaysSendEmailTranscript || omnichannelTranscriptEmail,
omnichannelHideConversationAfterClosing,
},
});

const {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Accordion, Box, Field, FieldGroup, FieldLabel, FieldRow, FieldHint, Tag, ToggleSwitch } from '@rocket.chat/fuselage';
import { useUniqueId } from '@rocket.chat/fuselage-hooks';
import { useTranslation, usePermission } from '@rocket.chat/ui-contexts';
import { useTranslation, usePermission, useSetting } from '@rocket.chat/ui-contexts';
import React from 'react';
import { useFormContext } from 'react-hook-form';

Expand All @@ -12,8 +12,10 @@ const PreferencesConversationTranscript = () => {
const { register } = useFormContext();

const hasLicense = useHasLicenseModule('livechat-enterprise');
const alwaysSendEmailTranscript = useSetting('Livechat_transcript_send_always');
const canSendTranscriptPDF = usePermission('request-pdf-transcript');
const canSendTranscriptEmail = usePermission('send-omnichannel-chat-transcript');
const canSendTranscriptEmailPermission = usePermission('send-omnichannel-chat-transcript');
const canSendTranscriptEmail = canSendTranscriptEmailPermission && !alwaysSendEmailTranscript;
const cantSendTranscriptPDF = !canSendTranscriptPDF || !hasLicense;

const omnichannelTranscriptPDF = useUniqueId();
Expand Down Expand Up @@ -42,7 +44,7 @@ const PreferencesConversationTranscript = () => {
<FieldLabel htmlFor={omnichannelTranscriptEmail}>
<Box display='flex' alignItems='center'>
{t('Omnichannel_transcript_email')}
{!canSendTranscriptEmail && (
{!canSendTranscriptEmailPermission && (
<Box marginInline={4}>
<Tag>{t('No_permission')}</Tag>
</Box>
Expand Down
13 changes: 12 additions & 1 deletion apps/meteor/server/settings/omnichannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,12 +404,23 @@ export const createOmniSettings = () =>
enableQuery: [{ _id: 'FileUpload_Enabled', value: true }, omnichannelEnabledQuery],
});

// Making these 2 settings "depend" on each other
// Prevents us from having both as true and then asking visitor if it wants a Transcript
// But send it anyways because of send_always being enabled. So one can only be turned on
// if the other is off.
await this.add('Livechat_enable_transcript', false, {
type: 'boolean',
group: 'Omnichannel',
public: true,
i18nLabel: 'Transcript_Enabled',
enableQuery: omnichannelEnabledQuery,
enableQuery: [{ _id: 'Livechat_transcript_send_always', value: false }, omnichannelEnabledQuery],
});

await this.add('Livechat_transcript_send_always', false, {
type: 'boolean',
group: 'Omnichannel',
public: true,
enableQuery: [{ _id: 'Livechat_enable_transcript', value: false }, omnichannelEnabledQuery],
});

await this.add('Livechat_transcript_show_system_messages', false, {
Expand Down
Loading

0 comments on commit 393e613

Please sign in to comment.