diff --git a/.github/history.json b/.github/history.json index e7fbf7e15705..8279f3d801c9 100644 --- a/.github/history.json +++ b/.github/history.json @@ -97999,6 +97999,80 @@ "5.0" ], "pull_requests": [] + }, + "5.4.1": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "27557", + "title": "[FIX] Custom languages not being applied to i18next", + "userLogin": "ggazzo", + "milestone": "5.4.1", + "contributors": [ + "ggazzo", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "27558", + "title": "[FIX] Registration and Login placeholders not being used", + "userLogin": "ggazzo", + "milestone": "5.4.1", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "27475", + "title": "[FIX] Fix Login with Show default form disabled", + "userLogin": "ggazzo", + "milestone": "5.4.1", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "27328", + "title": "[FIX] Message Actions menu does not close upon choosing an action", + "userLogin": "filipemarins", + "milestone": "5.4.1", + "contributors": [ + "filipemarins", + "yash-rajpal", + "hugocostadev", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "27538", + "title": "Chore: Deprecate unused omnichannel API", + "userLogin": "murtaza98", + "milestone": "5.4.1", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "27432", + "title": "[FIX] Pagination not working on current chats", + "userLogin": "MartinSchoeler", + "milestone": "5.4.1", + "contributors": [ + "MartinSchoeler", + "aleksandernsilva", + "kodiakhq[bot]", + "web-flow" + ] + } + ] } }, "5.4.0-rc.0": { diff --git a/HISTORY.md b/HISTORY.md index d5b36d4b7222..11a73a86d906 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,4 +1,43 @@ +# 5.4.1 +`2022-12-16 · 5 🐛 · 1 🔍 · 7 👩‍💻👨‍💻` + +### Engine versions +- Node: `14.19.3` +- NPM: `6.14.17` +- MongoDB: `4.2, 4.4, 5.0` + +### 🐛 Bug fixes + + +- Custom languages not being applied to i18next ([#27557](https://github.com/RocketChat/Rocket.Chat/pull/27557)) + +- Fix Login with Show default form disabled ([#27475](https://github.com/RocketChat/Rocket.Chat/pull/27475)) + +- Message Actions menu does not close upon choosing an action ([#27328](https://github.com/RocketChat/Rocket.Chat/pull/27328)) + +- Pagination not working on current chats ([#27432](https://github.com/RocketChat/Rocket.Chat/pull/27432)) + +- Registration and Login placeholders not being used ([#27558](https://github.com/RocketChat/Rocket.Chat/pull/27558)) + +
+🔍 Minor changes + + +- Chore: Deprecate unused omnichannel API ([#27538](https://github.com/RocketChat/Rocket.Chat/pull/27538)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@MartinSchoeler](https://github.com/MartinSchoeler) +- [@aleksandernsilva](https://github.com/aleksandernsilva) +- [@filipemarins](https://github.com/filipemarins) +- [@ggazzo](https://github.com/ggazzo) +- [@hugocostadev](https://github.com/hugocostadev) +- [@murtaza98](https://github.com/murtaza98) +- [@yash-rajpal](https://github.com/yash-rajpal) + # 5.4.0 `2022-12-05 · 9 🎉 · 13 🚀 · 61 🐛 · 87 🔍 · 57 👩‍💻👨‍💻` @@ -392,11 +431,11 @@ ![Screenshot from 2021-01-28 03-47-19](https://user-images.githubusercontent.com/38764067/106062371-b0e97300-611c-11eb-97de-1ff4c668a4c8.png) ![Screenshot from 2021-01-28 03-47-55](https://user-images.githubusercontent.com/38764067/106062374-b21aa000-611c-11eb-83db-93a3d0858d46.png) -- User approval email doesn't display username and e-mail when user logs in with OAuth ([#27111](https://github.com/RocketChat/Rocket.Chat/pull/27111)) +- User approval email doesn't display username and e-mail when user logs in with OAuth ([#27111](https://github.com/RocketChat/Rocket.Chat/pull/27111) by [@carlosrodrigues94](https://github.com/carlosrodrigues94)) - User auto complete breaks on enter key press ([#27213](https://github.com/RocketChat/Rocket.Chat/pull/27213)) -- User email address does not change when provisioned by OAuth ([#27148](https://github.com/RocketChat/Rocket.Chat/pull/27148)) +- User email address does not change when provisioned by OAuth ([#27148](https://github.com/RocketChat/Rocket.Chat/pull/27148) by [@carlosrodrigues94](https://github.com/carlosrodrigues94)) - UserCard not opening inside Threads ([#27096](https://github.com/RocketChat/Rocket.Chat/pull/27096)) @@ -710,6 +749,7 @@ - [@TedsCabin](https://github.com/TedsCabin) - [@aKn1ghtOut](https://github.com/aKn1ghtOut) - [@aswinidev](https://github.com/aswinidev) +- [@carlosrodrigues94](https://github.com/carlosrodrigues94) - [@dependabot[bot]](https://github.com/dependabot[bot]) - [@edward-raven](https://github.com/edward-raven) - [@eltociear](https://github.com/eltociear) @@ -735,7 +775,6 @@ - [@MarcosSpessatto](https://github.com/MarcosSpessatto) - [@MartinSchoeler](https://github.com/MartinSchoeler) - [@aleksandernsilva](https://github.com/aleksandernsilva) -- [@carlosrodrigues94](https://github.com/carlosrodrigues94) - [@casalsgh](https://github.com/casalsgh) - [@cauefcr](https://github.com/cauefcr) - [@d-gubert](https://github.com/d-gubert) @@ -1814,7 +1853,7 @@ This PR fixes an issue that was happening when an invalid token was passed on SlackBridge, basically the app crashes because the error was not being handled -- Slash commands description as undefined ([#26372](https://github.com/RocketChat/Rocket.Chat/pull/26372)) +- Slash commands description as undefined ([#26372](https://github.com/RocketChat/Rocket.Chat/pull/26372) by [@carlosrodrigues94](https://github.com/carlosrodrigues94)) - SMS service check ([#26558](https://github.com/RocketChat/Rocket.Chat/pull/26558)) @@ -1850,7 +1889,7 @@ - Chore: Accounts/token to TS ([#26434](https://github.com/RocketChat/Rocket.Chat/pull/26434)) -- Chore: Add end-to-end tests to teams listing in the `directory` endpoint ([#26347](https://github.com/RocketChat/Rocket.Chat/pull/26347)) +- Chore: Add end-to-end tests to teams listing in the `directory` endpoint ([#26347](https://github.com/RocketChat/Rocket.Chat/pull/26347) by [@carlosrodrigues94](https://github.com/carlosrodrigues94)) - Chore: Add license env var to ee tests ([#26650](https://github.com/RocketChat/Rocket.Chat/pull/26650)) @@ -2172,6 +2211,7 @@ ### 👩‍💻👨‍💻 Contributors 😍 - [@Karting06](https://github.com/Karting06) +- [@carlosrodrigues94](https://github.com/carlosrodrigues94) - [@imyaman](https://github.com/imyaman) - [@pierreozoux](https://github.com/pierreozoux) - [@shrinish123](https://github.com/shrinish123) @@ -2185,7 +2225,6 @@ - [@MartinSchoeler](https://github.com/MartinSchoeler) - [@albuquerquefabio](https://github.com/albuquerquefabio) - [@aleksandernsilva](https://github.com/aleksandernsilva) -- [@carlosrodrigues94](https://github.com/carlosrodrigues94) - [@casalsgh](https://github.com/casalsgh) - [@cauefcr](https://github.com/cauefcr) - [@d-gubert](https://github.com/d-gubert) @@ -2632,7 +2671,7 @@ Demo gif: ![privacy-tab](https://user-images.githubusercontent.com/43561537/173878394-333057d4-3c7e-434e-a3ca-d3e08f33c7bc.gif) -- Matrix Federation UX improvements ([#25847](https://github.com/RocketChat/Rocket.Chat/pull/25847)) +- Matrix Federation UX improvements ([#25847](https://github.com/RocketChat/Rocket.Chat/pull/25847) by [@carlosrodrigues94](https://github.com/carlosrodrigues94)) - Message Template React Component ([#23971](https://github.com/RocketChat/Rocket.Chat/pull/23971)) @@ -2702,7 +2741,7 @@ - Add tooltip to sidebar room menu ([#24405](https://github.com/RocketChat/Rocket.Chat/pull/24405) by [@Himanshu664](https://github.com/Himanshu664)) -- add warnings for federation setup ([#25684](https://github.com/RocketChat/Rocket.Chat/pull/25684)) +- add warnings for federation setup ([#25684](https://github.com/RocketChat/Rocket.Chat/pull/25684) by [@carlosrodrigues94](https://github.com/carlosrodrigues94)) - Added MaxNickNameLength and MaxBioLength constants ([#25231](https://github.com/RocketChat/Rocket.Chat/pull/25231) by [@aakash-gitdev](https://github.com/aakash-gitdev)) @@ -3926,7 +3965,7 @@ - Regression: Federated users not showing as federated in Room Members ([#26249](https://github.com/RocketChat/Rocket.Chat/pull/26249)) -- Regression: fix `directory` endpoint not listing teams ([#26310](https://github.com/RocketChat/Rocket.Chat/pull/26310)) +- Regression: fix `directory` endpoint not listing teams ([#26310](https://github.com/RocketChat/Rocket.Chat/pull/26310) by [@carlosrodrigues94](https://github.com/carlosrodrigues94)) - Regression: Fix app icons breaking UI ([#26278](https://github.com/RocketChat/Rocket.Chat/pull/26278)) @@ -4081,7 +4120,7 @@ ### I'm definitely open for better looking alternatives. Please leave a comment if you have a better solution to share. -- Regression: Matrix Federation regressions ([#26283](https://github.com/RocketChat/Rocket.Chat/pull/26283)) +- Regression: Matrix Federation regressions ([#26283](https://github.com/RocketChat/Rocket.Chat/pull/26283) by [@carlosrodrigues94](https://github.com/carlosrodrigues94)) - Regression: Messages in new message template Crashing. ([#25327](https://github.com/RocketChat/Rocket.Chat/pull/25327)) @@ -4268,6 +4307,7 @@ - [@Sh0uld](https://github.com/Sh0uld) - [@aakash-gitdev](https://github.com/aakash-gitdev) - [@amolghode1981](https://github.com/amolghode1981) +- [@carlosrodrigues94](https://github.com/carlosrodrigues94) - [@cuonghuunguyen](https://github.com/cuonghuunguyen) - [@dependabot[bot]](https://github.com/dependabot[bot]) - [@divinespear](https://github.com/divinespear) @@ -4293,7 +4333,6 @@ - [@alansikora](https://github.com/alansikora) - [@albuquerquefabio](https://github.com/albuquerquefabio) - [@aleksandernsilva](https://github.com/aleksandernsilva) -- [@carlosrodrigues94](https://github.com/carlosrodrigues94) - [@cauefcr](https://github.com/cauefcr) - [@csuadev](https://github.com/csuadev) - [@d-gubert](https://github.com/d-gubert) @@ -4605,7 +4644,7 @@ - Add tooltip to sidebar room menu ([#24405](https://github.com/RocketChat/Rocket.Chat/pull/24405) by [@Himanshu664](https://github.com/Himanshu664)) -- add warnings for federation setup ([#25684](https://github.com/RocketChat/Rocket.Chat/pull/25684)) +- add warnings for federation setup ([#25684](https://github.com/RocketChat/Rocket.Chat/pull/25684) by [@carlosrodrigues94](https://github.com/carlosrodrigues94)) - Added MaxNickNameLength and MaxBioLength constants ([#25231](https://github.com/RocketChat/Rocket.Chat/pull/25231) by [@aakash-gitdev](https://github.com/aakash-gitdev)) @@ -5277,6 +5316,7 @@ - [@Himanshu664](https://github.com/Himanshu664) - [@aakash-gitdev](https://github.com/aakash-gitdev) - [@amolghode1981](https://github.com/amolghode1981) +- [@carlosrodrigues94](https://github.com/carlosrodrigues94) - [@cuonghuunguyen](https://github.com/cuonghuunguyen) - [@dependabot[bot]](https://github.com/dependabot[bot]) - [@divinespear](https://github.com/divinespear) @@ -5299,7 +5339,6 @@ - [@alansikora](https://github.com/alansikora) - [@albuquerquefabio](https://github.com/albuquerquefabio) - [@aleksandernsilva](https://github.com/aleksandernsilva) -- [@carlosrodrigues94](https://github.com/carlosrodrigues94) - [@cauefcr](https://github.com/cauefcr) - [@d-gubert](https://github.com/d-gubert) - [@debdutdeb](https://github.com/debdutdeb) diff --git a/_templates/service/new/.eslintrc.json.ejs.t b/_templates/service/new/.eslintrc.json.ejs.t new file mode 100644 index 000000000000..8700fafc2033 --- /dev/null +++ b/_templates/service/new/.eslintrc.json.ejs.t @@ -0,0 +1,7 @@ +--- +to: ee/apps/<%= name %>/.eslintrc.json +--- +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/_templates/service/new/package.json.ejs.t b/_templates/service/new/package.json.ejs.t new file mode 100644 index 000000000000..0b83e0d9b59b --- /dev/null +++ b/_templates/service/new/package.json.ejs.t @@ -0,0 +1,51 @@ +--- +to: ee/apps/<%= name %>/package.json +--- +{ + "name": "@rocket.chat/<%= name.toLowerCase() %>", + "private": true, + "version": "0.1.0", + "description": "Rocket.Chat service", + "scripts": { + "build": "tsc -p tsconfig.json", + "ms": "TRANSPORTER=${TRANSPORTER:-TCP} MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts", + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "eslint src", + "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" + }, + "keywords": [ + "rocketchat" + ], + "author": "Rocket.Chat", + "dependencies": { + "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/emitter": "0.31.22", + "@rocket.chat/model-typings": "workspace:^", + "@rocket.chat/models": "workspace:^", + "@rocket.chat/rest-typings": "workspace:^", + "@rocket.chat/string-helpers": "0.31.22", + "@types/node": "^14.18.21", + "ejson": "^2.2.2", + "eventemitter3": "^4.0.7", + "fibers": "^5.0.3", + "mem": "^8.1.1", + "moleculer": "^0.14.21", + "mongodb": "^4.12.1", + "nats": "^2.4.0", + "pino": "^8.4.2", + "polka": "^0.5.2" + }, + "devDependencies": { + "@rocket.chat/eslint-config": "workspace:^", + "@types/eslint": "^8.4.10", + "@types/polka": "^0.5.4", + "eslint": "^8.29.0", + "ts-node": "^10.9.1", + "typescript": "~4.5.5" + }, + "main": "./dist/ee/apps/<%= name %>/src/service.js", + "files": [ + "/dist" + ] +} + diff --git a/_templates/service/new/service.ejs.t b/_templates/service/new/service.ejs.t new file mode 100644 index 000000000000..546151f65ea6 --- /dev/null +++ b/_templates/service/new/service.ejs.t @@ -0,0 +1,43 @@ +--- +to: ee/apps/<%= name %>/src/service.ts +--- +import type { Document } from 'mongodb'; +import polka from 'polka'; + +import { api } from '../../../../apps/meteor/server/sdk/api'; +import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; +import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; +import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; + +const PORT = process.env.PORT || 3034; + +(async () => { + const db = await getConnection(); + + const trash = await getCollection(Collections.Trash); + + registerServiceModels(db, trash); + + api.setBroker(broker); + + // need to import service after models are registered + const { <%= h.changeCase.pascalCase(name) %> } = await import('./<%= h.changeCase.pascalCase(name) %>'); + + api.registerService(new <%= h.changeCase.pascalCase(name) %>()); + + await api.start(); + + polka() + .get('/health', async function (_req, res) { + try { + await api.nodeList(); + res.end('ok'); + } catch (err) { + console.error('Service not healthy', err); + + res.writeHead(500); + res.end('not healthy'); + } + }) + .listen(PORT); +})(); diff --git a/_templates/service/new/servicesClass.ejs.t b/_templates/service/new/servicesClass.ejs.t new file mode 100644 index 000000000000..951207486f8f --- /dev/null +++ b/_templates/service/new/servicesClass.ejs.t @@ -0,0 +1,16 @@ +--- +to: ee/apps/<%= name %>/src/<%= h.changeCase.pascalCase(name) %>.ts +--- +import { ServiceClass } from '../../../../apps/meteor/server/sdk/types/ServiceClass'; + +export class <%= h.changeCase.pascalCase(name) %> extends ServiceClass { + protected name = '<%= name %>'; + + constructor() { + super(); + + // your stuff + } + + // more stuff +} diff --git a/_templates/service/new/tsconfig.json.ejs.t b/_templates/service/new/tsconfig.json.ejs.t new file mode 100644 index 000000000000..a4632558dd8f --- /dev/null +++ b/_templates/service/new/tsconfig.json.ejs.t @@ -0,0 +1,34 @@ +--- +to: ee/apps/<%= name %>/tsconfig.json +--- +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "target": "es2018", + "lib": ["esnext", "dom"], + "allowJs": true, + "checkJs": false, + "incremental": true, + + /* Strict Type-Checking Options */ + "noImplicitAny": true, + "strictNullChecks": true, + "strictPropertyInitialization": false, + "strictFunctionTypes": false, + + /* Additional Checks */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": false, + "noFallthroughCasesInSwitch": false, + + /* Module Resolution Options */ + "outDir": "./dist", + "importsNotUsedAsValues": "preserve", + "declaration": false, + "declarationMap": false + }, + "files": ["./src/service.ts"], + "include": ["../../../apps/meteor/definition/externals/meteor"], + "exclude": ["./dist"] +} diff --git a/apps/meteor/app/api/server/v1/channels.js b/apps/meteor/app/api/server/v1/channels.js index 2b02a1f86079..6b5fb82a6e25 100644 --- a/apps/meteor/app/api/server/v1/channels.js +++ b/apps/meteor/app/api/server/v1/channels.js @@ -287,7 +287,7 @@ API.v1.addRoute( const ourQuery = Object.assign({}, query, { rid: findResult._id }); - const { cursor, totalCount } = Uploads.findPaginated(ourQuery, { + const { cursor, totalCount } = Uploads.findPaginatedWithoutThumbs(ourQuery, { sort: sort || { name: 1 }, skip: offset, limit: count, diff --git a/apps/meteor/app/api/server/v1/groups.js b/apps/meteor/app/api/server/v1/groups.js index 6593effe5b72..db2745df5872 100644 --- a/apps/meteor/app/api/server/v1/groups.js +++ b/apps/meteor/app/api/server/v1/groups.js @@ -346,7 +346,7 @@ API.v1.addRoute( const ourQuery = Object.assign({}, query, { rid: findResult.rid }); - const { cursor, totalCount } = Uploads.findPaginated(ourQuery, { + const { cursor, totalCount } = Uploads.findPaginatedWithoutThumbs(ourQuery, { sort: sort || { name: 1 }, skip: offset, limit: count, diff --git a/apps/meteor/app/api/server/v1/im.ts b/apps/meteor/app/api/server/v1/im.ts index eb66fefd6723..53683c0dbe86 100644 --- a/apps/meteor/app/api/server/v1/im.ts +++ b/apps/meteor/app/api/server/v1/im.ts @@ -1,7 +1,7 @@ /** * Docs: https://github.com/RocketChat/developer-docs/blob/master/reference/api/rest-api/endpoints/team-collaboration-endpoints/im-endpoints */ -import type { IMessage, IRoom, ISubscription, IUpload } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; import { isDmDeleteProps, isDmFileProps, @@ -23,9 +23,6 @@ import { createDirectMessage } from '../../../../server/methods/createDirectMess import { addUserToFileObj } from '../helpers/addUserToFileObj'; import { settings } from '../../../settings/server'; -interface IImFilesObject extends IUpload { - userId: string; -} // TODO: Refact or remove type findDirectMessageRoomProps = @@ -218,7 +215,7 @@ API.v1.addRoute( const ourQuery = query ? { rid: room._id, ...query } : { rid: room._id }; - const { cursor, totalCount } = Uploads.findPaginated(ourQuery, { + const { cursor, totalCount } = Uploads.findPaginatedWithoutThumbs(ourQuery, { sort: sort || { name: 1 }, skip: offset, limit: count, diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index e43cadac6ac2..af74049434f0 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -432,6 +432,7 @@ API.v1.addRoute( inclusiveFieldsKeys.includes('username') && 'username.*', inclusiveFieldsKeys.includes('name') && 'name.*', inclusiveFieldsKeys.includes('type') && 'type.*', + inclusiveFieldsKeys.includes('customFields') && 'customFields.*', ].filter(Boolean) as string[], this.queryOperations, ) diff --git a/apps/meteor/app/file-upload/server/config/GoogleStorage.js b/apps/meteor/app/file-upload/server/config/GoogleStorage.js index ce900d9811aa..acdda2bf508d 100644 --- a/apps/meteor/app/file-upload/server/config/GoogleStorage.js +++ b/apps/meteor/app/file-upload/server/config/GoogleStorage.js @@ -69,6 +69,7 @@ const GoogleCloudStorageUserDataFiles = new FileUploadClass({ const configure = _.debounce(function () { const bucket = settings.get('FileUpload_GoogleStorage_Bucket'); + const projectId = settings.get('FileUpload_GoogleStorage_ProjectId'); const accessId = settings.get('FileUpload_GoogleStorage_AccessId'); const secret = settings.get('FileUpload_GoogleStorage_Secret'); const URLExpiryTimeSpan = settings.get('FileUpload_S3_URLExpiryTimeSpan'); @@ -82,6 +83,7 @@ const configure = _.debounce(function () { credentials: { client_email: accessId, private_key: secret, + projectId, }, }, bucket, diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.js b/apps/meteor/app/file-upload/server/lib/FileUpload.js index d0ae8333fe64..14c47463dd4e 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.js +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.js @@ -327,6 +327,8 @@ export const FileUpload = { name: `thumb-${file.name}`, size: buffer.length, type: file.type, + originalFileId: file._id, + typeGroup: 'thumb', rid, userId, }; diff --git a/apps/meteor/app/file-upload/server/startup/settings.ts b/apps/meteor/app/file-upload/server/startup/settings.ts index f55887f25993..fe37730d0095 100644 --- a/apps/meteor/app/file-upload/server/startup/settings.ts +++ b/apps/meteor/app/file-upload/server/startup/settings.ts @@ -203,6 +203,18 @@ settingsRegistry.addGroup('FileUpload', function () { }, secret: true, }); + + this.add('FileUpload_GoogleStorage_ProjectId', '', { + type: 'string', + multiline: false, + private: true, + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'GoogleCloudStorage', + }, + secret: true, + }); + this.add('FileUpload_GoogleStorage_Proxy_Avatars', false, { type: 'boolean', enableQuery: { diff --git a/apps/meteor/app/lib/server/methods/deleteMessage.ts b/apps/meteor/app/lib/server/methods/deleteMessage.ts index 8ae4ef8248d5..8f96fd670ba6 100644 --- a/apps/meteor/app/lib/server/methods/deleteMessage.ts +++ b/apps/meteor/app/lib/server/methods/deleteMessage.ts @@ -28,6 +28,7 @@ Meteor.methods({ u: 1, rid: 1, file: 1, + files: 1, ts: 1, }, }); diff --git a/apps/meteor/app/livechat/imports/server/rest/inquiries.ts b/apps/meteor/app/livechat/imports/server/rest/inquiries.ts index 3248e6af4d61..b9c49671645d 100644 --- a/apps/meteor/app/livechat/imports/server/rest/inquiries.ts +++ b/apps/meteor/app/livechat/imports/server/rest/inquiries.ts @@ -12,6 +12,7 @@ import { LivechatInquiry } from '@rocket.chat/models'; import { API } from '../../../../api/server'; import { Users, LivechatDepartment } from '../../../../models/server'; import { findInquiries, findOneInquiryByRoomId } from '../../../server/api/lib/inquiries'; +import { deprecationWarning } from '../../../../api/server/helpers/deprecationWarning'; API.v1.addRoute( 'livechat/inquiries.list', @@ -81,15 +82,19 @@ API.v1.addRoute( const { department } = this.requestParams(); return API.v1.success( - await findInquiries({ - userId: this.userId, - department, - status: LivechatInquiryStatus.QUEUED, - pagination: { - offset, - count, - sort, - }, + deprecationWarning({ + endpoint: 'livechat/inquiries.queued', + versionWillBeRemoved: '6.0', + response: await findInquiries({ + userId: this.userId, + department, + status: LivechatInquiryStatus.QUEUED, + pagination: { + offset, + count, + sort, + }, + }), }), ); }, diff --git a/apps/meteor/app/models/client/index.ts b/apps/meteor/app/models/client/index.ts index fbcbee481f5c..641f0a00ba82 100644 --- a/apps/meteor/app/models/client/index.ts +++ b/apps/meteor/app/models/client/index.ts @@ -23,9 +23,13 @@ import { WebdavAccounts } from './models/WebdavAccounts'; import CustomSounds from './models/CustomSounds'; import EmojiCustom from './models/EmojiCustom'; +/** @deprecated */ const Users = _.extend({}, users, Meteor.users); +/** @deprecated */ const Subscriptions = _.extend({}, subscriptions, ChatSubscription); -const Messages = _.extend({}, ChatMessage); +/** @deprecated */ +const Messages = _.extend({}, ChatMessage) as typeof ChatMessage; +/** @deprecated */ const Rooms = _.extend({}, ChatRoom); export { diff --git a/apps/meteor/app/models/server/models/LivechatInquiry.ts b/apps/meteor/app/models/server/models/LivechatInquiry.ts index a7d52cb97bdf..901b93e4cc80 100644 --- a/apps/meteor/app/models/server/models/LivechatInquiry.ts +++ b/apps/meteor/app/models/server/models/LivechatInquiry.ts @@ -1,6 +1,7 @@ import type { ILivechatInquiryRecord } from '@rocket.chat/core-typings'; import type { FindOptions, FindCursor, UpdateResult, DeleteResult } from 'mongodb'; +import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; import { Base } from './_Base'; export class LivechatInquiry extends Base { @@ -244,7 +245,7 @@ export class LivechatInquiry extends Base { aggregate.push({ $match: { _id } }); } - return collectionObj.aggregate(aggregate).toArray(); + return collectionObj.aggregate(aggregate, { readPreference: readSecondaryPreferred() }).toArray(); } removeDefaultAgentById(inquiryId: string): UpdateResult { diff --git a/apps/meteor/app/models/server/models/LivechatRooms.js b/apps/meteor/app/models/server/models/LivechatRooms.js index 9759b85b1488..98c21251910b 100644 --- a/apps/meteor/app/models/server/models/LivechatRooms.js +++ b/apps/meteor/app/models/server/models/LivechatRooms.js @@ -4,6 +4,7 @@ import { Settings } from '@rocket.chat/models'; import { Base } from './_Base'; import Rooms from './Rooms'; +import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; export class LivechatRooms extends Base { constructor(...args) { @@ -494,152 +495,158 @@ export class LivechatRooms extends Base { } getAnalyticsMetricsBetweenDateWithMessages(t, date, { departmentId } = {}, extraQuery) { - return this.model.rawCollection().aggregate([ - { - $match: { - t, - ts: { - $gte: new Date(date.gte), // ISO Date, ts >= date.gte - $lt: new Date(date.lt), // ISODate, ts < date.lt + return this.model.rawCollection().aggregate( + [ + { + $match: { + t, + ts: { + $gte: new Date(date.gte), // ISO Date, ts >= date.gte + $lt: new Date(date.lt), // ISODate, ts < date.lt + }, + ...(departmentId && departmentId !== 'undefined' && { departmentId }), }, - ...(departmentId && departmentId !== 'undefined' && { departmentId }), }, - }, - { $addFields: { roomId: '$_id' } }, - { - $lookup: { - from: 'rocketchat_message', - // mongo doesn't like _id as variable name here :( - let: { roomId: '$roomId' }, - pipeline: [ - { - $match: { - $expr: { - $and: [ - { - $eq: ['$$roomId', '$rid'], - }, - { - // this is similar to do { $exists: false } - $lte: ['$t', null], - }, - ...(extraQuery ? [extraQuery] : []), - ], + { $addFields: { roomId: '$_id' } }, + { + $lookup: { + from: 'rocketchat_message', + // mongo doesn't like _id as variable name here :( + let: { roomId: '$roomId' }, + pipeline: [ + { + $match: { + $expr: { + $and: [ + { + $eq: ['$$roomId', '$rid'], + }, + { + // this is similar to do { $exists: false } + $lte: ['$t', null], + }, + ...(extraQuery ? [extraQuery] : []), + ], + }, }, }, - }, - ], - as: 'messages', - }, - }, - { - $unwind: { - path: '$messages', - preserveNullAndEmptyArrays: true, + ], + as: 'messages', + }, }, - }, - { - $group: { - _id: { - _id: '$_id', - ts: '$ts', - departmentId: '$departmentId', - open: '$open', - servedBy: '$servedBy', - metrics: '$metrics', + { + $unwind: { + path: '$messages', + preserveNullAndEmptyArrays: true, }, - messagesCount: { - $sum: 1, + }, + { + $group: { + _id: { + _id: '$_id', + ts: '$ts', + departmentId: '$departmentId', + open: '$open', + servedBy: '$servedBy', + metrics: '$metrics', + }, + messagesCount: { + $sum: 1, + }, }, }, - }, - { - $project: { - _id: '$_id._id', - ts: '$_id.ts', - departmentId: '$_id.departmentId', - open: '$_id.open', - servedBy: '$_id.servedBy', - metrics: '$_id.metrics', - msgs: '$messagesCount', + { + $project: { + _id: '$_id._id', + ts: '$_id.ts', + departmentId: '$_id.departmentId', + open: '$_id.open', + servedBy: '$_id.servedBy', + metrics: '$_id.metrics', + msgs: '$messagesCount', + }, }, - }, - ]); + ], + { readPreference: readSecondaryPreferred() }, + ); } getAnalyticsBetweenDate(date, { departmentId } = {}) { - return this.model.rawCollection().aggregate([ - { - $match: { - t: 'l', - ts: { - $gte: new Date(date.gte), // ISO Date, ts >= date.gte - $lt: new Date(date.lt), // ISODate, ts < date.lt + return this.model.rawCollection().aggregate( + [ + { + $match: { + t: 'l', + ts: { + $gte: new Date(date.gte), // ISO Date, ts >= date.gte + $lt: new Date(date.lt), // ISODate, ts < date.lt + }, + ...(departmentId && departmentId !== 'undefined' && { departmentId }), }, - ...(departmentId && departmentId !== 'undefined' && { departmentId }), }, - }, - { $addFields: { roomId: '$_id' } }, - { - $lookup: { - from: 'rocketchat_message', - // mongo doesn't like _id as variable name here :( - let: { roomId: '$roomId' }, - pipeline: [ - { - $match: { - $expr: { - $and: [ - { - $eq: ['$$roomId', '$rid'], - }, - { - // this is similar to do { $exists: false } - $lte: ['$t', null], - }, - ], + { $addFields: { roomId: '$_id' } }, + { + $lookup: { + from: 'rocketchat_message', + // mongo doesn't like _id as variable name here :( + let: { roomId: '$roomId' }, + pipeline: [ + { + $match: { + $expr: { + $and: [ + { + $eq: ['$$roomId', '$rid'], + }, + { + // this is similar to do { $exists: false } + $lte: ['$t', null], + }, + ], + }, }, }, - }, - ], - as: 'messages', - }, - }, - { - $unwind: { - path: '$messages', - preserveNullAndEmptyArrays: true, + ], + as: 'messages', + }, }, - }, - { - $group: { - _id: { - _id: '$_id', - ts: '$ts', - departmentId: '$departmentId', - open: '$open', - servedBy: '$servedBy', - metrics: '$metrics', - onHold: '$onHold', + { + $unwind: { + path: '$messages', + preserveNullAndEmptyArrays: true, }, - messagesCount: { - $sum: 1, + }, + { + $group: { + _id: { + _id: '$_id', + ts: '$ts', + departmentId: '$departmentId', + open: '$open', + servedBy: '$servedBy', + metrics: '$metrics', + onHold: '$onHold', + }, + messagesCount: { + $sum: 1, + }, }, }, - }, - { - $project: { - _id: '$_id._id', - ts: '$_id.ts', - departmentId: '$_id.departmentId', - open: '$_id.open', - servedBy: '$_id.servedBy', - metrics: '$_id.metrics', - msgs: '$messagesCount', - onHold: '$_id.onHold', + { + $project: { + _id: '$_id._id', + ts: '$_id.ts', + departmentId: '$_id.departmentId', + open: '$_id.open', + servedBy: '$_id.servedBy', + metrics: '$_id.metrics', + msgs: '$messagesCount', + onHold: '$_id.onHold', + }, }, - }, - ]); + ], + { readPreference: readSecondaryPreferred() }, + ); } closeByRoomId(roomId, closeInfo) { 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 f1295db53604..5704c55eb617 100644 --- a/apps/meteor/app/theme/client/imports/general/base_old.css +++ b/apps/meteor/app/theme/client/imports/general/base_old.css @@ -2220,46 +2220,6 @@ background-color: var(--selection-background); } -.rc-old .dropzone { - & .dropzone-overlay { - position: absolute; - z-index: 1000000; - top: 0; - right: 0; - bottom: 0; - left: 0; - - display: none; - - animation-name: zoomIn; - animation-duration: 0.1s; - - text-align: center; - - background: rgba(255, 255, 255, 0.8); - - font-size: 42px; - align-items: center; - justify-content: center; - - &--enabled { - color: var(--color-blue); - - border: 4px dashed var(--color-blue); - } - - &--disabled { - color: var(--color-red); - - border: 4px dashed var(--color-red); - } - } - - &.over .dropzone-overlay { - display: flex; - } -} - @keyframes zoomIn { 0% { transform: scale3d(0.9, 0.9, 0.9); @@ -2650,78 +2610,3 @@ height: 100px; } } - -.room-leader .chat-now { - position: absolute; - right: 25px; - - height: 30px; - padding: 4px; - - cursor: pointer; - text-align: center; - text-decoration: none; - - color: #555555; - border: 1px solid var(--color-gray-light); - border-radius: var(--border-radius); - - font-family: arial; - font-size: 14px; -} - -.room-leader a.chat-now:hover { - color: #555555; -} - -.room-leader { - position: absolute; - z-index: 9; - right: 0; - left: 0; - - display: flex; - - visibility: visible; - flex-direction: column; - - height: 57px; - padding-bottom: 8px; - - transition: transform 0.15s cubic-bezier(0.5, 0, 0.1, 1), visibility 0.15s cubic-bezier(0.5, 0, 0.1, 1); - - border-bottom: 1px solid var(--color-gray-lightest); - justify-content: center; - - &.message:hover { - background-color: #ffffff; - } - - &.animated-hidden { - visibility: hidden; - - transform: translateY(-100%); - } - - & .leader-name { - font-size: 18px; - } - - & .leader-status { - & .status-text { - padding-left: 15px; - - font-size: 14px; - } - - & .color-ball { - position: absolute; - - width: 10px; - height: 10px; - margin-top: 5px; - - border-radius: 5px; - } - } -} diff --git a/apps/meteor/app/threads/client/flextab/dropzone.ts b/apps/meteor/app/threads/client/flextab/dropzone.ts deleted file mode 100644 index eb9477c09cab..000000000000 --- a/apps/meteor/app/threads/client/flextab/dropzone.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import moment from 'moment'; -import _ from 'underscore'; - -import { Users } from '../../../models/client'; -import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; -import { settings } from '../../../settings/client'; -import type { ThreadTemplateInstance } from './thread'; - -const userCanDrop = (rid: IRoom['_id']) => - !roomCoordinator.readOnly(rid, Users.findOne({ _id: Meteor.userId() }, { fields: { username: 1 } })); - -async function createFileFromUrl(url: string): Promise { - let response; - try { - response = await fetch(url); - } catch (error) { - throw error; - } - - const data = await response.blob(); - const metadata = { - type: data.type, - }; - const { mime } = await import('../../../utils/lib/mimeTypes'); - const file = new File( - [data], - `File - ${moment().format(settings.get('Message_TimeAndDateFormat'))}.${mime.extension(data.type)}`, - metadata, - ); - return file; -} - -export const dropzoneHelpers = { - dragAndDrop(): string | undefined { - return settings.get('FileUpload_Enabled') ? 'dropzone--disabled' : undefined; - }, - - isDropzoneDisabled(): string { - return settings.get('FileUpload_Enabled') ? 'dropzone-overlay--enabled' : 'dropzone-overlay--disabled'; - }, - - dragAndDropLabel(this: ThreadTemplateInstance['data']): string { - if (!userCanDrop(this.rid)) { - return 'error-not-allowed'; - } - - if (!settings.get('FileUpload_Enabled')) { - return 'FileUpload_Disabled'; - } - - return 'Drop_to_upload_file'; - }, -}; - -export const dropzoneEvents = { - 'dragenter .dropzone'(this: ThreadTemplateInstance['data'], e: JQuery.DragEnterEvent) { - const types = e.originalEvent?.dataTransfer?.types; - - if ( - types && - types.length > 0 && - _.some(types, (type) => type.indexOf('text/') === -1 || type.indexOf('text/uri-list') !== -1 || type.indexOf('text/plain') !== -1) && - userCanDrop(this.rid) - ) { - e.currentTarget.classList.add('over'); - } - e.stopPropagation(); - }, - - 'dragleave .dropzone-overlay'(e: JQuery.DragLeaveEvent) { - e.currentTarget.parentNode.classList.remove('over'); - e.stopPropagation(); - }, - - 'dragover .dropzone-overlay'(event: JQuery.DragOverEvent) { - document.querySelectorAll('.over.dropzone').forEach((dropzone) => { - if (dropzone !== event.currentTarget.parentNode) { - dropzone.classList.remove('over'); - } - }); - - if (event.originalEvent?.dataTransfer) { - if (['move', 'linkMove'].includes(event.originalEvent.dataTransfer.effectAllowed)) { - event.originalEvent.dataTransfer.dropEffect = 'move'; - } else { - event.originalEvent.dataTransfer.dropEffect = 'copy'; - } - } - - event.stopPropagation(); - }, - - async 'dropped .dropzone-overlay'(this: ThreadTemplateInstance['data'], event: JQuery.DropEvent, instance: ThreadTemplateInstance) { - event.currentTarget.parentNode.classList.remove('over'); - - event.stopPropagation(); - event.preventDefault(); - - if (!userCanDrop(this.rid) || !settings.get('FileUpload_Enabled')) { - return false; - } - - const dataTransfer = event.originalEvent?.dataTransfer; - - if (!dataTransfer) { - return; - } - - let files = Array.from(dataTransfer.files ?? []); - - if (files.length < 1) { - const transferData = dataTransfer.getData('text') ?? dataTransfer.getData('url'); - - if (dataTransfer.types.includes('text/uri-list')) { - const url = dataTransfer.getData('text/html').match(''); - const imgURL = url?.[1]; - - if (!imgURL) { - return; - } - - const file = await createFileFromUrl(imgURL); - if (typeof file === 'string') { - instance.onTextDrop?.(file); - return; - } - files = [file]; - } - if (dataTransfer.types.includes('text/plain') && !dataTransfer.types.includes('text/x-moz-url')) { - instance.onTextDrop?.(transferData.trim()); - return; - } - } - const { mime } = await import('../../../utils/lib/mimeTypes'); - - instance.onFileDrop?.( - Array.from(files).map((file) => { - Object.defineProperty(file, 'type', { value: mime.lookup(file.name) }); - return file; - }), - ); - }, -}; diff --git a/apps/meteor/app/threads/client/flextab/messageBoxFollow.html b/apps/meteor/app/threads/client/flextab/messageBoxFollow.html deleted file mode 100644 index c0dd5b9c6a71..000000000000 --- a/apps/meteor/app/threads/client/flextab/messageBoxFollow.html +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/apps/meteor/app/threads/client/flextab/messageBoxFollow.ts b/apps/meteor/app/threads/client/flextab/messageBoxFollow.ts deleted file mode 100644 index 1f48106b1504..000000000000 --- a/apps/meteor/app/threads/client/flextab/messageBoxFollow.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Template } from 'meteor/templating'; - -import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling'; -import './messageBoxFollow.html'; - -Template.messageBoxFollow.events({ - 'click .js-follow'(this: { tmid: string }) { - const { tmid } = this; - callWithErrorHandling('followMessage', { mid: tmid }); - }, -}); diff --git a/apps/meteor/app/threads/client/flextab/thread.html b/apps/meteor/app/threads/client/flextab/thread.html deleted file mode 100644 index cfb5e31d2ba4..000000000000 --- a/apps/meteor/app/threads/client/flextab/thread.html +++ /dev/null @@ -1,32 +0,0 @@ - diff --git a/apps/meteor/app/threads/client/flextab/thread.ts b/apps/meteor/app/threads/client/flextab/thread.ts deleted file mode 100644 index c420fe494501..000000000000 --- a/apps/meteor/app/threads/client/flextab/thread.ts +++ /dev/null @@ -1,416 +0,0 @@ -import _ from 'underscore'; -import { Meteor } from 'meteor/meteor'; -import { Mongo } from 'meteor/mongo'; -import { Template } from 'meteor/templating'; -import { Session } from 'meteor/session'; -import { ReactiveDict } from 'meteor/reactive-dict'; -import { Tracker } from 'meteor/tracker'; -import { FlowRouter } from 'meteor/kadira:flow-router'; -import type { IMessage, IEditedMessage, ISubscription, IRoom } from '@rocket.chat/core-typings'; -import type { ContextType } from 'react'; - -import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling'; -import { messageContext } from '../../../ui-utils/client/lib/messageContext'; -import { upsertMessageBulk } from '../../../ui-utils/client/lib/RoomHistoryManager'; -import { Messages } from '../../../models/client'; -import { dropzoneEvents, dropzoneHelpers } from './dropzone'; -import { getUserPreference } from '../../../utils/client'; -import { settings } from '../../../settings/client'; -import { callbacks } from '../../../../lib/callbacks'; -import { getCommonRoomEvents } from '../../../ui/client/views/app/lib/getCommonRoomEvents'; -import './thread.html'; -import type { MessageBoxTemplateInstance } from '../../../ui-message/client/messageBox/messageBox'; -import type { MessageContext } from '../../../../client/views/room/contexts/MessageContext'; -import type { ChatContext } from '../../../../client/views/room/contexts/ChatContext'; -import type MessageHighlightContext from '../../../../client/views/room/MessageList/contexts/MessageHighlightContext'; - -export type ThreadTemplateInstance = Blaze.TemplateInstance<{ - mainMessage: IMessage; - subscription: ISubscription; - jump: unknown; - following: boolean; - rid: IRoom['_id']; - tabBar: { - openRoomInfo: (username: string) => void; - }; - chatContext: ContextType; - messageContext: ContextType; - messageHighlightContext: () => ContextType; -}> & { - firstNode: HTMLElement; - wrapper?: HTMLElement; - Threads: Mongo.Collection, IMessage> & { - direct: Mongo.Collection, IMessage>; - queries: unknown[]; - }; - threadsObserve?: Meteor.LiveQueryHandle; - callbackRemove?: () => void; - state: ReactiveDict<{ - rid: string; - tmid?: string; - loading?: boolean; - sendToChannel: boolean; - jump?: string | null; - editingMID?: IMessage['_id']; - }>; - closeThread: () => void; - loadMore: () => Promise; - atBottom?: boolean; - sendToBottom: () => void; - sendToBottomIfNecessary: () => void; - onFileDrop: (files: File[]) => void; - onTextDrop: (text: string) => void; - lastJump?: string; -}; - -const sort = { ts: 1 }; - -Template.thread.events({ - ...dropzoneEvents, - ...getCommonRoomEvents(), - 'click .js-close'(e: JQuery.ClickEvent) { - e.preventDefault(); - e.stopPropagation(); - const { close } = this; - return close?.(); - }, - 'scroll .js-scroll-thread': _.throttle(({ currentTarget: e }: JQuery.ScrollEvent, i: ThreadTemplateInstance) => { - i.atBottom = e.scrollTop >= e.scrollHeight - e.clientHeight; - }, 150), - 'click .toggle-hidden'(e: JQuery.ClickEvent) { - const id = e.currentTarget.dataset.message; - document.querySelector(`#thread-${id}`)?.classList.toggle('message--ignored'); - }, -}); - -Template.thread.helpers({ - ...dropzoneHelpers, - mainMessage() { - const { Threads, state } = Template.instance() as ThreadTemplateInstance; - const tmid = state.get('tmid'); - return Threads.findOne({ _id: tmid }); - }, - isLoading() { - return (Template.instance() as ThreadTemplateInstance).state.get('loading') !== false; - }, - messages() { - const { Threads, state } = Template.instance() as ThreadTemplateInstance; - const tmid = state.get('tmid'); - - return Threads.find({ tmid, _id: { $ne: tmid } }, { sort }); - }, - customClass(msg: IMessage) { - const { state } = Template.instance() as ThreadTemplateInstance; - return msg._id === state.get('editingMID') ? 'editing' : ''; - }, - customClassMain() { - const { state } = Template.instance() as ThreadTemplateInstance; - return ['thread-main', state.get('tmid') === state.get('editingMID') ? 'editing' : ''].filter(Boolean).join(' '); - }, - _messageContext(this: { mainMessage: IMessage }) { - const result = messageContext.call(this, { rid: this.mainMessage.rid }); - return { - ...result, - settings: { - ...result.settings, - showReplyButton: false, - showreply: false, - }, - }; - }, - messageBoxData(): MessageBoxTemplateInstance['data'] { - const instance = Template.instance() as ThreadTemplateInstance; - const { - mainMessage: { rid, _id: tmid }, - subscription, - chatContext, - } = Template.currentData() as ThreadTemplateInstance['data']; - - if (!chatContext) { - throw new Error('chatContext is not defined'); - } - - const showFormattingTips = settings.get('Message_ShowFormattingTips'); - const alsoSendPreferenceState = getUserPreference(Meteor.userId(), 'alsoSendThreadToChannel'); - - return { - chatContext, - showFormattingTips, - tshow: instance.state.get('sendToChannel'), - subscription, - rid, - tmid, - onSend: async ( - _event: Event, - { - value: text, - tshow, - }: { - value: string; - tshow?: boolean; - }, - ) => { - instance.sendToBottom(); - if (alsoSendPreferenceState === 'default') { - instance.state.set('sendToChannel', false); - } - - await chatContext.flows.sendMessage({ - text, - tshow, - }); - }, - onEscape: () => { - instance.closeThread(); - }, - onNavigateToPreviousMessage: () => chatContext.messageEditing.toPreviousMessage(), - onNavigateToNextMessage: () => chatContext.messageEditing.toNextMessage(), - onUploadFiles: (files: readonly File[]) => { - return chatContext.flows.uploadFiles(files); - }, - }; - }, - hideUsername() { - return getUserPreference(Meteor.userId(), 'hideUsernames') ? 'hide-usernames' : undefined; - }, - checkboxData() { - const instance = Template.instance() as ThreadTemplateInstance; - const checked = instance.state.get('sendToChannel'); - return { - id: 'sendAlso', - checked, - onChange: () => instance.state.set('sendToChannel', !checked), - }; - }, - // TODO: remove this - chatContext() { - const { chatContext } = (Template.instance() as ThreadTemplateInstance).data; - return () => chatContext; - }, - // TODO: remove this - messageContext() { - const { messageContext } = (Template.instance() as ThreadTemplateInstance).data; - return () => messageContext; - }, -}); - -Template.thread.onCreated(async function (this: ThreadTemplateInstance) { - this.Threads = new Mongo.Collection(null) as Mongo.Collection, IMessage> & { - direct: Mongo.Collection, IMessage>; - queries: unknown[]; - }; - - const preferenceState = getUserPreference(Meteor.userId(), 'alsoSendThreadToChannel'); - - let sendToChannel; - switch (preferenceState) { - case 'always': - sendToChannel = true; - break; - case 'never': - sendToChannel = false; - break; - default: - sendToChannel = !this.data.mainMessage.tcount; - } - - const { mainMessage } = Template.currentData(); - - this.state = new ReactiveDict(undefined, { - sendToChannel, - tmid: mainMessage._id, - rid: mainMessage.rid, - }); - - this.loadMore = async () => { - const { tmid } = Tracker.nonreactive(() => this.state.all()); - if (!tmid) { - return; - } - - this.state.set('loading', true); - - const messages = await callWithErrorHandling('getThreadMessages', { tmid }); - - upsertMessageBulk({ msgs: messages }, this.Threads); - upsertMessageBulk({ msgs: messages }, Messages); - - Tracker.afterFlush(() => { - this.state.set('loading', false); - }); - }; - - this.closeThread = () => { - const { - route, - params: { context, tab, ...params }, - } = FlowRouter.current(); - if (!route || !route.name) { - throw new Error('FlowRouter.current().route.name is undefined'); - } - FlowRouter.go(route.name, params); - }; -}); - -Template.thread.onRendered(function (this: ThreadTemplateInstance) { - const rid = Tracker.nonreactive(() => this.state.get('rid')); - if (!rid) { - throw new Error('No rid found'); - } - - this.atBottom = true; - this.wrapper = this.find('.js-scroll-thread'); - - this.sendToBottom = _.throttle(() => { - this.atBottom = true; - if (this.wrapper) { - this.wrapper.scrollTop = this.wrapper.scrollHeight; - } - }, 300); - - this.sendToBottomIfNecessary = () => { - this.atBottom && this.sendToBottom(); - }; - - const list = this.firstNode.querySelector('.js-scroll-thread ul'); - - if (!list) { - throw new Error('Could not find list element'); - } - - const observer = new ResizeObserver(this.sendToBottomIfNecessary); - observer.observe(list); - - this.onTextDrop = (droppedText: string) => { - const composer = this.data.chatContext?.composer; - - if (!composer) { - return; - } - - const { text, selection } = composer; - - const initText = text.slice(0, selection.start ?? undefined); - const finalText = text.slice(selection.end ?? undefined, text.length); - - composer.setText(initText + droppedText + finalText); - }; - - this.onFileDrop = (files) => { - const rid = this.state.get('rid'); - - if (!rid) { - throw new Error('No rid found'); - } - - this.data.chatContext?.flows.uploadFiles(files); - }; - - this.autorun(() => { - const rid = this.state.get('rid'); - const tmid = this.state.get('tmid'); - if (!rid) { - return; - } - this.callbackRemove?.(); - - this.callbackRemove = () => callbacks.remove('streamNewMessage', `thread-${rid}`); - - callbacks.add( - 'streamNewMessage', - _.debounce((msg: IEditedMessage) => { - if (Session.get('openedRoom') !== msg.rid || rid !== msg.rid || msg.editedAt || msg.tmid !== tmid) { - return; - } - Meteor.call('readThreads', tmid); - }, 1000), - callbacks.priority.MEDIUM, - `thread-${rid}`, - ); - }); - - this.autorun(() => { - const tmid = this.state.get('tmid'); - this.threadsObserve?.stop(); - - this.threadsObserve = Messages.find( - { $or: [{ tmid }, { _id: tmid }], _hidden: { $ne: true } }, - { - fields: { - collapsed: 0, - threadMsg: 0, - repliesCount: 0, - }, - }, - ).observe({ - added: ({ _id, ...message }: IMessage) => { - this.Threads.upsert({ _id }, message); - }, - changed: ({ _id, ...message }: IMessage) => { - this.Threads.update({ _id }, message); - }, - removed: ({ _id }: IMessage) => this.Threads.remove(_id), - }); - - this.loadMore(); - }); - - this.autorun(() => { - FlowRouter.watchPathChange(); - const jump = FlowRouter.getQueryParam('jump'); - const { mainMessage } = Template.currentData(); - this.state.set({ - tmid: mainMessage._id, - rid: mainMessage.rid, - jump, - }); - }); - - this.autorun(() => { - const jump = this.state.get('jump'); - const loading = this.state.get('loading'); - - if (jump && this.lastJump !== jump && loading === false) { - this.lastJump = jump; - this.find('.js-scroll-thread').style.scrollBehavior = 'smooth'; - this.state.set('jump', null); - Tracker.afterFlush(() => { - const message = this.find(`#thread-${jump}`); - message.classList.add('highlight'); - const removeClass = () => { - message.classList.remove('highlight'); - message.removeEventListener('animationend', removeClass); - }; - message.addEventListener('animationend', removeClass); - setTimeout(() => { - message.scrollIntoView(); - }, 300); - }); - } - }); - - this.autorun(() => { - const { Threads, state } = Template.instance() as ThreadTemplateInstance; - const tmid = state.get('tmid'); - const threads = Threads.findOne({ $or: [{ tmid }, { _id: tmid }] }); - const isLoading = state.get('loading'); - - if (!isLoading && !threads) { - this.closeThread(); - } - }); - - this.autorun(() => { - const { messageHighlightContext } = Template.currentData() as ThreadTemplateInstance['data']; - - this.state.set('editingMID', messageHighlightContext()?.highlightMessageId); - }); -}); - -Template.thread.onDestroyed(function (this: ThreadTemplateInstance) { - const { Threads, threadsObserve, callbackRemove } = this; - Threads.remove({}); - threadsObserve?.stop(); - - callbackRemove?.(); -}); diff --git a/apps/meteor/app/threads/client/flextab/threads.ts b/apps/meteor/app/threads/client/flextab/threads.ts deleted file mode 100644 index 19a5c0fc47b9..000000000000 --- a/apps/meteor/app/threads/client/flextab/threads.ts +++ /dev/null @@ -1 +0,0 @@ -import '../threads.css'; diff --git a/apps/meteor/app/threads/client/index.ts b/apps/meteor/app/threads/client/index.ts index 9ed8c6900727..4dad7ebd4846 100644 --- a/apps/meteor/app/threads/client/index.ts +++ b/apps/meteor/app/threads/client/index.ts @@ -1,7 +1,5 @@ import './flextab/threadlist'; -import './flextab/thread.ts'; -import './flextab/messageBoxFollow.ts'; -import './flextab/threads.ts'; import './messageAction/follow'; import './messageAction/unfollow'; import './messageAction/replyInThread'; +import './threads.css'; diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBox.ts b/apps/meteor/app/ui-message/client/messageBox/messageBox.ts index 6453dfd564d7..1fc8a3f7575b 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBox.ts +++ b/apps/meteor/app/ui-message/client/messageBox/messageBox.ts @@ -209,13 +209,12 @@ Template.messageBox.onCreated(function (this: MessageBoxTemplateInstance) { this.isSendIconVisible = new ReactiveVar(false); this.set = (value) => { - const { input } = this; - if (!input) { + if (!this.input) { return; } - input.value = value; - $(input).trigger('change').trigger('input'); + this.input.value = value; + $(this.input).trigger('change').trigger('input'); }; this.insertNewLine = () => { @@ -315,8 +314,10 @@ Template.messageBox.onRendered(function (this: MessageBoxTemplateInstance) { this.input = input; if (chatContext) { - const storageID = `${rid}${tmid ? `-${tmid}` : ''}`; - chatContext.setComposerAPI(createComposerAPI(input, storageID)); + const storageID = `messagebox_${rid}${tmid ? `-${tmid}` : ''}`; + const composer = createComposerAPI(input, storageID); + chatContext.setComposerAPI(composer); + this.set = composer.setText; } setTimeout(() => { @@ -351,7 +352,7 @@ Template.messageBox.onRendered(function (this: MessageBoxTemplateInstance) { } const shadow = this.find('.js-input-message-shadow'); - this.autogrow = onResize ? setupAutogrow(input, shadow, onResize) : null; + this.autogrow = setupAutogrow(input, shadow, onResize); }); }); }); diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxAutogrow.ts b/apps/meteor/app/ui-message/client/messageBox/messageBoxAutogrow.ts index 192c92933f56..c8194f48abcd 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxAutogrow.ts +++ b/apps/meteor/app/ui-message/client/messageBox/messageBoxAutogrow.ts @@ -2,7 +2,7 @@ import _ from 'underscore'; const replaceWhitespaces = (whitespaces: string) => `${' '.repeat(whitespaces.length - 1)} `; -export const setupAutogrow = (textarea: HTMLTextAreaElement, shadow: HTMLElement, callback: () => void) => { +export const setupAutogrow = (textarea: HTMLTextAreaElement, shadow: HTMLElement, callback: (() => void) | undefined) => { const width = textarea.clientWidth; const height = textarea.clientHeight; const { font, lineHeight, maxHeight: maxHeightPx } = window.getComputedStyle(textarea); diff --git a/apps/meteor/app/ui-message/index.js b/apps/meteor/app/ui-message/index.ts similarity index 100% rename from apps/meteor/app/ui-message/index.js rename to apps/meteor/app/ui-message/index.ts diff --git a/apps/meteor/app/ui-sidenav/client/sideNav.html b/apps/meteor/app/ui-sidenav/client/sideNav.html index 37f514f80131..000122c3a833 100644 --- a/apps/meteor/app/ui-sidenav/client/sideNav.html +++ b/apps/meteor/app/ui-sidenav/client/sideNav.html @@ -14,7 +14,7 @@ {{_ "More_unreads"}} -
+
{{> Template.dynamic template=flexTemplate data=flexData }}
{{> sidebarFooter}} diff --git a/apps/meteor/app/ui-utils/client/lib/readMessages.ts b/apps/meteor/app/ui-utils/client/lib/readMessages.ts index d36a76d90866..b6a90fcf4758 100644 --- a/apps/meteor/app/ui-utils/client/lib/readMessages.ts +++ b/apps/meteor/app/ui-utils/client/lib/readMessages.ts @@ -160,7 +160,7 @@ export class ReadMessage extends Emitter { if (firstUnreadRecord) { room.unreadFirstId = firstUnreadRecord._id; document.querySelector('.message.first-unread')?.classList.remove('first-unread'); - document.querySelector(`.message#${firstUnreadRecord._id}`)?.classList.add('first-unread'); + document.querySelector(`.message[data-id="${firstUnreadRecord._id}"]`)?.classList.add('first-unread'); } } } diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index ab65439e2352..4c2e1053a4fc 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -43,8 +43,7 @@ export class ChatMessages implements ChatAPI { return; } - this.composer.setText((await this.data.getDraft(undefined)) ?? ''); - await this.currentEditing.stop(); + await this.currentEditing.cancel(); }, toNextMessage: async () => { if (!this.composer || !this.currentEditing) { @@ -55,18 +54,17 @@ export class ChatMessages implements ChatAPI { const nextMessage = currentMessage ? await this.data.findNextOwnMessage(currentMessage) : undefined; if (nextMessage) { - this.messageEditing.editMessage(nextMessage, { cursorAtStart: true }); + await this.messageEditing.editMessage(nextMessage, { cursorAtStart: true }); return; } - await this.currentEditing.stop(); - this.composer.setText((await this.data.getDraft(undefined)) ?? ''); + await this.currentEditing.cancel(); }, editMessage: async (message: IMessage, { cursorAtStart = false }: { cursorAtStart?: boolean } = {}) => { const text = (await this.data.getDraft(message._id)) || message.attachments?.[0].description || message.msg; const cursorPosition = cursorAtStart ? 0 : text.length; - this.currentEditing?.stop(); + await this.currentEditing?.stop(); if (!this.composer || !(await this.data.canUpdateMessage(message))) { return; @@ -152,16 +150,17 @@ export class ChatMessages implements ChatAPI { } await this.data.discardDraft(this.currentEditingMID); - this.currentEditing?.stop(); + await this.currentEditing?.stop(); + this.composer?.setText((await this.data.getDraft(undefined)) ?? ''); }, }; } - private release() { + private async release() { this.composer?.release(); if (this.currentEditing) { if (!this.params.tmid) { - this.currentEditing.cancel(); + await this.currentEditing.cancel(); } this.composer?.clear(); } diff --git a/apps/meteor/client/components/MarkdownText.tsx b/apps/meteor/client/components/MarkdownText.tsx index 191f72c8d690..a5591227aad9 100644 --- a/apps/meteor/client/components/MarkdownText.tsx +++ b/apps/meteor/client/components/MarkdownText.tsx @@ -26,7 +26,7 @@ marked.Lexer.rules.gfm = { }; const linkMarked = (href: string | null, _title: string | null, text: string): string => - `${text} `; + `${text} `; const paragraphMarked = (text: string): string => text; const brMarked = (): string => ' '; const listItemMarked = (text: string): string => { @@ -46,6 +46,9 @@ inlineRenderer.hr = horizontalRuleMarked; inlineWithoutBreaks.link = linkMarked; inlineWithoutBreaks.paragraph = paragraphMarked; inlineWithoutBreaks.br = brMarked; +inlineWithoutBreaks.image = brMarked; +inlineWithoutBreaks.code = paragraphMarked; +inlineWithoutBreaks.codespan = paragraphMarked; inlineWithoutBreaks.listitem = listItemMarked; inlineWithoutBreaks.hr = horizontalRuleMarked; @@ -118,6 +121,15 @@ const MarkdownText: FC> = ({ } })(); + // Add a hook to make all links open a new window + dompurify.addHook('afterSanitizeAttributes', (node) => { + // set all elements owning target to target=_blank + if ('target' in node) { + node.setAttribute('target', '_blank'); + node.setAttribute('rel', 'nofollow noopener noreferrer'); + } + }); + return preserveHtml ? html : html && sanitizer(html, { ADD_ATTR: ['target'], ALLOWED_URI_REGEXP: getRegexp(schemes) }); }, [preserveHtml, sanitizer, content, variant, markedOptions, parseEmoji, schemes]); diff --git a/apps/meteor/client/components/Page/PageHeader.tsx b/apps/meteor/client/components/Page/PageHeader.tsx index 416cba913386..aed5c44f39ed 100644 --- a/apps/meteor/client/components/Page/PageHeader.tsx +++ b/apps/meteor/client/components/Page/PageHeader.tsx @@ -19,11 +19,15 @@ const PageHeader: FC = ({ children = undefined, title, onClickB const { isMobile } = useLayout(); return ( - + = ({ title, onClose, children, ...props }) => ( {(title || onClose) && ( {title && ( - + {title} )} diff --git a/apps/meteor/client/components/Sidebar/SidebarGenericItem.tsx b/apps/meteor/client/components/Sidebar/SidebarGenericItem.tsx index feeb1176632e..7fc522c31f09 100644 --- a/apps/meteor/client/components/Sidebar/SidebarGenericItem.tsx +++ b/apps/meteor/client/components/Sidebar/SidebarGenericItem.tsx @@ -1,5 +1,4 @@ -import { css } from '@rocket.chat/css-in-js'; -import { Box } from '@rocket.chat/fuselage'; +import { Box, SidebarItem } from '@rocket.chat/fuselage'; import type colors from '@rocket.chat/fuselage-tokens/colors'; import type { ReactElement, ReactNode } from 'react'; import React, { memo } from 'react'; @@ -7,6 +6,7 @@ import React, { memo } from 'react'; type SidebarGenericItemProps = { href?: string; active?: boolean; + featured?: boolean; children: ReactNode; customColors?: { default: typeof colors[string]; @@ -16,42 +16,12 @@ type SidebarGenericItemProps = { textColor?: string; }; -const SidebarGenericItem = ({ - href, - active, - children, - customColors, - textColor = 'default', - ...props -}: SidebarGenericItemProps): ReactElement => ( - - +const SidebarGenericItem = ({ href, active, children, ...props }: SidebarGenericItemProps): ReactElement => ( + + {children} - + ); export default memo(SidebarGenericItem); diff --git a/apps/meteor/client/components/Sidebar/SidebarNavigationItem.tsx b/apps/meteor/client/components/Sidebar/SidebarNavigationItem.tsx index 24c386bb32b2..131be118e30d 100644 --- a/apps/meteor/client/components/Sidebar/SidebarNavigationItem.tsx +++ b/apps/meteor/client/components/Sidebar/SidebarNavigationItem.tsx @@ -35,8 +35,8 @@ const SidebarNavigationItem: FC = ({ return ( {icon && } - - {label} {tag && {tag}} + + {label} {tag && {tag}} ); diff --git a/apps/meteor/client/components/VerticalBar/VerticalBarAction.tsx b/apps/meteor/client/components/VerticalBar/VerticalBarAction.tsx index 5a186cd8e830..afe5b5b15b40 100644 --- a/apps/meteor/client/components/VerticalBar/VerticalBarAction.tsx +++ b/apps/meteor/client/components/VerticalBar/VerticalBarAction.tsx @@ -3,13 +3,15 @@ import { IconButton } from '@rocket.chat/fuselage'; import type { ReactElement, MouseEventHandler, ComponentProps } from 'react'; import React, { memo } from 'react'; -const VerticalBarAction = ({ - name, - ...props -}: { +type VerticalBarActionProps = { name: ComponentProps['name']; title?: string; + disabled?: boolean; onClick?: MouseEventHandler; -}): ReactElement => ; +}; + +const VerticalBarAction = ({ name, ...props }: VerticalBarActionProps): ReactElement => ( + +); export default memo(VerticalBarAction); diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 970e2a58d2d2..982092d56ac5 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -87,7 +87,7 @@ export type ChatAPI = { | undefined; readonly flows: { readonly uploadFiles: (files: readonly File[]) => Promise; - readonly sendMessage: ({ text, tshow }: { text: string; tshow?: boolean }) => Promise; + readonly sendMessage: ({ text, tshow }: { text: string; tshow?: boolean }) => Promise; readonly processSlashCommand: (message: IMessage) => Promise; readonly processTooLongMessage: (message: IMessage) => Promise; readonly processMessageEditing: (message: Pick & Partial>) => Promise; diff --git a/apps/meteor/client/lib/chats/data.ts b/apps/meteor/client/lib/chats/data.ts index f2cfa17528d0..b0e323cd6ffb 100644 --- a/apps/meteor/client/lib/chats/data.ts +++ b/apps/meteor/client/lib/chats/data.ts @@ -12,7 +12,6 @@ import { call } from '../utils/call'; import { prependReplies } from '../utils/prependReplies'; import type { DataAPI } from './ChatAPI'; -const messagesCollection = Messages as Mongo.Collection; const roomsCollection = Rooms as Mongo.Collection; const subscriptionsCollection = Subscriptions as Mongo.Collection; @@ -38,7 +37,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage }; const findMessageByID = async (mid: IMessage['_id']): Promise => - messagesCollection.findOne({ _id: mid, _hidden: { $ne: true } }, { reactive: false }) ?? call('getSingleMessage', mid); + Messages.findOne({ _id: mid, _hidden: { $ne: true } }, { reactive: false }) ?? call('getSingleMessage', mid); const getMessageByID = async (mid: IMessage['_id']): Promise => { const message = await findMessageByID(mid); @@ -51,7 +50,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage }; const findLastMessage = async (): Promise => - messagesCollection.findOne({ rid, tmid: tmid ?? { $exists: false }, _hidden: { $ne: true } }, { sort: { ts: -1 }, reactive: false }); + Messages.findOne({ rid, tmid: tmid ?? { $exists: false }, _hidden: { $ne: true } }, { sort: { ts: -1 }, reactive: false }); const getLastMessage = async (): Promise => { const message = await findLastMessage(); @@ -70,7 +69,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage return undefined; } - return messagesCollection.findOne( + return Messages.findOne( { rid, 'tmid': tmid ?? { $exists: false }, 'u._id': uid, '_hidden': { $ne: true } }, { sort: { ts: -1 }, reactive: false }, ); @@ -93,7 +92,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage return undefined; } - return messagesCollection.findOne( + return Messages.findOne( { rid, 'tmid': tmid ?? { $exists: false }, 'u._id': uid, '_hidden': { $ne: true }, 'ts': { $lt: message.ts } }, { sort: { ts: -1 }, reactive: false }, ); @@ -116,7 +115,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage return undefined; } - return messagesCollection.findOne( + return Messages.findOne( { rid, 'tmid': tmid ?? { $exists: false }, 'u._id': uid, '_hidden': { $ne: true }, 'ts': { $gt: message.ts } }, { sort: { ts: 1 }, reactive: false }, ); @@ -133,7 +132,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage }; const pushEphemeralMessage = async (message: Omit): Promise => { - messagesCollection.upsert({ _id: message._id }, { $set: { ...message, rid, ...(tmid && { tmid }) } }); + Messages.upsert({ _id: message._id }, { $set: { ...message, rid, ...(tmid && { tmid }) } }); }; const canUpdateMessage = async (message: IMessage): Promise => { diff --git a/apps/meteor/client/lib/chats/flows/sendMessage.ts b/apps/meteor/client/lib/chats/flows/sendMessage.ts index 9320e7fcf05b..e40394d515d2 100644 --- a/apps/meteor/client/lib/chats/flows/sendMessage.ts +++ b/apps/meteor/client/lib/chats/flows/sendMessage.ts @@ -32,13 +32,13 @@ const process = async (chat: ChatAPI, message: IMessage): Promise => { await call('sendMessage', message); }; -export const sendMessage = async (chat: ChatAPI, { text, tshow }: { text: string; tshow?: boolean }): Promise => { +export const sendMessage = async (chat: ChatAPI, { text, tshow }: { text: string; tshow?: boolean }): Promise => { if (!(await chat.data.isSubscribedToRoom())) { try { await chat.data.joinRoom(); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); - return; + return false; } } @@ -48,7 +48,7 @@ export const sendMessage = async (chat: ChatAPI, { text, tshow }: { text: string if (!text && !chat.currentEditing) { // Nothing to do - return; + return false; } if (text) { @@ -64,7 +64,7 @@ export const sendMessage = async (chat: ChatAPI, { text, tshow }: { text: string } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } - return; + return true; } if (chat.currentEditing) { @@ -72,20 +72,22 @@ export const sendMessage = async (chat: ChatAPI, { text, tshow }: { text: string if (!originalMessage) { dispatchToastMessage({ type: 'warning', message: t('Message_not_found') }); - return; + return false; } try { if (await chat.flows.processMessageEditing({ ...originalMessage, msg: '' })) { chat.currentEditing.stop(); - return; + return false; } await chat.currentEditing?.reset(); await chat.flows.requestMessageDeletion(originalMessage); - return; + return false; } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } } + + return false; }; diff --git a/apps/meteor/client/providers/MeteorProvider.tsx b/apps/meteor/client/providers/MeteorProvider.tsx index e4bd8c946e20..e6896093a1d5 100644 --- a/apps/meteor/client/providers/MeteorProvider.tsx +++ b/apps/meteor/client/providers/MeteorProvider.tsx @@ -25,11 +25,11 @@ const MeteorProvider: FC = ({ children }) => ( - - - - - + + + + + @@ -51,11 +51,11 @@ const MeteorProvider: FC = ({ children }) => ( - - - - - + + + + + diff --git a/apps/meteor/client/providers/TranslationProvider.tsx b/apps/meteor/client/providers/TranslationProvider.tsx index 711779ae52cf..8272f6acd7bb 100644 --- a/apps/meteor/client/providers/TranslationProvider.tsx +++ b/apps/meteor/client/providers/TranslationProvider.tsx @@ -1,5 +1,6 @@ +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { TranslationContext, useAbsoluteUrl } from '@rocket.chat/ui-contexts'; +import { useSetting, TranslationContext, useAbsoluteUrl } from '@rocket.chat/ui-contexts'; import i18next from 'i18next'; import I18NextHttpBackend from 'i18next-http-backend'; import { TAPi18n, TAPi18next } from 'meteor/rocketchat:tap-i18n'; @@ -16,46 +17,105 @@ type TranslationNamespace = Extract exten : never : never; -const namespaces = ['onboarding', 'registration'] as TranslationNamespace[]; +const namespacesDefault = ['onboarding', 'registration'] as TranslationNamespace[]; + +const parseToJSON = (customTranslations: string) => { + try { + return JSON.parse(customTranslations); + } catch (e) { + return false; + } +}; const useI18next = (lng: string): typeof i18next => { const basePath = useAbsoluteUrl()('/i18n'); - const i18n = useState(() => { + const customTranslations = useSetting('Custom_Translations'); + + const parse = useMutableCallback((data: string, lngs?: string | string[], namespaces: string | string[] = []): { [key: string]: any } => { + const parsedCustomTranslations = typeof customTranslations === 'string' && parseToJSON(customTranslations); + + const source = JSON.parse(data); + const result: { [key: string]: any } = {}; + + for (const [key, value] of Object.entries(source)) { + const prefix = (Array.isArray(namespaces) ? namespaces : [namespaces]).find((namespace) => key.startsWith(`${namespace}.`)); + + if (prefix) { + result[key.slice(prefix.length + 1)] = value; + } + } + + if (lngs && parsedCustomTranslations) { + for (const language of Array.isArray(lngs) ? lngs : [lngs]) { + if (!parsedCustomTranslations[language]) { + continue; + } + + for (const [key, value] of Object.entries(parsedCustomTranslations[language])) { + const prefix = (Array.isArray(namespaces) ? namespaces : [namespaces]).find((namespace) => key.startsWith(`${namespace}.`)); + + if (prefix) { + result[key.slice(prefix.length + 1)] = value; + } + } + } + } + + return result; + }); + + const [i18n] = useState(() => { const i18n = i18next.createInstance().use(I18NextHttpBackend).use(initReactI18next); i18n.init({ lng, fallbackLng: 'en', - ns: namespaces, + ns: namespacesDefault, nsSeparator: '.', + partialBundledLanguages: true, debug: false, backend: { loadPath: `${basePath}/{{lng}}.json`, - parse: (data: string, _languages?: string | string[], namespaces: string | string[] = []): { [key: string]: any } => { - const source = JSON.parse(data); - const result: { [key: string]: any } = {}; - - for (const key of Object.keys(source)) { - const prefix = (Array.isArray(namespaces) ? namespaces : [namespaces]).find((namespace) => key.startsWith(`${namespace}.`)); - - if (prefix) { - result[key.slice(prefix.length + 1)] = source[key]; - } - } - - return result; - }, + parse, }, }); return i18n; - })[0]; + }); useEffect(() => { i18n.changeLanguage(lng); }, [i18n, lng]); + useEffect(() => { + if (!customTranslations || typeof customTranslations !== 'string') { + return; + } + + const parsedCustomTranslations: Record> = JSON.parse(customTranslations); + + for (const [ln, translations] of Object.entries(parsedCustomTranslations)) { + const namespaces = Object.entries(translations).reduce((acc, [key, value]): Record> => { + const namespace = key.split('.')[0]; + + if (namespacesDefault.includes(namespace as unknown as TranslationNamespace)) { + acc[namespace] = acc[namespace] ?? {}; + acc[namespace][key] = value; + acc[namespace][key.slice(namespace.length + 1)] = value; + return acc; + } + acc.project = acc.project ?? {}; + acc.project[key] = value; + return acc; + }, {} as Record>); + + for (const [namespace, translations] of Object.entries(namespaces)) { + i18n.addResourceBundle(ln, namespace, translations); + } + } + }, [customTranslations, i18n]); + return i18n; }; diff --git a/apps/meteor/client/sidebar/RoomList/RoomList.tsx b/apps/meteor/client/sidebar/RoomList/RoomList.tsx index 09ad31e2b50e..f74729b23b11 100644 --- a/apps/meteor/client/sidebar/RoomList/RoomList.tsx +++ b/apps/meteor/client/sidebar/RoomList/RoomList.tsx @@ -10,7 +10,6 @@ import { useAvatarTemplate } from '../hooks/useAvatarTemplate'; import { usePreventDefault } from '../hooks/usePreventDefault'; import { useRoomList } from '../hooks/useRoomList'; import { useShortcutOpenMenu } from '../hooks/useShortcutOpenMenu'; -import { useSidebarPaletteColor } from '../hooks/useSidebarPaletteColor'; import { useTemplateByViewMode } from '../hooks/useTemplateByViewMode'; import RoomListRow from './RoomListRow'; import ScrollerWithCustomProps from './ScrollerWithCustomProps'; @@ -43,7 +42,6 @@ const RoomList = (): ReactElement => { usePreventDefault(ref); useShortcutOpenMenu(ref); - useSidebarPaletteColor(); return ( diff --git a/apps/meteor/client/sidebar/RoomList/normalizeSidebarMessage.ts b/apps/meteor/client/sidebar/RoomList/normalizeSidebarMessage.ts index f7cb2edd5ffe..9a506b861e56 100644 --- a/apps/meteor/client/sidebar/RoomList/normalizeSidebarMessage.ts +++ b/apps/meteor/client/sidebar/RoomList/normalizeSidebarMessage.ts @@ -1,12 +1,13 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { escapeHTML } from '@rocket.chat/string-helpers'; import type { useTranslation } from '@rocket.chat/ui-contexts'; +import emojione from 'emojione'; import { filterMarkdown } from '../../../app/markdown/lib/markdown'; export const normalizeSidebarMessage = (message: IMessage, t: ReturnType): string | undefined => { if (message.msg) { - return escapeHTML(filterMarkdown(message.msg)); + return escapeHTML(filterMarkdown(emojione.shortnameToUnicode(message.msg))); } if (message.attachments) { diff --git a/apps/meteor/client/sidebar/footer/SidebarFooterDefault.tsx b/apps/meteor/client/sidebar/footer/SidebarFooterDefault.tsx index 41e61b31eb6f..aba6617be232 100644 --- a/apps/meteor/client/sidebar/footer/SidebarFooterDefault.tsx +++ b/apps/meteor/client/sidebar/footer/SidebarFooterDefault.tsx @@ -1,5 +1,5 @@ import { css } from '@rocket.chat/css-in-js'; -import { Box, Divider, Palette, SidebarFooter as Footer } from '@rocket.chat/fuselage'; +import { Box, SidebarDivider, Palette, SidebarFooter as Footer } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React from 'react'; @@ -20,7 +20,7 @@ const SidebarFooterDefault = (): ReactElement => { return (