From 6a3ed7d4ec8c6ac8dfe142a3dd1a41171edb27ee Mon Sep 17 00:00:00 2001 From: Filipe Marins Date: Mon, 27 Mar 2023 18:14:21 -0300 Subject: [PATCH] fix: sound notification for queued chats with manual routing (#28411) Co-authored-by: Kevin Aleman <11577696+KevLehman@users.noreply.github.com> --- .../lib/stream/{inquiry.js => inquiry.ts} | 0 .../{queueManager.js => queueManager.ts} | 67 ++++++++----------- .../client/providers/OmnichannelProvider.tsx | 19 +++++- .../client/sidebar/hooks/useRoomList.ts | 6 +- .../ee/server/models/LivechatInquiry.ts | 3 +- .../src/OmichannelRoutingConfig.ts | 4 +- 6 files changed, 52 insertions(+), 47 deletions(-) rename apps/meteor/app/livechat/client/lib/stream/{inquiry.js => inquiry.ts} (100%) rename apps/meteor/app/livechat/client/lib/stream/{queueManager.js => queueManager.ts} (58%) diff --git a/apps/meteor/app/livechat/client/lib/stream/inquiry.js b/apps/meteor/app/livechat/client/lib/stream/inquiry.ts similarity index 100% rename from apps/meteor/app/livechat/client/lib/stream/inquiry.js rename to apps/meteor/app/livechat/client/lib/stream/inquiry.ts diff --git a/apps/meteor/app/livechat/client/lib/stream/queueManager.js b/apps/meteor/app/livechat/client/lib/stream/queueManager.ts similarity index 58% rename from apps/meteor/app/livechat/client/lib/stream/queueManager.js rename to apps/meteor/app/livechat/client/lib/stream/queueManager.ts index 7930cb49087..bcfad362b2e 100644 --- a/apps/meteor/app/livechat/client/lib/stream/queueManager.js +++ b/apps/meteor/app/livechat/client/lib/stream/queueManager.ts @@ -1,73 +1,61 @@ -import { Meteor } from 'meteor/meteor'; +import type { ILivechatDepartment, ILivechatInquiryRecord, IOmnichannelAgent } from '@rocket.chat/core-typings'; -import { APIClient, getUserPreference } from '../../../../utils/client'; +import { APIClient } from '../../../../utils/client'; import { LivechatInquiry } from '../../collections/LivechatInquiry'; import { inquiryDataStream } from './inquiry'; import { callWithErrorHandling } from '../../../../../client/lib/utils/callWithErrorHandling'; -import { CustomSounds } from '../../../../custom-sounds/client/lib/CustomSounds'; const departments = new Set(); -const newInquirySound = () => { - const userId = Meteor.userId(); - const audioVolume = getUserPreference(userId, 'notificationsSoundVolume'); - const newRoomNotification = getUserPreference(userId, 'newRoomNotification'); - - if (newRoomNotification !== 'none') { - CustomSounds.play(newRoomNotification, { - volume: Number((audioVolume / 100).toPrecision(2)), - }); - } -}; +type ILivechatInquiryWithType = ILivechatInquiryRecord & { type?: 'added' | 'removed' | 'changed' }; const events = { - added: (inquiry) => { + added: (inquiry: ILivechatInquiryWithType) => { delete inquiry.type; departments.has(inquiry.department) && LivechatInquiry.insert({ ...inquiry, alert: true, _updatedAt: new Date(inquiry._updatedAt) }); - newInquirySound(); }, - changed: (inquiry) => { + changed: (inquiry: ILivechatInquiryWithType) => { if (inquiry.status !== 'queued' || (inquiry.department && !departments.has(inquiry.department))) { return LivechatInquiry.remove(inquiry._id); } delete inquiry.type; - const saveResult = LivechatInquiry.upsert({ _id: inquiry._id }, { ...inquiry, alert: true, _updatedAt: new Date(inquiry._updatedAt) }); - if (saveResult?.insertedId) { - newInquirySound(); - } + LivechatInquiry.upsert({ _id: inquiry._id }, { ...inquiry, alert: true, _updatedAt: new Date(inquiry._updatedAt) }); }, - removed: (inquiry) => LivechatInquiry.remove(inquiry._id), + removed: (inquiry: ILivechatInquiryWithType) => LivechatInquiry.remove(inquiry._id), }; -const updateCollection = (inquiry) => { +const updateCollection = (inquiry: ILivechatInquiryWithType) => { + if (!inquiry.type) { + return; + } events[inquiry.type](inquiry); }; const getInquiriesFromAPI = async () => { - const { inquiries } = await APIClient.get('/v1/livechat/inquiries.queuedForUser'); + const { inquiries } = await APIClient.get('/v1/livechat/inquiries.queuedForUser', {}); return inquiries; }; -const removeListenerOfDepartment = (departmentId) => { +const removeListenerOfDepartment = (departmentId: ILivechatDepartment['_id']) => { inquiryDataStream.removeListener(`department/${departmentId}`, updateCollection); departments.delete(departmentId); }; -const appendListenerToDepartment = (departmentId) => { +const appendListenerToDepartment = (departmentId: ILivechatDepartment['_id']) => { departments.add(departmentId); inquiryDataStream.on(`department/${departmentId}`, updateCollection); return () => removeListenerOfDepartment(departmentId); }; -const addListenerForeachDepartment = (departments = []) => { +const addListenerForeachDepartment = (departments: ILivechatDepartment['_id'][] = []) => { const cleanupFunctions = departments.map((department) => appendListenerToDepartment(department)); return () => cleanupFunctions.forEach((cleanup) => cleanup()); }; -const updateInquiries = async (inquiries = []) => +const updateInquiries = async (inquiries: ILivechatInquiryRecord[] = []) => inquiries.forEach((inquiry) => LivechatInquiry.upsert({ _id: inquiry._id }, { ...inquiry, _updatedAt: new Date(inquiry._updatedAt) })); -const getAgentsDepartments = async (userId) => { - const { departments } = await APIClient.get(`/v1/livechat/agents/${userId}/departments`, { enabledDepartmentsOnly: true }); +const getAgentsDepartments = async (userId: IOmnichannelAgent['_id']) => { + const { departments } = await APIClient.get(`/v1/livechat/agents/${userId}/departments`, { enabledDepartmentsOnly: 'true' }); return departments; }; @@ -78,9 +66,9 @@ const addGlobalListener = () => { return removeGlobalListener; }; -const subscribe = async (userId) => { +const subscribe = async (userId: IOmnichannelAgent['_id']) => { const config = await callWithErrorHandling('livechat:getRoutingConfig'); - if (config && config.autoAssignAgent) { + if (config?.autoAssignAgent) { return; } @@ -89,23 +77,24 @@ const subscribe = async (userId) => { // Register to all depts + public queue always to match the inquiry list returned by backend const cleanDepartmentListeners = addListenerForeachDepartment(agentDepartments); const globalCleanup = addGlobalListener(); + const inquiriesFromAPI = (await getInquiriesFromAPI()) as unknown as ILivechatInquiryRecord[]; - updateInquiries(await getInquiriesFromAPI()); + await updateInquiries(inquiriesFromAPI); return () => { LivechatInquiry.remove({}); removeGlobalListener(); - cleanDepartmentListeners && cleanDepartmentListeners(); - globalCleanup && globalCleanup(); + cleanDepartmentListeners?.(); + globalCleanup?.(); departments.clear(); }; }; export const initializeLivechatInquiryStream = (() => { - let cleanUp; + let cleanUp: (() => void) | undefined; - return async (...args) => { - cleanUp && cleanUp(); - cleanUp = await subscribe(...args); + return async (...args: any[]) => { + cleanUp?.(); + cleanUp = await subscribe(...(args as [IOmnichannelAgent['_id']])); }; })(); diff --git a/apps/meteor/client/providers/OmnichannelProvider.tsx b/apps/meteor/client/providers/OmnichannelProvider.tsx index fcbcec2a135..eaac4f4fa09 100644 --- a/apps/meteor/client/providers/OmnichannelProvider.tsx +++ b/apps/meteor/client/providers/OmnichannelProvider.tsx @@ -1,4 +1,9 @@ -import type { IOmnichannelAgent, IRoom, OmichannelRoutingConfig, OmnichannelSortingMechanismSettingType } from '@rocket.chat/core-typings'; +import type { + IOmnichannelAgent, + OmichannelRoutingConfig, + OmnichannelSortingMechanismSettingType, + ILivechatInquiryRecord, +} from '@rocket.chat/core-typings'; import { useSafely } from '@rocket.chat/fuselage-hooks'; import { useUser, useSetting, usePermission, useMethod, useEndpoint, useStream } from '@rocket.chat/ui-contexts'; import { useQuery, useQueryClient } from '@tanstack/react-query'; @@ -9,6 +14,7 @@ import { LivechatInquiry } from '../../app/livechat/client/collections/LivechatI import { initializeLivechatInquiryStream } from '../../app/livechat/client/lib/stream/queueManager'; import { getOmniChatSortQuery } from '../../app/livechat/lib/inquiries'; import { Notifications } from '../../app/notifications/client'; +import { KonchatNotification } from '../../app/ui/client'; import { useHasLicenseModule } from '../../ee/client/hooks/useHasLicenseModule'; import { ClientLogger } from '../../lib/ClientLogger'; import type { OmnichannelContextValue } from '../contexts/OmnichannelContext'; @@ -47,6 +53,7 @@ const OmnichannelProvider: FC = ({ children }) => { const getRoutingConfig = useMethod('livechat:getRoutingConfig'); const [routeConfig, setRouteConfig] = useSafely(useState(undefined)); + const [queueNotification, setQueueNotification] = useState(new Set()); const accessible = hasAccess && omniChannelEnabled; const iceServersSetting: any = useSetting('WebRTC_Servers'); @@ -116,7 +123,7 @@ const OmnichannelProvider: FC = ({ children }) => { }; }, [manuallySelected, user?._id]); - const queue = useReactiveValue( + const queue = useReactiveValue( useCallback(() => { if (!manuallySelected) { return undefined; @@ -135,6 +142,14 @@ const OmnichannelProvider: FC = ({ children }) => { }, [manuallySelected, omnichannelPoolMaxIncoming, omnichannelSortingMechanism, user?._id]), ); + queue?.map(({ rid }) => { + if (queueNotification.has(rid)) { + return; + } + setQueueNotification((prev) => new Set([...prev, rid])); + return KonchatNotification.newRoom(rid); + }); + const contextValue = useMemo(() => { if (!enabled) { return emptyContextValue; diff --git a/apps/meteor/client/sidebar/hooks/useRoomList.ts b/apps/meteor/client/sidebar/hooks/useRoomList.ts index 2175e0941fd..436c7c1dc71 100644 --- a/apps/meteor/client/sidebar/hooks/useRoomList.ts +++ b/apps/meteor/client/sidebar/hooks/useRoomList.ts @@ -1,4 +1,4 @@ -import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; +import type { ILivechatInquiryRecord, IRoom, ISubscription } from '@rocket.chat/core-typings'; import { useDebouncedState } from '@rocket.chat/fuselage-hooks'; import { useUserPreference, useUserSubscriptions, useSetting } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; @@ -10,7 +10,7 @@ import { useQueryOptions } from './useQueryOptions'; const query = { open: { $ne: false } }; -const emptyQueue: IRoom[] = []; +const emptyQueue: ILivechatInquiryRecord[] = []; export const useRoomList = (): Array => { const [roomList, setRoomList] = useDebouncedState<(ISubscription & IRoom)[]>([], 150); @@ -29,7 +29,7 @@ export const useRoomList = (): Array => { const incomingCalls = useVideoConfIncomingCalls(); - let queue: IRoom[] = emptyQueue; + let queue = emptyQueue; if (inquiries.enabled) { queue = inquiries.queue; } diff --git a/apps/meteor/ee/server/models/LivechatInquiry.ts b/apps/meteor/ee/server/models/LivechatInquiry.ts index 810f7bbc166..73392b70379 100644 --- a/apps/meteor/ee/server/models/LivechatInquiry.ts +++ b/apps/meteor/ee/server/models/LivechatInquiry.ts @@ -1,6 +1,7 @@ import { registerModel } from '@rocket.chat/models'; +import { trashCollection } from '../../../server/database/trash'; import { db } from '../../../server/database/utils'; import { LivechatInquiryRawEE } from './raw/LivechatInquiry'; -registerModel('ILivechatInquiryModel', new LivechatInquiryRawEE(db)); +registerModel('ILivechatInquiryModel', new LivechatInquiryRawEE(db, trashCollection)); diff --git a/packages/core-typings/src/OmichannelRoutingConfig.ts b/packages/core-typings/src/OmichannelRoutingConfig.ts index 43d1344ff5c..2868cf1c47d 100644 --- a/packages/core-typings/src/OmichannelRoutingConfig.ts +++ b/packages/core-typings/src/OmichannelRoutingConfig.ts @@ -1,4 +1,4 @@ -import type { IRoom } from './IRoom'; +import type { ILivechatInquiryRecord } from './IInquiry'; export type OmichannelRoutingConfig = { previewRoom: boolean; @@ -13,7 +13,7 @@ export type OmichannelRoutingConfig = { export type Inquiries = | { enabled: true; - queue: Array; + queue: Array; } | { enabled: false;