diff --git a/.changeset/app-release-info-page.md b/.changeset/app-release-info-page.md new file mode 100644 index 000000000000..245131c987e5 --- /dev/null +++ b/.changeset/app-release-info-page.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed Marketplace Release Info tab loading loop \ No newline at end of file diff --git a/.changeset/flat-fireants-itch.md b/.changeset/flat-fireants-itch.md new file mode 100644 index 000000000000..ffd919d3a774 --- /dev/null +++ b/.changeset/flat-fireants-itch.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +fix: spotlight doesnt update with new rooms diff --git a/.changeset/gorgeous-lamps-tan.md b/.changeset/gorgeous-lamps-tan.md new file mode 100644 index 000000000000..8d31b582ca77 --- /dev/null +++ b/.changeset/gorgeous-lamps-tan.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix emoji being rendered as big on headers and other places than message text diff --git a/.changeset/moody-teachers-cheer.md b/.changeset/moody-teachers-cheer.md new file mode 100644 index 000000000000..bed5e5eeb39a --- /dev/null +++ b/.changeset/moody-teachers-cheer.md @@ -0,0 +1,9 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-services": patch +"@rocket.chat/core-typings": patch +"@rocket.chat/model-typings": patch +"@rocket.chat/rest-typings": patch +--- + +fix: Resume on-hold chat not working with max-chat's allowed per agent config diff --git a/.changeset/nervous-bulldogs-fix.md b/.changeset/nervous-bulldogs-fix.md new file mode 100644 index 000000000000..571184e4cecf --- /dev/null +++ b/.changeset/nervous-bulldogs-fix.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Re-added pagination to Department's agents list diff --git a/.changeset/shiny-donkeys-prove.md b/.changeset/shiny-donkeys-prove.md new file mode 100644 index 000000000000..1aad73d1e1d7 --- /dev/null +++ b/.changeset/shiny-donkeys-prove.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +--- + +fix: [ENTERPRISE] Guest users can join more than maxRoomsPerGuest rooms diff --git a/.changeset/tricky-tomatoes-sit.md b/.changeset/tricky-tomatoes-sit.md new file mode 100644 index 000000000000..5abba73ae52c --- /dev/null +++ b/.changeset/tricky-tomatoes-sit.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +fix: Message sent triggering thread subscriptions multiple times diff --git a/.github/workflows/new-release.yml b/.github/workflows/new-release.yml index feeaf0a7c8ae..f5869aa323c7 100644 --- a/.github/workflows/new-release.yml +++ b/.github/workflows/new-release.yml @@ -17,6 +17,9 @@ on: default: master required: false +env: + HUSKY: 0 + jobs: new-release: runs-on: ubuntu-latest @@ -25,15 +28,15 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 - # with: - # ref: ${{ github.event.inputs.base-ref }} - - name: Setup Node.js 16 - uses: actions/setup-node@v3 + - name: Setup NodeJS + uses: ./.github/actions/setup-node with: - node-version: 16 + node-version: 14.21.3 + cache-modules: true + install: true - - run: yarn install + - uses: dtinth/setup-github-actions-caching-for-turbo@v1 - name: Build run: yarn build diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 854d91524c84..bb56beb975b9 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -7,6 +7,9 @@ on: concurrency: ${{ github.workflow }}-${{ github.ref }} +env: + HUSKY: 0 + jobs: release: name: Release @@ -15,13 +18,14 @@ jobs: - name: Checkout Repo uses: actions/checkout@v3 - - name: Setup Node.js 16 - uses: actions/setup-node@v3 + - name: Setup NodeJS + uses: ./.github/actions/setup-node with: - node-version: 16 + node-version: 14.21.3 + cache-modules: true + install: true - - name: Install Dependencies - run: yarn + - uses: dtinth/setup-github-actions-caching-for-turbo@v1 - name: Build run: yarn build diff --git a/HISTORY.md b/HISTORY.md index 30439bc8d1b7..07bbdbb660b5 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,162 @@ +# 6.2.5 + +`2023-06-07 ยท 1 ๐Ÿ› ยท 1 ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป` + +### Engine versions +- Node: `14.21.3` +- NPM: `6.14.17` +- MongoDB: `4.4, 5.0, 6.0` +- Apps-Engine: `1.39.1` + +### ๐Ÿ› Bug fixes + + +- Seat counter including apps ([#29489](https://github.com/RocketChat/Rocket.Chat/pull/29489)) + +### ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป Core Team ๐Ÿค“ + +- [@ggazzo](https://github.com/ggazzo) + +# 6.2.4 + +`2023-06-07 ยท 2 ๐Ÿ› ยท 2 ๐Ÿ” ยท 5 ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป` + +### Engine versions +- Node: `14.21.3` +- NPM: `6.14.17` +- MongoDB: `4.4, 5.0, 6.0` +- Apps-Engine: `1.39.1` + +### ๐Ÿ› Bug fixes + + +- Imported messages are not displayed ([#29485](https://github.com/RocketChat/Rocket.Chat/pull/29485)) + + - Fix issue with imported messages not being displayed in the room. + - Fix importer agent being added as a member of every imported room. + +- message deletion fails if has files attached on filesystem storage ([#29313](https://github.com/RocketChat/Rocket.Chat/pull/29313)) + +
+๐Ÿ” Minor changes + + +- Fix Omnichannel Webhooks tests ([#29344](https://github.com/RocketChat/Rocket.Chat/pull/29344)) + +- use local httpbin container on github CI's ([#29067](https://github.com/RocketChat/Rocket.Chat/pull/29067)) + +
+ +### ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป Core Team ๐Ÿค“ + +- [@KevLehman](https://github.com/KevLehman) +- [@debdutdeb](https://github.com/debdutdeb) +- [@matheusbsilva137](https://github.com/matheusbsilva137) +- [@murtaza98](https://github.com/murtaza98) +- [@sampaiodiego](https://github.com/sampaiodiego) + +# 6.2.3 + +`2023-06-05 ยท 1 ๐ŸŽ‰ ยท 9 ๐Ÿ› ยท 9 ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป` + +### Engine versions +- Node: `14.21.3` +- NPM: `6.14.17` +- MongoDB: `4.4, 5.0, 6.0` +- Apps-Engine: `1.39.1` + +### ๐ŸŽ‰ New features + + +- Add support to use `TRANSPORTER` env var for monolith deployments ([#29373](https://github.com/RocketChat/Rocket.Chat/pull/29373)) + +### ๐Ÿ› Bug fixes + + +- `queuedForUser` endpoint not filtering by status ([#29189](https://github.com/RocketChat/Rocket.Chat/pull/29189)) + +- Embedded layout stops working after navigation occurs ([#29147](https://github.com/RocketChat/Rocket.Chat/pull/29147)) + +- File upload in Safari, IOS devices ([#27121](https://github.com/RocketChat/Rocket.Chat/pull/27121)) + +- fix wrong %s translations ([#29395](https://github.com/RocketChat/Rocket.Chat/pull/29395)) + +- getActiveLocalUserCount query ([#29349](https://github.com/RocketChat/Rocket.Chat/pull/29349)) + +- Import progress page stuck at 0% ([#29421](https://github.com/RocketChat/Rocket.Chat/pull/29421)) + + - Fix incorrect percentage calc; + - Fix import progress page stuck at 0%. + +- Importer crashes when sending the "active status" e-mail notification to users ([#29401](https://github.com/RocketChat/Rocket.Chat/pull/29401)) + + - Fix `getImportProgress` endpoint returning incorrect count info (`total: 0, completed: 0`); + - Fix server crash when sending the "active status" e-mail notification to users on import. + +- OTR session closing after 10 seconds without warning ([#29245](https://github.com/RocketChat/Rocket.Chat/pull/29245)) + + The `Notifications` streamer is sending two events when there's a license applied to the workspace and it's not using Microservices. I'm avoiding this by simply ignoring the second event in OTRRoom. + + There is still an issue where there's 2 system messages being fired after accepting, but that's not a really big concern since it doesn't break anything. + +- unable to create user if Accounts_ManuallyApproveNewUsers is enabled ([#29293](https://github.com/RocketChat/Rocket.Chat/pull/29293)) + +### ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป Core Team ๐Ÿค“ + +- [@KevLehman](https://github.com/KevLehman) +- [@debdutdeb](https://github.com/debdutdeb) +- [@gabriellsh](https://github.com/gabriellsh) +- [@ggazzo](https://github.com/ggazzo) +- [@jessicaschelly](https://github.com/jessicaschelly) +- [@matheusbsilva137](https://github.com/matheusbsilva137) +- [@murtaza98](https://github.com/murtaza98) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@yash-rajpal](https://github.com/yash-rajpal) + +# 6.2.2 + +`2023-05-19 ยท 1 ๐Ÿ› ยท 2 ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป` + +### Engine versions +- Node: `14.21.3` +- NPM: `6.14.17` +- MongoDB: `4.4, 5.0, 6.0` +- Apps-Engine: `1.39.1` + +### ๐Ÿ› Bug fixes + + +- Livechat `CSP` whitelist validation ([#29278](https://github.com/RocketChat/Rocket.Chat/pull/29278)) + + This PR fixes the Livechat CSP validation, which was incorrectly blocking access to the widget for all non whitelisted domains. + +### ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป Core Team ๐Ÿค“ + +- [@aleksandernsilva](https://github.com/aleksandernsilva) +- [@ggazzo](https://github.com/ggazzo) + +# 6.2.1 + +`2023-05-17 ยท 1 ๐Ÿ” ยท 1 ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป` + +### Engine versions +- Node: `14.21.3` +- NPM: `6.14.17` +- MongoDB: `4.4, 5.0, 6.0` +- Apps-Engine: `1.39.1` + +
+๐Ÿ” Minor changes + + +- Bump apps engine and vm2 ([#29262](https://github.com/RocketChat/Rocket.Chat/pull/29262)) + +
+ +### ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป Core Team ๐Ÿค“ + +- [@AllanPazRibeiro](https://github.com/AllanPazRibeiro) + # 6.2.0 `2023-05-15 ยท 9 ๐ŸŽ‰ ยท 2 ๐Ÿš€ ยท 80 ๐Ÿ› ยท 290 ๐Ÿ” ยท 56 ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป` diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md new file mode 100644 index 000000000000..20542d3f6726 --- /dev/null +++ b/apps/meteor/CHANGELOG.md @@ -0,0 +1,26 @@ +# @rocket.chat/meteor + +## 6.2.6 + +### Patch Changes + +- [#29545](https://github.com/RocketChat/Rocket.Chat/pull/29545) [`8ade880306`](https://github.com/RocketChat/Rocket.Chat/commit/8ade880306a2f4be6fb979c9db32a1ca5bdf4c1f) Thanks [@github-actions](https://github.com/apps/github-actions)! - fix: Frontend crash if IndexedDB is not available, i.e. in Firefox private mode + +- Updated dependencies []: + - @rocket.chat/core-typings@6.2.6 + - @rocket.chat/rest-typings@6.2.6 + - @rocket.chat/omnichannel-services@0.0.2 + - @rocket.chat/pdf-worker@0.0.2 + - @rocket.chat/presence@0.0.2 + - @rocket.chat/api-client@0.0.2 + - @rocket.chat/core-services@0.0.2 + - @rocket.chat/gazzodown@0.0.1 + - @rocket.chat/model-typings@0.0.2 + - @rocket.chat/ui-contexts@0.0.2 + - @rocket.chat/models@0.0.2 + - @rocket.chat/ui-theming@0.0.1 + - @rocket.chat/fuselage-ui-kit@0.31.16 + - @rocket.chat/ui-client@0.0.1 + - @rocket.chat/ui-video-conf@0.0.1 + - @rocket.chat/web-ui-registration@0.0.1 + - @rocket.chat/instance-status@0.0.2 diff --git a/apps/meteor/app/emoji-emojione/client/emojione-sprites.css b/apps/meteor/app/emoji-emojione/client/emojione-sprites.css index 2a480d6294b1..f6c4cf58512e 100644 --- a/apps/meteor/app/emoji-emojione/client/emojione-sprites.css +++ b/apps/meteor/app/emoji-emojione/client/emojione-sprites.css @@ -28,8 +28,3 @@ image-rendering: -webkit-optimize-contrast; image-rendering: optimizeQuality; } - -.big > .emojione { - width: 44px; - height: 44px; -} diff --git a/apps/meteor/app/emoji-emojione/lib/generateEmojiIndex.mjs b/apps/meteor/app/emoji-emojione/lib/generateEmojiIndex.mjs index d300fd64c6c7..1361a72c0dd2 100644 --- a/apps/meteor/app/emoji-emojione/lib/generateEmojiIndex.mjs +++ b/apps/meteor/app/emoji-emojione/lib/generateEmojiIndex.mjs @@ -9,11 +9,11 @@ import _ from 'underscore'; import gm from 'gm'; // lgtm[js/unused-local-variable] const assetFolder = '../../../node_modules/emojione-assets'; -const emojiJsonFile = `${ assetFolder }/emoji.json`; +const emojiJsonFile = `${assetFolder}/emoji.json`; if (!fs.existsSync(emojiJsonFile)) { - console.error(`${ emojiJsonFile } doesn't exist.`); - console.error('Maybe you need to run \'meteor npm install emojione-assets\' or \'meteor npm install\'?'); + console.error(`${emojiJsonFile} doesn't exist.`); + console.error("Maybe you need to run 'meteor npm install emojione-assets' or 'meteor npm install'?"); } else { const emojiJson = fs.readFileSync(emojiJsonFile); generateEmojiPicker(emojiJson); @@ -51,7 +51,6 @@ function generateEmojiPicker(data) { * Mapping category hashes into human readable and translated names */\n\n`; - const emojiCategoriesMapping = [ { key: 'people', i18n: 'Smileys_and_People' }, { key: 'nature', i18n: 'Animals_and_Nature' }, @@ -66,11 +65,11 @@ function generateEmojiPicker(data) { // emojiCategories output += `export const emojiCategories = [\n`; for (let category in emojisByCategory) { - const map = emojiCategoriesMapping.find(o => o.key === category); + const map = emojiCategoriesMapping.find((o) => o.key === category); if (map) { output += `\t{ key: '${category}', i18n: '${map.i18n}' },\n`; } else { - if(category !== 'modifier' || category !== 'regional'){ + if (category !== 'modifier' || category !== 'regional') { console.error(`No emojiCategory mapping for ${category}`); } } @@ -86,7 +85,6 @@ function generateEmojiPicker(data) { } else { output += `\t${toneList[tone]}: 1,\n`; } - } output += `};\n`; @@ -96,16 +94,16 @@ function generateEmojiPicker(data) { output += `\t${category}: [\n`; for (let emoji in emojisByCategory[category]) { - output += `\t\t'${emojiList[emojisByCategory[category][emoji]].shortname.replace(/:/g,'')}',\n`; + output += `\t\t'${emojiList[emojisByCategory[category][emoji]].shortname.replace(/:/g, '')}',\n`; } output += `\t],\n`; } output += `};\n`; - fs.writeFileSync("emojiPicker.js", output, { + fs.writeFileSync('emojiPicker.js', output, { encoding: 'utf8', - flag: 'w' + flag: 'w', }); console.log('Generated emojiPicker.js!'); @@ -116,41 +114,44 @@ function generateEmojiPicker(data) { for (let category in emojisByCategory) { let srcList = []; let diversityList = []; - const emojis = _.filter(emojiList, x => x.category === category); - const spritePath = `../../../public/packages/emojione/${ category }-sprites.png`; + const emojis = _.filter(emojiList, (x) => x.category === category); + const spritePath = `../../../public/packages/emojione/${category}-sprites.png`; _.each(emojis, function (emoji) { - srcList.push(`${ assetFolder }/png/64/${ emoji.code_points.base }.png`); - if(emoji.diversity){ + srcList.push(`${assetFolder}/png/64/${emoji.code_points.base}.png`); + if (emoji.diversity) { diversityList[emoji.code_points.base] = true; } }); - spriteCss += `@import './${ category }-sprites.css';\n`; - - nsg({ - src: srcList, - spritePath: spritePath, - layout: 'packed', - stylesheet: 'emojione.tpl', - stylesheetPath: `../client/${ category }-sprites.css`, - compositor: 'gm', - layoutOptions: { - scaling: 1, + spriteCss += `@import './${category}-sprites.css';\n`; + + nsg( + { + src: srcList, + spritePath: spritePath, + layout: 'packed', + stylesheet: 'emojione.tpl', + stylesheetPath: `../client/${category}-sprites.css`, + compositor: 'gm', + layoutOptions: { + scaling: 1, + }, + stylesheetOptions: { + prefix: '', + diversityList: diversityList, + category: category, + spritePath: `/packages/emojione/${category}-sprites.png`, + pixelRatio: 1, + }, }, - stylesheetOptions: { - prefix: '', - diversityList: diversityList, - category: category, - spritePath: `/packages/emojione/${ category }-sprites.png`, - pixelRatio: 1 - } - }, function (err) { - if (err) { - console.error(err); - return; - } - console.log(`${ category }'s sprite generated!`); - }); + function (err) { + if (err) { + console.error(err); + return; + } + console.log(`${category}'s sprite generated!`); + }, + ); } spriteCss += ` @@ -173,14 +174,9 @@ function generateEmojiPicker(data) { image-rendering: -webkit-optimize-contrast; image-rendering: optimizeQuality; } - -.emojione.big { - width: 44px; - height: 44px; -} `; - fs.writeFileSync("../client/emojione-sprites.css", spriteCss, { + fs.writeFileSync('../client/emojione-sprites.css', spriteCss, { encoding: 'utf8', - flag: 'w' + flag: 'w', }); } diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts index 341cb60a4322..62f2cf04dece 100644 --- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts +++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts @@ -26,6 +26,11 @@ export const addUserToRoom = async function ( const userToBeAdded = typeof user !== 'string' ? user : await Users.findOneByUsername(user.replace('@', '')); const roomDirectives = roomCoordinator.getRoomDirectives(room.t); + + if (!userToBeAdded) { + throw new Meteor.Error('user-not-found'); + } + if ( !(await roomDirectives.allowMemberAction(room, RoomMemberActions.JOIN, userToBeAdded._id)) && !(await roomDirectives.allowMemberAction(room, RoomMemberActions.INVITE, userToBeAdded._id)) @@ -39,6 +44,8 @@ export const addUserToRoom = async function ( throw new Meteor.Error((error as any)?.message); } + await callbacks.run('beforeAddedToRoom', { user: userToBeAdded, inviter: userToBeAdded }); + // Check if user is already in room const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userToBeAdded._id); if (subscription || !userToBeAdded) { diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 318aa089e124..6d0048659501 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -144,7 +144,7 @@ export const createRoom = async ( } else { for await (const username of [...new Set(members)]) { const member = await Users.findOneByUsername(username, { - projection: { 'username': 1, 'settings.preferences': 1, 'federated': 1 }, + projection: { 'username': 1, 'settings.preferences': 1, 'federated': 1, 'roles': 1 }, }); if (!member) { continue; @@ -152,6 +152,7 @@ export const createRoom = async ( try { await callbacks.run('federation.beforeAddUserToARoom', { user: member, inviter: owner }, room); + await callbacks.run('beforeAddedToRoom', { user: member, inviter: owner }); } catch (error) { continue; } diff --git a/apps/meteor/app/livechat/server/lib/Helper.js b/apps/meteor/app/livechat/server/lib/Helper.js index d4dac0d6acf7..98a03ce3001d 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.js +++ b/apps/meteor/app/livechat/server/lib/Helper.js @@ -382,7 +382,6 @@ export const forwardRoomToAgent = async (room, transferData) => { if (oldServedBy && servedBy._id !== oldServedBy._id) { await RoutingManager.removeAllRoomSubscriptions(room, servedBy); } - await Message.saveSystemMessage('uj', rid, servedBy.username, servedBy); setImmediate(() => { Apps.triggerEvent(AppEvents.IPostLivechatRoomTransferred, { @@ -505,7 +504,7 @@ export const forwardRoomToDepartment = async (room, guest, transferData) => { await LivechatRooms.removeAgentByRoomId(rid); await dispatchAgentDelegated(rid, null); const newInquiry = await LivechatInquiry.findOneById(inquiry._id); - await queueInquiry(room, newInquiry); + await queueInquiry(newInquiry); logger.debug(`Inquiry ${inquiry._id} queued succesfully`); } diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.js b/apps/meteor/app/livechat/server/lib/QueueManager.js index fe44d023ebb7..644c8833cd6c 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.js +++ b/apps/meteor/app/livechat/server/lib/QueueManager.js @@ -14,7 +14,7 @@ export const saveQueueInquiry = async (inquiry) => { await callbacks.run('livechat.afterInquiryQueued', inquiry); }; -export const queueInquiry = async (room, inquiry, defaultAgent) => { +export const queueInquiry = async (inquiry, defaultAgent) => { const inquiryAgent = await RoutingManager.delegateAgent(defaultAgent, inquiry); logger.debug(`Delegating inquiry with id ${inquiry._id} to agent ${defaultAgent?.username}`); @@ -70,7 +70,7 @@ export const QueueManager = { await LivechatRooms.updateRoomCount(); - await queueInquiry(room, inquiry, agent); + await queueInquiry(inquiry, agent); logger.debug(`Inquiry ${inquiry._id} queued`); const newRoom = await LivechatRooms.findOneById(rid); @@ -126,7 +126,7 @@ export const QueueManager = { const inquiry = await LivechatInquiry.findOneById(await createLivechatInquiry({ rid, name, guest, message, extraData: { source } })); logger.debug(`Generated inquiry for visitor ${v._id} with id ${inquiry._id} [Not queued]`); - await queueInquiry(room, inquiry, defaultAgent); + await queueInquiry(inquiry, defaultAgent); logger.debug(`Inquiry ${inquiry._id} queued`); return room; diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.js b/apps/meteor/app/livechat/server/lib/RoutingManager.js index 17bb2e9f1561..1419ac73868d 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.js +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.js @@ -106,7 +106,7 @@ export const RoutingManager = { const user = await Users.findOneById(agent.agentId); const room = await LivechatRooms.findOneById(rid); - await Message.saveSystemMessage('command', rid, 'connected', user); + await Promise.all([Message.saveSystemMessage('command', rid, 'connected', user), Message.saveSystemMessage('uj', rid, '', user)]); await dispatchAgentDelegated(rid, agent.agentId); logger.debug(`Agent ${agent.agentId} assigned to inquriy ${inquiry._id}. Instances notified`); @@ -176,7 +176,7 @@ export const RoutingManager = { return room; } - if (room.servedBy && room.servedBy._id === agent.agentId && !room.onHold) { + if (room.servedBy && room.servedBy._id === agent.agentId) { logger.debug(`Cannot take Inquiry ${inquiry._id}: Already taken by agent ${room.servedBy._id}`); return room; } diff --git a/apps/meteor/app/theme/client/imports/components/emoji.css b/apps/meteor/app/theme/client/imports/components/emoji.css index f7c641be4379..4d7f19f4acb6 100644 --- a/apps/meteor/app/theme/client/imports/components/emoji.css +++ b/apps/meteor/app/theme/client/imports/components/emoji.css @@ -20,8 +20,3 @@ line-height: normal; image-rendering: auto; } - -.emoji.big { - width: 44px; - height: 44px; -} diff --git a/apps/meteor/app/ui-message/client/actionButtons/messageBox.ts b/apps/meteor/app/ui-message/client/actionButtons/messageBox.ts index 8bc1c4924bc1..828c6bcaf577 100644 --- a/apps/meteor/app/ui-message/client/actionButtons/messageBox.ts +++ b/apps/meteor/app/ui-message/client/actionButtons/messageBox.ts @@ -28,12 +28,13 @@ export const onAdded = (button: IUIActionButton): void => ), ); }, - action() { + action(params) { void triggerActionButtonAction({ - rid: RoomManager.opened, + rid: params.rid, + tmid: params.tmid, actionId: button.actionId, appId: button.appId, - payload: { context: button.context }, + payload: { context: button.context, message: params.chat.composer?.text }, }); }, }); diff --git a/apps/meteor/app/ui-utils/client/lib/messageBox.ts b/apps/meteor/app/ui-utils/client/lib/messageBox.ts index daad27f622e8..e5cd105741de 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageBox.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageBox.ts @@ -1,20 +1,13 @@ import type { IMessage, IRoom } from '@rocket.chat/core-typings'; -import type { ContextType } from 'react'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import type { ChatContext } from '../../../../client/views/room/contexts/ChatContext'; +import type { ChatAPI } from '../../../../client/lib/chats/ChatAPI'; type MessageBoxAction = { label: TranslationKey; id: string; icon?: string; - action: (params: { - rid: IRoom['_id']; - tmid?: IMessage['_id']; - event: Event; - messageBox: HTMLElement; - chat: ContextType; - }) => void; + action: (params: { rid: IRoom['_id']; tmid?: IMessage['_id']; event: Event; chat: ChatAPI }) => void; condition?: () => boolean; }; diff --git a/apps/meteor/client/components/GenericTable/hooks/usePagination.ts b/apps/meteor/client/components/GenericTable/hooks/usePagination.ts index 31276fd57ce2..803752fda45e 100644 --- a/apps/meteor/client/components/GenericTable/hooks/usePagination.ts +++ b/apps/meteor/client/components/GenericTable/hooks/usePagination.ts @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { useCurrent } from './useCurrent'; import { useItemsPerPage } from './useItemsPerPage'; @@ -18,6 +18,11 @@ export const usePagination = (): { const itemsPerPageLabel = useItemsPerPageLabel(); const showingResultsLabel = useShowingResultsLabel(); + // Reset to first page when itemsPerPage changes + useEffect(() => { + setCurrent(0); + }, [itemsPerPage, setCurrent]); + return useMemo( () => ({ itemsPerPage, diff --git a/apps/meteor/client/components/message/MessageContentBody.tsx b/apps/meteor/client/components/message/MessageContentBody.tsx index 8bfae4082d09..4674528a483f 100644 --- a/apps/meteor/client/components/message/MessageContentBody.tsx +++ b/apps/meteor/client/components/message/MessageContentBody.tsx @@ -1,7 +1,7 @@ import { css } from '@rocket.chat/css-in-js'; -import { MessageBody, Box, Palette } from '@rocket.chat/fuselage'; +import { MessageBody, Box, Palette, Skeleton } from '@rocket.chat/fuselage'; import { Markup } from '@rocket.chat/gazzodown'; -import React from 'react'; +import React, { Suspense } from 'react'; import type { MessageWithMdEnforced } from '../../lib/parseMessageTextToAstMarkdown'; import GazzodownText from '../GazzodownText'; @@ -55,9 +55,11 @@ const MessageContentBody = ({ mentions, channels, md, searchText }: MessageConte return ( - - - + }> + + + + ); diff --git a/apps/meteor/client/sidebar/search/SearchList.tsx b/apps/meteor/client/sidebar/search/SearchList.tsx index e48c52e0b68f..300365f36449 100644 --- a/apps/meteor/client/sidebar/search/SearchList.tsx +++ b/apps/meteor/client/sidebar/search/SearchList.tsx @@ -101,7 +101,7 @@ const useSearchItems = (filterText: string): UseQueryResult<(ISubscription & IRo const getSpotlight = useMethod('spotlight'); return useQuery( - ['sidebar/search/spotlight', name, usernamesFromClient, type], + ['sidebar/search/spotlight', name, usernamesFromClient, type, localRooms.map(({ _id }) => _id)], async () => { if (localRooms.length === LIMIT) { return localRooms; diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/DepartmentAgentsTable.tsx b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/DepartmentAgentsTable.tsx index 2f2f8d39e995..c4e4c7acb7c6 100644 --- a/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/DepartmentAgentsTable.tsx +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/DepartmentAgentsTable.tsx @@ -1,9 +1,11 @@ +import { Pagination } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; +import React, { useMemo } from 'react'; import type { Control, UseFormRegister } from 'react-hook-form'; import { useWatch, useFieldArray } from 'react-hook-form'; import { GenericTable, GenericTableBody, GenericTableHeader, GenericTableHeaderCell } from '../../../../components/GenericTable'; +import { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; import type { FormValues } from '../EditDepartment'; import AddAgent from './AddAgent'; import AgentRow from './AgentRow'; @@ -18,6 +20,9 @@ function DepartmentAgentsTable({ control, register }: DepartmentAgentsTableProps const { fields, append, remove } = useFieldArray({ control, name: 'agentList' }); const agentList = useWatch({ control, name: 'agentList' }); + const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination(); + const page = useMemo(() => fields.slice(current, current + itemsPerPage), [current, fields, itemsPerPage]); + return ( <> @@ -31,11 +36,21 @@ function DepartmentAgentsTable({ control, register }: DepartmentAgentsTableProps - {fields.map((agent, index) => ( + {page.map((agent, index) => ( remove(index)} /> ))} + + ); } diff --git a/apps/meteor/client/views/room/UserCard/UserCardWithData.tsx b/apps/meteor/client/views/room/UserCard/UserCardWithData.tsx index b9c701cc4428..cf43ff90d163 100644 --- a/apps/meteor/client/views/room/UserCard/UserCardWithData.tsx +++ b/apps/meteor/client/views/room/UserCard/UserCardWithData.tsx @@ -5,12 +5,12 @@ import { useSetting, useRolesDescription } from '@rocket.chat/ui-contexts'; import type { ReactElement, UIEvent } from 'react'; import React, { useMemo, useRef } from 'react'; +import { getUserDisplayName } from '../../../../lib/getUserDisplayName'; import { Backdrop } from '../../../components/Backdrop'; import LocalTime from '../../../components/LocalTime'; import UserCard from '../../../components/UserCard'; import { ReactiveUserStatus } from '../../../components/UserStatus'; -import { AsyncStatePhase } from '../../../hooks/useAsyncState'; -import { useEndpointData } from '../../../hooks/useEndpointData'; +import { useUserInfoQuery } from '../../../hooks/useUserInfoQuery'; import { useActionSpread } from '../../hooks/useActionSpread'; import { useUserInfoActions } from '../hooks/useUserInfoActions'; @@ -25,21 +25,18 @@ type UserCardWithDataProps = { const UserCardWithData = ({ username, target, rid, open, onClose }: UserCardWithDataProps): ReactElement => { const ref = useRef(target); const getRoles = useRolesDescription(); - const showRealNames = useSetting('UI_Use_Real_Name'); + const showRealNames = Boolean(useSetting('UI_Use_Real_Name')); - const query = useMemo(() => ({ username }), [username]); - const { value: data, phase: state } = useEndpointData('/v1/users.info', { params: query }); + const { data, isLoading } = useUserInfoQuery({ username }); ref.current = target; - const isLoading = state === AsyncStatePhase.LOADING; - const user = useMemo(() => { const defaultValue = isLoading ? undefined : null; const { _id, - name = username, + name, roles = defaultValue, statusText = defaultValue, bio = defaultValue, @@ -50,7 +47,7 @@ const UserCardWithData = ({ username, target, rid, open, onClose }: UserCardWith return { _id, - name: showRealNames ? name : username, + name: getUserDisplayName(name, username, showRealNames), username, roles: roles && getRoles(roles).map((role, index) => {role}), bio, diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/hooks/useResumeChatOnHoldMutation.ts b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/hooks/useResumeChatOnHoldMutation.ts index 33160291e26f..9992be969758 100644 --- a/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/hooks/useResumeChatOnHoldMutation.ts +++ b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/hooks/useResumeChatOnHoldMutation.ts @@ -1,18 +1,20 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { useMethod } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import type { UseMutationOptions, UseMutationResult } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query'; export const useResumeChatOnHoldMutation = ( options?: Omit, 'mutationFn'>, ): UseMutationResult => { - const resumeChatOnHold = useMethod('livechat:resumeOnHold'); + const resumeChatOnHold = useEndpoint('POST', '/v1/livechat/room.resumeOnHold'); + + const dispatchToastMessage = useToastMessageDispatch(); const queryClient = useQueryClient(); return useMutation( - async (rid) => { - await resumeChatOnHold(rid, { clientAction: true }); + async (roomId) => { + await resumeChatOnHold({ roomId }); }, { ...options, @@ -22,6 +24,9 @@ export const useResumeChatOnHoldMutation = ( await queryClient.invalidateQueries(['subscriptions', { rid }]); return options?.onSuccess?.(data, rid, context); }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, }, ); }; diff --git a/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/ActionsToolbarDropdown.tsx b/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/ActionsToolbarDropdown.tsx index e0b26f190757..406160124696 100644 --- a/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/ActionsToolbarDropdown.tsx +++ b/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBoxActionsToolbar/ActionsToolbarDropdown.tsx @@ -7,6 +7,7 @@ import React, { useRef, Fragment } from 'react'; import { messageBox } from '../../../../../../../../app/ui-utils/client'; import type { ChatAPI } from '../../../../../../../lib/chats/ChatAPI'; import { useDropdownVisibility } from '../../../../../../../sidebar/header/hooks/useDropdownVisibility'; +import { useChat } from '../../../../../contexts/ChatContext'; import CreateDiscussionAction from './actions/CreateDiscussionAction'; import ShareLocationAction from './actions/ShareLocationAction'; import WebdavAction from './actions/WebdavAction'; @@ -19,7 +20,13 @@ type ActionsToolbarDropdownProps = { actions?: ReactNode[]; }; -const ActionsToolbarDropdown = ({ chatContext, isRecording, rid, tmid, actions, ...props }: ActionsToolbarDropdownProps) => { +const ActionsToolbarDropdown = ({ isRecording, rid, tmid, actions, ...props }: ActionsToolbarDropdownProps) => { + const chatContext = useChat(); + + if (!chatContext) { + throw new Error('useChat must be used within a ChatProvider'); + } + const t = useTranslation(); const reference = useRef(null); const target = useRef(null); @@ -66,7 +73,17 @@ const ActionsToolbarDropdown = ({ chatContext, isRecording, rid, tmid, actions, {actionGroup.title} {actionGroup.items.map((item) => ( - diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts index 9a4ff6f2490f..51f424697a82 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts +++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts @@ -82,8 +82,9 @@ export const useThreadMainMessageQuery = ( useEffect(() => { return () => { unsubscribeRef.current?.(); + unsubscribeRef.current = undefined; }; - }, []); + }, [tmid]); return useQuery( ['rooms', room._id, 'threads', tmid, 'main-message'] as const, @@ -96,17 +97,17 @@ export const useThreadMainMessageQuery = ( throw new Error('Invalid main message'); } - unsubscribeRef.current?.(); - - unsubscribeRef.current = subscribeToMessage(mainMessage, { - onMutate: () => { - queryClient.invalidateQueries(queryKey, { exact: true }); - }, - onDelete: () => { - onDelete?.(); - queryClient.invalidateQueries(queryKey, { exact: true }); - }, - }); + unsubscribeRef.current = + unsubscribeRef.current || + subscribeToMessage(mainMessage, { + onMutate: () => { + queryClient.invalidateQueries(queryKey, { exact: true }); + }, + onDelete: () => { + onDelete?.(); + queryClient.invalidateQueries(queryKey, { exact: true }); + }, + }); return mainMessage; }, diff --git a/apps/meteor/ee/app/license/server/license.ts b/apps/meteor/ee/app/license/server/license.ts index f8cd98dd3245..21f0b3e7b61c 100644 --- a/apps/meteor/ee/app/license/server/license.ts +++ b/apps/meteor/ee/app/license/server/license.ts @@ -21,6 +21,7 @@ interface IValidLicense { } let maxGuestUsers = 0; +let maxRoomsPerGuest = 0; let maxActiveUsers = 0; class LicenseClass { @@ -192,6 +193,10 @@ class LicenseClass { maxGuestUsers = license.maxGuestUsers; } + if (license.maxRoomsPerGuest > maxRoomsPerGuest) { + maxRoomsPerGuest = license.maxRoomsPerGuest; + } + if (license.maxActiveUsers > maxActiveUsers) { maxActiveUsers = license.maxActiveUsers; } @@ -323,6 +328,10 @@ export function getMaxGuestUsers(): number { return maxGuestUsers; } +export function getMaxRoomsPerGuest(): number { + return maxRoomsPerGuest; +} + export function getMaxActiveUsers(): number { return maxActiveUsers; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts index 663110670d3b..97ce66d4e395 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts @@ -1,56 +1,77 @@ -import { Meteor } from 'meteor/meteor'; -import { isPOSTLivechatRoomPriorityParams } from '@rocket.chat/rest-typings'; +import { isLivechatRoomOnHoldProps, isLivechatRoomResumeOnHoldProps, isPOSTLivechatRoomPriorityParams } from '@rocket.chat/rest-typings'; import { LivechatRooms, Subscriptions } from '@rocket.chat/models'; +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { OmnichannelEEService } from '@rocket.chat/core-services'; import { API } from '../../../../../app/api/server'; import { hasPermissionAsync } from '../../../../../app/authorization/server/functions/hasPermission'; -import { LivechatEnterprise } from '../lib/LivechatEnterprise'; import { removePriorityFromRoom, updateRoomPriority } from './lib/priorities'; import { i18n } from '../../../../../server/lib/i18n'; API.v1.addRoute( 'livechat/room.onHold', - { authRequired: true, permissionsRequired: ['on-hold-livechat-room'] }, + { authRequired: true, permissionsRequired: ['on-hold-livechat-room'], validateParams: isLivechatRoomOnHoldProps }, { async post() { const { roomId } = this.bodyParams; - if (!roomId || roomId.trim() === '') { - return API.v1.failure('Invalid room Id'); - } - const room = await LivechatRooms.findOneById(roomId); - if (!room || room.t !== 'l') { - return API.v1.failure('Invalid room Id'); - } + type Room = Pick; - if (room.lastMessage?.token) { - return API.v1.failure('You cannot place chat on-hold, when the Contact has sent the last message'); + const room = await LivechatRooms.findOneById(roomId, { + projection: { _id: 1, t: 1, open: 1, onHold: 1, lastMessage: 1, servedBy: 1 }, + }); + if (!room) { + throw new Error('error-invalid-room'); } - if (room.onHold) { - return API.v1.failure('Room is already On-Hold'); + const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId, { projection: { _id: 1 } }); + if (!subscription && !(await hasPermissionAsync(this.userId, 'on-hold-others-livechat-room'))) { + throw new Error('Not_authorized'); } - if (!room.open) { - return API.v1.failure('Room cannot be placed on hold after being closed'); + const onHoldBy = { _id: this.userId, username: this.user.username, name: this.user.name }; + const comment = i18n.t('Omnichannel_On_Hold_manually', { + user: onHoldBy.name || `@${onHoldBy.username}`, + }); + + await OmnichannelEEService.placeRoomOnHold(room, comment, this.user); + + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'livechat/room.resumeOnHold', + { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isLivechatRoomResumeOnHoldProps }, + { + async post() { + const { roomId } = this.bodyParams; + if (!roomId || roomId.trim() === '') { + throw new Error('invalid-param'); } - const user = await Meteor.userAsync(); - if (!user) { - return API.v1.failure('Invalid user'); + type Room = Pick; + + const room = await LivechatRooms.findOneById(roomId, { + projection: { t: 1, open: 1, onHold: 1, servedBy: 1 }, + }); + if (!room) { + throw new Error('error-invalid-room'); } - const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, user._id, { projection: { _id: 1 } }); + const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId, { projection: { _id: 1 } }); if (!subscription && !(await hasPermissionAsync(this.userId, 'on-hold-others-livechat-room'))) { - return API.v1.failure('Not authorized'); + throw new Error('Not_authorized'); } - const onHoldBy = { _id: user._id, username: user.username, name: (user as any).name }; + const { name, username, _id: userId } = this.user; + const onHoldBy = { _id: userId, username, name }; const comment = i18n.t('Omnichannel_On_Hold_manually', { user: onHoldBy.name || `@${onHoldBy.username}`, }); - await LivechatEnterprise.placeRoomOnHold(room, comment, onHoldBy); + await OmnichannelEEService.resumeRoomOnHold(room, comment, this.user, true); return API.v1.success(); }, diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts index bb8f45b2c65e..99c563bbdb24 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts @@ -1,3 +1,5 @@ +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; + import { callbacks } from '../../../../../lib/callbacks'; import { settings } from '../../../../../app/settings/server'; import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; @@ -6,7 +8,7 @@ import { i18n } from '../../../../../server/lib/i18n'; let autoCloseOnHoldChatTimeout = 0; -const handleAfterOnHold = async (room: any = {}): Promise => { +const handleAfterOnHold = async (room: Pick): Promise => { const { _id: rid } = room; if (!rid) { cbLogger.debug('Skipping callback. No room provided'); @@ -24,7 +26,7 @@ const handleAfterOnHold = async (room: any = {}): Promise => { i18n.t('Closed_automatically_because_chat_was_onhold_for_seconds', { onHoldTime: autoCloseOnHoldChatTimeout, }); - await AutoCloseOnHoldScheduler.scheduleRoom(room._id, autoCloseOnHoldChatTimeout, closeComment); + await AutoCloseOnHoldScheduler.scheduleRoom(rid, autoCloseOnHoldChatTimeout, closeComment); }; settings.watch('Livechat_auto_close_on_hold_chats_timeout', (value) => { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts index b4e9b185a559..c631656a2a07 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts @@ -1,20 +1,28 @@ +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; + import { callbacks } from '../../../../../lib/callbacks'; -import { LivechatEnterprise } from '../lib/LivechatEnterprise'; +import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; import { cbLogger } from '../lib/logger'; -const handleAfterOnHoldChatResumed = async (room: any): Promise => { - if (!room?._id || !room.onHold) { - cbLogger.debug('Skipping callback. No room provided or room is not on hold'); - return; +type IRoom = Pick; + +const handleAfterOnHoldChatResumed = async (room: IRoom): Promise => { + if (!room?._id) { + cbLogger.debug('Skipping callback. No room provided'); + return room; } - cbLogger.debug(`Removing current on hold timers for room ${room._id}`); - void LivechatEnterprise.releaseOnHoldChat(room); + const { _id: roomId } = room; + + cbLogger.debug(`Removing current on hold timers for room ${roomId}`); + await AutoCloseOnHoldScheduler.unscheduleRoom(roomId); + + return room; }; callbacks.add( 'livechat:afterOnHoldChatResumed', - (room) => handleAfterOnHoldChatResumed(room), + handleAfterOnHoldChatResumed, callbacks.priority.HIGH, 'livechat-after-on-hold-chat-resumed', ); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts index 5ee6d446a449..18131d9b60d3 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts @@ -1,10 +1,6 @@ -import { Subscriptions, LivechatInquiry, LivechatRooms } from '@rocket.chat/models'; - import { callbacks } from '../../../../../lib/callbacks'; -import { queueInquiry } from '../../../../../app/livechat/server/lib/QueueManager'; import { settings } from '../../../../../app/settings/server'; import { cbLogger } from '../lib/logger'; -import { dispatchAgentDelegated } from '../../../../../app/livechat/server/lib/Helper'; const handleOnAgentAssignmentFailed = async ({ inquiry, @@ -23,24 +19,6 @@ const handleOnAgentAssignmentFailed = async ({ return; } - if (room.onHold) { - cbLogger.debug('Room is on hold. Removing current assignations before queueing again'); - const { _id: roomId } = room; - - const { _id: inquiryId } = inquiry; - await LivechatInquiry.queueInquiryAndRemoveDefaultAgent(inquiryId); - await LivechatRooms.removeAgentByRoomId(roomId); - await Subscriptions.removeByRoomId(roomId); - await dispatchAgentDelegated(roomId, null); - - const newInquiry = await LivechatInquiry.findOneById(inquiryId); - - await queueInquiry(room, newInquiry); - - cbLogger.debug('Room queued successfully'); - return; - } - if (!settings.get('Livechat_waiting_queue')) { cbLogger.debug('Skipping callback. Queue disabled by setting'); return; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.ts index e80445e1c057..59d96b3fac1b 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.ts @@ -1,24 +1,46 @@ +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { LivechatRooms, Subscriptions } from '@rocket.chat/models'; + import { callbacks } from '../../../../../lib/callbacks'; import { settings } from '../../../../../app/settings/server'; import { debouncedDispatchWaitingQueueStatus } from '../lib/Helper'; -import { LivechatEnterprise } from '../lib/LivechatEnterprise'; +import { callbackLogger } from '../../../../../app/livechat/server/lib/callbackLogger'; +import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; -callbacks.add( - 'livechat.closeRoom', - async (params) => { - const { room } = params; +type LivechatCloseCallbackParams = { + room: IOmnichannelRoom; +}; - await LivechatEnterprise.releaseOnHoldChat(room); +const onCloseLivechat = async (params: LivechatCloseCallbackParams) => { + const { + room, + room: { _id: roomId }, + } = params; - if (!settings.get('Livechat_waiting_queue')) { - return params; - } + callbackLogger.debug(`[onCloseLivechat] clearing onHold related data for room ${roomId}`); - const { departmentId } = room || {}; - debouncedDispatchWaitingQueueStatus(departmentId); + await Promise.all([ + LivechatRooms.unsetOnHoldByRoomId(roomId), + Subscriptions.unsetOnHoldByRoomId(roomId), + AutoCloseOnHoldScheduler.unscheduleRoom(roomId), + ]); + callbackLogger.debug(`[onCloseLivechat] clearing onHold related data for room ${roomId} completed`); + + if (!settings.get('Livechat_waiting_queue')) { return params; - }, + } + + const { departmentId } = room || {}; + callbackLogger.debug(`[onCloseLivechat] dispatching waiting queue status for department ${departmentId}`); + debouncedDispatchWaitingQueueStatus(departmentId); + + return params; +}; + +callbacks.add( + 'livechat.closeRoom', + (params: LivechatCloseCallbackParams) => onCloseLivechat(params), callbacks.priority.HIGH, 'livechat-waiting-queue-monitor-close-room', ); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/resumeOnHold.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/resumeOnHold.ts index 9bb92947b151..8682f03852b1 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/resumeOnHold.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/resumeOnHold.ts @@ -1,46 +1,68 @@ -import { Meteor } from 'meteor/meteor'; -import { isOmnichannelRoom, isEditedMessage } from '@rocket.chat/core-typings'; -import { LivechatRooms } from '@rocket.chat/models'; +import type { ILivechatVisitor, IMessage, IOmnichannelRoom, IRoom, IUser } from '@rocket.chat/core-typings'; +import { isEditedMessage, isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { LivechatRooms, LivechatVisitors, Users } from '@rocket.chat/models'; +import { OmnichannelEEService } from '@rocket.chat/core-services'; import { callbacks } from '../../../../../lib/callbacks'; +import { callbackLogger } from '../../../../../app/livechat/server/lib/callbackLogger'; +import { i18n } from '../../../../../server/lib/i18n'; -callbacks.add( - 'afterSaveMessage', - async (message, roomParams) => { - // skips this callback if the message was edited - if (isEditedMessage(message)) { - return message; - } +const resumeOnHoldCommentAndUser = async (room: IOmnichannelRoom): Promise<{ comment: string; resumedBy: IUser }> => { + const { + v: { _id: visitorId }, + _id: rid, + } = room; + const visitor = await LivechatVisitors.findOneById>(visitorId, { + projection: { name: 1, username: 1 }, + }); + if (!visitor) { + callbackLogger.error(`[afterSaveMessage] Visitor Not found for room ${rid} while trying to resume on hold`); + throw new Error('Visitor not found while trying to resume on hold'); + } - // if the message has a type means it is a special message (like the closing comment), so skips - if (message.t) { - return message; - } + const guest = visitor.name || visitor.username; - if (!isOmnichannelRoom(roomParams)) { - return message; - } + const resumeChatComment = i18n.t('Omnichannel_on_hold_chat_automatically', { guest }); - const { _id: rid, t: roomType, v: roomVisitor } = roomParams; + const resumedBy = await Users.findOneById('rocket.cat'); + if (!resumedBy) { + callbackLogger.error(`[afterSaveMessage] User Not found for room ${rid} while trying to resume on hold`); + throw new Error(`User not found while trying to resume on hold`); + } - // message valid only if it is a livechat room - if (!(typeof roomType !== 'undefined' && roomType === 'l' && roomVisitor && roomVisitor.token)) { - return message; - } + return { comment: resumeChatComment, resumedBy }; +}; + +const handleAfterSaveMessage = async (message: IMessage, room: IRoom) => { + if (isEditedMessage(message) || message.t || !isOmnichannelRoom(room)) { + return message; + } + + const { _id: rid, v: roomVisitor } = room; - // Need to read the room every time, the room object is not updated - const room = await LivechatRooms.findOneById(rid, { projection: { t: 1, v: 1, onHold: 1 } }); - if (!room) { + if (!roomVisitor?._id) { + return message; + } + + // Need to read the room every time, the room object is not updated + const updatedRoom = await LivechatRooms.findOneById(rid); + if (!updatedRoom) { + return message; + } + + if (message.token && room.onHold) { + callbackLogger.debug(`[afterSaveMessage] Room ${rid} is on hold, resuming it now since visitor sent a message`); + + try { + const { comment: resumeChatComment, resumedBy } = await resumeOnHoldCommentAndUser(updatedRoom); + await OmnichannelEEService.resumeRoomOnHold(updatedRoom, resumeChatComment, resumedBy); + } catch (error) { + callbackLogger.error(`[afterSaveMessage] Error while resuming room ${rid} on hold: Error: `, error); return message; } + } - // if a visitor sends a message in room which is On Hold - if (message.token && room.onHold) { - await Meteor.callAsync('livechat:resumeOnHold', rid, { clientAction: false }); - } + return message; +}; - return message; - }, - callbacks.priority.HIGH, - 'livechat-resume-on-hold', -); +callbacks.add('afterSaveMessage', handleAfterSaveMessage, callbacks.priority.HIGH, 'livechat-resume-on-hold'); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/scheduleAutoTransfer.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/scheduleAutoTransfer.ts index 831d8f3b29a6..a23e4686b438 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/scheduleAutoTransfer.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/scheduleAutoTransfer.ts @@ -1,4 +1,5 @@ -import type { IMessage, IOmnichannelRoom } from '@rocket.chat/core-typings'; +import type { IMessage, IOmnichannelRoom, IRoom } from '@rocket.chat/core-typings'; +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms } from '@rocket.chat/models'; import { AutoTransferChatScheduler } from '../lib/AutoTransferChatScheduler'; @@ -16,7 +17,7 @@ let autoTransferTimeout = 0; const handleAfterTakeInquiryCallback = async (inquiry: any = {}): Promise => { const { rid } = inquiry; - if (!rid || !rid.trim()) { + if (!rid?.trim()) { cbLogger.debug('Skipping callback. Invalid room id'); return; } @@ -43,15 +44,25 @@ const handleAfterTakeInquiryCallback = async (inquiry: any = {}): Promise = return inquiry; }; -const handleAfterSaveMessage = async (message: any = {}, room: any = {}): Promise => { - const { _id: rid, t, autoTransferredAt, autoTransferOngoing } = room; - const { token } = message; +const handleAfterSaveMessage = async (message: IMessage, room: IRoom | undefined): Promise => { + if (!room || !isOmnichannelRoom(room)) { + return message; + } + + const { _id: rid, autoTransferredAt, autoTransferOngoing } = room; + const { token, t: messageType } = message; + + if (messageType) { + // ignore system messages + return message; + } if (!autoTransferTimeout || autoTransferTimeout <= 0) { return message; } - if (!rid || !message || rid === '' || t !== 'l' || token) { + if (!message || token) { + // ignore messages from visitors return message; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/index.ts b/apps/meteor/ee/app/livechat-enterprise/server/index.ts index 0cd5321b41fa..4233260aabc5 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/index.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/index.ts @@ -17,7 +17,6 @@ import './hooks/checkAgentBeforeTakeInquiry'; import './hooks/handleNextAgentPreferredEvents'; import './hooks/onCheckRoomParamsApi'; import './hooks/onLoadConfigApi'; -import './hooks/onCloseLivechat'; import './hooks/onSaveVisitorInfo'; import './hooks/scheduleAutoTransfer'; import './hooks/resumeOnHold'; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts index 975519e16a87..8434fcce07f0 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts @@ -69,7 +69,7 @@ class AutoTransferChatSchedulerClass { await this.scheduler.cancel({ name: jobName }); } - private async transferRoom(roomId: string): Promise { + private async transferRoom(roomId: string): Promise { this.logger.debug(`Transferring room ${roomId}`); const room = await LivechatRooms.findOneById(roomId, { _id: 1, @@ -79,7 +79,7 @@ class AutoTransferChatSchedulerClass { departmentId: 1, }); if (!room?.open || !room?.servedBy?._id) { - return false; + throw new Error('Room is not open or is not being served by an agent'); } const { @@ -91,37 +91,42 @@ class AutoTransferChatSchedulerClass { if (!RoutingManager.getConfig().autoAssignAgent) { this.logger.debug(`Auto-assign agent is disabled, returning room ${roomId} as inquiry`); - return Livechat.returnRoomAsInquiry(room._id, departmentId, { + + await Livechat.returnRoomAsInquiry(room._id, departmentId, { scope: 'autoTransferUnansweredChatsToQueue', comment: timeoutDuration, transferredBy: await this.getSchedulerUser(), }); + return; } const agent = await RoutingManager.getNextAgent(departmentId, ignoreAgentId); - if (agent) { - this.logger.debug(`Transferring room ${roomId} to agent ${agent.agentId}`); - return forwardRoomToAgent(room, { - userId: agent.agentId, - transferredBy: await this.getSchedulerUser(), - transferredTo: agent, - scope: 'autoTransferUnansweredChatsToAgent', - comment: timeoutDuration, - }); + if (!agent) { + this.logger.error(`No agent found to transfer room ${room._id} which hasn't been answered in ${timeoutDuration} seconds`); + return; } - this.logger.debug(`No agent found to transfer room ${roomId}`); - return false; + this.logger.debug(`Transferring room ${roomId} to agent ${agent.agentId}`); + + await forwardRoomToAgent(room, { + userId: agent.agentId, + transferredBy: await this.getSchedulerUser(), + transferredTo: agent, + scope: 'autoTransferUnansweredChatsToAgent', + comment: timeoutDuration, + }); } private async executeJob({ attrs: { data } }: any = {}): Promise { const { roomId } = data; - if (await this.transferRoom(roomId)) { - await LivechatRooms.setAutoTransferredAtById(roomId); - } + try { + await this.transferRoom(roomId); - await this.unscheduleRoom(roomId); + await Promise.all([LivechatRooms.setAutoTransferredAtById(roomId), this.unscheduleRoom(roomId)]); + } catch (error) { + this.logger.error(`Error while executing job ${SCHEDULER_NAME} for room ${roomId}:`, error); + } } } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts index 72534e72d021..15dd04e335eb 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts @@ -3,17 +3,14 @@ import { Match, check } from 'meteor/check'; import { LivechatInquiry, Users, - LivechatRooms, LivechatDepartment as LivechatDepartmentRaw, OmnichannelServiceLevelAgreements, LivechatTag, LivechatUnitMonitors, LivechatUnit, } from '@rocket.chat/models'; -import { Message } from '@rocket.chat/core-services'; import type { IOmnichannelBusinessUnit, - IOmnichannelRoom, IOmnichannelServiceLevelAgreements, LivechatDepartmentDTO, InquiryWithAgentInfo, @@ -27,9 +24,7 @@ import { processWaitingQueue, updateSLAInquiries } from './Helper'; import { removeSLAFromRooms } from './SlaHelper'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; import { settings } from '../../../../../app/settings/server'; -import { logger, queueLogger } from './logger'; -import { callbacks } from '../../../../../lib/callbacks'; -import { AutoCloseOnHoldScheduler } from './AutoCloseOnHoldScheduler'; +import { queueLogger } from './logger'; import { getInquirySortMechanismSetting } from '../../../../../app/livechat/server/lib/settings'; export const LivechatEnterprise = { @@ -191,33 +186,6 @@ export const LivechatEnterprise = { await removeSLAFromRooms(_id); }, - async placeRoomOnHold(room: IOmnichannelRoom, comment: string, onHoldBy: { _id: string; username?: string; name?: string }) { - logger.debug(`Attempting to place room ${room._id} on hold by user ${onHoldBy?._id}`); - const { _id: roomId, onHold } = room; - if (!roomId || onHold) { - logger.debug(`Room ${roomId} invalid or already on hold. Skipping`); - return false; - } - await LivechatRooms.setOnHoldByRoomId(roomId); - - await Message.saveSystemMessage('omnichannel_placed_chat_on_hold', roomId, '', onHoldBy, { comment }); - - await callbacks.run('livechat:afterOnHold', room); - - logger.debug(`Room ${room._id} set on hold succesfully`); - return true; - }, - - async releaseOnHoldChat(room: IOmnichannelRoom) { - const { _id: roomId, onHold } = room; - if (!roomId || !onHold) { - return; - } - - await AutoCloseOnHoldScheduler.unscheduleRoom(roomId); - await LivechatRooms.unsetOnHoldAndPredictedVisitorAbandonmentByRoomId(roomId); - }, - /** * @param {string|null} _id - The department id * @param {Partial} departmentData diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts index 376b0783f95b..845b52be2d16 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts @@ -1,10 +1,10 @@ import type { IOmnichannelRoom, IUser } from '@rocket.chat/core-typings'; import { LivechatVisitors, LivechatRooms, LivechatDepartment, Users } from '@rocket.chat/models'; +import { OmnichannelEEService } from '@rocket.chat/core-services'; import { cronJobs } from '@rocket.chat/cron'; import { settings } from '../../../../../app/settings/server'; import { Livechat } from '../../../../../app/livechat/server/lib/LivechatTyped'; -import { LivechatEnterprise } from './LivechatEnterprise'; import { i18n } from '../../../../../server/lib/i18n'; import { callbacks } from '../../../../../lib/callbacks'; import { schedulerLogger } from './logger'; @@ -123,7 +123,7 @@ export class VisitorInactivityMonitor { const comment = i18n.t('Omnichannel_On_Hold_due_to_inactivity', { guest, timeout }); const result = await Promise.allSettled([ - LivechatEnterprise.placeRoomOnHold(room, comment, this.user), + OmnichannelEEService.placeRoomOnHold(room, comment, this.user), LivechatRooms.unsetPredictedVisitorAbandonmentByRoomId(room._id), ]); this.logger.debug(`Room ${room._id} placed on hold`); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts b/apps/meteor/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts index 7842dd8028fe..5a66cf219991 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts @@ -1,11 +1,13 @@ import { Meteor } from 'meteor/meteor'; -import type { ILivechatVisitor } from '@rocket.chat/core-typings'; +import type { ILivechatVisitor, IOmnichannelSystemMessage } from '@rocket.chat/core-typings'; +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatVisitors, LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models'; -import { Message } from '@rocket.chat/core-services'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; +import { Message } from '@rocket.chat/core-services'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; import { callbacks } from '../../../../../lib/callbacks'; +import { methodDeprecationLogger } from '../../../../../app/lib/server/lib/deprecationWarningLogger'; import { i18n } from '../../../../../server/lib/i18n'; async function resolveOnHoldCommentInfo(options: { clientAction: boolean }, room: any, onHoldChatResumedBy: any): Promise { @@ -38,8 +40,12 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async 'livechat:resumeOnHold'(roomId, options = { clientAction: false }) { + methodDeprecationLogger.warn( + 'Method "livechat:resumeOnHold" is deprecated and will be removed in next major version. Please use "livechat/room.resumeOnHold" API instead.', + ); + const room = await LivechatRooms.findOneById(roomId); - if (!room || room.t !== 'l') { + if (!room || !isOmnichannelRoom(room)) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'livechat:resumeOnHold', }); @@ -58,19 +64,30 @@ Meteor.methods({ }); } - const { servedBy: { _id: agentId, username } = {} } = room; + if (!room.servedBy) { + throw new Meteor.Error('error-unserved-rooms-cannot-be-placed-onhold', 'Error! Un-served rooms cannot be placed OnHold', { + method: 'livechat:resumeOnHold', + }); + } + + const { + servedBy: { _id: agentId, username }, + } = room; await RoutingManager.takeInquiry(inquiry, { agentId, username }, options); const onHoldChatResumedBy = options.clientAction ? await Meteor.userAsync() : await Users.findOneById('rocket.cat'); if (!onHoldChatResumedBy) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:resumeOnHold' }); + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'livechat:resumeOnHold', + }); } const comment = await resolveOnHoldCommentInfo(options, room, onHoldChatResumedBy); - await Message.saveSystemMessage('omnichannel_on_hold_chat_resumed', roomId, '', onHoldChatResumedBy, { comment }); + await Message.saveSystemMessage('omnichannel_on_hold_chat_resumed', roomId, '', onHoldChatResumedBy, { + comment, + }); - const updatedRoom = await LivechatRooms.findOneById(roomId); - updatedRoom && setImmediate(() => callbacks.run('livechat:afterOnHoldChatResumed', updatedRoom)); + setImmediate(() => callbacks.run('livechat:afterOnHoldChatResumed', room)); }, }); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts b/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts new file mode 100644 index 000000000000..2b099cd1aa9a --- /dev/null +++ b/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts @@ -0,0 +1,180 @@ +import { ServiceClassInternal, Message } from '@rocket.chat/core-services'; +import type { IOmnichannelEEService } from '@rocket.chat/core-services'; +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import type { IOmnichannelRoom, IUser, ILivechatInquiryRecord, IOmnichannelSystemMessage } from '@rocket.chat/core-typings'; +import { LivechatRooms, Subscriptions, LivechatInquiry } from '@rocket.chat/models'; + +import { Logger } from '../../../../../app/logger/server'; +import { callbacks } from '../../../../../lib/callbacks'; +import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; +import { dispatchAgentDelegated } from '../../../../../app/livechat/server/lib/Helper'; +import { queueInquiry } from '../../../../../app/livechat/server/lib/QueueManager'; + +export class OmnichannelEE extends ServiceClassInternal implements IOmnichannelEEService { + protected name = 'omnichannel-ee'; + + protected internal = true; + + logger: Logger; + + constructor() { + super(); + this.logger = new Logger('OmnichannelEE'); + } + + async placeRoomOnHold( + room: Pick, + comment: string, + onHoldBy: Pick, + ) { + this.logger.debug(`Attempting to place room ${room._id} on hold by user ${onHoldBy?._id}`); + + const { _id: roomId } = room; + + if (!room || !isOmnichannelRoom(room)) { + throw new Error('error-invalid-room'); + } + if (!room.open) { + throw new Error('error-room-already-closed'); + } + if (room.onHold) { + throw new Error('error-room-is-already-on-hold'); + } + if (room.lastMessage?.token) { + throw new Error('error-contact-sent-last-message-so-cannot-place-on-hold'); + } + if (!room.servedBy) { + throw new Error('error-unserved-rooms-cannot-be-placed-onhold'); + } + + await Promise.all([ + LivechatRooms.setOnHoldByRoomId(roomId), + Subscriptions.setOnHoldByRoomId(roomId), + Message.saveSystemMessage('omnichannel_placed_chat_on_hold', roomId, '', onHoldBy, { comment }), + ]); + + await callbacks.run('livechat:afterOnHold', room); + + this.logger.debug(`Room ${room._id} set on hold successfully`); + } + + async resumeRoomOnHold( + room: Pick, + comment: string, + resumeBy: Pick, + clientAction = false, + ) { + this.logger.debug(`Attempting to resume room ${room._id} on hold by user ${resumeBy?._id}`); + + if (!room || !isOmnichannelRoom(room)) { + throw new Error('error-invalid-room'); + } + + if (!room.open) { + throw new Error('This_conversation_is_already_closed'); + } + + if (!room.onHold) { + throw new Error('error-room-not-on-hold'); + } + + const { _id: roomId, servedBy } = room; + + if (!servedBy) { + this.logger.error(`No serving agent found for room ${roomId}`); + throw new Error('error-room-not-served'); + } + + const inquiry = await LivechatInquiry.findOneByRoomId(roomId, {}); + if (!inquiry) { + this.logger.error(`No inquiry found for room ${roomId}`); + throw new Error('error-invalid-inquiry'); + } + + await this.attemptToAssignRoomToServingAgentElseQueueIt({ + room, + inquiry, + servingAgent: servedBy, + clientAction, + }); + + await Promise.all([ + LivechatRooms.unsetOnHoldByRoomId(roomId), + Subscriptions.unsetOnHoldByRoomId(roomId), + Message.saveSystemMessage('omnichannel_on_hold_chat_resumed', roomId, '', resumeBy, { comment }), + ]); + + await callbacks.run('livechat:afterOnHoldChatResumed', room); + + this.logger.debug(`Room ${room._id} resumed successfully`); + } + + private async attemptToAssignRoomToServingAgentElseQueueIt({ + room, + inquiry, + servingAgent, + clientAction, + }: { + room: Pick; + inquiry: ILivechatInquiryRecord; + servingAgent: NonNullable; + clientAction: boolean; + }) { + try { + const agent = { + agentId: servingAgent._id, + username: servingAgent.username, + }; + + await callbacks.run('livechat.checkAgentBeforeTakeInquiry', { + agent, + inquiry, + options: {}, + }); + + return; + } catch (e) { + this.logger.debug(`Agent ${servingAgent._id} is not available to take the inquiry ${inquiry._id}`, e); + if (clientAction) { + // if the action was triggered by the client, we should throw the error + // so the client can handle it and show the error message to the user + throw e; + } + } + + this.logger.debug(`Attempting to queue inquiry ${inquiry._id}`); + + await this.removeCurrentAgentFromRoom({ room, inquiry }); + + const { _id: inquiryId } = inquiry; + const newInquiry = await LivechatInquiry.findOneById(inquiryId); + + await queueInquiry(newInquiry); + + this.logger.debug('Room queued successfully'); + } + + private async removeCurrentAgentFromRoom({ + room, + inquiry, + }: { + room: Pick; + inquiry: ILivechatInquiryRecord; + }): Promise { + this.logger.debug(`Attempting to remove current agent from room ${room._id}`); + + const { _id: roomId } = room; + + const { _id: inquiryId } = inquiry; + + await Promise.all([ + LivechatRooms.removeAgentByRoomId(roomId), + LivechatInquiry.queueInquiryAndRemoveDefaultAgent(inquiryId), + RoutingManager.removeAllRoomSubscriptions(room), + ]); + + await dispatchAgentDelegated(roomId, null); + + this.logger.debug(`Current agent removed from room ${room._id} successfully`); + } +} diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx b/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx index d5d79388e62d..b89dcee32315 100644 --- a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx +++ b/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx @@ -38,13 +38,13 @@ const EngagementDashboardPage = ({ tab = 'users', onSelectTab }: EngagementDashb