diff --git a/.changeset/clean-cars-teach.md b/.changeset/clean-cars-teach.md new file mode 100644 index 000000000000..dc569636d246 --- /dev/null +++ b/.changeset/clean-cars-teach.md @@ -0,0 +1,11 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/core-typings": minor +"@rocket.chat/livechat": minor +--- + +**Added the ability for premium workspaces to hide Rocket.Chat's watermark as well as change the Livechat widget's logo** + +The new settings (named below) can be found in the Omnichannel workspace settings within the livechat section. +- Hide "powered by Rocket.Chat" +- Livechat widget logo (svg, png, jpg) diff --git a/.changeset/cyan-shoes-fly.md b/.changeset/cyan-shoes-fly.md new file mode 100644 index 000000000000..e89b64a01a31 --- /dev/null +++ b/.changeset/cyan-shoes-fly.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/livechat": minor +--- + +Added Livechat setting `Hide system messages` & API method `setHiddenSystemMessages`, to customize system message visibility within the widget. diff --git a/.changeset/fifty-cups-sort.md b/.changeset/fifty-cups-sort.md index 8f3e4512f055..389391ef8cc9 100644 --- a/.changeset/fifty-cups-sort.md +++ b/.changeset/fifty-cups-sort.md @@ -1,6 +1,6 @@ --- "@rocket.chat/meteor": minor -"@rocket.chat/rest-typings": patch +"@rocket.chat/rest-typings": minor --- Created a new endpoint to get a filtered and paginated list of users. diff --git a/.changeset/fuzzy-cherries-buy.md b/.changeset/fuzzy-cherries-buy.md new file mode 100644 index 000000000000..e185a148c917 --- /dev/null +++ b/.changeset/fuzzy-cherries-buy.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": major +--- + +Api login should not suggest which credential is wrong (password/username) + +Failed login attemps will always return `Unauthorized` instead of the internal fail reason diff --git a/.changeset/fuzzy-vans-own.md b/.changeset/fuzzy-vans-own.md new file mode 100644 index 000000000000..3508c6786a73 --- /dev/null +++ b/.changeset/fuzzy-vans-own.md @@ -0,0 +1,16 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/livechat": minor +--- + +Improved Livechat's theming capabilities + + +| Name (`setTheme`, `initialize`) | Workspace setting | Default value | Description | +|---------------------------------|----------------------------------------|---------------|-------------| +| `position` | Livechat widget position on the screen | `right` | Changes the widget position on the screen. Can be `left` or `right` | +| `background` | Livechat background | `N/A` | Changes the message list background. Accepts the same values as the CSS property [background](https://developer.mozilla.org/en-US/docs/Web/CSS/background) | +| `guestBubbleBackgroundColor` | `N/A` | `N/A` | Changes the guest's message bubble background color | +| `agentBubbleBackgroundColor` | `N/A` | `N/A` | Changes the agent's message bubble background color | +| `hideGuestAvatar` | `N/A` | `false` | Hides/shows the guest avatar | +| `hideAgentAvatar` | `N/A` | `true` | Hides/shows the agent avatar | diff --git a/.changeset/good-baboons-shop.md b/.changeset/good-baboons-shop.md new file mode 100644 index 000000000000..5a7230d841fb --- /dev/null +++ b/.changeset/good-baboons-shop.md @@ -0,0 +1,13 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/rest-typings": minor +--- + +**Added Livechat's new theming settings to Appearance page (available for Premium workspaces)** + +Newly added settings are: +- `Livechat widget position on the screen`: Changes the widget position between left or right of the viewport +- `Livechat background`: Changes the message list background. Receives the same value as the CSS's background property. +- `Hide system messages`: Changes the visibility of system messages displayed on the widget. +- `Hide "powered by Rocket.Chat"`: Changes the visibility of Rocket.Chat's watermark on the widget. + diff --git a/.changeset/good-ghosts-doubt.md b/.changeset/good-ghosts-doubt.md new file mode 100644 index 000000000000..5f4ed8f5a36d --- /dev/null +++ b/.changeset/good-ghosts-doubt.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Introduces a resizable Contextualbar allowing users to change the width just by dragging it diff --git a/.changeset/happy-pillows-call.md b/.changeset/happy-pillows-call.md new file mode 100644 index 000000000000..9100e1f7fe1b --- /dev/null +++ b/.changeset/happy-pillows-call.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +--- + +Changed logic that process custom fields from visitors when updating its data, making the process more reliable and faster. diff --git a/.changeset/long-phones-reflect.md b/.changeset/long-phones-reflect.md new file mode 100644 index 000000000000..4c822bd2fecb --- /dev/null +++ b/.changeset/long-phones-reflect.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fixed a small issue that was causing the room layout to shift when loading apps messages diff --git a/.changeset/nine-houses-reply.md b/.changeset/nine-houses-reply.md new file mode 100644 index 000000000000..29bbe0882a76 --- /dev/null +++ b/.changeset/nine-houses-reply.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/livechat": patch +--- + +Livechat: A registered user loses their messages if 'registerGuest' is called using the same token. diff --git a/.changeset/pink-ants-sing.md b/.changeset/pink-ants-sing.md new file mode 100644 index 000000000000..7b4841a11561 --- /dev/null +++ b/.changeset/pink-ants-sing.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed a UI issue that allowed a user to "mark" a room as favorite even when a room was not default. The Back-End was correctly ignoring the `favorite` property from being updated when the room was not default, but the UI still allowed users to try. +As UI allowed but changes were not saved, this gave the impression that the function was not working. diff --git a/.changeset/pink-parrots-end.md b/.changeset/pink-parrots-end.md index 133c7aec1350..9f1863f6915c 100644 --- a/.changeset/pink-parrots-end.md +++ b/.changeset/pink-parrots-end.md @@ -1,6 +1,6 @@ --- "@rocket.chat/meteor": minor -"@rocket.chat/rest-typings": patch +"@rocket.chat/rest-typings": minor --- Created a new endpoint to resend the welcome email to a given user diff --git a/.changeset/smart-squids-begin.md b/.changeset/smart-squids-begin.md new file mode 100644 index 000000000000..48f3f460ea7e --- /dev/null +++ b/.changeset/smart-squids-begin.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes the missing space between name and user name on system messages diff --git a/.changeset/strange-rivers-live.md b/.changeset/strange-rivers-live.md new file mode 100644 index 000000000000..b1ebd05c284d --- /dev/null +++ b/.changeset/strange-rivers-live.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/core-typings': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Added support for allowing agents to forward inquiries to departments that may not have any online agents given that `Allow department to receive forwarded inquiries even when there's no available agents` is set to `true` in the department configuration. +This configuration empowers agents to seamlessly direct incoming requests to the designated department, ensuring efficient handling of queries even when departmental resources are not actively online. When an agent becomes available, any pending inquiries will be automatically routed to them if the routing algorithm supports it. diff --git a/.changeset/strong-bananas-flash.md b/.changeset/strong-bananas-flash.md new file mode 100644 index 000000000000..d41697836d11 --- /dev/null +++ b/.changeset/strong-bananas-flash.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed duplicate API calls during livechat room forwarding by adding loading state for submit button diff --git a/.changeset/sweet-books-trade.md b/.changeset/sweet-books-trade.md new file mode 100644 index 000000000000..be828d662f32 --- /dev/null +++ b/.changeset/sweet-books-trade.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fixed search room not showing the new name room name changes diff --git a/.changeset/thirty-berries-learn.md b/.changeset/thirty-berries-learn.md new file mode 100644 index 000000000000..99a88729c99a --- /dev/null +++ b/.changeset/thirty-berries-learn.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": patch +--- + +**Added tag to premium settings in CE workspaces** + +A premium tag will be displayed alongside the setting label for premium exclusive settings in CE workspaces. +This will bring visibility for users of our premium features while also informing them of the reason why the setting is currently disabled. diff --git a/.changeset/thirty-ducks-smell.md b/.changeset/thirty-ducks-smell.md new file mode 100644 index 000000000000..e6f482d5fcea --- /dev/null +++ b/.changeset/thirty-ducks-smell.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/models": patch +--- + +Fix proxified model props were missing context before attribution diff --git a/.changeset/tough-doors-juggle.md b/.changeset/tough-doors-juggle.md new file mode 100644 index 000000000000..9da2ab1c6075 --- /dev/null +++ b/.changeset/tough-doors-juggle.md @@ -0,0 +1,9 @@ +--- +"@rocket.chat/meteor": patch +--- + +Introduced a new step to the queue worker: when an inquiry that's on an improper status is selected for processing, queue worker will first check its status and will attempt to fix it. +For example, if an inquiry points to a closed room, there's no point in processing, system will now remove the inquiry +If an inquiry is already taken, the inquiry will be updated to reflect the new status and clean the queue. + +This prevents issues where the queue worker attempted to process an inquiry _forever_ because it was in an improper state. diff --git a/.changeset/twenty-dolls-obey.md b/.changeset/twenty-dolls-obey.md new file mode 100644 index 000000000000..c886cf9cbc83 --- /dev/null +++ b/.changeset/twenty-dolls-obey.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fix error during migration 304. Throwing `Cannot read property 'finally' of undefined` error. diff --git a/.changeset/two-suns-marry.md b/.changeset/two-suns-marry.md new file mode 100644 index 000000000000..3eae6383a62f --- /dev/null +++ b/.changeset/two-suns-marry.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +feat: `ConnectionStatusBar` redesign diff --git a/.changeset/wild-keys-obey.md b/.changeset/wild-keys-obey.md new file mode 100644 index 000000000000..9de92ee5671b --- /dev/null +++ b/.changeset/wild-keys-obey.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/livechat": patch +--- + +Fixes issue causing a desync in different browser windows when a chat is closed and started again diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml index 284a0985b78e..364957ecdf01 100644 --- a/.github/actions/build-docker/action.yml +++ b/.github/actions/build-docker/action.yml @@ -13,6 +13,10 @@ inputs: required: false description: 'Platform' type: string + build-containers: + required: false + description: 'Containers to build along with Rocket.Chat' + type: string runs: using: composite @@ -54,11 +58,7 @@ runs: - name: Build Docker images shell: bash run: | - args=(rocketchat) - - if [[ '${{ inputs.platform }}' = 'alpine' ]]; then - args+=($SERVICES_PUBLISH) - fi; + args=(rocketchat ${{ inputs.build-containers }}) docker compose -f docker-compose-ci.yml build "${args[@]}" @@ -66,10 +66,6 @@ runs: if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') shell: bash run: | - args=(rocketchat) - - if [[ '${{ inputs.platform }}' = 'alpine' ]]; then - args+=($SERVICES_PUBLISH) - fi; + args=(rocketchat ${{ inputs.build-containers }}) docker compose -f docker-compose-ci.yml push "${args[@]}" diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index d3a463492cbb..0e921e81f1f3 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -41,6 +41,5 @@ runs: cache: 'yarn' - name: yarn install - if: steps.cache-node-modules.outputs.cache-hit != 'true' shell: bash run: yarn diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 1d953b9eb01f..12bb361d76f2 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -86,6 +86,14 @@ jobs: name: MongoDB ${{ matrix.mongodb-version }} (${{ matrix.shard }}/${{ inputs.total-shard }}) steps: + - 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: ${{ secrets.CR_USER }} + password: ${{ secrets.CR_PAT }} + - name: Launch MongoDB uses: supercharge/mongodb-github-action@v1.10.0 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a78ca940d03..51e034505a85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -219,7 +219,6 @@ jobs: 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' strategy: fail-fast: false @@ -237,6 +236,7 @@ jobs: CR_PAT: ${{ secrets.CR_PAT }} node-version: ${{ needs.release-versions.outputs.node-version }} platform: ${{ matrix.platform }} + build-containers: ${{ matrix.platform == 'alpine' && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }} build-gh-docker: name: 🚢 Build Docker Images for Production @@ -248,7 +248,6 @@ jobs: 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' strategy: fail-fast: false @@ -264,6 +263,7 @@ jobs: CR_PAT: ${{ secrets.CR_PAT }} node-version: ${{ needs.release-versions.outputs.node-version }} platform: ${{ matrix.platform }} + build-containers: ${{ matrix.platform == 'alpine' && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }} - name: Rename official Docker tag to GitHub Container Registry if: matrix.platform == 'official' diff --git a/.vscode/settings.json b/.vscode/settings.json index c9d889142050..4eaf1836d1fd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,6 +29,7 @@ "oauthapps", "omnichannel", "photoswipe", + "proxify", "searchbox", "tmid", "tshow" diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 2ed3ce7e07a0..b2c9284c751c 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,189 @@ # @rocket.chat/meteor +## 6.6.6 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- ([#32064](https://github.com/RocketChat/Rocket.Chat/pull/32064)) Fix an issue affecting Rocket.Chat Apps utilizing the OAuth 2 library from Apps Engine, ensuring that apps like Google Drive and Google Calendar are operational once more. + +- ([#32056](https://github.com/RocketChat/Rocket.Chat/pull/32056)) Fix error during migration 304. Throwing `Cannot read property 'finally' of undefined` error. + +-
Updated dependencies [ada096901a]: + + - @rocket.chat/models@0.0.34 + - @rocket.chat/omnichannel-services@0.1.10 + - @rocket.chat/presence@0.1.10 + - @rocket.chat/core-services@0.3.10 + - @rocket.chat/cron@0.0.30 + - @rocket.chat/instance-status@0.0.34 + - @rocket.chat/core-typings@6.6.6 + - @rocket.chat/rest-typings@6.6.6 + - @rocket.chat/api-client@0.1.28 + - @rocket.chat/license@0.1.10 + - @rocket.chat/pdf-worker@0.0.34 + - @rocket.chat/gazzodown@4.0.6 + - @rocket.chat/model-typings@0.3.6 + - @rocket.chat/ui-contexts@4.0.6 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/fuselage-ui-kit@4.0.6 + - @rocket.chat/ui-theming@0.1.2 + - @rocket.chat/ui-client@4.0.6 + - @rocket.chat/ui-video-conf@4.0.6 + - @rocket.chat/web-ui-registration@4.0.6 +
+ +## 6.6.5 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- ([#31998](https://github.com/RocketChat/Rocket.Chat/pull/31998)) Introduced a new step to the queue worker: when an inquiry that's on an improper status is selected for processing, queue worker will first check its status and will attempt to fix it. + For example, if an inquiry points to a closed room, there's no point in processing, system will now remove the inquiry + If an inquiry is already taken, the inquiry will be updated to reflect the new status and clean the queue. + + This prevents issues where the queue worker attempted to process an inquiry _forever_ because it was in an improper state. + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@4.0.5 + - @rocket.chat/ui-theming@0.1.2 + - @rocket.chat/fuselage-ui-kit@4.0.5 + - @rocket.chat/gazzodown@4.0.5 + - @rocket.chat/ui-client@4.0.5 + - @rocket.chat/ui-video-conf@4.0.5 + - @rocket.chat/web-ui-registration@4.0.5 + - @rocket.chat/core-typings@6.6.5 + - @rocket.chat/rest-typings@6.6.5 + - @rocket.chat/api-client@0.1.27 + - @rocket.chat/license@0.1.9 + - @rocket.chat/omnichannel-services@0.1.9 + - @rocket.chat/pdf-worker@0.0.33 + - @rocket.chat/presence@0.1.9 + - @rocket.chat/core-services@0.3.9 + - @rocket.chat/cron@0.0.29 + - @rocket.chat/model-typings@0.3.5 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.0.33 + - @rocket.chat/instance-status@0.0.33 +
+ +## 6.6.4 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- ([#31700](https://github.com/RocketChat/Rocket.Chat/pull/31700)) Fixed matrix homeserver domain setting not being visible in admin panel + +- ([#32012](https://github.com/RocketChat/Rocket.Chat/pull/32012)) Don't use the registration.yaml file to configure Matrix Federation anymore. + +- ([#31927](https://github.com/RocketChat/Rocket.Chat/pull/31927)) `stopped` lifecycle method was unexpectedly synchronous when using microservices, causing our code to create race conditions. + +-
Updated dependencies [c2872a93f2]: + + - @rocket.chat/core-services@0.3.8 + - @rocket.chat/omnichannel-services@0.1.8 + - @rocket.chat/presence@0.1.8 + - @rocket.chat/core-typings@6.6.4 + - @rocket.chat/rest-typings@6.6.4 + - @rocket.chat/api-client@0.1.26 + - @rocket.chat/license@0.1.8 + - @rocket.chat/pdf-worker@0.0.32 + - @rocket.chat/cron@0.0.28 + - @rocket.chat/gazzodown@4.0.4 + - @rocket.chat/model-typings@0.3.4 + - @rocket.chat/ui-contexts@4.0.4 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/fuselage-ui-kit@4.0.4 + - @rocket.chat/models@0.0.32 + - @rocket.chat/ui-theming@0.1.2 + - @rocket.chat/ui-client@4.0.4 + - @rocket.chat/ui-video-conf@4.0.4 + - @rocket.chat/web-ui-registration@4.0.4 + - @rocket.chat/instance-status@0.0.32 +
+ +## 6.6.3 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- ([#31895](https://github.com/RocketChat/Rocket.Chat/pull/31895)) Fix users presence stuck as online after connecting using mobile apps + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.6.3 + - @rocket.chat/rest-typings@6.6.3 + - @rocket.chat/api-client@0.1.25 + - @rocket.chat/license@0.1.7 + - @rocket.chat/omnichannel-services@0.1.7 + - @rocket.chat/pdf-worker@0.0.31 + - @rocket.chat/presence@0.1.7 + - @rocket.chat/core-services@0.3.7 + - @rocket.chat/cron@0.0.27 + - @rocket.chat/gazzodown@4.0.3 + - @rocket.chat/model-typings@0.3.3 + - @rocket.chat/ui-contexts@4.0.3 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/fuselage-ui-kit@4.0.3 + - @rocket.chat/models@0.0.31 + - @rocket.chat/ui-theming@0.1.2 + - @rocket.chat/ui-client@4.0.3 + - @rocket.chat/ui-video-conf@4.0.3 + - @rocket.chat/web-ui-registration@4.0.3 + - @rocket.chat/instance-status@0.0.31 +
+ +## 6.6.2 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- ([#31844](https://github.com/RocketChat/Rocket.Chat/pull/31844)) Fixed Federation not working with Microservice deployments + +- ([#31823](https://github.com/RocketChat/Rocket.Chat/pull/31823)) Revert unintentional changes real time presence data payload + +- ([#31833](https://github.com/RocketChat/Rocket.Chat/pull/31833)) Fix web UI not showing users presence updating to offline + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@4.0.2 + - @rocket.chat/ui-theming@0.1.2 + - @rocket.chat/fuselage-ui-kit@4.0.2 + - @rocket.chat/gazzodown@4.0.2 + - @rocket.chat/ui-client@4.0.2 + - @rocket.chat/ui-video-conf@4.0.2 + - @rocket.chat/web-ui-registration@4.0.2 + - @rocket.chat/core-typings@6.6.2 + - @rocket.chat/rest-typings@6.6.2 + - @rocket.chat/api-client@0.1.24 + - @rocket.chat/license@0.1.6 + - @rocket.chat/omnichannel-services@0.1.6 + - @rocket.chat/pdf-worker@0.0.30 + - @rocket.chat/presence@0.1.6 + - @rocket.chat/core-services@0.3.6 + - @rocket.chat/cron@0.0.26 + - @rocket.chat/model-typings@0.3.2 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.0.30 + - @rocket.chat/instance-status@0.0.30 +
+ ## 6.6.1 ### Patch Changes diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index ae08dae04938..47e2c03e6064 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -322,7 +322,7 @@ API.v1.addRoute( { async post() { // eslint-disable-next-line @typescript-eslint/naming-convention - const { prid, pmid, reply, t_name, users, encrypted } = this.bodyParams; + const { prid, pmid, reply, t_name, users, encrypted, topic } = this.bodyParams; if (!prid) { return API.v1.failure('Body parameter "prid" is required.'); } @@ -344,6 +344,7 @@ API.v1.addRoute( reply, users: users?.filter(isTruthy) || [], encrypted, + topic, }); return API.v1.success({ discussion }); diff --git a/apps/meteor/app/assets/server/assets.ts b/apps/meteor/app/assets/server/assets.ts index 529526ba6750..0acba139d5f4 100644 --- a/apps/meteor/app/assets/server/assets.ts +++ b/apps/meteor/app/assets/server/assets.ts @@ -1,7 +1,7 @@ import crypto from 'crypto'; import type { ServerResponse, IncomingMessage } from 'http'; -import type { IRocketChatAssets, IRocketChatAsset } from '@rocket.chat/core-typings'; +import type { IRocketChatAssets, IRocketChatAsset, ISetting } from '@rocket.chat/core-typings'; import { Settings } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import type { NextHandleFunction } from 'connect'; @@ -20,7 +20,10 @@ import { getURL } from '../../utils/server/getURL'; const RocketChatAssetsInstance = new RocketChatFile.GridFS({ name: 'assets', }); -const assets: IRocketChatAssets = { + +type IRocketChatAssetsConfig = Record }>; + +const assets: IRocketChatAssetsConfig = { logo: { label: 'logo (svg, png, jpg)', defaultUrl: 'images/logo/logo.svg', @@ -189,9 +192,27 @@ const assets: IRocketChatAssets = { extensions: ['svg'], }, }, + livechat_widget_logo: { + label: 'widget logo (svg, png, jpg)', + constraints: { + type: 'image', + extensions: ['svg', 'png', 'jpg', 'jpeg'], + }, + settingOptions: { + section: 'Livechat', + group: 'Omnichannel', + invalidValue: { + defaultUrl: undefined, + }, + enableQuery: { _id: 'Livechat_enabled', value: true }, + enterprise: true, + modules: ['livechat-enterprise'], + sorter: 999 + 1, + }, + }, }; -function getAssetByKey(key: string): IRocketChatAsset { +function getAssetByKey(key: string) { return assets[key as keyof IRocketChatAssets]; } @@ -325,7 +346,7 @@ class RocketChatAssetsClass { export const RocketChatAssets = new RocketChatAssetsClass(); -async function addAssetToSetting(asset: string, value: IRocketChatAsset): Promise { +export async function addAssetToSetting(asset: string, value: IRocketChatAsset, options?: Partial): Promise { const key = `Assets_${asset}`; await settingsRegistry.add( @@ -334,19 +355,21 @@ async function addAssetToSetting(asset: string, value: IRocketChatAsset): Promis defaultUrl: value.defaultUrl, }, { - type: 'asset', - group: 'Assets', - fileConstraints: value.constraints, - i18nLabel: value.label, - asset, - public: true, - wizard: value.wizard, + ...{ + type: 'asset', + group: 'Assets', + fileConstraints: value.constraints, + i18nLabel: value.label, + asset, + public: true, + }, + ...options, }, ); const currentValue = settings.get(key); - if (typeof currentValue === 'object' && currentValue.defaultUrl !== getAssetByKey(asset).defaultUrl) { + if (currentValue && typeof currentValue === 'object' && currentValue.defaultUrl !== getAssetByKey(asset).defaultUrl) { currentValue.defaultUrl = getAssetByKey(asset).defaultUrl; await Settings.updateValueById(key, currentValue); } @@ -354,8 +377,8 @@ async function addAssetToSetting(asset: string, value: IRocketChatAsset): Promis void (async () => { for await (const key of Object.keys(assets)) { - const value = getAssetByKey(key); - await addAssetToSetting(key, value); + const { wizard, settingOptions, ...value } = getAssetByKey(key); + await addAssetToSetting(key, value, { ...settingOptions, wizard }); } })(); diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index e3b97c1aae88..c4f568f3c4f8 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -1,3 +1,4 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; import { Roles, Settings, Users } from '@rocket.chat/models'; import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers'; import { Accounts } from 'meteor/accounts-base'; @@ -5,7 +6,6 @@ import { Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; -import { AppEvents, Apps } from '../../../../ee/server/apps/orchestrator'; import { callbacks } from '../../../../lib/callbacks'; import { beforeCreateUserCallback } from '../../../../lib/callbacks/beforeCreateUserCallback'; import { parseCSV } from '../../../../lib/utils/parseCSV'; @@ -350,8 +350,8 @@ const insertUserDocAsync = async function (options, user) { if (!options.skipAppsEngineEvent) { // `post` triggered events don't need to wait for the promise to resolve - Apps.triggerEvent(AppEvents.IPostUserCreated, { user, performedBy: await safeGetMeteorUser() }).catch((e) => { - Apps.getRocketChatLogger().error('Error while executing post user created event:', e); + Apps.self?.triggerEvent(AppEvents.IPostUserCreated, { user, performedBy: await safeGetMeteorUser() }).catch((e) => { + Apps.self?.getRocketChatLogger().error('Error while executing post user created event:', e); }); } @@ -424,7 +424,7 @@ const validateLoginAttemptAsync = async function (login) { */ if (login.type !== 'resume') { // App IPostUserLoggedIn event hook - await Apps.triggerEvent(AppEvents.IPostUserLoggedIn, login.user); + await Apps.self?.triggerEvent(AppEvents.IPostUserLoggedIn, login.user); } return true; diff --git a/apps/meteor/app/cors/server/cors.ts b/apps/meteor/app/cors/server/cors.ts index 2a862635e1cd..b4936d1456bc 100644 --- a/apps/meteor/app/cors/server/cors.ts +++ b/apps/meteor/app/cors/server/cors.ts @@ -16,14 +16,28 @@ type NextFunction = (err?: any) => void; const logger = new Logger('CORS'); +let templatePromise: Promise | void; + +declare module 'meteor/webapp' { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace WebApp { + function setInlineScriptsAllowed(allowed: boolean): Promise; + } +} + settings.watch( 'Enable_CSP', - Meteor.bindEnvironment((enabled) => { - WebAppInternals.setInlineScriptsAllowed(!enabled); + Meteor.bindEnvironment(async (enabled) => { + templatePromise = WebAppInternals.setInlineScriptsAllowed(!enabled); }), ); -WebApp.rawConnectHandlers.use((_req: http.IncomingMessage, res: http.ServerResponse, next: NextFunction) => { +WebApp.rawConnectHandlers.use(async (_req: http.IncomingMessage, res: http.ServerResponse, next: NextFunction) => { + if (templatePromise) { + await templatePromise; + templatePromise = void 0; + } + // XSS Protection for old browsers (IE) res.setHeader('X-XSS-Protection', '1'); @@ -46,6 +60,8 @@ WebApp.rawConnectHandlers.use((_req: http.IncomingMessage, res: http.ServerRespo const inlineHashes = [ // Hash for `window.close()`, required by the CAS login popup. "'sha256-jqxtvDkBbRAl9Hpqv68WdNOieepg8tJSYu1xIy7zT34='", + // Hash for /apps/meteor/packages/rocketchat-livechat/assets/demo.html:25 + "'sha256-aui5xYk3Lu1dQcnsPlNZI+qDTdfzdUv3fzsw80VLJgw='", ] .filter(Boolean) .join(' '); diff --git a/apps/meteor/app/discussion/server/methods/createDiscussion.ts b/apps/meteor/app/discussion/server/methods/createDiscussion.ts index 6d60e9af24bc..18b42ba1a31f 100644 --- a/apps/meteor/app/discussion/server/methods/createDiscussion.ts +++ b/apps/meteor/app/discussion/server/methods/createDiscussion.ts @@ -62,6 +62,7 @@ type CreateDiscussionProperties = { users: Array>; user: IUser; encrypted?: boolean; + topic?: string; }; const create = async ({ @@ -72,6 +73,7 @@ const create = async ({ users, user, encrypted, + topic, }: CreateDiscussionProperties): Promise => { // if you set both, prid and pmid, and the rooms dont match... should throw an error) let message: null | IMessage = null; @@ -145,7 +147,7 @@ const create = async ({ const type = await roomCoordinator.getRoomDirectives(parentRoom.t).getDiscussionType(parentRoom); const description = parentRoom.encrypted ? '' : message?.msg; - const topic = parentRoom.name; + const discussionTopic = topic || parentRoom.name; if (!type) { throw new Meteor.Error('error-invalid-type', 'Cannot define discussion room type', { @@ -163,7 +165,7 @@ const create = async ({ { fname: discussionName, description, // TODO discussions remove - topic, // TODO discussions remove + topic: discussionTopic, prid, encrypted, }, @@ -203,7 +205,7 @@ declare module '@rocket.chat/ui-contexts' { export const createDiscussion = async ( userId: string, - { prid, pmid, t_name: discussionName, reply, users, encrypted }: Omit, + { prid, pmid, t_name: discussionName, reply, users, encrypted, topic }: Omit, ): Promise< IRoom & { rid: string; @@ -229,7 +231,7 @@ export const createDiscussion = async ( }); } - return create({ prid, pmid, t_name: discussionName, reply, users, user, encrypted }); + return create({ prid, pmid, t_name: discussionName, reply, users, user, encrypted, topic }); }; Meteor.methods({ diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.ts b/apps/meteor/app/file-upload/server/lib/FileUpload.ts index 35bb62cebe94..4458f9d61881 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.ts +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.ts @@ -8,6 +8,7 @@ import stream from 'stream'; import URL from 'url'; import { hashLoginToken } from '@rocket.chat/account-utils'; +import { Apps, AppEvents } from '@rocket.chat/apps'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; import type { IUpload } from '@rocket.chat/core-typings'; import { Users, Avatars, UserDataFiles, Uploads, Settings, Subscriptions, Messages, Rooms } from '@rocket.chat/models'; @@ -21,7 +22,6 @@ import sharp from 'sharp'; import type { WritableStreamBuffer } from 'stream-buffers'; import streamBuffers from 'stream-buffers'; -import { AppEvents, Apps } from '../../../../ee/server/apps'; import { i18n } from '../../../../server/lib/i18n'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; @@ -177,7 +177,7 @@ export const FileUpload = { // App IPreFileUpload event hook try { - await Apps.triggerEvent(AppEvents.IPreFileUpload, { file, content: content || Buffer.from([]) }); + await Apps.self?.triggerEvent(AppEvents.IPreFileUpload, { file, content: content || Buffer.from([]) }); } catch (error: any) { if (error.name === AppsEngineException.name) { throw new Meteor.Error('error-app-prevented', error.message); @@ -587,15 +587,7 @@ export const FileUpload = { } // eslint-disable-next-line prettier/prettier - const headersToProxy = [ - 'age', - 'cache-control', - 'content-length', - 'content-type', - 'date', - 'expired', - 'last-modified', - ]; + const headersToProxy = ['age', 'cache-control', 'content-length', 'content-type', 'date', 'expired', 'last-modified']; headersToProxy.forEach((header) => { fileRes.headers[header] && res.setHeader(header, String(fileRes.headers[header])); diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts index 4e29576cf3bb..4506b659bf4d 100644 --- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts +++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts @@ -1,3 +1,4 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; import { Message, Team } from '@rocket.chat/core-services'; import type { IUser } from '@rocket.chat/core-typings'; @@ -5,7 +6,6 @@ import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; -import { AppEvents, Apps } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; @@ -54,7 +54,7 @@ export const addUserToRoom = async function ( } try { - await Apps.triggerEvent(AppEvents.IPreRoomUserJoined, room, userToBeAdded, inviter); + await Apps.self?.triggerEvent(AppEvents.IPreRoomUserJoined, room, userToBeAdded, inviter); } catch (error: any) { if (error.name === AppsEngineException.name) { throw new Meteor.Error('error-app-prevented', error.message); @@ -118,7 +118,7 @@ export const addUserToRoom = async function ( // Keep the current event await callbacks.run('afterJoinRoom', userToBeAdded, room); - void Apps.triggerEvent(AppEvents.IPostRoomUserJoined, room, userToBeAdded, inviter); + void Apps.self?.triggerEvent(AppEvents.IPostRoomUserJoined, room, userToBeAdded, inviter); }); } diff --git a/apps/meteor/app/lib/server/functions/createDirectRoom.ts b/apps/meteor/app/lib/server/functions/createDirectRoom.ts index 28bb74d7abe9..c1de81332543 100644 --- a/apps/meteor/app/lib/server/functions/createDirectRoom.ts +++ b/apps/meteor/app/lib/server/functions/createDirectRoom.ts @@ -1,3 +1,4 @@ +import { AppEvents, Apps } from '@rocket.chat/apps'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; import type { ISubscriptionExtraData } from '@rocket.chat/core-services'; import type { ICreatedRoom, IRoom, ISubscription, IUser } from '@rocket.chat/core-typings'; @@ -6,7 +7,6 @@ import { Random } from '@rocket.chat/random'; import { Meteor } from 'meteor/meteor'; import type { MatchKeysAndValues } from 'mongodb'; -import { Apps } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { isTruthy } from '../../../../lib/isTruthy'; import { settings } from '../../../settings/server'; @@ -103,7 +103,7 @@ export async function createDirectRoom( _USERNAMES: usernames, }; - const prevent = await Apps.triggerEvent('IPreRoomCreatePrevent', tmpRoom).catch((error) => { + const prevent = await Apps.self?.triggerEvent(AppEvents.IPreRoomCreatePrevent, tmpRoom).catch((error) => { if (error.name === AppsEngineException.name) { throw new Meteor.Error('error-app-prevented', error.message); } @@ -115,7 +115,10 @@ export async function createDirectRoom( throw new Meteor.Error('error-app-prevented', 'A Rocket.Chat App prevented the room creation.'); } - const result = await Apps.triggerEvent('IPreRoomCreateModify', await Apps.triggerEvent('IPreRoomCreateExtend', tmpRoom)); + const result = await Apps.self?.triggerEvent( + AppEvents.IPreRoomCreateModify, + await Apps.self?.triggerEvent(AppEvents.IPreRoomCreateExtend, tmpRoom), + ); if (typeof result === 'object') { Object.assign(roomInfo, result); @@ -169,7 +172,7 @@ export async function createDirectRoom( await callbacks.run('afterCreateDirectRoom', insertedRoom, { members: roomMembers, creatorId: options?.creator }); - void Apps.triggerEvent('IPostRoomCreate', insertedRoom); + void Apps.self?.triggerEvent(AppEvents.IPostRoomCreate, insertedRoom); } return { diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index f3c6730b1dfe..615a0faf8bb3 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -1,4 +1,5 @@ /* eslint-disable complexity */ +import { AppEvents, Apps } from '@rocket.chat/apps'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; import { Message, Team } from '@rocket.chat/core-services'; import type { ICreateRoomParams, ISubscriptionExtraData } from '@rocket.chat/core-services'; @@ -6,7 +7,6 @@ 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 { Apps } from '../../../../ee/server/apps/orchestrator'; import { callbacks } from '../../../../lib/callbacks'; import { beforeCreateRoomCallback } from '../../../../lib/callbacks/beforeCreateRoomCallback'; import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; @@ -198,7 +198,7 @@ export const createRoom = async ( _USERNAMES: members, }; - const prevent = await Apps.triggerEvent('IPreRoomCreatePrevent', tmp).catch((error) => { + const prevent = await Apps.self?.triggerEvent(AppEvents.IPreRoomCreatePrevent, tmp).catch((error) => { if (error.name === AppsEngineException.name) { throw new Meteor.Error('error-app-prevented', error.message); } @@ -210,7 +210,10 @@ export const createRoom = async ( throw new Meteor.Error('error-app-prevented', 'A Rocket.Chat App prevented the room creation.'); } - const eventResult = await Apps.triggerEvent('IPreRoomCreateModify', await Apps.triggerEvent('IPreRoomCreateExtend', tmp)); + const eventResult = await Apps.self?.triggerEvent( + AppEvents.IPreRoomCreateModify, + await Apps.triggerEvent(AppEvents.IPreRoomCreateExtend, tmp), + ); if (eventResult && typeof eventResult === 'object' && delete eventResult._USERNAMES) { Object.assign(roomProps, eventResult); @@ -242,7 +245,7 @@ export const createRoom = async ( callbacks.runAsync('federation.afterCreateFederatedRoom', room, { owner, originalMemberList: members }); } - void Apps.triggerEvent('IPostRoomCreate', room); + void Apps.self?.triggerEvent(AppEvents.IPostRoomCreate, room); return { rid: room._id, // backwards compatible inserted: true, diff --git a/apps/meteor/app/lib/server/functions/deleteMessage.ts b/apps/meteor/app/lib/server/functions/deleteMessage.ts index cd4456b24514..9368787bf7ea 100644 --- a/apps/meteor/app/lib/server/functions/deleteMessage.ts +++ b/apps/meteor/app/lib/server/functions/deleteMessage.ts @@ -1,9 +1,9 @@ +import { AppEvents, Apps } from '@rocket.chat/apps'; import { api } from '@rocket.chat/core-services'; import type { AtLeast, IMessage, IUser } from '@rocket.chat/core-typings'; import { Messages, Rooms, Uploads, Users, ReadReceipts } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { Apps } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { broadcastMessageFromData } from '../../../../server/modules/watchers/lib/messages'; import { canDeleteMessageAsync } from '../../../authorization/server/functions/canDeleteMessage'; @@ -29,14 +29,14 @@ export const deleteMessageValidatingPermission = async (message: AtLeast { - const deletedMsg = await Messages.findOneById(message._id); + const deletedMsg: IMessage | null = await Messages.findOneById(message._id); const isThread = (deletedMsg?.tcount || 0) > 0; const keepHistory = settings.get('Message_KeepHistory') || isThread; const showDeletedStatus = settings.get('Message_ShowDeletedStatus') || isThread; - const bridges = Apps?.isLoaded() && Apps.getBridges(); + const bridges = Apps.self?.isLoaded() && Apps.getBridges(); if (deletedMsg && bridges) { - const prevent = await bridges.getListenerBridge().messageEvent('IPreMessageDeletePrevent', deletedMsg); + const prevent = await bridges.getListenerBridge().messageEvent(AppEvents.IPreMessageDeletePrevent, deletedMsg); if (prevent) { throw new Meteor.Error('error-app-prevented-deleting', 'A Rocket.Chat App prevented the message deleting.'); } @@ -95,7 +95,7 @@ export async function deleteMessage(message: IMessage, user: IUser): Promise { const originalMessage = originalMsg || (await Messages.findOneById(message._id)); + if (!originalMessage) { + throw new Error('Invalid message ID.'); + } - // For the Rocket.Chat Apps :) - if (message && Apps && Apps.isLoaded()) { - const appMessage = Object.assign({}, originalMessage, message); + let messageData: IMessage = Object.assign({}, originalMessage, message); - const prevent = await Apps.getBridges()?.getListenerBridge().messageEvent('IPreMessageUpdatedPrevent', appMessage); + // For the Rocket.Chat Apps :) + if (message && Apps.self && Apps.isLoaded()) { + const prevent = await Apps.getBridges().getListenerBridge().messageEvent(AppEvents.IPreMessageUpdatedPrevent, messageData); if (prevent) { throw new Meteor.Error('error-app-prevented-updating', 'A Rocket.Chat App prevented the message updating.'); } - let result; - result = await Apps.getBridges()?.getListenerBridge().messageEvent('IPreMessageUpdatedExtend', appMessage); - result = await Apps.getBridges()?.getListenerBridge().messageEvent('IPreMessageUpdatedModify', result); + let result = await Apps.getBridges().getListenerBridge().messageEvent(AppEvents.IPreMessageUpdatedExtend, messageData); + result = await Apps.getBridges().getListenerBridge().messageEvent(AppEvents.IPreMessageUpdatedModify, result); if (typeof result === 'object') { - message = Object.assign(appMessage, result); + Object.assign(messageData, result); } } // If we keep history of edits, insert a new message to store history information if (settings.get('Message_KeepHistory')) { - await Messages.cloneAndSaveAsHistoryById(message._id, user as Required>); + await Messages.cloneAndSaveAsHistoryById(messageData._id, user as Required>); } - Object.assign, Omit>(message, { + Object.assign(messageData, { editedAt: new Date(), editedBy: { _id: user._id, @@ -48,17 +50,16 @@ export const updateMessage = async function ( }, }); - parseUrlsInMessage(message, previewUrls); + parseUrlsInMessage(messageData, previewUrls); - const room = await Rooms.findOneById(message.rid); + const room = await Rooms.findOneById(messageData.rid); if (!room) { return; } - // TODO remove type cast - message = await Message.beforeSave({ message: message as IMessage, room, user }); + messageData = await Message.beforeSave({ message: messageData, room, user }); - const { _id, ...editedMessage } = message; + const { _id, ...editedMessage } = messageData; if (!editedMessage.msg) { delete editedMessage.md; @@ -75,10 +76,10 @@ export const updateMessage = async function ( }, ); - if (Apps?.isLoaded()) { + if (Apps.self?.isLoaded()) { // This returns a promise, but it won't mutate anything about the message // so, we don't really care if it is successful or fails - void Apps.getBridges()?.getListenerBridge().messageEvent('IPostMessageUpdated', message); + void Apps.getBridges()?.getListenerBridge().messageEvent(AppEvents.IPostMessageUpdated, messageData); } setImmediate(async () => { diff --git a/apps/meteor/app/lib/server/lib/loginErrorMessageOverride.js b/apps/meteor/app/lib/server/lib/loginErrorMessageOverride.js deleted file mode 100644 index 4e054b81b2cf..000000000000 --- a/apps/meteor/app/lib/server/lib/loginErrorMessageOverride.js +++ /dev/null @@ -1,14 +0,0 @@ -// Do not disclose if user exists when password is invalid -import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; - -const { _runLoginHandlers } = Accounts; -Accounts._runLoginHandlers = function (methodInvocation, options) { - const result = _runLoginHandlers.call(Accounts, methodInvocation, options); - - if (result.error && result.error.reason === 'Incorrect password') { - result.error = new Meteor.Error(403, 'User not found'); - } - - return result; -}; diff --git a/apps/meteor/app/lib/server/lib/loginErrorMessageOverride.ts b/apps/meteor/app/lib/server/lib/loginErrorMessageOverride.ts new file mode 100644 index 000000000000..e2a6e0d10581 --- /dev/null +++ b/apps/meteor/app/lib/server/lib/loginErrorMessageOverride.ts @@ -0,0 +1,16 @@ +// Do not disclose if user exists when password is invalid +import { Accounts } from 'meteor/accounts-base'; +import { Meteor } from 'meteor/meteor'; + +const { _runLoginHandlers } = Accounts; + +Accounts._options.ambiguousErrorMessages = true; +Accounts._runLoginHandlers = async function (methodInvocation, options) { + const result = await _runLoginHandlers.call(Accounts, methodInvocation, options); + + if (result.error instanceof Meteor.Error) { + result.error = new Meteor.Error(401, 'User not found'); + } + + return result; +}; diff --git a/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts b/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts index ed9929622c6d..2d651950da19 100644 --- a/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts +++ b/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts @@ -1,3 +1,4 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; import { Users } from '@rocket.chat/models'; import { SHA256 } from '@rocket.chat/sha256'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; @@ -5,7 +6,6 @@ import { Accounts } from 'meteor/accounts-base'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { AppEvents, Apps } from '../../../../ee/server/apps/orchestrator'; import { trim } from '../../../../lib/utils/stringUtils'; import { settings } from '../../../settings/server'; import { deleteUser } from '../functions/deleteUser'; @@ -66,7 +66,7 @@ Meteor.methods({ await deleteUser(uid, confirmRelinquish); // App IPostUserDeleted event hook - await Apps.triggerEvent(AppEvents.IPostUserDeleted, { user }); + await Apps.self?.triggerEvent(AppEvents.IPostUserDeleted, { user }); return true; }, diff --git a/apps/meteor/app/livechat/imports/server/rest/appearance.ts b/apps/meteor/app/livechat/imports/server/rest/appearance.ts index 5a0a884d97b2..55607551ec3a 100644 --- a/apps/meteor/app/livechat/imports/server/rest/appearance.ts +++ b/apps/meteor/app/livechat/imports/server/rest/appearance.ts @@ -1,3 +1,4 @@ +import type { ISettingSelectOption } from '@rocket.chat/core-typings'; import { Settings } from '@rocket.chat/models'; import { isPOSTLivechatAppearanceParams } from '@rocket.chat/rest-typings'; @@ -45,6 +46,10 @@ API.v1.addRoute( 'Livechat_name_field_registration_form', 'Livechat_email_field_registration_form', 'Livechat_registration_form_message', + 'Livechat_hide_watermark', + 'Livechat_background', + 'Livechat_widget_position', + 'Livechat_hide_system_messages', ]; const valid = settings.every((setting) => validSettingList.includes(setting._id)); @@ -60,6 +65,10 @@ API.v1.addRoute( return; } + if (dbSetting.type === 'multiSelect' && (!Array.isArray(setting.value) || !validateValues(setting.value, dbSetting.values))) { + return; + } + switch (dbSetting?.type) { case 'boolean': return { @@ -91,7 +100,11 @@ API.v1.addRoute( }, ); -function coerceInt(value: string | number | boolean): number { +function validateValues(values: string[], allowedValues: ISettingSelectOption[] = []): boolean { + return values.every((value) => allowedValues.some((allowedValue) => allowedValue.key === value)); +} + +function coerceInt(value: string | number | boolean | string[]): number { if (typeof value === 'number') { return value; } @@ -100,6 +113,10 @@ function coerceInt(value: string | number | boolean): number { return 0; } + if (Array.isArray(value)) { + return 0; + } + const parsedValue = parseInt(value, 10); if (Number.isNaN(parsedValue)) { return 0; diff --git a/apps/meteor/app/livechat/server/api/lib/appearance.ts b/apps/meteor/app/livechat/server/api/lib/appearance.ts index d0a0a3df6b17..785413ead9d1 100644 --- a/apps/meteor/app/livechat/server/api/lib/appearance.ts +++ b/apps/meteor/app/livechat/server/api/lib/appearance.ts @@ -24,6 +24,10 @@ export async function findAppearance(): Promise<{ appearance: ISetting[] }> { 'Livechat_email_field_registration_form', 'Livechat_registration_form_message', 'Livechat_conversation_finished_text', + 'Livechat_hide_watermark', + 'Livechat_background', + 'Livechat_widget_position', + 'Livechat_hide_system_messages', ], }, }; diff --git a/apps/meteor/app/livechat/server/api/lib/livechat.ts b/apps/meteor/app/livechat/server/api/lib/livechat.ts index 24f5f8cc7c36..00229dae2de5 100644 --- a/apps/meteor/app/livechat/server/api/lib/livechat.ts +++ b/apps/meteor/app/livechat/server/api/lib/livechat.ts @@ -170,12 +170,17 @@ export async function settings({ businessUnit = '' }: { businessUnit?: string } limitTextLength: initSettings.Livechat_enable_message_character_limit && (initSettings.Livechat_message_character_limit || initSettings.Message_MaxAllowedSize), + hiddenSystemMessages: initSettings.Livechat_hide_system_messages, + livechatLogo: initSettings.Assets_livechat_widget_logo, + hideWatermark: initSettings.Livechat_hide_watermark || false, }, theme: { title: initSettings.Livechat_title, color: initSettings.Livechat_title_color, offlineTitle: initSettings.Livechat_offline_title, offlineColor: initSettings.Livechat_offline_title_color, + position: initSettings.Livechat_widget_position || 'right', + background: initSettings.Livechat_background, actionLinks: { webrtc: [ { diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index 3d78280c5109..9c19f5bbdec8 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -1,4 +1,4 @@ -import type { ILivechatVisitor, IRoom } from '@rocket.chat/core-typings'; +import type { ILivechatCustomField, ILivechatVisitor, IRoom } from '@rocket.chat/core-typings'; import { LivechatVisitors as VisitorsRaw, LivechatCustomField, LivechatRooms } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -68,16 +68,46 @@ API.v1.addRoute('livechat/visitor', { ); } - if (customFields && Array.isArray(customFields)) { - for await (const field of customFields) { - const customField = await LivechatCustomField.findOneById(field.key); - if (!customField) { - continue; - } - const { key, value, overwrite } = field; - if (customField.scope === 'visitor' && !(await VisitorsRaw.updateLivechatDataByToken(token, key, value, overwrite))) { - return API.v1.failure(); - } + if (customFields && Array.isArray(customFields) && customFields.length > 0) { + const keys = customFields.map((field) => field.key); + const errors: string[] = []; + + const processedKeys = await Promise.all( + await LivechatCustomField.findByIdsAndScope>(keys, 'visitor', { + projection: { _id: 1 }, + }) + .map(async (field) => { + const customField = customFields.find((f) => f.key === field._id); + if (!customField) { + return; + } + + const { key, value, overwrite } = customField; + // TODO: Change this to Bulk update + if (!(await VisitorsRaw.updateLivechatDataByToken(token, key, value, overwrite))) { + errors.push(key); + } + + return key; + }) + .toArray(), + ); + + if (processedKeys.length !== keys.length) { + LivechatTyped.logger.warn({ + msg: 'Some custom fields were not processed', + visitorId, + missingKeys: keys.filter((key) => !processedKeys.includes(key)), + }); + } + + if (errors.length > 0) { + LivechatTyped.logger.error({ + msg: 'Error updating custom fields', + visitorId, + errors, + }); + throw new Error('error-updating-custom-fields'); } visitor = await VisitorsRaw.findOneEnabledById(visitorId, {}); diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index bf575d9e346d..453869d4425a 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -1,3 +1,4 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; import { LivechatTransferEventType } from '@rocket.chat/apps-engine/definition/livechat'; import { api, Message, Omnichannel } from '@rocket.chat/core-services'; import type { @@ -30,7 +31,6 @@ import { import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { Apps, AppEvents } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { validateEmail as validatorFunc } from '../../../../lib/emailValidator'; import { i18n } from '../../../../server/lib/i18n'; @@ -273,7 +273,7 @@ export const removeAgentFromSubscription = async (rid: string, { _id, username } await Message.saveSystemMessage('ul', rid, username || '', { _id: user._id, username: user.username, name: user.name }); setImmediate(() => { - void Apps.triggerEvent(AppEvents.IPostLivechatAgentUnassigned, { room, user }); + void Apps.self?.triggerEvent(AppEvents.IPostLivechatAgentUnassigned, { room, user }); }); }; @@ -452,7 +452,7 @@ export const forwardRoomToAgent = async (room: IOmnichannelRoom, transferData: T } setImmediate(() => { - void Apps.triggerEvent(AppEvents.IPostLivechatRoomTransferred, { + void Apps.self?.triggerEvent(AppEvents.IPostLivechatRoomTransferred, { type: LivechatTransferEventType.AGENT, room: rid, from: oldServedBy?._id, @@ -482,7 +482,7 @@ export const updateChatDepartment = async ({ ]); setImmediate(() => { - void Apps.triggerEvent(AppEvents.IPostLivechatRoomTransferred, { + void Apps.self?.triggerEvent(AppEvents.IPostLivechatRoomTransferred, { type: LivechatTransferEventType.DEPARTMENT, room: rid, from: oldDepartmentId, @@ -539,10 +539,24 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi agent = { agentId, username }; } - if (!RoutingManager.getConfig()?.autoAssignAgent || !(await Omnichannel.isWithinMACLimit(room))) { + const department = await LivechatDepartment.findOneById< + Pick + >(departmentId, { + projection: { + allowReceiveForwardOffline: 1, + fallbackForwardDepartment: 1, + name: 1, + }, + }); + + if ( + !RoutingManager.getConfig()?.autoAssignAgent || + !(await Omnichannel.isWithinMACLimit(room)) || + (department?.allowReceiveForwardOffline && !(await LivechatTyped.checkOnlineAgents(departmentId))) + ) { logger.debug(`Room ${room._id} will be on department queue`); await LivechatTyped.saveTransferHistory(room, transferData); - return RoutingManager.unassignAgent(inquiry, departmentId); + return RoutingManager.unassignAgent(inquiry, departmentId, true); } // Fake the department to forward the inquiry - Case the forward process does not success @@ -559,11 +573,6 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi const { servedBy, chatQueued } = roomTaken; if (!chatQueued && oldServedBy && servedBy && oldServedBy._id === servedBy._id) { - const department = departmentId - ? await LivechatDepartment.findOneById>(departmentId, { - projection: { fallbackForwardDepartment: 1, name: 1 }, - }) - : null; if (!department?.fallbackForwardDepartment?.length) { logger.debug(`Cannot forward room ${room._id}. Chat assigned to agent ${servedBy._id} (Previous was ${oldServedBy._id})`); throw new Error('error-no-agents-online-in-department'); diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index d9e1fe84d854..dc5aa506f405 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -1,6 +1,7 @@ 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, @@ -42,7 +43,6 @@ import moment from 'moment-timezone'; import type { Filter, FindCursor, UpdateFilter } from 'mongodb'; import UAParser from 'ua-parser-js'; -import { Apps, AppEvents } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { trim } from '../../../../lib/utils/stringUtils'; import { i18n } from '../../../../server/lib/i18n'; @@ -329,8 +329,8 @@ class LivechatClass { * @deprecated the `AppEvents.ILivechatRoomClosedHandler` event will be removed * in the next major version of the Apps-Engine */ - void Apps.getBridges()?.getListenerBridge().livechatEvent(AppEvents.ILivechatRoomClosedHandler, newRoom); - void Apps.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatRoomClosed, newRoom); + void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.ILivechatRoomClosedHandler, newRoom); + void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatRoomClosed, newRoom); }); if (process.env.TEST_MODE) { await callbacks.run('livechat.closeRoom', { @@ -1126,6 +1126,11 @@ class LivechatClass { 'Livechat_show_agent_info', 'Livechat_clear_local_storage_when_chat_ended', 'Livechat_history_monitor_type', + 'Livechat_hide_system_messages', + 'Livechat_widget_position', + 'Livechat_background', + 'Assets_livechat_widget_logo', + 'Livechat_hide_watermark', ] as const; type SettingTypes = (typeof validSettings)[number] | 'Livechat_Show_Connecting'; @@ -1421,7 +1426,7 @@ class LivechatClass { const ret = await LivechatVisitors.saveGuestById(_id, updateData); setImmediate(() => { - void Apps.triggerEvent(AppEvents.IPostLivechatGuestSaved, _id); + void Apps.self?.triggerEvent(AppEvents.IPostLivechatGuestSaved, _id); }); return ret; @@ -1787,7 +1792,7 @@ class LivechatClass { await LivechatRooms.saveRoomById(roomData); setImmediate(() => { - void Apps.triggerEvent(AppEvents.IPostLivechatRoomSaved, roomData._id); + void Apps.self?.triggerEvent(AppEvents.IPostLivechatRoomSaved, roomData._id); }); if (guestData?.name?.trim().length) { diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index 4569f3da42b8..8be71aa4c991 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -1,3 +1,4 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; import { Omnichannel } from '@rocket.chat/core-services'; import type { ILivechatInquiryRecord, ILivechatVisitor, IMessage, IOmnichannelRoom, SelectedAgent } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; @@ -5,7 +6,6 @@ import { LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { Apps, AppEvents } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { checkServiceStatus, createLivechatRoom, createLivechatInquiry } from './Helper'; import { RoutingManager } from './RoutingManager'; @@ -105,7 +105,7 @@ export const QueueManager: queueManager = { throw new Error('inquiry-not-found'); } - void Apps.triggerEvent(AppEvents.IPostLivechatRoomStarted, room); + void Apps.self?.triggerEvent(AppEvents.IPostLivechatRoomStarted, room); await LivechatRooms.updateRoomCount(); await queueInquiry(inquiry, agent); diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index f1fe1d506a8a..19437d800ee2 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -1,3 +1,4 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; import { Message, Omnichannel } from '@rocket.chat/core-services'; import type { ILivechatInquiryRecord, @@ -16,7 +17,6 @@ import { LivechatInquiry, LivechatRooms, Subscriptions, Rooms, Users } from '@ro import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { Apps, AppEvents } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { settings } from '../../../settings/server'; import { @@ -46,7 +46,7 @@ type Routing = { options?: { clientAction?: boolean; forwardingToDepartment?: { oldDepartmentId?: string; transferData?: any } }, ): Promise<(IOmnichannelRoom & { chatQueued?: boolean }) | null | void>; assignAgent(inquiry: InquiryWithAgentInfo, agent: SelectedAgent): Promise; - unassignAgent(inquiry: ILivechatInquiryRecord, departmentId?: string): Promise; + unassignAgent(inquiry: ILivechatInquiryRecord, departmentId?: string, shouldQueue?: boolean): Promise; takeInquiry( inquiry: Omit< ILivechatInquiryRecord, @@ -154,11 +154,11 @@ export const RoutingManager: Routing = { await dispatchAgentDelegated(rid, agent.agentId); logger.debug(`Agent ${agent.agentId} assigned to inquriy ${inquiry._id}. Instances notified`); - void Apps.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatAgentAssigned, { room, user }); + void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatAgentAssigned, { room, user }); return inquiry; }, - async unassignAgent(inquiry, departmentId) { + async unassignAgent(inquiry, departmentId, shouldQueue = false) { const { rid, department } = inquiry; const room = await LivechatRooms.findOneById(rid); @@ -181,6 +181,10 @@ export const RoutingManager: Routing = { const { servedBy } = room; + if (shouldQueue) { + await LivechatInquiry.queueInquiry(inquiry._id); + } + if (servedBy) { await LivechatRooms.removeAgentByRoomId(rid); await this.removeAllRoomSubscriptions(room); diff --git a/apps/meteor/app/livechat/server/methods/saveDepartment.ts b/apps/meteor/app/livechat/server/methods/saveDepartment.ts index dd83a294cb0e..45b3b2ec2168 100644 --- a/apps/meteor/app/livechat/server/methods/saveDepartment.ts +++ b/apps/meteor/app/livechat/server/methods/saveDepartment.ts @@ -21,6 +21,7 @@ declare module '@rocket.chat/ui-contexts' { chatClosingTags?: string[]; fallbackForwardDepartment?: string; departmentsAllowedToForward?: string[]; + allowReceiveForwardOffline?: boolean; }, departmentAgents?: | { diff --git a/apps/meteor/app/livechat/server/statistics/LivechatAgentActivityMonitor.ts b/apps/meteor/app/livechat/server/statistics/LivechatAgentActivityMonitor.ts index 76e9e04f1a24..b82dcc30411d 100644 --- a/apps/meteor/app/livechat/server/statistics/LivechatAgentActivityMonitor.ts +++ b/apps/meteor/app/livechat/server/statistics/LivechatAgentActivityMonitor.ts @@ -52,21 +52,19 @@ export class LivechatAgentActivityMonitor { // TODO use service event socket.connected instead Meteor.onConnection((connection: unknown) => this._handleMeteorConnection(connection as ISocketConnection)); callbacks.add('livechat.agentStatusChanged', this._handleAgentStatusChanged); - callbacks.add('livechat.setUserStatusLivechat', async (...args) => { + callbacks.add('livechat.setUserStatusLivechat', (...args) => { return this._handleUserStatusLivechatChanged(...args); }); this._started = true; } async _startMonitoring(): Promise { - await this.scheduler.add(this._name, '0 0 * * *', async () => this._updateActiveSessions()); + await this.scheduler.add(this._name, '0 0 * * *', () => this._updateActiveSessions()); } async _updateActiveSessions(): Promise { - const openLivechatAgentSessions = await LivechatAgentActivity.findOpenSessions(); - if (!(await openLivechatAgentSessions.count())) { - return; - } + const openLivechatAgentSessions = LivechatAgentActivity.findOpenSessions(); + const today = moment(new Date()); const startedAt = new Date(today.year(), today.month(), today.date()); for await (const session of openLivechatAgentSessions) { @@ -74,15 +72,11 @@ export class LivechatAgentActivityMonitor { const stoppedAt = new Date(startDate.year(), startDate.month(), startDate.date(), 23, 59, 59); const data = { ...formatDate(startDate.toDate()), agentId: session.agentId }; const availableTime = moment(stoppedAt).diff(moment(new Date(session.lastStartedAt)), 'seconds'); - await LivechatAgentActivity.updateLastStoppedAt({ - ...data, - availableTime, - lastStoppedAt: stoppedAt, - }); - await LivechatAgentActivity.updateServiceHistory({ - ...data, - serviceHistory: { startedAt: session.lastStartedAt, stoppedAt }, - }); + + await Promise.all([ + LivechatAgentActivity.updateLastStoppedAt({ ...data, availableTime, lastStoppedAt: stoppedAt }), + LivechatAgentActivity.updateServiceHistory({ ...data, serviceHistory: { startedAt: session.lastStartedAt, stoppedAt } }), + ]); await this._createOrUpdateSession(session.agentId, startedAt); } } @@ -96,7 +90,9 @@ export class LivechatAgentActivityMonitor { if (!session) { return; } - const user = await Users.findOneById(session.userId); + const user = await Users.findOneById>(session.userId, { + projection: { _id: 1, status: 1, statusLivechat: 1 }, + }); if (user && user.status !== 'offline' && user.statusLivechat === 'available') { await this._createOrUpdateSession(user._id); } @@ -112,7 +108,7 @@ export class LivechatAgentActivityMonitor { return; } - const user = await Users.findOneById(userId); + const user = await Users.findOneById>(userId, { projection: { statusLivechat: 1 } }); if (!user || user.statusLivechat !== 'available') { return; } @@ -129,7 +125,7 @@ export class LivechatAgentActivityMonitor { return; } - const user = await Users.findOneById(userId); + const user = await Users.findOneById>(userId, { projection: { status: 1 } }); if (user && user.status === 'offline') { return; } @@ -158,16 +154,13 @@ export class LivechatAgentActivityMonitor { const stoppedAt = new Date(); const availableTime = moment(stoppedAt).diff(moment(new Date(livechatSession.lastStartedAt)), 'seconds'); - await LivechatAgentActivity.updateLastStoppedAt({ - agentId, - date, - availableTime, - lastStoppedAt: stoppedAt, - }); - await LivechatAgentActivity.updateServiceHistory({ - agentId, - date, - serviceHistory: { startedAt: livechatSession.lastStartedAt, stoppedAt }, - }); + await Promise.all([ + LivechatAgentActivity.updateLastStoppedAt({ agentId, date, availableTime, lastStoppedAt: stoppedAt }), + LivechatAgentActivity.updateServiceHistory({ + agentId, + date, + serviceHistory: { startedAt: livechatSession.lastStartedAt, stoppedAt }, + }), + ]); } } diff --git a/apps/meteor/app/mailer/server/api.ts b/apps/meteor/app/mailer/server/api.ts index b50fdfd26a2a..e562fc8e7b39 100644 --- a/apps/meteor/app/mailer/server/api.ts +++ b/apps/meteor/app/mailer/server/api.ts @@ -1,3 +1,4 @@ +import { AppEvents, Apps } from '@rocket.chat/apps'; import type { ISetting } from '@rocket.chat/core-typings'; import { Settings } from '@rocket.chat/models'; import { escapeHTML } from '@rocket.chat/string-helpers'; @@ -7,7 +8,6 @@ import { Meteor } from 'meteor/meteor'; import stripHtml from 'string-strip-html'; import _ from 'underscore'; -import { Apps } from '../../../ee/server/apps'; import { validateEmail } from '../../../lib/emailValidator'; import { strLeft, strRightBack } from '../../../lib/utils/stringUtils'; import { i18n } from '../../../server/lib/i18n'; @@ -170,7 +170,7 @@ export const sendNoWrap = async ({ const email = { to, from, replyTo, subject, html, text, headers }; - const eventResult = await Apps.triggerEvent('IPreEmailSent', { email }); + const eventResult = await Apps.self?.triggerEvent(AppEvents.IPreEmailSent, { email }); setImmediate(() => Email.sendAsync(eventResult || email).catch((e) => console.error(e))); }; diff --git a/apps/meteor/app/message-pin/server/pinMessage.ts b/apps/meteor/app/message-pin/server/pinMessage.ts index 1ed0a172028b..dc17a75a0192 100644 --- a/apps/meteor/app/message-pin/server/pinMessage.ts +++ b/apps/meteor/app/message-pin/server/pinMessage.ts @@ -1,3 +1,4 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; import { Message } from '@rocket.chat/core-services'; import { isQuoteAttachment, isRegisterUser } from '@rocket.chat/core-typings'; import type { IMessage, MessageAttachment, MessageQuoteAttachment } from '@rocket.chat/core-typings'; @@ -6,7 +7,6 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { Apps, AppEvents } from '../../../ee/server/apps/orchestrator'; import { isTruthy } from '../../../lib/isTruthy'; import { broadcastMessageFromData } from '../../../server/modules/watchers/lib/messages'; import { canAccessRoomAsync, roomAccessAttributes } from '../../authorization/server'; @@ -129,7 +129,7 @@ Meteor.methods({ } // App IPostMessagePinned event hook - await Apps.triggerEvent(AppEvents.IPostMessagePinned, originalMessage, await Meteor.userAsync(), originalMessage.pinned); + await Apps.self?.triggerEvent(AppEvents.IPostMessagePinned, originalMessage, await Meteor.userAsync(), originalMessage.pinned); const msgId = await Message.saveSystemMessage('message_pinned', originalMessage.rid, '', me, { attachments: [ @@ -216,7 +216,7 @@ Meteor.methods({ } // App IPostMessagePinned event hook - await Apps.triggerEvent(AppEvents.IPostMessagePinned, originalMessage, await Meteor.userAsync(), originalMessage.pinned); + await Apps.self?.triggerEvent(AppEvents.IPostMessagePinned, originalMessage, await Meteor.userAsync(), originalMessage.pinned); await Messages.setPinnedByIdAndUserId(originalMessage._id, originalMessage.pinnedBy, originalMessage.pinned); if (settings.get('Message_Read_Receipt_Store_Users')) { diff --git a/apps/meteor/app/message-star/server/starMessage.ts b/apps/meteor/app/message-star/server/starMessage.ts index 8f025d920057..7ac8fd619d31 100644 --- a/apps/meteor/app/message-star/server/starMessage.ts +++ b/apps/meteor/app/message-star/server/starMessage.ts @@ -1,9 +1,9 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; import type { IMessage } from '@rocket.chat/core-typings'; import { Messages, Subscriptions, Rooms } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; -import { Apps, AppEvents } from '../../../ee/server/apps/orchestrator'; import { broadcastMessageFromData } from '../../../server/modules/watchers/lib/messages'; import { canAccessRoomAsync, roomAccessAttributes } from '../../authorization/server'; import { isTheLastMessage } from '../../lib/server/functions/isTheLastMessage'; @@ -57,7 +57,7 @@ Meteor.methods({ await Rooms.updateLastMessageStar(room._id, uid, message.starred); } - await Apps.triggerEvent(AppEvents.IPostMessageStarred, message, await Meteor.userAsync(), message.starred); + await Apps.self?.triggerEvent(AppEvents.IPostMessageStarred, message, await Meteor.userAsync(), message.starred); await Messages.updateUserStarById(message._id, uid, message.starred); diff --git a/apps/meteor/app/reactions/server/setReaction.ts b/apps/meteor/app/reactions/server/setReaction.ts index 27fe4d36a053..36eaab695512 100644 --- a/apps/meteor/app/reactions/server/setReaction.ts +++ b/apps/meteor/app/reactions/server/setReaction.ts @@ -1,3 +1,4 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; import { api } from '@rocket.chat/core-services'; import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { Messages, EmojiCustom, Rooms, Users } from '@rocket.chat/models'; @@ -5,7 +6,6 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; -import { AppEvents, Apps } from '../../../ee/server/apps/orchestrator'; import { callbacks } from '../../../lib/callbacks'; import { i18n } from '../../../server/lib/i18n'; import { broadcastMessageFromData } from '../../../server/modules/watchers/lib/messages'; @@ -106,7 +106,7 @@ async function setReaction(room: IRoom, user: IUser, message: IMessage, reaction isReacted = true; } - await Apps.triggerEvent(AppEvents.IPostMessageReacted, message, user, reaction, isReacted); + await Apps.self?.triggerEvent(AppEvents.IPostMessageReacted, message, user, reaction, isReacted); void broadcastMessageFromData({ id: message._id, diff --git a/apps/meteor/app/statistics/server/lib/getAppsStatistics.js b/apps/meteor/app/statistics/server/lib/getAppsStatistics.js index 6337b287506a..1d84bead3e85 100644 --- a/apps/meteor/app/statistics/server/lib/getAppsStatistics.js +++ b/apps/meteor/app/statistics/server/lib/getAppsStatistics.js @@ -1,17 +1,18 @@ +import { Apps } from '@rocket.chat/apps'; import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; -import { Apps } from '../../../../ee/server/apps'; import { Info } from '../../../utils/rocketchat.info'; export function getAppsStatistics() { return { engineVersion: Info.marketplaceApiVersion, - totalInstalled: Apps.isInitialized() && Apps.getManager().get().length, - totalActive: Apps.isInitialized() && Apps.getManager().get({ enabled: true }).length, + totalInstalled: (Apps.self?.isInitialized() && Apps.getManager().get().length) ?? 0, + totalActive: (Apps.self?.isInitialized() && Apps.getManager().get({ enabled: true }).length) ?? 0, totalFailed: - Apps.isInitialized() && - Apps.getManager() - .get({ disabled: true }) - .filter(({ app: { status } }) => status !== AppStatus.MANUALLY_DISABLED).length, + (Apps.self?.isInitialized() && + Apps.getManager() + .get({ disabled: true }) + .filter(({ app: { status } }) => status !== AppStatus.MANUALLY_DISABLED).length) ?? + 0, }; } diff --git a/apps/meteor/app/threads/server/methods/followMessage.ts b/apps/meteor/app/threads/server/methods/followMessage.ts index cede3dda33a7..05650d0ad2ef 100644 --- a/apps/meteor/app/threads/server/methods/followMessage.ts +++ b/apps/meteor/app/threads/server/methods/followMessage.ts @@ -1,10 +1,10 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; import type { IMessage } from '@rocket.chat/core-typings'; import { Messages } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { Apps, AppEvents } from '../../../../ee/server/apps/orchestrator'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { RateLimiter } from '../../../lib/server'; import { settings } from '../../../settings/server'; @@ -44,7 +44,7 @@ Meteor.methods({ const followResult = await follow({ tmid: message.tmid || message._id, uid }); const isFollowed = true; - await Apps.triggerEvent(AppEvents.IPostMessageFollowed, message, await Meteor.userAsync(), isFollowed); + await Apps.self?.triggerEvent(AppEvents.IPostMessageFollowed, message, await Meteor.userAsync(), isFollowed); return followResult; }, diff --git a/apps/meteor/app/threads/server/methods/unfollowMessage.ts b/apps/meteor/app/threads/server/methods/unfollowMessage.ts index c5dad1233173..afc9206b038f 100644 --- a/apps/meteor/app/threads/server/methods/unfollowMessage.ts +++ b/apps/meteor/app/threads/server/methods/unfollowMessage.ts @@ -1,10 +1,10 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; import type { IMessage } from '@rocket.chat/core-typings'; import { Messages } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { Apps, AppEvents } from '../../../../ee/server/apps/orchestrator'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { RateLimiter } from '../../../lib/server'; import { settings } from '../../../settings/server'; @@ -44,7 +44,7 @@ Meteor.methods({ const unfollowResult = await unfollow({ rid: message.rid, tmid: message.tmid || message._id, uid }); const isFollowed = false; - await Apps.triggerEvent(AppEvents.IPostMessageFollowed, message, await Meteor.userAsync(), isFollowed); + await Apps.self?.triggerEvent(AppEvents.IPostMessageFollowed, message, await Meteor.userAsync(), isFollowed); return unfollowResult; }, diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index f2031f21f05d..4eb357fe9ee6 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "6.7.0-develop" + "version": "6.8.0-develop" } diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarDialog.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarDialog.tsx index 087149bd4a4e..c3421f3fc9d3 100644 --- a/apps/meteor/client/components/Contextualbar/ContextualbarDialog.tsx +++ b/apps/meteor/client/components/Contextualbar/ContextualbarDialog.tsx @@ -1,4 +1,5 @@ import { Contextualbar } from '@rocket.chat/fuselage'; +import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; import { useLayoutSizes, useLayoutContextualBarPosition } from '@rocket.chat/ui-contexts'; import type { ComponentProps, KeyboardEvent } from 'react'; import React, { useCallback, useRef } from 'react'; @@ -6,6 +7,7 @@ import type { AriaDialogProps } from 'react-aria'; import { FocusScope, useDialog } from 'react-aria'; import { useRoomToolbox } from '../../views/room/contexts/RoomToolboxContext'; +import ContextualbarResizable from './ContextualbarResizable'; type ContextualbarDialogProps = AriaDialogProps & ComponentProps; @@ -38,7 +40,16 @@ const ContextualbarDialog = (props: ContextualbarDialogProps) => { return ( - + + + + + + + + + + ); }; diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarResizable.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarResizable.tsx new file mode 100644 index 000000000000..05dd9bf0cf2e --- /dev/null +++ b/apps/meteor/client/components/Contextualbar/ContextualbarResizable.tsx @@ -0,0 +1,40 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Palette, Box } from '@rocket.chat/fuselage'; +import { useLocalStorage } from '@rocket.chat/fuselage-hooks'; +import { Resizable } from 're-resizable'; +import type { ComponentProps } from 'react'; +import React from 'react'; + +type ContextualbarResizableProps = { defaultWidth: string } & ComponentProps; + +const ContextualbarResizable = ({ defaultWidth, children, ...props }: ContextualbarResizableProps) => { + const [contextualbarWidth, setContextualbarWidth] = useLocalStorage('contextualbarWidth', defaultWidth); + const handleStyle = css` + height: 100%; + &:hover { + background-color: ${Palette.stroke['stroke-highlight']}; + } + `; + + return ( + { + setContextualbarWidth(elRef.style.width); + }} + defaultSize={{ + width: contextualbarWidth, + height: '100%', + }} + minWidth={defaultWidth} + maxWidth='50%' + minHeight='100%' + handleStyles={{ left: { width: '3px', zIndex: '5', left: 0 } }} + handleComponent={{ left: }} + > + {children} + + ); +}; + +export default ContextualbarResizable; diff --git a/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx b/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx index c191fb150873..e6bce31b0b87 100644 --- a/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx +++ b/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx @@ -32,6 +32,7 @@ type CreateDiscussionFormValues = { encrypted: boolean; usernames: Array; firstMessage: string; + topic: string; }; type CreateDiscussionProps = { @@ -49,6 +50,7 @@ const CreateDiscussion = ({ onClose, defaultParentRoom, parentMessageId, nameSug handleSubmit, control, watch, + register, } = useForm({ mode: 'onBlur', defaultValues: { @@ -57,6 +59,7 @@ const CreateDiscussion = ({ onClose, defaultParentRoom, parentMessageId, nameSug encrypted: false, usernames: [], firstMessage: '', + topic: '', }, }); @@ -72,21 +75,23 @@ const CreateDiscussion = ({ onClose, defaultParentRoom, parentMessageId, nameSug }, }); - const handleCreate = async ({ name, parentRoom, encrypted, usernames, firstMessage }: CreateDiscussionFormValues) => { + const handleCreate = async ({ name, parentRoom, encrypted, usernames, firstMessage, topic }: CreateDiscussionFormValues) => { createDiscussionMutation.mutate({ prid: defaultParentRoom || parentRoom, t_name: name, users: usernames, reply: encrypted ? undefined : firstMessage, + topic, ...(parentMessageId && { pmid: parentMessageId }), }); }; - const targetChannelField = useUniqueId(); - const encryptedField = useUniqueId(); - const discussionField = useUniqueId(); - const usersField = useUniqueId(); - const firstMessageField = useUniqueId(); + const parentRoomId = useUniqueId(); + const encryptedId = useUniqueId(); + const discussionNameId = useUniqueId(); + const membersId = useUniqueId(); + const firstMessageId = useUniqueId(); + const topicId = useUniqueId(); return ( {t('Discussion_description')} - + {t('Discussion_target_channel')} @@ -123,36 +128,26 @@ const CreateDiscussion = ({ onClose, defaultParentRoom, parentMessageId, nameSug onBlur={onBlur} onChange={onChange} value={value} - id={targetChannelField} - placeholder={t('Discussion_target_channel_description')} + id={parentRoomId} + placeholder={t('Search_options')} disabled={Boolean(defaultParentRoom)} aria-invalid={Boolean(errors.parentRoom)} aria-required='true' - aria-describedby={`${targetChannelField}-error`} + aria-describedby={`${parentRoomId}-error`} /> )} /> )} {errors.parentRoom && ( - + {errors.parentRoom.message} )} - - {t('Encrypted')} - } - /> - - - - - {t('Discussion_name')} + + {t('Name')} ( } /> )} /> {errors.name && ( - + {errors.name.message} )} - {t('Invite_Users')} + {t('Topic')} + + + + + {t('Displayed_next_to_name')} + + + + {t('Members')} ( )} /> - {t('Discussion_first_message_title')} + {t('Discussion_first_message_title')} ( )} /> - {encrypted && {t('Discussion_first_message_disabled_due_to_e2e')}} + {encrypted ? ( + {t('Discussion_first_message_disabled_due_to_e2e')} + ) : ( + {t('First_message_hint')} + )} + + + + {t('Encrypted')} + } + /> + + {encrypted ? ( + {t('Encrypted_messages', { roomType: 'discussion' })} + ) : ( + {t('Encrypted_messages_false')} + )} diff --git a/apps/meteor/client/components/InfoPanel/InfoPanelActionGroup.tsx b/apps/meteor/client/components/InfoPanel/InfoPanelActionGroup.tsx index 3bf9f7e33c70..00af64b0fa61 100644 --- a/apps/meteor/client/components/InfoPanel/InfoPanelActionGroup.tsx +++ b/apps/meteor/client/components/InfoPanel/InfoPanelActionGroup.tsx @@ -6,7 +6,7 @@ import Section from './InfoPanelSection'; const InfoPanelActionGroup: FC> = (props) => (
- +
); diff --git a/apps/meteor/client/components/modal/ModalBackdrop.tsx b/apps/meteor/client/components/ModalBackdrop.tsx similarity index 100% rename from apps/meteor/client/components/modal/ModalBackdrop.tsx rename to apps/meteor/client/components/ModalBackdrop.tsx diff --git a/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx b/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx index 5e65c43a957c..977e298e638e 100644 --- a/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx @@ -36,7 +36,15 @@ const ForwardChatModal = ({ const getUserData = useEndpoint('GET', '/v1/users.info'); const idleAgentsAllowedForForwarding = useSetting('Livechat_enabled_when_agent_idle') as boolean; - const { getValues, handleSubmit, register, setFocus, setValue, watch } = useForm(); + const { + getValues, + handleSubmit, + register, + setFocus, + setValue, + watch, + formState: { isSubmitting }, + } = useForm(); useEffect(() => { setFocus('comment'); @@ -71,7 +79,7 @@ const ForwardChatModal = ({ uid = user?._id; } - onForward(departmentId, uid, comment); + await onForward(departmentId, uid, comment); }, [getUserData, onForward], ); @@ -146,7 +154,7 @@ const ForwardChatModal = ({ - diff --git a/apps/meteor/client/components/connectionStatus/ConnectionStatusBar.tsx b/apps/meteor/client/components/connectionStatus/ConnectionStatusBar.tsx index 6046aecc0bae..5a26392b60b9 100644 --- a/apps/meteor/client/components/connectionStatus/ConnectionStatusBar.tsx +++ b/apps/meteor/client/components/connectionStatus/ConnectionStatusBar.tsx @@ -1,11 +1,37 @@ -import { Box, Icon } from '@rocket.chat/fuselage'; +import { css } from '@rocket.chat/css-in-js'; +import { Box, Button, Icon, Palette } from '@rocket.chat/fuselage'; import { useConnectionStatus } from '@rocket.chat/ui-contexts'; -import type { MouseEvent } from 'react'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useReconnectCountdown } from './useReconnectCountdown'; +const connectionStatusBarStyle = css` + color: ${Palette.statusColor['status-font-on-warning']}; + background-color: ${Palette.surface['surface-tint']}; + border-color: ${Palette.statusColor['status-font-on-warning']}; + + position: fixed; + z-index: 1000000; + + display: flex; + justify-content: space-between; + align-items: center; + + .rcx-connection-status-bar--wrapper { + display: flex; + align-items: center; + column-gap: 8px; + } + .rcx-connection-status-bar--content { + display: flex; + align-items: center; + column-gap: 8px; + } + .rcx-connection-status-bar--info { + color: ${Palette.text['font-default']}; + } +`; function ConnectionStatusBar() { const { connected, retryTime, status, reconnect } = useConnectionStatus(); const reconnectCountdown = useReconnectCountdown(retryTime, status); @@ -15,37 +41,32 @@ function ConnectionStatusBar() { return null; } - const handleRetryClick = (event: MouseEvent) => { - event.preventDefault(); - reconnect?.(); - }; - return ( - {' '} - - {t('meteor_status', { context: status })} - {status === 'waiting' && <> {t('meteor_status_reconnect_in', { count: reconnectCountdown })}} - {['waiting', 'offline'].includes(status) && ( - <> - {' '} - - {t('meteor_status_try_now', { context: status })} - - - )} - + + + + {t('meteor_status', { context: status })} + {['waiting', 'failed', 'offline'].includes(status) && ( + + {status === 'waiting' ? t('meteor_status_reconnect_in', { count: reconnectCountdown }) : t('meteor_status_try_again_later')} + + )} + + + ); } diff --git a/apps/meteor/client/components/message/uikit/UiKitMessageBlock.tsx b/apps/meteor/client/components/message/uikit/UiKitMessageBlock.tsx index 3b8570e0de22..dbdd8f4e731b 100644 --- a/apps/meteor/client/components/message/uikit/UiKitMessageBlock.tsx +++ b/apps/meteor/client/components/message/uikit/UiKitMessageBlock.tsx @@ -1,8 +1,8 @@ import type { IMessage, IRoom } from '@rocket.chat/core-typings'; -import { MessageBlock, Skeleton } from '@rocket.chat/fuselage'; +import { MessageBlock } from '@rocket.chat/fuselage'; import { UiKitComponent, UiKitMessage as UiKitMessageSurfaceRender, UiKitContext } from '@rocket.chat/fuselage-ui-kit'; import type { MessageSurfaceLayout } from '@rocket.chat/ui-kit'; -import React, { Suspense } from 'react'; +import React from 'react'; import { useMessageBlockContextValue } from '../../../uikit/hooks/useMessageBlockContextValue'; import GazzodownText from '../../GazzodownText'; @@ -20,9 +20,7 @@ const UiKitMessageBlock = ({ rid, mid, blocks }: UiKitMessageBlockProps) => { - }> - - + diff --git a/apps/meteor/client/components/message/variants/SystemMessage.tsx b/apps/meteor/client/components/message/variants/SystemMessage.tsx index d69b1ada10dc..9b1a82a156eb 100644 --- a/apps/meteor/client/components/message/variants/SystemMessage.tsx +++ b/apps/meteor/client/components/message/variants/SystemMessage.tsx @@ -85,7 +85,12 @@ const SystemMessage = ({ message, showUserAvatar, ...props }: SystemMessageProps {...triggerProps} > {getUserDisplayName(user.name, user.username, showRealName)} - {showUsername && @{user.username}} + {showUsername && ( + <> + {' '} + @{user.username} + + )} {messageType && ( import('../../views/teams/contextualBar/channels/TeamsChannels')); +const TeamsChannels = lazy(() => import('../../views/teams/contextualBar/channels')); export const useTeamChannelsRoomAction = () => { return useMemo( diff --git a/apps/meteor/client/meteorOverrides/login/google.ts b/apps/meteor/client/meteorOverrides/login/google.ts index 2742cade15d2..4e99ac3a281b 100644 --- a/apps/meteor/client/meteorOverrides/login/google.ts +++ b/apps/meteor/client/meteorOverrides/login/google.ts @@ -8,16 +8,6 @@ import { overrideLoginMethod, type LoginCallback } from '../../lib/2fa/overrideL import { wrapRequestCredentialFn } from '../../lib/wrapRequestCredentialFn'; import { createOAuthTotpLoginMethod } from './oauth'; -declare module 'meteor/accounts-base' { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace Accounts { - export const _options: { - restrictCreationByEmailDomain?: string | (() => string); - forbidClientAccountCreation?: boolean | undefined; - }; - } -} - declare module 'meteor/meteor' { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Meteor { diff --git a/apps/meteor/client/components/modal/ModalPortal.tsx b/apps/meteor/client/portals/ModalPortal.tsx similarity index 75% rename from apps/meteor/client/components/modal/ModalPortal.tsx rename to apps/meteor/client/portals/ModalPortal.tsx index 577f89e72103..d7c9ae9caa2d 100644 --- a/apps/meteor/client/components/modal/ModalPortal.tsx +++ b/apps/meteor/client/portals/ModalPortal.tsx @@ -2,16 +2,13 @@ import type { ReactElement, ReactNode } from 'react'; import React, { memo, useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; -import { createAnchor } from '../../lib/utils/createAnchor'; -import { deleteAnchor } from '../../lib/utils/deleteAnchor'; +import { createAnchor } from '../lib/utils/createAnchor'; +import { deleteAnchor } from '../lib/utils/deleteAnchor'; type ModalPortalProps = { children?: ReactNode; }; -/** - * @todo: move to portals folder - */ const ModalPortal = ({ children }: ModalPortalProps): ReactElement => { const [modalRoot] = useState(() => createAnchor('modal-root')); useEffect(() => (): void => deleteAnchor(modalRoot), [modalRoot]); diff --git a/apps/meteor/client/components/TooltipPortal.tsx b/apps/meteor/client/portals/TooltipPortal.tsx similarity index 87% rename from apps/meteor/client/components/TooltipPortal.tsx rename to apps/meteor/client/portals/TooltipPortal.tsx index 937f6ed879ca..2ee0830313c4 100644 --- a/apps/meteor/client/components/TooltipPortal.tsx +++ b/apps/meteor/client/portals/TooltipPortal.tsx @@ -6,7 +6,7 @@ import { createAnchor } from '../lib/utils/createAnchor'; import { deleteAnchor } from '../lib/utils/deleteAnchor'; const TooltipPortal: FC = ({ children }) => { - const [tooltipRoot] = useState(() => createAnchor('react-tooltip')); + const [tooltipRoot] = useState(() => createAnchor('tooltip-root')); useEffect(() => (): void => deleteAnchor(tooltipRoot), [tooltipRoot]); return <>{createPortal(children, tooltipRoot)}; }; diff --git a/apps/meteor/client/providers/TooltipProvider.tsx b/apps/meteor/client/providers/TooltipProvider.tsx index 0fc7c996b9f3..4cc9aa3a767c 100644 --- a/apps/meteor/client/providers/TooltipProvider.tsx +++ b/apps/meteor/client/providers/TooltipProvider.tsx @@ -4,7 +4,7 @@ import { TooltipContext } from '@rocket.chat/ui-contexts'; import type { FC, ReactNode } from 'react'; import React, { useEffect, useMemo, useRef, memo, useCallback, useState } from 'react'; -import TooltipPortal from '../components/TooltipPortal'; +import TooltipPortal from '../portals/TooltipPortal'; const TooltipProvider: FC = ({ children }) => { const lastAnchor = useRef(); diff --git a/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx b/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx index 2708e6e0a1e3..5738798f194e 100644 --- a/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx +++ b/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx @@ -30,10 +30,12 @@ import { useForm, Controller } from 'react-hook-form'; import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule'; import UserAutoCompleteMultipleFederated from '../../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; import { goToRoomById } from '../../../lib/utils/goToRoomById'; +import { useEncryptedRoomDescription } from '../hooks/useEncryptedRoomDescription'; type CreateChannelModalProps = { teamId?: string; onClose: () => void; + reload?: () => void; }; type CreateChannelModalPayload = { @@ -57,7 +59,7 @@ const getFederationHintKey = (licenseModule: ReturnType { +const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModalProps): ReactElement => { const t = useTranslation(); const canSetReadOnly = usePermissionWithScopedRoles('set-readonly', ['owner']); const e2eEnabled = useSetting('E2E_Enable'); @@ -68,6 +70,7 @@ const CreateChannelModal = ({ teamId = '', onClose }: CreateChannelModalProps): const canCreateChannel = usePermission('create-c'); const canCreatePrivateChannel = usePermission('create-p'); + const getEncryptedHint = useEncryptedRoomDescription('channel'); const channelNameRegex = useMemo(() => new RegExp(`^${namesValidation}$`), [namesValidation]); const federatedModule = useHasLicenseModule('federation'); @@ -110,7 +113,7 @@ const CreateChannelModal = ({ teamId = '', onClose }: CreateChannelModalProps): }, }); - const { isPrivate, broadcast, readOnly, federated } = watch(); + const { isPrivate, broadcast, readOnly, federated, encrypted } = watch(); useEffect(() => { if (!isPrivate) { @@ -137,7 +140,7 @@ const CreateChannelModal = ({ teamId = '', onClose }: CreateChannelModalProps): } if (!allowSpecialNames && !channelNameRegex.test(name)) { - return t('error-invalid-name'); + return t('Name_cannot_have_special_characters'); } const { exists } = await channelNameExists({ roomName: name }); @@ -171,6 +174,7 @@ const CreateChannelModal = ({ teamId = '', onClose }: CreateChannelModalProps): } dispatchToastMessage({ type: 'success', message: t('Room_has_been_created') }); + reload?.(); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } finally { @@ -209,7 +213,7 @@ const CreateChannelModal = ({ teamId = '', onClose }: CreateChannelModalProps): - {t('Channel_name')} + {t('Name')} } aria-invalid={errors.name ? 'true' : 'false'} - aria-describedby={`${nameId}-error`} + aria-describedby={`${nameId}-error ${nameId}-hint`} aria-required='true' /> @@ -231,13 +235,24 @@ const CreateChannelModal = ({ teamId = '', onClose }: CreateChannelModalProps): {errors.name.message} )} + {!allowSpecialNames && {t('No_spaces')}} {t('Topic')} - {t('Channel_what_is_this_channel_about')} + {t('Displayed_next_to_name')} + + + {t('Members')} + ( + + )} + /> @@ -258,7 +273,7 @@ const CreateChannelModal = ({ teamId = '', onClose }: CreateChannelModalProps): /> - {isPrivate ? t('Only_invited_users_can_acess_this_channel') : t('Everyone_can_access_this_channel')} + {isPrivate ? t('People_can_only_join_by_being_invited') : t('Anyone_can_access')} @@ -283,48 +298,46 @@ const CreateChannelModal = ({ teamId = '', onClose }: CreateChannelModalProps): - {t('Read_only')} + {t('Encrypted')} ( )} /> - - {readOnly ? t('Only_authorized_users_can_write_new_messages') : t('All_users_in_the_channel_can_write_new_messages')} - + {getEncryptedHint({ isPrivate, broadcast, encrypted })} - {t('Encrypted')} + {t('Read_only')} ( )} /> - - {isPrivate ? t('Encrypted_channel_Description') : t('Encrypted_not_available')} - + + {readOnly ? t('Read_only_field_hint_enabled', { roomType: 'channel' }) : t('Anyone_can_send_new_messages')} + @@ -344,17 +357,7 @@ const CreateChannelModal = ({ teamId = '', onClose }: CreateChannelModalProps): )} /> - {t('Broadcast_channel_Description')} - - - {t('Add_members')} - ( - - )} - /> + {broadcast && {t('Broadcast_hint_enabled', { roomType: 'channel' })}} diff --git a/apps/meteor/client/sidebar/header/CreateDirectMessage.tsx b/apps/meteor/client/sidebar/header/CreateDirectMessage.tsx index 626d1202a8e0..7c34f7ac01a3 100644 --- a/apps/meteor/client/sidebar/header/CreateDirectMessage.tsx +++ b/apps/meteor/client/sidebar/header/CreateDirectMessage.tsx @@ -1,5 +1,5 @@ import type { IUser } from '@rocket.chat/core-typings'; -import { Box, Modal, Button, FieldGroup, Field, FieldRow, FieldLabel, FieldError } from '@rocket.chat/fuselage'; +import { Box, Modal, Button, FieldGroup, Field, FieldRow, FieldError, FieldHint } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useTranslation, useEndpoint, useToastMessageDispatch, useSetting } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; @@ -20,7 +20,7 @@ const CreateDirectMessage = ({ onClose }: { onClose: () => void }) => { const { control, handleSubmit, - formState: { isDirty, isSubmitting, isValidating, errors }, + formState: { isSubmitting, isValidating, errors }, } = useForm({ mode: 'onBlur', defaultValues: { users: [] } }); const mutateDirectMessage = useMutation({ @@ -47,17 +47,14 @@ const CreateDirectMessage = ({ onClose }: { onClose: () => void }) => { - {t('Direct_message_creation_description')} - - {t('Members')} - + {t('Direct_message_creation_description')} users.length + 1 > directMaxUsers ? t('error-direct-message-max-user-exceeded', { maxUsers: directMaxUsers }) @@ -71,7 +68,7 @@ const CreateDirectMessage = ({ onClose }: { onClose: () => void }) => { value={value} onBlur={onBlur} id={membersFieldId} - aria-describedby={`${membersFieldId}-error`} + aria-describedby={`${membersFieldId}-hint ${membersFieldId}-error`} aria-required='true' aria-invalid={Boolean(errors.users)} /> @@ -83,13 +80,14 @@ const CreateDirectMessage = ({ onClose }: { onClose: () => void }) => { {errors.users.message} )} + {t('Direct_message_creation_description_hint')} - diff --git a/apps/meteor/client/sidebar/header/CreateTeam/CreateTeamModal.tsx b/apps/meteor/client/sidebar/header/CreateTeam/CreateTeamModal.tsx index 115a8563f393..b56bb003aa9e 100644 --- a/apps/meteor/client/sidebar/header/CreateTeam/CreateTeamModal.tsx +++ b/apps/meteor/client/sidebar/header/CreateTeam/CreateTeamModal.tsx @@ -11,6 +11,7 @@ import { FieldRow, FieldError, FieldDescription, + FieldHint, } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { @@ -27,6 +28,7 @@ import { Controller, useForm } from 'react-hook-form'; import UserAutoCompleteMultiple from '../../../components/UserAutoCompleteMultiple'; import { goToRoomById } from '../../../lib/utils/goToRoomById'; +import { useEncryptedRoomDescription } from '../hooks/useEncryptedRoomDescription'; type CreateTeamModalInputs = { name: string; @@ -65,7 +67,7 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => } if (teamNameRegex && !teamNameRegex?.test(name)) { - return t('Teams_Errors_team_name', { name }); + return t('Name_cannot_have_special_characters'); } const { exists } = await checkTeamNameExists({ roomName: name }); @@ -80,7 +82,7 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => handleSubmit, setValue, watch, - formState: { isDirty, errors, isSubmitting }, + formState: { errors, isSubmitting }, } = useForm({ defaultValues: { isPrivate: true, @@ -91,7 +93,7 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => }, }); - const { isPrivate, broadcast, readOnly } = watch(); + const { isPrivate, broadcast, readOnly, encrypted } = watch(); useEffect(() => { if (!isPrivate) { @@ -107,7 +109,7 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => const canChangeReadOnly = !broadcast; const canChangeEncrypted = isPrivate && !broadcast && e2eEnabled && !e2eEnabledForPrivateByDefault; - const isButtonEnabled = isDirty && canCreateTeam; + const getEncryptedHint = useEncryptedRoomDescription('team'); const handleCreateTeam = async ({ name, @@ -164,6 +166,9 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => + + {t('Teams_new_description')} + @@ -177,10 +182,9 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => required: t('error-the-field-is-required', { field: t('Name') }), validate: (value) => validateTeamName(value), })} - placeholder={t('Team_Name')} addon={} error={errors.name?.message} - aria-describedby={`${nameId}-error`} + aria-describedby={`${nameId}-error ${nameId}-hint`} aria-required='true' /> @@ -189,23 +193,27 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => {errors.name.message} )} + {!allowSpecialNames && {t('No_spaces')}} - - {t('Teams_New_Description_Label')}{' '} - - ({t('optional')}) - - + {t('Topic')} - + + + + {t('Displayed_next_to_name')} + + {t('Teams_New_Add_members_Label')} + ( + + )} + /> + {t('Teams_New_Private_Label')} @@ -218,7 +226,7 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => /> - {isPrivate ? t('Teams_New_Private_Description_Enabled') : t('Teams_New_Private_Description_Disabled')} + {isPrivate ? t('People_can_only_join_by_being_invited') : t('Anyone_can_access')} @@ -240,7 +248,7 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => /> - {readOnly ? t('Only_authorized_users_can_write_new_messages') : t('Teams_New_Read_only_Description')} + {readOnly ? t('Read_only_field_hint_enabled', { roomType: 'team' }) : t('Anyone_can_send_new_messages')} @@ -261,9 +269,7 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => )} /> - - {isPrivate ? t('Teams_New_Encrypted_Description_Enabled') : t('Teams_New_Encrypted_Description_Disabled')} - + {getEncryptedHint({ isPrivate, broadcast, encrypted })} @@ -276,27 +282,14 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => )} /> - {t('Teams_New_Broadcast_Description')} - - - - {t('Teams_New_Add_members_Label')}{' '} - - ({t('optional')}) - - - } - /> + {broadcast && {t('Teams_New_Broadcast_Description')}} - diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useCreateRoomItems.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useCreateRoomItems.tsx index c6847fbe7d1c..7b2770a60ab5 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useCreateRoomItems.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useCreateRoomItems.tsx @@ -44,7 +44,7 @@ export const useCreateRoomItems = (): GenericMenuItemProps[] => { }; const createDirectMessageItem: GenericMenuItemProps = { id: 'direct', - content: t('Direct_Messages'), + content: t('Direct_message'), icon: 'balloon', onClick: () => { createDirectMessage(); @@ -60,9 +60,9 @@ export const useCreateRoomItems = (): GenericMenuItemProps[] => { }; return [ - ...(canCreateChannel ? [createChannelItem] : []), - ...(canCreateTeam ? [createTeamItem] : []), ...(canCreateDirectMessages ? [createDirectMessageItem] : []), ...(canCreateDiscussion && discussionEnabled ? [createDiscussionItem] : []), + ...(canCreateChannel ? [createChannelItem] : []), + ...(canCreateTeam ? [createTeamItem] : []), ]; }; diff --git a/apps/meteor/client/sidebar/header/hooks/useEncryptedRoomDescription.tsx b/apps/meteor/client/sidebar/header/hooks/useEncryptedRoomDescription.tsx new file mode 100644 index 000000000000..09796dd7a6b7 --- /dev/null +++ b/apps/meteor/client/sidebar/header/hooks/useEncryptedRoomDescription.tsx @@ -0,0 +1,23 @@ +import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; + +export const useEncryptedRoomDescription = (roomType: 'channel' | 'team') => { + const t = useTranslation(); + const e2eEnabled = useSetting('E2E_Enable'); + const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms'); + + return ({ isPrivate, broadcast, encrypted }: { isPrivate: boolean; broadcast: boolean; encrypted: boolean }) => { + if (!e2eEnabled) { + return t('Not_available_for_this_workspace'); + } + if (!isPrivate) { + return t('Encrypted_not_available', { roomType }); + } + if (broadcast) { + return t('Not_available_for_broadcast', { roomType }); + } + if (e2eEnabledForPrivateByDefault || encrypted) { + return t('Encrypted_messages', { roomType }); + } + return t('Encrypted_messages_false'); + }; +}; diff --git a/apps/meteor/client/sidebar/search/SearchList.tsx b/apps/meteor/client/sidebar/search/SearchList.tsx index ceb89d3d7092..d215a77ce4bd 100644 --- a/apps/meteor/client/sidebar/search/SearchList.tsx +++ b/apps/meteor/client/sidebar/search/SearchList.tsx @@ -101,7 +101,7 @@ const useSearchItems = (filterText: string): UseQueryResult<(ISubscription & IRo const getSpotlight = useMethod('spotlight'); return useQuery( - ['sidebar/search/spotlight', name, usernamesFromClient, type, localRooms.map(({ _id }) => _id)], + ['sidebar/search/spotlight', name, usernamesFromClient, type, localRooms.map(({ _id, name }) => _id + name)], async () => { if (localRooms.length === LIMIT) { return localRooms; diff --git a/apps/meteor/client/views/admin/rooms/EditRoom.tsx b/apps/meteor/client/views/admin/rooms/EditRoom.tsx index 1522f4694160..cc165bca215b 100644 --- a/apps/meteor/client/views/admin/rooms/EditRoom.tsx +++ b/apps/meteor/client/views/admin/rooms/EditRoom.tsx @@ -86,7 +86,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps) => { canViewReactWhenReadOnly, } = useEditAdminRoomPermissions(room); - const { roomType, readOnly, archived } = watch(); + const { roomType, readOnly, archived, isDefault } = watch(); const changeArchiving = archived !== !!room.archived; @@ -324,7 +324,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps) => { name='favorite' control={control} render={({ field: { value, ...field } }) => ( - + )} /> diff --git a/apps/meteor/client/views/admin/settings/MemoizedSetting.tsx b/apps/meteor/client/views/admin/settings/MemoizedSetting.tsx index 5534e4cef7b3..d0b5c10e2c80 100644 --- a/apps/meteor/client/views/admin/settings/MemoizedSetting.tsx +++ b/apps/meteor/client/views/admin/settings/MemoizedSetting.tsx @@ -55,7 +55,7 @@ type MemoizedSettingProps = { onResetButtonClick?: () => void; className?: string; invisible?: boolean; - label?: string; + label?: ReactNode; sectionChanged?: boolean; hasResetButton?: boolean; disabled?: boolean; diff --git a/apps/meteor/client/views/admin/settings/Setting.tsx b/apps/meteor/client/views/admin/settings/Setting.tsx index d9076e5fb4f6..6a08352b9180 100644 --- a/apps/meteor/client/views/admin/settings/Setting.tsx +++ b/apps/meteor/client/views/admin/settings/Setting.tsx @@ -1,6 +1,6 @@ import type { ISettingColor, SettingEditor, SettingValue } from '@rocket.chat/core-typings'; import { isSettingColor, isSetting } from '@rocket.chat/core-typings'; -import { Button } from '@rocket.chat/fuselage'; +import { Box, Button, Tag } from '@rocket.chat/fuselage'; import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks'; import { useSettingStructure, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -96,7 +96,7 @@ function Setting({ className = undefined, settingId, sectionChanged }: SettingPr const { _id, disabled, readonly, type, packageValue, i18nLabel, i18nDescription, alert, invisible } = setting; - const label = (t.has(i18nLabel) && t(i18nLabel)) || (t.has(_id) && t(_id)) || i18nLabel || _id; + const labelText = (t.has(i18nLabel) && t(i18nLabel)) || (t.has(_id) && t(_id)) || i18nLabel || _id; const hint = useMemo( () => @@ -119,6 +119,21 @@ function Setting({ className = undefined, settingId, sectionChanged }: SettingPr [shouldDisableEnterprise, t], ); + const label = useMemo(() => { + if (!shouldDisableEnterprise) { + return labelText; + } + + return ( + <> + + {labelText} + + {t('Premium')} + + ); + }, [labelText, shouldDisableEnterprise, t]); + const hasResetButton = !shouldDisableEnterprise && !readonly && @@ -132,7 +147,7 @@ function Setting({ className = undefined, settingId, sectionChanged }: SettingPr return ( , 'onChangeValue'> & { asset?: any; - required?: boolean; fileConstraints?: { extensions: string[] }; }; -function AssetSettingInput({ _id, label, value, asset, required, fileConstraints }: AssetSettingInputProps): ReactElement { +function AssetSettingInput({ _id, label, value, asset, required, disabled, fileConstraints }: AssetSettingInputProps): ReactElement { const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); @@ -78,19 +75,20 @@ function AssetSettingInput({ _id, label, value, asset, required, fileConstraints )}
{value?.url ? ( - ) : ( -
+ {t('Select_file')} -
+ )}
diff --git a/apps/meteor/client/views/admin/settings/inputs/BooleanSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/BooleanSettingInput.tsx index 7dc45f6a5392..cf18a6f0d1df 100644 --- a/apps/meteor/client/views/admin/settings/inputs/BooleanSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/BooleanSettingInput.tsx @@ -3,18 +3,10 @@ import type { ReactElement, SyntheticEvent } from 'react'; import React from 'react'; import ResetSettingButton from '../ResetSettingButton'; +import type { SettingInputProps } from './types'; + +type BooleanSettingInputProps = SettingInputProps; -type BooleanSettingInputProps = { - _id: string; - label: string; - disabled?: boolean; - readonly?: boolean; - required?: boolean; - value: boolean; - hasResetButton: boolean; - onChangeValue: (value: boolean) => void; - onResetButtonClick: () => void; -}; function BooleanSettingInput({ _id, label, diff --git a/apps/meteor/client/views/admin/settings/inputs/CodeMirror/CodeMirrorBox.tsx b/apps/meteor/client/views/admin/settings/inputs/CodeMirror/CodeMirrorBox.tsx index 730b22b6b58c..ce6d2dfd6000 100644 --- a/apps/meteor/client/views/admin/settings/inputs/CodeMirror/CodeMirrorBox.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/CodeMirror/CodeMirrorBox.tsx @@ -2,10 +2,10 @@ import { css } from '@rocket.chat/css-in-js'; import { Box, Button, ButtonGroup } from '@rocket.chat/fuselage'; import { useToggle } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; +import type { ReactElement, ReactNode } from 'react'; import React from 'react'; -const CodeMirrorBox = ({ label, children }: { label: string; children: ReactElement }) => { +const CodeMirrorBox = ({ label, children }: { label: ReactNode; children: ReactElement }) => { const t = useTranslation(); const [fullScreen, toggleFullScreen] = useToggle(false); diff --git a/apps/meteor/client/views/admin/settings/inputs/CodeSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/CodeSettingInput.tsx index 0424e862bd08..f019e4742bf4 100644 --- a/apps/meteor/client/views/admin/settings/inputs/CodeSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/CodeSettingInput.tsx @@ -5,21 +5,13 @@ import React from 'react'; import ResetSettingButton from '../ResetSettingButton'; import CodeMirror from './CodeMirror'; import CodeMirrorBox from './CodeMirror/CodeMirrorBox'; +import type { SettingInputProps } from './types'; -type CodeSettingInputProps = { - _id: string; - label: string; +type CodeSettingInputProps = SettingInputProps & { hint: string; - value?: string; code: string; - placeholder?: string; readonly: boolean; - autocomplete: boolean; disabled: boolean; - required?: boolean; - hasResetButton: boolean; - onChangeValue: (value: string) => void; - onResetButtonClick: () => void; }; function CodeSettingInput({ diff --git a/apps/meteor/client/views/admin/settings/inputs/ColorSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/ColorSettingInput.tsx index a4f61b811549..dab9e63f7435 100644 --- a/apps/meteor/client/views/admin/settings/inputs/ColorSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/ColorSettingInput.tsx @@ -5,23 +5,14 @@ import type { ReactElement } from 'react'; import React, { useCallback } from 'react'; import ResetSettingButton from '../ResetSettingButton'; +import type { SettingInputProps } from './types'; -type ColorSettingInputProps = { - _id: string; - label: string; +type ColorSettingInputProps = SettingInputProps & { value: string; editor: string; allowedTypes?: TranslationKey[]; - placeholder?: string; - readonly?: boolean; - autocomplete?: boolean; - disabled?: boolean; - required?: boolean; - hasResetButton?: boolean; - onChangeValue?: (value: string) => void; - onChangeEditor?: (value: string) => void; - onResetButtonClick?: () => void; }; + function ColorSettingInput({ _id, label, diff --git a/apps/meteor/client/views/admin/settings/inputs/FontSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/FontSettingInput.tsx index 1c1c45913bc1..4508f52222ba 100644 --- a/apps/meteor/client/views/admin/settings/inputs/FontSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/FontSettingInput.tsx @@ -3,20 +3,12 @@ import type { FormEventHandler, ReactElement } from 'react'; import React from 'react'; import ResetSettingButton from '../ResetSettingButton'; +import type { SettingInputProps } from './types'; -type FontSettingInputProps = { - _id: string; - label: string; +type FontSettingInputProps = SettingInputProps & { value: string; - placeholder?: string; - readonly?: boolean; - autocomplete?: boolean; - disabled?: boolean; - required?: boolean; - hasResetButton?: boolean; - onChangeValue?: (value: string) => void; - onResetButtonClick?: () => void; }; + function FontSettingInput({ _id, label, diff --git a/apps/meteor/client/views/admin/settings/inputs/GenericSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/GenericSettingInput.tsx index daabea2b1d0e..8f6c38dbe22e 100644 --- a/apps/meteor/client/views/admin/settings/inputs/GenericSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/GenericSettingInput.tsx @@ -3,20 +3,12 @@ import type { FormEventHandler, ReactElement } from 'react'; import React from 'react'; import ResetSettingButton from '../ResetSettingButton'; +import type { SettingInputProps } from './types'; -type GenericSettingInputProps = { - _id: string; - label: string; +type GenericSettingInputProps = SettingInputProps & { value: string; - placeholder?: string; - readonly?: boolean; - autocomplete?: boolean; - disabled?: boolean; - required?: boolean; - hasResetButton?: boolean; - onChangeValue?: (value: string) => void; - onResetButtonClick?: () => void; }; + function GenericSettingInput({ _id, label, diff --git a/apps/meteor/client/views/admin/settings/inputs/IntSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/IntSettingInput.tsx index 16100f128466..053f00be2f85 100644 --- a/apps/meteor/client/views/admin/settings/inputs/IntSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/IntSettingInput.tsx @@ -3,19 +3,10 @@ import type { FormEventHandler, ReactElement } from 'react'; import React from 'react'; import ResetSettingButton from '../ResetSettingButton'; +import type { SettingInputProps } from './types'; -type IntSettingInputProps = { - _id: string; - label: string; +type IntSettingInputProps = SettingInputProps & { value: string; - placeholder?: string; - readonly?: boolean; - autocomplete?: boolean; - disabled?: boolean; - required?: boolean; - hasResetButton?: boolean; - onChangeValue?: (value: string | number) => void; - onResetButtonClick?: () => void; }; function IntSettingInput({ diff --git a/apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.tsx index f91642a5eed9..6044cc754602 100644 --- a/apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.tsx @@ -4,20 +4,9 @@ import type { ReactElement } from 'react'; import React from 'react'; import ResetSettingButton from '../ResetSettingButton'; +import type { SettingInputProps } from './types'; -type LanguageSettingInputProps = { - _id: string; - label: string; - value: string; - placeholder?: string; - readonly?: boolean; - autocomplete?: boolean; - disabled?: boolean; - required?: boolean; - hasResetButton?: boolean; - onChangeValue?: (value: string | number) => void; - onResetButtonClick?: () => void; -}; +type LanguageSettingInputProps = SettingInputProps; function LanguageSettingInput({ _id, diff --git a/apps/meteor/client/views/admin/settings/inputs/LookupSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/LookupSettingInput.tsx index 360eba91a785..d2c7029f994d 100644 --- a/apps/meteor/client/views/admin/settings/inputs/LookupSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/LookupSettingInput.tsx @@ -6,20 +6,10 @@ import React from 'react'; import type { AsyncState } from '../../../../hooks/useAsyncState'; import { useEndpointData } from '../../../../hooks/useEndpointData'; import ResetSettingButton from '../ResetSettingButton'; +import type { SettingInputProps } from './types'; -type LookupSettingInputProps = { - _id: string; - label: string; - value?: string; +type LookupSettingInputProps = SettingInputProps & { lookupEndpoint: PathPattern extends `/${infer U}` ? U : PathPattern; - placeholder?: string; - readonly?: boolean; - autocomplete?: boolean; - disabled?: boolean; - required?: boolean; - hasResetButton?: boolean; - onChangeValue?: (value: string) => void; - onResetButtonClick?: () => void; }; function LookupSettingInput({ diff --git a/apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.tsx index c0d12ee401cf..a484dc1133f0 100644 --- a/apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.tsx @@ -5,21 +5,11 @@ import type { ReactElement } from 'react'; import React from 'react'; import ResetSettingButton from '../ResetSettingButton'; +import type { SettingInputProps } from './types'; export type valuesOption = { key: string; i18nLabel: TranslationKey }; -type MultiSelectSettingInputProps = { - _id: string; - label: string; - value?: [string, string]; +type MultiSelectSettingInputProps = SettingInputProps<[string, string], string[]> & { values: valuesOption[]; - placeholder?: string; - readonly?: boolean; - autocomplete?: boolean; - disabled?: boolean; - required?: boolean; - hasResetButton?: boolean; - onChangeValue?: (value: string[]) => void; - onResetButtonClick?: () => void; }; function MultiSelectSettingInput({ diff --git a/apps/meteor/client/views/admin/settings/inputs/PasswordSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/PasswordSettingInput.tsx index 249fb7e8c90d..75743c9f5a6c 100644 --- a/apps/meteor/client/views/admin/settings/inputs/PasswordSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/PasswordSettingInput.tsx @@ -3,20 +3,9 @@ import type { EventHandler, ReactElement, SyntheticEvent } from 'react'; import React from 'react'; import ResetSettingButton from '../ResetSettingButton'; +import type { SettingInputProps } from './types'; -type PasswordSettingInputProps = { - _id: string; - label: string; - value?: string | number | readonly string[] | undefined; - placeholder?: string; - readonly?: boolean; - autocomplete?: boolean; - disabled?: boolean; - required?: boolean; - hasResetButton?: boolean; - onChangeValue?: (value: string) => void; - onResetButtonClick?: () => void; -}; +type PasswordSettingInputProps = SettingInputProps; function PasswordSettingInput({ _id, diff --git a/apps/meteor/client/views/admin/settings/inputs/RelativeUrlSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/RelativeUrlSettingInput.tsx index 681232e41802..ac03596cc273 100644 --- a/apps/meteor/client/views/admin/settings/inputs/RelativeUrlSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/RelativeUrlSettingInput.tsx @@ -4,20 +4,9 @@ import type { EventHandler, ReactElement, SyntheticEvent } from 'react'; import React from 'react'; import ResetSettingButton from '../ResetSettingButton'; +import type { SettingInputProps } from './types'; -type RelativeUrlSettingInputProps = { - _id: string; - label: string; - value?: string; - placeholder?: string; - readonly?: boolean; - autocomplete?: boolean; - disabled?: boolean; - required?: boolean; - hasResetButton?: boolean; - onChangeValue?: (value: string) => void; - onResetButtonClick?: () => void; -}; +type RelativeUrlSettingInputProps = SettingInputProps; function RelativeUrlSettingInput({ _id, diff --git a/apps/meteor/client/views/admin/settings/inputs/RoomPickSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/RoomPickSettingInput.tsx index fab66ff7a066..df2a4c1b0688 100644 --- a/apps/meteor/client/views/admin/settings/inputs/RoomPickSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/RoomPickSettingInput.tsx @@ -5,19 +5,9 @@ import React from 'react'; import RoomAutoCompleteMultiple from '../../../../components/RoomAutoCompleteMultiple'; import ResetSettingButton from '../ResetSettingButton'; +import type { SettingInputProps } from './types'; -type RoomPickSettingInputProps = { - _id: string; - label: string; - value?: SettingValueRoomPick | ''; - placeholder?: string; - readonly?: boolean; - disabled?: boolean; - required?: boolean; - hasResetButton?: boolean; - onChangeValue: (value: SettingValueRoomPick) => void; - onResetButtonClick?: () => void; -}; +type RoomPickSettingInputProps = SettingInputProps; function RoomPickSettingInput({ _id, diff --git a/apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.tsx index f12104f07d0c..2ace88fda2cb 100644 --- a/apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.tsx @@ -5,20 +5,10 @@ import type { ReactElement } from 'react'; import React from 'react'; import ResetSettingButton from '../ResetSettingButton'; +import type { SettingInputProps } from './types'; -type SelectSettingInputProps = { - _id: string; - label: string; - value?: string; +type SelectSettingInputProps = SettingInputProps & { values?: { key: string; i18nLabel: TranslationKey }[]; - placeholder?: string; - readonly?: boolean; - autocomplete?: boolean; - disabled?: boolean; - required?: boolean; - hasResetButton?: boolean; - onChangeValue?: (value: string) => void; - onResetButtonClick?: () => void; }; function SelectSettingInput({ diff --git a/apps/meteor/client/views/admin/settings/inputs/SelectTimezoneSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/SelectTimezoneSettingInput.tsx index 55f877c77c44..fd1fc147a51a 100644 --- a/apps/meteor/client/views/admin/settings/inputs/SelectTimezoneSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/SelectTimezoneSettingInput.tsx @@ -4,20 +4,9 @@ import type { ReactElement } from 'react'; import React from 'react'; import ResetSettingButton from '../ResetSettingButton'; +import type { SettingInputProps } from './types'; -type SelectTimezoneSettingInputProps = { - _id: string; - label: string; - value?: string; - placeholder?: string; - readonly?: boolean; - autocomplete?: boolean; - disabled?: boolean; - required?: boolean; - hasResetButton?: boolean; - onChangeValue?: (value: string) => void; - onResetButtonClick?: () => void; -}; +type SelectTimezoneSettingInputProps = SettingInputProps; function SelectTimezoneSettingInput({ _id, diff --git a/apps/meteor/client/views/admin/settings/inputs/StringSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/StringSettingInput.tsx index 1803f54a3631..2652fb122162 100644 --- a/apps/meteor/client/views/admin/settings/inputs/StringSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/StringSettingInput.tsx @@ -3,22 +3,12 @@ import type { EventHandler, ReactElement, SyntheticEvent } from 'react'; import React from 'react'; import ResetSettingButton from '../ResetSettingButton'; +import type { SettingInputProps } from './types'; -type StringSettingInputProps = { - _id: string; - label: string; +type StringSettingInputProps = SettingInputProps & { name?: string; - value?: string; multiline?: boolean; - placeholder?: string; - readonly?: boolean; error?: string; - autocomplete?: boolean; - disabled?: boolean; - required?: boolean; - hasResetButton?: boolean; - onChangeValue?: (value: string) => void; - onResetButtonClick?: () => void; }; function StringSettingInput({ diff --git a/apps/meteor/client/views/admin/settings/inputs/types.ts b/apps/meteor/client/views/admin/settings/inputs/types.ts new file mode 100644 index 000000000000..d978a3ad6b4d --- /dev/null +++ b/apps/meteor/client/views/admin/settings/inputs/types.ts @@ -0,0 +1,18 @@ +import type { ReactNode } from 'react'; + +export type SettingInputProps = { + _id: string; + label: ReactNode; + value?: V; + placeholder?: string; + readonly?: boolean; + autocomplete?: boolean; + disabled: boolean; + required?: boolean; + hint?: string; + editor?: string; + hasResetButton: boolean; + onChangeValue: (value: R) => void; + onResetButtonClick?: () => void; + onChangeEditor?: (value: string | undefined) => void; +}; diff --git a/apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts b/apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts index 1926e2e38407..737294b52ba5 100644 --- a/apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts +++ b/apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts @@ -54,16 +54,12 @@ const useFilteredUsers = ({ searchTerm, prevSearchTerm, sortData, paginationData }, [current, itemsPerPage, prevSearchTerm, searchTerm, selectedRoles, setCurrent, sortBy, sortDirection, tab]); const getUsers = useEndpoint('GET', '/v1/users.listByStatus'); - const dispatchToastMessage = useToastMessageDispatch(); - const usersListQueryResult = useQuery(['users.list', payload, tab], async () => getUsers(payload), { onError: (error) => { dispatchToastMessage({ type: 'error', message: error }); }, }); - return usersListQueryResult; }; - export default useFilteredUsers; diff --git a/apps/meteor/client/views/hooks/roomActions/useDeleteRoom.tsx b/apps/meteor/client/views/hooks/roomActions/useDeleteRoom.tsx index be4728732284..48b6507a331d 100644 --- a/apps/meteor/client/views/hooks/roomActions/useDeleteRoom.tsx +++ b/apps/meteor/client/views/hooks/roomActions/useDeleteRoom.tsx @@ -15,7 +15,8 @@ export const useDeleteRoom = (room: IRoom | Pick, { const dispatchToastMessage = useToastMessageDispatch(); const hasPermissionToDelete = usePermission(`delete-${room.t}`, room._id); const canDeleteRoom = isRoomFederated(room) ? false : hasPermissionToDelete; - + // eslint-disable-next-line no-nested-ternary + const roomType = 'prid' in room ? 'discussion' : room.teamId && room.teamMain ? 'team' : 'channel'; const isAdminRoute = router.getRouteName() === 'admin-rooms'; const deleteRoomEndpoint = useEndpoint('POST', '/v1/rooms.delete'); @@ -24,7 +25,7 @@ export const useDeleteRoom = (room: IRoom | Pick, { const deleteRoomMutation = useMutation({ mutationFn: deleteRoomEndpoint, onSuccess: () => { - dispatchToastMessage({ type: 'success', message: t('Room_has_been_deleted') }); + dispatchToastMessage({ type: 'success', message: t('Deleted_roomType', { roomName: room.name, roomType }) }); if (isAdminRoute) { return router.navigate('/admin/rooms'); } @@ -79,8 +80,14 @@ export const useDeleteRoom = (room: IRoom | Pick, { }; setModal( - setModal(null)} confirmText={t('Yes_delete_it')}> - {t('Delete_Room_Warning')} + setModal(null)} + confirmText={t('Yes_delete_it')} + > + {t('Delete_Room_Warning', { roomType })} , ); }); diff --git a/apps/meteor/client/views/modal/ModalRegion.tsx b/apps/meteor/client/views/modal/ModalRegion.tsx index b014a5be4f7c..5cbad2b52bc1 100644 --- a/apps/meteor/client/views/modal/ModalRegion.tsx +++ b/apps/meteor/client/views/modal/ModalRegion.tsx @@ -2,8 +2,8 @@ import { useModal, useCurrentModal } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { lazy, useCallback } from 'react'; -import ModalBackdrop from '../../components/modal/ModalBackdrop'; -import ModalPortal from '../../components/modal/ModalPortal'; +import ModalBackdrop from '../../components/ModalBackdrop'; +import ModalPortal from '../../portals/ModalPortal'; const FocusScope = lazy(() => import('react-aria').then((module) => ({ default: module.FocusScope }))); diff --git a/apps/meteor/client/views/omnichannel/appearance/AppearanceFieldLabel.tsx b/apps/meteor/client/views/omnichannel/appearance/AppearanceFieldLabel.tsx new file mode 100644 index 000000000000..ba8b54aeaccf --- /dev/null +++ b/apps/meteor/client/views/omnichannel/appearance/AppearanceFieldLabel.tsx @@ -0,0 +1,32 @@ +import { FieldLabel as BaseFieldLabel, Box, Tag } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ComponentProps } from 'react'; +import React from 'react'; + +import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule'; + +type FieldLabelProps = ComponentProps & { + premium?: boolean; + children: string; +}; + +const FieldLabel = ({ children: label, premium = false }: FieldLabelProps) => { + const t = useTranslation(); + const hasLicense = useHasLicenseModule('livechat-enterprise'); + const shouldDisableEnterprise = premium && !hasLicense; + + if (!shouldDisableEnterprise) { + return {label}; + } + + return ( + + + {label} + + {t('Premium')} + + ); +}; + +export default FieldLabel; diff --git a/apps/meteor/client/views/omnichannel/appearance/AppearanceForm.tsx b/apps/meteor/client/views/omnichannel/appearance/AppearanceForm.tsx index 258cda39e962..91c8d20f49f7 100644 --- a/apps/meteor/client/views/omnichannel/appearance/AppearanceForm.tsx +++ b/apps/meteor/client/views/omnichannel/appearance/AppearanceForm.tsx @@ -1,6 +1,5 @@ import { Field, - FieldLabel, FieldRow, TextInput, ToggleSwitch, @@ -9,6 +8,9 @@ import { InputBox, TextAreaInput, NumberInput, + Select, + MultiSelect, + FieldHint, } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; @@ -16,8 +18,13 @@ import type { ChangeEvent } from 'react'; import React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; +import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule'; +import MarkdownText from '../../../components/MarkdownText'; +import FieldLabel from './AppearanceFieldLabel'; + const AppearanceForm = () => { const t = useTranslation(); + const isEnterprise = useHasLicenseModule('livechat-enterprise'); const { control, watch } = useFormContext(); const { Livechat_enable_message_character_limit } = watch(); @@ -41,9 +48,101 @@ const AppearanceForm = () => { const livechatRegistrationFormMessageField = useUniqueId(); const livechatConversationFinishedMessageField = useUniqueId(); const livechatConversationFinishedTextField = useUniqueId(); + const livechatHideWatermarkField = useUniqueId(); + const livechatWidgetPositionField = useUniqueId(); + const livechatBackgroundField = useUniqueId(); + const livechatHideSystemMessagesField = useUniqueId(); return ( + + + + + + {t('Livechat_hide_watermark')} + + ( + + )} + /> + + + + + + {t('Livechat_background')} + + + ( + + )} + /> + + + + + + + + + {t('Livechat_widget_position_on_the_screen')} + + + ( + setType(val as 'all' | 'autoJoin')} value={type} options={options} /> - - - - - - {loading && ( - - - - )} - {!loading && channels.length === 0 && } - - {!loading && channels.length > 0 && ( - <> - - - {t('Showing')}: {channels.length} - - - - {t('Total')}: {total} - - - - }} - itemContent={(index, data) => } - /> - - - )} - - - {(onClickAddExisting || onClickCreateNew) && ( - - - {onClickAddExisting && ( - - )} - {onClickCreateNew && ( - - )} - - - )} - - ); -}; - -export default BaseTeamsChannels; diff --git a/apps/meteor/client/views/teams/contextualBar/channels/ConfirmationModal/ConfirmationModal.tsx b/apps/meteor/client/views/teams/contextualBar/channels/ConfirmationModal/ConfirmationModal.tsx deleted file mode 100644 index 3768c851fed4..000000000000 --- a/apps/meteor/client/views/teams/contextualBar/channels/ConfirmationModal/ConfirmationModal.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Button, Modal } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import type { FC } from 'react'; -import React, { memo } from 'react'; - -type ConfirmationModalProps = { - onClose: () => void; - onConfirmAction: any; - labelButton: string; - content: string; -}; - -const ConfirmationModal: FC = ({ onClose, onConfirmAction, labelButton, content }) => { - const t = useTranslation(); - - const handleConfirm = (): void => { - onConfirmAction(); - onClose(); - }; - - return ( - - - - {t('Confirmation')} - - - {content} - - - - - - - - ); -}; - -export default memo(ConfirmationModal); diff --git a/apps/meteor/client/views/teams/contextualBar/channels/ConfirmationModal/index.ts b/apps/meteor/client/views/teams/contextualBar/channels/ConfirmationModal/index.ts deleted file mode 100644 index 70dcc831cc1a..000000000000 --- a/apps/meteor/client/views/teams/contextualBar/channels/ConfirmationModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './ConfirmationModal'; diff --git a/apps/meteor/client/views/teams/contextualBar/channels/RoomActions.js b/apps/meteor/client/views/teams/contextualBar/channels/RoomActions.js deleted file mode 100644 index 22c31a137b27..000000000000 --- a/apps/meteor/client/views/teams/contextualBar/channels/RoomActions.js +++ /dev/null @@ -1,154 +0,0 @@ -import { Box, CheckBox, Menu, Option } from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, usePermission, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useMemo } from 'react'; - -import { useEndpointAction } from '../../../../hooks/useEndpointAction'; -import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; -import ConfirmationModal from './ConfirmationModal'; - -const useReactModal = (Component, props) => { - const setModal = useSetModal(); - - return useMutableCallback(() => { - const handleClose = () => { - setModal(null); - }; - - setModal(() => ); - }); -}; - -const RoomActions = ({ room, reload }) => { - const t = useTranslation(); - const rid = room._id; - const type = room.t; - - const dispatchToastMessage = useToastMessageDispatch(); - - const canDeleteTeamChannel = usePermission(type === 'c' ? 'delete-c' : 'delete-p', rid); - const canEditTeamChannel = usePermission('edit-team-channel', rid); - const canRemoveTeamChannel = usePermission('remove-team-channel', rid); - - const updateRoomEndpoint = useEndpointAction('POST', '/v1/teams.updateRoom'); - const removeRoomEndpoint = useEndpointAction('POST', '/v1/teams.removeRoom', { successMessage: t('Room_has_been_removed') }); - const deleteRoomEndpoint = useEndpointAction('POST', room.t === 'c' ? '/v1/channels.delete' : '/v1/groups.delete', { - successMessage: t('Room_has_been_deleted'), - }); - - const RemoveFromTeamAction = useReactModal(ConfirmationModal, { - onConfirmAction: async () => { - try { - await removeRoomEndpoint({ teamId: room.teamId, roomId: room._id }); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - reload(); - }, - labelButton: t('Remove'), - content: ( - - {t('Team_Remove_from_team_modal_content', { - teamName: roomCoordinator.getRoomName(room.t, room), - })} - - ), - }); - - const DeleteChannelAction = useReactModal(ConfirmationModal, { - onConfirmAction: async () => { - try { - await deleteRoomEndpoint({ roomId: room._id }); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - reload(); - }, - labelButton: t('Delete'), - content: ( - <> - - {t('Team_Delete_Channel_modal_content_danger')} - - - {' '} - {t('Team_Delete_Channel_modal_content')} - - - ), - }); - - const menuOptions = useMemo(() => { - const AutoJoinAction = async () => { - try { - await updateRoomEndpoint({ - roomId: rid, - isDefault: !room.teamDefault, - }); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - - reload(); - }; - - return [ - canEditTeamChannel && { - label: { - label: t('Team_Auto-join'), - icon: type === 'c' ? 'hash' : 'hashtag-lock', - }, - action: AutoJoinAction, - }, - canRemoveTeamChannel && { - label: { - label: t('Team_Remove_from_team'), - icon: 'cross', - }, - action: RemoveFromTeamAction, - }, - canDeleteTeamChannel && { - label: { - label: t('Delete'), - icon: 'trash', - }, - action: DeleteChannelAction, - }, - ].filter(Boolean); - }, [ - DeleteChannelAction, - RemoveFromTeamAction, - rid, - type, - room.teamDefault, - t, - updateRoomEndpoint, - reload, - dispatchToastMessage, - canDeleteTeamChannel, - canRemoveTeamChannel, - canEditTeamChannel, - ]); - - return ( - - icon.match(/hash/) ? ( - - ) : ( - - - ) - } - options={(canEditTeamChannel || canRemoveTeamChannel || canDeleteTeamChannel) && menuOptions} - /> - ); -}; - -export default RoomActions; diff --git a/apps/meteor/client/views/teams/contextualBar/channels/Row.js b/apps/meteor/client/views/teams/contextualBar/channels/Row.js deleted file mode 100644 index 62fca7fab96e..000000000000 --- a/apps/meteor/client/views/teams/contextualBar/channels/Row.js +++ /dev/null @@ -1,13 +0,0 @@ -import React, { memo } from 'react'; - -import TeamsChannelItem from './TeamsChannelItem'; - -function Row({ room, onClickView, reload }) { - if (!room) { - return ; - } - - return onClickView(room)} reload={reload} />; -} - -export default memo(Row); diff --git a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItem.js b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItem.tsx similarity index 77% rename from apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItem.js rename to apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItem.tsx index 7ee347910f65..981c65630d0a 100644 --- a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItem.js +++ b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItem.tsx @@ -1,3 +1,4 @@ +import type { IRoom } from '@rocket.chat/core-typings'; import { Box, Icon, @@ -17,9 +18,15 @@ import React, { useState } from 'react'; import { usePreventPropagation } from '../../../../hooks/usePreventPropagation'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; -import RoomActions from './RoomActions'; +import TeamsChannelItemMenu from './TeamsChannelItemMenu'; -const TeamsChannelItem = ({ room, onClickView, reload }) => { +type TeamsChannelItemProps = { + room: IRoom; + onClickView: (room: IRoom) => void; + reload: () => void; +}; + +const TeamsChannelItem = ({ room, onClickView, reload }: TeamsChannelItemProps) => { const t = useTranslation(); const rid = room._id; const type = room.t; @@ -37,8 +44,12 @@ const TeamsChannelItem = ({ room, onClickView, reload }) => { const onClick = usePreventPropagation(); + if (!room) { + return ; + } + return ( - ); }; -export default Object.assign(TeamsChannelItem, { - Skeleton: OptionSkeleton, -}); +export default TeamsChannelItem; diff --git a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItemMenu.tsx b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItemMenu.tsx new file mode 100644 index 000000000000..54ee216eaaa0 --- /dev/null +++ b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItemMenu.tsx @@ -0,0 +1,59 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { CheckBox } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import GenericMenu from '../../../../components/GenericMenu/GenericMenu'; +import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; +import { useDeleteRoom } from '../../../hooks/roomActions/useDeleteRoom'; +import { useRemoveRoomFromTeam } from './hooks/useRemoveRoomFromTeam'; +import { useToggleAutoJoin } from './hooks/useToggleAutoJoin'; + +const TeamsChannelItemMenu = ({ room, reload }: { room: IRoom; reload?: () => void }) => { + const t = useTranslation(); + + const { handleRemoveRoom, canRemoveTeamChannel } = useRemoveRoomFromTeam(room, { reload }); + const { handleDelete, canDeleteRoom } = useDeleteRoom(room, { reload }); + const { handleToggleAutoJoin, canEditTeamChannel } = useToggleAutoJoin(room, { reload }); + + const toggleAutoJoin = { + id: 'toggleAutoJoin', + icon: room.t === 'c' ? 'hash' : 'hashtag-lock', + content: t('Team_Auto-join'), + onClick: handleToggleAutoJoin, + addon: , + }; + + const removeRoom = { + id: 'removeRoom', + icon: 'cross', + content: t('Team_Remove_from_team'), + onClick: handleRemoveRoom, + variant: 'danger', + }; + + const deleteRoom = { + id: 'deleteRoom', + icon: 'trash', + content: t('Delete'), + onClick: handleDelete, + variant: 'danger', + }; + + return ( + + ); +}; + +export default TeamsChannelItemMenu; diff --git a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.tsx b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.tsx index 49d89e3153be..6d0bfddd8610 100644 --- a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.tsx +++ b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.tsx @@ -1,91 +1,151 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { useMutableCallback, useLocalStorage, useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, usePermission } from '@rocket.chat/ui-contexts'; -import type { FC, SyntheticEvent } from 'react'; -import React, { useCallback, useMemo, useState } from 'react'; - -import { useRecordList } from '../../../../hooks/lists/useRecordList'; -import { AsyncStatePhase } from '../../../../lib/asyncState'; -import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; -import CreateChannelWithData from '../../../../sidebar/header/CreateChannel'; -import { useRoom } from '../../../room/contexts/RoomContext'; -import { useRoomToolbox } from '../../../room/contexts/RoomToolboxContext'; -import RoomInfo from '../../../room/contextualBar/Info'; -import AddExistingModal from './AddExistingModal'; -import BaseTeamsChannels from './BaseTeamsChannels'; -import { useTeamsChannelList } from './hooks/useTeamsChannelList'; - -const useReactModal = (Component: FC, teamId: string, reload: () => void) => { - const setModal = useSetModal(); - - return useMutableCallback((e: SyntheticEvent) => { - e.preventDefault(); - - const handleClose = () => { - setModal(null); - reload(); - }; - - setModal(() => ); - }); +import type { SelectOption } from '@rocket.chat/fuselage'; +import { Box, Icon, TextInput, Margins, Select, Throbber, ButtonGroup, Button } from '@rocket.chat/fuselage'; +import { useMutableCallback, useAutoFocus, useDebouncedCallback } from '@rocket.chat/fuselage-hooks'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ChangeEvent, Dispatch, SetStateAction, SyntheticEvent } from 'react'; +import React, { useMemo } from 'react'; +import { Virtuoso } from 'react-virtuoso'; + +import { + ContextualbarHeader, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarClose, + ContextualbarContent, + ContextualbarFooter, + ContextualbarEmptyContent, +} from '../../../../components/Contextualbar'; +import { VirtuosoScrollbars } from '../../../../components/CustomScrollbars'; +import InfiniteListAnchor from '../../../../components/InfiniteListAnchor'; +import TeamsChannelItem from './TeamsChannelItem'; + +type TeamsChannelsProps = { + loading: boolean; + channels: IRoom[]; + text: string; + type: 'all' | 'autoJoin'; + setType: Dispatch>; + setText: (e: ChangeEvent) => void; + onClickClose: () => void; + onClickAddExisting: false | ((e: SyntheticEvent) => void); + onClickCreateNew: false | ((e: SyntheticEvent) => void); + total: number; + loadMoreItems: (start: number, end: number) => void; + onClickView: (room: IRoom) => void; + reload: () => void; }; -const TeamsChannels = () => { - const room = useRoom(); - const { teamId } = room; - - if (!teamId) { - throw new Error('Invalid teamId'); - } - - const [state, setState] = useState<{ tab?: string; rid?: string }>({}); - const { closeTab } = useRoomToolbox(); - - const [type, setType] = useLocalStorage<'all' | 'autoJoin'>('channels-list-type', 'all'); - const [text, setText] = useState(''); - - const debouncedText = useDebouncedValue(text, 800); - - const { teamsChannelList, loadMoreItems, reload } = useTeamsChannelList( - useMemo(() => ({ teamId, text: debouncedText, type }), [teamId, debouncedText, type]), +const TeamsChannels = ({ + loading, + channels = [], + text, + type, + setText, + setType, + onClickClose, + onClickAddExisting, + onClickCreateNew, + total, + loadMoreItems, + onClickView, + reload, +}: TeamsChannelsProps) => { + const t = useTranslation(); + const inputRef = useAutoFocus(true); + + const options: SelectOption[] = useMemo( + () => [ + ['all', t('All')], + ['autoJoin', t('Team_Auto-join')], + ], + [t], ); - const { phase, items, itemCount: total } = useRecordList(teamsChannelList); + const lm = useMutableCallback((start) => !loading && loadMoreItems(start, Math.min(50, total - start))); - const handleTextChange = useCallback((event) => { - setText(event.currentTarget.value); - }, []); + const loadMoreChannels = useDebouncedCallback( + () => { + if (channels.length >= total) { + return; + } - const canAddExistingTeam = usePermission('add-team-channel', room._id); - const addExisting = useReactModal(AddExistingModal, teamId, reload); - const createNew = useReactModal(CreateChannelWithData, teamId, reload); - - const goToRoom = useCallback((room) => roomCoordinator.openRouteLink(room.t, room), []); - const handleBack = useCallback(() => setState({}), [setState]); - const viewRoom = useMutableCallback((room: IRoom) => { - goToRoom(room); - }); - - if (state?.tab === 'RoomInfo' && state?.rid) { - return setState({})} />; - } + lm(channels.length); + }, + 300, + [lm, channels], + ); return ( - + <> + + + {t('Team_Channels')} + {onClickClose && } + + + + + + } + /> + + ( -
+
{children}
); diff --git a/packages/livechat/src/components/Messages/Message/index.js b/packages/livechat/src/components/Messages/Message/index.js index 56cd74239e5a..74387dae6719 100644 --- a/packages/livechat/src/components/Messages/Message/index.js +++ b/packages/livechat/src/components/Messages/Message/index.js @@ -92,9 +92,20 @@ const getMessageUsernames = (compact, message) => { return [username]; }; -const Message = ({ avatarResolver, attachmentResolver = getAttachmentUrl, use, me, compact, className, style = {}, t, ...message }) => ( +const Message = ({ + avatarResolver, + attachmentResolver = getAttachmentUrl, + use, + me, + compact, + className, + style = {}, + t, + hideAvatar, + ...message +}) => ( - {!message.type && } + {!message.type && !hideAvatar && } {renderContent({ text: message.type ? getSystemMessageText(message, t) : message.msg, diff --git a/packages/livechat/src/components/Messages/MessageAvatars/index.js b/packages/livechat/src/components/Messages/MessageAvatars/index.js index 07ca6644f79f..aa25528be11b 100644 --- a/packages/livechat/src/components/Messages/MessageAvatars/index.js +++ b/packages/livechat/src/components/Messages/MessageAvatars/index.js @@ -4,10 +4,18 @@ import { createClassName } from '../../../helpers/createClassName'; import { Avatar } from '../../Avatar'; import styles from './styles.scss'; -export const MessageAvatars = memo(({ avatarResolver = () => null, usernames = [], className, style = {} }) => ( -
- {usernames.map((username) => ( - - ))} -
-)); +export const MessageAvatars = memo(({ avatarResolver = () => null, usernames = [], className, style = {} }) => { + const avatars = usernames.filter(Boolean); + + if (!avatars.length) { + return null; + } + + return ( +
+ {avatars.map((username) => ( + + ))} +
+ ); +}); diff --git a/packages/livechat/src/components/Messages/MessageBubble/styles.scss b/packages/livechat/src/components/Messages/MessageBubble/styles.scss index 1d1e8bc9664e..e155aa194c24 100644 --- a/packages/livechat/src/components/Messages/MessageBubble/styles.scss +++ b/packages/livechat/src/components/Messages/MessageBubble/styles.scss @@ -12,11 +12,8 @@ $message-bubble-me-link-color: $color-text-lighter; $message-bubble-nude-padding: 0; $message-bubble-quoted-padding: 12px 12px 12px 0; $message-bubble-quoted-indicator-width: 3px; -$message-bubble-quoted-indicator-margin: - (-2 * $message-bubble-quoted-indicator-width) - (2 * $message-bubble-quoted-indicator-width) - (-2 * $message-bubble-quoted-indicator-width) - $message-bubble-quoted-indicator-width; +$message-bubble-quoted-indicator-margin: (-2 * $message-bubble-quoted-indicator-width) (2 * $message-bubble-quoted-indicator-width) + (-2 * $message-bubble-quoted-indicator-width) $message-bubble-quoted-indicator-width; $message-bubble-quoted-indicator-border-radius: $default-border-radius; $message-bubble-quoted-indicator-color: $color-green; @@ -28,7 +25,7 @@ $message-bubble-quoted-indicator-color: $color-green; color: $message-bubble-color; border-radius: $message-bubble-border-radius; - background-color: $message-bubble-background-color; + background-color: var(--receiver-bubble-background-color, $message-bubble-background-color); align-items: stretch; flex-flow: row nowrap; justify-content: flex-start; @@ -39,7 +36,7 @@ $message-bubble-quoted-indicator-color: $color-green; &--inverse { color: var(--font-color, $message-bubble-me-color); - background-color: var(--color, $message-bubble-me-background-color); + background-color: var(--sender-bubble-background-color, var(--color, $message-bubble-me-background-color)); a { color: var(--font-color, $message-bubble-me-link-color); @@ -55,13 +52,13 @@ $message-bubble-quoted-indicator-color: $color-green; &--quoted { padding: $message-bubble-quoted-padding; - background-color: $message-bubble-background-color; + background-color: var(--receiver-bubble-background-color, $message-bubble-background-color); &::before { width: $message-bubble-quoted-indicator-width; margin: -6px 6px -6px 3px; - content: ""; + content: ''; border-radius: $message-bubble-quoted-indicator-border-radius; background-color: $message-bubble-quoted-indicator-color; diff --git a/packages/livechat/src/components/Messages/MessageList/index.js b/packages/livechat/src/components/Messages/MessageList/index.js index 9e94a5b83893..cd7e2017397f 100644 --- a/packages/livechat/src/components/Messages/MessageList/index.js +++ b/packages/livechat/src/components/Messages/MessageList/index.js @@ -144,8 +144,8 @@ export class MessageList extends MemoizedComponent { typingUsernames, }) => { const items = []; - const { incomingCallAlert } = store.state; - const { ongoingCall } = store.state; + const { incomingCallAlert, ongoingCall } = store.state; + const { hideSenderAvatar = false, hideReceiverAvatar = false } = this.props || {}; for (let i = 0; i < messages.length; ++i) { const previousMessage = messages[i - 1]; @@ -180,6 +180,7 @@ export class MessageList extends MemoizedComponent { items.push(); } + const isMe = uid && message.u && uid === message.u._id; items.push( open ? ( -
+
{children}
diff --git a/packages/livechat/src/components/Screen/ChatButton.tsx b/packages/livechat/src/components/Screen/ChatButton.tsx new file mode 100644 index 000000000000..9eb0391fbe51 --- /dev/null +++ b/packages/livechat/src/components/Screen/ChatButton.tsx @@ -0,0 +1,29 @@ +import ChatIcon from '../../icons/chat.svg'; +import CloseIcon from '../../icons/close.svg'; +import { Button } from '../Button'; + +type ChatButtonProps = { + text: string; + minimized: boolean; + badge: number; + onClick: () => void; + triggered?: boolean; + className?: string; + logoUrl?: string; +}; + +export const ChatButton = ({ text, minimized, badge, onClick, triggered = false, className, logoUrl }: ChatButtonProps) => { + const openIcon = logoUrl ? Livechat : ; + + return ( + + ); +}; diff --git a/packages/livechat/src/components/Screen/ScreenProvider.tsx b/packages/livechat/src/components/Screen/ScreenProvider.tsx index 8243879f2549..1d1295b126b3 100644 --- a/packages/livechat/src/components/Screen/ScreenProvider.tsx +++ b/packages/livechat/src/components/Screen/ScreenProvider.tsx @@ -6,9 +6,11 @@ import { parse } from 'query-string'; import { isActiveSession } from '../../helpers/isActiveSession'; import { parentCall } from '../../lib/parentCall'; import Triggers from '../../lib/triggers'; -import store, { StoreContext } from '../../store'; +import { StoreContext } from '../../store'; export type ScreenContextValue = { + hideWatermark: boolean; + livechatLogo: { url: string } | undefined; notificationsEnabled: boolean; minimized: boolean; expanded: boolean; @@ -27,9 +29,15 @@ export type ScreenContextValue = { onDismissAlert: () => unknown; dismissNotification: () => void; theme?: { - color: string; - fontColor: string; - iconColor: string; + color?: string; + fontColor?: string; + iconColor?: string; + position?: 'left' | 'right'; + guestBubbleBackgroundColor?: string; + agentBubbleBackgroundColor?: string; + background?: string; + hideGuestAvatar?: boolean; + hideAgentAvatar?: boolean; }; }; @@ -38,6 +46,8 @@ export const ScreenContext = createContext({ color: '', fontColor: '', iconColor: '', + hideAgentAvatar: false, + hideGuestAvatar: true, }, notificationsEnabled: true, minimized: true, @@ -50,13 +60,42 @@ export const ScreenContext = createContext({ } as ScreenContextValue); export const ScreenProvider: FunctionalComponent = ({ children }) => { - const { dispatch, config, sound, minimized = true, undocked, expanded = false, alerts, modal, iframe } = useContext(StoreContext); + const { + dispatch, + config, + sound, + minimized = true, + undocked, + expanded = false, + alerts, + modal, + iframe, + ...store + } = useContext(StoreContext); const { department, name, email } = iframe.guest || {}; - const { color } = config.theme || {}; - const { color: customColor, fontColor: customFontColor, iconColor: customIconColor } = iframe.theme || {}; + const { color, position: configPosition, background } = config.theme || {}; + const { livechatLogo, hideWatermark = false } = config.settings || {}; + + const { + color: customColor, + fontColor: customFontColor, + iconColor: customIconColor, + guestBubbleBackgroundColor, + agentBubbleBackgroundColor, + position: customPosition, + background: customBackground, + hideAgentAvatar = false, + hideGuestAvatar = true, + } = iframe.theme || {}; const [poppedOut, setPopedOut] = useState(false); + const position = customPosition || configPosition || 'right'; + + useEffect(() => { + parentCall('setWidgetPosition', position || 'right'); + }, [position]); + const handleEnableNotifications = () => { dispatch({ sound: { ...sound, enabled: true } }); }; @@ -118,11 +157,19 @@ export const ScreenProvider: FunctionalComponent = ({ children }) => { color: customColor || color, fontColor: customFontColor, iconColor: customIconColor, + position, + guestBubbleBackgroundColor, + agentBubbleBackgroundColor, + background: customBackground || background, + hideAgentAvatar, + hideGuestAvatar, }, notificationsEnabled: sound?.enabled, minimized: !poppedOut && (minimized || undocked), expanded: !minimized && expanded, windowed: !minimized && poppedOut, + livechatLogo, + hideWatermark, sound, alerts, modal, diff --git a/packages/livechat/src/components/Screen/index.js b/packages/livechat/src/components/Screen/index.js index f9bf036e19e2..4123a832d8ee 100644 --- a/packages/livechat/src/components/Screen/index.js +++ b/packages/livechat/src/components/Screen/index.js @@ -1,12 +1,12 @@ import { useContext, useEffect } from 'preact/hooks'; import { createClassName } from '../../helpers/createClassName'; -import ChatIcon from '../../icons/chat.svg'; import CloseIcon from '../../icons/close.svg'; import { Button } from '../Button'; import { Footer, FooterContent, PoweredBy } from '../Footer'; import { PopoverContainer } from '../Popover'; import { Sound } from '../Sound'; +import { ChatButton } from './ChatButton'; import ScreenHeader from './Header'; import { ScreenContext } from './ScreenProvider'; import styles from './styles.scss'; @@ -15,29 +15,20 @@ export const ScreenContent = ({ children, nopadding, triggered = false, full = f
{children}
); -export const ScreenFooter = ({ children, options, limit }) => ( -
- {children && {children}} - - {options} - {limit} - - -
-); +export const ScreenFooter = ({ children, options, limit }) => { + const { hideWatermark } = useContext(ScreenContext); -const ChatButton = ({ text, minimized, badge, onClick, triggered = false, agent }) => ( - -); + return ( +
+ {children && {children}} + + {options} + {limit} + {!hideWatermark && } + +
+ ); +}; const CssVar = ({ theme }) => { useEffect(() => { @@ -69,6 +60,9 @@ const CssVar = ({ theme }) => { ${theme.color ? `--color: ${theme.color};` : ''} ${theme.fontColor ? `--font-color: ${theme.fontColor};` : ''} ${theme.iconColor ? `--icon-color: ${theme.iconColor};` : ''} + ${theme.guestBubbleBackgroundColor ? `--sender-bubble-background-color: ${theme.guestBubbleBackgroundColor};` : ''} + ${theme.agentBubbleBackgroundColor ? `--receiver-bubble-background-color: ${theme.agentBubbleBackgroundColor};` : ''} + ${theme.background ? `--message-list-background: ${theme.background};` : ''} } `} ); @@ -78,6 +72,7 @@ const CssVar = ({ theme }) => { export const Screen = ({ title, color, agent, children, className, unread, triggered = false, queueInfo, onSoundStop }) => { const { theme = {}, + livechatLogo, notificationsEnabled, minimized = false, expanded = false, @@ -95,7 +90,15 @@ export const Screen = ({ title, color, agent, children, className, unread, trigg } = useContext(ScreenContext); return ( -
+
{triggered && (