diff --git a/.changeset/bright-carpets-fly.md b/.changeset/bright-carpets-fly.md new file mode 100644 index 000000000000..6a8ac2608569 --- /dev/null +++ b/.changeset/bright-carpets-fly.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +new: ring mobile users on direct conference calls diff --git a/.changeset/bright-snakes-vanish.md b/.changeset/bright-snakes-vanish.md new file mode 100644 index 000000000000..f198bfe93ae9 --- /dev/null +++ b/.changeset/bright-snakes-vanish.md @@ -0,0 +1,9 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed an issue causing `queue time` to be calculated from current time when a room was closed without being served. +Now: +- For served rooms: queue time = servedBy time - queuedAt +- For not served, but open rooms = now - queuedAt +- For not served and closed rooms = closedAt - queuedAt diff --git a/.changeset/chilled-phones-give.md b/.changeset/chilled-phones-give.md new file mode 100644 index 000000000000..cb0887db0883 --- /dev/null +++ b/.changeset/chilled-phones-give.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-typings": patch +"@rocket.chat/rest-typings": patch +--- + +Fixed `overrideDestinationChannelEnabled` treated as a required param in `integrations.create` and `integration.update` endpoints diff --git a/.changeset/cool-students-tan.md b/.changeset/cool-students-tan.md new file mode 100644 index 000000000000..07760541628a --- /dev/null +++ b/.changeset/cool-students-tan.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +feat(apps): `ActionManagerBusyState` component for apps `ui.interaction` diff --git a/.changeset/eighty-kids-jog.md b/.changeset/eighty-kids-jog.md new file mode 100644 index 000000000000..6410813d80a6 --- /dev/null +++ b/.changeset/eighty-kids-jog.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/livechat': minor +'@rocket.chat/meteor': minor +--- + +Added new Omnichannel's trigger condition "After starting a chat". diff --git a/.changeset/fair-cats-destroy.md b/.changeset/fair-cats-destroy.md new file mode 100644 index 000000000000..7dfb74955a94 --- /dev/null +++ b/.changeset/fair-cats-destroy.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +--- + +When setting a room as read-only, do not allow previously unmuted users to send messages. diff --git a/.changeset/fast-pumpkins-smoke.md b/.changeset/fast-pumpkins-smoke.md new file mode 100644 index 000000000000..2374776bf3b5 --- /dev/null +++ b/.changeset/fast-pumpkins-smoke.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix: finnish translation diff --git a/.changeset/fifty-cars-divide.md b/.changeset/fifty-cars-divide.md new file mode 100644 index 000000000000..6c09cf6869c8 --- /dev/null +++ b/.changeset/fifty-cars-divide.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed issue with custom OAuth services' settings not being be fully removed diff --git a/.changeset/fuzzy-glasses-divide.md b/.changeset/fuzzy-glasses-divide.md new file mode 100644 index 000000000000..cf77bbde5507 --- /dev/null +++ b/.changeset/fuzzy-glasses-divide.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Added a new Roles bridge to RC Apps-Engine for reading and retrieving role details. diff --git a/.changeset/good-elephants-live.md b/.changeset/good-elephants-live.md new file mode 100644 index 000000000000..8cb3e9d87fc4 --- /dev/null +++ b/.changeset/good-elephants-live.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed message fetching method in LivechatBridge for Apps diff --git a/.changeset/grumpy-candles-rule.md b/.changeset/grumpy-candles-rule.md new file mode 100644 index 000000000000..28673ce91a73 --- /dev/null +++ b/.changeset/grumpy-candles-rule.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix: rejected conference calls continue to ring diff --git a/.changeset/heavy-baboons-laugh.md b/.changeset/heavy-baboons-laugh.md new file mode 100644 index 000000000000..5c32965dcf62 --- /dev/null +++ b/.changeset/heavy-baboons-laugh.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +User information crashing for some locales diff --git a/.changeset/hip-hounds-ring.md b/.changeset/hip-hounds-ring.md new file mode 100644 index 000000000000..be1ebb295afd --- /dev/null +++ b/.changeset/hip-hounds-ring.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Added ability to disable private app installation via envvar (DISABLED_PRIVATE_APP_INSTALLATION) diff --git a/.changeset/many-icons-provide.md b/.changeset/many-icons-provide.md new file mode 100644 index 000000000000..bf82407980ad --- /dev/null +++ b/.changeset/many-icons-provide.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Don't allow to report self messages diff --git a/.changeset/moody-pans-act.md b/.changeset/moody-pans-act.md new file mode 100644 index 000000000000..6c307604eaa9 --- /dev/null +++ b/.changeset/moody-pans-act.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fix seat counter including bots users diff --git a/.changeset/pretty-bees-give.md b/.changeset/pretty-bees-give.md new file mode 100644 index 000000000000..8891420308c5 --- /dev/null +++ b/.changeset/pretty-bees-give.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/rest-typings": minor +--- + +Add option to select what URL previews should be generated for each message. diff --git a/.changeset/real-pets-visit.md b/.changeset/real-pets-visit.md new file mode 100644 index 000000000000..d6531285597c --- /dev/null +++ b/.changeset/real-pets-visit.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-typings": patch +--- + +Fixed `default` field not being returned from the `setDefault` endpoints when setting to false diff --git a/.changeset/rotten-turtles-agree.md b/.changeset/rotten-turtles-agree.md new file mode 100644 index 000000000000..f915aa38f758 --- /dev/null +++ b/.changeset/rotten-turtles-agree.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: stop blinking "Room not found" before dm creation diff --git a/.changeset/serious-geckos-drive.md b/.changeset/serious-geckos-drive.md new file mode 100644 index 000000000000..454337399772 --- /dev/null +++ b/.changeset/serious-geckos-drive.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/ui-client': minor +'@rocket.chat/meteor': minor +--- + +Added Reports Metrics Dashboard to Omnichannel diff --git a/.changeset/shiny-garlics-carry.md b/.changeset/shiny-garlics-carry.md new file mode 100644 index 000000000000..117063d93f6f --- /dev/null +++ b/.changeset/shiny-garlics-carry.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fix CORS headers not being set for assets diff --git a/.changeset/short-cobras-tell.md b/.changeset/short-cobras-tell.md new file mode 100644 index 000000000000..1c28ce7bad11 --- /dev/null +++ b/.changeset/short-cobras-tell.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Reorganized the message menu diff --git a/.changeset/sour-cows-refuse.md b/.changeset/sour-cows-refuse.md new file mode 100644 index 000000000000..d907c063f568 --- /dev/null +++ b/.changeset/sour-cows-refuse.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed inviter not informed when inviting member to room via `/invite` slashcommand diff --git a/.changeset/stale-roses-knock.md b/.changeset/stale-roses-knock.md new file mode 100644 index 000000000000..25e93fa8c346 --- /dev/null +++ b/.changeset/stale-roses-knock.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: User timezone not being respected on Current Chat's filter diff --git a/.changeset/tricky-years-swim.md b/.changeset/tricky-years-swim.md new file mode 100644 index 000000000000..2ab1254525b2 --- /dev/null +++ b/.changeset/tricky-years-swim.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Deprecate `livechat:getOverviewData` and `livechat:getAgentOverviewData` methods and create API endpoints `livechat/analytics/overview` and `livechat/analytics/agent-overview` to fetch analytics data diff --git a/.changeset/unlucky-turtles-search.md b/.changeset/unlucky-turtles-search.md new file mode 100644 index 000000000000..fffa51020e30 --- /dev/null +++ b/.changeset/unlucky-turtles-search.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed Accounts profile form name change was not working diff --git a/.changeset/wet-frogs-kiss.md b/.changeset/wet-frogs-kiss.md new file mode 100644 index 000000000000..24395a78f85d --- /dev/null +++ b/.changeset/wet-frogs-kiss.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: Missing padding on Omnichannel contacts Contextualbar loading state diff --git a/.changeset/wild-spiders-smell.md b/.changeset/wild-spiders-smell.md new file mode 100644 index 000000000000..9694d6259d3a --- /dev/null +++ b/.changeset/wild-spiders-smell.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed an issue where a mailer error was being sent to customers using offline message's form on Omnichannel instead of the translated one diff --git a/.changeset/wise-onions-trade.md b/.changeset/wise-onions-trade.md new file mode 100644 index 000000000000..cb5c731fb6fb --- /dev/null +++ b/.changeset/wise-onions-trade.md @@ -0,0 +1,11 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +"@rocket.chat/livechat": patch +"@rocket.chat/mock-providers": patch +"@rocket.chat/ui-client": patch +"@rocket.chat/ui-contexts": patch +"@rocket.chat/web-ui-registration": patch +--- + +Fixed the login page language switcher, now the component has a new look, is reactive and the language selection becomes concrete upon login in. Also changed the default language of the login page to be the browser language. diff --git a/.changeset/wise-ways-fetch.md b/.changeset/wise-ways-fetch.md new file mode 100644 index 000000000000..a81063813c35 --- /dev/null +++ b/.changeset/wise-ways-fetch.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fixed the unread messages mark not showing diff --git a/.changeset/yellow-schools-tell.md b/.changeset/yellow-schools-tell.md new file mode 100644 index 000000000000..c1040fa0856a --- /dev/null +++ b/.changeset/yellow-schools-tell.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/eslint-config': minor +--- + +Unpublished changes in ESLint config diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 8ec446b270e0..e22b293d444b 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -142,7 +142,7 @@ jobs: path: | ~/.cache/ms-playwright # This is the version of Playwright that we are using, if you are willing to upgrade, you should update this. - key: playwright-1.23.1. + key: playwright-1.37.1 - name: Install Playwright if: inputs.type == 'ui' && steps.cache-playwright.outputs.cache-hit != 'true' @@ -203,7 +203,8 @@ jobs: IS_EE: ${{ inputs.release == 'ee' && 'true' || '' }} REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }} REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} - REPORTER_ROCKETCHAT_REPORT: ${{ github.ref == 'refs/heads/develop' && 'true' || '' }} + REPORTER_ROCKETCHAT_REPORT: ${{ github.event.pull_request.draft != 'true' && 'true' || '' }} + REPORTER_ROCKETCHAT_RUN: ${{ github.run_number }} REPORTER_ROCKETCHAT_BRANCH: ${{ github.ref }} REPORTER_ROCKETCHAT_DRAFT: ${{ github.event.pull_request.draft }} QASE_API_TOKEN: ${{ secrets.QASE_API_TOKEN }} diff --git a/.yarn/patches/@storybook-react-docgen-typescript-plugin-npm-1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0-b31cc57c40.patch b/.yarn/patches/@storybook-react-docgen-typescript-plugin-npm-1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0-b31cc57c40.patch new file mode 100644 index 000000000000..ca069ee350c0 --- /dev/null +++ b/.yarn/patches/@storybook-react-docgen-typescript-plugin-npm-1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0-b31cc57c40.patch @@ -0,0 +1,130 @@ +diff --git a/dist/generateDocgenCodeBlock.js b/dist/generateDocgenCodeBlock.js +index 0993ac13e4b2aae6d24cf408d6a585b4ddeb7337..1405896291288eb1322d6c42144afd3b4fbd1abf 100644 +--- a/dist/generateDocgenCodeBlock.js ++++ b/dist/generateDocgenCodeBlock.js +@@ -34,7 +34,7 @@ function insertTsIgnoreBeforeStatement(statement) { + * ``` + */ + function setDisplayName(d) { +- return insertTsIgnoreBeforeStatement(typescript_1.default.createExpressionStatement(typescript_1.default.createBinary(typescript_1.default.createPropertyAccess(typescript_1.default.createIdentifier(d.displayName), typescript_1.default.createIdentifier("displayName")), typescript_1.default.SyntaxKind.EqualsToken, typescript_1.default.createLiteral(d.displayName)))); ++ return insertTsIgnoreBeforeStatement(typescript_1.default.factory.createExpressionStatement(typescript_1.default.factory.createBinaryExpression(typescript_1.default.factory.createPropertyAccessExpression(typescript_1.default.factory.createIdentifier(d.displayName), typescript_1.default.factory.createIdentifier("displayName")), typescript_1.default.SyntaxKind.EqualsToken, typescript_1.default.factory.createStringLiteral(d.displayName)))); + } + /** + * Set a component prop description. +@@ -65,7 +65,7 @@ function createPropDefinition(propName, prop, options) { + * + * @param defaultValue Default prop value or null if not set. + */ +- const setDefaultValue = (defaultValue) => typescript_1.default.createPropertyAssignment(typescript_1.default.createLiteral("defaultValue"), ++ const setDefaultValue = (defaultValue) => typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createStringLiteral("defaultValue"), + // Use a more extensive check on defaultValue. Sometimes the parser + // returns an empty object. + defaultValue !== null && +@@ -75,12 +75,19 @@ function createPropDefinition(propName, prop, options) { + (typeof defaultValue.value === "string" || + typeof defaultValue.value === "number" || + typeof defaultValue.value === "boolean") +- ? typescript_1.default.createObjectLiteral([ +- typescript_1.default.createPropertyAssignment(typescript_1.default.createIdentifier("value"), typescript_1.default.createLiteral(defaultValue.value)), ++ ? typescript_1.default.factory.createObjectLiteralExpression([ ++ typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createIdentifier("value"), typeof defaultValue.value === "string" ++ ? typescript_1.default.factory.createStringLiteral(defaultValue.value) ++ : // eslint-disable-next-line no-nested-ternary ++ typeof defaultValue.value === "number" ++ ? typescript_1.default.factory.createNumericLiteral(defaultValue.value) ++ : defaultValue.value ++ ? typescript_1.default.factory.createTrue() ++ : typescript_1.default.factory.createFalse()), + ]) +- : typescript_1.default.createNull()); ++ : typescript_1.default.factory.createNull()); + /** Set a property with a string value */ +- const setStringLiteralField = (fieldName, fieldValue) => typescript_1.default.createPropertyAssignment(typescript_1.default.createLiteral(fieldName), typescript_1.default.createLiteral(fieldValue)); ++ const setStringLiteralField = (fieldName, fieldValue) => typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createStringLiteral(fieldName), typescript_1.default.factory.createStringLiteral(fieldValue)); + /** + * ``` + * SimpleComponent.__docgenInfo.props.someProp.description = "Prop description."; +@@ -101,7 +108,7 @@ function createPropDefinition(propName, prop, options) { + * ``` + * @param required Whether prop is required or not. + */ +- const setRequired = (required) => typescript_1.default.createPropertyAssignment(typescript_1.default.createLiteral("required"), required ? typescript_1.default.createTrue() : typescript_1.default.createFalse()); ++ const setRequired = (required) => typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createStringLiteral("required"), required ? typescript_1.default.factory.createTrue() : typescript_1.default.factory.createFalse()); + /** + * ``` + * SimpleComponent.__docgenInfo.props.someProp.type = { +@@ -113,7 +120,7 @@ function createPropDefinition(propName, prop, options) { + */ + const setValue = (typeValue) => Array.isArray(typeValue) && + typeValue.every((value) => typeof value.value === "string") +- ? typescript_1.default.createPropertyAssignment(typescript_1.default.createLiteral("value"), typescript_1.default.createArrayLiteral(typeValue.map((value) => typescript_1.default.createObjectLiteral([ ++ ? typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createStringLiteral("value"), typescript_1.default.factory.createArrayLiteralExpression(typeValue.map((value) => typescript_1.default.factory.createObjectLiteralExpression([ + setStringLiteralField("value", value.value), + ])))) + : undefined; +@@ -130,9 +137,9 @@ function createPropDefinition(propName, prop, options) { + if (valueField) { + objectFields.push(valueField); + } +- return typescript_1.default.createPropertyAssignment(typescript_1.default.createLiteral(options.typePropName), typescript_1.default.createObjectLiteral(objectFields)); ++ return typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createStringLiteral(options.typePropName), typescript_1.default.factory.createObjectLiteralExpression(objectFields)); + }; +- return typescript_1.default.createPropertyAssignment(typescript_1.default.createLiteral(propName), typescript_1.default.createObjectLiteral([ ++ return typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createStringLiteral(propName), typescript_1.default.factory.createObjectLiteralExpression([ + setDefaultValue(prop.defaultValue), + setDescription(prop.description), + setName(prop.name), +@@ -158,10 +165,10 @@ function createPropDefinition(propName, prop, options) { + * @param relativeFilename Relative file path of the component source file. + */ + function insertDocgenIntoGlobalCollection(d, docgenCollectionName, relativeFilename) { +- return insertTsIgnoreBeforeStatement(typescript_1.default.createIf(typescript_1.default.createBinary(typescript_1.default.createTypeOf(typescript_1.default.createIdentifier(docgenCollectionName)), typescript_1.default.SyntaxKind.ExclamationEqualsEqualsToken, typescript_1.default.createLiteral("undefined")), insertTsIgnoreBeforeStatement(typescript_1.default.createStatement(typescript_1.default.createBinary(typescript_1.default.createElementAccess(typescript_1.default.createIdentifier(docgenCollectionName), typescript_1.default.createLiteral(`${relativeFilename}#${d.displayName}`)), typescript_1.default.SyntaxKind.EqualsToken, typescript_1.default.createObjectLiteral([ +- typescript_1.default.createPropertyAssignment(typescript_1.default.createIdentifier("docgenInfo"), typescript_1.default.createPropertyAccess(typescript_1.default.createIdentifier(d.displayName), typescript_1.default.createIdentifier("__docgenInfo"))), +- typescript_1.default.createPropertyAssignment(typescript_1.default.createIdentifier("name"), typescript_1.default.createLiteral(d.displayName)), +- typescript_1.default.createPropertyAssignment(typescript_1.default.createIdentifier("path"), typescript_1.default.createLiteral(`${relativeFilename}#${d.displayName}`)), ++ return insertTsIgnoreBeforeStatement(typescript_1.default.factory.createIfStatement(typescript_1.default.factory.createBinaryExpression(typescript_1.default.factory.createTypeOfExpression(typescript_1.default.factory.createIdentifier(docgenCollectionName)), typescript_1.default.SyntaxKind.ExclamationEqualsEqualsToken, typescript_1.default.factory.createStringLiteral("undefined")), insertTsIgnoreBeforeStatement(typescript_1.default.factory.createExpressionStatement(typescript_1.default.factory.createBinaryExpression(typescript_1.default.factory.createElementAccessExpression(typescript_1.default.factory.createIdentifier(docgenCollectionName), typescript_1.default.factory.createStringLiteral(`${relativeFilename}#${d.displayName}`)), typescript_1.default.SyntaxKind.EqualsToken, typescript_1.default.factory.createObjectLiteralExpression([ ++ typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createIdentifier("docgenInfo"), typescript_1.default.factory.createPropertyAccessExpression(typescript_1.default.factory.createIdentifier(d.displayName), typescript_1.default.factory.createIdentifier("__docgenInfo"))), ++ typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createIdentifier("name"), typescript_1.default.factory.createStringLiteral(d.displayName)), ++ typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createIdentifier("path"), typescript_1.default.factory.createStringLiteral(`${relativeFilename}#${d.displayName}`)), + ])))))); + } + /** +@@ -180,15 +187,15 @@ function insertDocgenIntoGlobalCollection(d, docgenCollectionName, relativeFilen + * @param options Generator options. + */ + function setComponentDocGen(d, options) { +- return insertTsIgnoreBeforeStatement(typescript_1.default.createStatement(typescript_1.default.createBinary( ++ return insertTsIgnoreBeforeStatement(typescript_1.default.factory.createExpressionStatement(typescript_1.default.factory.createBinaryExpression( + // SimpleComponent.__docgenInfo +- typescript_1.default.createPropertyAccess(typescript_1.default.createIdentifier(d.displayName), typescript_1.default.createIdentifier("__docgenInfo")), typescript_1.default.SyntaxKind.EqualsToken, typescript_1.default.createObjectLiteral([ ++ typescript_1.default.factory.createPropertyAccessExpression(typescript_1.default.factory.createIdentifier(d.displayName), typescript_1.default.factory.createIdentifier("__docgenInfo")), typescript_1.default.SyntaxKind.EqualsToken, typescript_1.default.factory.createObjectLiteralExpression([ + // SimpleComponent.__docgenInfo.description +- typescript_1.default.createPropertyAssignment(typescript_1.default.createLiteral("description"), typescript_1.default.createLiteral(d.description)), ++ typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createStringLiteral("description"), typescript_1.default.factory.createStringLiteral(d.description)), + // SimpleComponent.__docgenInfo.displayName +- typescript_1.default.createPropertyAssignment(typescript_1.default.createLiteral("displayName"), typescript_1.default.createLiteral(d.displayName)), ++ typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createStringLiteral("displayName"), typescript_1.default.factory.createStringLiteral(d.displayName)), + // SimpleComponent.__docgenInfo.props +- typescript_1.default.createPropertyAssignment(typescript_1.default.createLiteral("props"), typescript_1.default.createObjectLiteral(Object.entries(d.props).map(([propName, prop]) => createPropDefinition(propName, prop, options)))), ++ typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createStringLiteral("props"), typescript_1.default.factory.createObjectLiteralExpression(Object.entries(d.props).map(([propName, prop]) => createPropDefinition(propName, prop, options)))), + ])))); + } + function generateDocgenCodeBlock(options) { +@@ -196,7 +203,7 @@ function generateDocgenCodeBlock(options) { + const relativeFilename = path_1.default + .relative("./", path_1.default.resolve("./", options.filename)) + .replace(/\\/g, "/"); +- const wrapInTryStatement = (statements) => typescript_1.default.createTry(typescript_1.default.createBlock(statements, true), typescript_1.default.createCatchClause(typescript_1.default.createVariableDeclaration(typescript_1.default.createIdentifier("__react_docgen_typescript_loader_error")), typescript_1.default.createBlock([])), undefined); ++ const wrapInTryStatement = (statements) => typescript_1.default.factory.createTryStatement(typescript_1.default.factory.createBlock(statements, true), typescript_1.default.factory.createCatchClause(typescript_1.default.factory.createVariableDeclaration(typescript_1.default.factory.createIdentifier("__react_docgen_typescript_loader_error")), typescript_1.default.factory.createBlock([])), undefined); + const codeBlocks = options.componentDocs.map((d) => wrapInTryStatement([ + options.setDisplayName ? setDisplayName(d) : null, + setComponentDocGen(d, options), +@@ -208,7 +215,7 @@ function generateDocgenCodeBlock(options) { + const printer = typescript_1.default.createPrinter({ newLine: typescript_1.default.NewLineKind.LineFeed }); + const printNode = (sourceNode) => printer.printNode(typescript_1.default.EmitHint.Unspecified, sourceNode, sourceFile); + // Concat original source code with code from generated code blocks. +- const result = codeBlocks.reduce((acc, node) => `${acc}\n${printNode(node)}`, ++ const result = codeBlocks.reduce((acc, node) => `${acc}\n${printNode(node)}`, + // Use original source text rather than using printNode on the parsed form + // to prevent issue where literals are stripped within components. + // Ref: https://github.com/strothj/react-docgen-typescript-loader/issues/7 diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index f8a1a24f8430..182d30ddabb9 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,96 @@ # @rocket.chat/meteor +## 6.3.3 + +### Patch Changes + +- bcf147f515: Bump @rocket.chat/meteor version. +- Bump @rocket.chat/meteor version. +- c2fe38cb34: Added ability to disable private app installation via envvar (DISABLE_PRIVATE_APP_INSTALLATION) +- ded9666f27: Fix CORS headers not being set for assets +- f25081bc8a: Removed an unused authentication flow + - @rocket.chat/core-typings@6.3.3 + - @rocket.chat/rest-typings@6.3.3 + - @rocket.chat/api-client@0.1.3 + - @rocket.chat/omnichannel-services@0.0.9 + - @rocket.chat/pdf-worker@0.0.9 + - @rocket.chat/presence@0.0.9 + - @rocket.chat/core-services@0.1.3 + - @rocket.chat/cron@0.0.5 + - @rocket.chat/gazzodown@1.0.3 + - @rocket.chat/model-typings@0.0.9 + - @rocket.chat/ui-contexts@1.0.3 + - @rocket.chat/fuselage-ui-kit@1.0.3 + - @rocket.chat/models@0.0.9 + - @rocket.chat/ui-theming@0.0.1 + - @rocket.chat/ui-client@1.0.3 + - @rocket.chat/ui-video-conf@1.0.3 + - @rocket.chat/web-ui-registration@1.0.3 + - @rocket.chat/instance-status@0.0.9 + +## 6.3.2 + +### Patch Changes + +- 778b155ab4: Bump @rocket.chat/meteor version. +- Bump @rocket.chat/meteor version. +- 5660169ec8: fixed layout changing from embedded view when navigating +- f7b93f2a6a: Fixed an issue where timeout for http requests in Apps-Engine bridges was too short +- 653d97ce22: fix: mobile app unable to detect successful SAML login +- 8a0e36f7b1: fixed the video recorder window not closing after permission is denied. + - @rocket.chat/core-typings@6.3.2 + - @rocket.chat/rest-typings@6.3.2 + - @rocket.chat/api-client@0.1.2 + - @rocket.chat/omnichannel-services@0.0.8 + - @rocket.chat/pdf-worker@0.0.8 + - @rocket.chat/presence@0.0.8 + - @rocket.chat/core-services@0.1.2 + - @rocket.chat/cron@0.0.4 + - @rocket.chat/gazzodown@1.0.2 + - @rocket.chat/model-typings@0.0.8 + - @rocket.chat/ui-contexts@1.0.2 + - @rocket.chat/fuselage-ui-kit@1.0.2 + - @rocket.chat/models@0.0.8 + - @rocket.chat/ui-theming@0.0.1 + - @rocket.chat/ui-client@1.0.2 + - @rocket.chat/ui-video-conf@1.0.2 + - @rocket.chat/web-ui-registration@1.0.2 + - @rocket.chat/instance-status@0.0.8 + +## 6.3.1 + +### Patch Changes + +- a874d5b305: Translation files are requested multiple times +- cf9f16b17c: fix: Performance issue on `Messages.countByType` aggregation caused by unindexed property on messages collection +- be2b5c66cf: Bump @rocket.chat/meteor version. +- Bump @rocket.chat/meteor version. +- ce2f2eaad3: Added ability to freeze or completely disable integration scripts through envvars +- f29c3268ee: fixed an issue where 2fa was not working after an OAuth redirect +- 09a24e59ef: Fix performance issue on Engagement Dashboard aggregation +- f29c3268ee: fixed an issue where oauth login was not working with some providers +- 25d5e3cd9e: Fixed unable to create admin user using ADMIN\_\* environment variables +- 34f08e7c95: Fixed failing user data exports +- f29c3268ee: fixed an issue on oauth login that caused missing emails to be detected as changed data + - @rocket.chat/core-typings@6.3.1 + - @rocket.chat/rest-typings@6.3.1 + - @rocket.chat/api-client@0.1.1 + - @rocket.chat/omnichannel-services@0.0.7 + - @rocket.chat/pdf-worker@0.0.7 + - @rocket.chat/presence@0.0.7 + - @rocket.chat/core-services@0.1.1 + - @rocket.chat/cron@0.0.3 + - @rocket.chat/gazzodown@1.0.1 + - @rocket.chat/model-typings@0.0.7 + - @rocket.chat/ui-contexts@1.0.1 + - @rocket.chat/fuselage-ui-kit@1.0.1 + - @rocket.chat/models@0.0.7 + - @rocket.chat/ui-theming@0.0.1 + - @rocket.chat/ui-client@1.0.1 + - @rocket.chat/ui-video-conf@1.0.1 + - @rocket.chat/web-ui-registration@1.0.1 + - @rocket.chat/instance-status@0.0.7 + ## 6.3.0 ### Minor Changes diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index e12e6f0cdd46..75fc98e69c4d 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -14,6 +14,7 @@ import { hasPermissionAsync } from '../../../authorization/server/functions/hasP import { deleteMessageValidatingPermission } from '../../../lib/server/functions/deleteMessage'; import { processWebhookMessage } from '../../../lib/server/functions/processWebhookMessage'; import { executeSendMessage } from '../../../lib/server/methods/sendMessage'; +import { executeUpdateMessage } from '../../../lib/server/methods/updateMessage'; import { executeSetReaction } from '../../../reactions/server/setReaction'; import { settings } from '../../../settings/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; @@ -215,7 +216,7 @@ API.v1.addRoute( throw new Meteor.Error('error-invalid-params', 'The "message" parameter must be provided.'); } - const sent = await executeSendMessage(this.userId, this.bodyParams.message as Pick); + const sent = await executeSendMessage(this.userId, this.bodyParams.message as Pick, this.bodyParams.previewUrls); const [message] = await normalizeMessagesForUser([sent], this.userId); return API.v1.success({ @@ -310,6 +311,7 @@ API.v1.addRoute( roomId: String, msgId: String, text: String, // Using text to be consistant with chat.postMessage + previewUrls: Match.Maybe([String]), }), ); @@ -325,7 +327,7 @@ API.v1.addRoute( } // Permission checks are already done in the updateMessage method, so no need to duplicate them - await Meteor.callAsync('updateMessage', { _id: msg._id, msg: this.bodyParams.text, rid: msg.rid }); + await executeUpdateMessage(this.userId, { _id: msg._id, msg: this.bodyParams.text, rid: msg.rid }, this.bodyParams.previewUrls); const updatedMessage = await Messages.findOneById(msg._id); const [message] = await normalizeMessagesForUser(updatedMessage ? [updatedMessage] : [], this.userId); diff --git a/apps/meteor/app/apps/server/bridges/bridges.js b/apps/meteor/app/apps/server/bridges/bridges.js index 6c67c116ba01..6d52a1d8e56d 100644 --- a/apps/meteor/app/apps/server/bridges/bridges.js +++ b/apps/meteor/app/apps/server/bridges/bridges.js @@ -15,6 +15,7 @@ import { AppMessageBridge } from './messages'; import { AppModerationBridge } from './moderation'; import { AppOAuthAppsBridge } from './oauthApps'; import { AppPersistenceBridge } from './persistence'; +import { AppRoleBridge } from './roles'; import { AppRoomBridge } from './rooms'; import { AppSchedulerBridge } from './scheduler'; import { AppSettingBridge } from './settings'; @@ -51,6 +52,7 @@ export class RealAppBridges extends AppBridges { this._internalFedBridge = new AppInternalFederationBridge(); this._moderationBridge = new AppModerationBridge(orch); this._threadBridge = new AppThreadBridge(orch); + this._roleBridge = new AppRoleBridge(orch); } getCommandBridge() { @@ -144,4 +146,8 @@ export class RealAppBridges extends AppBridges { getModerationBridge() { return this._moderationBridge; } + + getRoleBridge() { + return this._roleBridge; + } } diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 6bead3d80a62..71f7387e1aa5 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -6,7 +6,7 @@ import type { ILivechatTransferData, IDepartment, } from '@rocket.chat/apps-engine/definition/livechat'; -import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { IMessage as IAppsEngineMesage } from '@rocket.chat/apps-engine/definition/messages'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; import { LivechatBridge } from '@rocket.chat/apps-engine/server/bridges/LivechatBridge'; import type { SelectedAgent } from '@rocket.chat/core-typings'; @@ -280,7 +280,7 @@ export class AppLivechatBridge extends LivechatBridge { return Promise.all((await LivechatDepartment.findEnabledWithAgents().toArray()).map(boundConverter)); } - protected async _fetchLivechatRoomMessages(appId: string, roomId: string): Promise> { + protected async _fetchLivechatRoomMessages(appId: string, roomId: string): Promise> { this.orch.debugLog(`The App ${appId} is getting the transcript for livechat room ${roomId}.`); const messageConverter = this.orch.getConverters()?.get('messages'); @@ -288,9 +288,9 @@ export class AppLivechatBridge extends LivechatBridge { throw new Error('Could not get the message converter to process livechat room messages'); } - const boundMessageConverter = messageConverter.convertMessage.bind(messageConverter); + const livechatMessages = await Livechat.getRoomMessages({ rid: roomId }); - return (await Livechat.getRoomMessages({ rid: roomId })).map(boundMessageConverter); + return Promise.all(livechatMessages.map((message) => messageConverter.convertMessage(message) as Promise)); } protected async setCustomFields( diff --git a/apps/meteor/app/apps/server/bridges/roles.ts b/apps/meteor/app/apps/server/bridges/roles.ts new file mode 100644 index 000000000000..f973b7f49ed2 --- /dev/null +++ b/apps/meteor/app/apps/server/bridges/roles.ts @@ -0,0 +1,33 @@ +import type { IRole } from '@rocket.chat/apps-engine/definition/roles'; +import { RoleBridge } from '@rocket.chat/apps-engine/server/bridges'; +import { Roles } from '@rocket.chat/models'; + +import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; + +export class AppRoleBridge extends RoleBridge { + constructor(private readonly orch: AppServerOrchestrator) { + super(); + } + + protected async getOneByIdOrName(idOrName: IRole['id'] | IRole['name'], appId: string): Promise { + this.orch.debugLog(`The App ${appId} is getting the roleByIdOrName: "${idOrName}"`); + + const role = await Roles.findOneByIdOrName(idOrName); + return this.orch.getConverters()?.get('roles').convertRole(role); + } + + protected async getCustomRoles(appId: string): Promise> { + this.orch.debugLog(`The App ${appId} is getting the custom roles`); + + const cursor = Roles.findCustomRoles(); + + const roles: IRole[] = []; + + for await (const role of cursor) { + const convRole = await this.orch.getConverters()?.get('roles').convertRole(role); + roles.push(convRole); + } + + return roles; + } +} diff --git a/apps/meteor/app/apps/server/converters/index.ts b/apps/meteor/app/apps/server/converters/index.ts index 39a6a260f69f..96716af03ca7 100644 --- a/apps/meteor/app/apps/server/converters/index.ts +++ b/apps/meteor/app/apps/server/converters/index.ts @@ -1,5 +1,6 @@ import { AppDepartmentsConverter } from './departments'; import { AppMessagesConverter } from './messages'; +import { AppRolesConverter } from './roles'; import { AppRoomsConverter } from './rooms'; import { AppSettingsConverter } from './settings'; import { AppUploadsConverter } from './uploads'; @@ -16,4 +17,5 @@ export { AppDepartmentsConverter, AppUploadsConverter, AppVisitorsConverter, + AppRolesConverter, }; diff --git a/apps/meteor/app/apps/server/converters/roles.ts b/apps/meteor/app/apps/server/converters/roles.ts new file mode 100644 index 000000000000..4ac1f3956420 --- /dev/null +++ b/apps/meteor/app/apps/server/converters/roles.ts @@ -0,0 +1,29 @@ +import type { IRole as AppsEngineRole } from '@rocket.chat/apps-engine/definition/roles'; +import type { IRole } from '@rocket.chat/core-typings'; +import { Roles } from '@rocket.chat/models'; + +import { transformMappedData } from '../../../../ee/lib/misc/transformMappedData'; + +export class AppRolesConverter { + async convertById(roleId: string): Promise { + const role = await Roles.findOneById(roleId); + + if (!role) { + return; + } + return this.convertRole(role); + } + + async convertRole(role: IRole): Promise { + const map = { + id: '_id', + name: 'name', + description: 'description', + mandatory2fa: 'mandatory2fa', + protected: 'protected', + scope: 'scope', + }; + + return (await transformMappedData(role, map)) as unknown as AppsEngineRole; + } +} diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.js index 111298213619..ae38feff5eff 100644 --- a/apps/meteor/app/apps/server/converters/rooms.js +++ b/apps/meteor/app/apps/server/converters/rooms.js @@ -53,7 +53,7 @@ export class AppRoomsConverter { let departmentId; if (room.department) { - const department = await LivechatDepartment.findOneById(room.department.id); + const department = await LivechatDepartment.findOneById(room.department.id, { projection: { _id: 1 } }); departmentId = department._id; } diff --git a/apps/meteor/app/authorization/server/functions/upsertPermissions.ts b/apps/meteor/app/authorization/server/functions/upsertPermissions.ts index 41b1ffbc2c52..bb908916c885 100644 --- a/apps/meteor/app/authorization/server/functions/upsertPermissions.ts +++ b/apps/meteor/app/authorization/server/functions/upsertPermissions.ts @@ -35,9 +35,7 @@ export const upsertPermissions = async (): Promise => { [key: string]: IPermission; } = {}; - const selector = { level: 'settings' as const, ...(settingId && { settingId }) }; - - await Permissions.find(selector).forEach((permission: IPermission) => { + await Permissions.findByLevel('settings', settingId).forEach((permission: IPermission) => { previousSettingPermissions[permission._id] = permission; }); return previousSettingPermissions; diff --git a/apps/meteor/app/autotranslate/client/lib/actionButton.ts b/apps/meteor/app/autotranslate/client/lib/actionButton.ts index 29c4cfb4778f..24cea2d8d28a 100644 --- a/apps/meteor/app/autotranslate/client/lib/actionButton.ts +++ b/apps/meteor/app/autotranslate/client/lib/actionButton.ts @@ -24,6 +24,7 @@ Meteor.startup(() => { icon: 'language', label: 'Translate', context: ['message', 'message-mobile', 'threads'], + type: 'interaction', action(_, props) { const { message = messageArgs(this).msg } = props; const language = AutoTranslate.getLanguage(message.rid); @@ -58,6 +59,7 @@ Meteor.startup(() => { icon: 'language', label: 'View_original', context: ['message', 'message-mobile', 'threads'], + type: 'interaction', action(_, props) { const { message = messageArgs(this).msg } = props; const language = AutoTranslate.getLanguage(message.rid); diff --git a/apps/meteor/app/cors/server/cors.ts b/apps/meteor/app/cors/server/cors.ts index 69a0b557ee64..03a42e45a17b 100644 --- a/apps/meteor/app/cors/server/cors.ts +++ b/apps/meteor/app/cors/server/cors.ts @@ -79,7 +79,7 @@ WebApp.rawConnectHandlers.use((_req: http.IncomingMessage, res: http.ServerRespo const _staticFilesMiddleware = WebAppInternals.staticFilesMiddleware; // @ts-expect-error - accessing internal property of webapp -WebAppInternals._staticFilesMiddleware = function ( +WebAppInternals.staticFilesMiddleware = function ( staticFiles: StaticFiles, req: http.IncomingMessage, res: http.ServerResponse, diff --git a/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts b/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts index 170b2895cc61..5eb2ef38e5b7 100644 --- a/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts +++ b/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts @@ -19,6 +19,7 @@ Meteor.startup(() => { id: 'start-discussion', icon: 'discussion', label: 'Discussion_start', + type: 'communication', context: ['message', 'message-mobile', 'videoconf'], async action(_, props) { const { message = messageArgs(this).msg, room } = props; diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 35b57815047a..42e9a3a99553 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -466,7 +466,7 @@ class E2E extends Emitter { await Promise.all( urls.map(async (url) => { - if (!url.includes(Meteor.absoluteUrl())) { + if (!url.includes(settings.get('Site_Url'))) { return; } diff --git a/apps/meteor/app/e2e/server/methods/setRoomKeyID.ts b/apps/meteor/app/e2e/server/methods/setRoomKeyID.ts index 76942a681805..005df0bb2a7a 100644 --- a/apps/meteor/app/e2e/server/methods/setRoomKeyID.ts +++ b/apps/meteor/app/e2e/server/methods/setRoomKeyID.ts @@ -31,7 +31,7 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.setRoomKeyID' }); } - const room = await Rooms.findOneById(rid, { fields: { e2eKeyId: 1 } }); + const room = await Rooms.findOneById>(rid, { projection: { e2eKeyId: 1 } }); if (!room) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.setRoomKeyID' }); diff --git a/apps/meteor/app/federation/server/endpoints/dispatch.js b/apps/meteor/app/federation/server/endpoints/dispatch.js index cf9e91b44af9..59ffcb0f342f 100644 --- a/apps/meteor/app/federation/server/endpoints/dispatch.js +++ b/apps/meteor/app/federation/server/endpoints/dispatch.js @@ -457,8 +457,8 @@ const eventHandlers = { // Denormalize user const denormalizedUser = normalizers.denormalizeUser(user); - // Mute user - await Rooms.unmuteUsernameByRoomId(roomId, denormalizedUser.username); + // Unmute user + await Rooms.unmuteMutedUsernameByRoomId(roomId, denormalizedUser.username); } return eventResult; diff --git a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts index 307e83f7aef7..bf84957ba8ea 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts @@ -32,7 +32,7 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn alias: Match.Maybe(String), emoji: Match.Maybe(String), scriptEnabled: Boolean, - overrideDestinationChannelEnabled: Boolean, + overrideDestinationChannelEnabled: Match.Maybe(Boolean), script: Match.Maybe(String), avatar: Match.Maybe(String), }), @@ -92,6 +92,7 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn ...integration, type: 'webhook-incoming', channel: channels, + overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled ?? false, token: Random.id(48), userId: user._id, _createdAt: new Date(), diff --git a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts index 5e5fe67f3977..b865c72e0cca 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts @@ -171,7 +171,9 @@ Meteor.methods({ script: integration.script, scriptEnabled: integration.scriptEnabled, }), - overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled, + ...(typeof integration.overrideDestinationChannelEnabled !== 'undefined' && { + overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled, + }), _updatedAt: new Date(), _updatedBy: await Users.findOne({ _id: this.userId }, { projection: { username: 1 } }), }, diff --git a/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts b/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts index c7ef5afab3dc..2a63d024bdd9 100644 --- a/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts +++ b/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts @@ -1,9 +1,10 @@ -import type { IMessage } from '@rocket.chat/core-typings'; +import type { IMessage, AtLeast } from '@rocket.chat/core-typings'; import { getMessageUrlRegex } from '../../../../lib/getMessageUrlRegex'; import { Markdown } from '../../../markdown/server'; +import { settings } from '../../../settings/server'; -export const parseUrlsInMessage = (message: IMessage & { parseUrls?: boolean }): IMessage => { +export const parseUrlsInMessage = (message: AtLeast & { parseUrls?: boolean }, previewUrls?: string[]) => { if (message.parseUrls === false) { return message; } @@ -13,13 +14,15 @@ export const parseUrlsInMessage = (message: IMessage & { parseUrls?: boolean }): const urls = message.html?.match(getMessageUrlRegex()) || []; if (urls) { - message.urls = [...new Set(urls)].map((url) => ({ url, meta: {} })); + message.urls = [...new Set(urls)].map((url) => ({ + url, + meta: {}, + ...(previewUrls && !previewUrls.includes(url) && !url.includes(settings.get('Site_Url')) && { ignoreParse: true }), + })); } message = Markdown.mountTokensBack(message, false); message.msg = message.html || message.msg; delete message.html; delete message.tokens; - - return message; }; diff --git a/apps/meteor/app/lib/server/functions/sendMessage.js b/apps/meteor/app/lib/server/functions/sendMessage.js index 70a630127713..a1399b5b19e9 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.js +++ b/apps/meteor/app/lib/server/functions/sendMessage.js @@ -203,7 +203,16 @@ function cleanupMessageObject(message) { ['customClass'].forEach((field) => delete message[field]); } -export const sendMessage = async function (user, message, room, upsert = false) { +/** + * Validates and sends the message object. + * @param {IUser} user + * @param {AtLeast} message + * @param {IRoom} room + * @param {boolean} [upsert=false] + * @param {string[]} [previewUrls] + * @returns {Promise} + */ +export const sendMessage = async function (user, message, room, upsert = false, previewUrls = undefined) { if (!user || !message || !room._id) { return false; } @@ -236,7 +245,7 @@ export const sendMessage = async function (user, message, room, upsert = false) cleanupMessageObject(message); - parseUrlsInMessage(message); + parseUrlsInMessage(message, previewUrls); message = await callbacks.run('beforeSaveMessage', message, room); if (message) { diff --git a/apps/meteor/app/lib/server/functions/updateMessage.ts b/apps/meteor/app/lib/server/functions/updateMessage.ts index ba122c18b4af..05c17906374e 100644 --- a/apps/meteor/app/lib/server/functions/updateMessage.ts +++ b/apps/meteor/app/lib/server/functions/updateMessage.ts @@ -1,4 +1,4 @@ -import type { IEditedMessage, IMessage, IUser } from '@rocket.chat/core-typings'; +import type { IEditedMessage, IMessage, IUser, AtLeast } from '@rocket.chat/core-typings'; import { Messages, Rooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; @@ -7,7 +7,12 @@ import { callbacks } from '../../../../lib/callbacks'; import { settings } from '../../../settings/server'; import { parseUrlsInMessage } from './parseUrlsInMessage'; -export const updateMessage = async function (message: IMessage, user: IUser, originalMsg?: IMessage): Promise { +export const updateMessage = async function ( + message: AtLeast, + user: IUser, + originalMsg?: IMessage, + previewUrls?: string[], +): Promise { const originalMessage = originalMsg || (await Messages.findOneById(message._id)); // For the Rocket.Chat Apps :) @@ -33,7 +38,7 @@ export const updateMessage = async function (message: IMessage, user: IUser, ori await Messages.cloneAndSaveAsHistoryById(message._id, user as Required>); } - Object.assign>(message, { + Object.assign, Omit>(message, { editedAt: new Date(), editedBy: { _id: user._id, @@ -41,7 +46,7 @@ export const updateMessage = async function (message: IMessage, user: IUser, ori }, }); - parseUrlsInMessage(message); + parseUrlsInMessage(message, previewUrls); message = await callbacks.run('beforeSaveMessage', message); diff --git a/apps/meteor/app/lib/server/methods/removeOAuthService.ts b/apps/meteor/app/lib/server/methods/removeOAuthService.ts index 1be3edeb2caa..6d1bb688979d 100644 --- a/apps/meteor/app/lib/server/methods/removeOAuthService.ts +++ b/apps/meteor/app/lib/server/methods/removeOAuthService.ts @@ -61,6 +61,7 @@ Meteor.methods({ Settings.removeById(`Accounts_OAuth_Custom-${name}-channels_admin`), Settings.removeById(`Accounts_OAuth_Custom-${name}-map_channels`), Settings.removeById(`Accounts_OAuth_Custom-${name}-groups_channel_map`), + Settings.removeById(`Accounts_OAuth_Custom-${name}-merge_users_distinct_services`), ]); }, }); diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index de9024110a41..ebdcdfd43d9b 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -15,7 +15,7 @@ import { settings } from '../../../settings/server'; import { sendMessage } from '../functions/sendMessage'; import { RateLimiter } from '../lib'; -export async function executeSendMessage(uid: IUser['_id'], message: AtLeast) { +export async function executeSendMessage(uid: IUser['_id'], message: AtLeast, previewUrls?: string[]) { if (message.tshow && !message.tmid) { throw new Meteor.Error('invalid-params', 'tshow provided but missing tmid', { method: 'sendMessage', @@ -82,7 +82,7 @@ export async function executeSendMessage(uid: IUser['_id'], message: AtLeast): any; + sendMessage(message: AtLeast, previewUrls?: string[]): any; } } Meteor.methods({ - sendMessage(message) { + sendMessage(message, previewUrls) { check(message, Object); const uid = Meteor.userId(); @@ -118,7 +118,7 @@ Meteor.methods({ } try { - return executeSendMessage(uid, message); + return executeSendMessage(uid, message, previewUrls); } catch (error: any) { if ((error.error || error.message) === 'error-not-allowed') { throw new Meteor.Error(error.error || error.message, error.reason, { diff --git a/apps/meteor/app/lib/server/methods/updateMessage.ts b/apps/meteor/app/lib/server/methods/updateMessage.ts index 2ef7d236b2a9..a34492400130 100644 --- a/apps/meteor/app/lib/server/methods/updateMessage.ts +++ b/apps/meteor/app/lib/server/methods/updateMessage.ts @@ -1,4 +1,4 @@ -import type { IEditedMessage, IMessage } from '@rocket.chat/core-typings'; +import type { IEditedMessage, IMessage, IUser, AtLeast } from '@rocket.chat/core-typings'; import { Messages, Users } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Match, check } from 'meteor/check'; @@ -12,97 +12,102 @@ import { updateMessage } from '../functions/updateMessage'; const allowedEditedFields = ['tshow', 'alias', 'attachments', 'avatar', 'emoji', 'msg']; -declare module '@rocket.chat/ui-contexts' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - updateMessage(message: IEditedMessage): void; +export async function executeUpdateMessage(uid: IUser['_id'], message: AtLeast, previewUrls?: string[]) { + const originalMessage = await Messages.findOneById(message._id); + if (!originalMessage?._id) { + return; } -} -Meteor.methods({ - async updateMessage(message: IEditedMessage) { - check(message, Match.ObjectIncluding({ _id: String })); + Object.entries(message).forEach(([key, value]) => { + if (!allowedEditedFields.includes(key) && value !== originalMessage[key as keyof IMessage]) { + throw new Meteor.Error('error-invalid-update-key', `Cannot update the message ${key}`, { + method: 'updateMessage', + }); + } + }); - const uid = Meteor.userId(); + const msgText = originalMessage?.attachments?.[0]?.description ?? originalMessage.msg; + if (msgText === message.msg && !previewUrls) { + return; + } - if (!uid) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateMessage' }); - } + if (!!message.tmid && originalMessage._id === message.tmid) { + throw new Meteor.Error('error-message-same-as-tmid', 'Cannot set tmid the same as the _id', { + method: 'updateMessage', + }); + } - const originalMessage = await Messages.findOneById(message._id); - if (!originalMessage?._id) { - return; - } + if (!originalMessage.tmid && !!message.tmid) { + throw new Meteor.Error('error-message-change-to-thread', 'Cannot update message to a thread', { method: 'updateMessage' }); + } - Object.entries(message).forEach(([key, value]) => { - if (!allowedEditedFields.includes(key) && value !== originalMessage[key as keyof IMessage]) { - throw new Meteor.Error('error-invalid-update-key', `Cannot update the message ${key}`, { - method: 'updateMessage', - }); - } + const _hasPermission = await hasPermissionAsync(uid, 'edit-message', message.rid); + const editAllowed = settings.get('Message_AllowEditing'); + const editOwn = originalMessage.u && originalMessage.u._id === uid; + + if (!_hasPermission && (!editAllowed || !editOwn)) { + throw new Meteor.Error('error-action-not-allowed', 'Message editing not allowed', { + method: 'updateMessage', + action: 'Message_editing', }); + } - const msgText = originalMessage?.attachments?.[0]?.description ?? originalMessage.msg; - if (msgText === message.msg) { - return; - } + const blockEditInMinutes = settings.get('Message_AllowEditing_BlockEditInMinutes'); + const bypassBlockTimeLimit = await hasPermissionAsync(uid, 'bypass-time-limit-edit-and-delete'); - if (!!message.tmid && originalMessage._id === message.tmid) { - throw new Meteor.Error('error-message-same-as-tmid', 'Cannot set tmid the same as the _id', { + if (!bypassBlockTimeLimit && Match.test(blockEditInMinutes, Number) && blockEditInMinutes !== 0) { + let currentTsDiff = 0; + let msgTs; + + if (originalMessage.ts instanceof Date || Match.test(originalMessage.ts, Number)) { + msgTs = moment(originalMessage.ts); + } + if (msgTs) { + currentTsDiff = moment().diff(msgTs, 'minutes'); + } + if (currentTsDiff >= blockEditInMinutes) { + throw new Meteor.Error('error-message-editing-blocked', 'Message editing is blocked', { method: 'updateMessage', }); } + } - if (!originalMessage.tmid && !!message.tmid) { - throw new Meteor.Error('error-message-change-to-thread', 'Cannot update message to a thread', { method: 'updateMessage' }); - } + const user = await Users.findOneById(uid); + if (!user) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateMessage' }); + } + await canSendMessageAsync(message.rid, { uid: user._id, username: user.username ?? undefined, ...user }); - const _hasPermission = await hasPermissionAsync(uid, 'edit-message', message.rid); - const editAllowed = settings.get('Message_AllowEditing'); - const editOwn = originalMessage.u && originalMessage.u._id === uid; + // It is possible to have an empty array as the attachments property, so ensure both things exist + if (originalMessage.attachments && originalMessage.attachments.length > 0 && originalMessage.attachments[0].description !== undefined) { + originalMessage.attachments[0].description = message.msg; + message.attachments = originalMessage.attachments; + message.msg = originalMessage.msg; + } - if (!_hasPermission && (!editAllowed || !editOwn)) { - throw new Meteor.Error('error-action-not-allowed', 'Message editing not allowed', { - method: 'updateMessage', - action: 'Message_editing', - }); - } + message.u = originalMessage.u; - const blockEditInMinutes = settings.get('Message_AllowEditing_BlockEditInMinutes'); - const bypassBlockTimeLimit = await hasPermissionAsync(uid, 'bypass-time-limit-edit-and-delete'); - - if (!bypassBlockTimeLimit && Match.test(blockEditInMinutes, Number) && blockEditInMinutes !== 0) { - let currentTsDiff = 0; - let msgTs; - - if (originalMessage.ts instanceof Date || Match.test(originalMessage.ts, Number)) { - msgTs = moment(originalMessage.ts); - } - if (msgTs) { - currentTsDiff = moment().diff(msgTs, 'minutes'); - } - if (currentTsDiff >= blockEditInMinutes) { - throw new Meteor.Error('error-message-editing-blocked', 'Message editing is blocked', { - method: 'updateMessage', - }); - } - } + return updateMessage(message, user, originalMessage, previewUrls); +} - const user = await Users.findOneById(uid); - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateMessage' }); - } - await canSendMessageAsync(message.rid, { uid: user._id, username: user.username ?? undefined, ...user }); +declare module '@rocket.chat/ui-contexts' { + // eslint-disable-next-line @typescript-eslint/naming-convention + interface ServerMethods { + updateMessage(message: IEditedMessage, previewUrls?: string[]): void; + } +} - // It is possible to have an empty array as the attachments property, so ensure both things exist - if (originalMessage.attachments && originalMessage.attachments.length > 0 && originalMessage.attachments[0].description !== undefined) { - originalMessage.attachments[0].description = message.msg; - message.attachments = originalMessage.attachments; - message.msg = originalMessage.msg; - } +Meteor.methods({ + async updateMessage(message: IEditedMessage, previewUrls?: string[]) { + check(message, Match.ObjectIncluding({ _id: String })); + check(previewUrls, Match.Maybe([String])); + + const uid = Meteor.userId(); - message.u = originalMessage.u; + if (!uid) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateMessage' }); + } - return updateMessage(message, user, originalMessage); + return executeUpdateMessage(uid, message, previewUrls); }, }); diff --git a/apps/meteor/app/livechat/lib/inquiries.ts b/apps/meteor/app/livechat/lib/inquiries.ts index dddf32ee5467..488151aa4166 100644 --- a/apps/meteor/app/livechat/lib/inquiries.ts +++ b/apps/meteor/app/livechat/lib/inquiries.ts @@ -6,13 +6,16 @@ type ReturnType = | { priorityWeight: SortOrder; ts: SortOrder; + _updatedAt: SortOrder; } | { estimatedWaitingTimeQueue: SortOrder; ts: SortOrder; + _updatedAt: SortOrder; } | { ts: SortOrder; + _updatedAt: SortOrder; }; export const getOmniChatSortQuery = ( @@ -20,11 +23,11 @@ export const getOmniChatSortQuery = ( ): ReturnType => { switch (sortByMechanism) { case OmnichannelSortingMechanismSettingType.Priority: - return { priorityWeight: 1, ts: 1 }; + return { priorityWeight: 1, ts: 1, _updatedAt: -1 }; case OmnichannelSortingMechanismSettingType.SLAs: - return { estimatedWaitingTimeQueue: 1, ts: 1 }; + return { estimatedWaitingTimeQueue: 1, ts: 1, _updatedAt: -1 }; case OmnichannelSortingMechanismSettingType.Timestamp: default: - return { ts: 1 }; + return { ts: 1, _updatedAt: -1 }; } }; diff --git a/apps/meteor/app/livechat/server/api/lib/departments.ts b/apps/meteor/app/livechat/server/api/lib/departments.ts index 215938fbb20b..049dbebaf7aa 100644 --- a/apps/meteor/app/livechat/server/api/lib/departments.ts +++ b/apps/meteor/app/livechat/server/api/lib/departments.ts @@ -129,7 +129,7 @@ export async function findDepartmentById({ department: await LivechatDepartment.findOne(query), ...(includeAgents && canViewLivechatDepartments && { - agents: await LivechatDepartmentAgents.find({ departmentId }).toArray(), + agents: await LivechatDepartmentAgents.findByDepartmentId(departmentId).toArray(), }), }; @@ -192,6 +192,6 @@ export async function findDepartmentsBetweenIds({ ids: string[]; fields: Record; }): Promise<{ departments: ILivechatDepartment[] }> { - const departments = await LivechatDepartment.findInIds(ids, fields).toArray(); + const departments = await LivechatDepartment.findInIds(ids, { projection: fields }).toArray(); return { departments }; } diff --git a/apps/meteor/app/livechat/server/api/lib/inquiries.ts b/apps/meteor/app/livechat/server/api/lib/inquiries.ts index 450e035af257..19cbfc21ede9 100644 --- a/apps/meteor/app/livechat/server/api/lib/inquiries.ts +++ b/apps/meteor/app/livechat/server/api/lib/inquiries.ts @@ -8,8 +8,10 @@ import { getOmniChatSortQuery } from '../../../lib/inquiries'; import { getInquirySortMechanismSetting } from '../../lib/settings'; const agentDepartments = async (userId: IUser['_id']): Promise => { - const agentDepartments = (await LivechatDepartmentAgents.findByAgentId(userId).toArray()).map(({ departmentId }) => departmentId); - return (await LivechatDepartment.find({ _id: { $in: agentDepartments }, enabled: true }).toArray()).map(({ _id }) => _id); + const agentDepartments = (await LivechatDepartmentAgents.findByAgentId(userId, { projection: { departmentId: 1 } }).toArray()).map( + ({ departmentId }) => departmentId, + ); + return (await LivechatDepartment.findEnabledInIds(agentDepartments, { projection: { _id: 1 } }).toArray()).map(({ _id }) => _id); }; const applyDepartmentRestrictions = async ( diff --git a/apps/meteor/app/livechat/server/api/rest.ts b/apps/meteor/app/livechat/server/api/rest.ts index d1eb008ab4df..f9da6690185e 100644 --- a/apps/meteor/app/livechat/server/api/rest.ts +++ b/apps/meteor/app/livechat/server/api/rest.ts @@ -12,3 +12,4 @@ import './v1/transfer'; import './v1/contact'; import './v1/webhooks'; import './v1/integration'; +import './v1/statistics'; diff --git a/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts b/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts index 8302e014b894..b01e60d2265f 100644 --- a/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts +++ b/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts @@ -10,11 +10,12 @@ API.v1.addRoute( { async post() { const { name, email, message, department, host } = this.bodyParams; - if (!(await Livechat.sendOfflineMessage({ name, email, message, department, host }))) { - return API.v1.failure({ message: i18n.t('Error_sending_livechat_offline_message') }); + try { + await Livechat.sendOfflineMessage({ name, email, message, department, host }); + return API.v1.success({ message: i18n.t('Livechat_offline_message_sent') }); + } catch (e) { + return API.v1.failure(i18n.t('Error_sending_livechat_offline_message')); } - - return API.v1.success({ message: i18n.t('Livechat_offline_message_sent') }); }, }, ); diff --git a/apps/meteor/app/livechat/server/api/v1/statistics.ts b/apps/meteor/app/livechat/server/api/v1/statistics.ts new file mode 100644 index 000000000000..078f366bb484 --- /dev/null +++ b/apps/meteor/app/livechat/server/api/v1/statistics.ts @@ -0,0 +1,65 @@ +import { Users } from '@rocket.chat/models'; +import { isLivechatAnalyticsAgentOverviewProps, isLivechatAnalyticsOverviewProps } from '@rocket.chat/rest-typings'; + +import { API } from '../../../../api/server'; +import { settings } from '../../../../settings/server'; +import { Livechat } from '../../lib/Livechat'; + +API.v1.addRoute( + 'livechat/analytics/agent-overview', + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: isLivechatAnalyticsAgentOverviewProps, + }, + { + async get() { + const { name, departmentId, from, to } = this.queryParams; + + if (!name) { + throw new Error('invalid-chart-name'); + } + + const user = await Users.findOneById(this.userId, { projection: { _id: 1, utcOffset: 1 } }); + return API.v1.success( + await Livechat.Analytics.getAgentOverviewData({ + departmentId, + utcOffset: user?.utcOffset || 0, + daterange: { from, to }, + chartOptions: { name }, + }), + ); + }, + }, +); + +API.v1.addRoute( + 'livechat/analytics/overview', + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: isLivechatAnalyticsOverviewProps, + }, + { + async get() { + const { name, departmentId, from, to } = this.queryParams; + + if (!name) { + throw new Error('invalid-chart-name'); + } + + const user = await Users.findOneById(this.userId, { projection: { _id: 1, utcOffset: 1 } }); + const language = user?.language || settings.get('Language') || 'en'; + + return API.v1.success( + await Livechat.Analytics.getAnalyticsOverviewData({ + departmentId, + utcOffset: user?.utcOffset || 0, + daterange: { from, to }, + analyticsOptions: { name }, + language, + }), + ); + }, + }, +); diff --git a/apps/meteor/app/livechat/server/hooks/offlineMessageToChannel.ts b/apps/meteor/app/livechat/server/hooks/offlineMessageToChannel.ts index c8166d1a7454..7cc8f8e6e9ad 100644 --- a/apps/meteor/app/livechat/server/hooks/offlineMessageToChannel.ts +++ b/apps/meteor/app/livechat/server/hooks/offlineMessageToChannel.ts @@ -1,3 +1,4 @@ +import type { ILivechatDepartment } from '@rocket.chat/core-typings'; import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatDepartment, Users, Rooms } from '@rocket.chat/models'; @@ -17,9 +18,12 @@ callbacks.add( let departmentName; const { name, email, department, message: text, host } = data; if (department && department !== '') { - const dept = await LivechatDepartment.findOneById(department, { - projection: { name: 1, offlineMessageChannelName: 1 }, - }); + const dept = await LivechatDepartment.findOneById>( + department, + { + projection: { name: 1, offlineMessageChannelName: 1 }, + }, + ); departmentName = dept?.name; if (dept?.offlineMessageChannelName) { channelName = dept.offlineMessageChannelName; @@ -30,7 +34,7 @@ callbacks.add( return data; } - const room: any = await Rooms.findOneByName(channelName, { projection: { t: 1, archived: 1 } }); + const room = await Rooms.findOneByName(channelName, { projection: { t: 1, archived: 1 } }); if (!room || room.archived || (isOmnichannelRoom(room) && room.closedAt)) { return data; } diff --git a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts index b2c168ff9602..8a5a4c280670 100644 --- a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts +++ b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts @@ -1,4 +1,4 @@ -import type { IOmnichannelRoom, IMessage, IBusinessHourWorkHour } from '@rocket.chat/core-typings'; +import type { IOmnichannelRoom, IMessage, IBusinessHourWorkHour, ILivechatDepartment } from '@rocket.chat/core-typings'; import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatBusinessHours, LivechatDepartment, Messages, LivechatRooms } from '@rocket.chat/models'; import moment from 'moment'; @@ -27,7 +27,11 @@ const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLas return getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage); } let officeDays; - const department = room.departmentId ? await LivechatDepartment.findOneById(room.departmentId) : null; + const department = room.departmentId + ? await LivechatDepartment.findOneById>(room.departmentId, { + projection: { businessHourId: 1 }, + }) + : null; if (department?.businessHourId) { const businessHour = await LivechatBusinessHours.findOneById(department.businessHourId); if (!businessHour) { diff --git a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts index 48a52aa41fe0..ec584ec001d6 100644 --- a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts +++ b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts @@ -8,12 +8,12 @@ import { callbackLogger } from '../lib/logger'; callbacks.add( 'afterSaveMessage', async (message, room) => { - callbackLogger.debug(`Calculating Omnichannel metrics for room ${room._id}`); // check if room is livechat if (!isOmnichannelRoom(room)) { return message; } + callbackLogger.debug(`Calculating Omnichannel metrics for room ${room._id}`); // skips this callback if the message was edited if (!message || isEditedMessage(message)) { return message; diff --git a/apps/meteor/app/livechat/server/hooks/saveLastVisitorMessageTs.ts b/apps/meteor/app/livechat/server/hooks/saveLastVisitorMessageTs.ts index 576b14722a2a..4bc28c3990ba 100644 --- a/apps/meteor/app/livechat/server/hooks/saveLastVisitorMessageTs.ts +++ b/apps/meteor/app/livechat/server/hooks/saveLastVisitorMessageTs.ts @@ -12,10 +12,11 @@ callbacks.add( if (message.t) { return message; } - if (message.token) { - await LivechatRooms.setVisitorLastMessageTimestampByRoomId(room._id, message.ts); + if (!message.token) { + return message; } - return message; + + await LivechatRooms.setVisitorLastMessageTimestampByRoomId(room._id, message.ts); }, callbacks.priority.HIGH, 'save-last-visitor-message-timestamp', diff --git a/apps/meteor/app/livechat/server/lib/Departments.ts b/apps/meteor/app/livechat/server/lib/Departments.ts index 9743b1b65d3f..0dd48a328fd1 100644 --- a/apps/meteor/app/livechat/server/lib/Departments.ts +++ b/apps/meteor/app/livechat/server/lib/Departments.ts @@ -1,3 +1,4 @@ +import type { ILivechatDepartmentAgents } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; import { LivechatDepartment, LivechatDepartmentAgents, LivechatRooms } from '@rocket.chat/models'; @@ -24,7 +25,10 @@ class DepartmentHelperClass { } this.logger.debug(`Department record removed: ${_id}`); - const agentsIds: string[] = await LivechatDepartmentAgents.findAgentsByDepartmentId(department._id) + const agentsIds: string[] = await LivechatDepartmentAgents.findAgentsByDepartmentId>( + department._id, + { projection: { agentId: 1 } }, + ) .cursor.map((agent) => agent.agentId) .toArray(); diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index 5f7fad7d2fe8..75722e709b17 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -12,6 +12,7 @@ import type { ILivechatDepartmentAgents, TransferByData, ILivechatAgent, + ILivechatDepartment, } from '@rocket.chat/core-typings'; import { LivechatInquiryStatus, OmnichannelSourceType, DEFAULT_SLA_CONFIG, UserStatus } from '@rocket.chat/core-typings'; import { LivechatPriorityWeight } from '@rocket.chat/core-typings/src/ILivechatPriority'; @@ -519,7 +520,9 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi if (!user) { throw new Error('error-user-is-offline'); } - const isInDepartment = await LivechatDepartmentAgents.findOneByAgentIdAndDepartmentId(agentId, departmentId); + const isInDepartment = await LivechatDepartmentAgents.findOneByAgentIdAndDepartmentId(agentId, departmentId, { + projection: { _id: 1 }, + }); if (!isInDepartment) { throw new Error('error-user-not-belong-to-department'); } @@ -549,7 +552,11 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi const { servedBy, chatQueued } = roomTaken; if (!chatQueued && oldServedBy && servedBy && oldServedBy._id === servedBy._id) { - const department = departmentId ? await LivechatDepartment.findOneById(departmentId) : null; + const department = departmentId + ? await LivechatDepartment.findOneById>(departmentId, { + projection: { fallbackForwardDepartment: 1 }, + }) + : null; if (!department?.fallbackForwardDepartment?.length) { logger.debug(`Cannot forward room ${room._id}. Chat assigned to agent ${servedBy._id} (Previous was ${oldServedBy._id})`); throw new Error('error-no-agents-online-in-department'); diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 21de9c332ee3..52138740e295 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -709,7 +709,9 @@ export const Livechat = { }); } const ret = (await LivechatDepartmentRaw.removeById(_id)).deletedCount; - const agentsIds = (await LivechatDepartmentAgents.findByDepartmentId(_id).toArray()).map((agent) => agent.agentId); + const agentsIds = (await LivechatDepartmentAgents.findByDepartmentId(_id, { projection: { agentId: 1 } }).toArray()).map( + (agent) => agent.agentId, + ); await LivechatDepartmentAgents.removeByDepartmentId(_id); await LivechatDepartmentRaw.unsetFallbackDepartmentByDepartmentId(_id); if (ret) { @@ -839,7 +841,7 @@ export const Livechat = { async sendOfflineMessage(data = {}) { if (!settings.get('Livechat_display_offline_form')) { - return false; + throw new Error('error-offline-form-disabled'); } const { message, name, email, department, host } = data; @@ -892,8 +894,6 @@ export const Livechat = { setImmediate(() => { callbacks.run('livechat.offlineMessage', data); }); - - return true; }, async notifyAgentStatusChanged(userId, status) { diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 289ac0647545..1c60a257d319 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -9,6 +9,7 @@ import type { SelectedAgent, ILivechatAgent, IMessage, + ILivechatDepartment, } from '@rocket.chat/core-typings'; import { UserStatus, isOmnichannelRoom } from '@rocket.chat/core-typings'; import { Logger, type MainLogger } from '@rocket.chat/logger'; @@ -299,7 +300,10 @@ class LivechatClass { room = null; } - if (guest.department && !(await LivechatDepartment.findOneById(guest.department))) { + if ( + guest.department && + !(await LivechatDepartment.findOneById>(guest.department, { projection: { _id: 1 } })) + ) { await LivechatVisitors.removeDepartmentById(guest._id); const tmpGuest = await LivechatVisitors.findOneById(guest._id); if (tmpGuest) { @@ -357,7 +361,9 @@ class LivechatClass { return onlineForDep; } - const dep = await LivechatDepartment.findOneById(department); + const dep = await LivechatDepartment.findOneById>(department, { + projection: { fallbackForwardDepartment: 1 }, + }); if (!dep?.fallbackForwardDepartment) { return onlineForDep; } @@ -586,7 +592,7 @@ class LivechatClass { if (department) { Livechat.logger.debug(`Attempt to find a department with id/name ${department}`); - const dep = await LivechatDepartment.findOneByIdOrName(department); + const dep = await LivechatDepartment.findOneByIdOrName(department, { projection: { _id: 1 } }); if (!dep) { Livechat.logger.debug('Invalid department provided'); throw new Meteor.Error('error-invalid-department', 'The provided department is invalid'); @@ -673,7 +679,12 @@ class LivechatClass { }; } - const department = await LivechatDepartment.findOneById(departmentId); + const department = await LivechatDepartment.findOneById>( + departmentId, + { + projection: { requestTagBeforeClosingChat: 1, chatClosingTags: 1 }, + }, + ); if (!department) { return { updatedOptions: { diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index ebbd931c1b2b..0e975ca06763 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -1,4 +1,4 @@ -import { Message } from '@rocket.chat/core-services'; +import { Message, Omnichannel } from '@rocket.chat/core-services'; import type { ILivechatInquiryRecord, ILivechatVisitor, @@ -35,7 +35,7 @@ type Routing = { methods: Record; startQueue(): void; isMethodSet(): boolean; - setMethodNameAndStartQueue(name: string): void; + setMethodNameAndStartQueue(name: string): Promise; registerMethod(name: string, Method: IRoutingMethodConstructor): void; getMethod(): IRoutingMethod; getConfig(): RoutingMethodConfig | undefined; @@ -73,7 +73,7 @@ export const RoutingManager: Routing = { return !!this.methodName; }, - setMethodNameAndStartQueue(name) { + async setMethodNameAndStartQueue(name) { logger.debug(`Changing default routing method from ${this.methodName} to ${name}`); if (!this.methods[name]) { logger.warn(`Cannot change routing method to ${name}. Selected Routing method does not exists. Defaulting to Manual_Selection`); @@ -82,7 +82,7 @@ export const RoutingManager: Routing = { this.methodName = name; } - this.startQueue(); + void (await Omnichannel.getQueueWorker()).shouldStart(); }, // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/apps/meteor/app/livechat/server/methods/discardTranscript.ts b/apps/meteor/app/livechat/server/methods/discardTranscript.ts index 2e03b6ebd929..d46c8ffea35f 100644 --- a/apps/meteor/app/livechat/server/methods/discardTranscript.ts +++ b/apps/meteor/app/livechat/server/methods/discardTranscript.ts @@ -17,6 +17,9 @@ Meteor.methods({ async 'livechat:discardTranscript'(rid: string) { methodDeprecationLogger.method('livechat:discardTranscript', '7.0.0'); check(rid, String); + methodDeprecationLogger.warn( + 'The method "livechat:discardTranscript" is deprecated and will be removed after version v7.0.0. Use "livechat/transcript/:rid" (DELETE) instead.', + ); const user = Meteor.userId(); diff --git a/apps/meteor/app/livechat/server/methods/getAgentData.ts b/apps/meteor/app/livechat/server/methods/getAgentData.ts index f4ba67ebe10e..d32d24d7f7c1 100644 --- a/apps/meteor/app/livechat/server/methods/getAgentData.ts +++ b/apps/meteor/app/livechat/server/methods/getAgentData.ts @@ -4,6 +4,7 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { settings } from '../../../settings/server'; declare module '@rocket.chat/ui-contexts' { @@ -21,6 +22,10 @@ Meteor.methods({ check(roomId, String); check(token, String); + methodDeprecationLogger.warn( + 'The method "livechat:getAgentData" is deprecated and will be removed after version v7.0.0. Use "livechat/agent.info/:rid/:token" instead.', + ); + const room = await LivechatRooms.findOneById(roomId); const visitor = await LivechatVisitors.getVisitorByToken(token); diff --git a/apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts b/apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts index afbc2e8bb7fc..94fae239b74c 100644 --- a/apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts +++ b/apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts @@ -3,6 +3,7 @@ import type { ServerMethods, TranslationKey } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { Livechat } from '../lib/Livechat'; declare module '@rocket.chat/ui-contexts' { @@ -17,6 +18,10 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async 'livechat:getAgentOverviewData'(options) { + methodDeprecationLogger.warn( + 'The method "livechat:getAgentOverviewData" is deprecated and will be removed after version v7.0.0. Use "livechat/analytics/agent-overview" instead.', + ); + const uid = Meteor.userId(); if (!uid || !(await hasPermissionAsync(uid, 'view-livechat-manager'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { diff --git a/apps/meteor/app/livechat/server/methods/sendOfflineMessage.ts b/apps/meteor/app/livechat/server/methods/sendOfflineMessage.ts index c5c31951894e..9a475de5e32d 100644 --- a/apps/meteor/app/livechat/server/methods/sendOfflineMessage.ts +++ b/apps/meteor/app/livechat/server/methods/sendOfflineMessage.ts @@ -9,7 +9,7 @@ import { Livechat } from '../lib/Livechat'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - 'livechat:sendOfflineMessage'(data: { name: string; email: string; message: string }): Promise; + 'livechat:sendOfflineMessage'(data: { name: string; email: string; message: string }): Promise; } } @@ -23,7 +23,7 @@ Meteor.methods({ message: String, }); - return Livechat.sendOfflineMessage(data); + await Livechat.sendOfflineMessage(data); }, }); diff --git a/apps/meteor/app/livechat/server/roomAccessValidator.compatibility.ts b/apps/meteor/app/livechat/server/roomAccessValidator.compatibility.ts index 8e598ee108d3..d5ef83272550 100644 --- a/apps/meteor/app/livechat/server/roomAccessValidator.compatibility.ts +++ b/apps/meteor/app/livechat/server/roomAccessValidator.compatibility.ts @@ -1,4 +1,4 @@ -import type { IUser, ILivechatDepartment, IOmnichannelRoom } from '@rocket.chat/core-typings'; +import type { IUser, IOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatDepartmentAgents, LivechatInquiry, LivechatRooms, LivechatDepartment } from '@rocket.chat/models'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; @@ -47,10 +47,10 @@ export const validators: OmnichannelRoomAccessValidator[] = [ let departmentIds; if (!(await hasRoleAsync(user._id, 'livechat-manager'))) { - const departmentAgents = (await LivechatDepartmentAgents.findByAgentId(user._id).toArray()).map((d) => d.departmentId); - departmentIds = (await LivechatDepartment.find({ _id: { $in: departmentAgents }, enabled: true }).toArray()).map( - (d: ILivechatDepartment) => d._id, + const departmentAgents = (await LivechatDepartmentAgents.findByAgentId(user._id, { projection: { departmentId: 1 } }).toArray()).map( + (d) => d.departmentId, ); + departmentIds = (await LivechatDepartment.findEnabledInIds(departmentAgents, { projection: { _id: 1 } }).toArray()).map((d) => d._id); } const filter = { @@ -75,7 +75,9 @@ export const validators: OmnichannelRoomAccessValidator[] = [ if (!room.departmentId || room.open || !user?._id) { return; } - const agentOfDepartment = await LivechatDepartmentAgents.findOneByAgentIdAndDepartmentId(user._id, room.departmentId); + const agentOfDepartment = await LivechatDepartmentAgents.findOneByAgentIdAndDepartmentId(user._id, room.departmentId, { + projection: { _id: 1 }, + }); if (!agentOfDepartment) { return; } diff --git a/apps/meteor/app/livechat/server/startup.ts b/apps/meteor/app/livechat/server/startup.ts index 84b2adfa0755..f24f88975b22 100644 --- a/apps/meteor/app/livechat/server/startup.ts +++ b/apps/meteor/app/livechat/server/startup.ts @@ -73,7 +73,7 @@ Meteor.startup(async () => { }); settings.watch('Livechat_Routing_Method', (value) => { - RoutingManager.setMethodNameAndStartQueue(value); + void RoutingManager.setMethodNameAndStartQueue(value); }); // Remove when accounts.onLogout is async diff --git a/apps/meteor/app/mailer/server/api.ts b/apps/meteor/app/mailer/server/api.ts index 957bbbe3cc51..b50fdfd26a2a 100644 --- a/apps/meteor/app/mailer/server/api.ts +++ b/apps/meteor/app/mailer/server/api.ts @@ -75,11 +75,12 @@ export const wrap = (html: string, data: { [key: string]: unknown } = {}): strin } if (!body) { - throw new Error('`body` is not set yet'); + throw new Error('error-email-body-not-initialized'); } return replaceEscaped(body.replace('{{body}}', html), data); }; + export const inlinecss = (html: string): string => { const css = settings.get('email_style'); return css ? juice.inlineContent(html, css) : html; diff --git a/apps/meteor/app/message-mark-as-unread/client/actionButton.ts b/apps/meteor/app/message-mark-as-unread/client/actionButton.ts index f8c9b8c39ca3..fc4a9d80c43c 100644 --- a/apps/meteor/app/message-mark-as-unread/client/actionButton.ts +++ b/apps/meteor/app/message-mark-as-unread/client/actionButton.ts @@ -14,6 +14,7 @@ Meteor.startup(() => { icon: 'flag', label: 'Mark_unread', context: ['message', 'message-mobile', 'threads'], + type: 'interaction', async action(_, props) { const { message = messageArgs(this).msg } = props; @@ -44,7 +45,7 @@ Meteor.startup(() => { return message.u._id !== user._id; }, - order: 10, + order: 4, group: 'menu', }); }); diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts index 3b734e70e233..06c3014a8a56 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts @@ -221,8 +221,8 @@ export class SAML { }, ); - if (username && fullName && (username !== user.username || fullName !== user.name)) { - await saveUserIdentity({ _id: user._id, name: fullName, username }); + if ((username && username !== user.username) || (fullName && fullName !== user.name)) { + await saveUserIdentity({ _id: user._id, name: fullName || undefined, username }); } // sending token along with the userId @@ -430,7 +430,7 @@ export class SAML { }; await this.storeCredential(credentialToken, loginResult); - const url = `${Meteor.absoluteUrl('saml')}/${credentialToken}`; + const url = Meteor.absoluteUrl(SAMLUtils.getValidationActionRedirectPath(credentialToken)); res.writeHead(302, { Location: url, }); diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts index 9b6e2a42a4c5..70df22120b75 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts @@ -131,6 +131,11 @@ export class SAMLUtils { return newTemplate; } + public static getValidationActionRedirectPath(credentialToken: string): string { + // the saml_idp_credentialToken param is needed by the mobile app + return `saml/${credentialToken}?saml_idp_credentialToken=${credentialToken}`; + } + public static log(obj: any, ...args: Array): void { if (debug && logger) { logger.debug(obj, ...args); diff --git a/apps/meteor/app/oembed/server/jumpToMessage.ts b/apps/meteor/app/oembed/server/jumpToMessage.ts index a2fbe466edf4..9c5be2639c7c 100644 --- a/apps/meteor/app/oembed/server/jumpToMessage.ts +++ b/apps/meteor/app/oembed/server/jumpToMessage.ts @@ -4,7 +4,6 @@ import URL from 'url'; import type { MessageAttachment, IMessage, IOmnichannelRoom } from '@rocket.chat/core-typings'; import { isQuoteAttachment } from '@rocket.chat/core-typings'; import { Messages, Users, Rooms } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../lib/callbacks'; import { createQuoteAttachment } from '../../../lib/createQuoteAttachment'; @@ -51,7 +50,7 @@ callbacks.add( for await (const item of msg.urls) { // if the URL doesn't belong to the current server, skip - if (!item.url.includes(Meteor.absoluteUrl())) { + if (!item.url.includes(settings.get('Site_Url'))) { continue; } diff --git a/apps/meteor/app/oembed/server/server.ts b/apps/meteor/app/oembed/server/server.ts index 75387f796219..3bf1ae3245be 100644 --- a/apps/meteor/app/oembed/server/server.ts +++ b/apps/meteor/app/oembed/server/server.ts @@ -18,6 +18,7 @@ import { isURL } from '../../../lib/utils/isURL'; import { settings } from '../../settings/server'; import { Info } from '../../utils/rocketchat.info'; +const MAX_EXTERNAL_URL_PREVIEWS = 5; const log = new Logger('OEmbed'); // Detect encoding // Priority: @@ -287,16 +288,25 @@ const rocketUrlParser = async function (message: IMessage): Promise { log.debug('Parsing message URLs'); if (Array.isArray(message.urls)) { log.debug('URLs found', message.urls.length); + + if ( + message.attachments || + message.urls.filter((item) => !item.url.includes(settings.get('Site_Url'))).length > MAX_EXTERNAL_URL_PREVIEWS + ) { + log.debug('All URL ignored'); + return message; + } + const attachments: MessageAttachment[] = []; let changed = false; for await (const item of message.urls) { if (item.ignoreParse === true) { log.debug('URL ignored', item.url); - break; + continue; } if (!isURL(item.url)) { - break; + continue; } const data = await getUrlMetaWithCache(item.url); if (data != null) { diff --git a/apps/meteor/app/push/server/apn.ts b/apps/meteor/app/push/server/apn.ts index e64bf08c1c78..1794a2886324 100644 --- a/apps/meteor/app/push/server/apn.ts +++ b/apps/meteor/app/push/server/apn.ts @@ -65,7 +65,7 @@ export const sendAPN = ({ note.payload.messageFrom = notification.from; note.priority = priority; - note.topic = notification.topic; + note.topic = `${notification.topic}${notification.apn?.topicSuffix || ''}`; note.mutableContent = true; void apnConnection.send(note, userToken).then((response) => { diff --git a/apps/meteor/app/push/server/definition.ts b/apps/meteor/app/push/server/definition.ts index c849d06c11b8..39ab94de0a36 100644 --- a/apps/meteor/app/push/server/definition.ts +++ b/apps/meteor/app/push/server/definition.ts @@ -26,6 +26,7 @@ export type PendingPushNotification = { notId?: number; apn?: { category?: string; + topicSuffix?: string; }; gcm?: { style?: string; diff --git a/apps/meteor/app/push/server/push.ts b/apps/meteor/app/push/server/push.ts index 4ffc2c288eb5..fe08f7b0a214 100644 --- a/apps/meteor/app/push/server/push.ts +++ b/apps/meteor/app/push/server/push.ts @@ -200,6 +200,17 @@ class PushClass { } } + private getGatewayNotificationData(notification: PendingPushNotification): Omit { + // Gateway accepts every attribute from the PendingPushNotification type, except for the priority and apn.topicSuffix + const { priority: _priority, apn, ...notifData } = notification; + const { topicSuffix: _topicSuffix, ...apnData } = apn || ({} as RequiredField['apn']); + + return { + ...notifData, + ...(notification.apn ? { apn: { ...apnData } } : {}), + }; + } + private async sendNotificationGateway( app: IAppsTokens, notification: PendingPushNotification, @@ -210,20 +221,21 @@ class PushClass { return; } - // Gateway accepts every attribute from the PendingPushNotification type, except for the priority - const { priority: _priority, ...notifData } = notification; + const { topicSuffix = '' } = notification.apn || {}; + + const gatewayNotification = this.getGatewayNotificationData(notification); for (const gateway of this.options.gateways) { logger.debug('send to token', app.token); if ('apn' in app.token && app.token.apn) { countApn.push(app._id); - return this.sendGatewayPush(gateway, 'apn', app.token.apn, { topic: app.appName, ...notifData }); + return this.sendGatewayPush(gateway, 'apn', app.token.apn, { topic: `${app.appName}${topicSuffix}`, ...gatewayNotification }); } if ('gcm' in app.token && app.token.gcm) { countGcm.push(app._id); - return this.sendGatewayPush(gateway, 'gcm', app.token.gcm, notifData); + return this.sendGatewayPush(gateway, 'gcm', app.token.gcm, gatewayNotification); } } } @@ -306,6 +318,7 @@ class PushClass { contentAvailable: Match.Optional(Match.Integer), apn: Match.Optional({ category: Match.Optional(String), + topicSuffix: Match.Optional(String), }), gcm: Match.Optional({ image: Match.Optional(String), @@ -344,7 +357,7 @@ class PushClass { ...(this.hasApnOptions(options) ? { apn: { - ...pick(options.apn, 'category'), + ...pick(options.apn, 'category', 'topicSuffix'), }, } : {}), diff --git a/apps/meteor/app/reactions/client/init.ts b/apps/meteor/app/reactions/client/init.ts index 53fe884febfc..24840b9de7cf 100644 --- a/apps/meteor/app/reactions/client/init.ts +++ b/apps/meteor/app/reactions/client/init.ts @@ -40,6 +40,6 @@ Meteor.startup(() => { return true; }, order: -3, - group: ['message', 'menu'], + group: 'message', }); }); diff --git a/apps/meteor/app/slashcommands-invite/server/server.ts b/apps/meteor/app/slashcommands-invite/server/server.ts index ba1def1538ab..de525d8c6fc6 100644 --- a/apps/meteor/app/slashcommands-invite/server/server.ts +++ b/apps/meteor/app/slashcommands-invite/server/server.ts @@ -1,6 +1,7 @@ import { api } from '@rocket.chat/core-services'; import type { IUser, SlashCommandCallbackParams } from '@rocket.chat/core-typings'; import { Subscriptions, Users } from '@rocket.chat/models'; +import { Meteor } from 'meteor/meteor'; import { i18n } from '../../../server/lib/i18n'; import { addUsersToRoomMethod } from '../../lib/server/methods/addUsersToRoom'; @@ -57,13 +58,25 @@ slashCommands.add({ }); } + const inviter = await Users.findOneById(userId); + + if (!inviter) { + throw new Meteor.Error('error-user-not-found', 'Inviter not found', { + method: 'slashcommand-invite', + }); + } + await Promise.all( usersFiltered.map(async (user) => { try { - return await addUsersToRoomMethod(userId, { - rid: message.rid, - users: [user.username || ''], - }); + return await addUsersToRoomMethod( + userId, + { + rid: message.rid, + users: [user.username || ''], + }, + inviter, + ); } catch ({ error }: any) { if (typeof error !== 'string') { return; diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index d6af563638ba..8cfe45b42232 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -122,11 +122,11 @@ export const statistics = { statistics.totalThreads = await Messages.countThreads(); // livechat visitors - statistics.totalLivechatVisitors = await LivechatVisitors.col.estimatedDocumentCount(); + statistics.totalLivechatVisitors = await LivechatVisitors.estimatedDocumentCount(); // livechat agents statistics.totalLivechatAgents = await Users.countAgents(); - statistics.totalLivechatManagers = await Users.col.countDocuments({ roles: 'livechat-manager' }); + statistics.totalLivechatManagers = await Users.countDocuments({ roles: 'livechat-manager' }); // livechat enabled statistics.livechatEnabled = settings.get('Livechat_enabled'); @@ -147,14 +147,14 @@ export const statistics = { // Number of departments statsPms.push( - LivechatDepartment.col.count().then((count) => { + LivechatDepartment.estimatedDocumentCount().then((count) => { statistics.departments = count; }), ); // Number of archived departments statsPms.push( - LivechatDepartment.col.countDocuments({ archived: true }).then((count) => { + LivechatDepartment.countArchived().then((count) => { statistics.archivedDepartments = count; }), ); diff --git a/apps/meteor/app/theme/client/imports/general/base_old.css b/apps/meteor/app/theme/client/imports/general/base_old.css index 6d9f5631fc6a..7f1ede6067fc 100644 --- a/apps/meteor/app/theme/client/imports/general/base_old.css +++ b/apps/meteor/app/theme/client/imports/general/base_old.css @@ -988,10 +988,6 @@ font-size: 12px; font-weight: 300; } - - & div.switch-language { - margin-top: 20px; - } } & .share { diff --git a/apps/meteor/app/threads/client/messageAction/follow.ts b/apps/meteor/app/threads/client/messageAction/follow.ts index 21ad8daba901..b4ca1e63e9f9 100644 --- a/apps/meteor/app/threads/client/messageAction/follow.ts +++ b/apps/meteor/app/threads/client/messageAction/follow.ts @@ -18,6 +18,7 @@ Meteor.startup(() => { id: 'follow-message', icon: 'bell', label: 'Follow_message', + type: 'interaction', context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], async action(_, { message }) { if (!message) { @@ -44,7 +45,7 @@ Meteor.startup(() => { } return user?._id ? !replies.includes(user._id) : false; }, - order: 2, + order: 1, group: 'menu', }); }); diff --git a/apps/meteor/app/threads/client/messageAction/replyInThread.ts b/apps/meteor/app/threads/client/messageAction/replyInThread.ts index a8c3d46d1497..03f6606a2073 100644 --- a/apps/meteor/app/threads/client/messageAction/replyInThread.ts +++ b/apps/meteor/app/threads/client/messageAction/replyInThread.ts @@ -37,7 +37,7 @@ Meteor.startup(() => { return Boolean(subscription); }, order: -1, - group: ['message', 'menu'], + group: 'message', }); }); }); diff --git a/apps/meteor/app/threads/client/messageAction/unfollow.ts b/apps/meteor/app/threads/client/messageAction/unfollow.ts index b76aa0b1189b..853e2adf535f 100644 --- a/apps/meteor/app/threads/client/messageAction/unfollow.ts +++ b/apps/meteor/app/threads/client/messageAction/unfollow.ts @@ -17,6 +17,7 @@ Meteor.startup(() => { id: 'unfollow-message', icon: 'bell-off', label: 'Unfollow_message', + type: 'interaction', context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], async action(_, { message }) { if (!message) { diff --git a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts index 5bd378cc3a11..9c970ccf697b 100644 --- a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts +++ b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts @@ -6,6 +6,7 @@ import { Mongo } from 'meteor/mongo'; import { ReactiveVar } from 'meteor/reactive-var'; import type { MinimongoCollection } from '../../../../client/definitions/MinimongoCollection'; +import { baseURI } from '../../../../client/lib/baseURI'; import { getConfig } from '../../../../client/lib/utils/getConfig'; import { isTruthy } from '../../../../lib/isTruthy'; import { withDebouncing } from '../../../../lib/utils/highOrderFunctions'; @@ -34,6 +35,10 @@ const hasUnserializedUpdatedAt = (record: T): record is T & { _updatedAt: Con '_updatedAt' in record && !((record as unknown as { _updatedAt: unknown })._updatedAt instanceof Date); +localforage.config({ + name: baseURI, +}); + export class CachedCollection extends Emitter<{ changed: T; removed: T }> { private static MAX_CACHE_TIME = 60 * 60 * 24 * 30; diff --git a/apps/meteor/app/ui-message/client/ActionManager.js b/apps/meteor/app/ui-message/client/ActionManager.js index fbb6f17a68c9..ebe9d1aed093 100644 --- a/apps/meteor/app/ui-message/client/ActionManager.js +++ b/apps/meteor/app/ui-message/client/ActionManager.js @@ -13,7 +13,7 @@ import { t } from '../../utils/lib/i18n'; const UiKitModal = lazy(() => import('../../../client/views/modal/uikit/UiKitModal')); -const events = new Emitter(); +export const events = new Emitter(); export const on = (...args) => { events.on(...args); @@ -168,6 +168,8 @@ export const handlePayloadUserInteraction = (type, { /* appId,*/ triggerId, ...d export const triggerAction = async ({ type, actionId, appId, rid, mid, viewId, container, tmid, ...rest }) => new Promise(async (resolve, reject) => { + events.emit('busy', { busy: true }); + const triggerId = generateTriggerId(appId); const payload = rest.payload || rest; @@ -190,6 +192,8 @@ export const triggerAction = async ({ type, actionId, appId, rid, mid, viewId, c } catch (e) { reject(e); return {}; + } finally { + events.emit('busy', { busy: false }); } })(); diff --git a/apps/meteor/app/ui-utils/client/index.ts b/apps/meteor/app/ui-utils/client/index.ts index aaa5a0825706..6409db5a3592 100644 --- a/apps/meteor/app/ui-utils/client/index.ts +++ b/apps/meteor/app/ui-utils/client/index.ts @@ -2,7 +2,6 @@ import './lib/messageActionDefault'; export { MessageAction } from './lib/MessageAction'; export { messageBox } from './lib/messageBox'; -export { readMessage } from './lib/readMessages'; export { LegacyRoomManager } from './lib/LegacyRoomManager'; export { upsertMessage, RoomHistoryManager } from './lib/RoomHistoryManager'; export { mainReady } from './lib/mainReady'; diff --git a/apps/meteor/app/ui-utils/client/lib/MessageAction.ts b/apps/meteor/app/ui-utils/client/lib/MessageAction.ts index 780e094d7928..043dfe87b606 100644 --- a/apps/meteor/app/ui-utils/client/lib/MessageAction.ts +++ b/apps/meteor/app/ui-utils/client/lib/MessageAction.ts @@ -23,6 +23,8 @@ export type MessageActionContext = | 'search' | 'videoconf-threads'; +type MessageActionType = 'communication' | 'interaction' | 'duplication' | 'apps' | 'management'; + type MessageActionConditionProps = { message: IMessage; user: IUser | undefined; @@ -62,6 +64,7 @@ export type MessageActionConfig = { }, ) => any; condition?: (props: MessageActionConditionProps) => Promise | boolean; + type?: MessageActionType; }; class MessageAction { diff --git a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts index 27ac9d893b15..4e2f0c020a12 100644 --- a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts @@ -13,7 +13,6 @@ import { getConfig } from '../../../../client/lib/utils/getConfig'; import { waitForElement } from '../../../../client/lib/utils/waitForElement'; import { ChatMessage, ChatSubscription } from '../../../models/client'; import { getUserPreference } from '../../../utils/client'; -import { readMessage } from './readMessages'; export async function upsertMessage( { @@ -196,14 +195,12 @@ class RoomHistoryManagerClass extends Emitter { } waitAfterFlush(() => { + this.emit('loaded-messages'); const heightDiff = wrapper.scrollHeight - (previousHeight ?? NaN); wrapper.scrollTop = (scroll ?? NaN) + heightDiff; }); room.isLoading.set(false); - waitAfterFlush(() => { - readMessage.refreshUnreadMark(rid); - }); } public async getMoreNext(rid: IRoom['_id'], atBottomRef: MutableRefObject) { @@ -299,9 +296,8 @@ class RoomHistoryManagerClass extends Emitter { upsertMessageBulk({ msgs: Array.from(result.messages).filter((msg) => msg.t !== 'command'), subscription }); - readMessage.refreshUnreadMark(message.rid); - Tracker.afterFlush(async () => { + this.emit('loaded-messages'); room.isLoading.set(false); }); diff --git a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts index 0805e723def6..5618442ee6da 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts @@ -9,9 +9,9 @@ import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; import { dispatchToastMessage } from '../../../../client/lib/toast'; import { messageArgs } from '../../../../client/lib/utils/messageArgs'; import { router } from '../../../../client/providers/RouterProvider'; +import ForwardMessageModal from '../../../../client/views/room/modals/ForwardMessageModal/ForwardMessageModal'; import ReactionList from '../../../../client/views/room/modals/ReactionListModal'; import ReportMessageModal from '../../../../client/views/room/modals/ReportMessageModal'; -import ShareMessageModal from '../../../../client/views/room/modals/ShareMessageModal'; import { hasAtLeastOnePermission, hasPermission } from '../../../authorization/client'; import { ChatRoom, Subscriptions } from '../../../models/client'; import { t } from '../../../utils/lib/i18n'; @@ -31,6 +31,7 @@ Meteor.startup(async () => { label: 'Reply_in_direct_message', context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], role: 'link', + type: 'communication', action(_, props) { const { message = messageArgs(this).msg } = props; roomCoordinator.openRouteLink( @@ -65,15 +66,16 @@ Meteor.startup(async () => { }); MessageAction.addButton({ - id: 'share-message', + id: 'forward-message', icon: 'arrow-forward', - label: 'Share_Message', + label: 'Forward_message', context: ['message', 'message-mobile', 'threads'], + type: 'communication', async action(_, props) { const { message = messageArgs(this).msg } = props; const permalink = await getPermaLink(message._id); imperativeModal.open({ - component: ShareMessageModal, + component: ForwardMessageModal, props: { message, permalink, @@ -84,7 +86,7 @@ Meteor.startup(async () => { }); }, order: 0, - group: ['message', 'menu'], + group: 'message', }); MessageAction.addButton({ @@ -112,15 +114,16 @@ Meteor.startup(async () => { return true; }, order: -2, - group: ['message', 'menu'], + group: 'message', }); MessageAction.addButton({ id: 'permalink', icon: 'permalink', - label: 'Get_link', + label: 'Copy_link', // classes: 'clipboard', context: ['message', 'message-mobile', 'threads', 'federated'], + type: 'duplication', async action(_, props) { try { const { message = messageArgs(this).msg } = props; @@ -134,16 +137,17 @@ Meteor.startup(async () => { condition({ subscription }) { return !!subscription; }, - order: 4, + order: 5, group: 'menu', }); MessageAction.addButton({ id: 'copy', icon: 'copy', - label: 'Copy', + label: 'Copy_text', // classes: 'clipboard', context: ['message', 'message-mobile', 'threads', 'federated'], + type: 'duplication', async action(_, props) { const { message = messageArgs(this).msg } = props; const msgText = getMainMessageText(message).msg; @@ -153,7 +157,7 @@ Meteor.startup(async () => { condition({ subscription }) { return !!subscription; }, - order: 5, + order: 6, group: 'menu', }); @@ -162,20 +166,21 @@ Meteor.startup(async () => { icon: 'edit', label: 'Edit', context: ['message', 'message-mobile', 'threads', 'federated'], + type: 'management', async action(_, props) { const { message = messageArgs(this).msg, chat } = props; await chat?.messageEditing.editMessage(message); }, - condition({ message, subscription, settings, room }) { + condition({ message, subscription, settings, room, user }) { if (subscription == null) { return false; } if (isRoomFederated(room)) { - return message.u._id === Meteor.userId(); + return message.u._id === user?._id; } const canEditMessage = hasAtLeastOnePermission('edit-message', message.rid); const isEditAllowed = settings.Message_AllowEditing; - const editOwn = message.u && message.u._id === Meteor.userId(); + const editOwn = message.u && message.u._id === user?._id; if (!(canEditMessage || (isEditAllowed && editOwn))) { return false; } @@ -195,7 +200,7 @@ Meteor.startup(async () => { } return true; }, - order: 6, + order: 8, group: 'menu', }); @@ -205,15 +210,16 @@ Meteor.startup(async () => { label: 'Delete', context: ['message', 'message-mobile', 'threads', 'federated'], color: 'alert', + type: 'management', async action(this: unknown, _, { message = messageArgs(this).msg, chat }) { await chat?.flows.requestMessageDeletion(message); }, - condition({ message, subscription, room, chat }) { + condition({ message, subscription, room, chat, user }) { if (!subscription) { return false; } if (isRoomFederated(room)) { - return message.u._id === Meteor.userId(); + return message.u._id === user?._id; } const isLivechatRoom = roomCoordinator.isLivechatRoom(room.t); if (isLivechatRoom) { @@ -222,7 +228,7 @@ Meteor.startup(async () => { return chat?.data.canDeleteMessage(message) ?? false; }, - order: 18, + order: 10, group: 'menu', }); @@ -232,6 +238,7 @@ Meteor.startup(async () => { label: 'Report', context: ['message', 'message-mobile', 'threads', 'federated'], color: 'alert', + type: 'management', action(this: unknown, _, { message = messageArgs(this).msg }) { imperativeModal.open({ component: ReportMessageModal, @@ -241,14 +248,15 @@ Meteor.startup(async () => { }, }); }, - condition({ subscription, room }) { + condition({ subscription, room, message, user }) { const isLivechatRoom = roomCoordinator.isLivechatRoom(room.t); - if (isLivechatRoom) { + if (isLivechatRoom || message.u._id === user?._id) { return false; } + return Boolean(subscription); }, - order: 17, + order: 9, group: 'menu', }); @@ -257,6 +265,7 @@ Meteor.startup(async () => { icon: 'emoji', label: 'Reactions', context: ['message', 'message-mobile', 'threads'], + type: 'interaction', action(this: unknown, _, { message: { reactions = {} } = messageArgs(this).msg }) { imperativeModal.open({ component: ReactionList, @@ -266,7 +275,7 @@ Meteor.startup(async () => { condition({ message: { reactions } }) { return !!reactions; }, - order: 18, + order: 9, group: 'menu', }); }); diff --git a/apps/meteor/app/ui-utils/client/lib/readMessages.ts b/apps/meteor/app/ui-utils/client/lib/readMessages.ts deleted file mode 100644 index 2d3951fef053..000000000000 --- a/apps/meteor/app/ui-utils/client/lib/readMessages.ts +++ /dev/null @@ -1,171 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { Emitter } from '@rocket.chat/emitter'; -import { Meteor } from 'meteor/meteor'; - -import { RoomManager } from '../../../../client/lib/RoomManager'; -import { ChatSubscription, ChatMessage } from '../../../models/client'; -import { sdk } from '../../../utils/client/lib/SDKClient'; -import { LegacyRoomManager } from './LegacyRoomManager'; -import { RoomHistoryManager } from './RoomHistoryManager'; - -class ReadMessage extends Emitter { - protected enabled: boolean; - - protected debug = false; - - constructor() { - super(); - this.enable(); - } - - protected log(...args: any[]) { - return this.debug && console.log(...args); - } - - public enable() { - this.enabled = document.hasFocus(); - } - - public disable() { - this.enabled = false; - } - - public isEnable() { - return this.enabled === true; - } - - public read(rid: IRoom['_id'] | undefined = RoomManager.opened) { - if (!this.enabled) { - this.log('readMessage -> readNow canceled by enabled: false'); - return; - } - - if (!rid) { - this.log('readMessage -> readNow canceled by rid: undefined'); - return; - } - - const subscription = ChatSubscription.findOne({ rid }); - if (!subscription) { - this.log('readMessage -> readNow canceled, no subscription found for rid:', rid); - return; - } - - if (subscription.alert === false && subscription.unread === 0) { - this.log('readMessage -> readNow canceled, alert', subscription.alert, 'and unread', subscription.unread); - return; - } - - const room = LegacyRoomManager.getOpenedRoomByRid(rid); - if (!room) { - this.log('readMessage -> readNow canceled, no room found for typeName:', subscription.t + subscription.name); - return; - } - - // Only read messages if user saw the first unread message - const unreadMark = document.querySelector('.message.first-unread, .rcx-message-divider--unread'); - if (unreadMark) { - const visible = unreadMark.offsetTop >= 0; - - if (!visible) { - this.log('readMessage -> readNow canceled, unread mark visible:', visible); - return; - } - // if unread mark is not visible and there is more more not loaded unread messages - } else if (RoomHistoryManager.getRoom(rid).unreadNotLoaded.get() > 0) { - return; - } - - return this.readNow(rid); - } - - public readNow(rid: IRoom['_id'] | undefined = RoomManager.opened) { - if (!rid) { - this.log('readMessage -> readNow canceled, no rid informed'); - return; - } - - const subscription = ChatSubscription.findOne({ rid }); - if (!subscription) { - this.log('readMessage -> readNow canceled, no subscription found for rid:', rid); - return; - } - - return sdk.rest.post('/v1/subscriptions.read', { rid }).then(() => { - RoomHistoryManager.getRoom(rid).unreadNotLoaded.set(0); - return this.emit(rid); - }); - } - - public refreshUnreadMark(rid: IRoom['_id']) { - if (!rid) { - return; - } - - const subscription = ChatSubscription.findOne({ rid }, { reactive: false }); - if (!subscription) { - return; - } - - const room = LegacyRoomManager.getOpenedRoomByRid(rid); - if (!room) { - return; - } - - if (!subscription.alert && subscription.unread === 0) { - document.querySelector('.message.first-unread')?.classList.remove('first-unread'); - room.unreadSince.set(undefined); - return; - } - - let lastReadRecord = ChatMessage.findOne( - { - rid: subscription.rid, - ts: { - $lt: subscription.ls, - }, - }, - { - sort: { - ts: -1, - }, - }, - ) as { ts: Date } | undefined; - const { unreadNotLoaded } = RoomHistoryManager.getRoom(rid); - - if (!lastReadRecord && unreadNotLoaded.get() === 0) { - lastReadRecord = { ts: new Date(0) }; - } - - room.unreadSince.set(lastReadRecord || unreadNotLoaded.get() > 0 ? subscription.ls : undefined); - - if (!lastReadRecord) { - return; - } - - const firstUnreadRecord = ChatMessage.findOne( - { - 'rid': subscription.rid, - 'ts': { - $gt: lastReadRecord.ts, - }, - 'u._id': { - $ne: Meteor.userId() ?? undefined, - }, - }, - { - sort: { - ts: 1, - }, - }, - ); - - if (firstUnreadRecord) { - room.unreadFirstId = firstUnreadRecord._id; - document.querySelector('.message.first-unread')?.classList.remove('first-unread'); - document.querySelector(`.message[data-id="${firstUnreadRecord._id}"]`)?.classList.add('first-unread'); - } - } -} - -export const readMessage = new ReadMessage(); diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index fad3625d0e2f..4a4b04f11833 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -12,11 +12,13 @@ import { replyBroadcast } from '../../../../client/lib/chats/flows/replyBroadcas import { requestMessageDeletion } from '../../../../client/lib/chats/flows/requestMessageDeletion'; import { sendMessage } from '../../../../client/lib/chats/flows/sendMessage'; import { uploadFiles } from '../../../../client/lib/chats/flows/uploadFiles'; +import { ReadStateManager } from '../../../../client/lib/chats/readStateManager'; import { createUploadsAPI } from '../../../../client/lib/chats/uploads'; import { setHighlightMessage, clearHighlightMessage, } from '../../../../client/views/room/MessageList/providers/messageHighlightSubscription'; +import * as ActionManager from '../../../ui-message/client/ActionManager'; import { UserAction } from './UserAction'; type DeepWritable = T extends (...args: any) => any @@ -37,8 +39,12 @@ export class ChatMessages implements ChatAPI { public data: DataAPI; + public readStateManager: ReadStateManager; + public uploads: UploadsAPI; + public ActionManager: any; + public userCard: { open(username: string): (event: UIEvent) => void; close(): void }; public emojiPicker: { @@ -144,11 +150,14 @@ export class ChatMessages implements ChatAPI { this.uid = params.uid; this.data = createDataAPI({ rid, tmid }); this.uploads = createUploadsAPI({ rid, tmid }); + this.ActionManager = ActionManager; const unimplemented = () => { throw new Error('Flow is not implemented'); }; + this.readStateManager = new ReadStateManager(rid); + this.userCard = { open: unimplemented, close: unimplemented, diff --git a/apps/meteor/app/utils/lib/i18n.ts b/apps/meteor/app/utils/lib/i18n.ts index 309585ee284b..13d5c667709d 100644 --- a/apps/meteor/app/utils/lib/i18n.ts +++ b/apps/meteor/app/utils/lib/i18n.ts @@ -5,9 +5,7 @@ import { isObject } from '../../../lib/utils/isObject'; export const i18n = i18next.use(sprintf); -export const addSprinfToI18n = function (t: (typeof i18n)['t']): typeof t & { - (key: string, ...replaces: any): string; -} { +export const addSprinfToI18n = function (t: (typeof i18n)['t']) { return function (key: string, ...replaces: any): string { if (replaces[0] === undefined || isObject(replaces[0])) { return t(key, ...replaces); diff --git a/apps/meteor/client/components/ActionManagerBusyState.tsx b/apps/meteor/client/components/ActionManagerBusyState.tsx new file mode 100644 index 000000000000..dcf82342917c --- /dev/null +++ b/apps/meteor/client/components/ActionManagerBusyState.tsx @@ -0,0 +1,37 @@ +import { Box } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { useEffect, useState } from 'react'; + +import { useUiKitActionManager } from '../hooks/useUiKitActionManager'; + +const ActionManagerBusyState = () => { + const t = useTranslation(); + const actionManager = useUiKitActionManager(); + const [busy, setBusy] = useState(false); + + useEffect(() => { + if (!actionManager) { + return; + } + + actionManager.on('busy', ({ busy }: { busy: boolean }) => setBusy(busy)); + + return () => { + actionManager.off('busy'); + }; + }, [actionManager]); + + if (busy) { + return ( + + + {t('Loading')} + + + ); + } + + return null; +}; + +export default ActionManagerBusyState; diff --git a/apps/meteor/client/components/GenericModal/GenericModal.tsx b/apps/meteor/client/components/GenericModal/GenericModal.tsx index 8b862130f052..8ae487642b92 100644 --- a/apps/meteor/client/components/GenericModal/GenericModal.tsx +++ b/apps/meteor/client/components/GenericModal/GenericModal.tsx @@ -1,4 +1,5 @@ import { Button, Modal } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import type { Keys as IconName } from '@rocket.chat/icons'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { FC, ComponentProps, ReactElement, ReactNode } from 'react'; @@ -73,14 +74,15 @@ const GenericModal: FC = ({ ...props }) => { const t = useTranslation(); + const genericModalId = useUniqueId(); return ( - + {renderIcon(icon, variant)} {tagline && {tagline}} - {title ?? t('Are_you_sure')} + {title ?? t('Are_you_sure')} diff --git a/apps/meteor/client/components/Page/Page.tsx b/apps/meteor/client/components/Page/Page.tsx index d3d9c072ab1e..59f0849e5333 100644 --- a/apps/meteor/client/components/Page/Page.tsx +++ b/apps/meteor/client/components/Page/Page.tsx @@ -5,7 +5,7 @@ import React, { useState } from 'react'; import PageContext from './PageContext'; type PageProps = Omit, 'backgroundColor'> & { - background?: 'light' | 'tint' | 'neutral'; + background?: 'light' | 'tint' | 'neutral' | 'room'; }; const Page = ({ background = 'light', ...props }: PageProps): ReactElement => { diff --git a/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple.tsx/UserAndRoomAutoCompleteMultiple.tsx b/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.tsx similarity index 100% rename from apps/meteor/client/components/UserAndRoomAutoCompleteMultiple.tsx/UserAndRoomAutoCompleteMultiple.tsx rename to apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/UserAndRoomAutoCompleteMultiple.tsx diff --git a/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple.tsx/index.ts b/apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/index.ts similarity index 100% rename from apps/meteor/client/components/UserAndRoomAutoCompleteMultiple.tsx/index.ts rename to apps/meteor/client/components/UserAndRoomAutoCompleteMultiple/index.ts diff --git a/apps/meteor/client/components/UserStatusMenu.tsx b/apps/meteor/client/components/UserStatusMenu.tsx index b5d317b444ac..8f2dd24d6017 100644 --- a/apps/meteor/client/components/UserStatusMenu.tsx +++ b/apps/meteor/client/components/UserStatusMenu.tsx @@ -87,6 +87,7 @@ const UserStatusMenu = ({ onKeyUp={handleKeyUp} onKeyDown={handleKeyDown} margin={margin} + aria-label={t('User_status_menu')} > diff --git a/apps/meteor/client/components/message/list/MessageListSkeleton.tsx b/apps/meteor/client/components/message/list/MessageListSkeleton.tsx index 7c780fae5946..5581f5ffc63d 100644 --- a/apps/meteor/client/components/message/list/MessageListSkeleton.tsx +++ b/apps/meteor/client/components/message/list/MessageListSkeleton.tsx @@ -10,11 +10,7 @@ type MessageListSkeletonProps = { const MessageListSkeleton = ({ messageCount = 2 }: MessageListSkeletonProps): ReactElement => { const widths = useMemo( - () => - Array.from( - { length: messageCount }, - () => `${availablePercentualWidths[Math.floor(Math.random() * availablePercentualWidths.length)]}%`, - ), + () => Array.from({ length: messageCount }, (_, index) => `${availablePercentualWidths[index % availablePercentualWidths.length]}%`), [messageCount], ); diff --git a/apps/meteor/client/components/message/toolbox/MessageActionMenu.tsx b/apps/meteor/client/components/message/toolbox/MessageActionMenu.tsx index b8417cfb6cd0..3d1796083112 100644 --- a/apps/meteor/client/components/message/toolbox/MessageActionMenu.tsx +++ b/apps/meteor/client/components/message/toolbox/MessageActionMenu.tsx @@ -1,41 +1,66 @@ -import { MessageToolboxItem, Option, OptionDivider, Box } from '@rocket.chat/fuselage'; +import { Box, MessageToolboxItem, Option, OptionDivider, OptionTitle } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import type { ComponentProps, UIEvent, ReactElement } from 'react'; -import React, { useState, Fragment, useRef } from 'react'; +import type { ComponentProps, MouseEvent, MouseEventHandler, ReactElement } from 'react'; +import React, { Fragment, useRef, useState } from 'react'; import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; import { useEmbeddedLayout } from '../../../hooks/useEmbeddedLayout'; import ToolboxDropdown from './ToolboxDropdown'; type MessageActionConfigOption = Omit & { - action: (event: UIEvent) => void; + action: ((event: MouseEvent) => void) & MouseEventHandler; }; type MessageActionMenuProps = { options: MessageActionConfigOption[]; }; +const getSectionOrder = (section: string): number => { + switch (section) { + case 'communication': + return 0; + case 'interaction': + return 1; + case 'duplication': + return 2; + case 'apps': + return 3; + case 'management': + return 4; + default: + return 5; + } +}; + const MessageActionMenu = ({ options, ...props }: MessageActionMenuProps): ReactElement => { const ref = useRef(null); - const t = useTranslation(); const [visible, setVisible] = useState(false); const isLayoutEmbedded = useEmbeddedLayout(); - const groupOptions = options - .map(({ color, ...option }) => ({ - ...option, - ...(color === 'alert' && { variant: 'danger' as const }), - })) - .reduce((acc, option) => { - const group = option.variant ? option.variant : ''; - acc[group] = acc[group] || []; - if (!(isLayoutEmbedded && option.id === 'reply-directly')) acc[group].push(option); + const groupOptions = options.reduce((acc, option) => { + const { type = '' } = option; + + if (option.color === 'alert') { + option.variant = 'danger' as const; + } + + const order = getSectionOrder(type); + const [sectionType, options] = acc[getSectionOrder(type)] ?? [type, []]; + + if (!(isLayoutEmbedded && option.id === 'reply-directly')) { + options.push(option); + } + + if (options.length === 0) { return acc; - }, {} as { [key: string]: MessageActionConfigOption[] }) as { - [key: string]: MessageActionConfigOption[]; - }; + } + + acc[order] = [sectionType, options]; + + return acc; + }, [] as unknown as [section: string, options: Array][]); return ( <> @@ -51,8 +76,9 @@ const MessageActionMenu = ({ options, ...props }: MessageActionMenuProps): React <> setVisible(!visible)} /> - {Object.entries(groupOptions).map(([, options], index, arr) => ( + {groupOptions.map(([section, options], index, arr) => ( + {section === 'apps' && Apps} {options.map((option) => (