From a075950e2351898f248895b9e6c5abe02a0dbdcb Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Wed, 27 Dec 2023 18:07:51 +0000 Subject: [PATCH 001/152] Bump 6.5.2 --- .changeset/bump-patch-1703700471583.md | 5 +++++ yarn.lock | 16 ++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 .changeset/bump-patch-1703700471583.md diff --git a/.changeset/bump-patch-1703700471583.md b/.changeset/bump-patch-1703700471583.md new file mode 100644 index 000000000000..e1eaa7980afb --- /dev/null +++ b/.changeset/bump-patch-1703700471583.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/yarn.lock b/yarn.lock index e90e17ebfdaf..e74b35ec9f98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8217,9 +8217,9 @@ __metadata: "@rocket.chat/icons": "*" "@rocket.chat/prettier-config": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-contexts": 3.0.0 + "@rocket.chat/ui-contexts": 3.0.1 "@rocket.chat/ui-kit": "*" - "@rocket.chat/ui-video-conf": 3.0.0 + "@rocket.chat/ui-video-conf": 3.0.1 "@tanstack/react-query": "*" react: "*" react-dom: "*" @@ -8301,14 +8301,14 @@ __metadata: ts-jest: ~29.0.5 typescript: ~5.2.2 peerDependencies: - "@rocket.chat/core-typings": 6.5.0 + "@rocket.chat/core-typings": 6.5.1 "@rocket.chat/css-in-js": "*" "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-tokens": "*" "@rocket.chat/message-parser": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-client": 3.0.0 - "@rocket.chat/ui-contexts": 3.0.0 + "@rocket.chat/ui-client": 3.0.1 + "@rocket.chat/ui-contexts": 3.0.1 katex: "*" react: "*" languageName: unknown @@ -9469,7 +9469,7 @@ __metadata: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" - "@rocket.chat/ui-contexts": 3.0.0 + "@rocket.chat/ui-contexts": 3.0.1 react: ~17.0.2 languageName: unknown linkType: soft @@ -9622,7 +9622,7 @@ __metadata: "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-contexts": 3.0.0 + "@rocket.chat/ui-contexts": 3.0.1 react: ^17.0.2 react-dom: ^17.0.2 languageName: unknown @@ -9708,7 +9708,7 @@ __metadata: peerDependencies: "@rocket.chat/layout": "*" "@rocket.chat/tools": "*" - "@rocket.chat/ui-contexts": 3.0.0 + "@rocket.chat/ui-contexts": 3.0.1 "@tanstack/react-query": "*" react: "*" react-hook-form: "*" From c2e7116a6b522c245e60b3e323fe1fb9ee2f6ee1 Mon Sep 17 00:00:00 2001 From: Sayan4444 <112304873+Sayan4444@users.noreply.github.com> Date: Tue, 2 Jan 2024 21:19:25 +0530 Subject: [PATCH 002/152] fix: close image gallery on backdrop click (#31344) --- .../client/components/ImageGallery/ImageGallery.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx index 0675532432aa..ebce7d42e541 100644 --- a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx +++ b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx @@ -109,10 +109,10 @@ const ImageGallery = () => { return createPortal( -
+
- - + e.stopPropagation()} /> + e.stopPropagation()} /> { {images?.map(({ _id, url }) => (
- + e.stopPropagation()} />
From 84c4b0709e3e5f4657b36d12464627462fdf7e95 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Wed, 27 Dec 2023 08:52:00 -0300 Subject: [PATCH 003/152] fix: inquiries being limited to 50 items (#31267) --- .changeset/three-moles-look.md | 5 +++++ .../livechat/client/lib/stream/queueManager.ts | 16 +++++++++++----- .../client/providers/OmnichannelProvider.tsx | 9 +++------ 3 files changed, 19 insertions(+), 11 deletions(-) create mode 100644 .changeset/three-moles-look.md diff --git a/.changeset/three-moles-look.md b/.changeset/three-moles-look.md new file mode 100644 index 000000000000..a6429fb6197e --- /dev/null +++ b/.changeset/three-moles-look.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed conversations in queue being limited to 50 items diff --git a/apps/meteor/app/livechat/client/lib/stream/queueManager.ts b/apps/meteor/app/livechat/client/lib/stream/queueManager.ts index 906ace402bb9..5ba2cf0d9791 100644 --- a/apps/meteor/app/livechat/client/lib/stream/queueManager.ts +++ b/apps/meteor/app/livechat/client/lib/stream/queueManager.ts @@ -2,6 +2,7 @@ import type { ILivechatDepartment, ILivechatInquiryRecord, IOmnichannelAgent } f import { queryClient } from '../../../../../client/lib/queryClient'; import { callWithErrorHandling } from '../../../../../client/lib/utils/callWithErrorHandling'; +import { settings } from '../../../../settings/client'; import { sdk } from '../../../../utils/client/lib/SDKClient'; import { LivechatInquiry } from '../../collections/LivechatInquiry'; @@ -39,7 +40,8 @@ const removeInquiry = async (inquiry: ILivechatInquiryRecord) => { }; const getInquiriesFromAPI = async () => { - const { inquiries } = await sdk.rest.get('/v1/livechat/inquiries.queuedForUser', {}); + const count = settings.get('Livechat_guest_pool_max_number_incoming_livechats_displayed') ?? 0; + const { inquiries } = await sdk.rest.get('/v1/livechat/inquiries.queuedForUser', { count }); return inquiries; }; @@ -96,9 +98,12 @@ const subscribe = async (userId: IOmnichannelAgent['_id']) => { // Register to all depts + public queue always to match the inquiry list returned by backend const cleanDepartmentListeners = addListenerForeachDepartment(agentDepartments); const globalCleanup = addGlobalListener(); - const inquiriesFromAPI = (await getInquiriesFromAPI()) as unknown as ILivechatInquiryRecord[]; - await updateInquiries(inquiriesFromAPI); + const computation = Tracker.autorun(async () => { + const inquiriesFromAPI = (await getInquiriesFromAPI()) as unknown as ILivechatInquiryRecord[]; + + await updateInquiries(inquiriesFromAPI); + }); return () => { LivechatInquiry.remove({}); @@ -106,14 +111,15 @@ const subscribe = async (userId: IOmnichannelAgent['_id']) => { cleanDepartmentListeners?.(); globalCleanup?.(); departments.clear(); + computation.stop(); }; }; export const initializeLivechatInquiryStream = (() => { let cleanUp: (() => void) | undefined; - return async (...args: any[]) => { + return async (...args: Parameters) => { cleanUp?.(); - cleanUp = await subscribe(...(args as [IOmnichannelAgent['_id']])); + cleanUp = await subscribe(...args); }; })(); diff --git a/apps/meteor/client/providers/OmnichannelProvider.tsx b/apps/meteor/client/providers/OmnichannelProvider.tsx index fcf57a399905..fe1915f49eb3 100644 --- a/apps/meteor/client/providers/OmnichannelProvider.tsx +++ b/apps/meteor/client/providers/OmnichannelProvider.tsx @@ -41,7 +41,7 @@ const OmnichannelProvider: FC = ({ children }) => { const omniChannelEnabled = useSetting('Livechat_enabled') as boolean; const omnichannelRouting = useSetting('Livechat_Routing_Method'); const showOmnichannelQueueLink = useSetting('Livechat_show_queue_list_link') as boolean; - const omnichannelPoolMaxIncoming = useSetting('Livechat_guest_pool_max_number_incoming_livechats_displayed') as number; + const omnichannelPoolMaxIncoming = useSetting('Livechat_guest_pool_max_number_incoming_livechats_displayed') ?? 0; const omnichannelSortingMechanism = useSetting('Omnichannel_sorting_mechanism') as OmnichannelSortingMechanismSettingType; const loggerRef = useRef(new ClientLogger('OmnichannelProvider')); @@ -130,16 +130,13 @@ const OmnichannelProvider: FC = ({ children }) => { } return LivechatInquiry.find( - { - status: 'queued', - $or: [{ defaultAgent: { $exists: false } }, { 'defaultAgent.agentId': user?._id }], - }, + { status: 'queued' }, { sort: getOmniChatSortQuery(omnichannelSortingMechanism), limit: omnichannelPoolMaxIncoming, }, ).fetch(); - }, [manuallySelected, omnichannelPoolMaxIncoming, omnichannelSortingMechanism, user?._id]), + }, [manuallySelected, omnichannelPoolMaxIncoming, omnichannelSortingMechanism]), ); queue?.map(({ rid }) => { From fb7bfb0277073c6e60844480036d276536e113a7 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 2 Jan 2024 18:01:13 -0300 Subject: [PATCH 004/152] ci: update release PR description with change log --- .github/workflows/pr-update-description.yml | 39 ++++++++++ packages/release-action/src/index.ts | 3 + .../release-action/src/updatePRDescription.ts | 75 +++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 .github/workflows/pr-update-description.yml create mode 100644 packages/release-action/src/updatePRDescription.ts diff --git a/.github/workflows/pr-update-description.yml b/.github/workflows/pr-update-description.yml new file mode 100644 index 000000000000..71b4ffeda801 --- /dev/null +++ b/.github/workflows/pr-update-description.yml @@ -0,0 +1,39 @@ +name: 'Release PR Description' + +on: + pull_request: + branches: + - master + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + update-pr: + runs-on: ubuntu-latest + if: startsWith(github.head_ref, 'release-') + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.CI_PAT }} + + - name: Setup NodeJS + uses: ./.github/actions/setup-node + with: + node-version: 14.21.3 + cache-modules: true + install: true + + - uses: dtinth/setup-github-actions-caching-for-turbo@v1 + + - name: Build packages + run: yarn build + + - name: Update PR description + uses: ./packages/release-action + with: + action: update-pr-description + env: + GITHUB_TOKEN: ${{ secrets.CI_PAT }} + diff --git a/packages/release-action/src/index.ts b/packages/release-action/src/index.ts index ca9dab36ac65..51956e9e2a8d 100644 --- a/packages/release-action/src/index.ts +++ b/packages/release-action/src/index.ts @@ -7,6 +7,7 @@ import { bumpNextVersion } from './bumpNextVersion'; import { setupGitUser } from './gitUtils'; import { publishRelease } from './publishRelease'; import { startPatchRelease } from './startPatchRelease'; +import { updatePRDescription } from './updatePRDescription'; // const getOptionalInput = (name: string) => core.getInput(name) || undefined; @@ -45,6 +46,8 @@ import { startPatchRelease } from './startPatchRelease'; await bumpNextVersion({ githubToken, mainPackagePath }); } else if (action === 'patch') { await startPatchRelease({ baseRef, githubToken, mainPackagePath }); + } else if (action === 'update-pr-description') { + await updatePRDescription({ githubToken, mainPackagePath }); } })().catch((err) => { core.error(err); diff --git a/packages/release-action/src/updatePRDescription.ts b/packages/release-action/src/updatePRDescription.ts new file mode 100644 index 000000000000..0fc6386636ea --- /dev/null +++ b/packages/release-action/src/updatePRDescription.ts @@ -0,0 +1,75 @@ +import fs from 'fs'; +import path from 'path'; + +import * as core from '@actions/core'; +import { exec } from '@actions/exec'; +import * as github from '@actions/github'; + +import { setupOctokit } from './setupOctokit'; +import { getChangelogEntry, getEngineVersionsMd, readPackageJson } from './utils'; + +export async function updatePRDescription({ + githubToken, + mainPackagePath, + cwd = process.cwd(), +}: { + githubToken: string; + mainPackagePath: string; + cwd?: string; +}) { + const octokit = setupOctokit(githubToken); + + // generate change logs from changesets + await exec('yarn', ['changeset', 'version']); + + // get version from main package + const { version: newVersion } = await readPackageJson(mainPackagePath); + + const mainPackageChangelog = path.join(mainPackagePath, 'CHANGELOG.md'); + + const changelogContents = fs.readFileSync(mainPackageChangelog, 'utf8'); + const changelogEntry = getChangelogEntry(changelogContents, newVersion); + if (!changelogEntry) { + // we can find a changelog but not the entry for this version + // if this is true, something has probably gone wrong + throw new Error('Could not find changelog entry for version newVersion'); + } + + const releaseBody = (await getEngineVersionsMd(cwd)) + changelogEntry.content; + + core.info('get PR description'); + const result = await octokit.rest.pulls.get({ + pull_number: github.context.issue.number, + body: releaseBody, + ...github.context.repo, + }); + + const { body: originalBody = '' } = result.data; + + const cleanBody = originalBody?.replace(/.*/s, '').trim() || ''; + + const bodyUpdated = `${cleanBody} + + + + +_You can see below a preview of the release change log:_ + +# ${newVersion} + +${releaseBody} +`; + + if (bodyUpdated === originalBody) { + core.info('no changes to PR description'); + return; + } + + core.info('update PR description'); + await octokit.rest.pulls.update({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + pull_number: github.context.issue.number, + body: bodyUpdated, + }); +} From 886d92009eb481ecc5097f35983e9bf1444a9c3c Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 2 Jan 2024 20:18:07 -0300 Subject: [PATCH 005/152] fix: wrong value used for workspace registration --- .changeset/tiny-glasses-help.md | 5 +++++ .../app/cloud/server/functions/buildRegistrationData.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/tiny-glasses-help.md diff --git a/.changeset/tiny-glasses-help.md b/.changeset/tiny-glasses-help.md new file mode 100644 index 000000000000..676c68763a87 --- /dev/null +++ b/.changeset/tiny-glasses-help.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fix wrong value used for Workspace Registration diff --git a/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts b/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts index cc1af6fb1767..07db80c5cca9 100644 --- a/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts +++ b/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts @@ -65,7 +65,7 @@ export async function buildWorkspaceRegistrationData('Language'); const organizationType = settings.get('Organization_Type'); const industry = settings.get('Industry'); - const orgSize = settings.get('Organization_Size'); + const orgSize = settings.get('Size'); const workspaceType = settings.get('Server_Type'); const seats = await Users.getActiveLocalUserCount(); From d11bf17012662b1439c09532b38dce3fca02adce Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Wed, 3 Jan 2024 01:04:49 +0000 Subject: [PATCH 006/152] Release 6.5.2 [no ci] --- .changeset/bump-patch-1703700471583.md | 5 ---- .changeset/three-moles-look.md | 5 ---- .changeset/tiny-glasses-help.md | 5 ---- apps/meteor/.docker/Dockerfile.rhel | 2 +- apps/meteor/CHANGELOG.md | 29 +++++++++++++++++++ apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/ee/server/services/CHANGELOG.md | 10 +++++++ apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 4 +-- ee/apps/account-service/CHANGELOG.md | 10 +++++++ ee/apps/account-service/package.json | 2 +- ee/apps/authorization-service/CHANGELOG.md | 10 +++++++ ee/apps/authorization-service/package.json | 2 +- ee/apps/ddp-streamer/CHANGELOG.md | 12 ++++++++ ee/apps/ddp-streamer/package.json | 2 +- ee/apps/omnichannel-transcript/CHANGELOG.md | 11 +++++++ ee/apps/omnichannel-transcript/package.json | 2 +- ee/apps/presence-service/CHANGELOG.md | 10 +++++++ ee/apps/presence-service/package.json | 2 +- ee/apps/queue-worker/CHANGELOG.md | 10 +++++++ ee/apps/queue-worker/package.json | 2 +- ee/apps/stream-hub-service/CHANGELOG.md | 9 ++++++ ee/apps/stream-hub-service/package.json | 2 +- ee/packages/api-client/CHANGELOG.md | 7 +++++ ee/packages/api-client/package.json | 2 +- ee/packages/ddp-client/CHANGELOG.md | 7 +++++ ee/packages/ddp-client/package.json | 2 +- ee/packages/license/CHANGELOG.md | 6 ++++ ee/packages/license/package.json | 2 +- ee/packages/omnichannel-services/CHANGELOG.md | 11 +++++++ ee/packages/omnichannel-services/package.json | 2 +- ee/packages/pdf-worker/CHANGELOG.md | 6 ++++ ee/packages/pdf-worker/package.json | 2 +- ee/packages/presence/CHANGELOG.md | 8 +++++ ee/packages/presence/package.json | 2 +- package.json | 2 +- packages/core-services/CHANGELOG.md | 8 +++++ packages/core-services/package.json | 2 +- packages/core-typings/CHANGELOG.md | 2 ++ packages/core-typings/package.json | 2 +- packages/cron/CHANGELOG.md | 7 +++++ packages/cron/package.json | 2 +- packages/fuselage-ui-kit/CHANGELOG.md | 8 +++++ packages/fuselage-ui-kit/package.json | 6 ++-- packages/gazzodown/CHANGELOG.md | 8 +++++ packages/gazzodown/package.json | 8 ++--- packages/instance-status/CHANGELOG.md | 6 ++++ packages/instance-status/package.json | 2 +- packages/livechat/CHANGELOG.md | 6 ++++ packages/livechat/package.json | 2 +- packages/model-typings/CHANGELOG.md | 6 ++++ packages/model-typings/package.json | 2 +- packages/models/CHANGELOG.md | 6 ++++ packages/models/package.json | 2 +- packages/rest-typings/CHANGELOG.md | 6 ++++ packages/rest-typings/package.json | 2 +- packages/ui-client/CHANGELOG.md | 6 ++++ packages/ui-client/package.json | 4 +-- packages/ui-contexts/CHANGELOG.md | 8 +++++ packages/ui-contexts/package.json | 2 +- packages/ui-video-conf/CHANGELOG.md | 6 ++++ packages/ui-video-conf/package.json | 4 +-- packages/uikit-playground/CHANGELOG.md | 7 +++++ packages/uikit-playground/package.json | 2 +- packages/web-ui-registration/CHANGELOG.md | 6 ++++ packages/web-ui-registration/package.json | 4 +-- 66 files changed, 294 insertions(+), 57 deletions(-) delete mode 100644 .changeset/bump-patch-1703700471583.md delete mode 100644 .changeset/three-moles-look.md delete mode 100644 .changeset/tiny-glasses-help.md diff --git a/.changeset/bump-patch-1703700471583.md b/.changeset/bump-patch-1703700471583.md deleted file mode 100644 index e1eaa7980afb..000000000000 --- a/.changeset/bump-patch-1703700471583.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Bump @rocket.chat/meteor version. diff --git a/.changeset/three-moles-look.md b/.changeset/three-moles-look.md deleted file mode 100644 index a6429fb6197e..000000000000 --- a/.changeset/three-moles-look.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed conversations in queue being limited to 50 items diff --git a/.changeset/tiny-glasses-help.md b/.changeset/tiny-glasses-help.md deleted file mode 100644 index 676c68763a87..000000000000 --- a/.changeset/tiny-glasses-help.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fix wrong value used for Workspace Registration diff --git a/apps/meteor/.docker/Dockerfile.rhel b/apps/meteor/.docker/Dockerfile.rhel index 200b67422197..48bc616940c9 100644 --- a/apps/meteor/.docker/Dockerfile.rhel +++ b/apps/meteor/.docker/Dockerfile.rhel @@ -1,6 +1,6 @@ FROM registry.access.redhat.com/ubi8/nodejs-12 -ENV RC_VERSION 6.5.1 +ENV RC_VERSION 6.5.2 MAINTAINER buildmaster@rocket.chat diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 125ba1541bf2..965e0bacbcf9 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,34 @@ # @rocket.chat/meteor +## 6.5.2 + +### Patch Changes + +- a075950e23: Bump @rocket.chat/meteor version. +- Bump @rocket.chat/meteor version. +- 84c4b0709e: Fixed conversations in queue being limited to 50 items +- 886d92009e: Fix wrong value used for Workspace Registration + - @rocket.chat/core-typings@6.5.2 + - @rocket.chat/rest-typings@6.5.2 + - @rocket.chat/api-client@0.1.20 + - @rocket.chat/license@0.1.2 + - @rocket.chat/omnichannel-services@0.1.2 + - @rocket.chat/pdf-worker@0.0.26 + - @rocket.chat/presence@0.1.2 + - @rocket.chat/core-services@0.3.2 + - @rocket.chat/cron@0.0.22 + - @rocket.chat/gazzodown@3.0.2 + - @rocket.chat/model-typings@0.2.2 + - @rocket.chat/ui-contexts@3.0.2 + - @rocket.chat/server-cloud-communication@0.0.1 + - @rocket.chat/fuselage-ui-kit@3.0.2 + - @rocket.chat/models@0.0.26 + - @rocket.chat/ui-theming@0.1.1 + - @rocket.chat/ui-client@3.0.2 + - @rocket.chat/ui-video-conf@3.0.2 + - @rocket.chat/web-ui-registration@3.0.2 + - @rocket.chat/instance-status@0.0.26 + ## 6.5.1 ### Patch Changes diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 5072eaeef660..f8a662f2e1c6 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "6.5.1" + "version": "6.5.2" } diff --git a/apps/meteor/ee/server/services/CHANGELOG.md b/apps/meteor/ee/server/services/CHANGELOG.md index 32506d52706c..ec85085468e1 100644 --- a/apps/meteor/ee/server/services/CHANGELOG.md +++ b/apps/meteor/ee/server/services/CHANGELOG.md @@ -1,5 +1,15 @@ # rocketchat-services +## 1.1.20 + +### Patch Changes + +- @rocket.chat/core-typings@6.5.2 +- @rocket.chat/rest-typings@6.5.2 +- @rocket.chat/core-services@0.3.2 +- @rocket.chat/model-typings@0.2.2 +- @rocket.chat/models@0.0.26 + ## 1.1.19 ### Patch Changes diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index c44ad6cf5849..f3eae1cb06a6 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -1,7 +1,7 @@ { "name": "rocketchat-services", "private": true, - "version": "1.1.19", + "version": "1.1.20", "description": "Rocket.Chat Authorization service", "main": "index.js", "scripts": { diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 35f4653ced08..382808842576 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "6.5.1", + "version": "6.5.2", "private": true, "author": { "name": "Rocket.Chat", @@ -77,7 +77,7 @@ "@rocket.chat/livechat": "workspace:^", "@rocket.chat/mock-providers": "workspace:^", "@settlin/spacebars-loader": "^1.0.9", - "@storybook/addon-a11y": "6.5.16", + "@storybook/addon-a11y": "6.5.26", "@storybook/addon-essentials": "~6.5.16", "@storybook/addon-interactions": "~6.5.16", "@storybook/addon-postcss": "~2.0.0", diff --git a/ee/apps/account-service/CHANGELOG.md b/ee/apps/account-service/CHANGELOG.md index e85436ebfb4f..d8ba4f0ed840 100644 --- a/ee/apps/account-service/CHANGELOG.md +++ b/ee/apps/account-service/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/account-service +## 0.3.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.5.2 +- @rocket.chat/rest-typings@6.5.2 +- @rocket.chat/core-services@0.3.2 +- @rocket.chat/model-typings@0.2.2 +- @rocket.chat/models@0.0.26 + ## 0.3.1 ### Patch Changes diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index b40c269b1728..8a2d63f9f233 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/account-service", "private": true, - "version": "0.3.1", + "version": "0.3.2", "description": "Rocket.Chat Account service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/authorization-service/CHANGELOG.md b/ee/apps/authorization-service/CHANGELOG.md index c66e47fe7af3..232fff7f9b30 100644 --- a/ee/apps/authorization-service/CHANGELOG.md +++ b/ee/apps/authorization-service/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/authorization-service +## 0.3.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.5.2 +- @rocket.chat/rest-typings@6.5.2 +- @rocket.chat/core-services@0.3.2 +- @rocket.chat/model-typings@0.2.2 +- @rocket.chat/models@0.0.26 + ## 0.3.1 ### Patch Changes diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index 6d6797fab894..838908ecda35 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/authorization-service", "private": true, - "version": "0.3.1", + "version": "0.3.2", "description": "Rocket.Chat Authorization service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/ddp-streamer/CHANGELOG.md b/ee/apps/ddp-streamer/CHANGELOG.md index 0aff47ff570e..afb1f595b128 100644 --- a/ee/apps/ddp-streamer/CHANGELOG.md +++ b/ee/apps/ddp-streamer/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/ddp-streamer +## 0.2.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.5.2 +- @rocket.chat/rest-typings@6.5.2 +- @rocket.chat/core-services@0.3.2 +- @rocket.chat/model-typings@0.2.2 +- @rocket.chat/ui-contexts@3.0.2 +- @rocket.chat/models@0.0.26 +- @rocket.chat/instance-status@0.0.26 + ## 0.2.1 ### Patch Changes diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index fa36b8fb0eae..2c31661254f9 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/ddp-streamer", "private": true, - "version": "0.2.1", + "version": "0.2.2", "description": "Rocket.Chat DDP-Streamer service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/omnichannel-transcript/CHANGELOG.md b/ee/apps/omnichannel-transcript/CHANGELOG.md index 731eb6908a83..c19a77dd775b 100644 --- a/ee/apps/omnichannel-transcript/CHANGELOG.md +++ b/ee/apps/omnichannel-transcript/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/omnichannel-transcript +## 0.3.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.5.2 +- @rocket.chat/omnichannel-services@0.1.2 +- @rocket.chat/pdf-worker@0.0.26 +- @rocket.chat/core-services@0.3.2 +- @rocket.chat/model-typings@0.2.2 +- @rocket.chat/models@0.0.26 + ## 0.3.1 ### Patch Changes diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 421eebe9fbf8..ef2abfafdc5c 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/omnichannel-transcript", "private": true, - "version": "0.3.1", + "version": "0.3.2", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/presence-service/CHANGELOG.md b/ee/apps/presence-service/CHANGELOG.md index f233462ab0f7..00f7155544e3 100644 --- a/ee/apps/presence-service/CHANGELOG.md +++ b/ee/apps/presence-service/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/presence-service +## 0.3.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.5.2 +- @rocket.chat/presence@0.1.2 +- @rocket.chat/core-services@0.3.2 +- @rocket.chat/model-typings@0.2.2 +- @rocket.chat/models@0.0.26 + ## 0.3.1 ### Patch Changes diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index b0001eb9c894..0cab51cab390 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/presence-service", "private": true, - "version": "0.3.1", + "version": "0.3.2", "description": "Rocket.Chat Presence service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/queue-worker/CHANGELOG.md b/ee/apps/queue-worker/CHANGELOG.md index e54cc9fda23c..e4d3d2b7036d 100644 --- a/ee/apps/queue-worker/CHANGELOG.md +++ b/ee/apps/queue-worker/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/queue-worker +## 0.3.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.5.2 +- @rocket.chat/omnichannel-services@0.1.2 +- @rocket.chat/core-services@0.3.2 +- @rocket.chat/model-typings@0.2.2 +- @rocket.chat/models@0.0.26 + ## 0.3.1 ### Patch Changes diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index 25d536e8d11e..075701deb928 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/queue-worker", "private": true, - "version": "0.3.1", + "version": "0.3.2", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/stream-hub-service/CHANGELOG.md b/ee/apps/stream-hub-service/CHANGELOG.md index d4e2438d7ec7..e541ff27dff4 100644 --- a/ee/apps/stream-hub-service/CHANGELOG.md +++ b/ee/apps/stream-hub-service/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/stream-hub-service +## 0.3.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.5.2 +- @rocket.chat/core-services@0.3.2 +- @rocket.chat/model-typings@0.2.2 +- @rocket.chat/models@0.0.26 + ## 0.3.1 ### Patch Changes diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index 3e39bde7f50e..8ee78e5e5162 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/stream-hub-service", "private": true, - "version": "0.3.1", + "version": "0.3.2", "description": "Rocket.Chat Stream Hub service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/packages/api-client/CHANGELOG.md b/ee/packages/api-client/CHANGELOG.md index 3407d68b664a..40d608800fe4 100644 --- a/ee/packages/api-client/CHANGELOG.md +++ b/ee/packages/api-client/CHANGELOG.md @@ -1,5 +1,12 @@ # @rocket.chat/api-client +## 0.1.20 + +### Patch Changes + +- @rocket.chat/core-typings@6.5.2 +- @rocket.chat/rest-typings@6.5.2 + ## 0.1.19 ### Patch Changes diff --git a/ee/packages/api-client/package.json b/ee/packages/api-client/package.json index 6010be23c26d..648b3c68f8d0 100644 --- a/ee/packages/api-client/package.json +++ b/ee/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/api-client", - "version": "0.1.19", + "version": "0.1.20", "devDependencies": { "@swc/core": "^1.3.95", "@swc/jest": "^0.2.29", diff --git a/ee/packages/ddp-client/CHANGELOG.md b/ee/packages/ddp-client/CHANGELOG.md index 0015f31f22f8..f02cc13f7d03 100644 --- a/ee/packages/ddp-client/CHANGELOG.md +++ b/ee/packages/ddp-client/CHANGELOG.md @@ -1,5 +1,12 @@ # @rocket.chat/ddp-client +## 0.2.11 + +### Patch Changes + +- @rocket.chat/rest-typings@6.5.2 +- @rocket.chat/api-client@0.1.20 + ## 0.2.10 ### Patch Changes diff --git a/ee/packages/ddp-client/package.json b/ee/packages/ddp-client/package.json index 375aa7d40c2c..8ac8574c0fc3 100644 --- a/ee/packages/ddp-client/package.json +++ b/ee/packages/ddp-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ddp-client", - "version": "0.2.10", + "version": "0.2.11", "devDependencies": { "@swc/core": "^1.3.95", "@swc/jest": "^0.2.29", diff --git a/ee/packages/license/CHANGELOG.md b/ee/packages/license/CHANGELOG.md index cee46b612d9d..5206d34e6014 100644 --- a/ee/packages/license/CHANGELOG.md +++ b/ee/packages/license/CHANGELOG.md @@ -1,5 +1,11 @@ # @rocket.chat/license +## 0.1.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.5.2 + ## 0.1.1 ### Patch Changes diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 890a8c156427..9ba72fd1f30d 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/license", - "version": "0.1.1", + "version": "0.1.2", "private": true, "devDependencies": { "@swc/core": "^1.3.95", diff --git a/ee/packages/omnichannel-services/CHANGELOG.md b/ee/packages/omnichannel-services/CHANGELOG.md index 75ca235fcdd2..c3eb265253bd 100644 --- a/ee/packages/omnichannel-services/CHANGELOG.md +++ b/ee/packages/omnichannel-services/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/omnichannel-services +## 0.1.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.5.2 +- @rocket.chat/rest-typings@6.5.2 +- @rocket.chat/pdf-worker@0.0.26 +- @rocket.chat/core-services@0.3.2 +- @rocket.chat/model-typings@0.2.2 +- @rocket.chat/models@0.0.26 + ## 0.1.1 ### Patch Changes diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index be89ade58553..366f15281d41 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/omnichannel-services", - "version": "0.1.1", + "version": "0.1.2", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/pdf-worker/CHANGELOG.md b/ee/packages/pdf-worker/CHANGELOG.md index e2e460560821..1769d5ef9145 100644 --- a/ee/packages/pdf-worker/CHANGELOG.md +++ b/ee/packages/pdf-worker/CHANGELOG.md @@ -1,5 +1,11 @@ # @rocket.chat/pdf-worker +## 0.0.26 + +### Patch Changes + +- @rocket.chat/core-typings@6.5.2 + ## 0.0.25 ### Patch Changes diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index dd1dc2b3b0ff..95fee579428c 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/pdf-worker", - "version": "0.0.25", + "version": "0.0.26", "private": true, "devDependencies": { "@storybook/addon-essentials": "~6.5.16", diff --git a/ee/packages/presence/CHANGELOG.md b/ee/packages/presence/CHANGELOG.md index 50d1609e7c96..4e58e342beb0 100644 --- a/ee/packages/presence/CHANGELOG.md +++ b/ee/packages/presence/CHANGELOG.md @@ -1,5 +1,13 @@ # @rocket.chat/presence +## 0.1.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.5.2 +- @rocket.chat/core-services@0.3.2 +- @rocket.chat/models@0.0.26 + ## 0.1.1 ### Patch Changes diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index cb8eab270526..f53cec0527e8 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/presence", - "version": "0.1.1", + "version": "0.1.2", "private": true, "devDependencies": { "@babel/core": "~7.22.20", diff --git a/package.json b/package.json index 364f38d1ae41..c07e168a2e6c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "6.5.1", + "version": "6.5.2", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/core-services/CHANGELOG.md b/packages/core-services/CHANGELOG.md index 15f5278724e3..d5983c60ebf9 100644 --- a/packages/core-services/CHANGELOG.md +++ b/packages/core-services/CHANGELOG.md @@ -1,5 +1,13 @@ # @rocket.chat/core-services +## 0.3.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.5.2 +- @rocket.chat/rest-typings@6.5.2 +- @rocket.chat/models@0.0.26 + ## 0.3.1 ### Patch Changes diff --git a/packages/core-services/package.json b/packages/core-services/package.json index db298d2c1a59..47aa031788ad 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/core-services", - "version": "0.3.1", + "version": "0.3.2", "private": true, "devDependencies": { "@babel/core": "~7.22.20", diff --git a/packages/core-typings/CHANGELOG.md b/packages/core-typings/CHANGELOG.md index ade8d2311ebe..2d17f87863ec 100644 --- a/packages/core-typings/CHANGELOG.md +++ b/packages/core-typings/CHANGELOG.md @@ -1,5 +1,7 @@ # @rocket.chat/core-typings +## 6.5.2 + ## 6.5.1 ### Patch Changes diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 16191fc43ed5..370aa00c7ddf 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", - "version": "6.5.1", + "version": "6.5.2", "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", "eslint": "~8.45.0", diff --git a/packages/cron/CHANGELOG.md b/packages/cron/CHANGELOG.md index 190bc292bc48..6d649476e341 100644 --- a/packages/cron/CHANGELOG.md +++ b/packages/cron/CHANGELOG.md @@ -1,5 +1,12 @@ # @rocket.chat/cron +## 0.0.22 + +### Patch Changes + +- @rocket.chat/core-typings@6.5.2 +- @rocket.chat/models@0.0.26 + ## 0.0.21 ### Patch Changes diff --git a/packages/cron/package.json b/packages/cron/package.json index dcaae8a7ae87..941209a931b8 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/cron", - "version": "0.0.21", + "version": "0.0.22", "private": true, "devDependencies": { "@types/jest": "~29.5.7", diff --git a/packages/fuselage-ui-kit/CHANGELOG.md b/packages/fuselage-ui-kit/CHANGELOG.md index 8dc3c6c9560b..fe2030f98383 100644 --- a/packages/fuselage-ui-kit/CHANGELOG.md +++ b/packages/fuselage-ui-kit/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## 3.0.2 + +### Patch Changes + +- @rocket.chat/gazzodown@3.0.2 +- @rocket.chat/ui-contexts@3.0.2 +- @rocket.chat/ui-video-conf@3.0.2 + ## 3.0.1 ### Patch Changes diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 775aa5a9428e..f05cf7312cbe 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/fuselage-ui-kit", "private": true, - "version": "3.0.1", + "version": "3.0.2", "description": "UiKit elements for Rocket.Chat Apps built under Fuselage design system", "homepage": "https://rocketchat.github.io/Rocket.Chat.Fuselage/", "author": { @@ -48,9 +48,9 @@ "@rocket.chat/icons": "*", "@rocket.chat/prettier-config": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-contexts": "3.0.1", + "@rocket.chat/ui-contexts": "3.0.2", "@rocket.chat/ui-kit": "*", - "@rocket.chat/ui-video-conf": "3.0.1", + "@rocket.chat/ui-video-conf": "3.0.2", "@tanstack/react-query": "*", "react": "*", "react-dom": "*" diff --git a/packages/gazzodown/CHANGELOG.md b/packages/gazzodown/CHANGELOG.md index acd5bdce318d..9eb50f2eb05d 100644 --- a/packages/gazzodown/CHANGELOG.md +++ b/packages/gazzodown/CHANGELOG.md @@ -1,5 +1,13 @@ # @rocket.chat/gazzodown +## 3.0.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.5.2 +- @rocket.chat/ui-contexts@3.0.2 +- @rocket.chat/ui-client@3.0.2 + ## 3.0.1 ### Patch Changes diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index d3eedfed28b2..8ccc6dbaa18e 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/gazzodown", - "version": "3.0.1", + "version": "3.0.2", "private": true, "devDependencies": { "@babel/core": "~7.22.20", @@ -65,14 +65,14 @@ "/dist" ], "peerDependencies": { - "@rocket.chat/core-typings": "6.5.1", + "@rocket.chat/core-typings": "6.5.2", "@rocket.chat/css-in-js": "*", "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-tokens": "*", "@rocket.chat/message-parser": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-client": "3.0.1", - "@rocket.chat/ui-contexts": "3.0.1", + "@rocket.chat/ui-client": "3.0.2", + "@rocket.chat/ui-contexts": "3.0.2", "katex": "*", "react": "*" }, diff --git a/packages/instance-status/CHANGELOG.md b/packages/instance-status/CHANGELOG.md index 803d8b0ccd90..d728fe76837a 100644 --- a/packages/instance-status/CHANGELOG.md +++ b/packages/instance-status/CHANGELOG.md @@ -1,5 +1,11 @@ # @rocket.chat/instance-status +## 0.0.26 + +### Patch Changes + +- @rocket.chat/models@0.0.26 + ## 0.0.25 ### Patch Changes diff --git a/packages/instance-status/package.json b/packages/instance-status/package.json index 34a98562d491..7ddea11e2ea3 100644 --- a/packages/instance-status/package.json +++ b/packages/instance-status/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/instance-status", - "version": "0.0.25", + "version": "0.0.26", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/livechat/CHANGELOG.md b/packages/livechat/CHANGELOG.md index dbe5d2dd64a9..ba3fff4290d5 100644 --- a/packages/livechat/CHANGELOG.md +++ b/packages/livechat/CHANGELOG.md @@ -1,5 +1,11 @@ # @rocket.chat/livechat Change Log +## 1.14.11 + +### Patch Changes + +- @rocket.chat/gazzodown@3.0.2 + ## 1.14.10 ### Patch Changes diff --git a/packages/livechat/package.json b/packages/livechat/package.json index f3ed1fb4c540..a6010f17ec81 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/livechat", - "version": "1.14.10", + "version": "1.14.11", "files": [ "/build" ], diff --git a/packages/model-typings/CHANGELOG.md b/packages/model-typings/CHANGELOG.md index d3cd53378808..410f6af99889 100644 --- a/packages/model-typings/CHANGELOG.md +++ b/packages/model-typings/CHANGELOG.md @@ -1,5 +1,11 @@ # @rocket.chat/model-typings +## 0.2.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.5.2 + ## 0.2.1 ### Patch Changes diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index 7aa614550194..c026b3ad8256 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/model-typings", - "version": "0.2.1", + "version": "0.2.2", "private": true, "devDependencies": { "@types/jest": "~29.5.7", diff --git a/packages/models/CHANGELOG.md b/packages/models/CHANGELOG.md index 8861ba27c492..0455bb65fe80 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/CHANGELOG.md @@ -1,5 +1,11 @@ # @rocket.chat/models +## 0.0.26 + +### Patch Changes + +- @rocket.chat/model-typings@0.2.2 + ## 0.0.25 ### Patch Changes diff --git a/packages/models/package.json b/packages/models/package.json index 3b19c6415c0e..d9281117e8dc 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/models", - "version": "0.0.25", + "version": "0.0.26", "private": true, "devDependencies": { "@types/jest": "~29.5.7", diff --git a/packages/rest-typings/CHANGELOG.md b/packages/rest-typings/CHANGELOG.md index 0e7df4130b53..2892cce58c7b 100644 --- a/packages/rest-typings/CHANGELOG.md +++ b/packages/rest-typings/CHANGELOG.md @@ -1,5 +1,11 @@ # @rocket.chat/rest-typings +## 6.5.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.5.2 + ## 6.5.1 ### Patch Changes diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index df4e2dd7179f..d7fd0127b95b 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "6.5.1", + "version": "6.5.2", "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", "@types/jest": "~29.5.7", diff --git a/packages/ui-client/CHANGELOG.md b/packages/ui-client/CHANGELOG.md index 183a47816e39..d8b0e35da31d 100644 --- a/packages/ui-client/CHANGELOG.md +++ b/packages/ui-client/CHANGELOG.md @@ -1,5 +1,11 @@ # @rocket.chat/ui-client +## 3.0.2 + +### Patch Changes + +- @rocket.chat/ui-contexts@3.0.2 + ## 3.0.1 ### Patch Changes diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index d6a6615ee67a..0c2e8a66a304 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-client", - "version": "3.0.1", + "version": "3.0.2", "private": true, "devDependencies": { "@babel/core": "~7.22.20", @@ -61,7 +61,7 @@ "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", - "@rocket.chat/ui-contexts": "3.0.1", + "@rocket.chat/ui-contexts": "3.0.2", "react": "~17.0.2" }, "volta": { diff --git a/packages/ui-contexts/CHANGELOG.md b/packages/ui-contexts/CHANGELOG.md index ec8ff0ce76ae..546adf3189ca 100644 --- a/packages/ui-contexts/CHANGELOG.md +++ b/packages/ui-contexts/CHANGELOG.md @@ -1,5 +1,13 @@ # @rocket.chat/ui-contexts +## 3.0.2 + +### Patch Changes + +- @rocket.chat/core-typings@6.5.2 +- @rocket.chat/rest-typings@6.5.2 +- @rocket.chat/ddp-client@0.2.11 + ## 3.0.1 ### Patch Changes diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 760a71bd34b9..46f2292c56fa 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-contexts", - "version": "3.0.1", + "version": "3.0.2", "private": true, "devDependencies": { "@rocket.chat/core-typings": "workspace:^", diff --git a/packages/ui-video-conf/CHANGELOG.md b/packages/ui-video-conf/CHANGELOG.md index 6a6e50ec9c5a..f96c59883d24 100644 --- a/packages/ui-video-conf/CHANGELOG.md +++ b/packages/ui-video-conf/CHANGELOG.md @@ -1,5 +1,11 @@ # @rocket.chat/ui-video-conf +## 3.0.2 + +### Patch Changes + +- @rocket.chat/ui-contexts@3.0.2 + ## 3.0.1 ### Patch Changes diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index fa64c14c0e90..c90c46ef6abb 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-video-conf", - "version": "3.0.1", + "version": "3.0.2", "private": true, "devDependencies": { "@babel/core": "~7.22.20", @@ -35,7 +35,7 @@ "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-contexts": "3.0.1", + "@rocket.chat/ui-contexts": "3.0.2", "react": "^17.0.2", "react-dom": "^17.0.2" }, diff --git a/packages/uikit-playground/CHANGELOG.md b/packages/uikit-playground/CHANGELOG.md index 19d22833be50..29ca8498fdca 100644 --- a/packages/uikit-playground/CHANGELOG.md +++ b/packages/uikit-playground/CHANGELOG.md @@ -1,5 +1,12 @@ # @rocket.chat/uikit-playground +## 0.2.11 + +### Patch Changes + +- @rocket.chat/ui-contexts@3.0.2 +- @rocket.chat/fuselage-ui-kit@3.0.2 + ## 0.2.10 ### Patch Changes diff --git a/packages/uikit-playground/package.json b/packages/uikit-playground/package.json index 83015854f23a..ded97aa45790 100644 --- a/packages/uikit-playground/package.json +++ b/packages/uikit-playground/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/uikit-playground", "private": true, - "version": "0.2.10", + "version": "0.2.11", "type": "module", "scripts": { "dev": "vite", diff --git a/packages/web-ui-registration/CHANGELOG.md b/packages/web-ui-registration/CHANGELOG.md index afdf34b9f108..ed642f33bdd0 100644 --- a/packages/web-ui-registration/CHANGELOG.md +++ b/packages/web-ui-registration/CHANGELOG.md @@ -1,5 +1,11 @@ # @rocket.chat/web-ui-registration +## 3.0.2 + +### Patch Changes + +- @rocket.chat/ui-contexts@3.0.2 + ## 3.0.1 ### Patch Changes diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index b2397e67153a..c08e36978c28 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/web-ui-registration", - "version": "3.0.1", + "version": "3.0.2", "private": true, "homepage": "https://rocket.chat", "main": "./dist/index.js", @@ -51,7 +51,7 @@ "peerDependencies": { "@rocket.chat/layout": "*", "@rocket.chat/tools": "*", - "@rocket.chat/ui-contexts": "3.0.1", + "@rocket.chat/ui-contexts": "3.0.2", "@tanstack/react-query": "*", "react": "*", "react-hook-form": "*", From 3ebc8e0868c859d2d8e636787645c29a89dea1e5 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 2 Jan 2024 23:38:47 -0300 Subject: [PATCH 007/152] chore: fix dep version [no ci] --- apps/meteor/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 382808842576..2febb36430a6 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -77,7 +77,7 @@ "@rocket.chat/livechat": "workspace:^", "@rocket.chat/mock-providers": "workspace:^", "@settlin/spacebars-loader": "^1.0.9", - "@storybook/addon-a11y": "6.5.26", + "@storybook/addon-a11y": "6.5.16", "@storybook/addon-essentials": "~6.5.16", "@storybook/addon-interactions": "~6.5.16", "@storybook/addon-postcss": "~2.0.0", From 505b67e19b14f703950ff168d1d2c502b37a10b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Wed, 3 Jan 2024 11:07:14 -0300 Subject: [PATCH 008/152] chore: Replace `FramedIcon` in favor of fuselage component (#31356) --- .../components/GenericCard/GenericCard.tsx | 11 +- .../components/cards/FeaturesCard.tsx | 101 ++++++++---------- .../UsersUploadsCard/UsersUploadsCard.tsx | 70 ++++++------ .../workspace/VersionCard/VersionCard.tsx | 14 +-- .../components/VersionCardActionItem.tsx | 17 ++- .../ui-client/src/components/FramedIcon.tsx | 37 ------- packages/ui-client/src/components/index.ts | 1 - 7 files changed, 101 insertions(+), 150 deletions(-) delete mode 100644 packages/ui-client/src/components/FramedIcon.tsx diff --git a/apps/meteor/client/components/GenericCard/GenericCard.tsx b/apps/meteor/client/components/GenericCard/GenericCard.tsx index 007c7da9f15e..335b8e6be959 100644 --- a/apps/meteor/client/components/GenericCard/GenericCard.tsx +++ b/apps/meteor/client/components/GenericCard/GenericCard.tsx @@ -1,6 +1,5 @@ -import { Card, CardTitle, CardBody, CardControls, CardHeader } from '@rocket.chat/fuselage'; +import { Card, CardTitle, CardBody, CardControls, CardHeader, FramedIcon } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; -import { FramedIcon } from '@rocket.chat/ui-client'; import type { ComponentProps, ReactElement } from 'react'; import React from 'react'; @@ -11,17 +10,21 @@ type GenericCardProps = { body: string; buttons?: ReactElement[]; icon?: ComponentProps['icon']; - type?: ComponentProps['type']; + type?: 'info' | 'success' | 'warning' | 'danger' | 'neutral'; } & ComponentProps; export const GenericCard: React.FC = ({ title, body, buttons, icon, type, ...props }) => { const cardId = useUniqueId(); const descriptionId = useUniqueId(); + const iconType = type && { + [type]: true, + }; + return ( - {icon && } + {icon && } {title} {body} diff --git a/apps/meteor/client/views/admin/subscription/components/cards/FeaturesCard.tsx b/apps/meteor/client/views/admin/subscription/components/cards/FeaturesCard.tsx index fbd70a844bdd..8af99de7d38c 100644 --- a/apps/meteor/client/views/admin/subscription/components/cards/FeaturesCard.tsx +++ b/apps/meteor/client/views/admin/subscription/components/cards/FeaturesCard.tsx @@ -1,6 +1,5 @@ -import { Box, Card, CardBody, CardControls, CardTitle } from '@rocket.chat/fuselage'; +import { Box, Card, CardBody, CardControls, CardTitle, FramedIcon } from '@rocket.chat/fuselage'; import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; -import { FramedIcon } from '@rocket.chat/ui-client'; import type { ReactElement } from 'react'; import React from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,7 +8,8 @@ import { PRICING_LINK } from '../../utils/links'; import InfoTextIconModal from '../InfoTextIconModal'; type FeatureSet = { - type: 'neutral' | 'success'; + success?: boolean; + neutral?: boolean; title: string; infoText?: string; }; @@ -19,68 +19,59 @@ type FeaturesCardProps = { isEnterprise: boolean; }; +const getFeatureSet = (modules: string[], isEnterprise: boolean): FeatureSet[] => { + const featureSet: FeatureSet[] = [ + { + success: isEnterprise, + title: 'Premium_and_unlimited_apps', + }, + { + success: isEnterprise, + title: 'Premium_omnichannel_capabilities', + }, + { + success: isEnterprise, + title: 'Unlimited_push_notifications', + }, + { + success: modules.includes('videoconference-enterprise'), + title: 'Video_call_manager', + }, + { + success: modules.includes('hide-watermark'), + title: 'Remove_RocketChat_Watermark', + infoText: 'Remove_RocketChat_Watermark_InfoText', + }, + { + success: modules.includes('scalability'), + title: 'High_scalabaility', + }, + { + success: modules.includes('custom-roles'), + title: 'Custom_roles', + }, + { + success: modules.includes('auditing'), + title: 'Message_audit', + }, + ]; + + // eslint-disable-next-line no-nested-ternary + return featureSet.sort(({ success: a }, { success: b }) => (a === b ? 0 : a ? -1 : 1)); +}; + const FeaturesCard = ({ activeModules, isEnterprise }: FeaturesCardProps): ReactElement => { const { t } = useTranslation(); const isSmall = useMediaQuery('(min-width: 1180px)'); - const getFeatureSet = (modules: string[], isEnterprise: boolean): FeatureSet[] => { - const featureSet: FeatureSet[] = [ - { - type: isEnterprise ? 'success' : 'neutral', - title: 'Premium_and_unlimited_apps', - }, - { - type: isEnterprise ? 'success' : 'neutral', - title: 'Premium_omnichannel_capabilities', - }, - { - type: isEnterprise ? 'success' : 'neutral', - title: 'Unlimited_push_notifications', - }, - { - type: modules.includes('videoconference-enterprise') ? 'success' : 'neutral', - title: 'Video_call_manager', - }, - { - type: modules.includes('hide-watermark') ? 'success' : 'neutral', - title: 'Remove_RocketChat_Watermark', - infoText: 'Remove_RocketChat_Watermark_InfoText', - }, - { - type: modules.includes('scalability') ? 'success' : 'neutral', - title: 'High_scalabaility', - }, - { - type: modules.includes('custom-roles') ? 'success' : 'neutral', - title: 'Custom_roles', - }, - { - type: modules.includes('auditing') ? 'success' : 'neutral', - title: 'Message_audit', - }, - ]; - - const sortedFeatureSet = featureSet.sort((a, b) => { - if (a.type === 'success' && b.type !== 'success') { - return -1; - } - if (a.type !== 'success' && b.type === 'success') { - return 1; - } - return featureSet.indexOf(a) - featureSet.indexOf(b); - }); - - return sortedFeatureSet; - }; - return ( {!isEnterprise ? t('Unlock_premium_capabilities') : t('Includes')} - {getFeatureSet(activeModules, isEnterprise).map(({ type, title, infoText }, index) => ( + {getFeatureSet(activeModules, isEnterprise).map(({ success, title, infoText }, index) => ( - + {t(title)} diff --git a/apps/meteor/client/views/admin/workspace/UsersUploadsCard/UsersUploadsCard.tsx b/apps/meteor/client/views/admin/workspace/UsersUploadsCard/UsersUploadsCard.tsx index c3e239c22b8e..3298f38b0f11 100644 --- a/apps/meteor/client/views/admin/workspace/UsersUploadsCard/UsersUploadsCard.tsx +++ b/apps/meteor/client/views/admin/workspace/UsersUploadsCard/UsersUploadsCard.tsx @@ -1,5 +1,5 @@ import type { IStats } from '@rocket.chat/core-typings'; -import { Button, Card, CardControls } from '@rocket.chat/fuselage'; +import { Button, Card, CardBody, CardControls } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -28,41 +28,43 @@ const UsersUploadsCard = ({ statistics }: UsersUploadsCardProps): ReactElement = return ( - - - - - - - - } - /> + + + + + + + + + } + /> - - - - - - - - } - /> + + + + + + + + } + /> - - - - - } - /> + + + + + } + /> + - diff --git a/apps/meteor/client/views/admin/rooms/useEditAdminRoomPermissions.ts b/apps/meteor/client/views/admin/rooms/useEditAdminRoomPermissions.ts index a250015098a1..2f7d6cb7e0b9 100644 --- a/apps/meteor/client/views/admin/rooms/useEditAdminRoomPermissions.ts +++ b/apps/meteor/client/views/admin/rooms/useEditAdminRoomPermissions.ts @@ -5,20 +5,37 @@ import { RoomSettingsEnum } from '../../../../definition/IRoomTypeConfig'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; export const useEditAdminRoomPermissions = (room: Pick) => { - const [canViewName, canViewTopic, canViewAnnouncement, canViewArchived, canViewDescription, canViewType, canViewReadOnly] = - useMemo(() => { - const isAllowed = roomCoordinator.getRoomDirectives(room.t).allowRoomSettingChange; - return [ - isAllowed?.(room, RoomSettingsEnum.NAME), - isAllowed?.(room, RoomSettingsEnum.TOPIC), - isAllowed?.(room, RoomSettingsEnum.ANNOUNCEMENT), - isAllowed?.(room, RoomSettingsEnum.ARCHIVE_OR_UNARCHIVE), - isAllowed?.(room, RoomSettingsEnum.DESCRIPTION), - isAllowed?.(room, RoomSettingsEnum.TYPE), - isAllowed?.(room, RoomSettingsEnum.READ_ONLY), - isAllowed?.(room, RoomSettingsEnum.REACT_WHEN_READ_ONLY), - ]; - }, [room]); + const [ + canViewName, + canViewTopic, + canViewAnnouncement, + canViewArchived, + canViewDescription, + canViewType, + canViewReadOnly, + canViewReactWhenReadOnly, + ] = useMemo(() => { + const isAllowed = roomCoordinator.getRoomDirectives(room.t).allowRoomSettingChange; + return [ + isAllowed?.(room, RoomSettingsEnum.NAME), + isAllowed?.(room, RoomSettingsEnum.TOPIC), + isAllowed?.(room, RoomSettingsEnum.ANNOUNCEMENT), + isAllowed?.(room, RoomSettingsEnum.ARCHIVE_OR_UNARCHIVE), + isAllowed?.(room, RoomSettingsEnum.DESCRIPTION), + isAllowed?.(room, RoomSettingsEnum.TYPE), + isAllowed?.(room, RoomSettingsEnum.READ_ONLY), + isAllowed?.(room, RoomSettingsEnum.REACT_WHEN_READ_ONLY), + ]; + }, [room]); - return { canViewName, canViewTopic, canViewAnnouncement, canViewArchived, canViewDescription, canViewType, canViewReadOnly }; + return { + canViewName, + canViewTopic, + canViewAnnouncement, + canViewArchived, + canViewDescription, + canViewType, + canViewReadOnly, + canViewReactWhenReadOnly, + }; }; diff --git a/apps/meteor/tests/e2e/administration.spec.ts b/apps/meteor/tests/e2e/administration.spec.ts index 2601c2409b61..f2103dbd3c82 100644 --- a/apps/meteor/tests/e2e/administration.spec.ts +++ b/apps/meteor/tests/e2e/administration.spec.ts @@ -3,12 +3,14 @@ import { faker } from '@faker-js/faker'; import { IS_EE } from './config/constants'; import { Users } from './fixtures/userStates'; import { Admin } from './page-objects'; +import { createTargetChannel } from './utils'; import { test, expect } from './utils/test'; test.use({ storageState: Users.admin.state }); test.describe.parallel('administration', () => { let poAdmin: Admin; + let targetChannel: string; test.beforeEach(async ({ page }) => { poAdmin = new Admin(page); @@ -56,6 +58,9 @@ test.describe.parallel('administration', () => { }); test.describe('Rooms', () => { + test.beforeAll(async ({ api }) => { + targetChannel = await createTargetChannel(api); + }); test.beforeEach(async ({ page }) => { await page.goto('/admin/rooms'); }); @@ -64,6 +69,15 @@ test.describe.parallel('administration', () => { await poAdmin.inputSearchRooms.type('general'); await page.waitForSelector('[qa-room-id="GENERAL"]'); }); + + test('should edit target channel', async () => { + await poAdmin.inputSearchRooms.type(targetChannel); + await poAdmin.getRoomRow(targetChannel).click(); + await poAdmin.privateLabel.click(); + await poAdmin.btnSave.click(); + await expect(poAdmin.getRoomRow(targetChannel)).toContainText('Private Channel'); + }); + }); test.describe('Permissions', () => { diff --git a/apps/meteor/tests/e2e/page-objects/admin.ts b/apps/meteor/tests/e2e/page-objects/admin.ts index 112d285a205f..f9a365a4d20d 100644 --- a/apps/meteor/tests/e2e/page-objects/admin.ts +++ b/apps/meteor/tests/e2e/page-objects/admin.ts @@ -16,6 +16,18 @@ export class Admin { return this.page.locator('input[placeholder ="Search rooms"]'); } + getRoomRow(name?: string): Locator { + return this.page.locator('[role="link"]', { hasText: name }); + } + + get btnSave(): Locator { + return this.page.locator('button >> text="Save"'); + } + + get privateLabel(): Locator { + return this.page.locator(`label >> text=Private`); + } + get inputSearchUsers(): Locator { return this.page.locator('input[placeholder="Search Users"]'); } From a9564a4176b3355accfa7a1cfab6c0bdb8fe5968 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Mon, 8 Jan 2024 09:49:13 -0300 Subject: [PATCH 025/152] chore: remove RedHat stuff (#31388) --- .github/workflows/ci.yml | 15 ---------- apps/meteor/.docker/Dockerfile.rhel | 44 ----------------------------- package.json | 1 - 3 files changed, 60 deletions(-) delete mode 100644 apps/meteor/.docker/Dockerfile.rhel diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fdcea4c42133..a8f8d29610bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -724,21 +724,6 @@ jobs: # Makes build fail if the release isn't there curl --fail https://releases.rocket.chat/$RC_VERSION/info - - name: RedHat Registry - if: github.event_name == 'release' - env: - REDHAT_REGISTRY_PID: ${{ secrets.REDHAT_REGISTRY_PID }} - REDHAT_REGISTRY_KEY: ${{ secrets.REDHAT_REGISTRY_KEY }} - run: | - GIT_TAG="${GITHUB_REF#*tags/}" - - curl -X POST \ - https://connect.redhat.com/api/v2/projects/$REDHAT_REGISTRY_PID/build \ - -H "Authorization: Bearer $REDHAT_REGISTRY_KEY" \ - -H 'Cache-Control: no-cache' \ - -H 'Content-Type: application/json' \ - -d '{"tag":"'$GIT_TAG'"}' - trigger-dependent-workflows: runs-on: ubuntu-latest if: github.event_name == 'release' diff --git a/apps/meteor/.docker/Dockerfile.rhel b/apps/meteor/.docker/Dockerfile.rhel deleted file mode 100644 index dc90b0f88133..000000000000 --- a/apps/meteor/.docker/Dockerfile.rhel +++ /dev/null @@ -1,44 +0,0 @@ -FROM registry.access.redhat.com/ubi8/nodejs-12 - -ENV RC_VERSION 6.6.0-develop - -MAINTAINER buildmaster@rocket.chat - -LABEL name="Rocket.Chat" \ - vendor="Rocket.Chat" \ - version="${RC_VERSION}" \ - release="1" \ - url="https://rocket.chat" \ - summary="The Ultimate Open Source Web Chat Platform" \ - description="The Ultimate Open Source Web Chat Platform" \ - run="docker run -d --name ${NAME} ${IMAGE}" - -USER root -RUN dnf install -y python38 && rm -rf /var/cache /var/log/dnf* /var/log/yum.* -USER default - -RUN set -x \ - && gpg --keyserver keys.openpgp.org --recv-keys 0E163286C20D07B9787EBE9FD7F9D0414FD08104 \ - && curl -SLf "https://releases.rocket.chat/${RC_VERSION}/download" -o rocket.chat.tgz \ - && curl -SLf "https://releases.rocket.chat/${RC_VERSION}/asc" -o rocket.chat.tgz.asc \ - && gpg --verify rocket.chat.tgz.asc \ - && tar -zxf rocket.chat.tgz -C /opt/app-root/src/ \ - && cd /opt/app-root/src/bundle/programs/server \ - && npm install - -COPY licenses /licenses - -VOLUME /opt/app-root/src/uploads - -WORKDIR /opt/app-root/src/bundle - -ENV DEPLOY_METHOD=docker-redhat \ - NODE_ENV=production \ - MONGO_URL=mongodb://mongo:27017/rocketchat \ - HOME=/tmp \ - PORT=3000 \ - ROOT_URL=http://localhost:3000 - -EXPOSE 3000 - -CMD ["node", "main.js"] diff --git a/package.json b/package.json index c0ffec861cda..b6e3ba26cc0f 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "updateFiles": [ "package.json", "apps/meteor/package.json", - "apps/meteor/.docker/Dockerfile.rhel", "apps/meteor/app/utils/rocketchat.info" ] }, From d6165ad77fabac3427cc28c3b12abf3d3f9821bc Mon Sep 17 00:00:00 2001 From: Hardik Bhatia <98163873+hardikbhatia777@users.noreply.github.com> Date: Mon, 8 Jan 2024 20:09:36 +0530 Subject: [PATCH 026/152] fix: Disable quote avatars according to user preference (#31393) Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com> --- .changeset/itchy-zoos-appear.md | 5 +++++ .../message/content/attachments/QuoteAttachment.tsx | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/itchy-zoos-appear.md diff --git a/.changeset/itchy-zoos-appear.md b/.changeset/itchy-zoos-appear.md new file mode 100644 index 000000000000..6d9ab31eb7c8 --- /dev/null +++ b/.changeset/itchy-zoos-appear.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Fixes an issue where avatars are not being disabled based on preference on quote attachments diff --git a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx index 16f4764fb63c..493e3e9ea918 100644 --- a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx @@ -1,6 +1,7 @@ import type { MessageQuoteAttachment } from '@rocket.chat/core-typings'; import { css } from '@rocket.chat/css-in-js'; import { Box, Palette } from '@rocket.chat/fuselage'; +import { useUserPreference } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -37,6 +38,7 @@ type QuoteAttachmentProps = { export const QuoteAttachment = ({ attachment }: QuoteAttachmentProps): ReactElement => { const formatTime = useTimeAgo(); + const displayAvatarPreference = useUserPreference('displayAvatars'); return ( <> @@ -50,7 +52,7 @@ export const QuoteAttachment = ({ attachment }: QuoteAttachmentProps): ReactElem borderInlineStartColor='light' > - + {displayAvatarPreference && } From 7a187dcbaa0f621be5e1c30225342db21bc5b8a6 Mon Sep 17 00:00:00 2001 From: Sayan4444 <112304873+Sayan4444@users.noreply.github.com> Date: Mon, 8 Jan 2024 20:55:07 +0530 Subject: [PATCH 027/152] fix: Dropping a file from another browser window creates two upload dialogs (#31332) Co-authored-by: Hugo Costa <20212776+hugocostadev@users.noreply.github.com> --- .changeset/clean-melons-return.md | 5 +++++ .../room/body/hooks/useFileUploadDropTarget.ts | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 .changeset/clean-melons-return.md diff --git a/.changeset/clean-melons-return.md b/.changeset/clean-melons-return.md new file mode 100644 index 000000000000..3b521860efbc --- /dev/null +++ b/.changeset/clean-melons-return.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed image dropping from another browser window creates two upload dialogs in some OS and browsers diff --git a/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts b/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts index 2df567e77fb0..2427f7217401 100644 --- a/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts +++ b/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts @@ -40,7 +40,21 @@ export const useFileUploadDropTarget = (): readonly [ const onFileDrop = useMutableCallback(async (files: File[]) => { const { mime } = await import('../../../../../app/utils/lib/mimeTypes'); - const uploads = Array.from(files).map((file) => { + const getUniqueFiles = () => { + const uniqueFiles: File[] = []; + const st: Set = new Set(); + files.forEach((file) => { + const key = file.size; + if (!st.has(key)) { + uniqueFiles.push(file); + st.add(key); + } + }); + return uniqueFiles; + }; + const uniqueFiles = getUniqueFiles(); + + const uploads = Array.from(uniqueFiles).map((file) => { Object.defineProperty(file, 'type', { value: mime.lookup(file.name) }); return file; }); From 2fa8055d067f6e26d1774dd975b2f1a0e5038faf Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Mon, 8 Jan 2024 13:08:46 -0300 Subject: [PATCH 028/152] fix: room avatar UnHandledPromiseRejection (#31389) --- apps/meteor/server/ufs/ufs-methods.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/server/ufs/ufs-methods.ts b/apps/meteor/server/ufs/ufs-methods.ts index 05228e059292..23a6048fda45 100644 --- a/apps/meteor/server/ufs/ufs-methods.ts +++ b/apps/meteor/server/ufs/ufs-methods.ts @@ -71,7 +71,7 @@ export async function ufsComplete(fileId: string, storeName: string): Promise Date: Mon, 8 Jan 2024 15:25:03 -0300 Subject: [PATCH 029/152] feat: Hide UI elements through window postmessage (#31184) --- .changeset/big-teachers-change.md | 6 + .../app/ui-utils/client/lib/messageBox.ts | 3 +- .../message/toolbox/MessageToolbox.tsx | 11 +- .../client/hooks/useAppActionButtons.ts | 2 +- apps/meteor/client/hooks/useFileInput.ts | 23 ++++ .../client/providers/LayoutProvider.tsx | 23 +++- .../room/Header/RoomToolbox/RoomToolbox.tsx | 2 +- .../room/composer/messageBox/MessageBox.tsx | 10 +- .../ActionsToolbarDropdown.tsx | 96 ++------------- .../MessageBoxActionsToolbar.tsx | 93 ++++++++++----- .../actions/FileUploadAction.tsx | 75 ------------ .../hooks/ToolbarAction.ts | 10 ++ .../useAudioMessageAction.ts} | 39 +++--- .../useCreateDiscussionAction.tsx} | 24 ++-- .../hooks/useFileUploadAction.ts | 52 ++++++++ .../useShareLocationAction.tsx} | 27 +++-- .../hooks/useToolbarActions.ts | 112 ++++++++++++++++++ .../useVideoMessageAction.ts} | 45 ++----- .../useWebdavActions.tsx} | 48 ++++---- .../useUserInfoActions/useUserInfoActions.ts | 6 +- .../room/providers/RoomToolboxProvider.tsx | 5 +- .../page-objects/fragments/home-content.ts | 2 +- packages/ui-contexts/src/LayoutContext.ts | 12 ++ .../src/hooks/useLayoutHiddenActions.ts | 6 + packages/ui-contexts/src/index.ts | 1 + 25 files changed, 420 insertions(+), 313 deletions(-) create mode 100644 .changeset/big-teachers-change.md create mode 100644 apps/meteor/client/hooks/useFileInput.ts delete mode 100644 apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/FileUploadAction.tsx create mode 100644 apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/ToolbarAction.ts rename apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/{actions/AudioMessageAction.tsx => hooks/useAudioMessageAction.ts} (68%) rename apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/{actions/CreateDiscussionAction.tsx => hooks/useCreateDiscussionAction.tsx} (68%) create mode 100644 apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts rename apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/{actions/ShareLocationAction.tsx => hooks/useShareLocationAction.tsx} (65%) create mode 100644 apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useToolbarActions.ts rename apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/{actions/VideoMessageAction.tsx => hooks/useVideoMessageAction.ts} (63%) rename apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/{actions/WebdavAction.tsx => hooks/useWebdavActions.tsx} (60%) create mode 100644 packages/ui-contexts/src/hooks/useLayoutHiddenActions.ts diff --git a/.changeset/big-teachers-change.md b/.changeset/big-teachers-change.md new file mode 100644 index 000000000000..ec8980779031 --- /dev/null +++ b/.changeset/big-teachers-change.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/ui-contexts": minor +--- + +Add the possibility to hide some elements through postMessage events. diff --git a/apps/meteor/app/ui-utils/client/lib/messageBox.ts b/apps/meteor/app/ui-utils/client/lib/messageBox.ts index 3f3c545af57e..3418adef1c1c 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageBox.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageBox.ts @@ -1,4 +1,5 @@ import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import type { Keys as IconName } from '@rocket.chat/icons'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import type { ChatAPI } from '../../../../client/lib/chats/ChatAPI'; @@ -6,7 +7,7 @@ import type { ChatAPI } from '../../../../client/lib/chats/ChatAPI'; export type MessageBoxAction = { label: TranslationKey; id: string; - icon?: string; + icon: IconName; action: (params: { rid: IRoom['_id']; tmid?: IMessage['_id']; event: Event; chat: ChatAPI }) => void; condition?: () => boolean; }; diff --git a/apps/meteor/client/components/message/toolbox/MessageToolbox.tsx b/apps/meteor/client/components/message/toolbox/MessageToolbox.tsx index 3b9cdd84c25d..d0c426dcc466 100644 --- a/apps/meteor/client/components/message/toolbox/MessageToolbox.tsx +++ b/apps/meteor/client/components/message/toolbox/MessageToolbox.tsx @@ -2,7 +2,7 @@ import type { IMessage, IRoom, ISubscription, ITranslatedMessage } from '@rocket import { isThreadMessage, isRoomFederated, isVideoConfMessage } from '@rocket.chat/core-typings'; import { MessageToolbox as FuselageMessageToolbox, MessageToolboxItem } from '@rocket.chat/fuselage'; import { useFeaturePreview } from '@rocket.chat/ui-client'; -import { useUser, useSettings, useTranslation, useMethod } from '@rocket.chat/ui-contexts'; +import { useUser, useSettings, useTranslation, useMethod, useLayoutHiddenActions } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { memo, useMemo } from 'react'; @@ -70,13 +70,18 @@ const MessageToolbox = ({ const actionButtonApps = useMessageActionAppsActionButtons(context); + const { messageToolbox: hiddenActions } = useLayoutHiddenActions(); + const actionsQueryResult = useQuery(['rooms', room._id, 'messages', message._id, 'actions'] as const, async () => { const props = { message, room, user, subscription, settings: mapSettings, chat }; const toolboxItems = await MessageAction.getAll(props, context, 'message'); const menuItems = await MessageAction.getAll(props, context, 'menu'); - return { message: toolboxItems, menu: menuItems }; + return { + message: toolboxItems.filter((action) => !hiddenActions.includes(action.id)), + menu: menuItems.filter((action) => !hiddenActions.includes(action.id)), + }; }); const toolbox = useRoomToolbox(); @@ -85,7 +90,7 @@ const MessageToolbox = ({ const autoTranslateOptions = useAutoTranslate(subscription); - if (selecting) { + if (selecting || (!actionsQueryResult.data?.message.length && !actionsQueryResult.data?.menu.length)) { return null; } diff --git a/apps/meteor/client/hooks/useAppActionButtons.ts b/apps/meteor/client/hooks/useAppActionButtons.ts index 5647ca36656e..5ee20f7772bf 100644 --- a/apps/meteor/client/hooks/useAppActionButtons.ts +++ b/apps/meteor/client/hooks/useAppActionButtons.ts @@ -76,7 +76,7 @@ export const useMessageboxAppsActionButtons = () => { return applyButtonFilters(action); }) .map((action) => { - const item: MessageBoxAction = { + const item: Omit = { id: getIdForActionButton(action), label: Utilities.getI18nKeyForApp(action.labelI18n, action.appId), action: (params) => { diff --git a/apps/meteor/client/hooks/useFileInput.ts b/apps/meteor/client/hooks/useFileInput.ts new file mode 100644 index 000000000000..c9662b820d8f --- /dev/null +++ b/apps/meteor/client/hooks/useFileInput.ts @@ -0,0 +1,23 @@ +import { useRef, useEffect } from 'react'; +import type { AllHTMLAttributes } from 'react'; + +export const useFileInput = (props: AllHTMLAttributes) => { + const ref = useRef(); + + useEffect(() => { + const fileInput = document.createElement('input'); + fileInput.setAttribute('style', 'display: none;'); + Object.entries(props).forEach(([key, value]) => { + fileInput.setAttribute(key, value); + }); + document.body.appendChild(fileInput); + ref.current = fileInput; + + return (): void => { + ref.current = undefined; + fileInput.remove(); + }; + }, [props]); + + return ref; +}; diff --git a/apps/meteor/client/providers/LayoutProvider.tsx b/apps/meteor/client/providers/LayoutProvider.tsx index 5cc113e172c5..a4f8fa84f9ff 100644 --- a/apps/meteor/client/providers/LayoutProvider.tsx +++ b/apps/meteor/client/providers/LayoutProvider.tsx @@ -3,10 +3,18 @@ import { LayoutContext, useRouter, useSetting } from '@rocket.chat/ui-contexts'; import type { FC } from 'react'; import React, { useMemo, useState, useEffect } from 'react'; +const hiddenActionsDefaultValue = { + roomToolbox: [], + messageToolbox: [], + composerToolbox: [], + userToolbox: [], +}; + const LayoutProvider: FC = ({ children }) => { const showTopNavbarEmbeddedLayout = Boolean(useSetting('UI_Show_top_navbar_embedded_layout')); const [isCollapsed, setIsCollapsed] = useState(false); const breakpoints = useBreakpoints(); // ["xs", "sm", "md", "lg", "xl", xxl"] + const [hiddenActions, setHiddenActions] = useState(hiddenActionsDefaultValue); const router = useRouter(); // Once the layout is embedded, it can't be changed @@ -18,6 +26,18 @@ const LayoutProvider: FC = ({ children }) => { setIsCollapsed(isMobile); }, [isMobile]); + useEffect(() => { + const eventHandler = (event: MessageEvent) => { + if (event.data?.event !== 'overrideUi') { + return; + } + + setHiddenActions({ ...hiddenActionsDefaultValue, ...event.data.hideActions }); + }; + window.addEventListener('message', eventHandler); + return () => window.removeEventListener('message', eventHandler); + }, []); + return ( { contextualBarExpanded: breakpoints.includes('sm'), // eslint-disable-next-line no-nested-ternary contextualBarPosition: breakpoints.includes('sm') ? (breakpoints.includes('lg') ? 'relative' : 'absolute') : 'fixed', + hiddenActions, }), - [isMobile, isEmbedded, showTopNavbarEmbeddedLayout, isCollapsed, breakpoints, router], + [isMobile, isEmbedded, showTopNavbarEmbeddedLayout, isCollapsed, breakpoints, router, hiddenActions], )} /> ); diff --git a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx index 00c9d9cdeecd..17f80a490064 100644 --- a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx +++ b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx @@ -91,7 +91,7 @@ const RoomToolbox = ({ className }: RoomToolboxProps) => { {featuredActions.map(mapToToolboxItem)} {featuredActions.length > 0 && } {visibleActions.map(mapToToolboxItem)} - {(normalActions.length > 6 || !roomToolboxExpanded) && ( + {(normalActions.length > 6 || !roomToolboxExpanded) && !!hiddenActions.length && ( )} diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 330fdbb8771d..25bc84e2a64e 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; import { Button, Tag, Box } from '@rocket.chat/fuselage'; import { useContentBoxSize, useMutableCallback } from '@rocket.chat/fuselage-hooks'; @@ -410,15 +411,14 @@ const MessageBox = ({ disabled={isRecording || !canSend} /> )} - diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/ActionsToolbarDropdown.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/ActionsToolbarDropdown.tsx index 5066ecb192e1..8da907c99c35 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/ActionsToolbarDropdown.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/ActionsToolbarDropdown.tsx @@ -1,106 +1,26 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { Dropdown, IconButton, Option, OptionTitle, OptionIcon, OptionContent } from '@rocket.chat/fuselage'; -import { useTranslation, useUserRoom } from '@rocket.chat/ui-contexts'; -import type { ComponentProps, ReactNode } from 'react'; -import React, { useRef, Fragment } from 'react'; +import { Dropdown, IconButton } from '@rocket.chat/fuselage'; +import type { ReactNode } from 'react'; +import React, { useRef } from 'react'; -import { messageBox } from '../../../../../../app/ui-utils/client'; -import { useMessageboxAppsActionButtons } from '../../../../../hooks/useAppActionButtons'; -import type { ChatAPI } from '../../../../../lib/chats/ChatAPI'; import { useDropdownVisibility } from '../../../../../sidebar/header/hooks/useDropdownVisibility'; -import { useChat } from '../../../contexts/ChatContext'; -import CreateDiscussionAction from './actions/CreateDiscussionAction'; -import ShareLocationAction from './actions/ShareLocationAction'; -import WebdavAction from './actions/WebdavAction'; type ActionsToolbarDropdownProps = { - chatContext?: ChatAPI; - rid: IRoom['_id']; - isRecording?: boolean; - tmid?: string; - actions?: ReactNode[]; + disabled?: boolean; + children: () => ReactNode[]; }; -const ActionsToolbarDropdown = ({ isRecording, rid, tmid, actions, ...props }: ActionsToolbarDropdownProps) => { - const chatContext = useChat(); - - if (!chatContext) { - throw new Error('useChat must be used within a ChatProvider'); - } - - const t = useTranslation(); +const ActionsToolbarDropdown = ({ children, ...props }: ActionsToolbarDropdownProps) => { const reference = useRef(null); const target = useRef(null); - const room = useUserRoom(rid); - const { isVisible, toggle } = useDropdownVisibility({ reference, target }); - const apps = useMessageboxAppsActionButtons(); - - const groups = { - ...(apps.isSuccess && - apps.data.length > 0 && { - Apps: apps.data, - }), - ...messageBox.actions.get(), - }; - - const messageBoxActions = Object.entries(groups).map(([name, group]) => { - const items = group.map((item) => ({ - icon: item.icon, - name: t(item.label), - type: 'messagebox-action', - id: item.id, - action: item.action, - })); - - return { - title: t.has(name) && t(name), - items, - }; - }); - return ( <> - toggle()} - {...props} - /> + toggle()} {...props} /> {isVisible && ( - {t('Create_new')} - {room && } - {actions} - - {room && } - {messageBoxActions?.map((actionGroup, index) => ( - - {actionGroup.title} - {actionGroup.items.map((item) => ( - - ))} - - ))} + {children()} )} diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index d83b134e67af..ab0c9ec1fd5d 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -1,52 +1,91 @@ import type { IRoom, IMessage } from '@rocket.chat/core-typings'; +import { Option, OptionTitle, OptionIcon, OptionContent } from '@rocket.chat/fuselage'; +import { MessageComposerAction, MessageComposerActionsDivider } from '@rocket.chat/ui-composer'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ComponentProps } from 'react'; import React, { memo } from 'react'; +import { useChat } from '../../../contexts/ChatContext'; import ActionsToolbarDropdown from './ActionsToolbarDropdown'; -import AudioMessageAction from './actions/AudioMessageAction'; -import FileUploadAction from './actions/FileUploadAction'; -import VideoMessageAction from './actions/VideoMessageAction'; +import { useToolbarActions } from './hooks/useToolbarActions'; type MessageBoxActionsToolbarProps = { + canSend: boolean; + typing: boolean; + isMicrophoneDenied: boolean; variant: 'small' | 'large'; isRecording: boolean; - typing: boolean; - canSend: boolean; rid: IRoom['_id']; tmid?: IMessage['_id']; - isMicrophoneDenied?: boolean; }; const MessageBoxActionsToolbar = ({ - variant = 'large', - isRecording, - typing, canSend, + typing, + isRecording, rid, tmid, + variant = 'large', isMicrophoneDenied, - ...props }: MessageBoxActionsToolbarProps) => { - const actions = [ - , - , - , - ]; - - let featuredAction; - if (variant === 'small') { - featuredAction = actions.splice(1, 1); + const data = useToolbarActions({ + canSend, + typing, + isRecording, + isMicrophoneDenied: Boolean(isMicrophoneDenied), + rid, + tmid, + variant, + }); + + const { featured, menu } = data; + const t = useTranslation(); + const chatContext = useChat(); + + if (!chatContext) { + throw new Error('useChat must be used within a ChatProvider'); + } + + if (!featured.length && !menu.length) { + return null; } return ( <> - {variant !== 'small' && actions} - {variant === 'small' && featuredAction} - + + {featured.map((action) => ( + + ))} + {menu.length > 0 && ( + + {() => + menu.map((option) => { + if (typeof option === 'string') { + return {t.has(option) ? t(option) : option}; + } + + return ( + + ); + }) + } + + )} ); }; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/FileUploadAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/FileUploadAction.tsx deleted file mode 100644 index f9c826fceb4b..000000000000 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/FileUploadAction.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Option, OptionContent, OptionIcon } from '@rocket.chat/fuselage'; -import { MessageComposerAction } from '@rocket.chat/ui-composer'; -import { useTranslation, useSetting } from '@rocket.chat/ui-contexts'; -import type { ChangeEvent, AllHTMLAttributes } from 'react'; -import React, { useRef } from 'react'; - -import type { ChatAPI } from '../../../../../../lib/chats/ChatAPI'; -import { useChat } from '../../../../contexts/ChatContext'; - -type FileUploadActionProps = { - collapsed?: boolean; - chatContext?: ChatAPI; // TODO: remove this when the composer is migrated to React -} & Omit, 'is'>; - -const FileUploadAction = ({ collapsed, chatContext, disabled, ...props }: FileUploadActionProps) => { - const t = useTranslation(); - const fileUploadEnabled = useSetting('FileUpload_Enabled'); - const fileInputRef = useRef(null); - const chat = useChat() ?? chatContext; - - const resetFileInput = () => { - if (!fileInputRef.current) { - return; - } - - fileInputRef.current.value = ''; - }; - - const handleUploadChange = async (e: ChangeEvent) => { - const { mime } = await import('../../../../../../../app/utils/lib/mimeTypes'); - const filesToUpload = Array.from(e.target.files ?? []).map((file) => { - Object.defineProperty(file, 'type', { - value: mime.lookup(file.name), - }); - return file; - }); - chat?.flows.uploadFiles(filesToUpload, resetFileInput); - }; - - const handleUpload = () => { - fileInputRef.current?.click(); - }; - - if (collapsed) { - return ( - <> - - - - ); - } - - return ( - <> - - - - ); -}; - -export default FileUploadAction; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/ToolbarAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/ToolbarAction.ts new file mode 100644 index 000000000000..63f8ded271f5 --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/ToolbarAction.ts @@ -0,0 +1,10 @@ +import type { Keys as IconName } from '@rocket.chat/icons'; + +export type ToolbarAction = { + title?: string; + disabled?: boolean; + onClick: (...params: any) => unknown; + icon: IconName; + label: string; + id: string; +}; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/AudioMessageAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useAudioMessageAction.ts similarity index 68% rename from apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/AudioMessageAction.tsx rename to apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useAudioMessageAction.ts index 41bee06c19c0..87ece1793299 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/AudioMessageAction.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useAudioMessageAction.ts @@ -1,28 +1,22 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { MessageComposerAction } from '@rocket.chat/ui-composer'; -import { useSetting } from '@rocket.chat/ui-contexts'; -import type { AllHTMLAttributes } from 'react'; -import React, { useEffect, useMemo } from 'react'; +import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEffect, useMemo } from 'react'; import { AudioRecorder } from '../../../../../../../app/ui/client/lib/recorderjs/AudioRecorder'; -import type { ChatAPI } from '../../../../../../lib/chats/ChatAPI'; import { useChat } from '../../../../contexts/ChatContext'; import { useMediaActionTitle } from '../../hooks/useMediaActionTitle'; import { useMediaPermissions } from '../../hooks/useMediaPermissions'; +import type { ToolbarAction } from './ToolbarAction'; const audioRecorder = new AudioRecorder(); -type AudioMessageActionProps = { - chatContext?: ChatAPI; - isMicrophoneDenied?: boolean; -} & Omit, 'is'>; - -const AudioMessageAction = ({ chatContext, disabled, isMicrophoneDenied, ...props }: AudioMessageActionProps) => { +export const useAudioMessageAction = (disabled: boolean, isMicrophoneDenied: boolean): ToolbarAction => { const isFileUploadEnabled = useSetting('FileUpload_Enabled') as boolean; const isAudioRecorderEnabled = useSetting('Message_AudioRecorderEnabled') as boolean; const fileUploadMediaTypeBlackList = useSetting('FileUpload_MediaTypeBlackList') as string; const fileUploadMediaTypeWhiteList = useSetting('FileUpload_MediaTypeWhiteList') as string; const [isPermissionDenied] = useMediaPermissions('microphone'); + const t = useTranslation(); const isAllowed = useMemo( () => @@ -39,7 +33,7 @@ const AudioMessageAction = ({ chatContext, disabled, isMicrophoneDenied, ...prop const getMediaActionTitle = useMediaActionTitle('audio', isPermissionDenied, isFileUploadEnabled, isAudioRecorderEnabled, isAllowed); - const chat = useChat() ?? chatContext; + const chat = useChat(); const stopRecording = useMutableCallback(() => { chat?.action.stop('recording'); @@ -61,17 +55,12 @@ const AudioMessageAction = ({ chatContext, disabled, isMicrophoneDenied, ...prop const handleRecordButtonClick = () => chat?.composer?.setRecordingMode(true); - return ( - - ); + return { + id: 'audio-message', + title: getMediaActionTitle, + disabled: !isAllowed || Boolean(disabled), + onClick: handleRecordButtonClick, + icon: 'mic', + label: t('Audio_message'), + }; }; - -export default AudioMessageAction; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/CreateDiscussionAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useCreateDiscussionAction.tsx similarity index 68% rename from apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/CreateDiscussionAction.tsx rename to apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useCreateDiscussionAction.tsx index 419c6c2cfdda..9b85a8a7a6c3 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/CreateDiscussionAction.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useCreateDiscussionAction.tsx @@ -1,12 +1,16 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; -import { Option, OptionIcon, OptionContent } from '@rocket.chat/fuselage'; import { useTranslation, useSetting, usePermission, useSetModal } from '@rocket.chat/ui-contexts'; import React from 'react'; import CreateDiscussion from '../../../../../../components/CreateDiscussion'; +import type { ToolbarAction } from './ToolbarAction'; + +export const useCreateDiscussionAction = (room?: IRoom): ToolbarAction => { + if (!room) { + throw new Error('Invalid room'); + } -const CreateDiscussionAction = ({ room }: { room: IRoom }) => { const setModal = useSetModal(); const t = useTranslation(); @@ -19,12 +23,12 @@ const CreateDiscussionAction = ({ room }: { room: IRoom }) => { const allowDiscussion = room && discussionEnabled && !isRoomFederated(room) && (canStartDiscussion || canSstartDiscussionOtherUser); - return ( - - ); + return { + id: 'create-discussion', + title: !allowDiscussion ? t('Not_Available') : undefined, + disabled: !allowDiscussion, + onClick: handleCreateDiscussion, + icon: 'discussion', + label: t('Discussion'), + }; }; - -export default CreateDiscussionAction; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts new file mode 100644 index 000000000000..8794aa687b28 --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts @@ -0,0 +1,52 @@ +import { useTranslation, useSetting } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +import { useFileInput } from '../../../../../../hooks/useFileInput'; +import { useChat } from '../../../../contexts/ChatContext'; +import type { ToolbarAction } from './ToolbarAction'; + +const fileInputProps = { type: 'file', multiple: true }; + +export const useFileUploadAction = (disabled: boolean): ToolbarAction => { + const t = useTranslation(); + const fileUploadEnabled = useSetting('FileUpload_Enabled'); + const fileInputRef = useFileInput(fileInputProps); + const chat = useChat(); + + useEffect(() => { + const resetFileInput = () => { + if (!fileInputRef?.current) { + return; + } + + fileInputRef.current.value = ''; + }; + + const handleUploadChange = async () => { + const { mime } = await import('../../../../../../../app/utils/lib/mimeTypes'); + const filesToUpload = Array.from(fileInputRef?.current?.files ?? []).map((file) => { + Object.defineProperty(file, 'type', { + value: mime.lookup(file.name), + }); + return file; + }); + chat?.flows.uploadFiles(filesToUpload, resetFileInput); + }; + + fileInputRef.current?.addEventListener('change', handleUploadChange); + return () => fileInputRef?.current?.removeEventListener('change', handleUploadChange); + }, [chat, fileInputRef]); + + const handleUpload = () => { + fileInputRef?.current?.click(); + }; + + return { + id: 'file-upload', + icon: 'clip', + label: t('File'), + title: t('File'), + onClick: handleUpload, + disabled: !fileUploadEnabled || disabled, + }; +}; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/ShareLocationAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useShareLocationAction.tsx similarity index 65% rename from apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/ShareLocationAction.tsx rename to apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useShareLocationAction.tsx index 3589406108f9..4a30e3b2b646 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/ShareLocationAction.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useShareLocationAction.tsx @@ -1,12 +1,16 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; -import { Option, OptionTitle, OptionIcon, OptionContent } from '@rocket.chat/fuselage'; import { useSetting, useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; import ShareLocationModal from '../../../../ShareLocation/ShareLocationModal'; +import type { ToolbarAction } from './ToolbarAction'; + +export const useShareLocationAction = (room?: IRoom, tmid?: string): ToolbarAction => { + if (!room) { + throw new Error('Invalid room'); + } -const ShareLocationAction = ({ room, tmid }: { room: IRoom; tmid?: string }) => { const t = useTranslation(); const setModal = useSetModal(); @@ -19,15 +23,12 @@ const ShareLocationAction = ({ room, tmid }: { room: IRoom; tmid?: string }) => const allowGeolocation = room && canGetGeolocation && !isRoomFederated(room); - return ( - <> - {t('Share')} - - - ); + return { + id: 'share-location', + icon: 'map-pin', + label: t('Location'), + title: !allowGeolocation ? t('Not_Available') : undefined, + onClick: handleShareLocation, + disabled: !allowGeolocation, + }; }; - -export default ShareLocationAction; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useToolbarActions.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useToolbarActions.ts new file mode 100644 index 000000000000..a98d2e885671 --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useToolbarActions.ts @@ -0,0 +1,112 @@ +import { useUserRoom, useTranslation, useLayoutHiddenActions } from '@rocket.chat/ui-contexts'; + +import { messageBox } from '../../../../../../../app/ui-utils/client'; +import { isTruthy } from '../../../../../../../lib/isTruthy'; +import { useMessageboxAppsActionButtons } from '../../../../../../hooks/useAppActionButtons'; +import type { ToolbarAction } from './ToolbarAction'; +import { useAudioMessageAction } from './useAudioMessageAction'; +import { useCreateDiscussionAction } from './useCreateDiscussionAction'; +import { useFileUploadAction } from './useFileUploadAction'; +import { useShareLocationAction } from './useShareLocationAction'; +import { useVideoMessageAction } from './useVideoMessageAction'; +import { useWebdavActions } from './useWebdavActions'; + +type ToolbarActionsOptions = { + variant: 'small' | 'large'; + canSend: boolean; + typing: boolean; + isRecording: boolean; + isMicrophoneDenied: boolean; + rid: string; + tmid?: string; +}; + +const isHidden = (hiddenActions: Array, action: ToolbarAction) => { + if (!action) { + return true; + } + return hiddenActions.includes(action.id); +}; + +export const useToolbarActions = ({ canSend, typing, isRecording, isMicrophoneDenied, rid, tmid, variant }: ToolbarActionsOptions) => { + const room = useUserRoom(rid); + const t = useTranslation(); + + const videoMessageAction = useVideoMessageAction(!canSend || typing || isRecording); + const audioMessageAction = useAudioMessageAction(!canSend || typing || isRecording || isMicrophoneDenied, isMicrophoneDenied); + const fileUploadAction = useFileUploadAction(!canSend || typing || isRecording); + const webdavActions = useWebdavActions(); + const createDiscussionAction = useCreateDiscussionAction(room); + const shareLocationAction = useShareLocationAction(room, tmid); + + const apps = useMessageboxAppsActionButtons(); + const { composerToolbox: hiddenActions } = useLayoutHiddenActions(); + + const allActions = { + ...(!isHidden(hiddenActions, videoMessageAction) && { videoMessageAction }), + ...(!isHidden(hiddenActions, audioMessageAction) && { audioMessageAction }), + ...(!isHidden(hiddenActions, fileUploadAction) && { fileUploadAction }), + ...(!isHidden(hiddenActions, createDiscussionAction) && { createDiscussionAction }), + ...(!isHidden(hiddenActions, shareLocationAction) && { shareLocationAction }), + ...(!hiddenActions.includes('webdav-add') && { webdavActions }), + }; + + const data: { featured: ToolbarAction[]; menu: Array } = (() => { + const featured: Array = []; + const createNew = []; + const share = []; + + if (variant === 'small') { + featured.push(allActions.audioMessageAction); + createNew.push(allActions.videoMessageAction, allActions.fileUploadAction); + } else { + featured.push(allActions.videoMessageAction, allActions.audioMessageAction, allActions.fileUploadAction); + } + + if (allActions.webdavActions) { + createNew.push(...allActions.webdavActions); + } + + share.push(allActions.shareLocationAction); + + const groups = { + ...(apps.isSuccess && + apps.data.length > 0 && { + Apps: apps.data, + }), + ...messageBox.actions.get(), + }; + + const messageBoxActions = Object.entries(groups).reduce>((acc, [name, group]) => { + const items = group + .filter((item) => !hiddenActions.includes(item.id)) + .map( + (item): ToolbarAction => ({ + id: item.id, + icon: item.icon, + label: t(item.label), + onClick: item.action, + }), + ); + + if (items.length === 0) { + return acc; + } + return [...acc, (t.has(name) && t(name)) || name, ...items]; + }, []); + + const createNewFiltered = createNew.filter(isTruthy); + const shareFiltered = share.filter(isTruthy); + + return { + featured: featured.filter(isTruthy), + menu: [ + ...(createNewFiltered.length > 0 ? ['Create_new', ...createNewFiltered] : []), + ...(shareFiltered.length > 0 ? ['Share', ...shareFiltered] : []), + ...messageBoxActions, + ], + }; + })(); + + return data; +}; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/VideoMessageAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts similarity index 63% rename from apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/VideoMessageAction.tsx rename to apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts index fa6fa2484c9f..7068f1338b11 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/VideoMessageAction.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts @@ -1,22 +1,14 @@ -import { Option, OptionIcon, OptionContent } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { MessageComposerAction } from '@rocket.chat/ui-composer'; import { useTranslation, useSetting } from '@rocket.chat/ui-contexts'; -import type { AllHTMLAttributes } from 'react'; -import React, { useEffect, useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { VideoRecorder } from '../../../../../../../app/ui/client/lib/recorderjs/videoRecorder'; -import type { ChatAPI } from '../../../../../../lib/chats/ChatAPI'; import { useChat } from '../../../../contexts/ChatContext'; import { useMediaActionTitle } from '../../hooks/useMediaActionTitle'; import { useMediaPermissions } from '../../hooks/useMediaPermissions'; +import type { ToolbarAction } from './ToolbarAction'; -type VideoMessageActionProps = { - collapsed?: boolean; - chatContext?: ChatAPI; // TODO: remove this when the composer is migrated to React -} & Omit, 'is'>; - -const VideoMessageAction = ({ collapsed, chatContext, disabled, ...props }: VideoMessageActionProps) => { +export const useVideoMessageAction = (disabled: boolean): ToolbarAction => { const t = useTranslation(); const isFileUploadEnabled = useSetting('FileUpload_Enabled') as boolean; const isVideoRecorderEnabled = useSetting('Message_VideoRecorderEnabled') as boolean; @@ -41,7 +33,7 @@ const VideoMessageAction = ({ collapsed, chatContext, disabled, ...props }: Vide const getMediaActionTitle = useMediaActionTitle('video', isPermissionDenied, isFileUploadEnabled, isVideoRecorderEnabled, isAllowed); - const chat = useChat() ?? chatContext; + const chat = useChat(); const handleOpenVideoMessage = () => { if (!chat?.composer?.recordingVideo.get()) { @@ -61,25 +53,12 @@ const VideoMessageAction = ({ collapsed, chatContext, disabled, ...props }: Vide handleDenyVideo(isPermissionDenied); }, [handleDenyVideo, isPermissionDenied]); - if (collapsed) { - return ( - - ); - } - - return ( - - ); + return { + id: 'video-message', + title: getMediaActionTitle, + disabled: !isAllowed || Boolean(disabled), + onClick: handleOpenVideoMessage, + icon: 'video', + label: t('Video_message'), + }; }; - -export default VideoMessageAction; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/WebdavAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx similarity index 60% rename from apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/WebdavAction.tsx rename to apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx index 333c1e4968f3..c60d4d533f75 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/WebdavAction.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx @@ -1,18 +1,17 @@ import type { IWebdavAccountIntegration } from '@rocket.chat/core-typings'; -import { Option, OptionIcon, OptionContent } from '@rocket.chat/fuselage'; import { useTranslation, useSetting, useSetModal } from '@rocket.chat/ui-contexts'; import React from 'react'; import { WebdavAccounts } from '../../../../../../../app/models/client'; import { useReactiveValue } from '../../../../../../hooks/useReactiveValue'; -import type { ChatAPI } from '../../../../../../lib/chats/ChatAPI'; import { useChat } from '../../../../contexts/ChatContext'; import AddWebdavAccountModal from '../../../../webdav/AddWebdavAccountModal'; import WebdavFilePickerModal from '../../../../webdav/WebdavFilePickerModal'; +import type { ToolbarAction } from './ToolbarAction'; const getWebdavAccounts = (): IWebdavAccountIntegration[] => WebdavAccounts.find().fetch(); -const WebdavAction = ({ chatContext }: { chatContext?: ChatAPI }) => { +export const useWebdavActions = (): Array => { const t = useTranslation(); const setModal = useSetModal(); const webDavAccounts = useReactiveValue(getWebdavAccounts); @@ -21,7 +20,7 @@ const WebdavAction = ({ chatContext }: { chatContext?: ChatAPI }) => { const handleCreateWebDav = () => setModal( setModal(null)} onConfirm={() => setModal(null)} />); - const chat = useChat() ?? chatContext; + const chat = useChat(); const handleUpload = async (file: File, description?: string) => chat?.uploads.send(file, { @@ -31,26 +30,23 @@ const WebdavAction = ({ chatContext }: { chatContext?: ChatAPI }) => { const handleOpenWebdav = (account: IWebdavAccountIntegration) => setModal( setModal(null)} />); - return ( - <> - - {webDavEnabled && - webDavAccounts.length > 0 && - webDavAccounts.map((account) => ( - - ))} - - ); + return [ + { + id: 'webdav-add', + title: !webDavEnabled ? t('WebDAV_Integration_Not_Allowed') : undefined, + disabled: !webDavEnabled, + onClick: handleCreateWebDav, + icon: 'cloud-plus', + label: t('Add_Server'), + }, + ...(webDavEnabled && webDavAccounts.length > 0 + ? webDavAccounts.map((account) => ({ + id: account._id, + disabled: false, + onClick: () => handleOpenWebdav(account), + icon: 'cloud-plus' as const, + label: account.name, + })) + : []), + ]; }; - -export default WebdavAction; diff --git a/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts b/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts index dfe9c0341e00..a058fb862ad5 100644 --- a/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts +++ b/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts @@ -1,5 +1,6 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; import type { Icon } from '@rocket.chat/fuselage'; +import { useLayoutHiddenActions } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import { useMemo } from 'react'; @@ -51,6 +52,7 @@ export const useUserInfoActions = ( const call = useCallAction(user); const reportUserOption = useReportUser(user); const isLayoutEmbedded = useEmbeddedLayout(); + const { userToolbox: hiddenActions } = useLayoutHiddenActions(); const userinfoActions = useMemo( () => ({ @@ -83,7 +85,7 @@ export const useUserInfoActions = ( ); const actionSpread = useMemo(() => { - const entries = Object.entries(userinfoActions); + const entries = Object.entries(userinfoActions).filter(([key]) => !hiddenActions.includes(key)); const options = entries.slice(0, size); const slicedOptions = entries.slice(size, entries.length); @@ -105,7 +107,7 @@ export const useUserInfoActions = ( }, [] as UserMenuAction); return { actions: options, menuActions }; - }, [size, userinfoActions]); + }, [size, userinfoActions, hiddenActions]); return actionSpread; }; diff --git a/apps/meteor/client/views/room/providers/RoomToolboxProvider.tsx b/apps/meteor/client/views/room/providers/RoomToolboxProvider.tsx index c05885f5ab4d..2071a67bd7c3 100644 --- a/apps/meteor/client/views/room/providers/RoomToolboxProvider.tsx +++ b/apps/meteor/client/views/room/providers/RoomToolboxProvider.tsx @@ -1,6 +1,6 @@ import type { RoomType, IRoom } from '@rocket.chat/core-typings'; import { useMutableCallback, useStableArray } from '@rocket.chat/fuselage-hooks'; -import { useUserId, useSetting, useRouter, useRouteParameter } from '@rocket.chat/ui-contexts'; +import { useUserId, useSetting, useRouter, useRouteParameter, useLayoutHiddenActions } from '@rocket.chat/ui-contexts'; import type { ReactNode } from 'react'; import React, { useMemo } from 'react'; @@ -87,10 +87,13 @@ const RoomToolboxProvider = ({ children }: RoomToolboxProviderProps) => { const allowAnonymousRead = useSetting('Accounts_AllowAnonymousRead', false); const uid = useUserId(); + const { roomToolbox: hiddenActions } = useLayoutHiddenActions(); + const actions = useStableArray( [...coreRoomActions, ...appsRoomActions] .filter((action) => uid || (allowAnonymousRead && 'anonymous' in action && action.anonymous)) .filter((action) => !action.groups || action.groups.includes(getGroup(room))) + .filter((action) => !hiddenActions.includes(action.id)) .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)), ); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 434fe6ba95eb..79c9617355e9 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -149,7 +149,7 @@ export class HomeContent { } get btnRecordAudio(): Locator { - return this.page.locator('[data-qa-id="audio-record"]'); + return this.page.locator('[data-qa-id="audio-message"]'); } get btnMenuMoreActions() { diff --git a/packages/ui-contexts/src/LayoutContext.ts b/packages/ui-contexts/src/LayoutContext.ts index 694f55cffe38..2d900b5a7612 100644 --- a/packages/ui-contexts/src/LayoutContext.ts +++ b/packages/ui-contexts/src/LayoutContext.ts @@ -20,6 +20,12 @@ export type LayoutContextValue = { size: SizeLayout; contextualBarExpanded: boolean; contextualBarPosition: 'absolute' | 'relative' | 'fixed'; + hiddenActions: { + roomToolbox: Array; + messageToolbox: Array; + composerToolbox: Array; + userToolbox: Array; + }; }; export const LayoutContext = createContext({ @@ -40,4 +46,10 @@ export const LayoutContext = createContext({ }, contextualBarPosition: 'relative', contextualBarExpanded: false, + hiddenActions: { + roomToolbox: [], + messageToolbox: [], + composerToolbox: [], + userToolbox: [], + }, }); diff --git a/packages/ui-contexts/src/hooks/useLayoutHiddenActions.ts b/packages/ui-contexts/src/hooks/useLayoutHiddenActions.ts new file mode 100644 index 000000000000..d578f02e9bc9 --- /dev/null +++ b/packages/ui-contexts/src/hooks/useLayoutHiddenActions.ts @@ -0,0 +1,6 @@ +import { useContext } from 'react'; + +import type { LayoutContextValue } from '../LayoutContext'; +import { LayoutContext } from '../LayoutContext'; + +export const useLayoutHiddenActions = (): LayoutContextValue['hiddenActions'] => useContext(LayoutContext).hiddenActions; diff --git a/packages/ui-contexts/src/index.ts b/packages/ui-contexts/src/index.ts index 6e7b31d8eaf3..fb2f2b84d377 100644 --- a/packages/ui-contexts/src/index.ts +++ b/packages/ui-contexts/src/index.ts @@ -38,6 +38,7 @@ export { useLayout } from './hooks/useLayout'; export { useLayoutContextualBarExpanded } from './hooks/useLayoutContextualBarExpanded'; export { useLayoutContextualBarPosition } from './hooks/useLayoutContextualBarPosition'; export { useLayoutSizes } from './hooks/useLayoutSizes'; +export { useLayoutHiddenActions } from './hooks/useLayoutHiddenActions'; export { useLoadLanguage } from './hooks/useLoadLanguage'; export { useLoginWithPassword } from './hooks/useLoginWithPassword'; export { useLoginServices } from './hooks/useLoginServices'; From 86d75ce221b3699163d717c481d2ef5e6b427076 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:07:27 -0300 Subject: [PATCH 030/152] refactor: Subscribe to only one stream at a time (#31345) --- apps/meteor/app/utils/client/lib/SDKClient.ts | 196 +++++++++++------- .../client/providers/ServerProvider.tsx | 62 +----- 2 files changed, 134 insertions(+), 124 deletions(-) diff --git a/apps/meteor/app/utils/client/lib/SDKClient.ts b/apps/meteor/app/utils/client/lib/SDKClient.ts index e9e20bbe658b..18ff309970df 100644 --- a/apps/meteor/app/utils/client/lib/SDKClient.ts +++ b/apps/meteor/app/utils/client/lib/SDKClient.ts @@ -45,114 +45,166 @@ const isChangedCollectionPayload = ( return true; }; -export const createSDK = (rest: RestClientInterface) => { - const ev = new Emitter(); +type EventMap = StreamKeys> = { + [key in `stream-${N}/${K}`]: StreamerCallbackArgs; +}; + +type StreamMapValue = { + stop: () => void; + onChange: ReturnType['onChange']; + ready: () => Promise; + isReady: boolean; + unsubList: Set<() => void>; +}; + +const createNewMeteorStream = (streamName: StreamNames, key: StreamKeys, args: unknown[]): StreamMapValue => { + const ee = new Emitter(); + const meta = { + ready: false, + }; + const sub = Meteor.connection.subscribe( + `stream-${streamName}`, + key, + { useCollection: false, args }, + { + onReady: (args: any) => { + meta.ready = true; + ee.emit('ready', [undefined, args]); + }, + onError: (err: any) => { + console.error(err); + ee.emit('ready', [err]); + }, + }, + ); + + const onChange: ReturnType['onChange'] = (cb) => { + if (meta.ready) { + cb({ + msg: 'ready', + + subs: [], + }); + return; + } + ee.once('ready', ([error, result]) => { + if (error) { + cb({ + msg: 'nosub', + + id: '', + error, + }); + return; + } - const streams = new Map void>(); + cb(result); + }); + }; + + const ready = () => { + if (meta.ready) { + return Promise.resolve(); + } + return new Promise((r) => { + ee.once('ready', r); + }); + }; + + return { + stop: sub.stop, + onChange, + ready, + get isReady() { + return meta.ready; + }, + unsubList: new Set(), + }; +}; + +const createStreamManager = () => { + // Emitter that replicates stream messages to registered callbacks + const streamProxy = new Emitter(); + + // Collection of unsubscribe callbacks for each stream. + // const proxyUnsubLists = new Map void>>(); + + const streams = new Map(); Meteor.connection._stream.on('message', (rawMsg: string) => { const msg = DDPCommon.parseDDP(rawMsg); if (!isChangedCollectionPayload(msg)) { return; } - ev.emit(`${msg.collection}/${msg.fields.eventName}`, msg.fields.args); + streamProxy.emit(`${msg.collection}/${msg.fields.eventName}` as any, msg.fields.args as any); }); const stream: SDK['stream'] = >( name: N, data: [key: K, ...args: unknown[]], - cb: (...args: StreamerCallbackArgs) => void, + callback: (...args: StreamerCallbackArgs) => void, + _options?: { + retransmit?: boolean | undefined; + retransmitToSelf?: boolean | undefined; + }, ): ReturnType => { const [key, ...args] = data; - const streamName = `stream-${name}`; - const streamKey = `${streamName}/${key}`; - - const ee = new Emitter(); + const eventLiteral = `stream-${name}/${key}` as const; - const meta = { - ready: false, + const proxyCallback = (args?: unknown): void => { + if (!args || !Array.isArray(args)) { + throw new Error('Invalid streamer callback'); + } + callback(...(args as StreamerCallbackArgs)); }; - const sub = Meteor.connection.subscribe( - streamName, - key, - { useCollection: false, args }, - { - onReady: (args: any) => { - meta.ready = true; - ee.emit('ready', [undefined, args]); - }, - onError: (err: any) => { - console.error(err); - ee.emit('ready', [err]); - }, - }, - ); + streamProxy.on(eventLiteral, proxyCallback); - const onChange: ReturnType['onChange'] = (cb) => { - if (meta.ready) { - cb({ - msg: 'ready', + const stop = (): void => { + streamProxy.off(eventLiteral, proxyCallback); - subs: [], - }); + // If someone is still listening, don't unsubscribe + if (streamProxy.has(eventLiteral)) { return; } - ee.once('ready', ([error, result]) => { - if (error) { - cb({ - msg: 'nosub', - - id: '', - error, - }); - return; - } - - cb(result); - }); - }; - const ready = () => { - if (meta.ready) { - return Promise.resolve(); + if (stream) { + stream.stop(); + streams.delete(eventLiteral); } - return new Promise((r) => { - ee.once('ready', r); - }); - }; - - const removeEv = ev.on(`${streamKey}`, (args) => cb(...args)); - - const stop = () => { - streams.delete(`${streamKey}`); - sub.stop(); - removeEv(); }; - streams.set(`${streamKey}`, stop); + const stream = streams.get(eventLiteral) || createNewMeteorStream(name, key, args); + stream.unsubList.add(stop); + if (!streams.has(eventLiteral)) { + streams.set(eventLiteral, stream); + } return { id: '', name, params: data as any, stop, - ready, - onChange, - get isReady() { - return meta.ready; - }, + ready: stream.ready, + onChange: stream.onChange, + isReady: stream.isReady, }; }; - const stop = (name: string, key: string) => { - const streamKey = `stream-${name}/${key}`; - const stop = streams.get(streamKey); - if (stop) { - stop(); + const stopAll = (streamName: string, key: string) => { + const stream = streams.get(`stream-${streamName}/${key}`); + + if (stream) { + stream.unsubList.forEach((stop) => stop()); } }; + return { stream, stopAll }; +}; + +export const createSDK = (rest: RestClientInterface) => { + const { stream, stopAll } = createStreamManager(); + const publish = (name: string, args: unknown[]) => { Meteor.call(`stream-${name}`, ...args); }; @@ -163,7 +215,7 @@ export const createSDK = (rest: RestClientInterface) => { return { rest, - stop, + stop: stopAll, stream, publish, call, diff --git a/apps/meteor/client/providers/ServerProvider.tsx b/apps/meteor/client/providers/ServerProvider.tsx index 8fab8415849d..8eb5e2e37b6b 100644 --- a/apps/meteor/client/providers/ServerProvider.tsx +++ b/apps/meteor/client/providers/ServerProvider.tsx @@ -1,5 +1,4 @@ import type { Serialized } from '@rocket.chat/core-typings'; -import { Emitter } from '@rocket.chat/emitter'; import type { Method, PathFor, OperationParams, OperationResult, UrlParams, PathPattern } from '@rocket.chat/rest-typings'; import type { ServerMethodName, @@ -59,57 +58,16 @@ const callEndpoint = ( const uploadToEndpoint = (endpoint: PathFor<'POST'>, formData: any): Promise => sdk.rest.post(endpoint as any, formData); -type EventMap = StreamKeys> = { - [key in `${N}/${K}`]: StreamerCallbackArgs; -}; - -const ee = new Emitter(); - -const events = new Map void>(); - -const getStream = ( - streamName: N, - _options?: { - retransmit?: boolean | undefined; - retransmitToSelf?: boolean | undefined; - }, -) => { - return >(eventName: K, callback: (...args: StreamerCallbackArgs) => void): (() => void) => { - const eventLiteral = `${streamName}/${eventName}` as const; - const emitterCallback = (args?: unknown): void => { - if (!args || !Array.isArray(args)) { - throw new Error('Invalid streamer callback'); - } - callback(...(args as StreamerCallbackArgs)); - }; - - ee.on(eventLiteral, emitterCallback); - - const streamHandler = (...args: StreamerCallbackArgs): void => { - ee.emit(eventLiteral, args); - }; - - const stop = (): void => { - // If someone is still listening, don't unsubscribe - ee.off(eventLiteral, emitterCallback); - - if (ee.has(eventLiteral)) { - return; - } - - const unsubscribe = events.get(eventLiteral); - if (unsubscribe) { - unsubscribe(); - events.delete(eventLiteral); - } - }; - - if (!events.has(eventLiteral)) { - events.set(eventLiteral, sdk.stream(streamName, [eventName], streamHandler).stop); - } - return stop; - }; -}; +const getStream = + ( + streamName: N, + _options?: { + retransmit?: boolean | undefined; + retransmitToSelf?: boolean | undefined; + }, + ) => + >(eventName: K, callback: (...args: StreamerCallbackArgs) => void): (() => void) => + sdk.stream(streamName, [eventName], callback).stop; const contextValue = { info, From 319f05ec79b577045133a3b5752aeb18780f4f8b Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Mon, 8 Jan 2024 18:11:43 -0300 Subject: [PATCH 031/152] fix: users being logged out after using 2FA (#31380) --- .changeset/beige-deers-laugh.md | 5 +++++ .../2fa/server/methods/validateTempToken.ts | 20 +++++++++++++++---- apps/meteor/app/api/server/v1/users.ts | 17 +++++++++++++++- .../TwoFactorModal/TwoFactorModal.tsx | 13 +++--------- .../views/account/security/TwoFactorTOTP.tsx | 6 ++---- 5 files changed, 42 insertions(+), 19 deletions(-) create mode 100644 .changeset/beige-deers-laugh.md diff --git a/.changeset/beige-deers-laugh.md b/.changeset/beige-deers-laugh.md new file mode 100644 index 000000000000..fe7faf5c6f9b --- /dev/null +++ b/.changeset/beige-deers-laugh.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fix user being logged out after using 2FA diff --git a/apps/meteor/app/2fa/server/methods/validateTempToken.ts b/apps/meteor/app/2fa/server/methods/validateTempToken.ts index 5931d0a8e80d..e1804930a48c 100644 --- a/apps/meteor/app/2fa/server/methods/validateTempToken.ts +++ b/apps/meteor/app/2fa/server/methods/validateTempToken.ts @@ -33,12 +33,24 @@ Meteor.methods({ secret: user.services.totp.tempSecret, token: userToken, }); + if (!verified) { + throw new Meteor.Error('invalid-totp'); + } + + const { codes, hashedCodes } = TOTP.generateCodes(); - if (verified) { - const { codes, hashedCodes } = TOTP.generateCodes(); + await Users.enable2FAAndSetSecretAndCodesByUserId(userId, user.services.totp.tempSecret, hashedCodes); - await Users.enable2FAAndSetSecretAndCodesByUserId(userId, user.services.totp.tempSecret, hashedCodes); - return { codes }; + // Once the TOTP is validated we logout all other clients + const { 'x-auth-token': xAuthToken } = this.connection?.httpHeaders ?? {}; + if (xAuthToken) { + const hashedToken = Accounts._hashLoginToken(xAuthToken); + + if (!(await Users.removeNonPATLoginTokensExcept(this.userId, hashedToken))) { + throw new Meteor.Error('error-logging-out-other-clients', 'Error logging out other clients'); + } } + + return { codes }; }, }); diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index b23d41255c3b..10ea2f0b5ac2 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -1,4 +1,4 @@ -import { Team, api } from '@rocket.chat/core-services'; +import { MeteorError, Team, api } from '@rocket.chat/core-services'; import type { IExportOperation, ILoginToken, IPersonalAccessToken, IUser, UserStatus } from '@rocket.chat/core-typings'; import { Users, Subscriptions } from '@rocket.chat/models'; import { @@ -792,8 +792,23 @@ API.v1.addRoute( { authRequired: true }, { async post() { + const hasUnverifiedEmail = this.user.emails?.some((email) => !email.verified); + if (hasUnverifiedEmail) { + throw new MeteorError('error-invalid-user', 'You need to verify your emails before setting up 2FA'); + } + await Users.enableEmail2FAByUserId(this.userId); + // When 2FA is enable we logout all other clients + const xAuthToken = this.request.headers['x-auth-token'] as string; + if (xAuthToken) { + const hashedToken = Accounts._hashLoginToken(xAuthToken); + + if (!(await Users.removeNonPATLoginTokensExcept(this.userId, hashedToken))) { + throw new MeteorError('error-logging-out-other-clients', 'Error logging out other clients'); + } + } + return API.v1.success(); }, }, diff --git a/apps/meteor/client/components/TwoFactorModal/TwoFactorModal.tsx b/apps/meteor/client/components/TwoFactorModal/TwoFactorModal.tsx index 3e9824673bf2..0382b6eab52e 100644 --- a/apps/meteor/client/components/TwoFactorModal/TwoFactorModal.tsx +++ b/apps/meteor/client/components/TwoFactorModal/TwoFactorModal.tsx @@ -1,4 +1,3 @@ -import { useEndpoint } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -29,24 +28,18 @@ type TwoFactorModalProps = { ); const TwoFactorModal = ({ onConfirm, onClose, invalidAttempt, ...props }: TwoFactorModalProps): ReactElement => { - const logoutOtherSessions = useEndpoint('POST', '/v1/users.logoutOtherClients'); - - const confirm = (code: any, method: Method): void => { - onConfirm(code, method); - logoutOtherSessions(); - }; if (props.method === Method.TOTP) { - return ; + return ; } if (props.method === Method.EMAIL) { const { emailOrUsername } = props; - return ; + return ; } if (props.method === Method.PASSWORD) { - return ; + return ; } throw new Error('Invalid Two Factor method'); diff --git a/apps/meteor/client/views/account/security/TwoFactorTOTP.tsx b/apps/meteor/client/views/account/security/TwoFactorTOTP.tsx index 30cc87861838..e095efcba2d6 100644 --- a/apps/meteor/client/views/account/security/TwoFactorTOTP.tsx +++ b/apps/meteor/client/views/account/security/TwoFactorTOTP.tsx @@ -1,6 +1,6 @@ import { Box, Button, TextInput, Margins } from '@rocket.chat/fuselage'; import { useSafely } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, useUser, useMethod, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; +import { useSetModal, useToastMessageDispatch, useUser, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ComponentProps } from 'react'; import React, { useState, useCallback, useEffect } from 'react'; import { useForm } from 'react-hook-form'; @@ -16,7 +16,6 @@ const TwoFactorTOTP = (props: ComponentProps): ReactElement => { const user = useUser(); const setModal = useSetModal(); - const logoutOtherSessions = useEndpoint('POST', '/v1/users.logoutOtherClients'); const enableTotpFn = useMethod('2fa:enable'); const disableTotpFn = useMethod('2fa:disable'); const verifyCodeFn = useMethod('2fa:validateTempToken'); @@ -86,13 +85,12 @@ const TwoFactorTOTP = (props: ComponentProps): ReactElement => { return dispatchToastMessage({ type: 'error', message: t('Invalid_two_factor_code') }); } - logoutOtherSessions(); setModal(); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } }, - [closeModal, dispatchToastMessage, logoutOtherSessions, setModal, t, verifyCodeFn], + [closeModal, dispatchToastMessage, setModal, t, verifyCodeFn], ); const handleRegenerateCodes = useCallback(() => { From 9a6e9b4e280b6044cd49344921e426f31622faeb Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Tue, 9 Jan 2024 15:01:38 -0300 Subject: [PATCH 032/152] fix: login buttons remain visible until refresh after disabling authentication service (#31371) --- .changeset/little-planes-wonder.md | 7 +++ .../rocketchat-mongo-config/server/index.js | 4 +- .../modules/watchers/watchers.module.ts | 6 +++ apps/meteor/server/services/meteor/service.ts | 8 +-- .../tests/e2e/fixtures/inject-initial-data.ts | 4 ++ apps/meteor/tests/e2e/oauth.spec.ts | 26 ++++++++++ apps/meteor/tests/e2e/page-objects/auth.ts | 4 ++ .../tests/end-to-end/api/08-settings.js | 49 +++++++++++++++++++ ee/apps/ddp-streamer/src/DDPStreamer.ts | 4 +- packages/core-services/src/events/Events.ts | 15 +++++- 10 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 .changeset/little-planes-wonder.md create mode 100644 apps/meteor/tests/e2e/oauth.spec.ts diff --git a/.changeset/little-planes-wonder.md b/.changeset/little-planes-wonder.md new file mode 100644 index 000000000000..13c90d0efcdc --- /dev/null +++ b/.changeset/little-planes-wonder.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/core-services': patch +'@rocket.chat/ddp-streamer': patch +'@rocket.chat/meteor': patch +--- + +Fixed an issue that caused login buttons to not be reactively removed from the login page when the related authentication service was disabled by an admin. diff --git a/apps/meteor/packages/rocketchat-mongo-config/server/index.js b/apps/meteor/packages/rocketchat-mongo-config/server/index.js index 65464a31095c..684620d09054 100644 --- a/apps/meteor/packages/rocketchat-mongo-config/server/index.js +++ b/apps/meteor/packages/rocketchat-mongo-config/server/index.js @@ -4,8 +4,8 @@ import { PassThrough } from 'stream'; import { Email } from 'meteor/email'; import { Mongo } from 'meteor/mongo'; -const shouldDisableOplog = ['yes', 'true'].includes(String(process.env.USE_NATIVE_OPLOG).toLowerCase()); -if (!shouldDisableOplog) { +const shouldUseNativeOplog = ['yes', 'true'].includes(String(process.env.USE_NATIVE_OPLOG).toLowerCase()); +if (!shouldUseNativeOplog) { Package['disable-oplog'] = {}; } diff --git a/apps/meteor/server/modules/watchers/watchers.module.ts b/apps/meteor/server/modules/watchers/watchers.module.ts index 88e465edc018..efa0866ab4a0 100644 --- a/apps/meteor/server/modules/watchers/watchers.module.ts +++ b/apps/meteor/server/modules/watchers/watchers.module.ts @@ -339,7 +339,13 @@ export function initWatchers(watcher: DatabaseWatcher, broadcast: BroadcastCallb }); watcher.on(LoginServiceConfiguration.getCollectionName(), async ({ clientAction, id }) => { + if (clientAction === 'removed') { + void broadcast('watch.loginServiceConfiguration', { clientAction, id }); + return; + } + const data = await LoginServiceConfiguration.findOne>(id, { projection: { secret: 0 } }); + if (!data) { return; } diff --git a/apps/meteor/server/services/meteor/service.ts b/apps/meteor/server/services/meteor/service.ts index 8b9462740c6c..95d2061e2f67 100644 --- a/apps/meteor/server/services/meteor/service.ts +++ b/apps/meteor/server/services/meteor/service.ts @@ -152,9 +152,11 @@ export class MeteorService extends ServiceClassInternal implements IMeteor { return; } - serviceConfigCallbacks.forEach((callbacks) => { - callbacks[clientAction === 'inserted' ? 'added' : 'changed']?.(id, data); - }); + if (data) { + serviceConfigCallbacks.forEach((callbacks) => { + callbacks[clientAction === 'inserted' ? 'added' : 'changed']?.(id, data); + }); + } }); } diff --git a/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts b/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts index 11cea78b3f3d..38835db4aaa6 100644 --- a/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts +++ b/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts @@ -57,6 +57,10 @@ export default async function injectInitialData() { _id: 'API_Enable_Rate_Limiter_Dev', value: false, }, + { + _id: 'Accounts_OAuth_Google', + value: false, + }, ].map((setting) => connection .db() diff --git a/apps/meteor/tests/e2e/oauth.spec.ts b/apps/meteor/tests/e2e/oauth.spec.ts new file mode 100644 index 000000000000..e8ad6a6c7e54 --- /dev/null +++ b/apps/meteor/tests/e2e/oauth.spec.ts @@ -0,0 +1,26 @@ +import { Registration } from './page-objects'; +import { setSettingValueById } from './utils/setSettingValueById'; +import { test, expect } from './utils/test'; + +test.describe('OAuth', () => { + let poRegistration: Registration; + + test.beforeEach(async ({ page }) => { + poRegistration = new Registration(page); + + await page.goto('/home'); + }); + + test('Login Page', async ({ api }) => { + await test.step('expect OAuth button to be visible', async () => { + await expect((await setSettingValueById(api, 'Accounts_OAuth_Google', true)).status()).toBe(200); + await expect(poRegistration.btnLoginWithGoogle).toBeVisible({ timeout: 10000 }); + }); + + await test.step('expect OAuth button to not be visible', async () => { + await expect((await setSettingValueById(api, 'Accounts_OAuth_Google', false)).status()).toBe(200); + + await expect(poRegistration.btnLoginWithGoogle).not.toBeVisible({ timeout: 10000 }); + }); + }); +}); \ No newline at end of file diff --git a/apps/meteor/tests/e2e/page-objects/auth.ts b/apps/meteor/tests/e2e/page-objects/auth.ts index 9b47d2e44adc..d0a7e13d6505 100644 --- a/apps/meteor/tests/e2e/page-objects/auth.ts +++ b/apps/meteor/tests/e2e/page-objects/auth.ts @@ -20,6 +20,10 @@ export class Registration { return this.page.locator('role=button[name="Login"]'); } + get btnLoginWithGoogle(): Locator { + return this.page.locator('role=button[name="Sign in with Google"]'); + } + get goToRegister(): Locator { return this.page.locator('role=link[name="Create an account"]'); } diff --git a/apps/meteor/tests/end-to-end/api/08-settings.js b/apps/meteor/tests/end-to-end/api/08-settings.js index de8a21ffac41..d517b60eea9d 100644 --- a/apps/meteor/tests/end-to-end/api/08-settings.js +++ b/apps/meteor/tests/end-to-end/api/08-settings.js @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { before, describe, it } from 'mocha'; import { getCredentials, api, request, credentials } from '../../data/api-data.js'; +import { updateSetting } from '../../data/permissions.helper'; describe('[Settings]', function () { this.retries(0); @@ -84,6 +85,54 @@ describe('[Settings]', function () { }) .end(done); }); + + describe('With OAuth enabled', () => { + before((done) => { + updateSetting('Accounts_OAuth_Google', true).then(done); + }); + + it('should include the OAuth service in the response', (done) => { + // wait 3 seconds before getting the service list so the server has had time to update it + setTimeout(() => { + request + .get(api('service.configurations')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('configurations'); + + expect(res.body.configurations.find(({ service }) => service === 'google')).to.exist; + }) + .end(done); + }, 3000); + }); + }); + + describe('With OAuth disabled', () => { + before((done) => { + updateSetting('Accounts_OAuth_Google', false).then(done); + }); + + it('should not include the OAuth service in the response', (done) => { + // wait 3 seconds before getting the service list so the server has had time to update it + setTimeout(() => { + request + .get(api('service.configurations')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('configurations'); + + expect(res.body.configurations.find(({ service }) => service === 'google')).to.not.exist; + }) + .end(done); + }, 3000); + }); + }); }); describe('/settings.oauth', () => { diff --git a/ee/apps/ddp-streamer/src/DDPStreamer.ts b/ee/apps/ddp-streamer/src/DDPStreamer.ts index 8cc55a09134c..79905fc8206d 100644 --- a/ee/apps/ddp-streamer/src/DDPStreamer.ts +++ b/ee/apps/ddp-streamer/src/DDPStreamer.ts @@ -44,7 +44,9 @@ export class DDPStreamer extends ServiceClass { return; } - events.emit('meteor.loginServiceConfiguration', clientAction === 'inserted' ? 'added' : 'changed', data); + if (data) { + events.emit('meteor.loginServiceConfiguration', clientAction === 'inserted' ? 'added' : 'changed', data); + } }); this.onEvent('meteor.clientVersionUpdated', (versions): void => { diff --git a/packages/core-services/src/events/Events.ts b/packages/core-services/src/events/Events.ts index 67327c3ea215..6315bf43fa13 100644 --- a/packages/core-services/src/events/Events.ts +++ b/packages/core-services/src/events/Events.ts @@ -40,6 +40,19 @@ import type { AutoUpdateRecord } from '../types/IMeteor'; type ClientAction = 'inserted' | 'updated' | 'removed' | 'changed'; +type LoginServiceConfigurationEvent = { + id: string; +} & ( + | { + clientAction: 'removed'; + data?: never; + } + | { + clientAction: Omit; + data: Partial; + } +); + export type EventSignatures = { 'room.video-conference': (params: { rid: string; callId: string }) => void; 'shutdown': (params: Record) => void; @@ -235,7 +248,7 @@ export type EventSignatures = { } ), ): void; - 'watch.loginServiceConfiguration'(data: { clientAction: ClientAction; data: Partial; id: string }): void; + 'watch.loginServiceConfiguration'(data: LoginServiceConfigurationEvent): void; 'watch.instanceStatus'(data: { clientAction: ClientAction; data?: undefined | Partial; From 1b486a1cc3ded5363a8f1c6ff0b9f0b715a86de3 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Tue, 9 Jan 2024 16:09:38 -0300 Subject: [PATCH 033/152] refactor(client): Move Meteor overrides (#28366) --- apps/meteor/app/2fa/client/TOTPCrowd.js | 38 ----- apps/meteor/app/2fa/client/TOTPGoogle.js | 39 ----- apps/meteor/app/2fa/client/TOTPLDAP.js | 54 ------- apps/meteor/app/2fa/client/TOTPOAuth.js | 142 ------------------ apps/meteor/app/2fa/client/TOTPPassword.js | 71 --------- apps/meteor/app/2fa/client/TOTPSaml.js | 35 ----- apps/meteor/app/2fa/client/index.ts | 7 - .../app/2fa/client/overrideMeteorCall.ts | 53 ------- apps/meteor/app/apple/client/index.ts | 2 +- .../apple/server/appleOauthRegisterService.ts | 2 +- apps/meteor/app/cas/client/cas_client.ts | 68 --------- apps/meteor/app/cas/client/index.ts | 1 - apps/meteor/app/crowd/client/index.ts | 1 - apps/meteor/app/crowd/client/loginHelper.js | 26 ---- ...{custom_oauth_client.js => CustomOAuth.ts} | 45 +++--- .../server/custom_oauth_server.js | 2 +- apps/meteor/app/dolphin/client/lib.ts | 2 +- apps/meteor/app/drupal/client/lib.ts | 2 +- .../app/github-enterprise/client/lib.ts | 2 +- apps/meteor/app/gitlab/client/lib.ts | 2 +- apps/meteor/app/lib/server/oauth/oauth.js | 2 +- .../app/meteor-accounts-saml/client/index.ts | 1 - .../client/saml_client.js | 86 ----------- .../server/methods/samlLogout.ts | 2 +- apps/meteor/app/models/client/index.ts | 13 -- apps/meteor/app/nextcloud/client/lib.ts | 2 +- apps/meteor/app/tokenpass/client/lib.ts | 2 +- apps/meteor/app/wordpress/client/lib.ts | 2 +- .../client/definitions/IOAuthProvider.ts | 9 ++ apps/meteor/client/importPackages.ts | 5 - .../client/lib/2fa/overrideLoginMethod.ts | 109 +++++++++++--- .../meteor/client/lib/2fa/process2faReturn.ts | 34 +++-- apps/meteor/client/lib/2fa/utils.ts | 23 --- apps/meteor/client/lib/openCASLoginPopup.ts | 62 ++++++++ apps/meteor/client/main.ts | 2 +- .../ddpOverREST.ts} | 35 +++-- apps/meteor/client/meteorOverrides/index.ts | 16 ++ .../client/meteorOverrides/login/cas.ts | 20 +++ .../client/meteorOverrides/login/crowd.ts | 49 ++++++ .../client/meteorOverrides/login/facebook.ts | 11 ++ .../client/meteorOverrides/login/github.ts | 11 ++ .../client/meteorOverrides/login/google.ts | 72 +++++++++ .../client/meteorOverrides/login/ldap.ts | 52 +++++++ .../client/meteorOverrides/login/linkedin.ts | 18 +++ .../login/meteorDeveloperAccount.ts | 11 ++ .../client/meteorOverrides/login/oauth.ts | 127 ++++++++++++++++ .../client/meteorOverrides/login/password.ts | 67 +++++++++ .../client/meteorOverrides/login/saml.ts | 111 ++++++++++++++ .../client/meteorOverrides/login/twitter.ts | 11 ++ .../meteorOverrides/oauthRedirectUri.ts} | 7 + .../client/meteorOverrides/totpOnCall.ts | 63 ++++++++ .../client/meteorOverrides/userAndUsers.ts | 14 ++ .../providers/UserProvider/UserProvider.tsx | 8 +- apps/meteor/client/startup/customOAuth.ts | 2 +- apps/meteor/client/startup/index.ts | 2 - apps/meteor/client/startup/ldap.ts | 16 -- apps/meteor/client/startup/oauth.ts | 20 --- .../marketplace/hooks/useAppRequestStats.ts | 3 +- .../externals/meteor/accounts-base.d.ts | 12 +- .../externals/meteor/facebook-oauth.d.ts | 3 + .../externals/meteor/github-oauth.d.ts | 3 + .../externals/meteor/google-oauth.d.ts | 3 + .../meteor/meteor-developer-oauth.d.ts | 3 + .../definition/externals/meteor/meteor.d.ts | 19 +-- .../definition/externals/meteor/oauth.d.ts | 24 ++- .../meteor/pauli-linkedin-oauth.d.ts | 3 + .../externals/meteor/twitter-oauth.d.ts | 3 + .../externals/service-configuration.d.ts | 11 +- apps/meteor/lib/oauthRedirectUriServer.ts | 2 +- .../linkedin-oauth/linkedin-client.js | 2 +- .../linkedin-oauth/linkedin-server.js | 2 +- .../core-typings/src/ICustomOAuthConfig.ts | 3 +- packages/rest-typings/src/v1/misc.ts | 4 +- packages/ui-contexts/src/UserContext.ts | 2 +- 74 files changed, 970 insertions(+), 823 deletions(-) delete mode 100644 apps/meteor/app/2fa/client/TOTPCrowd.js delete mode 100644 apps/meteor/app/2fa/client/TOTPGoogle.js delete mode 100644 apps/meteor/app/2fa/client/TOTPLDAP.js delete mode 100644 apps/meteor/app/2fa/client/TOTPOAuth.js delete mode 100644 apps/meteor/app/2fa/client/TOTPPassword.js delete mode 100644 apps/meteor/app/2fa/client/TOTPSaml.js delete mode 100644 apps/meteor/app/2fa/client/index.ts delete mode 100644 apps/meteor/app/2fa/client/overrideMeteorCall.ts delete mode 100644 apps/meteor/app/cas/client/cas_client.ts delete mode 100644 apps/meteor/app/cas/client/index.ts delete mode 100644 apps/meteor/app/crowd/client/index.ts delete mode 100644 apps/meteor/app/crowd/client/loginHelper.js rename apps/meteor/app/custom-oauth/client/{custom_oauth_client.js => CustomOAuth.ts} (68%) delete mode 100644 apps/meteor/app/meteor-accounts-saml/client/index.ts delete mode 100644 apps/meteor/app/meteor-accounts-saml/client/saml_client.js create mode 100644 apps/meteor/client/definitions/IOAuthProvider.ts create mode 100644 apps/meteor/client/lib/openCASLoginPopup.ts rename apps/meteor/client/{lib/meteorCallWrapper.ts => meteorOverrides/ddpOverREST.ts} (63%) create mode 100644 apps/meteor/client/meteorOverrides/index.ts create mode 100644 apps/meteor/client/meteorOverrides/login/cas.ts create mode 100644 apps/meteor/client/meteorOverrides/login/crowd.ts create mode 100644 apps/meteor/client/meteorOverrides/login/facebook.ts create mode 100644 apps/meteor/client/meteorOverrides/login/github.ts create mode 100644 apps/meteor/client/meteorOverrides/login/google.ts create mode 100644 apps/meteor/client/meteorOverrides/login/ldap.ts create mode 100644 apps/meteor/client/meteorOverrides/login/linkedin.ts create mode 100644 apps/meteor/client/meteorOverrides/login/meteorDeveloperAccount.ts create mode 100644 apps/meteor/client/meteorOverrides/login/oauth.ts create mode 100644 apps/meteor/client/meteorOverrides/login/password.ts create mode 100644 apps/meteor/client/meteorOverrides/login/saml.ts create mode 100644 apps/meteor/client/meteorOverrides/login/twitter.ts rename apps/meteor/{lib/oauthRedirectUriClient.ts => client/meteorOverrides/oauthRedirectUri.ts} (80%) create mode 100644 apps/meteor/client/meteorOverrides/totpOnCall.ts create mode 100644 apps/meteor/client/meteorOverrides/userAndUsers.ts delete mode 100644 apps/meteor/client/startup/ldap.ts delete mode 100644 apps/meteor/client/startup/oauth.ts create mode 100644 apps/meteor/definition/externals/meteor/facebook-oauth.d.ts create mode 100644 apps/meteor/definition/externals/meteor/github-oauth.d.ts create mode 100644 apps/meteor/definition/externals/meteor/google-oauth.d.ts create mode 100644 apps/meteor/definition/externals/meteor/meteor-developer-oauth.d.ts create mode 100644 apps/meteor/definition/externals/meteor/pauli-linkedin-oauth.d.ts create mode 100644 apps/meteor/definition/externals/meteor/twitter-oauth.d.ts diff --git a/apps/meteor/app/2fa/client/TOTPCrowd.js b/apps/meteor/app/2fa/client/TOTPCrowd.js deleted file mode 100644 index 6b4e55a85211..000000000000 --- a/apps/meteor/app/2fa/client/TOTPCrowd.js +++ /dev/null @@ -1,38 +0,0 @@ -import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; - -import '../../crowd/client/index'; -import { overrideLoginMethod } from '../../../client/lib/2fa/overrideLoginMethod'; -import { reportError } from '../../../client/lib/2fa/utils'; - -Meteor.loginWithCrowdAndTOTP = function (username, password, code, callback) { - const loginRequest = { - crowd: true, - username, - crowdPassword: password, - }; - - Accounts.callLoginMethod({ - methodArguments: [ - { - totp: { - login: loginRequest, - code, - }, - }, - ], - userCallback(error) { - if (error) { - reportError(error, callback); - } else { - callback && callback(); - } - }, - }); -}; - -const { loginWithCrowd } = Meteor; - -Meteor.loginWithCrowd = function (username, password, callback) { - overrideLoginMethod(loginWithCrowd, [username, password], callback, Meteor.loginWithCrowdAndTOTP); -}; diff --git a/apps/meteor/app/2fa/client/TOTPGoogle.js b/apps/meteor/app/2fa/client/TOTPGoogle.js deleted file mode 100644 index bb1e509a46d7..000000000000 --- a/apps/meteor/app/2fa/client/TOTPGoogle.js +++ /dev/null @@ -1,39 +0,0 @@ -import { Accounts } from 'meteor/accounts-base'; -import { Google } from 'meteor/google-oauth'; -import { Meteor } from 'meteor/meteor'; - -import { overrideLoginMethod } from '../../../client/lib/2fa/overrideLoginMethod'; - -const loginWithGoogleAndTOTP = function (options, code, callback) { - // support a callback without options - if (!callback && typeof options === 'function') { - callback = options; - options = null; - } - - if (Meteor.isCordova && Google.signIn) { - // After 20 April 2017, Google OAuth login will no longer work from - // a WebView, so Cordova apps must use Google Sign-In instead. - // https://github.com/meteor/meteor/issues/8253 - Google.signIn(options, callback); - return; - } // Use Google's domain-specific login page if we want to restrict creation to - // a particular email domain. (Don't use it if restrictCreationByEmailDomain - // is a function.) Note that all this does is change Google's UI --- - // accounts-base/accounts_server.js still checks server-side that the server - // has the proper email address after the OAuth conversation. - - if (typeof Accounts._options.restrictCreationByEmailDomain === 'string') { - options = Object.assign({}, options || {}); - options.loginUrlParameters = Object.assign({}, options.loginUrlParameters || {}); - options.loginUrlParameters.hd = Accounts._options.restrictCreationByEmailDomain; - } - - const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback, code); - Google.requestCredential(options, credentialRequestCompleteCallback); -}; - -const { loginWithGoogle } = Meteor; -Meteor.loginWithGoogle = function (options, cb) { - overrideLoginMethod(loginWithGoogle, [options], cb, loginWithGoogleAndTOTP); -}; diff --git a/apps/meteor/app/2fa/client/TOTPLDAP.js b/apps/meteor/app/2fa/client/TOTPLDAP.js deleted file mode 100644 index f3b833d04a72..000000000000 --- a/apps/meteor/app/2fa/client/TOTPLDAP.js +++ /dev/null @@ -1,54 +0,0 @@ -import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; - -import '../../../client/startup/ldap'; -import { overrideLoginMethod } from '../../../client/lib/2fa/overrideLoginMethod'; -import { reportError } from '../../../client/lib/2fa/utils'; - -Meteor.loginWithLDAPAndTOTP = function (...args) { - // Pull username and password - const username = args.shift(); - const ldapPass = args.shift(); - - // Check if last argument is a function. if it is, pop it off and set callback to it - const callback = typeof args[args.length - 1] === 'function' ? args.pop() : null; - // The last argument before the callback is the totp code - const code = args.pop(); - - // if args still holds options item, grab it - const ldapOptions = args.length > 0 ? args.shift() : {}; - - // Set up loginRequest object - const loginRequest = { - ldap: true, - username, - ldapPass, - ldapOptions, - }; - - Accounts.callLoginMethod({ - methodArguments: [ - { - totp: { - login: loginRequest, - code, - }, - }, - ], - userCallback(error) { - if (error) { - reportError(error, callback); - } else { - callback && callback(); - } - }, - }); -}; - -const { loginWithLDAP } = Meteor; - -Meteor.loginWithLDAP = function (...args) { - const callback = typeof args[args.length - 1] === 'function' ? args.pop() : null; - - overrideLoginMethod(loginWithLDAP, args, callback, Meteor.loginWithLDAPAndTOTP, args[0]); -}; diff --git a/apps/meteor/app/2fa/client/TOTPOAuth.js b/apps/meteor/app/2fa/client/TOTPOAuth.js deleted file mode 100644 index 47c5e70998b6..000000000000 --- a/apps/meteor/app/2fa/client/TOTPOAuth.js +++ /dev/null @@ -1,142 +0,0 @@ -import { capitalize } from '@rocket.chat/string-helpers'; -import { Accounts } from 'meteor/accounts-base'; -import { Facebook } from 'meteor/facebook-oauth'; -import { Github } from 'meteor/github-oauth'; -import { Meteor } from 'meteor/meteor'; -import { MeteorDeveloperAccounts } from 'meteor/meteor-developer-oauth'; -import { OAuth } from 'meteor/oauth'; -import { Linkedin } from 'meteor/pauli:linkedin-oauth'; -import { Twitter } from 'meteor/twitter-oauth'; - -import { overrideLoginMethod } from '../../../client/lib/2fa/overrideLoginMethod'; -import { process2faReturn } from '../../../client/lib/2fa/process2faReturn'; -import { convertError } from '../../../client/lib/2fa/utils'; -import { CustomOAuth } from '../../custom-oauth/client/custom_oauth_client'; - -let lastCredentialToken = null; -let lastCredentialSecret = null; - -Accounts.oauth.tryLoginAfterPopupClosed = function (credentialToken, callback, totpCode, credentialSecret = null) { - credentialSecret = credentialSecret || OAuth._retrieveCredentialSecret(credentialToken) || null; - const methodArgument = { - oauth: { - credentialToken, - credentialSecret, - }, - }; - - lastCredentialToken = credentialToken; - lastCredentialSecret = credentialSecret; - - if (totpCode && typeof totpCode === 'string') { - methodArgument.totp = { - code: totpCode, - }; - } - - Accounts.callLoginMethod({ - methodArguments: [methodArgument], - userCallback: - callback && - function (err) { - callback(convertError(err)); - }, - }); -}; - -Accounts.oauth.credentialRequestCompleteHandler = function (callback, totpCode) { - return function (credentialTokenOrError) { - if (credentialTokenOrError && credentialTokenOrError instanceof Error) { - callback && callback(credentialTokenOrError); - } else { - Accounts.oauth.tryLoginAfterPopupClosed(credentialTokenOrError, callback, totpCode); - } - }; -}; - -const createOAuthTotpLoginMethod = (credentialProvider) => (options, code, callback) => { - // support a callback without options - if (!callback && typeof options === 'function') { - callback = options; - options = null; - } - - if (lastCredentialToken && lastCredentialSecret) { - Accounts.oauth.tryLoginAfterPopupClosed(lastCredentialToken, callback, code, lastCredentialSecret); - } else { - const provider = (credentialProvider && credentialProvider()) || this; - const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback, code); - provider.requestCredential(options, credentialRequestCompleteCallback); - } - - lastCredentialToken = null; - lastCredentialSecret = null; -}; - -const loginWithOAuthTokenAndTOTP = createOAuthTotpLoginMethod(); - -const loginWithFacebookAndTOTP = createOAuthTotpLoginMethod(() => Facebook); -const { loginWithFacebook } = Meteor; -Meteor.loginWithFacebook = function (options, cb) { - overrideLoginMethod(loginWithFacebook, [options], cb, loginWithFacebookAndTOTP); -}; - -const loginWithGithubAndTOTP = createOAuthTotpLoginMethod(() => Github); -const { loginWithGithub } = Meteor; -Meteor.loginWithGithub = function (options, cb) { - overrideLoginMethod(loginWithGithub, [options], cb, loginWithGithubAndTOTP); -}; - -const loginWithMeteorDeveloperAccountAndTOTP = createOAuthTotpLoginMethod(() => MeteorDeveloperAccounts); -const { loginWithMeteorDeveloperAccount } = Meteor; -Meteor.loginWithMeteorDeveloperAccount = function (options, cb) { - overrideLoginMethod(loginWithMeteorDeveloperAccount, [options], cb, loginWithMeteorDeveloperAccountAndTOTP); -}; - -const loginWithTwitterAndTOTP = createOAuthTotpLoginMethod(() => Twitter); -const { loginWithTwitter } = Meteor; -Meteor.loginWithTwitter = function (options, cb) { - overrideLoginMethod(loginWithTwitter, [options], cb, loginWithTwitterAndTOTP); -}; - -const loginWithLinkedinAndTOTP = createOAuthTotpLoginMethod(() => Linkedin); -const { loginWithLinkedin } = Meteor; -Meteor.loginWithLinkedin = function (options, cb) { - overrideLoginMethod(loginWithLinkedin, [options], cb, loginWithLinkedinAndTOTP); -}; - -Accounts.onPageLoadLogin(async (loginAttempt) => { - if (loginAttempt?.error?.error !== 'totp-required') { - return; - } - - const { methodArguments } = loginAttempt; - if (!methodArguments?.length) { - return; - } - - const oAuthArgs = methodArguments.find((arg) => arg.oauth); - const { credentialToken, credentialSecret } = oAuthArgs.oauth; - const cb = loginAttempt.userCallback; - - await process2faReturn({ - error: loginAttempt.error, - originalCallback: cb, - onCode: (code) => { - Accounts.oauth.tryLoginAfterPopupClosed(credentialToken, cb, code, credentialSecret); - }, - }); -}); - -const oldConfigureLogin = CustomOAuth.prototype.configureLogin; -CustomOAuth.prototype.configureLogin = function (...args) { - const loginWithService = `loginWith${capitalize(String(this.name || ''))}`; - - oldConfigureLogin.apply(this, args); - - const oldMethod = Meteor[loginWithService]; - - Meteor[loginWithService] = function (options, cb) { - overrideLoginMethod(oldMethod, [options], cb, loginWithOAuthTokenAndTOTP); - }; -}; diff --git a/apps/meteor/app/2fa/client/TOTPPassword.js b/apps/meteor/app/2fa/client/TOTPPassword.js deleted file mode 100644 index 20269744fa77..000000000000 --- a/apps/meteor/app/2fa/client/TOTPPassword.js +++ /dev/null @@ -1,71 +0,0 @@ -import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; - -import { process2faReturn } from '../../../client/lib/2fa/process2faReturn'; -import { isTotpInvalidError, isTotpMaxAttemptsError, reportError } from '../../../client/lib/2fa/utils'; -import { dispatchToastMessage } from '../../../client/lib/toast'; -import { t } from '../../utils/lib/i18n'; - -Meteor.loginWithPasswordAndTOTP = function (selector, password, code, callback) { - if (typeof selector === 'string') { - if (selector.indexOf('@') === -1) { - selector = { username: selector }; - } else { - selector = { email: selector }; - } - } - - Accounts.callLoginMethod({ - methodArguments: [ - { - totp: { - login: { - user: selector, - password: Accounts._hashPassword(password), - }, - code, - }, - }, - ], - userCallback(error) { - if (error) { - reportError(error, callback); - } else { - callback && callback(); - } - }, - }); -}; - -const { loginWithPassword } = Meteor; - -Meteor.loginWithPassword = function (email, password, cb) { - loginWithPassword(email, password, async (error) => { - await process2faReturn({ - error, - originalCallback: cb, - emailOrUsername: email, - onCode: (code) => { - Meteor.loginWithPasswordAndTOTP(email, password, code, (error) => { - if (isTotpMaxAttemptsError(error)) { - dispatchToastMessage({ - type: 'error', - message: t('totp-max-attempts'), - }); - cb(); - return; - } - if (isTotpInvalidError(error)) { - dispatchToastMessage({ - type: 'error', - message: t('Invalid_two_factor_code'), - }); - cb(); - return; - } - cb(error); - }); - }, - }); - }); -}; diff --git a/apps/meteor/app/2fa/client/TOTPSaml.js b/apps/meteor/app/2fa/client/TOTPSaml.js deleted file mode 100644 index 7d9ec34541df..000000000000 --- a/apps/meteor/app/2fa/client/TOTPSaml.js +++ /dev/null @@ -1,35 +0,0 @@ -import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; - -import '../../meteor-accounts-saml/client/saml_client'; -import { overrideLoginMethod } from '../../../client/lib/2fa/overrideLoginMethod'; -import { reportError } from '../../../client/lib/2fa/utils'; - -Meteor.loginWithSamlTokenAndTOTP = function (credentialToken, code, callback) { - Accounts.callLoginMethod({ - methodArguments: [ - { - totp: { - login: { - saml: true, - credentialToken, - }, - code, - }, - }, - ], - userCallback(error) { - if (error) { - reportError(error, callback); - } else { - callback && callback(); - } - }, - }); -}; - -const { loginWithSamlToken } = Meteor; - -Meteor.loginWithSamlToken = function (options, callback) { - overrideLoginMethod(loginWithSamlToken, [options], callback, Meteor.loginWithSamlTokenAndTOTP); -}; diff --git a/apps/meteor/app/2fa/client/index.ts b/apps/meteor/app/2fa/client/index.ts deleted file mode 100644 index 1e8f20eb784c..000000000000 --- a/apps/meteor/app/2fa/client/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import './TOTPPassword'; -import './TOTPOAuth'; -import './TOTPGoogle'; -import './TOTPSaml'; -import './TOTPLDAP'; -import './TOTPCrowd'; -import './overrideMeteorCall'; diff --git a/apps/meteor/app/2fa/client/overrideMeteorCall.ts b/apps/meteor/app/2fa/client/overrideMeteorCall.ts deleted file mode 100644 index e373c8a421be..000000000000 --- a/apps/meteor/app/2fa/client/overrideMeteorCall.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { process2faReturn, process2faAsyncReturn } from '../../../client/lib/2fa/process2faReturn'; -import { isTotpInvalidError } from '../../../client/lib/2fa/utils'; -import { t } from '../../utils/lib/i18n'; - -const { call, callAsync } = Meteor; - -type Callback = { - (error: unknown): void; - (error: unknown, result: unknown): void; -}; - -const callWithTotp = - (methodName: string, args: unknown[], callback: Callback) => - (twoFactorCode: string, twoFactorMethod: string): unknown => - call(methodName, ...args, { twoFactorCode, twoFactorMethod }, (error: unknown, result: unknown): void => { - if (isTotpInvalidError(error)) { - callback(new Error(twoFactorMethod === 'password' ? t('Invalid_password') : t('Invalid_two_factor_code'))); - return; - } - - callback(error, result); - }); - -const callWithoutTotp = (methodName: string, args: unknown[], callback: Callback) => (): unknown => - call(methodName, ...args, async (error: unknown, result: unknown): Promise => { - await process2faReturn({ - error, - result, - onCode: callWithTotp(methodName, args, callback), - originalCallback: callback, - emailOrUsername: undefined, - }); - }); - -Meteor.call = function (methodName: string, ...args: unknown[]): unknown { - const callback = args.length > 0 && typeof args[args.length - 1] === 'function' ? (args.pop() as Callback) : (): void => undefined; - - return callWithoutTotp(methodName, args, callback)(); -}; - -Meteor.callAsync = async function _callAsyncWithTotp(methodName: string, ...args: unknown[]): Promise { - try { - return await callAsync(methodName, ...args); - } catch (error: unknown) { - return process2faAsyncReturn({ - error, - onCode: (twoFactorCode, twoFactorMethod) => Meteor.callAsync(methodName, ...args, { twoFactorCode, twoFactorMethod }), - emailOrUsername: undefined, - }); - } -}; diff --git a/apps/meteor/app/apple/client/index.ts b/apps/meteor/app/apple/client/index.ts index 3e4d15a67fe1..2c59dbe5b3d4 100644 --- a/apps/meteor/app/apple/client/index.ts +++ b/apps/meteor/app/apple/client/index.ts @@ -1,4 +1,4 @@ -import { CustomOAuth } from '../../custom-oauth/client/custom_oauth_client'; +import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth'; import { config } from '../lib/config'; new CustomOAuth('apple', config); diff --git a/apps/meteor/app/apple/server/appleOauthRegisterService.ts b/apps/meteor/app/apple/server/appleOauthRegisterService.ts index e19564542e15..b9558fa701f7 100644 --- a/apps/meteor/app/apple/server/appleOauthRegisterService.ts +++ b/apps/meteor/app/apple/server/appleOauthRegisterService.ts @@ -70,7 +70,7 @@ settings.watchMultiple( secret, enabled: settings.get('Accounts_OAuth_Apple'), loginStyle: 'popup', - clientId, + clientId: clientId as string, buttonColor: '#000', buttonLabelColor: '#FFF', }, diff --git a/apps/meteor/app/cas/client/cas_client.ts b/apps/meteor/app/cas/client/cas_client.ts deleted file mode 100644 index ea4b3047f6bf..000000000000 --- a/apps/meteor/app/cas/client/cas_client.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Random } from '@rocket.chat/random'; -import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings/client'; - -const openCenteredPopup = (url: string, width: number, height: number) => { - const screenX = typeof window.screenX !== 'undefined' ? window.screenX : window.screenLeft; - const screenY = typeof window.screenY !== 'undefined' ? window.screenY : window.screenTop; - const outerWidth = typeof window.outerWidth !== 'undefined' ? window.outerWidth : document.body.clientWidth; - const outerHeight = typeof window.outerHeight !== 'undefined' ? window.outerHeight : document.body.clientHeight - 22; - // XXX what is the 22? - - // Use `outerWidth - width` and `outerHeight - height` for help in - // positioning the popup centered relative to the current window - const left = screenX + (outerWidth - width) / 2; - const top = screenY + (outerHeight - height) / 2; - const features = `width=${width},height=${height},left=${left},top=${top},scrollbars=yes`; - - const newwindow = window.open(url, 'Login', features); - newwindow?.focus(); - - return newwindow; -}; - -(Meteor as any).loginWithCas = (_?: unknown, callback?: () => void) => { - const credentialToken = Random.id(); - const loginUrl = settings.get('CAS_login_url'); - const popupWidth = settings.get('CAS_popup_width') || 800; - const popupHeight = settings.get('CAS_popup_height') || 600; - - if (!loginUrl) { - return; - } - - const appUrl = Meteor.absoluteUrl().replace(/\/$/, '') + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX; - // check if the provided CAS URL already has some parameters - const delim = loginUrl.split('?').length > 1 ? '&' : '?'; - const popupUrl = `${loginUrl}${delim}service=${appUrl}/_cas/${credentialToken}`; - - const popup = openCenteredPopup(popupUrl, popupWidth, popupHeight); - - const checkPopupOpen = setInterval(() => { - let popupClosed; - try { - // Fix for #328 - added a second test criteria (popup.closed === undefined) - // to humour this Android quirk: - // http://code.google.com/p/android/issues/detail?id=21061 - popupClosed = popup?.closed || popup?.closed === undefined; - } catch (e) { - // For some unknown reason, IE9 (and others?) sometimes (when - // the popup closes too quickly?) throws "SCRIPT16386: No such - // interface supported" when trying to read 'popup.closed'. Try - // again in 100ms. - return; - } - - if (popupClosed) { - clearInterval(checkPopupOpen); - - // check auth on server. - Accounts.callLoginMethod({ - methodArguments: [{ cas: { credentialToken } }], - userCallback: callback, - }); - } - }, 100); -}; diff --git a/apps/meteor/app/cas/client/index.ts b/apps/meteor/app/cas/client/index.ts deleted file mode 100644 index 75213558d6d8..000000000000 --- a/apps/meteor/app/cas/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './cas_client'; diff --git a/apps/meteor/app/crowd/client/index.ts b/apps/meteor/app/crowd/client/index.ts deleted file mode 100644 index fecf898e1ae4..000000000000 --- a/apps/meteor/app/crowd/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './loginHelper'; diff --git a/apps/meteor/app/crowd/client/loginHelper.js b/apps/meteor/app/crowd/client/loginHelper.js deleted file mode 100644 index a2bb14023b3a..000000000000 --- a/apps/meteor/app/crowd/client/loginHelper.js +++ /dev/null @@ -1,26 +0,0 @@ -import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; - -Meteor.loginWithCrowd = function (...args) { - // Pull username and password - const username = args.shift(); - const password = args.shift(); - const callback = args.shift(); - - const loginRequest = { - crowd: true, - username, - crowdPassword: password, - }; - Accounts.callLoginMethod({ - methodArguments: [loginRequest], - userCallback(error) { - if (callback) { - if (error) { - return callback(error); - } - return callback(); - } - }, - }); -}; diff --git a/apps/meteor/app/custom-oauth/client/custom_oauth_client.js b/apps/meteor/app/custom-oauth/client/CustomOAuth.ts similarity index 68% rename from apps/meteor/app/custom-oauth/client/custom_oauth_client.js rename to apps/meteor/app/custom-oauth/client/CustomOAuth.ts index c516f115aede..58c4142d1349 100644 --- a/apps/meteor/app/custom-oauth/client/custom_oauth_client.js +++ b/apps/meteor/app/custom-oauth/client/CustomOAuth.ts @@ -1,3 +1,4 @@ +import type { OauthConfig } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; import { capitalize } from '@rocket.chat/string-helpers'; import { Accounts } from 'meteor/accounts-base'; @@ -6,6 +7,9 @@ import { Meteor } from 'meteor/meteor'; import { OAuth } from 'meteor/oauth'; import { ServiceConfiguration } from 'meteor/service-configuration'; +import type { IOAuthProvider } from '../../../client/definitions/IOAuthProvider'; +import { overrideLoginMethod, type LoginCallback } from '../../../client/lib/2fa/overrideLoginMethod'; +import { createOAuthTotpLoginMethod } from '../../../client/meteorOverrides/login/oauth'; import { isURL } from '../../../lib/utils/isURL'; // Request custom OAuth credentials for the user @@ -14,8 +18,16 @@ import { isURL } from '../../../lib/utils/isURL'; // completion. Takes one argument, credentialToken on success, or Error on // error. -export class CustomOAuth { - constructor(name, options) { +export class CustomOAuth implements IOAuthProvider { + public serverURL: string; + + public authorizePath: string; + + public scope: string; + + public responseType: string; + + constructor(public readonly name: string, options: OauthConfig) { this.name = name; if (!Match.test(this.name, String)) { throw new Meteor.Error('CustomOAuth: Name is required and must be String'); @@ -28,7 +40,7 @@ export class CustomOAuth { this.configureLogin(); } - configure(options) { + configure(options: OauthConfig) { if (!Match.test(options, Object)) { throw new Meteor.Error('CustomOAuth: Options is required and must be Object'); } @@ -56,31 +68,28 @@ export class CustomOAuth { } configureLogin() { - const loginWithService = `loginWith${capitalize(String(this.name || ''))}`; + const loginWithService = `loginWith${capitalize(String(this.name || ''))}` as const; - Meteor[loginWithService] = async (options, callback) => { - // support a callback without options - if (!callback && typeof options === 'function') { - callback = options; - options = null; - } + const loginWithOAuthTokenAndTOTP = createOAuthTotpLoginMethod(this); + const loginWithOAuthToken = async (options?: Meteor.LoginWithExternalServiceOptions, callback?: LoginCallback) => { const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); await this.requestCredential(options, credentialRequestCompleteCallback); }; - } - async requestCredential(options, credentialRequestCompleteCallback) { - // support both (options, callback) and (callback). - if (!credentialRequestCompleteCallback && typeof options === 'function') { - credentialRequestCompleteCallback = options; - options = {}; - } + (Meteor as any)[loginWithService] = (options: Meteor.LoginWithExternalServiceOptions, callback: LoginCallback) => { + overrideLoginMethod(loginWithOAuthToken, [options], callback, loginWithOAuthTokenAndTOTP); + }; + } + async requestCredential( + options: Meteor.LoginWithExternalServiceOptions = {}, + credentialRequestCompleteCallback: (credentialTokenOrError?: string | Error) => void, + ) { const config = await ServiceConfiguration.configurations.findOneAsync({ service: this.name }); if (!config) { if (credentialRequestCompleteCallback) { - credentialRequestCompleteCallback(new ServiceConfiguration.ConfigError()); + credentialRequestCompleteCallback(new Accounts.ConfigError()); } return; } diff --git a/apps/meteor/app/custom-oauth/server/custom_oauth_server.js b/apps/meteor/app/custom-oauth/server/custom_oauth_server.js index bb939febaef8..6b225069734d 100644 --- a/apps/meteor/app/custom-oauth/server/custom_oauth_server.js +++ b/apps/meteor/app/custom-oauth/server/custom_oauth_server.js @@ -106,7 +106,7 @@ export class CustomOAuth { async getAccessToken(query) { const config = await ServiceConfiguration.configurations.findOneAsync({ service: this.name }); if (!config) { - throw new ServiceConfiguration.ConfigError(); + throw new Accounts.ConfigError(); } let response = undefined; diff --git a/apps/meteor/app/dolphin/client/lib.ts b/apps/meteor/app/dolphin/client/lib.ts index c04ee1b7859d..31a767dd5556 100644 --- a/apps/meteor/app/dolphin/client/lib.ts +++ b/apps/meteor/app/dolphin/client/lib.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { CustomOAuth } from '../../custom-oauth/client/custom_oauth_client'; +import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth'; import { settings } from '../../settings/client'; const config = { diff --git a/apps/meteor/app/drupal/client/lib.ts b/apps/meteor/app/drupal/client/lib.ts index 9edbb560a450..f477a326d706 100644 --- a/apps/meteor/app/drupal/client/lib.ts +++ b/apps/meteor/app/drupal/client/lib.ts @@ -2,7 +2,7 @@ import type { OauthConfig } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { CustomOAuth } from '../../custom-oauth/client/custom_oauth_client'; +import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth'; import { settings } from '../../settings/client'; // Drupal Server CallBack URL needs to be http(s)://{rocketchat.server}[:port]/_oauth/drupal diff --git a/apps/meteor/app/github-enterprise/client/lib.ts b/apps/meteor/app/github-enterprise/client/lib.ts index ec03985df0cf..97b9e6867799 100644 --- a/apps/meteor/app/github-enterprise/client/lib.ts +++ b/apps/meteor/app/github-enterprise/client/lib.ts @@ -2,7 +2,7 @@ import type { OauthConfig } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { CustomOAuth } from '../../custom-oauth/client/custom_oauth_client'; +import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth'; import { settings } from '../../settings/client'; // GitHub Enterprise Server CallBack URL needs to be http(s)://{rocketchat.server}[:port]/_oauth/github_enterprise diff --git a/apps/meteor/app/gitlab/client/lib.ts b/apps/meteor/app/gitlab/client/lib.ts index a1b2ded0cc1a..518478f91227 100644 --- a/apps/meteor/app/gitlab/client/lib.ts +++ b/apps/meteor/app/gitlab/client/lib.ts @@ -2,7 +2,7 @@ import type { OauthConfig } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { CustomOAuth } from '../../custom-oauth/client/custom_oauth_client'; +import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth'; import { settings } from '../../settings/client'; const config: OauthConfig = { diff --git a/apps/meteor/app/lib/server/oauth/oauth.js b/apps/meteor/app/lib/server/oauth/oauth.js index 2618a6e7a569..27342416bedb 100644 --- a/apps/meteor/app/lib/server/oauth/oauth.js +++ b/apps/meteor/app/lib/server/oauth/oauth.js @@ -36,7 +36,7 @@ Accounts.registerLoginHandler(async (options) => { // Make sure we're configured if (!(await ServiceConfiguration.configurations.findOneAsync({ service: options.serviceName }))) { - throw new ServiceConfiguration.ConfigError(); + throw new Accounts.ConfigError(); } if (!_.contains(Accounts.oauth.serviceNames(), service.serviceName)) { diff --git a/apps/meteor/app/meteor-accounts-saml/client/index.ts b/apps/meteor/app/meteor-accounts-saml/client/index.ts deleted file mode 100644 index 5ca4ae3d5c18..000000000000 --- a/apps/meteor/app/meteor-accounts-saml/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './saml_client'; diff --git a/apps/meteor/app/meteor-accounts-saml/client/saml_client.js b/apps/meteor/app/meteor-accounts-saml/client/saml_client.js deleted file mode 100644 index f1f14be530dd..000000000000 --- a/apps/meteor/app/meteor-accounts-saml/client/saml_client.js +++ /dev/null @@ -1,86 +0,0 @@ -import { Random } from '@rocket.chat/random'; -import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; -import { ServiceConfiguration } from 'meteor/service-configuration'; - -import { sdk } from '../../utils/client/lib/SDKClient'; - -if (!Accounts.saml) { - Accounts.saml = {}; -} - -// Override the standard logout behaviour. -// -// If we find a samlProvider, and we are using single -// logout we will initiate logout from rocketchat via saml. -// If not using single logout, we just do the standard logout. -// This can be overridden by a configured logout behaviour. -// -// TODO: This may need some work as it is not clear if we are really -// logging out of the idp when doing the standard logout. - -const MeteorLogout = Meteor.logout; -const logoutBehaviour = { - TERMINATE_SAML: 'SAML', - ONLY_RC: 'Local', -}; - -Meteor.logout = async function (...args) { - const samlService = await ServiceConfiguration.configurations.findOneAsync({ service: 'saml' }); - if (samlService) { - const provider = samlService.clientConfig && samlService.clientConfig.provider; - if (provider) { - if (samlService.logoutBehaviour == null || samlService.logoutBehaviour === logoutBehaviour.TERMINATE_SAML) { - if (samlService.idpSLORedirectURL) { - console.info('SAML session terminated via SLO'); - return Meteor.logoutWithSaml({ provider }); - } - } - - if (samlService.logoutBehaviour === logoutBehaviour.ONLY_RC) { - console.info('SAML session not terminated, only the Rocket.Chat session is going to be killed'); - } - } - } - return MeteorLogout.apply(Meteor, args); -}; - -Meteor.loginWithSaml = function (options /* , callback*/) { - options = options || {}; - const credentialToken = `id-${Random.id()}`; - options.credentialToken = credentialToken; - - window.location.href = `_saml/authorize/${options.provider}/${options.credentialToken}`; -}; - -Meteor.logoutWithSaml = function (options /* , callback*/) { - // Accounts.saml.idpInitiatedSLO(options, callback); - sdk - .call('samlLogout', options.provider) - .then((result) => { - if (!result) { - MeteorLogout.apply(Meteor); - return; - } - - // Remove the userId from the client to prevent calls to the server while the logout is processed. - // If the logout fails, the userId will be reloaded on the resume call - Meteor._localStorage.removeItem(Accounts.USER_ID_KEY); - - // A nasty bounce: 'result' has the SAML LogoutRequest but we need a proper 302 to redirected from the server. - window.location.replace(Meteor.absoluteUrl(`_saml/sloRedirect/${options.provider}/?redirect=${encodeURIComponent(result)}`)); - }) - .catch(() => MeteorLogout.apply(Meteor)); -}; - -Meteor.loginWithSamlToken = function (token, userCallback) { - Accounts.callLoginMethod({ - methodArguments: [ - { - saml: true, - credentialToken: token, - }, - ], - userCallback, - }); -}; diff --git a/apps/meteor/app/meteor-accounts-saml/server/methods/samlLogout.ts b/apps/meteor/app/meteor-accounts-saml/server/methods/samlLogout.ts index 2b960059f164..956426082d40 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/methods/samlLogout.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/methods/samlLogout.ts @@ -28,7 +28,7 @@ function getSamlServiceProviderOptions(provider: string): IServiceProviderOption declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - samlLogout(provider: string): Promise; + samlLogout(provider: string): string | undefined; } } diff --git a/apps/meteor/app/models/client/index.ts b/apps/meteor/app/models/client/index.ts index b4c540318a9b..6b023c3cc7c7 100644 --- a/apps/meteor/app/models/client/index.ts +++ b/apps/meteor/app/models/client/index.ts @@ -16,19 +16,6 @@ import { UserRoles } from './models/UserRoles'; import { Users } from './models/Users'; import { WebdavAccounts } from './models/WebdavAccounts'; -// overwrite Meteor.users collection so records on it don't get erased whenever the client reconnects to websocket -const meteorUserOverwrite = () => { - const uid = Meteor.userId(); - - if (!uid) { - return null; - } - - return (Users.findOne({ _id: uid }) ?? null) as Meteor.User | null; -}; -Meteor.users = Users as typeof Meteor.users; -Meteor.user = meteorUserOverwrite; - export { Base, Roles, diff --git a/apps/meteor/app/nextcloud/client/lib.ts b/apps/meteor/app/nextcloud/client/lib.ts index 12a54217691c..fb7f5391bc3a 100644 --- a/apps/meteor/app/nextcloud/client/lib.ts +++ b/apps/meteor/app/nextcloud/client/lib.ts @@ -3,7 +3,7 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import _ from 'underscore'; -import { CustomOAuth } from '../../custom-oauth/client/custom_oauth_client'; +import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth'; import { settings } from '../../settings/client'; const config: OauthConfig = { diff --git a/apps/meteor/app/tokenpass/client/lib.ts b/apps/meteor/app/tokenpass/client/lib.ts index c8c1daf1cd60..e0b40a9b6de9 100644 --- a/apps/meteor/app/tokenpass/client/lib.ts +++ b/apps/meteor/app/tokenpass/client/lib.ts @@ -2,7 +2,7 @@ import type { OauthConfig } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { CustomOAuth } from '../../custom-oauth/client/custom_oauth_client'; +import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth'; import { settings } from '../../settings/client'; const config: OauthConfig = { diff --git a/apps/meteor/app/wordpress/client/lib.ts b/apps/meteor/app/wordpress/client/lib.ts index 7dd5215ccc60..b213d5fb88c2 100644 --- a/apps/meteor/app/wordpress/client/lib.ts +++ b/apps/meteor/app/wordpress/client/lib.ts @@ -3,7 +3,7 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import _ from 'underscore'; -import { CustomOAuth } from '../../custom-oauth/client/custom_oauth_client'; +import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth'; import { settings } from '../../settings/client'; const config: OauthConfig = { diff --git a/apps/meteor/client/definitions/IOAuthProvider.ts b/apps/meteor/client/definitions/IOAuthProvider.ts new file mode 100644 index 000000000000..00bc3be2b040 --- /dev/null +++ b/apps/meteor/client/definitions/IOAuthProvider.ts @@ -0,0 +1,9 @@ +import type { Meteor } from 'meteor/meteor'; + +export interface IOAuthProvider { + readonly name: string; + requestCredential( + options: Meteor.LoginWithExternalServiceOptions | undefined, + credentialRequestCompleteCallback: (credentialTokenOrError?: string | Error) => void, + ): void; +} diff --git a/apps/meteor/client/importPackages.ts b/apps/meteor/client/importPackages.ts index fc9ce52e9993..c60a13dbfc50 100644 --- a/apps/meteor/client/importPackages.ts +++ b/apps/meteor/client/importPackages.ts @@ -1,11 +1,7 @@ import '../app/cors/client'; -import '../app/2fa/client'; import '../app/apple/client'; import '../app/authorization/client'; import '../app/autotranslate/client'; -import '../app/cas/client'; -import '../app/crowd/client'; -import '../app/custom-oauth/client/custom_oauth_client'; import '../app/custom-sounds/client'; import '../app/dolphin/client'; import '../app/drupal/client'; @@ -36,7 +32,6 @@ import '../app/tokenpass/client'; import '../app/webdav/client'; import '../app/webrtc/client'; import '../app/wordpress/client'; -import '../app/meteor-accounts-saml/client'; import '../app/e2e/client'; import '../app/discussion/client'; import '../app/threads/client'; diff --git a/apps/meteor/client/lib/2fa/overrideLoginMethod.ts b/apps/meteor/client/lib/2fa/overrideLoginMethod.ts index fcda6907cbcf..7cf01ba3370c 100644 --- a/apps/meteor/client/lib/2fa/overrideLoginMethod.ts +++ b/apps/meteor/client/lib/2fa/overrideLoginMethod.ts @@ -1,46 +1,105 @@ -import { t } from '../../../app/utils/lib/i18n'; -import { dispatchToastMessage } from '../toast'; -import { process2faReturn } from './process2faReturn'; -import { isTotpInvalidError, isTotpRequiredError } from './utils'; - -type LoginCallback = { - (error: unknown): void; - (error: unknown, result: unknown): void; -}; +import { isTotpInvalidError, isTotpMaxAttemptsError, isTotpRequiredError } from './utils'; -type LoginMethod = (...args: [...args: A, cb: LoginCallback]) => void; +type LoginError = globalThis.Error | Meteor.Error | Meteor.TypedError; -type LoginMethodWithTotp = (...args: [...args: A, code: string, cb: LoginCallback]) => void; +export type LoginCallback = (error: LoginError | undefined, result?: unknown) => void; -export const overrideLoginMethod = ( - loginMethod: LoginMethod, - loginArgs: A, - callback: LoginCallback, - loginMethodTOTP: LoginMethodWithTotp, - emailOrUsername: string, -): void => { - loginMethod.call(null, ...loginArgs, async (error: unknown, result?: unknown) => { +export const overrideLoginMethod = ( + loginMethod: (...args: [...args: TArgs, cb: LoginCallback]) => void, + loginArgs: TArgs, + callback: LoginCallback | undefined, + loginMethodTOTP: (...args: [...args: TArgs, code: string, cb: LoginCallback]) => void, +) => { + loginMethod(...loginArgs, async (error: LoginError | undefined, result?: unknown) => { if (!isTotpRequiredError(error)) { - callback(error); + callback?.(error); return; } + const { process2faReturn } = await import('./process2faReturn'); + await process2faReturn({ error, result, - emailOrUsername, + emailOrUsername: typeof loginArgs[0] === 'string' ? loginArgs[0] : undefined, originalCallback: callback, onCode: (code: string) => { - loginMethodTOTP?.call(null, ...loginArgs, code, (error: unknown) => { + loginMethodTOTP(...loginArgs, code, (error: LoginError | undefined, result?: unknown) => { + if (!error) { + callback?.(undefined, result); + return; + } + if (isTotpInvalidError(error)) { - dispatchToastMessage({ type: 'error', message: t('Invalid_two_factor_code') }); - callback(null); + callback?.(error); return; } - callback(error); + Promise.all([import('../../../app/utils/lib/i18n'), import('../toast')]).then(([{ t }, { dispatchToastMessage }]) => { + if (isTotpMaxAttemptsError(error)) { + dispatchToastMessage({ + type: 'error', + message: t('totp-max-attempts'), + }); + callback?.(undefined); + return; + } + + dispatchToastMessage({ type: 'error', message: t('Invalid_two_factor_code') }); + callback?.(undefined); + }); }); }, }); }); }; + +export const handleLogin = Promise>( + login: TLoginFunction, + loginWithTOTP: (...args: [...args: Parameters, code: string]) => ReturnType, +) => { + return (...args: [...loginArgs: Parameters, callback?: LoginCallback]) => { + const loginArgs = args.slice(0, -1) as Parameters; + const callback = args.slice(-1)[0] as LoginCallback | undefined; + + return login(...loginArgs) + .catch(async (error: LoginError | undefined) => { + if (!isTotpRequiredError(error)) { + return Promise.reject(error); + } + + const { process2faAsyncReturn } = await import('./process2faReturn'); + return process2faAsyncReturn({ + emailOrUsername: typeof loginArgs[0] === 'string' ? loginArgs[0] : undefined, + error, + onCode: (code: string) => loginWithTOTP(...loginArgs, code), + }); + }) + .then((result: unknown) => callback?.(undefined, result)) + .catch((error: LoginError | undefined) => { + if (!isTotpInvalidError(error)) { + callback?.(error); + return; + } + + Promise.all([import('../../../app/utils/lib/i18n'), import('../toast')]).then(([{ t }, { dispatchToastMessage }]) => { + dispatchToastMessage({ type: 'error', message: t('Invalid_two_factor_code') }); + callback?.(undefined); + }); + }); + }; +}; + +export const callLoginMethod = (options: Omit) => + new Promise((resolve, reject) => { + Accounts.callLoginMethod({ + ...options, + userCallback: (error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }, + }); + }); diff --git a/apps/meteor/client/lib/2fa/process2faReturn.ts b/apps/meteor/client/lib/2fa/process2faReturn.ts index 95f7f1dcb361..57a8d98b05b0 100644 --- a/apps/meteor/client/lib/2fa/process2faReturn.ts +++ b/apps/meteor/client/lib/2fa/process2faReturn.ts @@ -3,6 +3,7 @@ import { Meteor } from 'meteor/meteor'; import { lazy } from 'react'; import { imperativeModal } from '../imperativeModal'; +import type { LoginCallback } from './overrideLoginMethod'; import { isTotpInvalidError, isTotpRequiredError } from './utils'; const TwoFactorModal = lazy(() => import('../../components/TwoFactorModal')); @@ -35,6 +36,23 @@ function assertModalProps(props: { } } +const getProps = ( + method: 'totp' | 'email' | 'password', + emailOrUsername?: { username: string } | { email: string } | { id: string } | string, +) => { + switch (method) { + case 'totp': + return { method }; + case 'email': + return { + method, + emailOrUsername: typeof emailOrUsername === 'string' ? emailOrUsername : Meteor.user()?.username, + }; + case 'password': + return { method }; + } +}; + export async function process2faReturn({ error, result, @@ -42,23 +60,19 @@ export async function process2faReturn({ onCode, emailOrUsername, }: { - error: unknown; + error: globalThis.Error | Meteor.Error | Meteor.TypedError | undefined; result: unknown; - originalCallback: { - (error: unknown): void; - (error: unknown, result: unknown): void; - }; + originalCallback: LoginCallback | undefined; onCode: (code: string, method: string) => void; - emailOrUsername: string | null | undefined; + emailOrUsername: { username: string } | { email: string } | { id: string } | string | null | undefined; }): Promise { if (!(isTotpRequiredError(error) || isTotpInvalidError(error)) || !hasRequiredTwoFactorMethod(error)) { - originalCallback(error, result); + originalCallback?.(error, result); return; } const props = { - method: error.details.method, - emailOrUsername: emailOrUsername || error.details.emailOrUsername || Meteor.user()?.username, + ...getProps(error.details.method, emailOrUsername || error.details.emailOrUsername), // eslint-disable-next-line no-nested-ternary invalidAttempt: isTotpInvalidError(error), }; @@ -69,7 +83,7 @@ export async function process2faReturn({ onCode(code, props.method); } catch (error) { process2faReturn({ - error, + error: error as globalThis.Error | Meteor.Error | Meteor.TypedError | undefined, result, originalCallback, onCode, diff --git a/apps/meteor/client/lib/2fa/utils.ts b/apps/meteor/client/lib/2fa/utils.ts index e57037a14899..ab2234f2e589 100644 --- a/apps/meteor/client/lib/2fa/utils.ts +++ b/apps/meteor/client/lib/2fa/utils.ts @@ -1,6 +1,3 @@ -import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; - export const isTotpRequiredError = ( error: unknown, ): error is Meteor.Error & ({ error: 'totp-required' } | { errorType: 'totp-required' }) => @@ -17,23 +14,3 @@ export const isTotpMaxAttemptsError = ( ): error is Meteor.Error & ({ error: 'totp-max-attempts' } | { errorType: 'totp-max-attempts' }) => (error as { error?: unknown } | undefined)?.error === 'totp-max-attempts' || (error as { errorType?: unknown } | undefined)?.errorType === 'totp-max-attempts'; - -const isLoginCancelledError = (error: unknown): error is Meteor.Error => - error instanceof Meteor.Error && error.error === Accounts.LoginCancelledError.numericError; - -export const reportError = (error: T, callback?: (error?: T) => void): void => { - if (callback) { - callback(error); - return; - } - - throw error; -}; - -export const convertError = (error: T): Accounts.LoginCancelledError | T => { - if (isLoginCancelledError(error)) { - return new Accounts.LoginCancelledError(error.reason); - } - - return error; -}; diff --git a/apps/meteor/client/lib/openCASLoginPopup.ts b/apps/meteor/client/lib/openCASLoginPopup.ts new file mode 100644 index 000000000000..d82a48599e4b --- /dev/null +++ b/apps/meteor/client/lib/openCASLoginPopup.ts @@ -0,0 +1,62 @@ +import { Meteor } from 'meteor/meteor'; + +import { settings } from '../../app/settings/client'; + +const openCenteredPopup = (url: string, width: number, height: number) => { + const screenX = window.screenX ?? window.screenLeft; + const screenY = window.screenY ?? window.screenTop; + const outerWidth = window.outerWidth ?? document.body.clientWidth; + const outerHeight = window.outerHeight ?? document.body.clientHeight - 22; + // XXX what is the 22? Probably the height of the title bar. + // Use `outerWidth - width` and `outerHeight - height` for help in + // positioning the popup centered relative to the current window + const left = screenX + (outerWidth - width) / 2; + const top = screenY + (outerHeight - height) / 2; + const features = `width=${width},height=${height},left=${left},top=${top},scrollbars=yes`; + + const newwindow = window.open(url, 'Login', features); + + if (!newwindow) { + throw new Error('Could not open popup'); + } + + newwindow.focus(); + + return newwindow; +}; + +const getPopupUrl = (credentialToken: string): string => { + const loginUrl = settings.get('CAS_login_url'); + + if (!loginUrl) { + throw new Error('CAS_login_url not set'); + } + + const appUrl = Meteor.absoluteUrl().replace(/\/$/, '') + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX; + const serviceUrl = `${appUrl}/_cas/${credentialToken}`; + const url = new URL(loginUrl); + url.searchParams.set('service', serviceUrl); + + return url.href; +}; + +const waitForPopupClose = (popup: Window) => { + return new Promise((resolve) => { + const checkPopupOpen = setInterval(() => { + if (popup.closed || popup.closed === undefined) { + clearInterval(checkPopupOpen); + resolve(); + } + }, 100); + }); +}; + +export const openCASLoginPopup = async (credentialToken: string) => { + const popupWidth = settings.get('CAS_popup_width') || 800; + const popupHeight = settings.get('CAS_popup_height') || 600; + + const popupUrl = getPopupUrl(credentialToken); + const popup = openCenteredPopup(popupUrl, popupWidth, popupHeight); + + await waitForPopupClose(popup); +}; diff --git a/apps/meteor/client/main.ts b/apps/meteor/client/main.ts index 4183195fb263..0a35c44a10be 100644 --- a/apps/meteor/client/main.ts +++ b/apps/meteor/client/main.ts @@ -9,7 +9,7 @@ FlowRouter.notFound = { }; import('./polyfills') - .then(() => Promise.all([import('./lib/meteorCallWrapper'), import('../lib/oauthRedirectUriClient')])) + .then(() => import('./meteorOverrides')) .then(() => import('../ee/client/ecdh')) .then(() => import('./importPackages')) .then(() => Promise.all([import('./methods'), import('./startup')])) diff --git a/apps/meteor/client/lib/meteorCallWrapper.ts b/apps/meteor/client/meteorOverrides/ddpOverREST.ts similarity index 63% rename from apps/meteor/client/lib/meteorCallWrapper.ts rename to apps/meteor/client/meteorOverrides/ddpOverREST.ts index b5a2f8785a69..9bd2021ec027 100644 --- a/apps/meteor/client/lib/meteorCallWrapper.ts +++ b/apps/meteor/client/meteorOverrides/ddpOverREST.ts @@ -6,7 +6,11 @@ import { sdk } from '../../app/utils/client/lib/SDKClient'; const bypassMethods: string[] = ['setUserStatus', 'logout']; -function shouldBypass({ method, params }: Meteor.IDDPMessage): boolean { +const shouldBypass = ({ msg, method, params }: Meteor.IDDPMessage): boolean => { + if (msg !== 'method') { + return true; + } + if (method === 'login' && params[0]?.resume) { return true; } @@ -20,14 +24,12 @@ function shouldBypass({ method, params }: Meteor.IDDPMessage): boolean { } return false; -} +}; -function wrapMeteorDDPCalls(): void { - const { _send } = Meteor.connection; - - Meteor.connection._send = function _DDPSendOverREST(message): void { - if (message.msg !== 'method' || shouldBypass(message)) { - return _send.call(Meteor.connection, message); +const withDDPOverREST = (_send: (this: Meteor.IMeteorConnection, message: Meteor.IDDPMessage) => void) => { + return function _sendOverREST(this: Meteor.IMeteorConnection, message: Meteor.IDDPMessage): void { + if (shouldBypass(message)) { + return _send.call(this, message); } const endpoint = Tracker.nonreactive(() => (!Meteor.userId() ? 'method.callAnon' : 'method.call')); @@ -36,19 +38,20 @@ function wrapMeteorDDPCalls(): void { message: DDPCommon.stringifyDDP({ ...message }), }; - const processResult = (_message: any): void => { + const processResult = (_message: string): void => { // Prevent error on reconnections and method retry. // On those cases the API will be called 2 times but // the handler will be deleted after the first execution. - if (!Meteor.connection._methodInvokers[message.id]) { + if (!this._methodInvokers[message.id]) { return; } - Meteor.connection._livedata_data({ + this._livedata_data({ msg: 'updated', methods: [message.id], }); - Meteor.connection.onMessage(_message); + this.onMessage(_message); }; + const method = encodeURIComponent(message.method.replace(/\//g, ':')); sdk.rest @@ -56,7 +59,7 @@ function wrapMeteorDDPCalls(): void { .then(({ message: _message }) => { processResult(_message); if (message.method === 'login') { - const parsedMessage = DDPCommon.parseDDP(_message as any) as { result?: { token?: string } }; + const parsedMessage = DDPCommon.parseDDP(_message) as { result?: { token?: string } }; if (parsedMessage.result?.token) { Meteor.loginWithToken(parsedMessage.result.token); } @@ -66,6 +69,8 @@ function wrapMeteorDDPCalls(): void { console.error(error); }); }; -} +}; -window.USE_REST_FOR_DDP_CALLS && wrapMeteorDDPCalls(); +if (window.USE_REST_FOR_DDP_CALLS) { + Meteor.connection._send = withDDPOverREST(Meteor.connection._send); +} diff --git a/apps/meteor/client/meteorOverrides/index.ts b/apps/meteor/client/meteorOverrides/index.ts new file mode 100644 index 000000000000..9a1b0eb1f7be --- /dev/null +++ b/apps/meteor/client/meteorOverrides/index.ts @@ -0,0 +1,16 @@ +import './ddpOverREST'; +import './totpOnCall'; +import './oauthRedirectUri'; +import './userAndUsers'; +import './login/cas'; +import './login/crowd'; +import './login/facebook'; +import './login/github'; +import './login/google'; +import './login/ldap'; +import './login/linkedin'; +import './login/meteorDeveloperAccount'; +import './login/oauth'; +import './login/password'; +import './login/saml'; +import './login/twitter'; diff --git a/apps/meteor/client/meteorOverrides/login/cas.ts b/apps/meteor/client/meteorOverrides/login/cas.ts new file mode 100644 index 000000000000..93a9f1d5b236 --- /dev/null +++ b/apps/meteor/client/meteorOverrides/login/cas.ts @@ -0,0 +1,20 @@ +import { Random } from '@rocket.chat/random'; +import { Meteor } from 'meteor/meteor'; + +import { callLoginMethod } from '../../lib/2fa/overrideLoginMethod'; + +declare module 'meteor/meteor' { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Meteor { + function loginWithCas(_?: unknown, callback?: (err?: any) => void): void; + } +} + +Meteor.loginWithCas = (_, callback) => { + const credentialToken = Random.id(); + import('../../lib/openCASLoginPopup') + .then(({ openCASLoginPopup }) => openCASLoginPopup(credentialToken)) + .then(() => callLoginMethod({ methodArguments: [{ cas: { credentialToken } }] })) + .then(() => callback?.()) + .catch(callback); +}; diff --git a/apps/meteor/client/meteorOverrides/login/crowd.ts b/apps/meteor/client/meteorOverrides/login/crowd.ts new file mode 100644 index 000000000000..9b1d4b83d402 --- /dev/null +++ b/apps/meteor/client/meteorOverrides/login/crowd.ts @@ -0,0 +1,49 @@ +import { Meteor } from 'meteor/meteor'; + +import { callLoginMethod, handleLogin, type LoginCallback } from '../../lib/2fa/overrideLoginMethod'; + +declare module 'meteor/meteor' { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Meteor { + function loginWithCrowd( + userDescriptor: { username: string } | { email: string } | { id: string } | string, + password: string, + callback?: LoginCallback, + ): void; + } +} + +const loginWithCrowd = (userDescriptor: { username: string } | { email: string } | { id: string } | string, password: string) => { + const loginRequest = { + crowd: true, + username: userDescriptor, + crowdPassword: password, + }; + + return callLoginMethod({ methodArguments: [loginRequest] }); +}; + +const loginWithCrowdAndTOTP = ( + userDescriptor: { username: string } | { email: string } | { id: string } | string, + password: string, + code: string, +) => { + const loginRequest = { + crowd: true, + username: userDescriptor, + crowdPassword: password, + }; + + return callLoginMethod({ + methodArguments: [ + { + totp: { + login: loginRequest, + code, + }, + }, + ], + }); +}; + +Meteor.loginWithCrowd = handleLogin(loginWithCrowd, loginWithCrowdAndTOTP); diff --git a/apps/meteor/client/meteorOverrides/login/facebook.ts b/apps/meteor/client/meteorOverrides/login/facebook.ts new file mode 100644 index 000000000000..09875021238c --- /dev/null +++ b/apps/meteor/client/meteorOverrides/login/facebook.ts @@ -0,0 +1,11 @@ +import { Facebook } from 'meteor/facebook-oauth'; +import { Meteor } from 'meteor/meteor'; + +import { overrideLoginMethod } from '../../lib/2fa/overrideLoginMethod'; +import { createOAuthTotpLoginMethod } from './oauth'; + +const { loginWithFacebook } = Meteor; +const loginWithFacebookAndTOTP = createOAuthTotpLoginMethod(Facebook); +Meteor.loginWithFacebook = (options, callback) => { + overrideLoginMethod(loginWithFacebook, [options], callback, loginWithFacebookAndTOTP); +}; diff --git a/apps/meteor/client/meteorOverrides/login/github.ts b/apps/meteor/client/meteorOverrides/login/github.ts new file mode 100644 index 000000000000..15e514ab6d56 --- /dev/null +++ b/apps/meteor/client/meteorOverrides/login/github.ts @@ -0,0 +1,11 @@ +import { Github } from 'meteor/github-oauth'; +import { Meteor } from 'meteor/meteor'; + +import { overrideLoginMethod } from '../../lib/2fa/overrideLoginMethod'; +import { createOAuthTotpLoginMethod } from './oauth'; + +const { loginWithGithub } = Meteor; +const loginWithGithubAndTOTP = createOAuthTotpLoginMethod(Github); +Meteor.loginWithGithub = (options, callback) => { + overrideLoginMethod(loginWithGithub, [options], callback, loginWithGithubAndTOTP); +}; diff --git a/apps/meteor/client/meteorOverrides/login/google.ts b/apps/meteor/client/meteorOverrides/login/google.ts new file mode 100644 index 000000000000..149f55b00ace --- /dev/null +++ b/apps/meteor/client/meteorOverrides/login/google.ts @@ -0,0 +1,72 @@ +import { Accounts } from 'meteor/accounts-base'; +import { Google } from 'meteor/google-oauth'; +import { Meteor } from 'meteor/meteor'; + +import { overrideLoginMethod, type LoginCallback } from '../../lib/2fa/overrideLoginMethod'; +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); + }; + } +} + +declare module 'meteor/meteor' { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Meteor { + function loginWithGoogle( + options?: + | Meteor.LoginWithExternalServiceOptions & { + loginUrlParameters?: { + include_granted_scopes?: boolean; + hd?: string; + }; + }, + callback?: LoginCallback, + ): void; + } +} + +const { loginWithGoogle } = Meteor; + +const innerLoginWithGoogleAndTOTP = createOAuthTotpLoginMethod(Google); + +const loginWithGoogleAndTOTP = ( + options: + | (Meteor.LoginWithExternalServiceOptions & { + loginUrlParameters?: { + include_granted_scopes?: boolean; + hd?: string; + }; + }) + | undefined, + code: string, + callback?: LoginCallback, +) => { + if (Meteor.isCordova && Google.signIn) { + // After 20 April 2017, Google OAuth login will no longer work from + // a WebView, so Cordova apps must use Google Sign-In instead. + // https://github.com/meteor/meteor/issues/8253 + Google.signIn(options, callback); + return; + } // Use Google's domain-specific login page if we want to restrict creation to + + // a particular email domain. (Don't use it if restrictCreationByEmailDomain + // is a function.) Note that all this does is change Google's UI --- + // accounts-base/accounts_server.js still checks server-side that the server + // has the proper email address after the OAuth conversation. + if (typeof Accounts._options.restrictCreationByEmailDomain === 'string') { + options = Object.assign({}, options || {}); + options.loginUrlParameters = Object.assign({}, options.loginUrlParameters || {}); + options.loginUrlParameters.hd = Accounts._options.restrictCreationByEmailDomain; + } + + innerLoginWithGoogleAndTOTP(options, code, callback); +}; + +Meteor.loginWithGoogle = (options, callback) => { + overrideLoginMethod(loginWithGoogle, [options], callback, loginWithGoogleAndTOTP); +}; diff --git a/apps/meteor/client/meteorOverrides/login/ldap.ts b/apps/meteor/client/meteorOverrides/login/ldap.ts new file mode 100644 index 000000000000..77a16ce3675d --- /dev/null +++ b/apps/meteor/client/meteorOverrides/login/ldap.ts @@ -0,0 +1,52 @@ +import { Meteor } from 'meteor/meteor'; + +import { callLoginMethod, handleLogin, type LoginCallback } from '../../lib/2fa/overrideLoginMethod'; + +declare module 'meteor/meteor' { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Meteor { + function loginWithLDAP( + username: string | { username: string } | { email: string } | { id: string }, + ldapPass: string, + callback?: LoginCallback, + ): void; + } +} + +const loginWithLDAP = (username: string | { username: string } | { email: string } | { id: string }, ldapPass: string) => + callLoginMethod({ + methodArguments: [ + { + ldap: true, + username, + ldapPass, + ldapOptions: {}, + }, + ], + }); + +const loginWithLDAPAndTOTP = ( + username: string | { username: string } | { email: string } | { id: string }, + ldapPass: string, + code: string, +) => { + const loginRequest = { + ldap: true, + username, + ldapPass, + ldapOptions: {}, + }; + + return callLoginMethod({ + methodArguments: [ + { + totp: { + login: loginRequest, + code, + }, + }, + ], + }); +}; + +Meteor.loginWithLDAP = handleLogin(loginWithLDAP, loginWithLDAPAndTOTP); diff --git a/apps/meteor/client/meteorOverrides/login/linkedin.ts b/apps/meteor/client/meteorOverrides/login/linkedin.ts new file mode 100644 index 000000000000..0f309ee360f6 --- /dev/null +++ b/apps/meteor/client/meteorOverrides/login/linkedin.ts @@ -0,0 +1,18 @@ +import { Meteor } from 'meteor/meteor'; +import { Linkedin } from 'meteor/pauli:linkedin-oauth'; + +import type { LoginCallback } from '../../lib/2fa/overrideLoginMethod'; +import { overrideLoginMethod } from '../../lib/2fa/overrideLoginMethod'; +import { createOAuthTotpLoginMethod } from './oauth'; + +declare module 'meteor/meteor' { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Meteor { + function loginWithLinkedin(options?: Meteor.LoginWithExternalServiceOptions, callback?: LoginCallback): void; + } +} +const { loginWithLinkedin } = Meteor; +const loginWithLinkedinAndTOTP = createOAuthTotpLoginMethod(Linkedin); +Meteor.loginWithLinkedin = (options, callback) => { + overrideLoginMethod(loginWithLinkedin, [options], callback, loginWithLinkedinAndTOTP); +}; diff --git a/apps/meteor/client/meteorOverrides/login/meteorDeveloperAccount.ts b/apps/meteor/client/meteorOverrides/login/meteorDeveloperAccount.ts new file mode 100644 index 000000000000..9577194f4043 --- /dev/null +++ b/apps/meteor/client/meteorOverrides/login/meteorDeveloperAccount.ts @@ -0,0 +1,11 @@ +import { Meteor } from 'meteor/meteor'; +import { MeteorDeveloperAccounts } from 'meteor/meteor-developer-oauth'; + +import { overrideLoginMethod } from '../../lib/2fa/overrideLoginMethod'; +import { createOAuthTotpLoginMethod } from './oauth'; + +const { loginWithMeteorDeveloperAccount } = Meteor; +const loginWithMeteorDeveloperAccountAndTOTP = createOAuthTotpLoginMethod(MeteorDeveloperAccounts); +Meteor.loginWithMeteorDeveloperAccount = (options, callback) => { + overrideLoginMethod(loginWithMeteorDeveloperAccount, [options], callback, loginWithMeteorDeveloperAccountAndTOTP); +}; diff --git a/apps/meteor/client/meteorOverrides/login/oauth.ts b/apps/meteor/client/meteorOverrides/login/oauth.ts new file mode 100644 index 000000000000..a3f9d72c9cbf --- /dev/null +++ b/apps/meteor/client/meteorOverrides/login/oauth.ts @@ -0,0 +1,127 @@ +import { Accounts } from 'meteor/accounts-base'; +import { Meteor } from 'meteor/meteor'; +import { OAuth } from 'meteor/oauth'; + +import type { IOAuthProvider } from '../../definitions/IOAuthProvider'; +import type { LoginCallback } from '../../lib/2fa/overrideLoginMethod'; + +const isLoginCancelledError = (error: unknown): error is Meteor.Error => + error instanceof Meteor.Error && error.error === Accounts.LoginCancelledError.numericError; + +export const convertError = (error: T): Accounts.LoginCancelledError | T => { + if (isLoginCancelledError(error)) { + return new Accounts.LoginCancelledError(error.reason); + } + + return error; +}; + +let lastCredentialToken: string | null = null; +let lastCredentialSecret: string | null | undefined = null; + +const meteorOAuthRetrieveCredentialSecret = OAuth._retrieveCredentialSecret; +OAuth._retrieveCredentialSecret = (credentialToken: string): string | null => { + let secret = meteorOAuthRetrieveCredentialSecret.call(OAuth, credentialToken); + if (!secret) { + const localStorageKey = `${OAuth._storageTokenPrefix}${credentialToken}`; + secret = localStorage.getItem(localStorageKey); + localStorage.removeItem(localStorageKey); + } + + return secret; +}; + +const tryLoginAfterPopupClosed = ( + credentialToken: string, + callback?: (error?: globalThis.Error | Meteor.Error | Meteor.TypedError) => void, + totpCode?: string, + credentialSecret?: string | null, +) => { + credentialSecret = credentialSecret || OAuth._retrieveCredentialSecret(credentialToken) || null; + const methodArgument = { + oauth: { + credentialToken, + credentialSecret, + }, + ...(typeof totpCode === 'string' && + !!totpCode && { + totp: { + code: totpCode, + }, + }), + }; + + lastCredentialToken = credentialToken; + lastCredentialSecret = credentialSecret; + + if (typeof totpCode === 'string' && !!totpCode) { + methodArgument.totp = { + code: totpCode, + }; + } + + Accounts.callLoginMethod({ + methodArguments: [methodArgument], + userCallback: (err) => { + callback?.(convertError(err)); + }, + }); +}; + +const credentialRequestCompleteHandler = + (callback?: (error?: globalThis.Error | Meteor.Error | Meteor.TypedError) => void, totpCode?: string) => + (credentialTokenOrError?: string | globalThis.Error | Meteor.Error | Meteor.TypedError) => { + if (!credentialTokenOrError) { + callback?.(new Meteor.Error('No credential token passed')); + return; + } + + if (credentialTokenOrError instanceof Error) { + callback?.(credentialTokenOrError); + return; + } + + tryLoginAfterPopupClosed(credentialTokenOrError, callback, totpCode); + }; + +export const createOAuthTotpLoginMethod = + (provider: IOAuthProvider) => (options: Meteor.LoginWithExternalServiceOptions | undefined, code: string, callback?: LoginCallback) => { + if (lastCredentialToken && lastCredentialSecret) { + tryLoginAfterPopupClosed(lastCredentialToken, callback, code, lastCredentialSecret); + } else { + const credentialRequestCompleteCallback = credentialRequestCompleteHandler(callback, code); + provider.requestCredential(options, credentialRequestCompleteCallback); + } + + lastCredentialToken = null; + lastCredentialSecret = null; + }; + +Accounts.oauth.credentialRequestCompleteHandler = credentialRequestCompleteHandler; + +Accounts.onPageLoadLogin(async (loginAttempt: any) => { + if (loginAttempt?.error?.error !== 'totp-required') { + return; + } + + const { methodArguments } = loginAttempt; + if (!methodArguments?.length) { + return; + } + + const oAuthArgs = methodArguments.find((arg: any) => arg.oauth); + const { credentialToken, credentialSecret } = oAuthArgs.oauth; + const cb = loginAttempt.userCallback; + + const { process2faReturn } = await import('../../lib/2fa/process2faReturn'); + + await process2faReturn({ + error: loginAttempt.error, + originalCallback: cb, + onCode: (code) => { + tryLoginAfterPopupClosed(credentialToken, cb, code, credentialSecret); + }, + emailOrUsername: undefined, + result: undefined, + }); +}); diff --git a/apps/meteor/client/meteorOverrides/login/password.ts b/apps/meteor/client/meteorOverrides/login/password.ts new file mode 100644 index 000000000000..f1c6e32f2282 --- /dev/null +++ b/apps/meteor/client/meteorOverrides/login/password.ts @@ -0,0 +1,67 @@ +import { Accounts } from 'meteor/accounts-base'; +import { Meteor } from 'meteor/meteor'; + +import { overrideLoginMethod, type LoginCallback } from '../../lib/2fa/overrideLoginMethod'; + +declare module 'meteor/meteor' { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Meteor { + function loginWithPassword( + userDescriptor: { username: string } | { email: string } | { id: string } | string, + password: string, + callback?: LoginCallback, + ): void; + } +} + +const loginWithPasswordAndTOTP = ( + userDescriptor: { username: string } | { email: string } | { id: string } | string, + password: string, + code: string, + callback?: LoginCallback, +) => { + if (typeof userDescriptor === 'string') { + if (userDescriptor.indexOf('@') === -1) { + userDescriptor = { username: userDescriptor }; + } else { + userDescriptor = { email: userDescriptor }; + } + } + + Accounts.callLoginMethod({ + methodArguments: [ + { + totp: { + login: { + user: userDescriptor, + password: Accounts._hashPassword(password), + }, + code, + }, + }, + ], + userCallback(error) { + if (!error) { + callback?.(undefined); + return; + } + + if (callback) { + callback(error); + return; + } + + throw error; + }, + }); +}; + +const { loginWithPassword } = Meteor; + +Meteor.loginWithPassword = ( + userDescriptor: { username: string } | { email: string } | { id: string } | string, + password: string, + callback?: LoginCallback, +) => { + overrideLoginMethod(loginWithPassword, [userDescriptor, password], callback, loginWithPasswordAndTOTP); +}; diff --git a/apps/meteor/client/meteorOverrides/login/saml.ts b/apps/meteor/client/meteorOverrides/login/saml.ts new file mode 100644 index 000000000000..8972cfe4812f --- /dev/null +++ b/apps/meteor/client/meteorOverrides/login/saml.ts @@ -0,0 +1,111 @@ +import { Random } from '@rocket.chat/random'; +import { Accounts } from 'meteor/accounts-base'; +import { Meteor } from 'meteor/meteor'; +import { ServiceConfiguration } from 'meteor/service-configuration'; + +import { type LoginCallback, callLoginMethod, handleLogin } from '../../lib/2fa/overrideLoginMethod'; + +declare module 'meteor/meteor' { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Meteor { + function loginWithSamlToken(credentialToken: string, callback?: LoginCallback): void; + + function loginWithSaml(options: { provider: string; credentialToken?: string }): void; + } +} + +declare module 'meteor/accounts-base' { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Accounts { + export let saml: { + credentialToken?: string; + credentialSecret?: string; + }; + } +} + +declare module 'meteor/service-configuration' { + // eslint-disable-next-line @typescript-eslint/naming-convention + interface Configuration { + logoutBehaviour?: 'SAML' | 'Local'; + idpSLORedirectURL?: string; + } +} + +if (!Accounts.saml) { + Accounts.saml = {}; +} + +const { logout } = Meteor; + +Meteor.logout = async function (...args) { + const { sdk } = await import('../../../app/utils/client/lib/SDKClient'); + const samlService = await ServiceConfiguration.configurations.findOneAsync({ service: 'saml' }); + if (samlService) { + const provider = (samlService.clientConfig as { provider?: string } | undefined)?.provider; + if (provider) { + if (samlService.logoutBehaviour == null || samlService.logoutBehaviour === 'SAML') { + if (samlService.idpSLORedirectURL) { + console.info('SAML session terminated via SLO'); + sdk + .call('samlLogout', provider) + .then((result) => { + if (!result) { + logout.apply(Meteor); + return; + } + + // Remove the userId from the client to prevent calls to the server while the logout is processed. + // If the logout fails, the userId will be reloaded on the resume call + Meteor._localStorage.removeItem(Accounts.USER_ID_KEY); + + // A nasty bounce: 'result' has the SAML LogoutRequest but we need a proper 302 to redirected from the server. + window.location.replace(Meteor.absoluteUrl(`_saml/sloRedirect/${provider}/?redirect=${encodeURIComponent(result)}`)); + }) + .catch(() => logout.apply(Meteor)); + return; + } + } + + if (samlService.logoutBehaviour === 'Local') { + console.info('SAML session not terminated, only the Rocket.Chat session is going to be killed'); + } + } + } + return logout.apply(Meteor, args); +}; + +Meteor.loginWithSaml = (options) => { + options = options || {}; + const credentialToken = `id-${Random.id()}`; + options.credentialToken = credentialToken; + + window.location.href = `_saml/authorize/${options.provider}/${options.credentialToken}`; +}; + +const loginWithSamlToken = (credentialToken: string) => + callLoginMethod({ + methodArguments: [ + { + saml: true, + credentialToken, + }, + ], + }); + +const loginWithSamlTokenAndTOTP = (credentialToken: string, code: string) => + callLoginMethod({ + methodArguments: [ + { + totp: { + login: { + saml: true, + credentialToken, + }, + code, + }, + }, + ], + }); + +Meteor.loginWithSamlToken = handleLogin(loginWithSamlToken, loginWithSamlTokenAndTOTP); diff --git a/apps/meteor/client/meteorOverrides/login/twitter.ts b/apps/meteor/client/meteorOverrides/login/twitter.ts new file mode 100644 index 000000000000..955277b1ce56 --- /dev/null +++ b/apps/meteor/client/meteorOverrides/login/twitter.ts @@ -0,0 +1,11 @@ +import { Meteor } from 'meteor/meteor'; +import { Twitter } from 'meteor/twitter-oauth'; + +import { overrideLoginMethod } from '../../lib/2fa/overrideLoginMethod'; +import { createOAuthTotpLoginMethod } from './oauth'; + +const { loginWithTwitter } = Meteor; +const loginWithTwitterAndTOTP = createOAuthTotpLoginMethod(Twitter); +Meteor.loginWithTwitter = (options, callback) => { + overrideLoginMethod(loginWithTwitter, [options], callback, loginWithTwitterAndTOTP); +}; diff --git a/apps/meteor/lib/oauthRedirectUriClient.ts b/apps/meteor/client/meteorOverrides/oauthRedirectUri.ts similarity index 80% rename from apps/meteor/lib/oauthRedirectUriClient.ts rename to apps/meteor/client/meteorOverrides/oauthRedirectUri.ts index cb5581210432..23f53acfe1d7 100644 --- a/apps/meteor/lib/oauthRedirectUriClient.ts +++ b/apps/meteor/client/meteorOverrides/oauthRedirectUri.ts @@ -1,5 +1,12 @@ import { OAuth } from 'meteor/oauth'; +declare module 'meteor/oauth' { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace OAuth { + function _redirectUri(serviceName: string, config: any, params: any, absoluteUrlOptions: any): string; + } +} + const { _redirectUri } = OAuth; OAuth._redirectUri = (serviceName: string, config: any, params: unknown, absoluteUrlOptions: unknown): string => { diff --git a/apps/meteor/client/meteorOverrides/totpOnCall.ts b/apps/meteor/client/meteorOverrides/totpOnCall.ts new file mode 100644 index 000000000000..247b3897842f --- /dev/null +++ b/apps/meteor/client/meteorOverrides/totpOnCall.ts @@ -0,0 +1,63 @@ +import { Meteor } from 'meteor/meteor'; + +import { t } from '../../app/utils/lib/i18n'; +import type { LoginCallback } from '../lib/2fa/overrideLoginMethod'; +import { process2faReturn, process2faAsyncReturn } from '../lib/2fa/process2faReturn'; +import { isTotpInvalidError } from '../lib/2fa/utils'; + +const withSyncTOTP = (call: (name: string, ...args: any[]) => any) => { + const callWithTotp = + (methodName: string, args: unknown[], callback: LoginCallback) => + (twoFactorCode: string, twoFactorMethod: string): unknown => + call( + methodName, + ...args, + { twoFactorCode, twoFactorMethod }, + (error: globalThis.Error | Meteor.Error | Meteor.TypedError | undefined, result: unknown): void => { + if (isTotpInvalidError(error)) { + callback(new Error(twoFactorMethod === 'password' ? t('Invalid_password') : t('Invalid_two_factor_code'))); + return; + } + + callback(error, result); + }, + ); + + const callWithoutTotp = (methodName: string, args: unknown[], callback: LoginCallback) => (): unknown => + call( + methodName, + ...args, + async (error: globalThis.Error | Meteor.Error | Meteor.TypedError | undefined, result: unknown): Promise => { + await process2faReturn({ + error, + result, + onCode: callWithTotp(methodName, args, callback), + originalCallback: callback, + emailOrUsername: undefined, + }); + }, + ); + + return function (methodName: string, ...args: unknown[]): unknown { + const callback = args.length > 0 && typeof args[args.length - 1] === 'function' ? (args.pop() as LoginCallback) : (): void => undefined; + + return callWithoutTotp(methodName, args, callback)(); + }; +}; + +const withAsyncTOTP = (callAsync: (name: string, ...args: any[]) => Promise) => { + return async function callAsyncWithTOTP(methodName: string, ...args: unknown[]): Promise { + try { + return await callAsync(methodName, ...args); + } catch (error: unknown) { + return process2faAsyncReturn({ + error, + onCode: (twoFactorCode, twoFactorMethod) => Meteor.callAsync(methodName, ...args, { twoFactorCode, twoFactorMethod }), + emailOrUsername: undefined, + }); + } + }; +}; + +Meteor.call = withSyncTOTP(Meteor.call); +Meteor.callAsync = withAsyncTOTP(Meteor.callAsync); diff --git a/apps/meteor/client/meteorOverrides/userAndUsers.ts b/apps/meteor/client/meteorOverrides/userAndUsers.ts new file mode 100644 index 000000000000..84bd85ff38d2 --- /dev/null +++ b/apps/meteor/client/meteorOverrides/userAndUsers.ts @@ -0,0 +1,14 @@ +import { Users } from '../../app/models/client/models/Users'; + +Meteor.users = Users as typeof Meteor.users; + +// overwrite Meteor.users collection so records on it don't get erased whenever the client reconnects to websocket +Meteor.user = function user(): Meteor.User | null { + const uid = Meteor.userId(); + + if (!uid) { + return null; + } + + return (Users.findOne({ _id: uid }) ?? null) as Meteor.User | null; +}; diff --git a/apps/meteor/client/providers/UserProvider/UserProvider.tsx b/apps/meteor/client/providers/UserProvider/UserProvider.tsx index 09f631ffa6a6..b6e30134cdaa 100644 --- a/apps/meteor/client/providers/UserProvider/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider/UserProvider.tsx @@ -53,7 +53,7 @@ const logout = (): Promise => }); }); -export type LoginMethods = keyof typeof Meteor; +export type LoginMethods = keyof typeof Meteor extends infer T ? (T extends `loginWith${string}` ? T : never) : never; type UserProviderProps = { children: ReactNode; @@ -107,7 +107,7 @@ const UserProvider = ({ children }: UserProviderProps): ReactElement => { ), loginWithPassword: (user: string | { username: string } | { email: string } | { id: string }, password: string): Promise => new Promise((resolve, reject) => { - Meteor[loginMethod](user, password, (error: Error | Meteor.Error | Meteor.TypedError | undefined) => { + Meteor[loginMethod](user, password, (error) => { if (error) { reject(error); return; @@ -120,9 +120,9 @@ const UserProvider = ({ children }: UserProviderProps): ReactElement => { loginWithService: ({ service, clientConfig = {} }: T): (() => Promise) => { const loginMethods = { 'meteor-developer': 'MeteorDeveloperAccount', - }; + } as const; - const loginWithService = `loginWith${(loginMethods as any)[service] || capitalize(String(service || ''))}`; + const loginWithService = `loginWith${loginMethods[service] || capitalize(String(service || ''))}`; const method: (config: unknown, cb: (error: any) => void) => Promise = (Meteor as any)[loginWithService] as any; diff --git a/apps/meteor/client/startup/customOAuth.ts b/apps/meteor/client/startup/customOAuth.ts index 5b0e3dfb4261..529540566b95 100644 --- a/apps/meteor/client/startup/customOAuth.ts +++ b/apps/meteor/client/startup/customOAuth.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { ServiceConfiguration } from 'meteor/service-configuration'; -import { CustomOAuth } from '../../app/custom-oauth/client/custom_oauth_client'; +import { CustomOAuth } from '../../app/custom-oauth/client/CustomOAuth'; Meteor.startup(() => { ServiceConfiguration.configurations diff --git a/apps/meteor/client/startup/index.ts b/apps/meteor/client/startup/index.ts index 61eaa0da16ed..5fa2bb0bc5ec 100644 --- a/apps/meteor/client/startup/index.ts +++ b/apps/meteor/client/startup/index.ts @@ -10,13 +10,11 @@ import './e2e'; import './forceLogout'; import './iframeCommands'; import './incomingMessages'; -import './ldap'; import './loadMissedMessages'; import './loginViaQuery'; import './messageObserve'; import './messageTypes'; import './notifications'; -import './oauth'; import './otr'; import './reloadRoomAfterLogin'; import './roles'; diff --git a/apps/meteor/client/startup/ldap.ts b/apps/meteor/client/startup/ldap.ts deleted file mode 100644 index 13f6048bb2eb..000000000000 --- a/apps/meteor/client/startup/ldap.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; - -(Meteor as any).loginWithLDAP = function (username: string, password: string, callback?: (err?: any) => void): void { - Accounts.callLoginMethod({ - methodArguments: [ - { - ldap: true, - username, - ldapPass: password, - ldapOptions: {}, - }, - ], - userCallback: callback, - }); -}; diff --git a/apps/meteor/client/startup/oauth.ts b/apps/meteor/client/startup/oauth.ts deleted file mode 100644 index 23f5ec8246b4..000000000000 --- a/apps/meteor/client/startup/oauth.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { OAuth } from 'meteor/oauth'; - -// OAuth._retrieveCredentialSecret is a meteor method modified to also check the global localStorage -// This was necessary because of the "Forget User Session on Window Close" setting. -// The setting changes Meteor._localStorage to use the browser's session storage instead, but that doesn't happen on the Oauth's popup code. - -Meteor.startup(() => { - const meteorOAuthRetrieveCredentialSecret = OAuth._retrieveCredentialSecret; - OAuth._retrieveCredentialSecret = (credentialToken: string): string | null => { - let secret = meteorOAuthRetrieveCredentialSecret.call(OAuth, credentialToken); - if (!secret) { - const localStorageKey = `${OAuth._storageTokenPrefix}${credentialToken}`; - secret = localStorage.getItem(localStorageKey); - localStorage.removeItem(localStorageKey); - } - - return secret; - }; -}); diff --git a/apps/meteor/client/views/marketplace/hooks/useAppRequestStats.ts b/apps/meteor/client/views/marketplace/hooks/useAppRequestStats.ts index af25282b7e53..3fff24cc9216 100644 --- a/apps/meteor/client/views/marketplace/hooks/useAppRequestStats.ts +++ b/apps/meteor/client/views/marketplace/hooks/useAppRequestStats.ts @@ -8,7 +8,8 @@ export const useAppRequestStats = () => { return useQuery({ queryKey: ['app-requests-stats'], - queryFn: async () => (await fetchRequestStats()).data, + queryFn: () => fetchRequestStats(), + select: ({ data }) => data, refetchOnWindowFocus: false, retry: false, enabled: canManageApp, diff --git a/apps/meteor/definition/externals/meteor/accounts-base.d.ts b/apps/meteor/definition/externals/meteor/accounts-base.d.ts index 0e15e50b39fb..3f0b148120e7 100644 --- a/apps/meteor/definition/externals/meteor/accounts-base.d.ts +++ b/apps/meteor/definition/externals/meteor/accounts-base.d.ts @@ -24,7 +24,7 @@ declare module 'meteor/accounts-base' { function _runLoginHandlers(methodInvocation: T, loginRequest: Record): LoginMethodResult | undefined; - function registerLoginHandler(name: string, handler: (options: any) => undefined | Object): void; + function registerLoginHandler(name: string, handler: (options: any) => undefined | object): void; function _storedLoginToken(): unknown; @@ -53,5 +53,15 @@ declare module 'meteor/accounts-base' { const LOGIN_TOKEN_KEY: string; const _accountData: Record; + + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace oauth { + function credentialRequestCompleteHandler( + callback?: (error?: globalThis.Error | Meteor.Error | Meteor.TypedError) => void, + totpCode?: string, + ): (credentialTokenOrError?: string | globalThis.Error | Meteor.Error | Meteor.TypedError) => void; + + function registerService(name: string): void; + } } } diff --git a/apps/meteor/definition/externals/meteor/facebook-oauth.d.ts b/apps/meteor/definition/externals/meteor/facebook-oauth.d.ts new file mode 100644 index 000000000000..d16e36efb67c --- /dev/null +++ b/apps/meteor/definition/externals/meteor/facebook-oauth.d.ts @@ -0,0 +1,3 @@ +declare module 'meteor/facebook-oauth' { + export const Facebook: any; +} diff --git a/apps/meteor/definition/externals/meteor/github-oauth.d.ts b/apps/meteor/definition/externals/meteor/github-oauth.d.ts new file mode 100644 index 000000000000..ac67405bd4e4 --- /dev/null +++ b/apps/meteor/definition/externals/meteor/github-oauth.d.ts @@ -0,0 +1,3 @@ +declare module 'meteor/github-oauth' { + export const Github: any; +} diff --git a/apps/meteor/definition/externals/meteor/google-oauth.d.ts b/apps/meteor/definition/externals/meteor/google-oauth.d.ts new file mode 100644 index 000000000000..a15f3de64b7f --- /dev/null +++ b/apps/meteor/definition/externals/meteor/google-oauth.d.ts @@ -0,0 +1,3 @@ +declare module 'meteor/google-oauth' { + export const Google: any; +} diff --git a/apps/meteor/definition/externals/meteor/meteor-developer-oauth.d.ts b/apps/meteor/definition/externals/meteor/meteor-developer-oauth.d.ts new file mode 100644 index 000000000000..9a86a4b483e7 --- /dev/null +++ b/apps/meteor/definition/externals/meteor/meteor-developer-oauth.d.ts @@ -0,0 +1,3 @@ +declare module 'meteor/meteor-developer-oauth' { + export const MeteorDeveloperAccounts: any; +} diff --git a/apps/meteor/definition/externals/meteor/meteor.d.ts b/apps/meteor/definition/externals/meteor/meteor.d.ts index 4854d24a37ba..7d8722896f8e 100644 --- a/apps/meteor/definition/externals/meteor/meteor.d.ts +++ b/apps/meteor/definition/externals/meteor/meteor.d.ts @@ -110,20 +110,6 @@ declare module 'meteor/meteor' { function _relativeToSiteRootUrl(path: string): string; const _localStorage: Window['localStorage']; - function loginWithLDAP( - username: string | object, - password: string, - cb: (error?: Error | Meteor.Error | Meteor.TypedError) => void, - ): void; - - function loginWithCrowd( - username: string | object, - password: string, - cb: (error?: Error | Meteor.Error | Meteor.TypedError) => void, - ): void; - - function loginWithSamlToken(token: string, cb: (error?: Error | Meteor.Error | Meteor.TypedError) => void): void; - function methods(methods: { [TMethodName in keyof TServerMethods]?: ( this: MethodThisType, @@ -137,4 +123,9 @@ declare module 'meteor/meteor' { } | undefined; } + + // eslint-disable-next-line no-var + var Meteor: { + [key: `loginWith${string}`]: any; + }; } diff --git a/apps/meteor/definition/externals/meteor/oauth.d.ts b/apps/meteor/definition/externals/meteor/oauth.d.ts index 0f75bc611996..9573b2888f49 100644 --- a/apps/meteor/definition/externals/meteor/oauth.d.ts +++ b/apps/meteor/definition/externals/meteor/oauth.d.ts @@ -1,6 +1,7 @@ declare module 'meteor/oauth' { import type { IRocketChatRecord } from '@rocket.chat/core-typings'; import type { Mongo } from 'meteor/mongo'; + import type { Configuration } from 'meteor/service-configuration'; interface IOauthCredentials extends IRocketChatRecord { key: string; @@ -13,12 +14,29 @@ declare module 'meteor/oauth' { } namespace OAuth { - function _redirectUri(serviceName: string, config: any, params: any, absoluteUrlOptions: any): string; - function _retrieveCredentialSecret(credentialToken: string): string | null; function _retrievePendingCredential(key: string, ...args: string[]): void; function openSecret(secret: string): string; function retrieveCredential(credentialToken: string, credentialSecret: string); - const _storageTokenPrefix: string; + function _retrieveCredentialSecret(credentialToken: string): string | null; const _pendingCredentials: Mongo.Collection; + const _storageTokenPrefix: string; + + function launchLogin(options: { + loginService: string; + loginStyle: string; + loginUrl: string; + credentialRequestCompleteCallback?: (credentialTokenOrError?: string | Error) => void; + credentialToken: string; + popupOptions: { + width: number; + height: number; + }; + }): void; + + function _stateParam(loginStyle: string, credentialToken: string, redirectUrl?: string): string; + + function _redirectUri(serviceName: string, config: Configuration, params?: any, absoluteUrlOptions?: any): string; + + function _loginStyle(serviceName: string, config: Configuration, options?: Meteor.LoginWithExternalServiceOptions): string; } } diff --git a/apps/meteor/definition/externals/meteor/pauli-linkedin-oauth.d.ts b/apps/meteor/definition/externals/meteor/pauli-linkedin-oauth.d.ts new file mode 100644 index 000000000000..14c313059710 --- /dev/null +++ b/apps/meteor/definition/externals/meteor/pauli-linkedin-oauth.d.ts @@ -0,0 +1,3 @@ +declare module 'meteor/pauli:linkedin-oauth' { + export const Linkedin: any; +} diff --git a/apps/meteor/definition/externals/meteor/twitter-oauth.d.ts b/apps/meteor/definition/externals/meteor/twitter-oauth.d.ts new file mode 100644 index 000000000000..9bef027292dc --- /dev/null +++ b/apps/meteor/definition/externals/meteor/twitter-oauth.d.ts @@ -0,0 +1,3 @@ +declare module 'meteor/twitter-oauth' { + export const Twitter: any; +} diff --git a/apps/meteor/definition/externals/service-configuration.d.ts b/apps/meteor/definition/externals/service-configuration.d.ts index 1a208c71e044..bdea46eedab2 100644 --- a/apps/meteor/definition/externals/service-configuration.d.ts +++ b/apps/meteor/definition/externals/service-configuration.d.ts @@ -9,15 +9,6 @@ declare module 'meteor/service-configuration' { buttonColor?: string; clientConfig: unknown; + clientId?: string; } } - -declare module 'meteor' { - interface Configuration { - appId: string; - secret: string; - } - const ServiceConfiguration: { - configurations: Mongo.Collection; - }; -} diff --git a/apps/meteor/lib/oauthRedirectUriServer.ts b/apps/meteor/lib/oauthRedirectUriServer.ts index ce89f00ef2a2..fe2bb788f6fd 100644 --- a/apps/meteor/lib/oauthRedirectUriServer.ts +++ b/apps/meteor/lib/oauthRedirectUriServer.ts @@ -4,7 +4,7 @@ import { SystemLogger } from '../server/lib/logger/system'; const { _redirectUri } = OAuth; -OAuth._redirectUri = (serviceName: string, config: any, params: unknown, absoluteUrlOptions: unknown): string => { +OAuth._redirectUri = (serviceName, config, params, absoluteUrlOptions) => { const ret = _redirectUri(serviceName, config, params, absoluteUrlOptions); // DEPRECATED: Remove in v5.0.0 diff --git a/apps/meteor/packages/linkedin-oauth/linkedin-client.js b/apps/meteor/packages/linkedin-oauth/linkedin-client.js index 29be9dd1af22..4803d69d34b1 100644 --- a/apps/meteor/packages/linkedin-oauth/linkedin-client.js +++ b/apps/meteor/packages/linkedin-oauth/linkedin-client.js @@ -18,7 +18,7 @@ Linkedin.requestCredential = async function (options, credentialRequestCompleteC const config = await ServiceConfiguration.configurations.findOneAsync({ service: 'linkedin' }); if (!config) { - throw new ServiceConfiguration.ConfigError('Service not configured'); + throw new Accounts.ConfigError('Service not configured'); } const credentialToken = Random.secret(); diff --git a/apps/meteor/packages/linkedin-oauth/linkedin-server.js b/apps/meteor/packages/linkedin-oauth/linkedin-server.js index 62e4dad143ce..09a12a528dda 100644 --- a/apps/meteor/packages/linkedin-oauth/linkedin-server.js +++ b/apps/meteor/packages/linkedin-oauth/linkedin-server.js @@ -9,7 +9,7 @@ export const Linkedin = {}; // - expiresIn: lifetime of token in seconds const getTokenResponse = async function (query) { const config = await ServiceConfiguration.configurations.findOneAsync({ service: 'linkedin' }); - if (!config) throw new ServiceConfiguration.ConfigError('Service not configured'); + if (!config) throw new Accounts.ConfigError('Service not configured'); let responseContent; try { diff --git a/packages/core-typings/src/ICustomOAuthConfig.ts b/packages/core-typings/src/ICustomOAuthConfig.ts index dfbddc5ce2d5..ff695865cf9d 100644 --- a/packages/core-typings/src/ICustomOAuthConfig.ts +++ b/packages/core-typings/src/ICustomOAuthConfig.ts @@ -1,7 +1,7 @@ export type OauthConfig = { serverURL?: string; identityPath?: string; - addAutopublishFields: { + addAutopublishFields?: { forLoggedInUser: string[]; forOtherUsers: string[]; }; @@ -13,4 +13,5 @@ export type OauthConfig = { tokenSentVia?: string; usernameField?: string; mergeUsers?: boolean; + responseType?: string; }; diff --git a/packages/rest-typings/src/v1/misc.ts b/packages/rest-typings/src/v1/misc.ts index 804b72a763de..cc06e1cc330a 100644 --- a/packages/rest-typings/src/v1/misc.ts +++ b/packages/rest-typings/src/v1/misc.ts @@ -235,13 +235,13 @@ export type MiscEndpoints = { '/v1/method.call/:method': { POST: (params: { message: string }) => { - message: unknown; + message: string; }; }; '/v1/method.callAnon/:method': { POST: (params: { message: string }) => { - message: unknown; + message: string; }; }; diff --git a/packages/ui-contexts/src/UserContext.ts b/packages/ui-contexts/src/UserContext.ts index 92587c61fa05..14b9644e6a3c 100644 --- a/packages/ui-contexts/src/UserContext.ts +++ b/packages/ui-contexts/src/UserContext.ts @@ -29,7 +29,7 @@ export type LoginService = { clientConfig: unknown; title: string; - service: string; + service: 'meteor-developer'; buttonLabelText?: string; icon?: string; From ebbad299b9dcce641ebd8175160ff65f3c0452fa Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Wed, 10 Jan 2024 11:05:12 -0300 Subject: [PATCH 034/152] chore: internalize meteor cookies (from meteor 3) (#31366) --- .../meteor/packages/meteor-cookies/cookies.js | 611 ++++++++++++++++++ .../meteor/packages/meteor-cookies/package.js | 15 + 2 files changed, 626 insertions(+) create mode 100644 apps/meteor/packages/meteor-cookies/cookies.js create mode 100644 apps/meteor/packages/meteor-cookies/package.js diff --git a/apps/meteor/packages/meteor-cookies/cookies.js b/apps/meteor/packages/meteor-cookies/cookies.js new file mode 100644 index 000000000000..f560babbfc90 --- /dev/null +++ b/apps/meteor/packages/meteor-cookies/cookies.js @@ -0,0 +1,611 @@ +import { Meteor } from 'meteor/meteor'; + +let fetch; +let WebApp; + +if (Meteor.isServer) { + WebApp = require('meteor/webapp').WebApp; +} else { + fetch = require('meteor/fetch').fetch; +} + +const NoOp = () => {}; +const urlRE = /\/___cookie___\/set/; +const rootUrl = Meteor.isServer ? process.env.ROOT_URL : (window.__meteor_runtime_config__.ROOT_URL || window.__meteor_runtime_config__.meteorEnv.ROOT_URL || false); +const mobileRootUrl = Meteor.isServer ? process.env.MOBILE_ROOT_URL : (window.__meteor_runtime_config__.MOBILE_ROOT_URL || window.__meteor_runtime_config__.meteorEnv.MOBILE_ROOT_URL || false); + +const helpers = { + isUndefined(obj) { + return obj === void 0; + }, + isArray(obj) { + return Array.isArray(obj); + }, + clone(obj) { + if (!this.isObject(obj)) return obj; + return this.isArray(obj) ? obj.slice() : Object.assign({}, obj); + } +}; +const _helpers = ['Number', 'Object', 'Function']; +for (let i = 0; i < _helpers.length; i++) { + helpers['is' + _helpers[i]] = function (obj) { + return Object.prototype.toString.call(obj) === '[object ' + _helpers[i] + ']'; + }; +} + +/** + * @url https://github.com/jshttp/cookie/blob/master/index.js + * @name cookie + * @author jshttp + * @license + * (The MIT License) + * + * Copyright (c) 2012-2014 Roman Shtylman + * Copyright (c) 2015 Douglas Christopher Wilson + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * 'Software'), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +const decode = decodeURIComponent; +const encode = encodeURIComponent; +const pairSplitRegExp = /; */; + +/** + * RegExp to match field-content in RFC 7230 sec 3.2 + * + * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + * field-vchar = VCHAR / obs-text + * obs-text = %x80-FF + */ +const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/; + +/** + * @function + * @name tryDecode + * @param {String} str + * @param {Function} d + * @summary Try decoding a string using a decoding function. + * @private + */ +const tryDecode = (str, d) => { + try { + return d(str); + } catch (e) { + return str; + } +}; + +/** + * @function + * @name parse + * @param {String} str + * @param {Object} [options] + * @return {Object} + * @summary + * Parse a cookie header. + * Parse the given cookie header string into an object + * The object has the various cookies as keys(names) => values + * @private + */ +const parse = (str, options) => { + if (typeof str !== 'string') { + throw new Meteor.Error(404, 'argument str must be a string'); + } + const obj = {}; + const opt = options || {}; + let val; + let key; + let eqIndx; + + str.split(pairSplitRegExp).forEach((pair) => { + eqIndx = pair.indexOf('='); + if (eqIndx < 0) { + return; + } + key = pair.substr(0, eqIndx).trim(); + key = tryDecode(unescape(key), (opt.decode || decode)); + val = pair.substr(++eqIndx, pair.length).trim(); + if (val[0] === '"') { + val = val.slice(1, -1); + } + if (void 0 === obj[key]) { + obj[key] = tryDecode(val, (opt.decode || decode)); + } + }); + return obj; +}; + +/** + * @function + * @name antiCircular + * @param data {Object} - Circular or any other object which needs to be non-circular + * @private + */ +const antiCircular = (_obj) => { + const object = helpers.clone(_obj); + const cache = new Map(); + return JSON.stringify(object, (key, value) => { + if (typeof value === 'object' && value !== null) { + if (cache.get(value)) { + return void 0; + } + cache.set(value, true); + } + return value; + }); +}; + +/** + * @function + * @name serialize + * @param {String} name + * @param {String} val + * @param {Object} [options] + * @return { cookieString: String, sanitizedValue: Mixed } + * @summary + * Serialize data into a cookie header. + * Serialize the a name value pair into a cookie string suitable for + * http headers. An optional options object specified cookie parameters. + * serialize('foo', 'bar', { httpOnly: true }) => "foo=bar; httpOnly" + * @private + */ +const serialize = (key, val, opt = {}) => { + let name; + + if (!fieldContentRegExp.test(key)) { + name = escape(key); + } else { + name = key; + } + + let sanitizedValue = val; + let value = val; + if (!helpers.isUndefined(value)) { + if (helpers.isObject(value) || helpers.isArray(value)) { + const stringified = antiCircular(value); + value = encode(`JSON.parse(${stringified})`); + sanitizedValue = JSON.parse(stringified); + } else { + value = encode(value); + if (value && !fieldContentRegExp.test(value)) { + value = escape(value); + } + } + } else { + value = ''; + } + + const pairs = [`${name}=${value}`]; + + if (helpers.isNumber(opt.maxAge)) { + pairs.push(`Max-Age=${opt.maxAge}`); + } + + if (opt.domain && typeof opt.domain === 'string') { + if (!fieldContentRegExp.test(opt.domain)) { + throw new Meteor.Error(404, 'option domain is invalid'); + } + pairs.push(`Domain=${opt.domain}`); + } + + if (opt.path && typeof opt.path === 'string') { + if (!fieldContentRegExp.test(opt.path)) { + throw new Meteor.Error(404, 'option path is invalid'); + } + pairs.push(`Path=${opt.path}`); + } else { + pairs.push('Path=/'); + } + + opt.expires = opt.expires || opt.expire || false; + if (opt.expires === Infinity) { + pairs.push('Expires=Fri, 31 Dec 9999 23:59:59 GMT'); + } else if (opt.expires instanceof Date) { + pairs.push(`Expires=${opt.expires.toUTCString()}`); + } else if (opt.expires === 0) { + pairs.push('Expires=0'); + } else if (helpers.isNumber(opt.expires)) { + pairs.push(`Expires=${(new Date(opt.expires)).toUTCString()}`); + } + + if (opt.httpOnly) { + pairs.push('HttpOnly'); + } + + if (opt.secure) { + pairs.push('Secure'); + } + + if (opt.firstPartyOnly) { + pairs.push('First-Party-Only'); + } + + if (opt.sameSite) { + pairs.push(opt.sameSite === true ? 'SameSite' : `SameSite=${opt.sameSite}`); + } + + return { cookieString: pairs.join('; '), sanitizedValue }; +}; + +const isStringifiedRegEx = /JSON\.parse\((.*)\)/; +const isTypedRegEx = /false|true|null/; +const deserialize = (string) => { + if (typeof string !== 'string') { + return string; + } + + if (isStringifiedRegEx.test(string)) { + let obj = string.match(isStringifiedRegEx)[1]; + if (obj) { + try { + return JSON.parse(decode(obj)); + } catch (e) { + console.error('[ostrio:cookies] [.get()] [deserialize()] Exception:', e, string, obj); + return string; + } + } + return string; + } else if (isTypedRegEx.test(string)) { + try { + return JSON.parse(string); + } catch (e) { + return string; + } + } + return string; +}; + +/** + * @locus Anywhere + * @class __cookies + * @param opts {Object} - Options (configuration) object + * @param opts._cookies {Object|String} - Current cookies as String or Object + * @param opts.TTL {Number|Boolean} - Default cookies expiration time (max-age) in milliseconds, by default - session (false) + * @param opts.runOnServer {Boolean} - Expose Cookies class to Server + * @param opts.response {http.ServerResponse|Object} - This object is created internally by a HTTP server + * @param opts.allowQueryStringCookies {Boolean} - Allow passing Cookies in a query string (in URL). Primary should be used only in Cordova environment + * @param opts.allowedCordovaOrigins {Regex|Boolean} - [Server] Allow setting Cookies from that specific origin which in Meteor/Cordova is localhost:12XXX (^http://localhost:12[0-9]{3}$) + * @summary Internal Class + */ +class __cookies { + constructor(opts) { + this.__pendingCookies = []; + this.TTL = opts.TTL || false; + this.response = opts.response || false; + this.runOnServer = opts.runOnServer || false; + this.allowQueryStringCookies = opts.allowQueryStringCookies || false; + this.allowedCordovaOrigins = opts.allowedCordovaOrigins || false; + + if (this.allowedCordovaOrigins === true) { + this.allowedCordovaOrigins = /^http:\/\/localhost:12[0-9]{3}$/; + } + + this.originRE = new RegExp(`^https?:\/\/(${rootUrl ? rootUrl : ''}${mobileRootUrl ? ('|' + mobileRootUrl) : ''})$`); + + if (helpers.isObject(opts._cookies)) { + this.cookies = opts._cookies; + } else { + this.cookies = parse(opts._cookies); + } + } + + /** + * @locus Anywhere + * @memberOf __cookies + * @name get + * @param {String} key - The name of the cookie to read + * @param {String} _tmp - Unparsed string instead of user's cookies + * @summary Read a cookie. If the cookie doesn't exist a null value will be returned. + * @returns {String|void} + */ + get(key, _tmp) { + const cookieString = _tmp ? parse(_tmp) : this.cookies; + if (!key || !cookieString) { + return void 0; + } + + if (cookieString.hasOwnProperty(key)) { + return deserialize(cookieString[key]); + } + + return void 0; + } + + /** + * @locus Anywhere + * @memberOf __cookies + * @name set + * @param {String} key - The name of the cookie to create/overwrite + * @param {String} value - The value of the cookie + * @param {Object} opts - [Optional] Cookie options (see readme docs) + * @summary Create/overwrite a cookie. + * @returns {Boolean} + */ + set(key, value, opts = {}) { + if (key && !helpers.isUndefined(value)) { + if (helpers.isNumber(this.TTL) && opts.expires === undefined) { + opts.expires = new Date(+new Date() + this.TTL); + } + const { cookieString, sanitizedValue } = serialize(key, value, opts); + + this.cookies[key] = sanitizedValue; + if (Meteor.isClient) { + document.cookie = cookieString; + } else if (this.response) { + this.__pendingCookies.push(cookieString); + this.response.setHeader('Set-Cookie', this.__pendingCookies); + } + return true; + } + return false; + } + + /** + * @locus Anywhere + * @memberOf __cookies + * @name remove + * @param {String} key - The name of the cookie to create/overwrite + * @param {String} path - [Optional] The path from where the cookie will be + * readable. E.g., "/", "/mydir"; if not specified, defaults to the current + * path of the current document location (string or null). The path must be + * absolute (see RFC 2965). For more information on how to use relative paths + * in this argument, see: https://developer.mozilla.org/en-US/docs/Web/API/document.cookie#Using_relative_URLs_in_the_path_parameter + * @param {String} domain - [Optional] The domain from where the cookie will + * be readable. E.g., "example.com", ".example.com" (includes all subdomains) + * or "subdomain.example.com"; if not specified, defaults to the host portion + * of the current document location (string or null). + * @summary Remove a cookie(s). + * @returns {Boolean} + */ + remove(key, path = '/', domain = '') { + if (key && this.cookies.hasOwnProperty(key)) { + const { cookieString } = serialize(key, '', { + domain, + path, + expires: new Date(0) + }); + + delete this.cookies[key]; + if (Meteor.isClient) { + document.cookie = cookieString; + } else if (this.response) { + this.response.setHeader('Set-Cookie', cookieString); + } + return true; + } else if (!key && this.keys().length > 0 && this.keys()[0] !== '') { + const keys = Object.keys(this.cookies); + for (let i = 0; i < keys.length; i++) { + this.remove(keys[i]); + } + return true; + } + return false; + } + + /** + * @locus Anywhere + * @memberOf __cookies + * @name has + * @param {String} key - The name of the cookie to create/overwrite + * @param {String} _tmp - Unparsed string instead of user's cookies + * @summary Check whether a cookie exists in the current position. + * @returns {Boolean} + */ + has(key, _tmp) { + const cookieString = _tmp ? parse(_tmp) : this.cookies; + if (!key || !cookieString) { + return false; + } + + return cookieString.hasOwnProperty(key); + } + + /** + * @locus Anywhere + * @memberOf __cookies + * @name keys + * @summary Returns an array of all readable cookies from this location. + * @returns {[String]} + */ + keys() { + if (this.cookies) { + return Object.keys(this.cookies); + } + return []; + } + + /** + * @locus Client + * @memberOf __cookies + * @name send + * @param cb {Function} - Callback + * @summary Send all cookies over XHR to server. + * @returns {void} + */ + send(cb = NoOp) { + if (Meteor.isServer) { + cb(new Meteor.Error(400, 'Can\'t run `.send()` on server, it\'s Client only method!')); + } + + if (this.runOnServer) { + let path = `${window.__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || window.__meteor_runtime_config__.meteorEnv.ROOT_URL_PATH_PREFIX || ''}/___cookie___/set`; + let query = ''; + + if (Meteor.isCordova && this.allowQueryStringCookies) { + const cookiesKeys = this.keys(); + const cookiesArray = []; + for (let i = 0; i < cookiesKeys.length; i++) { + const { sanitizedValue } = serialize(cookiesKeys[i], this.get(cookiesKeys[i])); + const pair = `${cookiesKeys[i]}=${sanitizedValue}`; + if (!cookiesArray.includes(pair)) { + cookiesArray.push(pair); + } + } + + if (cookiesArray.length) { + path = Meteor.absoluteUrl('___cookie___/set'); + query = `?___cookies___=${encodeURIComponent(cookiesArray.join('; '))}`; + } + } + + fetch(`${path}${query}`, { + credentials: 'include', + type: 'cors' + }).then((response) => { + cb(void 0, response); + }).catch(cb); + } else { + cb(new Meteor.Error(400, 'Can\'t send cookies on server when `runOnServer` is false.')); + } + return void 0; + } +} + +/** + * @function + * @locus Server + * @summary Middleware handler + * @private + */ +const __middlewareHandler = (request, response, opts) => { + let _cookies = {}; + if (opts.runOnServer) { + if (request.headers && request.headers.cookie) { + _cookies = parse(request.headers.cookie); + } + + return new __cookies({ + _cookies, + TTL: opts.TTL, + runOnServer: opts.runOnServer, + response, + allowQueryStringCookies: opts.allowQueryStringCookies + }); + } + + throw new Meteor.Error(400, 'Can\'t use middleware when `runOnServer` is false.'); +}; + +/** + * @locus Anywhere + * @class Cookies + * @param opts {Object} + * @param opts.TTL {Number} - Default cookies expiration time (max-age) in milliseconds, by default - session (false) + * @param opts.auto {Boolean} - [Server] Auto-bind in middleware as `req.Cookies`, by default `true` + * @param opts.handler {Function} - [Server] Middleware handler + * @param opts.runOnServer {Boolean} - Expose Cookies class to Server + * @param opts.allowQueryStringCookies {Boolean} - Allow passing Cookies in a query string (in URL). Primary should be used only in Cordova environment + * @param opts.allowedCordovaOrigins {Regex|Boolean} - [Server] Allow setting Cookies from that specific origin which in Meteor/Cordova is localhost:12XXX (^http://localhost:12[0-9]{3}$) + * @summary Main Cookie class + */ +class Cookies extends __cookies { + constructor(opts = {}) { + opts.TTL = helpers.isNumber(opts.TTL) ? opts.TTL : false; + opts.runOnServer = (opts.runOnServer !== false) ? true : false; + opts.allowQueryStringCookies = (opts.allowQueryStringCookies !== true) ? false : true; + + if (Meteor.isClient) { + opts._cookies = document.cookie; + super(opts); + } else { + opts._cookies = {}; + super(opts); + opts.auto = (opts.auto !== false) ? true : false; + this.opts = opts; + this.handler = helpers.isFunction(opts.handler) ? opts.handler : false; + this.onCookies = helpers.isFunction(opts.onCookies) ? opts.onCookies : false; + + if (opts.runOnServer && !Cookies.isLoadedOnServer) { + Cookies.isLoadedOnServer = true; + if (opts.auto) { + WebApp.connectHandlers.use((req, res, next) => { + if (urlRE.test(req._parsedUrl.path)) { + const matchedCordovaOrigin = !!req.headers.origin + && this.allowedCordovaOrigins + && this.allowedCordovaOrigins.test(req.headers.origin); + const matchedOrigin = matchedCordovaOrigin + || (!!req.headers.origin && this.originRE.test(req.headers.origin)); + + if (matchedOrigin) { + res.setHeader('Access-Control-Allow-Credentials', 'true'); + res.setHeader('Access-Control-Allow-Origin', req.headers.origin); + } + + const cookiesArray = []; + let cookiesObject = {}; + if (matchedCordovaOrigin && opts.allowQueryStringCookies && req.query.___cookies___) { + cookiesObject = parse(decodeURIComponent(req.query.___cookies___)); + } else if (req.headers.cookie) { + cookiesObject = parse(req.headers.cookie); + } + + const cookiesKeys = Object.keys(cookiesObject); + if (cookiesKeys.length) { + for (let i = 0; i < cookiesKeys.length; i++) { + const { cookieString } = serialize(cookiesKeys[i], cookiesObject[cookiesKeys[i]]); + if (!cookiesArray.includes(cookieString)) { + cookiesArray.push(cookieString); + } + } + + if (cookiesArray.length) { + res.setHeader('Set-Cookie', cookiesArray); + } + } + + helpers.isFunction(this.onCookies) && this.onCookies(__middlewareHandler(req, res, opts)); + + res.writeHead(200); + res.end(''); + } else { + req.Cookies = __middlewareHandler(req, res, opts); + helpers.isFunction(this.handler) && this.handler(req.Cookies); + next(); + } + }); + } + } + } + } + + /** + * @locus Server + * @memberOf Cookies + * @name middleware + * @summary Get Cookies instance into callback + * @returns {void} + */ + middleware() { + if (!Meteor.isServer) { + throw new Meteor.Error(500, '[ostrio:cookies] Can\'t use `.middleware()` on Client, it\'s Server only!'); + } + + return (req, res, next) => { + helpers.isFunction(this.handler) && this.handler(__middlewareHandler(req, res, this.opts)); + next(); + }; + } +} + +if (Meteor.isServer) { + Cookies.isLoadedOnServer = false; +} + +/* Export the Cookies class */ +export { Cookies }; diff --git a/apps/meteor/packages/meteor-cookies/package.js b/apps/meteor/packages/meteor-cookies/package.js new file mode 100644 index 000000000000..c7f19499f838 --- /dev/null +++ b/apps/meteor/packages/meteor-cookies/package.js @@ -0,0 +1,15 @@ +Package.describe({ + name: 'ostrio:cookies', + version: '2.7.2', + summary: 'Isomorphic bulletproof Server, Client, Browser, and Cordova cookies', + git: 'https://github.com/veliovgroup/Meteor-Cookies', + documentation: 'README.md', +}); + +Package.onUse((api) => { + api.use('ecmascript', ['client', 'server']); + api.use('webapp', 'server'); + api.use('fetch', 'client'); + api.mainModule('cookies.js', ['client', 'server']); +}); + From f57e21576d65940cb2ef37542a034d879aa45a71 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Wed, 10 Jan 2024 11:53:07 -0300 Subject: [PATCH 035/152] chore: internalize meteor inject initial (from meteor 3) (#31367) --- .../meteor-inject-initial/lib/inject-core.js | 50 ++++++ .../lib/inject-server.js | 156 ++++++++++++++++++ .../packages/meteor-inject-initial/package.js | 16 ++ 3 files changed, 222 insertions(+) create mode 100644 apps/meteor/packages/meteor-inject-initial/lib/inject-core.js create mode 100644 apps/meteor/packages/meteor-inject-initial/lib/inject-server.js create mode 100644 apps/meteor/packages/meteor-inject-initial/package.js diff --git a/apps/meteor/packages/meteor-inject-initial/lib/inject-core.js b/apps/meteor/packages/meteor-inject-initial/lib/inject-core.js new file mode 100644 index 000000000000..6f3a23a66c96 --- /dev/null +++ b/apps/meteor/packages/meteor-inject-initial/lib/inject-core.js @@ -0,0 +1,50 @@ +// Hijack core node API and attach data to the response dynamically +// We are simply using this hack because, there is no way to alter +// Meteor's html content on the server side + +Inject._hijackWrite = function (res) { + const originalWrite = res.write; + res.write = function (chunk, encoding) { + // prevent hijacking other http requests + if (!res.iInjected && encoding === undefined && /^/.test(chunk)) { + chunk = chunk.toString(); + + for (id in Inject.rawModHtmlFuncs) { + chunk = Inject.rawModHtmlFuncs[id](chunk, res); + if (!_.isString(chunk)) { + throw new Error(`Inject func id "${id}" must return HTML, not ${typeof chunk}\n${JSON.stringify(chunk, null, 2)}`); + } + } + + res.iInjected = true; + } + + originalWrite.call(res, chunk, encoding); + }; +}; + +WebApp.connectHandlers.use(function (req, res, next) { + // We only separate this to make testing easier + Inject._hijackWrite(res); + + next(); +}); + +// meteor algorithm to check if this is a meteor serving http request or not +Inject.appUrl = function (url) { + if (url === '/favicon.ico' || url === '/robots.txt') return false; + + // NOTE: app.manifest is not a web standard like favicon.ico and + // robots.txt. It is a file id we have chosen to use for HTML5 + // appcache URLs. It is included here to prevent using an appcache + // then removing it from poisoning an app permanently. Eventually, + // once we have server side routing, this won't be needed as + // unknown URLs with return a 404 automatically. + if (url === '/app.manifest') return false; + + // Avoid serving app HTML for declared routes such as /sockjs/. + if (typeof RoutePolicy !== 'undefined' && RoutePolicy.classify(url)) return false; + + // we currently return app HTML on all URLs by default + return true; +}; diff --git a/apps/meteor/packages/meteor-inject-initial/lib/inject-server.js b/apps/meteor/packages/meteor-inject-initial/lib/inject-server.js new file mode 100644 index 000000000000..bfc85c7fdaef --- /dev/null +++ b/apps/meteor/packages/meteor-inject-initial/lib/inject-server.js @@ -0,0 +1,156 @@ +function escapeReplaceString(str) { + /* + * When using string.replace(str, newSubStr), the dollar sign ("$") is + * considered a special character in newSubStr, and needs to be escaped + * as "$$". We have to do this twice, for escaping the newSubStr in + * this function, and for the resulting string which is passed back. + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace + */ + return str.replace(/\$/g, '$$$$'); +} + +Inject = { + // stores in a script type=application/ejson tag, accessed with Injected.obj('id') + obj(id, data, res) { + this._checkForObjOrFunction(data, 'Inject.obj(id, data [,res]) expects `data` to be an Object or Function'); + + if (res) { + this._resAssign(res, 'objList', id, data); + } else { + this.objList[id] = data; + } + }, + objList: {}, + + // Inserts a META called `id`, whose `content` can be accessed with Injected.meta() + meta(id, data, res) { + this._checkForTextOrFunction(data, 'Inject.meta(id, data [,res]) expects `data` to be an String or Function'); + + if (res) { + this._resAssign(res, 'metaList', id, data); + } else { + this.metaList[id] = data; + } + }, + metaList: {}, + + rawHead(id, textOrFunc, res) { + this._checkForTextOrFunction(textOrFunc, 'Inject.rawHead(id, content [,res]) expects `content` to be an String or Function'); + + if (res) { + this._resAssign(res, 'rawHeads', id, textOrFunc); + } else { + this.rawHeads[id] = textOrFunc; + } + }, + rawHeads: {}, + + rawBody(id, textOrFunc, res) { + this._checkForTextOrFunction(textOrFunc, 'Inject.rawBody(id, content [,res]) expects `content` to be an String or Function'); + + if (res) { + this._resAssign(res, 'rawBodies', id, textOrFunc); + } else { + this.rawBodies[id] = textOrFunc; + } + }, + rawBodies: {}, + + // The callback receives the entire HTML page and must return a modified version + rawModHtml(id, func) { + if (!_.isFunction(func)) { + const message = `Inject func id "${id}" should be a function, not ${typeof func}`; + throw new Error(message); + } + + this.rawModHtmlFuncs[id] = func; + }, + rawModHtmlFuncs: {}, + + _injectObjects(html, res) { + const objs = _.extend({}, Inject.objList, res.Inject && res.Inject.objList); + if (_.isEmpty(objs)) { + return html; + } + + let obj; + let injectHtml = ''; + for (id in objs) { + obj = _.isFunction(objs[id]) ? objs[id](res) : objs[id]; + injectHtml += ` \n`; + } + + return html.replace('', `\n${escapeReplaceString(injectHtml)}`); + }, + + _injectMeta(html, res) { + const metas = _.extend({}, Inject.metaList, res.Inject && res.Inject.metaList); + if (_.isEmpty(metas)) return html; + + let injectHtml = ''; + for (id in metas) { + const meta = this._evalToText(metas[id], res, html); + (injectHtml += ` \n`), res; + } + + return html.replace('', `\n${escapeReplaceString(injectHtml)}`); + }, + + _injectHeads(html, res) { + const heads = _.extend({}, Inject.rawHeads, res.Inject && res.Inject.rawHeads); + if (_.isEmpty(heads)) return html; + + let injectHtml = ''; + for (id in heads) { + const head = this._evalToText(heads[id], res, html); + injectHtml += `${head}\n`; + } + + return html.replace('', `\n${escapeReplaceString(injectHtml)}`); + }, + + _injectBodies(html, res) { + const bodies = _.extend({}, Inject.rawBodies, res.Inject && res.Inject.rawBodies); + if (_.isEmpty(bodies)) return html; + + let injectHtml = ''; + for (id in bodies) { + const body = this._evalToText(bodies[id], res, html); + injectHtml += `${body}\n`; + } + + return html.replace('', `\n${escapeReplaceString(injectHtml)}`); + }, + + // ensure object exists and store there + _resAssign(res, key, id, value) { + if (!res.Inject) res.Inject = {}; + if (!res.Inject[key]) res.Inject[key] = {}; + res.Inject[key][id] = value; + }, + + _checkForTextOrFunction(arg, message) { + if (!(_.isString(arg) || _.isFunction(arg))) { + throw new Error(message); + } + }, + + _checkForObjOrFunction(arg, message) { + if (!(_.isObject(arg) || _.isFunction(arg))) { + throw new Error(message); + } + }, + + // we don't handle errors here. Let them to handle in a higher level + _evalToText(textOrFunc, res, html) { + if (_.isFunction(textOrFunc)) { + return textOrFunc(res, html); + } + return textOrFunc; + }, +}; + +Inject.rawModHtml('injectHeads', Inject._injectHeads.bind(Inject)); +Inject.rawModHtml('injectMeta', Inject._injectMeta.bind(Inject)); +Inject.rawModHtml('injectBodies', Inject._injectBodies.bind(Inject)); +Inject.rawModHtml('injectObjects', Inject._injectObjects.bind(Inject)); diff --git a/apps/meteor/packages/meteor-inject-initial/package.js b/apps/meteor/packages/meteor-inject-initial/package.js new file mode 100644 index 000000000000..5ae6f530aa60 --- /dev/null +++ b/apps/meteor/packages/meteor-inject-initial/package.js @@ -0,0 +1,16 @@ +Package.describe({ + summary: 'Allow injection of arbitrary data to initial Meteor HTML page', + version: '1.0.5', + git: 'https://github.com/meteorhacks/meteor-inject-initial.git', + name: 'meteorhacks:inject-initial', +}); + +Package.onUse(function (api) { + api.use(['routepolicy', 'webapp'], 'server'); + api.use(['ejson', 'underscore'], ['server']); + + api.addFiles('lib/inject-server.js', 'server'); + api.addFiles('lib/inject-core.js', 'server'); + + api.export('Inject', 'server'); +}); From c93e1890e24b80e44554985e723c28a4c0e85ab2 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Wed, 10 Jan 2024 12:34:03 -0300 Subject: [PATCH 036/152] chore: internalize meteor run as user (from meteor 3) (#31370) --- .../lib/collection.overwrites.js | 54 +++++++++ .../packages/meteor-run-as-user/lib/common.js | 109 ++++++++++++++++++ .../meteor-run-as-user/lib/pre.1.0.3.js | 96 +++++++++++++++ .../packages/meteor-run-as-user/package.js | 29 +++++ 4 files changed, 288 insertions(+) create mode 100644 apps/meteor/packages/meteor-run-as-user/lib/collection.overwrites.js create mode 100644 apps/meteor/packages/meteor-run-as-user/lib/common.js create mode 100644 apps/meteor/packages/meteor-run-as-user/lib/pre.1.0.3.js create mode 100644 apps/meteor/packages/meteor-run-as-user/package.js diff --git a/apps/meteor/packages/meteor-run-as-user/lib/collection.overwrites.js b/apps/meteor/packages/meteor-run-as-user/lib/collection.overwrites.js new file mode 100644 index 000000000000..01603673d4c7 --- /dev/null +++ b/apps/meteor/packages/meteor-run-as-user/lib/collection.overwrites.js @@ -0,0 +1,54 @@ +// This file overwrites the default metoer Mongo.Collection modifiers: "insert", +// "update", "remove" +// +// The new methods are checking if Meteor is in "restricted" mode to apply +// allow and deny rules if needed. +// +// This will allow us to run the modifiers inside of a "Meteor.runAsUser" with +// security checks. +_.each(['insert', 'update', 'remove'], function (method) { + + var _super = Mongo.Collection.prototype[method]; + + Mongo.Collection.prototype[method] = function ( /* arguments */ ) { + var self = this; + var args = _.toArray(arguments); + + // Check if this method is run in restricted mode and collection is + // restricted. + if (Meteor.isRestricted() && self._restricted) { + + var generatedId = null; + if (method === 'insert' && !_.has(args[0], '_id')) { + generatedId = self._makeNewID(); + } + + // short circuit if there is no way it will pass. + if (self._validators[method].allow.length === 0) { + throw new Meteor.Error( + 403, 'Access denied. No allow validators set on restricted ' + + 'collection for method \'' + method + '\'.'); + } + + var validatedMethodName = + '_validated' + method.charAt(0).toUpperCase() + method.slice(1); + args.unshift(Meteor.userId()); + + if (method === 'insert') { + args.push(generatedId); + + self[validatedMethodName].apply(self, args); + // xxx: for now we return the id since self._validatedInsert doesn't + // yet return the new id + return generatedId || args[0]._id; + + } + + return self[validatedMethodName].apply(self, args); + + } + + return _super.apply(self, args); + }; + +}); diff --git a/apps/meteor/packages/meteor-run-as-user/lib/common.js b/apps/meteor/packages/meteor-run-as-user/lib/common.js new file mode 100644 index 000000000000..e12269db2630 --- /dev/null +++ b/apps/meteor/packages/meteor-run-as-user/lib/common.js @@ -0,0 +1,109 @@ +// This file adds the actual "Meteor.runAsUser" and "Meteor.isRestricted" api +// +// It's done by using a DDP method invocation, setting a user id and a +// "isRestricted" flag on it. +// +// If run inside of an existing DDP invocation a nested version will be created. + +var restrictedMode = new Meteor.EnvironmentVariable(); + +/** + * Returns true if inside a runAsUser user scope + * @return {Boolean} True if in a runAsUser user scope + */ +Meteor.isRestricted = function () { + return !!restrictedMode.get(); +}; + +/** + * Run code restricted + * @param {Function} f Code to run in restricted mode + * @return {Any} Result of code running + */ +Meteor.runRestricted = function(f) { + if (Meteor.isRestricted()) { + return f(); + } else { + return restrictedMode.withValue(true, f); + } +}; + +/** + * Run code unrestricted + * @param {Function} f Code to run in restricted mode + * @return {Any} Result of code running + */ +Meteor.runUnrestricted = function(f) { + if (Meteor.isRestricted()) { + return restrictedMode.withValue(false, f); + } else { + f(); + } +}; + +/** + * Run as a user + * @param {String} userId The id of user to run as + * @param {Function} f Function to run as user + * @return {Any} Returns function result + */ +Meteor.runAsUser = function (userId, f) { + var currentInvocation = DDP._CurrentInvocation.get(); + + // Create a new method invocation + var invocation = new DDPCommon.MethodInvocation( + (currentInvocation) ? currentInvocation : { + connection: null + } + ); + + // Now run as user on this invocation + invocation.setUserId(userId); + + return DDP._CurrentInvocation.withValue(invocation, function () { + return f.apply(invocation, [userId]); + }); +}; + +/** + * Run as restricted user + * @param {Function} f Function to run unrestricted + * @return {Any} Returns function result + */ +Meteor.runAsRestrictedUser = function(userId, f) { + return Meteor.runRestricted(function() { + return Meteor.runAsUser(userId, f); + }); +}; + +var adminMode = new Meteor.EnvironmentVariable(); + +/** + * Check if code is running isside an invocation / method + */ +Meteor.isAdmin = function() { + return !!adminMode.get(); +}; + +/** + * Make the function run outside invocation + */ +Meteor.runAsAdmin = function(f) { + if (Meteor.isAdmin()) { + return f(); + } else { + return adminMode.withValue(false, f); + } +}; + +/** + * Make sure code runs outside an invocation on the + * server + */ +Meteor.runOutsideInvocation = function(f) { + if (Meteor.isServer && DDP._CurrentInvocation.get()) { + DDP._CurrentInvocation.withValue(null, f); + } else { + f(); + } +}; diff --git a/apps/meteor/packages/meteor-run-as-user/lib/pre.1.0.3.js b/apps/meteor/packages/meteor-run-as-user/lib/pre.1.0.3.js new file mode 100644 index 000000000000..f707a1128b62 --- /dev/null +++ b/apps/meteor/packages/meteor-run-as-user/lib/pre.1.0.3.js @@ -0,0 +1,96 @@ +// This code will go away in later versions of Meteor, this is just a "polyfill" +// until the next release of Meteor maybe 1.0.3? +// +if (typeof DDPCommon === 'undefined') { + DDPCommon = {}; + + DDPCommon.MethodInvocation = function (options) { + var self = this; + + // true if we're running not the actual method, but a stub (that is, + // if we're on a client (which may be a browser, or in the future a + // server connecting to another server) and presently running a + // simulation of a server-side method for latency compensation + // purposes). not currently true except in a client such as a browser, + // since there's usually no point in running stubs unless you have a + // zero-latency connection to the user. + + /** + * @summary Access inside a method invocation. Boolean value, true if this invocation is a stub. + * @locus Anywhere + * @name isSimulation + * @memberOf MethodInvocation + * @instance + * @type {Boolean} + */ + this.isSimulation = options.isSimulation; + + // call this function to allow other method invocations (from the + // same client) to continue running without waiting for this one to + // complete. + this._unblock = options.unblock || function () {}; + this._calledUnblock = false; + + // current user id + + /** + * @summary The id of the user that made this method call, or `null` if no user was logged in. + * @locus Anywhere + * @name userId + * @memberOf MethodInvocation + * @instance + */ + this.userId = options.userId; + + // sets current user id in all appropriate server contexts and + // reruns subscriptions + this._setUserId = options.setUserId || function () {}; + + // On the server, the connection this method call came in on. + + /** + * @summary Access inside a method invocation. The [connection](#meteor_onconnection) that this method was received on. `null` if the method is not associated with a connection, eg. a server initiated method call. + * @locus Server + * @name connection + * @memberOf MethodInvocation + * @instance + */ + this.connection = options.connection; + + // The seed for randomStream value generation + this.randomSeed = options.randomSeed; + + // This is set by RandomStream.get; and holds the random stream state + this.randomStream = null; + }; + + _.extend(DDPCommon.MethodInvocation.prototype, { + /** + * @summary Call inside a method invocation. Allow subsequent method from this client to begin running in a new fiber. + * @locus Server + * @memberOf MethodInvocation + * @instance + */ + unblock: function () { + var self = this; + self._calledUnblock = true; + self._unblock(); + }, + + /** + * @summary Set the logged in user. + * @locus Server + * @memberOf MethodInvocation + * @instance + * @param {String | null} userId The value that should be returned by `userId` on this connection. + */ + setUserId: function (userId) { + var self = this; + if (self._calledUnblock) + throw new Error("Can't call setUserId in a method after calling unblock"); + self.userId = userId; + // self._setUserId(userId); + } + + }); +} diff --git a/apps/meteor/packages/meteor-run-as-user/package.js b/apps/meteor/packages/meteor-run-as-user/package.js new file mode 100644 index 000000000000..a9958fa76ec8 --- /dev/null +++ b/apps/meteor/packages/meteor-run-as-user/package.js @@ -0,0 +1,29 @@ +Package.describe({ + name: 'dispatch:run-as-user', + version: '1.1.1', + summary: 'Adds Meteor.runAsUser(user, f) and Meteor.isRestricted()', + git: 'https://github.com/DispatchMe/Meteor-run-as-user.git', + documentation: 'README.md', +}); + +Package.onUse(function (api) { + + api.use([ + 'meteor', + 'check', + 'underscore', + 'mongo', + 'ddp', + // 'ddp-common', + // 'ddp-client' + ]); + + api.addFiles( + [ + 'lib/pre.1.0.3.js', // Waiting for ddp-common and ddp-client + 'lib/common.js', + 'lib/collection.overwrites.js', + ], + ['client', 'server'], + ); +}); From 5845014425e17f1958152754ca21e1a300f4cfd3 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Wed, 10 Jan 2024 13:17:32 -0300 Subject: [PATCH 037/152] chore: internalize flow-router (from meteor 3 branch) (#31365) --- apps/meteor/packages/flow-router/.gitignore | 3 + .../flow-router/.npm/package/.gitignore | 1 + .../packages/flow-router/.npm/package/README | 7 + .../.npm/package/npm-shrinkwrap.json | 28 + .../packages/flow-router/client/_init.js | 11 + .../packages/flow-router/client/group.js | 57 ++ .../packages/flow-router/client/modules.js | 2 + .../packages/flow-router/client/route.js | 125 ++++ .../packages/flow-router/client/router.js | 587 ++++++++++++++++++ .../packages/flow-router/client/triggers.js | 112 ++++ .../meteor/packages/flow-router/lib/router.js | 9 + apps/meteor/packages/flow-router/package.js | 41 ++ .../packages/flow-router/server/_init.js | 4 + .../packages/flow-router/server/group.js | 18 + .../flow-router/server/plugins/fast_render.js | 40 ++ .../packages/flow-router/server/route.js | 28 + .../packages/flow-router/server/router.js | 146 +++++ 17 files changed, 1219 insertions(+) create mode 100644 apps/meteor/packages/flow-router/.gitignore create mode 100644 apps/meteor/packages/flow-router/.npm/package/.gitignore create mode 100644 apps/meteor/packages/flow-router/.npm/package/README create mode 100644 apps/meteor/packages/flow-router/.npm/package/npm-shrinkwrap.json create mode 100644 apps/meteor/packages/flow-router/client/_init.js create mode 100644 apps/meteor/packages/flow-router/client/group.js create mode 100644 apps/meteor/packages/flow-router/client/modules.js create mode 100644 apps/meteor/packages/flow-router/client/route.js create mode 100644 apps/meteor/packages/flow-router/client/router.js create mode 100644 apps/meteor/packages/flow-router/client/triggers.js create mode 100644 apps/meteor/packages/flow-router/lib/router.js create mode 100644 apps/meteor/packages/flow-router/package.js create mode 100644 apps/meteor/packages/flow-router/server/_init.js create mode 100644 apps/meteor/packages/flow-router/server/group.js create mode 100644 apps/meteor/packages/flow-router/server/plugins/fast_render.js create mode 100644 apps/meteor/packages/flow-router/server/route.js create mode 100644 apps/meteor/packages/flow-router/server/router.js diff --git a/apps/meteor/packages/flow-router/.gitignore b/apps/meteor/packages/flow-router/.gitignore new file mode 100644 index 000000000000..22ee0ccee14f --- /dev/null +++ b/apps/meteor/packages/flow-router/.gitignore @@ -0,0 +1,3 @@ +.build* +*.browserify.js.cached +*.browserify.js.map diff --git a/apps/meteor/packages/flow-router/.npm/package/.gitignore b/apps/meteor/packages/flow-router/.npm/package/.gitignore new file mode 100644 index 000000000000..3c3629e647f5 --- /dev/null +++ b/apps/meteor/packages/flow-router/.npm/package/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/apps/meteor/packages/flow-router/.npm/package/README b/apps/meteor/packages/flow-router/.npm/package/README new file mode 100644 index 000000000000..3d492553a438 --- /dev/null +++ b/apps/meteor/packages/flow-router/.npm/package/README @@ -0,0 +1,7 @@ +This directory and the files immediately inside it are automatically generated +when you change this package's NPM dependencies. Commit the files in this +directory (npm-shrinkwrap.json, .gitignore, and this README) to source control +so that others run the same versions of sub-dependencies. + +You should NOT check in the node_modules directory that Meteor automatically +creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/apps/meteor/packages/flow-router/.npm/package/npm-shrinkwrap.json b/apps/meteor/packages/flow-router/.npm/package/npm-shrinkwrap.json new file mode 100644 index 000000000000..47445b724946 --- /dev/null +++ b/apps/meteor/packages/flow-router/.npm/package/npm-shrinkwrap.json @@ -0,0 +1,28 @@ +{ + "lockfileVersion": 1, + "dependencies": { + "page": { + "version": "https://github.com/kadirahq/page.js/archive/34ddf45ea8e4c37269ce3df456b44fc0efc595c6.tar.gz", + "integrity": "sha512-BGG5XDCSYGI4C/pGoYdGIIU1YpEYdCYN2HxwuKpYLgIDdT+tlqRs4IOQTJLTRnXKbt9r07Cucj0Y6fTGZcYizw==", + "dependencies": { + "path-to-regexp": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.2.1.tgz", + "integrity": "sha512-DBw9IhWfevR2zCVwEZURTuQNseCvu/Q9f5ZgqMCK0Rh61bDa4uyjPAOy9b55yKiPT59zZn+7uYKxmWwsguInwg==", + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + } + } + } + } + }, + "qs": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-5.2.0.tgz", + "integrity": "sha512-VH4FeG98gs6AkHivaW2O14vsOPBL9E80Sj7fITunoDijiYQ1lsVwJYmm1CSL+oLyO2N5HPdo23GXAG64uKOAZQ==" + } + } +} \ No newline at end of file diff --git a/apps/meteor/packages/flow-router/client/_init.js b/apps/meteor/packages/flow-router/client/_init.js new file mode 100644 index 000000000000..a18fdc897bac --- /dev/null +++ b/apps/meteor/packages/flow-router/client/_init.js @@ -0,0 +1,11 @@ +// Export Router Instance +FlowRouter = new Router(); +FlowRouter.Router = Router; +FlowRouter.Route = Route; + +// Initialize FlowRouter +Meteor.startup(function () { + if(!FlowRouter._askedToWait) { + FlowRouter.initialize(); + } +}); diff --git a/apps/meteor/packages/flow-router/client/group.js b/apps/meteor/packages/flow-router/client/group.js new file mode 100644 index 000000000000..b93296bc2ada --- /dev/null +++ b/apps/meteor/packages/flow-router/client/group.js @@ -0,0 +1,57 @@ +Group = function(router, options, parent) { + options = options || {}; + + if (options.prefix && !/^\/.*/.test(options.prefix)) { + var message = "group's prefix must start with '/'"; + throw new Error(message); + } + + this._router = router; + this.prefix = options.prefix || ''; + this.name = options.name; + this.options = options; + + this._triggersEnter = options.triggersEnter || []; + this._triggersExit = options.triggersExit || []; + this._subscriptions = options.subscriptions || Function.prototype; + + this.parent = parent; + if (this.parent) { + this.prefix = parent.prefix + this.prefix; + + this._triggersEnter = parent._triggersEnter.concat(this._triggersEnter); + this._triggersExit = this._triggersExit.concat(parent._triggersExit); + } +}; + +Group.prototype.route = function(pathDef, options, group) { + options = options || {}; + + if (!/^\/.*/.test(pathDef)) { + var message = "route's path must start with '/'"; + throw new Error(message); + } + + group = group || this; + pathDef = this.prefix + pathDef; + + var triggersEnter = options.triggersEnter || []; + options.triggersEnter = this._triggersEnter.concat(triggersEnter); + + var triggersExit = options.triggersExit || []; + options.triggersExit = triggersExit.concat(this._triggersExit); + + return this._router.route(pathDef, options, group); +}; + +Group.prototype.group = function(options) { + return new Group(this._router, options, this); +}; + +Group.prototype.callSubscriptions = function(current) { + if (this.parent) { + this.parent.callSubscriptions(current); + } + + this._subscriptions.call(current.route, current.params, current.queryParams); +}; diff --git a/apps/meteor/packages/flow-router/client/modules.js b/apps/meteor/packages/flow-router/client/modules.js new file mode 100644 index 000000000000..7b734f449b30 --- /dev/null +++ b/apps/meteor/packages/flow-router/client/modules.js @@ -0,0 +1,2 @@ +page = require('page'); +qs = require('qs'); diff --git a/apps/meteor/packages/flow-router/client/route.js b/apps/meteor/packages/flow-router/client/route.js new file mode 100644 index 000000000000..b82e9721380f --- /dev/null +++ b/apps/meteor/packages/flow-router/client/route.js @@ -0,0 +1,125 @@ +Route = function(router, pathDef, options, group) { + options = options || {}; + + this.options = options; + this.pathDef = pathDef + + // Route.path is deprecated and will be removed in 3.0 + this.path = pathDef; + + if (options.name) { + this.name = options.name; + } + + this._action = options.action || Function.prototype; + this._subscriptions = options.subscriptions || Function.prototype; + this._triggersEnter = options.triggersEnter || []; + this._triggersExit = options.triggersExit || []; + this._subsMap = {}; + this._router = router; + + this._params = new ReactiveDict(); + this._queryParams = new ReactiveDict(); + this._routeCloseDep = new Tracker.Dependency(); + + // tracks the changes in the URL + this._pathChangeDep = new Tracker.Dependency(); + + this.group = group; +}; + +Route.prototype.clearSubscriptions = function() { + this._subsMap = {}; +}; + +Route.prototype.register = function(name, sub, options) { + this._subsMap[name] = sub; +}; + + +Route.prototype.getSubscription = function(name) { + return this._subsMap[name]; +}; + + +Route.prototype.getAllSubscriptions = function() { + return this._subsMap; +}; + +Route.prototype.callAction = function(current) { + var self = this; + self._action(current.params, current.queryParams); +}; + +Route.prototype.callSubscriptions = function(current) { + this.clearSubscriptions(); + if (this.group) { + this.group.callSubscriptions(current); + } + + this._subscriptions(current.params, current.queryParams); +}; + +Route.prototype.getRouteName = function() { + this._routeCloseDep.depend(); + return this.name; +}; + +Route.prototype.getParam = function(key) { + this._routeCloseDep.depend(); + return this._params.get(key); +}; + +Route.prototype.getQueryParam = function(key) { + this._routeCloseDep.depend(); + return this._queryParams.get(key); +}; + +Route.prototype.watchPathChange = function() { + this._pathChangeDep.depend(); +}; + +Route.prototype.registerRouteClose = function() { + this._params = new ReactiveDict(); + this._queryParams = new ReactiveDict(); + this._routeCloseDep.changed(); + this._pathChangeDep.changed(); +}; + +Route.prototype.registerRouteChange = function(currentContext, routeChanging) { + // register params + var params = currentContext.params; + this._updateReactiveDict(this._params, params); + + // register query params + var queryParams = currentContext.queryParams; + this._updateReactiveDict(this._queryParams, queryParams); + + // if the route is changing, we need to defer triggering path changing + // if we did this, old route's path watchers will detect this + // Real issue is, above watcher will get removed with the new route + // So, we don't need to trigger it now + // We are doing it on the route close event. So, if they exists they'll + // get notify that + if(!routeChanging) { + this._pathChangeDep.changed(); + } +}; + +Route.prototype._updateReactiveDict = function(dict, newValues) { + var currentKeys = _.keys(newValues); + var oldKeys = _.keys(dict.keyDeps); + + // set new values + // params is an array. So, _.each(params) does not works + // to iterate params + _.each(currentKeys, function(key) { + dict.set(key, newValues[key]); + }); + + // remove keys which does not exisits here + var removedKeys = _.difference(oldKeys, currentKeys); + _.each(removedKeys, function(key) { + dict.set(key, undefined); + }); +}; diff --git a/apps/meteor/packages/flow-router/client/router.js b/apps/meteor/packages/flow-router/client/router.js new file mode 100644 index 000000000000..ae91751f2a72 --- /dev/null +++ b/apps/meteor/packages/flow-router/client/router.js @@ -0,0 +1,587 @@ +Router = function () { + var self = this; + this.globals = []; + this.subscriptions = Function.prototype; + + this._tracker = this._buildTracker(); + this._current = {}; + + // tracks the current path change + this._onEveryPath = new Tracker.Dependency(); + + this._globalRoute = new Route(this); + + // holds onRoute callbacks + this._onRouteCallbacks = []; + + // if _askedToWait is true. We don't automatically start the router + // in Meteor.startup callback. (see client/_init.js) + // Instead user need to call `.initialize() + this._askedToWait = false; + this._initialized = false; + this._triggersEnter = []; + this._triggersExit = []; + this._routes = []; + this._routesMap = {}; + this._updateCallbacks(); + this.notFound = this.notfound = null; + // indicate it's okay (or not okay) to run the tracker + // when doing subscriptions + // using a number and increment it help us to support FlowRouter.go() + // and legitimate reruns inside tracker on the same event loop. + // this is a solution for #145 + this.safeToRun = 0; + + // Meteor exposes to the client the path prefix that was defined using the + // ROOT_URL environement variable on the server using the global runtime + // configuration. See #315. + this._basePath = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ''; + + // this is a chain contains a list of old routes + // most of the time, there is only one old route + // but when it's the time for a trigger redirect we've a chain + this._oldRouteChain = []; + + this.env = { + replaceState: new Meteor.EnvironmentVariable(), + reload: new Meteor.EnvironmentVariable(), + trailingSlash: new Meteor.EnvironmentVariable() + }; + + // redirect function used inside triggers + this._redirectFn = function(pathDef, fields, queryParams) { + if (/^http(s)?:\/\//.test(pathDef)) { + var message = "Redirects to URLs outside of the app are not supported in this version of Flow Router. Use 'window.location = yourUrl' instead"; + throw new Error(message); + } + self.withReplaceState(function() { + var path = FlowRouter.path(pathDef, fields, queryParams); + self._page.redirect(path); + }); + }; + this._initTriggersAPI(); +}; + +Router.prototype.route = function(pathDef, options, group) { + if (!/^\/.*/.test(pathDef)) { + var message = "route's path must start with '/'"; + throw new Error(message); + } + + options = options || {}; + var self = this; + var route = new Route(this, pathDef, options, group); + + // calls when the page route being activates + route._actionHandle = function (context, next) { + var oldRoute = self._current.route; + self._oldRouteChain.push(oldRoute); + + var queryParams = self._qs.parse(context.querystring); + // _qs.parse() gives us a object without prototypes, + // created with Object.create(null) + // Meteor's check doesn't play nice with it. + // So, we need to fix it by cloning it. + // see more: https://github.com/meteorhacks/flow-router/issues/164 + queryParams = JSON.parse(JSON.stringify(queryParams)); + + self._current = { + path: context.path, + context: context, + params: context.params, + queryParams: queryParams, + route: route, + oldRoute: oldRoute + }; + + // we need to invalidate if all the triggers have been completed + // if not that means, we've been redirected to another path + // then we don't need to invalidate + var afterAllTriggersRan = function() { + self._invalidateTracker(); + }; + + var triggers = self._triggersEnter.concat(route._triggersEnter); + Triggers.runTriggers( + triggers, + self._current, + self._redirectFn, + afterAllTriggersRan + ); + }; + + // calls when you exit from the page js route + route._exitHandle = function(context, next) { + var triggers = self._triggersExit.concat(route._triggersExit); + Triggers.runTriggers( + triggers, + self._current, + self._redirectFn, + next + ); + }; + + this._routes.push(route); + if (options.name) { + this._routesMap[options.name] = route; + } + + this._updateCallbacks(); + this._triggerRouteRegister(route); + + return route; +}; + +Router.prototype.group = function(options) { + return new Group(this, options); +}; + +Router.prototype.path = function(pathDef, fields, queryParams) { + if (this._routesMap[pathDef]) { + pathDef = this._routesMap[pathDef].pathDef; + } + + var path = ""; + + // Prefix the path with the router global prefix + if (this._basePath) { + path += "/" + this._basePath + "/"; + } + + fields = fields || {}; + var regExp = /(:[\w\(\)\\\+\*\.\?]+)+/g; + path += pathDef.replace(regExp, function(key) { + var firstRegexpChar = key.indexOf("("); + // get the content behind : and (\\d+/) + key = key.substring(1, (firstRegexpChar > 0)? firstRegexpChar: undefined); + // remove +?* + key = key.replace(/[\+\*\?]+/g, ""); + + // this is to allow page js to keep the custom characters as it is + // we need to encode 2 times otherwise "/" char does not work properly + // So, in that case, when I includes "/" it will think it's a part of the + // route. encoding 2times fixes it + return encodeURIComponent(encodeURIComponent(fields[key] || "")); + }); + + // Replace multiple slashes with single slash + path = path.replace(/\/\/+/g, "/"); + + // remove trailing slash + // but keep the root slash if it's the only one + path = path.match(/^\/{1}$/) ? path: path.replace(/\/$/, ""); + + // explictly asked to add a trailing slash + if(this.env.trailingSlash.get() && _.last(path) !== "/") { + path += "/"; + } + + var strQueryParams = this._qs.stringify(queryParams || {}); + if(strQueryParams) { + path += "?" + strQueryParams; + } + + return path; +}; + +Router.prototype.go = function(pathDef, fields, queryParams) { + var path = this.path(pathDef, fields, queryParams); + + var useReplaceState = this.env.replaceState.get(); + if(useReplaceState) { + this._page.replace(path); + } else { + this._page(path); + } +}; + +Router.prototype.reload = function() { + var self = this; + + self.env.reload.withValue(true, function() { + self._page.replace(self._current.path); + }); +}; + +Router.prototype.redirect = function(path) { + this._page.redirect(path); +}; + +Router.prototype.setParams = function(newParams) { + if(!this._current.route) {return false;} + + var pathDef = this._current.route.pathDef; + var existingParams = this._current.params; + var params = {}; + _.each(_.keys(existingParams), function(key) { + params[key] = existingParams[key]; + }); + + params = _.extend(params, newParams); + var queryParams = this._current.queryParams; + + this.go(pathDef, params, queryParams); + return true; +}; + +Router.prototype.setQueryParams = function(newParams) { + if(!this._current.route) {return false;} + + var queryParams = _.clone(this._current.queryParams); + _.extend(queryParams, newParams); + + for (var k in queryParams) { + if (queryParams[k] === null || queryParams[k] === undefined) { + delete queryParams[k]; + } + } + + var pathDef = this._current.route.pathDef; + var params = this._current.params; + this.go(pathDef, params, queryParams); + return true; +}; + +// .current is not reactive +// This is by design. use .getParam() instead +// If you really need to watch the path change, use .watchPathChange() +Router.prototype.current = function() { + // We can't trust outside, that's why we clone this + // Anyway, we can't clone the whole object since it has non-jsonable values + // That's why we clone what's really needed. + var current = _.clone(this._current); + current.queryParams = EJSON.clone(current.queryParams); + current.params = EJSON.clone(current.params); + return current; +}; + +// Implementing Reactive APIs +var reactiveApis = [ + 'getParam', 'getQueryParam', + 'getRouteName', 'watchPathChange' +]; +reactiveApis.forEach(function(api) { + Router.prototype[api] = function(arg1) { + // when this is calling, there may not be any route initiated + // so we need to handle it + var currentRoute = this._current.route; + if(!currentRoute) { + this._onEveryPath.depend(); + return; + } + + // currently, there is only one argument. If we've more let's add more args + // this is not clean code, but better in performance + return currentRoute[api].call(currentRoute, arg1); + }; +}); + +Router.prototype.subsReady = function() { + var callback = null; + var args = _.toArray(arguments); + + if (typeof _.last(args) === "function") { + callback = args.pop(); + } + + var currentRoute = this.current().route; + var globalRoute = this._globalRoute; + + // we need to depend for every route change and + // rerun subscriptions to check the ready state + this._onEveryPath.depend(); + + if(!currentRoute) { + return false; + } + + var subscriptions; + if(args.length === 0) { + subscriptions = _.values(globalRoute.getAllSubscriptions()); + subscriptions = subscriptions.concat(_.values(currentRoute.getAllSubscriptions())); + } else { + subscriptions = _.map(args, function(subName) { + return globalRoute.getSubscription(subName) || currentRoute.getSubscription(subName); + }); + } + + var isReady = function() { + var ready = _.every(subscriptions, function(sub) { + return sub && sub.ready(); + }); + + return ready; + }; + + if (callback) { + Tracker.autorun(function(c) { + if (isReady()) { + callback(); + c.stop(); + } + }); + } else { + return isReady(); + } +}; + +Router.prototype.withReplaceState = function(fn) { + return this.env.replaceState.withValue(true, fn); +}; + +Router.prototype.withTrailingSlash = function(fn) { + return this.env.trailingSlash.withValue(true, fn); +}; + +Router.prototype._notfoundRoute = function(context) { + this._current = { + path: context.path, + context: context, + params: [], + queryParams: {}, + }; + + // XXX this.notfound kept for backwards compatibility + this.notFound = this.notFound || this.notfound; + if(!this.notFound) { + console.error("There is no route for the path:", context.path); + return; + } + + this._current.route = new Route(this, "*", this.notFound); + this._invalidateTracker(); +}; + +Router.prototype.initialize = function(options) { + options = options || {}; + + if(this._initialized) { + throw new Error("FlowRouter is already initialized"); + } + + var self = this; + this._updateCallbacks(); + + // Implementing idempotent routing + // by overriding page.js`s "show" method. + // Why? + // It is impossible to bypass exit triggers, + // because they execute before the handler and + // can not know what the next path is, inside exit trigger. + // + // we need override both show, replace to make this work + // since we use redirect when we are talking about withReplaceState + _.each(['show', 'replace'], function(fnName) { + var original = self._page[fnName]; + self._page[fnName] = function(path, state, dispatch, push) { + var reload = self.env.reload.get(); + if (!reload && self._current.path === path) { + return; + } + + original.call(this, path, state, dispatch, push); + }; + }); + + // this is very ugly part of pagejs and it does decoding few times + // in unpredicatable manner. See #168 + // this is the default behaviour and we need keep it like that + // we are doing a hack. see .path() + this._page.base(this._basePath); + this._page({ + decodeURLComponents: true, + hashbang: !!options.hashbang + }); + + this._initialized = true; +}; + +Router.prototype._buildTracker = function() { + var self = this; + + // main autorun function + var tracker = Tracker.autorun(function () { + if(!self._current || !self._current.route) { + return; + } + + // see the definition of `this._processingContexts` + var currentContext = self._current; + var route = currentContext.route; + var path = currentContext.path; + + if(self.safeToRun === 0) { + var message = + "You can't use reactive data sources like Session" + + " inside the `.subscriptions` method!"; + throw new Error(message); + } + + // We need to run subscriptions inside a Tracker + // to stop subs when switching between routes + // But we don't need to run this tracker with + // other reactive changes inside the .subscription method + // We tackle this with the `safeToRun` variable + self._globalRoute.clearSubscriptions(); + self.subscriptions.call(self._globalRoute, path); + route.callSubscriptions(currentContext); + + // otherwise, computations inside action will trigger to re-run + // this computation. which we do not need. + Tracker.nonreactive(function() { + var isRouteChange = currentContext.oldRoute !== currentContext.route; + var isFirstRoute = !currentContext.oldRoute; + // first route is not a route change + if(isFirstRoute) { + isRouteChange = false; + } + + // Clear oldRouteChain just before calling the action + // We still need to get a copy of the oldestRoute first + // It's very important to get the oldest route and registerRouteClose() it + // See: https://github.com/kadirahq/flow-router/issues/314 + var oldestRoute = self._oldRouteChain[0]; + self._oldRouteChain = []; + + currentContext.route.registerRouteChange(currentContext, isRouteChange); + route.callAction(currentContext); + + Tracker.afterFlush(function() { + self._onEveryPath.changed(); + if(isRouteChange) { + // We need to trigger that route (definition itself) has changed. + // So, we need to re-run all the register callbacks to current route + // This is pretty important, otherwise tracker + // can't identify new route's items + + // We also need to afterFlush, otherwise this will re-run + // helpers on templates which are marked for destroying + if(oldestRoute) { + oldestRoute.registerRouteClose(); + } + } + }); + }); + + self.safeToRun--; + }); + + return tracker; +}; + +Router.prototype._invalidateTracker = function() { + var self = this; + this.safeToRun++; + this._tracker.invalidate(); + // After the invalidation we need to flush to make changes imediately + // otherwise, we have face some issues context mix-maches and so on. + // But there are some cases we can't flush. So we need to ready for that. + + // we clearly know, we can't flush inside an autorun + // this may leads some issues on flow-routing + // we may need to do some warning + if(!Tracker.currentComputation) { + // Still there are some cases where we can't flush + // eg:- when there is a flush currently + // But we've no public API or hacks to get that state + // So, this is the only solution + try { + Tracker.flush(); + } catch(ex) { + // only handling "while flushing" errors + if(!/Tracker\.flush while flushing/.test(ex.message)) { + return; + } + + // XXX: fix this with a proper solution by removing subscription mgt. + // from the router. Then we don't need to run invalidate using a tracker + + // this happens when we are trying to invoke a route change + // with inside a route chnage. (eg:- Template.onCreated) + // Since we use page.js and tracker, we don't have much control + // over this process. + // only solution is to defer route execution. + + // It's possible to have more than one path want to defer + // But, we only need to pick the last one. + // self._nextPath = self._current.path; + Meteor.defer(function() { + var path = self._nextPath; + if(!path) { + return; + } + + delete self._nextPath; + self.env.reload.withValue(true, function() { + self.go(path); + }); + }); + } + } +}; + +Router.prototype._updateCallbacks = function () { + var self = this; + + self._page.callbacks = []; + self._page.exits = []; + + _.each(self._routes, function(route) { + self._page(route.pathDef, route._actionHandle); + self._page.exit(route.pathDef, route._exitHandle); + }); + + self._page("*", function(context) { + self._notfoundRoute(context); + }); +}; + +Router.prototype._initTriggersAPI = function() { + var self = this; + this.triggers = { + enter: function(triggers, filter) { + triggers = Triggers.applyFilters(triggers, filter); + if(triggers.length) { + self._triggersEnter = self._triggersEnter.concat(triggers); + } + }, + + exit: function(triggers, filter) { + triggers = Triggers.applyFilters(triggers, filter); + if(triggers.length) { + self._triggersExit = self._triggersExit.concat(triggers); + } + } + }; +}; + +Router.prototype.wait = function() { + if(this._initialized) { + throw new Error("can't wait after FlowRouter has been initialized"); + } + + this._askedToWait = true; +}; + +Router.prototype.onRouteRegister = function(cb) { + this._onRouteCallbacks.push(cb); +}; + +Router.prototype._triggerRouteRegister = function(currentRoute) { + // We should only need to send a safe set of fields on the route + // object. + // This is not to hide what's inside the route object, but to show + // these are the public APIs + var routePublicApi = _.pick(currentRoute, 'name', 'pathDef', 'path'); + var omittingOptionFields = [ + 'triggersEnter', 'triggersExit', 'action', 'subscriptions', 'name' + ]; + routePublicApi.options = _.omit(currentRoute.options, omittingOptionFields); + + _.each(this._onRouteCallbacks, function(cb) { + cb(routePublicApi); + }); +}; + +Router.prototype._page = page; +Router.prototype._qs = qs; diff --git a/apps/meteor/packages/flow-router/client/triggers.js b/apps/meteor/packages/flow-router/client/triggers.js new file mode 100644 index 000000000000..7733332ca513 --- /dev/null +++ b/apps/meteor/packages/flow-router/client/triggers.js @@ -0,0 +1,112 @@ +// a set of utility functions for triggers + +Triggers = {}; + +// Apply filters for a set of triggers +// @triggers - a set of triggers +// @filter - filter with array fileds with `only` and `except` +// support only either `only` or `except`, but not both +Triggers.applyFilters = function(triggers, filter) { + if(!(triggers instanceof Array)) { + triggers = [triggers]; + } + + if(!filter) { + return triggers; + } + + if(filter.only && filter.except) { + throw new Error("Triggers don't support only and except filters at once"); + } + + if(filter.only && !(filter.only instanceof Array)) { + throw new Error("only filters needs to be an array"); + } + + if(filter.except && !(filter.except instanceof Array)) { + throw new Error("except filters needs to be an array"); + } + + if(filter.only) { + return Triggers.createRouteBoundTriggers(triggers, filter.only); + } + + if(filter.except) { + return Triggers.createRouteBoundTriggers(triggers, filter.except, true); + } + + throw new Error("Provided a filter but not supported"); +}; + +// create triggers by bounding them to a set of route names +// @triggers - a set of triggers +// @names - list of route names to be bound (trigger runs only for these names) +// @negate - negate the result (triggers won't run for above names) +Triggers.createRouteBoundTriggers = function(triggers, names, negate) { + var namesMap = {}; + _.each(names, function(name) { + namesMap[name] = true; + }); + + var filteredTriggers = _.map(triggers, function(originalTrigger) { + var modifiedTrigger = function(context, next) { + var routeName = context.route.name; + var matched = (namesMap[routeName])? 1: -1; + matched = (negate)? matched * -1 : matched; + + if(matched === 1) { + originalTrigger(context, next); + } + }; + return modifiedTrigger; + }); + + return filteredTriggers; +}; + +// run triggers and abort if redirected or callback stopped +// @triggers - a set of triggers +// @context - context we need to pass (it must have the route) +// @redirectFn - function which used to redirect +// @after - called after if only all the triggers runs +Triggers.runTriggers = function(triggers, context, redirectFn, after) { + var abort = false; + var inCurrentLoop = true; + var alreadyRedirected = false; + + for(var lc=0; lc 0)? firstRegexpChar: undefined); + // remove +?* + key = key.replace(/[\+\*\?]+/g, ""); + + return fields[key] || ""; + }); + + path = path.replace(/\/\/+/g, "/"); // Replace multiple slashes with single slash + + // remove trailing slash + // but keep the root slash if it's the only one + path = path.match(/^\/{1}$/) ? path: path.replace(/\/$/, ""); + + var strQueryParams = Qs.stringify(queryParams || {}); + if(strQueryParams) { + path += "?" + strQueryParams; + } + + return path; +}; + +Router.prototype.onRouteRegister = function(cb) { + this._onRouteCallbacks.push(cb); +}; + +Router.prototype._triggerRouteRegister = function(currentRoute) { + // We should only need to send a safe set of fields on the route + // object. + // This is not to hide what's inside the route object, but to show + // these are the public APIs + var routePublicApi = _.pick(currentRoute, 'name', 'pathDef', 'path'); + var omittingOptionFields = [ + 'triggersEnter', 'triggersExit', 'action', 'subscriptions', 'name' + ]; + routePublicApi.options = _.omit(currentRoute.options, omittingOptionFields); + + _.each(this._onRouteCallbacks, function(cb) { + cb(routePublicApi); + }); +}; + + +Router.prototype.go = function() { + // client only +}; + + +Router.prototype.current = function() { + // client only +}; + + +Router.prototype.triggers = { + enter: function() { + // client only + }, + exit: function() { + // client only + } +}; + +Router.prototype.middleware = function() { + // client only +}; + + +Router.prototype.getState = function() { + // client only +}; + + +Router.prototype.getAllStates = function() { + // client only +}; + + +Router.prototype.setState = function() { + // client only +}; + + +Router.prototype.removeState = function() { + // client only +}; + + +Router.prototype.clearStates = function() { + // client only +}; + + +Router.prototype.ready = function() { + // client only +}; + + +Router.prototype.initialize = function() { + // client only +}; + +Router.prototype.wait = function() { + // client only +}; From 8c69edd01f402f786cfefbde1fc77e7c5592a9b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Wed, 10 Jan 2024 14:02:04 -0300 Subject: [PATCH 038/152] feat: add `ImageGallery` zoom controls (#31369) --- .changeset/funny-buses-own.md | 5 ++ .../components/ImageGallery/ImageGallery.tsx | 57 ++++++++++++++----- 2 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 .changeset/funny-buses-own.md diff --git a/.changeset/funny-buses-own.md b/.changeset/funny-buses-own.md new file mode 100644 index 000000000000..faa0159807cf --- /dev/null +++ b/.changeset/funny-buses-own.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +feat: add `ImageGallery` zoom controls diff --git a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx index ebce7d42e541..676418731e51 100644 --- a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx +++ b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx @@ -1,5 +1,5 @@ import { css } from '@rocket.chat/css-in-js'; -import { Box, IconButton, Palette, Throbber } from '@rocket.chat/fuselage'; +import { Box, ButtonGroup, IconButton, Palette, Throbber } from '@rocket.chat/fuselage'; import React, { useRef, useState } from 'react'; import { FocusScope } from 'react-aria'; import { createPortal } from 'react-dom'; @@ -13,6 +13,7 @@ import 'swiper/modules/navigation/navigation.min.css'; import 'swiper/modules/keyboard/keyboard.min.css'; import 'swiper/modules/zoom/zoom.min.css'; +import { usePreventPropagation } from '../../hooks/usePreventPropagation'; import ImageGalleryLoader from './ImageGalleryLoader'; import { useImageGallery } from './hooks/useImageGallery'; @@ -40,13 +41,6 @@ const swiperStyle = css` color: var(--rcx-color-font-pure-white, #ffffff) !important; } - .rcx-swiper-close-button { - position: absolute; - z-index: 10; - top: 10px; - right: 10px; - } - .rcx-swiper-prev-button, .rcx-swiper-next-button { position: absolute; @@ -94,11 +88,43 @@ const swiperStyle = css` color: ${Palette.text['font-pure-white']}; } + + .rcx-swiper-controls { + position: absolute; + top: 0; + right: 0; + padding: 10px; + z-index: 2; + + width: 100%; + display: flex; + justify-content: flex-end; + transition: background-color 0.2s; + &:hover { + background-color: ${Palette.surface['surface-overlay']}; + transition: background-color 0.2s; + } + } `; const ImageGallery = () => { const swiperRef = useRef(null); const [, setSwiperInst] = useState(); + const [zoomScale, setZoomScale] = useState(1); + + const handleZoom = (ratio: number) => { + if (swiperRef.current?.swiper.zoom) { + const { scale, in: zoomIn } = swiperRef.current?.swiper.zoom; + setZoomScale(scale + ratio); + return zoomIn(scale + ratio); + } + }; + + const handleZoomIn = () => handleZoom(1); + const handleZoomOut = () => handleZoom(-1); + const handleResize = () => handleZoom(-(zoomScale - 1)); + + const preventPropagation = usePreventPropagation(); const { isLoading, loadMore, images, onClose } = useImageGallery(); @@ -110,9 +136,14 @@ const ImageGallery = () => {
- - e.stopPropagation()} /> - e.stopPropagation()} /> + + {zoomScale !== 1 && } + + + + + + { prevEl: '.rcx-swiper-prev-button', }} keyboard - zoom + zoom={{ toggle: false }} lazyPreloaderClass='rcx-lazy-preloader' runCallbacksOnInit onKeyPress={(_, keyCode) => String(keyCode) === '27' && onClose()} @@ -131,7 +162,7 @@ const ImageGallery = () => { {images?.map(({ _id, url }) => (
- e.stopPropagation()} /> +
From c5693fb8c89ffd29c988a5ed2b2356a4264bbd8f Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 10 Jan 2024 15:22:19 -0300 Subject: [PATCH 039/152] fix: multiple indexes creations during migrations (#31413) --- .changeset/fresh-maps-rhyme.md | 5 +++++ apps/meteor/server/models/raw/BaseRaw.ts | 14 +++++++++++++- apps/meteor/server/startup/migrations/v304.ts | 10 ++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 .changeset/fresh-maps-rhyme.md diff --git a/.changeset/fresh-maps-rhyme.md b/.changeset/fresh-maps-rhyme.md new file mode 100644 index 000000000000..2a272a3f81a2 --- /dev/null +++ b/.changeset/fresh-maps-rhyme.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: multiple indexes creation error during 304 migration diff --git a/apps/meteor/server/models/raw/BaseRaw.ts b/apps/meteor/server/models/raw/BaseRaw.ts index e43bac39b339..5ab3b9802105 100644 --- a/apps/meteor/server/models/raw/BaseRaw.ts +++ b/apps/meteor/server/models/raw/BaseRaw.ts @@ -78,6 +78,8 @@ export abstract class BaseRaw< this.preventSetUpdatedAt = options?.preventSetUpdatedAt ?? false; } + private pendingIndexes: Promise | undefined; + public async createIndexes() { const indexes = this.modelIndexes(); if (this.options?._updatedAtIndexOptions) { @@ -85,7 +87,17 @@ export abstract class BaseRaw< } if (indexes?.length) { - return this.col.createIndexes(indexes); + if (this.pendingIndexes) { + await this.pendingIndexes; + } + + this.pendingIndexes = this.col.createIndexes(indexes) as unknown as Promise; + + void this.pendingIndexes.finally(() => { + this.pendingIndexes = undefined; + }); + + return this.pendingIndexes; } } diff --git a/apps/meteor/server/startup/migrations/v304.ts b/apps/meteor/server/startup/migrations/v304.ts index db9aa44b4ee0..48cb217643d0 100644 --- a/apps/meteor/server/startup/migrations/v304.ts +++ b/apps/meteor/server/startup/migrations/v304.ts @@ -6,6 +6,16 @@ addMigration({ version: 304, name: 'Drop wrong index from analytics collection', async up() { + const indexes = await Analytics.col.indexes(); + + if ( + indexes.find( + (index) => index.name === 'room._id_1_date_1' && index.partialFilterExpression && index.partialFilterExpression.type === 'rooms', + ) + ) { + return; + } + await Analytics.col.dropIndex('room._id_1_date_1'); await Analytics.createIndexes(); }, From eecf782737eaef3f746e7e2f2a8af0c7619bc56d Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Wed, 10 Jan 2024 16:19:49 -0300 Subject: [PATCH 040/152] refactor(i18n): Translation's lint and load (#31343) --- apps/meteor/.scripts/check-i18n.js | 109 -------- apps/meteor/.scripts/translation-check.ts | 249 ++++++++++++++++++ ...translationDiff.js => translation-diff.ts} | 21 +- .../{fix-i18n.js => translation-fix-order.ts} | 6 +- apps/meteor/app/utils/lib/i18n.ts | 121 +++++++++ .../lib/utils/applyCustomTranslations.ts | 28 -- .../client/providers/TranslationProvider.tsx | 155 ++++------- apps/meteor/package.json | 8 +- .../rocketchat-i18n/i18n/ar.i18n.json | 2 +- .../rocketchat-i18n/i18n/ca.i18n.json | 2 +- .../rocketchat-i18n/i18n/cs.i18n.json | 2 +- .../rocketchat-i18n/i18n/da.i18n.json | 2 +- .../rocketchat-i18n/i18n/de-IN.i18n.json | 2 +- .../rocketchat-i18n/i18n/de.i18n.json | 2 +- .../rocketchat-i18n/i18n/en.i18n.json | 4 +- .../rocketchat-i18n/i18n/es.i18n.json | 4 +- .../rocketchat-i18n/i18n/fi.i18n.json | 6 +- .../rocketchat-i18n/i18n/fr.i18n.json | 2 +- .../rocketchat-i18n/i18n/hu.i18n.json | 2 +- .../rocketchat-i18n/i18n/it.i18n.json | 2 +- .../rocketchat-i18n/i18n/ja.i18n.json | 4 +- .../rocketchat-i18n/i18n/ka-GE.i18n.json | 10 +- .../rocketchat-i18n/i18n/km.i18n.json | 2 +- .../rocketchat-i18n/i18n/ko.i18n.json | 2 +- .../rocketchat-i18n/i18n/mn.i18n.json | 6 +- .../rocketchat-i18n/i18n/ms-MY.i18n.json | 2 +- .../rocketchat-i18n/i18n/nl.i18n.json | 2 +- .../rocketchat-i18n/i18n/pl.i18n.json | 6 +- .../rocketchat-i18n/i18n/pt-BR.i18n.json | 2 +- .../rocketchat-i18n/i18n/pt.i18n.json | 2 +- .../rocketchat-i18n/i18n/ru.i18n.json | 6 +- .../rocketchat-i18n/i18n/sv.i18n.json | 6 +- .../rocketchat-i18n/i18n/ta-IN.i18n.json | 2 +- .../rocketchat-i18n/i18n/tr.i18n.json | 2 +- .../rocketchat-i18n/i18n/zh-TW.i18n.json | 2 +- .../rocketchat-i18n/i18n/zh.i18n.json | 2 +- apps/meteor/server/lib/i18n.ts | 15 +- apps/meteor/server/settings/email.ts | 2 +- packages/i18n/src/index.mjs | 6 +- packages/ui-contexts/package.json | 2 + .../ui-contexts/src/TranslationContext.ts | 7 +- packages/ui-contexts/src/en.json | 1 - yarn.lock | 13 +- 43 files changed, 515 insertions(+), 318 deletions(-) delete mode 100644 apps/meteor/.scripts/check-i18n.js create mode 100644 apps/meteor/.scripts/translation-check.ts rename apps/meteor/.scripts/{translationDiff.js => translation-diff.ts} (54%) rename apps/meteor/.scripts/{fix-i18n.js => translation-fix-order.ts} (84%) delete mode 100644 apps/meteor/client/lib/utils/applyCustomTranslations.ts delete mode 120000 packages/ui-contexts/src/en.json diff --git a/apps/meteor/.scripts/check-i18n.js b/apps/meteor/.scripts/check-i18n.js deleted file mode 100644 index a56980f2406a..000000000000 --- a/apps/meteor/.scripts/check-i18n.js +++ /dev/null @@ -1,109 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const fg = require('fast-glob'); - -const regexVar = /__[a-zA-Z_]+__/g; - -const validateKeys = (json, usedKeys) => - usedKeys - .filter(({ key }) => typeof json[key] !== 'undefined') - .reduce((prev, cur) => { - const { key, replaces } = cur; - - const miss = replaces.filter((replace) => json[key] && json[key].indexOf(replace) === -1); - - if (miss.length > 0) { - prev.push({ key, miss }); - } - - return prev; - }, []); - -const removeMissingKeys = (i18nFiles, usedKeys) => { - i18nFiles.forEach((file) => { - const json = JSON.parse(fs.readFileSync(file, 'utf8')); - if (Object.keys(json).length === 0) { - return; - } - - validateKeys(json, usedKeys).forEach(({ key }) => { - json[key] = null; - }); - - fs.writeFileSync(file, JSON.stringify(json, null, 2)); - }); -}; - -const checkUniqueKeys = (content, json, filename) => { - const matchKeys = content.matchAll(/^\s+"([^"]+)"/gm); - - const allKeys = [...matchKeys]; - - if (allKeys.length !== Object.keys(json).length) { - throw new Error(`Duplicated keys found on file ${filename}`); - } -}; - -const validate = (i18nFiles, usedKeys) => { - const totalErrors = i18nFiles.reduce((errors, file) => { - const content = fs.readFileSync(file, 'utf8'); - const json = JSON.parse(content); - - checkUniqueKeys(content, json, file); - - // console.log('json, usedKeys2', json, usedKeys); - - const result = validateKeys(json, usedKeys); - - if (result.length === 0) { - return errors; - } - - console.log('\n## File', file, `(${result.length} errors)`); - - result.forEach(({ key, miss }) => { - console.log('\n- Key:', key, '\n Missing variables:', miss.join(', ')); - }); - - return errors + result.length; - }, 0); - - if (totalErrors > 0) { - throw new Error(`\n${totalErrors} errors found`); - } -}; - -const checkFiles = async (sourcePath, sourceFile, fix = false) => { - const content = fs.readFileSync(path.join(sourcePath, sourceFile), 'utf8'); - const sourceContent = JSON.parse(content); - - checkUniqueKeys(content, sourceContent, sourceFile); - - const usedKeys = Object.entries(sourceContent).map(([key, value]) => { - const replaces = value.match(regexVar); - return { - key, - replaces, - }; - }); - - const keysWithInterpolation = usedKeys.filter(({ replaces }) => !!replaces); - - const i18nFiles = await fg([`${sourcePath}/**/*.i18n.json`]); - - if (fix) { - return removeMissingKeys(i18nFiles, keysWithInterpolation); - } - - validate(i18nFiles, keysWithInterpolation); -}; - -(async () => { - try { - await checkFiles('./packages/rocketchat-i18n/i18n', 'en.i18n.json', process.argv[2] === '--fix'); - } catch (e) { - console.error(e); - process.exit(1); - } -})(); diff --git a/apps/meteor/.scripts/translation-check.ts b/apps/meteor/.scripts/translation-check.ts new file mode 100644 index 000000000000..0a1b9d319b2c --- /dev/null +++ b/apps/meteor/.scripts/translation-check.ts @@ -0,0 +1,249 @@ +import type { PathLike } from 'node:fs'; +import { readFile, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { inspect } from 'node:util'; + +import fg from 'fast-glob'; +import i18next from 'i18next'; +import supportsColor from 'supports-color'; + +const hasDuplicatedKeys = (content: string, json: Record) => { + const matchKeys = content.matchAll(/^\s+"([^"]+)"/gm); + + const allKeys = [...matchKeys]; + + return allKeys.length !== Object.keys(json).length; +}; + +const parseFile = async (path: PathLike) => { + const content = await readFile(path, 'utf-8'); + let json: Record; + try { + json = JSON.parse(content); + } catch (e) { + if (e instanceof SyntaxError) { + const matches = /^Unexpected token .* in JSON at position (\d+)$/.exec(e.message); + + if (matches) { + const [, positionStr] = matches; + const position = parseInt(positionStr, 10); + const line = content.slice(0, position).split('\n').length; + const column = position - content.slice(0, position).lastIndexOf('\n'); + throw new SyntaxError(`Invalid JSON on file ${path}:${line}:${column}`); + } + } + throw new SyntaxError(`Invalid JSON on file ${path}: ${e.message}`); + } + + if (hasDuplicatedKeys(content, json)) { + throw new SyntaxError(`Duplicated keys found on file ${path}`); + } + + return json; +}; + +const insertTranslation = (json: Record, refKey: string, [key, value]: [key: string, value: string]) => { + const entries = Object.entries(json); + + const refIndex = entries.findIndex(([entryKey]) => entryKey === refKey); + + if (refIndex === -1) { + throw new Error(`Reference key ${refKey} not found`); + } + + const movingEntries = entries.slice(refIndex + 1); + + for (const [key] of movingEntries) { + delete json[key]; + } + + json[key] = value; + + for (const [key, value] of movingEntries) { + json[key] = value; + } +}; + +const persistFile = async (path: PathLike, json: Record) => { + const content = JSON.stringify(json, null, 2); + + await writeFile(path, content, 'utf-8'); +}; + +const oldPlaceholderFormat = /__([a-zA-Z_]+)__/g; + +const checkPlaceholdersFormat = async ({ json, path, fix = false }: { json: Record; path: PathLike; fix?: boolean }) => { + const outdatedKeys = Object.entries(json) + .map(([key, value]) => ({ + key, + value, + placeholders: value.match(oldPlaceholderFormat), + })) + .filter((outdatedKey): outdatedKey is { key: string; value: string; placeholders: RegExpMatchArray } => !!outdatedKey.placeholders); + + if (outdatedKeys.length > 0) { + const message = `Outdated placeholder format on file ${path}: ${inspect(outdatedKeys, { colors: !!supportsColor.stdout })}`; + + if (fix) { + console.warn(message); + + for (const { key, value } of outdatedKeys) { + const newValue = value.replace(oldPlaceholderFormat, (_, name) => `{{${name}}}`); + + json[key] = newValue; + } + + await persistFile(path, json); + + return; + } + + throw new Error(message); + } +}; + +export const extractSingularKeys = (json: Record, lng: string) => { + if (!i18next.isInitialized) { + i18next.init({ initImmediate: false }); + } + + const pluralSuffixes = i18next.services.pluralResolver.getSuffixes(lng) as string[]; + + const singularKeys = new Set( + Object.keys(json).map((key) => { + for (const pluralSuffix of pluralSuffixes) { + if (key.endsWith(pluralSuffix)) { + return key.slice(0, -pluralSuffix.length); + } + } + + return key; + }), + ); + + return [singularKeys, pluralSuffixes] as const; +}; + +const checkMissingPlurals = async ({ + json, + path, + lng, + fix = false, +}: { + json: Record; + path: PathLike; + lng: string; + fix?: boolean; +}) => { + const [singularKeys, pluralSuffixes] = extractSingularKeys(json, lng); + + const missingPluralKeys: { singularKey: string; existing: string[]; missing: string[] }[] = []; + + for (const singularKey of singularKeys) { + if (singularKey in json) { + continue; + } + + const pluralKeys = pluralSuffixes.map((suffix) => `${singularKey}${suffix}`); + + const existing = pluralKeys.filter((key) => key in json); + const missing = pluralKeys.filter((key) => !(key in json)); + + if (missing.length > 0) { + missingPluralKeys.push({ singularKey, existing, missing }); + } + } + + if (missingPluralKeys.length > 0) { + const message = `Missing plural keys on file ${path}: ${inspect(missingPluralKeys, { colors: !!supportsColor.stdout })}`; + + if (fix) { + console.warn(message); + + for (const { existing, missing } of missingPluralKeys) { + for (const missingKey of missing) { + const refKey = existing.slice(-1)[0]; + const value = json[refKey]; + insertTranslation(json, refKey, [missingKey, value]); + } + } + + await persistFile(path, json); + + return; + } + + throw new Error(message); + } +}; + +const checkExceedingKeys = async ({ + json, + path, + lng, + sourceJson, + sourceLng, + fix = false, +}: { + json: Record; + path: PathLike; + lng: string; + sourceJson: Record; + sourceLng: string; + fix?: boolean; +}) => { + const [singularKeys] = extractSingularKeys(json, lng); + const [sourceSingularKeys] = extractSingularKeys(sourceJson, sourceLng); + + const exceedingKeys = [...singularKeys].filter((key) => !sourceSingularKeys.has(key)); + + if (exceedingKeys.length > 0) { + const message = `Exceeding keys on file ${path}: ${inspect(exceedingKeys, { colors: !!supportsColor.stdout })}`; + + if (fix) { + for (const key of exceedingKeys) { + delete json[key]; + } + + await persistFile(path, json); + + return; + } + + throw new Error(message); + } +}; + +const checkFiles = async (sourceDirPath: string, sourceLng: string, fix = false) => { + const sourcePath = join(sourceDirPath, `${sourceLng}.i18n.json`); + const sourceJson = await parseFile(sourcePath); + + await checkPlaceholdersFormat({ json: sourceJson, path: sourcePath, fix }); + await checkMissingPlurals({ json: sourceJson, path: sourcePath, lng: sourceLng, fix }); + + const i18nFiles = await fg([join(sourceDirPath, `**/*.i18n.json`), `!${sourcePath}`]); + + const languageFileRegex = /\/([^\/]*?).i18n.json$/; + const translations = await Promise.all( + i18nFiles.map(async (path) => { + const lng = languageFileRegex.exec(path)?.[1]; + if (!lng) { + throw new Error(`Invalid language file path ${path}`); + } + + return { path, json: await parseFile(path), lng }; + }), + ); + + for await (const { path, json, lng } of translations) { + await checkPlaceholdersFormat({ json, path, fix }); + await checkMissingPlurals({ json, path, lng, fix }); + await checkExceedingKeys({ json, path, lng, sourceJson, sourceLng, fix }); + } +}; + +const fix = process.argv[2] === '--fix'; +checkFiles('./packages/rocketchat-i18n/i18n', 'en', fix).catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/apps/meteor/.scripts/translationDiff.js b/apps/meteor/.scripts/translation-diff.ts similarity index 54% rename from apps/meteor/.scripts/translationDiff.js rename to apps/meteor/.scripts/translation-diff.ts index 7c83e33c76ee..0ee7a1c72b9d 100644 --- a/apps/meteor/.scripts/translationDiff.js +++ b/apps/meteor/.scripts/translation-diff.ts @@ -1,18 +1,14 @@ -#!/usr/bin/env node +#!/usr/bin/env ts-node -const fs = require('fs'); -const path = require('path'); -const util = require('util'); - -// Convert fs.readFile into Promise version of same -const readFile = util.promisify(fs.readFile); +import { readFile } from 'fs/promises'; +import path from 'path'; const translationDir = path.resolve(__dirname, '../packages/rocketchat-i18n/i18n/'); -async function translationDiff(source, target) { +async function translationDiff(source: string, target: string) { console.debug('loading translations from', translationDir); - function diffKeys(a, b) { + function diffKeys(a: Record, b: Record) { const diff = {}; Object.keys(a).forEach((key) => { if (!b[key]) { @@ -29,10 +25,9 @@ async function translationDiff(source, target) { return diffKeys(sourceTranslations, targetTranslations); } -console.log('Note: You can set the source and target language of the comparison with env-variables SOURCE/TARGET_LANGUAGE'); -const sourceLang = process.env.SOURCE_LANGUAGE || 'en'; -const targetLang = process.env.TARGET_LANGUAGE || 'de'; +const sourceLang = process.argv[2] || 'en'; +const targetLang = process.argv[3] || 'de'; translationDiff(sourceLang, targetLang).then((diff) => { console.log('Diff between', sourceLang, 'and', targetLang); - console.log(JSON.stringify(diff, '', 2)); + console.log(JSON.stringify(diff, undefined, 2)); }); diff --git a/apps/meteor/.scripts/fix-i18n.js b/apps/meteor/.scripts/translation-fix-order.ts similarity index 84% rename from apps/meteor/.scripts/fix-i18n.js rename to apps/meteor/.scripts/translation-fix-order.ts index f0002c8ca4eb..14eba2e73682 100644 --- a/apps/meteor/.scripts/fix-i18n.js +++ b/apps/meteor/.scripts/translation-fix-order.ts @@ -6,11 +6,11 @@ * - remove all keys not present in source i18n file */ -const fs = require('fs'); +import fs from 'fs'; -const fg = require('fast-glob'); +import fg from 'fast-glob'; -const fixFiles = (path, source, newlineAtEnd = false) => { +const fixFiles = (path: string, source: string, newlineAtEnd = false) => { const sourceFile = JSON.parse(fs.readFileSync(`${path}${source}`, 'utf8')); const sourceKeys = Object.keys(sourceFile); diff --git a/apps/meteor/app/utils/lib/i18n.ts b/apps/meteor/app/utils/lib/i18n.ts index a491159e49e9..efbfa15cefa6 100644 --- a/apps/meteor/app/utils/lib/i18n.ts +++ b/apps/meteor/app/utils/lib/i18n.ts @@ -1,3 +1,4 @@ +import type { RocketchatI18nKeys } from '@rocket.chat/i18n'; import i18next from 'i18next'; import sprintf from 'i18next-sprintf-postprocessor'; @@ -19,3 +20,123 @@ export const addSprinfToI18n = function (t: (key: string, ...replaces: any) => s }; export const t = addSprinfToI18n(i18n.t.bind(i18n)); + +/** + * Extract the translation keys from a flat object and group them by namespace + * + * Example: + * + * ```js + * const source = { + * 'core.key1': 'value1', + * 'core.key2': 'value2', + * 'onboarding.key1': 'value1', + * 'onboarding.key2': 'value2', + * 'registration.key1': 'value1', + * 'registration.key2': 'value2', + * 'cloud.key1': 'value1', + * 'cloud.key2': 'value2', + * 'subscription.key1': 'value1', + * 'subscription.key2': 'value2', + * }; + * + * const result = extractTranslationNamespaces(source); + * + * console.log(result); + * + * // { + * // core: { + * // key1: 'value1', + * // key2: 'value2' + * // }, + * // onboarding: { + * // key1: 'value1', + * // key2: 'value2' + * // }, + * // registration: { + * // key1: 'value1', + * // key2: 'value2' + * // }, + * // cloud: { + * // key1: 'value1', + * // key2: 'value2' + * // }, + * // subscription: { + * // key1: 'value1', + * // key2: 'value2' + * // } + * // } + * ``` + * + * @param source the flat object with the translation keys + */ +export const extractTranslationNamespaces = (source: Record): Record> => { + const result: Record> = { + core: {}, + onboarding: {}, + registration: {}, + cloud: {}, + subscription: {}, + }; + + for (const [key, value] of Object.entries(source)) { + const prefix = availableTranslationNamespaces.find((namespace) => key.startsWith(`${namespace}.`)); + const keyWithoutNamespace = prefix ? key.slice(prefix.length + 1) : key; + const ns = prefix ?? defaultTranslationNamespace; + result[ns][keyWithoutNamespace] = value; + } + + return result; +}; + +/** + * Extract only the translation keys that match the given namespaces + * + * @param source the flat object with the translation keys + * @param namespaces the namespaces to extract + */ +export const extractTranslationKeys = (source: Record, namespaces: string | string[] = []): { [key: string]: any } => { + const all = extractTranslationNamespaces(source); + return Array.isArray(namespaces) + ? (namespaces as TranslationNamespace[]).reduce((result, namespace) => ({ ...result, ...all[namespace] }), {}) + : all[namespaces as TranslationNamespace]; +}; + +export type TranslationNamespace = + | (Extract extends `${infer T}.${string}` ? (T extends Lowercase ? T : never) : never) + | 'core'; + +const namespacesMap: Record = { + core: true, + onboarding: true, + registration: true, + cloud: true, + subscription: true, +}; + +export const availableTranslationNamespaces = Object.keys(namespacesMap) as TranslationNamespace[]; +export const defaultTranslationNamespace: TranslationNamespace = 'core'; + +export const applyCustomTranslations = ( + i18n: typeof i18next, + parsedCustomTranslations: Record>, + { namespaces, languages }: { namespaces?: string[]; languages?: string[] } = {}, +) => { + for (const [lng, translations] of Object.entries(parsedCustomTranslations)) { + if (languages && !languages.includes(lng)) { + continue; + } + + for (const [key, value] of Object.entries(translations)) { + const prefix = availableTranslationNamespaces.find((namespace) => key.startsWith(`${namespace}.`)); + const keyWithoutNamespace = prefix ? key.slice(prefix.length + 1) : key; + const ns = prefix ?? defaultTranslationNamespace; + + if (namespaces && !namespaces.includes(ns)) { + continue; + } + + i18n.addResourceBundle(lng, ns, { [keyWithoutNamespace]: value }, true, true); + } + } +}; diff --git a/apps/meteor/client/lib/utils/applyCustomTranslations.ts b/apps/meteor/client/lib/utils/applyCustomTranslations.ts deleted file mode 100644 index f629ed1aaace..000000000000 --- a/apps/meteor/client/lib/utils/applyCustomTranslations.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { settings } from '../../../app/settings/client'; -import { i18n } from '../../../app/utils/lib/i18n'; - -const parseToJSON = (customTranslations: string) => { - try { - return JSON.parse(customTranslations); - } catch (e) { - return false; - } -}; - -export const applyCustomTranslations = (): void => { - const customTranslations: string | undefined = settings.get('Custom_Translations'); - - if (!customTranslations || !parseToJSON(customTranslations)) { - return; - } - - try { - const parsedCustomTranslations: Record = JSON.parse(customTranslations); - - for (const [lang, translations] of Object.entries(parsedCustomTranslations)) { - i18n.addResourceBundle(lang, 'core', translations); - } - } catch (e) { - console.error('Invalid setting Custom_Translations', e); - } -}; diff --git a/apps/meteor/client/providers/TranslationProvider.tsx b/apps/meteor/client/providers/TranslationProvider.tsx index f9fdf299a5d6..7f98c374f949 100644 --- a/apps/meteor/client/providers/TranslationProvider.tsx +++ b/apps/meteor/client/providers/TranslationProvider.tsx @@ -1,8 +1,8 @@ -import { useLocalStorage, useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useLocalStorage } from '@rocket.chat/fuselage-hooks'; import languages from '@rocket.chat/i18n/dist/languages'; import en from '@rocket.chat/i18n/src/locales/en.i18n.json'; import { normalizeLanguage } from '@rocket.chat/tools'; -import type { TranslationKey, TranslationContextValue } from '@rocket.chat/ui-contexts'; +import type { TranslationContextValue } from '@rocket.chat/ui-contexts'; import { useMethod, useSetting, TranslationContext } from '@rocket.chat/ui-contexts'; import type i18next from 'i18next'; import I18NextHttpBackend from 'i18next-http-backend'; @@ -14,99 +14,73 @@ import { I18nextProvider, initReactI18next, useTranslation } from 'react-i18next import { CachedCollectionManager } from '../../app/ui-cached-collection/client'; import { getURL } from '../../app/utils/client'; -import { i18n, addSprinfToI18n } from '../../app/utils/lib/i18n'; +import { + i18n, + addSprinfToI18n, + extractTranslationKeys, + applyCustomTranslations, + availableTranslationNamespaces, + defaultTranslationNamespace, + extractTranslationNamespaces, +} from '../../app/utils/lib/i18n'; import { AppClientOrchestratorInstance } from '../../ee/client/apps/orchestrator'; -import { applyCustomTranslations } from '../lib/utils/applyCustomTranslations'; import { isRTLScriptLanguage } from '../lib/utils/isRTLScriptLanguage'; i18n.use(I18NextHttpBackend).use(initReactI18next).use(sprintf); -type TranslationNamespace = Extract extends `${infer T}.${string}` - ? T extends Lowercase - ? T - : never - : never; - -const namespacesDefault = ['core', 'onboarding', 'registration', 'cloud'] as TranslationNamespace[]; - -const parseToJSON = (customTranslations: string): Record> | false => { - try { - return JSON.parse(customTranslations); - } catch (e) { - return false; - } -}; - -const localeCache = new Map>(); - -const useI18next = (lng: string): typeof i18next => { +const useCustomTranslations = (i18n: typeof i18next) => { const customTranslations = useSetting('Custom_Translations'); - const parsedCustomTranslations = useMemo(() => { + const parsedCustomTranslations = useMemo((): Record> | undefined => { if (!customTranslations || typeof customTranslations !== 'string') { - return; + return undefined; } - return parseToJSON(customTranslations); + try { + return JSON.parse(customTranslations); + } catch (e) { + console.error(e); + return undefined; + } }, [customTranslations]); - const extractKeys = useMutableCallback( - (source: Record, lngs?: string | string[], namespaces: string | string[] = []): { [key: string]: any } => { - const result: { [key: string]: any } = {}; - - for (const [key, value] of Object.entries(source)) { - const [prefix] = key.split('.'); - - if (prefix && Array.isArray(namespaces) ? namespaces.includes(prefix) : prefix === namespaces) { - result[key.slice(prefix.length + 1)] = value; - continue; - } - - if (Array.isArray(namespaces) ? namespaces.includes('core') : namespaces === 'core') { - result[key] = value; - } - } + useEffect(() => { + if (!parsedCustomTranslations) { + return; + } - if (lngs && parsedCustomTranslations) { - for (const language of Array.isArray(lngs) ? lngs : [lngs]) { - if (!parsedCustomTranslations[language]) { - continue; - } + applyCustomTranslations(i18n, parsedCustomTranslations); - for (const [key, value] of Object.entries(parsedCustomTranslations[language])) { - const prefix = (Array.isArray(namespaces) ? namespaces : [namespaces]).find((namespace) => key.startsWith(`${namespace}.`)); + const handleLanguageChanged = (): void => { + applyCustomTranslations(i18n, parsedCustomTranslations); + }; - if (prefix) { - result[key.slice(prefix.length + 1)] = value; - continue; - } + i18n.on('languageChanged', handleLanguageChanged); - if (Array.isArray(namespaces) ? namespaces.includes('core') : namespaces === 'core') { - result[key] = value; - } - } - } - } + return () => { + i18n.off('languageChanged', handleLanguageChanged); + }; + }, [i18n, parsedCustomTranslations]); +}; - return result; - }, - ); +const localeCache = new Map>(); +const useI18next = (lng: string): typeof i18next => { if (!i18n.isInitialized) { i18n.init({ lng, fallbackLng: 'en', - ns: namespacesDefault, + ns: availableTranslationNamespaces, + defaultNS: defaultTranslationNamespace, nsSeparator: '.', resources: { - en: extractKeys(en), + en: extractTranslationNamespaces(en), }, partialBundledLanguages: true, - defaultNS: 'core', backend: { loadPath: 'i18n/{{lng}}.json', - parse: (data: string, lngs?: string | string[], namespaces: string | string[] = []) => - extractKeys(JSON.parse(data), lngs, namespaces), + parse: (data: string, _lngs?: string | string[], namespaces: string | string[] = []) => + extractTranslationKeys(JSON.parse(data), namespaces), request: (_options, url, _payload, callback) => { const params = url.split('/'); @@ -137,47 +111,12 @@ const useI18next = (lng: string): typeof i18next => { } useEffect(() => { - if (i18n.language !== lng) { - i18n.changeLanguage(lng); - } + i18n.changeLanguage(lng); }, [lng]); - useEffect(() => { - if (!parsedCustomTranslations) { - return; - } - - for (const [ln, translations] of Object.entries(parsedCustomTranslations)) { - if (!translations) { - continue; - } - const namespaces = Object.entries(translations).reduce((acc, [key, value]): Record> => { - const namespace = key.split('.')[0]; - - if (namespacesDefault.includes(namespace as unknown as TranslationNamespace)) { - acc[namespace] = acc[namespace] ?? {}; - acc[namespace][key] = value; - acc[namespace][key.slice(namespace.length + 1)] = value; - return acc; - } - acc.project = acc.project ?? {}; - acc.project[key] = value; - return acc; - }, {} as Record>); - - for (const [namespace, translations] of Object.entries(namespaces)) { - i18n.addResourceBundle(ln, namespace, translations); - } - } - }, [parsedCustomTranslations]); - return i18n; }; -type TranslationProviderProps = { - children: ReactNode; -}; - const useAutoLanguage = () => { const serverLanguage = useSetting('Language'); const browserLanguage = normalizeLanguage(window.navigator.userLanguage ?? window.navigator.language); @@ -206,11 +145,17 @@ const getLanguageName = (code: string, lng: string): string => { } }; +type TranslationProviderProps = { + children: ReactNode; +}; + const TranslationProvider = ({ children }: TranslationProviderProps): ReactElement => { const loadLocale = useMethod('loadLocale'); const language = useAutoLanguage(); const i18nextInstance = useI18next(language); + useCustomTranslations(i18nextInstance); + const availableLanguages = useMemo( () => [ { @@ -290,8 +235,8 @@ const TranslationProviderInner = ({ () => ({ language: i18n.language, languages: availableLanguages, - loadLanguage: async (language: string): Promise => { - i18n.changeLanguage(language).then(() => applyCustomTranslations()); + loadLanguage: async (language: string) => { + i18n.changeLanguage(language); }, translate: Object.assign(addSprinfToI18n(t), { has: ((key, options) => key && i18n.exists(key, options)) as TranslationContextValue['translate']['has'], diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 2db612565917..e63b4bb7c71f 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -44,9 +44,9 @@ ".testunit:definition": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' mocha --config ./.mocharc.definition.js", "testunit-watch": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' mocha --watch --config ./.mocharc.js", "test": "npm run testapi && npm run testui", - "translation-diff": "node .scripts/translationDiff.js", - "translation-check": "node .scripts/check-i18n.js", - "translation-fix-order": "node .scripts/fix-i18n.js", + "translation-diff": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node .scripts/translation-diff.ts", + "translation-check": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node .scripts/translation-check.ts", + "translation-fix-order": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node .scripts/translation-fix-order.ts", "version": "node .scripts/version.js", "set-version": "node .scripts/set-version.js", "release": "meteor npm run set-version --silent", @@ -148,6 +148,7 @@ "@types/strict-uri-encode": "^2.0.1", "@types/string-strip-html": "^5.0.1", "@types/supertest": "^2.0.15", + "@types/supports-color": "~7.2.0", "@types/textarea-caret": "^3.0.2", "@types/ua-parser-js": "^0.7.38", "@types/use-subscription": "^1.0.1", @@ -204,6 +205,7 @@ "stylelint": "^14.9.1", "stylelint-order": "^5.0.0", "supertest": "^6.2.3", + "supports-color": "~7.2.0", "template-file": "^6.0.1", "ts-node": "^10.9.1", "typescript": "~5.3.2" diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json index f4edf2fed22f..6060705de4d3 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json @@ -2575,7 +2575,7 @@ "leave-c_description": "إذن لمغادرة القنوات", "leave-p": "مغادرة المجموعات الخاصة", "leave-p_description": "إذن لمغادرة المجموعات الخاصة", - "Lets_get_you_new_one": "دعنا نحضر لك واحدة جديدة!", + "Lets_get_you_new_one_": "دعنا نحضر لك واحدة جديدة!", "Link_Preview": "رابط المعاينة", "List_of_Channels": "قائمة Channels", "List_of_departments_for_forward": "قائمة الأقسام المسموح بإعادة توجيهها (اختياري)", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json index b56b0b645d6b..f61d28735fe6 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json @@ -2549,7 +2549,7 @@ "leave-c_description": "Permís per sortir de canals", "leave-p": "Sortir de grups privats", "leave-p_description": "Permís per sortir de grups privats", - "Lets_get_you_new_one": "Et portem un de nou!", + "Lets_get_you_new_one_": "Et portem un de nou!", "List_of_Channels": "Llista de canals", "List_of_departments_for_forward": "Llista de departaments permesos per reenviament (opcional)", "List_of_departments_for_forward_description": "Permetre establir una llista restringida de departaments que poden rebre xats d'aquest departament", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json index ef36a4dbb928..1c5931521c91 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json @@ -2169,7 +2169,7 @@ "Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "Pokud nechcete zobrazovat roli, ponechte pole popisu prázdné", "leave-c": "Odejít z místností", "leave-p": "Opustit soukromé skupiny", - "Lets_get_you_new_one": "Pojďme si pořídit nový!", + "Lets_get_you_new_one_": "Pojďme si pořídit nový!", "List_of_Channels": "Seznam místností", "List_of_departments_for_forward": "Seznam oddělení povolených pro přesměrování (volitelné)", "List_of_departments_for_forward_description": "Omezit oddělení do kterých je možné přesměrovat konverzace z aktuálního", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json index 4bf7e41fb9e1..d9aaf3f0a003 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json @@ -2180,7 +2180,7 @@ "Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "Lad beskrivelsesfeltet være tomt, hvis du ikke vil vise rollen", "leave-c": "Forlad kanaler", "leave-p": "Forlad private grupper", - "Lets_get_you_new_one": "Lad os finde en ny til dig!", + "Lets_get_you_new_one_": "Lad os finde en ny til dig!", "List_of_Channels": "Liste over kanaler", "List_of_departments_for_forward": "Liste over tilladte afdelinger til videresendelse (valgfrit)", "List_of_departments_for_forward_description": "Tillad at indstille en begrænset liste over afdelinger der kan modtage chats fra denne afdeling", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de-IN.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de-IN.i18n.json index 5947c8517e54..2248f5ef3022 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de-IN.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de-IN.i18n.json @@ -1767,7 +1767,7 @@ "Leave_the_current_channel": "Aktuellen Kanal verlassen", "leave-c": "Kanäle verlassen", "leave-p": "Verlasse private Gruppen", - "Lets_get_you_new_one": "Lass mich Ihnen ein neues geben!", + "Lets_get_you_new_one_": "Lass mich Ihnen ein neues geben!", "List_of_Channels": "Liste der Kanäle", "List_of_Direct_Messages": "Liste der Direktnachrichten", "Livechat": "Livechat", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json index de81f4a1a00f..68c54237e99b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json @@ -2863,7 +2863,7 @@ "leave-c_description": "Berechtigung, Channels zu verlassen", "leave-p": "Private Gruppen verlassen", "leave-p_description": "Erlaubnis, private Gruppen zu verlassen", - "Lets_get_you_new_one": "Geben wir Ihnen ein neues!", + "Lets_get_you_new_one_": "Geben wir Ihnen ein neues!", "License": "Lizenz", "Link_Preview": "Link-Vorschau", "List_of_Channels": "Liste der Channels", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 79bdfbf74a0f..e4b947b0a047 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -3082,7 +3082,7 @@ "leave-c_description": "Permission to leave channels", "leave-p": "Leave Private Groups", "leave-p_description": "Permission to leave private groups", - "Lets_get_you_new_one": "Let's get you a new one!", + "Lets_get_you_new_one_": "Let's get you a new one!", "Let_them_know": "Let them know", "License": "License", "Line": "Line", @@ -5891,7 +5891,7 @@ "Your_password_is_wrong": "Your password is wrong!", "Your_password_was_changed_by_an_admin": "Your password was changed by an admin.", "Your_push_was_sent_to_s_devices": "Your push was sent to %s devices", - "Your_request_to_join__roomName__has_been_made_it_could_take_up_to_15_minutes_to_be_processed": "Your request to join __roomName__ has been made, it could take up to 15 minutes to be processed. You'll be notified when it's ready to go.", + "Your_request_to_join__roomName__has_been_made_it_could_take_up_to_15_minutes_to_be_processed": "Your request to join {{roomName}} has been made, it could take up to 15 minutes to be processed. You'll be notified when it's ready to go.", "Your_question": "Your question", "Your_server_link": "Your server link", "Your_temporary_password_is_password": "Your temporary password is [password].", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json index 908e6d6a9f71..9e1fed728dce 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json @@ -2570,7 +2570,7 @@ "leave-c_description": "Permiso para salir de canales", "leave-p": "Salir de grupos privados", "leave-p_description": "Permiso para salir de grupos privados", - "Lets_get_you_new_one": "Vamos a darte uno nuevo", + "Lets_get_you_new_one_": "Vamos a darte uno nuevo", "List_of_Channels": "Lista de Channels", "List_of_departments_for_forward": "Lista de departamentos permitidos para reenvío (opcional)", "List_of_departments_for_forward_description": "Permitir establecer una lista restringida de departamentos que pueden recibir chats de este departamento", @@ -4905,8 +4905,10 @@ "subscription.callout.capabilitiesDisabled": "Características desactivadas", "subscription.callout.description.limitsExceeded_one": "Su espacio de trabajo ha superado el límite de <1> {{val}} . <3> Administre su suscripción para incrementar los límites.", "subscription.callout.description.limitsExceeded_other": "Su espacio de trabajo ha superado los límites <1> {{val, list}} . <3> Administre su suscripción para incrementar los límites.", + "subscription.callout.description.limitsExceeded_many": "Su espacio de trabajo ha superado los límites <1> {{val, list}} . <3> Administre su suscripción para incrementar los límites.", "subscription.callout.description.limitsReached_one": "Su espacio de trabajo ha alcanzado el límite <1> {{val}} . <3> Administre su suscripción para incrementar los límites.", "subscription.callout.description.limitsReached_other": "Su espacio de trabajo ha alcanzado los límites <1> {{val, list}} . <3> Administre su suscripción para incrementar los límites.", + "subscription.callout.description.limitsReached_many": "Su espacio de trabajo ha alcanzado los límites <1> {{val, list}} . <3> Administre su suscripción para incrementar los límites.", "subscription.callout.allPremiumCapabilitiesDisabled": "Todas las funciones premium desactivadas", "subscription.callout.activeUsers": "puestos", "subscription.callout.guestUsers": "invitados", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json index 191fe4a82ea0..6cc6bae167e6 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json @@ -2908,7 +2908,7 @@ "leave-c_description": "Oikeus poistua kanavilta", "leave-p": "Poistu yksityisistä ryhmistä", "leave-p_description": "Oikeus poistua yksityisistä ryhmistä", - "Lets_get_you_new_one": "Hankitaan uusi!", + "Lets_get_you_new_one_": "Hankitaan uusi!", "License": "Käyttöoikeus", "Line": "Rivi", "Link": "Linkki", @@ -4187,7 +4187,7 @@ "SAML_AuthnContext_Template": "AuthnContext-malli", "SAML_AuthnContext_Template_Description": "Voit käyttää tässä mitä tahansa muuttujaa AuthnRequest-mallista. \n \n Jos haluat lisätä lisää authn-konteksteja, kopioi {{AuthnContextClassRef}}-tunniste ja korvaa {{\\_\\_authnContext\\_\\}}-muuttuja uudella kontekstilla.", "SAML_AuthnRequest_Template": "AuthnRequest-malli", - "SAML_AuthnRequest_Template_Description": "Seuraavat muuttujat ovat käytettävissä: \n- **\\_\\_newId\\__\\_**: Satunnaisesti luotu id-merkkijono \n- **\\__\\_instant\\_\\_\\_**: Nykyinen aikaleima \n- **\\_\\_callbackUrl\\_\\_**: Rocket.Chatin takaisinkutsun URL-osoite. \n- **\\_\\_entryPoint\\_\\_**: {{Custom Entry Point}} -asetuksen arvo. \n- **\\____________**: {{Custom Issuer}} -asetuksen arvo. \n- **\\_\\_identifierFormatTag\\_\\_**: __NameID-käytäntömallin__ sisältö, jos voimassa oleva {{Identifier Format}} on määritetty. \n- **\\_\\_identifierFormat\\_\\_**: {{Identifier Format}} -asetuksen arvo. \n- **\\_\\_authnContextTag\\_\\_**: __AuthnContext-mallin__ sisältö, jos voimassa oleva {{Custom Authn Context}} on määritetty. \n- **\\_\\_authnContextComparison\\_\\_**: {{Authn Context Comparison}} -asetuksen arvo. \n- **\\_\\_authnContext\\_\\_**: {{Custom Authn Context}} -asetuksen arvo.", + "SAML_AuthnRequest_Template_Description": "Seuraavat muuttujat ovat käytettävissä: \n- **\\_\\_newId\\_\\_**: Satunnaisesti luotu id-merkkijono \n- **\\_\\_instant\\_\\_**: Nykyinen aikaleima \n- **\\_\\_callbackUrl\\_\\_**: Rocket.Chatin takaisinkutsun URL-osoite. \n- **\\_\\_entryPoint\\_\\_**: {{Custom Entry Point}} -asetuksen arvo. \n- **\\_\\_issuer\\_\\_**: {{Custom Issuer}} -asetuksen arvo. \n- **\\_\\_identifierFormatTag\\_\\_**: {{NameID Policy Template}} sisältö, jos voimassa oleva {{Identifier Format}} on määritetty. \n- **\\_\\_identifierFormat\\_\\_**: {{Identifier Format}} -asetuksen arvo. \n- **\\_\\_authnContextTag\\_\\_**: {{AuthnContext Template}} sisältö, jos voimassa oleva {{Custom Authn Context}} on määritetty. \n- **\\_\\_authnContextComparison\\_\\_**: {{Authn Context Comparison}} -asetuksen arvo. \n- **\\_\\_authnContext\\_\\_**: {{Custom Authn Context}} -asetuksen arvo.", "SAML_Connection": "Yhteys", "SAML_Enterprise": "Yritys", "SAML_General": "Yleinen", @@ -4236,7 +4236,7 @@ "SAML_LogoutResponse_Template_Description": "Seuraavat muuttujat ovat käytettävissä: \n- **\\_\\_newId\\__\\_**: Satunnaisesti luotu id-merkkijono \n- **\\_\\_inResponseToId\\_\\_**: IdP:ltä vastaanotetun uloskirjautumispyynnön tunnus \n- **\\_\\_instant\\_\\__**: Nykyinen aikaleima \n- **\\_\\_idpSLORedirectURL\\_\\_**: IDP:n yksittäisen uloskirjautumisen URL-osoite, johon ohjataan. \n- **\\_\\_issuer\\_\\__**: {{Custom Issuer}} -asetuksen arvo. \n- **\\_\\_identifierFormat\\_\\_**: {{Identifier Format}} -asetuksen arvo. \n- **\\_\\__nameID\\_\\__**: IdP:n uloskirjautumispyynnöstä saatu NameID. \n- **\\_\\_sessionIndex\\_\\_**: IdP:n uloskirjautumispyynnöstä saatu sessionIndex.", "SAML_Metadata_Certificate_Template_Description": "Seuraavat muuttujat ovat käytettävissä: \n- **\\_\\_certificate\\_\\_**: Yksityinen varmenne väitteen salausta varten.", "SAML_Metadata_Template": "Metadatan tietomalli", - "SAML_Metadata_Template_Description": "Seuraavat muuttujat ovat käytettävissä: \n- **\\_\\_sloLocation\\_\\_**: Rocket.Chat Single LogOut URL-osoite. \n- **\\____issuer\\_____**: {{Custom Issuer}} -asetuksen arvo. \n- **\\_\\_identifierFormat\\_\\_**: {{Identifier Format}} -asetuksen arvo. \n- **\\_\\_certificateTag\\_\\_**: Jos yksityinen varmenne on määritetty, tämä sisältää {{Metadata Certificate Template}} -varmenteen mallin__, muutoin sitä ei oteta huomioon. \n- **\\_\\_callbackUrl\\_\\_**: Rocket.Chatin takaisinkutsun URL-osoite.", + "SAML_Metadata_Template_Description": "Seuraavat muuttujat ovat käytettävissä: \n- **\\_\\_sloLocation\\_\\_**: Rocket.Chat Single LogOut URL-osoite. \n- **\\_\\_issuer\\_\\_**: {{Custom Issuer}} -asetuksen arvo. \n- **\\_\\_identifierFormat\\_\\_**: {{Identifier Format}} -asetuksen arvo. \n- **\\_\\_certificateTag\\_\\_**: Jos yksityinen varmenne on määritetty, tämä sisältää {{Metadata Certificate Template}} -varmenteen mallin__, muutoin sitä ei oteta huomioon. \n- **\\_\\_callbackUrl\\_\\_**: Rocket.Chatin takaisinkutsun URL-osoite.", "SAML_MetadataCertificate_Template": "Metadatan varmenteen malli", "SAML_NameIdPolicy_Template": "NameID Policy malli", "SAML_NameIdPolicy_Template_Description": "Voit käyttää mitä tahansa muuttujaa Authorize Request Template -mallista.", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json index 816a042332a8..f239fcc0fc74 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json @@ -2569,7 +2569,7 @@ "leave-c_description": "Autorisation de quitter les canaux", "leave-p": "Quitter les groupes privés", "leave-p_description": "Autorisation de quitter les groupes privés", - "Lets_get_you_new_one": "Nous allons vous en fournir un nouveau", + "Lets_get_you_new_one_": "Nous allons vous en fournir un nouveau", "List_of_Channels": "Liste des canaux", "List_of_departments_for_forward": "Liste des départements autorisés pour le transfert (optionnel)", "List_of_departments_for_forward_description": "Autoriser à définir une liste restreinte de départements qui peuvent recevoir des chats de ce département", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json index 670e353850f0..0f9e12f8b599 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json @@ -2801,7 +2801,7 @@ "leave-c_description": "Jogosultság a csatornák elhagyásához", "leave-p": "Személyes csoportok elhagyása", "leave-p_description": "Jogosultság a személyes csoportok elhagyásához", - "Lets_get_you_new_one": "Had adjunk Önnek egy újat!", + "Lets_get_you_new_one_": "Had adjunk Önnek egy újat!", "License": "Licenc", "Link_Preview": "Hivatkozás előnézete", "List_of_Channels": "Csatornák listája", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json index adda184a03b8..4a656c8b3337 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json @@ -21,7 +21,7 @@ "24_Hour": "Orologio 24 ore", "A_new_owner_will_be_assigned_automatically_to__count__rooms": "Un nuovo proprietario verrà assegnato automaticamente a{{count}}stanze.", "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "Un nuovo proprietario verrà assegnato automaticamente alla stanza {{roomName}}.", - "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "Un nuovo proprietario verrà assegnato automaticamente a queste_count__stanze:
__rooms__.", + "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "Un nuovo proprietario verrà assegnato automaticamente a queste_count__stanze:
{{rooms}}.", "Accept_Call": "Accetta la chiamata", "Accept": "Accetta", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Accetta richieste livechat in arrivo anche se non c'è alcun operatore online", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json index 80b1dcafa5d1..9487983bf9f7 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json @@ -2546,7 +2546,7 @@ "leave-c_description": "チャネルから退出する権限", "leave-p": "プライベートグループから退出", "leave-p_description": "プライベートグループから退出する権限", - "Lets_get_you_new_one": "新たな挑戦をしてみましょう!", + "Lets_get_you_new_one_": "新たな挑戦をしてみましょう!", "List_of_Channels": "Channel一覧", "List_of_departments_for_forward": "転送が許可されている部署の一覧(オプション)", "List_of_departments_for_forward_description": "この部署からチャットを受信できる部署の制限リストを設定することを許可します", @@ -3615,7 +3615,7 @@ "SAML_General": "一般", "SAML_Custom_Authn_Context": "カスタム認証コンテキスト", "SAML_Custom_Authn_Context_Comparison": "認証コンテキストの比較", - "SAML_Custom_Authn_Context_description": "要求からauthnコンテキストを除外するには、これを空のままにします。 \n \n複数の認証コンテキストを追加するには、__AuthnContextTemplate__設定に直接追加します。", + "SAML_Custom_Authn_Context_description": "要求からauthnコンテキストを除外するには、これを空のままにします。 \n \n複数の認証コンテキストを追加するには、{{AuthnContext Template}}設定に直接追加します。", "SAML_Custom_Cert": "カスタム証明書", "SAML_Custom_Debug": "デバッグを有効にする", "SAML_Custom_EMail_Field": "メールのフィールド名", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json index 7f3d31510b6f..8e673e5346cc 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json @@ -2059,7 +2059,7 @@ "Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "დატოვეთ აღწერილობის ველი ცარიელი, თუ არ გსურთ როლის ჩვენება", "leave-c": "დატოვეთ არხები", "leave-p": "დატოვე პირადი ჯგუფები", - "Lets_get_you_new_one": "მიიღეთ ახალი!", + "Lets_get_you_new_one_": "მიიღეთ ახალი!", "List_of_Channels": "არხების სია", "List_of_departments_for_forward": "გასაგზავნად ნებადართული განყოფილებების სია (არჩევითი)", "List_of_departments_for_forward_description": "ნება დართეთ შეიქმნას შეზღუდული სია (განყოფილებების) , რომელთაც შეუძლიათ მიიღონ ჩათები ამ განყოფილებიდან", @@ -2803,8 +2803,8 @@ "Room_archivation_state_false": "აქტიური", "Room_archivation_state_true": "დაარქივებულია", "Room_archived": "ოთახი დაარქივებულია", - "room_changed_announcement": "ოთახის განცხადება შეიცვალა __room_announcement__,__username__-ის მიერ", - "room_changed_description": "ოთახის აღწერა შეიცვალა: __room_description__ __ მომხმარებელი__-ის მიერ ", + "room_changed_announcement": "ოთახის განცხადება შეიცვალა {{room_announcement}},{{username}}-ის მიერ", + "room_changed_description": "ოთახის აღწერა შეიცვალა: {{room_description}} __ მომხმარებელი__-ის მიერ ", "room_changed_topic": "ოთახის თემა შეიცვალა: {{room_topic}} {{user_by}}", "Room_default_change_to_private_will_be_default_no_more": "ეს არის დეფაულტ არხი და პირად ჯგუფად გადაკეთების შემთხვევაში აღარ იქნება დეფაულტ არხი.გსურთ გაგრძელება?", "Room_description_changed_successfully": "ოთახის აღწერა წარმატებით შეიცვალა", @@ -3247,7 +3247,7 @@ "This_room_has_been_archived_by__username_": "ეს ოთახი დაარქივდა {{username}}-ის მიერ", "This_room_has_been_unarchived_by__username_": "ეს ოთახი ამოარქივდა {{username}}-ის მიერ", "This_week": "ეს კვირა", - "Thread_message": "კომენტარი გააკეთა * __ მომხმარებლის __ ის გზავნილზე: _ __msg__ _", + "Thread_message": "კომენტარი გააკეთა * __ მომხმარებლის __ ის გზავნილზე: _ {{msg}} _", "Thursday": "ხუთშაბათი", "Time_in_seconds": "დრო წამებში", "Timeouts": "თაიმაუტები", @@ -3432,7 +3432,7 @@ "User_removed_by": "მომხმარებელი {{user_removed}} {{user_by}}.", "User_sent_a_message_on_channel": "{{username}} შეტყობინების გაგზავნა {{channel}}", "User_sent_a_message_to_you": "{{username}} გამოგიგზავნათ შეტყობინება", - "user_sent_an_attachment": "__username__– მა გაგზავნა დანართი", + "user_sent_an_attachment": "{{username}}– მა გაგზავნა დანართი", "User_Settings": "მომხმარებლის პარამეტრები", "User_started_a_new_conversation": "{{username}}– მა დაიწყო ახალი საუბარი", "User_unmuted_by": "მომხმარებელი {{user_unmuted}} {{user_by}}.", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json index 8acc4f3c84d4..b58c8c5c5dc1 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json @@ -1815,7 +1815,7 @@ "Leave_the_current_channel": "ចាកចេញពីឆានែលបច្ចុប្បន្ន", "leave-c": "ចាកចេញពីឆានែល", "leave-p": "ចាកចេញពីក្រុមឯកជន", - "Lets_get_you_new_one": "ចូរយើងទទួលបានអ្នកថ្មី!", + "Lets_get_you_new_one_": "ចូរយើងទទួលបានអ្នកថ្មី!", "List_of_Channels": "បញ្ជីឆានែល", "List_of_Direct_Messages": "បញ្ជីនៃការផ្ញើសារដោយផ្ទាល់", "Livechat": "Livechat", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json index 94c8816e57b2..0341f6388348 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json @@ -2227,7 +2227,7 @@ "Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "역할을 표시하지 않으려면 설명 필드를 비워두세요.", "leave-c": "Channel 나가기", "leave-p": "비공개 그룹에서 나가기", - "Lets_get_you_new_one": "새로 생성", + "Lets_get_you_new_one_": "새로 생성", "List_of_Channels": "Channel 목록", "List_of_departments_for_forward": "전달이 허용 된 부서 목록 (선택 사항)", "List_of_departments_for_forward_description": "이 부서에서 채팅을 받을 수 있는 제한된 부서 목록을 설정하도록 허용", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json index ebf318f3aa07..19da31a1b2e5 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json @@ -2555,15 +2555,15 @@ "User_Presence": "Хэрэглэгчийн байдал", "User_removed": "Хэрэглэгч устгагдсан", "User_removed_by": "Хэрэглэгч {{user_removed}}хасагдсан {{user_by}}.", - "User_sent_a_message_on_channel": "__усername__мессеж илгээгдсэн __channel__", + "User_sent_a_message_on_channel": "{{username}} мессеж илгээгдсэн {{channel}}", "User_sent_a_message_to_you": "__зориулагчтанд зурвас илгээж байна", "user_sent_an_attachment": "{{user}} хавсралтыг илгээсэн", "User_Settings": "Хэрэглэгчийн тохиргоо", "User_unmuted_by": "Хэрэглэгч {{user_unmuted}}нээгдсэн {{user_by}}.", "User_unmuted_in_room": "Хэрэглэгчид унтаагүй байна", "User_updated_successfully": "Хэрэглэгч шинэчлэгдсэн", - "User_uploaded_a_file_on_channel": "__усername____channel__ дээр файл байршуулсан", - "User_uploaded_a_file_to_you": "__усername__танд файл илгээв", + "User_uploaded_a_file_on_channel": "{{username}} {{channel}} дээр файл байршуулсан", + "User_uploaded_a_file_to_you": "{{username}} танд файл илгээв", "User_uploaded_file": "Файлыг байршуулсан", "User_uploaded_image": "Зургийг байршуулсан", "user-generate-access-token": "Хэрэглэгч нэвтрэх тэмдгийг үүсгэнэ", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json index dcc6057f2efc..8d2cb17ea0d2 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json @@ -1,6 +1,6 @@ { "500": "Ralat Pelayan Dalaman", - "__username__was_set__role__by__user_by_": "__nama pengguna__ ditubuhkan __role__ oleh __user_by__", + "__username__was_set__role__by__user_by_": "__nama pengguna__ ditubuhkan {{role}} oleh {{user_by}}", "@username": "@pengguna", "@username_message": "@username ", "#channel": "#channel", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json index 25b0a24ca538..23e356f28eee 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json @@ -2563,7 +2563,7 @@ "leave-c_description": "Toestemming om kanalen te verlaten", "leave-p": "Verlaat privégroepen", "leave-p_description": "Toestemming om privégroepen te verlaten", - "Lets_get_you_new_one": "Laten we een nieuwe voor je regelen!", + "Lets_get_you_new_one_": "Laten we een nieuwe voor je regelen!", "List_of_Channels": "Lijst met kanalen", "List_of_departments_for_forward": "Lijst met afdelingen die mogen worden doorgestuurd (optioneel)", "List_of_departments_for_forward_description": "Sta toe om een beperkte lijst van afdelingen in te stellen die chats van deze afdeling kunnen ontvangen", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json index 3c315fcd5b99..aa8ca813b445 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json @@ -2761,7 +2761,7 @@ "leave-c_description": "Zezwolenie na opuszczenie kanałów", "leave-p": "Opuść grupy prywatne", "leave-p_description": "Zezwolenie na opuszczenie grup prywatnych", - "Lets_get_you_new_one": "Zróbmy ci nową!", + "Lets_get_you_new_one_": "Zróbmy ci nową!", "License": "Licencja", "Link_Preview": "Podgląd linków", "List_of_Channels": "Lista kanałów", @@ -5163,9 +5163,9 @@ "You": "ty", "You_reacted_with": "Zareagowałeś z {{emoji}}", "Users_reacted_with": "{{users}} zareagowali z {{emoji}}", - "Users_and_more_reacted_with": "__users__ i __count__ więcej zareagowali z __emoji__", + "Users_and_more_reacted_with": "{{users}} i {{count}} więcej zareagowali z {{emoji}}", "You_and_users_Reacted_with": "Ty i {{users}} zareagowali z {{emoji}}", - "You_users_and_more_Reacted_with": "Ty, __users__ i __count__ więcej zareagowali z __emoji__", + "You_users_and_more_Reacted_with": "Ty, {{users}} i {{count}} więcej zareagowali z {{emoji}}", "You_are_converting_team_to_channel": "Przekształcasz ten zespół w kanał.", "you_are_in_preview_mode_of": "Jesteś w trybie podglądu kanału # {{room_name}}", "you_are_in_preview_mode_of_incoming_livechat": "Jesteś w trybie podglądu wiadomości przychodzącej livechat", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 90ed0d6db384..4daea7308d8a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -2618,7 +2618,7 @@ "leave-c_description": "Permissão para deixar canais", "leave-p": "Deixar grupos privados", "leave-p_description": "Permissão para deixar grupos privados", - "Lets_get_you_new_one": "Vamos pegar outro!", + "Lets_get_you_new_one_": "Vamos pegar outro!", "List_of_Channels": "Lista de Canais", "List_of_departments_for_forward": "Lista de departamentos permitidos para encaminhamento (opcional).", "List_of_departments_for_forward_description": "Permite definir uma lista restrita de departamentos que podem receber conversas deste departamento.", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json index 367c9c2cd97b..9bc920849c8c 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json @@ -1803,7 +1803,7 @@ "Leave_the_current_channel": "Sai deste canal", "leave-c": "Sair dos canais", "leave-p": "Sair dos grupos privados", - "Lets_get_you_new_one": "Vamos pegar uma nova!", + "Lets_get_you_new_one_": "Vamos pegar uma nova!", "List_of_Channels": "Lista de Canais", "List_of_Direct_Messages": "Lista de Mensagens Diretas", "Livechat": "Livechat", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json index e1699812a3d9..e3dc8d0a5791 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json @@ -2710,7 +2710,7 @@ "leave-c_description": "Разрешение покидать каналы", "leave-p": "Оставить личные группы", "leave-p_description": "Разрешение покидать приватные группы", - "Lets_get_you_new_one": "Давайте получим новый!", + "Lets_get_you_new_one_": "Давайте получим новый!", "License": "Лицензия", "List_of_Channels": "Список чатов", "List_of_departments_for_forward": "Список департаментов, разрешенных к перенаправлению (необязательно)", @@ -3841,8 +3841,8 @@ "SAML_LogoutRequest_Template": "Шаблон запроса на выход из системы", "SAML_LogoutRequest_Template_Description": "Доступны следующие переменные: \n- **\\_\\_\\_newId\\_\\_**: Случайно сгенерированная строка идентификатора \n- **\\_\\_\\_\\_стоянная\\_\\_**: Текущая метка времени \n- **\\_\\_idpSLORedirectURL\\_\\_**: URL IDP Single LogOut для перенаправления. \n- **\\_\\_\\_\\_issuer\\_\\_**: Значение параметра {{Custom Issuer}}. \n- **\\_\\_identifierFormat\\_\\_**: Значение параметра __Формат идентификатора__. \n- **\\_\\_\\_\\_nameID\\_\\_\\_**: Идентификатор имени, полученный от IdP, когда пользователь вошел в систему. \n- **\\_\\_sessionIndex\\_\\_**: Индекс сессии, полученный от IdP, когда пользователь вошел в систему.", "SAML_LogoutResponse_Template": "Шаблон выхода из системы", - "SAML_LogoutResponse_Template_Description": "Доступны следующие переменные: \n- **__newId__**: Случайно сгенерированная идентификационная строка \n- **__inResponseToId__**: Идентификатор запроса на выход из системы, полученный от IdP \n- **instant_**: Текущая метка времени \n- **__idpSLORedirectURL__**: URL одиночного входа в систему IDP для переадресации. \n- **issuer_**: Значение параметра {{Custom Issuer}}. \n- **{{identifierFormat}}**: Значение параметра {{Identifier Format}}. \n- **__nameID___**: Идентификатор имени, полученный из запроса на выход из системы IdP. \n- **__sessionIndex__**: СессияИндекс, полученный из запроса на выход из системы IdP.", - "SAML_Metadata_Certificate_Template_Description": "Доступны следующие переменные: \n- **__certificate__**: Частный сертификат для шифрования утверждения.", + "SAML_LogoutResponse_Template_Description": "Доступны следующие переменные: \n- **\\_\\_newId\\_\\_**: Случайно сгенерированная идентификационная строка \n- **\\_\\_inResponseToId\\_\\_**: Идентификатор запроса на выход из системы, полученный от IdP \n- **\\_\\_instant\\_\\_**: Текущая метка времени \n- **\\_\\_idpSLORedirectURL\\_\\_**: URL одиночного входа в систему IDP для переадресации. \n- **\\_\\_issuer\\_\\_**: Значение параметра {{Custom Issuer}}. \n- **\\_\\_identifierFormat\\_\\_**: Значение параметра {{Identifier Format}}. \n- **\\_\\_nameID\\_\\_**: Идентификатор имени, полученный из запроса на выход из системы IdP. \n- **\\_\\_sessionIndex\\_\\_**: СессияИндекс, полученный из запроса на выход из системы IdP.", + "SAML_Metadata_Certificate_Template_Description": "Доступны следующие переменные: \n- **\\_\\_certificate\\_\\_**: Частный сертификат для шифрования утверждения.", "SAML_Metadata_Template": "Шаблон метаданных", "SAML_Metadata_Template_Description": "Доступны следующие переменные: \n- **\\_\\_sloLocation\\_\\_**:URL одиночного входа в систему Rocket.Chat. \n- **\\__\\issuer\\__\\_**: Значение параметра {{Custom Issuer}}. \n- **\\_\\_identifierFormat\\_\\_**: Значение параметра {{Identifier Format}}. \n- **\\__\\certificateTag\\__\\_**: Если настроен личный сертификат, он будет включать {{Metadata Certificate Template}}, в противном случае он будет проигнорирован. \n- **\\__\\callbackUrl\\__\\_**: URL обратного вызова Rocket.Chat.", "SAML_MetadataCertificate_Template": "Шаблон сертификата метаданных", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json index 01ef2e16e50c..94964cee15d6 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json @@ -2909,7 +2909,7 @@ "leave-c_description": "Behörighet att lämna kanaler", "leave-p": "Lämna privata grupper", "leave-p_description": "Tillstånd att lämna privata grupper", - "Lets_get_you_new_one": "Vi ordnar en ny.", + "Lets_get_you_new_one_": "Vi ordnar en ny.", "License": "Licens", "Line": "Linje", "Link": "Länk", @@ -5732,7 +5732,7 @@ "RegisterWorkspace_Token_Step_Two": "Kopiera ditt token och klistra in det nedan.", "RegisterWorkspace_with_email": "Registrera arbetsytan med e-post", "RegisterWorkspace_Setup_Subtitle": "För att registrera arbetsytan måste det associeras med ett Rocket.Chat Cloud-konto.", - "RegisterWorkspace_Setup_Steps": "Steg __steg__ av __numberOfSteps__", + "RegisterWorkspace_Setup_Steps": "Steg {{step}} av {{numberOfSteps}}", "RegisterWorkspace_Setup_Label": "E-postadress för molnkonto", "RegisterWorkspace_Setup_Have_Account_Title": "Har du ett konto?", "RegisterWorkspace_Setup_Have_Account_Subtitle": "Ange din e-postadress till Cloud-kontot för att koppla arbetsytan till ditt konto.", @@ -5747,7 +5747,7 @@ "cloud.RegisterWorkspace_Setup_Terms_Privacy": "Jag godkänner <1>villkoren och <3>integritetspolicyn", "Larger_amounts_of_active_connections": "För större mängder aktiva anslutningar kan du överväga vår", "Uninstall_grandfathered_app": "Avinstallera {{appName}}?", - "App_will_lose_grandfathered_status": "**Denna {{context}}-app kommer att förlora sin status som gammal app.** \n \nArbetsytorna i Community Edition kan ha upp till {{limit}} __kontext__-appar aktiverade. Gamla appar inkluderas i gränsen, men gränsen tillämpas inte på dem.", + "App_will_lose_grandfathered_status": "**Denna {{context}}-app kommer att förlora sin status som gammal app.** \n \nArbetsytorna i Community Edition kan ha upp till {{limit}} {{context}}-appar aktiverade. Gamla appar inkluderas i gränsen, men gränsen tillämpas inte på dem.", "Theme_Appearence": "Utseende för tema", "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json index 33ed4e6d83df..89366d9df2a0 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json @@ -2524,7 +2524,7 @@ "Use_url_for_avatar": "சின்னம் URL ஐ பயன்படுத்த", "Use_User_Preferences_or_Global_Settings": "பயனர் விருப்பங்கள் அல்லது உலகளாவிய அமைப்புகள் பயன்படுத்தவும்", "User": "பயனர்", - "User__username__is_now_a_leader_of__room_name_": "பயனர் __இயக்குநர்__ இப்போது __room_name__ இன் தலைவர்", + "User__username__is_now_a_leader_of__room_name_": "பயனர் {{username}} இப்போது {{room_name}} இன் தலைவர்", "User__username__is_now_a_moderator_of__room_name_": "பயனர் {{username}} இப்போது {{room_name}} ஒரு மதிப்பீட்டாளர்", "User__username__is_now_an_owner_of__room_name_": "பயனர் {{username}} இப்போது {{room_name}} ஒரு உரிமையாளர் ஆவார்", "User__username__removed_from__room_name__leaders": "{{room_name}} தலைவர்களிடமிருந்து பயனர் {{username}} நீக்கப்பட்டது", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json index 3485f01cd190..9f969914d5d0 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json @@ -1833,7 +1833,7 @@ "Leave_the_current_channel": "Geçerli kanalı bırak", "leave-c": "Kanallardan Çık", "leave-p": "Özel Grupları Bırak", - "Lets_get_you_new_one": "Size yeni bir tane verelim!", + "Lets_get_you_new_one_": "Size yeni bir tane verelim!", "List_of_Channels": "Kanal Listesi", "List_of_Direct_Messages": "Doğrudan İletiler Listesi", "Livechat": "Canlı Görüşme", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json index c1a4796794db..8bbb614c560b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json @@ -2517,7 +2517,7 @@ "Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "如果不想顯示角色,請將描述欄位保持空白", "leave-c": "保留 Channel", "leave-p": "離開私人群組", - "Lets_get_you_new_one": "來取得新的!", + "Lets_get_you_new_one_": "來取得新的!", "List_of_Channels": "Channel 列表", "List_of_departments_for_forward": "允許轉送的部門列表(可選)", "List_of_departments_for_forward_description": "允許設定可以接收從此部門聊天記錄部門的受限列表", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json index 1a698984199d..7d4a50c8e362 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json @@ -2265,7 +2265,7 @@ "Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "如果不想显示对应角色,请将描述字段留空", "leave-c": "保留频道", "leave-p": "离开私人组", - "Lets_get_you_new_one": "新版本即将到来", + "Lets_get_you_new_one_": "新版本即将到来", "List_of_Channels": "频道列表", "List_of_departments_for_forward": "允许转发的部门列表(可选)", "List_of_departments_for_forward_description": "允许设置一个列表来限制可从此部门接收聊天的部门", diff --git a/apps/meteor/server/lib/i18n.ts b/apps/meteor/server/lib/i18n.ts index 265305ef71d6..bc3ed6184937 100644 --- a/apps/meteor/server/lib/i18n.ts +++ b/apps/meteor/server/lib/i18n.ts @@ -2,13 +2,20 @@ import type { RocketchatI18nKeys } from '@rocket.chat/i18n'; import i18nDict from '@rocket.chat/i18n'; import type { TOptions } from 'i18next'; -import { i18n } from '../../app/utils/lib/i18n'; +import { availableTranslationNamespaces, defaultTranslationNamespace, extractTranslationNamespaces, i18n } from '../../app/utils/lib/i18n'; void i18n.init({ lng: 'en', - defaultNS: 'core', - resources: Object.fromEntries(Object.entries(i18nDict).map(([key, value]) => [key, { core: value }])), - initImmediate: true, + defaultNS: defaultTranslationNamespace, + ns: availableTranslationNamespaces, + nsSeparator: '.', + resources: Object.fromEntries( + Object.entries(i18nDict).map(([language, source]) => [ + language, + extractTranslationNamespaces(source as unknown as Record), + ]), + ), + initImmediate: false, }); declare module 'i18next' { diff --git a/apps/meteor/server/settings/email.ts b/apps/meteor/server/settings/email.ts index 9de6ecf7efc0..fdfd43c7557a 100644 --- a/apps/meteor/server/settings/email.ts +++ b/apps/meteor/server/settings/email.ts @@ -499,7 +499,7 @@ export const createEmailSettings = () => await this.add( 'Forgot_Password_Email', - '

{Forgot_password}

{Lets_get_you_new_one}

{Reset}

{If_you_didnt_ask_for_reset_ignore_this_email}

', + '

{Forgot_password}

{Lets_get_you_new_one_}

{Reset}

{If_you_didnt_ask_for_reset_ignore_this_email}

', { type: 'code', code: 'text/html', diff --git a/packages/i18n/src/index.mjs b/packages/i18n/src/index.mjs index 41f5eb83a4d6..3204c6c3fd80 100644 --- a/packages/i18n/src/index.mjs +++ b/packages/i18n/src/index.mjs @@ -88,9 +88,13 @@ const tds = `export interface RocketchatI18n { ${keys.map((key) => `${JSON.stringify(key)}: string;`).join('\n\t')} } -export declare const dict: Record; +const dict: { + [language: string]: RocketchatI18n; +}; export type RocketchatI18nKeys = keyof RocketchatI18n; + +export = dict; `; const languages = files.map((file) => path.basename(file, '.i18n.json')); diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 37d099dcbce6..b97863140c51 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -6,6 +6,7 @@ "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "~0.31.25", "@rocket.chat/fuselage-hooks": "~0.32.1", + "@rocket.chat/i18n": "workspace:~", "@rocket.chat/rest-typings": "workspace:^", "@types/jest": "~29.5.7", "@types/react": "~17.0.69", @@ -25,6 +26,7 @@ "@rocket.chat/ddp-client": "workspace:^", "@rocket.chat/emitter": "*", "@rocket.chat/fuselage-hooks": "*", + "@rocket.chat/i18n": "workspace:~", "@rocket.chat/rest-typings": "workspace:^", "react": "~17.0.2", "use-sync-external-store": "^1.2.0" diff --git a/packages/ui-contexts/src/TranslationContext.ts b/packages/ui-contexts/src/TranslationContext.ts index b73428932d8f..d8fa4423cc27 100644 --- a/packages/ui-contexts/src/TranslationContext.ts +++ b/packages/ui-contexts/src/TranslationContext.ts @@ -1,9 +1,6 @@ +import type { RocketchatI18nKeys } from '@rocket.chat/i18n'; import { createContext } from 'react'; -import type keys from './en.json'; - -export { keys }; - export type TranslationLanguage = { en: string; name: string; @@ -11,7 +8,7 @@ export type TranslationLanguage = { key: string; }; -export type TranslationKey = keyof typeof keys | `app-${string}.${string}`; +export type TranslationKey = RocketchatI18nKeys | `app-${string}.${string}`; export type TranslationContextValue = { languages: TranslationLanguage[]; diff --git a/packages/ui-contexts/src/en.json b/packages/ui-contexts/src/en.json deleted file mode 120000 index 2d8842c5138a..000000000000 --- a/packages/ui-contexts/src/en.json +++ /dev/null @@ -1 +0,0 @@ -../../../apps/meteor/private/i18n/en.i18n.json \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index b3d14ad61044..17350a085065 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9586,6 +9586,7 @@ __metadata: "@types/strict-uri-encode": ^2.0.1 "@types/string-strip-html": ^5.0.1 "@types/supertest": ^2.0.15 + "@types/supports-color": ~7.2.0 "@types/textarea-caret": ^3.0.2 "@types/ua-parser-js": ^0.7.38 "@types/use-subscription": ^1.0.1 @@ -9770,6 +9771,7 @@ __metadata: stylelint: ^14.9.1 stylelint-order: ^5.0.0 supertest: ^6.2.3 + supports-color: ~7.2.0 suretype: ~2.4.1 swiper: ^9.3.2 tar-stream: ^1.6.2 @@ -10387,6 +10389,7 @@ __metadata: "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": ~0.31.25 "@rocket.chat/fuselage-hooks": ~0.32.1 + "@rocket.chat/i18n": "workspace:~" "@rocket.chat/password-policies": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@types/jest": ~29.5.7 @@ -10406,6 +10409,7 @@ __metadata: "@rocket.chat/ddp-client": "workspace:^" "@rocket.chat/emitter": "*" "@rocket.chat/fuselage-hooks": "*" + "@rocket.chat/i18n": "workspace:~" "@rocket.chat/rest-typings": "workspace:^" react: ~17.0.2 use-sync-external-store: ^1.2.0 @@ -14192,6 +14196,13 @@ __metadata: languageName: node linkType: hard +"@types/supports-color@npm:~7.2.0": + version: 7.2.1 + resolution: "@types/supports-color@npm:7.2.1" + checksum: abf7d9348deadf5386cf5faec062a4132e647a179584f52cace87435248f520be73c58ac28618cf5684e6b0ed6bb635d5a975cc71ff613af7db2d5648557ef45 + languageName: node + linkType: hard + "@types/tapable@npm:^1, @types/tapable@npm:^1.0.5": version: 1.0.8 resolution: "@types/tapable@npm:1.0.8" @@ -38182,7 +38193,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0, supports-color@npm:^7.2.0": +"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0, supports-color@npm:^7.2.0, supports-color@npm:~7.2.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" dependencies: From 116ad559c2019b3b98b645a12d0b5df3a8bdd310 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Thu, 11 Jan 2024 15:31:16 -0300 Subject: [PATCH 041/152] chore: Replace `Dropdown` in favor of `MenuV2` on `MessageComposer` (#31374) --- .../components/GenericMenu/GenericMenu.tsx | 9 +- .../GenericMenu/GenericMenuItem.tsx | 4 +- .../ActionsToolbarDropdown.tsx | 30 ---- .../MessageBoxActionsToolbar.tsx | 162 ++++++++++++------ .../hooks/ToolbarAction.ts | 10 -- .../hooks/useAudioMessageAction.ts | 12 +- .../hooks/useCreateDiscussionAction.tsx | 15 +- .../hooks/useFileUploadAction.ts | 7 +- .../hooks/useShareLocationAction.tsx | 9 +- .../hooks/useToolbarActions.ts | 112 ------------ .../hooks/useVideoMessageAction.ts | 12 +- .../hooks/useWebdavActions.tsx | 24 ++- .../FormattingToolbarDropdown.tsx | 67 +++----- .../MessageBoxFormattingToolbar.tsx | 15 +- apps/meteor/package.json | 2 +- .../rocketchat-i18n/i18n/en.i18n.json | 2 + .../meteor/tests/e2e/message-composer.spec.ts | 37 ++++ .../tests/e2e/page-objects/home-channel.ts | 4 + ee/packages/ui-theming/package.json | 2 +- packages/fuselage-ui-kit/package.json | 2 +- packages/gazzodown/package.json | 2 +- packages/ui-client/package.json | 2 +- packages/ui-composer/package.json | 2 +- .../MessageComposerToolbar.tsx | 2 +- packages/ui-video-conf/package.json | 2 +- packages/uikit-playground/package.json | 2 +- yarn.lock | 24 +-- 27 files changed, 249 insertions(+), 324 deletions(-) delete mode 100644 apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/ActionsToolbarDropdown.tsx delete mode 100644 apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/ToolbarAction.ts delete mode 100644 apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useToolbarActions.ts create mode 100644 apps/meteor/tests/e2e/message-composer.spec.ts diff --git a/apps/meteor/client/components/GenericMenu/GenericMenu.tsx b/apps/meteor/client/components/GenericMenu/GenericMenu.tsx index f660b4b85f35..9d8367f7ad98 100644 --- a/apps/meteor/client/components/GenericMenu/GenericMenu.tsx +++ b/apps/meteor/client/components/GenericMenu/GenericMenu.tsx @@ -8,8 +8,9 @@ import GenericMenuItem from './GenericMenuItem'; import { useHandleMenuAction } from './hooks/useHandleMenuAction'; type GenericMenuCommonProps = { - icon?: ComponentProps['icon']; title: string; + icon?: ComponentProps['icon']; + disabled?: boolean; }; type GenericMenuConditionalProps = | { @@ -27,7 +28,7 @@ type GenericMenuConditionalProps = type GenericMenuProps = GenericMenuCommonProps & GenericMenuConditionalProps & Omit, 'children'>; -const GenericMenu = ({ title, icon = 'menu', onAction, ...props }: GenericMenuProps) => { +const GenericMenu = ({ title, icon = 'menu', disabled, onAction, ...props }: GenericMenuProps) => { const t = useTranslation(); const sections = 'sections' in props && props.sections; @@ -44,8 +45,8 @@ const GenericMenu = ({ title, icon = 'menu', onAction, ...props }: GenericMenuPr const isMenuEmpty = !(sections && sections.length > 0) && !(items && items.length > 0); - if (isMenuEmpty) { - return ; + if (isMenuEmpty || disabled) { + return ; } return ( diff --git a/apps/meteor/client/components/GenericMenu/GenericMenuItem.tsx b/apps/meteor/client/components/GenericMenu/GenericMenuItem.tsx index ae79d5a4a78b..ec987a1ee28d 100644 --- a/apps/meteor/client/components/GenericMenu/GenericMenuItem.tsx +++ b/apps/meteor/client/components/GenericMenu/GenericMenuItem.tsx @@ -1,5 +1,5 @@ import { MenuItemColumn, MenuItemContent, MenuItemIcon, MenuItemInput } from '@rocket.chat/fuselage'; -import type { ComponentProps, ReactNode } from 'react'; +import type { ComponentProps, MouseEvent, ReactNode } from 'react'; import React from 'react'; export type GenericMenuItemProps = { @@ -7,7 +7,7 @@ export type GenericMenuItemProps = { icon?: ComponentProps['name']; content?: ReactNode; addon?: ReactNode; - onClick?: () => void; + onClick?: (e?: MouseEvent) => void; status?: ReactNode; disabled?: boolean; description?: ReactNode; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/ActionsToolbarDropdown.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/ActionsToolbarDropdown.tsx deleted file mode 100644 index 8da907c99c35..000000000000 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/ActionsToolbarDropdown.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Dropdown, IconButton } from '@rocket.chat/fuselage'; -import type { ReactNode } from 'react'; -import React, { useRef } from 'react'; - -import { useDropdownVisibility } from '../../../../../sidebar/header/hooks/useDropdownVisibility'; - -type ActionsToolbarDropdownProps = { - disabled?: boolean; - children: () => ReactNode[]; -}; - -const ActionsToolbarDropdown = ({ children, ...props }: ActionsToolbarDropdownProps) => { - const reference = useRef(null); - const target = useRef(null); - - const { isVisible, toggle } = useDropdownVisibility({ reference, target }); - - return ( - <> - toggle()} {...props} /> - {isVisible && ( - - {children()} - - )} - - ); -}; - -export default ActionsToolbarDropdown; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index ab0c9ec1fd5d..dd4b93edd0ee 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -1,13 +1,23 @@ import type { IRoom, IMessage } from '@rocket.chat/core-typings'; -import { Option, OptionTitle, OptionIcon, OptionContent } from '@rocket.chat/fuselage'; +import type { Icon } from '@rocket.chat/fuselage'; import { MessageComposerAction, MessageComposerActionsDivider } from '@rocket.chat/ui-composer'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import type { ComponentProps } from 'react'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import { useUserRoom, useTranslation, useLayoutHiddenActions } from '@rocket.chat/ui-contexts'; +import type { ComponentProps, MouseEvent } from 'react'; import React, { memo } from 'react'; +import { messageBox } from '../../../../../../app/ui-utils/client'; +import { isTruthy } from '../../../../../../lib/isTruthy'; +import GenericMenu from '../../../../../components/GenericMenu/GenericMenu'; +import type { GenericMenuItemProps } from '../../../../../components/GenericMenu/GenericMenuItem'; +import { useMessageboxAppsActionButtons } from '../../../../../hooks/useAppActionButtons'; import { useChat } from '../../../contexts/ChatContext'; -import ActionsToolbarDropdown from './ActionsToolbarDropdown'; -import { useToolbarActions } from './hooks/useToolbarActions'; +import { useAudioMessageAction } from './hooks/useAudioMessageAction'; +import { useCreateDiscussionAction } from './hooks/useCreateDiscussionAction'; +import { useFileUploadAction } from './hooks/useFileUploadAction'; +import { useShareLocationAction } from './hooks/useShareLocationAction'; +import { useVideoMessageAction } from './hooks/useVideoMessageAction'; +import { useWebdavActions } from './hooks/useWebdavActions'; type MessageBoxActionsToolbarProps = { canSend: boolean; @@ -19,6 +29,13 @@ type MessageBoxActionsToolbarProps = { tmid?: IMessage['_id']; }; +const isHidden = (hiddenActions: Array, action: GenericMenuItemProps) => { + if (!action) { + return true; + } + return hiddenActions.includes(action.id); +}; + const MessageBoxActionsToolbar = ({ canSend, typing, @@ -28,17 +45,6 @@ const MessageBoxActionsToolbar = ({ variant = 'large', isMicrophoneDenied, }: MessageBoxActionsToolbarProps) => { - const data = useToolbarActions({ - canSend, - typing, - isRecording, - isMicrophoneDenied: Boolean(isMicrophoneDenied), - rid, - tmid, - variant, - }); - - const { featured, menu } = data; const t = useTranslation(); const chatContext = useChat(); @@ -46,46 +52,100 @@ const MessageBoxActionsToolbar = ({ throw new Error('useChat must be used within a ChatProvider'); } - if (!featured.length && !menu.length) { - return null; + const room = useUserRoom(rid); + + const audioMessageAction = useAudioMessageAction(!canSend || typing || isRecording || isMicrophoneDenied, isMicrophoneDenied); + const videoMessageAction = useVideoMessageAction(!canSend || typing || isRecording); + const fileUploadAction = useFileUploadAction(!canSend || typing || isRecording); + const webdavActions = useWebdavActions(); + const createDiscussionAction = useCreateDiscussionAction(room); + const shareLocationAction = useShareLocationAction(room, tmid); + + const apps = useMessageboxAppsActionButtons(); + const { composerToolbox: hiddenActions } = useLayoutHiddenActions(); + + const allActions = { + ...(!isHidden(hiddenActions, audioMessageAction) && { audioMessageAction }), + ...(!isHidden(hiddenActions, videoMessageAction) && { videoMessageAction }), + ...(!isHidden(hiddenActions, fileUploadAction) && { fileUploadAction }), + ...(!isHidden(hiddenActions, createDiscussionAction) && { createDiscussionAction }), + ...(!isHidden(hiddenActions, shareLocationAction) && { shareLocationAction }), + ...(!hiddenActions.includes('webdav-add') && { webdavActions }), + }; + + const featured = []; + const createNew = []; + const share = []; + + createNew.push(allActions.createDiscussionAction); + + if (variant === 'small') { + featured.push(allActions.audioMessageAction); + createNew.push(allActions.videoMessageAction, allActions.fileUploadAction); + } else { + featured.push(allActions.audioMessageAction, allActions.videoMessageAction, allActions.fileUploadAction); } + if (allActions.webdavActions) { + createNew.push(...allActions.webdavActions); + } + + share.push(allActions.shareLocationAction); + + const groups = { + ...(apps.isSuccess && + apps.data.length > 0 && { + Apps: apps.data, + }), + ...messageBox.actions.get(), + }; + + const messageBoxActions = Object.entries(groups).map(([name, group]) => { + const items: GenericMenuItemProps[] = group + .filter((item) => !hiddenActions.includes(item.id)) + .map((item) => ({ + id: item.id, + icon: item.icon as ComponentProps['name'], + content: t(item.label), + onClick: (event?: MouseEvent) => + item.action({ + rid, + tmid, + event: event as unknown as Event, + chat: chatContext, + }), + gap: Boolean(!item.icon), + })); + + return { + title: t(name as TranslationKey), + items: items || [], + }; + }); + + const createNewFiltered = createNew.filter(isTruthy); + const shareFiltered = share.filter(isTruthy); + + const renderAction = ({ id, icon, content, disabled, onClick }: GenericMenuItemProps) => { + if (!icon) { + return; + } + + return ; + }; + return ( <> - {featured.map((action) => ( - - ))} - {menu.length > 0 && ( - - {() => - menu.map((option) => { - if (typeof option === 'string') { - return {t.has(option) ? t(option) : option}; - } - - return ( - - ); - }) - } - - )} + {featured.map((action) => action && renderAction(action))} + ); }; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/ToolbarAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/ToolbarAction.ts deleted file mode 100644 index 63f8ded271f5..000000000000 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/ToolbarAction.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Keys as IconName } from '@rocket.chat/icons'; - -export type ToolbarAction = { - title?: string; - disabled?: boolean; - onClick: (...params: any) => unknown; - icon: IconName; - label: string; - id: string; -}; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useAudioMessageAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useAudioMessageAction.ts index 87ece1793299..58cd7b72189b 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useAudioMessageAction.ts +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useAudioMessageAction.ts @@ -1,22 +1,21 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetting } from '@rocket.chat/ui-contexts'; import { useEffect, useMemo } from 'react'; import { AudioRecorder } from '../../../../../../../app/ui/client/lib/recorderjs/AudioRecorder'; +import type { GenericMenuItemProps } from '../../../../../../components/GenericMenu/GenericMenuItem'; import { useChat } from '../../../../contexts/ChatContext'; import { useMediaActionTitle } from '../../hooks/useMediaActionTitle'; import { useMediaPermissions } from '../../hooks/useMediaPermissions'; -import type { ToolbarAction } from './ToolbarAction'; const audioRecorder = new AudioRecorder(); -export const useAudioMessageAction = (disabled: boolean, isMicrophoneDenied: boolean): ToolbarAction => { +export const useAudioMessageAction = (disabled: boolean, isMicrophoneDenied: boolean): GenericMenuItemProps => { const isFileUploadEnabled = useSetting('FileUpload_Enabled') as boolean; const isAudioRecorderEnabled = useSetting('Message_AudioRecorderEnabled') as boolean; const fileUploadMediaTypeBlackList = useSetting('FileUpload_MediaTypeBlackList') as string; const fileUploadMediaTypeWhiteList = useSetting('FileUpload_MediaTypeWhiteList') as string; const [isPermissionDenied] = useMediaPermissions('microphone'); - const t = useTranslation(); const isAllowed = useMemo( () => @@ -57,10 +56,9 @@ export const useAudioMessageAction = (disabled: boolean, isMicrophoneDenied: boo return { id: 'audio-message', - title: getMediaActionTitle, + content: getMediaActionTitle, + icon: 'mic', disabled: !isAllowed || Boolean(disabled), onClick: handleRecordButtonClick, - icon: 'mic', - label: t('Audio_message'), }; }; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useCreateDiscussionAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useCreateDiscussionAction.tsx index 9b85a8a7a6c3..50702f6887aa 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useCreateDiscussionAction.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useCreateDiscussionAction.tsx @@ -4,16 +4,16 @@ import { useTranslation, useSetting, usePermission, useSetModal } from '@rocket. import React from 'react'; import CreateDiscussion from '../../../../../../components/CreateDiscussion'; -import type { ToolbarAction } from './ToolbarAction'; +import type { GenericMenuItemProps } from '../../../../../../components/GenericMenu/GenericMenuItem'; + +export const useCreateDiscussionAction = (room?: IRoom): GenericMenuItemProps => { + const t = useTranslation(); + const setModal = useSetModal(); -export const useCreateDiscussionAction = (room?: IRoom): ToolbarAction => { if (!room) { throw new Error('Invalid room'); } - const setModal = useSetModal(); - const t = useTranslation(); - const handleCreateDiscussion = () => setModal( setModal(null)} defaultParentRoom={room?.prid || room?._id} />); @@ -25,10 +25,9 @@ export const useCreateDiscussionAction = (room?: IRoom): ToolbarAction => { return { id: 'create-discussion', - title: !allowDiscussion ? t('Not_Available') : undefined, + content: t('Discussion'), + icon: 'discussion', disabled: !allowDiscussion, onClick: handleCreateDiscussion, - icon: 'discussion', - label: t('Discussion'), }; }; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts index 8794aa687b28..aba008a353a5 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts @@ -1,13 +1,13 @@ import { useTranslation, useSetting } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; +import type { GenericMenuItemProps } from '../../../../../../components/GenericMenu/GenericMenuItem'; import { useFileInput } from '../../../../../../hooks/useFileInput'; import { useChat } from '../../../../contexts/ChatContext'; -import type { ToolbarAction } from './ToolbarAction'; const fileInputProps = { type: 'file', multiple: true }; -export const useFileUploadAction = (disabled: boolean): ToolbarAction => { +export const useFileUploadAction = (disabled: boolean): GenericMenuItemProps => { const t = useTranslation(); const fileUploadEnabled = useSetting('FileUpload_Enabled'); const fileInputRef = useFileInput(fileInputProps); @@ -43,9 +43,8 @@ export const useFileUploadAction = (disabled: boolean): ToolbarAction => { return { id: 'file-upload', + content: t('Upload_file'), icon: 'clip', - label: t('File'), - title: t('File'), onClick: handleUpload, disabled: !fileUploadEnabled || disabled, }; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useShareLocationAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useShareLocationAction.tsx index 4a30e3b2b646..bf44496b0d67 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useShareLocationAction.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useShareLocationAction.tsx @@ -1,12 +1,12 @@ -import type { IRoom } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; import { useSetting, useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; +import type { GenericMenuItemProps } from '../../../../../../components/GenericMenu/GenericMenuItem'; import ShareLocationModal from '../../../../ShareLocation/ShareLocationModal'; -import type { ToolbarAction } from './ToolbarAction'; -export const useShareLocationAction = (room?: IRoom, tmid?: string): ToolbarAction => { +export const useShareLocationAction = (room?: IRoom, tmid?: IMessage['tmid']): GenericMenuItemProps => { if (!room) { throw new Error('Invalid room'); } @@ -25,9 +25,8 @@ export const useShareLocationAction = (room?: IRoom, tmid?: string): ToolbarActi return { id: 'share-location', + content: t('Location'), icon: 'map-pin', - label: t('Location'), - title: !allowGeolocation ? t('Not_Available') : undefined, onClick: handleShareLocation, disabled: !allowGeolocation, }; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useToolbarActions.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useToolbarActions.ts deleted file mode 100644 index a98d2e885671..000000000000 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useToolbarActions.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { useUserRoom, useTranslation, useLayoutHiddenActions } from '@rocket.chat/ui-contexts'; - -import { messageBox } from '../../../../../../../app/ui-utils/client'; -import { isTruthy } from '../../../../../../../lib/isTruthy'; -import { useMessageboxAppsActionButtons } from '../../../../../../hooks/useAppActionButtons'; -import type { ToolbarAction } from './ToolbarAction'; -import { useAudioMessageAction } from './useAudioMessageAction'; -import { useCreateDiscussionAction } from './useCreateDiscussionAction'; -import { useFileUploadAction } from './useFileUploadAction'; -import { useShareLocationAction } from './useShareLocationAction'; -import { useVideoMessageAction } from './useVideoMessageAction'; -import { useWebdavActions } from './useWebdavActions'; - -type ToolbarActionsOptions = { - variant: 'small' | 'large'; - canSend: boolean; - typing: boolean; - isRecording: boolean; - isMicrophoneDenied: boolean; - rid: string; - tmid?: string; -}; - -const isHidden = (hiddenActions: Array, action: ToolbarAction) => { - if (!action) { - return true; - } - return hiddenActions.includes(action.id); -}; - -export const useToolbarActions = ({ canSend, typing, isRecording, isMicrophoneDenied, rid, tmid, variant }: ToolbarActionsOptions) => { - const room = useUserRoom(rid); - const t = useTranslation(); - - const videoMessageAction = useVideoMessageAction(!canSend || typing || isRecording); - const audioMessageAction = useAudioMessageAction(!canSend || typing || isRecording || isMicrophoneDenied, isMicrophoneDenied); - const fileUploadAction = useFileUploadAction(!canSend || typing || isRecording); - const webdavActions = useWebdavActions(); - const createDiscussionAction = useCreateDiscussionAction(room); - const shareLocationAction = useShareLocationAction(room, tmid); - - const apps = useMessageboxAppsActionButtons(); - const { composerToolbox: hiddenActions } = useLayoutHiddenActions(); - - const allActions = { - ...(!isHidden(hiddenActions, videoMessageAction) && { videoMessageAction }), - ...(!isHidden(hiddenActions, audioMessageAction) && { audioMessageAction }), - ...(!isHidden(hiddenActions, fileUploadAction) && { fileUploadAction }), - ...(!isHidden(hiddenActions, createDiscussionAction) && { createDiscussionAction }), - ...(!isHidden(hiddenActions, shareLocationAction) && { shareLocationAction }), - ...(!hiddenActions.includes('webdav-add') && { webdavActions }), - }; - - const data: { featured: ToolbarAction[]; menu: Array } = (() => { - const featured: Array = []; - const createNew = []; - const share = []; - - if (variant === 'small') { - featured.push(allActions.audioMessageAction); - createNew.push(allActions.videoMessageAction, allActions.fileUploadAction); - } else { - featured.push(allActions.videoMessageAction, allActions.audioMessageAction, allActions.fileUploadAction); - } - - if (allActions.webdavActions) { - createNew.push(...allActions.webdavActions); - } - - share.push(allActions.shareLocationAction); - - const groups = { - ...(apps.isSuccess && - apps.data.length > 0 && { - Apps: apps.data, - }), - ...messageBox.actions.get(), - }; - - const messageBoxActions = Object.entries(groups).reduce>((acc, [name, group]) => { - const items = group - .filter((item) => !hiddenActions.includes(item.id)) - .map( - (item): ToolbarAction => ({ - id: item.id, - icon: item.icon, - label: t(item.label), - onClick: item.action, - }), - ); - - if (items.length === 0) { - return acc; - } - return [...acc, (t.has(name) && t(name)) || name, ...items]; - }, []); - - const createNewFiltered = createNew.filter(isTruthy); - const shareFiltered = share.filter(isTruthy); - - return { - featured: featured.filter(isTruthy), - menu: [ - ...(createNewFiltered.length > 0 ? ['Create_new', ...createNewFiltered] : []), - ...(shareFiltered.length > 0 ? ['Share', ...shareFiltered] : []), - ...messageBoxActions, - ], - }; - })(); - - return data; -}; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts index 7068f1338b11..cd7e2c5af6fc 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts @@ -1,15 +1,14 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useTranslation, useSetting } from '@rocket.chat/ui-contexts'; +import { useSetting } from '@rocket.chat/ui-contexts'; import { useEffect, useMemo } from 'react'; import { VideoRecorder } from '../../../../../../../app/ui/client/lib/recorderjs/videoRecorder'; +import type { GenericMenuItemProps } from '../../../../../../components/GenericMenu/GenericMenuItem'; import { useChat } from '../../../../contexts/ChatContext'; import { useMediaActionTitle } from '../../hooks/useMediaActionTitle'; import { useMediaPermissions } from '../../hooks/useMediaPermissions'; -import type { ToolbarAction } from './ToolbarAction'; -export const useVideoMessageAction = (disabled: boolean): ToolbarAction => { - const t = useTranslation(); +export const useVideoMessageAction = (disabled: boolean): GenericMenuItemProps => { const isFileUploadEnabled = useSetting('FileUpload_Enabled') as boolean; const isVideoRecorderEnabled = useSetting('Message_VideoRecorderEnabled') as boolean; const fileUploadMediaTypeBlackList = useSetting('FileUpload_MediaTypeBlackList') as string; @@ -55,10 +54,9 @@ export const useVideoMessageAction = (disabled: boolean): ToolbarAction => { return { id: 'video-message', - title: getMediaActionTitle, + content: getMediaActionTitle, + icon: 'video', disabled: !isAllowed || Boolean(disabled), onClick: handleOpenVideoMessage, - icon: 'video', - label: t('Video_message'), }; }; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx index c60d4d533f75..5605f410fa5f 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx @@ -3,25 +3,23 @@ import { useTranslation, useSetting, useSetModal } from '@rocket.chat/ui-context import React from 'react'; import { WebdavAccounts } from '../../../../../../../app/models/client'; +import type { GenericMenuItemProps } from '../../../../../../components/GenericMenu/GenericMenuItem'; import { useReactiveValue } from '../../../../../../hooks/useReactiveValue'; import { useChat } from '../../../../contexts/ChatContext'; import AddWebdavAccountModal from '../../../../webdav/AddWebdavAccountModal'; import WebdavFilePickerModal from '../../../../webdav/WebdavFilePickerModal'; -import type { ToolbarAction } from './ToolbarAction'; const getWebdavAccounts = (): IWebdavAccountIntegration[] => WebdavAccounts.find().fetch(); -export const useWebdavActions = (): Array => { +export const useWebdavActions = (): GenericMenuItemProps[] => { const t = useTranslation(); const setModal = useSetModal(); - const webDavAccounts = useReactiveValue(getWebdavAccounts); - const webDavEnabled = useSetting('Webdav_Integration_Enabled'); - - const handleCreateWebDav = () => setModal( setModal(null)} onConfirm={() => setModal(null)} />); - + const webDavAccounts = useReactiveValue(getWebdavAccounts); const chat = useChat(); + const handleAddWebDav = () => setModal( setModal(null)} onConfirm={() => setModal(null)} />); + const handleUpload = async (file: File, description?: string) => chat?.uploads.send(file, { description, @@ -33,19 +31,17 @@ export const useWebdavActions = (): Array => { return [ { id: 'webdav-add', - title: !webDavEnabled ? t('WebDAV_Integration_Not_Allowed') : undefined, - disabled: !webDavEnabled, - onClick: handleCreateWebDav, + content: t('Add_Server'), icon: 'cloud-plus', - label: t('Add_Server'), + disabled: !webDavEnabled, + onClick: handleAddWebDav, }, ...(webDavEnabled && webDavAccounts.length > 0 ? webDavAccounts.map((account) => ({ id: account._id, - disabled: false, - onClick: () => handleOpenWebdav(account), + content: account.name, icon: 'cloud-plus' as const, - label: account.name, + onClick: () => handleOpenWebdav(account), })) : []), ]; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx index 880bc3f93284..21eb5b94f6d6 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx @@ -1,55 +1,40 @@ -import { Dropdown, IconButton, Option, OptionTitle, OptionIcon, OptionContent } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useRef } from 'react'; +import React from 'react'; import type { FormattingButton } from '../../../../../../app/ui-message/client/messageBox/messageBoxFormatting'; +import GenericMenu from '../../../../../components/GenericMenu/GenericMenu'; +import type { GenericMenuItemProps } from '../../../../../components/GenericMenu/GenericMenuItem'; import type { ComposerAPI } from '../../../../../lib/chats/ChatAPI'; -import { useDropdownVisibility } from '../../../../../sidebar/header/hooks/useDropdownVisibility'; type FormattingToolbarDropdownProps = { composer: ComposerAPI; items: FormattingButton[]; + disabled: boolean; }; -const FormattingToolbarDropdown = ({ composer, items, ...props }: FormattingToolbarDropdownProps) => { +const FormattingToolbarDropdown = ({ composer, items, disabled }: FormattingToolbarDropdownProps) => { const t = useTranslation(); - const reference = useRef(null); - const target = useRef(null); - - const { isVisible, toggle } = useDropdownVisibility({ reference, target }); - - return ( - <> - toggle()} /> - {isVisible && ( - - {t('Message_Formatting_Toolbox')} - {items.map((formatter, index) => { - const handleFormattingAction = () => { - if ('link' in formatter) { - window.open(formatter.link, '_blank', 'rel=noreferrer noopener'); - return; - } - composer.wrapSelection(formatter.pattern); - }; - - return ( - - ); - })} - - )} - - ); + + const formattingItems: GenericMenuItemProps[] = items.map((formatter) => { + const handleFormattingAction = () => { + if ('link' in formatter) { + window.open(formatter.link, '_blank', 'rel=noreferrer noopener'); + return; + } + composer.wrapSelection(formatter.pattern); + }; + + return { + id: `formatter-${formatter.label}`, + content: t(formatter.label), + icon: 'icon' in formatter ? formatter.icon : 'link', + onClick: () => handleFormattingAction(), + }; + }); + + const sections = [{ title: t('Message_Formatting_Toolbox'), items: formattingItems }]; + + return ; }; export default FormattingToolbarDropdown; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/MessageBoxFormattingToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/MessageBoxFormattingToolbar.tsx index fb8d997aaa1f..afe8a58ec72a 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/MessageBoxFormattingToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/MessageBoxFormattingToolbar.tsx @@ -7,13 +7,13 @@ import type { ComposerAPI } from '../../../../../lib/chats/ChatAPI'; import FormattingToolbarDropdown from './FormattingToolbarDropdown'; type MessageBoxFormattingToolbarProps = { - disabled: boolean; - items: FormattingButton[]; composer: ComposerAPI; variant?: 'small' | 'large'; + items: FormattingButton[]; + disabled: boolean; }; -const MessageBoxFormattingToolbar = ({ items, variant = 'large', composer, ...props }: MessageBoxFormattingToolbarProps) => { +const MessageBoxFormattingToolbar = ({ items, variant = 'large', composer, disabled }: MessageBoxFormattingToolbarProps) => { const t = useTranslation(); if (variant === 'small') { @@ -24,12 +24,12 @@ const MessageBoxFormattingToolbar = ({ items, variant = 'large', composer, ...pr <> {'icon' in featuredFormatter && ( composer.wrapSelection(featuredFormatter.pattern)} icon={featuredFormatter.icon} + disabled={disabled} /> )} - + ); } @@ -39,7 +39,7 @@ const MessageBoxFormattingToolbar = ({ items, variant = 'large', composer, ...pr {items.map((formatter) => 'icon' in formatter ? ( ) : ( { + let poHomeChannel: HomeChannel; + let targetChannel: string; + + test.beforeAll(async ({ api }) => { + targetChannel = await createTargetChannel(api); + }); + + test.beforeEach(async ({ page }) => { + poHomeChannel = new HomeChannel(page); + + await page.goto('/home'); + }); + + test('should have all formatters and the main actions visible on toolbar', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.content.sendMessage('hello composer'); + + await expect(poHomeChannel.composerToolboxActions).toHaveCount(11); + }); + + test('should have only the main formatter and the main action', async ({ page }) => { + await page.setViewportSize({ width: 768, height: 600 }); + + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.content.sendMessage('hello composer'); + + await expect(poHomeChannel.composerToolboxActions).toHaveCount(5); + }); +}); diff --git a/apps/meteor/tests/e2e/page-objects/home-channel.ts b/apps/meteor/tests/e2e/page-objects/home-channel.ts index 24403b22b845..8cc5d3245691 100644 --- a/apps/meteor/tests/e2e/page-objects/home-channel.ts +++ b/apps/meteor/tests/e2e/page-objects/home-channel.ts @@ -40,4 +40,8 @@ export class HomeChannel { await this.toastSuccess.locator('button >> i.rcx-icon--name-cross.rcx-icon').click(); await this.page.mouse.move(0, 0); } + + get composerToolboxActions(): Locator { + return this.page.locator('[role=toolbar][aria-label="Composer Primary Actions"] button'); + } } diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index fe3d1af5c36e..3410c74958fc 100644 --- a/ee/packages/ui-theming/package.json +++ b/ee/packages/ui-theming/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.41.0", + "@rocket.chat/fuselage": "^0.42.0", "@rocket.chat/fuselage-hooks": "~0.32.1", "@rocket.chat/icons": "~0.32.0", "@rocket.chat/ui-contexts": "workspace:~", diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 631ab9a2e11d..d4f9f051c0cc 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -62,7 +62,7 @@ "@babel/preset-typescript": "~7.22.15", "@rocket.chat/apps-engine": "1.41.0", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.41.0", + "@rocket.chat/fuselage": "^0.42.0", "@rocket.chat/fuselage-hooks": "~0.32.1", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/icons": "~0.32.0", diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 73aa863da52d..ded010f61656 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.20", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.41.0", + "@rocket.chat/fuselage": "^0.42.0", "@rocket.chat/fuselage-tokens": "~0.32.0", "@rocket.chat/message-parser": "~0.31.27", "@rocket.chat/styled": "~0.31.25", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 3aee938bc7c9..23b8828e4a4c 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -5,7 +5,7 @@ "devDependencies": { "@babel/core": "~7.22.20", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.41.0", + "@rocket.chat/fuselage": "^0.42.0", "@rocket.chat/fuselage-hooks": "~0.32.1", "@rocket.chat/icons": "~0.32.0", "@rocket.chat/mock-providers": "workspace:^", diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index 7ceeee6964e0..f8bb912a4072 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -5,7 +5,7 @@ "devDependencies": { "@babel/core": "~7.22.20", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.41.0", + "@rocket.chat/fuselage": "^0.42.0", "@rocket.chat/icons": "~0.32.0", "@storybook/addon-actions": "~6.5.16", "@storybook/addon-docs": "~6.5.16", diff --git a/packages/ui-composer/src/MessageComposer/MessageComposerToolbar.tsx b/packages/ui-composer/src/MessageComposer/MessageComposerToolbar.tsx index 3521b40aef70..e357356c0050 100644 --- a/packages/ui-composer/src/MessageComposer/MessageComposerToolbar.tsx +++ b/packages/ui-composer/src/MessageComposer/MessageComposerToolbar.tsx @@ -2,7 +2,7 @@ import { Box } from '@rocket.chat/fuselage'; import type { ComponentProps, ReactElement } from 'react'; const MessageComposerToolbar = (props: ComponentProps): ReactElement => ( - + ); export default MessageComposerToolbar; diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 50c860242b6b..c319f4bfd6da 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.20", "@rocket.chat/css-in-js": "~0.31.25", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.41.0", + "@rocket.chat/fuselage": "^0.42.0", "@rocket.chat/fuselage-hooks": "~0.32.1", "@rocket.chat/icons": "~0.32.0", "@rocket.chat/styled": "~0.31.25", diff --git a/packages/uikit-playground/package.json b/packages/uikit-playground/package.json index 60431de57a3b..009eec761168 100644 --- a/packages/uikit-playground/package.json +++ b/packages/uikit-playground/package.json @@ -15,7 +15,7 @@ "@codemirror/tooltip": "^0.19.16", "@lezer/highlight": "^1.1.6", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.41.0", + "@rocket.chat/fuselage": "^0.42.0", "@rocket.chat/fuselage-hooks": "~0.32.1", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/fuselage-tokens": "~0.32.0", diff --git a/yarn.lock b/yarn.lock index 17350a085065..a31fa7be0b8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9047,7 +9047,7 @@ __metadata: "@babel/preset-typescript": ~7.22.15 "@rocket.chat/apps-engine": 1.41.0 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.41.0 + "@rocket.chat/fuselage": ^0.42.0 "@rocket.chat/fuselage-hooks": ~0.32.1 "@rocket.chat/fuselage-polyfills": ~0.31.25 "@rocket.chat/gazzodown": "workspace:^" @@ -9100,9 +9100,9 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/fuselage@npm:^0.41.0": - version: 0.41.0 - resolution: "@rocket.chat/fuselage@npm:0.41.0" +"@rocket.chat/fuselage@npm:^0.42.0": + version: 0.42.0 + resolution: "@rocket.chat/fuselage@npm:0.42.0" dependencies: "@rocket.chat/css-in-js": ^0.31.25 "@rocket.chat/css-supports": ^0.31.25 @@ -9120,7 +9120,7 @@ __metadata: react: ^17.0.2 react-dom: ^17.0.2 react-virtuoso: 1.2.4 - checksum: 8ee33c5626ff7fb8970714696332efc723ca0793470d0b4da9a169aa6b2528ec75068d775e8f149dd3a1ea401ff0240be892022b64bd80e5bd4fc04349e0c0aa + checksum: e9c5869a6b2918ad0490cbcd2b3d7777e8b76bcd9c8d772327af9c83136d99261fcd1266aaa1013b034cbe8a5b88adc3f1383a60c9c2051173c363d0eebae80e languageName: node linkType: hard @@ -9131,7 +9131,7 @@ __metadata: "@babel/core": ~7.22.20 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.41.0 + "@rocket.chat/fuselage": ^0.42.0 "@rocket.chat/fuselage-tokens": ~0.32.0 "@rocket.chat/message-parser": ~0.31.27 "@rocket.chat/styled": ~0.31.25 @@ -9462,7 +9462,7 @@ __metadata: "@rocket.chat/favicon": "workspace:^" "@rocket.chat/forked-matrix-appservice-bridge": ^4.0.2 "@rocket.chat/forked-matrix-bot-sdk": ^0.6.0-beta.3 - "@rocket.chat/fuselage": ^0.41.0 + "@rocket.chat/fuselage": ^0.42.0 "@rocket.chat/fuselage-hooks": ~0.32.1 "@rocket.chat/fuselage-polyfills": ~0.31.25 "@rocket.chat/fuselage-toastbar": ~0.31.25 @@ -10304,7 +10304,7 @@ __metadata: dependencies: "@babel/core": ~7.22.20 "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.41.0 + "@rocket.chat/fuselage": ^0.42.0 "@rocket.chat/fuselage-hooks": ~0.32.1 "@rocket.chat/icons": ~0.32.0 "@rocket.chat/mock-providers": "workspace:^" @@ -10355,7 +10355,7 @@ __metadata: dependencies: "@babel/core": ~7.22.20 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.41.0 + "@rocket.chat/fuselage": ^0.42.0 "@rocket.chat/icons": ~0.32.0 "@storybook/addon-actions": ~6.5.16 "@storybook/addon-docs": ~6.5.16 @@ -10446,7 +10446,7 @@ __metadata: resolution: "@rocket.chat/ui-theming@workspace:ee/packages/ui-theming" dependencies: "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.41.0 + "@rocket.chat/fuselage": ^0.42.0 "@rocket.chat/fuselage-hooks": ~0.32.1 "@rocket.chat/icons": ~0.32.0 "@rocket.chat/ui-contexts": "workspace:~" @@ -10489,7 +10489,7 @@ __metadata: "@rocket.chat/css-in-js": ~0.31.25 "@rocket.chat/emitter": ~0.31.25 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.41.0 + "@rocket.chat/fuselage": ^0.42.0 "@rocket.chat/fuselage-hooks": ~0.32.1 "@rocket.chat/icons": ~0.32.0 "@rocket.chat/styled": ~0.31.25 @@ -10532,7 +10532,7 @@ __metadata: "@codemirror/tooltip": ^0.19.16 "@lezer/highlight": ^1.1.6 "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.41.0 + "@rocket.chat/fuselage": ^0.42.0 "@rocket.chat/fuselage-hooks": ~0.32.1 "@rocket.chat/fuselage-polyfills": ~0.31.25 "@rocket.chat/fuselage-tokens": ~0.32.0 From 2f8c98f7a8c997afbecdd518fe88c17df73c1ccc Mon Sep 17 00:00:00 2001 From: Ahmed Nefzaoui Date: Thu, 11 Jan 2024 21:25:22 +0100 Subject: [PATCH 042/152] fix: Automate text direction based on the language in messages (#29367) Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com> --- .changeset/cyan-penguins-listen.md | 5 +++++ apps/meteor/client/components/message/IgnoredContent.tsx | 2 +- apps/meteor/client/components/message/MessageContentBody.tsx | 2 +- .../contactHistory/MessageList/ContactHistoryMessage.tsx | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .changeset/cyan-penguins-listen.md diff --git a/.changeset/cyan-penguins-listen.md b/.changeset/cyan-penguins-listen.md new file mode 100644 index 000000000000..96ca04b25e7c --- /dev/null +++ b/.changeset/cyan-penguins-listen.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue where texts are not being displayed in the correct direction on messages diff --git a/apps/meteor/client/components/message/IgnoredContent.tsx b/apps/meteor/client/components/message/IgnoredContent.tsx index 31b62639b849..b1973b13178a 100644 --- a/apps/meteor/client/components/message/IgnoredContent.tsx +++ b/apps/meteor/client/components/message/IgnoredContent.tsx @@ -17,7 +17,7 @@ const IgnoredContent = ({ onShowMessageIgnored }: IgnoredContentProps): ReactEle }; return ( - +

{t('Message_Ignored')} diff --git a/apps/meteor/client/components/message/MessageContentBody.tsx b/apps/meteor/client/components/message/MessageContentBody.tsx index 5552e6da0745..c2465c86c835 100644 --- a/apps/meteor/client/components/message/MessageContentBody.tsx +++ b/apps/meteor/client/components/message/MessageContentBody.tsx @@ -53,7 +53,7 @@ const MessageContentBody = ({ mentions, channels, md, searchText }: MessageConte `; return ( - + }> diff --git a/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx b/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx index dab42e58e300..c6ad907a14e1 100644 --- a/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx +++ b/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx @@ -101,7 +101,7 @@ const ContactHistoryMessage: FC<{ )} {!message.blocks && message.md && ( - + )} From 1499e895009d50b53bacd9aaa4aa78181667fc06 Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Fri, 12 Jan 2024 11:54:24 -0300 Subject: [PATCH 043/152] fix: Atlassian Crowd integration doesn't work (#31391) --- .changeset/witty-pumas-pretend.md | 5 + apps/meteor/app/api/server/v1/ldap.ts | 2 +- apps/meteor/app/crowd/server/crowd.ts | 99 +++++++++++++------ apps/meteor/app/crowd/server/methods.ts | 2 +- .../rocketchat-i18n/i18n/ar.i18n.json | 2 +- .../rocketchat-i18n/i18n/ca.i18n.json | 2 +- .../rocketchat-i18n/i18n/cs.i18n.json | 2 +- .../rocketchat-i18n/i18n/da.i18n.json | 2 +- .../rocketchat-i18n/i18n/de.i18n.json | 2 +- .../rocketchat-i18n/i18n/en.i18n.json | 3 +- .../rocketchat-i18n/i18n/es.i18n.json | 2 +- .../rocketchat-i18n/i18n/fi.i18n.json | 2 +- .../rocketchat-i18n/i18n/fr.i18n.json | 2 +- .../rocketchat-i18n/i18n/hu.i18n.json | 2 +- .../rocketchat-i18n/i18n/ja.i18n.json | 2 +- .../rocketchat-i18n/i18n/ko.i18n.json | 2 +- .../rocketchat-i18n/i18n/nl.i18n.json | 2 +- .../rocketchat-i18n/i18n/pl.i18n.json | 2 +- .../rocketchat-i18n/i18n/pt-BR.i18n.json | 2 +- .../rocketchat-i18n/i18n/ru.i18n.json | 2 +- .../rocketchat-i18n/i18n/sv.i18n.json | 2 +- .../rocketchat-i18n/i18n/zh-TW.i18n.json | 2 +- .../rocketchat-i18n/i18n/zh.i18n.json | 2 +- 23 files changed, 95 insertions(+), 52 deletions(-) create mode 100644 .changeset/witty-pumas-pretend.md diff --git a/.changeset/witty-pumas-pretend.md b/.changeset/witty-pumas-pretend.md new file mode 100644 index 000000000000..53e1b45a15ea --- /dev/null +++ b/.changeset/witty-pumas-pretend.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed Atlassian Crowd integration with Rocket.Chat not working diff --git a/apps/meteor/app/api/server/v1/ldap.ts b/apps/meteor/app/api/server/v1/ldap.ts index f0d3a52b504f..9a057c9b0afc 100644 --- a/apps/meteor/app/api/server/v1/ldap.ts +++ b/apps/meteor/app/api/server/v1/ldap.ts @@ -31,7 +31,7 @@ API.v1.addRoute( } return API.v1.success({ - message: 'Connection_success' as const, + message: 'LDAP_Connection_successful' as const, }); }, }, diff --git a/apps/meteor/app/crowd/server/crowd.ts b/apps/meteor/app/crowd/server/crowd.ts index b6b94f33e566..e43c65c70f91 100644 --- a/apps/meteor/app/crowd/server/crowd.ts +++ b/apps/meteor/app/crowd/server/crowd.ts @@ -68,23 +68,46 @@ export class CROWD { this.crowdClient = new AtlassianCrowd(this.options); } - async checkConnection() { - await this.crowdClient.ping(); + async checkConnection(): Promise { + return new Promise((resolve, reject) => + this.crowdClient.ping((err: any) => { + if (err) { + reject(err); + } + resolve(); + }), + ); } - async fetchCrowdUser(crowdUsername: string) { - const userResponse = await this.crowdClient.user.find(crowdUsername); + async fetchCrowdUser(crowdUsername: string): Promise> { + return new Promise((resolve, reject) => + this.crowdClient.user.find(crowdUsername, (err: any, userResponse: Record) => { + if (err) { + reject(err); + } + resolve({ + displayname: userResponse['display-name'], + username: userResponse.name, + email: userResponse.email, + active: userResponse.active, + crowd_username: crowdUsername, + }); + }), + ); + } - return { - displayname: userResponse['display-name'], - username: userResponse.name, - email: userResponse.email, - active: userResponse.active, - crowd_username: crowdUsername, - }; + async searchForCrowdUserByMail(email?: string): Promise | undefined> { + return new Promise((resolve) => + this.crowdClient.search('user', `email=" ${email} "`, (err: any, response: Record) => { + if (err) { + resolve(undefined); + } + resolve(response); + }), + ); } - async authenticate(username: string, password: string) { + async authenticate(username: string, password: string): Promise | undefined> { if (!username || !password) { logger.error('No username or password'); return; @@ -134,24 +157,30 @@ export class CROWD { logger.debug('New user. User is not synced yet.'); } logger.debug('Going to crowd:', crowdUsername); - const auth = await this.crowdClient.user.authenticate(crowdUsername, password); - - if (!auth) { - return; - } - const crowdUser: Record = await this.fetchCrowdUser(crowdUsername); - - if (user && settings.get('CROWD_Allow_Custom_Username') === true) { - crowdUser.username = user.username; - } + return new Promise((resolve, reject) => + this.crowdClient.user.authenticate(crowdUsername, password, async (err: any, res: Record) => { + if (err) { + reject(err); + } + const user = res; + try { + const crowdUser: Record = await this.fetchCrowdUser(crowdUsername); + if (user && settings.get('CROWD_Allow_Custom_Username') === true) { + crowdUser.username = user.name; + } - if (user) { - crowdUser._id = user._id; - } - crowdUser.password = password; + if (user) { + crowdUser._id = user._id; + } + crowdUser.password = password; - return crowdUser; + resolve(crowdUser); + } catch (err) { + reject(err); + } + }), + ); } async syncDataToUser(crowdUser: Record, id: string) { @@ -219,7 +248,7 @@ export class CROWD { const email = user.emails?.[0].address; logger.info('Attempting to find for user by email', email); - const response = this.crowdClient.searchSync('user', `email=" ${email} "`); + const response = await this.searchForCrowdUserByMail(email); if (!response || response.users.length === 0) { logger.warn('Could not find user in CROWD with username or email:', crowdUsername, email); if (settings.get('CROWD_Remove_Orphaned_Users') === true) { @@ -257,8 +286,15 @@ export class CROWD { } async updateUserCollection(crowdUser: Record) { + const username = crowdUser.crowd_username || crowdUser.username; + const mail = crowdUser.email; + + // If id is not provided, user is linked by crowd_username or email address const userQuery = { - _id: crowdUser._id, + ...(crowdUser._id && { _id: crowdUser._id }), + ...(!crowdUser._id && { + $or: [{ crowd_username: username }, { 'emails.address': mail }], + }), }; // find our existing user if they exist @@ -321,16 +357,17 @@ Accounts.registerLoginHandler('crowd', async function (this: typeof Accounts, lo } if (!user) { - logger.debug(`User ${loginRequest.username} is not allowd to access Rocket.Chat`); + logger.debug(`User ${loginRequest.username} is not allowed to access Rocket.Chat`); return new Meteor.Error('not-authorized', 'User is not authorized by crowd'); } const result = await crowd.updateUserCollection(user); return result; - } catch (err) { + } catch (err: any) { logger.debug({ err }); logger.error('Crowd user not authenticated due to an error'); + throw new Meteor.Error('user-not-found', err.message); } }); diff --git a/apps/meteor/app/crowd/server/methods.ts b/apps/meteor/app/crowd/server/methods.ts index 758ebd1fcb3c..a621e3c8d027 100644 --- a/apps/meteor/app/crowd/server/methods.ts +++ b/apps/meteor/app/crowd/server/methods.ts @@ -38,7 +38,7 @@ Meteor.methods({ await crowd.checkConnection(); return { - message: 'Connection_success' as const, + message: 'Crowd_Connection_successful' as const, params: [], }; } catch (err) { diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json index 6060705de4d3..b841354a8b0f 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json @@ -925,7 +925,7 @@ "Connection_Closed": "تم إغلاق الاتصال", "Connection_Reset": "إعادة تعيين الاتصال", "Connection_error": "خطأ في الاتصال", - "Connection_success": "اتصال LDAP ناجح", + "LDAP_Connection_successful": "اتصال LDAP ناجح", "Connection_failed": "فشل اتصال LDAP", "Connectivity_Services": "خدمات الاتصال", "Consulting": "الاستشارات", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json index f61d28735fe6..f960770eb116 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json @@ -920,7 +920,7 @@ "Connection_Closed": "Connexió tancada", "Connection_Reset": "Connexió restablerta", "Connection_error": "Error de connexió", - "Connection_success": "Connexió LDAP correcta", + "LDAP_Connection_successful": "Connexió LDAP correcta", "Connection_failed": "Error de connexió LDAP", "Connectivity_Services": "Serveis de connectivitat", "Consulting": "Consultant", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json index 1c5931521c91..ec0faf0829aa 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json @@ -753,7 +753,7 @@ "Connect": "Připojit", "Connection_Closed": "Připojení bylo uzavřeno", "Connection_Reset": "Obnovit připojení", - "Connection_success": "LDAP připojení úspěšné", + "LDAP_Connection_successful": "LDAP připojení úspěšné", "Connectivity_Services": "Služby připojení", "Consulting": "Konzultace", "Contact": "Kontakt", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json index d9aaf3f0a003..522a3776dcc9 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json @@ -757,7 +757,7 @@ "Connect": "Forbind", "Connection_Closed": "Forbindelse lukket", "Connection_Reset": "Nulstilning af forbindelse", - "Connection_success": "LDAP-forbindelse lykkedes", + "LDAP_Connection_successful": "LDAP-forbindelse lykkedes", "Connectivity_Services": "Connectivity Services", "Consulting": "Rådgivning", "Contact": "Kontakt", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json index 68c54237e99b..e5241fe68e1b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json @@ -1046,7 +1046,7 @@ "Connection_Closed": "Verbindung geschlossen", "Connection_Reset": "Verbindung zurücksetzen", "Connection_error": "Verbindungsfehler", - "Connection_success": "LDAP-Verbindung erfolgreich", + "LDAP_Connection_successful": "LDAP-Verbindung erfolgreich", "Connection_failed": "LDAP-Verbindung fehlgeschlagen", "Connectivity_Services": "Verbindungsdienste", "Consulting": "Beratung", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 7f465b9175ac..bf9610f634aa 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1133,7 +1133,6 @@ "Connection_Closed": "Connection closed", "Connection_Reset": "Connection reset", "Connection_error": "Connection error", - "Connection_success": "LDAP Connection Successful", "Connection_failed": "LDAP Connection Failed", "Connectivity_Services": "Connectivity Services", "Consulting": "Consulting", @@ -1457,6 +1456,7 @@ "Created_by": "Created by", "CRM_Integration": "CRM Integration", "CROWD_Allow_Custom_Username": "Allow custom username in Rocket.Chat", + "Crowd_Connection_successful": "Crowd Connection Successful", "CROWD_Reject_Unauthorized": "Reject Unauthorized", "Crowd_Remove_Orphaned_Users": "Remove Orphaned Users", "Crowd_sync_interval_Description": "The interval between synchronizations. Example `every 24 hours` or `on the first day of the week`, more examples at [Cron Text Parser](http://bunkat.github.io/later/parsers.html#text)", @@ -2905,6 +2905,7 @@ "LDAP_Connection": "Connection", "LDAP_Connection_Authentication": "Authentication", "LDAP_Connection_Encryption": "Encryption", + "LDAP_Connection_successful": "LDAP Connection Successful", "LDAP_Connection_Timeouts": "Timeouts", "LDAP_UserSearch": "User Search", "LDAP_UserSearch_Filter": "Search Filter", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json index 9e1fed728dce..861ea5723b4a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json @@ -931,7 +931,7 @@ "Connection_Closed": "Conexión cerrada", "Connection_Reset": "Conexión restablecida", "Connection_error": "Error de conexión", - "Connection_success": "Conexión LDAP correcta", + "LDAP_Connection_successful": "Conexión LDAP correcta", "Connection_failed": "Error de conexión LDAP", "Connectivity_Services": "Servicios de conectividad", "Consulting": "Consultoría", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json index 6cc6bae167e6..629665260a4d 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json @@ -1067,7 +1067,7 @@ "Connection_Closed": "Yhteys katkaistu", "Connection_Reset": "Yhteyden nollaus", "Connection_error": "Yhteysvirhe", - "Connection_success": "LDAP-yhteys onnistui", + "LDAP_Connection_successful": "LDAP-yhteys onnistui", "Connection_failed": "LDAP-yhteys epäonnistui", "Connectivity_Services": "Yhteyspalvelut", "Consulting": "Konsultointi", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json index f239fcc0fc74..c0c866af1b0a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json @@ -928,7 +928,7 @@ "Connection_Closed": "Connexion fermée", "Connection_Reset": "Connexion réinitialisée", "Connection_error": "Erreur de connexion", - "Connection_success": "Connexion LDAP réussie", + "LDAP_Connection_successful": "Connexion LDAP réussie", "Connection_failed": "Échec de la connexion LDAP", "Connectivity_Services": "Services de connectivité", "Consulting": "Conseil", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json index 0f9e12f8b599..9149c72fdeec 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json @@ -1028,7 +1028,7 @@ "Connection_Closed": "Kapcsolat lezárva", "Connection_Reset": "Kapcsolat visszaállítva", "Connection_error": "Kapcsolódási hiba", - "Connection_success": "Az LDAP-kapcsolat sikeres", + "LDAP_Connection_successful": "Az LDAP-kapcsolat sikeres", "Connection_failed": "Az LDAP-kapcsolat sikertelen", "Connectivity_Services": "Kapcsolódási szolgáltatások", "Consulting": "Tanácsadás", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json index 9487983bf9f7..81f51b667cd9 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json @@ -915,7 +915,7 @@ "Connection_Closed": "接続が閉じられました", "Connection_Reset": "接続のリセット", "Connection_error": "接続エラー", - "Connection_success": "LDAP接続に成功しました", + "LDAP_Connection_successful": "LDAP接続に成功しました", "Connection_failed": "LDAP接続が失敗しました", "Connectivity_Services": "接続サービス", "Consulting": "コンサルティング", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json index 0341f6388348..6cd9d58a8692 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json @@ -795,7 +795,7 @@ "Connect": "연결", "Connection_Closed": "연결이 닫혔습니다.", "Connection_Reset": "연결 재설정", - "Connection_success": "LDAP 연결 성공", + "LDAP_Connection_successful": "LDAP 연결 성공", "Connectivity_Services": "연결 서비스", "Consulting": "컨설팅", "Contact": "연락처", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json index 23e356f28eee..fd9833d15769 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json @@ -923,7 +923,7 @@ "Connection_Closed": "Verbinding gesloten", "Connection_Reset": "Verbinding gereset", "Connection_error": "Verbindingsfout", - "Connection_success": "LDAP-verbinding geslaagd", + "LDAP_Connection_successful": "LDAP-verbinding geslaagd", "Connection_failed": "LDAP-verbinding mislukt", "Connectivity_Services": "Connectiviteitsdiensten", "Consulting": "Consultant", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json index aa8ca813b445..2ff9ceb1ab86 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json @@ -1007,7 +1007,7 @@ "Connection_Closed": "Połączenie zamknięte", "Connection_Reset": "Reset połączenia", "Connection_error": "Błąd połączenia", - "Connection_success": "Nawiązano połączenie z LDAP", + "LDAP_Connection_successful": "Nawiązano połączenie z LDAP", "Connection_failed": "Nie można nawiązać połączenia z LDAP", "Connectivity_Services": "Usługi łączności", "Consulting": "Doradztwo", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 4daea7308d8a..260d3828c796 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -965,7 +965,7 @@ "Connection_Closed": "Conexão fechada", "Connection_Reset": "Redefinição de conexão", "Connection_error": "Erro de conexão", - "Connection_success": "Conexão com LDAP bem-sucedida", + "LDAP_Connection_successful": "Conexão com LDAP bem-sucedida", "Connection_failed": "Falha na conexão com o LDAP", "Connectivity_Services": "Serviços de conectividade", "Consulting": "Consultoria", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json index e3dc8d0a5791..c73e1fa58d20 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json @@ -1028,7 +1028,7 @@ "Connection_Closed": "Соединение закрыто", "Connection_Reset": "Сброс соединения", "Connection_error": "Ошибка подключения", - "Connection_success": "Подключение к LDAP успешное", + "LDAP_Connection_successful": "Подключение к LDAP успешное", "Connection_failed": "Сбой подключения LDAP", "Connectivity_Services": "Connectivity Services", "Consulting": "Консалтинг", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json index 94964cee15d6..03d733f7eaea 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json @@ -1064,7 +1064,7 @@ "Connection_Closed": "Anslutningen är stängd", "Connection_Reset": "Anslutning återställd", "Connection_error": "Anslutningsfel", - "Connection_success": "LDAP-anslutningen har upprättats", + "LDAP_Connection_successful": "LDAP-anslutningen har upprättats", "Connection_failed": "LDAP-anslutningen kunde inte upprättas", "Connectivity_Services": "Anslutningstjänster", "Consulting": "Consulting", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json index 8bbb614c560b..d5e8e05f71fb 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json @@ -914,7 +914,7 @@ "Connection_Closed": "連接關閉", "Connection_Reset": "連線重置", "Connection_error": "連線錯誤", - "Connection_success": "LDAP 連接成功", + "LDAP_Connection_successful": "LDAP 連接成功", "Connection_failed": "LDAP 連線失敗", "Connectivity_Services": "連線的服務", "Consulting": "諮詢", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json index 7d4a50c8e362..d2df7281184e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json @@ -809,7 +809,7 @@ "Connect_SSL_TLS": "使用 SSL/TLS 连接", "Connection_Closed": "连接关闭", "Connection_Reset": "连接重置", - "Connection_success": "LDAP 连接成功", + "LDAP_Connection_successful": "LDAP 连接成功", "Connectivity_Services": "连接性服务", "Consulting": "咨询", "Contact": "联系人", From 22ffaff22d52263e8c5374045d51d92fd4b9e55b Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 12 Jan 2024 09:36:01 -0600 Subject: [PATCH 044/152] fix: Some Omnichannel charts not available when calling through endpoint (#31414) --- .../packages/rocketchat-i18n/i18n/en.i18n.json | 2 +- .../omnichannel-analytics/ChartData.ts | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index bf9610f634aa..628434d61c65 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -28,7 +28,7 @@ "@username": "@username", "@username_message": "@username ", "#channel": "#channel", - "%_of_conversations": "%% of Conversations", + "%_of_conversations": "% of Conversations", "0_Errors_Only": "0 - Errors Only", "1_Errors_and_Information": "1 - Errors and Information", "2_Erros_Information_and_Debug": "2 - Errors, Information and Debug", diff --git a/apps/meteor/server/services/omnichannel-analytics/ChartData.ts b/apps/meteor/server/services/omnichannel-analytics/ChartData.ts index 044dca954c6d..f2b1f21b5b9a 100644 --- a/apps/meteor/server/services/omnichannel-analytics/ChartData.ts +++ b/apps/meteor/server/services/omnichannel-analytics/ChartData.ts @@ -8,7 +8,9 @@ type ChartDataValidActions = | 'Avg_chat_duration' | 'Total_messages' | 'Avg_first_response_time' - | 'Avg_reaction_time'; + | 'Avg_reaction_time' + | 'Best_first_response_time' + | 'Avg_response_time'; type DateParam = { gte: Date; @@ -22,7 +24,15 @@ export class ChartData { if (!action) { return false; } - return ['Total_conversations', 'Avg_chat_duration', 'Total_messages', 'Avg_first_response_time', 'Avg_reaction_time'].includes(action); + return [ + 'Total_conversations', + 'Avg_chat_duration', + 'Total_messages', + 'Avg_first_response_time', + 'Avg_reaction_time', + 'Best_first_response_time', + 'Avg_response_time', + ].includes(action); } callAction(action: T, ...args: [DateParam, string?, Filter?]) { @@ -37,6 +47,10 @@ export class ChartData { return this.Avg_first_response_time(...args); case 'Avg_reaction_time': return this.Avg_reaction_time(...args); + case 'Best_first_response_time': + return this.Best_first_response_time(...args); + case 'Avg_response_time': + return this.Avg_response_time(...args); default: throw new Error('Invalid action'); } From 8c234246c370f98a1f07d9fb1e0a0af68d25707a Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Fri, 12 Jan 2024 13:29:43 -0300 Subject: [PATCH 045/152] chore: Add missing changeset file (#31431) --- .changeset/silent-shirts-knock.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/silent-shirts-knock.md diff --git a/.changeset/silent-shirts-knock.md b/.changeset/silent-shirts-knock.md new file mode 100644 index 000000000000..53db729a0d19 --- /dev/null +++ b/.changeset/silent-shirts-knock.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where `AppRow` bundleIn verification breaks the UI From 30211804933b2f8d506fc606da52521850fcdb83 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 12 Jan 2024 16:32:45 -0300 Subject: [PATCH 046/152] chore(i18n): prevent error on `./dist` folder deletion (#31440) --- packages/i18n/src/index.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/i18n/src/index.mjs b/packages/i18n/src/index.mjs index 3204c6c3fd80..c952cac7de07 100644 --- a/packages/i18n/src/index.mjs +++ b/packages/i18n/src/index.mjs @@ -100,7 +100,9 @@ export = dict; const languages = files.map((file) => path.basename(file, '.i18n.json')); // write the files -fs.rmdirSync(`./dist`, { recursive: true }); +if (fs.existsSync(`./dist`)) { + fs.rmdirSync(`./dist`, { recursive: true }); +} fs.mkdirSync(`./dist`, { recursive: true }); fs.writeFileSync(`./dist/languages.js`, `module.exports = ${JSON.stringify(languages, null, 2)}`); From dd2967ce04220f67258f39cc33806bceb927bb78 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 12 Jan 2024 14:17:11 -0600 Subject: [PATCH 047/152] chore: Make room closing process more sequential (#31435) --- apps/meteor/app/livechat/server/api/v1/room.ts | 4 ++++ .../app/livechat/server/lib/LivechatTyped.ts | 16 +++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 23d7fe2c507a..f610b9a9d3de 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -416,6 +416,10 @@ API.v1.addRoute( throw new Error('error-invalid-room'); } + if (!room.open) { + throw new Error('room-closed'); + } + if (!(await Omnichannel.isWithinMACLimit(room))) { throw new Error('error-mac-limit-reached'); } diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index ea508b047882..5e9c08fcc1ef 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -290,11 +290,17 @@ class LivechatClass { this.logger.debug(`Updating DB for room ${room._id} with close data`); - await Promise.all([ - LivechatRooms.closeRoomById(rid, closeData), - LivechatInquiry.removeByRoomId(rid), - Subscriptions.removeByRoomId(rid), - ]); + const removedInquiry = await LivechatInquiry.removeByRoomId(rid); + if (removedInquiry && removedInquiry.deletedCount !== 1) { + throw new Error('Error removing inquiry'); + } + + const updatedRoom = await LivechatRooms.closeRoomById(rid, closeData); + if (!updatedRoom || updatedRoom.modifiedCount !== 1) { + throw new Error('Error closing room'); + } + + await Subscriptions.removeByRoomId(rid); this.logger.debug(`DB updated for room ${room._id}`); From afd5fdd52107f41ce0794ac9fc8989ba6b892ede Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Fri, 12 Jan 2024 18:07:46 -0300 Subject: [PATCH 048/152] fix: LDAP Group filter doesn't work and throws "No Such Object" error on login (#31377) --- .changeset/wet-crabs-brush.md | 5 +++++ apps/meteor/server/lib/ldap/Connection.ts | 4 ++-- apps/meteor/server/lib/ldap/Manager.ts | 9 ++++----- 3 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 .changeset/wet-crabs-brush.md diff --git a/.changeset/wet-crabs-brush.md b/.changeset/wet-crabs-brush.md new file mode 100644 index 000000000000..375d59addc07 --- /dev/null +++ b/.changeset/wet-crabs-brush.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed LDAP "Group filter" malfunction, which prevented LDAP users from logging in. diff --git a/apps/meteor/server/lib/ldap/Connection.ts b/apps/meteor/server/lib/ldap/Connection.ts index 2ab6ba9c73cf..167f1b36e508 100644 --- a/apps/meteor/server/lib/ldap/Connection.ts +++ b/apps/meteor/server/lib/ldap/Connection.ts @@ -465,9 +465,9 @@ export class LDAPConnection { searchLogger.debug({ msg: 'Group filter LDAP:', filter: searchOptions.filter }); - const result = await this.searchRaw(this.options.baseDN, searchOptions); + const result = await this.searchAndCount(this.options.baseDN, searchOptions); - if (!Array.isArray(result) || result.length === 0) { + if (result === 0) { return false; } return true; diff --git a/apps/meteor/server/lib/ldap/Manager.ts b/apps/meteor/server/lib/ldap/Manager.ts index 99fe356d53c1..4a5cdf2df8d6 100644 --- a/apps/meteor/server/lib/ldap/Manager.ts +++ b/apps/meteor/server/lib/ldap/Manager.ts @@ -200,6 +200,10 @@ export class LDAPManager { } const [ldapUser] = users; + if (!(await ldap.isUserAcceptedByGroupFilter(escapedUsername, ldapUser.dn))) { + throw new Error('User not found'); + } + if (!(await ldap.authenticate(ldapUser.dn, password))) { logger.debug(`Wrong password for ${escapedUsername}`); throw new Error('Invalid user or wrong password'); @@ -212,11 +216,6 @@ export class LDAPManager { authLogger.debug(`Bind successful but user ${ldapUser.dn} was not found via search`); } } - - if (!(await ldap.isUserAcceptedByGroupFilter(escapedUsername, ldapUser.dn))) { - throw new Error('User not in a valid group'); - } - return ldapUser; } catch (error) { logger.error(error); From e3252f5448464b056471ef8d99deb4a8d2c93a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Mon, 15 Jan 2024 12:00:27 -0300 Subject: [PATCH 049/152] fix: quote image gallery (#31415) --- .changeset/smooth-kangaroos-mate.md | 5 +++ .../components/ImageGallery/ImageGallery.tsx | 15 ++------ .../ImageGallery/ImageGalleryError.tsx | 26 ++++++++++++++ ...leryLoader.tsx => ImageGalleryLoading.tsx} | 4 +-- .../ImageGallery/hooks/useImageGallery.ts | 21 ------------ .../client/components/ImageGallery/index.ts | 4 ++- .../content/attachments/QuoteAttachment.tsx | 2 +- .../client/providers/ImageGalleryProvider.tsx | 34 +++++++++++-------- .../room/ImageGallery/ImageGalleryData.tsx | 28 +++++++++++++++ .../room}/ImageGallery/hooks/useImagesList.ts | 6 ++-- 10 files changed, 90 insertions(+), 55 deletions(-) create mode 100644 .changeset/smooth-kangaroos-mate.md create mode 100644 apps/meteor/client/components/ImageGallery/ImageGalleryError.tsx rename apps/meteor/client/components/ImageGallery/{ImageGalleryLoader.tsx => ImageGalleryLoading.tsx} (84%) delete mode 100644 apps/meteor/client/components/ImageGallery/hooks/useImageGallery.ts create mode 100644 apps/meteor/client/views/room/ImageGallery/ImageGalleryData.tsx rename apps/meteor/client/{components => views/room}/ImageGallery/hooks/useImagesList.ts (86%) diff --git a/.changeset/smooth-kangaroos-mate.md b/.changeset/smooth-kangaroos-mate.md new file mode 100644 index 000000000000..81b2156192a3 --- /dev/null +++ b/.changeset/smooth-kangaroos-mate.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix: quote image gallery diff --git a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx index 676418731e51..07ef83a12176 100644 --- a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx +++ b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx @@ -1,3 +1,4 @@ +import type { IUpload } from '@rocket.chat/core-typings'; import { css } from '@rocket.chat/css-in-js'; import { Box, ButtonGroup, IconButton, Palette, Throbber } from '@rocket.chat/fuselage'; import React, { useRef, useState } from 'react'; @@ -14,8 +15,6 @@ import 'swiper/modules/keyboard/keyboard.min.css'; import 'swiper/modules/zoom/zoom.min.css'; import { usePreventPropagation } from '../../hooks/usePreventPropagation'; -import ImageGalleryLoader from './ImageGalleryLoader'; -import { useImageGallery } from './hooks/useImageGallery'; const swiperStyle = css` .swiper { @@ -107,7 +106,7 @@ const swiperStyle = css` } `; -const ImageGallery = () => { +export const ImageGallery = ({ images, onClose, loadMore }: { images: IUpload[]; onClose: () => void; loadMore?: () => void }) => { const swiperRef = useRef(null); const [, setSwiperInst] = useState(); const [zoomScale, setZoomScale] = useState(1); @@ -126,18 +125,12 @@ const ImageGallery = () => { const preventPropagation = usePreventPropagation(); - const { isLoading, loadMore, images, onClose } = useImageGallery(); - - if (isLoading) { - return ; - } - return createPortal(

- {zoomScale !== 1 && } + {zoomScale !== 1 && } @@ -176,5 +169,3 @@ const ImageGallery = () => { document.body, ); }; - -export default ImageGallery; diff --git a/apps/meteor/client/components/ImageGallery/ImageGalleryError.tsx b/apps/meteor/client/components/ImageGallery/ImageGalleryError.tsx new file mode 100644 index 000000000000..b0c85ed13572 --- /dev/null +++ b/apps/meteor/client/components/ImageGallery/ImageGalleryError.tsx @@ -0,0 +1,26 @@ +import { css } from '@rocket.chat/css-in-js'; +import { IconButton, ModalBackdrop } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; +import { createPortal } from 'react-dom'; + +import GenericError from '../GenericError/GenericError'; + +const closeButtonStyle = css` + position: absolute; + z-index: 10; + top: 10px; + right: 10px; +`; + +export const ImageGalleryError = ({ onClose }: { onClose: () => void }) => { + const t = useTranslation(); + + return createPortal( + + + + , + document.body, + ); +}; diff --git a/apps/meteor/client/components/ImageGallery/ImageGalleryLoader.tsx b/apps/meteor/client/components/ImageGallery/ImageGalleryLoading.tsx similarity index 84% rename from apps/meteor/client/components/ImageGallery/ImageGalleryLoader.tsx rename to apps/meteor/client/components/ImageGallery/ImageGalleryLoading.tsx index 131b82780f22..6e5f2d0463b8 100644 --- a/apps/meteor/client/components/ImageGallery/ImageGalleryLoader.tsx +++ b/apps/meteor/client/components/ImageGallery/ImageGalleryLoading.tsx @@ -10,7 +10,7 @@ const closeButtonStyle = css` right: 10px; `; -const ImageGalleryLoader = ({ onClose }: { onClose: () => void }) => +export const ImageGalleryLoading = ({ onClose }: { onClose: () => void }) => createPortal( @@ -18,5 +18,3 @@ const ImageGalleryLoader = ({ onClose }: { onClose: () => void }) => , document.body, ); - -export default ImageGalleryLoader; diff --git a/apps/meteor/client/components/ImageGallery/hooks/useImageGallery.ts b/apps/meteor/client/components/ImageGallery/hooks/useImageGallery.ts deleted file mode 100644 index 9d058a010fdc..000000000000 --- a/apps/meteor/client/components/ImageGallery/hooks/useImageGallery.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useMemo, useContext } from 'react'; - -import { ImageGalleryContext } from '../../../contexts/ImageGalleryContext'; -import { useRecordList } from '../../../hooks/lists/useRecordList'; -import { useRoom } from '../../../views/room/contexts/RoomContext'; -import { useImagesList } from './useImagesList'; - -export const useImageGallery = () => { - const { _id: rid } = useRoom(); - const { imageId, onClose } = useContext(ImageGalleryContext); - - const { filesList, loadMoreItems } = useImagesList(useMemo(() => ({ roomId: rid, startingFromId: imageId }), [imageId, rid])); - const { phase, items: filesItems } = useRecordList(filesList); - - return { - images: filesItems, - isLoading: phase === 'loading', - loadMore: () => loadMoreItems(filesItems.length - 1), - onClose, - }; -}; diff --git a/apps/meteor/client/components/ImageGallery/index.ts b/apps/meteor/client/components/ImageGallery/index.ts index db657797badb..fb1b333e5e15 100644 --- a/apps/meteor/client/components/ImageGallery/index.ts +++ b/apps/meteor/client/components/ImageGallery/index.ts @@ -1 +1,3 @@ -export { default } from './ImageGallery'; +export * from './ImageGallery'; +export * from './ImageGalleryError'; +export * from './ImageGalleryLoading'; diff --git a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx index 493e3e9ea918..7c2c2011cac9 100644 --- a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx @@ -70,7 +70,7 @@ export const QuoteAttachment = ({ attachment }: QuoteAttachmentProps): ReactElem {attachment.md ? : attachment.text.substring(attachment.text.indexOf('\n') + 1)} {attachment.attachments && ( - + )} diff --git a/apps/meteor/client/providers/ImageGalleryProvider.tsx b/apps/meteor/client/providers/ImageGalleryProvider.tsx index 6d14e28c53ce..1cd07f29882c 100644 --- a/apps/meteor/client/providers/ImageGalleryProvider.tsx +++ b/apps/meteor/client/providers/ImageGalleryProvider.tsx @@ -1,7 +1,8 @@ import React, { type ReactNode, useEffect, useState } from 'react'; -import ImageGallery from '../components/ImageGallery/ImageGallery'; +import { ImageGallery } from '../components/ImageGallery'; import { ImageGalleryContext } from '../contexts/ImageGalleryContext'; +import ImageGalleryData from '../views/room/ImageGallery/ImageGalleryData'; type ImageGalleryProviderProps = { children: ReactNode; @@ -9,34 +10,39 @@ type ImageGalleryProviderProps = { const ImageGalleryProvider = ({ children }: ImageGalleryProviderProps) => { const [imageId, setImageId] = useState(); + const [quotedImageUrl, setQuotedImageUrl] = useState(); useEffect(() => { - document.addEventListener('click', (event: Event) => { + const handleImageClick = (event: Event) => { const target = event?.target as HTMLElement | null; + + if (target?.closest('.rcx-attachment__details')) { + return setQuotedImageUrl(target.dataset.id); + } if (target?.classList.contains('gallery-item')) { - return setImageId(target.dataset.id || target?.parentElement?.parentElement?.dataset.id); + const id = target.closest('.gallery-item-container')?.getAttribute('data-id') || undefined; + return setImageId(target.dataset.id || id); } - if (target?.classList.contains('gallery-item-container')) { return setImageId(target.dataset.id); } - if ( - target?.classList.contains('gallery-item') && - target?.parentElement?.parentElement?.classList.contains('gallery-item-container') - ) { - return setImageId(target.dataset.id || target?.parentElement?.parentElement?.dataset.id); + if (target?.classList.contains('rcx-avatar__element') && target.closest('.gallery-item')) { + const avatarTarget = target.closest('.gallery-item-container')?.getAttribute('data-id') || undefined; + return setImageId(avatarTarget); } + }; + document.addEventListener('click', handleImageClick); - if (target?.classList.contains('rcx-avatar__element') && target?.parentElement?.classList.contains('gallery-item')) { - return setImageId(target.dataset.id || target?.parentElement?.parentElement?.dataset.id); - } - }); + return () => document.removeEventListener('click', handleImageClick); }, []); return ( setImageId(undefined) }}> {children} - {!!imageId && } + {!!quotedImageUrl && ( + setQuotedImageUrl(undefined)} /> + )} + {!!imageId && } ); }; diff --git a/apps/meteor/client/views/room/ImageGallery/ImageGalleryData.tsx b/apps/meteor/client/views/room/ImageGallery/ImageGalleryData.tsx new file mode 100644 index 000000000000..207ad3c0d9b3 --- /dev/null +++ b/apps/meteor/client/views/room/ImageGallery/ImageGalleryData.tsx @@ -0,0 +1,28 @@ +import React, { useContext, useMemo } from 'react'; + +import { ImageGallery, ImageGalleryError, ImageGalleryLoading } from '../../../components/ImageGallery'; +import { ImageGalleryContext } from '../../../contexts/ImageGalleryContext'; +import { useRecordList } from '../../../hooks/lists/useRecordList'; +import { useRoom } from '../contexts/RoomContext'; +import { useImagesList } from './hooks/useImagesList'; + +const ImageGalleryData = () => { + const { _id: rid } = useRoom(); + + const { imageId, onClose } = useContext(ImageGalleryContext); + + const { filesList, loadMoreItems } = useImagesList(useMemo(() => ({ roomId: rid, startingFromId: imageId }), [imageId, rid])); + const { phase, items: images, error } = useRecordList(filesList); + + if (error) { + return ; + } + + if (phase === 'loading') { + return ; + } + + return loadMoreItems(images.length - 1)} onClose={onClose} />; +}; + +export default ImageGalleryData; diff --git a/apps/meteor/client/components/ImageGallery/hooks/useImagesList.ts b/apps/meteor/client/views/room/ImageGallery/hooks/useImagesList.ts similarity index 86% rename from apps/meteor/client/components/ImageGallery/hooks/useImagesList.ts rename to apps/meteor/client/views/room/ImageGallery/hooks/useImagesList.ts index 69a237ca98e1..35f2a8bdceec 100644 --- a/apps/meteor/client/components/ImageGallery/hooks/useImagesList.ts +++ b/apps/meteor/client/views/room/ImageGallery/hooks/useImagesList.ts @@ -2,9 +2,9 @@ import type { ChannelsImagesProps } from '@rocket.chat/rest-typings'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useCallback, useEffect, useState } from 'react'; -import { useScrollableRecordList } from '../../../hooks/lists/useScrollableRecordList'; -import { useComponentDidUpdate } from '../../../hooks/useComponentDidUpdate'; -import { ImagesList } from '../../../lib/lists/ImagesList'; +import { useScrollableRecordList } from '../../../../hooks/lists/useScrollableRecordList'; +import { useComponentDidUpdate } from '../../../../hooks/useComponentDidUpdate'; +import { ImagesList } from '../../../../lib/lists/ImagesList'; export const useImagesList = ( options: ChannelsImagesProps, From 5deac49e5f16887e8dc77cd138c9cea7968d112e Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 15 Jan 2024 12:29:27 -0300 Subject: [PATCH 050/152] chore: reorganize server imports (#31439) --- apps/meteor/ee/app/license/server/index.ts | 3 - apps/meteor/ee/app/license/server/startup.ts | 222 +++++++++--------- apps/meteor/ee/server/index.ts | 5 +- apps/meteor/ee/server/models/startup.ts | 2 +- .../ee/server/startup/apps/trialExpiration.ts | 5 +- apps/meteor/ee/server/startup/index.ts | 18 +- .../personal-access-tokens/server/index.ts | 1 - apps/meteor/imports/startup/server/index.ts | 1 - apps/meteor/server/configureLogLevel.ts | 10 +- apps/meteor/server/main.ts | 105 +++------ apps/meteor/server/methods/index.ts | 56 +++++ apps/meteor/server/publications/index.ts | 5 + apps/meteor/server/routes/index.ts | 5 + apps/meteor/server/services/startup.ts | 64 ++--- apps/meteor/server/settings/index.ts | 84 ++++--- apps/meteor/server/startup/index.ts | 15 +- .../meteor/server/startup/migrations/index.ts | 3 +- apps/meteor/server/startup/migrations/xrun.ts | 15 +- 18 files changed, 321 insertions(+), 298 deletions(-) delete mode 100644 apps/meteor/imports/personal-access-tokens/server/index.ts delete mode 100644 apps/meteor/imports/startup/server/index.ts create mode 100644 apps/meteor/server/methods/index.ts create mode 100644 apps/meteor/server/publications/index.ts create mode 100644 apps/meteor/server/routes/index.ts diff --git a/apps/meteor/ee/app/license/server/index.ts b/apps/meteor/ee/app/license/server/index.ts index 403922524fa8..9177532a9e21 100644 --- a/apps/meteor/ee/app/license/server/index.ts +++ b/apps/meteor/ee/app/license/server/index.ts @@ -1,5 +1,2 @@ import './settings'; import './methods'; -import './startup'; - -export { getStatistics } from './getStatistics'; diff --git a/apps/meteor/ee/app/license/server/startup.ts b/apps/meteor/ee/app/license/server/startup.ts index e483cb8d7336..7cdbe12111b2 100644 --- a/apps/meteor/ee/app/license/server/startup.ts +++ b/apps/meteor/ee/app/license/server/startup.ts @@ -11,140 +11,142 @@ import { callbacks } from '../../../../lib/callbacks'; import { applyLicense, applyLicenseOrRemove } from './applyLicense'; import { getAppCount } from './lib/getAppCount'; -settings.watch('Site_Url', (value) => { - if (value) { - void License.setWorkspaceUrl(value); - } -}); - -License.onValidateLicense(async () => { - await Settings.updateValueById('Enterprise_License', License.encryptedLicense); - await Settings.updateValueById('Enterprise_License_Status', 'Valid'); -}); - -License.onInvalidateLicense(async () => { - await Settings.updateValueById('Enterprise_License_Status', 'Invalid'); -}); - -License.onRemoveLicense(async () => { - await Settings.updateValueById('Enterprise_License', ''); - await Settings.updateValueById('Enterprise_License_Status', 'Invalid'); -}); - -/** - * This is a debounced function that will sync the workspace data to the cloud. - * it caches the context, waits for a second and then syncs the data. - */ - -const syncByTriggerDebounced = (() => { - let timeout: NodeJS.Timeout | undefined; - const contexts: Set = new Set(); - return async (context: string) => { - contexts.add(context); - if (timeout) { - clearTimeout(timeout); +export const startLicense = async () => { + settings.watch('Site_Url', (value) => { + if (value) { + void License.setWorkspaceUrl(value); } + }); - timeout = setTimeout(() => { - timeout = undefined; - void syncByTrigger([...contexts]); - contexts.clear(); - }, 1000); - }; -})(); + License.onValidateLicense(async () => { + await Settings.updateValueById('Enterprise_License', License.encryptedLicense); + await Settings.updateValueById('Enterprise_License_Status', 'Valid'); + }); -const syncByTrigger = async (contexts: string[]) => { - if (!License.encryptedLicense) { - return; - } + License.onInvalidateLicense(async () => { + await Settings.updateValueById('Enterprise_License_Status', 'Invalid'); + }); - const existingData = wrapExceptions(() => JSON.parse(settings.get('Enterprise_License_Data'))).catch(() => ({})) ?? {}; + License.onRemoveLicense(async () => { + await Settings.updateValueById('Enterprise_License', ''); + await Settings.updateValueById('Enterprise_License_Status', 'Invalid'); + }); - const date = new Date(); + /** + * This is a debounced function that will sync the workspace data to the cloud. + * it caches the context, waits for a second and then syncs the data. + */ + + const syncByTriggerDebounced = (() => { + let timeout: NodeJS.Timeout | undefined; + const contexts: Set = new Set(); + return async (context: string) => { + contexts.add(context); + if (timeout) { + clearTimeout(timeout); + } + + timeout = setTimeout(() => { + timeout = undefined; + void syncByTrigger([...contexts]); + contexts.clear(); + }, 1000); + }; + })(); + + const syncByTrigger = async (contexts: string[]) => { + if (!License.encryptedLicense) { + return; + } - const day = date.getDate(); - const month = date.getMonth() + 1; - const year = date.getFullYear(); + const existingData = wrapExceptions(() => JSON.parse(settings.get('Enterprise_License_Data'))).catch(() => ({})) ?? {}; - const period = `${year}-${month}-${day}`; + const date = new Date(); - const [, , signed] = License.encryptedLicense.split('.'); + const day = date.getDate(); + const month = date.getMonth() + 1; + const year = date.getFullYear(); - // Check if this sync has already been done. Based on License, behavior. + const period = `${year}-${month}-${day}`; - if ([...contexts.values()].every((context) => existingData.signed === signed && existingData[context] === period)) { - return; - } + const [, , signed] = License.encryptedLicense.split('.'); - const obj = Object.fromEntries(contexts.map((context) => [context, period])); + // Check if this sync has already been done. Based on License, behavior. - await Settings.updateValueById( - 'Enterprise_License_Data', - JSON.stringify({ - ...(existingData.signed === signed && existingData), - ...existingData, - ...obj, - signed, - }), - ); + if ([...contexts.values()].every((context) => existingData.signed === signed && existingData[context] === period)) { + return; + } - try { - await syncWorkspace(); - } catch (error) { - console.error(error); - } -}; + const obj = Object.fromEntries(contexts.map((context) => [context, period])); + + await Settings.updateValueById( + 'Enterprise_License_Data', + JSON.stringify({ + ...(existingData.signed === signed && existingData), + ...existingData, + ...obj, + signed, + }), + ); + + try { + await syncWorkspace(); + } catch (error) { + console.error(error); + } + }; -// When settings are loaded, apply the current license if there is one. -settings.onReady(async () => { - if (!(await applyLicense(settings.get('Enterprise_License') ?? '', false))) { - // License from the envvar is always treated as new, because it would have been saved on the setting if it was already in use. - if (process.env.ROCKETCHAT_LICENSE && !License.hasValidLicense()) { - await applyLicense(process.env.ROCKETCHAT_LICENSE, true); + // When settings are loaded, apply the current license if there is one. + settings.onReady(async () => { + if (!(await applyLicense(settings.get('Enterprise_License') ?? '', false))) { + // License from the envvar is always treated as new, because it would have been saved on the setting if it was already in use. + if (process.env.ROCKETCHAT_LICENSE && !License.hasValidLicense()) { + await applyLicense(process.env.ROCKETCHAT_LICENSE, true); + } } - } - // After the current license is already loaded, watch the setting value to react to new licenses being applied. - settings.change('Enterprise_License', (license) => applyLicenseOrRemove(license, true)); + // After the current license is already loaded, watch the setting value to react to new licenses being applied. + settings.change('Enterprise_License', (license) => applyLicenseOrRemove(license, true)); - callbacks.add('workspaceLicenseRemoved', () => License.remove()); + callbacks.add('workspaceLicenseRemoved', () => License.remove()); - callbacks.add('workspaceLicenseChanged', (updatedLicense) => applyLicense(updatedLicense, true)); + callbacks.add('workspaceLicenseChanged', (updatedLicense) => applyLicense(updatedLicense, true)); - License.onInstall(async () => void api.broadcast('license.actions', {} as Record, boolean>)); + License.onInstall(async () => void api.broadcast('license.actions', {} as Record, boolean>)); - License.onInvalidate(async () => void api.broadcast('license.actions', {} as Record, boolean>)); + License.onInvalidate(async () => void api.broadcast('license.actions', {} as Record, boolean>)); - License.onBehaviorTriggered('prevent_action', (context) => syncByTriggerDebounced(`prevent_action_${context.limit}`)); + License.onBehaviorTriggered('prevent_action', (context) => syncByTriggerDebounced(`prevent_action_${context.limit}`)); - License.onBehaviorTriggered('start_fair_policy', async (context) => syncByTriggerDebounced(`start_fair_policy_${context.limit}`)); + License.onBehaviorTriggered('start_fair_policy', async (context) => syncByTriggerDebounced(`start_fair_policy_${context.limit}`)); - License.onBehaviorTriggered('disable_modules', async (context) => syncByTriggerDebounced(`disable_modules_${context.limit}`)); + License.onBehaviorTriggered('disable_modules', async (context) => syncByTriggerDebounced(`disable_modules_${context.limit}`)); - License.onChange(() => api.broadcast('license.sync')); + License.onChange(() => api.broadcast('license.sync')); - License.onBehaviorToggled('prevent_action', (context) => { - if (!context.limit) { - return; - } - void api.broadcast('license.actions', { - [context.limit]: true, - } as Record, boolean>); - }); + License.onBehaviorToggled('prevent_action', (context) => { + if (!context.limit) { + return; + } + void api.broadcast('license.actions', { + [context.limit]: true, + } as Record, boolean>); + }); - License.onBehaviorToggled('allow_action', (context) => { - if (!context.limit) { - return; - } - void api.broadcast('license.actions', { - [context.limit]: false, - } as Record, boolean>); + License.onBehaviorToggled('allow_action', (context) => { + if (!context.limit) { + return; + } + void api.broadcast('license.actions', { + [context.limit]: false, + } as Record, boolean>); + }); }); -}); - -License.setLicenseLimitCounter('activeUsers', () => Users.getActiveLocalUserCount()); -License.setLicenseLimitCounter('guestUsers', () => Users.getActiveLocalGuestCount()); -License.setLicenseLimitCounter('roomsPerGuest', async (context) => (context?.userId ? Subscriptions.countByUserId(context.userId) : 0)); -License.setLicenseLimitCounter('privateApps', () => getAppCount('private')); -License.setLicenseLimitCounter('marketplaceApps', () => getAppCount('marketplace')); -License.setLicenseLimitCounter('monthlyActiveContacts', () => LivechatVisitors.countVisitorsOnPeriod(moment.utc().format('YYYY-MM'))); + + License.setLicenseLimitCounter('activeUsers', () => Users.getActiveLocalUserCount()); + License.setLicenseLimitCounter('guestUsers', () => Users.getActiveLocalGuestCount()); + License.setLicenseLimitCounter('roomsPerGuest', async (context) => (context?.userId ? Subscriptions.countByUserId(context.userId) : 0)); + License.setLicenseLimitCounter('privateApps', () => getAppCount('private')); + License.setLicenseLimitCounter('marketplaceApps', () => getAppCount('marketplace')); + License.setLicenseLimitCounter('monthlyActiveContacts', () => LivechatVisitors.countVisitorsOnPeriod(moment.utc().format('YYYY-MM'))); +}; diff --git a/apps/meteor/ee/server/index.ts b/apps/meteor/ee/server/index.ts index f5b385c9a805..f00caa896e43 100644 --- a/apps/meteor/ee/server/index.ts +++ b/apps/meteor/ee/server/index.ts @@ -1,4 +1,5 @@ -import '../app/license/server/index'; +import './models/startup'; +import '../app/license/server'; import '../app/api-enterprise/server/index'; import '../app/authorization/server/index'; import '../app/canned-responses/server/index'; @@ -11,3 +12,5 @@ import './requestSeatsRoute'; import './configuration/index'; import './local-services/ldap/service'; import './methods/getReadReceipts'; + +export { registerEEBroker } from './startup'; diff --git a/apps/meteor/ee/server/models/startup.ts b/apps/meteor/ee/server/models/startup.ts index 4fd8433358ca..f77bcd1d7619 100644 --- a/apps/meteor/ee/server/models/startup.ts +++ b/apps/meteor/ee/server/models/startup.ts @@ -8,7 +8,7 @@ import('./OmnichannelServiceLevelAgreements'); import('./AuditLog'); import('./ReadReceipts'); -await License.onLicense('livechat-enterprise', () => { +void License.onLicense('livechat-enterprise', () => { import('./CannedResponse'); import('./LivechatTag'); import('./LivechatUnit'); diff --git a/apps/meteor/ee/server/startup/apps/trialExpiration.ts b/apps/meteor/ee/server/startup/apps/trialExpiration.ts index eec50e91b7dd..e6bb9e47c749 100644 --- a/apps/meteor/ee/server/startup/apps/trialExpiration.ts +++ b/apps/meteor/ee/server/startup/apps/trialExpiration.ts @@ -1,10 +1,9 @@ import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; -import { Apps } from '../../apps'; - Meteor.startup(() => { - License.onInvalidateLicense(() => { + License.onInvalidateLicense(async () => { + const { Apps } = await import('../../apps'); void Apps.disableApps(); }); }); diff --git a/apps/meteor/ee/server/startup/index.ts b/apps/meteor/ee/server/startup/index.ts index 5c0795f088c0..ade83ea57227 100644 --- a/apps/meteor/ee/server/startup/index.ts +++ b/apps/meteor/ee/server/startup/index.ts @@ -11,12 +11,14 @@ import { api } from '@rocket.chat/core-services'; import { isRunningMs } from '../../../server/lib/isRunningMs'; -// only starts network broker if running in micro services mode -if (isRunningMs()) { - const { broker } = await import('./broker'); +export const registerEEBroker = async (): Promise => { + // only starts network broker if running in micro services mode + if (isRunningMs()) { + const { broker } = await import('./broker'); - api.setBroker(broker); - void api.start(); -} else { - require('./presence'); -} + api.setBroker(broker); + void api.start(); + } else { + require('./presence'); + } +}; diff --git a/apps/meteor/imports/personal-access-tokens/server/index.ts b/apps/meteor/imports/personal-access-tokens/server/index.ts deleted file mode 100644 index 03c0fe96cba3..000000000000 --- a/apps/meteor/imports/personal-access-tokens/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './api/methods'; diff --git a/apps/meteor/imports/startup/server/index.ts b/apps/meteor/imports/startup/server/index.ts deleted file mode 100644 index 90c02562d36c..000000000000 --- a/apps/meteor/imports/startup/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -import '../../personal-access-tokens/server'; diff --git a/apps/meteor/server/configureLogLevel.ts b/apps/meteor/server/configureLogLevel.ts index b328d79a023a..7320eea97ded 100644 --- a/apps/meteor/server/configureLogLevel.ts +++ b/apps/meteor/server/configureLogLevel.ts @@ -2,7 +2,9 @@ import type { LogLevelSetting } from '@rocket.chat/logger'; import { logLevel } from '@rocket.chat/logger'; import { Settings } from '@rocket.chat/models'; -const LogLevel = await Settings.getValueById('Log_Level'); -if (LogLevel) { - logLevel.emit('changed', LogLevel as LogLevelSetting); -} +export const configureLogLevel = async (): Promise => { + const LogLevel = await Settings.getValueById('Log_Level'); + if (LogLevel) { + logLevel.emit('changed', LogLevel as LogLevelSetting); + } +}; diff --git a/apps/meteor/server/main.ts b/apps/meteor/server/main.ts index b9418fe43830..9d0a5ff8ed87 100644 --- a/apps/meteor/server/main.ts +++ b/apps/meteor/server/main.ts @@ -1,85 +1,38 @@ import './models/startup'; -import './configureLogLevel'; -import './settings/index'; -import '../ee/server/models/startup'; -import './services/startup'; -import '../app/settings/server'; -import '../lib/oauthRedirectUriServer'; +/** + * ./settings uses top level await, in theory the settings creation + * and the startup should be done in parallel + */ +import './settings'; +import { startLicense } from '../ee/app/license/server/startup'; +import { registerEEBroker } from '../ee/server'; +import { configureLogLevel } from './configureLogLevel'; +import { registerServices } from './services/startup'; +import { startup } from './startup'; + import './lib/logger/startup'; -import './importPackages'; -import '../imports/startup/server'; + import '../app/lib/server/startup'; -import '../ee/server/startup'; -import './startup'; -import '../ee/server'; +import './importPackages'; +import './methods'; +import './publications'; +import './routes'; +import '../lib/oauthRedirectUriServer'; + import './lib/pushConfig'; + import './configuration/accounts_meld'; import './configuration/ldap'; -import './methods/OEmbedCacheCleanup'; -import './methods/addAllUserToRoom'; -import './methods/addRoomLeader'; -import './methods/addRoomModerator'; -import './methods/addRoomOwner'; -import './methods/afterVerifyEmail'; -import './methods/browseChannels'; -import './methods/canAccessRoom'; -import './methods/channelsList'; -import './methods/createDirectMessage'; -import './methods/deleteFileMessage'; -import './methods/deleteUser'; -import './methods/eraseRoom'; -import './methods/getAvatarSuggestion'; -import './methods/getPasswordPolicy'; -import './methods/getRoomById'; -import './methods/getRoomIdByNameOrId'; -import './methods/getRoomNameById'; -import './methods/getSetupWizardParameters'; -import './methods/getTotalChannels'; -import './methods/getUsersOfRoom'; -import './methods/hideRoom'; -import './methods/ignoreUser'; -import './methods/loadHistory'; -import './methods/loadLocale'; -import './methods/loadMissedMessages'; -import './methods/loadNextMessages'; -import './methods/loadSurroundingMessages'; -import './methods/logoutCleanUp'; -import './methods/messageSearch'; -import './methods/muteUserInRoom'; -import './methods/openRoom'; -import './methods/readMessages'; -import './methods/readThreads'; -import './methods/registerUser'; -import './methods/removeRoomLeader'; -import './methods/removeRoomModerator'; -import './methods/removeRoomOwner'; -import './methods/removeUserFromRoom'; -import './methods/reportMessage'; -import './methods/requestDataDownload'; -import './methods/resetAvatar'; -import './methods/roomNameExists'; -import './methods/saveUserPreferences'; -import './methods/saveUserProfile'; -import './methods/sendConfirmationEmail'; -import './methods/sendForgotPasswordEmail'; -import './methods/setAvatarFromService'; -import './methods/setUserActiveStatus'; -import './methods/setUserPassword'; -import './methods/toggleFavorite'; -import './methods/unmuteUserInRoom'; -import './methods/userPresence'; -import './methods/userSetUtcOffset'; -import './publications/messages'; -import './publications/room'; -import './publications/settings'; -import './publications/spotlight'; -import './publications/subscription'; -import './routes/avatar'; -import './routes/health'; -import './routes/i18n'; -import './routes/timesync'; -import './routes/userDataDownload'; -import './stream/stdout'; +import './stream/stdout'; import './features/EmailInbox/index'; + +await (async () => { + await configureLogLevel(); + await registerServices(); + await import('../app/settings/server'); + await registerEEBroker(); + await startup(); + await startLicense(); +})(); diff --git a/apps/meteor/server/methods/index.ts b/apps/meteor/server/methods/index.ts new file mode 100644 index 000000000000..27c345964637 --- /dev/null +++ b/apps/meteor/server/methods/index.ts @@ -0,0 +1,56 @@ +import '../../imports/personal-access-tokens/server/api/methods'; + +import './OEmbedCacheCleanup'; +import './addAllUserToRoom'; +import './addRoomLeader'; +import './addRoomModerator'; +import './addRoomOwner'; +import './afterVerifyEmail'; +import './browseChannels'; +import './canAccessRoom'; +import './channelsList'; +import './createDirectMessage'; +import './deleteFileMessage'; +import './deleteUser'; +import './eraseRoom'; +import './getAvatarSuggestion'; +import './getPasswordPolicy'; +import './getRoomById'; +import './getRoomIdByNameOrId'; +import './getRoomNameById'; +import './getSetupWizardParameters'; +import './getTotalChannels'; +import './getUsersOfRoom'; +import './hideRoom'; +import './ignoreUser'; +import './loadHistory'; +import './loadLocale'; +import './loadMissedMessages'; +import './loadNextMessages'; +import './loadSurroundingMessages'; +import './logoutCleanUp'; +import './messageSearch'; +import './muteUserInRoom'; +import './openRoom'; +import './readMessages'; +import './readThreads'; +import './registerUser'; +import './removeRoomLeader'; +import './removeRoomModerator'; +import './removeRoomOwner'; +import './removeUserFromRoom'; +import './reportMessage'; +import './requestDataDownload'; +import './resetAvatar'; +import './roomNameExists'; +import './saveUserPreferences'; +import './saveUserProfile'; +import './sendConfirmationEmail'; +import './sendForgotPasswordEmail'; +import './setAvatarFromService'; +import './setUserActiveStatus'; +import './setUserPassword'; +import './toggleFavorite'; +import './unmuteUserInRoom'; +import './userPresence'; +import './userSetUtcOffset'; diff --git a/apps/meteor/server/publications/index.ts b/apps/meteor/server/publications/index.ts new file mode 100644 index 000000000000..b880933c2e30 --- /dev/null +++ b/apps/meteor/server/publications/index.ts @@ -0,0 +1,5 @@ +import './messages'; +import './room'; +import './settings'; +import './spotlight'; +import './subscription'; diff --git a/apps/meteor/server/routes/index.ts b/apps/meteor/server/routes/index.ts new file mode 100644 index 000000000000..e60f0ceb3f24 --- /dev/null +++ b/apps/meteor/server/routes/index.ts @@ -0,0 +1,5 @@ +import './avatar'; +import './health'; +import './i18n'; +import './timesync'; +import './userDataDownload'; diff --git a/apps/meteor/server/services/startup.ts b/apps/meteor/server/services/startup.ts index 3b13ff75497b..c7b2fa547a42 100644 --- a/apps/meteor/server/services/startup.ts +++ b/apps/meteor/server/services/startup.ts @@ -31,38 +31,38 @@ import { UploadService } from './upload/service'; import { VideoConfService } from './video-conference/service'; import { VoipService } from './voip/service'; -const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; +export const registerServices = async (): Promise => { + const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; -api.registerService(new AppsEngineService()); -api.registerService(new AnalyticsService()); -api.registerService(new AuthorizationLivechat()); -api.registerService(new BannerService()); -api.registerService(new CalendarService()); -api.registerService(new LDAPService()); -api.registerService(new MediaService()); -api.registerService(new MeteorService()); -api.registerService(new NPSService()); -api.registerService(new RoomService()); -api.registerService(new SAUMonitorService()); -api.registerService(new VoipService(db)); -api.registerService(new OmnichannelService()); -api.registerService(new OmnichannelVoipService()); -api.registerService(new TeamService()); -api.registerService(new UiKitCoreAppService()); -api.registerService(new PushService()); -api.registerService(new DeviceManagementService()); -api.registerService(new VideoConfService()); -api.registerService(new UploadService()); -api.registerService(new MessageService()); -api.registerService(new TranslationService()); -api.registerService(new SettingsService()); -api.registerService(new OmnichannelIntegrationService()); -api.registerService(new ImportService()); -api.registerService(new OmnichannelAnalyticsService()); + api.registerService(new AppsEngineService()); + api.registerService(new AnalyticsService()); + api.registerService(new AuthorizationLivechat()); + api.registerService(new BannerService()); + api.registerService(new CalendarService()); + api.registerService(new LDAPService()); + api.registerService(new MediaService()); + api.registerService(new MeteorService()); + api.registerService(new NPSService()); + api.registerService(new RoomService()); + api.registerService(new SAUMonitorService()); + api.registerService(new VoipService(db)); + api.registerService(new OmnichannelService()); + api.registerService(new OmnichannelVoipService()); + api.registerService(new TeamService()); + api.registerService(new UiKitCoreAppService()); + api.registerService(new PushService()); + api.registerService(new DeviceManagementService()); + api.registerService(new VideoConfService()); + api.registerService(new UploadService()); + api.registerService(new MessageService()); + api.registerService(new TranslationService()); + api.registerService(new SettingsService()); + api.registerService(new OmnichannelIntegrationService()); + api.registerService(new ImportService()); + api.registerService(new OmnichannelAnalyticsService()); -// if the process is running in micro services mode we don't need to register services that will run separately -if (!isRunningMs()) { - void (async (): Promise => { + // if the process is running in micro services mode we don't need to register services that will run separately + if (!isRunningMs()) { const { Presence } = await import('@rocket.chat/presence'); const { Authorization } = await import('./authorization/service'); @@ -75,5 +75,5 @@ if (!isRunningMs()) { // Always register the service and manage licensing inside the service (tbd) api.registerService(new QueueWorker(db, Logger)); api.registerService(new OmnichannelTranscript(Logger)); - })(); -} + } +}; diff --git a/apps/meteor/server/settings/index.ts b/apps/meteor/server/settings/index.ts index aaae0b7ba0bd..2a7973eddeec 100644 --- a/apps/meteor/server/settings/index.ts +++ b/apps/meteor/server/settings/index.ts @@ -37,47 +37,43 @@ import { createVConfSettings } from './video-conference'; import { createWebDavSettings } from './webdav'; import { createWebRTCSettings } from './webrtc'; -async function createSettings() { - await Promise.all([ - createAccountSettings(), - createAnalyticsSettings(), - createAssetsSettings(), - createBotsSettings(), - createCallCenterSettings(), - createCasSettings(), - createCrowdSettings(), - createEmojiSettings(), - createSoundsSettings(), - createDiscussionsSettings(), - createEmailSettings(), - createE2ESettings(), - createFederationSettings(), - createFileUploadSettings(), - createGeneralSettings(), - createIRCSettings(), - createLdapSettings(), - createLogSettings(), - createLayoutSettings(), - createMessageSettings(), - createMetaSettings(), - createMiscSettings(), - createMobileSettings(), - createOauthSettings(), - createOmniSettings(), - createOTRSettings(), - createPushSettings(), - createRateLimitSettings(), - createRetentionSettings(), - createSetupWSettings(), - createSlackBridgeSettings(), - createSmarshSettings(), - createThreadSettings(), - createTroubleshootSettings(), - createVConfSettings(), - createUserDataSettings(), - createWebDavSettings(), - createWebRTCSettings(), - ]); -} - -await createSettings(); +await Promise.all([ + createAccountSettings(), + createAnalyticsSettings(), + createAssetsSettings(), + createBotsSettings(), + createCallCenterSettings(), + createCasSettings(), + createCrowdSettings(), + createEmojiSettings(), + createSoundsSettings(), + createDiscussionsSettings(), + createEmailSettings(), + createE2ESettings(), + createFederationSettings(), + createFileUploadSettings(), + createGeneralSettings(), + createIRCSettings(), + createLdapSettings(), + createLogSettings(), + createLayoutSettings(), + createMessageSettings(), + createMetaSettings(), + createMiscSettings(), + createMobileSettings(), + createOauthSettings(), + createOmniSettings(), + createOTRSettings(), + createPushSettings(), + createRateLimitSettings(), + createRetentionSettings(), + createSetupWSettings(), + createSlackBridgeSettings(), + createSmarshSettings(), + createThreadSettings(), + createTroubleshootSettings(), + createVConfSettings(), + createUserDataSettings(), + createWebDavSettings(), + createWebRTCSettings(), +]); diff --git a/apps/meteor/server/startup/index.ts b/apps/meteor/server/startup/index.ts index 17f53ae2d63d..ccbd621025c5 100644 --- a/apps/meteor/server/startup/index.ts +++ b/apps/meteor/server/startup/index.ts @@ -1,4 +1,3 @@ -import './migrations'; import './appcache'; import './callbacks'; import './cron'; @@ -10,9 +9,13 @@ import '../hooks'; import '../lib/rooms/roomTypes'; import '../lib/settingsRegenerator'; import { isRunningMs } from '../lib/isRunningMs'; +import { performMigrationProcedure } from './migrations'; -// only starts network broker if running in micro services mode -if (!isRunningMs()) { - require('./localServices'); - require('./watchDb'); -} +export const startup = async () => { + await performMigrationProcedure(); + // only starts network broker if running in micro services mode + if (!isRunningMs()) { + require('./localServices'); + require('./watchDb'); + } +}; diff --git a/apps/meteor/server/startup/migrations/index.ts b/apps/meteor/server/startup/migrations/index.ts index 8247f9a72bb5..4cda096b151c 100644 --- a/apps/meteor/server/startup/migrations/index.ts +++ b/apps/meteor/server/startup/migrations/index.ts @@ -37,4 +37,5 @@ import './v300'; import './v301'; import './v303'; import './v304'; -import './xrun'; + +export * from './xrun'; diff --git a/apps/meteor/server/startup/migrations/xrun.ts b/apps/meteor/server/startup/migrations/xrun.ts index c560d488187c..61cfaff50231 100644 --- a/apps/meteor/server/startup/migrations/xrun.ts +++ b/apps/meteor/server/startup/migrations/xrun.ts @@ -6,10 +6,11 @@ const { MIGRATION_VERSION = 'latest' } = process.env; const [version, ...subcommands] = MIGRATION_VERSION.split(','); -await migrateDatabase(version === 'latest' ? version : parseInt(version), subcommands); - -// perform operations when the server is starting with a different version -await onServerVersionChange(async () => { - await upsertPermissions(); - await ensureCloudWorkspaceRegistered(); -}); +export const performMigrationProcedure = async (): Promise => { + await migrateDatabase(version === 'latest' ? version : parseInt(version), subcommands); + // perform operations when the server is starting with a different version + await onServerVersionChange(async () => { + await upsertPermissions(); + await ensureCloudWorkspaceRegistered(); + }); +}; From de2658e87438fe639f07bd3af74ef6d6bc74631b Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Mon, 15 Jan 2024 16:24:47 +0000 Subject: [PATCH 051/152] Bump 6.5.3 --- .changeset/bump-patch-1705335886930.md | 5 +++++ yarn.lock | 16 ++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 .changeset/bump-patch-1705335886930.md diff --git a/.changeset/bump-patch-1705335886930.md b/.changeset/bump-patch-1705335886930.md new file mode 100644 index 000000000000..e1eaa7980afb --- /dev/null +++ b/.changeset/bump-patch-1705335886930.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/yarn.lock b/yarn.lock index e74b35ec9f98..4f42755cc462 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8217,9 +8217,9 @@ __metadata: "@rocket.chat/icons": "*" "@rocket.chat/prettier-config": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-contexts": 3.0.1 + "@rocket.chat/ui-contexts": 3.0.2 "@rocket.chat/ui-kit": "*" - "@rocket.chat/ui-video-conf": 3.0.1 + "@rocket.chat/ui-video-conf": 3.0.2 "@tanstack/react-query": "*" react: "*" react-dom: "*" @@ -8301,14 +8301,14 @@ __metadata: ts-jest: ~29.0.5 typescript: ~5.2.2 peerDependencies: - "@rocket.chat/core-typings": 6.5.1 + "@rocket.chat/core-typings": 6.5.2 "@rocket.chat/css-in-js": "*" "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-tokens": "*" "@rocket.chat/message-parser": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-client": 3.0.1 - "@rocket.chat/ui-contexts": 3.0.1 + "@rocket.chat/ui-client": 3.0.2 + "@rocket.chat/ui-contexts": 3.0.2 katex: "*" react: "*" languageName: unknown @@ -9469,7 +9469,7 @@ __metadata: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" - "@rocket.chat/ui-contexts": 3.0.1 + "@rocket.chat/ui-contexts": 3.0.2 react: ~17.0.2 languageName: unknown linkType: soft @@ -9622,7 +9622,7 @@ __metadata: "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-contexts": 3.0.1 + "@rocket.chat/ui-contexts": 3.0.2 react: ^17.0.2 react-dom: ^17.0.2 languageName: unknown @@ -9708,7 +9708,7 @@ __metadata: peerDependencies: "@rocket.chat/layout": "*" "@rocket.chat/tools": "*" - "@rocket.chat/ui-contexts": 3.0.1 + "@rocket.chat/ui-contexts": 3.0.2 "@tanstack/react-query": "*" react: "*" react-hook-form: "*" From 65772e49a06a4d9360a187b1c60a303d45733d14 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Mon, 15 Jan 2024 13:28:02 -0300 Subject: [PATCH 052/152] refactor(client): Move components from `app/` to `client/` (#31234) --- .../client/lib/{userCard.tsx => userCard.ts} | 22 ++--- .../UserAvatarEditor/UserAvatarEditor.tsx | 83 ++++++++++--------- .../UserAvatarEditor/UserAvatarSuggestion.ts | 6 ++ .../UserAvatarSuggestions.tsx | 48 ++++------- .../UserAvatarEditor/readFileAsDataURL.ts | 16 ++++ .../useUserAvatarSuggestions.ts | 12 +++ .../useOmnichannelExternalFrameRoomAction.ts | 2 +- .../client/hooks/useAvatarSuggestions.ts | 8 -- .../client/lib/portals/portalsSubscription.ts | 8 +- .../omnichannel}/ExternalFrameContainer.tsx | 10 +-- .../client/views/room/UserCardHolder.tsx | 22 +++++ .../views/room/composer}/ComposerBoxPopup.tsx | 6 +- .../ComposerBoxPopupCannedResponse.tsx | 6 +- .../room/composer}/ComposerBoxPopupEmoji.tsx | 8 +- .../composer}/ComposerBoxPopupPreview.tsx | 33 ++++---- .../room/composer}/ComposerBoxPopupRoom.tsx | 6 +- .../ComposerBoxPopupSlashCommand.tsx | 6 +- .../room/composer}/ComposerBoxPopupUser.tsx | 8 +- .../composer}/hooks/useComposerBoxPopup.ts | 4 +- .../hooks/useComposerBoxPopupQueries.ts | 4 +- .../composer}/hooks/useEnablePopupPreview.ts | 2 +- .../room/composer/messageBox/MessageBox.tsx | 8 +- .../room/providers/ComposerPopupProvider.tsx | 26 +++--- .../server/methods/getAvatarSuggestion.ts | 18 ++-- package.json | 2 +- packages/rest-typings/src/v1/users.ts | 20 ++--- yarn.lock | 60 +++++++------- 27 files changed, 241 insertions(+), 213 deletions(-) rename apps/meteor/app/ui/client/lib/{userCard.tsx => userCard.ts} (70%) create mode 100644 apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarSuggestion.ts create mode 100644 apps/meteor/client/components/avatar/UserAvatarEditor/readFileAsDataURL.ts create mode 100644 apps/meteor/client/components/avatar/UserAvatarEditor/useUserAvatarSuggestions.ts delete mode 100644 apps/meteor/client/hooks/useAvatarSuggestions.ts rename apps/meteor/{app/livechat/client/externalFrame => client/views/omnichannel}/ExternalFrameContainer.tsx (83%) create mode 100644 apps/meteor/client/views/room/UserCardHolder.tsx rename apps/meteor/{app/ui-message/client/popup => client/views/room/composer}/ComposerBoxPopup.tsx (97%) rename apps/meteor/{app/ui-message/client/popup/components/composerBoxPopup => client/views/room/composer}/ComposerBoxPopupCannedResponse.tsx (69%) rename apps/meteor/{app/ui-message/client/popup/components/composerBoxPopup => client/views/room/composer}/ComposerBoxPopupEmoji.tsx (63%) rename apps/meteor/{app/ui-message/client/popup/components/composerBoxPopupPreview => client/views/room/composer}/ComposerBoxPopupPreview.tsx (86%) rename apps/meteor/{app/ui-message/client/popup/components/composerBoxPopup => client/views/room/composer}/ComposerBoxPopupRoom.tsx (74%) rename apps/meteor/{app/ui-message/client/popup/components/composerBoxPopup => client/views/room/composer}/ComposerBoxPopupSlashCommand.tsx (74%) rename apps/meteor/{app/ui-message/client/popup/components/composerBoxPopup => client/views/room/composer}/ComposerBoxPopupUser.tsx (84%) rename apps/meteor/{app/ui-message/client/popup => client/views/room/composer}/hooks/useComposerBoxPopup.ts (97%) rename apps/meteor/{app/ui-message/client/popup => client/views/room/composer}/hooks/useComposerBoxPopupQueries.ts (89%) rename apps/meteor/{app/ui-message/client/popup => client/views/room/composer}/hooks/useEnablePopupPreview.ts (71%) diff --git a/apps/meteor/app/ui/client/lib/userCard.tsx b/apps/meteor/app/ui/client/lib/userCard.ts similarity index 70% rename from apps/meteor/app/ui/client/lib/userCard.tsx rename to apps/meteor/app/ui/client/lib/userCard.ts index e4fd5b343140..90b3d4ec5201 100644 --- a/apps/meteor/app/ui/client/lib/userCard.tsx +++ b/apps/meteor/app/ui/client/lib/userCard.ts @@ -1,14 +1,12 @@ import type { ComponentProps } from 'react'; -import React, { Suspense, createElement, lazy } from 'react'; +import { createElement } from 'react'; import { createPortal } from 'react-dom'; -import { useSyncExternalStore } from 'use-sync-external-store/shim'; import { registerPortal } from '../../../../client/lib/portals/portalsSubscription'; import { queueMicrotask } from '../../../../client/lib/utils/queueMicrotask'; +import UserCardHolder from '../../../../client/views/room/UserCardHolder'; -const UserCard = lazy(() => import('../../../../client/views/room/UserCard')); - -type UserCardProps = ComponentProps; +type UserCardProps = ReturnType['getProps']>; let props: UserCardProps; @@ -29,16 +27,6 @@ const subscribeToProps = (callback: () => void) => { }; }; -const UserCardWithProps = () => { - const props = useSyncExternalStore(subscribeToProps, getProps); - - return ( - - - - ); -}; - const createContainer = () => { const container = document.createElement('div'); container.id = 'react-user-card'; @@ -67,8 +55,8 @@ export const openUserCard = (params: Omit) => { } if (!unregisterPortal) { - const children = createElement(UserCardWithProps); - const portal = <>{createPortal(children, container)}; + const children = createElement(UserCardHolder, { getProps, subscribeToProps }); + const portal = createPortal(children, container); unregisterPortal = registerPortal(container, portal); } diff --git a/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.tsx b/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.tsx index ae03418be755..aec9961e8631 100644 --- a/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.tsx +++ b/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.tsx @@ -1,5 +1,5 @@ import type { IUser, AvatarObject } from '@rocket.chat/core-typings'; -import { Box, Button, TextInput, Margins, Avatar, IconButton } from '@rocket.chat/fuselage'; +import { Box, Button, TextInput, Avatar, IconButton } from '@rocket.chat/fuselage'; import { useToastMessageDispatch, useSetting, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ChangeEvent } from 'react'; import React, { useState, useCallback } from 'react'; @@ -7,17 +7,11 @@ import React, { useState, useCallback } from 'react'; import { useSingleFileInput } from '../../../hooks/useSingleFileInput'; import { isValidImageFormat } from '../../../lib/utils/isValidImageFormat'; import UserAvatar from '../UserAvatar'; +import type { UserAvatarSuggestion } from './UserAvatarSuggestion'; import UserAvatarSuggestions from './UserAvatarSuggestions'; +import { readFileAsDataURL } from './readFileAsDataURL'; -const toDataURL = (file: File, callback: (result: FileReader['result']) => void): void => { - const reader = new FileReader(); - reader.onloadend = function (e): void { - callback(e?.target?.result || null); - }; - reader.readAsDataURL(file); -}; - -type UserAvatarEditorType = { +type UserAvatarEditorProps = { currentUsername: IUser['username']; username: IUser['username']; setAvatarObj: (obj: AvatarObject) => void; @@ -25,7 +19,7 @@ type UserAvatarEditorType = { etag: IUser['avatarETag']; }; -function UserAvatarEditor({ currentUsername, username, setAvatarObj, disabled, etag }: UserAvatarEditorType): ReactElement { +function UserAvatarEditor({ currentUsername, username, setAvatarObj, disabled, etag }: UserAvatarEditorProps): ReactElement { const t = useTranslation(); const rotateImages = useSetting('FileUpload_RotateImages'); const [avatarFromUrl, setAvatarFromUrl] = useState(''); @@ -35,14 +29,15 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, disabled, e const setUploadedPreview = useCallback( async (file, avatarObj) => { setAvatarObj(avatarObj); - toDataURL(file, async (dataURL) => { - if (typeof dataURL === 'string' && (await isValidImageFormat(dataURL))) { + try { + const dataURL = await readFileAsDataURL(file); + + if (await isValidImageFormat(dataURL)) { setNewAvatarSource(dataURL); - return; } - + } catch (error) { dispatchToastMessage({ type: 'error', message: t('Avatar_format_invalid') }); - }); + } }, [setAvatarObj, t, dispatchToastMessage], ); @@ -65,6 +60,14 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, disabled, e setAvatarFromUrl(event.currentTarget.value); }; + const handleSelectSuggestion = useCallback( + (suggestion: UserAvatarSuggestion) => { + setAvatarObj(suggestion as unknown as AvatarObject); + setNewAvatarSource(suggestion.blob); + }, + [setAvatarObj, setNewAvatarSource], + ); + return ( {t('Profile_picture')} @@ -81,32 +84,30 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, disabled, e /> - - - - - - - - - {t('Use_url_for_avatar')} - + + + + - + + + {t('Use_url_for_avatar')} + diff --git a/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarSuggestion.ts b/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarSuggestion.ts new file mode 100644 index 000000000000..e2ac26c6d0f3 --- /dev/null +++ b/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarSuggestion.ts @@ -0,0 +1,6 @@ +export type UserAvatarSuggestion = { + blob: string; + contentType: string; + service: string; + url: string; +}; diff --git a/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarSuggestions.tsx b/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarSuggestions.tsx index 04b0c92acd95..4ccb7d304683 100644 --- a/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarSuggestions.tsx +++ b/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarSuggestions.tsx @@ -1,43 +1,31 @@ -import type { AvatarObject } from '@rocket.chat/core-typings'; -import { Box, Button, Margins, Avatar } from '@rocket.chat/fuselage'; +import { Button, Avatar } from '@rocket.chat/fuselage'; import React, { useCallback } from 'react'; -import { useAvatarSuggestions } from '../../../hooks/useAvatarSuggestions'; +import type { UserAvatarSuggestion } from './UserAvatarSuggestion'; +import { useUserAvatarSuggestions } from './useUserAvatarSuggestions'; type UserAvatarSuggestionsProps = { - setAvatarObj: (obj: AvatarObject) => void; - setNewAvatarSource: (source: string) => void; disabled?: boolean; + onSelectOne?: (suggestion: UserAvatarSuggestion) => void; }; -const UserAvatarSuggestions = ({ setAvatarObj, setNewAvatarSource, disabled }: UserAvatarSuggestionsProps) => { - const handleClick = useCallback( - (suggestion) => () => { - setAvatarObj(suggestion); - setNewAvatarSource(suggestion.blob); - }, - [setAvatarObj, setNewAvatarSource], - ); +function UserAvatarSuggestions({ disabled, onSelectOne }: UserAvatarSuggestionsProps) { + const { data: suggestions = [] } = useUserAvatarSuggestions(); - const { data } = useAvatarSuggestions(); - const suggestions = Object.values(data?.suggestions || {}); + const handleClick = useCallback((suggestion: UserAvatarSuggestion) => () => onSelectOne?.(suggestion), [onSelectOne]); return ( - - {suggestions && - suggestions.length > 0 && - suggestions.map( - (suggestion) => - suggestion.blob && ( - - ), - )} - + <> + {suggestions.map( + (suggestion) => + suggestion.blob && ( + + ), + )} + ); -}; +} export default UserAvatarSuggestions; diff --git a/apps/meteor/client/components/avatar/UserAvatarEditor/readFileAsDataURL.ts b/apps/meteor/client/components/avatar/UserAvatarEditor/readFileAsDataURL.ts new file mode 100644 index 000000000000..d2bb3f7beed1 --- /dev/null +++ b/apps/meteor/client/components/avatar/UserAvatarEditor/readFileAsDataURL.ts @@ -0,0 +1,16 @@ +export const readFileAsDataURL = (file: File) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = (event) => { + const result = event.target?.result; + if (typeof result === 'string') { + resolve(result); + return; + } + reject(new Error('Failed to read file')); + }; + reader.onerror = (event) => { + reject(new Error(`Failed to read file: ${event}`)); + }; + reader.readAsDataURL(file); + }); diff --git a/apps/meteor/client/components/avatar/UserAvatarEditor/useUserAvatarSuggestions.ts b/apps/meteor/client/components/avatar/UserAvatarEditor/useUserAvatarSuggestions.ts new file mode 100644 index 000000000000..42bb09406d3b --- /dev/null +++ b/apps/meteor/client/components/avatar/UserAvatarEditor/useUserAvatarSuggestions.ts @@ -0,0 +1,12 @@ +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; + +export const useUserAvatarSuggestions = () => { + const getAvatarSuggestions = useEndpoint('GET', '/v1/users.getAvatarSuggestion'); + + return useQuery({ + queryKey: ['account', 'profile', 'avatar-suggestions'], + queryFn: async () => getAvatarSuggestions(), + select: (data) => Object.values(data.suggestions), + }); +}; diff --git a/apps/meteor/client/hooks/roomActions/useOmnichannelExternalFrameRoomAction.ts b/apps/meteor/client/hooks/roomActions/useOmnichannelExternalFrameRoomAction.ts index 7f03b369b449..fe1e704f0b1c 100644 --- a/apps/meteor/client/hooks/roomActions/useOmnichannelExternalFrameRoomAction.ts +++ b/apps/meteor/client/hooks/roomActions/useOmnichannelExternalFrameRoomAction.ts @@ -3,7 +3,7 @@ import { lazy, useMemo } from 'react'; import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext'; -const ExternalFrameContainer = lazy(() => import('../../../app/livechat/client/externalFrame/ExternalFrameContainer')); +const ExternalFrameContainer = lazy(() => import('../../views/omnichannel/ExternalFrameContainer')); export const useOmnichannelExternalFrameRoomAction = () => { const enabled = useSetting('Omnichannel_External_Frame_Enabled', false); diff --git a/apps/meteor/client/hooks/useAvatarSuggestions.ts b/apps/meteor/client/hooks/useAvatarSuggestions.ts deleted file mode 100644 index 223cab8ca4b4..000000000000 --- a/apps/meteor/client/hooks/useAvatarSuggestions.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { useEndpoint } from '@rocket.chat/ui-contexts'; -import { useQuery } from '@tanstack/react-query'; - -export const useAvatarSuggestions = () => { - const getAvatarSuggestions = useEndpoint('GET', '/v1/users.getAvatarSuggestion'); - - return useQuery(['getAvatarSuggestion'], async () => getAvatarSuggestions()); -}; diff --git a/apps/meteor/client/lib/portals/portalsSubscription.ts b/apps/meteor/client/lib/portals/portalsSubscription.ts index 24295d624936..513393eb983a 100644 --- a/apps/meteor/client/lib/portals/portalsSubscription.ts +++ b/apps/meteor/client/lib/portals/portalsSubscription.ts @@ -1,9 +1,9 @@ import { Emitter } from '@rocket.chat/emitter'; import { Random } from '@rocket.chat/random'; -import type { ReactElement } from 'react'; +import type { ReactPortal } from 'react'; type SubscribedPortal = { - portal: ReactElement; + portal: ReactPortal; key: string; }; @@ -11,7 +11,7 @@ type PortalsSubscription = { subscribe: (callback: () => void) => () => void; getSnapshot: () => SubscribedPortal[]; has: (key: unknown) => boolean; - set: (key: unknown, portal: ReactElement) => void; + set: (key: unknown, portal: ReactPortal) => void; delete: (key: unknown) => void; }; @@ -43,7 +43,7 @@ export const unregisterPortal = (key: unknown): void => { portalsSubscription.delete(key); }; -export const registerPortal = (key: unknown, portal: ReactElement): (() => void) => { +export const registerPortal = (key: unknown, portal: ReactPortal): (() => void) => { portalsSubscription.set(key, portal); return (): void => { unregisterPortal(key); diff --git a/apps/meteor/app/livechat/client/externalFrame/ExternalFrameContainer.tsx b/apps/meteor/client/views/omnichannel/ExternalFrameContainer.tsx similarity index 83% rename from apps/meteor/app/livechat/client/externalFrame/ExternalFrameContainer.tsx rename to apps/meteor/client/views/omnichannel/ExternalFrameContainer.tsx index 5b37ee8de2d2..e339d48af103 100644 --- a/apps/meteor/app/livechat/client/externalFrame/ExternalFrameContainer.tsx +++ b/apps/meteor/client/views/omnichannel/ExternalFrameContainer.tsx @@ -2,11 +2,11 @@ import { useSetting, useUserId } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import React, { useMemo } from 'react'; -import { useRoom } from '../../../../client/views/room/contexts/RoomContext'; -import { sdk } from '../../../utils/client/lib/SDKClient'; -import { encrypt, getKeyFromString } from './crypto'; +import { encrypt, getKeyFromString } from '../../../app/livechat/client/externalFrame/crypto'; +import { sdk } from '../../../app/utils/client/lib/SDKClient'; +import { useRoom } from '../room/contexts/RoomContext'; -const ExternalFrameContainer = () => { +function ExternalFrameContainer() { const uid = useUserId(); const room = useRoom(); const { 'X-Auth-Token': authToken } = sdk.rest.getCredentials() || {}; @@ -42,6 +42,6 @@ const ExternalFrameContainer = () => {