diff --git a/.docker/Dockerfile.rhel b/.docker/Dockerfile.rhel index e1dbdf4224c8a..3d57e08b5b72c 100644 --- a/.docker/Dockerfile.rhel +++ b/.docker/Dockerfile.rhel @@ -1,6 +1,6 @@ FROM registry.access.redhat.com/ubi8/nodejs-12 -ENV RC_VERSION 4.5.2 +ENV RC_VERSION 4.5.3 MAINTAINER buildmaster@rocket.chat diff --git a/.github/history.json b/.github/history.json index 2380269cdb767..84aed3e1e3a7e 100644 --- a/.github/history.json +++ b/.github/history.json @@ -71572,6 +71572,85 @@ ] } ] + }, + "4.5.3": { + "node_version": "14.18.3", + "npm_version": "6.14.15", + "apps_engine_version": "1.31.0", + "mongo_versions": [ + "'3.6'", + "'4.0'", + "'4.2'", + "'4.4'", + "'5.0'" + ], + "pull_requests": [ + { + "pr": "24864", + "title": "[FIX] Disable voip button when call is in progress", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24863", + "title": "[FIX] Broken build caused by PRs modifying same file differently", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman", + "tiagoevanp" + ] + }, + { + "pr": "24838", + "title": "[FIX] [VOIP] SidebarFooter component ", + "userLogin": "tiagoevanp", + "description": "- Improve the CallProvider code;\r\n- Adjust the text case of the VoIP component on the FooterSidebar;\r\n- Fix the bad behavior with the changes in queue's name.", + "milestone": "4.5.3", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "24837", + "title": "[IMPROVE] Standarize queue behavior for managers and agents when subscribing", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24789", + "title": "[FIX] VoIP button gets disabled whenever user status changes", + "userLogin": "amolghode1981", + "milestone": "4.5.3", + "contributors": [ + "amolghode1981" + ] + }, + { + "pr": "24799", + "title": "[FIX] Wrong param usage on queue summary call", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24829", + "title": "[FIX] Show only enabled departments on forward", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + } + ] } } } \ No newline at end of file diff --git a/.snapcraft/resources/prepareRocketChat b/.snapcraft/resources/prepareRocketChat index c30d21c60ee21..32691c4952165 100755 --- a/.snapcraft/resources/prepareRocketChat +++ b/.snapcraft/resources/prepareRocketChat @@ -1,6 +1,6 @@ #!/bin/bash -curl -SLf "https://releases.rocket.chat/4.5.2/download/" -o rocket.chat.tgz +curl -SLf "https://releases.rocket.chat/4.5.3/download/" -o rocket.chat.tgz tar xf rocket.chat.tgz --strip 1 diff --git a/.snapcraft/snap/snapcraft.yaml b/.snapcraft/snap/snapcraft.yaml index 5d1cc3b31d052..ee183e7ae98e9 100644 --- a/.snapcraft/snap/snapcraft.yaml +++ b/.snapcraft/snap/snapcraft.yaml @@ -7,7 +7,7 @@ # 5. `snapcraft snap` name: rocketchat-server -version: 4.5.2 +version: 4.5.3 summary: Rocket.Chat server description: Have your own Slack like online chat, built with Meteor. https://rocket.chat/ confinement: strict diff --git a/HISTORY.md b/HISTORY.md index ce74d1516cac4..3e4d804cbc566 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,11 +1,50 @@ +# 4.5.3 +`2022-03-18 ยท 1 ๐Ÿš€ ยท 6 ๐Ÿ› ยท 3 ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป` + +### Engine versions +- Node: `14.18.3` +- NPM: `6.14.15` +- MongoDB: `'3.6', '4.0', '4.2', '4.4', '5.0'` +- Apps-Engine: `1.31.0` + +### ๐Ÿš€ Improvements + + +- Standarize queue behavior for managers and agents when subscribing ([#24837](https://github.com/RocketChat/Rocket.Chat/pull/24837)) + +### ๐Ÿ› Bug fixes + + +- **VOIP:** SidebarFooter component ([#24838](https://github.com/RocketChat/Rocket.Chat/pull/24838)) + + - Improve the CallProvider code; + - Adjust the text case of the VoIP component on the FooterSidebar; + - Fix the bad behavior with the changes in queue's name. + +- Broken build caused by PRs modifying same file differently ([#24863](https://github.com/RocketChat/Rocket.Chat/pull/24863)) + +- Disable voip button when call is in progress ([#24864](https://github.com/RocketChat/Rocket.Chat/pull/24864)) + +- Show only enabled departments on forward ([#24829](https://github.com/RocketChat/Rocket.Chat/pull/24829)) + +- VoIP button gets disabled whenever user status changes ([#24789](https://github.com/RocketChat/Rocket.Chat/pull/24789)) + +- Wrong param usage on queue summary call ([#24799](https://github.com/RocketChat/Rocket.Chat/pull/24799)) + +### ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป Core Team ๐Ÿค“ + +- [@KevLehman](https://github.com/KevLehman) +- [@amolghode1981](https://github.com/amolghode1981) +- [@tiagoevanp](https://github.com/tiagoevanp) + # 4.5.2 `2022-03-12 ยท 1 ๐Ÿš€ ยท 7 ๐Ÿ› ยท 1 ๐Ÿ” ยท 8 ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป` ### Engine versions - Node: `14.18.3` - NPM: `6.14.15` -- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` +- MongoDB: `'3.6', '4.0', '4.2', '4.4', '5.0'` - Apps-Engine: `1.31.0` ### ๐Ÿš€ Improvements @@ -140,7 +179,7 @@ ### ๐ŸŽ‰ New features -- E2E password generator ([#24114](https://github.com/RocketChat/Rocket.Chat/pull/24114)) +- E2E password generator ([#24114](https://github.com/RocketChat/Rocket.Chat/pull/24114) by [@eduardofcabrera](https://github.com/eduardofcabrera)) - Marketplace sort filter ([#24567](https://github.com/RocketChat/Rocket.Chat/pull/24567)) @@ -170,7 +209,7 @@ - **ENTERPRISE:** Improve how micro services are loaded ([#24388](https://github.com/RocketChat/Rocket.Chat/pull/24388)) -- Add return button in chats opened from the list of current chats ([#24458](https://github.com/RocketChat/Rocket.Chat/pull/24458)) +- Add return button in chats opened from the list of current chats ([#24458](https://github.com/RocketChat/Rocket.Chat/pull/24458) by [@LucasFASouza](https://github.com/LucasFASouza)) The new return button for Omnichannel chats came out with release 3.15 but the feature was only available for chats that were opened from Omnichannel Contact Center. Now, the same UI/UX is supported for chats opened from Current Chats list. @@ -181,7 +220,7 @@ ![image](https://user-images.githubusercontent.com/32396925/153285591-fad8e4a0-d2ea-4a02-8b2a-15e383b3c876.png) -- Add tooltips on action buttons of Canned Response message composer ([#24483](https://github.com/RocketChat/Rocket.Chat/pull/24483)) +- Add tooltips on action buttons of Canned Response message composer ([#24483](https://github.com/RocketChat/Rocket.Chat/pull/24483) by [@LucasFASouza](https://github.com/LucasFASouza)) The tooltips were missing on the action buttons of CR message composer. @@ -196,7 +235,7 @@ - Added a new "All" tab which shows all integrations in Integrations ([#24109](https://github.com/RocketChat/Rocket.Chat/pull/24109) by [@aswinidev](https://github.com/aswinidev)) -- ChatBox Text to File Description ([#24451](https://github.com/RocketChat/Rocket.Chat/pull/24451)) +- ChatBox Text to File Description ([#24451](https://github.com/RocketChat/Rocket.Chat/pull/24451) by [@eduardofcabrera](https://github.com/eduardofcabrera)) The text content from chatbox goes to the file description when drag and drop a file. @@ -212,7 +251,7 @@ ### after ![Screen Shot 2022-01-28 at 13 32 02](https://user-images.githubusercontent.com/27704687/151585101-75b98502-9aae-4198-bc3e-4956750e5d8b.png) -- Convert tag edit with department data to tsx ([#24369](https://github.com/RocketChat/Rocket.Chat/pull/24369)) +- Convert tag edit with department data to tsx ([#24369](https://github.com/RocketChat/Rocket.Chat/pull/24369) by [@LucasFASouza](https://github.com/LucasFASouza)) - Descriptive tooltip for Encrypted Key on Room Header ([#24121](https://github.com/RocketChat/Rocket.Chat/pull/24121)) @@ -347,7 +386,7 @@ Improved type checking for decorator `twoFactorRequired`. -- Chore: Add description to global OTR setting ([#24333](https://github.com/RocketChat/Rocket.Chat/pull/24333)) +- Chore: Add description to global OTR setting ([#24333](https://github.com/RocketChat/Rocket.Chat/pull/24333) by [@pedrogssouza](https://github.com/pedrogssouza)) - Chore: Bump Fuselage packages ([#24573](https://github.com/RocketChat/Rocket.Chat/pull/24573)) @@ -359,23 +398,23 @@ This pull request converts 26 more files from Javascript to Typescript, to check variable types and increase validation on the code. -- Chore: Convert to typescript the me slashCommands files ([#24321](https://github.com/RocketChat/Rocket.Chat/pull/24321)) +- Chore: Convert to typescript the me slashCommands files ([#24321](https://github.com/RocketChat/Rocket.Chat/pull/24321) by [@eduardofcabrera](https://github.com/eduardofcabrera)) Convert to typescript the me slashCommands files -- Chore: Convert to typescript the mute and unmute slash commands files ([#24325](https://github.com/RocketChat/Rocket.Chat/pull/24325)) +- Chore: Convert to typescript the mute and unmute slash commands files ([#24325](https://github.com/RocketChat/Rocket.Chat/pull/24325) by [@eduardofcabrera](https://github.com/eduardofcabrera)) Convert to typescript the mute and unmute slash commands files -- Chore: Convert to typescript the slash commands create files ([#24306](https://github.com/RocketChat/Rocket.Chat/pull/24306)) +- Chore: Convert to typescript the slash commands create files ([#24306](https://github.com/RocketChat/Rocket.Chat/pull/24306) by [@eduardofcabrera](https://github.com/eduardofcabrera)) Convert Slash Commands create files to typescript. -- Chore: Convert to typescript the slash commands invite files ([#24311](https://github.com/RocketChat/Rocket.Chat/pull/24311)) +- Chore: Convert to typescript the slash commands invite files ([#24311](https://github.com/RocketChat/Rocket.Chat/pull/24311) by [@eduardofcabrera](https://github.com/eduardofcabrera)) Convert to typescript the slash commands invite files -- Chore: Convert to typescript the unarchive slash commands files ([#24331](https://github.com/RocketChat/Rocket.Chat/pull/24331)) +- Chore: Convert to typescript the unarchive slash commands files ([#24331](https://github.com/RocketChat/Rocket.Chat/pull/24331) by [@eduardofcabrera](https://github.com/eduardofcabrera)) Convert to typescript the unarchive slash commands files @@ -385,7 +424,7 @@ - Chore: Improve PR title validation regex ([#24467](https://github.com/RocketChat/Rocket.Chat/pull/24467)) -- Chore: Js to ts slash commands archive ([#24304](https://github.com/RocketChat/Rocket.Chat/pull/24304)) +- Chore: Js to ts slash commands archive ([#24304](https://github.com/RocketChat/Rocket.Chat/pull/24304) by [@eduardofcabrera](https://github.com/eduardofcabrera)) Convert Slash Commands archive files to typescript @@ -496,20 +535,21 @@ ### ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป Contributors ๐Ÿ˜ +- [@LucasFASouza](https://github.com/LucasFASouza) - [@aswinidev](https://github.com/aswinidev) - [@dependabot[bot]](https://github.com/dependabot[bot]) +- [@eduardofcabrera](https://github.com/eduardofcabrera) +- [@pedrogssouza](https://github.com/pedrogssouza) ### ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป Core Team ๐Ÿค“ - [@KevLehman](https://github.com/KevLehman) -- [@LucasFASouza](https://github.com/LucasFASouza) - [@MartinSchoeler](https://github.com/MartinSchoeler) - [@albuquerquefabio](https://github.com/albuquerquefabio) - [@amolghode1981](https://github.com/amolghode1981) - [@d-gubert](https://github.com/d-gubert) - [@debdutdeb](https://github.com/debdutdeb) - [@dougfabris](https://github.com/dougfabris) -- [@eduardofcabrera](https://github.com/eduardofcabrera) - [@felipe-rod123](https://github.com/felipe-rod123) - [@filipemarins](https://github.com/filipemarins) - [@gabriellsh](https://github.com/gabriellsh) @@ -519,7 +559,6 @@ - [@matheusbsilva137](https://github.com/matheusbsilva137) - [@murtaza98](https://github.com/murtaza98) - [@ostjen](https://github.com/ostjen) -- [@pedrogssouza](https://github.com/pedrogssouza) - [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) - [@renatobecker](https://github.com/renatobecker) - [@rique223](https://github.com/rique223) @@ -871,7 +910,7 @@ It replaces some templates used by login and invitation flows with React components. It also drops `main` template, allowing `appLayout` to just handle components now. -- Chore: Slash Commands Join to Typescript ([#24254](https://github.com/RocketChat/Rocket.Chat/pull/24254)) +- Chore: Slash Commands Join to Typescript ([#24254](https://github.com/RocketChat/Rocket.Chat/pull/24254) by [@eduardofcabrera](https://github.com/eduardofcabrera)) Convert the slash commands .js files to .ts files. @@ -952,6 +991,7 @@ - [@arshxyz](https://github.com/arshxyz) - [@aswinidev](https://github.com/aswinidev) - [@dependabot[bot]](https://github.com/dependabot[bot]) +- [@eduardofcabrera](https://github.com/eduardofcabrera) - [@grahhnt](https://github.com/grahhnt) - [@mbreslein-thd](https://github.com/mbreslein-thd) - [@nishant23122000](https://github.com/nishant23122000) @@ -967,7 +1007,6 @@ - [@d-gubert](https://github.com/d-gubert) - [@debdutdeb](https://github.com/debdutdeb) - [@dougfabris](https://github.com/dougfabris) -- [@eduardofcabrera](https://github.com/eduardofcabrera) - [@gabriellsh](https://github.com/gabriellsh) - [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) @@ -22421,4 +22460,4 @@ - [@graywolf336](https://github.com/graywolf336) - [@marceloschmidt](https://github.com/marceloschmidt) - [@rodrigok](https://github.com/rodrigok) -- [@sampaiodiego](https://github.com/sampaiodiego) +- [@sampaiodiego](https://github.com/sampaiodiego) \ No newline at end of file diff --git a/app/livechat/client/lib/stream/queueManager.js b/app/livechat/client/lib/stream/queueManager.js index a63979ff19ad6..f76e5679d2795 100644 --- a/app/livechat/client/lib/stream/queueManager.js +++ b/app/livechat/client/lib/stream/queueManager.js @@ -45,7 +45,7 @@ const updateCollection = (inquiry) => { }; const getInquiriesFromAPI = async () => { - const { inquiries } = await APIClient.v1.get('livechat/inquiries.queued?sort={"ts": 1}'); + const { inquiries } = await APIClient.v1.get('livechat/inquiries.queuedForUser?sort={"ts": 1}'); return inquiries; }; @@ -59,7 +59,7 @@ const appendListenerToDepartment = (departmentId) => { inquiryDataStream.on(`department/${departmentId}`, updateCollection); return () => removeListenerOfDepartment(departmentId); }; -const addListenerForeachDepartment = async (departments = []) => { +const addListenerForeachDepartment = (departments = []) => { const cleanupFunctions = departments.map((department) => appendListenerToDepartment(department)); return () => cleanupFunctions.forEach((cleanup) => cleanup()); }; @@ -87,14 +87,17 @@ const subscribe = async (userId) => { const agentDepartments = (await getAgentsDepartments(userId)).map((department) => department.departmentId); - const cleanUp = agentDepartments.length ? await addListenerForeachDepartment(agentDepartments) : addGlobalListener(); + // Register to all depts + public queue always to match the inquiry list returned by backend + const cleanDepartmentListeners = addListenerForeachDepartment(agentDepartments); + const globalCleanup = addGlobalListener(); updateInquiries(await getInquiriesFromAPI()); return () => { LivechatInquiry.remove({}); removeGlobalListener(); - cleanUp && cleanUp(); + cleanDepartmentListeners && cleanDepartmentListeners(); + globalCleanup && globalCleanup(); departments.clear(); }; }; diff --git a/app/livechat/client/views/app/tabbar/visitorForward.js b/app/livechat/client/views/app/tabbar/visitorForward.js index 6853c58e40a79..b591ed84e52d0 100644 --- a/app/livechat/client/views/app/tabbar/visitorForward.js +++ b/app/livechat/client/views/app/tabbar/visitorForward.js @@ -99,7 +99,7 @@ Template.visitorForward.onCreated(async function () { } }); - const { departments } = await APIClient.v1.get('livechat/department'); + const { departments } = await APIClient.v1.get('livechat/department?enabled=true'); this.departments.set(departments); }); diff --git a/app/livechat/imports/server/rest/inquiries.js b/app/livechat/imports/server/rest/inquiries.js index 41f6c421ea37e..09c92c681a15f 100644 --- a/app/livechat/imports/server/rest/inquiries.js +++ b/app/livechat/imports/server/rest/inquiries.js @@ -5,6 +5,7 @@ import { API } from '../../../../api/server'; import { hasPermission } from '../../../../authorization'; import { Users, LivechatDepartment, LivechatInquiry } from '../../../../models'; import { findInquiries, findOneInquiryByRoomId } from '../../../server/api/lib/inquiries'; +import { LivechatInquiryStatus } from '../../../../../definition/IInquiry'; API.v1.addRoute( 'livechat/inquiries.list', @@ -101,6 +102,31 @@ API.v1.addRoute( }, ); +API.v1.addRoute( + 'livechat/inquiries.queuedForUser', + { authRequired: true }, + { + async get() { + const { offset, count } = this.getPaginationItems(); + const { sort } = this.parseJsonQuery(); + const { department } = this.requestParams(); + + return API.v1.success( + await findInquiries({ + userId: this.userId, + filterDepartment: department, + status: LivechatInquiryStatus.QUEUED, + pagination: { + offset, + count, + sort, + }, + }), + ); + }, + }, +); + API.v1.addRoute( 'livechat/inquiries.getOne', { authRequired: true }, diff --git a/app/livechat/server/api/lib/inquiries.js b/app/livechat/server/api/lib/inquiries.js index b83f86317547a..2b113433fe0c0 100644 --- a/app/livechat/server/api/lib/inquiries.js +++ b/app/livechat/server/api/lib/inquiries.js @@ -1,6 +1,5 @@ import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { LivechatDepartmentAgents, LivechatDepartment, LivechatInquiry } from '../../../../models/server/raw'; -import { hasAnyRoleAsync } from '../../../../authorization/server/functions/hasRole'; const agentDepartments = async (userId) => { const agentDepartments = (await LivechatDepartmentAgents.findByAgentId(userId).toArray()).map(({ departmentId }) => departmentId); @@ -8,10 +7,6 @@ const agentDepartments = async (userId) => { }; const applyDepartmentRestrictions = async (userId, filterDepartment) => { - if (await hasAnyRoleAsync(userId, ['livechat-manager'])) { - return filterDepartment; - } - const allowedDepartments = await agentDepartments(userId); if (allowedDepartments && Array.isArray(allowedDepartments) && allowedDepartments.length > 0) { if (!filterDepartment) { @@ -48,6 +43,8 @@ export async function findInquiries({ userId, department: filterDepartment, stat $and: [{ defaultAgent: { $exists: true } }, { 'defaultAgent.agentId': userId }], }, { ...(department && { department }) }, + // Add _always_ the "public queue" to returned list of inquiries, even if agent already has departments + { department: { $exists: false } }, ], }; diff --git a/app/utils/rocketchat.info b/app/utils/rocketchat.info index 49de4d160a25a..0f4b5aeb791a1 100644 --- a/app/utils/rocketchat.info +++ b/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "4.5.2" + "version": "4.5.3" } diff --git a/client/components/Omnichannel/hooks/useDepartmentsList.ts b/client/components/Omnichannel/hooks/useDepartmentsList.ts index 1fd67f5367e6e..31a5c39fc65fd 100644 --- a/client/components/Omnichannel/hooks/useDepartmentsList.ts +++ b/client/components/Omnichannel/hooks/useDepartmentsList.ts @@ -14,6 +14,7 @@ type DepartmentsListOptions = { haveAll?: boolean; haveNone?: boolean; excludeDepartmentId?: string; + enabled?: boolean; }; export const useDepartmentsList = ( @@ -44,6 +45,7 @@ export const useDepartmentsList = ( count: end + start, sort: `{ "name": 1 }`, excludeDepartmentId: options.excludeDepartmentId, + enabled: options.enabled, }); const items = departments @@ -87,6 +89,7 @@ export const useDepartmentsList = ( options.onlyMyDepartments, options.haveNone, options.excludeDepartmentId, + options.enabled, t, ], ); diff --git a/client/components/Omnichannel/modals/ForwardChatModal.js b/client/components/Omnichannel/modals/ForwardChatModal.js index 68ae71f86490a..ea151f7a483bc 100644 --- a/client/components/Omnichannel/modals/ForwardChatModal.js +++ b/client/components/Omnichannel/modals/ForwardChatModal.js @@ -32,7 +32,7 @@ const ForwardChatModal = ({ onForward, onCancel, room, ...props }) => { const debouncedDepartmentsFilter = useDebouncedValue(departmentsFilter, 500); const { itemsList: departmentsList, loadMoreItems: loadMoreDepartments } = useDepartmentsList( - useMemo(() => ({ filter: debouncedDepartmentsFilter }), [debouncedDepartmentsFilter]), + useMemo(() => ({ filter: debouncedDepartmentsFilter, enabled: true }), [debouncedDepartmentsFilter]), ); const { phase: departmentsPhase, items: departmentsItems, itemCount: departmentsTotal } = useRecordList(departmentsList); diff --git a/client/lib/voip/QueueAggregator.ts b/client/lib/voip/QueueAggregator.ts index e0facda36b139..676517eb1f86b 100644 --- a/client/lib/voip/QueueAggregator.ts +++ b/client/lib/voip/QueueAggregator.ts @@ -28,7 +28,7 @@ export class QueueAggregator { // Maintains the history of the queue that the agent has served private sessionQueueCallServingHistory: IQueueServingRecord[]; - private currentlyServing: IQueueServingRecord | undefined; + private currentlyServing: IQueueServingRecord; private currentQueueMembershipStatus: Record; @@ -97,17 +97,24 @@ export class QueueAggregator { return totalCallWaitingCount; } - callRinging(queueInfo: { queuename: string; callerId: { id: string; name: string } }): void { + getCurrentQueueName(): string { + if (this.currentlyServing.queueInfo) { + return this.currentlyServing.queueInfo.queueName; + } + + return ''; + } + + callRinging(queueInfo: { queuename: string; callerid: { id: string; name: string } }): void { if (!this.currentQueueMembershipStatus[queueInfo.queuename]) { - // something is wrong. Queue is not found in the membership details. return; } const queueServing: IQueueServingRecord = { queueInfo: this.currentQueueMembershipStatus[queueInfo.queuename], callerId: { - callerId: queueInfo.callerId.id, - callerName: queueInfo.callerId.name, + callerId: queueInfo.callerid.id, + callerName: queueInfo.callerid.name, }, callStarted: undefined, callEnded: undefined, diff --git a/client/lib/voip/VoIPUser.ts b/client/lib/voip/VoIPUser.ts index 9b6af9bfe438c..c823e7a0bd412 100644 --- a/client/lib/voip/VoIPUser.ts +++ b/client/lib/voip/VoIPUser.ts @@ -64,7 +64,7 @@ export class VoIPUser extends Emitter implements OutgoingRequestDele private mode: WorkflowTypes; - private queueInfo?: QueueAggregator; + private queueInfo: QueueAggregator; get callState(): CallStates { return this._callState; @@ -567,6 +567,9 @@ export class VoIPUser extends Emitter implements OutgoingRequestDele if (this._callState !== 'ANSWER_SENT' && this._callState !== 'IN_CALL' && this._callState !== 'ON_HOLD') { throw new Error(`Incorrect call State = ${this.callState}`); } + + // When call ends, force state to be revisited + this.emit('stateChanged'); switch (this.session.state) { case SessionState.Initial: if (this.session instanceof Invitation) { @@ -649,11 +652,15 @@ export class VoIPUser extends Emitter implements OutgoingRequestDele this.queueInfo?.setMembership(subscription); } - getAggregator(): QueueAggregator | undefined { + getAggregator(): QueueAggregator { return this.queueInfo; } getRegistrarState(): string | undefined { return this.registerer?.state.toString().toLocaleLowerCase(); } + + clear(): void { + this.userAgent?.stop(); + } } diff --git a/client/providers/CallProvider/CallProvider.tsx b/client/providers/CallProvider/CallProvider.tsx index 14ec7d084bc68..cfe83606adb06 100644 --- a/client/providers/CallProvider/CallProvider.tsx +++ b/client/providers/CallProvider/CallProvider.tsx @@ -15,7 +15,8 @@ import { useEndpoint, useStream } from '../../contexts/ServerContext'; import { useSetting } from '../../contexts/SettingsContext'; import { useUser } from '../../contexts/UserContext'; import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; -import { isUseVoipClientResultError, isUseVoipClientResultLoading, useVoipClient } from './hooks/useVoipClient'; +import { QueueAggregator } from '../../lib/voip/QueueAggregator'; +import { useVoipClient } from './hooks/useVoipClient'; const startRingback = (user: IUser): void => { const audioVolume = getUserPreference(user, 'notificationsSoundVolume'); @@ -43,7 +44,8 @@ export const CallProvider: FC = ({ children }) => { const AudioTagPortal: FC = ({ children }) => useMemo(() => createPortal(children, document.body), [children]); - const [queueCounter, setQueueCounter] = useState(''); + const [queueCounter, setQueueCounter] = useState(0); + const [queueName, setQueueName] = useState(''); const setModal = useSetModal(); @@ -51,17 +53,35 @@ export const CallProvider: FC = ({ children }) => { setModal(); }, [setModal]); + const [queueAggregator, setQueueAggregator] = useState(); + useEffect(() => { - if (!voipEnabled || !user) { + if (!result?.voipClient) { return; } - if (isUseVoipClientResultError(result) || isUseVoipClientResultLoading(result)) { + setQueueAggregator(result.voipClient.getAggregator()); + }, [result]); + + useEffect(() => { + if (!voipEnabled || !user || !queueAggregator) { return; } - const queueAggregator = result.voipClient.getAggregator(); - if (!queueAggregator) { + const handleAgentCalled = async (queue: { + queuename: string; + callerId: { id: string; name: string }; + queuedcalls: string; + }): Promise => { + queueAggregator.callRinging({ queuename: queue.queuename, callerid: queue.callerId }); + setQueueName(queueAggregator.getCurrentQueueName()); + }; + + return subscribeToNotifyUser(`${user._id}/agentcalled`, handleAgentCalled); + }, [subscribeToNotifyUser, user, voipEnabled, queueAggregator]); + + useEffect(() => { + if (!voipEnabled || !user || !queueAggregator) { return; } @@ -71,118 +91,82 @@ export const CallProvider: FC = ({ children }) => { queuedcalls: string; }): Promise => { queueAggregator.queueJoined(joiningDetails); - setQueueCounter(queueAggregator.getCallWaitingCount().toString()); + setQueueCounter(queueAggregator.getCallWaitingCount()); }; return subscribeToNotifyUser(`${user._id}/callerjoined`, handleQueueJoined); - }, [result, subscribeToNotifyUser, user, voipEnabled]); + }, [subscribeToNotifyUser, user, voipEnabled, queueAggregator]); useEffect(() => { - if (!voipEnabled || !user) { - return; - } - - if (isUseVoipClientResultError(result) || isUseVoipClientResultLoading(result)) { - return; - } - - const queueAggregator = result.voipClient.getAggregator(); - if (!queueAggregator) { + if (!voipEnabled || !user || !queueAggregator) { return; } const handleAgentConnected = (queue: { queuename: string; queuedcalls: string; waittimeinqueue: string }): void => { queueAggregator.callPickedup(queue); - setQueueCounter(queueAggregator.getCallWaitingCount().toString()); + setQueueName(queueAggregator.getCurrentQueueName()); + setQueueCounter(queueAggregator.getCallWaitingCount()); }; return subscribeToNotifyUser(`${user._id}/agentconnected`, handleAgentConnected); - }, [result, subscribeToNotifyUser, user, voipEnabled]); + }, [queueAggregator, subscribeToNotifyUser, user, voipEnabled]); useEffect(() => { - if (!voipEnabled || !user) { - return; - } - - if (isUseVoipClientResultError(result) || isUseVoipClientResultLoading(result)) { - return; - } - - const queueAggregator = result.voipClient.getAggregator(); - if (!queueAggregator) { + if (!voipEnabled || !user || !queueAggregator) { return; } const handleMemberAdded = (queue: { queuename: string; queuedcalls: string }): void => { queueAggregator.memberAdded(queue); - setQueueCounter(queueAggregator.getCallWaitingCount().toString()); + setQueueName(queueAggregator.getCurrentQueueName()); + setQueueCounter(queueAggregator.getCallWaitingCount()); }; return subscribeToNotifyUser(`${user._id}/queuememberadded`, handleMemberAdded); - }, [result, subscribeToNotifyUser, user, voipEnabled]); + }, [queueAggregator, subscribeToNotifyUser, user, voipEnabled]); useEffect(() => { - if (!voipEnabled || !user) { - return; - } - - if (isUseVoipClientResultError(result) || isUseVoipClientResultLoading(result)) { - return; - } - - const queueAggregator = result.voipClient.getAggregator(); - if (!queueAggregator) { + if (!voipEnabled || !user || !queueAggregator) { return; } const handleMemberRemoved = (queue: { queuename: string; queuedcalls: string }): void => { queueAggregator.memberRemoved(queue); - setQueueCounter(queueAggregator.getCallWaitingCount().toString()); + setQueueCounter(queueAggregator.getCallWaitingCount()); }; return subscribeToNotifyUser(`${user._id}/queuememberremoved`, handleMemberRemoved); - }, [result, subscribeToNotifyUser, user, voipEnabled]); + }, [queueAggregator, subscribeToNotifyUser, user, voipEnabled]); useEffect(() => { - if (!voipEnabled || !user) { - return; - } - - if (isUseVoipClientResultError(result) || isUseVoipClientResultLoading(result)) { - return; - } - - const queueAggregator = result.voipClient.getAggregator(); - if (!queueAggregator) { + if (!voipEnabled || !user || !queueAggregator) { return; } const handleCallAbandon = (queue: { queuename: string; queuedcallafterabandon: string }): void => { queueAggregator.queueAbandoned(queue); - setQueueCounter(queueAggregator.getCallWaitingCount().toString()); + setQueueName(queueAggregator.getCurrentQueueName()); + setQueueCounter(queueAggregator.getCallWaitingCount()); }; return subscribeToNotifyUser(`${user._id}/callabandoned`, handleCallAbandon); - }, [result, subscribeToNotifyUser, user, voipEnabled]); + }, [queueAggregator, subscribeToNotifyUser, user, voipEnabled]); useEffect(() => { - if (!voipEnabled || !user) { + if (!voipEnabled || !user || !queueAggregator) { return; } const handleCallHangup = (_event: { roomId: string }): void => { + setQueueName(queueAggregator.getCurrentQueueName()); openWrapUpModal(); }; return subscribeToNotifyUser(`${user._id}/call.callerhangup`, handleCallHangup); - }, [openWrapUpModal, result, subscribeToNotifyUser, user, voipEnabled]); + }, [openWrapUpModal, queueAggregator, subscribeToNotifyUser, user, voipEnabled]); useEffect(() => { - if (isUseVoipClientResultError(result)) { - return; - } - - if (isUseVoipClientResultLoading(result)) { + if (!result.voipClient) { return; } @@ -225,7 +209,7 @@ export const CallProvider: FC = ({ children }) => { * */ remoteAudioMediaRef.current && result.voipClient.switchMediaRenderer({ remoteMediaElement: remoteAudioMediaRef.current }); - }); + }, [result.voipClient]); const visitorEndpoint = useEndpoint('POST', 'livechat/visitor'); const voipEndpoint = useEndpoint('GET', 'voip/room'); @@ -241,7 +225,14 @@ export const CallProvider: FC = ({ children }) => { }; } - if (isUseVoipClientResultError(result)) { + if (!user?.extension) { + return { + enabled: false, + ready: false, + }; + } + + if (result.error) { return { enabled: true, ready: false, @@ -249,7 +240,7 @@ export const CallProvider: FC = ({ children }) => { }; } - if (isUseVoipClientResultLoading(result)) { + if (!result.voipClient) { return { enabled: true, ready: false, @@ -266,9 +257,10 @@ export const CallProvider: FC = ({ children }) => { enabled: true, ready: true, openedRoomInfo: roomInfo, - registrationInfo, voipClient, + registrationInfo, queueCounter, + queueName, actions: { mute: (): Promise => voipClient.muteCall(true), // voipClient.mute(), unmute: (): Promise => voipClient.muteCall(false), // voipClient.unmute() @@ -291,7 +283,7 @@ export const CallProvider: FC = ({ children }) => { const voipRoom = visitor && (await voipEndpoint({ token: visitor.token, agentId: user._id })); voipRoom.room && roomCoordinator.openRouteLink(voipRoom.room.t, { rid: voipRoom.room._id, name: voipRoom.room.name }); voipRoom.room && setRoomInfo({ v: { token: voipRoom.room.v.token }, rid: voipRoom.room._id }); - const queueAggregator = result.voipClient.getAggregator(); + const queueAggregator = voipClient.getAggregator(); if (queueAggregator) { queueAggregator.callStarted(); } @@ -302,14 +294,26 @@ export const CallProvider: FC = ({ children }) => { closeRoom: async ({ comment, tags }): Promise => { roomInfo && (await voipCloseRoomEndpoint({ rid: roomInfo.rid, token: roomInfo.v.token || '', comment: comment || '', tags })); homeRoute.push({}); - const queueAggregator = result.voipClient.getAggregator(); + const queueAggregator = voipClient.getAggregator(); if (queueAggregator) { queueAggregator.callEnded(); } }, openWrapUpModal, }; - }, [queueCounter, voipEnabled, homeRoute, openWrapUpModal, result, roomInfo, user, visitorEndpoint, voipCloseRoomEndpoint, voipEndpoint]); + }, [ + voipEnabled, + user, + result, + roomInfo, + queueCounter, + queueName, + openWrapUpModal, + visitorEndpoint, + voipEndpoint, + voipCloseRoomEndpoint, + homeRoute, + ]); return ( diff --git a/client/providers/CallProvider/hooks/useVoipClient.ts b/client/providers/CallProvider/hooks/useVoipClient.ts index 7c3dd350c6cb6..35bafdfa0072d 100644 --- a/client/providers/CallProvider/hooks/useVoipClient.ts +++ b/client/providers/CallProvider/hooks/useVoipClient.ts @@ -6,25 +6,16 @@ import { IRegistrationInfo } from '../../../../definition/voip/IRegistrationInfo import { WorkflowTypes } from '../../../../definition/voip/WorkflowTypes'; import { useEndpoint } from '../../../contexts/ServerContext'; import { useSetting } from '../../../contexts/SettingsContext'; -import { useUser } from '../../../contexts/UserContext'; +import { useUser, useUserId } from '../../../contexts/UserContext'; import { SimpleVoipUser } from '../../../lib/voip/SimpleVoipUser'; import { VoIPUser } from '../../../lib/voip/VoIPUser'; import { useWebRtcServers } from './useWebRtcServers'; -type UseVoipClientResult = UseVoipClientResultResolved | UseVoipClientResultError | UseVoipClientResultLoading; - -type UseVoipClientResultResolved = { - voipClient: VoIPUser; - registrationInfo: IRegistrationInfo; +type UseVoipClientResult = { + voipClient?: VoIPUser; + registrationInfo?: IRegistrationInfo; + error?: Error | unknown; }; -type UseVoipClientResultError = { error: Error }; -type UseVoipClientResultLoading = Record; - -export const isUseVoipClientResultError = (result: UseVoipClientResult): result is UseVoipClientResultError => - !!(result as UseVoipClientResultError).error; - -export const isUseVoipClientResultLoading = (result: UseVoipClientResult): result is UseVoipClientResultLoading => - !result || !Object.keys(result).length; const isSignedResponse = (data: any): data is { result: string } => typeof data?.result === 'string'; @@ -33,17 +24,18 @@ export const useVoipClient = (): UseVoipClientResult => { const registrationInfo = useEndpoint('GET', 'connector.extension.getRegistrationInfoByUserId'); const membership = useEndpoint('GET', 'voip/queues.getMembershipSubscription'); const user = useUser(); - const iceServers = useWebRtcServers(); + const userId = useUserId(); + const [extension, setExtension] = useSafely(useState(null)); + const iceServers = useWebRtcServers(); const [result, setResult] = useSafely(useState({})); - useEffect(() => { - if (!user || !user?._id || !voipEnabled) { + if (!userId || !extension || !voipEnabled) { setResult({}); return; } - - registrationInfo({ id: user._id }).then( + let client: VoIPUser; + registrationInfo({ id: userId }).then( (data) => { let parsedData: IRegistrationInfo; if (isSignedResponse(data)) { @@ -59,30 +51,47 @@ export const useVoipClient = (): UseVoipClientResult => { callServerConfig: { websocketPath }, } = parsedData; - let client: VoIPUser; (async (): Promise => { try { const subscription = await membership({ extension }); client = await SimpleVoipUser.create(extension, password, host, websocketPath, iceServers, 'video'); // Today we are hardcoding workflow mode. - // In futue, this should be read from configuration + // In future, this should be ready from configuration client.setWorkflowMode(WorkflowTypes.CONTACT_CENTER_USER); client.setMembershipSubscription(subscription); setResult({ voipClient: client, registrationInfo: parsedData }); - } catch (e) { - setResult({ error: e as Error }); + } catch (error) { + setResult({ error }); } })(); }, - (error) => { - setResult({ error: error as Error }); + (error: Error) => { + setResult({ error }); }, ); return (): void => { - // client?.disconnect(); - // TODO how to close the client? before creating a new one? + if (client) { + client.clear(); + } }; - }, [user, iceServers, registrationInfo, setResult, membership, voipEnabled]); + }, [userId, iceServers, registrationInfo, setResult, membership, voipEnabled, extension]); + useEffect(() => { + if (!user) { + setResult({}); + return; + } + if (user.extension) { + setExtension(user.extension); + } else { + setExtension(null); + + if (!result.voipClient) { + return; + } + + result.voipClient.clear(); + } + }, [result, setExtension, setResult, user]); return result; }; diff --git a/client/sidebar/footer/voip/index.tsx b/client/sidebar/footer/voip/index.tsx index c9523916fa3f7..71d6093c90d71 100644 --- a/client/sidebar/footer/voip/index.tsx +++ b/client/sidebar/footer/voip/index.tsx @@ -48,7 +48,7 @@ export const VoipFooter = (): ReactElement | null => { case 'IN_CALL': return t('In_progress'); case 'OFFER_RECEIVED': - return t('Calling'); + return t('Ringing'); case 'ON_HOLD': return t('On_Hold'); } diff --git a/client/sidebar/sections/components/OmnichannelCallToggleReady.tsx b/client/sidebar/sections/components/OmnichannelCallToggleReady.tsx index e48899f795698..04bde69f30e4a 100644 --- a/client/sidebar/sections/components/OmnichannelCallToggleReady.tsx +++ b/client/sidebar/sections/components/OmnichannelCallToggleReady.tsx @@ -2,20 +2,39 @@ import { Sidebar } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import React, { ReactElement, useEffect, useState } from 'react'; -import { useCallClient } from '../../../contexts/CallContext'; +import { useCallClient, useCallerInfo } from '../../../contexts/CallContext'; import { useTranslation } from '../../../contexts/TranslationContext'; export const OmnichannelCallToggleReady = (): ReactElement => { const [agentEnabled, setAgentEnabled] = useState(false); // TODO: get from AgentInfo const t = useTranslation(); const [registered, setRegistered] = useState(false); + const voipClient = useCallClient(); + const [onCall, setOnCall] = useState(false); + const callerInfo = useCallerInfo(); + + const getTooltip = (): string => { + if (!registered) { + return t('Enable'); + } + if (!onCall) { + // Color for this state still not defined + return t('Disable'); + } + + return t('Cannot_disable_while_on_call'); + }; const voipCallIcon = { - title: !registered ? t('Enable') : t('Disable'), + title: getTooltip(), color: registered ? 'success' : undefined, icon: registered ? 'phone' : 'phone-disabled', } as const; - const voipClient = useCallClient(); + + useEffect(() => { + // Any of the 2 states means the user is already talking + setOnCall(['IN_CALL', 'ON_HOLD'].includes(callerInfo.state)); + }, [callerInfo]); useEffect(() => { let agentEnabled = false; @@ -26,8 +45,12 @@ export const OmnichannelCallToggleReady = (): ReactElement => { setAgentEnabled(agentEnabled); setRegistered(agentEnabled); }, [voipClient]); + // TODO: move registration flow to context provider const handleVoipCallStatusChange = useMutableCallback((): void => { + if (onCall) { + return; + } // TODO: backend set voip call status // voipClient.setVoipCallStatus(!registered); if (agentEnabled) { @@ -74,5 +97,5 @@ export const OmnichannelCallToggleReady = (): ReactElement => { }; }, [onRegistered, onRegistrationError, onUnregistered, onUnregistrationError, voipClient]); - return ; + return ; }; diff --git a/package-lock.json b/package-lock.json index e042f196f9f63..e5106478eee96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "Rocket.Chat", - "version": "4.5.2", + "version": "4.5.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -24382,7 +24382,7 @@ "dev": true }, "lamejs": { - "version": "git+https://github.com/zhuker/lamejs.git#564612b5b57336238a5920ba4c301b49f7cb2bab", + "version": "git+https://github.com/zhuker/lamejs.git#582bbba6a12f981b984d8fb9e1874499fed85675", "from": "git+https://github.com/zhuker/lamejs.git", "requires": { "use-strict": "1.0.1" diff --git a/package.json b/package.json index 1e1ae3e518ea2..2d31481d65b2e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "Rocket.Chat", "description": "The Ultimate Open Source WebChat Platform", - "version": "4.5.2", + "version": "4.5.3", "author": { "name": "Rocket.Chat", "url": "https://rocket.chat/" diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 4449936d3b1a4..d9a229ebd2f27 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -712,7 +712,9 @@ "Call": "Call", "Calling": "Calling", "Call_Center": "Call Center", - "Calls_in_queue": "__calls__ Calls In Queue", + "Calls_in_queue": "__calls__ call in queue", + "Calls_in_queue_plural": "__calls__ calls in queue", + "Calls_in_queue_empty": "Queue is empty", "Call_declined": "Call Declined!", "Call_Information": "Call Information", "Call_provider": "Call Provider", @@ -737,6 +739,7 @@ "Cannot_invite_users_to_direct_rooms": "Cannot invite users to direct rooms", "Cannot_open_conversation_with_yourself": "Cannot Direct Message with yourself", "Cannot_share_your_location": "Cannot share your location...", + "Cannot_disable_while_on_call": "Can't change status during calls ", "CAS_autoclose": "Autoclose Login Popup", "CAS_base_url": "SSO Base URL", "CAS_base_url_Description": "The base URL of your external SSO service e.g: https://sso.example.undef/sso/", @@ -3266,7 +3269,7 @@ "Omnichannel_External_Frame_Encryption_JWK_Description": "If provided it will encrypt the user's token with the provided key and the external system will need to decrypt the data to access the token", "Omnichannel_External_Frame_URL": "External frame URL", "On": "On", - "On_Hold": "On Hold", + "On_Hold": "On hold", "On_Hold_Chats": "On Hold", "On_Hold_conversations": "On hold conversations", "online": "online", @@ -3659,6 +3662,7 @@ "Return_to_home": "Return to home", "Return_to_previous_page": "Return to previous page", "Return_to_the_queue": "Return back to the Queue", + "Ringing": "Ringing", "Robot_Instructions_File_Content": "Robots.txt File Contents", "Default_Referrer_Policy": "Default Referrer Policy", "Default_Referrer_Policy_Description": "This controls the 'referrer' header that's sent when requesting embedded media from other servers. For more information, refer to this link from MDN. Remember, a full page refresh is required for this to take effect", diff --git a/server/services/voip/service.ts b/server/services/voip/service.ts index 127f3d66d7342..e0afc4bdefe3f 100644 --- a/server/services/voip/service.ts +++ b/server/services/voip/service.ts @@ -99,9 +99,14 @@ export class VoipService extends ServiceClassInternal implements IVoipService { const queueInfo: { name: string; members: string[] }[] = []; for await (const queue of queues) { const queueDetails = (await this.commandHandler.executeCommand(Commands.queue_details, { - queue, + queueName: queue, })) as IVoipConnectorResult; - + const details = queueDetails.result as IQueueDetails; + if (!details.members || !details.members.length) { + // Go to the next queue if queue does not have any + // memmbers. + continue; + } queueInfo.push({ name: queue, members: (queueDetails.result as IQueueDetails).members.map((member) => member.name.replace('PJSIP/', '')),