diff --git a/.changeset/breezy-bugs-jam.md b/.changeset/breezy-bugs-jam.md new file mode 100644 index 000000000000..7e7cc7b8283b --- /dev/null +++ b/.changeset/breezy-bugs-jam.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: Managers allowed to make deactivated agent's available diff --git a/.changeset/four-parents-cheer.md b/.changeset/four-parents-cheer.md new file mode 100644 index 000000000000..2fbb8e2b279f --- /dev/null +++ b/.changeset/four-parents-cheer.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +load sounds right before playing them diff --git a/.changeset/gold-moose-press.md b/.changeset/gold-moose-press.md new file mode 100644 index 000000000000..605fb7c649ea --- /dev/null +++ b/.changeset/gold-moose-press.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fix moment timestamps language change diff --git a/.changeset/honest-mirrors-sit.md b/.changeset/honest-mirrors-sit.md new file mode 100644 index 000000000000..4e4298cb8110 --- /dev/null +++ b/.changeset/honest-mirrors-sit.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Disabled call to tags enterprise endpoint when on community license diff --git a/.changeset/importer-progress-bar.md b/.changeset/importer-progress-bar.md new file mode 100644 index 000000000000..49c04289ddcb --- /dev/null +++ b/.changeset/importer-progress-bar.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed Importer Progress Bar progress indicator + diff --git a/.changeset/lucky-balloons-divide.md b/.changeset/lucky-balloons-divide.md new file mode 100644 index 000000000000..beb4cbfe3b57 --- /dev/null +++ b/.changeset/lucky-balloons-divide.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fix engagement dashboard not showing data diff --git a/.changeset/nine-bottles-press.md b/.changeset/nine-bottles-press.md new file mode 100644 index 000000000000..f9a57fa676ad --- /dev/null +++ b/.changeset/nine-bottles-press.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +feat: Add flag to disable teams mention via troubleshoot page diff --git a/.changeset/seven-jobs-tickle.md b/.changeset/seven-jobs-tickle.md new file mode 100644 index 000000000000..870bafbb7d9d --- /dev/null +++ b/.changeset/seven-jobs-tickle.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +--- + +fix: agent role being removed upon user deactivation diff --git a/.changeset/slimy-cheetahs-heal.md b/.changeset/slimy-cheetahs-heal.md new file mode 100644 index 000000000000..44233bc87766 --- /dev/null +++ b/.changeset/slimy-cheetahs-heal.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed selected departments not being displayed due to pagination diff --git a/.changeset/strong-laws-pump.md b/.changeset/strong-laws-pump.md new file mode 100644 index 000000000000..a4afefd65316 --- /dev/null +++ b/.changeset/strong-laws-pump.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/model-typings': patch +'@rocket.chat/meteor': patch +--- + +Change SAU aggregation to consider only sessions from few days ago instead of the whole past. + +This is particularly important for large workspaces in case the cron job did not run for some time, in that case the amount of sessions would accumulate and the aggregation would take a long time to run. diff --git a/.changeset/three-birds-tickle.md b/.changeset/three-birds-tickle.md new file mode 100644 index 000000000000..0ce911d9f6fa --- /dev/null +++ b/.changeset/three-birds-tickle.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +chore: Increase cache time from 5s to 10s on `getUnits` helpers. This should reduce the number of DB calls made by this method to fetch the unit limitations for a user. diff --git a/.changeset/tough-candles-heal.md b/.changeset/tough-candles-heal.md new file mode 100644 index 000000000000..59ad9c1fb3a1 --- /dev/null +++ b/.changeset/tough-candles-heal.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-typings": patch +"@rocket.chat/model-typings": patch +--- + +Fixes a problem where the calculated time for considering the visitor abandonment was the first message from the visitor and not the visitor's reply to the agent. diff --git a/.changeset/wise-walls-tan.md b/.changeset/wise-walls-tan.md new file mode 100644 index 000000000000..f558de82ec4c --- /dev/null +++ b/.changeset/wise-walls-tan.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +fix: missing params on updateOwnBasicInfo endpoint diff --git a/.changeset/yellow-buttons-agree.md b/.changeset/yellow-buttons-agree.md new file mode 100644 index 000000000000..a86d172a4544 --- /dev/null +++ b/.changeset/yellow-buttons-agree.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/ui-client': minor +'@rocket.chat/meteor': minor +--- + +feat: add ChangePassword field to Account/Security diff --git a/.changeset/young-trains-glow.md b/.changeset/young-trains-glow.md new file mode 100644 index 000000000000..77f50812143f --- /dev/null +++ b/.changeset/young-trains-glow.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Fixed the issue of apps icon uneven alignment in case of missing icons inside message composer toolbar & message toolbar menu. diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml new file mode 100644 index 000000000000..808b8acdcbe3 --- /dev/null +++ b/.github/actions/build-docker/action.yml @@ -0,0 +1,73 @@ +name: 'Meteor Docker' + +inputs: + CR_USER: + required: true + CR_PAT: + required: true + node-version: + required: true + description: 'Node version' + type: string + platform: + required: false + description: 'Platform' + type: string + +runs: + using: composite + + steps: + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ inputs.CR_USER }} + password: ${{ inputs.CR_PAT }} + + - name: Restore build + uses: actions/download-artifact@v3 + with: + name: build + path: /tmp/build + + - name: Unpack build + shell: bash + run: | + cd /tmp/build + tar xzf Rocket.Chat.tar.gz + rm Rocket.Chat.tar.gz + + - uses: dtinth/setup-github-actions-caching-for-turbo@v1 + + - name: Setup NodeJS + uses: ./.github/actions/setup-node + with: + node-version: ${{ inputs.node-version }} + cache-modules: true + install: true + + - run: yarn build + shell: bash + + - name: Build Docker images + shell: bash + run: | + args=(rocketchat) + + if [[ '${{ inputs.platform }}' = 'alpine' ]]; then + args+=($SERVICES_PUBLISH) + fi; + + docker compose -f docker-compose-ci.yml build "${args[@]}" + + - name: Publish Docker images to GitHub Container Registry + shell: bash + run: | + args=(rocketchat) + + if [[ '${{ inputs.platform }}' = 'alpine' ]]; then + args+=($SERVICES_PUBLISH) + fi; + + docker compose -f docker-compose-ci.yml push "${args[@]}" diff --git a/.github/actions/meteor-build/action.yml b/.github/actions/meteor-build/action.yml new file mode 100644 index 000000000000..21fec059c8de --- /dev/null +++ b/.github/actions/meteor-build/action.yml @@ -0,0 +1,129 @@ +name: 'Meteor Build' + +inputs: + coverage: + required: false + description: 'Enable coverage' + type: boolean + reset-meteor: + required: false + description: 'Reset Meteor' + type: boolean + node-version: + required: true + description: 'Node version' + type: string + +runs: + using: composite + + steps: + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 4 + + - name: Setup NodeJS + uses: ./.github/actions/setup-node + with: + node-version: ${{ inputs.node-version }} + cache-modules: true + install: true + + # - name: Free disk space + # run: | + # sudo apt clean + # docker rmi $(docker image ls -aq) + # df -h + + - name: Cache vite + uses: actions/cache@v3 + with: + path: ./node_modules/.vite + key: vite-local-cache-${{ runner.OS }}-${{ hashFiles('package.json') }} + restore-keys: | + vite-local-cache-${{ runner.os }}- + + - name: Cache meteor local + uses: actions/cache@v3 + with: + path: ./apps/meteor/.meteor/local + key: meteor-local-cache-${{ runner.OS }}-${{ hashFiles('apps/meteor/.meteor/versions') }} + restore-keys: | + meteor-local-cache-${{ runner.os }}- + + - name: Cache meteor + uses: actions/cache@v3 + with: + path: ~/.meteor + key: meteor-cache-${{ runner.OS }}-${{ hashFiles('apps/meteor/.meteor/release') }} + restore-keys: | + meteor-cache-${{ runner.os }}- + + - name: Install Meteor + shell: bash + run: | + # Restore bin from cache + set +e + METEOR_SYMLINK_TARGET=$(readlink ~/.meteor/meteor) + METEOR_TOOL_DIRECTORY=$(dirname "$METEOR_SYMLINK_TARGET") + set -e + LAUNCHER=$HOME/.meteor/$METEOR_TOOL_DIRECTORY/scripts/admin/launch-meteor + if [ -e $LAUNCHER ] + then + echo "Cached Meteor bin found, restoring it" + sudo cp "$LAUNCHER" "/usr/local/bin/meteor" + else + echo "No cached Meteor bin found." + fi + + # only install meteor if bin isn't found + command -v meteor >/dev/null 2>&1 || curl https://install.meteor.com | sed s/--progress-bar/-sL/g | /bin/sh + + - name: Versions + shell: bash + run: | + npm --versions + yarn -v + node -v + meteor --version + meteor npm --versions + meteor node -v + git version + + - uses: dtinth/setup-github-actions-caching-for-turbo@v1 + + - name: Translation check + shell: bash + run: yarn turbo run translation-check + + - name: Reset Meteor + shell: bash + if: ${{ inputs.reset-meteor == 'true' }} + working-directory: ./apps/meteor + run: meteor reset + + - name: Build Rocket.Chat From Pull Request + shell: bash + if: startsWith(github.ref, 'refs/pull/') == true + env: + METEOR_PROFILE: 1000 + BABEL_ENV: ${{ inputs.coverage == 'true' && 'coverage' || '' }} + run: yarn build:ci -- --directory /tmp/dist + + - name: Build Rocket.Chat + shell: bash + if: startsWith(github.ref, 'refs/pull/') != true + run: yarn build:ci -- --directory /tmp/dist + + - name: Prepare build + shell: bash + run: | + cd /tmp/dist + tar czf /tmp/Rocket.Chat.tar.gz bundle + + - name: Store build + uses: actions/upload-artifact@v3 + with: + name: build + path: /tmp/Rocket.Chat.tar.gz diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 9a05543605db..e14857a97a09 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -57,6 +57,8 @@ on: required: false REPORTER_ROCKETCHAT_API_KEY: required: false + CODECOV_TOKEN: + required: false env: MONGO_URL: mongodb://localhost:27017/rocketchat?replicaSet=rs0&directConnection=true @@ -237,6 +239,7 @@ jobs: directory: ./apps/meteor flags: e2e verbose: true + token: ${{ secrets.CODECOV_TOKEN }} - name: Store e2e-ee-coverage if: inputs.type == 'ui' && inputs.release == 'ee' diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml index 03c6bc2352ab..b4ef5cb273ad 100644 --- a/.github/workflows/ci-test-unit.yml +++ b/.github/workflows/ci-test-unit.yml @@ -6,6 +6,9 @@ on: node-version: required: true type: string + secrets: + CODECOV_TOKEN: + required: false env: MONGO_URL: mongodb://localhost:27017/rocketchat?replicaSet=rs0&directConnection=true @@ -36,3 +39,4 @@ jobs: with: flags: unit verbose: true + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3fe46ecf66c3..22250705ea58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,7 +123,7 @@ jobs: run: yarn build build: - name: 📦 Meteor Build + name: 📦 Meteor Build - coverage needs: [release-versions, packages-build] runs-on: ubuntu-20.04 @@ -138,111 +138,38 @@ jobs: echo "github.event_name: ${{ github.event_name }}" cat $GITHUB_EVENT_PATH - - name: Set Swap Space - uses: pierotofy/set-swap-space@master - with: - swap-size-gb: 4 - - uses: actions/checkout@v3 - - name: Setup NodeJS - uses: ./.github/actions/setup-node + - uses: ./.github/actions/meteor-build with: node-version: ${{ needs.release-versions.outputs.node-version }} - cache-modules: true - install: true - - # - name: Free disk space - # run: | - # sudo apt clean - # docker rmi $(docker image ls -aq) - # df -h - - - name: Cache vite - uses: actions/cache@v3 - with: - path: ./node_modules/.vite - key: vite-local-cache-${{ runner.OS }}-${{ hashFiles('package.json') }} - restore-keys: | - vite-local-cache-${{ runner.os }}- - - - name: Cache meteor local - uses: actions/cache@v3 - with: - path: ./apps/meteor/.meteor/local - key: meteor-local-cache-${{ runner.OS }}-${{ hashFiles('apps/meteor/.meteor/versions') }} - restore-keys: | - meteor-local-cache-${{ runner.os }}- - - - name: Cache meteor - uses: actions/cache@v3 - with: - path: ~/.meteor - key: meteor-cache-${{ runner.OS }}-${{ hashFiles('apps/meteor/.meteor/release') }} - restore-keys: | - meteor-cache-${{ runner.os }}- + coverage: true - - name: Install Meteor - run: | - # Restore bin from cache - set +e - METEOR_SYMLINK_TARGET=$(readlink ~/.meteor/meteor) - METEOR_TOOL_DIRECTORY=$(dirname "$METEOR_SYMLINK_TARGET") - set -e - LAUNCHER=$HOME/.meteor/$METEOR_TOOL_DIRECTORY/scripts/admin/launch-meteor - if [ -e $LAUNCHER ] - then - echo "Cached Meteor bin found, restoring it" - sudo cp "$LAUNCHER" "/usr/local/bin/meteor" - else - echo "No cached Meteor bin found." - fi - - # only install meteor if bin isn't found - command -v meteor >/dev/null 2>&1 || curl https://install.meteor.com | sed s/--progress-bar/-sL/g | /bin/sh + build-prod: + name: 📦 Meteor Build - official + needs: [tests-done, release-versions, packages-build] + if: (github.event_name == 'release' || github.ref == 'refs/heads/develop') + runs-on: ubuntu-20.04 - - name: Versions + steps: + - name: Github Info run: | - npm --versions - yarn -v - node -v - meteor --version - meteor npm --versions - meteor node -v - git version - - - uses: dtinth/setup-github-actions-caching-for-turbo@v1 - - - name: Translation check - run: yarn turbo run translation-check - - - name: Reset Meteor - if: startsWith(github.ref, 'refs/tags/') == 'true' || github.ref == 'refs/heads/develop' - working-directory: ./apps/meteor - run: meteor reset - - - name: Build Rocket.Chat From Pull Request - if: startsWith(github.ref, 'refs/pull/') == true - env: - METEOR_PROFILE: 1000 - run: yarn build:ci -- --directory /tmp/dist - - - name: Build Rocket.Chat - if: startsWith(github.ref, 'refs/pull/') != true - run: yarn build:ci -- --directory /tmp/dist + echo "GITHUB_ACTION: $GITHUB_ACTION" + echo "GITHUB_ACTOR: $GITHUB_ACTOR" + echo "GITHUB_REF: $GITHUB_REF" + echo "GITHUB_HEAD_REF: $GITHUB_HEAD_REF" + echo "GITHUB_BASE_REF: $GITHUB_BASE_REF" + echo "github.event_name: ${{ github.event_name }}" + cat $GITHUB_EVENT_PATH - - name: Prepare build - run: | - cd /tmp/dist - tar czf /tmp/Rocket.Chat.tar.gz bundle + - uses: actions/checkout@v3 - - name: Store build - uses: actions/upload-artifact@v3 + - uses: ./.github/actions/meteor-build with: - name: build - path: /tmp/Rocket.Chat.tar.gz + node-version: ${{ needs.release-versions.outputs.node-version }} + coverage: false - build-gh-docker: + build-gh-docker-coverage: name: 🚢 Build Docker Images for Testing needs: [build, release-versions] if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') @@ -262,56 +189,39 @@ jobs: steps: - uses: actions/checkout@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ secrets.CR_USER }} - password: ${{ secrets.CR_PAT }} - - - name: Restore build - uses: actions/download-artifact@v3 - with: - name: build - path: /tmp/build - - - name: Unpack build - run: | - cd /tmp/build - tar xzf Rocket.Chat.tar.gz - rm Rocket.Chat.tar.gz - - - uses: dtinth/setup-github-actions-caching-for-turbo@v1 - - - name: Setup NodeJS - uses: ./.github/actions/setup-node + - uses: ./.github/actions/build-docker with: + CR_USER: ${{ secrets.CR_USER }} + CR_PAT: ${{ secrets.CR_PAT }} node-version: ${{ needs.release-versions.outputs.node-version }} - cache-modules: true - install: true + platform: ${{ matrix.platform }} - - run: yarn build - - - name: Build Docker images - run: | - args=(rocketchat) - - if [[ '${{ matrix.platform }}' = 'alpine' ]]; then - args+=($SERVICES_PUBLISH) - fi; + build-gh-docker: + name: 🚢 Build Docker Images for Production + needs: [build-prod, release-versions] + runs-on: ubuntu-20.04 - docker compose -f docker-compose-ci.yml build "${args[@]}" + env: + RC_DOCKERFILE: ${{ matrix.platform == 'alpine' && needs.release-versions.outputs.rc-dockerfile-alpine || needs.release-versions.outputs.rc-dockerfile }} + RC_DOCKER_TAG: ${{ matrix.platform == 'alpine' && needs.release-versions.outputs.rc-docker-tag-alpine || needs.release-versions.outputs.rc-docker-tag }} + DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} + LOWERCASE_REPOSITORY: ${{ needs.release-versions.outputs.lowercase-repo }} + SERVICES_PUBLISH: 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service' - - name: Publish Docker images to GitHub Container Registry - run: | - args=(rocketchat) + strategy: + fail-fast: false + matrix: + platform: ['official', 'alpine'] - if [[ '${{ matrix.platform }}' = 'alpine' ]]; then - args+=($SERVICES_PUBLISH) - fi; + steps: + - uses: actions/checkout@v3 - docker compose -f docker-compose-ci.yml push "${args[@]}" + - uses: ./.github/actions/build-docker + with: + CR_USER: ${{ secrets.CR_USER }} + CR_PAT: ${{ secrets.CR_PAT }} + node-version: ${{ needs.release-versions.outputs.node-version }} + platform: ${{ matrix.platform }} - name: Rename official Docker tag to GitHub Container Registry if: matrix.platform == 'official' @@ -337,10 +247,12 @@ jobs: uses: ./.github/workflows/ci-test-unit.yml with: node-version: ${{ needs.release-versions.outputs.node-version }} + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} test-api: name: 🔨 Test API (CE) - needs: [checks, build-gh-docker, release-versions] + needs: [checks, build-gh-docker-coverage, release-versions] uses: ./.github/workflows/ci-test-e2e.yml with: @@ -359,7 +271,7 @@ jobs: test-ui: name: 🔨 Test UI (CE) - needs: [checks, build-gh-docker, release-versions] + needs: [checks, build-gh-docker-coverage, release-versions] uses: ./.github/workflows/ci-test-e2e.yml with: @@ -385,7 +297,7 @@ jobs: test-api-ee: name: 🔨 Test API (EE) - needs: [checks, build-gh-docker, release-versions] + needs: [checks, build-gh-docker-coverage, release-versions] uses: ./.github/workflows/ci-test-e2e.yml with: @@ -407,7 +319,7 @@ jobs: test-ui-ee: name: 🔨 Test UI (EE) - needs: [checks, build-gh-docker, release-versions] + needs: [checks, build-gh-docker-coverage, release-versions] uses: ./.github/workflows/ci-test-e2e.yml with: @@ -431,6 +343,7 @@ jobs: QASE_API_TOKEN: ${{ secrets.QASE_API_TOKEN }} REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }} REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} tests-done: name: ✅ Tests Done @@ -446,7 +359,7 @@ jobs: name: 🚀 Publish build and update our registry runs-on: ubuntu-20.04 if: github.event_name == 'release' || github.ref == 'refs/heads/develop' - needs: [tests-done, release-versions] + needs: [build-gh-docker, release-versions] steps: - uses: actions/checkout@v3 diff --git a/apps/meteor/.babelrc b/apps/meteor/.babelrc index a8c20b400ca5..382b93318fab 100644 --- a/apps/meteor/.babelrc +++ b/apps/meteor/.babelrc @@ -1,9 +1,15 @@ { - "presets": [ - "@babel/preset-env", - "@babel/preset-react" - ], - "plugins": [ - "babel-plugin-istanbul" - ] + "presets": ["@babel/preset-env", "@babel/preset-react"], + "env": { + "coverage": { + "plugins": [ + [ + "istanbul", + { + "exclude": ["**/*.spec.js", "**/*.test.js"] + } + ] + ] + } + } } diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 182d30ddabb9..51ff6d4d1826 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,34 @@ # @rocket.chat/meteor +## 6.3.4 + +### Patch Changes + +- db919f9b23: Bump @rocket.chat/meteor version. +- Bump @rocket.chat/meteor version. +- ebeb088441: fix: Prevent `RoomProvider.useEffect` from subscribing to room-data stream multiple times +- 8a7d5d3898: fix: agent role being removed upon user deactivation +- 759fe2472a: chore: Increase cache time from 5s to 10s on `getUnits` helpers. This should reduce the number of DB calls made by this method to fetch the unit limitations for a user. +- Updated dependencies [8a7d5d3898] + - @rocket.chat/model-typings@0.0.10 + - @rocket.chat/omnichannel-services@0.0.10 + - @rocket.chat/models@0.0.10 + - @rocket.chat/presence@0.0.10 + - @rocket.chat/core-services@0.1.4 + - @rocket.chat/cron@0.0.6 + - @rocket.chat/instance-status@0.0.10 + - @rocket.chat/core-typings@6.3.4 + - @rocket.chat/rest-typings@6.3.4 + - @rocket.chat/api-client@0.1.4 + - @rocket.chat/pdf-worker@0.0.10 + - @rocket.chat/gazzodown@1.0.4 + - @rocket.chat/ui-contexts@1.0.4 + - @rocket.chat/fuselage-ui-kit@1.0.4 + - @rocket.chat/ui-theming@0.0.1 + - @rocket.chat/ui-client@1.0.4 + - @rocket.chat/ui-video-conf@1.0.4 + - @rocket.chat/web-ui-registration@1.0.4 + ## 6.3.3 ### Patch Changes diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index a4e3f974ac65..b23d41255c3b 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -126,7 +126,9 @@ API.v1.addRoute( realname: this.bodyParams.data.name, username: this.bodyParams.data.username, nickname: this.bodyParams.data.nickname, + bio: this.bodyParams.data.bio, statusText: this.bodyParams.data.statusText, + statusType: this.bodyParams.data.statusType, newPassword: this.bodyParams.data.newPassword, typedPassword: this.bodyParams.data.currentPassword, }; diff --git a/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts b/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts index 4ff220b38b6f..a4f59136a1f9 100644 --- a/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts +++ b/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts @@ -42,7 +42,7 @@ class CustomSoundsClass { const audio = document.createElement('audio'); audio.id = getCustomSoundId(sound._id); - audio.preload = 'auto'; + audio.preload = 'none'; audio.appendChild(source); document.body.appendChild(audio); diff --git a/apps/meteor/app/lib/server/functions/getFullUserData.ts b/apps/meteor/app/lib/server/functions/getFullUserData.ts index c04148c07ba8..0703b24d9210 100644 --- a/apps/meteor/app/lib/server/functions/getFullUserData.ts +++ b/apps/meteor/app/lib/server/functions/getFullUserData.ts @@ -21,6 +21,7 @@ const defaultFields = { avatarETag: 1, extension: 1, federated: 1, + statusLivechat: 1, } as const; const fullFields = { diff --git a/apps/meteor/app/livechat/server/api/v1/agent.ts b/apps/meteor/app/livechat/server/api/v1/agent.ts index b13c48bb579d..7a7b70ea7c8b 100644 --- a/apps/meteor/app/livechat/server/api/v1/agent.ts +++ b/apps/meteor/app/livechat/server/api/v1/agent.ts @@ -73,16 +73,21 @@ API.v1.addRoute( const agentId = inputAgentId || this.userId; - const agent = await Users.findOneAgentById>(agentId, { + const agent = await Users.findOneAgentById>(agentId, { projection: { status: 1, statusLivechat: 1, + active: 1, }, }); if (!agent) { return API.v1.notFound('Agent not found'); } + if (!agent.active) { + return API.v1.failure('error-user-deactivated'); + } + const newStatus: ILivechatAgentStatus = status || (agent.statusLivechat === ILivechatAgentStatus.AVAILABLE ? ILivechatAgentStatus.NOT_AVAILABLE : ILivechatAgentStatus.AVAILABLE); diff --git a/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts b/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts index 2c5ffe38169d..55de5bbf6315 100644 --- a/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts +++ b/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts @@ -1,13 +1,10 @@ -import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; -import type { ILivechatBusinessHour, ILivechatDepartment } from '@rocket.chat/core-typings'; +import type { ILivechatAgentStatus, ILivechatBusinessHour, ILivechatDepartment } from '@rocket.chat/core-typings'; import type { ILivechatBusinessHoursModel, IUsersModel } from '@rocket.chat/model-typings'; import { LivechatBusinessHours, Users } from '@rocket.chat/models'; import moment from 'moment-timezone'; import type { UpdateFilter } from 'mongodb'; import type { IWorkHoursCronJobsWrapper } from '../../../../server/models/raw/LivechatBusinessHours'; -import { businessHourLogger } from '../lib/logger'; -import { filterBusinessHoursThatMustBeOpened } from './Helper'; export interface IBusinessHourBehavior { findHoursToCreateJobs(): Promise; @@ -61,29 +58,6 @@ export abstract class AbstractBusinessHourBehavior { { livechatStatusSystemModified: true }, ); } - - async onNewAgentCreated(agentId: string): Promise { - businessHourLogger.debug(`Executing onNewAgentCreated for agentId: ${agentId}`); - - const defaultBusinessHour = await LivechatBusinessHours.findOneDefaultBusinessHour(); - if (!defaultBusinessHour) { - businessHourLogger.debug(`No default business hour found for agentId: ${agentId}`); - return; - } - - const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([defaultBusinessHour]); - if (!businessHourToOpen.length) { - businessHourLogger.debug( - `No business hour to open found for agentId: ${agentId}. Default business hour is closed. Setting agentId: ${agentId} to status: ${ILivechatAgentStatus.NOT_AVAILABLE}`, - ); - await Users.setLivechatStatus(agentId, ILivechatAgentStatus.NOT_AVAILABLE); - return; - } - - await Users.addBusinessHourByAgentIds([agentId], defaultBusinessHour._id); - - businessHourLogger.debug(`Setting agentId: ${agentId} to status: ${ILivechatAgentStatus.AVAILABLE}`); - } } export abstract class AbstractBusinessHourType { diff --git a/apps/meteor/app/livechat/server/business-hour/Single.ts b/apps/meteor/app/livechat/server/business-hour/Single.ts index 63135fa53224..d899f2717376 100644 --- a/apps/meteor/app/livechat/server/business-hour/Single.ts +++ b/apps/meteor/app/livechat/server/business-hour/Single.ts @@ -1,9 +1,10 @@ -import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; +import { ILivechatAgentStatus, LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; +import { LivechatBusinessHours, Users } from '@rocket.chat/models'; import { businessHourLogger } from '../lib/logger'; import type { IBusinessHourBehavior } from './AbstractBusinessHour'; import { AbstractBusinessHourBehavior } from './AbstractBusinessHour'; -import { openBusinessHourDefault } from './Helper'; +import { filterBusinessHoursThatMustBeOpened, openBusinessHourDefault } from './Helper'; export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior implements IBusinessHourBehavior { async openBusinessHoursByDayAndHour(): Promise { @@ -26,6 +27,35 @@ export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior imp return openBusinessHourDefault(); } + async onNewAgentCreated(agentId: string): Promise { + const defaultBusinessHour = await LivechatBusinessHours.findOneDefaultBusinessHour(); + if (!defaultBusinessHour) { + businessHourLogger.debug('No default business hour found for agentId', { + agentId, + }); + return; + } + + const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([defaultBusinessHour]); + if (!businessHourToOpen.length) { + businessHourLogger.debug({ + msg: 'No business hours found. Moving agent to NOT_AVAILABLE status', + agentId, + newStatus: ILivechatAgentStatus.NOT_AVAILABLE, + }); + await Users.setLivechatStatus(agentId, ILivechatAgentStatus.NOT_AVAILABLE); + return; + } + + await Users.addBusinessHourByAgentIds([agentId], defaultBusinessHour._id); + + businessHourLogger.debug({ + msg: 'Business hours found. Moving agent to AVAILABLE status', + agentId, + newStatus: ILivechatAgentStatus.AVAILABLE, + }); + } + afterSaveBusinessHours(): Promise { return openBusinessHourDefault(); } diff --git a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts index 50fe5846637b..30900481c4e2 100644 --- a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts +++ b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts @@ -1,4 +1,5 @@ -import type { IUser } from '@rocket.chat/core-typings'; +import { type IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; import { Livechat } from '../lib/Livechat'; @@ -33,8 +34,11 @@ const handleAgentCreated = async (user: IUser) => { const handleDeactivateUser = async (user: IUser) => { if (wasAgent(user)) { - callbackLogger.debug('Removing agent', user._id); - await Livechat.removeAgent(user.username); + callbackLogger.debug({ + msg: 'Removing agent extension & making agent unavailable', + userId: user._id, + }); + await Users.makeAgentUnavailableAndUnsetExtension(user._id); } }; diff --git a/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts b/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts index 5467e517768e..5ebf924e7334 100644 --- a/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts +++ b/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts @@ -1,3 +1,4 @@ +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; import { isOmnichannelRoom, isEditedMessage } from '@rocket.chat/core-typings'; import { LivechatRooms } from '@rocket.chat/models'; @@ -15,28 +16,39 @@ callbacks.add( return message; } + // skips this callback if the message is a system message + if (message.t) { + return message; + } + // if the message has a token, it was sent by the visitor, so ignore it if (message.token) { return message; } - if (room.responseBy) { - await LivechatRooms.setAgentLastMessageTs(room._id); - } - // check if room is yet awaiting for response - if (!(typeof room.t !== 'undefined' && room.t === 'l' && room.waitingResponse)) { + // check if room is yet awaiting for response from visitor + if (!room.waitingResponse) { + // case where agent sends second message or any subsequent message in a room before visitor responds to the first message + // in this case, we just need to update the lastMessageTs of the responseBy object + if (room.responseBy) { + await LivechatRooms.setAgentLastMessageTs(room._id); + } return message; } - await LivechatRooms.setResponseByRoomId(room._id, { - user: { - _id: message.u._id, - username: message.u.username, - }, - }); + // This is the first message from agent after visitor had last responded + const responseBy: IOmnichannelRoom['responseBy'] = room.responseBy || { + _id: message.u._id, + username: message.u.username, + firstResponseTs: new Date(message.ts), + lastMessageTs: new Date(message.ts), + }; + + // this unsets waitingResponse and sets responseBy object + await LivechatRooms.setResponseByRoomId(room._id, responseBy); return message; }, - callbacks.priority.LOW, + callbacks.priority.HIGH, 'markRoomResponded', ); diff --git a/apps/meteor/app/mentions/server/getMentionedTeamMembers.ts b/apps/meteor/app/mentions/server/getMentionedTeamMembers.ts new file mode 100644 index 000000000000..b1355c90cb93 --- /dev/null +++ b/apps/meteor/app/mentions/server/getMentionedTeamMembers.ts @@ -0,0 +1,35 @@ +import { Team } from '@rocket.chat/core-services'; +import type { IMessage } from '@rocket.chat/core-typings'; + +import { callbacks } from '../../../lib/callbacks'; +import { settings } from '../../settings/server'; + +interface IExtraDataForNotification { + userMentions: any[]; + otherMentions: any[]; + message: IMessage; +} + +const beforeGetMentions = async (mentionIds: string[], extra?: IExtraDataForNotification) => { + const { otherMentions } = extra ?? {}; + + const teamIds = otherMentions?.filter(({ type }) => type === 'team').map(({ _id }) => _id); + + if (!teamIds?.length) { + return mentionIds; + } + + const members = await Team.getMembersByTeamIds(teamIds, { projection: { userId: 1 } }); + mentionIds.push(...new Set(members.map(({ userId }) => userId).filter((userId) => !mentionIds.includes(userId)))); + + return mentionIds; +}; + +settings.watch('Troubleshoot_Disable_Teams_Mention', (value) => { + if (value) { + callbacks.remove('beforeGetMentions', 'before-get-mentions-get-teams'); + return; + } + + callbacks.add('beforeGetMentions', beforeGetMentions, callbacks.priority.MEDIUM, 'before-get-mentions-get-teams'); +}); diff --git a/apps/meteor/app/mentions/server/index.ts b/apps/meteor/app/mentions/server/index.ts index 474d41a439e1..a04af05b9db1 100644 --- a/apps/meteor/app/mentions/server/index.ts +++ b/apps/meteor/app/mentions/server/index.ts @@ -1,2 +1,3 @@ -import './server'; +import './getMentionedTeamMembers'; import './methods/getUserMentionsByChannel'; +import './server'; diff --git a/apps/meteor/app/mentions/server/server.ts b/apps/meteor/app/mentions/server/server.ts index 5eb70aae4656..13765e99d856 100644 --- a/apps/meteor/app/mentions/server/server.ts +++ b/apps/meteor/app/mentions/server/server.ts @@ -1,5 +1,5 @@ -import { api } from '@rocket.chat/core-services'; -import type { IUser, IRoom } from '@rocket.chat/core-typings'; +import { api, Team } from '@rocket.chat/core-services'; +import type { IUser, IRoom, ITeam } from '@rocket.chat/core-typings'; import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; @@ -9,16 +9,32 @@ import { settings } from '../../settings/server'; import MentionsServer from './Mentions'; export class MentionQueries { - async getUsers(usernames: string[]): Promise<(Pick & { type: 'user' })[]> { + async getUsers( + usernames: string[], + ): Promise<((Pick & { type: 'user' }) | (Pick & { type: 'team' }))[]> { + const uniqueUsernames = [...new Set(usernames)]; + const teams = await Team.listByNames(uniqueUsernames, { projection: { name: 1 } }); + const users = await Users.find( - { username: { $in: [...new Set(usernames)] } }, + { username: { $in: uniqueUsernames } }, { projection: { _id: true, username: true, name: 1 } }, ).toArray(); - return users.map((user) => ({ + const taggedUsers = users.map((user) => ({ ...user, - type: 'user', + type: 'user' as const, })); + + if (settings.get('Troubleshoot_Disable_Teams_Mention')) { + return taggedUsers; + } + + const taggedTeams = teams.map((team) => ({ + ...team, + type: 'team' as const, + })); + + return [...taggedUsers, ...taggedTeams]; } async getUser(userId: string): Promise { diff --git a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts index b77a268fe114..b3aa68337106 100644 --- a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts +++ b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts @@ -46,7 +46,7 @@ export class SAUMonitorClass { constructor() { this._started = false; this._dailyComputeJobName = 'aggregate-sessions'; - this._dailyFinishSessionsJobName = 'aggregate-sessions'; + this._dailyFinishSessionsJobName = 'finish-sessions'; } async start(): Promise { @@ -318,33 +318,19 @@ export class SAUMonitorClass { return; } - logger.info('[aggregate] - Aggregating data.'); + const today = new Date(); - const date = new Date(); - date.setDate(date.getDate() - 0); // yesterday - const yesterday = getDateObj(date); + // get sessions from 3 days ago to make sure even if a few cron jobs were skipped, we still have the data + const threeDaysAgo = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 3, 0, 0, 0, 0); - for await (const record of aggregates.dailySessionsOfYesterday(Sessions.col, yesterday)) { - await Sessions.updateOne( - { _id: `${record.userId}-${record.year}-${record.month}-${record.day}` }, - { $set: record }, - { upsert: true }, - ); + const period = { start: getDateObj(threeDaysAgo), end: getDateObj(today) }; + + logger.info({ msg: '[aggregate] - Aggregating data.', period }); + + for await (const record of aggregates.dailySessions(Sessions.col, period)) { + await Sessions.updateDailySessionById(`${record.userId}-${record.year}-${record.month}-${record.day}`, record); } - await Sessions.updateMany( - { - type: 'session', - year: { $lte: yesterday.year }, - month: { $lte: yesterday.month }, - day: { $lte: yesterday.day }, - }, - { - $set: { - type: 'computed-session', - _computedAt: new Date(), - }, - }, - ); + await Sessions.updateAllSessionsByDateToComputed(period); } } diff --git a/apps/meteor/client/components/GazzodownText.tsx b/apps/meteor/client/components/GazzodownText.tsx index 7912e44ad925..76868f84e3af 100644 --- a/apps/meteor/client/components/GazzodownText.tsx +++ b/apps/meteor/client/components/GazzodownText.tsx @@ -49,6 +49,7 @@ const GazzodownText = ({ mentions, channels, searchText, children }: GazzodownTe const useEmoji = Boolean(useUserPreference('useEmojis')); const useRealName = Boolean(useSetting('UI_Use_Real_Name')); const ownUserId = useUserId(); + const showMentionSymbol = Boolean(useUserPreference('mentionsWithSymbol')); const chat = useChat(); @@ -122,6 +123,7 @@ const GazzodownText = ({ mentions, channels, searchText, children }: GazzodownTe useRealName, isMobile, ownUserId, + showMentionSymbol, }} > {children} diff --git a/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts b/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts index 3b39ea79d42f..fd3c0a29effe 100644 --- a/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts +++ b/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts @@ -20,7 +20,6 @@ type DepartmentListItem = { _id: string; label: string; value: string; - _updatedAt: Date; }; export const useDepartmentsList = ( @@ -66,7 +65,6 @@ export const useDepartmentsList = ( _id, label: department.archived ? `${name} [${t('Archived')}]` : name, value: _id, - _updatedAt: new Date(_updatedAt || ''), }; }); @@ -75,7 +73,6 @@ export const useDepartmentsList = ( _id: '', label: t('All'), value: 'all', - _updatedAt: new Date(), }); options.haveNone && @@ -83,7 +80,6 @@ export const useDepartmentsList = ( _id: '', label: t('None'), value: '', - _updatedAt: new Date(), }); return { diff --git a/apps/meteor/client/components/Omnichannel/hooks/useLivechatTags.ts b/apps/meteor/client/components/Omnichannel/hooks/useLivechatTags.ts index 4bd85be40342..ce5704b66482 100644 --- a/apps/meteor/client/components/Omnichannel/hooks/useLivechatTags.ts +++ b/apps/meteor/client/components/Omnichannel/hooks/useLivechatTags.ts @@ -1,6 +1,8 @@ import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; +import { useOmnichannel } from '../../../hooks/omnichannel/useOmnichannel'; + type Props = { department?: string; text?: string; @@ -9,13 +11,19 @@ type Props = { export const useLivechatTags = (options: Props) => { const getTags = useEndpoint('GET', '/v1/livechat/tags'); + const { isEnterprise } = useOmnichannel(); const { department, text, viewAll } = options; - return useQuery(['/v1/livechat/tags', text, department], () => - getTags({ - text: text || '', - ...(department && { department }), - viewAll: viewAll ? 'true' : 'false', - }), + return useQuery( + ['/v1/livechat/tags', text, department], + () => + getTags({ + text: text || '', + ...(department && { department }), + viewAll: viewAll ? 'true' : 'false', + }), + { + enabled: isEnterprise, + }, ); }; diff --git a/apps/meteor/client/components/message/toolbox/MessageActionMenu.tsx b/apps/meteor/client/components/message/toolbox/MessageActionMenu.tsx index 4c5d442652f2..54a320ebf3d7 100644 --- a/apps/meteor/client/components/message/toolbox/MessageActionMenu.tsx +++ b/apps/meteor/client/components/message/toolbox/MessageActionMenu.tsx @@ -101,6 +101,7 @@ const MessageActionMenu = ({ options, onChangeMenuVisibility, ...props }: Messag data-qa-type='message-action' data-qa-id={option.id} role={option.role ? option.role : 'button'} + gap={!option.icon && option.type === 'apps'} /> ))} {index !== arr.length - 1 && } diff --git a/apps/meteor/client/lib/2fa/process2faReturn.ts b/apps/meteor/client/lib/2fa/process2faReturn.ts index e5ff73be77b1..95f7f1dcb361 100644 --- a/apps/meteor/client/lib/2fa/process2faReturn.ts +++ b/apps/meteor/client/lib/2fa/process2faReturn.ts @@ -1,10 +1,12 @@ import { SHA256 } from '@rocket.chat/sha256'; import { Meteor } from 'meteor/meteor'; +import { lazy } from 'react'; -import TwoFactorModal from '../../components/TwoFactorModal'; import { imperativeModal } from '../imperativeModal'; import { isTotpInvalidError, isTotpRequiredError } from './utils'; +const TwoFactorModal = lazy(() => import('../../components/TwoFactorModal')); + const twoFactorMethods = ['totp', 'email', 'password'] as const; type TwoFactorMethod = (typeof twoFactorMethods)[number]; diff --git a/apps/meteor/client/main.ts b/apps/meteor/client/main.ts index bb08f0242d4f..334515980787 100644 --- a/apps/meteor/client/main.ts +++ b/apps/meteor/client/main.ts @@ -1,13 +1,15 @@ -import '../ee/client/ecdh'; -import './polyfills'; +import { FlowRouter } from 'meteor/kadira:flow-router'; -import '../lib/oauthRedirectUriClient'; -import './lib/meteorCallWrapper'; -import './importPackages'; +FlowRouter.wait(); -import '../ee/client'; -import './methods'; -import './startup'; -import './views/admin'; -import './views/marketplace'; -import './views/account'; +FlowRouter.notFound = { + action: () => undefined, +}; + +import('./polyfills') + .then(() => Promise.all([import('./lib/meteorCallWrapper'), import('../lib/oauthRedirectUriClient')])) + .then(() => import('../ee/client/ecdh')) + .then(() => import('./importPackages')) + .then(() => Promise.all([import('./methods'), import('./startup')])) + .then(() => import('../ee/client')) + .then(() => Promise.all([import('./views/admin'), import('./views/marketplace'), import('./views/account')])); diff --git a/apps/meteor/client/polyfills/index.ts b/apps/meteor/client/polyfills/index.ts index 46f5bcb8d68d..f07d828a4602 100644 --- a/apps/meteor/client/polyfills/index.ts +++ b/apps/meteor/client/polyfills/index.ts @@ -4,4 +4,3 @@ import './childNodeRemove'; import './cssVars'; import './customEventPolyfill'; import './hoverTouchClick'; -import './objectFromEntries'; diff --git a/apps/meteor/client/polyfills/objectFromEntries.ts b/apps/meteor/client/polyfills/objectFromEntries.ts deleted file mode 100644 index d59198ebd1d3..000000000000 --- a/apps/meteor/client/polyfills/objectFromEntries.ts +++ /dev/null @@ -1,5 +0,0 @@ -Object.fromEntries = - Object.fromEntries || - function fromEntries(entries: Iterable): { [k: string]: T } { - return [...entries].reduce((obj, { 0: key, 1: val }) => Object.assign(obj, { [key]: val }), {}); - }; diff --git a/apps/meteor/client/providers/RouterProvider.tsx b/apps/meteor/client/providers/RouterProvider.tsx index 0dd7ee31deed..0f146ec83128 100644 --- a/apps/meteor/client/providers/RouterProvider.tsx +++ b/apps/meteor/client/providers/RouterProvider.tsx @@ -17,12 +17,6 @@ import React from 'react'; import { appLayout } from '../lib/appLayout'; import { queueMicrotask } from '../lib/utils/queueMicrotask'; -FlowRouter.wait(); - -FlowRouter.notFound = { - action: () => undefined, -}; - const subscribers = new Set<() => void>(); const listenToRouteChange = () => { diff --git a/apps/meteor/client/providers/TranslationProvider.tsx b/apps/meteor/client/providers/TranslationProvider.tsx index 5850f0da3982..fdddb9ec5349 100644 --- a/apps/meteor/client/providers/TranslationProvider.tsx +++ b/apps/meteor/client/providers/TranslationProvider.tsx @@ -226,6 +226,7 @@ const TranslationProvider = ({ children }: TranslationProviderProps): ReactEleme useEffect(() => { if (moment.locales().includes(language.toLowerCase())) { + moment.locale(language); return; } diff --git a/apps/meteor/client/providers/UserProvider/UserProvider.tsx b/apps/meteor/client/providers/UserProvider/UserProvider.tsx index fa9d683815ed..432a197671f3 100644 --- a/apps/meteor/client/providers/UserProvider/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider/UserProvider.tsx @@ -71,7 +71,8 @@ const UserProvider = ({ children }: UserProviderProps): ReactElement => { const setUserPreferences = useEndpoint('POST', '/v1/users.setPreferences'); - useCreateFontStyleElement(user?.settings?.preferences?.fontSize); + const createFontStyleElement = useCreateFontStyleElement(); + createFontStyleElement(user?.settings?.preferences?.fontSize); const loginMethod: LoginMethods = (isLdapEnabled && 'loginWithLDAP') || (isCrowdEnabled && 'loginWithCrowd') || 'loginWithPassword'; diff --git a/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx b/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx index f289cf2eeee2..ceb904613533 100644 --- a/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx +++ b/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx @@ -1,59 +1,94 @@ -import { Accordion, Box, Button, ButtonGroup, Field, FieldGroup, RadioButton, Select, Tag } from '@rocket.chat/fuselage'; +import { css } from '@rocket.chat/css-in-js'; +import type { SelectOption } from '@rocket.chat/fuselage'; +import { + Icon, + FieldDescription, + Accordion, + Box, + Button, + ButtonGroup, + Field, + FieldGroup, + FieldHint, + FieldLabel, + FieldRow, + RadioButton, + Select, + Tag, + ToggleSwitch, +} from '@rocket.chat/fuselage'; import { useLocalStorage, useUniqueId } from '@rocket.chat/fuselage-hooks'; -import type { FontSize } from '@rocket.chat/rest-typings'; -import { useSetModal, useTranslation, useToastMessageDispatch, useUserPreference, useEndpoint } from '@rocket.chat/ui-contexts'; -import type { ThemePreference } from '@rocket.chat/ui-theming/src/types/themes'; -import React from 'react'; +import { useSetModal, useTranslation, useToastMessageDispatch, useEndpoint, useSetting } from '@rocket.chat/ui-contexts'; +import { useMutation } from '@tanstack/react-query'; +import React, { useMemo } from 'react'; import { Controller, useForm } from 'react-hook-form'; import Page from '../../../components/Page'; import { useIsEnterprise } from '../../../hooks/useIsEnterprise'; +import { getDirtyFields } from '../../../lib/getDirtyFields'; import HighContrastUpsellModal from './HighContrastUpsellModal'; +import MentionsWithSymbolUpsellModal from './MentionsWithSymbolUpsellModal'; import { fontSizes } from './fontSizes'; -import { useAdjustableFontSize } from './hooks/useAdsjustableFontSize'; +import type { AccessibilityPreferencesData } from './hooks/useAcessibilityPreferencesValues'; +import { useAccessiblityPreferencesValues } from './hooks/useAcessibilityPreferencesValues'; +import { useCreateFontStyleElement } from './hooks/useCreateFontStyleElement'; import { themeItems as themes } from './themeItems'; const AccessibilityPage = () => { const t = useTranslation(); const setModal = useSetModal(); - const dispatchToastMessage = useToastMessageDispatch(); const { data: license } = useIsEnterprise(); + const preferencesValues = useAccessiblityPreferencesValues(); - const fontSizeId = useUniqueId(); - const [fontSize, setFontSize] = useAdjustableFontSize(); + const { themeAppearence } = preferencesValues; + const [, setPrevTheme] = useLocalStorage('prevTheme', themeAppearence); + const createFontStyleElement = useCreateFontStyleElement(); + const displayRolesEnabled = useSetting('UI_DisplayRoles'); - const themePreference = useUserPreference('themeAppearence') || 'auto'; - const [, setPrevTheme] = useLocalStorage('prevTheme', themePreference); + const timeFormatOptions = useMemo( + (): SelectOption[] => [ + ['0', t('Default')], + ['1', t('12_Hour')], + ['2', t('24_Hour')], + ], + [t], + ); - const setUserPreferences = useEndpoint('POST', '/v1/users.setPreferences'); + const fontSizeId = useUniqueId(); + const mentionsWithSymbolId = useUniqueId(); + const clockModeId = useUniqueId(); + const hideUsernamesId = useUniqueId(); + const hideRolesId = useUniqueId(); const { formState: { isDirty, dirtyFields }, handleSubmit, control, reset, + watch, } = useForm({ - defaultValues: { themeAppearence: themePreference, fontSize }, + defaultValues: preferencesValues, + }); + + const currentData = watch(); + + const setUserPreferencesEndpoint = useEndpoint('POST', '/v1/users.setPreferences'); + + const setPreferencesAction = useMutation({ + mutationFn: setUserPreferencesEndpoint, + onSuccess: () => dispatchToastMessage({ type: 'success', message: t('Preferences_saved') }), + onError: (error) => dispatchToastMessage({ type: 'error', message: error }), + onSettled: (_data, _error, { data: { fontSize } }) => { + reset(currentData); + dirtyFields.themeAppearence && setPrevTheme(themeAppearence); + dirtyFields.fontSize && fontSize && createFontStyleElement(fontSize); + }, }); - const handleSave = async ({ themeAppearence, fontSize }: { themeAppearence: ThemePreference; fontSize: FontSize }) => { - try { - await setUserPreferences({ data: { themeAppearence, fontSize } }); - // dirtyFields.themeAppearence && (await setUserPreferences({ data: { themeAppearence, fontSize } })); - // dirtyFields.fontSize && (await setUserPreferences({ data: { fontSize } })); - dispatchToastMessage({ type: 'success', message: t('Preferences_saved') }); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } finally { - if (dirtyFields.themeAppearence) { - setPrevTheme(themePreference); - } - if (dirtyFields.fontSize) { - setFontSize(fontSize); - } - reset({ themeAppearence, fontSize }); - } + const handleSaveData = (formData: AccessibilityPreferencesData) => { + const data = getDirtyFields(formData, dirtyFields); + setPreferencesAction.mutateAsync({ data }); }; return ( @@ -72,15 +107,18 @@ const AccessibilityPage = () => { return ( - + {t.has(title) ? t(title) : title} {communityDisabled && ( - {t('Enterprise')} + + + {t('Enterprise')} + )} - - + + { return onChange(id)} checked={value === id} />; }} /> - + - + {t.has(description) ? t(description) : description} - + ); })} @@ -110,10 +148,10 @@ const AccessibilityPage = () => { - + {t('Font_size')} - - + + { + )} + /> + + + + {t('Show_usernames')} + + ( + onChange(!(e.target as HTMLInputElement).checked)} + /> + )} + /> + + + {t('Show_or_hide_the_username_of_message_authors')} + + {displayRolesEnabled && ( + + + {t('Show_roles')} + + ( + onChange(!(e.target as HTMLInputElement).checked)} + /> + )} + /> + + + {t('Show_or_hide_the_user_roles_of_message_authors')} + + )} @@ -131,8 +260,8 @@ const AccessibilityPage = () => { - - + diff --git a/apps/meteor/client/views/account/accessibility/HighContrastUpsellModal.tsx b/apps/meteor/client/views/account/accessibility/HighContrastUpsellModal.tsx index c06501aae282..bcb28ea587d1 100644 --- a/apps/meteor/client/views/account/accessibility/HighContrastUpsellModal.tsx +++ b/apps/meteor/client/views/account/accessibility/HighContrastUpsellModal.tsx @@ -33,7 +33,7 @@ const HighContrastUpsellModal = ({ onClose }: { onClose: () => void }) => { onClose={onClose} onCancel={handleTalkToSales} onConfirm={handleGoFullyFeatured} - cancelText={t('Talk_to_sales')} + cancelText={t('Talk_to_an_expert')} confirmText={t('Start_free_trial')} /> ); diff --git a/apps/meteor/client/views/account/accessibility/MentionsWithSymbolUpsellModal.tsx b/apps/meteor/client/views/account/accessibility/MentionsWithSymbolUpsellModal.tsx new file mode 100644 index 000000000000..8a998af348c0 --- /dev/null +++ b/apps/meteor/client/views/account/accessibility/MentionsWithSymbolUpsellModal.tsx @@ -0,0 +1,41 @@ +import { useRole, useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import GenericUpsellModal from '../../../components/GenericUpsellModal'; +import { useUpsellActions } from '../../../components/GenericUpsellModal/hooks'; + +const MentionsWithSymbolUpsellModal = ({ onClose }: { onClose: () => void }) => { + const t = useTranslation(); + + const isAdmin = useRole('admin'); + const { handleGoFullyFeatured, handleTalkToSales } = useUpsellActions(); + + if (!isAdmin) { + return ( + + ); + } + return ( + + ); +}; +export default MentionsWithSymbolUpsellModal; diff --git a/apps/meteor/client/views/account/accessibility/hooks/useAcessibilityPreferencesValues.ts b/apps/meteor/client/views/account/accessibility/hooks/useAcessibilityPreferencesValues.ts new file mode 100644 index 000000000000..339cdcff7dd9 --- /dev/null +++ b/apps/meteor/client/views/account/accessibility/hooks/useAcessibilityPreferencesValues.ts @@ -0,0 +1,31 @@ +import type { FontSize } from '@rocket.chat/rest-typings'; +import { useUserPreference } from '@rocket.chat/ui-contexts'; +import type { ThemePreference } from '@rocket.chat/ui-theming/src/types/themes'; + +export type AccessibilityPreferencesData = { + themeAppearence?: ThemePreference; + fontSize?: FontSize; + fontSizePreference?: FontSize; + mentionsWithSymbol?: boolean; + clockMode?: 0 | 1 | 2; + hideUsernames?: boolean; + hideRoles?: boolean; +}; + +export const useAccessiblityPreferencesValues = (): AccessibilityPreferencesData => { + const themeAppearence = useUserPreference('themeAppearence') || 'auto'; + const fontSize = useUserPreference('fontSize') || '100%'; + const mentionsWithSymbol = useUserPreference('mentionsWithSymbol') || false; + const clockMode = useUserPreference<0 | 1 | 2>('clockMode') ?? 0; + const hideUsernames = useUserPreference('hideUsernames'); + const hideRoles = useUserPreference('hideRoles'); + + return { + themeAppearence, + fontSize, + mentionsWithSymbol, + clockMode, + hideUsernames, + hideRoles, + }; +}; diff --git a/apps/meteor/client/views/account/accessibility/hooks/useAdsjustableFontSize.tsx b/apps/meteor/client/views/account/accessibility/hooks/useAdsjustableFontSize.tsx deleted file mode 100644 index 35607de1f9b5..000000000000 --- a/apps/meteor/client/views/account/accessibility/hooks/useAdsjustableFontSize.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import type { FontSize } from '@rocket.chat/rest-typings'; -import { useUserPreference } from '@rocket.chat/ui-contexts'; -import { useState } from 'react'; - -import { useCreateFontStyleElement } from './useCreateFontStyleElement'; - -export const useAdjustableFontSize = (): [FontSize, (value: FontSize) => void] => { - const fontSizePreference = useUserPreference('fontSize') || '100%'; - const [fontSize, setFontSize] = useState(fontSizePreference); - - useCreateFontStyleElement(fontSize); - - return [fontSize, setFontSize]; -}; diff --git a/apps/meteor/client/views/account/accessibility/hooks/useCreateFontStyleElement.ts b/apps/meteor/client/views/account/accessibility/hooks/useCreateFontStyleElement.ts index 63ecd3c0cf4b..923b3af88c60 100644 --- a/apps/meteor/client/views/account/accessibility/hooks/useCreateFontStyleElement.ts +++ b/apps/meteor/client/views/account/accessibility/hooks/useCreateFontStyleElement.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useMemo } from 'react'; const createStyleElement = (id: string) => { const styleElement = document.getElementById(id); @@ -10,11 +10,14 @@ const createStyleElement = (id: string) => { return newStyleElement; }; -export const useCreateFontStyleElement = (fontSize: string): void => { - useEffect(() => { - const styleElement = createStyleElement('rcx-font-size'); - const css = `html { font-size: ${fontSize}; }`; - styleElement.innerHTML = css; - document.head.appendChild(styleElement); - }, [fontSize]); +export const useCreateFontStyleElement = (): ((fontSize: string) => void) => { + return useMemo( + () => (fontSize: string) => { + const styleElement = createStyleElement('rcx-font-size'); + const css = `html { font-size: ${fontSize}; }`; + styleElement.innerHTML = css; + document.head.appendChild(styleElement); + }, + [], + ); }; diff --git a/apps/meteor/client/views/account/preferences/PreferencesMessagesSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesMessagesSection.tsx index e5157d517967..30c4c5fe6725 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesMessagesSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesMessagesSection.tsx @@ -1,14 +1,16 @@ import type { SelectOption } from '@rocket.chat/fuselage'; -import { Accordion, Field, Select, FieldGroup, ToggleSwitch, Box } from '@rocket.chat/fuselage'; +import { FieldDescription, FieldLabel, Accordion, Field, Select, FieldGroup, ToggleSwitch, Box } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; -import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useMemo } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; const PreferencesMessagesSection = () => { const t = useTranslation(); - const displayRolesEnabled = useSetting('UI_DisplayRoles'); const { control } = useFormContext(); + const router = useRouter(); + + const handleGoToAccessibilityPage = () => router.navigate('/account/accessibility-and-appearance'); const alsoSendThreadMessageToChannelOptions = useMemo( (): SelectOption[] => [ @@ -19,15 +21,6 @@ const PreferencesMessagesSection = () => { [t], ); - const timeFormatOptions = useMemo( - (): SelectOption[] => [ - ['0', t('Default')], // TO DO: update SelectOption type to accept number as first item - ['1', t('12_Hour')], - ['2', t('24_Hour')], - ], - [t], - ); - const sendOnEnterOptions = useMemo( (): SelectOption[] => [ ['normal', t('Enter_Normal')], @@ -40,14 +33,11 @@ const PreferencesMessagesSection = () => { const unreadAlertId = useUniqueId(); const showThreadsInMainChannelId = useUniqueId(); const alsoSendThreadToChannelId = useUniqueId(); - const clockModeId = useUniqueId(); const useEmojisId = useUniqueId(); const convertAsciiEmojiId = useUniqueId(); const autoImageLoadId = useUniqueId(); const saveMobileBandwidthId = useUniqueId(); const collapseMediaByDefaultId = useUniqueId(); - const hideUsernamesId = useUniqueId(); - const hideRolesId = useUniqueId(); const hideFlexTabId = useUniqueId(); const displayAvatarsId = useUniqueId(); const sendOnEnterId = useUniqueId(); @@ -114,16 +104,10 @@ const PreferencesMessagesSection = () => { - {t('Message_TimeFormat')} - - ( -