diff --git a/.changeset/bump-patch-1732728542480.md b/.changeset/bump-patch-1732728542480.md new file mode 100644 index 000000000000..e1eaa7980afb --- /dev/null +++ b/.changeset/bump-patch-1732728542480.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1732835767114.md b/.changeset/bump-patch-1732835767114.md new file mode 100644 index 000000000000..e1eaa7980afb --- /dev/null +++ b/.changeset/bump-patch-1732835767114.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1733170421495.md b/.changeset/bump-patch-1733170421495.md new file mode 100644 index 000000000000..e1eaa7980afb --- /dev/null +++ b/.changeset/bump-patch-1733170421495.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/clean-flies-collect.md b/.changeset/clean-flies-collect.md new file mode 100644 index 000000000000..4921355b51e8 --- /dev/null +++ b/.changeset/clean-flies-collect.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fix user highlights not matching only whole words diff --git a/.changeset/curvy-flies-greet.md b/.changeset/curvy-flies-greet.md new file mode 100644 index 000000000000..aeac8382b152 --- /dev/null +++ b/.changeset/curvy-flies-greet.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Remove unused client side `setUserActiveStatus` meteor method. diff --git a/.changeset/fair-colts-remain.md b/.changeset/fair-colts-remain.md new file mode 100644 index 000000000000..7ce003e50fd5 --- /dev/null +++ b/.changeset/fair-colts-remain.md @@ -0,0 +1,13 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/account-service": patch +"@rocket.chat/authorization-service": patch +"@rocket.chat/ddp-streamer": patch +"@rocket.chat/omnichannel-transcript": patch +"@rocket.chat/presence-service": patch +"@rocket.chat/queue-worker": patch +"@rocket.chat/stream-hub-service": patch +"rocketchat-services": patch +--- + +Bump meteor to 3.0.4 and Node version to 20.18.0 diff --git a/.changeset/forty-gorillas-kneel.md b/.changeset/forty-gorillas-kneel.md new file mode 100644 index 000000000000..42df0ed8c0e4 --- /dev/null +++ b/.changeset/forty-gorillas-kneel.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/apps-engine": patch +--- + +Deprecated the `from` field in the apps email bridge and made it optional, using the server's settings when the field is omitted diff --git a/.changeset/friendly-ravens-teach.md b/.changeset/friendly-ravens-teach.md new file mode 100644 index 000000000000..1c464a8679b6 --- /dev/null +++ b/.changeset/friendly-ravens-teach.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +adds unread badge to sidebar collapser diff --git a/.changeset/green-papayas-thank.md b/.changeset/green-papayas-thank.md new file mode 100644 index 000000000000..22547db942ef --- /dev/null +++ b/.changeset/green-papayas-thank.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/model-typings': patch +'@rocket.chat/core-typings': patch +'@rocket.chat/meteor': patch +--- + +Fixes an issue where updating custom emojis didn’t work as expected, ensuring that uploaded emojis now update correctly and display without any caching problems. diff --git a/.changeset/happy-stingrays-provide.md b/.changeset/happy-stingrays-provide.md new file mode 100644 index 000000000000..fba25665133a --- /dev/null +++ b/.changeset/happy-stingrays-provide.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes issue that could cause multiple discussions to be created when creating it from a message action diff --git a/.changeset/honest-pumpkins-joke.md b/.changeset/honest-pumpkins-joke.md new file mode 100644 index 000000000000..aa1abce9ad6d --- /dev/null +++ b/.changeset/honest-pumpkins-joke.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +adds missing html attributes in sidebar item templates diff --git a/.changeset/lazy-avocados-whisper.md b/.changeset/lazy-avocados-whisper.md new file mode 100644 index 000000000000..b1296186c37c --- /dev/null +++ b/.changeset/lazy-avocados-whisper.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Improves thread metrics featuring user avatars, better titles and repositioned elements. diff --git a/.changeset/lemon-foxes-carry.md b/.changeset/lemon-foxes-carry.md new file mode 100644 index 000000000000..7e14dda30747 --- /dev/null +++ b/.changeset/lemon-foxes-carry.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +--- + +Fixes message character limit not being applied to file upload descriptions \ No newline at end of file diff --git a/.changeset/light-terms-ring.md b/.changeset/light-terms-ring.md new file mode 100644 index 000000000000..4437c5c4d596 --- /dev/null +++ b/.changeset/light-terms-ring.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes the issue where newly created teams are incorrectly displayed as channels on the sidebar when the DISABLE_DB_WATCHERS environment variable is enabled diff --git a/.changeset/mean-cobras-sneeze.md b/.changeset/mean-cobras-sneeze.md new file mode 100644 index 000000000000..39717f0c0d89 --- /dev/null +++ b/.changeset/mean-cobras-sneeze.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +Adds cursor pagination on chat.syncMessages endpoint diff --git a/.changeset/metal-avocados-serve.md b/.changeset/metal-avocados-serve.md new file mode 100644 index 000000000000..478407fcb97b --- /dev/null +++ b/.changeset/metal-avocados-serve.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/livechat": patch +--- + +Fixes the 'Finish Chat' option in Livechat appearing before the conversation is started, which caused the action to fail. diff --git a/.changeset/neat-flies-drive.md b/.changeset/neat-flies-drive.md new file mode 100644 index 000000000000..27b5270f81f9 --- /dev/null +++ b/.changeset/neat-flies-drive.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Adds a divider on Create team modal - advanced settings diff --git a/.changeset/nervous-rivers-fry.md b/.changeset/nervous-rivers-fry.md new file mode 100644 index 000000000000..278259c53a86 --- /dev/null +++ b/.changeset/nervous-rivers-fry.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/apps-engine': patch +'@rocket.chat/meteor': patch +--- + +Fixed an issue that would grant network permission to app's processes in wrong cases diff --git a/.changeset/old-coins-bow.md b/.changeset/old-coins-bow.md new file mode 100644 index 000000000000..1790cc205160 --- /dev/null +++ b/.changeset/old-coins-bow.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/apps-engine': patch +--- + +Fixes an issue that would cause apps to appear disabled after a subprocess restart diff --git a/.changeset/pink-dodos-greet.md b/.changeset/pink-dodos-greet.md new file mode 100644 index 000000000000..f122a2e72fc7 --- /dev/null +++ b/.changeset/pink-dodos-greet.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes sidepanel not replicating sidebar sort preference diff --git a/.changeset/plenty-snakes-dream.md b/.changeset/plenty-snakes-dream.md new file mode 100644 index 000000000000..eecbf0cbb466 --- /dev/null +++ b/.changeset/plenty-snakes-dream.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Adds a new route to allow fetching avatars by the user's id `/avatar/uid/` diff --git a/.changeset/popular-queens-brake.md b/.changeset/popular-queens-brake.md new file mode 100644 index 000000000000..5114920b8fde --- /dev/null +++ b/.changeset/popular-queens-brake.md @@ -0,0 +1,17 @@ +--- +'@rocket.chat/model-typings': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/apps-engine': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +These changes aims to add: +- A brand-new omnichannel contact profile +- The ability to communicate with known contacts only +- Communicate with verified contacts only +- Merge verified contacts across different channels +- Block contact channels +- Resolve conflicting contact information when registered via different channels +- An advanced contact center filters diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 000000000000..7ef999ab9254 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,113 @@ +{ + "mode": "pre", + "tag": "rc", + "initialVersions": { + "@rocket.chat/meteor": "7.1.0-develop", + "rocketchat-services": "2.0.0", + "@rocket.chat/uikit-playground": "0.6.0", + "@rocket.chat/account-service": "0.4.9", + "@rocket.chat/authorization-service": "0.4.9", + "@rocket.chat/ddp-streamer": "0.3.9", + "@rocket.chat/omnichannel-transcript": "0.4.9", + "@rocket.chat/presence-service": "0.4.9", + "@rocket.chat/queue-worker": "0.4.9", + "@rocket.chat/stream-hub-service": "0.4.9", + "@rocket.chat/license": "1.0.0", + "@rocket.chat/network-broker": "0.1.1", + "@rocket.chat/omnichannel-services": "0.3.6", + "@rocket.chat/pdf-worker": "0.2.6", + "@rocket.chat/presence": "0.2.9", + "@rocket.chat/ui-theming": "0.4.0", + "@rocket.chat/account-utils": "0.0.2", + "@rocket.chat/agenda": "0.1.0", + "@rocket.chat/api-client": "0.2.9", + "@rocket.chat/apps": "0.2.0", + "@rocket.chat/apps-engine": "1.47.0", + "@rocket.chat/base64": "1.0.13", + "@rocket.chat/cas-validate": "0.0.2", + "@rocket.chat/core-services": "0.7.1", + "@rocket.chat/core-typings": "7.1.0-develop", + "@rocket.chat/cron": "0.1.9", + "@rocket.chat/ddp-client": "0.3.9", + "@rocket.chat/eslint-config": "0.7.0", + "@rocket.chat/favicon": "0.0.2", + "@rocket.chat/freeswitch": "1.0.0", + "@rocket.chat/fuselage-ui-kit": "12.0.0", + "@rocket.chat/gazzodown": "12.0.0", + "@rocket.chat/i18n": "1.0.0", + "@rocket.chat/instance-status": "0.1.9", + "@rocket.chat/jest-presets": "0.0.1", + "@rocket.chat/jwt": "0.1.1", + "@rocket.chat/livechat": "1.20.1", + "@rocket.chat/log-format": "0.0.2", + "@rocket.chat/logger": "0.0.2", + "@rocket.chat/message-parser": "0.31.31", + "@rocket.chat/mock-providers": "0.1.4", + "@rocket.chat/model-typings": "1.0.0", + "@rocket.chat/models": "1.0.0", + "@rocket.chat/poplib": "0.0.2", + "@rocket.chat/password-policies": "0.0.2", + "@rocket.chat/patch-injection": "0.0.1", + "@rocket.chat/peggy-loader": "0.31.27", + "@rocket.chat/random": "1.2.2", + "@rocket.chat/release-action": "2.2.3", + "@rocket.chat/release-changelog": "0.1.0", + "@rocket.chat/rest-typings": "7.1.0-develop", + "@rocket.chat/server-cloud-communication": "0.0.2", + "@rocket.chat/server-fetch": "0.0.3", + "@rocket.chat/sha256": "1.0.10", + "@rocket.chat/tools": "0.2.2", + "@rocket.chat/tracing": "0.0.1", + "@rocket.chat/ui-avatar": "8.0.0", + "@rocket.chat/ui-client": "12.0.0", + "@rocket.chat/ui-composer": "0.4.0", + "@rocket.chat/ui-contexts": "12.0.0", + "@rocket.chat/ui-kit": "0.37.0", + "@rocket.chat/ui-video-conf": "12.0.0", + "@rocket.chat/ui-voip": "2.0.0", + "@rocket.chat/web-ui-registration": "12.0.0" + }, + "changesets": [ + "bump-patch-1732728542480", + "bump-patch-1732835767114", + "bump-patch-1733170421495", + "clean-flies-collect", + "curvy-flies-greet", + "fair-colts-remain", + "forty-gorillas-kneel", + "friendly-ravens-teach", + "green-papayas-thank", + "happy-stingrays-provide", + "honest-pumpkins-joke", + "lazy-avocados-whisper", + "lemon-foxes-carry", + "light-terms-ring", + "mean-cobras-sneeze", + "metal-avocados-serve", + "neat-flies-drive", + "nervous-rivers-fry", + "old-coins-bow", + "pink-dodos-greet", + "plenty-snakes-dream", + "popular-queens-brake", + "real-jeans-worry", + "serious-mice-film", + "seven-berries-check", + "seven-otters-fold", + "silent-steaks-happen", + "smart-radios-reflect", + "spicy-spiders-search", + "spotty-ads-knock", + "stale-actors-enjoy", + "sweet-needles-melt", + "swift-suns-perform", + "three-dragons-brush", + "tricky-trees-destroy", + "twelve-horses-suffer", + "twenty-news-own", + "two-guests-tan", + "unlucky-kangaroos-yawn", + "unlucky-wasps-check", + "weak-trees-exercise" + ] +} diff --git a/.changeset/real-jeans-worry.md b/.changeset/real-jeans-worry.md new file mode 100644 index 000000000000..9b16e7681a98 --- /dev/null +++ b/.changeset/real-jeans-worry.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes banner breaking the UI with specific payloads diff --git a/.changeset/serious-mice-film.md b/.changeset/serious-mice-film.md new file mode 100644 index 000000000000..35a2d6704071 --- /dev/null +++ b/.changeset/serious-mice-film.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes client-side updates for recent emoji list when custom emojis are modified. diff --git a/.changeset/seven-berries-check.md b/.changeset/seven-berries-check.md new file mode 100644 index 000000000000..b8cd3c49897f --- /dev/null +++ b/.changeset/seven-berries-check.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +--- + +Adds "Master volume" and "Call ringer volume" to the user preferences sound section. diff --git a/.changeset/seven-otters-fold.md b/.changeset/seven-otters-fold.md new file mode 100644 index 000000000000..7f2af2075f73 --- /dev/null +++ b/.changeset/seven-otters-fold.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Sends server statistics only once a day despite multiple instance being started at different times. diff --git a/.changeset/silent-steaks-happen.md b/.changeset/silent-steaks-happen.md new file mode 100644 index 000000000000..1ae791c68177 --- /dev/null +++ b/.changeset/silent-steaks-happen.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Improves the customizability of the naming of automatic Persistent video calls discussions, allowing the date of the call to be in different parts of the name, using the `[date]` keyword. diff --git a/.changeset/smart-radios-reflect.md b/.changeset/smart-radios-reflect.md new file mode 100644 index 000000000000..58ea7413e51c --- /dev/null +++ b/.changeset/smart-radios-reflect.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-services": patch +--- + +stops calling an object through proxy calling getQueueWorker diff --git a/.changeset/spicy-spiders-search.md b/.changeset/spicy-spiders-search.md new file mode 100644 index 000000000000..d86bc93313c5 --- /dev/null +++ b/.changeset/spicy-spiders-search.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed an issue where the installed apps list would go stale without a refresh in some cases diff --git a/.changeset/spotty-ads-knock.md b/.changeset/spotty-ads-knock.md new file mode 100644 index 000000000000..b40e70b74a98 --- /dev/null +++ b/.changeset/spotty-ads-knock.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes display of emoji aliases in custom emoji list by adding commas between aliases diff --git a/.changeset/stale-actors-enjoy.md b/.changeset/stale-actors-enjoy.md new file mode 100644 index 000000000000..baff2b19b667 --- /dev/null +++ b/.changeset/stale-actors-enjoy.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes `waiting queue` feature. When `Livechat_waiting_queue` setting is enabled, incoming conversations should be sent to the queue instead of being assigned directly. diff --git a/.changeset/sweet-needles-melt.md b/.changeset/sweet-needles-melt.md new file mode 100644 index 000000000000..51cd6e03d831 --- /dev/null +++ b/.changeset/sweet-needles-melt.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes edge case of thread unread not being added to unread group diff --git a/.changeset/swift-suns-perform.md b/.changeset/swift-suns-perform.md new file mode 100644 index 000000000000..2a52249d984c --- /dev/null +++ b/.changeset/swift-suns-perform.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/livechat": patch +--- + +Fixes livechat popout mode not working correctly in cross domain situations diff --git a/.changeset/three-dragons-brush.md b/.changeset/three-dragons-brush.md new file mode 100644 index 000000000000..d80c4dc83306 --- /dev/null +++ b/.changeset/three-dragons-brush.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/apps-engine': minor +'@rocket.chat/livechat': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Prevent apps' subprocesses from crashing on unhandled rejections or uncaught exceptions diff --git a/.changeset/tricky-trees-destroy.md b/.changeset/tricky-trees-destroy.md new file mode 100644 index 000000000000..3d43cc5b571a --- /dev/null +++ b/.changeset/tricky-trees-destroy.md @@ -0,0 +1,4 @@ +--- +"@rocket.chat/meteor": patch +--- +Adds login and permission validation for resetIrcConnection method diff --git a/.changeset/twelve-horses-suffer.md b/.changeset/twelve-horses-suffer.md new file mode 100644 index 000000000000..bc7f7d5b3ba4 --- /dev/null +++ b/.changeset/twelve-horses-suffer.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Adds a confirmation modal to the cancel subscription action diff --git a/.changeset/twenty-news-own.md b/.changeset/twenty-news-own.md new file mode 100644 index 000000000000..c48d06e0a05e --- /dev/null +++ b/.changeset/twenty-news-own.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Disables the possiblity to upload exempted apps diff --git a/.changeset/two-guests-tan.md b/.changeset/two-guests-tan.md new file mode 100644 index 000000000000..ff44f0ef493b --- /dev/null +++ b/.changeset/two-guests-tan.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/apps-engine': patch +--- + +Removed the 1 second timeout of `Pre` app events. Now they will follow the "global" configuration diff --git a/.changeset/unlucky-kangaroos-yawn.md b/.changeset/unlucky-kangaroos-yawn.md new file mode 100644 index 000000000000..1aaa97cbd8d8 --- /dev/null +++ b/.changeset/unlucky-kangaroos-yawn.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +--- + +Updates VoIP field labels from 'Free Extension Numbers' to 'Available Extensions' to better describe the field's purpose and improve clarity. diff --git a/.changeset/unlucky-wasps-check.md b/.changeset/unlucky-wasps-check.md new file mode 100644 index 000000000000..fd7e8af17824 --- /dev/null +++ b/.changeset/unlucky-wasps-check.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where resizable handler renders over the expanded thread view while using contextualbarResizable feature preview diff --git a/.changeset/weak-trees-exercise.md b/.changeset/weak-trees-exercise.md new file mode 100644 index 000000000000..230c087ccd83 --- /dev/null +++ b/.changeset/weak-trees-exercise.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/apps-engine': minor +--- + +Add support to configure apps runtime timeout via the APPS_ENGINE_RUNTIME_TIMEOUT environment variable diff --git a/.github/actions/build-docker-image/action.yml b/.github/actions/build-docker-image/action.yml deleted file mode 100644 index fa0535d332c6..000000000000 --- a/.github/actions/build-docker-image/action.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: 'Build Docker image' -description: 'Build Rocket.Chat Docker image' - -inputs: - root-dir: - required: true - docker-tag: - required: true - release: - required: true - username: - required: false - password: - required: false - deno-version: - required: true - type: string - -outputs: - image-name: - value: ${{ steps.build-image.outputs.image-name }} - -runs: - using: composite - steps: - # - shell: bash - # name: Free disk space - # run: | - # sudo swapoff -a - # sudo rm -f /swapfile - # sudo apt clean - # docker rmi $(docker image ls -aq) - # df -h - - - shell: bash - id: build-image - run: | - cd ${{ inputs.root-dir }} - - LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") - - IMAGE_NAME_BASE="ghcr.io/${LOWERCASE_REPOSITORY}/rocket.chat:${{ inputs.docker-tag }}" - - IMAGE_NAME="${IMAGE_NAME_BASE}.${{ inputs.release }}" - - echo "Build Docker image ${IMAGE_NAME}" - - DOCKER_PATH="${GITHUB_WORKSPACE}/apps/meteor/.docker" - if [[ '${{ inputs.release }}' = 'preview' ]]; then - DOCKER_PATH="${DOCKER_PATH}-mongo" - fi; - - DOCKERFILE_PATH="${DOCKER_PATH}/Dockerfile" - if [[ '${{ inputs.release }}' = 'debian' ]]; then - DOCKERFILE_PATH="${DOCKERFILE_PATH}.${{ inputs.release }}" - fi; - - echo "Copy Dockerfile for release: ${{ inputs.release }}" - cp $DOCKERFILE_PATH ./Dockerfile - if [ -e ${DOCKER_PATH}/entrypoint.sh ]; then - cp ${DOCKER_PATH}/entrypoint.sh . - fi; - - echo "Build ${{ inputs.release }} Docker image" - docker build --build-arg DENO_VERSION=${{ inputs.deno-version }} -t $IMAGE_NAME . - - echo "image-name-base=${IMAGE_NAME_BASE}" >> $GITHUB_OUTPUT - echo "image-name=${IMAGE_NAME}" >> $GITHUB_OUTPUT - - - name: Login to GitHub Container Registry - if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop' - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ inputs.username }} - password: ${{ inputs.password }} - - - name: Publish image - shell: bash - if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop' - run: | - echo "Push Docker image: ${{ steps.build-image.outputs.image-name }}" - - docker push ${{ steps.build-image.outputs.image-name }} - - if [[ '${{ inputs.release }}' = 'official' ]]; then - echo "Push release official without variant" - - docker tag ${{ steps.build-image.outputs.image-name }} ${{ steps.build-image.outputs.image-name-base }} - docker push ${{ steps.build-image.outputs.image-name-base }} - fi; diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml index 2891d892c359..5c601abe7811 100644 --- a/.github/actions/build-docker/action.yml +++ b/.github/actions/build-docker/action.yml @@ -16,7 +16,7 @@ inputs: platform: required: false description: 'Platform' - type: string + default: 'alpine' build-containers: required: false description: 'Containers to build along with Rocket.Chat' @@ -42,8 +42,8 @@ runs: steps: - name: Login to GitHub Container Registry - if: inputs.publish-image == 'true' &&(github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') - uses: docker/login-action@v2 + if: inputs.publish-image == 'true' && github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ inputs.CR_USER }} @@ -61,6 +61,7 @@ runs: cd /tmp/build tar xzf Rocket.Chat.tar.gz rm Rocket.Chat.tar.gz + - uses: rharkor/caching-for-turbo@v1.5 # if we are testing a PR from a fork, we already called the turbo cache at this point, so it should be false if: inputs.turbo-cache == 'true' @@ -84,7 +85,7 @@ runs: - run: yarn build if: inputs.setup == 'true' shell: bash - - if: ${{ inputs.platform == 'official' }} + - if: ${{ inputs.platform == 'alpine' }} uses: actions/cache@v3 with: path: /tmp/build/matrix-sdk-crypto.linux-x64-musl.node @@ -100,7 +101,7 @@ runs: docker compose -f docker-compose-ci.yml build "${args[@]}" - name: Publish Docker images to GitHub Container Registry - if: inputs.publish-image == 'true' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') + if: inputs.publish-image == 'true' && github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') shell: bash run: | args=(rocketchat ${{ inputs.build-containers }}) diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index ea90fc57b07c..b538d485535d 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -53,7 +53,7 @@ runs: - name: Use Node.js ${{ inputs.node-version }} id: node-version - uses: actions/setup-node@v3.7.0 + uses: actions/setup-node@v4.1.0 with: node-version: ${{ inputs.node-version }} cache: 'yarn' diff --git a/.github/actions/update-version-durability/package-lock.json b/.github/actions/update-version-durability/package-lock.json index 2d3043be183d..c4a4dac3b61d 100644 --- a/.github/actions/update-version-durability/package-lock.json +++ b/.github/actions/update-version-durability/package-lock.json @@ -46,54 +46,50 @@ } }, "node_modules/@octokit/auth-token": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", - "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", - "peer": true, + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", + "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==", "engines": { "node": ">= 18" } }, "node_modules/@octokit/core": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz", - "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==", - "peer": true, + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", + "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", "dependencies": { - "@octokit/auth-token": "^4.0.0", - "@octokit/graphql": "^7.1.0", - "@octokit/request": "^8.3.1", - "@octokit/request-error": "^5.1.0", + "@octokit/auth-token": "^5.0.0", + "@octokit/graphql": "^8.0.0", + "@octokit/request": "^9.0.0", + "@octokit/request-error": "^6.0.1", "@octokit/types": "^13.0.0", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" + "before-after-hook": "^3.0.2", + "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/endpoint": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz", - "integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==", - "peer": true, + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", + "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", "dependencies": { - "@octokit/types": "^13.1.0", - "universal-user-agent": "^6.0.0" + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.2" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/graphql": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.0.tgz", - "integrity": "sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==", - "peer": true, + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz", + "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==", "dependencies": { - "@octokit/request": "^8.3.0", + "@octokit/request": "^9.0.0", "@octokit/types": "^13.0.0", - "universal-user-agent": "^6.0.0" + "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 18" @@ -144,29 +140,25 @@ } }, "node_modules/@octokit/request": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz", - "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==", - "peer": true, + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", + "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", "dependencies": { - "@octokit/endpoint": "^9.0.1", - "@octokit/request-error": "^5.1.0", + "@octokit/endpoint": "^10.0.0", + "@octokit/request-error": "^6.0.1", "@octokit/types": "^13.1.0", - "universal-user-agent": "^6.0.0" + "universal-user-agent": "^7.0.2" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/request-error": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", - "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", - "peer": true, + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz", + "integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==", "dependencies": { - "@octokit/types": "^13.1.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" + "@octokit/types": "^13.0.0" }, "engines": { "node": ">= 18" @@ -186,91 +178,6 @@ "node": ">= 18" } }, - "node_modules/@octokit/rest/node_modules/@octokit/auth-token": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", - "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==", - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/core": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", - "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", - "dependencies": { - "@octokit/auth-token": "^5.0.0", - "@octokit/graphql": "^8.0.0", - "@octokit/request": "^9.0.0", - "@octokit/request-error": "^6.0.1", - "@octokit/types": "^13.0.0", - "before-after-hook": "^3.0.2", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/endpoint": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", - "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", - "dependencies": { - "@octokit/types": "^13.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/graphql": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz", - "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==", - "dependencies": { - "@octokit/request": "^9.0.0", - "@octokit/types": "^13.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/request": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.1.tgz", - "integrity": "sha512-pyAguc0p+f+GbQho0uNetNQMmLG1e80WjkIaqqgUkihqUp0boRU6nKItXO4VWnr+nbZiLGEyy4TeKRwqaLvYgw==", - "dependencies": { - "@octokit/endpoint": "^10.0.0", - "@octokit/request-error": "^6.0.1", - "@octokit/types": "^13.1.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/request-error": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.1.tgz", - "integrity": "sha512-1mw1gqT3fR/WFvnoVpY/zUM2o/XkMs/2AszUUG9I69xn0JFLv6PGkPhNk5lbfvROs79wiS0bqiJNxfCZcRJJdg==", - "dependencies": { - "@octokit/types": "^13.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/rest/node_modules/before-after-hook": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", - "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==" - }, - "node_modules/@octokit/rest/node_modules/universal-user-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", - "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==" - }, "node_modules/@octokit/types": { "version": "13.5.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", @@ -294,9 +201,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -309,10 +216,9 @@ "integrity": "sha512-c0iKWc527T2MQcYhIMMw9OHN8kcXSf/ijadWzURhZWi6e6cnBXxAQ5IlXbYd0YZJE9lFtXRB1fJVQrvJf5DmPQ==" }, "node_modules/before-after-hook": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", - "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", - "peer": true + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", + "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==" }, "node_modules/colors": { "version": "1.4.0", @@ -341,12 +247,6 @@ "node": ">=0.4.0" } }, - "node_modules/deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", - "peer": true - }, "node_modules/diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", @@ -406,15 +306,6 @@ "node": ">= 0.6" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "peer": true, - "dependencies": { - "wrappy": "1" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -451,10 +342,9 @@ } }, "node_modules/universal-user-agent": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", - "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", - "peer": true + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", + "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==" }, "node_modules/uuid": { "version": "8.3.2", @@ -463,12 +353,6 @@ "bin": { "uuid": "dist/bin/uuid" } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "peer": true } } } diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c3b3e868f2a5..7b7e7e3ba392 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,22 +1,25 @@ version: 2 updates: + # Main repo NPM package + - package-ecosystem: npm + directory: '/' + schedule: + interval: daily + open-pull-requests-limit: 10 + labels: + - 'automerge' -# Main repo NPM package -- package-ecosystem: npm - directory: "/" - schedule: - interval: daily - open-pull-requests-limit: 10 + ## EE dependencies + - package-ecosystem: npm + directory: '/ee/server/services' + schedule: + interval: daily + open-pull-requests-limit: 10 + labels: + - 'automerge' -## EE dependencies -- package-ecosystem: npm - directory: "/ee/server/services" - schedule: - interval: daily - open-pull-requests-limit: 10 - -# Maintain dependencies for GitHub Actions -- package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" + # Maintain dependencies for GitHub Actions + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'daily' diff --git a/.github/workflows/ci-code-check.yml b/.github/workflows/ci-code-check.yml index 6712cc49356d..ccdd37cf6ac4 100644 --- a/.github/workflows/ci-code-check.yml +++ b/.github/workflows/ci-code-check.yml @@ -55,7 +55,7 @@ jobs: name: turbo-build path: .turbo/cache - name: Cache TypeCheck - uses: actions/cache@v3 + uses: actions/cache@v4 if: matrix.check == 'ts' with: path: ./apps/meteor/tsconfig.typecheck.tsbuildinfo @@ -70,7 +70,7 @@ jobs: run: yarn turbo run typecheck - name: Cache eslint - uses: actions/cache@v3 + uses: actions/cache@v4 if: matrix.check == 'lint' with: path: ./apps/meteor/.eslintcache diff --git a/.github/workflows/ci-deploy-gh-pages.yml b/.github/workflows/ci-deploy-gh-pages.yml index 6da5693303b4..abfe62d51389 100644 --- a/.github/workflows/ci-deploy-gh-pages.yml +++ b/.github/workflows/ci-deploy-gh-pages.yml @@ -11,13 +11,13 @@ jobs: deploy-preview: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: rharkor/caching-for-turbo@v1.5 - name: Setup NodeJS uses: ./.github/actions/setup-node with: - node-version: 20.17.0 + node-version: 20.18.0 deno-version: 1.37.1 cache-modules: true install: true diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 8f5d258ef165..58dc52bb25d4 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -18,12 +18,6 @@ on: rc-docker-tag: required: true type: string - rc-dockerfile-debian: - required: true - type: string - rc-docker-tag-debian: - required: true - type: string gh-docker-tag: required: true type: string @@ -83,8 +77,8 @@ jobs: test: runs-on: ubuntu-20.04 env: - RC_DOCKERFILE: ${{ matrix.mongodb-version == '7.0' && inputs.rc-dockerfile-debian || inputs.rc-dockerfile }} - RC_DOCKER_TAG: ${{ matrix.mongodb-version == '7.0' && inputs.rc-docker-tag-debian || inputs.rc-docker-tag }} + RC_DOCKERFILE: ${{ inputs.rc-dockerfile }}.${{ (matrix.mongodb-version == '7.0' && 'debian' && false) || 'alpine' }} + RC_DOCKER_TAG: ${{ inputs.rc-docker-tag }}.${{ (matrix.mongodb-version == '7.0' && 'debian' && false) || 'alpine' }} strategy: fail-fast: false @@ -92,7 +86,7 @@ jobs: mongodb-version: ${{ fromJSON(inputs.mongodb-version) }} shard: ${{ fromJSON(inputs.shard) }} - name: MongoDB ${{ matrix.mongodb-version }}${{ inputs.db-watcher-disabled == 'true' && ' [no watchers]' || '' }} (${{ matrix.shard }}/${{ inputs.total-shard }}) - ${{ matrix.mongodb-version == '7.0' && 'Debian' || 'Alpine (Official)' }} + name: MongoDB ${{ matrix.mongodb-version }}${{ inputs.db-watcher-disabled == 'true' && ' [no watchers]' || '' }} (${{ matrix.shard }}/${{ inputs.total-shard }}) - ${{ (matrix.mongodb-version == '7.0' && 'Debian' && false) || 'Alpine (Official)' }} steps: - name: Collect Workflow Telemetry @@ -112,8 +106,8 @@ jobs: sudo sysctl -p - name: Login to GitHub Container Registry - if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') - uses: docker/login-action@v2 + if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') && github.actor != 'dependabot[bot]' + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ secrets.CR_USER }} @@ -143,10 +137,12 @@ jobs: with: name: turbo-build path: .turbo/cache + - run: yarn build + # if we are testing a PR from a fork, we need to build the docker image at this point - uses: ./.github/actions/build-docker - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository + if: github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name != github.repository || github.actor == 'dependabot[bot]') with: CR_USER: ${{ secrets.CR_USER }} CR_PAT: ${{ secrets.CR_PAT }} @@ -204,13 +200,13 @@ jobs: - name: Cache Playwright binaries if: inputs.type == 'ui' - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-playwright with: path: | ~/.cache/ms-playwright # This is the version of Playwright that we are using, if you are willing to upgrade, you should update this. - key: playwright-1.40.1 + key: playwright-1.48.2 - name: Install Playwright if: inputs.type == 'ui' && steps.cache-playwright.outputs.cache-hit != 'true' @@ -238,6 +234,7 @@ jobs: - name: Remove unused Docker images run: docker system prune -af + - name: E2E Test API if: inputs.type == 'api' working-directory: ./apps/meteor @@ -305,7 +302,11 @@ jobs: include-hidden-files: true - name: Show server logs if E2E test failed - if: failure() + if: failure() && inputs.release == 'ee' + run: docker compose -f docker-compose-ci.yml logs + + - name: Show server logs if E2E test failed + if: failure() && inputs.release != 'ee' run: docker compose -f docker-compose-ci.yml logs rocketchat - name: Extract e2e:ee:coverage diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07870e10e520..bc56fe6b309a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,11 +31,10 @@ jobs: gh-docker-tag: ${{ steps.docker.outputs.gh-docker-tag }} lowercase-repo: ${{ steps.var.outputs.lowercase-repo }} rc-dockerfile: '${{ github.workspace }}/apps/meteor/.docker/Dockerfile' - rc-docker-tag: '${{ steps.docker.outputs.gh-docker-tag }}.official' - rc-dockerfile-debian: '${{ github.workspace }}/apps/meteor/.docker/Dockerfile.debian' - rc-docker-tag-debian: '${{ steps.docker.outputs.gh-docker-tag }}.debian' + rc-docker-tag: '${{ steps.docker.outputs.gh-docker-tag }}' node-version: ${{ steps.var.outputs.node-version }} deno-version: ${{ steps.var.outputs.deno-version }} + official-platform: 'alpine' # this is 100% intentional, secrets are not available for forks, so ee-tests will always fail # to avoid this, we are using a dummy license, expiring at 2025-06-31 enterprise-license: X/XumwIkgwQuld0alWKt37lVA90XjKOrfiMvMZ0/RtqsMtrdL9GoAk+4jXnaY1b2ePoG7XSzGhuxEDxFKIWJK3hIKGNTvrd980LgH5sM5+1T4P42ivSpd8UZi0bwjJkCFLIu9RozzYwslGG0IehMxe0S6VjcO0UYlUJtbMCBHuR2WmTAmO6YVU3ln+pZCbrPFaTPSS1RovhKaNCNkZwIx/CLWW8UTXUuFV/ML4PbKKVoa5nvvJwPeatgL7UCnlSD90lfCiiuikpzj/Y/JLkIL6velFbwNxsrxg9iRJ2k0sKheMMSmlTiGzSvZUm+na5WQq91aKGncih+DmaEZA7QGrjp4eoA0dqTk6OmItsy0fHmQhvZIOKNMeO7vNQiLbaSV6rqibrzu7WPpeIvsvL57T1h37USoCSB6+jDqkzdfoqIpz8BxTiJDj1d8xGPJFVrgxoqQqkj9qIP/gCaEz5DF39QFv5sovk4yK2O8fEQYod2d14V9yECYl4szZPMk1IBfCAC2w7czWGHHFonhL+CQGT403y5wmDmnsnjlCqMKF72odqfTPTI8XnCvJDriPMWohnQEAGtTTyciAhNokx/mjAVJ4NeZPcsbm4BjhvJvnjxx/BhYhBBTNWPaCSZzocfrGUj9Z+ZA7BEz+xAFQyGDx3xRzqIXfT0G7w8fvgYJMU= @@ -167,7 +166,7 @@ jobs: HARDENED_MODE: '1' - name: Cache vite - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ./node_modules/.vite key: vite-local-cache-${{ runner.OS }}-${{ hashFiles('package.json') }} @@ -191,7 +190,7 @@ jobs: runs-on: ubuntu-latest needs: [release-versions, packages-build] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: rharkor/caching-for-turbo@v1.5 if: github.event.action != 'closed' @@ -200,7 +199,7 @@ jobs: uses: ./.github/actions/setup-node if: github.event.action != 'closed' with: - node-version: 20.17.0 + node-version: 20.18.0 deno-version: 1.37.1 cache-modules: true install: true @@ -291,7 +290,7 @@ jobs: steps: - name: check cache for matrix-rust-sdk-crypto-nodejs id: matrix-rust-sdk-crypto-nodejs - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: /tmp/build/matrix-sdk-crypto.linux-x64-musl.node key: matrix-rust-sdk-crypto-nodejs-v0.2.0-beta.1 @@ -307,7 +306,7 @@ jobs: - if: steps.matrix-rust-sdk-crypto-nodejs.outputs.cache-hit != 'true' uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '20.18.0' - if: steps.matrix-rust-sdk-crypto-nodejs.outputs.cache-hit != 'true' uses: dtolnay/rust-toolchain@stable @@ -335,51 +334,60 @@ jobs: runs-on: ubuntu-20.04 env: - RC_DOCKERFILE: ${{ matrix.platform == 'debian' && needs.release-versions.outputs.rc-dockerfile-debian || needs.release-versions.outputs.rc-dockerfile }} - RC_DOCKER_TAG: ${{ matrix.platform == 'debian' && needs.release-versions.outputs.rc-docker-tag-debian || needs.release-versions.outputs.rc-docker-tag }} + RC_DOCKERFILE: ${{ needs.release-versions.outputs.rc-dockerfile }}.${{ matrix.platform }} + RC_DOCKER_TAG: ${{ needs.release-versions.outputs.rc-docker-tag }}.${{ matrix.platform }} DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} LOWERCASE_REPOSITORY: ${{ needs.release-versions.outputs.lowercase-repo }} strategy: fail-fast: false matrix: - platform: ['official', 'debian'] + platform: ['alpine'] steps: - uses: actions/checkout@v4 # we only build and publish the actual docker images if not a PR from a fork - uses: ./.github/actions/build-docker - if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') + if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') && github.actor != 'dependabot[bot]' with: CR_USER: ${{ secrets.CR_USER }} CR_PAT: ${{ secrets.CR_PAT }} node-version: ${{ needs.release-versions.outputs.node-version }} deno-version: ${{ needs.release-versions.outputs.deno-version }} platform: ${{ matrix.platform }} - build-containers: ${{ matrix.platform == 'debian' && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }} + build-containers: ${{ matrix.platform == needs.release-versions.outputs.official-platform && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Make sure matrix bindings load - if: ${{ matrix.platform == 'official' }} + if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') && matrix.platform == 'alpine' && github.actor != 'dependabot[bot]' run: | docker run --rm -w /app/bundle/programs/server/npm/node_modules/matrix-appservice-bridge ghcr.io/rocketchat/rocket.chat:$RC_DOCKER_TAG -e 'require(".")' + - name: Rename official Docker tag to GitHub Container Registry + if: matrix.platform == needs.release-versions.outputs.official-platform + run: | + IMAGE_NAME_BASE="ghcr.io/${LOWERCASE_REPOSITORY}/rocket.chat:${DOCKER_TAG}" + + echo "Push Docker image: ${IMAGE_NAME_BASE}.official" + docker tag ${IMAGE_NAME_BASE}.${{matrix.platform}} ${IMAGE_NAME_BASE}.official + docker push ${IMAGE_NAME_BASE}.official + build-gh-docker: name: 🚢 Build Docker Images for Production needs: [build-prod, release-versions] runs-on: ubuntu-20.04 env: - RC_DOCKERFILE: ${{ matrix.platform == 'debian' && needs.release-versions.outputs.rc-dockerfile-debian || needs.release-versions.outputs.rc-dockerfile }} - RC_DOCKER_TAG: ${{ matrix.platform == 'debian' && needs.release-versions.outputs.rc-docker-tag-debian || needs.release-versions.outputs.rc-docker-tag }} + RC_DOCKERFILE: ${{ needs.release-versions.outputs.rc-dockerfile }}.${{ matrix.platform }} + RC_DOCKER_TAG: ${{ needs.release-versions.outputs.rc-docker-tag }}.${{ matrix.platform }} DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} LOWERCASE_REPOSITORY: ${{ needs.release-versions.outputs.lowercase-repo }} strategy: fail-fast: false matrix: - platform: ['official', 'debian'] + platform: ['alpine'] steps: - uses: actions/checkout@v4 @@ -391,18 +399,22 @@ jobs: node-version: ${{ needs.release-versions.outputs.node-version }} deno-version: ${{ needs.release-versions.outputs.deno-version }} platform: ${{ matrix.platform }} - build-containers: ${{ matrix.platform == 'debian' && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }} + build-containers: ${{ matrix.platform == needs.release-versions.outputs.official-platform && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Rename official Docker tag to GitHub Container Registry - if: matrix.platform == 'official' + if: matrix.platform == needs.release-versions.outputs.official-platform run: | IMAGE_NAME_BASE="ghcr.io/${LOWERCASE_REPOSITORY}/rocket.chat:${DOCKER_TAG}" echo "Push Docker image: ${IMAGE_NAME_BASE}" - docker tag ${IMAGE_NAME_BASE}.official $IMAGE_NAME_BASE + docker tag ${IMAGE_NAME_BASE}.${{matrix.platform}} $IMAGE_NAME_BASE docker push $IMAGE_NAME_BASE + echo "Push Docker image: ${IMAGE_NAME_BASE}.official" + docker tag ${IMAGE_NAME_BASE}.${{matrix.platform}} ${IMAGE_NAME_BASE}.official + docker push ${IMAGE_NAME_BASE}.official + checks: needs: [release-versions, packages-build] @@ -437,8 +449,6 @@ jobs: lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} - rc-dockerfile-debian: ${{ needs.release-versions.outputs.rc-dockerfile-debian }} - rc-docker-tag-debian: ${{ needs.release-versions.outputs.rc-docker-tag-debian }} gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} secrets: CR_USER: ${{ secrets.CR_USER }} @@ -461,8 +471,6 @@ jobs: lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} - rc-dockerfile-debian: ${{ needs.release-versions.outputs.rc-dockerfile-debian }} - rc-docker-tag-debian: ${{ needs.release-versions.outputs.rc-docker-tag-debian }} gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} retries: ${{ (github.event_name == 'release' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') && 2 || 0 }} secrets: @@ -489,8 +497,6 @@ jobs: lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} - rc-dockerfile-debian: ${{ needs.release-versions.outputs.rc-dockerfile-debian }} - rc-docker-tag-debian: ${{ needs.release-versions.outputs.rc-docker-tag-debian }} gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} secrets: CR_USER: ${{ secrets.CR_USER }} @@ -514,8 +520,6 @@ jobs: lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} - rc-dockerfile-debian: ${{ needs.release-versions.outputs.rc-dockerfile-debian }} - rc-docker-tag-debian: ${{ needs.release-versions.outputs.rc-docker-tag-debian }} gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} retries: ${{ (github.event_name == 'release' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') && 2 || 0 }} secrets: @@ -545,8 +549,6 @@ jobs: lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} - rc-dockerfile-debian: ${{ needs.release-versions.outputs.rc-dockerfile-debian }} - rc-docker-tag-debian: ${{ needs.release-versions.outputs.rc-docker-tag-debian }} gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} retries: ${{ (github.event_name == 'release' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') && 2 || 0 }} db-watcher-disabled: 'true' @@ -651,48 +653,15 @@ jobs: aws s3 cp $ROCKET_DEPLOY_DIR/ s3://download.rocket.chat/build/ --recursive - build-docker-preview: - name: 🚢 Build Docker Image (preview) - runs-on: ubuntu-20.04 - needs: [build, checks, release-versions] - if: github.event_name == 'release' || github.ref == 'refs/heads/develop' - steps: - - uses: actions/checkout@v4 - - - name: Restore build - uses: actions/download-artifact@v4 - with: - name: build - path: /tmp/build - - - name: Unpack build - run: | - cd /tmp/build - tar xzf Rocket.Chat.tar.gz - rm Rocket.Chat.tar.gz - - - name: Build Docker image - id: build-docker-image-preview - uses: ./.github/actions/build-docker-image - with: - root-dir: /tmp/build - docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} - release: preview - username: ${{ secrets.CR_USER }} - password: ${{ secrets.CR_PAT }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - deno-version: ${{ needs.release-versions.outputs.deno-version }} - docker-image-publish: name: 🚀 Publish Docker Image (main) runs-on: ubuntu-20.04 - needs: [deploy, build-docker-preview, release-versions] + needs: [deploy, release-versions] strategy: matrix: # this is currently a mix of variants and different images - release: ['official', 'preview', 'debian'] - + release: ['alpine'] env: IMAGE_NAME: 'rocketchat/rocket.chat' diff --git a/.github/workflows/new-release.yml b/.github/workflows/new-release.yml index cd6fc3dec3f2..b3ead30e8966 100644 --- a/.github/workflows/new-release.yml +++ b/.github/workflows/new-release.yml @@ -34,7 +34,7 @@ jobs: - name: Setup NodeJS uses: ./.github/actions/setup-node with: - node-version: 20.17.0 + node-version: 20.18.0 deno-version: 1.37.1 cache-modules: true install: true diff --git a/.github/workflows/pr-update-description.yml b/.github/workflows/pr-update-description.yml index 66a8e4436a34..7a0d8650a97f 100644 --- a/.github/workflows/pr-update-description.yml +++ b/.github/workflows/pr-update-description.yml @@ -21,7 +21,7 @@ jobs: - name: Setup NodeJS uses: ./.github/actions/setup-node with: - node-version: 20.17.0 + node-version: 20.18.0 deno-version: 1.37.1 cache-modules: true install: true diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 33f2de48feeb..2f803576ef51 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -24,7 +24,7 @@ jobs: - name: Setup NodeJS uses: ./.github/actions/setup-node with: - node-version: 20.17.0 + node-version: 20.18.0 deno-version: 1.37.1 cache-modules: true install: true diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index ad64416da04c..506f4cc1e5dd 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -1,7 +1,7 @@ name: Release candidate cut on: schedule: - - cron: '28 0 20 * *' # run at minute 28 to avoid the chance of delay due to high load on GH + - cron: '28 12 20 * *' # run at minute 28 to avoid the chance of delay due to high load on GH jobs: new-release: runs-on: ubuntu-latest @@ -15,7 +15,7 @@ jobs: - name: Setup NodeJS uses: ./.github/actions/setup-node with: - node-version: 20.17.0 + node-version: 20.18.0 deno-version: 1.37.1 cache-modules: true install: true diff --git a/.github/workflows/update-version-durability.yml b/.github/workflows/update-version-durability.yml index bdf6b75e0f5b..45fd4859502b 100644 --- a/.github/workflows/update-version-durability.yml +++ b/.github/workflows/update-version-durability.yml @@ -14,22 +14,22 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Use Node.js - uses: actions/setup-node@v4.0.4 - with: - node-version: '20.17.0' + - name: Use Node.js + uses: actions/setup-node@v4.1.0 + with: + node-version: '20.18.0' - - name: Install dependencies - run: | - cd ./.github/actions/update-version-durability - npm install + - name: Install dependencies + run: | + cd ./.github/actions/update-version-durability + npm install - - name: Update Version Durability - uses: ./.github/actions/update-version-durability - with: - GH_TOKEN: ${{ secrets.CI_PAT }} - D360_TOKEN: ${{ secrets.D360_TOKEN }} - D360_ARTICLE_ID: 800f8d52-409d-478d-b560-f82a2c0eb7fb - PUBLISH: true + - name: Update Version Durability + uses: ./.github/actions/update-version-durability + with: + GH_TOKEN: ${{ secrets.CI_PAT }} + D360_TOKEN: ${{ secrets.D360_TOKEN }} + D360_ARTICLE_ID: 800f8d52-409d-478d-b560-f82a2c0eb7fb + PUBLISH: true diff --git a/.kodiak.toml b/.kodiak.toml index 7f89eed8f169..378f922d81e4 100644 --- a/.kodiak.toml +++ b/.kodiak.toml @@ -16,3 +16,5 @@ include_coauthors=true [merge.automerge_dependencies] versions = ["minor", "patch"] usernames = ["dependabot"] +[approve] +auto_approve_usernames = ["dependabot"] diff --git a/.yarn/patches/swiper-npm-11.1.14-8126fa478a.patch b/.yarn/patches/swiper-npm-11.1.14-8126fa478a.patch new file mode 100644 index 000000000000..b70fd3bb9d26 --- /dev/null +++ b/.yarn/patches/swiper-npm-11.1.14-8126fa478a.patch @@ -0,0 +1,62 @@ +diff --git a/package.json b/package.json +index 9f37b1dd9e3ea128aafb0ceef83641e864474308..795e15b1b008596f0a278ff43cb1103794c10d9d 100644 +--- a/package.json ++++ b/package.json +@@ -24,6 +24,7 @@ + "default": "./swiper-bundle.mjs" + }, + "./css": "./swiper.css", ++ "./swiper.css": "./swiper.css", + "./css/bundle": "./swiper-bundle.css", + "./swiper-bundle.css": "./swiper-bundle.css", + "./css/a11y": "./modules/a11y.css", +@@ -49,6 +50,7 @@ + "./css/thumbs": "./modules/thumbs.css", + "./css/virtual": "./modules/virtual.css", + "./css/zoom": "./modules/zoom.css", ++ "./modules/zoom.css": "./modules/zoom.css", + "./less": "./swiper.less", + "./less/a11y": "./modules/a11y.less", + "./less/autoplay": "./modules/autoplay.less", +@@ -136,6 +138,10 @@ + "types": "./swiper-react.d.ts", + "default": "./swiper-react.mjs" + }, ++ "./swiper-react.mjs": { ++ "types": "./swiper-react.d.ts", ++ "default": "./swiper-react.mjs" ++ }, + "./vue": { + "types": "./swiper-vue.d.ts", + "default": "./swiper-vue.mjs" +@@ -144,6 +150,10 @@ + "types": "./types/modules/index.d.ts", + "default": "./modules/index.mjs" + }, ++ "./modules/index.mjs": { ++ "types": "./types/modules/index.d.ts", ++ "default": "./modules/index.mjs" ++ }, + "./types": "./types/index.d.ts", + "./package.json": "./package.json" + }, +@@ -152,6 +162,9 @@ + "modules": [ + "./types/modules/index.d.ts" + ], ++ "modules/index.mjs": [ ++ "./types/modules/index.d.ts" ++ ], + "element": [ + "./swiper-element.d.ts" + ], +@@ -161,6 +174,9 @@ + "react": [ + "./swiper-react.d.ts" + ], ++ "swiper-react.mjs": [ ++ "./swiper-react.d.ts" ++ ], + "vue": [ + "./swiper-vue.d.ts" + ] diff --git a/README.md b/README.md index 564ca75d2b11..d4053587a5c7 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Free for 30 days. Afterward, choose between continuing to host on our secure clo You can follow these instructions to setup a dev environment: -- Install **Node 14.x (LTS)** either [manually](https://nodejs.org/dist/latest-v14.x/) or using a tool like [nvm](https://github.com/creationix/nvm) or [volta](https://volta.sh/) (recommended) +- Install **Node 20.x (LTS)** either [manually](https://nodejs.org/dist/latest-v20.x/) or using a tool like [nvm](https://github.com/creationix/nvm) or [volta](https://volta.sh/) (recommended) - Install **Meteor** ([version here](apps/meteor/.meteor/release)): https://docs.meteor.com/about/install.html - Install **yarn**: https://yarnpkg.com/getting-started/install - Install **Deno 1.x**: https://docs.deno.com/runtime/fundamentals/installation/ @@ -88,7 +88,7 @@ yarn turbo run ms After initialized, you can access the server at http://localhost:4000 -> ⚠️ Check more detailed information in the [Rocket.Chat Environment Setup](https://developer.rocket.chat/rocket.chat/rocket-chat-environment-setup) guide +> ⚠️ Check more detailed information in the [Rocket.Chat Environment Setup](https://developer.rocket.chat/docs/server-environment-setup) guide # 💻 Installation diff --git a/apps/meteor/.docker-mongo/Dockerfile b/apps/meteor/.docker-mongo/Dockerfile index 17784e1769a3..560fde4a69dc 100644 --- a/apps/meteor/.docker-mongo/Dockerfile +++ b/apps/meteor/.docker-mongo/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.17.0-bullseye-slim +FROM node:20.18.0-bullseye-slim LABEL maintainer="buildmaster@rocket.chat" diff --git a/apps/meteor/.docker/Dockerfile b/apps/meteor/.docker/Dockerfile.alpine similarity index 98% rename from apps/meteor/.docker/Dockerfile rename to apps/meteor/.docker/Dockerfile.alpine index 7bff2a067c0a..0f1e170f9570 100644 --- a/apps/meteor/.docker/Dockerfile +++ b/apps/meteor/.docker/Dockerfile.alpine @@ -1,4 +1,4 @@ -FROM node:20.17.0-alpine3.20 +FROM node:20.18.0-alpine3.20 LABEL maintainer="buildmaster@rocket.chat" diff --git a/apps/meteor/.docker/Dockerfile.debian b/apps/meteor/.docker/Dockerfile.debian index 7bbca15a9142..22134532ece0 100644 --- a/apps/meteor/.docker/Dockerfile.debian +++ b/apps/meteor/.docker/Dockerfile.debian @@ -2,7 +2,7 @@ ARG DENO_VERSION="1.37.1" FROM denoland/deno:bin-${DENO_VERSION} as deno -FROM node:20.17.0-bullseye-slim +FROM node:20.18.0-bullseye-slim LABEL maintainer="buildmaster@rocket.chat" diff --git a/apps/meteor/.eslintignore b/apps/meteor/.eslintignore index 3ff422a3ed3f..f7051767d772 100644 --- a/apps/meteor/.eslintignore +++ b/apps/meteor/.eslintignore @@ -12,3 +12,5 @@ !/.storybook/ !/client/.eslintrc.js !/ee/client/.eslintrc.js +/storybook-static/ +/packages/ diff --git a/apps/meteor/.meteor/packages b/apps/meteor/.meteor/packages index 70518d252df9..ead1cc41fd09 100644 --- a/apps/meteor/.meteor/packages +++ b/apps/meteor/.meteor/packages @@ -14,7 +14,7 @@ rocketchat:streamer rocketchat:version rocketchat:user-presence -accounts-base@3.0.2 +accounts-base@3.0.3 accounts-facebook@1.3.4 accounts-github@1.5.1 accounts-google@1.4.1 @@ -27,14 +27,14 @@ google-oauth@1.4.5 oauth@3.0.0 oauth2@1.3.3 -check@1.4.2 +check@1.4.4 ddp-rate-limiter@1.2.2 rate-limit@1.1.2 email@3.1.0 meteor-base@1.5.2 ddp-common@1.4.4 -webapp@2.0.1 +webapp@2.0.3 mongo@2.0.2 @@ -59,7 +59,7 @@ tracker@1.3.4 reactive-dict@1.3.2 reactive-var@1.0.13 -babel-compiler@7.11.0 +babel-compiler@7.11.1 standard-minifier-css@1.9.3 dynamic-import@0.7.4 ecmascript@0.16.9 diff --git a/apps/meteor/.meteor/release b/apps/meteor/.meteor/release index c41cba9c61a2..b1e86a359f7c 100644 --- a/apps/meteor/.meteor/release +++ b/apps/meteor/.meteor/release @@ -1 +1 @@ -METEOR@3.0.3 +METEOR@3.0.4 diff --git a/apps/meteor/.meteor/versions b/apps/meteor/.meteor/versions index c41710f5aa16..0f8f733b3148 100644 --- a/apps/meteor/.meteor/versions +++ b/apps/meteor/.meteor/versions @@ -1,4 +1,4 @@ -accounts-base@3.0.2 +accounts-base@3.0.3 accounts-facebook@1.3.4 accounts-github@1.5.1 accounts-google@1.4.1 @@ -8,24 +8,24 @@ accounts-password@3.0.2 accounts-twitter@1.5.2 allow-deny@2.0.0 autoupdate@2.0.0 -babel-compiler@7.11.0 +babel-compiler@7.11.1 babel-runtime@1.5.2 base64@1.0.13 binary-heap@1.0.12 boilerplate-generator@2.0.0 callback-hook@1.6.0 -check@1.4.2 +check@1.4.4 core-runtime@1.0.0 ddp@1.4.2 -ddp-client@3.0.1 +ddp-client@3.0.2 ddp-common@1.4.4 ddp-rate-limiter@1.2.2 -ddp-server@3.0.1 +ddp-server@3.0.2 diff-sequence@1.1.3 dispatch:run-as-user@1.1.1 dynamic-import@0.7.4 ecmascript@0.16.9 -ecmascript-runtime@0.8.2 +ecmascript-runtime@0.8.3 ecmascript-runtime-client@0.12.2 ecmascript-runtime-server@0.11.1 ejson@1.1.4 @@ -51,7 +51,7 @@ meteorhacks:inject-initial@1.0.5 minifier-css@2.0.0 minimongo@2.0.1 modern-browsers@0.1.11 -modules@0.20.1 +modules@0.20.2 modules-runtime@0.13.2 mongo@2.0.2 mongo-decimal@0.1.4-beta300.7 @@ -89,8 +89,8 @@ tracker@1.3.4 twitter-oauth@1.3.4 typescript@5.4.3 underscore@1.6.4 -url@1.3.3 -webapp@2.0.1 +url@1.3.4 +webapp@2.0.3 webapp-hashing@1.1.2 zodern:caching-minifier@0.5.0 zodern:standard-minifier-js@5.2.0 diff --git a/apps/meteor/.mocharc.js b/apps/meteor/.mocharc.js index fb5324f4c0fd..f11b315b8cef 100644 --- a/apps/meteor/.mocharc.js +++ b/apps/meteor/.mocharc.js @@ -32,8 +32,10 @@ module.exports = { 'tests/unit/app/**/*.tests.js', 'tests/unit/app/**/*.tests.ts', 'tests/unit/lib/**/*.tests.ts', + 'server/routes/avatar/**/*.spec.ts', 'tests/unit/lib/**/*.spec.ts', 'tests/unit/server/**/*.tests.ts', 'tests/unit/server/**/*.spec.ts', + 'app/api/**/*.spec.ts', ], }; diff --git a/apps/meteor/.stylelintignore b/apps/meteor/.stylelintignore index 4bd9c6d458d6..33637d3dd3e7 100644 --- a/apps/meteor/.stylelintignore +++ b/apps/meteor/.stylelintignore @@ -1,3 +1,4 @@ app/theme/client/vendor/fontello/css/fontello.css app/meteor-autocomplete/client/autocomplete.css app/emoji-emojione/client/*.css +storybook-static diff --git a/apps/meteor/.stylelintrc b/apps/meteor/.stylelintrc index 0b2488ab89f3..f0a8cfc1475e 100644 --- a/apps/meteor/.stylelintrc +++ b/apps/meteor/.stylelintrc @@ -1,327 +1,59 @@ { - "plugins": [ - "stylelint-order" - ], - "rules": { - "at-rule-empty-line-before": [ "always", { - except: [ - "blockless-after-same-name-blockless", - "first-nested", - ], - ignore: ["after-comment"], - } ], - "at-rule-name-case": "lower", - "at-rule-name-space-after": "always", - "at-rule-semicolon-newline-after": "always", - "block-closing-brace-empty-line-before": "never", - "block-closing-brace-newline-after": "always", - "block-closing-brace-newline-before": "always", - "block-closing-brace-space-before": "never-single-line", + "extends": ["stylelint-config-standard"], + "plugins": ["stylelint-order", "stylelint-selector-bem-pattern"], + "rules": { + "at-rule-empty-line-before": [ + "always", + { + "except": ["blockless-after-same-name-blockless", "first-nested"], + "ignore": ["after-comment"] + } + ], "block-no-empty": true, - "block-opening-brace-newline-after": "always", - "block-opening-brace-space-after": "never-single-line", - "block-opening-brace-space-before": "always", - "color-hex-case": "lower", "color-hex-length": "long", "color-no-invalid-hex": true, - "comment-empty-line-before": [ "always", { - except: ["first-nested"], - ignore: ["stylelint-commands"], - } ], + "comment-empty-line-before": [ + "always", + { + "except": ["first-nested"], + "ignore": ["stylelint-commands"] + } + ], "comment-no-empty": true, "comment-whitespace-inside": "always", "custom-property-empty-line-before": "never", - "declaration-bang-space-after": "never", - "declaration-bang-space-before": "always", - "declaration-block-no-duplicate-properties": [ true, { - ignore: ["consecutive-duplicates-with-different-values"], - } ], + "declaration-block-no-duplicate-properties": [ + true, + { + "ignore": ["consecutive-duplicates-with-different-values"] + } + ], "declaration-block-no-redundant-longhand-properties": true, "declaration-block-no-shorthand-property-overrides": true, - "declaration-block-semicolon-newline-after": "always", - "declaration-block-semicolon-space-after": "always-single-line", - "declaration-block-semicolon-space-before": "never", "declaration-block-single-line-max-declarations": 1, - "declaration-block-trailing-semicolon": "always", - "declaration-colon-newline-after": "always-multi-line", - "declaration-colon-space-after": "always-single-line", - "declaration-colon-space-before": "never", "font-family-no-duplicate-names": true, - "function-comma-newline-after": "always-multi-line", - "function-comma-space-after": "always-single-line", - "function-comma-space-before": "never", "function-linear-gradient-no-nonstandard-direction": true, - "function-max-empty-lines": 0, "function-name-case": "lower", - "function-parentheses-newline-inside": "always-multi-line", - "function-parentheses-space-inside": "never-single-line", - "function-whitespace-after": "always", - "indentation": "tab", "keyframe-declaration-no-important": true, "length-zero-no-unit": true, - "max-empty-lines": 1, - "media-feature-colon-space-after": "always", - "media-feature-colon-space-before": "never", - "media-feature-name-case": "lower", "media-feature-name-no-unknown": true, - "media-feature-parentheses-space-inside": "never", - "media-feature-range-operator-space-after": "always", - "media-feature-range-operator-space-before": "always", - "media-query-list-comma-newline-after": "always-multi-line", - "media-query-list-comma-space-after": "always-single-line", - "media-query-list-comma-space-before": "never", "no-duplicate-selectors": true, "no-empty-source": true, - "no-eol-whitespace": true, - "no-extra-semicolons": true, - "no-missing-end-of-source-newline": true, - "number-leading-zero": "always", - "number-no-trailing-zeros": true, - "property-case": "lower", "property-no-unknown": true, - "rule-empty-line-before": [ "always", { - except: ["first-nested"], - ignore: ["after-comment"], - } ], - "selector-attribute-brackets-space-inside": "never", - "selector-attribute-operator-space-after": "never", - "selector-attribute-operator-space-before": "never", - "selector-combinator-space-after": "always", - "selector-combinator-space-before": "always", - "selector-descendant-combinator-no-non-space": true, - "selector-list-comma-newline-after": "always", - "selector-list-comma-space-before": "never", - "selector-max-empty-lines": 0, - "selector-pseudo-class-case": "lower", + "rule-empty-line-before": [ + "always", + { + "except": ["first-nested"], + "ignore": ["after-comment"] + } + ], "selector-pseudo-class-no-unknown": true, - "selector-pseudo-class-parentheses-space-inside": "never", - "selector-pseudo-element-case": "lower", "selector-pseudo-element-colon-notation": "double", "selector-pseudo-element-no-unknown": true, "selector-type-case": "lower", "selector-type-no-unknown": true, "shorthand-property-no-redundant-values": true, "string-no-newline": true, - "unit-case": "lower", - "unit-no-unknown": true, - "value-list-comma-newline-after": "always-multi-line", - "value-list-comma-space-after": "always-single-line", - "value-list-comma-space-before": "never", - "value-list-max-empty-lines": 0, - "order/properties-order": [ - [ - { - "emptyLineBefore": "always", - "order": "strict", - "properties": [ - "position", - "z-index", - "top", - "right", - "bottom", - "left" - ] - }, - { - "emptyLineBefore": "always", - "order": "strict", - "properties": [ - "display", - "visibility", - "float", - "clear", - "overflow", - "overflow-x", - "overflow-y", - "clip", - "zoom", - "flex-direction", - "flex-order", - "flex-pack", - "flex-align", - "flex" - ] - }, - { - "emptyLineBefore": "always", - "order": "strict", - "properties": [ - "box-sizing", - "width", - "min-width", - "max-width", - "height", - "min-height", - "max-height", - "margin", - "margin-top", - "margin-right", - "margin-bottom", - "margin-left", - "padding", - "padding-top", - "padding-right", - "padding-bottom", - "padding-left" - ] - }, - { - "emptyLineBefore": "always", - "order": "strict", - "properties": [ - "table-layout", - "empty-cells", - "caption-side", - "border-spacing", - "border-collapse", - "list-style", - "list-style-position", - "list-style-type", - "list-style-image" - ] - }, - { - "emptyLineBefore": "always", - "order": "strict", - "properties": [ - "content", - "quotes", - "counter-reset", - "counter-increment", - "resize", - "cursor", - "user-select", - "nav-index", - "nav-up", - "nav-right", - "nav-down", - "nav-left", - "transition", - "transition-delay", - "transition-timing-function", - "transition-duration", - "transition-property", - "transform", - "transform-origin", - "animation", - "animation-name", - "animation-duration", - "animation-play-state", - "animation-timing-function", - "animation-delay", - "animation-iteration-count", - "animation-direction", - "text-align", - "text-align-last", - "vertical-align", - "white-space", - "text-decoration", - "text-emphasis", - "text-emphasis-color", - "text-emphasis-style", - "text-emphasis-position", - "text-indent", - "text-justify", - "text-transform", - "letter-spacing", - "word-spacing", - "text-outline", - "text-transform", - "text-wrap", - "text-overflow", - "text-overflow-ellipsis", - "text-overflow-mode", - "word-wrap", - "word-break", - "tab-size", - "hyphens", - "pointer-events" - ] - }, - { - "emptyLineBefore": "always", - "order": "strict", - "properties": [ - "opacity", - "filter:progid:DXImageTransform.Microsoft.Alpha(Opacity", - "color", - "border", - "border-collapse", - "border-width", - "border-style", - "border-color", - "border-top", - "border-top-width", - "border-top-style", - "border-top-color", - "border-right", - "border-right-width", - "border-right-style", - "border-right-color", - "border-bottom", - "border-bottom-width", - "border-bottom-style", - "border-bottom-color", - "border-left", - "border-left-width", - "border-left-style", - "border-left-color", - "border-radius", - "border-top-left-radius", - "border-top-right-radius", - "border-bottom-right-radius", - "border-bottom-left-radius", - "border-image", - "border-image-source", - "border-image-slice", - "border-image-width", - "border-image-outset", - "border-image-repeat", - "outline", - "outline-width", - "outline-style", - "outline-color", - "outline-offset", - "background", - "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader", - "background-color", - "background-image", - "background-repeat", - "background-attachment", - "background-position", - "background-position-x", - "background-position-y", - "background-clip", - "background-origin", - "background-size", - "box-decoration-break", - "box-shadow", - "filter:progid:DXImageTransform.Microsoft.gradient", - "text-shadow" - ] - }, - { - "emptyLineBefore": "always", - "order": "strict", - "properties": [ - "font", - "font-family", - "font-size", - "font-weight", - "font-style", - "font-variant", - "font-size-adjust", - "font-stretch", - "font-effect", - "font-emphasize", - "font-emphasize-position", - "font-emphasize-style", - "font-smooth", - "line-height" - ] - } - ], - { unspecified: "bottomAlphabetical" } - ] + "unit-no-unknown": true } } diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 60da08d7fcd5..179042fce8a9 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,219 @@ # @rocket.chat/meteor +## 7.1.0-rc.3 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 + - @rocket.chat/rest-typings@7.1.0-rc.3 + - @rocket.chat/license@1.0.1-rc.3 + - @rocket.chat/omnichannel-services@0.3.7-rc.3 + - @rocket.chat/pdf-worker@0.2.7-rc.3 + - @rocket.chat/presence@0.2.10-rc.3 + - @rocket.chat/api-client@0.2.10-rc.3 + - @rocket.chat/apps@0.2.1-rc.3 + - @rocket.chat/core-services@0.7.2-rc.3 + - @rocket.chat/cron@0.1.10-rc.3 + - @rocket.chat/freeswitch@1.0.1-rc.3 + - @rocket.chat/fuselage-ui-kit@13.0.0-rc.3 + - @rocket.chat/gazzodown@13.0.0-rc.3 + - @rocket.chat/model-typings@1.1.0-rc.3 + - @rocket.chat/ui-contexts@13.0.0-rc.3 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.1.2-rc.3 + - @rocket.chat/models@1.0.1-rc.3 + - @rocket.chat/ui-theming@0.4.0 + - @rocket.chat/ui-avatar@9.0.0-rc.3 + - @rocket.chat/ui-client@13.0.0-rc.3 + - @rocket.chat/ui-video-conf@13.0.0-rc.3 + - @rocket.chat/ui-voip@3.0.0-rc.3 + - @rocket.chat/web-ui-registration@13.0.0-rc.3 + - @rocket.chat/instance-status@0.1.10-rc.3 +
+ +## 7.1.0-rc.2 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 + - @rocket.chat/rest-typings@7.1.0-rc.2 + - @rocket.chat/license@1.0.1-rc.2 + - @rocket.chat/omnichannel-services@0.3.7-rc.2 + - @rocket.chat/pdf-worker@0.2.7-rc.2 + - @rocket.chat/presence@0.2.10-rc.2 + - @rocket.chat/api-client@0.2.10-rc.2 + - @rocket.chat/apps@0.2.1-rc.2 + - @rocket.chat/core-services@0.7.2-rc.2 + - @rocket.chat/cron@0.1.10-rc.2 + - @rocket.chat/freeswitch@1.0.1-rc.2 + - @rocket.chat/fuselage-ui-kit@13.0.0-rc.2 + - @rocket.chat/gazzodown@13.0.0-rc.2 + - @rocket.chat/model-typings@1.1.0-rc.2 + - @rocket.chat/ui-contexts@13.0.0-rc.2 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.1.2-rc.2 + - @rocket.chat/models@1.0.1-rc.2 + - @rocket.chat/ui-theming@0.4.0 + - @rocket.chat/ui-avatar@9.0.0-rc.2 + - @rocket.chat/ui-client@13.0.0-rc.2 + - @rocket.chat/ui-video-conf@13.0.0-rc.2 + - @rocket.chat/ui-voip@3.0.0-rc.2 + - @rocket.chat/web-ui-registration@13.0.0-rc.2 + - @rocket.chat/instance-status@0.1.10-rc.2 +
+ +## 7.1.0-rc.1 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.1 + - @rocket.chat/rest-typings@7.1.0-rc.1 + - @rocket.chat/license@1.0.1-rc.1 + - @rocket.chat/omnichannel-services@0.3.7-rc.1 + - @rocket.chat/pdf-worker@0.2.7-rc.1 + - @rocket.chat/presence@0.2.10-rc.1 + - @rocket.chat/api-client@0.2.10-rc.1 + - @rocket.chat/apps@0.2.1-rc.1 + - @rocket.chat/core-services@0.7.2-rc.1 + - @rocket.chat/cron@0.1.10-rc.1 + - @rocket.chat/freeswitch@1.0.1-rc.1 + - @rocket.chat/fuselage-ui-kit@13.0.0-rc.1 + - @rocket.chat/gazzodown@13.0.0-rc.1 + - @rocket.chat/model-typings@1.1.0-rc.1 + - @rocket.chat/ui-contexts@13.0.0-rc.1 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.1.2-rc.1 + - @rocket.chat/models@1.0.1-rc.1 + - @rocket.chat/ui-theming@0.4.0 + - @rocket.chat/ui-avatar@9.0.0-rc.1 + - @rocket.chat/ui-client@13.0.0-rc.1 + - @rocket.chat/ui-video-conf@13.0.0-rc.1 + - @rocket.chat/ui-voip@3.0.0-rc.1 + - @rocket.chat/web-ui-registration@13.0.0-rc.1 + - @rocket.chat/instance-status@0.1.10-rc.1 +
+ +## 7.1.0-rc.0 + +### Minor Changes + +- ([#33897](https://github.com/RocketChat/Rocket.Chat/pull/33897)) adds unread badge to sidebar collapser + +- ([#32906](https://github.com/RocketChat/Rocket.Chat/pull/32906)) Improves thread metrics featuring user avatars, better titles and repositioned elements. + +- ([#33810](https://github.com/RocketChat/Rocket.Chat/pull/33810)) Adds cursor pagination on chat.syncMessages endpoint + +- ([#33214](https://github.com/RocketChat/Rocket.Chat/pull/33214)) Adds a new route to allow fetching avatars by the user's id `/avatar/uid/` + +- ([#32727](https://github.com/RocketChat/Rocket.Chat/pull/32727)) These changes aims to add: + - A brand-new omnichannel contact profile + - The ability to communicate with known contacts only + - Communicate with verified contacts only + - Merge verified contacts across different channels + - Block contact channels + - Resolve conflicting contact information when registered via different channels + - An advanced contact center filters +- ([#33920](https://github.com/RocketChat/Rocket.Chat/pull/33920)) Improves the customizability of the naming of automatic Persistent video calls discussions, allowing the date of the call to be in different parts of the name, using the `[date]` keyword. + +- ([#33997](https://github.com/RocketChat/Rocket.Chat/pull/33997)) Prevent apps' subprocesses from crashing on unhandled rejections or uncaught exceptions + +- ([#33814](https://github.com/RocketChat/Rocket.Chat/pull/33814)) Adds a confirmation modal to the cancel subscription action + +- ([#33949](https://github.com/RocketChat/Rocket.Chat/pull/33949)) Disables the possiblity to upload exempted apps + +### Patch Changes + +- ([#33776](https://github.com/RocketChat/Rocket.Chat/pull/33776)) Fix user highlights not matching only whole words + +- ([#33818](https://github.com/RocketChat/Rocket.Chat/pull/33818)) Remove unused client side `setUserActiveStatus` meteor method. + +- ([#33596](https://github.com/RocketChat/Rocket.Chat/pull/33596)) Bump meteor to 3.0.4 and Node version to 20.18.0 + +- ([#33713](https://github.com/RocketChat/Rocket.Chat/pull/33713)) Deprecated the `from` field in the apps email bridge and made it optional, using the server's settings when the field is omitted + +- ([#32991](https://github.com/RocketChat/Rocket.Chat/pull/32991)) Fixes an issue where updating custom emojis didn’t work as expected, ensuring that uploaded emojis now update correctly and display without any caching problems. + +- ([#33985](https://github.com/RocketChat/Rocket.Chat/pull/33985)) Fixes issue that could cause multiple discussions to be created when creating it from a message action + +- ([#33904](https://github.com/RocketChat/Rocket.Chat/pull/33904)) adds missing html attributes in sidebar item templates + +- ([#33218](https://github.com/RocketChat/Rocket.Chat/pull/33218)) Fixes message character limit not being applied to file upload descriptions + +- ([#33908](https://github.com/RocketChat/Rocket.Chat/pull/33908)) Fixes the issue where newly created teams are incorrectly displayed as channels on the sidebar when the DISABLE_DB_WATCHERS environment variable is enabled + +- ([#33953](https://github.com/RocketChat/Rocket.Chat/pull/33953)) Adds a divider on Create team modal - advanced settings + +- ([#33786](https://github.com/RocketChat/Rocket.Chat/pull/33786)) Fixed an issue that would grant network permission to app's processes in wrong cases + +- ([#33986](https://github.com/RocketChat/Rocket.Chat/pull/33986)) Fixes sidepanel not replicating sidebar sort preference + +- ([#33689](https://github.com/RocketChat/Rocket.Chat/pull/33689)) Fixes banner breaking the UI with specific payloads + +- ([#33808](https://github.com/RocketChat/Rocket.Chat/pull/33808)) Fixes client-side updates for recent emoji list when custom emojis are modified. + +- ([#33902](https://github.com/RocketChat/Rocket.Chat/pull/33902)) Adds "Master volume" and "Call ringer volume" to the user preferences sound section. + +- ([#33311](https://github.com/RocketChat/Rocket.Chat/pull/33311)) Sends server statistics only once a day despite multiple instance being started at different times. + +- ([#33719](https://github.com/RocketChat/Rocket.Chat/pull/33719)) stops calling an object through proxy calling getQueueWorker + +- ([#33785](https://github.com/RocketChat/Rocket.Chat/pull/33785)) Fixed an issue where the installed apps list would go stale without a refresh in some cases + +- ([#33278](https://github.com/RocketChat/Rocket.Chat/pull/33278)) Fixes display of emoji aliases in custom emoji list by adding commas between aliases + +- ([#33772](https://github.com/RocketChat/Rocket.Chat/pull/33772)) Fixes `waiting queue` feature. When `Livechat_waiting_queue` setting is enabled, incoming conversations should be sent to the queue instead of being assigned directly. + +- ([#33963](https://github.com/RocketChat/Rocket.Chat/pull/33963)) Fixes edge case of thread unread not being added to unread group + +- ([#33994](https://github.com/RocketChat/Rocket.Chat/pull/33994)) Adds login and permission validation for resetIrcConnection method + +- ([#33880](https://github.com/RocketChat/Rocket.Chat/pull/33880)) Updates VoIP field labels from 'Free Extension Numbers' to 'Available Extensions' to better describe the field's purpose and improve clarity. + +- ([#33958](https://github.com/RocketChat/Rocket.Chat/pull/33958)) Fixes an issue where resizable handler renders over the expanded thread view while using contextualbarResizable feature preview + +-
Updated dependencies [82767d8fd8a52ac348e8aded1d238e688d36129b, 80e36bfc3938775eb26aa5576f1b9b98896e1cc4, 66ecc64fc1d4464ad2818ad04e23a09cdf221194, 6c83bf0657004ee9cf43d5c832f51826a6591165, e7edeac3bdd22da0a04b8e873d5a008e249fb4be, 3569b0a9c48f8b94ebaef2f8b607c52fdb8e570a, b4841cb7206d855d7a1bc7604683a5b4a48b7176, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0, d1e6a73796269824fb1aa7afcc7b8aa242e34e90, 661cc01237629ce83699d6c25df25d12985e88bf, 63ccadc012499e004445ad6bc6cd2ff777aecbd1, ce7024af36fcde97b1da5b2731f6edc4a4c236b8, 616655585cb1c5c60d7cee97e25b17af3dfda794, e5fe727f6a2f0e60cdf7ba225e1f6caa6db2045c, d398866dba725918017e3609807f9d0ab9b89b72, 322bafd4bd1fe91ed34610501b269e4d8951944c, d398866dba725918017e3609807f9d0ab9b89b72]: + + - @rocket.chat/apps-engine@1.48.0-rc.0 + - @rocket.chat/model-typings@1.1.0-rc.0 + - @rocket.chat/core-typings@7.1.0-rc.0 + - @rocket.chat/i18n@1.1.0-rc.0 + - @rocket.chat/rest-typings@7.1.0-rc.0 + - @rocket.chat/core-services@0.7.2-rc.0 + - @rocket.chat/presence@0.2.10-rc.0 + - @rocket.chat/apps@0.2.1-rc.0 + - @rocket.chat/fuselage-ui-kit@13.0.0-rc.0 + - @rocket.chat/omnichannel-services@0.3.7-rc.0 + - @rocket.chat/models@1.0.1-rc.0 + - @rocket.chat/license@1.0.1-rc.0 + - @rocket.chat/pdf-worker@0.2.7-rc.0 + - @rocket.chat/api-client@0.2.10-rc.0 + - @rocket.chat/cron@0.1.10-rc.0 + - @rocket.chat/freeswitch@1.0.1-rc.0 + - @rocket.chat/gazzodown@13.0.0-rc.0 + - @rocket.chat/ui-contexts@13.0.0-rc.0 + - @rocket.chat/web-ui-registration@13.0.0-rc.0 + - @rocket.chat/network-broker@0.1.2-rc.0 + - @rocket.chat/instance-status@0.1.10-rc.0 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/ui-theming@0.4.0 + - @rocket.chat/ui-avatar@9.0.0-rc.0 + - @rocket.chat/ui-client@13.0.0-rc.0 + - @rocket.chat/ui-video-conf@13.0.0-rc.0 + - @rocket.chat/ui-voip@3.0.0-rc.0 +
+ ## 7.0.0 ### Major Changes diff --git a/apps/meteor/app/2fa/server/code/EmailCheck.ts b/apps/meteor/app/2fa/server/code/EmailCheck.ts index 5baf218a62bb..8c9865c0b004 100644 --- a/apps/meteor/app/2fa/server/code/EmailCheck.ts +++ b/apps/meteor/app/2fa/server/code/EmailCheck.ts @@ -4,10 +4,10 @@ import { Random } from '@rocket.chat/random'; import bcrypt from 'bcrypt'; import { Accounts } from 'meteor/accounts-base'; +import type { ICodeCheck, IProcessInvalidCodeResult } from './ICodeCheck'; import { i18n } from '../../../../server/lib/i18n'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; -import type { ICodeCheck, IProcessInvalidCodeResult } from './ICodeCheck'; export class EmailCheck implements ICodeCheck { public readonly name = 'email'; diff --git a/apps/meteor/app/2fa/server/code/PasswordCheckFallback.ts b/apps/meteor/app/2fa/server/code/PasswordCheckFallback.ts index 6c441127f79d..ba462abc1f93 100644 --- a/apps/meteor/app/2fa/server/code/PasswordCheckFallback.ts +++ b/apps/meteor/app/2fa/server/code/PasswordCheckFallback.ts @@ -1,8 +1,8 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Accounts } from 'meteor/accounts-base'; -import { settings } from '../../../settings/server'; import type { ICodeCheck, IProcessInvalidCodeResult } from './ICodeCheck'; +import { settings } from '../../../settings/server'; export class PasswordCheckFallback implements ICodeCheck { public readonly name = 'password'; diff --git a/apps/meteor/app/2fa/server/code/TOTPCheck.ts b/apps/meteor/app/2fa/server/code/TOTPCheck.ts index 3aa2604c7965..236016883f89 100644 --- a/apps/meteor/app/2fa/server/code/TOTPCheck.ts +++ b/apps/meteor/app/2fa/server/code/TOTPCheck.ts @@ -1,8 +1,8 @@ import type { IUser } from '@rocket.chat/core-typings'; +import type { ICodeCheck, IProcessInvalidCodeResult } from './ICodeCheck'; import { settings } from '../../../settings/server'; import { TOTP } from '../lib/totp'; -import type { ICodeCheck, IProcessInvalidCodeResult } from './ICodeCheck'; export class TOTPCheck implements ICodeCheck { public readonly name = 'totp'; diff --git a/apps/meteor/app/2fa/server/code/index.ts b/apps/meteor/app/2fa/server/code/index.ts index b05157416e31..d4be080259cf 100644 --- a/apps/meteor/app/2fa/server/code/index.ts +++ b/apps/meteor/app/2fa/server/code/index.ts @@ -5,11 +5,11 @@ import { Users } from '@rocket.chat/models'; import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; -import { settings } from '../../../settings/server'; import { EmailCheck } from './EmailCheck'; import type { ICodeCheck } from './ICodeCheck'; import { PasswordCheckFallback } from './PasswordCheckFallback'; import { TOTPCheck } from './TOTPCheck'; +import { settings } from '../../../settings/server'; export interface ITwoFactorOptions { disablePasswordFallback?: boolean; diff --git a/apps/meteor/app/2fa/server/loginHandler.ts b/apps/meteor/app/2fa/server/loginHandler.ts index a51ba72ec7fa..b554330e140f 100644 --- a/apps/meteor/app/2fa/server/loginHandler.ts +++ b/apps/meteor/app/2fa/server/loginHandler.ts @@ -3,8 +3,8 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { OAuth } from 'meteor/oauth'; -import { callbacks } from '../../../lib/callbacks'; import { checkCodeForUser } from './code/index'; +import { callbacks } from '../../../lib/callbacks'; const isMeteorError = (error: any): error is Meteor.Error => { return error?.meteorError !== undefined; diff --git a/apps/meteor/app/api/server/api.ts b/apps/meteor/app/api/server/api.ts index 7bb7f5af28ba..51930f1e9e50 100644 --- a/apps/meteor/app/api/server/api.ts +++ b/apps/meteor/app/api/server/api.ts @@ -14,15 +14,6 @@ import type { Request, Response } from 'meteor/rocketchat:restivus'; import { Restivus } from 'meteor/rocketchat:restivus'; import _ from 'underscore'; -import { isObject } from '../../../lib/utils/isObject'; -import { getNestedProp } from '../../../server/lib/getNestedProp'; -import { getRestPayload } from '../../../server/lib/logger/logPayloads'; -import { checkCodeForUser } from '../../2fa/server/code'; -import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; -import { notifyOnUserChangeAsync } from '../../lib/server/lib/notifyListener'; -import { metrics } from '../../metrics/server'; -import { settings } from '../../settings/server'; -import { getDefaultUserFields } from '../../utils/server/functions/getDefaultUserFields'; import type { PermissionsPayload } from './api.helpers'; import { checkPermissionsForInvocation, checkPermissions, parseDeprecation } from './api.helpers'; import type { @@ -37,6 +28,15 @@ import type { } from './definition'; import { getUserInfo } from './helpers/getUserInfo'; import { parseJsonQuery } from './helpers/parseJsonQuery'; +import { isObject } from '../../../lib/utils/isObject'; +import { getNestedProp } from '../../../server/lib/getNestedProp'; +import { getRestPayload } from '../../../server/lib/logger/logPayloads'; +import { checkCodeForUser } from '../../2fa/server/code'; +import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; +import { notifyOnUserChangeAsync } from '../../lib/server/lib/notifyListener'; +import { metrics } from '../../metrics/server'; +import { settings } from '../../settings/server'; +import { getDefaultUserFields } from '../../utils/server/functions/getDefaultUserFields'; const logger = new Logger('API'); @@ -189,10 +189,13 @@ export class APIClass extends Restivus { } public setLimitedCustomFields(customFields: string[]): void { - const nonPublicFieds = customFields.reduce((acc, customField) => { - acc[`customFields.${customField}`] = 0; - return acc; - }, {} as Record); + const nonPublicFieds = customFields.reduce( + (acc, customField) => { + acc[`customFields.${customField}`] = 0; + return acc; + }, + {} as Record, + ); this.limitedUserFieldsToExclude = { ...this.defaultLimitedUserFieldsToExclude, ...nonPublicFieds, diff --git a/apps/meteor/app/api/server/definition.ts b/apps/meteor/app/api/server/definition.ts index acace73a8226..c15e0bc10d98 100644 --- a/apps/meteor/app/api/server/definition.ts +++ b/apps/meteor/app/api/server/definition.ts @@ -22,7 +22,7 @@ export type FailureResult } ? T : TOptions extends { validateParams: { GET: ValidateFunction } } - ? T - : Partial> & { offset?: number; count?: number } + ? T + : Partial> & { offset?: number; count?: number } : Record; // TODO make it unsafe readonly bodyParams: TMethod extends 'GET' ? Record : TOptions extends { validateParams: ValidateFunction } - ? T - : TOptions extends { validateParams: infer V } - ? V extends { [key in TMethod]: ValidateFunction } ? T - : Partial> - : // TODO remove the extra (optionals) params when all the endpoints that use these are typed correctly - Partial>; + : TOptions extends { validateParams: infer V } + ? V extends { [key in TMethod]: ValidateFunction } + ? T + : Partial> + : // TODO remove the extra (optionals) params when all the endpoints that use these are typed correctly + Partial>; readonly request: Request; readonly queryOperations: TOptions extends { queryOperations: infer T } ? T : never; @@ -163,18 +163,18 @@ export type ActionThis = | SuccessResult> diff --git a/apps/meteor/app/api/server/lib/getServerInfo.spec.ts b/apps/meteor/app/api/server/lib/getServerInfo.spec.ts index ca55cfa33e3e..574519d92858 100644 --- a/apps/meteor/app/api/server/lib/getServerInfo.spec.ts +++ b/apps/meteor/app/api/server/lib/getServerInfo.spec.ts @@ -22,7 +22,8 @@ const { getServerInfo } = proxyquire.noCallThru().load('./getServerInfo', { settings: new Map(), }, }); -describe('#getServerInfo()', () => { +// #ToDo: Fix those tests in a separate PR +describe.skip('#getServerInfo()', () => { beforeEach(() => { hasAllPermissionAsyncMock.reset(); getCachedSupportedVersionsTokenMock.reset(); diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.spec.ts b/apps/meteor/app/api/server/lib/getUploadFormData.spec.ts new file mode 100644 index 000000000000..dc7afb77bd19 --- /dev/null +++ b/apps/meteor/app/api/server/lib/getUploadFormData.spec.ts @@ -0,0 +1,178 @@ +import { Readable } from 'stream'; + +import { expect } from 'chai'; +import type { Request } from 'express'; + +import { getUploadFormData } from './getUploadFormData'; + +const createMockRequest = ( + fields: Record, + file?: { + fieldname: string; + filename: string; + content: string | Buffer; + mimetype?: string; + }, +): Readable & { headers: Record } => { + const boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW'; + const parts: string[] = []; + + if (file) { + parts.push( + `--${boundary}`, + `Content-Disposition: form-data; name="${file.fieldname}"; filename="${file.filename}"`, + `Content-Type: ${file.mimetype || 'application/octet-stream'}`, + '', + file.content.toString(), + ); + } + + for (const [name, value] of Object.entries(fields)) { + parts.push(`--${boundary}`, `Content-Disposition: form-data; name="${name}"`, '', value); + } + + parts.push(`--${boundary}--`); + + const mockRequest: any = new Readable({ + read() { + this.push(Buffer.from(parts.join('\r\n'))); + this.push(null); + }, + }); + + mockRequest.headers = { + 'content-type': `multipart/form-data; boundary=${boundary}`, + }; + + return mockRequest as Readable & { headers: Record }; +}; + +describe('getUploadFormData', () => { + it('should successfully parse a single file upload and fields', async () => { + const mockRequest = createMockRequest( + { fieldName: 'fieldValue' }, + { + fieldname: 'fileField', + filename: 'test.txt', + content: 'Hello, this is a test file!', + mimetype: 'text/plain', + }, + ); + + const result = await getUploadFormData({ request: mockRequest as Request }, { field: 'fileField' }); + + expect(result).to.deep.include({ + fieldname: 'fileField', + filename: 'test.txt', + mimetype: 'text/plain', + fields: { fieldName: 'fieldValue' }, + }); + + expect(result.fileBuffer).to.not.be.undefined; + expect(result.fileBuffer.toString()).to.equal('Hello, this is a test file!'); + }); + it('should parse a file upload with multiple additional fields', async () => { + const mockRequest = createMockRequest( + { + fieldName: 'fieldValue', + extraField1: 'extraValue1', + extraField2: 'extraValue2', + }, + { + fieldname: 'fileField', + filename: 'test_with_fields.txt', + content: 'This file has additional fields!', + mimetype: 'text/plain', + }, + ); + + const result = await getUploadFormData({ request: mockRequest as Request }, { field: 'fileField' }); + + expect(result).to.deep.include({ + fieldname: 'fileField', + filename: 'test_with_fields.txt', + mimetype: 'text/plain', + fields: { + fieldName: 'fieldValue', + extraField1: 'extraValue1', + extraField2: 'extraValue2', + }, + }); + + expect(result.fileBuffer).to.not.be.undefined; + expect(result.fileBuffer.toString()).to.equal('This file has additional fields!'); + }); + + it('should handle a file upload when fileOptional is true', async () => { + const mockRequest = createMockRequest( + { fieldName: 'fieldValue' }, + { + fieldname: 'fileField', + filename: 'optional.txt', + content: 'This file is optional!', + mimetype: 'text/plain', + }, + ); + + const result = await getUploadFormData({ request: mockRequest as Request }, { fileOptional: true }); + + expect(result).to.deep.include({ + fieldname: 'fileField', + filename: 'optional.txt', + mimetype: 'text/plain', + fields: { fieldName: 'fieldValue' }, + }); + + expect(result.fileBuffer).to.not.be.undefined; + expect(result.fileBuffer?.toString()).to.equal('This file is optional!'); + }); + + it('should throw an error when no file is uploaded and fileOptional is false', async () => { + const mockRequest = createMockRequest({ fieldName: 'fieldValue' }); + + try { + await getUploadFormData({ request: mockRequest as Request }, { fileOptional: false }); + throw new Error('Expected function to throw'); + } catch (error) { + expect((error as Error).message).to.equal('[No file uploaded]'); + } + }); + + it('should return fields without errors when no file is uploaded but fileOptional is true', async () => { + const mockRequest = createMockRequest({ fieldName: 'fieldValue' }); // No file + + const result = await getUploadFormData({ request: mockRequest as Request }, { fileOptional: true }); + + expect(result).to.deep.equal({ + fields: { fieldName: 'fieldValue' }, + file: undefined, + fileBuffer: undefined, + fieldname: undefined, + filename: undefined, + encoding: undefined, + mimetype: undefined, + }); + }); + + it('should reject an oversized file', async () => { + const mockRequest = createMockRequest( + {}, + { + fieldname: 'fileField', + filename: 'large.txt', + content: 'x'.repeat(1024 * 1024 * 2), // 2 MB file + mimetype: 'text/plain', + }, + ); + + try { + await getUploadFormData( + { request: mockRequest as Request }, + { sizeLimit: 1024 * 1024 }, // 1 MB limit + ); + throw new Error('Expected function to throw'); + } catch (error) { + expect((error as Error).message).to.equal('[error-file-too-large]'); + } + }); +}); diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.ts b/apps/meteor/app/api/server/lib/getUploadFormData.ts index 3136a6c16e13..93ceafdde92f 100644 --- a/apps/meteor/app/api/server/lib/getUploadFormData.ts +++ b/apps/meteor/app/api/server/lib/getUploadFormData.ts @@ -17,6 +17,42 @@ type UploadResult = { fields: K; }; +type UploadResultWithOptionalFile = + | UploadResult + | ({ + [P in keyof Omit, 'fields'>]: undefined; + } & { + fields: K; + }); + +export async function getUploadFormData< + T extends string, + K extends Record = Record, + V extends ValidateFunction = ValidateFunction, +>( + { request }: { request: Request }, + options: { + field?: T; + validate?: V; + sizeLimit?: number; + fileOptional: true; + }, +): Promise>; + +export async function getUploadFormData< + T extends string, + K extends Record = Record, + V extends ValidateFunction = ValidateFunction, +>( + { request }: { request: Request }, + options?: { + field?: T; + validate?: V; + sizeLimit?: number; + fileOptional?: false | undefined; + }, +): Promise>; + export async function getUploadFormData< T extends string, K extends Record = Record, @@ -27,8 +63,9 @@ export async function getUploadFormData< field?: T; validate?: V; sizeLimit?: number; + fileOptional?: boolean; } = {}, -): Promise> { +): Promise> { const limits = { files: 1, ...(options.sizeLimit && options.sizeLimit > -1 && { fileSize: options.sizeLimit }), @@ -37,9 +74,17 @@ export async function getUploadFormData< const bb = busboy({ headers: request.headers, defParamCharset: 'utf8', limits }); const fields = Object.create(null) as K; - let uploadedFile: UploadResult | undefined; + let uploadedFile: UploadResultWithOptionalFile | undefined = { + fields, + encoding: undefined, + filename: undefined, + fieldname: undefined, + mimetype: undefined, + fileBuffer: undefined, + file: undefined, + }; - let returnResult = (_value: UploadResult) => { + let returnResult = (_value: UploadResultWithOptionalFile) => { // noop }; let returnError = (_error?: Error | string | null | undefined) => { @@ -52,6 +97,9 @@ export async function getUploadFormData< function onEnd() { if (!uploadedFile) { + return returnError(new MeteorError('No file or fields were uploaded')); + } + if (!options.fileOptional && !uploadedFile?.file) { return returnError(new MeteorError('No file uploaded')); } if (options.validate !== undefined && !options.validate(fields)) { @@ -121,7 +169,7 @@ export async function getUploadFormData< request.pipe(bb); - return new Promise((resolve, reject) => { + return new Promise>((resolve, reject) => { returnResult = resolve; returnError = reject; }); diff --git a/apps/meteor/app/api/server/lib/maybeMigrateLivechatRoom.ts b/apps/meteor/app/api/server/lib/maybeMigrateLivechatRoom.ts new file mode 100644 index 000000000000..3a66cef2e416 --- /dev/null +++ b/apps/meteor/app/api/server/lib/maybeMigrateLivechatRoom.ts @@ -0,0 +1,40 @@ +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import type { IOmnichannelRoom, IRoom } from '@rocket.chat/core-typings'; +import { LivechatRooms } from '@rocket.chat/models'; +import type { FindOptions } from 'mongodb'; + +import { projectionAllowsAttribute } from './projectionAllowsAttribute'; +import { migrateVisitorIfMissingContact } from '../../../livechat/server/lib/contacts/migrateVisitorIfMissingContact'; + +/** + * If the room is a livechat room and it doesn't yet have a contact, trigger the migration for its visitor and source + * The migration will create/use a contact and assign it to every room that matches this visitorId and source. + **/ +export async function maybeMigrateLivechatRoom( + room: IOmnichannelRoom | null, + options: FindOptions = {}, +): Promise { + if (!room || !isOmnichannelRoom(room)) { + return room; + } + + // Already migrated + if (room.contactId) { + return room; + } + + // If the query options specify that contactId is not needed, then do not trigger the migration + if (!projectionAllowsAttribute('contactId', options)) { + return room; + } + + const contactId = await migrateVisitorIfMissingContact(room.v._id, room.source); + + // Did not migrate + if (!contactId) { + return room; + } + + // Load the room again with the same options so it can be reloaded with the contactId in place + return LivechatRooms.findOneById(room._id, options); +} diff --git a/apps/meteor/app/api/server/lib/projectionAllowsAttribute.spec.ts b/apps/meteor/app/api/server/lib/projectionAllowsAttribute.spec.ts new file mode 100644 index 000000000000..a637f9e2a7db --- /dev/null +++ b/apps/meteor/app/api/server/lib/projectionAllowsAttribute.spec.ts @@ -0,0 +1,29 @@ +import { expect } from 'chai'; + +import { projectionAllowsAttribute } from './projectionAllowsAttribute'; + +describe('projectionAllowsAttribute', () => { + it('should return true if there are no options', () => { + expect(projectionAllowsAttribute('attributeName')).to.be.equal(true); + }); + + it('should return true if there is no projection', () => { + expect(projectionAllowsAttribute('attributeName', {})).to.be.equal(true); + }); + + it('should return true if the field is projected', () => { + expect(projectionAllowsAttribute('attributeName', { projection: { attributeName: 1 } })).to.be.equal(true); + }); + + it('should return false if the field is disallowed by projection', () => { + expect(projectionAllowsAttribute('attributeName', { projection: { attributeName: 0 } })).to.be.equal(false); + }); + + it('should return false if the field is not projected and others are', () => { + expect(projectionAllowsAttribute('attributeName', { projection: { anotherAttribute: 1 } })).to.be.equal(false); + }); + + it('should return true if the field is not projected and others are disallowed', () => { + expect(projectionAllowsAttribute('attributeName', { projection: { anotherAttribute: 0 } })).to.be.equal(true); + }); +}); diff --git a/apps/meteor/app/api/server/lib/projectionAllowsAttribute.ts b/apps/meteor/app/api/server/lib/projectionAllowsAttribute.ts new file mode 100644 index 000000000000..a71f6dada960 --- /dev/null +++ b/apps/meteor/app/api/server/lib/projectionAllowsAttribute.ts @@ -0,0 +1,19 @@ +import type { IRocketChatRecord } from '@rocket.chat/core-typings'; +import type { FindOptions } from 'mongodb'; + +export function projectionAllowsAttribute(attributeName: string, options?: FindOptions): boolean { + if (!options?.projection) { + return true; + } + + if (attributeName in options.projection) { + return Boolean(options.projection[attributeName]); + } + + const projectingAllowedFields = Object.values(options.projection).some((value) => Boolean(value)); + + // If the attribute is not on the projection list, return the opposite of the values in the projection. aka: + // if the projection is specifying blocked fields, then this field is allowed; + // if the projection is specifying allowed fields, then this field is blocked; + return !projectingAllowedFields; +} diff --git a/apps/meteor/app/api/server/lib/users.ts b/apps/meteor/app/api/server/lib/users.ts index 164ea8c2b747..aa99d9216667 100644 --- a/apps/meteor/app/api/server/lib/users.ts +++ b/apps/meteor/app/api/server/lib/users.ts @@ -183,20 +183,22 @@ export async function findPaginatedUsersByStatus({ ...(canSeeExtension ? { freeSwitchExtension: 1 } : {}), }; - match.$or = [ - ...(canSeeAllUserInfo ? [{ 'emails.address': { $regex: escapeRegExp(searchTerm || ''), $options: 'i' } }] : []), - { - username: { $regex: escapeRegExp(searchTerm || ''), $options: 'i' }, - }, - { - name: { $regex: escapeRegExp(searchTerm || ''), $options: 'i' }, - }, - ]; + if (searchTerm?.trim()) { + match.$or = [ + ...(canSeeAllUserInfo ? [{ 'emails.address': { $regex: escapeRegExp(searchTerm || ''), $options: 'i' } }] : []), + { + username: { $regex: escapeRegExp(searchTerm || ''), $options: 'i' }, + }, + { + name: { $regex: escapeRegExp(searchTerm || ''), $options: 'i' }, + }, + ]; + } if (roles?.length && !roles.includes('all')) { match.roles = { $in: roles }; } - const { cursor, totalCount } = await Users.findPaginated( + const { cursor, totalCount } = Users.findPaginated( { ...match, }, diff --git a/apps/meteor/app/api/server/v1/assets.ts b/apps/meteor/app/api/server/v1/assets.ts index 395bbd791afa..fd9f31d40923 100644 --- a/apps/meteor/app/api/server/v1/assets.ts +++ b/apps/meteor/app/api/server/v1/assets.ts @@ -1,13 +1,19 @@ +import { Settings } from '@rocket.chat/models'; import { isAssetsUnsetAssetProps } from '@rocket.chat/rest-typings'; -import { RocketChatAssets, setAsset, unsetAsset, refreshClients } from '../../../assets/server'; +import { updateAuditedByUser } from '../../../../server/settings/lib/auditedSettingUpdates'; +import { RocketChatAssets, refreshClients } from '../../../assets/server'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { API } from '../api'; import { getUploadFormData } from '../lib/getUploadFormData'; API.v1.addRoute( 'assets.setAsset', - { authRequired: true }, + { + authRequired: true, + permissionsRequired: ['manage-assets'], + }, { async post() { const asset = await getUploadFormData( @@ -29,7 +35,19 @@ API.v1.addRoute( throw new Error('Invalid asset'); } - await setAsset(this.userId, fileBuffer, mimetype, assetName); + const { key, value } = await RocketChatAssets.setAssetWithBuffer(fileBuffer, mimetype, assetName); + + const { modifiedCount } = await updateAuditedByUser({ + _id: this.userId, + username: this.user.username!, + ip: this.requestIp, + useragent: this.request.headers['user-agent'] || '', + })(Settings.updateValueById, key, value); + + if (modifiedCount) { + void notifyOnSettingChangedById(key); + } + if (refreshAllClients) { await refreshClients(this.userId); } @@ -44,6 +62,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isAssetsUnsetAssetProps, + permissionsRequired: ['manage-assets'], }, { async post() { @@ -52,7 +71,20 @@ API.v1.addRoute( if (!isValidAsset) { throw Error('Invalid asset'); } - await unsetAsset(this.userId, assetName); + + const { key, value } = await RocketChatAssets.unsetAsset(assetName); + + const { modifiedCount } = await updateAuditedByUser({ + _id: this.userId, + username: this.user.username!, + ip: this.requestIp, + useragent: this.request.headers['user-agent'] || '', + })(Settings.updateValueById, key, value); + + if (modifiedCount) { + void notifyOnSettingChangedById(key); + } + if (refreshAllClients) { await refreshClients(this.userId); } diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index b5cfd9e46ce6..cef220b050bc 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -1,5 +1,5 @@ import { Message } from '@rocket.chat/core-services'; -import type { IMessage } from '@rocket.chat/core-typings'; +import type { IMessage, IThreadMainMessage } from '@rocket.chat/core-typings'; import { Messages, Users, Rooms, Subscriptions } from '@rocket.chat/models'; import { isChatReportMessageProps, @@ -7,6 +7,7 @@ import { isChatUpdateProps, isChatGetThreadsListProps, isChatDeleteProps, + isChatSyncMessagesProps, } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Meteor } from 'meteor/meteor'; @@ -21,6 +22,7 @@ import { processWebhookMessage } from '../../../lib/server/functions/processWebh import { executeSendMessage } from '../../../lib/server/methods/sendMessage'; import { executeUpdateMessage } from '../../../lib/server/methods/updateMessage'; import { applyAirGappedRestrictionsValidation } from '../../../license/server/airGappedRestrictionsWrapper'; +import { pinMessage } from '../../../message-pin/server/pinMessage'; import { OEmbed } from '../../../oembed/server/server'; import { executeSetReaction } from '../../../reactions/server/setReaction'; import { settings } from '../../../settings/server'; @@ -73,22 +75,32 @@ API.v1.addRoute( API.v1.addRoute( 'chat.syncMessages', - { authRequired: true }, + { authRequired: true, validateParams: isChatSyncMessagesProps }, { async get() { - const { roomId, lastUpdate } = this.queryParams; + const { roomId, lastUpdate, count, next, previous, type } = this.queryParams; if (!roomId) { - throw new Meteor.Error('error-roomId-param-not-provided', 'The required "roomId" query param is missing.'); + throw new Meteor.Error('error-param-required', 'The required "roomId" query param is missing'); + } + + if (!lastUpdate && !type) { + throw new Meteor.Error('error-param-required', 'The "type" or "lastUpdate" parameters must be provided'); } - if (!lastUpdate) { - throw new Meteor.Error('error-lastUpdate-param-not-provided', 'The required "lastUpdate" query param is missing.'); - } else if (isNaN(Date.parse(lastUpdate))) { - throw new Meteor.Error('error-roomId-param-invalid', 'The "lastUpdate" query parameter must be a valid date.'); + if (lastUpdate && isNaN(Date.parse(lastUpdate))) { + throw new Meteor.Error('error-lastUpdate-param-invalid', 'The "lastUpdate" query parameter must be a valid date'); } - const result = await Meteor.callAsync('messages/get', roomId, { lastUpdate: new Date(lastUpdate) }); + const getMessagesQuery = { + ...(lastUpdate && { lastUpdate: new Date(lastUpdate) }), + ...(next && { next }), + ...(previous && { previous }), + ...(count && { count }), + ...(type && { type }), + }; + + const result = await Meteor.callAsync('messages/get', roomId, getMessagesQuery); if (!result) { return API.v1.failure(); @@ -96,8 +108,9 @@ API.v1.addRoute( return API.v1.success({ result: { - updated: await normalizeMessagesForUser(result.updated, this.userId), - deleted: result.deleted, + ...(result.updated && { updated: await normalizeMessagesForUser(result.updated, this.userId) }), + ...(result.deleted && { deleted: result.deleted }), + ...(result.cursor && { cursor: result.cursor }), }, }); }, @@ -145,7 +158,7 @@ API.v1.addRoute( throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.'); } - const pinnedMessage = await Meteor.callAsync('pinMessage', msg); + const pinnedMessage = await pinMessage(msg, this.userId); const [message] = await normalizeMessagesForUser([pinnedMessage], this.userId); @@ -161,6 +174,21 @@ API.v1.addRoute( { authRequired: true }, { async post() { + const { text, attachments } = this.bodyParams; + const maxAllowedSize = settings.get('Message_MaxAllowedSize') ?? 0; + + if (text && text.length > maxAllowedSize) { + return API.v1.failure('error-message-size-exceeded'); + } + + if (attachments && attachments.length > 0) { + for (const attachment of attachments) { + if (attachment.text && attachment.text.length > maxAllowedSize) { + return API.v1.failure('error-message-size-exceeded'); + } + } + } + const messageReturn = (await applyAirGappedRestrictionsValidation(() => processWebhookMessage(this.bodyParams, this.user)))[0]; if (!messageReturn) { @@ -522,7 +550,7 @@ API.v1.addRoute( }; const threadQuery = { ...query, ...typeThread, rid: room._id, tcount: { $exists: true } }; - const { cursor, totalCount } = await Messages.findPaginated(threadQuery, { + const { cursor, totalCount } = await Messages.findPaginated(threadQuery, { sort: sort || { tlm: -1 }, skip: offset, limit: count, diff --git a/apps/meteor/app/api/server/v1/emoji-custom.ts b/apps/meteor/app/api/server/v1/emoji-custom.ts index 261743e6c0a9..5b255b6ef0c1 100644 --- a/apps/meteor/app/api/server/v1/emoji-custom.ts +++ b/apps/meteor/app/api/server/v1/emoji-custom.ts @@ -1,9 +1,11 @@ import { Media } from '@rocket.chat/core-services'; +import type { IEmojiCustom } from '@rocket.chat/core-typings'; import { EmojiCustom } from '@rocket.chat/models'; import { isEmojiCustomList } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; import { SystemLogger } from '../../../../server/lib/logger/system'; +import type { EmojiData } from '../../../emoji-custom/server/lib/insertOrUpdateEmoji'; import { insertOrUpdateEmoji } from '../../../emoji-custom/server/lib/insertOrUpdateEmoji'; import { uploadEmojiCustomWithBuffer } from '../../../emoji-custom/server/lib/uploadEmojiCustom'; import { settings } from '../../../settings/server'; @@ -114,16 +116,15 @@ API.v1.addRoute( fields.extension = extension; try { - await Meteor.callAsync('insertOrUpdateEmoji', { - ...fields, - newFile: true, - aliases: fields.aliases || '', - }); - await Meteor.callAsync('uploadEmojiCustom', fileBuffer, mimetype, { + const emojiData = await insertOrUpdateEmoji(this.userId, { ...fields, newFile: true, aliases: fields.aliases || '', + name: fields.name, + extension: fields.extension, }); + + await uploadEmojiCustomWithBuffer(this.userId, fileBuffer, mimetype, emojiData); } catch (e) { SystemLogger.error(e); return API.v1.failure(); @@ -143,7 +144,7 @@ API.v1.addRoute( { request: this.request, }, - { field: 'emoji', sizeLimit: settings.get('FileUpload_MaxFileSize') }, + { field: 'emoji', sizeLimit: settings.get('FileUpload_MaxFileSize'), fileOptional: true }, ); const { fields, fileBuffer, mimetype } = emoji; @@ -152,41 +153,40 @@ API.v1.addRoute( throw new Meteor.Error('The required "_id" query param is missing.'); } - const emojiToUpdate = await EmojiCustom.findOneById(fields._id); + const emojiToUpdate = await EmojiCustom.findOneById>(fields._id, { + projection: { name: 1, extension: 1 }, + }); if (!emojiToUpdate) { throw new Meteor.Error('Emoji not found.'); } - fields.previousName = emojiToUpdate.name; - fields.previousExtension = emojiToUpdate.extension; - fields.aliases = fields.aliases || ''; - const newFile = Boolean(emoji && fileBuffer.length); + const emojiData: EmojiData = { + previousName: emojiToUpdate.name, + previousExtension: emojiToUpdate.extension, + aliases: fields.aliases || '', + name: fields.name, + extension: fields.extension, + _id: fields._id, + newFile: false, + }; - if (fields.newFile) { + const isNewFile = fileBuffer?.length && !!mimetype; + if (isNewFile) { + emojiData.newFile = isNewFile; const isUploadable = await Media.isImage(fileBuffer); if (!isUploadable) { throw new Meteor.Error('emoji-is-not-image', "Emoji file provided cannot be uploaded since it's not an image"); } const [, extension] = mimetype.split('/'); - fields.extension = extension; + emojiData.extension = extension; } else { - fields.extension = emojiToUpdate.extension; + emojiData.extension = emojiToUpdate.extension; } - const emojiData = { - name: fields.name, - _id: fields._id, - aliases: fields.aliases, - extension: fields.extension, - previousName: fields.previousName, - previousExtension: fields.previousExtension, - newFile, - }; - - await insertOrUpdateEmoji(this.userId, emojiData); - if (fields.newFile) { - await uploadEmojiCustomWithBuffer(this.userId, fileBuffer, mimetype, emojiData); + const updatedEmojiData = await insertOrUpdateEmoji(this.userId, emojiData); + if (isNewFile) { + await uploadEmojiCustomWithBuffer(this.userId, fileBuffer, mimetype, updatedEmojiData); } return API.v1.success(); }, diff --git a/apps/meteor/app/api/server/v1/import.ts b/apps/meteor/app/api/server/v1/import.ts index 54dbce4d82d1..6104076c4fb4 100644 --- a/apps/meteor/app/api/server/v1/import.ts +++ b/apps/meteor/app/api/server/v1/import.ts @@ -15,8 +15,6 @@ import { } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; -import { PendingAvatarImporter } from '../../../importer-pending-avatars/server/PendingAvatarImporter'; -import { PendingFileImporter } from '../../../importer-pending-files/server/PendingFileImporter'; import { Importers } from '../../../importer/server'; import { executeUploadImportFile, @@ -26,6 +24,8 @@ import { executeStartImport, executeGetLatestImportOperations, } from '../../../importer/server/methods'; +import { PendingAvatarImporter } from '../../../importer-pending-avatars/server/PendingAvatarImporter'; +import { PendingFileImporter } from '../../../importer-pending-files/server/PendingFileImporter'; import { API } from '../api'; API.v1.addRoute( @@ -33,6 +33,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isUploadImportFileParamsPOST, + permissionsRequired: ['run-import'], }, { async post() { diff --git a/apps/meteor/app/api/server/v1/invites.ts b/apps/meteor/app/api/server/v1/invites.ts index ac053f21f288..ac916befc4fd 100644 --- a/apps/meteor/app/api/server/v1/invites.ts +++ b/apps/meteor/app/api/server/v1/invites.ts @@ -63,8 +63,6 @@ API.v1.addRoute( { async post() { const { token } = this.bodyParams; - // eslint-disable-next-line react-hooks/rules-of-hooks - // eslint-disable-next-line react-hooks/rules-of-hooks return API.v1.success(await useInviteToken(this.userId, token)); }, diff --git a/apps/meteor/app/api/server/v1/misc.ts b/apps/meteor/app/api/server/v1/misc.ts index c026236231d8..1d502f04df1e 100644 --- a/apps/meteor/app/api/server/v1/misc.ts +++ b/apps/meteor/app/api/server/v1/misc.ts @@ -20,6 +20,7 @@ import { v4 as uuidv4 } from 'uuid'; import { i18n } from '../../../../server/lib/i18n'; import { SystemLogger } from '../../../../server/lib/logger/system'; +import { resetAuditedSettingByUser, updateAuditedByUser } from '../../../../server/settings/lib/auditedSettingUpdates'; import { getLogs } from '../../../../server/stream/stdout'; import { passwordPolicy } from '../../../lib/server'; import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; @@ -683,20 +684,32 @@ API.v1.addRoute( settingsIds.push('Deployment_FingerPrint_Verified'); + const auditSettingOperation = updateAuditedByUser({ + _id: this.userId, + username: this.user.username!, + ip: this.requestIp, + useragent: this.request.headers['user-agent'] || '', + }); + const promises = settingsIds.map((settingId) => { if (settingId === 'uniqueID') { - return Settings.resetValueById('uniqueID', process.env.DEPLOYMENT_ID || uuidv4()); + return auditSettingOperation(Settings.resetValueById, 'uniqueID', process.env.DEPLOYMENT_ID || uuidv4()); } if (settingId === 'Cloud_Workspace_Access_Token_Expires_At') { - return Settings.resetValueById('Cloud_Workspace_Access_Token_Expires_At', new Date(0)); + return auditSettingOperation(Settings.resetValueById, 'Cloud_Workspace_Access_Token_Expires_At', new Date(0)); } if (settingId === 'Deployment_FingerPrint_Verified') { - return Settings.updateValueById('Deployment_FingerPrint_Verified', true); + return auditSettingOperation(Settings.updateValueById, 'Deployment_FingerPrint_Verified', true); } - return Settings.resetValueById(settingId); + return resetAuditedSettingByUser({ + _id: this.userId, + username: this.user.username!, + ip: this.requestIp, + useragent: this.request.headers['user-agent'] || '', + })(Settings.resetValueById, settingId); }); (await Promise.all(promises)).forEach((value, index) => { diff --git a/apps/meteor/app/api/server/v1/roles.ts b/apps/meteor/app/api/server/v1/roles.ts index fc9bd273996d..20f6e38f3567 100644 --- a/apps/meteor/app/api/server/v1/roles.ts +++ b/apps/meteor/app/api/server/v1/roles.ts @@ -165,9 +165,7 @@ API.v1.addRoute( throw new Meteor.Error('error-role-protected', 'Cannot delete a protected role'); } - const existingUsers = await Roles.findUsersInRole(role._id); - - if (existingUsers && (await existingUsers.count()) > 0) { + if ((await Roles.countUsersInRole(role._id)) > 0) { throw new Meteor.Error('error-role-in-use', "Cannot delete role because it's in use"); } @@ -217,7 +215,7 @@ API.v1.addRoute( } if (role._id === 'admin') { - const adminCount = await (await Roles.findUsersInRole('admin')).count(); + const adminCount = await Roles.countUsersInRole('admin'); if (adminCount === 1) { throw new Meteor.Error('error-admin-required', 'You need to have at least one admin'); } diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 355cce24d40b..50bc12cab618 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -196,6 +196,10 @@ API.v1.addRoute( const fileStore = FileUpload.getStore('Uploads'); const uploadedFile = await fileStore.insert(details, fileBuffer); + if ((fields.description?.length ?? 0) > settings.get('Message_MaxAllowedSize')) { + throw new Meteor.Error('error-message-size-exceeded'); + } + uploadedFile.description = fields.description; delete fields.description; @@ -299,6 +303,10 @@ API.v1.addRoute( throw new Meteor.Error('invalid-file'); } + if ((this.bodyParams.description?.length ?? 0) > settings.get('Message_MaxAllowedSize')) { + throw new Meteor.Error('error-message-size-exceeded'); + } + file.description = this.bodyParams.description; delete this.bodyParams.description; @@ -434,7 +442,7 @@ API.v1.addRoute( const parent = discussionParent || parentRoom; return API.v1.success({ - room: (await Rooms.findOneByIdOrName(room._id, { projection: fields })) ?? undefined, + room: await Rooms.findOneByIdOrName(room._id, { projection: fields }), ...(team && { team }), ...(parent && { parent }), }); @@ -459,9 +467,14 @@ API.v1.addRoute( }, ); +/* +TO-DO: 8.0.0 should use the ajv validation +which will change this endpoint's +response errors. +*/ API.v1.addRoute( 'rooms.createDiscussion', - { authRequired: true }, + { authRequired: true /* , validateParams: isRoomsCreateDiscussionProps */ }, { async post() { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/apps/meteor/app/api/server/v1/settings.ts b/apps/meteor/app/api/server/v1/settings.ts index 0e3509fb1956..e9183cb9e38e 100644 --- a/apps/meteor/app/api/server/v1/settings.ts +++ b/apps/meteor/app/api/server/v1/settings.ts @@ -17,6 +17,7 @@ import { Meteor } from 'meteor/meteor'; import type { FindOptions } from 'mongodb'; import _ from 'underscore'; +import { updateAuditedByUser } from '../../../../server/settings/lib/auditedSettingUpdates'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { disableCustomScripts } from '../../../lib/server/functions/disableCustomScripts'; import { notifyOnSettingChanged, notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; @@ -200,9 +201,16 @@ API.v1.addRoute( return API.v1.success(); } + const auditSettingOperation = updateAuditedByUser({ + _id: this.userId, + username: this.user.username!, + ip: this.requestIp, + useragent: this.request.headers['user-agent'] || '', + }); + if (isSettingColor(setting) && isSettingsUpdatePropsColor(this.bodyParams)) { const updateOptionsPromise = Settings.updateOptionsById(this.urlParams._id, { editor: this.bodyParams.editor }); - const updateValuePromise = Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value); + const updateValuePromise = auditSettingOperation(Settings.updateValueNotHiddenById, this.urlParams._id, this.bodyParams.value); const [updateOptionsResult, updateValueResult] = await Promise.all([updateOptionsPromise, updateValuePromise]); @@ -214,7 +222,12 @@ API.v1.addRoute( } if (isSettingsUpdatePropDefault(this.bodyParams)) { - const { matchedCount } = await Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value); + const { matchedCount } = await auditSettingOperation( + Settings.updateValueNotHiddenById, + this.urlParams._id, + this.bodyParams.value, + ); + if (!matchedCount) { return API.v1.failure(); } diff --git a/apps/meteor/app/api/server/v1/subscriptions.ts b/apps/meteor/app/api/server/v1/subscriptions.ts index b92d9ba572fd..be0aaea4b4e1 100644 --- a/apps/meteor/app/api/server/v1/subscriptions.ts +++ b/apps/meteor/app/api/server/v1/subscriptions.ts @@ -35,7 +35,7 @@ API.v1.addRoute( ? { update: result, remove: [], - } + } : result, ); }, diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index b7187ec8cd81..1dcbd3ab3c78 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -155,7 +155,7 @@ API.v1.addRoute( : { twoFactorCode: userData.typedPassword, twoFactorMethod: 'password', - }; + }; await Meteor.callAsync('saveUserProfile', userData, this.bodyParams.customFields, twoFactorOptions); @@ -512,7 +512,7 @@ API.v1.addRoute( { $limit: count, }, - ] + ] : []; const result = await Users.col diff --git a/apps/meteor/app/api/server/v1/voip/extensions.ts b/apps/meteor/app/api/server/v1/voip/extensions.ts index 1a0fecf7cfc5..91b0466626b6 100644 --- a/apps/meteor/app/api/server/v1/voip/extensions.ts +++ b/apps/meteor/app/api/server/v1/voip/extensions.ts @@ -3,10 +3,10 @@ import type { IVoipExtensionBase } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; +import { logger } from './logger'; import { settings } from '../../../../settings/server'; import { generateJWT } from '../../../../utils/server/lib/JWTHelper'; import { API } from '../../api'; -import { logger } from './logger'; // Get the connector version and type API.v1.addRoute( diff --git a/apps/meteor/app/api/server/v1/voip/omnichannel.ts b/apps/meteor/app/api/server/v1/voip/omnichannel.ts index e2128375ea42..abd92b9fa589 100644 --- a/apps/meteor/app/api/server/v1/voip/omnichannel.ts +++ b/apps/meteor/app/api/server/v1/voip/omnichannel.ts @@ -3,10 +3,10 @@ import type { IUser, IVoipExtensionWithAgentInfo } from '@rocket.chat/core-typin import { Users } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; +import { logger } from './logger'; import { notifyOnUserChange } from '../../../../lib/server/lib/notifyListener'; import { API } from '../../api'; import { getPaginationItems } from '../../helpers/getPaginationItems'; -import { logger } from './logger'; function filter( array: IVoipExtensionWithAgentInfo[], diff --git a/apps/meteor/app/apple/server/appleOauthRegisterService.ts b/apps/meteor/app/apple/server/appleOauthRegisterService.ts index b9558fa701f7..7cb748c7ab91 100644 --- a/apps/meteor/app/apple/server/appleOauthRegisterService.ts +++ b/apps/meteor/app/apple/server/appleOauthRegisterService.ts @@ -1,9 +1,9 @@ import { KJUR } from 'jsrsasign'; import { ServiceConfiguration } from 'meteor/service-configuration'; +import { AppleCustomOAuth } from './AppleCustomOAuth'; import { settings } from '../../settings/server'; import { config } from '../lib/config'; -import { AppleCustomOAuth } from './AppleCustomOAuth'; new AppleCustomOAuth('apple', config); diff --git a/apps/meteor/app/apps/server/bridges/bridges.js b/apps/meteor/app/apps/server/bridges/bridges.js index aeab9f191039..2e9def737582 100644 --- a/apps/meteor/app/apps/server/bridges/bridges.js +++ b/apps/meteor/app/apps/server/bridges/bridges.js @@ -4,6 +4,7 @@ import { AppActivationBridge } from './activation'; import { AppApisBridge } from './api'; import { AppCloudBridge } from './cloud'; import { AppCommandsBridge } from './commands'; +import { AppContactBridge } from './contact'; import { AppDetailChangesBridge } from './details'; import { AppEmailBridge } from './email'; import { AppEnvironmentalVariableBridge } from './environmental'; @@ -55,6 +56,7 @@ export class RealAppBridges extends AppBridges { this._threadBridge = new AppThreadBridge(orch); this._roleBridge = new AppRoleBridge(orch); this._emailBridge = new AppEmailBridge(orch); + this._contactBridge = new AppContactBridge(orch); } getCommandBridge() { @@ -156,4 +158,8 @@ export class RealAppBridges extends AppBridges { getEmailBridge() { return this._emailBridge; } + + getContactBridge() { + return this._contactBridge; + } } diff --git a/apps/meteor/app/apps/server/bridges/contact.ts b/apps/meteor/app/apps/server/bridges/contact.ts new file mode 100644 index 000000000000..802b0bb3ec16 --- /dev/null +++ b/apps/meteor/app/apps/server/bridges/contact.ts @@ -0,0 +1,39 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import type { ILivechatContact } from '@rocket.chat/apps-engine/definition/livechat'; +import { ContactBridge } from '@rocket.chat/apps-engine/server/bridges'; + +import { addContactEmail } from '../../../livechat/server/lib/contacts/addContactEmail'; +import { verifyContactChannel } from '../../../livechat/server/lib/contacts/verifyContactChannel'; + +export class AppContactBridge extends ContactBridge { + constructor(private readonly orch: IAppServerOrchestrator) { + super(); + } + + async getById(contactId: ILivechatContact['_id'], appId: string): Promise { + this.orch.debugLog(`The app ${appId} is fetching a contact`); + return this.orch.getConverters().get('contacts').convertById(contactId); + } + + async verifyContact( + verifyContactChannelParams: { + contactId: string; + field: string; + value: string; + visitorId: string; + roomId: string; + }, + appId: string, + ): Promise { + this.orch.debugLog(`The app ${appId} is verifing a contact`); + // Note: If there is more than one app installed, whe should validate the app that called this method to be same one + // selected in the setting. + await verifyContactChannel(verifyContactChannelParams); + } + + protected async addContactEmail(contactId: ILivechatContact['_id'], email: string, appId: string): Promise { + this.orch.debugLog(`The app ${appId} is adding a new email to the contact`); + const contact = await addContactEmail(contactId, email); + return this.orch.getConverters().get('contacts').convertContact(contact); + } +} diff --git a/apps/meteor/app/apps/server/bridges/email.ts b/apps/meteor/app/apps/server/bridges/email.ts index 4c9cb9a93ed6..6d75a4504483 100644 --- a/apps/meteor/app/apps/server/bridges/email.ts +++ b/apps/meteor/app/apps/server/bridges/email.ts @@ -3,6 +3,7 @@ import type { IEmail } from '@rocket.chat/apps-engine/definition/email'; import { EmailBridge } from '@rocket.chat/apps-engine/server/bridges'; import * as Mailer from '../../../mailer/server/api'; +import { settings } from '../../../settings/server'; export class AppEmailBridge extends EmailBridge { constructor(private readonly orch: IAppServerOrchestrator) { @@ -10,7 +11,13 @@ export class AppEmailBridge extends EmailBridge { } protected async sendEmail(email: IEmail, appId: string): Promise { + let { from } = email; + if (!from) { + this.orch.debugLog(`The app ${appId} didn't provide a from address, using the default one.`); + from = String(settings.get('From_Email')); + } + this.orch.debugLog(`The app ${appId} is sending an email.`); - await Mailer.send(email); + await Mailer.send({ ...email, from }); } } diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 821d1fdd60d5..f2521d2f8cf5 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -10,7 +10,9 @@ import { LivechatVisitors, LivechatRooms, LivechatDepartment, Users } from '@roc import { callbacks } from '../../../../lib/callbacks'; import { deasyncPromise } from '../../../../server/deasync/deasync'; -import { type ILivechatMessage, Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped'; +import { Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped'; +import { getRoomMessages } from '../../../livechat/server/lib/getRoomMessages'; +import type { ILivechatMessage } from '../../../livechat/server/lib/localTypes'; import { settings } from '../../../settings/server'; declare module '@rocket.chat/apps/dist/converters/IAppMessagesConverter' { @@ -352,7 +354,7 @@ export class AppLivechatBridge extends LivechatBridge { throw new Error('Could not get the message converter to process livechat room messages'); } - const livechatMessages = await LivechatTyped.getRoomMessages({ rid: roomId }); + const livechatMessages = await getRoomMessages({ rid: roomId }); return Promise.all(await livechatMessages.map((message) => messageConverter.convertMessage(message, livechatMessages)).toArray()); } diff --git a/apps/meteor/app/apps/server/bridges/settings.ts b/apps/meteor/app/apps/server/bridges/settings.ts index 37803d4f94f3..cada833aadbd 100644 --- a/apps/meteor/app/apps/server/bridges/settings.ts +++ b/apps/meteor/app/apps/server/bridges/settings.ts @@ -3,6 +3,7 @@ import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import { ServerSettingBridge } from '@rocket.chat/apps-engine/server/bridges/ServerSettingBridge'; import { Settings } from '@rocket.chat/models'; +import { updateAuditedByApp } from '../../../../server/settings/lib/auditedSettingUpdates'; import { notifyOnSettingChanged, notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; export class AppSettingBridge extends ServerSettingBridge { @@ -56,7 +57,15 @@ export class AppSettingBridge extends ServerSettingBridge { throw new Error(`The setting "${setting.id}" is not readable.`); } - (await Settings.updateValueById(setting.id, setting.value)).modifiedCount && void notifyOnSettingChangedById(setting.id); + if ( + ( + await updateAuditedByApp({ + _id: appId, + })(Settings.updateValueById, setting.id, setting.value) + ).modifiedCount + ) { + void notifyOnSettingChangedById(setting.id); + } } protected async incrementValue(id: string, value: number, appId: string): Promise { diff --git a/apps/meteor/app/apps/server/converters/contacts.ts b/apps/meteor/app/apps/server/converters/contacts.ts new file mode 100644 index 000000000000..8dc49a829d09 --- /dev/null +++ b/apps/meteor/app/apps/server/converters/contacts.ts @@ -0,0 +1,125 @@ +import type { IAppContactsConverter, IAppsLivechatContact } from '@rocket.chat/apps'; +import type { ILivechatContact } from '@rocket.chat/core-typings'; +import { LivechatContacts } from '@rocket.chat/models'; + +import { transformMappedData } from './transformMappedData'; + +export class AppContactsConverter implements IAppContactsConverter { + async convertById(contactId: ILivechatContact['_id']): Promise { + const contact = await LivechatContacts.findOneById(contactId); + if (!contact) { + return; + } + + return this.convertContact(contact); + } + + async convertContact(contact: undefined | null): Promise; + + async convertContact(contact: ILivechatContact): Promise; + + async convertContact(contact: ILivechatContact | undefined | null): Promise { + if (!contact) { + return; + } + + return structuredClone(contact); + } + + convertAppContact(contact: undefined | null): Promise; + + convertAppContact(contact: IAppsLivechatContact): Promise; + + async convertAppContact(contact: IAppsLivechatContact | undefined | null): Promise { + if (!contact) { + return; + } + + // Map every attribute individually to ensure there are no extra data coming from the app and leaking into anything else. + const map = { + _id: '_id', + _updatedAt: '_updatedAt', + name: 'name', + phones: { + from: 'phones', + list: true, + map: { + phoneNumber: 'phoneNumber', + }, + }, + emails: { + from: 'emails', + list: true, + map: { + address: 'address', + }, + }, + contactManager: 'contactManager', + unknown: 'unknown', + conflictingFields: { + from: 'conflictingFields', + list: true, + map: { + field: 'field', + value: 'value', + }, + }, + customFields: 'customFields', + channels: { + from: 'channels', + list: true, + map: { + name: 'name', + verified: 'verified', + visitor: { + from: 'visitor', + map: { + visitorId: 'visitorId', + source: { + from: 'source', + map: { + type: 'type', + id: 'id', + }, + }, + }, + }, + blocked: 'blocked', + field: 'field', + value: 'value', + verifiedAt: 'verifiedAt', + details: { + from: 'details', + map: { + type: 'type', + id: 'id', + alias: 'alias', + label: 'label', + sidebarIcon: 'sidebarIcon', + defaultIcon: 'defaultIcon', + destination: 'destination', + }, + }, + lastChat: { + from: 'lastChat', + map: { + _id: '_id', + ts: 'ts', + }, + }, + }, + }, + createdAt: 'createdAt', + lastChat: { + from: 'lastChat', + map: { + _id: '_id', + ts: 'ts', + }, + }, + importIds: 'importIds', + }; + + return transformMappedData(contact, map); + } +} diff --git a/apps/meteor/app/apps/server/converters/index.ts b/apps/meteor/app/apps/server/converters/index.ts index 96716af03ca7..af64888f4d26 100644 --- a/apps/meteor/app/apps/server/converters/index.ts +++ b/apps/meteor/app/apps/server/converters/index.ts @@ -1,3 +1,4 @@ +import { AppContactsConverter } from './contacts'; import { AppDepartmentsConverter } from './departments'; import { AppMessagesConverter } from './messages'; import { AppRolesConverter } from './roles'; @@ -9,6 +10,7 @@ import { AppVideoConferencesConverter } from './videoConferences'; import { AppVisitorsConverter } from './visitors'; export { + AppContactsConverter, AppMessagesConverter, AppRoomsConverter, AppSettingsConverter, diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.js index a98a6701b2c2..741f98932191 100644 --- a/apps/meteor/app/apps/server/converters/rooms.js +++ b/apps/meteor/app/apps/server/converters/rooms.js @@ -1,5 +1,5 @@ import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; -import { LivechatVisitors, Rooms, LivechatDepartment, Users } from '@rocket.chat/models'; +import { LivechatVisitors, Rooms, LivechatDepartment, Users, LivechatContacts } from '@rocket.chat/models'; import { transformMappedData } from './transformMappedData'; @@ -75,6 +75,12 @@ export class AppRoomsConverter { }; } + let contactId; + if (room.contact?._id) { + const contact = await LivechatContacts.findOneById(room.contact._id, { projection: { _id: 1 } }); + contactId = contact._id; + } + const newRoom = { ...(room.id && { _id: room.id }), fname: room.displayName, @@ -100,6 +106,7 @@ export class AppRoomsConverter { customFields: room.customFields, livechatData: room.livechatData, prid: typeof room.parentRoom === 'undefined' ? undefined : room.parentRoom.id, + contactId, ...(room._USERNAMES && { _USERNAMES: room._USERNAMES }), ...(room.source && { source: { @@ -180,6 +187,15 @@ export class AppRoomsConverter { return this.orch.getConverters().get('visitors').convertById(v._id); }, + contact: (room) => { + const { contactId } = room; + + if (!contactId) { + return undefined; + } + + return this.orch.getConverters().get('contacts').convertById(contactId); + }, // Note: room.v is not just visitor, it also contains channel related visitor data // so we need to pass this data to the converter // So suppose you have a contact whom we're contacting using SMS via 2 phone no's, diff --git a/apps/meteor/app/apps/server/converters/threads.ts b/apps/meteor/app/apps/server/converters/threads.ts index e31ee094b4d7..d6284688b984 100644 --- a/apps/meteor/app/apps/server/converters/threads.ts +++ b/apps/meteor/app/apps/server/converters/threads.ts @@ -1,8 +1,8 @@ import type { IAppRoomsConverter, IAppThreadsConverter, IAppUsersConverter, IAppsMessage, IAppsUser } from '@rocket.chat/apps'; import type { IMessage as AppsEngineMessage, IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; -import type { IUser } from '@rocket.chat/core-typings'; -import { isEditedMessage, type IMessage } from '@rocket.chat/core-typings'; +import { isEditedMessage } from '@rocket.chat/core-typings'; +import type { IUser, IMessage } from '@rocket.chat/core-typings'; import { Messages } from '@rocket.chat/models'; import { cachedFunction } from './cachedFunction'; diff --git a/apps/meteor/app/apps/server/converters/transformMappedData.ts b/apps/meteor/app/apps/server/converters/transformMappedData.ts index df2f16138d73..f18a89df11ee 100644 --- a/apps/meteor/app/apps/server/converters/transformMappedData.ts +++ b/apps/meteor/app/apps/server/converters/transformMappedData.ts @@ -1,5 +1,3 @@ -import cloneDeep from 'lodash.clonedeep'; - /** * Transforms a `data` source object to another object, * essentially applying a to -> from mapping provided by @@ -62,16 +60,41 @@ import cloneDeep from 'lodash.clonedeep'; * @returns Object The data after transformations have been applied */ -export const transformMappedData = async < - ResultType extends { - -readonly [p in keyof MapType]: MapType[p] extends keyof DataType - ? DataType[MapType[p]] - : MapType[p] extends (...args: any[]) => any +type MapFor = { + [p in string]: + | string + | ((data: DataType) => Promise) + | ((data: DataType) => unknown) + | { from: string; list: true } + | { from: string; map: MapFor; list?: boolean }; +}; + +type ResultFor, MapType extends MapFor> = { + -readonly [p in keyof MapType]: MapType[p] extends keyof DataType + ? DataType[MapType[p]] + : MapType[p] extends (...args: any[]) => any ? Awaited> - : never; - }, + : MapType[p] extends { from: infer KeyName; map?: Record; list?: boolean } + ? KeyName extends keyof DataType + ? MapType[p]['list'] extends true + ? DataType[KeyName] extends any[] + ? MapType[p]['map'] extends MapFor + ? ResultFor[] + : DataType[KeyName] + : DataType[KeyName][] + : DataType[KeyName] extends object + ? MapType[p]['map'] extends MapFor + ? ResultFor + : never + : never + : never + : never; +}; + +export const transformMappedData = async < + ResultType extends ResultFor, DataType extends Record, - MapType extends { [p in string]: string | ((data: DataType) => Promise) | ((data: DataType) => unknown) }, + MapType extends MapFor, UnmappedProperties extends { [p in keyof DataType as Exclude]: DataType[p]; }, @@ -79,7 +102,7 @@ export const transformMappedData = async < data: DataType, map: MapType, ): Promise => { - const originalData: DataType = cloneDeep(data); + const originalData: DataType = structuredClone(data); const transformedData: Record = {}; for await (const [to, from] of Object.entries(map)) { @@ -94,6 +117,26 @@ export const transformMappedData = async < transformedData[to] = originalData[from]; } delete originalData[from]; + } else if (typeof from === 'object' && 'from' in from) { + const { from: fromName } = from; + + if (from.list) { + if (Array.isArray(originalData[fromName])) { + if ('map' in from && from.map) { + if (typeof originalData[fromName] === 'object') { + transformedData[to] = await Promise.all(originalData[fromName].map((item) => transformMappedData(item, from.map))); + } + } else { + transformedData[to] = [...originalData[fromName]]; + } + } else if (originalData[fromName] !== undefined && originalData[fromName] !== null) { + transformedData[to] = [originalData[fromName]]; + } + } else { + transformedData[to] = await transformMappedData(originalData[fromName], from.map); + } + + delete originalData[fromName]; } } diff --git a/apps/meteor/app/apps/server/converters/visitors.js b/apps/meteor/app/apps/server/converters/visitors.js index 32864e3e900e..c8fb0b7c4a21 100644 --- a/apps/meteor/app/apps/server/converters/visitors.js +++ b/apps/meteor/app/apps/server/converters/visitors.js @@ -36,7 +36,6 @@ export class AppVisitorsConverter { visitorEmails: 'visitorEmails', livechatData: 'livechatData', status: 'status', - contactId: 'contactId', }; return transformMappedData(visitor, map); @@ -55,7 +54,6 @@ export class AppVisitorsConverter { phone: visitor.phone, livechatData: visitor.livechatData, status: visitor.status || 'online', - contactId: visitor.contactId, ...(visitor.visitorEmails && { visitorEmails: visitor.visitorEmails }), ...(visitor.department && { department: visitor.department }), }; diff --git a/apps/meteor/app/assets/server/assets.ts b/apps/meteor/app/assets/server/assets.ts index ef307f00b30c..e9160b0ece61 100644 --- a/apps/meteor/app/assets/server/assets.ts +++ b/apps/meteor/app/assets/server/assets.ts @@ -220,16 +220,14 @@ class RocketChatAssetsClass { return assets; } - public async setAsset(binaryContent: string, contentType: string, asset: string): Promise { - const file = Buffer.from(binaryContent, 'binary'); - await this.setAssetWithBuffer(file, contentType, asset); - } - - public async setAssetWithBuffer(binaryContent: Buffer, contentType: string, asset: string): Promise { - await this._setAsset(binaryContent, contentType, asset); - } - - private async _setAsset(file: Buffer, contentType: string, asset: string): Promise { + public async setAssetWithBuffer( + file: Buffer, + contentType: string, + asset: string, + ): Promise<{ + key: string; + value: IRocketChatAsset; + }> { const assetInstance = getAssetByKey(asset); if (!assetInstance) { throw new Meteor.Error('error-invalid-asset', 'Invalid asset', { @@ -260,29 +258,29 @@ class RocketChatAssetsClass { await RocketChatAssetsInstance.deleteFile(asset); const ws = RocketChatAssetsInstance.createWriteStream(asset, contentType); - ws.on('end', () => { - return setTimeout(async () => { - const key = `Assets_${asset}`; - const value = { - url: `assets/${asset}.${extension}`, - defaultUrl: assetInstance.defaultUrl, - }; - - void (async () => { - const { modifiedCount } = await Settings.updateValueById(key, value); - if (modifiedCount) { - void notifyOnSettingChangedById(key); - } - })(); - - return RocketChatAssets.processAsset(key, value); - }, 200); - }); - rs.pipe(ws); + return new Promise((resolve) => { + ws.on('end', () => { + return setTimeout(async () => { + const key = `Assets_${asset}`; + const value = { + url: `assets/${asset}.${extension}`, + defaultUrl: assetInstance.defaultUrl, + } as IRocketChatAsset; + + await RocketChatAssets.processAsset(key, value); + resolve({ + key, + value, + }); + }, 200); + }); + + rs.pipe(ws); + }); } - public async unsetAsset(asset: string): Promise { + public async unsetAsset(asset: string) { if (!getAssetByKey(asset)) { throw new Meteor.Error('error-invalid-asset', 'Invalid asset', { function: 'RocketChat.Assets.unsetAsset', @@ -295,14 +293,12 @@ class RocketChatAssetsClass { defaultUrl: getAssetByKey(asset).defaultUrl, }; - void (async () => { - const { modifiedCount } = await Settings.updateValueById(key, value); - if (modifiedCount) { - void notifyOnSettingChangedById(key); - } - })(); - await RocketChatAssets.processAsset(key, value); + + return { + key, + value, + }; } public refreshClients(): boolean { @@ -374,14 +370,12 @@ export async function addAssetToSetting(asset: string, value: IRocketChatAsset, defaultUrl: value.defaultUrl, }, { - ...{ - type: 'asset', - group: 'Assets', - fileConstraints: value.constraints, - i18nLabel: value.label, - asset, - public: true, - }, + type: 'asset', + group: 'Assets', + fileConstraints: value.constraints, + i18nLabel: value.label, + asset, + public: true, ...options, }, ); @@ -425,32 +419,6 @@ export const refreshClients = async (userId: string) => { return RocketChatAssets.refreshClients(); }; -export const unsetAsset = async (userId: string, asset: string) => { - if (!userId) { - throw new Error('Invalid user'); - } - - const _hasPermission = await hasPermissionAsync(userId, 'manage-assets'); - if (!_hasPermission) { - throw new Error('Managing assets not allowed'); - } - - return RocketChatAssets.unsetAsset(asset); -}; - -export const setAsset = async (userId: string, binaryContent: Buffer, contentType: string, asset: string) => { - if (!userId) { - throw new Error('Invalid user'); - } - - const _hasPermission = await hasPermissionAsync(userId, 'manage-assets'); - if (!_hasPermission) { - throw new Error('Managing assets not allowed'); - } - - await RocketChatAssets.setAssetWithBuffer(binaryContent, contentType, asset); -}; - const listener = (req: IncomingMessage, res: ServerResponse, next: NextHandleFunction) => { if (!req.url) { return; diff --git a/apps/meteor/app/assets/server/index.ts b/apps/meteor/app/assets/server/index.ts index 8aa8c123f928..6802fadf6d20 100644 --- a/apps/meteor/app/assets/server/index.ts +++ b/apps/meteor/app/assets/server/index.ts @@ -1 +1 @@ -export { RocketChatAssets, setAsset, unsetAsset, refreshClients } from './assets'; +export { RocketChatAssets, refreshClients } from './assets'; diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index 0577ceac0ba7..03c9602e59f0 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -338,6 +338,7 @@ Accounts.insertUserDoc = async function (options, user) { if (!roles.includes('admin') && !hasAdmin) { roles.push('admin'); if (settings.get('Show_Setup_Wizard') === 'pending') { + // TODO: audit (await Settings.updateValueById('Show_Setup_Wizard', 'in_progress')).modifiedCount && void notifyOnSettingChangedById('Show_Setup_Wizard'); } diff --git a/apps/meteor/app/authorization/client/hasPermission.ts b/apps/meteor/app/authorization/client/hasPermission.ts index 66171aeadd28..3495da05556c 100644 --- a/apps/meteor/app/authorization/client/hasPermission.ts +++ b/apps/meteor/app/authorization/client/hasPermission.ts @@ -26,7 +26,7 @@ const createPermissionValidator = } } - const permission = Models.ChatPermissions.findOne(permissionId, { + const permission = Models.Permissions.findOne(permissionId, { fields: { roles: 1 }, }); const roles = permission?.roles ?? []; @@ -99,18 +99,3 @@ export const userHasAllPermission = ( ): boolean => validatePermissions(permissions, scope, all, userId); export const hasPermission = hasAllPermission; - -/** - * This function is used to check if the user will have the permission after something happens. - * For example, The user is creating a new channel and he wants to set read-only config to true. - * This is a problem, set-readonly is a permission related with the scoped permissions `owner` - * so the user cannot set this permission to true during the channel creation, because there is no room yet to be owned and used as scope, but is possible - * during the channel update, which is weird. - * - * @param permissions The permissions to check - * @param scopedRoles The scoped roles to check to be included in the user roles - * @returns If the user will have the permission - */ - -export const willHavePermission = (permissions: IPermission['_id'] | IPermission['_id'][], scopedRoles: IPermission['_id'][]): boolean => - validatePermissions(permissions, undefined, all, undefined, scopedRoles); diff --git a/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts b/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts index fea37fd1c2a5..1651affba0de 100644 --- a/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts +++ b/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts @@ -1,9 +1,9 @@ import type { IUser, IRoom } from '@rocket.chat/core-typings'; import { Rooms } from '@rocket.chat/models'; -import { getValue } from '../../../settings/server/raw'; import { canAccessRoomAsync } from './canAccessRoom'; import { hasPermissionAsync } from './hasPermission'; +import { getValue } from '../../../settings/server/raw'; const elapsedTime = (ts: Date): number => { const dif = Date.now() - ts.getTime(); diff --git a/apps/meteor/app/authorization/server/functions/canSendMessage.ts b/apps/meteor/app/authorization/server/functions/canSendMessage.ts index 9cf5c5ec78d2..97767ee001b0 100644 --- a/apps/meteor/app/authorization/server/functions/canSendMessage.ts +++ b/apps/meteor/app/authorization/server/functions/canSendMessage.ts @@ -1,10 +1,10 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; import { Subscriptions, Rooms } from '@rocket.chat/models'; -import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; -import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { canAccessRoomAsync } from './canAccessRoom'; import { hasPermissionAsync } from './hasPermission'; +import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; +import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; const subscriptionOptions = { projection: { diff --git a/apps/meteor/app/authorization/server/functions/getRoles.ts b/apps/meteor/app/authorization/server/functions/getRoles.ts index 59ab1ef53732..bee995f885d3 100644 --- a/apps/meteor/app/authorization/server/functions/getRoles.ts +++ b/apps/meteor/app/authorization/server/functions/getRoles.ts @@ -2,3 +2,6 @@ import type { IRole } from '@rocket.chat/core-typings'; import { Roles } from '@rocket.chat/models'; export const getRoles = async (): Promise => Roles.find().toArray(); + +export const getRoleIds = async (): Promise => + (await Roles.find({}, { projection: { _id: 1 } }).toArray()).map(({ _id }) => _id); diff --git a/apps/meteor/app/authorization/server/methods/deleteRole.ts b/apps/meteor/app/authorization/server/methods/deleteRole.ts index 140852e0f1ec..512468f2d6d7 100644 --- a/apps/meteor/app/authorization/server/methods/deleteRole.ts +++ b/apps/meteor/app/authorization/server/methods/deleteRole.ts @@ -56,7 +56,7 @@ Meteor.methods({ }); } - const users = await (await Roles.findUsersInRole(role._id)).count(); + const users = await Roles.countUsersInRole(role._id); if (users > 0) { throw new Meteor.Error('error-role-in-use', "Cannot delete role because it's in use", { diff --git a/apps/meteor/app/autotranslate/client/lib/actionButton.ts b/apps/meteor/app/autotranslate/client/lib/actionButton.ts index 24cea2d8d28a..3901ef2df8e7 100644 --- a/apps/meteor/app/autotranslate/client/lib/actionButton.ts +++ b/apps/meteor/app/autotranslate/client/lib/actionButton.ts @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; +import { AutoTranslate } from './autotranslate'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; -import { messageArgs } from '../../../../client/lib/utils/messageArgs'; import { hasTranslationLanguageInAttachments, hasTranslationLanguageInMessage, @@ -12,7 +12,6 @@ import { Messages } from '../../../models/client'; import { settings } from '../../../settings/client'; import { MessageAction } from '../../../ui-utils/client/lib/MessageAction'; import { sdk } from '../../../utils/client/lib/SDKClient'; -import { AutoTranslate } from './autotranslate'; Meteor.startup(() => { AutoTranslate.init(); @@ -25,8 +24,7 @@ Meteor.startup(() => { label: 'Translate', context: ['message', 'message-mobile', 'threads'], type: 'interaction', - action(_, props) { - const { message = messageArgs(this).msg } = props; + action(_, { message }) { const language = AutoTranslate.getLanguage(message.rid); if (!hasTranslationLanguageInMessage(message, language) && !hasTranslationLanguageInAttachments(message.attachments, language)) { (AutoTranslate.messageIdsToWait as any)[message._id] = true; @@ -61,7 +59,7 @@ Meteor.startup(() => { context: ['message', 'message-mobile', 'threads'], type: 'interaction', action(_, props) { - const { message = messageArgs(this).msg } = props; + const { message } = props; const language = AutoTranslate.getLanguage(message.rid); if (!hasTranslationLanguageInMessage(message, language) && !hasTranslationLanguageInAttachments(message.attachments, language)) { (AutoTranslate.messageIdsToWait as any)[message._id] = true; diff --git a/apps/meteor/app/autotranslate/server/deeplTranslate.ts b/apps/meteor/app/autotranslate/server/deeplTranslate.ts index 349133f7fd4c..caa4cf97c9a8 100644 --- a/apps/meteor/app/autotranslate/server/deeplTranslate.ts +++ b/apps/meteor/app/autotranslate/server/deeplTranslate.ts @@ -13,10 +13,10 @@ import type { import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import _ from 'underscore'; +import { TranslationProviderRegistry, AutoTranslate } from './autotranslate'; import { i18n } from '../../../server/lib/i18n'; import { SystemLogger } from '../../../server/lib/logger/system'; import { settings } from '../../settings/server'; -import { TranslationProviderRegistry, AutoTranslate } from './autotranslate'; const proApiEndpoint = 'https://api.deepl.com/v2/translate'; const freeApiEndpoint = 'https://api-free.deepl.com/v2/translate'; diff --git a/apps/meteor/app/autotranslate/server/googleTranslate.ts b/apps/meteor/app/autotranslate/server/googleTranslate.ts index 0e0b7a205765..da5e156458dc 100644 --- a/apps/meteor/app/autotranslate/server/googleTranslate.ts +++ b/apps/meteor/app/autotranslate/server/googleTranslate.ts @@ -13,10 +13,10 @@ import type { import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import _ from 'underscore'; +import { AutoTranslate, TranslationProviderRegistry } from './autotranslate'; import { i18n } from '../../../server/lib/i18n'; import { SystemLogger } from '../../../server/lib/logger/system'; import { settings } from '../../settings/server'; -import { AutoTranslate, TranslationProviderRegistry } from './autotranslate'; /** * Represents google translate class diff --git a/apps/meteor/app/autotranslate/server/msTranslate.ts b/apps/meteor/app/autotranslate/server/msTranslate.ts index f885a23b8e6b..ad36ff0b8b77 100644 --- a/apps/meteor/app/autotranslate/server/msTranslate.ts +++ b/apps/meteor/app/autotranslate/server/msTranslate.ts @@ -6,10 +6,10 @@ import type { IMessage, IProviderMetadata, ISupportedLanguage, ITranslationResul import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import _ from 'underscore'; -import { i18n } from '../../../server/lib/i18n'; -import { settings } from '../../settings/server'; import { TranslationProviderRegistry, AutoTranslate } from './autotranslate'; import { msLogger } from './logger'; +import { i18n } from '../../../server/lib/i18n'; +import { settings } from '../../settings/server'; /** * Microsoft translation service provider class representation. diff --git a/apps/meteor/app/canned-responses/client/startup/responses.js b/apps/meteor/app/canned-responses/client/startup/responses.ts similarity index 72% rename from apps/meteor/app/canned-responses/client/startup/responses.js rename to apps/meteor/app/canned-responses/client/startup/responses.ts index 595945283261..6d761adb890d 100644 --- a/apps/meteor/app/canned-responses/client/startup/responses.js +++ b/apps/meteor/app/canned-responses/client/startup/responses.ts @@ -6,13 +6,6 @@ import { settings } from '../../../settings/client'; import { sdk } from '../../../utils/client/lib/SDKClient'; import { CannedResponse } from '../collections/CannedResponse'; -const events = { - changed: ({ type, ...response }) => { - CannedResponse.upsert({ _id: response._id }, response); - }, - removed: (response) => CannedResponse.remove({ _id: response._id }), -}; - Meteor.startup(() => { Tracker.autorun(async (c) => { if (!Meteor.userId()) { @@ -27,12 +20,24 @@ Meteor.startup(() => { Tracker.afterFlush(() => { try { // TODO: check options - sdk.stream('canned-responses', ['canned-responses'], (response, options) => { + sdk.stream('canned-responses', ['canned-responses'], (...[response, options]) => { const { agentsId } = options || {}; if (Array.isArray(agentsId) && !agentsId.includes(Meteor.userId())) { return; } - events[response.type](response); + + switch (response.type) { + case 'changed': { + const { type, ...fields } = response; + CannedResponse.upsert({ _id: response._id }, fields); + break; + } + + case 'removed': { + CannedResponse.remove({ _id: response._id }); + break; + } + } }); } catch (error) { console.log(error); diff --git a/apps/meteor/app/cloud/server/functions/connectWorkspace.ts b/apps/meteor/app/cloud/server/functions/connectWorkspace.ts index b7dc1d8b21b0..9d29fb7eb399 100644 --- a/apps/meteor/app/cloud/server/functions/connectWorkspace.ts +++ b/apps/meteor/app/cloud/server/functions/connectWorkspace.ts @@ -1,10 +1,10 @@ import { serverFetch as fetch } from '@rocket.chat/server-fetch'; +import { getRedirectUri } from './getRedirectUri'; +import { saveRegistrationData } from './saveRegistrationData'; import { CloudWorkspaceConnectionError } from '../../../../lib/errors/CloudWorkspaceConnectionError'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { settings } from '../../../settings/server'; -import { getRedirectUri } from './getRedirectUri'; -import { saveRegistrationData } from './saveRegistrationData'; const fetchRegistrationDataPayload = async ({ token, diff --git a/apps/meteor/app/cloud/server/functions/getCheckoutUrl.ts b/apps/meteor/app/cloud/server/functions/getCheckoutUrl.ts index 2665f31c8498..914020e96471 100644 --- a/apps/meteor/app/cloud/server/functions/getCheckoutUrl.ts +++ b/apps/meteor/app/cloud/server/functions/getCheckoutUrl.ts @@ -1,10 +1,10 @@ import { serverFetch as fetch } from '@rocket.chat/server-fetch'; +import { getWorkspaceAccessTokenOrThrow } from './getWorkspaceAccessToken'; +import { syncWorkspace } from './syncWorkspace'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { settings } from '../../../settings/server'; import { getURL } from '../../../utils/server/getURL'; -import { getWorkspaceAccessTokenOrThrow } from './getWorkspaceAccessToken'; -import { syncWorkspace } from './syncWorkspace'; export const fallback = `https://go.rocket.chat/i/contact-sales`; diff --git a/apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.ts b/apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.ts index 0550a3d7f238..d787fd16cf65 100644 --- a/apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.ts +++ b/apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.ts @@ -1,6 +1,7 @@ import { Settings } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; +import { updateAuditedBySystem } from '../../../../server/settings/lib/auditedSettingUpdates'; import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { userScopes } from '../oauthScopes'; @@ -9,7 +10,9 @@ import { getRedirectUri } from './getRedirectUri'; export async function getOAuthAuthorizationUrl() { const state = Random.id(); - await Settings.updateValueById('Cloud_Workspace_Registration_State', state); + await updateAuditedBySystem({ + reason: 'getOAuthAuthorizationUrl', + })(Settings.updateValueById, 'Cloud_Workspace_Registration_State', state); void notifyOnSettingChangedById('Cloud_Workspace_Registration_State'); diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceKey.ts b/apps/meteor/app/cloud/server/functions/getWorkspaceKey.ts index 639f29402fe9..2a2b8e8858fd 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceKey.ts +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceKey.ts @@ -1,5 +1,5 @@ -import { settings } from '../../../settings/server'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; +import { settings } from '../../../settings/server'; export async function getWorkspaceKey() { const { workspaceRegistered } = await retrieveRegistrationStatus(); diff --git a/apps/meteor/app/cloud/server/functions/registerPreIntentWorkspaceWizard.ts b/apps/meteor/app/cloud/server/functions/registerPreIntentWorkspaceWizard.ts index e0865c24156a..a102ed359053 100644 --- a/apps/meteor/app/cloud/server/functions/registerPreIntentWorkspaceWizard.ts +++ b/apps/meteor/app/cloud/server/functions/registerPreIntentWorkspaceWizard.ts @@ -2,9 +2,9 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; +import { buildWorkspaceRegistrationData } from './buildRegistrationData'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { settings } from '../../../settings/server'; -import { buildWorkspaceRegistrationData } from './buildRegistrationData'; export async function registerPreIntentWorkspaceWizard(): Promise { const firstUser = (await Users.getOldest({ projection: { name: 1, emails: 1 } })) as IUser | undefined; diff --git a/apps/meteor/app/cloud/server/functions/removeLicense.ts b/apps/meteor/app/cloud/server/functions/removeLicense.ts index b9afe2ddf09a..88f1d6825177 100644 --- a/apps/meteor/app/cloud/server/functions/removeLicense.ts +++ b/apps/meteor/app/cloud/server/functions/removeLicense.ts @@ -1,12 +1,12 @@ import { serverFetch as fetch } from '@rocket.chat/server-fetch'; +import { CloudWorkspaceAccessTokenEmptyError, getWorkspaceAccessToken } from './getWorkspaceAccessToken'; +import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; +import { syncWorkspace } from './syncWorkspace'; import { callbacks } from '../../../../lib/callbacks'; import { CloudWorkspaceConnectionError } from '../../../../lib/errors/CloudWorkspaceConnectionError'; import { CloudWorkspaceRegistrationError } from '../../../../lib/errors/CloudWorkspaceRegistrationError'; import { settings } from '../../../settings/server'; -import { CloudWorkspaceAccessTokenEmptyError, getWorkspaceAccessToken } from './getWorkspaceAccessToken'; -import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; -import { syncWorkspace } from './syncWorkspace'; export async function removeLicense() { try { diff --git a/apps/meteor/app/cloud/server/functions/removeWorkspaceRegistrationInfo.ts b/apps/meteor/app/cloud/server/functions/removeWorkspaceRegistrationInfo.ts index bf2b5d085945..678f6233b0a2 100644 --- a/apps/meteor/app/cloud/server/functions/removeWorkspaceRegistrationInfo.ts +++ b/apps/meteor/app/cloud/server/functions/removeWorkspaceRegistrationInfo.ts @@ -1,7 +1,8 @@ import { Settings, WorkspaceCredentials } from '@rocket.chat/models'; -import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; +import { updateAuditedBySystem } from '../../../../server/settings/lib/auditedSettingUpdates'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; export async function removeWorkspaceRegistrationInfo() { const { workspaceRegistered } = await retrieveRegistrationStatus(); @@ -24,10 +25,12 @@ export async function removeWorkspaceRegistrationInfo() { const promises = settingsIds.map((settingId) => { if (settingId === 'Show_Setup_Wizard') { - return Settings.updateValueById('Show_Setup_Wizard', 'in_progress'); + return updateAuditedBySystem({ + reason: 'removeWorkspaceRegistrationInfo', + })(Settings.updateValueById, 'Show_Setup_Wizard', 'in_progress'); } - return Settings.resetValueById(settingId, null); + return updateAuditedBySystem({ reason: 'removeWorkspaceRegistrationInfo' })(Settings.resetValueById, settingId, null); }); (await Promise.all(promises)).forEach((value, index) => { diff --git a/apps/meteor/app/cloud/server/functions/saveRegistrationData.ts b/apps/meteor/app/cloud/server/functions/saveRegistrationData.ts index 63633c567845..c820411775d3 100644 --- a/apps/meteor/app/cloud/server/functions/saveRegistrationData.ts +++ b/apps/meteor/app/cloud/server/functions/saveRegistrationData.ts @@ -1,9 +1,10 @@ import { applyLicense } from '@rocket.chat/license'; import { Settings } from '@rocket.chat/models'; +import { syncCloudData } from './syncWorkspace/syncCloudData'; +import { updateAuditedBySystem } from '../../../../server/settings/lib/auditedSettingUpdates'; import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; -import { syncCloudData } from './syncWorkspace/syncCloudData'; type SaveRegistrationDataDTO = { workspaceId: string; @@ -59,7 +60,13 @@ async function saveRegistrationDataBase({ { _id: 'Cloud_Workspace_Registration_Client_Uri', value: registration_client_uri }, ]; - const promises = [...settingsData.map(({ _id, value }) => Settings.updateValueById(_id, value))]; + const promises = [ + ...settingsData.map(({ _id, value }) => + updateAuditedBySystem({ + reason: 'saveRegistrationDataBase', + })(Settings.updateValueById, _id, value), + ), + ]; (await Promise.all(promises)).forEach((value, index) => { if (value?.modifiedCount) { @@ -67,7 +74,10 @@ async function saveRegistrationDataBase({ } }); - // TODO: Why is this taking so long that needs a timeout? + // Question: Why is this taking so long that needs a timeout? + // Answer: we use cache that requires a 'roundtrip' through the db and the application + // we need to make sure that the cache is updated before we continue the procedures + // we don't actually need to wait a whole second for this, but look this is just a retry mechanism it doesn't mean that actually takes all this time for await (const retry of Array.from({ length: 10 })) { const isSettingsUpdated = settings.get('Register_Server') === true && diff --git a/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts b/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts index 1fb2dcc06449..bdd6cedc018d 100644 --- a/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts +++ b/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts @@ -1,12 +1,13 @@ import { Settings } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; -import { SystemLogger } from '../../../../server/lib/logger/system'; -import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; -import { settings } from '../../../settings/server'; import { buildWorkspaceRegistrationData } from './buildRegistrationData'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; import { syncWorkspace } from './syncWorkspace'; +import { SystemLogger } from '../../../../server/lib/logger/system'; +import { updateAuditedBySystem } from '../../../../server/settings/lib/auditedSettingUpdates'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; +import { settings } from '../../../settings/server'; export async function startRegisterWorkspace(resend = false) { const { workspaceRegistered } = await retrieveRegistrationStatus(); @@ -15,7 +16,6 @@ export async function startRegisterWorkspace(resend = false) { return true; } - (await Settings.updateValueById('Register_Server', true)).modifiedCount && void notifyOnSettingChangedById('Register_Server'); const regInfo = await buildWorkspaceRegistrationData(undefined); @@ -48,7 +48,9 @@ export async function startRegisterWorkspace(resend = false) { return false; } - await Settings.updateValueById('Cloud_Workspace_Id', payload.id); + await updateAuditedBySystem({ + reason: 'startRegisterWorkspace', + })(Settings.updateValueById, 'Cloud_Workspace_Id', payload.id); return true; } diff --git a/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts b/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts index 382478db61c7..36b858d932c8 100644 --- a/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts +++ b/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts @@ -1,9 +1,9 @@ import type { CloudRegistrationIntentData } from '@rocket.chat/core-typings'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; +import { buildWorkspaceRegistrationData } from './buildRegistrationData'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { settings } from '../../../settings/server'; -import { buildWorkspaceRegistrationData } from './buildRegistrationData'; export async function startRegisterWorkspaceSetupWizard(resend = false, email: string): Promise { const regInfo = await buildWorkspaceRegistrationData(email); diff --git a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts index f4334bd04d64..ad0dffca27d8 100644 --- a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts +++ b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts @@ -12,6 +12,7 @@ import { supportedVersions as supportedVersionsFromBuild } from '../../../../uti import { buildVersionUpdateMessage } from '../../../../version-check/server/functions/buildVersionUpdateMessage'; import { generateWorkspaceBearerHttpHeader } from '../getWorkspaceAccessToken'; import { supportedVersionsChooseLatest } from './supportedVersionsChooseLatest'; +import { updateAuditedBySystem } from '../../../../../server/settings/lib/auditedSettingUpdates'; declare module '@rocket.chat/core-typings' { interface ILicenseV3 { @@ -34,7 +35,7 @@ export const wrapPromise = ( } > => promise - .then((result) => ({ success: true, result } as const)) + .then((result) => ({ success: true, result }) as const) .catch((error) => ({ success: false, error, @@ -66,7 +67,15 @@ const cacheValueInSettings = ( SystemLogger.debug(`Resetting cached value ${key} in settings`); const value = await fn(); - (await Settings.updateValueById(key, value)).modifiedCount && void notifyOnSettingChangedById(key); + if ( + ( + await updateAuditedBySystem({ + reason: 'cacheValueInSettings reset', + })(Settings.updateValueById, key, value) + ).modifiedCount + ) { + void notifyOnSettingChangedById(key); + } return value; }; diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/index.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/index.ts index a410ea274c07..2ae63939e9d9 100644 --- a/apps/meteor/app/cloud/server/functions/syncWorkspace/index.ts +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/index.ts @@ -1,10 +1,10 @@ import { CloudWorkspaceRegistrationError } from '../../../../../lib/errors/CloudWorkspaceRegistrationError'; import { SystemLogger } from '../../../../../server/lib/logger/system'; import { CloudWorkspaceAccessTokenEmptyError, CloudWorkspaceAccessTokenError, isAbortError } from '../getWorkspaceAccessToken'; -import { getCachedSupportedVersionsToken } from '../supportedVersionsToken/supportedVersionsToken'; import { announcementSync } from './announcementSync'; import { legacySyncWorkspace } from './legacySyncWorkspace'; import { syncCloudData } from './syncCloudData'; +import { getCachedSupportedVersionsToken } from '../supportedVersionsToken/supportedVersionsToken'; /** * Syncs the workspace with the cloud diff --git a/apps/meteor/app/cloud/server/functions/userLogout.ts b/apps/meteor/app/cloud/server/functions/userLogout.ts index 386137ced604..590a581ed4f0 100644 --- a/apps/meteor/app/cloud/server/functions/userLogout.ts +++ b/apps/meteor/app/cloud/server/functions/userLogout.ts @@ -1,10 +1,10 @@ import { Users } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; -import { SystemLogger } from '../../../../server/lib/logger/system'; -import { settings } from '../../../settings/server'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; import { userLoggedOut } from './userLoggedOut'; +import { SystemLogger } from '../../../../server/lib/logger/system'; +import { settings } from '../../../settings/server'; export async function userLogout(userId: string): Promise { const { workspaceRegistered } = await retrieveRegistrationStatus(); diff --git a/apps/meteor/app/cloud/server/index.ts b/apps/meteor/app/cloud/server/index.ts index 6a82c16331c0..95c987610351 100644 --- a/apps/meteor/app/cloud/server/index.ts +++ b/apps/meteor/app/cloud/server/index.ts @@ -1,12 +1,12 @@ import { cronJobs } from '@rocket.chat/cron'; import { Meteor } from 'meteor/meteor'; -import { SystemLogger } from '../../../server/lib/logger/system'; import { connectWorkspace } from './functions/connectWorkspace'; import { CloudWorkspaceAccessTokenEmptyError, getWorkspaceAccessToken } from './functions/getWorkspaceAccessToken'; import { getWorkspaceAccessTokenWithScope } from './functions/getWorkspaceAccessTokenWithScope'; import { retrieveRegistrationStatus } from './functions/retrieveRegistrationStatus'; import { syncWorkspace } from './functions/syncWorkspace'; +import { SystemLogger } from '../../../server/lib/logger/system'; import './methods'; const licenseCronName = 'Cloud Workspace Sync'; diff --git a/apps/meteor/app/cloud/server/methods.ts b/apps/meteor/app/cloud/server/methods.ts index 29daefe0d58c..c132dbb1a48c 100644 --- a/apps/meteor/app/cloud/server/methods.ts +++ b/apps/meteor/app/cloud/server/methods.ts @@ -2,7 +2,6 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; import { buildWorkspaceRegistrationData } from './functions/buildRegistrationData'; import { checkUserHasCloudLogin } from './functions/checkUserHasCloudLogin'; import { connectWorkspace } from './functions/connectWorkspace'; @@ -12,6 +11,7 @@ import { retrieveRegistrationStatus } from './functions/retrieveRegistrationStat import { startRegisterWorkspace } from './functions/startRegisterWorkspace'; import { syncWorkspace } from './functions/syncWorkspace'; import { userLogout } from './functions/userLogout'; +import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/apps/meteor/app/crowd/server/crowd.ts b/apps/meteor/app/crowd/server/crowd.ts index 70f54dd7b726..3219a851c8c4 100644 --- a/apps/meteor/app/crowd/server/crowd.ts +++ b/apps/meteor/app/crowd/server/crowd.ts @@ -6,13 +6,13 @@ import AtlassianCrowd from 'atlassian-crowd-patched'; import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; +import { logger } from './logger'; import { crowdIntervalValuesToCronMap } from '../../../server/settings/crowd'; import { deleteUser } from '../../lib/server/functions/deleteUser'; import { _setRealName } from '../../lib/server/functions/setRealName'; import { setUserActiveStatus } from '../../lib/server/functions/setUserActiveStatus'; import { notifyOnUserChange, notifyOnUserChangeById, notifyOnUserChangeAsync } from '../../lib/server/lib/notifyListener'; import { settings } from '../../settings/server'; -import { logger } from './logger'; type CrowdUser = Pick & { crowd: Record; crowd_username: string }; diff --git a/apps/meteor/app/crowd/server/methods.ts b/apps/meteor/app/crowd/server/methods.ts index 48faa2fcbcab..c659afc4fbf3 100644 --- a/apps/meteor/app/crowd/server/methods.ts +++ b/apps/meteor/app/crowd/server/methods.ts @@ -2,10 +2,10 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; -import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; -import { settings } from '../../settings/server'; import { CROWD } from './crowd'; import { logger } from './logger'; +import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; +import { settings } from '../../settings/server'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/apps/meteor/app/custom-oauth/client/CustomOAuth.ts b/apps/meteor/app/custom-oauth/client/CustomOAuth.ts index 1d57d1969d93..7849498fa4b9 100644 --- a/apps/meteor/app/custom-oauth/client/CustomOAuth.ts +++ b/apps/meteor/app/custom-oauth/client/CustomOAuth.ts @@ -27,7 +27,10 @@ export class CustomOAuth implements IOAuthProvider { public responseType: string; - constructor(public readonly name: string, options: OauthConfig) { + constructor( + public readonly name: string, + options: OauthConfig, + ) { this.name = name; if (!Match.test(this.name, String)) { throw new Meteor.Error('CustomOAuth: Name is required and must be String'); diff --git a/apps/meteor/app/custom-oauth/server/custom_oauth_server.js b/apps/meteor/app/custom-oauth/server/custom_oauth_server.js index a546a7c527eb..c45f3619c8aa 100644 --- a/apps/meteor/app/custom-oauth/server/custom_oauth_server.js +++ b/apps/meteor/app/custom-oauth/server/custom_oauth_server.js @@ -9,12 +9,12 @@ import { OAuth } from 'meteor/oauth'; import { ServiceConfiguration } from 'meteor/service-configuration'; import _ from 'underscore'; +import { normalizers, fromTemplate, renameInvalidProperties } from './transform_helpers'; import { callbacks } from '../../../lib/callbacks'; import { isURL } from '../../../lib/utils/isURL'; import { notifyOnUserChange } from '../../lib/server/lib/notifyListener'; import { registerAccessTokenService } from '../../lib/server/oauth/oauth'; import { settings } from '../../settings/server'; -import { normalizers, fromTemplate, renameInvalidProperties } from './transform_helpers'; const logger = new Logger('CustomOAuth'); diff --git a/apps/meteor/app/custom-sounds/server/methods/uploadCustomSound.ts b/apps/meteor/app/custom-sounds/server/methods/uploadCustomSound.ts index f955f373ed4d..64286bb71d86 100644 --- a/apps/meteor/app/custom-sounds/server/methods/uploadCustomSound.ts +++ b/apps/meteor/app/custom-sounds/server/methods/uploadCustomSound.ts @@ -3,10 +3,10 @@ import type { RequiredField } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Meteor } from 'meteor/meteor'; +import type { ICustomSoundData } from './insertOrUpdateSound'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { RocketChatFile } from '../../../file/server'; import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds'; -import type { ICustomSoundData } from './insertOrUpdateSound'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts b/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts deleted file mode 100644 index ecf014248830..000000000000 --- a/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import CreateDiscussion from '../../../client/components/CreateDiscussion/CreateDiscussion'; -import { imperativeModal } from '../../../client/lib/imperativeModal'; -import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; -import { messageArgs } from '../../../client/lib/utils/messageArgs'; -import { hasPermission } from '../../authorization/client'; -import { settings } from '../../settings/client'; -import { MessageAction } from '../../ui-utils/client'; - -Meteor.startup(() => { - Tracker.autorun(() => { - if (!settings.get('Discussion_enabled')) { - return MessageAction.removeButton('start-discussion'); - } - - MessageAction.addButton({ - id: 'start-discussion', - icon: 'discussion', - label: 'Discussion_start', - type: 'communication', - context: ['message', 'message-mobile', 'videoconf'], - async action(_, props) { - const { message = messageArgs(this).msg, room } = props; - - imperativeModal.open({ - component: CreateDiscussion, - props: { - defaultParentRoom: room?.prid || room?._id, - onClose: imperativeModal.close, - parentMessageId: message._id, - nameSuggestion: message?.msg?.substr(0, 140), - }, - }); - }, - condition({ - message: { - u: { _id: uid }, - drid, - dcount, - }, - room, - subscription, - user, - }) { - if (drid || !Number.isNaN(Number(dcount))) { - return false; - } - if (!subscription) { - return false; - } - const isLivechatRoom = roomCoordinator.isLivechatRoom(room.t); - if (isLivechatRoom) { - return false; - } - - if (!user) { - return false; - } - - return uid !== user._id ? hasPermission('start-discussion-other-user', room._id) : hasPermission('start-discussion', room._id); - }, - order: 1, - group: 'menu', - }); - }); -}); diff --git a/apps/meteor/app/discussion/client/index.ts b/apps/meteor/app/discussion/client/index.ts deleted file mode 100644 index 7c0a6f72e6cc..000000000000 --- a/apps/meteor/app/discussion/client/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Other UI extensions -import './createDiscussionMessageAction'; diff --git a/apps/meteor/app/discussion/server/methods/createDiscussion.ts b/apps/meteor/app/discussion/server/methods/createDiscussion.ts index f2b4533489d8..c4b8fb8e9a2e 100644 --- a/apps/meteor/app/discussion/server/methods/createDiscussion.ts +++ b/apps/meteor/app/discussion/server/methods/createDiscussion.ts @@ -3,6 +3,7 @@ import type { IMessage, IRoom, IUser, MessageAttachmentDefault } from '@rocket.c import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Messages, Rooms, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; +import { check, Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { i18n } from '../../../../server/lib/i18n'; @@ -246,6 +247,13 @@ Meteor.methods({ * @param {boolean} encrypted - if the discussion's e2e encryption should be enabled. */ async createDiscussion({ prid, pmid, t_name: discussionName, reply, users, encrypted }: CreateDiscussionProperties) { + check(prid, Match.Maybe(String)); + check(pmid, Match.Maybe(String)); + check(reply, Match.Maybe(String)); + check(discussionName, String); + check(users, [String]); + check(encrypted, Match.Maybe(Boolean)); + const uid = Meteor.userId(); if (!uid) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { diff --git a/apps/meteor/app/dolphin/client/index.ts b/apps/meteor/app/dolphin/client/index.ts index 0cc16f78319d..cf327e4971bb 100644 --- a/apps/meteor/app/dolphin/client/index.ts +++ b/apps/meteor/app/dolphin/client/index.ts @@ -1,2 +1 @@ import './lib'; -import './login-button.css'; diff --git a/apps/meteor/app/dolphin/client/login-button.css b/apps/meteor/app/dolphin/client/login-button.css deleted file mode 100644 index b511270b27f1..000000000000 --- a/apps/meteor/app/dolphin/client/login-button.css +++ /dev/null @@ -1,16 +0,0 @@ -.icon-dolphin { - display: inline-block; - - width: 30px; - height: 20px; - - vertical-align: middle; - - background-image: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNDMwIDQzMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxnIGlkPSJzaWduLWluIiB0cmFuc2Zvcm09Im1hdHJpeCgwLjUgMCAwIDAuNSAwIDApIj4NCjxwYXRoIGZpbGw9IiNmZmZmZmYiIGQ9Ik02NjAuNjcyIDUwMC4wMzJxMCAxNC41MDggLTEwLjYwMiAyNS4xMWwtMzAzLjU1MiAzMDMuNTUycS0xMC42MDIgMTAuNjAyIC0yNS4xMSAxMC42MDJ0LTI1LjExIC0xMC42MDIgLTEwLjYwMiAtMjUuMTF2LTE2MC43MDRoLTI0OS45ODRxLTE0LjUwOCAwIC0yNS4xMSAtMTAuNjAydC0xMC42MDIgLTI1LjExdi0yMTQuMjcycTAgLTE0LjUwOCAxMC42MDIgLTI1LjExdDI1LjExIC0xMC42MDJoMjQ5Ljk4NHYtMTYwLjcwNHEwIC0xNC41MDggMTAuNjAyIC0yNS4xMXQyNS4xMSAtMTAuNjAyIDI1LjExIDEwLjYwMmwzMDMuNTUyIDMwMy41NTJxMTAuNjAyIDEwLjYwMiAxMC42MDIgMjUuMTF6bTE5Ni40MTYgLTE5Ni40MTZ2MzkyLjgzMnEwIDY2LjQwMiAtNDcuMTUxIDExMy41NTN0LTExMy41NTMgNDcuMTUxaC0xNzguNTZxLTcuMjU0IDAgLTEyLjU1NSAtNS4zMDF0LTUuMzAxIC0xMi41NTVxMCAtMi4yMzIgLS41NTggLTExLjE2dC0uMjc5IC0xNC43ODcgMS42NzQgLTEzLjExMyA1LjU4IC0xMC44ODEgMTEuNDM5IC0zLjYyN2gxNzguNTZxMzYuODI4IDAgNjMuMDU0IC0yNi4yMjZ0MjYuMjI2IC02My4wNTR2LTM5Mi44MzJxMCAtMzYuODI4IC0yNi4yMjYgLTYzLjA1NHQtNjMuMDU0IC0yNi4yMjZoLTE3NC4wOTZ0LTYuNDE3IC0uNTU4IC02LjQxNyAtMS42NzQgLTQuNDY0IC0zLjA2OSAtMy45MDYgLTUuMDIyIC0xLjExNiAtNy41MzNxMCAtMi4yMzIgLS41NTggLTExLjE2dC0uMjc5IC0xNC43ODcgMS42NzQgLTEzLjExMyA1LjU4IC0xMC44ODEgMTEuNDM5IC0zLjYyN2gxNzguNTZxNjYuNDAyIDAgMTEzLjU1MyA0Ny4xNTF0NDcuMTUxIDExMy41NTN6Ii8+DQo8L2c+DQo8L3N2Zz4=); - background-repeat: no-repeat; -} - -.icon-dolphin ~ .icon-spin, -.icon-dolphin ~ span { - vertical-align: middle; -} diff --git a/apps/meteor/app/drupal/client/index.ts b/apps/meteor/app/drupal/client/index.ts index 0cc16f78319d..cf327e4971bb 100644 --- a/apps/meteor/app/drupal/client/index.ts +++ b/apps/meteor/app/drupal/client/index.ts @@ -1,2 +1 @@ import './lib'; -import './login-button.css'; diff --git a/apps/meteor/app/drupal/client/login-button.css b/apps/meteor/app/drupal/client/login-button.css deleted file mode 100644 index 2cda29b8c62e..000000000000 --- a/apps/meteor/app/drupal/client/login-button.css +++ /dev/null @@ -1,13 +0,0 @@ -.icon-drupal.service-icon { - display: inline-block; - - width: 21px; - height: 28px; - - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAcCAYAAACOGPReAAAABGdBTUEAALGPC/xhBQAAAAZiS0dEAP8A/wD/oL2nkwAAAcNJREFUSMetlbFrFFEQh7/dJnhpDHcx6SyiRLhLkUKDsRZB8S9IWi2Syj8iVooQ0FLBNlqJRpNO8Br7FLkoRDBooQHhNCr6WTjB48zd7d7uNI+d+c333pt9bx5kNPVGVm1WYF39rTbKhD7zr62XBVwI4MsYF4sCZ9S2uq6mMbbVmWGBx9WW+kGdCN+J+G6pY3mBo2pT/ape6IrNx2qb6mhW4Ii6qf5Ur/TQXI74pjoyCJioa3F8FgdoF0K3pib9hLfiDy9l3NVS6G/3EiyH4GbO+q9E3nJ3oKF+Vx/33Urvkj2K/EZn4JW6l/uY/MsfU9+rzUPHXCz/+jDADvC14Myhrqpf1EpB6LHgrCbqa+BzkiSXikAD/ByopcAUsFUUGLYFTKVABWiXBG0DlRTYB8ZLgo4D+ynQAuolQevAdgq8AM6rk0Vo0R7ngQ3UU+qvvNfzCOhKcE4fOu6r39TpIYHT0XsfdDon45puq7WcwFrk7f1XQvVsdPS3Wd+geMPeRN65XqJZdVf9od5Tz/TZ7t3Q7aqzg2avqnfUg2gQO+pT9WGMO+E/CF21m5H0gU8AV4GLwEmgCnwC3gEbwJMkST4elfsHJbIReGbv9uUAAAAASUVORK5CYII=); - background-repeat: no-repeat; -} - -.button.external-login.drupal { - background-color: #0f85b6; -} diff --git a/apps/meteor/app/e2e/client/events.js b/apps/meteor/app/e2e/client/events.ts similarity index 83% rename from apps/meteor/app/e2e/client/events.js rename to apps/meteor/app/e2e/client/events.ts index c59b20594b85..9ccef3d7b28d 100644 --- a/apps/meteor/app/e2e/client/events.js +++ b/apps/meteor/app/e2e/client/events.ts @@ -3,5 +3,5 @@ import { Accounts } from 'meteor/accounts-base'; import { e2e } from './rocketchat.e2e'; Accounts.onLogout(() => { - e2e.stopClient(); + void e2e.stopClient(); }); diff --git a/apps/meteor/app/e2e/client/helper.js b/apps/meteor/app/e2e/client/helper.ts similarity index 64% rename from apps/meteor/app/e2e/client/helper.js rename to apps/meteor/app/e2e/client/helper.ts index 25d9e9407801..66ca3bf1cc2e 100644 --- a/apps/meteor/app/e2e/client/helper.js +++ b/apps/meteor/app/e2e/client/helper.ts @@ -1,24 +1,20 @@ import { Random } from '@rocket.chat/random'; import ByteBuffer from 'bytebuffer'; -// eslint-disable-next-line no-proto -const StaticArrayBufferProto = new ArrayBuffer().__proto__; - -export function toString(thing) { +export function toString(thing: any) { if (typeof thing === 'string') { return thing; } - // eslint-disable-next-line new-cap - return new ByteBuffer.wrap(thing).toString('binary'); + + return ByteBuffer.wrap(thing).toString('binary'); } -export function toArrayBuffer(thing) { +export function toArrayBuffer(thing: any) { if (thing === undefined) { return undefined; } - if (thing === Object(thing)) { - // eslint-disable-next-line no-proto - if (thing.__proto__ === StaticArrayBufferProto) { + if (typeof thing === 'object') { + if (Object.getPrototypeOf(thing) === ArrayBuffer.prototype) { return thing; } } @@ -26,11 +22,11 @@ export function toArrayBuffer(thing) { if (typeof thing !== 'string') { throw new Error(`Tried to convert a non-string of type ${typeof thing} to an array buffer`); } - // eslint-disable-next-line new-cap - return new ByteBuffer.wrap(thing, 'binary').toArrayBuffer(); + + return ByteBuffer.wrap(thing, 'binary').toArrayBuffer(); } -export function joinVectorAndEcryptedData(vector, encryptedData) { +export function joinVectorAndEcryptedData(vector: any, encryptedData: any) { const cipherText = new Uint8Array(encryptedData); const output = new Uint8Array(vector.length + cipherText.length); output.set(vector, 0); @@ -38,30 +34,30 @@ export function joinVectorAndEcryptedData(vector, encryptedData) { return output; } -export function splitVectorAndEcryptedData(cipherText) { +export function splitVectorAndEcryptedData(cipherText: any) { const vector = cipherText.slice(0, 16); const encryptedData = cipherText.slice(16); return [vector, encryptedData]; } -export async function encryptRSA(key, data) { +export async function encryptRSA(key: any, data: any) { return crypto.subtle.encrypt({ name: 'RSA-OAEP' }, key, data); } -export async function encryptAES(vector, key, data) { +export async function encryptAES(vector: any, key: any, data: any) { return crypto.subtle.encrypt({ name: 'AES-CBC', iv: vector }, key, data); } -export async function encryptAESCTR(vector, key, data) { +export async function encryptAESCTR(vector: any, key: any, data: any) { return crypto.subtle.encrypt({ name: 'AES-CTR', counter: vector, length: 64 }, key, data); } -export async function decryptRSA(key, data) { +export async function decryptRSA(key: any, data: any) { return crypto.subtle.decrypt({ name: 'RSA-OAEP' }, key, data); } -export async function decryptAES(vector, key, data) { +export async function decryptAES(vector: any, key: any, data: any) { return crypto.subtle.decrypt({ name: 'AES-CBC', iv: vector }, key, data); } @@ -86,54 +82,54 @@ export async function generateRSAKey() { ); } -export async function exportJWKKey(key) { +export async function exportJWKKey(key: any) { return crypto.subtle.exportKey('jwk', key); } -export async function importRSAKey(keyData, keyUsages = ['encrypt', 'decrypt']) { +export async function importRSAKey(keyData: any, keyUsages: ReadonlyArray = ['encrypt', 'decrypt']) { return crypto.subtle.importKey( - 'jwk', + 'jwk' as any, keyData, { name: 'RSA-OAEP', modulusLength: 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]), hash: { name: 'SHA-256' }, - }, + } as any, true, keyUsages, ); } -export async function importAESKey(keyData, keyUsages = ['encrypt', 'decrypt']) { +export async function importAESKey(keyData: any, keyUsages: ReadonlyArray = ['encrypt', 'decrypt']) { return crypto.subtle.importKey('jwk', keyData, { name: 'AES-CBC' }, true, keyUsages); } -export async function importRawKey(keyData, keyUsages = ['deriveKey']) { +export async function importRawKey(keyData: any, keyUsages: ReadonlyArray = ['deriveKey']) { return crypto.subtle.importKey('raw', keyData, { name: 'PBKDF2' }, false, keyUsages); } -export async function deriveKey(salt, baseKey, keyUsages = ['encrypt', 'decrypt']) { +export async function deriveKey(salt: any, baseKey: any, keyUsages: ReadonlyArray = ['encrypt', 'decrypt']) { const iterations = 1000; const hash = 'SHA-256'; return crypto.subtle.deriveKey({ name: 'PBKDF2', salt, iterations, hash }, baseKey, { name: 'AES-CBC', length: 256 }, true, keyUsages); } -export async function readFileAsArrayBuffer(file) { - return new Promise((resolve, reject) => { +export async function readFileAsArrayBuffer(file: any) { + return new Promise((resolve, reject) => { const reader = new FileReader(); - reader.onload = function (evt) { - resolve(evt.target.result); + reader.onload = (evt) => { + resolve(evt.target?.result); }; - reader.onerror = function (evt) { + reader.onerror = (evt) => { reject(evt); }; reader.readAsArrayBuffer(file); }); } -export async function generateMnemonicPhrase(n, sep = ' ') { +export async function generateMnemonicPhrase(n: any, sep = ' ') { const { default: wordList } = await import('./wordList'); const result = new Array(n); let len = wordList.length; @@ -147,14 +143,14 @@ export async function generateMnemonicPhrase(n, sep = ' ') { return result.join(sep); } -export async function createSha256HashFromText(data) { +export async function createSha256HashFromText(data: any) { const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(data)); return Array.from(new Uint8Array(hash)) .map((b) => b.toString(16).padStart(2, '0')) .join(''); } -export async function sha256HashFromArrayBuffer(arrayBuffer) { +export async function sha256HashFromArrayBuffer(arrayBuffer: any) { const hashArray = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', arrayBuffer))); return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); } diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.ts similarity index 88% rename from apps/meteor/app/e2e/client/rocketchat.e2e.room.js rename to apps/meteor/app/e2e/client/rocketchat.e2e.room.ts index 4c9de837dce0..dc7efb60dc14 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.ts @@ -2,12 +2,6 @@ import { Base64 } from '@rocket.chat/base64'; import { Emitter } from '@rocket.chat/emitter'; import EJSON from 'ejson'; -import { RoomManager } from '../../../client/lib/RoomManager'; -import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; -import { RoomSettingsEnum } from '../../../definition/IRoomTypeConfig'; -import { ChatRoom, Subscriptions, Messages } from '../../models/client'; -import { sdk } from '../../utils/client/lib/SDKClient'; -import { t } from '../../utils/lib/i18n'; import { E2ERoomState } from './E2ERoomState'; import { toString, @@ -30,11 +24,17 @@ import { } from './helper'; import { log, logError } from './logger'; import { e2e } from './rocketchat.e2e'; +import { RoomManager } from '../../../client/lib/RoomManager'; +import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; +import { RoomSettingsEnum } from '../../../definition/IRoomTypeConfig'; +import { Rooms, Subscriptions, Messages } from '../../models/client'; +import { sdk } from '../../utils/client/lib/SDKClient'; +import { t } from '../../utils/lib/i18n'; const KEY_ID = Symbol('keyID'); const PAUSED = Symbol('PAUSED'); -const permitedMutations = { +const permitedMutations: any = { [E2ERoomState.NOT_STARTED]: [E2ERoomState.ESTABLISHING, E2ERoomState.DISABLED, E2ERoomState.KEYS_RECEIVED], [E2ERoomState.READY]: [E2ERoomState.DISABLED, E2ERoomState.CREATING_KEYS, E2ERoomState.WAITING_KEYS], [E2ERoomState.ERROR]: [E2ERoomState.KEYS_RECEIVED, E2ERoomState.NOT_STARTED], @@ -49,7 +49,7 @@ const permitedMutations = { ], }; -const filterMutation = (currentState, nextState) => { +const filterMutation = (currentState: any, nextState: any): any => { if (currentState === nextState) { return nextState === E2ERoomState.ERROR; } @@ -66,11 +66,29 @@ const filterMutation = (currentState, nextState) => { }; export class E2ERoom extends Emitter { - state = undefined; + state: any = undefined; - [PAUSED] = undefined; + [PAUSED]: boolean | undefined = undefined; - constructor(userId, room) { + [KEY_ID]: any; + + userId: any; + + roomId: any; + + typeOfRoom: any; + + roomKeyId: any; + + groupSessionKey: any; + + oldKeys: any; + + sessionKeyExportedString: string | undefined; + + sessionKeyExported: any; + + constructor(userId: any, room: any) { super(); this.userId = userId; @@ -93,11 +111,11 @@ export class E2ERoom extends Emitter { this.setState(E2ERoomState.NOT_STARTED); } - log(...msg) { + log(...msg: unknown[]) { log(`E2E ROOM { state: ${this.state}, rid: ${this.roomId} }`, ...msg); } - error(...msg) { + error(...msg: unknown[]) { logError(`E2E ROOM { state: ${this.state}, rid: ${this.roomId} }`, ...msg); } @@ -109,7 +127,7 @@ export class E2ERoom extends Emitter { return this.state; } - setState(requestedState) { + setState(requestedState: any) { const currentState = this.state; const nextState = filterMutation(currentState, requestedState); @@ -120,7 +138,7 @@ export class E2ERoom extends Emitter { this.state = nextState; this.log(currentState, '->', nextState); - this.emit('STATE_CHANGED', currentState, nextState, this); + this.emit('STATE_CHANGED', currentState); this.emit(nextState, this); } @@ -160,7 +178,7 @@ export class E2ERoom extends Emitter { this.setState(E2ERoomState.KEYS_RECEIVED); } - async shouldConvertSentMessages(message) { + async shouldConvertSentMessages(message: any) { if (!this.isReady() || this[PAUSED]) { return false; } @@ -197,7 +215,7 @@ export class E2ERoom extends Emitter { async decryptSubscription() { const subscription = Subscriptions.findOne({ rid: this.roomId }); - if (subscription.lastMessage?.t !== 'e2e') { + if (subscription?.lastMessage?.t !== 'e2e') { this.log('decryptSubscriptions nothing to do'); return; } @@ -245,7 +263,7 @@ export class E2ERoom extends Emitter { this.log('decryptOldRoomKeys Done'); } - async exportOldRoomKeys(oldKeys) { + async exportOldRoomKeys(oldKeys: any) { this.log('exportOldRoomKeys starting'); if (!oldKeys || oldKeys.length === 0) { this.log('exportOldRoomKeys nothing to do'); @@ -294,7 +312,7 @@ export class E2ERoom extends Emitter { this.setState(E2ERoomState.ESTABLISHING); try { - const groupKey = Subscriptions.findOne({ rid: this.roomId }).E2EKey; + const groupKey = Subscriptions.findOne({ rid: this.roomId })?.E2EKey; if (groupKey) { await this.importGroupKey(groupKey); this.setState(E2ERoomState.READY); @@ -307,7 +325,7 @@ export class E2ERoom extends Emitter { } try { - const room = ChatRoom.findOne({ _id: this.roomId }); + const room = Rooms.findOne({ _id: this.roomId })!; // Only room creator can set keys for room if (!room.e2eKeyId && this.userShouldCreateKeys(room)) { this.setState(E2ERoomState.CREATING_KEYS); @@ -325,7 +343,7 @@ export class E2ERoom extends Emitter { } } - userShouldCreateKeys(room) { + userShouldCreateKeys(room: any) { // On DMs, we'll allow any user to set the keys if (room.t === 'd') { return true; @@ -334,15 +352,15 @@ export class E2ERoom extends Emitter { return room.u._id === this.userId; } - isSupportedRoomType(type) { + isSupportedRoomType(type: any) { return roomCoordinator.getRoomDirectives(type).allowRoomSettingChange({}, RoomSettingsEnum.E2E); } - async decryptSessionKey(key) { + async decryptSessionKey(key: any) { return importAESKey(JSON.parse(await this.exportSessionKey(key))); } - async exportSessionKey(key) { + async exportSessionKey(key: any) { key = key.slice(12); key = Base64.decode(key); @@ -350,7 +368,7 @@ export class E2ERoom extends Emitter { return toString(decryptedKey); } - async importGroupKey(groupKey) { + async importGroupKey(groupKey: any) { this.log('Importing room key ->', this.roomId); // Get existing group key // const keyID = groupKey.slice(0, 12); @@ -374,7 +392,7 @@ export class E2ERoom extends Emitter { // Import session key for use. try { - const key = await importAESKey(JSON.parse(this.sessionKeyExportedString)); + const key = await importAESKey(JSON.parse(this.sessionKeyExportedString!)); // Key has been obtained. E2E is now in session. this.groupSessionKey = key; } catch (error) { @@ -402,8 +420,8 @@ export class E2ERoom extends Emitter { await sdk.rest.post('/v1/e2e.updateGroupKey', { rid: this.roomId, uid: this.userId, - key: await this.encryptGroupKeyForParticipant(e2e.publicKey), - }); + key: await this.encryptGroupKeyForParticipant(e2e.publicKey!), + } as any); await this.encryptKeyForOtherParticipants(); } catch (error) { this.error('Error exporting group key: ', error); @@ -434,7 +452,7 @@ export class E2ERoom extends Emitter { } } - onRoomKeyReset(keyID) { + onRoomKeyReset(keyID: any) { this.log(`Room keyID was reset. New keyID: ${keyID} Previous keyID: ${this.keyID}`); this.setState(E2ERoomState.WAITING_KEYS); this.keyID = keyID; @@ -455,10 +473,10 @@ export class E2ERoom extends Emitter { return; } - const usersSuggestedGroupKeys = { [this.roomId]: [] }; + const usersSuggestedGroupKeys = { [this.roomId]: [] as any[] }; for await (const user of users) { - const encryptedGroupKey = await this.encryptGroupKeyForParticipant(user.e2e.public_key); - const oldKeys = await this.encryptOldKeysForParticipant(user.e2e.public_key, decryptedOldGroupKeys); + const encryptedGroupKey = await this.encryptGroupKeyForParticipant(user.e2e!.public_key!); + const oldKeys = await this.encryptOldKeysForParticipant(user.e2e?.public_key, decryptedOldGroupKeys); usersSuggestedGroupKeys[this.roomId].push({ _id: user._id, key: encryptedGroupKey, ...(oldKeys && { oldKeys }) }); } @@ -469,7 +487,7 @@ export class E2ERoom extends Emitter { } } - async encryptOldKeysForParticipant(public_key, oldRoomKeys) { + async encryptOldKeysForParticipant(publicKey: any, oldRoomKeys: any) { if (!oldRoomKeys || oldRoomKeys.length === 0) { return; } @@ -477,7 +495,7 @@ export class E2ERoom extends Emitter { let userKey; try { - userKey = await importRSAKey(JSON.parse(public_key), ['encrypt']); + userKey = await importRSAKey(JSON.parse(publicKey), ['encrypt']); } catch (error) { return this.error('Error importing user key: ', error); } @@ -499,10 +517,10 @@ export class E2ERoom extends Emitter { } } - async encryptGroupKeyForParticipant(public_key) { + async encryptGroupKeyForParticipant(publicKey: string) { let userKey; try { - userKey = await importRSAKey(JSON.parse(public_key), ['encrypt']); + userKey = await importRSAKey(JSON.parse(publicKey), ['encrypt']); } catch (error) { return this.error('Error importing user key: ', error); } @@ -519,7 +537,7 @@ export class E2ERoom extends Emitter { } // Encrypts files before upload. I/O is in arraybuffers. - async encryptFile(file) { + async encryptFile(file: any) { // if (!this.isSupportedRoomType(this.typeOfRoom)) { // return; // } @@ -554,7 +572,7 @@ export class E2ERoom extends Emitter { } // Decrypt uploaded encrypted files. I/O is in arraybuffers. - async decryptFile(file, key, iv) { + async decryptFile(file: any, key: any, iv: any) { const ivArray = Base64.decode(iv); const cryptoKey = await window.crypto.subtle.importKey('jwk', key, { name: 'AES-CTR' }, true, ['encrypt', 'decrypt']); @@ -562,7 +580,7 @@ export class E2ERoom extends Emitter { } // Encrypts messages - async encryptText(data) { + async encryptText(data: any) { const vector = crypto.getRandomValues(new Uint8Array(16)); try { @@ -575,7 +593,7 @@ export class E2ERoom extends Emitter { } // Helper function for encryption of content - async encryptMessageContent(contentToBeEncrypted) { + async encryptMessageContent(contentToBeEncrypted: any) { const data = new TextEncoder().encode(EJSON.stringify(contentToBeEncrypted)); return { @@ -585,7 +603,7 @@ export class E2ERoom extends Emitter { } // Helper function for encryption of content - async encryptMessage(message) { + async encryptMessage(message: any) { const { msg, attachments, ...rest } = message; const content = await this.encryptMessageContent({ msg, attachments }); @@ -599,7 +617,7 @@ export class E2ERoom extends Emitter { } // Helper function for encryption of messages - encrypt(message) { + encrypt(message: any) { if (!this.isSupportedRoomType(this.typeOfRoom)) { return; } @@ -610,7 +628,7 @@ export class E2ERoom extends Emitter { const ts = new Date(); - const data = new TextEncoder('UTF-8').encode( + const data = new TextEncoder().encode( EJSON.stringify({ _id: message._id, text: message.msg, @@ -622,7 +640,7 @@ export class E2ERoom extends Emitter { return this.encryptText(data); } - async decryptContent(data) { + async decryptContent(data: any) { if (data.content && data.content.algorithm === 'rc.v1.aes-sha2') { const content = await this.decrypt(data.content.ciphertext); Object.assign(data, content); @@ -632,7 +650,7 @@ export class E2ERoom extends Emitter { } // Decrypt messages - async decryptMessage(message) { + async decryptMessage(message: any) { if (message.t !== 'e2e' || message.e2e === 'done') { return message; } @@ -653,12 +671,12 @@ export class E2ERoom extends Emitter { }; } - async doDecrypt(vector, key, cipherText) { + async doDecrypt(vector: any, key: any, cipherText: any) { const result = await decryptAES(vector, key, cipherText); return EJSON.parse(new TextDecoder('UTF-8').decode(new Uint8Array(result))); } - async decrypt(message) { + async decrypt(message: any) { const keyID = message.slice(0, 12); message = message.slice(12); @@ -666,7 +684,7 @@ export class E2ERoom extends Emitter { let oldKey = ''; if (keyID !== this.keyID) { - const oldRoomKey = this.oldKeys?.find((key) => key.e2eKeyId === keyID); + const oldRoomKey = this.oldKeys?.find((key: any) => key.e2eKeyId === keyID); // Messages already contain a keyID stored with them // That means that if we cannot find a keyID for the key the message has preppended to // The message is indecipherable. @@ -691,21 +709,21 @@ export class E2ERoom extends Emitter { } } - provideKeyToUser(keyId) { + provideKeyToUser(keyId: any) { if (this.keyID !== keyId) { return; } - this.encryptKeyForOtherParticipants(); + void this.encryptKeyForOtherParticipants(); this.setState(E2ERoomState.READY); } - onStateChange(cb) { + onStateChange(cb: any) { this.on('STATE_CHANGED', cb); return () => this.off('STATE_CHANGED', cb); } - async encryptGroupKeyForParticipantsWaitingForTheKeys(users) { + async encryptGroupKeyForParticipantsWaitingForTheKeys(users: any[]) { if (!this.isReady()) { return; } diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 3b2fd01621e4..38df2ba1bc34 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -10,22 +10,6 @@ import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import * as banners from '../../../client/lib/banners'; -import type { LegacyBannerPayload } from '../../../client/lib/banners'; -import { imperativeModal } from '../../../client/lib/imperativeModal'; -import { dispatchToastMessage } from '../../../client/lib/toast'; -import { mapMessageFromApi } from '../../../client/lib/utils/mapMessageFromApi'; -import { waitUntilFind } from '../../../client/lib/utils/waitUntilFind'; -import EnterE2EPasswordModal from '../../../client/views/e2e/EnterE2EPasswordModal'; -import SaveE2EPasswordModal from '../../../client/views/e2e/SaveE2EPasswordModal'; -import { createQuoteAttachment } from '../../../lib/createQuoteAttachment'; -import { getMessageUrlRegex } from '../../../lib/getMessageUrlRegex'; -import { isTruthy } from '../../../lib/isTruthy'; -import { ChatRoom, Subscriptions, Messages } from '../../models/client'; -import { settings } from '../../settings/client'; -import { getUserAvatarURL } from '../../utils/client'; -import { sdk } from '../../utils/client/lib/SDKClient'; -import { t } from '../../utils/lib/i18n'; import { E2EEState } from './E2EEState'; import { toString, @@ -43,8 +27,24 @@ import { } from './helper'; import { log, logError } from './logger'; import { E2ERoom } from './rocketchat.e2e.room'; +import * as banners from '../../../client/lib/banners'; +import type { LegacyBannerPayload } from '../../../client/lib/banners'; +import { imperativeModal } from '../../../client/lib/imperativeModal'; +import { dispatchToastMessage } from '../../../client/lib/toast'; +import { mapMessageFromApi } from '../../../client/lib/utils/mapMessageFromApi'; +import { waitUntilFind } from '../../../client/lib/utils/waitUntilFind'; +import EnterE2EPasswordModal from '../../../client/views/e2e/EnterE2EPasswordModal'; +import SaveE2EPasswordModal from '../../../client/views/e2e/SaveE2EPasswordModal'; +import { createQuoteAttachment } from '../../../lib/createQuoteAttachment'; +import { getMessageUrlRegex } from '../../../lib/getMessageUrlRegex'; +import { isTruthy } from '../../../lib/isTruthy'; +import { Rooms, Subscriptions, Messages } from '../../models/client'; +import { settings } from '../../settings/client'; +import { getUserAvatarURL } from '../../utils/client'; +import { sdk } from '../../utils/client/lib/SDKClient'; +import { t } from '../../utils/lib/i18n'; -import './events.js'; +import './events'; let failedToDecodeKey = false; @@ -254,7 +254,7 @@ class E2E extends Emitter { } async getInstanceByRoomId(rid: IRoom['_id']): Promise { - const room = await waitUntilFind(() => ChatRoom.findOne({ _id: rid })); + const room = await waitUntilFind(() => Rooms.findOne({ _id: rid })); if (room.t !== 'd' && room.t !== 'p') { return null; @@ -835,7 +835,7 @@ class E2E extends Emitter { } const keyDistribution = async () => { - const roomIds = ChatRoom.find({ + const roomIds = Rooms.find({ 'usersWaitingForE2EKeys': { $exists: true }, 'usersWaitingForE2EKeys.userId': { $ne: Meteor.userId() }, }).map((room) => room._id); diff --git a/apps/meteor/app/emoji-custom/client/lib/emojiCustom.js b/apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts similarity index 57% rename from apps/meteor/app/emoji-custom/client/lib/emojiCustom.js rename to apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts index 64f1df9bd932..8b24cd0c29c7 100644 --- a/apps/meteor/app/emoji-custom/client/lib/emojiCustom.js +++ b/apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts @@ -1,57 +1,63 @@ +import type { IEmoji } from '@rocket.chat/core-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Meteor } from 'meteor/meteor'; -import { Session } from 'meteor/session'; -import { emoji, updateRecent } from '../../../emoji/client'; -import { CachedCollectionManager } from '../../../ui-cached-collection/client'; +import { onLoggedIn } from '../../../../client/lib/loggedIn'; +import { emoji, removeFromRecent, replaceEmojiInRecent } from '../../../emoji/client'; import { getURL } from '../../../utils/client'; import { sdk } from '../../../utils/client/lib/SDKClient'; -import { isSetNotNull } from './function-isSet'; -export const getEmojiUrlFromName = function (name, extension) { - if (name == null) { - return; +const isSetNotNull = (fn: () => unknown) => { + let value; + try { + value = fn(); + } catch (e) { + value = null; } + return value !== null && value !== undefined; +}; - const key = `emoji_random_${name}`; - - const random = isSetNotNull(() => Session.keys[key]) ? Session.keys[key] : 0; +const getEmojiUrlFromName = (name: string, extension: string, etag?: string) => { + if (!name) { + return; + } - return getURL(`/emoji-custom/${encodeURIComponent(name)}.${extension}?_dc=${random}`); + return getURL(`/emoji-custom/${encodeURIComponent(name)}.${extension}${etag ? `?etag=${etag}` : ''}`); }; -export const deleteEmojiCustom = function (emojiData) { +export const deleteEmojiCustom = (emojiData: IEmoji) => { delete emoji.list[`:${emojiData.name}:`]; const arrayIndex = emoji.packages.emojiCustom.emojisByCategory.rocket.indexOf(emojiData.name); if (arrayIndex !== -1) { emoji.packages.emojiCustom.emojisByCategory.rocket.splice(arrayIndex, 1); } - const arrayIndexList = emoji.packages.emojiCustom.list.indexOf(`:${emojiData.name}:`); + const arrayIndexList = emoji.packages.emojiCustom.list?.indexOf(`:${emojiData.name}:`) ?? -1; if (arrayIndexList !== -1) { - emoji.packages.emojiCustom.list.splice(arrayIndexList, 1); + emoji.packages.emojiCustom.list?.splice(arrayIndexList, 1); } - if (isSetNotNull(() => emojiData.aliases)) { + if (emojiData.aliases) { for (const alias of emojiData.aliases) { delete emoji.list[`:${alias}:`]; - const aliasIndex = emoji.packages.emojiCustom.list.indexOf(`:${alias}:`); + const aliasIndex = emoji.packages.emojiCustom.list?.indexOf(`:${alias}:`) ?? -1; if (aliasIndex !== -1) { - emoji.packages.emojiCustom.list.splice(aliasIndex, 1); + emoji.packages.emojiCustom.list?.splice(aliasIndex, 1); } } } - updateRecent('rocket'); + + removeFromRecent(emojiData.name, emoji.packages.base.emojisByCategory.recent); }; -export const updateEmojiCustom = function (emojiData) { +export const updateEmojiCustom = (emojiData: IEmoji) => { const previousExists = isSetNotNull(() => emojiData.previousName); const currentAliases = isSetNotNull(() => emojiData.aliases); if (previousExists && isSetNotNull(() => emoji.list[`:${emojiData.previousName}:`].aliases)) { - for (const alias of emoji.list[`:${emojiData.previousName}:`].aliases) { + for (const alias of emoji.list[`:${emojiData.previousName}:`].aliases ?? []) { delete emoji.list[`:${alias}:`]; - const aliasIndex = emoji.packages.emojiCustom.list.indexOf(`:${alias}:`); + const aliasIndex = emoji.packages.emojiCustom.list?.indexOf(`:${alias}:`) ?? -1; if (aliasIndex !== -1) { - emoji.packages.emojiCustom.list.splice(aliasIndex, 1); + emoji.packages.emojiCustom.list?.splice(aliasIndex, 1); } } } @@ -61,9 +67,9 @@ export const updateEmojiCustom = function (emojiData) { if (arrayIndex !== -1) { emoji.packages.emojiCustom.emojisByCategory.rocket.splice(arrayIndex, 1); } - const arrayIndexList = emoji.packages.emojiCustom.list.indexOf(`:${emojiData.previousName}:`); + const arrayIndexList = emoji.packages.emojiCustom.list?.indexOf(`:${emojiData.previousName}:`) ?? -1; if (arrayIndexList !== -1) { - emoji.packages.emojiCustom.list.splice(arrayIndexList, 1); + emoji.packages.emojiCustom.list?.splice(arrayIndexList, 1); } delete emoji.list[`:${emojiData.previousName}:`]; } @@ -71,23 +77,26 @@ export const updateEmojiCustom = function (emojiData) { const categoryIndex = emoji.packages.emojiCustom.emojisByCategory.rocket.indexOf(`${emojiData.name}`); if (categoryIndex === -1) { emoji.packages.emojiCustom.emojisByCategory.rocket.push(`${emojiData.name}`); - emoji.packages.emojiCustom.list.push(`:${emojiData.name}:`); + emoji.packages.emojiCustom.list?.push(`:${emojiData.name}:`); } emoji.list[`:${emojiData.name}:`] = Object.assign({ emojiPackage: 'emojiCustom' }, emoji.list[`:${emojiData.name}:`], emojiData); if (currentAliases) { for (const alias of emojiData.aliases) { - emoji.packages.emojiCustom.list.push(`:${alias}:`); - emoji.list[`:${alias}:`] = {}; - emoji.list[`:${alias}:`].emojiPackage = 'emojiCustom'; - emoji.list[`:${alias}:`].aliasOf = emojiData.name; + emoji.packages.emojiCustom.list?.push(`:${alias}:`); + emoji.list[`:${alias}:`] = { + emojiPackage: 'emojiCustom', + aliasOf: emojiData.name, + }; } } - updateRecent('rocket'); + if (previousExists) { + replaceEmojiInRecent({ oldEmoji: emojiData.previousName, newEmoji: emojiData.name }); + } }; -const customRender = (html) => { - const emojisMatchGroup = emoji.packages.emojiCustom.list.map(escapeRegExp).join('|'); +const customRender = (html: string) => { + const emojisMatchGroup = emoji.packages.emojiCustom.list?.map(escapeRegExp).join('|'); if (emojisMatchGroup !== emoji.packages.emojiCustom._regexpSignature) { emoji.packages.emojiCustom._regexpSignature = emojisMatchGroup; emoji.packages.emojiCustom._regexp = new RegExp( @@ -96,22 +105,23 @@ const customRender = (html) => { ); } - html = html.replace(emoji.packages.emojiCustom._regexp, (shortname) => { - if (typeof shortname === 'undefined' || shortname === '' || emoji.packages.emojiCustom.list.indexOf(shortname) === -1) { + html = html.replace(emoji.packages.emojiCustom._regexp!, (shortname) => { + if (typeof shortname === 'undefined' || shortname === '' || (emoji.packages.emojiCustom.list?.indexOf(shortname) ?? -1) === -1) { return shortname; } let emojiAlias = shortname.replace(/:/g, ''); let dataCheck = emoji.list[shortname]; - if (dataCheck.hasOwnProperty('aliasOf')) { + if (dataCheck.aliasOf) { emojiAlias = dataCheck.aliasOf; dataCheck = emoji.list[`:${emojiAlias}:`]; } return `${shortname}`; }); @@ -125,26 +135,25 @@ emoji.packages.emojiCustom = { list: [], _regexpSignature: null, _regexp: null, - + emojisByCategory: {}, render: customRender, renderPicker: customRender, }; -Meteor.startup(() => - CachedCollectionManager.onLogin(async () => { +Meteor.startup(() => { + onLoggedIn(async () => { try { const { emojis: { update: emojis }, - } = await sdk.rest.get('/v1/emoji-custom.list'); + } = await sdk.rest.get('/v1/emoji-custom.list', { query: '' }); emoji.packages.emojiCustom.emojisByCategory = { rocket: [] }; for (const currentEmoji of emojis) { emoji.packages.emojiCustom.emojisByCategory.rocket.push(currentEmoji.name); - emoji.packages.emojiCustom.list.push(`:${currentEmoji.name}:`); - emoji.list[`:${currentEmoji.name}:`] = currentEmoji; - emoji.list[`:${currentEmoji.name}:`].emojiPackage = 'emojiCustom'; + emoji.packages.emojiCustom.list?.push(`:${currentEmoji.name}:`); + emoji.list[`:${currentEmoji.name}:`] = { ...currentEmoji, emojiPackage: 'emojiCustom' } as any; for (const alias of currentEmoji.aliases) { - emoji.packages.emojiCustom.list.push(`:${alias}:`); + emoji.packages.emojiCustom.list?.push(`:${alias}:`); emoji.list[`:${alias}:`] = { emojiPackage: 'emojiCustom', aliasOf: currentEmoji.name, @@ -154,5 +163,5 @@ Meteor.startup(() => } catch (e) { console.error('Error getting custom emoji', e); } - }), -); + }); +}); diff --git a/apps/meteor/app/emoji-custom/client/lib/function-isSet.js b/apps/meteor/app/emoji-custom/client/lib/function-isSet.js deleted file mode 100644 index 0ccf1abe02ab..000000000000 --- a/apps/meteor/app/emoji-custom/client/lib/function-isSet.js +++ /dev/null @@ -1,9 +0,0 @@ -export const isSetNotNull = function (fn) { - let value; - try { - value = fn(); - } catch (e) { - value = null; - } - return value !== null && value !== undefined; -}; diff --git a/apps/meteor/app/emoji-custom/server/lib/insertOrUpdateEmoji.ts b/apps/meteor/app/emoji-custom/server/lib/insertOrUpdateEmoji.ts index 7e838baee9b0..85a9648cf6d9 100644 --- a/apps/meteor/app/emoji-custom/server/lib/insertOrUpdateEmoji.ts +++ b/apps/meteor/app/emoji-custom/server/lib/insertOrUpdateEmoji.ts @@ -2,16 +2,15 @@ import { api } from '@rocket.chat/core-services'; import { EmojiCustom } from '@rocket.chat/models'; import limax from 'limax'; import { Meteor } from 'meteor/meteor'; -import _ from 'underscore'; import { trim } from '../../../../lib/utils/stringUtils'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { RocketChatFileEmojiCustomInstance } from '../startup/emoji-custom'; -type EmojiData = { +export type EmojiData = { _id?: string; name: string; - aliases: string; + aliases?: string; extension: string; previousName?: string; previousExtension?: string; @@ -33,7 +32,6 @@ export async function insertOrUpdateEmoji(userId: string | null, emojiData: Emoj } emojiData.name = limax(emojiData.name, { replacement: '_' }); - emojiData.aliases = limax(emojiData.aliases, { replacement: '_' }); // allow all characters except colon, whitespace, comma, >, <, &, ", ', /, \, (, ) // more practical than allowing specific sets of characters; also allows foreign languages @@ -42,7 +40,7 @@ export async function insertOrUpdateEmoji(userId: string | null, emojiData: Emoj // silently strip colon; this allows for uploading :emojiname: as emojiname emojiData.name = emojiData.name.replace(/:/g, ''); - emojiData.aliases = emojiData.aliases.replace(/:/g, ''); + emojiData.aliases = emojiData.aliases?.replace(/:/g, ''); if (nameValidation.test(emojiData.name)) { throw new Meteor.Error('error-input-is-not-a-valid-field', `${emojiData.name} is not a valid name`, { @@ -61,7 +59,11 @@ export async function insertOrUpdateEmoji(userId: string | null, emojiData: Emoj field: 'Alias_Set', }); } - aliases = _.without(emojiData.aliases.split(/[\s,]/).filter(Boolean), emojiData.name); + aliases = emojiData.aliases + .split(/\s*,\s*/) + .filter(Boolean) + .map((alias) => limax(alias, { replacement: '_' })) + .filter((alias) => alias !== emojiData.name); } emojiData.extension = emojiData.extension === 'svg+xml' ? 'png' : emojiData.extension; @@ -119,7 +121,8 @@ export async function insertOrUpdateEmoji(userId: string | null, emojiData: Emoj const rs = await RocketChatFileEmojiCustomInstance.getFileWithReadStream( encodeURIComponent(`${emojiData.previousName}.${emojiData.previousExtension}`), ); - if (rs !== null) { + + if (rs) { await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.name}.${emojiData.extension}`)); const ws = RocketChatFileEmojiCustomInstance.createWriteStream( encodeURIComponent(`${emojiData.name}.${emojiData.previousExtension}`), diff --git a/apps/meteor/app/emoji-custom/server/lib/uploadEmojiCustom.ts b/apps/meteor/app/emoji-custom/server/lib/uploadEmojiCustom.ts index 07633eaa1a7d..cfa887c4a441 100644 --- a/apps/meteor/app/emoji-custom/server/lib/uploadEmojiCustom.ts +++ b/apps/meteor/app/emoji-custom/server/lib/uploadEmojiCustom.ts @@ -1,8 +1,11 @@ import { api, Media } from '@rocket.chat/core-services'; +import { EmojiCustom } from '@rocket.chat/models'; +import { Random } from '@rocket.chat/random'; import limax from 'limax'; import { Meteor } from 'meteor/meteor'; import sharp from 'sharp'; +import type { EmojiData } from './insertOrUpdateEmoji'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { RocketChatFile } from '../../../file/server'; import { RocketChatFileEmojiCustomInstance } from '../startup/emoji-custom'; @@ -15,17 +18,14 @@ const getFile = async (file: Buffer, extension: string) => { return sharp(file).png().toBuffer(); }; -type EmojiData = { - _id?: string; - name: string; - aliases?: string | string[]; - extension: string; - previousName?: string; - previousExtension?: string; - newFile?: boolean; -}; +export type EmojiDataWithAliases = Omit & { aliases?: string | string[] }; -export async function uploadEmojiCustom(userId: string | null, binaryContent: string, contentType: string, emojiData: EmojiData) { +export async function uploadEmojiCustom( + userId: string | null, + binaryContent: string, + contentType: string, + emojiData: EmojiDataWithAliases, +) { return uploadEmojiCustomWithBuffer(userId, Buffer.from(binaryContent, 'binary'), contentType, emojiData); } @@ -33,7 +33,7 @@ export async function uploadEmojiCustomWithBuffer( userId: string | null, buffer: Buffer, contentType: string, - emojiData: EmojiData, + emojiData: EmojiDataWithAliases, ): Promise { // technically, since this method doesnt have any datatype validations, users can // upload videos as emojis. The FE won't play them, but they will waste space for sure. @@ -41,9 +41,12 @@ export async function uploadEmojiCustomWithBuffer( throw new Meteor.Error('not_authorized'); } + if (!Array.isArray(emojiData.aliases)) { + // delete aliases for notification purposes. here, it is a string or undefined rather than an array + delete emojiData.aliases; + } + emojiData.name = limax(emojiData.name, { replacement: '_' }); - // delete aliases for notification purposes. here, it is a string rather than an array - delete emojiData.aliases; const file = await getFile(buffer, emojiData.extension); emojiData.extension = emojiData.extension === 'svg+xml' ? 'png' : emojiData.extension; @@ -67,8 +70,10 @@ export async function uploadEmojiCustomWithBuffer( encodeURIComponent(`${emojiData.name}.${emojiData.extension}`), contentType, ); - ws.on('end', () => { - setTimeout(() => api.broadcast('emoji.updateCustom', emojiData), 500); + ws.on('end', async () => { + const etag = Random.hexString(6); + await EmojiCustom.setETagByName(emojiData.name, etag); + setTimeout(() => api.broadcast('emoji.updateCustom', { ...emojiData, etag }), 500); resolve(); }); diff --git a/apps/meteor/app/emoji/client/emojiParser.js b/apps/meteor/app/emoji/client/emojiParser.ts similarity index 74% rename from apps/meteor/app/emoji/client/emojiParser.js rename to apps/meteor/app/emoji/client/emojiParser.ts index 0b3b722aaebd..ff4a010bed5c 100644 --- a/apps/meteor/app/emoji/client/emojiParser.js +++ b/apps/meteor/app/emoji/client/emojiParser.ts @@ -1,12 +1,10 @@ -import { isIE11 } from '../../../client/lib/utils/isIE11'; import { emoji } from './lib'; +import { isIE11 } from '../../../client/lib/utils/isIE11'; /** * emojiParser is a function that will replace emojis - * @param {{ html: string }} message - The message object - * @return {{ html: string }} */ -export const emojiParser = ({ html }) => { +export const emojiParser = (html: string) => { html = html.trim(); // ' to apostrophe (') for emojis such as :') @@ -28,8 +26,12 @@ export const emojiParser = ({ html }) => { let hasText = false; if (!isIE11) { - const filter = (node) => { - if (node.nodeType === Node.ELEMENT_NODE && (node.classList.contains('emojione') || node.classList.contains('emoji'))) { + const isElement = (node: Node): node is Element => node.nodeType === Node.ELEMENT_NODE; + + const isTextNode = (node: Node): node is Text => node.nodeType === Node.TEXT_NODE; + + const filter = (node: Node) => { + if (isElement(node) && (node.classList.contains('emojione') || node.classList.contains('emoji'))) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; @@ -38,7 +40,7 @@ export const emojiParser = ({ html }) => { const walker = document.createTreeWalker(checkEmojiOnly, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, filter); while (walker.nextNode()) { - if (walker.currentNode.nodeType === Node.TEXT_NODE && walker.currentNode.nodeValue.trim() !== '') { + if (isTextNode(walker.currentNode) && walker.currentNode.nodeValue.trim() !== '') { hasText = true; break; } @@ -60,5 +62,5 @@ export const emojiParser = ({ html }) => { // line breaks '
' back to '
' html = html.replace(/
/g, '
'); - return { html }; + return html; }; diff --git a/apps/meteor/app/emoji/client/helpers.ts b/apps/meteor/app/emoji/client/helpers.ts index 35badda26a73..a203216640f5 100644 --- a/apps/meteor/app/emoji/client/helpers.ts +++ b/apps/meteor/app/emoji/client/helpers.ts @@ -138,7 +138,7 @@ export const getEmojisBySearchTerm = ( return emojis; }; -export const removeFromRecent = (emoji: string, recentEmojis: string[], setRecentEmojis: (emojis: string[]) => void) => { +export const removeFromRecent = (emoji: string, recentEmojis: string[], setRecentEmojis?: (emojis: string[]) => void) => { const _emoji = emoji.replace(/(^:|:$)/g, ''); const pos = recentEmojis.indexOf(_emoji as never); @@ -146,7 +146,7 @@ export const removeFromRecent = (emoji: string, recentEmojis: string[], setRecen return; } recentEmojis.splice(pos, 1); - setRecentEmojis(recentEmojis); + setRecentEmojis?.(recentEmojis); }; export const updateRecent = (recentList: string[]) => { @@ -156,6 +156,15 @@ export const updateRecent = (recentList: string[]) => { }); }; +export const replaceEmojiInRecent = ({ oldEmoji, newEmoji }: { oldEmoji: string; newEmoji: string }) => { + const recentPkgList: string[] = emoji.packages.base.emojisByCategory.recent; + const pos = recentPkgList.indexOf(oldEmoji); + + if (pos !== -1) { + recentPkgList[pos] = newEmoji; + } +}; + const getEmojiRender = (emojiName: string) => { const emojiPackageName = emoji.list[emojiName]?.emojiPackage; const emojiPackage = emoji.packages[emojiPackageName]; diff --git a/apps/meteor/app/emoji/lib/rocketchat.ts b/apps/meteor/app/emoji/lib/rocketchat.ts index 49d6ffbe41aa..1c5b515407ed 100644 --- a/apps/meteor/app/emoji/lib/rocketchat.ts +++ b/apps/meteor/app/emoji/lib/rocketchat.ts @@ -9,6 +9,9 @@ export type EmojiPackage = { renderPicker: (emojiToRender: string) => string | undefined; ascii?: boolean; sprites?: unknown; + list?: string[]; + _regexpSignature?: string | null; + _regexp?: RegExp | null; }; export type EmojiPackages = { @@ -16,14 +19,27 @@ export type EmojiPackages = { [key: string]: EmojiPackage; }; list: { - [key: keyof NonNullable]: { - category: string; - emojiPackage: string; - shortnames: string[]; - uc_base: string; - uc_greedy: string; - uc_match: string; - uc_output: string; - }; + [key: keyof NonNullable]: + | { + category: string; + emojiPackage: string; + shortnames: string[]; + uc_base: string; + uc_greedy: string; + uc_match: string; + uc_output: string; + aliases?: string[]; + aliasOf?: undefined; + extension?: string; + etag?: string; + } + | { + emojiPackage: string; + aliasOf: string; + extension?: undefined; + aliases?: undefined; + shortnames?: undefined; + etag?: string; + }; }; }; diff --git a/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts b/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts index 984561fe13cd..24224a599abf 100644 --- a/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts +++ b/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts @@ -110,7 +110,7 @@ process.on('unhandledRejection', (error) => { console.error('Future node.js versions will automatically exit the process'); console.error('================================='); - if (process.env.NODE_ENV === 'development' || process.env.EXIT_UNHANDLEDPROMISEREJECTION) { + if (process.env.TEST_MODE || process.env.NODE_ENV === 'development' || process.env.EXIT_UNHANDLEDPROMISEREJECTION) { process.exit(1); } }); @@ -125,4 +125,8 @@ process.on('uncaughtException', async (error) => { console.error('==========================='); void errorHandler.trackError(error.message, error.stack); + + if (process.env.TEST_MODE || process.env.NODE_ENV === 'development' || process.env.EXIT_UNHANDLEDPROMISEREJECTION) { + process.exit(1); + } }); diff --git a/apps/meteor/app/federation/server/endpoints/dispatch.js b/apps/meteor/app/federation/server/endpoints/dispatch.js index 4f2a197b25ee..d80f74bf18d3 100644 --- a/apps/meteor/app/federation/server/endpoints/dispatch.js +++ b/apps/meteor/app/federation/server/endpoints/dispatch.js @@ -6,6 +6,7 @@ import EJSON from 'ejson'; import { API } from '../../../api/server'; import { FileUpload } from '../../../file-upload/server'; import { deleteRoom } from '../../../lib/server/functions/deleteRoom'; +import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { notifyOnMessageChange, notifyOnRoomChanged, @@ -551,6 +552,18 @@ API.v1.addRoute( { authRequired: false, rateLimiterOptions: { numRequestsAllowed: 30, intervalTimeInMS: 1000 } }, { async post() { + /* + The legacy federation has been deprecated for over a year + and no longer receives any updates. This feature also has + relevant security issues that weren't addressed. + Workspaces should migrate to the newer matrix federation. + */ + apiDeprecationLogger.endpoint(this.request.route, '8.0.0', this.response, 'Use Matrix Federation instead.'); + + if (!process.env.ENABLE_INSECURE_LEGACY_FEDERATION) { + return API.v1.failure('Deprecated. ENABLE_INSECURE_LEGACY_FEDERATION environment variable is needed to enable it.'); + } + if (!isFederationEnabled()) { return API.v1.failure('Federation not enabled'); } diff --git a/apps/meteor/app/federation/server/endpoints/requestFromLatest.js b/apps/meteor/app/federation/server/endpoints/requestFromLatest.js index b45e8944e466..8dca41d3f442 100644 --- a/apps/meteor/app/federation/server/endpoints/requestFromLatest.js +++ b/apps/meteor/app/federation/server/endpoints/requestFromLatest.js @@ -2,6 +2,7 @@ import { FederationRoomEvents } from '@rocket.chat/models'; import EJSON from 'ejson'; import { API } from '../../../api/server'; +import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { dispatchEvents } from '../handler'; import { decryptIfNeeded } from '../lib/crypt'; import { isFederationEnabled } from '../lib/isFederationEnabled'; @@ -12,6 +13,18 @@ API.v1.addRoute( { authRequired: false }, { async post() { + /* + The legacy federation has been deprecated for over a year + and no longer receives any updates. This feature also has + relevant security issues that weren't addressed. + Workspaces should migrate to the newer matrix federation. + */ + apiDeprecationLogger.endpoint(this.request.route, '8.0.0', this.response, 'Use Matrix Federation instead.'); + + if (!process.env.ENABLE_INSECURE_LEGACY_FEDERATION) { + return API.v1.failure('Deprecated. ENABLE_INSECURE_LEGACY_FEDERATION environment variable is needed to enable it.'); + } + if (!isFederationEnabled()) { return API.v1.failure('Federation not enabled'); } diff --git a/apps/meteor/app/federation/server/endpoints/uploads.js b/apps/meteor/app/federation/server/endpoints/uploads.js index da5740f95f16..10c513b3741c 100644 --- a/apps/meteor/app/federation/server/endpoints/uploads.js +++ b/apps/meteor/app/federation/server/endpoints/uploads.js @@ -2,6 +2,7 @@ import { Uploads } from '@rocket.chat/models'; import { API } from '../../../api/server'; import { FileUpload } from '../../../file-upload/server'; +import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { isFederationEnabled } from '../lib/isFederationEnabled'; API.v1.addRoute( @@ -9,6 +10,18 @@ API.v1.addRoute( { authRequired: false }, { async get() { + /* + The legacy federation has been deprecated for over a year + and no longer receives any updates. This feature also has + relevant security issues that weren't addressed. + Workspaces should migrate to the newer matrix federation. + */ + apiDeprecationLogger.endpoint(this.request.route, '8.0.0', this.response, 'Use Matrix Federation instead.'); + + if (!process.env.ENABLE_INSECURE_LEGACY_FEDERATION) { + return API.v1.failure('Deprecated. ENABLE_INSECURE_LEGACY_FEDERATION environment variable is needed to enable it.'); + } + if (!isFederationEnabled()) { return API.v1.failure('Federation not enabled'); } diff --git a/apps/meteor/app/federation/server/endpoints/users.js b/apps/meteor/app/federation/server/endpoints/users.js index 0d9a4ff4b0ce..fba9b1642e8f 100644 --- a/apps/meteor/app/federation/server/endpoints/users.js +++ b/apps/meteor/app/federation/server/endpoints/users.js @@ -1,6 +1,7 @@ import { Users } from '@rocket.chat/models'; import { API } from '../../../api/server'; +import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { isFederationEnabled } from '../lib/isFederationEnabled'; import { serverLogger } from '../lib/logger'; import { normalizers } from '../normalizers'; @@ -12,6 +13,18 @@ API.v1.addRoute( { authRequired: false }, { async get() { + /* + The legacy federation has been deprecated for over a year + and no longer receives any updates. This feature also has + relevant security issues that weren't addressed. + Workspaces should migrate to the newer matrix federation. + */ + apiDeprecationLogger.endpoint(this.request.route, '8.0.0', this.response, 'Use Matrix Federation instead.'); + + if (!process.env.ENABLE_INSECURE_LEGACY_FEDERATION) { + return API.v1.failure('Deprecated. ENABLE_INSECURE_LEGACY_FEDERATION environment variable is needed to enable it.'); + } + if (!isFederationEnabled()) { return API.v1.failure('Federation not enabled'); } @@ -39,6 +52,18 @@ API.v1.addRoute( { authRequired: false }, { async get() { + /* + The legacy federation has been deprecated for over a year + and no longer receives any updates. This feature also has + relevant security issues that weren't addressed. + Workspaces should migrate to the newer matrix federation. + */ + apiDeprecationLogger.endpoint(this.request.route, '8.0.0', this.response, 'Use Matrix Federation instead.'); + + if (!process.env.ENABLE_INSECURE_LEGACY_FEDERATION) { + return API.v1.failure('Deprecated. ENABLE_INSECURE_LEGACY_FEDERATION environment variable is needed to enable it.'); + } + if (!isFederationEnabled()) { return API.v1.failure('Federation not enabled'); } diff --git a/apps/meteor/app/federation/server/functions/helpers.ts b/apps/meteor/app/federation/server/functions/helpers.ts index c684b7b8f74a..54df33cdfa2d 100644 --- a/apps/meteor/app/federation/server/functions/helpers.ts +++ b/apps/meteor/app/federation/server/functions/helpers.ts @@ -57,10 +57,13 @@ export const getFederatedRoomData = async ( // Find all subscriptions of this room const s = await Subscriptions.findByRoomIdWhenUsernameExists(room._id).toArray(); - const subscriptions = s.reduce((acc, s) => { - acc[s.u._id] = s; - return acc; - }, {} as { [k: string]: ISubscription }); + const subscriptions = s.reduce( + (acc, s) => { + acc[s.u._id] = s; + return acc; + }, + {} as { [k: string]: ISubscription }, + ); // Get all user ids const userIds = Object.keys(subscriptions); diff --git a/apps/meteor/app/federation/server/handler/index.js b/apps/meteor/app/federation/server/handler/index.ts similarity index 80% rename from apps/meteor/app/federation/server/handler/index.js rename to apps/meteor/app/federation/server/handler/index.ts index c5b19856f19f..f7a3ae53ec29 100644 --- a/apps/meteor/app/federation/server/handler/index.js +++ b/apps/meteor/app/federation/server/handler/index.ts @@ -5,7 +5,7 @@ import { federationRequestToPeer } from '../lib/http'; import { isFederationEnabled } from '../lib/isFederationEnabled'; import { clientLogger } from '../lib/logger'; -export async function federationSearchUsers(query) { +export async function federationSearchUsers(query: string) { if (!isFederationEnabled()) { throw disabled('client.searchUsers'); } @@ -23,7 +23,7 @@ export async function federationSearchUsers(query) { return users; } -export async function getUserByUsername(query) { +export async function getUserByUsername(query: string) { if (!isFederationEnabled()) { throw disabled('client.searchUsers'); } @@ -41,7 +41,13 @@ export async function getUserByUsername(query) { return user; } -export async function requestEventsFromLatest(domain, fromDomain, contextType, contextQuery, latestEventIds) { +export async function requestEventsFromLatest( + domain: string, + fromDomain: string, + contextType: unknown, + contextQuery: unknown, + latestEventIds: unknown, +) { if (!isFederationEnabled()) { throw disabled('client.requestEventsFromLatest'); } @@ -64,7 +70,7 @@ export async function requestEventsFromLatest(domain, fromDomain, contextType, c }); } -export async function dispatchEvents(domains, events) { +export async function dispatchEvents(domains: string[], events: unknown[]) { if (!isFederationEnabled()) { throw disabled('client.dispatchEvents'); } @@ -80,11 +86,11 @@ export async function dispatchEvents(domains, events) { } } -export async function dispatchEvent(domains, event) { +export async function dispatchEvent(domains: string[], event: unknown) { await dispatchEvents([...new Set(domains)], [event]); } -export async function getUpload(domain, fileId) { +export async function getUpload(domain: string, fileId: string) { const { data: { upload, buffer }, } = await federationRequestToPeer('GET', domain, `/api/v1/federation.uploads?${qs.stringify({ upload_id: fileId })}`); diff --git a/apps/meteor/app/federation/server/lib/dns.js b/apps/meteor/app/federation/server/lib/dns.js index f8ef27e1dcf6..83ce0cc70349 100644 --- a/apps/meteor/app/federation/server/lib/dns.js +++ b/apps/meteor/app/federation/server/lib/dns.js @@ -3,10 +3,10 @@ import util from 'util'; import mem from 'mem'; -import * as federationErrors from '../functions/errors'; import { federationRequest } from './http'; import { isFederationEnabled } from './isFederationEnabled'; import { dnsLogger } from './logger'; +import * as federationErrors from '../functions/errors'; const dnsResolveSRV = util.promisify(dnsResolver.resolveSrv); const dnsResolveTXT = util.promisify(dnsResolver.resolveTxt); diff --git a/apps/meteor/app/file-upload/server/config/AmazonS3.ts b/apps/meteor/app/file-upload/server/config/AmazonS3.ts index 0f551d3b90d1..48b7a9b850a9 100644 --- a/apps/meteor/app/file-upload/server/config/AmazonS3.ts +++ b/apps/meteor/app/file-upload/server/config/AmazonS3.ts @@ -3,11 +3,11 @@ import https from 'https'; import _ from 'underscore'; +import { forceDownload } from './helper'; import { settings } from '../../../settings/server'; import type { S3Options } from '../../ufs/AmazonS3/server'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; import '../../ufs/AmazonS3/server'; -import { forceDownload } from './helper'; const get: FileUploadClass['get'] = async function (this: FileUploadClass, file, req, res) { const forcedDownload = forceDownload(req); diff --git a/apps/meteor/app/file-upload/server/config/FileSystem.ts b/apps/meteor/app/file-upload/server/config/FileSystem.ts index 75fdb5afc8ae..c39cf5807d60 100644 --- a/apps/meteor/app/file-upload/server/config/FileSystem.ts +++ b/apps/meteor/app/file-upload/server/config/FileSystem.ts @@ -1,10 +1,10 @@ import fsp from 'fs/promises'; +import { getContentDisposition } from './helper'; import { UploadFS } from '../../../../server/ufs'; import { settings } from '../../../settings/server'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; import { getFileRange, setRangeHeaders } from '../lib/ranges'; -import { getContentDisposition } from './helper'; const FileSystemUploads = new FileUploadClass({ name: 'FileSystem:Uploads', diff --git a/apps/meteor/app/file-upload/server/config/GoogleStorage.ts b/apps/meteor/app/file-upload/server/config/GoogleStorage.ts index 8fb901b5a123..ccd94d533b62 100644 --- a/apps/meteor/app/file-upload/server/config/GoogleStorage.ts +++ b/apps/meteor/app/file-upload/server/config/GoogleStorage.ts @@ -3,10 +3,10 @@ import https from 'https'; import _ from 'underscore'; +import { forceDownload } from './helper'; import { settings } from '../../../settings/server'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; import '../../ufs/GoogleStorage/server'; -import { forceDownload } from './helper'; const get: FileUploadClass['get'] = async function (this: FileUploadClass, file, req, res) { const forcedDownload = forceDownload(req); diff --git a/apps/meteor/app/file-upload/server/config/GridFS.ts b/apps/meteor/app/file-upload/server/config/GridFS.ts index 3bb5f806f3a7..551569d4c58c 100644 --- a/apps/meteor/app/file-upload/server/config/GridFS.ts +++ b/apps/meteor/app/file-upload/server/config/GridFS.ts @@ -6,10 +6,10 @@ import zlib from 'zlib'; import type { IUpload } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; +import { getContentDisposition } from './helper'; import { UploadFS } from '../../../../server/ufs'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; import { getFileRange, setRangeHeaders } from '../lib/ranges'; -import { getContentDisposition } from './helper'; const logger = new Logger('FileUpload'); diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.ts b/apps/meteor/app/file-upload/server/lib/FileUpload.ts index ac97923be41e..cffdfe6288f2 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.ts +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.ts @@ -22,6 +22,7 @@ import sharp from 'sharp'; import type { WritableStreamBuffer } from 'stream-buffers'; import streamBuffers from 'stream-buffers'; +import { streamToBuffer } from './streamToBuffer'; import { i18n } from '../../../../server/lib/i18n'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; @@ -33,7 +34,6 @@ import { settings } from '../../../settings/server'; import { mime } from '../../../utils/lib/mimeTypes'; import { isValidJWT, generateJWT } from '../../../utils/server/lib/JWTHelper'; import { fileUploadIsValidContentType } from '../../../utils/server/restrictions'; -import { streamToBuffer } from './streamToBuffer'; const cookie = new Cookies(); let maxFileSize = 0; @@ -384,7 +384,7 @@ export const FileUpload = { ? { width, height, - } + } : undefined, }; diff --git a/apps/meteor/app/github-enterprise/client/github-enterprise-login-button.css b/apps/meteor/app/github-enterprise/client/github-enterprise-login-button.css deleted file mode 100644 index e7ba61d7be38..000000000000 --- a/apps/meteor/app/github-enterprise/client/github-enterprise-login-button.css +++ /dev/null @@ -1,9 +0,0 @@ -.icon-github_enterprise::before { - height: 1em; - - content: ""; - - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wJGBYxHYxl31wAAAHpSURBVDjLpZI/aFNRFMZ/973bJqGRPopV4qNq/+SpTYnWRhCKilShg9BGcHOM+GfQoZuLk4iLgw4qZNBaHLuIdBNHl7Ta1qdNFI3SihnaNG1MpH3vuiQYQnwZvHCG893zffc751z4z6PX5T5gA1DAKnAaOAQEgAfAVeCpl+CeCrlRuEC6maO4h0A1wl4tPAHMqNUthvrDdHYY7A3t4rDVjeO6rBU2FaABM1WCrBNoi48Mi+nH9yj+KtPibAKwJXfQ5vcRG7soUnYmWEuQgAEIYBv4cGpoILI0Z4tyYYPegS6UguyijZQ6J45GSNmZHzUcJYD2ii2Ajv7efZ8WZ6ZwXFj79hXpayW4O0SL1Nl/8jzZlZ9dQLFS70pgvZKIyGD0yvu5eRmMnrk1PjI81ir1qBACTdPevXj95mVuNX8XKDQc/+T334bZZ104cvzYw2s3J3qAL5WXSsDbf61NNMBu+wOBs+VSyQ84Nfhg028ZGx3/qyy0lC7lgi7lghBitoon03lvB8l0/k7Wnk+8mny0cyXzEcfZxgwfZPTyRMHsOzAFXE9YhtNQIJnOx4FpJXT1eSkn2g0frqMoFrfoCXcqlCOAGwnLuO/l4JymcWl5uRxzXUKghBAiZ5r+WaV4lrCM555zqO+x2d0ftGmpiA/0k70AAAAASUVORK5CYII=); - background-repeat: no-repeat; - background-position: center center; -} diff --git a/apps/meteor/app/github-enterprise/client/index.ts b/apps/meteor/app/github-enterprise/client/index.ts index 7a032388cc0c..cf327e4971bb 100644 --- a/apps/meteor/app/github-enterprise/client/index.ts +++ b/apps/meteor/app/github-enterprise/client/index.ts @@ -1,2 +1 @@ import './lib'; -import './github-enterprise-login-button.css'; diff --git a/apps/meteor/app/gitlab/client/gitlab-login-button.css b/apps/meteor/app/gitlab/client/gitlab-login-button.css index c73a1ee23c91..fa9d41b34e74 100644 --- a/apps/meteor/app/gitlab/client/gitlab-login-button.css +++ b/apps/meteor/app/gitlab/client/gitlab-login-button.css @@ -1,3 +1,3 @@ #login-buttons-image-gitlab { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wJGBYxHYxl31wAAAHpSURBVDjLpZI/aFNRFMZ/973bJqGRPopV4qNq/+SpTYnWRhCKilShg9BGcHOM+GfQoZuLk4iLgw4qZNBaHLuIdBNHl7Ta1qdNFI3SihnaNG1MpH3vuiQYQnwZvHCG893zffc751z4z6PX5T5gA1DAKnAaOAQEgAfAVeCpl+CeCrlRuEC6maO4h0A1wl4tPAHMqNUthvrDdHYY7A3t4rDVjeO6rBU2FaABM1WCrBNoi48Mi+nH9yj+KtPibAKwJXfQ5vcRG7soUnYmWEuQgAEIYBv4cGpoILI0Z4tyYYPegS6UguyijZQ6J45GSNmZHzUcJYD2ii2Ajv7efZ8WZ6ZwXFj79hXpayW4O0SL1Nl/8jzZlZ9dQLFS70pgvZKIyGD0yvu5eRmMnrk1PjI81ir1qBACTdPevXj95mVuNX8XKDQc/+T334bZZ104cvzYw2s3J3qAL5WXSsDbf61NNMBu+wOBs+VSyQ84Nfhg028ZGx3/qyy0lC7lgi7lghBitoon03lvB8l0/k7Wnk+8mny0cyXzEcfZxgwfZPTyRMHsOzAFXE9YhtNQIJnOx4FpJXT1eSkn2g0frqMoFrfoCXcqlCOAGwnLuO/l4JymcWl5uRxzXUKghBAiZ5r+WaV4lrCM555zqO+x2d0ftGmpiA/0k70AAAAASUVORK5CYII=); + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wJGBYxHYxl31wAAAHpSURBVDjLpZI/aFNRFMZ/973bJqGRPopV4qNq/+SpTYnWRhCKilShg9BGcHOM+GfQoZuLk4iLgw4qZNBaHLuIdBNHl7Ta1qdNFI3SihnaNG1MpH3vuiQYQnwZvHCG893zffc751z4z6PX5T5gA1DAKnAaOAQEgAfAVeCpl+CeCrlRuEC6maO4h0A1wl4tPAHMqNUthvrDdHYY7A3t4rDVjeO6rBU2FaABM1WCrBNoi48Mi+nH9yj+KtPibAKwJXfQ5vcRG7soUnYmWEuQgAEIYBv4cGpoILI0Z4tyYYPegS6UguyijZQ6J45GSNmZHzUcJYD2ii2Ajv7efZ8WZ6ZwXFj79hXpayW4O0SL1Nl/8jzZlZ9dQLFS70pgvZKIyGD0yvu5eRmMnrk1PjI81ir1qBACTdPevXj95mVuNX8XKDQc/+T334bZZ104cvzYw2s3J3qAL5WXSsDbf61NNMBu+wOBs+VSyQ84Nfhg028ZGx3/qyy0lC7lgi7lghBitoon03lvB8l0/k7Wnk+8mny0cyXzEcfZxgwfZPTyRMHsOzAFXE9YhtNQIJnOx4FpJXT1eSkn2g0frqMoFrfoCXcqlCOAGwnLuO/l4JymcWl5uRxzXUKghBAiZ5r+WaV4lrCM555zqO+x2d0ftGmpiA/0k70AAAAASUVORK5CYII="); } diff --git a/apps/meteor/app/importer-csv/server/CsvImporter.ts b/apps/meteor/app/importer-csv/server/CsvImporter.ts index a9844e747640..88cd39a84758 100644 --- a/apps/meteor/app/importer-csv/server/CsvImporter.ts +++ b/apps/meteor/app/importer-csv/server/CsvImporter.ts @@ -7,10 +7,11 @@ import { Importer, ProgressStep, ImporterWebsocket } from '../../importer/server import type { ConverterOptions } from '../../importer/server/classes/ImportDataConverter'; import type { ImporterProgress } from '../../importer/server/classes/ImporterProgress'; import type { ImporterInfo } from '../../importer/server/definitions/ImporterInfo'; +import { addParsedContacts } from '../../importer-omnichannel-contacts/server/addParsedContacts'; import { notifyOnSettingChanged } from '../../lib/server/lib/notifyListener'; export class CsvImporter extends Importer { - private csvParser: (csv: string) => string[]; + private csvParser: (csv: string) => string[][]; constructor(info: ImporterInfo, importRecord: IImport, converterOptions: ConverterOptions = {}) { super(info, importRecord, converterOptions); @@ -46,6 +47,7 @@ export class CsvImporter extends Importer { let messagesCount = 0; let usersCount = 0; let channelsCount = 0; + let contactsCount = 0; const dmRooms = new Set(); const roomIds = new Map(); const usedUsernames = new Set(); @@ -140,6 +142,18 @@ export class CsvImporter extends Importer { continue; } + // Parse the contacts + if (entry.entryName.toLowerCase() === 'contacts.csv') { + await super.updateProgress(ProgressStep.PREPARING_CONTACTS); + const parsedContacts = this.csvParser(entry.getData().toString()); + + contactsCount = await addParsedContacts.call(this.converter, parsedContacts); + + await super.updateRecord({ 'count.contacts': contactsCount }); + increaseProgressCount(); + continue; + } + // Parse the messages if (entry.entryName.indexOf('/') > -1) { if (this.progress.step !== ProgressStep.PREPARING_MESSAGES) { @@ -258,12 +272,12 @@ export class CsvImporter extends Importer { } } - await super.addCountToTotal(messagesCount + usersCount + channelsCount); + await super.addCountToTotal(messagesCount + usersCount + channelsCount + contactsCount); ImporterWebsocket.progressUpdated({ rate: 100 }); - // Ensure we have at least a single user, channel, or message - if (usersCount === 0 && channelsCount === 0 && messagesCount === 0) { - this.logger.error('No users, channels, or messages found in the import file.'); + // Ensure we have at least a single record of any kind + if (usersCount === 0 && channelsCount === 0 && messagesCount === 0 && contactsCount === 0) { + this.logger.error('No valid record found in the import file.'); await super.updateProgress(ProgressStep.ERROR); } diff --git a/apps/meteor/app/importer-csv/server/index.ts b/apps/meteor/app/importer-csv/server/index.ts index 2d913f740955..f20c375ae943 100644 --- a/apps/meteor/app/importer-csv/server/index.ts +++ b/apps/meteor/app/importer-csv/server/index.ts @@ -1,5 +1,5 @@ -import { Importers } from '../../importer/server'; import { CsvImporter } from './CsvImporter'; +import { Importers } from '../../importer/server'; Importers.add({ key: 'csv', diff --git a/apps/meteor/app/importer-omnichannel-contacts/server/ContactImporter.ts b/apps/meteor/app/importer-omnichannel-contacts/server/ContactImporter.ts new file mode 100644 index 000000000000..ca5f66bc05ea --- /dev/null +++ b/apps/meteor/app/importer-omnichannel-contacts/server/ContactImporter.ts @@ -0,0 +1,48 @@ +import fs from 'node:fs'; + +import type { IImport } from '@rocket.chat/core-typings'; +import { parse } from 'csv-parse/lib/sync'; + +import { addParsedContacts } from './addParsedContacts'; +import { Importer, ProgressStep, ImporterWebsocket } from '../../importer/server'; +import type { ConverterOptions } from '../../importer/server/classes/ImportDataConverter'; +import type { ImporterProgress } from '../../importer/server/classes/ImporterProgress'; +import type { ImporterInfo } from '../../importer/server/definitions/ImporterInfo'; + +export class ContactImporter extends Importer { + private csvParser: (csv: string) => string[][]; + + constructor(info: ImporterInfo, importRecord: IImport, converterOptions: ConverterOptions = {}) { + super(info, importRecord, converterOptions); + + this.csvParser = parse; + } + + async prepareUsingLocalFile(fullFilePath: string): Promise { + this.logger.debug('start preparing import operation'); + await this.converter.clearImportData(); + + ImporterWebsocket.progressUpdated({ rate: 0 }); + + await super.updateProgress(ProgressStep.PREPARING_CONTACTS); + // Reading the whole file at once for compatibility with the code written for the other importers + // We can change this to a stream once we improve the rest of the importer classes + const fileContents = fs.readFileSync(fullFilePath, { encoding: 'utf8' }); + if (!fileContents || typeof fileContents !== 'string') { + throw new Error('Failed to load file contents.'); + } + + const parsedContacts = this.csvParser(fileContents); + const contactsCount = await addParsedContacts.call(this.converter, parsedContacts); + + if (contactsCount === 0) { + this.logger.error('No contacts found in the import file.'); + await super.updateProgress(ProgressStep.ERROR); + } else { + await super.updateRecord({ 'count.contacts': contactsCount, 'count.total': contactsCount }); + ImporterWebsocket.progressUpdated({ rate: 100 }); + } + + return super.getProgress(); + } +} diff --git a/apps/meteor/app/importer-omnichannel-contacts/server/addParsedContacts.ts b/apps/meteor/app/importer-omnichannel-contacts/server/addParsedContacts.ts new file mode 100644 index 000000000000..cc00e7ed1f9b --- /dev/null +++ b/apps/meteor/app/importer-omnichannel-contacts/server/addParsedContacts.ts @@ -0,0 +1,39 @@ +import { Random } from '@rocket.chat/random'; + +import type { ImportDataConverter } from '../../importer/server/classes/ImportDataConverter'; + +export async function addParsedContacts(this: ImportDataConverter, parsedContacts: string[][]): Promise { + const columnNames = parsedContacts.shift(); + let addedContacts = 0; + + for await (const parsedData of parsedContacts) { + const contactData = parsedData.reduce( + (acc, value, index) => { + const columnName = columnNames && index < columnNames.length ? columnNames[index] : `column${index}`; + return { + ...acc, + [columnName]: value, + }; + }, + {} as Record, + ); + + if (!contactData.emails && !contactData.phones && !contactData.name) { + continue; + } + + const { emails = '', phones = '', name = '', manager: contactManager = undefined, id = Random.id(), ...customFields } = contactData; + + await this.addContact({ + importIds: [id], + emails: emails.split(';'), + phones: phones.split(';'), + name, + contactManager, + customFields, + }); + addedContacts++; + } + + return addedContacts; +} diff --git a/apps/meteor/app/importer-omnichannel-contacts/server/index.ts b/apps/meteor/app/importer-omnichannel-contacts/server/index.ts new file mode 100644 index 000000000000..0ba882650bf0 --- /dev/null +++ b/apps/meteor/app/importer-omnichannel-contacts/server/index.ts @@ -0,0 +1,12 @@ +import { License } from '@rocket.chat/license'; + +import { ContactImporter } from './ContactImporter'; +import { Importers } from '../../importer/server'; + +License.onValidFeature('contact-id-verification', () => { + Importers.add({ + key: 'omnichannel_contact', + name: 'omnichannel_contacts_importer', + importer: ContactImporter, + }); +}); diff --git a/apps/meteor/app/importer-pending-avatars/server/PendingAvatarImporter.ts b/apps/meteor/app/importer-pending-avatars/server/PendingAvatarImporter.ts index de37ba200289..f057da4a625d 100644 --- a/apps/meteor/app/importer-pending-avatars/server/PendingAvatarImporter.ts +++ b/apps/meteor/app/importer-pending-avatars/server/PendingAvatarImporter.ts @@ -10,8 +10,7 @@ export class PendingAvatarImporter extends Importer { this.logger.debug('start preparing import operation'); await super.updateProgress(ProgressStep.PREPARING_STARTED); - const users = Users.findAllUsersWithPendingAvatar(); - const fileCount = await users.count(); + const fileCount = await Users.countAllUsersWithPendingAvatar(); if (fileCount === 0) { await super.updateProgress(ProgressStep.DONE); diff --git a/apps/meteor/app/importer-pending-avatars/server/index.ts b/apps/meteor/app/importer-pending-avatars/server/index.ts index b69c6de8e745..2710c06b9d15 100644 --- a/apps/meteor/app/importer-pending-avatars/server/index.ts +++ b/apps/meteor/app/importer-pending-avatars/server/index.ts @@ -1,5 +1,5 @@ -import { Importers } from '../../importer/server'; import { PendingAvatarImporter } from './PendingAvatarImporter'; +import { Importers } from '../../importer/server'; Importers.add({ key: 'pending-avatars', diff --git a/apps/meteor/app/importer-pending-files/server/index.ts b/apps/meteor/app/importer-pending-files/server/index.ts index 24961551cdb5..e18b0de0a29e 100644 --- a/apps/meteor/app/importer-pending-files/server/index.ts +++ b/apps/meteor/app/importer-pending-files/server/index.ts @@ -1,5 +1,5 @@ -import { Importers } from '../../importer/server'; import { PendingFileImporter } from './PendingFileImporter'; +import { Importers } from '../../importer/server'; Importers.add({ key: 'pending-files', diff --git a/apps/meteor/app/importer-slack-users/server/index.ts b/apps/meteor/app/importer-slack-users/server/index.ts index ab99ede8f912..67b2dbdb319a 100644 --- a/apps/meteor/app/importer-slack-users/server/index.ts +++ b/apps/meteor/app/importer-slack-users/server/index.ts @@ -1,5 +1,5 @@ -import { Importers } from '../../importer/server'; import { SlackUsersImporter } from './SlackUsersImporter'; +import { Importers } from '../../importer/server'; Importers.add({ key: 'slack-users', diff --git a/apps/meteor/app/importer-slack/server/SlackImporter.ts b/apps/meteor/app/importer-slack/server/SlackImporter.ts index 344db6656531..59c07d26e969 100644 --- a/apps/meteor/app/importer-slack/server/SlackImporter.ts +++ b/apps/meteor/app/importer-slack/server/SlackImporter.ts @@ -545,13 +545,16 @@ export class SlackImporter extends Importer { // Process the reactions if (message.reactions && message.reactions.length > 0) { - newMessage.reactions = message.reactions.reduce((newReactions, reaction) => { - const name = `:${reaction.name}:`; - return { - ...newReactions, - ...(reaction.users?.length ? { name: { name, users: this._replaceSlackUserIds(reaction.users) } } : {}), - }; - }, {} as Required['reactions']); + newMessage.reactions = message.reactions.reduce( + (newReactions, reaction) => { + const name = `:${reaction.name}:`; + return { + ...newReactions, + ...(reaction.users?.length ? { name: { name, users: this._replaceSlackUserIds(reaction.users) } } : {}), + }; + }, + {} as Required['reactions'], + ); } if (message.type === 'message') { diff --git a/apps/meteor/app/importer-slack/server/index.ts b/apps/meteor/app/importer-slack/server/index.ts index b8040d77538a..d32edde7271f 100644 --- a/apps/meteor/app/importer-slack/server/index.ts +++ b/apps/meteor/app/importer-slack/server/index.ts @@ -1,5 +1,5 @@ -import { Importers } from '../../importer/server'; import { SlackImporter } from './SlackImporter'; +import { Importers } from '../../importer/server'; Importers.add({ key: 'slack', diff --git a/apps/meteor/app/importer/lib/ImporterProgressStep.ts b/apps/meteor/app/importer/lib/ImporterProgressStep.ts index 1b5ffe53c93f..5e7ea1b75966 100644 --- a/apps/meteor/app/importer/lib/ImporterProgressStep.ts +++ b/apps/meteor/app/importer/lib/ImporterProgressStep.ts @@ -11,6 +11,7 @@ export const ProgressStep = Object.freeze({ PREPARING_USERS: 'importer_preparing_users', PREPARING_CHANNELS: 'importer_preparing_channels', PREPARING_MESSAGES: 'importer_preparing_messages', + PREPARING_CONTACTS: 'importer_preparing_contacts', USER_SELECTION: 'importer_user_selection', @@ -18,6 +19,7 @@ export const ProgressStep = Object.freeze({ IMPORTING_USERS: 'importer_importing_users', IMPORTING_CHANNELS: 'importer_importing_channels', IMPORTING_MESSAGES: 'importer_importing_messages', + IMPORTING_CONTACTS: 'importer_importing_contacts', IMPORTING_FILES: 'importer_importing_files', FINISHING: 'importer_finishing', @@ -35,6 +37,7 @@ export const ImportPreparingStartedStates: IImportProgress['step'][] = [ ProgressStep.PREPARING_USERS, ProgressStep.PREPARING_CHANNELS, ProgressStep.PREPARING_MESSAGES, + ProgressStep.PREPARING_CONTACTS, ]; export const ImportingStartedStates: IImportProgress['step'][] = [ @@ -42,6 +45,7 @@ export const ImportingStartedStates: IImportProgress['step'][] = [ ProgressStep.IMPORTING_USERS, ProgressStep.IMPORTING_CHANNELS, ProgressStep.IMPORTING_MESSAGES, + ProgressStep.IMPORTING_CONTACTS, ProgressStep.IMPORTING_FILES, ProgressStep.FINISHING, ]; diff --git a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts index 64226f8752a1..60275205de22 100644 --- a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts +++ b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts @@ -1,9 +1,10 @@ -import type { IImportRecord, IImportUser, IImportMessage, IImportChannel } from '@rocket.chat/core-typings'; +import type { IImportRecord, IImportUser, IImportMessage, IImportChannel, IImportContact } from '@rocket.chat/core-typings'; import type { Logger } from '@rocket.chat/logger'; import { ImportData } from '@rocket.chat/models'; import { pick } from '@rocket.chat/tools'; import type { IConversionCallbacks } from '../definitions/IConversionCallbacks'; +import { ContactConverter } from './converters/ContactConverter'; import { ConverterCache } from './converters/ConverterCache'; import { type MessageConversionCallbacks, MessageConverter } from './converters/MessageConverter'; import type { RecordConverter, RecordConverterOptions } from './converters/RecordConverter'; @@ -21,6 +22,8 @@ export class ImportDataConverter { protected _messageConverter: MessageConverter; + protected _contactConverter: ContactConverter; + protected _cache = new ConverterCache(); public get options(): ConverterOptions { @@ -34,6 +37,7 @@ export class ImportDataConverter { }; this.initializeUserConverter(logger); + this.initializeContactConverter(logger); this.initializeRoomConverter(logger); this.initializeMessageConverter(logger); } @@ -74,6 +78,14 @@ export class ImportDataConverter { this._userConverter = new UserConverter(userOptions, logger, this._cache); } + protected initializeContactConverter(logger: Logger): void { + const contactOptions = { + ...this.getRecordConverterOptions(), + }; + + this._contactConverter = new ContactConverter(contactOptions, logger, this._cache); + } + protected initializeRoomConverter(logger: Logger): void { const roomOptions = { ...this.getRecordConverterOptions(), @@ -90,6 +102,10 @@ export class ImportDataConverter { this._messageConverter = new MessageConverter(messageOptions, logger, this._cache); } + async addContact(data: IImportContact): Promise { + return this._contactConverter.addObject(data); + } + async addUser(data: IImportUser): Promise { return this._userConverter.addObject(data); } @@ -104,6 +120,10 @@ export class ImportDataConverter { }); } + async convertContacts(callbacks: IConversionCallbacks): Promise { + return this._contactConverter.convertData(callbacks); + } + async convertUsers(callbacks: IConversionCallbacks): Promise { return this._userConverter.convertData(callbacks); } @@ -118,6 +138,7 @@ export class ImportDataConverter { async convertData(startedByUserId: string, callbacks: IConversionCallbacks = {}): Promise { await this.convertUsers(callbacks); + await this.convertContacts(callbacks); await this.convertChannels(startedByUserId, callbacks); await this.convertMessages(callbacks); diff --git a/apps/meteor/app/importer/server/classes/Importer.ts b/apps/meteor/app/importer/server/classes/Importer.ts index 5f40ead0e1ea..49430c101d45 100644 --- a/apps/meteor/app/importer/server/classes/Importer.ts +++ b/apps/meteor/app/importer/server/classes/Importer.ts @@ -6,6 +6,7 @@ import type { IImportUser, IImportProgress, IImporterShortSelection, + IImportContact, } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; import { Settings, ImportData, Imports } from '@rocket.chat/models'; @@ -13,14 +14,14 @@ import AdmZip from 'adm-zip'; import type { MatchKeysAndValues, MongoServerError } from 'mongodb'; import { Selection, SelectionChannel, SelectionUser } from '..'; -import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; -import { t } from '../../../utils/lib/i18n'; -import { ProgressStep, ImportPreparingStartedStates } from '../../lib/ImporterProgressStep'; -import type { ImporterInfo } from '../definitions/ImporterInfo'; import { ImportDataConverter } from './ImportDataConverter'; import type { ConverterOptions } from './ImportDataConverter'; import { ImporterProgress } from './ImporterProgress'; import { ImporterWebsocket } from './ImporterWebsocket'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; +import { t } from '../../../utils/lib/i18n'; +import { ProgressStep, ImportPreparingStartedStates } from '../../lib/ImporterProgressStep'; +import type { ImporterInfo } from '../definitions/ImporterInfo'; type OldSettings = { allowedDomainList?: string | null; @@ -137,6 +138,20 @@ export class Importer { const id = userData.importIds[0]; return importSelection.users.list.includes(id); } + + case 'contact': { + if (importSelection.contacts?.all) { + return true; + } + if (!importSelection.contacts?.list?.length) { + return false; + } + + const contactData = data as IImportContact; + + const id = contactData.importIds[0]; + return importSelection.contacts.list.includes(id); + } } return false; @@ -181,6 +196,9 @@ export class Importer { await this.updateProgress(ProgressStep.IMPORTING_USERS); await this.converter.convertUsers({ beforeImportFn, afterImportFn, onErrorFn, afterBatchFn }); + await this.updateProgress(ProgressStep.IMPORTING_CONTACTS); + await this.converter.convertContacts({ beforeImportFn, afterImportFn, onErrorFn }); + await this.updateProgress(ProgressStep.IMPORTING_CHANNELS); await this.converter.convertChannels(startedByUserId, { beforeImportFn, afterImportFn, onErrorFn }); @@ -303,14 +321,10 @@ export class Importer { } async maybeUpdateRecord() { - // Only update the database every 500 messages (or 50 for users/channels) + // Only update the database every 500 messages (or 50 for other records) // Or the completed is greater than or equal to the total amount const count = this.progress.count.completed + this.progress.count.error; - const range = ([ProgressStep.IMPORTING_USERS, ProgressStep.IMPORTING_CHANNELS] as IImportProgress['step'][]).includes( - this.progress.step, - ) - ? 50 - : 500; + const range = this.progress.step === ProgressStep.IMPORTING_MESSAGES ? 500 : 50; if (count % range === 0 || count >= this.progress.count.total || count - this._lastProgressReportTotal > range) { this._lastProgressReportTotal = this.progress.count.completed + this.progress.count.error; @@ -358,6 +372,7 @@ export class Importer { const users = await ImportData.getAllUsersForSelection(); const channels = await ImportData.getAllChannelsForSelection(); + const contacts = await ImportData.getAllContactsForSelection(); const hasDM = await ImportData.checkIfDirectMessagesExists(); const selectionUsers = users.map( @@ -367,13 +382,20 @@ export class Importer { const selectionChannels = channels.map( (c) => new SelectionChannel(c.data.importIds[0], c.data.name, Boolean(c.data.archived), true, c.data.t === 'p', c.data.t === 'd'), ); + const selectionContacts = contacts.map((c) => ({ + id: c.data.importIds[0], + name: c.data.name || '', + emails: c.data.emails || [], + phones: c.data.phones || [], + do_import: true, + })); const selectionMessages = await ImportData.countMessages(); if (hasDM) { selectionChannels.push(new SelectionChannel('__directMessages__', t('Direct_Messages'), false, true, true, true)); } - const results = new Selection(this.info.name, selectionUsers, selectionChannels, selectionMessages); + const results = new Selection(this.info.name, selectionUsers, selectionChannels, selectionMessages, selectionContacts); return results; } diff --git a/apps/meteor/app/importer/server/classes/ImporterSelection.ts b/apps/meteor/app/importer/server/classes/ImporterSelection.ts index 107dbbf9c824..d955bd38b4f7 100644 --- a/apps/meteor/app/importer/server/classes/ImporterSelection.ts +++ b/apps/meteor/app/importer/server/classes/ImporterSelection.ts @@ -1,4 +1,9 @@ -import type { IImporterSelection, IImporterSelectionChannel, IImporterSelectionUser } from '@rocket.chat/core-typings'; +import type { + IImporterSelection, + IImporterSelectionChannel, + IImporterSelectionUser, + IImporterSelectionContact, +} from '@rocket.chat/core-typings'; export class ImporterSelection implements IImporterSelection { public name: string; @@ -7,6 +12,8 @@ export class ImporterSelection implements IImporterSelection { public channels: IImporterSelectionChannel[]; + public contacts: IImporterSelectionContact[]; + public message_count: number; /** @@ -17,10 +24,17 @@ export class ImporterSelection implements IImporterSelection { * @param channels the channels which can be selected * @param messageCount the number of messages */ - constructor(name: string, users: IImporterSelectionUser[], channels: IImporterSelectionChannel[], messageCount: number) { + constructor( + name: string, + users: IImporterSelectionUser[], + channels: IImporterSelectionChannel[], + messageCount: number, + contacts: IImporterSelectionContact[], + ) { this.name = name; this.users = users; this.channels = channels; this.message_count = messageCount; + this.contacts = contacts; } } diff --git a/apps/meteor/app/importer/server/classes/converters/ContactConverter.ts b/apps/meteor/app/importer/server/classes/converters/ContactConverter.ts new file mode 100644 index 000000000000..eb6b25acd385 --- /dev/null +++ b/apps/meteor/app/importer/server/classes/converters/ContactConverter.ts @@ -0,0 +1,42 @@ +import type { IImportContact, IImportContactRecord } from '@rocket.chat/core-typings'; +import { LivechatVisitors } from '@rocket.chat/models'; + +import { RecordConverter } from './RecordConverter'; +import { createContact } from '../../../../livechat/server/lib/contacts/createContact'; +import { getAllowedCustomFields } from '../../../../livechat/server/lib/contacts/getAllowedCustomFields'; +import { validateCustomFields } from '../../../../livechat/server/lib/contacts/validateCustomFields'; + +export class ContactConverter extends RecordConverter { + protected async convertCustomFields(customFields: IImportContact['customFields']): Promise { + if (!customFields) { + return; + } + + const allowedCustomFields = await getAllowedCustomFields(); + return validateCustomFields(allowedCustomFields, customFields, { ignoreAdditionalFields: true }); + } + + protected async convertRecord(record: IImportContactRecord): Promise { + const { data } = record; + + await createContact({ + name: data.name || (await this.generateNewContactName()), + emails: data.emails, + phones: data.phones, + customFields: await this.convertCustomFields(data.customFields), + contactManager: await this._cache.getIdOfUsername(data.contactManager), + unknown: false, + importIds: data.importIds, + }); + + return true; + } + + protected async generateNewContactName(): Promise { + return LivechatVisitors.getNextVisitorUsername(); + } + + protected getDataType(): 'contact' { + return 'contact'; + } +} diff --git a/apps/meteor/app/importer/server/classes/converters/ConverterCache.ts b/apps/meteor/app/importer/server/classes/converters/ConverterCache.ts index cefbf9cc7dbb..284e51dddcd5 100644 --- a/apps/meteor/app/importer/server/classes/converters/ConverterCache.ts +++ b/apps/meteor/app/importer/server/classes/converters/ConverterCache.ts @@ -1,4 +1,4 @@ -import type { IImportUser } from '@rocket.chat/core-typings'; +import type { IImportUser, IUser } from '@rocket.chat/core-typings'; import { Rooms, Users } from '@rocket.chat/models'; export type UserIdentification = { @@ -17,6 +17,8 @@ export class ConverterCache { // display name uses a different cache because it's only used on mentions so we don't need to load it every time we load an user private _userDisplayNameCache = new Map(); + private _userNameToIdCache = new Map(); + private _roomCache = new Map(); private _roomNameCache = new Map(); @@ -28,6 +30,9 @@ export class ConverterCache { }; this._userCache.set(importId, cache); + if (username) { + this._userNameToIdCache.set(username, _id); + } return cache; } @@ -57,6 +62,10 @@ export class ConverterCache { this.addUser(userData.importIds[0], userData._id, userData.username); } + addUsernameToId(username: string, id: string): void { + this._userNameToIdCache.set(username, id); + } + async findImportedRoomId(importId: string): Promise { if (this._roomCache.has(importId)) { return this._roomCache.get(importId) as string; @@ -195,4 +204,19 @@ export class ConverterCache { ) ).filter((user) => user) as string[]; } + + async getIdOfUsername(username: string | undefined): Promise { + if (!username) { + return; + } + + if (this._userNameToIdCache.has(username)) { + return this._userNameToIdCache.get(username); + } + + const user = await Users.findOneByUsername>(username, { projection: { _id: 1 } }); + this.addUsernameToId(username, user?._id); + + return user?._id; + } } diff --git a/apps/meteor/app/importer/server/classes/converters/MessageConverter.ts b/apps/meteor/app/importer/server/classes/converters/MessageConverter.ts index b4540ed6182f..732e46b4398b 100644 --- a/apps/meteor/app/importer/server/classes/converters/MessageConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/MessageConverter.ts @@ -2,10 +2,10 @@ import type { IImportMessageRecord, IMessage as IDBMessage, IImportMessage, IImp import { Rooms } from '@rocket.chat/models'; import limax from 'limax'; -import { insertMessage } from '../../../../lib/server/functions/insertMessage'; -import type { IConversionCallbacks } from '../../definitions/IConversionCallbacks'; import type { UserIdentification, MentionedChannel } from './ConverterCache'; import { RecordConverter } from './RecordConverter'; +import { insertMessage } from '../../../../lib/server/functions/insertMessage'; +import type { IConversionCallbacks } from '../../definitions/IConversionCallbacks'; export type MessageConversionCallbacks = IConversionCallbacks & { afterImportAllMessagesFn?: (roomIds: string[]) => Promise }; diff --git a/apps/meteor/app/importer/server/classes/converters/RecordConverter.ts b/apps/meteor/app/importer/server/classes/converters/RecordConverter.ts index d0a6d60fa723..9003fe4bd416 100644 --- a/apps/meteor/app/importer/server/classes/converters/RecordConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/RecordConverter.ts @@ -4,8 +4,8 @@ import { ImportData } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { type FindCursor, ObjectId } from 'mongodb'; -import type { IConversionCallbacks } from '../../definitions/IConversionCallbacks'; import { ConverterCache } from './ConverterCache'; +import type { IConversionCallbacks } from '../../definitions/IConversionCallbacks'; export type RecordConverterOptions = { workInMemory?: boolean; diff --git a/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts b/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts index f57fa1a7cb88..e9a14fed6a03 100644 --- a/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts @@ -2,13 +2,13 @@ import type { IImportChannel, IImportChannelRecord, IRoom } from '@rocket.chat/c import { Subscriptions, Rooms, Users } from '@rocket.chat/models'; import limax from 'limax'; +import { RecordConverter } from './RecordConverter'; import { createDirectMessage } from '../../../../../server/methods/createDirectMessage'; import { saveRoomSettings } from '../../../../channel-settings/server/methods/saveRoomSettings'; import { notifyOnSubscriptionChangedByRoomId } from '../../../../lib/server/lib/notifyListener'; import { createChannelMethod } from '../../../../lib/server/methods/createChannel'; import { createPrivateGroupMethod } from '../../../../lib/server/methods/createPrivateGroup'; import type { IConversionCallbacks } from '../../definitions/IConversionCallbacks'; -import { RecordConverter } from './RecordConverter'; export class RoomConverter extends RecordConverter { public startedByUserId: string; diff --git a/apps/meteor/app/importer/server/classes/converters/UserConverter.ts b/apps/meteor/app/importer/server/classes/converters/UserConverter.ts index 2d2bb7bad80a..421af3cb611e 100644 --- a/apps/meteor/app/importer/server/classes/converters/UserConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/UserConverter.ts @@ -5,6 +5,8 @@ import { SHA256 } from '@rocket.chat/sha256'; import { hash as bcryptHash } from 'bcrypt'; import { Accounts } from 'meteor/accounts-base'; +import { RecordConverter, type RecordConverterOptions } from './RecordConverter'; +import { generateTempPassword } from './generateTempPassword'; import { callbacks as systemCallbacks } from '../../../../../lib/callbacks'; import { addUserToDefaultChannels } from '../../../../lib/server/functions/addUserToDefaultChannels'; import { generateUsernameSuggestion } from '../../../../lib/server/functions/getUsernameSuggestion'; @@ -12,7 +14,6 @@ import { saveUserIdentity } from '../../../../lib/server/functions/saveUserIdent import { setUserActiveStatus } from '../../../../lib/server/functions/setUserActiveStatus'; import { notifyOnUserChange } from '../../../../lib/server/lib/notifyListener'; import type { IConversionCallbacks } from '../../definitions/IConversionCallbacks'; -import { RecordConverter, type RecordConverterOptions } from './RecordConverter'; export type UserConverterOptions = { flagEmailsAsVerified?: boolean; @@ -319,15 +320,15 @@ export class UserConverter extends RecordConverter { + async hashPassword(password: string): Promise { return bcryptHash(SHA256(password), Accounts._bcryptRounds()); } - private generateTempPassword(userData: IImportUser): string { - return `${Date.now()}${userData.name || ''}${userData.emails.length ? userData.emails[0].toUpperCase() : ''}`; + generateTempPassword(userData: IImportUser): string { + return generateTempPassword(userData); } - private async buildNewUserObject(userData: IImportUser): Promise> { + async buildNewUserObject(userData: IImportUser): Promise> { return { type: userData.type || 'user', ...(userData.username && { username: userData.username }), @@ -376,7 +377,7 @@ export class UserConverter extends RecordConverter({ script: integration.script, scriptEnabled: integration.scriptEnabled, scriptEngine, - }), + }), ...(typeof integration.overrideDestinationChannelEnabled !== 'undefined' && { overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled, }), diff --git a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts index 4449902437c9..6ee39d81a588 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts @@ -91,7 +91,7 @@ Meteor.methods({ scriptEnabled: integration.scriptEnabled, scriptEngine, ...(integration.scriptCompiled ? { scriptCompiled: integration.scriptCompiled } : { scriptError: integration.scriptError }), - }), + }), triggerWords: integration.triggerWords, retryFailedCalls: integration.retryFailedCalls, retryCount: integration.retryCount, @@ -107,7 +107,7 @@ Meteor.methods({ $unset: { ...(integration.scriptCompiled ? { scriptError: 1 as const } : { scriptCompiled: 1 as const }), }, - }), + }), }, ); diff --git a/apps/meteor/app/integrations/server/triggers.ts b/apps/meteor/app/integrations/server/triggers.ts index 64b95827645f..06fc9b0a9e1a 100644 --- a/apps/meteor/app/integrations/server/triggers.ts +++ b/apps/meteor/app/integrations/server/triggers.ts @@ -1,6 +1,6 @@ +import { triggerHandler } from './lib/triggerHandler'; import { callbacks } from '../../../lib/callbacks'; import { afterLeaveRoomCallback } from '../../../lib/callbacks/afterLeaveRoomCallback'; -import { triggerHandler } from './lib/triggerHandler'; const callbackHandler = function _callbackHandler(eventType: string) { return function _wrapperFunction(...args: any[]) { diff --git a/apps/meteor/app/invites/server/functions/useInviteToken.ts b/apps/meteor/app/invites/server/functions/useInviteToken.ts index 21e89bd22fbc..f514736da590 100644 --- a/apps/meteor/app/invites/server/functions/useInviteToken.ts +++ b/apps/meteor/app/invites/server/functions/useInviteToken.ts @@ -1,10 +1,10 @@ import { Invites, Subscriptions, Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; +import { validateInviteToken } from './validateInviteToken'; import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { addUserToRoom } from '../../../lib/server/functions/addUserToRoom'; -import { validateInviteToken } from './validateInviteToken'; export const useInviteToken = async (userId: string, token: string) => { if (!userId) { diff --git a/apps/meteor/app/irc/server/irc-bridge/index.js b/apps/meteor/app/irc/server/irc-bridge/index.js index bc5b4f0bc33f..26d4948bd0f1 100644 --- a/apps/meteor/app/irc/server/irc-bridge/index.js +++ b/apps/meteor/app/irc/server/irc-bridge/index.js @@ -7,6 +7,7 @@ import { callbacks } from '../../../../lib/callbacks'; import { afterLeaveRoomCallback } from '../../../../lib/callbacks/afterLeaveRoomCallback'; import { afterLogoutCleanUpCallback } from '../../../../lib/callbacks/afterLogoutCleanUpCallback'; import { withThrottling } from '../../../../lib/utils/highOrderFunctions'; +import { updateAuditedBySystem } from '../../../../server/settings/lib/auditedSettingUpdates'; import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import * as servers from '../servers'; import * as localCommandHandlers from './localHandlers'; @@ -22,7 +23,9 @@ const updateLastPing = withThrottling({ wait: 10_000 })(() => { } void (async () => { - const updatedValue = await Settings.updateValueById('IRC_Bridge_Last_Ping', new Date(), { upsert: true }); + const updatedValue = await updateAuditedBySystem({ + reason: 'updateLastPing', + })(Settings.updateValueById, 'IRC_Bridge_Last_Ping', new Date(), { upsert: true }); if (updatedValue.modifiedCount || updatedValue.upsertedCount) { void notifyOnSettingChangedById('IRC_Bridge_Last_Ping'); } diff --git a/apps/meteor/app/irc/server/irc-bridge/localHandlers/index.js b/apps/meteor/app/irc/server/irc-bridge/localHandlers/index.ts similarity index 100% rename from apps/meteor/app/irc/server/irc-bridge/localHandlers/index.js rename to apps/meteor/app/irc/server/irc-bridge/localHandlers/index.ts diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/index.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/index.ts similarity index 100% rename from apps/meteor/app/irc/server/irc-bridge/peerHandlers/index.js rename to apps/meteor/app/irc/server/irc-bridge/peerHandlers/index.ts diff --git a/apps/meteor/app/irc/server/irc.js b/apps/meteor/app/irc/server/irc.js index 2f6efcb99e41..7012e2829134 100644 --- a/apps/meteor/app/irc/server/irc.js +++ b/apps/meteor/app/irc/server/irc.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; -import { settings } from '../../settings/server'; import Bridge from './irc-bridge'; +import { settings } from '../../settings/server'; if (!!settings.get('IRC_Enabled') === true) { // Normalize the config values diff --git a/apps/meteor/app/irc/server/methods/resetIrcConnection.ts b/apps/meteor/app/irc/server/methods/resetIrcConnection.ts index aaaeef1c06b8..1fed8c77d28e 100644 --- a/apps/meteor/app/irc/server/methods/resetIrcConnection.ts +++ b/apps/meteor/app/irc/server/methods/resetIrcConnection.ts @@ -2,6 +2,8 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; +import { updateAuditedByUser } from '../../../../server/settings/lib/auditedSettingUpdates'; +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import Bridge from '../irc-bridge'; @@ -16,13 +18,33 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async resetIrcConnection() { const ircEnabled = Boolean(settings.get('IRC_Enabled')); + const uid = Meteor.userId(); - const updatedLastPingValue = await Settings.updateValueById('IRC_Bridge_Last_Ping', new Date(0), { upsert: true }); + if (!uid) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'resetIrcConnection' }); + } + + if (!(await hasPermissionAsync(uid, 'edit-privileged-setting'))) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'resetIrcConnection' }); + } + + const auditSettingOperation = updateAuditedByUser({ + _id: uid, + username: (await Meteor.userAsync())!.username!, + ip: this.connection?.clientAddress || '', + useragent: this.connection?.httpHeaders['user-agent'] || '', + }); + + const updatedLastPingValue = await auditSettingOperation(Settings.updateValueById, 'IRC_Bridge_Last_Ping', new Date(0), { + upsert: true, + }); if (updatedLastPingValue.modifiedCount || updatedLastPingValue.upsertedCount) { void notifyOnSettingChangedById('IRC_Bridge_Last_Ping'); } - const updatedResetTimeValue = await Settings.updateValueById('IRC_Bridge_Reset_Time', new Date(), { upsert: true }); + const updatedResetTimeValue = await auditSettingOperation(Settings.updateValueById, 'IRC_Bridge_Reset_Time', new Date(), { + upsert: true, + }); if (updatedResetTimeValue.modifiedCount || updatedResetTimeValue.upsertedCount) { void notifyOnSettingChangedById('IRC_Bridge_Last_Ping'); } diff --git a/apps/meteor/app/irc/server/servers/index.js b/apps/meteor/app/irc/server/servers/index.ts similarity index 100% rename from apps/meteor/app/irc/server/servers/index.js rename to apps/meteor/app/irc/server/servers/index.ts diff --git a/apps/meteor/app/lib/client/OAuthProxy.js b/apps/meteor/app/lib/client/OAuthProxy.ts similarity index 86% rename from apps/meteor/app/lib/client/OAuthProxy.js rename to apps/meteor/app/lib/client/OAuthProxy.ts index a5035783c676..ec9143528fc9 100644 --- a/apps/meteor/app/lib/client/OAuthProxy.js +++ b/apps/meteor/app/lib/client/OAuthProxy.ts @@ -6,12 +6,12 @@ OAuth.launchLogin = ((func) => function (options) { const proxy = settings.get('Accounts_OAuth_Proxy_services').replace(/\s/g, '').split(','); if (proxy.includes(options.loginService)) { - const redirect_uri = options.loginUrl.match(/(&redirect_uri=)([^&]+|$)/)[2]; + const redirectUri = options.loginUrl.match(/(&redirect_uri=)([^&]+|$)/)?.[2]; options.loginUrl = options.loginUrl.replace( /(&redirect_uri=)([^&]+|$)/, `$1${encodeURIComponent(settings.get('Accounts_OAuth_Proxy_host'))}/oauth_redirect`, ); - options.loginUrl = options.loginUrl.replace(/(&state=)([^&]+|$)/, `$1${redirect_uri}!$2`); + options.loginUrl = options.loginUrl.replace(/(&state=)([^&]+|$)/, `$1${redirectUri}!$2`); options.loginUrl = `${settings.get('Accounts_OAuth_Proxy_host')}/redirect/${encodeURIComponent(options.loginUrl)}`; } diff --git a/apps/meteor/app/lib/client/methods/sendMessage.ts b/apps/meteor/app/lib/client/methods/sendMessage.ts index 19220f901458..f87684eb978e 100644 --- a/apps/meteor/app/lib/client/methods/sendMessage.ts +++ b/apps/meteor/app/lib/client/methods/sendMessage.ts @@ -6,7 +6,7 @@ import { onClientMessageReceived } from '../../../../client/lib/onClientMessageR import { dispatchToastMessage } from '../../../../client/lib/toast'; import { callbacks } from '../../../../lib/callbacks'; import { trim } from '../../../../lib/utils/stringUtils'; -import { ChatMessage, ChatRoom } from '../../../models/client'; +import { Messages, Rooms } from '../../../models/client'; import { settings } from '../../../settings/client'; import { t } from '../../../utils/lib/i18n'; @@ -16,7 +16,7 @@ Meteor.methods({ if (!uid || trim(message.msg) === '') { return false; } - const messageAlreadyExists = message._id && ChatMessage.findOne({ _id: message._id }); + const messageAlreadyExists = message._id && Messages.findOne({ _id: message._id }); if (messageAlreadyExists) { return dispatchToastMessage({ type: 'error', message: t('Message_Already_Sent') }); } @@ -36,13 +36,13 @@ Meteor.methods({ } // If the room is federated, send the message to matrix only - const room = ChatRoom.findOne({ _id: message.rid }, { fields: { federated: 1, name: 1 } }); + const room = Rooms.findOne({ _id: message.rid }, { fields: { federated: 1, name: 1 } }); if (room?.federated) { return; } await onClientMessageReceived(message as IMessage).then((message) => { - ChatMessage.insert(message); + Messages.insert(message); return callbacks.run('afterSaveMessage', message, { room }); }); }, diff --git a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts index 3fb9c419aa5f..f5317a7b0e3b 100644 --- a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts +++ b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts @@ -2,11 +2,11 @@ import { Message } from '@rocket.chat/core-services'; import type { IUser } from '@rocket.chat/core-typings'; import { Subscriptions } from '@rocket.chat/models'; +import { getDefaultChannels } from './getDefaultChannels'; import { callbacks } from '../../../../lib/callbacks'; import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; import { notifyOnSubscriptionChangedById } from '../lib/notifyListener'; -import { getDefaultChannels } from './getDefaultChannels'; export const addUserToDefaultChannels = async function (user: IUser, silenced?: boolean): Promise { await callbacks.run('beforeJoinDefaultChannels', user); diff --git a/apps/meteor/app/lib/server/functions/checkUsernameAvailability.ts b/apps/meteor/app/lib/server/functions/checkUsernameAvailability.ts index 316a3b8779a0..498828444e4b 100644 --- a/apps/meteor/app/lib/server/functions/checkUsernameAvailability.ts +++ b/apps/meteor/app/lib/server/functions/checkUsernameAvailability.ts @@ -4,8 +4,8 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; -import { settings } from '../../../settings/server'; import { validateName } from './validateName'; +import { settings } from '../../../settings/server'; let usernameBlackList: RegExp[] = []; diff --git a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts index 765a03cad87b..8fc449cdc615 100644 --- a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts +++ b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts @@ -2,10 +2,10 @@ import { api } from '@rocket.chat/core-services'; import type { IRoom } from '@rocket.chat/core-typings'; import { Messages, Rooms, Subscriptions, ReadReceipts, Users } from '@rocket.chat/models'; +import { deleteRoom } from './deleteRoom'; import { i18n } from '../../../../server/lib/i18n'; import { FileUpload } from '../../../file-upload/server'; import { notifyOnRoomChangedById, notifyOnSubscriptionChangedById } from '../lib/notifyListener'; -import { deleteRoom } from './deleteRoom'; export async function cleanRoomHistory({ rid = '', diff --git a/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts b/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts index 263b137ae00c..1e7672005867 100644 --- a/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts +++ b/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts @@ -3,8 +3,8 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms, Subscriptions } from '@rocket.chat/models'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import type { CloseRoomParams } from '../../../livechat/server/lib/LivechatTyped'; import { Livechat } from '../../../livechat/server/lib/LivechatTyped'; +import type { CloseRoomParams } from '../../../livechat/server/lib/localTypes'; import { notifyOnSubscriptionChanged } from '../lib/notifyListener'; export const closeLivechatRoom = async ( @@ -67,12 +67,12 @@ export const closeLivechatRoom = async ( requestedBy: user, }, }, - } + } : { emailTranscript: { sendToVisitor: false, }, - }), + }), }), }; diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 769155b66b60..fda2314d3f2e 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -7,13 +7,13 @@ import type { ICreatedRoom, IUser, IRoom, RoomType } from '@rocket.chat/core-typ import { Rooms, Subscriptions, Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; +import { createDirectRoom } from './createDirectRoom'; import { callbacks } from '../../../../lib/callbacks'; import { beforeCreateRoomCallback } from '../../../../lib/callbacks/beforeCreateRoomCallback'; import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; import { notifyOnRoomChanged, notifyOnSubscriptionChangedById } from '../lib/notifyListener'; -import { createDirectRoom } from './createDirectRoom'; const isValidName = (name: unknown): name is string => { return typeof name === 'string' && name.trim().length > 0; diff --git a/apps/meteor/app/lib/server/functions/deleteUser.ts b/apps/meteor/app/lib/server/functions/deleteUser.ts index 483085d40811..e0a217a0d39b 100644 --- a/apps/meteor/app/lib/server/functions/deleteUser.ts +++ b/apps/meteor/app/lib/server/functions/deleteUser.ts @@ -16,6 +16,10 @@ import { } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; +import { getSubscribedRoomsForUserWithDetails, shouldRemoveOrChangeOwner } from './getRoomsWithSingleOwner'; +import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; +import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; +import { updateGroupDMsName } from './updateGroupDMsName'; import { callbacks } from '../../../../lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; import { FileUpload } from '../../../file-upload/server'; @@ -26,10 +30,6 @@ import { notifyOnLivechatDepartmentAgentChanged, notifyOnUserChange, } from '../lib/notifyListener'; -import { getSubscribedRoomsForUserWithDetails, shouldRemoveOrChangeOwner } from './getRoomsWithSingleOwner'; -import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; -import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; -import { updateGroupDMsName } from './updateGroupDMsName'; export async function deleteUser(userId: string, confirmRelinquish = false, deletedBy?: IUser['_id']): Promise { if (userId === 'rocket.cat') { diff --git a/apps/meteor/app/lib/server/functions/getRoomByNameOrIdWithOptionToJoin.ts b/apps/meteor/app/lib/server/functions/getRoomByNameOrIdWithOptionToJoin.ts index 113f8f59a557..1dd803f8b13c 100644 --- a/apps/meteor/app/lib/server/functions/getRoomByNameOrIdWithOptionToJoin.ts +++ b/apps/meteor/app/lib/server/functions/getRoomByNameOrIdWithOptionToJoin.ts @@ -2,9 +2,9 @@ import type { IRoom, IUser, RoomType } from '@rocket.chat/core-typings'; import { Rooms, Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; +import { addUserToRoom } from './addUserToRoom'; import { isObject } from '../../../../lib/utils/isObject'; import { createDirectMessage } from '../../../../server/methods/createDirectMessage'; -import { addUserToRoom } from './addUserToRoom'; export const getRoomByNameOrIdWithOptionToJoin = async ({ user, diff --git a/apps/meteor/app/lib/server/functions/loadMessageHistory.ts b/apps/meteor/app/lib/server/functions/loadMessageHistory.ts index fee7061cae96..5addbd896889 100644 --- a/apps/meteor/app/lib/server/functions/loadMessageHistory.ts +++ b/apps/meteor/app/lib/server/functions/loadMessageHistory.ts @@ -49,7 +49,7 @@ export async function loadMessageHistory({ hiddenMessageTypes, options, showThreadMessages, - ).toArray() + ).toArray() : await Messages.findVisibleByRoomIdNotContainingTypes(rid, hiddenMessageTypes, options, showThreadMessages).toArray(); const messages = await normalizeMessagesForUser(records, userId); let unreadNotLoaded = 0; diff --git a/apps/meteor/app/lib/server/functions/notifications/index.ts b/apps/meteor/app/lib/server/functions/notifications/index.ts index 54b18f502ae7..538ff2ad5ed9 100644 --- a/apps/meteor/app/lib/server/functions/notifications/index.ts +++ b/apps/meteor/app/lib/server/functions/notifications/index.ts @@ -47,22 +47,3 @@ export function replaceMentionedUsernamesWithFullNames(message: string, mentions }); return message; } - -/** - * Checks if a message contains a user highlight - * - * @param {string} message - * @param {array|undefined} highlights - * - * @returns {boolean} - */ -export function messageContainsHighlight(message: Pick, highlights: string[] | undefined): boolean { - if (!highlights || highlights.length === 0) { - return false; - } - - return highlights.some((highlight: string) => { - const regexp = new RegExp(escapeRegExp(highlight), 'i'); - return regexp.test(message.msg); - }); -} diff --git a/apps/meteor/app/lib/server/functions/notifications/messageContainsHighlight.ts b/apps/meteor/app/lib/server/functions/notifications/messageContainsHighlight.ts new file mode 100644 index 000000000000..d749441ba0b4 --- /dev/null +++ b/apps/meteor/app/lib/server/functions/notifications/messageContainsHighlight.ts @@ -0,0 +1,22 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { escapeRegExp } from '@rocket.chat/string-helpers'; + +/** + * Checks if a message contains a user highlight + * + * @param {string} message + * @param {array|undefined} highlights + * + * @returns {boolean} + */ +export function messageContainsHighlight(message: Pick, highlights: string[] | undefined): boolean { + if (!highlights || highlights.length === 0) { + return false; + } + + return highlights.some((highlight: string) => { + const hl = escapeRegExp(highlight); + const regexp = new RegExp(`(? => { // no bulk deletion for files diff --git a/apps/meteor/app/lib/server/functions/saveCustomFields.ts b/apps/meteor/app/lib/server/functions/saveCustomFields.ts index 0657ca4bbf2b..a67304d5c5a4 100644 --- a/apps/meteor/app/lib/server/functions/saveCustomFields.ts +++ b/apps/meteor/app/lib/server/functions/saveCustomFields.ts @@ -1,7 +1,7 @@ -import { trim } from '../../../../lib/utils/stringUtils'; -import { settings } from '../../../settings/server'; import { saveCustomFieldsWithoutValidation } from './saveCustomFieldsWithoutValidation'; import { validateCustomFields } from './validateCustomFields'; +import { trim } from '../../../../lib/utils/stringUtils'; +import { settings } from '../../../settings/server'; export const saveCustomFields = async function (userId: string, formData: Record): Promise { if (trim(settings.get('Accounts_CustomFields')) !== '') { diff --git a/apps/meteor/app/lib/server/functions/saveUser.js b/apps/meteor/app/lib/server/functions/saveUser.js deleted file mode 100644 index ef6a7e9fe7bd..000000000000 --- a/apps/meteor/app/lib/server/functions/saveUser.js +++ /dev/null @@ -1,475 +0,0 @@ -import { Apps, AppEvents } from '@rocket.chat/apps'; -import { isUserFederated } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; -import Gravatar from 'gravatar'; -import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; -import _ from 'underscore'; - -import { callbacks } from '../../../../lib/callbacks'; -import { trim } from '../../../../lib/utils/stringUtils'; -import { getNewUserRoles } from '../../../../server/services/user/lib/getNewUserRoles'; -import { getRoles } from '../../../authorization/server'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import * as Mailer from '../../../mailer/server/api'; -import { settings } from '../../../settings/server'; -import { safeGetMeteorUser } from '../../../utils/server/functions/safeGetMeteorUser'; -import { validateEmailDomain } from '../lib'; -import { generatePassword } from '../lib/generatePassword'; -import { notifyOnUserChangeById, notifyOnUserChange } from '../lib/notifyListener'; -import { passwordPolicy } from '../lib/passwordPolicy'; -import { checkEmailAvailability } from './checkEmailAvailability'; -import { checkUsernameAvailability } from './checkUsernameAvailability'; -import { saveUserIdentity } from './saveUserIdentity'; -import { setEmail } from './setEmail'; -import { setStatusText } from './setStatusText'; -import { setUserAvatar } from './setUserAvatar'; - -const MAX_BIO_LENGTH = 260; -const MAX_NICKNAME_LENGTH = 120; - -let html = ''; -let passwordChangedHtml = ''; -Meteor.startup(() => { - Mailer.getTemplate('Accounts_UserAddedEmail_Email', (template) => { - html = template; - }); - - Mailer.getTemplate('Password_Changed_Email', (template) => { - passwordChangedHtml = template; - }); -}); - -async function _sendUserEmail(subject, html, userData) { - const email = { - to: userData.email, - from: settings.get('From_Email'), - subject, - html, - data: { - email: userData.email, - password: userData.password, - }, - }; - - if (typeof userData.name !== 'undefined') { - email.data.name = userData.name; - } - - try { - await Mailer.send(email); - } catch (error) { - throw new Meteor.Error('error-email-send-failed', `Error trying to send email: ${error.message}`, { - function: 'RocketChat.saveUser', - message: error.message, - }); - } -} - -async function validateUserData(userId, userData) { - const existingRoles = _.pluck(await getRoles(), '_id'); - - if (userData.verified && userData._id && userId === userData._id) { - throw new Meteor.Error('error-action-not-allowed', 'Editing email verification is not allowed', { - method: 'insertOrUpdateUser', - action: 'Editing_user', - }); - } - - if (userData._id && userId !== userData._id && !(await hasPermissionAsync(userId, 'edit-other-user-info'))) { - throw new Meteor.Error('error-action-not-allowed', 'Editing user is not allowed', { - method: 'insertOrUpdateUser', - action: 'Editing_user', - }); - } - - if (!userData._id && !(await hasPermissionAsync(userId, 'create-user'))) { - throw new Meteor.Error('error-action-not-allowed', 'Adding user is not allowed', { - method: 'insertOrUpdateUser', - action: 'Adding_user', - }); - } - - if (userData.roles && _.difference(userData.roles, existingRoles).length > 0) { - throw new Meteor.Error('error-action-not-allowed', 'The field Roles consist invalid role id', { - method: 'insertOrUpdateUser', - action: 'Assign_role', - }); - } - - if (userData.roles && userData.roles.includes('admin') && !(await hasPermissionAsync(userId, 'assign-admin-role'))) { - throw new Meteor.Error('error-action-not-allowed', 'Assigning admin is not allowed', { - method: 'insertOrUpdateUser', - action: 'Assign_admin', - }); - } - - if (settings.get('Accounts_RequireNameForSignUp') && !userData._id && !trim(userData.name)) { - throw new Meteor.Error('error-the-field-is-required', 'The field Name is required', { - method: 'insertOrUpdateUser', - field: 'Name', - }); - } - - if (!userData._id && !trim(userData.username)) { - throw new Meteor.Error('error-the-field-is-required', 'The field Username is required', { - method: 'insertOrUpdateUser', - field: 'Username', - }); - } - - let nameValidation; - - try { - nameValidation = new RegExp(`^${settings.get('UTF8_User_Names_Validation')}$`); - } catch (e) { - nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); - } - - if (userData.username && !nameValidation.test(userData.username)) { - throw new Meteor.Error('error-input-is-not-a-valid-field', `${_.escape(userData.username)} is not a valid username`, { - method: 'insertOrUpdateUser', - input: userData.username, - field: 'Username', - }); - } - - if (!userData._id && !userData.password && !userData.setRandomPassword) { - throw new Meteor.Error('error-the-field-is-required', 'The field Password is required', { - method: 'insertOrUpdateUser', - field: 'Password', - }); - } - - if (!userData._id) { - if (!(await checkUsernameAvailability(userData.username))) { - throw new Meteor.Error('error-field-unavailable', `${_.escape(userData.username)} is already in use :(`, { - method: 'insertOrUpdateUser', - field: userData.username, - }); - } - - if (userData.email && !(await checkEmailAvailability(userData.email))) { - throw new Meteor.Error('error-field-unavailable', `${_.escape(userData.email)} is already in use :(`, { - method: 'insertOrUpdateUser', - field: userData.email, - }); - } - } -} - -/** - * Validate permissions to edit user fields - * - * @param {string} userId - * @param {{ _id: string, roles?: string[], username?: string, name?: string, statusText?: string, email?: string, password?: string}} userData - */ -export async function validateUserEditing(userId, userData) { - const editingMyself = userData._id && userId === userData._id; - - const canEditOtherUserInfo = await hasPermissionAsync(userId, 'edit-other-user-info'); - const canEditOtherUserPassword = await hasPermissionAsync(userId, 'edit-other-user-password'); - const user = await Users.findOneById(userData._id); - - const isEditingUserRoles = (previousRoles, newRoles) => - typeof newRoles !== 'undefined' && !_.isEqual(_.sortBy(previousRoles), _.sortBy(newRoles)); - const isEditingField = (previousValue, newValue) => typeof newValue !== 'undefined' && newValue !== previousValue; - - if (isEditingUserRoles(user.roles, userData.roles) && !(await hasPermissionAsync(userId, 'assign-roles'))) { - throw new Meteor.Error('error-action-not-allowed', 'Assign roles is not allowed', { - method: 'insertOrUpdateUser', - action: 'Assign_role', - }); - } - - if (!settings.get('Accounts_AllowUserProfileChange') && !canEditOtherUserInfo && !canEditOtherUserPassword) { - throw new Meteor.Error('error-action-not-allowed', 'Edit user profile is not allowed', { - method: 'insertOrUpdateUser', - action: 'Update_user', - }); - } - - if ( - isEditingField(user.username, userData.username) && - !settings.get('Accounts_AllowUsernameChange') && - (!canEditOtherUserInfo || editingMyself) - ) { - throw new Meteor.Error('error-action-not-allowed', 'Edit username is not allowed', { - method: 'insertOrUpdateUser', - action: 'Update_user', - }); - } - - if ( - isEditingField(user.statusText, userData.statusText) && - !settings.get('Accounts_AllowUserStatusMessageChange') && - (!canEditOtherUserInfo || editingMyself) - ) { - throw new Meteor.Error('error-action-not-allowed', 'Edit user status is not allowed', { - method: 'insertOrUpdateUser', - action: 'Update_user', - }); - } - - if ( - isEditingField(user.name, userData.name) && - !settings.get('Accounts_AllowRealNameChange') && - (!canEditOtherUserInfo || editingMyself) - ) { - throw new Meteor.Error('error-action-not-allowed', 'Edit user real name is not allowed', { - method: 'insertOrUpdateUser', - action: 'Update_user', - }); - } - - if ( - user.emails?.[0] && - isEditingField(user.emails[0].address, userData.email) && - !settings.get('Accounts_AllowEmailChange') && - (!canEditOtherUserInfo || editingMyself) - ) { - throw new Meteor.Error('error-action-not-allowed', 'Edit user email is not allowed', { - method: 'insertOrUpdateUser', - action: 'Update_user', - }); - } - - if (userData.password && !settings.get('Accounts_AllowPasswordChange') && (!canEditOtherUserPassword || editingMyself)) { - throw new Meteor.Error('error-action-not-allowed', 'Edit user password is not allowed', { - method: 'insertOrUpdateUser', - action: 'Update_user', - }); - } -} - -const handleBio = (updateUser, bio) => { - if (bio && bio.trim()) { - if (bio.length > MAX_BIO_LENGTH) { - throw new Meteor.Error('error-bio-size-exceeded', `Bio size exceeds ${MAX_BIO_LENGTH} characters`, { - method: 'saveUserProfile', - }); - } - updateUser.$set = updateUser.$set || {}; - updateUser.$set.bio = bio; - } else { - updateUser.$unset = updateUser.$unset || {}; - updateUser.$unset.bio = 1; - } -}; - -const handleNickname = (updateUser, nickname) => { - if (nickname && nickname.trim()) { - if (nickname.length > MAX_NICKNAME_LENGTH) { - throw new Meteor.Error('error-nickname-size-exceeded', `Nickname size exceeds ${MAX_NICKNAME_LENGTH} characters`, { - method: 'saveUserProfile', - }); - } - updateUser.$set = updateUser.$set || {}; - updateUser.$set.nickname = nickname; - } else { - updateUser.$unset = updateUser.$unset || {}; - updateUser.$unset.nickname = 1; - } -}; - -const saveNewUser = async function (userData, sendPassword) { - await validateEmailDomain(userData.email); - - const roles = (!!userData.roles && userData.roles.length > 0 && userData.roles) || getNewUserRoles(); - const isGuest = roles && roles.length === 1 && roles.includes('guest'); - - // insert user - const createUser = { - username: userData.username, - password: userData.password, - joinDefaultChannels: userData.joinDefaultChannels, - isGuest, - globalRoles: roles, - skipNewUserRolesSetting: true, - }; - if (userData.email) { - createUser.email = userData.email; - } - - const _id = await Accounts.createUserAsync(createUser); - - const updateUser = { - $set: { - ...(typeof userData.name !== 'undefined' && { name: userData.name }), - settings: userData.settings || {}, - }, - }; - - if (typeof userData.requirePasswordChange !== 'undefined') { - updateUser.$set.requirePasswordChange = userData.requirePasswordChange; - } - - if (typeof userData.verified === 'boolean') { - updateUser.$set['emails.0.verified'] = userData.verified; - } - - handleBio(updateUser, userData.bio); - handleNickname(updateUser, userData.nickname); - - await Users.updateOne({ _id }, updateUser); - - if (userData.sendWelcomeEmail) { - await _sendUserEmail(settings.get('Accounts_UserAddedEmail_Subject'), html, userData); - } - - if (sendPassword) { - await _sendUserEmail(settings.get('Password_Changed_Email_Subject'), passwordChangedHtml, userData); - } - - userData._id = _id; - - if (settings.get('Accounts_SetDefaultAvatar') === true && userData.email) { - const gravatarUrl = Gravatar.url(userData.email, { - default: '404', - size: '200', - protocol: 'https', - }); - - try { - await setUserAvatar(userData, gravatarUrl, '', 'url'); - } catch (e) { - // Ignore this error for now, as it not being successful isn't bad - } - } - - void notifyOnUserChangeById({ clientAction: 'inserted', id: _id }); - - return _id; -}; - -export const saveUser = async function (userId, userData) { - const oldUserData = userData._id && (await Users.findOneById(userData._id)); - if (oldUserData && isUserFederated(oldUserData)) { - throw new Meteor.Error('Edit_Federated_User_Not_Allowed', 'Not possible to edit a federated user'); - } - - await validateUserData(userId, userData); - - await callbacks.run('beforeSaveUser', { - user: userData, - oldUser: oldUserData, - }); - - let sendPassword = false; - - if (userData.hasOwnProperty('setRandomPassword')) { - if (userData.setRandomPassword) { - userData.password = generatePassword(); - userData.requirePasswordChange = true; - sendPassword = true; - } - - delete userData.setRandomPassword; - } - - if (!userData._id) { - return saveNewUser(userData, sendPassword); - } - - await validateUserEditing(userId, userData); - - // update user - if (userData.hasOwnProperty('username') || userData.hasOwnProperty('name')) { - if ( - !(await saveUserIdentity({ - _id: userData._id, - username: userData.username, - name: userData.name, - updateUsernameInBackground: true, - })) - ) { - throw new Meteor.Error('error-could-not-save-identity', 'Could not save user identity', { - method: 'saveUser', - }); - } - } - - if (typeof userData.statusText === 'string') { - await setStatusText(userData._id, userData.statusText); - } - - if (userData.email) { - const shouldSendVerificationEmailToUser = userData.verified !== true; - await setEmail(userData._id, userData.email, shouldSendVerificationEmailToUser); - } - - if ( - userData.password && - userData.password.trim() && - (await hasPermissionAsync(userId, 'edit-other-user-password')) && - passwordPolicy.validate(userData.password) - ) { - await Accounts.setPasswordAsync(userData._id, userData.password.trim()); - } else { - sendPassword = false; - } - - const updateUser = { - $set: {}, - $unset: {}, - }; - - handleBio(updateUser, userData.bio); - handleNickname(updateUser, userData.nickname); - - if (userData.roles) { - updateUser.$set.roles = userData.roles; - } - if (userData.settings) { - updateUser.$set.settings = { preferences: userData.settings.preferences }; - } - - if (userData.language) { - updateUser.$set.language = userData.language; - } - - if (typeof userData.requirePasswordChange !== 'undefined') { - updateUser.$set.requirePasswordChange = userData.requirePasswordChange; - if (!userData.requirePasswordChange) { - updateUser.$unset.requirePasswordChangeReason = 1; - } - } - - if (typeof userData.verified === 'boolean') { - updateUser.$set['emails.0.verified'] = userData.verified; - } - - await Users.updateOne({ _id: userData._id }, updateUser); - - // App IPostUserUpdated event hook - const userUpdated = await Users.findOneById(userData._id); - - await callbacks.run('afterSaveUser', { - user: userUpdated, - oldUser: oldUserData, - }); - - await Apps.self?.triggerEvent(AppEvents.IPostUserUpdated, { - user: userUpdated, - previousUser: oldUserData, - performedBy: await safeGetMeteorUser(), - }); - - if (sendPassword) { - await _sendUserEmail(settings.get('Password_Changed_Email_Subject'), passwordChangedHtml, userData); - } - - if (typeof userData.verified === 'boolean') { - delete userData.verified; - } - void notifyOnUserChange({ - clientAction: 'updated', - id: userData._id, - diff: { - ...userData, - emails: userUpdated.emails, - }, - }); - - return true; -}; diff --git a/apps/meteor/app/lib/server/functions/saveUser/handleBio.ts b/apps/meteor/app/lib/server/functions/saveUser/handleBio.ts new file mode 100644 index 000000000000..1d2f572f5cd9 --- /dev/null +++ b/apps/meteor/app/lib/server/functions/saveUser/handleBio.ts @@ -0,0 +1,22 @@ +import { MeteorError } from '@rocket.chat/core-services'; +import type { DeepPartial, DeepWritable, IUser } from '@rocket.chat/core-typings'; +import type { UpdateFilter } from 'mongodb'; + +import type { SaveUserData } from './saveUser'; + +const MAX_BIO_LENGTH = 260; + +export const handleBio = (updateUser: DeepWritable>>, bio: SaveUserData['bio']) => { + if (bio?.trim()) { + if (bio.length > MAX_BIO_LENGTH) { + throw new MeteorError('error-bio-size-exceeded', `Bio size exceeds ${MAX_BIO_LENGTH} characters`, { + method: 'saveUserProfile', + }); + } + updateUser.$set = updateUser.$set || {}; + updateUser.$set.bio = bio; + } else { + updateUser.$unset = updateUser.$unset || {}; + updateUser.$unset.bio = 1; + } +}; diff --git a/apps/meteor/app/lib/server/functions/saveUser/handleNickname.ts b/apps/meteor/app/lib/server/functions/saveUser/handleNickname.ts new file mode 100644 index 000000000000..4a37ec9e1518 --- /dev/null +++ b/apps/meteor/app/lib/server/functions/saveUser/handleNickname.ts @@ -0,0 +1,22 @@ +import { MeteorError } from '@rocket.chat/core-services'; +import type { DeepPartial, DeepWritable, IUser } from '@rocket.chat/core-typings'; +import type { UpdateFilter } from 'mongodb'; + +import type { SaveUserData } from './saveUser'; + +const MAX_NICKNAME_LENGTH = 120; + +export const handleNickname = (updateUser: DeepWritable>>, nickname: SaveUserData['nickname']) => { + if (nickname?.trim()) { + if (nickname.length > MAX_NICKNAME_LENGTH) { + throw new MeteorError('error-nickname-size-exceeded', `Nickname size exceeds ${MAX_NICKNAME_LENGTH} characters`, { + method: 'saveUserProfile', + }); + } + updateUser.$set = updateUser.$set || {}; + updateUser.$set.nickname = nickname; + } else { + updateUser.$unset = updateUser.$unset || {}; + updateUser.$unset.nickname = 1; + } +}; diff --git a/apps/meteor/app/lib/server/functions/saveUser/index.ts b/apps/meteor/app/lib/server/functions/saveUser/index.ts new file mode 100644 index 000000000000..3fd0668e6e47 --- /dev/null +++ b/apps/meteor/app/lib/server/functions/saveUser/index.ts @@ -0,0 +1,2 @@ +export { saveUser } from './saveUser'; +export { validateUserEditing } from './validateUserEditing'; diff --git a/apps/meteor/app/lib/server/functions/saveUser/saveNewUser.ts b/apps/meteor/app/lib/server/functions/saveUser/saveNewUser.ts new file mode 100644 index 000000000000..18e2858e81c0 --- /dev/null +++ b/apps/meteor/app/lib/server/functions/saveUser/saveNewUser.ts @@ -0,0 +1,84 @@ +import type { DeepPartial, DeepWritable, IUser, RequiredField } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; +import Gravatar from 'gravatar'; +import type { UpdateFilter } from 'mongodb'; + +import { getNewUserRoles } from '../../../../../server/services/user/lib/getNewUserRoles'; +import { settings } from '../../../../settings/server'; +import { notifyOnUserChangeById } from '../../lib/notifyListener'; +import { validateEmailDomain } from '../../lib/validateEmailDomain'; +import { setUserAvatar } from '../setUserAvatar'; +import { handleBio } from './handleBio'; +import { handleNickname } from './handleNickname'; +import type { SaveUserData } from './saveUser'; +import { sendPasswordEmail, sendWelcomeEmail } from './sendUserEmail'; + +export const saveNewUser = async function (userData: SaveUserData, sendPassword: boolean) { + await validateEmailDomain(userData.email); + + const roles = (!!userData.roles && userData.roles.length > 0 && userData.roles) || getNewUserRoles(); + const isGuest = roles && roles.length === 1 && roles.includes('guest'); + + // insert user + const createUser: Record = { + username: userData.username, + password: userData.password, + joinDefaultChannels: userData.joinDefaultChannels, + isGuest, + globalRoles: roles, + skipNewUserRolesSetting: true, + }; + if (userData.email) { + createUser.email = userData.email; + } + + const _id = await Accounts.createUserAsync(createUser); + + const updateUser: RequiredField>>, '$set'> = { + $set: { + ...(typeof userData.name !== 'undefined' && { name: userData.name }), + settings: userData.settings || {}, + }, + }; + + if (typeof userData.requirePasswordChange !== 'undefined') { + updateUser.$set.requirePasswordChange = userData.requirePasswordChange; + } + + if (typeof userData.verified === 'boolean') { + updateUser.$set['emails.0.verified'] = userData.verified; + } + + handleBio(updateUser, userData.bio); + handleNickname(updateUser, userData.nickname); + + await Users.updateOne({ _id }, updateUser as UpdateFilter); + + if (userData.sendWelcomeEmail) { + await sendWelcomeEmail(userData); + } + + if (sendPassword) { + await sendPasswordEmail(userData); + } + + userData._id = _id; + + if (settings.get('Accounts_SetDefaultAvatar') === true && userData.email) { + const gravatarUrl = Gravatar.url(userData.email, { + default: '404', + size: '200', + protocol: 'https', + }); + + try { + await setUserAvatar({ ...userData, _id }, gravatarUrl, '', 'url'); + } catch (e) { + // Ignore this error for now, as it not being successful isn't bad + } + } + + void notifyOnUserChangeById({ clientAction: 'inserted', id: _id }); + + return _id; +}; diff --git a/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts b/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts new file mode 100644 index 000000000000..ce9624c60444 --- /dev/null +++ b/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts @@ -0,0 +1,179 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; +import { isUserFederated } from '@rocket.chat/core-typings'; +import type { DeepWritable, DeepPartial, IUser, IRole, IUserSettings, RequiredField } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; +import { Accounts } from 'meteor/accounts-base'; +import { Meteor } from 'meteor/meteor'; +import type { UpdateFilter } from 'mongodb'; + +import { callbacks } from '../../../../../lib/callbacks'; +import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { safeGetMeteorUser } from '../../../../utils/server/functions/safeGetMeteorUser'; +import { generatePassword } from '../../lib/generatePassword'; +import { notifyOnUserChange } from '../../lib/notifyListener'; +import { passwordPolicy } from '../../lib/passwordPolicy'; +import { saveUserIdentity } from '../saveUserIdentity'; +import { setEmail } from '../setEmail'; +import { setStatusText } from '../setStatusText'; +import { handleBio } from './handleBio'; +import { handleNickname } from './handleNickname'; +import { saveNewUser } from './saveNewUser'; +import { sendPasswordEmail } from './sendUserEmail'; +import { validateUserData } from './validateUserData'; +import { validateUserEditing } from './validateUserEditing'; + +export type SaveUserData = { + _id?: IUser['_id']; + setRandomPassword?: boolean; + + password?: string; + requirePasswordChange?: boolean; + + username?: string; + name?: string; + + statusText?: string; + email?: string; + verified?: boolean; + + bio?: string; + nickname?: string; + + roles?: IRole['_id'][]; + settings?: Partial; + language?: string; + + joinDefaultChannels?: boolean; + sendWelcomeEmail?: boolean; +}; + +export const saveUser = async function (userId: IUser['_id'], userData: SaveUserData) { + const oldUserData = userData._id && (await Users.findOneById(userData._id)); + if (oldUserData && isUserFederated(oldUserData)) { + throw new Meteor.Error('Edit_Federated_User_Not_Allowed', 'Not possible to edit a federated user'); + } + + await validateUserData(userId, userData); + + await callbacks.run('beforeSaveUser', { + user: userData, + oldUser: oldUserData, + }); + + let sendPassword = false; + + if (userData.hasOwnProperty('setRandomPassword')) { + if (userData.setRandomPassword) { + userData.password = generatePassword(); + userData.requirePasswordChange = true; + sendPassword = true; + } + + delete userData.setRandomPassword; + } + + if (!userData._id) { + return saveNewUser(userData, sendPassword); + } + + await validateUserEditing(userId, userData as RequiredField); + + // update user + if (userData.hasOwnProperty('username') || userData.hasOwnProperty('name')) { + if ( + !(await saveUserIdentity({ + _id: userData._id, + username: userData.username, + name: userData.name, + updateUsernameInBackground: true, + })) + ) { + throw new Meteor.Error('error-could-not-save-identity', 'Could not save user identity', { + method: 'saveUser', + }); + } + } + + if (typeof userData.statusText === 'string') { + await setStatusText(userData._id, userData.statusText); + } + + if (userData.email) { + const shouldSendVerificationEmailToUser = userData.verified !== true; + await setEmail(userData._id, userData.email, shouldSendVerificationEmailToUser); + } + + if ( + userData.password?.trim() && + (await hasPermissionAsync(userId, 'edit-other-user-password')) && + passwordPolicy.validate(userData.password) + ) { + await Accounts.setPasswordAsync(userData._id, userData.password.trim()); + } else { + sendPassword = false; + } + + const updateUser: RequiredField>>, '$set' | '$unset'> = { + $set: {}, + $unset: {}, + }; + + handleBio(updateUser, userData.bio); + handleNickname(updateUser, userData.nickname); + + if (userData.roles) { + updateUser.$set.roles = userData.roles; + } + if (userData.settings) { + updateUser.$set.settings = { preferences: userData.settings.preferences }; + } + + if (userData.language) { + updateUser.$set.language = userData.language; + } + + if (typeof userData.requirePasswordChange !== 'undefined') { + updateUser.$set.requirePasswordChange = userData.requirePasswordChange; + if (!userData.requirePasswordChange) { + updateUser.$unset.requirePasswordChangeReason = 1; + } + } + + if (typeof userData.verified === 'boolean') { + updateUser.$set['emails.0.verified'] = userData.verified; + } + + await Users.updateOne({ _id: userData._id }, updateUser as UpdateFilter); + + // App IPostUserUpdated event hook + const userUpdated = await Users.findOneById(userData._id); + + await callbacks.run('afterSaveUser', { + user: userUpdated, + oldUser: oldUserData, + }); + + await Apps.self?.triggerEvent(AppEvents.IPostUserUpdated, { + user: userUpdated, + previousUser: oldUserData, + performedBy: await safeGetMeteorUser(), + }); + + if (sendPassword) { + await sendPasswordEmail(userData); + } + + if (typeof userData.verified === 'boolean') { + delete userData.verified; + } + void notifyOnUserChange({ + clientAction: 'updated', + id: userData._id, + diff: { + ...userData, + emails: userUpdated?.emails, + }, + }); + + return true; +}; diff --git a/apps/meteor/app/lib/server/functions/saveUser/sendUserEmail.ts b/apps/meteor/app/lib/server/functions/saveUser/sendUserEmail.ts new file mode 100644 index 000000000000..cba836cdbf9c --- /dev/null +++ b/apps/meteor/app/lib/server/functions/saveUser/sendUserEmail.ts @@ -0,0 +1,54 @@ +import { MeteorError } from '@rocket.chat/core-services'; + +import type { SaveUserData } from './saveUser'; +import * as Mailer from '../../../../mailer/server/api'; +import { settings } from '../../../../settings/server'; + +let html = ''; +let passwordChangedHtml = ''; +Meteor.startup(() => { + Mailer.getTemplate('Accounts_UserAddedEmail_Email', (template) => { + html = template; + }); + + Mailer.getTemplate('Password_Changed_Email', (template) => { + passwordChangedHtml = template; + }); +}); + +export async function sendUserEmail(subject: string, html: string, userData: SaveUserData): Promise { + if (!userData.email) { + return; + } + + const email = { + to: userData.email, + from: settings.get('From_Email'), + subject, + html, + data: { + email: userData.email, + password: userData.password, + ...(typeof userData.name !== 'undefined' ? { name: userData.name } : {}), + }, + }; + + try { + await Mailer.send(email); + } catch (error) { + const errorMessage = typeof error === 'object' && error && 'message' in error ? error.message : ''; + + throw new MeteorError('error-email-send-failed', `Error trying to send email: ${errorMessage}`, { + function: 'RocketChat.saveUser', + message: errorMessage, + }); + } +} + +export async function sendWelcomeEmail(userData: SaveUserData) { + return sendUserEmail(settings.get('Accounts_UserAddedEmail_Subject'), html, userData); +} + +export async function sendPasswordEmail(userData: SaveUserData) { + return sendUserEmail(settings.get('Password_Changed_Email_Subject'), passwordChangedHtml, userData); +} diff --git a/apps/meteor/app/lib/server/functions/saveUser/validateUserData.ts b/apps/meteor/app/lib/server/functions/saveUser/validateUserData.ts new file mode 100644 index 000000000000..52652a6c47b4 --- /dev/null +++ b/apps/meteor/app/lib/server/functions/saveUser/validateUserData.ts @@ -0,0 +1,107 @@ +import { MeteorError } from '@rocket.chat/core-services'; +import type { IUser } from '@rocket.chat/core-typings'; +import { makeFunction } from '@rocket.chat/patch-injection'; +import escape from 'lodash.escape'; + +import { trim } from '../../../../../lib/utils/stringUtils'; +import { getRoleIds } from '../../../../authorization/server/functions/getRoles'; +import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { settings } from '../../../../settings/server'; +import { checkEmailAvailability } from '../checkEmailAvailability'; +import { checkUsernameAvailability } from '../checkUsernameAvailability'; +import type { SaveUserData } from './saveUser'; + +export const validateUserData = makeFunction(async (userId: IUser['_id'], userData: SaveUserData): Promise => { + const existingRoles = await getRoleIds(); + + if (userData.verified && userData._id && userId === userData._id) { + throw new MeteorError('error-action-not-allowed', 'Editing email verification is not allowed', { + method: 'insertOrUpdateUser', + action: 'Editing_user', + }); + } + + if (userData._id && userId !== userData._id && !(await hasPermissionAsync(userId, 'edit-other-user-info'))) { + throw new MeteorError('error-action-not-allowed', 'Editing user is not allowed', { + method: 'insertOrUpdateUser', + action: 'Editing_user', + }); + } + + if (!userData._id && !(await hasPermissionAsync(userId, 'create-user'))) { + throw new MeteorError('error-action-not-allowed', 'Adding user is not allowed', { + method: 'insertOrUpdateUser', + action: 'Adding_user', + }); + } + + if (userData.roles) { + const newRoles = userData.roles.filter((roleId) => !existingRoles.includes(roleId)); + if (newRoles.length > 0) { + throw new MeteorError('error-action-not-allowed', 'The field Roles consist invalid role id', { + method: 'insertOrUpdateUser', + action: 'Assign_role', + }); + } + } + + if (userData.roles?.includes('admin') && !(await hasPermissionAsync(userId, 'assign-admin-role'))) { + throw new MeteorError('error-action-not-allowed', 'Assigning admin is not allowed', { + method: 'insertOrUpdateUser', + action: 'Assign_admin', + }); + } + + if (settings.get('Accounts_RequireNameForSignUp') && !userData._id && !trim(userData.name)) { + throw new MeteorError('error-the-field-is-required', 'The field Name is required', { + method: 'insertOrUpdateUser', + field: 'Name', + }); + } + + if (!userData._id && !trim(userData.username)) { + throw new MeteorError('error-the-field-is-required', 'The field Username is required', { + method: 'insertOrUpdateUser', + field: 'Username', + }); + } + + let nameValidation; + + try { + nameValidation = new RegExp(`^${settings.get('UTF8_User_Names_Validation')}$`); + } catch (e) { + nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); + } + + if (userData.username && !nameValidation.test(userData.username)) { + throw new MeteorError('error-input-is-not-a-valid-field', `${escape(userData.username)} is not a valid username`, { + method: 'insertOrUpdateUser', + input: userData.username, + field: 'Username', + }); + } + + if (!userData._id && !userData.password && !userData.setRandomPassword) { + throw new MeteorError('error-the-field-is-required', 'The field Password is required', { + method: 'insertOrUpdateUser', + field: 'Password', + }); + } + + if (!userData._id) { + if (userData.username && !(await checkUsernameAvailability(userData.username))) { + throw new MeteorError('error-field-unavailable', `${escape(userData.username)} is already in use :(`, { + method: 'insertOrUpdateUser', + field: userData.username, + }); + } + + if (userData.email && !(await checkEmailAvailability(userData.email))) { + throw new MeteorError('error-field-unavailable', `${escape(userData.email)} is already in use :(`, { + method: 'insertOrUpdateUser', + field: userData.email, + }); + } + } +}); diff --git a/apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts b/apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts new file mode 100644 index 000000000000..fc151204fba1 --- /dev/null +++ b/apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts @@ -0,0 +1,96 @@ +import { MeteorError } from '@rocket.chat/core-services'; +import type { IUser, RequiredField } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; + +import type { SaveUserData } from './saveUser'; +import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { settings } from '../../../../settings/server'; + +const isEditingUserRoles = (previousRoles: IUser['roles'], newRoles?: IUser['roles']) => + newRoles !== undefined && + (newRoles.some((item) => !previousRoles.includes(item)) || previousRoles.some((item) => !newRoles.includes(item))); +const isEditingField = (previousValue?: string, newValue?: string) => typeof newValue !== 'undefined' && newValue !== previousValue; + +/** + * Validate permissions to edit user fields + * + * @param {string} userId + * @param {{ _id: string, roles?: string[], username?: string, name?: string, statusText?: string, email?: string, password?: string}} userData + */ +export async function validateUserEditing(userId: IUser['_id'], userData: RequiredField): Promise { + const editingMyself = userData._id && userId === userData._id; + + const canEditOtherUserInfo = await hasPermissionAsync(userId, 'edit-other-user-info'); + const canEditOtherUserPassword = await hasPermissionAsync(userId, 'edit-other-user-password'); + const user = await Users.findOneById(userData._id); + + if (!user) { + throw new MeteorError('error-invalid-user', 'Invalid user'); + } + + if (isEditingUserRoles(user.roles, userData.roles) && !(await hasPermissionAsync(userId, 'assign-roles'))) { + throw new MeteorError('error-action-not-allowed', 'Assign roles is not allowed', { + method: 'insertOrUpdateUser', + action: 'Assign_role', + }); + } + + if (!settings.get('Accounts_AllowUserProfileChange') && !canEditOtherUserInfo && !canEditOtherUserPassword) { + throw new MeteorError('error-action-not-allowed', 'Edit user profile is not allowed', { + method: 'insertOrUpdateUser', + action: 'Update_user', + }); + } + + if ( + isEditingField(user.username, userData.username) && + !settings.get('Accounts_AllowUsernameChange') && + (!canEditOtherUserInfo || editingMyself) + ) { + throw new MeteorError('error-action-not-allowed', 'Edit username is not allowed', { + method: 'insertOrUpdateUser', + action: 'Update_user', + }); + } + + if ( + isEditingField(user.statusText, userData.statusText) && + !settings.get('Accounts_AllowUserStatusMessageChange') && + (!canEditOtherUserInfo || editingMyself) + ) { + throw new MeteorError('error-action-not-allowed', 'Edit user status is not allowed', { + method: 'insertOrUpdateUser', + action: 'Update_user', + }); + } + + if ( + isEditingField(user.name, userData.name) && + !settings.get('Accounts_AllowRealNameChange') && + (!canEditOtherUserInfo || editingMyself) + ) { + throw new MeteorError('error-action-not-allowed', 'Edit user real name is not allowed', { + method: 'insertOrUpdateUser', + action: 'Update_user', + }); + } + + if ( + user.emails?.[0] && + isEditingField(user.emails[0].address, userData.email) && + !settings.get('Accounts_AllowEmailChange') && + (!canEditOtherUserInfo || editingMyself) + ) { + throw new MeteorError('error-action-not-allowed', 'Edit user email is not allowed', { + method: 'insertOrUpdateUser', + action: 'Update_user', + }); + } + + if (userData.password && !settings.get('Accounts_AllowPasswordChange') && (!canEditOtherUserPassword || editingMyself)) { + throw new MeteorError('error-action-not-allowed', 'Edit user password is not allowed', { + method: 'insertOrUpdateUser', + action: 'Update_user', + }); + } +} diff --git a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts index 1729a1ba8abd..7a200a029725 100644 --- a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts +++ b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts @@ -1,6 +1,10 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Messages, VideoConference, LivechatDepartmentAgents, Rooms, Subscriptions, Users } from '@rocket.chat/models'; +import { _setRealName } from './setRealName'; +import { _setUsername } from './setUsername'; +import { updateGroupDMsName } from './updateGroupDMsName'; +import { validateName } from './validateName'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { FileUpload } from '../../../file-upload/server'; import { @@ -8,10 +12,6 @@ import { notifyOnSubscriptionChangedByUserId, notifyOnSubscriptionChangedByNameAndRoomType, } from '../lib/notifyListener'; -import { _setRealName } from './setRealName'; -import { _setUsername } from './setUsername'; -import { updateGroupDMsName } from './updateGroupDMsName'; -import { validateName } from './validateName'; /** * diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index aba5ddb7264c..828de8451a21 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -4,6 +4,7 @@ import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import { Messages } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; +import { parseUrlsInMessage } from './parseUrlsInMessage'; import { isRelativeURL } from '../../../../lib/utils/isRelativeURL'; import { isURL } from '../../../../lib/utils/isURL'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; @@ -12,7 +13,6 @@ import { settings } from '../../../settings/server'; import { afterSaveMessage } from '../lib/afterSaveMessage'; import { notifyOnRoomChangedById, notifyOnMessageChange } from '../lib/notifyListener'; import { validateCustomMessageFields } from '../lib/validateCustomMessageFields'; -import { parseUrlsInMessage } from './parseUrlsInMessage'; // TODO: most of the types here are wrong, but I don't want to change them now diff --git a/apps/meteor/app/lib/server/functions/setRoomAvatar.ts b/apps/meteor/app/lib/server/functions/setRoomAvatar.ts index 42fc615f09db..8a3467bdc4dc 100644 --- a/apps/meteor/app/lib/server/functions/setRoomAvatar.ts +++ b/apps/meteor/app/lib/server/functions/setRoomAvatar.ts @@ -4,8 +4,8 @@ import { isRegisterUser } from '@rocket.chat/core-typings'; import { Avatars, Rooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { FileUpload } from '../../../file-upload/server'; import { RocketChatFile } from '../../../file/server'; +import { FileUpload } from '../../../file-upload/server'; export const setRoomAvatar = async function (rid: string, dataURI: string, user: IUser): Promise { if (!isRegisterUser(user)) { diff --git a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts index 929c24210d2d..466a5862853d 100644 --- a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts +++ b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts @@ -6,6 +6,10 @@ import { Accounts } from 'meteor/accounts-base'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { closeOmnichannelConversations } from './closeOmnichannelConversations'; +import { shouldRemoveOrChangeOwner, getSubscribedRoomsForUserWithDetails } from './getRoomsWithSingleOwner'; +import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; +import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; import { callbacks } from '../../../../lib/callbacks'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; @@ -15,10 +19,6 @@ import { notifyOnSubscriptionChangedByNameAndRoomType, notifyOnUserChange, } from '../lib/notifyListener'; -import { closeOmnichannelConversations } from './closeOmnichannelConversations'; -import { shouldRemoveOrChangeOwner, getSubscribedRoomsForUserWithDetails } from './getRoomsWithSingleOwner'; -import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; -import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; async function reactivateDirectConversations(userId: string) { // since both users can be deactivated at the same time, we should just reactivate rooms if both users are active diff --git a/apps/meteor/app/lib/server/functions/setUserAvatar.ts b/apps/meteor/app/lib/server/functions/setUserAvatar.ts index 13ccd2de6954..c678b8a07e22 100644 --- a/apps/meteor/app/lib/server/functions/setUserAvatar.ts +++ b/apps/meteor/app/lib/server/functions/setUserAvatar.ts @@ -5,12 +5,12 @@ import type { Response } from '@rocket.chat/server-fetch'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { Meteor } from 'meteor/meteor'; +import { checkUrlForSsrf } from './checkUrlForSsrf'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { FileUpload } from '../../../file-upload/server'; import { RocketChatFile } from '../../../file/server'; +import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; -import { checkUrlForSsrf } from './checkUrlForSsrf'; export const setAvatarFromServiceWithValidation = async ( userId: string, diff --git a/apps/meteor/app/lib/server/functions/setUsername.ts b/apps/meteor/app/lib/server/functions/setUsername.ts index c4d2c47c6d9d..57591d2ed2c9 100644 --- a/apps/meteor/app/lib/server/functions/setUsername.ts +++ b/apps/meteor/app/lib/server/functions/setUsername.ts @@ -5,12 +5,9 @@ import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; -import { callbacks } from '../../../../lib/callbacks'; -import { SystemLogger } from '../../../../server/lib/logger/system'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { settings } from '../../../settings/server'; import { RateLimiter } from '../lib'; -import { notifyOnUserChange } from '../lib/notifyListener'; import { addUserToRoom } from './addUserToRoom'; import { checkUsernameAvailability } from './checkUsernameAvailability'; import { getAvatarSuggestionForUser } from './getAvatarSuggestionForUser'; @@ -18,6 +15,9 @@ import { joinDefaultChannels } from './joinDefaultChannels'; import { saveUserIdentity } from './saveUserIdentity'; import { setUserAvatar } from './setUserAvatar'; import { validateUsername } from './validateUsername'; +import { callbacks } from '../../../../lib/callbacks'; +import { SystemLogger } from '../../../../server/lib/logger/system'; +import { notifyOnUserChange } from '../lib/notifyListener'; export const setUsernameWithValidation = async (userId: string, username: string, joinDefaultChannelsSilenced?: boolean): Promise => { if (!username) { diff --git a/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts b/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts index feb26ce6a1b0..ac204af51439 100644 --- a/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts +++ b/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts @@ -8,14 +8,14 @@ const getName = (members: IUser[]): string => members.map(({ username }) => user async function getUsersWhoAreInTheSameGroupDMsAs(user: IUser) { // add all users to single array so we can fetch details from them all at once - const rooms = Rooms.findGroupDMsByUids([user._id], { projection: { uids: 1 } }); - if ((await rooms.count()) === 0) { + if ((await Rooms.countGroupDMsByUids([user._id])) === 0) { return; } const userIds = new Set(); const users = new Map(); + const rooms = Rooms.findGroupDMsByUids([user._id], { projection: { uids: 1 } }); await rooms.forEach((room) => { if (!room.uids) { return; diff --git a/apps/meteor/app/lib/server/functions/updateMessage.ts b/apps/meteor/app/lib/server/functions/updateMessage.ts index 96683d40348f..d480a21bd713 100644 --- a/apps/meteor/app/lib/server/functions/updateMessage.ts +++ b/apps/meteor/app/lib/server/functions/updateMessage.ts @@ -4,11 +4,11 @@ import type { IMessage, IUser, AtLeast } from '@rocket.chat/core-typings'; import { Messages, Rooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; +import { parseUrlsInMessage } from './parseUrlsInMessage'; import { settings } from '../../../settings/server'; import { afterSaveMessage } from '../lib/afterSaveMessage'; import { notifyOnRoomChangedById, notifyOnMessageChange } from '../lib/notifyListener'; import { validateCustomMessageFields } from '../lib/validateCustomMessageFields'; -import { parseUrlsInMessage } from './parseUrlsInMessage'; export const updateMessage = async function ( message: AtLeast, diff --git a/apps/meteor/app/lib/server/lib/interceptDirectReplyEmails.js b/apps/meteor/app/lib/server/lib/interceptDirectReplyEmails.js index f9af0911bcc6..77aaa03887c5 100644 --- a/apps/meteor/app/lib/server/lib/interceptDirectReplyEmails.js +++ b/apps/meteor/app/lib/server/lib/interceptDirectReplyEmails.js @@ -1,9 +1,9 @@ import POP3Lib from '@rocket.chat/poplib'; import { simpleParser } from 'mailparser'; +import { processDirectEmail } from './processDirectEmail'; import { IMAPInterceptor } from '../../../../server/email/IMAPInterceptor'; import { settings } from '../../../settings/server'; -import { processDirectEmail } from './processDirectEmail'; export class DirectReplyIMAPInterceptor extends IMAPInterceptor { constructor(imapConfig, options = {}) { @@ -127,10 +127,13 @@ export class POP3Helper { start() { this.log('POP3 started'); - this.running = setInterval(() => { - // get new emails and process - this.POP3 = new POP3Intercepter(); - }, Math.max(this.frequency * 60 * 1000, 2 * 60 * 1000)); + this.running = setInterval( + () => { + // get new emails and process + this.POP3 = new POP3Intercepter(); + }, + Math.max(this.frequency * 60 * 1000, 2 * 60 * 1000), + ); } log(...args) { diff --git a/apps/meteor/app/lib/server/lib/notifyListener.ts b/apps/meteor/app/lib/server/lib/notifyListener.ts index 778fe89dbbf4..6a28fabe5c51 100644 --- a/apps/meteor/app/lib/server/lib/notifyListener.ts +++ b/apps/meteor/app/lib/server/lib/notifyListener.ts @@ -21,9 +21,11 @@ import type { IMessage, SettingValue, MessageTypesValues, + ILivechatContact, } from '@rocket.chat/core-typings'; import { Rooms, + LivechatRooms, Permissions, Settings, PbxEvents, @@ -87,6 +89,16 @@ export const notifyOnRoomChangedByUsernamesOrUids = withDbWatcherCheck( }, ); +export const notifyOnRoomChangedByContactId = withDbWatcherCheck( + async (contactId: T['_id'], clientAction: ClientAction = 'updated'): Promise => { + const cursor = LivechatRooms.findOpenByContactId(contactId); + + void cursor.forEach((room) => { + void api.broadcast('watch.rooms', { clientAction, room }); + }); + }, +); + export const notifyOnRoomChangedByUserDM = withDbWatcherCheck( async (userId: T['u']['_id'], clientAction: ClientAction = 'updated'): Promise => { const items = Rooms.findDMsByUids([userId]); @@ -251,6 +263,20 @@ export const notifyOnLivechatInquiryChangedById = withDbWatcherCheck( }, ); +export const notifyOnLivechatInquiryChangedByVisitorIds = withDbWatcherCheck( + async ( + visitorIds: ILivechatInquiryRecord['v']['_id'][], + clientAction: Exclude = 'updated', + diff?: Partial & { queuedAt: Date; takenAt: Date }>, + ): Promise => { + const cursor = LivechatInquiry.findByVisitorIds(visitorIds); + + void cursor.forEach((inquiry) => { + void api.broadcast('watch.inquiries', { clientAction, inquiry, diff }); + }); + }, +); + export const notifyOnLivechatInquiryChangedByRoom = withDbWatcherCheck( async ( rid: ILivechatInquiryRecord['rid'], @@ -553,6 +579,19 @@ export const notifyOnSubscriptionChangedByUserIdAndRoomType = withDbWatcherCheck }, ); +export const notifyOnSubscriptionChangedByVisitorIds = withDbWatcherCheck( + async ( + visitorIds: Exclude['_id'][], + clientAction: Exclude = 'updated', + ): Promise => { + const cursor = Subscriptions.findOpenByVisitorIds(visitorIds, { projection: subscriptionFields }); + + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); + }, +); + export const notifyOnSubscriptionChangedByNameAndRoomType = withDbWatcherCheck( async (filter: Partial>, clientAction: Exclude = 'updated'): Promise => { const cursor = Subscriptions.findByNameAndRoomType(filter, { projection: subscriptionFields }); diff --git a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts index fdb99c83207c..ac8587c0717a 100644 --- a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts +++ b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts @@ -2,25 +2,16 @@ import type { IMessage, IRoom, IUser, RoomType } from '@rocket.chat/core-typings import { isEditedMessage } from '@rocket.chat/core-typings'; import type { Updater } from '@rocket.chat/models'; import { Subscriptions, Rooms } from '@rocket.chat/models'; -import { escapeRegExp } from '@rocket.chat/string-helpers'; import moment from 'moment'; -import { callbacks } from '../../../../lib/callbacks'; -import { settings } from '../../../settings/server'; import { notifyOnSubscriptionChanged, notifyOnSubscriptionChangedByRoomIdAndUserId, notifyOnSubscriptionChangedByRoomIdAndUserIds, } from './notifyListener'; - -function messageContainsHighlight(message: IMessage, highlights: string[]): boolean { - if (!highlights || highlights.length === 0) return false; - - return highlights.some((highlight: string) => { - const regexp = new RegExp(escapeRegExp(highlight), 'i'); - return regexp.test(message.msg); - }); -} +import { callbacks } from '../../../../lib/callbacks'; +import { settings } from '../../../settings/server'; +import { messageContainsHighlight } from '../functions/notifications/messageContainsHighlight'; export async function getMentions(message: IMessage): Promise<{ toAll: boolean; toHere: boolean; mentionIds: string[] }> { const { diff --git a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts index 94c25f476222..25330d912aaf 100644 --- a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts +++ b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts @@ -12,16 +12,17 @@ import emojione from 'emojione'; import moment from 'moment'; import type { RootFilterOperators } from 'mongodb'; +import { getMentions } from './notifyUsersOnMessage'; import { callbacks } from '../../../../lib/callbacks'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { Notification } from '../../../notification-queue/server/NotificationQueue'; import { settings } from '../../../settings/server'; -import { messageContainsHighlight, parseMessageTextPerUser, replaceMentionedUsernamesWithFullNames } from '../functions/notifications'; +import { parseMessageTextPerUser, replaceMentionedUsernamesWithFullNames } from '../functions/notifications'; import { notifyDesktopUser, shouldNotifyDesktop } from '../functions/notifications/desktop'; import { getEmailData, shouldNotifyEmail } from '../functions/notifications/email'; +import { messageContainsHighlight } from '../functions/notifications/messageContainsHighlight'; import { getPushData, shouldNotifyMobile } from '../functions/notifications/mobile'; -import { getMentions } from './notifyUsersOnMessage'; type SubscriptionAggregation = { receiver: [Pick | null]; diff --git a/apps/meteor/app/lib/server/lib/validateEmailDomain.js b/apps/meteor/app/lib/server/lib/validateEmailDomain.js index 3862dad83fef..898a3e6b8942 100644 --- a/apps/meteor/app/lib/server/lib/validateEmailDomain.js +++ b/apps/meteor/app/lib/server/lib/validateEmailDomain.js @@ -3,9 +3,9 @@ import util from 'util'; import { Meteor } from 'meteor/meteor'; +import { emailDomainDefaultBlackList } from './defaultBlockedDomainsList'; import { validateEmail } from '../../../../lib/emailValidator'; import { settings } from '../../../settings/server'; -import { emailDomainDefaultBlackList } from './defaultBlockedDomainsList'; const dnsResolveMx = util.promisify(dns.resolveMx); diff --git a/apps/meteor/app/lib/server/methods/getChannelHistory.ts b/apps/meteor/app/lib/server/methods/getChannelHistory.ts index 8f1f4c586141..8fe5812dd62a 100644 --- a/apps/meteor/app/lib/server/methods/getChannelHistory.ts +++ b/apps/meteor/app/lib/server/methods/getChannelHistory.ts @@ -3,7 +3,6 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Messages, Subscriptions, Rooms } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import _ from 'underscore'; import { canAccessRoomAsync } from '../../../authorization/server'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; @@ -64,7 +63,8 @@ Meteor.methods({ } // Verify oldest is a date if it exists - if (oldest !== undefined && !_.isDate(oldest)) { + + if (oldest !== undefined && {}.toString.call(oldest) !== '[object Date]') { throw new Meteor.Error('error-invalid-date', 'Invalid date', { method: 'getChannelHistory' }); } @@ -89,7 +89,7 @@ Meteor.methods({ options, showThreadMessages, inclusive, - ).toArray() + ).toArray() : await Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes( rid, oldest, @@ -98,7 +98,7 @@ Meteor.methods({ options, showThreadMessages, inclusive, - ).toArray(); + ).toArray(); const messages = await normalizeMessagesForUser(records, fromUserId); diff --git a/apps/meteor/app/lib/server/methods/insertOrUpdateUser.ts b/apps/meteor/app/lib/server/methods/insertOrUpdateUser.ts index 122b11172d57..3c5f07f624b3 100644 --- a/apps/meteor/app/lib/server/methods/insertOrUpdateUser.ts +++ b/apps/meteor/app/lib/server/methods/insertOrUpdateUser.ts @@ -19,12 +19,14 @@ Meteor.methods({ check(userData, Object); - if (!Meteor.userId()) { + const userId = Meteor.userId(); + + if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'insertOrUpdateUser', }); } - return saveUser(Meteor.userId(), userData); + return saveUser(userId, userData); }), }); diff --git a/apps/meteor/app/lib/server/methods/leaveRoom.ts b/apps/meteor/app/lib/server/methods/leaveRoom.ts index 4fc85b35fd05..8ded8b56ce2b 100644 --- a/apps/meteor/app/lib/server/methods/leaveRoom.ts +++ b/apps/meteor/app/lib/server/methods/leaveRoom.ts @@ -45,8 +45,7 @@ export const leaveRoomMethod = async (user: IUser, rid: string): Promise = // If user is room owner, check if there are other owners. If there isn't anyone else, warn user to set a new owner. if (await hasRoleAsync(user._id, 'owner', room._id)) { - const cursor = await Roles.findUsersInRole('owner', room._id); - const numOwners = await cursor.count(); + const numOwners = await Roles.countUsersInRole('owner', room._id); if (numOwners === 1) { throw new Meteor.Error('error-you-are-last-owner', 'You are the last owner. Please set new owner before leaving the room.', { method: 'leaveRoom', diff --git a/apps/meteor/app/lib/server/methods/saveSetting.ts b/apps/meteor/app/lib/server/methods/saveSetting.ts index 61f5fbbd34cc..ed48dc4e9f4f 100644 --- a/apps/meteor/app/lib/server/methods/saveSetting.ts +++ b/apps/meteor/app/lib/server/methods/saveSetting.ts @@ -4,6 +4,7 @@ import { Settings } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { updateAuditedByUser } from '../../../../server/settings/lib/auditedSettingUpdates'; import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; import { getSettingPermissionId } from '../../../authorization/lib'; import { hasPermissionAsync, hasAllPermissionAsync } from '../../../authorization/server/functions/hasPermission'; @@ -18,7 +19,7 @@ declare module '@rocket.chat/ddp-client' { } Meteor.methods({ - saveSetting: twoFactorRequired(async (_id, value, editor) => { + saveSetting: twoFactorRequired(async function (_id, value, editor) { const uid = Meteor.userId(); if (!uid) { throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', { @@ -66,7 +67,14 @@ Meteor.methods({ break; } - (await Settings.updateValueAndEditorById(_id, value as SettingValue, editor)).modifiedCount && + const auditSettingOperation = updateAuditedByUser({ + _id: uid, + username: (await Meteor.userAsync())!.username!, + ip: this.connection?.clientAddress || '', + useragent: this.connection?.httpHeaders['user-agent'] || '', + }); + + (await auditSettingOperation(Settings.updateValueAndEditorById, _id, value as SettingValue, editor)).modifiedCount && setting && void notifyOnSettingChanged({ ...setting, editor, value: value as SettingValue }); diff --git a/apps/meteor/app/lib/server/methods/saveSettings.ts b/apps/meteor/app/lib/server/methods/saveSettings.ts index 7a18bbc808d4..6ba989abda0d 100644 --- a/apps/meteor/app/lib/server/methods/saveSettings.ts +++ b/apps/meteor/app/lib/server/methods/saveSettings.ts @@ -5,6 +5,7 @@ import { Settings } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { updateAuditedByUser } from '../../../../server/settings/lib/auditedSettingUpdates'; import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; import { getSettingPermissionId } from '../../../authorization/lib'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; @@ -34,103 +35,107 @@ const validJSON = Match.Where((value: string) => { }); Meteor.methods({ - saveSettings: twoFactorRequired( - async ( - params: { - _id: ISetting['_id']; - value: ISetting['value']; - }[] = [], - ) => { - const uid = Meteor.userId(); - const settingsNotAllowed: ISetting['_id'][] = []; - if (uid === null) { - throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', { - method: 'saveSetting', - }); - } - const editPrivilegedSetting = await hasPermissionAsync(uid, 'edit-privileged-setting'); - const manageSelectedSettings = await hasPermissionAsync(uid, 'manage-selected-settings'); - - // if the id contains Organization_Name then change the Site_Name - const orgName = params.find(({ _id }) => _id === 'Organization_Name'); + saveSettings: twoFactorRequired(async function ( + params: { + _id: ISetting['_id']; + value: ISetting['value']; + }[] = [], + ) { + const uid = Meteor.userId(); + const settingsNotAllowed: ISetting['_id'][] = []; + if (uid === null) { + throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', { + method: 'saveSetting', + }); + } + const editPrivilegedSetting = await hasPermissionAsync(uid, 'edit-privileged-setting'); + const manageSelectedSettings = await hasPermissionAsync(uid, 'manage-selected-settings'); - if (orgName) { - // check if the site name is still the default value or ifs the same as organization name - const siteName = await Settings.findOneById('Site_Name'); + // if the id contains Organization_Name then change the Site_Name + const orgName = params.find(({ _id }) => _id === 'Organization_Name'); - if (siteName?.value === siteName?.packageValue || siteName?.value === settings.get('Organization_Name')) { - params.push({ - _id: 'Site_Name', - value: orgName.value, - }); - } - } + if (orgName) { + // check if the site name is still the default value or ifs the same as organization name + const siteName = await Settings.findOneById('Site_Name'); - await Promise.all( - params.map(async ({ _id, value }) => { - // Verify the _id passed in is a string. - check(_id, String); - if (!editPrivilegedSetting && !(manageSelectedSettings && (await hasPermissionAsync(uid, getSettingPermissionId(_id))))) { - return settingsNotAllowed.push(_id); - } - - // Disable custom scripts in cloud trials to prevent phishing campaigns - if (disableCustomScripts() && /^Custom_Script_/.test(_id)) { - return settingsNotAllowed.push(_id); - } - - const setting = await Settings.findOneById(_id); - // Verify the value is what it should be - switch (setting?.type) { - case 'roomPick': - check(value, Match.OneOf([Object], '')); - break; - case 'boolean': - check(value, Boolean); - break; - case 'timespan': - case 'int': - check(value, Number); - if (!Number.isInteger(value)) { - throw new Meteor.Error(`Invalid setting value ${value}`, 'Invalid setting value', { - method: 'saveSettings', - }); - } - - break; - case 'multiSelect': - check(value, Array); - break; - case 'code': - check(value, String); - if (isSettingCode(setting) && setting.code === 'application/json') { - check(value, validJSON); - } - break; - default: - check(value, String); - break; - } - }), - ); - - if (settingsNotAllowed.length) { - throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', { - method: 'saveSettings', - settingIds: settingsNotAllowed, + if (siteName?.value === siteName?.packageValue || siteName?.value === settings.get('Organization_Name')) { + params.push({ + _id: 'Site_Name', + value: orgName.value, }); } + } + + await Promise.all( + params.map(async ({ _id, value }) => { + // Verify the _id passed in is a string. + check(_id, String); + if (!editPrivilegedSetting && !(manageSelectedSettings && (await hasPermissionAsync(uid, getSettingPermissionId(_id))))) { + return settingsNotAllowed.push(_id); + } + + // Disable custom scripts in cloud trials to prevent phishing campaigns + if (disableCustomScripts() && /^Custom_Script_/.test(_id)) { + return settingsNotAllowed.push(_id); + } - const promises = params.map(({ _id, value }) => Settings.updateValueById(_id, value)); + const setting = await Settings.findOneById(_id); + // Verify the value is what it should be + switch (setting?.type) { + case 'roomPick': + check(value, Match.OneOf([Object], '')); + break; + case 'boolean': + check(value, Boolean); + break; + case 'timespan': + case 'int': + check(value, Number); + if (!Number.isInteger(value)) { + throw new Meteor.Error(`Invalid setting value ${value}`, 'Invalid setting value', { + method: 'saveSettings', + }); + } - (await Promise.all(promises)).forEach((value, index) => { - if (value?.modifiedCount) { - void notifyOnSettingChangedById(params[index]._id); + break; + case 'multiSelect': + check(value, Array); + break; + case 'code': + check(value, String); + if (isSettingCode(setting) && setting.code === 'application/json') { + check(value, validJSON); + } + break; + default: + check(value, String); + break; } + }), + ); + + if (settingsNotAllowed.length) { + throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', { + method: 'saveSettings', + settingIds: settingsNotAllowed, }); + } + + const auditSettingOperation = updateAuditedByUser({ + _id: uid, + username: (await Meteor.userAsync())!.username!, + ip: this.connection!.clientAddress || '', + useragent: this.connection!.httpHeaders['user-agent'] || '', + }); - return true; - }, - {}, - ), + const promises = params.map(({ _id, value }) => auditSettingOperation(Settings.updateValueById, _id, value)); + + (await Promise.all(promises)).forEach((value, index) => { + if (value?.modifiedCount) { + void notifyOnSettingChangedById(params[index]._id); + } + }); + + return true; + }, {}), }); diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 76134c81d0b3..6ab03df337ef 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -4,7 +4,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import type { RocketchatI18nKeys } from '@rocket.chat/i18n'; import { Messages, Users } from '@rocket.chat/models'; import type { TOptions } from 'i18next'; -import { check } from 'meteor/check'; +import { check, Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import moment from 'moment'; @@ -123,7 +123,24 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async sendMessage(message, previewUrls) { - check(message, Object); + check(message, { + _id: Match.Maybe(String), + rid: Match.Maybe(String), + msg: Match.Maybe(String), + tmid: Match.Maybe(String), + tshow: Match.Maybe(Boolean), + ts: Match.Maybe(Date), + t: Match.Maybe(String), + otrAck: Match.Maybe(String), + bot: Match.Maybe(Boolean), + content: Match.Maybe(Object), + e2e: Match.Maybe(String), + e2eMentions: Match.Maybe(Object), + customFields: Match.Maybe(Object), + federation: Match.Maybe(Object), + groupable: Match.Maybe(Boolean), + sentByEmail: Match.Maybe(Boolean), + }); const uid = Meteor.userId(); if (!uid) { diff --git a/apps/meteor/app/livechat/client/collections/LivechatInquiry.js b/apps/meteor/app/livechat/client/collections/LivechatInquiry.js deleted file mode 100644 index c43a9cb31ca5..000000000000 --- a/apps/meteor/app/livechat/client/collections/LivechatInquiry.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Mongo } from 'meteor/mongo'; - -export const LivechatInquiry = new Mongo.Collection(null); diff --git a/apps/meteor/app/livechat/client/collections/LivechatInquiry.ts b/apps/meteor/app/livechat/client/collections/LivechatInquiry.ts new file mode 100644 index 000000000000..16b9533d1649 --- /dev/null +++ b/apps/meteor/app/livechat/client/collections/LivechatInquiry.ts @@ -0,0 +1,4 @@ +import type { ILivechatInquiryRecord } from '@rocket.chat/core-typings'; +import { Mongo } from 'meteor/mongo'; + +export const LivechatInquiry = new Mongo.Collection(null); diff --git a/apps/meteor/app/livechat/client/index.ts b/apps/meteor/app/livechat/client/index.ts index c6884923db30..8346d2ddb978 100644 --- a/apps/meteor/app/livechat/client/index.ts +++ b/apps/meteor/app/livechat/client/index.ts @@ -1,4 +1,3 @@ import '../lib/messageTypes'; import './voip'; import './ui'; -import './stylesheets/livechat.css'; diff --git a/apps/meteor/app/livechat/client/lib/stream/queueManager.ts b/apps/meteor/app/livechat/client/lib/stream/queueManager.ts index 5ba2cf0d9791..c6a671e2883d 100644 --- a/apps/meteor/app/livechat/client/lib/stream/queueManager.ts +++ b/apps/meteor/app/livechat/client/lib/stream/queueManager.ts @@ -30,12 +30,12 @@ const events = { const invalidateRoomQueries = async (rid: string) => { await queryClient.invalidateQueries(['rooms', { reference: rid, type: 'l' }]); - await queryClient.removeQueries(['rooms', rid]); - await queryClient.removeQueries(['/v1/rooms.info', rid]); + queryClient.removeQueries(['rooms', rid]); + queryClient.removeQueries(['/v1/rooms.info', rid]); }; const removeInquiry = async (inquiry: ILivechatInquiryRecord) => { - await LivechatInquiry.remove(inquiry._id); + LivechatInquiry.remove(inquiry._id); return queryClient.invalidateQueries(['rooms', { reference: inquiry.rid, type: 'l' }]); }; diff --git a/apps/meteor/app/livechat/client/stylesheets/livechat.css b/apps/meteor/app/livechat/client/stylesheets/livechat.css deleted file mode 100644 index b6e8bf366940..000000000000 --- a/apps/meteor/app/livechat/client/stylesheets/livechat.css +++ /dev/null @@ -1,42 +0,0 @@ -:root { - --header-min-height: 30px; - --footer-min-height: 55px; - --link-font-color: #008ce3; - --primary-font-color: #444444; - --secondary-font-color: #7f7f7f; - --info-font-color: #aaaaaa; - --color-gray: #9ea2a8; -} - -.external-frame { - width: 100%; - height: 100%; -} - -.open { - padding-left: 4% !important; -} - -.closing-message-body-wrapper { - margin-top: 3%; -} - -.closing-message-title { - color: var(--color-dark-light); - - font-family: Inter; - font-size: 12px; - font-weight: bold; - font-style: normal; - line-height: 16px; -} - -.closing-message-text { - color: var(--color-gray); - - font-family: Inter; - font-size: 12px; - font-weight: 500; - font-style: italic; - line-height: 16px; -} diff --git a/apps/meteor/app/livechat/imports/server/rest/appearance.ts b/apps/meteor/app/livechat/imports/server/rest/appearance.ts index 7496b6243abe..215f208c06dc 100644 --- a/apps/meteor/app/livechat/imports/server/rest/appearance.ts +++ b/apps/meteor/app/livechat/imports/server/rest/appearance.ts @@ -3,6 +3,7 @@ import { Settings } from '@rocket.chat/models'; import { isPOSTLivechatAppearanceParams } from '@rocket.chat/rest-typings'; import { isTruthy } from '../../../../../lib/isTruthy'; +import { updateAuditedByUser } from '../../../../../server/settings/lib/auditedSettingUpdates'; import { API } from '../../../../api/server'; import { notifyOnSettingChangedById } from '../../../../lib/server/lib/notifyListener'; import { findAppearance } from '../../../server/api/lib/appearance'; @@ -92,7 +93,15 @@ API.v1.addRoute( .toArray(); const eligibleSettings = dbSettings.filter(isTruthy); - const promises = eligibleSettings.map(({ _id, value }) => Settings.updateValueById(_id, value)); + + const auditSettingOperation = updateAuditedByUser({ + _id: this.userId, + username: this.user.username!, + ip: this.requestIp, + useragent: this.request.headers['user-agent'] || '', + }); + + const promises = eligibleSettings.map(({ _id, value }) => auditSettingOperation(Settings.updateValueById, _id, value)); (await Promise.all(promises)).forEach((value, index) => { if (value?.modifiedCount) { void notifyOnSettingChangedById(eligibleSettings[index]._id); diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index e56feeac2fa3..966b7e6d6af4 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -14,8 +14,13 @@ import { findDepartmentAgents, findArchivedDepartments, } from '../../../server/api/lib/departments'; -import { DepartmentHelper } from '../../../server/lib/Departments'; -import { Livechat as LivechatTs } from '../../../server/lib/LivechatTyped'; +import { + saveDepartment, + archiveDepartment, + unarchiveDepartment, + saveDepartmentAgents, + removeDepartment, +} from '../../../server/lib/departmentsLib'; import { isDepartmentCreationAvailable } from '../../../server/lib/isDepartmentCreationAvailable'; API.v1.addRoute( @@ -62,7 +67,7 @@ API.v1.addRoute( const agents = this.bodyParams.agents ? { upsert: this.bodyParams.agents } : {}; const { departmentUnit } = this.bodyParams; - const department = await LivechatTs.saveDepartment( + const department = await saveDepartment( this.userId, null, this.bodyParams.department as ILivechatDepartment, @@ -131,7 +136,7 @@ API.v1.addRoute( } const agentParam = permissionToAddAgents && agents ? { upsert: agents } : {}; - await LivechatTs.saveDepartment(this.userId, _id, department, agentParam, departmentUnit || {}); + await saveDepartment(this.userId, _id, department, agentParam, departmentUnit || {}); return API.v1.success({ department: await LivechatDepartment.findOneById(_id), @@ -143,7 +148,7 @@ API.v1.addRoute( _id: String, }); - await DepartmentHelper.removeDepartment(this.urlParams._id); + await removeDepartment(this.urlParams._id); return API.v1.success(); }, @@ -191,7 +196,7 @@ API.v1.addRoute( }, { async post() { - await LivechatTs.archiveDepartment(this.urlParams._id); + await archiveDepartment(this.urlParams._id); return API.v1.success(); }, @@ -206,7 +211,7 @@ API.v1.addRoute( }, { async post() { - await LivechatTs.unarchiveDepartment(this.urlParams._id); + await unarchiveDepartment(this.urlParams._id); return API.v1.success(); }, }, @@ -271,7 +276,7 @@ API.v1.addRoute( remove: Array, }), ); - await LivechatTs.saveDepartmentAgents(this.urlParams._id, this.bodyParams); + await saveDepartmentAgents(this.urlParams._id, this.bodyParams); return API.v1.success(); }, diff --git a/apps/meteor/app/livechat/imports/server/rest/sms.ts b/apps/meteor/app/livechat/imports/server/rest/sms.ts index 15f08cdc1e83..2f13fd01f221 100644 --- a/apps/meteor/app/livechat/imports/server/rest/sms.ts +++ b/apps/meteor/app/livechat/imports/server/rest/sms.ts @@ -1,11 +1,11 @@ import { OmnichannelIntegration } from '@rocket.chat/core-services'; import type { ILivechatVisitor, - IOmnichannelRoom, IUpload, MessageAttachment, ServiceData, FileAttachmentProps, + IOmnichannelRoomInfo, } from '@rocket.chat/core-typings'; import { OmnichannelSourceType } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; @@ -20,8 +20,8 @@ import { FileUpload } from '../../../../file-upload/server'; import { checkUrlForSsrf } from '../../../../lib/server/functions/checkUrlForSsrf'; import { settings } from '../../../../settings/server'; import { setCustomField } from '../../../server/api/lib/customFields'; -import type { ILivechatMessage } from '../../../server/lib/LivechatTyped'; import { Livechat as LivechatTyped } from '../../../server/lib/LivechatTyped'; +import type { ILivechatMessage } from '../../../server/lib/localTypes'; const logger = new Logger('SMS'); @@ -122,10 +122,7 @@ API.v1.addRoute('livechat/sms-incoming/:service', { return API.v1.success(SMSService.error(new Error('Invalid visitor'))); } - const roomInfo: { - source?: IOmnichannelRoom['source']; - [key: string]: unknown; - } = { + const roomInfo: IOmnichannelRoomInfo = { sms: { from: sms.to, }, @@ -244,10 +241,7 @@ API.v1.addRoute('livechat/sms-incoming/:service', { const sendMessage: { guest: ILivechatVisitor; message: ILivechatMessage; - roomInfo: { - source?: IOmnichannelRoom['source']; - [key: string]: unknown; - }; + roomInfo: IOmnichannelRoomInfo; } = { guest: visitor, roomInfo, diff --git a/apps/meteor/app/livechat/lib/isSameChannel.ts b/apps/meteor/app/livechat/lib/isSameChannel.ts new file mode 100644 index 000000000000..bdc6516aa813 --- /dev/null +++ b/apps/meteor/app/livechat/lib/isSameChannel.ts @@ -0,0 +1,20 @@ +import type { ILivechatContactVisitorAssociation } from '@rocket.chat/core-typings'; + +export function isSameChannel(channel1: ILivechatContactVisitorAssociation, channel2: ILivechatContactVisitorAssociation): boolean { + if (!channel1 || !channel2) { + return false; + } + + if (channel1.visitorId !== channel2.visitorId) { + return false; + } + if (channel1.source.type !== channel2.source.type) { + return false; + } + + if ((channel1.source.id || channel2.source.id) && channel1.source.id !== channel2.source.id) { + return false; + } + + return true; +} diff --git a/apps/meteor/app/livechat/server/api/lib/livechat.ts b/apps/meteor/app/livechat/server/api/lib/livechat.ts index 01c4d9736c66..c3c22c327731 100644 --- a/apps/meteor/app/livechat/server/api/lib/livechat.ts +++ b/apps/meteor/app/livechat/server/api/lib/livechat.ts @@ -49,17 +49,7 @@ async function findDepartments( } export function findGuest(token: string): Promise { - return LivechatVisitors.getVisitorByToken(token, { - projection: { - name: 1, - username: 1, - token: 1, - visitorEmails: 1, - department: 1, - activity: 1, - contactId: 1, - }, - }); + return LivechatVisitors.getVisitorByToken(token); } export function findGuestWithoutActivity(token: string): Promise { diff --git a/apps/meteor/app/livechat/server/api/v1/agent.ts b/apps/meteor/app/livechat/server/api/v1/agent.ts index abc6163fe9c9..6c583e1b24db 100644 --- a/apps/meteor/app/livechat/server/api/v1/agent.ts +++ b/apps/meteor/app/livechat/server/api/v1/agent.ts @@ -7,6 +7,7 @@ import { API } from '../../../../api/server'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; import { RoutingManager } from '../../lib/RoutingManager'; +import { getRequiredDepartment } from '../../lib/departmentsLib'; import { findRoom, findGuest, findAgent, findOpenRoom } from '../lib/livechat'; API.v1.addRoute('livechat/agent.info/:rid/:token', { @@ -43,7 +44,7 @@ API.v1.addRoute( let { department } = this.queryParams; if (!department) { - const requireDepartment = await LivechatTyped.getRequiredDepartment(); + const requireDepartment = await getRequiredDepartment(); if (requireDepartment) { department = requireDepartment._id; } diff --git a/apps/meteor/app/livechat/server/api/v1/contact.ts b/apps/meteor/app/livechat/server/api/v1/contact.ts index 5c68a475a952..019ea07252dc 100644 --- a/apps/meteor/app/livechat/server/api/v1/contact.ts +++ b/apps/meteor/app/livechat/server/api/v1/contact.ts @@ -4,6 +4,7 @@ import { isPOSTUpdateOmnichannelContactsProps, isGETOmnichannelContactsProps, isGETOmnichannelContactHistoryProps, + isGETOmnichannelContactsChannelsProps, isGETOmnichannelContactsSearchProps, } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -12,7 +13,12 @@ import { Meteor } from 'meteor/meteor'; import { API } from '../../../../api/server'; import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems'; -import { getContactHistory, Contacts, createContact, updateContact, getContacts, isSingleContactEnabled } from '../../lib/Contacts'; +import { createContact } from '../../lib/contacts/createContact'; +import { getContactChannelsGrouped } from '../../lib/contacts/getContactChannelsGrouped'; +import { getContactHistory } from '../../lib/contacts/getContactHistory'; +import { getContacts } from '../../lib/contacts/getContacts'; +import { registerContact } from '../../lib/contacts/registerContact'; +import { updateContact } from '../../lib/contacts/updateContact'; API.v1.addRoute( 'omnichannel/contact', @@ -35,7 +41,7 @@ API.v1.addRoute( }), }); - const contact = await Contacts.registerContact(this.bodyParams); + const contact = await registerContact(this.bodyParams); return API.v1.success({ contact }); }, @@ -102,9 +108,6 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['create-livechat-contact'], validateParams: isPOSTOmnichannelContactsProps }, { async post() { - if (!isSingleContactEnabled()) { - return API.v1.unauthorized(); - } const contactId = await createContact({ ...this.bodyParams, unknown: false }); return API.v1.success({ contactId }); @@ -117,10 +120,6 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['update-livechat-contact'], validateParams: isPOSTUpdateOmnichannelContactsProps }, { async post() { - if (!isSingleContactEnabled()) { - return API.v1.unauthorized(); - } - const contact = await updateContact({ ...this.bodyParams }); return API.v1.success({ contact }); @@ -133,10 +132,17 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-livechat-contact'], validateParams: isGETOmnichannelContactsProps }, { async get() { - if (!isSingleContactEnabled()) { - return API.v1.unauthorized(); + const { contactId } = this.queryParams; + + if (!contactId) { + return API.v1.notFound(); + } + + const contact = await LivechatContacts.findOneById(contactId); + + if (!contact) { + return API.v1.notFound(); } - const contact = await LivechatContacts.findOneById(this.queryParams.contactId); return API.v1.success({ contact }); }, @@ -148,15 +154,11 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-livechat-contact'], validateParams: isGETOmnichannelContactsSearchProps }, { async get() { - if (!isSingleContactEnabled()) { - return API.v1.unauthorized(); - } - - const { searchText } = this.queryParams; + const query = this.queryParams; const { offset, count } = await getPaginationItems(this.queryParams); const { sort } = await this.parseJsonQuery(); - const result = await getContacts({ searchText, offset, count, sort }); + const result = await getContacts({ ...query, offset, count, sort }); return API.v1.success(result); }, @@ -168,10 +170,6 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-livechat-contact-history'], validateParams: isGETOmnichannelContactHistoryProps }, { async get() { - if (!isSingleContactEnabled()) { - return API.v1.unauthorized(); - } - const { contactId, source } = this.queryParams; const { offset, count } = await getPaginationItems(this.queryParams); const { sort } = await this.parseJsonQuery(); @@ -182,3 +180,17 @@ API.v1.addRoute( }, }, ); + +API.v1.addRoute( + 'omnichannel/contacts.channels', + { authRequired: true, permissionsRequired: ['view-livechat-contact'], validateParams: isGETOmnichannelContactsChannelsProps }, + { + async get() { + const { contactId } = this.queryParams; + + const channels = await getContactChannelsGrouped(contactId); + + return API.v1.success({ channels }); + }, + }, +); diff --git a/apps/meteor/app/livechat/server/api/v1/integration.ts b/apps/meteor/app/livechat/server/api/v1/integration.ts index a1f9c59ffb87..7e56c8ca8e59 100644 --- a/apps/meteor/app/livechat/server/api/v1/integration.ts +++ b/apps/meteor/app/livechat/server/api/v1/integration.ts @@ -2,6 +2,7 @@ import { Settings } from '@rocket.chat/models'; import { isPOSTomnichannelIntegrations } from '@rocket.chat/rest-typings'; import { trim } from '../../../../../lib/utils/stringUtils'; +import { updateAuditedByUser } from '../../../../../server/settings/lib/auditedSettingUpdates'; import { API } from '../../../../api/server'; import { notifyOnSettingChangedById } from '../../../../lib/server/lib/notifyListener'; @@ -50,7 +51,14 @@ API.v1.addRoute( }, ].filter(Boolean) as unknown as { _id: string; value: any }[]; - const promises = settingsIds.map((setting) => Settings.updateValueById(setting._id, setting.value)); + const auditSettingOperation = updateAuditedByUser({ + _id: this.userId, + username: this.user.username!, + ip: this.requestIp, + useragent: this.request.headers['user-agent'] || '', + }); + + const promises = settingsIds.map((setting) => auditSettingOperation(Settings.updateValueById, setting._id, setting.value)); (await Promise.all(promises)).forEach((value, index) => { if (value?.modifiedCount) { diff --git a/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts b/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts index a367df7a04ad..f6fb29d17442 100644 --- a/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts +++ b/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts @@ -2,7 +2,7 @@ import { isPOSTLivechatOfflineMessageParams } from '@rocket.chat/rest-typings'; import { i18n } from '../../../../../server/lib/i18n'; import { API } from '../../../../api/server'; -import { Livechat } from '../../lib/LivechatTyped'; +import { sendOfflineMessage } from '../../lib/messages'; API.v1.addRoute( 'livechat/offline.message', @@ -14,7 +14,7 @@ API.v1.addRoute( async post() { const { name, email, message, department, host } = this.bodyParams; try { - await Livechat.sendOfflineMessage({ name, email, message, department, host }); + await sendOfflineMessage({ name, email, message, department, host }); return API.v1.success({ message: i18n.t('Livechat_offline_message_sent') }); } catch (e) { return API.v1.failure(i18n.t('Error_sending_livechat_offline_message')); diff --git a/apps/meteor/app/livechat/server/api/v1/pageVisited.ts b/apps/meteor/app/livechat/server/api/v1/pageVisited.ts index 2688ad673af0..a97b5278462d 100644 --- a/apps/meteor/app/livechat/server/api/v1/pageVisited.ts +++ b/apps/meteor/app/livechat/server/api/v1/pageVisited.ts @@ -2,7 +2,7 @@ import type { IOmnichannelSystemMessage } from '@rocket.chat/core-typings'; import { isPOSTLivechatPageVisitedParams } from '@rocket.chat/rest-typings'; import { API } from '../../../../api/server'; -import { Livechat } from '../../lib/LivechatTyped'; +import { savePageHistory } from '../../lib/tracking'; API.v1.addRoute( 'livechat/page.visited', @@ -11,7 +11,7 @@ API.v1.addRoute( async post() { const { token, rid, pageInfo } = this.bodyParams; - const message = await Livechat.savePageHistory(token, rid, pageInfo); + const message = await savePageHistory(token, rid, pageInfo); if (!message) { return API.v1.success(); } diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 7d52617e074a..b46a8c3e0663 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -1,5 +1,5 @@ import { Omnichannel } from '@rocket.chat/core-services'; -import type { ILivechatAgent, IUser, SelectedAgent, TransferByData } from '@rocket.chat/core-typings'; +import type { ILivechatAgent, IOmnichannelInquiryExtraData, IUser, SelectedAgent, TransferByData } from '@rocket.chat/core-typings'; import { isOmnichannelRoom, OmnichannelSourceType } from '@rocket.chat/core-typings'; import { LivechatVisitors, Users, LivechatRooms, Messages } from '@rocket.chat/models'; import { @@ -23,8 +23,8 @@ import { addUserToRoom } from '../../../../lib/server/functions/addUserToRoom'; import { closeLivechatRoom } from '../../../../lib/server/functions/closeLivechatRoom'; import { settings as rcSettings } from '../../../../settings/server'; import { normalizeTransferredByData } from '../../lib/Helper'; -import type { CloseRoomParams } from '../../lib/LivechatTyped'; import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; +import type { CloseRoomParams } from '../../lib/localTypes'; import { findGuest, findRoom, settings, findAgent, onCheckRoomParams } from '../lib/livechat'; const isAgentWithInfo = (agentObj: ILivechatAgent | { hiddenInfo: boolean }): agentObj is ILivechatAgent => !('hiddenInfo' in agentObj); @@ -80,7 +80,12 @@ API.v1.addRoute( }, }; - const newRoom = await LivechatTyped.createRoom({ visitor: guest, roomInfo, agent, extraData: extraParams }); + const newRoom = await LivechatTyped.createRoom({ + visitor: guest, + roomInfo, + agent, + extraData: extraParams as IOmnichannelInquiryExtraData, + }); return API.v1.success({ room: newRoom, diff --git a/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts b/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts index ec21ff2de067..0f125910b9a4 100644 --- a/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts +++ b/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts @@ -4,12 +4,12 @@ import type { AgendaCronJobs } from '@rocket.chat/cron'; import { LivechatBusinessHours, LivechatDepartment, Users } from '@rocket.chat/models'; import moment from 'moment'; +import type { IBusinessHourBehavior, IBusinessHourType } from './AbstractBusinessHour'; +import { closeBusinessHour } from './closeBusinessHour'; import { callbacks } from '../../../../lib/callbacks'; import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { businessHourLogger } from '../lib/logger'; -import type { IBusinessHourBehavior, IBusinessHourType } from './AbstractBusinessHour'; -import { closeBusinessHour } from './closeBusinessHour'; const CRON_EVERY_MIDNIGHT_EXPRESSION = '0 0 * * *'; const CRON_DAYLIGHT_JOB_NAME = 'livechat-business-hour-daylight-saving-time-verifier'; diff --git a/apps/meteor/app/livechat/server/business-hour/Helper.ts b/apps/meteor/app/livechat/server/business-hour/Helper.ts index e19300691660..2f535ad190af 100644 --- a/apps/meteor/app/livechat/server/business-hour/Helper.ts +++ b/apps/meteor/app/livechat/server/business-hour/Helper.ts @@ -3,10 +3,10 @@ import { ILivechatAgentStatus, LivechatBusinessHourTypes } from '@rocket.chat/co import { LivechatBusinessHours, Users } from '@rocket.chat/models'; import moment from 'moment'; -import { notifyOnUserChangeAsync } from '../../../lib/server/lib/notifyListener'; -import { businessHourLogger } from '../lib/logger'; import { createDefaultBusinessHourRow } from './LivechatBusinessHours'; import { filterBusinessHoursThatMustBeOpened } from './filterBusinessHoursThatMustBeOpened'; +import { notifyOnUserChangeAsync } from '../../../lib/server/lib/notifyListener'; +import { businessHourLogger } from '../lib/logger'; export { filterBusinessHoursThatMustBeOpened }; diff --git a/apps/meteor/app/livechat/server/business-hour/Single.ts b/apps/meteor/app/livechat/server/business-hour/Single.ts index ea8166c75fa9..13a8673ad7fa 100644 --- a/apps/meteor/app/livechat/server/business-hour/Single.ts +++ b/apps/meteor/app/livechat/server/business-hour/Single.ts @@ -1,11 +1,11 @@ import { ILivechatAgentStatus, LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; import { LivechatBusinessHours, Users } from '@rocket.chat/models'; -import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; -import { businessHourLogger } from '../lib/logger'; import type { IBusinessHourBehavior } from './AbstractBusinessHour'; import { AbstractBusinessHourBehavior } from './AbstractBusinessHour'; import { filterBusinessHoursThatMustBeOpened, makeAgentsUnavailableBasedOnBusinessHour, openBusinessHourDefault } from './Helper'; +import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; +import { businessHourLogger } from '../lib/logger'; export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior implements IBusinessHourBehavior { async openBusinessHoursByDayAndHour(): Promise { diff --git a/apps/meteor/app/livechat/server/business-hour/closeBusinessHour.ts b/apps/meteor/app/livechat/server/business-hour/closeBusinessHour.ts index 976d8ec1705e..c18784adb5ea 100644 --- a/apps/meteor/app/livechat/server/business-hour/closeBusinessHour.ts +++ b/apps/meteor/app/livechat/server/business-hour/closeBusinessHour.ts @@ -2,9 +2,9 @@ import type { ILivechatBusinessHour, IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import { makeFunction } from '@rocket.chat/patch-injection'; -import { businessHourLogger } from '../lib/logger'; import { makeAgentsUnavailableBasedOnBusinessHour } from './Helper'; import { getAgentIdsForBusinessHour } from './getAgentIdsForBusinessHour'; +import { businessHourLogger } from '../lib/logger'; export const closeBusinessHourByAgentIds = async ( businessHourId: ILivechatBusinessHour['_id'], diff --git a/apps/meteor/app/livechat/server/business-hour/index.ts b/apps/meteor/app/livechat/server/business-hour/index.ts index 8b5f10b938ff..8df93c5cbaee 100644 --- a/apps/meteor/app/livechat/server/business-hour/index.ts +++ b/apps/meteor/app/livechat/server/business-hour/index.ts @@ -3,10 +3,10 @@ import { cronJobs } from '@rocket.chat/cron'; import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../../../lib/callbacks'; import { BusinessHourManager } from './BusinessHourManager'; import { DefaultBusinessHour } from './Default'; import { SingleBusinessHourBehavior } from './Single'; +import { callbacks } from '../../../../lib/callbacks'; export const businessHourManager = new BusinessHourManager(cronJobs); diff --git a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts index 2c718c058b00..752bce901d31 100644 --- a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts +++ b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts @@ -31,7 +31,7 @@ export const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, a const department = room.departmentId ? await LivechatDepartment.findOneById>(room.departmentId, { projection: { businessHourId: 1 }, - }) + }) : null; if (department?.businessHourId) { const businessHour = await LivechatBusinessHours.findOneById(department.businessHourId); diff --git a/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts b/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts index d8014dd3ecc0..6996af1664d6 100644 --- a/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts +++ b/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts @@ -12,7 +12,9 @@ callbacks.add( const { _id, - v: { _id: guestId, contactId }, + v: { _id: guestId }, + source, + contactId, } = room; const lastChat = { @@ -21,7 +23,14 @@ callbacks.add( }; await LivechatVisitors.setLastChatById(guestId, lastChat); if (contactId) { - await LivechatContacts.updateLastChatById(contactId, lastChat); + await LivechatContacts.updateLastChatById( + contactId, + { + visitorId: guestId, + source, + }, + lastChat, + ); } }, callbacks.priority.MEDIUM, diff --git a/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts b/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts index f6a35f4dd7f9..f0c445a78a78 100644 --- a/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts +++ b/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts @@ -3,7 +3,7 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; -import type { CloseRoomParams } from '../lib/LivechatTyped'; +import type { CloseRoomParams } from '../lib/localTypes'; import { sendTranscript } from '../lib/sendTranscript'; type LivechatCloseCallbackParams = { diff --git a/apps/meteor/app/livechat/server/lib/Contacts.ts b/apps/meteor/app/livechat/server/lib/Contacts.ts deleted file mode 100644 index dd7fb0b99848..000000000000 --- a/apps/meteor/app/livechat/server/lib/Contacts.ts +++ /dev/null @@ -1,405 +0,0 @@ -import type { - AtLeast, - ILivechatContact, - ILivechatContactChannel, - ILivechatCustomField, - ILivechatVisitor, - IOmnichannelRoom, - IUser, -} from '@rocket.chat/core-typings'; -import type { InsertionModel } from '@rocket.chat/model-typings'; -import { - LivechatVisitors, - Users, - LivechatRooms, - LivechatCustomField, - LivechatInquiry, - Rooms, - Subscriptions, - LivechatContacts, -} from '@rocket.chat/models'; -import type { PaginatedResult, VisitorSearchChatsResult } from '@rocket.chat/rest-typings'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; -import type { MatchKeysAndValues, OnlyFieldsOfType, FindOptions, Sort } from 'mongodb'; - -import { callbacks } from '../../../../lib/callbacks'; -import { trim } from '../../../../lib/utils/stringUtils'; -import { - notifyOnRoomChangedById, - notifyOnSubscriptionChangedByRoomId, - notifyOnLivechatInquiryChangedByRoom, -} from '../../../lib/server/lib/notifyListener'; -import { i18n } from '../../../utils/lib/i18n'; - -type RegisterContactProps = { - _id?: string; - token: string; - name: string; - username?: string; - email?: string; - phone?: string; - customFields?: Record; - contactManager?: { - username: string; - }; -}; - -type CreateContactParams = { - name: string; - emails?: string[]; - phones?: string[]; - unknown: boolean; - customFields?: Record; - contactManager?: string; - channels?: ILivechatContactChannel[]; -}; - -type UpdateContactParams = { - contactId: string; - name?: string; - emails?: string[]; - phones?: string[]; - customFields?: Record; - contactManager?: string; - channels?: ILivechatContactChannel[]; -}; - -type GetContactsParams = { - searchText?: string; - count: number; - offset: number; - sort: Sort; -}; - -type GetContactHistoryParams = { - contactId: string; - source?: string; - count: number; - offset: number; - sort: Sort; -}; - -export const Contacts = { - async registerContact({ - token, - name, - email = '', - phone, - username, - customFields = {}, - contactManager, - }: RegisterContactProps): Promise { - check(token, String); - - const visitorEmail = email.trim().toLowerCase(); - - if (contactManager?.username) { - // verify if the user exists with this username and has a livechat-agent role - const user = await Users.findOneByUsername(contactManager.username, { projection: { roles: 1 } }); - if (!user) { - throw new Meteor.Error('error-contact-manager-not-found', `No user found with username ${contactManager.username}`); - } - if (!user.roles || !Array.isArray(user.roles) || !user.roles.includes('livechat-agent')) { - throw new Meteor.Error('error-invalid-contact-manager', 'The contact manager must have the role "livechat-agent"'); - } - } - - let contactId; - - const user = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } }); - - if (user) { - contactId = user._id; - } else { - if (!username) { - username = await LivechatVisitors.getNextVisitorUsername(); - } - - let existingUser = null; - - if (visitorEmail !== '' && (existingUser = await LivechatVisitors.findOneGuestByEmailAddress(visitorEmail))) { - contactId = existingUser._id; - } else { - const userData = { - username, - ts: new Date(), - token, - }; - - contactId = (await LivechatVisitors.insertOne(userData)).insertedId; - } - } - - const allowedCF = await getAllowedCustomFields(); - const livechatData: Record = validateCustomFields(allowedCF, customFields, { ignoreAdditionalFields: true }); - - const fieldsToRemove = { - // if field is explicitely set to empty string, remove - ...(phone === '' && { phone: 1 }), - ...(visitorEmail === '' && { visitorEmails: 1 }), - ...(!contactManager?.username && { contactManager: 1 }), - }; - - const updateUser: { $set: MatchKeysAndValues; $unset?: OnlyFieldsOfType } = { - $set: { - token, - name, - livechatData, - // if phone has some value, set - ...(phone && { phone: [{ phoneNumber: phone }] }), - ...(visitorEmail && { visitorEmails: [{ address: visitorEmail }] }), - ...(contactManager?.username && { contactManager: { username: contactManager.username } }), - }, - ...(Object.keys(fieldsToRemove).length && { $unset: fieldsToRemove }), - }; - - await LivechatVisitors.updateOne({ _id: contactId }, updateUser); - - const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); - const rooms: IOmnichannelRoom[] = await LivechatRooms.findByVisitorId(contactId, {}, extraQuery).toArray(); - - if (rooms?.length) { - for await (const room of rooms) { - const { _id: rid } = room; - - const responses = await Promise.all([ - Rooms.setFnameById(rid, name), - LivechatInquiry.setNameByRoomId(rid, name), - Subscriptions.updateDisplayNameByRoomId(rid, name), - ]); - - if (responses[0]?.modifiedCount) { - void notifyOnRoomChangedById(rid); - } - - if (responses[1]?.modifiedCount) { - void notifyOnLivechatInquiryChangedByRoom(rid, 'updated', { name }); - } - - if (responses[2]?.modifiedCount) { - void notifyOnSubscriptionChangedByRoomId(rid); - } - } - } - - return contactId; - }, -}; - -export function isSingleContactEnabled(): boolean { - // The Single Contact feature is not yet available in production, but can already be partially used in test environments. - return process.env.TEST_MODE?.toUpperCase() === 'TRUE'; -} - -export async function createContactFromVisitor(visitor: ILivechatVisitor): Promise { - if (visitor.contactId) { - throw new Error('error-contact-already-exists'); - } - - const contactData: InsertionModel = { - name: visitor.name || visitor.username, - emails: visitor.visitorEmails, - phones: visitor.phone || undefined, - unknown: true, - channels: [], - customFields: visitor.livechatData, - createdAt: new Date(), - }; - - if (visitor.contactManager) { - const contactManagerId = await Users.findOneByUsername>(visitor.contactManager.username, { projection: { _id: 1 } }); - if (contactManagerId) { - contactData.contactManager = contactManagerId._id; - } - } - - const { insertedId: contactId } = await LivechatContacts.insertOne(contactData); - - await LivechatVisitors.updateOne({ _id: visitor._id }, { $set: { contactId } }); - - return contactId; -} - -export async function createContact(params: CreateContactParams): Promise { - const { name, emails, phones, customFields: receivedCustomFields = {}, contactManager, channels, unknown } = params; - - if (contactManager) { - await validateContactManager(contactManager); - } - - const allowedCustomFields = await getAllowedCustomFields(); - const customFields = validateCustomFields(allowedCustomFields, receivedCustomFields); - - const { insertedId } = await LivechatContacts.insertOne({ - name, - emails: emails?.map((address) => ({ address })), - phones: phones?.map((phoneNumber) => ({ phoneNumber })), - contactManager, - channels, - customFields, - unknown, - createdAt: new Date(), - }); - - return insertedId; -} - -export async function updateContact(params: UpdateContactParams): Promise { - const { contactId, name, emails, phones, customFields: receivedCustomFields, contactManager, channels } = params; - - const contact = await LivechatContacts.findOneById>(contactId, { projection: { _id: 1 } }); - - if (!contact) { - throw new Error('error-contact-not-found'); - } - - if (contactManager) { - await validateContactManager(contactManager); - } - - const customFields = receivedCustomFields && validateCustomFields(await getAllowedCustomFields(), receivedCustomFields); - - const updatedContact = await LivechatContacts.updateContact(contactId, { - name, - emails: emails?.map((address) => ({ address })), - phones: phones?.map((phoneNumber) => ({ phoneNumber })), - contactManager, - channels, - customFields, - }); - - return updatedContact; -} - -export async function getContacts(params: GetContactsParams): Promise> { - const { searchText, count, offset, sort } = params; - - const { cursor, totalCount } = LivechatContacts.findPaginatedContacts(searchText, { - limit: count, - skip: offset, - sort: sort ?? { name: 1 }, - }); - - const [contacts, total] = await Promise.all([cursor.toArray(), totalCount]); - - return { - contacts, - count, - offset, - total, - }; -} - -export async function getContactHistory( - params: GetContactHistoryParams, -): Promise> { - const { contactId, source, count, offset, sort } = params; - - const contact = await LivechatContacts.findOneById>(contactId, { projection: { channels: 1 } }); - - if (!contact) { - throw new Error('error-contact-not-found'); - } - - const visitorsIds = new Set(contact.channels?.map((channel: ILivechatContactChannel) => channel.visitorId)); - - if (!visitorsIds?.size) { - return { history: [], count: 0, offset, total: 0 }; - } - - const options: FindOptions = { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - projection: { - fname: 1, - ts: 1, - v: 1, - msgs: 1, - servedBy: 1, - closedAt: 1, - closedBy: 1, - closer: 1, - tags: 1, - source: 1, - }, - }; - - const { totalCount, cursor } = LivechatRooms.findPaginatedRoomsByVisitorsIdsAndSource({ - visitorsIds: Array.from(visitorsIds), - source, - options, - }); - - const [total, history] = await Promise.all([totalCount, cursor.toArray()]); - - return { - history, - count: history.length, - offset, - total, - }; -} - -async function getAllowedCustomFields(): Promise[]> { - return LivechatCustomField.findByScope( - 'visitor', - { - projection: { _id: 1, label: 1, regexp: 1, required: 1 }, - }, - false, - ).toArray(); -} - -export function validateCustomFields( - allowedCustomFields: AtLeast[], - customFields: Record, - options?: { ignoreAdditionalFields?: boolean }, -): Record { - const validValues: Record = {}; - - for (const cf of allowedCustomFields) { - if (!customFields.hasOwnProperty(cf._id)) { - if (cf.required) { - throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label })); - } - continue; - } - const cfValue: string = trim(customFields[cf._id]); - - if (!cfValue || typeof cfValue !== 'string') { - if (cf.required) { - throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label })); - } - continue; - } - - if (cf.regexp) { - const regex = new RegExp(cf.regexp); - if (!regex.test(cfValue)) { - throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label })); - } - } - - validValues[cf._id] = cfValue; - } - - if (!options?.ignoreAdditionalFields) { - const allowedCustomFieldIds = new Set(allowedCustomFields.map((cf) => cf._id)); - for (const key in customFields) { - if (!allowedCustomFieldIds.has(key)) { - throw new Error(i18n.t('error-custom-field-not-allowed', { key })); - } - } - } - - return validValues; -} - -export async function validateContactManager(contactManagerUserId: string) { - const contactManagerUser = await Users.findOneAgentById>(contactManagerUserId, { projection: { _id: 1 } }); - if (!contactManagerUser) { - throw new Error('error-contact-manager-not-found'); - } -} diff --git a/apps/meteor/app/livechat/server/lib/Departments.ts b/apps/meteor/app/livechat/server/lib/Departments.ts deleted file mode 100644 index 3dfa01e4f6b6..000000000000 --- a/apps/meteor/app/livechat/server/lib/Departments.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { ILivechatDepartment } from '@rocket.chat/core-typings'; -import { Logger } from '@rocket.chat/logger'; -import { LivechatDepartment, LivechatDepartmentAgents, LivechatRooms } from '@rocket.chat/models'; - -import { callbacks } from '../../../../lib/callbacks'; -import { notifyOnLivechatDepartmentAgentChanged } from '../../../lib/server/lib/notifyListener'; - -class DepartmentHelperClass { - logger = new Logger('Omnichannel:DepartmentHelper'); - - async removeDepartment(departmentId: string) { - this.logger.debug(`Removing department: ${departmentId}`); - - const department = await LivechatDepartment.findOneById>(departmentId, { - projection: { _id: 1, businessHourId: 1 }, - }); - if (!department) { - throw new Error('error-department-not-found'); - } - - const { _id } = department; - - const ret = await LivechatDepartment.removeById(_id); - if (ret.acknowledged !== true) { - throw new Error('error-failed-to-delete-department'); - } - - const removedAgents = await LivechatDepartmentAgents.findByDepartmentId(department._id, { projection: { agentId: 1 } }).toArray(); - - this.logger.debug( - `Performing post-department-removal actions: ${_id}. Removing department agents, unsetting fallback department and removing department from rooms`, - ); - - const removeByDept = LivechatDepartmentAgents.removeByDepartmentId(_id); - - const promiseResponses = await Promise.allSettled([ - removeByDept, - LivechatDepartment.unsetFallbackDepartmentByDepartmentId(_id), - LivechatRooms.bulkRemoveDepartmentAndUnitsFromRooms(_id), - ]); - - promiseResponses.forEach((response, index) => { - if (response.status === 'rejected') { - this.logger.error(`Error while performing post-department-removal actions: ${_id}. Action No: ${index}. Error:`, response.reason); - } - }); - - const { deletedCount } = await removeByDept; - - if (deletedCount > 0) { - removedAgents.forEach(({ _id: docId, agentId }) => { - void notifyOnLivechatDepartmentAgentChanged( - { - _id: docId, - agentId, - departmentId: _id, - }, - 'removed', - ); - }); - } - - await callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds: removedAgents.map(({ agentId }) => agentId) }); - - return ret; - } -} - -export const DepartmentHelper = new DepartmentHelperClass(); diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index a050e06c1943..c4d131c9740f 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -13,6 +13,10 @@ import type { TransferByData, ILivechatAgent, ILivechatDepartment, + IOmnichannelRoomInfo, + IOmnichannelInquiryExtraData, + IOmnichannelRoomExtraData, + ILivechatContact, } from '@rocket.chat/core-typings'; import { LivechatInquiryStatus, OmnichannelSourceType, DEFAULT_SLA_CONFIG, UserStatus } from '@rocket.chat/core-typings'; import { LivechatPriorityWeight } from '@rocket.chat/core-typings/src/ILivechatPriority'; @@ -26,11 +30,18 @@ import { Subscriptions, Rooms, Users, + LivechatContacts, } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { ObjectId } from 'mongodb'; +import { Livechat as LivechatTyped } from './LivechatTyped'; +import { queueInquiry, saveQueueInquiry } from './QueueManager'; +import { RoutingManager } from './RoutingManager'; +import { isVerifiedChannelInSource } from './contacts/isVerifiedChannelInSource'; +import { migrateVisitorIfMissingContact } from './contacts/migrateVisitorIfMissingContact'; +import { getOnlineAgents } from './getOnlineAgents'; import { callbacks } from '../../../../lib/callbacks'; import { validateEmail as validatorFunc } from '../../../../lib/emailValidator'; import { i18n } from '../../../../server/lib/i18n'; @@ -44,9 +55,6 @@ import { notifyOnSubscriptionChanged, } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; -import { Livechat as LivechatTyped } from './LivechatTyped'; -import { queueInquiry, saveQueueInquiry } from './QueueManager'; -import { RoutingManager } from './RoutingManager'; const logger = new Logger('LivechatHelper'); export const allowAgentSkipQueue = (agent: SelectedAgent) => { @@ -59,21 +67,13 @@ export const allowAgentSkipQueue = (agent: SelectedAgent) => { return hasRoleAsync(agent.agentId, 'bot'); }; -export const createLivechatRoom = async < - E extends Record & { - sla?: string; - customFields?: Record; - source?: OmnichannelSourceType; - }, ->( +export const createLivechatRoom = async ( rid: string, - name: string, guest: ILivechatVisitor, - roomInfo: Partial = {}, - extraData?: E, + roomInfo: IOmnichannelRoomInfo = { source: { type: OmnichannelSourceType.OTHER } }, + extraData?: IOmnichannelRoomExtraData, ) => { check(rid, String); - check(name, String); check( guest, Match.ObjectIncluding({ @@ -85,7 +85,7 @@ export const createLivechatRoom = async < ); const extraRoomInfo = await callbacks.run('livechat.beforeRoom', roomInfo, extraData); - const { _id, username, token, department: departmentId, status = 'online', contactId } = guest; + const { _id, username, token, department: departmentId, status = 'online' } = guest; const newRoomAt = new Date(); const { activity } = guest; @@ -94,13 +94,30 @@ export const createLivechatRoom = async < visitor: { _id, username, departmentId, status, activity }, }); + const source = extraRoomInfo.source || roomInfo.source; + + if (settings.get('Livechat_Require_Contact_Verification') === 'always') { + await LivechatContacts.updateContactChannel({ visitorId: _id, source }, { verified: false }); + } + + const contactId = await migrateVisitorIfMissingContact(_id, source); + const contact = + contactId && + (await LivechatContacts.findOneById>(contactId, { + projection: { name: 1, channels: 1 }, + })); + if (!contact) { + throw new Error('error-invalid-contact'); + } + const verified = Boolean(contact.channels.some((channel) => isVerifiedChannelInSource(channel, _id, source))); + // TODO: Solve `u` missing issue const room: InsertionModel = { _id: rid, msgs: 0, usersCount: 1, lm: newRoomAt, - fname: name, + fname: contact.name, t: 'l' as const, ts: newRoomAt, departmentId, @@ -109,12 +126,13 @@ export const createLivechatRoom = async < username, token, status, - contactId, ...(activity?.length && { activity }), }, + contactId, cl: false, open: true, waitingResponse: true, + verified, // this should be overridden by extraRoomInfo when provided // in case it's not provided, we'll use this "default" type source: { @@ -162,7 +180,7 @@ export const createLivechatInquiry = async ({ guest?: Pick; message?: string; initialStatus?: LivechatInquiryStatus; - extraData?: Pick; + extraData?: IOmnichannelInquiryExtraData; }) => { check(rid, String); check(name, String); @@ -403,13 +421,12 @@ export const dispatchInquiryQueued = async (inquiry: ILivechatInquiryRecord, age await saveQueueInquiry(inquiry); // Alert only the online agents of the queued request - const onlineAgents = await LivechatTyped.getOnlineAgents(department, agent); + const onlineAgents = await getOnlineAgents(department, agent); if (!onlineAgents) { logger.debug('Cannot notify agents of queued inquiry. No online agents found'); return; } - logger.debug(`Notifying ${await onlineAgents.count()} agents of new inquiry`); const notificationUserName = v && (v.name || v.username); for await (const agent of onlineAgents) { diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index e521ac98fe71..b94b070537ea 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -1,13 +1,9 @@ -import dns from 'dns'; -import * as util from 'util'; - import { Apps, AppEvents } from '@rocket.chat/apps'; import { Message, VideoConf, api, Omnichannel } from '@rocket.chat/core-services'; import type { IOmnichannelRoom, IOmnichannelRoomClosingInfo, IUser, - MessageTypesValues, ILivechatVisitor, SelectedAgent, ILivechatAgent, @@ -15,16 +11,15 @@ import type { ILivechatDepartment, AtLeast, TransferData, - MessageAttachment, - IMessageInbox, IOmnichannelAgent, - ILivechatDepartmentAgents, - LivechatDepartmentDTO, ILivechatInquiryRecord, - ILivechatContact, - ILivechatContactChannel, + UserStatus, + IOmnichannelRoomInfo, + IOmnichannelRoomExtraData, + IOmnichannelSource, + ILivechatContactVisitorAssociation, } from '@rocket.chat/core-typings'; -import { OmnichannelSourceType, ILivechatAgentStatus, UserStatus, isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { ILivechatAgentStatus, isOmnichannelRoom } from '@rocket.chat/core-typings'; import { Logger, type MainLogger } from '@rocket.chat/logger'; import { LivechatDepartment, @@ -43,12 +38,12 @@ import { import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import type { Filter, FindCursor, ClientSession, MongoError } from 'mongodb'; +import type { Filter, ClientSession } from 'mongodb'; import UAParser from 'ua-parser-js'; import { callbacks } from '../../../../lib/callbacks'; import { trim } from '../../../../lib/utils/stringUtils'; -import { client } from '../../../../server/database/utils'; +import { client, shouldRetryTransaction } from '../../../../server/database/utils'; import { i18n } from '../../../../server/lib/i18n'; import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; import { removeUserFromRolesAsync } from '../../../../server/lib/roles/removeUserFromRoles'; @@ -65,67 +60,25 @@ import { notifyOnRoomChangedById, notifyOnLivechatInquiryChangedByToken, notifyOnUserChange, - notifyOnLivechatDepartmentAgentChangedByDepartmentId, notifyOnSubscriptionChangedByRoomId, notifyOnSubscriptionChanged, } from '../../../lib/server/lib/notifyListener'; -import * as Mailer from '../../../mailer/server/api'; import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; import { businessHourManager } from '../business-hour'; -import { createContact, createContactFromVisitor, isSingleContactEnabled } from './Contacts'; -import { parseAgentCustomFields, updateDepartmentAgents, validateEmail, normalizeTransferredByData } from './Helper'; +import { parseAgentCustomFields, updateDepartmentAgents, normalizeTransferredByData } from './Helper'; import { QueueManager } from './QueueManager'; import { RoutingManager } from './RoutingManager'; -import { isDepartmentCreationAvailable } from './isDepartmentCreationAvailable'; -import type { CloseRoomParams, CloseRoomParamsByUser, CloseRoomParamsByVisitor } from './localTypes'; +import { Visitors, type RegisterGuestType } from './Visitors'; +import { registerGuestData } from './contacts/registerGuestData'; +import { getRequiredDepartment } from './departmentsLib'; +import type { CloseRoomParams, CloseRoomParamsByUser, CloseRoomParamsByVisitor, ILivechatMessage } from './localTypes'; import { parseTranscriptRequest } from './parseTranscriptRequest'; -type RegisterGuestType = Partial> & { - id?: string; - connectionData?: any; - email?: string; - phone?: { number: string }; -}; - -type OfflineMessageData = { - message: string; - name: string; - email: string; - department?: string; - host?: string; -}; - -type UploadedFile = { - _id: string; - name?: string; - type?: string; - size?: number; - description?: string; - identify?: { size: { width: number; height: number } }; - format?: string; -}; - -export interface ILivechatMessage { - token: string; - _id: string; - rid: string; - msg: string; - file?: UploadedFile; - files?: UploadedFile[]; - attachments?: MessageAttachment[]; - alias?: string; - groupable?: boolean; - blocks?: IMessage['blocks']; - email?: IMessageInbox['email']; -} - type AKeyOf = { [K in keyof T]?: T[K]; }; -type PageInfo = { title: string; location: { href: string }; change: string }; - type ICRMData = { _id: string; label?: string; @@ -153,8 +106,6 @@ const isRoomClosedByUserParams = (params: CloseRoomParams): params is CloseRoomP const isRoomClosedByVisitorParams = (params: CloseRoomParams): params is CloseRoomParamsByVisitor => (params as CloseRoomParamsByVisitor).visitor !== undefined; -const dnsResolveMx = util.promisify(dns.resolveMx); - class LivechatClass { logger: Logger; @@ -176,7 +127,7 @@ class LivechatClass { Livechat.logger.debug(`Fetching online bot agents for department ${department}`); const botAgents = await Livechat.getBotAgents(department); if (botAgents) { - const onlineBots = await botAgents.count(); + const onlineBots = await Livechat.countBotAgents(department); this.logger.debug(`Found ${onlineBots} online`); if (onlineBots > 0) { return true; @@ -189,27 +140,6 @@ class LivechatClass { return agentsOnline; } - async getOnlineAgents(department?: string, agent?: SelectedAgent | null): Promise | undefined> { - if (agent?.agentId) { - return Users.findOnlineAgents(agent.agentId); - } - - if (department) { - const departmentAgents = await LivechatDepartmentAgents.getOnlineForDepartment(department); - if (!departmentAgents) { - return; - } - - const agentIds = await departmentAgents.map(({ agentId }) => agentId).toArray(); - if (!agentIds.length) { - return; - } - - return Users.findByIds([...new Set(agentIds)]); - } - return Users.findOnlineAgents(); - } - async closeRoom(params: CloseRoomParams, attempts = 2): Promise { let newRoom: IOmnichannelRoom; let chatCloser: ChatCloser; @@ -228,10 +158,7 @@ class LivechatClass { this.logger.error({ err: e, msg: 'Failed to close room', afterAttempts: attempts }); await session.abortTransaction(); // Dont propagate transaction errors - if ( - (e as unknown as MongoError)?.errorLabels?.includes('UnknownTransactionCommitResult') || - (e as unknown as MongoError)?.errorLabels?.includes('TransientTransactionError') - ) { + if (shouldRetryTransaction(e)) { if (attempts > 0) { this.logger.debug(`Retrying close room because of transient error. Attempts left: ${attempts}`); return this.closeRoom(params, attempts - 1); @@ -397,22 +324,14 @@ class LivechatClass { return { room: newRoom, closedBy: closeData.closedBy, removedInquiry: inquiry }; } - async getRequiredDepartment(onlineRequired = true) { - const departments = LivechatDepartment.findEnabledWithAgents(); - - for await (const dept of departments) { - if (!dept.showOnRegistration) { - continue; - } - if (!onlineRequired) { - return dept; - } - - const onlineAgents = await LivechatDepartmentAgents.getOnlineForDepartment(dept._id); - if (onlineAgents && (await onlineAgents.count())) { - return dept; - } - } + private makeVisitorAssociation(visitorId: string, roomInfo: IOmnichannelSource): ILivechatContactVisitorAssociation { + return { + visitorId, + source: { + type: roomInfo.type, + id: roomInfo.id, + }, + }; } async createRoom({ @@ -426,21 +345,22 @@ class LivechatClass { visitor: ILivechatVisitor; message?: string; rid?: string; - roomInfo: { - source?: IOmnichannelRoom['source']; - [key: string]: unknown; - }; + roomInfo: IOmnichannelRoomInfo; agent?: SelectedAgent; - extraData?: Record; + extraData?: IOmnichannelRoomExtraData; }) { if (!settings.get('Livechat_enabled')) { throw new Meteor.Error('error-omnichannel-is-disabled'); } + if (await LivechatContacts.isChannelBlocked(this.makeVisitorAssociation(visitor._id, roomInfo.source))) { + throw new Error('error-contact-channel-blocked'); + } + const defaultAgent = await callbacks.run('livechat.checkDefaultAgentOnNewRoom', agent, visitor); // if no department selected verify if there is at least one active and pick the first if (!defaultAgent && !visitor.department) { - const department = await this.getRequiredDepartment(); + const department = await getRequiredDepartment(); Livechat.logger.debug(`No department or default agent selected for ${visitor._id}`); if (department) { @@ -461,55 +381,6 @@ class LivechatClass { extraData, }); - if (isSingleContactEnabled()) { - let { contactId } = visitor; - - if (!contactId) { - const visitorContact = await LivechatVisitors.findOne< - Pick - >(visitor._id, { - projection: { - name: 1, - contactManager: 1, - livechatData: 1, - phone: 1, - visitorEmails: 1, - username: 1, - contactId: 1, - }, - }); - - contactId = visitorContact?.contactId; - } - - if (!contactId) { - // ensure that old visitors have a contact - contactId = await createContactFromVisitor(visitor); - } - - const contact = await LivechatContacts.findOneById>(contactId, { - projection: { _id: 1, channels: 1 }, - }); - - if (contact) { - const channel = contact.channels?.find( - (channel: ILivechatContactChannel) => channel.name === roomInfo.source?.type && channel.visitorId === visitor._id, - ); - - if (!channel) { - Livechat.logger.debug(`Adding channel for contact ${contact._id}`); - - await LivechatContacts.addChannel(contact._id, { - name: roomInfo.source?.label || roomInfo.source?.type.toString() || OmnichannelSourceType.OTHER, - visitorId: visitor._id, - blocked: false, - verified: false, - details: roomInfo.source, - }); - } - } - } - Livechat.logger.debug(`Room obtained for visitor ${visitor._id} -> ${room._id}`); await Messages.setRoomIdByToken(visitor.token, room._id); @@ -517,21 +388,12 @@ class LivechatClass { return room; } - async getRoom< - E extends Record & { - sla?: string; - customFields?: Record; - source?: OmnichannelSourceType; - }, - >( + async getRoom( guest: ILivechatVisitor, message: Pick, - roomInfo: { - source?: IOmnichannelRoom['source']; - [key: string]: unknown; - }, + roomInfo: IOmnichannelRoomInfo, agent?: SelectedAgent, - extraData?: E, + extraData?: IOmnichannelRoomExtraData, ) { if (!settings.get('Livechat_enabled')) { throw new Meteor.Error('error-omnichannel-is-disabled'); @@ -539,6 +401,10 @@ class LivechatClass { Livechat.logger.debug(`Attempting to find or create a room for visitor ${guest._id}`); const room = await LivechatRooms.findOneById(message.rid); + if (room?.v._id && (await LivechatContacts.isChannelBlocked(this.makeVisitorAssociation(room.v._id, room.source)))) { + throw new Error('error-contact-channel-blocked'); + } + if (room && !room.open) { Livechat.logger.debug(`Last room for visitor ${guest._id} closed. Creating new one`); } @@ -582,30 +448,6 @@ class LivechatClass { return Users.checkOnlineAgents(); } - async setDepartmentForGuest({ token, department }: { token: string; department: string }) { - check(token, String); - check(department, String); - - Livechat.logger.debug(`Switching departments for user with token ${token} (to ${department})`); - - const updateUser = { - $set: { - department, - }, - }; - - const dep = await LivechatDepartment.findOneById>(department, { projection: { _id: 1 } }); - if (!dep) { - throw new Meteor.Error('invalid-department', 'Provided department does not exists'); - } - - const visitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } }); - if (!visitor) { - throw new Meteor.Error('invalid-token', 'Provided token is invalid'); - } - await LivechatVisitors.updateById(visitor._id, updateUser); - } - async removeRoom(rid: string) { Livechat.logger.debug(`Deleting room ${rid}`); check(rid, String); @@ -640,107 +482,14 @@ class LivechatClass { } } - isValidObject(obj: unknown): obj is Record { - return typeof obj === 'object' && obj !== null; - } - - async registerGuest({ - id, - token, - name, - phone, - email, - department, - username, - connectionData, - status = UserStatus.ONLINE, - }: RegisterGuestType): Promise { - check(token, String); - check(id, Match.Maybe(String)); - - Livechat.logger.debug(`New incoming conversation: id: ${id} | token: ${token}`); - - const visitorDataToUpdate: Partial & { userAgent?: string; ip?: string; host?: string } = { - token, - status, - ...(phone?.number ? { phone: [{ phoneNumber: phone.number }] } : {}), - ...(name ? { name } : {}), - }; - - if (email) { - const visitorEmail = email.trim().toLowerCase(); - validateEmail(visitorEmail); - visitorDataToUpdate.visitorEmails = [{ address: visitorEmail }]; - } - - const livechatVisitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } }); - - if (livechatVisitor?.department !== department && department) { - Livechat.logger.debug(`Attempt to find a department with id/name ${department}`); - const dep = await LivechatDepartment.findOneByIdOrName(department, { projection: { _id: 1 } }); - if (!dep) { - Livechat.logger.debug(`Invalid department provided: ${department}`); - throw new Meteor.Error('error-invalid-department', 'The provided department is invalid'); - } - Livechat.logger.debug(`Assigning visitor ${token} to department ${dep._id}`); - visitorDataToUpdate.department = dep._id; - } - - visitorDataToUpdate.token = livechatVisitor?.token || token; - - let existingUser = null; - - if (livechatVisitor) { - Livechat.logger.debug('Found matching user by token'); - visitorDataToUpdate._id = livechatVisitor._id; - } else if (phone?.number && (existingUser = await LivechatVisitors.findOneVisitorByPhone(phone.number))) { - Livechat.logger.debug('Found matching user by phone number'); - visitorDataToUpdate._id = existingUser._id; - // Don't change token when matching by phone number, use current visitor token - visitorDataToUpdate.token = existingUser.token; - } else if (email && (existingUser = await LivechatVisitors.findOneGuestByEmailAddress(email))) { - Livechat.logger.debug('Found matching user by email'); - visitorDataToUpdate._id = existingUser._id; - } else if (!livechatVisitor) { - Livechat.logger.debug(`No matches found. Attempting to create new user with token ${token}`); - - visitorDataToUpdate._id = id || undefined; - visitorDataToUpdate.username = username || (await LivechatVisitors.getNextVisitorUsername()); - visitorDataToUpdate.status = status; - visitorDataToUpdate.ts = new Date(); - - if (settings.get('Livechat_Allow_collect_and_store_HTTP_header_informations') && Livechat.isValidObject(connectionData)) { - Livechat.logger.debug(`Saving connection data for visitor ${token}`); - const { httpHeaders, clientAddress } = connectionData; - if (Livechat.isValidObject(httpHeaders)) { - visitorDataToUpdate.userAgent = httpHeaders['user-agent']; - visitorDataToUpdate.ip = httpHeaders['x-real-ip'] || httpHeaders['x-forwarded-for'] || clientAddress; - visitorDataToUpdate.host = httpHeaders?.host; - } - } - } - - if (isSingleContactEnabled()) { - const contactId = await createContact({ - name: name ?? (visitorDataToUpdate.username as string), - emails: email ? [email] : [], - phones: phone ? [phone.number] : [], - unknown: true, - }); - visitorDataToUpdate.contactId = contactId; - } - - const upsertedLivechatVisitor = await LivechatVisitors.updateOneByIdOrToken(visitorDataToUpdate, { - upsert: true, - returnDocument: 'after', - }); + async registerGuest(newData: RegisterGuestType): Promise { + const result = await Visitors.registerGuest(newData); - if (!upsertedLivechatVisitor.value) { - Livechat.logger.debug(`No visitor found after upsert`); - return null; + if (result) { + await registerGuestData(newData, result); } - return upsertedLivechatVisitor.value; + return result; } private async getBotAgents(department?: string) { @@ -751,6 +500,14 @@ class LivechatClass { return Users.findBotAgents(); } + private async countBotAgents(department?: string) { + if (department) { + return LivechatDepartmentAgents.countBotsForDepartment(department); + } + + return Users.countBotAgents(); + } + private async resolveChatTags( room: IOmnichannelRoom, options: CloseRoomParams['options'] = {}, @@ -815,16 +572,6 @@ class LivechatClass { }; } - private async sendEmail(from: string, to: string, replyTo: string, subject: string, html: string): Promise { - await Mailer.send({ - to, - from, - replyTo, - subject, - html, - }); - } - async sendRequest( postData: { type: string; @@ -963,57 +710,6 @@ class LivechatClass { }); } - async getRoomMessages({ rid }: { rid: string }) { - const room = await Rooms.findOneById(rid, { projection: { t: 1 } }); - if (room?.t !== 'l') { - throw new Meteor.Error('invalid-room'); - } - - const ignoredMessageTypes: MessageTypesValues[] = [ - 'livechat_navigation_history', - 'livechat_transcript_history', - 'command', - 'livechat-close', - 'livechat-started', - 'livechat_video_call', - ]; - - return Messages.findVisibleByRoomIdNotContainingTypes(rid, ignoredMessageTypes, { - sort: { ts: 1 }, - }); - } - - async archiveDepartment(_id: string) { - const department = await LivechatDepartment.findOneById>(_id, { - projection: { _id: 1, businessHourId: 1 }, - }); - - if (!department) { - throw new Error('department-not-found'); - } - - await Promise.all([LivechatDepartmentAgents.disableAgentsByDepartmentId(_id), LivechatDepartment.archiveDepartment(_id)]); - - void notifyOnLivechatDepartmentAgentChangedByDepartmentId(_id); - - await callbacks.run('livechat.afterDepartmentArchived', department); - } - - async unarchiveDepartment(_id: string) { - const department = await LivechatDepartment.findOneById(_id, { projection: { _id: 1 } }); - - if (!department) { - throw new Meteor.Error('department-not-found'); - } - - // TODO: these kind of actions should be on events instead of here - await Promise.all([LivechatDepartmentAgents.enableAgentsByDepartmentId(_id), LivechatDepartment.unarchiveDepartment(_id)]); - - void notifyOnLivechatDepartmentAgentChangedByDepartmentId(_id); - - return true; - } - async updateMessage({ guest, message }: { guest: ILivechatVisitor; message: AtLeast }) { check(message, Match.ObjectIncluding({ _id: String })); @@ -1149,69 +845,6 @@ class LivechatClass { return rcSettings; } - async sendOfflineMessage(data: OfflineMessageData) { - if (!settings.get('Livechat_display_offline_form')) { - throw new Error('error-offline-form-disabled'); - } - - const { message, name, email, department, host } = data; - - if (!email) { - throw new Error('error-invalid-email'); - } - - const emailMessage = `${message}`.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1
$2'); - - let html = '

New livechat message

'; - if (host && host !== '') { - html = html.concat(`

Sent from: ${host}

`); - } - html = html.concat(` -

Visitor name: ${name}

-

Visitor email: ${email}

-

Message:
${emailMessage}

`); - - const fromEmail = settings.get('From_Email').match(/\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}\b/i); - - let from: string; - if (fromEmail) { - from = fromEmail[0]; - } else { - from = settings.get('From_Email'); - } - - if (settings.get('Livechat_validate_offline_email')) { - const emailDomain = email.substr(email.lastIndexOf('@') + 1); - - try { - await dnsResolveMx(emailDomain); - } catch (e) { - throw new Meteor.Error('error-invalid-email-address'); - } - } - - // TODO Block offline form if Livechat_offline_email is undefined - // (it does not make sense to have an offline form that does nothing) - // `this.sendEmail` will throw an error if the email is invalid - // thus this breaks livechat, since the "to" email is invalid, and that returns an [invalid email] error to the livechat client - let emailTo = settings.get('Livechat_offline_email'); - if (department && department !== '') { - const dep = await LivechatDepartment.findOneByIdOrName(department, { projection: { email: 1 } }); - if (dep) { - emailTo = dep.email || emailTo; - } - } - - const fromText = `${name} - ${email} <${from}>`; - const replyTo = `${name} <${email}>`; - const subject = `Livechat offline message from ${name}: ${`${emailMessage}`.substring(0, 20)}`; - await this.sendEmail(fromText, emailTo, replyTo, subject, html); - - setImmediate(() => { - void callbacks.run('livechat.offlineMessage', data); - }); - } - async sendMessage({ guest, message, @@ -1220,10 +853,7 @@ class LivechatClass { }: { guest: ILivechatVisitor; message: ILivechatMessage; - roomInfo: { - source?: IOmnichannelRoom['source']; - [key: string]: unknown; - }; + roomInfo: IOmnichannelRoomInfo; agent?: SelectedAgent; }) { const { room, newRoom } = await this.getRoom(guest, message, roomInfo, agent); @@ -1508,53 +1138,6 @@ class LivechatClass { return true; } - async savePageHistory(token: string, roomId: string | undefined, pageInfo: PageInfo) { - this.logger.debug({ - msg: `Saving page movement history for visitor with token ${token}`, - pageInfo, - roomId, - }); - - if (pageInfo.change !== settings.get('Livechat_history_monitor_type')) { - return; - } - const user = await Users.findOneById('rocket.cat'); - - if (!user) { - throw new Error('error-invalid-user'); - } - - const pageTitle = pageInfo.title; - const pageUrl = pageInfo.location.href; - const extraData: { - navigation: { - page: PageInfo; - token: string; - }; - expireAt?: number; - _hidden?: boolean; - } = { - navigation: { - page: pageInfo, - token, - }, - }; - - if (!roomId) { - this.logger.warn(`Saving page history without room id for visitor with token ${token}`); - // keep history of unregistered visitors for 1 month - const keepHistoryMiliseconds = 2592000000; - extraData.expireAt = new Date().getTime() + keepHistoryMiliseconds; - } - - if (!settings.get('Livechat_Visitor_navigation_as_a_message')) { - extraData._hidden = true; - } - - // @ts-expect-error: Investigating on which case we won't receive a roomId and where that history is supposed to be stored - return Message.saveSystemMessage('livechat_navigation_history', roomId, `${pageTitle} - ${pageUrl}`, user, extraData); - } - async afterRemoveAgent(user: AtLeast) { await callbacks.run('livechat.afterAgentRemoved', { agent: user }); return true; @@ -1733,41 +1316,6 @@ class LivechatClass { return false; } - async saveDepartmentAgents( - _id: string, - departmentAgents: { - upsert?: Pick[]; - remove?: Pick[]; - }, - ) { - check(_id, String); - check(departmentAgents, { - upsert: Match.Maybe([ - Match.ObjectIncluding({ - agentId: String, - username: String, - count: Match.Maybe(Match.Integer), - order: Match.Maybe(Match.Integer), - }), - ]), - remove: Match.Maybe([ - Match.ObjectIncluding({ - agentId: String, - username: Match.Maybe(String), - count: Match.Maybe(Match.Integer), - order: Match.Maybe(Match.Integer), - }), - ]), - }); - - const department = await LivechatDepartment.findOneById>(_id, { projection: { enabled: 1 } }); - if (!department) { - throw new Meteor.Error('error-department-not-found', 'Department not found'); - } - - return updateDepartmentAgents(_id, departmentAgents, department.enabled); - } - async saveRoomInfo( roomData: { _id: string; @@ -1839,135 +1387,6 @@ class LivechatClass { return true; } - - /** - * @param {string|null} _id - The department id - * @param {Partial} departmentData - * @param {{upsert?: { agentId: string; count?: number; order?: number; }[], remove?: { agentId: string; count?: number; order?: number; }}} [departmentAgents] - The department agents - * @param {{_id?: string}} [departmentUnit] - The department's unit id - */ - async saveDepartment( - userId: string, - _id: string | null, - departmentData: LivechatDepartmentDTO, - departmentAgents?: { - upsert?: { agentId: string; count?: number; order?: number }[]; - remove?: { agentId: string; count?: number; order?: number }; - }, - departmentUnit?: { _id?: string }, - ) { - check(_id, Match.Maybe(String)); - if (departmentUnit?._id !== undefined && typeof departmentUnit._id !== 'string') { - throw new Meteor.Error('error-invalid-department-unit', 'Invalid department unit id provided', { - method: 'livechat:saveDepartment', - }); - } - - const department = _id - ? await LivechatDepartment.findOneById(_id, { projection: { _id: 1, archived: 1, enabled: 1, parentId: 1 } }) - : null; - - if (departmentUnit && !departmentUnit._id && department && department.parentId) { - const isLastDepartmentInUnit = (await LivechatDepartment.countDepartmentsInUnit(department.parentId)) === 1; - if (isLastDepartmentInUnit) { - throw new Meteor.Error('error-unit-cant-be-empty', "The last department in a unit can't be removed", { - method: 'livechat:saveDepartment', - }); - } - } - - if (!department && !(await isDepartmentCreationAvailable())) { - throw new Meteor.Error('error-max-departments-number-reached', 'Maximum number of departments reached', { - method: 'livechat:saveDepartment', - }); - } - - if (department?.archived && departmentData.enabled) { - throw new Meteor.Error('error-archived-department-cant-be-enabled', 'Archived departments cant be enabled', { - method: 'livechat:saveDepartment', - }); - } - - const defaultValidations: Record | BooleanConstructor | StringConstructor> = { - enabled: Boolean, - name: String, - description: Match.Optional(String), - showOnRegistration: Boolean, - email: String, - showOnOfflineForm: Boolean, - requestTagBeforeClosingChat: Match.Optional(Boolean), - chatClosingTags: Match.Optional([String]), - fallbackForwardDepartment: Match.Optional(String), - departmentsAllowedToForward: Match.Optional([String]), - allowReceiveForwardOffline: Match.Optional(Boolean), - }; - - // The Livechat Form department support addition/custom fields, so those fields need to be added before validating - Object.keys(departmentData).forEach((field) => { - if (!defaultValidations.hasOwnProperty(field)) { - defaultValidations[field] = Match.OneOf(String, Match.Integer, Boolean); - } - }); - - check(departmentData, defaultValidations); - check( - departmentAgents, - Match.Maybe({ - upsert: Match.Maybe(Array), - remove: Match.Maybe(Array), - }), - ); - - const { requestTagBeforeClosingChat, chatClosingTags, fallbackForwardDepartment } = departmentData; - if (requestTagBeforeClosingChat && (!chatClosingTags || chatClosingTags.length === 0)) { - throw new Meteor.Error( - 'error-validating-department-chat-closing-tags', - 'At least one closing tag is required when the department requires tag(s) on closing conversations.', - { method: 'livechat:saveDepartment' }, - ); - } - - if (_id && !department) { - throw new Meteor.Error('error-department-not-found', 'Department not found', { - method: 'livechat:saveDepartment', - }); - } - - if (fallbackForwardDepartment === _id) { - throw new Meteor.Error( - 'error-fallback-department-circular', - 'Cannot save department. Circular reference between fallback department and department', - ); - } - - if (fallbackForwardDepartment) { - const fallbackDep = await LivechatDepartment.findOneById(fallbackForwardDepartment, { - projection: { _id: 1, fallbackForwardDepartment: 1 }, - }); - if (!fallbackDep) { - throw new Meteor.Error('error-fallback-department-not-found', 'Fallback department not found', { - method: 'livechat:saveDepartment', - }); - } - } - - const departmentDB = await LivechatDepartment.createOrUpdateDepartment(_id, departmentData); - if (departmentDB && departmentAgents) { - await updateDepartmentAgents(departmentDB._id, departmentAgents, departmentDB.enabled); - } - - // Disable event - if (department?.enabled && !departmentDB?.enabled) { - await callbacks.run('livechat.afterDepartmentDisabled', departmentDB); - } - - if (departmentUnit) { - await callbacks.run('livechat.manageDepartmentUnit', { userId, departmentId: departmentDB._id, unitId: departmentUnit._id }); - } - - return departmentDB; - } } export const Livechat = new LivechatClass(); -export * from './localTypes'; diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index c6728d470870..fb1469bcde8b 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -1,20 +1,28 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; import { Omnichannel } from '@rocket.chat/core-services'; -import type { ILivechatDepartment } from '@rocket.chat/core-typings'; -import { - LivechatInquiryStatus, - type ILivechatInquiryRecord, - type ILivechatVisitor, - type IOmnichannelRoom, - type SelectedAgent, - type OmnichannelSourceType, +import type { + ILivechatDepartment, + IOmnichannelRoomInfo, + IOmnichannelRoomExtraData, + AtLeast, + ILivechatInquiryRecord, + ILivechatVisitor, + IOmnichannelRoom, + SelectedAgent, } from '@rocket.chat/core-typings'; +import { LivechatInquiryStatus } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; -import { LivechatDepartment, LivechatDepartmentAgents, LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models'; +import { LivechatContacts, LivechatDepartment, LivechatDepartmentAgents, LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { createLivechatRoom, createLivechatInquiry, allowAgentSkipQueue } from './Helper'; +import { Livechat } from './LivechatTyped'; +import { RoutingManager } from './RoutingManager'; +import { isVerifiedChannelInSource } from './contacts/isVerifiedChannelInSource'; +import { getOnlineAgents } from './getOnlineAgents'; +import { getInquirySortMechanismSetting } from './settings'; import { dispatchInquiryPosition } from '../../../../ee/app/livechat-enterprise/server/lib/Helper'; import { callbacks } from '../../../../lib/callbacks'; import { sendNotification } from '../../../lib/server'; @@ -25,10 +33,6 @@ import { } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { i18n } from '../../../utils/lib/i18n'; -import { createLivechatRoom, createLivechatInquiry, allowAgentSkipQueue } from './Helper'; -import { Livechat } from './LivechatTyped'; -import { RoutingManager } from './RoutingManager'; -import { getInquirySortMechanismSetting } from './settings'; const logger = new Logger('QueueManager'); @@ -94,8 +98,7 @@ export class QueueManager { const inquiryAgent = await RoutingManager.delegateAgent(defaultAgent, inquiry); logger.debug(`Delegating inquiry with id ${inquiry._id} to agent ${defaultAgent?.username}`); - await callbacks.run('livechat.beforeRouteChat', inquiry, inquiryAgent); - const dbInquiry = await LivechatInquiry.findOneById(inquiry._id); + const dbInquiry = await callbacks.run('livechat.beforeRouteChat', inquiry, inquiryAgent); if (!dbInquiry) { throw new Error('inquiry-not-found'); @@ -118,10 +121,20 @@ export class QueueManager { return this.fnQueueInquiryStatus({ room, agent }); } + const needVerification = ['once', 'always'].includes(settings.get('Livechat_Require_Contact_Verification')); + + if (needVerification && !(await this.isRoomContactVerified(room))) { + return LivechatInquiryStatus.VERIFYING; + } + if (!(await Omnichannel.isWithinMACLimit(room))) { return LivechatInquiryStatus.QUEUED; } + if (settings.get('Livechat_waiting_queue')) { + return LivechatInquiryStatus.QUEUED; + } + if (RoutingManager.getConfig()?.autoAssignAgent) { return LivechatInquiryStatus.READY; } @@ -133,41 +146,86 @@ export class QueueManager { return LivechatInquiryStatus.READY; } - static async queueInquiry(inquiry: ILivechatInquiryRecord, room: IOmnichannelRoom, defaultAgent?: SelectedAgent | null) { - if (inquiry.status === 'ready') { + static async processNewInquiry(inquiry: ILivechatInquiryRecord, room: IOmnichannelRoom, defaultAgent?: SelectedAgent | null) { + if (inquiry.status === LivechatInquiryStatus.VERIFYING) { + logger.debug({ msg: 'Inquiry is waiting for contact verification. Ignoring it', inquiry, defaultAgent }); + + if (defaultAgent) { + await LivechatInquiry.setDefaultAgentById(inquiry._id, defaultAgent); + } + return; + } + + if (inquiry.status === LivechatInquiryStatus.READY) { + logger.debug({ msg: 'Inquiry is ready. Delegating', inquiry, defaultAgent }); return RoutingManager.delegateInquiry(inquiry, defaultAgent, undefined, room); } - await callbacks.run('livechat.afterInquiryQueued', inquiry); + if (inquiry.status === LivechatInquiryStatus.QUEUED) { + await callbacks.run('livechat.afterInquiryQueued', inquiry); - void callbacks.run('livechat.chatQueued', room); + void callbacks.run('livechat.chatQueued', room); - await this.dispatchInquiryQueued(inquiry, room, defaultAgent); + return this.dispatchInquiryQueued(inquiry, room, defaultAgent); + } } - static async requestRoom< - E extends Record & { - sla?: string; - customFields?: Record; - source?: OmnichannelSourceType; - }, - >({ + static async verifyInquiry(inquiry: ILivechatInquiryRecord, room: IOmnichannelRoom) { + if (inquiry.status !== LivechatInquiryStatus.VERIFYING) { + return; + } + + const { defaultAgent: agent } = inquiry; + + const newStatus = await QueueManager.getInquiryStatus({ room, agent }); + + if (newStatus === inquiry.status) { + throw new Error('error-failed-to-verify-inquiry'); + } + + const newInquiry = await LivechatInquiry.setStatusById(inquiry._id, newStatus); + + await this.processNewInquiry(newInquiry, room, agent); + + const newRoom = await LivechatRooms.findOneById>(room._id, { + projection: { servedBy: 1, departmentId: 1 }, + }); + + if (!newRoom) { + logger.error(`Room with id ${room._id} not found after inquiry verification.`); + throw new Error('room-not-found'); + } + + await this.dispatchInquiryPosition(inquiry, newRoom); + } + + static async isRoomContactVerified(room: IOmnichannelRoom): Promise { + if (!room.contactId) { + return false; + } + + const contact = await LivechatContacts.findOneById(room.contactId, { projection: { channels: 1 } }); + if (!contact) { + return false; + } + + return Boolean(contact.channels.some((channel) => isVerifiedChannelInSource(channel, room.v._id, room.source))); + } + + static async requestRoom({ guest, rid = Random.id(), message, roomInfo, agent, - extraData: { customFields, ...extraData } = {} as E, + extraData: { customFields, ...extraData } = {}, }: { guest: ILivechatVisitor; rid?: string; message?: string; - roomInfo: { - source?: IOmnichannelRoom['source']; - [key: string]: unknown; - }; + roomInfo: IOmnichannelRoomInfo; agent?: SelectedAgent; - extraData?: E; + extraData?: IOmnichannelRoomExtraData; }) { logger.debug(`Requesting a room for guest ${guest._id}`); check( @@ -221,9 +279,7 @@ export class QueueManager { } } - const name = (roomInfo?.fname as string) || guest.name || guest.username; - - const room = await createLivechatRoom(rid, name, { ...guest, ...(department && { department }) }, roomInfo, { + const room = await createLivechatRoom(rid, { ...guest, ...(department && { department }) }, roomInfo, { ...extraData, ...(Boolean(customFields) && { customFields }), }); @@ -236,7 +292,7 @@ export class QueueManager { const inquiry = await createLivechatInquiry({ rid, - name, + name: room.fname, initialStatus: await this.getInquiryStatus({ room, agent: defaultAgent }), guest, message, @@ -255,16 +311,31 @@ export class QueueManager { void notifyOnSettingChanged(livechatSetting); } - const newRoom = (await this.queueInquiry(inquiry, room, defaultAgent)) ?? (await LivechatRooms.findOneById(rid)); + await this.processNewInquiry(inquiry, room, defaultAgent); + const newRoom = await LivechatRooms.findOneById(rid); + if (!newRoom) { logger.error(`Room with id ${rid} not found`); throw new Error('room-not-found'); } - if (!newRoom.servedBy && settings.get('Omnichannel_calculate_dispatch_service_queue_statistics')) { + await this.dispatchInquiryPosition(inquiry, newRoom); + return newRoom; + } + + static async dispatchInquiryPosition( + inquiry: ILivechatInquiryRecord, + room: AtLeast, + ): Promise { + if ( + !room.servedBy && + inquiry.status !== LivechatInquiryStatus.VERIFYING && + settings.get('Livechat_waiting_queue') && + settings.get('Omnichannel_calculate_dispatch_service_queue_statistics') + ) { const [inq] = await LivechatInquiry.getCurrentSortedQueueAsync({ inquiryId: inquiry._id, - department, + department: room.departmentId, queueSortBy: getInquirySortMechanismSetting(), }); @@ -272,8 +343,6 @@ export class QueueManager { void dispatchInquiryPosition(inq); } } - - return newRoom; } static async unarchiveRoom(archivedRoom: IOmnichannelRoom) { @@ -329,18 +398,21 @@ export class QueueManager { } private static dispatchInquiryQueued = async (inquiry: ILivechatInquiryRecord, room: IOmnichannelRoom, agent?: SelectedAgent | null) => { + if (RoutingManager.getConfig()?.autoAssignAgent) { + return; + } + logger.debug(`Notifying agents of new inquiry ${inquiry._id} queued`); const { department, rid, v } = inquiry; // Alert only the online agents of the queued request - const onlineAgents = await Livechat.getOnlineAgents(department, agent); + const onlineAgents = await getOnlineAgents(department, agent); if (!onlineAgents) { logger.debug('Cannot notify agents of queued inquiry. No online agents found'); return; } - logger.debug(`Notifying ${await onlineAgents.count()} agents of new inquiry`); const notificationUserName = v && (v.name || v.username); for await (const agent of onlineAgents) { diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index 28e5c72efc16..123c56f4d31f 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -1,5 +1,5 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; -import { Message, Omnichannel } from '@rocket.chat/core-services'; +import { Message } from '@rocket.chat/core-services'; import type { ILivechatInquiryRecord, ILivechatVisitor, @@ -12,15 +12,11 @@ import type { TransferData, } from '@rocket.chat/core-typings'; import { LivechatInquiryStatus } from '@rocket.chat/core-typings'; -import { License } from '@rocket.chat/license'; import { Logger } from '@rocket.chat/logger'; import { LivechatInquiry, LivechatRooms, Subscriptions, Rooms, Users } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../../../lib/callbacks'; -import { notifyOnLivechatInquiryChangedById, notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener'; -import { settings } from '../../../settings/server'; import { createLivechatSubscription, dispatchAgentDelegated, @@ -31,12 +27,14 @@ import { updateChatDepartment, allowAgentSkipQueue, } from './Helper'; +import { callbacks } from '../../../../lib/callbacks'; +import { notifyOnLivechatInquiryChangedById, notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener'; +import { settings } from '../../../settings/server'; const logger = new Logger('RoutingManager'); type Routing = { methods: Record; - startQueue(): Promise; isMethodSet(): boolean; registerMethod(name: string, Method: IRoutingMethodConstructor): void; getMethod(): IRoutingMethod; @@ -68,16 +66,6 @@ type Routing = { export const RoutingManager: Routing = { methods: {}, - async startQueue() { - const shouldPreventQueueStart = await License.shouldPreventAction('monthlyActiveContacts'); - - if (shouldPreventQueueStart) { - logger.error('Monthly Active Contacts limit reached. Queue will not start'); - return; - } - void (await Omnichannel.getQueueWorker()).shouldStart(); - }, - isMethodSet() { return settings.get('Livechat_Routing_Method') !== ''; }, diff --git a/apps/meteor/app/livechat/server/lib/Visitors.ts b/apps/meteor/app/livechat/server/lib/Visitors.ts new file mode 100644 index 000000000000..c7b4430df363 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/Visitors.ts @@ -0,0 +1,110 @@ +import { UserStatus, type ILivechatVisitor } from '@rocket.chat/core-typings'; +import { Logger } from '@rocket.chat/logger'; +import { LivechatDepartment, LivechatVisitors } from '@rocket.chat/models'; + +import { validateEmail } from './Helper'; +import { settings } from '../../../settings/server'; + +const logger = new Logger('Livechat - Visitor'); + +export type RegisterGuestType = Partial> & { + id?: string; + connectionData?: any; + email?: string; + phone?: { number: string }; +}; + +export const Visitors = { + isValidObject(obj: unknown): obj is Record { + return typeof obj === 'object' && obj !== null; + }, + + async registerGuest({ + id, + token, + name, + phone, + email, + department, + username, + connectionData, + status = UserStatus.ONLINE, + }: RegisterGuestType): Promise { + check(token, String); + check(id, Match.Maybe(String)); + + logger.debug(`New incoming conversation: id: ${id} | token: ${token}`); + + const visitorDataToUpdate: Partial & { userAgent?: string; ip?: string; host?: string } = { + token, + status, + ...(phone?.number && { phone: [{ phoneNumber: phone.number }] }), + ...(name && { name }), + }; + + if (email) { + const visitorEmail = email.trim().toLowerCase(); + validateEmail(visitorEmail); + visitorDataToUpdate.visitorEmails = [{ address: visitorEmail }]; + } + + const livechatVisitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } }); + + if (department && livechatVisitor?.department !== department) { + logger.debug(`Attempt to find a department with id/name ${department}`); + const dep = await LivechatDepartment.findOneByIdOrName(department, { projection: { _id: 1 } }); + if (!dep) { + logger.debug(`Invalid department provided: ${department}`); + throw new Meteor.Error('error-invalid-department', 'The provided department is invalid'); + } + logger.debug(`Assigning visitor ${token} to department ${dep._id}`); + visitorDataToUpdate.department = dep._id; + } + + visitorDataToUpdate.token = livechatVisitor?.token || token; + + let existingUser = null; + + if (livechatVisitor) { + logger.debug('Found matching user by token'); + visitorDataToUpdate._id = livechatVisitor._id; + } else if (phone?.number && (existingUser = await LivechatVisitors.findOneVisitorByPhone(phone.number))) { + logger.debug('Found matching user by phone number'); + visitorDataToUpdate._id = existingUser._id; + // Don't change token when matching by phone number, use current visitor token + visitorDataToUpdate.token = existingUser.token; + } else if (email && (existingUser = await LivechatVisitors.findOneGuestByEmailAddress(email))) { + logger.debug('Found matching user by email'); + visitorDataToUpdate._id = existingUser._id; + } else if (!livechatVisitor) { + logger.debug(`No matches found. Attempting to create new user with token ${token}`); + + visitorDataToUpdate._id = id || undefined; + visitorDataToUpdate.username = username || (await LivechatVisitors.getNextVisitorUsername()); + visitorDataToUpdate.status = status; + visitorDataToUpdate.ts = new Date(); + + if (settings.get('Livechat_Allow_collect_and_store_HTTP_header_informations') && this.isValidObject(connectionData)) { + logger.debug(`Saving connection data for visitor ${token}`); + const { httpHeaders, clientAddress } = connectionData; + if (this.isValidObject(httpHeaders)) { + visitorDataToUpdate.userAgent = httpHeaders['user-agent']; + visitorDataToUpdate.ip = httpHeaders['x-real-ip'] || httpHeaders['x-forwarded-for'] || clientAddress; + visitorDataToUpdate.host = httpHeaders?.host; + } + } + } + + const upsertedLivechatVisitor = await LivechatVisitors.updateOneByIdOrToken(visitorDataToUpdate, { + upsert: true, + returnDocument: 'after', + }); + + if (!upsertedLivechatVisitor.value) { + logger.debug(`No visitor found after upsert`); + return null; + } + + return upsertedLivechatVisitor.value; + }, +}; diff --git a/apps/meteor/app/livechat/server/lib/analytics/dashboards.ts b/apps/meteor/app/livechat/server/lib/analytics/dashboards.ts index dd0d54970065..cf5a0abd5d54 100644 --- a/apps/meteor/app/livechat/server/lib/analytics/dashboards.ts +++ b/apps/meteor/app/livechat/server/lib/analytics/dashboards.ts @@ -247,11 +247,11 @@ const getConversationsMetricsAsync = async ({ language: user.language || settings.get('Language') || 'en', })) || []; const metrics = ['Total_conversations', 'Open_conversations', 'On_Hold_conversations', 'Total_messages']; - const visitorsCount = await LivechatVisitors.getVisitorsBetweenDate({ + const visitorsCount = await LivechatVisitors.countVisitorsBetweenDate({ start: new Date(start), end: new Date(end), department: departmentId, - }).count(); + }); return { totalizers: [ ...totalizers.filter((metric: { title: string }) => metrics.includes(metric.title)), diff --git a/apps/meteor/app/livechat/server/lib/contacts/ContactMerger.ts b/apps/meteor/app/livechat/server/lib/contacts/ContactMerger.ts new file mode 100644 index 000000000000..48b6dcc41f9a --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/ContactMerger.ts @@ -0,0 +1,320 @@ +import type { + ILivechatContact, + ILivechatVisitor, + ILivechatContactChannel, + ILivechatContactConflictingField, + IUser, + DeepWritable, + IOmnichannelSource, +} from '@rocket.chat/core-typings'; +import { LivechatContacts } from '@rocket.chat/models'; +import type { ClientSession, UpdateFilter } from 'mongodb'; + +import { getContactManagerIdByUsername } from './getContactManagerIdByUsername'; +import { isSameChannel } from '../../../lib/isSameChannel'; + +type ManagerValue = { id: string } | { username: string }; +type ContactFields = { + email: string; + phone: string; + name: string; + username: string; + manager: ManagerValue; + channel: ILivechatContactChannel; +}; + +type CustomFieldAndValue = { type: `customFields.${string}`; value: string }; + +export type FieldAndValue = + | { type: keyof Omit; value: string } + | { type: 'manager'; value: ManagerValue } + | { type: 'channel'; value: ILivechatContactChannel } + | CustomFieldAndValue; + +type ConflictHandlingMode = 'conflict' | 'overwrite' | 'ignore'; + +type MergeFieldsIntoContactParams = { + fields: FieldAndValue[]; + contact: ILivechatContact; + conflictHandlingMode?: ConflictHandlingMode; + session?: ClientSession; +}; + +export class ContactMerger { + private managerList = new Map['username'], IUser['_id'] | undefined>(); + + private getManagerId(manager: ManagerValue): IUser['_id'] | undefined { + if ('id' in manager) { + return manager.id; + } + + return this.managerList.get(manager.username); + } + + private isSameManager(manager1: ManagerValue, manager2: ManagerValue): boolean { + if ('id' in manager1 && 'id' in manager2) { + return manager1.id === manager2.id; + } + if ('username' in manager1 && 'username' in manager2) { + return manager1.username === manager2.username; + } + + const id1 = this.getManagerId(manager1); + const id2 = this.getManagerId(manager2); + + if (!id1 || !id2) { + return false; + } + + return id1 === id2; + } + + private isSameField(field1: FieldAndValue, field2: FieldAndValue): boolean { + if (field1.type === 'manager' && field2.type === 'manager') { + return this.isSameManager(field1.value, field2.value); + } + + if (field1.type === 'channel' && field2.type === 'channel') { + return isSameChannel(field1.value.visitor, field2.value.visitor); + } + + if (field1.type !== field2.type) { + return false; + } + + if (field1.value === field2.value) { + return true; + } + + return false; + } + + private async loadDataForFields(session: ClientSession | undefined, ...fieldLists: FieldAndValue[][]): Promise { + for await (const fieldList of fieldLists) { + for await (const field of fieldList) { + if (field.type !== 'manager' || 'id' in field.value) { + continue; + } + + if (!field.value.username) { + continue; + } + + if (this.managerList.has(field.value.username)) { + continue; + } + + const id = await getContactManagerIdByUsername(field.value.username, session); + this.managerList.set(field.value.username, id); + } + } + } + + static async createWithFields(session: ClientSession | undefined, ...fieldLists: FieldAndValue[][]): Promise { + const merger = new ContactMerger(); + await merger.loadDataForFields(session, ...fieldLists); + + return merger; + } + + static getAllFieldsFromContact(contact: ILivechatContact): FieldAndValue[] { + const { customFields = {}, name, contactManager } = contact; + + const fields = new Set(); + + contact.emails?.forEach(({ address: value }) => fields.add({ type: 'email', value })); + contact.phones?.forEach(({ phoneNumber: value }) => fields.add({ type: 'phone', value })); + contact.channels.forEach((value) => fields.add({ type: 'channel', value })); + + if (name) { + fields.add({ type: 'name', value: name }); + } + + if (contactManager) { + fields.add({ type: 'manager', value: { id: contactManager } }); + } + + Object.keys(customFields).forEach((key) => + fields.add({ type: `customFields.${key}`, value: customFields[key] } as CustomFieldAndValue), + ); + + // If the contact already has conflicts, load their values as well + if (contact.conflictingFields) { + for (const conflict of contact.conflictingFields) { + fields.add({ type: conflict.field, value: conflict.value } as FieldAndValue); + } + } + + return [...fields]; + } + + static async getAllFieldsFromVisitor(visitor: ILivechatVisitor, source?: IOmnichannelSource): Promise { + const { livechatData: customFields = {}, contactManager, name, username } = visitor; + + const fields = new Set(); + + visitor.visitorEmails?.forEach(({ address: value }) => fields.add({ type: 'email', value })); + visitor.phone?.forEach(({ phoneNumber: value }) => fields.add({ type: 'phone', value })); + if (name) { + fields.add({ type: 'name', value: name }); + } + if (username) { + fields.add({ type: 'username', value: username }); + } + if (contactManager?.username) { + fields.add({ type: 'manager', value: { username: contactManager?.username } }); + } + Object.keys(customFields).forEach((key) => + fields.add({ type: `customFields.${key}`, value: customFields[key] } as CustomFieldAndValue), + ); + + if (source) { + fields.add({ + type: 'channel', + value: { + name: source.label || source.type.toString(), + visitor: { + visitorId: visitor._id, + source: { + type: source.type, + id: source.id, + }, + }, + blocked: false, + verified: false, + details: source, + }, + }); + } + + return [...fields]; + } + + static getFieldValuesByType(fields: FieldAndValue[], type: T): ContactFields[T][] { + return fields.filter((field) => field.type === type).map(({ value }) => value) as ContactFields[T][]; + } + + static async mergeFieldsIntoContact({ + fields, + contact, + conflictHandlingMode = 'conflict', + session, + }: MergeFieldsIntoContactParams): Promise { + const existingFields = ContactMerger.getAllFieldsFromContact(contact); + const overwriteData = conflictHandlingMode === 'overwrite'; + + const merger = await ContactMerger.createWithFields(session, fields, existingFields); + + const newFields = fields.filter((field) => { + // If the field already exists with the same value, ignore it + if (existingFields.some((existingField) => merger.isSameField(existingField, field))) { + return false; + } + + // If the field is an username and the contact already has a name, ignore it as well + if (field.type === 'username' && existingFields.some(({ type }) => type === 'name')) { + return false; + } + + return true; + }); + + const newPhones = ContactMerger.getFieldValuesByType(newFields, 'phone'); + const newEmails = ContactMerger.getFieldValuesByType(newFields, 'email'); + const newChannels = ContactMerger.getFieldValuesByType(newFields, 'channel'); + const newNamesOnly = ContactMerger.getFieldValuesByType(newFields, 'name'); + const newCustomFields = newFields.filter(({ type }) => type.startsWith('customFields.')) as CustomFieldAndValue[]; + // Usernames are ignored unless the contact has no other name + const newUsernames = !contact.name && !newNamesOnly.length ? ContactMerger.getFieldValuesByType(newFields, 'username') : []; + + const dataToSet: DeepWritable['$set']> = {}; + + // Names, Managers and Custom Fields need are set as conflicting fields if the contact already has them + const newNames = [...newNamesOnly, ...newUsernames]; + const newManagers = ContactMerger.getFieldValuesByType(newFields, 'manager') + .map((manager) => { + if ('id' in manager) { + return manager.id; + } + return merger.getManagerId(manager); + }) + .filter((id) => Boolean(id)); + + if (newNames.length && (!contact.name || overwriteData)) { + const firstName = newNames.shift(); + if (firstName) { + dataToSet.name = firstName; + } + } + + if (newManagers.length && (!contact.contactManager || overwriteData)) { + const firstManager = newManagers.shift(); + if (firstManager) { + dataToSet.contactManager = firstManager; + } + } + + const customFieldsPerName = new Map(); + for (const customField of newCustomFields) { + if (!customFieldsPerName.has(customField.type)) { + customFieldsPerName.set(customField.type, []); + } + customFieldsPerName.get(customField.type)?.push(customField); + } + + for (const [key, customFields] of customFieldsPerName) { + const fieldName = key.replace('customFields.', ''); + + // If the contact does not have this custom field yet, save the first value directly to the contact instead of as a conflict + if (!contact.customFields?.[fieldName] || overwriteData) { + const first = customFields.shift(); + if (first) { + dataToSet[key] = first.value; + } + } + } + + const allConflicts: ILivechatContactConflictingField[] = + conflictHandlingMode !== 'conflict' + ? [] + : [ + ...newNames.map((name): ILivechatContactConflictingField => ({ field: 'name', value: name })), + ...newManagers.map((manager): ILivechatContactConflictingField => ({ field: 'manager', value: manager as string })), + ]; + + // Phones, Emails and Channels are simply added to the contact's existing list + const dataToAdd: UpdateFilter['$addToSet'] = { + ...(newPhones.length ? { phones: { $each: newPhones.map((phoneNumber) => ({ phoneNumber })) } } : {}), + ...(newEmails.length ? { emails: { $each: newEmails.map((address) => ({ address })) } } : {}), + ...(newChannels.length ? { channels: { $each: newChannels } } : {}), + ...(allConflicts.length ? { conflictingFields: { $each: allConflicts } } : {}), + }; + + if (newChannels.length) { + dataToSet.preRegistration = false; + } + + const updateData: UpdateFilter = { + ...(Object.keys(dataToSet).length ? { $set: dataToSet } : {}), + ...(Object.keys(dataToAdd).length ? { $addToSet: dataToAdd } : {}), + }; + + if (Object.keys(updateData).length) { + await LivechatContacts.updateById(contact._id, updateData, { session }); + } + } + + public static async mergeVisitorIntoContact( + visitor: ILivechatVisitor, + contact: ILivechatContact, + source?: IOmnichannelSource, + ): Promise { + const fields = await ContactMerger.getAllFieldsFromVisitor(visitor, source); + + await ContactMerger.mergeFieldsIntoContact({ + fields, + contact, + conflictHandlingMode: contact.unknown ? 'overwrite' : 'conflict', + }); + } +} diff --git a/apps/meteor/app/livechat/server/lib/contacts/addContactEmail.ts b/apps/meteor/app/livechat/server/lib/contacts/addContactEmail.ts new file mode 100644 index 000000000000..e41267ff961d --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/addContactEmail.ts @@ -0,0 +1,20 @@ +import type { ILivechatContact } from '@rocket.chat/core-typings'; +import { LivechatContacts } from '@rocket.chat/models'; + +/** + * Adds a new email into the contact's email list, if the email is already in the list it does not add anything + * and simply return the data, since the email was aready registered :P + * + * @param contactId the id of the contact that will be updated + * @param email the email that will be added to the contact + * @returns the updated contact + */ +export async function addContactEmail(contactId: ILivechatContact['_id'], email: string): Promise { + const contact = await LivechatContacts.addEmail(contactId, email); + + if (!contact) { + throw new Error('error-contact-not-found'); + } + + return contact; +} diff --git a/apps/meteor/app/livechat/server/lib/contacts/createContact.ts b/apps/meteor/app/livechat/server/lib/contacts/createContact.ts new file mode 100644 index 000000000000..98cf238d9b5e --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/createContact.ts @@ -0,0 +1,52 @@ +import type { ILivechatContactChannel, IVisitorLastChat } from '@rocket.chat/core-typings'; +import { LivechatContacts } from '@rocket.chat/models'; + +import { getAllowedCustomFields } from './getAllowedCustomFields'; +import { validateContactManager } from './validateContactManager'; +import { validateCustomFields } from './validateCustomFields'; + +export type CreateContactParams = { + name: string; + emails?: string[]; + phones?: string[]; + unknown: boolean; + customFields?: Record; + lastChat?: IVisitorLastChat; + contactManager?: string; + channels?: ILivechatContactChannel[]; + importIds?: string[]; + shouldValidateCustomFields?: boolean; +}; + +export async function createContact({ + name, + emails, + phones, + customFields: receivedCustomFields = {}, + lastChat, + contactManager, + channels = [], + unknown, + importIds, + shouldValidateCustomFields = true, +}: CreateContactParams): Promise { + if (contactManager) { + await validateContactManager(contactManager); + } + + const customFields = shouldValidateCustomFields + ? validateCustomFields(await getAllowedCustomFields(), receivedCustomFields) + : receivedCustomFields; + + return LivechatContacts.insertContact({ + name, + emails: emails?.map((address) => ({ address })), + phones: phones?.map((phoneNumber) => ({ phoneNumber })), + contactManager, + channels, + customFields, + lastChat, + unknown, + ...(importIds?.length && { importIds }), + }); +} diff --git a/apps/meteor/app/livechat/server/lib/contacts/createContactFromVisitor.ts b/apps/meteor/app/livechat/server/lib/contacts/createContactFromVisitor.ts new file mode 100644 index 000000000000..c541399d3b35 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/createContactFromVisitor.ts @@ -0,0 +1,24 @@ +import type { ILivechatVisitor, IOmnichannelSource } from '@rocket.chat/core-typings'; +import { LivechatRooms } from '@rocket.chat/models'; + +import { createContact } from './createContact'; +import { mapVisitorToContact } from './mapVisitorToContact'; + +export async function createContactFromVisitor(visitor: ILivechatVisitor, source: IOmnichannelSource): Promise { + const contactData = await mapVisitorToContact(visitor, source); + + const contactId = await createContact(contactData); + + await LivechatRooms.setContactByVisitorAssociation( + { + visitorId: visitor._id, + source: { type: source.type, ...(source.id ? { id: source.id } : {}) }, + }, + { + _id: contactId, + name: contactData.name, + }, + ); + + return contactId; +} diff --git a/apps/meteor/app/livechat/server/lib/contacts/getAllowedCustomFields.ts b/apps/meteor/app/livechat/server/lib/contacts/getAllowedCustomFields.ts new file mode 100644 index 000000000000..d71f902c1122 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/getAllowedCustomFields.ts @@ -0,0 +1,12 @@ +import type { ILivechatCustomField } from '@rocket.chat/core-typings'; +import { LivechatCustomField } from '@rocket.chat/models'; + +export async function getAllowedCustomFields(): Promise[]> { + return LivechatCustomField.findByScope( + 'visitor', + { + projection: { _id: 1, label: 1, regexp: 1, required: 1 }, + }, + false, + ).toArray(); +} diff --git a/apps/meteor/app/livechat/server/lib/contacts/getContactChannelsGrouped.ts b/apps/meteor/app/livechat/server/lib/contacts/getContactChannelsGrouped.ts new file mode 100644 index 000000000000..9cf83224708b --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/getContactChannelsGrouped.ts @@ -0,0 +1,26 @@ +import type { ILivechatContact, ILivechatContactChannel } from '@rocket.chat/core-typings'; +import { LivechatContacts } from '@rocket.chat/models'; + +export async function getContactChannelsGrouped(contactId: string): Promise { + const contact = await LivechatContacts.findOneById>(contactId, { projection: { channels: 1 } }); + + if (!contact?.channels) { + return []; + } + + const groupedChannels = new Map(); + + contact.channels.forEach((channel: ILivechatContactChannel) => { + const existingChannel = groupedChannels.get(channel.name); + + if (!existingChannel) { + return groupedChannels.set(channel.name, channel); + } + + if ((channel.lastChat?.ts?.valueOf() || 0) > (existingChannel?.lastChat?.ts?.valueOf() || 0)) { + groupedChannels.set(channel.name, channel); + } + }); + + return [...groupedChannels.values()]; +} diff --git a/apps/meteor/app/livechat/server/lib/contacts/getContactHistory.ts b/apps/meteor/app/livechat/server/lib/contacts/getContactHistory.ts new file mode 100644 index 000000000000..e569ece4bd8c --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/getContactHistory.ts @@ -0,0 +1,76 @@ +import type { ILivechatContact, IOmnichannelRoom } from '@rocket.chat/core-typings'; +import type { FindPaginated } from '@rocket.chat/model-typings'; +import { LivechatContacts, LivechatRooms } from '@rocket.chat/models'; +import { makeFunction } from '@rocket.chat/patch-injection'; +import type { PaginatedResult, VisitorSearchChatsResult } from '@rocket.chat/rest-typings'; +import type { FindOptions, Sort, FindCursor } from 'mongodb'; + +export type GetContactHistoryParams = { + contactId: string; + source?: string; + count: number; + offset: number; + sort: Sort; +}; + +export const fetchContactHistory = makeFunction( + async ({ + contactId, + options, + }: { + contactId: string; + options?: FindOptions; + extraParams?: Record; + }): Promise>> => + LivechatRooms.findClosedRoomsByContactPaginated({ + contactId, + options, + }), +); + +export const getContactHistory = makeFunction( + async (params: GetContactHistoryParams): Promise> => { + const { contactId, count, offset, sort } = params; + + const contact = await LivechatContacts.findOneById>(contactId, { projection: { _id: 1 } }); + + if (!contact) { + throw new Error('error-contact-not-found'); + } + + const options: FindOptions = { + sort: sort || { closedAt: -1 }, + skip: offset, + limit: count, + projection: { + fname: 1, + ts: 1, + v: 1, + msgs: 1, + servedBy: 1, + closedAt: 1, + closedBy: 1, + closer: 1, + tags: 1, + source: 1, + lastMessage: 1, + verified: 1, + }, + }; + + const { totalCount, cursor } = await fetchContactHistory({ + contactId: contact._id, + options, + extraParams: params, + }); + + const [total, history] = await Promise.all([totalCount, cursor.toArray()]); + + return { + history, + count: history.length, + offset, + total, + }; + }, +); diff --git a/apps/meteor/app/livechat/server/lib/contacts/getContactIdByVisitor.ts b/apps/meteor/app/livechat/server/lib/contacts/getContactIdByVisitor.ts new file mode 100644 index 000000000000..922b05a7472f --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/getContactIdByVisitor.ts @@ -0,0 +1,8 @@ +import type { ILivechatContact, ILivechatContactVisitorAssociation } from '@rocket.chat/core-typings'; +import { LivechatContacts } from '@rocket.chat/models'; + +export async function getContactIdByVisitor(visitor: ILivechatContactVisitorAssociation): Promise { + const contact = await LivechatContacts.findOneByVisitor>(visitor, { projection: { _id: 1 } }); + + return contact?._id; +} diff --git a/apps/meteor/app/livechat/server/lib/contacts/getContactManagerIdByUsername.ts b/apps/meteor/app/livechat/server/lib/contacts/getContactManagerIdByUsername.ts new file mode 100644 index 000000000000..c43776a08879 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/getContactManagerIdByUsername.ts @@ -0,0 +1,12 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; +import type { ClientSession } from 'mongodb'; + +export async function getContactManagerIdByUsername( + username: Required['username'], + session?: ClientSession, +): Promise { + const user = await Users.findOneByUsername>(username, { projection: { _id: 1 }, session }); + + return user?._id; +} diff --git a/apps/meteor/app/livechat/server/lib/contacts/getContacts.ts b/apps/meteor/app/livechat/server/lib/contacts/getContacts.ts new file mode 100644 index 000000000000..09b7a2545a1c --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/getContacts.ts @@ -0,0 +1,50 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { LivechatContacts, Users } from '@rocket.chat/models'; +import type { PaginatedResult, ILivechatContactWithManagerData } from '@rocket.chat/rest-typings'; +import type { FindCursor, Sort } from 'mongodb'; + +export type GetContactsParams = { + searchText?: string; + count: number; + offset: number; + sort: Sort; + unknown?: boolean; +}; + +export async function getContacts(params: GetContactsParams): Promise> { + const { searchText, count, offset, sort, unknown } = params; + + const { cursor, totalCount } = LivechatContacts.findPaginatedContacts( + { searchText, unknown }, + { + limit: count, + skip: offset, + sort: sort ?? { name: 1 }, + }, + ); + + const [rawContacts, total] = await Promise.all([cursor.toArray(), totalCount]); + + const managerIds = [...new Set(rawContacts.map(({ contactManager }) => contactManager))]; + const managersCursor: FindCursor<[string, Pick]> = Users.findByIds(managerIds, { + projection: { name: 1, username: 1 }, + }).map((manager) => [manager._id, manager]); + const managersData = await managersCursor.toArray(); + const mappedManagers = Object.fromEntries(managersData); + + const contacts: ILivechatContactWithManagerData[] = rawContacts.map((contact) => { + const { contactManager, ...data } = contact; + + return { + ...data, + ...(contactManager ? { contactManager: mappedManagers[contactManager] } : {}), + }; + }); + + return { + contacts, + count, + offset, + total, + }; +} diff --git a/apps/meteor/app/livechat/server/lib/contacts/isAgentAvailableToTakeContactInquiry.ts b/apps/meteor/app/livechat/server/lib/contacts/isAgentAvailableToTakeContactInquiry.ts new file mode 100644 index 000000000000..a0daa07ac2f0 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/isAgentAvailableToTakeContactInquiry.ts @@ -0,0 +1,12 @@ +import type { ILivechatContact, ILivechatVisitor, IOmnichannelSource } from '@rocket.chat/core-typings'; +import { makeFunction } from '@rocket.chat/patch-injection'; + +export const isAgentAvailableToTakeContactInquiry = makeFunction( + async ( + _visitorId: ILivechatVisitor['_id'], + _source: IOmnichannelSource, + _contactId: ILivechatContact['_id'], + ): Promise<{ error: string; value: false } | { value: true }> => ({ + value: true, + }), +); diff --git a/apps/meteor/app/livechat/server/lib/contacts/isVerifiedChannelInSource.spec.ts b/apps/meteor/app/livechat/server/lib/contacts/isVerifiedChannelInSource.spec.ts new file mode 100644 index 000000000000..a49d94a0f5fc --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/isVerifiedChannelInSource.spec.ts @@ -0,0 +1,110 @@ +import { expect } from 'chai'; +import proxyquire from 'proxyquire'; + +const { isVerifiedChannelInSource } = proxyquire.noCallThru().load('./isVerifiedChannelInSource', {}); + +describe('isVerifiedChannelInSource', () => { + it('should return false if channel is not verified', () => { + const channel = { + verified: false, + }; + const visitorId = 'visitor1'; + const source = { + type: 'widget', + }; + + expect(isVerifiedChannelInSource(channel, visitorId, source)).to.be.false; + }); + + it('should return false if channel visitorId is different from visitorId', () => { + const channel = { + verified: true, + visitor: { + visitorId: 'visitor2', + }, + }; + const visitorId = 'visitor1'; + const source = { + type: 'widget', + }; + + expect(isVerifiedChannelInSource(channel, visitorId, source)).to.be.false; + }); + + it('should return false if channel visitor source type is different from source type', () => { + const channel = { + verified: true, + visitor: { + visitorId: 'visitor1', + source: { + type: 'web', + }, + }, + }; + const visitorId = 'visitor1'; + const source = { + type: 'widget', + }; + + expect(isVerifiedChannelInSource(channel, visitorId, source)).to.be.false; + }); + + it('should return false if channel visitor source id is different from source id', () => { + const channel = { + verified: true, + visitor: { + visitorId: 'visitor1', + source: { + type: 'widget', + id: 'source1', + }, + }, + }; + const visitorId = 'visitor1'; + const source = { + type: 'widget', + id: 'source2', + }; + + expect(isVerifiedChannelInSource(channel, visitorId, source)).to.be.false; + }); + + it('should return false if source id is not defined and channel visitor source id is defined', () => { + const channel = { + verified: true, + visitor: { + visitorId: 'visitor1', + source: { + type: 'widget', + id: 'source1', + }, + }, + }; + const visitorId = 'visitor1'; + const source = { + type: 'widget', + }; + + expect(isVerifiedChannelInSource(channel, visitorId, source)).to.be.false; + }); + + it('should return true if all conditions are met', () => { + const channel = { + verified: true, + visitor: { + visitorId: 'visitor1', + source: { + type: 'widget', + id: 'source1', + }, + }, + }; + const visitorId = 'visitor1'; + const source = { + type: 'widget', + id: 'source1', + }; + + expect(isVerifiedChannelInSource(channel, visitorId, source)).to.be.true; + }); +}); diff --git a/apps/meteor/app/livechat/server/lib/contacts/isVerifiedChannelInSource.ts b/apps/meteor/app/livechat/server/lib/contacts/isVerifiedChannelInSource.ts new file mode 100644 index 000000000000..14074e81a3d3 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/isVerifiedChannelInSource.ts @@ -0,0 +1,25 @@ +import type { ILivechatContactChannel, ILivechatVisitor, IOmnichannelSource } from '@rocket.chat/core-typings'; + +export const isVerifiedChannelInSource = ( + channel: ILivechatContactChannel, + visitorId: ILivechatVisitor['_id'], + source: IOmnichannelSource, +) => { + if (!channel.verified) { + return false; + } + + if (channel.visitor.visitorId !== visitorId) { + return false; + } + + if (channel.visitor.source.type !== source.type) { + return false; + } + + if ((source.id || channel.visitor.source.id) && channel.visitor.source.id !== source.id) { + return false; + } + + return true; +}; diff --git a/apps/meteor/app/livechat/server/lib/contacts/mapVisitorToContact.spec.ts b/apps/meteor/app/livechat/server/lib/contacts/mapVisitorToContact.spec.ts new file mode 100644 index 000000000000..c8c15fe0d0d9 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/mapVisitorToContact.spec.ts @@ -0,0 +1,206 @@ +import { OmnichannelSourceType, type ILivechatVisitor, type IOmnichannelSource } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +import type { CreateContactParams } from './createContact'; + +const getContactManagerIdByUsername = sinon.stub(); +const getAllowedCustomFields = sinon.stub(); + +const { mapVisitorToContact } = proxyquire.noCallThru().load('./mapVisitorToContact', { + './getContactManagerIdByUsername': { + getContactManagerIdByUsername, + }, + './getAllowedCustomFields': { getAllowedCustomFields }, +}); + +const testDate = new Date(); +const dataMap: [Partial, IOmnichannelSource, CreateContactParams][] = [ + [ + { + _id: 'visitor1', + username: 'Username', + name: 'Name', + visitorEmails: [{ address: 'email1@domain.com' }, { address: 'email2@domain.com' }], + phone: [{ phoneNumber: '10' }, { phoneNumber: '20' }], + contactManager: { + username: 'user1', + }, + }, + { + type: OmnichannelSourceType.WIDGET, + }, + { + name: 'Name', + emails: ['email1@domain.com', 'email2@domain.com'], + phones: ['10', '20'], + unknown: true, + channels: [ + { + name: 'widget', + visitor: { + visitorId: 'visitor1', + source: { + type: OmnichannelSourceType.WIDGET, + }, + }, + blocked: false, + verified: false, + details: { + type: OmnichannelSourceType.WIDGET, + }, + }, + ], + customFields: undefined, + shouldValidateCustomFields: false, + contactManager: 'manager1', + }, + ], + + [ + { + _id: 'visitor1', + username: 'Username', + }, + { + type: OmnichannelSourceType.SMS, + }, + { + name: 'Username', + emails: undefined, + phones: undefined, + unknown: true, + channels: [ + { + name: 'sms', + visitor: { + visitorId: 'visitor1', + source: { + type: OmnichannelSourceType.SMS, + }, + }, + blocked: false, + verified: false, + details: { + type: OmnichannelSourceType.SMS, + }, + }, + ], + customFields: undefined, + shouldValidateCustomFields: false, + contactManager: undefined, + }, + ], + + [ + { + _id: 'visitor1', + username: 'Username', + activity: ['2024-11'], + lastChat: { + _id: 'last-chat-id', + ts: testDate, + }, + }, + { + type: OmnichannelSourceType.WIDGET, + }, + { + name: 'Username', + emails: undefined, + phones: undefined, + unknown: false, + channels: [ + { + name: 'sms', + visitor: { + visitorId: 'visitor1', + source: { + type: OmnichannelSourceType.WIDGET, + }, + }, + blocked: false, + verified: false, + details: { + type: OmnichannelSourceType.WIDGET, + }, + lastChat: { + _id: 'last-chat-id', + ts: testDate, + }, + }, + ], + customFields: undefined, + shouldValidateCustomFields: false, + lastChat: { + _id: 'last-chat-id', + ts: testDate, + }, + contactManager: undefined, + }, + ], + + [ + { + _id: 'visitor1', + username: 'Username', + livechatData: { + customFieldId: 'customFieldValue', + invalidCustomFieldId: 'invalidCustomFieldValue', + }, + activity: [], + }, + { + type: OmnichannelSourceType.WIDGET, + }, + { + name: 'Username', + emails: undefined, + phones: undefined, + unknown: true, + channels: [ + { + name: 'sms', + visitor: { + visitorId: 'visitor1', + source: { + type: OmnichannelSourceType.WIDGET, + }, + }, + blocked: false, + verified: false, + details: { + type: OmnichannelSourceType.WIDGET, + }, + }, + ], + customFields: { + customFieldId: 'customFieldValue', + }, + shouldValidateCustomFields: false, + contactManager: undefined, + }, + ], +]; + +describe('mapVisitorToContact', () => { + beforeEach(() => { + getContactManagerIdByUsername.reset(); + getContactManagerIdByUsername.callsFake((username) => { + if (username === 'user1') { + return 'manager1'; + } + + return undefined; + }); + getAllowedCustomFields.resolves([{ _id: 'customFieldId', label: 'custom-field-label' }]); + }); + + const index = 0; + for (const [visitor, source, contact] of dataMap) { + it(`should map an ILivechatVisitor + IOmnichannelSource to an ILivechatContact [${index}]`, async () => { + expect(await mapVisitorToContact(visitor, source)).to.be.deep.equal(contact); + }); + } +}); diff --git a/apps/meteor/app/livechat/server/lib/contacts/mapVisitorToContact.ts b/apps/meteor/app/livechat/server/lib/contacts/mapVisitorToContact.ts new file mode 100644 index 000000000000..6478fd1911f8 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/mapVisitorToContact.ts @@ -0,0 +1,40 @@ +import type { ILivechatVisitor, IOmnichannelSource } from '@rocket.chat/core-typings'; + +import type { CreateContactParams } from './createContact'; +import { getAllowedCustomFields } from './getAllowedCustomFields'; +import { getContactManagerIdByUsername } from './getContactManagerIdByUsername'; +import { validateCustomFields } from './validateCustomFields'; + +export async function mapVisitorToContact(visitor: ILivechatVisitor, source: IOmnichannelSource): Promise { + return { + name: visitor.name || visitor.username, + emails: visitor.visitorEmails?.map(({ address }) => address), + phones: visitor.phone?.map(({ phoneNumber }) => phoneNumber), + unknown: !visitor.activity || visitor.activity.length === 0, + channels: [ + { + name: source.label || source.type.toString(), + visitor: { + visitorId: visitor._id, + source: { + type: source.type, + ...(source.id ? { id: source.id } : {}), + }, + }, + blocked: false, + verified: false, + details: source, + lastChat: visitor.lastChat, + }, + ], + customFields: + visitor.livechatData && + validateCustomFields(await getAllowedCustomFields(), visitor.livechatData, { + ignoreAdditionalFields: true, + ignoreValidationErrors: true, + }), + shouldValidateCustomFields: false, + lastChat: visitor.lastChat, + contactManager: visitor.contactManager?.username && (await getContactManagerIdByUsername(visitor.contactManager.username)), + }; +} diff --git a/apps/meteor/app/livechat/server/lib/contacts/mergeContacts.ts b/apps/meteor/app/livechat/server/lib/contacts/mergeContacts.ts new file mode 100644 index 000000000000..123f3f7dd15b --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/mergeContacts.ts @@ -0,0 +1,8 @@ +import type { ILivechatContact, ILivechatContactVisitorAssociation } from '@rocket.chat/core-typings'; +import { makeFunction } from '@rocket.chat/patch-injection'; +import type { ClientSession } from 'mongodb'; + +export const mergeContacts = makeFunction( + async (_contactId: string, _visitor: ILivechatContactVisitorAssociation, _session?: ClientSession): Promise => + null, +); diff --git a/apps/meteor/app/livechat/server/lib/contacts/migrateVisitorIfMissingContact.ts b/apps/meteor/app/livechat/server/lib/contacts/migrateVisitorIfMissingContact.ts new file mode 100644 index 000000000000..852effa66370 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/migrateVisitorIfMissingContact.ts @@ -0,0 +1,25 @@ +import type { ILivechatVisitor, IOmnichannelSource, ILivechatContact } from '@rocket.chat/core-typings'; +import { LivechatVisitors } from '@rocket.chat/models'; + +import { livechatContactsLogger as logger } from '../logger'; +import { getContactIdByVisitor } from './getContactIdByVisitor'; +import { migrateVisitorToContactId } from './migrateVisitorToContactId'; + +export async function migrateVisitorIfMissingContact( + visitorId: ILivechatVisitor['_id'], + source: IOmnichannelSource, +): Promise { + logger.debug(`Detecting visitor's contact ID`); + // Check if there is any contact already linking to this visitorId and source + const contactId = await getContactIdByVisitor({ visitorId, source }); + if (contactId) { + return contactId; + } + + const visitor = await LivechatVisitors.findOneById(visitorId); + if (!visitor) { + throw new Error('Failed to migrate visitor data into Contact information: visitor not found.'); + } + + return migrateVisitorToContactId({ visitor, source, requireRoom: false }); +} diff --git a/apps/meteor/app/livechat/server/lib/contacts/migrateVisitorToContactId.spec.ts b/apps/meteor/app/livechat/server/lib/contacts/migrateVisitorToContactId.spec.ts new file mode 100644 index 000000000000..47dd3bdde974 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/migrateVisitorToContactId.spec.ts @@ -0,0 +1,69 @@ +import { expect } from 'chai'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const modelsMock = { + LivechatContacts: { + findContactMatchingVisitor: sinon.stub(), + }, + LivechatRooms: { + setContactByVisitorAssociation: sinon.stub(), + findNewestByContactVisitorAssociation: sinon.stub(), + }, +}; + +const createContactFromVisitor = sinon.stub(); +const mergeVisitorIntoContact = sinon.stub(); + +const { migrateVisitorToContactId } = proxyquire.noCallThru().load('./migrateVisitorToContactId', { + './createContactFromVisitor': { + createContactFromVisitor, + }, + './ContactMerger': { + ContactMerger: { + mergeVisitorIntoContact, + }, + }, + '@rocket.chat/models': modelsMock, + '../logger': { + livechatContactsLogger: { + debug: sinon.stub(), + }, + }, +}); + +describe('migrateVisitorToContactId', () => { + beforeEach(() => { + modelsMock.LivechatContacts.findContactMatchingVisitor.reset(); + modelsMock.LivechatRooms.setContactByVisitorAssociation.reset(); + modelsMock.LivechatRooms.findNewestByContactVisitorAssociation.reset(); + createContactFromVisitor.reset(); + mergeVisitorIntoContact.reset(); + }); + + it('should not create a contact if there is no source for the visitor', async () => { + expect(await migrateVisitorToContactId({ visitor: { _id: 'visitor1' } })).to.be.null; + }); + + it('should attempt to create a new contact if there is no free existing contact matching the visitor data', async () => { + modelsMock.LivechatContacts.findContactMatchingVisitor.resolves(undefined); + const visitor = { _id: 'visitor1' }; + const source = { type: 'other' }; + modelsMock.LivechatRooms.findNewestByContactVisitorAssociation.resolves({ _id: 'room1', v: { _id: visitor._id }, source }); + createContactFromVisitor.resolves('contactCreated'); + + expect(await migrateVisitorToContactId({ visitor: { _id: 'visitor1' }, source })).to.be.equal('contactCreated'); + }); + + it('should not attempt to create a new contact if one is found for the visitor', async () => { + const visitor = { _id: 'visitor1' }; + const contact = { _id: 'contact1' }; + const source = { type: 'sms' }; + modelsMock.LivechatRooms.findNewestByContactVisitorAssociation.resolves({ _id: 'room1', v: { _id: visitor._id }, source }); + modelsMock.LivechatContacts.findContactMatchingVisitor.resolves(contact); + createContactFromVisitor.resolves('contactCreated'); + + expect(await migrateVisitorToContactId({ visitor, source })).to.be.equal('contact1'); + expect(mergeVisitorIntoContact.calledOnceWith(visitor, contact, source)).to.be.true; + }); +}); diff --git a/apps/meteor/app/livechat/server/lib/contacts/migrateVisitorToContactId.ts b/apps/meteor/app/livechat/server/lib/contacts/migrateVisitorToContactId.ts new file mode 100644 index 000000000000..c0f8ad917d6f --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/migrateVisitorToContactId.ts @@ -0,0 +1,55 @@ +import type { ILivechatVisitor, IOmnichannelSource, ILivechatContact, IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { LivechatContacts, LivechatRooms } from '@rocket.chat/models'; + +import { livechatContactsLogger as logger } from '../logger'; +import { ContactMerger } from './ContactMerger'; +import { createContactFromVisitor } from './createContactFromVisitor'; + +/** + This function assumes you already ensured that the visitor is not yet linked to any contact +**/ +export async function migrateVisitorToContactId({ + visitor, + source, + requireRoom = true, +}: { + visitor: ILivechatVisitor; + source: IOmnichannelSource; + requireRoom?: boolean; +}): Promise { + if (requireRoom) { + // Do not migrate the visitor with this source if they have no rooms matching it + const anyRoom = await LivechatRooms.findNewestByContactVisitorAssociation>( + { visitorId: visitor._id, source }, + { + projection: { _id: 1 }, + }, + ); + + if (!anyRoom) { + return null; + } + } + + // Search for any contact that is not yet associated with any visitor and that have the same email or phone number as this visitor. + const existingContact = await LivechatContacts.findContactMatchingVisitor(visitor); + if (!existingContact) { + logger.debug(`Creating a new contact for existing visitor ${visitor._id}`); + return createContactFromVisitor(visitor, source); + } + + // There is already an existing contact with no linked visitors and matching this visitor's phone or email, so let's use it + logger.debug(`Adding channel to existing contact ${existingContact._id}`); + await ContactMerger.mergeVisitorIntoContact(visitor, existingContact, source); + + // Update all existing rooms matching the visitor id and source to set the contactId to them + await LivechatRooms.setContactByVisitorAssociation( + { + visitorId: visitor._id, + source, + }, + existingContact, + ); + + return existingContact._id; +} diff --git a/apps/meteor/app/livechat/server/lib/contacts/registerContact.spec.ts b/apps/meteor/app/livechat/server/lib/contacts/registerContact.spec.ts new file mode 100644 index 000000000000..b3afb1403a38 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/registerContact.spec.ts @@ -0,0 +1,138 @@ +import { expect } from 'chai'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const modelsMock = { + 'Users': { + findOneAgentById: sinon.stub(), + findOneByUsername: sinon.stub(), + }, + 'LivechatContacts': { + findOneById: sinon.stub(), + insertOne: sinon.stub(), + upsertContact: sinon.stub(), + updateContact: sinon.stub(), + findContactMatchingVisitor: sinon.stub(), + findOneByVisitorId: sinon.stub(), + }, + 'LivechatRooms': { + findNewestByVisitorIdOrToken: sinon.stub(), + setContactIdByVisitorIdOrToken: sinon.stub(), + findByVisitorId: sinon.stub(), + }, + 'LivechatVisitors': { + findOneById: sinon.stub(), + updateById: sinon.stub(), + updateOne: sinon.stub(), + getVisitorByToken: sinon.stub(), + findOneGuestByEmailAddress: sinon.stub(), + }, + 'LivechatCustomField': { + findByScope: sinon.stub(), + }, + '@global': true, +}; + +const { registerContact } = proxyquire.noCallThru().load('./registerContact', { + 'meteor/meteor': sinon.stub(), + '@rocket.chat/models': modelsMock, + '@rocket.chat/tools': { wrapExceptions: sinon.stub() }, + './Helper': { validateEmail: sinon.stub() }, + './LivechatTyped': { + Livechat: { + logger: { + debug: sinon.stub(), + }, + }, + }, +}); + +describe('registerContact', () => { + beforeEach(() => { + modelsMock.Users.findOneByUsername.reset(); + modelsMock.LivechatVisitors.getVisitorByToken.reset(); + modelsMock.LivechatVisitors.updateOne.reset(); + modelsMock.LivechatVisitors.findOneGuestByEmailAddress.reset(); + modelsMock.LivechatCustomField.findByScope.reset(); + modelsMock.LivechatRooms.findByVisitorId.reset(); + }); + + it(`should throw an error if there's no token`, async () => { + modelsMock.Users.findOneByUsername.returns(undefined); + + await expect( + registerContact({ + email: 'test@test.com', + username: 'username', + name: 'Name', + contactManager: { + username: 'unknown', + }, + }), + ).to.eventually.be.rejectedWith('error-invalid-contact-data'); + }); + + it(`should throw an error if the token is not a string`, async () => { + modelsMock.Users.findOneByUsername.returns(undefined); + + await expect( + registerContact({ + token: 15, + email: 'test@test.com', + username: 'username', + name: 'Name', + contactManager: { + username: 'unknown', + }, + }), + ).to.eventually.be.rejectedWith('error-invalid-contact-data'); + }); + + it(`should throw an error if there's an invalid manager username`, async () => { + modelsMock.Users.findOneByUsername.returns(undefined); + + await expect( + registerContact({ + token: 'token', + email: 'test@test.com', + username: 'username', + name: 'Name', + contactManager: { + username: 'unknown', + }, + }), + ).to.eventually.be.rejectedWith('error-contact-manager-not-found'); + }); + + it(`should throw an error if the manager username does not belong to a livechat agent`, async () => { + modelsMock.Users.findOneByUsername.returns({ roles: ['user'] }); + + await expect( + registerContact({ + token: 'token', + email: 'test@test.com', + username: 'username', + name: 'Name', + contactManager: { + username: 'username', + }, + }), + ).to.eventually.be.rejectedWith('error-invalid-contact-manager'); + }); + + it('should register a contact when passing valid data', async () => { + modelsMock.LivechatVisitors.getVisitorByToken.returns({ _id: 'visitor1' }); + modelsMock.LivechatCustomField.findByScope.returns({ toArray: () => [] }); + modelsMock.LivechatRooms.findByVisitorId.returns({ toArray: () => [] }); + modelsMock.LivechatVisitors.updateOne.returns(undefined); + + await expect( + registerContact({ + token: 'token', + email: 'test@test.com', + username: 'username', + name: 'Name', + }), + ).to.eventually.be.equal('visitor1'); + }); +}); diff --git a/apps/meteor/app/livechat/server/lib/contacts/registerContact.ts b/apps/meteor/app/livechat/server/lib/contacts/registerContact.ts new file mode 100644 index 000000000000..cdc029801c3d --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/registerContact.ts @@ -0,0 +1,129 @@ +import { MeteorError } from '@rocket.chat/core-services'; +import type { ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { LivechatVisitors, Users, LivechatRooms, LivechatInquiry, Rooms, Subscriptions } from '@rocket.chat/models'; +import type { MatchKeysAndValues, OnlyFieldsOfType } from 'mongodb'; + +import { getAllowedCustomFields } from './getAllowedCustomFields'; +import { validateCustomFields } from './validateCustomFields'; +import { callbacks } from '../../../../../lib/callbacks'; +import { + notifyOnRoomChangedById, + notifyOnSubscriptionChangedByRoomId, + notifyOnLivechatInquiryChangedByRoom, +} from '../../../../lib/server/lib/notifyListener'; + +type RegisterContactProps = { + _id?: string; + token: string; + name: string; + username?: string; + email?: string; + phone?: string; + customFields?: Record; + contactManager?: { + username: string; + }; +}; + +export async function registerContact({ + token, + name, + email = '', + phone, + username, + customFields = {}, + contactManager, +}: RegisterContactProps): Promise { + if (!token || typeof token !== 'string') { + throw new MeteorError('error-invalid-contact-data', 'Invalid visitor token'); + } + + const visitorEmail = email.trim().toLowerCase(); + + if (contactManager?.username) { + // verify if the user exists with this username and has a livechat-agent role + const manager = await Users.findOneByUsername(contactManager.username, { projection: { roles: 1 } }); + if (!manager) { + throw new MeteorError('error-contact-manager-not-found', `No user found with username ${contactManager.username}`); + } + if (!manager.roles || !Array.isArray(manager.roles) || !manager.roles.includes('livechat-agent')) { + throw new MeteorError('error-invalid-contact-manager', 'The contact manager must have the role "livechat-agent"'); + } + } + + const existingUserByToken = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } }); + let visitorId = existingUserByToken?._id; + + if (!existingUserByToken) { + if (!username) { + username = await LivechatVisitors.getNextVisitorUsername(); + } + + const existingUserByEmail = await LivechatVisitors.findOneGuestByEmailAddress(visitorEmail); + visitorId = existingUserByEmail?._id; + + if (!existingUserByEmail) { + const userData = { + username, + ts: new Date(), + token, + }; + + visitorId = (await LivechatVisitors.insertOne(userData)).insertedId; + } + } + + const allowedCF = await getAllowedCustomFields(); + const livechatData: Record = validateCustomFields(allowedCF, customFields, { ignoreAdditionalFields: true }); + + const fieldsToRemove = { + // if field is explicitely set to empty string, remove + ...(phone === '' && { phone: 1 }), + ...(visitorEmail === '' && { visitorEmails: 1 }), + ...(!contactManager?.username && { contactManager: 1 }), + }; + + const updateUser: { $set: MatchKeysAndValues; $unset?: OnlyFieldsOfType } = { + $set: { + token, + name, + livechatData, + // if phone has some value, set + ...(phone && { phone: [{ phoneNumber: phone }] }), + ...(visitorEmail && { visitorEmails: [{ address: visitorEmail }] }), + ...(contactManager?.username && { contactManager: { username: contactManager.username } }), + }, + ...(Object.keys(fieldsToRemove).length && { $unset: fieldsToRemove }), + }; + + await LivechatVisitors.updateOne({ _id: visitorId }, updateUser); + + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + const rooms: IOmnichannelRoom[] = await LivechatRooms.findByVisitorId(visitorId, {}, extraQuery).toArray(); + + if (rooms?.length) { + for await (const room of rooms) { + const { _id: rid } = room; + + const responses = await Promise.all([ + Rooms.setFnameById(rid, name), + LivechatInquiry.setNameByRoomId(rid, name), + Subscriptions.updateDisplayNameByRoomId(rid, name), + ]); + + if (responses[0]?.modifiedCount) { + void notifyOnRoomChangedById(rid); + } + + if (responses[1]?.modifiedCount) { + void notifyOnLivechatInquiryChangedByRoom(rid, 'updated', { name }); + } + + if (responses[2]?.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } + } + } + + return visitorId as string; +} diff --git a/apps/meteor/app/livechat/server/lib/contacts/registerGuestData.ts b/apps/meteor/app/livechat/server/lib/contacts/registerGuestData.ts new file mode 100644 index 000000000000..471104aecae9 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/registerGuestData.ts @@ -0,0 +1,41 @@ +import type { AtLeast, ILivechatVisitor } from '@rocket.chat/core-typings'; +import { LivechatContacts } from '@rocket.chat/models'; +import { wrapExceptions } from '@rocket.chat/tools'; + +import { validateEmail } from '../Helper'; +import type { RegisterGuestType } from '../Visitors'; +import { ContactMerger, type FieldAndValue } from './ContactMerger'; + +export async function registerGuestData( + { name, phone, email, username }: Pick, + visitor: AtLeast, +): Promise { + const validatedEmail = + email && + wrapExceptions(() => { + const trimmedEmail = email.trim().toLowerCase(); + validateEmail(trimmedEmail); + return trimmedEmail; + }).suppress(); + + const fields = [ + { type: 'name', value: name }, + { type: 'phone', value: phone?.number }, + { type: 'email', value: validatedEmail }, + { type: 'username', value: username || visitor.username }, + ].filter((field) => Boolean(field.value)) as FieldAndValue[]; + + if (!fields.length) { + return; + } + + // If a visitor was updated who already had contacts, load up the contacts and update that information as well + const contacts = await LivechatContacts.findAllByVisitorId(visitor._id).toArray(); + for await (const contact of contacts) { + await ContactMerger.mergeFieldsIntoContact({ + fields, + contact, + conflictHandlingMode: contact.unknown ? 'overwrite' : 'conflict', + }); + } +} diff --git a/apps/meteor/app/livechat/server/lib/contacts/updateContact.spec.ts b/apps/meteor/app/livechat/server/lib/contacts/updateContact.spec.ts new file mode 100644 index 000000000000..b6e4bf929a6e --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/updateContact.spec.ts @@ -0,0 +1,52 @@ +import { expect } from 'chai'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const modelsMock = { + LivechatContacts: { + findOneById: sinon.stub(), + updateContact: sinon.stub(), + }, + LivechatRooms: { + updateContactDataByContactId: sinon.stub(), + }, +}; + +const { updateContact } = proxyquire.noCallThru().load('./updateContact', { + './getAllowedCustomFields': { + getAllowedCustomFields: sinon.stub(), + }, + './validateContactManager': { + validateContactManager: sinon.stub(), + }, + './validateCustomFields': { + validateCustomFields: sinon.stub(), + }, + + '@rocket.chat/models': modelsMock, +}); + +describe('updateContact', () => { + beforeEach(() => { + modelsMock.LivechatContacts.findOneById.reset(); + modelsMock.LivechatContacts.updateContact.reset(); + modelsMock.LivechatRooms.updateContactDataByContactId.reset(); + }); + + it('should throw an error if the contact does not exist', async () => { + modelsMock.LivechatContacts.findOneById.resolves(undefined); + await expect(updateContact('any_id')).to.be.rejectedWith('error-contact-not-found'); + expect(modelsMock.LivechatContacts.updateContact.getCall(0)).to.be.null; + }); + + it('should update the contact with correct params', async () => { + modelsMock.LivechatContacts.findOneById.resolves({ _id: 'contactId' }); + modelsMock.LivechatContacts.updateContact.resolves({ _id: 'contactId', name: 'John Doe' } as any); + + const updatedContact = await updateContact({ contactId: 'contactId', name: 'John Doe' }); + + expect(modelsMock.LivechatContacts.updateContact.getCall(0).args[0]).to.be.equal('contactId'); + expect(modelsMock.LivechatContacts.updateContact.getCall(0).args[1]).to.be.deep.contain({ name: 'John Doe' }); + expect(updatedContact).to.be.deep.equal({ _id: 'contactId', name: 'John Doe' }); + }); +}); diff --git a/apps/meteor/app/livechat/server/lib/contacts/updateContact.ts b/apps/meteor/app/livechat/server/lib/contacts/updateContact.ts new file mode 100644 index 000000000000..8ec929ae4b34 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/updateContact.ts @@ -0,0 +1,67 @@ +import type { ILivechatContact, ILivechatContactChannel } from '@rocket.chat/core-typings'; +import { LivechatContacts, LivechatInquiry, LivechatRooms, Subscriptions } from '@rocket.chat/models'; + +import { getAllowedCustomFields } from './getAllowedCustomFields'; +import { validateContactManager } from './validateContactManager'; +import { validateCustomFields } from './validateCustomFields'; +import { + notifyOnSubscriptionChangedByVisitorIds, + notifyOnRoomChangedByContactId, + notifyOnLivechatInquiryChangedByVisitorIds, +} from '../../../../lib/server/lib/notifyListener'; + +export type UpdateContactParams = { + contactId: string; + name?: string; + emails?: string[]; + phones?: string[]; + customFields?: Record; + contactManager?: string; + channels?: ILivechatContactChannel[]; + wipeConflicts?: boolean; +}; + +export async function updateContact(params: UpdateContactParams): Promise { + const { contactId, name, emails, phones, customFields: receivedCustomFields, contactManager, channels, wipeConflicts } = params; + + const contact = await LivechatContacts.findOneById>(contactId, { + projection: { _id: 1, name: 1 }, + }); + + if (!contact) { + throw new Error('error-contact-not-found'); + } + + if (contactManager) { + await validateContactManager(contactManager); + } + + const customFields = receivedCustomFields && validateCustomFields(await getAllowedCustomFields(), receivedCustomFields); + + const updatedContact = await LivechatContacts.updateContact(contactId, { + name, + emails: emails?.map((address) => ({ address })), + phones: phones?.map((phoneNumber) => ({ phoneNumber })), + contactManager, + channels, + customFields, + ...(wipeConflicts && { conflictingFields: [] }), + }); + + // If the contact name changed, update the name of its existing rooms and subscriptions + if (name !== undefined && name !== contact.name) { + await LivechatRooms.updateContactDataByContactId(contactId, { name }); + void notifyOnRoomChangedByContactId(contactId); + + const visitorIds = updatedContact.channels?.map((channel) => channel.visitor.visitorId); + if (visitorIds?.length) { + await Subscriptions.updateNameAndFnameByVisitorIds(visitorIds, name); + void notifyOnSubscriptionChangedByVisitorIds(visitorIds); + + await LivechatInquiry.updateNameByVisitorIds(visitorIds, name); + void notifyOnLivechatInquiryChangedByVisitorIds(visitorIds, 'updated', { name }); + } + } + + return updatedContact; +} diff --git a/apps/meteor/app/livechat/server/lib/contacts/validateContactManager.spec.ts b/apps/meteor/app/livechat/server/lib/contacts/validateContactManager.spec.ts new file mode 100644 index 000000000000..b3268ff8660d --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/validateContactManager.spec.ts @@ -0,0 +1,32 @@ +import { expect } from 'chai'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const modelsMock = { + Users: { + findOneAgentById: sinon.stub(), + }, +}; + +const { validateContactManager } = proxyquire.noCallThru().load('./validateContactManager', { + '@rocket.chat/models': modelsMock, +}); + +describe('validateContactManager', () => { + beforeEach(() => { + modelsMock.Users.findOneAgentById.reset(); + }); + + it('should throw an error if the user does not exist', async () => { + modelsMock.Users.findOneAgentById.resolves(undefined); + await expect(validateContactManager('any_id')).to.be.rejectedWith('error-contact-manager-not-found'); + }); + + it('should not throw an error if the user has the "livechat-agent" role', async () => { + const user = { _id: 'userId' }; + modelsMock.Users.findOneAgentById.resolves(user); + + await expect(validateContactManager('userId')).to.not.be.rejected; + expect(modelsMock.Users.findOneAgentById.getCall(0).firstArg).to.be.equal('userId'); + }); +}); diff --git a/apps/meteor/app/livechat/server/lib/contacts/validateContactManager.ts b/apps/meteor/app/livechat/server/lib/contacts/validateContactManager.ts new file mode 100644 index 000000000000..cea2c0fe0c37 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/validateContactManager.ts @@ -0,0 +1,9 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; + +export async function validateContactManager(contactManagerUserId: string) { + const contactManagerUser = await Users.findOneAgentById>(contactManagerUserId, { projection: { _id: 1 } }); + if (!contactManagerUser) { + throw new Error('error-contact-manager-not-found'); + } +} diff --git a/apps/meteor/app/livechat/server/lib/contacts/validateCustomFields.spec.ts b/apps/meteor/app/livechat/server/lib/contacts/validateCustomFields.spec.ts new file mode 100644 index 000000000000..39684a62fd91 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/validateCustomFields.spec.ts @@ -0,0 +1,62 @@ +import { expect } from 'chai'; +import proxyquire from 'proxyquire'; + +const { validateCustomFields } = proxyquire.noCallThru().load('./validateCustomFields', {}); + +describe('validateCustomFields', () => { + const mockCustomFields = [{ _id: 'cf1', label: 'Custom Field 1', regexp: '^[0-9]+$', required: true }]; + + it('should validate custom fields correctly', () => { + expect(() => validateCustomFields(mockCustomFields, { cf1: '123' })).to.not.throw(); + }); + + it('should throw an error if a required custom field is missing', () => { + expect(() => validateCustomFields(mockCustomFields, {})).to.throw(); + }); + + it('should not throw an error if a required custom field is missing, but the ignoreValidationErrors option is provided', () => { + expect(() => validateCustomFields(mockCustomFields, {}, { ignoreValidationErrors: true })) + .not.to.throw() + .and.to.equal({}); + }); + + it('should NOT throw an error when a non-required custom field is missing', () => { + const allowedCustomFields = [{ _id: 'field1', label: 'Field 1', required: false }]; + const customFields = {}; + + expect(() => validateCustomFields(allowedCustomFields, customFields)).not.to.throw(); + }); + + it('should throw an error if a custom field value does not match the regexp', () => { + expect(() => validateCustomFields(mockCustomFields, { cf1: 'invalid' })).to.throw(); + }); + + it('should not throw an error if a custom field value does not match the regexp, but the ignoreValidationErrors option is provided', () => { + expect(() => validateCustomFields(mockCustomFields, { cf1: 'invalid' }, { ignoreValidationErrors: true })) + .not.to.throw() + .and.to.equal({}); + }); + + it('should handle an empty customFields input without throwing an error', () => { + const allowedCustomFields = [{ _id: 'field1', label: 'Field 1', required: false }]; + const customFields = {}; + + expect(() => validateCustomFields(allowedCustomFields, customFields)).not.to.throw(); + }); + + it('should throw an error if a extra custom field is passed', () => { + const allowedCustomFields = [{ _id: 'field1', label: 'Field 1', required: false }]; + const customFields = { field2: 'value' }; + + expect(() => validateCustomFields(allowedCustomFields, customFields)).to.throw(); + }); + + it('should not throw an error if a extra custom field is passed, but the ignoreValidationErrors option is provided', () => { + const allowedCustomFields = [{ _id: 'field1', label: 'Field 1', required: false }]; + const customFields = { field2: 'value' }; + + expect(() => validateCustomFields(allowedCustomFields, customFields, { ignoreValidationErrors: true })) + .not.to.throw() + .and.to.equal({}); + }); +}); diff --git a/apps/meteor/app/livechat/server/lib/contacts/validateCustomFields.ts b/apps/meteor/app/livechat/server/lib/contacts/validateCustomFields.ts new file mode 100644 index 000000000000..4efede65b266 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/validateCustomFields.ts @@ -0,0 +1,56 @@ +import type { AtLeast, ILivechatCustomField } from '@rocket.chat/core-typings'; + +import { trim } from '../../../../../lib/utils/stringUtils'; +import { i18n } from '../../../../utils/lib/i18n'; + +export function validateCustomFields( + allowedCustomFields: AtLeast[], + customFields: Record, + { + ignoreAdditionalFields = false, + ignoreValidationErrors = false, + }: { ignoreAdditionalFields?: boolean; ignoreValidationErrors?: boolean } = {}, +): Record { + const validValues: Record = {}; + + for (const cf of allowedCustomFields) { + if (!customFields.hasOwnProperty(cf._id)) { + if (cf.required && !ignoreValidationErrors) { + throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label })); + } + continue; + } + const cfValue: string = trim(customFields[cf._id]); + + if (!cfValue || typeof cfValue !== 'string') { + if (cf.required && !ignoreValidationErrors) { + throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label })); + } + continue; + } + + if (cf.regexp) { + const regex = new RegExp(cf.regexp); + if (!regex.test(cfValue)) { + if (ignoreValidationErrors) { + continue; + } + + throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label })); + } + } + + validValues[cf._id] = cfValue; + } + + if (!ignoreAdditionalFields) { + const allowedCustomFieldIds = new Set(allowedCustomFields.map((cf) => cf._id)); + for (const key in customFields) { + if (!allowedCustomFieldIds.has(key)) { + throw new Error(i18n.t('error-custom-field-not-allowed', { key })); + } + } + } + + return validValues; +} diff --git a/apps/meteor/app/livechat/server/lib/contacts/verifyContactChannel.ts b/apps/meteor/app/livechat/server/lib/contacts/verifyContactChannel.ts new file mode 100644 index 000000000000..77bc1e4653d2 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/contacts/verifyContactChannel.ts @@ -0,0 +1,12 @@ +import type { ILivechatContact } from '@rocket.chat/core-typings'; +import { makeFunction } from '@rocket.chat/patch-injection'; + +export type VerifyContactChannelParams = { + contactId: string; + field: string; + value: string; + visitorId: string; + roomId: string; +}; + +export const verifyContactChannel = makeFunction(async (_params: VerifyContactChannelParams): Promise => null); diff --git a/apps/meteor/app/livechat/server/lib/departmentsLib.ts b/apps/meteor/app/livechat/server/lib/departmentsLib.ts new file mode 100644 index 000000000000..76c70cba49ba --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/departmentsLib.ts @@ -0,0 +1,304 @@ +import type { LivechatDepartmentDTO, ILivechatDepartment, ILivechatDepartmentAgents } from '@rocket.chat/core-typings'; +import { LivechatDepartment, LivechatDepartmentAgents, LivechatVisitors, LivechatRooms } from '@rocket.chat/models'; +import { Meteor } from 'meteor/meteor'; + +import { updateDepartmentAgents } from './Helper'; +import { isDepartmentCreationAvailable } from './isDepartmentCreationAvailable'; +import { livechatLogger } from './logger'; +import { callbacks } from '../../../../lib/callbacks'; +import { + notifyOnLivechatDepartmentAgentChangedByDepartmentId, + notifyOnLivechatDepartmentAgentChanged, +} from '../../../lib/server/lib/notifyListener'; +/** + * @param {string|null} _id - The department id + * @param {Partial} departmentData + * @param {{upsert?: { agentId: string; count?: number; order?: number; }[], remove?: { agentId: string; count?: number; order?: number; }}} [departmentAgents] - The department agents + * @param {{_id?: string}} [departmentUnit] - The department's unit id + */ +export async function saveDepartment( + userId: string, + _id: string | null, + departmentData: LivechatDepartmentDTO, + departmentAgents?: { + upsert?: { agentId: string; count?: number; order?: number }[]; + remove?: { agentId: string; count?: number; order?: number }; + }, + departmentUnit?: { _id?: string }, +) { + check(_id, Match.Maybe(String)); + if (departmentUnit?._id !== undefined && typeof departmentUnit._id !== 'string') { + throw new Meteor.Error('error-invalid-department-unit', 'Invalid department unit id provided', { + method: 'livechat:saveDepartment', + }); + } + + const department = _id + ? await LivechatDepartment.findOneById(_id, { projection: { _id: 1, archived: 1, enabled: 1, parentId: 1 } }) + : null; + + if (departmentUnit && !departmentUnit._id && department && department.parentId) { + const isLastDepartmentInUnit = (await LivechatDepartment.countDepartmentsInUnit(department.parentId)) === 1; + if (isLastDepartmentInUnit) { + throw new Meteor.Error('error-unit-cant-be-empty', "The last department in a unit can't be removed", { + method: 'livechat:saveDepartment', + }); + } + } + + if (!department && !(await isDepartmentCreationAvailable())) { + throw new Meteor.Error('error-max-departments-number-reached', 'Maximum number of departments reached', { + method: 'livechat:saveDepartment', + }); + } + + if (department?.archived && departmentData.enabled) { + throw new Meteor.Error('error-archived-department-cant-be-enabled', 'Archived departments cant be enabled', { + method: 'livechat:saveDepartment', + }); + } + + const defaultValidations: Record | BooleanConstructor | StringConstructor> = { + enabled: Boolean, + name: String, + description: Match.Optional(String), + showOnRegistration: Boolean, + email: String, + showOnOfflineForm: Boolean, + requestTagBeforeClosingChat: Match.Optional(Boolean), + chatClosingTags: Match.Optional([String]), + fallbackForwardDepartment: Match.Optional(String), + departmentsAllowedToForward: Match.Optional([String]), + allowReceiveForwardOffline: Match.Optional(Boolean), + }; + + // The Livechat Form department support addition/custom fields, so those fields need to be added before validating + Object.keys(departmentData).forEach((field) => { + if (!defaultValidations.hasOwnProperty(field)) { + defaultValidations[field] = Match.OneOf(String, Match.Integer, Boolean); + } + }); + + check(departmentData, defaultValidations); + check( + departmentAgents, + Match.Maybe({ + upsert: Match.Maybe(Array), + remove: Match.Maybe(Array), + }), + ); + + const { requestTagBeforeClosingChat, chatClosingTags, fallbackForwardDepartment } = departmentData; + if (requestTagBeforeClosingChat && (!chatClosingTags || chatClosingTags.length === 0)) { + throw new Meteor.Error( + 'error-validating-department-chat-closing-tags', + 'At least one closing tag is required when the department requires tag(s) on closing conversations.', + { method: 'livechat:saveDepartment' }, + ); + } + + if (_id && !department) { + throw new Meteor.Error('error-department-not-found', 'Department not found', { + method: 'livechat:saveDepartment', + }); + } + + if (fallbackForwardDepartment === _id) { + throw new Meteor.Error( + 'error-fallback-department-circular', + 'Cannot save department. Circular reference between fallback department and department', + ); + } + + if (fallbackForwardDepartment) { + const fallbackDep = await LivechatDepartment.findOneById(fallbackForwardDepartment, { + projection: { _id: 1, fallbackForwardDepartment: 1 }, + }); + if (!fallbackDep) { + throw new Meteor.Error('error-fallback-department-not-found', 'Fallback department not found', { + method: 'livechat:saveDepartment', + }); + } + } + + const departmentDB = await LivechatDepartment.createOrUpdateDepartment(_id, departmentData); + if (departmentDB && departmentAgents) { + await updateDepartmentAgents(departmentDB._id, departmentAgents, departmentDB.enabled); + } + + // Disable event + if (department?.enabled && !departmentDB?.enabled) { + await callbacks.run('livechat.afterDepartmentDisabled', departmentDB); + } + + if (departmentUnit) { + await callbacks.run('livechat.manageDepartmentUnit', { userId, departmentId: departmentDB._id, unitId: departmentUnit._id }); + } + + return departmentDB; +} + +export async function archiveDepartment(_id: string) { + const department = await LivechatDepartment.findOneById>(_id, { + projection: { _id: 1, businessHourId: 1 }, + }); + + if (!department) { + throw new Error('department-not-found'); + } + + await Promise.all([LivechatDepartmentAgents.disableAgentsByDepartmentId(_id), LivechatDepartment.archiveDepartment(_id)]); + + void notifyOnLivechatDepartmentAgentChangedByDepartmentId(_id); + + await callbacks.run('livechat.afterDepartmentArchived', department); +} + +export async function unarchiveDepartment(_id: string) { + const department = await LivechatDepartment.findOneById(_id, { projection: { _id: 1 } }); + + if (!department) { + throw new Meteor.Error('department-not-found'); + } + + // TODO: these kind of actions should be on events instead of here + await Promise.all([LivechatDepartmentAgents.enableAgentsByDepartmentId(_id), LivechatDepartment.unarchiveDepartment(_id)]); + + void notifyOnLivechatDepartmentAgentChangedByDepartmentId(_id); + + return true; +} + +export async function saveDepartmentAgents( + _id: string, + departmentAgents: { + upsert?: Pick[]; + remove?: Pick[]; + }, +) { + check(_id, String); + check(departmentAgents, { + upsert: Match.Maybe([ + Match.ObjectIncluding({ + agentId: String, + username: String, + count: Match.Maybe(Match.Integer), + order: Match.Maybe(Match.Integer), + }), + ]), + remove: Match.Maybe([ + Match.ObjectIncluding({ + agentId: String, + username: Match.Maybe(String), + count: Match.Maybe(Match.Integer), + order: Match.Maybe(Match.Integer), + }), + ]), + }); + + const department = await LivechatDepartment.findOneById>(_id, { projection: { enabled: 1 } }); + if (!department) { + throw new Meteor.Error('error-department-not-found', 'Department not found'); + } + + return updateDepartmentAgents(_id, departmentAgents, department.enabled); +} + +export async function setDepartmentForGuest({ token, department }: { token: string; department: string }) { + check(token, String); + check(department, String); + + livechatLogger.debug(`Switching departments for user with token ${token} (to ${department})`); + + const updateUser = { + $set: { + department, + }, + }; + + const dep = await LivechatDepartment.findOneById>(department, { projection: { _id: 1 } }); + if (!dep) { + throw new Meteor.Error('invalid-department', 'Provided department does not exists'); + } + + const visitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } }); + if (!visitor) { + throw new Meteor.Error('invalid-token', 'Provided token is invalid'); + } + await LivechatVisitors.updateById(visitor._id, updateUser); +} + +export async function removeDepartment(departmentId: string) { + livechatLogger.debug(`Removing department: ${departmentId}`); + + const department = await LivechatDepartment.findOneById>(departmentId, { + projection: { _id: 1, businessHourId: 1 }, + }); + if (!department) { + throw new Error('error-department-not-found'); + } + + const { _id } = department; + + const ret = await LivechatDepartment.removeById(_id); + if (ret.acknowledged !== true) { + throw new Error('error-failed-to-delete-department'); + } + + const removedAgents = await LivechatDepartmentAgents.findByDepartmentId(department._id, { projection: { agentId: 1 } }).toArray(); + + livechatLogger.debug( + `Performing post-department-removal actions: ${_id}. Removing department agents, unsetting fallback department and removing department from rooms`, + ); + + const removeByDept = LivechatDepartmentAgents.removeByDepartmentId(_id); + + const promiseResponses = await Promise.allSettled([ + removeByDept, + LivechatDepartment.unsetFallbackDepartmentByDepartmentId(_id), + LivechatRooms.bulkRemoveDepartmentAndUnitsFromRooms(_id), + ]); + + promiseResponses.forEach((response, index) => { + if (response.status === 'rejected') { + livechatLogger.error(`Error while performing post-department-removal actions: ${_id}. Action No: ${index}. Error:`, response.reason); + } + }); + + const { deletedCount } = await removeByDept; + + if (deletedCount > 0) { + removedAgents.forEach(({ _id: docId, agentId }) => { + void notifyOnLivechatDepartmentAgentChanged( + { + _id: docId, + agentId, + departmentId: _id, + }, + 'removed', + ); + }); + } + + await callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds: removedAgents.map(({ agentId }) => agentId) }); + + return ret; +} + +export async function getRequiredDepartment(onlineRequired = true) { + const departments = LivechatDepartment.findEnabledWithAgents(); + + for await (const dept of departments) { + if (!dept.showOnRegistration) { + continue; + } + if (!onlineRequired) { + return dept; + } + + const onlineAgents = await LivechatDepartmentAgents.countOnlineForDepartment(dept._id); + if (onlineAgents) { + return dept; + } + } +} diff --git a/apps/meteor/app/livechat/server/lib/getOnlineAgents.ts b/apps/meteor/app/livechat/server/lib/getOnlineAgents.ts new file mode 100644 index 000000000000..be92a7cfcc54 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/getOnlineAgents.ts @@ -0,0 +1,24 @@ +import type { ILivechatAgent, SelectedAgent } from '@rocket.chat/core-typings'; +import { Users, LivechatDepartmentAgents } from '@rocket.chat/models'; +import type { FindCursor } from 'mongodb'; + +export async function getOnlineAgents(department?: string, agent?: SelectedAgent | null): Promise | undefined> { + if (agent?.agentId) { + return Users.findOnlineAgents(agent.agentId); + } + + if (department) { + const departmentAgents = await LivechatDepartmentAgents.getOnlineForDepartment(department); + if (!departmentAgents) { + return; + } + + const agentIds = await departmentAgents.map(({ agentId }) => agentId).toArray(); + if (!agentIds.length) { + return; + } + + return Users.findByIds([...new Set(agentIds)]); + } + return Users.findOnlineAgents(); +} diff --git a/apps/meteor/app/livechat/server/lib/getRoomMessages.ts b/apps/meteor/app/livechat/server/lib/getRoomMessages.ts new file mode 100644 index 000000000000..8a9af72b7e23 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/getRoomMessages.ts @@ -0,0 +1,23 @@ +import type { MessageTypesValues, IRoom } from '@rocket.chat/core-typings'; +import { Rooms, Messages } from '@rocket.chat/models'; +import { Meteor } from 'meteor/meteor'; + +export async function getRoomMessages({ rid }: { rid: string }) { + const room = await Rooms.findOneById>(rid, { projection: { t: 1 } }); + if (room?.t !== 'l') { + throw new Meteor.Error('invalid-room'); + } + + const ignoredMessageTypes: MessageTypesValues[] = [ + 'livechat_navigation_history', + 'livechat_transcript_history', + 'command', + 'livechat-close', + 'livechat-started', + 'livechat_video_call', + ]; + + return Messages.findVisibleByRoomIdNotContainingTypes(rid, ignoredMessageTypes, { + sort: { ts: 1 }, + }); +} diff --git a/apps/meteor/app/livechat/server/lib/localTypes.ts b/apps/meteor/app/livechat/server/lib/localTypes.ts index c6acbbc5bcbd..d58cad8f50bc 100644 --- a/apps/meteor/app/livechat/server/lib/localTypes.ts +++ b/apps/meteor/app/livechat/server/lib/localTypes.ts @@ -1,4 +1,4 @@ -import type { IOmnichannelRoom, IUser, ILivechatVisitor } from '@rocket.chat/core-typings'; +import type { IOmnichannelRoom, IUser, ILivechatVisitor, IMessage, MessageAttachment, IMessageInbox } from '@rocket.chat/core-typings'; export type GenericCloseRoomParams = { room: IOmnichannelRoom; @@ -29,3 +29,27 @@ export type CloseRoomParamsByVisitor = { } & GenericCloseRoomParams; export type CloseRoomParams = CloseRoomParamsByUser | CloseRoomParamsByVisitor; + +type UploadedFile = { + _id: string; + name?: string; + type?: string; + size?: number; + description?: string; + identify?: { size: { width: number; height: number } }; + format?: string; +}; + +export interface ILivechatMessage { + token: string; + _id: string; + rid: string; + msg: string; + file?: UploadedFile; + files?: UploadedFile[]; + attachments?: MessageAttachment[]; + alias?: string; + groupable?: boolean; + blocks?: IMessage['blocks']; + email?: IMessageInbox['email']; +} diff --git a/apps/meteor/app/livechat/server/lib/logger.ts b/apps/meteor/app/livechat/server/lib/logger.ts index bb452cd4db04..a468818cfd10 100644 --- a/apps/meteor/app/livechat/server/lib/logger.ts +++ b/apps/meteor/app/livechat/server/lib/logger.ts @@ -2,3 +2,5 @@ import { Logger } from '@rocket.chat/logger'; export const callbackLogger = new Logger('[Omnichannel] Callback'); export const businessHourLogger = new Logger('Business Hour'); +export const livechatLogger = new Logger('Livechat'); +export const livechatContactsLogger = new Logger('Livechat Contacts'); diff --git a/apps/meteor/app/livechat/server/lib/messages.ts b/apps/meteor/app/livechat/server/lib/messages.ts new file mode 100644 index 000000000000..0f5c460e3c28 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/messages.ts @@ -0,0 +1,91 @@ +import dns from 'dns'; +import * as util from 'util'; + +import { LivechatDepartment } from '@rocket.chat/models'; + +import { callbacks } from '../../../../lib/callbacks'; +import * as Mailer from '../../../mailer/server/api'; +import { settings } from '../../../settings/server'; + +const dnsResolveMx = util.promisify(dns.resolveMx); + +type OfflineMessageData = { + message: string; + name: string; + email: string; + department?: string; + host?: string; +}; + +export async function sendOfflineMessage(data: OfflineMessageData) { + if (!settings.get('Livechat_display_offline_form')) { + throw new Error('error-offline-form-disabled'); + } + + const { message, name, email, department, host } = data; + + if (!email) { + throw new Error('error-invalid-email'); + } + + const emailMessage = `${message}`.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1
$2'); + + let html = '

New livechat message

'; + if (host && host !== '') { + html = html.concat(`

Sent from: ${host}

`); + } + html = html.concat(` +

Visitor name: ${name}

+

Visitor email: ${email}

+

Message:
${emailMessage}

`); + + const fromEmail = settings.get('From_Email').match(/\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}\b/i); + + let from: string; + if (fromEmail) { + from = fromEmail[0]; + } else { + from = settings.get('From_Email'); + } + + if (settings.get('Livechat_validate_offline_email')) { + const emailDomain = email.substr(email.lastIndexOf('@') + 1); + + try { + await dnsResolveMx(emailDomain); + } catch (e) { + throw new Meteor.Error('error-invalid-email-address'); + } + } + + // TODO Block offline form if Livechat_offline_email is undefined + // (it does not make sense to have an offline form that does nothing) + // `this.sendEmail` will throw an error if the email is invalid + // thus this breaks livechat, since the "to" email is invalid, and that returns an [invalid email] error to the livechat client + let emailTo = settings.get('Livechat_offline_email'); + if (department && department !== '') { + const dep = await LivechatDepartment.findOneByIdOrName(department, { projection: { email: 1 } }); + if (dep) { + emailTo = dep.email || emailTo; + } + } + + const fromText = `${name} - ${email} <${from}>`; + const replyTo = `${name} <${email}>`; + const subject = `Livechat offline message from ${name}: ${`${emailMessage}`.substring(0, 20)}`; + await sendEmail(fromText, emailTo, replyTo, subject, html); + + setImmediate(() => { + void callbacks.run('livechat.offlineMessage', data); + }); +} + +async function sendEmail(from: string, to: string, replyTo: string, subject: string, html: string): Promise { + await Mailer.send({ + to, + from, + replyTo, + subject, + html, + }); +} diff --git a/apps/meteor/app/livechat/server/lib/parseTranscriptRequest.ts b/apps/meteor/app/livechat/server/lib/parseTranscriptRequest.ts index 76595a7ff640..1a9b27b50151 100644 --- a/apps/meteor/app/livechat/server/lib/parseTranscriptRequest.ts +++ b/apps/meteor/app/livechat/server/lib/parseTranscriptRequest.ts @@ -1,8 +1,8 @@ import type { ILivechatVisitor, IOmnichannelRoom, IUser } from '@rocket.chat/core-typings'; import { LivechatVisitors, Users } from '@rocket.chat/models'; -import { settings } from '../../../settings/server'; import type { CloseRoomParams } from './localTypes'; +import { settings } from '../../../settings/server'; export const parseTranscriptRequest = async ( room: IOmnichannelRoom, diff --git a/apps/meteor/app/livechat/server/lib/sendTranscript.ts b/apps/meteor/app/livechat/server/lib/sendTranscript.ts index bc7c06e0eaae..e701e8f5b863 100644 --- a/apps/meteor/app/livechat/server/lib/sendTranscript.ts +++ b/apps/meteor/app/livechat/server/lib/sendTranscript.ts @@ -108,7 +108,7 @@ export async function sendTranscript({ messageType.data ? { ...messageType.data(message), interpolation: { escapeValue: false } } : { interpolation: { escapeValue: false } }, - )}` + )}` : message.msg; let filesHTML = ''; diff --git a/apps/meteor/app/livechat/server/lib/tracking.ts b/apps/meteor/app/livechat/server/lib/tracking.ts new file mode 100644 index 000000000000..bfbcf9912212 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/tracking.ts @@ -0,0 +1,54 @@ +import { Message } from '@rocket.chat/core-services'; +import { Users } from '@rocket.chat/models'; + +import { livechatLogger } from './logger'; +import { settings } from '../../../settings/server'; + +type PageInfo = { title: string; location: { href: string }; change: string }; + +export async function savePageHistory(token: string, roomId: string | undefined, pageInfo: PageInfo) { + livechatLogger.debug({ + msg: `Saving page movement history for visitor with token ${token}`, + pageInfo, + roomId, + }); + + if (pageInfo.change !== settings.get('Livechat_history_monitor_type')) { + return; + } + const user = await Users.findOneById('rocket.cat'); + + if (!user) { + throw new Error('error-invalid-user'); + } + + const pageTitle = pageInfo.title; + const pageUrl = pageInfo.location.href; + const extraData: { + navigation: { + page: PageInfo; + token: string; + }; + expireAt?: number; + _hidden?: boolean; + } = { + navigation: { + page: pageInfo, + token, + }, + }; + + if (!roomId) { + livechatLogger.warn(`Saving page history without room id for visitor with token ${token}`); + // keep history of unregistered visitors for 1 month + const keepHistoryMiliseconds = 2592000000; + extraData.expireAt = new Date().getTime() + keepHistoryMiliseconds; + } + + if (!settings.get('Livechat_Visitor_navigation_as_a_message')) { + extraData._hidden = true; + } + + // @ts-expect-error: Investigating on which case we won't receive a roomId and where that history is supposed to be stored + return Message.saveSystemMessage('livechat_navigation_history', roomId, `${pageTitle} - ${pageUrl}`, user, extraData); +} diff --git a/apps/meteor/app/livechat/server/methods/saveDepartment.ts b/apps/meteor/app/livechat/server/methods/saveDepartment.ts index 659f85f49945..484cf275088d 100644 --- a/apps/meteor/app/livechat/server/methods/saveDepartment.ts +++ b/apps/meteor/app/livechat/server/methods/saveDepartment.ts @@ -3,7 +3,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { Livechat } from '../lib/LivechatTyped'; +import { saveDepartment } from '../lib/departmentsLib'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -44,6 +44,6 @@ Meteor.methods({ }); } - return Livechat.saveDepartment(uid, _id, departmentData, { upsert: departmentAgents }, departmentUnit); + return saveDepartment(uid, _id, departmentData, { upsert: departmentAgents }, departmentUnit); }, }); diff --git a/apps/meteor/app/livechat/server/methods/sendFileLivechatMessage.ts b/apps/meteor/app/livechat/server/methods/sendFileLivechatMessage.ts index 0268207b2f6c..ef12cf728c73 100644 --- a/apps/meteor/app/livechat/server/methods/sendFileLivechatMessage.ts +++ b/apps/meteor/app/livechat/server/methods/sendFileLivechatMessage.ts @@ -11,8 +11,8 @@ import { Random } from '@rocket.chat/random'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { FileUpload } from '../../../file-upload/server'; import { sendMessageLivechat } from './sendMessageLivechat'; +import { FileUpload } from '../../../file-upload/server'; interface ISendFileLivechatMessage { roomId: string; diff --git a/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts b/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts index 6fac80397906..742631dcea90 100644 --- a/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts +++ b/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts @@ -6,7 +6,7 @@ import { Meteor } from 'meteor/meteor'; import { settings } from '../../../settings/server'; import { Livechat } from '../lib/LivechatTyped'; -import type { ILivechatMessage } from '../lib/LivechatTyped'; +import type { ILivechatMessage } from '../lib/localTypes'; interface ILivechatMessageAgent { agentId: string; diff --git a/apps/meteor/app/livechat/server/methods/takeInquiry.ts b/apps/meteor/app/livechat/server/methods/takeInquiry.ts index 733cbd995208..19a08761c614 100644 --- a/apps/meteor/app/livechat/server/methods/takeInquiry.ts +++ b/apps/meteor/app/livechat/server/methods/takeInquiry.ts @@ -6,6 +6,8 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { settings } from '../../../settings/server'; import { RoutingManager } from '../lib/RoutingManager'; +import { isAgentAvailableToTakeContactInquiry } from '../lib/contacts/isAgentAvailableToTakeContactInquiry'; +import { migrateVisitorIfMissingContact } from '../lib/contacts/migrateVisitorIfMissingContact'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -51,7 +53,15 @@ export const takeInquiry = async ( const room = await LivechatRooms.findOneById(inquiry.rid); if (!room || !(await Omnichannel.isWithinMACLimit(room))) { - throw new Error('error-mac-limit-reached'); + throw new Meteor.Error('error-mac-limit-reached'); + } + + const contactId = room.contactId ?? (await migrateVisitorIfMissingContact(room.v._id, room.source)); + if (contactId) { + const isAgentAvailableToTakeContactInquiryResult = await isAgentAvailableToTakeContactInquiry(inquiry.v._id, room.source, contactId); + if (!isAgentAvailableToTakeContactInquiryResult.value) { + throw new Meteor.Error(isAgentAvailableToTakeContactInquiryResult.error); + } } const agent = { diff --git a/apps/meteor/app/livechat/server/roomAccessValidator.compatibility.ts b/apps/meteor/app/livechat/server/roomAccessValidator.compatibility.ts index d5ef83272550..d772b0cb7f21 100644 --- a/apps/meteor/app/livechat/server/roomAccessValidator.compatibility.ts +++ b/apps/meteor/app/livechat/server/roomAccessValidator.compatibility.ts @@ -1,9 +1,9 @@ import type { IUser, IOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatDepartmentAgents, LivechatInquiry, LivechatRooms, LivechatDepartment } from '@rocket.chat/models'; +import { RoutingManager } from './lib/RoutingManager'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; import { hasRoleAsync } from '../../authorization/server/functions/hasRole'; -import { RoutingManager } from './lib/RoutingManager'; type OmnichannelRoomAccessValidator = ( room: IOmnichannelRoom, diff --git a/apps/meteor/app/livechat/server/sendMessageBySMS.ts b/apps/meteor/app/livechat/server/sendMessageBySMS.ts index c7f88646158b..5454474f09d9 100644 --- a/apps/meteor/app/livechat/server/sendMessageBySMS.ts +++ b/apps/meteor/app/livechat/server/sendMessageBySMS.ts @@ -2,10 +2,10 @@ import { OmnichannelIntegration } from '@rocket.chat/core-services'; import { isEditedMessage } from '@rocket.chat/core-typings'; import { LivechatVisitors } from '@rocket.chat/models'; +import { callbackLogger } from './lib/logger'; import { callbacks } from '../../../lib/callbacks'; import { settings } from '../../settings/server'; import { normalizeMessageFileUpload } from '../../utils/server/functions/normalizeMessageFileUpload'; -import { callbackLogger } from './lib/logger'; callbacks.add( 'afterOmnichannelSaveMessage', @@ -55,7 +55,7 @@ callbacks.add( return message; } - const visitor = await LivechatVisitors.getVisitorByToken(room.v.token, { projection: { phone: 1 } }); + const visitor = await LivechatVisitors.getVisitorByToken(room.v.token, { projection: { phone: 1, source: 1 } }); if (!visitor?.phone || visitor.phone.length === 0) { return message; diff --git a/apps/meteor/app/livechat/server/startup.ts b/apps/meteor/app/livechat/server/startup.ts index 32cf01d2e640..b41fc425bf06 100644 --- a/apps/meteor/app/livechat/server/startup.ts +++ b/apps/meteor/app/livechat/server/startup.ts @@ -5,24 +5,24 @@ import { LivechatRooms } from '@rocket.chat/models'; import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; +import { businessHourManager } from './business-hour'; +import { createDefaultBusinessHourIfNotExists } from './business-hour/Helper'; +import { Livechat as LivechatTyped } from './lib/LivechatTyped'; +import { LivechatAgentActivityMonitor } from './statistics/LivechatAgentActivityMonitor'; import { callbacks } from '../../../lib/callbacks'; import { beforeLeaveRoomCallback } from '../../../lib/callbacks/beforeLeaveRoomCallback'; import { i18n } from '../../../server/lib/i18n'; import { roomCoordinator } from '../../../server/lib/rooms/roomCoordinator'; +import { maybeMigrateLivechatRoom } from '../../api/server/lib/maybeMigrateLivechatRoom'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; import { notifyOnUserChange } from '../../lib/server/lib/notifyListener'; import { settings } from '../../settings/server'; -import { businessHourManager } from './business-hour'; -import { createDefaultBusinessHourIfNotExists } from './business-hour/Helper'; -import { Livechat as LivechatTyped } from './lib/LivechatTyped'; -import { RoutingManager } from './lib/RoutingManager'; -import { LivechatAgentActivityMonitor } from './statistics/LivechatAgentActivityMonitor'; import './roomAccessValidator.internalService'; const logger = new Logger('LivechatStartup'); Meteor.startup(async () => { - roomCoordinator.setRoomFind('l', (_id) => LivechatRooms.findOneById(_id)); + roomCoordinator.setRoomFind('l', async (id) => maybeMigrateLivechatRoom(await LivechatRooms.findOneById(id))); beforeLeaveRoomCallback.add( (user, room) => { @@ -78,14 +78,10 @@ Meteor.startup(async () => { process.env.TEST_MODE === 'true' ? { debounce: 10, - } + } : undefined, ); - settings.watch('Livechat_Routing_Method', () => { - void RoutingManager.startQueue(); - }); - // Remove when accounts.onLogout is async Accounts.onLogout(({ user }: { user?: IUser }) => { if (!user?.roles?.includes('livechat-agent') || user?.roles?.includes('bot')) { diff --git a/apps/meteor/app/mailer/server/api.ts b/apps/meteor/app/mailer/server/api.ts index 7e0ad0380b11..a32461d16a92 100644 --- a/apps/meteor/app/mailer/server/api.ts +++ b/apps/meteor/app/mailer/server/api.ts @@ -8,12 +8,12 @@ import { Meteor } from 'meteor/meteor'; import stripHtml from 'string-strip-html'; import _ from 'underscore'; +import { replaceVariables } from './replaceVariables'; import { validateEmail } from '../../../lib/emailValidator'; import { strLeft, strRightBack } from '../../../lib/utils/stringUtils'; import { i18n } from '../../../server/lib/i18n'; import { notifyOnSettingChanged } from '../../lib/server/lib/notifyListener'; import { settings } from '../../settings/server'; -import { replaceVariables } from './replaceVariables'; let contentHeader: string | undefined; let contentFooter: string | undefined; @@ -44,7 +44,7 @@ export const replace = (str: string, data: { [key: string]: unknown } = {}): str ? { fname: strLeft(String(data.name), ' '), lname: strRightBack(String(data.name), ' '), - } + } : {}), ...data, }; diff --git a/apps/meteor/app/markdown/lib/parser/original/code.js b/apps/meteor/app/markdown/lib/parser/original/code.js index 61059d24a06d..2bb2ef603b96 100644 --- a/apps/meteor/app/markdown/lib/parser/original/code.js +++ b/apps/meteor/app/markdown/lib/parser/original/code.js @@ -4,8 +4,8 @@ */ import { unescapeHTML } from '@rocket.chat/string-helpers'; -import hljs, { register } from '../../hljs'; import { addAsToken } from './token'; +import hljs, { register } from '../../hljs'; const inlinecode = (message) => { // Support `text` diff --git a/apps/meteor/app/message-mark-as-unread/client/actionButton.ts b/apps/meteor/app/message-mark-as-unread/client/actionButton.ts index e1e35d216029..fc052eaf63d3 100644 --- a/apps/meteor/app/message-mark-as-unread/client/actionButton.ts +++ b/apps/meteor/app/message-mark-as-unread/client/actionButton.ts @@ -2,9 +2,8 @@ import { Meteor } from 'meteor/meteor'; import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; import { dispatchToastMessage } from '../../../client/lib/toast'; -import { messageArgs } from '../../../client/lib/utils/messageArgs'; import { router } from '../../../client/providers/RouterProvider'; -import { ChatSubscription } from '../../models/client'; +import { Subscriptions } from '../../models/client'; import { LegacyRoomManager, MessageAction } from '../../ui-utils/client'; import { sdk } from '../../utils/client/lib/SDKClient'; @@ -15,11 +14,9 @@ Meteor.startup(() => { label: 'Mark_unread', context: ['message', 'message-mobile', 'threads'], type: 'interaction', - async action(_, props) { - const { message = messageArgs(this).msg } = props; - + async action(_, { message }) { try { - const subscription = ChatSubscription.findOne({ + const subscription = Subscriptions.findOne({ rid: message.rid, }); diff --git a/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts b/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts index f213ae4b7243..69a4444e8394 100644 --- a/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts +++ b/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts @@ -3,8 +3,8 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Messages, Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../lib/server/lib/notifyListener'; import logger from './logger'; +import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/apps/meteor/app/message-pin/server/pinMessage.ts b/apps/meteor/app/message-pin/server/pinMessage.ts index 9f3dd44cc16d..f3622ff7bcad 100644 --- a/apps/meteor/app/message-pin/server/pinMessage.ts +++ b/apps/meteor/app/message-pin/server/pinMessage.ts @@ -43,108 +43,101 @@ declare module '@rocket.chat/ddp-client' { } } -Meteor.methods({ - async pinMessage(message, pinnedAt) { - check(message._id, String); - - const userId = Meteor.userId(); - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'pinMessage', - }); - } +export async function pinMessage(originalMessage: IMessage, userId: string, pinnedAt?: Date) { + if (!settings.get('Message_AllowPinning')) { + throw new Meteor.Error('error-action-not-allowed', 'Message pinning not allowed', { + method: 'pinMessage', + action: 'Message_pinning', + }); + } - if (!settings.get('Message_AllowPinning')) { - throw new Meteor.Error('error-action-not-allowed', 'Message pinning not allowed', { - method: 'pinMessage', - action: 'Message_pinning', - }); - } + if (!(await hasPermissionAsync(userId, 'pin-message', originalMessage.rid))) { + throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'pinMessage' }); + } - let originalMessage = await Messages.findOneById(message._id); - if (originalMessage == null || originalMessage._id == null) { - throw new Meteor.Error('error-invalid-message', 'Message you are pinning was not found', { - method: 'pinMessage', - action: 'Message_pinning', - }); - } + const room = await Rooms.findOneById(originalMessage.rid); + if (!room) { + throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'pinMessage' }); + } - const subscription = await Subscriptions.findOneByRoomIdAndUserId(originalMessage.rid, userId, { projection: { _id: 1 } }); - if (!subscription) { - // If it's a valid message but on a room that the user is not subscribed to, report that the message was not found. - throw new Meteor.Error('error-invalid-message', 'Message you are pinning was not found', { - method: 'pinMessage', - action: 'Message_pinning', - }); - } + if (!(await canAccessRoomAsync(room, { _id: userId }))) { + throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'pinMessage' }); + } - if (!(await hasPermissionAsync(userId, 'pin-message', originalMessage.rid))) { - throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'pinMessage' }); - } + if (originalMessage.pinned) { + return originalMessage; + } - const me = await Users.findOneById(userId); - if (!me) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'pinMessage' }); - } + const me = await Users.findOneById(userId); + if (!me) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'pinMessage' }); + } - // If we keep history of edits, insert a new message to store history information - if (settings.get('Message_KeepHistory') && isRegisterUser(me)) { - await Messages.cloneAndSaveAsHistoryById(message._id, me); - } + originalMessage.pinned = true; + originalMessage.pinnedAt = pinnedAt || new Date(); + originalMessage.pinnedBy = { + _id: userId, + username: me.username, + }; - const room = await Rooms.findOneById(originalMessage.rid); - if (!room) { - throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'pinMessage' }); - } + originalMessage = await Message.beforeSave({ message: originalMessage, room, user: me }); - if (!(await canAccessRoomAsync(room, { _id: userId }))) { - throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'pinMessage' }); - } + await Messages.setPinnedByIdAndUserId(originalMessage._id, originalMessage.pinnedBy, originalMessage.pinned); + if (settings.get('Message_Read_Receipt_Store_Users')) { + await ReadReceipts.setPinnedByMessageId(originalMessage._id, originalMessage.pinned); + } + if (isTheLastMessage(room, originalMessage)) { + await Rooms.setLastMessagePinned(room._id, originalMessage.pinnedBy, originalMessage.pinned); + } - originalMessage.pinned = true; - originalMessage.pinnedAt = pinnedAt || new Date(); - originalMessage.pinnedBy = { - _id: userId, - username: me.username, - }; + const attachments: MessageAttachment[] = []; - originalMessage = await Message.beforeSave({ message: originalMessage, room, user: me }); + if (Array.isArray(originalMessage.attachments)) { + originalMessage.attachments.forEach((attachment) => { + if (!isQuoteAttachment(attachment) || shouldAdd(attachments, attachment)) { + attachments.push(attachment); + } + }); + } - await Messages.setPinnedByIdAndUserId(originalMessage._id, originalMessage.pinnedBy, originalMessage.pinned); - if (settings.get('Message_Read_Receipt_Store_Users')) { - await ReadReceipts.setPinnedByMessageId(message._id, originalMessage.pinned); - } - if (isTheLastMessage(room, message)) { - await Rooms.setLastMessagePinned(room._id, originalMessage.pinnedBy, originalMessage.pinned); - void notifyOnRoomChangedById(room._id); - } + // App IPostMessagePinned event hook + await Apps.self?.triggerEvent(AppEvents.IPostMessagePinned, originalMessage, await Meteor.userAsync(), originalMessage.pinned); + + const pinMessageType = originalMessage.t === 'e2e' ? 'message_pinned_e2e' : 'message_pinned'; + + return Message.saveSystemMessage(pinMessageType, originalMessage.rid, '', me, { + attachments: [ + { + text: originalMessage.msg, + author_name: originalMessage.u.username, + author_icon: getUserAvatarURL(originalMessage.u.username), + ts: originalMessage.ts, + attachments: attachments.map(recursiveRemove), + }, + ], + }); +} - const attachments: MessageAttachment[] = []; +Meteor.methods({ + async pinMessage(message, pinnedAt) { + check(message._id, String); - if (Array.isArray(originalMessage.attachments)) { - originalMessage.attachments.forEach((attachment) => { - if (!isQuoteAttachment(attachment) || shouldAdd(attachments, attachment)) { - attachments.push(attachment); - } + const userId = Meteor.userId(); + if (!userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'pinMessage', }); } - // App IPostMessagePinned event hook - await Apps.self?.triggerEvent(AppEvents.IPostMessagePinned, originalMessage, await Meteor.userAsync(), originalMessage.pinned); + const originalMessage = await Messages.findOneById(message._id); + if (!originalMessage?.rid) { + throw new Meteor.Error('error-invalid-message', 'Message you are pinning was not found', { + method: 'pinMessage', + action: 'Message_pinning', + }); + } - const pinMessageType = originalMessage.t === 'e2e' ? 'message_pinned_e2e' : 'message_pinned'; - - return Message.saveSystemMessage(pinMessageType, originalMessage.rid, '', me, { - attachments: [ - { - text: originalMessage.msg, - author_name: originalMessage.u.username, - author_icon: getUserAvatarURL(originalMessage.u.username), - ts: originalMessage.ts, - attachments: attachments.map(recursiveRemove), - }, - ], - }); + return pinMessage(originalMessage, userId, pinnedAt); }, async unpinMessage(message) { check(message._id, String); diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts index 03642cab1eda..e3b12ca15aa7 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts @@ -7,6 +7,8 @@ import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers'; import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; +import { SAMLServiceProvider } from './ServiceProvider'; +import { SAMLUtils } from './Utils'; import { ensureArray } from '../../../../lib/utils/arrayUtils'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { addUserToRoom } from '../../../lib/server/functions/addUserToRoom'; @@ -18,8 +20,6 @@ import { i18n } from '../../../utils/lib/i18n'; import type { ISAMLAction } from '../definition/ISAMLAction'; import type { ISAMLUser } from '../definition/ISAMLUser'; import type { IServiceProviderOptions } from '../definition/IServiceProviderOptions'; -import { SAMLServiceProvider } from './ServiceProvider'; -import { SAMLUtils } from './Utils'; const showErrorMessage = function (res: ServerResponse, err: string): void { res.writeHead(200, { diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/ServiceProvider.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/ServiceProvider.ts index bf3d2151ce0d..0ffb5fe50fdf 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/ServiceProvider.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/ServiceProvider.ts @@ -5,11 +5,11 @@ import zlib from 'zlib'; import { Meteor } from 'meteor/meteor'; +import { SAMLUtils } from './Utils'; import type { ILogoutResponse } from '../definition/ILogoutResponse'; import type { ISAMLRequest } from '../definition/ISAMLRequest'; import type { IServiceProviderOptions } from '../definition/IServiceProviderOptions'; import type { ILogoutRequestValidateCallback, ILogoutResponseValidateCallback, IResponseValidateCallback } from '../definition/callbacks'; -import { SAMLUtils } from './Utils'; import { AuthorizeRequest } from './generators/AuthorizeRequest'; import { LogoutRequest } from './generators/LogoutRequest'; import { LogoutResponse } from './generators/LogoutResponse'; diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts index 984c7bc458a3..c2e8e8b00050 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts @@ -3,12 +3,12 @@ import zlib from 'zlib'; import type { Logger } from '@rocket.chat/logger'; +import { StatusCode } from './constants'; import { ensureArray } from '../../../../lib/utils/arrayUtils'; import type { IUserDataMap, IAttributeMapping } from '../definition/IAttributeMapping'; import type { ISAMLGlobalSettings } from '../definition/ISAMLGlobalSettings'; import type { ISAMLUser } from '../definition/ISAMLUser'; import type { IServiceProviderOptions } from '../definition/IServiceProviderOptions'; -import { StatusCode } from './constants'; let providerList: Array = []; let debug = false; diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts index 5c16716720b0..f2b15c9af16e 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts @@ -2,13 +2,6 @@ import type { SAMLConfiguration } from '@rocket.chat/core-typings'; import { LoginServiceConfiguration } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { SystemLogger } from '../../../../server/lib/logger/system'; -import { - notifyOnLoginServiceConfigurationChanged, - notifyOnLoginServiceConfigurationChangedByService, -} from '../../../lib/server/lib/notifyListener'; -import { settings, settingsRegistry } from '../../../settings/server'; -import type { IServiceProviderOptions } from '../definition/IServiceProviderOptions'; import { SAMLUtils } from './Utils'; import { defaultAuthnContextTemplate, @@ -21,6 +14,13 @@ import { defaultMetadataTemplate, defaultMetadataCertificateTemplate, } from './constants'; +import { SystemLogger } from '../../../../server/lib/logger/system'; +import { + notifyOnLoginServiceConfigurationChanged, + notifyOnLoginServiceConfigurationChangedByService, +} from '../../../lib/server/lib/notifyListener'; +import { settings, settingsRegistry } from '../../../settings/server'; +import type { IServiceProviderOptions } from '../definition/IServiceProviderOptions'; const getSamlConfigs = function (service: string): SAMLConfiguration { const configs: SAMLConfiguration = { diff --git a/apps/meteor/app/meteor-accounts-saml/server/listener.ts b/apps/meteor/app/meteor-accounts-saml/server/listener.ts index 8dd449597681..92a0c520ab65 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/listener.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/listener.ts @@ -6,10 +6,10 @@ import { Meteor } from 'meteor/meteor'; import { RoutePolicy } from 'meteor/routepolicy'; import { WebApp } from 'meteor/webapp'; -import { SystemLogger } from '../../../server/lib/logger/system'; import type { ISAMLAction } from './definition/ISAMLAction'; import { SAML } from './lib/SAML'; import { SAMLUtils } from './lib/Utils'; +import { SystemLogger } from '../../../server/lib/logger/system'; RoutePolicy.declare('/_saml/', 'network'); diff --git a/apps/meteor/app/meteor-accounts-saml/server/loginHandler.ts b/apps/meteor/app/meteor-accounts-saml/server/loginHandler.ts index 4adbfc7e0674..b95513fef036 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/loginHandler.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/loginHandler.ts @@ -1,10 +1,10 @@ import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; -import { i18n } from '../../../server/lib/i18n'; -import { SystemLogger } from '../../../server/lib/logger/system'; import { SAML } from './lib/SAML'; import { SAMLUtils } from './lib/Utils'; +import { i18n } from '../../../server/lib/i18n'; +import { SystemLogger } from '../../../server/lib/logger/system'; const makeError = (message: string): Record => ({ type: 'saml', diff --git a/apps/meteor/app/meteor-accounts-saml/server/startup.ts b/apps/meteor/app/meteor-accounts-saml/server/startup.ts index 556ab7df13e7..721edce08d46 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/startup.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/startup.ts @@ -2,9 +2,9 @@ import { Logger } from '@rocket.chat/logger'; import debounce from 'lodash.debounce'; import { Meteor } from 'meteor/meteor'; -import { settings } from '../../settings/server'; import { SAMLUtils } from './lib/Utils'; import { loadSamlServiceProviders, addSettings } from './lib/settings'; +import { settings } from '../../settings/server'; const logger = new Logger('steffo:meteor-accounts-saml'); SAMLUtils.setLoggerInstance(logger); diff --git a/apps/meteor/app/metrics/server/lib/collectMetrics.ts b/apps/meteor/app/metrics/server/lib/collectMetrics.ts index 8f2b256f6759..a1ad41c9a95c 100644 --- a/apps/meteor/app/metrics/server/lib/collectMetrics.ts +++ b/apps/meteor/app/metrics/server/lib/collectMetrics.ts @@ -10,12 +10,12 @@ import client from 'prom-client'; import gcStats from 'prometheus-gc-stats'; import _ from 'underscore'; +import { metrics } from './metrics'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { getControl } from '../../../../server/lib/migrations'; import { settings } from '../../../settings/server'; import { getAppsStatistics } from '../../../statistics/server/lib/getAppsStatistics'; import { Info } from '../../../utils/rocketchat.info'; -import { metrics } from './metrics'; const { mongo } = MongoInternals.defaultRemoteCollectionDriver(); diff --git a/apps/meteor/app/models/client/index.ts b/apps/meteor/app/models/client/index.ts index 397a1e45bb18..e71979106615 100644 --- a/apps/meteor/app/models/client/index.ts +++ b/apps/meteor/app/models/client/index.ts @@ -1,42 +1,28 @@ -import { Base } from './models/Base'; -import { CachedChannelList } from './models/CachedChannelList'; import { CachedChatRoom } from './models/CachedChatRoom'; import { CachedChatSubscription } from './models/CachedChatSubscription'; -import { CachedUserList } from './models/CachedUserList'; -import { ChatMessage } from './models/ChatMessage'; -import { AuthzCachedCollection, ChatPermissions } from './models/ChatPermissions'; -import { ChatRoom } from './models/ChatRoom'; -import { ChatSubscription } from './models/ChatSubscription'; -import CustomSounds from './models/CustomSounds'; -import EmojiCustom from './models/EmojiCustom'; +import { Messages } from './models/Messages'; +import { AuthzCachedCollection, Permissions } from './models/Permissions'; import { Roles } from './models/Roles'; import { RoomRoles } from './models/RoomRoles'; +import { Rooms } from './models/Rooms'; +import { Subscriptions } from './models/Subscriptions'; import { UserRoles } from './models/UserRoles'; import { Users } from './models/Users'; export { - Base, Roles, - CachedChannelList, CachedChatRoom, CachedChatSubscription, - CachedUserList, RoomRoles, UserRoles, AuthzCachedCollection, - ChatPermissions, - CustomSounds, - EmojiCustom, + Permissions, /** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ Users, /** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ - ChatRoom, + Rooms, /** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ - ChatSubscription, + Subscriptions, /** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ - ChatSubscription as Subscriptions, - /** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ - ChatMessage, - /** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ - ChatMessage as Messages, + Messages, }; diff --git a/apps/meteor/app/models/client/models/CachedChannelList.ts b/apps/meteor/app/models/client/models/CachedChannelList.ts deleted file mode 100644 index 2ec0979e6c5e..000000000000 --- a/apps/meteor/app/models/client/models/CachedChannelList.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { Mongo } from 'meteor/mongo'; - -export const CachedChannelList = new Mongo.Collection(null); diff --git a/apps/meteor/app/models/client/models/CachedChatRoom.ts b/apps/meteor/app/models/client/models/CachedChatRoom.ts index 248b68554583..784503a1b0af 100644 --- a/apps/meteor/app/models/client/models/CachedChatRoom.ts +++ b/apps/meteor/app/models/client/models/CachedChatRoom.ts @@ -1,8 +1,8 @@ import type { IOmnichannelRoom, IRoom, IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; import { DEFAULT_SLA_CONFIG, LivechatPriorityWeight } from '@rocket.chat/core-typings'; -import { CachedCollection } from '../../../ui-cached-collection/client/models/CachedCollection'; import { CachedChatSubscription } from './CachedChatSubscription'; +import { CachedCollection } from '../../../../client/lib/cachedCollections/CachedCollection'; class CachedChatRoom extends CachedCollection { constructor() { diff --git a/apps/meteor/app/models/client/models/CachedChatSubscription.ts b/apps/meteor/app/models/client/models/CachedChatSubscription.ts index d17493e57f2e..6ea6ea6917e9 100644 --- a/apps/meteor/app/models/client/models/CachedChatSubscription.ts +++ b/apps/meteor/app/models/client/models/CachedChatSubscription.ts @@ -2,8 +2,8 @@ import type { IOmnichannelRoom, IRoomWithRetentionPolicy, ISubscription } from ' import { DEFAULT_SLA_CONFIG, LivechatPriorityWeight } from '@rocket.chat/core-typings'; import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; -import { CachedCollection } from '../../../ui-cached-collection/client/models/CachedCollection'; import { CachedChatRoom } from './CachedChatRoom'; +import { CachedCollection } from '../../../../client/lib/cachedCollections/CachedCollection'; declare module '@rocket.chat/core-typings' { interface ISubscription { diff --git a/apps/meteor/app/models/client/models/CachedUserList.ts b/apps/meteor/app/models/client/models/CachedUserList.ts deleted file mode 100644 index 0233659b47d5..000000000000 --- a/apps/meteor/app/models/client/models/CachedUserList.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Mongo } from 'meteor/mongo'; - -export const CachedUserList = new Mongo.Collection(null); diff --git a/apps/meteor/app/models/client/models/ChatRoom.ts b/apps/meteor/app/models/client/models/ChatRoom.ts deleted file mode 100644 index f23f20e8048e..000000000000 --- a/apps/meteor/app/models/client/models/ChatRoom.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { IMessage, IRoom } from '@rocket.chat/core-typings'; - -import { CachedChatRoom } from './CachedChatRoom'; - -/** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ -export const ChatRoom = Object.assign(CachedChatRoom.collection, { - setReactionsInLastMessage(this: typeof CachedChatRoom.collection, roomId: IRoom['_id'], lastMessage: IMessage) { - return this.update({ _id: roomId }, { $set: { lastMessage } }); - }, - unsetReactionsInLastMessage(this: typeof CachedChatRoom.collection, roomId: IRoom['_id']) { - return this.update({ _id: roomId }, { $unset: { 'lastMessage.reactions': 1 as const } }); - }, -}); diff --git a/apps/meteor/app/models/client/models/CustomSounds.ts b/apps/meteor/app/models/client/models/CustomSounds.ts deleted file mode 100644 index ccbf8c4b50eb..000000000000 --- a/apps/meteor/app/models/client/models/CustomSounds.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { ICustomSound } from '@rocket.chat/core-typings'; - -import { Base } from './Base'; - -class CustomSounds extends Base { - constructor() { - super(); - this._initModel('custom_sounds'); - } -} - -export default new CustomSounds(); diff --git a/apps/meteor/app/models/client/models/EmojiCustom.ts b/apps/meteor/app/models/client/models/EmojiCustom.ts deleted file mode 100644 index 48bf83e659b3..000000000000 --- a/apps/meteor/app/models/client/models/EmojiCustom.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { ICustomEmojiDescriptor } from '@rocket.chat/core-typings'; -import type { Mongo } from 'meteor/mongo'; - -import { Base } from './Base'; - -class EmojiCustom extends Base { - constructor() { - super(); - this._initModel('custom_emoji'); - } - - // find - findByNameOrAlias(name: ICustomEmojiDescriptor['name'], options?: Mongo.Options) { - const query = { - $or: [{ name }, { aliases: name }], - }; - - return this.find(query, options); - } -} - -export default new EmojiCustom(); diff --git a/apps/meteor/app/models/client/models/ChatMessage.ts b/apps/meteor/app/models/client/models/Messages.ts similarity index 68% rename from apps/meteor/app/models/client/models/ChatMessage.ts rename to apps/meteor/app/models/client/models/Messages.ts index 1e9b066bdaaa..94a9ff65f2f0 100644 --- a/apps/meteor/app/models/client/models/ChatMessage.ts +++ b/apps/meteor/app/models/client/models/Messages.ts @@ -1,4 +1,4 @@ -import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import type { IMessage } from '@rocket.chat/core-typings'; import { Mongo } from 'meteor/mongo'; import type { MinimongoCollection } from '../../../../client/definitions/MinimongoCollection'; @@ -11,19 +11,10 @@ class ChatMessageCollection super(null); } - findOneByRoomIdAndMessageId(rid: IRoom['_id'], messageId: IMessage['_id'], options?: Mongo.Options) { - const query = { - rid, - _id: messageId, - }; - - return this.findOne(query, options); - } - public declare _collection: MinimongoCollection['_collection']; public declare queries: MinimongoCollection['queries']; } /** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ -export const ChatMessage = new ChatMessageCollection(); +export const Messages = new ChatMessageCollection(); diff --git a/apps/meteor/app/models/client/models/ChatPermissions.ts b/apps/meteor/app/models/client/models/Permissions.ts similarity index 58% rename from apps/meteor/app/models/client/models/ChatPermissions.ts rename to apps/meteor/app/models/client/models/Permissions.ts index e836f58ebb2e..5793ab3e897d 100644 --- a/apps/meteor/app/models/client/models/ChatPermissions.ts +++ b/apps/meteor/app/models/client/models/Permissions.ts @@ -1,10 +1,10 @@ import type { IPermission } from '@rocket.chat/core-typings'; -import { CachedCollection } from '../../../ui-cached-collection/client'; +import { CachedCollection } from '../../../../client/lib/cachedCollections'; export const AuthzCachedCollection = new CachedCollection({ name: 'permissions', eventType: 'notify-logged', }); -export const ChatPermissions = AuthzCachedCollection.collection; +export const Permissions = AuthzCachedCollection.collection; diff --git a/apps/meteor/app/models/client/models/Roles.ts b/apps/meteor/app/models/client/models/Roles.ts index a048905cd2b4..62dec04f3262 100644 --- a/apps/meteor/app/models/client/models/Roles.ts +++ b/apps/meteor/app/models/client/models/Roles.ts @@ -1,10 +1,10 @@ -import type { IRole, IRoom, IUser } from '@rocket.chat/core-typings'; +import type { IRole, IUser } from '@rocket.chat/core-typings'; import { Mongo } from 'meteor/mongo'; import { ReactiveVar } from 'meteor/reactive-var'; -import type { MinimongoCollection } from '../../../../client/definitions/MinimongoCollection'; -import { ChatSubscription } from './ChatSubscription'; +import { Subscriptions } from './Subscriptions'; import { Users } from './Users'; +import type { MinimongoCollection } from '../../../../client/definitions/MinimongoCollection'; class RolesCollection extends Mongo.Collection implements MinimongoCollection { ready = new ReactiveVar(false); @@ -13,22 +13,6 @@ class RolesCollection extends Mongo.Collection implements MinimongoCollec super(null); } - findUsersInRole(roleId: IRole['_id'], scope: IRoom['_id'], options: any) { - const role = this.findOne(roleId); - const roleScope = role?.scope || 'Users'; - - switch (roleScope) { - case 'Subscriptions': - return ChatSubscription.findUsersInRoles(roleId, scope, options); - - case 'Users': - return Users.findUsersInRoles(roleId, scope, options); - - default: - return undefined; - } - } - isUserInRoles(userId: IUser['_id'], roles: IRole['_id'][] | IRole['_id'], scope?: string, ignoreSubscriptions = false) { roles = Array.isArray(roles) ? roles : [roles]; return roles.some((roleId) => { @@ -37,7 +21,7 @@ class RolesCollection extends Mongo.Collection implements MinimongoCollec switch (roleScope) { case 'Subscriptions': - return ChatSubscription.isUserInRole(userId, roleId, scope); + return Subscriptions.isUserInRole(userId, roleId, scope); case 'Users': return Users.isUserInRole(userId, roleId); diff --git a/apps/meteor/app/models/client/models/Rooms.ts b/apps/meteor/app/models/client/models/Rooms.ts new file mode 100644 index 000000000000..91328881c432 --- /dev/null +++ b/apps/meteor/app/models/client/models/Rooms.ts @@ -0,0 +1,4 @@ +import { CachedChatRoom } from './CachedChatRoom'; + +/** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ +export const Rooms = CachedChatRoom.collection; diff --git a/apps/meteor/app/models/client/models/ChatSubscription.ts b/apps/meteor/app/models/client/models/Subscriptions.ts similarity index 95% rename from apps/meteor/app/models/client/models/ChatSubscription.ts rename to apps/meteor/app/models/client/models/Subscriptions.ts index aa7892fe5031..ab33f79b1050 100644 --- a/apps/meteor/app/models/client/models/ChatSubscription.ts +++ b/apps/meteor/app/models/client/models/Subscriptions.ts @@ -3,11 +3,11 @@ import mem from 'mem'; import { Meteor } from 'meteor/meteor'; import type { Filter } from 'mongodb'; -import { isTruthy } from '../../../../lib/isTruthy'; import { CachedChatSubscription } from './CachedChatSubscription'; +import { isTruthy } from '../../../../lib/isTruthy'; /** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ -export const ChatSubscription = Object.assign(CachedChatSubscription.collection, { +export const Subscriptions = Object.assign(CachedChatSubscription.collection, { isUserInRole: mem( function (this: typeof CachedChatSubscription.collection, _uid: IUser['_id'], roleId: IRole['_id'], rid?: IRoom['_id']) { if (!rid) { diff --git a/apps/meteor/app/oauth2-server-config/server/admin/functions/addOAuthApp.ts b/apps/meteor/app/oauth2-server-config/server/admin/functions/addOAuthApp.ts index 8e828f5c11d9..43bcd0ce14bd 100644 --- a/apps/meteor/app/oauth2-server-config/server/admin/functions/addOAuthApp.ts +++ b/apps/meteor/app/oauth2-server-config/server/admin/functions/addOAuthApp.ts @@ -4,8 +4,8 @@ import { Random } from '@rocket.chat/random'; import type { OauthAppsAddParams } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; -import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { parseUriList } from './parseUriList'; +import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; export async function addOAuthApp(applicationParams: OauthAppsAddParams, uid: IUser['_id'] | undefined): Promise { if (!uid) { diff --git a/apps/meteor/app/otr/client/OTR.ts b/apps/meteor/app/otr/client/OTR.ts index 9f3eea155384..6237b73e2545 100644 --- a/apps/meteor/app/otr/client/OTR.ts +++ b/apps/meteor/app/otr/client/OTR.ts @@ -1,7 +1,7 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; -import type { IOTR } from '../lib/IOTR'; import { OTRRoom } from './OTRRoom'; +import type { IOTR } from '../lib/IOTR'; class OTR implements IOTR { private instancesByRoomId: { [rid: string]: OTRRoom }; diff --git a/apps/meteor/app/otr/lib/IOTR.ts b/apps/meteor/app/otr/lib/IOTR.ts index 9e7cd4ca6b8e..c7aeb0a3777a 100644 --- a/apps/meteor/app/otr/lib/IOTR.ts +++ b/apps/meteor/app/otr/lib/IOTR.ts @@ -1,7 +1,7 @@ import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; -import type { OTRRoom } from '../client/OTRRoom'; import type { OtrRoomState } from './OtrRoomState'; +import type { OTRRoom } from '../client/OTRRoom'; export interface IOnUserStreamData { roomId: IRoom['_id']; diff --git a/apps/meteor/app/push/server/push.ts b/apps/meteor/app/push/server/push.ts index 760fbea7c232..77092051eee2 100644 --- a/apps/meteor/app/push/server/push.ts +++ b/apps/meteor/app/push/server/push.ts @@ -7,12 +7,12 @@ import { JWT } from 'google-auth-library'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { settings } from '../../settings/server'; import { initAPN, sendAPN } from './apn'; import type { PushOptions, PendingPushNotification } from './definition'; import { sendFCM } from './fcm'; import { sendGCM } from './gcm'; import { logger } from './logger'; +import { settings } from '../../settings/server'; export const _matchToken = Match.OneOf({ apn: String }, { gcm: String }); @@ -484,14 +484,14 @@ class PushClass { apn: { ...pick(options.apn, 'category'), }, - } + } : {}), ...(this.hasGcmOptions(options) ? { gcm: { ...pick(options.gcm, 'image', 'style'), }, - } + } : {}), }; diff --git a/apps/meteor/app/reactions/client/init.ts b/apps/meteor/app/reactions/client/init.ts index 1943d7262939..d9573fbfe833 100644 --- a/apps/meteor/app/reactions/client/init.ts +++ b/apps/meteor/app/reactions/client/init.ts @@ -1,7 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; -import { messageArgs } from '../../../client/lib/utils/messageArgs'; import { MessageAction } from '../../ui-utils/client'; import { sdk } from '../../utils/client/lib/SDKClient'; @@ -11,8 +10,7 @@ Meteor.startup(() => { icon: 'add-reaction', label: 'Add_Reaction', context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], - action(event, props) { - const { message = messageArgs(this).msg, chat } = props; + action(event, { message, chat }) { event?.stopPropagation(); chat?.emojiPicker.open(event?.currentTarget as Element, (emoji) => sdk.call('setReaction', `:${emoji}:`, message._id)); }, diff --git a/apps/meteor/app/reactions/client/methods/setReaction.ts b/apps/meteor/app/reactions/client/methods/setReaction.ts index 1744d49c0ceb..2e3a03b248c2 100644 --- a/apps/meteor/app/reactions/client/methods/setReaction.ts +++ b/apps/meteor/app/reactions/client/methods/setReaction.ts @@ -4,7 +4,7 @@ import { Meteor } from 'meteor/meteor'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; import { emoji } from '../../../emoji/client'; -import { Messages, ChatRoom, Subscriptions } from '../../../models/client'; +import { Messages, Rooms, Subscriptions } from '../../../models/client'; Meteor.methods({ async setReaction(reaction, messageId) { @@ -23,7 +23,7 @@ Meteor.methods({ return false; } - const room: IRoom | undefined = ChatRoom.findOne({ _id: message.rid }); + const room: IRoom | undefined = Rooms.findOne({ _id: message.rid }); if (!room) { return false; } diff --git a/apps/meteor/app/search/server/model/SearchProvider.ts b/apps/meteor/app/search/server/model/SearchProvider.ts index 89428e4a7dc6..88388262b95d 100644 --- a/apps/meteor/app/search/server/model/SearchProvider.ts +++ b/apps/meteor/app/search/server/model/SearchProvider.ts @@ -1,9 +1,9 @@ import type { IMessageSearchSuggestion, IRoom, IUser } from '@rocket.chat/core-typings'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { SearchLogger } from '../logger/logger'; import type { IRawSearchResult } from './ISearchResult'; import { Settings } from './Settings'; +import { SearchLogger } from '../logger/logger'; export abstract class SearchProvider { private _key: string; diff --git a/apps/meteor/app/search/server/search.internalService.ts b/apps/meteor/app/search/server/search.internalService.ts index 2fbaa2e1da31..bf4a0c484ddc 100644 --- a/apps/meteor/app/search/server/search.internalService.ts +++ b/apps/meteor/app/search/server/search.internalService.ts @@ -1,9 +1,9 @@ import { api, ServiceClassInternal } from '@rocket.chat/core-services'; import { Users } from '@rocket.chat/models'; -import { settings } from '../../settings/server'; import { searchEventService } from './events'; import { searchProviderService } from './service'; +import { settings } from '../../settings/server'; class Search extends ServiceClassInternal { protected name = 'search'; diff --git a/apps/meteor/app/settings/server/SettingsRegistry.ts b/apps/meteor/app/settings/server/SettingsRegistry.ts index d7d2fa0a79f8..ec9cf6b902f5 100644 --- a/apps/meteor/app/settings/server/SettingsRegistry.ts +++ b/apps/meteor/app/settings/server/SettingsRegistry.ts @@ -4,12 +4,12 @@ import { Emitter } from '@rocket.chat/emitter'; import type { ISettingsModel } from '@rocket.chat/model-typings'; import { isEqual } from 'underscore'; -import { SystemLogger } from '../../../server/lib/logger/system'; import type { ICachedSettings } from './CachedSettings'; import { getSettingDefaults } from './functions/getSettingDefaults'; import { overrideSetting } from './functions/overrideSetting'; import { overwriteSetting } from './functions/overwriteSetting'; import { validateSetting } from './functions/validateSetting'; +import { SystemLogger } from '../../../server/lib/logger/system'; const blockedSettings = new Set(); const hiddenSettings = new Set(); diff --git a/apps/meteor/app/settings/server/applyMiddlewares.ts b/apps/meteor/app/settings/server/applyMiddlewares.ts index 93f6efc8a9b1..d707757f0def 100644 --- a/apps/meteor/app/settings/server/applyMiddlewares.ts +++ b/apps/meteor/app/settings/server/applyMiddlewares.ts @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/rules-of-hooks */ import { use } from './Middleware'; import { settings } from './cached'; diff --git a/apps/meteor/app/settings/server/index.ts b/apps/meteor/app/settings/server/index.ts index c358a174d69c..30a8c9fd6ba2 100644 --- a/apps/meteor/app/settings/server/index.ts +++ b/apps/meteor/app/settings/server/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/rules-of-hooks */ import { Settings } from '@rocket.chat/models'; import { use } from './Middleware'; diff --git a/apps/meteor/app/slackbridge/client/slackbridge_import.client.js b/apps/meteor/app/slackbridge/client/slackbridge_import.client.ts similarity index 85% rename from apps/meteor/app/slackbridge/client/slackbridge_import.client.js rename to apps/meteor/app/slackbridge/client/slackbridge_import.client.ts index eebc07ddb72d..2138fc2a35f9 100644 --- a/apps/meteor/app/slackbridge/client/slackbridge_import.client.js +++ b/apps/meteor/app/slackbridge/client/slackbridge_import.client.ts @@ -1,7 +1,7 @@ import { settings } from '../../settings/client'; import { slashCommands } from '../../utils/client/slashCommand'; -settings.onload('SlackBridge_Enabled', (key, value) => { +settings.onload('SlackBridge_Enabled', (_key, value) => { if (value) { slashCommands.add({ command: 'slackbridge-import', diff --git a/apps/meteor/app/slackbridge/server/RocketAdapter.js b/apps/meteor/app/slackbridge/server/RocketAdapter.js index 8ba2a76dcbc2..245e28c72203 100644 --- a/apps/meteor/app/slackbridge/server/RocketAdapter.js +++ b/apps/meteor/app/slackbridge/server/RocketAdapter.js @@ -6,13 +6,13 @@ import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; +import { rocketLogger } from './logger'; import { callbacks } from '../../../lib/callbacks'; import { sleep } from '../../../lib/utils/sleep'; import { createRoom } from '../../lib/server/functions/createRoom'; import { sendMessage } from '../../lib/server/functions/sendMessage'; import { setUserAvatar } from '../../lib/server/functions/setUserAvatar'; import { settings } from '../../settings/server'; -import { rocketLogger } from './logger'; export default class RocketAdapter { constructor(slackBridge) { diff --git a/apps/meteor/app/slackbridge/server/SlackAdapter.js b/apps/meteor/app/slackbridge/server/SlackAdapter.js index 0263d5369a4c..46a5ab6d35b5 100644 --- a/apps/meteor/app/slackbridge/server/SlackAdapter.js +++ b/apps/meteor/app/slackbridge/server/SlackAdapter.js @@ -8,6 +8,8 @@ import { App as SlackApp } from '@slack/bolt'; import { RTMClient } from '@slack/rtm-api'; import { Meteor } from 'meteor/meteor'; +import { SlackAPI } from './SlackAPI'; +import { slackLogger } from './logger'; import { saveRoomName, saveRoomTopic } from '../../channel-settings/server'; import { FileUpload } from '../../file-upload/server'; import { addUserToRoom } from '../../lib/server/functions/addUserToRoom'; @@ -20,8 +22,6 @@ import { updateMessage } from '../../lib/server/functions/updateMessage'; import { executeSetReaction } from '../../reactions/server/setReaction'; import { settings } from '../../settings/server'; import { getUserAvatarURL } from '../../utils/server/getUserAvatarURL'; -import { SlackAPI } from './SlackAPI'; -import { slackLogger } from './logger'; export default class SlackAdapter { constructor(slackBridge) { @@ -987,7 +987,7 @@ export default class SlackAdapter { const rocketChannel = await this.rocket.getChannel(slackMessage); const rocketUser = slackMessage.previous_message.user ? (await this.rocket.findUser(slackMessage.previous_message.user)) || - (await this.rocket.addUser(slackMessage.previous_message.user)) + (await this.rocket.addUser(slackMessage.previous_message.user)) : null; const rocketMsgObj = { diff --git a/apps/meteor/app/slackbridge/server/slackbridge.js b/apps/meteor/app/slackbridge/server/slackbridge.js index b5983e7fff58..89ff66a13397 100644 --- a/apps/meteor/app/slackbridge/server/slackbridge.js +++ b/apps/meteor/app/slackbridge/server/slackbridge.js @@ -1,9 +1,9 @@ import { debounce } from 'lodash'; -import { settings } from '../../settings/server'; import RocketAdapter from './RocketAdapter.js'; import SlackAdapter from './SlackAdapter.js'; import { classLogger, connLogger } from './logger'; +import { settings } from '../../settings/server'; /** * SlackBridge interfaces between this Rocket installation and a remote Slack installation. diff --git a/apps/meteor/app/slackbridge/server/slackbridge_import.server.js b/apps/meteor/app/slackbridge/server/slackbridge_import.server.js index 2cac64433cb5..6e7117af976a 100644 --- a/apps/meteor/app/slackbridge/server/slackbridge_import.server.js +++ b/apps/meteor/app/slackbridge/server/slackbridge_import.server.js @@ -2,10 +2,10 @@ import { Rooms, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { Match } from 'meteor/check'; +import { SlackBridge } from './slackbridge'; import { i18n } from '../../../server/lib/i18n'; import { msgStream } from '../../lib/server'; import { slashCommands } from '../../utils/server/slashCommand'; -import { SlackBridge } from './slackbridge'; async function SlackBridgeImport({ command, params, message, userId }) { if (command !== 'slackbridge-import' || !Match.test(params, String)) { diff --git a/apps/meteor/app/slashcommands-hide/server/hide.ts b/apps/meteor/app/slashcommands-hide/server/hide.ts index bc614b95e8ed..636974297914 100644 --- a/apps/meteor/app/slashcommands-hide/server/hide.ts +++ b/apps/meteor/app/slashcommands-hide/server/hide.ts @@ -41,7 +41,7 @@ slashCommands.add({ : await Rooms.findOne({ t: 'd', usernames: { $all: [user.username, strippedRoom] }, - }); + }); if (!roomObject) { void api.broadcast('notify.ephemeralMessage', user._id, message.rid, { msg: i18n.t('Channel_doesnt_exist', { diff --git a/apps/meteor/app/slashcommands-inviteall/server/server.ts b/apps/meteor/app/slashcommands-inviteall/server/server.ts index e74bb89899c2..436d283d4b5a 100644 --- a/apps/meteor/app/slashcommands-inviteall/server/server.ts +++ b/apps/meteor/app/slashcommands-inviteall/server/server.ts @@ -64,20 +64,20 @@ function inviteAll(type: T): SlashCommand['callback'] { return; } - const cursor = Subscriptions.findByRoomIdWhenUsernameExists(baseChannel._id, { - projection: { 'u.username': 1 }, - }); - try { const APIsettings = settings.get('API_User_Limit'); if (!APIsettings) { return; } - if ((await cursor.count()) > APIsettings) { + if ((await Subscriptions.countByRoomIdWhenUsernameExists(baseChannel._id)) > APIsettings) { throw new Meteor.Error('error-user-limit-exceeded', 'User Limit Exceeded', { method: 'addAllToRoom', }); } + + const cursor = Subscriptions.findByRoomIdWhenUsernameExists(baseChannel._id, { + projection: { 'u.username': 1 }, + }); const users = (await cursor.toArray()).map((s: ISubscription) => s.u.username).filter(isTruthy); if (!targetChannel && ['c', 'p'].indexOf(baseChannel.t) > -1) { diff --git a/apps/meteor/app/slashcommands-open/client/client.ts b/apps/meteor/app/slashcommands-open/client/client.ts index 99438a24eeb0..b0d4d45c22bb 100644 --- a/apps/meteor/app/slashcommands-open/client/client.ts +++ b/apps/meteor/app/slashcommands-open/client/client.ts @@ -3,7 +3,7 @@ import type { Mongo } from 'meteor/mongo'; import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; import { router } from '../../../client/providers/RouterProvider'; -import { Subscriptions, ChatSubscription } from '../../models/client'; +import { Subscriptions } from '../../models/client'; import { sdk } from '../../utils/client/lib/SDKClient'; import { slashCommands } from '../../utils/client/slashCommand'; @@ -23,7 +23,7 @@ slashCommands.add({ ...(type && { t: { $in: type } }), }; - const subscription = ChatSubscription.findOne(query); + const subscription = Subscriptions.findOne(query); if (subscription) { roomCoordinator.openRouteLink(subscription.t, subscription, router.getSearchParameters()); diff --git a/apps/meteor/app/slashcommands-topic/client/topic.ts b/apps/meteor/app/slashcommands-topic/client/topic.ts index f7e47c334b5a..bc32cacd65bd 100644 --- a/apps/meteor/app/slashcommands-topic/client/topic.ts +++ b/apps/meteor/app/slashcommands-topic/client/topic.ts @@ -3,7 +3,7 @@ import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; import { dispatchToastMessage } from '../../../client/lib/toast'; import { callbacks } from '../../../lib/callbacks'; import { hasPermission } from '../../authorization/client'; -import { ChatRoom } from '../../models/client/models/ChatRoom'; +import { Rooms } from '../../models/client/models/Rooms'; import { sdk } from '../../utils/client/lib/SDKClient'; import { slashCommands } from '../../utils/client/slashCommand'; @@ -13,7 +13,7 @@ slashCommands.add({ if (hasPermission('edit-room', message.rid)) { try { await sdk.call('saveRoomSettings', message.rid, 'roomTopic', params); - await callbacks.run('roomTopicChanged', ChatRoom.findOne(message.rid)); + await callbacks.run('roomTopicChanged', Rooms.findOne(message.rid)); } catch (error: unknown) { dispatchToastMessage({ type: 'error', message: error }); throw error; diff --git a/apps/meteor/app/smarsh-connector/server/functions/generateEml.ts b/apps/meteor/app/smarsh-connector/server/functions/generateEml.ts index ce110cbff430..cf0b7deab5db 100644 --- a/apps/meteor/app/smarsh-connector/server/functions/generateEml.ts +++ b/apps/meteor/app/smarsh-connector/server/functions/generateEml.ts @@ -3,10 +3,10 @@ import { Meteor } from 'meteor/meteor'; import moment from 'moment'; import 'moment-timezone'; +import { sendEmail } from './sendEmail'; import { i18n } from '../../../../server/lib/i18n'; import { settings } from '../../../settings/server'; import { MessageTypes } from '../../../ui-utils/server'; -import { sendEmail } from './sendEmail'; const start = ''; diff --git a/apps/meteor/app/smarsh-connector/server/startup.ts b/apps/meteor/app/smarsh-connector/server/startup.ts index f757dea7510b..edd4be457082 100644 --- a/apps/meteor/app/smarsh-connector/server/startup.ts +++ b/apps/meteor/app/smarsh-connector/server/startup.ts @@ -1,8 +1,8 @@ import { cronJobs } from '@rocket.chat/cron'; +import { generateEml } from './functions/generateEml'; import { smarshIntervalValuesToCronMap } from '../../../server/settings/smarsh'; import { settings } from '../../settings/server'; -import { generateEml } from './functions/generateEml'; const smarshJobName = 'Smarsh EML Connector'; diff --git a/apps/meteor/app/statistics/server/functions/otrStats.ts b/apps/meteor/app/statistics/server/functions/otrStats.ts index f936f8d213f7..6553cae42c05 100644 --- a/apps/meteor/app/statistics/server/functions/otrStats.ts +++ b/apps/meteor/app/statistics/server/functions/otrStats.ts @@ -1,7 +1,7 @@ import { Rooms } from '@rocket.chat/models'; -import telemetryEvent from '../lib/telemetryEvents'; import { updateCounter } from './updateStatsCounter'; +import telemetryEvent from '../lib/telemetryEvents'; type otrDataType = { rid: string }; diff --git a/apps/meteor/app/statistics/server/functions/sendUsageReport.ts b/apps/meteor/app/statistics/server/functions/sendUsageReport.ts index a4b1e6182c96..c39276be6f35 100644 --- a/apps/meteor/app/statistics/server/functions/sendUsageReport.ts +++ b/apps/meteor/app/statistics/server/functions/sendUsageReport.ts @@ -1,3 +1,4 @@ +import type { IStats } from '@rocket.chat/core-typings'; import type { Logger } from '@rocket.chat/logger'; import { Statistics } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; @@ -7,32 +8,53 @@ import { Meteor } from 'meteor/meteor'; import { statistics } from '..'; import { getWorkspaceAccessToken } from '../../../cloud/server'; -export async function sendUsageReport(logger: Logger): Promise { - return tracerSpan('generateStatistics', {}, async () => { - const cronStatistics = await statistics.save(); +async function sendStats(logger: Logger, cronStatistics: IStats): Promise { + try { + const token = await getWorkspaceAccessToken(); + const headers = { ...(token && { Authorization: `Bearer ${token}` }) }; - try { - const token = await getWorkspaceAccessToken(); - const headers = { ...(token && { Authorization: `Bearer ${token}` }) }; + const response = await fetch('https://collector.rocket.chat/', { + method: 'POST', + body: { + ...cronStatistics, + host: Meteor.absoluteUrl(), + }, + headers, + }); + + const { statsToken } = await response.json(); + + if (statsToken != null) { + await Statistics.updateOne({ _id: cronStatistics._id }, { $set: { statsToken } }); + return statsToken; + } + } catch (err) { + logger.error({ msg: 'Failed to send usage report', err }); + } +} - const response = await fetch('https://collector.rocket.chat/', { - method: 'POST', - body: { - ...cronStatistics, - host: Meteor.absoluteUrl(), - }, - headers, - }); +export async function sendUsageReport(logger: Logger): Promise { + return tracerSpan('generateStatistics', {}, async () => { + const last = await Statistics.findLast(); + if (last) { + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); - const { statsToken } = await response.json(); + // if the last data we have has less than 24h and was not sent to yet, send it + if (last.createdAt > yesterday) { + // but if it has the confirmation token, we can skip + if (last.statsToken) { + return last.statsToken; + } - if (statsToken != null) { - await Statistics.updateOne({ _id: cronStatistics._id }, { $set: { statsToken } }); - return statsToken; + // if it doesn't it means the request failed, so we try sending again with the same data + return sendStats(logger, last); } - } catch (error) { - /* error*/ - logger.warn('Failed to send usage report'); } + + // if our latest stats has more than 24h, it is time to generate a new one and send it + const cronStatistics = await statistics.save(); + + return sendStats(logger, cronStatistics); }); } diff --git a/apps/meteor/app/statistics/server/functions/slashCommandsStats.ts b/apps/meteor/app/statistics/server/functions/slashCommandsStats.ts index 6d973a64c6be..5cb9e26d6d57 100644 --- a/apps/meteor/app/statistics/server/functions/slashCommandsStats.ts +++ b/apps/meteor/app/statistics/server/functions/slashCommandsStats.ts @@ -1,5 +1,5 @@ -import telemetryEvent from '../lib/telemetryEvents'; import { updateCounter } from './updateStatsCounter'; +import telemetryEvent from '../lib/telemetryEvents'; type slashCommandsDataType = { command: string }; diff --git a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts index 0db63a929491..e1cdab7f0773 100644 --- a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts +++ b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts @@ -6,11 +6,11 @@ import mem from 'mem'; import { Meteor } from 'meteor/meteor'; import UAParser from 'ua-parser-js'; +import { UAParserMobile, UAParserDesktop } from './UAParserCustom'; import { getMostImportantRole } from '../../../../lib/roles/getMostImportantRole'; import { getClientAddress } from '../../../../server/lib/getClientAddress'; import { aggregates } from '../../../../server/models/raw/Sessions'; import { sauEvents } from '../../../../server/services/sauMonitor/events'; -import { UAParserMobile, UAParserDesktop } from './UAParserCustom'; type DateObj = { day: number; month: number; year: number }; diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 84b8f23f7790..25d93a6985c3 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -2,7 +2,7 @@ import { log } from 'console'; import os from 'os'; import { Analytics, Team, VideoConf, Presence } from '@rocket.chat/core-services'; -import type { IRoom, IStats } from '@rocket.chat/core-typings'; +import type { IRoom, IStats, ISetting } from '@rocket.chat/core-typings'; import { UserStatus } from '@rocket.chat/core-typings'; import { NotificationQueue, @@ -29,6 +29,10 @@ import { import { MongoInternals } from 'meteor/mongo'; import moment from 'moment'; +import { getAppsStatistics } from './getAppsStatistics'; +import { getStatistics as getEnterpriseStatistics } from './getEEStatistics'; +import { getImporterStatistics } from './getImporterStatistics'; +import { getServicesStatistics } from './getServicesStatistics'; import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; import { isRunningMs } from '../../../../server/lib/isRunningMs'; import { getControl } from '../../../../server/lib/migrations'; @@ -38,10 +42,6 @@ import { getStatistics as federationGetStatistics } from '../../../federation/se import { settings } from '../../../settings/server'; import { Info } from '../../../utils/rocketchat.info'; import { getMongoInfo } from '../../../utils/server/functions/getMongoInfo'; -import { getAppsStatistics } from './getAppsStatistics'; -import { getStatistics as getEnterpriseStatistics } from './getEEStatistics'; -import { getImporterStatistics } from './getImporterStatistics'; -import { getServicesStatistics } from './getServicesStatistics'; const getUserLanguages = async (totalUsers: number): Promise<{ [key: string]: number }> => { const result = await Users.getUserLanguages(); @@ -92,7 +92,7 @@ export const statistics = { }; // Version - const uniqueID = await Settings.findOne('uniqueID'); + const uniqueID = await Settings.findOne>('uniqueID', { projection: { createdAt: 1 } }); statistics.uniqueId = settings.get('uniqueID'); if (uniqueID) { statistics.installedAt = uniqueID.createdAt.toISOString(); @@ -126,10 +126,10 @@ export const statistics = { // Room statistics statistics.totalRooms = await Rooms.col.countDocuments({}); - statistics.totalChannels = await Rooms.findByType('c').count(); - statistics.totalPrivateGroups = await Rooms.findByType('p').count(); - statistics.totalDirect = await Rooms.findByType('d').count(); - statistics.totalLivechat = await Rooms.findByType('l').count(); + statistics.totalChannels = await Rooms.countByType('c'); + statistics.totalPrivateGroups = await Rooms.countByType('p'); + statistics.totalDirect = await Rooms.countByType('d'); + statistics.totalLivechat = await Rooms.countByType('l'); statistics.totalDiscussions = await Rooms.countDiscussions(); statistics.totalThreads = await Messages.countThreads(); @@ -183,7 +183,7 @@ export const statistics = { // Number of triggers statsPms.push( - LivechatTrigger.col.count().then((count) => { + LivechatTrigger.estimatedDocumentCount().then((count) => { statistics.totalTriggers = count; }), ); @@ -205,13 +205,13 @@ export const statistics = { // Number of Email Inboxes statsPms.push( - EmailInbox.col.count().then((count) => { + EmailInbox.estimatedDocumentCount().then((count) => { statistics.emailInboxes = count; }), ); statsPms.push( - LivechatBusinessHours.col.count().then((count) => { + LivechatBusinessHours.estimatedDocumentCount().then((count) => { statistics.BusinessHours = { // Number of Business Hours total: count, @@ -326,8 +326,7 @@ export const statistics = { room: IRoom, ) { return num + (room.prid ? room.msgs : 0); - }, - 0); + }, 0); statistics.totalPrivateGroupMessages = (await privateGroups.reduce(function _countPrivateGroupMessages(num: number, room: IRoom) { return num + room.msgs; @@ -520,7 +519,7 @@ export const statistics = { ); statsPms.push( - NotificationQueue.col.estimatedDocumentCount().then((count) => { + NotificationQueue.estimatedDocumentCount().then((count) => { statistics.pushQueue = count; }), ); @@ -546,27 +545,27 @@ export const statistics = { statistics.messageAuditLoad = settings.get('Message_Auditing_Panel_Load_Count'); statistics.joinJitsiButton = settings.get('Jitsi_Click_To_Join_Count'); statistics.slashCommandsJitsi = settings.get('Jitsi_Start_SlashCommands_Count'); - statistics.totalOTRRooms = await Rooms.findByCreatedOTR().count(); + statistics.totalOTRRooms = await Rooms.countByCreatedOTR({ readPreference }); statistics.totalOTR = settings.get('OTR_Count'); - statistics.totalBroadcastRooms = await Rooms.findByBroadcast().count(); + statistics.totalBroadcastRooms = await Rooms.countByBroadcast({ readPreference }); statistics.totalTriggeredEmails = settings.get('Triggered_Emails_Count'); statistics.totalRoomsWithStarred = await Messages.countRoomsWithStarredMessages({ readPreference }); statistics.totalRoomsWithPinned = await Messages.countRoomsWithPinnedMessages({ readPreference }); statistics.totalUserTOTP = await Users.countActiveUsersTOTPEnable({ readPreference }); statistics.totalUserEmail2fa = await Users.countActiveUsersEmail2faEnable({ readPreference }); - statistics.totalPinned = await Messages.findPinned({ readPreference }).count(); - statistics.totalStarred = await Messages.findStarred({ readPreference }).count(); - statistics.totalLinkInvitation = await Invites.find().count(); + statistics.totalPinned = await Messages.countPinned({ readPreference }); + statistics.totalStarred = await Messages.countStarred({ readPreference }); + statistics.totalLinkInvitation = await Invites.estimatedDocumentCount(); statistics.totalLinkInvitationUses = await Invites.countUses(); statistics.totalEmailInvitation = settings.get('Invitation_Email_Count'); - statistics.totalE2ERooms = await Rooms.findByE2E({ readPreference }).count(); + statistics.totalE2ERooms = await Rooms.countByE2E({ readPreference }); statistics.logoChange = Object.keys(settings.get('Assets_logo') || {}).includes('url'); statistics.showHomeButton = settings.get('Layout_Show_Home_Button'); statistics.totalEncryptedMessages = await Messages.countByType('e2e', { readPreference }); statistics.totalManuallyAddedUsers = settings.get('Manual_Entry_User_Count'); - statistics.totalSubscriptionRoles = await RolesRaw.findByScope('Subscriptions').count(); - statistics.totalUserRoles = await RolesRaw.findByScope('Users').count(); - statistics.totalCustomRoles = await RolesRaw.findCustomRoles({ readPreference }).count(); + statistics.totalSubscriptionRoles = await RolesRaw.countByScope('Subscriptions', { readPreference }); + statistics.totalUserRoles = await RolesRaw.countByScope('Users', { readPreference }); + statistics.totalCustomRoles = await RolesRaw.countCustomRoles({ readPreference }); statistics.totalWebRTCCalls = settings.get('WebRTC_Calls_Count'); statistics.uncaughtExceptionsCount = settings.get('Uncaught_Exceptions_Count'); diff --git a/apps/meteor/app/theme/client/imports/components/emoji.css b/apps/meteor/app/theme/client/imports/components/emoji.css index 5180c05c5f5c..58668075584d 100644 --- a/apps/meteor/app/theme/client/imports/components/emoji.css +++ b/apps/meteor/app/theme/client/imports/components/emoji.css @@ -1,21 +1,16 @@ .emoji { position: relative; - display: inline-block; overflow: hidden; - width: 1.375rem; height: 1.375rem; margin: 0 0.15em; - vertical-align: middle; white-space: nowrap; text-indent: 100%; - background-repeat: no-repeat; background-position: center; background-size: contain; - font-size: inherit; line-height: normal; image-rendering: auto; diff --git a/apps/meteor/app/theme/client/imports/components/flex-nav.css b/apps/meteor/app/theme/client/imports/components/flex-nav.css index c521fc6a49b2..6db8bf21ee94 100644 --- a/apps/meteor/app/theme/client/imports/components/flex-nav.css +++ b/apps/meteor/app/theme/client/imports/components/flex-nav.css @@ -3,12 +3,9 @@ z-index: 3; top: 0; left: 0; - width: 100%; height: 100%; - transition: transform 0.15s cubic-bezier(0.5, 0, 0.1, 1); - background-color: var(--flex-nav-background); will-change: transform; diff --git a/apps/meteor/app/theme/client/imports/components/loading.css b/apps/meteor/app/theme/client/imports/components/loading.css index a6ff93cc4fb7..d3212475d393 100644 --- a/apps/meteor/app/theme/client/imports/components/loading.css +++ b/apps/meteor/app/theme/client/imports/components/loading.css @@ -1,51 +1,44 @@ -.loading__animation { - display: flex; - align-items: center; - justify-content: center; -} - -.loading__animation__bounce { - display: inline-block; - - width: 1rem; - height: 1rem; - margin: 2px; - - animation: loading-bouncedelay 1.4s infinite ease-in-out both; - - -webkit-animation-delay: -0.32s; - animation-delay: -0.32s; - - border-radius: 100%; - background-color: var(--loading-bounce-color-light); - - @media (prefers-color-scheme: dark) { - background: var(--loading-bounce-color-dark); +.loading { + &__animation { + display: flex; + align-items: center; + justify-content: center; } -} - -.loading__animation__bounce--medium { - display: inline-block; - width: 1.25rem; - height: 1.25rem; -} + &__animation__bounce { + display: inline-block; + width: 1rem; + height: 1rem; + margin: 2px; + animation: loading-bouncedelay 1.4s infinite ease-in-out both; + animation-delay: -0.32s; + border-radius: 100%; + background-color: var(--loading-bounce-color-light); + + @media (prefers-color-scheme: dark) { + background: var(--loading-bounce-color-dark); + } + } -.loading__animation__bounce--large { - display: inline-block; + &__animation__bounce--medium { + display: inline-block; + width: 1.25rem; + height: 1.25rem; + } - width: 1.5rem; - height: 1.5rem; -} + &__animation__bounce--large { + display: inline-block; + width: 1.5rem; + height: 1.5rem; + } -.loading__animation__bounce + .loading__animation__bounce { - -webkit-animation-delay: -0.16s; - animation-delay: -0.16s; -} + &__animation__bounce + &__animation__bounce { + animation-delay: -0.16s; + } -.loading__animation__bounce + .loading__animation__bounce + .loading__animation__bounce { - -webkit-animation-delay: 0s; - animation-delay: 0s; + &__animation__bounce + &__animation__bounce + &__animation__bounce { + animation-delay: 0s; + } } @keyframes loading-bouncedelay { diff --git a/apps/meteor/app/theme/client/imports/components/main-content.css b/apps/meteor/app/theme/client/imports/components/main-content.css index 4c01f982ee36..3fd983bff8f4 100644 --- a/apps/meteor/app/theme/client/imports/components/main-content.css +++ b/apps/meteor/app/theme/client/imports/components/main-content.css @@ -1,14 +1,10 @@ .main-content { position: relative; - z-index: 0; - display: flex; flex-direction: column; flex: 1 1 100%; - width: 1vw; - height: 100%; } diff --git a/apps/meteor/app/theme/client/imports/components/message-box.css b/apps/meteor/app/theme/client/imports/components/message-box.css index 877a643e114f..f55a49f4ec37 100644 --- a/apps/meteor/app/theme/client/imports/components/message-box.css +++ b/apps/meteor/app/theme/client/imports/components/message-box.css @@ -1,114 +1,6 @@ .rc-message-box { position: relative; - width: 100%; padding: 0 24px; - font-size: var(--message-box-text-size); - - &__toolbar-formatting-item { - display: flex; - - min-width: 16px; - margin: 0 4px; - - transition: color 0.1s; - - color: var(--message-box-markdown-color); - align-items: center; - justify-content: center; - - &:hover, - &:focus, - &.active { - color: var(--message-box-markdown-hover-color); - } - } - - &__toolbar-formatting-link { - color: currentColor; - - font-size: 0.75rem; - } - - &__audio-message { - display: flex; - - &-mic { - display: flex; - } - - &-done, - &-cancel, - &-timer { - display: none; - } - - &-done { - color: var(--rc-color-success); - } - - &-cancel { - color: var(--rc-color-error); - } - - &-timer { - margin: 0 -0.25rem; - align-items: center; - justify-content: center; - - &-dot, - &-text { - margin: 0 0.25rem; - } - - &-text { - min-width: 3em; - } - - &-dot { - flex: 0 0 auto; - - width: 0.5rem; - height: 0.5rem; - - border-radius: 50%; - background-color: red; - } - } - - &--recording { - .rc-message-box__audio-message-mic, - .rc-message-box__audio-message-loading { - display: none; - } - - .rc-message-box__audio-message-done, - .rc-message-box__audio-message-cancel, - .rc-message-box__audio-message-timer { - display: flex; - } - } - - &--loading { - .rc-message-box__audio-message-mic, - .rc-message-box__audio-message-done, - .rc-message-box__audio-message-cancel, - .rc-message-box__audio-message-timer { - display: none; - } - - .rc-message-box__audio-message-loading { - display: flex; - } - } - - &--busy { - .rc-message-box__audio-message-mic { - cursor: not-allowed; - - opacity: 0.5; - } - } - } } diff --git a/apps/meteor/app/theme/client/imports/components/messages.css b/apps/meteor/app/theme/client/imports/components/messages.css deleted file mode 100644 index 34ae60609df0..000000000000 --- a/apps/meteor/app/theme/client/imports/components/messages.css +++ /dev/null @@ -1,67 +0,0 @@ -.messages-container-wrapper { - position: relative; -} - -.message-actions { - position: absolute; - z-index: 2; - top: -28px; - right: 0.4rem; - - display: none; - - box-sizing: border-box; - - padding: 2px; - - user-select: none; - - color: var(--color-darkest); - - border: 1px solid #f2f3f7; - border-radius: 4px; - - background: var(rcx-surface, #ffffff); - - font-size: 1.25rem; - - &__buttons { - display: flex; - } - - &__button, - &__menu { - padding: 2px; - - cursor: pointer; - - &:hover { - border-radius: 2px; - background: #eeeff1; - } - - &-icon { - fill: currentColor; - } - } -} - -.rc-ui-kit { - display: inline-block; - - width: 100%; - - max-width: 400px; -} - -.rtl .message-actions { - right: auto; - left: 2px; -} - -.message-popup .rcx-message-attachment { - overflow-y: auto !important; - - max-width: 100% !important; - max-height: 200px !important; -} diff --git a/apps/meteor/app/theme/client/imports/components/modal.css b/apps/meteor/app/theme/client/imports/components/modal.css deleted file mode 100644 index 50cdb198b072..000000000000 --- a/apps/meteor/app/theme/client/imports/components/modal.css +++ /dev/null @@ -1,168 +0,0 @@ -.rc-modal { - display: flex; - flex-direction: column; - - min-width: 400px; - max-width: 500px; - height: auto; - max-height: 90%; - - padding: 1rem; - - animation: dropdown-show 0.3s cubic-bezier(0.45, 0.05, 0.55, 0.95); - - border: none; - - background: white; - - box-shadow: 0 0 2px 0 rgba(47, 52, 61, 0.08), 0 0 12px 0 rgba(47, 52, 61, 0.12); - - &-wrapper { - position: fixed; - z-index: 10; - top: 0; - right: 0; - bottom: 0; - left: 0; - - display: flex; - - padding: 10px; - - background: rgba(47, 52, 61, 0.8); - align-items: center; - justify-content: center; - } - - &--modal { - width: 640px; - max-width: 100%; - } - - &--modal &__title { - font-size: 1.375rem; - font-weight: 500; - } - - &__description { - font-size: 0.874rem; - margin-block-end: 1.5rem; - margin-block-start: 0.5rem; - } - - &__title { - flex: 1 1 auto; - - font-size: 1rem; - } - - &__close { - cursor: pointer; - transform: rotate(45deg); - - font-size: 20px; - } - - &__header { - display: flex; - flex-direction: row; - flex: 0 0 auto; - - margin-bottom: -16px; - padding: 16px; - - font-size: 21px; - justify-content: flex-end; - } - - &__content { - position: relative; - - display: flex; - - overflow: hidden auto; - - flex-direction: column; - - min-height: 72px; - max-height: 90%; - - padding: 16px; - - animation: dropdown-show 0.1s cubic-bezier(0.45, 0.05, 0.55, 0.95); - align-items: stretch; - } - - &__content-icon { - margin: 1.2rem auto; - - font-size: 6rem; - - &--modal-warning { - color: var(--rc-color-alert); - } - - &--modal-success { - color: var(--rc-color-success-light); - } - - &--modal-info { - color: var(--rc-color-alert); - } - - &--modal-error { - color: var(--rc-color-error-light); - } - } - - &__content-text { - text-align: center; - word-break: break-word; - - font-size: 1rem; - line-height: 1.5; - } - - &__content-error { - display: none; - - width: 100%; - margin: 0.5rem 0; - - text-align: center; - word-break: break-word; - - color: var(--color-white); - background: var(--rc-color-error); - - line-height: 2; - } - - &__footer { - display: flex; - - flex: 0 0 auto; - - padding: 1rem; - - justify-content: space-between; - - & > .rc-button { - margin: 0; - } - } -} - -@media (width <= 400px) { - .rc-modal { - top: 0 !important; - bottom: 0; - left: 0 !important; - - width: 100%; - min-width: 100%; - max-width: 100%; - - animation: dropup-show 0.3s cubic-bezier(0.45, 0.05, 0.55, 0.95); - } -} diff --git a/apps/meteor/app/theme/client/imports/forms/button.css b/apps/meteor/app/theme/client/imports/forms/button.css deleted file mode 100644 index 463ed17346ca..000000000000 --- a/apps/meteor/app/theme/client/imports/forms/button.css +++ /dev/null @@ -1,219 +0,0 @@ -.rc-button { - &:not([disabled]):hover { - opacity: 0.6; - } - - &--icon > svg { - margin: 0 5px 0 -5px; - - font-size: 20px; - fill: currentColor; - - .rtl & { - margin: 0 -5px 0 5px; - } - } - - position: relative; - - display: flex; - - height: 40px; - - min-height: 40px; - - padding: 0 1.5rem; - - cursor: pointer; - transition: opacity 0.3s, background-color 0.3s, color 0.3s; - text-align: center; - - color: #000000; - - border-width: var(--button-border-width); - border-style: solid; - border-color: #000000; - - border-radius: var(--button-border-radius); - background-color: transparent; - - font-size: var(--button-text-size); - font-weight: 600; - - align-items: center; - justify-content: center; - - &:active, - &:focus:hover { - outline: none; - } - - &:active { - transform: translateY(2px); - - opacity: 0.9; - } - - &:active::before { - top: -2px; - } - - &:disabled { - cursor: default; - - color: var(--button-disabled-text-color); - border: 0; - border-color: var(--button-disabled-background); - background-color: var(--button-disabled-background); - } - - &--invisible { - visibility: hidden; - } - - &--primary { - color: var(--button-primary-text-color); - border: 0; - background-color: var(--button-primary-background); - } - - &--nude { - border: none; - background-color: inherit; - - font-weight: 400; - } - - &--primary.rc-button--nude { - color: var(--button-primary-background); - } - - &--primary.rc-button--outline { - color: var(--button-primary-background); - border-color: var(--button-primary-background); - } - - &--secondary { - color: var(--button-secondary-text-color); - border: 0; - border-color: var(--button-secondary-background); - background-color: var(--button-secondary-background); - } - - &--secondary.rc-button--outline { - color: var(--button-secondary-background); - border-color: var(--button-secondary-background); - } - - &--cancel { - color: var(--button-primary-text-color); - border: 0; - border-color: var(--button-cancel-color); - background-color: var(--button-cancel-color); - } - - &--cancel.rc-button--outline { - color: var(--button-cancel-color); - border-color: var(--button-cancel-color); - } - - &--small { - height: var(--button-height-small); - min-height: var(--button-height-small); - padding: var(--button-padding-small); - - font-size: var(--button-text-size-small); - } - - &--square { - display: flex; - flex: 0 0 var(--button-square-size); - - margin: 0; - padding: 0; - align-items: center; - justify-content: center; - } - - &--outline { - border-width: 2px; - border-style: solid; - background: transparent; - } - - &--stack { - width: 100%; - } - - &--no-padding { - padding-right: 0; - padding-left: 0; - } - - &.loading { - position: relative; - - padding-right: calc(3 * 0.782rem); - - transition: padding-right 0.3s; - - &::before { - position: absolute; - top: 25%; - right: 0.782rem; - - display: block; - - width: 20px; - height: 20px; - - content: ""; - animation: spin 1s infinite cubic-bezier(0.14, 0.48, 0.45, 0.63); - - border: 0.15rem solid rgba(127, 127, 127, 0.5); - border-top-color: white; - border-radius: 50%; - } - } - - &__group { - display: flex; - - flex-direction: row; - - margin: 10px -5px; - - justify-content: flex-end; - - & > .rc-button { - margin: 0 5px; - } - - &--wrap { - margin: 5px -5px; - flex-wrap: wrap; - - & > .rc-button { - margin: 5px; - } - } - - &--stretch { - justify-content: stretch; - - & > .rc-button { - flex: 1 1; - } - } - - &--vertical { - flex-direction: column; - } - } -} - -@media (width < 780px) { - .rc-button--full { - width: 100%; - } -} diff --git a/apps/meteor/app/theme/client/imports/forms/input.css b/apps/meteor/app/theme/client/imports/forms/input.css deleted file mode 100644 index 21ef11f574fa..000000000000 --- a/apps/meteor/app/theme/client/imports/forms/input.css +++ /dev/null @@ -1,245 +0,0 @@ -textarea.rc-input__element { - height: auto; - padding: 0.5rem 1rem; - - font-family: inherit; -} - -.rc-input { - position: relative; - - width: 100%; - - &__label { - display: block; - - cursor: pointer; - } - - &__title { - font-size: var(--input-font-size); - } - - &__wrapper { - position: relative; - - padding: 0.5rem 0; - - color: var(--input-icon-color); - } - - &__icon { - position: absolute; - top: 0; - left: 1rem; - - display: flex; - - width: 20px; - height: 100%; - - cursor: default; - align-items: center; - justify-content: center; - - & + .rc-input__element { - padding: 0 1rem 0 2.75rem; - } - - &--right { - right: 1rem; - left: auto; - } - - &--clickable { - cursor: pointer; - } - - &--right + .rc-input__element { - padding: 0 2.75rem 0 1rem; - } - } - - &__element { - width: 100%; - height: 2.5rem; - padding: 0 1rem; - - text-align: start; - - text-overflow: ellipsis; - - color: var(--input-text-color); - - border-width: var(--input-border-width); - border-color: var(--input-border-color); - border-radius: var(--input-border-radius); - background-color: transparent; - - font-size: var(--input-font-size); - line-height: normal; - - &--small { - height: 2rem; - } - - &::placeholder { - text-align: start; - text-overflow: ellipsis; - - color: var(--input-placeholder-color); - } - - &[type=color] { - height: 45px; - } - } - - &__description { - margin-bottom: 0.25rem; - - color: var(--input-description-text-color); - - font-size: var(--input-description-text-size); - } - - &__error { - display: flex; - - color: var(--input-error-color); - align-items: center; - - &-icon { - width: 20px; - - &--warning { - width: 20px; - height: 20px; - stroke: currentColor; - } - } - - &-message { - margin-left: 0.5rem; - } - } - - &--small { - font-size: 14px; - - & .rc-input__element { - padding-top: 8px; - padding-bottom: 8px; - } - } - - &--error { - .rc-tags { - border-color: var(--input-error-color); - } - - & .rc-input { - &__element { - border-color: var(--input-error-color); - } - } - } -} - -.rc-input-file { - position: relative; - - width: 100%; - - &__label { - display: block; - - cursor: pointer; - } - - &__title { - font-size: var(--input-font-size); - } - - &__wrapper { - display: flex; - flex-direction: row; - - width: 100%; - margin: 0.5rem 0; - padding: 0.3rem; - - color: var(--input-text-color); - - border-width: var(--input-border-width); - border-color: var(--input-border-color); - border-radius: var(--input-border-radius); - background-color: transparent; - - font-size: var(--input-font-size); - align-items: center; - justify-content: flex-end; - - &::placeholder { - color: var(--input-placeholder-color); - } - - & > .rc-button { - flex: 0 0 auto; - - height: 10px; - min-height: 31px; - margin: 0; - padding: 8px; - - border-color: #e1e5e8; - border-radius: 2px; - background: #f1f1f1; - } - } - - &__name { - overflow: hidden; - - flex: 0 1 auto; - - width: 100%; - - padding: 0 5px; - - white-space: nowrap; - - text-overflow: ellipsis; - - line-height: 16px; - } - - &__element { - display: none; - } - - &__description { - color: var(--color-gray); - - font-size: 0.875rem; - } -} - -select.rc-input { - width: 100%; - padding: 0.782rem; - - color: var(--input-text-color); - - border-width: var(--input-border-width); - border-color: var(--input-border-color); - border-radius: var(--input-border-radius); - background-color: transparent; - - font-size: var(--input-font-size); - appearance: none; - - &--small { - padding: 0.5rem 1rem; - } -} diff --git a/apps/meteor/app/theme/client/imports/general/base.css b/apps/meteor/app/theme/client/imports/general/base.css index 9aa9b6c5ea9f..0f1f90c91355 100644 --- a/apps/meteor/app/theme/client/imports/general/base.css +++ b/apps/meteor/app/theme/client/imports/general/base.css @@ -6,7 +6,6 @@ html { overflow-y: auto; - height: 100%; &.noscroll { @@ -16,17 +15,12 @@ html { body { position: relative; - display: flex; - overflow: visible; - flex-direction: column; - width: 100%; height: 100%; padding: 0; - font-family: var(--body-font-family); font-size: var(--text-small-size); -webkit-font-smoothing: antialiased; @@ -45,43 +39,32 @@ body { &::after { display: table; clear: both; - content: ''; } } button { padding: 0; - cursor: pointer; text-align: left; text-transform: inherit; - color: inherit; border-width: 0; background: none; - font-style: inherit; } #rocket-chat { position: relative; - display: flex; - overflow: hidden; - flex: 1 1 auto; - height: 100%; - max-height: 100%; - align-items: stretch; &.animated-hidden { visibility: hidden; - opacity: 0; } } @@ -96,37 +79,25 @@ button { @media print { #rocket-chat.menu-nav, - .simplebar-content-wrapper, - section.rcx-box.rcx-box--full, - .rc-old.main-content { + .main-content { overflow: visible !important; /* 1 */ - height: auto !important; /* 1 */ max-height: none !important; /* 1 */ flex-shrink: 0 !important; /* 1 */ } - div.simplebar-offset, - .simplebar-mask, - .rc-old .messages-container .wrapper { + .messages-container .wrapper { position: relative !important; /* 1 */ } - .simplebar-placeholder { - height: 0 !important; /* 1 */ - } - body { height: auto !important; /* 1 */ } - .rc-old .room-container, - .rc-old .room-container:first-child, - .rc-old .messages-container-main, - .rc-old .messages-container-wrapper, - .rc-old .main-content-flex { + .messages-container-main, + .messages-container-wrapper, + .main-content-flex { flex: 1 0 auto !important; /* 1 */ - height: auto !important; /* 1 */ } @@ -134,15 +105,15 @@ button { display: none !important; /* 1 */ } - .rcx-box.rcx-box--full { - overflow: visible !important /* 1 */; - - height: auto !important /* 1 */; + .rcx-box { + &--full.rcx-box { + overflow: visible !important /* 1 */; + height: auto !important /* 1 */; + } } .rc-scrollbars-container { overflow: visible !important; /* 1 */ - height: auto !important; /* 1 */ } @@ -153,6 +124,5 @@ button { .gallery-item { max-width: 100%; - cursor: pointer; } diff --git a/apps/meteor/app/theme/client/imports/general/base_old.css b/apps/meteor/app/theme/client/imports/general/base_old.css index 3120d9c05ff0..5b924491b54e 100644 --- a/apps/meteor/app/theme/client/imports/general/base_old.css +++ b/apps/meteor/app/theme/client/imports/general/base_old.css @@ -1,730 +1,37 @@ -.rc-old .no-scroll { - overflow: hidden !important; -} - -.rc-old code { - margin: 5px 0; - padding: 0.5em; - - text-align: left; - vertical-align: middle; - white-space: pre-wrap; - word-wrap: break-word; - - border-width: 1px; - border-radius: var(--border-radius); - - font-family: Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; - font-size: 13px; - font-weight: 600; - direction: ltr; - unicode-bidi: embed; - - &.inline { - display: inline; - - padding: 0.05rem 0.2rem; - - line-height: 1.25rem; - } -} - -.rc-old code.hljs { - overflow-y: hidden; -} - -.rc-old pre { - display: inline-block; - - width: 100%; -} - -.rc-old blockquote { - position: relative; - - display: block; - - clear: both; - - min-height: 20px; - - padding-left: 10px; - - &::after { - display: table; - clear: both; - - content: ''; - } - - &:first-child::before { - border-radius: 2px 2px 0 0; - } - - &:last-child::before { - border-radius: 0 0 2px 2px; - } - - &::before { - position: absolute; - top: 0; - bottom: 0; - left: 0; - - width: 2px; - - content: ' '; - } -} - -.rc-old .login-terms { - width: 520px; - max-width: 100%; - margin: auto; - padding: 10px; - - font-size: smaller; - - & a { - font-weight: bold !important; - } -} - -.upload-preview { - & .upload-preview-file { - height: 200px; - - background-repeat: no-repeat; - background-position: center center; - background-size: contain; - } -} - -.upload-preview-title { - padding: 3px; - - border-radius: 0 0 5px 5px; -} - -.copyonly { - display: none; - - width: 0; - height: 0; - - user-select: none; - vertical-align: baseline; - - font-size: 0; - -moz-box-orient: vertical; - - code & { - float: left; - } -} - -.rc-old .full-page .hidden { - display: none !important; - visibility: hidden !important; -} - -.rc-old .hidden { - display: none !important; -} - -.rc-old .page-container { - position: absolute; - top: 0; - left: 0; - - display: flex; - - overflow-y: hidden; - flex-direction: column; - - width: 100%; - height: 100%; - - padding: 1.25rem; - - & .content { - display: flex; - overflow-y: auto; - flex-direction: column; - flex: 1 1 auto; - - margin: 0 -1.25rem; - padding: 1.25rem 1.25rem 0; - - line-height: 1.3em; - -webkit-overflow-scrolling: touch; - - & fieldset { - margin-bottom: 1em; - } - - & .rocket-form { - & fieldset { - display: block; - - margin: 1em 0 1.5em; - - & small { - font-size: 11px; - } - } - - & legend { - position: relative; - - display: block; - - width: 100%; - margin: 12px 0; - - font-weight: bold; - - & h3 { - margin-bottom: 5px !important; - } - } - - & .submit { - margin-top: 20px; - - text-align: right; - } - } - } -} - -.rc-old .input-line { - display: block; - clear: both; - - margin-bottom: 12px; - - &::after { - display: table; - clear: both; - - content: ''; - } - - &:nth-last-child(1) { - margin-bottom: 0; - } - - &.search { - & i { - position: absolute; - top: 10px; - left: 7px; - } - - & .icon-spin { - right: 5px; - left: auto; - - animation-name: spin; - animation-duration: 2000ms; - animation-timing-function: linear; - animation-iteration-count: infinite; - - font-weight: 400; - } - - & input { - padding-left: 30px; - } - } - - & > label { - display: block; - - margin-bottom: 4px; - } - - & > div { - position: relative; - - & .right { - position: absolute; - z-index: 10; - top: 10px; - right: 10px; - } - } - - & > div.-autocomplete-container { - position: absolute; - } - - & input[type='text'] { - display: block; - } -} - -.rc-old form.inline { - & input[type='text'], - & input[type='number'], - & input[type='email'], - & input[type='url'], - & input[type='password'], - & select { - width: auto; - } -} - -.rc-old .-autocomplete-container { - top: auto; - - width: 100%; - - border-width: 0; - border-radius: 0; - box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.2); - - & p { - padding: 8px; - - font-size: 14px; - } - - & .loading-animation { - position: relative; - - min-height: 60px; - } -} - -.rc-old .-autocomplete-item { - padding: 8px 5px; - - cursor: pointer; - - font-size: 12px; -} - -.rc-old label.required::after { - content: ' *'; -} - -.rc-old.flex-nav { - position: fixed; - z-index: 3; - top: 0; - left: 0; - - overflow-x: hidden; - overflow-y: auto; - - width: var(--rooms-box-width); - height: 100%; - - transition: transform 0.15s cubic-bezier(0.5, 0, 0.1, 1); - - &.animated-hidden { - transform: translateX(-100%); - - & header, - & footer, - & .content { - transform: translateX(-100%); - } - } - - & header, - & footer, - & .content { - transition: transform 0.425s cubic-bezier(0, 0.8, 0.05, 1); - } - - & > section { - position: absolute; - top: 0; - left: 0; - - width: 100%; - height: 100%; - } - - & header { - position: absolute; - z-index: 110; - top: 0; - left: 0; - - display: table; - - width: 100%; - height: var(--header-min-height); - min-height: var(--header-min-height); - padding-left: 15px; - - cursor: pointer; - - & > div { - display: table-cell; - - text-align: left; - vertical-align: middle; - } - - & h4 { - position: relative; - - overflow: hidden; - - margin-top: 2px; - - text-overflow: ellipsis; - - font-size: 20px; - font-weight: 300; - line-height: 24px; - } - - & p { - margin-top: 4px; - - font-size: 13px; - font-weight: 400; - line-height: 18px; - } - } - - & footer { - position: absolute; - z-index: 120; - bottom: 0; - left: 0; - - display: table; - - width: 100%; - height: var(--footer-min-height); - padding: 0 10px; - - text-align: left; - - & > div { - display: table-cell; - - text-align: left; - vertical-align: middle; - } - } - - & .content { - position: absolute; - top: var(--header-min-height); - - display: block; - overflow-x: hidden; - overflow-y: auto; - - width: 100%; - height: calc(100% - calc(var(--header-min-height) + var(--footer-min-height))); - padding: 20px 10px; - direction: rtl; - -webkit-overflow-scrolling: touch; - - & > .wrapper { - direction: ltr; - } - - & h4 { - margin-bottom: 30px; - - text-transform: uppercase; - - font-size: 13px; - font-weight: 400; - } - } - - & .input-line { - margin-bottom: 25px; - - &:nth-last-child(1) { - margin-bottom: 0; - } - - & label { - margin-bottom: 0; - - font-weight: 400; - } - - & input[type='text'], - & input[type='password'], - & select { - padding: 0 8px 0 30px; - - border-width: 1px; - border-radius: var(--border-radius); - box-shadow: 0 0 0; - appearance: none; - } - - & .inline-fields { - & input, - & label, - & select { - display: inline-block; - } - } - - &.toggle { - font-size: 0; - - & > label { - display: inline-block; - - width: calc(100% - 40px); - - vertical-align: top; - - font-size: 14px; - } - - & > div { - display: inline-block; - - width: 40px; - } - } - - &.no-icon input { - padding: 0 8px; - } - } - - & .selected-users { - padding: 20px 0 0; - - & li { - display: inline-block; - - margin-right: 2px; - margin-bottom: 2px; - padding: 5px; - } - - & i { - cursor: pointer; - } - } -} - -.rc-old .new-room-highlight a { - animation: highlight 6s infinite; -} - -/* MAIN CONTENT + MAIN PAGES */ - -.rc-old .page-settings { - & .settings-file-preview { - display: flex; - align-items: center; - - & input[type='file'] { - position: absolute !important; - z-index: 10000; - top: 0; - left: 0; - - width: 100%; - height: 100%; - - cursor: pointer; - - opacity: 0; - - & * { - cursor: pointer; - } - } - - & .preview { - overflow: hidden; - - width: 100px; - height: 40px; - - margin-right: 0.75rem; - - border-width: var(--input-border-width); - border-color: var(--input-border-color); - border-radius: var(--input-border-radius); - - background-repeat: no-repeat; - background-position: center center; - background-size: contain; - - &.no-file { - display: flex; - - align-items: center; - justify-content: center; - } - } - } -} - -.rc-old .room-not-found { - display: flex; - flex-direction: column; - - font-size: 30px; - align-items: center; - justify-content: center; - - & div { - text-align: center; - - line-height: 40px; - } - - & i { - padding-bottom: 30px; - - font-size: 100px; - } -} - -.rc-old .container-bars { - position: relative; - z-index: 2; - +.copyonly { display: none; - visibility: hidden; - overflow: hidden; - flex-direction: column; - - margin: 8px 10px 0; - - transition: transform 0.4s ease, visibility 0.3s ease, opacity 0.3s ease; - transform: translateY(-10px); - - opacity: 0; - border-radius: var(--border-radius); - box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.2), 0 2px 10px 0 rgba(0, 0, 0, 0.16); - - font-size: 1em; - font-weight: bold; - - &.show { - display: flex; - visibility: visible; - - transform: translateY(0); - - opacity: 1; - } - - & > div { - padding: 0 10px; - - line-height: 28px; - } - - & .upload-progress { - position: relative; - - display: flex; - - height: 28px; - - & .upload-progress-progress { - position: absolute; - z-index: 1; - left: 0; - - width: 0%; - height: 100%; - - transition: width, 1s, ease-out; - } - - & .upload-progress-text { - z-index: 2; - right: 0; - left: 0; - - overflow: hidden; - - width: 50%; - - height: 100%; - padding: 0 10px; - - white-space: nowrap; - text-overflow: ellipsis; - flex-grow: 1; - - & > a { - float: right; - - cursor: pointer; - text-transform: uppercase; - } - } - - & .upload-progress-close { - position: relative; - z-index: 3; - - float: right; - - height: 100%; - - text-transform: uppercase; + width: 0; + height: 0; + user-select: none; + vertical-align: baseline; + font-size: 0; + -moz-box-orient: vertical; - font-weight: bold; - } + code & { + float: left; } } -.rc-old .flex-tab-main-content { - position: relative; - z-index: 1; - - overflow: auto; - flex-grow: 1; -} - -.rc-old .room-container { - height: 100%; -} - /* change to page-messages */ -.rc-old .messages-container { +.messages-container { position: relative; - display: flex; flex-direction: column; flex: 1; - width: 100%; &-wrapper { display: flex; - flex: 1 1 auto; - height: 1%; } &-main { position: relative; - display: flex; flex-direction: column; flex: 1 1 auto; - width: 50%; } @@ -732,13 +39,9 @@ position: absolute; top: 0; left: 0; - - overflow-x: hidden; - overflow-y: auto; - + overflow: hidden auto; width: 100%; height: 100%; - word-wrap: break-word; -webkit-overflow-scrolling: touch; } @@ -748,23 +51,8 @@ } } -.rc-old .preview-items .popup-item { - margin: 2px; - - padding: 0; - - cursor: pointer; - - user-select: none; - - border: solid 1px transparent; - - line-height: initial; -} - .messages-box { position: relative; - overflow: hidden; flex-grow: 1; @@ -775,392 +63,289 @@ & ul.messages-list { padding: 21px 0 10px; } - - & .editing .body { - border-radius: var(--border-radius); - } -} - -.rcx-message { - &.highlight { - animation: highlight 6s; - } -} - -.rc-old .attachment-description { - margin: 6px; - - line-height: 1; -} - -/* FLEX-TAB and FLEX-TAB views */ -.rc-old .main-content-flex { - display: flex; - - height: 100%; - flex-grow: 1; } -.page-loading { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - - display: flex; - - text-align: center; - - background-color: var(--page-loading-background-light); - - align-items: center; - justify-content: center; - - @media (prefers-color-scheme: dark) { - background: var(--page-loading-background-dark); - } +.highlight-text { + padding: 0 2px 2px; + color: var(--rcx-color-font-pure-white, #ffffff); + border-radius: var(--border-radius); + background-color: var(--rcx-color-badge-background-level-4, #f5455c); } -.rc-old.full-page { - display: flex; - overflow: auto; - +.inline-video { width: 100%; - min-height: 100%; - margin: auto; - - text-align: center; - - background-repeat: no-repeat; - background-attachment: fixed; - - background-position: center; - background-size: cover; - align-items: flex-start; - flex-flow: row nowrap; - - & .wrapper { - position: relative; - z-index: 10; - - width: 100%; - margin: auto; - padding: 20px; - - text-align: center; - } - - & .logo { - display: block; - - width: 100%; - max-width: 520px; - margin: 0 auto; - padding: 0 20px; - - & > img { - position: relative; - z-index: 20; - top: 0; - right: 0; - - display: inline-block; - - max-width: 100%; - max-height: 150px; - } - } - - & a:not(.rcx-box) { - font-weight: 300; - } - - & .cell { - display: table-cell; - - text-align: center; - vertical-align: middle; - } - - & header:not(.rcx-box) { - position: relative; - z-index: 1; - - display: block; - - max-width: 520px; - margin: auto; - padding: 0 20px; - } - - & .text { - position: relative; - z-index: 1; - - max-width: 580px; - margin: 0 auto 25px; - - font-weight: 300; - - & .button { - margin-top: 20px; - padding: 16px 20px; - - font-weight: 400; - } - - & h1 { - display: none; - - margin-bottom: 20px; - - letter-spacing: -0.5px; - text-transform: uppercase; - - font-size: 24px; - font-weight: 600; - } - - & h2 { - margin: 18px 0; - - letter-spacing: -0.5px; - text-transform: uppercase; - - font-size: 20px; - font-weight: 300; - } - - & h3 { - margin: 18px 0; - - letter-spacing: -0.5px; - text-transform: uppercase; - - font-size: 16px; - font-weight: 300; - } - - & p { - margin: 18px 0; - - font-size: 16px; - font-weight: 400; - line-height: 24px; - } - } - - & footer { - position: relative; - z-index: 1; - - padding: 20px 0 0; - - & h4 { - margin-bottom: 8px; - - text-transform: uppercase; - - font-size: 12px; - font-weight: 300; - } - } - - & .share { - min-height: 40px; + max-width: 480px; + height: auto; + max-height: 270px; +} - border-radius: 50%; +.load-more { + position: relative; + height: 2rem; +} - line-height: 20px; +.rcx-message { + &.highlight { + animation: highlight 6s; + } +} - &::before { - border-radius: 50%; - } +.page-loading { + position: absolute; + inset: 0; + display: flex; + text-align: center; + background-color: var(--page-loading-background-light); + align-items: center; + justify-content: center; - & span { - display: none; - } + @media (prefers-color-scheme: dark) { + background: var(--page-loading-background-dark); } } -.rc-old .highlight-text { - padding: 0 2px 2px; - - color: var(--rcx-color-font-pure-white, #ffffff); +/* FLEX-TAB and FLEX-TAB views */ +.main-content-flex { + display: flex; + height: 100%; + flex-grow: 1; +} - border-radius: var(--border-radius); - background-color: var(--rcx-color-badge-background-level-4, #f5455c); +.no-scroll { + overflow: hidden !important; } -@keyframes zoomIn { - 0% { - transform: scale3d(0.9, 0.9, 0.9); +code { + margin: 5px 0; + padding: 0.5em; + text-align: left; + vertical-align: middle; + white-space: pre-wrap; + word-wrap: break-word; + border-width: 1px; + border-radius: var(--border-radius); + font-family: Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + font-size: 13px; + font-weight: 600; + direction: ltr; + unicode-bidi: embed; - opacity: 0; + &.inline { + display: inline; + padding: 0.05rem 0.2rem; + line-height: 1.25rem; } - 50% { - opacity: 1; + &.hljs { + overflow-y: hidden; } } -.rc-old .touch .footer { - padding-right: 10px; - padding-left: 10px; +pre { + display: inline-block; + width: 100%; } -.rc-old .alert-icon { +blockquote { + position: relative; display: block; + clear: both; + min-height: 20px; + padding-left: 10px; + + &::after { + display: table; + clear: both; + content: ''; + } - margin-bottom: 20px; + &::before { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 2px; + content: ' '; + } - font-size: 80px; + &:first-child::before { + border-radius: 2px 2px 0 0; + } + + &:last-child::before { + border-radius: 0 0 2px 2px; + } } -.rc-old .inline-video { - width: 100%; - max-width: 480px; - height: auto; - max-height: 270px; +.new-room-highlight a { + animation: highlight 6s infinite; } -.rc-old .attention-message { - padding-top: 50px; +.page-settings { + & .settings-file-preview { + display: flex; + align-items: center; - font-size: 24px; + & input[type='file'] { + position: absolute !important; + z-index: 10000; + top: 0; + left: 0; + width: 100%; + height: 100%; + cursor: pointer; + opacity: 0; - & i { - display: block; + & * { + cursor: pointer; + } + } + + & .preview { + overflow: hidden; + width: 100px; + height: 40px; + margin-right: 0.75rem; + border-width: var(--input-border-width); + border-color: var(--input-border-color); + border-radius: var(--input-border-radius); + background-repeat: no-repeat; + background-position: center center; + background-size: contain; - margin-bottom: 20px; + &.no-file { + display: flex; + align-items: center; + justify-content: center; + } + } + } +} - font-size: 40px; +.room-not-found { + display: flex; + flex-direction: column; + font-size: 30px; + align-items: center; + justify-content: center; + + & div { + text-align: center; + line-height: 40px; } - & span { - display: block; + & i { + padding-bottom: 30px; + font-size: 100px; } } -.rc-old .load-more { +.container-bars { position: relative; + z-index: 2; + display: none; + visibility: hidden; + overflow: hidden; + flex-direction: column; + margin: 8px 10px 0; + transition: transform 0.4s ease, visibility 0.3s ease, opacity 0.3s ease; + transform: translateY(-10px); + opacity: 0; + border-radius: var(--border-radius); + box-shadow: 0 1px 1px 0 rgb(0 0 0 / 20%), 0 2px 10px 0 rgb(0 0 0 / 16%); + font-size: 1em; + font-weight: bold; - height: 2rem; -} + &.show { + display: flex; + visibility: visible; + transform: translateY(0); + opacity: 1; + } -.flex-tab.discussions > .flex-tab__content { - padding: 0; -} + & > div { + padding: 0 10px; + line-height: 28px; + } -.rc-old .flex-tab { - &__content { + & .upload-progress { + position: relative; display: flex; - flex-direction: column; + height: 28px; - height: 100%; - } + & .upload-progress-progress { + position: absolute; + z-index: 1; + left: 0; + width: 0%; + height: 100%; + transition: width, 1s, ease-out; + } - &__header { - flex: 0 0 auto; + & .upload-progress-text { + z-index: 2; + right: 0; + left: 0; + overflow: hidden; + width: 50%; + height: 100%; + padding: 0 10px; + white-space: nowrap; + text-overflow: ellipsis; + flex-grow: 1; - padding-bottom: 10px; - } + & > a { + float: right; + cursor: pointer; + text-transform: uppercase; + } + } - &__result { - overflow-x: hidden; - overflow-y: auto; - flex: 1 1 auto; + & .upload-progress-close { + position: relative; + z-index: 3; + float: right; + height: 100%; + text-transform: uppercase; + font-weight: bold; + } } +} - & .no-results { - text-align: center; - } +.flex-tab-main-content { + position: relative; + z-index: 1; + overflow: auto; + flex-grow: 1; } -.rc-old .code-colors { +.code-colors { color: var(--rcx-color-font-default, #1f2329); border-color: var(--rcx-color-stroke-extra-light, #ebecef); background-color: var(--rcx-color-surface-neutral, #e4e7ea); } -.rc-old .powered-by { - margin-top: 1em; -} - -.rc-old .code-error-box { - & .title { - margin-top: 5px; - padding: 5px; - - font-size: 16px; - font-weight: bold; - } - - & .script-error { - padding: 6px; - - border-left: 3px solid; - - font-size: 12px; - font-weight: bold; - } -} - -.rc-old .code-mirror-box { - & .CodeMirror { +.code-mirror-box { + & .CodeMirror { /* stylelint-disable-line */ border-width: var(--input-border-width); border-color: var(--input-border-color); border-radius: var(--input-border-radius); } &.code-mirror-box-fullscreen { - & .CodeMirror { + & .CodeMirror { /* stylelint-disable-line */ display: flex; flex-direction: column; flex-grow: 1; - & .CodeMirror-scroll { + & .CodeMirror-scroll { /* stylelint-disable-line */ flex-grow: 1; } } } } -.rc-old .collapse-switch { - cursor: pointer; -} - -.toggle-hidden { - cursor: pointer; - - font-style: italic; -} - -.rc-old .form-inline { - & input, - & select { - display: inline-block; - - width: auto; - - vertical-align: middle; - } - - & label { - display: inline-block; - - max-width: 100%; - } - - & .form-group { - display: inline-block; - - vertical-align: middle; - } -} - -.rc-old .embedded-view { +.embedded-view { & .messages-container { border-width: 0; @@ -1183,52 +368,20 @@ } } -.rc-old .user-info.deactivated { - text-decoration: line-through; - - opacity: 0.8; -} - -/* MEDIA QUERIES */ - -@media (width <= 1100px) { - .rc-old .flex-tab-container.opened .flex-tab { - position: absolute; - z-index: 100; - - right: 40px; - - height: 100%; - } -} - -@media (max-width: 767px) { - .rc-old.main-content { +@media (width <= 767px) { + .main-content { transition: right 0.25s cubic-bezier(0.5, 0, 0.1, 1), transform 0.1s linear; will-change: transform; } } -@media (height <= 480px) { - .rc-old .oauth-login { - margin-bottom: 6px; - } -} - -@media (width <= 440px) { - .rc-old .flex-tab-container.opened .flex-tab { - left: 0; - - width: auto; - } - - .flex-tab-container.opened { - left: 0; +@keyframes zoom-in { + 0% { + transform: scale3d(0.9, 0.9, 0.9); + opacity: 0; } -} -@media (height <= 400px) { - .upload-preview .upload-preview-file { - height: 100px; + 50% { + opacity: 1; } } diff --git a/apps/meteor/app/theme/client/imports/general/forms.css b/apps/meteor/app/theme/client/imports/general/forms.css deleted file mode 100644 index e02c81b5b089..000000000000 --- a/apps/meteor/app/theme/client/imports/general/forms.css +++ /dev/null @@ -1,204 +0,0 @@ -.input { - &.checkbox.toggle { - position: relative; - - min-height: 20px; - - & input { - position: absolute; - z-index: -1; - top: 0; - left: 0; - - width: 0; - height: 0; - - opacity: 0; - outline: 0; - - &:checked + label::after { - left: 25px; - } - } - - & label { - display: block; - - min-height: 20px; - - cursor: pointer; - user-select: none; - vertical-align: top; - - &::before { - position: absolute; - z-index: 0; - top: 0; - left: 0; - - display: block; - - width: 40px; - height: 16px; - - content: ''; - transition: background-color 0.2s ease-out; - - border-radius: 50px; - box-shadow: none; - } - - &::after { - position: absolute; - z-index: 1; - top: 1px; - left: 1px; - - width: 14px; - height: 14px; - - content: ''; - transition: left 0.2s ease-out; - - border-radius: 50%; - } - } - } -} - -.rc-form-group { - margin-bottom: var(--gap-between-elements); - - &--small { - margin-bottom: var(--default-small-padding); - } - - &--inline { - display: flex; - flex-direction: row; - - margin-right: -0.5rem; - - margin-left: -0.5rem; - - white-space: nowrap; - align-items: flex-end; - } - - & > .rc-form-item-inline { - flex: 1 1 auto; - - margin-right: 0.5rem; - - margin-left: 0.5rem; - - &--full { - flex: 0 0 100%; - } - } -} - -.rc-form-label { - display: block; - - margin-bottom: var(--label-margin-bottom); - - cursor: pointer; - - color: #2d343d; - - font-size: var(--input-font-size); -} - -.rc-form-fieldset { - padding: var(--default-padding); - - border-width: var(--input-border-width); - border-color: var(--input-border-color); - border-radius: var(--input-border-radius); - background-color: transparent; - - &--error { - color: var(--input-error-color); - border-color: var(--input-error-color); - } -} - -.rc-form-legend { - margin: 0 -5px; - - padding: 0 5px; - - color: #2d343d; - - font-size: 1rem; - font-weight: 500; -} - -.rc-button-group { - display: flex; - - margin: 0 calc(var(--default-small-padding) / -2); - - & .rc-button { - margin: 0 calc(var(--default-small-padding) / 2); - } -} - -.rc-grid { - display: flex; - align-items: flex-start; - flex-flow: row wrap; -} - -.rc-w50 { - flex: 0 0 50%; - - &.padded { - &:nth-child(odd) { - padding-right: 15px; - } - - &:nth-child(even) { - padding-left: 15px; - } - } -} - -@media (width <= 500px) { - .rc-grid { - display: flex; - flex-direction: column; - align-items: flex-start; - } - - .rc-w50 { - flex: 0 0 100%; - - &.padded { - &:nth-child(odd) { - padding-right: 0; - } - - &:nth-child(even) { - padding-left: 0; - } - } - } -} - -@keyframes spin { - 0% { - transform: rotate(0deg); - } - - 100% { - transform: rotate(360deg); - } -} - -@keyframes ellipsis { - to { - width: 1.25em; - } -} diff --git a/apps/meteor/app/theme/client/imports/general/react-root.css b/apps/meteor/app/theme/client/imports/general/react-root.css index 8930a8bf05ed..4cca0ea5d52c 100644 --- a/apps/meteor/app/theme/client/imports/general/react-root.css +++ b/apps/meteor/app/theme/client/imports/general/react-root.css @@ -1,13 +1,10 @@ #react-root { position: relative; - display: flex; overflow: visible; flex-direction: column; - width: 100vw; height: 100vh; padding: 0; - block-size: -webkit-fill-available; } diff --git a/apps/meteor/app/theme/client/imports/general/reset.css b/apps/meteor/app/theme/client/imports/general/reset.css index 425d9ff98319..3ab7367d23f2 100644 --- a/apps/meteor/app/theme/client/imports/general/reset.css +++ b/apps/meteor/app/theme/client/imports/general/reset.css @@ -87,9 +87,7 @@ audio, video { margin: 0; padding: 0; - vertical-align: baseline; - border: 0 solid; &::after, @@ -134,6 +132,5 @@ q::after { table { border-spacing: 0; - border-collapse: collapse; } 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 b054e601451b..9fe426b3df07 100644 --- a/apps/meteor/app/theme/client/imports/general/theme_old.css +++ b/apps/meteor/app/theme/client/imports/general/theme_old.css @@ -4,7 +4,6 @@ &::-webkit-scrollbar { width: 6px; height: 6px; - background: var(--transparent-dark); } @@ -22,15 +21,6 @@ background-color: var(--content-background-color); } -:root { - --rcx-color-surface-light: var(--content-background-color); - --rcx-color-background-light: var(--content-background-color); -} - -.primary-background-color { - background-color: var(--primary-background-color); -} - .color-primary-font-color { color: var(--primary-font-color); } @@ -39,127 +29,21 @@ color: var(--primary-action-color); } -.secondary-background-color { - background-color: var(--secondary-background-color); -} - -.border-secondary-background-color { - border-color: var(--secondary-background-color); -} - .secondary-font-color { color: var(--secondary-font-color); } -.border-component-color { - border-color: var(--component-color); -} - .background-component-color { background-color: var(--component-color); } -.color-component-color { - color: var(--component-color); -} - -.filter-item { - &:hover { - border-color: var(--info-font-color); - } - - &.active { - border-color: var(--primary-background-color); - } -} - -input:-webkit-autofill { - color: var(--primary-font-color) !important; - background-color: transparent !important; +.upload-progress-progress { + background-color: var(--success-background); } input, select, textarea { - color: var(--primary-font-color); border-style: solid; - border-color: var(--input-border-color); background-color: transparent; - - &::placeholder { - color: var(--input-placeholder-color); - } - - &[disabled]:not(.rcx-box--full) { - background-color: var(--button-disabled-background); - } -} - -.disabled label, -[disabled] label { - color: var(--input-placeholder-color); -} - -.-autocomplete-container { - background-color: var(--popup-list-background); -} - -.-autocomplete-item.selected { - background-color: var(--popup-list-selected-background); -} - -.rc-old input[type='button'], -.rc-old input[type='submit'] { - color: var(--button-secondary-text-color); - border-color: var(--button-secondary-background); - background: var(--button-secondary-background); -} - -.flex-tab { - a i, - a[class^='icon-'] { - color: var(--primary-font-color); - - &:hover { - opacity: 0.6; - } - } -} - -.error { - border-color: var(--error-color); -} - -.upload-progress-progress { - background-color: var(--success-background); -} - -.popup-user-status-online, -.status-online::after { - background-color: var(--rc-status-online); -} - -label.required::after { - color: var(--error-color); -} - -.main-content, -.flex-tab { - .loading-animation > .bounce { - background-color: var(--primary-font-color); - } -} - -.loading-animation.loading-animation--primary > .bounce { - background-color: var(--primary-font-color); -} - -@keyframes blink { - from { - color: var(--selection-color); - } - - to { - opacity: inherit; - } } diff --git a/apps/meteor/app/theme/client/imports/general/variables.css b/apps/meteor/app/theme/client/imports/general/variables.css index e8e06ac68f42..60f2a558be80 100644 --- a/apps/meteor/app/theme/client/imports/general/variables.css +++ b/apps/meteor/app/theme/client/imports/general/variables.css @@ -2,205 +2,45 @@ /* * Color palette */ - --color-dark-blue: #175cc4; - --color-blue: #1d74f5; - --color-light-blue: #4eb2f5; - --color-lighter-blue: #e8f2ff; - --color-purple: #861da8; --color-red: #f5455c; - --color-dark-red: #e0364d; - --color-orange: #f38c39; --color-yellow: #ffd21f; - --color-dark-yellow: #f6c502; --color-green: #2de0a5; /* * General Colors */ - --color-darkest: #1f2329; - --color-dark: #2f343d; - --color-dark-medium: #414852; - --color-dark-light: #6c727a; --color-gray: #9ea2a8; --color-gray-medium: #cbced1; - --color-gray-light: #e1e5e8; --color-gray-lightest: #f2f3f5; - --color-black: #000000; - --color-white: #ffffff; - /* #region colors Colors */ + /* Colors */ --rc-color-error: var(--color-red); - --rc-color-error-light: #e1364c; --rc-color-alert: var(--color-yellow); - --rc-color-alert-light: var(--color-dark-yellow); --rc-color-success: var(--color-green); - --rc-color-success-light: #25d198; - --rc-color-button-primary: var(--color-blue); - --rc-color-button-primary-light: var(--color-dark-blue); - --rc-color-alert-message-primary: var(--color-blue); - --rc-color-alert-message-primary-background: #f1f6ff; - --rc-color-alert-message-secondary: #7ca52b; - --rc-color-alert-message-secondary-background: #fafff1; - --rc-color-alert-message-warning: #d52d24; - --rc-color-alert-message-warning-background: #fff3f3; - --rc-color-primary: var(--color-dark); - --rc-color-primary-background: var(--color-dark); - --rc-color-primary-darkest: var(--color-darkest); - --rc-color-primary-dark: var(--color-dark-medium); --rc-color-primary-light: var(--color-gray); - --rc-color-primary-light-medium: var(--color-gray-medium); - --rc-color-primary-lightest: var(--color-gray-lightest); - --rc-color-content: var(--color-white); - --rc-color-link-active: var(--rc-color-button-primary); - --rc-color-announcement-text: #095ad2; - --rc-color-announcement-background: #d1ebfe; - --rc-color-announcement-text-hover: #01336b; - --rc-color-announcement-background-hover: #76b7fc; - /* #endregion */ - - /* #region colors Old Colors */ + /* Old Colors */ --content-background-color: #ffffff; - --primary-background-color: #04436a; --primary-font-color: #444444; --primary-action-color: #1d74f5; - --secondary-background-color: #f4f4f4; --secondary-font-color: #a0a0a0; - --secondary-action-color: #dddddd; --component-color: #f2f3f5; - --pending-color: #fcb316; - --error-color: #bc2031; - --success-color: #2de0a5; - --selection-color: #02acec; - --attention-color: #9c27b0; - --transparent-darker: rgba(0, 0, 0, 0.5); - - /* #endregion */ - - /* #region less-colors Old Colors (minor) */ - --tertiary-font-color: var(--transparent-lightest); - --link-font-color: var(--primary-action-color); - --info-font-color: var(--secondary-font-color); - --custom-scrollbar-color: var(--transparent-darker); - - /* #endregion */ - /* #region fonts Fonts */ - --body-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Meiryo UI', Arial, sans-serif; - - /* #endregion */ + /* Fonts */ + --body-font-family: -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, oxygen, ubuntu, cantarell, 'Helvetica Neue', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Meiryo UI', arial, sans-serif; /* * General */ - --text-size: 0.875rem; - --header-min-height: 60px; - --toolbar-height: 55px; - --footer-min-height: 70px; - --rooms-box-width: 280px; - --flex-tab-width: 380px; - --flex-tab-webrtc-width: 400px; - --flex-tab-webrtc-2-width: 850px; - --border: 1px; - --border-radius: 4px; --rc-status-online: var(--rc-color-success); --rc-status-away: var(--rc-color-alert); --rc-status-busy: var(--rc-color-error); --rc-status-invisible: var(--color-gray-medium); - --rc-status-offline: var(--transparent-darker); - --rc-status-invisible-sidebar: var(--rc-color-primary-darkest); - --default-padding: 1.5rem; - --default-small-padding: 1rem; - --status-bullet-size: 10px; - --status-bullet-radius: 50%; - --account-username-weight: 700; - --status-name-weight: 400; - --default-font-weight-header: 500; /* * General Typography */ - --text-default-size: 1rem; - --text-default-weight: 500; --text-small-size: 0.875rem; - --text-small-weight: 500; - --text-heading-size: 1.375rem; - --text-heading-weight: 700; - --text-label-size: 075rem; - --text-label-weight: 600; - --text-tiny-size: 075rem; - --text-tiny-weight: 400; - --text-micro-size: 0.625rem; - --text-micro-weight: 700; - - /* - * Forms - */ - --gap-between-elements: 2.5rem; - --label-margin-bottom: 1rem; - - /* - * Forms - Button - */ - --button-square-size: 36px; - --button-padding: 0.782rem; - --button-padding-small: 0 0.5rem; - --button-height-small: 28px; - --button-text-size-small: 13px; - --button-text-size: var(--input-font-size); - --button-border-width: var(--border); - --button-border-radius: var(--border-radius); - --button-disabled-background: var(--color-gray-light); - --button-disabled-text-color: var(--color-white); - --button-primary-background: var(--rc-color-button-primary); - --button-primary-text-color: var(--color-white); - --button-cancel-color: var(--rc-color-error); - --button-secondary-background: var(--color-gray-medium); - --button-secondary-text-color: var(--color-dark-medium); - - /* - * Forms - Input - */ - --input-font-size: 0.875rem; - --input-title-text-size: var(--input-font-size); - --input-title-color: var(--rcx-color-font-default, var(--rcx-color-neutral-800, #2f343d)); - --input-text-color: var(--rcx-color-font-default, var(--rcx-color-neutral-800, #2f343d)); - --input-placeholder-color: var(--rc-input-colors-placeholder-color, var(--rcx-color-font-annotation, var(--rcx-color-neutral-600, #9ea2a8))); - --input-icon-color: var(--rcx-color-font-default, var(--rcx-color-neutral-800, var(--color-dark))); - --input-border-color: var(--rcx-input-colors-border-color, var(--rcx-color-stroke-light, var(--rcx-color-neutral-500, var(--color-gray-light)))); - --input-border-width: var(--border); - --input-border-radius: var(--border-radius); - --input-description-text-color: var(--rcx-color-font-annotation, var(--rcx-color-neutral-600, var(--color-gray))); - --input-description-text-size: var(--input-font-size); - --input-error-color: var(--rc-color-error); - - /* - * Forms - popup list - */ - --popup-list-border-radius: var(--border-radius); - --popup-list-background: var(--color-white); - --popup-list-background-hover: var(--color-gray-lightest); - --popup-list-selected-background: var(--color-gray-lightest); - --popup-list-name-color: #2d343d; - --popup-list-name-size: 1rem; - - /* - * Forms - tags - */ - --tags-border-width: var(--border); - --tags-border-radius: var(--border-radius); - --tags-border-color: var(--color-gray-light); - --tags-text-color: var(--rc-color-primary); - --tags-background: #f2f3f5; - --tags-avatar-size: 20px; - - /* - * Forms - select avatar - */ - --select-avatar-size: 48px; - --select-avatar-preview-size: 150px; - --select-avatar-upload-background: var(--color-gray-light); - --select-avatar-upload-color: #2d343d; /* * Sidebar @@ -208,60 +48,8 @@ --sidebar-width: 17.5rem; --sidebar-md-width: 20rem; --sidebar-lg-width: 21rem; - --sidebar-small-width: 90%; - --sidebar-background-hover: var(--rc-color-primary-dark); - --sidebar-background-light: var(--rc-color-primary-lightest); - --sidebar-background-light-hover: var(--rc-color-primary-light); - --sidebar-background-light-active: var(--rc-color-primary-light-medium); --sidebar-default-padding: 24px; --sidebar-small-default-padding: 16px; - --sidebar-extra-small-default-padding: 12px; - --sidebar-footer-height: 48px; - --sidebar-small-header-padding: var(--sidebar-small-default-padding); - - /* - * Sidebar flex - */ - --sidebar-flex-search-background: var(--color-white); - --sidebar-flex-search-placeholder-color: var(--color-gray); - - /* - * Sidebar Account - */ - --sidebar-account-thumb-size: 23px; - --sidebar-small-account-thumb-size: 40px; - --sidebar-account-status-bullet-size: 10px; - --sidebar-small-account-status-bullet-size: 8px; - --sidebar-account-status-bullet-radius: 50%; - --sidebar-account-username-size: 1rem; - --sidebar-account-username-weight: 700; - --sidebar-small-account-username-weight: 400; - --sidebar-account-username-color: var(--color-white); - --sidebar-account-username-color-darker: var(--color-dark); - --sidebar-account-status-font-size: 0.875rem; - --sidebar-account-status-color: var(--color-gray); - - /* - * Sidebar Item - */ - --sidebar-item-radius: 2px; - --sidebar-item-height: 24px; - --sidebar-item-height-medium: 34px; - --sidebar-item-height-extended: 52px; - --sidebar-item-thumb-size: 18px; - --sidebar-item-thumb-size-medium: 27px; - --sidebar-item-thumb-size-extended: 36px; - --sidebar-item-text-color: var(--rc-color-primary-light); - --sidebar-item-background: inherit; - --sidebar-item-hover-background: var(--rc-color-primary-darkest); - --sidebar-item-active-background: var(--rc-color-primary-dark); - --sidebar-item-active-color: var(--sidebar-item-text-color); - --sidebar-item-unread-color: var(--rc-color-content); - --sidebar-item-unread-font-weight: 600; - --sidebar-item-popup-background: var(--rc-color-primary-dark); - --sidebar-item-user-status-size: 6px; - --sidebar-item-user-status-radius: 50%; - --sidebar-item-text-size: 0.875rem; /* * Rooms list @@ -270,89 +58,17 @@ --rooms-list-title-text-size: 0.75rem; --rooms-list-empty-text-color: var(--color-gray); --rooms-list-empty-text-size: 0.75rem; - --rooms-list-padding: var(--sidebar-default-padding); - --rooms-list-small-padding: var(--sidebar-small-default-padding); - - /* - * Chip - */ - --chip-background: #dddddd; - - /* - * Avatar - */ - --avatar-radius: var(--border-radius); - --avatar-initials-text-size: 22px; - --avatar-initials-text-weight: 700; - - /* - * Badge - */ - --badge-text-color: var(--color-white); - --badge-radius: 12px; - --badge-text-size: 0.75rem; - --badge-background: var(--rc-color-primary-dark); - --badge-unread-background: var(--color-blue); - --badge-user-mentions-background: var(--color-red); - --badge-group-mentions-background: var(--color-orange); - - /* - * Mention link - */ - --mention-link-radius: 4px; - --mention-link-background: #fff6d6; - --mention-link-text-color: #b68d00; - --mention-link-me-background: #ffe9ec; - --mention-link-me-text-color: var(--color-red); - --mention-link-group-background: #fde8d7; - --mention-link-group-text-color: var(--color-orange); /* * Message box */ --message-box-text-size: var(--input-font-size); - --message-box-placeholder-color: var(--rc-input-colors-placeholder-color, var(--rcx-color-font-annotation, var(--rcx-color-neutral-600, #9ea2a8))); - --message-box-markdown-color: var(--rcx-color-font-annotation, var(--rcx-color-neutral-600, var(--color-gray))); - --message-box-markdown-hover-color: var(--rcx-color-font-default, var(--rcx-color-neutral-800, var(--color-dark))); - --message-box-user-activity-color: var(--rcx-color-font-annotation, var(--rcx-color-neutral-600, var(--color-gray))); - --message-box-user-activity-text-size: 0.75rem; - --message-box-user-activity-user-color: var(--rcx-color-font-default, var(--rcx-color-neutral-800, var(--color-dark))); - --message-box-container-border-color: var(--rcx-input-colors-border-color, var(--rcx-color-stroke-light, var(--rcx-color-neutral-500, var(--color-gray-medium)))); - --message-box-container-border-width: var(--border); - --message-box-container-border-radius: var(--border-radius); - --message-box-editing-color: var(--rcx-color-status-background-warning-2, #fff6d6); - --message-box-popover-title-text-color: var(--rcx-color-font-annotation, var(--rcx-color-neutral-600, var(--color-gray))); - --message-box-popover-title-text-size: 0.75rem; - --message-box-color: var(--rcx-color-font-default, var(--rcx-color-neutral-800, #2f343d)); /* * Flex nav */ --flex-nav-background: var(--color-gray-lightest); - /* - * Popover - */ - --popover-padding: 1rem; - --popover-radius: var(--border-radius); - --popover-background: var(--color-white); - --popover-column-min-width: 130px; - --popover-column-padding: 1rem; - --popover-title-color: var(--color-dark); - --popover-title-text-size: 0.75rem; - --popover-item-color: var(--color-dark); - --popover-item-text-size: 0.875rem; - --popover-divider-height: 2px; - --popover-divider-color: var(--color-gray-light); - - /* - * Tooltip - */ - --tooltip-background: var(--color-darkest); - --tooltip-text-color: var(--color-white); - --tooltip-text-size: 0.75rem; - --tooltip-radius: var(--border-radius); - /* * Loading */ @@ -362,10 +78,8 @@ --loading-bounce-color-dark: var(--rcx-color-font-default, #e4e7ea); } -.rcx-sidebar--main { - --sidebar-background: var(--rcx-color-surface-tint, #262931); - --sidebar-item-text-color: var(--rcx-color-font-default, #9ea2a8); - --sidebar-border-color: var(--rcx-color-stroke-extra-light, #2f343d); +.rcx-sidebar { + &--main { + --sidebar-background: var(--rcx-color-surface-tint, #262931); + } } - -/* #endregion */ diff --git a/apps/meteor/app/theme/client/main.css b/apps/meteor/app/theme/client/main.css index 33b7a8f0d290..7fc8ddbfe643 100644 --- a/apps/meteor/app/theme/client/main.css +++ b/apps/meteor/app/theme/client/main.css @@ -1,27 +1,19 @@ /* General */ -@import 'imports/general/variables.css'; -@import 'imports/general/reset.css'; -@import 'imports/general/base_old.css'; -@import 'imports/general/base.css'; -@import 'imports/general/react-root.css'; - -/* Forms */ -@import 'imports/general/forms.css'; -@import 'imports/forms/button.css'; -@import 'imports/forms/input.css'; +@import url('imports/general/variables.css'); +@import url('imports/general/reset.css'); +@import url('imports/general/base_old.css'); +@import url('imports/general/base.css'); +@import url('imports/general/react-root.css'); /* Main */ -@import 'imports/components/flex-nav.css'; -@import 'imports/components/main-content.css'; -@import 'imports/components/message-box.css'; -@import 'imports/components/modal.css'; -@import 'imports/components/messages.css'; -@import 'imports/components/emoji.css'; -@import 'imports/components/loading.css'; +@import url('imports/components/flex-nav.css'); +@import url('imports/components/main-content.css'); +@import url('imports/components/message-box.css'); +@import url('imports/components/emoji.css'); +@import url('imports/components/loading.css'); /* Legacy theming */ -@import 'imports/general/theme_old.css'; -@import './vendor/fontello/css/fontello.css'; -@import './rocketchat.font.css'; -@import './mentionLink.css'; -@import '../../../node_modules/@rocket.chat/fuselage/dist/fuselage.css'; +@import url('imports/general/theme_old.css'); +@import url('./vendor/fontello/css/fontello.css'); +@import url('./rocketchat.font.css'); +@import url('../../../node_modules/@rocket.chat/fuselage/dist/fuselage.css'); diff --git a/apps/meteor/app/theme/client/mentionLink.css b/apps/meteor/app/theme/client/mentionLink.css deleted file mode 100644 index 5deed00646c9..000000000000 --- a/apps/meteor/app/theme/client/mentionLink.css +++ /dev/null @@ -1,37 +0,0 @@ -.mention-link { - padding: 0 2px 2px; - - cursor: pointer; - - transition: opacity 0.3s, background-color 0.3s, color 0.3s; - - color: var(--mention-link-text-color); - - border-radius: var(--mention-link-radius); - - background-color: var(--mention-link-background); - - font-weight: 700; - - &:hover { - opacity: 0.6; - } - - &--me { - color: var(--mention-link-me-text-color); - background-color: var(--mention-link-me-background); - - &:hover { - color: var(--mention-link-me-text-color); - } - } - - &--group { - color: var(--mention-link-group-text-color); - background-color: var(--mention-link-group-background); - - &:hover { - color: var(--mention-link-group-text-color); - } - } -} diff --git a/apps/meteor/app/theme/client/rocketchat.font.css b/apps/meteor/app/theme/client/rocketchat.font.css index 571c679fa697..4db1230558f5 100644 --- a/apps/meteor/app/theme/client/rocketchat.font.css +++ b/apps/meteor/app/theme/client/rocketchat.font.css @@ -1,9 +1,8 @@ @font-face { - font-family: 'RocketChat'; + font-family: RocketChat; font-weight: 400; font-style: normal; font-display: block; - src: url('/fonts/rocketchat.eot'); src: url('/fonts/rocketchat.eot?#iefix') format('embedded-opentype'), diff --git a/apps/meteor/app/threads/client/lib/normalizeThreadTitle.ts b/apps/meteor/app/threads/client/lib/normalizeThreadTitle.ts index 70a2a6008e56..c3d10b531b6b 100644 --- a/apps/meteor/app/threads/client/lib/normalizeThreadTitle.ts +++ b/apps/meteor/app/threads/client/lib/normalizeThreadTitle.ts @@ -2,7 +2,7 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { escapeHTML } from '@rocket.chat/string-helpers'; import { Meteor } from 'meteor/meteor'; -import { emojiParser } from '../../../emoji/client/emojiParser.js'; +import { emojiParser } from '../../../emoji/client/emojiParser'; import { filterMarkdown } from '../../../markdown/lib/markdown'; import { MentionsParser } from '../../../mentions/lib/MentionsParser'; import { Users } from '../../../models/client'; @@ -26,7 +26,7 @@ export function normalizeThreadTitle({ ...message }: Readonly) { userTemplate: ({ label }) => ` ${label} `, roomTemplate: ({ prefix, mention }) => `${prefix} ${mention} `, }); - const { html } = emojiParser({ html: filteredMessage }); + const html = emojiParser(filteredMessage); return instance.parse({ ...message, msg: filteredMessage, html }).html; } diff --git a/apps/meteor/app/threads/client/messageAction/replyInThread.ts b/apps/meteor/app/threads/client/messageAction/replyInThread.ts index 01d007e0d953..9d0ab8566e04 100644 --- a/apps/meteor/app/threads/client/messageAction/replyInThread.ts +++ b/apps/meteor/app/threads/client/messageAction/replyInThread.ts @@ -2,7 +2,6 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; -import { messageArgs } from '../../../../client/lib/utils/messageArgs'; import { router } from '../../../../client/providers/RouterProvider'; import { settings } from '../../../settings/client'; import { MessageAction } from '../../../ui-utils/client'; @@ -17,8 +16,7 @@ Meteor.startup(() => { icon: 'thread', label: 'Reply_in_thread', context: ['message', 'message-mobile', 'federated', 'videoconf'], - action(e, props) { - const { message = messageArgs(this).msg } = props; + action(e, { message }) { e?.stopPropagation(); router.navigate({ name: router.getRouteName()!, diff --git a/apps/meteor/app/threads/client/threads.css b/apps/meteor/app/threads/client/threads.css index a9a9ade825d4..9ec4365bd68b 100644 --- a/apps/meteor/app/threads/client/threads.css +++ b/apps/meteor/app/threads/client/threads.css @@ -1,20 +1,6 @@ -.thread-empty { - padding: calc(2 * var(--default-padding)); -} - .thread-list { - overflow-x: hidden; - overflow-y: auto; - + overflow: hidden auto; word-wrap: break-word; flex-grow: 1; flex-shrink: 1; } - -.contextual-bar__content.thread, -.contextual-bar__content.discussions, -.contextual-bar__content.channel-settings, -.contextual-bar__content.keyboard-shortcut-list, -.contextual-bar__content.threads { - padding: 0; -} diff --git a/apps/meteor/app/ui-cached-collection/client/index.ts b/apps/meteor/app/ui-cached-collection/client/index.ts deleted file mode 100644 index 45776cc4d22c..000000000000 --- a/apps/meteor/app/ui-cached-collection/client/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { CachedCollection } from './models/CachedCollection'; -export { CachedCollectionManager } from './models/CachedCollectionManager'; diff --git a/apps/meteor/app/ui-cached-collection/client/models/CachedCollectionManager.ts b/apps/meteor/app/ui-cached-collection/client/models/CachedCollectionManager.ts deleted file mode 100644 index b03115105841..000000000000 --- a/apps/meteor/app/ui-cached-collection/client/models/CachedCollectionManager.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Emitter } from '@rocket.chat/emitter'; -import { Accounts } from 'meteor/accounts-base'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import type { CachedCollection } from './CachedCollection'; - -class CachedCollectionManager extends Emitter<{ reconnect: void; login: string | null }> { - private items: CachedCollection[]; - - private _syncEnabled: boolean; - - private logged: boolean; - - private step: number; - - constructor() { - super(); - this.items = []; - this._syncEnabled = false; - this.logged = false; - - const { _unstoreLoginToken } = Accounts; - Accounts._unstoreLoginToken = (...args) => { - _unstoreLoginToken.apply(Accounts, args); - this.clearAllCacheOnLogout(); - }; - - Tracker.autorun(() => { - const [WAITING_FIRST_CONNECTION, WAITING_FIRST_DISCONNECTION, LISTENING_RECONNECTIONS] = [0, 1, 2]; - this.step = this.step || WAITING_FIRST_CONNECTION; - const { connected } = Meteor.status(); - switch (this.step) { - case WAITING_FIRST_CONNECTION: - return !connected || this.step++; - case WAITING_FIRST_DISCONNECTION: - return connected || this.step++; - case LISTENING_RECONNECTIONS: - return connected && this.emit('reconnect'); - } - }); - - Accounts.onLogin(() => { - this.emit('login', Meteor.userId()); - }); - Tracker.autorun(() => { - const uid = Meteor.userId(); - this.logged = uid !== null; - }); - } - - register(cachedCollection: CachedCollection) { - this.items.push(cachedCollection); - } - - clearAllCache() { - for (const item of this.items) { - void item.clearCache(); - } - } - - clearAllCacheOnLogout() { - for (const item of this.items) { - item.clearCacheOnLogout(); - } - } - - set syncEnabled(value) { - check(value, Boolean); - this._syncEnabled = value; - } - - get syncEnabled() { - return this._syncEnabled; - } - - onReconnect(cb: () => void) { - this.on('reconnect', cb); - } - - onLogin(cb: () => void) { - this.on('login', cb); - if (this.logged) { - cb(); - } - } -} - -const instance = new CachedCollectionManager(); - -export { - /** @deprecated */ - instance as CachedCollectionManager, -}; diff --git a/apps/meteor/app/ui-master/server/index.js b/apps/meteor/app/ui-master/server/index.ts similarity index 81% rename from apps/meteor/app/ui-master/server/index.js rename to apps/meteor/app/ui-master/server/index.ts index 2d4f3cc7de56..7e095b80b5f5 100644 --- a/apps/meteor/app/ui-master/server/index.js +++ b/apps/meteor/app/ui-master/server/index.ts @@ -1,13 +1,14 @@ +import type { ISettingColor } from '@rocket.chat/core-typings'; import { Settings } from '@rocket.chat/models'; import { escapeHTML } from '@rocket.chat/string-helpers'; import { Meteor } from 'meteor/meteor'; import { Inject } from 'meteor/meteorhacks:inject-initial'; import { Tracker } from 'meteor/tracker'; +import { applyHeadInjections, headInjections, injectIntoBody, injectIntoHead } from './inject'; import { withDebouncing } from '../../../lib/utils/highOrderFunctions'; import { settings } from '../../settings/server'; import { getURL } from '../../utils/server/getURL'; -import { applyHeadInjections, headInjections, injectIntoBody, injectIntoHead } from './inject'; import './scripts'; @@ -15,11 +16,11 @@ export * from './inject'; Meteor.startup(() => { Tracker.autorun(() => { - const injections = Object.values(headInjections.all()); + const injections = Object.values(headInjections.all()).filter((injection): injection is NonNullable => !!injection); Inject.rawModHtml('headInjections', applyHeadInjections(injections)); }); - settings.watch('Default_Referrer_Policy', (value) => { + settings.watch('Default_Referrer_Policy', (value) => { if (!value) { return injectIntoHead('noreferrer', ''); } @@ -40,7 +41,7 @@ Meteor.startup(() => { ); } - settings.watch('Assets_SvgFavicon_Enable', (value) => { + settings.watch('Assets_SvgFavicon_Enable', (value) => { const standardFavicons = ` `; @@ -56,7 +57,7 @@ Meteor.startup(() => { } }); - settings.watch('theme-color-sidebar-background', (value) => { + settings.watch('theme-color-sidebar-background', (value) => { const escapedValue = escapeHTML(value); injectIntoHead( 'theme-color-sidebar-background', @@ -64,7 +65,7 @@ Meteor.startup(() => { ); }); - settings.watch('Site_Name', (value = 'Rocket.Chat') => { + settings.watch('Site_Name', (value = 'Rocket.Chat') => { const escapedValue = escapeHTML(value); injectIntoHead( 'Site_Name', @@ -74,7 +75,7 @@ Meteor.startup(() => { ); }); - settings.watch('Meta_language', (value = '') => { + settings.watch('Meta_language', (value = '') => { const escapedValue = escapeHTML(value); injectIntoHead( 'Meta_language', @@ -82,27 +83,27 @@ Meteor.startup(() => { ); }); - settings.watch('Meta_robots', (value = '') => { + settings.watch('Meta_robots', (value = '') => { const escapedValue = escapeHTML(value); injectIntoHead('Meta_robots', ``); }); - settings.watch('Meta_msvalidate01', (value = '') => { + settings.watch('Meta_msvalidate01', (value = '') => { const escapedValue = escapeHTML(value); injectIntoHead('Meta_msvalidate01', ``); }); - settings.watch('Meta_google-site-verification', (value = '') => { + settings.watch('Meta_google-site-verification', (value = '') => { const escapedValue = escapeHTML(value); injectIntoHead('Meta_google-site-verification', ``); }); - settings.watch('Meta_fb_app_id', (value = '') => { + settings.watch('Meta_fb_app_id', (value = '') => { const escapedValue = escapeHTML(value); injectIntoHead('Meta_fb_app_id', ``); }); - settings.watch('Meta_custom', (value = '') => { + settings.watch('Meta_custom', (value = '') => { injectIntoHead('Meta_custom', value); }); @@ -127,7 +128,7 @@ const renderDynamicCssList = withDebouncing({ wait: 500 })(async () => { // const variables = RocketChat.models.Settings.findOne({_id:'theme-custom-variables'}, {fields: { value: 1}}); const colors = await Settings.find({ _id: /theme-color-rc/i }, { projection: { value: 1, editor: 1 } }).toArray(); const css = colors - .filter((color) => color && color.value) + .filter((color): color is ISettingColor => !!color?.value) .map(({ _id, value, editor }) => { if (editor === 'expression') { return `--${_id.replace('theme-color-', '')}: var(--${value});`; @@ -138,7 +139,7 @@ const renderDynamicCssList = withDebouncing({ wait: 500 })(async () => { injectIntoBody('dynamic-variables', ``); }); -renderDynamicCssList(); +await renderDynamicCssList(); settings.watchByRegex(/theme-color-rc/i, renderDynamicCssList); @@ -160,4 +161,4 @@ injectIntoBody( `, ); -injectIntoBody('icons', await Assets.getTextAsync('public/icons.svg')); +injectIntoBody('icons', (await Assets.getTextAsync('public/icons.svg')) ?? ''); diff --git a/apps/meteor/app/ui-master/server/inject.ts b/apps/meteor/app/ui-master/server/inject.ts index 1e00a0e47433..47b63db4bb3f 100644 --- a/apps/meteor/app/ui-master/server/inject.ts +++ b/apps/meteor/app/ui-master/server/inject.ts @@ -16,7 +16,7 @@ type Injection = tag: string; }; -export const headInjections = new ReactiveDict(); +export const headInjections = new ReactiveDict>(); const callback: NextHandleFunction = (req, res, next) => { if (req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS') { @@ -32,7 +32,7 @@ const callback: NextHandleFunction = (req, res, next) => { return; } - const injection = headInjections.get(pathname.replace(/^\//, '').split('_')[0]) as Injection | undefined; + const injection = headInjections.get(pathname.replace(/^\//, '').split('_')[0]); if (!injection || typeof injection === 'string') { next(); diff --git a/apps/meteor/app/ui-master/server/scripts.ts b/apps/meteor/app/ui-master/server/scripts.ts index 3e84a6e39c90..85ebaa8ccba6 100644 --- a/apps/meteor/app/ui-master/server/scripts.ts +++ b/apps/meteor/app/ui-master/server/scripts.ts @@ -1,5 +1,5 @@ -import { settings } from '../../settings/server'; import { addScript } from './inject'; +import { settings } from '../../settings/server'; const getContent = (): string => ` diff --git a/apps/meteor/app/ui-message/client/ActionManager.ts b/apps/meteor/app/ui-message/client/ActionManager.ts index 4c892d6d32f2..91d635bee4db 100644 --- a/apps/meteor/app/ui-message/client/ActionManager.ts +++ b/apps/meteor/app/ui-message/client/ActionManager.ts @@ -7,12 +7,12 @@ import { t } from 'i18next'; import type { ContextType } from 'react'; import { lazy } from 'react'; +import { UiKitTriggerTimeoutError } from './UiKitTriggerTimeoutError'; import * as banners from '../../../client/lib/banners'; import { imperativeModal } from '../../../client/lib/imperativeModal'; import { dispatchToastMessage } from '../../../client/lib/toast'; import { exhaustiveCheck } from '../../../lib/utils/exhaustiveCheck'; import { sdk } from '../../utils/client/lib/SDKClient'; -import { UiKitTriggerTimeoutError } from './UiKitTriggerTimeoutError'; const UiKitModal = lazy(() => import('../../../client/views/modal/uikit/UiKitModal')); diff --git a/apps/meteor/app/ui-message/client/findParentMessage.ts b/apps/meteor/app/ui-message/client/findParentMessage.ts index 0a3197c463a6..ca221d79812b 100644 --- a/apps/meteor/app/ui-message/client/findParentMessage.ts +++ b/apps/meteor/app/ui-message/client/findParentMessage.ts @@ -2,7 +2,7 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { callWithErrorHandling } from '../../../client/lib/utils/callWithErrorHandling'; import { withDebouncing } from '../../../lib/utils/highOrderFunctions'; -import { ChatMessage } from '../../models/client'; +import { Messages } from '../../models/client'; export const findParentMessage = (() => { const waiting: string[] = []; @@ -27,7 +27,7 @@ export const findParentMessage = (() => { }; return async (tmid: IMessage['_id']) => { - const message = ChatMessage.findOne({ _id: tmid }); + const message = Messages.findOne({ _id: tmid }); if (message) { return message; diff --git a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts index 741f7959fa90..eab54850f3bf 100644 --- a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts +++ b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts @@ -2,10 +2,10 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { Accounts } from 'meteor/accounts-base'; -import type { ComposerAPI } from '../../../../client/lib/chats/ChatAPI'; -import { withDebouncing } from '../../../../lib/utils/highOrderFunctions'; import type { FormattingButton } from './messageBoxFormatting'; import { formattingButtons } from './messageBoxFormatting'; +import type { ComposerAPI } from '../../../../client/lib/chats/ChatAPI'; +import { withDebouncing } from '../../../../lib/utils/highOrderFunctions'; export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string): ComposerAPI => { const triggerEvent = (input: HTMLTextAreaElement, evt: string): void => { diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts b/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts index 84ca6dcc1035..3c35d0fee301 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts +++ b/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts @@ -1,10 +1,10 @@ import type { Keys as IconName } from '@rocket.chat/icons'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import AddLinkComposerActionModal from './AddLinkComposerActionModal'; import type { ComposerAPI } from '../../../../client/lib/chats/ChatAPI'; import { imperativeModal } from '../../../../client/lib/imperativeModal'; import { settings } from '../../../settings/client'; -import AddLinkComposerActionModal from './AddLinkComposerActionModal'; type FormattingButtonDefault = { label: TranslationKey; condition?: () => boolean }; diff --git a/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts b/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts index 91b848ffefde..5f82e47921f8 100644 --- a/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts @@ -3,15 +3,15 @@ import type { Mongo } from 'meteor/mongo'; import { ReactiveVar } from 'meteor/reactive-var'; import { Tracker } from 'meteor/tracker'; +import { upsertMessage, RoomHistoryManager } from './RoomHistoryManager'; +import { mainReady } from './mainReady'; import { RoomManager } from '../../../../client/lib/RoomManager'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; import { fireGlobalEvent } from '../../../../client/lib/utils/fireGlobalEvent'; import { getConfig } from '../../../../client/lib/utils/getConfig'; import { callbacks } from '../../../../lib/callbacks'; -import { CachedChatRoom, ChatMessage, ChatSubscription, CachedChatSubscription } from '../../../models/client'; +import { CachedChatRoom, Messages, Subscriptions, CachedChatSubscription } from '../../../models/client'; import { sdk } from '../../../utils/client/lib/SDKClient'; -import { upsertMessage, RoomHistoryManager } from './RoomHistoryManager'; -import { mainReady } from './mainReady'; const maxRoomsOpen = parseInt(getConfig('maxRoomsOpen') ?? '5') || 5; @@ -106,8 +106,8 @@ const computation = Tracker.autorun(() => { // } // Do not load command messages into channel if (msg.t !== 'command') { - const subscription = ChatSubscription.findOne({ rid: record.rid }, { reactive: false }); - const isNew = !ChatMessage.findOne({ _id: msg._id, temp: { $ne: true } }); + const subscription = Subscriptions.findOne({ rid: record.rid }, { reactive: false }); + const isNew = !Messages.findOne({ _id: msg._id, temp: { $ne: true } }); await upsertMessage({ msg, subscription }); if (isNew) { @@ -140,10 +140,10 @@ const computation = Tracker.autorun(() => { }); sdk.stream('notify-room', [`${record.rid}/deleteMessage`], (msg) => { - ChatMessage.remove({ _id: msg._id }); + Messages.remove({ _id: msg._id }); // remove thread refenrece from deleted message - ChatMessage.update({ tmid: msg._id }, { $unset: { tmid: 1 } }, { multi: true }); + Messages.update({ tmid: msg._id }, { $unset: { tmid: 1 } }, { multi: true }); }); sdk.stream( @@ -168,19 +168,19 @@ const computation = Tracker.autorun(() => { } if (showDeletedStatus) { - return ChatMessage.update( + return Messages.update( query, { $set: { t: 'rm', msg: '', urls: [], mentions: [], attachments: [], reactions: {} } }, { multi: true }, ); } - return ChatMessage.remove(query); + return Messages.remove(query); }, ); sdk.stream('notify-room', [`${record.rid}/messagesRead`], ({ tmid, until }) => { if (tmid) { - return ChatMessage.update( + return Messages.update( { tmid, unread: true, @@ -189,7 +189,7 @@ const computation = Tracker.autorun(() => { { multi: true }, ); } - ChatMessage.update( + Messages.update( { rid: record.rid, unread: true, diff --git a/apps/meteor/app/ui-utils/client/lib/MessageAction.ts b/apps/meteor/app/ui-utils/client/lib/MessageAction.ts index c1f9590b98ee..0a5483c4acaa 100644 --- a/apps/meteor/app/ui-utils/client/lib/MessageAction.ts +++ b/apps/meteor/app/ui-utils/client/lib/MessageAction.ts @@ -47,7 +47,6 @@ export type MessageActionConfig = { group?: MessageActionGroup | MessageActionGroup[]; context?: MessageActionContext[]; action: ( - this: any, e: Pick | undefined, { message, diff --git a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts index 4e2f0c020a12..bbe8abb515f5 100644 --- a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts @@ -11,7 +11,7 @@ import { onClientMessageReceived } from '../../../../client/lib/onClientMessageR import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling'; import { getConfig } from '../../../../client/lib/utils/getConfig'; import { waitForElement } from '../../../../client/lib/utils/waitForElement'; -import { ChatMessage, ChatSubscription } from '../../../models/client'; +import { Messages, Subscriptions } from '../../../models/client'; import { getUserPreference } from '../../../utils/client'; export async function upsertMessage( @@ -22,7 +22,7 @@ export async function upsertMessage( msg: IMessage & { ignored?: boolean }; subscription?: ISubscription; }, - collection: MinimongoCollection = ChatMessage, + collection: MinimongoCollection = Messages, ) { const userId = msg.u?._id; @@ -42,7 +42,7 @@ export async function upsertMessage( export function upsertMessageBulk( { msgs, subscription }: { msgs: IMessage[]; subscription?: ISubscription }, - collection: MinimongoCollection = ChatMessage, + collection: MinimongoCollection = Messages, ) { const { queries } = collection; collection.queries = []; @@ -135,7 +135,7 @@ class RoomHistoryManagerClass extends Emitter { let ls = undefined; - const subscription = ChatSubscription.findOne({ rid }); + const subscription = Subscriptions.findOne({ rid }); if (subscription) { ({ ls } = subscription); } @@ -214,9 +214,9 @@ class RoomHistoryManagerClass extends Emitter { room.isLoading.set(true); - const lastMessage = ChatMessage.findOne({ rid, _hidden: { $ne: true } }, { sort: { ts: -1 } }); + const lastMessage = Messages.findOne({ rid, _hidden: { $ne: true } }, { sort: { ts: -1 } }); - const subscription = ChatSubscription.findOne({ rid }); + const subscription = Subscriptions.findOne({ rid }); if (lastMessage?.ts) { const { ts } = lastMessage; @@ -264,7 +264,7 @@ class RoomHistoryManagerClass extends Emitter { public async clear(rid: IRoom['_id']) { const room = this.getRoom(rid); - ChatMessage.remove({ rid }); + Messages.remove({ rid }); room.isLoading.set(true); room.hasMore.set(true); room.hasMoreNext.set(false); @@ -277,7 +277,7 @@ class RoomHistoryManagerClass extends Emitter { return; } - const messageAlreadyLoaded = Boolean(ChatMessage.findOne({ _id: message._id, _hidden: { $ne: true } })); + const messageAlreadyLoaded = Boolean(Messages.findOne({ _id: message._id, _hidden: { $ne: true } })); if (messageAlreadyLoaded) { return; @@ -286,7 +286,7 @@ class RoomHistoryManagerClass extends Emitter { const room = this.getRoom(message.rid); void this.clear(message.rid); - const subscription = ChatSubscription.findOne({ rid: message.rid }); + const subscription = Subscriptions.findOne({ rid: message.rid }); const result = await callWithErrorHandling('loadSurroundingMessages', message, defaultLimit); diff --git a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts index 2f2793f7493b..460ac2941e82 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts @@ -3,19 +3,18 @@ import { isE2EEMessage, isRoomFederated } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import moment from 'moment'; +import { MessageAction } from './MessageAction'; import { getPermaLink } from '../../../../client/lib/getPermaLink'; import { imperativeModal } from '../../../../client/lib/imperativeModal'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; import { dispatchToastMessage } from '../../../../client/lib/toast'; -import { messageArgs } from '../../../../client/lib/utils/messageArgs'; import { router } from '../../../../client/providers/RouterProvider'; import ForwardMessageModal from '../../../../client/views/room/modals/ForwardMessageModal/ForwardMessageModal'; import ReactionListModal from '../../../../client/views/room/modals/ReactionListModal'; import ReportMessageModal from '../../../../client/views/room/modals/ReportMessageModal'; import { hasAtLeastOnePermission, hasPermission } from '../../../authorization/client'; -import { ChatRoom, Subscriptions } from '../../../models/client'; +import { Rooms, Subscriptions } from '../../../models/client'; import { t } from '../../../utils/lib/i18n'; -import { MessageAction } from './MessageAction'; const getMainMessageText = (message: IMessage): IMessage => { const newMessage = { ...message }; @@ -32,8 +31,7 @@ Meteor.startup(async () => { context: ['message', 'message-mobile', 'threads', 'federated'], role: 'link', type: 'communication', - action(_, props) { - const { message = messageArgs(this).msg } = props; + action(_, { message }) { roomCoordinator.openRouteLink( 'd', { name: message.u.username }, @@ -53,7 +51,7 @@ Meteor.startup(async () => { // Check if we already have a DM started with the message user (not ourselves) or we can start one if (!!user && user._id !== message.u._id && !hasPermission('create-d')) { - const dmRoom = ChatRoom.findOne({ _id: [user._id, message.u._id].sort().join('') }); + const dmRoom = Rooms.findOne({ _id: [user._id, message.u._id].sort().join('') }); if (!dmRoom || !Subscriptions.findOne({ 'rid': dmRoom._id, 'u._id': user._id })) { return false; } @@ -74,8 +72,7 @@ Meteor.startup(async () => { label: 'Forward_message', context: ['message', 'message-mobile', 'threads'], type: 'communication', - async action(_, props) { - const { message = messageArgs(this).msg } = props; + async action(_, { message }) { const permalink = await getPermaLink(message._id); imperativeModal.open({ component: ForwardMessageModal, @@ -100,9 +97,7 @@ Meteor.startup(async () => { icon: 'quote', label: 'Quote', context: ['message', 'message-mobile', 'threads', 'federated'], - async action(_, props) { - const { message = messageArgs(this).msg, chat, autoTranslateOptions } = props; - + async action(_, { message, chat, autoTranslateOptions }) { if (message && autoTranslateOptions?.autoTranslateEnabled && autoTranslateOptions.showAutoTranslate(message)) { message.msg = message.translations && autoTranslateOptions.autoTranslateLanguage @@ -130,9 +125,8 @@ Meteor.startup(async () => { // classes: 'clipboard', context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], type: 'duplication', - async action(_, props) { + async action(_, { message }) { try { - const { message = messageArgs(this).msg } = props; const permalink = await getPermaLink(message._id); await navigator.clipboard.writeText(permalink); dispatchToastMessage({ type: 'success', message: t('Copied') }); @@ -157,8 +151,7 @@ Meteor.startup(async () => { // classes: 'clipboard', context: ['message', 'message-mobile', 'threads', 'federated'], type: 'duplication', - async action(_, props) { - const { message = messageArgs(this).msg } = props; + async action(_, { message }) { const msgText = getMainMessageText(message).msg; await navigator.clipboard.writeText(msgText); dispatchToastMessage({ type: 'success', message: t('Copied') }); @@ -176,8 +169,7 @@ Meteor.startup(async () => { label: 'Edit', context: ['message', 'message-mobile', 'threads', 'federated'], type: 'management', - async action(_, props) { - const { message = messageArgs(this).msg, chat } = props; + async action(_, { message, chat }) { await chat?.messageEditing.editMessage(message); }, condition({ message, subscription, settings, room, user }) { @@ -220,7 +212,7 @@ Meteor.startup(async () => { context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], color: 'alert', type: 'management', - async action(this: unknown, _, { message = messageArgs(this).msg, chat }) { + async action(_, { message, chat }) { await chat?.flows.requestMessageDeletion(message); }, condition({ message, subscription, room, chat, user }) { @@ -248,7 +240,7 @@ Meteor.startup(async () => { context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], color: 'alert', type: 'management', - action(this: unknown, _, { message = messageArgs(this).msg }) { + action(_, { message }) { imperativeModal.open({ component: ReportMessageModal, props: { @@ -275,7 +267,7 @@ Meteor.startup(async () => { label: 'Reactions', context: ['message', 'message-mobile', 'threads', 'videoconf', 'videoconf-threads'], type: 'interaction', - action(this: unknown, _, { message: { reactions = {} } = messageArgs(this).msg }) { + action(_, { message: { reactions = {} } }) { imperativeModal.open({ component: ReactionListModal, props: { reactions, onClose: imperativeModal.close }, diff --git a/apps/meteor/app/ui-utils/client/lib/messageBox.ts b/apps/meteor/app/ui-utils/client/lib/messageBox.ts index 3418adef1c1c..9dd26f64c8e0 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageBox.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageBox.ts @@ -48,13 +48,16 @@ class MessageBoxActions { get(group?: TranslationKey) { if (!group) { - return [...this.actions.entries()].reduce>((ret, [group, actions]) => { - const filteredActions = actions.filter((action) => !action.condition || action.condition()); - if (filteredActions.length) { - ret[group] = filteredActions; - } - return ret; - }, {} as Record); + return [...this.actions.entries()].reduce>( + (ret, [group, actions]) => { + const filteredActions = actions.filter((action) => !action.condition || action.condition()); + if (filteredActions.length) { + ret[group] = filteredActions; + } + return ret; + }, + {} as Record, + ); } return this.actions.get(group)?.filter((action) => !action.condition || action.condition()); diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index 5d78fd1e1d98..f7fff0b2a2aa 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -2,6 +2,7 @@ import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { isVideoConfMessage } from '@rocket.chat/core-typings'; import type { IActionManager } from '@rocket.chat/ui-contexts'; +import { UserAction } from './UserAction'; import type { ChatAPI, ComposerAPI, DataAPI, UploadsAPI } from '../../../../client/lib/chats/ChatAPI'; import { createDataAPI } from '../../../../client/lib/chats/data'; import { processMessageEditing } from '../../../../client/lib/chats/flows/processMessageEditing'; @@ -18,13 +19,12 @@ import { setHighlightMessage, clearHighlightMessage, } from '../../../../client/views/room/MessageList/providers/messageHighlightSubscription'; -import { UserAction } from './UserAction'; type DeepWritable = T extends (...args: any) => any ? T : { -readonly [P in keyof T]: DeepWritable; - }; + }; export class ChatMessages implements ChatAPI { public uid: string | null; diff --git a/apps/meteor/app/ui/client/lib/KonchatNotification.ts b/apps/meteor/app/ui/client/lib/KonchatNotification.ts index dbffdf2a80a1..ff67250c000b 100644 --- a/apps/meteor/app/ui/client/lib/KonchatNotification.ts +++ b/apps/meteor/app/ui/client/lib/KonchatNotification.ts @@ -10,9 +10,10 @@ import { router } from '../../../../client/providers/RouterProvider'; import { stripTags } from '../../../../lib/utils/stringUtils'; import { CustomSounds } from '../../../custom-sounds/client/lib/CustomSounds'; import { e2e } from '../../../e2e/client'; -import { ChatSubscription } from '../../../models/client'; +import { Subscriptions } from '../../../models/client'; import { getUserPreference } from '../../../utils/client'; import { getUserAvatarURL } from '../../../utils/client/getUserAvatarURL'; +import { getUserNotificationsSoundVolume } from '../../../utils/client/getUserNotificationsSoundVolume'; import { sdk } from '../../../utils/client/lib/SDKClient'; declare global { @@ -176,13 +177,13 @@ class KonchatNotification { const userId = Meteor.userId(); const newMessageNotification = getUserPreference(userId, 'newMessageNotification'); - const audioVolume = getUserPreference(userId, 'notificationsSoundVolume', 100); + const audioVolume = getUserNotificationsSoundVolume(userId); if (!rid) { return; } - const sub = ChatSubscription.findOne({ rid }, { fields: { audioNotificationValue: 1 } }); + const sub = Subscriptions.findOne({ rid }, { fields: { audioNotificationValue: 1 } }); if (!sub || sub.audioNotificationValue === 'none') { return; diff --git a/apps/meteor/app/ui/client/lib/recorderjs/AudioRecorder.ts b/apps/meteor/app/ui/client/lib/recorderjs/AudioRecorder.ts index 85a7853c1ece..d8bd8cbb323c 100644 --- a/apps/meteor/app/ui/client/lib/recorderjs/AudioRecorder.ts +++ b/apps/meteor/app/ui/client/lib/recorderjs/AudioRecorder.ts @@ -1,5 +1,5 @@ -import { settings } from '../../../../settings/client'; import { AudioEncoder } from './AudioEncoder'; +import { settings } from '../../../../settings/client'; export class AudioRecorder { private audioContext: AudioContext | undefined; diff --git a/apps/meteor/app/utils/client/getRoomAvatarURL.ts b/apps/meteor/app/utils/client/getRoomAvatarURL.ts index 061f62107b44..8f7dd9dec533 100644 --- a/apps/meteor/app/utils/client/getRoomAvatarURL.ts +++ b/apps/meteor/app/utils/client/getRoomAvatarURL.ts @@ -1,7 +1,7 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { settings } from '../../settings/client'; import { getAvatarURL } from './getAvatarURL'; +import { settings } from '../../settings/client'; export const getRoomAvatarURL = ({ roomId, cache = '' }: { roomId: IRoom['_id']; cache: IRoom['avatarETag'] }) => { const externalSource = (settings.get('Accounts_RoomAvatarExternalProviderUrl') || '').trim().replace(/\/$/, ''); diff --git a/apps/meteor/app/utils/client/getURL.ts b/apps/meteor/app/utils/client/getURL.ts index 040b6dfa9dc2..42970bc38869 100644 --- a/apps/meteor/app/utils/client/getURL.ts +++ b/apps/meteor/app/utils/client/getURL.ts @@ -4,7 +4,13 @@ import { Info } from '../rocketchat.info'; export const getURL = function ( path: string, // eslint-disable-next-line @typescript-eslint/naming-convention - params: Record = {}, + params: { + cdn?: boolean; + full?: boolean; + cloud?: boolean; + cloud_route?: string; + cloud_params?: Record; + } = {}, cloudDeepLinkUrl?: string, cacheKey?: boolean, ): string { diff --git a/apps/meteor/app/utils/client/getUserNotificationsSoundVolume.tsx b/apps/meteor/app/utils/client/getUserNotificationsSoundVolume.tsx new file mode 100644 index 000000000000..ea89cd51d4e3 --- /dev/null +++ b/apps/meteor/app/utils/client/getUserNotificationsSoundVolume.tsx @@ -0,0 +1,10 @@ +import type { IUser } from '@rocket.chat/core-typings'; + +import { getUserPreference } from './lib/getUserPreference'; + +export const getUserNotificationsSoundVolume = (userId: IUser['_id'] | null | undefined) => { + const masterVolume = getUserPreference(userId, 'masterVolume', 100); + const notificationsSoundVolume = getUserPreference(userId, 'notificationsSoundVolume', 100); + + return (notificationsSoundVolume * masterVolume) / 100; +}; diff --git a/apps/meteor/app/utils/client/lib/RestApiClient.ts b/apps/meteor/app/utils/client/lib/RestApiClient.ts index 53c95ee3e4fa..219c297406bc 100644 --- a/apps/meteor/app/utils/client/lib/RestApiClient.ts +++ b/apps/meteor/app/utils/client/lib/RestApiClient.ts @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/rules-of-hooks */ import { RestClient } from '@rocket.chat/api-client'; import { Accounts } from 'meteor/accounts-base'; diff --git a/apps/meteor/app/utils/lib/i18n.ts b/apps/meteor/app/utils/lib/i18n.ts index b69fe6b30513..57cc34225d8a 100644 --- a/apps/meteor/app/utils/lib/i18n.ts +++ b/apps/meteor/app/utils/lib/i18n.ts @@ -1,9 +1,18 @@ import type { RocketchatI18nKeys } from '@rocket.chat/i18n'; +import type { TOptions } from 'i18next'; import i18next from 'i18next'; import sprintf from 'i18next-sprintf-postprocessor'; import { isObject } from '../../../lib/utils/isObject'; +declare module 'i18next' { + // eslint-disable-next-line @typescript-eslint/naming-convention + interface TFunction { + (key: RocketchatI18nKeys): string; + (key: RocketchatI18nKeys, options: TOptions): string; + } +} + export const i18n = i18next.use(sprintf); export const addSprinfToI18n = function (t: (typeof i18n)['t']) { diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index d56c898b14c1..0fea18c07a50 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "7.0.0" + "version": "7.1.0-rc.3" } diff --git a/apps/meteor/app/utils/server/functions/normalizeMessageFileUpload.ts b/apps/meteor/app/utils/server/functions/normalizeMessageFileUpload.ts index 097bbc4e9eb8..173592a98276 100644 --- a/apps/meteor/app/utils/server/functions/normalizeMessageFileUpload.ts +++ b/apps/meteor/app/utils/server/functions/normalizeMessageFileUpload.ts @@ -20,7 +20,7 @@ export const normalizeMessageFileUpload = async (message: Omit { +const filterStarred = (message: T, uid?: string): T => { // if Allow_anonymous_read is enabled, uid will be undefined if (!uid) return message; @@ -20,7 +20,7 @@ function getNameOfUsername(users: Map, username: string): string return users.get(username) || username; } -export const normalizeMessagesForUser = async (messages: IMessage[], uid?: string): Promise => { +export const normalizeMessagesForUser = async (messages: T[], uid?: string): Promise => { // if not using real names, there is nothing else to do if (!settings.get('UI_Use_Real_Name')) { return messages.map((message) => filterStarred(message, uid)); diff --git a/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.ts b/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.ts index 4cca28f1d5a9..be53cee5959a 100644 --- a/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.ts +++ b/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.ts @@ -3,6 +3,7 @@ import semver from 'semver'; import { i18n } from '../../../../server/lib/i18n'; import { sendMessagesToAdmins } from '../../../../server/lib/sendMessagesToAdmins'; +import { updateAuditedBySystem } from '../../../../server/settings/lib/auditedSettingUpdates'; import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { Info } from '../../../utils/rocketchat.info'; @@ -38,8 +39,11 @@ export const buildVersionUpdateMessage = async ( continue; } - (await Settings.updateValueById('Update_LatestAvailableVersion', version.version)).modifiedCount && - void notifyOnSettingChangedById('Update_LatestAvailableVersion'); + ( + await updateAuditedBySystem({ + reason: 'buildVersionUpdateMessage', + })(Settings.updateValueById, 'Update_LatestAvailableVersion', version.version) + ).modifiedCount && void notifyOnSettingChangedById('Update_LatestAvailableVersion'); await sendMessagesToAdmins({ msgs: async ({ adminUser }) => [ diff --git a/apps/meteor/app/version-check/server/index.ts b/apps/meteor/app/version-check/server/index.ts index 6736e527f3d8..45e9c5197c30 100644 --- a/apps/meteor/app/version-check/server/index.ts +++ b/apps/meteor/app/version-check/server/index.ts @@ -1,8 +1,8 @@ import { cronJobs } from '@rocket.chat/cron'; import { Meteor } from 'meteor/meteor'; -import { settings } from '../../settings/server'; import { checkVersionUpdate } from './functions/checkVersionUpdate'; +import { settings } from '../../settings/server'; import './methods/banner_dismiss'; const jobName = 'version_check'; diff --git a/apps/meteor/app/webdav/server/lib/getWebdavCredentials.ts b/apps/meteor/app/webdav/server/lib/getWebdavCredentials.ts index 6f2c59d5660f..5c12c5b676f1 100644 --- a/apps/meteor/app/webdav/server/lib/getWebdavCredentials.ts +++ b/apps/meteor/app/webdav/server/lib/getWebdavCredentials.ts @@ -6,6 +6,6 @@ export function getWebdavCredentials(account: WebDAVClientOptions): WebDAVClient : { username: account.username, password: account.password, - }; + }; return cred; } diff --git a/apps/meteor/app/webrtc/client/WebRTCClass.js b/apps/meteor/app/webrtc/client/WebRTCClass.ts similarity index 65% rename from apps/meteor/app/webrtc/client/WebRTCClass.js rename to apps/meteor/app/webrtc/client/WebRTCClass.ts index eb9772966575..5d942c365634 100644 --- a/apps/meteor/app/webrtc/client/WebRTCClass.js +++ b/apps/meteor/app/webrtc/client/WebRTCClass.ts @@ -1,45 +1,142 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import type { StreamKeys, StreamNames, StreamerCallbackArgs } from '@rocket.chat/ddp-client'; import { Emitter } from '@rocket.chat/emitter'; import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; import { Tracker } from 'meteor/tracker'; +import { ChromeScreenShare } from './screenShare'; import GenericModal from '../../../client/components/GenericModal'; import { imperativeModal } from '../../../client/lib/imperativeModal'; import { goToRoomById } from '../../../client/lib/utils/goToRoomById'; -import { ChatSubscription } from '../../models/client'; +import { Subscriptions } from '../../models/client'; import { settings } from '../../settings/client'; import { sdk } from '../../utils/client/lib/SDKClient'; import { t } from '../../utils/lib/i18n'; import { WEB_RTC_EVENTS } from '../lib/constants'; -import { ChromeScreenShare } from './screenShare'; -class WebRTCTransportClass extends Emitter { - constructor(webrtcInstance) { +// FIXME: there is a mix of obsolete definitions and incorrect field assignments + +declare global { + // eslint-disable-next-line @typescript-eslint/naming-convention + interface RTCPeerConnection { + /** @deprecated non-standard */ + createdAt: number; + /** @deprecated non-standard */ + remoteMedia: MediaStreamConstraints; + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + interface RTCOfferOptions { + /** @deprecated non-standard */ + mandatory?: unknown; + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + interface MediaStream { + /** @deprecated non-standard */ + volume?: GainNode; + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + interface MediaStreamConstraints { + /** @deprecated non-standard */ + desktop?: boolean; + } + + /** @deprecated browser-specific global */ + const chrome: { + webstore: { + install(url: string, onSuccess: () => void, onError: (error: any) => void): void; + }; + }; + + // eslint-disable-next-line @typescript-eslint/naming-convention + interface Window { + rocketchatscreenshare?: unknown; + audioContext?: AudioContext; + } +} + +type EventData, TType> = Extract< + StreamerCallbackArgs, + [type: TType, data: any] +>[1]; + +type StatusData = EventData<'notify-room', `${string}/webrtc`, 'status'>; +type CallData = EventData<'notify-room-users', `${string}/webrtc`, 'call'>; +type CandidateData = EventData<'notify-user', `${string}/webrtc`, 'candidate'>; +type DescriptionData = EventData<'notify-user', `${string}/webrtc`, 'description'>; +type JoinData = EventData<'notify-user', `${string}/webrtc`, 'join'>; + +type RemoteItem = { + id: string; + url: MediaStream; + state: RTCIceConnectionState; + stateText?: string; + connected?: boolean; +}; + +type RemoteConnection = { + id: string; + media: MediaStreamConstraints; +}; + +class WebRTCTransportClass extends Emitter<{ + status: StatusData; + call: CallData; + candidate: CandidateData; + description: DescriptionData; + join: JoinData; +}> { + public debug = false; + + constructor(public webrtcInstance: WebRTCClass) { super(); - this.debug = false; - this.webrtcInstance = webrtcInstance; sdk.stream('notify-room', [`${this.webrtcInstance.room}/${WEB_RTC_EVENTS.WEB_RTC}`], (type, data) => { this.log('WebRTCTransportClass - onRoom', type, data); this.emit(type, data); }); } - log(...args) { + log(...args: unknown[]) { if (this.debug === true) { console.log(...args); } } - onUserStream(type, data) { + onUserStream(type: 'candidate', data: CandidateData): void; + + onUserStream(type: 'description', data: DescriptionData): void; + + onUserStream(type: 'join', data: JoinData): void; + + onUserStream( + ...[type, data]: + | [type: 'candidate', data: CandidateData] + | [type: 'description', data: DescriptionData] + | [type: 'join', data: JoinData] + ) { if (data.room !== this.webrtcInstance.room) { return; } this.log('WebRTCTransportClass - onUser', type, data); - this.emit(type, data); + + switch (type) { + case 'candidate': + this.emit('candidate', data); + break; + case 'description': + this.emit('description', data); + break; + case 'join': + this.emit('join', data); + break; + } } - startCall(data) { + startCall(data: CallData) { this.log('WebRTCTransportClass - startCall', this.webrtcInstance.room, this.webrtcInstance.selfId); sdk.publish('notify-room-users', [ `${this.webrtcInstance.room}/${WEB_RTC_EVENTS.WEB_RTC}`, @@ -53,7 +150,7 @@ class WebRTCTransportClass extends Emitter { ]); } - joinCall(data) { + joinCall(data: JoinData) { this.log('WebRTCTransportClass - joinCall', this.webrtcInstance.room, this.webrtcInstance.selfId); if (data.monitor === true) { sdk.publish('notify-user', [ @@ -80,75 +177,109 @@ class WebRTCTransportClass extends Emitter { } } - sendCandidate(data) { + sendCandidate(data: CandidateData) { data.from = this.webrtcInstance.selfId; data.room = this.webrtcInstance.room; this.log('WebRTCTransportClass - sendCandidate', data); sdk.publish('notify-user', [`${data.to}/${WEB_RTC_EVENTS.WEB_RTC}`, WEB_RTC_EVENTS.CANDIDATE, data]); } - sendDescription(data) { + sendDescription(data: DescriptionData) { data.from = this.webrtcInstance.selfId; data.room = this.webrtcInstance.room; this.log('WebRTCTransportClass - sendDescription', data); sdk.publish('notify-user', [`${data.to}/${WEB_RTC_EVENTS.WEB_RTC}`, WEB_RTC_EVENTS.DESCRIPTION, data]); } - sendStatus(data) { + sendStatus(data: StatusData) { this.log('WebRTCTransportClass - sendStatus', data, this.webrtcInstance.room); data.from = this.webrtcInstance.selfId; sdk.publish('notify-room', [`${this.webrtcInstance.room}/${WEB_RTC_EVENTS.WEB_RTC}`, WEB_RTC_EVENTS.STATUS, data]); } - onRemoteCall(fn) { + onRemoteCall(fn: (data: CallData) => void) { return this.on(WEB_RTC_EVENTS.CALL, fn); } - onRemoteJoin(fn) { + onRemoteJoin(fn: (data: JoinData) => void) { return this.on(WEB_RTC_EVENTS.JOIN, fn); } - onRemoteCandidate(fn) { + onRemoteCandidate(fn: (data: CandidateData) => void) { return this.on(WEB_RTC_EVENTS.CANDIDATE, fn); } - onRemoteDescription(fn) { + onRemoteDescription(fn: (data: DescriptionData) => void) { return this.on(WEB_RTC_EVENTS.DESCRIPTION, fn); } - onRemoteStatus(fn) { + onRemoteStatus(fn: (data: StatusData) => void) { return this.on(WEB_RTC_EVENTS.STATUS, fn); } } class WebRTCClass { - /* - @param seldId {String} - @param room {String} - */ + transport: WebRTCTransportClass; + + config: { iceServers: RTCIceServer[] }; + + debug: boolean; + + TransportClass: typeof WebRTCTransportClass; + + peerConnections: Record = {}; + + remoteItems: ReactiveVar; + + remoteItemsById: ReactiveVar>; + + callInProgress: ReactiveVar; + + audioEnabled: ReactiveVar; + + videoEnabled: ReactiveVar; + + overlayEnabled: ReactiveVar; + + screenShareEnabled: ReactiveVar; - constructor(selfId, room, autoAccept = false) { + localUrl: ReactiveVar; + + active: boolean; + + remoteMonitoring: boolean; + + monitor: boolean; + + navigator: string | undefined; + + screenShareAvailable: boolean; + + media: MediaStreamConstraints; + + constructor( + public selfId: string, + public room: string, + public autoAccept = false, + ) { this.config = { iceServers: [], }; this.debug = false; this.TransportClass = WebRTCTransportClass; - this.selfId = selfId; - this.room = room; - let servers = settings.get('WebRTC_Servers'); + let servers = settings.get('WebRTC_Servers'); if (servers && servers.trim() !== '') { servers = servers.replace(/\s/g, ''); - servers = servers.split(','); - servers.forEach((server) => { - server = server.split('@'); - const serverConfig = { - urls: server.pop(), + servers.split(',').forEach((server) => { + const parts = server.split('@'); + const serverConfig: RTCIceServer = { + urls: parts.pop()!, }; - if (server.length === 1) { - server = server[0].split(':'); - serverConfig.username = decodeURIComponent(server[0]); - serverConfig.credential = decodeURIComponent(server[1]); + if (parts.length === 1) { + const [username, credential] = parts[0].split(':'); + serverConfig.username = decodeURIComponent(username); + serverConfig.credential = decodeURIComponent(credential); } this.config.iceServers.push(serverConfig); }); @@ -161,11 +292,10 @@ class WebRTCClass { this.videoEnabled = new ReactiveVar(false); this.overlayEnabled = new ReactiveVar(false); this.screenShareEnabled = new ReactiveVar(false); - this.localUrl = new ReactiveVar(); + this.localUrl = new ReactiveVar(undefined); this.active = false; this.remoteMonitoring = false; this.monitor = false; - this.autoAccept = autoAccept; this.navigator = undefined; const userAgent = navigator.userAgent.toLocaleLowerCase(); @@ -179,7 +309,7 @@ class WebRTCClass { this.navigator = 'safari'; } - this.screenShareAvailable = ['chrome', 'firefox', 'electron'].includes(this.navigator); + this.screenShareAvailable = ['chrome', 'firefox', 'electron'].includes(this.navigator!); this.media = { video: true, audio: true, @@ -194,18 +324,41 @@ class WebRTCClass { setInterval(this.checkPeerConnections.bind(this), 1000); } - onUserStream(...args) { - return this.transport.onUserStream(...args); + onUserStream(type: 'candidate', data: CandidateData): void; + + onUserStream(type: 'description', data: DescriptionData): void; + + onUserStream(type: 'join', data: JoinData): void; + + onUserStream( + ...[type, data]: + | [type: 'candidate', data: CandidateData] + | [type: 'description', data: DescriptionData] + | [type: 'join', data: JoinData] + ) { + switch (type) { + case 'candidate': + this.transport.onUserStream('candidate', data); + break; + + case 'description': + this.transport.onUserStream('description', data); + break; + + case 'join': + this.transport.onUserStream('join', data); + break; + } } - log(...args) { + log(...args: unknown[]) { if (this.debug === true) { - console.log.apply(console, args); + console.log(...args); } } - onError(...args) { - console.error.apply(console, args); + onError(...args: unknown[]) { + console.error(...args); } checkPeerConnections() { @@ -221,13 +374,13 @@ class WebRTCClass { } updateRemoteItems() { - const items = []; - const itemsById = {}; + const items: RemoteItem[] = []; + const itemsById: Record = {}; const { peerConnections } = this; Object.entries(peerConnections).forEach(([id, peerConnection]) => { peerConnection.getRemoteStreams().forEach((remoteStream) => { - const item = { + const item: RemoteItem = { id, url: remoteStream, state: peerConnection.iceConnectionState, @@ -266,9 +419,9 @@ class WebRTCClass { if (this.active !== true || this.monitor === true || this.remoteMonitoring === true) { return; } - const remoteConnections = []; + const remoteConnections: RemoteConnection[] = []; const { peerConnections } = this; - Object.keys(peerConnections).entries(([id, { remoteMedia: media }]) => { + Object.entries(peerConnections).forEach(([id, { remoteMedia: media }]) => { remoteConnections.push({ id, media, @@ -281,16 +434,9 @@ class WebRTCClass { }); } - /* - @param data {Object} - from {String} - media {Object} - remoteConnections {Array[Object]} - id {String} - media {Object} - */ + callInProgressTimeout: ReturnType | undefined = undefined; - onRemoteStatus(data) { + onRemoteStatus(data: StatusData) { // this.log(onRemoteStatus, arguments); this.callInProgress.set(true); clearTimeout(this.callInProgressTimeout); @@ -300,7 +446,7 @@ class WebRTCClass { } const remoteConnections = [ { - id: data.from, + id: data.from!, media: data.media, }, ...data.remoteConnections, @@ -317,11 +463,7 @@ class WebRTCClass { }); } - /* - @param id {String} - */ - - getPeerConnection(id) { + getPeerConnection(id: string) { if (this.peerConnections[id] != null) { return this.peerConnections[id]; } @@ -386,8 +528,10 @@ class WebRTCClass { return peerConnection; } - _getUserMedia(media, onSuccess, onError) { - const onSuccessLocal = (stream) => { + audioContext: AudioContext | undefined; + + _getUserMedia(media: MediaStreamConstraints, onSuccess: (stream: MediaStream) => void, onError: (error?: any) => void) { + const onSuccessLocal = (stream: MediaStream) => { if (AudioContext && stream.getAudioTracks().length > 0) { const audioContext = new AudioContext(); const source = audioContext.createMediaStreamSource(stream); @@ -403,23 +547,24 @@ class WebRTCClass { } onSuccess(stream); }; - if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { + + if (navigator.mediaDevices?.getUserMedia) { return navigator.mediaDevices.getUserMedia(media).then(onSuccessLocal).catch(onError); } - navigator.getUserMedia(media, onSuccessLocal, onError); + navigator.getUserMedia?.(media, onSuccessLocal, onError); } - getUserMedia(media, onSuccess, onError = this.onError) { + getUserMedia(media: MediaStreamConstraints, onSuccess: (stream: MediaStream) => void, onError: (error: any) => void = this.onError) { if (media.desktop !== true) { - this._getUserMedia(media, onSuccess, onError); + void this._getUserMedia(media, onSuccess, onError); return; } if (this.screenShareAvailable !== true) { console.log('Screen share is not avaliable'); return; } - const getScreen = (audioStream) => { + const getScreen = (audioStream?: MediaStream) => { const refresh = function () { imperativeModal.open({ component: GenericModal, @@ -466,7 +611,7 @@ class WebRTCClass { return onError(false); } - const getScreenSuccess = (stream) => { + const getScreenSuccess = (stream: MediaStream) => { if (audioStream != null) { stream.addTrack(audioStream.getAudioTracks()[0]); } @@ -480,9 +625,9 @@ class WebRTCClass { mediaSource: 'window', }, }; - this._getUserMedia(media, getScreenSuccess, onError); + void this._getUserMedia(media, getScreenSuccess, onError); } else { - ChromeScreenShare.getSourceId(this.navigator, (id) => { + ChromeScreenShare.getSourceId(this.navigator!, (id) => { media = { audio: false, video: { @@ -494,21 +639,21 @@ class WebRTCClass { }, }, }; - this._getUserMedia(media, getScreenSuccess, onError); + void this._getUserMedia(media, getScreenSuccess, onError); }); } }; if (this.navigator === 'firefox' || media.audio == null || media.audio === false) { getScreen(); } else { - const getAudioSuccess = (audioStream) => { + const getAudioSuccess = (audioStream: MediaStream) => { getScreen(audioStream); }; const getAudioError = () => { getScreen(); }; - this._getUserMedia( + void this._getUserMedia( { audio: media.audio, }, @@ -518,37 +663,29 @@ class WebRTCClass { } } - /* - @param callback {Function} - */ - - getLocalUserMedia(callback, ...args) { + getLocalUserMedia(callback: (...args: any[]) => void, ...args: unknown[]) { this.log('getLocalUserMedia', [callback, ...args]); if (this.localStream != null) { return callback(null, this.localStream); } - const onSuccess = (stream) => { + const onSuccess = (stream: MediaStream) => { this.localStream = stream; !this.audioEnabled.get() && this.disableAudio(); !this.videoEnabled.get() && this.disableVideo(); this.localUrl.set(stream); const { peerConnections } = this; Object.entries(peerConnections).forEach(([, peerConnection]) => peerConnection.addStream(stream)); - document.querySelector('video#localVideo').srcObject = stream; + document.querySelector('video#localVideo')!.srcObject = stream; callback(null, this.localStream); }; - const onError = (error) => { + const onError = (error: any) => { callback(false); this.onError(error); }; this.getUserMedia(this.media, onSuccess, onError); } - /* - @param id {String} - */ - - stopPeerConnection = (id) => { + stopPeerConnection = (id: string) => { const peerConnection = this.peerConnections[id]; if (peerConnection == null) { return; @@ -563,7 +700,7 @@ class WebRTCClass { Object.keys(peerConnections).forEach(this.stopPeerConnection); - window.audioContext && window.audioContext.close(); + void window.audioContext?.close(); // FIXME: probably should be `this.audioContext` } setAudioEnabled(enabled = true) { @@ -590,6 +727,8 @@ class WebRTCClass { return this.enableAudio(); } + localStream: MediaStream | undefined; + setVideoEnabled(enabled = true) { if (this.localStream != null) { this.localStream.getVideoTracks().forEach((video) => { @@ -649,13 +788,7 @@ class WebRTCClass { this.stopAllPeerConnections(); } - /* - @param media {Object} - audio {Boolean} - video {Boolean} - */ - - startCall(media = {}, ...args) { + startCall(media: MediaStreamConstraints = {}, ...args: unknown[]) { this.log('startCall', [media, ...args]); this.media = media; this.getLocalUserMedia(() => { @@ -666,7 +799,7 @@ class WebRTCClass { }); } - startCallAsMonitor(media = {}, ...args) { + startCallAsMonitor(media: MediaStreamConstraints = {}, ...args: unknown[]) { this.log('startCallAsMonitor', [media, ...args]); this.media = media; this.active = true; @@ -677,16 +810,7 @@ class WebRTCClass { }); } - /* - @param data {Object} - from {String} - monitor {Boolean} - media {Object} - audio {Boolean} - video {Boolean} - */ - - onRemoteCall(data) { + onRemoteCall(data: CallData) { if (this.autoAccept === true) { setTimeout(() => { this.joinCall({ @@ -700,31 +824,31 @@ class WebRTCClass { const user = Meteor.users.findOne(data.from); let fromUsername = undefined; - if (user && user.username) { + if (user?.username) { fromUsername = user.username; } - const subscription = ChatSubscription.findOne({ + const subscription = Subscriptions.findOne({ rid: data.room, - }); + })!; let icon; let title; if (data.monitor === true) { - icon = 'eye'; + icon = 'eye' as const; title = t('WebRTC_monitor_call_from_%s', fromUsername); } else if (subscription && subscription.t === 'd') { - if (data.media && data.media.video) { - icon = 'videocam'; + if (data.media?.video) { + icon = 'video' as const; title = t('WebRTC_direct_video_call_from_%s', fromUsername); } else { - icon = 'phone'; + icon = 'phone' as const; title = t('WebRTC_direct_audio_call_from_%s', fromUsername); } - } else if (data.media && data.media.video) { - icon = 'videocam'; + } else if (data.media?.video) { + icon = 'video' as const; title = t('WebRTC_group_video_call_from_%s', subscription.name); } else { - icon = 'phone'; + icon = 'phone' as const; title = t('WebRTC_group_audio_call_from_%s', subscription.name); } @@ -737,7 +861,7 @@ class WebRTCClass { cancelText: t('No'), children: t('Do_you_want_to_accept'), onConfirm: () => { - goToRoomById(data.room); + void goToRoomById(data.room!); return this.joinCall({ to: data.from, monitor: data.monitor, @@ -750,32 +874,22 @@ class WebRTCClass { }); } - /* - @param data {Object} - to {String} - monitor {Boolean} - media {Object} - audio {Boolean} - video {Boolean} - desktop {Boolean} - */ - - joinCall(data = {}, ...args) { + joinCall(data: JoinData = {}, ...args: unknown[]) { data.media = this.media; this.log('joinCall', [data, ...args]); this.getLocalUserMedia(() => { - this.remoteMonitoring = data.monitor; + this.remoteMonitoring = data.monitor!; this.active = true; this.transport.joinCall(data); }); } - onRemoteJoin(data, ...args) { + onRemoteJoin(data: JoinData, ...args: unknown[]) { if (this.active !== true) { return; } this.log('onRemoteJoin', [data, ...args]); - let peerConnection = this.getPeerConnection(data.from); + let peerConnection = this.getPeerConnection(data.from!); // needsRefresh = false // if peerConnection.iceConnectionState isnt 'new' @@ -785,18 +899,18 @@ class WebRTCClass { // # if peerConnection.signalingState is "have-local-offer" or needsRefresh - if (peerConnection.signalingState !== 'checking') { - this.stopPeerConnection(data.from); - peerConnection = this.getPeerConnection(data.from); + if ((peerConnection.signalingState as RTCSignalingState | 'checking') !== 'checking') { + this.stopPeerConnection(data.from!); + peerConnection = this.getPeerConnection(data.from!); } if (peerConnection.iceConnectionState !== 'new') { return; } - peerConnection.remoteMedia = data.media; + peerConnection.remoteMedia = data.media!; if (this.localStream) { peerConnection.addStream(this.localStream); } - const onOffer = (offer) => { + const onOffer: RTCSessionDescriptionCallback = (offer) => { const onLocalDescription = () => { this.transport.sendDescription({ to: data.from, @@ -810,39 +924,39 @@ class WebRTCClass { }); }; - peerConnection.setLocalDescription(new RTCSessionDescription(offer), onLocalDescription, this.onError); + void peerConnection.setLocalDescription(new RTCSessionDescription(offer), onLocalDescription, this.onError); }; if (data.monitor === true) { - peerConnection.createOffer(onOffer, this.onError, { + void peerConnection.createOffer(onOffer, this.onError, { mandatory: { - OfferToReceiveAudio: data.media.audio, - OfferToReceiveVideo: data.media.video, + OfferToReceiveAudio: data.media?.audio, + OfferToReceiveVideo: data.media?.video, }, }); } else { - peerConnection.createOffer(onOffer, this.onError); + void peerConnection.createOffer(onOffer, this.onError); } } - onRemoteOffer(data, ...args) { + onRemoteOffer(data: Omit, ...args: unknown[]) { if (this.active !== true) { return; } this.log('onRemoteOffer', [data, ...args]); - let peerConnection = this.getPeerConnection(data.from); + let peerConnection = this.getPeerConnection(data.from!); if (['have-local-offer', 'stable'].includes(peerConnection.signalingState) && peerConnection.createdAt < data.ts) { - this.stopPeerConnection(data.from); - peerConnection = this.getPeerConnection(data.from); + this.stopPeerConnection(data.from!); + peerConnection = this.getPeerConnection(data.from!); } if (peerConnection.iceConnectionState !== 'new') { return; } - peerConnection.setRemoteDescription(new RTCSessionDescription(data.description)); + void peerConnection.setRemoteDescription(new RTCSessionDescription(data.description)); try { if (this.localStream) { @@ -852,7 +966,7 @@ class WebRTCClass { console.log(error); } - const onAnswer = (answer) => { + const onAnswer: RTCSessionDescriptionCallback = (answer) => { const onLocalDescription = () => { this.transport.sendDescription({ to: data.from, @@ -865,20 +979,13 @@ class WebRTCClass { }); }; - peerConnection.setLocalDescription(new RTCSessionDescription(answer), onLocalDescription, this.onError); + void peerConnection.setLocalDescription(new RTCSessionDescription(answer), onLocalDescription, this.onError); }; - peerConnection.createAnswer(onAnswer, this.onError); + void peerConnection.createAnswer(onAnswer, this.onError); } - /* - @param data {Object} - to {String} - from {String} - candidate {RTCIceCandidate JSON encoded} - */ - - onRemoteCandidate(data, ...args) { + onRemoteCandidate(data: CandidateData, ...args: unknown[]) { if (this.active !== true) { return; } @@ -886,32 +993,19 @@ class WebRTCClass { return; } this.log('onRemoteCandidate', [data, ...args]); - const peerConnection = this.getPeerConnection(data.from); + const peerConnection = this.getPeerConnection(data.from!); if ( peerConnection.iceConnectionState !== 'closed' && peerConnection.iceConnectionState !== 'failed' && peerConnection.iceConnectionState !== 'disconnected' && peerConnection.iceConnectionState !== 'completed' ) { - peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate)); + void peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate)); } - document.querySelector('video#remoteVideo').srcObject = this.remoteItems.get()[0]?.url; + document.querySelector('video#remoteVideo')!.srcObject = this.remoteItems.get()[0]?.url; } - /* - @param data {Object} - to {String} - from {String} - type {String} [offer, answer] - description {RTCSessionDescription JSON encoded} - ts {Integer} - media {Object} - audio {Boolean} - video {Boolean} - desktop {Boolean} - */ - - onRemoteDescription(data, ...args) { + onRemoteDescription(data: DescriptionData, ...args: unknown[]) { if (this.active !== true) { return; } @@ -919,7 +1013,7 @@ class WebRTCClass { return; } this.log('onRemoteDescription', [data, ...args]); - const peerConnection = this.getPeerConnection(data.from); + const peerConnection = this.getPeerConnection(data.from!); if (data.type === 'offer') { peerConnection.remoteMedia = data.media; this.onRemoteOffer({ @@ -928,20 +1022,22 @@ class WebRTCClass { description: data.description, }); } else { - peerConnection.setRemoteDescription(new RTCSessionDescription(data.description)); + void peerConnection.setRemoteDescription(new RTCSessionDescription(data.description)); } } } const WebRTC = new (class { + instancesByRoomId: Record = {}; + constructor() { this.instancesByRoomId = {}; } - getInstanceByRoomId(rid, visitorId = null) { + getInstanceByRoomId(rid: IRoom['_id'], visitorId: string | null = null) { let enabled = false; if (!visitorId) { - const subscription = ChatSubscription.findOne({ rid }); + const subscription = Subscriptions.findOne({ rid }); if (!subscription) { return; } @@ -956,17 +1052,17 @@ const WebRTC = new (class { enabled = settings.get('WebRTC_Enable_Channel'); break; case 'l': - enabled = settings.get('Omnichannel_call_provider') === 'WebRTC'; + enabled = settings.get('Omnichannel_call_provider') === 'WebRTC'; } } else { - enabled = settings.get('Omnichannel_call_provider') === 'WebRTC'; + enabled = settings.get('Omnichannel_call_provider') === 'WebRTC'; } enabled = enabled && settings.get('WebRTC_Enabled'); if (enabled === false) { return; } if (this.instancesByRoomId[rid] == null) { - let uid = Meteor.userId(); + let uid = Meteor.userId()!; let autoAccept = false; if (visitorId) { uid = visitorId; @@ -980,13 +1076,26 @@ const WebRTC = new (class { Meteor.startup(() => { Tracker.autorun(() => { - if (Meteor.userId()) { - sdk.stream('notify-user', [`${Meteor.userId()}/${WEB_RTC_EVENTS.WEB_RTC}`], (type, data) => { + const uid = Meteor.userId(); + + if (uid) { + sdk.stream('notify-user', [`${uid}/${WEB_RTC_EVENTS.WEB_RTC}`], (type, data) => { if (data.room == null) { return; } const webrtc = WebRTC.getInstanceByRoomId(data.room); - webrtc.onUserStream(type, data); + + switch (type) { + case 'candidate': + webrtc?.onUserStream('candidate', data); + break; + case 'description': + webrtc?.onUserStream('description', data); + break; + case 'join': + webrtc?.onUserStream('join', data); + break; + } }); } }); diff --git a/apps/meteor/app/webrtc/client/actionLink.tsx b/apps/meteor/app/webrtc/client/actionLink.tsx index d4575f2dd60f..90258eeedce8 100644 --- a/apps/meteor/app/webrtc/client/actionLink.tsx +++ b/apps/meteor/app/webrtc/client/actionLink.tsx @@ -2,12 +2,12 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { actionLinks } from '../../../client/lib/actionLinks'; import { dispatchToastMessage } from '../../../client/lib/toast'; -import { ChatRoom } from '../../models/client'; +import { Rooms } from '../../models/client'; import { sdk } from '../../utils/client/lib/SDKClient'; import { t } from '../../utils/lib/i18n'; actionLinks.register('joinLivechatWebRTCCall', (message: IMessage) => { - const room = ChatRoom.findOne({ _id: message.rid }); + const room = Rooms.findOne({ _id: message.rid }); if (!room) { throw new Error('Room not found'); } @@ -20,7 +20,7 @@ actionLinks.register('joinLivechatWebRTCCall', (message: IMessage) => { }); actionLinks.register('endLivechatWebRTCCall', async (message: IMessage) => { - const room = ChatRoom.findOne({ _id: message.rid }); + const room = Rooms.findOne({ _id: message.rid }); if (!room) { throw new Error('Room not found'); } diff --git a/apps/meteor/app/webrtc/client/adapter.js b/apps/meteor/app/webrtc/client/adapter.js deleted file mode 100644 index 972e68e09f3c..000000000000 --- a/apps/meteor/app/webrtc/client/adapter.js +++ /dev/null @@ -1,6 +0,0 @@ -window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; -window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription; -window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate; -window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription; -window.AudioContext = window.AudioContext || window.mozAudioContext || window.webkitAudioContext; -navigator.getUserMedia = navigator.getUserMedia || navigator.mozGetUserMedia || navigator.webkitGetUserMedia; diff --git a/apps/meteor/app/webrtc/client/adapter.ts b/apps/meteor/app/webrtc/client/adapter.ts new file mode 100644 index 000000000000..f98ae7815c05 --- /dev/null +++ b/apps/meteor/app/webrtc/client/adapter.ts @@ -0,0 +1,7 @@ +// FIXME: probably outdated +window.RTCPeerConnection = window.RTCPeerConnection ?? window.mozRTCPeerConnection ?? window.webkitRTCPeerConnection; +window.RTCSessionDescription = window.RTCSessionDescription ?? window.mozRTCSessionDescription ?? window.webkitRTCSessionDescription; +window.RTCIceCandidate = window.RTCIceCandidate ?? window.mozRTCIceCandidate ?? window.webkitRTCIceCandidate; +window.RTCSessionDescription = window.RTCSessionDescription ?? window.mozRTCSessionDescription ?? window.webkitRTCSessionDescription; +window.AudioContext = window.AudioContext ?? window.mozAudioContext ?? window.webkitAudioContext; +navigator.getUserMedia = navigator.getUserMedia ?? navigator.mozGetUserMedia ?? navigator.webkitGetUserMedia; diff --git a/apps/meteor/app/webrtc/client/screenShare.js b/apps/meteor/app/webrtc/client/screenShare.ts similarity index 77% rename from apps/meteor/app/webrtc/client/screenShare.js rename to apps/meteor/app/webrtc/client/screenShare.ts index ecb6f93a51d0..3fac4a05bfea 100644 --- a/apps/meteor/app/webrtc/client/screenShare.js +++ b/apps/meteor/app/webrtc/client/screenShare.ts @@ -1,18 +1,20 @@ import { fireGlobalEvent } from '../../../client/lib/utils/fireGlobalEvent'; export const ChromeScreenShare = { - callbacks: {}, - installed: false, - init() { - this.callbacks['get-RocketChatScreenSharingExtensionVersion'] = (version) => { + callbacks: { + 'get-RocketChatScreenSharingExtensionVersion': (version: unknown) => { if (version) { - this.installed = true; + ChromeScreenShare.installed = true; } - }; + }, + 'getSourceId': (_sourceId: string): void => undefined, + }, + installed: false, + init() { window.postMessage('get-RocketChatScreenSharingExtensionVersion', '*'); }, - getSourceId(navigator, callback) { - if (callback == null) { + getSourceId(navigator: string, callback: (sourceId: string) => void) { + if (!callback) { throw new Error('"callback" parameter is mandatory.'); } this.callbacks.getSourceId = callback; @@ -36,8 +38,7 @@ window.addEventListener('message', (e) => { throw new Error('PermissionDeniedError'); } if (e.data.version != null) { - ChromeScreenShare.callbacks['get-RocketChatScreenSharingExtensionVersion'] && - ChromeScreenShare.callbacks['get-RocketChatScreenSharingExtensionVersion'](e.data.version); + ChromeScreenShare.callbacks['get-RocketChatScreenSharingExtensionVersion']?.(e.data.version); } else if (e.data.sourceId != null) { return typeof ChromeScreenShare.callbacks.getSourceId === 'function' && ChromeScreenShare.callbacks.getSourceId(e.data.sourceId); } diff --git a/apps/meteor/app/wordpress/client/wordpress-login-button.css b/apps/meteor/app/wordpress/client/wordpress-login-button.css index edc54f247ae1..35c6d2e78eb4 100644 --- a/apps/meteor/app/wordpress/client/wordpress-login-button.css +++ b/apps/meteor/app/wordpress/client/wordpress-login-button.css @@ -1,3 +1,3 @@ #login-buttons-image-wordpress { - background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiP…A3LDI3LjUsMjUuOTQsMjMuMjE1LDI4LjQzNHoiLz4NCgk8L2c+DQo8L2c+DQo8L3N2Zz4NCg==); + background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiP…A3LDI3LjUsMjUuOTQsMjMuMjE1LDI4LjQzNHoiLz4NCgk8L2c+DQo8L2c+DQo8L3N2Zz4NCg=="); } diff --git a/apps/meteor/client/NavBarV2/NavBar.tsx b/apps/meteor/client/NavBarV2/NavBar.tsx index 7e61d53e5eff..116ff00b4075 100644 --- a/apps/meteor/client/NavBarV2/NavBar.tsx +++ b/apps/meteor/client/NavBarV2/NavBar.tsx @@ -4,10 +4,6 @@ import { usePermission, useTranslation, useUser } from '@rocket.chat/ui-contexts import { useVoipState } from '@rocket.chat/ui-voip'; import React, { useRef } from 'react'; -import { useIsCallEnabled, useIsCallReady } from '../contexts/CallContext'; -import { useOmnichannelEnabled } from '../hooks/omnichannel/useOmnichannelEnabled'; -import { useOmnichannelShowQueueLink } from '../hooks/omnichannel/useOmnichannelShowQueueLink'; -import { useHasLicenseModule } from '../hooks/useHasLicenseModule'; import { NavBarItemOmniChannelCallDialPad, NavBarItemOmnichannelContact, @@ -18,6 +14,10 @@ import { import { NavBarItemMarketPlaceMenu, NavBarItemAuditMenu, NavBarItemDirectoryPage, NavBarItemHomePage } from './NavBarPagesToolbar'; import { NavBarItemLoginPage, NavBarItemAdministrationMenu, UserMenu } from './NavBarSettingsToolbar'; import { NavBarItemVoipDialer } from './NavBarVoipToolbar'; +import { useIsCallEnabled, useIsCallReady } from '../contexts/CallContext'; +import { useOmnichannelEnabled } from '../hooks/omnichannel/useOmnichannelEnabled'; +import { useOmnichannelShowQueueLink } from '../hooks/omnichannel/useOmnichannelShowQueueLink'; +import { useHasLicenseModule } from '../hooks/useHasLicenseModule'; const NavBar = () => { const t = useTranslation(); @@ -59,7 +59,7 @@ const NavBar = () => { {showOmnichannelQueueLink && } {isCallReady && } - + {isCallEnabled && } diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggle.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggle.tsx index ce62cb51864b..0df36e6e73c3 100644 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggle.tsx +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggle.tsx @@ -1,10 +1,10 @@ import type { ComponentPropsWithoutRef } from 'react'; import React from 'react'; -import { useIsCallReady, useIsCallError } from '../../contexts/CallContext'; import NavBarItemOmnichannelCallToggleError from './NavBarItemOmnichannelCallToggleError'; import NavBarItemOmnichannelCallToggleLoading from './NavBarItemOmnichannelCallToggleLoading'; import NavBarItemOmnichannelCallToggleReady from './NavBarItemOmnichannelCallToggleReady'; +import { useIsCallReady, useIsCallError } from '../../contexts/CallContext'; type NavBarItemOmnichannelCallToggleProps = ComponentPropsWithoutRef< typeof NavBarItemOmnichannelCallToggleError | typeof NavBarItemOmnichannelCallToggleLoading | typeof NavBarItemOmnichannelCallToggleReady diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelLivechatToggle.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelLivechatToggle.tsx index 5bf174362e19..abf8ca432c28 100644 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelLivechatToggle.tsx +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelLivechatToggle.tsx @@ -1,15 +1,16 @@ import { Sidebar } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useEndpoint, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import type { ReactElement, ComponentProps } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { useOmnichannelAgentAvailable } from '../../hooks/omnichannel/useOmnichannelAgentAvailable'; type NavBarItemOmnichannelLivechatToggleProps = Omit, 'icon'>; const NavBarItemOmnichannelLivechatToggle = (props: NavBarItemOmnichannelLivechatToggleProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const agentAvailable = useOmnichannelAgentAvailable(); const changeAgentStatus = useEndpoint('POST', '/v1/livechat/agent.status'); const dispatchToastMessage = useToastMessageDispatch(); diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemAuditMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemAuditMenu.tsx index 7c8a50338e7d..7f47611f9bdb 100644 --- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemAuditMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemAuditMenu.tsx @@ -1,15 +1,16 @@ import { NavBarItem } from '@rocket.chat/fuselage'; import { GenericMenu } from '@rocket.chat/ui-client'; -import { useCurrentRoutePath, useTranslation } from '@rocket.chat/ui-contexts'; +import { useCurrentRoutePath } from '@rocket.chat/ui-contexts'; import type { HTMLAttributes } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { useAuditMenu } from './hooks/useAuditMenu'; type NavBarItemAuditMenuProps = Omit, 'is'>; const NavBarItemAuditMenu = (props: NavBarItemAuditMenuProps) => { - const t = useTranslation(); + const { t } = useTranslation(); const sections = useAuditMenu(); const currentRoute = useCurrentRoutePath(); diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemMarketPlaceMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemMarketPlaceMenu.tsx index 85687bb12a2e..1e7fbdefb083 100644 --- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemMarketPlaceMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemMarketPlaceMenu.tsx @@ -1,15 +1,16 @@ import { NavBarItem } from '@rocket.chat/fuselage'; import { GenericMenu } from '@rocket.chat/ui-client'; -import { useCurrentRoutePath, useTranslation } from '@rocket.chat/ui-contexts'; +import { useCurrentRoutePath } from '@rocket.chat/ui-contexts'; import type { HTMLAttributes } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { useMarketPlaceMenu } from './hooks/useMarketPlaceMenu'; type NavBarItemMarketPlaceMenuProps = Omit, 'is'>; const NavBarItemMarketPlaceMenu = (props: NavBarItemMarketPlaceMenuProps) => { - const t = useTranslation(); + const { t } = useTranslation(); const sections = useMarketPlaceMenu(); const currentRoute = useCurrentRoutePath(); diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.tsx index 97c8d7299497..7c0c36dc9f24 100644 --- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.tsx @@ -1,11 +1,12 @@ import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; -import { usePermission, useRouter, useTranslation } from '@rocket.chat/ui-contexts'; +import { usePermission, useRouter } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; export const useAuditMenu = () => { const router = useRouter(); - const t = useTranslation(); + const { t } = useTranslation(); const hasAuditLicense = useHasLicenseModule('auditing') === true; diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.tsx index 8236eec030e8..a17061050ce9 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.tsx @@ -1,15 +1,16 @@ import { NavBarItem } from '@rocket.chat/fuselage'; import { GenericMenu } from '@rocket.chat/ui-client'; -import { useCurrentRoutePath, useTranslation } from '@rocket.chat/ui-contexts'; +import { useCurrentRoutePath } from '@rocket.chat/ui-contexts'; import type { HTMLAttributes } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { useAdministrationMenu } from './hooks/useAdministrationMenu'; type NavBarItemAdministrationMenuProps = Omit, 'is'>; const NavBarItemAdministrationMenu = (props: NavBarItemAdministrationMenuProps) => { - const t = useTranslation(); + const { t } = useTranslation(); const currentRoute = useCurrentRoutePath(); const sections = useAdministrationMenu(); diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemLoginPage.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemLoginPage.tsx index a02c17db0b9b..1ef6f298fccd 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemLoginPage.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemLoginPage.tsx @@ -1,13 +1,14 @@ import { Button } from '@rocket.chat/fuselage'; -import { useSessionDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSessionDispatch } from '@rocket.chat/ui-contexts'; import type { HTMLAttributes } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; type NavBarItemLoginPageProps = Omit, 'is'>; const NavBarItemLoginPage = (props: NavBarItemLoginPageProps) => { const setForceLogin = useSessionDispatch('forceLogin'); - const t = useTranslation(); + const { t } = useTranslation(); return ( - diff --git a/apps/meteor/client/components/FilterByText.tsx b/apps/meteor/client/components/FilterByText.tsx index 25d8e225e3d8..08c3cb78e21f 100644 --- a/apps/meteor/client/components/FilterByText.tsx +++ b/apps/meteor/client/components/FilterByText.tsx @@ -1,28 +1,23 @@ import { Box, Icon, TextInput, Margins } from '@rocket.chat/fuselage'; import { useAutoFocus, useMergedRefs } from '@rocket.chat/fuselage-hooks'; -import type { ChangeEvent, FormEvent, HTMLAttributes } from 'react'; -import React, { forwardRef, memo, useCallback, useState } from 'react'; +import type { ChangeEvent, ComponentPropsWithoutRef, FormEvent } from 'react'; +import React, { forwardRef, memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -type FilterByTextProps = { - onChange: (filter: string) => void; +// TODO: consider changing the type of TextInput's `onChange` to (event: ChangeEvent) => void +type FilterByTextProps = Omit, 'onChange'> & { shouldAutoFocus?: boolean; -} & Omit, 'is' | 'onChange'>; + onChange?: (event: ChangeEvent) => void; +}; const FilterByText = forwardRef(function FilterByText( - { placeholder, onChange: setFilter, shouldAutoFocus = false, children, ...props }, + { placeholder, shouldAutoFocus = false, children, ...props }, ref, ) { const { t } = useTranslation(); - const [text, setText] = useState(''); const autoFocusRef = useAutoFocus(shouldAutoFocus); const mergedRefs = useMergedRefs(ref, autoFocusRef); - const handleInputChange = (event: ChangeEvent) => { - setText(event.currentTarget.value); - setFilter(event.currentTarget.value); - }; - const handleFormSubmit = useCallback((event: FormEvent) => { event.preventDefault(); }, []); @@ -35,8 +30,6 @@ const FilterByText = forwardRef(function Fi placeholder={placeholder ?? t('Search')} ref={mergedRefs} addon={} - onChange={handleInputChange} - value={text} flexGrow={2} minWidth='x220' aria-label={placeholder ?? t('Search')} diff --git a/apps/meteor/client/components/GazzodownText.tsx b/apps/meteor/client/components/GazzodownText.tsx index 73d55495630c..05c6788062a9 100644 --- a/apps/meteor/client/components/GazzodownText.tsx +++ b/apps/meteor/client/components/GazzodownText.tsx @@ -10,9 +10,9 @@ import React, { useCallback, memo, useMemo } from 'react'; import { detectEmoji } from '../lib/utils/detectEmoji'; import { fireGlobalEvent } from '../lib/utils/fireGlobalEvent'; +import { useMessageListHighlights } from './message/list/MessageListContext'; import { useUserCard } from '../views/room/contexts/UserCardContext'; import { useGoToRoom } from '../views/room/hooks/useGoToRoom'; -import { useMessageListHighlights } from './message/list/MessageListContext'; type GazzodownTextProps = { children: JSX.Element; @@ -54,7 +54,7 @@ const GazzodownText = ({ mentions, channels, searchText, children }: GazzodownTe const convertAsciiToEmoji = useUserPreference('convertAsciiEmoji', true); const useEmoji = Boolean(useUserPreference('useEmojis')); - const useRealName = Boolean(useSetting('UI_Use_Real_Name')); + const useRealName = useSetting('UI_Use_Real_Name', false); const ownUserId = useUserId(); const showMentionSymbol = Boolean(useUserPreference('mentionsWithSymbol')); diff --git a/apps/meteor/client/components/GenericModal/GenericModal.spec.tsx b/apps/meteor/client/components/GenericModal/GenericModal.spec.tsx index b47b6abf7b00..aa220edcc651 100644 --- a/apps/meteor/client/components/GenericModal/GenericModal.spec.tsx +++ b/apps/meteor/client/components/GenericModal/GenericModal.spec.tsx @@ -4,8 +4,8 @@ import userEvent from '@testing-library/user-event'; import type { ReactElement } from 'react'; import React, { Suspense } from 'react'; -import ModalProviderWithRegion from '../../providers/ModalProvider/ModalProviderWithRegion'; import GenericModal from './GenericModal'; +import ModalProviderWithRegion from '../../providers/ModalProvider/ModalProviderWithRegion'; const renderModal = (modalElement: ReactElement) => { const { diff --git a/apps/meteor/client/components/GenericModal/GenericModal.tsx b/apps/meteor/client/components/GenericModal/GenericModal.tsx index 409855e8c0ea..fd4df58bf63e 100644 --- a/apps/meteor/client/components/GenericModal/GenericModal.tsx +++ b/apps/meteor/client/components/GenericModal/GenericModal.tsx @@ -116,7 +116,7 @@ const GenericModal = ({ {onClose && } {children} - + {dontAskAgain} {annotation && !dontAskAgain && {annotation}} diff --git a/apps/meteor/client/components/GenericTable/GenericTable.tsx b/apps/meteor/client/components/GenericTable/GenericTable.tsx index 05173fbe7004..dcefd8de12d8 100644 --- a/apps/meteor/client/components/GenericTable/GenericTable.tsx +++ b/apps/meteor/client/components/GenericTable/GenericTable.tsx @@ -1,6 +1,6 @@ import { Box, Table } from '@rocket.chat/fuselage'; -import type { ComponentProps } from 'react'; -import React, { type ForwardedRef, type ReactNode, forwardRef } from 'react'; +import type { ComponentProps, ForwardedRef, ReactNode } from 'react'; +import React, { forwardRef } from 'react'; import { CustomScrollbars } from '../CustomScrollbars'; diff --git a/apps/meteor/client/components/GenericUpsellModal/hooks/useUpsellActions.ts b/apps/meteor/client/components/GenericUpsellModal/hooks/useUpsellActions.ts index 2a51a53b09bf..818d553e74d2 100644 --- a/apps/meteor/client/components/GenericUpsellModal/hooks/useUpsellActions.ts +++ b/apps/meteor/client/components/GenericUpsellModal/hooks/useUpsellActions.ts @@ -10,7 +10,7 @@ const TALK_TO_SALES_URL = 'https://go.rocket.chat/i/contact-sales'; export const useUpsellActions = (hasLicenseModule = false) => { const setModal = useSetModal(); const handleOpenLink = useExternalLink(); - const cloudWorkspaceHadTrial = useSetting('Cloud_Workspace_Had_Trial'); + const cloudWorkspaceHadTrial = useSetting('Cloud_Workspace_Had_Trial', false); const { data } = useIsEnterprise(); const shouldShowUpsell = !data?.isEnterprise || !hasLicenseModule; diff --git a/apps/meteor/client/components/Header/HeaderToolbarAction.tsx b/apps/meteor/client/components/Header/HeaderToolbarAction.tsx index bbf296ff23e1..720102b17574 100644 --- a/apps/meteor/client/components/Header/HeaderToolbarAction.tsx +++ b/apps/meteor/client/components/Header/HeaderToolbarAction.tsx @@ -8,20 +8,19 @@ import { import type { ComponentProps } from 'react'; import React, { forwardRef, memo } from 'react'; -const HeaderToolbarAction = forwardRef>(function HeaderToolbarAction( - props, - ref, -) { - return ( - - - - - - - - - ); -}); +const HeaderToolbarAction = forwardRef>( + function HeaderToolbarAction(props, ref) { + return ( + + + + + + + + + ); + }, +); export default memo(HeaderToolbarAction); diff --git a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx index e04bc003cf9b..214b76667b0b 100644 --- a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx +++ b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx @@ -5,15 +5,12 @@ import React, { useRef, useState } from 'react'; import { FocusScope } from 'react-aria'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; -import { Keyboard, Navigation, Zoom, A11y } from 'swiper'; -import type { SwiperClass, SwiperRef } from 'swiper/react'; -import { Swiper, SwiperSlide } from 'swiper/react'; +import { Navigation, Zoom, Keyboard, A11y } from 'swiper/modules/index.mjs'; +import type { SwiperClass, SwiperRef } from 'swiper/swiper-react'; +import { Swiper, SwiperSlide } from 'swiper/swiper-react.mjs'; -// Import Swiper styles import 'swiper/swiper.css'; -import 'swiper/modules/navigation/navigation.min.css'; -import 'swiper/modules/keyboard/keyboard.min.css'; -import 'swiper/modules/zoom/zoom.min.css'; +import 'swiper/modules/zoom.css'; import { usePreventPropagation } from '../../hooks/usePreventPropagation'; @@ -172,10 +169,10 @@ export const ImageGallery = ({ images, onClose, loadMore }: { images: IUpload[]; zoom={{ toggle: false }} lazyPreloaderClass='rcx-lazy-preloader' runCallbacksOnInit - onKeyPress={(_, keyCode) => String(keyCode) === '27' && onClose()} + onKeyPress={(_: SwiperClass, keyCode: string) => String(keyCode) === '27' && onClose()} modules={[Navigation, Zoom, Keyboard, A11y]} - onInit={(swiper) => setSwiperInst(swiper)} - onSlidesGridLengthChange={(swiper) => { + onInit={(swiper: SwiperClass) => setSwiperInst(swiper)} + onSlidesGridLengthChange={(swiper: SwiperClass) => { swiper.slideTo(images.length - gridSize, 0); setGridSize(images.length); }} diff --git a/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx b/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx index 5727c355cc08..d40884e60ba8 100644 --- a/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx +++ b/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx @@ -13,8 +13,8 @@ import { InfoPanelText, InfoPanelTitle, } from '.'; -import { createFakeRoom } from '../../../tests/mocks/data'; import RetentionPolicyCallout from './RetentionPolicyCallout'; +import { createFakeRoom } from '../../../tests/mocks/data'; export default { title: 'Info Panel/InfoPanel', diff --git a/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.spec.tsx b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.spec.tsx index a9305b5f1f15..332c70f26dd4 100644 --- a/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.spec.tsx +++ b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.spec.tsx @@ -1,9 +1,9 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; +import RetentionPolicyCallout from './RetentionPolicyCallout'; import { createRenteionPolicySettingsMock as createMock } from '../../../tests/mocks/client/mockRetentionPolicySettings'; import { createFakeRoom } from '../../../tests/mocks/data'; -import RetentionPolicyCallout from './RetentionPolicyCallout'; jest.useFakeTimers(); diff --git a/apps/meteor/client/components/MarkdownText.tsx b/apps/meteor/client/components/MarkdownText.tsx index a116ad83ddd9..7f13c7e469c4 100644 --- a/apps/meteor/client/components/MarkdownText.tsx +++ b/apps/meteor/client/components/MarkdownText.tsx @@ -123,7 +123,7 @@ const MarkdownText = ({ // We are using the old emoji parser here. This could come // with additional processing use, but is the workaround available right now. // Should be replaced in the future with the new parser. - return renderMessageEmoji({ html: markedHtml }); + return renderMessageEmoji(markedHtml); } return markedHtml; @@ -132,7 +132,7 @@ const MarkdownText = ({ // Add a hook to make all external links open a new window dompurify.addHook('afterSanitizeAttributes', (node) => { - if ('target' in node) { + if (isElement(node) && 'target' in node) { const href = node.getAttribute('href') || ''; node.setAttribute('title', `${t('Go_to_href', { href: href.replace(getBaseURI(), '') })}`); @@ -157,4 +157,6 @@ const MarkdownText = ({ ) : null; }; +const isElement = (node: Node): node is Element => node.nodeType === Node.ELEMENT_NODE; + export default MarkdownText; diff --git a/apps/meteor/client/components/NotFoundState.tsx b/apps/meteor/client/components/NotFoundState.tsx index 7140a0f67b53..7d3c0bcf9386 100644 --- a/apps/meteor/client/components/NotFoundState.tsx +++ b/apps/meteor/client/components/NotFoundState.tsx @@ -1,7 +1,8 @@ import { Box, States, StatesAction, StatesActions, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage'; -import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; +import { useRouter } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; type NotFoundProps = { title: string; @@ -9,7 +10,7 @@ type NotFoundProps = { }; const NotFoundState = ({ title, subtitle }: NotFoundProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const router = useRouter(); const handleGoHomeClick = () => { diff --git a/apps/meteor/client/components/Omnichannel/OmnichannelSortingDisclaimer.tsx b/apps/meteor/client/components/Omnichannel/OmnichannelSortingDisclaimer.tsx index 89e1a508b368..1d4827aedd79 100644 --- a/apps/meteor/client/components/Omnichannel/OmnichannelSortingDisclaimer.tsx +++ b/apps/meteor/client/components/Omnichannel/OmnichannelSortingDisclaimer.tsx @@ -1,13 +1,14 @@ import { OmnichannelSortingMechanismSettingType as OmniSortingType } from '@rocket.chat/core-typings'; -import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetting } from '@rocket.chat/ui-contexts'; import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useOmnichannelEnterpriseEnabled } from '../../hooks/omnichannel/useOmnichannelEnterpriseEnabled'; export const useOmnichannelSortingDisclaimer = () => { const isOmnichannelEnabled = useOmnichannelEnterpriseEnabled(); - const sortingMechanism = useSetting('Omnichannel_sorting_mechanism') || OmniSortingType.Timestamp; + const sortingMechanism = useSetting('Omnichannel_sorting_mechanism', OmniSortingType.Timestamp); const [{ [sortingMechanism]: type }] = useState({ [OmniSortingType.Priority]: 'Priorities', @@ -19,7 +20,7 @@ export const useOmnichannelSortingDisclaimer = () => { }; export const OmnichannelSortingDisclaimer = () => { - const t = useTranslation(); + const { t } = useTranslation(); const type = useOmnichannelSortingDisclaimer(); diff --git a/apps/meteor/client/components/Omnichannel/Tags.tsx b/apps/meteor/client/components/Omnichannel/Tags.tsx index 4d7c41dda439..d96c560b9e58 100644 --- a/apps/meteor/client/components/Omnichannel/Tags.tsx +++ b/apps/meteor/client/components/Omnichannel/Tags.tsx @@ -1,12 +1,13 @@ import { TextInput, Chip, Button, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import type { ChangeEvent, ReactElement } from 'react'; import React, { useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; -import { CurrentChatTags } from '../../views/omnichannel/additionalForms'; import { FormSkeleton } from './Skeleton'; import { useLivechatTags } from './hooks/useLivechatTags'; +import { CurrentChatTags } from '../../views/omnichannel/additionalForms'; type TagsProps = { tags?: string[]; @@ -17,7 +18,7 @@ type TagsProps = { }; const Tags = ({ tags = [], handler, error, tagRequired, department }: TagsProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const { data: tagsResult, isInitialLoading } = useLivechatTags({ department, diff --git a/apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts b/apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts index ef00a6c0b81d..630ba5f0116d 100644 --- a/apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts +++ b/apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts @@ -1,5 +1,6 @@ -import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useScrollableRecordList } from '../../../hooks/lists/useScrollableRecordList'; import { useComponentDidUpdate } from '../../../hooks/useComponentDidUpdate'; @@ -24,7 +25,7 @@ export const useAgentsList = ( reload: () => void; loadMoreItems: (start: number, end: number) => void; } => { - const t = useTranslation(); + const { t } = useTranslation(); const [itemsList, setItemsList] = useState(() => new RecordList()); const reload = useCallback(() => setItemsList(new RecordList()), []); diff --git a/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts b/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts index d8e1071bf509..ac658e799ed6 100644 --- a/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts +++ b/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts @@ -1,5 +1,6 @@ -import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useScrollableRecordList } from '../../../hooks/lists/useScrollableRecordList'; import { useComponentDidUpdate } from '../../../hooks/useComponentDidUpdate'; @@ -27,7 +28,7 @@ export const useDepartmentsList = ( reload: () => void; loadMoreItems: (start: number, end: number) => void; } => { - const t = useTranslation(); + const { t } = useTranslation(); const [itemsList, setItemsList] = useState(() => new RecordList()); const reload = useCallback(() => setItemsList(new RecordList()), []); diff --git a/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx b/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx index 489eb4a04602..887ad9355f46 100644 --- a/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx @@ -51,9 +51,9 @@ const CloseChatModal = ({ watch, } = useForm(); - const commentRequired = useSetting('Livechat_request_comment_when_closing_conversation') as boolean; - const alwaysSendTranscript = useSetting('Livechat_transcript_send_always'); - const customSubject = useSetting('Livechat_transcript_email_subject'); + const commentRequired = useSetting('Livechat_request_comment_when_closing_conversation', true); + const alwaysSendTranscript = useSetting('Livechat_transcript_send_always', false); + const customSubject = useSetting('Livechat_transcript_email_subject', ''); const [tagRequired, setTagRequired] = useState(false); const tags = watch('tags'); diff --git a/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx b/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx index 713dea4a0ed6..ccf57368475e 100644 --- a/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/EnterpriseDepartmentsModal.tsx @@ -1,14 +1,15 @@ import { Button, Modal, Box } from '@rocket.chat/fuselage'; import { useOutsideClick } from '@rocket.chat/fuselage-hooks'; -import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; +import { useRouter } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useRef } from 'react'; +import { useTranslation } from 'react-i18next'; import { useExternalLink } from '../../../hooks/useExternalLink'; import { useCheckoutUrl } from '../../../views/admin/subscription/hooks/useCheckoutUrl'; const EnterpriseDepartmentsModal = ({ closeModal }: { closeModal: () => void }): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const router = useRouter(); const ref = useRef(null); diff --git a/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx b/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx index 4a937b485f36..7dbb288b1697 100644 --- a/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx @@ -13,10 +13,11 @@ import { Option, } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import { useEndpoint, useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useSetting } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; import { useRecordList } from '../../../hooks/lists/useRecordList'; import { AsyncStatePhase } from '../../../hooks/useAsyncState'; @@ -33,9 +34,9 @@ const ForwardChatModal = ({ onCancel: () => void; room: IOmnichannelRoom; }): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const getUserData = useEndpoint('GET', '/v1/users.info'); - const idleAgentsAllowedForForwarding = useSetting('Livechat_enabled_when_agent_idle') as boolean; + const idleAgentsAllowedForForwarding = useSetting('Livechat_enabled_when_agent_idle', true); const { getValues, diff --git a/apps/meteor/client/components/Page/PageBlock.tsx b/apps/meteor/client/components/Page/PageBlock.tsx new file mode 100644 index 000000000000..a2102b7a3708 --- /dev/null +++ b/apps/meteor/client/components/Page/PageBlock.tsx @@ -0,0 +1,11 @@ +import type { Box } from '@rocket.chat/fuselage'; +import type { ComponentProps } from 'react'; +import React, { forwardRef } from 'react'; + +import PageContent from './PageContent'; + +const PageBlock = forwardRef>(function PageBlock(props, ref) { + return ; +}); + +export default PageBlock; diff --git a/apps/meteor/client/components/Page/PageBlockWithBorder.tsx b/apps/meteor/client/components/Page/PageBlockWithBorder.tsx index 8176f6734bfd..2bf0fab3068f 100644 --- a/apps/meteor/client/components/Page/PageBlockWithBorder.tsx +++ b/apps/meteor/client/components/Page/PageBlockWithBorder.tsx @@ -2,21 +2,12 @@ import type { Box } from '@rocket.chat/fuselage'; import type { ComponentProps } from 'react'; import React, { forwardRef, useContext } from 'react'; -import PageContent from './PageContent'; +import PageBlock from './PageBlock'; import PageContext from './PageContext'; const PageBlockWithBorder = forwardRef>(function PageBlockWithBorder(props, ref) { const [border] = useContext(PageContext); - return ( - - ); + return ; }); export default PageBlockWithBorder; diff --git a/apps/meteor/client/components/Page/PageHeader.tsx b/apps/meteor/client/components/Page/PageHeader.tsx index c6667e4fc5cc..8a079903884f 100644 --- a/apps/meteor/client/components/Page/PageHeader.tsx +++ b/apps/meteor/client/components/Page/PageHeader.tsx @@ -1,56 +1,24 @@ -import { Box, IconButton } from '@rocket.chat/fuselage'; -import { useDocumentTitle } from '@rocket.chat/ui-client'; -import { useLayout, useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentPropsWithoutRef, ReactNode } from 'react'; import React, { useContext } from 'react'; -import { HeaderToolbar } from '../Header'; -import SidebarToggler from '../SidebarToggler'; import PageContext from './PageContext'; +import PageHeaderNoShadow from './PageHeaderNoShadow'; type PageHeaderProps = { title: ReactNode; onClickBack?: () => void; borderBlockEndColor?: string; -} & Omit, 'title'>; +} & ComponentPropsWithoutRef; -const PageHeader = ({ children = undefined, title, onClickBack, borderBlockEndColor, ...props }: PageHeaderProps) => { - const t = useTranslation(); +const PageHeader = ({ borderBlockEndColor, ...props }: PageHeaderProps) => { const [border] = useContext(PageContext); - const { isMobile } = useLayout(); - - useDocumentTitle(typeof title === 'string' ? title : undefined); return ( - - - {isMobile && ( - - - - )} - {onClickBack && } - - {title} - - {children} - - + /> ); }; diff --git a/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx b/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx new file mode 100644 index 000000000000..51feca504834 --- /dev/null +++ b/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx @@ -0,0 +1,51 @@ +import { Box, IconButton } from '@rocket.chat/fuselage'; +import { useDocumentTitle } from '@rocket.chat/ui-client'; +import { useLayout } from '@rocket.chat/ui-contexts'; +import type { ComponentPropsWithoutRef, ReactNode } from 'react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { HeaderToolbar } from '../Header'; +import SidebarToggler from '../SidebarToggler'; + +type PageHeaderProps = { + title: ReactNode; + onClickBack?: () => void; + borderBlockEndColor?: string; +} & Omit, 'title'>; + +const PageHeaderNoShadow = ({ children = undefined, title, onClickBack, ...props }: PageHeaderProps) => { + const { t } = useTranslation(); + + const { isMobile } = useLayout(); + + useDocumentTitle(typeof title === 'string' ? title : undefined); + + return ( + + + {isMobile && ( + + + + )} + {onClickBack && } + + {title} + + {children} + + + ); +}; + +export default PageHeaderNoShadow; diff --git a/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx b/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx index 0da0e942b2e2..e3abc1d27219 100644 --- a/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx +++ b/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx @@ -51,7 +51,7 @@ const RoomAutoComplete = ({ value, onChange, scope = 'regular', renderRoomIcon, ? result.data.items.map(({ name, fname, _id, avatarETag, t, encrypted }) => ({ value: _id, label: { name: fname || name, avatarETag, type: t, encrypted }, - })) + })) : [], [result.data?.items, result.isSuccess], ); diff --git a/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx b/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx index 49006e025111..8fb1af477df6 100644 --- a/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx +++ b/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx @@ -31,7 +31,7 @@ const RoomAutoCompleteMultiple = ({ value, onChange, ...props }: RoomAutoComplet ? result.data.items.map(({ fname, name, _id, avatarETag, t }) => ({ value: _id, label: { name: fname || name, avatarETag, type: t }, - })) + })) : [], [result.data?.items, result.isSuccess], ); diff --git a/apps/meteor/client/components/RoomIcon/OmnichannelRoomIcon/OmnichannelAppSourceRoomIcon.tsx b/apps/meteor/client/components/RoomIcon/OmnichannelRoomIcon/OmnichannelAppSourceRoomIcon.tsx index 6a841c30491b..b6cc94007002 100644 --- a/apps/meteor/client/components/RoomIcon/OmnichannelRoomIcon/OmnichannelAppSourceRoomIcon.tsx +++ b/apps/meteor/client/components/RoomIcon/OmnichannelRoomIcon/OmnichannelAppSourceRoomIcon.tsx @@ -3,8 +3,8 @@ import { Icon, Box } from '@rocket.chat/fuselage'; import type { ComponentProps } from 'react'; import React from 'react'; -import { AsyncStatePhase } from '../../../lib/asyncState/AsyncStatePhase'; import { useOmnichannelRoomIcon } from './context/OmnichannelRoomIconContext'; +import { AsyncStatePhase } from '../../../lib/asyncState/AsyncStatePhase'; type OmnichannelAppSourceRoomIconProps = { source: IOmnichannelSourceFromApp; diff --git a/apps/meteor/client/components/RoomIcon/RoomIcon.tsx b/apps/meteor/client/components/RoomIcon/RoomIcon.tsx index d757071fedf5..12f4a13d1f33 100644 --- a/apps/meteor/client/components/RoomIcon/RoomIcon.tsx +++ b/apps/meteor/client/components/RoomIcon/RoomIcon.tsx @@ -4,8 +4,8 @@ import { Icon } from '@rocket.chat/fuselage'; import type { ComponentProps, ReactElement } from 'react'; import React, { isValidElement } from 'react'; -import { useRoomIcon } from '../../hooks/useRoomIcon'; import { OmnichannelRoomIcon } from './OmnichannelRoomIcon'; +import { useRoomIcon } from '../../hooks/useRoomIcon'; export const RoomIcon = ({ room, diff --git a/apps/meteor/client/components/Sidebar/SidebarItemsAssembler.tsx b/apps/meteor/client/components/Sidebar/SidebarItemsAssembler.tsx index b588e223d922..8507ff950dc4 100644 --- a/apps/meteor/client/components/Sidebar/SidebarItemsAssembler.tsx +++ b/apps/meteor/client/components/Sidebar/SidebarItemsAssembler.tsx @@ -2,9 +2,9 @@ import { Divider } from '@rocket.chat/fuselage'; import React, { Fragment, memo } from 'react'; import { useTranslation } from 'react-i18next'; +import SidebarNavigationItem from './SidebarNavigationItem'; import type { SidebarItem } from '../../lib/createSidebarItems'; import { isSidebarItem } from '../../lib/createSidebarItems'; -import SidebarNavigationItem from './SidebarNavigationItem'; type SidebarItemsAssemblerProps = { items: SidebarItem[]; diff --git a/apps/meteor/client/components/SidebarToggler/SidebarToggler.tsx b/apps/meteor/client/components/SidebarToggler/SidebarToggler.tsx index 2102621a35d7..1cdb993d433f 100644 --- a/apps/meteor/client/components/SidebarToggler/SidebarToggler.tsx +++ b/apps/meteor/client/components/SidebarToggler/SidebarToggler.tsx @@ -3,8 +3,8 @@ import { useLayout, useSession } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { memo } from 'react'; -import { useEmbeddedLayout } from '../../hooks/useEmbeddedLayout'; import SidebarTogglerButton from './SidebarTogglerButton'; +import { useEmbeddedLayout } from '../../hooks/useEmbeddedLayout'; const SideBarToggler = (): ReactElement => { const { sidebar } = useLayout(); diff --git a/apps/meteor/client/components/TwoFactorModal/TwoFactorEmailModal.tsx b/apps/meteor/client/components/TwoFactorModal/TwoFactorEmailModal.tsx index 9c7bd8191c92..1a8789bba3ee 100644 --- a/apps/meteor/client/components/TwoFactorModal/TwoFactorEmailModal.tsx +++ b/apps/meteor/client/components/TwoFactorModal/TwoFactorEmailModal.tsx @@ -1,8 +1,9 @@ import { Box, FieldGroup, TextInput, Field, FieldLabel, FieldRow, FieldError, Button } from '@rocket.chat/fuselage'; import { useAutoFocus, useUniqueId } from '@rocket.chat/fuselage-hooks'; -import { useToastMessageDispatch, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts'; import type { ReactElement, ChangeEvent, SyntheticEvent } from 'react'; import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import GenericModal from '../GenericModal'; import type { OnConfirm } from './TwoFactorModal'; @@ -17,7 +18,7 @@ type TwoFactorEmailModalProps = { const TwoFactorEmailModal = ({ onConfirm, onClose, emailOrUsername, invalidAttempt }: TwoFactorEmailModalProps): ReactElement => { const dispatchToastMessage = useToastMessageDispatch(); - const t = useTranslation(); + const { t } = useTranslation(); const [code, setCode] = useState(''); const ref = useAutoFocus(); diff --git a/apps/meteor/client/components/UrlChangeModal.tsx b/apps/meteor/client/components/UrlChangeModal.tsx index dbc152c7fbff..94233dfc7e18 100644 --- a/apps/meteor/client/components/UrlChangeModal.tsx +++ b/apps/meteor/client/components/UrlChangeModal.tsx @@ -26,7 +26,14 @@ const UrlChangeModal = ({ onConfirm, siteUrl, currentUrl, onClose }: UrlChangeMo }), }} /> -

+

); }; diff --git a/apps/meteor/client/components/UserInfo/UserInfoAvatar.tsx b/apps/meteor/client/components/UserInfo/UserInfoAvatar.tsx index c15992489264..f59355cfacbd 100644 --- a/apps/meteor/client/components/UserInfo/UserInfoAvatar.tsx +++ b/apps/meteor/client/components/UserInfo/UserInfoAvatar.tsx @@ -2,8 +2,6 @@ import { UserAvatar } from '@rocket.chat/ui-avatar'; import type { ComponentProps, ReactElement } from 'react'; import React from 'react'; -const UserInfoAvatar = ({ username, ...props }: ComponentProps): ReactElement => ( - -); +const UserInfoAvatar = (props: ComponentProps): ReactElement => ; export default UserInfoAvatar; diff --git a/apps/meteor/client/components/UserStatusMenu.tsx b/apps/meteor/client/components/UserStatusMenu.tsx index 8f2dd24d6017..f1c9afaa9b8b 100644 --- a/apps/meteor/client/components/UserStatusMenu.tsx +++ b/apps/meteor/client/components/UserStatusMenu.tsx @@ -1,8 +1,9 @@ import { UserStatus as UserStatusType } from '@rocket.chat/core-typings'; import { Button, PositionAnimated, Options, useCursor, Box } from '@rocket.chat/fuselage'; -import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetting } from '@rocket.chat/ui-contexts'; import type { ReactElement, ComponentProps } from 'react'; import React, { useRef, useCallback, useState, useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { UserStatus } from './UserStatus'; @@ -21,9 +22,9 @@ const UserStatusMenu = ({ optionWidth = undefined, placement = 'bottom-end', }: UserStatusMenuProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const [status, setStatus] = useState(initialStatus); - const allowInvisibleStatus = useSetting('Accounts_AllowInvisibleStatusOption') as boolean; + const allowInvisibleStatus = useSetting('Accounts_AllowInvisibleStatusOption', true); const options = useMemo(() => { const renderOption = (status: UserStatusType, label: string): ReactElement => ( diff --git a/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx b/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx index de4108d8f866..4eeb81405df3 100644 --- a/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx +++ b/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx @@ -4,9 +4,10 @@ import { css } from '@rocket.chat/css-in-js'; import { Box, Button, ButtonGroup } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { RoomAvatar } from '@rocket.chat/ui-avatar'; -import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { getAvatarURL } from '../../../app/utils/client/getAvatarURL'; import { useSingleFileInput } from '../../hooks/useSingleFileInput'; @@ -20,7 +21,7 @@ type RoomAvatarEditorProps = { }; const RoomAvatarEditor = ({ disabled = false, room, roomAvatar, onChangeAvatar }: RoomAvatarEditorProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const handleChangeAvatar = useMutableCallback(async (file) => { diff --git a/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.tsx b/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.tsx index 05d96d9536be..95659d762d30 100644 --- a/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.tsx +++ b/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.tsx @@ -2,15 +2,16 @@ import type { IUser, AvatarObject } from '@rocket.chat/core-typings'; import { Box, Button, Avatar, TextInput, IconButton, Label } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { UserAvatar } from '@rocket.chat/ui-avatar'; -import { useToastMessageDispatch, useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useSetting } from '@rocket.chat/ui-contexts'; import type { ReactElement, ChangeEvent } from 'react'; import React, { useState, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; -import { useSingleFileInput } from '../../../hooks/useSingleFileInput'; -import { isValidImageFormat } from '../../../lib/utils/isValidImageFormat'; import type { UserAvatarSuggestion } from './UserAvatarSuggestion'; import UserAvatarSuggestions from './UserAvatarSuggestions'; import { readFileAsDataURL } from './readFileAsDataURL'; +import { useSingleFileInput } from '../../../hooks/useSingleFileInput'; +import { isValidImageFormat } from '../../../lib/utils/isValidImageFormat'; type UserAvatarEditorProps = { currentUsername: IUser['username']; @@ -21,7 +22,7 @@ type UserAvatarEditorProps = { }; function UserAvatarEditor({ currentUsername, username, setAvatarObj, disabled, etag }: UserAvatarEditorProps): ReactElement { - const t = useTranslation(); + const { t } = useTranslation(); const rotateImages = useSetting('FileUpload_RotateImages'); const [avatarFromUrl, setAvatarFromUrl] = useState(''); const [newAvatarSource, setNewAvatarSource] = useState(); diff --git a/apps/meteor/client/components/connectionStatus/ConnectionStatusBar.stories.tsx b/apps/meteor/client/components/connectionStatus/ConnectionStatusBar.stories.tsx index 4f3da8b3f36f..3829d946784c 100644 --- a/apps/meteor/client/components/connectionStatus/ConnectionStatusBar.stories.tsx +++ b/apps/meteor/client/components/connectionStatus/ConnectionStatusBar.stories.tsx @@ -14,8 +14,9 @@ export default { }, } satisfies Meta; -const stateDecorator = (value: ContextType) => (fn: () => ReactElement) => - {fn()}; +const stateDecorator = (value: ContextType) => (fn: () => ReactElement) => ( + {fn()} +); const Template: StoryFn = () => ; diff --git a/apps/meteor/client/components/dashboards/DownloadDataButton.tsx b/apps/meteor/client/components/dashboards/DownloadDataButton.tsx index 2edaf80237e3..84ed48c86f01 100644 --- a/apps/meteor/client/components/dashboards/DownloadDataButton.tsx +++ b/apps/meteor/client/components/dashboards/DownloadDataButton.tsx @@ -1,8 +1,9 @@ import type { Box } from '@rocket.chat/fuselage'; import { IconButton } from '@rocket.chat/fuselage'; -import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { downloadCsvAs } from '../../lib/download'; @@ -24,7 +25,7 @@ const DownloadDataButton = ({ dataExtractor, ...props }: DownloadDataButtonProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const handleClick = (): void => { diff --git a/apps/meteor/client/components/dataView/Growth.stories.tsx b/apps/meteor/client/components/dataView/Growth.stories.tsx index 11092d52f993..28f52cd01f3f 100644 --- a/apps/meteor/client/components/dataView/Growth.stories.tsx +++ b/apps/meteor/client/components/dataView/Growth.stories.tsx @@ -2,8 +2,8 @@ import { Box } from '@rocket.chat/fuselage'; import type { Meta, StoryFn } from '@storybook/react'; import React from 'react'; -import { useAutoSequence } from '../../stories/hooks/useAutoSequence'; import Growth from './Growth'; +import { useAutoSequence } from '../../stories/hooks/useAutoSequence'; export default { title: 'Components/Data/Growth', diff --git a/apps/meteor/client/components/dataView/NegativeGrowthSymbol.stories.tsx b/apps/meteor/client/components/dataView/NegativeGrowthSymbol.stories.tsx index 9c5ca3f4d44d..19be0dde4af6 100644 --- a/apps/meteor/client/components/dataView/NegativeGrowthSymbol.stories.tsx +++ b/apps/meteor/client/components/dataView/NegativeGrowthSymbol.stories.tsx @@ -2,8 +2,8 @@ import { Box } from '@rocket.chat/fuselage'; import type { Meta, StoryFn } from '@storybook/react'; import React from 'react'; -import { useAutoSequence } from '../../stories/hooks/useAutoSequence'; import NegativeGrowthSymbol from './NegativeGrowthSymbol'; +import { useAutoSequence } from '../../stories/hooks/useAutoSequence'; export default { title: 'Components/Data/NegativeGrowthSymbol', diff --git a/apps/meteor/client/components/dataView/PositiveGrowthSymbol.stories.tsx b/apps/meteor/client/components/dataView/PositiveGrowthSymbol.stories.tsx index 8f08dfee9c0e..603c97edf788 100644 --- a/apps/meteor/client/components/dataView/PositiveGrowthSymbol.stories.tsx +++ b/apps/meteor/client/components/dataView/PositiveGrowthSymbol.stories.tsx @@ -2,8 +2,8 @@ import { Box } from '@rocket.chat/fuselage'; import type { Meta, StoryFn } from '@storybook/react'; import React from 'react'; -import { useAutoSequence } from '../../stories/hooks/useAutoSequence'; import PositiveGrowthSymbol from './PositiveGrowthSymbol'; +import { useAutoSequence } from '../../stories/hooks/useAutoSequence'; export default { title: 'Components/Data/PositiveGrowthSymbol', diff --git a/apps/meteor/client/components/message/MessageHeader.tsx b/apps/meteor/client/components/message/MessageHeader.tsx index 21a4d8484f8f..e2d7fb6f2669 100644 --- a/apps/meteor/client/components/message/MessageHeader.tsx +++ b/apps/meteor/client/components/message/MessageHeader.tsx @@ -11,16 +11,16 @@ import type { KeyboardEvent, ReactElement } from 'react'; import React, { memo } from 'react'; import { useTranslation } from 'react-i18next'; +import StatusIndicators from './StatusIndicators'; +import MessageRoles from './header/MessageRoles'; +import { useMessageListShowUsername, useMessageListShowRealName, useMessageListShowRoles } from './list/MessageListContext'; import { getUserDisplayName } from '../../../lib/getUserDisplayName'; import { useFormatDateAndTime } from '../../hooks/useFormatDateAndTime'; import { useFormatTime } from '../../hooks/useFormatTime'; import { useUserData } from '../../hooks/useUserData'; import type { UserPresence } from '../../lib/presence'; -import { useUserCard } from '../../views/room/contexts/UserCardContext'; -import StatusIndicators from './StatusIndicators'; -import MessageRoles from './header/MessageRoles'; import { useMessageRoles } from './header/hooks/useMessageRoles'; -import { useMessageListShowUsername, useMessageListShowRealName, useMessageListShowRoles } from './list/MessageListContext'; +import { useUserCard } from '../../views/room/contexts/UserCardContext'; type MessageHeaderProps = { message: IMessage; diff --git a/apps/meteor/client/components/message/StatusIndicators.tsx b/apps/meteor/client/components/message/StatusIndicators.tsx index eb47fb040b21..b0fd17adbd26 100644 --- a/apps/meteor/client/components/message/StatusIndicators.tsx +++ b/apps/meteor/client/components/message/StatusIndicators.tsx @@ -1,9 +1,10 @@ import type { IMessage, ITranslatedMessage } from '@rocket.chat/core-typings'; import { isEditedMessage, isE2EEMessage, isOTRMessage, isOTRAckMessage, isE2EEPinnedMessage } from '@rocket.chat/core-typings'; import { MessageStatusIndicator, MessageStatusIndicatorItem } from '@rocket.chat/fuselage'; -import { useUserId, useTranslation } from '@rocket.chat/ui-contexts'; +import { useUserId } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { useMessageDateFormatter, useShowStarred, useShowTranslated, useShowFollowing } from './list/MessageListContext'; @@ -12,7 +13,7 @@ type StatusIndicatorsProps = { }; const StatusIndicators = ({ message }: StatusIndicatorsProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const translated = useShowTranslated(message); const starred = useShowStarred({ message }); const following = useShowFollowing({ message }); @@ -41,7 +42,7 @@ const StatusIndicators = ({ message }: StatusIndicatorsProps): ReactElement => { : t('Message_has_been_edited_by_at', { username: message.editedBy.username || '?', date: formatter(message.editedAt), - }) + }) } /> )} diff --git a/apps/meteor/client/components/message/content/Attachments.tsx b/apps/meteor/client/components/message/content/Attachments.tsx index ea9c03b9e7d3..365d80572c49 100644 --- a/apps/meteor/client/components/message/content/Attachments.tsx +++ b/apps/meteor/client/components/message/content/Attachments.tsx @@ -10,13 +10,7 @@ type AttachmentsProps = { }; const Attachments = ({ attachments, id }: AttachmentsProps): ReactElement => { - return ( - <> - {attachments?.map((attachment, index) => ( - - ))} - - ); + return <>{attachments?.map((attachment, index) => )}; }; export default Attachments; diff --git a/apps/meteor/client/components/message/content/MessageActions.tsx b/apps/meteor/client/components/message/content/MessageActions.tsx index d38fbb6c64df..3700067a7272 100644 --- a/apps/meteor/client/components/message/content/MessageActions.tsx +++ b/apps/meteor/client/components/message/content/MessageActions.tsx @@ -6,8 +6,8 @@ import type { TranslationKey } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import { actionLinks } from '../../../lib/actionLinks'; import MessageAction from './actions/MessageAction'; +import { actionLinks } from '../../../lib/actionLinks'; type MessageActionOptions = { icon: IconName; diff --git a/apps/meteor/client/components/message/content/ThreadMetrics.spec.tsx b/apps/meteor/client/components/message/content/ThreadMetrics.spec.tsx new file mode 100644 index 000000000000..22e5e382fd74 --- /dev/null +++ b/apps/meteor/client/components/message/content/ThreadMetrics.spec.tsx @@ -0,0 +1,326 @@ +import { mockAppRoot, MockedRouterContext } from '@rocket.chat/mock-providers'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import type { ReactNode } from 'react'; +import React from 'react'; + +import ThreadMetrics from './ThreadMetrics'; +import ThreadMetricsFollow from './ThreadMetricsFollow'; +import ThreadMetricsParticipants from './ThreadMetricsParticipants'; + +const toggleFollowMock = + (done: jest.DoneCallback | (() => undefined)) => + ({ mid }: { mid: string }) => { + expect(mid).toBe('mid'); + done(); + return null; + }; + +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); + +const mockRoot = () => { + const AppRoot = mockAppRoot(); + const buildWithRouter = (navigate: (...args: any[]) => void) => { + const Wrapper = AppRoot.build(); + return function Mock({ children }: { children: ReactNode }) { + return ( + + 'thread' as any }}>{children} + + ); + }; + }; + + return Object.assign(AppRoot, { buildWithRouter }); +}; + +const mockedTranslations = [ + 'en', + 'core', + { + Follower_one: 'follower', + Follower_other: 'followers', + __count__replies__date__: '{{count}} replies {{date}}', + __count__replies: '{{count}} replies', + }, +] as const; + +let inlineSize = 400; +jest.mock('@rocket.chat/fuselage-hooks', () => { + const originalModule = jest.requireActual('@rocket.chat/fuselage-hooks'); + return { + ...originalModule, + useResizeObserver: () => ({ ref: () => undefined, borderBoxSize: { inlineSize } }), + }; +}); + +describe('Thread Metrics', () => { + describe('Main component', () => { + it('should render large followed with 3 participants and unread', async () => { + const navigateSpy = jest.fn(); + const navigateCallback = (route: any) => { + navigateSpy(route.name, route.params.rid, route.params.tab, route.params.context); + }; + + render( + , + { + wrapper: mockRoot() + .withEndpoint( + 'POST', + '/v1/chat.followMessage', + toggleFollowMock(() => undefined), + ) + .withEndpoint( + 'POST', + '/v1/chat.unfollowMessage', + toggleFollowMock(() => undefined), + ) + .withUserPreference('clockMode', 1) + .withSetting('Message_TimeFormat', 'LT') + .withTranslations(...mockedTranslations) + .buildWithRouter(navigateCallback), + legacyRoot: true, + }, + ); + + const followButton = screen.getByTitle('Following'); + expect(followButton).toBeVisible(); + + const badge = screen.getByTitle('Unread'); + expect(badge).toBeVisible(); + + expect(screen.getByTitle('followers')).toBeVisible(); + expect(screen.getByText('3')).toBeVisible(); + + const replyButton = screen.getByText('View_thread'); + expect(replyButton).toBeVisible(); + await userEvent.click(replyButton); + + expect(navigateSpy).toHaveBeenCalledWith('thread', 'rid', 'thread', 'mid'); + + const threadCount = screen.getByTitle('Last_message__date__'); + expect(threadCount).toHaveTextContent('5 replies July 1, 2024'); + }); + + it('should render small not followed with 3 participants and unread', async () => { + const navigateSpy = jest.fn(); + const navigateCallback = (route: any) => { + navigateSpy(route.name, route.params.rid, route.params.tab, route.params.context); + }; + inlineSize = 200; + + render( + , + { + wrapper: mockRoot() + .withEndpoint( + 'POST', + '/v1/chat.followMessage', + toggleFollowMock(() => undefined), + ) + .withEndpoint( + 'POST', + '/v1/chat.unfollowMessage', + toggleFollowMock(() => undefined), + ) + .withUserPreference('clockMode', 1) + .withSetting('Message_TimeFormat', 'LT') + .withTranslations(...mockedTranslations) + .buildWithRouter(navigateCallback), + legacyRoot: true, + }, + ); + const followButton = screen.getByTitle('Not_following'); + expect(followButton).toBeVisible(); + + const badge = screen.getByTitle('Unread'); + expect(badge).toBeVisible(); + + expect(screen.getByTitle('followers')).toBeVisible(); + expect(screen.getByText('3')).toBeVisible(); + + const replyButton = screen.getByText('View_thread'); + expect(replyButton).toBeVisible(); + await userEvent.click(replyButton); + + expect(navigateSpy).toHaveBeenCalledWith('thread', 'rid', 'thread', 'mid'); + + const threadCount = screen.getByTitle('Last_message__date__'); + expect(threadCount).toHaveTextContent('5 replies'); + }); + }); + + describe('ThreadMetricsFollow', () => { + it('should render not followed', async () => { + render(, { + wrapper: mockAppRoot() + .withEndpoint( + 'POST', + '/v1/chat.followMessage', + toggleFollowMock(() => undefined), + ) + .build(), + legacyRoot: true, + }); + const followButton = screen.getByTitle('Not_following'); + expect(followButton).toBeVisible(); + await userEvent.click(followButton); + }); + it('should render followed', async () => { + render(, { + wrapper: mockAppRoot() + .withEndpoint( + 'POST', + '/v1/chat.unfollowMessage', + toggleFollowMock(() => undefined), + ) + .build(), + legacyRoot: true, + }); + const followButton = screen.getByTitle('Following'); + expect(followButton).toBeVisible(); + await userEvent.click(followButton); + }); + it('should render unread badge', () => { + render(, { + wrapper: mockAppRoot().build(), + legacyRoot: true, + }); + const badge = screen.getByTitle('Unread'); + expect(badge).toBeVisible(); + }); + it('should render mention-all badge', () => { + render(, { + wrapper: mockAppRoot().build(), + legacyRoot: true, + }); + const badge = screen.getByTitle('mention-all'); + expect(badge).toBeVisible(); + }); + it('should render Mentions_you badge', () => { + render(, { + wrapper: mockAppRoot().build(), + legacyRoot: true, + }); + const badge = screen.getByTitle('Mentions_you'); + expect(badge).toBeVisible(); + }); + }); + describe('ThreadMetricsParticipants', () => { + it('should render 1 avatars', () => { + render(, { + wrapper: mockAppRoot() + .withUserPreference('displayAvatars', true) + .withTranslations(...mockedTranslations) + .build(), + legacyRoot: true, + }); + expect(screen.getByTitle('follower')).toBeVisible(); + const avatars = screen.getAllByRole('figure'); + expect(avatars.length).toBe(1); + expect(avatars.pop()).toBeVisible(); + }); + it('should render 2 avatars', () => { + render(, { + wrapper: mockAppRoot() + .withUserPreference('displayAvatars', true) + .withTranslations(...mockedTranslations) + .build(), + legacyRoot: true, + }); + expect(screen.getByTitle('followers')).toBeVisible(); + const avatars = screen.getAllByRole('figure'); + expect(avatars.length).toBe(2); + avatars.forEach((avatar) => expect(avatar).toBeVisible()); + }); + it('should render 2 avatars and "+1" text', () => { + render(, { + wrapper: mockAppRoot() + .withUserPreference('displayAvatars', true) + .withTranslations(...mockedTranslations) + .build(), + legacyRoot: true, + }); + expect(screen.getByTitle('followers')).toBeVisible(); + const avatars = screen.getAllByRole('figure'); + expect(avatars.length).toBe(2); + avatars.forEach((avatar) => expect(avatar).toBeVisible()); + expect(screen.getByText('+1')).toBeVisible(); + }); + it('should render 2 avatars and "+5" text', () => { + render(, { + wrapper: mockAppRoot() + .withUserPreference('displayAvatars', true) + .withTranslations(...mockedTranslations) + .build(), + legacyRoot: true, + }); + expect(screen.getByTitle('followers')).toBeVisible(); + + const avatars = screen.getAllByRole('figure'); + expect(avatars.length).toBe(2); + avatars.forEach((avatar) => expect(avatar).toBeVisible()); + + expect(screen.getByText('+5')).toBeVisible(); + }); + + it('should render user icon and 1 follower', () => { + render(, { + wrapper: mockAppRoot() + .withUserPreference('displayAvatars', false) + .withTranslations(...mockedTranslations) + .build(), + legacyRoot: true, + }); + const follower = screen.getByTitle('follower'); + expect(follower).toBeVisible(); + + // eslint-disable-next-line testing-library/no-node-access + expect(follower.querySelector('.rcx-icon--name-user')).toBeVisible(); + + expect(screen.getByText('1')).toBeVisible(); + }); + + it('should render user icon and 5 followers', () => { + render(, { + wrapper: mockAppRoot() + .withUserPreference('displayAvatars', false) + .withTranslations(...mockedTranslations) + .build(), + legacyRoot: true, + }); + const follower = screen.getByTitle('followers'); + expect(follower).toBeVisible(); + + // eslint-disable-next-line testing-library/no-node-access + expect(follower.querySelector('.rcx-icon--name-user')).toBeVisible(); + + expect(screen.getByText('5')).toBeVisible(); + }); + }); +}); diff --git a/apps/meteor/client/components/message/content/ThreadMetrics.tsx b/apps/meteor/client/components/message/content/ThreadMetrics.tsx index f3b6915a95dd..6d98b65650dd 100644 --- a/apps/meteor/client/components/message/content/ThreadMetrics.tsx +++ b/apps/meteor/client/components/message/content/ThreadMetrics.tsx @@ -1,15 +1,20 @@ -import { MessageMetricsItem, MessageBlock, MessageMetrics, MessageMetricsReply, MessageMetricsFollowing } from '@rocket.chat/fuselage'; -import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { + MessageMetricsItem, + MessageBlock, + MessageMetrics, + MessageMetricsReply, + MessageMetricsItemIcon, + MessageMetricsItemLabel, +} from '@rocket.chat/fuselage'; +import { useResizeObserver } from '@rocket.chat/fuselage-hooks'; import type { ReactElement } from 'react'; -import React, { useCallback } from 'react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import ThreadMetricsFollow from './ThreadMetricsFollow'; +import ThreadMetricsParticipants from './ThreadMetricsParticipants'; import { useTimeAgo } from '../../../hooks/useTimeAgo'; -import { useToggleFollowingThreadMutation } from '../../../views/room/contextualBar/Threads/hooks/useToggleFollowingThreadMutation'; import { useGoToThread } from '../../../views/room/hooks/useGoToThread'; -import { followStyle, anchor } from '../helpers/followSyle'; -import AllMentionNotification from '../notification/AllMentionNotification'; -import MeMentionNotification from '../notification/MeMentionNotification'; -import UnreadMessagesNotification from '../notification/UnreadMessagesNotification'; type ThreadMetricsProps = { unread: boolean; @@ -19,62 +24,44 @@ type ThreadMetricsProps = { mid: string; rid: string; counter: number; - participants: number; + participants: string[]; following: boolean; }; const ThreadMetrics = ({ unread, mention, all, rid, mid, counter, participants, following, lm }: ThreadMetricsProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const format = useTimeAgo(); const goToThread = useGoToThread(); - const dispatchToastMessage = useToastMessageDispatch(); - const toggleFollowingThreadMutation = useToggleFollowingThreadMutation({ - onError: (error) => { - dispatchToastMessage({ type: 'error', message: error }); - }, - }); + const { ref, borderBoxSize } = useResizeObserver(); - const handleFollow = useCallback(() => { - toggleFollowingThreadMutation.mutate({ rid, tmid: mid, follow: !following }); - }, [following, rid, mid, toggleFollowingThreadMutation]); + const isSmall = (borderBoxSize.inlineSize || Infinity) < 320; return ( - + - goToThread({ rid, tmid: mid })}> - {t('Reply')} + goToThread({ rid, tmid: mid })} + primary={!!unread} + position='relative' + overflow='visible' + > + {t('View_thread')} - - - {counter} + + {participants?.length > 0 && } + + + {isSmall ? ( + {t('__count__replies', { count: counter })} + ) : ( + {t('__count__replies__date__', { count: counter, date: format(lm) })} + )} - {!!participants && ( - - - {participants} - - )} - - - {format(lm)} - - - - - {(mention || all || unread) && ( - - - {(mention && ) || (all && ) || (unread && )} - - - )} ); diff --git a/apps/meteor/client/components/message/content/ThreadMetricsFollow.tsx b/apps/meteor/client/components/message/content/ThreadMetricsFollow.tsx new file mode 100644 index 000000000000..351d984d74d6 --- /dev/null +++ b/apps/meteor/client/components/message/content/ThreadMetricsFollow.tsx @@ -0,0 +1,50 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { MessageMetricsItem, MessageMetricsFollowing } from '@rocket.chat/fuselage'; +import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React, { useCallback } from 'react'; + +import ThreadMetricsBadge from './ThreadMetricsUnreadBadge'; +import { useToggleFollowingThreadMutation } from '../../../views/room/contextualBar/Threads/hooks/useToggleFollowingThreadMutation'; + +type ThreadMetricsFollowProps = { + following: boolean; + mid: IMessage['_id']; + rid: IMessage['rid']; + unread: boolean; + mention: boolean; + all: boolean; +}; + +const ThreadMetricsFollow = ({ following, mid, rid, unread, mention, all }: ThreadMetricsFollowProps): ReactElement => { + const t = useTranslation(); + + const dispatchToastMessage = useToastMessageDispatch(); + const toggleFollowingThreadMutation = useToggleFollowingThreadMutation({ + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + }); + + const handleFollow = useCallback( + (e) => { + e.preventDefault(); + e.stopPropagation(); + toggleFollowingThreadMutation.mutate({ rid, tmid: mid, follow: !following }); + }, + [following, rid, mid, toggleFollowingThreadMutation], + ); + + return ( + + } + /> + + ); +}; + +export default ThreadMetricsFollow; diff --git a/apps/meteor/client/components/message/content/ThreadMetricsParticipants.tsx b/apps/meteor/client/components/message/content/ThreadMetricsParticipants.tsx new file mode 100644 index 000000000000..e647fba6ad54 --- /dev/null +++ b/apps/meteor/client/components/message/content/ThreadMetricsParticipants.tsx @@ -0,0 +1,49 @@ +import { + MessageMetricsItem, + MessageMetricsItemLabel, + MessageMetricsItemAvatarRow, + MessageMetricsItemIcon, + MessageMetricsItemAvatarRowContent, +} from '@rocket.chat/fuselage'; +import { UserAvatar } from '@rocket.chat/ui-avatar'; +import { useTranslation, useUserPreference } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React from 'react'; + +type ThreadMetricsParticipantsProps = { + participants: Array; +}; + +const ThreadMetricsParticipants = ({ participants }: ThreadMetricsParticipantsProps): ReactElement => { + const t = useTranslation(); + + const hideAvatar = !useUserPreference('displayAvatars'); + + const participantsLengthExcludingVisibleAvatars = participants.length - 2; + const participantsLabel = participantsLengthExcludingVisibleAvatars > 0 ? `+${participantsLengthExcludingVisibleAvatars}` : undefined; + + return ( + + {hideAvatar && ( + <> + + {participants.length} + + )} + {!hideAvatar && ( + <> + + {participants.slice(0, 2).map((uid) => ( + + + + ))} + + {participantsLabel && {participantsLabel}} + + )} + + ); +}; + +export default ThreadMetricsParticipants; diff --git a/apps/meteor/client/components/message/content/ThreadMetricsUnreadBadge.tsx b/apps/meteor/client/components/message/content/ThreadMetricsUnreadBadge.tsx new file mode 100644 index 000000000000..9bf3bfc2bc35 --- /dev/null +++ b/apps/meteor/client/components/message/content/ThreadMetricsUnreadBadge.tsx @@ -0,0 +1,38 @@ +import { Badge } from '@rocket.chat/fuselage'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; +import type { ComponentProps } from 'react'; + +const getBadgeVariantAndTitle = ( + unread: boolean, + mention: boolean, + all: boolean, +): false | [ComponentProps['variant'], TranslationKey] => { + if (!unread) { + return false; + } + + if (mention) { + return ['danger', 'Mentions_you']; + } + + if (all) { + return ['warning', 'mention-all']; + } + + return ['primary', 'Unread']; +}; + +const ThreadMetricsUnreadBadge = ({ unread, mention, all }: { unread: boolean; mention: boolean; all: boolean }) => { + const t = useTranslation(); + const result = getBadgeVariantAndTitle(unread, mention, all); + + if (!result) return null; + + const [variant, title] = result; + + return ; +}; + +export default ThreadMetricsUnreadBadge; diff --git a/apps/meteor/client/components/message/content/attachments/DefaultAttachment.tsx b/apps/meteor/client/components/message/content/attachments/DefaultAttachment.tsx index c3f0fdcbf26d..0bb6fb37da1e 100644 --- a/apps/meteor/client/components/message/content/attachments/DefaultAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/DefaultAttachment.tsx @@ -3,8 +3,6 @@ import { isActionAttachment } from '@rocket.chat/core-typings'; import type { ReactNode, ComponentProps, ReactElement } from 'react'; import React from 'react'; -import MarkdownText from '../../../MarkdownText'; -import { useCollapse } from '../../hooks/useCollapse'; import { ActionAttachment } from './default/ActionAttachtment'; import FieldsAttachment from './default/FieldsAttachment'; import AttachmentAuthor from './structure/AttachmentAuthor'; @@ -17,6 +15,8 @@ import AttachmentRow from './structure/AttachmentRow'; import AttachmentText from './structure/AttachmentText'; import AttachmentThumb from './structure/AttachmentThumb'; import AttachmentTitle from './structure/AttachmentTitle'; +import MarkdownText from '../../../MarkdownText'; +import { useCollapse } from '../../hooks/useCollapse'; const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], diff --git a/apps/meteor/client/components/message/content/attachments/default/ActionAttachtment.tsx b/apps/meteor/client/components/message/content/attachments/default/ActionAttachtment.tsx index 2ecd19598b3b..1ca98ca3d9b3 100644 --- a/apps/meteor/client/components/message/content/attachments/default/ActionAttachtment.tsx +++ b/apps/meteor/client/components/message/content/attachments/default/ActionAttachtment.tsx @@ -2,8 +2,8 @@ import type { MessageAttachmentAction } from '@rocket.chat/core-typings'; import { Box, Button, ButtonGroup } from '@rocket.chat/fuselage'; import React from 'react'; -import { useExternalLink } from '../../../../../hooks/useExternalLink'; import ActionAttachmentButton from './ActionAttachmentButton'; +import { useExternalLink } from '../../../../../hooks/useExternalLink'; type ActionAttachmentProps = MessageAttachmentAction; diff --git a/apps/meteor/client/components/message/content/attachments/default/FieldsAttachment.tsx b/apps/meteor/client/components/message/content/attachments/default/FieldsAttachment.tsx index 4e6650e5ec98..b42463131a1f 100644 --- a/apps/meteor/client/components/message/content/attachments/default/FieldsAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/default/FieldsAttachment.tsx @@ -15,7 +15,7 @@ type FieldsAttachmentProps = { const FieldsAttachment = ({ fields }: FieldsAttachmentProps) => ( - {fields.map((field, index) => (field.short ? : ))} + {fields.map((field, index) => (field.short ? : ))} ); diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownload.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownload.tsx index ea81f48c034e..0ca7dce3c638 100644 --- a/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownload.tsx +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownload.tsx @@ -1,9 +1,9 @@ import type { ComponentPropsWithoutRef } from 'react'; import React from 'react'; -import type Action from '../../Action'; import AttachmentDownloadBase from './AttachmentDownloadBase'; import AttachmentEncryptedDownload from './AttachmentEncryptedDownload'; +import type Action from '../../Action'; type AttachmentDownloadProps = Omit, 'icon'> & { title?: string | undefined; href: string }; diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentEncryptedDownload.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentEncryptedDownload.tsx index f75b044c69a5..6a2cf0f0bc74 100644 --- a/apps/meteor/client/components/message/content/attachments/structure/AttachmentEncryptedDownload.tsx +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentEncryptedDownload.tsx @@ -1,8 +1,8 @@ import type { ComponentProps } from 'react'; import React from 'react'; -import { useDownloadFromServiceWorker } from '../../../../../hooks/useDownloadFromServiceWorker'; import AttachmentDownloadBase from './AttachmentDownloadBase'; +import { useDownloadFromServiceWorker } from '../../../../../hooks/useDownloadFromServiceWorker'; type AttachmentDownloadProps = ComponentProps; diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentSize.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentSize.tsx index 75a13e6cba34..2d05a7d4c670 100644 --- a/apps/meteor/client/components/message/content/attachments/structure/AttachmentSize.tsx +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentSize.tsx @@ -2,8 +2,8 @@ import type { Box } from '@rocket.chat/fuselage'; import type { ComponentPropsWithoutRef } from 'react'; import React from 'react'; -import { useFormatMemorySize } from '../../../../../hooks/useFormatMemorySize'; import Title from './AttachmentTitle'; +import { useFormatMemorySize } from '../../../../../hooks/useFormatMemorySize'; type AttachmentSizeProps = ComponentPropsWithoutRef & { size: number; wrapper?: boolean }; diff --git a/apps/meteor/client/components/message/content/location/MapView.tsx b/apps/meteor/client/components/message/content/location/MapView.tsx index 77e404dac1a3..3d5a520f8fbb 100644 --- a/apps/meteor/client/components/message/content/location/MapView.tsx +++ b/apps/meteor/client/components/message/content/location/MapView.tsx @@ -11,7 +11,7 @@ type MapViewProps = { }; const MapView = ({ latitude, longitude }: MapViewProps) => { - const googleMapsApiKey = useSetting('MapView_GMapsAPIKey'); + const googleMapsApiKey = useSetting('MapView_GMapsAPIKey', ''); const linkUrl = `https://maps.google.com/maps?daddr=${latitude},${longitude}`; diff --git a/apps/meteor/client/components/message/content/reactions/Reaction.tsx b/apps/meteor/client/components/message/content/reactions/Reaction.tsx index b2dea5eecfe9..46121e622012 100644 --- a/apps/meteor/client/components/message/content/reactions/Reaction.tsx +++ b/apps/meteor/client/components/message/content/reactions/Reaction.tsx @@ -1,11 +1,12 @@ import { MessageReaction as MessageReactionTemplate, MessageReactionEmoji, MessageReactionCounter } from '@rocket.chat/fuselage'; -import { useTooltipClose, useTooltipOpen, useTranslation } from '@rocket.chat/ui-contexts'; +import { useTooltipClose, useTooltipOpen } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement } from 'react'; import React, { useRef, useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import ReactionTooltip from './ReactionTooltip'; import { getEmojiClassNameAndDataTitle } from '../../../../lib/utils/renderEmoji'; import { MessageListContext } from '../../list/MessageListContext'; -import ReactionTooltip from './ReactionTooltip'; // TODO: replace it with proper usage of i18next plurals type ReactionProps = { @@ -17,7 +18,7 @@ type ReactionProps = { } & ComponentProps; const Reaction = ({ hasReacted, counter, name, names, messageId, ...props }: ReactionProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const ref = useRef(null); const openTooltip = useTooltipOpen(); const closeTooltip = useTooltipClose(); diff --git a/apps/meteor/client/components/message/content/urlPreviews/OEmbedCollapsible.tsx b/apps/meteor/client/components/message/content/urlPreviews/OEmbedCollapsible.tsx index 8761b60cc68f..78a79cb50d44 100644 --- a/apps/meteor/client/components/message/content/urlPreviews/OEmbedCollapsible.tsx +++ b/apps/meteor/client/components/message/content/urlPreviews/OEmbedCollapsible.tsx @@ -3,9 +3,9 @@ import type { ReactElement, ReactNode } from 'react'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import MessageCollapsible from '../../MessageCollapsible'; import OEmbedPreviewContent from './OEmbedPreviewContent'; import type { OEmbedPreviewMetadata } from './OEmbedPreviewMetadata'; +import MessageCollapsible from '../../MessageCollapsible'; type OEmbedCollapsibleProps = { children?: ReactNode } & OEmbedPreviewMetadata; diff --git a/apps/meteor/client/components/message/content/urlPreviews/OEmbedPreviewContent.tsx b/apps/meteor/client/components/message/content/urlPreviews/OEmbedPreviewContent.tsx index 550b325012ed..68d29b6440b8 100644 --- a/apps/meteor/client/components/message/content/urlPreviews/OEmbedPreviewContent.tsx +++ b/apps/meteor/client/components/message/content/urlPreviews/OEmbedPreviewContent.tsx @@ -8,8 +8,8 @@ import { import type { ReactElement, ReactNode } from 'react'; import React from 'react'; -import MarkdownText from '../../../MarkdownText'; import type { OEmbedPreviewMetadata } from './OEmbedPreviewMetadata'; +import MarkdownText from '../../../MarkdownText'; type OEmbedPreviewContentProps = { thumb?: ReactElement; children?: ReactNode } & OEmbedPreviewMetadata; diff --git a/apps/meteor/client/components/message/content/urlPreviews/UrlImagePreview.tsx b/apps/meteor/client/components/message/content/urlPreviews/UrlImagePreview.tsx index 8106a01004e0..449ad1edd790 100644 --- a/apps/meteor/client/components/message/content/urlPreviews/UrlImagePreview.tsx +++ b/apps/meteor/client/components/message/content/urlPreviews/UrlImagePreview.tsx @@ -2,8 +2,8 @@ import { Box, MessageGenericPreviewImage } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React from 'react'; -import { useOembedLayout } from '../../hooks/useOembedLayout'; import type { UrlPreviewMetadata } from './UrlPreviewMetadata'; +import { useOembedLayout } from '../../hooks/useOembedLayout'; const UrlImagePreview = ({ url }: Pick): ReactElement => { const { maxHeight: oembedMaxHeight } = useOembedLayout(); diff --git a/apps/meteor/client/components/message/content/urlPreviews/UrlPreview.tsx b/apps/meteor/client/components/message/content/urlPreviews/UrlPreview.tsx index 6205c9040b6b..a302c1c32ac2 100644 --- a/apps/meteor/client/components/message/content/urlPreviews/UrlPreview.tsx +++ b/apps/meteor/client/components/message/content/urlPreviews/UrlPreview.tsx @@ -1,16 +1,17 @@ import { Box } from '@rocket.chat/fuselage'; -import { useAttachmentAutoLoadEmbedMedia, useTranslation } from '@rocket.chat/ui-contexts'; +import { useAttachmentAutoLoadEmbedMedia } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; -import { useCollapse } from '../../hooks/useCollapse'; import type { UrlPreviewMetadata } from './UrlPreviewMetadata'; import UrlPreviewResolver from './UrlPreviewResolver'; +import { useCollapse } from '../../hooks/useCollapse'; const UrlPreview = (props: UrlPreviewMetadata): ReactElement => { const autoLoadMedia = useAttachmentAutoLoadEmbedMedia(); const [collapsed, collapse] = useCollapse(!autoLoadMedia); - const t = useTranslation(); + const { t } = useTranslation(); return ( <> diff --git a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts index 715a84a2359b..2a433abc4e2f 100644 --- a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts +++ b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts @@ -11,12 +11,12 @@ import type { Options } from '@rocket.chat/message-parser'; import { useSetting } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; +import { useSubscriptionFromMessageQuery } from './useSubscriptionFromMessageQuery'; import type { MessageWithMdEnforced } from '../../../lib/parseMessageTextToAstMarkdown'; import { parseMessageTextToAstMarkdown } from '../../../lib/parseMessageTextToAstMarkdown'; import { useAutoLinkDomains } from '../../../views/room/MessageList/hooks/useAutoLinkDomains'; import { useAutoTranslate } from '../../../views/room/MessageList/hooks/useAutoTranslate'; import { useKatex } from '../../../views/room/MessageList/hooks/useKatex'; -import { useSubscriptionFromMessageQuery } from './useSubscriptionFromMessageQuery'; const normalizeAttachments = (attachments: MessageAttachment[], name?: string, type?: string): MessageAttachment[] => { if (name) { @@ -65,7 +65,7 @@ export const useNormalizedMessage = (message: TMessag const customDomains = useAutoLinkDomains(); const subscription = useSubscriptionFromMessageQuery(message).data ?? undefined; const autoTranslateOptions = useAutoTranslate(subscription); - const showColors = useSetting('HexColorPreview_Enabled'); + const showColors = useSetting('HexColorPreview_Enabled', true); return useMemo(() => { const parseOptions: Options = { diff --git a/apps/meteor/client/components/message/hooks/useOembedLayout.ts b/apps/meteor/client/components/message/hooks/useOembedLayout.ts index 819bed285e42..2a2a623da657 100644 --- a/apps/meteor/client/components/message/hooks/useOembedLayout.ts +++ b/apps/meteor/client/components/message/hooks/useOembedLayout.ts @@ -15,7 +15,7 @@ export const useOembedLayout = (): OembedLayout => { very often, so this hook is not going to be re-evaluated very often either; this is why we don't need to memoize the result or store it in a context */ - const enabled = useSetting('API_Embed', false); + const enabled = useSetting('API_Embed', false); const { isMobile } = useLayout(); const maxWidth = isMobile ? ('100%' as const) : 368; diff --git a/apps/meteor/client/components/message/list/MessageListContext.tsx b/apps/meteor/client/components/message/list/MessageListContext.tsx index d525e2a4a39e..4527d03edd46 100644 --- a/apps/meteor/client/components/message/list/MessageListContext.tsx +++ b/apps/meteor/client/components/message/list/MessageListContext.tsx @@ -11,12 +11,11 @@ export type MessageListContextValue = { showRoles: boolean; showRealName: boolean; showUsername: boolean; - highlights?: - | { - highlight: string; - regex: RegExp; - urlRegex: RegExp; - }[]; + highlights?: { + highlight: string; + regex: RegExp; + urlRegex: RegExp; + }[]; katex?: { dollarSyntaxEnabled: boolean; parenthesisSyntaxEnabled: boolean; diff --git a/apps/meteor/client/components/message/toolbar/MessageActionMenu.tsx b/apps/meteor/client/components/message/toolbar/MessageActionMenu.tsx index 5bf1d8846498..143c0a3fe46a 100644 --- a/apps/meteor/client/components/message/toolbar/MessageActionMenu.tsx +++ b/apps/meteor/client/components/message/toolbar/MessageActionMenu.tsx @@ -38,18 +38,21 @@ const MessageActionMenu = ({ options, onChangeMenuVisibility, context, isMessage ...(option.disabled && option?.disabled?.(context) && { tooltip: t('Action_not_available_encrypted_content', { action: t(option.label) }) }), })) - .reduce((acc, option) => { - const group = option.type ? option.type : ''; - const section = acc.find((section: { id: string }) => section.id === group); - if (section) { - section.items.push(option); - return acc; - } - const newSection = { id: group, title: group === 'apps' ? t('Apps') : '', items: [option] }; - acc.push(newSection); + .reduce( + (acc, option) => { + const group = option.type ? option.type : ''; + const section = acc.find((section: { id: string }) => section.id === group); + if (section) { + section.items.push(option); + return acc; + } + const newSection = { id: group, title: group === 'apps' ? t('Apps') : '', items: [option] }; + acc.push(newSection); - return acc; - }, [] as unknown as MessageActionSection[]) + return acc; + }, + [] as unknown as MessageActionSection[], + ) .map((section) => { if (section.id !== 'apps') { return section; diff --git a/apps/meteor/client/components/message/toolbar/MessageToolbar.tsx b/apps/meteor/client/components/message/toolbar/MessageToolbar.tsx index 17357be8b6ab..2f52ebe42fa9 100644 --- a/apps/meteor/client/components/message/toolbar/MessageToolbar.tsx +++ b/apps/meteor/client/components/message/toolbar/MessageToolbar.tsx @@ -8,6 +8,10 @@ import { useQuery } from '@tanstack/react-query'; import type { ComponentProps, ReactElement } from 'react'; import React, { memo, useMemo, useRef } from 'react'; +import MessageActionMenu from './MessageActionMenu'; +import MessageToolbarStarsActionMenu from './MessageToolbarStarsActionMenu'; +import { useNewDiscussionMessageAction } from './useNewDiscussionMessageAction'; +import { useWebDAVMessageAction } from './useWebDAVMessageAction'; import type { MessageActionContext } from '../../../../app/ui-utils/client/lib/MessageAction'; import { MessageAction } from '../../../../app/ui-utils/client/lib/MessageAction'; import { useEmojiPickerData } from '../../../contexts/EmojiPickerContext'; @@ -18,9 +22,6 @@ import { useIsSelecting } from '../../../views/room/MessageList/contexts/Selecte import { useAutoTranslate } from '../../../views/room/MessageList/hooks/useAutoTranslate'; import { useChat } from '../../../views/room/contexts/ChatContext'; import { useRoomToolbox } from '../../../views/room/contexts/RoomToolboxContext'; -import MessageActionMenu from './MessageActionMenu'; -import MessageToolbarStarsActionMenu from './MessageToolbarStarsActionMenu'; -import { useWebDAVMessageAction } from './useWebDAVMessageAction'; const getMessageContext = (message: IMessage, room: IRoom, context?: MessageActionContext): MessageActionContext => { if (context) { @@ -85,6 +86,7 @@ const MessageToolbar = ({ // TODO: move this to another place useWebDAVMessageAction(); + useNewDiscussionMessageAction(); const actionsQueryResult = useQuery(['rooms', room._id, 'messages', message._id, 'actions'] as const, async () => { const props = { message, room, user, subscription, settings: mapSettings, chat }; diff --git a/apps/meteor/client/components/message/toolbar/useNewDiscussionMessageAction.tsx b/apps/meteor/client/components/message/toolbar/useNewDiscussionMessageAction.tsx new file mode 100644 index 000000000000..2812e1c06ba8 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/useNewDiscussionMessageAction.tsx @@ -0,0 +1,68 @@ +import { useSetModal, useSetting } from '@rocket.chat/ui-contexts'; +import React, { useEffect } from 'react'; + +import { hasPermission } from '../../../../app/authorization/client'; +import { MessageAction } from '../../../../app/ui-utils/client/lib/MessageAction'; +import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; +import CreateDiscussion from '../../CreateDiscussion'; + +export const useNewDiscussionMessageAction = () => { + const enabled = useSetting('Discussion_enabled', false); + + const setModal = useSetModal(); + + useEffect(() => { + if (!enabled) { + return MessageAction.removeButton('start-discussion'); + } + MessageAction.addButton({ + id: 'start-discussion', + icon: 'discussion', + label: 'Discussion_start', + type: 'communication', + context: ['message', 'message-mobile', 'videoconf'], + async action(_, { message, room }) { + setModal( + setModal(undefined)} + parentMessageId={message._id} + nameSuggestion={message?.msg?.substr(0, 140)} + />, + ); + }, + condition({ + message: { + u: { _id: uid }, + drid, + dcount, + }, + room, + subscription, + user, + }) { + if (drid || !Number.isNaN(Number(dcount))) { + return false; + } + if (!subscription) { + return false; + } + const isLivechatRoom = roomCoordinator.isLivechatRoom(room.t); + if (isLivechatRoom) { + return false; + } + + if (!user) { + return false; + } + + return uid !== user._id ? hasPermission('start-discussion-other-user', room._id) : hasPermission('start-discussion', room._id); + }, + order: 1, + group: 'menu', + }); + return () => { + MessageAction.removeButton('start-discussion'); + }; + }, [enabled, setModal]); +}; diff --git a/apps/meteor/client/components/message/toolbar/useWebDAVMessageAction.tsx b/apps/meteor/client/components/message/toolbar/useWebDAVMessageAction.tsx index a2be70077054..166872acaa42 100644 --- a/apps/meteor/client/components/message/toolbar/useWebDAVMessageAction.tsx +++ b/apps/meteor/client/components/message/toolbar/useWebDAVMessageAction.tsx @@ -4,11 +4,10 @@ import React, { useEffect } from 'react'; import { MessageAction } from '../../../../app/ui-utils/client/lib/MessageAction'; import { getURL } from '../../../../app/utils/client'; import { useWebDAVAccountIntegrationsQuery } from '../../../hooks/webdav/useWebDAVAccountIntegrationsQuery'; -import { messageArgs } from '../../../lib/utils/messageArgs'; import SaveToWebdavModal from '../../../views/room/webdav/SaveToWebdavModal'; export const useWebDAVMessageAction = () => { - const enabled = useSetting('Webdav_Integration_Enabled', false); + const enabled = useSetting('Webdav_Integration_Enabled', false); const { data } = useWebDAVAccountIntegrationsQuery({ enabled }); @@ -26,8 +25,7 @@ export const useWebDAVMessageAction = () => { condition: ({ message, subscription }) => { return !!subscription && !!data?.length && !!message.file; }, - action(_, props) { - const { message = messageArgs(this).msg } = props; + action(_, { message }) { const [attachment] = message.attachments || []; const url = getURL(attachment.title_link as string, { full: true }); diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 89da3724cfbc..c23709ad37f9 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -101,7 +101,7 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM unread={unread} mention={mention} all={all} - participants={normalizedMessage?.replies?.length} + participants={normalizedMessage?.replies} /> )} diff --git a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx index b2f9f1d019e2..8eb52f47e723 100644 --- a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx @@ -2,9 +2,10 @@ import type { IThreadMainMessage, IThreadMessage } from '@rocket.chat/core-typin import { isE2EEMessage, isQuoteAttachment } from '@rocket.chat/core-typings'; import { MessageBody } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useSetting, useUserId, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetting, useUserId } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { memo } from 'react'; +import { useTranslation } from 'react-i18next'; import { useUserData } from '../../../../hooks/useUserData'; import type { UserPresence } from '../../../../lib/presence'; @@ -34,7 +35,7 @@ const ThreadMessageContent = ({ message }: ThreadMessageContentProps): ReactElem const messageUser: UserPresence = { ...message.u, roles: [], ...useUserData(message.u._id) }; const readReceiptEnabled = useSetting('Message_Read_Receipt_Enabled', false); - const t = useTranslation(); + const { t } = useTranslation(); const normalizedMessage = useNormalizedMessage(message); diff --git a/apps/meteor/client/contexts/VideoConfContext.ts b/apps/meteor/client/contexts/VideoConfContext.ts index 2a4b2bd736fd..49a9006a1a81 100644 --- a/apps/meteor/client/contexts/VideoConfContext.ts +++ b/apps/meteor/client/contexts/VideoConfContext.ts @@ -1,7 +1,6 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { createContext, useContext } from 'react'; -import type { Subscription } from 'use-subscription'; -import { useSubscription } from 'use-subscription'; +import { useSyncExternalStore } from 'use-sync-external-store/shim'; import type { DirectCallData, ProviderCapabilities, CallPreferences, VideoConfManager } from '../lib/VideoConfManager'; @@ -22,11 +21,26 @@ type VideoConfContextValue = { rejectIncomingCall: (callId: string) => void; abortCall: () => void; setPreferences: (prefs: { mic?: boolean; cam?: boolean }) => void; - queryIncomingCalls: Subscription; - queryRinging: Subscription; - queryCalling: Subscription; - queryCapabilities: Subscription; - queryPreferences: Subscription; + queryIncomingCalls: { + subscribe: (cb: () => void) => () => void; + getSnapshot: () => DirectCallData[]; + }; + queryRinging: { + subscribe: (cb: () => void) => () => void; + getSnapshot: () => boolean; + }; + queryCalling: { + subscribe: (cb: () => void) => () => void; + getSnapshot: () => boolean; + }; + queryCapabilities: { + subscribe: (cb: () => void) => () => void; + getSnapshot: () => ProviderCapabilities; + }; + queryPreferences: { + subscribe: (cb: () => void) => () => void; + getSnapshot: () => CallPreferences; + }; }; export const VideoConfContext = createContext(undefined); @@ -49,24 +63,24 @@ export const useVideoConfAbortCall = (): VideoConfContextValue['abortCall'] => u export const useVideoConfRejectIncomingCall = (): VideoConfContextValue['rejectIncomingCall'] => useVideoConfContext().rejectIncomingCall; export const useVideoConfIncomingCalls = (): DirectCallData[] => { const { queryIncomingCalls } = useVideoConfContext(); - return useSubscription(queryIncomingCalls); + return useSyncExternalStore(queryIncomingCalls.subscribe, queryIncomingCalls.getSnapshot); }; export const useVideoConfSetPreferences = (): VideoConfContextValue['setPreferences'] => useVideoConfContext().setPreferences; export const useVideoConfIsRinging = (): boolean => { const { queryRinging } = useVideoConfContext(); - return useSubscription(queryRinging); + return useSyncExternalStore(queryRinging.subscribe, queryRinging.getSnapshot); }; export const useVideoConfIsCalling = (): boolean => { const { queryCalling } = useVideoConfContext(); - return useSubscription(queryCalling); + return useSyncExternalStore(queryCalling.subscribe, queryCalling.getSnapshot); }; export const useVideoConfCapabilities = (): ProviderCapabilities => { const { queryCapabilities } = useVideoConfContext(); - return useSubscription(queryCapabilities); + return useSyncExternalStore(queryCapabilities.subscribe, queryCapabilities.getSnapshot); }; export const useVideoConfPreferences = (): CallPreferences => { const { queryPreferences } = useVideoConfContext(); - return useSubscription(queryPreferences); + return useSyncExternalStore(queryPreferences.subscribe, queryPreferences.getSnapshot); }; export const useVideoConfManager = (): typeof VideoConfManager | undefined => useContext(VideoConfContext)?.manager; diff --git a/apps/meteor/client/definitions/IRocketChatDesktop.ts b/apps/meteor/client/definitions/IRocketChatDesktop.ts index 52bc172d719b..a0707e9ba6d7 100644 --- a/apps/meteor/client/definitions/IRocketChatDesktop.ts +++ b/apps/meteor/client/definitions/IRocketChatDesktop.ts @@ -5,6 +5,6 @@ export interface IRocketChatDesktop { getOutlookEvents?: (date: Date) => Promise; setOutlookExchangeUrl?: (url: string, userId: string) => Promise; hasOutlookCredentials?: () => Promise; - clearOutlookCredentials?: () => void; + clearOutlookCredentials?: () => Promise | void; openDocumentViewer?: (url: string, format: string, options: any) => void; } diff --git a/apps/meteor/client/definitions/global.d.ts b/apps/meteor/client/definitions/global.d.ts index 8b20108e8e48..0916ef237119 100644 --- a/apps/meteor/client/definitions/global.d.ts +++ b/apps/meteor/client/definitions/global.d.ts @@ -4,5 +4,80 @@ declare global { // eslint-disable-next-line @typescript-eslint/naming-convention interface Window { RocketChatDesktop?: IRocketChatDesktop; + + /** @deprecated use `window.RTCPeerConnection` */ + mozRTCPeerConnection?: RTCPeerConnection; + /** @deprecated use `window.RTCPeerConnection` */ + webkitRTCPeerConnection?: RTCPeerConnection; + + /** @deprecated use `window.RTCSessionDescription` */ + mozRTCSessionDescription?: RTCSessionDescription; + /** @deprecated use `window.RTCSessionDescription` */ + webkitRTCSessionDescription?: RTCSessionDescription; + /** @deprecated use `window.RTCIceCandidate` */ + mozRTCIceCandidate?: RTCIceCandidate; + /** @deprecated use `window.RTCIceCandidate` */ + webkitRTCIceCandidate?: RTCIceCandidate; + /** @deprecated use `window.RTCSessionDescription` */ + mozRTCSessionDescription?: RTCSessionDescription; + /** @deprecated use `window.RTCSessionDescription` */ + webkitRTCSessionDescription?: RTCSessionDescription; + /** @deprecated use `window.AudioContext` */ + mozAudioContext?: AudioContext; + /** @deprecated use `window.AudioContext` */ + webkitAudioContext?: AudioContext; + } + + interface Navigator { + /** @deprecated */ + getUserMedia?: ( + this: Navigator, + constraints?: MediaStreamConstraints | undefined, + onSuccess?: (stream: MediaStream) => void, + onError?: (error: any) => void, + ) => void; + /** @deprecated */ + webkitGetUserMedia?: ( + this: Navigator, + constraints?: MediaStreamConstraints | undefined, + onSuccess?: (stream: MediaStream) => void, + onError?: (error: any) => void, + ) => void; + /** @deprecated */ + mozGetUserMedia?: ( + this: Navigator, + constraints?: MediaStreamConstraints | undefined, + onSuccess?: (stream: MediaStream) => void, + onError?: (error: any) => void, + ) => void; + /** @deprecated */ + msGetUserMedia?: ( + this: Navigator, + constraints?: MediaStreamConstraints | undefined, + onSuccess?: (stream: MediaStream) => void, + onError?: (error: any) => void, + ) => void; + } + + interface RTCPeerConnection { + /** @deprecated use `getReceivers() */ + getRemoteStreams(): MediaStream[]; + /** @deprecated */ + addStream(stream: MediaStream): void; + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + interface MediaTrackConstraints { + /** @deprecated */ + mozMediaSource?: string; + /** @deprecated */ + mediaSource?: string; + /** @deprecated */ + mandatory?: { + chromeMediaSource: string; + chromeMediaSourceId: string; + maxWidth: number; + maxHeight: number; + }; } } diff --git a/apps/meteor/client/hooks/lists/useScrollableMessageList.ts b/apps/meteor/client/hooks/lists/useScrollableMessageList.ts index d78b13c3030f..679ba23b5cd2 100644 --- a/apps/meteor/client/hooks/lists/useScrollableMessageList.ts +++ b/apps/meteor/client/hooks/lists/useScrollableMessageList.ts @@ -1,10 +1,10 @@ import type { IMessage, Serialized } from '@rocket.chat/core-typings'; import { useCallback } from 'react'; +import { useScrollableRecordList } from './useScrollableRecordList'; import type { MessageList } from '../../lib/lists/MessageList'; import type { RecordListBatchChanges } from '../../lib/lists/RecordList'; import { mapMessageFromApi } from '../../lib/utils/mapMessageFromApi'; -import { useScrollableRecordList } from './useScrollableRecordList'; export const useScrollableMessageList = ( messageList: MessageList, diff --git a/apps/meteor/client/hooks/omnichannel/useIsRoomOverMacLimit.tsx b/apps/meteor/client/hooks/omnichannel/useIsRoomOverMacLimit.tsx index bf4e4b1bf957..310bea690a33 100644 --- a/apps/meteor/client/hooks/omnichannel/useIsRoomOverMacLimit.tsx +++ b/apps/meteor/client/hooks/omnichannel/useIsRoomOverMacLimit.tsx @@ -1,5 +1,5 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { isOmnichannelRoom, type IOmnichannelGenericRoom, isVoipRoom } from '@rocket.chat/core-typings'; +import { isOmnichannelRoom, isVoipRoom } from '@rocket.chat/core-typings'; +import type { IRoom, IOmnichannelGenericRoom } from '@rocket.chat/core-typings'; import { useIsOverMacLimit } from './useIsOverMacLimit'; diff --git a/apps/meteor/client/hooks/roomActions/useContactChatHistoryRoomAction.ts b/apps/meteor/client/hooks/roomActions/useContactChatHistoryRoomAction.ts deleted file mode 100644 index 38ee27b1a317..000000000000 --- a/apps/meteor/client/hooks/roomActions/useContactChatHistoryRoomAction.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { lazy, useMemo } from 'react'; - -import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext'; - -const ContactHistory = lazy(() => import('../../views/omnichannel/contactHistory/ContactHistory')); - -export const useContactChatHistoryRoomAction = () => { - return useMemo( - (): RoomToolboxActionConfig => ({ - id: 'contact-chat-history', - groups: ['live'], - title: 'Contact_Chat_History', - icon: 'clock', - tabComponent: ContactHistory, - order: 11, - }), - [], - ); -}; diff --git a/apps/meteor/client/hooks/roomActions/useContactProfileRoomAction.ts b/apps/meteor/client/hooks/roomActions/useContactProfileRoomAction.ts index 17dbc3425493..7cba545fe962 100644 --- a/apps/meteor/client/hooks/roomActions/useContactProfileRoomAction.ts +++ b/apps/meteor/client/hooks/roomActions/useContactProfileRoomAction.ts @@ -2,7 +2,7 @@ import { lazy, useMemo } from 'react'; import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext'; -const ContactsContextualBar = lazy(() => import('../../views/omnichannel/directory/contacts/contextualBar/ContactsContextualBar')); +const ContactInfoRouter = lazy(() => import('../../views/omnichannel/contactInfo/ContactInfoRouter')); export const useContactProfileRoomAction = () => { return useMemo( @@ -11,7 +11,7 @@ export const useContactProfileRoomAction = () => { groups: ['live' /* , 'voip'*/], title: 'Contact_Info', icon: 'user', - tabComponent: ContactsContextualBar, + tabComponent: ContactInfoRouter, order: 1, }), [], diff --git a/apps/meteor/client/hooks/roomActions/useRoomInfoRoomAction.ts b/apps/meteor/client/hooks/roomActions/useRoomInfoRoomAction.ts index 1b7608ff5212..573211b6fbd8 100644 --- a/apps/meteor/client/hooks/roomActions/useRoomInfoRoomAction.ts +++ b/apps/meteor/client/hooks/roomActions/useRoomInfoRoomAction.ts @@ -2,7 +2,7 @@ import { lazy, useMemo } from 'react'; import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext'; -const ChatsContextualBar = lazy(() => import('../../views/omnichannel/directory/chats/contextualBar/ChatsContextualBar')); +const ChatsContextualBar = lazy(() => import('../../views/omnichannel/directory/chats/ChatInfo/ChatInfoRouter')); export const useRoomInfoRoomAction = () => { return useMemo( diff --git a/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useStartCallRoomAction.tsx b/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useStartCallRoomAction.tsx index 18d3efd01053..a275fe755560 100644 --- a/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useStartCallRoomAction.tsx +++ b/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useStartCallRoomAction.tsx @@ -1,10 +1,10 @@ import { GenericMenu } from '@rocket.chat/ui-client'; import React, { useMemo } from 'react'; -import HeaderToolbarAction from '../../../components/Header/HeaderToolbarAction'; -import type { RoomToolboxActionConfig } from '../../../views/room/contexts/RoomToolboxContext'; import useVideoConfMenuOptions from './useVideoConfMenuOptions'; import useVoipMenuOptions from './useVoipMenuOptions'; +import HeaderToolbarAction from '../../../components/Header/HeaderToolbarAction'; +import type { RoomToolboxActionConfig } from '../../../views/room/contexts/RoomToolboxContext'; export const useStartCallRoomAction = () => { const videoCall = useVideoConfMenuOptions(); diff --git a/apps/meteor/client/hooks/roomActions/useWebRTCVideoRoomAction.ts b/apps/meteor/client/hooks/roomActions/useWebRTCVideoRoomAction.ts index 38a8488b2615..41b078429498 100644 --- a/apps/meteor/client/hooks/roomActions/useWebRTCVideoRoomAction.ts +++ b/apps/meteor/client/hooks/roomActions/useWebRTCVideoRoomAction.ts @@ -11,7 +11,7 @@ export const useWebRTCVideoRoomAction = () => { const enabled = useSetting('WebRTC_Enabled', false); const room = useRoom(); const federated = isRoomFederated(room); - const callProvider = useSetting('Omnichannel_call_provider', 'default-provider'); + const callProvider = useSetting('Omnichannel_call_provider', 'default-provider'); const allowed = enabled && callProvider === 'WebRTC' && room.servedBy; diff --git a/apps/meteor/client/hooks/useAppActionButtons.ts b/apps/meteor/client/hooks/useAppActionButtons.ts index 2a074fcc3be1..0dcb2d380d46 100644 --- a/apps/meteor/client/hooks/useAppActionButtons.ts +++ b/apps/meteor/client/hooks/useAppActionButtons.ts @@ -7,13 +7,13 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { useApplyButtonFilters, useApplyButtonAuthFilter } from './useApplyButtonFilters'; +import { useFilterActionsByContext } from './useFilterActions'; import { UiKitTriggerTimeoutError } from '../../app/ui-message/client/UiKitTriggerTimeoutError'; import type { MessageActionConfig, MessageActionContext } from '../../app/ui-utils/client/lib/MessageAction'; import type { MessageBoxAction } from '../../app/ui-utils/client/lib/messageBox'; import { Utilities } from '../../ee/lib/misc/Utilities'; import { useUiKitActionManager } from '../uikit/hooks/useUiKitActionManager'; -import { useApplyButtonFilters, useApplyButtonAuthFilter } from './useApplyButtonFilters'; -import { useFilterActionsByContext } from './useFilterActions'; const getIdForActionButton = ({ appId, actionId }: IUIActionButton): string => `${appId}/${actionId}`; diff --git a/apps/meteor/client/hooks/useClipboardWithToast.ts b/apps/meteor/client/hooks/useClipboardWithToast.ts index ddabbcf9cf0e..fd30e5989b54 100644 --- a/apps/meteor/client/hooks/useClipboardWithToast.ts +++ b/apps/meteor/client/hooks/useClipboardWithToast.ts @@ -1,9 +1,10 @@ import type { UseClipboardReturn } from '@rocket.chat/fuselage-hooks'; import { useClipboard, useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; export default function useClipboardWithToast(text: string): UseClipboardReturn { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); return useClipboard(text, { diff --git a/apps/meteor/client/hooks/useContinuousSoundNotification.ts b/apps/meteor/client/hooks/useContinuousSoundNotification.ts index 22fcabd4e6a9..80abb492363b 100644 --- a/apps/meteor/client/hooks/useContinuousSoundNotification.ts +++ b/apps/meteor/client/hooks/useContinuousSoundNotification.ts @@ -2,6 +2,7 @@ import type { ICustomSound } from '@rocket.chat/core-typings'; import { useSetting, useUserPreference, useUserSubscriptions } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; +import { useUserSoundPreferences } from './useUserSoundPreferences'; import { CustomSounds } from '../../app/custom-sounds/client/lib/CustomSounds'; const query = { t: 'l', ls: { $exists: false }, open: true }; @@ -11,12 +12,10 @@ export const useContinuousSoundNotification = () => { const playNewRoomSoundContinuously = useSetting('Livechat_continuous_sound_notification_new_livechat_room'); const newRoomNotification = useUserPreference('newRoomNotification'); - const audioVolume = useUserPreference('notificationsSoundVolume'); + const { notificationsSoundVolume } = useUserSoundPreferences(); const continuousCustomSoundId = newRoomNotification && `${newRoomNotification}-continuous`; - const volume = audioVolume !== undefined ? Number((audioVolume / 100).toPrecision(2)) : 1; - useEffect(() => { let audio: ICustomSound; if (playNewRoomSoundContinuously && continuousCustomSoundId) { @@ -46,8 +45,8 @@ export const useContinuousSoundNotification = () => { } CustomSounds.play(continuousCustomSoundId, { - volume, + volume: notificationsSoundVolume, loop: true, }); - }, [continuousCustomSoundId, playNewRoomSoundContinuously, userSubscriptions, volume]); + }, [continuousCustomSoundId, playNewRoomSoundContinuously, userSubscriptions, notificationsSoundVolume]); }; diff --git a/apps/meteor/client/hooks/useDecryptedMessage.spec.ts b/apps/meteor/client/hooks/useDecryptedMessage.spec.ts index 62e77a77b096..404ee4b85739 100644 --- a/apps/meteor/client/hooks/useDecryptedMessage.spec.ts +++ b/apps/meteor/client/hooks/useDecryptedMessage.spec.ts @@ -1,8 +1,8 @@ import { isE2EEMessage } from '@rocket.chat/core-typings'; import { renderHook, waitFor } from '@testing-library/react'; -import { e2e } from '../../app/e2e/client/rocketchat.e2e'; import { useDecryptedMessage } from './useDecryptedMessage'; +import { e2e } from '../../app/e2e/client/rocketchat.e2e'; // Mock the dependencies jest.mock('@rocket.chat/core-typings', () => ({ diff --git a/apps/meteor/client/hooks/useDeviceLogout.tsx b/apps/meteor/client/hooks/useDeviceLogout.tsx index c9c089aa2ce0..64cfde76b68b 100644 --- a/apps/meteor/client/hooks/useDeviceLogout.tsx +++ b/apps/meteor/client/hooks/useDeviceLogout.tsx @@ -1,8 +1,8 @@ import { useSetModal, useTranslation, useToastMessageDispatch, useRoute, useRouteParameter } from '@rocket.chat/ui-contexts'; import React, { useCallback } from 'react'; -import GenericModal from '../components/GenericModal'; import { useEndpointAction } from './useEndpointAction'; +import GenericModal from '../components/GenericModal'; export const useDeviceLogout = ( sessionId: string, diff --git a/apps/meteor/client/hooks/useDevicesMenuOption.tsx b/apps/meteor/client/hooks/useDevicesMenuOption.tsx index 035e5f38c8cf..e6395fcb936c 100644 --- a/apps/meteor/client/hooks/useDevicesMenuOption.tsx +++ b/apps/meteor/client/hooks/useDevicesMenuOption.tsx @@ -1,10 +1,11 @@ import { Box, Icon } from '@rocket.chat/fuselage'; -import { useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetModal } from '@rocket.chat/ui-contexts'; import type { ReactNode } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; -import DeviceSettingsModal from '../voip/modals/DeviceSettingsModal'; import { useHasLicenseModule } from './useHasLicenseModule'; +import DeviceSettingsModal from '../voip/modals/DeviceSettingsModal'; type DevicesMenuOption = { type?: 'option' | 'heading' | 'divider'; @@ -14,7 +15,7 @@ type DevicesMenuOption = { export const useDevicesMenuOption = (): DevicesMenuOption | null => { const isEnterprise = useHasLicenseModule('voip-enterprise'); - const t = useTranslation(); + const { t } = useTranslation(); const setModal = useSetModal(); const option = { diff --git a/apps/meteor/client/hooks/useDialModal.tsx b/apps/meteor/client/hooks/useDialModal.tsx index ef513e521283..50caa4587a79 100644 --- a/apps/meteor/client/hooks/useDialModal.tsx +++ b/apps/meteor/client/hooks/useDialModal.tsx @@ -1,5 +1,6 @@ -import { useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetModal } from '@rocket.chat/ui-contexts'; import React, { Suspense, lazy, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import { useIsVoipEnterprise } from '../contexts/CallContext'; import { dispatchToastMessage } from '../lib/toast'; @@ -19,7 +20,7 @@ type DialModalControls = { export const useDialModal = (): DialModalControls => { const setModal = useSetModal(); const isEnterprise = useIsVoipEnterprise(); - const t = useTranslation(); + const { t } = useTranslation(); const closeDialModal = useCallback(() => setModal(null), [setModal]); diff --git a/apps/meteor/client/hooks/useDownloadFromServiceWorker.ts b/apps/meteor/client/hooks/useDownloadFromServiceWorker.ts index 199d1507e284..c6a55716df65 100644 --- a/apps/meteor/client/hooks/useDownloadFromServiceWorker.ts +++ b/apps/meteor/client/hooks/useDownloadFromServiceWorker.ts @@ -1,5 +1,6 @@ import { Emitter } from '@rocket.chat/emitter'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import type { TFunction } from 'i18next'; import { useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; @@ -17,7 +18,7 @@ if ('serviceWorker' in navigator) { }); } -export const registerDownloadForUid = (uid: string, t: ReturnType['t'], title?: string) => { +export const registerDownloadForUid = (uid: string, t: TFunction, title?: string) => { ee.once(uid, ({ result }) => { downloadAs({ data: [new Blob([result])] }, title ?? t('Download')); }); diff --git a/apps/meteor/client/hooks/useEndpointAction.ts b/apps/meteor/client/hooks/useEndpointAction.ts index 2cfb22b18548..4baf82a20ff2 100644 --- a/apps/meteor/client/hooks/useEndpointAction.ts +++ b/apps/meteor/client/hooks/useEndpointAction.ts @@ -6,10 +6,10 @@ import { useMutation } from '@tanstack/react-query'; type UseEndpointActionOptions = (undefined extends UrlParams ? { keys?: UrlParams; - } + } : { keys: UrlParams; - }) & { + }) & { successMessage?: string; }; export function useEndpointAction( diff --git a/apps/meteor/client/hooks/useEndpointData.ts b/apps/meteor/client/hooks/useEndpointData.ts index 93942946ede0..0c0615d3ffdb 100644 --- a/apps/meteor/client/hooks/useEndpointData.ts +++ b/apps/meteor/client/hooks/useEndpointData.ts @@ -3,9 +3,9 @@ import type { OperationParams, OperationResult, PathPattern, UrlParams } from '@ import { useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts'; import { useCallback, useEffect } from 'react'; -import { getConfig } from '../lib/utils/getConfig'; import type { AsyncState } from './useAsyncState'; import { useAsyncState } from './useAsyncState'; +import { getConfig } from '../lib/utils/getConfig'; const log = (name: string): Console['log'] => process.env.NODE_ENV !== 'production' || getConfig('debug') === 'true' diff --git a/apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts b/apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts index fd88f0237d29..e6c668713fab 100644 --- a/apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts +++ b/apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts @@ -17,10 +17,13 @@ const handleFeaturePreviewEnableQuery = (item: FeaturePreviewProps, _: any, feat const groupFeaturePreview = (features: FeaturePreviewProps[]) => Object.entries( - features.reduce((result, currentValue) => { - (result[currentValue.group] = result[currentValue.group] || []).push(currentValue); - return result; - }, {} as Record), + features.reduce( + (result, currentValue) => { + (result[currentValue.group] = result[currentValue.group] || []).push(currentValue); + return result; + }, + {} as Record, + ), ); export const useFeaturePreviewEnableQuery = (features: FeaturePreviewProps[]) => { diff --git a/apps/meteor/client/hooks/useFormatDateAndTime.ts b/apps/meteor/client/hooks/useFormatDateAndTime.ts index 2fd8ad81e6ae..f35096274e9d 100644 --- a/apps/meteor/client/hooks/useFormatDateAndTime.ts +++ b/apps/meteor/client/hooks/useFormatDateAndTime.ts @@ -9,7 +9,7 @@ type UseFormatDateAndTimeParams = { export const useFormatDateAndTime = ({ withSeconds }: UseFormatDateAndTimeParams = {}): ((input: MomentInput) => string) => { const clockMode = useUserPreference('clockMode'); - const format = useSetting('Message_TimeAndDateFormat') as string; + const format = useSetting('Message_TimeAndDateFormat', 'LLL'); return useCallback( (time) => { diff --git a/apps/meteor/client/hooks/useFormatTime.ts b/apps/meteor/client/hooks/useFormatTime.ts index b1f7e2288ab8..430a73ee1c6d 100644 --- a/apps/meteor/client/hooks/useFormatTime.ts +++ b/apps/meteor/client/hooks/useFormatTime.ts @@ -6,7 +6,7 @@ const dayFormat = ['h:mm A', 'H:mm'] as const; export const useFormatTime = (): ((input: moment.MomentInput) => string) => { const clockMode = useUserPreference<1 | 2>('clockMode'); - const format = useSetting('Message_TimeFormat') as string; + const format = useSetting('Message_TimeFormat', 'LT'); const sameDay = clockMode !== undefined ? dayFormat[clockMode - 1] : format; return useCallback( diff --git a/apps/meteor/client/hooks/useLicenseLimitsByBehavior.ts b/apps/meteor/client/hooks/useLicenseLimitsByBehavior.ts index 7ccea2dd4bd4..ba7e165496ea 100644 --- a/apps/meteor/client/hooks/useLicenseLimitsByBehavior.ts +++ b/apps/meteor/client/hooks/useLicenseLimitsByBehavior.ts @@ -25,10 +25,10 @@ export const useLicenseLimitsByBehavior = () => { .map((key) => { const rule = license.limits[key] ?.filter((limit) => validateWarnLimit(limit.max, limits[key].value ?? 0, limit.behavior)) - .reduce<{ max: number; behavior: LicenseBehavior } | null>( - (maxLimit, currentLimit) => (!maxLimit || currentLimit.max > maxLimit.max ? currentLimit : maxLimit), - null, - ); + .reduce<{ + max: number; + behavior: LicenseBehavior; + } | null>((maxLimit, currentLimit) => (!maxLimit || currentLimit.max > maxLimit.max ? currentLimit : maxLimit), null); if (!rule) { return undefined; diff --git a/apps/meteor/client/hooks/useLoadRoomForAllowedAnonymousRead.ts b/apps/meteor/client/hooks/useLoadRoomForAllowedAnonymousRead.ts new file mode 100644 index 000000000000..b53ed9dde343 --- /dev/null +++ b/apps/meteor/client/hooks/useLoadRoomForAllowedAnonymousRead.ts @@ -0,0 +1,16 @@ +import { useSetting, useUserId } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +import { CachedChatRoom, CachedChatSubscription } from '../../app/models/client'; + +export const useLoadRoomForAllowedAnonymousRead = () => { + const userId = useUserId(); + const accountsAllowAnonymousRead = useSetting('Accounts_AllowAnonymousRead'); + + useEffect(() => { + if (!userId && accountsAllowAnonymousRead === true) { + CachedChatRoom.init(); + CachedChatSubscription.ready.set(true); + } + }, [accountsAllowAnonymousRead, userId]); +}; diff --git a/apps/meteor/client/hooks/useNotifyUser.ts b/apps/meteor/client/hooks/useNotifyUser.ts new file mode 100644 index 000000000000..818017d93e97 --- /dev/null +++ b/apps/meteor/client/hooks/useNotifyUser.ts @@ -0,0 +1,93 @@ +import type { AtLeast, ISubscription } from '@rocket.chat/core-typings'; +import { useRouter, useStream, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useCallback, useEffect } from 'react'; + +import { useEmbeddedLayout } from './useEmbeddedLayout'; +import { CachedChatSubscription } from '../../app/models/client'; +import { KonchatNotification } from '../../app/ui/client/lib/KonchatNotification'; +import { RoomManager } from '../lib/RoomManager'; +import { fireGlobalEvent } from '../lib/utils/fireGlobalEvent'; + +export const useNotifyUser = () => { + const user = useUser(); + const router = useRouter(); + const isLayoutEmbedded = useEmbeddedLayout(); + const notifyUserStream = useStream('notify-user'); + const muteFocusedConversations = useUserPreference('muteFocusedConversations'); + + const notifyNewRoom = useCallback( + async (sub: AtLeast): Promise => { + if (!user || user.status === 'busy') { + return; + } + + if ((!router.getRouteParameters().name || router.getRouteParameters().name !== sub.name) && !sub.ls && sub.alert === true) { + KonchatNotification.newRoom(sub.rid); + } + }, + [router, user], + ); + + const notifyNewMessageAudio = useCallback( + (rid?: string) => { + const hasFocus = document.hasFocus(); + const messageIsInOpenedRoom = RoomManager.opened === rid; + + if (isLayoutEmbedded) { + if (!hasFocus && messageIsInOpenedRoom) { + // Play a notification sound + void KonchatNotification.newMessage(rid); + } + } else if (!hasFocus || !messageIsInOpenedRoom || !muteFocusedConversations) { + // Play a notification sound + void KonchatNotification.newMessage(rid); + } + }, + [isLayoutEmbedded, muteFocusedConversations], + ); + + useEffect(() => { + if (!user?._id) { + return; + } + + notifyUserStream(`${user?._id}/notification`, (notification) => { + const openedRoomId = ['channel', 'group', 'direct'].includes(router.getRouteName() || '') ? RoomManager.opened : undefined; + + const hasFocus = document.hasFocus(); + const messageIsInOpenedRoom = openedRoomId === notification.payload.rid; + + fireGlobalEvent('notification', { + notification, + fromOpenedRoom: messageIsInOpenedRoom, + hasFocus, + }); + + if (isLayoutEmbedded) { + if (!hasFocus && messageIsInOpenedRoom) { + // Show a notification. + KonchatNotification.showDesktop(notification); + } + } else if (!hasFocus || !messageIsInOpenedRoom) { + // Show a notification. + KonchatNotification.showDesktop(notification); + } + + notifyNewMessageAudio(notification.payload.rid); + }); + + notifyUserStream(`${user?._id}/subscriptions-changed`, (action, sub) => { + if (action === 'removed') { + return; + } + + void notifyNewRoom(sub); + }); + + CachedChatSubscription.collection.find().observe({ + changed: (sub) => { + void notifyNewRoom(sub); + }, + }); + }, [isLayoutEmbedded, notifyNewMessageAudio, notifyNewRoom, notifyUserStream, router, user?._id]); +}; diff --git a/apps/meteor/client/hooks/useOTR.spec.tsx b/apps/meteor/client/hooks/useOTR.spec.tsx index 89082c072952..dbef63f3f5c6 100644 --- a/apps/meteor/client/hooks/useOTR.spec.tsx +++ b/apps/meteor/client/hooks/useOTR.spec.tsx @@ -1,10 +1,10 @@ import { useUserId } from '@rocket.chat/ui-contexts'; import { renderHook } from '@testing-library/react'; +import { useOTR } from './useOTR'; import OTR from '../../app/otr/client/OTR'; import { OtrRoomState } from '../../app/otr/lib/OtrRoomState'; import { useRoom } from '../views/room/contexts/RoomContext'; -import { useOTR } from './useOTR'; jest.mock('@rocket.chat/ui-contexts', () => ({ useUserId: jest.fn(), diff --git a/apps/meteor/client/hooks/useOTR.ts b/apps/meteor/client/hooks/useOTR.ts index 65f9004cf323..3c6008b15bbe 100644 --- a/apps/meteor/client/hooks/useOTR.ts +++ b/apps/meteor/client/hooks/useOTR.ts @@ -1,11 +1,11 @@ import { useUserId } from '@rocket.chat/ui-contexts'; import { useMemo, useCallback } from 'react'; +import { useReactiveValue } from './useReactiveValue'; import OTR from '../../app/otr/client/OTR'; import type { OTRRoom } from '../../app/otr/client/OTRRoom'; import { OtrRoomState } from '../../app/otr/lib/OtrRoomState'; import { useRoom } from '../views/room/contexts/RoomContext'; -import { useReactiveValue } from './useReactiveValue'; export const useOTR = (): { otr: OTRRoom | undefined; otrState: OtrRoomState } => { const uid = useUserId(); diff --git a/apps/meteor/client/hooks/usePruneWarningMessage.spec.ts b/apps/meteor/client/hooks/usePruneWarningMessage.spec.ts index 2bf4b813be12..1ca06d70308c 100644 --- a/apps/meteor/client/hooks/usePruneWarningMessage.spec.ts +++ b/apps/meteor/client/hooks/usePruneWarningMessage.spec.ts @@ -1,9 +1,9 @@ import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; import { renderHook } from '@testing-library/react'; +import { usePruneWarningMessage } from './usePruneWarningMessage'; import { createRenteionPolicySettingsMock as createMock } from '../../tests/mocks/client/mockRetentionPolicySettings'; import { createFakeRoom } from '../../tests/mocks/data'; -import { usePruneWarningMessage } from './usePruneWarningMessage'; jest.useFakeTimers(); diff --git a/apps/meteor/client/hooks/usePruneWarningMessage.ts b/apps/meteor/client/hooks/usePruneWarningMessage.ts index bb1eec5a5989..35cdb6596bc4 100644 --- a/apps/meteor/client/hooks/usePruneWarningMessage.ts +++ b/apps/meteor/client/hooks/usePruneWarningMessage.ts @@ -6,9 +6,9 @@ import { sendAt } from 'cron'; import intlFormat from 'date-fns/intlFormat'; import { useEffect, useState } from 'react'; +import { useFormattedRelativeTime } from './useFormattedRelativeTime'; import { getCronAdvancedTimerFromPrecisionSetting } from '../../lib/getCronAdvancedTimerFromPrecisionSetting'; import { useRetentionPolicy } from '../views/room/hooks/useRetentionPolicy'; -import { useFormattedRelativeTime } from './useFormattedRelativeTime'; const getMessage = ({ filesOnly, excludePinned }: { filesOnly: boolean; excludePinned: boolean }): TranslationKey => { if (filesOnly) { @@ -84,12 +84,12 @@ export const usePruneWarningMessage = (room: IRoom) => { const { maxAge, filesOnly, excludePinned } = retention; - const cronPrecision = String(useSetting('RetentionPolicy_Precision')) as CronPrecisionSetting; + const cronPrecision = useSetting('RetentionPolicy_Precision', '0'); const t = useTranslation(); - const enableAdvancedCronTimer = Boolean(useSetting('RetentionPolicy_Advanced_Precision')); - const advancedCronTimer = String(useSetting('RetentionPolicy_Advanced_Precision_Cron')); + const enableAdvancedCronTimer = useSetting('RetentionPolicy_Advanced_Precision', false); + const advancedCronTimer = useSetting('RetentionPolicy_Advanced_Precision_Cron', '*/30 * * * *'); const message = getMessage({ filesOnly, excludePinned }); diff --git a/apps/meteor/client/hooks/useRoomsList.ts b/apps/meteor/client/hooks/useRoomsList.ts index f09a361c9216..66582d3b62f2 100644 --- a/apps/meteor/client/hooks/useRoomsList.ts +++ b/apps/meteor/client/hooks/useRoomsList.ts @@ -2,9 +2,9 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useCallback, useState } from 'react'; -import { RecordList } from '../lib/lists/RecordList'; import { useScrollableRecordList } from './lists/useScrollableRecordList'; import { useComponentDidUpdate } from './useComponentDidUpdate'; +import { RecordList } from '../lib/lists/RecordList'; type RoomListOptions = { text: string; diff --git a/apps/meteor/client/hooks/useSortQueryOptions.spec.tsx b/apps/meteor/client/hooks/useSortQueryOptions.spec.tsx new file mode 100644 index 000000000000..63be719bf982 --- /dev/null +++ b/apps/meteor/client/hooks/useSortQueryOptions.spec.tsx @@ -0,0 +1,28 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { renderHook } from '@testing-library/react'; + +import { useSortQueryOptions } from './useSortQueryOptions'; + +it("should return query option to sort by last message when user preference is 'activity'", () => { + const { result } = renderHook(() => useSortQueryOptions(), { + legacyRoot: true, + wrapper: mockAppRoot().withUserPreference('sidebarSortby', 'activity').build(), + }); + expect(result.current.sort).toHaveProperty('lm', -1); +}); + +it("should return query option to sort by name when user preference is 'name'", () => { + const { result } = renderHook(() => useSortQueryOptions(), { + legacyRoot: true, + wrapper: mockAppRoot().withUserPreference('sidebarSortby', 'name').build(), + }); + expect(result.current.sort).toHaveProperty('lowerCaseName', 1); +}); + +it("should return query option to sort by fname when user preference is 'name' and showRealName is true", () => { + const { result } = renderHook(() => useSortQueryOptions(), { + legacyRoot: true, + wrapper: mockAppRoot().withUserPreference('sidebarSortby', 'name').withSetting('UI_Use_Real_Name', true).build(), + }); + expect(result.current.sort).toHaveProperty('lowerCaseFName', 1); +}); diff --git a/apps/meteor/client/sidebarv2/hooks/useQueryOptions.ts b/apps/meteor/client/hooks/useSortQueryOptions.ts similarity index 94% rename from apps/meteor/client/sidebarv2/hooks/useQueryOptions.ts rename to apps/meteor/client/hooks/useSortQueryOptions.ts index 55fd137759d1..e65c6f9411f9 100644 --- a/apps/meteor/client/sidebarv2/hooks/useQueryOptions.ts +++ b/apps/meteor/client/hooks/useSortQueryOptions.ts @@ -1,7 +1,7 @@ import { useUserPreference, useSetting } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; -export const useQueryOptions = (): { +export const useSortQueryOptions = (): { sort: | { lm?: -1 | 1 | undefined; diff --git a/apps/meteor/client/hooks/useTagsList.ts b/apps/meteor/client/hooks/useTagsList.ts index 3aa80b4e07b3..9ca827331d42 100644 --- a/apps/meteor/client/hooks/useTagsList.ts +++ b/apps/meteor/client/hooks/useTagsList.ts @@ -1,9 +1,9 @@ import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useCallback, useState } from 'react'; -import { RecordList } from '../lib/lists/RecordList'; import { useScrollableRecordList } from './lists/useScrollableRecordList'; import { useComponentDidUpdate } from './useComponentDidUpdate'; +import { RecordList } from '../lib/lists/RecordList'; type TagsListOptions = { filter: string; diff --git a/apps/meteor/client/hooks/useTimeAgo.ts b/apps/meteor/client/hooks/useTimeAgo.ts index 724f61d8c7c8..ee32e6110530 100644 --- a/apps/meteor/client/hooks/useTimeAgo.ts +++ b/apps/meteor/client/hooks/useTimeAgo.ts @@ -8,7 +8,7 @@ const dayFormat = ['h:mm A', 'H:mm'] as const; export const useTimeAgo = (): ((time: Date | number | string) => string) => { const clockMode = useUserPreference<1 | 2>('clockMode'); - const timeFormat = useSetting('Message_TimeFormat', 'LT'); + const timeFormat = useSetting('Message_TimeFormat', 'LT'); const format = clockMode !== undefined ? dayFormat[clockMode - 1] : timeFormat; return useCallback( (time) => { @@ -25,7 +25,7 @@ export const useTimeAgo = (): ((time: Date | number | string) => string) => { export const useShortTimeAgo = (): ((time: Date | string | number) => string) => { const clockMode = useUserPreference<1 | 2>('clockMode'); - const timeFormat = useSetting('Message_TimeFormat') as string; + const timeFormat = useSetting('Message_TimeFormat', 'LT'); const format = clockMode !== undefined ? dayFormat[clockMode - 1] : timeFormat; return useCallback( (time) => diff --git a/apps/meteor/client/hooks/useTimeFromNow.ts b/apps/meteor/client/hooks/useTimeFromNow.ts index 6ae2b65d2d51..3f6d9b3d0950 100644 --- a/apps/meteor/client/hooks/useTimeFromNow.ts +++ b/apps/meteor/client/hooks/useTimeFromNow.ts @@ -1,5 +1,5 @@ import moment from 'moment'; import { useCallback } from 'react'; -export const useTimeFromNow = (withSuffix: boolean): ((date: Date) => string) => +export const useTimeFromNow = (withSuffix: boolean): ((date?: Date | string) => string) => useCallback((date) => moment(date).fromNow(!withSuffix), [withSuffix]); diff --git a/apps/meteor/client/hooks/useUpdateAvatar.ts b/apps/meteor/client/hooks/useUpdateAvatar.ts index 556e2989804c..ac24f6d3b57d 100644 --- a/apps/meteor/client/hooks/useUpdateAvatar.ts +++ b/apps/meteor/client/hooks/useUpdateAvatar.ts @@ -1,6 +1,7 @@ import type { AvatarObject, AvatarServiceObject, AvatarReset, AvatarUrlObj, IUser } from '@rocket.chat/core-typings'; -import { useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useMethod } from '@rocket.chat/ui-contexts'; import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { useEndpointAction } from './useEndpointAction'; import { useEndpointUpload } from './useEndpointUpload'; @@ -15,7 +16,7 @@ export const useUpdateAvatar = ( avatarObj: AvatarObject, userId: IUser['_id'], ): (() => Promise<{ success: boolean } | null | undefined>) => { - const t = useTranslation(); + const { t } = useTranslation(); const avatarUrl = isAvatarUrl(avatarObj) ? avatarObj.avatarUrl : ''; const successMessage = t('Avatar_changed_successfully'); diff --git a/apps/meteor/client/hooks/useUserSoundPreferences.ts b/apps/meteor/client/hooks/useUserSoundPreferences.ts new file mode 100644 index 000000000000..e59a55648f55 --- /dev/null +++ b/apps/meteor/client/hooks/useUserSoundPreferences.ts @@ -0,0 +1,17 @@ +import { useUserPreference } from '@rocket.chat/ui-contexts'; + +const relativeVolume = (volume: number, masterVolume: number) => { + return (volume * masterVolume) / 100; +}; + +export const useUserSoundPreferences = () => { + const masterVolume = useUserPreference('masterVolume', 100) || 100; + const notificationsSoundVolume = useUserPreference('notificationsSoundVolume', 100) || 100; + const voipRingerVolume = useUserPreference('voipRingerVolume', 100) || 100; + + return { + masterVolume, + notificationsSoundVolume: relativeVolume(notificationsSoundVolume, masterVolume), + voipRingerVolume: relativeVolume(voipRingerVolume, masterVolume), + }; +}; diff --git a/apps/meteor/client/hooks/useVoipClient.ts b/apps/meteor/client/hooks/useVoipClient.ts index a9a941b1a0e1..995880f73c1f 100644 --- a/apps/meteor/client/hooks/useVoipClient.ts +++ b/apps/meteor/client/hooks/useVoipClient.ts @@ -5,10 +5,10 @@ import { useUser, useSetting, useEndpoint, useStream } from '@rocket.chat/ui-con import { KJUR } from 'jsrsasign'; import { useEffect, useState } from 'react'; +import { useHasLicenseModule } from './useHasLicenseModule'; import { EEVoipClient } from '../lib/voip/EEVoipClient'; import { VoIPUser } from '../lib/voip/VoIPUser'; import { useWebRtcServers } from '../providers/CallProvider/hooks/useWebRtcServers'; -import { useHasLicenseModule } from './useHasLicenseModule'; type UseVoipClientResult = { voipClient?: VoIPUser; @@ -23,7 +23,7 @@ const isSignedResponse = (data: any): data is { result: string } => typeof data? // Currently we only support the websocket connection and the SIP proxy connection being from the same host, // we need to add a new setting for SIP proxy if we want to support different hosts for them. export const useVoipClient = (): UseVoipClientResult => { - const settingVoipEnabled = Boolean(useSetting('VoIP_Enabled')); + const settingVoipEnabled = useSetting('VoIP_Enabled', false); const [voipConnectorEnabled, setVoipConnectorEnabled] = useSafely(useState(true)); diff --git a/apps/meteor/client/importPackages.ts b/apps/meteor/client/importPackages.ts index ddc173e63116..e08260b53032 100644 --- a/apps/meteor/client/importPackages.ts +++ b/apps/meteor/client/importPackages.ts @@ -36,12 +36,10 @@ import '../app/tokenpass/client'; import '../app/webrtc/client'; import '../app/wordpress/client'; import '../app/e2e/client'; -import '../app/discussion/client'; import '../app/threads/client'; import '../app/utils/client'; import '../app/settings/client'; import '../app/models/client'; import '../app/ui-utils/client'; -import '../app/ui-cached-collection/client'; import '../app/reactions/client'; import '../app/livechat/client'; diff --git a/apps/meteor/client/lib/RoomManager.ts b/apps/meteor/client/lib/RoomManager.ts index 840493aae406..c70d2278511c 100644 --- a/apps/meteor/client/lib/RoomManager.ts +++ b/apps/meteor/client/lib/RoomManager.ts @@ -2,8 +2,8 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; -import { RoomHistoryManager } from '../../app/ui-utils/client/lib/RoomHistoryManager'; import { getConfig } from './utils/getConfig'; +import { RoomHistoryManager } from '../../app/ui-utils/client/lib/RoomHistoryManager'; const debug = !!(getConfig('debug') || getConfig('debug-RoomStore')); diff --git a/apps/meteor/client/lib/VideoConfManager.ts b/apps/meteor/client/lib/VideoConfManager.ts index 7ae1c04db6df..a47d75179d92 100644 --- a/apps/meteor/client/lib/VideoConfManager.ts +++ b/apps/meteor/client/lib/VideoConfManager.ts @@ -3,8 +3,8 @@ import { Emitter } from '@rocket.chat/emitter'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { sdk } from '../../app/utils/client/lib/SDKClient'; import { getConfig } from './utils/getConfig'; +import { sdk } from '../../app/utils/client/lib/SDKClient'; const debug = !!(getConfig('debug') || getConfig('debug-VideoConf')); @@ -104,6 +104,8 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter; + private directCalls: DirectCallData[] = []; + private dismissedCalls: Set; private _preferences: CallPreferences; @@ -124,6 +126,13 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter(); this._preferences = { mic: true, cam: false }; this._capabilities = {}; + + this.on('incoming/changed', () => { + this.directCalls = [...this.incomingDirectCalls.values()] + // Filter out any calls that we're in the process of accepting, so they're already hidden from the UI + .filter((call) => !call.acceptTimeout) + .map(({ timeout: _, acceptTimeout: _t, ...call }) => ({ ...call, dismissed: this.isCallDismissed(call.callId) })); + }); } public isBusy(): boolean { @@ -147,12 +156,7 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter + ) : undefined + } + {...props} + /> + ); +}; + +export default RoomListCollapser; diff --git a/apps/meteor/client/sidebarv2/RoomList/RoomListRow.tsx b/apps/meteor/client/sidebarv2/RoomList/RoomListRow.tsx index 810368220ba9..473fad9b0e42 100644 --- a/apps/meteor/client/sidebarv2/RoomList/RoomListRow.tsx +++ b/apps/meteor/client/sidebarv2/RoomList/RoomListRow.tsx @@ -2,10 +2,10 @@ import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; import type { TFunction } from 'i18next'; import React, { memo, useMemo } from 'react'; +import SidebarItemTemplateWithData from './SidebarItemTemplateWithData'; import { useVideoConfAcceptCall, useVideoConfRejectIncomingCall, useVideoConfIncomingCalls } from '../../contexts/VideoConfContext'; import type { useAvatarTemplate } from '../hooks/useAvatarTemplate'; import type { useTemplateByViewMode } from '../hooks/useTemplateByViewMode'; -import SidebarItemTemplateWithData from './SidebarItemTemplateWithData'; type RoomListRowProps = { data: { diff --git a/apps/meteor/client/sidebarv2/RoomList/SidebarItemTemplateWithData.tsx b/apps/meteor/client/sidebarv2/RoomList/SidebarItemTemplateWithData.tsx index b286a95d451d..5417d3075c1e 100644 --- a/apps/meteor/client/sidebarv2/RoomList/SidebarItemTemplateWithData.tsx +++ b/apps/meteor/client/sidebarv2/RoomList/SidebarItemTemplateWithData.tsx @@ -6,6 +6,7 @@ import type { TFunction } from 'i18next'; import type { AllHTMLAttributes, ComponentType, ReactElement, ReactNode } from 'react'; import React, { memo, useMemo } from 'react'; +import { normalizeSidebarMessage } from './normalizeSidebarMessage'; import { RoomIcon } from '../../components/RoomIcon'; import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; import { isIOsDevice } from '../../lib/utils/isIOsDevice'; @@ -13,7 +14,7 @@ import { useOmnichannelPriorities } from '../../omnichannel/hooks/useOmnichannel import RoomMenu from '../RoomMenu'; import { OmnichannelBadges } from '../badges/OmnichannelBadges'; import type { useAvatarTemplate } from '../hooks/useAvatarTemplate'; -import { normalizeSidebarMessage } from './normalizeSidebarMessage'; +import { useUnreadDisplay } from '../hooks/useUnreadDisplay'; export const getMessage = (room: IRoom, lastMessage: IMessage | undefined, t: TFunction): string | undefined => { if (!lastMessage) { @@ -34,24 +35,6 @@ export const getMessage = (room: IRoom, lastMessage: IMessage | undefined, t: TF return `${lastMessage.u.name || lastMessage.u.username}: ${normalizeSidebarMessage(lastMessage, t)}`; }; -export const getBadgeTitle = (userMentions: number, threadUnread: number, groupMentions: number, unread: number, t: TFunction) => { - const title = [] as string[]; - if (userMentions) { - title.push(t('mentions_counter', { count: userMentions })); - } - if (threadUnread) { - title.push(t('threads_counter', { count: threadUnread })); - } - if (groupMentions) { - title.push(t('group_mentions_counter', { count: groupMentions })); - } - const count = unread - userMentions - groupMentions; - if (count > 0) { - title.push(t('unread_messages_counter', { count })); - } - return title.join(', '); -}; - type RoomListRowProps = { extended: boolean; t: TFunction; @@ -109,22 +92,10 @@ const SidebarItemTemplateWithData = ({ const href = roomCoordinator.getRouteLink(room.t, room) || ''; const title = roomCoordinator.getRoomName(room.t, room) || ''; - const { - lastMessage, - hideUnreadStatus, - hideMentionStatus, - unread = 0, - alert, - userMentions, - groupMentions, - tunread = [], - tunreadUser = [], - rid, - t: type, - cl, - } = room; + const { unreadTitle, unreadVariant, showUnread, unreadCount, highlightUnread: highlighted } = useUnreadDisplay(room); + + const { lastMessage, unread = 0, alert, rid, t: type, cl } = room; - const highlighted = Boolean(!hideUnreadStatus && (alert || unread)); const icon = ( : null; - const threadUnread = tunread.length > 0; - const variant = - ((userMentions || tunreadUser.length) && 'danger') || (threadUnread && 'primary') || (groupMentions && 'warning') || 'secondary'; - - const isUnread = unread > 0 || threadUnread; - const showBadge = !hideUnreadStatus || (!hideMentionStatus && (Boolean(userMentions) || tunreadUser.length > 0)); - - const badgeTitle = getBadgeTitle(userMentions, tunread.length, groupMentions, unread, t); - const badges = ( <> - {showBadge && isUnread && ( - - {unread + tunread?.length} + {showUnread && ( + + {unreadCount.total} )} {isOmnichannelRoom(room) && } @@ -197,7 +159,7 @@ const SidebarItemTemplateWithData = ({ ((): ReactElement => ( 0} rid={rid} unread={!!unread} roomOpen={selected} diff --git a/apps/meteor/client/sidebarv2/RoomMenu.tsx b/apps/meteor/client/sidebarv2/RoomMenu.tsx index aac68b9ef79e..8b031e8cfced 100644 --- a/apps/meteor/client/sidebarv2/RoomMenu.tsx +++ b/apps/meteor/client/sidebarv2/RoomMenu.tsx @@ -225,7 +225,7 @@ const RoomMenu = ({ }, action: handleToggleFavorite, }, - } + } : {}), ...(canLeave && { leaveRoom: { diff --git a/apps/meteor/client/sidebarv2/footer/SidebarFooter.tsx b/apps/meteor/client/sidebarv2/footer/SidebarFooter.tsx index f31f1a3e6b8f..7b4da5604137 100644 --- a/apps/meteor/client/sidebarv2/footer/SidebarFooter.tsx +++ b/apps/meteor/client/sidebarv2/footer/SidebarFooter.tsx @@ -1,9 +1,9 @@ import type { ReactElement } from 'react'; import React from 'react'; -import { useIsCallEnabled, useIsCallReady } from '../../contexts/CallContext'; import SidebarFooterDefault from './SidebarFooterDefault'; import { VoipFooter } from './voip'; +import { useIsCallEnabled, useIsCallReady } from '../../contexts/CallContext'; const SidebarFooter = (): ReactElement => { const isCallEnabled = useIsCallEnabled(); diff --git a/apps/meteor/client/sidebarv2/footer/SidebarFooterDefault.tsx b/apps/meteor/client/sidebarv2/footer/SidebarFooterDefault.tsx index 3d1c17dcebbe..c40e36173428 100644 --- a/apps/meteor/client/sidebarv2/footer/SidebarFooterDefault.tsx +++ b/apps/meteor/client/sidebarv2/footer/SidebarFooterDefault.tsx @@ -9,7 +9,7 @@ import { SidebarFooterWatermark } from './SidebarFooterWatermark'; const SidebarFooterDefault = (): ReactElement => { const [, , theme] = useThemeMode(); - const logo = String(useSetting(theme === 'dark' ? 'Layout_Sidenav_Footer_Dark' : 'Layout_Sidenav_Footer')).trim(); + const logo = useSetting(theme === 'dark' ? 'Layout_Sidenav_Footer_Dark' : 'Layout_Sidenav_Footer', '').trim(); const sidebarFooterStyle = css` & img { diff --git a/apps/meteor/client/sidebarv2/footer/voip/VoipFooter.tsx b/apps/meteor/client/sidebarv2/footer/voip/VoipFooter.tsx index 9344ba33c9ad..80154f307dd6 100644 --- a/apps/meteor/client/sidebarv2/footer/voip/VoipFooter.tsx +++ b/apps/meteor/client/sidebarv2/footer/voip/VoipFooter.tsx @@ -6,9 +6,9 @@ import type { ReactElement, MouseEvent, ReactNode } from 'react'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import { useOmnichannelContactLabel } from './hooks/useOmnichannelContactLabel'; import type { CallActionsType } from '../../../contexts/CallContext'; import type { VoipFooterMenuOptions } from '../../../hooks/useVoipFooterMenu'; -import { useOmnichannelContactLabel } from './hooks/useOmnichannelContactLabel'; type VoipFooterProps = { caller: ICallerInfo; @@ -56,7 +56,7 @@ const VoipFooter = ({ callerState === 'IN_CALL' || callerState === 'ON_HOLD' ? css` cursor: pointer; - ` + ` : ''; const handleHold = (e: MouseEvent): void => { diff --git a/apps/meteor/client/sidebarv2/footer/voip/index.tsx b/apps/meteor/client/sidebarv2/footer/voip/index.tsx index bc6226201511..0fd0d29ee910 100644 --- a/apps/meteor/client/sidebarv2/footer/voip/index.tsx +++ b/apps/meteor/client/sidebarv2/footer/voip/index.tsx @@ -1,7 +1,8 @@ import type { VoIpCallerInfo } from '@rocket.chat/core-typings'; -import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useCallActions, @@ -18,7 +19,7 @@ import SidebarFooterDefault from '../SidebarFooterDefault'; import VoipFooterComponent from './VoipFooter'; export const VoipFooter = (): ReactElement | null => { - const t = useTranslation(); + const { t } = useTranslation(); const callerInfo = useCallerInfo(); const callActions = useCallActions(); const dispatchEvent = useEndpoint('POST', '/v1/voip/events'); diff --git a/apps/meteor/client/sidebarv2/header/CreateChannelModal.tsx b/apps/meteor/client/sidebarv2/header/CreateChannelModal.tsx index 2ff468f4f72b..964b808cb10f 100644 --- a/apps/meteor/client/sidebarv2/header/CreateChannelModal.tsx +++ b/apps/meteor/client/sidebarv2/header/CreateChannelModal.tsx @@ -29,10 +29,10 @@ import type { ComponentProps, ReactElement } from 'react'; import React, { useEffect, useMemo } from 'react'; import { useForm, Controller } from 'react-hook-form'; +import { useEncryptedRoomDescription } from './hooks/useEncryptedRoomDescription'; import UserAutoCompleteMultipleFederated from '../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; import { useHasLicenseModule } from '../../hooks/useHasLicenseModule'; import { goToRoomById } from '../../lib/utils/goToRoomById'; -import { useEncryptedRoomDescription } from './hooks/useEncryptedRoomDescription'; type CreateChannelModalProps = { teamId?: string; @@ -67,7 +67,7 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal const e2eEnabled = useSetting('E2E_Enable'); const namesValidation = useSetting('UTF8_Channel_Names_Validation'); const allowSpecialNames = useSetting('UI_Allow_room_names_with_special_chars'); - const federationEnabled = useSetting('Federation_Matrix_enabled') || false; + const federationEnabled = useSetting('Federation_Matrix_enabled', false); const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms') && e2eEnabled; const canCreateChannel = usePermission('create-c'); diff --git a/apps/meteor/client/sidebarv2/header/CreateDirectMessage.tsx b/apps/meteor/client/sidebarv2/header/CreateDirectMessage.tsx index 070c363a0273..8597541e485a 100644 --- a/apps/meteor/client/sidebarv2/header/CreateDirectMessage.tsx +++ b/apps/meteor/client/sidebarv2/header/CreateDirectMessage.tsx @@ -13,7 +13,7 @@ type CreateDirectMessageProps = { onClose: () => void }; const CreateDirectMessage = ({ onClose }: CreateDirectMessageProps) => { const t = useTranslation(); - const directMaxUsers = useSetting('DirectMesssage_maxUsers') || 1; + const directMaxUsers = useSetting('DirectMesssage_maxUsers', 1); const membersFieldId = useUniqueId(); const dispatchToastMessage = useToastMessageDispatch(); diff --git a/apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx b/apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx index 9de721d8bbcd..b1406b991696 100644 --- a/apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx +++ b/apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx @@ -15,6 +15,7 @@ import { FieldHint, Accordion, AccordionItem, + Divider, } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; @@ -30,9 +31,9 @@ import type { ComponentProps, ReactElement } from 'react'; import React, { memo, useEffect, useMemo } from 'react'; import { Controller, useForm } from 'react-hook-form'; +import { useEncryptedRoomDescription } from './hooks/useEncryptedRoomDescription'; import UserAutoCompleteMultiple from '../../components/UserAutoCompleteMultiple'; import { goToRoomById } from '../../lib/utils/goToRoomById'; -import { useEncryptedRoomDescription } from './hooks/useEncryptedRoomDescription'; type CreateTeamModalInputs = { name: string; @@ -296,6 +297,7 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => { {t('Show_discussions_description')} + diff --git a/apps/meteor/client/sidebarv2/header/MatrixFederationSearch/FederatedRoomList.tsx b/apps/meteor/client/sidebarv2/header/MatrixFederationSearch/FederatedRoomList.tsx index b4ddbf32419d..0e01cacda0ee 100644 --- a/apps/meteor/client/sidebarv2/header/MatrixFederationSearch/FederatedRoomList.tsx +++ b/apps/meteor/client/sidebarv2/header/MatrixFederationSearch/FederatedRoomList.tsx @@ -1,15 +1,16 @@ import { Throbber, Box } from '@rocket.chat/fuselage'; import type { IFederationPublicRooms } from '@rocket.chat/rest-typings'; -import { useSetModal, useEndpoint, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetModal, useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { Virtuoso } from 'react-virtuoso'; -import { VirtuosoScrollbars } from '../../../components/CustomScrollbars'; -import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; import FederatedRoomListEmptyPlaceholder from './FederatedRoomListEmptyPlaceholder'; import FederatedRoomListItem from './FederatedRoomListItem'; import { useInfiniteFederationSearchPublicRooms } from './useInfiniteFederationSearchPublicRooms'; +import { VirtuosoScrollbars } from '../../../components/CustomScrollbars'; +import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; type FederatedRoomListProps = { serverName: string; @@ -22,7 +23,7 @@ const FederatedRoomList = ({ serverName, roomName, count }: FederatedRoomListPro const joinExternalPublicRoom = useEndpoint('POST', '/v1/federation/joinExternalPublicRoom'); const setModal = useSetModal(); - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const { data, isLoading, isFetchingNextPage, fetchNextPage } = useInfiniteFederationSearchPublicRooms(serverName, roomName, count); diff --git a/apps/meteor/client/sidebarv2/header/MatrixFederationSearch/MatrixFederationSearchModalContent.tsx b/apps/meteor/client/sidebarv2/header/MatrixFederationSearch/MatrixFederationSearchModalContent.tsx index ec6396a83440..6ee8e257c294 100644 --- a/apps/meteor/client/sidebarv2/header/MatrixFederationSearch/MatrixFederationSearchModalContent.tsx +++ b/apps/meteor/client/sidebarv2/header/MatrixFederationSearch/MatrixFederationSearchModalContent.tsx @@ -1,9 +1,10 @@ import type { SelectOption } from '@rocket.chat/fuselage'; import { Box, Select, TextInput } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetModal } from '@rocket.chat/ui-contexts'; import type { FormEvent } from 'react'; import React, { useCallback, useState, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import FederatedRoomList from './FederatedRoomList'; import FederatedRoomListErrorBoundary from './FederatedRoomListErrorBoundary'; @@ -31,7 +32,7 @@ const MatrixFederationSearchModalContent = ({ defaultSelectedServer, servers }: const debouncedRoomName = useDebouncedValue(roomName, 400); - const t = useTranslation(); + const { t } = useTranslation(); const serverOptions = useMemo>(() => servers.map((server): SelectOption => [server.name, server.name]), [servers]); diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomMenu.tsx b/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomMenu.tsx index 6a0c58b36311..795944b8d819 100644 --- a/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomMenu.tsx +++ b/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomMenu.tsx @@ -1,13 +1,14 @@ -import { useAtLeastOnePermission, useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import { useAtLeastOnePermission, useSetting } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; -import { useIsEnterprise } from '../../../../hooks/useIsEnterprise'; import { useCreateRoomItems } from './useCreateRoomItems'; import { useMatrixFederationItems } from './useMatrixFederationItems'; +import { useIsEnterprise } from '../../../../hooks/useIsEnterprise'; const CREATE_ROOM_PERMISSIONS = ['create-c', 'create-p', 'create-d', 'start-discussion', 'start-discussion-other-user']; export const useCreateRoom = () => { - const t = useTranslation(); + const { t } = useTranslation(); const showCreate = useAtLeastOnePermission(CREATE_ROOM_PERMISSIONS); const { data } = useIsEnterprise(); diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.tsx b/apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.tsx index 5dcb53f2ad2f..880d6d5f7e48 100644 --- a/apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.tsx +++ b/apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.tsx @@ -1,10 +1,11 @@ import { CheckBox } from '@rocket.chat/fuselage'; import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; -import { useEndpoint, useUserPreference, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useUserPreference } from '@rocket.chat/ui-contexts'; import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; export const useGroupingListItems = (): GenericMenuItemProps[] => { - const t = useTranslation(); + const { t } = useTranslation(); const sidebarGroupByType = useUserPreference('sidebarGroupByType'); const sidebarShowFavorites = useUserPreference('sidebarShowFavorites'); diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.tsx b/apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.tsx index 0e2bf818a0bf..4e8e7566c8e6 100644 --- a/apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.tsx +++ b/apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.tsx @@ -1,7 +1,8 @@ import { RadioButton } from '@rocket.chat/fuselage'; import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; -import { useEndpoint, useUserPreference, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useUserPreference } from '@rocket.chat/ui-contexts'; import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { OmnichannelSortingDisclaimer, @@ -9,7 +10,7 @@ import { } from '../../../../components/Omnichannel/OmnichannelSortingDisclaimer'; export const useSortModeItems = (): GenericMenuItemProps[] => { - const t = useTranslation(); + const { t } = useTranslation(); const saveUserPreferences = useEndpoint('POST', '/v1/users.setPreferences'); const sidebarSortBy = useUserPreference<'activity' | 'alphabetical'>('sidebarSortby', 'activity'); diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.tsx b/apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.tsx index a33230a15729..06fe43117d47 100644 --- a/apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.tsx +++ b/apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.tsx @@ -1,10 +1,11 @@ import { RadioButton, ToggleSwitch } from '@rocket.chat/fuselage'; import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; -import { useEndpoint, useUserPreference, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useUserPreference } from '@rocket.chat/ui-contexts'; import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; export const useViewModeItems = (): GenericMenuItemProps[] => { - const t = useTranslation(); + const { t } = useTranslation(); const saveUserPreferences = useEndpoint('POST', '/v1/users.setPreferences'); diff --git a/apps/meteor/client/sidebarv2/header/hooks/useEncryptedRoomDescription.tsx b/apps/meteor/client/sidebarv2/header/hooks/useEncryptedRoomDescription.tsx index 09796dd7a6b7..0cef06d39173 100644 --- a/apps/meteor/client/sidebarv2/header/hooks/useEncryptedRoomDescription.tsx +++ b/apps/meteor/client/sidebarv2/header/hooks/useEncryptedRoomDescription.tsx @@ -1,7 +1,8 @@ -import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; export const useEncryptedRoomDescription = (roomType: 'channel' | 'team') => { - const t = useTranslation(); + const { t } = useTranslation(); const e2eEnabled = useSetting('E2E_Enable'); const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms'); diff --git a/apps/meteor/client/sidebarv2/hooks/useRoomList.spec.tsx b/apps/meteor/client/sidebarv2/hooks/useRoomList.spec.tsx new file mode 100644 index 000000000000..bf4645b25db3 --- /dev/null +++ b/apps/meteor/client/sidebarv2/hooks/useRoomList.spec.tsx @@ -0,0 +1,313 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; +import { renderHook } from '@testing-library/react'; +import React from 'react'; + +import { useRoomList } from './useRoomList'; +import { createFakeRoom, createFakeSubscription, createFakeUser } from '../../../tests/mocks/data'; +import { VideoConfContext } from '../../contexts/VideoConfContext'; + +const user = createFakeUser({ + active: true, + roles: ['admin'], + type: 'user', +}); + +const emptyUnread = { + userMentions: 0, + groupMentions: 0, + unread: 0, + tunread: undefined, + tunreadUser: undefined, + tunreadGroup: undefined, + alert: false, +}; + +const unreadChannels = [ + { ...createFakeSubscription({ t: 'c', tunread: ['1'] }), ...createFakeRoom({ t: 'c' }) }, + { ...createFakeSubscription({ t: 'c', tunread: ['1'] }), ...createFakeRoom({ t: 'c' }) }, + { ...createFakeSubscription({ t: 'c', tunreadUser: ['1'] }), ...createFakeRoom({ t: 'c' }) }, + { ...createFakeSubscription({ t: 'c', tunreadUser: ['1'] }), ...createFakeRoom({ t: 'c' }) }, +]; + +const favoriteRooms = [ + { ...createFakeSubscription({ t: 'c', f: true, ...emptyUnread }), ...createFakeRoom({ t: 'c' }) }, + { ...createFakeSubscription({ t: 'c', f: true, ...emptyUnread }), ...createFakeRoom({ t: 'c' }) }, + { ...createFakeSubscription({ t: 'c', f: true, ...emptyUnread }), ...createFakeRoom({ t: 'c' }) }, +]; + +const teams = [ + { ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ teamMain: true }) }, + { ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ teamMain: true }) }, + { ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ teamMain: true }) }, + { ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ teamMain: true }) }, + { ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ teamMain: true }) }, +]; + +const discussionRooms = [ + { ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ prid: '123' }) }, + { ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ prid: '124' }) }, + { ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ prid: '125' }) }, + { ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ prid: '126' }) }, + { ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ prid: '127' }) }, +]; + +const directRooms = [ + { ...createFakeSubscription({ t: 'd', ...emptyUnread }), ...createFakeRoom({ t: 'd' }) }, + { ...createFakeSubscription({ t: 'd', ...emptyUnread }), ...createFakeRoom({ t: 'd' }) }, + { ...createFakeSubscription({ t: 'd', ...emptyUnread }), ...createFakeRoom({ t: 'd' }) }, + { ...createFakeSubscription({ t: 'd', ...emptyUnread }), ...createFakeRoom({ t: 'd' }) }, +]; + +const fakeRooms = [...unreadChannels, ...favoriteRooms, ...teams, ...discussionRooms, ...directRooms]; + +const emptyArr: any[] = []; + +const getWrapperSettings = ({ + sidebarGroupByType = false, + sidebarShowFavorites = false, + isDiscussionEnabled = false, + sidebarShowUnread = false, + fakeRoom = undefined, +}: { + sidebarGroupByType?: boolean; + sidebarShowFavorites?: boolean; + isDiscussionEnabled?: boolean; + sidebarShowUnread?: boolean; + fakeRoom?: SubscriptionWithRoom; +}) => + mockAppRoot() + .wrap((children) => ( + () => undefined, + getSnapshot: () => { + return emptyArr; + }, + }, + } as any + } + children={children} + /> + )) + .withUser(user) + .withSubscriptions([...fakeRooms, fakeRoom && fakeRoom].filter(Boolean) as unknown as SubscriptionWithRoom[]) + .withUserPreference('sidebarGroupByType', sidebarGroupByType) + .withUserPreference('sidebarShowFavorites', sidebarShowFavorites) + .withUserPreference('sidebarShowUnread', sidebarShowUnread) + .withSetting('Discussion_enabled', isDiscussionEnabled); + +it('should return roomList, groupsCount and groupsList', async () => { + const { + result: { + current: { roomList, groupsList, groupsCount }, + }, + } = renderHook(() => useRoomList({ collapsedGroups: [] }), { + legacyRoot: true, + wrapper: getWrapperSettings({}).build(), + }); + + expect(roomList).toBeDefined(); + expect(groupsList).toBeDefined(); + expect(groupsCount).toBeDefined(); +}); + +it('should return groupsCount with the correct count', async () => { + const { + result: { + current: { groupsCount, roomList }, + }, + } = renderHook(() => useRoomList({ collapsedGroups: [] }), { + legacyRoot: true, + wrapper: getWrapperSettings({}).build(), + }); + + expect(groupsCount).toContain(fakeRooms.length); + expect(groupsCount).not.toContain(fakeRooms.length + 5); + expect(groupsCount.reduce((a, b) => a + b, 0)).toBe(fakeRooms.length); + expect(groupsCount.reduce((a, b) => a + b, 0)).toEqual(roomList.length); +}); + +it('should return roomList with the subscribed rooms and the correct length', async () => { + const { + result: { + current: { roomList }, + }, + } = renderHook(() => useRoomList({ collapsedGroups: [] }), { + legacyRoot: true, + wrapper: getWrapperSettings({}).build(), + }); + expect(roomList).toContain(fakeRooms[0]); + expect(roomList).toHaveLength(fakeRooms.length); +}); + +it('should return groupsList with "Conversations" if preference sidebarGroupByType is not enabled', async () => { + const { + result: { + current: { groupsList }, + }, + } = renderHook(() => useRoomList({ collapsedGroups: [] }), { + legacyRoot: true, + wrapper: getWrapperSettings({}).build(), + }); + expect(groupsList).toContain('Conversations'); + expect(groupsList).toHaveLength(1); +}); + +it('should return groupsList with "Teams" if sidebarGroupByType is enabled and roomList has teams', async () => { + const { + result: { + current: { groupsList, groupsCount }, + }, + } = renderHook(() => useRoomList({ collapsedGroups: [] }), { + legacyRoot: true, + wrapper: getWrapperSettings({ sidebarGroupByType: true }).build(), + }); + + const teamsIndex = groupsList.indexOf('Teams'); + expect(groupsList).toContain('Teams'); + expect(groupsCount[teamsIndex]).toEqual(teams.length); +}); + +it('should return groupsList with "Favorites" if sidebarShowFavorites is enabled', async () => { + const { + result: { + current: { groupsList, groupsCount }, + }, + } = renderHook(() => useRoomList({ collapsedGroups: [] }), { + legacyRoot: true, + wrapper: getWrapperSettings({ sidebarShowFavorites: true, sidebarGroupByType: true }).build(), + }); + + const favoritesIndex = groupsList.indexOf('Favorites'); + expect(groupsList).toContain('Favorites'); + expect(groupsCount[favoritesIndex]).toEqual(favoriteRooms.length); +}); + +it('should return groupsList with "Discussions" if isDiscussionEnabled is enabled', async () => { + const { + result: { + current: { groupsList, groupsCount }, + }, + } = renderHook(() => useRoomList({ collapsedGroups: [] }), { + legacyRoot: true, + wrapper: getWrapperSettings({ isDiscussionEnabled: true, sidebarGroupByType: true }).build(), + }); + + const discussionIndex = groupsList.indexOf('Discussions'); + expect(groupsList).toContain('Discussions'); + expect(groupsCount[discussionIndex]).toEqual(discussionRooms.length); +}); + +it('should return groupsList without "Discussions" if isDiscussionEnabled is disabled', async () => { + const { result } = renderHook(() => useRoomList({ collapsedGroups: [] }), { + legacyRoot: true, + wrapper: getWrapperSettings({ isDiscussionEnabled: false, sidebarGroupByType: true }).build(), + }); + expect(result.current.groupsList).not.toContain('Discussions'); +}); + +it('should remove corresponding items from roomList and return groupCount 0 when group is collapsed', async () => { + const { + result: { + current: { roomList, groupsCount, groupsList }, + }, + } = renderHook(() => useRoomList({ collapsedGroups: ['Channels'] }), { + legacyRoot: true, + wrapper: getWrapperSettings({ sidebarGroupByType: true }).build(), + }); + const channelsIndex = groupsList.indexOf('Channels'); + expect(groupsCount[channelsIndex]).toEqual(0); + expect(roomList.length).toEqual(groupsCount.reduce((a, b) => a + b, 0)); +}); + +it('should always return groupsCount and groupsList with the same length', async () => { + const { result } = renderHook(() => useRoomList({ collapsedGroups: [] }), { + legacyRoot: true, + wrapper: getWrapperSettings({ sidebarGroupByType: true }).build(), + }); + expect(result.current.groupsCount.length).toEqual(result.current.groupsList.length); +}); + +it('should return "Unread" group with the correct items if sidebarShowUnread is enabled', async () => { + const { result } = renderHook(() => useRoomList({ collapsedGroups: [] }), { + legacyRoot: true, + wrapper: getWrapperSettings({ sidebarShowUnread: true, sidebarGroupByType: true }).build(), + }); + const unreadIndex = result.current.groupsList.indexOf('Unread'); + expect(result.current.groupsList).toContain('Unread'); + expect(result.current.groupsCount[unreadIndex]).toEqual(unreadChannels.length); +}); + +it('should not include unread room in unread group if hideUnreadStatus is enabled', async () => { + const { result } = renderHook(() => useRoomList({ collapsedGroups: [] }), { + legacyRoot: true, + wrapper: getWrapperSettings({ + sidebarShowUnread: true, + sidebarGroupByType: true, + fakeRoom: { + ...createFakeSubscription({ t: 'c', unread: 1, hideUnreadStatus: true }), + ...createFakeRoom({ t: 'c' }), + } as unknown as SubscriptionWithRoom, + }).build(), + }); + const unreadIndex = result.current.groupsList.indexOf('Unread'); + const roomListUnread = result.current.roomList.filter((room) => room.unread); + + expect(result.current.groupsCount[unreadIndex]).toEqual(unreadChannels.length); + expect(roomListUnread.length).not.toEqual(unreadChannels.length); +}); + +it('should accumulate unread data into `groupedUnreadInfo` when group is collapsed', async () => { + const { result } = renderHook(() => useRoomList({ collapsedGroups: ['Channels'] }), { + legacyRoot: true, + wrapper: getWrapperSettings({ sidebarGroupByType: true }).build(), + }); + + const channelsIndex = result.current.groupsList.indexOf('Channels'); + const { groupMentions, unread, userMentions, tunread, tunreadUser } = result.current.groupedUnreadInfo[channelsIndex]; + + expect(groupMentions).toEqual(fakeRooms.reduce((acc, cv) => acc + cv.groupMentions, 0)); + expect(unread).toEqual(fakeRooms.reduce((acc, cv) => acc + cv.unread, 0)); + expect(userMentions).toEqual(fakeRooms.reduce((acc, cv) => acc + cv.userMentions, 0)); + expect(tunread).toEqual(fakeRooms.reduce((acc, cv) => [...acc, ...(cv.tunread || [])], [] as string[])); + expect(tunreadUser).toEqual(fakeRooms.reduce((acc, cv) => [...acc, ...(cv.tunreadUser || [])], [] as string[])); +}); + +it('should add to unread group when has thread unread, even if alert is false', async () => { + const fakeRoom = { + ...createFakeSubscription({ ...emptyUnread, tunread: ['1'], alert: false }), + } as unknown as SubscriptionWithRoom; + + const { result } = renderHook(() => useRoomList({ collapsedGroups: [] }), { + legacyRoot: true, + wrapper: getWrapperSettings({ + sidebarGroupByType: true, + sidebarShowUnread: true, + fakeRoom, + }).build(), + }); + + const unreadGroup = result.current.roomList.splice(0, result.current.groupsCount[0]); + expect(unreadGroup.find((room) => room.name === fakeRoom.name)).toBeDefined(); +}); + +it('should not add room to unread group if thread unread is an empty array', async () => { + const fakeRoom = { + ...createFakeSubscription({ ...emptyUnread, tunread: [] }), + } as unknown as SubscriptionWithRoom; + + const { result } = renderHook(() => useRoomList({ collapsedGroups: [] }), { + legacyRoot: true, + wrapper: getWrapperSettings({ + sidebarGroupByType: true, + sidebarShowUnread: true, + fakeRoom, + }).build(), + }); + + const unreadGroup = result.current.roomList.splice(0, result.current.groupsCount[0]); + expect(unreadGroup.find((room) => room.name === fakeRoom.name)).toBeUndefined(); +}); diff --git a/apps/meteor/client/sidebarv2/hooks/useRoomList.ts b/apps/meteor/client/sidebarv2/hooks/useRoomList.ts index 5f9a1234c269..921d956147ba 100644 --- a/apps/meteor/client/sidebarv2/hooks/useRoomList.ts +++ b/apps/meteor/client/sidebarv2/hooks/useRoomList.ts @@ -1,13 +1,13 @@ import type { ILivechatInquiryRecord, IRoom, ISubscription } from '@rocket.chat/core-typings'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import type { SubscriptionWithRoom, TranslationKey } from '@rocket.chat/ui-contexts'; import { useUserPreference, useUserSubscriptions, useSetting } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; import { useVideoConfIncomingCalls } from '../../contexts/VideoConfContext'; import { useOmnichannelEnabled } from '../../hooks/omnichannel/useOmnichannelEnabled'; import { useQueuedInquiries } from '../../hooks/omnichannel/useQueuedInquiries'; -import { useQueryOptions } from './useQueryOptions'; +import { useSortQueryOptions } from '../../hooks/useSortQueryOptions'; const query = { open: { $ne: false } }; @@ -27,15 +27,16 @@ const order = [ 'Conversations', ] as const; -export const useRoomList = ({ - collapsedGroups, -}: { - collapsedGroups?: string[]; -}): { +type useRoomListReturnType = { roomList: Array; groupsCount: number[]; groupsList: TranslationKey[]; -} => { + groupedUnreadInfo: Pick< + SubscriptionWithRoom, + 'userMentions' | 'groupMentions' | 'unread' | 'tunread' | 'tunreadUser' | 'tunreadGroup' | 'alert' | 'hideUnreadStatus' + >[]; +}; +export const useRoomList = ({ collapsedGroups }: { collapsedGroups?: string[] }): useRoomListReturnType => { const showOmnichannel = useOmnichannelEnabled(); const sidebarGroupByType = useUserPreference('sidebarGroupByType'); const favoritesEnabled = useUserPreference('sidebarShowFavorites'); @@ -43,7 +44,7 @@ export const useRoomList = ({ const isDiscussionEnabled = useSetting('Discussion_enabled'); const sidebarShowUnread = useUserPreference('sidebarShowUnread'); - const options = useQueryOptions(); + const options = useSortQueryOptions(); const rooms = useUserSubscriptions(query, options); @@ -53,7 +54,7 @@ export const useRoomList = ({ const queue = inquiries.enabled ? inquiries.queue : emptyQueue; - const { groupsCount, groupsList, roomList } = useDebouncedValue( + const { groupsCount, groupsList, roomList, groupedUnreadInfo } = useDebouncedValue( useMemo(() => { const isCollapsed = (groupTitle: string) => collapsedGroups?.includes(groupTitle); @@ -77,7 +78,7 @@ export const useRoomList = ({ return incomingCall.add(room); } - if (sidebarShowUnread && (room.alert || room.unread) && !room.hideUnreadStatus) { + if (sidebarShowUnread && (room.alert || room.unread || room.tunread?.length) && !room.hideUnreadStatus) { return unread.add(room); } @@ -133,7 +134,7 @@ export const useRoomList = ({ !sidebarGroupByType && groups.set('Conversations', conversation); - const { groupsCount, groupsList, roomList } = sidebarOrder.reduce( + const { groupsCount, groupsList, roomList, groupedUnreadInfo } = sidebarOrder.reduce( (acc, key) => { const value = groups.get(key); @@ -142,11 +143,39 @@ export const useRoomList = ({ } acc.groupsList.push(key as TranslationKey); + + const groupedUnreadInfoAcc = { + userMentions: 0, + groupMentions: 0, + tunread: [], + tunreadUser: [], + unread: 0, + }; + if (isCollapsed(key)) { + const groupedUnreadInfo = [...value].reduce( + (counter, { userMentions, groupMentions, tunread, tunreadUser, unread, alert, hideUnreadStatus }) => { + if (hideUnreadStatus) { + return counter; + } + + counter.userMentions += userMentions || 0; + counter.groupMentions += groupMentions || 0; + counter.tunread = [...counter.tunread, ...(tunread || [])]; + counter.tunreadUser = [...counter.tunreadUser, ...(tunreadUser || [])]; + counter.unread += unread || 0; + !unread && !tunread?.length && alert && (counter.unread += 1); + return counter; + }, + groupedUnreadInfoAcc, + ); + + acc.groupedUnreadInfo.push(groupedUnreadInfo); acc.groupsCount.push(0); return acc; } + acc.groupedUnreadInfo.push(groupedUnreadInfoAcc); acc.groupsCount.push(value.size); acc.roomList.push(...value); return acc; @@ -155,14 +184,11 @@ export const useRoomList = ({ groupsCount: [], groupsList: [], roomList: [], - } as { - groupsCount: number[]; - groupsList: TranslationKey[]; - roomList: Array; - }, + groupedUnreadInfo: [], + } as useRoomListReturnType, ); - return { groupsCount, groupsList, roomList }; + return { groupsCount, groupsList, roomList, groupedUnreadInfo }; }, [ rooms, showOmnichannel, @@ -183,5 +209,6 @@ export const useRoomList = ({ roomList, groupsCount, groupsList, + groupedUnreadInfo, }; }; diff --git a/apps/meteor/client/sidebarv2/hooks/useUnreadDisplay.spec.tsx b/apps/meteor/client/sidebarv2/hooks/useUnreadDisplay.spec.tsx new file mode 100644 index 000000000000..0ae7f82e391b --- /dev/null +++ b/apps/meteor/client/sidebarv2/hooks/useUnreadDisplay.spec.tsx @@ -0,0 +1,237 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { renderHook } from '@testing-library/react'; + +import { useUnreadDisplay } from './useUnreadDisplay'; +import { createFakeSubscription } from '../../../tests/mocks/data'; + +const dmUnread = createFakeSubscription({ + t: 'd', + unread: 3, + userMentions: 0, + groupMentions: 0, + tunread: undefined, + tunreadUser: undefined, +}); + +const dmThread = createFakeSubscription({ + t: 'd', + unread: 3, + userMentions: 0, + groupMentions: 0, + tunread: ['1'], + tunreadUser: undefined, +}); + +const alert = createFakeSubscription({ + t: 'p', + unread: 0, + userMentions: 0, + groupMentions: 0, + tunread: undefined, + tunreadUser: undefined, + alert: true, +}); + +const mentionAndGroupMention = createFakeSubscription({ + t: 'p', + unread: 2, + userMentions: 1, + groupMentions: 1, + tunread: undefined, + tunreadUser: undefined, + alert: true, +}); + +const groupMention = createFakeSubscription({ + t: 'p', + unread: 2, + userMentions: 0, + groupMentions: 2, + tunread: undefined, + tunreadUser: undefined, + alert: true, +}); + +const tunread = createFakeSubscription({ + t: 'p', + unread: 0, + userMentions: 0, + groupMentions: 0, + tunread: ['1'], + tunreadUser: undefined, + alert: true, +}); + +const tunreadUser = createFakeSubscription({ + t: 'p', + unread: 1, + userMentions: 0, + groupMentions: 0, + tunread: ['1'], + tunreadUser: ['1'], + alert: true, +}); + +const hideUnreadStatus = createFakeSubscription({ + t: 'p', + hideUnreadStatus: true, +}); + +const hideUnreadAndMention = createFakeSubscription({ + t: 'p', + hideUnreadStatus: true, + hideMentionStatus: true, +}); + +const noUnread = createFakeSubscription({ + t: 'p', + unread: 0, + userMentions: 0, + groupMentions: 0, + tunread: undefined, + tunreadUser: undefined, +}); + +const wrapper = mockAppRoot() + .withTranslations('en', 'core', { + mentions_counter_one: '{{count}} mention', + mentions_counter_other: '{{count}} mentions', + threads_counter_one: '{{count}} unread threaded message', + threads_counter_other: '{{count}} unread threaded messages', + group_mentions_counter_one: '{{count}} group mention', + group_mentions_counter_other: '{{count}} group mentions', + unread_messages_counter_one: '{{count}} unread message', + unread_messages_counter_other: '{{count}} unread messages', + }) + .build(); + +it('should return correct unread data for [Direct message unread]', async () => { + const { result } = renderHook(() => useUnreadDisplay(dmUnread), { + legacyRoot: true, + wrapper, + }); + expect(result.current.unreadVariant).toBe('secondary'); + expect(result.current.unreadTitle).toBe('3 unread messages'); + + expect(result.current.unreadCount).toHaveProperty('mentions', 0); + expect(result.current.unreadCount).toHaveProperty('threads', 0); + expect(result.current.unreadCount).toHaveProperty('groupMentions', 0); + expect(result.current.unreadCount).toHaveProperty('total', 3); +}); + +it('should return correct unread data for [Direct message with thread unread]', async () => { + const { result } = renderHook(() => useUnreadDisplay(dmThread), { + legacyRoot: true, + wrapper, + }); + expect(result.current.unreadVariant).toBe('primary'); + expect(result.current.unreadTitle).toBe('1 unread threaded message, 3 unread messages'); + + expect(result.current.unreadCount).toHaveProperty('mentions', 0); + expect(result.current.unreadCount).toHaveProperty('threads', 1); + expect(result.current.unreadCount).toHaveProperty('groupMentions', 0); + expect(result.current.unreadCount).toHaveProperty('total', 4); +}); + +it('should return correct unread data for [Channel with unread messages alert only]', async () => { + const { result } = renderHook(() => useUnreadDisplay(alert), { + legacyRoot: true, + wrapper, + }); + + expect(result.current.highlightUnread).toBe(true); + expect(result.current.showUnread).toBe(false); + + expect(result.current.unreadCount).toHaveProperty('mentions', 0); + expect(result.current.unreadCount).toHaveProperty('threads', 0); + expect(result.current.unreadCount).toHaveProperty('groupMentions', 0); + expect(result.current.unreadCount).toHaveProperty('total', 0); +}); + +it('should return correct unread data for [Mention and group mention]', async () => { + const { result } = renderHook(() => useUnreadDisplay(mentionAndGroupMention), { + legacyRoot: true, + wrapper, + }); + expect(result.current.unreadVariant).toBe('danger'); + expect(result.current.unreadTitle).toBe('1 mention, 1 group mention'); + + expect(result.current.unreadCount).toHaveProperty('mentions', 1); + expect(result.current.unreadCount).toHaveProperty('threads', 0); + expect(result.current.unreadCount).toHaveProperty('groupMentions', 1); + expect(result.current.unreadCount).toHaveProperty('total', 2); +}); + +it('should return correct unread data for [Group mention]', async () => { + const { result } = renderHook(() => useUnreadDisplay(groupMention), { + legacyRoot: true, + wrapper, + }); + expect(result.current.unreadVariant).toBe('warning'); + expect(result.current.unreadTitle).toBe('2 group mentions'); + + expect(result.current.unreadCount).toHaveProperty('mentions', 0); + expect(result.current.unreadCount).toHaveProperty('threads', 0); + expect(result.current.unreadCount).toHaveProperty('groupMentions', 2); + expect(result.current.unreadCount).toHaveProperty('total', 2); +}); + +it('should return correct unread data for [Thread unread]', async () => { + const { result } = renderHook(() => useUnreadDisplay(tunread), { + legacyRoot: true, + wrapper, + }); + expect(result.current.unreadVariant).toBe('primary'); + expect(result.current.unreadTitle).toBe('1 unread threaded message'); + + expect(result.current.unreadCount).toHaveProperty('mentions', 0); + expect(result.current.unreadCount).toHaveProperty('groupMentions', 0); + expect(result.current.unreadCount).toHaveProperty('threads', 1); + expect(result.current.unreadCount).toHaveProperty('total', 1); +}); + +it('should return correct unread data for [Thread and thread user mention]', async () => { + const { result } = renderHook(() => useUnreadDisplay(tunreadUser), { + legacyRoot: true, + wrapper, + }); + expect(result.current.unreadVariant).toBe('danger'); + expect(result.current.unreadTitle).toBe('1 mention, 1 unread threaded message'); + + expect(result.current.unreadCount).toHaveProperty('mentions', 1); + expect(result.current.unreadCount).toHaveProperty('groupMentions', 0); + expect(result.current.unreadCount).toHaveProperty('threads', 1); + expect(result.current.unreadCount).toHaveProperty('total', 2); +}); + +it('should not highlight unread if hideUnreadStatus is enabled', async () => { + const { result } = renderHook(() => useUnreadDisplay(hideUnreadStatus), { + legacyRoot: true, + }); + + expect(result.current.highlightUnread).toBe(false); + expect(result.current.showUnread).toBe(true); +}); + +it('should not show unread if hideUnreadStatus and hideMentionStatus is enabled', async () => { + const { result } = renderHook(() => useUnreadDisplay(hideUnreadAndMention), { + legacyRoot: true, + }); + + expect(result.current.highlightUnread).toBe(false); + expect(result.current.showUnread).toBe(false); +}); + +it("should not show unread if there isn't any unread message", async () => { + const { result } = renderHook(() => useUnreadDisplay(noUnread), { + legacyRoot: true, + }); + + expect(result.current.highlightUnread).toBe(false); + expect(result.current.showUnread).toBe(false); + + expect(result.current.unreadCount).toHaveProperty('mentions', 0); + expect(result.current.unreadCount).toHaveProperty('groupMentions', 0); + expect(result.current.unreadCount).toHaveProperty('threads', 0); + expect(result.current.unreadCount).toHaveProperty('total', 0); +}); diff --git a/apps/meteor/client/sidebarv2/hooks/useUnreadDisplay.ts b/apps/meteor/client/sidebarv2/hooks/useUnreadDisplay.ts new file mode 100644 index 000000000000..859711ce46d5 --- /dev/null +++ b/apps/meteor/client/sidebarv2/hooks/useUnreadDisplay.ts @@ -0,0 +1,15 @@ +import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; + +import { getSubscriptionUnreadData } from '../../../lib/getSubscriptionUnreadData'; + +export const useUnreadDisplay = ( + unreadData: Pick< + SubscriptionWithRoom, + 'alert' | 'userMentions' | 'unread' | 'tunread' | 'tunreadUser' | 'groupMentions' | 'hideMentionStatus' | 'hideUnreadStatus' + >, +) => { + const { t } = useTranslation(); + + return getSubscriptionUnreadData(unreadData, t); +}; diff --git a/apps/meteor/client/sidebarv2/search/Row.tsx b/apps/meteor/client/sidebarv2/search/Row.tsx index f8541546ec4b..c0d332c5fa7e 100644 --- a/apps/meteor/client/sidebarv2/search/Row.tsx +++ b/apps/meteor/client/sidebarv2/search/Row.tsx @@ -2,8 +2,8 @@ import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; import type { ReactElement } from 'react'; import React, { memo } from 'react'; -import SidebarItemTemplateWithData from '../RoomList/SidebarItemTemplateWithData'; import UserItem from './UserItem'; +import SidebarItemTemplateWithData from '../RoomList/SidebarItemTemplateWithData'; type RowProps = { item: ISubscription & IRoom; diff --git a/apps/meteor/client/sidebarv2/sections/BannerSection.tsx b/apps/meteor/client/sidebarv2/sections/BannerSection.tsx index f6ce2ac835c0..a69c1b951b65 100644 --- a/apps/meteor/client/sidebarv2/sections/BannerSection.tsx +++ b/apps/meteor/client/sidebarv2/sections/BannerSection.tsx @@ -2,16 +2,16 @@ import { useSessionStorage } from '@rocket.chat/fuselage-hooks'; import { useRole, useSetting } from '@rocket.chat/ui-contexts'; import React from 'react'; -import { useAirGappedRestriction } from '../../hooks/useAirGappedRestriction'; import AirGappedRestrictionBanner from './AirGappedRestrictionBanner/AirGappedRestrictionBanner'; import StatusDisabledBanner from './StatusDisabledBanner'; +import { useAirGappedRestriction } from '../../hooks/useAirGappedRestriction'; const BannerSection = () => { const [isRestricted, isWarning, remainingDays] = useAirGappedRestriction(); const isAdmin = useRole('admin'); const [bannerDismissed, setBannerDismissed] = useSessionStorage('presence_cap_notifier', false); - const presenceDisabled = useSetting('Presence_broadcast_disabled'); + const presenceDisabled = useSetting('Presence_broadcast_disabled', false); if ((isWarning || isRestricted) && isAdmin) { return ; diff --git a/apps/meteor/client/startup/actionButtons/jumpToMessage.ts b/apps/meteor/client/startup/actionButtons/jumpToMessage.ts index 0b50078e4d9a..9e8d7d5e07b9 100644 --- a/apps/meteor/client/startup/actionButtons/jumpToMessage.ts +++ b/apps/meteor/client/startup/actionButtons/jumpToMessage.ts @@ -1,7 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { MessageAction } from '../../../app/ui-utils/client'; -import { messageArgs } from '../../lib/utils/messageArgs'; import { setMessageJumpQueryStringParameter } from '../../lib/utils/setMessageJumpQueryStringParameter'; Meteor.startup(() => { @@ -10,8 +9,7 @@ Meteor.startup(() => { icon: 'jump', label: 'Jump_to_message', context: ['mentions', 'threads', 'videoconf-threads'], - action(_, props) { - const { message = messageArgs(this).msg } = props; + action(_, { message }) { setMessageJumpQueryStringParameter(message._id); }, order: 100, diff --git a/apps/meteor/client/startup/actionButtons/jumpToPinMessage.ts b/apps/meteor/client/startup/actionButtons/jumpToPinMessage.ts index f618c5655d85..70460d94bedf 100644 --- a/apps/meteor/client/startup/actionButtons/jumpToPinMessage.ts +++ b/apps/meteor/client/startup/actionButtons/jumpToPinMessage.ts @@ -1,7 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { MessageAction } from '../../../app/ui-utils/client'; -import { messageArgs } from '../../lib/utils/messageArgs'; import { setMessageJumpQueryStringParameter } from '../../lib/utils/setMessageJumpQueryStringParameter'; Meteor.startup(() => { @@ -10,8 +9,7 @@ Meteor.startup(() => { icon: 'jump', label: 'Jump_to_message', context: ['pinned', 'message-mobile', 'direct'], - action(_, props) { - const { message = messageArgs(this).msg } = props; + action(_, { message }) { setMessageJumpQueryStringParameter(message._id); }, condition({ subscription }) { diff --git a/apps/meteor/client/startup/actionButtons/jumpToSearchMessage.ts b/apps/meteor/client/startup/actionButtons/jumpToSearchMessage.ts index b2715cf18c9f..2dcbe7fabe6c 100644 --- a/apps/meteor/client/startup/actionButtons/jumpToSearchMessage.ts +++ b/apps/meteor/client/startup/actionButtons/jumpToSearchMessage.ts @@ -1,7 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { MessageAction } from '../../../app/ui-utils/client'; -import { messageArgs } from '../../lib/utils/messageArgs'; import { setMessageJumpQueryStringParameter } from '../../lib/utils/setMessageJumpQueryStringParameter'; Meteor.startup(() => { @@ -10,8 +9,7 @@ Meteor.startup(() => { icon: 'jump', label: 'Jump_to_message', context: ['search'], - async action(_, props) { - const { message = messageArgs(this).msg } = props; + async action(_, { message }) { setMessageJumpQueryStringParameter(message._id); }, order: 100, diff --git a/apps/meteor/client/startup/actionButtons/jumpToStarMessage.ts b/apps/meteor/client/startup/actionButtons/jumpToStarMessage.ts index 1f4b9c871e02..73ac085d9edb 100644 --- a/apps/meteor/client/startup/actionButtons/jumpToStarMessage.ts +++ b/apps/meteor/client/startup/actionButtons/jumpToStarMessage.ts @@ -2,7 +2,6 @@ import { Meteor } from 'meteor/meteor'; import { settings } from '../../../app/settings/client'; import { MessageAction } from '../../../app/ui-utils/client'; -import { messageArgs } from '../../lib/utils/messageArgs'; import { setMessageJumpQueryStringParameter } from '../../lib/utils/setMessageJumpQueryStringParameter'; Meteor.startup(() => { @@ -11,8 +10,7 @@ Meteor.startup(() => { icon: 'jump', label: 'Jump_to_message', context: ['starred', 'threads', 'message-mobile', 'videoconf-threads'], - action(_, props) { - const { message = messageArgs(this).msg } = props; + action(_, { message }) { setMessageJumpQueryStringParameter(message._id); }, condition({ message, subscription, user }) { diff --git a/apps/meteor/client/startup/actionButtons/permalinkPinned.ts b/apps/meteor/client/startup/actionButtons/permalinkPinned.ts index ddd9237c3eb8..add09ca7dcd0 100644 --- a/apps/meteor/client/startup/actionButtons/permalinkPinned.ts +++ b/apps/meteor/client/startup/actionButtons/permalinkPinned.ts @@ -5,7 +5,6 @@ import { MessageAction } from '../../../app/ui-utils/client'; import { t } from '../../../app/utils/lib/i18n'; import { getPermaLink } from '../../lib/getPermaLink'; import { dispatchToastMessage } from '../../lib/toast'; -import { messageArgs } from '../../lib/utils/messageArgs'; Meteor.startup(() => { MessageAction.addButton({ @@ -13,9 +12,8 @@ Meteor.startup(() => { icon: 'permalink', label: 'Copy_link', context: ['pinned'], - async action(_, props) { + async action(_, { message }) { try { - const { message = messageArgs(this).msg } = props; const permalink = await getPermaLink(message._id); navigator.clipboard.writeText(permalink); dispatchToastMessage({ type: 'success', message: t('Copied') }); diff --git a/apps/meteor/client/startup/actionButtons/permalinkStar.ts b/apps/meteor/client/startup/actionButtons/permalinkStar.ts index a1ee8d79fc44..e4a235491cb7 100644 --- a/apps/meteor/client/startup/actionButtons/permalinkStar.ts +++ b/apps/meteor/client/startup/actionButtons/permalinkStar.ts @@ -5,7 +5,6 @@ import { MessageAction } from '../../../app/ui-utils/client'; import { t } from '../../../app/utils/lib/i18n'; import { getPermaLink } from '../../lib/getPermaLink'; import { dispatchToastMessage } from '../../lib/toast'; -import { messageArgs } from '../../lib/utils/messageArgs'; Meteor.startup(() => { MessageAction.addButton({ @@ -14,9 +13,8 @@ Meteor.startup(() => { label: 'Copy_link', // classes: 'clipboard', context: ['starred', 'threads', 'videoconf-threads'], - async action(_, props) { + async action(_, { message }) { try { - const { message = messageArgs(this).msg } = props; const permalink = await getPermaLink(message._id); navigator.clipboard.writeText(permalink); dispatchToastMessage({ type: 'success', message: t('Copied') }); diff --git a/apps/meteor/client/startup/actionButtons/pinMessage.tsx b/apps/meteor/client/startup/actionButtons/pinMessage.tsx index b383b4a3c648..8f54550d02d9 100644 --- a/apps/meteor/client/startup/actionButtons/pinMessage.tsx +++ b/apps/meteor/client/startup/actionButtons/pinMessage.tsx @@ -8,7 +8,6 @@ import { imperativeModal } from '../../lib/imperativeModal'; import { queryClient } from '../../lib/queryClient'; import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; import { dispatchToastMessage } from '../../lib/toast'; -import { messageArgs } from '../../lib/utils/messageArgs'; import PinMessageModal from '../../views/room/modals/PinMessageModal'; Meteor.startup(() => { @@ -18,8 +17,7 @@ Meteor.startup(() => { label: 'Pin', type: 'interaction', context: ['pinned', 'message', 'message-mobile', 'threads', 'direct', 'videoconf', 'videoconf-threads'], - async action(_, props) { - const { message = messageArgs(this).msg } = props; + async action(_, { message }) { const onConfirm = async () => { message.pinned = true; try { diff --git a/apps/meteor/client/startup/actionButtons/starMessage.ts b/apps/meteor/client/startup/actionButtons/starMessage.ts index b4696041a729..9e8fc7245191 100644 --- a/apps/meteor/client/startup/actionButtons/starMessage.ts +++ b/apps/meteor/client/startup/actionButtons/starMessage.ts @@ -6,7 +6,6 @@ import { sdk } from '../../../app/utils/client/lib/SDKClient'; import { queryClient } from '../../lib/queryClient'; import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; import { dispatchToastMessage } from '../../lib/toast'; -import { messageArgs } from '../../lib/utils/messageArgs'; Meteor.startup(() => { MessageAction.addButton({ @@ -15,9 +14,7 @@ Meteor.startup(() => { label: 'Star', type: 'interaction', context: ['starred', 'message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], - async action(_, props) { - const { message = messageArgs(this).msg } = props; - + async action(_, { message }) { try { await sdk.call('starMessage', { ...message, starred: true }); queryClient.invalidateQueries(['rooms', message.rid, 'starred-messages']); diff --git a/apps/meteor/client/startup/actionButtons/unpinMessage.ts b/apps/meteor/client/startup/actionButtons/unpinMessage.ts index f5803f480a79..aa5fa49bb48e 100644 --- a/apps/meteor/client/startup/actionButtons/unpinMessage.ts +++ b/apps/meteor/client/startup/actionButtons/unpinMessage.ts @@ -6,7 +6,6 @@ import { MessageAction } from '../../../app/ui-utils/client'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; import { queryClient } from '../../lib/queryClient'; import { dispatchToastMessage } from '../../lib/toast'; -import { messageArgs } from '../../lib/utils/messageArgs'; Meteor.startup(() => { MessageAction.addButton({ @@ -15,8 +14,7 @@ Meteor.startup(() => { label: 'Unpin', type: 'interaction', context: ['pinned', 'message', 'message-mobile', 'threads', 'direct', 'videoconf', 'videoconf-threads'], - async action(_, props) { - const { message = messageArgs(this).msg } = props; + async action(_, { message }) { message.pinned = false; try { await sdk.call('unpinMessage', message); diff --git a/apps/meteor/client/startup/actionButtons/unstarMessage.ts b/apps/meteor/client/startup/actionButtons/unstarMessage.ts index e5835b61fd8f..af33492a689a 100644 --- a/apps/meteor/client/startup/actionButtons/unstarMessage.ts +++ b/apps/meteor/client/startup/actionButtons/unstarMessage.ts @@ -5,7 +5,6 @@ import { MessageAction } from '../../../app/ui-utils/client'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; import { queryClient } from '../../lib/queryClient'; import { dispatchToastMessage } from '../../lib/toast'; -import { messageArgs } from '../../lib/utils/messageArgs'; Meteor.startup(() => { MessageAction.addButton({ @@ -14,9 +13,7 @@ Meteor.startup(() => { label: 'Unstar_Message', type: 'interaction', context: ['starred', 'message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], - async action(_, props) { - const { message = messageArgs(this).msg } = props; - + async action(_, { message }) { try { await sdk.call('starMessage', { ...message, starred: false }); queryClient.invalidateQueries(['rooms', message.rid, 'starred-messages']); diff --git a/apps/meteor/client/startup/collections.ts b/apps/meteor/client/startup/collections.ts deleted file mode 100644 index 304047fd9c1d..000000000000 --- a/apps/meteor/client/startup/collections.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { CachedChatRoom, CachedChatSubscription } from '../../app/models/client'; -import { settings } from '../../app/settings/client'; - -Meteor.startup(() => { - Tracker.autorun(() => { - if (!Meteor.userId() && settings.get('Accounts_AllowAnonymousRead') === true) { - CachedChatRoom.init(); - CachedChatSubscription.ready.set(true); - } - }); -}); diff --git a/apps/meteor/client/startup/e2e.ts b/apps/meteor/client/startup/e2e.ts index e45b62563726..3f439d99a696 100644 --- a/apps/meteor/client/startup/e2e.ts +++ b/apps/meteor/client/startup/e2e.ts @@ -6,7 +6,7 @@ import { Tracker } from 'meteor/tracker'; import { E2EEState } from '../../app/e2e/client/E2EEState'; import { e2e } from '../../app/e2e/client/rocketchat.e2e'; import { MentionsParser } from '../../app/mentions/lib/MentionsParser'; -import { ChatRoom } from '../../app/models/client'; +import { Rooms } from '../../app/models/client'; import { settings } from '../../app/settings/client'; import { onClientBeforeSendMessage } from '../lib/onClientBeforeSendMessage'; import { onClientMessageReceived } from '../lib/onClientMessageReceived'; @@ -79,7 +79,7 @@ Meteor.startup(() => { return message; } - const subscription = await waitUntilFind(() => ChatRoom.findOne({ _id: message.rid })); + const subscription = await waitUntilFind(() => Rooms.findOne({ _id: message.rid })); subscription.encrypted ? e2eRoom.resume() : e2eRoom.pause(); diff --git a/apps/meteor/client/startup/iframeCommands.ts b/apps/meteor/client/startup/iframeCommands.ts index f0db83ccdcbf..5184de9c6942 100644 --- a/apps/meteor/client/startup/iframeCommands.ts +++ b/apps/meteor/client/startup/iframeCommands.ts @@ -19,10 +19,13 @@ const commands = { } const newUrl = new URL(`${rtrim(baseURI, '/')}/${ltrim(data.path, '/')}`); - const newParams = Array.from(newUrl.searchParams.entries()).reduce((ret, [key, value]) => { - ret[key] = value; - return ret; - }, {} as Record); + const newParams = Array.from(newUrl.searchParams.entries()).reduce( + (ret, [key, value]) => { + ret[key] = value; + return ret; + }, + {} as Record, + ); const newPath = newUrl.pathname.replace( new RegExp(`^${escapeRegExp(__meteor_runtime_config__.ROOT_URL_PATH_PREFIX)}`), diff --git a/apps/meteor/client/startup/incomingMessages.ts b/apps/meteor/client/startup/incomingMessages.ts index e9659cc24724..8f4149e9de94 100644 --- a/apps/meteor/client/startup/incomingMessages.ts +++ b/apps/meteor/client/startup/incomingMessages.ts @@ -1,29 +1,25 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; -import { ChatMessage } from '../../app/models/client'; -import { CachedCollectionManager } from '../../app/ui-cached-collection/client'; +import { Messages } from '../../app/models/client'; import { sdk } from '../../app/utils/client/lib/SDKClient'; +import { onLoggedIn } from '../lib/loggedIn'; Meteor.startup(() => { - Tracker.autorun(() => { - if (!Meteor.userId()) { - return; - } - + onLoggedIn(() => { // Only event I found triggers this is from ephemeral messages // Other types of messages come from another stream - sdk.stream('notify-user', [`${Meteor.userId()}/message`], (msg: IMessage) => { + return sdk.stream('notify-user', [`${Meteor.userId()}/message`], (msg: IMessage) => { msg.u = msg.u || { username: 'rocket.cat' }; msg.private = true; - return ChatMessage.upsert({ _id: msg._id }, msg); + return Messages.upsert({ _id: msg._id }, msg); }); }); - CachedCollectionManager.onLogin(() => { - sdk.stream('notify-user', [`${Meteor.userId()}/subscriptions-changed`], (_action, sub) => { - ChatMessage.update( + onLoggedIn(() => { + return sdk.stream('notify-user', [`${Meteor.userId()}/subscriptions-changed`], (_action, sub) => { + Messages.update( { rid: sub.rid, ...('ignored' in sub && sub.ignored ? { 'u._id': { $nin: sub.ignored } } : { ignored: { $exists: true } }), @@ -32,7 +28,7 @@ Meteor.startup(() => { { multi: true }, ); if ('ignored' in sub && sub.ignored) { - ChatMessage.update( + Messages.update( { 'rid': sub.rid, 't': { $ne: 'command' }, 'u._id': { $in: sub.ignored } }, { $set: { ignored: true } }, { multi: true }, diff --git a/apps/meteor/client/startup/index.ts b/apps/meteor/client/startup/index.ts index e2264d795415..569b11bf1c18 100644 --- a/apps/meteor/client/startup/index.ts +++ b/apps/meteor/client/startup/index.ts @@ -5,7 +5,6 @@ import './afterLogoutCleanUp'; import './appRoot'; import './audit'; import './callbacks'; -import './collections'; import './customOAuth'; import './deviceManagement'; import './e2e'; diff --git a/apps/meteor/client/startup/loadMissedMessages.ts b/apps/meteor/client/startup/loadMissedMessages.ts index e56dc5e83add..37bd437d8af2 100644 --- a/apps/meteor/client/startup/loadMissedMessages.ts +++ b/apps/meteor/client/startup/loadMissedMessages.ts @@ -2,12 +2,12 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { ChatMessage, ChatSubscription } from '../../app/models/client'; +import { Messages, Subscriptions } from '../../app/models/client'; import { LegacyRoomManager, upsertMessage } from '../../app/ui-utils/client'; import { callWithErrorHandling } from '../lib/utils/callWithErrorHandling'; const loadMissedMessages = async function (rid: IRoom['_id']): Promise { - const lastMessage = ChatMessage.findOne({ rid, _hidden: { $ne: true }, temp: { $exists: false } }, { sort: { ts: -1 }, limit: 1 }); + const lastMessage = Messages.findOne({ rid, _hidden: { $ne: true }, temp: { $exists: false } }, { sort: { ts: -1 }, limit: 1 }); if (!lastMessage) { return; @@ -16,7 +16,7 @@ const loadMissedMessages = async function (rid: IRoom['_id']): Promise { try { const result = await callWithErrorHandling('loadMissedMessages', rid, lastMessage.ts); if (result) { - const subscription = ChatSubscription.findOne({ rid }); + const subscription = Subscriptions.findOne({ rid }); await Promise.all(Array.from(result).map((msg) => upsertMessage({ msg, subscription }))); } } catch (error) { diff --git a/apps/meteor/client/startup/messageObserve.ts b/apps/meteor/client/startup/messageObserve.ts index 60e730c94276..760c8b76e5c6 100644 --- a/apps/meteor/client/startup/messageObserve.ts +++ b/apps/meteor/client/startup/messageObserve.ts @@ -1,23 +1,23 @@ import { Meteor } from 'meteor/meteor'; -import { ChatMessage } from '../../app/models/client'; +import { Messages } from '../../app/models/client'; import { LegacyRoomManager } from '../../app/ui-utils/client'; Meteor.startup(() => { - ChatMessage.find().observe({ + Messages.find().observe({ removed(record) { if (!LegacyRoomManager.getOpenedRoomByRid(record.rid)) { return; } - const recordBefore = ChatMessage.findOne({ ts: { $lt: record.ts } }, { sort: { ts: -1 } }); + const recordBefore = Messages.findOne({ ts: { $lt: record.ts } }, { sort: { ts: -1 } }); if (recordBefore) { - ChatMessage.update({ _id: recordBefore._id }, { $set: { tick: new Date() } }); + Messages.update({ _id: recordBefore._id }, { $set: { tick: new Date() } }); } - const recordAfter = ChatMessage.findOne({ ts: { $gt: record.ts } }, { sort: { ts: 1 } }); + const recordAfter = Messages.findOne({ ts: { $gt: record.ts } }, { sort: { ts: 1 } }); if (recordAfter) { - return ChatMessage.update({ _id: recordAfter._id }, { $set: { tick: new Date() } }); + return Messages.update({ _id: recordAfter._id }, { $set: { tick: new Date() } }); } }, }); diff --git a/apps/meteor/client/startup/notifications/konchatNotifications.ts b/apps/meteor/client/startup/notifications/konchatNotifications.ts index 3d5f26cae547..494e6ab08d5b 100644 --- a/apps/meteor/client/startup/notifications/konchatNotifications.ts +++ b/apps/meteor/client/startup/notifications/konchatNotifications.ts @@ -1,49 +1,15 @@ -import type { AtLeast, ISubscription, IUser, ICalendarNotification } from '@rocket.chat/core-typings'; +import type { IUser, ICalendarNotification } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import { lazy } from 'react'; -import { CachedChatSubscription } from '../../../app/models/client'; import { settings } from '../../../app/settings/client'; -import { KonchatNotification } from '../../../app/ui/client/lib/KonchatNotification'; import { getUserPreference } from '../../../app/utils/client'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; -import { RoomManager } from '../../lib/RoomManager'; import { imperativeModal } from '../../lib/imperativeModal'; -import { fireGlobalEvent } from '../../lib/utils/fireGlobalEvent'; -import { isLayoutEmbedded } from '../../lib/utils/isLayoutEmbedded'; -import { router } from '../../providers/RouterProvider'; const OutlookCalendarEventModal = lazy(() => import('../../views/outlookCalendar/OutlookCalendarEventModal')); -const notifyNewRoom = async (sub: AtLeast): Promise => { - const user = Meteor.user() as IUser | null; - if (!user || user.status === 'busy') { - return; - } - - if ((!router.getRouteParameters().name || router.getRouteParameters().name !== sub.name) && !sub.ls && sub.alert === true) { - KonchatNotification.newRoom(sub.rid); - } -}; - -function notifyNewMessageAudio(rid?: string): void { - // This logic is duplicated in /client/startup/unread.coffee. - const hasFocus = document.hasFocus(); - const messageIsInOpenedRoom = RoomManager.opened === rid; - const muteFocusedConversations = getUserPreference(Meteor.userId(), 'muteFocusedConversations'); - - if (isLayoutEmbedded()) { - if (!hasFocus && messageIsInOpenedRoom) { - // Play a notification sound - void KonchatNotification.newMessage(rid); - } - } else if (!hasFocus || !messageIsInOpenedRoom || !muteFocusedConversations) { - // Play a notification sound - void KonchatNotification.newMessage(rid); - } -} - Meteor.startup(() => { const notifyUserCalendar = async function (notification: ICalendarNotification): Promise { const user = Meteor.user() as IUser | null; @@ -69,6 +35,7 @@ Meteor.startup(() => { }); }; }; + Tracker.autorun(() => { if (!Meteor.userId() || !settings.get('Outlook_Calendar_Enabled')) { sdk.stop('notify-user', `${Meteor.userId()}/calendar`); @@ -77,47 +44,4 @@ Meteor.startup(() => { sdk.stream('notify-user', [`${Meteor.userId()}/calendar`], notifyUserCalendar); }); - - Tracker.autorun(() => { - if (!Meteor.userId()) { - return; - } - - sdk.stream('notify-user', [`${Meteor.userId()}/notification`], (notification) => { - const openedRoomId = ['channel', 'group', 'direct'].includes(router.getRouteName()!) ? RoomManager.opened : undefined; - - // This logic is duplicated in /client/startup/unread.coffee. - const hasFocus = document.hasFocus(); - const messageIsInOpenedRoom = openedRoomId === notification.payload.rid; - - fireGlobalEvent('notification', { - notification, - fromOpenedRoom: messageIsInOpenedRoom, - hasFocus, - }); - - if (isLayoutEmbedded()) { - if (!hasFocus && messageIsInOpenedRoom) { - // Show a notification. - KonchatNotification.showDesktop(notification); - } - } else if (!hasFocus || !messageIsInOpenedRoom) { - // Show a notification. - KonchatNotification.showDesktop(notification); - } - - notifyNewMessageAudio(notification.payload.rid); - }); - - CachedChatSubscription.on('changed', (sub): void => { - void notifyNewRoom(sub); - }); - - sdk.stream('notify-user', [`${Meteor.userId()}/subscriptions-changed`], (action, sub) => { - if (action === 'removed') { - return; - } - void notifyNewRoom(sub); - }); - }); }); diff --git a/apps/meteor/client/startup/notifications/notification.ts b/apps/meteor/client/startup/notifications/notification.ts index d35297bd7772..31ba20da0060 100644 --- a/apps/meteor/client/startup/notifications/notification.ts +++ b/apps/meteor/client/startup/notifications/notification.ts @@ -5,6 +5,7 @@ import { Tracker } from 'meteor/tracker'; import { CustomSounds } from '../../../app/custom-sounds/client/lib/CustomSounds'; import { Users } from '../../../app/models/client'; import { getUserPreference } from '../../../app/utils/client'; +import { getUserNotificationsSoundVolume } from '../../../app/utils/client/getUserNotificationsSoundVolume'; Meteor.startup(() => { Tracker.autorun(() => { @@ -21,7 +22,7 @@ Meteor.startup(() => { }, }); const newRoomNotification = getUserPreference(user, 'newRoomNotification'); - const audioVolume = getUserPreference(user, 'notificationsSoundVolume', 100); + const audioVolume = getUserNotificationsSoundVolume(user?._id); if (!newRoomNotification) { return; diff --git a/apps/meteor/client/startup/readReceipt.ts b/apps/meteor/client/startup/readReceipt.ts index 36eb50b6bcbd..5998ea3e6c52 100644 --- a/apps/meteor/client/startup/readReceipt.ts +++ b/apps/meteor/client/startup/readReceipt.ts @@ -4,7 +4,6 @@ import { Tracker } from 'meteor/tracker'; import { settings } from '../../app/settings/client'; import { MessageAction } from '../../app/ui-utils/client'; import { imperativeModal } from '../lib/imperativeModal'; -import { messageArgs } from '../lib/utils/messageArgs'; import ReadReceiptsModal from '../views/room/modals/ReadReceiptsModal'; Meteor.startup(() => { @@ -21,8 +20,7 @@ Meteor.startup(() => { label: 'Read_Receipts', context: ['starred', 'message', 'message-mobile', 'threads', 'videoconf', 'videoconf-threads'], type: 'duplication', - action(_, props) { - const { message = messageArgs(this).msg } = props; + action(_, { message }) { imperativeModal.open({ component: ReadReceiptsModal, props: { messageId: message._id, onClose: imperativeModal.close }, diff --git a/apps/meteor/client/startup/roles.ts b/apps/meteor/client/startup/roles.ts index 4c54682ecded..ebccaa8d81b3 100644 --- a/apps/meteor/client/startup/roles.ts +++ b/apps/meteor/client/startup/roles.ts @@ -3,11 +3,11 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import { Roles } from '../../app/models/client'; -import { CachedCollectionManager } from '../../app/ui-cached-collection/client'; import { sdk } from '../../app/utils/client/lib/SDKClient'; +import { onLoggedIn } from '../lib/loggedIn'; Meteor.startup(() => { - CachedCollectionManager.onLogin(async () => { + onLoggedIn(async () => { const { roles } = await sdk.rest.get('/v1/roles.list'); // if a role is checked before this collection is populated, it will return undefined Roles._collection._docs._map = new Map(roles.map((record) => [Roles._collection._docs._idStringify(record._id), record])); diff --git a/apps/meteor/client/startup/routes.tsx b/apps/meteor/client/startup/routes.tsx index 5b204a7cf18e..e25a6795da53 100644 --- a/apps/meteor/client/startup/routes.tsx +++ b/apps/meteor/client/startup/routes.tsx @@ -8,7 +8,7 @@ const IndexRoute = lazy(() => import('../views/root/IndexRoute')); const MeetRoute = lazy(() => import('../views/meet/MeetRoute')); const HomePage = lazy(() => import('../views/home/HomePage')); const DirectoryPage = lazy(() => import('../views/directory')); -const OmnichannelDirectoryPage = lazy(() => import('../views/omnichannel/directory/OmnichannelDirectoryPage')); +const OmnichannelDirectoryRouter = lazy(() => import('../views/omnichannel/directory/OmnichannelDirectoryRouter')); const OmnichannelQueueList = lazy(() => import('../views/omnichannel/queueList')); const CMSPage = lazy(() => import('@rocket.chat/web-ui-registration').then(({ CMSPage }) => ({ default: CMSPage }))); const SecretURLPage = lazy(() => import('../views/invite/SecretURLPage')); @@ -48,10 +48,8 @@ declare module '@rocket.chat/ui-contexts' { pattern: '/directory/:tab?'; }; 'omnichannel-directory': { - pathname: `/omnichannel-directory${`/${string}` | ''}${`/${string}` | ''}${`/${string}` | ''}${`/${string}` | ''}${ - | `/${string}` - | ''}`; - pattern: '/omnichannel-directory/:page?/:bar?/:id?/:tab?/:context?'; + pathname: `/omnichannel-directory${`/${string}` | ''}${`/${string}` | ''}${`/${string}` | ''}`; + pattern: '/omnichannel-directory/:tab?/:context?/:id?/'; }; 'livechat-queue': { pathname: '/livechat-queue'; @@ -153,11 +151,11 @@ router.defineRoutes([ ), }, { - path: '/omnichannel-directory/:page?/:bar?/:id?/:tab?/:context?', + path: '/omnichannel-directory/:tab?/:context?/:id?/', id: 'omnichannel-directory', element: appLayout.wrap( - + , ), }, diff --git a/apps/meteor/client/startup/unread.ts b/apps/meteor/client/startup/unread.ts index d9c2a35efab5..edc48fbf894b 100644 --- a/apps/meteor/client/startup/unread.ts +++ b/apps/meteor/client/startup/unread.ts @@ -4,12 +4,12 @@ import { Meteor } from 'meteor/meteor'; import { Session } from 'meteor/session'; import { Tracker } from 'meteor/tracker'; -import { ChatSubscription, ChatRoom } from '../../app/models/client'; +import { Subscriptions, Rooms } from '../../app/models/client'; import { getUserPreference } from '../../app/utils/client'; import { fireGlobalEvent } from '../lib/utils/fireGlobalEvent'; const fetchSubscriptions = (): ISubscription[] => - ChatSubscription.find( + Subscriptions.find( { open: true, hideUnreadStatus: { $ne: true }, @@ -39,7 +39,7 @@ Meteor.startup(() => { const unreadCount = fetchSubscriptions().reduce( (ret, subscription) => Tracker.nonreactive(() => { - const room = ChatRoom.findOne({ _id: subscription.rid }, { fields: { usersCount: 1 } }); + const room = Rooms.findOne({ _id: subscription.rid }, { fields: { usersCount: 1 } }); fireGlobalEvent('unread-changed-by-subscription', { ...subscription, usersCount: room?.usersCount, diff --git a/apps/meteor/client/startup/userRoles.ts b/apps/meteor/client/startup/userRoles.ts index 77ba6978d485..1359dfa4d423 100644 --- a/apps/meteor/client/startup/userRoles.ts +++ b/apps/meteor/client/startup/userRoles.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { UserRoles, ChatMessage } from '../../app/models/client'; +import { UserRoles, Messages } from '../../app/models/client'; import { sdk } from '../../app/utils/client/lib/SDKClient'; import { dispatchToastMessage } from '../lib/toast'; @@ -26,7 +26,7 @@ Meteor.startup(() => { return; } UserRoles.upsert({ _id: role.u._id }, { $addToSet: { roles: role._id }, $set: { username: role.u.username } }); - ChatMessage.update({ 'u._id': role.u._id }, { $addToSet: { roles: role._id } }, { multi: true }); + Messages.update({ 'u._id': role.u._id }, { $addToSet: { roles: role._id } }, { multi: true }); } return; @@ -38,14 +38,14 @@ Meteor.startup(() => { return; } UserRoles.update({ _id: role.u._id }, { $pull: { roles: role._id } }); - ChatMessage.update({ 'u._id': role.u._id }, { $pull: { roles: role._id } }, { multi: true }); + Messages.update({ 'u._id': role.u._id }, { $pull: { roles: role._id } }, { multi: true }); } return; } if (role.type === 'changed') { - ChatMessage.update({ roles: role._id }, { $inc: { rerender: 1 } }, { multi: true }); + Messages.update({ roles: role._id }, { $inc: { rerender: 1 } }, { multi: true }); } }); } diff --git a/apps/meteor/client/stories/contexts/ModalContextMock.tsx b/apps/meteor/client/stories/contexts/ModalContextMock.tsx index ac1d303b2317..5dfa76613e8d 100644 --- a/apps/meteor/client/stories/contexts/ModalContextMock.tsx +++ b/apps/meteor/client/stories/contexts/ModalContextMock.tsx @@ -23,7 +23,7 @@ const ModalContextMock = ({ children }: ModalContextMockProps): ReactElement => }, }, currentModal: context.currentModal, - } + } : undefined, [context], ); diff --git a/apps/meteor/client/stories/contexts/ServerContextMock.tsx b/apps/meteor/client/stories/contexts/ServerContextMock.tsx index 89ded6b19037..8ae8c0047378 100644 --- a/apps/meteor/client/stories/contexts/ServerContextMock.tsx +++ b/apps/meteor/client/stories/contexts/ServerContextMock.tsx @@ -45,7 +45,7 @@ type Operations = { fn: ( params: void extends OperationParams ? void : OperationParams, ) => Promise ? Serialized> : void>; - } + } : never : never : never diff --git a/apps/meteor/client/ui.ts b/apps/meteor/client/ui.ts index 922b605a2b40..f5e55d64833b 100644 --- a/apps/meteor/client/ui.ts +++ b/apps/meteor/client/ui.ts @@ -9,7 +9,6 @@ import { useCallsRoomAction } from './hooks/roomActions/useCallsRoomAction'; import { useCannedResponsesRoomAction } from './hooks/roomActions/useCannedResponsesRoomAction'; import { useChannelSettingsRoomAction } from './hooks/roomActions/useChannelSettingsRoomAction'; import { useCleanHistoryRoomAction } from './hooks/roomActions/useCleanHistoryRoomAction'; -import { useContactChatHistoryRoomAction } from './hooks/roomActions/useContactChatHistoryRoomAction'; import { useContactProfileRoomAction } from './hooks/roomActions/useContactProfileRoomAction'; import { useDiscussionsRoomAction } from './hooks/roomActions/useDiscussionsRoomAction'; import { useE2EERoomAction } from './hooks/roomActions/useE2EERoomAction'; @@ -48,7 +47,6 @@ export const roomActionHooks = [ useCallsRoomAction, useCannedResponsesRoomAction, useCleanHistoryRoomAction, - useContactChatHistoryRoomAction, useContactProfileRoomAction, useDiscussionsRoomAction, useE2EERoomAction, diff --git a/apps/meteor/client/uikit/hooks/useContextualBarContextValue.ts b/apps/meteor/client/uikit/hooks/useContextualBarContextValue.ts index 1be1affad71f..10906deab703 100644 --- a/apps/meteor/client/uikit/hooks/useContextualBarContextValue.ts +++ b/apps/meteor/client/uikit/hooks/useContextualBarContextValue.ts @@ -1,8 +1,8 @@ import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks'; import type { UiKitContext } from '@rocket.chat/fuselage-ui-kit'; import type * as UiKit from '@rocket.chat/ui-kit'; -import type { Dispatch } from 'react'; -import { useMemo, type ContextType } from 'react'; +import { useMemo } from 'react'; +import type { Dispatch, ContextType } from 'react'; import { useUiKitActionManager } from './useUiKitActionManager'; diff --git a/apps/meteor/client/uikit/hooks/useMessageBlockContextValue.ts b/apps/meteor/client/uikit/hooks/useMessageBlockContextValue.ts index 2f8fb4933628..caf869ba39d7 100644 --- a/apps/meteor/client/uikit/hooks/useMessageBlockContextValue.ts +++ b/apps/meteor/client/uikit/hooks/useMessageBlockContextValue.ts @@ -3,6 +3,7 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import type { UiKitContext } from '@rocket.chat/fuselage-ui-kit'; import type { ContextType } from 'react'; +import { useUiKitActionManager } from './useUiKitActionManager'; import { useVideoConfDispatchOutgoing, useVideoConfIsCalling, @@ -12,7 +13,6 @@ import { useVideoConfSetPreferences, } from '../../contexts/VideoConfContext'; import { useVideoConfWarning } from '../../views/room/contextualBar/VideoConference/hooks/useVideoConfWarning'; -import { useUiKitActionManager } from './useUiKitActionManager'; export const useMessageBlockContextValue = (rid: IRoom['_id'], mid: IMessage['_id']): ContextType => { const joinCall = useVideoConfJoinCall(); diff --git a/apps/meteor/client/uikit/hooks/useModalContextValue.ts b/apps/meteor/client/uikit/hooks/useModalContextValue.ts index 4a0932c8e1e7..8e159805c738 100644 --- a/apps/meteor/client/uikit/hooks/useModalContextValue.ts +++ b/apps/meteor/client/uikit/hooks/useModalContextValue.ts @@ -1,8 +1,8 @@ import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks'; import type { UiKitContext } from '@rocket.chat/fuselage-ui-kit'; import type * as UiKit from '@rocket.chat/ui-kit'; -import type { Dispatch } from 'react'; -import { useMemo, type ContextType } from 'react'; +import { useMemo } from 'react'; +import type { Dispatch, ContextType } from 'react'; import { useUiKitActionManager } from './useUiKitActionManager'; diff --git a/apps/meteor/client/views/account/AccountRouter.tsx b/apps/meteor/client/views/account/AccountRouter.tsx index 8196a877f98c..a59babe1a08d 100644 --- a/apps/meteor/client/views/account/AccountRouter.tsx +++ b/apps/meteor/client/views/account/AccountRouter.tsx @@ -2,9 +2,9 @@ import { useRouter } from '@rocket.chat/ui-contexts'; import type { ReactElement, ReactNode } from 'react'; import React, { Suspense, useEffect } from 'react'; +import AccountSidebar from './AccountSidebar'; import PageSkeleton from '../../components/PageSkeleton'; import SidebarPortal from '../../sidebar/SidebarPortal'; -import AccountSidebar from './AccountSidebar'; type AccountRouterProps = { children?: ReactNode; diff --git a/apps/meteor/client/views/account/AccountSidebar.tsx b/apps/meteor/client/views/account/AccountSidebar.tsx index 27fa374a69d3..b1d87945ea5e 100644 --- a/apps/meteor/client/views/account/AccountSidebar.tsx +++ b/apps/meteor/client/views/account/AccountSidebar.tsx @@ -2,9 +2,9 @@ import { useCurrentRoutePath, useTranslation, useLayout } from '@rocket.chat/ui- import React, { memo } from 'react'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; +import { getAccountSidebarItems, subscribeToAccountSidebarItems } from './sidebarItems'; import Sidebar from '../../components/Sidebar'; import SettingsProvider from '../../providers/SettingsProvider'; -import { getAccountSidebarItems, subscribeToAccountSidebarItems } from './sidebarItems'; const AccountSidebar = () => { const t = useTranslation(); diff --git a/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx b/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx index 9bb4e57317cf..f4f67552ddc2 100644 --- a/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx +++ b/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx @@ -22,13 +22,13 @@ import { useMutation } from '@tanstack/react-query'; import React, { useMemo } from 'react'; import { Controller, useForm } from 'react-hook-form'; -import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; -import { getDirtyFields } from '../../../lib/getDirtyFields'; import { fontSizes } from './fontSizes'; import type { AccessibilityPreferencesData } from './hooks/useAcessibilityPreferencesValues'; import { useAccessiblityPreferencesValues } from './hooks/useAcessibilityPreferencesValues'; import { useCreateFontStyleElement } from './hooks/useCreateFontStyleElement'; import { themeItems as themes } from './themeItems'; +import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; +import { getDirtyFields } from '../../../lib/getDirtyFields'; const AccessibilityPage = () => { const t = useTranslation(); diff --git a/apps/meteor/client/views/account/deviceManagement/DeviceManagementAccountPage.tsx b/apps/meteor/client/views/account/deviceManagement/DeviceManagementAccountPage.tsx index d1af15f59711..f98e6608a5b7 100644 --- a/apps/meteor/client/views/account/deviceManagement/DeviceManagementAccountPage.tsx +++ b/apps/meteor/client/views/account/deviceManagement/DeviceManagementAccountPage.tsx @@ -2,8 +2,8 @@ import type { ReactElement } from 'react'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Page, PageHeader, PageContent } from '../../../components/Page'; import DeviceManagementAccountTable from './DeviceManagementAccountTable'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; const DeviceManagementAccountPage = (): ReactElement => { const { t } = useTranslation(); diff --git a/apps/meteor/client/views/account/deviceManagement/DeviceManagementAccountTable/DeviceManagementAccountTable.tsx b/apps/meteor/client/views/account/deviceManagement/DeviceManagementAccountTable/DeviceManagementAccountTable.tsx index 411f9ad02e6f..351021a9ab92 100644 --- a/apps/meteor/client/views/account/deviceManagement/DeviceManagementAccountTable/DeviceManagementAccountTable.tsx +++ b/apps/meteor/client/views/account/deviceManagement/DeviceManagementAccountTable/DeviceManagementAccountTable.tsx @@ -3,12 +3,12 @@ import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import DeviceManagementAccountRow from './DeviceManagementAccountRow'; import { GenericTableHeaderCell } from '../../../../components/GenericTable'; import { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; import { useSort } from '../../../../components/GenericTable/hooks/useSort'; import DeviceManagementTable from '../../../../components/deviceManagement/DeviceManagementTable'; import { useEndpointData } from '../../../../hooks/useEndpointData'; -import DeviceManagementAccountRow from './DeviceManagementAccountRow'; const sortMapping = { client: 'device.name', diff --git a/apps/meteor/client/views/account/integrations/AccountIntegrationsPage.tsx b/apps/meteor/client/views/account/integrations/AccountIntegrationsPage.tsx index ce9fad8591fa..9a13328cc6e9 100644 --- a/apps/meteor/client/views/account/integrations/AccountIntegrationsPage.tsx +++ b/apps/meteor/client/views/account/integrations/AccountIntegrationsPage.tsx @@ -1,14 +1,15 @@ import type { SelectOption } from '@rocket.chat/fuselage'; import { SelectLegacy, Box, Button, Field, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage'; import { useEffectEvent, useUniqueId } from '@rocket.chat/fuselage-hooks'; -import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import React, { useMemo } from 'react'; import { useForm, Controller } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { useRemoveWebDAVAccountIntegrationMutation } from './hooks/useRemoveWebDAVAccountIntegrationMutation'; import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import { useWebDAVAccountIntegrationsQuery } from '../../../hooks/webdav/useWebDAVAccountIntegrationsQuery'; import { getWebdavServerName } from '../../../lib/getWebdavServerName'; -import { useRemoveWebDAVAccountIntegrationMutation } from './hooks/useRemoveWebDAVAccountIntegrationMutation'; const AccountIntegrationsPage = () => { const { data: webdavAccountIntegrations } = useWebDAVAccountIntegrationsQuery(); @@ -25,7 +26,7 @@ const AccountIntegrationsPage = () => { ); const dispatchToastMessage = useToastMessageDispatch(); - const t = useTranslation(); + const { t } = useTranslation(); const removeMutation = useRemoveWebDAVAccountIntegrationMutation({ onSuccess: () => { diff --git a/apps/meteor/client/views/account/integrations/AccountIntegrationsRoute.tsx b/apps/meteor/client/views/account/integrations/AccountIntegrationsRoute.tsx index 00121c688345..fa0161b06125 100644 --- a/apps/meteor/client/views/account/integrations/AccountIntegrationsRoute.tsx +++ b/apps/meteor/client/views/account/integrations/AccountIntegrationsRoute.tsx @@ -2,11 +2,11 @@ import { useSetting } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import AccountIntegrationsPage from './AccountIntegrationsPage'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; const AccountIntegrationsRoute = (): ReactElement => { - const webdavEnabled = useSetting('Webdav_Integration_Enabled', false); + const webdavEnabled = useSetting('Webdav_Integration_Enabled', false); if (!webdavEnabled) { return ; diff --git a/apps/meteor/client/views/account/omnichannel/OmnichannelPreferencesPage.tsx b/apps/meteor/client/views/account/omnichannel/OmnichannelPreferencesPage.tsx index 8489b3cb4b8b..ddf549bd98c6 100644 --- a/apps/meteor/client/views/account/omnichannel/OmnichannelPreferencesPage.tsx +++ b/apps/meteor/client/views/account/omnichannel/OmnichannelPreferencesPage.tsx @@ -4,9 +4,9 @@ import type { ReactElement } from 'react'; import React from 'react'; import { useForm, FormProvider } from 'react-hook-form'; -import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; import PreferencesConversationTranscript from './PreferencesConversationTranscript'; import { PreferencesGeneral } from './PreferencesGeneral'; +import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; type FormData = { omnichannelTranscriptPDF: boolean; @@ -17,7 +17,7 @@ const OmnichannelPreferencesPage = (): ReactElement => { const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); - const alwaysSendEmailTranscript = useSetting('Livechat_transcript_send_always'); + const alwaysSendEmailTranscript = useSetting('Livechat_transcript_send_always', false); const omnichannelTranscriptPDF = useUserPreference('omnichannelTranscriptPDF') ?? false; const omnichannelTranscriptEmail = useUserPreference('omnichannelTranscriptEmail') ?? false; const omnichannelHideConversationAfterClosing = useUserPreference('omnichannelHideConversationAfterClosing') ?? true; diff --git a/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx b/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx index 77997422a21e..335b27d7c813 100644 --- a/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx +++ b/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx @@ -6,8 +6,6 @@ import type { ReactElement } from 'react'; import React from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; -import { getDirtyFields } from '../../../lib/getDirtyFields'; import PreferencesGlobalSection from './PreferencesGlobalSection'; import PreferencesHighlightsSection from './PreferencesHighlightsSection'; import PreferencesLocalizationSection from './PreferencesLocalizationSection'; @@ -18,6 +16,8 @@ import PreferencesSoundSection from './PreferencesSoundSection'; import PreferencesUserPresenceSection from './PreferencesUserPresenceSection'; import type { AccountPreferencesData } from './useAccountPreferencesValues'; import { useAccountPreferencesValues } from './useAccountPreferencesValues'; +import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; +import { getDirtyFields } from '../../../lib/getDirtyFields'; const AccountPreferencesPage = (): ReactElement => { const t = useTranslation(); diff --git a/apps/meteor/client/views/account/preferences/PreferencesGlobalSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesGlobalSection.tsx index d09492fdc5a6..0edc83dadf23 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesGlobalSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesGlobalSection.tsx @@ -1,12 +1,13 @@ import type { SelectOption } from '@rocket.chat/fuselage'; import { Accordion, Field, FieldGroup, FieldLabel, FieldRow, MultiSelect } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; -import { useUserPreference, useTranslation } from '@rocket.chat/ui-contexts'; +import { useUserPreference } from '@rocket.chat/ui-contexts'; import React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; const PreferencesGlobalSection = () => { - const t = useTranslation(); + const { t } = useTranslation(); const userDontAskAgainList = useUserPreference<{ action: string; label: string }[]>('dontAskAgainList') || []; const options: SelectOption[] = userDontAskAgainList.map(({ action, label }) => [action, label]); diff --git a/apps/meteor/client/views/account/preferences/PreferencesLocalizationSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesLocalizationSection.tsx index 0dc6e25f1ac3..08723d827444 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesLocalizationSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesLocalizationSection.tsx @@ -1,12 +1,13 @@ import type { SelectOption } from '@rocket.chat/fuselage'; import { Accordion, Field, FieldGroup, FieldLabel, FieldRow, Select } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; -import { useLanguages, useTranslation } from '@rocket.chat/ui-contexts'; +import { useLanguages } from '@rocket.chat/ui-contexts'; import React, { useMemo } from 'react'; import { useFormContext, Controller } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; const PreferencesLocalizationSection = () => { - const t = useTranslation(); + const { t } = useTranslation(); const languages = useLanguages(); const { control } = useFormContext(); diff --git a/apps/meteor/client/views/account/preferences/PreferencesMyDataSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesMyDataSection.tsx index 5fd4540b5e57..b8db60f623ce 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesMyDataSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesMyDataSection.tsx @@ -1,11 +1,12 @@ import { Accordion, ButtonGroup, Button, Box } from '@rocket.chat/fuselage'; -import { useSetModal, useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetModal, useToastMessageDispatch, useMethod } from '@rocket.chat/ui-contexts'; import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import MyDataModal from './MyDataModal'; const PreferencesMyDataSection = () => { - const t = useTranslation(); + const { t } = useTranslation(); const setModal = useSetModal(); const dispatchToastMessage = useToastMessageDispatch(); @@ -34,7 +35,7 @@ const PreferencesMyDataSection = () => { const text = result.url ? t('UserDataDownload_CompletedRequestExistedWithLink_Text', { download_link: result.url, - }) + }) : t('UserDataDownload_CompletedRequestExisted_Text'); setModal( diff --git a/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx index ddac7fda145b..b90d577524ba 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx @@ -3,9 +3,10 @@ import type { SelectOption } from '@rocket.chat/fuselage'; import { Accordion, Field, FieldLabel, FieldRow, FieldHint, Select, FieldGroup, ToggleSwitch, Button } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useUserPreference, useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import { useUserPreference, useSetting } from '@rocket.chat/ui-contexts'; import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; import { KonchatNotification } from '../../../../app/ui/client/lib/KonchatNotification'; @@ -22,7 +23,7 @@ const emailNotificationOptionsLabelMap = { // TODO: Test Notification Button not working const PreferencesNotificationsSection = () => { - const t = useTranslation(); + const { t, i18n } = useTranslation(); const [notificationsPermission, setNotificationsPermission] = useState(); @@ -57,8 +58,8 @@ const PreferencesNotificationsSection = () => { }, []); const notificationOptions = useMemo( - () => Object.entries(notificationOptionsLabelMap).map(([key, val]) => t.has(val) && [key, t(val)]), - [t], + () => Object.entries(notificationOptionsLabelMap).map(([key, val]) => i18n.exists(val) && [key, t(val)]), + [i18n, t], ) as SelectOption[]; const desktopNotificationOptions = useMemo((): SelectOption[] => { @@ -74,10 +75,12 @@ const PreferencesNotificationsSection = () => { }, [defaultMobileNotifications, notificationOptions, t]); const emailNotificationOptions = useMemo(() => { - const options = Object.entries(emailNotificationOptionsLabelMap).map(([key, val]) => t.has(val) && [key, t(val)]) as SelectOption[]; + const options = Object.entries(emailNotificationOptionsLabelMap).map( + ([key, val]) => i18n.exists(val) && [key, t(val)], + ) as SelectOption[]; options.unshift(['default', `${t('Default')} (${t(emailNotificationOptionsLabelMap[userEmailNotificationMode] as TranslationKey)})`]); return options; - }, [t, userEmailNotificationMode]); + }, [i18n, t, userEmailNotificationMode]); const { control } = useFormContext(); diff --git a/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx index 8306e2941b00..d88ecaa99dbe 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx @@ -1,8 +1,7 @@ import type { SelectOption } from '@rocket.chat/fuselage'; -import { Accordion, Field, FieldLabel, FieldRow, Select, FieldGroup, ToggleSwitch, Tooltip, Box } from '@rocket.chat/fuselage'; +import { Accordion, Field, FieldLabel, FieldRow, Select, FieldGroup, ToggleSwitch, FieldHint, Slider } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useTranslation, useCustomSound } from '@rocket.chat/ui-contexts'; -import type { ChangeEvent } from 'react'; import React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; @@ -12,16 +11,92 @@ const PreferencesSoundSection = () => { const customSound = useCustomSound(); const soundsList: SelectOption[] = customSound?.getList()?.map((value) => [value._id, value.name]) || []; const { control, watch } = useFormContext(); - const { newMessageNotification, notificationsSoundVolume } = watch(); + const { newMessageNotification, notificationsSoundVolume = 100, masterVolume = 100, voipRingerVolume = 100 } = watch(); const newRoomNotificationId = useUniqueId(); const newMessageNotificationId = useUniqueId(); const muteFocusedConversationsId = useUniqueId(); + const masterVolumeId = useUniqueId(); const notificationsSoundVolumeId = useUniqueId(); + const voipRingerVolumeId = useUniqueId(); return ( + + {t('Master_volume')} + + {t('Master_volume_hint')} + + + ( + + )} + /> + + + + {t('Notification_volume')} + + {t('Notification_volume_hint')} + + + ( + { + const soundVolume = (notificationsSoundVolume * masterVolume) / 100; + customSound.play(newMessageNotification, { volume: soundVolume / 100 }); + onChange(value); + }} + /> + )} + /> + + + + {t('Call_ringer_volume')} + + {t('Call_ringer_volume_hint')} + + + ( + { + const soundVolume = (voipRingerVolume * masterVolume) / 100; + customSound.play('telephone', { volume: soundVolume / 100 }); + onChange(value); + }} + /> + )} + /> + + {t('New_Room_Notification')} @@ -32,11 +107,11 @@ const PreferencesSoundSection = () => { { onChange(value); customSound.play(String(value), { volume: notificationsSoundVolume / 100 }); }} - options={soundsList} /> )} /> @@ -74,34 +149,6 @@ const PreferencesSoundSection = () => { /> - - {t('Notifications_Sound_Volume')} - - ( - ) => { - customSound.play(newMessageNotification, { volume: notificationsSoundVolume / 100 }); - onChange(Math.max(0, Math.min(Number(e.currentTarget.value), 100))); - }} - /> - )} - /> - - {notificationsSoundVolume} - - - ); diff --git a/apps/meteor/client/views/account/preferences/useAccountPreferencesValues.ts b/apps/meteor/client/views/account/preferences/useAccountPreferencesValues.ts index a85ef275638e..e6520b07f34d 100644 --- a/apps/meteor/client/views/account/preferences/useAccountPreferencesValues.ts +++ b/apps/meteor/client/views/account/preferences/useAccountPreferencesValues.ts @@ -35,7 +35,9 @@ export type AccountPreferencesData = { sidebarViewMode?: string; sidebarDisplayAvatar?: boolean; sidebarGroupByType?: boolean; + masterVolume?: number; notificationsSoundVolume?: number; + voipRingerVolume?: number; }; export const useAccountPreferencesValues = (): AccountPreferencesData => { @@ -70,7 +72,10 @@ export const useAccountPreferencesValues = (): AccountPreferencesData => { const newRoomNotification = useUserPreference('newRoomNotification'); const newMessageNotification = useUserPreference('newMessageNotification'); const muteFocusedConversations = useUserPreference('muteFocusedConversations'); - const notificationsSoundVolume = useUserPreference('notificationsSoundVolume'); + + const masterVolume = useUserPreference('masterVolume', 100); + const notificationsSoundVolume = useUserPreference('notificationsSoundVolume', 100); + const voipRingerVolume = useUserPreference('voipRingerVolume', 100); return { language, @@ -99,6 +104,8 @@ export const useAccountPreferencesValues = (): AccountPreferencesData => { newRoomNotification, newMessageNotification, muteFocusedConversations, + masterVolume, notificationsSoundVolume, + voipRingerVolume, }; }; diff --git a/apps/meteor/client/views/account/profile/AccountProfileForm.tsx b/apps/meteor/client/views/account/profile/AccountProfileForm.tsx index 95398b352049..54875f0686cb 100644 --- a/apps/meteor/client/views/account/profile/AccountProfileForm.tsx +++ b/apps/meteor/client/views/account/profile/AccountProfileForm.tsx @@ -27,14 +27,14 @@ import type { AllHTMLAttributes, ReactElement } from 'react'; import React, { useCallback } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; +import type { AccountProfileFormValues } from './getProfileInitialValues'; +import { useAccountProfileSettings } from './useAccountProfileSettings'; import { validateEmail } from '../../../../lib/emailValidator'; import { getUserEmailAddress } from '../../../../lib/getUserEmailAddress'; import UserStatusMenu from '../../../components/UserStatusMenu'; import UserAvatarEditor from '../../../components/avatar/UserAvatarEditor'; import { useUpdateAvatar } from '../../../hooks/useUpdateAvatar'; import { USER_STATUS_TEXT_MAX_LENGTH, BIO_TEXT_MAX_LENGTH } from '../../../lib/constants'; -import type { AccountProfileFormValues } from './getProfileInitialValues'; -import { useAccountProfileSettings } from './useAccountProfileSettings'; const AccountProfileForm = (props: AllHTMLAttributes): ReactElement => { const t = useTranslation(); diff --git a/apps/meteor/client/views/account/profile/AccountProfilePage.tsx b/apps/meteor/client/views/account/profile/AccountProfilePage.tsx index df1710b07509..0113d40a938a 100644 --- a/apps/meteor/client/views/account/profile/AccountProfilePage.tsx +++ b/apps/meteor/client/views/account/profile/AccountProfilePage.tsx @@ -15,12 +15,12 @@ import type { ReactElement } from 'react'; import React, { useState, useCallback } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import ConfirmOwnerChangeModal from '../../../components/ConfirmOwnerChangeModal'; -import { Page, PageFooter, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; -import { useAllowPasswordChange } from '../security/useAllowPasswordChange'; import AccountProfileForm from './AccountProfileForm'; import ActionConfirmModal from './ActionConfirmModal'; import { getProfileInitialValues } from './getProfileInitialValues'; +import ConfirmOwnerChangeModal from '../../../components/ConfirmOwnerChangeModal'; +import { Page, PageFooter, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; +import { useAllowPasswordChange } from '../security/useAllowPasswordChange'; // TODO: enforce useMutation const AccountProfilePage = (): ReactElement => { diff --git a/apps/meteor/client/views/account/profile/AccountProfileRoute.tsx b/apps/meteor/client/views/account/profile/AccountProfileRoute.tsx index 52d879c30537..77e184f1873c 100644 --- a/apps/meteor/client/views/account/profile/AccountProfileRoute.tsx +++ b/apps/meteor/client/views/account/profile/AccountProfileRoute.tsx @@ -2,8 +2,8 @@ import { useSetting } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import AccountProfilePage from './AccountProfilePage'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; const AccountProfileRoute = (): ReactElement => { const canViewProfile = useSetting('Accounts_AllowUserProfileChange'); diff --git a/apps/meteor/client/views/account/profile/useAccountProfileSettings.ts b/apps/meteor/client/views/account/profile/useAccountProfileSettings.ts index 272559d35460..18449ec2b827 100644 --- a/apps/meteor/client/views/account/profile/useAccountProfileSettings.ts +++ b/apps/meteor/client/views/account/profile/useAccountProfileSettings.ts @@ -2,14 +2,14 @@ import { useSetting } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; export const useAccountProfileSettings = () => { - const allowRealNameChange = useSetting('Accounts_AllowRealNameChange'); - const allowUserStatusMessageChange = useSetting('Accounts_AllowUserStatusMessageChange'); - const canChangeUsername = useSetting('Accounts_AllowUsernameChange'); - const allowEmailChange = useSetting('Accounts_AllowEmailChange'); + const allowRealNameChange = useSetting('Accounts_AllowRealNameChange', true); + const allowUserStatusMessageChange = useSetting('Accounts_AllowUserStatusMessageChange', true); + const canChangeUsername = useSetting('Accounts_AllowUsernameChange', true); + const allowEmailChange = useSetting('Accounts_AllowEmailChange', true); - const allowUserAvatarChange = useSetting('Accounts_AllowUserAvatarChange'); - const requireName = useSetting('Accounts_RequireNameForSignUp'); - const namesRegexSetting = useSetting('UTF8_User_Names_Validation'); + const allowUserAvatarChange = useSetting('Accounts_AllowUserAvatarChange', true); + const requireName = useSetting('Accounts_RequireNameForSignUp', true); + const namesRegexSetting = useSetting('UTF8_User_Names_Validation', '[0-9a-zA-Z-_.]+'); const namesRegex = useMemo(() => new RegExp(`^${namesRegexSetting}$`), [namesRegexSetting]); diff --git a/apps/meteor/client/views/account/security/AccountSecurityPage.tsx b/apps/meteor/client/views/account/security/AccountSecurityPage.tsx index c7d4157ddbd7..45b0e4c5eb3f 100644 --- a/apps/meteor/client/views/account/security/AccountSecurityPage.tsx +++ b/apps/meteor/client/views/account/security/AccountSecurityPage.tsx @@ -5,11 +5,11 @@ import type { ReactElement } from 'react'; import React from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; import ChangePassword from './ChangePassword'; import EndToEnd from './EndToEnd'; import TwoFactorEmail from './TwoFactorEmail'; import TwoFactorTOTP from './TwoFactorTOTP'; +import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; const passwordDefaultValues = { password: '', confirmationPassword: '' }; diff --git a/apps/meteor/client/views/account/security/AccountSecurityRoute.tsx b/apps/meteor/client/views/account/security/AccountSecurityRoute.tsx index 11a0d8b63882..ed7ed0310948 100644 --- a/apps/meteor/client/views/account/security/AccountSecurityRoute.tsx +++ b/apps/meteor/client/views/account/security/AccountSecurityRoute.tsx @@ -2,8 +2,8 @@ import { useSetting } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import AccountSecurityPage from './AccountSecurityPage'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; const AccountSecurityRoute = (): ReactElement => { const isTwoFactorEnabled = useSetting('Accounts_TwoFactorAuthentication_Enabled'); diff --git a/apps/meteor/client/views/account/security/ChangePassword.tsx b/apps/meteor/client/views/account/security/ChangePassword.tsx index e5cb61e34547..efffd7d197a6 100644 --- a/apps/meteor/client/views/account/security/ChangePassword.tsx +++ b/apps/meteor/client/views/account/security/ChangePassword.tsx @@ -1,17 +1,18 @@ import { Box, Field, FieldError, FieldGroup, FieldHint, FieldLabel, FieldRow, Icon, PasswordInput } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { PasswordVerifier, useValidatePassword } from '@rocket.chat/ui-client'; -import { useMethod, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useMethod, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import type { AllHTMLAttributes } from 'react'; import React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; import { useAllowPasswordChange } from './useAllowPasswordChange'; type PasswordFieldValues = { password: string; confirmationPassword: string }; const ChangePassword = (props: AllHTMLAttributes) => { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const passwordId = useUniqueId(); diff --git a/apps/meteor/client/views/account/security/TwoFactorEmail.tsx b/apps/meteor/client/views/account/security/TwoFactorEmail.tsx index 3654d17b5dec..092fa46dadee 100644 --- a/apps/meteor/client/views/account/security/TwoFactorEmail.tsx +++ b/apps/meteor/client/views/account/security/TwoFactorEmail.tsx @@ -1,12 +1,13 @@ import { Box, Button, Margins } from '@rocket.chat/fuselage'; -import { useUser, useTranslation } from '@rocket.chat/ui-contexts'; +import { useUser } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { useEndpointAction } from '../../../hooks/useEndpointAction'; const TwoFactorEmail = (props: ComponentProps) => { - const t = useTranslation(); + const { t } = useTranslation(); const user = useUser(); const isEnabled = user?.services?.email2fa?.enabled; diff --git a/apps/meteor/client/views/account/security/TwoFactorTOTP.tsx b/apps/meteor/client/views/account/security/TwoFactorTOTP.tsx index a00a64ee991b..bbd917a1055d 100644 --- a/apps/meteor/client/views/account/security/TwoFactorTOTP.tsx +++ b/apps/meteor/client/views/account/security/TwoFactorTOTP.tsx @@ -1,17 +1,18 @@ import { Box, Button, TextInput, Margins } from '@rocket.chat/fuselage'; import { useSafely } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, useUser, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetModal, useToastMessageDispatch, useUser, useMethod } from '@rocket.chat/ui-contexts'; import type { ReactElement, ComponentProps } from 'react'; import React, { useState, useCallback, useEffect } from 'react'; import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; import qrcode from 'yaqrcode'; +import BackupCodesModal from './BackupCodesModal'; import TextCopy from '../../../components/TextCopy'; import TwoFactorTotpModal from '../../../components/TwoFactorModal/TwoFactorTotpModal'; -import BackupCodesModal from './BackupCodesModal'; const TwoFactorTOTP = (props: ComponentProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const user = useUser(); const setModal = useSetModal(); diff --git a/apps/meteor/client/views/account/tokens/AccountTokensPage.tsx b/apps/meteor/client/views/account/tokens/AccountTokensPage.tsx index abf45ebedcb9..6850615db7a4 100644 --- a/apps/meteor/client/views/account/tokens/AccountTokensPage.tsx +++ b/apps/meteor/client/views/account/tokens/AccountTokensPage.tsx @@ -2,8 +2,8 @@ import type { ReactElement } from 'react'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Page, PageHeader, PageContent } from '../../../components/Page'; import AccountTokensTable from './AccountTokensTable'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; const AccountTokensPage = (): ReactElement => { const { t } = useTranslation(); diff --git a/apps/meteor/client/views/account/tokens/AccountTokensRoute.tsx b/apps/meteor/client/views/account/tokens/AccountTokensRoute.tsx index ed8964db15bb..0876bd8385a7 100644 --- a/apps/meteor/client/views/account/tokens/AccountTokensRoute.tsx +++ b/apps/meteor/client/views/account/tokens/AccountTokensRoute.tsx @@ -2,8 +2,8 @@ import { usePermission } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import AccountTokensPage from './AccountTokensPage'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; const AccountTokensRoute = (): ReactElement => { const canCreateTokens = usePermission('create-personal-access-tokens'); diff --git a/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx b/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx index adfb8916af2e..78cb8414f6e7 100644 --- a/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx +++ b/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx @@ -1,8 +1,11 @@ import { Box, Pagination, States, StatesAction, StatesActions, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage'; -import { useSetModal, useToastMessageDispatch, useUserId, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetModal, useToastMessageDispatch, useUserId, useMethod } from '@rocket.chat/ui-contexts'; import type { ReactElement, RefObject } from 'react'; import React, { useMemo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import AccountTokensRow from './AccountTokensRow'; +import AddToken from './AddToken'; import GenericModal from '../../../../components/GenericModal'; import GenericNoResults from '../../../../components/GenericNoResults'; import { @@ -16,11 +19,9 @@ import { usePagination } from '../../../../components/GenericTable/hooks/usePagi import { useEndpointData } from '../../../../hooks/useEndpointData'; import { useResizeInlineBreakpoint } from '../../../../hooks/useResizeInlineBreakpoint'; import { AsyncStatePhase } from '../../../../lib/asyncState'; -import AccountTokensRow from './AccountTokensRow'; -import AddToken from './AddToken'; const AccountTokensTable = (): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const setModal = useSetModal(); const userId = useUserId(); @@ -154,11 +155,11 @@ const AccountTokensTable = (): ReactElement => { filteredTokens && filteredTokens.map((filteredToken) => ( ))} diff --git a/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx b/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx index 71fd2933f431..b924f377e851 100644 --- a/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx +++ b/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx @@ -1,14 +1,15 @@ import type { SelectOption } from '@rocket.chat/fuselage'; import { Box, TextInput, Button, Margins, Select } from '@rocket.chat/fuselage'; -import { useSetModal, useToastMessageDispatch, useUserId, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetModal, useToastMessageDispatch, useUserId, useMethod } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback, useMemo, useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; import GenericModal from '../../../../components/GenericModal'; const AddToken = ({ reload }: { reload: () => void }): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const userId = useUserId(); const setModal = useSetModal(); const createTokenFn = useMethod('personalAccessTokens:generateToken'); diff --git a/apps/meteor/client/views/admin/AdministrationLayout.tsx b/apps/meteor/client/views/admin/AdministrationLayout.tsx index 257028e723d2..2e77f6f48d40 100644 --- a/apps/meteor/client/views/admin/AdministrationLayout.tsx +++ b/apps/meteor/client/views/admin/AdministrationLayout.tsx @@ -1,8 +1,8 @@ import type { ReactNode } from 'react'; import React from 'react'; -import SidebarPortal from '../../sidebar/SidebarPortal'; import AdminSidebar from './sidebar/AdminSidebar'; +import SidebarPortal from '../../sidebar/SidebarPortal'; type AdministrationLayoutProps = { children?: ReactNode; diff --git a/apps/meteor/client/views/admin/AdministrationRouter.tsx b/apps/meteor/client/views/admin/AdministrationRouter.tsx index b210435ae267..8ae97f463416 100644 --- a/apps/meteor/client/views/admin/AdministrationRouter.tsx +++ b/apps/meteor/client/views/admin/AdministrationRouter.tsx @@ -2,12 +2,12 @@ import { useRouter } from '@rocket.chat/ui-contexts'; import type { ReactElement, ReactNode } from 'react'; import React, { Suspense, useEffect } from 'react'; +import AdministrationLayout from './AdministrationLayout'; +import { getAdminSidebarItems } from './sidebarItems'; import PageSkeleton from '../../components/PageSkeleton'; import type { Item, SidebarDivider, SidebarItem } from '../../lib/createSidebarItems'; import { isGoRocketChatLink } from '../../lib/createSidebarItems'; import SettingsProvider from '../../providers/SettingsProvider'; -import AdministrationLayout from './AdministrationLayout'; -import { getAdminSidebarItems } from './sidebarItems'; const isSidebarDivider = (sidebarItem: SidebarItem): sidebarItem is SidebarDivider => { return (sidebarItem as SidebarDivider).divider === true; diff --git a/apps/meteor/client/views/admin/EditableSettingsContext.ts b/apps/meteor/client/views/admin/EditableSettingsContext.ts index 8e98adf83a88..ed149669b8c1 100644 --- a/apps/meteor/client/views/admin/EditableSettingsContext.ts +++ b/apps/meteor/client/views/admin/EditableSettingsContext.ts @@ -1,4 +1,4 @@ -import type { ISettingBase, SectionName, SettingId, GroupId, TabId, ISettingColor } from '@rocket.chat/core-typings'; +import type { ISettingBase, ISettingColor, ISetting } from '@rocket.chat/core-typings'; import type { SettingsContextQuery } from '@rocket.chat/ui-contexts'; import { createContext, useContext, useMemo } from 'react'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; @@ -15,32 +15,30 @@ type EditableSettingsContextQuery = SettingsContextQuery & { export type EditableSettingsContextValue = { readonly queryEditableSetting: ( - _id: SettingId, + _id: ISetting['_id'], ) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => EditableSetting | undefined]; readonly queryEditableSettings: ( query: EditableSettingsContextQuery, ) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => EditableSetting[]]; readonly queryGroupSections: ( - _id: GroupId, - tab?: TabId, - ) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => SectionName[]]; - readonly queryGroupTabs: (_id: GroupId) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => TabId[]]; + _id: ISetting['_id'], + tab?: ISetting['_id'], + ) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => string[]]; + readonly queryGroupTabs: ( + _id: ISetting['_id'], + ) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => ISetting['_id'][]]; readonly dispatch: (changes: Partial[]) => void; - readonly isEnterprise: boolean; }; export const EditableSettingsContext = createContext({ queryEditableSetting: () => [(): (() => void) => (): void => undefined, (): undefined => undefined], queryEditableSettings: () => [(): (() => void) => (): void => undefined, (): EditableSetting[] => []], - queryGroupSections: () => [(): (() => void) => (): void => undefined, (): SectionName[] => []], - queryGroupTabs: () => [(): (() => void) => (): void => undefined, (): TabId[] => []], + queryGroupSections: () => [(): (() => void) => (): void => undefined, (): string[] => []], + queryGroupTabs: () => [(): (() => void) => (): void => undefined, (): ISetting['_id'][] => []], dispatch: () => undefined, - isEnterprise: false, }); -export const useIsEnterprise = (): boolean => useContext(EditableSettingsContext).isEnterprise; - -export const useEditableSetting = (_id: SettingId): EditableSetting | undefined => { +export const useEditableSetting = (_id: ISetting['_id']): EditableSetting | undefined => { const { queryEditableSetting } = useContext(EditableSettingsContext); const [subscribe, getSnapshot] = useMemo(() => queryEditableSetting(_id), [queryEditableSetting, _id]); @@ -53,14 +51,14 @@ export const useEditableSettings = (query?: EditableSettingsContextQuery): Edita return useSyncExternalStore(subscribe, getSnapshot); }; -export const useEditableSettingsGroupSections = (_id: SettingId, tab?: TabId): SectionName[] => { +export const useEditableSettingsGroupSections = (_id: ISetting['_id'], tab?: ISetting['_id']): string[] => { const { queryGroupSections } = useContext(EditableSettingsContext); const [subscribe, getSnapshot] = useMemo(() => queryGroupSections(_id, tab), [queryGroupSections, _id, tab]); return useSyncExternalStore(subscribe, getSnapshot); }; -export const useEditableSettingsGroupTabs = (_id: SettingId): TabId[] => { +export const useEditableSettingsGroupTabs = (_id: ISetting['_id']): ISetting['_id'][] => { const { queryGroupTabs } = useContext(EditableSettingsContext); const [subscribe, getSnapshot] = useMemo(() => queryGroupTabs(_id), [queryGroupTabs, _id]); diff --git a/apps/meteor/client/views/admin/customEmoji/CustomEmoji.spec.tsx b/apps/meteor/client/views/admin/customEmoji/CustomEmoji.spec.tsx new file mode 100644 index 000000000000..d59a8c815150 --- /dev/null +++ b/apps/meteor/client/views/admin/customEmoji/CustomEmoji.spec.tsx @@ -0,0 +1,79 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { render, screen, waitFor } from '@testing-library/react'; +import React from 'react'; + +import '@testing-library/jest-dom'; + +import CustomEmoji from './CustomEmoji'; + +const appRoot = mockAppRoot().withEndpoint('GET', '/v1/emoji-custom.all', () => ({ + count: 1, + offset: 0, + total: 1, + success: true, + emojis: [ + { + _id: '1', + name: 'smile', + aliases: ['happy', 'joy'], + extension: 'webp', + _updatedAt: new Date().toISOString(), + etag: 'abcdef', + }, + ], +})); + +describe('CustomEmoji Component', () => { + const mockRef = { current: jest.fn() }; + const mockOnClick = jest.fn(); + + it('renders emoji list', async () => { + render(, { + legacyRoot: true, + wrapper: appRoot.build(), + }); + + await waitFor(() => { + expect(screen.getByText('smile')).toBeInTheDocument(); + }); + }); + + it("renders emoji's aliases as comma-separated values when aliases is an array", async () => { + render(, { + legacyRoot: true, + wrapper: appRoot.build(), + }); + + await waitFor(() => { + expect(screen.getByText('happy, joy')).toBeInTheDocument(); + }); + }); + + it("renders emoji's aliases values when aliases is a string", async () => { + render(, { + legacyRoot: true, + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/emoji-custom.all', () => ({ + count: 1, + offset: 0, + total: 1, + success: true, + emojis: [ + { + _id: '1', + name: 'smile', + aliases: 'happy' as any, + extension: 'webp', + _updatedAt: new Date().toISOString(), + etag: 'abcdef', + }, + ], + })) + .build(), + }); + + await waitFor(() => { + expect(screen.getByText('happy')).toBeInTheDocument(); + }); + }); +}); diff --git a/apps/meteor/client/views/admin/customEmoji/CustomEmoji.tsx b/apps/meteor/client/views/admin/customEmoji/CustomEmoji.tsx index 2bd795c3ccab..4c02776b80d5 100644 --- a/apps/meteor/client/views/admin/customEmoji/CustomEmoji.tsx +++ b/apps/meteor/client/views/admin/customEmoji/CustomEmoji.tsx @@ -66,7 +66,7 @@ const CustomEmoji = ({ onClick, reload }: CustomEmojiProps) => { return ( <> - + setText(event.target.value)} /> {isLoading && ( {headers} @@ -95,7 +95,7 @@ const CustomEmoji = ({ onClick, reload }: CustomEmojiProps) => { {emojis.name} - {emojis.aliases} + {Array.isArray(emojis.aliases) ? emojis.aliases.join(', ') : emojis.aliases} ))} diff --git a/apps/meteor/client/views/admin/customEmoji/CustomEmojiRoute.tsx b/apps/meteor/client/views/admin/customEmoji/CustomEmojiRoute.tsx index 7db77713b3ee..68daa83165db 100644 --- a/apps/meteor/client/views/admin/customEmoji/CustomEmojiRoute.tsx +++ b/apps/meteor/client/views/admin/customEmoji/CustomEmojiRoute.tsx @@ -1,8 +1,12 @@ import { Button } from '@rocket.chat/fuselage'; -import { useRoute, useRouteParameter, usePermission, useTranslation } from '@rocket.chat/ui-contexts'; +import { useRoute, useRouteParameter, usePermission } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import AddCustomEmoji from './AddCustomEmoji'; +import CustomEmoji from './CustomEmoji'; +import EditCustomEmojiWithData from './EditCustomEmojiWithData'; import { Contextualbar, ContextualbarHeader, @@ -12,12 +16,9 @@ import { } from '../../../components/Contextualbar'; import { Page, PageHeader, PageContent } from '../../../components/Page'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; -import AddCustomEmoji from './AddCustomEmoji'; -import CustomEmoji from './CustomEmoji'; -import EditCustomEmojiWithData from './EditCustomEmojiWithData'; const CustomEmojiRoute = (): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const route = useRoute('emoji-custom'); const context = useRouteParameter('context'); const id = useRouteParameter('id'); diff --git a/apps/meteor/client/views/admin/customEmoji/EditCustomEmoji.tsx b/apps/meteor/client/views/admin/customEmoji/EditCustomEmoji.tsx index 49ee12b69dd0..285c1a98513b 100644 --- a/apps/meteor/client/views/admin/customEmoji/EditCustomEmoji.tsx +++ b/apps/meteor/client/views/admin/customEmoji/EditCustomEmoji.tsx @@ -11,9 +11,10 @@ import { FieldError, IconButton, } from '@rocket.chat/fuselage'; -import { useSetModal, useToastMessageDispatch, useAbsoluteUrl, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetModal, useToastMessageDispatch, useAbsoluteUrl } from '@rocket.chat/ui-contexts'; import type { ChangeEvent } from 'react'; import React, { useCallback, useState, useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../components/Contextualbar'; import GenericModal from '../../../components/GenericModal'; @@ -29,11 +30,12 @@ type EditCustomEmojiProps = { name: string; aliases: string[]; extension: string; + etag?: string; }; }; const EditCustomEmoji = ({ close, onChange, data, ...props }: EditCustomEmojiProps) => { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const setModal = useSetModal(); const absoluteUrl = useAbsoluteUrl(); @@ -50,7 +52,7 @@ const EditCustomEmoji = ({ close, onChange, data, ...props }: EditCustomEmojiPro } if (data) { - return absoluteUrl(`/emoji-custom/${encodeURIComponent(data.name)}.${data.extension}`); + return absoluteUrl(`/emoji-custom/${encodeURIComponent(data.name)}.${data.extension}${data.etag ? `?etag=${data.etag}` : ''}`); } return null; diff --git a/apps/meteor/client/views/admin/customEmoji/EditCustomEmojiWithData.tsx b/apps/meteor/client/views/admin/customEmoji/EditCustomEmojiWithData.tsx index fff37214b530..161a00760902 100644 --- a/apps/meteor/client/views/admin/customEmoji/EditCustomEmojiWithData.tsx +++ b/apps/meteor/client/views/admin/customEmoji/EditCustomEmojiWithData.tsx @@ -1,10 +1,11 @@ import { Callout } from '@rocket.chat/fuselage'; -import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; -import { FormSkeleton } from '../../../components/Skeleton'; import EditCustomEmoji from './EditCustomEmoji'; +import { FormSkeleton } from '../../../components/Skeleton'; type EditCustomEmojiWithDataProps = { _id: string; @@ -13,7 +14,7 @@ type EditCustomEmojiWithDataProps = { }; const EditCustomEmojiWithData = ({ _id, onChange, close, ...props }: EditCustomEmojiWithDataProps) => { - const t = useTranslation(); + const { t } = useTranslation(); const query = useMemo(() => ({ _id }), [_id]); const getEmojis = useEndpoint('GET', '/v1/emoji-custom.list'); diff --git a/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx b/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx index 430d05ceaa53..e9994d03ce6e 100644 --- a/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx +++ b/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx @@ -1,11 +1,12 @@ import { Field, FieldLabel, FieldRow, TextInput, Box, Margins, Button, ButtonGroup, IconButton } from '@rocket.chat/fuselage'; -import { useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useMethod } from '@rocket.chat/ui-contexts'; import type { ReactElement, FormEvent } from 'react'; import React, { useState, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { validate, createSoundData } from './lib'; import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../components/Contextualbar'; import { useSingleFileInput } from '../../../hooks/useSingleFileInput'; -import { validate, createSoundData } from './lib'; type AddCustomSoundProps = { goToNew: (_id: string) => () => void; @@ -14,7 +15,7 @@ type AddCustomSoundProps = { }; const AddCustomSound = ({ goToNew, close, onChange, ...props }: AddCustomSoundProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const [name, setName] = useState(''); diff --git a/apps/meteor/client/views/admin/customSounds/CustomSoundsPage.tsx b/apps/meteor/client/views/admin/customSounds/CustomSoundsPage.tsx index 238ee3c2ad20..cb4d9bf3e0b2 100644 --- a/apps/meteor/client/views/admin/customSounds/CustomSoundsPage.tsx +++ b/apps/meteor/client/views/admin/customSounds/CustomSoundsPage.tsx @@ -1,7 +1,11 @@ import { Button } from '@rocket.chat/fuselage'; -import { useRoute, useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts'; +import { useRoute, useRouteParameter } from '@rocket.chat/ui-contexts'; import React, { useCallback, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import AddCustomSound from './AddCustomSound'; +import CustomSoundsTable from './CustomSoundsTable'; +import EditCustomSound from './EditCustomSound'; import { ContextualbarTitle, Contextualbar, @@ -10,12 +14,9 @@ import { ContextualbarDialog, } from '../../../components/Contextualbar'; import { Page, PageHeader, PageContent } from '../../../components/Page'; -import AddCustomSound from './AddCustomSound'; -import CustomSoundsTable from './CustomSoundsTable'; -import EditCustomSound from './EditCustomSound'; const CustomSoundsPage = () => { - const t = useTranslation(); + const { t } = useTranslation(); const id = useRouteParameter('id'); const route = useRoute('custom-sounds'); const context = useRouteParameter('context'); diff --git a/apps/meteor/client/views/admin/customSounds/CustomSoundsRoute.tsx b/apps/meteor/client/views/admin/customSounds/CustomSoundsRoute.tsx index 17859317917b..1b65cb9e996f 100644 --- a/apps/meteor/client/views/admin/customSounds/CustomSoundsRoute.tsx +++ b/apps/meteor/client/views/admin/customSounds/CustomSoundsRoute.tsx @@ -2,8 +2,8 @@ import { usePermission } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import CustomSoundsPage from './CustomSoundsPage'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; const CustomSoundsRoute = (): ReactElement => { const canManageCustomSounds = usePermission('manage-sounds'); diff --git a/apps/meteor/client/views/admin/customSounds/CustomSoundsTable/CustomSoundRow.tsx b/apps/meteor/client/views/admin/customSounds/CustomSoundsTable/CustomSoundRow.tsx index 789f07f0c1cd..88b312e996e1 100644 --- a/apps/meteor/client/views/admin/customSounds/CustomSoundsTable/CustomSoundRow.tsx +++ b/apps/meteor/client/views/admin/customSounds/CustomSoundsTable/CustomSoundRow.tsx @@ -1,7 +1,8 @@ import { Box, IconButton } from '@rocket.chat/fuselage'; -import { useCustomSound, useTranslation } from '@rocket.chat/ui-contexts'; +import { useCustomSound } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { GenericTableCell, GenericTableRow } from '../../../../components/GenericTable'; @@ -14,7 +15,7 @@ type CustomSoundRowProps = { }; const CustomSoundRow = ({ onClick, sound }: CustomSoundRowProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const [isPlay, setPlayAudio] = useState(false); const customSound = useCustomSound(); diff --git a/apps/meteor/client/views/admin/customSounds/CustomSoundsTable/CustomSoundsTable.tsx b/apps/meteor/client/views/admin/customSounds/CustomSoundsTable/CustomSoundsTable.tsx index f5930435e025..021bbad844b0 100644 --- a/apps/meteor/client/views/admin/customSounds/CustomSoundsTable/CustomSoundsTable.tsx +++ b/apps/meteor/client/views/admin/customSounds/CustomSoundsTable/CustomSoundsTable.tsx @@ -5,6 +5,7 @@ import { useQuery } from '@tanstack/react-query'; import type { MutableRefObject } from 'react'; import React, { useState, useMemo, useEffect } from 'react'; +import CustomSoundRow from './CustomSoundRow'; import FilterByText from '../../../../components/FilterByText'; import GenericNoResults from '../../../../components/GenericNoResults'; import { @@ -16,7 +17,6 @@ import { } from '../../../../components/GenericTable'; import { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; import { useSort } from '../../../../components/GenericTable/hooks/useSort'; -import CustomSoundRow from './CustomSoundRow'; type CustomSoundsTableProps = { onClick: (soundId: string) => () => void; @@ -28,7 +28,7 @@ const CustomSoundsTable = ({ reload, onClick }: CustomSoundsTableProps) => { const { sortBy, sortDirection, setSort } = useSort<'name'>('name'); const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination(); - const [text, setParams] = useState(''); + const [text, setText] = useState(''); const query = useDebouncedValue( useMemo( @@ -63,7 +63,7 @@ const CustomSoundsTable = ({ reload, onClick }: CustomSoundsTableProps) => { return ( <> - + setText(event.target.value)} /> {isLoading && ( {headers} diff --git a/apps/meteor/client/views/admin/customSounds/EditCustomSound.tsx b/apps/meteor/client/views/admin/customSounds/EditCustomSound.tsx index f21bf86fc267..c69dc9e9b0f1 100644 --- a/apps/meteor/client/views/admin/customSounds/EditCustomSound.tsx +++ b/apps/meteor/client/views/admin/customSounds/EditCustomSound.tsx @@ -1,10 +1,11 @@ -import { useEndpoint, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; -import { FormSkeleton } from '../../../components/Skeleton'; import EditSound from './EditSound'; +import { FormSkeleton } from '../../../components/Skeleton'; type EditCustomSoundProps = { _id: string | undefined; @@ -13,7 +14,7 @@ type EditCustomSoundProps = { }; function EditCustomSound({ _id, onChange, ...props }: EditCustomSoundProps): ReactElement | null { - const t = useTranslation(); + const { t } = useTranslation(); const getSounds = useEndpoint('GET', '/v1/custom-sounds.list'); const dispatchToastMessage = useToastMessageDispatch(); diff --git a/apps/meteor/client/views/admin/customSounds/EditSound.tsx b/apps/meteor/client/views/admin/customSounds/EditSound.tsx index 1274e9deda35..b87d8df9f194 100644 --- a/apps/meteor/client/views/admin/customSounds/EditSound.tsx +++ b/apps/meteor/client/views/admin/customSounds/EditSound.tsx @@ -1,12 +1,13 @@ import { Box, Button, ButtonGroup, Margins, TextInput, Field, FieldLabel, FieldRow, IconButton } from '@rocket.chat/fuselage'; -import { useSetModal, useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetModal, useToastMessageDispatch, useMethod } from '@rocket.chat/ui-contexts'; import type { ReactElement, SyntheticEvent } from 'react'; import React, { useCallback, useState, useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { validate, createSoundData } from './lib'; import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../components/Contextualbar'; import GenericModal from '../../../components/GenericModal'; import { useSingleFileInput } from '../../../hooks/useSingleFileInput'; -import { validate, createSoundData } from './lib'; type EditSoundProps = { close?: () => void; @@ -19,7 +20,7 @@ type EditSoundProps = { }; function EditSound({ close, onChange, data, ...props }: EditSoundProps): ReactElement { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const setModal = useSetModal(); diff --git a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusFormWithData.tsx b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusFormWithData.tsx index 101de668e6a1..d86d4d39e8dc 100644 --- a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusFormWithData.tsx +++ b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusFormWithData.tsx @@ -1,12 +1,13 @@ import type { IUserStatus } from '@rocket.chat/core-typings'; import { Box, Callout } from '@rocket.chat/fuselage'; -import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; -import { FormSkeleton } from '../../../components/Skeleton'; import CustomUserStatusForm from './CustomUserStatusForm'; +import { FormSkeleton } from '../../../components/Skeleton'; type CustomUserStatusFormWithDataProps = { _id?: IUserStatus['_id']; @@ -15,7 +16,7 @@ type CustomUserStatusFormWithDataProps = { }; const CustomUserStatusFormWithData = ({ _id, onReload, onClose }: CustomUserStatusFormWithDataProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const query = useMemo(() => ({ _id }), [_id]); const getCustomUserStatus = useEndpoint('GET', '/v1/custom-user-status.list'); diff --git a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusRoute.tsx b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusRoute.tsx index f3c9d7be4b25..875f3c9e0202 100644 --- a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusRoute.tsx +++ b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusRoute.tsx @@ -3,6 +3,10 @@ import { useRoute, useRouteParameter, usePermission, useTranslation, useSetting import type { ReactElement } from 'react'; import React, { useCallback, useRef, useEffect } from 'react'; +import CustomUserActiveConnections from './CustomUserActiveConnections'; +import CustomUserStatusFormWithData from './CustomUserStatusFormWithData'; +import CustomUserStatusService from './CustomUserStatusService'; +import CustomUserStatusTable from './CustomUserStatusTable'; import { Contextualbar, ContextualbarHeader, @@ -13,10 +17,6 @@ import { import { Page, PageHeader, PageContent } from '../../../components/Page'; import { useIsEnterprise } from '../../../hooks/useIsEnterprise'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; -import CustomUserActiveConnections from './CustomUserActiveConnections'; -import CustomUserStatusFormWithData from './CustomUserStatusFormWithData'; -import CustomUserStatusService from './CustomUserStatusService'; -import CustomUserStatusTable from './CustomUserStatusTable'; const CustomUserStatusRoute = (): ReactElement => { const t = useTranslation(); @@ -25,7 +25,7 @@ const CustomUserStatusRoute = (): ReactElement => { const id = useRouteParameter('id'); const canManageUserStatus = usePermission('manage-user-status'); const { data: license } = useIsEnterprise(); - const presenceDisabled = useSetting('Presence_broadcast_disabled'); + const presenceDisabled = useSetting('Presence_broadcast_disabled', false); useEffect(() => { presenceDisabled && route.push({ context: 'presence-service' }); diff --git a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusService.tsx b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusService.tsx index 626529e3010a..90bbd7b9dc78 100644 --- a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusService.tsx +++ b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusService.tsx @@ -23,7 +23,7 @@ import { useActiveConnections } from '../../hooks/useActiveConnections'; const CustomUserStatusService = () => { const { t } = useTranslation(); const result = useActiveConnections(); - const presenceDisabled = useSetting('Presence_broadcast_disabled'); + const presenceDisabled = useSetting('Presence_broadcast_disabled', false); const togglePresenceServiceEndpoint = useEndpoint('POST', '/v1/presence.enableBroadcast'); const disablePresenceService = useMutation(() => togglePresenceServiceEndpoint()); const { data: license, isLoading: licenseIsLoading } = useIsEnterprise(); diff --git a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/CustomUserStatusTable.tsx b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/CustomUserStatusTable.tsx index 0aa24542ca96..72551525cf3a 100644 --- a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/CustomUserStatusTable.tsx +++ b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/CustomUserStatusTable.tsx @@ -1,11 +1,13 @@ import { Pagination } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { useEndpoint, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement, MutableRefObject } from 'react'; import React, { useState, useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import CustomUserStatusRow from './CustomUserStatusRow'; import FilterByText from '../../../../components/FilterByText'; import GenericNoResult from '../../../../components/GenericNoResults'; import { @@ -17,7 +19,6 @@ import { } from '../../../../components/GenericTable'; import { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; import { useSort } from '../../../../components/GenericTable/hooks/useSort'; -import CustomUserStatusRow from './CustomUserStatusRow'; type CustomUserStatusProps = { reload: MutableRefObject<() => void>; @@ -26,7 +27,7 @@ type CustomUserStatusProps = { // TODO: Missing error state const CustomUserStatus = ({ reload, onClick }: CustomUserStatusProps): ReactElement | null => { - const t = useTranslation(); + const { t } = useTranslation(); const [text, setText] = useState(''); const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination(); const { sortBy, sortDirection, setSort } = useSort<'name' | 'statusType'>('name'); @@ -70,7 +71,7 @@ const CustomUserStatus = ({ reload, onClick }: CustomUserStatusProps): ReactElem return ( <> - + setText(event.target.value)} /> {data.length === 0 && } {data && data.length > 0 && ( <> @@ -91,9 +92,7 @@ const CustomUserStatus = ({ reload, onClick }: CustomUserStatusProps): ReactElem {isLoading && } - {data?.map((status) => ( - - ))} + {data?.map((status) => )} {isFetched && ( diff --git a/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminPage.tsx b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminPage.tsx index 317a9f519030..5ecb0180cc2e 100644 --- a/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminPage.tsx +++ b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminPage.tsx @@ -1,14 +1,15 @@ -import { useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts'; +import { useRouteParameter } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useRef } from 'react'; +import { useTranslation } from 'react-i18next'; -import { ContextualbarDialog } from '../../../components/Contextualbar'; -import { Page, PageHeader, PageContent } from '../../../components/Page'; import DeviceManagementAdminTable from './DeviceManagementAdminTable'; import DeviceManagementInfo from './DeviceManagementInfo'; +import { ContextualbarDialog } from '../../../components/Contextualbar'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; const DeviceManagementAdminPage = (): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const context = useRouteParameter('context'); const deviceId = useRouteParameter('id'); diff --git a/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminRoute.tsx b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminRoute.tsx index e7624ffd956e..6d7216ddf1c6 100644 --- a/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminRoute.tsx +++ b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminRoute.tsx @@ -1,17 +1,18 @@ -import { usePermission, useRouter, useSetModal, useCurrentModal, useTranslation } from '@rocket.chat/ui-contexts'; +import { usePermission, useRouter, useSetModal, useCurrentModal } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import DeviceManagementAdminPage from './DeviceManagementAdminPage'; import { getURL } from '../../../../app/utils/client/getURL'; import GenericUpsellModal from '../../../components/GenericUpsellModal'; import { useUpsellActions } from '../../../components/GenericUpsellModal/hooks'; import PageSkeleton from '../../../components/PageSkeleton'; import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; -import DeviceManagementAdminPage from './DeviceManagementAdminPage'; const DeviceManagementAdminRoute = (): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const router = useRouter(); const setModal = useSetModal(); const isModalOpen = !!useCurrentModal(); diff --git a/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminRow.tsx b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminRow.tsx index 5420be953601..c10a267ea96c 100644 --- a/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminRow.tsx +++ b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminRow.tsx @@ -1,8 +1,9 @@ import { Box, Menu, Option } from '@rocket.chat/fuselage'; import { useMediaQuery, useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; +import { useRoute } from '@rocket.chat/ui-contexts'; import type { KeyboardEvent, ReactElement } from 'react'; import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { GenericTableRow, GenericTableCell } from '../../../../components/GenericTable'; import DeviceIcon from '../../../../components/deviceManagement/DeviceIcon'; @@ -32,7 +33,7 @@ const DeviceManagementAdminRow = ({ rcVersion, onReload, }: DeviceRowProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const deviceManagementRouter = useRoute('device-management'); const formatDateAndTime = useFormatDateAndTime(); const mediaQuery = useMediaQuery('(min-width: 1024px)'); diff --git a/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminTable.tsx b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminTable.tsx index 84037afdcab8..e3ed1bc8e3aa 100644 --- a/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminTable.tsx +++ b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminTable.tsx @@ -4,13 +4,13 @@ import type { ReactElement, MutableRefObject } from 'react'; import React, { useState, useMemo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; +import DeviceManagementAdminRow from './DeviceManagementAdminRow'; import FilterByText from '../../../../components/FilterByText'; import { GenericTableHeaderCell } from '../../../../components/GenericTable'; import { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; import { useSort } from '../../../../components/GenericTable/hooks/useSort'; import DeviceManagementTable from '../../../../components/deviceManagement/DeviceManagementTable'; import { useEndpointData } from '../../../../hooks/useEndpointData'; -import DeviceManagementAdminRow from './DeviceManagementAdminRow'; const sortMapping = { client: 'device.name', @@ -76,7 +76,7 @@ const DeviceManagementAdminTable = ({ reloadRef }: { reloadRef: MutableRefObject return ( <> - + setText(event.target.value)} /> { - const t = useTranslation(); + const { t } = useTranslation(); const deviceManagementRouter = useRoute('device-management'); const formatDateAndTime = useFormatDateAndTime(); diff --git a/apps/meteor/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfoWithData.tsx b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfoWithData.tsx index f884b27e0cb1..97c4f16fac61 100644 --- a/apps/meteor/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfoWithData.tsx +++ b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfoWithData.tsx @@ -4,6 +4,7 @@ import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import DeviceManagementInfo from './DeviceManagementInfo'; import { Contextualbar, ContextualbarSkeleton, @@ -14,7 +15,6 @@ import { } from '../../../../components/Contextualbar'; import { useEndpointData } from '../../../../hooks/useEndpointData'; import { AsyncStatePhase } from '../../../../lib/asyncState'; -import DeviceManagementInfo from './DeviceManagementInfo'; const convertSessionFromAPI = ({ loginAt, diff --git a/apps/meteor/client/views/admin/emailInbox/EmailInboxForm.tsx b/apps/meteor/client/views/admin/emailInbox/EmailInboxForm.tsx index 95da33b159c2..ab92971db5ba 100644 --- a/apps/meteor/client/views/admin/emailInbox/EmailInboxForm.tsx +++ b/apps/meteor/client/views/admin/emailInbox/EmailInboxForm.tsx @@ -18,10 +18,11 @@ import { FieldHint, } from '@rocket.chat/fuselage'; import { useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, useRoute, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetModal, useToastMessageDispatch, useRoute, useEndpoint } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback } from 'react'; import { useForm, Controller } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; import { validateEmail } from '../../../../lib/emailValidator'; import AutoCompleteDepartment from '../../../components/AutoCompleteDepartment'; @@ -29,7 +30,7 @@ import GenericModal from '../../../components/GenericModal'; import { PageScrollableContentWithShadow } from '../../../components/Page'; const EmailInboxForm = ({ inboxData }: { inboxData?: IEmailInboxPayload }): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const setModal = useSetModal(); const router = useRoute('admin-email-inboxes'); diff --git a/apps/meteor/client/views/admin/emailInbox/EmailInboxFormWithData.tsx b/apps/meteor/client/views/admin/emailInbox/EmailInboxFormWithData.tsx index da6c601944e0..2f2f81c9c4e7 100644 --- a/apps/meteor/client/views/admin/emailInbox/EmailInboxFormWithData.tsx +++ b/apps/meteor/client/views/admin/emailInbox/EmailInboxFormWithData.tsx @@ -5,8 +5,8 @@ import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React from 'react'; -import { FormSkeleton } from '../../../components/Skeleton'; import EmailInboxForm from './EmailInboxForm'; +import { FormSkeleton } from '../../../components/Skeleton'; const EmailInboxFormWithData = ({ id }: { id: IEmailInbox['_id'] }): ReactElement => { const t = useTranslation(); diff --git a/apps/meteor/client/views/admin/emailInbox/EmailInboxPage.tsx b/apps/meteor/client/views/admin/emailInbox/EmailInboxPage.tsx index 12eb42cf7894..9d953cea63a0 100644 --- a/apps/meteor/client/views/admin/emailInbox/EmailInboxPage.tsx +++ b/apps/meteor/client/views/admin/emailInbox/EmailInboxPage.tsx @@ -1,15 +1,16 @@ import { Button } from '@rocket.chat/fuselage'; -import { useRouteParameter, useRouter, useTranslation } from '@rocket.chat/ui-contexts'; +import { useRouteParameter, useRouter } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; -import { Page, PageHeader, PageContent } from '../../../components/Page'; import EmailInboxForm from './EmailInboxForm'; import EmailInboxFormWithData from './EmailInboxFormWithData'; import EmailInboxTable from './EmailInboxTable'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; const EmailInboxPage = (): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const router = useRouter(); const id = useRouteParameter('_id'); diff --git a/apps/meteor/client/views/admin/emailInbox/EmailInboxRoute.tsx b/apps/meteor/client/views/admin/emailInbox/EmailInboxRoute.tsx index 7361d13b10c2..76aa229f3553 100644 --- a/apps/meteor/client/views/admin/emailInbox/EmailInboxRoute.tsx +++ b/apps/meteor/client/views/admin/emailInbox/EmailInboxRoute.tsx @@ -2,8 +2,8 @@ import { usePermission } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import EmailInboxPage from './EmailInboxPage'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; const EmailInboxRoute = (): ReactElement => { const canViewEmailInbox = usePermission('manage-email-inbox'); diff --git a/apps/meteor/client/views/admin/emailInbox/EmailInboxTable.tsx b/apps/meteor/client/views/admin/emailInbox/EmailInboxTable.tsx index 281eec89627f..0990b7a30e32 100644 --- a/apps/meteor/client/views/admin/emailInbox/EmailInboxTable.tsx +++ b/apps/meteor/client/views/admin/emailInbox/EmailInboxTable.tsx @@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { useMemo, useCallback } from 'react'; +import SendTestButton from './SendTestButton'; import GenericNoResults from '../../../components/GenericNoResults'; import { GenericTable, @@ -16,7 +17,6 @@ import { } from '../../../components/GenericTable'; import { usePagination } from '../../../components/GenericTable/hooks/usePagination'; import { useSort } from '../../../components/GenericTable/hooks/useSort'; -import SendTestButton from './SendTestButton'; const EmailInboxTable = (): ReactElement => { const t = useTranslation(); diff --git a/apps/meteor/client/views/admin/emailInbox/SendTestButton.tsx b/apps/meteor/client/views/admin/emailInbox/SendTestButton.tsx index c844b6e6f56e..f4ad162b6093 100644 --- a/apps/meteor/client/views/admin/emailInbox/SendTestButton.tsx +++ b/apps/meteor/client/views/admin/emailInbox/SendTestButton.tsx @@ -1,13 +1,14 @@ import type { IEmailInboxPayload } from '@rocket.chat/core-typings'; import { Button } from '@rocket.chat/fuselage'; -import { useToastMessageDispatch, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { GenericTableCell } from '../../../components/GenericTable'; const SendTestButton = ({ id }: { id: IEmailInboxPayload['_id'] }): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const sendTest = useEndpoint('POST', '/v1/email-inbox.send-test/:_id', { _id: id }); diff --git a/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx b/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx index 95de437462a5..c6c1864446fd 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx +++ b/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx @@ -3,10 +3,10 @@ import type { ReactElement } from 'react'; import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Page, PageHeader, PageScrollableContent } from '../../../components/Page'; import ChannelsTab from './channels/ChannelsTab'; import MessagesTab from './messages/MessagesTab'; import UsersTab from './users/UsersTab'; +import { Page, PageHeader, PageScrollableContent } from '../../../components/Page'; type EngagementDashboardPageProps = { tab: 'users' | 'messages' | 'channels'; diff --git a/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardRoute.tsx b/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardRoute.tsx index 17f6713220f9..ea5c20ca9e5b 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardRoute.tsx +++ b/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardRoute.tsx @@ -10,13 +10,13 @@ import { import type { ReactElement } from 'react'; import React, { useEffect } from 'react'; +import EngagementDashboardPage from './EngagementDashboardPage'; import { getURL } from '../../../../app/utils/client/getURL'; import GenericUpsellModal from '../../../components/GenericUpsellModal'; import { useUpsellActions } from '../../../components/GenericUpsellModal/hooks'; import PageSkeleton from '../../../components/PageSkeleton'; import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; -import EngagementDashboardPage from './EngagementDashboardPage'; const isValidTab = (tab: string | undefined): tab is 'users' | 'messages' | 'channels' => typeof tab === 'string' && ['users', 'messages', 'channels'].includes(tab); diff --git a/apps/meteor/client/views/admin/engagementDashboard/channels/useChannelsList.ts b/apps/meteor/client/views/admin/engagementDashboard/channels/useChannelsList.ts index f45c620c8b49..875f9c2d932d 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/channels/useChannelsList.ts +++ b/apps/meteor/client/views/admin/engagementDashboard/channels/useChannelsList.ts @@ -32,7 +32,7 @@ export const useChannelsList = ({ period, offset, count }: UseChannelsListOption ...response, start, end, - } + } : undefined; }, { diff --git a/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesPerChannelSection.tsx b/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesPerChannelSection.tsx index 88fad25990d0..86e7836b40d3 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesPerChannelSection.tsx +++ b/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesPerChannelSection.tsx @@ -18,13 +18,13 @@ import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { useMessageOrigins } from './useMessageOrigins'; +import { useTopFivePopularChannels } from './useTopFivePopularChannels'; import DownloadDataButton from '../../../../components/dashboards/DownloadDataButton'; import PeriodSelector from '../../../../components/dashboards/PeriodSelector'; import { usePeriodSelectorState } from '../../../../components/dashboards/usePeriodSelectorState'; import EngagementDashboardCardFilter from '../EngagementDashboardCardFilter'; import LegendSymbol from '../dataView/LegendSymbol'; -import { useMessageOrigins } from './useMessageOrigins'; -import { useTopFivePopularChannels } from './useTopFivePopularChannels'; const colors = { warning: Palette.statusColor['status-font-on-warning'].toString(), diff --git a/apps/meteor/client/views/admin/engagementDashboard/messages/useMessageOrigins.ts b/apps/meteor/client/views/admin/engagementDashboard/messages/useMessageOrigins.ts index f44b87b4f8f8..e4e9526b571f 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/messages/useMessageOrigins.ts +++ b/apps/meteor/client/views/admin/engagementDashboard/messages/useMessageOrigins.ts @@ -25,7 +25,7 @@ export const useMessageOrigins = ({ period }: UseMessageOriginsOptions) => { ...response, start, end, - } + } : undefined; }, { diff --git a/apps/meteor/client/views/admin/engagementDashboard/messages/useMessagesSent.ts b/apps/meteor/client/views/admin/engagementDashboard/messages/useMessagesSent.ts index 6b7f40b8ae74..daeeaa388f58 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/messages/useMessagesSent.ts +++ b/apps/meteor/client/views/admin/engagementDashboard/messages/useMessagesSent.ts @@ -25,7 +25,7 @@ export const useMessagesSent = ({ period }: UseMessagesSentOptions) => { ...response, start, end, - } + } : undefined; }, { diff --git a/apps/meteor/client/views/admin/engagementDashboard/messages/useTopFivePopularChannels.ts b/apps/meteor/client/views/admin/engagementDashboard/messages/useTopFivePopularChannels.ts index cc6707de919d..d7566d3df6fb 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/messages/useTopFivePopularChannels.ts +++ b/apps/meteor/client/views/admin/engagementDashboard/messages/useTopFivePopularChannels.ts @@ -25,7 +25,7 @@ export const useTopFivePopularChannels = ({ period }: UseTopFivePopularChannelsO ...response, start, end, - } + } : undefined; }, { diff --git a/apps/meteor/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx b/apps/meteor/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx index 7ac1b7c91289..02776ecb555e 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx +++ b/apps/meteor/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx @@ -6,12 +6,12 @@ import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { useActiveUsers } from './useActiveUsers'; import DownloadDataButton from '../../../../components/dashboards/DownloadDataButton'; import CounterSet from '../../../../components/dataView/CounterSet'; import { useFormatDate } from '../../../../hooks/useFormatDate'; import EngagementDashboardCardFilter from '../EngagementDashboardCardFilter'; import LegendSymbol from '../dataView/LegendSymbol'; -import { useActiveUsers } from './useActiveUsers'; type ActiveUsersSectionProps = { timezone: 'utc' | 'local'; diff --git a/apps/meteor/client/views/admin/engagementDashboard/users/useActiveUsers.ts b/apps/meteor/client/views/admin/engagementDashboard/users/useActiveUsers.ts index 2de826eb69b3..345540a08e1f 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/users/useActiveUsers.ts +++ b/apps/meteor/client/views/admin/engagementDashboard/users/useActiveUsers.ts @@ -24,7 +24,7 @@ export const useActiveUsers = ({ utc }: UseActiveUsersOptions) => { ...response, start, end, - } + } : undefined; }, { diff --git a/apps/meteor/client/views/admin/engagementDashboard/users/useHourlyChatActivity.ts b/apps/meteor/client/views/admin/engagementDashboard/users/useHourlyChatActivity.ts index 56dd4b883c83..f2a7b98ebc41 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/users/useHourlyChatActivity.ts +++ b/apps/meteor/client/views/admin/engagementDashboard/users/useHourlyChatActivity.ts @@ -24,7 +24,7 @@ export const useHourlyChatActivity = ({ displacement, utc }: UseHourlyChatActivi ? { ...response, day, - } + } : undefined; }, { diff --git a/apps/meteor/client/views/admin/engagementDashboard/users/useNewUsers.ts b/apps/meteor/client/views/admin/engagementDashboard/users/useNewUsers.ts index d43dc628c587..1d378dc77b49 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/users/useNewUsers.ts +++ b/apps/meteor/client/views/admin/engagementDashboard/users/useNewUsers.ts @@ -22,7 +22,7 @@ export const useNewUsers = ({ period, utc }: { period: Period['key']; utc: boole ...response, start, end, - } + } : undefined; }, { diff --git a/apps/meteor/client/views/admin/engagementDashboard/users/useUsersByTimeOfTheDay.ts b/apps/meteor/client/views/admin/engagementDashboard/users/useUsersByTimeOfTheDay.ts index 0faa8a434b2f..6a9fa7897814 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/users/useUsersByTimeOfTheDay.ts +++ b/apps/meteor/client/views/admin/engagementDashboard/users/useUsersByTimeOfTheDay.ts @@ -25,7 +25,7 @@ export const useUsersByTimeOfTheDay = ({ period, utc }: UseUsersByTimeOfTheDayOp ...response, start, end, - } + } : undefined; }, { diff --git a/apps/meteor/client/views/admin/engagementDashboard/users/useWeeklyChatActivity.ts b/apps/meteor/client/views/admin/engagementDashboard/users/useWeeklyChatActivity.ts index a04e9d5da633..50b6667d535b 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/users/useWeeklyChatActivity.ts +++ b/apps/meteor/client/views/admin/engagementDashboard/users/useWeeklyChatActivity.ts @@ -24,7 +24,7 @@ export const useWeeklyChatActivity = ({ displacement, utc }: UseWeeklyChatActivi ? { ...response, day, - } + } : undefined; }, { diff --git a/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx index a7d6bd77d136..3222e37a91d9 100644 --- a/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx +++ b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx @@ -2,10 +2,10 @@ import { usePermission } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { memo } from 'react'; +import AdminFeaturePreviewPage from './AdminFeaturePreviewPage'; import SettingsProvider from '../../../providers/SettingsProvider'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import EditableSettingsProvider from '../settings/EditableSettingsProvider'; -import AdminFeaturePreviewPage from './AdminFeaturePreviewPage'; const AdminFeaturePreviewRoute = (): ReactElement => { const canViewFeaturesPreview = usePermission('manage-cloud'); diff --git a/apps/meteor/client/views/admin/federationDashboard/FederationDashboardPage.tsx b/apps/meteor/client/views/admin/federationDashboard/FederationDashboardPage.tsx index 14dc96283f86..1d3bbbcfbe42 100644 --- a/apps/meteor/client/views/admin/federationDashboard/FederationDashboardPage.tsx +++ b/apps/meteor/client/views/admin/federationDashboard/FederationDashboardPage.tsx @@ -3,9 +3,9 @@ import type { ReactElement } from 'react'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import OverviewSection from './OverviewSection'; import ServersSection from './ServersSection'; +import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; function FederationDashboardPage(): ReactElement { const { t } = useTranslation(); diff --git a/apps/meteor/client/views/admin/federationDashboard/FederationDashboardRoute.tsx b/apps/meteor/client/views/admin/federationDashboard/FederationDashboardRoute.tsx index e4a9ab4d97c3..8227a0343c72 100644 --- a/apps/meteor/client/views/admin/federationDashboard/FederationDashboardRoute.tsx +++ b/apps/meteor/client/views/admin/federationDashboard/FederationDashboardRoute.tsx @@ -1,8 +1,8 @@ import { useRole } from '@rocket.chat/ui-contexts'; import React from 'react'; -import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import FederationDashboardPage from './FederationDashboardPage'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; const FederationDashboardRoute = () => { const authorized = useRole('admin'); diff --git a/apps/meteor/client/views/admin/federationDashboard/OverviewSection.tsx b/apps/meteor/client/views/admin/federationDashboard/OverviewSection.tsx index f289d6a3dc83..7ac7395fc84c 100644 --- a/apps/meteor/client/views/admin/federationDashboard/OverviewSection.tsx +++ b/apps/meteor/client/views/admin/federationDashboard/OverviewSection.tsx @@ -1,8 +1,9 @@ import { Box, Skeleton } from '@rocket.chat/fuselage'; -import { useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useMethod } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement, ReactNode } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import CounterSet from '../../../components/dataView/CounterSet'; @@ -41,7 +42,7 @@ const useOverviewData = (): [eventCount: ReactNode, userCount: ReactNode, server }; function OverviewSection(): ReactElement { - const t = useTranslation(); + const { t } = useTranslation(); const [eventCount, userCount, serverCount] = useOverviewData(); diff --git a/apps/meteor/client/views/admin/import/ImportHistoryPage.tsx b/apps/meteor/client/views/admin/import/ImportHistoryPage.tsx index 42b8cee969f5..4f9d9dfe2698 100644 --- a/apps/meteor/client/views/admin/import/ImportHistoryPage.tsx +++ b/apps/meteor/client/views/admin/import/ImportHistoryPage.tsx @@ -4,10 +4,10 @@ import { useToastMessageDispatch, useEndpoint, useTranslation, useRouter } from import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import React, { useMemo } from 'react'; -import { ProgressStep } from '../../../../app/importer/lib/ImporterProgressStep'; -import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import ImportOperationSummary from './ImportOperationSummary'; import ImportOperationSummarySkeleton from './ImportOperationSummarySkeleton'; +import { ProgressStep } from '../../../../app/importer/lib/ImporterProgressStep'; +import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; // TODO: review inner logic function ImportHistoryPage() { @@ -151,6 +151,9 @@ function ImportHistoryPage() { {t('Users')} + + {t('Contacts')} + {t('Channels')} @@ -180,9 +183,7 @@ function ImportHistoryPage() { {latestOperations.data .filter(({ _id }) => !currentOperation.data.valid || currentOperation.data._id !== _id) // Forcing valid=false as the current API only accept preparation/progress over currentOperation - ?.map((operation) => ( - - ))} + ?.map((operation) => )} )} diff --git a/apps/meteor/client/views/admin/import/ImportOperationSummary.tsx b/apps/meteor/client/views/admin/import/ImportOperationSummary.tsx index dc8d6bfe0ec5..e4d4378e5b69 100644 --- a/apps/meteor/client/views/admin/import/ImportOperationSummary.tsx +++ b/apps/meteor/client/views/admin/import/ImportOperationSummary.tsx @@ -1,8 +1,9 @@ import type { Serialized } from '@rocket.chat/core-typings'; import { TableRow, TableCell } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; +import { useRouter } from '@rocket.chat/ui-contexts'; import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import { ImportWaitingStates, @@ -24,6 +25,7 @@ type ImportOperationSummaryProps = { users?: number; channels?: number; messages?: number; + contacts?: number; total?: number; }; valid?: boolean; @@ -37,10 +39,10 @@ function ImportOperationSummary({ file = '', user, small, - count: { users = 0, channels = 0, messages = 0, total = 0 } = {}, + count: { users = 0, channels = 0, messages = 0, total = 0, contacts = 0 } = {}, valid, }: ImportOperationSummaryProps) { - const t = useTranslation(); + const { t } = useTranslation(); const formatDateAndTime = useFormatDateAndTime(); const fileName = useMemo(() => { @@ -89,7 +91,7 @@ function ImportOperationSummary({ role: 'link', action: true, onClick: handleClick, - } + } : {}; return ( @@ -101,6 +103,7 @@ function ImportOperationSummary({ {status && t(status.replace('importer_', 'importer_status_') as TranslationKey)} {fileName} {users} + {contacts} {channels} {messages} {total} diff --git a/apps/meteor/client/views/admin/import/ImportOperationSummarySkeleton.tsx b/apps/meteor/client/views/admin/import/ImportOperationSummarySkeleton.tsx index 8c2a465cb58b..42391a253d82 100644 --- a/apps/meteor/client/views/admin/import/ImportOperationSummarySkeleton.tsx +++ b/apps/meteor/client/views/admin/import/ImportOperationSummarySkeleton.tsx @@ -34,6 +34,9 @@ function ImportOperationSummarySkeleton({ small = false }: ImportOperationSummar + + + )} diff --git a/apps/meteor/client/views/admin/import/ImportProgressPage.tsx b/apps/meteor/client/views/admin/import/ImportProgressPage.tsx index a8e083543398..a5697f526aaf 100644 --- a/apps/meteor/client/views/admin/import/ImportProgressPage.tsx +++ b/apps/meteor/client/views/admin/import/ImportProgressPage.tsx @@ -1,20 +1,21 @@ import type { ProgressStep } from '@rocket.chat/core-typings'; import { Box, Margins, ProgressBar, Throbber } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useToastMessageDispatch, useEndpoint, useTranslation, useStream, useRouter } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useEndpoint, useStream, useRouter } from '@rocket.chat/ui-contexts'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import React, { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useErrorHandler } from './useErrorHandler'; import { ImportingStartedStates } from '../../../../app/importer/lib/ImporterProgressStep'; import { numberFormat } from '../../../../lib/utils/stringUtils'; import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; -import { useErrorHandler } from './useErrorHandler'; // TODO: review inner logic const ImportProgressPage = function ImportProgressPage() { const queryClient = useQueryClient(); const streamer = useStream('importers'); - const t = useTranslation(); + const { t, i18n } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const handleError = useErrorHandler(); @@ -77,7 +78,7 @@ const ImportProgressPage = function ImportProgressPage() { switch (step) { case 'importer_done': - t.has(message) && + i18n.exists(message) && dispatchToastMessage({ type: 'success', message: t(message), @@ -87,7 +88,7 @@ const ImportProgressPage = function ImportProgressPage() { case 'importer_import_failed': case 'importer_import_cancelled': - t.has(message) && handleError(message); + i18n.exists(message) && handleError(message); router.navigate('/admin/import'); return; diff --git a/apps/meteor/client/views/admin/import/ImportRoute.tsx b/apps/meteor/client/views/admin/import/ImportRoute.tsx index 96b5179b9ae0..50d4cfebef31 100644 --- a/apps/meteor/client/views/admin/import/ImportRoute.tsx +++ b/apps/meteor/client/views/admin/import/ImportRoute.tsx @@ -1,11 +1,11 @@ import { usePermission } from '@rocket.chat/ui-contexts'; import React from 'react'; -import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import ImportHistoryPage from './ImportHistoryPage'; import ImportProgressPage from './ImportProgressPage'; import NewImportPage from './NewImportPage'; import PrepareImportPage from './PrepareImportPage'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; type ImportHistoryRouteProps = { page: 'history' | 'new' | 'prepare' | 'progress'; diff --git a/apps/meteor/client/views/admin/import/NewImportPage.tsx b/apps/meteor/client/views/admin/import/NewImportPage.tsx index 82422c1f27ca..54de54e095dd 100644 --- a/apps/meteor/client/views/admin/import/NewImportPage.tsx +++ b/apps/meteor/client/views/admin/import/NewImportPage.tsx @@ -1,18 +1,19 @@ import { Box, Button, ButtonGroup, Callout, Chip, Field, Margins, Select, InputBox, TextInput, UrlInput } from '@rocket.chat/fuselage'; import { useUniqueId, useSafely } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useToastMessageDispatch, useRouter, useRouteParameter, useSetting, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useRouter, useRouteParameter, useSetting, useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ChangeEvent, DragEvent, FormEvent, Key, SyntheticEvent } from 'react'; import React, { useState, useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useErrorHandler } from './useErrorHandler'; import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import { useFormatMemorySize } from '../../../hooks/useFormatMemorySize'; -import { useErrorHandler } from './useErrorHandler'; // TODO: review inner logic function NewImportPage() { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const handleError = useErrorHandler(); @@ -29,7 +30,7 @@ function NewImportPage() { const importerKey = useRouteParameter('importerKey'); const importer = useMemo(() => (importers || []).find(({ key }) => key === importerKey), [importerKey, importers]); - const maxFileSize = useSetting('FileUpload_MaxFileSize') ?? 0; + const maxFileSize = useSetting('FileUpload_MaxFileSize', 0); const router = useRouter(); diff --git a/apps/meteor/client/views/admin/import/PrepareContacts.tsx b/apps/meteor/client/views/admin/import/PrepareContacts.tsx new file mode 100644 index 000000000000..f4fcd9f427e8 --- /dev/null +++ b/apps/meteor/client/views/admin/import/PrepareContacts.tsx @@ -0,0 +1,75 @@ +import type { IImporterSelectionContact } from '@rocket.chat/core-typings'; +import { CheckBox, Table, Pagination, TableHead, TableRow, TableCell, TableBody } from '@rocket.chat/fuselage'; +import type { Dispatch, SetStateAction, ChangeEvent } from 'react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { usePagination } from '../../../components/GenericTable/hooks/usePagination'; + +type PrepareContactsProps = { + contactsCount: number; + contacts: IImporterSelectionContact[]; + setContacts: Dispatch>; +}; + +const PrepareContacts = ({ contactsCount, contacts, setContacts }: PrepareContactsProps) => { + const { t } = useTranslation(); + const { current, itemsPerPage, setItemsPerPage, setCurrent, ...paginationProps } = usePagination(); + + return ( + <> +

+ + + + 0} + indeterminate={contactsCount > 0 && contactsCount !== contacts.length} + onChange={(): void => { + setContacts((contacts) => { + const isChecking = contactsCount === 0; + + return contacts.map((contact) => ({ ...contact, do_import: isChecking })); + }); + }} + /> + + {t('Name')} + {t('Emails')} + {t('Phones')} + + + + {contacts.slice(current, current + itemsPerPage).map((contact) => ( + + + ): void => { + const { checked } = event.currentTarget; + setContacts((contacts) => + contacts.map((_contact) => (_contact === contact ? { ..._contact, do_import: checked } : _contact)), + ); + }} + /> + + {contact.name} + {contact.emails.join('\n')} + {contact.phones.join('\n')} + + ))} + +
+ + + ); +}; + +export default PrepareContacts; diff --git a/apps/meteor/client/views/admin/import/PrepareImportPage.tsx b/apps/meteor/client/views/admin/import/PrepareImportPage.tsx index 5af842c4bebe..9f68a3f2c619 100644 --- a/apps/meteor/client/views/admin/import/PrepareImportPage.tsx +++ b/apps/meteor/client/views/admin/import/PrepareImportPage.tsx @@ -1,10 +1,16 @@ -import type { IImport, IImporterSelection, Serialized } from '@rocket.chat/core-typings'; +import type { IImport, IImporterSelection, IImporterSelectionContact, Serialized } from '@rocket.chat/core-typings'; import { Badge, Box, Button, ButtonGroup, Margins, ProgressBar, Throbber, Tabs } from '@rocket.chat/fuselage'; import { useDebouncedValue, useSafely } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useEndpoint, useTranslation, useStream, useRouter } from '@rocket.chat/ui-contexts'; import React, { useEffect, useState, useMemo } from 'react'; +import type { ChannelDescriptor } from './ChannelDescriptor'; +import PrepareChannels from './PrepareChannels'; +import PrepareContacts from './PrepareContacts'; +import PrepareUsers from './PrepareUsers'; +import type { UserDescriptor } from './UserDescriptor'; +import { useErrorHandler } from './useErrorHandler'; import { ProgressStep, ImportWaitingStates, @@ -15,11 +21,6 @@ import { } from '../../../../app/importer/lib/ImporterProgressStep'; import { numberFormat } from '../../../../lib/utils/stringUtils'; import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; -import type { ChannelDescriptor } from './ChannelDescriptor'; -import PrepareChannels from './PrepareChannels'; -import PrepareUsers from './PrepareUsers'; -import type { UserDescriptor } from './UserDescriptor'; -import { useErrorHandler } from './useErrorHandler'; const waitFor = (fn: () => Promise, predicate: (arg: T) => arg is U) => new Promise((resolve, reject) => { @@ -47,11 +48,13 @@ function PrepareImportPage() { const [status, setStatus] = useSafely(useState(null)); const [messageCount, setMessageCount] = useSafely(useState(0)); const [users, setUsers] = useState([]); + const [contacts, setContacts] = useState([]); const [channels, setChannels] = useState([]); const [isImporting, setImporting] = useSafely(useState(false)); const usersCount = useMemo(() => users.filter(({ do_import }) => do_import).length, [users]); const channelsCount = useMemo(() => channels.filter(({ do_import }) => do_import).length, [channels]); + const contactsCount = useMemo(() => contacts.filter(({ do_import }) => do_import).length, [contacts]); const router = useRouter(); @@ -89,6 +92,7 @@ function PrepareImportPage() { setMessageCount(data.message_count); setUsers(data.users.map((user) => ({ ...user, username: user.username ?? '', do_import: true }))); setChannels(data.channels.map((channel) => ({ ...channel, name: channel.name ?? '', do_import: true }))); + setContacts(data.contacts?.map((contact) => ({ ...contact, name: contact.name ?? '', do_import: true })) || []); setPreparing(false); setProgressRate(null); } catch (error) { @@ -153,6 +157,7 @@ function PrepareImportPage() { try { const usersToImport = users.filter(({ do_import }) => do_import).map(({ user_id }) => user_id); const channelsToImport = channels.filter(({ do_import }) => do_import).map(({ channel_id }) => channel_id); + const contactsToImport = contacts.filter(({ do_import }) => do_import).map(({ id }) => id); await startImport({ input: { @@ -164,6 +169,10 @@ function PrepareImportPage() { all: channels.length > 0 && channelsToImport.length === channels.length, list: (channelsToImport.length !== channels.length && channelsToImport) || undefined, }, + contacts: { + all: contacts.length > 0 && contactsToImport.length === contacts.length, + list: (contactsToImport.length !== contacts.length && contactsToImport) || undefined, + }, }, }); router.navigate('/admin/import/progress'); @@ -179,8 +188,8 @@ function PrepareImportPage() { const statusDebounced = useDebouncedValue(status, 100); const handleMinimumImportData = !!( - (!usersCount && !channelsCount && !messageCount) || - (!usersCount && !channelsCount && messageCount !== 0) + (!usersCount && !channelsCount && !contactsCount && !messageCount) || + (!usersCount && !channelsCount && !contactsCount && messageCount !== 0) ); return ( @@ -202,6 +211,9 @@ function PrepareImportPage() { {t('Users')} {usersCount} + + {t('Contacts')} {contactsCount} + {t('Channels')} {channelsCount} @@ -227,6 +239,9 @@ function PrepareImportPage() { )} {!isPreparing && tab === 'users' && } + {!isPreparing && tab === 'contacts' && ( + + )} {!isPreparing && tab === 'channels' && ( )} diff --git a/apps/meteor/client/views/admin/integrations/EditIntegrationsPageWithData.tsx b/apps/meteor/client/views/admin/integrations/EditIntegrationsPageWithData.tsx index 899ad4393290..15757a6681d3 100644 --- a/apps/meteor/client/views/admin/integrations/EditIntegrationsPageWithData.tsx +++ b/apps/meteor/client/views/admin/integrations/EditIntegrationsPageWithData.tsx @@ -1,14 +1,15 @@ import type { IIncomingIntegration } from '@rocket.chat/core-typings'; import { Box, Skeleton } from '@rocket.chat/fuselage'; -import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import EditIncomingWebhook from './incoming/EditIncomingWebhook'; import EditOutgoingWebhook from './outgoing/EditOutgoingWebhook'; const EditIntegrationsPageWithData = ({ integrationId }: { integrationId: IIncomingIntegration['_id'] }) => { - const t = useTranslation(); + const { t } = useTranslation(); const params = useMemo(() => ({ integrationId }), [integrationId]); const getIntegrations = useEndpoint('GET', '/v1/integrations.get'); diff --git a/apps/meteor/client/views/admin/integrations/IntegrationsPage.tsx b/apps/meteor/client/views/admin/integrations/IntegrationsPage.tsx index a54c83a12429..35c054109f9a 100644 --- a/apps/meteor/client/views/admin/integrations/IntegrationsPage.tsx +++ b/apps/meteor/client/views/admin/integrations/IntegrationsPage.tsx @@ -1,15 +1,16 @@ import { Button, ButtonGroup, Tabs } from '@rocket.chat/fuselage'; -import { useRouteParameter, useRouter, useTranslation } from '@rocket.chat/ui-contexts'; +import { useRouteParameter, useRouter } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; -import { Page, PageHeader, PageContent } from '../../../components/Page'; import IntegrationsTable from './IntegrationsTable'; import NewBot from './NewBot'; import NewZapier from './NewZapier'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; const IntegrationsPage = (): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const router = useRouter(); const context = useRouteParameter('context'); diff --git a/apps/meteor/client/views/admin/integrations/IntegrationsRoute.tsx b/apps/meteor/client/views/admin/integrations/IntegrationsRoute.tsx index b8520f7063c1..7af9bdd55d62 100644 --- a/apps/meteor/client/views/admin/integrations/IntegrationsRoute.tsx +++ b/apps/meteor/client/views/admin/integrations/IntegrationsRoute.tsx @@ -1,10 +1,10 @@ import { useRouteParameter, useAtLeastOnePermission } from '@rocket.chat/ui-contexts'; import React, { useMemo } from 'react'; -import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import EditIntegrationsPage from './EditIntegrationsPage'; import EditIntegrationsPageWithData from './EditIntegrationsPageWithData'; import IntegrationsPage from './IntegrationsPage'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import OutgoingWebhookHistoryPage from './outgoing/history/OutgoingWebhookHistoryPage'; const IntegrationsRoute = () => { diff --git a/apps/meteor/client/views/admin/integrations/IntegrationsTable.tsx b/apps/meteor/client/views/admin/integrations/IntegrationsTable.tsx index 791b5a6ad02e..6a3e4121903a 100644 --- a/apps/meteor/client/views/admin/integrations/IntegrationsTable.tsx +++ b/apps/meteor/client/views/admin/integrations/IntegrationsTable.tsx @@ -5,6 +5,7 @@ import { useEndpoint, useRoute, useTranslation, useLayout } from '@rocket.chat/u import { useQuery } from '@tanstack/react-query'; import React, { useMemo, useCallback, useState } from 'react'; +import IntegrationRow from './IntegrationRow'; import FilterByText from '../../../components/FilterByText'; import GenericNoResults from '../../../components/GenericNoResults'; import { @@ -16,7 +17,6 @@ import { } from '../../../components/GenericTable'; import { usePagination } from '../../../components/GenericTable/hooks/usePagination'; import { useSort } from '../../../components/GenericTable/hooks/useSort'; -import IntegrationRow from './IntegrationRow'; const IntegrationsTable = ({ type }: { type?: string }) => { const t = useTranslation(); @@ -97,7 +97,7 @@ const IntegrationsTable = ({ type }: { type?: string }) => { return ( <> - + setText(event.target.value)} /> {isLoading && ( {headers} diff --git a/apps/meteor/client/views/admin/integrations/hooks/useCreateIntegration.ts b/apps/meteor/client/views/admin/integrations/hooks/useCreateIntegration.ts index 7b157ec186c3..fbd1364ddcb8 100644 --- a/apps/meteor/client/views/admin/integrations/hooks/useCreateIntegration.ts +++ b/apps/meteor/client/views/admin/integrations/hooks/useCreateIntegration.ts @@ -1,8 +1,9 @@ -import { useEndpoint, useRouter, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useRouter, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; export const useCreateIntegration = (integrationType: 'webhook-incoming' | 'webhook-outgoing') => { - const t = useTranslation(); + const { t } = useTranslation(); const router = useRouter(); const createIntegration = useEndpoint('POST', '/v1/integrations.create'); const dispatchToastMessage = useToastMessageDispatch(); diff --git a/apps/meteor/client/views/admin/integrations/hooks/useDeleteIntegration.ts b/apps/meteor/client/views/admin/integrations/hooks/useDeleteIntegration.ts index a43df492a101..24e7ad3538ed 100644 --- a/apps/meteor/client/views/admin/integrations/hooks/useDeleteIntegration.ts +++ b/apps/meteor/client/views/admin/integrations/hooks/useDeleteIntegration.ts @@ -1,8 +1,9 @@ -import { useEndpoint, useRouter, useSetModal, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useRouter, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; export const useDeleteIntegration = (integrationType: 'webhook-incoming' | 'webhook-outgoing') => { - const t = useTranslation(); + const { t } = useTranslation(); const router = useRouter(); const setModal = useSetModal(); diff --git a/apps/meteor/client/views/admin/integrations/hooks/useUpdateIntegration.ts b/apps/meteor/client/views/admin/integrations/hooks/useUpdateIntegration.ts index 7ba61a3a8437..04a2ab0ea77f 100644 --- a/apps/meteor/client/views/admin/integrations/hooks/useUpdateIntegration.ts +++ b/apps/meteor/client/views/admin/integrations/hooks/useUpdateIntegration.ts @@ -1,8 +1,9 @@ -import { useEndpoint, useRouter, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useRouter, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; export const useUpdateIntegration = (integrationType: 'webhook-incoming' | 'webhook-outgoing') => { - const t = useTranslation(); + const { t } = useTranslation(); const router = useRouter(); const updateIntegration = useEndpoint('PUT', '/v1/integrations.update'); const dispatchToastMessage = useToastMessageDispatch(); diff --git a/apps/meteor/client/views/admin/integrations/incoming/EditIncomingWebhook.tsx b/apps/meteor/client/views/admin/integrations/incoming/EditIncomingWebhook.tsx index c14c2b19d02e..40303e51c5f3 100644 --- a/apps/meteor/client/views/admin/integrations/incoming/EditIncomingWebhook.tsx +++ b/apps/meteor/client/views/admin/integrations/incoming/EditIncomingWebhook.tsx @@ -5,12 +5,12 @@ import { useSetModal, useTranslation, useRouter, useRouteParameter } from '@rock import React, { useCallback } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; +import IncomingWebhookForm from './IncomingWebhookForm'; import GenericModal from '../../../../components/GenericModal'; import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../../components/Page'; import { useCreateIntegration } from '../hooks/useCreateIntegration'; import { useDeleteIntegration } from '../hooks/useDeleteIntegration'; import { useUpdateIntegration } from '../hooks/useUpdateIntegration'; -import IncomingWebhookForm from './IncomingWebhookForm'; const getInitialValue = (webhookData: Serialized | undefined) => ({ enabled: webhookData?.enabled ?? true, diff --git a/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx b/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx index 13249d024816..4393d3a3ef84 100644 --- a/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx +++ b/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx @@ -18,16 +18,17 @@ import { FieldHint, } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; -import { useAbsoluteUrl, useTranslation } from '@rocket.chat/ui-contexts'; +import { useAbsoluteUrl } from '@rocket.chat/ui-contexts'; import React, { useMemo } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; import useClipboardWithToast from '../../../../hooks/useClipboardWithToast'; import { useHighlightedCode } from '../../../../hooks/useHighlightedCode'; import { useExampleData } from '../hooks/useExampleIncomingData'; const IncomingWebhookForm = ({ webhookData }: { webhookData?: Serialized }) => { - const t = useTranslation(); + const { t } = useTranslation(); const absoluteUrl = useAbsoluteUrl(); const { @@ -178,7 +179,10 @@ const IncomingWebhookForm = ({ webhookData }: { webhookData?: Serialized {errors?.channel && ( @@ -265,7 +269,10 @@ const IncomingWebhookForm = ({ webhookData }: { webhookData?: Serialized {t('You_can_use_an_emoji_as_avatar')} - + diff --git a/apps/meteor/client/views/admin/integrations/outgoing/EditOutgoingWebhook.tsx b/apps/meteor/client/views/admin/integrations/outgoing/EditOutgoingWebhook.tsx index 1ca7d1f481cd..a9330238eee3 100644 --- a/apps/meteor/client/views/admin/integrations/outgoing/EditOutgoingWebhook.tsx +++ b/apps/meteor/client/views/admin/integrations/outgoing/EditOutgoingWebhook.tsx @@ -5,13 +5,13 @@ import { useSetModal, useTranslation, useRouter, useRouteParameter } from '@rock import React, { useCallback } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; +import OutgoingWebhookForm from './OutgoingWebhookForm'; import GenericModal from '../../../../components/GenericModal'; import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../../components/Page'; import { triggerWordsToArray, triggerWordsToString } from '../helpers/triggerWords'; import { useCreateIntegration } from '../hooks/useCreateIntegration'; import { useDeleteIntegration } from '../hooks/useDeleteIntegration'; import { useUpdateIntegration } from '../hooks/useUpdateIntegration'; -import OutgoingWebhookForm from './OutgoingWebhookForm'; const getInitialValue = (webhookData: Serialized | undefined, defaultToken: string) => ({ enabled: webhookData?.enabled ?? true, diff --git a/apps/meteor/client/views/admin/integrations/outgoing/OutgoingWebhookForm.tsx b/apps/meteor/client/views/admin/integrations/outgoing/OutgoingWebhookForm.tsx index 8abd6c82356f..b331796aa340 100644 --- a/apps/meteor/client/views/admin/integrations/outgoing/OutgoingWebhookForm.tsx +++ b/apps/meteor/client/views/admin/integrations/outgoing/OutgoingWebhookForm.tsx @@ -357,7 +357,10 @@ const OutgoingWebhookForm = () => { /> {t('You_can_use_an_emoji_as_avatar')} - + diff --git a/apps/meteor/client/views/admin/integrations/outgoing/history/HistoryItem.tsx b/apps/meteor/client/views/admin/integrations/outgoing/history/HistoryItem.tsx index e8b3895c29ef..e823172cba8a 100644 --- a/apps/meteor/client/views/admin/integrations/outgoing/history/HistoryItem.tsx +++ b/apps/meteor/client/views/admin/integrations/outgoing/history/HistoryItem.tsx @@ -1,15 +1,16 @@ import type { IIntegrationHistory, Serialized } from '@rocket.chat/core-typings'; import { Button, Icon, Box, Accordion, Field, FieldGroup, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useMethod } from '@rocket.chat/ui-contexts'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { outgoingEvents } from '../../../../../../app/integrations/lib/outgoingEvents'; import { useFormatDateAndTime } from '../../../../../hooks/useFormatDateAndTime'; import { useHighlightedCode } from '../../../../../hooks/useHighlightedCode'; const HistoryItem = ({ data }: { data: Serialized }) => { - const t = useTranslation(); + const { t } = useTranslation(); const replayOutgoingIntegration = useMethod('replayOutgoingIntegration'); diff --git a/apps/meteor/client/views/admin/integrations/outgoing/history/OutgoingWebhookHistoryPage.tsx b/apps/meteor/client/views/admin/integrations/outgoing/history/OutgoingWebhookHistoryPage.tsx index ac05b3bf0ddd..b5d04d9fb2d6 100644 --- a/apps/meteor/client/views/admin/integrations/outgoing/history/OutgoingWebhookHistoryPage.tsx +++ b/apps/meteor/client/views/admin/integrations/outgoing/history/OutgoingWebhookHistoryPage.tsx @@ -4,11 +4,11 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import type { ComponentProps } from 'react'; import React, { useMemo, useState, useEffect } from 'react'; +import HistoryContent from './HistoryContent'; import { sdk } from '../../../../../../app/utils/client/lib/SDKClient'; import { CustomScrollbars } from '../../../../../components/CustomScrollbars'; import { usePagination } from '../../../../../components/GenericTable/hooks/usePagination'; import { Page, PageHeader, PageContent } from '../../../../../components/Page'; -import HistoryContent from './HistoryContent'; const OutgoingWebhookHistoryPage = (props: ComponentProps) => { const dispatchToastMessage = useToastMessageDispatch(); diff --git a/apps/meteor/client/views/admin/invites/InviteRow.tsx b/apps/meteor/client/views/admin/invites/InviteRow.tsx index 70bd553c2e7f..147aa239f9c0 100644 --- a/apps/meteor/client/views/admin/invites/InviteRow.tsx +++ b/apps/meteor/client/views/admin/invites/InviteRow.tsx @@ -1,9 +1,10 @@ import type { IInvite } from '@rocket.chat/core-typings'; import { Box, IconButton } from '@rocket.chat/fuselage'; import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; -import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; import type { ReactElement, MouseEvent } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { GenericTableCell, GenericTableRow } from '../../../components/GenericTable'; import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime'; @@ -25,7 +26,7 @@ type InviteRowProps = Omit & { }; const InviteRow = ({ _id, createdAt, expires, uses, maxUses, onRemove }: InviteRowProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const formatDateAndTime = useFormatDateAndTime(); const removeInvite = useEndpoint('DELETE', '/v1/removeInvite/:_id', { _id }); diff --git a/apps/meteor/client/views/admin/invites/InvitesPage.tsx b/apps/meteor/client/views/admin/invites/InvitesPage.tsx index d8b6444ffa8b..670446c196f9 100644 --- a/apps/meteor/client/views/admin/invites/InvitesPage.tsx +++ b/apps/meteor/client/views/admin/invites/InvitesPage.tsx @@ -5,6 +5,7 @@ import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; +import InviteRow from './InviteRow'; import GenericModal from '../../../components/GenericModal'; import GenericNoResults from '../../../components/GenericNoResults'; import { @@ -15,7 +16,6 @@ import { GenericTableLoadingTable, } from '../../../components/GenericTable'; import { Page, PageHeader, PageContent } from '../../../components/Page'; -import InviteRow from './InviteRow'; const InvitesPage = (): ReactElement => { const t = useTranslation(); diff --git a/apps/meteor/client/views/admin/invites/InvitesRoute.tsx b/apps/meteor/client/views/admin/invites/InvitesRoute.tsx index cb05b33757e3..fd04e31ec09f 100644 --- a/apps/meteor/client/views/admin/invites/InvitesRoute.tsx +++ b/apps/meteor/client/views/admin/invites/InvitesRoute.tsx @@ -2,8 +2,8 @@ import { usePermission } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import InvitesPage from './InvitesPage'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; const InvitesRoute = (): ReactElement => { const canCreateInviteLinks = usePermission('create-invite-links'); diff --git a/apps/meteor/client/views/admin/mailer/MailerPage.tsx b/apps/meteor/client/views/admin/mailer/MailerPage.tsx index 19e6424ea9e9..1563f31efdb7 100644 --- a/apps/meteor/client/views/admin/mailer/MailerPage.tsx +++ b/apps/meteor/client/views/admin/mailer/MailerPage.tsx @@ -13,10 +13,11 @@ import { Box, } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; -import { useEndpoint, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; import React from 'react'; import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; import { validateEmail } from '../../../../lib/emailValidator'; import { isJSON } from '../../../../lib/utils/isJSON'; @@ -33,7 +34,7 @@ export type SendEmailFormValue = { const initialData = { fromEmail: '', query: '', dryRun: false, subject: '', emailBody: '' }; const MailerPage = () => { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const { diff --git a/apps/meteor/client/views/admin/mailer/MailerRoute.tsx b/apps/meteor/client/views/admin/mailer/MailerRoute.tsx index ab2609258753..c549fccdbe45 100644 --- a/apps/meteor/client/views/admin/mailer/MailerRoute.tsx +++ b/apps/meteor/client/views/admin/mailer/MailerRoute.tsx @@ -1,8 +1,8 @@ import { usePermission } from '@rocket.chat/ui-contexts'; import React from 'react'; -import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import MailerPage from './MailerPage'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; const MailerRoute = () => { const canAccessMailer = usePermission('access-mailer'); diff --git a/apps/meteor/client/views/admin/moderation/MessageReportInfo.tsx b/apps/meteor/client/views/admin/moderation/MessageReportInfo.tsx index 899f33b42ba5..d5b621e3c4a5 100644 --- a/apps/meteor/client/views/admin/moderation/MessageReportInfo.tsx +++ b/apps/meteor/client/views/admin/moderation/MessageReportInfo.tsx @@ -1,16 +1,17 @@ import { Box, Message } from '@rocket.chat/fuselage'; -import { useEndpoint, useSetting, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useSetting, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import ReportReason from './helpers/ReportReason'; const MessageReportInfo = ({ msgId }: { msgId: string }): JSX.Element => { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const getReportsByMessage = useEndpoint('GET', `/v1/moderation.reports`); - const useRealName = Boolean(useSetting('UI_Use_Real_Name')); + const useRealName = useSetting('UI_Use_Real_Name', false); const { data: reportsByMessage, diff --git a/apps/meteor/client/views/admin/moderation/ModConsoleReportDetails.tsx b/apps/meteor/client/views/admin/moderation/ModConsoleReportDetails.tsx index 0f4460bff63a..bde941c7719b 100644 --- a/apps/meteor/client/views/admin/moderation/ModConsoleReportDetails.tsx +++ b/apps/meteor/client/views/admin/moderation/ModConsoleReportDetails.tsx @@ -3,9 +3,9 @@ import { Tabs, TabsItem, ContextualbarHeader, ContextualbarTitle } from '@rocket import { useTranslation, useRouter, useRouteParameter } from '@rocket.chat/ui-contexts'; import React, { useState } from 'react'; -import { Contextualbar, ContextualbarClose, ContextualbarDialog } from '../../../components/Contextualbar'; import UserMessages from './UserMessages'; import UserReportInfo from './UserReports/UserReportInfo'; +import { Contextualbar, ContextualbarClose, ContextualbarDialog } from '../../../components/Contextualbar'; type ModConsoleReportDetailsProps = { userId: IUser['_id']; diff --git a/apps/meteor/client/views/admin/moderation/ModerationConsolePage.tsx b/apps/meteor/client/views/admin/moderation/ModerationConsolePage.tsx index 56801bfe145d..782aa97f0ffd 100644 --- a/apps/meteor/client/views/admin/moderation/ModerationConsolePage.tsx +++ b/apps/meteor/client/views/admin/moderation/ModerationConsolePage.tsx @@ -2,11 +2,11 @@ import { Tabs, TabsItem } from '@rocket.chat/fuselage'; import { useTranslation, useRouteParameter, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import React, { useCallback } from 'react'; -import { Page, PageHeader, PageContent } from '../../../components/Page'; -import { getPermaLink } from '../../../lib/getPermaLink'; import ModConsoleReportDetails from './ModConsoleReportDetails'; import ModerationConsoleTable from './ModerationConsoleTable'; import ModConsoleUsersTable from './UserReports/ModConsoleUsersTable'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; +import { getPermaLink } from '../../../lib/getPermaLink'; type TabType = 'users' | 'messages'; diff --git a/apps/meteor/client/views/admin/moderation/ModerationConsoleRoute.tsx b/apps/meteor/client/views/admin/moderation/ModerationConsoleRoute.tsx index d821c4cb90fa..77549e5bef09 100644 --- a/apps/meteor/client/views/admin/moderation/ModerationConsoleRoute.tsx +++ b/apps/meteor/client/views/admin/moderation/ModerationConsoleRoute.tsx @@ -1,8 +1,8 @@ import { usePermission, useRouteParameter, useRouter } from '@rocket.chat/ui-contexts'; import React, { useEffect } from 'react'; -import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import ModerationConsolePage from './ModerationConsolePage'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; const MODERATION_VALID_TABS = ['users', 'messages'] as const; diff --git a/apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx b/apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx index 2a26588776aa..050cfb3efd27 100644 --- a/apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx +++ b/apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx @@ -5,6 +5,8 @@ import { useQuery } from '@tanstack/react-query'; import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import ModerationConsoleTableRow from './ModerationConsoleTableRow'; +import ModerationFilter from './helpers/ModerationFilter'; import GenericNoResults from '../../../components/GenericNoResults'; import { GenericTable, @@ -15,8 +17,6 @@ import { } from '../../../components/GenericTable'; import { usePagination } from '../../../components/GenericTable/hooks/usePagination'; import { useSort } from '../../../components/GenericTable/hooks/useSort'; -import ModerationConsoleTableRow from './ModerationConsoleTableRow'; -import ModerationFilter from './helpers/ModerationFilter'; // TODO: Missing error state const ModerationConsoleTable = () => { @@ -100,7 +100,7 @@ const ModerationConsoleTable = () => { return ( <> - + {isLoading && ( {headers} diff --git a/apps/meteor/client/views/admin/moderation/ModerationConsoleTableRow.tsx b/apps/meteor/client/views/admin/moderation/ModerationConsoleTableRow.tsx index 65bf7069e074..bb661577f9d4 100644 --- a/apps/meteor/client/views/admin/moderation/ModerationConsoleTableRow.tsx +++ b/apps/meteor/client/views/admin/moderation/ModerationConsoleTableRow.tsx @@ -1,10 +1,10 @@ import type { IModerationAudit, IUser } from '@rocket.chat/core-typings'; import React from 'react'; -import { GenericTableCell, GenericTableRow } from '../../../components/GenericTable'; -import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime'; import ModerationConsoleActions from './ModerationConsoleActions'; import UserColumn from './helpers/UserColumn'; +import { GenericTableCell, GenericTableRow } from '../../../components/GenericTable'; +import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime'; export type ModerationConsoleRowProps = { report: IModerationAudit; diff --git a/apps/meteor/client/views/admin/moderation/UserMessages.tsx b/apps/meteor/client/views/admin/moderation/UserMessages.tsx index 3b3efa68ec5e..c7a44ed11226 100644 --- a/apps/meteor/client/views/admin/moderation/UserMessages.tsx +++ b/apps/meteor/client/views/admin/moderation/UserMessages.tsx @@ -1,16 +1,17 @@ import { Box, Callout, Message, StatesAction, StatesActions, StatesIcon, StatesTitle } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useEndpoint, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import React, { Fragment } from 'react'; +import { useTranslation } from 'react-i18next'; -import { ContextualbarFooter } from '../../../components/Contextualbar'; -import GenericNoResults from '../../../components/GenericNoResults'; import MessageContextFooter from './MessageContextFooter'; import ContextMessage from './helpers/ContextMessage'; +import { ContextualbarFooter } from '../../../components/Contextualbar'; +import GenericNoResults from '../../../components/GenericNoResults'; const UserMessages = ({ userId, onRedirect }: { userId: string; onRedirect: (mid: string) => void }) => { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const getUserMessages = useEndpoint('GET', '/v1/moderation.user.reportedMessages'); diff --git a/apps/meteor/client/views/admin/moderation/UserReports/ModConsoleUserActions.tsx b/apps/meteor/client/views/admin/moderation/UserReports/ModConsoleUserActions.tsx index 21128ff72fd3..b9f413e4edcc 100644 --- a/apps/meteor/client/views/admin/moderation/UserReports/ModConsoleUserActions.tsx +++ b/apps/meteor/client/views/admin/moderation/UserReports/ModConsoleUserActions.tsx @@ -2,10 +2,10 @@ import { GenericMenu } from '@rocket.chat/ui-client'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import type { ModConsoleUserRowProps } from './ModConsoleUserTableRow'; import useDeactivateUserAction from '../hooks/useDeactivateUserAction'; import useDismissUserAction from '../hooks/useDismissUserAction'; import useResetAvatarAction from '../hooks/useResetAvatarAction'; -import type { ModConsoleUserRowProps } from './ModConsoleUserTableRow'; const ModConsoleUserActions = ({ report, onClick }: Omit) => { const { t } = useTranslation(); diff --git a/apps/meteor/client/views/admin/moderation/UserReports/ModConsoleUserTableRow.tsx b/apps/meteor/client/views/admin/moderation/UserReports/ModConsoleUserTableRow.tsx index b37c5330d7e0..4a8e502ba19e 100644 --- a/apps/meteor/client/views/admin/moderation/UserReports/ModConsoleUserTableRow.tsx +++ b/apps/meteor/client/views/admin/moderation/UserReports/ModConsoleUserTableRow.tsx @@ -1,10 +1,10 @@ import type { IUser, UserReport, Serialized } from '@rocket.chat/core-typings'; import React from 'react'; +import ModConsoleUserActions from './ModConsoleUserActions'; import { GenericTableCell, GenericTableRow } from '../../../../components/GenericTable'; import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime'; import UserColumn from '../helpers/UserColumn'; -import ModConsoleUserActions from './ModConsoleUserActions'; export type ModConsoleUserRowProps = { report: Serialized & { count: number }>; diff --git a/apps/meteor/client/views/admin/moderation/UserReports/ModConsoleUsersTable.tsx b/apps/meteor/client/views/admin/moderation/UserReports/ModConsoleUsersTable.tsx index 2f8511181a30..82f9c8b7bbc6 100644 --- a/apps/meteor/client/views/admin/moderation/UserReports/ModConsoleUsersTable.tsx +++ b/apps/meteor/client/views/admin/moderation/UserReports/ModConsoleUsersTable.tsx @@ -2,9 +2,10 @@ import { Pagination, States, StatesAction, StatesActions, StatesIcon, StatesTitl import { useDebouncedValue, useMediaQuery, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useEndpoint, useRouter } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import ModConsoleUserTableRow from './ModConsoleUserTableRow'; import GenericNoResults from '../../../../components/GenericNoResults'; import { GenericTable, @@ -16,7 +17,6 @@ import { import { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; import { useSort } from '../../../../components/GenericTable/hooks/useSort'; import ModerationFilter from '../helpers/ModerationFilter'; -import ModConsoleUserTableRow from './ModConsoleUserTableRow'; const ModConsoleUsersTable = () => { const [text, setText] = useState(''); @@ -35,16 +35,20 @@ const ModConsoleUsersTable = () => { }); const { start, end } = dateRange; - const debouncedText = useDebouncedValue(text, 500); - - const query = { - selector: debouncedText, - sort: JSON.stringify({ [sortBy]: sortDirection === 'asc' ? 1 : -1 }), - count: itemsPerPage, - offset: current, - latest: end ? `${new Date(end).toISOString().slice(0, 10)}T23:59:59.999Z` : undefined, - oldest: start ? `${new Date(start).toISOString().slice(0, 10)}T00:00:00.000Z` : undefined, - }; + const query = useDebouncedValue( + useMemo( + () => ({ + selector: text, + sort: JSON.stringify({ [sortBy]: sortDirection === 'asc' ? 1 : -1 }), + count: itemsPerPage, + offset: current, + latest: end ? `${new Date(end).toISOString().slice(0, 10)}T23:59:59.999Z` : undefined, + oldest: start ? `${new Date(start).toISOString().slice(0, 10)}T00:00:00.000Z` : undefined, + }), + [current, end, itemsPerPage, sortBy, sortDirection, start, text], + ), + 500, + ); const getReports = useEndpoint('GET', '/v1/moderation.userReports'); @@ -98,8 +102,7 @@ const ModConsoleUsersTable = () => { return ( <> - - + {isLoading && ( {headers} diff --git a/apps/meteor/client/views/admin/moderation/UserReports/UserReportInfo.tsx b/apps/meteor/client/views/admin/moderation/UserReports/UserReportInfo.tsx index 7b410f5e6705..625daed50359 100644 --- a/apps/meteor/client/views/admin/moderation/UserReports/UserReportInfo.tsx +++ b/apps/meteor/client/views/admin/moderation/UserReports/UserReportInfo.tsx @@ -12,20 +12,21 @@ import { FieldRow, ContextualbarSkeleton, } from '@rocket.chat/fuselage'; -import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import UserContextFooter from './UserContextFooter'; import { ContextualbarScrollableContent } from '../../../../components/Contextualbar'; import GenericNoResults from '../../../../components/GenericNoResults'; import { UserCardRole } from '../../../../components/UserCard'; import { useFormatDate } from '../../../../hooks/useFormatDate'; import ReportReason from '../helpers/ReportReason'; import UserColumn from '../helpers/UserColumn'; -import UserContextFooter from './UserContextFooter'; const UserReportInfo = ({ userId }: { userId: string }) => { - const t = useTranslation(); + const { t } = useTranslation(); const getUserReports = useEndpoint('GET', '/v1/moderation.user.reportsByUserId'); const formatDateAndTime = useFormatDate(); diff --git a/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx b/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx index afca74f7528c..3d478755ca54 100644 --- a/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx +++ b/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx @@ -2,9 +2,11 @@ import type { IMessage, MessageReport, MessageAttachment } from '@rocket.chat/co import { isE2EEMessage, isQuoteAttachment } from '@rocket.chat/core-typings'; import { Message, MessageName, MessageToolbarItem, MessageToolbarWrapper, MessageUsername } from '@rocket.chat/fuselage'; import { UserAvatar } from '@rocket.chat/ui-avatar'; -import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetting } from '@rocket.chat/ui-contexts'; import React from 'react'; +import { useTranslation } from 'react-i18next'; +import ReportReasonCollapsible from './ReportReasonCollapsible'; import MessageContentBody from '../../../../components/message/MessageContentBody'; import Attachments from '../../../../components/message/content/Attachments'; import UiKitMessageBlock from '../../../../components/message/uikit/UiKitMessageBlock'; @@ -15,7 +17,6 @@ import { useUserDisplayName } from '../../../../hooks/useUserDisplayName'; import MessageReportInfo from '../MessageReportInfo'; import useDeleteMessage from '../hooks/useDeleteMessage'; import { useDismissMessageAction } from '../hooks/useDismissMessageAction'; -import ReportReasonCollapsible from './ReportReasonCollapsible'; const ContextMessage = ({ message, @@ -30,7 +31,7 @@ const ContextMessage = ({ onRedirect: (id: IMessage['_id']) => void; onChange: () => void; }): JSX.Element => { - const t = useTranslation(); + const { t } = useTranslation(); const isEncryptedMessage = isE2EEMessage(message); @@ -40,7 +41,7 @@ const ContextMessage = ({ const formatDateAndTime = useFormatDateAndTime(); const formatTime = useFormatTime(); const formatDate = useFormatDate(); - const useRealName = Boolean(useSetting('UI_Use_Real_Name')); + const useRealName = useSetting('UI_Use_Real_Name', false); const name = message.u.name || ''; const username = message.u.username || ''; diff --git a/apps/meteor/client/views/admin/moderation/helpers/ModerationFilter.tsx b/apps/meteor/client/views/admin/moderation/helpers/ModerationFilter.tsx index aaad5d0c4ebd..035ab6c16c13 100644 --- a/apps/meteor/client/views/admin/moderation/helpers/ModerationFilter.tsx +++ b/apps/meteor/client/views/admin/moderation/helpers/ModerationFilter.tsx @@ -1,19 +1,17 @@ import React from 'react'; -import { useTranslation } from 'react-i18next'; -import FilterByText from '../../../../components/FilterByText'; import DateRangePicker from './DateRangePicker'; +import FilterByText from '../../../../components/FilterByText'; type ModerationFilterProps = { + text: string; setText: (text: string) => void; setDateRange: (dateRange: { start: string; end: string }) => void; }; -const ModerationFilter = ({ setText, setDateRange }: ModerationFilterProps) => { - const { t } = useTranslation(); - +const ModerationFilter = ({ text, setText, setDateRange }: ModerationFilterProps) => { return ( - + setText(event.target.value)}> ); diff --git a/apps/meteor/client/views/admin/moderation/hooks/useDeleteMessage.tsx b/apps/meteor/client/views/admin/moderation/hooks/useDeleteMessage.tsx index 21d7c3b86c73..40bdabd33ac7 100644 --- a/apps/meteor/client/views/admin/moderation/hooks/useDeleteMessage.tsx +++ b/apps/meteor/client/views/admin/moderation/hooks/useDeleteMessage.tsx @@ -1,11 +1,12 @@ -import { useEndpoint, useSetModal, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import GenericModal from '../../../../components/GenericModal'; const useDeleteMessage = (mid: string, rid: string, onChange: () => void) => { - const t = useTranslation(); + const { t } = useTranslation(); const deleteMessage = useEndpoint('POST', '/v1/chat.delete'); const dismissMessage = useEndpoint('POST', '/v1/moderation.dismissReports'); const dispatchToastMessage = useToastMessageDispatch(); diff --git a/apps/meteor/client/views/admin/moderation/hooks/useDeleteMessagesAction.tsx b/apps/meteor/client/views/admin/moderation/hooks/useDeleteMessagesAction.tsx index 3fb22dfd0c5e..6107c946e6a1 100644 --- a/apps/meteor/client/views/admin/moderation/hooks/useDeleteMessagesAction.tsx +++ b/apps/meteor/client/views/admin/moderation/hooks/useDeleteMessagesAction.tsx @@ -1,12 +1,13 @@ import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; -import { useEndpoint, useRouteParameter, useRouter, useSetModal, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useRouteParameter, useRouter, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import GenericModal from '../../../../components/GenericModal'; const useDeleteMessagesAction = (userId: string): GenericMenuItemProps => { - const t = useTranslation(); + const { t } = useTranslation(); const deleteMessages = useEndpoint('POST', '/v1/moderation.user.deleteReportedMessages'); const dispatchToastMessage = useToastMessageDispatch(); const setModal = useSetModal(); diff --git a/apps/meteor/client/views/admin/moderation/hooks/useDismissMessageAction.tsx b/apps/meteor/client/views/admin/moderation/hooks/useDismissMessageAction.tsx index 6a7f6867ea74..2bde4ef30b00 100644 --- a/apps/meteor/client/views/admin/moderation/hooks/useDismissMessageAction.tsx +++ b/apps/meteor/client/views/admin/moderation/hooks/useDismissMessageAction.tsx @@ -1,11 +1,12 @@ -import { useEndpoint, useSetModal, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import GenericModal from '../../../../components/GenericModal'; export const useDismissMessageAction = (msgId: string): { action: () => void } => { - const t = useTranslation(); + const { t } = useTranslation(); const setModal = useSetModal(); const dispatchToastMessage = useToastMessageDispatch(); const queryClient = useQueryClient(); diff --git a/apps/meteor/client/views/admin/moderation/hooks/useResetAvatarAction.tsx b/apps/meteor/client/views/admin/moderation/hooks/useResetAvatarAction.tsx index 8e488234814b..7096d3f507e3 100644 --- a/apps/meteor/client/views/admin/moderation/hooks/useResetAvatarAction.tsx +++ b/apps/meteor/client/views/admin/moderation/hooks/useResetAvatarAction.tsx @@ -1,12 +1,13 @@ import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; -import { useEndpoint, useSetModal, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import GenericModal from '../../../../components/GenericModal'; const useResetAvatarAction = (userId: string): GenericMenuItemProps => { - const t = useTranslation(); + const { t } = useTranslation(); const setModal = useSetModal(); const dispatchToastMessage = useToastMessageDispatch(); const queryClient = useQueryClient(); diff --git a/apps/meteor/client/views/admin/oauthApps/EditOauthAppWithData.tsx b/apps/meteor/client/views/admin/oauthApps/EditOauthAppWithData.tsx index f87926429e67..6803d88c0021 100644 --- a/apps/meteor/client/views/admin/oauthApps/EditOauthAppWithData.tsx +++ b/apps/meteor/client/views/admin/oauthApps/EditOauthAppWithData.tsx @@ -1,14 +1,15 @@ import { Box } from '@rocket.chat/fuselage'; -import { useEndpoint, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; -import { FormSkeleton } from '../../../components/Skeleton'; import EditOauthApp from './EditOauthApp'; +import { FormSkeleton } from '../../../components/Skeleton'; const EditOauthAppWithData = ({ _id, ...props }: { _id: string }): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const getOauthApps = useEndpoint('GET', '/v1/oauth-apps.get'); diff --git a/apps/meteor/client/views/admin/oauthApps/OAuthAddApp.tsx b/apps/meteor/client/views/admin/oauthApps/OAuthAddApp.tsx index 0982a99ccd92..d173e248bf50 100644 --- a/apps/meteor/client/views/admin/oauthApps/OAuthAddApp.tsx +++ b/apps/meteor/client/views/admin/oauthApps/OAuthAddApp.tsx @@ -11,11 +11,12 @@ import { ToggleSwitch, FieldGroup, } from '@rocket.chat/fuselage'; -import { useToastMessageDispatch, useRoute, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useRoute, useEndpoint } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; import { ContextualbarScrollableContent } from '../../../components/Contextualbar'; @@ -26,7 +27,7 @@ type OAuthAddAppPayload = { }; const OAuthAddApp = (): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const { diff --git a/apps/meteor/client/views/admin/oauthApps/OAuthAppsPage.tsx b/apps/meteor/client/views/admin/oauthApps/OAuthAppsPage.tsx index cc9e174948e5..ae7d00ae4347 100644 --- a/apps/meteor/client/views/admin/oauthApps/OAuthAppsPage.tsx +++ b/apps/meteor/client/views/admin/oauthApps/OAuthAppsPage.tsx @@ -3,10 +3,10 @@ import { useRouteParameter, useTranslation, useRouter } from '@rocket.chat/ui-co import type { ReactElement } from 'react'; import React from 'react'; -import { Page, PageHeader, PageContent } from '../../../components/Page'; import EditOauthAppWithData from './EditOauthAppWithData'; import OAuthAddApp from './OAuthAddApp'; import OAuthAppsTable from './OAuthAppsTable'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; const OAuthAppsPage = (): ReactElement => { const t = useTranslation(); diff --git a/apps/meteor/client/views/admin/oauthApps/OAuthAppsRoute.tsx b/apps/meteor/client/views/admin/oauthApps/OAuthAppsRoute.tsx index fba79bf8664f..c9c419ba6808 100644 --- a/apps/meteor/client/views/admin/oauthApps/OAuthAppsRoute.tsx +++ b/apps/meteor/client/views/admin/oauthApps/OAuthAppsRoute.tsx @@ -2,8 +2,8 @@ import { usePermission } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import OAuthAppsPage from './OAuthAppsPage'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; const OAuthAppsRoute = (): ReactElement => { const canAccessOAuthApps = usePermission('manage-oauth-apps'); diff --git a/apps/meteor/client/views/admin/permissions/EditRolePage.tsx b/apps/meteor/client/views/admin/permissions/EditRolePage.tsx index 8980d64d17e8..c37ed2f2ee57 100644 --- a/apps/meteor/client/views/admin/permissions/EditRolePage.tsx +++ b/apps/meteor/client/views/admin/permissions/EditRolePage.tsx @@ -1,17 +1,18 @@ import type { IRole } from '@rocket.chat/core-typings'; import { Box, ButtonGroup, Button, Margins } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, useRoute, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetModal, useToastMessageDispatch, useRoute, useEndpoint } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; import { FormProvider, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import RoleForm from './RoleForm'; import { ContextualbarFooter, ContextualbarScrollableContent } from '../../../components/Contextualbar'; import GenericModal from '../../../components/GenericModal'; -import RoleForm from './RoleForm'; const EditRolePage = ({ role, isEnterprise }: { role?: IRole; isEnterprise: boolean }): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const setModal = useSetModal(); const usersInRoleRouter = useRoute('admin-permissions'); diff --git a/apps/meteor/client/views/admin/permissions/EditRolePageWithData.tsx b/apps/meteor/client/views/admin/permissions/EditRolePageWithData.tsx index d08eab83a934..7f96aa148990 100644 --- a/apps/meteor/client/views/admin/permissions/EditRolePageWithData.tsx +++ b/apps/meteor/client/views/admin/permissions/EditRolePageWithData.tsx @@ -1,16 +1,17 @@ import type { IRole } from '@rocket.chat/core-typings'; import { Callout } from '@rocket.chat/fuselage'; -import { useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts'; +import { useRouteParameter } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; -import PageSkeleton from '../../../components/PageSkeleton'; -import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; import EditRolePage from './EditRolePage'; import { useRole } from './hooks/useRole'; +import PageSkeleton from '../../../components/PageSkeleton'; +import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; const EditRolePageWithData = ({ roleId }: { roleId?: IRole['_id'] }): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const role = useRole(roleId); const context = useRouteParameter('context'); const hasCustomRolesModule = useHasLicenseModule('custom-roles'); diff --git a/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx b/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx index 3b8bea6097e4..a386fbda7fda 100644 --- a/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx +++ b/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx @@ -3,6 +3,8 @@ import { useRouteParameter, useRoute, useTranslation, useSetModal } from '@rocke import type { ReactElement } from 'react'; import React, { useEffect } from 'react'; +import CustomRoleUpsellModal from './CustomRoleUpsellModal'; +import EditRolePageWithData from './EditRolePageWithData'; import { Contextualbar, ContextualbarHeader, @@ -11,8 +13,6 @@ import { ContextualbarDialog, } from '../../../components/Contextualbar'; import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; -import CustomRoleUpsellModal from './CustomRoleUpsellModal'; -import EditRolePageWithData from './EditRolePageWithData'; const PermissionsContextBar = (): ReactElement | null => { const t = useTranslation(); diff --git a/apps/meteor/client/views/admin/permissions/PermissionsRouter.tsx b/apps/meteor/client/views/admin/permissions/PermissionsRouter.tsx index 31ab597df913..ddaff27eff56 100644 --- a/apps/meteor/client/views/admin/permissions/PermissionsRouter.tsx +++ b/apps/meteor/client/views/admin/permissions/PermissionsRouter.tsx @@ -2,11 +2,11 @@ import { useRouteParameter, usePermission } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; +import PermissionsTable from './PermissionsTable'; +import UsersInRole from './UsersInRole'; import PageSkeleton from '../../../components/PageSkeleton'; import { useIsEnterprise } from '../../../hooks/useIsEnterprise'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; -import PermissionsTable from './PermissionsTable'; -import UsersInRole from './UsersInRole'; const PermissionsRouter = (): ReactElement => { const canViewPermission = usePermission('access-permissions'); diff --git a/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionRow.tsx b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionRow.tsx index 7ddd6ca9ab45..72d9d2bf4134 100644 --- a/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionRow.tsx +++ b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionRow.tsx @@ -6,10 +6,10 @@ import type { ReactElement } from 'react'; import React, { useState, memo } from 'react'; import { useTranslation } from 'react-i18next'; +import RoleCell from './RoleCell'; import { CONSTANTS } from '../../../../../app/authorization/lib'; import { GenericTableRow, GenericTableCell } from '../../../../components/GenericTable'; import { useChangeRole } from '../hooks/useChangeRole'; -import RoleCell from './RoleCell'; const getName = (t: TFunction, permission: IPermission): string => { if (permission.level === CONSTANTS.SETTINGS_LEVEL) { diff --git a/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTable.tsx b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTable.tsx index 583f9d237252..b70867e86e8d 100644 --- a/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTable.tsx +++ b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTable.tsx @@ -5,6 +5,9 @@ import { useRoute, usePermission, useMethod, useTranslation, useSetModal } from import type { ReactElement } from 'react'; import React, { useState } from 'react'; +import PermissionRow from './PermissionRow'; +import PermissionsTableFilter from './PermissionsTableFilter'; +import RoleHeader from './RoleHeader'; import GenericNoResults from '../../../../components/GenericNoResults'; import { GenericTable, GenericTableHeader, GenericTableHeaderCell, GenericTableBody } from '../../../../components/GenericTable'; import { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; @@ -12,9 +15,6 @@ import { Page, PageHeader, PageContent } from '../../../../components/Page'; import CustomRoleUpsellModal from '../CustomRoleUpsellModal'; import PermissionsContextBar from '../PermissionsContextBar'; import { usePermissionsAndRoles } from '../hooks/usePermissionsAndRoles'; -import PermissionRow from './PermissionRow'; -import PermissionsTableFilter from './PermissionsTableFilter'; -import RoleHeader from './RoleHeader'; const PermissionsTable = ({ isEnterprise }: { isEnterprise: boolean }): ReactElement => { const t = useTranslation(); diff --git a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePage.tsx b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePage.tsx index 3e3c6988e6cf..5aa29e46f14d 100644 --- a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePage.tsx +++ b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePage.tsx @@ -7,10 +7,10 @@ import type { ReactElement } from 'react'; import React from 'react'; import { useForm, Controller } from 'react-hook-form'; +import UsersInRoleTable from './UsersInRoleTable'; import { Page, PageHeader, PageContent } from '../../../../components/Page'; import RoomAutoComplete from '../../../../components/RoomAutoComplete'; import UserAutoCompleteMultiple from '../../../../components/UserAutoCompleteMultiple'; -import UsersInRoleTable from './UsersInRoleTable'; type UsersInRolePayload = { rid?: IRoom['_id']; diff --git a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePageWithData.tsx b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePageWithData.tsx index 8639c2407a6e..4cf421ac19a1 100644 --- a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePageWithData.tsx +++ b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePageWithData.tsx @@ -2,8 +2,8 @@ import { useRouteParameter } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import { useRole } from '../hooks/useRole'; import UsersInRolePage from './UsersInRolePage'; +import { useRole } from '../hooks/useRole'; const UsersInRolePageWithData = (): ReactElement | null => { const _id = useRouteParameter('_id'); diff --git a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTable.tsx b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTable.tsx index ed7fdd93d79c..e5a815747f1a 100644 --- a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTable.tsx +++ b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTable.tsx @@ -1,11 +1,13 @@ import type { IRole, IRoom } from '@rocket.chat/core-typings'; import { Pagination } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetModal, useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import UsersInRoleTableRow from './UsersInRoleTableRow'; import GenericError from '../../../../../components/GenericError'; import GenericModal from '../../../../../components/GenericModal'; import GenericNoResults from '../../../../../components/GenericNoResults'; @@ -17,7 +19,6 @@ import { GenericTableLoadingTable, } from '../../../../../components/GenericTable'; import { usePagination } from '../../../../../components/GenericTable/hooks/usePagination'; -import UsersInRoleTableRow from './UsersInRoleTableRow'; type UsersInRoleTableProps = { roleName: IRole['name']; @@ -27,7 +28,7 @@ type UsersInRoleTableProps = { }; const UsersInRoleTable = ({ rid, roleId, roleName, description }: UsersInRoleTableProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const setModal = useSetModal(); const dispatchToastMessage = useToastMessageDispatch(); const queryClient = useQueryClient(); @@ -73,7 +74,7 @@ const UsersInRoleTable = ({ rid, roleId, roleName, description }: UsersInRoleTab setModal( setModal(null)} confirmText={t('Delete')}> - {t('The_user_s_will_be_removed_from_role_s', username, description || roleName)} + {t('The_user_s_will_be_removed_from_role_s', { postProcess: 'sprintf', sprintf: [username, description || roleName] })} , ); }); @@ -101,9 +102,7 @@ const UsersInRoleTable = ({ rid, roleId, roleName, description }: UsersInRoleTab {headers} - {users?.map((user) => ( - - ))} + {users?.map((user) => )} - ChatPermissions.find(getFilter(), { + Permissions.find(getFilter(), { sort: { _id: 1, }, @@ -34,7 +34,7 @@ export const usePermissionsAndRoles = ( }), [limit, skip, getFilter], ); - const getTotalPermissions = useCallback(() => ChatPermissions.find(getFilter()).count(), [getFilter]); + const getTotalPermissions = useCallback(() => Permissions.find(getFilter()).count(), [getFilter]); const permissions = useReactiveValue(getPermissions); const permissionsTotal = useReactiveValue(getTotalPermissions); diff --git a/apps/meteor/client/views/admin/rooms/EditRoom.tsx b/apps/meteor/client/views/admin/rooms/EditRoom.tsx index fa40f74cfadb..ff28f2460be4 100644 --- a/apps/meteor/client/views/admin/rooms/EditRoom.tsx +++ b/apps/meteor/client/views/admin/rooms/EditRoom.tsx @@ -14,17 +14,18 @@ import { FieldError, } from '@rocket.chat/fuselage'; import { useEffectEvent, useUniqueId } from '@rocket.chat/fuselage-hooks'; -import { useEndpoint, useRouter, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useRouter, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import React from 'react'; import { useForm, Controller } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { useEditAdminRoomPermissions } from './useEditAdminRoomPermissions'; import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../components/Contextualbar'; import RoomAvatarEditor from '../../../components/avatar/RoomAvatarEditor'; import { getDirtyFields } from '../../../lib/getDirtyFields'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; import { useArchiveRoom } from '../../hooks/roomActions/useArchiveRoom'; import { useDeleteRoom } from '../../hooks/roomActions/useDeleteRoom'; -import { useEditAdminRoomPermissions } from './useEditAdminRoomPermissions'; type EditRoomProps = { room: IRoom; @@ -63,7 +64,7 @@ const getInitialValues = (room: Pick): EditRoomFormV }); const EditRoom = ({ room, onChange, onDelete }: EditRoomProps) => { - const t = useTranslation(); + const { t } = useTranslation(); const router = useRouter(); const dispatchToastMessage = useToastMessageDispatch(); diff --git a/apps/meteor/client/views/admin/rooms/EditRoomWithData.tsx b/apps/meteor/client/views/admin/rooms/EditRoomWithData.tsx index 54245d3d55a9..9d3eb234c3aa 100644 --- a/apps/meteor/client/views/admin/rooms/EditRoomWithData.tsx +++ b/apps/meteor/client/views/admin/rooms/EditRoomWithData.tsx @@ -1,8 +1,10 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { useEndpoint, useRouter, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useRouter, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import React from 'react'; +import { useTranslation } from 'react-i18next'; +import EditRoom from './EditRoom'; import { Contextualbar, ContextualbarHeader, @@ -10,12 +12,11 @@ import { ContextualbarClose, ContextualbarSkeleton, } from '../../../components/Contextualbar'; -import EditRoom from './EditRoom'; type EditRoomWithDataProps = { rid?: IRoom['_id']; onReload: () => void }; const EditRoomWithData = ({ rid, onReload }: EditRoomWithDataProps) => { - const t = useTranslation(); + const { t } = useTranslation(); const router = useRouter(); const dispatchToastMessage = useToastMessageDispatch(); diff --git a/apps/meteor/client/views/admin/rooms/RoomRow.tsx b/apps/meteor/client/views/admin/rooms/RoomRow.tsx index 05b1079bfbde..16614380f592 100644 --- a/apps/meteor/client/views/admin/rooms/RoomRow.tsx +++ b/apps/meteor/client/views/admin/rooms/RoomRow.tsx @@ -3,8 +3,9 @@ import type { IRoom, RoomAdminFieldsType, Serialized } from '@rocket.chat/core-t import { Box, Icon } from '@rocket.chat/fuselage'; import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; import { RoomAvatar } from '@rocket.chat/ui-avatar'; -import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; +import { useRouter } from '@rocket.chat/ui-contexts'; import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { GenericTableCell, GenericTableRow } from '../../../components/GenericTable'; import { useFormatDate } from '../../../hooks/useFormatDate'; @@ -21,7 +22,7 @@ const getRoomDisplayName = (room: Pick, RoomAdminFieldsType>): room.t === 'd' ? room.usernames?.join(' x ') : roomCoordinator.getRoomName(room.t, room as IRoom); const RoomRow = ({ room }: { room: Pick, RoomAdminFieldsType> }) => { - const t = useTranslation(); + const { t } = useTranslation(); const mediaQuery = useMediaQuery('(min-width: 1024px)'); const router = useRouter(); const formatDate = useFormatDate(); diff --git a/apps/meteor/client/views/admin/rooms/RoomsPage.tsx b/apps/meteor/client/views/admin/rooms/RoomsPage.tsx index 0c8425726297..35ef4b651b5f 100644 --- a/apps/meteor/client/views/admin/rooms/RoomsPage.tsx +++ b/apps/meteor/client/views/admin/rooms/RoomsPage.tsx @@ -1,14 +1,15 @@ -import { useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts'; +import { useRouteParameter } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useRef } from 'react'; +import { useTranslation } from 'react-i18next'; -import { ContextualbarDialog } from '../../../components/Contextualbar'; -import { Page, PageHeader, PageContent } from '../../../components/Page'; import EditRoomWithData from './EditRoomWithData'; import RoomsTable from './RoomsTable'; +import { ContextualbarDialog } from '../../../components/Contextualbar'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; const RoomsPage = (): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const id = useRouteParameter('id'); const context = useRouteParameter('context'); diff --git a/apps/meteor/client/views/admin/rooms/RoomsRoute.tsx b/apps/meteor/client/views/admin/rooms/RoomsRoute.tsx index bda14931282d..9140d816fd7e 100644 --- a/apps/meteor/client/views/admin/rooms/RoomsRoute.tsx +++ b/apps/meteor/client/views/admin/rooms/RoomsRoute.tsx @@ -2,8 +2,8 @@ import { usePermission } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import RoomsPage from './RoomsPage'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; const RoomsRoute = (): ReactElement => { const canViewRoomAdministration = usePermission('view-room-administration'); diff --git a/apps/meteor/client/views/admin/rooms/RoomsTable.tsx b/apps/meteor/client/views/admin/rooms/RoomsTable.tsx index b4906b6970d1..8279c454f613 100644 --- a/apps/meteor/client/views/admin/rooms/RoomsTable.tsx +++ b/apps/meteor/client/views/admin/rooms/RoomsTable.tsx @@ -1,11 +1,14 @@ import { Pagination, States, StatesIcon, StatesTitle, StatesActions, StatesAction } from '@rocket.chat/fuselage'; import { useMediaQuery, useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import type { OptionProp } from '@rocket.chat/ui-client'; -import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement, MutableRefObject } from 'react'; import React, { useRef, useState, useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import RoomRow from './RoomRow'; +import RoomsTableFilters from './RoomsTableFilters'; import GenericNoResults from '../../../components/GenericNoResults'; import { GenericTable, @@ -16,8 +19,6 @@ import { } from '../../../components/GenericTable'; import { usePagination } from '../../../components/GenericTable/hooks/usePagination'; import { useSort } from '../../../components/GenericTable/hooks/useSort'; -import RoomRow from './RoomRow'; -import RoomsTableFilters from './RoomsTableFilters'; type RoomFilters = { searchText: string; @@ -27,7 +28,7 @@ type RoomFilters = { const DEFAULT_TYPES = ['d', 'p', 'c', 'l', 'discussions', 'teams']; const RoomsTable = ({ reload }: { reload: MutableRefObject<() => void> }): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const mediaQuery = useMediaQuery('(min-width: 1024px)'); const [roomFilters, setRoomFilters] = useState({ searchText: '', types: [] }); @@ -133,11 +134,7 @@ const RoomsTable = ({ reload }: { reload: MutableRefObject<() => void> }): React <> {headers} - - {data.rooms?.map((room) => ( - - ))} - + {data.rooms?.map((room) => )} settingsCollection.find(query).count() > 0); }; - return createReactiveSubscriptionFactory((_id: SettingId): EditableSetting | undefined => { + return createReactiveSubscriptionFactory((_id: ISetting['_id']): EditableSetting | undefined => { const settingsCollection = getSettingsCollection(); const editableSetting = settingsCollection.findOne(_id); @@ -94,7 +93,7 @@ const EditableSettingsProvider = ({ children, query = defaultQuery, omit = defau ? { section: query.section } : { $or: [{ section: { $exists: false } }, { section: '' }], - })), + })), }, { ...('tab' in query && @@ -102,7 +101,7 @@ const EditableSettingsProvider = ({ children, query = defaultQuery, omit = defau ? { tab: query.tab } : { $or: [{ tab: { $exists: false } }, { tab: '' }], - })), + })), }, ], }, @@ -121,7 +120,7 @@ const EditableSettingsProvider = ({ children, query = defaultQuery, omit = defau const queryGroupSections = useMemo( () => - createReactiveSubscriptionFactory((_id: GroupId, tab?: TabId) => + createReactiveSubscriptionFactory((_id: ISetting['_id'], tab?: ISetting['_id']) => Array.from( new Set( getSettingsCollection() @@ -132,7 +131,7 @@ const EditableSettingsProvider = ({ children, query = defaultQuery, omit = defau ? { tab } : { $or: [{ tab: { $exists: false } }, { tab: '' }], - }), + }), }, { fields: { @@ -155,7 +154,7 @@ const EditableSettingsProvider = ({ children, query = defaultQuery, omit = defau const queryGroupTabs = useMemo( () => - createReactiveSubscriptionFactory((_id: GroupId) => + createReactiveSubscriptionFactory((_id: ISetting['_id']) => Array.from( new Set( getSettingsCollection() @@ -193,10 +192,6 @@ const EditableSettingsProvider = ({ children, query = defaultQuery, omit = defau Tracker.flush(); }); - const { data } = useIsEnterprise(); - - const isEnterprise = data?.isEnterprise ?? false; - const contextValue = useMemo( () => ({ queryEditableSetting, @@ -204,9 +199,8 @@ const EditableSettingsProvider = ({ children, query = defaultQuery, omit = defau queryGroupSections, queryGroupTabs, dispatch, - isEnterprise, }), - [queryEditableSetting, queryEditableSettings, queryGroupSections, queryGroupTabs, dispatch, isEnterprise], + [queryEditableSetting, queryEditableSettings, queryGroupSections, queryGroupTabs, dispatch], ); return ; diff --git a/apps/meteor/client/views/admin/settings/Setting/Setting.stories.tsx b/apps/meteor/client/views/admin/settings/Setting/Setting.stories.tsx index 4e2534356b22..7705c223491a 100644 --- a/apps/meteor/client/views/admin/settings/Setting/Setting.stories.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/Setting.stories.tsx @@ -17,7 +17,7 @@ export default { }, decorators: [ (fn) => ( -
+
{fn()}
), diff --git a/apps/meteor/client/views/admin/settings/Setting/Setting.tsx b/apps/meteor/client/views/admin/settings/Setting/Setting.tsx index 19ed42927cfe..0fe1d6716690 100644 --- a/apps/meteor/client/views/admin/settings/Setting/Setting.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/Setting.tsx @@ -2,13 +2,15 @@ import type { ISettingColor, SettingEditor, SettingValue } from '@rocket.chat/co import { isSettingColor, isSetting } from '@rocket.chat/core-typings'; import { Box, Button, Tag } from '@rocket.chat/fuselage'; import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks'; -import { useSettingStructure, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSettingStructure } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useEffect, useMemo, useState, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; -import MarkdownText from '../../../../components/MarkdownText'; -import { useEditableSetting, useEditableSettingsDispatch, useIsEnterprise } from '../../EditableSettingsContext'; import MemoizedSetting from './MemoizedSetting'; +import MarkdownText from '../../../../components/MarkdownText'; +import { useEditableSetting, useEditableSettingsDispatch } from '../../EditableSettingsContext'; +import { useHasSettingModule } from '../hooks/useHasSettingModule'; type SettingProps = { className?: string; @@ -19,7 +21,7 @@ type SettingProps = { function Setting({ className = undefined, settingId, sectionChanged }: SettingProps): ReactElement { const setting = useEditableSetting(settingId); const persistedSetting = useSettingStructure(settingId); - const isEnterprise = useIsEnterprise(); + const hasSettingModule = useHasSettingModule(setting); if (!setting || !persistedSetting) { throw new Error(`Setting ${settingId} not found`); @@ -53,7 +55,7 @@ function Setting({ className = undefined, settingId, sectionChanged }: SettingPr [persistedSetting, dispatch], ); - const t = useTranslation(); + const { t, i18n } = useTranslation(); const [value, setValue] = useState(setting.value); const [editor, setEditor] = useState(isSettingColor(setting) ? setting.editor : undefined); @@ -95,16 +97,22 @@ function Setting({ className = undefined, settingId, sectionChanged }: SettingPr const { _id, disabled, readonly, type, packageValue, i18nLabel, i18nDescription, alert, invisible } = setting; - const labelText = (t.has(i18nLabel) && t(i18nLabel)) || (t.has(_id) && t(_id)) || i18nLabel || _id; + const labelText = (i18n.exists(i18nLabel) && t(i18nLabel)) || (i18n.exists(_id) && t(_id)) || i18nLabel || _id; const hint = useMemo( () => - i18nDescription && t.has(i18nDescription) ? : undefined, - [i18nDescription, t], + i18nDescription && i18n.exists(i18nDescription) ? ( + + ) : undefined, + [i18n, i18nDescription, t], + ); + + const callout = useMemo( + () => alert && , + [alert, i18n, t], ); - const callout = useMemo(() => alert && , [alert, t]); - const shouldDisableEnterprise = setting.enterprise && !isEnterprise; + const shouldDisableEnterprise = setting.enterprise && !hasSettingModule; const PRICING_URL = 'https://go.rocket.chat/i/see-paid-plan-customize-homepage'; diff --git a/apps/meteor/client/views/admin/settings/Setting/inputs/ActionSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/ActionSettingInput.tsx index 2f94048e3406..60cea596388a 100644 --- a/apps/meteor/client/views/admin/settings/Setting/inputs/ActionSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/ActionSettingInput.tsx @@ -1,9 +1,10 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Button, FieldRow, FieldHint } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useMethod, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useMethod, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; type ActionSettingInputProps = { _id: string; @@ -13,7 +14,7 @@ type ActionSettingInputProps = { sectionChanged: boolean; }; function ActionSettingInput({ _id, actionText, value, disabled, sectionChanged }: ActionSettingInputProps): ReactElement { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const actionMethod = useMethod(value); @@ -23,7 +24,7 @@ function ActionSettingInput({ _id, actionText, value, disabled, sectionChanged } const data: { message: TranslationKey; params?: string[] } = await actionMethod(); const params = data.params || []; - dispatchToastMessage({ type: 'success', message: t(data.message, ...params) }); + dispatchToastMessage({ type: 'success', message: t(data.message, { postProcess: 'sprintf', sprintf: params }) }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } diff --git a/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.stories.tsx index 3cc7d2252756..231d3d1a42b3 100644 --- a/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.stories.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.stories.tsx @@ -9,7 +9,7 @@ export default { component: AssetSettingInput, decorators: [ (fn) => ( -
+
{fn()}
diff --git a/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.styles.css b/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.styles.css index 6bbaf26e1687..7a8cd64f19f2 100644 --- a/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.styles.css +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.styles.css @@ -1,13 +1,12 @@ -.AssetSettingInput__input { - position: absolute !important; - z-index: 10000; - top: 0; - left: 0; - - width: 100%; - height: 100%; - - cursor: pointer; - - opacity: 0; +.asset-setting-input { + &__input { + position: absolute !important; + z-index: 10000; + top: 0; + left: 0; + width: 100%; + height: 100%; + cursor: pointer; + opacity: 0; + } } diff --git a/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.tsx index 14ba6f27b4c5..39604cd1146e 100644 --- a/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.tsx @@ -82,7 +82,7 @@ function AssetSettingInput({ _id, label, value, asset, required, disabled, fileC {t('Select_file')} ( -
+
{fn()}
), diff --git a/apps/meteor/client/views/admin/settings/Setting/inputs/TimespanSettingInput.spec.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/TimespanSettingInput.spec.tsx index e16bbd18a6aa..508b55c06242 100644 --- a/apps/meteor/client/views/admin/settings/Setting/inputs/TimespanSettingInput.spec.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/TimespanSettingInput.spec.tsx @@ -3,8 +3,8 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { TIMEUNIT } from '../../../../../lib/convertTimeUnit'; import { default as TimespanSettingInput, getHighestTimeUnit } from './TimespanSettingInput'; +import { TIMEUNIT } from '../../../../../lib/convertTimeUnit'; global.ResizeObserver = jest.fn().mockImplementation(() => ({ observe: jest.fn(), diff --git a/apps/meteor/client/views/admin/settings/SettingsGroupCard.tsx b/apps/meteor/client/views/admin/settings/SettingsGroupCard.tsx index 52e5fae49ed7..c795411ebb14 100644 --- a/apps/meteor/client/views/admin/settings/SettingsGroupCard.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsGroupCard.tsx @@ -3,9 +3,10 @@ import { css } from '@rocket.chat/css-in-js'; import { Button, Box, Card, CardTitle, CardBody, CardControls } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; +import { useRouter } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import MarkdownText from '../../../components/MarkdownText'; @@ -23,7 +24,7 @@ type SettingsGroupCardProps = { }; const SettingsGroupCard = ({ id, title, description, ...props }: SettingsGroupCardProps): ReactElement => { - const t = useTranslation(); + const { t, i18n } = useTranslation(); const router = useRouter(); const cardId = useUniqueId(); const descriptionId = useUniqueId(); @@ -33,7 +34,7 @@ const SettingsGroupCard = ({ id, title, description, ...props }: SettingsGroupCa {t(title)} - {description && t.has(description) && } + {description && i18n.exists(description) && } diff --git a/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPage.tsx b/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPage.tsx index 884c9f6e67e9..662dd2d52c9f 100644 --- a/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPage.tsx @@ -2,9 +2,10 @@ import type { ISetting, ISettingColor } from '@rocket.chat/core-typings'; import { Accordion, Box, Button, ButtonGroup } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useToastMessageDispatch, useSettingsDispatch, useSettings, useTranslation } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useSettingsDispatch, useSettings } from '@rocket.chat/ui-contexts'; import type { ReactNode, FormEvent, MouseEvent } from 'react'; import React, { useMemo, memo } from 'react'; +import { useTranslation } from 'react-i18next'; import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../../components/Page'; import type { EditableSetting } from '../../EditableSettingsContext'; @@ -31,7 +32,7 @@ const SettingsGroupPage = ({ tabs = undefined, isCustom = false, }: SettingsGroupPageProps) => { - const t = useTranslation(); + const { t, i18n } = useTranslation(); const dispatch = useSettingsDispatch(); const dispatchToastMessage = useToastMessageDispatch(); @@ -145,7 +146,7 @@ const SettingsGroupPage = ({ ) : ( - {i18nDescription && isTranslationKey(i18nDescription) && t.has(i18nDescription) && ( + {i18nDescription && isTranslationKey(i18nDescription) && i18n.exists(i18nDescription) && ( {t(i18nDescription)} diff --git a/apps/meteor/client/views/admin/settings/SettingsGroupSelector/SettingsGroupSelector.tsx b/apps/meteor/client/views/admin/settings/SettingsGroupSelector/SettingsGroupSelector.tsx index 5cce1e961bf5..316ddf01a6c7 100644 --- a/apps/meteor/client/views/admin/settings/SettingsGroupSelector/SettingsGroupSelector.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsGroupSelector/SettingsGroupSelector.tsx @@ -1,4 +1,4 @@ -import type { GroupId } from '@rocket.chat/core-typings'; +import type { ISetting } from '@rocket.chat/core-typings'; import { useSettingStructure } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -9,7 +9,7 @@ import OAuthGroupPage from '../groups/OAuthGroupPage'; import VoipGroupPage from '../groups/VoipGroupPage'; type SettingsGroupSelectorProps = { - groupId: GroupId; + groupId: ISetting['_id']; onClickBack?: () => void; }; diff --git a/apps/meteor/client/views/admin/settings/SettingsPage.tsx b/apps/meteor/client/views/admin/settings/SettingsPage.tsx index f82e16ce637c..c079185e7e92 100644 --- a/apps/meteor/client/views/admin/settings/SettingsPage.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsPage.tsx @@ -1,18 +1,19 @@ import { Icon, SearchInput, Skeleton, CardGrid } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useIsSettingsContextLoading, useTranslation } from '@rocket.chat/ui-contexts'; +import { useIsSettingsContextLoading } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import SettingsGroupCard from './SettingsGroupCard'; +import { useSettingsGroups } from './hooks/useSettingsGroups'; import GenericNoResults from '../../../components/GenericNoResults'; import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import PageBlockWithBorder from '../../../components/Page/PageBlockWithBorder'; -import SettingsGroupCard from './SettingsGroupCard'; -import { useSettingsGroups } from './hooks/useSettingsGroups'; const SettingsPage = (): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const [filter, setFilter] = useState(''); const handleChange = useCallback((e) => setFilter(e.currentTarget.value), []); diff --git a/apps/meteor/client/views/admin/settings/SettingsRoute.tsx b/apps/meteor/client/views/admin/settings/SettingsRoute.tsx index 0db9d336a02a..590276f65ba1 100644 --- a/apps/meteor/client/views/admin/settings/SettingsRoute.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsRoute.tsx @@ -2,10 +2,10 @@ import { useRouteParameter, useIsPrivilegedSettingsContext, useRouter } from '@r import type { ReactElement } from 'react'; import React from 'react'; -import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import EditableSettingsProvider from './EditableSettingsProvider'; import SettingsGroupSelector from './SettingsGroupSelector'; import SettingsPage from './SettingsPage'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; const omittedSettings = ['Cloud_Workspace_AirGapped_Restrictions_Remaining_Days']; diff --git a/apps/meteor/client/views/admin/settings/groups/BaseGroupPage.tsx b/apps/meteor/client/views/admin/settings/groups/BaseGroupPage.tsx index 2b2a35b00a63..652759da7ffb 100644 --- a/apps/meteor/client/views/admin/settings/groups/BaseGroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/groups/BaseGroupPage.tsx @@ -1,9 +1,9 @@ import type { ReactElement } from 'react'; import React from 'react'; -import { useEditableSettingsGroupSections, useEditableSettingsGroupTabs } from '../../EditableSettingsContext'; import GenericGroupPage from './GenericGroupPage'; import TabbedGroupPage from './TabbedGroupPage'; +import { useEditableSettingsGroupSections, useEditableSettingsGroupTabs } from '../../EditableSettingsContext'; type BaseGroupPageProps = { _id: string; diff --git a/apps/meteor/client/views/admin/settings/groups/LDAPGroupPage.tsx b/apps/meteor/client/views/admin/settings/groups/LDAPGroupPage.tsx index ae8fb0dabf2b..03ab25a2c2f9 100644 --- a/apps/meteor/client/views/admin/settings/groups/LDAPGroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/groups/LDAPGroupPage.tsx @@ -1,21 +1,22 @@ import type { ISetting } from '@rocket.chat/core-typings'; import { Button, Box, TextInput, Field, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, useSetting, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetModal, useToastMessageDispatch, useSetting, useEndpoint } from '@rocket.chat/ui-contexts'; import type { FormEvent } from 'react'; import React, { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import BaseGroupPage from './BaseGroupPage'; import GenericModal from '../../../../components/GenericModal'; import { useExternalLink } from '../../../../hooks/useExternalLink'; import { useEditableSettings } from '../../EditableSettingsContext'; -import BaseGroupPage from './BaseGroupPage'; type LDAPGroupPageProps = ISetting & { onClickBack?: () => void; }; function LDAPGroupPage({ _id, i18nLabel, onClickBack, ...group }: LDAPGroupPageProps) { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const testConnection = useEndpoint('POST', '/v1/ldap.testConnection'); const syncNow = useEndpoint('POST', '/v1/ldap.syncNow'); diff --git a/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/CreateOAuthModal.spec.tsx b/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/CreateOAuthModal.spec.tsx new file mode 100644 index 000000000000..e11b9dfdbd3f --- /dev/null +++ b/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/CreateOAuthModal.spec.tsx @@ -0,0 +1,51 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import CreateOAuthModal from './CreateOAuthModal'; + +it('should call onClose when Cancel is clicked', async () => { + const onConfirm = jest.fn(); + const onClose = jest.fn(); + + render(, { + wrapper: mockAppRoot().build(), + legacyRoot: true, + }); + + await userEvent.click(screen.getByText('Cancel')); + + expect(onClose).toHaveBeenCalled(); +}); + +it('should call onClose when X is clicked', async () => { + const onConfirm = jest.fn(); + const onClose = jest.fn(); + + render(, { + wrapper: mockAppRoot().build(), + legacyRoot: true, + }); + + await userEvent.click(screen.getByLabelText('Close')); + + expect(onClose).toHaveBeenCalled(); +}); + +it('should call onConfirm when Add button is clicked', async () => { + const onConfirm = jest.fn(); + const onClose = jest.fn(); + + render(, { + wrapper: mockAppRoot().build(), + legacyRoot: true, + }); + + const custoOAuthNameInput = screen.getByLabelText('Custom_OAuth_name'); + await userEvent.type(custoOAuthNameInput, 'Test'); + + await userEvent.click(screen.getByText('Add')); + + await waitFor(() => expect(onConfirm).toHaveBeenCalled()); +}); diff --git a/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/CreateOAuthModal.tsx b/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/CreateOAuthModal.tsx index 98357b149383..f3cd2a5dfe32 100644 --- a/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/CreateOAuthModal.tsx +++ b/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/CreateOAuthModal.tsx @@ -1,6 +1,8 @@ -import { TextInput, Field, FieldLabel, FieldRow, FieldError, Box } from '@rocket.chat/fuselage'; -import type { ReactElement, FormEvent, SyntheticEvent } from 'react'; -import React, { useState } from 'react'; +import { TextInput, Field, FieldLabel, FieldRow, FieldError, Box, FieldHint } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import type { ReactElement } from 'react'; +import React from 'react'; +import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import GenericModal from '../../../../../components/GenericModal'; @@ -10,43 +12,50 @@ type CreateOAuthModalProps = { onClose: () => void; }; +type CreateOAuthModalFields = { + customOAuthName: string; +}; + const CreateOAuthModal = ({ onConfirm, onClose }: CreateOAuthModalProps): ReactElement => { - const [text, setText] = useState(''); - const [error, setError] = useState(''); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: { + customOAuthName: '', + }, + }); + const { t } = useTranslation(); - const handleConfirm = (e: SyntheticEvent): void => { - e.preventDefault(); - if (!text.length) { - setError(t('Required_field', { field: t('Name') })); - return; - } - onConfirm(text); - }; + const customOAuthNameId = useUniqueId(); return ( } + wrapperFunction={(props) => onConfirm(customOAuthName))} {...props} />} title={t('Add_custom_oauth')} confirmText={t('Add')} onCancel={onClose} onClose={onClose} - onConfirm={handleConfirm} > - {t('Give_a_unique_name_for_the_custom_oauth')} + {t('Custom_OAuth_name')} ): void => { - setText(e.currentTarget.value); - setError(''); - }} + id={customOAuthNameId} + {...register('customOAuthName', { required: t('Required_field', { field: t('Custom_OAuth_name') }) })} + aria-required='true' + aria-describedby={`${customOAuthNameId}-error ${customOAuthNameId}-hint`} + aria-label={t('Custom_OAuth_name')} /> - {error && {error}} + {t('Custom_OAuth_name_hint')} + {errors.customOAuthName && ( + + {errors.customOAuthName.message} + + )} ); diff --git a/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/OAuthGroupPage.tsx b/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/OAuthGroupPage.tsx index a858555e46d3..ce5c54d36d7e 100644 --- a/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/OAuthGroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/OAuthGroupPage.tsx @@ -5,12 +5,12 @@ import { useToastMessageDispatch, useAbsoluteUrl, useMethod, useTranslation, use import type { ReactElement } from 'react'; import React, { memo, useEffect, useState } from 'react'; +import CreateOAuthModal from './CreateOAuthModal'; import { strRight } from '../../../../../../lib/utils/stringUtils'; import GenericModal from '../../../../../components/GenericModal'; import { useEditableSettingsGroupSections } from '../../../EditableSettingsContext'; import SettingsGroupPage from '../../SettingsGroupPage'; import SettingsSection from '../../SettingsSection'; -import CreateOAuthModal from './CreateOAuthModal'; type OAuthGroupPageProps = ISetting & { onClickBack?: () => void; diff --git a/apps/meteor/client/views/admin/settings/groups/TabbedGroupPage.tsx b/apps/meteor/client/views/admin/settings/groups/TabbedGroupPage.tsx index 1a3c122502cf..a4917c158816 100644 --- a/apps/meteor/client/views/admin/settings/groups/TabbedGroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/groups/TabbedGroupPage.tsx @@ -4,8 +4,8 @@ import type { ReactElement } from 'react'; import React, { memo, useState, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useEditableSettingsGroupSections } from '../../EditableSettingsContext'; import GenericGroupPage from './GenericGroupPage'; +import { useEditableSettingsGroupSections } from '../../EditableSettingsContext'; type TabbedGroupPageProps = { headerButtons?: ReactElement; diff --git a/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/AssignAgentButton.tsx b/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/AssignAgentButton.tsx index 8f0727260bf8..fe357b31cdb0 100644 --- a/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/AssignAgentButton.tsx +++ b/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/AssignAgentButton.tsx @@ -1,15 +1,16 @@ import { IconButton } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetModal } from '@rocket.chat/ui-contexts'; import React from 'react'; +import { useTranslation } from 'react-i18next'; -import { GenericTableCell } from '../../../../../components/GenericTable'; import AssignAgentModal from './AssignAgentModal'; +import { GenericTableCell } from '../../../../../components/GenericTable'; type AssignAgentButtonProps = { extension: string; reload: () => void }; const AssignAgentButton = ({ extension, reload }: AssignAgentButtonProps) => { - const t = useTranslation(); + const { t } = useTranslation(); const setModal = useSetModal(); const handleAssociation = useMutableCallback((e) => { diff --git a/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/AssignAgentModal.tsx b/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/AssignAgentModal.tsx index e6b0ddbb0c84..518ac64b35d0 100644 --- a/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/AssignAgentModal.tsx +++ b/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/AssignAgentModal.tsx @@ -1,7 +1,8 @@ import { Button, Modal, Select, Field, FieldGroup, FieldLabel, FieldRow, Box } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useToastMessageDispatch, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts'; import React, { useState, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import AutoCompleteAgentWithoutExtension from '../../../../../components/AutoCompleteAgentWithoutExtension'; import { AsyncStatePhase } from '../../../../../hooks/useAsyncState'; @@ -14,7 +15,7 @@ type AssignAgentModalProps = { }; const AssignAgentModal = ({ existingExtension, closeModal, reload }: AssignAgentModalProps) => { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const [agent, setAgent] = useState(''); const [extension, setExtension] = useState(existingExtension || ''); @@ -51,7 +52,7 @@ const AssignAgentModal = ({ existingExtension, closeModal, reload }: AssignAgent - {t('Free_Extension_Numbers')} + {t('Available_extensions')} diff --git a/apps/meteor/client/views/oauth/components/CurrentUserDisplay.tsx b/apps/meteor/client/views/oauth/components/CurrentUserDisplay.tsx index 6e52570418de..59e1584d5ae2 100644 --- a/apps/meteor/client/views/oauth/components/CurrentUserDisplay.tsx +++ b/apps/meteor/client/views/oauth/components/CurrentUserDisplay.tsx @@ -22,7 +22,7 @@ type CurrentUserDisplayProps = { }; const CurrentUserDisplay = ({ user }: CurrentUserDisplayProps) => { - const showRealNames = useSetting('UI_Use_Real_Name'); + const showRealNames = useSetting('UI_Use_Real_Name', false); const getRoles = useRolesDescription(); const { t } = useTranslation(); diff --git a/apps/meteor/client/views/oauth/components/ErrorPage.tsx b/apps/meteor/client/views/oauth/components/ErrorPage.tsx index 812a5badf114..9c0f0a14e915 100644 --- a/apps/meteor/client/views/oauth/components/ErrorPage.tsx +++ b/apps/meteor/client/views/oauth/components/ErrorPage.tsx @@ -2,8 +2,8 @@ import { States, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fu import React from 'react'; import { useTranslation } from 'react-i18next'; -import { getErrorMessage } from '../../../lib/errorHandling'; import Layout from './Layout'; +import { getErrorMessage } from '../../../lib/errorHandling'; type ErrorPageProps = { error: unknown; diff --git a/apps/meteor/client/views/oauth/components/Layout.tsx b/apps/meteor/client/views/oauth/components/Layout.tsx index 5f7857aed675..dce1a583b416 100644 --- a/apps/meteor/client/views/oauth/components/Layout.tsx +++ b/apps/meteor/client/views/oauth/components/Layout.tsx @@ -9,7 +9,7 @@ type LayoutProps = { }; const Layout = ({ children }: LayoutProps) => { - const hideLogo = useSetting('Layout_Login_Hide_Logo'); + const hideLogo = useSetting('Layout_Login_Hide_Logo', false); const customLogo = useAssetWithDarkModePath('logo'); const customBackground = useAssetWithDarkModePath('background'); diff --git a/apps/meteor/client/views/omnichannel/ExternalFrameContainer.tsx b/apps/meteor/client/views/omnichannel/ExternalFrameContainer.tsx index 3fe37eaced50..f59cf8c0c20c 100644 --- a/apps/meteor/client/views/omnichannel/ExternalFrameContainer.tsx +++ b/apps/meteor/client/views/omnichannel/ExternalFrameContainer.tsx @@ -10,8 +10,8 @@ function ExternalFrameContainer() { const uid = useUserId(); const room = useRoom(); const { 'X-Auth-Token': authToken } = sdk.rest.getCredentials() || {}; - const keyStr = useSetting('Omnichannel_External_Frame_Encryption_JWK'); - const frameURLSetting = useSetting('Omnichannel_External_Frame_URL'); + const keyStr = useSetting('Omnichannel_External_Frame_Encryption_JWK', ''); + const frameURLSetting = useSetting('Omnichannel_External_Frame_URL', ''); const token = useQuery(['externalFrame', keyStr, authToken], async () => { if (!keyStr || !authToken) { @@ -39,7 +39,7 @@ function ExternalFrameContainer() { return (
-