diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index f53a904ab5b43..a9b285b074a88 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -79,10 +79,10 @@ jobs: - uses: actions/checkout@v3 - - name: Use Node.js 14.19.3 + - name: Use Node.js 14.21.2 uses: actions/setup-node@v3 with: - node-version: '14.19.3' + node-version: '14.21.2' cache: 'yarn' - name: Free disk space @@ -222,7 +222,7 @@ jobs: strategy: matrix: - node-version: ['14.19.3'] + node-version: ['14.21.2'] mongodb-version: ['4.2', '4.4', '5.0'] steps: @@ -423,7 +423,7 @@ jobs: strategy: matrix: - node-version: ['14.19.3'] + node-version: ['14.21.2'] mongodb-version-ee: ['4.4'] steps: @@ -521,18 +521,18 @@ jobs: run: | docker ps - until echo "$(docker compose -f docker-compose-ci.yml logs ddp-streamer-service)" | grep -q "NetworkBroker started successfully"; do - echo "Waiting 'ddp-streamer' to start up" - ((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs ddp-streamer-service && exit 1 - sleep 10 - done; - until echo "$(docker compose -f docker-compose-ci.yml logs rocketchat)" | grep -q "SERVER RUNNING"; do echo "Waiting Rocket.Chat to start up" ((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs rocketchat && exit 1 sleep 10 done; + until echo "$(docker compose -f docker-compose-ci.yml logs ddp-streamer-service)" | grep -q "NetworkBroker started successfully"; do + echo "Waiting 'ddp-streamer' to start up" + ((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs ddp-streamer-service && exit 1 + sleep 10 + done; + - name: E2E Test API env: LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }} @@ -693,7 +693,7 @@ jobs: aws s3 cp $ROCKET_DEPLOY_DIR/ s3://download.rocket.chat/build/ --recursive curl -H "Content-Type: application/json" -H "X-Update-Token: $UPDATE_TOKEN" -d \ - "{\"nodeVersion\": \"14.19.3\", \"compatibleMongoVersions\": [\"4.2\", \"4.4\", \"5.0\"], \"commit\": \"$GITHUB_SHA\", \"tag\": \"$RC_VERSION\", \"branch\": \"$GIT_BRANCH\", \"artifactName\": \"$ARTIFACT_NAME\", \"releaseType\": \"$RC_RELEASE\"}" \ + "{\"nodeVersion\": \"14.21.2\", \"compatibleMongoVersions\": [\"4.2\", \"4.4\", \"5.0\"], \"commit\": \"$GITHUB_SHA\", \"tag\": \"$RC_VERSION\", \"branch\": \"$GIT_BRANCH\", \"artifactName\": \"$ARTIFACT_NAME\", \"releaseType\": \"$RC_RELEASE\"}" \ https://releases.rocket.chat/update # Makes build fail if the release isn't there diff --git a/.github/workflows/pr-title-checker.yml b/.github/workflows/pr-title-checker.yml index ee280e088f00d..3b0cc7ba6a494 100644 --- a/.github/workflows/pr-title-checker.yml +++ b/.github/workflows/pr-title-checker.yml @@ -7,6 +7,6 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: thehanimo/pr-title-checker@v1.3.4 + - uses: thehanimo/pr-title-checker@v1.3.6 with: GITHUB_TOKEN: ${{ secrets.RC_TITLE_CHECKER }} diff --git a/.vscode/client.code-snippets b/.vscode/client.code-snippets new file mode 100644 index 0000000000000..f0f7ddcdc8845 --- /dev/null +++ b/.vscode/client.code-snippets @@ -0,0 +1,36 @@ +{ + "Storybook stories module for React component": { + "scope": "typescriptreact", + "prefix": "sbmodule", + "body": [ + "import type { ComponentMeta, ComponentStory } from '@storybook/react';", + "import React from 'react';", + "", + "import $1 from './$1';", + "", + "export default {", + "\ttitle: '$2',", + "\tcomponent: $1,", + "} as ComponentMeta;", + "", + "export const Example: ComponentStory = (args) => <$1 {...args} />;", + ] + }, + "Storybook meta": { + "scope": "typescriptreact", + "prefix": "sbmeta", + "body": [ + "export default {", + "\ttitle: '$1',", + "\tcomponent: $2,", + "} as ComponentMeta;" + ] + }, + "Storybook story": { + "scope": "typescriptreact", + "prefix": "sbstory", + "body": [ + "export const $1: ComponentStory = (args) => <$2 {...args} />;" + ] + } +} diff --git a/.vscode/settings.json b/.vscode/settings.json index a32489e34a647..47310bec0703b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,10 +14,5 @@ } ], "typescript.tsdk": "./node_modules/typescript/lib", - "cSpell.words": [ - "livechat", - "omnichannel", - "photoswipe", - "tmid" - ] + "cSpell.words": ["katex", "livechat", "omnichannel", "photoswipe", "tmid"] } diff --git a/_templates/package/new/package.json.ejs.t b/_templates/package/new/package.json.ejs.t index 44fe58fea9b44..da6f3d189bd5e 100644 --- a/_templates/package/new/package.json.ejs.t +++ b/_templates/package/new/package.json.ejs.t @@ -11,7 +11,7 @@ to: packages/<%= name %>/package.json "eslint": "^8.12.0", "jest": "^27.5.1", "ts-jest": "^27.1.4", - "typescript": "~4.5.5" + "typescript": "~4.6.4" }, "scripts": { "lint": "eslint --ext .js,.jsx,.ts,.tsx .", diff --git a/_templates/service/new/package.json.ejs.t b/_templates/service/new/package.json.ejs.t index a985eb3c73f14..d1b5753dc417e 100644 --- a/_templates/service/new/package.json.ejs.t +++ b/_templates/service/new/package.json.ejs.t @@ -42,7 +42,7 @@ to: ee/apps/<%= name %>/package.json "@types/polka": "^0.5.4", "eslint": "^8.29.0", "ts-node": "^10.9.1", - "typescript": "~4.5.5" + "typescript": "~4.6.4" }, "main": "./dist/ee/apps/<%= name %>/src/service.js", "files": [ diff --git a/apps/meteor/.docker-mongo/Dockerfile b/apps/meteor/.docker-mongo/Dockerfile index f8556d971d8bc..998fda0463632 100644 --- a/apps/meteor/.docker-mongo/Dockerfile +++ b/apps/meteor/.docker-mongo/Dockerfile @@ -1,4 +1,4 @@ -FROM node:14.19.3-bullseye-slim +FROM node:14.21.2-bullseye-slim LABEL maintainer="buildmaster@rocket.chat" diff --git a/apps/meteor/.docker/Dockerfile b/apps/meteor/.docker/Dockerfile index ce06aeb052eba..aeb5f4648aaac 100644 --- a/apps/meteor/.docker/Dockerfile +++ b/apps/meteor/.docker/Dockerfile @@ -1,4 +1,4 @@ -FROM node:14.19.3-bullseye-slim +FROM node:14.21.2-bullseye-slim LABEL maintainer="buildmaster@rocket.chat" diff --git a/apps/meteor/.docker/Dockerfile.alpine b/apps/meteor/.docker/Dockerfile.alpine index 99aa4c2eb0164..cc896cf9051f4 100644 --- a/apps/meteor/.docker/Dockerfile.alpine +++ b/apps/meteor/.docker/Dockerfile.alpine @@ -1,4 +1,4 @@ -FROM node:14.19.3-alpine3.15 +FROM node:14.21.2-alpine3.16 RUN apk add --no-cache ttf-dejavu diff --git a/apps/meteor/.meteor/packages b/apps/meteor/.meteor/packages index def1feaa974ea..63e36de0e438c 100644 --- a/apps/meteor/.meteor/packages +++ b/apps/meteor/.meteor/packages @@ -13,33 +13,33 @@ accounts-facebook@1.3.3 accounts-github@1.5.0 accounts-google@1.4.0 accounts-meteor-developer@1.5.0 -accounts-password@2.3.1 +accounts-password@2.3.3 accounts-twitter@1.5.0 blaze-html-templates -check@1.3.1 -ddp-rate-limiter@1.1.0 +check@1.3.2 +ddp-rate-limiter@1.1.1 ddp-common@1.4.0 dynamic-import@0.7.2 -ecmascript@0.16.2 -typescript@4.5.4 -ejson@1.1.2 -email@2.2.1 +ecmascript@0.16.4 +typescript@4.6.4 +ejson@1.1.3 +email@2.2.3 http@2.0.0 logging@1.3.1 meteor-base@1.5.1 mobile-experience@1.1.0 -mongo@1.15.0 -random@1.2.0 +mongo@1.16.3 +random@1.2.1 rate-limit@1.0.9 -reactive-dict@1.3.0 -reactive-var@1.0.11 +reactive-dict@1.3.1 +reactive-var@1.0.12 reload@1.3.1 -service-configuration@1.3.0 -session@1.2.0 +service-configuration@1.3.1 +session@1.2.1 shell-server@0.5.0 spacebars -standard-minifier-js@2.8.0 -tracker@1.2.0 +standard-minifier-js@2.8.1 +tracker@1.2.1 rocketchat:livechat rocketchat:streamer @@ -66,27 +66,27 @@ raix:handlebar-helpers raix:ui-dropped-event rocketchat:tap-i18n@3.0.0 -underscore@1.0.10 +underscore@1.0.11 littledata:synced-cron -accounts-base@2.2.3 -accounts-oauth@1.4.1 +accounts-base@2.2.6 +accounts-oauth@1.4.2 autoupdate@1.8.0 -babel-compiler@7.9.0 -google-oauth@1.4.2 +babel-compiler@7.10.1 +google-oauth@1.4.3 htmljs matb33:collection-hooks meteorhacks:inject-initial -oauth@2.1.2 -oauth2@1.3.1 +oauth@2.1.3 +oauth2@1.3.2 routepolicy@1.1.1 sha@1.0.9 templating -webapp@1.13.1 -webapp-hashing@1.1.0 +webapp@1.13.2 +webapp-hashing@1.1.1 rocketchat:oauth2-server rocketchat:i18n dandv:caret-position facts-base@1.0.1 url@1.3.2 -standard-minifier-css +standard-minifier-css@1.8.3 diff --git a/apps/meteor/.meteor/release b/apps/meteor/.meteor/release index 66dd7b6647248..eff99d911ecbf 100644 --- a/apps/meteor/.meteor/release +++ b/apps/meteor/.meteor/release @@ -1 +1 @@ -METEOR@2.7.3 +METEOR@2.9.1 diff --git a/apps/meteor/.meteor/versions b/apps/meteor/.meteor/versions index 241cc62ec499f..7eadce0be1179 100644 --- a/apps/meteor/.meteor/versions +++ b/apps/meteor/.meteor/versions @@ -1,51 +1,51 @@ -accounts-base@2.2.3 +accounts-base@2.2.6 accounts-facebook@1.3.3 accounts-github@1.5.0 accounts-google@1.4.0 accounts-meteor-developer@1.5.0 -accounts-oauth@1.4.1 -accounts-password@2.3.1 +accounts-oauth@1.4.2 +accounts-password@2.3.3 accounts-twitter@1.5.0 aldeed:simple-schema@1.5.4 allow-deny@1.1.1 autoupdate@1.8.0 -babel-compiler@7.9.0 +babel-compiler@7.10.1 babel-runtime@1.5.1 base64@1.0.12 binary-heap@1.0.11 -blaze@2.6.0 -blaze-html-templates@1.2.1 +blaze@2.6.1 +blaze-html-templates@2.0.0 blaze-tools@1.1.3 boilerplate-generator@1.7.1 caching-compiler@1.2.2 caching-html-compiler@1.2.1 callback-hook@1.4.0 -check@1.3.1 +check@1.3.2 coffeescript@2.4.1 coffeescript-compiler@2.4.1 dandv:caret-position@2.1.1 -ddp@1.4.0 -ddp-client@2.5.0 +ddp@1.4.1 +ddp-client@2.6.1 ddp-common@1.4.0 -ddp-rate-limiter@1.1.0 -ddp-server@2.5.0 +ddp-rate-limiter@1.1.1 +ddp-server@2.6.0 deps@1.0.12 -diff-sequence@1.1.1 +diff-sequence@1.1.2 dispatch:run-as-user@1.1.1 dynamic-import@0.7.2 -ecmascript@0.16.2 +ecmascript@0.16.4 ecmascript-runtime@0.8.0 ecmascript-runtime-client@0.12.1 ecmascript-runtime-server@0.11.0 -ejson@1.1.2 -email@2.2.1 +ejson@1.1.3 +email@2.2.3 es5-shim@4.8.0 -facebook-oauth@1.11.0 +facebook-oauth@1.11.2 facts-base@1.0.1 -fetch@0.1.1 -geojson-utils@1.0.10 -github-oauth@1.4.0 -google-oauth@1.4.2 +fetch@0.1.3 +geojson-utils@1.0.11 +github-oauth@1.4.1 +google-oauth@1.4.3 hot-code-push@1.0.4 html-tools@1.1.3 htmljs@1.1.1 @@ -66,45 +66,45 @@ launch-screen@1.3.0 littledata:synced-cron@1.5.1 localstorage@1.2.0 logging@1.3.1 -matb33:collection-hooks@1.1.2 +matb33:collection-hooks@1.2.0 mdg:validation-error@0.5.1 -meteor@1.10.0 +meteor@1.10.4 meteor-base@1.5.1 -meteor-developer-oauth@1.3.1 +meteor-developer-oauth@1.3.2 meteorhacks:inject-initial@1.0.5 -minifier-css@1.6.0 -minifier-js@2.7.4 -minimongo@1.8.0 +minifier-css@1.6.2 +minifier-js@2.7.5 +minimongo@1.9.1 mobile-experience@1.1.0 mobile-status-bar@1.1.0 -modern-browsers@0.1.8 -modules@0.18.0 -modules-runtime@0.13.0 -mongo@1.15.0 +modern-browsers@0.1.9 +modules@0.19.0 +modules-runtime@0.13.1 +mongo@1.16.3 mongo-decimal@0.1.3 mongo-dev-server@1.1.0 mongo-id@1.0.8 mrt:reactive-store@0.0.1 mystor:device-detection@0.2.0 nooitaf:colors@1.2.0 -npm-mongo@4.3.1 -oauth@2.1.2 -oauth1@1.5.0 -oauth2@1.3.1 +npm-mongo@4.12.1 +oauth@2.1.3 +oauth1@1.5.1 +oauth2@1.3.2 observe-sequence@1.0.20 ordered-dict@1.1.0 ostrio:cookies@2.7.2 pauli:accounts-linkedin@6.0.0 pauli:linkedin-oauth@6.0.0 -promise@0.12.0 +promise@0.12.2 raix:eventemitter@1.0.0 raix:handlebar-helpers@0.2.5 raix:ui-dropped-event@0.0.7 -random@1.2.0 +random@1.2.1 rate-limit@1.0.9 react-fast-refresh@0.2.3 -reactive-dict@1.3.0 -reactive-var@1.0.11 +reactive-dict@1.3.1 +reactive-var@1.0.12 reload@1.3.1 retry@1.1.0 rocketchat:ddp@0.0.1 @@ -117,25 +117,26 @@ rocketchat:streamer@1.1.0 rocketchat:tap-i18n@3.0.0 rocketchat:version@1.0.0 routepolicy@1.1.1 -service-configuration@1.3.0 -session@1.2.0 +service-configuration@1.3.1 +session@1.2.1 sha@1.0.9 shell-server@0.5.0 simple:json-routes@2.3.1 socket-stream-client@0.5.0 spacebars@1.3.0 spacebars-compiler@1.3.1 -standard-minifier-css@1.8.1 -standard-minifier-js@2.8.0 +standard-minifier-css@1.8.3 +standard-minifier-js@2.8.1 templating@1.4.2 templating-compiler@1.4.1 -templating-runtime@1.6.0 +templating-runtime@1.6.1 templating-tools@1.2.2 -tracker@1.2.0 -twitter-oauth@1.3.0 -typescript@4.5.4 +tracker@1.2.1 +twitter-oauth@1.3.2 +typescript@4.6.4 ui@1.0.13 -underscore@1.0.10 +underscore@1.0.11 url@1.3.2 -webapp@1.13.1 -webapp-hashing@1.1.0 +webapp@1.13.2 +webapp-hashing@1.1.1 +zodern:types@1.0.9 diff --git a/apps/meteor/.mocharc.client.js b/apps/meteor/.mocharc.client.js index 071914fe61c59..07cde62887ca2 100644 --- a/apps/meteor/.mocharc.client.js +++ b/apps/meteor/.mocharc.client.js @@ -31,9 +31,9 @@ module.exports = { exit: false, slow: 200, spec: [ - 'tests/unit/client/**/*.spec.ts', + 'client/**/*.spec.{ts,tsx}', + 'tests/unit/client/**/*.spec.{ts,tsx}', 'tests/unit/lib/**/*.tests.ts', 'tests/unit/client/**/*.test.ts', - 'tests/unit/client/**/*.spec.tsx', ], }; diff --git a/apps/meteor/app/2fa/client/overrideMeteorCall.ts b/apps/meteor/app/2fa/client/overrideMeteorCall.ts index ac19a24219fc2..382499e2aedcc 100644 --- a/apps/meteor/app/2fa/client/overrideMeteorCall.ts +++ b/apps/meteor/app/2fa/client/overrideMeteorCall.ts @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { t } from '../../utils/client'; -import { process2faReturn } from '../../../client/lib/2fa/process2faReturn'; +import { process2faReturn, process2faAsyncReturn } from '../../../client/lib/2fa/process2faReturn'; import { isTotpInvalidError } from '../../../client/lib/2fa/utils'; -const { call } = Meteor; +const { call, callAsync } = Meteor; type Callback = { (error: unknown): void; @@ -34,8 +34,34 @@ const callWithoutTotp = (methodName: string, args: unknown[], callback: Callback }); }); +const callAsyncWithTotp = + (methodName: string, args: unknown[]) => + async (twoFactorCode: string, twoFactorMethod: string): Promise => { + try { + const result = await callAsync(methodName, ...args, { twoFactorCode, twoFactorMethod }); + + return result; + } catch (error: unknown) { + if (isTotpInvalidError(error)) { + throw new Error(twoFactorMethod === 'password' ? t('Invalid_password') : t('Invalid_two_factor_code')); + } + + throw error; + } + }; + Meteor.call = function (methodName: string, ...args: unknown[]): unknown { const callback = args.length > 0 && typeof args[args.length - 1] === 'function' ? (args.pop() as Callback) : (): void => undefined; return callWithoutTotp(methodName, args, callback)(); }; + +Meteor.callAsync = async function _callAsyncWithTotp(methodName: string, ...args: unknown[]): Promise { + const promise = callAsync(methodName, ...args); + + return process2faAsyncReturn({ + promise, + onCode: callAsyncWithTotp(methodName, args), + emailOrUsername: undefined, + }); +}; diff --git a/apps/meteor/app/action-links/README.md b/apps/meteor/app/action-links/README.md deleted file mode 100644 index 6967588117ae0..0000000000000 --- a/apps/meteor/app/action-links/README.md +++ /dev/null @@ -1,26 +0,0 @@ -RocketChat Action Links -============ - -Action Links are a way to add custom javascript functions to RocketChat messages. The links appear as a horizontal list below the message they correspond to, and by clicking the link will run a function you define server-side. - -Usage ------------- - -Add 'actionLinks' to any message object as in the example below. It should be an array of object, each containing a 'label' (this will be the text printed to click on), a method_id (this is the name of the method that will be run), and params (this is the parameters passed to the method_id function). - -~~~ -message.actionLinks = [{ label: "Another Option", method_id: "anotherFunction", params: "stuff"}, { label: "An Option", method_id: "functOne", params: ""}]; -~~~ - - - - - -The functions to be run need to be added to the actionLinkFuncts namespace. This is done by calling the RocketChat.actionLinks.register method. Your custom functions should take two parameters: the original message from the database, and the 'params' object given for that function. -~~~ -RocketChat.actionLinks.register('functOne', function (origDbMsg, params) { - - console.log("I did some stuff!"); - -}); -~~~ diff --git a/apps/meteor/app/action-links/client/lib/actionLinks.ts b/apps/meteor/app/action-links/client/lib/actionLinks.ts index 057592a2f7892..a69acc06ca85c 100644 --- a/apps/meteor/app/action-links/client/lib/actionLinks.ts +++ b/apps/meteor/app/action-links/client/lib/actionLinks.ts @@ -1,55 +1,28 @@ import { Meteor } from 'meteor/meteor'; import type { IMessage } from '@rocket.chat/core-typings'; -import { dispatchToastMessage } from '../../../../client/lib/toast'; +import { isLayoutEmbedded } from '../../../../client/lib/utils/isLayoutEmbedded'; +import { fireGlobalEvent } from '../../../../client/lib/utils/fireGlobalEvent'; // Action Links namespace creation. export const actionLinks = { - actions: new Map< - string, - (message: IMessage, params: string, instance?: Blaze.TemplateInstance | ((actionId: string, context: string) => void)) => void - >(), - register( - name: string, - fn: (message: IMessage, params: string, instance?: Blaze.TemplateInstance | ((actionId: string, context: string) => void)) => void, - ): void { + actions: new Map void>(), + register(name: string, fn: (message: IMessage, params: string) => void): void { actionLinks.actions.set(name, fn); }, - // getMessage(name, messageId) { - // const userId = Meteor.userId(); - // if (!userId) { - // throw new Meteor.Error('error-invalid-user', 'Invalid user', { - // function: 'actionLinks.getMessage', - // }); - // } - - // const message = Messages.findOne({ _id: messageId }); - // if (!message) { - // throw new Meteor.Error('error-invalid-message', 'Invalid message', { - // function: 'actionLinks.getMessage', - // }); - // } - - // const subscription = Subscriptions.findOne({ - // 'rid': message.rid, - // 'u._id': userId, - // }); - // if (!subscription) { - // throw new Meteor.Error('error-not-allowed', 'Not allowed', { - // function: 'actionLinks.getMessage', - // }); - // } - - // if (!message.actionLinks || !message.actionLinks[name]) { - // throw new Meteor.Error('error-invalid-actionlink', 'Invalid action link', { - // function: 'actionLinks.getMessage', - // }); - // } + run(actionMethodId: string, message: IMessage): void { + const embedded = isLayoutEmbedded(); + + if (embedded) { + fireGlobalEvent('click-action-link', { + actionlink: actionMethodId, + value: message._id, + message, + }); + return; + } - // return message; - // }, - run(method: string, message: IMessage, instance?: Blaze.TemplateInstance | ((actionId: string, context: string) => void)): void { - const actionLink = message.actionLinks?.find((action) => action.method_id === method); + const actionLink = message.actionLinks?.find((action) => action.method_id === actionMethodId); if (!actionLink) { throw new Meteor.Error('error-invalid-actionlink', 'Invalid action link'); @@ -60,21 +33,6 @@ export const actionLinks = { } const fn = actionLinks.actions.get(actionLink.method_id); - - let ranClient = false; - - if (fn) { - // run just on client side - fn(message, actionLink.params, instance); - - ranClient = true; - } - - // and run on server side - Meteor.call('actionLinkHandler', name, message._id, (error: unknown) => { - if (error && !ranClient) { - dispatchToastMessage({ type: 'error', message: error }); - } - }); + fn?.(message, actionLink.params); }, }; diff --git a/apps/meteor/app/action-links/server/actionLinkHandler.ts b/apps/meteor/app/action-links/server/actionLinkHandler.ts deleted file mode 100644 index 51c196ffb3b75..0000000000000 --- a/apps/meteor/app/action-links/server/actionLinkHandler.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; - -import { actionLinks } from './lib/actionLinks'; -// Action Links Handler. This method will be called off the client. - -Meteor.methods({ - actionLinkHandler(name, messageId) { - check(messageId, String); - check(name, String); - - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'actionLinkHandler' }); - } - - const message = actionLinks.getMessage(name, messageId); - - if (!message) { - throw new Meteor.Error('error-invalid-message', 'Invalid message', { method: 'actionLinkHandler' }); - } - - // NOTE: based on types (and how FE uses it) this should be the way of doing it - const actionLink = message.actionLinks?.find((action) => action.method_id === name); - - if (!actionLink) { - throw new Meteor.Error('error-invalid-actionlink', 'Invalid action link', { method: 'actionLinkHandler' }); - } - - actionLinks.actions[actionLink.method_id](message, actionLink.params); - }, -}); diff --git a/apps/meteor/app/action-links/server/index.ts b/apps/meteor/app/action-links/server/index.ts deleted file mode 100644 index 1b59ca164a314..0000000000000 --- a/apps/meteor/app/action-links/server/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { actionLinks } from './lib/actionLinks'; -import './actionLinkHandler'; - -export { actionLinks }; diff --git a/apps/meteor/app/action-links/server/lib/actionLinks.ts b/apps/meteor/app/action-links/server/lib/actionLinks.ts deleted file mode 100644 index 58b8fa950b03e..0000000000000 --- a/apps/meteor/app/action-links/server/lib/actionLinks.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { IMessage } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; - -import { getMessageForUser } from '../../../../server/lib/messages/getMessageForUser'; - -function getMessageById(messageId: IMessage['_id']): IMessage | undefined { - try { - const user = Meteor.userId(); - if (!user) { - return; - } - return Promise.await(getMessageForUser(messageId, user)); - } catch (e) { - throw new Meteor.Error(e instanceof Error ? e.message : String(e), 'Invalid message', { - function: 'actionLinks.getMessage', - }); - } -} - -type ActionLinkHandler = (message: IMessage, params?: string, instance?: undefined) => void; - -// Action Links namespace creation. -export const actionLinks = { - actions: {} as { [key: string]: ActionLinkHandler }, - register(name: string, funct: ActionLinkHandler): void { - actionLinks.actions[name] = funct; - }, - getMessage(name: string, messageId: IMessage['_id']): IMessage | undefined { - const message = getMessageById(messageId); - - if (!message) { - throw new Meteor.Error('error-invalid-message', 'Invalid message', { - function: 'actionLinks.getMessage', - }); - } - - if (!message.actionLinks?.some((action) => action.method_id === name)) { - throw new Meteor.Error('error-invalid-actionlink', 'Invalid action link', { - function: 'actionLinks.getMessage', - }); - } - - return message; - }, -}; diff --git a/apps/meteor/app/api/server/v1/e2e.ts b/apps/meteor/app/api/server/v1/e2e.ts index 47420ab2f62d4..70f1066e09f47 100644 --- a/apps/meteor/app/api/server/v1/e2e.ts +++ b/apps/meteor/app/api/server/v1/e2e.ts @@ -8,6 +8,7 @@ import { import type { IUser } from '@rocket.chat/core-typings'; import { API } from '../api'; +import { handleSuggestedGroupKey } from '../../../e2e/server/functions/handleSuggestedGroupKey'; API.v1.addRoute( 'e2e.fetchMyKeys', @@ -195,3 +196,37 @@ API.v1.addRoute( }, }, ); + +API.v1.addRoute( + 'e2e.acceptSuggestedGroupKey', + { + authRequired: true, + validateParams: ise2eGetUsersOfRoomWithoutKeyParamsGET, + }, + { + async post() { + const { rid } = this.bodyParams; + + await handleSuggestedGroupKey('accept', rid, this.userId, 'e2e.acceptSuggestedGroupKey'); + + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'e2e.rejectSuggestedGroupKey', + { + authRequired: true, + validateParams: ise2eGetUsersOfRoomWithoutKeyParamsGET, + }, + { + async post() { + const { rid } = this.bodyParams; + + await handleSuggestedGroupKey('reject', rid, this.userId, 'e2e.rejectSuggestedGroupKey'); + + return API.v1.success(); + }, + }, +); diff --git a/apps/meteor/app/apps/client/orchestrator.ts b/apps/meteor/app/apps/client/orchestrator.ts index 6e3591de0be06..86b958d8775d8 100644 --- a/apps/meteor/app/apps/client/orchestrator.ts +++ b/apps/meteor/app/apps/client/orchestrator.ts @@ -7,7 +7,7 @@ import type { IPermission } from '@rocket.chat/apps-engine/definition/permission import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage/IAppStorageItem'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import type { AppScreenshot, Serialized } from '@rocket.chat/core-typings'; +import type { AppScreenshot, AppRequestFilter, Pagination, IRestResponse, Serialized, AppRequest } from '@rocket.chat/core-typings'; import type { App } from '../../../client/views/admin/apps/types'; import { dispatchToastMessage } from '../../../client/lib/toast'; @@ -234,6 +234,28 @@ class AppClientOrchestrator { throw new Error('Failed to build external url'); } + public async appRequests( + appId: string, + filter: AppRequestFilter, + sort: string, + pagination: Pagination, + ): Promise> { + try { + const response: IRestResponse = await APIClient.get( + `/apps/app-request?appId=${appId}&q=${filter}&sort=${sort}&limit=${pagination.limit}&offset=${pagination.offset}`, + ); + + const restResponse = { + data: response.data, + meta: response.meta, + }; + + return restResponse; + } catch (e: unknown) { + throw new Error('Could not get the list of app requests'); + } + } + public async getCategories(): Promise> { const result = await APIClient.get('/apps', { categories: 'true' }); diff --git a/apps/meteor/app/apps/server/appRequestsCron.ts b/apps/meteor/app/apps/server/appRequestsCron.ts new file mode 100644 index 0000000000000..9709dc8c9bad3 --- /dev/null +++ b/apps/meteor/app/apps/server/appRequestsCron.ts @@ -0,0 +1,70 @@ +import { Meteor } from 'meteor/meteor'; +import { HTTP } from 'meteor/http'; +import { SyncedCron } from 'meteor/littledata:synced-cron'; + +import { settings } from '../../settings/server'; +import { Apps } from './orchestrator'; +import { getWorkspaceAccessToken } from '../../cloud/server'; +import { appRequestNotififyForUsers } from './marketplace/appRequestNotifyUsers'; + +export const appsNotifyAppRequests = Meteor.bindEnvironment(function _appsNotifyAppRequests() { + try { + const installedApps = Promise.await(Apps.installedApps({ enabled: true })); + if (!installedApps || installedApps.length === 0) { + return; + } + + const workspaceUrl = settings.get('Site_Url'); + const token = Promise.await(getWorkspaceAccessToken()); + const baseUrl = Apps.getMarketplaceUrl(); + if (!baseUrl) { + Apps.debugLog(`could not load marketplace base url to send app requests notifications`); + return; + } + + const options = { + headers: { + Authorization: `Bearer ${token}`, + }, + }; + + const pendingSentUrl = `${baseUrl}/v1/app-request/sent/pending`; + const result = HTTP.get(pendingSentUrl, options); + const data = result.data?.data; + const filtered = installedApps.filter((app) => data.indexOf(app.getID()) !== -1); + + filtered.forEach((app) => { + const appId = app.getID(); + const appName = app.getName(); + + const usersNotified = Promise.await<(string | Error)[]>( + appRequestNotififyForUsers(baseUrl, workspaceUrl, appId, appName) + .then((response) => { + // Mark all app requests as sent + HTTP.post(`${baseUrl}/v1/app-request/markAsSent/${appId}`, options); + return response; + }) + .catch((err) => { + Apps.debugLog(`could not send app request notifications for app ${appId}. Error: ${err}`); + return err; + }), + ); + + const errors = usersNotified.filter((batch) => batch instanceof Error); + if (errors.length > 0) { + Apps.debugLog(`Some batches of users could not be notified for app ${appId}. Errors: ${errors}`); + } + }); + } catch (err) { + Apps.debugLog(err); + } +}); + +// Scheduling as every 12 hours to avoid multiple instances hiting the marketplace at the same time +SyncedCron.add({ + name: 'Apps-Request-End-Users:notify', + schedule: (parser) => parser.text('every 12 hours'), + job() { + appsNotifyAppRequests(); + }, +}); diff --git a/apps/meteor/app/apps/server/communication/rest.js b/apps/meteor/app/apps/server/communication/rest.js index 1dd088b5cd4fd..595f15e7d35fa 100644 --- a/apps/meteor/app/apps/server/communication/rest.js +++ b/apps/meteor/app/apps/server/communication/rest.js @@ -885,5 +885,34 @@ export class AppsRestApi { }, }, ); + + this.api.addRoute( + 'app-request', + { authRequired: true }, + { + async get() { + const baseUrl = orchestrator.getMarketplaceUrl(); + const { appId, q = '', sort = '', limit = 25, offset = 0 } = this.queryParams; + const headers = getDefaultHeaders(); + + const token = await getWorkspaceAccessToken(); + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + try { + const data = HTTP.get(`${baseUrl}/v1/app-request?appId=${appId}&q=${q}&sort=${sort}&limit=${limit}&offset=${offset}`, { + headers, + }); + + return API.v1.success({ data }); + } catch (e) { + orchestrator.getRocketChatLogger().error('Error getting all non sent app requests from the Marketplace:', e.message); + + return API.v1.failure(e.message); + } + }, + }, + ); } } diff --git a/apps/meteor/app/apps/server/index.ts b/apps/meteor/app/apps/server/index.ts index ad3096af31588..35f7c2cc041fd 100644 --- a/apps/meteor/app/apps/server/index.ts +++ b/apps/meteor/app/apps/server/index.ts @@ -1,3 +1,4 @@ import './cron'; +import './appRequestsCron'; export { Apps, AppEvents } from './orchestrator'; diff --git a/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts b/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts new file mode 100644 index 0000000000000..b5589bd07502a --- /dev/null +++ b/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts @@ -0,0 +1,99 @@ +import { HTTP } from 'meteor/http'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import type { AppRequest, IUser, Pagination } from '@rocket.chat/core-typings'; + +import { API } from '../../../api/server'; +import { getWorkspaceAccessToken } from '../../../cloud/server'; +import { sendDirectMessageToUsers } from '../../../../server/lib/sendDirectMessageToUsers'; + +const ROCKET_CAT_USERID = 'rocket.cat'; +const DEFAULT_LIMIT = 100; + +const notifyBatchOfUsersError = (error: Error) => { + return new Error(`could not notify the batch of users. Error ${error}`); +}; + +const notifyBatchOfUsers = async (appName: string, learnMoreUrl: string, appRequests: AppRequest[]): Promise => { + const batchRequesters = appRequests.reduce((acc: string[], appRequest: AppRequest) => { + // Prevent duplicate requesters + if (!acc.includes(appRequest.requester.id)) { + acc.push(appRequest.requester.id); + } + + return acc; + }, []); + + const msgFn = (user: IUser): string => { + const defaultLang = user.language || 'en'; + const msg = `${TAPi18n.__('App_request_enduser_message', { appname: appName, learnmore: learnMoreUrl, lng: defaultLang })}`; + + return msg; + }; + + try { + return await sendDirectMessageToUsers(ROCKET_CAT_USERID, batchRequesters, msgFn); + } catch (e) { + throw e; + } +}; + +export const appRequestNotififyForUsers = async ( + marketplaceBaseUrl: string, + workspaceUrl: string, + appId: string, + appName: string, +): Promise<(string | Error)[]> => { + try { + const token = await getWorkspaceAccessToken(); + const headers = { + Authorization: `Bearer ${token}`, + }; + + // First request + const pagination: Pagination = { limit: DEFAULT_LIMIT, offset: 0 }; + + // First request to get the total and the first batch + const data = HTTP.get( + `${marketplaceBaseUrl}/v1/app-request?appId=${appId}&q=notification-not-sent&limit=${pagination.limit}&offset=${pagination.offset}`, + { headers }, + ); + + const appRequests = API.v1.success({ data }); + const { total } = appRequests.body.data.data.meta; + + if (total === undefined || total === 0) { + return []; + } + + // Calculate the number of loops - 1 because the first request was already made + const loops = Math.ceil(total / pagination.limit) - 1; + const requestsCollection = []; + const learnMore = `${workspaceUrl}admin/marketplace/all/info/${appId}`; + + // Notify first batch + requestsCollection.push( + Promise.resolve(appRequests.body.data.data.data) + .then((response) => notifyBatchOfUsers(appName, learnMore, response)) + .catch(notifyBatchOfUsersError), + ); + + // Batch requests + for (let i = 0; i < loops; i++) { + pagination.offset += pagination.limit; + + const request = HTTP.get( + `${marketplaceBaseUrl}/v1/app-request?appId=${appId}&q=notification-not-sent&limit=${pagination.limit}&offset=${pagination.offset}`, + { headers }, + ); + + requestsCollection.push(notifyBatchOfUsers(appName, learnMore, request.data.data)); + } + + const finalResult = await Promise.all(requestsCollection); + + // Return the list of users that were notified + return finalResult.flat(); + } catch (e) { + throw e; + } +}; diff --git a/apps/meteor/app/apps/server/orchestrator.js b/apps/meteor/app/apps/server/orchestrator.js index de3d950025b8f..64bcf0fba0a0e 100644 --- a/apps/meteor/app/apps/server/orchestrator.js +++ b/apps/meteor/app/apps/server/orchestrator.js @@ -191,6 +191,14 @@ export class AppServerOrchestrator { return this._manager.updateAppsMarketplaceInfo(apps).then(() => this._manager.get()); } + async installedApps(filter = {}) { + if (!this.isLoaded()) { + return; + } + + return this._manager.get(filter); + } + async triggerEvent(event, ...payload) { if (!this.isLoaded()) { return; diff --git a/apps/meteor/app/authorization/server/functions/canSendMessage.ts b/apps/meteor/app/authorization/server/functions/canSendMessage.ts index 6b962fbda47c5..bf9eb0ed7cbdd 100644 --- a/apps/meteor/app/authorization/server/functions/canSendMessage.ts +++ b/apps/meteor/app/authorization/server/functions/canSendMessage.ts @@ -16,7 +16,7 @@ const subscriptionOptions = { async function validateRoomMessagePermissionsAsync( room: IRoom | null, { uid, username, type }: { uid: IUser['_id']; username: IUser['username']; type: IUser['type'] }, - extraData: Record, + extraData?: Record, ): Promise { if (!room) { throw new Error('error-invalid-room'); @@ -48,7 +48,7 @@ async function validateRoomMessagePermissionsAsync( export async function canSendMessageAsync( rid: IRoom['_id'], { uid, username, type }: { uid: IUser['_id']; username: IUser['username']; type: IUser['type'] }, - extraData: Record, + extraData?: Record, ): Promise { const room = await Rooms.findOneById(rid); if (!room) { @@ -62,14 +62,14 @@ export async function canSendMessageAsync( export function canSendMessage( rid: IRoom['_id'], { uid, username, type }: { uid: IUser['_id']; username: IUser['username']; type: IUser['type'] }, - extraData: Record, + extraData?: Record, ): IRoom { return Promise.await(canSendMessageAsync(rid, { uid, username, type }, extraData)); } export function validateRoomMessagePermissions( room: IRoom, { uid, username, type }: { uid: IUser['_id']; username: IUser['username']; type: IUser['type'] }, - extraData: Record, + extraData?: Record, ): void { return Promise.await(validateRoomMessagePermissionsAsync(room, { uid, username, type }, extraData)); } diff --git a/apps/meteor/app/autotranslate/client/lib/actionButton.ts b/apps/meteor/app/autotranslate/client/lib/actionButton.ts index eb8cba74c34bb..78bbabd0f37af 100644 --- a/apps/meteor/app/autotranslate/client/lib/actionButton.ts +++ b/apps/meteor/app/autotranslate/client/lib/actionButton.ts @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { isTranslatedMessage } from '@rocket.chat/core-typings'; import { AutoTranslate } from './autotranslate'; import { settings } from '../../../settings/client'; @@ -8,6 +7,10 @@ import { hasAtLeastOnePermission } from '../../../authorization/client'; import { MessageAction } from '../../../ui-utils/client/lib/MessageAction'; import { messageArgs } from '../../../../client/lib/utils/messageArgs'; import { Messages } from '../../../models/client'; +import { + hasTranslationLanguageInAttachments, + hasTranslationLanguageInMessage, +} from '../../../../client/views/room/MessageList/lib/autoTranslate'; Meteor.startup(() => { AutoTranslate.init(); @@ -22,8 +25,7 @@ Meteor.startup(() => { action(_, props) { const { message = messageArgs(this).msg } = props; const language = AutoTranslate.getLanguage(message.rid); - if (!isTranslatedMessage(message) || !message.translations[language]) { - // } && !_.find(message.attachments, attachment => { return attachment.translations && attachment.translations[language]; })) { + if (!hasTranslationLanguageInMessage(message, language) && !hasTranslationLanguageInAttachments(message.attachments, language)) { (AutoTranslate.messageIdsToWait as any)[message._id] = true; Messages.update({ _id: message._id }, { $set: { autoTranslateFetching: true } }); Meteor.call('autoTranslate.translateMessage', message, language); @@ -31,12 +33,19 @@ Meteor.startup(() => { const action = 'autoTranslateShowInverse' in message ? '$unset' : '$set'; Messages.update({ _id: message._id }, { [action]: { autoTranslateShowInverse: true } }); }, - condition({ message, user }) { + condition({ message, subscription, user }) { if (!user) { return false; } + const language = subscription?.autoTranslateLanguage || AutoTranslate.getLanguage(message.rid) || ''; - return Boolean(message?.u && message.u._id !== user._id && isTranslatedMessage(message) && message.autoTranslateShowInverse); + return Boolean( + (message?.u && + message.u._id !== user._id && + subscription?.autoTranslate && + (message as { autoTranslateShowInverse?: boolean }).autoTranslateShowInverse) || + (!hasTranslationLanguageInMessage(message, language) && !hasTranslationLanguageInAttachments(message.attachments, language)), + ); }, order: 90, }); @@ -48,8 +57,7 @@ Meteor.startup(() => { action(_, props) { const { message = messageArgs(this).msg } = props; const language = AutoTranslate.getLanguage(message.rid); - if (!isTranslatedMessage(message) || !message.translations[language]) { - // } && !_.find(message.attachments, attachment => { return attachment.translations && attachment.translations[language]; })) { + if (!hasTranslationLanguageInMessage(message, language) && !hasTranslationLanguageInAttachments(message.attachments, language)) { (AutoTranslate.messageIdsToWait as any)[message._id] = true; Messages.update({ _id: message._id }, { $set: { autoTranslateFetching: true } }); Meteor.call('autoTranslate.translateMessage', message, language); @@ -57,12 +65,19 @@ Meteor.startup(() => { const action = 'autoTranslateShowInverse' in message ? '$unset' : '$set'; Messages.update({ _id: message._id }, { [action]: { autoTranslateShowInverse: true } }); }, - condition({ message, user }) { + condition({ message, subscription, user }) { + const language = subscription?.autoTranslateLanguage || AutoTranslate.getLanguage(message.rid) || ''; if (!user) { return false; } - return Boolean(message?.u && message.u._id !== user._id && isTranslatedMessage(message) && !message.autoTranslateShowInverse); + return Boolean( + message?.u && + message.u._id !== user._id && + subscription?.autoTranslate && + !(message as { autoTranslateShowInverse?: boolean }).autoTranslateShowInverse && + (hasTranslationLanguageInMessage(message, language) || hasTranslationLanguageInAttachments(message.attachments, language)), + ); }, order: 90, }); diff --git a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts index 71ff7cc92c5a9..199df2e5db71f 100644 --- a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts +++ b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts @@ -9,10 +9,15 @@ import type { IUser, MessageAttachmentDefault, } from '@rocket.chat/core-typings'; +import { isTranslatedMessageAttachment } from '@rocket.chat/core-typings'; import { Subscriptions, Messages } from '../../../models/client'; import { hasPermission } from '../../../authorization/client'; import { call } from '../../../../client/lib/utils/call'; +import { + hasTranslationLanguageInAttachments, + hasTranslationLanguageInMessage, +} from '../../../../client/views/room/MessageList/lib/autoTranslate'; let userLanguage = 'en'; let username = ''; @@ -55,6 +60,9 @@ export const AutoTranslate = { language: string, autoTranslateShowInverse: boolean, ): MessageAttachmentDefault[] { + if (!isTranslatedMessageAttachment(attachments)) { + return attachments; + } for (const attachment of attachments) { if (attachment.author_name !== username) { if (attachment.text && attachment.translations && attachment.translations[language]) { @@ -134,16 +142,11 @@ export const createAutoTranslateMessageRenderer = (): ((message: ITranslatedMess message.translations = {}; } if (!!subscription?.autoTranslate !== !!message.autoTranslateShowInverse) { - const hasAttachmentsTranslate = - message.attachments?.some( - (attachment) => - 'translations' in attachment && - typeof attachment.translations === 'object' && - autoTranslateLanguage in attachment.translations, - ) ?? false; - message.translations.original = message.html; - if (message.translations[autoTranslateLanguage] && !hasAttachmentsTranslate) { + if ( + message.translations[autoTranslateLanguage] && + !hasTranslationLanguageInAttachments(message.attachments, autoTranslateLanguage) + ) { message.html = message.translations[autoTranslateLanguage]; } @@ -155,12 +158,6 @@ export const createAutoTranslateMessageRenderer = (): ((message: ITranslatedMess ); } } - } else if (message.attachments && message.attachments.length > 0) { - message.attachments = AutoTranslate.translateAttachments( - message.attachments, - autoTranslateLanguage, - !!message.autoTranslateShowInverse, - ); } return message; }; @@ -177,7 +174,8 @@ export const createAutoTranslateMessageStreamHandler = (): ((message: ITranslate subscription && subscription.autoTranslate === true && message.msg && - (!message.translations || !message.translations[language]) + (!message.translations || + (!hasTranslationLanguageInMessage(message, language) && !hasTranslationLanguageInAttachments(message.attachments, language))) ) { // || (message.attachments && !_.find(message.attachments, attachment => { return attachment.translations && attachment.translations[language]; })) Messages.update({ _id: message._id }, { $set: { autoTranslateFetching: true } }); diff --git a/apps/meteor/app/autotranslate/server/autotranslate.ts b/apps/meteor/app/autotranslate/server/autotranslate.ts index 8566cdd18e4a6..1d926fe5bb77a 100644 --- a/apps/meteor/app/autotranslate/server/autotranslate.ts +++ b/apps/meteor/app/autotranslate/server/autotranslate.ts @@ -67,7 +67,7 @@ export class TranslationProviderRegistry { return TranslationProviderRegistry.enabled ? TranslationProviderRegistry.getActiveProvider()?.getSupportedLanguages(target) : undefined; } - static translateMessage(message: IMessage, room: IRoom, targetLanguage: string): IMessage | undefined { + static translateMessage(message: IMessage, room: IRoom, targetLanguage?: string): IMessage | undefined { return TranslationProviderRegistry.enabled ? TranslationProviderRegistry.getActiveProvider()?.translateMessage(message, room, targetLanguage) : undefined; @@ -281,7 +281,7 @@ export abstract class AutoTranslate { * @param {object} targetLanguage * @returns {object} unmodified message object. */ - translateMessage(message: IMessage, room: IRoom, targetLanguage: string): IMessage { + translateMessage(message: IMessage, room: IRoom, targetLanguage?: string): IMessage { let targetLanguages: string[]; if (targetLanguage) { targetLanguages = [targetLanguage]; @@ -305,10 +305,13 @@ export abstract class AutoTranslate { Meteor.defer(() => { for (const [index, attachment] of message.attachments?.entries() ?? []) { if (attachment.description || attachment.text) { - const translations = this._translateAttachmentDescriptions(attachment, targetLanguages); + // Removes the initial link `[ ](quoterl)` from quote message before translation + const translatedText = attachment?.text?.replace(/\[(.*?)\]\(.*?\)/g, '$1') || attachment?.text; + const attachmentMessage = { ...attachment, text: translatedText }; + const translations = this._translateAttachmentDescriptions(attachmentMessage, targetLanguages); + if (!_.isEmpty(translations)) { Messages.addAttachmentTranslations(message._id, index, translations); - Messages.addTranslations(message._id, translations, TranslationProviderRegistry[Provider]); } } } diff --git a/apps/meteor/app/autotranslate/server/googleTranslate.ts b/apps/meteor/app/autotranslate/server/googleTranslate.ts index 91a068b880aaa..135601d48efcf 100644 --- a/apps/meteor/app/autotranslate/server/googleTranslate.ts +++ b/apps/meteor/app/autotranslate/server/googleTranslate.ts @@ -146,6 +146,7 @@ class GoogleAutoTranslate extends AutoTranslate { params: { key: this.apiKey, target: language, + format: 'text', }, query, }); @@ -190,6 +191,7 @@ class GoogleAutoTranslate extends AutoTranslate { params: { key: this.apiKey, target: language, + format: 'text', }, query, }); diff --git a/apps/meteor/app/discussion/server/methods/createDiscussion.js b/apps/meteor/app/discussion/server/methods/createDiscussion.ts similarity index 60% rename from apps/meteor/app/discussion/server/methods/createDiscussion.js rename to apps/meteor/app/discussion/server/methods/createDiscussion.ts index 212201f2b2937..e400940991389 100644 --- a/apps/meteor/app/discussion/server/methods/createDiscussion.js +++ b/apps/meteor/app/discussion/server/methods/createDiscussion.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; -import { Match } from 'meteor/check'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import type { IMessage, IRoom, IUser, MessageAttachmentDefault } from '@rocket.chat/core-typings'; import { hasAtLeastOnePermission, canSendMessage } from '../../../authorization/server'; import { Messages, Rooms } from '../../../models/server'; @@ -10,38 +10,58 @@ import { settings } from '../../../settings/server'; import { callbacks } from '../../../../lib/callbacks'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; -const getParentRoom = (rid) => { +const getParentRoom = (rid: IRoom['_id']) => { const room = Rooms.findOne(rid); return room && (room.prid ? Rooms.findOne(room.prid, { fields: { _id: 1 } }) : room); }; -const createDiscussionMessage = (rid, user, drid, msg, message_embedded) => { +const createDiscussionMessage = ( + rid: IRoom['_id'], + user: IUser, + drid: IRoom['_id'], + msg: IMessage['msg'], + messageEmbedded?: MessageAttachmentDefault, +): IMessage => { const welcomeMessage = { msg, rid, drid, - attachments: [message_embedded].filter((e) => e), + attachments: [messageEmbedded].filter((e) => e), }; - return Messages.createWithTypeRoomIdMessageAndUser('discussion-created', rid, '', user, welcomeMessage); + return Messages.createWithTypeRoomIdMessageAndUser('discussion-created', rid, '', user, welcomeMessage) as IMessage; }; -const mentionMessage = (rid, { _id, username, name }, message_embedded) => { +const mentionMessage = ( + rid: IRoom['_id'], + { _id, username, name }: Pick, + messageEmbedded?: MessageAttachmentDefault, +) => { const welcomeMessage = { rid, u: { _id, username, name }, ts: new Date(), _updatedAt: new Date(), - attachments: [message_embedded].filter((e) => e), + attachments: [messageEmbedded].filter((e) => e), }; return Messages.insert(welcomeMessage); }; -const create = ({ prid, pmid, t_name, reply, users, user, encrypted }) => { +type CreateDiscussionProperties = { + prid: IRoom['_id']; + pmid?: IMessage['_id']; + t_name: string; + reply?: string; + users: Array>; + user: IUser; + encrypted?: boolean; +}; + +const create = ({ prid, pmid, t_name: discussionName, reply, users, user, encrypted }: CreateDiscussionProperties) => { // if you set both, prid and pmid, and the rooms dont match... should throw an error) - let message = false; + let message: undefined | IMessage; if (pmid) { - message = Messages.findOne({ _id: pmid }); + message = Messages.findOne({ _id: pmid }) as IMessage | undefined; if (!message) { throw new Meteor.Error('error-invalid-message', 'Invalid message', { method: 'DiscussionCreation', @@ -49,7 +69,9 @@ const create = ({ prid, pmid, t_name, reply, users, user, encrypted }) => { } if (prid) { if (prid !== getParentRoom(message.rid)._id) { - throw new Meteor.Error('error-invalid-arguments', { method: 'DiscussionCreation' }); + throw new Meteor.Error('error-invalid-arguments', 'Root message room ID does not match parent room ID ', { + method: 'DiscussionCreation', + }); } } else { prid = message.rid; @@ -57,30 +79,24 @@ const create = ({ prid, pmid, t_name, reply, users, user, encrypted }) => { } if (!prid) { - throw new Meteor.Error('error-invalid-arguments', { method: 'DiscussionCreation' }); + throw new Meteor.Error('error-invalid-arguments', 'Missing parent room ID', { method: 'DiscussionCreation' }); } - let p_room; + let parentRoom; try { - p_room = canSendMessage(prid, { uid: user._id, username: user.username, type: user.type }); + parentRoom = canSendMessage(prid, { uid: user._id, username: user.username, type: user.type }); } catch (error) { - throw new Meteor.Error(error.message); + throw new Meteor.Error((error as Error).message); } - if (p_room.prid) { + if (parentRoom.prid) { throw new Meteor.Error('error-nested-discussion', 'Cannot create nested discussions', { method: 'DiscussionCreation', }); } - if (!Match.Maybe(encrypted, Boolean)) { - throw new Meteor.Error('error-invalid-arguments', 'Invalid encryption state', { - method: 'DiscussionCreation', - }); - } - if (typeof encrypted !== 'boolean') { - encrypted = p_room.encrypted; + encrypted = Boolean(parentRoom.encrypted); } if (encrypted && reply) { @@ -111,18 +127,24 @@ const create = ({ prid, pmid, t_name, reply, users, user, encrypted }) => { // auto invite the replied message owner const invitedUsers = message ? [message.u.username, ...users] : users; - const type = roomCoordinator.getRoomDirectives(p_room.t)?.getDiscussionType(); - const description = p_room.encrypted ? '' : message.msg; - const topic = p_room.name; + const type = roomCoordinator.getRoomDirectives(parentRoom.t)?.getDiscussionType(parentRoom); + const description = parentRoom.encrypted ? '' : message?.msg; + const topic = parentRoom.name; + + if (!type) { + throw new Meteor.Error('error-invalid-type', 'Cannot define discussion room type', { + method: 'DiscussionCreation', + }); + } const discussion = createRoom( type, name, - user.username, - [...new Set(invitedUsers)], + user.username as string, + [...new Set(invitedUsers)].filter(Boolean), false, { - fname: t_name, + fname: discussionName, description, // TODO discussions remove topic, // TODO discussions remove prid, @@ -130,23 +152,24 @@ const create = ({ prid, pmid, t_name, reply, users, user, encrypted }) => { }, { // overrides name validation to allow anything, because discussion's name is randomly generated - nameValidationRegex: /.*/, + nameValidationRegex: '.*', + creator: user._id, }, ); let discussionMsg; - if (pmid) { - if (p_room.encrypted) { + if (message) { + if (parentRoom.encrypted) { message.msg = TAPi18n.__('Encrypted_message'); } - mentionMessage(discussion._id, user, attachMessage(message, p_room)); + mentionMessage(discussion._id, user, attachMessage(message, parentRoom)); - discussionMsg = createDiscussionMessage(message.rid, user, discussion._id, t_name, attachMessage(message, p_room)); + discussionMsg = createDiscussionMessage(message.rid, user, discussion._id, discussionName, attachMessage(message, parentRoom)); } else { - discussionMsg = createDiscussionMessage(prid, user, discussion._id, t_name); + discussionMsg = createDiscussionMessage(prid, user, discussion._id, discussionName); } - callbacks.runAsync('afterSaveMessage', discussionMsg, p_room); + callbacks.runAsync('afterSaveMessage', discussionMsg, parentRoom); if (reply) { sendMessage(user, { msg: reply }, discussion); @@ -165,7 +188,7 @@ Meteor.methods({ * @param {string[]} users - users to be added * @param {boolean} encrypted - if the discussion's e2e encryption should be enabled. */ - createDiscussion({ prid, pmid, t_name, reply, users, encrypted }) { + createDiscussion({ prid, pmid, t_name: discussionName, reply, users, encrypted }: CreateDiscussionProperties) { if (!settings.get('Discussion_enabled')) { throw new Meteor.Error('error-action-not-allowed', 'You are not allowed to create a discussion', { method: 'createDiscussion' }); } @@ -181,6 +204,6 @@ Meteor.methods({ throw new Meteor.Error('error-action-not-allowed', 'You are not allowed to create a discussion', { method: 'createDiscussion' }); } - return create({ uid, prid, pmid, t_name, reply, users, user: Meteor.user(), encrypted }); + return create({ prid, pmid, t_name: discussionName, reply, users, user: Meteor.user() as IUser, encrypted }); }, }); diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index fea71e3811821..739a939b8c35b 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -264,7 +264,8 @@ export class E2ERoom extends Emitter { const decryptedKey = await decryptRSA(e2e.privateKey, groupKey); this.sessionKeyExportedString = toString(decryptedKey); } catch (error) { - return this.error('Error decrypting group key: ', error); + this.error('Error decrypting group key: ', error); + return false; } this.keyID = Base64.encode(this.sessionKeyExportedString).slice(0, 12); @@ -275,8 +276,11 @@ export class E2ERoom extends Emitter { // Key has been obtained. E2E is now in session. this.groupSessionKey = key; } catch (error) { - return this.error('Error importing group key: ', error); + this.error('Error importing group key: ', error); + return false; } + + return true; } async createGroupKey() { diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index cdb088de99d12..50a5fb1073103 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -136,6 +136,18 @@ class E2E extends Emitter { }); } + async acceptSuggestedKey(rid: string): Promise { + await APIClient.post('/v1/e2e.acceptSuggestedGroupKey', { + rid, + }); + } + + async rejectSuggestedKey(rid: string): Promise { + await APIClient.post('/v1/e2e.rejectSuggestedGroupKey', { + rid, + }); + } + getKeysFromLocalStorage(): KeyPair { return { public_key: Meteor._localStorage.getItem('public_key'), diff --git a/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts b/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts new file mode 100644 index 0000000000000..8b6d313e57e25 --- /dev/null +++ b/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts @@ -0,0 +1,29 @@ +import { Meteor } from 'meteor/meteor'; +import { Subscriptions } from '@rocket.chat/models'; + +export async function handleSuggestedGroupKey( + handle: 'accept' | 'reject', + rid: string, + userId: string | null, + method: string, +): Promise { + if (!userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method }); + } + + const sub = await Subscriptions.findOneByRoomIdAndUserId(rid, userId); + if (!sub) { + throw new Meteor.Error('error-subscription-not-found', 'Subscription not found', { method }); + } + + const suggestedKey = String(sub.E2ESuggestedKey ?? '').trim(); + if (!suggestedKey) { + throw new Meteor.Error('error-no-suggested-key-available', 'No suggested key available', { method }); + } + + if (handle === 'accept') { + await Subscriptions.setGroupE2EKey(sub._id, suggestedKey); + } + + await Subscriptions.unsetGroupE2ESuggestedKey(sub._id); +} diff --git a/apps/meteor/app/e2e/server/methods/updateGroupKey.js b/apps/meteor/app/e2e/server/methods/updateGroupKey.js deleted file mode 100644 index 9aae29003ddff..0000000000000 --- a/apps/meteor/app/e2e/server/methods/updateGroupKey.js +++ /dev/null @@ -1,17 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Subscriptions } from '../../../models/server'; - -Meteor.methods({ - 'e2e.updateGroupKey'(rid, uid, key) { - const mySub = Subscriptions.findOneByRoomIdAndUserId(rid, Meteor.userId()); - if (mySub) { - // I have a subscription to this room - const userSub = Subscriptions.findOneByRoomIdAndUserId(rid, uid); - if (userSub) { - // uid also has subscription to this room - return Subscriptions.updateGroupE2EKey(userSub._id, key); - } - } - }, -}); diff --git a/apps/meteor/app/e2e/server/methods/updateGroupKey.ts b/apps/meteor/app/e2e/server/methods/updateGroupKey.ts new file mode 100644 index 0000000000000..fd5c0b055adad --- /dev/null +++ b/apps/meteor/app/e2e/server/methods/updateGroupKey.ts @@ -0,0 +1,27 @@ +import { Meteor } from 'meteor/meteor'; +import { Subscriptions } from '@rocket.chat/models'; + +Meteor.methods({ + async 'e2e.updateGroupKey'(rid, uid, key) { + const userId = Meteor.userId(); + if (!userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'e2e.acceptSuggestedGroupKey' }); + } + + // I have a subscription to this room + const mySub = await Subscriptions.findOneByRoomIdAndUserId(rid, userId); + + if (mySub) { + // Setting the key to myself, can set directly to the final field + if (userId === uid) { + return Subscriptions.setGroupE2EKey(mySub._id, key); + } + + // uid also has subscription to this room + const userSub = await Subscriptions.findOneByRoomIdAndUserId(rid, uid); + if (userSub) { + return Subscriptions.setGroupE2ESuggestedKey(userSub._id, key); + } + } + }, +}); diff --git a/apps/meteor/app/emoji-custom/client/lib/emojiCustom.js b/apps/meteor/app/emoji-custom/client/lib/emojiCustom.js index a3df7135cc5ff..8ae032cede8cc 100644 --- a/apps/meteor/app/emoji-custom/client/lib/emojiCustom.js +++ b/apps/meteor/app/emoji-custom/client/lib/emojiCustom.js @@ -6,23 +6,18 @@ import { isSetNotNull } from './function-isSet'; import { RoomManager } from '../../../ui-utils/client'; import { emoji, EmojiPicker } from '../../../emoji/client'; import { CachedCollectionManager } from '../../../ui-cached-collection/client'; -import { APIClient } from '../../../utils/client'; +import { APIClient, getURL } from '../../../utils/client'; export const getEmojiUrlFromName = function (name, extension) { - Session.get; + if (name == null) { + return; + } const key = `emoji_random_${name}`; - let random = 0; - if (isSetNotNull(() => Session.keys[key])) { - random = Session.keys[key]; - } + const random = isSetNotNull(() => Session.keys[key]) ? Session.keys[key] : 0; - if (name == null) { - return; - } - const path = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ''; - return `${path}/emoji-custom/${encodeURIComponent(name)}.${extension}?_dc=${random}`; + return getURL(`/emoji-custom/${encodeURIComponent(name)}.${extension}?_dc=${random}`); }; export const deleteEmojiCustom = function (emojiData) { @@ -200,8 +195,6 @@ Meteor.startup(() => }; } } - - EmojiPicker.updateRecent('rocket'); } catch (e) { console.error('Error getting custom emoji', e); } diff --git a/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js b/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js index 8551d201410c0..f408d594c0a5a 100644 --- a/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js +++ b/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js @@ -82,28 +82,20 @@ Meteor.startup(function () { return; } - let fileUploadDate = undefined; - if (file.uploadDate != null) { - fileUploadDate = file.uploadDate.toUTCString(); - } + const fileUploadDate = file.uploadDate != null ? file.uploadDate.toUTCString() : undefined; const reqModifiedHeader = req.headers['if-modified-since']; - if (reqModifiedHeader != null) { - if (reqModifiedHeader === fileUploadDate) { - res.setHeader('Last-Modified', reqModifiedHeader); - res.writeHead(304); - res.end(); - return; - } + if (reqModifiedHeader != null && reqModifiedHeader === fileUploadDate) { + res.setHeader('Last-Modified', reqModifiedHeader); + res.writeHead(304); + res.end(); + return; } - res.setHeader('Cache-Control', 'public, max-age=0'); - res.setHeader('Expires', '-1'); - if (fileUploadDate != null) { - res.setHeader('Last-Modified', fileUploadDate); - } else { - res.setHeader('Last-Modified', new Date().toUTCString()); - } + res.setHeader('Cache-Control', 'public, max-age=31536000'); + res.setHeader('Last-Modified', fileUploadDate || new Date().toUTCString()); + res.setHeader('Content-Length', file.length); + if (/^svg$/i.test(params.emoji.split('.').pop())) { res.setHeader('Content-Type', 'image/svg+xml'); } else if (/^png$/i.test(params.emoji.split('.').pop())) { @@ -111,7 +103,6 @@ Meteor.startup(function () { } else { res.setHeader('Content-Type', 'image/jpeg'); } - res.setHeader('Content-Length', file.length); file.readStream.pipe(res); }), diff --git a/apps/meteor/app/emoji/client/emojiPicker.js b/apps/meteor/app/emoji/client/emojiPicker.js index 671698280cc16..dc94b461757a3 100644 --- a/apps/meteor/app/emoji/client/emojiPicker.js +++ b/apps/meteor/app/emoji/client/emojiPicker.js @@ -17,7 +17,12 @@ const emojiListByCategory = new ReactiveDict('emojiList'); const getEmojiElement = (emoji, image) => image && `
  • ${image}
  • `; -const createEmojiList = (category, actualTone) => { +// used as a function so `t` can use the user's language correctly when called +const loadMoreLink = () => `
  • ${t('Load_more')}
  • `; + +let customItems = 90; + +const createEmojiList = (category, actualTone, limit = null) => { const html = Object.values(emoji.packages) .map((emojiPackage) => { @@ -25,12 +30,24 @@ const createEmojiList = (category, actualTone) => { return; } - return emojiPackage.emojisByCategory[category] - .map((current) => { - const tone = actualTone > 0 && emojiPackage.toneList.hasOwnProperty(current) ? `_tone${actualTone}` : ''; - return getEmojiElement(current, emojiPackage.renderPicker(`:${current}${tone}:`)); - }) - .join(''); + const result = []; + + const total = emojiPackage.emojisByCategory[category].length; + + const listTotal = limit ? Math.min(limit, total) : total; + + for (let i = 0; i < listTotal; i++) { + const current = emojiPackage.emojisByCategory[category][i]; + + const tone = actualTone > 0 && emojiPackage.toneList.hasOwnProperty(current) ? `_tone${actualTone}` : ''; + result.push(getEmojiElement(current, emojiPackage.renderPicker(`:${current}${tone}:`))); + } + + if (limit > 0 && total > limit) { + result.push(loadMoreLink()); + } + + return result.join(''); }) .join('') || `
  • ${t('No_emojis_found')}
  • `; @@ -38,25 +55,28 @@ const createEmojiList = (category, actualTone) => { }; export function updateRecentEmoji(category) { - emojiListByCategory.set(category, createEmojiList(category)); + emojiListByCategory.set(category, createEmojiList(category, null, category === 'rocket' ? customItems : null)); } const createPickerEmojis = (instance) => { const categories = instance.categoriesList; const actualTone = instance.tone; - categories.forEach((category) => emojiListByCategory.set(category.key, createEmojiList(category.key, actualTone))); + categories.forEach((category) => + emojiListByCategory.set(category.key, createEmojiList(category.key, actualTone, category.key === 'rocket' ? customItems : null)), + ); }; -function getEmojisBySearchTerm(searchTerm) { +function getEmojisBySearchTerm(searchTerm, limit) { let html = '
      '; - const t = Template.instance(); - const actualTone = t.tone; + const actualTone = Template.instance().tone; EmojiPicker.currentCategory.set(''); const searchRegExp = new RegExp(escapeRegExp(searchTerm.replace(/:/g, '')), 'i'); + let totalFound = 0; + for (let current in emoji.list) { if (!emoji.list.hasOwnProperty(current)) { continue; @@ -89,8 +109,14 @@ function getEmojisBySearchTerm(searchTerm) { if (emojiFound) { const image = emoji.packages[emojiPackage].renderPicker(`:${current}${tone}:`); html += getEmojiElement(current, image); + totalFound++; } } + + if (totalFound >= limit) { + html += loadMoreLink(); + break; + } } html += '
    '; @@ -116,7 +142,7 @@ Template.emojiPicker.helpers({ return Template.instance().currentSearchTerm.get().length > 0; }, searchResults() { - return getEmojisBySearchTerm(Template.instance().currentSearchTerm.get()); + return getEmojisBySearchTerm(Template.instance().currentSearchTerm.get(), Template.instance().searchTermItems.get()); }, emojiList(category) { return emojiListByCategory.get(category); @@ -190,6 +216,18 @@ Template.emojiPicker.events({ instance.$('.tone-selector').toggleClass('show'); }, + 'click .emoji-picker-load-more > a'(event, instance) { + event.stopPropagation(); + event.preventDefault(); + + if (instance.currentSearchTerm.get().length > 0) { + instance.searchTermItems.set(instance.searchTermItems.get() + 90); + return; + } + + customItems += 90; + emojiListByCategory.set('rocket', createEmojiList('rocket', 0, customItems)); + }, 'click .tone-selector .tone'(event, instance) { event.stopPropagation(); event.preventDefault(); @@ -241,6 +279,7 @@ Template.emojiPicker.events({ input.val(''); } instance.currentSearchTerm.set(''); + instance.searchTermItems.set(90); EmojiPicker.pickEmoji(_emoji + tone); }, @@ -266,6 +305,7 @@ Template.emojiPicker.onCreated(function () { const recent = EmojiPicker.getRecent(); this.currentSearchTerm = new ReactiveVar(''); + this.searchTermItems = new ReactiveVar(90); this.categoriesList = []; for (const emojiPackage in emoji.packages) { diff --git a/apps/meteor/app/emoji/client/lib/EmojiPicker.js b/apps/meteor/app/emoji/client/lib/EmojiPicker.js index c318f0e3c8de4..55afab1ba11ef 100644 --- a/apps/meteor/app/emoji/client/lib/EmojiPicker.js +++ b/apps/meteor/app/emoji/client/lib/EmojiPicker.js @@ -6,6 +6,7 @@ import { ReactiveVar } from 'meteor/reactive-var'; import { Tracker } from 'meteor/tracker'; import { emoji } from '../../lib/rocketchat'; +import { updateRecentEmoji } from '../emojiPicker'; let updatePositions = true; @@ -93,6 +94,10 @@ export const EmojiPicker = { return $('.emoji-picker').css(cssProperties); }, + /** + * @param {Element} source + * @param {(emoji: string) => void} callback + */ async open(source, callback) { if (!this.initiated) { await this.init(); @@ -152,9 +157,7 @@ export const EmojiPicker = { this.recent.splice(pos, 1); Meteor._localStorage.setItem('emoji.recent', this.recent); }, - async updateRecent(category) { - const emojiPickerImport = await import('../emojiPicker'); - const { updateRecentEmoji } = emojiPickerImport; + updateRecent(category) { updateRecentEmoji(category); }, calculateCategoryPositions() { diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.js b/apps/meteor/app/file-upload/server/lib/FileUpload.js index 14c47463dd4e0..ffd263db6f932 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.js +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.js @@ -337,7 +337,7 @@ export const FileUpload = { }, uploadsOnValidate(file) { - if (!/^image\/((x-windows-)?bmp|p?jpeg|png|gif)$/.test(file.type)) { + if (!/^image\/((x-windows-)?bmp|p?jpeg|png|gif|webp)$/.test(file.type)) { return; } diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index ccba5bbc24d73..8df934acacf37 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -8,6 +8,7 @@ import { FileUpload } from '../lib/FileUpload'; import { canAccessRoom } from '../../../authorization/server/functions/canAccessRoom'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { omit } from '../../../../lib/utils/omit'; +import { getFileExtension } from '../../../../lib/utils/getFileExtension'; function validateFileRequiredFields(file: Partial): asserts file is AtLeast { const requiredFields = ['_id', 'name', 'type', 'size']; @@ -106,9 +107,11 @@ export const parseFileIntoMessageAttachments = async ( const attachment = { title: file.name, type: 'file', + format: getFileExtension(file.name), description: file.description, title_link: fileUrl, title_link_download: true, + size: file.size as number, }; attachments.push(attachment); } diff --git a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.js b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.js index d01717912f3b1..de4fc6c0c3b3a 100644 --- a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.js +++ b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.js @@ -63,14 +63,14 @@ export function getMentions(message) { const incGroupMentions = (rid, roomType, excludeUserId, unreadCount) => { const incUnreadByGroup = ['all_messages', 'group_mentions_only', 'user_and_group_mentions_only'].includes(unreadCount); - const incUnread = roomType === 'd' || incUnreadByGroup ? 1 : 0; + const incUnread = roomType === 'd' || roomType === 'l' || incUnreadByGroup ? 1 : 0; Subscriptions.incGroupMentionsAndUnreadForRoomIdExcludingUserId(rid, excludeUserId, 1, incUnread); }; const incUserMentions = (rid, roomType, uids, unreadCount) => { const incUnreadByUser = ['all_messages', 'user_mentions_only', 'user_and_group_mentions_only'].includes(unreadCount); - const incUnread = roomType === 'd' || incUnreadByUser ? 1 : 0; + const incUnread = roomType === 'd' || roomType === 'l' || incUnreadByUser ? 1 : 0; Subscriptions.incUserMentionsAndUnreadForRoomIdAndUserIds(rid, uids, 1, incUnread); }; @@ -86,6 +86,26 @@ const getUserIdsFromHighlights = (rid, message) => { .map(({ u: { _id: uid } }) => uid); }; +/* + * {IRoom['t']} roomType - The type of the room + * @returns {string} - The setting value for unread count + */ +const getUnreadSettingCount = (roomType) => { + let unreadSetting = 'Unread_Count'; + switch (roomType) { + case 'd': { + unreadSetting = 'Unread_Count_DM'; + break; + } + case 'l': { + unreadSetting = 'Unread_Count_Omni'; + break; + } + } + + return settings.get(unreadSetting); +}; + export async function updateUsersSubscriptions(message, room) { // Don't increase unread counter on thread messages if (room != null && !message.tmid) { @@ -93,8 +113,7 @@ export async function updateUsersSubscriptions(message, room) { const userIds = new Set(mentionIds); - const unreadSetting = room.t === 'd' ? 'Unread_Count_DM' : 'Unread_Count'; - const unreadCount = settings.get(unreadSetting); + const unreadCount = getUnreadSettingCount(room.t); getUserIdsFromHighlights(room._id, message).forEach((uid) => userIds.add(uid)); diff --git a/apps/meteor/app/lib/server/startup/settings.ts b/apps/meteor/app/lib/server/startup/settings.ts index 2a3b25cd97718..4e45541acdce8 100644 --- a/apps/meteor/app/lib/server/startup/settings.ts +++ b/apps/meteor/app/lib/server/startup/settings.ts @@ -945,6 +945,20 @@ settingsRegistry.addGroup('General', function () { ], public: true, }); + this.add('Unread_Count_Omni', 'all_messages', { + type: 'select', + values: [ + { + key: 'all_messages', + i18nLabel: 'All_messages', + }, + { + key: 'mentions_only', + i18nLabel: 'Mentions_only', + }, + ], + public: true, + }); this.add('DeepLink_Url', 'https://go.rocket.chat', { type: 'string', diff --git a/apps/meteor/app/mentions-flextab/client/actionButton.ts b/apps/meteor/app/mentions-flextab/client/actionButton.ts index 33bfff0ac86e8..aa39b619f3d49 100644 --- a/apps/meteor/app/mentions-flextab/client/actionButton.ts +++ b/apps/meteor/app/mentions-flextab/client/actionButton.ts @@ -26,7 +26,7 @@ Meteor.startup(function () { tab: 'thread', context: message.tmid, rid: message.rid, - name: Rooms.findOne({ _id: message.rid }).name, + name: Rooms.findOne({ _id: message.rid })?.name ?? '', }, { jump: message._id, diff --git a/apps/meteor/app/mentions-flextab/client/views/mentionsFlexTab.js b/apps/meteor/app/mentions-flextab/client/views/mentionsFlexTab.js index 92b10b8a57d06..6ae77a09fa24a 100644 --- a/apps/meteor/app/mentions-flextab/client/views/mentionsFlexTab.js +++ b/apps/meteor/app/mentions-flextab/client/views/mentionsFlexTab.js @@ -4,7 +4,7 @@ import { Mongo } from 'meteor/mongo'; import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; -import { messageContext } from '../../../ui-utils/client/lib/messageContext'; +import { createMessageContext } from '../../../ui-utils/client/lib/messageContext'; import { upsertMessageBulk } from '../../../ui-utils/client/lib/RoomHistoryManager'; import { APIClient } from '../../../utils/client'; import { Messages, Users } from '../../../models/client'; @@ -23,7 +23,7 @@ Template.mentionsFlexTab.helpers({ hasMore() { return Template.instance().hasMore.get(); }, - messageContext, + messageContext: createMessageContext, }); Template.mentionsFlexTab.onCreated(function () { diff --git a/apps/meteor/app/message-pin/client/actionButton.ts b/apps/meteor/app/message-pin/client/actionButton.ts index 7cf76926263d5..a661ba0c0b7a6 100644 --- a/apps/meteor/app/message-pin/client/actionButton.ts +++ b/apps/meteor/app/message-pin/client/actionButton.ts @@ -83,7 +83,7 @@ Meteor.startup(function () { context: message.tmid, rid: message.rid, jump: message._id, - name: Rooms.findOne({ _id: message.rid }).name, + name: Rooms.findOne({ _id: message.rid })?.name ?? '', }, { jump: message._id, diff --git a/apps/meteor/app/message-pin/client/views/pinnedMessages.js b/apps/meteor/app/message-pin/client/views/pinnedMessages.js index 75fc482f913c2..f5843f32d42e8 100644 --- a/apps/meteor/app/message-pin/client/views/pinnedMessages.js +++ b/apps/meteor/app/message-pin/client/views/pinnedMessages.js @@ -4,7 +4,7 @@ import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; import { upsertMessageBulk } from '../../../ui-utils/client/lib/RoomHistoryManager'; -import { messageContext } from '../../../ui-utils/client/lib/messageContext'; +import { createMessageContext } from '../../../ui-utils/client/lib/messageContext'; import { APIClient } from '../../../utils/client'; import { Messages } from '../../../models/client'; import { getCommonRoomEvents } from '../../../ui/client/views/app/lib/getCommonRoomEvents'; @@ -22,7 +22,7 @@ Template.pinnedMessages.helpers({ hasMore() { return Template.instance().hasMore.get(); }, - messageContext, + messageContext: createMessageContext, }); Template.pinnedMessages.onCreated(function () { diff --git a/apps/meteor/app/message-snippet/client/tabBar/views/snippetedMessages.js b/apps/meteor/app/message-snippet/client/tabBar/views/snippetedMessages.js index 4ee7fee88472a..3166433bb5279 100644 --- a/apps/meteor/app/message-snippet/client/tabBar/views/snippetedMessages.js +++ b/apps/meteor/app/message-snippet/client/tabBar/views/snippetedMessages.js @@ -3,7 +3,7 @@ import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; import { Mongo } from 'meteor/mongo'; -import { messageContext } from '../../../../ui-utils/client/lib/messageContext'; +import { createMessageContext } from '../../../../ui-utils/client/lib/messageContext'; import { APIClient } from '../../../../utils/client'; import { Messages } from '../../../../models/client'; import { upsertMessageBulk } from '../../../../ui-utils/client/lib/RoomHistoryManager'; @@ -21,7 +21,7 @@ Template.snippetedMessages.helpers({ hasMore() { return Template.instance().hasMore.get(); }, - messageContext, + messageContext: createMessageContext, }); Template.snippetedMessages.onCreated(function () { diff --git a/apps/meteor/app/message-star/client/actionButton.ts b/apps/meteor/app/message-star/client/actionButton.ts index 03e8aa3aa3b63..8a37b39b35076 100644 --- a/apps/meteor/app/message-star/client/actionButton.ts +++ b/apps/meteor/app/message-star/client/actionButton.ts @@ -83,7 +83,7 @@ Meteor.startup(function () { context: message.tmid, rid: message.rid, jump: message._id, - name: Rooms.findOne({ _id: message.rid }).name, + name: Rooms.findOne({ _id: message.rid })?.name ?? '', }, { jump: message._id, diff --git a/apps/meteor/app/message-star/client/views/starredMessages.js b/apps/meteor/app/message-star/client/views/starredMessages.js index 8b44b85e3f7f1..4c486444036f5 100644 --- a/apps/meteor/app/message-star/client/views/starredMessages.js +++ b/apps/meteor/app/message-star/client/views/starredMessages.js @@ -4,7 +4,7 @@ import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; import { Mongo } from 'meteor/mongo'; -import { messageContext } from '../../../ui-utils/client/lib/messageContext'; +import { createMessageContext } from '../../../ui-utils/client/lib/messageContext'; import { Messages } from '../../../models/client'; import { upsertMessageBulk } from '../../../ui-utils/client/lib/RoomHistoryManager'; import { APIClient } from '../../../utils/client'; @@ -23,7 +23,7 @@ Template.starredMessages.helpers({ hasMore() { return Template.instance().hasMore.get(); }, - messageContext, + messageContext: createMessageContext, }); Template.starredMessages.onCreated(function () { diff --git a/apps/meteor/app/models/client/index.ts b/apps/meteor/app/models/client/index.ts index 641f0a00ba822..70229d8b2bc32 100644 --- a/apps/meteor/app/models/client/index.ts +++ b/apps/meteor/app/models/client/index.ts @@ -30,7 +30,7 @@ const Subscriptions = _.extend({}, subscriptions, ChatSubscription); /** @deprecated */ const Messages = _.extend({}, ChatMessage) as typeof ChatMessage; /** @deprecated */ -const Rooms = _.extend({}, ChatRoom); +const Rooms = _.extend({}, ChatRoom) as typeof ChatRoom; export { Base, diff --git a/apps/meteor/app/models/client/models/ChatMessage.ts b/apps/meteor/app/models/client/models/ChatMessage.ts index 93cb48d3f0383..a3d0356ef8ddd 100644 --- a/apps/meteor/app/models/client/models/ChatMessage.ts +++ b/apps/meteor/app/models/client/models/ChatMessage.ts @@ -6,14 +6,6 @@ class ChatMessageCollection extends Mongo.Collection) { const query = { rid, diff --git a/apps/meteor/app/models/client/models/ChatRoom.js b/apps/meteor/app/models/client/models/ChatRoom.js index c0fbba1ff2862..a8b849a07e2f3 100644 --- a/apps/meteor/app/models/client/models/ChatRoom.js +++ b/apps/meteor/app/models/client/models/ChatRoom.js @@ -1,5 +1,6 @@ import { CachedChatRoom } from './CachedChatRoom'; +/** @type {import('meteor/mongo').Mongo.Collection} */ export const ChatRoom = CachedChatRoom.collection; ChatRoom.setReactionsInLastMessage = function (roomId, lastMessage) { diff --git a/apps/meteor/app/models/server/models/Subscriptions.js b/apps/meteor/app/models/server/models/Subscriptions.js index aa8773bb83658..42d12daca9620 100644 --- a/apps/meteor/app/models/server/models/Subscriptions.js +++ b/apps/meteor/app/models/server/models/Subscriptions.js @@ -347,13 +347,6 @@ export class Subscriptions extends Base { return this.find(query, options); } - updateGroupE2EKey(_id, key) { - const query = { _id }; - const update = { $set: { E2EKey: key } }; - this.update(query, update); - return this.findOne({ _id }); - } - /** * @param {IRole['_id'][]} roles * @param {string} scope the value for the role scope (room id) diff --git a/apps/meteor/app/reactions/client/init.js b/apps/meteor/app/reactions/client/init.js index dcd3b9564a2eb..009eeb44b5fef 100644 --- a/apps/meteor/app/reactions/client/init.js +++ b/apps/meteor/app/reactions/client/init.js @@ -1,55 +1,10 @@ import { Meteor } from 'meteor/meteor'; -import { Blaze } from 'meteor/blaze'; -import { Rooms, Subscriptions } from '../../models/client'; import { MessageAction } from '../../ui-utils'; import { messageArgs } from '../../../client/lib/utils/messageArgs'; import { EmojiPicker } from '../../emoji'; import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; -export const EmojiEvents = { - 'click .add-reaction'(event) { - event.preventDefault(); - event.stopPropagation(); - const data = Blaze.getData(event.currentTarget); - const { - msg: { rid, _id: mid, private: isPrivate }, - } = messageArgs(data); - const user = Meteor.user(); - const room = Rooms.findOne({ _id: rid }); - - if (!room) { - return false; - } - - if (!Subscriptions.findOne({ rid })) { - return false; - } - - if (isPrivate) { - return false; - } - - if (roomCoordinator.readOnly(room._id, user) && !room.reactWhenReadOnly) { - return false; - } - - EmojiPicker.open(event.currentTarget, (emoji) => { - Meteor.call('setReaction', `:${emoji}:`, mid); - }); - }, - - 'click .reactions > li:not(.add-reaction)'(event) { - event.preventDefault(); - - const data = Blaze.getData(event.currentTarget); - const { - msg: { _id: mid }, - } = messageArgs(data); - Meteor.call('setReaction', $(event.currentTarget).attr('data-emoji'), mid); - }, -}; - Meteor.startup(function () { MessageAction.addButton({ id: 'reaction-message', diff --git a/apps/meteor/app/reactions/client/methods/setReaction.js b/apps/meteor/app/reactions/client/methods/setReaction.js index 26286e4e11303..75ec87148f6f6 100644 --- a/apps/meteor/app/reactions/client/methods/setReaction.js +++ b/apps/meteor/app/reactions/client/methods/setReaction.js @@ -42,10 +42,10 @@ Meteor.methods({ if (_.isEmpty(message.reactions)) { delete message.reactions; - Messages.unsetReactions(messageId); + Messages.update({ _id: messageId }, { $unset: { reactions: 1 } }); callbacks.run('unsetReaction', messageId, reaction); } else { - Messages.setReactions(messageId, message.reactions); + Messages.update({ _id: messageId }, { $set: { reactions: message.reactions } }); callbacks.run('setReaction', messageId, reaction); } } else { @@ -59,7 +59,7 @@ Meteor.methods({ } message.reactions[reaction].usernames.push(user.username); - Messages.setReactions(messageId, message.reactions); + Messages.update({ _id: messageId }, { $set: { reactions: message.reactions } }); callbacks.run('setReaction', messageId, reaction); } }, diff --git a/apps/meteor/app/search/client/provider/result.js b/apps/meteor/app/search/client/provider/result.js index 4749b5d0f6844..804c808ce05ef 100644 --- a/apps/meteor/app/search/client/provider/result.js +++ b/apps/meteor/app/search/client/provider/result.js @@ -6,7 +6,7 @@ import { Session } from 'meteor/session'; import { Template } from 'meteor/templating'; import _ from 'underscore'; -import { messageContext } from '../../../ui-utils/client/lib/messageContext'; +import { createMessageContext } from '../../../ui-utils/client/lib/messageContext'; import { MessageAction, RoomHistoryManager } from '../../../ui-utils'; import { messageArgs } from '../../../../client/lib/utils/messageArgs'; import { Rooms } from '../../../models/client'; @@ -125,7 +125,7 @@ Template.DefaultSearchResultTemplate.helpers({ return { customClass: 'search', actionContext: 'search', ...msg, groupable: false }; }, messageContext() { - const result = messageContext.call(this, { rid: Session.get('openedRoom') }); + const result = createMessageContext.call(this, { rid: Session.get('openedRoom') }); return { ...result, settings: { diff --git a/apps/meteor/app/theme/client/imports/components/emojiPicker.css b/apps/meteor/app/theme/client/imports/components/emojiPicker.css index 8374e4874eeca..817f4b2852d95 100644 --- a/apps/meteor/app/theme/client/imports/components/emojiPicker.css +++ b/apps/meteor/app/theme/client/imports/components/emojiPicker.css @@ -101,6 +101,10 @@ } } + & li.emoji-picker-load-more { + text-align: center; + } + &.visible { display: block; } diff --git a/apps/meteor/app/theme/client/imports/general/theme_old.css b/apps/meteor/app/theme/client/imports/general/theme_old.css index d06a342dfe149..b1ab49cd06c41 100644 --- a/apps/meteor/app/theme/client/imports/general/theme_old.css +++ b/apps/meteor/app/theme/client/imports/general/theme_old.css @@ -55,18 +55,6 @@ color: var(--success-color); } -.pending-color { - color: var(--pending-color); -} - -.pending-background { - background-color: var(--pending-background); -} - -.pending-border { - border-color: var(--pending-border); -} - .error-color { color: var(--error-color); } @@ -83,10 +71,6 @@ background-color: var(--attention-color); } -.color-tertiary-font-color { - color: var(--tertiary-font-color); -} - .error-background { background-color: var(--error-background); } @@ -95,10 +79,6 @@ border-color: var(--error-border); } -.background-transparent-darkest { - background-color: var(--transparent-darkest); -} - .background-transparent-darker { background-color: var(--transparent-darker); } @@ -123,22 +103,6 @@ border-color: var(--transparent-dark); } -.background-transparent-light { - background-color: var(--transparent-light); -} - -.border-transparent-lighter { - border-color: var(--transparent-lighter); -} - -.background-transparent-lightest { - background-color: var(--transparent-lightest); -} - -.color-primary-action-contrast { - color: var(--primary-action-contrast); -} - * { -webkit-overflow-scrolling: touch; @@ -255,25 +219,6 @@ textarea { border-color: var(--error-color); } -.admin-table-row { - background-color: var(--transparent-light); - - &:nth-of-type(even) { - background-color: var(--transparent-lightest); - } -} - -.full-page, -.page-loading { - a { - color: var(--tertiary-font-color); - } - - a:hover { - color: var(--primary-background-contrast); - } -} - #login-card { .input-text { input:-webkit-autofill { @@ -290,15 +235,6 @@ textarea { background-color: var(--message-box-editing-color); } -.rc-old { - & .popup-item { - &.selected { - color: var(--primary-action-contrast); - background-color: var(--primary-action-color); - } - } -} - .messages-box { &.selectable .selected { background-color: var(--selection-background); diff --git a/apps/meteor/app/theme/client/imports/general/variables.css b/apps/meteor/app/theme/client/imports/general/variables.css index d823c19475dcf..5195c8a8b0610 100644 --- a/apps/meteor/app/theme/client/imports/general/variables.css +++ b/apps/meteor/app/theme/client/imports/general/variables.css @@ -2,14 +2,6 @@ /* * Color palette */ - --color-dark-100: #0c0d0f; - --color-dark-90: #1e232a; - --color-dark-80: #2e343e; - --color-dark-70: #53585f; - --color-dark-30: #9da2a9; - --color-dark-20: #caced1; - --color-dark-10: #e0e5e8; - --color-dark-05: #f1f2f4; --color-dark-blue: #175cc4; --color-blue: #1d74f5; --color-light-blue: #4eb2f5; @@ -21,7 +13,6 @@ --color-yellow: #ffd21f; --color-dark-yellow: #f6c502; --color-green: #2de0a5; - --color-dark-green: #26d198; /* * General Colors diff --git a/apps/meteor/app/theme/server/Theme.ts b/apps/meteor/app/theme/server/Theme.ts deleted file mode 100644 index 7c7653ae2aade..0000000000000 --- a/apps/meteor/app/theme/server/Theme.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { performance } from 'perf_hooks'; - -import { Meteor } from 'meteor/meteor'; -import less from 'less'; -import AutoPrefixerLessPlugin from 'less-plugin-autoprefixer'; -import { Settings } from '@rocket.chat/models'; -import type { ISetting, ISettingColor } from '@rocket.chat/core-typings'; - -import { withDebouncing } from '../../../lib/utils/highOrderFunctions'; -import { settingsRegistry, settings } from '../../settings/server'; -import type { Logger } from '../../logger/server'; - -export class Theme { - private variables: Record< - string, - { - type: 'font' | 'color'; - value: unknown; - editor?: ISettingColor['editor']; - } - > = {}; - - private customCSS = ''; - - private compileDelayed: () => void = withDebouncing({ wait: 100 })(Meteor.bindEnvironment(this.compile.bind(this))); - - private logger: Logger; - - public constructor({ logger }: { logger: Logger }) { - this.logger = logger; - this.watchSettings(); - } - - private watchSettings() { - settingsRegistry.add('css', ''); - settingsRegistry.addGroup('Layout'); - - settings.watchByRegex(/^theme-./, (key, value) => { - if (key === 'theme-custom-css' && !!value) { - this.customCSS = String(value); - } else { - const name = key.replace(/^theme-[a-z]+-/, ''); - if (this.variables[name]) { - this.variables[name].value = value; - } - } - - this.compileDelayed(); - }); - } - - private compile() { - const options: Less.Options = { - compress: true, - plugins: [new AutoPrefixerLessPlugin()], - }; - - const start = performance.now(); - - less.render(this.customCSS, options, (err, data) => { - this.logger.info({ stop_rendering: performance.now() - start }); - - if (err) { - this.logger.error(err); - return; - } - - Settings.updateValueById('css', data?.css); - - Meteor.startup(() => { - Meteor.setTimeout(() => { - process.emit('message', { refresh: 'client' }); - }, 200); - }); - }); - } - - public addVariable(type: 'font', name: string, value: ISetting['value'], section: ISetting['section'], persist?: boolean): void; - - public addVariable( - type: 'color', - name: string, - value: ISettingColor['value'], - section: ISettingColor['section'], - persist: boolean, - editor: ISettingColor['editor'], - ): void; - - public addVariable( - type: 'font' | 'color', - name: string, - value: ISetting['value'], - section: ISetting['section'], - persist = true, - editor?: ISettingColor['editor'], - ) { - this.variables[name] = { - type, - value, - editor, - }; - - if (!persist) { - return; - } - - // TODO: this is a hack to make the type checker happy - const config: Partial & { allowedTypes?: ['color', 'expression'] } = { - group: 'Layout', - type, - section, - public: true, - ...(type === 'color' && { - editor, - allowedTypes: ['color', 'expression'], - }), - }; - - settingsRegistry.add(`theme-${type}-${name}`, value, config); - } - - public getCss() { - return String(settings.get('css') || ''); - } -} diff --git a/apps/meteor/app/theme/server/server.ts b/apps/meteor/app/theme/server/server.ts index e8d29cf55bc75..06674b619405c 100644 --- a/apps/meteor/app/theme/server/server.ts +++ b/apps/meteor/app/theme/server/server.ts @@ -1,19 +1,25 @@ import crypto from 'crypto'; +import { Meteor } from 'meteor/meteor'; +import { Settings } from '@rocket.chat/models'; import { WebApp } from 'meteor/webapp'; import { settings } from '../../settings/server'; -import { Logger } from '../../logger/server'; import { addStyle } from '../../ui-master/server/inject'; -import { Theme } from './Theme'; -const logger = new Logger('rocketchat:theme'); - -export const theme = new Theme({ logger }); +settings.watch('theme-custom-css', (value) => { + if (!value || typeof value !== 'string') { + addStyle('css-theme', ''); + return; + } + addStyle('css-theme', value); +}); -settings.watch('css', () => { - addStyle('css-theme', theme.getCss()); - process.emit('message', { refresh: 'client' }); +// TODO: Add a migration to remove this setting from the database +Meteor.startup(() => { + Settings.deleteMany({ _id: /theme-color/ }); + Settings.deleteOne({ _id: /theme-font/ }); + Settings.deleteOne({ _id: 'css' }); }); WebApp.rawConnectHandlers.use((req, res, next) => { @@ -25,10 +31,13 @@ WebApp.rawConnectHandlers.use((req, res, next) => { return; } - const data = theme.getCss(); + const style = settings.get('theme-custom-css'); + if (typeof style !== 'string') { + throw new Error('Invalid theme-custom-css setting'); + } res.setHeader('Content-Type', 'text/css; charset=UTF-8'); - res.setHeader('Content-Length', data.length); - res.setHeader('ETag', `"${crypto.createHash('sha1').update(data).digest('hex')}"`); - res.end(data, 'utf-8'); + res.setHeader('Content-Length', style.length); + res.setHeader('ETag', `"${crypto.createHash('sha1').update(style).digest('hex')}"`); + res.end(style, 'utf-8'); }); diff --git a/apps/meteor/app/theme/server/variables.ts b/apps/meteor/app/theme/server/variables.ts index 21600d34489f9..93fe0d75bffb3 100644 --- a/apps/meteor/app/theme/server/variables.ts +++ b/apps/meteor/app/theme/server/variables.ts @@ -1,53 +1,4 @@ -import { SettingEditor } from '@rocket.chat/core-typings'; - -import { theme } from './server'; import { settingsRegistry } from '../../settings/server'; -// TODO: Define registers/getters/setters for packages to work with established -// heirarchy of colors instead of making duplicate definitions -// TODO: Settings pages to show simple separation of major/minor/addon colors -// TODO: Add setting toggle to use defaults for minor colours and hide settings - -// New colors, used for shades on solid backgrounds -// Defined range of transparencies reduces random colour variances -// Major colors form the core of the scheme -// Names changed to reflect usage, comments show pre-refactor names - -const variablesContent = Assets.getText('client/imports/general/variables.css') ?? ''; - -const regionRegex = /\/\*\s*#region\s+([^ ]*?)\s+(.*?)\s*\*\/([^]*?)\/\*\s*#endregion\s*\*\//gim; - -for (let matches = regionRegex.exec(variablesContent); matches; matches = regionRegex.exec(variablesContent)) { - const [, type, section, content] = matches; - [...(content.match(/--(.*?):\s*(.*?);/gim) ?? [])].forEach((entry) => { - const [, name, value] = /--(.*?):\s*(.*?);/im.exec(entry) ?? []; - - if (type === 'fonts') { - theme.addVariable('font', name, value, 'Fonts', true); - return; - } - - if (type === 'colors') { - if (/var/.test(value)) { - const [, variableName] = value.match(/var\(--(.*?)\)/i) ?? []; - theme.addVariable('color', name, variableName, section, true, SettingEditor.EXPRESSION); - return; - } - - theme.addVariable('color', name, value, section, true, SettingEditor.COLOR); - return; - } - - if (type === 'less-colors') { - if (/var/.test(value)) { - const [, variableName] = value.match(/var\(--(.*?)\)/i) ?? []; - theme.addVariable('color', name, `@${variableName}`, section, true, SettingEditor.EXPRESSION); - return; - } - - theme.addVariable('color', name, value, section, true, SettingEditor.COLOR); - } - }); -} settingsRegistry.add('theme-custom-css', '', { group: 'Layout', diff --git a/apps/meteor/app/ui-login/username/username.html b/apps/meteor/app/ui-login/username/username.html index 247777adde727..21608c45929fe 100644 --- a/apps/meteor/app/ui-login/username/username.html +++ b/apps/meteor/app/ui-login/username/username.html @@ -1,5 +1,5 @@