diff --git a/.changeset/add-tagalog-language.md b/.changeset/add-tagalog-language.md new file mode 100644 index 0000000000000..b36a3739fbca1 --- /dev/null +++ b/.changeset/add-tagalog-language.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/meteor': patch +'@rocket.chat/i18n': minor +--- + +Added Tagalog (`tl`) as a selectable interface language. It appears in the user's **Account → Preferences → Localization → Language** dropdown; interface strings fall back to English until translations are contributed (same approach as other not-yet-translated locales). diff --git a/.changeset/all-readers-reply.md b/.changeset/all-readers-reply.md new file mode 100644 index 0000000000000..a11015f20cd48 --- /dev/null +++ b/.changeset/all-readers-reply.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Shows a confirmation modal when switching attribute store setting diff --git a/.changeset/bump-patch-1782440081682.md b/.changeset/bump-patch-1782440081682.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1782440081682.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1782512866566.md b/.changeset/bump-patch-1782512866566.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1782512866566.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1783039947861.md b/.changeset/bump-patch-1783039947861.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1783039947861.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bumpy-coats-reply.md b/.changeset/bumpy-coats-reply.md new file mode 100644 index 0000000000000..d261107317837 --- /dev/null +++ b/.changeset/bumpy-coats-reply.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes a memory leakage on the CodeMirror component (used by `code`-typed settings) diff --git a/.changeset/chubby-garlics-decide.md b/.changeset/chubby-garlics-decide.md new file mode 100644 index 0000000000000..4350c7a318cf4 --- /dev/null +++ b/.changeset/chubby-garlics-decide.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/apps': patch +--- + +Replaces {} with Object.create(null) to ensure defense-in-depth against prototype pollution diff --git a/.changeset/crisp-corners-itch.md b/.changeset/crisp-corners-itch.md new file mode 100644 index 0000000000000..1a02cccba6f9a --- /dev/null +++ b/.changeset/crisp-corners-itch.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes S3 file upload failing when the region setting is empty or the endpoint is configured without a URL scheme diff --git a/.changeset/crisp-taxis-pay.md b/.changeset/crisp-taxis-pay.md new file mode 100644 index 0000000000000..5ccb2c7abc7f5 --- /dev/null +++ b/.changeset/crisp-taxis-pay.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes `users.sendConfirmationEmail` rejecting unauthenticated requests, which prevented unverified users from resending their verification email from the login screen diff --git a/.changeset/cute-humans-follow.md b/.changeset/cute-humans-follow.md new file mode 100644 index 0000000000000..3788df75f8d2d --- /dev/null +++ b/.changeset/cute-humans-follow.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/model-typings': patch +'@rocket.chat/models': patch +'@rocket.chat/meteor': patch +--- + +Fixes auto-translate not activating for users who set their language preference after joining rooms diff --git a/.changeset/ddp-migrate-batch2-callers.md b/.changeset/ddp-migrate-batch2-callers.md new file mode 100644 index 0000000000000..9558b6c12b085 --- /dev/null +++ b/.changeset/ddp-migrate-batch2-callers.md @@ -0,0 +1,12 @@ +--- +'@rocket.chat/meteor': patch +--- + +Migrate six client DDP callers to their REST equivalents (the DDP methods stay registered on the server for external SDK/mobile clients, with a deprecation log pointing at the REST route until 9.0.0 removes them): + +- `loadMissedMessages` → `GET /v1/chat.syncMessages` +- `joinRoom` → `POST /v1/channels.join` (channel-only; non-`c` rooms now error via REST the same way they used to via DDP) +- `userSetUtcOffset` → `POST /v1/users.setPreferences` (new `utcOffset` field) +- `deleteFileMessage` → `POST /v1/chat.delete` (new `fileId` body shape) +- `spotlight` → `GET /v1/spotlight` (new `usernames` / `type` / `rid` query params) +- `listCustomSounds` → `GET /v1/custom-sounds.list` diff --git a/.changeset/ddp-migrate-batch3-callers.md b/.changeset/ddp-migrate-batch3-callers.md new file mode 100644 index 0000000000000..4606c3a4adcc1 --- /dev/null +++ b/.changeset/ddp-migrate-batch3-callers.md @@ -0,0 +1,10 @@ +--- +'@rocket.chat/meteor': patch +--- + +Migrate four client DDP callers to their REST equivalents (the DDP methods stay registered on the server for external SDK/mobile clients, with a deprecation log pointing at the REST route until 9.0.0 removes them): + +- `deleteCustomSound` → `POST /v1/custom-sounds.delete` +- `blockUser` / `unblockUser` → `POST /v1/im.blockUser` (single toggle with `{ roomId, block: boolean }`) +- `saveSettings` → `POST /v1/settings` +- `e2e.requestSubscriptionKeys` → `POST /v1/e2e.requestSubscriptionKeys` diff --git a/.changeset/dull-trams-follow.md b/.changeset/dull-trams-follow.md new file mode 100644 index 0000000000000..eacb88108a0f7 --- /dev/null +++ b/.changeset/dull-trams-follow.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) diff --git a/.changeset/early-deer-fry.md b/.changeset/early-deer-fry.md new file mode 100644 index 0000000000000..c2999ef41ebcf --- /dev/null +++ b/.changeset/early-deer-fry.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue that allowed users to create a DM and send messages to a deactivated account diff --git a/.changeset/easy-laws-talk.md b/.changeset/easy-laws-talk.md new file mode 100644 index 0000000000000..51ff0a2702f2b --- /dev/null +++ b/.changeset/easy-laws-talk.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes non-deterministic comparator in team's channel desertion table diff --git a/.changeset/eight-areas-see.md b/.changeset/eight-areas-see.md new file mode 100644 index 0000000000000..d349b9765616f --- /dev/null +++ b/.changeset/eight-areas-see.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/core-services': patch +'@rocket.chat/meteor': patch +--- + +Fixes an issue where updating an app in quick succession could crash the server. diff --git a/.changeset/eight-dingos-press.md b/.changeset/eight-dingos-press.md new file mode 100644 index 0000000000000..457a7c98a29a2 --- /dev/null +++ b/.changeset/eight-dingos-press.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes incoming integrations inability of reaching internal hosts by adding "ignoreSsrfValidation: true" to it diff --git a/.changeset/eighty-onions-deny.md b/.changeset/eighty-onions-deny.md new file mode 100644 index 0000000000000..dd0dd5ceaa2c7 --- /dev/null +++ b/.changeset/eighty-onions-deny.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes error handling when using invalid regular expressions on message search diff --git a/.changeset/famous-birds-join.md b/.changeset/famous-birds-join.md new file mode 100644 index 0000000000000..09cc78ba0d86e --- /dev/null +++ b/.changeset/famous-birds-join.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes the message list shifting when typing in the fully expanded message composer diff --git a/.changeset/fast-apes-know.md b/.changeset/fast-apes-know.md new file mode 100644 index 0000000000000..47242a936a4c2 --- /dev/null +++ b/.changeset/fast-apes-know.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Escapes HTML tags in exported data diff --git a/.changeset/few-lies-drum.md b/.changeset/few-lies-drum.md new file mode 100644 index 0000000000000..9231c92914c72 --- /dev/null +++ b/.changeset/few-lies-drum.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue with embedded layout where users were able to receive VideoConf calls, causing the application to crash diff --git a/.changeset/fix-join-room-subscription-refresh.md b/.changeset/fix-join-room-subscription-refresh.md new file mode 100644 index 0000000000000..464efd6ad7825 --- /dev/null +++ b/.changeset/fix-join-room-subscription-refresh.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed the "not subscribed" room screen not updating after joining a room. The join mutation invalidated a stale React Query key that no longer matched the open-room query, so the UI kept showing the join prompt until a manual page refresh. It now invalidates the correct `rooms` reference key, so the room opens immediately after joining. diff --git a/.changeset/fluffy-poets-check.md b/.changeset/fluffy-poets-check.md new file mode 100644 index 0000000000000..4ecdb9e7788ac --- /dev/null +++ b/.changeset/fluffy-poets-check.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/media-signaling': minor +--- + +Fixes a soft lock issue where the call negotiation doesn't proceed if the call is accepted while still being initialized diff --git a/.changeset/full-results-share.md b/.changeset/full-results-share.md new file mode 100644 index 0000000000000..2b681e3cecebe --- /dev/null +++ b/.changeset/full-results-share.md @@ -0,0 +1,11 @@ +--- +'@rocket.chat/presence-service': minor +'@rocket.chat/core-services': patch +'@rocket.chat/model-typings': patch +'@rocket.chat/core-typings': patch +'@rocket.chat/presence': minor +'@rocket.chat/models': patch +'@rocket.chat/meteor': minor +--- + +Adds the backend foundation for a unified presence engine with a priority-based claim system (internal > manual > external), status expiration, and previous state restore. diff --git a/.changeset/heavy-balloons-cut.md b/.changeset/heavy-balloons-cut.md new file mode 100644 index 0000000000000..2c8a4fb47f337 --- /dev/null +++ b/.changeset/heavy-balloons-cut.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue that caused the UI to attempt to mark an unserved livechat room as read diff --git a/.changeset/itchy-years-learn.md b/.changeset/itchy-years-learn.md new file mode 100644 index 0000000000000..8f12de526f174 --- /dev/null +++ b/.changeset/itchy-years-learn.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/apps': patch +'@rocket.chat/meteor': patch +--- + +Fixes an issue where an app's bot user presence was incorrectly shown as Offline after updating it. diff --git a/.changeset/khaki-gifts-follow.md b/.changeset/khaki-gifts-follow.md new file mode 100644 index 0000000000000..6852340e54240 --- /dev/null +++ b/.changeset/khaki-gifts-follow.md @@ -0,0 +1,13 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/abac": minor +--- + +Allows using Virtru as the attribute store for ABAC decisions. + +### Important + +- When using virtru as the store, the internal attribute store is disabled. +- On switch, existing ABAC attributes from rooms will be removed. Rooms will continue to be private & no users will be removed until you add attributes again. +- Users are only allowed to see & edit rooms they have access to. Access decision is evaluated on Virtru +- A user/app with the `bypass-abac-store-validation` permission can assign any attributes to rooms, even if the user doesn't have them assigned on Virtru. diff --git a/.changeset/libretranslate-autotranslate-provider.md b/.changeset/libretranslate-autotranslate-provider.md new file mode 100644 index 0000000000000..63eeeb9d2877f --- /dev/null +++ b/.changeset/libretranslate-autotranslate-provider.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/meteor': minor +'@rocket.chat/i18n': patch +--- + +Added LibreTranslate as a message auto-translation provider, alongside Google, DeepL and Microsoft. LibreTranslate can be self-hosted, enabling fully on-premise / offline message auto-translation. Configure the instance URL (and optional API key) under **Admin → Settings → Message → Auto-Translate → LibreTranslate** and select it as the Service Provider. diff --git a/.changeset/loose-knives-retire.md b/.changeset/loose-knives-retire.md new file mode 100644 index 0000000000000..8bb9dcbb35d14 --- /dev/null +++ b/.changeset/loose-knives-retire.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes the behavior when the login token expires to redirect the user to the login page diff --git a/.changeset/metal-regions-yell.md b/.changeset/metal-regions-yell.md new file mode 100644 index 0000000000000..626b327792526 --- /dev/null +++ b/.changeset/metal-regions-yell.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where temporary AD/LDAP lockouts would deactivate users on rocket.chat. diff --git a/.changeset/neat-planets-hope.md b/.changeset/neat-planets-hope.md new file mode 100644 index 0000000000000..250758fd51940 --- /dev/null +++ b/.changeset/neat-planets-hope.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +Adds custom-sounds.delete API endpoint. diff --git a/.changeset/nice-baboons-flash.md b/.changeset/nice-baboons-flash.md new file mode 100644 index 0000000000000..f6abb11c3663d --- /dev/null +++ b/.changeset/nice-baboons-flash.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/federation-matrix': patch +'@rocket.chat/meteor': patch +--- + +Fixes an issue where editing or deleting a message in a federated room caused subsequent messages to stop syncing between servers + +Note: this prevents the issue from happening, but does not restore rooms that are already affected. Recovering those requires a separate, one-time repair. diff --git a/.changeset/old-cities-enjoy.md b/.changeset/old-cities-enjoy.md new file mode 100644 index 0000000000000..581cafa174ceb --- /dev/null +++ b/.changeset/old-cities-enjoy.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fixes issue that caused threads to sometimes not scroll when sending messages diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000000..b75977a681ae5 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,136 @@ +{ + "mode": "pre", + "tag": "rc", + "initialVersions": { + "@rocket.chat/meteor": "8.6.0-develop", + "rocketchat-services": "2.0.48", + "@rocket.chat/uikit-playground": "0.7.13", + "@rocket.chat/account-service": "0.4.57", + "@rocket.chat/authorization-service": "0.6.1", + "@rocket.chat/ddp-streamer": "0.3.57", + "@rocket.chat/omnichannel-transcript": "0.4.57", + "@rocket.chat/presence-service": "0.4.57", + "@rocket.chat/queue-worker": "0.4.57", + "@rocket.chat/abac": "0.2.1", + "@rocket.chat/federation-matrix": "0.1.4", + "@rocket.chat/license": "1.1.16", + "@rocket.chat/media-calls": "0.5.0", + "@rocket.chat/network-broker": "0.2.36", + "@rocket.chat/omni-core-ee": "0.0.22", + "@rocket.chat/omnichannel-services": "0.3.54", + "@rocket.chat/pdf-worker": "0.3.35", + "@rocket.chat/presence": "0.2.57", + "@rocket.chat/account-utils": "0.0.3", + "@rocket.chat/agenda": "0.1.1", + "@rocket.chat/api-client": "0.2.56", + "@rocket.chat/apps": "0.7.0", + "@rocket.chat/apps-engine": "1.63.0", + "@rocket.chat/base64": "1.0.14", + "@rocket.chat/cas-validate": "0.0.4", + "@rocket.chat/core-services": "0.14.1", + "@rocket.chat/core-typings": "8.6.0-develop", + "@rocket.chat/cron": "0.1.57", + "@rocket.chat/ddp-client": "1.1.0", + "@rocket.chat/desktop-api": "1.2.0", + "@rocket.chat/eslint-config": "0.8.0", + "@rocket.chat/favicon": "0.0.5", + "@rocket.chat/fuselage-ui-kit": "31.0.0", + "@rocket.chat/gazzodown": "31.0.0", + "@rocket.chat/http-router": "7.9.23", + "@rocket.chat/i18n": "3.1.0", + "@rocket.chat/instance-status": "0.1.57", + "@rocket.chat/jest-presets": "0.0.2", + "@rocket.chat/jwt": "0.2.1", + "@rocket.chat/livechat": "2.1.4", + "@rocket.chat/log-format": "0.0.3", + "@rocket.chat/logger": "1.0.1", + "@rocket.chat/media-signaling": "1.0.0", + "@rocket.chat/message-parser": "0.31.36", + "@rocket.chat/message-types": "0.1.1", + "@rocket.chat/mock-providers": "0.4.17", + "@rocket.chat/model-typings": "2.3.0", + "@rocket.chat/models": "2.3.0", + "@rocket.chat/mongo-adapter": "0.0.3", + "@rocket.chat/poplib": "0.0.3", + "@rocket.chat/omni-core": "0.1.1", + "@rocket.chat/password-policies": "0.1.1", + "@rocket.chat/patch-injection": "0.0.2", + "@rocket.chat/peggy-loader": "0.31.28", + "@rocket.chat/random": "1.2.3", + "@rocket.chat/release-action": "2.2.4", + "@rocket.chat/release-changelog": "0.1.1", + "@rocket.chat/rest-typings": "8.6.0-develop", + "@rocket.chat/server-cloud-communication": "0.0.3", + "@rocket.chat/server-fetch": "0.2.1", + "@rocket.chat/sha256": "1.0.13", + "@rocket.chat/storybook-config": "0.0.3", + "@rocket.chat/tools": "0.3.0", + "@rocket.chat/tracing": "0.0.2", + "@rocket.chat/tsconfig": "0.0.0", + "@rocket.chat/ui-avatar": "27.0.0", + "@rocket.chat/ui-client": "31.0.0", + "@rocket.chat/ui-composer": "2.0.0", + "@rocket.chat/ui-contexts": "31.0.0", + "@rocket.chat/ui-kit": "1.1.0", + "@rocket.chat/ui-video-conf": "31.0.0", + "@rocket.chat/ui-voip": "21.0.0", + "@rocket.chat/web-ui-registration": "31.0.0" + }, + "changesets": [ + "add-tagalog-language", + "all-readers-reply", + "bump-patch-1782440081682", + "bump-patch-1782512866566", + "bump-patch-1783039947861", + "bumpy-coats-reply", + "chubby-garlics-decide", + "crisp-corners-itch", + "crisp-taxis-pay", + "cute-humans-follow", + "ddp-migrate-batch2-callers", + "ddp-migrate-batch3-callers", + "dull-trams-follow", + "early-deer-fry", + "easy-laws-talk", + "eight-areas-see", + "eight-dingos-press", + "eighty-onions-deny", + "famous-birds-join", + "fast-apes-know", + "few-lies-drum", + "fix-join-room-subscription-refresh", + "fluffy-poets-check", + "full-results-share", + "heavy-balloons-cut", + "itchy-years-learn", + "khaki-gifts-follow", + "libretranslate-autotranslate-provider", + "loose-knives-retire", + "metal-regions-yell", + "neat-planets-hope", + "nice-baboons-flash", + "old-cities-enjoy", + "pretty-rocks-retire", + "rest-chat-delete-by-fileid", + "rest-e2e-request-subscription-keys", + "rest-im-block-user", + "rest-settings-post", + "rest-spotlight-params-and-anonymous", + "rest-users-setpreferences-utcoffset", + "rich-bananas-shine", + "rooms-join-endpoint", + "salty-suits-strive", + "short-mice-cover", + "silent-coats-open", + "slick-hats-arrive", + "sup-1064-pat-bypass-two-factor", + "swift-rooms-load", + "tasty-deserts-doubt", + "thick-fans-leave", + "tricky-comics-wink", + "true-cooks-lick", + "upset-rats-work", + "wise-regions-sip", + "wise-years-shout" + ] +} diff --git a/.changeset/pretty-rocks-retire.md b/.changeset/pretty-rocks-retire.md new file mode 100644 index 0000000000000..b290e9c4cf10f --- /dev/null +++ b/.changeset/pretty-rocks-retire.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue on ABAC audit page that sent local timestamps instead of UTC ones as filters to fetch audit logs diff --git a/.changeset/rest-chat-delete-by-fileid.md b/.changeset/rest-chat-delete-by-fileid.md new file mode 100644 index 0000000000000..56a660a415400 --- /dev/null +++ b/.changeset/rest-chat-delete-by-fileid.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +`POST /v1/chat.delete` now accepts `{ fileId, asUser? }` as an alternative to `{ msgId, roomId, asUser? }`. When `fileId` is provided the server resolves the owning message via `Messages.getMessageByFileId` before running the existing permission and deletion flow. diff --git a/.changeset/rest-e2e-request-subscription-keys.md b/.changeset/rest-e2e-request-subscription-keys.md new file mode 100644 index 0000000000000..3b6ee9bd087ef --- /dev/null +++ b/.changeset/rest-e2e-request-subscription-keys.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +Added `POST /v1/e2e.requestSubscriptionKeys` (replaces the deprecated `e2e.requestSubscriptionKeys` DDP method). Auth-gated, no body. Broadcasts `notify.e2e.keyRequest` for every encrypted room the caller is subscribed to without an E2E key, matching the DDP method's behavior. The legacy DDP method remains registered until 9.0.0 with a deprecation log pointing at the new route. diff --git a/.changeset/rest-im-block-user.md b/.changeset/rest-im-block-user.md new file mode 100644 index 0000000000000..d4f840ef42da8 --- /dev/null +++ b/.changeset/rest-im-block-user.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +Added `POST /v1/im.blockUser` (replaces the deprecated `blockUser` / `unblockUser` DDP methods). Body is `{ roomId, block: boolean }` — `block: true` blocks the other DM participant, `block: false` unblocks. Auth-gated and per-room via the `RoomMemberActions.BLOCK` directive (DM-only). Both legacy DDP methods remain registered until 9.0.0 with deprecation logs pointing at the new route. diff --git a/.changeset/rest-settings-post.md b/.changeset/rest-settings-post.md new file mode 100644 index 0000000000000..b1b7f3c4f8686 --- /dev/null +++ b/.changeset/rest-settings-post.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +Added `POST /v1/settings` for batched admin setting updates (replaces the deprecated `saveSettings` DDP method). Body is `{ settings: { _id, value }[] }`. The endpoint requires authentication, enforces 2FA (`twoFactorRequired: true`), and runs the same per-setting permission chain (`edit-privileged-setting` OR `manage-selected-settings` + per-id permission) and audit/notify side effects the DDP method already performed. The legacy DDP method remains registered until 9.0.0 with a deprecation log pointing at the new route. diff --git a/.changeset/rest-spotlight-params-and-anonymous.md b/.changeset/rest-spotlight-params-and-anonymous.md new file mode 100644 index 0000000000000..032538dd9b963 --- /dev/null +++ b/.changeset/rest-spotlight-params-and-anonymous.md @@ -0,0 +1,11 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +`GET /v1/spotlight` now mirrors the DDP `spotlight` method: + +- accepts optional `usernames` (comma-separated string), `type` (JSON-encoded `{ users?, mentions?, rooms?, includeFederatedRooms? }`) and `rid` query params; +- response items expose `nickname` / `outside` (users) and `uids` / `usernames` / `fname` (rooms); +- `status` on each user is now optional — outside/federated users were already being returned without one and the previous required-field schema rejected them as `Response validation failed`; +- the endpoint is no longer auth-gated, allowing anonymous-read flows (e.g. `Accounts_AllowAnonymousRead`) to keep finding public channels through the navbar search. diff --git a/.changeset/rest-users-setpreferences-utcoffset.md b/.changeset/rest-users-setpreferences-utcoffset.md new file mode 100644 index 0000000000000..9681c8b57f0cc --- /dev/null +++ b/.changeset/rest-users-setpreferences-utcoffset.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +`POST /v1/users.setPreferences` now accepts an optional `data.utcOffset` (number) field. The value is stored at the user-document root via `Users.setUtcOffset` (not under `settings.preferences`), matching what the legacy `userSetUtcOffset` DDP method did. diff --git a/.changeset/rich-bananas-shine.md b/.changeset/rich-bananas-shine.md new file mode 100644 index 0000000000000..eacb88108a0f7 --- /dev/null +++ b/.changeset/rich-bananas-shine.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) diff --git a/.changeset/rooms-join-endpoint.md b/.changeset/rooms-join-endpoint.md new file mode 100644 index 0000000000000..3c30a35ea7ebc --- /dev/null +++ b/.changeset/rooms-join-endpoint.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/rest-typings": minor +--- + +Added a new `rooms.join` REST endpoint that lets a user join any room type, replicating the behavior of the deprecated `joinRoom` DDP method. Unlike `channels.join`, it resolves all room types through the shared `Room.join` service (access checks, join codes, federation and omnichannel rules). The client now uses `rooms.join` instead of `channels.join`. diff --git a/.changeset/salty-suits-strive.md b/.changeset/salty-suits-strive.md new file mode 100644 index 0000000000000..e9506dc744605 --- /dev/null +++ b/.changeset/salty-suits-strive.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes missing permission check on the `POST /api/v1/fingerprint` endpoint diff --git a/.changeset/short-mice-cover.md b/.changeset/short-mice-cover.md new file mode 100644 index 0000000000000..a1d0b0ad967b2 --- /dev/null +++ b/.changeset/short-mice-cover.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes rooms failing to open on transient network errors, and prevents a "Room Not Found" flash when opening a room with a local subscription diff --git a/.changeset/silent-coats-open.md b/.changeset/silent-coats-open.md new file mode 100644 index 0000000000000..e8e88a7966104 --- /dev/null +++ b/.changeset/silent-coats-open.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes the Chat Limits locking mechanism to allow bot agents to skip the lock as they aren't limited diff --git a/.changeset/slick-hats-arrive.md b/.changeset/slick-hats-arrive.md new file mode 100644 index 0000000000000..4ce84a4a831ee --- /dev/null +++ b/.changeset/slick-hats-arrive.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/federation-matrix': patch +'@rocket.chat/core-typings': patch +'@rocket.chat/meteor': patch +--- + +Fixes an issue where `description` was incorrectly being used as alternative text for image attachments diff --git a/.changeset/sup-1064-pat-bypass-two-factor.md b/.changeset/sup-1064-pat-bypass-two-factor.md new file mode 100644 index 0000000000000..c3b3576124aeb --- /dev/null +++ b/.changeset/sup-1064-pat-bypass-two-factor.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/meteor': patch +'@rocket.chat/core-typings': patch +--- + +Fixes REST API endpoints that require two-factor authentication (such as `users.update`) rejecting requests authenticated with a Personal Access Token created with "Ignore Two Factor Authentication", returning `totp-required` even though the token was meant to bypass the check. The two-factor authorization check now resolves the login token from the REST connection, so `bypassTwoFactor` tokens are honored again. diff --git a/.changeset/swift-rooms-load.md b/.changeset/swift-rooms-load.md new file mode 100644 index 0000000000000..534b1a1687d48 --- /dev/null +++ b/.changeset/swift-rooms-load.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Speeds up room opening by removing redundant work in the message history load. On the client, the prefetched first history batch no longer blocks on the message-list DOM before rendering, and the history pager no longer fires an extra `loadHistory` round trip just to reach a full page of visible messages when the latest page contains thread replies. On the server, `loadHistory` reuses the already-fetched room document instead of querying it twice, and runs message normalization and the unread (first-unread + count) queries concurrently instead of sequentially. diff --git a/.changeset/tasty-deserts-doubt.md b/.changeset/tasty-deserts-doubt.md new file mode 100644 index 0000000000000..c754eddcd89e0 --- /dev/null +++ b/.changeset/tasty-deserts-doubt.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/core-typings': patch +'@rocket.chat/meteor': patch +--- + +Fixes an issue where ui crashes when message attachment fields contains non-string `value` field diff --git a/.changeset/thick-fans-leave.md b/.changeset/thick-fans-leave.md new file mode 100644 index 0000000000000..e53d5041886ff --- /dev/null +++ b/.changeset/thick-fans-leave.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where the channel selection modal would not load when removing a member from a team. diff --git a/.changeset/tricky-comics-wink.md b/.changeset/tricky-comics-wink.md new file mode 100644 index 0000000000000..2e78e5adb6856 --- /dev/null +++ b/.changeset/tricky-comics-wink.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/media-signaling': patch +--- + +Fixes an issue where voice calls could fail if the user navigated between rooms during the initial connection diff --git a/.changeset/true-cooks-lick.md b/.changeset/true-cooks-lick.md new file mode 100644 index 0000000000000..265b130bedfe2 --- /dev/null +++ b/.changeset/true-cooks-lick.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/apps-engine': minor +'@rocket.chat/meteor': minor +--- + +Exposes the `isFederated` and `federation` fields for room and user objects in apps diff --git a/.changeset/upset-rats-work.md b/.changeset/upset-rats-work.md new file mode 100644 index 0000000000000..d84f7439c76be --- /dev/null +++ b/.changeset/upset-rats-work.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +"@rocket.chat/ui-voip": minor +--- + +Introduces popout functionality for voice calls diff --git a/.changeset/wise-regions-sip.md b/.changeset/wise-regions-sip.md new file mode 100644 index 0000000000000..00dff608d60be --- /dev/null +++ b/.changeset/wise-regions-sip.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue on `canAccessRoom` where `abacAttributes` were not fetched in some endpoint calls diff --git a/.changeset/wise-years-shout.md b/.changeset/wise-years-shout.md new file mode 100644 index 0000000000000..5baefb314be9f --- /dev/null +++ b/.changeset/wise-years-shout.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/ui-voip": patch +--- + +Fixes voice call UI not reflecting active call state when a new view mounted while a call was already active (e.g. opening the popout window) diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index 875fb14bceb39..580340d6e0933 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -89,7 +89,6 @@ runs: shell: bash run: | CACHE_DIR="${{ runner.temp }}/mongodb-memory-server" - mkdir -p "$CACHE_DIR" echo "MONGOMS_DOWNLOAD_DIR=$CACHE_DIR" >> $GITHUB_ENV echo "MONGOMS_PREFER_GLOBAL_PATH=false" >> $GITHUB_ENV diff --git a/.github/actions/update-version-durability/package-lock.json b/.github/actions/update-version-durability/package-lock.json index ac82e53d36018..c48e783a33d09 100644 --- a/.github/actions/update-version-durability/package-lock.json +++ b/.github/actions/update-version-durability/package-lock.json @@ -20,12 +20,22 @@ } }, "node_modules/@actions/core": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz", - "integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "license": "MIT", + "dependencies": { + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "license": "MIT", "dependencies": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" + "@actions/io": "^1.0.1" } }, "node_modules/@actions/http-client": { @@ -37,6 +47,12 @@ "undici": "^5.25.4" } }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "license": "MIT" + }, "node_modules/@fastify/busboy": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", @@ -406,16 +422,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -507,9 +523,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -611,14 +627,6 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==" - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } } } } diff --git a/.github/workflows/ci-code-check.yml b/.github/workflows/ci-code-check.yml index 4416a9f8ada5a..21ff95c641948 100644 --- a/.github/workflows/ci-code-check.yml +++ b/.github/workflows/ci-code-check.yml @@ -41,7 +41,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 + - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 - uses: ./.github/actions/restore-packages diff --git a/.github/workflows/ci-deploy-gh-pages.yml b/.github/workflows/ci-deploy-gh-pages.yml index dc6b044dbd19b..e1b8b81517da6 100644 --- a/.github/workflows/ci-deploy-gh-pages.yml +++ b/.github/workflows/ci-deploy-gh-pages.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-24.04-arm steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 + - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 - name: Setup NodeJS uses: ./.github/actions/setup-node diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index f90122800df9c..c8421db957b60 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -125,7 +125,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 + - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 - uses: ./.github/actions/restore-packages @@ -175,6 +175,13 @@ jobs: - name: Start containers for CE if: inputs.release == 'ce' + env: + # API tests intentionally invoke deprecated DDP methods through + # /v1/method.call/:method. TEST_MODE='api' keeps every other test + # behavior on (rate-limiter bypass, short cache TTLs) while letting + # the deprecation logger log without throwing. Other suites use the + # docker-compose default of TEST_MODE='true'. + TEST_MODE: ${{ (inputs.type == 'api' || inputs.type == 'api-livechat') && 'api' || 'true' }} run: | # when we are testing CE, we only need to start the rocketchat container DEBUG_LOG_LEVEL=${DEBUG_LOG_LEVEL:-0} docker compose -f docker-compose-ci.yml up -d rocketchat --wait @@ -185,6 +192,7 @@ jobs: ENTERPRISE_LICENSE: ${{ inputs.enterprise-license }} TRANSPORTER: ${{ inputs.transporter }} COMPOSE_PROFILES: ${{ inputs.type == 'api' && 'api' || '' }} + TEST_MODE: ${{ (inputs.type == 'api' || inputs.type == 'api-livechat') && 'api' || 'true' }} run: | DEBUG_LOG_LEVEL=${DEBUG_LOG_LEVEL:-0} docker compose -f docker-compose-ci.yml up -d --wait diff --git a/.github/workflows/ci-test-storybook.yml b/.github/workflows/ci-test-storybook.yml index 144b5895ffc18..9c7732f88a48d 100644 --- a/.github/workflows/ci-test-storybook.yml +++ b/.github/workflows/ci-test-storybook.yml @@ -37,7 +37,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 + - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 - uses: ./.github/actions/restore-packages diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml index 13ea1f31d6490..a13e1fe4834cd 100644 --- a/.github/workflows/ci-test-unit.yml +++ b/.github/workflows/ci-test-unit.yml @@ -41,7 +41,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 + - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 - uses: ./.github/actions/restore-packages diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc8e1c7f9018c..77939861d1a42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,8 +38,8 @@ jobs: meteor-rc-cache-key: ${{ steps.ci-cache-keys.outputs.meteor-rc-cache-key }} github-actions-changed: ${{ steps.diff.outputs.actions-changed }} # this is 100% intentional, secrets are not available for forks, so ee-tests will always fail - # to avoid this, we are using a dummy license, expiring at 2026-07-01 - enterprise-license: Uo7Jcr6WW0XYA8ydHd+Sk6pZ9/0V6dIASnyTwvUrNym/zJg2Ma3eYNKkC8osXLCc72y1ahohnWY7/+7IYkvono3GYXQR+IGvYbbrVgNR6OjMahd9P/odHZL1GFTm2qHrEL5Hh/XEOG+YluFeRdWPzCizQlp4zGGOi0+PkQo096TR9NVCLrsErVl2MW1WM6ZM1W5EUJG9pKly4BQnaOTUAlor1im6i8qPTDCKrISZfLiZEWuQKaPW/GE3mRKjQNjDh0CabX1N2S880pRRGoozBYAnp2NmFfrQW0+5ihKisBTIeMbMZ7K5NE5PkYU1nhQDcc+rpDHtwG9Ceg5X0J+oea3UfrPTmDON2aSI0iO22kvL6G7QI3fyrEIvJrMbxcNKxAFeQYgnjisw/b06+chWSG4jG686Fx58XrVS87dFhWL9WoGltsk1dJCntUQvI1sX6zOfpvyg1iWRnHfYDOrwoWlX57XMm29fWineEoqnOOTOVnA/uP+DKEhercQ9Xuo7Cr6zJxpQpwd03e7ODVjiEbTDqlkZE687rmxRCD4Wmu8L86WIl2xSEIajKLX301Ww5mz/FdLqk+Mg32lkW66W3azQKvJ1440NBrYxhpJ+dl9vSFMb3s1+xnz1cYUbjUcq9mARvORcgy5mLwKulmqT6Sq0Uvbv10YCO0TW0beXYW8= + # to avoid this, we are using a dummy license, expiring at 2027-07-01 + enterprise-license: MK+bpK5NveUuNlWGaQXGoy+8b74Luet82M3ZGcBB8b5P9Y+m67NEtpW64dc1d5lEWi6d0nFjCjtCMneVD7bKxodz/Cml8URKEo5P7cQb/9wmeT0MzAhYNaRFZlIGkZ3ITF59pDV2u4HZuosEDJikVRwnaJ5ZoU/pOsHSPUPhTyGNIqLeKynODtUpfwDdIKEmHxpf2yVkKjgRiIJmbWjM6A4k+MNNYXWVXHzye7GggqWVg/ZcT7nKU1CCadpLhTJiIrgrrPzil1G5DQ4xnLs3Q2tu2dILSDiW5OYw/ywu2yCMicTjMq4MLL5SXDQJj6WoJzZ54HosbvsDzOXvsdC9gI1CjhPL2uRuvC8XLrzn3vL2UgXnifzD1VrLTtdZ+aSADveqtlzYlRWtqoUFBbNw8o+YVHdhbZGR0beMoAyRbHi5EMpxpad3L+NyztUIT/Uh/IjQ/C2SQZ6jB0GKPBOPxFLN56FNhTGrffLFR++TVoBu0Iquc7kajWkNit3bVbZvbx+oFcVW2PcjQ/+i2jpJjbgtUFUKrTKxGMAXTWoDzIQQ35zNzGAy268IM4Ymp5JmsVEnBOEUkbF9yx6fzkO6xZhpsHf0muklnW0kA+Tlore/TUrBWh1/RwWlQeZlxM5NyWoRM5onQmr/k/4BmObtL1Hpmbk8oMG29z89xtE9y/4= steps: - name: Github Info run: | @@ -246,7 +246,7 @@ jobs: restore-keys: | vite-local-cache-${{ runner.arch }}-${{ runner.os }}- - - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 + - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 if: steps.packages-cache-build.outputs.cache-hit != 'true' - name: Build Rocket.Chat Packages @@ -695,7 +695,7 @@ jobs: cache-modules: true install: true - - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 + - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 - name: Restore turbo build uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b6ccbed4870e1..d2034fd0f6436 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -28,7 +28,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 # Override language selection by uncommenting this and choosing your languages with: languages: javascript @@ -36,7 +36,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -50,4 +50,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 diff --git a/.github/workflows/new-release.yml b/.github/workflows/new-release.yml index 577a1453eb1c3..415e37e3c8f57 100644 --- a/.github/workflows/new-release.yml +++ b/.github/workflows/new-release.yml @@ -38,7 +38,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 + - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 - name: Build packages run: yarn build diff --git a/.github/workflows/pr-update-description.yml b/.github/workflows/pr-update-description.yml index 89e29fc3505a5..58fa2d371e213 100644 --- a/.github/workflows/pr-update-description.yml +++ b/.github/workflows/pr-update-description.yml @@ -25,7 +25,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 + - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 - name: Build packages run: yarn build diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 699e234554101..7be1cf06211f2 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -28,7 +28,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 + - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 - name: Build packages run: yarn build diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 376120c91d383..a956df9f1f018 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -19,7 +19,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@2f61e9bebb2f51f2ed041ce70377b0f81b3b1e0d # v2.4.1 + - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 - name: Build packages run: yarn build diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index db848f3c5ac26..a4e06de4a534c 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -9,7 +9,7 @@ jobs: permissions: issues: write steps: - - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 + - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 with: days-before-issue-stale: 14 days-before-issue-close: 14 diff --git a/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch b/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch new file mode 100644 index 0000000000000..50cec88736f0c --- /dev/null +++ b/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch @@ -0,0 +1,870 @@ +diff --git a/dist/calculatePosition.main.js b/dist/calculatePosition.main.js +index a7b270bb7e0eb62a0153965bf15d847ad20aa5aa..242e220482ede7cc771de25f41c1e27703d74376 100644 +--- a/dist/calculatePosition.main.js ++++ b/dist/calculatePosition.main.js +@@ -42,28 +42,37 @@ const $5935ba4d7da2c103$var$TOTAL_SIZE = { + height: 'totalHeight' + }; + const $5935ba4d7da2c103$var$PARSED_PLACEMENT_CACHE = {}; +-let $5935ba4d7da2c103$var$visualViewport = typeof document !== 'undefined' ? window.visualViewport : null; ++let $5935ba4d7da2c103$var$getWindow = (targetNode)=>{ ++ return (targetNode === null || targetNode === void 0 ? void 0 : targetNode.ownerDocument.defaultView) || window; ++}; ++let $5935ba4d7da2c103$var$getVisualViewport = (actualWindow)=>{ ++ return actualWindow.visualViewport || null; ++}; ++function $5935ba4d7da2c103$var$getDocumentElement(node) { ++ return node.ownerDocument.documentElement || document.documentElement; ++} + function $5935ba4d7da2c103$var$getContainerDimensions(containerNode) { ++ const visualViewport = $5935ba4d7da2c103$var$getVisualViewport($5935ba4d7da2c103$var$getWindow(containerNode)); + let width = 0, height = 0, totalWidth = 0, totalHeight = 0, top = 0, left = 0; + let scroll = {}; + var _visualViewport_scale; +- let isPinchZoomedIn = ((_visualViewport_scale = $5935ba4d7da2c103$var$visualViewport === null || $5935ba4d7da2c103$var$visualViewport === void 0 ? void 0 : $5935ba4d7da2c103$var$visualViewport.scale) !== null && _visualViewport_scale !== void 0 ? _visualViewport_scale : 1) > 1; ++ let isPinchZoomedIn = ((_visualViewport_scale = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale) !== null && _visualViewport_scale !== void 0 ? _visualViewport_scale : 1) > 1; + if (containerNode.tagName === 'BODY') { +- let documentElement = document.documentElement; ++ let documentElement = $5935ba4d7da2c103$var$getDocumentElement(containerNode); + totalWidth = documentElement.clientWidth; + totalHeight = documentElement.clientHeight; + var _visualViewport_width; +- width = (_visualViewport_width = $5935ba4d7da2c103$var$visualViewport === null || $5935ba4d7da2c103$var$visualViewport === void 0 ? void 0 : $5935ba4d7da2c103$var$visualViewport.width) !== null && _visualViewport_width !== void 0 ? _visualViewport_width : totalWidth; ++ width = (_visualViewport_width = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.width) !== null && _visualViewport_width !== void 0 ? _visualViewport_width : totalWidth; + var _visualViewport_height; +- height = (_visualViewport_height = $5935ba4d7da2c103$var$visualViewport === null || $5935ba4d7da2c103$var$visualViewport === void 0 ? void 0 : $5935ba4d7da2c103$var$visualViewport.height) !== null && _visualViewport_height !== void 0 ? _visualViewport_height : totalHeight; ++ height = (_visualViewport_height = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.height) !== null && _visualViewport_height !== void 0 ? _visualViewport_height : totalHeight; + scroll.top = documentElement.scrollTop || containerNode.scrollTop; + scroll.left = documentElement.scrollLeft || containerNode.scrollLeft; + // The goal of the below is to get a top/left value that represents the top/left of the visual viewport with + // respect to the layout viewport origin. This combined with the scrollTop/scrollLeft will allow us to calculate + // coordinates/values with respect to the visual viewport or with respect to the layout viewport. +- if ($5935ba4d7da2c103$var$visualViewport) { +- top = $5935ba4d7da2c103$var$visualViewport.offsetTop; +- left = $5935ba4d7da2c103$var$visualViewport.offsetLeft; ++ if (visualViewport) { ++ top = visualViewport.offsetTop; ++ left = visualViewport.offsetLeft; + } + } else { + ({ width: width, height: height, top: top, left: left } = $5935ba4d7da2c103$var$getOffset(containerNode)); +@@ -80,9 +89,9 @@ function $5935ba4d7da2c103$var$getContainerDimensions(containerNode) { + scroll.top = 0; + scroll.left = 0; + var _visualViewport_pageTop; +- top = (_visualViewport_pageTop = $5935ba4d7da2c103$var$visualViewport === null || $5935ba4d7da2c103$var$visualViewport === void 0 ? void 0 : $5935ba4d7da2c103$var$visualViewport.pageTop) !== null && _visualViewport_pageTop !== void 0 ? _visualViewport_pageTop : 0; ++ top = (_visualViewport_pageTop = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.pageTop) !== null && _visualViewport_pageTop !== void 0 ? _visualViewport_pageTop : 0; + var _visualViewport_pageLeft; +- left = (_visualViewport_pageLeft = $5935ba4d7da2c103$var$visualViewport === null || $5935ba4d7da2c103$var$visualViewport === void 0 ? void 0 : $5935ba4d7da2c103$var$visualViewport.pageLeft) !== null && _visualViewport_pageLeft !== void 0 ? _visualViewport_pageLeft : 0; ++ left = (_visualViewport_pageLeft = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.pageLeft) !== null && _visualViewport_pageLeft !== void 0 ? _visualViewport_pageLeft : 0; + } + return { + width: width, +@@ -128,7 +137,8 @@ containerDimensions, padding, containerOffsetWithBoundary) { + else return 0; + } + function $5935ba4d7da2c103$var$getMargins(node) { +- let style = window.getComputedStyle(node); ++ const actualWindow = $5935ba4d7da2c103$var$getWindow(node); ++ let style = actualWindow.getComputedStyle(node); + return { + top: parseInt(style.marginTop, 10) || 0, + bottom: parseInt(style.marginBottom, 10) || 0, +@@ -275,13 +285,14 @@ function $5935ba4d7da2c103$export$6839422d1f33cee9(placementInput, childOffset, + } + function $5935ba4d7da2c103$export$b3ceb0cbf1056d98(opts) { + let { placement: placement, targetNode: targetNode, overlayNode: overlayNode, scrollNode: scrollNode, padding: padding, shouldFlip: shouldFlip, boundaryElement: boundaryElement, offset: offset, crossOffset: crossOffset, maxHeight: maxHeight, arrowSize: arrowSize = 0, arrowBoundaryOffset: arrowBoundaryOffset = 0 } = opts; ++ const actualWindow = $5935ba4d7da2c103$var$getWindow(targetNode); + let container = overlayNode instanceof HTMLElement ? $5935ba4d7da2c103$var$getContainingBlock(overlayNode) : document.documentElement; + let isViewportContainer = container === document.documentElement; +- const containerPositionStyle = window.getComputedStyle(container).position; ++ const containerPositionStyle = actualWindow.getComputedStyle(container).position; + let isContainerPositioned = !!containerPositionStyle && containerPositionStyle !== 'static'; + let childOffset = isViewportContainer ? $5935ba4d7da2c103$var$getOffset(targetNode) : $5935ba4d7da2c103$var$getPosition(targetNode, container); + if (!isViewportContainer) { +- let { marginTop: marginTop, marginLeft: marginLeft } = window.getComputedStyle(targetNode); ++ let { marginTop: marginTop, marginLeft: marginLeft } = actualWindow.getComputedStyle(targetNode); + childOffset.top += parseInt(marginTop, 10) || 0; + childOffset.left += parseInt(marginLeft, 10) || 0; + } +@@ -306,7 +317,7 @@ function $5935ba4d7da2c103$export$b3ceb0cbf1056d98(opts) { + } + function $5935ba4d7da2c103$var$getOffset(node) { + let { top: top, left: left, width: width, height: height } = node.getBoundingClientRect(); +- let { scrollTop: scrollTop, scrollLeft: scrollLeft, clientTop: clientTop, clientLeft: clientLeft } = document.documentElement; ++ let { scrollTop: scrollTop, scrollLeft: scrollLeft, clientTop: clientTop, clientLeft: clientLeft } = $5935ba4d7da2c103$var$getDocumentElement(node); + return { + top: top + scrollTop - clientTop, + left: left + scrollLeft - clientLeft, +@@ -315,7 +326,8 @@ function $5935ba4d7da2c103$var$getOffset(node) { + }; + } + function $5935ba4d7da2c103$var$getPosition(node, parent) { +- let style = window.getComputedStyle(node); ++ const actualWindow = $5935ba4d7da2c103$var$getWindow(node); ++ let style = actualWindow.getComputedStyle(node); + let offset; + if (style.position === 'fixed') { + let { top: top, left: left, width: width, height: height } = node.getBoundingClientRect(); +@@ -328,7 +340,7 @@ function $5935ba4d7da2c103$var$getPosition(node, parent) { + } else { + offset = $5935ba4d7da2c103$var$getOffset(node); + let parentOffset = $5935ba4d7da2c103$var$getOffset(parent); +- let parentStyle = window.getComputedStyle(parent); ++ let parentStyle = actualWindow.getComputedStyle(parent); + parentOffset.top += (parseInt(parentStyle.borderTopWidth, 10) || 0) - parent.scrollTop; + parentOffset.left += (parseInt(parentStyle.borderLeftWidth, 10) || 0) - parent.scrollLeft; + offset.top -= parentOffset.top; +@@ -345,10 +357,11 @@ function $5935ba4d7da2c103$var$getContainingBlock(node) { + // The offsetParent of an element in most cases equals the containing block. + // https://w3c.github.io/csswg-drafts/cssom-view/#dom-htmlelement-offsetparent + let offsetParent = node.offsetParent; ++ const actualWindow = $5935ba4d7da2c103$var$getWindow(node); + // The offsetParent algorithm terminates at the document body, + // even if the body is not a containing block. Double check that + // and use the documentElement if so. +- if (offsetParent && offsetParent === document.body && window.getComputedStyle(offsetParent).position === 'static' && !$5935ba4d7da2c103$var$isContainingBlock(offsetParent)) offsetParent = document.documentElement; ++ if (offsetParent && offsetParent === (node.ownerDocument || document).body && actualWindow.getComputedStyle(offsetParent).position === 'static' && !$5935ba4d7da2c103$var$isContainingBlock(offsetParent)) offsetParent = $5935ba4d7da2c103$var$getDocumentElement(node); + // TODO(later): handle table elements? + // The offsetParent can be null if the element has position: fixed, or a few other cases. + // We have to walk up the tree manually in this case because fixed positioned elements +@@ -358,11 +371,12 @@ function $5935ba4d7da2c103$var$getContainingBlock(node) { + while(offsetParent && !$5935ba4d7da2c103$var$isContainingBlock(offsetParent))offsetParent = offsetParent.parentElement; + } + // Fall back to the viewport. +- return offsetParent || document.documentElement; ++ return offsetParent || $5935ba4d7da2c103$var$getDocumentElement(node); + } + // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block + function $5935ba4d7da2c103$var$isContainingBlock(node) { +- let style = window.getComputedStyle(node); ++ const actualWindow = $5935ba4d7da2c103$var$getWindow(node); ++ let style = actualWindow.getComputedStyle(node); + return style.transform !== 'none' || /transform|perspective/.test(style.willChange) || style.filter !== 'none' || style.contain === 'paint' || 'backdropFilter' in style && style.backdropFilter !== 'none' || 'WebkitBackdropFilter' in style && style.WebkitBackdropFilter !== 'none'; + } + +diff --git a/dist/calculatePosition.main.js.map b/dist/calculatePosition.main.js.map +index ad521e103b06701d6efc5361fbaee754f9d0b3b2..860c2cbce8ce457f15813d176f17d10c945775bc 100644 +--- a/dist/calculatePosition.main.js.map ++++ b/dist/calculatePosition.main.js.map +@@ -1 +1 @@ +-{"mappings":"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","sources":["packages/@react-aria/overlays/src/calculatePosition.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {Axis, Placement, PlacementAxis, SizeAxis} from '@react-types/overlays';\nimport {clamp, isWebKit} from '@react-aria/utils';\n\ninterface Position {\n top?: number,\n left?: number,\n bottom?: number,\n right?: number\n}\n\ninterface Dimensions {\n width: number,\n height: number,\n totalWidth: number,\n totalHeight: number,\n top: number,\n left: number,\n scroll: Position\n}\n\ninterface ParsedPlacement {\n placement: PlacementAxis,\n crossPlacement: PlacementAxis,\n axis: Axis,\n crossAxis: Axis,\n size: SizeAxis,\n crossSize: SizeAxis\n}\n\ninterface Offset {\n top: number,\n left: number,\n width: number,\n height: number\n}\n\ninterface PositionOpts {\n arrowSize: number,\n placement: Placement,\n targetNode: Element,\n overlayNode: Element,\n scrollNode: Element,\n padding: number,\n shouldFlip: boolean,\n boundaryElement: Element,\n offset: number,\n crossOffset: number,\n maxHeight?: number,\n arrowBoundaryOffset?: number\n}\n\ntype HeightGrowthDirection = 'top' | 'bottom';\n\nexport interface PositionResult {\n position: Position,\n arrowOffsetLeft?: number,\n arrowOffsetTop?: number,\n maxHeight: number,\n placement: PlacementAxis\n}\n\nconst AXIS = {\n top: 'top',\n bottom: 'top',\n left: 'left',\n right: 'left'\n};\n\nconst FLIPPED_DIRECTION = {\n top: 'bottom',\n bottom: 'top',\n left: 'right',\n right: 'left'\n};\n\nconst CROSS_AXIS = {\n top: 'left',\n left: 'top'\n};\n\nconst AXIS_SIZE = {\n top: 'height',\n left: 'width'\n};\n\nconst TOTAL_SIZE = {\n width: 'totalWidth',\n height: 'totalHeight'\n};\n\nconst PARSED_PLACEMENT_CACHE = {};\n\nlet visualViewport = typeof document !== 'undefined' ? window.visualViewport : null;\n\nfunction getContainerDimensions(containerNode: Element): Dimensions {\n let width = 0, height = 0, totalWidth = 0, totalHeight = 0, top = 0, left = 0;\n let scroll: Position = {};\n let isPinchZoomedIn = (visualViewport?.scale ?? 1) > 1;\n\n if (containerNode.tagName === 'BODY') {\n let documentElement = document.documentElement;\n totalWidth = documentElement.clientWidth;\n totalHeight = documentElement.clientHeight;\n width = visualViewport?.width ?? totalWidth;\n height = visualViewport?.height ?? totalHeight;\n scroll.top = documentElement.scrollTop || containerNode.scrollTop;\n scroll.left = documentElement.scrollLeft || containerNode.scrollLeft;\n\n // The goal of the below is to get a top/left value that represents the top/left of the visual viewport with\n // respect to the layout viewport origin. This combined with the scrollTop/scrollLeft will allow us to calculate\n // coordinates/values with respect to the visual viewport or with respect to the layout viewport.\n if (visualViewport) {\n top = visualViewport.offsetTop;\n left = visualViewport.offsetLeft;\n }\n } else {\n ({width, height, top, left} = getOffset(containerNode));\n scroll.top = containerNode.scrollTop;\n scroll.left = containerNode.scrollLeft;\n totalWidth = width;\n totalHeight = height;\n }\n\n if (isWebKit() && (containerNode.tagName === 'BODY' || containerNode.tagName === 'HTML') && isPinchZoomedIn) {\n // Safari will report a non-zero scrollTop/Left for the non-scrolling body/HTML element when pinch zoomed in unlike other browsers.\n // Set to zero for parity calculations so we get consistent positioning of overlays across all browsers.\n // Also switch to visualViewport.pageTop/pageLeft so that we still accomodate for scroll positioning for body/HTML elements that are actually scrollable\n // before pinch zoom happens\n scroll.top = 0;\n scroll.left = 0;\n top = visualViewport?.pageTop ?? 0;\n left = visualViewport?.pageLeft ?? 0;\n }\n\n return {width, height, totalWidth, totalHeight, scroll, top, left};\n}\n\nfunction getScroll(node: Element): Offset {\n return {\n top: node.scrollTop,\n left: node.scrollLeft,\n width: node.scrollWidth,\n height: node.scrollHeight\n };\n}\n\n// Determines the amount of space required when moving the overlay to ensure it remains in the boundary\nfunction getDelta(\n axis: Axis,\n offset: number,\n size: number,\n // The dimensions of the boundary element that the popover is\n // positioned within (most of the time this is the ).\n boundaryDimensions: Dimensions,\n // The dimensions of the containing block element that the popover is\n // positioned relative to (e.g. parent with position: relative).\n // Usually this is the same as the boundary element, but if the popover\n // is portaled somewhere other than the body and has an ancestor with\n // position: relative/absolute, it will be different.\n containerDimensions: Dimensions,\n padding: number,\n containerOffsetWithBoundary: Offset\n) {\n let containerScroll = containerDimensions.scroll[axis] ?? 0;\n // The height/width of the boundary. Matches the axis along which we are adjusting the overlay position\n let boundarySize = boundaryDimensions[AXIS_SIZE[axis]];\n // Calculate the edges of the boundary (accomodating for the boundary padding) and the edges of the overlay.\n // Note that these values are with respect to the visual viewport (aka 0,0 is the top left of the viewport)\n let boundaryStartEdge = boundaryDimensions.scroll[AXIS[axis]] + padding;\n let boundaryEndEdge = boundarySize + boundaryDimensions.scroll[AXIS[axis]] - padding;\n let startEdgeOffset = offset - containerScroll + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];\n let endEdgeOffset = offset - containerScroll + size + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];\n\n // If any of the overlay edges falls outside of the boundary, shift the overlay the required amount to align one of the overlay's\n // edges with the closest boundary edge.\n if (startEdgeOffset < boundaryStartEdge) {\n return boundaryStartEdge - startEdgeOffset;\n } else if (endEdgeOffset > boundaryEndEdge) {\n return Math.max(boundaryEndEdge - endEdgeOffset, boundaryStartEdge - startEdgeOffset);\n } else {\n return 0;\n }\n}\n\nfunction getMargins(node: Element): Position {\n let style = window.getComputedStyle(node);\n return {\n top: parseInt(style.marginTop, 10) || 0,\n bottom: parseInt(style.marginBottom, 10) || 0,\n left: parseInt(style.marginLeft, 10) || 0,\n right: parseInt(style.marginRight, 10) || 0\n };\n}\n\nfunction parsePlacement(input: Placement): ParsedPlacement {\n if (PARSED_PLACEMENT_CACHE[input]) {\n return PARSED_PLACEMENT_CACHE[input];\n }\n\n let [placement, crossPlacement] = input.split(' ');\n let axis: Axis = AXIS[placement] || 'right';\n let crossAxis: Axis = CROSS_AXIS[axis];\n\n if (!AXIS[crossPlacement]) {\n crossPlacement = 'center';\n }\n\n let size = AXIS_SIZE[axis];\n let crossSize = AXIS_SIZE[crossAxis];\n PARSED_PLACEMENT_CACHE[input] = {placement, crossPlacement, axis, crossAxis, size, crossSize};\n return PARSED_PLACEMENT_CACHE[input];\n}\n\nfunction computePosition(\n childOffset: Offset,\n boundaryDimensions: Dimensions,\n overlaySize: Offset,\n placementInfo: ParsedPlacement,\n offset: number,\n crossOffset: number,\n containerOffsetWithBoundary: Offset,\n isContainerPositioned: boolean,\n arrowSize: number,\n arrowBoundaryOffset: number\n) {\n let {placement, crossPlacement, axis, crossAxis, size, crossSize} = placementInfo;\n let position: Position = {};\n\n // button position\n position[crossAxis] = childOffset[crossAxis] ?? 0;\n if (crossPlacement === 'center') {\n // + (button size / 2) - (overlay size / 2)\n // at this point the overlay center should match the button center\n position[crossAxis]! += ((childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0)) / 2;\n } else if (crossPlacement !== crossAxis) {\n // + (button size) - (overlay size)\n // at this point the overlay bottom should match the button bottom\n position[crossAxis]! += (childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0);\n }/* else {\n the overlay top should match the button top\n } */\n\n position[crossAxis]! += crossOffset;\n\n // overlay top overlapping arrow with button bottom\n const minPosition = childOffset[crossAxis] - overlaySize[crossSize] + arrowSize + arrowBoundaryOffset;\n // overlay bottom overlapping arrow with button top\n const maxPosition = childOffset[crossAxis] + childOffset[crossSize] - arrowSize - arrowBoundaryOffset;\n position[crossAxis] = clamp(position[crossAxis]!, minPosition, maxPosition);\n\n // Floor these so the position isn't placed on a partial pixel, only whole pixels. Shouldn't matter if it was floored or ceiled, so chose one.\n if (placement === axis) {\n // If the container is positioned (non-static), then we use the container's actual\n // height, as `bottom` will be relative to this height. But if the container is static,\n // then it can only be the `document.body`, and `bottom` will be relative to _its_\n // container, which should be as large as boundaryDimensions.\n const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary[size] : boundaryDimensions[TOTAL_SIZE[size]]);\n position[FLIPPED_DIRECTION[axis]] = Math.floor(containerHeight - childOffset[axis] + offset);\n } else {\n position[axis] = Math.floor(childOffset[axis] + childOffset[size] + offset);\n }\n return position;\n}\n\nfunction getMaxHeight(\n position: Position,\n boundaryDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n isContainerPositioned: boolean,\n margins: Position,\n padding: number,\n overlayHeight: number,\n heightGrowthDirection: HeightGrowthDirection\n) {\n const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary.height : boundaryDimensions[TOTAL_SIZE.height]);\n // For cases where position is set via \"bottom\" instead of \"top\", we need to calculate the true overlay top with respect to the boundary. Reverse calculate this with the same method\n // used in computePosition.\n let overlayTop = position.top != null ? containerOffsetWithBoundary.top + position.top : containerOffsetWithBoundary.top + (containerHeight - (position.bottom ?? 0) - overlayHeight);\n let maxHeight = heightGrowthDirection !== 'top' ?\n // We want the distance between the top of the overlay to the bottom of the boundary\n Math.max(0,\n (boundaryDimensions.height + boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the bottom of the boundary\n - overlayTop // this is the top of the overlay\n - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding\n )\n // We want the distance between the bottom of the overlay to the top of the boundary\n : Math.max(0,\n (overlayTop + overlayHeight) // this is the bottom of the overlay\n - (boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the top of the boundary\n - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding\n );\n return Math.min(boundaryDimensions.height - (padding * 2), maxHeight);\n}\n\nfunction getAvailableSpace(\n boundaryDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n childOffset: Offset,\n margins: Position,\n padding: number,\n placementInfo: ParsedPlacement\n) {\n let {placement, axis, size} = placementInfo;\n if (placement === axis) {\n return Math.max(0, childOffset[axis] - boundaryDimensions[axis] - (boundaryDimensions.scroll[axis] ?? 0) + containerOffsetWithBoundary[axis] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding);\n }\n\n return Math.max(0, boundaryDimensions[size] + boundaryDimensions[axis] + boundaryDimensions.scroll[axis] - containerOffsetWithBoundary[axis] - childOffset[axis] - childOffset[size] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding);\n}\n\nexport function calculatePositionInternal(\n placementInput: Placement,\n childOffset: Offset,\n overlaySize: Offset,\n scrollSize: Offset,\n margins: Position,\n padding: number,\n flip: boolean,\n boundaryDimensions: Dimensions,\n containerDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n offset: number,\n crossOffset: number,\n isContainerPositioned: boolean,\n userSetMaxHeight: number | undefined,\n arrowSize: number,\n arrowBoundaryOffset: number\n): PositionResult {\n let placementInfo = parsePlacement(placementInput);\n let {size, crossAxis, crossSize, placement, crossPlacement} = placementInfo;\n let position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n let normalizedOffset = offset;\n let space = getAvailableSpace(\n boundaryDimensions,\n containerOffsetWithBoundary,\n childOffset,\n margins,\n padding + offset,\n placementInfo\n );\n\n // Check if the scroll size of the overlay is greater than the available space to determine if we need to flip\n if (flip && scrollSize[size] > space) {\n let flippedPlacementInfo = parsePlacement(`${FLIPPED_DIRECTION[placement]} ${crossPlacement}` as Placement);\n let flippedPosition = computePosition(childOffset, boundaryDimensions, overlaySize, flippedPlacementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n let flippedSpace = getAvailableSpace(\n boundaryDimensions,\n containerOffsetWithBoundary,\n childOffset,\n margins,\n padding + offset,\n flippedPlacementInfo\n );\n\n // If the available space for the flipped position is greater than the original available space, flip.\n if (flippedSpace > space) {\n placementInfo = flippedPlacementInfo;\n position = flippedPosition;\n normalizedOffset = offset;\n }\n }\n\n // Determine the direction the height of the overlay can grow so that we can choose how to calculate the max height\n let heightGrowthDirection: HeightGrowthDirection = 'bottom';\n if (placementInfo.axis === 'top') {\n if (placementInfo.placement === 'top') {\n heightGrowthDirection = 'top';\n } else if (placementInfo.placement === 'bottom') {\n heightGrowthDirection = 'bottom';\n }\n } else if (placementInfo.crossAxis === 'top') {\n if (placementInfo.crossPlacement === 'top') {\n heightGrowthDirection = 'bottom';\n } else if (placementInfo.crossPlacement === 'bottom') {\n heightGrowthDirection = 'top';\n }\n }\n\n let delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary);\n position[crossAxis]! += delta;\n\n let maxHeight = getMaxHeight(\n position,\n boundaryDimensions,\n containerOffsetWithBoundary,\n isContainerPositioned,\n margins,\n padding,\n overlaySize.height,\n heightGrowthDirection\n );\n\n if (userSetMaxHeight && userSetMaxHeight < maxHeight) {\n maxHeight = userSetMaxHeight;\n }\n\n overlaySize.height = Math.min(overlaySize.height, maxHeight);\n\n position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, normalizedOffset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary);\n position[crossAxis]! += delta;\n\n let arrowPosition: Position = {};\n\n // All values are transformed so that 0 is at the top/left of the overlay depending on the orientation\n // Prefer the arrow being in the center of the trigger/overlay anchor element\n // childOffset[crossAxis] + .5 * childOffset[crossSize] = absolute position with respect to the trigger's coordinate system that would place the arrow in the center of the trigger\n // position[crossAxis] - margins[AXIS[crossAxis]] = value use to transform the position to a value with respect to the overlay's coordinate system. A child element's (aka arrow) position absolute's \"0\"\n // is positioned after the margin of its parent (aka overlay) so we need to subtract it to get the proper coordinate transform\n let preferredArrowPosition = childOffset[crossAxis] + .5 * childOffset[crossSize] - position[crossAxis]! - margins[AXIS[crossAxis]];\n\n // Min/Max position limits for the arrow with respect to the overlay\n const arrowMinPosition = arrowSize / 2 + arrowBoundaryOffset;\n // overlaySize[crossSize] - margins = true size of the overlay\n const overlayMargin = AXIS[crossAxis] === 'left' ? (margins.left ?? 0) + (margins.right ?? 0) : (margins.top ?? 0) + (margins.bottom ?? 0);\n const arrowMaxPosition = overlaySize[crossSize] - overlayMargin - (arrowSize / 2) - arrowBoundaryOffset;\n\n // Min/Max position limits for the arrow with respect to the trigger/overlay anchor element\n // Same margin accomodation done here as well as for the preferredArrowPosition\n const arrowOverlappingChildMinEdge = childOffset[crossAxis] + (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]);\n const arrowOverlappingChildMaxEdge = childOffset[crossAxis] + childOffset[crossSize] - (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]);\n\n // Clamp the arrow positioning so that it always is within the bounds of the anchor and the overlay\n const arrowPositionOverlappingChild = clamp(preferredArrowPosition, arrowOverlappingChildMinEdge, arrowOverlappingChildMaxEdge);\n arrowPosition[crossAxis] = clamp(arrowPositionOverlappingChild, arrowMinPosition, arrowMaxPosition);\n\n return {\n position,\n maxHeight: maxHeight,\n arrowOffsetLeft: arrowPosition.left,\n arrowOffsetTop: arrowPosition.top,\n placement: placementInfo.placement\n };\n}\n\n/**\n * Determines where to place the overlay with regards to the target and the position of an optional indicator.\n */\nexport function calculatePosition(opts: PositionOpts): PositionResult {\n let {\n placement,\n targetNode,\n overlayNode,\n scrollNode,\n padding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize = 0,\n arrowBoundaryOffset = 0\n } = opts;\n\n let container = overlayNode instanceof HTMLElement ? getContainingBlock(overlayNode) : document.documentElement;\n let isViewportContainer = container === document.documentElement;\n const containerPositionStyle = window.getComputedStyle(container).position;\n let isContainerPositioned = !!containerPositionStyle && containerPositionStyle !== 'static';\n let childOffset: Offset = isViewportContainer ? getOffset(targetNode) : getPosition(targetNode, container);\n\n if (!isViewportContainer) {\n let {marginTop, marginLeft} = window.getComputedStyle(targetNode);\n childOffset.top += parseInt(marginTop, 10) || 0;\n childOffset.left += parseInt(marginLeft, 10) || 0;\n }\n\n let overlaySize: Offset = getOffset(overlayNode);\n let margins = getMargins(overlayNode);\n overlaySize.width += (margins.left ?? 0) + (margins.right ?? 0);\n overlaySize.height += (margins.top ?? 0) + (margins.bottom ?? 0);\n\n let scrollSize = getScroll(scrollNode);\n let boundaryDimensions = getContainerDimensions(boundaryElement);\n let containerDimensions = getContainerDimensions(container);\n // If the container is the HTML element wrapping the body element, the retrieved scrollTop/scrollLeft will be equal to the\n // body element's scroll. Set the container's scroll values to 0 since the overlay's edge position value in getDelta don't then need to be further offset\n // by the container scroll since they are essentially the same containing element and thus in the same coordinate system\n let containerOffsetWithBoundary: Offset = boundaryElement.tagName === 'BODY' ? getOffset(container) : getPosition(container, boundaryElement);\n if (container.tagName === 'HTML' && boundaryElement.tagName === 'BODY') {\n containerDimensions.scroll.top = 0;\n containerDimensions.scroll.left = 0;\n }\n\n return calculatePositionInternal(\n placement,\n childOffset,\n overlaySize,\n scrollSize,\n margins,\n padding,\n shouldFlip,\n boundaryDimensions,\n containerDimensions,\n containerOffsetWithBoundary,\n offset,\n crossOffset,\n isContainerPositioned,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n );\n}\n\nfunction getOffset(node: Element): Offset {\n let {top, left, width, height} = node.getBoundingClientRect();\n let {scrollTop, scrollLeft, clientTop, clientLeft} = document.documentElement;\n return {\n top: top + scrollTop - clientTop,\n left: left + scrollLeft - clientLeft,\n width,\n height\n };\n}\n\nfunction getPosition(node: Element, parent: Element): Offset {\n let style = window.getComputedStyle(node);\n let offset: Offset;\n if (style.position === 'fixed') {\n let {top, left, width, height} = node.getBoundingClientRect();\n offset = {top, left, width, height};\n } else {\n offset = getOffset(node);\n let parentOffset = getOffset(parent);\n let parentStyle = window.getComputedStyle(parent);\n parentOffset.top += (parseInt(parentStyle.borderTopWidth, 10) || 0) - parent.scrollTop;\n parentOffset.left += (parseInt(parentStyle.borderLeftWidth, 10) || 0) - parent.scrollLeft;\n offset.top -= parentOffset.top;\n offset.left -= parentOffset.left;\n }\n\n offset.top -= parseInt(style.marginTop, 10) || 0;\n offset.left -= parseInt(style.marginLeft, 10) || 0;\n return offset;\n}\n\n// Returns the containing block of an element, which is the element that\n// this element will be positioned relative to.\n// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block\nfunction getContainingBlock(node: HTMLElement): Element {\n // The offsetParent of an element in most cases equals the containing block.\n // https://w3c.github.io/csswg-drafts/cssom-view/#dom-htmlelement-offsetparent\n let offsetParent = node.offsetParent;\n\n // The offsetParent algorithm terminates at the document body,\n // even if the body is not a containing block. Double check that\n // and use the documentElement if so.\n if (\n offsetParent &&\n offsetParent === document.body &&\n window.getComputedStyle(offsetParent).position === 'static' &&\n !isContainingBlock(offsetParent)\n ) {\n offsetParent = document.documentElement;\n }\n\n // TODO(later): handle table elements?\n\n // The offsetParent can be null if the element has position: fixed, or a few other cases.\n // We have to walk up the tree manually in this case because fixed positioned elements\n // are still positioned relative to their containing block, which is not always the viewport.\n if (offsetParent == null) {\n offsetParent = node.parentElement;\n while (offsetParent && !isContainingBlock(offsetParent)) {\n offsetParent = offsetParent.parentElement;\n }\n }\n\n // Fall back to the viewport.\n return offsetParent || document.documentElement;\n}\n\n// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\nfunction isContainingBlock(node: Element): boolean {\n let style = window.getComputedStyle(node);\n return (\n style.transform !== 'none' ||\n /transform|perspective/.test(style.willChange) ||\n style.filter !== 'none' ||\n style.contain === 'paint' ||\n ('backdropFilter' in style && style.backdropFilter !== 'none') ||\n ('WebkitBackdropFilter' in style && style.WebkitBackdropFilter !== 'none')\n );\n}\n"],"names":[],"version":3,"file":"calculatePosition.main.js.map"} +\ No newline at end of file ++{"mappings":"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","sources":["packages/@react-aria/overlays/src/calculatePosition.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {Axis, Placement, PlacementAxis, SizeAxis} from '@react-types/overlays';\nimport {clamp, isWebKit} from '@react-aria/utils';\n\ninterface Position {\n top?: number,\n left?: number,\n bottom?: number,\n right?: number\n}\n\ninterface Dimensions {\n width: number,\n height: number,\n totalWidth: number,\n totalHeight: number,\n top: number,\n left: number,\n scroll: Position\n}\n\ninterface ParsedPlacement {\n placement: PlacementAxis,\n crossPlacement: PlacementAxis,\n axis: Axis,\n crossAxis: Axis,\n size: SizeAxis,\n crossSize: SizeAxis\n}\n\ninterface Offset {\n top: number,\n left: number,\n width: number,\n height: number\n}\n\ninterface PositionOpts {\n arrowSize: number,\n placement: Placement,\n targetNode: Element,\n overlayNode: Element,\n scrollNode: Element,\n padding: number,\n shouldFlip: boolean,\n boundaryElement: Element,\n offset: number,\n crossOffset: number,\n maxHeight?: number,\n arrowBoundaryOffset?: number\n}\n\ntype HeightGrowthDirection = 'top' | 'bottom';\n\nexport interface PositionResult {\n position: Position,\n arrowOffsetLeft?: number,\n arrowOffsetTop?: number,\n maxHeight: number,\n placement: PlacementAxis\n}\n\nconst AXIS = {\n top: 'top',\n bottom: 'top',\n left: 'left',\n right: 'left'\n};\n\nconst FLIPPED_DIRECTION = {\n top: 'bottom',\n bottom: 'top',\n left: 'right',\n right: 'left'\n};\n\nconst CROSS_AXIS = {\n top: 'left',\n left: 'top'\n};\n\nconst AXIS_SIZE = {\n top: 'height',\n left: 'width'\n};\n\nconst TOTAL_SIZE = {\n width: 'totalWidth',\n height: 'totalHeight'\n};\n\nconst PARSED_PLACEMENT_CACHE = {};\n\nlet getWindow = (targetNode?: Element | null): Window => {\n return targetNode?.ownerDocument.defaultView || window;\n};\n\nlet getVisualViewport = (actualWindow: Window): VisualViewport | null => {\n return actualWindow.visualViewport || null;\n};\n\n\nfunction getDocumentElement(node: Element): Element {\n return node.ownerDocument.documentElement || document.documentElement;\n}\n\nfunction getContainerDimensions(containerNode: Element): Dimensions {\n const visualViewport = getVisualViewport(getWindow(containerNode));\n\n let width = 0, height = 0, totalWidth = 0, totalHeight = 0, top = 0, left = 0;\n let scroll: Position = {};\n let isPinchZoomedIn = (visualViewport?.scale ?? 1) > 1;\n\n\n if (containerNode.tagName === 'BODY') {\n let documentElement = getDocumentElement(containerNode);\n totalWidth = documentElement.clientWidth;\n totalHeight = documentElement.clientHeight;\n width = visualViewport?.width ?? totalWidth;\n height = visualViewport?.height ?? totalHeight;\n scroll.top = documentElement.scrollTop || containerNode.scrollTop;\n scroll.left = documentElement.scrollLeft || containerNode.scrollLeft;\n\n // The goal of the below is to get a top/left value that represents the top/left of the visual viewport with\n // respect to the layout viewport origin. This combined with the scrollTop/scrollLeft will allow us to calculate\n // coordinates/values with respect to the visual viewport or with respect to the layout viewport.\n if (visualViewport) {\n top = visualViewport.offsetTop;\n left = visualViewport.offsetLeft;\n }\n } else {\n ({width, height, top, left} = getOffset(containerNode));\n scroll.top = containerNode.scrollTop;\n scroll.left = containerNode.scrollLeft;\n totalWidth = width;\n totalHeight = height;\n }\n\n if (isWebKit() && (containerNode.tagName === 'BODY' || containerNode.tagName === 'HTML') && isPinchZoomedIn) {\n // Safari will report a non-zero scrollTop/Left for the non-scrolling body/HTML element when pinch zoomed in unlike other browsers.\n // Set to zero for parity calculations so we get consistent positioning of overlays across all browsers.\n // Also switch to visualViewport.pageTop/pageLeft so that we still accomodate for scroll positioning for body/HTML elements that are actually scrollable\n // before pinch zoom happens\n scroll.top = 0;\n scroll.left = 0;\n top = visualViewport?.pageTop ?? 0;\n left = visualViewport?.pageLeft ?? 0;\n }\n\n return {width, height, totalWidth, totalHeight, scroll, top, left};\n}\n\nfunction getScroll(node: Element): Offset {\n return {\n top: node.scrollTop,\n left: node.scrollLeft,\n width: node.scrollWidth,\n height: node.scrollHeight\n };\n}\n\n// Determines the amount of space required when moving the overlay to ensure it remains in the boundary\nfunction getDelta(\n axis: Axis,\n offset: number,\n size: number,\n // The dimensions of the boundary element that the popover is\n // positioned within (most of the time this is the ).\n boundaryDimensions: Dimensions,\n // The dimensions of the containing block element that the popover is\n // positioned relative to (e.g. parent with position: relative).\n // Usually this is the same as the boundary element, but if the popover\n // is portaled somewhere other than the body and has an ancestor with\n // position: relative/absolute, it will be different.\n containerDimensions: Dimensions,\n padding: number,\n containerOffsetWithBoundary: Offset\n) {\n let containerScroll = containerDimensions.scroll[axis] ?? 0;\n // The height/width of the boundary. Matches the axis along which we are adjusting the overlay position\n let boundarySize = boundaryDimensions[AXIS_SIZE[axis]];\n // Calculate the edges of the boundary (accomodating for the boundary padding) and the edges of the overlay.\n // Note that these values are with respect to the visual viewport (aka 0,0 is the top left of the viewport)\n let boundaryStartEdge = boundaryDimensions.scroll[AXIS[axis]] + padding;\n let boundaryEndEdge = boundarySize + boundaryDimensions.scroll[AXIS[axis]] - padding;\n let startEdgeOffset = offset - containerScroll + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];\n let endEdgeOffset = offset - containerScroll + size + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];\n\n // If any of the overlay edges falls outside of the boundary, shift the overlay the required amount to align one of the overlay's\n // edges with the closest boundary edge.\n if (startEdgeOffset < boundaryStartEdge) {\n return boundaryStartEdge - startEdgeOffset;\n } else if (endEdgeOffset > boundaryEndEdge) {\n return Math.max(boundaryEndEdge - endEdgeOffset, boundaryStartEdge - startEdgeOffset);\n } else {\n return 0;\n }\n}\n\nfunction getMargins(node: Element): Position {\n const actualWindow = getWindow(node);\n let style = actualWindow.getComputedStyle(node);\n return {\n top: parseInt(style.marginTop, 10) || 0,\n bottom: parseInt(style.marginBottom, 10) || 0,\n left: parseInt(style.marginLeft, 10) || 0,\n right: parseInt(style.marginRight, 10) || 0\n };\n}\n\nfunction parsePlacement(input: Placement): ParsedPlacement {\n if (PARSED_PLACEMENT_CACHE[input]) {\n return PARSED_PLACEMENT_CACHE[input];\n }\n\n let [placement, crossPlacement] = input.split(' ');\n let axis: Axis = AXIS[placement] || 'right';\n let crossAxis: Axis = CROSS_AXIS[axis];\n\n if (!AXIS[crossPlacement]) {\n crossPlacement = 'center';\n }\n\n let size = AXIS_SIZE[axis];\n let crossSize = AXIS_SIZE[crossAxis];\n PARSED_PLACEMENT_CACHE[input] = {placement, crossPlacement, axis, crossAxis, size, crossSize};\n return PARSED_PLACEMENT_CACHE[input];\n}\n\nfunction computePosition(\n childOffset: Offset,\n boundaryDimensions: Dimensions,\n overlaySize: Offset,\n placementInfo: ParsedPlacement,\n offset: number,\n crossOffset: number,\n containerOffsetWithBoundary: Offset,\n isContainerPositioned: boolean,\n arrowSize: number,\n arrowBoundaryOffset: number\n) {\n let {placement, crossPlacement, axis, crossAxis, size, crossSize} = placementInfo;\n let position: Position = {};\n\n // button position\n position[crossAxis] = childOffset[crossAxis] ?? 0;\n if (crossPlacement === 'center') {\n // + (button size / 2) - (overlay size / 2)\n // at this point the overlay center should match the button center\n position[crossAxis]! += ((childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0)) / 2;\n } else if (crossPlacement !== crossAxis) {\n // + (button size) - (overlay size)\n // at this point the overlay bottom should match the button bottom\n position[crossAxis]! += (childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0);\n }/* else {\n the overlay top should match the button top\n } */\n\n position[crossAxis]! += crossOffset;\n\n // overlay top overlapping arrow with button bottom\n const minPosition = childOffset[crossAxis] - overlaySize[crossSize] + arrowSize + arrowBoundaryOffset;\n // overlay bottom overlapping arrow with button top\n const maxPosition = childOffset[crossAxis] + childOffset[crossSize] - arrowSize - arrowBoundaryOffset;\n position[crossAxis] = clamp(position[crossAxis]!, minPosition, maxPosition);\n\n // Floor these so the position isn't placed on a partial pixel, only whole pixels. Shouldn't matter if it was floored or ceiled, so chose one.\n if (placement === axis) {\n // If the container is positioned (non-static), then we use the container's actual\n // height, as `bottom` will be relative to this height. But if the container is static,\n // then it can only be the `document.body`, and `bottom` will be relative to _its_\n // container, which should be as large as boundaryDimensions.\n const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary[size] : boundaryDimensions[TOTAL_SIZE[size]]);\n position[FLIPPED_DIRECTION[axis]] = Math.floor(containerHeight - childOffset[axis] + offset);\n } else {\n position[axis] = Math.floor(childOffset[axis] + childOffset[size] + offset);\n }\n return position;\n}\n\nfunction getMaxHeight(\n position: Position,\n boundaryDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n isContainerPositioned: boolean,\n margins: Position,\n padding: number,\n overlayHeight: number,\n heightGrowthDirection: HeightGrowthDirection\n) {\n const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary.height : boundaryDimensions[TOTAL_SIZE.height]);\n // For cases where position is set via \"bottom\" instead of \"top\", we need to calculate the true overlay top with respect to the boundary. Reverse calculate this with the same method\n // used in computePosition.\n let overlayTop = position.top != null ? containerOffsetWithBoundary.top + position.top : containerOffsetWithBoundary.top + (containerHeight - (position.bottom ?? 0) - overlayHeight);\n let maxHeight = heightGrowthDirection !== 'top' ?\n // We want the distance between the top of the overlay to the bottom of the boundary\n Math.max(0,\n (boundaryDimensions.height + boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the bottom of the boundary\n - overlayTop // this is the top of the overlay\n - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding\n )\n // We want the distance between the bottom of the overlay to the top of the boundary\n : Math.max(0,\n (overlayTop + overlayHeight) // this is the bottom of the overlay\n - (boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the top of the boundary\n - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding\n );\n return Math.min(boundaryDimensions.height - (padding * 2), maxHeight);\n}\n\nfunction getAvailableSpace(\n boundaryDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n childOffset: Offset,\n margins: Position,\n padding: number,\n placementInfo: ParsedPlacement\n) {\n let {placement, axis, size} = placementInfo;\n if (placement === axis) {\n return Math.max(0, childOffset[axis] - boundaryDimensions[axis] - (boundaryDimensions.scroll[axis] ?? 0) + containerOffsetWithBoundary[axis] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding);\n }\n\n return Math.max(0, boundaryDimensions[size] + boundaryDimensions[axis] + boundaryDimensions.scroll[axis] - containerOffsetWithBoundary[axis] - childOffset[axis] - childOffset[size] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding);\n}\n\nexport function calculatePositionInternal(\n placementInput: Placement,\n childOffset: Offset,\n overlaySize: Offset,\n scrollSize: Offset,\n margins: Position,\n padding: number,\n flip: boolean,\n boundaryDimensions: Dimensions,\n containerDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n offset: number,\n crossOffset: number,\n isContainerPositioned: boolean,\n userSetMaxHeight: number | undefined,\n arrowSize: number,\n arrowBoundaryOffset: number\n): PositionResult {\n let placementInfo = parsePlacement(placementInput);\n let {size, crossAxis, crossSize, placement, crossPlacement} = placementInfo;\n let position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n let normalizedOffset = offset;\n let space = getAvailableSpace(\n boundaryDimensions,\n containerOffsetWithBoundary,\n childOffset,\n margins,\n padding + offset,\n placementInfo\n );\n\n // Check if the scroll size of the overlay is greater than the available space to determine if we need to flip\n if (flip && scrollSize[size] > space) {\n let flippedPlacementInfo = parsePlacement(`${FLIPPED_DIRECTION[placement]} ${crossPlacement}` as Placement);\n let flippedPosition = computePosition(childOffset, boundaryDimensions, overlaySize, flippedPlacementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n let flippedSpace = getAvailableSpace(\n boundaryDimensions,\n containerOffsetWithBoundary,\n childOffset,\n margins,\n padding + offset,\n flippedPlacementInfo\n );\n\n // If the available space for the flipped position is greater than the original available space, flip.\n if (flippedSpace > space) {\n placementInfo = flippedPlacementInfo;\n position = flippedPosition;\n normalizedOffset = offset;\n }\n }\n\n // Determine the direction the height of the overlay can grow so that we can choose how to calculate the max height\n let heightGrowthDirection: HeightGrowthDirection = 'bottom';\n if (placementInfo.axis === 'top') {\n if (placementInfo.placement === 'top') {\n heightGrowthDirection = 'top';\n } else if (placementInfo.placement === 'bottom') {\n heightGrowthDirection = 'bottom';\n }\n } else if (placementInfo.crossAxis === 'top') {\n if (placementInfo.crossPlacement === 'top') {\n heightGrowthDirection = 'bottom';\n } else if (placementInfo.crossPlacement === 'bottom') {\n heightGrowthDirection = 'top';\n }\n }\n\n let delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary);\n position[crossAxis]! += delta;\n\n let maxHeight = getMaxHeight(\n position,\n boundaryDimensions,\n containerOffsetWithBoundary,\n isContainerPositioned,\n margins,\n padding,\n overlaySize.height,\n heightGrowthDirection\n );\n\n if (userSetMaxHeight && userSetMaxHeight < maxHeight) {\n maxHeight = userSetMaxHeight;\n }\n\n overlaySize.height = Math.min(overlaySize.height, maxHeight);\n\n position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, normalizedOffset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary);\n position[crossAxis]! += delta;\n\n let arrowPosition: Position = {};\n\n // All values are transformed so that 0 is at the top/left of the overlay depending on the orientation\n // Prefer the arrow being in the center of the trigger/overlay anchor element\n // childOffset[crossAxis] + .5 * childOffset[crossSize] = absolute position with respect to the trigger's coordinate system that would place the arrow in the center of the trigger\n // position[crossAxis] - margins[AXIS[crossAxis]] = value use to transform the position to a value with respect to the overlay's coordinate system. A child element's (aka arrow) position absolute's \"0\"\n // is positioned after the margin of its parent (aka overlay) so we need to subtract it to get the proper coordinate transform\n let preferredArrowPosition = childOffset[crossAxis] + .5 * childOffset[crossSize] - position[crossAxis]! - margins[AXIS[crossAxis]];\n\n // Min/Max position limits for the arrow with respect to the overlay\n const arrowMinPosition = arrowSize / 2 + arrowBoundaryOffset;\n // overlaySize[crossSize] - margins = true size of the overlay\n const overlayMargin = AXIS[crossAxis] === 'left' ? (margins.left ?? 0) + (margins.right ?? 0) : (margins.top ?? 0) + (margins.bottom ?? 0);\n const arrowMaxPosition = overlaySize[crossSize] - overlayMargin - (arrowSize / 2) - arrowBoundaryOffset;\n\n // Min/Max position limits for the arrow with respect to the trigger/overlay anchor element\n // Same margin accomodation done here as well as for the preferredArrowPosition\n const arrowOverlappingChildMinEdge = childOffset[crossAxis] + (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]);\n const arrowOverlappingChildMaxEdge = childOffset[crossAxis] + childOffset[crossSize] - (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]);\n\n // Clamp the arrow positioning so that it always is within the bounds of the anchor and the overlay\n const arrowPositionOverlappingChild = clamp(preferredArrowPosition, arrowOverlappingChildMinEdge, arrowOverlappingChildMaxEdge);\n arrowPosition[crossAxis] = clamp(arrowPositionOverlappingChild, arrowMinPosition, arrowMaxPosition);\n\n return {\n position,\n maxHeight: maxHeight,\n arrowOffsetLeft: arrowPosition.left,\n arrowOffsetTop: arrowPosition.top,\n placement: placementInfo.placement\n };\n}\n\n/**\n * Determines where to place the overlay with regards to the target and the position of an optional indicator.\n */\nexport function calculatePosition(opts: PositionOpts): PositionResult {\n let {\n placement,\n targetNode,\n overlayNode,\n scrollNode,\n padding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize = 0,\n arrowBoundaryOffset = 0\n } = opts;\n const actualWindow = getWindow(targetNode);\n\n let container = overlayNode instanceof HTMLElement ? getContainingBlock(overlayNode) : document.documentElement;\n let isViewportContainer = container === document.documentElement;\n const containerPositionStyle = actualWindow.getComputedStyle(container).position;\n let isContainerPositioned = !!containerPositionStyle && containerPositionStyle !== 'static';\n let childOffset: Offset = isViewportContainer ? getOffset(targetNode) : getPosition(targetNode, container);\n\n if (!isViewportContainer) {\n let {marginTop, marginLeft} = actualWindow.getComputedStyle(targetNode);\n childOffset.top += parseInt(marginTop, 10) || 0;\n childOffset.left += parseInt(marginLeft, 10) || 0;\n }\n\n let overlaySize: Offset = getOffset(overlayNode);\n let margins = getMargins(overlayNode);\n overlaySize.width += (margins.left ?? 0) + (margins.right ?? 0);\n overlaySize.height += (margins.top ?? 0) + (margins.bottom ?? 0);\n\n let scrollSize = getScroll(scrollNode);\n let boundaryDimensions = getContainerDimensions(boundaryElement);\n let containerDimensions = getContainerDimensions(container);\n // If the container is the HTML element wrapping the body element, the retrieved scrollTop/scrollLeft will be equal to the\n // body element's scroll. Set the container's scroll values to 0 since the overlay's edge position value in getDelta don't then need to be further offset\n // by the container scroll since they are essentially the same containing element and thus in the same coordinate system\n let containerOffsetWithBoundary: Offset = boundaryElement.tagName === 'BODY' ? getOffset(container) : getPosition(container, boundaryElement);\n if (container.tagName === 'HTML' && boundaryElement.tagName === 'BODY') {\n containerDimensions.scroll.top = 0;\n containerDimensions.scroll.left = 0;\n }\n\n return calculatePositionInternal(\n placement,\n childOffset,\n overlaySize,\n scrollSize,\n margins,\n padding,\n shouldFlip,\n boundaryDimensions,\n containerDimensions,\n containerOffsetWithBoundary,\n offset,\n crossOffset,\n isContainerPositioned,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n );\n}\n\nfunction getOffset(node: Element): Offset {\n let {top, left, width, height} = node.getBoundingClientRect();\n let {scrollTop, scrollLeft, clientTop, clientLeft} = getDocumentElement(node);\n return {\n top: top + scrollTop - clientTop,\n left: left + scrollLeft - clientLeft,\n width,\n height\n };\n}\n\nfunction getPosition(node: Element, parent: Element): Offset {\n const actualWindow = getWindow(node);\n\n let style = actualWindow.getComputedStyle(node);\n let offset: Offset;\n if (style.position === 'fixed') {\n let {top, left, width, height} = node.getBoundingClientRect();\n offset = {top, left, width, height};\n } else {\n offset = getOffset(node);\n let parentOffset = getOffset(parent);\n let parentStyle = actualWindow.getComputedStyle(parent);\n parentOffset.top += (parseInt(parentStyle.borderTopWidth, 10) || 0) - parent.scrollTop;\n parentOffset.left += (parseInt(parentStyle.borderLeftWidth, 10) || 0) - parent.scrollLeft;\n offset.top -= parentOffset.top;\n offset.left -= parentOffset.left;\n }\n\n offset.top -= parseInt(style.marginTop, 10) || 0;\n offset.left -= parseInt(style.marginLeft, 10) || 0;\n return offset;\n}\n\n// Returns the containing block of an element, which is the element that\n// this element will be positioned relative to.\n// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block\nfunction getContainingBlock(node: HTMLElement): Element {\n // The offsetParent of an element in most cases equals the containing block.\n // https://w3c.github.io/csswg-drafts/cssom-view/#dom-htmlelement-offsetparent\n let offsetParent = node.offsetParent;\n const actualWindow = getWindow(node);\n\n // The offsetParent algorithm terminates at the document body,\n // even if the body is not a containing block. Double check that\n // and use the documentElement if so.\n if (\n offsetParent &&\n offsetParent === (node.ownerDocument || document).body &&\n actualWindow.getComputedStyle(offsetParent).position === 'static' &&\n !isContainingBlock(offsetParent)\n ) {\n offsetParent = getDocumentElement(node);\n }\n\n // TODO(later): handle table elements?\n\n // The offsetParent can be null if the element has position: fixed, or a few other cases.\n // We have to walk up the tree manually in this case because fixed positioned elements\n // are still positioned relative to their containing block, which is not always the viewport.\n if (offsetParent == null) {\n offsetParent = node.parentElement;\n while (offsetParent && !isContainingBlock(offsetParent)) {\n offsetParent = offsetParent.parentElement;\n }\n }\n\n // Fall back to the viewport.\n return offsetParent || getDocumentElement(node);\n}\n\n// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\nfunction isContainingBlock(node: Element): boolean {\n const actualWindow = getWindow(node);\n let style = actualWindow.getComputedStyle(node);\n return (\n style.transform !== 'none' ||\n /transform|perspective/.test(style.willChange) ||\n style.filter !== 'none' ||\n style.contain === 'paint' ||\n ('backdropFilter' in style && style.backdropFilter !== 'none') ||\n ('WebkitBackdropFilter' in style && style.WebkitBackdropFilter !== 'none')\n );\n}\n"],"names":[],"version":3,"file":"calculatePosition.main.js.map"} +\ No newline at end of file +diff --git a/dist/calculatePosition.mjs b/dist/calculatePosition.mjs +index 2b4d227e0547ad0acf92152255b8052a4b30ad3d..bc0435df4048872a693c0a97942aa3ce8fb563d8 100644 +--- a/dist/calculatePosition.mjs ++++ b/dist/calculatePosition.mjs +@@ -36,28 +36,37 @@ const $edcf132a9284368a$var$TOTAL_SIZE = { + height: 'totalHeight' + }; + const $edcf132a9284368a$var$PARSED_PLACEMENT_CACHE = {}; +-let $edcf132a9284368a$var$visualViewport = typeof document !== 'undefined' ? window.visualViewport : null; ++let $edcf132a9284368a$var$getWindow = (targetNode)=>{ ++ return (targetNode === null || targetNode === void 0 ? void 0 : targetNode.ownerDocument.defaultView) || window; ++}; ++let $edcf132a9284368a$var$getVisualViewport = (actualWindow)=>{ ++ return actualWindow.visualViewport || null; ++}; ++function $edcf132a9284368a$var$getDocumentElement(node) { ++ return node.ownerDocument.documentElement || document.documentElement; ++} + function $edcf132a9284368a$var$getContainerDimensions(containerNode) { ++ const visualViewport = $edcf132a9284368a$var$getVisualViewport($edcf132a9284368a$var$getWindow(containerNode)); + let width = 0, height = 0, totalWidth = 0, totalHeight = 0, top = 0, left = 0; + let scroll = {}; + var _visualViewport_scale; +- let isPinchZoomedIn = ((_visualViewport_scale = $edcf132a9284368a$var$visualViewport === null || $edcf132a9284368a$var$visualViewport === void 0 ? void 0 : $edcf132a9284368a$var$visualViewport.scale) !== null && _visualViewport_scale !== void 0 ? _visualViewport_scale : 1) > 1; ++ let isPinchZoomedIn = ((_visualViewport_scale = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale) !== null && _visualViewport_scale !== void 0 ? _visualViewport_scale : 1) > 1; + if (containerNode.tagName === 'BODY') { +- let documentElement = document.documentElement; ++ let documentElement = $edcf132a9284368a$var$getDocumentElement(containerNode); + totalWidth = documentElement.clientWidth; + totalHeight = documentElement.clientHeight; + var _visualViewport_width; +- width = (_visualViewport_width = $edcf132a9284368a$var$visualViewport === null || $edcf132a9284368a$var$visualViewport === void 0 ? void 0 : $edcf132a9284368a$var$visualViewport.width) !== null && _visualViewport_width !== void 0 ? _visualViewport_width : totalWidth; ++ width = (_visualViewport_width = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.width) !== null && _visualViewport_width !== void 0 ? _visualViewport_width : totalWidth; + var _visualViewport_height; +- height = (_visualViewport_height = $edcf132a9284368a$var$visualViewport === null || $edcf132a9284368a$var$visualViewport === void 0 ? void 0 : $edcf132a9284368a$var$visualViewport.height) !== null && _visualViewport_height !== void 0 ? _visualViewport_height : totalHeight; ++ height = (_visualViewport_height = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.height) !== null && _visualViewport_height !== void 0 ? _visualViewport_height : totalHeight; + scroll.top = documentElement.scrollTop || containerNode.scrollTop; + scroll.left = documentElement.scrollLeft || containerNode.scrollLeft; + // The goal of the below is to get a top/left value that represents the top/left of the visual viewport with + // respect to the layout viewport origin. This combined with the scrollTop/scrollLeft will allow us to calculate + // coordinates/values with respect to the visual viewport or with respect to the layout viewport. +- if ($edcf132a9284368a$var$visualViewport) { +- top = $edcf132a9284368a$var$visualViewport.offsetTop; +- left = $edcf132a9284368a$var$visualViewport.offsetLeft; ++ if (visualViewport) { ++ top = visualViewport.offsetTop; ++ left = visualViewport.offsetLeft; + } + } else { + ({ width: width, height: height, top: top, left: left } = $edcf132a9284368a$var$getOffset(containerNode)); +@@ -74,9 +83,9 @@ function $edcf132a9284368a$var$getContainerDimensions(containerNode) { + scroll.top = 0; + scroll.left = 0; + var _visualViewport_pageTop; +- top = (_visualViewport_pageTop = $edcf132a9284368a$var$visualViewport === null || $edcf132a9284368a$var$visualViewport === void 0 ? void 0 : $edcf132a9284368a$var$visualViewport.pageTop) !== null && _visualViewport_pageTop !== void 0 ? _visualViewport_pageTop : 0; ++ top = (_visualViewport_pageTop = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.pageTop) !== null && _visualViewport_pageTop !== void 0 ? _visualViewport_pageTop : 0; + var _visualViewport_pageLeft; +- left = (_visualViewport_pageLeft = $edcf132a9284368a$var$visualViewport === null || $edcf132a9284368a$var$visualViewport === void 0 ? void 0 : $edcf132a9284368a$var$visualViewport.pageLeft) !== null && _visualViewport_pageLeft !== void 0 ? _visualViewport_pageLeft : 0; ++ left = (_visualViewport_pageLeft = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.pageLeft) !== null && _visualViewport_pageLeft !== void 0 ? _visualViewport_pageLeft : 0; + } + return { + width: width, +@@ -122,7 +131,8 @@ containerDimensions, padding, containerOffsetWithBoundary) { + else return 0; + } + function $edcf132a9284368a$var$getMargins(node) { +- let style = window.getComputedStyle(node); ++ const actualWindow = $edcf132a9284368a$var$getWindow(node); ++ let style = actualWindow.getComputedStyle(node); + return { + top: parseInt(style.marginTop, 10) || 0, + bottom: parseInt(style.marginBottom, 10) || 0, +@@ -269,13 +279,14 @@ function $edcf132a9284368a$export$6839422d1f33cee9(placementInput, childOffset, + } + function $edcf132a9284368a$export$b3ceb0cbf1056d98(opts) { + let { placement: placement, targetNode: targetNode, overlayNode: overlayNode, scrollNode: scrollNode, padding: padding, shouldFlip: shouldFlip, boundaryElement: boundaryElement, offset: offset, crossOffset: crossOffset, maxHeight: maxHeight, arrowSize: arrowSize = 0, arrowBoundaryOffset: arrowBoundaryOffset = 0 } = opts; ++ const actualWindow = $edcf132a9284368a$var$getWindow(targetNode); + let container = overlayNode instanceof HTMLElement ? $edcf132a9284368a$var$getContainingBlock(overlayNode) : document.documentElement; + let isViewportContainer = container === document.documentElement; +- const containerPositionStyle = window.getComputedStyle(container).position; ++ const containerPositionStyle = actualWindow.getComputedStyle(container).position; + let isContainerPositioned = !!containerPositionStyle && containerPositionStyle !== 'static'; + let childOffset = isViewportContainer ? $edcf132a9284368a$var$getOffset(targetNode) : $edcf132a9284368a$var$getPosition(targetNode, container); + if (!isViewportContainer) { +- let { marginTop: marginTop, marginLeft: marginLeft } = window.getComputedStyle(targetNode); ++ let { marginTop: marginTop, marginLeft: marginLeft } = actualWindow.getComputedStyle(targetNode); + childOffset.top += parseInt(marginTop, 10) || 0; + childOffset.left += parseInt(marginLeft, 10) || 0; + } +@@ -300,7 +311,7 @@ function $edcf132a9284368a$export$b3ceb0cbf1056d98(opts) { + } + function $edcf132a9284368a$var$getOffset(node) { + let { top: top, left: left, width: width, height: height } = node.getBoundingClientRect(); +- let { scrollTop: scrollTop, scrollLeft: scrollLeft, clientTop: clientTop, clientLeft: clientLeft } = document.documentElement; ++ let { scrollTop: scrollTop, scrollLeft: scrollLeft, clientTop: clientTop, clientLeft: clientLeft } = $edcf132a9284368a$var$getDocumentElement(node); + return { + top: top + scrollTop - clientTop, + left: left + scrollLeft - clientLeft, +@@ -309,7 +320,8 @@ function $edcf132a9284368a$var$getOffset(node) { + }; + } + function $edcf132a9284368a$var$getPosition(node, parent) { +- let style = window.getComputedStyle(node); ++ const actualWindow = $edcf132a9284368a$var$getWindow(node); ++ let style = actualWindow.getComputedStyle(node); + let offset; + if (style.position === 'fixed') { + let { top: top, left: left, width: width, height: height } = node.getBoundingClientRect(); +@@ -322,7 +334,7 @@ function $edcf132a9284368a$var$getPosition(node, parent) { + } else { + offset = $edcf132a9284368a$var$getOffset(node); + let parentOffset = $edcf132a9284368a$var$getOffset(parent); +- let parentStyle = window.getComputedStyle(parent); ++ let parentStyle = actualWindow.getComputedStyle(parent); + parentOffset.top += (parseInt(parentStyle.borderTopWidth, 10) || 0) - parent.scrollTop; + parentOffset.left += (parseInt(parentStyle.borderLeftWidth, 10) || 0) - parent.scrollLeft; + offset.top -= parentOffset.top; +@@ -339,10 +351,11 @@ function $edcf132a9284368a$var$getContainingBlock(node) { + // The offsetParent of an element in most cases equals the containing block. + // https://w3c.github.io/csswg-drafts/cssom-view/#dom-htmlelement-offsetparent + let offsetParent = node.offsetParent; ++ const actualWindow = $edcf132a9284368a$var$getWindow(node); + // The offsetParent algorithm terminates at the document body, + // even if the body is not a containing block. Double check that + // and use the documentElement if so. +- if (offsetParent && offsetParent === document.body && window.getComputedStyle(offsetParent).position === 'static' && !$edcf132a9284368a$var$isContainingBlock(offsetParent)) offsetParent = document.documentElement; ++ if (offsetParent && offsetParent === (node.ownerDocument || document).body && actualWindow.getComputedStyle(offsetParent).position === 'static' && !$edcf132a9284368a$var$isContainingBlock(offsetParent)) offsetParent = $edcf132a9284368a$var$getDocumentElement(node); + // TODO(later): handle table elements? + // The offsetParent can be null if the element has position: fixed, or a few other cases. + // We have to walk up the tree manually in this case because fixed positioned elements +@@ -352,11 +365,12 @@ function $edcf132a9284368a$var$getContainingBlock(node) { + while(offsetParent && !$edcf132a9284368a$var$isContainingBlock(offsetParent))offsetParent = offsetParent.parentElement; + } + // Fall back to the viewport. +- return offsetParent || document.documentElement; ++ return offsetParent || $edcf132a9284368a$var$getDocumentElement(node); + } + // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block + function $edcf132a9284368a$var$isContainingBlock(node) { +- let style = window.getComputedStyle(node); ++ const actualWindow = $edcf132a9284368a$var$getWindow(node); ++ let style = actualWindow.getComputedStyle(node); + return style.transform !== 'none' || /transform|perspective/.test(style.willChange) || style.filter !== 'none' || style.contain === 'paint' || 'backdropFilter' in style && style.backdropFilter !== 'none' || 'WebkitBackdropFilter' in style && style.WebkitBackdropFilter !== 'none'; + } + +diff --git a/dist/calculatePosition.module.js b/dist/calculatePosition.module.js +index 2b4d227e0547ad0acf92152255b8052a4b30ad3d..bc0435df4048872a693c0a97942aa3ce8fb563d8 100644 +--- a/dist/calculatePosition.module.js ++++ b/dist/calculatePosition.module.js +@@ -36,28 +36,37 @@ const $edcf132a9284368a$var$TOTAL_SIZE = { + height: 'totalHeight' + }; + const $edcf132a9284368a$var$PARSED_PLACEMENT_CACHE = {}; +-let $edcf132a9284368a$var$visualViewport = typeof document !== 'undefined' ? window.visualViewport : null; ++let $edcf132a9284368a$var$getWindow = (targetNode)=>{ ++ return (targetNode === null || targetNode === void 0 ? void 0 : targetNode.ownerDocument.defaultView) || window; ++}; ++let $edcf132a9284368a$var$getVisualViewport = (actualWindow)=>{ ++ return actualWindow.visualViewport || null; ++}; ++function $edcf132a9284368a$var$getDocumentElement(node) { ++ return node.ownerDocument.documentElement || document.documentElement; ++} + function $edcf132a9284368a$var$getContainerDimensions(containerNode) { ++ const visualViewport = $edcf132a9284368a$var$getVisualViewport($edcf132a9284368a$var$getWindow(containerNode)); + let width = 0, height = 0, totalWidth = 0, totalHeight = 0, top = 0, left = 0; + let scroll = {}; + var _visualViewport_scale; +- let isPinchZoomedIn = ((_visualViewport_scale = $edcf132a9284368a$var$visualViewport === null || $edcf132a9284368a$var$visualViewport === void 0 ? void 0 : $edcf132a9284368a$var$visualViewport.scale) !== null && _visualViewport_scale !== void 0 ? _visualViewport_scale : 1) > 1; ++ let isPinchZoomedIn = ((_visualViewport_scale = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale) !== null && _visualViewport_scale !== void 0 ? _visualViewport_scale : 1) > 1; + if (containerNode.tagName === 'BODY') { +- let documentElement = document.documentElement; ++ let documentElement = $edcf132a9284368a$var$getDocumentElement(containerNode); + totalWidth = documentElement.clientWidth; + totalHeight = documentElement.clientHeight; + var _visualViewport_width; +- width = (_visualViewport_width = $edcf132a9284368a$var$visualViewport === null || $edcf132a9284368a$var$visualViewport === void 0 ? void 0 : $edcf132a9284368a$var$visualViewport.width) !== null && _visualViewport_width !== void 0 ? _visualViewport_width : totalWidth; ++ width = (_visualViewport_width = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.width) !== null && _visualViewport_width !== void 0 ? _visualViewport_width : totalWidth; + var _visualViewport_height; +- height = (_visualViewport_height = $edcf132a9284368a$var$visualViewport === null || $edcf132a9284368a$var$visualViewport === void 0 ? void 0 : $edcf132a9284368a$var$visualViewport.height) !== null && _visualViewport_height !== void 0 ? _visualViewport_height : totalHeight; ++ height = (_visualViewport_height = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.height) !== null && _visualViewport_height !== void 0 ? _visualViewport_height : totalHeight; + scroll.top = documentElement.scrollTop || containerNode.scrollTop; + scroll.left = documentElement.scrollLeft || containerNode.scrollLeft; + // The goal of the below is to get a top/left value that represents the top/left of the visual viewport with + // respect to the layout viewport origin. This combined with the scrollTop/scrollLeft will allow us to calculate + // coordinates/values with respect to the visual viewport or with respect to the layout viewport. +- if ($edcf132a9284368a$var$visualViewport) { +- top = $edcf132a9284368a$var$visualViewport.offsetTop; +- left = $edcf132a9284368a$var$visualViewport.offsetLeft; ++ if (visualViewport) { ++ top = visualViewport.offsetTop; ++ left = visualViewport.offsetLeft; + } + } else { + ({ width: width, height: height, top: top, left: left } = $edcf132a9284368a$var$getOffset(containerNode)); +@@ -74,9 +83,9 @@ function $edcf132a9284368a$var$getContainerDimensions(containerNode) { + scroll.top = 0; + scroll.left = 0; + var _visualViewport_pageTop; +- top = (_visualViewport_pageTop = $edcf132a9284368a$var$visualViewport === null || $edcf132a9284368a$var$visualViewport === void 0 ? void 0 : $edcf132a9284368a$var$visualViewport.pageTop) !== null && _visualViewport_pageTop !== void 0 ? _visualViewport_pageTop : 0; ++ top = (_visualViewport_pageTop = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.pageTop) !== null && _visualViewport_pageTop !== void 0 ? _visualViewport_pageTop : 0; + var _visualViewport_pageLeft; +- left = (_visualViewport_pageLeft = $edcf132a9284368a$var$visualViewport === null || $edcf132a9284368a$var$visualViewport === void 0 ? void 0 : $edcf132a9284368a$var$visualViewport.pageLeft) !== null && _visualViewport_pageLeft !== void 0 ? _visualViewport_pageLeft : 0; ++ left = (_visualViewport_pageLeft = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.pageLeft) !== null && _visualViewport_pageLeft !== void 0 ? _visualViewport_pageLeft : 0; + } + return { + width: width, +@@ -122,7 +131,8 @@ containerDimensions, padding, containerOffsetWithBoundary) { + else return 0; + } + function $edcf132a9284368a$var$getMargins(node) { +- let style = window.getComputedStyle(node); ++ const actualWindow = $edcf132a9284368a$var$getWindow(node); ++ let style = actualWindow.getComputedStyle(node); + return { + top: parseInt(style.marginTop, 10) || 0, + bottom: parseInt(style.marginBottom, 10) || 0, +@@ -269,13 +279,14 @@ function $edcf132a9284368a$export$6839422d1f33cee9(placementInput, childOffset, + } + function $edcf132a9284368a$export$b3ceb0cbf1056d98(opts) { + let { placement: placement, targetNode: targetNode, overlayNode: overlayNode, scrollNode: scrollNode, padding: padding, shouldFlip: shouldFlip, boundaryElement: boundaryElement, offset: offset, crossOffset: crossOffset, maxHeight: maxHeight, arrowSize: arrowSize = 0, arrowBoundaryOffset: arrowBoundaryOffset = 0 } = opts; ++ const actualWindow = $edcf132a9284368a$var$getWindow(targetNode); + let container = overlayNode instanceof HTMLElement ? $edcf132a9284368a$var$getContainingBlock(overlayNode) : document.documentElement; + let isViewportContainer = container === document.documentElement; +- const containerPositionStyle = window.getComputedStyle(container).position; ++ const containerPositionStyle = actualWindow.getComputedStyle(container).position; + let isContainerPositioned = !!containerPositionStyle && containerPositionStyle !== 'static'; + let childOffset = isViewportContainer ? $edcf132a9284368a$var$getOffset(targetNode) : $edcf132a9284368a$var$getPosition(targetNode, container); + if (!isViewportContainer) { +- let { marginTop: marginTop, marginLeft: marginLeft } = window.getComputedStyle(targetNode); ++ let { marginTop: marginTop, marginLeft: marginLeft } = actualWindow.getComputedStyle(targetNode); + childOffset.top += parseInt(marginTop, 10) || 0; + childOffset.left += parseInt(marginLeft, 10) || 0; + } +@@ -300,7 +311,7 @@ function $edcf132a9284368a$export$b3ceb0cbf1056d98(opts) { + } + function $edcf132a9284368a$var$getOffset(node) { + let { top: top, left: left, width: width, height: height } = node.getBoundingClientRect(); +- let { scrollTop: scrollTop, scrollLeft: scrollLeft, clientTop: clientTop, clientLeft: clientLeft } = document.documentElement; ++ let { scrollTop: scrollTop, scrollLeft: scrollLeft, clientTop: clientTop, clientLeft: clientLeft } = $edcf132a9284368a$var$getDocumentElement(node); + return { + top: top + scrollTop - clientTop, + left: left + scrollLeft - clientLeft, +@@ -309,7 +320,8 @@ function $edcf132a9284368a$var$getOffset(node) { + }; + } + function $edcf132a9284368a$var$getPosition(node, parent) { +- let style = window.getComputedStyle(node); ++ const actualWindow = $edcf132a9284368a$var$getWindow(node); ++ let style = actualWindow.getComputedStyle(node); + let offset; + if (style.position === 'fixed') { + let { top: top, left: left, width: width, height: height } = node.getBoundingClientRect(); +@@ -322,7 +334,7 @@ function $edcf132a9284368a$var$getPosition(node, parent) { + } else { + offset = $edcf132a9284368a$var$getOffset(node); + let parentOffset = $edcf132a9284368a$var$getOffset(parent); +- let parentStyle = window.getComputedStyle(parent); ++ let parentStyle = actualWindow.getComputedStyle(parent); + parentOffset.top += (parseInt(parentStyle.borderTopWidth, 10) || 0) - parent.scrollTop; + parentOffset.left += (parseInt(parentStyle.borderLeftWidth, 10) || 0) - parent.scrollLeft; + offset.top -= parentOffset.top; +@@ -339,10 +351,11 @@ function $edcf132a9284368a$var$getContainingBlock(node) { + // The offsetParent of an element in most cases equals the containing block. + // https://w3c.github.io/csswg-drafts/cssom-view/#dom-htmlelement-offsetparent + let offsetParent = node.offsetParent; ++ const actualWindow = $edcf132a9284368a$var$getWindow(node); + // The offsetParent algorithm terminates at the document body, + // even if the body is not a containing block. Double check that + // and use the documentElement if so. +- if (offsetParent && offsetParent === document.body && window.getComputedStyle(offsetParent).position === 'static' && !$edcf132a9284368a$var$isContainingBlock(offsetParent)) offsetParent = document.documentElement; ++ if (offsetParent && offsetParent === (node.ownerDocument || document).body && actualWindow.getComputedStyle(offsetParent).position === 'static' && !$edcf132a9284368a$var$isContainingBlock(offsetParent)) offsetParent = $edcf132a9284368a$var$getDocumentElement(node); + // TODO(later): handle table elements? + // The offsetParent can be null if the element has position: fixed, or a few other cases. + // We have to walk up the tree manually in this case because fixed positioned elements +@@ -352,11 +365,12 @@ function $edcf132a9284368a$var$getContainingBlock(node) { + while(offsetParent && !$edcf132a9284368a$var$isContainingBlock(offsetParent))offsetParent = offsetParent.parentElement; + } + // Fall back to the viewport. +- return offsetParent || document.documentElement; ++ return offsetParent || $edcf132a9284368a$var$getDocumentElement(node); + } + // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block + function $edcf132a9284368a$var$isContainingBlock(node) { +- let style = window.getComputedStyle(node); ++ const actualWindow = $edcf132a9284368a$var$getWindow(node); ++ let style = actualWindow.getComputedStyle(node); + return style.transform !== 'none' || /transform|perspective/.test(style.willChange) || style.filter !== 'none' || style.contain === 'paint' || 'backdropFilter' in style && style.backdropFilter !== 'none' || 'WebkitBackdropFilter' in style && style.WebkitBackdropFilter !== 'none'; + } + +diff --git a/dist/calculatePosition.module.js.map b/dist/calculatePosition.module.js.map +index fa03ef022522d4b420f64631f9343e5bd5f510fd..baa979d9a5732a5a1678d9ea9fcd6b74951c6cc2 100644 +--- a/dist/calculatePosition.module.js.map ++++ b/dist/calculatePosition.module.js.map +@@ -1 +1 @@ +-{"mappings":"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","sources":["packages/@react-aria/overlays/src/calculatePosition.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {Axis, Placement, PlacementAxis, SizeAxis} from '@react-types/overlays';\nimport {clamp, isWebKit} from '@react-aria/utils';\n\ninterface Position {\n top?: number,\n left?: number,\n bottom?: number,\n right?: number\n}\n\ninterface Dimensions {\n width: number,\n height: number,\n totalWidth: number,\n totalHeight: number,\n top: number,\n left: number,\n scroll: Position\n}\n\ninterface ParsedPlacement {\n placement: PlacementAxis,\n crossPlacement: PlacementAxis,\n axis: Axis,\n crossAxis: Axis,\n size: SizeAxis,\n crossSize: SizeAxis\n}\n\ninterface Offset {\n top: number,\n left: number,\n width: number,\n height: number\n}\n\ninterface PositionOpts {\n arrowSize: number,\n placement: Placement,\n targetNode: Element,\n overlayNode: Element,\n scrollNode: Element,\n padding: number,\n shouldFlip: boolean,\n boundaryElement: Element,\n offset: number,\n crossOffset: number,\n maxHeight?: number,\n arrowBoundaryOffset?: number\n}\n\ntype HeightGrowthDirection = 'top' | 'bottom';\n\nexport interface PositionResult {\n position: Position,\n arrowOffsetLeft?: number,\n arrowOffsetTop?: number,\n maxHeight: number,\n placement: PlacementAxis\n}\n\nconst AXIS = {\n top: 'top',\n bottom: 'top',\n left: 'left',\n right: 'left'\n};\n\nconst FLIPPED_DIRECTION = {\n top: 'bottom',\n bottom: 'top',\n left: 'right',\n right: 'left'\n};\n\nconst CROSS_AXIS = {\n top: 'left',\n left: 'top'\n};\n\nconst AXIS_SIZE = {\n top: 'height',\n left: 'width'\n};\n\nconst TOTAL_SIZE = {\n width: 'totalWidth',\n height: 'totalHeight'\n};\n\nconst PARSED_PLACEMENT_CACHE = {};\n\nlet visualViewport = typeof document !== 'undefined' ? window.visualViewport : null;\n\nfunction getContainerDimensions(containerNode: Element): Dimensions {\n let width = 0, height = 0, totalWidth = 0, totalHeight = 0, top = 0, left = 0;\n let scroll: Position = {};\n let isPinchZoomedIn = (visualViewport?.scale ?? 1) > 1;\n\n if (containerNode.tagName === 'BODY') {\n let documentElement = document.documentElement;\n totalWidth = documentElement.clientWidth;\n totalHeight = documentElement.clientHeight;\n width = visualViewport?.width ?? totalWidth;\n height = visualViewport?.height ?? totalHeight;\n scroll.top = documentElement.scrollTop || containerNode.scrollTop;\n scroll.left = documentElement.scrollLeft || containerNode.scrollLeft;\n\n // The goal of the below is to get a top/left value that represents the top/left of the visual viewport with\n // respect to the layout viewport origin. This combined with the scrollTop/scrollLeft will allow us to calculate\n // coordinates/values with respect to the visual viewport or with respect to the layout viewport.\n if (visualViewport) {\n top = visualViewport.offsetTop;\n left = visualViewport.offsetLeft;\n }\n } else {\n ({width, height, top, left} = getOffset(containerNode));\n scroll.top = containerNode.scrollTop;\n scroll.left = containerNode.scrollLeft;\n totalWidth = width;\n totalHeight = height;\n }\n\n if (isWebKit() && (containerNode.tagName === 'BODY' || containerNode.tagName === 'HTML') && isPinchZoomedIn) {\n // Safari will report a non-zero scrollTop/Left for the non-scrolling body/HTML element when pinch zoomed in unlike other browsers.\n // Set to zero for parity calculations so we get consistent positioning of overlays across all browsers.\n // Also switch to visualViewport.pageTop/pageLeft so that we still accomodate for scroll positioning for body/HTML elements that are actually scrollable\n // before pinch zoom happens\n scroll.top = 0;\n scroll.left = 0;\n top = visualViewport?.pageTop ?? 0;\n left = visualViewport?.pageLeft ?? 0;\n }\n\n return {width, height, totalWidth, totalHeight, scroll, top, left};\n}\n\nfunction getScroll(node: Element): Offset {\n return {\n top: node.scrollTop,\n left: node.scrollLeft,\n width: node.scrollWidth,\n height: node.scrollHeight\n };\n}\n\n// Determines the amount of space required when moving the overlay to ensure it remains in the boundary\nfunction getDelta(\n axis: Axis,\n offset: number,\n size: number,\n // The dimensions of the boundary element that the popover is\n // positioned within (most of the time this is the ).\n boundaryDimensions: Dimensions,\n // The dimensions of the containing block element that the popover is\n // positioned relative to (e.g. parent with position: relative).\n // Usually this is the same as the boundary element, but if the popover\n // is portaled somewhere other than the body and has an ancestor with\n // position: relative/absolute, it will be different.\n containerDimensions: Dimensions,\n padding: number,\n containerOffsetWithBoundary: Offset\n) {\n let containerScroll = containerDimensions.scroll[axis] ?? 0;\n // The height/width of the boundary. Matches the axis along which we are adjusting the overlay position\n let boundarySize = boundaryDimensions[AXIS_SIZE[axis]];\n // Calculate the edges of the boundary (accomodating for the boundary padding) and the edges of the overlay.\n // Note that these values are with respect to the visual viewport (aka 0,0 is the top left of the viewport)\n let boundaryStartEdge = boundaryDimensions.scroll[AXIS[axis]] + padding;\n let boundaryEndEdge = boundarySize + boundaryDimensions.scroll[AXIS[axis]] - padding;\n let startEdgeOffset = offset - containerScroll + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];\n let endEdgeOffset = offset - containerScroll + size + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];\n\n // If any of the overlay edges falls outside of the boundary, shift the overlay the required amount to align one of the overlay's\n // edges with the closest boundary edge.\n if (startEdgeOffset < boundaryStartEdge) {\n return boundaryStartEdge - startEdgeOffset;\n } else if (endEdgeOffset > boundaryEndEdge) {\n return Math.max(boundaryEndEdge - endEdgeOffset, boundaryStartEdge - startEdgeOffset);\n } else {\n return 0;\n }\n}\n\nfunction getMargins(node: Element): Position {\n let style = window.getComputedStyle(node);\n return {\n top: parseInt(style.marginTop, 10) || 0,\n bottom: parseInt(style.marginBottom, 10) || 0,\n left: parseInt(style.marginLeft, 10) || 0,\n right: parseInt(style.marginRight, 10) || 0\n };\n}\n\nfunction parsePlacement(input: Placement): ParsedPlacement {\n if (PARSED_PLACEMENT_CACHE[input]) {\n return PARSED_PLACEMENT_CACHE[input];\n }\n\n let [placement, crossPlacement] = input.split(' ');\n let axis: Axis = AXIS[placement] || 'right';\n let crossAxis: Axis = CROSS_AXIS[axis];\n\n if (!AXIS[crossPlacement]) {\n crossPlacement = 'center';\n }\n\n let size = AXIS_SIZE[axis];\n let crossSize = AXIS_SIZE[crossAxis];\n PARSED_PLACEMENT_CACHE[input] = {placement, crossPlacement, axis, crossAxis, size, crossSize};\n return PARSED_PLACEMENT_CACHE[input];\n}\n\nfunction computePosition(\n childOffset: Offset,\n boundaryDimensions: Dimensions,\n overlaySize: Offset,\n placementInfo: ParsedPlacement,\n offset: number,\n crossOffset: number,\n containerOffsetWithBoundary: Offset,\n isContainerPositioned: boolean,\n arrowSize: number,\n arrowBoundaryOffset: number\n) {\n let {placement, crossPlacement, axis, crossAxis, size, crossSize} = placementInfo;\n let position: Position = {};\n\n // button position\n position[crossAxis] = childOffset[crossAxis] ?? 0;\n if (crossPlacement === 'center') {\n // + (button size / 2) - (overlay size / 2)\n // at this point the overlay center should match the button center\n position[crossAxis]! += ((childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0)) / 2;\n } else if (crossPlacement !== crossAxis) {\n // + (button size) - (overlay size)\n // at this point the overlay bottom should match the button bottom\n position[crossAxis]! += (childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0);\n }/* else {\n the overlay top should match the button top\n } */\n\n position[crossAxis]! += crossOffset;\n\n // overlay top overlapping arrow with button bottom\n const minPosition = childOffset[crossAxis] - overlaySize[crossSize] + arrowSize + arrowBoundaryOffset;\n // overlay bottom overlapping arrow with button top\n const maxPosition = childOffset[crossAxis] + childOffset[crossSize] - arrowSize - arrowBoundaryOffset;\n position[crossAxis] = clamp(position[crossAxis]!, minPosition, maxPosition);\n\n // Floor these so the position isn't placed on a partial pixel, only whole pixels. Shouldn't matter if it was floored or ceiled, so chose one.\n if (placement === axis) {\n // If the container is positioned (non-static), then we use the container's actual\n // height, as `bottom` will be relative to this height. But if the container is static,\n // then it can only be the `document.body`, and `bottom` will be relative to _its_\n // container, which should be as large as boundaryDimensions.\n const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary[size] : boundaryDimensions[TOTAL_SIZE[size]]);\n position[FLIPPED_DIRECTION[axis]] = Math.floor(containerHeight - childOffset[axis] + offset);\n } else {\n position[axis] = Math.floor(childOffset[axis] + childOffset[size] + offset);\n }\n return position;\n}\n\nfunction getMaxHeight(\n position: Position,\n boundaryDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n isContainerPositioned: boolean,\n margins: Position,\n padding: number,\n overlayHeight: number,\n heightGrowthDirection: HeightGrowthDirection\n) {\n const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary.height : boundaryDimensions[TOTAL_SIZE.height]);\n // For cases where position is set via \"bottom\" instead of \"top\", we need to calculate the true overlay top with respect to the boundary. Reverse calculate this with the same method\n // used in computePosition.\n let overlayTop = position.top != null ? containerOffsetWithBoundary.top + position.top : containerOffsetWithBoundary.top + (containerHeight - (position.bottom ?? 0) - overlayHeight);\n let maxHeight = heightGrowthDirection !== 'top' ?\n // We want the distance between the top of the overlay to the bottom of the boundary\n Math.max(0,\n (boundaryDimensions.height + boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the bottom of the boundary\n - overlayTop // this is the top of the overlay\n - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding\n )\n // We want the distance between the bottom of the overlay to the top of the boundary\n : Math.max(0,\n (overlayTop + overlayHeight) // this is the bottom of the overlay\n - (boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the top of the boundary\n - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding\n );\n return Math.min(boundaryDimensions.height - (padding * 2), maxHeight);\n}\n\nfunction getAvailableSpace(\n boundaryDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n childOffset: Offset,\n margins: Position,\n padding: number,\n placementInfo: ParsedPlacement\n) {\n let {placement, axis, size} = placementInfo;\n if (placement === axis) {\n return Math.max(0, childOffset[axis] - boundaryDimensions[axis] - (boundaryDimensions.scroll[axis] ?? 0) + containerOffsetWithBoundary[axis] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding);\n }\n\n return Math.max(0, boundaryDimensions[size] + boundaryDimensions[axis] + boundaryDimensions.scroll[axis] - containerOffsetWithBoundary[axis] - childOffset[axis] - childOffset[size] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding);\n}\n\nexport function calculatePositionInternal(\n placementInput: Placement,\n childOffset: Offset,\n overlaySize: Offset,\n scrollSize: Offset,\n margins: Position,\n padding: number,\n flip: boolean,\n boundaryDimensions: Dimensions,\n containerDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n offset: number,\n crossOffset: number,\n isContainerPositioned: boolean,\n userSetMaxHeight: number | undefined,\n arrowSize: number,\n arrowBoundaryOffset: number\n): PositionResult {\n let placementInfo = parsePlacement(placementInput);\n let {size, crossAxis, crossSize, placement, crossPlacement} = placementInfo;\n let position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n let normalizedOffset = offset;\n let space = getAvailableSpace(\n boundaryDimensions,\n containerOffsetWithBoundary,\n childOffset,\n margins,\n padding + offset,\n placementInfo\n );\n\n // Check if the scroll size of the overlay is greater than the available space to determine if we need to flip\n if (flip && scrollSize[size] > space) {\n let flippedPlacementInfo = parsePlacement(`${FLIPPED_DIRECTION[placement]} ${crossPlacement}` as Placement);\n let flippedPosition = computePosition(childOffset, boundaryDimensions, overlaySize, flippedPlacementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n let flippedSpace = getAvailableSpace(\n boundaryDimensions,\n containerOffsetWithBoundary,\n childOffset,\n margins,\n padding + offset,\n flippedPlacementInfo\n );\n\n // If the available space for the flipped position is greater than the original available space, flip.\n if (flippedSpace > space) {\n placementInfo = flippedPlacementInfo;\n position = flippedPosition;\n normalizedOffset = offset;\n }\n }\n\n // Determine the direction the height of the overlay can grow so that we can choose how to calculate the max height\n let heightGrowthDirection: HeightGrowthDirection = 'bottom';\n if (placementInfo.axis === 'top') {\n if (placementInfo.placement === 'top') {\n heightGrowthDirection = 'top';\n } else if (placementInfo.placement === 'bottom') {\n heightGrowthDirection = 'bottom';\n }\n } else if (placementInfo.crossAxis === 'top') {\n if (placementInfo.crossPlacement === 'top') {\n heightGrowthDirection = 'bottom';\n } else if (placementInfo.crossPlacement === 'bottom') {\n heightGrowthDirection = 'top';\n }\n }\n\n let delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary);\n position[crossAxis]! += delta;\n\n let maxHeight = getMaxHeight(\n position,\n boundaryDimensions,\n containerOffsetWithBoundary,\n isContainerPositioned,\n margins,\n padding,\n overlaySize.height,\n heightGrowthDirection\n );\n\n if (userSetMaxHeight && userSetMaxHeight < maxHeight) {\n maxHeight = userSetMaxHeight;\n }\n\n overlaySize.height = Math.min(overlaySize.height, maxHeight);\n\n position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, normalizedOffset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary);\n position[crossAxis]! += delta;\n\n let arrowPosition: Position = {};\n\n // All values are transformed so that 0 is at the top/left of the overlay depending on the orientation\n // Prefer the arrow being in the center of the trigger/overlay anchor element\n // childOffset[crossAxis] + .5 * childOffset[crossSize] = absolute position with respect to the trigger's coordinate system that would place the arrow in the center of the trigger\n // position[crossAxis] - margins[AXIS[crossAxis]] = value use to transform the position to a value with respect to the overlay's coordinate system. A child element's (aka arrow) position absolute's \"0\"\n // is positioned after the margin of its parent (aka overlay) so we need to subtract it to get the proper coordinate transform\n let preferredArrowPosition = childOffset[crossAxis] + .5 * childOffset[crossSize] - position[crossAxis]! - margins[AXIS[crossAxis]];\n\n // Min/Max position limits for the arrow with respect to the overlay\n const arrowMinPosition = arrowSize / 2 + arrowBoundaryOffset;\n // overlaySize[crossSize] - margins = true size of the overlay\n const overlayMargin = AXIS[crossAxis] === 'left' ? (margins.left ?? 0) + (margins.right ?? 0) : (margins.top ?? 0) + (margins.bottom ?? 0);\n const arrowMaxPosition = overlaySize[crossSize] - overlayMargin - (arrowSize / 2) - arrowBoundaryOffset;\n\n // Min/Max position limits for the arrow with respect to the trigger/overlay anchor element\n // Same margin accomodation done here as well as for the preferredArrowPosition\n const arrowOverlappingChildMinEdge = childOffset[crossAxis] + (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]);\n const arrowOverlappingChildMaxEdge = childOffset[crossAxis] + childOffset[crossSize] - (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]);\n\n // Clamp the arrow positioning so that it always is within the bounds of the anchor and the overlay\n const arrowPositionOverlappingChild = clamp(preferredArrowPosition, arrowOverlappingChildMinEdge, arrowOverlappingChildMaxEdge);\n arrowPosition[crossAxis] = clamp(arrowPositionOverlappingChild, arrowMinPosition, arrowMaxPosition);\n\n return {\n position,\n maxHeight: maxHeight,\n arrowOffsetLeft: arrowPosition.left,\n arrowOffsetTop: arrowPosition.top,\n placement: placementInfo.placement\n };\n}\n\n/**\n * Determines where to place the overlay with regards to the target and the position of an optional indicator.\n */\nexport function calculatePosition(opts: PositionOpts): PositionResult {\n let {\n placement,\n targetNode,\n overlayNode,\n scrollNode,\n padding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize = 0,\n arrowBoundaryOffset = 0\n } = opts;\n\n let container = overlayNode instanceof HTMLElement ? getContainingBlock(overlayNode) : document.documentElement;\n let isViewportContainer = container === document.documentElement;\n const containerPositionStyle = window.getComputedStyle(container).position;\n let isContainerPositioned = !!containerPositionStyle && containerPositionStyle !== 'static';\n let childOffset: Offset = isViewportContainer ? getOffset(targetNode) : getPosition(targetNode, container);\n\n if (!isViewportContainer) {\n let {marginTop, marginLeft} = window.getComputedStyle(targetNode);\n childOffset.top += parseInt(marginTop, 10) || 0;\n childOffset.left += parseInt(marginLeft, 10) || 0;\n }\n\n let overlaySize: Offset = getOffset(overlayNode);\n let margins = getMargins(overlayNode);\n overlaySize.width += (margins.left ?? 0) + (margins.right ?? 0);\n overlaySize.height += (margins.top ?? 0) + (margins.bottom ?? 0);\n\n let scrollSize = getScroll(scrollNode);\n let boundaryDimensions = getContainerDimensions(boundaryElement);\n let containerDimensions = getContainerDimensions(container);\n // If the container is the HTML element wrapping the body element, the retrieved scrollTop/scrollLeft will be equal to the\n // body element's scroll. Set the container's scroll values to 0 since the overlay's edge position value in getDelta don't then need to be further offset\n // by the container scroll since they are essentially the same containing element and thus in the same coordinate system\n let containerOffsetWithBoundary: Offset = boundaryElement.tagName === 'BODY' ? getOffset(container) : getPosition(container, boundaryElement);\n if (container.tagName === 'HTML' && boundaryElement.tagName === 'BODY') {\n containerDimensions.scroll.top = 0;\n containerDimensions.scroll.left = 0;\n }\n\n return calculatePositionInternal(\n placement,\n childOffset,\n overlaySize,\n scrollSize,\n margins,\n padding,\n shouldFlip,\n boundaryDimensions,\n containerDimensions,\n containerOffsetWithBoundary,\n offset,\n crossOffset,\n isContainerPositioned,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n );\n}\n\nfunction getOffset(node: Element): Offset {\n let {top, left, width, height} = node.getBoundingClientRect();\n let {scrollTop, scrollLeft, clientTop, clientLeft} = document.documentElement;\n return {\n top: top + scrollTop - clientTop,\n left: left + scrollLeft - clientLeft,\n width,\n height\n };\n}\n\nfunction getPosition(node: Element, parent: Element): Offset {\n let style = window.getComputedStyle(node);\n let offset: Offset;\n if (style.position === 'fixed') {\n let {top, left, width, height} = node.getBoundingClientRect();\n offset = {top, left, width, height};\n } else {\n offset = getOffset(node);\n let parentOffset = getOffset(parent);\n let parentStyle = window.getComputedStyle(parent);\n parentOffset.top += (parseInt(parentStyle.borderTopWidth, 10) || 0) - parent.scrollTop;\n parentOffset.left += (parseInt(parentStyle.borderLeftWidth, 10) || 0) - parent.scrollLeft;\n offset.top -= parentOffset.top;\n offset.left -= parentOffset.left;\n }\n\n offset.top -= parseInt(style.marginTop, 10) || 0;\n offset.left -= parseInt(style.marginLeft, 10) || 0;\n return offset;\n}\n\n// Returns the containing block of an element, which is the element that\n// this element will be positioned relative to.\n// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block\nfunction getContainingBlock(node: HTMLElement): Element {\n // The offsetParent of an element in most cases equals the containing block.\n // https://w3c.github.io/csswg-drafts/cssom-view/#dom-htmlelement-offsetparent\n let offsetParent = node.offsetParent;\n\n // The offsetParent algorithm terminates at the document body,\n // even if the body is not a containing block. Double check that\n // and use the documentElement if so.\n if (\n offsetParent &&\n offsetParent === document.body &&\n window.getComputedStyle(offsetParent).position === 'static' &&\n !isContainingBlock(offsetParent)\n ) {\n offsetParent = document.documentElement;\n }\n\n // TODO(later): handle table elements?\n\n // The offsetParent can be null if the element has position: fixed, or a few other cases.\n // We have to walk up the tree manually in this case because fixed positioned elements\n // are still positioned relative to their containing block, which is not always the viewport.\n if (offsetParent == null) {\n offsetParent = node.parentElement;\n while (offsetParent && !isContainingBlock(offsetParent)) {\n offsetParent = offsetParent.parentElement;\n }\n }\n\n // Fall back to the viewport.\n return offsetParent || document.documentElement;\n}\n\n// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\nfunction isContainingBlock(node: Element): boolean {\n let style = window.getComputedStyle(node);\n return (\n style.transform !== 'none' ||\n /transform|perspective/.test(style.willChange) ||\n style.filter !== 'none' ||\n style.contain === 'paint' ||\n ('backdropFilter' in style && style.backdropFilter !== 'none') ||\n ('WebkitBackdropFilter' in style && style.WebkitBackdropFilter !== 'none')\n );\n}\n"],"names":[],"version":3,"file":"calculatePosition.module.js.map"} +\ No newline at end of file ++{"mappings":"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","sources":["packages/@react-aria/overlays/src/calculatePosition.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {Axis, Placement, PlacementAxis, SizeAxis} from '@react-types/overlays';\nimport {clamp, isWebKit} from '@react-aria/utils';\n\ninterface Position {\n top?: number,\n left?: number,\n bottom?: number,\n right?: number\n}\n\ninterface Dimensions {\n width: number,\n height: number,\n totalWidth: number,\n totalHeight: number,\n top: number,\n left: number,\n scroll: Position\n}\n\ninterface ParsedPlacement {\n placement: PlacementAxis,\n crossPlacement: PlacementAxis,\n axis: Axis,\n crossAxis: Axis,\n size: SizeAxis,\n crossSize: SizeAxis\n}\n\ninterface Offset {\n top: number,\n left: number,\n width: number,\n height: number\n}\n\ninterface PositionOpts {\n arrowSize: number,\n placement: Placement,\n targetNode: Element,\n overlayNode: Element,\n scrollNode: Element,\n padding: number,\n shouldFlip: boolean,\n boundaryElement: Element,\n offset: number,\n crossOffset: number,\n maxHeight?: number,\n arrowBoundaryOffset?: number\n}\n\ntype HeightGrowthDirection = 'top' | 'bottom';\n\nexport interface PositionResult {\n position: Position,\n arrowOffsetLeft?: number,\n arrowOffsetTop?: number,\n maxHeight: number,\n placement: PlacementAxis\n}\n\nconst AXIS = {\n top: 'top',\n bottom: 'top',\n left: 'left',\n right: 'left'\n};\n\nconst FLIPPED_DIRECTION = {\n top: 'bottom',\n bottom: 'top',\n left: 'right',\n right: 'left'\n};\n\nconst CROSS_AXIS = {\n top: 'left',\n left: 'top'\n};\n\nconst AXIS_SIZE = {\n top: 'height',\n left: 'width'\n};\n\nconst TOTAL_SIZE = {\n width: 'totalWidth',\n height: 'totalHeight'\n};\n\nconst PARSED_PLACEMENT_CACHE = {};\n\nlet getWindow = (targetNode?: Element | null): Window => {\n return targetNode?.ownerDocument.defaultView || window;\n};\n\nlet getVisualViewport = (actualWindow: Window): VisualViewport | null => {\n return actualWindow.visualViewport || null;\n};\n\n\nfunction getDocumentElement(node: Element): Element {\n return node.ownerDocument.documentElement || document.documentElement;\n}\n\nfunction getContainerDimensions(containerNode: Element): Dimensions {\n const visualViewport = getVisualViewport(getWindow(containerNode));\n\n let width = 0, height = 0, totalWidth = 0, totalHeight = 0, top = 0, left = 0;\n let scroll: Position = {};\n let isPinchZoomedIn = (visualViewport?.scale ?? 1) > 1;\n\n\n if (containerNode.tagName === 'BODY') {\n let documentElement = getDocumentElement(containerNode);\n totalWidth = documentElement.clientWidth;\n totalHeight = documentElement.clientHeight;\n width = visualViewport?.width ?? totalWidth;\n height = visualViewport?.height ?? totalHeight;\n scroll.top = documentElement.scrollTop || containerNode.scrollTop;\n scroll.left = documentElement.scrollLeft || containerNode.scrollLeft;\n\n // The goal of the below is to get a top/left value that represents the top/left of the visual viewport with\n // respect to the layout viewport origin. This combined with the scrollTop/scrollLeft will allow us to calculate\n // coordinates/values with respect to the visual viewport or with respect to the layout viewport.\n if (visualViewport) {\n top = visualViewport.offsetTop;\n left = visualViewport.offsetLeft;\n }\n } else {\n ({width, height, top, left} = getOffset(containerNode));\n scroll.top = containerNode.scrollTop;\n scroll.left = containerNode.scrollLeft;\n totalWidth = width;\n totalHeight = height;\n }\n\n if (isWebKit() && (containerNode.tagName === 'BODY' || containerNode.tagName === 'HTML') && isPinchZoomedIn) {\n // Safari will report a non-zero scrollTop/Left for the non-scrolling body/HTML element when pinch zoomed in unlike other browsers.\n // Set to zero for parity calculations so we get consistent positioning of overlays across all browsers.\n // Also switch to visualViewport.pageTop/pageLeft so that we still accomodate for scroll positioning for body/HTML elements that are actually scrollable\n // before pinch zoom happens\n scroll.top = 0;\n scroll.left = 0;\n top = visualViewport?.pageTop ?? 0;\n left = visualViewport?.pageLeft ?? 0;\n }\n\n return {width, height, totalWidth, totalHeight, scroll, top, left};\n}\n\nfunction getScroll(node: Element): Offset {\n return {\n top: node.scrollTop,\n left: node.scrollLeft,\n width: node.scrollWidth,\n height: node.scrollHeight\n };\n}\n\n// Determines the amount of space required when moving the overlay to ensure it remains in the boundary\nfunction getDelta(\n axis: Axis,\n offset: number,\n size: number,\n // The dimensions of the boundary element that the popover is\n // positioned within (most of the time this is the ).\n boundaryDimensions: Dimensions,\n // The dimensions of the containing block element that the popover is\n // positioned relative to (e.g. parent with position: relative).\n // Usually this is the same as the boundary element, but if the popover\n // is portaled somewhere other than the body and has an ancestor with\n // position: relative/absolute, it will be different.\n containerDimensions: Dimensions,\n padding: number,\n containerOffsetWithBoundary: Offset\n) {\n let containerScroll = containerDimensions.scroll[axis] ?? 0;\n // The height/width of the boundary. Matches the axis along which we are adjusting the overlay position\n let boundarySize = boundaryDimensions[AXIS_SIZE[axis]];\n // Calculate the edges of the boundary (accomodating for the boundary padding) and the edges of the overlay.\n // Note that these values are with respect to the visual viewport (aka 0,0 is the top left of the viewport)\n let boundaryStartEdge = boundaryDimensions.scroll[AXIS[axis]] + padding;\n let boundaryEndEdge = boundarySize + boundaryDimensions.scroll[AXIS[axis]] - padding;\n let startEdgeOffset = offset - containerScroll + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];\n let endEdgeOffset = offset - containerScroll + size + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];\n\n // If any of the overlay edges falls outside of the boundary, shift the overlay the required amount to align one of the overlay's\n // edges with the closest boundary edge.\n if (startEdgeOffset < boundaryStartEdge) {\n return boundaryStartEdge - startEdgeOffset;\n } else if (endEdgeOffset > boundaryEndEdge) {\n return Math.max(boundaryEndEdge - endEdgeOffset, boundaryStartEdge - startEdgeOffset);\n } else {\n return 0;\n }\n}\n\nfunction getMargins(node: Element): Position {\n const actualWindow = getWindow(node);\n let style = actualWindow.getComputedStyle(node);\n return {\n top: parseInt(style.marginTop, 10) || 0,\n bottom: parseInt(style.marginBottom, 10) || 0,\n left: parseInt(style.marginLeft, 10) || 0,\n right: parseInt(style.marginRight, 10) || 0\n };\n}\n\nfunction parsePlacement(input: Placement): ParsedPlacement {\n if (PARSED_PLACEMENT_CACHE[input]) {\n return PARSED_PLACEMENT_CACHE[input];\n }\n\n let [placement, crossPlacement] = input.split(' ');\n let axis: Axis = AXIS[placement] || 'right';\n let crossAxis: Axis = CROSS_AXIS[axis];\n\n if (!AXIS[crossPlacement]) {\n crossPlacement = 'center';\n }\n\n let size = AXIS_SIZE[axis];\n let crossSize = AXIS_SIZE[crossAxis];\n PARSED_PLACEMENT_CACHE[input] = {placement, crossPlacement, axis, crossAxis, size, crossSize};\n return PARSED_PLACEMENT_CACHE[input];\n}\n\nfunction computePosition(\n childOffset: Offset,\n boundaryDimensions: Dimensions,\n overlaySize: Offset,\n placementInfo: ParsedPlacement,\n offset: number,\n crossOffset: number,\n containerOffsetWithBoundary: Offset,\n isContainerPositioned: boolean,\n arrowSize: number,\n arrowBoundaryOffset: number\n) {\n let {placement, crossPlacement, axis, crossAxis, size, crossSize} = placementInfo;\n let position: Position = {};\n\n // button position\n position[crossAxis] = childOffset[crossAxis] ?? 0;\n if (crossPlacement === 'center') {\n // + (button size / 2) - (overlay size / 2)\n // at this point the overlay center should match the button center\n position[crossAxis]! += ((childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0)) / 2;\n } else if (crossPlacement !== crossAxis) {\n // + (button size) - (overlay size)\n // at this point the overlay bottom should match the button bottom\n position[crossAxis]! += (childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0);\n }/* else {\n the overlay top should match the button top\n } */\n\n position[crossAxis]! += crossOffset;\n\n // overlay top overlapping arrow with button bottom\n const minPosition = childOffset[crossAxis] - overlaySize[crossSize] + arrowSize + arrowBoundaryOffset;\n // overlay bottom overlapping arrow with button top\n const maxPosition = childOffset[crossAxis] + childOffset[crossSize] - arrowSize - arrowBoundaryOffset;\n position[crossAxis] = clamp(position[crossAxis]!, minPosition, maxPosition);\n\n // Floor these so the position isn't placed on a partial pixel, only whole pixels. Shouldn't matter if it was floored or ceiled, so chose one.\n if (placement === axis) {\n // If the container is positioned (non-static), then we use the container's actual\n // height, as `bottom` will be relative to this height. But if the container is static,\n // then it can only be the `document.body`, and `bottom` will be relative to _its_\n // container, which should be as large as boundaryDimensions.\n const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary[size] : boundaryDimensions[TOTAL_SIZE[size]]);\n position[FLIPPED_DIRECTION[axis]] = Math.floor(containerHeight - childOffset[axis] + offset);\n } else {\n position[axis] = Math.floor(childOffset[axis] + childOffset[size] + offset);\n }\n return position;\n}\n\nfunction getMaxHeight(\n position: Position,\n boundaryDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n isContainerPositioned: boolean,\n margins: Position,\n padding: number,\n overlayHeight: number,\n heightGrowthDirection: HeightGrowthDirection\n) {\n const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary.height : boundaryDimensions[TOTAL_SIZE.height]);\n // For cases where position is set via \"bottom\" instead of \"top\", we need to calculate the true overlay top with respect to the boundary. Reverse calculate this with the same method\n // used in computePosition.\n let overlayTop = position.top != null ? containerOffsetWithBoundary.top + position.top : containerOffsetWithBoundary.top + (containerHeight - (position.bottom ?? 0) - overlayHeight);\n let maxHeight = heightGrowthDirection !== 'top' ?\n // We want the distance between the top of the overlay to the bottom of the boundary\n Math.max(0,\n (boundaryDimensions.height + boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the bottom of the boundary\n - overlayTop // this is the top of the overlay\n - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding\n )\n // We want the distance between the bottom of the overlay to the top of the boundary\n : Math.max(0,\n (overlayTop + overlayHeight) // this is the bottom of the overlay\n - (boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the top of the boundary\n - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding\n );\n return Math.min(boundaryDimensions.height - (padding * 2), maxHeight);\n}\n\nfunction getAvailableSpace(\n boundaryDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n childOffset: Offset,\n margins: Position,\n padding: number,\n placementInfo: ParsedPlacement\n) {\n let {placement, axis, size} = placementInfo;\n if (placement === axis) {\n return Math.max(0, childOffset[axis] - boundaryDimensions[axis] - (boundaryDimensions.scroll[axis] ?? 0) + containerOffsetWithBoundary[axis] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding);\n }\n\n return Math.max(0, boundaryDimensions[size] + boundaryDimensions[axis] + boundaryDimensions.scroll[axis] - containerOffsetWithBoundary[axis] - childOffset[axis] - childOffset[size] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding);\n}\n\nexport function calculatePositionInternal(\n placementInput: Placement,\n childOffset: Offset,\n overlaySize: Offset,\n scrollSize: Offset,\n margins: Position,\n padding: number,\n flip: boolean,\n boundaryDimensions: Dimensions,\n containerDimensions: Dimensions,\n containerOffsetWithBoundary: Offset,\n offset: number,\n crossOffset: number,\n isContainerPositioned: boolean,\n userSetMaxHeight: number | undefined,\n arrowSize: number,\n arrowBoundaryOffset: number\n): PositionResult {\n let placementInfo = parsePlacement(placementInput);\n let {size, crossAxis, crossSize, placement, crossPlacement} = placementInfo;\n let position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n let normalizedOffset = offset;\n let space = getAvailableSpace(\n boundaryDimensions,\n containerOffsetWithBoundary,\n childOffset,\n margins,\n padding + offset,\n placementInfo\n );\n\n // Check if the scroll size of the overlay is greater than the available space to determine if we need to flip\n if (flip && scrollSize[size] > space) {\n let flippedPlacementInfo = parsePlacement(`${FLIPPED_DIRECTION[placement]} ${crossPlacement}` as Placement);\n let flippedPosition = computePosition(childOffset, boundaryDimensions, overlaySize, flippedPlacementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n let flippedSpace = getAvailableSpace(\n boundaryDimensions,\n containerOffsetWithBoundary,\n childOffset,\n margins,\n padding + offset,\n flippedPlacementInfo\n );\n\n // If the available space for the flipped position is greater than the original available space, flip.\n if (flippedSpace > space) {\n placementInfo = flippedPlacementInfo;\n position = flippedPosition;\n normalizedOffset = offset;\n }\n }\n\n // Determine the direction the height of the overlay can grow so that we can choose how to calculate the max height\n let heightGrowthDirection: HeightGrowthDirection = 'bottom';\n if (placementInfo.axis === 'top') {\n if (placementInfo.placement === 'top') {\n heightGrowthDirection = 'top';\n } else if (placementInfo.placement === 'bottom') {\n heightGrowthDirection = 'bottom';\n }\n } else if (placementInfo.crossAxis === 'top') {\n if (placementInfo.crossPlacement === 'top') {\n heightGrowthDirection = 'bottom';\n } else if (placementInfo.crossPlacement === 'bottom') {\n heightGrowthDirection = 'top';\n }\n }\n\n let delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary);\n position[crossAxis]! += delta;\n\n let maxHeight = getMaxHeight(\n position,\n boundaryDimensions,\n containerOffsetWithBoundary,\n isContainerPositioned,\n margins,\n padding,\n overlaySize.height,\n heightGrowthDirection\n );\n\n if (userSetMaxHeight && userSetMaxHeight < maxHeight) {\n maxHeight = userSetMaxHeight;\n }\n\n overlaySize.height = Math.min(overlaySize.height, maxHeight);\n\n position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, normalizedOffset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);\n delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary);\n position[crossAxis]! += delta;\n\n let arrowPosition: Position = {};\n\n // All values are transformed so that 0 is at the top/left of the overlay depending on the orientation\n // Prefer the arrow being in the center of the trigger/overlay anchor element\n // childOffset[crossAxis] + .5 * childOffset[crossSize] = absolute position with respect to the trigger's coordinate system that would place the arrow in the center of the trigger\n // position[crossAxis] - margins[AXIS[crossAxis]] = value use to transform the position to a value with respect to the overlay's coordinate system. A child element's (aka arrow) position absolute's \"0\"\n // is positioned after the margin of its parent (aka overlay) so we need to subtract it to get the proper coordinate transform\n let preferredArrowPosition = childOffset[crossAxis] + .5 * childOffset[crossSize] - position[crossAxis]! - margins[AXIS[crossAxis]];\n\n // Min/Max position limits for the arrow with respect to the overlay\n const arrowMinPosition = arrowSize / 2 + arrowBoundaryOffset;\n // overlaySize[crossSize] - margins = true size of the overlay\n const overlayMargin = AXIS[crossAxis] === 'left' ? (margins.left ?? 0) + (margins.right ?? 0) : (margins.top ?? 0) + (margins.bottom ?? 0);\n const arrowMaxPosition = overlaySize[crossSize] - overlayMargin - (arrowSize / 2) - arrowBoundaryOffset;\n\n // Min/Max position limits for the arrow with respect to the trigger/overlay anchor element\n // Same margin accomodation done here as well as for the preferredArrowPosition\n const arrowOverlappingChildMinEdge = childOffset[crossAxis] + (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]);\n const arrowOverlappingChildMaxEdge = childOffset[crossAxis] + childOffset[crossSize] - (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]);\n\n // Clamp the arrow positioning so that it always is within the bounds of the anchor and the overlay\n const arrowPositionOverlappingChild = clamp(preferredArrowPosition, arrowOverlappingChildMinEdge, arrowOverlappingChildMaxEdge);\n arrowPosition[crossAxis] = clamp(arrowPositionOverlappingChild, arrowMinPosition, arrowMaxPosition);\n\n return {\n position,\n maxHeight: maxHeight,\n arrowOffsetLeft: arrowPosition.left,\n arrowOffsetTop: arrowPosition.top,\n placement: placementInfo.placement\n };\n}\n\n/**\n * Determines where to place the overlay with regards to the target and the position of an optional indicator.\n */\nexport function calculatePosition(opts: PositionOpts): PositionResult {\n let {\n placement,\n targetNode,\n overlayNode,\n scrollNode,\n padding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize = 0,\n arrowBoundaryOffset = 0\n } = opts;\n const actualWindow = getWindow(targetNode);\n\n let container = overlayNode instanceof HTMLElement ? getContainingBlock(overlayNode) : document.documentElement;\n let isViewportContainer = container === document.documentElement;\n const containerPositionStyle = actualWindow.getComputedStyle(container).position;\n let isContainerPositioned = !!containerPositionStyle && containerPositionStyle !== 'static';\n let childOffset: Offset = isViewportContainer ? getOffset(targetNode) : getPosition(targetNode, container);\n\n if (!isViewportContainer) {\n let {marginTop, marginLeft} = actualWindow.getComputedStyle(targetNode);\n childOffset.top += parseInt(marginTop, 10) || 0;\n childOffset.left += parseInt(marginLeft, 10) || 0;\n }\n\n let overlaySize: Offset = getOffset(overlayNode);\n let margins = getMargins(overlayNode);\n overlaySize.width += (margins.left ?? 0) + (margins.right ?? 0);\n overlaySize.height += (margins.top ?? 0) + (margins.bottom ?? 0);\n\n let scrollSize = getScroll(scrollNode);\n let boundaryDimensions = getContainerDimensions(boundaryElement);\n let containerDimensions = getContainerDimensions(container);\n // If the container is the HTML element wrapping the body element, the retrieved scrollTop/scrollLeft will be equal to the\n // body element's scroll. Set the container's scroll values to 0 since the overlay's edge position value in getDelta don't then need to be further offset\n // by the container scroll since they are essentially the same containing element and thus in the same coordinate system\n let containerOffsetWithBoundary: Offset = boundaryElement.tagName === 'BODY' ? getOffset(container) : getPosition(container, boundaryElement);\n if (container.tagName === 'HTML' && boundaryElement.tagName === 'BODY') {\n containerDimensions.scroll.top = 0;\n containerDimensions.scroll.left = 0;\n }\n\n return calculatePositionInternal(\n placement,\n childOffset,\n overlaySize,\n scrollSize,\n margins,\n padding,\n shouldFlip,\n boundaryDimensions,\n containerDimensions,\n containerOffsetWithBoundary,\n offset,\n crossOffset,\n isContainerPositioned,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n );\n}\n\nfunction getOffset(node: Element): Offset {\n let {top, left, width, height} = node.getBoundingClientRect();\n let {scrollTop, scrollLeft, clientTop, clientLeft} = getDocumentElement(node);\n return {\n top: top + scrollTop - clientTop,\n left: left + scrollLeft - clientLeft,\n width,\n height\n };\n}\n\nfunction getPosition(node: Element, parent: Element): Offset {\n const actualWindow = getWindow(node);\n\n let style = actualWindow.getComputedStyle(node);\n let offset: Offset;\n if (style.position === 'fixed') {\n let {top, left, width, height} = node.getBoundingClientRect();\n offset = {top, left, width, height};\n } else {\n offset = getOffset(node);\n let parentOffset = getOffset(parent);\n let parentStyle = actualWindow.getComputedStyle(parent);\n parentOffset.top += (parseInt(parentStyle.borderTopWidth, 10) || 0) - parent.scrollTop;\n parentOffset.left += (parseInt(parentStyle.borderLeftWidth, 10) || 0) - parent.scrollLeft;\n offset.top -= parentOffset.top;\n offset.left -= parentOffset.left;\n }\n\n offset.top -= parseInt(style.marginTop, 10) || 0;\n offset.left -= parseInt(style.marginLeft, 10) || 0;\n return offset;\n}\n\n// Returns the containing block of an element, which is the element that\n// this element will be positioned relative to.\n// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block\nfunction getContainingBlock(node: HTMLElement): Element {\n // The offsetParent of an element in most cases equals the containing block.\n // https://w3c.github.io/csswg-drafts/cssom-view/#dom-htmlelement-offsetparent\n let offsetParent = node.offsetParent;\n const actualWindow = getWindow(node);\n\n // The offsetParent algorithm terminates at the document body,\n // even if the body is not a containing block. Double check that\n // and use the documentElement if so.\n if (\n offsetParent &&\n offsetParent === (node.ownerDocument || document).body &&\n actualWindow.getComputedStyle(offsetParent).position === 'static' &&\n !isContainingBlock(offsetParent)\n ) {\n offsetParent = getDocumentElement(node);\n }\n\n // TODO(later): handle table elements?\n\n // The offsetParent can be null if the element has position: fixed, or a few other cases.\n // We have to walk up the tree manually in this case because fixed positioned elements\n // are still positioned relative to their containing block, which is not always the viewport.\n if (offsetParent == null) {\n offsetParent = node.parentElement;\n while (offsetParent && !isContainingBlock(offsetParent)) {\n offsetParent = offsetParent.parentElement;\n }\n }\n\n // Fall back to the viewport.\n return offsetParent || getDocumentElement(node);\n}\n\n// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\nfunction isContainingBlock(node: Element): boolean {\n const actualWindow = getWindow(node);\n let style = actualWindow.getComputedStyle(node);\n return (\n style.transform !== 'none' ||\n /transform|perspective/.test(style.willChange) ||\n style.filter !== 'none' ||\n style.contain === 'paint' ||\n ('backdropFilter' in style && style.backdropFilter !== 'none') ||\n ('WebkitBackdropFilter' in style && style.WebkitBackdropFilter !== 'none')\n );\n}\n"],"names":[],"version":3,"file":"calculatePosition.module.js.map"} +\ No newline at end of file +diff --git a/dist/useOverlayPosition.main.js b/dist/useOverlayPosition.main.js +index e87062a9b2838ef2e70b22daed6034a34532e804..be61fefe0644bf05f10cf1d0df85b1153680e4db 100644 +--- a/dist/useOverlayPosition.main.js ++++ b/dist/useOverlayPosition.main.js +@@ -25,12 +25,22 @@ $parcel$export(module.exports, "useOverlayPosition", () => $cd94b4896dd97759$exp + + + +-let $cd94b4896dd97759$var$visualViewport = typeof document !== 'undefined' ? window.visualViewport : null; ++let $cd94b4896dd97759$var$getWindowAndVisualViewport = (targetNode)=>{ ++ let actualWindow = (targetNode === null || targetNode === void 0 ? void 0 : targetNode.ownerDocument.defaultView) || window; ++ let visualViewport = (actualWindow === null || actualWindow === void 0 ? void 0 : actualWindow.visualViewport) || null; ++ return [ ++ actualWindow, ++ visualViewport ++ ]; ++}; + function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { + let { direction: direction } = (0, $6TXnl$reactariai18n.useLocale)(); + let { arrowSize: arrowSize = 0, targetRef: targetRef, overlayRef: overlayRef, scrollRef: scrollRef = overlayRef, placement: placement = 'bottom', containerPadding: containerPadding = 12, shouldFlip: shouldFlip = true, boundaryElement: boundaryElement = typeof document !== 'undefined' ? document.body : null, offset: offset = 0, crossOffset: crossOffset = 0, shouldUpdatePosition: shouldUpdatePosition = true, isOpen: isOpen = true, onClose: onClose, maxHeight: maxHeight, arrowBoundaryOffset: arrowBoundaryOffset = 0 } = props; + let [position, setPosition] = (0, $6TXnl$react.useState)(null); ++ let [actualWindow, visualViewport] = $cd94b4896dd97759$var$getWindowAndVisualViewport(targetRef.current); + let deps = [ ++ visualViewport, ++ actualWindow, + shouldUpdatePosition, + placement, + overlayRef.current, +@@ -50,23 +60,23 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { + // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might + // just be a non-realistic use case + // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles +- let lastScale = (0, $6TXnl$react.useRef)($cd94b4896dd97759$var$visualViewport === null || $cd94b4896dd97759$var$visualViewport === void 0 ? void 0 : $cd94b4896dd97759$var$visualViewport.scale); ++ let lastScale = (0, $6TXnl$react.useRef)(visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale); + (0, $6TXnl$react.useEffect)(()=>{ +- if (isOpen) lastScale.current = $cd94b4896dd97759$var$visualViewport === null || $cd94b4896dd97759$var$visualViewport === void 0 ? void 0 : $cd94b4896dd97759$var$visualViewport.scale; ++ if (isOpen) lastScale.current = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale; + }, [ + isOpen + ]); + let updatePosition = (0, $6TXnl$react.useCallback)(()=>{ + if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !boundaryElement) return; +- if (($cd94b4896dd97759$var$visualViewport === null || $cd94b4896dd97759$var$visualViewport === void 0 ? void 0 : $cd94b4896dd97759$var$visualViewport.scale) !== lastScale.current) return; ++ if ((visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale) !== lastScale.current) return; + // Determine a scroll anchor based on the focused element. + // This stores the offset of the anchor element from the scroll container + // so it can be restored after repositioning. This way if the overlay height + // changes, the focused element appears to stay in the same position. + let anchor = null; +- if (scrollRef.current && scrollRef.current.contains(document.activeElement)) { +- var _document_activeElement; +- let anchorRect = (_document_activeElement = document.activeElement) === null || _document_activeElement === void 0 ? void 0 : _document_activeElement.getBoundingClientRect(); ++ if (scrollRef.current && scrollRef.current.contains(actualWindow.document.activeElement)) { ++ var _actualWindow_document_activeElement; ++ let anchorRect = (_actualWindow_document_activeElement = actualWindow.document.activeElement) === null || _actualWindow_document_activeElement === void 0 ? void 0 : _actualWindow_document_activeElement.getBoundingClientRect(); + let scrollRect = scrollRef.current.getBoundingClientRect(); + var _anchorRect_top; + // Anchor from the top if the offset is in the top half of the scrollable element, +@@ -85,11 +95,10 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { + // RAC collections populating after a second render and properly set a correct max height + positioning when it populates. + let overlay = overlayRef.current; + if (!maxHeight && overlayRef.current) { +- var _window_visualViewport; + overlay.style.top = '0px'; + overlay.style.bottom = ''; +- var _window_visualViewport_height; +- overlay.style.maxHeight = ((_window_visualViewport_height = (_window_visualViewport = window.visualViewport) === null || _window_visualViewport === void 0 ? void 0 : _window_visualViewport.height) !== null && _window_visualViewport_height !== void 0 ? _window_visualViewport_height : window.innerHeight) + 'px'; ++ var _visualViewport_height; ++ overlay.style.maxHeight = ((_visualViewport_height = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.height) !== null && _visualViewport_height !== void 0 ? _visualViewport_height : actualWindow.innerHeight) + 'px'; + } + let position = (0, $5935ba4d7da2c103$exports.calculatePosition)({ + placement: $cd94b4896dd97759$var$translateRTL(placement, direction), +@@ -115,8 +124,8 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { + Object.keys(position.position).forEach((key)=>overlay.style[key] = position.position[key] + 'px'); + overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : ''; + // Restore scroll position relative to anchor element. +- if (anchor && document.activeElement && scrollRef.current) { +- let anchorRect = document.activeElement.getBoundingClientRect(); ++ if (anchor && actualWindow.document.activeElement && scrollRef.current) { ++ let anchorRect = actualWindow.document.activeElement.getBoundingClientRect(); + let scrollRect = scrollRef.current.getBoundingClientRect(); + let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type]; + scrollRef.current.scrollTop += newOffset - anchor.offset; +@@ -129,7 +138,7 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { + // eslint-disable-next-line react-hooks/exhaustive-deps + (0, $6TXnl$reactariautils.useLayoutEffect)(updatePosition, deps); + // Update position on window resize +- $cd94b4896dd97759$var$useResize(updatePosition); ++ $cd94b4896dd97759$var$useResize(updatePosition, actualWindow); + // Update position when the overlay changes size (might need to flip). + (0, $6TXnl$reactariautils.useResizeObserver)({ + ref: overlayRef, +@@ -158,14 +167,15 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { + let onScroll = ()=>{ + if (isResizing.current) onResize(); + }; +- $cd94b4896dd97759$var$visualViewport === null || $cd94b4896dd97759$var$visualViewport === void 0 ? void 0 : $cd94b4896dd97759$var$visualViewport.addEventListener('resize', onResize); +- $cd94b4896dd97759$var$visualViewport === null || $cd94b4896dd97759$var$visualViewport === void 0 ? void 0 : $cd94b4896dd97759$var$visualViewport.addEventListener('scroll', onScroll); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.addEventListener('resize', onResize); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.addEventListener('scroll', onScroll); + return ()=>{ +- $cd94b4896dd97759$var$visualViewport === null || $cd94b4896dd97759$var$visualViewport === void 0 ? void 0 : $cd94b4896dd97759$var$visualViewport.removeEventListener('resize', onResize); +- $cd94b4896dd97759$var$visualViewport === null || $cd94b4896dd97759$var$visualViewport === void 0 ? void 0 : $cd94b4896dd97759$var$visualViewport.removeEventListener('scroll', onScroll); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.removeEventListener('resize', onResize); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.removeEventListener('scroll', onScroll); + }; + }, [ +- updatePosition ++ updatePosition, ++ visualViewport + ]); + let close = (0, $6TXnl$react.useCallback)(()=>{ + if (!isResizing.current) onClose === null || onClose === void 0 ? void 0 : onClose(); +@@ -202,14 +212,15 @@ function $cd94b4896dd97759$export$d39e1813b3bdd0e1(props) { + updatePosition: updatePosition + }; + } +-function $cd94b4896dd97759$var$useResize(onResize) { ++function $cd94b4896dd97759$var$useResize(onResize, actualWindow = window) { + (0, $6TXnl$reactariautils.useLayoutEffect)(()=>{ +- window.addEventListener('resize', onResize, false); ++ actualWindow.addEventListener('resize', onResize, false); + return ()=>{ +- window.removeEventListener('resize', onResize, false); ++ actualWindow.removeEventListener('resize', onResize, false); + }; + }, [ +- onResize ++ onResize, ++ actualWindow + ]); + } + function $cd94b4896dd97759$var$translateRTL(position, direction) { +diff --git a/dist/useOverlayPosition.main.js.map b/dist/useOverlayPosition.main.js.map +index 05aa0e8aaf07ec8b4946a29a6276ac77519813c0..f7954c5d3a07ea0e0e3a65ab3642dc58af0a8eb7 100644 +--- a/dist/useOverlayPosition.main.js.map ++++ b/dist/useOverlayPosition.main.js.map +@@ -1 +1 @@ +-{"mappings":"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","sources":["packages/@react-aria/overlays/src/useOverlayPosition.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {calculatePosition, PositionResult} from './calculatePosition';\nimport {DOMAttributes, RefObject} from '@react-types/shared';\nimport {Placement, PlacementAxis, PositionProps} from '@react-types/overlays';\nimport {useCallback, useEffect, useRef, useState} from 'react';\nimport {useCloseOnScroll} from './useCloseOnScroll';\nimport {useLayoutEffect, useResizeObserver} from '@react-aria/utils';\nimport {useLocale} from '@react-aria/i18n';\n\nexport interface AriaPositionProps extends PositionProps {\n /**\n * Cross size of the overlay arrow in pixels.\n * @default 0\n */\n arrowSize?: number,\n /**\n * Element that that serves as the positioning boundary.\n * @default document.body\n */\n boundaryElement?: Element,\n /**\n * The ref for the element which the overlay positions itself with respect to.\n */\n targetRef: RefObject,\n /**\n * The ref for the overlay element.\n */\n overlayRef: RefObject,\n /**\n * A ref for the scrollable region within the overlay.\n * @default overlayRef\n */\n scrollRef?: RefObject,\n /**\n * Whether the overlay should update its position automatically.\n * @default true\n */\n shouldUpdatePosition?: boolean,\n /** Handler that is called when the overlay should close. */\n onClose?: (() => void) | null,\n /**\n * The maxHeight specified for the overlay element.\n * By default, it will take all space up to the current viewport height.\n */\n maxHeight?: number,\n /**\n * The minimum distance the arrow's edge should be from the edge of the overlay element.\n * @default 0\n */\n arrowBoundaryOffset?: number\n}\n\nexport interface PositionAria {\n /** Props for the overlay container element. */\n overlayProps: DOMAttributes,\n /** Props for the overlay tip arrow if any. */\n arrowProps: DOMAttributes,\n /** Placement of the overlay with respect to the overlay trigger. */\n placement: PlacementAxis | null,\n /** Updates the position of the overlay. */\n updatePosition(): void\n}\n\ninterface ScrollAnchor {\n type: 'top' | 'bottom',\n offset: number\n}\n\nlet visualViewport = typeof document !== 'undefined' ? window.visualViewport : null;\n\n/**\n * Handles positioning overlays like popovers and menus relative to a trigger\n * element, and updating the position when the window resizes.\n */\nexport function useOverlayPosition(props: AriaPositionProps): PositionAria {\n let {direction} = useLocale();\n let {\n arrowSize = 0,\n targetRef,\n overlayRef,\n scrollRef = overlayRef,\n placement = 'bottom' as Placement,\n containerPadding = 12,\n shouldFlip = true,\n boundaryElement = typeof document !== 'undefined' ? document.body : null,\n offset = 0,\n crossOffset = 0,\n shouldUpdatePosition = true,\n isOpen = true,\n onClose,\n maxHeight,\n arrowBoundaryOffset = 0\n } = props;\n let [position, setPosition] = useState(null);\n\n let deps = [\n shouldUpdatePosition,\n placement,\n overlayRef.current,\n targetRef.current,\n scrollRef.current,\n containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n isOpen,\n direction,\n maxHeight,\n arrowBoundaryOffset,\n arrowSize\n ];\n\n // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might\n // just be a non-realistic use case\n // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles\n let lastScale = useRef(visualViewport?.scale);\n useEffect(() => {\n if (isOpen) {\n lastScale.current = visualViewport?.scale;\n }\n }, [isOpen]);\n\n let updatePosition = useCallback(() => {\n if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !boundaryElement) {\n return;\n }\n\n if (visualViewport?.scale !== lastScale.current) {\n return;\n }\n\n // Determine a scroll anchor based on the focused element.\n // This stores the offset of the anchor element from the scroll container\n // so it can be restored after repositioning. This way if the overlay height\n // changes, the focused element appears to stay in the same position.\n let anchor: ScrollAnchor | null = null;\n if (scrollRef.current && scrollRef.current.contains(document.activeElement)) {\n let anchorRect = document.activeElement?.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n // Anchor from the top if the offset is in the top half of the scrollable element,\n // otherwise anchor from the bottom.\n anchor = {\n type: 'top',\n offset: (anchorRect?.top ?? 0) - scrollRect.top\n };\n if (anchor.offset > scrollRect.height / 2) {\n anchor.type = 'bottom';\n anchor.offset = (anchorRect?.bottom ?? 0) - scrollRect.bottom;\n }\n }\n\n // Always reset the overlay's previous max height if not defined by the user so that we can compensate for\n // RAC collections populating after a second render and properly set a correct max height + positioning when it populates.\n let overlay = (overlayRef.current as HTMLElement);\n if (!maxHeight && overlayRef.current) {\n overlay.style.top = '0px';\n overlay.style.bottom = '';\n overlay.style.maxHeight = (window.visualViewport?.height ?? window.innerHeight) + 'px';\n }\n\n let position = calculatePosition({\n placement: translateRTL(placement, direction),\n overlayNode: overlayRef.current,\n targetNode: targetRef.current,\n scrollNode: scrollRef.current || overlayRef.current,\n padding: containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n });\n\n if (!position.position) {\n return;\n }\n\n // Modify overlay styles directly so positioning happens immediately without the need of a second render\n // This is so we don't have to delay autoFocus scrolling or delay applying preventScroll for popovers\n overlay.style.top = '';\n overlay.style.bottom = '';\n overlay.style.left = '';\n overlay.style.right = '';\n\n Object.keys(position.position).forEach(key => overlay.style[key] = (position.position!)[key] + 'px');\n overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : '';\n\n // Restore scroll position relative to anchor element.\n if (anchor && document.activeElement && scrollRef.current) {\n let anchorRect = document.activeElement.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type];\n scrollRef.current.scrollTop += newOffset - anchor.offset;\n }\n\n // Trigger a set state for a second render anyway for arrow positioning\n setPosition(position);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n // Update position when anything changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(updatePosition, deps);\n\n // Update position on window resize\n useResize(updatePosition);\n\n // Update position when the overlay changes size (might need to flip).\n useResizeObserver({\n ref: overlayRef,\n onResize: updatePosition\n });\n\n // Update position when the target changes size (might need to flip).\n useResizeObserver({\n ref: targetRef,\n onResize: updatePosition\n });\n\n // Reposition the overlay and do not close on scroll while the visual viewport is resizing.\n // This will ensure that overlays adjust their positioning when the iOS virtual keyboard appears.\n let isResizing = useRef(false);\n useLayoutEffect(() => {\n let timeout: ReturnType;\n let onResize = () => {\n isResizing.current = true;\n clearTimeout(timeout);\n\n timeout = setTimeout(() => {\n isResizing.current = false;\n }, 500);\n\n updatePosition();\n };\n\n // Only reposition the overlay if a scroll event happens immediately as a result of resize (aka the virtual keyboard has appears)\n // We don't want to reposition the overlay if the user has pinch zoomed in and is scrolling the viewport around.\n let onScroll = () => {\n if (isResizing.current) {\n onResize();\n }\n };\n\n visualViewport?.addEventListener('resize', onResize);\n visualViewport?.addEventListener('scroll', onScroll);\n return () => {\n visualViewport?.removeEventListener('resize', onResize);\n visualViewport?.removeEventListener('scroll', onScroll);\n };\n }, [updatePosition]);\n\n let close = useCallback(() => {\n if (!isResizing.current) {\n onClose?.();\n }\n }, [onClose, isResizing]);\n\n // When scrolling a parent scrollable region of the trigger (other than the body),\n // we hide the popover. Otherwise, its position would be incorrect.\n useCloseOnScroll({\n triggerRef: targetRef,\n isOpen,\n onClose: onClose && close\n });\n\n return {\n overlayProps: {\n style: {\n position: 'absolute',\n zIndex: 100000, // should match the z-index in ModalTrigger\n ...position?.position,\n maxHeight: position?.maxHeight ?? '100vh'\n }\n },\n placement: position?.placement ?? null,\n arrowProps: {\n 'aria-hidden': 'true',\n role: 'presentation',\n style: {\n left: position?.arrowOffsetLeft,\n top: position?.arrowOffsetTop\n }\n },\n updatePosition\n };\n}\n\nfunction useResize(onResize) {\n useLayoutEffect(() => {\n window.addEventListener('resize', onResize, false);\n return () => {\n window.removeEventListener('resize', onResize, false);\n };\n }, [onResize]);\n}\n\nfunction translateRTL(position, direction) {\n if (direction === 'rtl') {\n return position.replace('start', 'right').replace('end', 'left');\n }\n return position.replace('start', 'left').replace('end', 'right');\n}\n"],"names":[],"version":3,"file":"useOverlayPosition.main.js.map"} +\ No newline at end of file ++{"mappings":"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","sources":["packages/@react-aria/overlays/src/useOverlayPosition.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {calculatePosition, PositionResult} from './calculatePosition';\nimport {DOMAttributes, RefObject} from '@react-types/shared';\nimport {Placement, PlacementAxis, PositionProps} from '@react-types/overlays';\nimport {useCallback, useEffect, useRef, useState} from 'react';\nimport {useCloseOnScroll} from './useCloseOnScroll';\nimport {useLayoutEffect, useResizeObserver} from '@react-aria/utils';\nimport {useLocale} from '@react-aria/i18n';\n\nexport interface AriaPositionProps extends PositionProps {\n /**\n * Cross size of the overlay arrow in pixels.\n * @default 0\n */\n arrowSize?: number,\n /**\n * Element that that serves as the positioning boundary.\n * @default document.body\n */\n boundaryElement?: Element,\n /**\n * The ref for the element which the overlay positions itself with respect to.\n */\n targetRef: RefObject,\n /**\n * The ref for the overlay element.\n */\n overlayRef: RefObject,\n /**\n * A ref for the scrollable region within the overlay.\n * @default overlayRef\n */\n scrollRef?: RefObject,\n /**\n * Whether the overlay should update its position automatically.\n * @default true\n */\n shouldUpdatePosition?: boolean,\n /** Handler that is called when the overlay should close. */\n onClose?: (() => void) | null,\n /**\n * The maxHeight specified for the overlay element.\n * By default, it will take all space up to the current viewport height.\n */\n maxHeight?: number,\n /**\n * The minimum distance the arrow's edge should be from the edge of the overlay element.\n * @default 0\n */\n arrowBoundaryOffset?: number\n}\n\nexport interface PositionAria {\n /** Props for the overlay container element. */\n overlayProps: DOMAttributes,\n /** Props for the overlay tip arrow if any. */\n arrowProps: DOMAttributes,\n /** Placement of the overlay with respect to the overlay trigger. */\n placement: PlacementAxis | null,\n /** Updates the position of the overlay. */\n updatePosition(): void\n}\n\ninterface ScrollAnchor {\n type: 'top' | 'bottom',\n offset: number\n}\n\nlet getWindowAndVisualViewport = (targetNode?: Element | null): [Window, VisualViewport | null] => {\n let actualWindow = targetNode?.ownerDocument.defaultView || window;\n let visualViewport = actualWindow?.visualViewport || null;\n return [actualWindow, visualViewport];\n};\n\n\n/**\n * Handles positioning overlays like popovers and menus relative to a trigger\n * element, and updating the position when the window resizes.\n */\nexport function useOverlayPosition(props: AriaPositionProps): PositionAria {\n let {direction} = useLocale();\n let {\n arrowSize = 0,\n targetRef,\n overlayRef,\n scrollRef = overlayRef,\n placement = 'bottom' as Placement,\n containerPadding = 12,\n shouldFlip = true,\n boundaryElement = typeof document !== 'undefined' ? document.body : null,\n offset = 0,\n crossOffset = 0,\n shouldUpdatePosition = true,\n isOpen = true,\n onClose,\n maxHeight,\n arrowBoundaryOffset = 0\n } = props;\n let [position, setPosition] = useState(null);\n let [actualWindow, visualViewport] = getWindowAndVisualViewport(targetRef.current);\n let deps = [\n visualViewport,\n actualWindow,\n shouldUpdatePosition,\n placement,\n overlayRef.current,\n targetRef.current,\n scrollRef.current,\n containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n isOpen,\n direction,\n maxHeight,\n arrowBoundaryOffset,\n arrowSize\n ];\n\n // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might\n // just be a non-realistic use case\n // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles\n let lastScale = useRef(visualViewport?.scale);\n useEffect(() => {\n if (isOpen) {\n lastScale.current = visualViewport?.scale;\n }\n }, [isOpen]);\n\n let updatePosition = useCallback(() => {\n if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !boundaryElement) {\n return;\n }\n\n if (visualViewport?.scale !== lastScale.current) {\n return;\n }\n\n // Determine a scroll anchor based on the focused element.\n // This stores the offset of the anchor element from the scroll container\n // so it can be restored after repositioning. This way if the overlay height\n // changes, the focused element appears to stay in the same position.\n let anchor: ScrollAnchor | null = null;\n if (scrollRef.current && scrollRef.current.contains(actualWindow.document.activeElement)) {\n let anchorRect = actualWindow.document.activeElement?.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n // Anchor from the top if the offset is in the top half of the scrollable element,\n // otherwise anchor from the bottom.\n anchor = {\n type: 'top',\n offset: (anchorRect?.top ?? 0) - scrollRect.top\n };\n if (anchor.offset > scrollRect.height / 2) {\n anchor.type = 'bottom';\n anchor.offset = (anchorRect?.bottom ?? 0) - scrollRect.bottom;\n }\n }\n\n // Always reset the overlay's previous max height if not defined by the user so that we can compensate for\n // RAC collections populating after a second render and properly set a correct max height + positioning when it populates.\n let overlay = (overlayRef.current as HTMLElement);\n if (!maxHeight && overlayRef.current) {\n overlay.style.top = '0px';\n overlay.style.bottom = '';\n overlay.style.maxHeight = (visualViewport?.height ?? actualWindow.innerHeight) + 'px';\n }\n\n let position = calculatePosition({\n placement: translateRTL(placement, direction),\n overlayNode: overlayRef.current,\n targetNode: targetRef.current,\n scrollNode: scrollRef.current || overlayRef.current,\n padding: containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n });\n\n if (!position.position) {\n return;\n }\n\n // Modify overlay styles directly so positioning happens immediately without the need of a second render\n // This is so we don't have to delay autoFocus scrolling or delay applying preventScroll for popovers\n overlay.style.top = '';\n overlay.style.bottom = '';\n overlay.style.left = '';\n overlay.style.right = '';\n\n Object.keys(position.position).forEach(key => overlay.style[key] = (position.position!)[key] + 'px');\n overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : '';\n\n // Restore scroll position relative to anchor element.\n if (anchor && actualWindow.document.activeElement && scrollRef.current) {\n let anchorRect = actualWindow.document.activeElement.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type];\n scrollRef.current.scrollTop += newOffset - anchor.offset;\n }\n\n // Trigger a set state for a second render anyway for arrow positioning\n setPosition(position);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n // Update position when anything changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(updatePosition, deps);\n\n // Update position on window resize\n useResize(updatePosition, actualWindow);\n\n // Update position when the overlay changes size (might need to flip).\n useResizeObserver({\n ref: overlayRef,\n onResize: updatePosition\n });\n\n // Update position when the target changes size (might need to flip).\n useResizeObserver({\n ref: targetRef,\n onResize: updatePosition\n });\n\n // Reposition the overlay and do not close on scroll while the visual viewport is resizing.\n // This will ensure that overlays adjust their positioning when the iOS virtual keyboard appears.\n let isResizing = useRef(false);\n useLayoutEffect(() => {\n let timeout: ReturnType;\n let onResize = () => {\n isResizing.current = true;\n clearTimeout(timeout);\n\n timeout = setTimeout(() => {\n isResizing.current = false;\n }, 500);\n\n updatePosition();\n };\n\n // Only reposition the overlay if a scroll event happens immediately as a result of resize (aka the virtual keyboard has appears)\n // We don't want to reposition the overlay if the user has pinch zoomed in and is scrolling the viewport around.\n let onScroll = () => {\n if (isResizing.current) {\n onResize();\n }\n };\n\n visualViewport?.addEventListener('resize', onResize);\n visualViewport?.addEventListener('scroll', onScroll);\n return () => {\n visualViewport?.removeEventListener('resize', onResize);\n visualViewport?.removeEventListener('scroll', onScroll);\n };\n }, [updatePosition, visualViewport]);\n\n let close = useCallback(() => {\n if (!isResizing.current) {\n onClose?.();\n }\n }, [onClose, isResizing]);\n\n // When scrolling a parent scrollable region of the trigger (other than the body),\n // we hide the popover. Otherwise, its position would be incorrect.\n useCloseOnScroll({\n triggerRef: targetRef,\n isOpen,\n onClose: onClose && close\n });\n\n return {\n overlayProps: {\n style: {\n position: 'absolute',\n zIndex: 100000, // should match the z-index in ModalTrigger\n ...position?.position,\n maxHeight: position?.maxHeight ?? '100vh'\n }\n },\n placement: position?.placement ?? null,\n arrowProps: {\n 'aria-hidden': 'true',\n role: 'presentation',\n style: {\n left: position?.arrowOffsetLeft,\n top: position?.arrowOffsetTop\n }\n },\n updatePosition\n };\n}\n\nfunction useResize(onResize, actualWindow: Window = window) {\n useLayoutEffect(() => {\n actualWindow.addEventListener('resize', onResize, false);\n return () => {\n actualWindow.removeEventListener('resize', onResize, false);\n };\n }, [onResize, actualWindow]);\n}\n\nfunction translateRTL(position, direction) {\n if (direction === 'rtl') {\n return position.replace('start', 'right').replace('end', 'left');\n }\n return position.replace('start', 'left').replace('end', 'right');\n}\n"],"names":[],"version":3,"file":"useOverlayPosition.main.js.map"} +\ No newline at end of file +diff --git a/dist/useOverlayPosition.mjs b/dist/useOverlayPosition.mjs +index 1a1371765ece0839f2ddfdcac60731e2c904e93f..e0b8530ae2a4fe8755e4a9160c79237828e18dae 100644 +--- a/dist/useOverlayPosition.mjs ++++ b/dist/useOverlayPosition.mjs +@@ -19,12 +19,22 @@ import {useLocale as $39EOa$useLocale} from "@react-aria/i18n"; + + + +-let $2a41e45df1593e64$var$visualViewport = typeof document !== 'undefined' ? window.visualViewport : null; ++let $2a41e45df1593e64$var$getWindowAndVisualViewport = (targetNode)=>{ ++ let actualWindow = (targetNode === null || targetNode === void 0 ? void 0 : targetNode.ownerDocument.defaultView) || window; ++ let visualViewport = (actualWindow === null || actualWindow === void 0 ? void 0 : actualWindow.visualViewport) || null; ++ return [ ++ actualWindow, ++ visualViewport ++ ]; ++}; + function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + let { direction: direction } = (0, $39EOa$useLocale)(); + let { arrowSize: arrowSize = 0, targetRef: targetRef, overlayRef: overlayRef, scrollRef: scrollRef = overlayRef, placement: placement = 'bottom', containerPadding: containerPadding = 12, shouldFlip: shouldFlip = true, boundaryElement: boundaryElement = typeof document !== 'undefined' ? document.body : null, offset: offset = 0, crossOffset: crossOffset = 0, shouldUpdatePosition: shouldUpdatePosition = true, isOpen: isOpen = true, onClose: onClose, maxHeight: maxHeight, arrowBoundaryOffset: arrowBoundaryOffset = 0 } = props; + let [position, setPosition] = (0, $39EOa$useState)(null); ++ let [actualWindow, visualViewport] = $2a41e45df1593e64$var$getWindowAndVisualViewport(targetRef.current); + let deps = [ ++ visualViewport, ++ actualWindow, + shouldUpdatePosition, + placement, + overlayRef.current, +@@ -44,23 +54,23 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might + // just be a non-realistic use case + // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles +- let lastScale = (0, $39EOa$useRef)($2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.scale); ++ let lastScale = (0, $39EOa$useRef)(visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale); + (0, $39EOa$useEffect)(()=>{ +- if (isOpen) lastScale.current = $2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.scale; ++ if (isOpen) lastScale.current = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale; + }, [ + isOpen + ]); + let updatePosition = (0, $39EOa$useCallback)(()=>{ + if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !boundaryElement) return; +- if (($2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.scale) !== lastScale.current) return; ++ if ((visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale) !== lastScale.current) return; + // Determine a scroll anchor based on the focused element. + // This stores the offset of the anchor element from the scroll container + // so it can be restored after repositioning. This way if the overlay height + // changes, the focused element appears to stay in the same position. + let anchor = null; +- if (scrollRef.current && scrollRef.current.contains(document.activeElement)) { +- var _document_activeElement; +- let anchorRect = (_document_activeElement = document.activeElement) === null || _document_activeElement === void 0 ? void 0 : _document_activeElement.getBoundingClientRect(); ++ if (scrollRef.current && scrollRef.current.contains(actualWindow.document.activeElement)) { ++ var _actualWindow_document_activeElement; ++ let anchorRect = (_actualWindow_document_activeElement = actualWindow.document.activeElement) === null || _actualWindow_document_activeElement === void 0 ? void 0 : _actualWindow_document_activeElement.getBoundingClientRect(); + let scrollRect = scrollRef.current.getBoundingClientRect(); + var _anchorRect_top; + // Anchor from the top if the offset is in the top half of the scrollable element, +@@ -79,11 +89,10 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + // RAC collections populating after a second render and properly set a correct max height + positioning when it populates. + let overlay = overlayRef.current; + if (!maxHeight && overlayRef.current) { +- var _window_visualViewport; + overlay.style.top = '0px'; + overlay.style.bottom = ''; +- var _window_visualViewport_height; +- overlay.style.maxHeight = ((_window_visualViewport_height = (_window_visualViewport = window.visualViewport) === null || _window_visualViewport === void 0 ? void 0 : _window_visualViewport.height) !== null && _window_visualViewport_height !== void 0 ? _window_visualViewport_height : window.innerHeight) + 'px'; ++ var _visualViewport_height; ++ overlay.style.maxHeight = ((_visualViewport_height = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.height) !== null && _visualViewport_height !== void 0 ? _visualViewport_height : actualWindow.innerHeight) + 'px'; + } + let position = (0, $edcf132a9284368a$export$b3ceb0cbf1056d98)({ + placement: $2a41e45df1593e64$var$translateRTL(placement, direction), +@@ -109,8 +118,8 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + Object.keys(position.position).forEach((key)=>overlay.style[key] = position.position[key] + 'px'); + overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : ''; + // Restore scroll position relative to anchor element. +- if (anchor && document.activeElement && scrollRef.current) { +- let anchorRect = document.activeElement.getBoundingClientRect(); ++ if (anchor && actualWindow.document.activeElement && scrollRef.current) { ++ let anchorRect = actualWindow.document.activeElement.getBoundingClientRect(); + let scrollRect = scrollRef.current.getBoundingClientRect(); + let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type]; + scrollRef.current.scrollTop += newOffset - anchor.offset; +@@ -123,7 +132,7 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + // eslint-disable-next-line react-hooks/exhaustive-deps + (0, $39EOa$useLayoutEffect)(updatePosition, deps); + // Update position on window resize +- $2a41e45df1593e64$var$useResize(updatePosition); ++ $2a41e45df1593e64$var$useResize(updatePosition, actualWindow); + // Update position when the overlay changes size (might need to flip). + (0, $39EOa$useResizeObserver)({ + ref: overlayRef, +@@ -152,14 +161,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + let onScroll = ()=>{ + if (isResizing.current) onResize(); + }; +- $2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.addEventListener('resize', onResize); +- $2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.addEventListener('scroll', onScroll); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.addEventListener('resize', onResize); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.addEventListener('scroll', onScroll); + return ()=>{ +- $2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.removeEventListener('resize', onResize); +- $2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.removeEventListener('scroll', onScroll); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.removeEventListener('resize', onResize); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.removeEventListener('scroll', onScroll); + }; + }, [ +- updatePosition ++ updatePosition, ++ visualViewport + ]); + let close = (0, $39EOa$useCallback)(()=>{ + if (!isResizing.current) onClose === null || onClose === void 0 ? void 0 : onClose(); +@@ -196,14 +206,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + updatePosition: updatePosition + }; + } +-function $2a41e45df1593e64$var$useResize(onResize) { ++function $2a41e45df1593e64$var$useResize(onResize, actualWindow = window) { + (0, $39EOa$useLayoutEffect)(()=>{ +- window.addEventListener('resize', onResize, false); ++ actualWindow.addEventListener('resize', onResize, false); + return ()=>{ +- window.removeEventListener('resize', onResize, false); ++ actualWindow.removeEventListener('resize', onResize, false); + }; + }, [ +- onResize ++ onResize, ++ actualWindow + ]); + } + function $2a41e45df1593e64$var$translateRTL(position, direction) { +diff --git a/dist/useOverlayPosition.module.js b/dist/useOverlayPosition.module.js +index c87314995a34cb98128a28c4c0acf23dbefc9359..aacbbefa74c9183cff99ce922cfecdbb9442b384 100644 +--- a/dist/useOverlayPosition.module.js ++++ b/dist/useOverlayPosition.module.js +@@ -19,12 +19,22 @@ import {useLocale as $39EOa$useLocale} from "@react-aria/i18n"; + + + +-let $2a41e45df1593e64$var$visualViewport = typeof document !== 'undefined' ? window.visualViewport : null; ++let $2a41e45df1593e64$var$getWindowAndVisualViewport = (targetNode)=>{ ++ let actualWindow = (targetNode === null || targetNode === void 0 ? void 0 : targetNode.ownerDocument.defaultView) || window; ++ let visualViewport = (actualWindow === null || actualWindow === void 0 ? void 0 : actualWindow.visualViewport) || null; ++ return [ ++ actualWindow, ++ visualViewport ++ ]; ++}; + function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + let { direction: direction } = (0, $39EOa$useLocale)(); + let { arrowSize: arrowSize = 0, targetRef: targetRef, overlayRef: overlayRef, scrollRef: scrollRef = overlayRef, placement: placement = 'bottom', containerPadding: containerPadding = 12, shouldFlip: shouldFlip = true, boundaryElement: boundaryElement = typeof document !== 'undefined' ? document.body : null, offset: offset = 0, crossOffset: crossOffset = 0, shouldUpdatePosition: shouldUpdatePosition = true, isOpen: isOpen = true, onClose: onClose, maxHeight: maxHeight, arrowBoundaryOffset: arrowBoundaryOffset = 0 } = props; + let [position, setPosition] = (0, $39EOa$useState)(null); ++ let [actualWindow, visualViewport] = $2a41e45df1593e64$var$getWindowAndVisualViewport(targetRef.current); + let deps = [ ++ visualViewport, ++ actualWindow, + shouldUpdatePosition, + placement, + overlayRef.current, +@@ -44,23 +54,23 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might + // just be a non-realistic use case + // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles +- let lastScale = (0, $39EOa$useRef)($2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.scale); ++ let lastScale = (0, $39EOa$useRef)(visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale); + (0, $39EOa$useEffect)(()=>{ +- if (isOpen) lastScale.current = $2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.scale; ++ if (isOpen) lastScale.current = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale; + }, [ + isOpen + ]); + let updatePosition = (0, $39EOa$useCallback)(()=>{ + if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !boundaryElement) return; +- if (($2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.scale) !== lastScale.current) return; ++ if ((visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.scale) !== lastScale.current) return; + // Determine a scroll anchor based on the focused element. + // This stores the offset of the anchor element from the scroll container + // so it can be restored after repositioning. This way if the overlay height + // changes, the focused element appears to stay in the same position. + let anchor = null; +- if (scrollRef.current && scrollRef.current.contains(document.activeElement)) { +- var _document_activeElement; +- let anchorRect = (_document_activeElement = document.activeElement) === null || _document_activeElement === void 0 ? void 0 : _document_activeElement.getBoundingClientRect(); ++ if (scrollRef.current && scrollRef.current.contains(actualWindow.document.activeElement)) { ++ var _actualWindow_document_activeElement; ++ let anchorRect = (_actualWindow_document_activeElement = actualWindow.document.activeElement) === null || _actualWindow_document_activeElement === void 0 ? void 0 : _actualWindow_document_activeElement.getBoundingClientRect(); + let scrollRect = scrollRef.current.getBoundingClientRect(); + var _anchorRect_top; + // Anchor from the top if the offset is in the top half of the scrollable element, +@@ -79,11 +89,10 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + // RAC collections populating after a second render and properly set a correct max height + positioning when it populates. + let overlay = overlayRef.current; + if (!maxHeight && overlayRef.current) { +- var _window_visualViewport; + overlay.style.top = '0px'; + overlay.style.bottom = ''; +- var _window_visualViewport_height; +- overlay.style.maxHeight = ((_window_visualViewport_height = (_window_visualViewport = window.visualViewport) === null || _window_visualViewport === void 0 ? void 0 : _window_visualViewport.height) !== null && _window_visualViewport_height !== void 0 ? _window_visualViewport_height : window.innerHeight) + 'px'; ++ var _visualViewport_height; ++ overlay.style.maxHeight = ((_visualViewport_height = visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.height) !== null && _visualViewport_height !== void 0 ? _visualViewport_height : actualWindow.innerHeight) + 'px'; + } + let position = (0, $edcf132a9284368a$export$b3ceb0cbf1056d98)({ + placement: $2a41e45df1593e64$var$translateRTL(placement, direction), +@@ -109,8 +118,8 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + Object.keys(position.position).forEach((key)=>overlay.style[key] = position.position[key] + 'px'); + overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : ''; + // Restore scroll position relative to anchor element. +- if (anchor && document.activeElement && scrollRef.current) { +- let anchorRect = document.activeElement.getBoundingClientRect(); ++ if (anchor && actualWindow.document.activeElement && scrollRef.current) { ++ let anchorRect = actualWindow.document.activeElement.getBoundingClientRect(); + let scrollRect = scrollRef.current.getBoundingClientRect(); + let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type]; + scrollRef.current.scrollTop += newOffset - anchor.offset; +@@ -123,7 +132,7 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + // eslint-disable-next-line react-hooks/exhaustive-deps + (0, $39EOa$useLayoutEffect)(updatePosition, deps); + // Update position on window resize +- $2a41e45df1593e64$var$useResize(updatePosition); ++ $2a41e45df1593e64$var$useResize(updatePosition, actualWindow); + // Update position when the overlay changes size (might need to flip). + (0, $39EOa$useResizeObserver)({ + ref: overlayRef, +@@ -152,14 +161,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + let onScroll = ()=>{ + if (isResizing.current) onResize(); + }; +- $2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.addEventListener('resize', onResize); +- $2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.addEventListener('scroll', onScroll); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.addEventListener('resize', onResize); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.addEventListener('scroll', onScroll); + return ()=>{ +- $2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.removeEventListener('resize', onResize); +- $2a41e45df1593e64$var$visualViewport === null || $2a41e45df1593e64$var$visualViewport === void 0 ? void 0 : $2a41e45df1593e64$var$visualViewport.removeEventListener('scroll', onScroll); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.removeEventListener('resize', onResize); ++ visualViewport === null || visualViewport === void 0 ? void 0 : visualViewport.removeEventListener('scroll', onScroll); + }; + }, [ +- updatePosition ++ updatePosition, ++ visualViewport + ]); + let close = (0, $39EOa$useCallback)(()=>{ + if (!isResizing.current) onClose === null || onClose === void 0 ? void 0 : onClose(); +@@ -196,14 +206,15 @@ function $2a41e45df1593e64$export$d39e1813b3bdd0e1(props) { + updatePosition: updatePosition + }; + } +-function $2a41e45df1593e64$var$useResize(onResize) { ++function $2a41e45df1593e64$var$useResize(onResize, actualWindow = window) { + (0, $39EOa$useLayoutEffect)(()=>{ +- window.addEventListener('resize', onResize, false); ++ actualWindow.addEventListener('resize', onResize, false); + return ()=>{ +- window.removeEventListener('resize', onResize, false); ++ actualWindow.removeEventListener('resize', onResize, false); + }; + }, [ +- onResize ++ onResize, ++ actualWindow + ]); + } + function $2a41e45df1593e64$var$translateRTL(position, direction) { +diff --git a/dist/useOverlayPosition.module.js.map b/dist/useOverlayPosition.module.js.map +index fc91cdbf9437c324ff1264e12f9f9bbb84f5bd04..efdaeaec6a36c075bf62420fb03c1c9775150835 100644 +--- a/dist/useOverlayPosition.module.js.map ++++ b/dist/useOverlayPosition.module.js.map +@@ -1 +1 @@ +-{"mappings":"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","sources":["packages/@react-aria/overlays/src/useOverlayPosition.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {calculatePosition, PositionResult} from './calculatePosition';\nimport {DOMAttributes, RefObject} from '@react-types/shared';\nimport {Placement, PlacementAxis, PositionProps} from '@react-types/overlays';\nimport {useCallback, useEffect, useRef, useState} from 'react';\nimport {useCloseOnScroll} from './useCloseOnScroll';\nimport {useLayoutEffect, useResizeObserver} from '@react-aria/utils';\nimport {useLocale} from '@react-aria/i18n';\n\nexport interface AriaPositionProps extends PositionProps {\n /**\n * Cross size of the overlay arrow in pixels.\n * @default 0\n */\n arrowSize?: number,\n /**\n * Element that that serves as the positioning boundary.\n * @default document.body\n */\n boundaryElement?: Element,\n /**\n * The ref for the element which the overlay positions itself with respect to.\n */\n targetRef: RefObject,\n /**\n * The ref for the overlay element.\n */\n overlayRef: RefObject,\n /**\n * A ref for the scrollable region within the overlay.\n * @default overlayRef\n */\n scrollRef?: RefObject,\n /**\n * Whether the overlay should update its position automatically.\n * @default true\n */\n shouldUpdatePosition?: boolean,\n /** Handler that is called when the overlay should close. */\n onClose?: (() => void) | null,\n /**\n * The maxHeight specified for the overlay element.\n * By default, it will take all space up to the current viewport height.\n */\n maxHeight?: number,\n /**\n * The minimum distance the arrow's edge should be from the edge of the overlay element.\n * @default 0\n */\n arrowBoundaryOffset?: number\n}\n\nexport interface PositionAria {\n /** Props for the overlay container element. */\n overlayProps: DOMAttributes,\n /** Props for the overlay tip arrow if any. */\n arrowProps: DOMAttributes,\n /** Placement of the overlay with respect to the overlay trigger. */\n placement: PlacementAxis | null,\n /** Updates the position of the overlay. */\n updatePosition(): void\n}\n\ninterface ScrollAnchor {\n type: 'top' | 'bottom',\n offset: number\n}\n\nlet visualViewport = typeof document !== 'undefined' ? window.visualViewport : null;\n\n/**\n * Handles positioning overlays like popovers and menus relative to a trigger\n * element, and updating the position when the window resizes.\n */\nexport function useOverlayPosition(props: AriaPositionProps): PositionAria {\n let {direction} = useLocale();\n let {\n arrowSize = 0,\n targetRef,\n overlayRef,\n scrollRef = overlayRef,\n placement = 'bottom' as Placement,\n containerPadding = 12,\n shouldFlip = true,\n boundaryElement = typeof document !== 'undefined' ? document.body : null,\n offset = 0,\n crossOffset = 0,\n shouldUpdatePosition = true,\n isOpen = true,\n onClose,\n maxHeight,\n arrowBoundaryOffset = 0\n } = props;\n let [position, setPosition] = useState(null);\n\n let deps = [\n shouldUpdatePosition,\n placement,\n overlayRef.current,\n targetRef.current,\n scrollRef.current,\n containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n isOpen,\n direction,\n maxHeight,\n arrowBoundaryOffset,\n arrowSize\n ];\n\n // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might\n // just be a non-realistic use case\n // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles\n let lastScale = useRef(visualViewport?.scale);\n useEffect(() => {\n if (isOpen) {\n lastScale.current = visualViewport?.scale;\n }\n }, [isOpen]);\n\n let updatePosition = useCallback(() => {\n if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !boundaryElement) {\n return;\n }\n\n if (visualViewport?.scale !== lastScale.current) {\n return;\n }\n\n // Determine a scroll anchor based on the focused element.\n // This stores the offset of the anchor element from the scroll container\n // so it can be restored after repositioning. This way if the overlay height\n // changes, the focused element appears to stay in the same position.\n let anchor: ScrollAnchor | null = null;\n if (scrollRef.current && scrollRef.current.contains(document.activeElement)) {\n let anchorRect = document.activeElement?.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n // Anchor from the top if the offset is in the top half of the scrollable element,\n // otherwise anchor from the bottom.\n anchor = {\n type: 'top',\n offset: (anchorRect?.top ?? 0) - scrollRect.top\n };\n if (anchor.offset > scrollRect.height / 2) {\n anchor.type = 'bottom';\n anchor.offset = (anchorRect?.bottom ?? 0) - scrollRect.bottom;\n }\n }\n\n // Always reset the overlay's previous max height if not defined by the user so that we can compensate for\n // RAC collections populating after a second render and properly set a correct max height + positioning when it populates.\n let overlay = (overlayRef.current as HTMLElement);\n if (!maxHeight && overlayRef.current) {\n overlay.style.top = '0px';\n overlay.style.bottom = '';\n overlay.style.maxHeight = (window.visualViewport?.height ?? window.innerHeight) + 'px';\n }\n\n let position = calculatePosition({\n placement: translateRTL(placement, direction),\n overlayNode: overlayRef.current,\n targetNode: targetRef.current,\n scrollNode: scrollRef.current || overlayRef.current,\n padding: containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n });\n\n if (!position.position) {\n return;\n }\n\n // Modify overlay styles directly so positioning happens immediately without the need of a second render\n // This is so we don't have to delay autoFocus scrolling or delay applying preventScroll for popovers\n overlay.style.top = '';\n overlay.style.bottom = '';\n overlay.style.left = '';\n overlay.style.right = '';\n\n Object.keys(position.position).forEach(key => overlay.style[key] = (position.position!)[key] + 'px');\n overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : '';\n\n // Restore scroll position relative to anchor element.\n if (anchor && document.activeElement && scrollRef.current) {\n let anchorRect = document.activeElement.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type];\n scrollRef.current.scrollTop += newOffset - anchor.offset;\n }\n\n // Trigger a set state for a second render anyway for arrow positioning\n setPosition(position);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n // Update position when anything changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(updatePosition, deps);\n\n // Update position on window resize\n useResize(updatePosition);\n\n // Update position when the overlay changes size (might need to flip).\n useResizeObserver({\n ref: overlayRef,\n onResize: updatePosition\n });\n\n // Update position when the target changes size (might need to flip).\n useResizeObserver({\n ref: targetRef,\n onResize: updatePosition\n });\n\n // Reposition the overlay and do not close on scroll while the visual viewport is resizing.\n // This will ensure that overlays adjust their positioning when the iOS virtual keyboard appears.\n let isResizing = useRef(false);\n useLayoutEffect(() => {\n let timeout: ReturnType;\n let onResize = () => {\n isResizing.current = true;\n clearTimeout(timeout);\n\n timeout = setTimeout(() => {\n isResizing.current = false;\n }, 500);\n\n updatePosition();\n };\n\n // Only reposition the overlay if a scroll event happens immediately as a result of resize (aka the virtual keyboard has appears)\n // We don't want to reposition the overlay if the user has pinch zoomed in and is scrolling the viewport around.\n let onScroll = () => {\n if (isResizing.current) {\n onResize();\n }\n };\n\n visualViewport?.addEventListener('resize', onResize);\n visualViewport?.addEventListener('scroll', onScroll);\n return () => {\n visualViewport?.removeEventListener('resize', onResize);\n visualViewport?.removeEventListener('scroll', onScroll);\n };\n }, [updatePosition]);\n\n let close = useCallback(() => {\n if (!isResizing.current) {\n onClose?.();\n }\n }, [onClose, isResizing]);\n\n // When scrolling a parent scrollable region of the trigger (other than the body),\n // we hide the popover. Otherwise, its position would be incorrect.\n useCloseOnScroll({\n triggerRef: targetRef,\n isOpen,\n onClose: onClose && close\n });\n\n return {\n overlayProps: {\n style: {\n position: 'absolute',\n zIndex: 100000, // should match the z-index in ModalTrigger\n ...position?.position,\n maxHeight: position?.maxHeight ?? '100vh'\n }\n },\n placement: position?.placement ?? null,\n arrowProps: {\n 'aria-hidden': 'true',\n role: 'presentation',\n style: {\n left: position?.arrowOffsetLeft,\n top: position?.arrowOffsetTop\n }\n },\n updatePosition\n };\n}\n\nfunction useResize(onResize) {\n useLayoutEffect(() => {\n window.addEventListener('resize', onResize, false);\n return () => {\n window.removeEventListener('resize', onResize, false);\n };\n }, [onResize]);\n}\n\nfunction translateRTL(position, direction) {\n if (direction === 'rtl') {\n return position.replace('start', 'right').replace('end', 'left');\n }\n return position.replace('start', 'left').replace('end', 'right');\n}\n"],"names":[],"version":3,"file":"useOverlayPosition.module.js.map"} +\ No newline at end of file ++{"mappings":"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","sources":["packages/@react-aria/overlays/src/useOverlayPosition.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {calculatePosition, PositionResult} from './calculatePosition';\nimport {DOMAttributes, RefObject} from '@react-types/shared';\nimport {Placement, PlacementAxis, PositionProps} from '@react-types/overlays';\nimport {useCallback, useEffect, useRef, useState} from 'react';\nimport {useCloseOnScroll} from './useCloseOnScroll';\nimport {useLayoutEffect, useResizeObserver} from '@react-aria/utils';\nimport {useLocale} from '@react-aria/i18n';\n\nexport interface AriaPositionProps extends PositionProps {\n /**\n * Cross size of the overlay arrow in pixels.\n * @default 0\n */\n arrowSize?: number,\n /**\n * Element that that serves as the positioning boundary.\n * @default document.body\n */\n boundaryElement?: Element,\n /**\n * The ref for the element which the overlay positions itself with respect to.\n */\n targetRef: RefObject,\n /**\n * The ref for the overlay element.\n */\n overlayRef: RefObject,\n /**\n * A ref for the scrollable region within the overlay.\n * @default overlayRef\n */\n scrollRef?: RefObject,\n /**\n * Whether the overlay should update its position automatically.\n * @default true\n */\n shouldUpdatePosition?: boolean,\n /** Handler that is called when the overlay should close. */\n onClose?: (() => void) | null,\n /**\n * The maxHeight specified for the overlay element.\n * By default, it will take all space up to the current viewport height.\n */\n maxHeight?: number,\n /**\n * The minimum distance the arrow's edge should be from the edge of the overlay element.\n * @default 0\n */\n arrowBoundaryOffset?: number\n}\n\nexport interface PositionAria {\n /** Props for the overlay container element. */\n overlayProps: DOMAttributes,\n /** Props for the overlay tip arrow if any. */\n arrowProps: DOMAttributes,\n /** Placement of the overlay with respect to the overlay trigger. */\n placement: PlacementAxis | null,\n /** Updates the position of the overlay. */\n updatePosition(): void\n}\n\ninterface ScrollAnchor {\n type: 'top' | 'bottom',\n offset: number\n}\n\nlet getWindowAndVisualViewport = (targetNode?: Element | null): [Window, VisualViewport | null] => {\n let actualWindow = targetNode?.ownerDocument.defaultView || window;\n let visualViewport = actualWindow?.visualViewport || null;\n return [actualWindow, visualViewport];\n};\n\n\n/**\n * Handles positioning overlays like popovers and menus relative to a trigger\n * element, and updating the position when the window resizes.\n */\nexport function useOverlayPosition(props: AriaPositionProps): PositionAria {\n let {direction} = useLocale();\n let {\n arrowSize = 0,\n targetRef,\n overlayRef,\n scrollRef = overlayRef,\n placement = 'bottom' as Placement,\n containerPadding = 12,\n shouldFlip = true,\n boundaryElement = typeof document !== 'undefined' ? document.body : null,\n offset = 0,\n crossOffset = 0,\n shouldUpdatePosition = true,\n isOpen = true,\n onClose,\n maxHeight,\n arrowBoundaryOffset = 0\n } = props;\n let [position, setPosition] = useState(null);\n let [actualWindow, visualViewport] = getWindowAndVisualViewport(targetRef.current);\n let deps = [\n visualViewport,\n actualWindow,\n shouldUpdatePosition,\n placement,\n overlayRef.current,\n targetRef.current,\n scrollRef.current,\n containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n isOpen,\n direction,\n maxHeight,\n arrowBoundaryOffset,\n arrowSize\n ];\n\n // Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might\n // just be a non-realistic use case\n // Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles\n let lastScale = useRef(visualViewport?.scale);\n useEffect(() => {\n if (isOpen) {\n lastScale.current = visualViewport?.scale;\n }\n }, [isOpen]);\n\n let updatePosition = useCallback(() => {\n if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !boundaryElement) {\n return;\n }\n\n if (visualViewport?.scale !== lastScale.current) {\n return;\n }\n\n // Determine a scroll anchor based on the focused element.\n // This stores the offset of the anchor element from the scroll container\n // so it can be restored after repositioning. This way if the overlay height\n // changes, the focused element appears to stay in the same position.\n let anchor: ScrollAnchor | null = null;\n if (scrollRef.current && scrollRef.current.contains(actualWindow.document.activeElement)) {\n let anchorRect = actualWindow.document.activeElement?.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n // Anchor from the top if the offset is in the top half of the scrollable element,\n // otherwise anchor from the bottom.\n anchor = {\n type: 'top',\n offset: (anchorRect?.top ?? 0) - scrollRect.top\n };\n if (anchor.offset > scrollRect.height / 2) {\n anchor.type = 'bottom';\n anchor.offset = (anchorRect?.bottom ?? 0) - scrollRect.bottom;\n }\n }\n\n // Always reset the overlay's previous max height if not defined by the user so that we can compensate for\n // RAC collections populating after a second render and properly set a correct max height + positioning when it populates.\n let overlay = (overlayRef.current as HTMLElement);\n if (!maxHeight && overlayRef.current) {\n overlay.style.top = '0px';\n overlay.style.bottom = '';\n overlay.style.maxHeight = (visualViewport?.height ?? actualWindow.innerHeight) + 'px';\n }\n\n let position = calculatePosition({\n placement: translateRTL(placement, direction),\n overlayNode: overlayRef.current,\n targetNode: targetRef.current,\n scrollNode: scrollRef.current || overlayRef.current,\n padding: containerPadding,\n shouldFlip,\n boundaryElement,\n offset,\n crossOffset,\n maxHeight,\n arrowSize,\n arrowBoundaryOffset\n });\n\n if (!position.position) {\n return;\n }\n\n // Modify overlay styles directly so positioning happens immediately without the need of a second render\n // This is so we don't have to delay autoFocus scrolling or delay applying preventScroll for popovers\n overlay.style.top = '';\n overlay.style.bottom = '';\n overlay.style.left = '';\n overlay.style.right = '';\n\n Object.keys(position.position).forEach(key => overlay.style[key] = (position.position!)[key] + 'px');\n overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : '';\n\n // Restore scroll position relative to anchor element.\n if (anchor && actualWindow.document.activeElement && scrollRef.current) {\n let anchorRect = actualWindow.document.activeElement.getBoundingClientRect();\n let scrollRect = scrollRef.current.getBoundingClientRect();\n let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type];\n scrollRef.current.scrollTop += newOffset - anchor.offset;\n }\n\n // Trigger a set state for a second render anyway for arrow positioning\n setPosition(position);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n // Update position when anything changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(updatePosition, deps);\n\n // Update position on window resize\n useResize(updatePosition, actualWindow);\n\n // Update position when the overlay changes size (might need to flip).\n useResizeObserver({\n ref: overlayRef,\n onResize: updatePosition\n });\n\n // Update position when the target changes size (might need to flip).\n useResizeObserver({\n ref: targetRef,\n onResize: updatePosition\n });\n\n // Reposition the overlay and do not close on scroll while the visual viewport is resizing.\n // This will ensure that overlays adjust their positioning when the iOS virtual keyboard appears.\n let isResizing = useRef(false);\n useLayoutEffect(() => {\n let timeout: ReturnType;\n let onResize = () => {\n isResizing.current = true;\n clearTimeout(timeout);\n\n timeout = setTimeout(() => {\n isResizing.current = false;\n }, 500);\n\n updatePosition();\n };\n\n // Only reposition the overlay if a scroll event happens immediately as a result of resize (aka the virtual keyboard has appears)\n // We don't want to reposition the overlay if the user has pinch zoomed in and is scrolling the viewport around.\n let onScroll = () => {\n if (isResizing.current) {\n onResize();\n }\n };\n\n visualViewport?.addEventListener('resize', onResize);\n visualViewport?.addEventListener('scroll', onScroll);\n return () => {\n visualViewport?.removeEventListener('resize', onResize);\n visualViewport?.removeEventListener('scroll', onScroll);\n };\n }, [updatePosition, visualViewport]);\n\n let close = useCallback(() => {\n if (!isResizing.current) {\n onClose?.();\n }\n }, [onClose, isResizing]);\n\n // When scrolling a parent scrollable region of the trigger (other than the body),\n // we hide the popover. Otherwise, its position would be incorrect.\n useCloseOnScroll({\n triggerRef: targetRef,\n isOpen,\n onClose: onClose && close\n });\n\n return {\n overlayProps: {\n style: {\n position: 'absolute',\n zIndex: 100000, // should match the z-index in ModalTrigger\n ...position?.position,\n maxHeight: position?.maxHeight ?? '100vh'\n }\n },\n placement: position?.placement ?? null,\n arrowProps: {\n 'aria-hidden': 'true',\n role: 'presentation',\n style: {\n left: position?.arrowOffsetLeft,\n top: position?.arrowOffsetTop\n }\n },\n updatePosition\n };\n}\n\nfunction useResize(onResize, actualWindow: Window = window) {\n useLayoutEffect(() => {\n actualWindow.addEventListener('resize', onResize, false);\n return () => {\n actualWindow.removeEventListener('resize', onResize, false);\n };\n }, [onResize, actualWindow]);\n}\n\nfunction translateRTL(position, direction) {\n if (direction === 'rtl') {\n return position.replace('start', 'right').replace('end', 'left');\n }\n return position.replace('start', 'left').replace('end', 'right');\n}\n"],"names":[],"version":3,"file":"useOverlayPosition.module.js.map"} +\ No newline at end of file diff --git a/apps/meteor/.mocharc.api.livechat.js b/apps/meteor/.mocharc.api.livechat.js index 48a3a13e2751f..7b98b659bc8de 100644 --- a/apps/meteor/.mocharc.api.livechat.js +++ b/apps/meteor/.mocharc.api.livechat.js @@ -10,5 +10,6 @@ module.exports = /** @satisfies {import('mocha').MochaOptions} */ ({ bail: true, retries: 0, file: 'tests/end-to-end/teardown.ts', + reporter: 'tests/end-to-end/reporter.ts', spec: ['tests/end-to-end/api/livechat/**/*'], }); diff --git a/apps/meteor/.mocharc.js b/apps/meteor/.mocharc.js index 298f6dcc7c9d1..bcbb789e3dd98 100644 --- a/apps/meteor/.mocharc.js +++ b/apps/meteor/.mocharc.js @@ -27,6 +27,7 @@ module.exports = { 'tests/unit/lib/**/*.spec.ts', 'tests/unit/server/**/*.tests.ts', 'tests/unit/server/**/*.spec.ts', + 'app/2fa/server/**/*.spec.ts', 'app/api/server/lib/**/*.spec.ts', 'app/file-upload/server/**/*.spec.ts', 'app/statistics/server/**/*.spec.ts', diff --git a/apps/meteor/.storybook/decorators.tsx b/apps/meteor/.storybook/decorators.tsx index 822f524e5b7ad..75d24df1195a4 100644 --- a/apps/meteor/.storybook/decorators.tsx +++ b/apps/meteor/.storybook/decorators.tsx @@ -8,33 +8,18 @@ import TranslationContextMock from '../client/stories/contexts/TranslationContex const MockedAppRoot = mockAppRoot().build(); -export const rocketChatDecorator: Decorator = (fn, { parameters }) => { - const linkElement = document.getElementById('theme-styles') || document.createElement('link'); - if (linkElement.id !== 'theme-styles') { - require('../app/theme/client/main.css'); - require('../app/theme/client/rocketchat.font.css'); - linkElement.setAttribute('id', 'theme-styles'); - linkElement.setAttribute('rel', 'stylesheet'); - linkElement.setAttribute('href', 'https://open.rocket.chat/theme.css'); - document.head.appendChild(linkElement); - } - - return ( - - - - - - -
{fn()}
-
-
-
-
-
- ); -}; +export const RocketChatDecorator: Decorator = (Story, { parameters }) => ( + + + + + +
+ +
+
+
+
+
+
+); diff --git a/apps/meteor/.storybook/logo.svg b/apps/meteor/.storybook/logo.svg deleted file mode 100644 index 6ae18fa4b93eb..0000000000000 --- a/apps/meteor/.storybook/logo.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/apps/meteor/.storybook/logo.svg.d.ts b/apps/meteor/.storybook/logo.svg.d.ts deleted file mode 100644 index 623caebae2b66..0000000000000 --- a/apps/meteor/.storybook/logo.svg.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare const path: string; -export = path; diff --git a/apps/meteor/.storybook/main.ts b/apps/meteor/.storybook/main.ts index f3e6bb831c797..1cbaa4b13cdae 100644 --- a/apps/meteor/.storybook/main.ts +++ b/apps/meteor/.storybook/main.ts @@ -1,26 +1,15 @@ import { dirname, join, resolve } from 'path'; -import type { StorybookConfig } from '@storybook/react-webpack5'; +import baseConfig from '@rocket.chat/storybook-config/main'; import webpack from 'webpack'; -export default { - stories: ['../client/**/*.stories.{js,tsx}', '../app/**/*.stories.{js,tsx}', '../ee/app/**/*.stories.{js,tsx}'], - - addons: [ - getAbsolutePath('@storybook/addon-essentials'), - getAbsolutePath('@storybook/addon-interactions'), - getAbsolutePath('@storybook/addon-webpack5-compiler-swc'), - getAbsolutePath('@storybook/addon-styling-webpack'), - getAbsolutePath('@storybook/addon-a11y'), - ], - - typescript: { - reactDocgen: 'react-docgen', - }, +export default baseConfig({ + stories: ['../client/**/*.stories.{js,tsx}'], webpackFinal: async (config) => { // Those aliases are needed because dependencies in the monorepo use another // dependencies that are not hoisted on this workspace + const swiperRoot = dirname(require.resolve('swiper/package.json')); config.resolve = { ...config.resolve, alias: { @@ -29,16 +18,19 @@ export default { // 'react/jsx-runtime': require.resolve('../../../node_modules/react/jsx-runtime'), '@tanstack/react-query': require.resolve('../../../node_modules/@tanstack/react-query'), '@rocket.chat/fuselage$': require.resolve('../../../node_modules/@rocket.chat/fuselage'), + // Meteor's bundler ignores the `exports` field, so source code reaches + // into swiper's internals via deep file paths. Webpack honors `exports` + // and rejects them, so map each subpath to the actual file. + 'swiper/swiper-react.mjs$': join(swiperRoot, 'swiper-react.mjs'), + 'swiper/swiper-react$': join(swiperRoot, 'swiper-react.d.ts'), + 'swiper/modules/index.mjs$': join(swiperRoot, 'modules/index.mjs'), + 'swiper/swiper.css$': join(swiperRoot, 'swiper.css'), + 'swiper/modules/zoom.css$': join(swiperRoot, 'modules/zoom.css'), }, - // This is only needed because of Fontello + // This is only needed because of Rocket.Chat's icon font. roots: [...(config.resolve?.roots ?? []), resolve(__dirname, '../../../apps/meteor/public')], }; - config.module?.rules?.push({ - test: /\.info$/, - type: 'json', - }); - // Strip the `env` option that addon-webpack5-compiler-swc injects on swc-loader; // it conflicts with `jsc.target` from `.swcrc` (Meteor's Modern Build Stack). for (const rule of (config.module?.rules ?? []) as any[]) { @@ -48,21 +40,11 @@ export default { } config.plugins?.push( - new webpack.NormalModuleReplacementPlugin(/^meteor/, require.resolve('./mocks/meteor.js')), + new webpack.NormalModuleReplacementPlugin(/^meteor/, require.resolve('./mocks/meteor.ts')), new webpack.NormalModuleReplacementPlugin(/(app)\/*.*\/(server)\/*/, require.resolve('./mocks/empty.ts')), + new webpack.NormalModuleReplacementPlugin(/rocketchat\.info$/, require.resolve('./mocks/rocketchat.info.ts')), ); return config; }, - - framework: { - name: getAbsolutePath('@storybook/react-webpack5'), - options: {}, - }, - - docs: {}, -} satisfies StorybookConfig; - -function getAbsolutePath(value: any): string { - return dirname(require.resolve(join(value, 'package.json'))); -} +}); diff --git a/apps/meteor/.storybook/mocks/meteor.js b/apps/meteor/.storybook/mocks/meteor.ts similarity index 87% rename from apps/meteor/.storybook/mocks/meteor.js rename to apps/meteor/.storybook/mocks/meteor.ts index 12c90ae1501bf..e3f0e0a979e70 100644 --- a/apps/meteor/.storybook/mocks/meteor.js +++ b/apps/meteor/.storybook/mocks/meteor.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ export const Meteor = { Device: { isDesktop: () => false, @@ -32,7 +33,7 @@ export const Tracker = { autorun: () => ({ stop: () => {}, }), - nonreactive: (fn) => fn(), + nonreactive: (fn: () => any) => fn(), Dependency: () => {}, }; @@ -50,11 +51,11 @@ export const Mongo = { }), }; -export const ReactiveVar = (val) => { +export const ReactiveVar = (val: unknown) => { let currentVal = val; return { get: () => currentVal, - set: (val) => { + set: (val: unknown) => { currentVal = val; }, }; @@ -93,3 +94,5 @@ export const Session = { get: () => {}, set: () => {}, }; + +export const DDPCommon = {}; diff --git a/apps/meteor/.storybook/mocks/rocketchat.info.ts b/apps/meteor/.storybook/mocks/rocketchat.info.ts new file mode 100644 index 0000000000000..c1f0afb34c6bf --- /dev/null +++ b/apps/meteor/.storybook/mocks/rocketchat.info.ts @@ -0,0 +1,3 @@ +export const version = '1.0.0'; + +export const Info = { version }; diff --git a/apps/meteor/.storybook/preview.ts b/apps/meteor/.storybook/preview.ts index 379c1f4a34647..fdcb2aba28ad6 100644 --- a/apps/meteor/.storybook/preview.ts +++ b/apps/meteor/.storybook/preview.ts @@ -1,22 +1,17 @@ -import type { Decorator, Parameters } from '@storybook/react'; +import { parameters, decorators } from '@rocket.chat/storybook-config/preview'; +import type { Preview } from '@storybook/react'; -import { rocketChatDecorator } from './decorators'; +import { RocketChatDecorator } from './decorators'; -export const decorators: Decorator[] = [rocketChatDecorator]; +import '../app/theme/client/main.css'; +import 'highlight.js/styles/github.css'; -export const parameters: Parameters = { - backgrounds: { - grid: { - cellSize: 4, - cellAmount: 4, - opacity: 0.5, - }, - }, - options: { - storySort: { - method: 'alphabetical', - order: ['Components', '*', 'Enterprise'], - }, +const preview: Preview = { + parameters: { + ...parameters, }, + decorators: [...decorators, RocketChatDecorator], + tags: ['autodocs'], }; -export const tags = ['autodocs']; + +export default preview; diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 1983c0d96a520..bb47d24962e31 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,11 +1,210 @@ # @rocket.chat/meteor -## 8.5.1 +## 8.6.0-rc.3 ### Patch Changes - Bump @rocket.chat/meteor version. +-
Updated dependencies []: + + - @rocket.chat/core-typings@8.6.0-rc.3 + - @rocket.chat/rest-typings@8.6.0-rc.3 +
+ +## 8.6.0-rc.2 + +### Patch Changes + +- Bump @rocket.chat/meteor version. +-
Updated dependencies []: + + - @rocket.chat/core-typings@8.6.0-rc.2 + - @rocket.chat/rest-typings@8.6.0-rc.2 +
+ +## 8.6.0-rc.1 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- ([#41057](https://github.com/RocketChat/Rocket.Chat/pull/41057)) Fixes incoming integrations inability of reaching internal hosts by adding "ignoreSsrfValidation: true" to it + +- ([#41046](https://github.com/RocketChat/Rocket.Chat/pull/41046)) Fixes an issue where editing or deleting a message in a federated room caused subsequent messages to stop syncing between servers + + Note: this prevents the issue from happening, but does not restore rooms that are already affected. Recovering those requires a separate, one-time repair. + +- ([#41065](https://github.com/RocketChat/Rocket.Chat/pull/41065)) Fixes REST API endpoints that require two-factor authentication (such as `users.update`) rejecting requests authenticated with a Personal Access Token created with "Ignore Two Factor Authentication", returning `totp-required` even though the token was meant to bypass the check. The two-factor authorization check now resolves the login token from the REST connection, so `bypassTwoFactor` tokens are honored again. + +-
Updated dependencies [308e1c5c3a8f6432dc03472914f563d1c0d15bdf, 6fa5378a940cbc809800b3c7d7c0639810bb0ab8, 5d5edd8520ddb424bd336e3ec802c1f4a4e7d1ce, 9861932cd7653c457a5b09e379fcb60a33947cf5]: + + - @rocket.chat/federation-matrix@0.1.5-rc.1 + - @rocket.chat/core-typings@8.6.0-rc.1 + - @rocket.chat/media-signaling@1.1.0-rc.1 + - @rocket.chat/ui-voip@22.0.0-rc.1 + - @rocket.chat/rest-typings@8.6.0-rc.1 +
+ +## 8.6.0-rc.0 + +### Minor Changes + +- ([#40826](https://github.com/RocketChat/Rocket.Chat/pull/40826)) Shows a confirmation modal when switching attribute store setting + +- ([#40274](https://github.com/RocketChat/Rocket.Chat/pull/40274)) Adds the backend foundation for a unified presence engine with a priority-based claim system (internal > manual > external), status expiration, and previous state restore. + +- ([#40634](https://github.com/RocketChat/Rocket.Chat/pull/40634)) Allows using Virtru as the attribute store for ABAC decisions. + + ### Important + + - When using virtru as the store, the internal attribute store is disabled. + - On switch, existing ABAC attributes from rooms will be removed. Rooms will continue to be private & no users will be removed until you add attributes again. + - Users are only allowed to see & edit rooms they have access to. Access decision is evaluated on Virtru + - A user/app with the `bypass-abac-store-validation` permission can assign any attributes to rooms, even if the user doesn't have them assigned on Virtru. + +- ([#40900](https://github.com/RocketChat/Rocket.Chat/pull/40900)) Added LibreTranslate as a message auto-translation provider, alongside Google, DeepL and Microsoft. LibreTranslate can be self-hosted, enabling fully on-premise / offline message auto-translation. Configure the instance URL (and optional API key) under **Admin → Settings → Message → Auto-Translate → LibreTranslate** and select it as the Service Provider. + +- ([#40532](https://github.com/RocketChat/Rocket.Chat/pull/40532)) Adds custom-sounds.delete API endpoint. + +- ([#40711](https://github.com/RocketChat/Rocket.Chat/pull/40711)) `POST /v1/chat.delete` now accepts `{ fileId, asUser? }` as an alternative to `{ msgId, roomId, asUser? }`. When `fileId` is provided the server resolves the owning message via `Messages.getMessageByFileId` before running the existing permission and deletion flow. + +- ([#40724](https://github.com/RocketChat/Rocket.Chat/pull/40724)) Added `POST /v1/e2e.requestSubscriptionKeys` (replaces the deprecated `e2e.requestSubscriptionKeys` DDP method). Auth-gated, no body. Broadcasts `notify.e2e.keyRequest` for every encrypted room the caller is subscribed to without an E2E key, matching the DDP method's behavior. The legacy DDP method remains registered until 9.0.0 with a deprecation log pointing at the new route. + +- ([#40724](https://github.com/RocketChat/Rocket.Chat/pull/40724)) Added `POST /v1/im.blockUser` (replaces the deprecated `blockUser` / `unblockUser` DDP methods). Body is `{ roomId, block: boolean }` — `block: true` blocks the other DM participant, `block: false` unblocks. Auth-gated and per-room via the `RoomMemberActions.BLOCK` directive (DM-only). Both legacy DDP methods remain registered until 9.0.0 with deprecation logs pointing at the new route. + +- ([#40724](https://github.com/RocketChat/Rocket.Chat/pull/40724)) Added `POST /v1/settings` for batched admin setting updates (replaces the deprecated `saveSettings` DDP method). Body is `{ settings: { _id, value }[] }`. The endpoint requires authentication, enforces 2FA (`twoFactorRequired: true`), and runs the same per-setting permission chain (`edit-privileged-setting` OR `manage-selected-settings` + per-id permission) and audit/notify side effects the DDP method already performed. The legacy DDP method remains registered until 9.0.0 with a deprecation log pointing at the new route. + +- ([#40711](https://github.com/RocketChat/Rocket.Chat/pull/40711)) `GET /v1/spotlight` now mirrors the DDP `spotlight` method: + + - accepts optional `usernames` (comma-separated string), `type` (JSON-encoded `{ users?, mentions?, rooms?, includeFederatedRooms? }`) and `rid` query params; + - response items expose `nickname` / `outside` (users) and `uids` / `usernames` / `fname` (rooms); + - `status` on each user is now optional — outside/federated users were already being returned without one and the previous required-field schema rejected them as `Response validation failed`; + - the endpoint is no longer auth-gated, allowing anonymous-read flows (e.g. `Accounts_AllowAnonymousRead`) to keep finding public channels through the navbar search. + +- ([#40711](https://github.com/RocketChat/Rocket.Chat/pull/40711)) `POST /v1/users.setPreferences` now accepts an optional `data.utcOffset` (number) field. The value is stored at the user-document root via `Users.setUtcOffset` (not under `settings.preferences`), matching what the legacy `userSetUtcOffset` DDP method did. + +- ([#40996](https://github.com/RocketChat/Rocket.Chat/pull/40996)) Added a new `rooms.join` REST endpoint that lets a user join any room type, replicating the behavior of the deprecated `joinRoom` DDP method. Unlike `channels.join`, it resolves all room types through the shared `Room.join` service (access checks, join codes, federation and omnichannel rules). The client now uses `rooms.join` instead of `channels.join`. + +- ([#40791](https://github.com/RocketChat/Rocket.Chat/pull/40791)) Exposes the `isFederated` and `federation` fields for room and user objects in apps + +- ([#40202](https://github.com/RocketChat/Rocket.Chat/pull/40202)) Introduces popout functionality for voice calls + +### Patch Changes + +- ([#40988](https://github.com/RocketChat/Rocket.Chat/pull/40988)) Added Tagalog (`tl`) as a selectable interface language. It appears in the user's **Account → Preferences → Localization → Language** dropdown; interface strings fall back to English until translations are contributed (same approach as other not-yet-translated locales). + +- ([#40902](https://github.com/RocketChat/Rocket.Chat/pull/40902)) Fixes a memory leakage on the CodeMirror component (used by `code`-typed settings) + +- ([#40759](https://github.com/RocketChat/Rocket.Chat/pull/40759)) Fixes S3 file upload failing when the region setting is empty or the endpoint is configured without a URL scheme + +- ([#40702](https://github.com/RocketChat/Rocket.Chat/pull/40702)) Fixes `users.sendConfirmationEmail` rejecting unauthenticated requests, which prevented unverified users from resending their verification email from the login screen + +- ([#40992](https://github.com/RocketChat/Rocket.Chat/pull/40992)) Fixes auto-translate not activating for users who set their language preference after joining rooms + +- ([#40711](https://github.com/RocketChat/Rocket.Chat/pull/40711)) Migrate six client DDP callers to their REST equivalents (the DDP methods stay registered on the server for external SDK/mobile clients, with a deprecation log pointing at the REST route until 9.0.0 removes them): + + - `loadMissedMessages` → `GET /v1/chat.syncMessages` + - `joinRoom` → `POST /v1/channels.join` (channel-only; non-`c` rooms now error via REST the same way they used to via DDP) + - `userSetUtcOffset` → `POST /v1/users.setPreferences` (new `utcOffset` field) + - `deleteFileMessage` → `POST /v1/chat.delete` (new `fileId` body shape) + - `spotlight` → `GET /v1/spotlight` (new `usernames` / `type` / `rid` query params) + - `listCustomSounds` → `GET /v1/custom-sounds.list` + +- ([#40724](https://github.com/RocketChat/Rocket.Chat/pull/40724)) Migrate four client DDP callers to their REST equivalents (the DDP methods stay registered on the server for external SDK/mobile clients, with a deprecation log pointing at the REST route until 9.0.0 removes them): + + - `deleteCustomSound` → `POST /v1/custom-sounds.delete` + - `blockUser` / `unblockUser` → `POST /v1/im.blockUser` (single toggle with `{ roomId, block: boolean }`) + - `saveSettings` → `POST /v1/settings` + - `e2e.requestSubscriptionKeys` → `POST /v1/e2e.requestSubscriptionKeys` + +- ([#41017](https://github.com/RocketChat/Rocket.Chat/pull/41017)) Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) + +- ([#40767](https://github.com/RocketChat/Rocket.Chat/pull/40767)) Fixes an issue that allowed users to create a DM and send messages to a deactivated account + +- ([#40857](https://github.com/RocketChat/Rocket.Chat/pull/40857)) Fixes non-deterministic comparator in team's channel desertion table + +- ([#41009](https://github.com/RocketChat/Rocket.Chat/pull/41009)) Fixes an issue where updating an app in quick succession could crash the server. + +- ([#40788](https://github.com/RocketChat/Rocket.Chat/pull/40788)) Fixes error handling when using invalid regular expressions on message search + +- ([#41007](https://github.com/RocketChat/Rocket.Chat/pull/41007)) Fixes the message list shifting when typing in the fully expanded message composer + +- ([#40802](https://github.com/RocketChat/Rocket.Chat/pull/40802)) Escapes HTML tags in exported data + +- ([#40982](https://github.com/RocketChat/Rocket.Chat/pull/40982)) Fixes an issue with embedded layout where users were able to receive VideoConf calls, causing the application to crash + +- ([#40996](https://github.com/RocketChat/Rocket.Chat/pull/40996)) Fixed the "not subscribed" room screen not updating after joining a room. The join mutation invalidated a stale React Query key that no longer matched the open-room query, so the UI kept showing the join prompt until a manual page refresh. It now invalidates the correct `rooms` reference key, so the room opens immediately after joining. + +- ([#40719](https://github.com/RocketChat/Rocket.Chat/pull/40719) by [@copilot-swe-agent](https://github.com/copilot-swe-agent)) Fixes an issue that caused the UI to attempt to mark an unserved livechat room as read + +- ([#41009](https://github.com/RocketChat/Rocket.Chat/pull/41009)) Fixes an issue where an app's bot user presence was incorrectly shown as Offline after updating it. + +- ([#40849](https://github.com/RocketChat/Rocket.Chat/pull/40849)) Fixes the behavior when the login token expires to redirect the user to the login page + +- ([#40842](https://github.com/RocketChat/Rocket.Chat/pull/40842)) Fixes an issue where temporary AD/LDAP lockouts would deactivate users on rocket.chat. + +- ([#40956](https://github.com/RocketChat/Rocket.Chat/pull/40956)) fixes issue that caused threads to sometimes not scroll when sending messages + +- ([#40684](https://github.com/RocketChat/Rocket.Chat/pull/40684)) Fixes an issue on ABAC audit page that sent local timestamps instead of UTC ones as filters to fetch audit logs + +- ([#40889](https://github.com/RocketChat/Rocket.Chat/pull/40889)) Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) + +- ([#40706](https://github.com/RocketChat/Rocket.Chat/pull/40706)) Fixes missing permission check on the `POST /api/v1/fingerprint` endpoint + +- ([#40991](https://github.com/RocketChat/Rocket.Chat/pull/40991)) Fixes rooms failing to open on transient network errors, and prevents a "Room Not Found" flash when opening a room with a local subscription + +- ([#40635](https://github.com/RocketChat/Rocket.Chat/pull/40635) by [@copilot-swe-agent](https://github.com/copilot-swe-agent)) Fixes the Chat Limits locking mechanism to allow bot agents to skip the lock as they aren't limited + +- ([#40839](https://github.com/RocketChat/Rocket.Chat/pull/40839)) Fixes an issue where `description` was incorrectly being used as alternative text for image attachments + +- ([#40954](https://github.com/RocketChat/Rocket.Chat/pull/40954)) Speeds up room opening by removing redundant work in the message history load. On the client, the prefetched first history batch no longer blocks on the message-list DOM before rendering, and the history pager no longer fires an extra `loadHistory` round trip just to reach a full page of visible messages when the latest page contains thread replies. On the server, `loadHistory` reuses the already-fetched room document instead of querying it twice, and runs message normalization and the unread (first-unread + count) queries concurrently instead of sequentially. + +- ([#39273](https://github.com/RocketChat/Rocket.Chat/pull/39273) by [@metaloozee](https://github.com/metaloozee)) Fixes an issue where ui crashes when message attachment fields contains non-string `value` field + +- ([#40955](https://github.com/RocketChat/Rocket.Chat/pull/40955)) Fixes an issue where the channel selection modal would not load when removing a member from a team. + +- ([#40864](https://github.com/RocketChat/Rocket.Chat/pull/40864)) Fixes an issue on `canAccessRoom` where `abacAttributes` were not fetched in some endpoint calls + +-
Updated dependencies [6ae500ab8983b334d0df3e07925b610d0ff9d38c, 4319d3eda1df3cd45b8e2b7b2b193ae9798a9ade, 24dc6ec3cc19c2ef182a07e1f7c5368f67584d5c, 73e12e1707baea845395e0582892f65456598672, a7279cebc73edfa4b991eb593730c08e8f5e9001, ebc9c17b6ba63ee754320eadfba20c024c53c18f, 7380c44c751eff9ee624d80bf26370411ffed78b, a7279cebc73edfa4b991eb593730c08e8f5e9001, 3c47215f4724bf41a56950542feba7c2d5d9eb7f, 25722dbb970665c66d0acfee415650f96e52cd50, f4f361234f00bd44efe348df4355e9d3cf80efe0, 9a36221f1fbf5ca417325204637c9f32fe760443, f57901d91feaccedd00dee65b78775b20235825b, f57901d91feaccedd00dee65b78775b20235825b, f57901d91feaccedd00dee65b78775b20235825b, 9a36221f1fbf5ca417325204637c9f32fe760443, 9a36221f1fbf5ca417325204637c9f32fe760443, fa685d0ddfdf1167705a58b5d846a993144e3734, 6bd9182ae1d914a55e70866db43e8d2038f7be28, f63b965f82b0ddc590c633706f7c31c8c5251b53, 9ab1cf6e088cc099f4fc2ba9460ce5dd41bd1dc2, ff751747f8a0637888364bde42ae18ac92a38768]: + + - @rocket.chat/i18n@3.2.0-rc.0 + - @rocket.chat/apps@0.7.1-rc.0 + - @rocket.chat/model-typings@2.3.1-rc.0 + - @rocket.chat/models@2.3.1-rc.0 + - @rocket.chat/core-services@0.14.2-rc.0 + - @rocket.chat/media-signaling@1.1.0-rc.0 + - @rocket.chat/core-typings@8.6.0-rc.0 + - @rocket.chat/presence@0.3.0-rc.0 + - @rocket.chat/abac@0.3.0-rc.0 + - @rocket.chat/rest-typings@8.6.0-rc.0 + - @rocket.chat/federation-matrix@0.1.5-rc.0 + - @rocket.chat/apps-engine@1.64.0-rc.0 + - @rocket.chat/ui-voip@22.0.0-rc.0 + - @rocket.chat/omnichannel-services@0.3.55-rc.0 + - @rocket.chat/ui-contexts@32.0.0-rc.0 + - @rocket.chat/web-ui-registration@32.0.0-rc.0 + - @rocket.chat/media-calls@0.5.1-rc.0 + - @rocket.chat/omni-core-ee@0.0.23-rc.0 + - @rocket.chat/cron@0.1.58-rc.0 + - @rocket.chat/instance-status@0.1.58-rc.0 + - @rocket.chat/omni-core@0.1.2-rc.0 + - @rocket.chat/server-fetch@0.2.2-rc.0 + - @rocket.chat/ui-client@32.0.0-rc.0 + - @rocket.chat/network-broker@0.2.37-rc.0 + - @rocket.chat/ddp-client@1.1.1-rc.0 + - @rocket.chat/fuselage-ui-kit@32.0.0-rc.0 + - @rocket.chat/ui-composer@3.0.0-rc.0 + - @rocket.chat/gazzodown@32.0.0-rc.0 + - @rocket.chat/ui-avatar@28.0.0-rc.0 + - @rocket.chat/ui-video-conf@32.0.0-rc.0 +
+ +## 8.5.1 + +### Patch Changes + +- Bump @rocket.chat/meteor version. - Bump @rocket.chat/meteor version. - ([#40917](https://github.com/RocketChat/Rocket.Chat/pull/40917) by [@dionisio-bot](https://github.com/dionisio-bot)) Escapes HTML tags in exported data @@ -21,6 +220,7 @@ - @rocket.chat/federation-matrix@0.1.5 - @rocket.chat/core-typings@8.5.1 - @rocket.chat/rest-typings@8.5.1 + ## 8.5.0 diff --git a/apps/meteor/app/2fa/server/code/checkCodeForUser.spec.ts b/apps/meteor/app/2fa/server/code/checkCodeForUser.spec.ts new file mode 100644 index 0000000000000..850c01a5bb90b --- /dev/null +++ b/apps/meteor/app/2fa/server/code/checkCodeForUser.spec.ts @@ -0,0 +1,193 @@ +import { expect } from 'chai'; +import { before, after, beforeEach, describe, it } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const settingsGet = sinon.stub(); +const totpEnabled = sinon.stub().returns(false); + +class MeteorError extends Error { + error: string; + + details?: unknown; + + constructor(error: string, reason?: string, details?: unknown) { + super(reason ?? error); + this.error = error; + this.details = details; + } +} + +// Disabled second-factor methods so the password fallback is the only candidate. +class TOTPCheck { + name = 'totp'; + + isEnabled(): boolean { + return totpEnabled(); + } + + async processInvalidCode(): Promise<{ codeGenerated: boolean }> { + return { codeGenerated: false }; + } +} + +class EmailCheck { + name = 'email'; + + isEnabled(): boolean { + return false; + } +} + +class PasswordCheckFallback { + name = 'password'; + + isEnabled(): boolean { + return true; + } + + async verify(): Promise { + return false; + } + + async processInvalidCode(): Promise<{ codeGenerated: boolean }> { + return { codeGenerated: false }; + } + + async maxFaildedAttemtpsReached(): Promise { + return false; + } +} + +const { checkCodeForUser, getFingerprintFromConnection } = proxyquire.noCallThru().load('./index', { + '@rocket.chat/models': { + Users: { + findOneById: async () => null, + setTwoFactorAuthorizationHashAndUntilForUserIdAndToken: async () => undefined, + }, + }, + 'meteor/accounts-base': { + Accounts: { + _getLoginToken: () => 'token-hash', + }, + }, + 'meteor/meteor': { + Meteor: { Error: MeteorError }, + }, + './TOTPCheck': { TOTPCheck }, + './EmailCheck': { EmailCheck }, + './PasswordCheckFallback': { PasswordCheckFallback }, + '../../../lib/server/functions/getModifiedHttpHeaders': { + normalizeHeaders: (headers: unknown) => headers, + }, + '../../../settings/server': { + settings: { get: settingsGet }, + }, +}); + +const REMEMBER_FOR_SECONDS = 1800; + +const buildUser = (createdAt: Date) => ({ + _id: 'user-id', + createdAt, + services: { + password: { bcrypt: 'hashed' }, + resume: { loginTokens: [{ hashedToken: 'token-hash' }] }, + }, +}); + +const connection = { + id: 'connection-id', + httpHeaders: { 'user-agent': 'agent' }, + clientAddress: '127.0.0.1', +}; + +describe('checkCodeForUser - post-registration grace window', () => { + let originalTestMode: string | undefined; + + before(() => { + originalTestMode = process.env.TEST_MODE; + delete process.env.TEST_MODE; + + settingsGet.callsFake((key: string) => { + switch (key) { + case 'Accounts_TwoFactorAuthentication_Enabled': + return true; + case 'Accounts_TwoFactorAuthentication_RememberFor': + return REMEMBER_FOR_SECONDS; + case 'Accounts_TwoFactorAuthentication_Enforce_Password_Fallback': + return true; + default: + return false; + } + }); + }); + + after(() => { + if (originalTestMode === undefined) { + delete process.env.TEST_MODE; + } else { + process.env.TEST_MODE = originalTestMode; + } + }); + + beforeEach(() => { + totpEnabled.reset(); + totpEnabled.returns(false); + }); + + it('should not prompt a freshly registered user even when disableRememberMe is set (Setup Wizard regression)', async () => { + const user = buildUser(new Date()); + + const result = await checkCodeForUser({ + user, + options: { disableRememberMe: true }, + connection, + }); + + expect(result).to.be.equal(true); + }); + + it('should still prompt a freshly registered user who already has a 2FA method configured', async () => { + totpEnabled.returns(true); + const user = buildUser(new Date()); + + await expect( + checkCodeForUser({ + user, + options: { disableRememberMe: true }, + connection, + }), + ).to.be.rejectedWith('TOTP Required'); + }); + + it('should still prompt when the post-registration grace window has expired and disableRememberMe is set', async () => { + const user = buildUser(new Date(Date.now() - (REMEMBER_FOR_SECONDS + 60) * 1000)); + + await expect( + checkCodeForUser({ + user, + options: { disableRememberMe: true }, + connection, + }), + ).to.be.rejectedWith('TOTP Required'); + }); + + it('should ignore a remembered authorization when disableRememberMe is set and grace window expired', async () => { + const user = buildUser(new Date(Date.now() - (REMEMBER_FOR_SECONDS + 60) * 1000)); + // A previously remembered 2FA authorization that is still valid in time. + user.services.resume.loginTokens[0] = { + hashedToken: 'token-hash', + twoFactorAuthorizedUntil: new Date(Date.now() + 60 * 1000), + twoFactorAuthorizedHash: getFingerprintFromConnection(connection), + } as (typeof user.services.resume.loginTokens)[number]; + + await expect( + checkCodeForUser({ + user, + options: { disableRememberMe: true }, + connection, + }), + ).to.be.rejectedWith('TOTP Required'); + }); +}); diff --git a/apps/meteor/app/2fa/server/code/index.spec.ts b/apps/meteor/app/2fa/server/code/index.spec.ts new file mode 100644 index 0000000000000..3cdfec39ed3dd --- /dev/null +++ b/apps/meteor/app/2fa/server/code/index.spec.ts @@ -0,0 +1,135 @@ +import { expect } from 'chai'; +import { afterEach, beforeEach, describe, it } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const settingsMock = sinon.stub(); +const getLoginTokenStub = sinon.stub(); + +class TOTPCheckMock { + name = 'totp'; + + isEnabled() { + return true; + } + + async processInvalidCode() { + return {}; + } + + async verify() { + return false; + } + + async maxFaildedAttemtpsReached() { + return false; + } +} + +class DisabledCheckMock { + name = 'email'; + + isEnabled() { + return false; + } +} + +class MeteorErrorMock extends Error { + error: string; + + details: unknown; + + constructor(error: string, reason?: string, details?: unknown) { + super(reason); + this.error = error; + this.details = details; + } +} + +const { checkCodeForUser } = proxyquire.noCallThru().load('./index', { + './TOTPCheck': { TOTPCheck: TOTPCheckMock }, + './EmailCheck': { EmailCheck: DisabledCheckMock }, + './PasswordCheckFallback': { PasswordCheckFallback: class extends DisabledCheckMock {} }, + '../../../lib/server/functions/getModifiedHttpHeaders': { normalizeHeaders: (headers: unknown) => headers }, + '../../../settings/server': { settings: { get: settingsMock } }, + '@rocket.chat/models': { + Users: { + findOneById: async () => null, + setTwoFactorAuthorizationHashAndUntilForUserIdAndToken: async () => undefined, + }, + }, + 'meteor/accounts-base': { Accounts: { _getLoginToken: getLoginTokenStub } }, + 'meteor/meteor': { Meteor: { Error: MeteorErrorMock } }, +}); + +const HASHED_TOKEN = 'hashed-login-token'; + +const userWithBypassToken = { + _id: 'user-id', + services: { resume: { loginTokens: [{ hashedToken: HASHED_TOKEN, bypassTwoFactor: true }] } }, +} as any; + +const connection = { + id: 'connection-id', + clientAddress: '127.0.0.1', + httpHeaders: {}, +} as any; + +describe('checkCodeForUser - bypassTwoFactor token resolution (SUP-1064)', () => { + let testMode: string | undefined; + + beforeEach(() => { + // TEST_MODE short-circuits the whole check; remove it so the token resolution path runs. + testMode = process.env.TEST_MODE; + delete process.env.TEST_MODE; + + settingsMock.reset(); + settingsMock.withArgs('Accounts_TwoFactorAuthentication_Enabled').returns(true); + getLoginTokenStub.reset(); + }); + + afterEach(() => { + if (testMode !== undefined) { + process.env.TEST_MODE = testMode; + } + }); + + it('should honor a bypassTwoFactor token resolved from the REST connection (connection.token)', async () => { + // REST: the token is not registered in account data, only carried on the connection. + getLoginTokenStub.returns(undefined); + + const authorized = await checkCodeForUser({ + user: userWithBypassToken, + connection: { ...connection, token: HASHED_TOKEN }, + options: {}, + }); + + expect(authorized).to.be.equal(true); + }); + + it('should honor a bypassTwoFactor token resolved from account data (DDP, _getLoginToken)', async () => { + // DDP: the token is registered in Accounts._accountData and read via _getLoginToken. + getLoginTokenStub.returns(HASHED_TOKEN); + + const authorized = await checkCodeForUser({ + user: userWithBypassToken, + connection: { ...connection, token: undefined }, + options: {}, + }); + + expect(authorized).to.be.equal(true); + }); + + it('should still require a second factor when the token cannot be resolved from either source', async () => { + // Regression guard: this is the buggy state (#38017) the fix addresses. + getLoginTokenStub.returns(undefined); + + await expect( + checkCodeForUser({ + user: userWithBypassToken, + connection: { ...connection, token: undefined }, + options: {}, + }), + ).to.be.rejectedWith('TOTP Required'); + }); +}); diff --git a/apps/meteor/app/2fa/server/code/index.ts b/apps/meteor/app/2fa/server/code/index.ts index 8242b1c66185e..cbba0724214f3 100644 --- a/apps/meteor/app/2fa/server/code/index.ts +++ b/apps/meteor/app/2fa/server/code/index.ts @@ -54,7 +54,7 @@ export async function getUserForCheck(userId: string): Promise { }); } -function getFingerprintFromConnection(connection: IMethodConnection): string { +export function getFingerprintFromConnection(connection: IMethodConnection): string { const data = JSON.stringify({ userAgent: connection.httpHeaders['user-agent'], clientAddress: connection.clientAddress, @@ -77,7 +77,11 @@ function getRememberDate(from: Date = new Date()): Date | undefined { } function isAuthorizedForToken(connection: IMethodConnection, user: IUser, options: ITwoFactorOptions): boolean { - const currentToken = Accounts._getLoginToken(connection.id); + // Resolve the current login token from both transports: + // - DDP: the login flow registers it in `Accounts._accountData`, read via `_getLoginToken`. + // - REST: it is not registered in account data, so it is carried on `connection.token`. + // Both are needed; REST is the fallback that fixes `bypassTwoFactor` PATs (SUP-1064). + const currentToken = Accounts._getLoginToken(connection.id) || connection.token; const tokenObject = user.services?.resume?.loginTokens?.find((i) => i.hashedToken === currentToken); if (!tokenObject) { @@ -93,14 +97,18 @@ function isAuthorizedForToken(connection: IMethodConnection, user: IUser, option return true; } - if (options.disableRememberMe === true) { - return false; + // Skip 2FA for a freshly registered user who has not set up any 2FA method yet, + // until the grace period that starts at registration expires. + // (e.g. the Setup Wizard saving settings between steps before any 2FA is configured) + const rememberPeriodEnd = user.createdAt && getRememberDate(user.createdAt); + const isWithinRememberPeriod = rememberPeriodEnd && rememberPeriodEnd >= new Date(); + const hasNoTwoFactorMethod = getAvailableMethodNames(user).length === 0; + if (isWithinRememberPeriod && hasNoTwoFactorMethod) { + return true; } - // remember user right after their registration - const rememberAfterRegistration = user.createdAt && getRememberDate(user.createdAt); - if (rememberAfterRegistration && rememberAfterRegistration >= new Date()) { - return true; + if (options.disableRememberMe === true) { + return false; } if (!tokenObject.twoFactorAuthorizedUntil || !tokenObject.twoFactorAuthorizedHash) { @@ -119,7 +127,9 @@ function isAuthorizedForToken(connection: IMethodConnection, user: IUser, option } async function rememberAuthorization(connection: IMethodConnection, user: IUser): Promise { - const currentToken = Accounts._getLoginToken(connection.id); + // Same dual-transport resolution as `isAuthorizedForToken`: DDP reads from `Accounts._accountData` + // via `_getLoginToken`, REST falls back to the token carried on `connection.token`. + const currentToken = Accounts._getLoginToken(connection.id) || connection.token; const expires = getRememberDate(); if (!expires) { diff --git a/apps/meteor/app/api/server/ApiClass.ts b/apps/meteor/app/api/server/ApiClass.ts index d51726049e0dd..63caf9ba1edaf 100644 --- a/apps/meteor/app/api/server/ApiClass.ts +++ b/apps/meteor/app/api/server/ApiClass.ts @@ -346,6 +346,10 @@ export class APIClass; + + public internalError(msg: T): InternalError; + public internalError(msg?: T): InternalError { return { statusCode: 500, diff --git a/apps/meteor/app/api/server/definition.ts b/apps/meteor/app/api/server/definition.ts index c728de5b16be5..62e11c9af3745 100644 --- a/apps/meteor/app/api/server/definition.ts +++ b/apps/meteor/app/api/server/definition.ts @@ -362,7 +362,7 @@ type Results = { : K extends 404 ? NotFoundResult> : K extends ErrorStatusCodes - ? InternalError, K> + ? InternalError, K> : never; }[keyof TResponse] & { headers?: Record; diff --git a/apps/meteor/app/api/server/helpers/getUserFromParams.ts b/apps/meteor/app/api/server/helpers/getUserFromParams.ts index 984658371da2c..1ec636c963003 100644 --- a/apps/meteor/app/api/server/helpers/getUserFromParams.ts +++ b/apps/meteor/app/api/server/helpers/getUserFromParams.ts @@ -1,6 +1,7 @@ // Convenience method, almost need to turn it into a middleware of sorts import type { IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; +import { isTruthy } from '@rocket.chat/tools'; import { Meteor } from 'meteor/meteor'; export async function getUserFromParams( @@ -10,10 +11,16 @@ export async function getUserFromParams( user?: string; }, full?: T, -): Promise> { +): Promise< + T extends true + ? IUser + : Pick +> { let user; - const projection = full ? {} : { username: 1, name: 1, status: 1, statusText: 1, roles: 1 }; + const projection = full + ? {} + : { username: 1, name: 1, status: 1, statusDefault: 1, statusText: 1, statusSource: 1, statusExpiresAt: 1, roles: 1 }; if (params.userId?.trim()) { user = await Users.findOneById(params.userId, { projection }); } else if (params.username?.trim()) { @@ -57,3 +64,35 @@ export async function getUserListFromParams(params: { return Users.findByUsernamesIgnoringCase(userListParam, { projection: { username: 1 } }).toArray(); } + +/** + * Resolves a list of usernames from the request params without requiring the users to + * already exist locally. `username`/`usernames`/`user` are passed through verbatim, while + * `userId`/`userIds` are resolved to their usernames via the database. + * + * Unlike `getUserListFromParams`, this does not drop usernames that have no local record yet + * — which is what federation invites rely on: the federated user record is created lazily + * inside `addUsersToRoomMethod`. + */ +export async function getUsernameListFromParams(params: { + userId?: string; + username?: string; + user?: string; + userIds?: string[]; + usernames?: string[]; +}): Promise { + const usernames = [...(params.usernames || []), params.username, params.user].filter(isTruthy); + const userIds = [...(params.userIds || []), params.userId].filter(isTruthy); + + const usernamesFromIds = userIds.length + ? (await Users.findByIds(userIds, { projection: { username: 1 } }).toArray()).map((u) => u.username).filter(isTruthy) + : []; + + const all = [...new Set([...usernames, ...usernamesFromIds])]; + + if (!all.length) { + throw new Meteor.Error('error-users-params-not-provided', 'Please provide "userId" or "username" or "userIds" or "usernames" as param'); + } + + return all; +} diff --git a/apps/meteor/app/api/server/lib/getServerInfo.ts b/apps/meteor/app/api/server/lib/getServerInfo.ts index 4c65847c86ae6..adaf6ec6998c5 100644 --- a/apps/meteor/app/api/server/lib/getServerInfo.ts +++ b/apps/meteor/app/api/server/lib/getServerInfo.ts @@ -1,4 +1,5 @@ import type { IWorkspaceInfo } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { getTrimmedServerVersion } from './getTrimmedServerVersion'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; @@ -15,6 +16,8 @@ export async function getServerInfo(userId?: string): Promise { const cloudWorkspaceId = settings.get('Cloud_Workspace_Id'); return { + workspaceUrl: License.getWorkspaceUrl(), + hashedWorkspaceUrl: License.getHashedWorkspaceUrl(), version: getTrimmedServerVersion(), ...(hasPermissionToViewStatistics && { info: { diff --git a/apps/meteor/app/api/server/lib/rooms.ts b/apps/meteor/app/api/server/lib/rooms.ts index 216e17a42422e..f93551e4e9c08 100644 --- a/apps/meteor/app/api/server/lib/rooms.ts +++ b/apps/meteor/app/api/server/lib/rooms.ts @@ -1,7 +1,8 @@ -import type { IRoom, ISubscription, RoomAdminFieldsType, RoomType } from '@rocket.chat/core-typings'; +import type { IRoom, IRoomAbacRedaction, ISubscription, RoomAdminFieldsType, RoomType } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions } from '@rocket.chat/models'; import type { FindOptions, Sort } from 'mongodb'; +import { scopeAdminRoomsForAbac } from './scopeAdminRoomsForAbac'; import { adminFields } from '../../../../lib/rooms/adminFields'; import { hasAtLeastOnePermissionAsync, hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { stripABACManagedFieldsForAdmin } from '../../../authorization/server/lib/isABACManagedRoom'; @@ -17,7 +18,7 @@ export async function findAdminRooms({ types: Array; pagination: { offset: number; count: number; sort: Sort }; }): Promise<{ - rooms: IRoom[]; + rooms: Array & IRoomAbacRedaction>; count: number; offset: number; total: number; @@ -49,14 +50,20 @@ export async function findAdminRooms({ ]); return { - rooms, + rooms: await scopeAdminRoomsForAbac(rooms, uid), count: rooms.length, offset, total, }; } -export async function findAdminRoom({ uid, rid }: { uid: string; rid: string }): Promise | null> { +export async function findAdminRoom({ + uid, + rid, +}: { + uid: string; + rid: string; +}): Promise<(Pick & IRoomAbacRedaction) | null> { if (!(await hasPermissionAsync(uid, 'view-room-administration'))) { throw new Error('error-not-authorized'); } @@ -65,7 +72,9 @@ export async function findAdminRoom({ uid, rid }: { uid: string; rid: string }): if (!room) { return null; } - return stripABACManagedFieldsForAdmin(room); + + const [scoped] = await scopeAdminRoomsForAbac([stripABACManagedFieldsForAdmin(room)], uid); + return scoped ?? null; } export async function findChannelAndPrivateAutocomplete({ uid, selector }: { uid: string; selector: { name: string } }): Promise<{ diff --git a/apps/meteor/app/api/server/lib/scopeAdminRoomsForAbac.ts b/apps/meteor/app/api/server/lib/scopeAdminRoomsForAbac.ts new file mode 100644 index 0000000000000..d87d7ce6351ad --- /dev/null +++ b/apps/meteor/app/api/server/lib/scopeAdminRoomsForAbac.ts @@ -0,0 +1,7 @@ +import type { IRoom, IRoomAbacRedaction, RoomAdminFieldsType } from '@rocket.chat/core-typings'; +import { makeFunction } from '@rocket.chat/patch-injection'; + +export const scopeAdminRoomsForAbac = makeFunction( + async (rooms: Pick[], _uid: string): Promise & IRoomAbacRedaction>> => + rooms, +); diff --git a/apps/meteor/app/api/server/v1/call-history.ts b/apps/meteor/app/api/server/v1/call-history.ts index b14c5621e9149..114cda89ed049 100644 --- a/apps/meteor/app/api/server/v1/call-history.ts +++ b/apps/meteor/app/api/server/v1/call-history.ts @@ -1,3 +1,4 @@ +import { CallHistory as CallHistoryService } from '@rocket.chat/core-services'; import type { CallHistoryItem, CallHistoryItemState, IMediaCall } from '@rocket.chat/core-typings'; import { CallHistory, MediaCalls } from '@rocket.chat/models'; import type { PaginatedRequest, PaginatedResult } from '@rocket.chat/rest-typings'; @@ -9,7 +10,6 @@ import { validateUnauthorizedErrorResponse, validateForbiddenErrorResponse, } from '@rocket.chat/rest-typings'; -import { escapeRegExp } from '@rocket.chat/string-helpers'; import { ensureArray } from '../../../../lib/utils/arrayUtils'; import type { ExtractRoutesFromAPI } from '../ApiClass'; @@ -104,37 +104,17 @@ const callHistoryListEndpoints = API.v1.get( const { direction, state, filter } = this.queryParams; - const filterText = typeof filter === 'string' && filter.trim(); - - const stateFilter = state && ensureArray(state); - const query = { - uid: this.userId, - ...(direction && { direction }), - ...(stateFilter?.length && { state: { $in: stateFilter } }), - ...(filterText && { - $or: [ - { - external: false, - contactName: { $regex: escapeRegExp(filterText), $options: 'i' }, - }, - { - external: false, - contactUsername: { $regex: escapeRegExp(filterText), $options: 'i' }, - }, - { - external: true, - contactExtension: { $regex: escapeRegExp(filterText), $options: 'i' }, - }, - ], - }), - }; + const searchTerm = typeof filter === 'string' && filter.trim(); - const { cursor, totalCount } = CallHistory.findPaginated(query, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - }); - const [items, total] = await Promise.all([cursor.toArray(), totalCount]); + const { items, total } = await CallHistoryService.search( + this.userId, + { + ...(searchTerm && { searchTerm }), + ...(direction && { direction }), + ...(state && { inStates: ensureArray(state) }), + }, + { count, offset, sort }, + ); return API.v1.success({ items, diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index 84cd4ebbf03fc..44be9f89b54cc 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -1,5 +1,13 @@ import { Team, Room } from '@rocket.chat/core-services'; -import { TeamType, type IRoom, type ISubscription, type IUser, type RoomType, type UserStatus } from '@rocket.chat/core-typings'; +import { + TeamType, + isRoomNativeFederated, + type IRoom, + type ISubscription, + type IUser, + type RoomType, + type UserStatus, +} from '@rocket.chat/core-typings'; import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models'; import { isChannelsAddAllProps, @@ -55,7 +63,7 @@ import { API } from '../api'; import { addUserToFileObj } from '../helpers/addUserToFileObj'; import { composeRoomWithLastMessage } from '../helpers/composeRoomWithLastMessage'; import { getPaginationItems } from '../helpers/getPaginationItems'; -import { getUserFromParams, getUserListFromParams } from '../helpers/getUserFromParams'; +import { getUserFromParams, getUserListFromParams, getUsernameListFromParams } from '../helpers/getUserFromParams'; // Returns the channel IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property async function findChannelByIdOrName({ @@ -945,6 +953,18 @@ API.v1.addRoute( async post() { const findResult = await findChannelByIdOrName({ params: this.bodyParams }); + // Federated rooms invite by raw username: the federated user record is created + // lazily inside addUsersToRoomMethod, so we must not require it to exist locally yet. + if (isRoomNativeFederated(findResult)) { + const users = await getUsernameListFromParams(this.bodyParams); + + await addUsersToRoomMethod(this.userId, { rid: findResult._id, users }, this.user); + + return API.v1.success({ + channel: await findChannelByIdOrName({ params: this.bodyParams, userId: this.userId }), + }); + } + const users = await getUserListFromParams(this.bodyParams); if (!users.length) { diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 383f4312c53b5..62378bd436e7a 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -521,7 +521,7 @@ const chatEndpoints = API.v1 authRequired: true, body: isChatDeleteProps, response: { - 200: ajv.compile<{ _id: string; ts: string; message: Pick }>({ + 200: ajv.compile<{ _id?: string; ts?: string; message?: Pick }>({ type: 'object', properties: { _id: { type: 'string' }, @@ -538,7 +538,7 @@ const chatEndpoints = API.v1 }, success: { type: 'boolean', enum: [true] }, }, - required: ['_id', 'ts', 'message', 'success'], + required: ['success'], additionalProperties: false, }), 400: validateBadRequestErrorResponse, @@ -546,13 +546,22 @@ const chatEndpoints = API.v1 }, }, async function action() { - const msg = await Messages.findOneById(this.bodyParams.msgId, { projection: { u: 1, rid: 1 } }); + // Deleting by fileId resolves the message that references the file and deletes it. + // An orphan upload (a file with no associated message) is not deletable through this + // endpoint and intentionally returns a failure below. + const msg = + 'fileId' in this.bodyParams + ? await Messages.getMessageByFileId(this.bodyParams.fileId) + : await Messages.findOneById(this.bodyParams.msgId, { projection: { u: 1, rid: 1 } }); if (!msg) { + if ('fileId' in this.bodyParams) { + return API.v1.failure(`No message found with the file id: "${this.bodyParams.fileId}".`); + } return API.v1.failure(`No message found with the id of "${this.bodyParams.msgId}".`); } - if (this.bodyParams.roomId !== msg.rid) { + if ('roomId' in this.bodyParams && this.bodyParams.roomId !== msg.rid) { return API.v1.failure('The room id provided does not match where the message is from.'); } @@ -576,7 +585,7 @@ const chatEndpoints = API.v1 return API.v1.success({ _id: msg._id, ts: Date.now().toString(), - message: msg, + message: { _id: msg._id, rid: msg.rid, u: msg.u }, }); }, ) diff --git a/apps/meteor/app/api/server/v1/custom-sounds.ts b/apps/meteor/app/api/server/v1/custom-sounds.ts index b74a396c5ee58..6b68b260ae7f6 100644 --- a/apps/meteor/app/api/server/v1/custom-sounds.ts +++ b/apps/meteor/app/api/server/v1/custom-sounds.ts @@ -5,17 +5,20 @@ import { isCustomSoundsGetOneProps, isCustomSoundsListProps, isCustomSoundsCreateProps, + isCustomSoundsDeleteProps, isCustomSoundsUpdateProps, ajv, validateBadRequestErrorResponse, validateNotFoundErrorResponse, validateForbiddenErrorResponse, validateUnauthorizedErrorResponse, + validateInternalErrorResponse, } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { MAX_CUSTOM_SOUND_SIZE_BYTES, CUSTOM_SOUND_ALLOWED_MIME_TYPES } from '../../../../lib/constants'; import { SystemLogger } from '../../../../server/lib/logger/system'; +import { deleteCustomSound } from '../../../custom-sounds/server/lib/deleteCustomSound'; import { insertOrUpdateSound } from '../../../custom-sounds/server/lib/insertOrUpdateSound'; import { uploadCustomSound } from '../../../custom-sounds/server/lib/uploadCustomSound'; import { getExtension, getMimeTypeFromFileName } from '../../../utils/lib/mimeTypes'; @@ -58,6 +61,18 @@ const updateCustomSoundsResponse = ajv.compile<{ success: boolean }>({ required: ['success'], }); +const deleteCustomSoundsResponse = ajv.compile({ + additionalProperties: false, + type: 'object', + properties: { + success: { + type: 'boolean', + description: 'Indicates if the request was successful.', + }, + }, + required: ['success'], +}); + const customSoundsEndpoints = API.v1 .get( 'custom-sounds.list', @@ -280,6 +295,39 @@ const customSoundsEndpoints = API.v1 return API.v1.failure(error instanceof Error ? error.message : 'Unknown error'); } }, + ) + .post( + 'custom-sounds.delete', + { + response: { + 200: deleteCustomSoundsResponse, + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + 404: validateNotFoundErrorResponse, + 500: validateInternalErrorResponse, + }, + authRequired: true, + body: isCustomSoundsDeleteProps, + permissionsRequired: ['manage-sounds'], + }, + async function action() { + const { _id } = this.bodyParams; + + try { + await deleteCustomSound(_id); + + return API.v1.success(); + } catch (error: unknown) { + this.logger.error({ error }); + + if (error instanceof Meteor.Error && error.error === 'Custom_Sound_Error_Invalid_Sound') { + return API.v1.failure(error.error); + } + + return API.v1.internalError(); + } + }, ); export type CustomSoundEndpoints = ExtractRoutesFromAPI; diff --git a/apps/meteor/app/api/server/v1/e2e.ts b/apps/meteor/app/api/server/v1/e2e.ts index 489a9e27a5953..a864d2cfcaba1 100644 --- a/apps/meteor/app/api/server/v1/e2e.ts +++ b/apps/meteor/app/api/server/v1/e2e.ts @@ -16,6 +16,7 @@ import { handleSuggestedGroupKey } from '../../../e2e/server/functions/handleSug import { provideUsersSuggestedGroupKeys } from '../../../e2e/server/functions/provideUsersSuggestedGroupKeys'; import { resetRoomKey } from '../../../e2e/server/functions/resetRoomKey'; import { getUsersOfRoomWithoutKeyMethod } from '../../../e2e/server/methods/getUsersOfRoomWithoutKey'; +import { requestSubscriptionKeysMethod } from '../../../e2e/server/methods/requestSubscriptionKeys'; import { setRoomKeyIDMethod } from '../../../e2e/server/methods/setRoomKeyID'; import { setUserPublicAndPrivateKeysMethod } from '../../../e2e/server/methods/setUserPublicAndPrivateKeys'; import { updateGroupKey } from '../../../e2e/server/methods/updateGroupKey'; @@ -196,6 +197,24 @@ const e2eEndpoints = API.v1 return API.v1.success(); }, ) + .post( + 'e2e.requestSubscriptionKeys', + { + authRequired: true, + response: { + 401: validateUnauthorizedErrorResponse, + 200: ajv.compile({ + type: 'object', + }), + }, + }, + + async function action() { + await requestSubscriptionKeysMethod(this.userId); + + return API.v1.success(); + }, + ) .get( 'e2e.fetchMyKeys', { diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index dba48f1a9ffa9..9916a44a6323d 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -1,5 +1,12 @@ import { Team, isMeteorError } from '@rocket.chat/core-services'; -import type { IIntegration, IUser, IRoom, RoomType, UserStatus } from '@rocket.chat/core-typings'; +import { + isRoomNativeFederated, + type IIntegration, + type IUser, + type IRoom, + type RoomType, + type UserStatus, +} from '@rocket.chat/core-typings'; import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models'; import { isGroupsOnlineProps, isGroupsMessagesProps, isGroupsFilesProps } from '@rocket.chat/rest-typings'; import { isTruthy } from '@rocket.chat/tools'; @@ -35,7 +42,7 @@ import { API } from '../api'; import { addUserToFileObj } from '../helpers/addUserToFileObj'; import { composeRoomWithLastMessage } from '../helpers/composeRoomWithLastMessage'; import { getPaginationItems } from '../helpers/getPaginationItems'; -import { getUserFromParams, getUserListFromParams } from '../helpers/getUserFromParams'; +import { getUserFromParams, getUserListFromParams, getUsernameListFromParams } from '../helpers/getUserFromParams'; async function getRoomFromParams(params: { roomId?: string } | { roomName?: string }): Promise { if ( @@ -583,19 +590,28 @@ API.v1.addRoute( throw new Meteor.Error('error-room-param-not-provided', 'The parameter "roomId" or "roomName" is required'); } - const { _id: rid, t: type } = (await Rooms.findOneByIdOrName(idOrName)) || {}; + const groupRoom = await Rooms.findOneByIdOrName(idOrName); + const { _id: rid, t: type } = groupRoom || {}; if (!rid || type !== 'p') { throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group'); } - const users = await getUserListFromParams(this.bodyParams); + // Federated rooms invite by raw username: the federated user record is created + // lazily inside addUsersToRoomMethod, so we must not require it to exist locally yet. + if (groupRoom && isRoomNativeFederated(groupRoom)) { + const usernames = await getUsernameListFromParams(this.bodyParams); - if (!users.length) { - throw new Meteor.Error('error-empty-invite-list', 'Cannot invite if no valid users are provided'); - } + await addUsersToRoomMethod(this.userId, { rid, users: usernames }, this.user); + } else { + const users = await getUserListFromParams(this.bodyParams); + + if (!users.length) { + throw new Meteor.Error('error-empty-invite-list', 'Cannot invite if no valid users are provided'); + } - await addUsersToRoomMethod(this.userId, { rid, users: users.map((u) => u.username).filter(isTruthy) }, this.user); + await addUsersToRoomMethod(this.userId, { rid, users: users.map((u) => u.username).filter(isTruthy) }, this.user); + } const room = await Rooms.findOneById(rid, { projection: API.v1.defaultFieldsToExclude }); diff --git a/apps/meteor/app/api/server/v1/im.ts b/apps/meteor/app/api/server/v1/im.ts index 1cbcf3237e3ac..826f181d4419a 100644 --- a/apps/meteor/app/api/server/v1/im.ts +++ b/apps/meteor/app/api/server/v1/im.ts @@ -9,6 +9,7 @@ import { validateUnauthorizedErrorResponse, validateForbiddenErrorResponse, validateBadRequestErrorResponse, + isDmBlockUserProps, isDmFileProps, isDmMemberProps, isDmMessagesProps, @@ -26,7 +27,9 @@ import { hideRoomMethod } from '../../../../server/methods/hideRoom'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { saveRoomSettings } from '../../../channel-settings/server/methods/saveRoomSettings'; +import { blockUserMethod } from '../../../lib/server/functions/blockUser'; import { getRoomByNameOrIdWithOptionToJoin } from '../../../lib/server/functions/getRoomByNameOrIdWithOptionToJoin'; +import { unblockUserMethod } from '../../../lib/server/functions/unblockUser'; import { getChannelHistory } from '../../../lib/server/methods/getChannelHistory'; import { settings } from '../../../settings/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; @@ -924,6 +927,42 @@ const dmCreateAction = (_path: Path): TypedAction({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [true] }, + }, + required: ['success'], + additionalProperties: false, + }), + }, +} as const; + +const dmBlockUserAction = (_path: Path): TypedAction => + async function action() { + const { roomId, block } = this.bodyParams; + const { room } = await findDirectMessageRoom({ roomId }, this.userId); + + const blocked = room.uids?.find((uid) => uid !== this.userId); + if (!blocked) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'im.blockUser' }); + } + + if (block) { + await blockUserMethod(this.userId, { rid: room._id, blocked }); + } else { + await unblockUserMethod(this.userId, { rid: room._id, blocked }); + } + + return API.v1.success(); + }; + const dmEndpoints = API.v1 .post('im.delete', dmDeleteEndpointsProps, dmDeleteAction('im.delete')) .post('dm.delete', dmDeleteEndpointsProps, dmDeleteAction('dm.delete')) @@ -950,7 +989,8 @@ const dmEndpoints = API.v1 .get('dm.list', dmListEndpointsProps, dmListAction('dm.list')) .get('im.list', dmListEndpointsProps, dmListAction('im.list')) .get('dm.list.everyone', dmListEveryoneEndpointsProps, dmListEveryoneAction('dm.list.everyone')) - .get('im.list.everyone', dmListEveryoneEndpointsProps, dmListEveryoneAction('im.list.everyone')); + .get('im.list.everyone', dmListEveryoneEndpointsProps, dmListEveryoneAction('im.list.everyone')) + .post('im.blockUser', dmBlockUserEndpointsProps, dmBlockUserAction('im.blockUser')); export type DmEndpoints = ExtractRoutesFromAPI; diff --git a/apps/meteor/app/api/server/v1/invites.ts b/apps/meteor/app/api/server/v1/invites.ts index 0e0ea055160ee..86a971a1b2d3d 100644 --- a/apps/meteor/app/api/server/v1/invites.ts +++ b/apps/meteor/app/api/server/v1/invites.ts @@ -250,6 +250,7 @@ const invites = API.v1 }, async function action() { const { token } = this.bodyParams; + // eslint-disable-next-line react-hooks/rules-of-hooks return API.v1.success(await useInviteToken(this.userId, token)); }, ) diff --git a/apps/meteor/app/api/server/v1/misc.ts b/apps/meteor/app/api/server/v1/misc.ts index d7b0be701a3f5..44ebb96478b23 100644 --- a/apps/meteor/app/api/server/v1/misc.ts +++ b/apps/meteor/app/api/server/v1/misc.ts @@ -6,6 +6,8 @@ import { ajv, isShieldSvgProps, isSpotlightProps, + parseSpotlightUsernames, + parseSpotlightType, isDirectoryProps, isFingerprintProps, isMeteorCall, @@ -339,7 +341,11 @@ API.v1.get( ); const spotlightResponseSchema = ajv.compile<{ - users: Pick[]; + users: (Pick & + Partial> & { + nickname?: string; + outside?: boolean; + })[]; rooms: Pick[]; }>({ type: 'object', @@ -356,7 +362,7 @@ const spotlightResponseSchema = ajv.compile<{ statusText: { type: 'string' }, avatarETag: { type: 'string' }, }, - required: ['_id', 'name', 'username', 'status'], + required: ['_id', 'name', 'username'], additionalProperties: true, }, }, @@ -383,7 +389,10 @@ const spotlightResponseSchema = ajv.compile<{ API.v1.get( 'spotlight', { - authRequired: true, + // DDP `spotlight` accepts anonymous calls (Accounts_AllowAnonymousRead). + // Keep parity so anonymous-user / embedded-layout flows can still + // resolve a public channel through the navbar search. + authRequired: false, query: isSpotlightProps, response: { 200: spotlightResponseSchema, @@ -392,9 +401,15 @@ API.v1.get( }, }, async function action() { - const { query } = this.queryParams; + const { query, usernames, type, rid } = this.queryParams; - const result = await spotlightMethod({ text: query, userId: this.userId }); + const result = await spotlightMethod({ + text: query, + userId: this.userId, + usernames: parseSpotlightUsernames(usernames), + type: parseSpotlightType(type), + rid, + }); return API.v1.success(result); }, diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index d927487d9b868..1cf83d741bdb2 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -1,6 +1,7 @@ -import { FederationMatrix, MeteorError, Team } from '@rocket.chat/core-services'; +import { FederationMatrix, MeteorError, Room, Team } from '@rocket.chat/core-services'; import { type IRoom, + type IRoomAbacRedaction, type IUpload, type RequiredField, type RoomAdminFieldsType, @@ -23,6 +24,7 @@ import { isRoomsIsMemberProps, isRoomsCleanHistoryProps, isRoomsOpenProps, + isRoomsJoinProps, isRoomsMembersOrderedByRoleProps, isRoomsChangeArchivationStateProps, isRoomsHideProps, @@ -89,6 +91,7 @@ import { findChannelAndPrivateAutocompleteWithPagination, findRoomsAvailableForTeams, } from '../lib/rooms'; +import { scopeAdminRoomsForAbac } from '../lib/scopeAdminRoomsForAbac'; export async function findRoomByIdOrName({ params, @@ -532,7 +535,7 @@ API.v1.get( }, }, async function action() { - const room = await findRoomByIdOrName({ params: this.queryParams }); + const room = await findRoomByIdOrName({ params: this.queryParams, checkedArchived: false }); const { fields } = await this.parseJsonQuery(); if (!room || !(await canAccessRoomAsync(room, { _id: this.userId }))) { @@ -712,10 +715,15 @@ API.v1.get( authRequired: true, query: isRoomsAdminRoomsProps, response: { - 200: ajv.compile<{ rooms: IRoom[]; count: number; offset: number; total: number }>({ + 200: ajv.compile<{ + rooms: Array & IRoomAbacRedaction>; + count: number; + offset: number; + total: number; + }>({ type: 'object', properties: { - rooms: { type: 'array', items: { type: 'object' } }, // relaxed: IRoom with admin fields + rooms: { type: 'array', items: { type: 'object' } }, // relaxed: IRoom with admin fields + optional ABAC redaction count: { type: 'number' }, offset: { type: 'number' }, total: { type: 'number' }, @@ -785,10 +793,17 @@ API.v1.get( authRequired: true, query: isRoomsAdminRoomsGetRoomProps, response: { - 200: ajv.compile>({ + 200: ajv.compile & IRoomAbacRedaction>({ allOf: [ { $ref: '#/components/schemas/IRoomAdmin' }, - { type: 'object', properties: { success: { type: 'boolean', enum: [true] } }, required: ['success'] }, + { + type: 'object', + properties: { + success: { type: 'boolean', enum: [true] }, + abacAttributesRedacted: { type: 'boolean' }, + }, + required: ['success'], + }, ], }), 400: validateBadRequestErrorResponse, @@ -1237,6 +1252,37 @@ API.v1.post( }, ); +API.v1.post( + 'rooms.join', + { + authRequired: true, + body: isRoomsJoinProps, + response: { + 200: ajv.compile<{ room: IRoom }>({ + type: 'object', + properties: { + room: { type: 'object' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['room', 'success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { + const { joinCode, ...params } = this.bodyParams; + const room = await findRoomByIdOrName({ params }); + + await Room.join({ room, user: this.user, joinCode }); + + return API.v1.success({ + room: await findRoomByIdOrName({ params }), + }); + }, +); + API.v1.post( 'rooms.hide', { @@ -1447,7 +1493,7 @@ export const roomEndpoints = API.v1 401: validateUnauthorizedErrorResponse, 403: validateUnauthorizedErrorResponse, 200: ajv.compile<{ - rooms: IRoom[]; + rooms: Array & IRoomAbacRedaction>; count: number; offset: number; total: number; @@ -1485,7 +1531,7 @@ export const roomEndpoints = API.v1 const [rooms, total] = await Promise.all([cursor.map(stripABACManagedFieldsForAdmin).toArray(), totalCount]); return API.v1.success({ - rooms, + rooms: await scopeAdminRoomsForAbac(rooms, this.userId), count: rooms.length, offset, total, diff --git a/apps/meteor/app/api/server/v1/settings.ts b/apps/meteor/app/api/server/v1/settings.ts index 97e4a51d3133c..6c764f4105bef 100644 --- a/apps/meteor/app/api/server/v1/settings.ts +++ b/apps/meteor/app/api/server/v1/settings.ts @@ -15,8 +15,10 @@ import { isSettingsUpdatePropsColor, isSettingsPublicWithPaginationProps, isSettingsGetParams, + isSettingsBulkProps, validateForbiddenErrorResponse, validateUnauthorizedErrorResponse, + validateBadRequestErrorResponse, } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; import type { FindOptions } from 'mongodb'; @@ -25,6 +27,7 @@ import _ from 'underscore'; import { updateAuditedByUser } from '../../../../server/settings/lib/auditedSettingUpdates'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { disableCustomScripts } from '../../../lib/server/functions/disableCustomScripts'; +import { saveSettingsBulk } from '../../../lib/server/functions/saveSettingsBulk'; import { checkSettingValueBounds } from '../../../lib/server/lib/checkSettingValueBonds'; import { notifyOnSettingChanged, notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { addOAuthServiceMethod } from '../../../lib/server/methods/addOAuthService'; @@ -404,6 +407,31 @@ API.v1.post( }, ); +API.v1.post( + 'settings', + { + authRequired: true, + twoFactorRequired: true, + twoFactorOptions: { disableRememberMe: true }, + body: isSettingsBulkProps, + response: { + 200: settingByIdPostResponseSchema, + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + }, + }, + async function action() { + await saveSettingsBulk(this.userId, this.bodyParams.settings, { + username: this.user.username ?? '', + ip: this.requestIp ?? '', + useragent: this.request.headers.get('user-agent') ?? '', + }); + + return API.v1.success(); + }, +); + API.v1.get( 'service.configurations', { diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 46ec48b96833b..66a5ff9304009 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -1,4 +1,4 @@ -import { MeteorError, Team, api, Calendar } from '@rocket.chat/core-services'; +import { MeteorError, Presence, Team } from '@rocket.chat/core-services'; import type { IExportOperation, ILoginToken, IPersonalAccessToken, IUser, UserStatus } from '@rocket.chat/core-typings'; import { Users, Subscriptions, Sessions, OAuthAccessTokens, OAuthRefreshTokens, OAuthAuthCodes } from '@rocket.chat/models'; import { @@ -29,7 +29,7 @@ import { validateForbiddenErrorResponse, } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { getLoginExpirationInMs, wrapExceptions } from '@rocket.chat/tools'; +import { getLoginExpirationInMs } from '@rocket.chat/tools'; import { Accounts } from 'meteor/accounts-base'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -40,6 +40,7 @@ import { regeneratePersonalAccessTokenOfUser } from '../../../../imports/persona import { removePersonalAccessTokenOfUser } from '../../../../imports/personal-access-tokens/server/api/methods/removeToken'; import { UserChangedAuditStore } from '../../../../server/lib/auditServerEvents/userChanged'; import { i18n } from '../../../../server/lib/i18n'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey'; import { registerUser } from '../../../../server/methods/registerUser'; import { requestDataDownload } from '../../../../server/methods/requestDataDownload'; @@ -66,7 +67,6 @@ import { saveCustomFieldsWithoutValidation } from '../../../lib/server/functions import { saveUser } from '../../../lib/server/functions/saveUser'; import { sendWelcomeEmail } from '../../../lib/server/functions/saveUser/sendUserEmail'; import { canEditExtension } from '../../../lib/server/functions/saveUser/validateUserEditing'; -import { setStatusText } from '../../../lib/server/functions/setStatusText'; import { setUserAvatar } from '../../../lib/server/functions/setUserAvatar'; import { setUsernameWithValidation } from '../../../lib/server/functions/setUsername'; import { validateCustomFields } from '../../../lib/server/functions/validateCustomFields'; @@ -670,7 +670,7 @@ API.v1.addRoute( if (!canViewFullOtherUserInfo) { return API.v1.forbidden(); } - const escapedEmail = escapeRegExp(this.queryParams.email as string); + const escapedEmail = escapeRegExp(this.queryParams.email); nonEmptyQuery['emails.address'] = { $regex: `^${escapedEmail}$`, $options: 'i', @@ -1483,7 +1483,7 @@ API.v1 API.v1.post( 'users.sendConfirmationEmail', { - authRequired: true, + authRequired: false, body: isUsersSendConfirmationEmailParamsPOST, rateLimiterOptions: { numRequestsAllowed: 1, @@ -1492,16 +1492,11 @@ API.v1.post( response: { 200: voidSuccessResponse, 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, }, }, async function action() { - const { email } = this.bodyParams; - - if (await sendConfirmationEmail(email)) { - return API.v1.success(); - } - return API.v1.failure(); + void sendConfirmationEmail(this.bodyParams.email).catch((err) => SystemLogger.error({ msg: 'sendConfirmationEmail failed', err })); + return API.v1.success(); }, ); @@ -1543,13 +1538,15 @@ API.v1.get( status: 1, utcOffset: 1, statusText: 1, + statusSource: 1, + statusExpiresAt: 1, avatarETag: 1, }, }; if (ids) { return API.v1.success({ - users: await Users.findNotOfflineByIds(Array.isArray(ids) ? ids : ids.split(','), options).toArray(), + users: await Users.findPresenceUsersByIds(Array.isArray(ids) ? ids : ids.split(','), options).toArray(), full: false, }); } @@ -1580,15 +1577,22 @@ API.v1 authRequired: true, query: isUsersRequestDataDownloadParamsGET, response: { - 200: ajv.compile<{ requested: boolean; exportOperation: IExportOperation }>({ + 200: ajv.compile<{ + requested: boolean; + exportOperation: IExportOperation; + url: string | null; + pendingOperationsBeforeMyRequest: number; + }>({ type: 'object', properties: { requested: { type: 'boolean' }, // IExportOperation has complex/dynamic shape not yet in typia exportOperation: { type: 'object' }, + url: { type: 'string', nullable: true }, + pendingOperationsBeforeMyRequest: { type: 'number' }, success: { type: 'boolean', enum: [true] }, }, - required: ['requested', 'exportOperation', 'success'], + required: ['requested', 'exportOperation', 'pendingOperationsBeforeMyRequest', 'success'], additionalProperties: false, }), 401: validateUnauthorizedErrorResponse, @@ -1596,14 +1600,13 @@ API.v1 }, async function action() { const { fullExport = false } = this.queryParams; - const result = (await requestDataDownload({ userData: this.user, fullExport: fullExport === 'true' })) as { - requested: boolean; - exportOperation: IExportOperation; - }; + const result = await requestDataDownload({ userData: this.user, fullExport: fullExport === 'true' }); return API.v1.success({ requested: Boolean(result.requested), exportOperation: result.exportOperation, + url: result.url, + pendingOperationsBeforeMyRequest: result.pendingOperationsBeforeMyRequest, }); }, ) @@ -1939,8 +1942,9 @@ API.v1 intervalTimeInMS: 60000, }, body: ajv.compile<{ - status?: UserStatus; + status?: Exclude; message?: string; + expiresAt?: string; userId?: string; username?: string; user?: string; @@ -1949,6 +1953,7 @@ API.v1 properties: { status: { type: 'string', enum: ['online', 'away', 'offline', 'busy'] }, message: { type: 'string', nullable: true }, + expiresAt: { type: 'string', nullable: true }, userId: { type: 'string' }, username: { type: 'string' }, user: { type: 'string' }, @@ -1977,12 +1982,6 @@ API.v1 ), ); - if (!settings.get('Accounts_AllowUserStatusMessageChange')) { - throw new Meteor.Error('error-not-allowed', 'Change status is not allowed', { - method: 'users.setStatus', - }); - } - const user = await (async () => { if (isUserFromParams(this.bodyParams, this.userId, this.user)) { return Users.findOneById(this.userId); @@ -1996,47 +1995,38 @@ API.v1 return API.v1.forbidden(); } - const { _id, username, roles, name } = user; - let { statusText, status } = user; + const { status, message, expiresAt } = this.bodyParams; - if (this.bodyParams.message || this.bodyParams.message === '') { - await setStatusText(user, this.bodyParams.message, { emit: false }); - statusText = this.bodyParams.message; + if (message && !settings.get('Accounts_AllowUserStatusMessageChange')) { + throw new Meteor.Error('error-not-allowed', 'Change status is not allowed', { + method: 'users.setStatus', + }); } - if (this.bodyParams.status) { - const validStatus = ['online', 'away', 'offline', 'busy']; - if (validStatus.includes(this.bodyParams.status)) { - status = this.bodyParams.status; + const statusExpiresAt = expiresAt ? new Date(expiresAt) : undefined; + if (statusExpiresAt && Number.isNaN(statusExpiresAt.getTime())) { + throw new Meteor.Error('error-invalid-date', 'Invalid expiresAt date string', { + method: 'users.setStatus', + }); + } - if (status === 'offline' && !settings.get('Accounts_AllowInvisibleStatusOption')) { - throw new Meteor.Error('error-status-not-allowed', 'Invisible status is disabled', { - method: 'users.setStatus', - }); - } + if (statusExpiresAt && statusExpiresAt.getTime() <= Date.now()) { + throw new Meteor.Error('error-invalid-date', 'expiresAt must be a future date', { + method: 'users.setStatus', + }); + } - await Users.updateOne( - { _id: user._id }, - { - $set: { - status, - statusDefault: status, - }, - }, - ); + // If status is missing (message-only update), keep the user's chosen status (statusDefault), + // not the computed status — otherwise a transient auto-away/offline gets pinned as a manual claim. + const effectiveStatus = status || user.statusDefault || ('online' as UserStatus); - void wrapExceptions(() => Calendar.cancelUpcomingStatusChanges(user._id)).suppress(); - } else { - throw new Meteor.Error('error-invalid-status', 'Valid status types include online, away, offline, and busy.', { - method: 'users.setStatus', - }); - } + if (effectiveStatus === 'offline' && !settings.get('Accounts_AllowInvisibleStatusOption')) { + throw new Meteor.Error('error-status-not-allowed', 'Invisible status is disabled', { + method: 'users.setStatus', + }); } - void api.broadcast('presence.status', { - user: { status, _id, username, statusText, roles, name }, - previousStatus: user.status, - }); + await Presence.setStatus(user._id, effectiveStatus, message, statusExpiresAt); return API.v1.success(); }, @@ -2047,12 +2037,14 @@ API.v1 authRequired: true, query: isUsersGetStatusParamsGET, response: { - 200: ajv.compile<{ _id: string; status: string; connectionStatus?: string }>({ + 200: ajv.compile<{ _id: string; status: string; connectionStatus?: string; statusSource?: string; statusExpiresAt?: string }>({ type: 'object', properties: { _id: { type: 'string' }, status: statusType, connectionStatus: { type: 'string', nullable: true }, + statusSource: { type: 'string', nullable: true }, + statusExpiresAt: { type: 'string', nullable: true }, success: { type: 'boolean', enum: [true] }, }, required: ['_id', 'status', 'success'], @@ -2066,9 +2058,10 @@ API.v1 if (isUserFromParams(this.queryParams, this.userId, this.user)) { return API.v1.success({ _id: this.userId, - // message: user.statusText, connectionStatus: (this.user.statusConnection || 'offline') as 'online' | 'offline' | 'away' | 'busy', status: (this.user.status || 'offline') as 'online' | 'offline' | 'away' | 'busy', + ...(this.user.statusSource && { statusSource: this.user.statusSource }), + ...(this.user.statusExpiresAt && { statusExpiresAt: this.user.statusExpiresAt.toISOString() }), }); } @@ -2076,8 +2069,9 @@ API.v1 return API.v1.success({ _id: user._id, - // message: user.statusText, status: (user.status || 'offline') as 'online' | 'offline' | 'away' | 'busy', + ...(user.statusSource && { statusSource: user.statusSource }), + ...(user.statusExpiresAt && { statusExpiresAt: user.statusExpiresAt.toISOString() }), }); }, ); diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts index 11b2b405fb2e6..1a2963a005676 100644 --- a/apps/meteor/app/apps/server/bridges/rooms.ts +++ b/apps/meteor/app/apps/server/bridges/rooms.ts @@ -37,6 +37,8 @@ const rawRoomProjection: FindOptions['projection'] = { prid: 1, teamId: 1, teamMain: 1, + federated: 1, + federation: 1, livechatData: 1, waitingResponse: 1, open: 1, diff --git a/apps/meteor/app/apps/server/bridges/users.ts b/apps/meteor/app/apps/server/bridges/users.ts index 816416fa0002c..4a49f6b613e9d 100644 --- a/apps/meteor/app/apps/server/bridges/users.ts +++ b/apps/meteor/app/apps/server/bridges/users.ts @@ -2,7 +2,7 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import { UserBridge } from '@rocket.chat/apps/dist/server/bridges/UserBridge'; import type { IUserCreationOptions, IUser, UserType } from '@rocket.chat/apps-engine/definition/users'; import { Presence } from '@rocket.chat/core-services'; -import type { UserStatus } from '@rocket.chat/core-typings'; +import type { PresenceSource, UserStatus } from '@rocket.chat/core-typings'; import { Subscriptions, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; @@ -179,6 +179,28 @@ export class AppUserBridge extends UserBridge { return true; } + protected async setActiveState( + userId: IUser['id'], + state: Pick, + appId: string, + ): Promise { + this.orch.debugLog(`The App ${appId} is setting active state for user ${userId}`); + + await Presence.setActiveState(userId, { + statusDefault: state.statusDefault as UserStatus, + statusText: state.statusText, + statusSource: state.statusSource as PresenceSource, + ...(state.statusExpiresAt && { statusExpiresAt: state.statusExpiresAt }), + ...(state.statusId && { statusId: state.statusId }), + }); + } + + protected async endActiveState(userId: IUser['id'], appId: string, statusId?: string): Promise { + this.orch.debugLog(`The App ${appId} is ending active state for user ${userId}`); + + await Presence.endActiveState(userId, statusId); + } + protected async getActiveUserCount(): Promise { return Users.getActiveLocalUserCount(); } diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.js index 2c699399f1b5e..38c4f25678e24 100644 --- a/apps/meteor/app/apps/server/converters/rooms.js +++ b/apps/meteor/app/apps/server/converters/rooms.js @@ -59,6 +59,8 @@ export class AppRoomsConverter { contactId: 'contactId', departmentId: 'departmentId', parentRoomId: 'prid', + isFederated: 'federated', + federation: 'federation', visitor: (data) => { const { v } = data; if (!v) { @@ -261,6 +263,8 @@ export class AppRoomsConverter { ...(room.lastModifiedAt && { lm: room.lastModifiedAt }), ...(room.customFields && { customFields: room.customFields }), ...(room.livechatData && { livechatData: room.livechatData }), + ...(typeof room.isFederated !== 'undefined' && { federated: room.isFederated }), + ...(typeof room.federation !== 'undefined' && { federation: room.federation }), ...(typeof room.parentRoom !== 'undefined' && { prid: room.parentRoom.id }), ...(contactId && { contactId }), ...(room._USERNAMES && { _USERNAMES: room._USERNAMES }), @@ -304,6 +308,8 @@ export class AppRoomsConverter { closer: 'closer', teamId: 'teamId', isTeamMain: 'teamMain', + isFederated: 'federated', + federation: 'federation', isDefault: (room) => { const result = !!room.default; delete room.default; diff --git a/apps/meteor/app/apps/server/converters/users.js b/apps/meteor/app/apps/server/converters/users.js index 7ce44b28f8355..4bca06ab168ed 100644 --- a/apps/meteor/app/apps/server/converters/users.js +++ b/apps/meteor/app/apps/server/converters/users.js @@ -45,6 +45,8 @@ export class AppUsersConverter { lastLoginAt: user.lastLogin, appId: user.appId, customFields: user.customFields, + isFederated: user.federated, + federation: user.federation, sipExtension: user.freeSwitchExtension, settings: { preferences: { @@ -75,6 +77,8 @@ export class AppUsersConverter { _updatedAt: user.updatedAt, lastLogin: user.lastLoginAt, appId: user.appId, + federated: user.isFederated, + federation: user.federation, freeSwitchExtension: user.sipExtension, }); } diff --git a/apps/meteor/app/authorization/server/functions/canAccessRoom.ts b/apps/meteor/app/authorization/server/functions/canAccessRoom.ts index 9a20cda8dffa1..152e2bfd87d88 100644 --- a/apps/meteor/app/authorization/server/functions/canAccessRoom.ts +++ b/apps/meteor/app/authorization/server/functions/canAccessRoom.ts @@ -7,4 +7,5 @@ export const roomAccessAttributes = { t: 1, teamId: 1, prid: 1, + abacAttributes: 1, }; diff --git a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts index 428cd2f04fb67..d3207b9205930 100644 --- a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts +++ b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts @@ -95,10 +95,12 @@ export const AutoTranslate = { const loadProviders = async () => { try { - [this.providersMetadata, this.supportedLanguages] = await Promise.all([ + const [providersMetadata, supportedLanguagesResponse] = await Promise.all([ sdk.call('autoTranslate.getProviderUiMetadata'), - sdk.call('autoTranslate.getSupportedLanguages', 'en'), + sdk.rest.get('/v1/autotranslate.getSupportedLanguages', { targetLanguage: 'en' }), ]); + this.providersMetadata = providersMetadata; + this.supportedLanguages = supportedLanguagesResponse.languages; } catch (e: unknown) { // Avoid unwanted error message on UI when autotranslate is disabled while fetching data console.error((e as Error).message); diff --git a/apps/meteor/app/autotranslate/server/index.ts b/apps/meteor/app/autotranslate/server/index.ts index 8d76d327670ef..e98a30265b12d 100644 --- a/apps/meteor/app/autotranslate/server/index.ts +++ b/apps/meteor/app/autotranslate/server/index.ts @@ -10,6 +10,7 @@ import './methods/saveSettings'; import './methods/translateMessage'; import './googleTranslate'; import './deeplTranslate'; +import './libreTranslate'; import './msTranslate'; import './methods/getProviderUiMetadata'; diff --git a/apps/meteor/app/autotranslate/server/libreTranslate.ts b/apps/meteor/app/autotranslate/server/libreTranslate.ts new file mode 100644 index 0000000000000..fd0134240b5df --- /dev/null +++ b/apps/meteor/app/autotranslate/server/libreTranslate.ts @@ -0,0 +1,179 @@ +import type { IMessage, MessageAttachment, IProviderMetadata, ITranslationResult, ISupportedLanguage } from '@rocket.chat/core-typings'; +import { serverFetch as fetch } from '@rocket.chat/server-fetch'; + +import { TranslationProviderRegistry, AutoTranslate } from './autotranslate'; +import { libreLogger } from './logger'; +import { i18n } from '../../../server/lib/i18n'; +import { settings } from '../../settings/server'; + +interface ILibreTranslateLanguage { + code: string; + name: string; + targets?: string[]; +} + +const REQUEST_TIMEOUT = 10000; + +const toLanguageTag = (code: string): string => { + try { + return new Intl.Locale(code).toString(); + } catch (err) { + libreLogger.error({ msg: 'Invalid language code returned by LibreTranslate', code, err }); + return code; + } +}; + +/** + * LibreTranslate translation service provider class representation. + * @class + * @augments AutoTranslate + */ +class LibreTranslateAutoTranslate extends AutoTranslate { + apiKey: string; + + apiEndPointUrl: string; + + constructor() { + super(); + this.name = 'libre-translate'; + this.apiEndPointUrl = ''; + this.apiKey = ''; + + settings.watch('AutoTranslate_LibreTranslateAPIURL', (value) => { + this.apiEndPointUrl = (value || '').replace(/\/$/, ''); + }); + + settings.watch('AutoTranslate_LibreTranslateAPIKey', (value) => { + this.apiKey = value; + }); + } + + _getProviderMetadata(): IProviderMetadata { + return { + name: this.name, + displayName: i18n.t('AutoTranslate_LibreTranslate'), + settings: this._getSettings(), + }; + } + + _getSettings(): IProviderMetadata['settings'] { + return { + apiKey: this.apiKey, + apiEndPointUrl: this.apiEndPointUrl, + }; + } + + async getSupportedLanguages(target: string): Promise { + if (!this.apiEndPointUrl) { + return []; + } + + if (this.supportedLanguages[target]) { + return this.supportedLanguages[target]; + } + + try { + // SECURITY: the URL comes from a setting only an admin can change, so disabling SSRF validation is safe here. + const request = await fetch(`${this.apiEndPointUrl}/languages`, { + ignoreSsrfValidation: true, + timeout: REQUEST_TIMEOUT, + }); + if (!request.ok) { + throw new Error('Failed to fetch supported languages'); + } + + const result = (await request.json()) as ILibreTranslateLanguage[]; + + this.supportedLanguages[target || 'en'] = result.map(({ code, name }) => ({ + language: toLanguageTag(code), + name, + })); + return this.supportedLanguages[target || 'en']; + } catch (err) { + libreLogger.error({ msg: 'Error fetching supported languages', err }); + return []; + } + } + + private resolveTargetLanguage(language: string, supportedLanguages: ISupportedLanguage[]): string { + if (language.includes('-') && !supportedLanguages.find((supported) => supported.language === language)) { + return language.slice(0, 2); + } + return language; + } + + // `q` is sent as an array of lines; LibreTranslate returns `translatedText` as an array (one entry per line). + private async _query(lines: string[], targetLanguage: string): Promise { + const body: Record = { + q: lines, + source: 'auto', + target: targetLanguage, + format: 'text', + ...(this.apiKey && { api_key: this.apiKey }), + }; + + // SECURITY: the URL comes from a setting only an admin can change, so disabling SSRF validation is safe here. + const result = await fetch(`${this.apiEndPointUrl}/translate`, { + method: 'POST', + ignoreSsrfValidation: true, + timeout: REQUEST_TIMEOUT, + headers: { 'Content-Type': 'application/json' }, + body, + }); + + if (!result.ok) { + throw new Error(result.statusText); + } + + const json = (await result.json()) as { translatedText?: string | string[] }; + if (json.translatedText === undefined) { + return null; + } + return Array.isArray(json.translatedText) ? json.translatedText.join('\n') : json.translatedText; + } + + async _translateMessage(message: IMessage, targetLanguages: string[]): Promise { + const translations: { [k: string]: string } = {}; + if (!this.apiEndPointUrl) { + return translations; + } + + const msgs = message.msg.split('\n'); + const supportedLanguages = await this.getSupportedLanguages('en'); + for (const targetLanguage of targetLanguages) { + const language = this.resolveTargetLanguage(targetLanguage, supportedLanguages); + try { + const translatedText = await this._query(msgs, language); + if (translatedText !== null) { + translations[language] = this.deTokenize(Object.assign({}, message, { msg: translatedText })); + } + } catch (err) { + libreLogger.error({ msg: 'Error translating message', err }); + } + } + return translations; + } + + async _translateAttachmentDescriptions(attachment: MessageAttachment, targetLanguages: string[]): Promise { + const translations: { [k: string]: string } = {}; + if (!this.apiEndPointUrl) { + return translations; + } + + const supportedLanguages = await this.getSupportedLanguages('en'); + for (const targetLanguage of targetLanguages) { + const language = this.resolveTargetLanguage(targetLanguage, supportedLanguages); + try { + const translatedText = await this._query([attachment.description || attachment.text || ''], language); + if (translatedText !== null) { + translations[language] = translatedText; + } + } catch (err) { + libreLogger.error({ msg: 'Error translating message attachment', err }); + } + } + return translations; + } +} + +TranslationProviderRegistry.registerProvider(new LibreTranslateAutoTranslate()); diff --git a/apps/meteor/app/autotranslate/server/logger.ts b/apps/meteor/app/autotranslate/server/logger.ts index 9fbd129acd735..895a9d75c6a60 100644 --- a/apps/meteor/app/autotranslate/server/logger.ts +++ b/apps/meteor/app/autotranslate/server/logger.ts @@ -3,3 +3,5 @@ import { Logger } from '@rocket.chat/logger'; const logger = new Logger('AutoTranslate'); export const msLogger = logger.section('Microsoft'); + +export const libreLogger = logger.section('LibreTranslate'); diff --git a/apps/meteor/app/autotranslate/server/methods/saveSettings.ts b/apps/meteor/app/autotranslate/server/methods/saveSettings.ts index 8472781be6830..fa5badb0e1af5 100644 --- a/apps/meteor/app/autotranslate/server/methods/saveSettings.ts +++ b/apps/meteor/app/autotranslate/server/methods/saveSettings.ts @@ -1,6 +1,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Meteor } from 'meteor/meteor'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { saveAutoTranslateSettings } from '../functions/saveSettings'; declare module '@rocket.chat/ddp-client' { @@ -12,6 +13,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async 'autoTranslate.saveSettings'(rid, field, value, options) { + methodDeprecationLogger.method('autoTranslate.saveSettings', '9.0.0', '/v1/autotranslate.saveSettings'); const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { diff --git a/apps/meteor/app/cloud/server/methods.ts b/apps/meteor/app/cloud/server/methods.ts index c132dbb1a48c2..ef6e6b34927ea 100644 --- a/apps/meteor/app/cloud/server/methods.ts +++ b/apps/meteor/app/cloud/server/methods.ts @@ -12,6 +12,7 @@ import { startRegisterWorkspace } from './functions/startRegisterWorkspace'; import { syncWorkspace } from './functions/syncWorkspace'; import { userLogout } from './functions/userLogout'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../lib/server/lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -40,6 +41,7 @@ Meteor.methods({ * Prefer using cloud.registrationStatus rest api. */ async 'cloud:checkRegisterStatus'() { + methodDeprecationLogger.method('cloud:checkRegisterStatus', '9.0.0', '/v1/cloud.registrationStatus'); const uid = Meteor.userId(); if (!uid) { diff --git a/apps/meteor/app/custom-sounds/server/lib/deleteCustomSound.ts b/apps/meteor/app/custom-sounds/server/lib/deleteCustomSound.ts new file mode 100644 index 0000000000000..ec5721164b1fe --- /dev/null +++ b/apps/meteor/app/custom-sounds/server/lib/deleteCustomSound.ts @@ -0,0 +1,20 @@ +import { api } from '@rocket.chat/core-services'; +import { CustomSounds } from '@rocket.chat/models'; +import { Meteor } from 'meteor/meteor'; + +import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds'; + +export const deleteCustomSound = async (_id: string): Promise => { + const sound = await CustomSounds.findOneById(_id); + + if (!sound) { + throw new Meteor.Error('Custom_Sound_Error_Invalid_Sound', 'Invalid sound', { + method: 'deleteCustomSound', + }); + } + + await RocketChatFileCustomSoundsInstance.deleteFile(`${sound._id}.${sound.extension}`); + await CustomSounds.removeById(_id); + + void api.broadcast('notify.deleteCustomSound', { soundData: sound }); +}; diff --git a/apps/meteor/app/custom-sounds/server/methods/deleteCustomSound.ts b/apps/meteor/app/custom-sounds/server/methods/deleteCustomSound.ts index 1c63c7a67d9fc..5b122fa5e116f 100644 --- a/apps/meteor/app/custom-sounds/server/methods/deleteCustomSound.ts +++ b/apps/meteor/app/custom-sounds/server/methods/deleteCustomSound.ts @@ -1,11 +1,11 @@ -import { api } from '@rocket.chat/core-services'; import type { ICustomSound } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { CustomSounds } from '@rocket.chat/models'; +import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { deleteCustomSound } from '../lib/deleteCustomSound'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -16,24 +16,12 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async deleteCustomSound(_id) { - let sound = null; - - if (this.userId && (await hasPermissionAsync(this.userId, 'manage-sounds'))) { - sound = await CustomSounds.findOneById(_id); - } else { + methodDeprecationLogger.method('deleteCustomSound', '9.0.0', '/v1/custom-sounds.delete'); + if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-sounds'))) { throw new Meteor.Error('not_authorized'); } - - if (sound == null) { - throw new Meteor.Error('Custom_Sound_Error_Invalid_Sound', 'Invalid sound', { - method: 'deleteCustomSound', - }); - } - - await RocketChatFileCustomSoundsInstance.deleteFile(`${sound._id}.${sound.extension}`); - await CustomSounds.removeById(_id); - void api.broadcast('notify.deleteCustomSound', { soundData: sound }); - + check(_id, String); + await deleteCustomSound(_id); return true; }, }); diff --git a/apps/meteor/app/custom-sounds/server/methods/listCustomSounds.ts b/apps/meteor/app/custom-sounds/server/methods/listCustomSounds.ts index eda1325d77335..e627b96368f47 100644 --- a/apps/meteor/app/custom-sounds/server/methods/listCustomSounds.ts +++ b/apps/meteor/app/custom-sounds/server/methods/listCustomSounds.ts @@ -3,6 +3,8 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { CustomSounds } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; + declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -12,6 +14,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async listCustomSounds() { + methodDeprecationLogger.method('listCustomSounds', '9.0.0', '/v1/custom-sounds.list'); return CustomSounds.find({}).toArray(); }, }); diff --git a/apps/meteor/app/discussion/server/methods/createDiscussion.ts b/apps/meteor/app/discussion/server/methods/createDiscussion.ts index 65804410c405a..10e965758c906 100644 --- a/apps/meteor/app/discussion/server/methods/createDiscussion.ts +++ b/apps/meteor/app/discussion/server/methods/createDiscussion.ts @@ -15,6 +15,7 @@ import { attachMessage } from '../../../lib/server/functions/attachMessage'; import { createRoom } from '../../../lib/server/functions/createRoom'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; import { afterSaveMessageAsync } from '../../../lib/server/lib/afterSaveMessage'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { settings } from '../../../settings/server'; const getParentRoom = async (rid: IRoom['_id']) => { @@ -85,7 +86,7 @@ const create = async ({ } if (prid) { const parentRoom = await getParentRoom(message.rid); - if (!parentRoom || prid !== parentRoom._id) { + if (prid !== parentRoom?._id) { throw new Meteor.Error('error-invalid-arguments', 'Root message room ID does not match parent room ID ', { method: 'DiscussionCreation', }); @@ -247,6 +248,7 @@ Meteor.methods({ * @param {boolean} encrypted - if the discussion's e2e encryption should be enabled. */ async createDiscussion({ prid, pmid, t_name: discussionName, reply, users, encrypted }: CreateDiscussionProperties) { + methodDeprecationLogger.method('createDiscussion', '9.0.0', '/v1/rooms.createDiscussion'); check(prid, Match.Maybe(String)); check(pmid, Match.Maybe(String)); check(reply, Match.Maybe(String)); diff --git a/apps/meteor/app/e2e/server/methods/fetchMyKeys.ts b/apps/meteor/app/e2e/server/methods/fetchMyKeys.ts index df1150cee8463..b788542dda765 100644 --- a/apps/meteor/app/e2e/server/methods/fetchMyKeys.ts +++ b/apps/meteor/app/e2e/server/methods/fetchMyKeys.ts @@ -2,6 +2,8 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; + declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -11,6 +13,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async 'e2e.fetchMyKeys'() { + methodDeprecationLogger.method('e2e.fetchMyKeys', '9.0.0', '/v1/e2e.fetchMyKeys'); const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'e2e.fetchMyKeys' }); diff --git a/apps/meteor/app/e2e/server/methods/requestSubscriptionKeys.ts b/apps/meteor/app/e2e/server/methods/requestSubscriptionKeys.ts index cf899a5d64ad7..2beea4346dcaa 100644 --- a/apps/meteor/app/e2e/server/methods/requestSubscriptionKeys.ts +++ b/apps/meteor/app/e2e/server/methods/requestSubscriptionKeys.ts @@ -3,6 +3,8 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Subscriptions, Rooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; + declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -10,8 +12,31 @@ declare module '@rocket.chat/ddp-client' { } } +export const requestSubscriptionKeysMethod = async (userId: string): Promise => { + // Get all encrypted rooms that the user is subscribed to and has no E2E key yet + const subscriptions = await Subscriptions.findByUserIdWithoutE2E(userId).toArray(); + const roomIds = subscriptions.map((subscription) => subscription.rid); + + // For all subscriptions without E2E key, get the rooms that have encryption enabled + const query = { + e2eKeyId: { + $exists: true, + }, + _id: { + $in: roomIds, + }, + }; + + const rooms = Rooms.find(query); + await rooms.forEach((room) => { + void api.broadcast('notify.e2e.keyRequest', room._id, room.e2eKeyId); + }); +}; + Meteor.methods({ async 'e2e.requestSubscriptionKeys'() { + methodDeprecationLogger.method('e2e.requestSubscriptionKeys', '9.0.0', '/v1/e2e.requestSubscriptionKeys'); + const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { @@ -19,24 +44,7 @@ Meteor.methods({ }); } - // Get all encrypted rooms that the user is subscribed to and has no E2E key yet - const subscriptions = await Subscriptions.findByUserIdWithoutE2E(userId).toArray(); - const roomIds = subscriptions.map((subscription) => subscription.rid); - - // For all subscriptions without E2E key, get the rooms that have encryption enabled - const query = { - e2eKeyId: { - $exists: true, - }, - _id: { - $in: roomIds, - }, - }; - - const rooms = Rooms.find(query); - await rooms.forEach((room) => { - void api.broadcast('notify.e2e.keyRequest', room._id, room.e2eKeyId); - }); + await requestSubscriptionKeysMethod(userId); return true; }, diff --git a/apps/meteor/app/e2e/server/methods/resetOwnE2EKey.ts b/apps/meteor/app/e2e/server/methods/resetOwnE2EKey.ts index 13e59e0286554..2c63a89b6a382 100644 --- a/apps/meteor/app/e2e/server/methods/resetOwnE2EKey.ts +++ b/apps/meteor/app/e2e/server/methods/resetOwnE2EKey.ts @@ -4,6 +4,7 @@ import { Meteor } from 'meteor/meteor'; import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey'; import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -14,6 +15,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ 'e2e.resetOwnE2EKey': twoFactorRequired(async () => { + methodDeprecationLogger.method('e2e.resetOwnE2EKey', '9.0.0', '/v1/users.resetE2EKey'); const userId = Meteor.userId(); if (!userId) { diff --git a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts index da5f93090e1b8..9d54f9a81b532 100644 --- a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts +++ b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts @@ -2,6 +2,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Rooms, Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { notifyOnRoomChangedById } from '../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ddp-client' { @@ -42,6 +43,7 @@ export const setUserPublicAndPrivateKeysMethod = async ( Meteor.methods({ async 'e2e.setUserPublicAndPrivateKeys'(keyPair) { + methodDeprecationLogger.method('e2e.setUserPublicAndPrivateKeys', '9.0.0', '/v1/e2e.setUserPublicAndPrivateKeys'); const userId = Meteor.userId(); if (!userId) { diff --git a/apps/meteor/app/emoji-custom/server/methods/deleteEmojiCustom.ts b/apps/meteor/app/emoji-custom/server/methods/deleteEmojiCustom.ts index ce6aaeaf417f6..0ca581f7fe1c1 100644 --- a/apps/meteor/app/emoji-custom/server/methods/deleteEmojiCustom.ts +++ b/apps/meteor/app/emoji-custom/server/methods/deleteEmojiCustom.ts @@ -5,6 +5,7 @@ import { EmojiCustom } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { RocketChatFileEmojiCustomInstance } from '../startup/emoji-custom'; declare module '@rocket.chat/ddp-client' { @@ -35,6 +36,7 @@ export const deleteEmojiCustom = async (userId: string, emojiID: ICustomEmojiDes Meteor.methods({ async deleteEmojiCustom(emojiID) { + methodDeprecationLogger.method('deleteEmojiCustom', '9.0.0', '/v1/emoji-custom.delete'); if (!this.userId) { throw new Meteor.Error('not_authorized'); } diff --git a/apps/meteor/app/emoji-custom/server/methods/insertOrUpdateEmoji.ts b/apps/meteor/app/emoji-custom/server/methods/insertOrUpdateEmoji.ts index 1891d1b3ed95d..ed3bc693bac05 100644 --- a/apps/meteor/app/emoji-custom/server/methods/insertOrUpdateEmoji.ts +++ b/apps/meteor/app/emoji-custom/server/methods/insertOrUpdateEmoji.ts @@ -1,6 +1,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Meteor } from 'meteor/meteor'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { insertOrUpdateEmoji } from '../lib/insertOrUpdateEmoji'; declare module '@rocket.chat/ddp-client' { @@ -20,6 +21,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async insertOrUpdateEmoji(emojiData) { + methodDeprecationLogger.method('insertOrUpdateEmoji', '9.0.0', ['/v1/emoji-custom.create', '/v1/emoji-custom.update']); const emoji = await insertOrUpdateEmoji(this.userId, emojiData); if (!emojiData._id) { diff --git a/apps/meteor/app/emoji-custom/server/methods/uploadEmojiCustom.ts b/apps/meteor/app/emoji-custom/server/methods/uploadEmojiCustom.ts index e387888b1311c..91e6ed8270265 100644 --- a/apps/meteor/app/emoji-custom/server/methods/uploadEmojiCustom.ts +++ b/apps/meteor/app/emoji-custom/server/methods/uploadEmojiCustom.ts @@ -1,6 +1,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Meteor } from 'meteor/meteor'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { uploadEmojiCustom } from '../lib/uploadEmojiCustom'; declare module '@rocket.chat/ddp-client' { @@ -20,6 +21,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async uploadEmojiCustom(binaryContent, contentType, emojiData) { + methodDeprecationLogger.method('uploadEmojiCustom', '9.0.0', '/v1/emoji-custom.create'); await uploadEmojiCustom(this.userId, binaryContent, contentType, emojiData); }, }); diff --git a/apps/meteor/app/file-upload/server/config/AmazonS3.ts b/apps/meteor/app/file-upload/server/config/AmazonS3.ts index 77a554e46b7af..f5129b1a8daca 100644 --- a/apps/meteor/app/file-upload/server/config/AmazonS3.ts +++ b/apps/meteor/app/file-upload/server/config/AmazonS3.ts @@ -4,11 +4,14 @@ import https from 'node:https'; import _ from 'underscore'; import { forceDownload } from './helper'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { settings } from '../../../settings/server'; import type { S3Options } from '../../ufs/AmazonS3/server'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; import '../../ufs/AmazonS3/server'; +const hasScheme = (value: string) => /^[a-z][a-z0-9+.-]*:\/\//i.test(value); + const get: FileUploadClass['get'] = async function (this: FileUploadClass, file, req, res) { const forcedDownload = forceDownload(req); @@ -94,6 +97,14 @@ const configure = _.debounce(() => { config.connection.region = Region; } + // Back-compat: AWS SDK v2 defaulted unset region to us-east-1; v3 throws. + if (!Region && !process.env.AWS_REGION) { + config.connection.region = 'us-east-1'; + SystemLogger.warn( + 'FileUpload_S3_Region is empty and AWS_REGION is not set; defaulting to us-east-1. Set FileUpload_S3_Region or AWS_REGION to silence this warning.', + ); + } + if (AWSAccessKeyId && AWSSecretAccessKey) { config.connection.credentials = { accessKeyId: AWSAccessKeyId, @@ -101,8 +112,13 @@ const configure = _.debounce(() => { }; } + // Back-compat: AWS SDK v2 accepted scheme-less endpoints; v3 throws. if (BucketURL) { - config.connection.endpoint = BucketURL; + const isValidScheme = hasScheme(BucketURL); + config.connection.endpoint = isValidScheme ? BucketURL : `https://${BucketURL}`; + if (!isValidScheme) { + SystemLogger.warn(`FileUpload_S3_BucketURL "${BucketURL}" has no scheme; defaulting to "https://${BucketURL}".`); + } } AmazonS3Uploads.store = FileUpload.configureUploadsStore('AmazonS3', AmazonS3Uploads.name, config); diff --git a/apps/meteor/app/importer/server/methods/downloadPublicImportFile.ts b/apps/meteor/app/importer/server/methods/downloadPublicImportFile.ts index a2f60404c4914..2485f5fb487f4 100644 --- a/apps/meteor/app/importer/server/methods/downloadPublicImportFile.ts +++ b/apps/meteor/app/importer/server/methods/downloadPublicImportFile.ts @@ -9,6 +9,7 @@ import { Meteor } from 'meteor/meteor'; import { Importers } from '..'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { ProgressStep } from '../../lib/ImporterProgressStep'; import { RocketChatImportFileInstance } from '../startup/store'; @@ -84,6 +85,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async downloadPublicImportFile(fileUrl: string, importerKey: string) { + methodDeprecationLogger.method('downloadPublicImportFile', '9.0.0', '/v1/downloadPublicImportFile'); const userId = Meteor.userId(); if (!userId) { diff --git a/apps/meteor/app/importer/server/methods/getImportFileData.ts b/apps/meteor/app/importer/server/methods/getImportFileData.ts index e029f4c2bfecf..2688cca265c2d 100644 --- a/apps/meteor/app/importer/server/methods/getImportFileData.ts +++ b/apps/meteor/app/importer/server/methods/getImportFileData.ts @@ -8,6 +8,7 @@ import { Meteor } from 'meteor/meteor'; import { Importers } from '..'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { ProgressStep } from '../../lib/ImporterProgressStep'; import { RocketChatImportFileInstance } from '../startup/store'; @@ -71,6 +72,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async getImportFileData() { + methodDeprecationLogger.method('getImportFileData', '9.0.0', '/v1/getImportFileData'); const userId = Meteor.userId(); if (!userId) { diff --git a/apps/meteor/app/importer/server/methods/getImportProgress.ts b/apps/meteor/app/importer/server/methods/getImportProgress.ts index 7b73030df22e6..2dbad5af56493 100644 --- a/apps/meteor/app/importer/server/methods/getImportProgress.ts +++ b/apps/meteor/app/importer/server/methods/getImportProgress.ts @@ -5,6 +5,7 @@ import { Meteor } from 'meteor/meteor'; import { Importers } from '..'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; export const executeGetImportProgress = async (): Promise => { const operation = await Imports.findLastImport(); @@ -32,6 +33,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async getImportProgress() { + methodDeprecationLogger.method('getImportProgress', '9.0.0', '/v1/getImportProgress'); const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', 'getImportProgress'); diff --git a/apps/meteor/app/importer/server/methods/getLatestImportOperations.ts b/apps/meteor/app/importer/server/methods/getLatestImportOperations.ts index 0471cb57955d5..602486070269f 100644 --- a/apps/meteor/app/importer/server/methods/getLatestImportOperations.ts +++ b/apps/meteor/app/importer/server/methods/getLatestImportOperations.ts @@ -4,6 +4,7 @@ import { Imports } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; export const executeGetLatestImportOperations = async () => { const data = Imports.find( @@ -26,6 +27,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async getLatestImportOperations() { + methodDeprecationLogger.method('getLatestImportOperations', '9.0.0', '/v1/getLatestImportOperations'); const userId = Meteor.userId(); if (!userId) { diff --git a/apps/meteor/app/importer/server/methods/startImport.ts b/apps/meteor/app/importer/server/methods/startImport.ts index d86638662f6f3..32fe5c393252c 100644 --- a/apps/meteor/app/importer/server/methods/startImport.ts +++ b/apps/meteor/app/importer/server/methods/startImport.ts @@ -6,6 +6,7 @@ import { Meteor } from 'meteor/meteor'; import { Importers } from '..'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; export const executeStartImport = async ({ input }: StartImportParamsPOST, startedByUserId: IUser['_id']) => { const operation = await Imports.findLastImport(); @@ -33,6 +34,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async startImport({ input }: StartImportParamsPOST) { + methodDeprecationLogger.method('startImport', '9.0.0', '/v1/startImport'); if (!input || typeof input !== 'object' || !isStartImportParamsPOST({ input })) { throw new Meteor.Error(`Invalid Selection data provided to the importer.`); } diff --git a/apps/meteor/app/importer/server/methods/uploadImportFile.ts b/apps/meteor/app/importer/server/methods/uploadImportFile.ts index df5d2af883fae..22f9493ca7dcb 100644 --- a/apps/meteor/app/importer/server/methods/uploadImportFile.ts +++ b/apps/meteor/app/importer/server/methods/uploadImportFile.ts @@ -6,6 +6,7 @@ import { Meteor } from 'meteor/meteor'; import { Importers } from '..'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { RocketChatFile } from '../../../file/server'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { ProgressStep } from '../../lib/ImporterProgressStep'; import { RocketChatImportFileInstance } from '../startup/store'; @@ -64,6 +65,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async uploadImportFile(binaryContent, contentType, fileName, importerKey) { + methodDeprecationLogger.method('uploadImportFile', '9.0.0', '/v1/uploadImportFile'); const userId = Meteor.userId(); if (!userId) { diff --git a/apps/meteor/app/integrations/server/api/api.ts b/apps/meteor/app/integrations/server/api/api.ts index 25019e64d3799..de016532e92e2 100644 --- a/apps/meteor/app/integrations/server/api/api.ts +++ b/apps/meteor/app/integrations/server/api/api.ts @@ -17,6 +17,7 @@ import { loggerMiddleware } from '../../../api/server/middlewares/logger'; import { metricsMiddleware } from '../../../api/server/middlewares/metrics'; import { tracerSpanMiddleware } from '../../../api/server/middlewares/tracer'; import type { APIActionContext } from '../../../api/server/router'; +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import type { WebhookResponseItem } from '../../../lib/server/functions/processWebhookMessage'; import { processWebhookMessage } from '../../../lib/server/functions/processWebhookMessage'; import { metrics } from '../../../metrics/server'; @@ -168,6 +169,15 @@ async function executeIntegrationRest( return API.v1.unavailable('Service Unavailable'); } + if (!(await hasPermissionAsync(this.user._id, 'message-impersonate'))) { + incomingLogger.error({ + msg: 'Error trying to execute integration', + integration: this.request.integration.name, + error: 'The assigned user from the integration must have the message-impersonate permission.', + }); + return API.v1.failure('error-user-lacks-message-impersonate-permission'); + } + const defaultValues = { channel: this.request.integration.channel, alias: this.request.integration.alias || '', diff --git a/apps/meteor/app/integrations/server/lib/isolated-vm/buildSandbox.ts b/apps/meteor/app/integrations/server/lib/isolated-vm/buildSandbox.ts index 16aacd8ae1fa1..a2387ad123ccb 100644 --- a/apps/meteor/app/integrations/server/lib/isolated-vm/buildSandbox.ts +++ b/apps/meteor/app/integrations/server/lib/isolated-vm/buildSandbox.ts @@ -119,8 +119,9 @@ export const buildSandbox = (context: Context) => { jail.setSync( 'serverFetch', - new ivm.Reference(async (url: string, ...args: any[]) => { - const result = await fetch(url, ...args); + new ivm.Reference(async (url: string, options?: any, allowSelfSignedCerts?: boolean) => { + // SECURITY: Integrations can only be configured by users with enough privileges. It's ok to disable this check here. + const result = await fetch(url, { ...(options ?? {}), ignoreSsrfValidation: true }, allowSelfSignedCerts); return makeTransferable(result); }), ); diff --git a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts index 49116d1335ee8..869a2fc4c9121 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts @@ -6,8 +6,8 @@ import { removeEmpty } from '@rocket.chat/tools'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { addUserRolesAsync } from '../../../../../server/lib/roles/addUserRoles'; import { hasPermissionAsync, hasAllPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger'; import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; import { compileIntegrationScript } from '../../lib/compileIntegrationScript'; import { validateScriptEngine, isScriptEngineFrozen } from '../../lib/validateScriptEngine'; @@ -82,7 +82,7 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn validateScriptEngine(integration.scriptEngine ?? 'isolated-vm'); } - const user = await Users.findOne({ username: integration.username }); + const user = await Users.findOneByUsername(integration.username, { projection: { _id: 1 } }); if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { @@ -90,6 +90,16 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn }); } + if (!(await hasPermissionAsync(user._id, 'message-impersonate'))) { + throw new Meteor.Error( + 'error-user-lacks-message-impersonate-permission', + "User selected for the incoming integration lacks the 'message-impersonate' permission.", + { + method: 'addIncomingIntegration', + }, + ); + } + // Default to transpiling with Babel for backwards compatibility; integrations // can opt-out per-record by setting `skipTranspile: true` (removed in 9.0.0). const skipTranspile = integration.skipTranspile === true; @@ -157,8 +167,6 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn } } - await addUserRolesAsync(user._id, ['bot']); - const strippedIntegrationData = removeEmpty(integrationData); const { insertedId } = await Integrations.insertOne(strippedIntegrationData); @@ -175,6 +183,7 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn Meteor.methods({ async addIncomingIntegration(integration: INewIncomingIntegration): Promise { + methodDeprecationLogger.method('addIncomingIntegration', '9.0.0', '/v1/integrations.create'); const { userId } = this; if (!userId) { diff --git a/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts index 9a116f51bbafa..3258aeed8aebc 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts @@ -3,6 +3,7 @@ import { Integrations } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger'; import { notifyOnIntegrationChangedById } from '../../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ddp-client' { @@ -40,6 +41,7 @@ export const deleteIncomingIntegration = async (integrationId: string, userId: s Meteor.methods({ async deleteIncomingIntegration(integrationId) { + methodDeprecationLogger.method('deleteIncomingIntegration', '9.0.0', '/v1/integrations.remove'); const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('not_authorized', 'Unauthorized', { diff --git a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts index 894b5b31bd405..ba5155e64fc4c 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts @@ -4,8 +4,8 @@ import { Integrations, Subscriptions, Users, Rooms } from '@rocket.chat/models'; import { wrapExceptions } from '@rocket.chat/tools'; import { Meteor } from 'meteor/meteor'; -import { addUserRolesAsync } from '../../../../../server/lib/roles/addUserRoles'; import { hasAllPermissionAsync, hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger'; import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; import { compileIntegrationScript } from '../../lib/compileIntegrationScript'; import { isScriptEngineFrozen, validateScriptEngine } from '../../lib/validateScriptEngine'; @@ -143,15 +143,23 @@ export const updateIncomingIntegration = async ( } const username = 'username' in integration ? integration.username : currentIntegration.username; - const user = await Users.findOne({ username }); + const user = await Users.findOneByUsername(username, { projection: { _id: 1, username: 1 } }); - if (!user?._id) { + if (!user) { throw new Meteor.Error('error-invalid-post-as-user', 'Invalid Post As User', { method: 'updateIncomingIntegration', }); } - await addUserRolesAsync(user._id, ['bot']); + if (!(await hasPermissionAsync(user._id, 'message-impersonate'))) { + throw new Meteor.Error( + 'error-user-lacks-message-impersonate-permission', + "User selected for the incoming integration lacks the 'message-impersonate' permission.", + { + method: 'updateIncomingIntegration', + }, + ); + } const updatedIntegration = await Integrations.findOneAndUpdate( { _id: integrationId }, @@ -191,6 +199,7 @@ export const updateIncomingIntegration = async ( Meteor.methods({ async updateIncomingIntegration(integrationId, integration) { + methodDeprecationLogger.method('updateIncomingIntegration', '9.0.0', '/v1/integrations.update'); if (!this.userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateOutgoingIntegration', diff --git a/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts index 033e797a6d65a..c965a4c88d955 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts @@ -6,6 +6,7 @@ import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger'; import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; import { validateOutgoingIntegration } from '../../lib/validateOutgoingIntegration'; import { validateScriptEngine } from '../../lib/validateScriptEngine'; @@ -75,6 +76,7 @@ export const addOutgoingIntegration = async (userId: string, integration: INewOu Meteor.methods({ async addOutgoingIntegration(integration: INewOutgoingIntegration): Promise { + methodDeprecationLogger.method('addOutgoingIntegration', '9.0.0', '/v1/integrations.create'); const { userId } = this; if (!userId) { throw new Meteor.Error('Invalid User'); diff --git a/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts index 46d4c65159c07..b9e44d7d5adbd 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts @@ -3,6 +3,7 @@ import { Integrations, IntegrationHistory } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger'; import { notifyOnIntegrationChangedById } from '../../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ddp-client' { @@ -48,6 +49,7 @@ export const deleteOutgoingIntegration = async (integrationId: string, userId: s Meteor.methods({ async deleteOutgoingIntegration(integrationId) { + methodDeprecationLogger.method('deleteOutgoingIntegration', '9.0.0', '/v1/integrations.remove'); const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('not_authorized', 'Unauthorized', { diff --git a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts index aee22f252efab..f76b70e1ba768 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts @@ -5,6 +5,7 @@ import { wrapExceptions } from '@rocket.chat/tools'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger'; import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; import { validateOutgoingIntegration } from '../../lib/validateOutgoingIntegration'; import { isScriptEngineFrozen, validateScriptEngine } from '../../lib/validateScriptEngine'; @@ -119,6 +120,7 @@ export const updateOutgoingIntegration = async ( Meteor.methods({ async updateOutgoingIntegration(integrationId, _integration) { + methodDeprecationLogger.method('updateOutgoingIntegration', '9.0.0', '/v1/integrations.update'); if (!this.userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateOutgoingIntegration', diff --git a/apps/meteor/app/lib/server/functions/blockUser.ts b/apps/meteor/app/lib/server/functions/blockUser.ts new file mode 100644 index 0000000000000..c748ade354a34 --- /dev/null +++ b/apps/meteor/app/lib/server/functions/blockUser.ts @@ -0,0 +1,35 @@ +import { Subscriptions, Rooms } from '@rocket.chat/models'; +import { Meteor } from 'meteor/meteor'; + +import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; +import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; +import { notifyOnSubscriptionChangedByRoomIdAndUserIds } from '../lib/notifyListener'; + +export const blockUserMethod = async (userId: string, { rid, blocked }: { rid: string; blocked: string }): Promise => { + const room = await Rooms.findOne({ _id: rid }); + + if (!room) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' }); + } + + if (!(await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.BLOCK, userId))) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' }); + } + + const [blockedUser, blockerUser] = await Promise.all([ + Subscriptions.findOneByRoomIdAndUserId(rid, blocked, { projection: { _id: 1 } }), + Subscriptions.findOneByRoomIdAndUserId(rid, userId, { projection: { _id: 1 } }), + ]); + + if (!blockedUser || !blockerUser) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' }); + } + + const [blockedResponse, blockerResponse] = await Subscriptions.setBlockedByRoomId(rid, blocked, userId); + + const listenerUsers = [...(blockedResponse?.modifiedCount ? [blocked] : []), ...(blockerResponse?.modifiedCount ? [userId] : [])]; + + if (listenerUsers.length) { + void notifyOnSubscriptionChangedByRoomIdAndUserIds(rid, listenerUsers); + } +}; diff --git a/apps/meteor/app/lib/server/functions/createDirectRoom.ts b/apps/meteor/app/lib/server/functions/createDirectRoom.ts index 957d8e057d294..8c5e1fbdd3249 100644 --- a/apps/meteor/app/lib/server/functions/createDirectRoom.ts +++ b/apps/meteor/app/lib/server/functions/createDirectRoom.ts @@ -82,6 +82,9 @@ export async function createDirectRoom( const isNewRoom = !room; + // Mirror setUserActiveStatus: a 1-on-1 DM containing a deactivated user is read-only + const isReadOnlyForDeactivatedMember = uids.length === 2 && roomMembers.some((member) => member.active === false); + const roomInfo = { t: 'd', usernames, @@ -90,6 +93,7 @@ export async function createDirectRoom( ts: new Date(), uids, ...roomExtraData, + ...(isReadOnlyForDeactivatedMember && { ro: true, reactWhenReadOnly: false }), }; if (isNewRoom) { diff --git a/apps/meteor/app/lib/server/functions/getFullUserData.ts b/apps/meteor/app/lib/server/functions/getFullUserData.ts index 702622053aeb3..08f9df9f83716 100644 --- a/apps/meteor/app/lib/server/functions/getFullUserData.ts +++ b/apps/meteor/app/lib/server/functions/getFullUserData.ts @@ -18,6 +18,8 @@ export const defaultFields = { bio: 1, reason: 1, statusText: 1, + statusSource: 1, + statusExpiresAt: 1, avatarETag: 1, federated: 1, statusLivechat: 1, @@ -110,6 +112,11 @@ export async function getFullUserDataByUniqueSearchTerm( const fields = getFields(canViewAllInfo); + // When PDP type is not local, Rocket.Chat doesn't manage ABAC attributes, so there's no point in fetching them + if (settings.get('ABAC_PDP_Type') !== 'local') { + delete fields.abacAttributes; + } + const options = { projection: { ...fields, diff --git a/apps/meteor/app/lib/server/functions/loadMessageHistory.ts b/apps/meteor/app/lib/server/functions/loadMessageHistory.ts index 5addbd896889b..6361c40ce99f3 100644 --- a/apps/meteor/app/lib/server/functions/loadMessageHistory.ts +++ b/apps/meteor/app/lib/server/functions/loadMessageHistory.ts @@ -1,4 +1,4 @@ -import type { IMessage, MessageTypesValues } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, MessageTypesValues } from '@rocket.chat/core-typings'; import { Messages, Rooms } from '@rocket.chat/models'; import type { FindOptions } from 'mongodb'; @@ -14,6 +14,7 @@ export async function loadMessageHistory({ ls, showThreadMessages = true, offset = 0, + room: providedRoom, }: { // userId is undefined if user is reading anonymously userId?: string; @@ -23,8 +24,9 @@ export async function loadMessageHistory({ ls?: string | Date; showThreadMessages?: boolean; offset?: number; + room?: IRoom; }) { - const room = await Rooms.findOneById(rid, { projection: { sysMes: 1 } }); + const room = providedRoom ?? (await Rooms.findOneById(rid, { projection: { sysMes: 1 } })); if (!room) { throw new Error('error-invalid-room'); @@ -51,46 +53,42 @@ export async function loadMessageHistory({ showThreadMessages, ).toArray() : await Messages.findVisibleByRoomIdNotContainingTypes(rid, hiddenMessageTypes, options, showThreadMessages).toArray(); - const messages = await normalizeMessagesForUser(records, userId); - let unreadNotLoaded = 0; - let firstUnread; - if (ls) { - const firstMessage = messages[messages.length - 1]; + const firstMessage = records[records.length - 1]; + const lastSeen = ls ? new Date(ls) : undefined; + const hasValidLastSeen = lastSeen !== undefined && !Number.isNaN(lastSeen.getTime()); - const lastSeen = new Date(ls); + const computeUnread = async (): Promise<{ firstUnread: IMessage | undefined; unreadNotLoaded: number }> => { + if (!hasValidLastSeen || !lastSeen || !firstMessage || !(new Date(firstMessage.ts) > lastSeen)) { + return { firstUnread: undefined, unreadNotLoaded: 0 }; + } - if (firstMessage && new Date(firstMessage.ts) > lastSeen) { - const unreadMessages = Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes( + const [firstUnreadRecords, unreadNotLoaded] = await Promise.all([ + Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes( rid, lastSeen, firstMessage.ts, hiddenMessageTypes, - { - limit: 1, - sort: { - ts: 1, - }, - }, + { limit: 1, sort: { ts: 1 } }, showThreadMessages, - ); - - const totalCursor = await Messages.countVisibleByRoomIdBetweenTimestampsNotContainingTypes( + ).toArray(), + Messages.countVisibleByRoomIdBetweenTimestampsNotContainingTypes( rid, lastSeen, firstMessage.ts, hiddenMessageTypes, showThreadMessages, - ); + ), + ]); - firstUnread = (await unreadMessages.toArray())[0]; - unreadNotLoaded = totalCursor; - } - } + return { firstUnread: firstUnreadRecords[0], unreadNotLoaded }; + }; + + const [messages, unread] = await Promise.all([normalizeMessagesForUser(records, userId), computeUnread()]); return { messages, - firstUnread, - unreadNotLoaded, + firstUnread: unread.firstUnread, + unreadNotLoaded: unread.unreadNotLoaded, }; } diff --git a/apps/meteor/app/lib/server/functions/saveSettingsBulk.ts b/apps/meteor/app/lib/server/functions/saveSettingsBulk.ts new file mode 100644 index 0000000000000..2ae4d607ce630 --- /dev/null +++ b/apps/meteor/app/lib/server/functions/saveSettingsBulk.ts @@ -0,0 +1,143 @@ +import type { ISetting, ISettingColor } from '@rocket.chat/core-typings'; +import { isSettingCode, isSettingColor } from '@rocket.chat/core-typings'; +import { Settings } from '@rocket.chat/models'; +import { Match, check } from 'meteor/check'; +import { Meteor } from 'meteor/meteor'; + +import { disableCustomScripts } from './disableCustomScripts'; +import { updateAuditedByUser } from '../../../../server/settings/lib/auditedSettingUpdates'; +import { getSettingPermissionId } from '../../../authorization/lib'; +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { settings } from '../../../settings/server'; +import { checkSettingValueBounds } from '../lib/checkSettingValueBonds'; +import { notifyOnSettingChangedById } from '../lib/notifyListener'; + +const validJSON = Match.Where((value: string) => { + try { + value === '' || JSON.parse(value); + return true; + } catch (_) { + throw new Meteor.Error('Invalid JSON provided'); + } +}); + +const checkInteger = (value: ISetting['value']) => { + if (!Number.isInteger(value)) { + throw new Meteor.Error('error-invalid-setting-value', `Invalid setting value ${value}`, { + method: 'saveSettings', + }); + } +}; + +export type SaveSettingsAudit = { + username: string; + ip: string; + useragent: string; +}; + +export const saveSettingsBulk = async ( + uid: string, + params: { _id: ISetting['_id']; value: ISetting['value']; editor?: ISettingColor['editor'] }[], + audit: SaveSettingsAudit, +): Promise => { + const settingsNotAllowed: ISetting['_id'][] = []; + + const editPrivilegedSetting = await hasPermissionAsync(uid, 'edit-privileged-setting'); + const manageSelectedSettings = await hasPermissionAsync(uid, 'manage-selected-settings'); + + // if the id contains Organization_Name then change the Site_Name + const orgName = params.find(({ _id }) => _id === 'Organization_Name'); + + if (orgName) { + // check if the site name is still the default value or ifs the same as organization name + const siteName = await Settings.findOneById('Site_Name'); + + if (siteName?.value === siteName?.packageValue || siteName?.value === settings.get('Organization_Name')) { + params.push({ + _id: 'Site_Name', + value: orgName.value, + }); + } + } + + await Promise.all( + params.map(async ({ _id, value }) => { + // Verify the _id passed in is a string. + check(_id, String); + if (!editPrivilegedSetting && !(manageSelectedSettings && (await hasPermissionAsync(uid, getSettingPermissionId(_id))))) { + return settingsNotAllowed.push(_id); + } + + // Disable custom scripts in cloud trials to prevent phishing campaigns + if (disableCustomScripts() && /^Custom_Script_/.test(_id)) { + return settingsNotAllowed.push(_id); + } + + const setting = await Settings.findOneById(_id); + // Verify the value is what it should be + switch (setting?.type) { + case 'roomPick': + check(value, Match.OneOf([Object], '')); + break; + case 'boolean': + check(value, Boolean); + break; + case 'timespan': + case 'int': + case 'range': + check(value, Number); + checkInteger(value); + checkSettingValueBounds(setting, value); + break; + case 'multiSelect': + check(value, Array); + break; + case 'code': + check(value, String); + if (isSettingCode(setting) && setting.code === 'application/json') { + check(value, validJSON); + } + break; + default: + check(value, String); + break; + } + }), + ); + + if (settingsNotAllowed.length) { + throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', { + method: 'saveSettings', + settingIds: settingsNotAllowed, + }); + } + + const auditSettingOperation = updateAuditedByUser({ + _id: uid, + username: audit.username, + ip: audit.ip, + useragent: audit.useragent, + }); + + const promises = params.map(async ({ _id, value, editor }) => { + const valueResult = await auditSettingOperation(Settings.updateValueById, _id, value); + + if (!editor) { + return Boolean(valueResult?.modifiedCount); + } + + const setting = await Settings.findOneById(_id, { projection: { type: 1 } }); + if (!setting || !isSettingColor(setting)) { + return Boolean(valueResult?.modifiedCount); + } + + const { modifiedCount } = await Settings.updateOptionsById(_id, { editor }); + return Boolean(valueResult?.modifiedCount) || Boolean(modifiedCount); + }); + + (await Promise.all(promises)).forEach((changed, index) => { + if (changed) { + void notifyOnSettingChangedById(params[index]._id); + } + }); +}; diff --git a/apps/meteor/app/lib/server/functions/setUserAvatar.ts b/apps/meteor/app/lib/server/functions/setUserAvatar.ts index d65e73a887d63..8665c2b7160f0 100644 --- a/apps/meteor/app/lib/server/functions/setUserAvatar.ts +++ b/apps/meteor/app/lib/server/functions/setUserAvatar.ts @@ -110,10 +110,10 @@ export async function setUserAvatar( } catch (e) { SystemLogger.info({ msg: 'Not a valid response from the avatar url', - url: encodeURI(dataURI), + url: dataURI, err: e, }); - throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${encodeURI(dataURI)}`, { + throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${dataURI}`, { function: 'setUserAvatar', url: dataURI, }); @@ -123,13 +123,13 @@ export async function setUserAvatar( if (response.status !== 404) { SystemLogger.info({ msg: 'Error while handling the setting of the avatar from a url', - url: encodeURI(dataURI), + url: dataURI, username: user.username, status: response.status, }); throw new Meteor.Error( 'error-avatar-url-handling', - `Error while handling avatar setting from a URL (${encodeURI(dataURI)}) for ${user.username}`, + `Error while handling avatar setting from a URL (${dataURI}) for ${user.username}`, { function: 'RocketChat.setUserAvatar', url: dataURI, username: user.username }, ); } diff --git a/apps/meteor/app/lib/server/functions/unblockUser.ts b/apps/meteor/app/lib/server/functions/unblockUser.ts new file mode 100644 index 0000000000000..03ee770f69d6b --- /dev/null +++ b/apps/meteor/app/lib/server/functions/unblockUser.ts @@ -0,0 +1,23 @@ +import { Subscriptions } from '@rocket.chat/models'; +import { Meteor } from 'meteor/meteor'; + +import { notifyOnSubscriptionChangedByRoomIdAndUserIds } from '../lib/notifyListener'; + +export const unblockUserMethod = async (userId: string, { rid, blocked }: { rid: string; blocked: string }): Promise => { + const [blockedUser, blockerUser] = await Promise.all([ + Subscriptions.findOneByRoomIdAndUserId(rid, blocked, { projection: { _id: 1 } }), + Subscriptions.findOneByRoomIdAndUserId(rid, userId, { projection: { _id: 1 } }), + ]); + + if (!blockedUser || !blockerUser) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'unblockUser' }); + } + + const [blockedResponse, blockerResponse] = await Subscriptions.unsetBlockedByRoomId(rid, blocked, userId); + + const listenerUsers = [...(blockedResponse?.modifiedCount ? [blocked] : []), ...(blockerResponse?.modifiedCount ? [userId] : [])]; + + if (listenerUsers.length) { + void notifyOnSubscriptionChangedByRoomIdAndUserIds(rid, listenerUsers); + } +}; diff --git a/apps/meteor/app/lib/server/lib/RateLimiter.js b/apps/meteor/app/lib/server/lib/RateLimiter.js index cdb32299263d2..ac2b35bad3c97 100644 --- a/apps/meteor/app/lib/server/lib/RateLimiter.js +++ b/apps/meteor/app/lib/server/lib/RateLimiter.js @@ -2,7 +2,7 @@ import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; export const RateLimiterClass = new (class { limitMethod(methodName, numRequests, timeInterval, matchers) { - if (process.env.TEST_MODE === 'true') { + if (process.env.TEST_MODE === 'true' || process.env.TEST_MODE === 'api') { return; } const match = { diff --git a/apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts b/apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts index 0d1bff3250a4c..9a1b46374c49a 100644 --- a/apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts +++ b/apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts @@ -26,12 +26,20 @@ const compareVersions = (version: string, message: string) => { export type DeprecationLoggerNextPlannedVersion = '9.0.0'; +// `TEST_MODE=true` makes the logger throw so accidental usage of a deprecated +// method/endpoint surfaces immediately in the test run. The API end-to-end +// suite sets `TEST_MODE=api` instead — every other test-mode behavior stays on +// (rate limiter bypass, short cache TTLs, etc.) but the deprecation logger +// only logs because the suite intentionally exercises deprecated DDP methods +// through `/v1/method.call/:method` and the streamer. +const shouldThrowOnDeprecation = process.env.TEST_MODE === 'true'; + export const apiDeprecationLogger = ((logger) => { return { endpoint: (endpoint: string, version: DeprecationLoggerNextPlannedVersion, res: Response, info = '') => { const message = `The endpoint "${endpoint}" is deprecated and will be removed on version ${version}${info ? ` (${info})` : ''}`; - if (process.env.TEST_MODE === 'true') { + if (shouldThrowOnDeprecation) { throw new Error(message); } @@ -59,7 +67,7 @@ export const apiDeprecationLogger = ((logger) => { }) ?? `The parameter "${parameter}" in the endpoint "${endpoint}" is deprecated and will be removed on version ${version}`; compareVersions(version, message); - if (process.env.TEST_MODE === 'true') { + if (shouldThrowOnDeprecation) { throw new Error(message); } @@ -87,7 +95,7 @@ export const apiDeprecationLogger = ((logger) => { version, }) ?? `The usage of the endpoint "${endpoint}" is deprecated and will be removed on version ${version}`; - if (process.env.TEST_MODE === 'true') { + if (shouldThrowOnDeprecation) { throw new Error(message); } @@ -114,7 +122,7 @@ export const methodDeprecationLogger = ((logger) => { const paths = infoArray.map((p) => (typeof p === 'string' && p.startsWith('/') ? `"${p}"` : p)).join(' or '); const replacement = infoArray.length > 0 ? `Use the ${paths} endpoint${infoArray.length > 1 ? 's' : ''} instead` : ''; const message = `The method "${method}" is deprecated and will be removed on version ${version}${replacement ? ` (${replacement})` : ''}`; - if (process.env.TEST_MODE === 'true') { + if (shouldThrowOnDeprecation) { throw new Error(message); } compareVersions(version, message); @@ -124,7 +132,7 @@ export const methodDeprecationLogger = ((logger) => { }, parameter: (method: string, parameter: string, version: DeprecationLoggerNextPlannedVersion) => { const message = `The parameter "${parameter}" in the method "${method}" is deprecated and will be removed on version ${version}`; - if (process.env.TEST_MODE === 'true') { + if (shouldThrowOnDeprecation) { throw new Error(message); } @@ -149,7 +157,7 @@ export const methodDeprecationLogger = ((logger) => { version, }) ?? `The usage of the method "${method}" is deprecated and will be removed on version ${version}`; - if (process.env.TEST_MODE === 'true') { + if (shouldThrowOnDeprecation) { throw new Error(message); } @@ -162,7 +170,7 @@ export const methodDeprecationLogger = ((logger) => { }, /** @deprecated */ warn: (message: string) => { - if (process.env.TEST_MODE === 'true') { + if (shouldThrowOnDeprecation) { throw new Error(message); } diff --git a/apps/meteor/app/lib/server/methods/addUserToRoom.ts b/apps/meteor/app/lib/server/methods/addUserToRoom.ts index 8100cafc33b00..4c23ae7494ad4 100644 --- a/apps/meteor/app/lib/server/methods/addUserToRoom.ts +++ b/apps/meteor/app/lib/server/methods/addUserToRoom.ts @@ -2,6 +2,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Meteor } from 'meteor/meteor'; import { addUsersToRoomMethod } from './addUsersToRoom'; +import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -12,6 +13,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async addUserToRoom(data) { + methodDeprecationLogger.method('addUserToRoom', '9.0.0', ['/v1/channels.invite', '/v1/groups.invite']); const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { diff --git a/apps/meteor/app/lib/server/methods/addUsersToRoom.ts b/apps/meteor/app/lib/server/methods/addUsersToRoom.ts index c222a7a8b753d..81aaa465e7f4e 100644 --- a/apps/meteor/app/lib/server/methods/addUsersToRoom.ts +++ b/apps/meteor/app/lib/server/methods/addUsersToRoom.ts @@ -9,6 +9,7 @@ import { beforeAddUsersToRoom } from '../../../../server/lib/callbacks/beforeAdd import { i18n } from '../../../../server/lib/i18n'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { addUserToRoom } from '../functions/addUserToRoom'; +import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -122,6 +123,7 @@ export const addUsersToRoomMethod = async (userId: string, data: { rid: string; Meteor.methods({ async addUsersToRoom(data) { + methodDeprecationLogger.method('addUsersToRoom', '9.0.0', ['/v1/channels.invite', '/v1/groups.invite']); const uid = Meteor.userId(); // Validate user and room if (!uid) { diff --git a/apps/meteor/app/lib/server/methods/archiveRoom.ts b/apps/meteor/app/lib/server/methods/archiveRoom.ts index 1ccc74ee644f5..5a7e989df175d 100644 --- a/apps/meteor/app/lib/server/methods/archiveRoom.ts +++ b/apps/meteor/app/lib/server/methods/archiveRoom.ts @@ -8,6 +8,7 @@ import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { archiveRoom } from '../functions/archiveRoom'; +import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -42,6 +43,7 @@ export const executeArchiveRoom = async (userId: string, rid: string) => { Meteor.methods({ async archiveRoom(rid) { + methodDeprecationLogger.method('archiveRoom', '9.0.0', '/v1/channels.archive'); const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'archiveRoom' }); diff --git a/apps/meteor/app/lib/server/methods/blockUser.ts b/apps/meteor/app/lib/server/methods/blockUser.ts index 7fe6ec803dd1d..cc873f62e8e24 100644 --- a/apps/meteor/app/lib/server/methods/blockUser.ts +++ b/apps/meteor/app/lib/server/methods/blockUser.ts @@ -1,11 +1,9 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Subscriptions, Rooms } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; -import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; -import { notifyOnSubscriptionChangedByRoomIdAndUserIds } from '../lib/notifyListener'; +import { blockUserMethod } from '../functions/blockUser'; +import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -16,40 +14,17 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async blockUser({ rid, blocked }) { + methodDeprecationLogger.method('blockUser', '9.0.0', '/v1/im.blockUser'); check(rid, String); check(blocked, String); + const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'blockUser' }); } - const room = await Rooms.findOne({ _id: rid }); - - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' }); - } - - if (!(await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.BLOCK, userId))) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' }); - } - - const [blockedUser, blockerUser] = await Promise.all([ - Subscriptions.findOneByRoomIdAndUserId(rid, blocked, { projection: { _id: 1 } }), - Subscriptions.findOneByRoomIdAndUserId(rid, userId, { projection: { _id: 1 } }), - ]); - - if (!blockedUser || !blockerUser) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' }); - } - - const [blockedResponse, blockerResponse] = await Subscriptions.setBlockedByRoomId(rid, blocked, userId); - - const listenerUsers = [...(blockedResponse?.modifiedCount ? [blocked] : []), ...(blockerResponse?.modifiedCount ? [userId] : [])]; - - if (listenerUsers.length) { - void notifyOnSubscriptionChangedByRoomIdAndUserIds(rid, listenerUsers); - } + await blockUserMethod(userId, { rid, blocked }); return true; }, diff --git a/apps/meteor/app/lib/server/methods/createChannel.ts b/apps/meteor/app/lib/server/methods/createChannel.ts index 210dada149712..2180d4bc1d235 100644 --- a/apps/meteor/app/lib/server/methods/createChannel.ts +++ b/apps/meteor/app/lib/server/methods/createChannel.ts @@ -6,6 +6,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { createRoom } from '../functions/createRoom'; +import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -60,6 +61,7 @@ export const createChannelMethod = async ( Meteor.methods({ async createChannel(name, members, readOnly = false, customFields = {}, extraData = {}) { + methodDeprecationLogger.method('createChannel', '9.0.0', '/v1/channels.create'); const uid = Meteor.userId(); if (!uid) { diff --git a/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts b/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts index 478a7872ce0fe..25ad56575e278 100644 --- a/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts +++ b/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts @@ -9,6 +9,7 @@ import { Meteor } from 'meteor/meteor'; import { trim } from '../../../../lib/utils/stringUtils'; import { settings } from '../../../settings/server'; import { deleteUser } from '../functions/deleteUser'; +import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -63,6 +64,7 @@ export const deleteUserOwnAccount = async (fromUserId: string, password: string, Meteor.methods({ async deleteUserOwnAccount(password, confirmRelinquish) { + methodDeprecationLogger.method('deleteUserOwnAccount', '9.0.0', '/v1/users.deleteOwnAccount'); check(password, String); const uid = Meteor.userId(); diff --git a/apps/meteor/app/lib/server/methods/getChannelHistory.ts b/apps/meteor/app/lib/server/methods/getChannelHistory.ts index cd2096a82a1a9..a41fc82dc3d54 100644 --- a/apps/meteor/app/lib/server/methods/getChannelHistory.ts +++ b/apps/meteor/app/lib/server/methods/getChannelHistory.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { settings } from '../../../settings/server/cached'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; +import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; import { getHiddenSystemMessages } from '../lib/getHiddenSystemMessages'; declare module '@rocket.chat/ddp-client' { @@ -155,6 +156,7 @@ export const getChannelHistory = async ({ Meteor.methods({ async getChannelHistory({ rid, latest, oldest, inclusive, offset = 0, count = 20, unreads, showThreadMessages = true }) { + methodDeprecationLogger.method('getChannelHistory', '9.0.0', '/v1/channels.history'); check(rid, String); if (!Meteor.userId()) { diff --git a/apps/meteor/app/lib/server/methods/getUsernameSuggestion.ts b/apps/meteor/app/lib/server/methods/getUsernameSuggestion.ts index b42799f240813..601f0e9564b71 100644 --- a/apps/meteor/app/lib/server/methods/getUsernameSuggestion.ts +++ b/apps/meteor/app/lib/server/methods/getUsernameSuggestion.ts @@ -2,6 +2,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Meteor } from 'meteor/meteor'; import { generateUsernameSuggestion } from '../functions/getUsernameSuggestion'; +import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -12,6 +13,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async getUsernameSuggestion() { + methodDeprecationLogger.method('getUsernameSuggestion', '9.0.0', '/v1/users.getUsernameSuggestion'); const user = await Meteor.userAsync(); if (!user) { diff --git a/apps/meteor/app/lib/server/methods/joinRoom.ts b/apps/meteor/app/lib/server/methods/joinRoom.ts index 787f729bd578b..3ea5d3fbe6273 100644 --- a/apps/meteor/app/lib/server/methods/joinRoom.ts +++ b/apps/meteor/app/lib/server/methods/joinRoom.ts @@ -5,6 +5,8 @@ import { Rooms } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; + declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -14,6 +16,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async joinRoom(rid, code) { + methodDeprecationLogger.method('joinRoom', '9.0.0', '/v1/rooms.join'); check(rid, String); const user = await Meteor.userAsync(); diff --git a/apps/meteor/app/lib/server/methods/leaveRoom.ts b/apps/meteor/app/lib/server/methods/leaveRoom.ts index 8ded8b56ce2b0..171e37ce54ec7 100644 --- a/apps/meteor/app/lib/server/methods/leaveRoom.ts +++ b/apps/meteor/app/lib/server/methods/leaveRoom.ts @@ -9,6 +9,7 @@ import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; import { removeUserFromRoom } from '../functions/removeUserFromRoom'; +import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -58,6 +59,7 @@ export const leaveRoomMethod = async (user: IUser, rid: string): Promise = Meteor.methods({ async leaveRoom(rid) { + methodDeprecationLogger.method('leaveRoom', '9.0.0', ['/v1/channels.leave', '/v1/groups.leave', '/v1/im.leave']); check(rid, String); if (!Meteor.userId()) { diff --git a/apps/meteor/app/lib/server/methods/saveSetting.ts b/apps/meteor/app/lib/server/methods/saveSetting.ts index 044a618dacac0..5cbc8559ab10e 100644 --- a/apps/meteor/app/lib/server/methods/saveSetting.ts +++ b/apps/meteor/app/lib/server/methods/saveSetting.ts @@ -9,6 +9,7 @@ import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; import { getSettingPermissionId } from '../../../authorization/lib'; import { hasPermissionAsync, hasAllPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { disableCustomScripts } from '../functions/disableCustomScripts'; +import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; import { notifyOnSettingChanged } from '../lib/notifyListener'; declare module '@rocket.chat/ddp-client' { @@ -20,6 +21,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ saveSetting: twoFactorRequired(async function (_id: string, value: SettingValue, editor: SettingEditor) { + methodDeprecationLogger.method('saveSetting', '9.0.0', '/v1/settings/:_id'); const uid = Meteor.userId(); if (!uid) { throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', { @@ -74,9 +76,9 @@ Meteor.methods({ useragent: this.connection?.httpHeaders['user-agent'] || '', }); - (await auditSettingOperation(Settings.updateValueAndEditorById, _id, value as SettingValue, editor)).modifiedCount && + (await auditSettingOperation(Settings.updateValueAndEditorById, _id, value, editor)).modifiedCount && setting && - void notifyOnSettingChanged({ ...setting, editor, value: value as SettingValue }); + void notifyOnSettingChanged({ ...setting, editor, value }); return true; }), diff --git a/apps/meteor/app/lib/server/methods/saveSettings.ts b/apps/meteor/app/lib/server/methods/saveSettings.ts index 11458f2f0be78..c01f40481b106 100644 --- a/apps/meteor/app/lib/server/methods/saveSettings.ts +++ b/apps/meteor/app/lib/server/methods/saveSettings.ts @@ -1,18 +1,10 @@ import type { ISetting } from '@rocket.chat/core-typings'; -import { isSettingCode } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Settings } from '@rocket.chat/models'; -import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { updateAuditedByUser } from '../../../../server/settings/lib/auditedSettingUpdates'; import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; -import { getSettingPermissionId } from '../../../authorization/lib'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { settings } from '../../../settings/server'; -import { disableCustomScripts } from '../functions/disableCustomScripts'; -import { checkSettingValueBounds } from '../lib/checkSettingValueBonds'; -import { notifyOnSettingChangedById } from '../lib/notifyListener'; +import { saveSettingsBulk } from '../functions/saveSettingsBulk'; +import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -26,23 +18,6 @@ declare module '@rocket.chat/ddp-client' { } } -const validJSON = Match.Where((value: string) => { - try { - value === '' || JSON.parse(value); - return true; - } catch (_) { - throw new Meteor.Error('Invalid JSON provided'); - } -}); - -const checkInteger = (value: ISetting['value']) => { - if (!Number.isInteger(value)) { - throw new Meteor.Error('error-invalid-setting-value', `Invalid setting value ${value}`, { - method: 'saveSettings', - }); - } -}; - Meteor.methods({ saveSettings: twoFactorRequired(async function ( params: { @@ -50,96 +25,19 @@ Meteor.methods({ value: ISetting['value']; }[] = [], ) { + methodDeprecationLogger.method('saveSettings', '9.0.0', '/v1/settings'); + const uid = Meteor.userId(); - const settingsNotAllowed: ISetting['_id'][] = []; if (uid === null) { throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', { method: 'saveSetting', }); } - const editPrivilegedSetting = await hasPermissionAsync(uid, 'edit-privileged-setting'); - const manageSelectedSettings = await hasPermissionAsync(uid, 'manage-selected-settings'); - - // if the id contains Organization_Name then change the Site_Name - const orgName = params.find(({ _id }) => _id === 'Organization_Name'); - - if (orgName) { - // check if the site name is still the default value or ifs the same as organization name - const siteName = await Settings.findOneById('Site_Name'); - - if (siteName?.value === siteName?.packageValue || siteName?.value === settings.get('Organization_Name')) { - params.push({ - _id: 'Site_Name', - value: orgName.value, - }); - } - } - - await Promise.all( - params.map(async ({ _id, value }) => { - // Verify the _id passed in is a string. - check(_id, String); - if (!editPrivilegedSetting && !(manageSelectedSettings && (await hasPermissionAsync(uid, getSettingPermissionId(_id))))) { - return settingsNotAllowed.push(_id); - } - - // Disable custom scripts in cloud trials to prevent phishing campaigns - if (disableCustomScripts() && /^Custom_Script_/.test(_id)) { - return settingsNotAllowed.push(_id); - } - - const setting = await Settings.findOneById(_id); - // Verify the value is what it should be - switch (setting?.type) { - case 'roomPick': - check(value, Match.OneOf([Object], '')); - break; - case 'boolean': - check(value, Boolean); - break; - case 'timespan': - case 'int': - case 'range': - check(value, Number); - checkInteger(value); - checkSettingValueBounds(setting, value); - break; - case 'multiSelect': - check(value, Array); - break; - case 'code': - check(value, String); - if (isSettingCode(setting) && setting.code === 'application/json') { - check(value, validJSON); - } - break; - default: - check(value, String); - break; - } - }), - ); - - if (settingsNotAllowed.length) { - throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', { - method: 'saveSettings', - settingIds: settingsNotAllowed, - }); - } - const auditSettingOperation = updateAuditedByUser({ - _id: uid, + await saveSettingsBulk(uid, params, { username: (await Meteor.userAsync())!.username!, - ip: this.connection!.clientAddress || '', - useragent: this.connection!.httpHeaders['user-agent'] || '', - }); - - const promises = params.map(({ _id, value }) => auditSettingOperation(Settings.updateValueById, _id, value)); - - (await Promise.all(promises)).forEach((value, index) => { - if (value?.modifiedCount) { - void notifyOnSettingChangedById(params[index]._id); - } + ip: this.connection.clientAddress || '', + useragent: this.connection.httpHeaders['user-agent'] || '', }); return true; diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 2f5094e138f88..cf5deabc68a66 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -18,7 +18,6 @@ import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; import { sendMessage } from '../functions/sendMessage'; import { RateLimiter } from '../lib'; - /** * * @param uid diff --git a/apps/meteor/app/lib/server/methods/setEmail.ts b/apps/meteor/app/lib/server/methods/setEmail.ts index e1faece1e9919..67c59572631d1 100644 --- a/apps/meteor/app/lib/server/methods/setEmail.ts +++ b/apps/meteor/app/lib/server/methods/setEmail.ts @@ -6,6 +6,7 @@ import { Meteor } from 'meteor/meteor'; import { settings } from '../../../settings/server'; import { setEmail } from '../functions/setEmail'; import { RateLimiter } from '../lib'; +import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -24,7 +25,7 @@ export const setEmailFunction = async (email: string, user: Meteor.User | IUser) }); } - if (user.emails?.[0] && user.emails[0].address === email) { + if (user.emails?.[0]?.address === email) { return email; } @@ -39,6 +40,7 @@ export const setEmailFunction = async (email: string, user: Meteor.User | IUser) Meteor.methods({ async setEmail(email) { + methodDeprecationLogger.method('setEmail', '9.0.0', '/v1/users.updateOwnBasicInfo'); const user = await Meteor.userAsync(); if (!user) { diff --git a/apps/meteor/app/lib/server/methods/setRealName.ts b/apps/meteor/app/lib/server/methods/setRealName.ts index 2a1bac15b63bc..da72e1b6257bd 100644 --- a/apps/meteor/app/lib/server/methods/setRealName.ts +++ b/apps/meteor/app/lib/server/methods/setRealName.ts @@ -5,6 +5,7 @@ import { Meteor } from 'meteor/meteor'; import { settings } from '../../../settings/server'; import { setRealName } from '../functions/setRealName'; import { RateLimiter } from '../lib'; +import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -15,6 +16,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async setRealName(name) { + methodDeprecationLogger.method('setRealName', '9.0.0', '/v1/users.updateOwnBasicInfo'); check(name, String); const userId = Meteor.userId(); if (!userId) { diff --git a/apps/meteor/app/lib/server/methods/unarchiveRoom.ts b/apps/meteor/app/lib/server/methods/unarchiveRoom.ts index 1d28dc54bc2f4..389f0af6c8d5f 100644 --- a/apps/meteor/app/lib/server/methods/unarchiveRoom.ts +++ b/apps/meteor/app/lib/server/methods/unarchiveRoom.ts @@ -6,6 +6,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { unarchiveRoom } from '../functions/unarchiveRoom'; +import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -37,6 +38,7 @@ export const executeUnarchiveRoom = async (userId: string, rid: string) => { Meteor.methods({ async unarchiveRoom(rid) { + methodDeprecationLogger.method('unarchiveRoom', '9.0.0', '/v1/channels.unarchive'); const userId = Meteor.userId(); if (!userId) { diff --git a/apps/meteor/app/lib/server/methods/unblockUser.ts b/apps/meteor/app/lib/server/methods/unblockUser.ts index 7b4bc56600103..ef940bf40b4eb 100644 --- a/apps/meteor/app/lib/server/methods/unblockUser.ts +++ b/apps/meteor/app/lib/server/methods/unblockUser.ts @@ -1,9 +1,9 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Subscriptions } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { notifyOnSubscriptionChangedByRoomIdAndUserIds } from '../lib/notifyListener'; +import { unblockUserMethod } from '../functions/unblockUser'; +import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -14,30 +14,17 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async unblockUser({ rid, blocked }) { + methodDeprecationLogger.method('unblockUser', '9.0.0', '/v1/im.blockUser'); check(rid, String); check(blocked, String); + const userId = Meteor.userId(); if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'blockUser' }); - } - - const [blockedUser, blockerUser] = await Promise.all([ - Subscriptions.findOneByRoomIdAndUserId(rid, blocked, { projection: { _id: 1 } }), - Subscriptions.findOneByRoomIdAndUserId(rid, userId, { projection: { _id: 1 } }), - ]); - - if (!blockedUser || !blockerUser) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' }); + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'unblockUser' }); } - const [blockedResponse, blockerResponse] = await Subscriptions.unsetBlockedByRoomId(rid, blocked, userId); - - const listenerUsers = [...(blockedResponse?.modifiedCount ? [blocked] : []), ...(blockerResponse?.modifiedCount ? [userId] : [])]; - - if (listenerUsers.length) { - void notifyOnSubscriptionChangedByRoomIdAndUserIds(rid, listenerUsers); - } + await unblockUserMethod(userId, { rid, blocked }); return true; }, diff --git a/apps/meteor/app/livechat/server/api/v1/config.ts b/apps/meteor/app/livechat/server/api/v1/config.ts index 426f7c128882f..b7d8776bb6704 100644 --- a/apps/meteor/app/livechat/server/api/v1/config.ts +++ b/apps/meteor/app/livechat/server/api/v1/config.ts @@ -8,7 +8,10 @@ import { RoutingManager } from '../../lib/RoutingManager'; import { online } from '../../lib/service-status'; import { settings, findOpenRoom, getExtraConfigInfo, findAgent, findGuestWithoutActivity } from '../lib/livechat'; -const cachedSettings = mem(settings, { maxAge: process.env.TEST_MODE === 'true' ? 1 : 1000, cacheKey: JSON.stringify }); +const cachedSettings = mem(settings, { + maxAge: process.env.TEST_MODE === 'true' || process.env.TEST_MODE === 'api' ? 1 : 1000, + cacheKey: JSON.stringify, +}); API.v1.addRoute( 'livechat/config', diff --git a/apps/meteor/app/livechat/server/lib/AnalyticsTyped.ts b/apps/meteor/app/livechat/server/lib/AnalyticsTyped.ts index c0be707ba2122..c1c6b7554e257 100644 --- a/apps/meteor/app/livechat/server/lib/AnalyticsTyped.ts +++ b/apps/meteor/app/livechat/server/lib/AnalyticsTyped.ts @@ -2,7 +2,7 @@ import { OmnichannelAnalytics } from '@rocket.chat/core-services'; import mem from 'mem'; export const getAgentOverviewDataCached = mem(OmnichannelAnalytics.getAgentOverviewData, { - maxAge: process.env.TEST_MODE === 'true' ? 1 : 60000, + maxAge: process.env.TEST_MODE === 'true' || process.env.TEST_MODE === 'api' ? 1 : 60000, cacheKey: JSON.stringify, }); // Agent overview data on realtime is cached for 5 seconds @@ -12,6 +12,6 @@ export const getAnalyticsOverviewDataCached = mem(OmnichannelAnalytics.getAnalyt cacheKey: JSON.stringify, }); export const getAnalyticsOverviewDataCachedForRealtime = mem(OmnichannelAnalytics.getAnalyticsOverviewData, { - maxAge: process.env.TEST_MODE === 'true' ? 1 : 5000, + maxAge: process.env.TEST_MODE === 'true' || process.env.TEST_MODE === 'api' ? 1 : 5000, cacheKey: JSON.stringify, }); diff --git a/apps/meteor/app/livechat/server/livechat.ts b/apps/meteor/app/livechat/server/livechat.ts index 3efc2feb17bd3..70dd67257650d 100644 --- a/apps/meteor/app/livechat/server/livechat.ts +++ b/apps/meteor/app/livechat/server/livechat.ts @@ -38,7 +38,9 @@ function parseExtraAttributes(widgetData: string): string { return doc.documentElement.innerHTML; } -const memoizedParseExtraAttributes = mem(parseExtraAttributes, { maxAge: process.env.TEST_MODE === 'true' ? 1 : 60000 }); +const memoizedParseExtraAttributes = mem(parseExtraAttributes, { + maxAge: process.env.TEST_MODE === 'true' || process.env.TEST_MODE === 'api' ? 1 : 60000, +}); WebApp.connectHandlers.use('/livechat', (req, res, next) => { if (!req.url) { diff --git a/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts b/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts index 67fcaecdcba42..434b99ebb4303 100644 --- a/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts +++ b/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts @@ -4,6 +4,7 @@ import { LivechatVisitors } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { settings } from '../../../settings/server'; import type { ILivechatMessage } from '../lib/localTypes'; import { sendMessage } from '../lib/messages'; @@ -81,6 +82,7 @@ export const sendMessageLivechat = async ({ Meteor.methods({ async sendMessageLivechat({ token, _id, rid, msg, file, files, attachments }: ILivechatMessage, agent: ILivechatMessageAgent) { + methodDeprecationLogger.method('sendMessageLivechat', '9.0.0', '/v1/livechat/message'); return sendMessageLivechat({ message: { token, _id, rid, msg, file, files, attachments }, agent }); }, }); diff --git a/apps/meteor/app/livechat/server/startup.ts b/apps/meteor/app/livechat/server/startup.ts index 813644d5a51c9..1ba15a5e0baeb 100644 --- a/apps/meteor/app/livechat/server/startup.ts +++ b/apps/meteor/app/livechat/server/startup.ts @@ -120,7 +120,7 @@ Meteor.startup(async () => { } await businessHourManager.stopManager(); }, - process.env.TEST_MODE === 'true' + process.env.TEST_MODE === 'true' || process.env.TEST_MODE === 'api' ? { debounce: 10, } diff --git a/apps/meteor/app/mentions/server/methods/getUserMentionsByChannel.ts b/apps/meteor/app/mentions/server/methods/getUserMentionsByChannel.ts index 31efd978fc01f..a9e5b97b81349 100644 --- a/apps/meteor/app/mentions/server/methods/getUserMentionsByChannel.ts +++ b/apps/meteor/app/mentions/server/methods/getUserMentionsByChannel.ts @@ -5,6 +5,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { canAccessRoomAsync } from '../../../authorization/server'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -38,6 +39,7 @@ export const getUserMentionsByChannel = async ( Meteor.methods({ async getUserMentionsByChannel({ roomId, options }) { + methodDeprecationLogger.method('getUserMentionsByChannel', '9.0.0', '/v1/channels.getAllUserMentionsByChannel'); const uid = Meteor.userId(); if (!uid) { diff --git a/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts b/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts index ccf972c561e31..cdb4f5d2a31cb 100644 --- a/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts +++ b/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts @@ -4,6 +4,7 @@ import { Messages, Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import logger from './logger'; +import { methodDeprecationLogger } from '../../lib/server/lib/deprecationWarningLogger'; import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ddp-client' { @@ -81,6 +82,7 @@ export const unreadMessages = async (userId: string, firstUnreadMessage?: Pick({ async unreadMessages(firstUnreadMessage, room) { + methodDeprecationLogger.method('unreadMessages', '9.0.0', '/v1/subscriptions.unread'); const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { diff --git a/apps/meteor/app/message-pin/server/pinMessage.ts b/apps/meteor/app/message-pin/server/pinMessage.ts index 58f295ec02991..2962f4680adad 100644 --- a/apps/meteor/app/message-pin/server/pinMessage.ts +++ b/apps/meteor/app/message-pin/server/pinMessage.ts @@ -11,6 +11,7 @@ import { Meteor } from 'meteor/meteor'; import { canAccessRoomAsync, roomAccessAttributes } from '../../authorization/server'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; import { isTheLastMessage } from '../../lib/server/functions/isTheLastMessage'; +import { methodDeprecationLogger } from '../../lib/server/lib/deprecationWarningLogger'; import { notifyOnRoomChangedById, notifyOnMessageChange } from '../../lib/server/lib/notifyListener'; import { settings } from '../../settings/server'; import { getUserAvatarURL } from '../../utils/server/getUserAvatarURL'; @@ -198,6 +199,7 @@ export const unpinMessage = async (userId: string, message: IMessage) => { Meteor.methods({ async pinMessage(message, pinnedAt) { + methodDeprecationLogger.method('pinMessage', '9.0.0', '/v1/chat.pinMessage'); check(message._id, String); const userId = Meteor.userId(); @@ -210,6 +212,7 @@ Meteor.methods({ return pinMessage(message, userId, pinnedAt); }, async unpinMessage(message) { + methodDeprecationLogger.method('unpinMessage', '9.0.0', '/v1/chat.unPinMessage'); check(message._id, String); const userId = Meteor.userId(); diff --git a/apps/meteor/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.ts b/apps/meteor/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.ts index c123ce07072db..ea86151253ec4 100644 --- a/apps/meteor/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.ts +++ b/apps/meteor/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.ts @@ -4,6 +4,7 @@ import { OAuthAccessTokens, OAuthApps, OAuthAuthCodes } from '@rocket.chat/model import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -34,6 +35,7 @@ export const deleteOAuthApp = async (userId: string, applicationId: IOAuthApps[' Meteor.methods({ async deleteOAuthApp(applicationId) { + methodDeprecationLogger.method('deleteOAuthApp', '9.0.0', '/v1/oauth-apps.delete'); if (!this.userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'deleteOAuthApp' }); } diff --git a/apps/meteor/app/oauth2-server-config/server/admin/methods/updateOAuthApp.ts b/apps/meteor/app/oauth2-server-config/server/admin/methods/updateOAuthApp.ts index f6d2e26575e37..a5adfbaea4904 100644 --- a/apps/meteor/app/oauth2-server-config/server/admin/methods/updateOAuthApp.ts +++ b/apps/meteor/app/oauth2-server-config/server/admin/methods/updateOAuthApp.ts @@ -4,6 +4,7 @@ import { OAuthApps, Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger'; import { parseUriList } from '../functions/parseUriList'; declare module '@rocket.chat/ddp-client' { @@ -77,6 +78,7 @@ export const updateOAuthApp = async ( Meteor.methods({ async updateOAuthApp(applicationId, application) { + methodDeprecationLogger.method('updateOAuthApp', '9.0.0', '/v1/oauth-apps.update'); if (!this.userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateOAuthApp' }); } diff --git a/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.ts b/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.ts index ea2a2d4545192..1a737384acca7 100644 --- a/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.ts +++ b/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.ts @@ -4,6 +4,7 @@ import { Subscriptions } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { notifyOnSubscriptionChangedById } from '../../../lib/server/lib/notifyListener'; import { getUserNotificationPreference } from '../../../utils/server/getUserNotificationPreference'; @@ -132,6 +133,7 @@ export const saveNotificationSettingsMethod = async ( Meteor.methods({ async saveNotificationSettings(roomId, field, value) { + methodDeprecationLogger.method('saveNotificationSettings', '9.0.0', '/v1/rooms.saveNotification'); const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { diff --git a/apps/meteor/app/push/server/methods.ts b/apps/meteor/app/push/server/methods.ts index e4dddc8910dfc..43514d0f14735 100644 --- a/apps/meteor/app/push/server/methods.ts +++ b/apps/meteor/app/push/server/methods.ts @@ -8,6 +8,7 @@ import { Meteor } from 'meteor/meteor'; import { logger } from './logger'; import { _matchToken } from './push'; +import { methodDeprecationLogger } from '../../lib/server/lib/deprecationWarningLogger'; type PushUpdateOptions = { id?: string; @@ -27,6 +28,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async 'raix:push-update'(options) { + methodDeprecationLogger.method('raix:push-update', '9.0.0', '/v1/push.token'); logger.debug({ msg: 'Got push token from app', options }); check(options, { diff --git a/apps/meteor/app/settings/server/CachedSettings.ts b/apps/meteor/app/settings/server/CachedSettings.ts index 8332f45c5c497..9a0c119182175 100644 --- a/apps/meteor/app/settings/server/CachedSettings.ts +++ b/apps/meteor/app/settings/server/CachedSettings.ts @@ -183,7 +183,7 @@ export class CachedSettings } const mergeFunction = - process.env.TEST_MODE !== 'true' + process.env.TEST_MODE !== 'true' && process.env.TEST_MODE !== 'api' ? _.debounce((): void => { callback(_id.map((id) => this.store.get(id)?.value) as T[]); }, 100) diff --git a/apps/meteor/app/slashcommands-open/client/client.ts b/apps/meteor/app/slashcommands-open/client/client.ts index 88cee2fda319a..29354603ba716 100644 --- a/apps/meteor/app/slashcommands-open/client/client.ts +++ b/apps/meteor/app/slashcommands-open/client/client.ts @@ -27,11 +27,11 @@ slashCommands.add({ roomCoordinator.openRouteLink(subscription.t, subscription, router.getSearchParameters()); } - if (type && type.indexOf('d') === -1) { + if (type?.indexOf('d') === -1) { return; } try { - await sdk.call('createDirectMessage', room); + await sdk.rest.post('/v1/im.create', { username: room }); const subscription = Subscriptions.state.find(predicate); if (!subscription) { return; diff --git a/apps/meteor/app/slashcommands-status/client/status.ts b/apps/meteor/app/slashcommands-status/client/status.ts index 3698b5fda4cb5..04795d1c1d8b9 100644 --- a/apps/meteor/app/slashcommands-status/client/status.ts +++ b/apps/meteor/app/slashcommands-status/client/status.ts @@ -12,7 +12,7 @@ slashCommands.add({ } try { - await sdk.call('setUserStatus', undefined, params); + await sdk.rest.post('/v1/users.setStatus', { message: params }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } diff --git a/apps/meteor/app/slashcommands-status/server/status.ts b/apps/meteor/app/slashcommands-status/server/status.ts index 0a1f02b182571..89bad86a4a373 100644 --- a/apps/meteor/app/slashcommands-status/server/status.ts +++ b/apps/meteor/app/slashcommands-status/server/status.ts @@ -14,12 +14,11 @@ slashCommands.add({ return; } - const user = await Users.findOneById>( - userId, - { - projection: { language: 1, username: 1, name: 1, status: 1, roles: 1, statusText: 1 }, - }, - ); + const user = await Users.findOneById< + Pick + >(userId, { + projection: { language: 1, username: 1, name: 1, status: 1, statusDefault: 1, roles: 1, statusText: 1 }, + }); const lng = user?.language || settings.get('Language') || 'en'; if (!user) { diff --git a/apps/meteor/app/slashcommands-topic/client/topic.ts b/apps/meteor/app/slashcommands-topic/client/topic.ts index 697f87716a903..f28dddcafd9fb 100644 --- a/apps/meteor/app/slashcommands-topic/client/topic.ts +++ b/apps/meteor/app/slashcommands-topic/client/topic.ts @@ -12,7 +12,7 @@ slashCommands.add({ callback: async function Topic({ params, message }: SlashCommandCallbackParams<'topic'>): Promise { if (hasPermission('edit-room', message.rid)) { try { - await sdk.call('saveRoomSettings', message.rid, 'roomTopic', params); + await sdk.rest.post('/v1/rooms.saveRoomSettings', { rid: message.rid, roomTopic: params }); await clientCallbacks.run('roomTopicChanged', Rooms.state.get(message.rid)); } catch (error: unknown) { dispatchToastMessage({ type: 'error', message: error }); diff --git a/apps/meteor/app/statistics/server/methods/getStatistics.ts b/apps/meteor/app/statistics/server/methods/getStatistics.ts index a804ef83584e3..1b383bdc54536 100644 --- a/apps/meteor/app/statistics/server/methods/getStatistics.ts +++ b/apps/meteor/app/statistics/server/methods/getStatistics.ts @@ -2,6 +2,7 @@ import type { IStats } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Meteor } from 'meteor/meteor'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { getLastStatistics } from '../functions/getLastStatistics'; declare module '@rocket.chat/ddp-client' { @@ -13,6 +14,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async getStatistics(refresh) { + methodDeprecationLogger.method('getStatistics', '9.0.0', '/v1/statistics'); const uid = Meteor.userId(); if (!uid) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getStatistics' }); diff --git a/apps/meteor/app/threads/server/methods/followMessage.ts b/apps/meteor/app/threads/server/methods/followMessage.ts index ef32595e930a0..c8477473d771d 100644 --- a/apps/meteor/app/threads/server/methods/followMessage.ts +++ b/apps/meteor/app/threads/server/methods/followMessage.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { RateLimiter } from '../../../lib/server'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { notifyOnMessageChange } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { follow } from '../functions'; @@ -50,6 +51,7 @@ export const followMessage = async (user: IUser, { mid }: { mid: IMessage['_id'] Meteor.methods({ async followMessage({ mid }) { + methodDeprecationLogger.method('followMessage', '9.0.0', '/v1/chat.followMessage'); check(mid, String); const user = (await Meteor.userAsync()) as IUser; diff --git a/apps/meteor/app/threads/server/methods/getThreadsList.ts b/apps/meteor/app/threads/server/methods/getThreadsList.ts index c51449ff2d4d3..f9a51fa9cd79e 100644 --- a/apps/meteor/app/threads/server/methods/getThreadsList.ts +++ b/apps/meteor/app/threads/server/methods/getThreadsList.ts @@ -4,6 +4,7 @@ import { Messages, Rooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { canAccessRoomAsync } from '../../../authorization/server'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { settings } from '../../../settings/server'; const MAX_LIMIT = 100; @@ -17,6 +18,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async getThreadsList({ rid, limit = 50, skip = 0 }) { + methodDeprecationLogger.method('getThreadsList', '9.0.0', '/v1/chat.getThreadsList'); if (limit > MAX_LIMIT) { throw new Meteor.Error('error-not-allowed', `max limit: ${MAX_LIMIT}`, { method: 'getThreadsList', diff --git a/apps/meteor/app/threads/server/methods/unfollowMessage.ts b/apps/meteor/app/threads/server/methods/unfollowMessage.ts index f5ffaa19fc7af..e1d48869254ed 100644 --- a/apps/meteor/app/threads/server/methods/unfollowMessage.ts +++ b/apps/meteor/app/threads/server/methods/unfollowMessage.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { RateLimiter } from '../../../lib/server'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { notifyOnMessageChange } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { unfollow } from '../functions'; @@ -50,6 +51,7 @@ export const unfollowMessage = async (user: IUser, { mid }: { mid: IMessage['_id Meteor.methods({ async unfollowMessage({ mid }) { + methodDeprecationLogger.method('unfollowMessage', '9.0.0', '/v1/chat.unfollowMessage'); check(mid, String); const user = (await Meteor.userAsync()) as IUser; diff --git a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts index 04447635ad45e..2018abfa5fa91 100644 --- a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts +++ b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts @@ -15,7 +15,7 @@ export const createComposerAPI = ( persistDraft: (value: string) => void, initialDraft: string, quoteChainLimit: number, - composerRef: RefObject, + composerRef: RefObject, { rid, tmid }: { rid: string; tmid?: string }, ): ComposerAPI => { const triggerEvent = (input: HTMLTextAreaElement, evt: string): void => { diff --git a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts index 95f94a4e356c7..8c61f98469232 100644 --- a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts @@ -7,7 +7,6 @@ import { onClientMessageReceived } from '../../../../client/lib/onClientMessageR import { getUserId } from '../../../../client/lib/user'; import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling'; import { getConfig } from '../../../../client/lib/utils/getConfig'; -import { waitForElement } from '../../../../client/lib/utils/waitForElement'; import { Messages, Subscriptions } from '../../../../client/stores'; import { getUserPreference } from '../../../utils/client'; @@ -107,10 +106,14 @@ class RoomHistoryManagerClass extends Emitter { private run(fn: () => void) { const difference = this.lastRequest ? differenceInMilliseconds(new Date(), this.lastRequest) : Infinity; - if (difference > 500) { + // Original cooldown was 500ms which forced ~330ms wait on the second getMore call when a + // user opens a room. Pagination throughput here is bounded by the loadHistory server + // method itself, so a smaller client-side spacing is enough to avoid hammering. + const minSpacingMs = 100; + if (difference > minSpacingMs) { return fn(); } - return setTimeout(fn, 500 - difference); + return setTimeout(fn, minSpacingMs - difference); } public isLoaded(rid: IRoom['_id']) { @@ -171,12 +174,13 @@ class RoomHistoryManagerClass extends Emitter { room.oldestTs = messages[messages.length - 1].ts; } - const wrapper = await waitForElement('.messages-box .wrapper [data-overlayscrollbars-viewport]'); - - room.scroll = { - scrollHeight: wrapper.scrollHeight, - scrollTop: wrapper.scrollTop, - }; + const wrapper = document.querySelector('.messages-box .wrapper [data-overlayscrollbars-viewport]'); + if (wrapper) { + room.scroll = { + scrollHeight: wrapper.scrollHeight, + scrollTop: wrapper.scrollTop, + }; + } await upsertMessageBulk({ msgs: messages.filter((msg) => msg.t !== 'command'), @@ -189,7 +193,7 @@ class RoomHistoryManagerClass extends Emitter { room.loaded = 0; } - const visibleMessages = messages.filter((msg) => !msg.tmid || showThreadsInMainChannel || msg.tshow); + const visibleMessages = messages.filter((msg) => msg.t !== 'command' && (!msg.tmid || showThreadsInMainChannel || msg.tshow)); room.loaded += visibleMessages.length; @@ -197,7 +201,7 @@ class RoomHistoryManagerClass extends Emitter { this.updateRoom(rid, { hasMore: false }); } - if (room.hasMore && (visibleMessages.length === 0 || room.loaded < limit)) { + if (room.hasMore && visibleMessages.length === 0) { return this.getMore(rid); } diff --git a/apps/meteor/app/user-status/server/methods/deleteCustomUserStatus.ts b/apps/meteor/app/user-status/server/methods/deleteCustomUserStatus.ts index 72ccb9ccbd07d..7592e99413d30 100644 --- a/apps/meteor/app/user-status/server/methods/deleteCustomUserStatus.ts +++ b/apps/meteor/app/user-status/server/methods/deleteCustomUserStatus.ts @@ -4,6 +4,7 @@ import { CustomUserStatus } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -30,6 +31,7 @@ export const deleteCustomUserStatus = async (userId: string, userStatusID: strin Meteor.methods({ async deleteCustomUserStatus(userStatusID) { + methodDeprecationLogger.method('deleteCustomUserStatus', '9.0.0', '/v1/custom-user-status.delete'); if (!this.userId) { throw new Meteor.Error('not_authorized'); } diff --git a/apps/meteor/app/user-status/server/methods/getUserStatusText.ts b/apps/meteor/app/user-status/server/methods/getUserStatusText.ts index 911a69854b955..fa22fdde45eea 100644 --- a/apps/meteor/app/user-status/server/methods/getUserStatusText.ts +++ b/apps/meteor/app/user-status/server/methods/getUserStatusText.ts @@ -2,6 +2,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Meteor } from 'meteor/meteor'; import { getStatusText } from '../../../lib/server/functions/getStatusText'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -12,6 +13,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async getUserStatusText(userId) { + methodDeprecationLogger.method('getUserStatusText', '9.0.0', '/v1/users.presence'); const currentUserId = Meteor.userId(); if (!currentUserId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getUserStatusText' }); diff --git a/apps/meteor/app/user-status/server/methods/insertOrUpdateUserStatus.ts b/apps/meteor/app/user-status/server/methods/insertOrUpdateUserStatus.ts index 16f8dc716b06a..2d3477a02fcd6 100644 --- a/apps/meteor/app/user-status/server/methods/insertOrUpdateUserStatus.ts +++ b/apps/meteor/app/user-status/server/methods/insertOrUpdateUserStatus.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { trim } from '../../../../lib/utils/stringUtils'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; type InsertOrUpdateUserStatus = { _id?: string; @@ -104,6 +105,7 @@ export const insertOrUpdateUserStatus = async (userId: string, userStatusData: I Meteor.methods({ async insertOrUpdateUserStatus(userStatusData) { + methodDeprecationLogger.method('insertOrUpdateUserStatus', '9.0.0', ['/v1/custom-user-status.create', '/v1/custom-user-status.update']); if (!this.userId) { throw new Meteor.Error('not_authorized'); } diff --git a/apps/meteor/app/user-status/server/methods/setUserStatus.ts b/apps/meteor/app/user-status/server/methods/setUserStatus.ts index 0235c4b681853..5ad743df9fb7b 100644 --- a/apps/meteor/app/user-status/server/methods/setUserStatus.ts +++ b/apps/meteor/app/user-status/server/methods/setUserStatus.ts @@ -1,11 +1,9 @@ import { Presence } from '@rocket.chat/core-services'; -import type { IUser } from '@rocket.chat/core-typings'; +import { UserStatus, type IUser } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { RateLimiter } from '../../../lib/server'; -import { setStatusText } from '../../../lib/server/functions/setStatusText'; import { settings } from '../../../settings/server'; declare module '@rocket.chat/ddp-client' { @@ -16,30 +14,25 @@ declare module '@rocket.chat/ddp-client' { } export const setUserStatusMethod = async ( - user: Pick, + user: Pick, statusType: IUser['status'], statusText: IUser['statusText'], ): Promise => { - if (statusType) { - if (statusType === 'offline' && !settings.get('Accounts_AllowInvisibleStatusOption')) { - throw new Meteor.Error('error-status-not-allowed', 'Invisible status is disabled', { - method: 'setUserStatus', - }); - } - await Presence.setStatus(user._id, statusType); + if (statusText != null && !settings.get('Accounts_AllowUserStatusMessageChange')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { + method: 'setUserStatus', + }); } - if (statusText || statusText === '') { - check(statusText, String); - - if (!settings.get('Accounts_AllowUserStatusMessageChange')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'setUserStatus', - }); - } + const effectiveStatus = statusType || user.statusDefault || UserStatus.ONLINE; - await setStatusText(user, statusText); + if (effectiveStatus === UserStatus.OFFLINE && !settings.get('Accounts_AllowInvisibleStatusOption')) { + throw new Meteor.Error('error-status-not-allowed', 'Invisible status is disabled', { + method: 'setUserStatus', + }); } + + await Presence.setStatus(user._id, effectiveStatus, statusText); }; Meteor.methods({ diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index d0447bb5b8bfa..af6ae5e13dfeb 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "8.5.1" + "version": "8.6.0-rc.3" } diff --git a/apps/meteor/app/utils/server/functions/getBaseUserFields.ts b/apps/meteor/app/utils/server/functions/getBaseUserFields.ts index 6ee9992826727..acd5233047ddc 100644 --- a/apps/meteor/app/utils/server/functions/getBaseUserFields.ts +++ b/apps/meteor/app/utils/server/functions/getBaseUserFields.ts @@ -10,6 +10,8 @@ export const getBaseUserFields = (allowServiceKeys = false): UserFields => ({ 'status': 1, 'statusDefault': 1, 'statusText': 1, + 'statusSource': 1, + 'statusExpiresAt': 1, 'statusConnection': 1, 'bio': 1, 'avatarOrigin': 1, diff --git a/apps/meteor/app/utils/server/slashCommand.ts b/apps/meteor/app/utils/server/slashCommand.ts index 42eb86f5f9b19..faa9f0606650f 100644 --- a/apps/meteor/app/utils/server/slashCommand.ts +++ b/apps/meteor/app/utils/server/slashCommand.ts @@ -10,6 +10,8 @@ import type { import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Meteor } from 'meteor/meteor'; +import { methodDeprecationLogger } from '../../lib/server/lib/deprecationWarningLogger'; + interface ISlashCommandAddParams { command: string; callback?: SlashCommand['callback']; @@ -139,6 +141,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async slashCommand(command) { + methodDeprecationLogger.method('slashCommand', '9.0.0', '/v1/commands.run'); const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { diff --git a/apps/meteor/app/version-check/server/methods/banner_dismiss.ts b/apps/meteor/app/version-check/server/methods/banner_dismiss.ts index a3bf337626f92..5b89641262d05 100644 --- a/apps/meteor/app/version-check/server/methods/banner_dismiss.ts +++ b/apps/meteor/app/version-check/server/methods/banner_dismiss.ts @@ -2,6 +2,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ddp-client' { @@ -13,6 +14,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async 'banner/dismiss'({ id }) { + methodDeprecationLogger.method('banner/dismiss', '9.0.0', '/v1/banners.dismiss'); const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'banner/dismiss' }); diff --git a/apps/meteor/client/apps/gameCenter/GameCenter.tsx b/apps/meteor/client/apps/gameCenter/GameCenter.tsx index 058cc7dfdcd49..b424ad7515c6c 100644 --- a/apps/meteor/client/apps/gameCenter/GameCenter.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenter.tsx @@ -1,8 +1,8 @@ import type { IExternalComponent } from '@rocket.chat/apps-engine/definition/externalComponent'; -import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useStableCallback } from '@rocket.chat/fuselage-hooks'; import { useRoomToolbox } from '@rocket.chat/ui-contexts'; import { useState } from 'react'; -import type { MouseEvent, ReactElement } from 'react'; +import type { MouseEvent } from 'react'; import GameCenterContainer from './GameCenterContainer'; import GameCenterList from './GameCenterList'; @@ -11,16 +11,16 @@ import { preventSyntheticEvent } from '../../lib/utils/preventSyntheticEvent'; export type IGame = IExternalComponent; -const GameCenter = (): ReactElement => { +const GameCenter = () => { const [openedGame, setOpenedGame] = useState(); const { closeTab } = useRoomToolbox(); const result = useExternalComponentsQuery(); - const handleClose = useEffectEvent(() => closeTab()); + const handleClose = useStableCallback(() => closeTab()); - const handleBack = useEffectEvent((e: MouseEvent) => { + const handleBack = useStableCallback((e: MouseEvent) => { setOpenedGame(undefined); preventSyntheticEvent(e); }); diff --git a/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx b/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx index a275e98107a92..c6b9b95ffe120 100644 --- a/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx @@ -7,7 +7,6 @@ import { ContextualbarClose, ContextualbarDialog, } from '@rocket.chat/ui-client'; -import type { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; import type { IGame } from './GameCenter'; @@ -18,7 +17,7 @@ interface IGameCenterContainerProps { game: IGame; } -const GameCenterContainer = ({ handleClose, handleBack, game }: IGameCenterContainerProps): ReactElement => { +const GameCenterContainer = ({ handleClose, handleBack, game }: IGameCenterContainerProps) => { const { t } = useTranslation(); return ( diff --git a/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx b/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx index e3a6f13271d01..bad66d4d3f2df 100644 --- a/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx @@ -2,11 +2,11 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; import { Random } from '@rocket.chat/random'; import { GenericModal } from '@rocket.chat/ui-client'; -import type { ReactElement } from 'react'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import type { IGame } from './GameCenter'; +import { sdk } from '../../../app/utils/client/lib/SDKClient'; import UserAutoCompleteMultiple from '../../components/UserAutoCompleteMultiple'; import { useOpenedRoom } from '../../lib/RoomManager'; import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; @@ -19,7 +19,7 @@ interface IGameCenterInvitePlayersModalProps { onClose: () => void; } -const GameCenterInvitePlayersModal = ({ game, onClose }: IGameCenterInvitePlayersModalProps): ReactElement => { +const GameCenterInvitePlayersModal = ({ game, onClose }: IGameCenterInvitePlayersModalProps) => { const { t } = useTranslation(); const [users, setUsers] = useState>([]); const { name } = game; @@ -30,14 +30,14 @@ const GameCenterInvitePlayersModal = ({ game, onClose }: IGameCenterInvitePlayer const privateGroupName = `${name.replace(/\s/g, '-')}-${Random.id(10)}`; try { - const result = await callWithErrorHandling('createPrivateGroup' as any, privateGroupName, users); + const { group } = await sdk.rest.post('/v1/groups.create', { name: privateGroupName, members: users }); - roomCoordinator.openRouteLink(result.t, result); + roomCoordinator.openRouteLink(group.t, { rid: group._id, name: group.name }); - if (openedRoom === result.rid) { - callWithErrorHandling('sendMessage', { + if (openedRoom === group._id) { + await callWithErrorHandling('sendMessage', { _id: Random.id(), - rid: result.rid, + rid: group._id, msg: t('Apps_Game_Center_Play_Game_Together', { name }), }); } diff --git a/apps/meteor/client/apps/gameCenter/GameCenterList.tsx b/apps/meteor/client/apps/gameCenter/GameCenterList.tsx index 539a4f20b4a92..4a9f179a317b8 100644 --- a/apps/meteor/client/apps/gameCenter/GameCenterList.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterList.tsx @@ -8,7 +8,6 @@ import { ContextualbarSkeleton, } from '@rocket.chat/ui-client'; import { useSetModal } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -22,7 +21,7 @@ interface IGameCenterListProps { isLoading: boolean; } -const GameCenterList = ({ handleClose, handleOpenGame, games, isLoading }: IGameCenterListProps): ReactElement => { +const GameCenterList = ({ handleClose, handleOpenGame, games, isLoading }: IGameCenterListProps) => { const { t } = useTranslation(); const setModal = useSetModal(); diff --git a/apps/meteor/client/components/ABAC/ABACUpsellModal/ABACUpsellModal.stories.tsx b/apps/meteor/client/components/ABAC/ABACUpsellModal/ABACUpsellModal.stories.tsx index f82c261020522..40bdad068de7d 100644 --- a/apps/meteor/client/components/ABAC/ABACUpsellModal/ABACUpsellModal.stories.tsx +++ b/apps/meteor/client/components/ABAC/ABACUpsellModal/ABACUpsellModal.stories.tsx @@ -1,6 +1,6 @@ import { mockAppRoot } from '@rocket.chat/mock-providers'; -import { action } from '@storybook/addon-actions'; import type { Meta } from '@storybook/react'; +import { action } from 'storybook/actions'; import ABACUpsellModal from './ABACUpsellModal'; diff --git a/apps/meteor/client/components/ConfirmOwnerChangeModal.tsx b/apps/meteor/client/components/ConfirmOwnerChangeModal.tsx index 8ff91eb9da725..470d041d16fff 100644 --- a/apps/meteor/client/components/ConfirmOwnerChangeModal.tsx +++ b/apps/meteor/client/components/ConfirmOwnerChangeModal.tsx @@ -1,5 +1,5 @@ import { Box } from '@rocket.chat/fuselage'; -import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useStableCallback } from '@rocket.chat/fuselage-hooks'; import { GenericModal } from '@rocket.chat/ui-client'; import type { ComponentPropsWithoutRef } from 'react'; import { Trans } from 'react-i18next'; @@ -18,7 +18,7 @@ const ConfirmOwnerChangeModal = ({ onConfirm, onCancel, }: ConfirmOwnerChangeModalProps) => { - const getChangeOwnerRooms = useEffectEvent(() => { + const getChangeOwnerRooms = useStableCallback(() => { if (shouldChangeOwner.length === 0) { return ''; } @@ -50,7 +50,7 @@ const ConfirmOwnerChangeModal = ({ ); }); - const getRemovedRooms = useEffectEvent(() => { + const getRemovedRooms = useStableCallback(() => { if (shouldBeRemoved.length === 0) { return ''; } diff --git a/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.stories.tsx b/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.stories.tsx index e36828a4a48d9..2a0643f8627ac 100644 --- a/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.stories.tsx +++ b/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryFn } from '@storybook/react'; +import type { StoryObj, Meta } from '@storybook/react'; import CreateDiscussion from './CreateDiscussion'; @@ -13,4 +13,4 @@ export default { }, } satisfies Meta; -export const Default: StoryFn = (args) => ; +export const Default: StoryObj = {}; diff --git a/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx b/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx index 6a41769ef5566..0c24e53cdffe2 100644 --- a/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx +++ b/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx @@ -11,7 +11,7 @@ import { FieldRow, FieldError, } from '@rocket.chat/fuselage-forms'; -import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useStableCallback } from '@rocket.chat/fuselage-hooks'; import { GenericModal } from '@rocket.chat/ui-client'; import { useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; @@ -69,7 +69,7 @@ const CreateDiscussion = ({ }, }); - const onParentRoomChange = useEffectEvent((room: IRoom | undefined) => { + const onParentRoomChange = useStableCallback((room: IRoom | undefined) => { if (!room) { return; } diff --git a/apps/meteor/client/components/CreateDiscussion/__snapshots__/CreateDiscussion.spec.tsx.snap b/apps/meteor/client/components/CreateDiscussion/__snapshots__/CreateDiscussion.spec.tsx.snap index f79ddc71ebcc7..915ce0fc0ed33 100644 --- a/apps/meteor/client/components/CreateDiscussion/__snapshots__/CreateDiscussion.spec.tsx.snap +++ b/apps/meteor/client/components/CreateDiscussion/__snapshots__/CreateDiscussion.spec.tsx.snap @@ -215,6 +215,7 @@ exports[`CreateDiscussion renders Default without crashing 1`] = ` aria-haspopup="listbox" aria-invalid="false" aria-labelledby="react-aria-:r4:-label" + autocomplete="off" class="rcx-box rcx-box--full rcx-box--animated rcx-input-box--undecorated rcx-input-box rcx-css-trljwa rcx-css-pqpp8q" id="react-aria-:r4:" name="usernames" diff --git a/apps/meteor/client/components/Emoji.tsx b/apps/meteor/client/components/Emoji.tsx index 46e36459537c6..accd2517b20e1 100644 --- a/apps/meteor/client/components/Emoji.tsx +++ b/apps/meteor/client/components/Emoji.tsx @@ -1,5 +1,4 @@ import styled from '@rocket.chat/styled'; -import type { ReactElement } from 'react'; import { getEmojiClassNameAndDataTitle } from '../lib/utils/renderEmoji'; @@ -20,7 +19,7 @@ const EmojiComponent = styled('span', ({ fillContainer: _fillContainer, ...props : ''} `; -function Emoji({ emojiHandle, className = undefined, fillContainer }: EmojiProps): ReactElement { +function Emoji({ emojiHandle, className = undefined, fillContainer }: EmojiProps) { const { className: emojiClassName, image, ...props } = getEmojiClassNameAndDataTitle(emojiHandle); return ( diff --git a/apps/meteor/client/components/FingerprintChangeModal.tsx b/apps/meteor/client/components/FingerprintChangeModal.tsx index 066d77334cf44..170451eba0233 100644 --- a/apps/meteor/client/components/FingerprintChangeModal.tsx +++ b/apps/meteor/client/components/FingerprintChangeModal.tsx @@ -1,6 +1,5 @@ import { Box } from '@rocket.chat/fuselage'; import { ExternalLink, GenericModal } from '@rocket.chat/ui-client'; -import type { ReactElement } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { links } from '../lib/links'; @@ -11,7 +10,7 @@ type FingerprintChangeModalProps = { onClose: () => void; }; -const FingerprintChangeModal = ({ onConfirm, onCancel, onClose }: FingerprintChangeModalProps): ReactElement => { +const FingerprintChangeModal = ({ onConfirm, onCancel, onClose }: FingerprintChangeModalProps) => { const { t } = useTranslation(); return ( { +const FingerprintChangeModalConfirmation = ({ onConfirm, onCancel, onClose, newWorkspace }: FingerprintChangeModalConfirmationProps) => { const { t } = useTranslation(); return ( , 'variant' | 'children' | 'onClose' | 'onDismiss'> & { - subtitle?: string | ReactElement; - description?: string | ReactElement; + subtitle?: ReactNode; + description?: ReactNode; img: ComponentProps['src']; - imgWidth?: ComponentProps['width']; imgHeight?: ComponentProps['height']; imgAlt?: string; diff --git a/apps/meteor/client/components/ListItem.stories.tsx b/apps/meteor/client/components/ListItem.stories.tsx index bbeb1d830f3e0..bd9e412995a9d 100644 --- a/apps/meteor/client/components/ListItem.stories.tsx +++ b/apps/meteor/client/components/ListItem.stories.tsx @@ -1,5 +1,5 @@ import { Tile, OptionTitle, Box } from '@rocket.chat/fuselage'; -import type { Meta, StoryFn } from '@storybook/react'; +import type { StoryObj, Meta, StoryFn } from '@storybook/react'; import ListItem from './Sidebar/ListItem'; @@ -27,6 +27,7 @@ export const ListWithIcon: StoryFn = () => { ); }; + export const NoIcon: StoryFn = () => { return ( @@ -41,44 +42,51 @@ export const NoIcon: StoryFn = () => { ); }; -export const MixedWithGap: StoryFn = () => { - return ( - - - Title - - - - - - - ); -}; -MixedWithGap.parameters = { - docs: { - description: { - story: - " When using `ListItem`, you can also use the `gap` prop to add spacing to the left. If the list is mixed with items **with and without** icons, it's recommended to add the gap.", +export const MixedWithGap: StoryObj = { + render: () => { + return ( + + + Title + + + + + + + ); + }, + + parameters: { + docs: { + description: { + story: + " When using `ListItem`, you can also use the `gap` prop to add spacing to the left. If the list is mixed with items **with and without** icons, it's recommended to add the gap.", + }, }, }, }; -export const MixedWithoutGap: StoryFn = () => { - return ( - - - Title - - - - - - - ); -}; -MixedWithoutGap.parameters = { - docs: { - description: { - story: 'Not recommended. Prefer adding the `gap` prop to the items without icons.', + +export const MixedWithoutGap: StoryObj = { + render: () => { + return ( + + + Title + + + + + + + ); + }, + + parameters: { + docs: { + description: { + story: 'Not recommended. Prefer adding the `gap` prop to the items without icons.', + }, }, }, }; diff --git a/apps/meteor/client/components/ListSkeleton.tsx b/apps/meteor/client/components/ListSkeleton.tsx index 034f167bca0e8..f507ccdd42b0b 100644 --- a/apps/meteor/client/components/ListSkeleton.tsx +++ b/apps/meteor/client/components/ListSkeleton.tsx @@ -1,5 +1,4 @@ import { Box, Skeleton } from '@rocket.chat/fuselage'; -import type { ReactElement } from 'react'; import { memo, useMemo } from 'react'; const availablePercentualWidths = [47, 68, 75, 82]; @@ -8,7 +7,7 @@ type ListSkeletonProps = { listCount?: number; }; -const ListSkeleton = ({ listCount = 2 }: ListSkeletonProps): ReactElement => { +const ListSkeleton = ({ listCount = 2 }: ListSkeletonProps) => { const widths = useMemo( () => Array.from({ length: listCount }, (_, index) => `${availablePercentualWidths[index % availablePercentualWidths.length]}%`), [listCount], diff --git a/apps/meteor/client/components/LocalTime.tsx b/apps/meteor/client/components/LocalTime.tsx index be5133050e22b..ded8e707ed4cd 100644 --- a/apps/meteor/client/components/LocalTime.tsx +++ b/apps/meteor/client/components/LocalTime.tsx @@ -1,4 +1,3 @@ -import type { ReactElement } from 'react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,7 +7,7 @@ type LocalTimeProps = { utcOffset: number; }; -const LocalTime = ({ utcOffset }: LocalTimeProps): ReactElement => { +const LocalTime = ({ utcOffset }: LocalTimeProps) => { const time = useUTCClock(utcOffset); const { t } = useTranslation(); diff --git a/apps/meteor/client/components/NotFoundState.tsx b/apps/meteor/client/components/NotFoundState.tsx index 47911f496b7ec..6fd3fe363807b 100644 --- a/apps/meteor/client/components/NotFoundState.tsx +++ b/apps/meteor/client/components/NotFoundState.tsx @@ -1,6 +1,5 @@ import { Box, States, StatesAction, StatesActions, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage'; import { useRouter } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; type NotFoundProps = { @@ -8,7 +7,7 @@ type NotFoundProps = { subtitle: string; }; -const NotFoundState = ({ title, subtitle }: NotFoundProps): ReactElement => { +const NotFoundState = ({ title, subtitle }: NotFoundProps) => { const { t } = useTranslation(); const router = useRouter(); diff --git a/apps/meteor/client/components/PageSkeleton.tsx b/apps/meteor/client/components/PageSkeleton.tsx index ca6b78d3c0257..19f7b682fad55 100644 --- a/apps/meteor/client/components/PageSkeleton.tsx +++ b/apps/meteor/client/components/PageSkeleton.tsx @@ -1,8 +1,7 @@ import { Box, Button, ButtonGroup, Skeleton } from '@rocket.chat/fuselage'; import { Page, PageHeader, PageContent } from '@rocket.chat/ui-client'; -import type { ReactElement } from 'react'; -const PageSkeleton = (): ReactElement => ( +const PageSkeleton = () => ( }> diff --git a/apps/meteor/client/components/PaginatedVirtualList/PaginatedVirtualList.spec.tsx b/apps/meteor/client/components/PaginatedVirtualList/PaginatedVirtualList.spec.tsx new file mode 100644 index 0000000000000..64df4122619f8 --- /dev/null +++ b/apps/meteor/client/components/PaginatedVirtualList/PaginatedVirtualList.spec.tsx @@ -0,0 +1,216 @@ +import type { UseInfiniteQueryResult } from '@tanstack/react-query'; +import { act, fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; +import type { CSSProperties, HTMLAttributes, ReactNode } from 'react'; +import * as React from 'react'; +import { Children, forwardRef, isValidElement } from 'react'; + +import PaginatedVirtualList from './PaginatedVirtualList'; + +const mockVirtualizerHandle = { + scrollToIndex: jest.fn(), + scrollTo: jest.fn(), + findItemIndex: jest.fn((offset: number) => offset), + scrollOffset: 0, + scrollSize: 1000, + viewportSize: 300, +}; + +type MockVListProps = { + children: ReactNode; + bufferSize?: number; + onScroll?: (offset: number) => void; + as?: React.ElementType; + item?: React.ElementType; + style?: CSSProperties; + className?: string; +}; + +jest.mock('virtua', () => { + return { + Virtualizer: React.forwardRef( + ( + { children, bufferSize, onScroll, as: asRoot = 'div', item: asItem = 'div', style, className }: MockVListProps, + ref: React.Ref, + ) => { + React.useImperativeHandle(ref, () => mockVirtualizerHandle); + const Root = asRoot; + const Item = asItem; + const wrapped = Children.map(children, (child, index) => { + const key = isValidElement(child) && child.key != null ? String(child.key) : `row-${index}`; + return {child}; + }); + + return ( + onScroll?.(mockVirtualizerHandle.scrollOffset)} + > + {wrapped} + + ); + }, + ), + }; +}); + +jest.mock('@rocket.chat/ui-client', () => ({ + ...jest.requireActual('@rocket.chat/ui-client'), + CustomVirtuaScrollbars: forwardRef>(function CustomVirtuaScrollbars( + { children, ...props }, + ref, + ) { + // eslint-disable-next-line testing-library/no-node-access + const content = isValidElement<{ children?: ReactNode }>(children) && children.type === 'div' ? children.props.children : children; + + return ( +
+ {content} +
+ ); + }), +})); + +const items = Array.from({ length: 10 }, (_, index) => ({ _id: `${index}` })); + +type VirtualListTestItem = (typeof items)[number]; + +const renderVirtualList = ( + props: Partial<{ + items: VirtualListTestItem[]; + totalCount: number; + renderItem: (item: VirtualListTestItem, index: number) => ReactNode; + overscan?: number; + onEndReached?: UseInfiniteQueryResult['fetchNextPage']; + }> = {}, +) => render(
{item._id}
} {...props} />); + +const advanceDebouncedScroll = async () => { + await act(async () => { + await jest.advanceTimersByTimeAsync(300); + }); +}; + +describe('PaginatedVirtualList', () => { + beforeEach(() => { + mockVirtualizerHandle.scrollOffset = 0; + mockVirtualizerHandle.scrollSize = 1000; + mockVirtualizerHandle.viewportSize = 300; + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('has no accessibility violations', async () => { + const { container } = renderVirtualList(); + expect(await axe(container)).toHaveNoViolations(); + }); + + it('calls onEndReached when scrolled near the bottom', async () => { + jest.useFakeTimers(); + const onEndReached = jest.fn().mockResolvedValue(undefined); + + renderVirtualList({ onEndReached }); + expect(onEndReached).not.toHaveBeenCalled(); + + mockVirtualizerHandle.scrollOffset = 700; + fireEvent.scroll(screen.getByTestId('virtual-list')); + await advanceDebouncedScroll(); + + expect(onEndReached).toHaveBeenCalledTimes(1); + }); + + it('does not call onEndReached when all items are loaded', () => { + const onEndReached = jest.fn().mockResolvedValue(undefined); + + renderVirtualList({ onEndReached, totalCount: items.length }); + mockVirtualizerHandle.scrollOffset = 700; + fireEvent.scroll(screen.getByTestId('virtual-list')); + + expect(onEndReached).not.toHaveBeenCalled(); + }); + + it('does not call onEndReached repeatedly for the same item count', async () => { + jest.useFakeTimers(); + const onEndReached = jest.fn().mockResolvedValue(undefined); + + renderVirtualList({ onEndReached }); + mockVirtualizerHandle.scrollOffset = 700; + fireEvent.scroll(screen.getByTestId('virtual-list')); + fireEvent.scroll(screen.getByTestId('virtual-list')); + await advanceDebouncedScroll(); + + expect(onEndReached).toHaveBeenCalledTimes(1); + }); + + it('calls onEndReached after a same-size dataset reset', async () => { + jest.useFakeTimers(); + const onEndReached = jest.fn().mockResolvedValue(undefined); + const { rerender } = renderVirtualList({ onEndReached }); + mockVirtualizerHandle.scrollOffset = 700; + fireEvent.scroll(screen.getByTestId('virtual-list')); + await advanceDebouncedScroll(); + + const resetItems = Array.from({ length: 10 }, (_, index) => ({ _id: `reset-${index}` })); + rerender( +
{item._id}
} onEndReached={onEndReached} />, + ); + fireEvent.scroll(screen.getByTestId('virtual-list')); + await advanceDebouncedScroll(); + + expect(onEndReached).toHaveBeenCalledTimes(2); + }); + + it('passes overscan through to virtua buffer size', () => { + renderVirtualList({ overscan: 25 }); + + expect(screen.getByTestId('virtual-list')).toHaveAttribute('data-buffer-size', '25'); + }); + + it('allows onEndReached to retry after a failed load', async () => { + jest.useFakeTimers(); + const onEndReached = jest.fn().mockRejectedValue(new Error('failed to load more items')); + + renderVirtualList({ onEndReached }); + mockVirtualizerHandle.scrollOffset = 700; + fireEvent.scroll(screen.getByTestId('virtual-list')); + await advanceDebouncedScroll(); + + fireEvent.scroll(screen.getByTestId('virtual-list')); + await advanceDebouncedScroll(); + + expect(onEndReached).toHaveBeenCalledTimes(2); + }); + + it('allows onEndReached to retry after a synchronous throw', async () => { + jest.useFakeTimers(); + const onEndReached = jest + .fn() + .mockImplementationOnce(() => { + throw new Error('failed to load more items'); + }) + .mockImplementation(() => undefined); + + renderVirtualList({ onEndReached }); + mockVirtualizerHandle.scrollOffset = 700; + fireEvent.scroll(screen.getByTestId('virtual-list')); + await advanceDebouncedScroll(); + fireEvent.scroll(screen.getByTestId('virtual-list')); + await advanceDebouncedScroll(); + + expect(onEndReached).toHaveBeenCalledTimes(2); + }); + + it('calls onEndReached when the viewport is underfilled', () => { + const onEndReached = jest.fn().mockResolvedValue(undefined); + mockVirtualizerHandle.scrollSize = 200; + + renderVirtualList({ onEndReached }); + + expect(onEndReached).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/meteor/client/components/PaginatedVirtualList/PaginatedVirtualList.tsx b/apps/meteor/client/components/PaginatedVirtualList/PaginatedVirtualList.tsx new file mode 100644 index 0000000000000..e6ab96a1f6fd8 --- /dev/null +++ b/apps/meteor/client/components/PaginatedVirtualList/PaginatedVirtualList.tsx @@ -0,0 +1,103 @@ +import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks'; +import { CustomVirtuaScrollbars } from '@rocket.chat/ui-client'; +import type { UseInfiniteQueryResult } from '@tanstack/react-query'; +import type { ReactNode } from 'react'; +import { useCallback, useLayoutEffect, useRef } from 'react'; +import type { VirtualizerHandle } from 'virtua'; +import { Virtualizer } from 'virtua'; + +import { VirtuaListContainer } from './VirtuaListContainer'; + +const NEAR_BOTTOM_THRESHOLD = -20; + +const scrollViewportStyle = { + height: '100%', + width: '100%', + overflow: 'auto', +} as const; + +type PaginatedVirtualListProps = { + items: T[]; + totalCount: number; + renderItem: (item: T, index: number) => ReactNode; + overscan?: number; + onEndReached?: UseInfiniteQueryResult['fetchNextPage']; +}; + +function PaginatedVirtualList({ + items, + totalCount, + renderItem, + overscan, + onEndReached, +}: PaginatedVirtualListProps) { + const virtualizerRef = useRef(null); + const isEndReachedLockedRef = useRef(false); + const firstItemId = items[0]?._id ?? ''; + const lastItemId = items[items.length - 1]?._id ?? ''; + + const checkEndReached = useCallback( + async (offset: number) => { + if (isEndReachedLockedRef.current) { + return; + } + + const handle = virtualizerRef.current; + + if (!handle || !onEndReached) { + return; + } + + const { scrollSize, viewportSize } = handle; + if (viewportSize <= 0 || items.length >= totalCount) { + return; + } + + const nearBottom = offset - scrollSize + viewportSize >= NEAR_BOTTOM_THRESHOLD; + if (!nearBottom) { + return; + } + + isEndReachedLockedRef.current = true; + + try { + await onEndReached(); + } catch { + isEndReachedLockedRef.current = false; + } + }, + [items.length, onEndReached, totalCount], + ); + + const handleScroll = useDebouncedCallback( + (offset: number) => { + checkEndReached(offset); + }, + 300, + [checkEndReached], + ); + + useLayoutEffect(() => { + isEndReachedLockedRef.current = false; + + const handle = virtualizerRef.current; + if (!handle) { + return; + } + checkEndReached(handle.scrollOffset); + }, [checkEndReached, firstItemId, items.length, lastItemId, totalCount]); + + return ( + +
+ + {items.map((item, index) => ( +
{renderItem(item, index)}
+ ))} +
+
+
+ ); +} + +export default PaginatedVirtualList; diff --git a/apps/meteor/client/components/PaginatedVirtualList/VirtuaListContainer.tsx b/apps/meteor/client/components/PaginatedVirtualList/VirtuaListContainer.tsx new file mode 100644 index 0000000000000..afcffac6ad716 --- /dev/null +++ b/apps/meteor/client/components/PaginatedVirtualList/VirtuaListContainer.tsx @@ -0,0 +1,24 @@ +import type { CSSProperties, HTMLAttributes, ReactNode } from 'react'; +import { forwardRef } from 'react'; + +const listResetStyle = { + margin: 0, + padding: 0, + listStyle: 'none', +} as const; + +export type VirtuaListContainerProps = { + children: ReactNode; + style: CSSProperties; +} & Omit, 'children' | 'style'>; + +export const VirtuaListContainer = forwardRef(function VirtuaListContainer( + { children, style, ...props }, + ref, +) { + return ( +
    + {children} +
+ ); +}); diff --git a/apps/meteor/client/components/PaginatedVirtualList/index.ts b/apps/meteor/client/components/PaginatedVirtualList/index.ts new file mode 100644 index 0000000000000..ac0eee09623e8 --- /dev/null +++ b/apps/meteor/client/components/PaginatedVirtualList/index.ts @@ -0,0 +1 @@ +export { default as PaginatedVirtualList } from './PaginatedVirtualList'; diff --git a/apps/meteor/client/components/RawText.tsx b/apps/meteor/client/components/RawText.tsx index 58ae89e4d041c..975c2100761df 100644 --- a/apps/meteor/client/components/RawText.tsx +++ b/apps/meteor/client/components/RawText.tsx @@ -1,9 +1,6 @@ import DOMPurify from 'dompurify'; -import type { ReactElement } from 'react'; /** @deprecated */ -const RawText = ({ children }: { children: string }): ReactElement => ( - -); +const RawText = ({ children }: { children: string }) => ; export default RawText; diff --git a/apps/meteor/client/components/ResultsLiveRegion.tsx b/apps/meteor/client/components/ResultsLiveRegion.tsx index 3df599c66a509..7fc92094fb503 100644 --- a/apps/meteor/client/components/ResultsLiveRegion.tsx +++ b/apps/meteor/client/components/ResultsLiveRegion.tsx @@ -1,14 +1,28 @@ import { VisuallyHidden } from '@react-aria/visually-hidden'; import { useTranslation } from 'react-i18next'; -const ResultsLiveRegion = ({ shouldAnnounce, itemCount }: { shouldAnnounce: boolean; itemCount: number }) => { +const ResultsLiveRegion = ({ + shouldAnnounce, + itemCount, + isLoading = false, +}: { + shouldAnnounce: boolean; + itemCount: number; + isLoading?: boolean; +}) => { const { t } = useTranslation(); - if (itemCount === 0) { - return {shouldAnnounce && t('No_results_found')}; - } + const message = (() => { + if (isLoading) { + return t('Loading'); + } + if (!shouldAnnounce) { + return ''; + } + return itemCount === 0 ? t('No_results_found') : t('__count__result_found', { count: itemCount }); + })(); - return {shouldAnnounce && t('__count__result_found', { count: itemCount })}; + return {message}; }; export default ResultsLiveRegion; diff --git a/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx b/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx index 5ebc5393b0847..e4e9472464670 100644 --- a/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx +++ b/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx @@ -6,7 +6,7 @@ import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { RoomAvatar } from '@rocket.chat/ui-avatar'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { keepPreviousData, useQuery } from '@tanstack/react-query'; -import type { ReactElement } from 'react'; +import type { ReactNode } from 'react'; import { forwardRef, memo, useMemo, useState } from 'react'; type LabelType = { name: string; avatarETag?: string; type: IRoom['t']; encrypted?: IRoom['encrypted'] }; @@ -19,7 +19,7 @@ const generateQuery = ( type RoomAutoCompleteProps = Omit, 'filter'> & { scope?: 'admin' | 'regular'; - renderRoomIcon?: (props: { encrypted: IRoom['encrypted']; type: IRoom['t'] }) => ReactElement | null; + renderRoomIcon?: (props: { encrypted: IRoom['encrypted']; type: IRoom['t'] }) => ReactNode; setSelectedRoom?: (room: IRoom | undefined) => void; }; diff --git a/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx b/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx index 401a8879e6093..99d558b72f9d5 100644 --- a/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx +++ b/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx @@ -3,7 +3,7 @@ import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { RoomAvatar } from '@rocket.chat/ui-avatar'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { keepPreviousData, useQuery } from '@tanstack/react-query'; -import type { ReactElement, ComponentProps } from 'react'; +import type { ComponentProps } from 'react'; import { memo, useMemo, useState } from 'react'; const generateQuery = ( @@ -16,7 +16,7 @@ type RoomAutoCompleteProps = Omit, 'filter'> readOnly?: boolean; }; -const RoomAutoCompleteMultiple = ({ value, onChange, ...props }: RoomAutoCompleteProps): ReactElement => { +const RoomAutoCompleteMultiple = ({ value, onChange, ...props }: RoomAutoCompleteProps) => { const [filter, setFilter] = useState(''); const filterDebounced = useDebouncedValue(filter, 300); const autocomplete = useEndpoint('GET', '/v1/rooms.autocomplete.channelAndPrivate'); @@ -50,7 +50,7 @@ const RoomAutoCompleteMultiple = ({ value, onChange, ...props }: RoomAutoComplet filter={filter} setFilter={setFilter} multiple - renderSelected={({ selected: { value, label }, onRemove, ...props }): ReactElement => ( + renderSelected={({ selected: { value, label }, onRemove, ...props }) => ( @@ -58,7 +58,7 @@ const RoomAutoCompleteMultiple = ({ value, onChange, ...props }: RoomAutoComplet )} - renderItem={({ value, label, ...props }): ReactElement => ( + renderItem={({ value, label, ...props }) => (