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
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