diff --git a/.changeset/afraid-boxes-destroy.md b/.changeset/afraid-boxes-destroy.md new file mode 100644 index 0000000000000..c18d0ac6e4658 --- /dev/null +++ b/.changeset/afraid-boxes-destroy.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-typings": patch +--- + +Fixes an issue in `Admin > Settings` page where sometimes settings guarded by a license module would not be editable despite having the required modules. diff --git a/.changeset/angry-poems-build.md b/.changeset/angry-poems-build.md new file mode 100644 index 0000000000000..46b9abc38d8de --- /dev/null +++ b/.changeset/angry-poems-build.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes incorrect message sender for incoming webhooks when "Post As" field is updated by ensuring both username and userId are synced to reflect the selected user. diff --git a/.changeset/beige-days-push.md b/.changeset/beige-days-push.md new file mode 100644 index 0000000000000..bcf10b1759a1c --- /dev/null +++ b/.changeset/beige-days-push.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/core-typings': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Adds new settings to allow configuring custom variables with string manipulation functions on the LDAP data mapper diff --git a/.changeset/big-tips-greet.md b/.changeset/big-tips-greet.md new file mode 100644 index 0000000000000..701e013dec0da --- /dev/null +++ b/.changeset/big-tips-greet.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/apps-engine": minor +"@rocket.chat/apps": minor +--- + +Allows apps to react to department status changes. diff --git a/.changeset/bright-forks-drop.md b/.changeset/bright-forks-drop.md new file mode 100644 index 0000000000000..2cad496c2e68a --- /dev/null +++ b/.changeset/bright-forks-drop.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes user email verification update not working on admin > users diff --git a/.changeset/bump-patch-1745354933374.md b/.changeset/bump-patch-1745354933374.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1745354933374.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1745631711629.md b/.changeset/bump-patch-1745631711629.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1745631711629.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1746034829149.md b/.changeset/bump-patch-1746034829149.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1746034829149.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1746046159552.md b/.changeset/bump-patch-1746046159552.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1746046159552.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1746475877217.md b/.changeset/bump-patch-1746475877217.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1746475877217.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1746477216002.md b/.changeset/bump-patch-1746477216002.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1746477216002.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1746564900627.md b/.changeset/bump-patch-1746564900627.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1746564900627.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1746748187032.md b/.changeset/bump-patch-1746748187032.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1746748187032.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/dirty-seas-explode.md b/.changeset/dirty-seas-explode.md new file mode 100644 index 0000000000000..3b7a205ff7a94 --- /dev/null +++ b/.changeset/dirty-seas-explode.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue with apps loading in microservices in some cases by ensuring all services have started before trying to load apps diff --git a/.changeset/eighty-wombats-smile.md b/.changeset/eighty-wombats-smile.md new file mode 100644 index 0000000000000..23ceecd25050b --- /dev/null +++ b/.changeset/eighty-wombats-smile.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/apps-engine': minor +'@rocket.chat/meteor': minor +--- + +Adds the ability to dynamically add and remove options from select/multi-select settings in the Apps Engine to support more flexible configuration scenarios by exposing two new methods on the settings API. diff --git a/.changeset/eleven-laws-crash.md b/.changeset/eleven-laws-crash.md new file mode 100644 index 0000000000000..d13835ed795f6 --- /dev/null +++ b/.changeset/eleven-laws-crash.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where the outlook calendar items list is not rendering properly diff --git a/.changeset/famous-falcons-laugh.md b/.changeset/famous-falcons-laugh.md new file mode 100644 index 0000000000000..e7008a8d55da9 --- /dev/null +++ b/.changeset/famous-falcons-laugh.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Ensures seat limit validation in LDAP sync, preventing activations beyond license restrictions. \ No newline at end of file diff --git a/.changeset/few-mangos-protect.md b/.changeset/few-mangos-protect.md new file mode 100644 index 0000000000000..e92d17f6fab4f --- /dev/null +++ b/.changeset/few-mangos-protect.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/models": minor +--- + +Allows search omnichannel rooms by the exact visitor name using double quotes to have a faster response diff --git a/.changeset/fifty-moles-boil.md b/.changeset/fifty-moles-boil.md new file mode 100644 index 0000000000000..d4e1edbf9923c --- /dev/null +++ b/.changeset/fifty-moles-boil.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +"@rocket.chat/models": patch +--- + +Fixes an issue where the app's logs index was not being created by default sometimes, also set to be always 30 days diff --git a/.changeset/fluffy-weeks-float.md b/.changeset/fluffy-weeks-float.md new file mode 100644 index 0000000000000..cca4e2c037d11 --- /dev/null +++ b/.changeset/fluffy-weeks-float.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an error causing the server to throw an "unhandled promise rejection" when removing an agent from a department without a business hour when using `Multiple` business hours diff --git a/.changeset/four-clocks-collect.md b/.changeset/four-clocks-collect.md new file mode 100644 index 0000000000000..4b6b1e08e8b66 --- /dev/null +++ b/.changeset/four-clocks-collect.md @@ -0,0 +1,13 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +"@rocket.chat/models": patch +--- + +Fixes the behavior of "Maximum number of simultaneous chats" settings, making them more predictable. Previously, we applied a single limit per operation, being the order: `Department > Agent > Global`. This caused the department limit to take prescedence over agent's specific limit, causing some unwanted side effects. + +The new way of applying the filter is as follows: +- An agent can accept chats from multiple departments, respecting each department’s limit individually. +- The total number of active chats (across all departments) must not exceed the configured Agent-Level or Global limit. +- If neither the Agent-Level nor Global Limit is set, only department-specific limits apply. +- If no limits are set at any level, there is no restriction on the number of chats an agent can handle. diff --git a/.changeset/four-dragons-warn.md b/.changeset/four-dragons-warn.md new file mode 100644 index 0000000000000..c8b3da4d93300 --- /dev/null +++ b/.changeset/four-dragons-warn.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/ui-client': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Replaces the parent room tag in room header in favor of a button to back to the parent room +> This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it diff --git a/.changeset/fuzzy-eyes-clean.md b/.changeset/fuzzy-eyes-clean.md new file mode 100644 index 0000000000000..e2d9c0cc2b7d6 --- /dev/null +++ b/.changeset/fuzzy-eyes-clean.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes the save button loading state in app settings, ensuring it resets properly after saving. diff --git a/.changeset/gold-laws-wink.md b/.changeset/gold-laws-wink.md new file mode 100644 index 0000000000000..4af030b274cf0 --- /dev/null +++ b/.changeset/gold-laws-wink.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes a fetch infinite loop when adding agents to a department diff --git a/.changeset/good-bobcats-argue.md b/.changeset/good-bobcats-argue.md new file mode 100644 index 0000000000000..e4603f8e99b0c --- /dev/null +++ b/.changeset/good-bobcats-argue.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes a GUI crash when editing a canned response with tags via room contextual bar. diff --git a/.changeset/gorgeous-turtles-flow.md b/.changeset/gorgeous-turtles-flow.md new file mode 100644 index 0000000000000..747603a02381a --- /dev/null +++ b/.changeset/gorgeous-turtles-flow.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +--- + +Fixes an issue with the leave room confirmation modal not displaying the room's name. diff --git a/.changeset/healthy-insects-cheer.md b/.changeset/healthy-insects-cheer.md new file mode 100644 index 0000000000000..3c8446154963c --- /dev/null +++ b/.changeset/healthy-insects-cheer.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where the incoming webhooks integration allowed messages to be sent to public channels under private teams by users who were not members of the team. diff --git a/.changeset/heavy-boats-mix.md b/.changeset/heavy-boats-mix.md new file mode 100644 index 0000000000000..1477fce4c3564 --- /dev/null +++ b/.changeset/heavy-boats-mix.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/models': patch +'@rocket.chat/meteor': patch +--- + +Fixes an issue when sending a message on an omnichannel room would cause the web client to fetch the room data again. diff --git a/.changeset/honest-toys-guess.md b/.changeset/honest-toys-guess.md new file mode 100644 index 0000000000000..67fae4cb61c43 --- /dev/null +++ b/.changeset/honest-toys-guess.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where the code input type in settings renders duplicate code text boxes. diff --git a/.changeset/hot-beers-glow.md b/.changeset/hot-beers-glow.md new file mode 100644 index 0000000000000..0cc220e20a7af --- /dev/null +++ b/.changeset/hot-beers-glow.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/ui-voip': patch +'@rocket.chat/meteor': patch +--- + +Fixes an issue causing VoIP calls to no longer reach the client after a temporary disconnection diff --git a/.changeset/hungry-wasps-remember.md b/.changeset/hungry-wasps-remember.md new file mode 100644 index 0000000000000..7cb6cdd3f77a6 --- /dev/null +++ b/.changeset/hungry-wasps-remember.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/ui-client': minor +'@rocket.chat/meteor': minor +--- + +Removes the avatar in the room header +> This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it diff --git a/.changeset/lovely-waves-sniff.md b/.changeset/lovely-waves-sniff.md new file mode 100644 index 0000000000000..33b01c482b78b --- /dev/null +++ b/.changeset/lovely-waves-sniff.md @@ -0,0 +1,9 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +"@rocket.chat/mock-providers": minor +"@rocket.chat/ui-client": minor +"@rocket.chat/ui-contexts": minor +--- + +Adds a new admin page to audit settings changes in a server diff --git a/.changeset/nine-paws-sit.md b/.changeset/nine-paws-sit.md new file mode 100644 index 0000000000000..04b0112a8b8ad --- /dev/null +++ b/.changeset/nine-paws-sit.md @@ -0,0 +1,16 @@ +--- +'@rocket.chat/network-broker': minor +'@rocket.chat/mock-providers': minor +'@rocket.chat/pdf-worker': minor +'@rocket.chat/core-services': minor +'@rocket.chat/model-typings': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/ui-contexts': minor +'@rocket.chat/ui-voip': minor +'@rocket.chat/models': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments diff --git a/.changeset/odd-waves-destroy.md b/.changeset/odd-waves-destroy.md new file mode 100644 index 0000000000000..484fe1d3b2616 --- /dev/null +++ b/.changeset/odd-waves-destroy.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes a race condition while federating that caused direct messages to not work diff --git a/.changeset/old-readers-battle.md b/.changeset/old-readers-battle.md new file mode 100644 index 0000000000000..fb00619ef9325 --- /dev/null +++ b/.changeset/old-readers-battle.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/core-typings': patch +'@rocket.chat/ui-voip': patch +'@rocket.chat/i18n': patch +'@rocket.chat/meteor': patch +--- + +Fixes an issue where Voice Calls were unable to gather Ice Servers diff --git a/.changeset/plenty-baboons-kneel.md b/.changeset/plenty-baboons-kneel.md new file mode 100644 index 0000000000000..338a3a21dfa68 --- /dev/null +++ b/.changeset/plenty-baboons-kneel.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/ui-client': minor +'@rocket.chat/meteor': minor +--- + +Places the room topic next to the room title +> This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it diff --git a/.changeset/polite-turkeys-fetch.md b/.changeset/polite-turkeys-fetch.md new file mode 100644 index 0000000000000..b442a2e682426 --- /dev/null +++ b/.changeset/polite-turkeys-fetch.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-services": patch +--- + +Fixes an issue where the bypass to call methods over microservices always returns to `{}` diff --git a/.changeset/poor-spies-hug.md b/.changeset/poor-spies-hug.md new file mode 100644 index 0000000000000..59b57b1f2dc28 --- /dev/null +++ b/.changeset/poor-spies-hug.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/pdf-worker": patch +--- + +Fixes an issue with PDF generation process that caused the server to hang when a single message consisted of too many (+30) markdown elements and was followed and preceded by more messages. diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000000..13daa4eeb9e85 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,133 @@ +{ + "mode": "pre", + "tag": "rc", + "initialVersions": { + "@rocket.chat/meteor": "7.6.0-develop", + "rocketchat-services": "2.0.10", + "@rocket.chat/uikit-playground": "0.6.10", + "@rocket.chat/account-service": "0.4.19", + "@rocket.chat/authorization-service": "0.4.19", + "@rocket.chat/ddp-streamer": "0.3.19", + "@rocket.chat/omnichannel-transcript": "0.4.19", + "@rocket.chat/presence-service": "0.4.19", + "@rocket.chat/queue-worker": "0.4.19", + "@rocket.chat/stream-hub-service": "0.4.19", + "@rocket.chat/license": "1.0.10", + "@rocket.chat/network-broker": "0.1.11", + "@rocket.chat/omnichannel-services": "0.3.16", + "@rocket.chat/pdf-worker": "0.2.16", + "@rocket.chat/presence": "0.2.19", + "@rocket.chat/ui-theming": "0.4.3", + "@rocket.chat/account-utils": "0.0.2", + "@rocket.chat/agenda": "0.1.0", + "@rocket.chat/api-client": "0.2.19", + "@rocket.chat/apps": "0.4.0", + "@rocket.chat/apps-engine": "1.50.0", + "@rocket.chat/base64": "1.0.13", + "@rocket.chat/cas-validate": "0.0.3", + "@rocket.chat/core-services": "0.8.0", + "@rocket.chat/core-typings": "7.6.0-develop", + "@rocket.chat/cron": "0.1.19", + "@rocket.chat/ddp-client": "0.3.19", + "@rocket.chat/eslint-config": "0.7.0", + "@rocket.chat/favicon": "0.0.2", + "@rocket.chat/freeswitch": "1.2.6", + "@rocket.chat/fuselage-ui-kit": "17.0.0", + "@rocket.chat/gazzodown": "17.0.0", + "@rocket.chat/i18n": "1.5.0", + "@rocket.chat/instance-status": "0.1.19", + "@rocket.chat/jest-presets": "0.0.1", + "@rocket.chat/jwt": "0.1.1", + "@rocket.chat/livechat": "1.22.6", + "@rocket.chat/log-format": "0.0.2", + "@rocket.chat/logger": "0.0.2", + "@rocket.chat/message-parser": "0.31.32", + "@rocket.chat/mock-providers": "0.1.9", + "@rocket.chat/model-typings": "1.5.0", + "@rocket.chat/models": "1.4.0", + "@rocket.chat/mongo-adapter": "0.0.2", + "@rocket.chat/poplib": "0.0.2", + "@rocket.chat/password-policies": "0.0.2", + "@rocket.chat/patch-injection": "0.0.1", + "@rocket.chat/peggy-loader": "0.31.27", + "@rocket.chat/random": "1.2.2", + "@rocket.chat/release-action": "2.2.3", + "@rocket.chat/release-changelog": "0.1.0", + "@rocket.chat/rest-typings": "7.6.0-develop", + "@rocket.chat/server-cloud-communication": "0.0.2", + "@rocket.chat/server-fetch": "0.0.3", + "@rocket.chat/sha256": "1.0.12", + "@rocket.chat/tools": "0.2.2", + "@rocket.chat/tracing": "0.0.1", + "@rocket.chat/ui-avatar": "13.0.0", + "@rocket.chat/ui-client": "17.0.0", + "@rocket.chat/ui-composer": "0.5.2", + "@rocket.chat/ui-contexts": "17.0.0", + "@rocket.chat/ui-kit": "0.37.0", + "@rocket.chat/ui-video-conf": "17.0.0", + "@rocket.chat/ui-voip": "7.0.0", + "@rocket.chat/web-ui-registration": "17.0.0" + }, + "changesets": [ + "afraid-boxes-destroy", + "angry-poems-build", + "beige-days-push", + "big-tips-greet", + "bright-forks-drop", + "bump-patch-1745354933374", + "bump-patch-1745631711629", + "bump-patch-1746034829149", + "bump-patch-1746046159552", + "bump-patch-1746475877217", + "bump-patch-1746477216002", + "bump-patch-1746564900627", + "bump-patch-1746748187032", + "dirty-seas-explode", + "eighty-wombats-smile", + "eleven-laws-crash", + "famous-falcons-laugh", + "few-mangos-protect", + "fifty-moles-boil", + "fluffy-weeks-float", + "four-clocks-collect", + "four-dragons-warn", + "fuzzy-eyes-clean", + "gold-laws-wink", + "good-bobcats-argue", + "gorgeous-turtles-flow", + "healthy-insects-cheer", + "heavy-boats-mix", + "honest-toys-guess", + "hot-beers-glow", + "hungry-wasps-remember", + "lovely-waves-sniff", + "nine-paws-sit", + "odd-waves-destroy", + "old-readers-battle", + "plenty-baboons-kneel", + "polite-turkeys-fetch", + "poor-spies-hug", + "purple-hairs-hang", + "rotten-candles-train", + "serious-grapes-smell", + "shy-dolls-protect", + "slow-ravens-tie", + "small-snails-burn", + "strong-shoes-end", + "stupid-rabbits-hide", + "sweet-ravens-refuse", + "tall-hornets-live", + "tasty-pianos-bathe", + "ten-schools-collect", + "thin-cycles-return", + "three-parrots-lie", + "tidy-cups-smoke", + "warm-steaks-fetch", + "wet-penguins-end", + "witty-buttons-greet", + "witty-foxes-thank", + "yellow-comics-cheer", + "young-avocados-brake", + "young-kiwis-fly" + ] +} diff --git a/.changeset/purple-hairs-hang.md b/.changeset/purple-hairs-hang.md new file mode 100644 index 0000000000000..bb7afddddc693 --- /dev/null +++ b/.changeset/purple-hairs-hang.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue where enterprise routing algorithms could get stuck on selecting the same agent due to chat limits being applied after agent selection, but before agent assignment diff --git a/.changeset/rotten-candles-train.md b/.changeset/rotten-candles-train.md new file mode 100644 index 0000000000000..8837d7009a2a3 --- /dev/null +++ b/.changeset/rotten-candles-train.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes the "Enabled" toggle always being displayed as active when editing a business hour. diff --git a/.changeset/serious-grapes-smell.md b/.changeset/serious-grapes-smell.md new file mode 100644 index 0000000000000..ad30efee76014 --- /dev/null +++ b/.changeset/serious-grapes-smell.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue with dynamic API routes requiring a server restart to be operable. diff --git a/.changeset/shy-dolls-protect.md b/.changeset/shy-dolls-protect.md new file mode 100644 index 0000000000000..9295c3eac9a22 --- /dev/null +++ b/.changeset/shy-dolls-protect.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where OAuth login buttons were not showing up on the login page diff --git a/.changeset/slow-ravens-tie.md b/.changeset/slow-ravens-tie.md new file mode 100644 index 0000000000000..8714790f332bd --- /dev/null +++ b/.changeset/slow-ravens-tie.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/core-typings": minor +"@rocket.chat/model-typings": minor +"@rocket.chat/models": minor +--- + +Implements auditing events for `/v1/users.update` API endpoint diff --git a/.changeset/small-snails-burn.md b/.changeset/small-snails-burn.md new file mode 100644 index 0000000000000..47d20011d850f --- /dev/null +++ b/.changeset/small-snails-burn.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/ui-client': minor +'@rocket.chat/meteor': minor +--- + +Restores the previous room announcement style +> This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it diff --git a/.changeset/strong-shoes-end.md b/.changeset/strong-shoes-end.md new file mode 100644 index 0000000000000..b0996aaa8a4da --- /dev/null +++ b/.changeset/strong-shoes-end.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Allows deleting federated remote users in case they are not present in the homeserver. diff --git a/.changeset/stupid-rabbits-hide.md b/.changeset/stupid-rabbits-hide.md new file mode 100644 index 0000000000000..341fea6af5a5d --- /dev/null +++ b/.changeset/stupid-rabbits-hide.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/core-typings': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Adds a new setting to allow syncing federated users data through LDAP diff --git a/.changeset/sweet-ravens-refuse.md b/.changeset/sweet-ravens-refuse.md new file mode 100644 index 0000000000000..52c0ee8214565 --- /dev/null +++ b/.changeset/sweet-ravens-refuse.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where header toolbox items are missing the proper margin in e2ee setup room header diff --git a/.changeset/tall-hornets-live.md b/.changeset/tall-hornets-live.md new file mode 100644 index 0000000000000..522c6bdc00d1f --- /dev/null +++ b/.changeset/tall-hornets-live.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where a federated user's display name would be overwritten by its username diff --git a/.changeset/tasty-pianos-bathe.md b/.changeset/tasty-pianos-bathe.md new file mode 100644 index 0000000000000..f247a9a24fd22 --- /dev/null +++ b/.changeset/tasty-pianos-bathe.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue where quick reactions (Feature Preview) would be absent on first access and after page refreshes diff --git a/.changeset/ten-schools-collect.md b/.changeset/ten-schools-collect.md new file mode 100644 index 0000000000000..d67233ddb4a53 --- /dev/null +++ b/.changeset/ten-schools-collect.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +--- + +Improves UX for users with mandatory 2FA roles by clarifying required actions diff --git a/.changeset/thin-cycles-return.md b/.changeset/thin-cycles-return.md new file mode 100644 index 0000000000000..e86391f5a0c18 --- /dev/null +++ b/.changeset/thin-cycles-return.md @@ -0,0 +1,9 @@ +--- +'@rocket.chat/ui-contexts': minor +'@rocket.chat/ui-client': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Moves the room search functionality from the sidebar to the navbar and reorganize their relative actions +> This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it diff --git a/.changeset/three-parrots-lie.md b/.changeset/three-parrots-lie.md new file mode 100644 index 0000000000000..609fa5183e7c6 --- /dev/null +++ b/.changeset/three-parrots-lie.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where rocket.chat would not properly log OAuth errors nor remove the credential with the error from the internal list diff --git a/.changeset/tidy-cups-smoke.md b/.changeset/tidy-cups-smoke.md new file mode 100644 index 0000000000000..d7220b0390a03 --- /dev/null +++ b/.changeset/tidy-cups-smoke.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes Omnichannel Contact Center's chats filter not working when "From" and "To" fields have the same date diff --git a/.changeset/warm-steaks-fetch.md b/.changeset/warm-steaks-fetch.md new file mode 100644 index 0000000000000..842d7f04b46e6 --- /dev/null +++ b/.changeset/warm-steaks-fetch.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes contact's conflict resolution not working due to invalid parameters diff --git a/.changeset/wet-penguins-end.md b/.changeset/wet-penguins-end.md new file mode 100644 index 0000000000000..b2a22d4a74130 --- /dev/null +++ b/.changeset/wet-penguins-end.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Separates voice call and video call room actions on room header. diff --git a/.changeset/witty-buttons-greet.md b/.changeset/witty-buttons-greet.md new file mode 100644 index 0000000000000..4a12544ed764c --- /dev/null +++ b/.changeset/witty-buttons-greet.md @@ -0,0 +1,4 @@ +--- +"@rocket.chat/meteor": patch +--- +Fixes issue with some charts on engagement dashboard not showing local time and missing data visualizers diff --git a/.changeset/witty-foxes-thank.md b/.changeset/witty-foxes-thank.md new file mode 100644 index 0000000000000..99ce5cd6e37e4 --- /dev/null +++ b/.changeset/witty-foxes-thank.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +--- + +Fixes a typo in the app update success toast diff --git a/.changeset/yellow-comics-cheer.md b/.changeset/yellow-comics-cheer.md new file mode 100644 index 0000000000000..1ebf8920c7a3d --- /dev/null +++ b/.changeset/yellow-comics-cheer.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue on the LDAP Background Sync where the process would stop syncing users if any of the LDAP users couldn't be properly mapped to a Rocket.Chat User (for example, by not having an email address) diff --git a/.changeset/young-avocados-brake.md b/.changeset/young-avocados-brake.md new file mode 100644 index 0000000000000..a20df69c72426 --- /dev/null +++ b/.changeset/young-avocados-brake.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Adds close action to contact unknown callout displayed within Livechat rooms diff --git a/.changeset/young-kiwis-fly.md b/.changeset/young-kiwis-fly.md new file mode 100644 index 0000000000000..468668c9a9eaa --- /dev/null +++ b/.changeset/young-kiwis-fly.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes contact custom fields not being updated when updating a visitor's custom field diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 12c5aea31216d..7ff84fed5d6d2 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -276,7 +276,7 @@ jobs: REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }} REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} - REPORTER_ROCKETCHAT_REPORT: ${{ github.event.pull_request.draft != 'true' && 'true' || '' }} + REPORTER_ROCKETCHAT_REPORT: ${{ github.event.pull_request.draft != 'true' && secrets.REPORTER_ROCKETCHAT_URL != '' && 'true' || '' }} REPORTER_ROCKETCHAT_RUN: ${{ github.run_number }} REPORTER_ROCKETCHAT_BRANCH: ${{ github.ref }} REPORTER_ROCKETCHAT_DRAFT: ${{ github.event.pull_request.draft }} diff --git a/apps/meteor/.docker/Dockerfile.alpine b/apps/meteor/.docker/Dockerfile.alpine index e098d21722a5d..ce2e8163caf14 100644 --- a/apps/meteor/.docker/Dockerfile.alpine +++ b/apps/meteor/.docker/Dockerfile.alpine @@ -48,13 +48,6 @@ RUN cd /app/bundle/programs/server \ # && npm install isolated-vm@4.6.0 \ # && mv node_modules/isolated-vm npm/node_modules/isolated-vm \ # # End hack for isolated-vm - # TODO: remove with meteor 3.1.2 - && cd /tmp \ - && npm install useragent-ng@2.4.4 --no-save \ - && rm -rf /app/bundle/programs/server/npm/node_modules/meteor/webapp/node_modules/useragent-ng \ - && mv node_modules/useragent-ng /app/bundle/programs/server/npm/node_modules/meteor/webapp/node_modules/useragent-ng \ - && rm -rf /tmp/node_modules \ - # end workaround for useragent-ng/meteor<3.1.2 && cd /app/bundle/programs/server/npm \ && npm rebuild bcrypt --build-from-source \ && npm cache clear --force diff --git a/apps/meteor/.meteor/versions b/apps/meteor/.meteor/versions index 979496429ffe3..f7f70ed740e93 100644 --- a/apps/meteor/.meteor/versions +++ b/apps/meteor/.meteor/versions @@ -26,14 +26,14 @@ dispatch:run-as-user@1.1.1 dynamic-import@0.7.4 ecmascript@0.16.10 ecmascript-runtime@0.8.3 -ecmascript-runtime-client@0.12.2 +ecmascript-runtime-client@0.12.3 ecmascript-runtime-server@0.11.1 ejson@1.1.4 email@3.1.2 es5-shim@4.8.1 facebook-oauth@1.11.5 facts-base@1.0.2 -fetch@0.1.5 +fetch@0.1.6 geojson-utils@1.0.12 github-oauth@1.4.2 google-oauth@1.4.5 @@ -42,22 +42,22 @@ http@3.0.0 id-map@1.2.0 inter-process-messaging@0.1.2 localstorage@1.2.1 -logging@1.3.5 +logging@1.3.6 meteor@2.1.0 meteor-base@1.5.2 meteor-developer-oauth@1.3.3 meteorhacks:inject-initial@1.0.5 -minifier-css@2.0.0 +minifier-css@2.0.1 minimongo@2.0.2 -modern-browsers@0.2.0 +modern-browsers@0.2.1 modules@0.20.3 modules-runtime@0.13.2 -mongo@2.1.0 +mongo@2.1.1 mongo-decimal@0.2.0 mongo-dev-server@1.1.1 mongo-id@1.0.9 npm-mongo@6.10.2 -oauth@3.0.1 +oauth@3.0.2 oauth1@1.5.2 oauth2@1.3.3 ordered-dict@1.2.0 diff --git a/apps/meteor/.mocharc.js b/apps/meteor/.mocharc.js index 076f6c56b66df..4a47aa3a52d65 100644 --- a/apps/meteor/.mocharc.js +++ b/apps/meteor/.mocharc.js @@ -25,6 +25,8 @@ module.exports = { exit: true, spec: [ 'lib/callbacks.spec.ts', + 'server/lib/ldap/*.spec.ts', + 'server/lib/ldap/**/*.spec.ts', 'ee/server/lib/ldap/*.spec.ts', 'ee/tests/**/*.tests.ts', 'ee/tests/**/*.spec.ts', diff --git a/apps/meteor/.scripts/replaceTranslationSprintfParams.ts b/apps/meteor/.scripts/replaceTranslationSprintfParams.ts new file mode 100644 index 0000000000000..140eee0a168df --- /dev/null +++ b/apps/meteor/.scripts/replaceTranslationSprintfParams.ts @@ -0,0 +1,140 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { stdin as input, stdout as output } from 'node:process'; +import { createInterface } from 'node:readline/promises'; + +import fg from 'fast-glob'; + +const LOCALES_DIR = join(process.cwd(), '..', '..', 'packages', 'i18n', 'src', 'locales'); + +/** + * Counts occurrences of a substring in a string + */ +const countOccurrences = (str: string, substring: string): number => { + let count = 0; + let position = str.indexOf(substring); + + while (position !== -1) { + count++; + position = str.indexOf(substring, position + 1); + } + + return count; +}; + +/** + * Parse a JSON file and return its content + */ +const parseFile = async (path: string): Promise> => { + const content = await readFile(path, 'utf-8'); + try { + return JSON.parse(content); + } catch (e) { + console.error(`Error parsing JSON file at ${path}: ${(e as Error).message}`); + process.exit(1); + } +}; + +/** + * Save a JSON file with proper formatting + */ +const saveFile = async (path: string, json: Record): Promise => { + try { + await writeFile(path, JSON.stringify(json, null, 2), 'utf-8'); + console.log(`Updated ${path}`); + } catch (e) { + console.error(`Error saving file at ${path}: ${(e as Error).message}`); + } +}; + +/** + * Main function to replace %s tokens with named parameters + */ +const replaceTranslationSprintfParams = async (): Promise => { + // Check if a translation key was provided + const translationKey = process.argv[2]; + + if (!translationKey) { + console.error('Please provide a translation key as parameter'); + process.exit(1); + } + + // Find all translation files + const translationFiles = await fg('*.i18n.json', { cwd: LOCALES_DIR, absolute: true }); + + if (translationFiles.length === 0) { + console.error(`No translation files found in ${LOCALES_DIR}`); + process.exit(1); + } + + console.log(`Found ${translationFiles.length} translation files`); + + // Find the key in the English file first to count the %s tokens + const enFilePath = translationFiles.find((file) => file.endsWith('en.i18n.json')); + + if (!enFilePath) { + console.error('English translation file not found'); + process.exit(1); + } + + const enTranslations = await parseFile(enFilePath); + + if (!enTranslations[translationKey]) { + console.error(`Translation key "${translationKey}" not found in English translations`); + process.exit(1); + } + + const englishValue = enTranslations[translationKey]; + const tokenCount = countOccurrences(englishValue, '%s'); + + console.log(`Found translation key "${translationKey}" with value: "${englishValue}"`); + console.log(`This string contains ${tokenCount} "%s" tokens`); + + if (tokenCount === 0) { + console.log('No %s tokens found, nothing to do'); + process.exit(0); + } + + // Prompt for parameter names + const rl = createInterface({ input, output }); + const promptMessage = `Please provide ${tokenCount} parameter names (comma-separated): `; + const paramNamesInput = await rl.question(promptMessage); + rl.close(); + + // Split and trim parameter names + const paramNames = paramNamesInput.split(',').map((name) => name.trim()); + + if (paramNames.length !== tokenCount) { + console.error(`Expected ${tokenCount} parameter names, but got ${paramNames.length}`); + process.exit(1); + } + + // Process all translation files + for (const filePath of translationFiles) { + // eslint-disable-next-line no-await-in-loop + const translations = await parseFile(filePath); + + if (translations[translationKey]) { + let updatedValue = translations[translationKey]; + let paramIndex = 0; + + // Replace each %s token with a named parameter + while (updatedValue.includes('%s') && paramIndex < paramNames.length) { + updatedValue = updatedValue.replace('%s', `{{${paramNames[paramIndex]}}}`); + paramIndex++; + } + + translations[translationKey] = updatedValue; + // eslint-disable-next-line no-await-in-loop + await saveFile(filePath, translations); + } + } + + console.log('All translation files have been updated'); +}; + +// Run the script +replaceTranslationSprintfParams().catch((error) => { + console.error('Error:', error); + process.exit(1); +}); diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 59b3593463c33..057a50115a37b 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,11 +1,433 @@ # @rocket.chat/meteor -## 7.5.1 +## 7.6.0-rc.8 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- ([#35864](https://github.com/RocketChat/Rocket.Chat/pull/35864)) Fixes an issue where OAuth login buttons were not showing up on the login page + +- ([#35852](https://github.com/RocketChat/Rocket.Chat/pull/35852)) Fixes an issue where rocket.chat would not properly log OAuth errors nor remove the credential with the error from the internal list + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/rest-typings@7.6.0-rc.8 + - @rocket.chat/license@1.0.12-rc.8 + - @rocket.chat/omnichannel-services@0.3.18-rc.8 + - @rocket.chat/pdf-worker@0.3.0-rc.8 + - @rocket.chat/presence@0.2.21-rc.8 + - @rocket.chat/api-client@0.2.21-rc.8 + - @rocket.chat/apps@0.5.0-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/cron@0.1.21-rc.8 + - @rocket.chat/freeswitch@1.2.8-rc.8 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.8 + - @rocket.chat/gazzodown@18.0.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 + - @rocket.chat/ui-contexts@18.0.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.2.0-rc.8 + - @rocket.chat/ui-theming@0.4.3 + - @rocket.chat/ui-avatar@14.0.0-rc.8 + - @rocket.chat/ui-client@18.0.0-rc.8 + - @rocket.chat/ui-video-conf@18.0.0-rc.8 + - @rocket.chat/ui-voip@8.0.0-rc.8 + - @rocket.chat/web-ui-registration@18.0.0-rc.8 + - @rocket.chat/instance-status@0.1.21-rc.8 +
+ +## 7.6.0-rc.7 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/rest-typings@7.6.0-rc.7 + - @rocket.chat/license@1.0.12-rc.7 + - @rocket.chat/omnichannel-services@0.3.18-rc.7 + - @rocket.chat/pdf-worker@0.3.0-rc.7 + - @rocket.chat/presence@0.2.21-rc.7 + - @rocket.chat/api-client@0.2.21-rc.7 + - @rocket.chat/apps@0.5.0-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/cron@0.1.21-rc.7 + - @rocket.chat/freeswitch@1.2.8-rc.7 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.7 + - @rocket.chat/gazzodown@18.0.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 + - @rocket.chat/ui-contexts@18.0.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.2.0-rc.7 + - @rocket.chat/ui-theming@0.4.3 + - @rocket.chat/ui-avatar@14.0.0-rc.7 + - @rocket.chat/ui-client@18.0.0-rc.7 + - @rocket.chat/ui-video-conf@18.0.0-rc.7 + - @rocket.chat/ui-voip@8.0.0-rc.7 + - @rocket.chat/web-ui-registration@18.0.0-rc.7 + - @rocket.chat/instance-status@0.1.21-rc.7 +
+ +## 7.6.0-rc.6 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/rest-typings@7.6.0-rc.6 + - @rocket.chat/license@1.0.12-rc.6 + - @rocket.chat/omnichannel-services@0.3.18-rc.6 + - @rocket.chat/pdf-worker@0.3.0-rc.6 + - @rocket.chat/presence@0.2.21-rc.6 + - @rocket.chat/api-client@0.2.21-rc.6 + - @rocket.chat/apps@0.5.0-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/cron@0.1.21-rc.6 + - @rocket.chat/freeswitch@1.2.8-rc.6 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.6 + - @rocket.chat/gazzodown@18.0.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 + - @rocket.chat/ui-contexts@18.0.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.2.0-rc.6 + - @rocket.chat/ui-theming@0.4.3 + - @rocket.chat/ui-avatar@14.0.0-rc.6 + - @rocket.chat/ui-client@18.0.0-rc.6 + - @rocket.chat/ui-video-conf@18.0.0-rc.6 + - @rocket.chat/ui-voip@8.0.0-rc.6 + - @rocket.chat/web-ui-registration@18.0.0-rc.6 + - @rocket.chat/instance-status@0.1.21-rc.6 +
+ +## 7.6.0-rc.5 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/rest-typings@7.6.0-rc.5 + - @rocket.chat/license@1.0.12-rc.5 + - @rocket.chat/omnichannel-services@0.3.18-rc.5 + - @rocket.chat/pdf-worker@0.3.0-rc.5 + - @rocket.chat/presence@0.2.21-rc.5 + - @rocket.chat/api-client@0.2.21-rc.5 + - @rocket.chat/apps@0.5.0-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/cron@0.1.21-rc.5 + - @rocket.chat/freeswitch@1.2.8-rc.5 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.5 + - @rocket.chat/gazzodown@18.0.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 + - @rocket.chat/ui-contexts@18.0.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.2.0-rc.5 + - @rocket.chat/ui-theming@0.4.3 + - @rocket.chat/ui-avatar@14.0.0-rc.5 + - @rocket.chat/ui-client@18.0.0-rc.5 + - @rocket.chat/ui-video-conf@18.0.0-rc.5 + - @rocket.chat/ui-voip@8.0.0-rc.5 + - @rocket.chat/web-ui-registration@18.0.0-rc.5 + - @rocket.chat/instance-status@0.1.21-rc.5 +
+ +## 7.6.0-rc.4 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/rest-typings@7.6.0-rc.4 + - @rocket.chat/license@1.0.12-rc.4 + - @rocket.chat/omnichannel-services@0.3.18-rc.4 + - @rocket.chat/pdf-worker@0.3.0-rc.4 + - @rocket.chat/presence@0.2.21-rc.4 + - @rocket.chat/api-client@0.2.21-rc.4 + - @rocket.chat/apps@0.5.0-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/cron@0.1.21-rc.4 + - @rocket.chat/freeswitch@1.2.8-rc.4 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.4 + - @rocket.chat/gazzodown@18.0.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 + - @rocket.chat/ui-contexts@18.0.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.2.0-rc.4 + - @rocket.chat/ui-theming@0.4.3 + - @rocket.chat/ui-avatar@14.0.0-rc.4 + - @rocket.chat/ui-client@18.0.0-rc.4 + - @rocket.chat/ui-video-conf@18.0.0-rc.4 + - @rocket.chat/ui-voip@8.0.0-rc.4 + - @rocket.chat/web-ui-registration@18.0.0-rc.4 + - @rocket.chat/instance-status@0.1.21-rc.4 +
+ +## 7.6.0-rc.3 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- ([#35879](https://github.com/RocketChat/Rocket.Chat/pull/35879)) Fixes contact's conflict resolution not working due to invalid parameters + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/rest-typings@7.6.0-rc.3 + - @rocket.chat/license@1.0.12-rc.3 + - @rocket.chat/omnichannel-services@0.3.18-rc.3 + - @rocket.chat/pdf-worker@0.3.0-rc.3 + - @rocket.chat/presence@0.2.21-rc.3 + - @rocket.chat/api-client@0.2.21-rc.3 + - @rocket.chat/apps@0.5.0-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/cron@0.1.21-rc.3 + - @rocket.chat/freeswitch@1.2.8-rc.3 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.3 + - @rocket.chat/gazzodown@18.0.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 + - @rocket.chat/ui-contexts@18.0.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.2.0-rc.3 + - @rocket.chat/ui-theming@0.4.3 + - @rocket.chat/ui-avatar@14.0.0-rc.3 + - @rocket.chat/ui-client@18.0.0-rc.3 + - @rocket.chat/ui-video-conf@18.0.0-rc.3 + - @rocket.chat/ui-voip@8.0.0-rc.3 + - @rocket.chat/web-ui-registration@18.0.0-rc.3 + - @rocket.chat/instance-status@0.1.21-rc.3 +
+ +## 7.6.0-rc.2 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/rest-typings@7.6.0-rc.2 + - @rocket.chat/license@1.0.12-rc.2 + - @rocket.chat/omnichannel-services@0.3.18-rc.2 + - @rocket.chat/pdf-worker@0.3.0-rc.2 + - @rocket.chat/presence@0.2.21-rc.2 + - @rocket.chat/api-client@0.2.21-rc.2 + - @rocket.chat/apps@0.5.0-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/cron@0.1.21-rc.2 + - @rocket.chat/freeswitch@1.2.8-rc.2 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.2 + - @rocket.chat/gazzodown@18.0.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 + - @rocket.chat/ui-contexts@18.0.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.2.0-rc.2 + - @rocket.chat/ui-theming@0.4.3 + - @rocket.chat/ui-avatar@14.0.0-rc.2 + - @rocket.chat/ui-client@18.0.0-rc.2 + - @rocket.chat/ui-video-conf@18.0.0-rc.2 + - @rocket.chat/ui-voip@8.0.0-rc.2 + - @rocket.chat/web-ui-registration@18.0.0-rc.2 + - @rocket.chat/instance-status@0.1.21-rc.2 +
+ +## 7.6.0-rc.1 ### Patch Changes - Bump @rocket.chat/meteor version. +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/rest-typings@7.6.0-rc.1 + - @rocket.chat/license@1.0.11-rc.1 + - @rocket.chat/omnichannel-services@0.3.17-rc.1 + - @rocket.chat/pdf-worker@0.3.0-rc.1 + - @rocket.chat/presence@0.2.20-rc.1 + - @rocket.chat/api-client@0.2.20-rc.1 + - @rocket.chat/apps@0.5.0-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/cron@0.1.20-rc.1 + - @rocket.chat/freeswitch@1.2.7-rc.1 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.1 + - @rocket.chat/gazzodown@18.0.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 + - @rocket.chat/ui-contexts@18.0.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.2.0-rc.1 + - @rocket.chat/ui-theming@0.4.3 + - @rocket.chat/ui-avatar@14.0.0-rc.1 + - @rocket.chat/ui-client@18.0.0-rc.1 + - @rocket.chat/ui-video-conf@18.0.0-rc.1 + - @rocket.chat/ui-voip@8.0.0-rc.1 + - @rocket.chat/web-ui-registration@18.0.0-rc.1 + - @rocket.chat/instance-status@0.1.20-rc.1 +
+ +## 7.6.0-rc.0 + +### Minor Changes + +- ([#35717](https://github.com/RocketChat/Rocket.Chat/pull/35717)) Adds new settings to allow configuring custom variables with string manipulation functions on the LDAP data mapper + +- ([#35280](https://github.com/RocketChat/Rocket.Chat/pull/35280)) Allows apps to react to department status changes. + +- ([#35644](https://github.com/RocketChat/Rocket.Chat/pull/35644)) Adds the ability to dynamically add and remove options from select/multi-select settings in the Apps Engine to support more flexible configuration scenarios by exposing two new methods on the settings API. + +- ([#34954](https://github.com/RocketChat/Rocket.Chat/pull/34954) by [@tapiarafael](https://github.com/tapiarafael)) Allows search omnichannel rooms by the exact visitor name using double quotes to have a faster response + +- ([#35613](https://github.com/RocketChat/Rocket.Chat/pull/35613)) Replaces the parent room tag in room header in favor of a button to back to the parent room + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#35615](https://github.com/RocketChat/Rocket.Chat/pull/35615)) Removes the avatar in the room header + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#35218](https://github.com/RocketChat/Rocket.Chat/pull/35218)) Adds a new admin page to audit settings changes in a server + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +- ([#35631](https://github.com/RocketChat/Rocket.Chat/pull/35631)) Places the room topic next to the room title + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#34494](https://github.com/RocketChat/Rocket.Chat/pull/34494)) Implements auditing events for `/v1/users.update` API endpoint + +- ([#35672](https://github.com/RocketChat/Rocket.Chat/pull/35672)) Restores the previous room announcement style + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#35464](https://github.com/RocketChat/Rocket.Chat/pull/35464)) Allows deleting federated remote users in case they are not present in the homeserver. + +- ([#35718](https://github.com/RocketChat/Rocket.Chat/pull/35718)) Adds a new setting to allow syncing federated users data through LDAP + +- ([#35807](https://github.com/RocketChat/Rocket.Chat/pull/35807)) Moves the room search functionality from the sidebar to the navbar and reorganize their relative actions + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#35700](https://github.com/RocketChat/Rocket.Chat/pull/35700)) Separates voice call and video call room actions on room header. + +- ([#35703](https://github.com/RocketChat/Rocket.Chat/pull/35703)) Adds close action to contact unknown callout displayed within Livechat rooms + +### Patch Changes + +- ([#35790](https://github.com/RocketChat/Rocket.Chat/pull/35790)) Fixes an issue in `Admin > Settings` page where sometimes settings guarded by a license module would not be editable despite having the required modules. + +- ([#35795](https://github.com/RocketChat/Rocket.Chat/pull/35795)) Fixes incorrect message sender for incoming webhooks when "Post As" field is updated by ensuring both username and userId are synced to reflect the selected user. + +- ([#35299](https://github.com/RocketChat/Rocket.Chat/pull/35299)) Fixes user email verification update not working on admin > users + +- ([#35762](https://github.com/RocketChat/Rocket.Chat/pull/35762)) Fixes an issue with apps loading in microservices in some cases by ensuring all services have started before trying to load apps + +- ([#35816](https://github.com/RocketChat/Rocket.Chat/pull/35816)) Fixes an issue where the outlook calendar items list is not rendering properly + +- ([#35244](https://github.com/RocketChat/Rocket.Chat/pull/35244) by [@tapiarafael](https://github.com/tapiarafael)) Ensures seat limit validation in LDAP sync, preventing activations beyond license restrictions. + +- ([#35497](https://github.com/RocketChat/Rocket.Chat/pull/35497)) Fixes an issue where the app's logs index was not being created by default sometimes, also set to be always 30 days + +- ([#35736](https://github.com/RocketChat/Rocket.Chat/pull/35736)) Fixes an error causing the server to throw an "unhandled promise rejection" when removing an agent from a department without a business hour when using `Multiple` business hours + +- ([#35722](https://github.com/RocketChat/Rocket.Chat/pull/35722)) Fixes the behavior of "Maximum number of simultaneous chats" settings, making them more predictable. Previously, we applied a single limit per operation, being the order: `Department > Agent > Global`. This caused the department limit to take prescedence over agent's specific limit, causing some unwanted side effects. + + The new way of applying the filter is as follows: + + - An agent can accept chats from multiple departments, respecting each department’s limit individually. + - The total number of active chats (across all departments) must not exceed the configured Agent-Level or Global limit. + - If neither the Agent-Level nor Global Limit is set, only department-specific limits apply. + - If no limits are set at any level, there is no restriction on the number of chats an agent can handle. + +- ([#35394](https://github.com/RocketChat/Rocket.Chat/pull/35394) by [@sushen123](https://github.com/sushen123)) Fixes the save button loading state in app settings, ensuring it resets properly after saving. + +- ([#35779](https://github.com/RocketChat/Rocket.Chat/pull/35779)) Fixes a fetch infinite loop when adding agents to a department + +- ([#35679](https://github.com/RocketChat/Rocket.Chat/pull/35679)) Fixes a GUI crash when editing a canned response with tags via room contextual bar. + +- ([#35568](https://github.com/RocketChat/Rocket.Chat/pull/35568)) Fixes an issue with the leave room confirmation modal not displaying the room's name. + +- ([#35782](https://github.com/RocketChat/Rocket.Chat/pull/35782)) Fixes an issue where the incoming webhooks integration allowed messages to be sent to public channels under private teams by users who were not members of the team. + +- ([#35618](https://github.com/RocketChat/Rocket.Chat/pull/35618)) Fixes an issue when sending a message on an omnichannel room would cause the web client to fetch the room data again. + +- ([#35273](https://github.com/RocketChat/Rocket.Chat/pull/35273)) Fixes an issue where the code input type in settings renders duplicate code text boxes. + +- ([#35765](https://github.com/RocketChat/Rocket.Chat/pull/35765)) Fixes an issue causing VoIP calls to no longer reach the client after a temporary disconnection + +- ([#35397](https://github.com/RocketChat/Rocket.Chat/pull/35397)) Fixes a race condition while federating that caused direct messages to not work + +- ([#35832](https://github.com/RocketChat/Rocket.Chat/pull/35832)) Fixes an issue where Voice Calls were unable to gather Ice Servers + +- ([#35757](https://github.com/RocketChat/Rocket.Chat/pull/35757)) Fixes an issue where the bypass to call methods over microservices always returns to `{}` + +- ([#35831](https://github.com/RocketChat/Rocket.Chat/pull/35831)) Fixes an issue where enterprise routing algorithms could get stuck on selecting the same agent due to chat limits being applied after agent selection, but before agent assignment + +- ([#35698](https://github.com/RocketChat/Rocket.Chat/pull/35698)) Fixes the "Enabled" toggle always being displayed as active when editing a business hour. + +- ([#35715](https://github.com/RocketChat/Rocket.Chat/pull/35715)) Fixes an issue with dynamic API routes requiring a server restart to be operable. + +- ([#35614](https://github.com/RocketChat/Rocket.Chat/pull/35614)) Fixes an issue where header toolbox items are missing the proper margin in e2ee setup room header + +- ([#35716](https://github.com/RocketChat/Rocket.Chat/pull/35716)) Fixes an issue where a federated user's display name would be overwritten by its username + +- ([#35766](https://github.com/RocketChat/Rocket.Chat/pull/35766)) Fixes an issue where quick reactions (Feature Preview) would be absent on first access and after page refreshes + +- ([#35709](https://github.com/RocketChat/Rocket.Chat/pull/35709)) Improves UX for users with mandatory 2FA roles by clarifying required actions + +- ([#35616](https://github.com/RocketChat/Rocket.Chat/pull/35616)) Fixes Omnichannel Contact Center's chats filter not working when "From" and "To" fields have the same date + +- ([#35019](https://github.com/RocketChat/Rocket.Chat/pull/35019)) Fixes issue with some charts on engagement dashboard not showing local time and missing data visualizers + +- ([#35733](https://github.com/RocketChat/Rocket.Chat/pull/35733)) Fixes a typo in the app update success toast + +- ([#35810](https://github.com/RocketChat/Rocket.Chat/pull/35810)) Fixes an issue on the LDAP Background Sync where the process would stop syncing users if any of the LDAP users couldn't be properly mapped to a Rocket.Chat User (for example, by not having an email address) + +- ([#35580](https://github.com/RocketChat/Rocket.Chat/pull/35580)) Fixes contact custom fields not being updated when updating a visitor's custom field + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d649a761edd71e1325a635b757ef1df2e5a778a4, bbd14f84214b4785f2b58cfeb8e9117bdfbf18e8, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, f545617c2ac3d67af533e64c2670d8d564a56d15, bffc49f426259925c415651c2b2a58083dac547a, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, ec7894139c56d0e29ac696c448cc932efb6cb0f0, 6bf386dcc2a560963cf719fbc2d96569ce23a2de, 1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 5e3ab1a07163cd22ad4c41502ef232845d26bdc2, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 2ee1a81de770a682f6e7a8590a896e76a32f4e3c, 47ae69912cd90743e7bf836fdee4be481a01bbba, 72725d391e79b44e7380ee2fe640e2e4426c77ca, 4b28126ac94cf1d3312b30ad9863ca02673f49d4, cc344bea08c08501f50e9cee620b2926a322a4ee, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de, be67bb771294c337c28da5e61ae47ab4e32244d1, 895ea3fdbba1d0e3cf1bed03cb8d0abfcca5d351]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/i18n@1.6.0-rc.0 + - @rocket.chat/apps-engine@1.51.0-rc.0 + - @rocket.chat/apps@0.5.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/ui-client@18.0.0-rc.0 + - @rocket.chat/ui-voip@8.0.0-rc.0 + - @rocket.chat/ui-contexts@18.0.0-rc.0 + - @rocket.chat/network-broker@0.2.0-rc.0 + - @rocket.chat/pdf-worker@0.3.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 + - @rocket.chat/license@1.0.11-rc.0 + - @rocket.chat/omnichannel-services@0.3.17-rc.0 + - @rocket.chat/presence@0.2.20-rc.0 + - @rocket.chat/api-client@0.2.20-rc.0 + - @rocket.chat/cron@0.1.20-rc.0 + - @rocket.chat/freeswitch@1.2.7-rc.0 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.0 + - @rocket.chat/gazzodown@18.0.0-rc.0 + - @rocket.chat/web-ui-registration@18.0.0-rc.0 + - @rocket.chat/instance-status@0.1.20-rc.0 + - @rocket.chat/ui-theming@0.4.3 + - @rocket.chat/ui-avatar@14.0.0-rc.0 + - @rocket.chat/ui-video-conf@18.0.0-rc.0 + - @rocket.chat/server-cloud-communication@0.0.2 +
+ +## 7.5.1 + - Bump @rocket.chat/meteor version. - ([#35732](https://github.com/RocketChat/Rocket.Chat/pull/35732) by [@dionisio-bot](https://github.com/dionisio-bot)) Fixes an issue with dynamic API routes requiring a server restart to be operable. diff --git a/apps/meteor/app/2fa/server/loginHandler.ts b/apps/meteor/app/2fa/server/loginHandler.ts index b554330e140f4..1d039c7a89e4a 100644 --- a/apps/meteor/app/2fa/server/loginHandler.ts +++ b/apps/meteor/app/2fa/server/loginHandler.ts @@ -85,7 +85,7 @@ OAuth._retrievePendingCredential = async function (key, ...args): Promise Analytics - -## Features Enabled -* **Messages**: `true/false` determines whether to use custom events to track how many times a user does something with a message. This ranges from sending messages, editing messages, reacting to messages, pinning, starring, and etc. -* **Rooms**: `true/false` determines whether to use custom events to track how many times a user does actions related to a room (channel, direct message, group). This ranges from creating, leaving, archiving, renaming, and etc. -* **Users**: `true/false` determines whether to use custom events to track how many times a user does actions related to users. This ranges from resetting passwords, creating new users, updating profile pictures, etc. diff --git a/apps/meteor/app/api/server/api.ts b/apps/meteor/app/api/server/api.ts index a93cf080140db..2898624f191cf 100644 --- a/apps/meteor/app/api/server/api.ts +++ b/apps/meteor/app/api/server/api.ts @@ -1,4 +1,5 @@ import type { IMethodConnection, IUser, IRoom } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Logger } from '@rocket.chat/logger'; import { Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; @@ -6,8 +7,7 @@ import type { JoinPathPattern, Method } from '@rocket.chat/rest-typings'; import { ajv } from '@rocket.chat/rest-typings/src/v1/Ajv'; import { wrapExceptions } from '@rocket.chat/tools'; import type { ValidateFunction } from 'ajv'; -import express from 'express'; -import type { Request, Response } from 'express'; +import type express from 'express'; import { Accounts } from 'meteor/accounts-base'; import { DDP } from 'meteor/ddp'; import { DDPCommon } from 'meteor/ddp-common'; @@ -33,15 +33,19 @@ import type { TypedAction, TypedOptions, UnauthorizedResult, + RedirectStatusCodes, + RedirectResult, } from './definition'; import { getUserInfo } from './helpers/getUserInfo'; import { parseJsonQuery } from './helpers/parseJsonQuery'; import { cors } from './middlewares/cors'; import { loggerMiddleware } from './middlewares/logger'; import { metricsMiddleware } from './middlewares/metrics'; +import { remoteAddressMiddleware } from './middlewares/remoteAddressMiddleware'; import { tracerSpanMiddleware } from './middlewares/tracer'; import type { Route } from './router'; import { Router } from './router'; +import { license } from '../../../ee/app/api-enterprise/server/middlewares/license'; import { isObject } from '../../../lib/utils/isObject'; import { getNestedProp } from '../../../server/lib/getNestedProp'; import { shouldBreakInVersion } from '../../../server/lib/shouldBreakInVersion'; @@ -84,7 +88,7 @@ interface IAPIDefaultFieldsToExclude { inviteToken: number; } -type RateLimiterOptions = { +export type RateLimiterOptions = { numRequestsAllowed?: number; intervalTimeInMS?: number; }; @@ -101,34 +105,6 @@ const rateLimiterDictionary: Record< } > = {}; -const getRequestIP = (req: Request): string | null => { - const socket = req.socket || (req.connection as any)?.socket; - const remoteAddress = String( - req.headers['x-real-ip'] || (typeof socket !== 'string' && (socket?.remoteAddress || req.connection?.remoteAddress || null)), - ); - const forwardedFor = String(req.headers['x-forwarded-for']); - - if (!socket) { - return remoteAddress || forwardedFor || null; - } - - const httpForwardedCount = parseInt(String(process.env.HTTP_FORWARDED_COUNT)) || 0; - if (httpForwardedCount <= 0) { - return remoteAddress; - } - - if (!forwardedFor || typeof forwardedFor.valueOf() !== 'string') { - return remoteAddress; - } - - const forwardedForIPs = forwardedFor.trim().split(/\s*,\s*/); - if (httpForwardedCount > forwardedForIPs.length) { - return remoteAddress; - } - - return forwardedForIPs[forwardedForIPs.length - httpForwardedCount]; -}; - const generateConnection = ( ipAddress: string, httpHeaders: Record, @@ -216,7 +192,6 @@ export class APIClass< services: 0, inviteToken: 0, }; - this.router = new Router(`/${this.apiPath}`.replace(/\/$/, '').replaceAll('//', '/')); if (useDefaultAuth) { @@ -273,6 +248,13 @@ export class APIClass< return finalResult as SuccessResult; } + public redirect(code: C, result: T): RedirectResult { + return { + statusCode: code, + body: result, + }; + } + public failure(result?: T): FailureResult; public failure( @@ -338,6 +320,16 @@ export class APIClass< }; } + public unavailableResult(msg?: T): InternalError { + return { + statusCode: 503, + body: { + success: false, + error: msg || 'Service unavailable', + }, + }; + } + public unauthorized(msg?: T): UnauthorizedResult { return { statusCode: 401, @@ -397,9 +389,12 @@ export class APIClass< rateLimiterDictionary[objectForRateLimitMatch.route].rateLimiter.increment(objectForRateLimitMatch); const attemptResult = await rateLimiterDictionary[objectForRateLimitMatch.route].rateLimiter.check(objectForRateLimitMatch); const timeToResetAttempsInSeconds = Math.ceil(attemptResult.timeToReset / 1000); - response.setHeader('X-RateLimit-Limit', rateLimiterDictionary[objectForRateLimitMatch.route].options.numRequestsAllowed ?? ''); - response.setHeader('X-RateLimit-Remaining', attemptResult.numInvocationsLeft); - response.setHeader('X-RateLimit-Reset', new Date().getTime() + attemptResult.timeToReset); + response.headers.set( + 'X-RateLimit-Limit', + String(rateLimiterDictionary[objectForRateLimitMatch.route].options.numRequestsAllowed ?? ''), + ); + response.headers.set('X-RateLimit-Remaining', String(attemptResult.numInvocationsLeft)); + response.headers.set('X-RateLimit-Reset', String(new Date().getTime() + attemptResult.timeToReset)); if (!attemptResult.allowed) { throw new Meteor.Error( @@ -436,7 +431,7 @@ export class APIClass< }: { routes: string[]; rateLimiterOptions: RateLimiterOptions | boolean; - endpoints: string[]; + endpoints: Record | string[]; }): void { if (typeof rateLimiterOptions !== 'object') { throw new Meteor.Error('"rateLimiterOptions" must be an object'); @@ -483,8 +478,8 @@ export class APIClass< if (options && (!('twoFactorRequired' in options) || !options.twoFactorRequired)) { return; } - const code = request.headers['x-2fa-code'] ? String(request.headers['x-2fa-code']) : undefined; - const method = request.headers['x-2fa-method'] ? String(request.headers['x-2fa-method']) : undefined; + const code = request.headers.get('x-2fa-code') ? String(request.headers.get('x-2fa-code')) : undefined; + const method = request.headers.get('x-2fa-method') ? String(request.headers.get('x-2fa-method')) : undefined; await checkCodeForUser({ user: userId, @@ -618,7 +613,7 @@ export class APIClass< path: TPathPattern; } & Omit) > { - this.addRoute([subpath], { ...options, typed: true }, { [method.toLowerCase()]: { action } } as any); + this.addRoute([subpath], { tags: [], ...options, typed: true }, { [method.toLowerCase()]: { action } } as any); this.registerTypedRoutes(method, subpath, options); return this; } @@ -750,6 +745,7 @@ export class APIClass< // Note: This is required due to Restivus calling `addRoute` in the constructor of itself Object.keys(operations).forEach((method) => { const _options = { ...options }; + const { tags = ['Missing Documentation'] } = _options as Record; if (typeof operations[method as keyof Operations] === 'function') { (operations as Record)[method as string] = { @@ -769,14 +765,12 @@ export class APIClass< const api = this; (operations[method as keyof Operations] as Record).action = async function _internalRouteActionHandler() { - this.requestIp = getRequestIP(this.request)!; - if (options.authRequired || options.authOrAnonRequired) { - const user = await api.authenticatedRoute(this.request); + const user = await api.authenticatedRoute.call(this, this.request); this.user = user!; - this.userId = String(this.request.headers['x-user-id']); - this.token = (this.request.headers['x-auth-token'] && - Accounts._hashLoginToken(String(this.request.headers['x-auth-token'])))!; + this.userId = String(this.request.headers.get('x-user-id')); + const authToken = this.request.headers.get('x-auth-token'); + this.token = (authToken && Accounts._hashLoginToken(String(authToken)))!; } if (!this.user && options.authRequired && !options.authOrAnonRequired && !settings.get('Accounts_AllowAnonymousRead')) { @@ -794,7 +788,7 @@ export class APIClass< const objectForRateLimitMatch = { IPAddr: this.requestIp, - route: `/${api.apiPath}${this.request.route.path}${this.request.method.toLowerCase()}`, + route: `/${route}${this.request.method.toLowerCase()}`, }; let result; @@ -897,12 +891,12 @@ export class APIClass< return result; } as InnerAction; - // Allow the endpoints to make usage of the logger which respects the user's settings (operations[method as keyof Operations] as Record).logger = logger; this.router[method.toLowerCase() as 'get' | 'post' | 'put' | 'delete']( `/${route}`.replaceAll('//', '/'), - _options as TypedOptions, + { ..._options, tags } as TypedOptions, + license(_options as TypedOptions, License), (operations[method as keyof Operations] as Record).action as any, ); this._routes.push({ @@ -920,9 +914,11 @@ export class APIClass< } protected async authenticatedRoute(req: Request): Promise { - const { 'x-user-id': userId } = req.headers; + const headers = Object.fromEntries(req.headers.entries()); - const userToken = String(req.headers['x-auth-token']); + const { 'x-user-id': userId } = headers; + + const userToken = String(headers['x-auth-token']); if (userId && userToken) { return Users.findOne( @@ -961,7 +957,7 @@ export class APIClass< return bodyParams; } - const code = bodyCode || request.headers['x-2fa-code']; + const code = bodyCode || request.headers.get('x-2fa-code'); const auth: Record = { password, @@ -1023,7 +1019,7 @@ export class APIClass< const args = loginCompatibility(this.bodyParams, request); const invocation = new DDPCommon.MethodInvocation({ - connection: generateConnection(getRequestIP(request) || '', this.request.headers), + connection: generateConnection(this.requestIp || '', this.request.headers), }); try { @@ -1215,13 +1211,10 @@ settings.watch('API_Enable_Rate_Limiter_Limit_Calls_Default', (value) => API.v1.reloadRoutesToRefreshRateLimiter(); }); -Meteor.startup(() => { - (WebApp.connectHandlers as unknown as ReturnType).use( +export const startRestAPI = () => { + (WebApp.rawConnectHandlers as unknown as ReturnType).use( API.api - .use((_req, res, next) => { - res.removeHeader('X-Powered-By'); - next(); - }) + .use(remoteAddressMiddleware) .use(cors(settings)) .use(loggerMiddleware(logger)) .use(metricsMiddleware(API.v1, settings, metrics.rocketchatRestApi)) @@ -1229,18 +1222,4 @@ Meteor.startup(() => { .use(API.v1.router) .use(API.default.router).router, ); -}); - -(WebApp.connectHandlers as unknown as ReturnType) - .use( - express.json({ - limit: '50mb', - }), - ) - .use( - express.urlencoded({ - extended: true, - limit: '50mb', - }), - ) - .use(express.query({})); +}; diff --git a/apps/meteor/app/api/server/default/openApi.ts b/apps/meteor/app/api/server/default/openApi.ts index 96570e7495720..6f5b31f2625ea 100644 --- a/apps/meteor/app/api/server/default/openApi.ts +++ b/apps/meteor/app/api/server/default/openApi.ts @@ -77,7 +77,7 @@ API.default.addRoute( get() { const { withUndocumented = false } = this.queryParams; - return API.default.success(makeOpenAPIResponse(getTypedRoutes(API.v1.typedRoutes, { withUndocumented }))); + return API.default.success(makeOpenAPIResponse(getTypedRoutes(API.api.typedRoutes, { withUndocumented }))); }, }, ); @@ -85,6 +85,10 @@ API.default.addRoute( app.use( '/api-docs', swaggerUi.serve, - swaggerUi.setup(makeOpenAPIResponse(getTypedRoutes(API.v1.typedRoutes, { withUndocumented: false }))), + swaggerUi.setup(null, { + swaggerOptions: { + url: `${settings.get('Site_Url')}/api/docs/json`, + }, + }), ); WebApp.connectHandlers.use(app); diff --git a/apps/meteor/app/api/server/definition.ts b/apps/meteor/app/api/server/definition.ts index 742953a4c3651..25ae81f08edb7 100644 --- a/apps/meteor/app/api/server/definition.ts +++ b/apps/meteor/app/api/server/definition.ts @@ -1,13 +1,20 @@ -import type { IUser } from '@rocket.chat/core-typings'; +import type { IUser, LicenseModule } from '@rocket.chat/core-typings'; import type { Logger } from '@rocket.chat/logger'; import type { Method, MethodOf, OperationParams, OperationResult, PathPattern, UrlParams } from '@rocket.chat/rest-typings'; import type { ValidateFunction } from 'ajv'; -import type { Request, Response } from 'express'; import type { ITwoFactorOptions } from '../../2fa/server/code'; -export type SuccessResult = { - statusCode: 200; +export type SuccessStatusCodes = Exclude, Range<200>>; + +export type RedirectStatusCodes = Exclude, Range<300>>; + +export type AuthorizationStatusCodes = Exclude, Range<400>>; + +export type ErrorStatusCodes = Exclude, Range<500>>, 509>; + +export type SuccessResult = { + statusCode: TStatusCode; body: T extends object ? { success: true } & T : T; }; @@ -26,6 +33,11 @@ export type FailureResult = { + statusCode: TStatusCode; + body: T; +}; + export type UnauthorizedResult = { statusCode: 401; body: { @@ -43,10 +55,10 @@ export type ForbiddenResult = { }; }; -export type InternalError = { - statusCode: 500; +export type InternalError = { + statusCode: StatusCode; body: { - error: T | 'Internal server error'; + error: T | D; success: false; }; }; @@ -124,6 +136,7 @@ export type PartialThis = { readonly response: Response; readonly userId: string; readonly bodyParams: Record; + readonly path: string; readonly queryParams: Record; readonly queryOperations?: string[]; readonly queryFields?: string[]; @@ -236,24 +249,25 @@ export type ActionOperations as Lowercase]: ActionOperation, TPathPattern, TOptions>; }; +type Range = Result['length'] extends N + ? Result[number] + : Range; + +type HTTPStatusCodes = SuccessStatusCodes | RedirectStatusCodes | AuthorizationStatusCodes | ErrorStatusCodes; export type TypedOptions = { response: { - 200: ValidateFunction; - 300?: ValidateFunction; - 400?: ValidateFunction; - 401?: ValidateFunction; - 403?: ValidateFunction; - 404?: ValidateFunction; - 500?: ValidateFunction; + [K in HTTPStatusCodes]?: ValidateFunction; }; query?: ValidateFunction; body?: ValidateFunction; tags?: string[]; typed?: boolean; + license?: LicenseModule[]; } & Options; export type TypedThis = { userId: TOptions['authRequired'] extends true ? string : string | undefined; + user: TOptions['authRequired'] extends true ? IUser : IUser | null; token: TOptions['authRequired'] extends true ? string : string | undefined; queryParams: TOptions['query'] extends ValidateFunction ? Query : never; urlParams: UrlParams extends Record ? UrlParams : never; @@ -278,24 +292,27 @@ type PromiseOrValue = T | Promise; type InferResult = TResult extends ValidateFunction ? T : TResult; type Results = { - [K in keyof TResponse]: K extends 200 - ? SuccessResult> - : K extends 400 - ? FailureResult> - : K extends 401 - ? UnauthorizedResult> - : K extends 403 - ? ForbiddenResult> - : K extends 404 - ? NotFoundResult> - : K extends 500 - ? InternalError> - : never; + [K in keyof TResponse]: K extends SuccessStatusCodes + ? SuccessResult, K> + : K extends RedirectStatusCodes + ? RedirectResult, K> + : K extends 400 + ? FailureResult> + : K extends 401 + ? UnauthorizedResult> + : K extends 403 + ? ForbiddenResult> + : K extends 404 + ? NotFoundResult> + : K extends ErrorStatusCodes + ? InternalError, K> + : never; }[keyof TResponse] & { headers?: Record; }; -export type TypedAction = ( - this: TypedThis, - request: Request, -) => PromiseOrValue>; +export type TypedAction< + TOptions extends TypedOptions, + TPath extends string = '', + TThis extends TypedThis = TypedThis, +> = (this: TThis, request: Request) => PromiseOrValue>; diff --git a/apps/meteor/app/api/server/helpers/addUserToFileObj.ts b/apps/meteor/app/api/server/helpers/addUserToFileObj.ts index 4c46192b53224..1a9471e6d550d 100644 --- a/apps/meteor/app/api/server/helpers/addUserToFileObj.ts +++ b/apps/meteor/app/api/server/helpers/addUserToFileObj.ts @@ -1,8 +1,10 @@ import type { IUpload, IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; +const isString = (value: unknown): value is string => typeof value === 'string'; + export async function addUserToFileObj(files: IUpload[]): Promise<(IUpload & { user?: Pick })[]> { - const uids = files.map(({ userId }) => userId).filter(Boolean); + const uids = files.map(({ userId }) => userId).filter(isString); const users = await Users.findByIds(uids, { projection: { name: 1, username: 1 } }).toArray(); diff --git a/apps/meteor/app/api/server/helpers/getLoggedInUser.ts b/apps/meteor/app/api/server/helpers/getLoggedInUser.ts index 55c7c2d219557..d3fc562eeb20f 100644 --- a/apps/meteor/app/api/server/helpers/getLoggedInUser.ts +++ b/apps/meteor/app/api/server/helpers/getLoggedInUser.ts @@ -1,11 +1,10 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; -import type { Request } from 'express'; import { Accounts } from 'meteor/accounts-base'; export async function getLoggedInUser(request: Request): Promise | null> { - const token = request.headers['x-auth-token']; - const userId = request.headers['x-user-id']; + const token = request.headers.get('x-auth-token'); + const userId = request.headers.get('x-user-id'); if (!token || !userId || typeof token !== 'string' || typeof userId !== 'string') { return null; } diff --git a/apps/meteor/app/api/server/helpers/isWidget.ts b/apps/meteor/app/api/server/helpers/isWidget.ts index 49fbe84111d7b..258820ff7de5e 100644 --- a/apps/meteor/app/api/server/helpers/isWidget.ts +++ b/apps/meteor/app/api/server/helpers/isWidget.ts @@ -1,7 +1,7 @@ import { parse } from 'cookie'; -export const isWidget = (headers: Record = {}): boolean => { - const { rc_room_type: roomType, rc_is_widget: isWidget } = parse(headers.cookie || ''); +export const isWidget = (headers: Headers): boolean => { + const { rc_room_type: roomType, rc_is_widget: isWidget } = parse(headers.get('cookie') || ''); const isLivechatRoom = roomType && roomType === 'l'; return !!(isLivechatRoom && isWidget === 't'); diff --git a/apps/meteor/app/api/server/helpers/parseJsonQuery.ts b/apps/meteor/app/api/server/helpers/parseJsonQuery.ts index 16f370e15bd40..9879f1cb4f9bc 100644 --- a/apps/meteor/app/api/server/helpers/parseJsonQuery.ts +++ b/apps/meteor/app/api/server/helpers/parseJsonQuery.ts @@ -24,15 +24,7 @@ export async function parseJsonQuery(api: PartialThis): Promise<{ */ query: Record; }> { - const { - request: { path: route }, - userId, - queryParams: params, - logger, - queryFields, - queryOperations, - response, - } = api; + const { userId, queryParams: params, logger, queryFields, queryOperations, response, path } = api; let sort; if (params.sort) { @@ -60,7 +52,7 @@ export async function parseJsonQuery(api: PartialThis): Promise<{ let fields: Record | undefined; if (params.fields && isUnsafeQueryParamsAllowed) { try { - apiDeprecationLogger.parameter(route, 'fields', '8.0.0', response, messageGenerator); + apiDeprecationLogger.parameter(api.path, 'fields', '8.0.0', response, messageGenerator); fields = JSON.parse(params.fields) as Record; Object.entries(fields).forEach(([key, value]) => { if (value !== 1 && value !== 0) { @@ -80,7 +72,7 @@ export async function parseJsonQuery(api: PartialThis): Promise<{ // Verify the user's selected fields only contains ones which their role allows if (typeof fields === 'object') { let nonSelectableFields = Object.keys(API.v1.defaultFieldsToExclude); - if (route.includes('/v1/users.')) { + if (path.includes('/v1/users.')) { nonSelectableFields = nonSelectableFields.concat( Object.keys( (await hasPermissionAsync(userId, 'view-full-other-user-info')) @@ -99,7 +91,7 @@ export async function parseJsonQuery(api: PartialThis): Promise<{ // Limit the fields by default fields = Object.assign({}, fields, API.v1.defaultFieldsToExclude); - if (route.includes('/v1/users.')) { + if (path.includes('/v1/users.')) { if (await hasPermissionAsync(userId, 'view-full-other-user-info')) { fields = Object.assign(fields, API.v1.limitedUserFieldsToExcludeIfIsPrivilegedUser); } else { @@ -109,7 +101,7 @@ export async function parseJsonQuery(api: PartialThis): Promise<{ let query: Record = {}; if (params.query && isUnsafeQueryParamsAllowed) { - apiDeprecationLogger.parameter(route, 'query', '8.0.0', response, messageGenerator); + apiDeprecationLogger.parameter(api.path, 'query', '8.0.0', response, messageGenerator); try { query = ejson.parse(params.query); query = clean(query, pathAllowConf.def); @@ -125,7 +117,7 @@ export async function parseJsonQuery(api: PartialThis): Promise<{ if (typeof query === 'object') { let nonQueryableFields = Object.keys(API.v1.defaultFieldsToExclude); - if (route.includes('/v1/users.')) { + if (api.path.includes('/v1/users.')) { if (await hasPermissionAsync(userId, 'view-full-other-user-info')) { nonQueryableFields = nonQueryableFields.concat(Object.keys(API.v1.limitedUserFieldsToExcludeIfIsPrivilegedUser)); } else { diff --git a/apps/meteor/app/api/server/index.ts b/apps/meteor/app/api/server/index.ts index e4f6370bfb069..9efdb889fd0d7 100644 --- a/apps/meteor/app/api/server/index.ts +++ b/apps/meteor/app/api/server/index.ts @@ -48,7 +48,6 @@ import './v1/voip/omnichannel'; import './v1/voip'; import './v1/federation'; import './v1/moderation'; -import './v1/server-events'; // This has to come last so all endpoints are registered before generating the OpenAPI documentation import './default/openApi'; diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.spec.ts b/apps/meteor/app/api/server/lib/getUploadFormData.spec.ts index dc7afb77bd197..c9cced78f9591 100644 --- a/apps/meteor/app/api/server/lib/getUploadFormData.spec.ts +++ b/apps/meteor/app/api/server/lib/getUploadFormData.spec.ts @@ -1,7 +1,4 @@ -import { Readable } from 'stream'; - import { expect } from 'chai'; -import type { Request } from 'express'; import { getUploadFormData } from './getUploadFormData'; @@ -13,7 +10,7 @@ const createMockRequest = ( content: string | Buffer; mimetype?: string; }, -): Readable & { headers: Record } => { +): Request => { const boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW'; const parts: string[] = []; @@ -33,18 +30,31 @@ const createMockRequest = ( parts.push(`--${boundary}--`); - const mockRequest: any = new Readable({ - read() { - this.push(Buffer.from(parts.join('\r\n'))); - this.push(null); - }, - }); + const buffer = Buffer.from(parts.join('\r\n')); - mockRequest.headers = { - 'content-type': `multipart/form-data; boundary=${boundary}`, + const mockRequest: any = { + headers: { + entries: () => [['content-type', `multipart/form-data; boundary=${boundary}`]], + }, + blob: async () => ({ + stream: () => { + let hasRead = false; + return { + getReader: () => ({ + read: async () => { + if (!hasRead) { + hasRead = true; + return { value: buffer, done: false }; + } + return { done: true }; + }, + }), + }; + }, + }), }; - return mockRequest as Readable & { headers: Record }; + return mockRequest as Request & { headers: Record }; }; describe('getUploadFormData', () => { @@ -59,7 +69,7 @@ describe('getUploadFormData', () => { }, ); - const result = await getUploadFormData({ request: mockRequest as Request }, { field: 'fileField' }); + const result = await getUploadFormData({ request: mockRequest }, { field: 'fileField' }); expect(result).to.deep.include({ fieldname: 'fileField', @@ -86,7 +96,7 @@ describe('getUploadFormData', () => { }, ); - const result = await getUploadFormData({ request: mockRequest as Request }, { field: 'fileField' }); + const result = await getUploadFormData({ request: mockRequest }, { field: 'fileField' }); expect(result).to.deep.include({ fieldname: 'fileField', @@ -114,7 +124,7 @@ describe('getUploadFormData', () => { }, ); - const result = await getUploadFormData({ request: mockRequest as Request }, { fileOptional: true }); + const result = await getUploadFormData({ request: mockRequest }, { fileOptional: true }); expect(result).to.deep.include({ fieldname: 'fileField', @@ -131,7 +141,7 @@ describe('getUploadFormData', () => { const mockRequest = createMockRequest({ fieldName: 'fieldValue' }); try { - await getUploadFormData({ request: mockRequest as Request }, { fileOptional: false }); + await getUploadFormData({ request: mockRequest }, { fileOptional: false }); throw new Error('Expected function to throw'); } catch (error) { expect((error as Error).message).to.equal('[No file uploaded]'); @@ -141,7 +151,7 @@ describe('getUploadFormData', () => { it('should return fields without errors when no file is uploaded but fileOptional is true', async () => { const mockRequest = createMockRequest({ fieldName: 'fieldValue' }); // No file - const result = await getUploadFormData({ request: mockRequest as Request }, { fileOptional: true }); + const result = await getUploadFormData({ request: mockRequest }, { fileOptional: true }); expect(result).to.deep.equal({ fields: { fieldName: 'fieldValue' }, @@ -167,7 +177,7 @@ describe('getUploadFormData', () => { try { await getUploadFormData( - { request: mockRequest as Request }, + { request: mockRequest }, { sizeLimit: 1024 * 1024 }, // 1 MB limit ); throw new Error('Expected function to throw'); diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.ts b/apps/meteor/app/api/server/lib/getUploadFormData.ts index 93ceafdde92f8..bf9b792e9b08e 100644 --- a/apps/meteor/app/api/server/lib/getUploadFormData.ts +++ b/apps/meteor/app/api/server/lib/getUploadFormData.ts @@ -1,9 +1,8 @@ -import type { Readable } from 'stream'; +import { Readable } from 'stream'; import { MeteorError } from '@rocket.chat/core-services'; import type { ValidateFunction } from 'ajv'; import busboy from 'busboy'; -import type { Request } from 'express'; import { getMimeType } from '../../../utils/lib/mimeTypes'; @@ -71,7 +70,7 @@ export async function getUploadFormData< ...(options.sizeLimit && options.sizeLimit > -1 && { fileSize: options.sizeLimit }), }; - const bb = busboy({ headers: request.headers, defParamCharset: 'utf8', limits }); + const bb = busboy({ headers: Object.fromEntries(request.headers.entries()), defParamCharset: 'utf8', limits }); const fields = Object.create(null) as K; let uploadedFile: UploadResultWithOptionalFile | undefined = { @@ -142,8 +141,6 @@ export async function getUploadFormData< } function cleanup() { - request.unpipe(bb); - request.on('readable', request.read.bind(request)); bb.removeAllListeners(); } @@ -167,7 +164,29 @@ export async function getUploadFormData< returnError(); }); - request.pipe(bb); + const webReadableStream = await request.blob().then((blob) => blob.stream()); + + const nodeReadableStream = new Readable({ + async read() { + const reader = webReadableStream.getReader(); + try { + const processChunk = async () => { + const { done, value } = await reader.read(); + if (done) { + this.push(null); + return; + } + this.push(Buffer.from(value)); + await processChunk(); + }; + await processChunk(); + } catch (err: any) { + this.destroy(err); + } + }, + }); + + nodeReadableStream.pipe(bb); return new Promise>((resolve, reject) => { returnResult = resolve; diff --git a/apps/meteor/app/api/server/lib/users.ts b/apps/meteor/app/api/server/lib/users.ts index aa99d92166678..c95338d8a4ae3 100644 --- a/apps/meteor/app/api/server/lib/users.ts +++ b/apps/meteor/app/api/server/lib/users.ts @@ -2,7 +2,7 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Users, Subscriptions } from '@rocket.chat/models'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import type { Mongo } from 'meteor/mongo'; -import type { Filter, RootFilterOperators } from 'mongodb'; +import type { Filter, FindOptions, RootFilterOperators } from 'mongodb'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { settings } from '../../../settings/server'; @@ -21,7 +21,7 @@ export async function findUsersToAutocomplete({ const searchFields = settings.get('Accounts_SearchFields').trim().split(','); const exceptions = selector.exceptions || []; const conditions = selector.conditions || {}; - const options = { + const options: FindOptions & { limit: number } = { projection: { name: 1, username: 1, diff --git a/apps/meteor/app/api/server/middlewares/cors.ts b/apps/meteor/app/api/server/middlewares/cors.ts index db6dde775918a..44f7e39acafc0 100644 --- a/apps/meteor/app/api/server/middlewares/cors.ts +++ b/apps/meteor/app/api/server/middlewares/cors.ts @@ -1,4 +1,4 @@ -import type { NextFunction, Request, Response } from 'express'; +import type { MiddlewareHandler } from 'hono'; import type { CachedSettings } from '../../../settings/server/CachedSettings'; @@ -7,55 +7,55 @@ const defaultHeaders = { 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, X-User-Id, X-Auth-Token, x-visitor-token, Authorization', }; -export const cors = (settings: CachedSettings) => (req: Request, res: Response, next: NextFunction) => { - if (req.method !== 'OPTIONS') { - if (settings.get('API_Enable_CORS')) { - res.setHeader('Vary', 'Origin'); - res.setHeader('Access-Control-Allow-Methods', defaultHeaders['Access-Control-Allow-Methods']); - res.setHeader('Access-Control-Allow-Headers', defaultHeaders['Access-Control-Allow-Headers']); +export const cors = + (settings: CachedSettings): MiddlewareHandler => + async (c, next) => { + const { req, res } = c; + if (req.method !== 'OPTIONS') { + if (settings.get('API_Enable_CORS')) { + res.headers.set('Vary', 'Origin'); + res.headers.set('Access-Control-Allow-Methods', defaultHeaders['Access-Control-Allow-Methods']); + res.headers.set('Access-Control-Allow-Headers', defaultHeaders['Access-Control-Allow-Headers']); + } + + await next(); + return; } - next(); - return; - } - - // check if a pre-flight request - if (!req.headers['access-control-request-method'] && !req.headers.origin) { - next(); - return; - } - - if (!settings.get('API_Enable_CORS')) { - res.writeHead(405); - res.write('CORS not enabled. Go to "Admin > General > REST Api" to enable it.'); - res.end(); - return; - } - - const CORSOriginSetting = String(settings.get('API_CORS_Origin')); - - if (CORSOriginSetting === '*') { - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', defaultHeaders['Access-Control-Allow-Methods']); - res.setHeader('Access-Control-Allow-Headers', defaultHeaders['Access-Control-Allow-Headers']); - next(); - return; - } - - const origins = CORSOriginSetting.trim() - .split(',') - .map((origin) => String(origin).trim().toLocaleLowerCase()); - - // if invalid origin reply without required CORS headers - if (!req.headers.origin || !origins.includes(req.headers.origin)) { - res.writeHead(403, 'Forbidden'); - res.end(); - return; - } - - res.setHeader('Vary', 'Origin'); - res.setHeader('Access-Control-Allow-Origin', req.headers.origin); - res.setHeader('Access-Control-Allow-Methods', defaultHeaders['Access-Control-Allow-Methods']); - res.setHeader('Access-Control-Allow-Headers', defaultHeaders['Access-Control-Allow-Headers']); - next(); -}; + // check if a pre-flight request + if (!req.header('access-control-request-method') && !req.header('origin')) { + await next(); + return; + } + + if (!settings.get('API_Enable_CORS')) { + return c.body('CORS not enabled. Go to "Admin > General > REST Api" to enable it.', 405); + } + + const CORSOriginSetting = String(settings.get('API_CORS_Origin')); + + if (CORSOriginSetting === '*') { + res.headers.set('Access-Control-Allow-Origin', '*'); + res.headers.set('Access-Control-Allow-Methods', defaultHeaders['Access-Control-Allow-Methods']); + res.headers.set('Access-Control-Allow-Headers', defaultHeaders['Access-Control-Allow-Headers']); + await next(); + return; + } + + const origins = CORSOriginSetting.trim() + .split(',') + .map((origin) => String(origin).trim().toLocaleLowerCase()); + + const originHeader = req.header('origin'); + + // if invalid origin reply without required CORS headers + if (!originHeader || !origins.includes(originHeader)) { + return c.body('Invalid origin', 403); + } + + res.headers.set('Vary', 'Origin'); + res.headers.set('Access-Control-Allow-Origin', originHeader); + res.headers.set('Access-Control-Allow-Methods', defaultHeaders['Access-Control-Allow-Methods']); + res.headers.set('Access-Control-Allow-Headers', defaultHeaders['Access-Control-Allow-Headers']); + await next(); + }; diff --git a/apps/meteor/app/api/server/middlewares/honoAdapter.ts b/apps/meteor/app/api/server/middlewares/honoAdapter.ts new file mode 100644 index 0000000000000..86d9e83cb3a1b --- /dev/null +++ b/apps/meteor/app/api/server/middlewares/honoAdapter.ts @@ -0,0 +1,31 @@ +import { Readable } from 'stream'; + +import type { Request, Response } from 'express'; +import type { Hono } from 'hono'; + +export const honoAdapter = (hono: Hono) => async (expressReq: Request, res: Response) => { + (expressReq as unknown as any).duplex = 'half'; + + if (Readable.isDisturbed(expressReq)) { + return; + } + + const { body, ...req } = expressReq; + + const honoRes = await hono.request( + expressReq.originalUrl, + { + ...req, + ...(['POST', 'PUT', 'DELETE'].includes(expressReq.method) && { body: expressReq as unknown as ReadableStream }), + headers: new Headers(Object.fromEntries(Object.entries(expressReq.headers)) as Record), + }, + { + incoming: expressReq, + }, + ); + res.status(honoRes.status); + honoRes.headers.forEach((value, key) => res.setHeader(key, value)); + // Converting it to a Buffer because res.send appends always a charset to the Content-Type + // https://github.com/expressjs/express/issues/2238 + res.send(Buffer.from(await honoRes.text())); +}; diff --git a/apps/meteor/app/api/server/middlewares/logger.ts b/apps/meteor/app/api/server/middlewares/logger.ts index 5233435a19a41..a9a733de86c4c 100644 --- a/apps/meteor/app/api/server/middlewares/logger.ts +++ b/apps/meteor/app/api/server/middlewares/logger.ts @@ -1,28 +1,36 @@ import type { Logger } from '@rocket.chat/logger'; -import type { Request, Response, NextFunction } from 'express'; +import type { MiddlewareHandler } from 'hono'; import { getRestPayload } from '../../../../server/lib/logger/logPayloads'; -export const loggerMiddleware = (logger: Logger) => async (req: Request, res: Response, next: NextFunction) => { - const startTime = Date.now(); +export const loggerMiddleware = + (logger: Logger): MiddlewareHandler => + async (c, next) => { + const startTime = Date.now(); + + let payload = {}; + + try { + payload = await c.req.raw.clone().json(); + // eslint-disable-next-line no-empty + } catch {} + + const log = logger.logger.child({ + method: c.req.method, + url: c.req.url, + userId: c.req.header('x-user-id'), + userAgent: c.req.header('user-agent'), + length: c.req.header('content-length'), + host: c.req.header('host'), + referer: c.req.header('referer'), + remoteIP: c.get('remoteAddress'), + ...(['POST', 'PUT', 'PATCH', 'DELETE'].includes(c.req.method) && getRestPayload(payload)), + }); + + await next(); - const log = logger.logger.child({ - method: req.method, - url: req.url, - userId: req.headers['x-user-id'], - userAgent: req.headers['user-agent'], - length: req.headers['content-length'], - host: req.headers.host, - referer: req.headers.referer, - remoteIP: req.ip, - ...getRestPayload(req.body), - }); - res.once('finish', () => { log.http({ - status: res.statusCode, + status: c.res.status, responseTime: Date.now() - startTime, }); - }); - - next(); -}; + }; diff --git a/apps/meteor/app/api/server/middlewares/metrics.ts b/apps/meteor/app/api/server/middlewares/metrics.ts index 9206a51375001..518febc7132a4 100644 --- a/apps/meteor/app/api/server/middlewares/metrics.ts +++ b/apps/meteor/app/api/server/middlewares/metrics.ts @@ -1,24 +1,23 @@ -import type { Request, Response, NextFunction } from 'express'; +import type { MiddlewareHandler } from 'hono'; import type { Summary } from 'prom-client'; import type { CachedSettings } from '../../../settings/server/CachedSettings'; import type { APIClass } from '../api'; export const metricsMiddleware = - (api: APIClass, settings: CachedSettings, summary: Summary) => async (req: Request, res: Response, next: NextFunction) => { - const { method, path } = req; + (api: APIClass, settings: CachedSettings, summary: Summary): MiddlewareHandler => + async (c, next) => { + const { method, path } = c.req; const rocketchatRestApiEnd = summary.startTimer({ method, version: api.version, - ...(settings.get('Prometheus_API_User_Agent') && { user_agent: req.headers['user-agent'] }), - entrypoint: path.startsWith('method.call') ? decodeURIComponent(req.url.slice(8)) : path, + ...(settings.get('Prometheus_API_User_Agent') && { user_agent: c.req.header('user-agent') }), + entrypoint: path.startsWith('method.call') ? decodeURIComponent(c.req.url.slice(8)) : path, }); - res.once('finish', () => { - rocketchatRestApiEnd({ - status: res.statusCode, - }); + await next(); + rocketchatRestApiEnd({ + status: c.res.status, }); - next(); }; diff --git a/apps/meteor/app/api/server/middlewares/remoteAddressMiddleware.ts b/apps/meteor/app/api/server/middlewares/remoteAddressMiddleware.ts new file mode 100644 index 0000000000000..6e129e88f467a --- /dev/null +++ b/apps/meteor/app/api/server/middlewares/remoteAddressMiddleware.ts @@ -0,0 +1,40 @@ +import type { IncomingMessage } from 'http'; + +import type { Context, MiddlewareHandler } from 'hono'; + +type HttpBindings = { + incoming: IncomingMessage; +}; + +const getRemoteAddress = (c: Context) => { + const bindings = (c.env?.server ? c.env.server : c.env) as HttpBindings; + + const forwardedFor = c.req.header('x-forwarded-for'); + const socket = bindings.incoming.socket.remoteAddress || bindings.incoming.connection.remoteAddress; + const remoteAddress = c.req.header('x-real-ip') || socket; + + if (!socket) { + return remoteAddress || forwardedFor; + } + + const httpForwardedCount = parseInt(String(process.env.HTTP_FORWARDED_COUNT)) || 0; + if (httpForwardedCount <= 0) { + return remoteAddress; + } + + if (!forwardedFor || typeof forwardedFor.valueOf() !== 'string') { + return remoteAddress; + } + + const forwardedForIPs = forwardedFor.trim().split(/\s*,\s*/); + if (httpForwardedCount > forwardedForIPs.length) { + return remoteAddress; + } + return forwardedForIPs[forwardedForIPs.length - httpForwardedCount]; +}; + +export const remoteAddressMiddleware: MiddlewareHandler = async function (c, next) { + const remoteAddress = getRemoteAddress(c); + c.set('remoteAddress', remoteAddress); + return next(); +}; diff --git a/apps/meteor/app/api/server/middlewares/tracer.ts b/apps/meteor/app/api/server/middlewares/tracer.ts index e229672598aa1..bc3f03778cda7 100644 --- a/apps/meteor/app/api/server/middlewares/tracer.ts +++ b/apps/meteor/app/api/server/middlewares/tracer.ts @@ -1,32 +1,25 @@ import { tracerSpan } from '@rocket.chat/tracing'; -import type { Request, Response, NextFunction } from 'express'; +import type { MiddlewareHandler } from 'hono'; -export const tracerSpanMiddleware = async (req: Request, res: Response, next: NextFunction) => { - try { - await tracerSpan( - `${req.method} ${req.url}`, - { - attributes: { - url: req.url, - route: req.route?.path, - method: req.method, - userId: req.userId, // Assuming userId is attached to the request object - }, +export const tracerSpanMiddleware: MiddlewareHandler = async (c, next) => { + return tracerSpan( + `${c.req.method} ${c.req.url}`, + { + attributes: { + url: c.req.url, + // route: c.req.route?.path, + method: c.req.method, + userId: (c.req.raw.clone() as any).userId, // Assuming userId is attached to the request object }, - async (span) => { - if (span) { - res.setHeader('X-Trace-Id', span.spanContext().traceId); - } + }, + async (span) => { + if (span) { + c.header('X-Trace-Id', span.spanContext().traceId); + } - next(); + await next(); - await new Promise((resolve) => { - res.once('finish', resolve); - }); - span?.setAttribute('status', res.statusCode); - }, - ); - } catch (error) { - next(error); - } + span?.setAttribute('status', c.res.status); + }, + ); }; diff --git a/apps/meteor/app/api/server/router.spec.ts b/apps/meteor/app/api/server/router.spec.ts index 36997fc23d604..5c11ef6e0456f 100644 --- a/apps/meteor/app/api/server/router.spec.ts +++ b/apps/meteor/app/api/server/router.spec.ts @@ -9,7 +9,10 @@ describe('Router use method', () => { const ajv = new Ajv(); const app = express(); const api = new Router('/api'); - const v1 = new Router('/v1'); + const v1 = new Router('/v1').use(async (x, next) => { + x.header('x-api-version', 'v1'); + await next(); + }); const v2 = new Router('/v2'); const test = new Router('/test').get( '/', @@ -33,27 +36,56 @@ describe('Router use method', () => { }, ); - app.use( - api - .use( - v1 - .use((req, _res, next) => { - (req as any).customProperty = 'customValue'; - next(); - }) - .use(test), - ) - .use(v2.use(test)).router, - ); + app.use(api.use(v1.use(test)).use(v2.use(test)).router); const response1 = await request(app).get('/api/v1/test'); expect(response1.statusCode).toBe(200); - expect(response1.body).toHaveProperty('customProperty', 'customValue'); + expect(response1.headers).toHaveProperty('x-api-version', 'v1'); const response2 = await request(app).get('/api/v2/test'); expect(response2.statusCode).toBe(200); - expect(response2.body).not.toHaveProperty('customProperty', 'customValue'); + expect(response2.headers).not.toHaveProperty('x-api-version'); + }); + + it('should parse nested query params into object for GET requests', async () => { + const ajv = new Ajv(); + const app = express(); + + const isTestQueryParams = ajv.compile({ + type: 'object', + properties: { + outerProperty: { type: 'object', properties: { innerProperty: { type: 'string' } } }, + }, + additionalProperties: false, + }); + + const api = new Router('/api').get( + '/test', + { + response: { + 200: isTestQueryParams, + }, + query: isTestQueryParams, + }, + async function action() { + const { outerProperty } = this.queryParams as any; + return { + statusCode: 200, + body: { + outerProperty, + }, + }; + }, + ); + + app.use(api.router); + + const response1 = await request(app).get('/api/test?outerProperty[innerProperty]=test'); + + expect(response1.statusCode).toBe(200); + expect(response1.body).toHaveProperty('outerProperty'); + expect(response1.body.outerProperty).toHaveProperty('innerProperty', 'test'); }); }); diff --git a/apps/meteor/app/api/server/router.ts b/apps/meteor/app/api/server/router.ts index 6da042bb02ba9..c18598f03c8b5 100644 --- a/apps/meteor/app/api/server/router.ts +++ b/apps/meteor/app/api/server/router.ts @@ -1,8 +1,24 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import type { Method } from '@rocket.chat/rest-typings'; import type { AnySchema } from 'ajv'; import express from 'express'; +import type { HonoRequest, MiddlewareHandler } from 'hono'; +import { Hono } from 'hono'; +import qs from 'qs'; // Using qs specifically to keep express compatibility import type { TypedAction, TypedOptions } from './definition'; +import { honoAdapter } from './middlewares/honoAdapter'; + +type MiddlewareHandlerListAndActionHandler = [ + ...MiddlewareHandler[], + TypedAction, +]; + +function splitArray(arr: [...T[], U]): [T[], U] { + const last = arr[arr.length - 1]; + const rest = arr.slice(0, -1) as T[]; + return [rest, last as U]; +} export type Route = { responses: Record< @@ -36,6 +52,18 @@ export type Route = { }[]; tags?: string[]; }; +declare module 'hono' { + interface ContextVariableMap { + 'route': string; + 'bodyParams-override'?: Record; + } +} + +declare global { + interface Request { + route: string; + } +} export class Router< TBasePath extends string, @@ -43,19 +71,17 @@ export class Router< [x: string]: unknown; } = NonNullable, > { - public router; - - private innerRouter: express.Router; + protected innerRouter: Hono<{ + Variables: { + remoteAddress: string; + }; + }>; constructor(readonly base: TBasePath) { - // eslint-disable-next-line new-cap - this.router = express.Router(); - // eslint-disable-next-line new-cap - this.innerRouter = express.Router(); - this.router.use(this.base, this.innerRouter); + this.innerRouter = new Hono(); } - private typedRoutes: Record> = {}; + public typedRoutes: Record> = {}; private registerTypedRoutes< TSubPathPattern extends string, @@ -111,11 +137,47 @@ export class Router< }; } + private async parseBodyParams(request: HonoRequest, overrideBodyParams: Record = {}) { + if (Object.keys(overrideBodyParams).length !== 0) { + return overrideBodyParams; + } + + try { + let parsedBody = {}; + const contentType = request.header('content-type'); + + if (contentType?.includes('application/json')) { + parsedBody = await request.raw.clone().json(); + } else if (contentType?.includes('multipart/form-data')) { + parsedBody = await request.raw.clone().formData(); + } else { + parsedBody = await request.raw.clone().text(); + } + // This is necessary to keep the compatibility with the previous version, otherwise the bodyParams will be an empty string when no content-type is sent + if (parsedBody === '') { + return {}; + } + + if (Array.isArray(parsedBody)) { + return parsedBody; + } + + return { ...parsedBody }; + // eslint-disable-next-line no-empty + } catch {} + + return {}; + } + + private parseQueryParams(request: HonoRequest) { + return qs.parse(request.raw.url.split('?')?.[1] || ''); + } + private method( method: Method, subpath: TSubPathPattern, options: TOptions, - action: TypedAction, + ...actions: MiddlewareHandlerListAndActionHandler ): Router< TBasePath, | TOperations @@ -124,26 +186,41 @@ export class Router< path: TPathPattern; } & Omit) > { - this.innerRouter[method.toLowerCase() as Lowercase](`/${subpath}`.replace('//', '/'), async (req, res) => { + const [middlewares, action] = splitArray(actions); + + this.innerRouter[method.toLowerCase() as Lowercase](`/${subpath}`.replace('//', '/'), ...middlewares, async (c) => { + const { req, res } = c; + req.raw.route = `${c.var.route ?? ''}${subpath}`; + + const queryParams = this.parseQueryParams(req); + if (options.query) { const validatorFn = options.query; - if (typeof options.query === 'function' && !validatorFn(req.query)) { - return res.status(400).json({ - success: false, - errorType: 'error-invalid-params', - error: validatorFn.errors?.map((error: any) => error.message).join('\n '), - }); + if (typeof options.query === 'function' && !validatorFn(queryParams)) { + return c.json( + { + success: false, + errorType: 'error-invalid-params', + error: validatorFn.errors?.map((error: any) => error.message).join('\n '), + }, + 400, + ); } } + const bodyParams = await this.parseBodyParams(req, c.var['bodyParams-override']); + if (options.body) { const validatorFn = options.body; - if (typeof options.body === 'function' && !validatorFn((req as any).bodyParams || req.body)) { - return res.status(400).json({ - success: false, - errorType: 'error-invalid-params', - error: validatorFn.errors?.map((error: any) => error.message).join('\n '), - }); + if (typeof options.body === 'function' && !validatorFn((req as any).bodyParams || bodyParams)) { + return c.json( + { + success: false, + errorType: 'error-invalid-params', + error: validatorFn.errors?.map((error: any) => error.message).join('\n '), + }, + 400, + ); } } @@ -153,13 +230,15 @@ export class Router< headers = {}, } = await action.apply( { - urlParams: req.params, - queryParams: req.query, - bodyParams: (req as any).bodyParams || req.body, - request: req, + requestIp: c.get('remoteAddress'), + urlParams: req.param(), + queryParams, + bodyParams, + request: req.raw.clone(), + path: req.path, response: res, } as any, - [req], + [req.raw.clone()], ); if (process.env.NODE_ENV === 'test' || process.env.TEST_MODE) { const responseValidatorFn = options?.response?.[statusCode]; @@ -175,7 +254,7 @@ export class Router< const responseHeaders = Object.fromEntries( Object.entries({ - ...res.header, + ...res.headers, 'Content-Type': 'application/json', 'Cache-Control': 'no-store', 'Pragma': 'no-cache', @@ -183,15 +262,21 @@ export class Router< }).map(([key, value]) => [key.toLowerCase(), value]), ); - res.writeHead(statusCode, responseHeaders); + const contentType = (responseHeaders['content-type'] || 'application/json') as string; - if (responseHeaders['content-type']?.match(/json|javascript/) !== null) { - body !== undefined && res.write(JSON.stringify(body)); - } else { - body !== undefined && res.write(body); + const isContentLess = (statusCode: number): statusCode is 101 | 204 | 205 | 304 => { + return [101, 204, 205, 304].includes(statusCode); + }; + + if (isContentLess(statusCode)) { + return c.status(statusCode); } - res.end(); + return c.body( + (contentType?.match(/json|javascript/) ? JSON.stringify(body) : body) as any, + statusCode, + responseHeaders as Record, + ); }); this.registerTypedRoutes(method, subpath, options); return this; @@ -200,7 +285,7 @@ export class Router< get( subpath: TSubPathPattern, options: TOptions, - action: TypedAction, + ...action: MiddlewareHandlerListAndActionHandler ): Router< TBasePath, | TOperations @@ -209,13 +294,13 @@ export class Router< path: TPathPattern; } & Omit) > { - return this.method('GET', subpath, options, action); + return this.method('GET', subpath, options, ...action); } post( subpath: TSubPathPattern, options: TOptions, - action: TypedAction, + ...action: MiddlewareHandlerListAndActionHandler ): Router< TBasePath, | TOperations @@ -224,13 +309,13 @@ export class Router< path: TPathPattern; } & Omit) > { - return this.method('POST', subpath, options, action); + return this.method('POST', subpath, options, ...action); } put( subpath: TSubPathPattern, options: TOptions, - action: TypedAction, + ...action: MiddlewareHandlerListAndActionHandler ): Router< TBasePath, | TOperations @@ -239,13 +324,13 @@ export class Router< path: TPathPattern; } & Omit) > { - return this.method('PUT', subpath, options, action); + return this.method('PUT', subpath, options, ...action); } delete( subpath: TSubPathPattern, options: TOptions, - action: TypedAction, + ...action: MiddlewareHandlerListAndActionHandler ): Router< TBasePath, | TOperations @@ -254,29 +339,50 @@ export class Router< path: TPathPattern; } & Omit) > { - return this.method('DELETE', subpath, options, action); + return this.method('DELETE', subpath, options, ...action); } - use void>(fn: FN): Router; + use(fn: FN): Router; use>( innerRouter: IRouter, ): IRouter extends Router ? Router> : never; - use(innerRouter: any): any { + use(innerRouter: unknown): any { if (innerRouter instanceof Router) { this.typedRoutes = { ...this.typedRoutes, ...Object.fromEntries(Object.entries(innerRouter.typedRoutes).map(([path, routes]) => [`${this.base}${path}`, routes])), }; - this.innerRouter.use(innerRouter.router); + this.innerRouter.route(innerRouter.base, innerRouter.innerRouter); } if (typeof innerRouter === 'function') { - this.innerRouter.use(innerRouter); + this.innerRouter.use(innerRouter as any); } return this as any; } + + get router(): express.Router { + // eslint-disable-next-line new-cap + const router = express.Router(); + const hono = new Hono(); + router.use( + this.base, + honoAdapter( + hono + .use(`${this.base}/*`, (c, next) => { + c.set('route', `${c.var.route || ''}${this.base}`); + return next(); + }) + .route(this.base, this.innerRouter) + .options('*', (c) => { + return c.body('OK'); + }), + ), + ); + return router; + } } type Prettify = { diff --git a/apps/meteor/app/api/server/v1/assets.ts b/apps/meteor/app/api/server/v1/assets.ts index fd9f31d40923a..2843cf8627d51 100644 --- a/apps/meteor/app/api/server/v1/assets.ts +++ b/apps/meteor/app/api/server/v1/assets.ts @@ -41,7 +41,7 @@ API.v1.addRoute( _id: this.userId, username: this.user.username!, ip: this.requestIp, - useragent: this.request.headers['user-agent'] || '', + useragent: this.request.headers.get('user-agent') || '', })(Settings.updateValueById, key, value); if (modifiedCount) { @@ -78,7 +78,7 @@ API.v1.addRoute( _id: this.userId, username: this.user.username!, ip: this.requestIp, - useragent: this.request.headers['user-agent'] || '', + useragent: this.request.headers.get('user-agent') || '', })(Settings.updateValueById, key, value); if (modifiedCount) { diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index 24d0bbac7d033..7824ac0c2070e 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -1,5 +1,5 @@ import { Team, Room } from '@rocket.chat/core-services'; -import { TEAM_TYPE, type IRoom, type ISubscription, type IUser, type RoomType } from '@rocket.chat/core-typings'; +import { TEAM_TYPE, 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, @@ -1013,7 +1013,7 @@ API.v1.addRoute( }, ]; - const { cursor, totalCount } = await Rooms.findPaginated(ourQuery, { + const { cursor, totalCount } = Rooms.findPaginated(ourQuery, { sort: sort || { name: 1 }, skip: offset, limit: count, @@ -1052,7 +1052,7 @@ API.v1.addRoute( }); } - const { cursor, totalCount } = await Rooms.findPaginatedByTypeAndIds('c', rids, { + const { cursor, totalCount } = Rooms.findPaginatedByTypeAndIds('c', rids, { sort: sort || { name: 1 }, skip: offset, limit: count, @@ -1103,7 +1103,7 @@ API.v1.addRoute( const { cursor, totalCount } = await findUsersOfRoom({ rid: findResult._id, - ...(status && { status: { $in: status } }), + ...(status && { status: { $in: status as UserStatus[] } }), skip, limit, filter, diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 7569f321fa203..39b1109a948f6 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -375,7 +375,7 @@ API.v1.addRoute( _id: msg._id, msg: msgFromBody, rid: msg.rid, - customFields: this.bodyParams.customFields as Record | undefined, + ...(this.bodyParams.customFields && { customFields: this.bodyParams.customFields }), }, this.bodyParams.previewUrls, ), diff --git a/apps/meteor/app/api/server/v1/custom-user-status.ts b/apps/meteor/app/api/server/v1/custom-user-status.ts index 4f17c0db1ae89..037928cf1cdcf 100644 --- a/apps/meteor/app/api/server/v1/custom-user-status.ts +++ b/apps/meteor/app/api/server/v1/custom-user-status.ts @@ -4,6 +4,8 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { deleteCustomUserStatus } from '../../../user-status/server/methods/deleteCustomUserStatus'; +import { insertOrUpdateUserStatus } from '../../../user-status/server/methods/insertOrUpdateUserStatus'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; @@ -53,10 +55,10 @@ API.v1.addRoute( const userStatusData = { name: this.bodyParams.name, - statusType: this.bodyParams.statusType, + statusType: this.bodyParams.statusType || '', }; - await Meteor.callAsync('insertOrUpdateUserStatus', userStatusData); + await insertOrUpdateUserStatus(this.userId, userStatusData); const customUserStatus = await CustomUserStatus.findOneByName(userStatusData.name); if (!customUserStatus) { @@ -80,7 +82,7 @@ API.v1.addRoute( return API.v1.failure('The "customUserStatusId" params is required!'); } - await Meteor.callAsync('deleteCustomUserStatus', customUserStatusId); + await deleteCustomUserStatus(this.userId, customUserStatusId); return API.v1.success(); }, @@ -111,7 +113,7 @@ API.v1.addRoute( return API.v1.failure(`No custom user status found with the id of "${userStatusData._id}".`); } - await Meteor.callAsync('insertOrUpdateUserStatus', userStatusData); + await insertOrUpdateUserStatus(this.userId, userStatusData); const customUserStatus = await CustomUserStatus.findOneById(userStatusData._id); diff --git a/apps/meteor/app/api/server/v1/e2e.ts b/apps/meteor/app/api/server/v1/e2e.ts index 09e6517e4e8d0..a8a5e9175d876 100644 --- a/apps/meteor/app/api/server/v1/e2e.ts +++ b/apps/meteor/app/api/server/v1/e2e.ts @@ -1,5 +1,4 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { Subscriptions } from '@rocket.chat/models'; +import { Subscriptions, Users } from '@rocket.chat/models'; import { ise2eGetUsersOfRoomWithoutKeyParamsGET, ise2eSetRoomKeyIDParamsPOST, @@ -10,13 +9,16 @@ import { isE2EResetRoomKeyProps, } from '@rocket.chat/rest-typings'; import ExpiryMap from 'expiry-map'; -import { Meteor } from 'meteor/meteor'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { handleSuggestedGroupKey } from '../../../e2e/server/functions/handleSuggestedGroupKey'; import { provideUsersSuggestedGroupKeys } from '../../../e2e/server/functions/provideUsersSuggestedGroupKeys'; import { resetRoomKey } from '../../../e2e/server/functions/resetRoomKey'; +import { getUsersOfRoomWithoutKeyMethod } from '../../../e2e/server/methods/getUsersOfRoomWithoutKey'; +import { setRoomKeyIDMethod } from '../../../e2e/server/methods/setRoomKeyID'; +import { setUserPublicAndPrivateKeysMethod } from '../../../e2e/server/methods/setUserPublicAndPrivateKeys'; +import { updateGroupKey } from '../../../e2e/server/methods/updateGroupKey'; import { settings } from '../../../settings/server'; import { API } from '../api'; @@ -31,10 +33,7 @@ API.v1.addRoute( }, { async get() { - const result: { - public_key: string; - private_key: string; - } = await Meteor.callAsync('e2e.fetchMyKeys'); + const result = await Users.fetchKeysByUserId(this.userId); return API.v1.success(result); }, @@ -51,9 +50,7 @@ API.v1.addRoute( async get() { const { rid } = this.queryParams; - const result: { - users: IUser[]; - } = await Meteor.callAsync('e2e.getUsersOfRoomWithoutKey', rid); + const result = await getUsersOfRoomWithoutKeyMethod(this.userId, rid); return API.v1.success(result); }, @@ -102,7 +99,7 @@ API.v1.addRoute( async post() { const { rid, keyID } = this.bodyParams; - await Meteor.callAsync('e2e.setRoomKeyID', rid, keyID); + await setRoomKeyIDMethod(this.userId, rid, keyID); return API.v1.success(); }, @@ -153,7 +150,7 @@ API.v1.addRoute( // eslint-disable-next-line @typescript-eslint/naming-convention const { public_key, private_key, force } = this.bodyParams; - await Meteor.callAsync('e2e.setUserPublicAndPrivateKeys', { + await setUserPublicAndPrivateKeysMethod(this.userId, { public_key, private_key, force, @@ -210,7 +207,7 @@ API.v1.addRoute( async post() { const { uid, rid, key } = this.bodyParams; - await Meteor.callAsync('e2e.updateGroupKey', rid, uid, key); + await updateGroupKey(rid, uid, key, this.userId); return API.v1.success(); }, diff --git a/apps/meteor/app/api/server/v1/emoji-custom.ts b/apps/meteor/app/api/server/v1/emoji-custom.ts index 5b255b6ef0c18..a2f886afb1c11 100644 --- a/apps/meteor/app/api/server/v1/emoji-custom.ts +++ b/apps/meteor/app/api/server/v1/emoji-custom.ts @@ -8,6 +8,7 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; import type { EmojiData } from '../../../emoji-custom/server/lib/insertOrUpdateEmoji'; import { insertOrUpdateEmoji } from '../../../emoji-custom/server/lib/insertOrUpdateEmoji'; import { uploadEmojiCustomWithBuffer } from '../../../emoji-custom/server/lib/uploadEmojiCustom'; +import { deleteEmojiCustom } from '../../../emoji-custom/server/methods/deleteEmojiCustom'; import { settings } from '../../../settings/server'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; @@ -203,7 +204,7 @@ API.v1.addRoute( return API.v1.failure('The "emojiId" params is required!'); } - await Meteor.callAsync('deleteEmojiCustom', emojiId); + await deleteEmojiCustom(this.userId, emojiId); return API.v1.success(); }, diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index d2dcc6f34f466..ea2b3ef840ba5 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -1,5 +1,5 @@ import { Team, isMeteorError } from '@rocket.chat/core-services'; -import type { IIntegration, IUser, IRoom, RoomType } from '@rocket.chat/core-typings'; +import type { IIntegration, IUser, IRoom, RoomType, UserStatus } from '@rocket.chat/core-typings'; import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models'; import { isGroupsOnlineProps, isGroupsMessagesProps } from '@rocket.chat/rest-typings'; import { check, Match } from 'meteor/check'; @@ -740,7 +740,7 @@ API.v1.addRoute( const { cursor, totalCount } = await findUsersOfRoom({ rid: findResult.rid, - ...(status && { status: { $in: status } }), + ...(status && { status: { $in: status as UserStatus[] } }), skip, limit, filter, diff --git a/apps/meteor/app/api/server/v1/im.ts b/apps/meteor/app/api/server/v1/im.ts index f345bad4118c2..5f196263e1dfe 100644 --- a/apps/meteor/app/api/server/v1/im.ts +++ b/apps/meteor/app/api/server/v1/im.ts @@ -1,7 +1,7 @@ /** * Docs: https://github.com/RocketChat/developer-docs/blob/master/reference/api/rest-api/endpoints/team-collaboration-endpoints/im-endpoints */ -import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, ISubscription, IUser } from '@rocket.chat/core-typings'; import { Subscriptions, Uploads, Messages, Rooms, Users } from '@rocket.chat/models'; import { isDmDeleteProps, @@ -13,6 +13,7 @@ import { } from '@rocket.chat/rest-typings'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import type { FindOptions } from 'mongodb'; import { eraseRoom } from '../../../../server/lib/eraseRoom'; import { openRoom } from '../../../../server/lib/openRoom'; @@ -338,7 +339,7 @@ API.v1.addRoute( room._id, ); - const options = { + const options: FindOptions = { projection: { _id: 1, username: 1, diff --git a/apps/meteor/app/api/server/v1/integrations.ts b/apps/meteor/app/api/server/v1/integrations.ts index 1c1dd9dd50f3b..5fb1781aa1411 100644 --- a/apps/meteor/app/api/server/v1/integrations.ts +++ b/apps/meteor/app/api/server/v1/integrations.ts @@ -10,7 +10,6 @@ import { } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Match, check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; import type { Filter } from 'mongodb'; import { @@ -19,8 +18,10 @@ import { } from '../../../integrations/server/lib/mountQueriesBasedOnPermission'; import { addIncomingIntegration } from '../../../integrations/server/methods/incoming/addIncomingIntegration'; import { deleteIncomingIntegration } from '../../../integrations/server/methods/incoming/deleteIncomingIntegration'; +import { updateIncomingIntegration } from '../../../integrations/server/methods/incoming/updateIncomingIntegration'; import { addOutgoingIntegration } from '../../../integrations/server/methods/outgoing/addOutgoingIntegration'; import { deleteOutgoingIntegration } from '../../../integrations/server/methods/outgoing/deleteOutgoingIntegration'; +import { updateOutgoingIntegration } from '../../../integrations/server/methods/outgoing/updateOutgoingIntegration'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; import { findOneIntegration } from '../lib/integrations'; @@ -248,7 +249,7 @@ API.v1.addRoute( return API.v1.failure('No integration found.'); } - await Meteor.callAsync('updateOutgoingIntegration', integration._id, bodyParams); + await updateOutgoingIntegration(this.userId, integration._id, bodyParams); return API.v1.success({ integration: await Integrations.findOne({ _id: integration._id }), @@ -260,7 +261,7 @@ API.v1.addRoute( return API.v1.failure('No integration found.'); } - await Meteor.callAsync('updateIncomingIntegration', integration._id, bodyParams); + await updateIncomingIntegration(this.userId, integration._id, bodyParams); return API.v1.success({ integration: await Integrations.findOne({ _id: integration._id }), diff --git a/apps/meteor/app/api/server/v1/misc.ts b/apps/meteor/app/api/server/v1/misc.ts index bf273b75070dc..40d30fd8d2450 100644 --- a/apps/meteor/app/api/server/v1/misc.ts +++ b/apps/meteor/app/api/server/v1/misc.ts @@ -671,7 +671,7 @@ API.v1.addRoute( _id: this.userId, username: this.user.username!, ip: this.requestIp, - useragent: this.request.headers['user-agent'] || '', + useragent: this.request.headers.get('user-agent') || '', }); const promises = settingsIds.map((settingId) => { @@ -691,7 +691,7 @@ API.v1.addRoute( _id: this.userId, username: this.user.username!, ip: this.requestIp, - useragent: this.request.headers['user-agent'] || '', + useragent: this.request.headers.get('user-agent') || '', })(Settings.resetValueById, settingId); }); diff --git a/apps/meteor/app/api/server/v1/oauthapps.ts b/apps/meteor/app/api/server/v1/oauthapps.ts index 97d489295d424..fbdaf37ac2313 100644 --- a/apps/meteor/app/api/server/v1/oauthapps.ts +++ b/apps/meteor/app/api/server/v1/oauthapps.ts @@ -4,6 +4,8 @@ import { isUpdateOAuthAppParams, isOauthAppsGetParams, isOauthAppsAddParams, isD import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { addOAuthApp } from '../../../oauth2-server-config/server/admin/functions/addOAuthApp'; +import { deleteOAuthApp } from '../../../oauth2-server-config/server/admin/methods/deleteOAuthApp'; +import { updateOAuthApp } from '../../../oauth2-server-config/server/admin/methods/updateOAuthApp'; import { API } from '../api'; API.v1.addRoute( @@ -56,7 +58,7 @@ API.v1.addRoute( async post() { const { appId } = this.bodyParams; - const result = await Meteor.callAsync('updateOAuthApp', appId, this.bodyParams); + const result = await updateOAuthApp(this.userId, appId, this.bodyParams); return API.v1.success(result); }, @@ -74,7 +76,7 @@ API.v1.addRoute( async post() { const { appId } = this.bodyParams; - const result = await Meteor.callAsync('deleteOAuthApp', appId); + const result = await deleteOAuthApp(this.userId, appId); return API.v1.success(result); }, diff --git a/apps/meteor/app/api/server/v1/permissions.ts b/apps/meteor/app/api/server/v1/permissions.ts index 65dfafe5ccce2..9b7cee5d3c7b5 100644 --- a/apps/meteor/app/api/server/v1/permissions.ts +++ b/apps/meteor/app/api/server/v1/permissions.ts @@ -3,6 +3,7 @@ import { Permissions, Roles } from '@rocket.chat/models'; import { isBodyParamsValidPermissionUpdate } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; +import { permissionsGetMethod } from '../../../authorization/server/streamer/permissions'; import { notifyOnPermissionChangedById } from '../../../lib/server/lib/notifyListener'; import { API } from '../api'; @@ -21,7 +22,7 @@ API.v1.addRoute( updatedSinceDate = new Date(updatedSince); } - const result = (await Meteor.callAsync('permissions/get', updatedSinceDate)) as { + const result = (await permissionsGetMethod(updatedSinceDate)) as { update: IPermission[]; remove: IPermission[]; }; @@ -69,7 +70,7 @@ API.v1.addRoute( void notifyOnPermissionChangedById(permission._id); } - const result = (await Meteor.callAsync('permissions/get')) as IPermission[]; + const result = (await permissionsGetMethod()) as IPermission[]; return API.v1.success({ permissions: result, diff --git a/apps/meteor/app/api/server/v1/push.ts b/apps/meteor/app/api/server/v1/push.ts index a2c29f85db407..f84076d83d51e 100644 --- a/apps/meteor/app/api/server/v1/push.ts +++ b/apps/meteor/app/api/server/v1/push.ts @@ -1,3 +1,4 @@ +import type { IAppsTokens } from '@rocket.chat/core-typings'; import { Messages, AppsTokens, Users, Rooms, Settings } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { Match, check } from 'meteor/check'; @@ -5,6 +6,7 @@ import { Meteor } from 'meteor/meteor'; import { executePushTest } from '../../../../server/lib/pushConfig'; import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; +import { pushUpdate } from '../../../push/server/methods'; import PushNotification from '../../../push-notifications/server/lib/PushNotification'; import { settings } from '../../../settings/server'; import { API } from '../api'; @@ -34,10 +36,15 @@ API.v1.addRoute( throw new Meteor.Error('error-appName-param-not-valid', 'The required "appName" body param is missing or invalid.'); } - const result = await Meteor.callAsync('raix:push-update', { + const authToken = this.request.headers.get('x-auth-token'); + if (!authToken) { + throw new Meteor.Error('error-authToken-param-not-valid', 'The required "authToken" header param is missing or invalid.'); + } + + const result = await pushUpdate({ id: deviceId, - token: { [type]: value }, - authToken: this.request.headers['x-auth-token'], + token: { [type]: value } as IAppsTokens['token'], + authToken, appName, userId: this.userId, }); diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index e34a0fd94696e..446374d934684 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -25,7 +25,9 @@ import { findUsersOfRoomOrderedByRole } from '../../../../server/lib/findUsersOf import { openRoom } from '../../../../server/lib/openRoom'; import { hideRoomMethod } from '../../../../server/methods/hideRoom'; import { muteUserInRoom } from '../../../../server/methods/muteUserInRoom'; +import { toggleFavoriteMethod } from '../../../../server/methods/toggleFavorite'; import { unmuteUserInRoom } from '../../../../server/methods/unmuteUserInRoom'; +import { roomsGetMethod } from '../../../../server/publications/room'; import { canAccessRoomAsync, canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { saveRoomSettings } from '../../../channel-settings/server/methods/saveRoomSettings'; @@ -34,9 +36,12 @@ import { FileUpload } from '../../../file-upload/server'; import { sendFileMessage } from '../../../file-upload/server/methods/sendFileMessage'; import { syncRolePrioritiesForRoomIfRequired } from '../../../lib/server/functions/syncRolePrioritiesForRoomIfRequired'; import { executeArchiveRoom } from '../../../lib/server/methods/archiveRoom'; +import { cleanRoomHistoryMethod } from '../../../lib/server/methods/cleanRoomHistory'; import { leaveRoomMethod } from '../../../lib/server/methods/leaveRoom'; import { executeUnarchiveRoom } from '../../../lib/server/methods/unarchiveRoom'; import { applyAirGappedRestrictionsValidation } from '../../../license/server/airGappedRestrictionsWrapper'; +import type { NotificationFieldType } from '../../../push-notifications/server/methods/saveNotificationSettings'; +import { saveNotificationSettingsMethod } from '../../../push-notifications/server/methods/saveNotificationSettings'; import { settings } from '../../../settings/server'; import { API } from '../api'; import { composeRoomWithLastMessage } from '../helpers/composeRoomWithLastMessage'; @@ -144,7 +149,7 @@ API.v1.addRoute( } } - let result: { update: IRoom[]; remove: IRoom[] } = await Meteor.callAsync('rooms/get', updatedSinceDate); + let result = await roomsGetMethod(this.userId, updatedSinceDate); if (Array.isArray(result)) { result = { @@ -353,7 +358,12 @@ API.v1.addRoute( await Promise.all( Object.keys(notifications as Notifications).map(async (notificationKey) => - Meteor.callAsync('saveNotificationSettings', roomId, notificationKey, notifications[notificationKey as keyof Notifications]), + saveNotificationSettingsMethod( + this.userId, + roomId, + notificationKey as NotificationFieldType, + notifications[notificationKey as keyof Notifications], + ), ), ); @@ -375,7 +385,7 @@ API.v1.addRoute( const room = await findRoomByIdOrName({ params: this.bodyParams }); - await Meteor.callAsync('toggleFavorite', room._id, favorite); + await toggleFavoriteMethod(this.userId, room._id, favorite); return API.v1.success(); }, @@ -414,7 +424,7 @@ API.v1.addRoute( return API.v1.failure('Body parameter "oldest" is required.'); } - const count = await Meteor.callAsync('cleanRoomHistory', { + const count = await cleanRoomHistoryMethod(this.userId, { roomId: _id, latest: new Date(latest), oldest: new Date(oldest), @@ -424,7 +434,7 @@ API.v1.addRoute( filesOnly: [true, 'true', 1, '1'].includes(filesOnly ?? false), ignoreThreads: [true, 'true', 1, '1'].includes(ignoreThreads ?? false), ignoreDiscussion: [true, 'true', 1, '1'].includes(ignoreDiscussion ?? false), - fromUsers: users, + fromUsers: users?.filter(isTruthy) || [], }); return API.v1.success({ _id, count }); diff --git a/apps/meteor/app/api/server/v1/server-events.ts b/apps/meteor/app/api/server/v1/server-events.ts deleted file mode 100644 index 59fe33d41e530..0000000000000 --- a/apps/meteor/app/api/server/v1/server-events.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { ServerEvents } from '@rocket.chat/models'; -import { isServerEventsAuditSettingsProps } from '@rocket.chat/rest-typings'; -import { ajv } from '@rocket.chat/rest-typings/src/v1/Ajv'; - -import { API } from '../api'; -import { getPaginationItems } from '../helpers/getPaginationItems'; - -API.v1.get( - 'audit.settings', - { - response: { - 200: ajv.compile({ - additionalProperties: false, - type: 'object', - properties: { - events: { - type: 'array', - items: { - type: 'object', - }, - }, - count: { - type: 'number', - description: 'The number of events returned in this response.', - }, - offset: { - type: 'number', - description: 'The number of events that were skipped in this response.', - }, - total: { - type: 'number', - description: 'The total number of events that match the query.', - }, - success: { - type: 'boolean', - description: 'Indicates if the request was successful.', - }, - }, - required: ['events', 'count', 'offset', 'total', 'success'], - }), - 400: ajv.compile({ - type: 'object', - properties: { - success: { - type: 'boolean', - enum: [false], - }, - error: { - type: 'string', - }, - errorType: { - type: 'string', - }, - }, - required: ['success', 'error'], - }), - }, - query: isServerEventsAuditSettingsProps, - authRequired: true, - permissionsRequired: ['can-audit'], - }, - async function action() { - const { start, end, settingId, actor } = this.queryParams; - - if (start && isNaN(Date.parse(start as string))) { - return API.v1.failure('The "start" query parameter must be a valid date.'); - } - - if (end && isNaN(Date.parse(end as string))) { - return API.v1.failure('The "end" query parameter must be a valid date.'); - } - - const { offset, count } = await getPaginationItems(this.queryParams as Record); - const { sort } = await this.parseJsonQuery(); - const _sort = { ts: sort?.ts ? sort?.ts : -1 }; - - const { cursor, totalCount } = ServerEvents.findPaginated( - { - ...(settingId && { 'data.key': 'id', 'data.value': settingId }), - ...(actor && { actor }), - ts: { - $gte: start ? new Date(start as string) : new Date(0), - $lte: end ? new Date(end as string) : new Date(), - }, - t: 'settings.changed', - }, - { - sort: _sort, - skip: offset, - limit: count, - allowDiskUse: true, - }, - ); - - const [events, total] = await Promise.all([cursor.toArray(), totalCount]); - - return API.v1.success({ - events, - count: events.length, - offset, - total, - }); - }, -); diff --git a/apps/meteor/app/api/server/v1/settings.ts b/apps/meteor/app/api/server/v1/settings.ts index 6d2bbab89afd8..438fda41850b3 100644 --- a/apps/meteor/app/api/server/v1/settings.ts +++ b/apps/meteor/app/api/server/v1/settings.ts @@ -212,7 +212,7 @@ API.v1.addRoute( _id: this.userId, username: this.user.username!, ip: this.requestIp, - useragent: this.request.headers['user-agent'] || '', + useragent: this.request.headers.get('user-agent') || '', }); if (isSettingColor(setting) && isSettingsUpdatePropsColor(this.bodyParams)) { diff --git a/apps/meteor/app/api/server/v1/subscriptions.ts b/apps/meteor/app/api/server/v1/subscriptions.ts index 200fa7a5eabb9..a3192aeb9ec14 100644 --- a/apps/meteor/app/api/server/v1/subscriptions.ts +++ b/apps/meteor/app/api/server/v1/subscriptions.ts @@ -100,7 +100,11 @@ API.v1.addRoute( }, { async post() { - await unreadMessages(this.userId, (this.bodyParams as any).firstUnreadMessage, (this.bodyParams as any).roomId); + await unreadMessages( + this.userId, + 'firstUnreadMessage' in this.bodyParams ? this.bodyParams.firstUnreadMessage : undefined, + 'roomId' in this.bodyParams ? this.bodyParams.roomId : undefined, + ); return API.v1.success(); }, diff --git a/apps/meteor/app/api/server/v1/teams.ts b/apps/meteor/app/api/server/v1/teams.ts index 4b54cbffb7226..84d1ba5db264c 100644 --- a/apps/meteor/app/api/server/v1/teams.ts +++ b/apps/meteor/app/api/server/v1/teams.ts @@ -441,9 +441,9 @@ API.v1.addRoute( const canSeeAllMembers = await hasPermissionAsync(this.userId, 'view-all-teams', team.roomId); const query = { - username: username ? new RegExp(escapeRegExp(username), 'i') : undefined, - name: name ? new RegExp(escapeRegExp(name), 'i') : undefined, - status: status ? { $in: status as UserStatus[] } : undefined, + ...(username && { username: new RegExp(escapeRegExp(username), 'i') }), + ...(name && { name: new RegExp(escapeRegExp(name), 'i') }), + ...(status && { status: { $in: status as UserStatus[] } }), }; const { records, total } = await Team.members(this.userId, team._id, canSeeAllMembers, { offset, count }, query); diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 55f70e9e84722..a065cc47407c7 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -28,6 +28,7 @@ import type { Filter } from 'mongodb'; import { generatePersonalAccessTokenOfUser } from '../../../../imports/personal-access-tokens/server/api/methods/generateToken'; import { regeneratePersonalAccessTokenOfUser } from '../../../../imports/personal-access-tokens/server/api/methods/regenerateToken'; 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 { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey'; import { sendWelcomeEmail } from '../../../../server/lib/sendWelcomeEmail'; @@ -80,7 +81,7 @@ API.v1.addRoute( const user = await getUserFromParams(this.queryParams); const url = getURL(`/avatar/${user.username}`, { cdn: false, full: true }); - this.response.setHeader('Location', url); + this.response.headers.set('Location', url); return { statusCode: 307, @@ -114,8 +115,14 @@ API.v1.addRoute( if (userData.name && !validateNameChars(userData.name)) { return API.v1.failure('Name contains invalid characters'); } + const auditStore = new UserChangedAuditStore({ + _id: this.user._id, + ip: this.requestIp, + useragent: this.request.headers.get('user-agent') || '', + username: this.user.username || '', + }); - await saveUser(this.userId, userData); + await saveUser(this.userId, userData, { auditStore }); if (typeof this.bodyParams.data.active !== 'undefined') { const { @@ -123,9 +130,9 @@ API.v1.addRoute( data: { active }, confirmRelinquish, } = this.bodyParams; - await executeSetUserActiveStatus(this.userId, userId, active, Boolean(confirmRelinquish)); } + const { fields } = await this.parseJsonQuery(); const user = await Users.findOneById(this.bodyParams.userId, { projection: fields }); @@ -233,9 +240,7 @@ API.v1.addRoute( }); } - let user = await (async (): Promise< - Pick | undefined | null - > => { + let user = await (async (): Promise | undefined | null> => { if (isUserFromParams(this.bodyParams, this.userId, this.user)) { return Users.findOneById(this.userId); } @@ -269,9 +274,11 @@ API.v1.addRoute( const sentTheUserByFormData = fields.userId || fields.username; if (sentTheUserByFormData) { if (fields.userId) { - user = await Users.findOneById(fields.userId, { projection: { username: 1 } }); + user = await Users.findOneById>(fields.userId, { projection: { username: 1 } }); } else if (fields.username) { - user = await Users.findOneByUsernameIgnoringCase(fields.username, { projection: { username: 1 } }); + user = await Users.findOneByUsernameIgnoringCase>(fields.username, { + projection: { username: 1 }, + }); } if (!user) { @@ -885,7 +892,7 @@ API.v1.addRoute( await Users.enableEmail2FAByUserId(this.userId); // When 2FA is enable we logout all other clients - const xAuthToken = this.request.headers['x-auth-token'] as string; + const xAuthToken = this.request.headers.get('x-auth-token') as string; if (!xAuthToken) { return API.v1.success(); } @@ -1063,7 +1070,7 @@ API.v1.addRoute( { authRequired: true }, { async post() { - const xAuthToken = this.request.headers['x-auth-token'] as string; + const xAuthToken = this.request.headers.get('x-auth-token') as string; if (!xAuthToken) { throw new Meteor.Error('error-parameter-required', 'x-auth-token is required'); diff --git a/apps/meteor/app/api/server/v1/voip/rooms.ts b/apps/meteor/app/api/server/v1/voip/rooms.ts index 2e4c7cea7c1b0..13d2e2c31a054 100644 --- a/apps/meteor/app/api/server/v1/voip/rooms.ts +++ b/apps/meteor/app/api/server/v1/voip/rooms.ts @@ -1,5 +1,5 @@ import { LivechatVoip } from '@rocket.chat/core-services'; -import type { ILivechatAgent, IVoipRoom } from '@rocket.chat/core-typings'; +import type { IVoipRoom } from '@rocket.chat/core-typings'; import { VoipRoom, LivechatVisitors, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { isVoipRoomProps, isVoipRoomsProps, isVoipRoomCloseProps } from '@rocket.chat/rest-typings'; @@ -128,7 +128,7 @@ API.v1.addRoute( return API.v1.failure('agent-not-found'); } - const agentObj: ILivechatAgent = await Users.findOneAgentById(agentId, { + const agentObj = await Users.findOneAgentById(agentId, { projection: { username: 1 }, }); if (!agentObj?.username) { diff --git a/apps/meteor/app/apple/client/index.ts b/apps/meteor/app/apple/client/index.ts index 2c59dbe5b3d45..f2579fed790d6 100644 --- a/apps/meteor/app/apple/client/index.ts +++ b/apps/meteor/app/apple/client/index.ts @@ -1,4 +1,4 @@ import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth'; import { config } from '../lib/config'; -new CustomOAuth('apple', config); +CustomOAuth.configureOAuthService('apple', config); diff --git a/apps/meteor/app/apps/server/bridges/activation.ts b/apps/meteor/app/apps/server/bridges/activation.ts index dc5a0f57e0035..399dfd285e65e 100644 --- a/apps/meteor/app/apps/server/bridges/activation.ts +++ b/apps/meteor/app/apps/server/bridges/activation.ts @@ -1,6 +1,7 @@ import type { IAppServerOrchestrator, AppStatus } from '@rocket.chat/apps'; import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; import { AppActivationBridge as ActivationBridge } from '@rocket.chat/apps-engine/server/bridges/AppActivationBridge'; +import { UserStatus } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; export class AppActivationBridge extends ActivationBridge { @@ -29,7 +30,7 @@ export class AppActivationBridge extends ActivationBridge { } protected async appStatusChanged(app: ProxiedApp, status: AppStatus): Promise { - const userStatus = ['auto_enabled', 'manually_enabled'].includes(status) ? 'online' : 'offline'; + const userStatus = ['auto_enabled', 'manually_enabled'].includes(status) ? UserStatus.ONLINE : UserStatus.OFFLINE; await Users.updateStatusByAppId(app.getID(), userStatus); diff --git a/apps/meteor/app/apps/server/bridges/api.ts b/apps/meteor/app/apps/server/bridges/api.ts index 46bb70e3339a3..f115897c82d82 100644 --- a/apps/meteor/app/apps/server/bridges/api.ts +++ b/apps/meteor/app/apps/server/bridges/api.ts @@ -8,13 +8,10 @@ import express from 'express'; import { Meteor } from 'meteor/meteor'; import { WebApp } from 'meteor/webapp'; +import { apiServer } from './router'; import { authenticationMiddleware } from '../../../api/server/middlewares/authentication'; -const apiServer = express(); - -apiServer.disable('x-powered-by'); - -WebApp.connectHandlers.use(apiServer); +WebApp.rawConnectHandlers.use(apiServer); interface IRequestWithPrivateHash extends Request { _privateHash?: string; diff --git a/apps/meteor/app/apps/server/bridges/listeners.js b/apps/meteor/app/apps/server/bridges/listeners.js index ebf57f7ccceb3..31aa2c0052695 100644 --- a/apps/meteor/app/apps/server/bridges/listeners.js +++ b/apps/meteor/app/apps/server/bridges/listeners.js @@ -50,6 +50,8 @@ export class AppListenerBridge { case AppInterface.IPostLivechatRoomTransferred: case AppInterface.IPostLivechatGuestSaved: case AppInterface.IPostLivechatRoomSaved: + case AppInterface.IPostLivechatDepartmentRemoved: + case AppInterface.IPostLivechatDepartmentDisabled: return 'livechatEvent'; case AppInterface.IPostUserCreated: case AppInterface.IPostUserUpdated: @@ -197,6 +199,16 @@ export class AppListenerBridge { .getManager() .getListenerManager() .executeListener(inte, await this.orch.getConverters().get('rooms').convertById(data)); + case AppInterface.IPostLivechatDepartmentDisabled: + return this.orch + .getManager() + .getListenerManager() + .executeListener(inte, await this.orch.getConverters().get('departments').convertDepartment(data)); + case AppInterface.IPostLivechatDepartmentRemoved: + return this.orch + .getManager() + .getListenerManager() + .executeListener(inte, await this.orch.getConverters().get('departments').convertDepartment(data)); default: const room = await this.orch.getConverters().get('rooms').convertRoom(data); diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 361ba2098bbd0..6c1152e678b3d 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -10,12 +10,15 @@ import { LivechatVisitors, LivechatRooms, LivechatDepartment, Users } from '@roc import { callbacks } from '../../../../lib/callbacks'; import { deasyncPromise } from '../../../../server/deasync/deasync'; -import { Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped'; import { closeRoom } from '../../../livechat/server/lib/closeRoom'; +import { setCustomFields } from '../../../livechat/server/lib/custom-fields'; import { getRoomMessages } from '../../../livechat/server/lib/getRoomMessages'; +import { registerGuest } from '../../../livechat/server/lib/guests'; import type { ILivechatMessage } from '../../../livechat/server/lib/localTypes'; import { updateMessage, sendMessage } from '../../../livechat/server/lib/messages'; import { createRoom } from '../../../livechat/server/lib/rooms'; +import { online } from '../../../livechat/server/lib/service-status'; +import { transfer } from '../../../livechat/server/lib/transfer'; import { settings } from '../../../settings/server'; declare module '@rocket.chat/apps/dist/converters/IAppMessagesConverter' { @@ -38,11 +41,11 @@ export class AppLivechatBridge extends LivechatBridge { protected isOnline(departmentId?: string): boolean { // This function will be converted to sync inside the apps-engine code // TODO: Track Deprecation - return deasyncPromise(LivechatTyped.online(departmentId)); + return deasyncPromise(online(departmentId)); } protected async isOnlineAsync(departmentId?: string): Promise { - return LivechatTyped.online(departmentId); + return online(departmentId); } protected async createMessage(message: IAppsLivechatMessage, appId: string): Promise { @@ -210,7 +213,7 @@ export class AppLivechatBridge extends LivechatBridge { ...(visitor.visitorEmails?.length && { email: visitor.visitorEmails[0].address }), }; - const livechatVisitor = await LivechatTyped.registerGuest(registerData); + const livechatVisitor = await registerGuest(registerData); if (!livechatVisitor) { throw new Error('Invalid visitor, cannot create'); @@ -234,7 +237,7 @@ export class AppLivechatBridge extends LivechatBridge { ...(visitor.visitorEmails?.length && { email: visitor.visitorEmails[0].address }), }; - const livechatVisitor = await LivechatTyped.registerGuest(registerData); + const livechatVisitor = await registerGuest(registerData); return this.orch.getConverters()?.get('visitors').convertVisitor(livechatVisitor); } @@ -276,7 +279,7 @@ export class AppLivechatBridge extends LivechatBridge { } // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. - return LivechatTyped.transfer( + return transfer( (await this.orch.getConverters()?.get('rooms').convertAppRoom(currentRoom)) as IOmnichannelRoom, this.orch.getConverters()?.get('visitors').convertAppVisitor(visitor), { userId, departmentId, transferredBy, transferredTo }, @@ -367,6 +370,6 @@ export class AppLivechatBridge extends LivechatBridge { ): Promise { this.orch.debugLog(`The App ${appId} is setting livechat visitor's custom fields.`); - return LivechatTyped.setCustomFields(data); + return setCustomFields(data); } } diff --git a/apps/meteor/app/apps/server/bridges/router.spec.ts b/apps/meteor/app/apps/server/bridges/router.spec.ts new file mode 100644 index 0000000000000..61ebe714a2acd --- /dev/null +++ b/apps/meteor/app/apps/server/bridges/router.spec.ts @@ -0,0 +1,95 @@ +import request from 'supertest'; + +import { apiServer } from './router'; + +apiServer.post('/api/apps/private/:appId/:hash', (req, res) => { + res.json({ + body: req.body, + params: req.params, + query: req.query, + }); +}); + +apiServer.post('/api/apps/public/:appId', (req, res) => { + res.json({ + body: req.body, + params: req.params, + query: req.query, + }); +}); + +describe('API Server Routes', () => { + it('should handle POST requests to /api/apps/private/:appId/:hash using json encoding', async () => { + const appId = 'testAppId'; + const hash = 'testHash'; + await request(apiServer) + .post(`/api/apps/private/${appId}/${hash}`) + .set('Content-Type', 'application/json') + .send({ + key: 'value', + }) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ + body: { key: 'value' }, + params: { appId, hash }, + query: {}, + }); + }); + }); + + it('should handle POST requests to /api/apps/private/:appId/:hash using x-www-form-urlencoded encoding', async () => { + const appId = 'testAppId'; + const hash = 'testHash'; + await request(apiServer) + .post(`/api/apps/private/${appId}/${hash}`) + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('key=value') + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ + body: { key: 'value' }, + params: { appId, hash }, + query: {}, + }); + }); + }); + + it('should handle POST requests to /api/apps/public/:appId using json encoding', async () => { + const appId = 'testAppId'; + await request(apiServer) + .post(`/api/apps/public/${appId}`) + .set('Content-Type', 'application/json') + .send({ + key: 'value', + }) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ + body: { key: 'value' }, + params: { appId }, + query: {}, + }); + }); + }); + + it('should handle POST requests to /api/apps/public/:appId using x-www-form-urlencoded encoding', async () => { + const appId = 'testAppId'; + await request(apiServer) + .post(`/api/apps/public/${appId}`) + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('key=value') + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ + body: { key: 'value' }, + params: { appId }, + query: {}, + }); + }); + }); +}); diff --git a/apps/meteor/app/apps/server/bridges/router.ts b/apps/meteor/app/apps/server/bridges/router.ts new file mode 100644 index 0000000000000..993162b63c16f --- /dev/null +++ b/apps/meteor/app/apps/server/bridges/router.ts @@ -0,0 +1,9 @@ +import bodyParser from 'body-parser'; +import express from 'express'; + +export const apiServer = express(); + +apiServer.disable('x-powered-by'); + +apiServer.use('/api/apps/private/:appId/:hash', bodyParser.urlencoded(), bodyParser.json()); +apiServer.use('/api/apps/public/:appId', bodyParser.urlencoded(), bodyParser.json()); diff --git a/apps/meteor/app/apps/server/converters/messages.js b/apps/meteor/app/apps/server/converters/messages.js index 8cc6f4ea270f2..a824df3228396 100644 --- a/apps/meteor/app/apps/server/converters/messages.js +++ b/apps/meteor/app/apps/server/converters/messages.js @@ -1,6 +1,7 @@ import { isMessageFromVisitor } from '@rocket.chat/core-typings'; import { Messages, Rooms, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; +import { removeEmpty } from '@rocket.chat/tools'; import { cachedFunction } from './cachedFunction'; import { transformMappedData } from './transformMappedData'; @@ -236,39 +237,37 @@ export class AppMessagesConverter { } return attachments.map((attachment) => - Object.assign( - { - collapsed: attachment.collapsed, - color: attachment.color, - text: attachment.text, - ts: attachment.timestamp ? attachment.timestamp.toJSON() : attachment.timestamp, - message_link: attachment.timestampLink, - thumb_url: attachment.thumbnailUrl, - author_name: attachment.author ? attachment.author.name : undefined, - author_link: attachment.author ? attachment.author.link : undefined, - author_icon: attachment.author ? attachment.author.icon : undefined, - title: attachment.title ? attachment.title.value : undefined, - title_link: attachment.title ? attachment.title.link : undefined, - title_link_download: attachment.title ? attachment.title.displayDownloadLink : undefined, - image_dimensions: attachment.imageDimensions, - image_preview: attachment.imagePreview, - image_url: attachment.imageUrl, - image_type: attachment.imageType, - image_size: attachment.imageSize, - audio_url: attachment.audioUrl, - audio_type: attachment.audioType, - audio_size: attachment.audioSize, - video_url: attachment.videoUrl, - video_type: attachment.videoType, - video_size: attachment.videoSize, - fields: attachment.fields, - button_alignment: attachment.actionButtonsAlignment, - actions: attachment.actions, - type: attachment.type, - description: attachment.description, - }, - attachment._unmappedProperties_, - ), + removeEmpty({ + collapsed: attachment.collapsed, + color: attachment.color, + text: attachment.text, + ts: attachment.timestamp ? attachment.timestamp.toJSON() : attachment.timestamp, + message_link: attachment.timestampLink, + thumb_url: attachment.thumbnailUrl, + author_name: attachment.author ? attachment.author.name : undefined, + author_link: attachment.author ? attachment.author.link : undefined, + author_icon: attachment.author ? attachment.author.icon : undefined, + title: attachment.title ? attachment.title.value : undefined, + title_link: attachment.title ? attachment.title.link : undefined, + title_link_download: attachment.title ? attachment.title.displayDownloadLink : undefined, + image_dimensions: attachment.imageDimensions, + image_preview: attachment.imagePreview, + image_url: attachment.imageUrl, + image_type: attachment.imageType, + image_size: attachment.imageSize, + audio_url: attachment.audioUrl, + audio_type: attachment.audioType, + audio_size: attachment.audioSize, + video_url: attachment.videoUrl, + video_type: attachment.videoType, + video_size: attachment.videoSize, + fields: attachment.fields, + button_alignment: attachment.actionButtonsAlignment, + actions: attachment.actions, + type: attachment.type, + description: attachment.description, + ...attachment._unmappedProperties_, + }), ); } diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.js index b2bbcda49610d..2404451366af5 100644 --- a/apps/meteor/app/apps/server/converters/rooms.js +++ b/apps/meteor/app/apps/server/converters/rooms.js @@ -20,120 +20,147 @@ export class AppRoomsConverter { return this.convertRoom(room); } - async convertAppRoom(room, isPartial = false) { - if (!room) { - return undefined; + async __getCreator(user) { + if (!user) { + return; } - let u; - if (room.creator) { - const creator = await Users.findOneById(room.creator.id); - u = { - _id: creator._id, - username: creator.username, - name: creator.name, - }; + const creator = await Users.findOneById(user, { projection: { _id: 1, username: 1, name: 1 } }); + if (!creator) { + return; } - let v; - if (room.visitor) { - const visitor = await LivechatVisitors.findOneEnabledById(room.visitor.id); + return { + _id: creator._id, + username: creator.username, + name: creator.name, + }; + } - const { lastMessageTs, phone } = room.visitorChannelInfo; + async __getVisitor({ visitor: roomVisitor, visitorChannelInfo }) { + if (!roomVisitor) { + return; + } - v = { - _id: visitor._id, - username: visitor.username, - token: visitor.token, - status: visitor.status || 'online', - ...(lastMessageTs && { lastMessageTs }), - ...(phone && { phone }), - }; + const visitor = await LivechatVisitors.findOneEnabledById(roomVisitor.id); + if (!visitor) { + return; } - let departmentId; - if (room.department) { - const department = await LivechatDepartment.findOneById(room.department.id, { projection: { _id: 1 } }); - departmentId = department._id; + const { lastMessageTs, phone } = visitorChannelInfo; + + return { + _id: visitor._id, + username: visitor.username, + token: visitor.token, + status: visitor.status || 'online', + ...(lastMessageTs && { lastMessageTs }), + ...(phone && { phone }), + }; + } + + async __getUserIdAndUsername(userObj) { + if (!userObj?.id) { + return; + } + + const user = await Users.findOneById(userObj.id, { projection: { _id: 1, username: 1 } }); + if (!user) { + return; + } + + return { + _id: user._id, + username: user.username, + }; + } + + async __getRoomCloser(room, v) { + if (!room.closedBy) { + return; } - let servedBy; - if (room.servedBy) { - const user = await Users.findOneById(room.servedBy.id); - servedBy = { + if (room.closer === 'user') { + const user = await Users.findOneById(room.closedBy.id, { projection: { _id: 1, username: 1 } }); + if (!user) { + return; + } + + return { _id: user._id, username: user.username, }; } - let closedBy; - if (room.closedBy) { - if (room.closer === 'user') { - const user = await Users.findOneById(room.closedBy.id); - closedBy = { - _id: user._id, - username: user.username, - }; - } else if (room.closer === 'visitor') { - closedBy = { - _id: v._id, - username: v.username, - }; - } + if (room.closer === 'visitor' && v) { + return { + _id: v._id, + username: v.username, + }; } + } - let contactId; - if (room.contact?._id) { - const contact = await LivechatContacts.findOneById(room.contact._id, { projection: { _id: 1 } }); - contactId = contact._id; + // TODO do we really need this? + async __getContactId({ contact }) { + if (!contact?._id) { + return; } + const contactFromDb = await LivechatContacts.findOneById(contact._id, { projection: { _id: 1 } }); + return contactFromDb?._id; + } - let _default; - if (typeof room.isDefault !== 'undefined') { - _default = room.isDefault; + // TODO do we really need this? + async __getDepartment({ department }) { + if (!department) { + return; } + const dept = await LivechatDepartment.findOneById(department.id, { projection: { _id: 1 } }); + return dept?._id; + } - let ro; - if (typeof room.isReadOnly !== 'undefined') { - ro = room.isReadOnly; + async convertAppRoom(room, isPartial = false) { + if (!room) { + return undefined; } - let sysMes; - if (typeof room.displaySystemMessages !== 'undefined') { - sysMes = room.displaySystemMessages; - } + const u = await this.__getCreator(room.creator?.id); - let msgs; - if (typeof room.messageCount !== 'undefined') { - msgs = room.messageCount; - } + const v = await this.__getVisitor(room); + + const departmentId = await this.__getDepartment(room); + + const servedBy = await this.__getUserIdAndUsername(room.servedBy); + + const closedBy = await this.__getRoomCloser(room, v); + + const contactId = await this.__getContactId(room); const newRoom = { ...(room.id && { _id: room.id }), - fname: room.displayName, - name: room.slugifiedName, - t: room.type, - u, - v, - ro, - sysMes, - msgs, - departmentId, - servedBy, - closedBy, - members: room.members, - uids: room.userIds, - default: _default, - waitingResponse: typeof room.isWaitingResponse === 'undefined' ? undefined : !!room.isWaitingResponse, - open: typeof room.isOpen === 'undefined' ? undefined : !!room.isOpen, - ts: room.createdAt, - _updatedAt: room.updatedAt, - closedAt: room.closedAt, - lm: room.lastModifiedAt, - customFields: room.customFields, - livechatData: room.livechatData, - prid: typeof room.parentRoom === 'undefined' ? undefined : room.parentRoom.id, - contactId, + ...(typeof room.type !== 'undefined' && { t: room.type }), + ...(typeof room.createdAt !== 'undefined' && { ts: room.createdAt }), + ...(typeof room.messageCount !== 'undefined' && { msgs: room.messageCount || 0 }), + ...(typeof room.updatedAt !== 'undefined' && { _updatedAt: room.updatedAt }), + ...(room.displayName && { fname: room.displayName }), + ...(room.type !== 'd' && room.slugifiedName && { name: room.slugifiedName }), + ...(room.members && { members: room.members }), + ...(typeof room.isDefault !== 'undefined' && { default: room.isDefault }), + ...(typeof room.isReadOnly !== 'undefined' && { ro: room.isReadOnly }), + ...(typeof room.displaySystemMessages !== 'undefined' && { sysMes: room.displaySystemMessages }), + ...(u && { u }), + ...(v && { v }), + ...(departmentId && { departmentId }), + ...(servedBy && { servedBy }), + ...(closedBy && { closedBy }), + ...(room.userIds && { uids: room.userIds }), + ...(typeof room.isWaitingResponse !== 'undefined' && { waitingResponse: !!room.isWaitingResponse }), + ...(typeof room.isOpen !== 'undefined' && { open: !!room.isOpen }), + ...(room.closedAt && { closedAt: room.closedAt }), + ...(room.lastModifiedAt && { lm: room.lastModifiedAt }), + ...(room.customFields && { customFields: room.customFields }), + ...(room.livechatData && { livechatData: room.livechatData }), + ...(typeof room.parentRoom !== 'undefined' && { prid: room.parentRoom.id }), + ...(contactId && { contactId }), ...(room._USERNAMES && { _USERNAMES: room._USERNAMES }), ...(room.source && { source: { @@ -142,13 +169,7 @@ export class AppRoomsConverter { }), }; - if (isPartial) { - Object.entries(newRoom).forEach(([key, value]) => { - if (typeof value === 'undefined') { - delete newRoom[key]; - } - }); - } else { + if (!isPartial) { Object.assign(newRoom, room._unmappedProperties_); } diff --git a/apps/meteor/app/apps/server/converters/users.js b/apps/meteor/app/apps/server/converters/users.js index e89bf71a04281..b8f4d5c9043b7 100644 --- a/apps/meteor/app/apps/server/converters/users.js +++ b/apps/meteor/app/apps/server/converters/users.js @@ -1,5 +1,6 @@ import { UserStatusConnection, UserType } from '@rocket.chat/apps-engine/definition/users'; import { Users } from '@rocket.chat/models'; +import { removeEmpty } from '@rocket.chat/tools'; export class AppUsersConverter { constructor(orch) { @@ -56,7 +57,7 @@ export class AppUsersConverter { return undefined; } - return { + return removeEmpty({ _id: user.id, username: user.username, emails: user.emails, @@ -71,7 +72,7 @@ export class AppUsersConverter { _updatedAt: user.updatedAt, lastLogin: user.lastLoginAt, appId: user.appId, - }; + }); } _convertUserTypeToEnum(type) { diff --git a/apps/meteor/app/authorization/server/streamer/permissions/index.ts b/apps/meteor/app/authorization/server/streamer/permissions/index.ts index e74cf37869fd8..545b7067e677a 100644 --- a/apps/meteor/app/authorization/server/streamer/permissions/index.ts +++ b/apps/meteor/app/authorization/server/streamer/permissions/index.ts @@ -14,22 +14,27 @@ declare module '@rocket.chat/ddp-client' { } } +export const permissionsGetMethod = async ( + updatedAt?: Date, +): Promise>[] }> => { + const records = await Permissions.find(updatedAt && { _updatedAt: { $gt: updatedAt } }).toArray(); + + if (updatedAt instanceof Date) { + return { + update: records, + remove: await Permissions.trashFindDeletedAfter(updatedAt, {}, { projection: { _id: 1, _deletedAt: 1 } }).toArray(), + }; + } + + return records; +}; + Meteor.methods({ async 'permissions/get'(updatedAt?: Date) { check(updatedAt, Match.Maybe(Date)); - // TODO: should we return this for non logged users? // TODO: we could cache this collection - const records = await Permissions.find(updatedAt && { _updatedAt: { $gt: updatedAt } }).toArray(); - - if (updatedAt instanceof Date) { - return { - update: records, - remove: await Permissions.trashFindDeletedAfter(updatedAt, {}, { projection: { _id: 1, _deletedAt: 1 } }).toArray(), - }; - } - - return records; + return permissionsGetMethod(updatedAt); }, }); diff --git a/apps/meteor/app/canned-responses/client/collections/CannedResponse.ts b/apps/meteor/app/canned-responses/client/collections/CannedResponse.ts deleted file mode 100644 index 6ea1f70e9897e..0000000000000 --- a/apps/meteor/app/canned-responses/client/collections/CannedResponse.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Mongo } from 'meteor/mongo'; - -export const CannedResponse = new Mongo.Collection<{ - _id: string; - shortcut: string; - text: string; -}>(null); diff --git a/apps/meteor/app/canned-responses/client/index.ts b/apps/meteor/app/canned-responses/client/index.ts deleted file mode 100644 index 36fd7b5cc6af9..0000000000000 --- a/apps/meteor/app/canned-responses/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './startup/responses'; diff --git a/apps/meteor/app/canned-responses/client/startup/responses.ts b/apps/meteor/app/canned-responses/client/startup/responses.ts deleted file mode 100644 index 6d761adb890da..0000000000000 --- a/apps/meteor/app/canned-responses/client/startup/responses.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { hasPermission } from '../../../authorization/client'; -import { settings } from '../../../settings/client'; -import { sdk } from '../../../utils/client/lib/SDKClient'; -import { CannedResponse } from '../collections/CannedResponse'; - -Meteor.startup(() => { - Tracker.autorun(async (c) => { - if (!Meteor.userId()) { - return; - } - if (!settings.get('Canned_Responses_Enable')) { - return; - } - if (!hasPermission('view-canned-responses')) { - return; - } - Tracker.afterFlush(() => { - try { - // TODO: check options - sdk.stream('canned-responses', ['canned-responses'], (...[response, options]) => { - const { agentsId } = options || {}; - if (Array.isArray(agentsId) && !agentsId.includes(Meteor.userId())) { - return; - } - - switch (response.type) { - case 'changed': { - const { type, ...fields } = response; - CannedResponse.upsert({ _id: response._id }, fields); - break; - } - - case 'removed': { - CannedResponse.remove({ _id: response._id }); - break; - } - } - }); - } catch (error) { - console.log(error); - } - }); - - const { responses } = await sdk.rest.get('/v1/canned-responses.get'); - responses.forEach((response) => CannedResponse.insert(response)); - c.stop(); - }); -}); diff --git a/apps/meteor/app/custom-oauth/client/CustomOAuth.ts b/apps/meteor/app/custom-oauth/client/CustomOAuth.ts index 7849498fa4b9a..36b02abb5ca50 100644 --- a/apps/meteor/app/custom-oauth/client/CustomOAuth.ts +++ b/apps/meteor/app/custom-oauth/client/CustomOAuth.ts @@ -18,6 +18,8 @@ import { isURL } from '../../../lib/utils/isURL'; // completion. Takes one argument, credentialToken on success, or Error on // error. +const configuredOAuthServices = new Map(); + export class CustomOAuth implements IOAuthProvider { public serverURL: string; @@ -122,4 +124,32 @@ export class CustomOAuth implements IOAuthProvider { }, }); } + + static configureOAuthService(serviceName: string, options: OauthConfig): CustomOAuth { + const existingInstance = configuredOAuthServices.get(serviceName); + if (existingInstance) { + existingInstance.configure(options); + return existingInstance; + } + + // If we don't have a reference to the instance for this service and it was already registered on meteor, + // then there's nothing we can do to update it + if (Accounts.oauth.serviceNames().includes(serviceName)) { + throw new Error(`CustomOAuth service [${serviceName}] already registered, skipping new configuration.`); + } + + const instance = new CustomOAuth(serviceName, options); + configuredOAuthServices.set(serviceName, instance); + return instance; + } + + static configureCustomOAuthService(serviceName: string, options: OauthConfig): CustomOAuth | undefined { + // Custom OAuth services are configured based on the login service list, so if this ends up being called multiple times, simply ignore it + // Non-Custom OAuth services are configured based on code, so if configureOAuthService is called multiple times for them, it's a bug and it should throw. + try { + return this.configureOAuthService(serviceName, options); + } catch (e) { + console.error(e); + } + } } diff --git a/apps/meteor/app/custom-sounds/client/index.ts b/apps/meteor/app/custom-sounds/client/index.ts deleted file mode 100644 index 95992988ccfb1..0000000000000 --- a/apps/meteor/app/custom-sounds/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CustomSounds } from './lib/CustomSounds'; diff --git a/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts b/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts deleted file mode 100644 index f925caf7f8098..0000000000000 --- a/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts +++ /dev/null @@ -1,147 +0,0 @@ -import type { ICustomSound } from '@rocket.chat/core-typings'; -import { ReactiveVar } from 'meteor/reactive-var'; - -import { getURL } from '../../../utils/client'; -import { sdk } from '../../../utils/client/lib/SDKClient'; - -const getCustomSoundId = (soundId: ICustomSound['_id']) => `custom-sound-${soundId}`; -const getAssetUrl = (asset: string, params?: Record) => getURL(asset, params, undefined, true); - -const defaultSounds = [ - { _id: 'chime', name: 'Chime', extension: 'mp3', src: getAssetUrl('sounds/chime.mp3') }, - { _id: 'door', name: 'Door', extension: 'mp3', src: getAssetUrl('sounds/door.mp3') }, - { _id: 'beep', name: 'Beep', extension: 'mp3', src: getAssetUrl('sounds/beep.mp3') }, - { _id: 'chelle', name: 'Chelle', extension: 'mp3', src: getAssetUrl('sounds/chelle.mp3') }, - { _id: 'ding', name: 'Ding', extension: 'mp3', src: getAssetUrl('sounds/ding.mp3') }, - { _id: 'droplet', name: 'Droplet', extension: 'mp3', src: getAssetUrl('sounds/droplet.mp3') }, - { _id: 'highbell', name: 'Highbell', extension: 'mp3', src: getAssetUrl('sounds/highbell.mp3') }, - { _id: 'seasons', name: 'Seasons', extension: 'mp3', src: getAssetUrl('sounds/seasons.mp3') }, - { _id: 'telephone', name: 'Telephone', extension: 'mp3', src: getAssetUrl('sounds/telephone.mp3') }, - { _id: 'outbound-call-ringing', name: 'Outbound Call Ringing', extension: 'mp3', src: getAssetUrl('sounds/outbound-call-ringing.mp3') }, - { _id: 'call-ended', name: 'Call Ended', extension: 'mp3', src: getAssetUrl('sounds/call-ended.mp3') }, - { _id: 'dialtone', name: 'Dialtone', extension: 'mp3', src: getAssetUrl('sounds/dialtone.mp3') }, - { _id: 'ringtone', name: 'Ringtone', extension: 'mp3', src: getAssetUrl('sounds/ringtone.mp3') }, -]; - -class CustomSoundsClass { - list: ReactiveVar>; - - initialFetchDone: boolean; - - constructor() { - this.list = new ReactiveVar({}); - this.initialFetchDone = false; - defaultSounds.forEach((sound) => this.add(sound)); - } - - add(sound: ICustomSound) { - if (!sound.src) { - sound.src = this.getURL(sound); - } - - const source = document.createElement('source'); - source.src = sound.src; - - const audio = document.createElement('audio'); - audio.id = getCustomSoundId(sound._id); - audio.preload = 'none'; - audio.appendChild(source); - - document.body.appendChild(audio); - - const list = this.list.get(); - list[sound._id] = sound; - this.list.set(list); - } - - remove(sound: ICustomSound) { - const list = this.list.get(); - delete list[sound._id]; - this.list.set(list); - const audio = document.querySelector(`#${getCustomSoundId(sound._id)}`); - audio?.remove(); - } - - getSound(soundId: ICustomSound['_id']) { - const list = this.list.get(); - return list[soundId]; - } - - update(sound: ICustomSound) { - const audio = document.querySelector(`#${getCustomSoundId(sound._id)}`); - if (audio) { - const list = this.list.get(); - if (!sound.src) { - sound.src = this.getURL(sound); - } - list[sound._id] = sound; - this.list.set(list); - const sourceEl = audio.querySelector('source'); - if (sourceEl) { - sourceEl.src = sound.src; - } - audio.load(); - } else { - this.add(sound); - } - } - - getURL(sound: ICustomSound) { - return getAssetUrl(`/custom-sounds/${sound._id}.${sound.extension}`, { _dc: sound.random || 0 }); - } - - getList() { - const list = Object.values(this.list.get()); - return list.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? '')); - } - - play = async (soundId: ICustomSound['_id'], { volume = 1, loop = false } = {}) => { - const audio = document.querySelector(`#${getCustomSoundId(soundId)}`); - if (!audio?.play) { - return; - } - - audio.volume = volume; - audio.loop = loop; - await audio.play(); - - return audio; - }; - - pause = (soundId: ICustomSound['_id']) => { - const audio = document.querySelector(`#${getCustomSoundId(soundId)}`); - if (!audio?.pause) { - return; - } - - audio.pause(); - }; - - stop = (soundId: ICustomSound['_id']) => { - const audio = document.querySelector(`#${getCustomSoundId(soundId)}`); - if (!audio?.load) { - return; - } - - audio?.load(); - }; - - isPlaying = (soundId: ICustomSound['_id']) => { - const audio = document.querySelector(`#${getCustomSoundId(soundId)}`); - - return audio && audio.duration > 0 && !audio.paused; - }; - - fetchCustomSoundList = async () => { - if (this.initialFetchDone) { - return; - } - const result = await sdk.call('listCustomSounds'); - for (const sound of result) { - this.add(sound); - } - this.initialFetchDone = true; - }; -} - -export const CustomSounds = new CustomSoundsClass(); diff --git a/apps/meteor/app/dolphin/client/hooks/useDolphin.ts b/apps/meteor/app/dolphin/client/hooks/useDolphin.ts index 7a9d700238c95..7ae3266f5a824 100644 --- a/apps/meteor/app/dolphin/client/hooks/useDolphin.ts +++ b/apps/meteor/app/dolphin/client/hooks/useDolphin.ts @@ -16,7 +16,7 @@ const config = { accessTokenParam: 'access_token', }; -const Dolphin = new CustomOAuth('dolphin', config); +const Dolphin = CustomOAuth.configureOAuthService('dolphin', config); export const useDolphin = () => { const enabled = useSetting('Accounts_OAuth_Dolphin'); diff --git a/apps/meteor/app/drupal/client/hooks/useDrupal.ts b/apps/meteor/app/drupal/client/hooks/useDrupal.ts index 33295b33684a3..f4808caa78b93 100644 --- a/apps/meteor/app/drupal/client/hooks/useDrupal.ts +++ b/apps/meteor/app/drupal/client/hooks/useDrupal.ts @@ -23,7 +23,7 @@ const config: OauthConfig = { accessTokenParam: 'access_token', }; -const Drupal = new CustomOAuth('drupal', config); +const Drupal = CustomOAuth.configureOAuthService('drupal', config); export const useDrupal = () => { const drupalUrl = useSetting('API_Drupal_URL') as string; diff --git a/apps/meteor/app/e2e/server/methods/getUsersOfRoomWithoutKey.ts b/apps/meteor/app/e2e/server/methods/getUsersOfRoomWithoutKey.ts index 1f1a21262de86..80480bd55c4f4 100644 --- a/apps/meteor/app/e2e/server/methods/getUsersOfRoomWithoutKey.ts +++ b/apps/meteor/app/e2e/server/methods/getUsersOfRoomWithoutKey.ts @@ -13,6 +13,27 @@ declare module '@rocket.chat/ddp-client' { } } +export const getUsersOfRoomWithoutKeyMethod = async ( + userId: string, + rid: IRoom['_id'], +): Promise<{ users: Pick[] }> => { + if (!(await canAccessRoomIdAsync(rid, userId))) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.getUsersOfRoomWithoutKey' }); + } + + const subscriptions = await Subscriptions.findByRidWithoutE2EKey(rid, { + projection: { 'u._id': 1 }, + }).toArray(); + const userIds = subscriptions.map((s) => s.u._id); + const options = { projection: { 'e2e.public_key': 1 } }; + + const users = await Users.findByIdsWithPublicE2EKey(userIds, options).toArray(); + + return { + users, + }; +}; + Meteor.methods({ async 'e2e.getUsersOfRoomWithoutKey'(rid) { check(rid, String); @@ -30,20 +51,6 @@ Meteor.methods({ }); } - if (!(await canAccessRoomIdAsync(rid, userId))) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.getUsersOfRoomWithoutKey' }); - } - - const subscriptions = await Subscriptions.findByRidWithoutE2EKey(rid, { - projection: { 'u._id': 1 }, - }).toArray(); - const userIds = subscriptions.map((s) => s.u._id); - const options = { projection: { 'e2e.public_key': 1 } }; - - const users = await Users.findByIdsWithPublicE2EKey(userIds, options).toArray(); - - return { - users, - }; + return getUsersOfRoomWithoutKeyMethod(userId, rid); }, }); diff --git a/apps/meteor/app/e2e/server/methods/setRoomKeyID.ts b/apps/meteor/app/e2e/server/methods/setRoomKeyID.ts index b52913e4f9849..a9b4ae5dda49e 100644 --- a/apps/meteor/app/e2e/server/methods/setRoomKeyID.ts +++ b/apps/meteor/app/e2e/server/methods/setRoomKeyID.ts @@ -14,6 +14,28 @@ declare module '@rocket.chat/ddp-client' { } } +export const setRoomKeyIDMethod = async (userId: string, rid: IRoom['_id'], keyID: string): Promise => { + if (!(await canAccessRoomIdAsync(rid, userId))) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.setRoomKeyID' }); + } + + const room = await Rooms.findOneById>(rid, { projection: { e2eKeyId: 1 } }); + + if (!room) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.setRoomKeyID' }); + } + + if (room.e2eKeyId) { + throw new Meteor.Error('error-room-e2e-key-already-exists', 'E2E Key ID already exists', { + method: 'e2e.setRoomKeyID', + }); + } + + await Rooms.setE2eKeyId(room._id, keyID); + + void notifyOnRoomChangedById(room._id); +}; + Meteor.methods({ async 'e2e.setRoomKeyID'(rid, keyID) { check(rid, String); @@ -28,24 +50,6 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.setRoomKeyID' }); } - if (!(await canAccessRoomIdAsync(rid, userId))) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.setRoomKeyID' }); - } - - const room = await Rooms.findOneById>(rid, { projection: { e2eKeyId: 1 } }); - - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.setRoomKeyID' }); - } - - if (room.e2eKeyId) { - throw new Meteor.Error('error-room-e2e-key-already-exists', 'E2E Key ID already exists', { - method: 'e2e.setRoomKeyID', - }); - } - - await Rooms.setE2eKeyId(room._id, keyID); - - void notifyOnRoomChangedById(room._id); + await setRoomKeyIDMethod(userId, rid, keyID); }, }); diff --git a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts index 45a00886af1e1..da5f93090e1b8 100644 --- a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts +++ b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts @@ -11,6 +11,35 @@ declare module '@rocket.chat/ddp-client' { } } +const isKeysResult = (result: any): result is { public_key: string; private_key: string } => { + return result.private_key && result.public_key; +}; + +export const setUserPublicAndPrivateKeysMethod = async ( + userId: string, + keyPair: { public_key: string; private_key: string; force?: boolean }, +): Promise => { + if (!keyPair.force) { + const keys = await Users.fetchKeysByUserId(userId); + + if (isKeysResult(keys)) { + throw new Meteor.Error('error-keys-already-set', 'Keys already set', { + method: 'e2e.setUserPublicAndPrivateKeys', + }); + } + } + + await Users.setE2EPublicAndPrivateKeysByUserId(userId, { + private_key: keyPair.private_key, + public_key: keyPair.public_key, + }); + + const subscribedRoomIds = await Rooms.getSubscribedRoomIdsWithoutE2EKeys(userId); + await Rooms.addUserIdToE2EEQueueByRoomIds(subscribedRoomIds, userId); + + void notifyOnRoomChangedById(subscribedRoomIds); +}; + Meteor.methods({ async 'e2e.setUserPublicAndPrivateKeys'(keyPair) { const userId = Meteor.userId(); @@ -21,24 +50,12 @@ Meteor.methods({ }); } - if (!keyPair.force) { - const keys = await Users.fetchKeysByUserId(userId); - - if (keys.private_key && keys.public_key) { - throw new Meteor.Error('error-keys-already-set', 'Keys already set', { - method: 'e2e.setUserPublicAndPrivateKeys', - }); - } + if (!keyPair.public_key || !keyPair.private_key) { + throw new Meteor.Error('error-invalid-keys', 'Invalid keys', { + method: 'e2e.setUserPublicAndPrivateKeys', + }); } - await Users.setE2EPublicAndPrivateKeysByUserId(userId, { - private_key: keyPair.private_key, - public_key: keyPair.public_key, - }); - - const subscribedRoomIds = await Rooms.getSubscribedRoomIdsWithoutE2EKeys(userId); - await Rooms.addUserIdToE2EEQueueByRoomIds(subscribedRoomIds, userId); - - void notifyOnRoomChangedById(subscribedRoomIds); + await setUserPublicAndPrivateKeysMethod(userId, keyPair); }, }); diff --git a/apps/meteor/app/ecdh/Session.ts b/apps/meteor/app/ecdh/Session.ts index e530d9cdb1294..47728667b9071 100644 --- a/apps/meteor/app/ecdh/Session.ts +++ b/apps/meteor/app/ecdh/Session.ts @@ -41,7 +41,9 @@ export class Session { const sodium = await this.sodium(); const nonce = await sodium.randombytes_buf(24); - const ciphertext = await sodium.crypto_secretbox(Buffer.from(plaintext).toString(this.stringFormatRawData), nonce, this.encryptKey); + const buffer = Buffer.isBuffer(plaintext) ? plaintext : Buffer.from(plaintext); + + const ciphertext = await sodium.crypto_secretbox(Buffer.from(buffer).toString(this.stringFormatRawData), nonce, this.encryptKey); return Buffer.concat([nonce, ciphertext]); } diff --git a/apps/meteor/app/emoji-custom/client/index.ts b/apps/meteor/app/emoji-custom/client/index.ts deleted file mode 100644 index 780a12a3898f5..0000000000000 --- a/apps/meteor/app/emoji-custom/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './lib/emojiCustom'; diff --git a/apps/meteor/app/emoji-custom/server/methods/deleteEmojiCustom.ts b/apps/meteor/app/emoji-custom/server/methods/deleteEmojiCustom.ts index c16d3b82449bf..ce6aaeaf417f6 100644 --- a/apps/meteor/app/emoji-custom/server/methods/deleteEmojiCustom.ts +++ b/apps/meteor/app/emoji-custom/server/methods/deleteEmojiCustom.ts @@ -14,23 +14,31 @@ declare module '@rocket.chat/ddp-client' { } } +export const deleteEmojiCustom = async (userId: string, emojiID: ICustomEmojiDescriptor['_id']): Promise => { + if (!(await hasPermissionAsync(userId, 'manage-emoji'))) { + throw new Meteor.Error('not_authorized'); + } + + const emoji = await EmojiCustom.findOneById(emojiID); + if (emoji == null) { + throw new Meteor.Error('Custom_Emoji_Error_Invalid_Emoji', 'Invalid emoji', { + method: 'deleteEmojiCustom', + }); + } + + await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emoji.name}.${emoji.extension}`)); + await EmojiCustom.removeById(emojiID); + void api.broadcast('emoji.deleteCustom', emoji); + + return true; +}; + Meteor.methods({ async deleteEmojiCustom(emojiID) { - if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-emoji'))) { + if (!this.userId) { throw new Meteor.Error('not_authorized'); } - const emoji = await EmojiCustom.findOneById(emojiID); - if (emoji == null) { - throw new Meteor.Error('Custom_Emoji_Error_Invalid_Emoji', 'Invalid emoji', { - method: 'deleteEmojiCustom', - }); - } - - await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emoji.name}.${emoji.extension}`)); - await EmojiCustom.removeById(emojiID); - void api.broadcast('emoji.deleteCustom', emoji); - - return true; + return deleteEmojiCustom(this.userId, emojiID); }, }); diff --git a/apps/meteor/app/emoji/client/helpers.ts b/apps/meteor/app/emoji/client/helpers.ts index cbd1b08b687de..d36dc958cf20e 100644 --- a/apps/meteor/app/emoji/client/helpers.ts +++ b/apps/meteor/app/emoji/client/helpers.ts @@ -21,15 +21,17 @@ export const createEmojiListByCategorySubscription = ( actualTone: number, recentEmojis: string[], setRecentEmojis: (emojis: string[]) => void, + setQuickReactions: () => void, ): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => ReturnType] => { let result: ReturnType = [[], []]; updateRecent(recentEmojis); const sub = (cb: () => void) => { result = createPickerEmojis(customItemsLimit, actualTone, recentEmojis, setRecentEmojis); - + setQuickReactions(); return emojiEmitter.on('updated', () => { result = createPickerEmojis(customItemsLimit, actualTone, recentEmojis, setRecentEmojis); + setQuickReactions(); cb(); }); }; diff --git a/apps/meteor/app/federation/server/endpoints/dispatch.js b/apps/meteor/app/federation/server/endpoints/dispatch.js index d80f74bf18d35..65a675722bba2 100644 --- a/apps/meteor/app/federation/server/endpoints/dispatch.js +++ b/apps/meteor/app/federation/server/endpoints/dispatch.js @@ -1,6 +1,7 @@ import { api } from '@rocket.chat/core-services'; import { eventTypes } from '@rocket.chat/core-typings'; import { FederationServers, FederationRoomEvents, Rooms, Messages, Subscriptions, Users, ReadReceipts } from '@rocket.chat/models'; +import { removeEmpty } from '@rocket.chat/tools'; import EJSON from 'ejson'; import { API } from '../../../api/server'; @@ -120,8 +121,8 @@ const eventHandlers = { if (persistedUser) { // Update the federation, if its not already set (if it's set, this is likely an event being reprocessed) - if (!persistedUser.federation) { - await Users.updateOne({ _id: persistedUser._id }, { $set: { federation: user.federation } }); + if (!persistedUser.federation && user.federation) { + await Users.updateOne({ _id: persistedUser._id }, { $set: { federation: removeEmpty(user.federation) } }); federationAltered = true; } } else { @@ -139,8 +140,11 @@ const eventHandlers = { try { if (persistedSubscription) { // Update the federation, if its not already set (if it's set, this is likely an event being reprocessed - if (!persistedSubscription.federation) { - await Subscriptions.updateOne({ _id: persistedSubscription._id }, { $set: { federation: subscription.federation } }); + if (!persistedSubscription.federation && subscription.federation) { + await Subscriptions.updateOne( + { _id: persistedSubscription._id }, + { $set: { federation: removeEmpty(subscription.federation) } }, + ); federationAltered = true; } } else { @@ -148,7 +152,7 @@ const eventHandlers = { const denormalizedSubscription = normalizers.denormalizeSubscription(subscription); // Create the subscription - const { insertedId } = await Subscriptions.insertOne(denormalizedSubscription); + const { insertedId } = await Subscriptions.insertOne(removeEmpty(denormalizedSubscription)); if (insertedId) { void notifyOnSubscriptionChangedById(insertedId); } diff --git a/apps/meteor/app/federation/server/functions/addUser.js b/apps/meteor/app/federation/server/functions/addUser.js index 8420a47944af7..0cddb226d263a 100644 --- a/apps/meteor/app/federation/server/functions/addUser.js +++ b/apps/meteor/app/federation/server/functions/addUser.js @@ -1,4 +1,5 @@ import { FederationServers, Users } from '@rocket.chat/models'; +import { removeEmpty } from '@rocket.chat/tools'; import { Meteor } from 'meteor/meteor'; import { getUserByUsername } from '../handler'; @@ -19,7 +20,7 @@ export async function addUser(query) { try { // Create the local user - userId = await Users.create(user); + userId = await Users.create(removeEmpty(user)); // Refresh the servers list await FederationServers.refreshServers(); diff --git a/apps/meteor/app/file/server/file.server.ts b/apps/meteor/app/file/server/file.server.ts index 94163e314145b..0af01c1bfdab7 100644 --- a/apps/meteor/app/file/server/file.server.ts +++ b/apps/meteor/app/file/server/file.server.ts @@ -183,8 +183,11 @@ class FileSystem implements IRocketChatFileStore { } return new Promise((resolve) => { const data: Buffer[] = []; - file.readStream.on('data', (chunk: Buffer) => { - return data.push(chunk); + file.readStream.on('data', (chunk) => { + if (Buffer.isBuffer(chunk)) { + return data.push(chunk); + } + return data.push(Buffer.from(chunk)); }); file.readStream.on('end', () => { resolve({ @@ -210,7 +213,7 @@ export const RocketChatFile = { }, dataURIParse(dataURI: string | Buffer) { - const imageData = Buffer.from(dataURI).toString().split(';base64,'); + const imageData = (Buffer.isBuffer(dataURI) ? dataURI : Buffer.from(dataURI)).toString().split(';base64,'); return { image: imageData[1], contentType: imageData[0].replace('data:', ''), diff --git a/apps/meteor/app/github-enterprise/client/hooks/useGitHubEnterpriseAuth.ts b/apps/meteor/app/github-enterprise/client/hooks/useGitHubEnterpriseAuth.ts index f61af7bde79e2..79af68ef234aa 100644 --- a/apps/meteor/app/github-enterprise/client/hooks/useGitHubEnterpriseAuth.ts +++ b/apps/meteor/app/github-enterprise/client/hooks/useGitHubEnterpriseAuth.ts @@ -18,7 +18,7 @@ const config: OauthConfig = { }, }; -const GitHubEnterprise = new CustomOAuth('github_enterprise', config); +const GitHubEnterprise = CustomOAuth.configureOAuthService('github_enterprise', config); export const useGitHubEnterpriseAuth = () => { const githubApiUrl = useSetting('API_GitHub_Enterprise_URL') as string; diff --git a/apps/meteor/app/gitlab/client/hooks/useGitLabAuth.ts b/apps/meteor/app/gitlab/client/hooks/useGitLabAuth.ts index e5f7c32c701e6..c5776723e4981 100644 --- a/apps/meteor/app/gitlab/client/hooks/useGitLabAuth.ts +++ b/apps/meteor/app/gitlab/client/hooks/useGitLabAuth.ts @@ -16,7 +16,7 @@ const config: OauthConfig = { accessTokenParam: 'access_token', }; -const Gitlab = new CustomOAuth('gitlab', config); +const Gitlab = CustomOAuth.configureOAuthService('gitlab', config); export const useGitLabAuth = () => { const gitlabApiUrl = useSetting('API_Gitlab_URL') as string; diff --git a/apps/meteor/app/importer/server/classes/converters/ConverterCache.ts b/apps/meteor/app/importer/server/classes/converters/ConverterCache.ts index 284e51dddcd57..3dec0ab78d624 100644 --- a/apps/meteor/app/importer/server/classes/converters/ConverterCache.ts +++ b/apps/meteor/app/importer/server/classes/converters/ConverterCache.ts @@ -215,7 +215,9 @@ export class ConverterCache { } const user = await Users.findOneByUsername>(username, { projection: { _id: 1 } }); - this.addUsernameToId(username, user?._id); + if (user) { + this.addUsernameToId(username, user._id); + } return user?._id; } diff --git a/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts b/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts index e9a14fed6a038..ba7d01b0009ee 100644 --- a/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts @@ -1,5 +1,6 @@ import type { IImportChannel, IImportChannelRecord, IRoom } from '@rocket.chat/core-typings'; import { Subscriptions, Rooms, Users } from '@rocket.chat/models'; +import { removeEmpty } from '@rocket.chat/tools'; import limax from 'limax'; import { RecordConverter } from './RecordConverter'; @@ -150,7 +151,7 @@ export class RoomConverter extends RecordConverter { const roomUpdate: { $set?: Record; $addToSet?: Record } = {}; if (Object.keys(set).length > 0) { - roomUpdate.$set = set; + roomUpdate.$set = removeEmpty(set); } if (roomData.importIds.length) { diff --git a/apps/meteor/app/importer/server/classes/converters/UserConverter.ts b/apps/meteor/app/importer/server/classes/converters/UserConverter.ts index 1b411100e13c1..5074443c8995a 100644 --- a/apps/meteor/app/importer/server/classes/converters/UserConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/UserConverter.ts @@ -127,7 +127,7 @@ export class UserConverter extends RecordConverter { + async findExistingUser(data: IImportUser): Promise { if (data.emails.length) { const emailUser = await Users.findOneByEmailAddress(data.emails[0], {}); @@ -138,7 +138,7 @@ export class UserConverter extends RecordConverter(data.username, {}); } } @@ -201,7 +201,7 @@ export class UserConverter extends RecordConverter, currentPath: string): void => { for (const key in source) { - if (!source.hasOwnProperty(key)) { + if (!source.hasOwnProperty(key) || source[key] === undefined) { continue; } @@ -221,7 +221,7 @@ export class UserConverter extends RecordConverter { + async insertOrUpdateUser(existingUser: IUser | null | undefined, data: IImportUser): Promise { if (!data.username && !existingUser?.username) { const emails = data.emails.filter(Boolean).map((email) => ({ address: email })); data.username = await generateUsernameSuggestion({ @@ -259,6 +259,10 @@ export class UserConverter extends RecordConverter[0]); + const localUsername = userData.federated ? undefined : userData.username; + if (userData.name || localUsername) { + await saveUserIdentity({ _id, name: userData.name, username: localUsername } as Parameters[0]); } if (userData.importIds.length) { @@ -347,6 +354,7 @@ export class UserConverter extends RecordConverter { - if (req.headers['content-type'] !== 'application/x-www-form-urlencoded') { - return next(); - } - - // make sure body has only one key and it is 'payload' - if (!req.body || typeof req.body !== 'object' || !('payload' in req.body) || Object.keys(req.body).length !== 1) { +const middleware = async (c, next) => { + const { req } = c; + if (req.raw.headers.get('content-type') !== 'application/x-www-form-urlencoded') { return next(); } try { - req.bodyParams = JSON.parse(req.body.payload); + const content = await req.raw.clone().text(); + const body = Object.fromEntries(new URLSearchParams(content)); + if (!body || typeof body !== 'object' || Object.keys(body).length !== 1) { + return next(); + } - return next(); + if (body.payload) { + // need to compose the full payload in this weird way because body-parser thought it was a form + c.set('bodyParams-override', JSON.parse(body.payload)); + return next(); + } + incomingLogger.debug({ + msg: 'Body received as application/x-www-form-urlencoded without the "payload" key, parsed as string', + content, + }); + c.set('bodyParams-override', JSON.parse(content)); } catch (e) { - res.writeHead(400); - res.end(JSON.stringify({ success: false, error: e.message })); + c.body(JSON.stringify({ success: false, error: e.message }), 400); } return next(); -}); +}; + +// middleware for special requests that are urlencoded but have a json payload (like GitHub webhooks) +Api.router.use(middleware); Api.addRoute( ':integrationId/:userId/:token', @@ -419,5 +434,5 @@ Api.addRoute( ); Meteor.startup(() => { - WebApp.connectHandlers.use(Api.router.router); + WebApp.rawConnectHandlers.use(Api.router.router); }); diff --git a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts index 1ce65a03deb00..774d7a0d597a0 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts @@ -23,181 +23,197 @@ declare module '@rocket.chat/ddp-client' { } } -Meteor.methods({ - // eslint-disable-next-line complexity - async updateIncomingIntegration(integrationId, integration) { - if (!this.userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'updateOutgoingIntegration', - }); - } +function validateChannels(channelString: string | undefined): string[] { + if (!channelString || typeof channelString.valueOf() !== 'string' || channelString.trim() === '') { + throw new Meteor.Error('error-invalid-channel', 'Invalid channel', { + method: 'updateIncomingIntegration', + }); + } + + const channels = channelString.split(',').map((channel) => channel.trim()); - if (!integration.channel || typeof integration.channel.valueOf() !== 'string' || integration.channel.trim() === '') { - throw new Meteor.Error('error-invalid-channel', 'Invalid channel', { + for (const channel of channels) { + if (!validChannelChars.includes(channel[0])) { + throw new Meteor.Error('error-invalid-channel-start-with-chars', 'Invalid channel. Start with @ or #', { method: 'updateIncomingIntegration', }); } + } - const channels = integration.channel.split(',').map((channel) => channel.trim()); + return channels; +} - for (const channel of channels) { - if (!validChannelChars.includes(channel[0])) { - throw new Meteor.Error('error-invalid-channel-start-with-chars', 'Invalid channel. Start with @ or #', { - method: 'updateIncomingIntegration', - }); - } - } +export const updateIncomingIntegration = async ( + userId: string, + integrationId: string, + integration: INewIncomingIntegration | IUpdateIncomingIntegration, +): Promise => { + const channels = validateChannels(integration.channel); + + let currentIntegration; + + if (await hasPermissionAsync(userId, 'manage-incoming-integrations')) { + currentIntegration = await Integrations.findOneById(integrationId); + } else if (await hasPermissionAsync(userId, 'manage-own-incoming-integrations')) { + currentIntegration = await Integrations.findOne({ + '_id': integrationId, + '_createdBy._id': userId, + }); + } else { + throw new Meteor.Error('not_authorized', 'Unauthorized', { + method: 'updateIncomingIntegration', + }); + } - let currentIntegration; + if (!currentIntegration) { + throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { + method: 'updateIncomingIntegration', + }); + } - if (await hasPermissionAsync(this.userId, 'manage-incoming-integrations')) { - currentIntegration = await Integrations.findOneById(integrationId); - } else if (await hasPermissionAsync(this.userId, 'manage-own-incoming-integrations')) { - currentIntegration = await Integrations.findOne({ - '_id': integrationId, - '_createdBy._id': this.userId, - }); - } else { - throw new Meteor.Error('not_authorized', 'Unauthorized', { - method: 'updateIncomingIntegration', - }); - } + const oldScriptEngine = currentIntegration.scriptEngine; + const scriptEngine = integration.scriptEngine ?? oldScriptEngine ?? 'isolated-vm'; + if ( + integration.script?.trim() && + (scriptEngine !== oldScriptEngine || integration.script?.trim() !== currentIntegration.script?.trim()) + ) { + wrapExceptions(() => validateScriptEngine(scriptEngine)).catch((e) => { + throw new Meteor.Error(e.message); + }); + } - if (!currentIntegration) { - throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { - method: 'updateIncomingIntegration', - }); - } + const isFrozen = isScriptEngineFrozen(scriptEngine); - const oldScriptEngine = currentIntegration.scriptEngine; - const scriptEngine = integration.scriptEngine ?? oldScriptEngine ?? 'isolated-vm'; - if ( - integration.script?.trim() && - (scriptEngine !== oldScriptEngine || integration.script?.trim() !== currentIntegration.script?.trim()) - ) { - wrapExceptions(() => validateScriptEngine(scriptEngine)).catch((e) => { - throw new Meteor.Error(e.message); - }); - } + if (!isFrozen) { + let scriptCompiled: string | undefined; + let scriptError: Pick | undefined; - const isFrozen = isScriptEngineFrozen(scriptEngine); - - if (!isFrozen) { - let scriptCompiled: string | undefined; - let scriptError: Pick | undefined; - - if (integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') { - try { - let babelOptions = Babel.getDefaultOptions({ runtime: false }); - babelOptions = _.extend(babelOptions, { compact: true, minified: true, comments: false }); - - scriptCompiled = Babel.compile(integration.script, babelOptions).code; - scriptError = undefined; - await Integrations.updateOne( - { _id: integrationId }, - { - $set: { - scriptCompiled, - }, - $unset: { scriptError: 1 as const }, - }, - ); - } catch (e) { - scriptCompiled = undefined; - if (e instanceof Error) { - const { name, message, stack } = e; - scriptError = { name, message, stack }; - } - await Integrations.updateOne( - { _id: integrationId }, - { - $set: { - scriptError, - }, - $unset: { - scriptCompiled: 1 as const, - }, + if (integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') { + try { + let babelOptions = Babel.getDefaultOptions({ runtime: false }); + babelOptions = _.extend(babelOptions, { compact: true, minified: true, comments: false }); + + scriptCompiled = Babel.compile(integration.script, babelOptions).code; + scriptError = undefined; + await Integrations.updateOne( + { _id: integrationId }, + { + $set: { + scriptCompiled, }, - ); + $unset: { scriptError: 1 as const }, + }, + ); + } catch (e) { + scriptCompiled = undefined; + if (e instanceof Error) { + const { name, message, stack } = e; + scriptError = { name, message, stack }; } + await Integrations.updateOne( + { _id: integrationId }, + { + $set: { + scriptError, + }, + $unset: { + scriptCompiled: 1 as const, + }, + }, + ); } } + } - for await (let channel of channels) { - const channelType = channel[0]; - channel = channel.slice(1); - let record; - - switch (channelType) { - case '#': - record = await Rooms.findOne({ - $or: [{ _id: channel }, { name: channel }], - }); - break; - case '@': - record = await Users.findOne({ - $or: [{ _id: channel }, { username: channel }], - }); - break; - } + for await (let channel of channels) { + const channelType = channel[0]; + channel = channel.slice(1); + let record; - if (!record) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { - method: 'updateIncomingIntegration', + switch (channelType) { + case '#': + record = await Rooms.findOne({ + $or: [{ _id: channel }, { name: channel }], }); - } - - if ( - !(await hasAllPermissionAsync(this.userId, ['manage-incoming-integrations', 'manage-own-incoming-integrations'])) && - !(await Subscriptions.findOneByRoomIdAndUserId(record._id, this.userId, { projection: { _id: 1 } })) - ) { - throw new Meteor.Error('error-invalid-channel', 'Invalid Channel', { - method: 'updateIncomingIntegration', + break; + case '@': + record = await Users.findOne({ + $or: [{ _id: channel }, { username: channel }], }); - } + break; } - const user = await Users.findOne({ username: currentIntegration.username }); + if (!record) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { + method: 'updateIncomingIntegration', + }); + } - if (!user?._id) { - throw new Meteor.Error('error-invalid-post-as-user', 'Invalid Post As User', { + if ( + !(await hasAllPermissionAsync(userId, ['manage-incoming-integrations', 'manage-own-incoming-integrations'])) && + !(await Subscriptions.findOneByRoomIdAndUserId(record._id, userId, { projection: { _id: 1 } })) + ) { + throw new Meteor.Error('error-invalid-channel', 'Invalid Channel', { method: 'updateIncomingIntegration', }); } + } + + const username = 'username' in integration ? integration.username : currentIntegration.username; + const user = await Users.findOne({ username }); + + if (!user?._id) { + throw new Meteor.Error('error-invalid-post-as-user', 'Invalid Post As User', { + method: 'updateIncomingIntegration', + }); + } - await addUserRolesAsync(user._id, ['bot']); - - const updatedIntegration = await Integrations.findOneAndUpdate( - { _id: integrationId }, - { - $set: { - enabled: integration.enabled, - name: integration.name, - avatar: integration.avatar, - emoji: integration.emoji, - alias: integration.alias, - channel: channels, - ...('username' in integration && { username: integration.username }), - ...(isFrozen - ? {} - : { - script: integration.script, - scriptEnabled: integration.scriptEnabled, - scriptEngine, - }), - ...(typeof integration.overrideDestinationChannelEnabled !== 'undefined' && { - overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled, - }), - _updatedAt: new Date(), - _updatedBy: await Users.findOne({ _id: this.userId }, { projection: { username: 1 } }), - }, + await addUserRolesAsync(user._id, ['bot']); + + const updatedIntegration = await Integrations.findOneAndUpdate( + { _id: integrationId }, + { + $set: { + enabled: integration.enabled, + name: integration.name, + avatar: integration.avatar, + emoji: integration.emoji, + alias: integration.alias, + channel: channels, + ...('username' in integration && { username: user.username, userId: user._id }), + ...(isFrozen + ? {} + : { + script: integration.script, + scriptEnabled: integration.scriptEnabled, + scriptEngine, + }), + ...(typeof integration.overrideDestinationChannelEnabled !== 'undefined' && { + overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled, + }), + _updatedAt: new Date(), + _updatedBy: await Users.findOne({ _id: userId }, { projection: { username: 1 } }), }, - ); + }, + { returnDocument: 'after' }, + ); + + if (updatedIntegration) { + void notifyOnIntegrationChanged(updatedIntegration); + } + + return updatedIntegration; +}; - if (updatedIntegration) { - void notifyOnIntegrationChanged(updatedIntegration); +Meteor.methods({ + // eslint-disable-next-line complexity + async updateIncomingIntegration(integrationId, integration) { + if (!this.userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'updateOutgoingIntegration', + }); } - return updatedIntegration; + return updateIncomingIntegration(this.userId, integrationId, integration); }, }); diff --git a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts index e4c1c48e04875..29b150a60011a 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts @@ -19,102 +19,111 @@ declare module '@rocket.chat/ddp-client' { } } -Meteor.methods({ - async updateOutgoingIntegration(integrationId, _integration) { - if (!this.userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'updateOutgoingIntegration', - }); - } - - const integration = await validateOutgoingIntegration(_integration, this.userId); +export const updateOutgoingIntegration = async ( + userId: string, + integrationId: string, + _integration: INewOutgoingIntegration | IUpdateOutgoingIntegration, +): Promise => { + const integration = await validateOutgoingIntegration(_integration, userId); - if (!integration.token || integration.token.trim() === '') { - throw new Meteor.Error('error-invalid-token', 'Invalid token', { - method: 'updateOutgoingIntegration', - }); - } + if (!integration.token || integration.token.trim() === '') { + throw new Meteor.Error('error-invalid-token', 'Invalid token', { + method: 'updateOutgoingIntegration', + }); + } - let currentIntegration: IIntegration | null; + let currentIntegration: IIntegration | null; - if (await hasPermissionAsync(this.userId, 'manage-outgoing-integrations')) { - currentIntegration = await Integrations.findOneById(integrationId); - } else if (await hasPermissionAsync(this.userId, 'manage-own-outgoing-integrations')) { - currentIntegration = await Integrations.findOne({ - '_id': integrationId, - '_createdBy._id': this.userId, - }); - } else { - throw new Meteor.Error('not_authorized', 'Unauthorized', { - method: 'updateOutgoingIntegration', - }); - } + if (await hasPermissionAsync(userId, 'manage-outgoing-integrations')) { + currentIntegration = await Integrations.findOneById(integrationId); + } else if (await hasPermissionAsync(userId, 'manage-own-outgoing-integrations')) { + currentIntegration = await Integrations.findOne({ + '_id': integrationId, + '_createdBy._id': userId, + }); + } else { + throw new Meteor.Error('not_authorized', 'Unauthorized', { + method: 'updateOutgoingIntegration', + }); + } - if (!currentIntegration) { - throw new Meteor.Error('invalid_integration', '[methods] updateOutgoingIntegration -> integration not found'); - } + if (!currentIntegration) { + throw new Meteor.Error('invalid_integration', '[methods] updateOutgoingIntegration -> integration not found'); + } - const oldScriptEngine = currentIntegration.scriptEngine; - const scriptEngine = integration.scriptEngine ?? oldScriptEngine ?? 'isolated-vm'; - if ( - integration.script?.trim() && - (scriptEngine !== oldScriptEngine || integration.script?.trim() !== currentIntegration.script?.trim()) - ) { - wrapExceptions(() => validateScriptEngine(scriptEngine)).catch((e) => { - throw new Meteor.Error(e.message); - }); - } + const oldScriptEngine = currentIntegration.scriptEngine; + const scriptEngine = integration.scriptEngine ?? oldScriptEngine ?? 'isolated-vm'; + if ( + integration.script?.trim() && + (scriptEngine !== oldScriptEngine || integration.script?.trim() !== currentIntegration.script?.trim()) + ) { + wrapExceptions(() => validateScriptEngine(scriptEngine)).catch((e) => { + throw new Meteor.Error(e.message); + }); + } - const isFrozen = isScriptEngineFrozen(scriptEngine); + const isFrozen = isScriptEngineFrozen(scriptEngine); - const updatedIntegration = await Integrations.findOneAndUpdate( - { _id: integrationId }, - { - $set: { - event: integration.event, - enabled: integration.enabled, - name: integration.name, - avatar: integration.avatar, - emoji: integration.emoji, - alias: integration.alias, - channel: typeof integration.channel === 'string' ? [integration.channel] : integration.channel, - targetRoom: integration.targetRoom, - impersonateUser: integration.impersonateUser, - username: integration.username, - userId: integration.userId, - urls: integration.urls, - token: integration.token, - ...(isFrozen - ? {} - : { - script: integration.script, - scriptEnabled: integration.scriptEnabled, - scriptEngine, - ...(integration.scriptCompiled ? { scriptCompiled: integration.scriptCompiled } : { scriptError: integration.scriptError }), - }), - triggerWords: integration.triggerWords, - retryFailedCalls: integration.retryFailedCalls, - retryCount: integration.retryCount, - retryDelay: integration.retryDelay, - triggerWordAnywhere: integration.triggerWordAnywhere, - runOnEdits: integration.runOnEdits, - _updatedAt: new Date(), - _updatedBy: await Users.findOne({ _id: this.userId }, { projection: { username: 1 } }), - }, + const updatedIntegration = await Integrations.findOneAndUpdate( + { _id: integrationId }, + { + $set: { + event: integration.event, + enabled: integration.enabled, + name: integration.name, + avatar: integration.avatar, + emoji: integration.emoji, + alias: integration.alias, + channel: typeof integration.channel === 'string' ? [integration.channel] : integration.channel, + targetRoom: integration.targetRoom, + impersonateUser: integration.impersonateUser, + username: integration.username, + userId: integration.userId, + urls: integration.urls, + token: integration.token, ...(isFrozen ? {} : { - $unset: { - ...(integration.scriptCompiled ? { scriptError: 1 as const } : { scriptCompiled: 1 as const }), - }, + script: integration.script, + scriptEnabled: integration.scriptEnabled, + scriptEngine, + ...(integration.scriptCompiled ? { scriptCompiled: integration.scriptCompiled } : { scriptError: integration.scriptError }), }), + triggerWords: integration.triggerWords, + retryFailedCalls: integration.retryFailedCalls, + retryCount: integration.retryCount, + retryDelay: integration.retryDelay, + triggerWordAnywhere: integration.triggerWordAnywhere, + runOnEdits: integration.runOnEdits, + _updatedAt: new Date(), + _updatedBy: await Users.findOne({ _id: userId }, { projection: { username: 1 } }), }, - ); + ...(isFrozen + ? {} + : { + $unset: { + ...(integration.scriptCompiled ? { scriptError: 1 as const } : { scriptCompiled: 1 as const }), + }, + }), + }, + { returnDocument: 'after' }, + ); + + if (updatedIntegration) { + await notifyOnIntegrationChanged(updatedIntegration); + } - if (updatedIntegration) { - await notifyOnIntegrationChanged(updatedIntegration); + return updatedIntegration; +}; + +Meteor.methods({ + async updateOutgoingIntegration(integrationId, _integration) { + if (!this.userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'updateOutgoingIntegration', + }); } - return updatedIntegration; + return updateOutgoingIntegration(this.userId, integrationId, _integration); }, }); diff --git a/apps/meteor/app/lib/client/index.ts b/apps/meteor/app/lib/client/index.ts index 35ec26a6f1dfd..2769be7fe5764 100644 --- a/apps/meteor/app/lib/client/index.ts +++ b/apps/meteor/app/lib/client/index.ts @@ -1,5 +1,3 @@ import '../lib/MessageTypes'; import './OAuthProxy'; import './methods/sendMessage'; - -export * from './lib'; diff --git a/apps/meteor/app/lib/client/lib/LoginPresence.ts b/apps/meteor/app/lib/client/lib/LoginPresence.ts deleted file mode 100644 index cea1f982012f9..0000000000000 --- a/apps/meteor/app/lib/client/lib/LoginPresence.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../../settings/client'; - -class LoginPresence { - private awayTime = 600_000; // 10 minutes - - private started = false; - - private timer: ReturnType; - - private startTimer(): void { - this.stopTimer(); - if (!this.awayTime) { - return; - } - this.timer = setTimeout(() => this.disconnect(), this.awayTime); - } - - private stopTimer(): void { - clearTimeout(this.timer); - } - - private disconnect(): void { - const status = Meteor.status(); - if (status && status.status !== 'offline') { - if (!Meteor.userId() && settings.get('Accounts_AllowAnonymousRead') !== true) { - Meteor.disconnect(); - } - } - this.stopTimer(); - } - - private connect(): void { - const status = Meteor.status(); - if (status && status.status === 'offline') { - Meteor.reconnect(); - } - } - - public start(): void { - if (this.started) { - return; - } - - window.addEventListener('focus', () => { - this.stopTimer(); - this.connect(); - }); - - window.addEventListener('blur', () => { - this.startTimer(); - }); - - if (!window.document.hasFocus()) { - this.startTimer(); - } - - this.started = true; - } -} - -const instance = new LoginPresence(); - -instance.start(); - -export { instance as LoginPresence }; diff --git a/apps/meteor/app/lib/client/lib/index.ts b/apps/meteor/app/lib/client/lib/index.ts deleted file mode 100644 index 847fde4009808..0000000000000 --- a/apps/meteor/app/lib/client/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LoginPresence } from './LoginPresence'; diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts index 2997acaf5924c..a97293e783577 100644 --- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts +++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts @@ -13,6 +13,10 @@ import { settings } from '../../../settings/server'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; import { notifyOnRoomChangedById, notifyOnSubscriptionChangedById } from '../lib/notifyListener'; +/** + * This function adds user to the given room. + * Caution - It does not validates if the user has permission to join room + */ export const addUserToRoom = async function ( rid: string, user: Pick | string, diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 914b3d9a5d937..22a5d7c69dc7b 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -1,4 +1,3 @@ -/* eslint-disable complexity */ import { AppEvents, Apps } from '@rocket.chat/apps'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; import { Federation, FederationEE, License, Message, Team } from '@rocket.chat/core-services'; @@ -203,7 +202,7 @@ export const createRoom = async ( }, ts: now, ro: readOnly === true, - sidepanel, + ...(sidepanel && { sidepanel }), }; if (teamId) { diff --git a/apps/meteor/app/lib/server/functions/deleteUser.ts b/apps/meteor/app/lib/server/functions/deleteUser.ts index fbdb3215cf00d..17b2dcb340b03 100644 --- a/apps/meteor/app/lib/server/functions/deleteUser.ts +++ b/apps/meteor/app/lib/server/functions/deleteUser.ts @@ -1,5 +1,5 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; -import { api } from '@rocket.chat/core-services'; +import { api, Federation, FederationEE, License } from '@rocket.chat/core-services'; import { isUserFederated, type IUser } from '@rocket.chat/core-typings'; import { Integrations, @@ -23,6 +23,7 @@ import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; import { updateGroupDMsName } from './updateGroupDMsName'; import { callbacks } from '../../../../lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; +import { VerificationStatus } from '../../../../server/services/federation/infrastructure/matrix/helpers/MatrixIdVerificationTypes'; import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; import { @@ -49,16 +50,22 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele } if (isUserFederated(user)) { - throw new Meteor.Error('error-not-allowed', 'Deleting federated, external user is not allowed', { - method: 'deleteUser', - }); - } + const service = (await License.hasValidLicense()) ? FederationEE : Federation; - const remoteUser = await MatrixBridgedUser.getExternalUserIdByLocalUserId(userId); - if (remoteUser) { - throw new Meteor.Error('error-not-allowed', 'User participated in federation, this user can only be deactivated permanently', { - method: 'deleteUser', - }); + const result = await service.verifyMatrixIds([user.username as string]); + + if (result.get(user.username as string) === VerificationStatus.VERIFIED) { + throw new Meteor.Error('error-not-allowed', 'Deleting federated, external user is not allowed', { + method: 'deleteUser', + }); + } + } else { + const remoteUser = await MatrixBridgedUser.getExternalUserIdByLocalUserId(userId); + if (remoteUser) { + throw new Meteor.Error('error-not-allowed', 'User participated in federation, this user can only be deactivated permanently', { + method: 'deleteUser', + }); + } } const subscribedRooms = await getSubscribedRoomsForUserWithDetails(userId); diff --git a/apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts b/apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts index e62727814de38..2eb9c704b604b 100644 --- a/apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts +++ b/apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts @@ -1,5 +1,5 @@ -export const getModifiedHttpHeaders = (httpHeaders: Record) => { - const modifiedHttpHeaders = { ...httpHeaders }; +export const getModifiedHttpHeaders = (httpHeaders: Headers) => { + const modifiedHttpHeaders = { ...Object.fromEntries(httpHeaders.entries()) }; if ('x-auth-token' in modifiedHttpHeaders) { modifiedHttpHeaders['x-auth-token'] = '[redacted]'; diff --git a/apps/meteor/app/lib/server/functions/getRoomByNameOrIdWithOptionToJoin.ts b/apps/meteor/app/lib/server/functions/getRoomByNameOrIdWithOptionToJoin.ts index 1dd803f8b13c5..e1aeabe1b46a5 100644 --- a/apps/meteor/app/lib/server/functions/getRoomByNameOrIdWithOptionToJoin.ts +++ b/apps/meteor/app/lib/server/functions/getRoomByNameOrIdWithOptionToJoin.ts @@ -1,8 +1,8 @@ +import { Room } from '@rocket.chat/core-services'; import type { IRoom, IUser, RoomType } from '@rocket.chat/core-typings'; import { Rooms, Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { addUserToRoom } from './addUserToRoom'; import { isObject } from '../../../../lib/utils/isObject'; import { createDirectMessage } from '../../../../server/methods/createDirectMessage'; @@ -88,7 +88,7 @@ export const getRoomByNameOrIdWithOptionToJoin = async ({ // If the room type is channel and joinChannel has been passed, try to join them // if they can't join the room, this will error out! if (room.t === 'c' && joinChannel) { - await addUserToRoom(room._id, user); + await Room.join({ room, user }); } return room; diff --git a/apps/meteor/app/lib/server/functions/processWebhookMessage.ts b/apps/meteor/app/lib/server/functions/processWebhookMessage.ts index ae304b2af01d3..372df0d8ed77c 100644 --- a/apps/meteor/app/lib/server/functions/processWebhookMessage.ts +++ b/apps/meteor/app/lib/server/functions/processWebhookMessage.ts @@ -1,4 +1,5 @@ import type { IMessage, IUser, RequiredField, MessageAttachment } from '@rocket.chat/core-typings'; +import { removeEmpty } from '@rocket.chat/tools'; import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; @@ -135,7 +136,7 @@ export const processWebhookMessage = async function ( await validateRoomMessagePermissionsAsync(room, { uid: user._id, ...user }); - const messageReturn = await sendMessage(user, message, room); + const messageReturn = await sendMessage(user, removeEmpty(message), room); sentData.push({ channel, message: messageReturn }); } diff --git a/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts b/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts index e4a10c4c5a57f..13884b6e34b9b 100644 --- a/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts +++ b/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts @@ -1,4 +1,5 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; +import { MeteorError } from '@rocket.chat/core-services'; import { isUserFederated } from '@rocket.chat/core-typings'; import type { IUser, IRole, IUserSettings, RequiredField } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; @@ -7,6 +8,7 @@ import type { ClientSession } from 'mongodb'; import { callbacks } from '../../../../../lib/callbacks'; import { wrapInSessionTransaction, onceTransactionCommitedSuccessfully } from '../../../../../server/database/utils'; +import type { UserChangedAuditStore } from '../../../../../server/lib/auditServerEvents/userChanged'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { safeGetMeteorUser } from '../../../../utils/server/functions/safeGetMeteorUser'; import { generatePassword } from '../../lib/generatePassword'; @@ -50,12 +52,17 @@ export type SaveUserData = { sendWelcomeEmail?: boolean; customFields?: Record; + active?: boolean; }; export type UpdateUserData = RequiredField; export const isUpdateUserData = (params: SaveUserData): params is UpdateUserData => '_id' in params && !!params._id; +type SaveUserOptions = { + auditStore?: UserChangedAuditStore; +}; + const _saveUser = (session?: ClientSession) => - async function (userId: IUser['_id'], userData: SaveUserData) { + async function (userId: IUser['_id'], userData: SaveUserData, options?: SaveUserOptions) { const oldUserData = userData._id && (await Users.findOneById(userData._id)); if (oldUserData && isUserFederated(oldUserData)) { throw new Meteor.Error('Edit_Federated_User_Not_Allowed', 'Not possible to edit a federated user'); @@ -81,10 +88,18 @@ const _saveUser = (session?: ClientSession) => } if (!isUpdateUserData(userData)) { - // pass session? + // TODO audit new users return saveNewUser(userData, sendPassword); } + if (!oldUserData) { + throw new MeteorError('error-user-not-found', 'User not found', { + method: 'saveUser', + }); + } + + options?.auditStore?.setOriginalUser(oldUserData); + await validateUserEditing(userId, userData); // update user @@ -147,8 +162,14 @@ const _saveUser = (session?: ClientSession) => } } - if (typeof userData.verified === 'boolean' && !userData.email) { - updater.set('emails.0.verified', userData.verified); + if (typeof userData.verified === 'boolean') { + if (oldUserData && 'emails' in oldUserData && oldUserData.emails?.some(({ address }) => address === userData.email)) { + const index = oldUserData.emails.findIndex(({ address }) => address === userData.email); + updater.set(`emails.${index}.verified`, userData.verified); + } + if (!userData.email) { + updater.set(`emails.0.verified`, userData.verified); + } } if (userData.customFields) { @@ -158,6 +179,16 @@ const _saveUser = (session?: ClientSession) => await Users.updateFromUpdater({ _id: userData._id }, updater, { session }); await onceTransactionCommitedSuccessfully(async () => { + if (session && options?.auditStore) { + // setting this inside here to avoid moving `executeSetUserActiveStatus` from the endpoint fn + // updater will be commited by this point, so it won't affect the external user activation/deactivation + if (userData.active !== undefined) { + updater.set('active', userData.active); + } + options.auditStore.setUpdateFilter(updater.getRawUpdateFilter()); + void options.auditStore.commitAuditEvent(); + } + // App IPostUserUpdated event hook // We need to pass the session here to ensure this record is fetched // with the uncommited transaction data. @@ -203,5 +234,9 @@ export const saveUser = (() => { if (!process.env.DEBUG_DISABLE_USER_AUDIT) { return wrapInSessionTransaction(_saveUser); } - return _saveUser(); + + const saveUserNoSession = _saveUser(); + return function saveUser(userId: IUser['_id'], userData: SaveUserData, _options?: any) { + return saveUserNoSession(userId, userData); + }; })(); diff --git a/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts b/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts index 0f83cfe1c0880..6e0eb970b9bc5 100644 --- a/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts +++ b/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts @@ -1,4 +1,5 @@ import type { IUser } from '@rocket.chat/core-typings'; +import { isNotUndefined } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions, Users } from '@rocket.chat/models'; import type { ClientSession } from 'mongodb'; @@ -13,8 +14,8 @@ async function getUsersWhoAreInTheSameGroupDMsAs(user: IUser) { return; } - const userIds = new Set(); - const users = new Map(); + const userIds = new Set(); + const users = new Map(); const rooms = Rooms.findGroupDMsByUids([user._id], { projection: { uids: 1 } }); await rooms.forEach((room) => { @@ -25,9 +26,7 @@ async function getUsersWhoAreInTheSameGroupDMsAs(user: IUser) { room.uids.forEach((uid) => uid !== user._id && userIds.add(uid)); }); - (await Users.findByIds([...userIds], { projection: { username: 1, name: 1 } }).toArray()).forEach((user: IUser) => - users.set(user._id, user), - ); + (await Users.findByIds([...userIds], { projection: { username: 1, name: 1 } }).toArray()).forEach((user) => users.set(user._id, user)); return users; } @@ -59,7 +58,7 @@ export const updateGroupDMsName = async ( const rooms = Rooms.findGroupDMsByUids([userThatChangedName._id], { projection: { uids: 1 }, session }); // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - const getMembers = (uids: string[]) => uids.map((uid) => users.get(uid)).filter(Boolean); + const getMembers = (uids: string[]) => uids.map((uid) => users.get(uid)).filter(isNotUndefined); // loop rooms to update the subscriptions from them all for await (const room of rooms) { diff --git a/apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts b/apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts index be6b107dc044d..5b76b007c1620 100644 --- a/apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts +++ b/apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts @@ -1,5 +1,4 @@ import { Logger } from '@rocket.chat/logger'; -import type { Response } from 'express'; import semver from 'semver'; import { metrics } from '../../../metrics/server'; @@ -12,9 +11,9 @@ const throwErrorsForVersionsUnder = process.env.ROCKET_CHAT_DEPRECATION_THROW_ER const writeDeprecationHeader = (res: Response | undefined, type: string, message: string, version: string) => { if (res) { - res.setHeader('x-deprecation-type', type); - res.setHeader('x-deprecation-message', message); - res.setHeader('x-deprecation-version', version); + res.headers.set('x-deprecation-type', type); + res.headers.set('x-deprecation-message', message); + res.headers.set('x-deprecation-version', version); } }; diff --git a/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts b/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts index c804128d27bd8..23bd39f3090fb 100644 --- a/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts +++ b/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts @@ -7,24 +7,64 @@ import { canAccessRoomAsync } from '../../../authorization/server'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { cleanRoomHistory } from '../functions/cleanRoomHistory'; +type CleanRoomHistoryParams = { + roomId: string; + latest: Date; + oldest: Date; + inclusive?: boolean; + limit?: number; + excludePinned?: boolean; + ignoreDiscussion?: boolean; + filesOnly?: boolean; + fromUsers?: string[]; + ignoreThreads?: boolean; +}; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - cleanRoomHistory(data: { - roomId: string; - latest: Date; - oldest: Date; - inclusive?: boolean; - limit?: number; - excludePinned?: boolean; - ignoreDiscussion?: boolean; - filesOnly?: boolean; - fromUsers?: string[]; - ignoreThreads?: boolean; - }): number; + cleanRoomHistory(data: CleanRoomHistoryParams): number; } } +export const cleanRoomHistoryMethod = async ( + userId: string, + { + roomId, + latest, + oldest, + inclusive = true, + limit, + excludePinned = false, + ignoreDiscussion = true, + filesOnly = false, + fromUsers = [], + ignoreThreads, + }: CleanRoomHistoryParams, +): Promise => { + if (!(await hasPermissionAsync(userId, 'clean-channel-history', roomId))) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'cleanRoomHistory' }); + } + + const room = await findRoomByIdOrName({ params: { roomId } }); + + if (!room || !(await canAccessRoomAsync(room, { _id: userId }))) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'cleanRoomHistory' }); + } + + return cleanRoomHistory({ + rid: roomId, + latest, + oldest, + inclusive, + limit, + excludePinned, + ignoreDiscussion, + filesOnly, + fromUsers, + ignoreThreads, + }); +}; + Meteor.methods({ async cleanRoomHistory({ roomId, @@ -54,18 +94,8 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'cleanRoomHistory' }); } - if (!(await hasPermissionAsync(userId, 'clean-channel-history', roomId))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'cleanRoomHistory' }); - } - - const room = await findRoomByIdOrName({ params: { roomId } }); - - if (!room || !(await canAccessRoomAsync(room, { _id: userId }))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'cleanRoomHistory' }); - } - - return cleanRoomHistory({ - rid: roomId, + return cleanRoomHistoryMethod(userId, { + roomId, latest, oldest, inclusive, diff --git a/apps/meteor/app/livechat/client/collections/LivechatInquiry.ts b/apps/meteor/app/livechat/client/collections/LivechatInquiry.ts deleted file mode 100644 index 16b9533d1649e..0000000000000 --- a/apps/meteor/app/livechat/client/collections/LivechatInquiry.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { ILivechatInquiryRecord } from '@rocket.chat/core-typings'; -import { Mongo } from 'meteor/mongo'; - -export const LivechatInquiry = new Mongo.Collection(null); diff --git a/apps/meteor/app/livechat/client/lib/stream/queueManager.ts b/apps/meteor/app/livechat/client/lib/stream/queueManager.ts index 78769e5a960c6..caa7e74f81917 100644 --- a/apps/meteor/app/livechat/client/lib/stream/queueManager.ts +++ b/apps/meteor/app/livechat/client/lib/stream/queueManager.ts @@ -1,10 +1,11 @@ -import type { ILivechatDepartment, ILivechatInquiryRecord, IOmnichannelAgent } from '@rocket.chat/core-typings'; +import type { ILivechatDepartment, ILivechatInquiryRecord, IOmnichannelAgent, Serialized } from '@rocket.chat/core-typings'; +import { useLivechatInquiryStore } from '../../../../../client/hooks/useLivechatInquiryStore'; import { queryClient } from '../../../../../client/lib/queryClient'; import { callWithErrorHandling } from '../../../../../client/lib/utils/callWithErrorHandling'; +import { mapMessageFromApi } from '../../../../../client/lib/utils/mapMessageFromApi'; import { settings } from '../../../../settings/client'; import { sdk } from '../../../../utils/client/lib/SDKClient'; -import { LivechatInquiry } from '../../collections/LivechatInquiry'; const departments = new Set(); @@ -14,7 +15,7 @@ const events = { return; } - LivechatInquiry.insert({ ...inquiry, alert: true, _updatedAt: new Date(inquiry._updatedAt) }); + useLivechatInquiryStore.getState().add({ ...inquiry, alert: true }); await invalidateRoomQueries(inquiry.rid); }, changed: async (inquiry: ILivechatInquiryRecord) => { @@ -22,7 +23,7 @@ const events = { return removeInquiry(inquiry); } - LivechatInquiry.upsert({ _id: inquiry._id }, { ...inquiry, alert: true, _updatedAt: new Date(inquiry._updatedAt) }); + useLivechatInquiryStore.getState().merge({ ...inquiry, alert: true }); await invalidateRoomQueries(inquiry.rid); }, removed: (inquiry: ILivechatInquiryRecord) => removeInquiry(inquiry), @@ -49,7 +50,7 @@ const invalidateRoomQueries = async (rid: string) => { }; const removeInquiry = async (inquiry: ILivechatInquiryRecord) => { - LivechatInquiry.remove(inquiry._id); + useLivechatInquiryStore.getState().discard(inquiry._id); return queryClient.invalidateQueries({ queryKey: ['rooms', { reference: inquiry.rid, type: 'l' }] }); }; @@ -76,8 +77,19 @@ const addListenerForeachDepartment = (departments: ILivechatDepartment['_id'][] return () => cleanupFunctions.forEach((cleanup) => cleanup()); }; -const updateInquiries = async (inquiries: ILivechatInquiryRecord[] = []) => - inquiries.forEach((inquiry) => LivechatInquiry.upsert({ _id: inquiry._id }, { ...inquiry, _updatedAt: new Date(inquiry._updatedAt) })); +const updateInquiries = async (inquiries: Serialized[] = []) => + inquiries.forEach((inquiry) => { + useLivechatInquiryStore.getState().merge({ + ...inquiry, + alert: true, + ts: new Date(inquiry.ts), + v: { ...inquiry.v, lastMessageTs: inquiry.v.lastMessageTs ? new Date(inquiry.v.lastMessageTs) : undefined }, + estimatedInactivityCloseTimeAt: inquiry.estimatedInactivityCloseTimeAt ? new Date(inquiry.estimatedInactivityCloseTimeAt) : undefined, + lockedAt: inquiry.lockedAt ? new Date(inquiry.lockedAt) : undefined, + lastMessage: inquiry.lastMessage ? mapMessageFromApi(inquiry.lastMessage) : undefined, + _updatedAt: new Date(inquiry._updatedAt), + }); + }); const getAgentsDepartments = async (userId: IOmnichannelAgent['_id']) => { const { departments } = await sdk.rest.get(`/v1/livechat/agents/${userId}/departments`, { enabledDepartmentsOnly: 'true' }); @@ -118,13 +130,13 @@ const subscribe = async (userId: IOmnichannelAgent['_id']) => { const globalCleanup = addGlobalListener(); const computation = Tracker.autorun(async () => { - const inquiriesFromAPI = (await getInquiriesFromAPI()) as unknown as ILivechatInquiryRecord[]; + const inquiriesFromAPI = await getInquiriesFromAPI(); await updateInquiries(inquiriesFromAPI); }); return () => { - LivechatInquiry.remove({}); + useLivechatInquiryStore.getState().discardAll(); removeGlobalListener(); cleanAgentListener?.(); cleanDepartmentListeners?.(); diff --git a/apps/meteor/app/livechat/imports/server/rest/appearance.ts b/apps/meteor/app/livechat/imports/server/rest/appearance.ts index 215f208c06dc7..126c93d5fc93c 100644 --- a/apps/meteor/app/livechat/imports/server/rest/appearance.ts +++ b/apps/meteor/app/livechat/imports/server/rest/appearance.ts @@ -98,7 +98,7 @@ API.v1.addRoute( _id: this.userId, username: this.user.username!, ip: this.requestIp, - useragent: this.request.headers['user-agent'] || '', + useragent: this.request.headers.get('user-agent') || '', }); const promises = eligibleSettings.map(({ _id, value }) => auditSettingOperation(Settings.updateValueById, _id, value)); diff --git a/apps/meteor/app/livechat/imports/server/rest/sms.ts b/apps/meteor/app/livechat/imports/server/rest/sms.ts index 3b7aa07773071..b579f490a4d88 100644 --- a/apps/meteor/app/livechat/imports/server/rest/sms.ts +++ b/apps/meteor/app/livechat/imports/server/rest/sms.ts @@ -20,7 +20,7 @@ import { FileUpload } from '../../../../file-upload/server'; import { checkUrlForSsrf } from '../../../../lib/server/functions/checkUrlForSsrf'; import { settings } from '../../../../settings/server'; import { setCustomField } from '../../../server/api/lib/customFields'; -import { Livechat as LivechatTyped } from '../../../server/lib/LivechatTyped'; +import { registerGuest } from '../../../server/lib/guests'; import type { ILivechatMessage } from '../../../server/lib/localTypes'; import { sendMessage } from '../../../server/lib/messages'; import { createRoom } from '../../../server/lib/rooms'; @@ -76,7 +76,7 @@ const defineVisitor = async (smsNumber: string, targetDepartment?: string) => { data.department = targetDepartment; } - const livechatVisitor = await LivechatTyped.registerGuest(data); + const livechatVisitor = await registerGuest(data); if (!livechatVisitor) { throw new Meteor.Error('error-invalid-visitor', 'Invalid visitor'); @@ -108,7 +108,7 @@ API.v1.addRoute('livechat/sms-incoming/:service', { const smsDepartment = settings.get('SMS_Default_Omnichannel_Department'); const SMSService = await OmnichannelIntegration.getSmsService(service); - if (!SMSService.validateRequest(this.request)) { + if (!(await SMSService.validateRequest(this.request.clone()))) { return API.v1.failure('Invalid request'); } diff --git a/apps/meteor/app/livechat/imports/server/rest/upload.ts b/apps/meteor/app/livechat/imports/server/rest/upload.ts index 86c815cce72c9..8cb0a0511eade 100644 --- a/apps/meteor/app/livechat/imports/server/rest/upload.ts +++ b/apps/meteor/app/livechat/imports/server/rest/upload.ts @@ -10,7 +10,7 @@ import { sendFileLivechatMessage } from '../../../server/methods/sendFileLivecha API.v1.addRoute('livechat/upload/:rid', { async post() { - if (!this.request.headers['x-visitor-token']) { + if (!this.request.headers.get('x-visitor-token')) { return API.v1.forbidden(); } @@ -22,7 +22,7 @@ API.v1.addRoute('livechat/upload/:rid', { }); } - const visitorToken = this.request.headers['x-visitor-token']; + const visitorToken = this.request.headers.get('x-visitor-token'); const visitor = await LivechatVisitors.getVisitorByToken(visitorToken as string, {}); if (!visitor) { diff --git a/apps/meteor/app/livechat/imports/server/rest/users.ts b/apps/meteor/app/livechat/imports/server/rest/users.ts index 85ceb5103687a..b8d5088d80418 100644 --- a/apps/meteor/app/livechat/imports/server/rest/users.ts +++ b/apps/meteor/app/livechat/imports/server/rest/users.ts @@ -7,7 +7,7 @@ import { API } from '../../../../api/server'; import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems'; import { hasAtLeastOnePermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { findAgents, findManagers } from '../../../server/api/lib/users'; -import { Livechat } from '../../../server/lib/LivechatTyped'; +import { addManager, addAgent, removeAgent, removeManager } from '../../../server/lib/omni-users'; const emptyStringArray: string[] = []; @@ -73,12 +73,12 @@ API.v1.addRoute( }, async post() { if (this.urlParams.type === 'agent') { - const user = await Livechat.addAgent(this.bodyParams.username); + const user = await addAgent(this.bodyParams.username); if (user) { return API.v1.success({ user }); } } else if (this.urlParams.type === 'manager') { - const user = await Livechat.addManager(this.bodyParams.username); + const user = await addManager(this.bodyParams.username); if (user) { return API.v1.success({ user }); } @@ -130,11 +130,11 @@ API.v1.addRoute( } if (this.urlParams.type === 'agent') { - if (await Livechat.removeAgent(user.username)) { + if (await removeAgent(user.username)) { return API.v1.success(); } } else if (this.urlParams.type === 'manager') { - if (await Livechat.removeManager(user.username)) { + if (await removeManager(user.username)) { return API.v1.success(); } } else { diff --git a/apps/meteor/app/livechat/server/api/lib/livechat.ts b/apps/meteor/app/livechat/server/api/lib/livechat.ts index a6c774fb4ddfb..df43140b28f5b 100644 --- a/apps/meteor/app/livechat/server/api/lib/livechat.ts +++ b/apps/meteor/app/livechat/server/api/lib/livechat.ts @@ -6,13 +6,8 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../../lib/callbacks'; import { i18n } from '../../../../../server/lib/i18n'; import { normalizeAgent } from '../../lib/Helper'; -import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; import { getInitSettings } from '../../lib/settings'; -export function online(department: string, skipSettingCheck = false, skipFallbackCheck = false): Promise { - return LivechatTyped.online(department, skipSettingCheck, skipFallbackCheck); -} - async function findTriggers(): Promise[]> { const triggers = await LivechatTrigger.findEnabled().toArray(); const hasLicense = License.hasModule('livechat-enterprise'); @@ -93,10 +88,10 @@ export async function findAgent(agentId?: string): Promise = {}): { +export function normalizeHttpHeaderData(headers: Headers = new Headers()): { httpHeaders: Record; } { - const httpHeaders = Object.assign({}, headers); + const httpHeaders = Object.fromEntries(headers.entries()); return { httpHeaders }; } diff --git a/apps/meteor/app/livechat/server/api/lib/users.ts b/apps/meteor/app/livechat/server/api/lib/users.ts index 49ac5682a6c04..62663603768fe 100644 --- a/apps/meteor/app/livechat/server/api/lib/users.ts +++ b/apps/meteor/app/livechat/server/api/lib/users.ts @@ -51,7 +51,7 @@ async function findUsers({ sortedResults, totalCount: [{ total } = { total: 0 }], }, - ] = await Users.findAgentsWithDepartments(role, query, { + ] = await Users.findAgentsWithDepartments(role, query, { sort: sort || { name: 1 }, skip: offset, limit: count, diff --git a/apps/meteor/app/livechat/server/api/v1/agent.ts b/apps/meteor/app/livechat/server/api/v1/agent.ts index 0cd3139bf6a5c..e5b59bde2d01c 100644 --- a/apps/meteor/app/livechat/server/api/v1/agent.ts +++ b/apps/meteor/app/livechat/server/api/v1/agent.ts @@ -5,9 +5,9 @@ import { isGETAgentNextToken, isPOSTLivechatAgentStatusProps } from '@rocket.cha import { API } from '../../../../api/server'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; import { RoutingManager } from '../../lib/RoutingManager'; import { getRequiredDepartment } from '../../lib/departmentsLib'; +import { setUserStatusLivechat, allowAgentChangeServiceStatus } from '../../lib/utils'; import { findRoom, findGuest, findAgent, findOpenRoom } from '../lib/livechat'; API.v1.addRoute('livechat/agent.info/:rid/:token', { @@ -96,7 +96,7 @@ API.v1.addRoute( return API.v1.success({ status: agent.statusLivechat }); } - const canChangeStatus = await LivechatTyped.allowAgentChangeServiceStatus(newStatus, agentId); + const canChangeStatus = await allowAgentChangeServiceStatus(newStatus, agentId); if (agentId !== this.userId) { if (!(await hasPermissionAsync(this.userId, 'manage-livechat-agents'))) { @@ -107,7 +107,7 @@ API.v1.addRoute( // Next version we'll update this to return an error // And update the FE accordingly if (canChangeStatus) { - await LivechatTyped.setUserStatusLivechat(agentId, newStatus); + await setUserStatusLivechat(agentId, newStatus); return API.v1.success({ status: newStatus }); } @@ -118,7 +118,7 @@ API.v1.addRoute( return API.v1.failure('error-business-hours-are-closed'); } - await LivechatTyped.setUserStatusLivechat(agentId, newStatus); + await setUserStatusLivechat(agentId, newStatus); return API.v1.success({ status: newStatus }); }, diff --git a/apps/meteor/app/livechat/server/api/v1/config.ts b/apps/meteor/app/livechat/server/api/v1/config.ts index 17a2945e75def..55afdfc00e6ad 100644 --- a/apps/meteor/app/livechat/server/api/v1/config.ts +++ b/apps/meteor/app/livechat/server/api/v1/config.ts @@ -3,7 +3,7 @@ import mem from 'mem'; import { API } from '../../../../api/server'; import { settings as serverSettings } from '../../../../settings/server'; -import { Livechat } from '../../lib/LivechatTyped'; +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 }); @@ -23,7 +23,7 @@ API.v1.addRoute( const config = await cachedSettings({ businessUnit }); - const status = await Livechat.online(department); + const status = await online(department); const guest = token ? await findGuestWithoutActivity(token) : null; const room = guest ? await findOpenRoom(guest.token) : undefined; diff --git a/apps/meteor/app/livechat/server/api/v1/contact.ts b/apps/meteor/app/livechat/server/api/v1/contact.ts index 03cc5ddeaabd0..702662c917f25 100644 --- a/apps/meteor/app/livechat/server/api/v1/contact.ts +++ b/apps/meteor/app/livechat/server/api/v1/contact.ts @@ -9,6 +9,7 @@ import { isGETOmnichannelContactsCheckExistenceProps, } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; +import { removeEmpty } from '@rocket.chat/tools'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -121,7 +122,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['update-livechat-contact'], validateParams: isPOSTUpdateOmnichannelContactsProps }, { async post() { - const contact = await updateContact({ ...this.bodyParams }); + const contact = await updateContact(removeEmpty(this.bodyParams)); return API.v1.success({ contact }); }, diff --git a/apps/meteor/app/livechat/server/api/v1/customField.ts b/apps/meteor/app/livechat/server/api/v1/customField.ts index 80fe83bf55096..c678824d905e1 100644 --- a/apps/meteor/app/livechat/server/api/v1/customField.ts +++ b/apps/meteor/app/livechat/server/api/v1/customField.ts @@ -2,7 +2,7 @@ import { isLivechatCustomFieldsProps, isPOSTLivechatCustomFieldParams, isPOSTLiv import { API } from '../../../../api/server'; import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems'; -import { Livechat } from '../../lib/LivechatTyped'; +import { setCustomFields } from '../../lib/custom-fields'; import { findLivechatCustomFields, findCustomFieldById } from '../lib/customFields'; import { findGuest } from '../lib/livechat'; @@ -12,15 +12,12 @@ API.v1.addRoute( { async post() { const { token, key, value, overwrite } = this.bodyParams; - const guest = await findGuest(token); if (!guest) { throw new Error('invalid-token'); } - if (!(await Livechat.setCustomFields({ token, key, value, overwrite }))) { - return API.v1.failure(); - } + await setCustomFields({ token, key, value, overwrite }); return API.v1.success({ field: { key, value, overwrite } }); }, @@ -46,9 +43,7 @@ API.v1.addRoute( overwrite: boolean; }): Promise<{ Key: string; value: string; overwrite: boolean }> => { const data = Object.assign({ token }, customField); - if (!(await Livechat.setCustomFields(data))) { - throw new Error('error-setting-custom-field'); - } + await setCustomFields(data); return { Key: customField.key, value: customField.value, overwrite: customField.overwrite }; }, diff --git a/apps/meteor/app/livechat/server/api/v1/integration.ts b/apps/meteor/app/livechat/server/api/v1/integration.ts index 7e56c8ca8e591..48b748f4fb7be 100644 --- a/apps/meteor/app/livechat/server/api/v1/integration.ts +++ b/apps/meteor/app/livechat/server/api/v1/integration.ts @@ -55,7 +55,7 @@ API.v1.addRoute( _id: this.userId, username: this.user.username!, ip: this.requestIp, - useragent: this.request.headers['user-agent'] || '', + useragent: this.request.headers.get('user-agent') || '', }); const promises = settingsIds.map((setting) => auditSettingOperation(Settings.updateValueById, setting._id, setting.value)); diff --git a/apps/meteor/app/livechat/server/api/v1/message.ts b/apps/meteor/app/livechat/server/api/v1/message.ts index 5fbeb138e67f2..75efff450948c 100644 --- a/apps/meteor/app/livechat/server/api/v1/message.ts +++ b/apps/meteor/app/livechat/server/api/v1/message.ts @@ -17,7 +17,7 @@ import { isWidget } from '../../../../api/server/helpers/isWidget'; import { loadMessageHistory } from '../../../../lib/server/functions/loadMessageHistory'; import { settings } from '../../../../settings/server'; import { normalizeMessageFileUpload } from '../../../../utils/server/functions/normalizeMessageFileUpload'; -import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; +import { registerGuest } from '../../lib/guests'; import { updateMessage, deleteMessage, sendMessage } from '../../lib/messages'; import { findGuest, findRoom, normalizeHttpHeaderData } from '../lib/livechat'; @@ -269,7 +269,7 @@ API.v1.addRoute( const guest: typeof this.bodyParams.visitor & { connectionData?: unknown } = this.bodyParams.visitor; guest.connectionData = normalizeHttpHeaderData(this.request.headers); - const visitor = await LivechatTyped.registerGuest(guest); + const visitor = await registerGuest(guest); if (!visitor) { throw new Error('error-livechat-visitor-registration'); } diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 071016456db0b..faec79b0208de 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -23,11 +23,12 @@ import { addUserToRoom } from '../../../../lib/server/functions/addUserToRoom'; import { closeLivechatRoom } from '../../../../lib/server/functions/closeLivechatRoom'; import { settings as rcSettings } from '../../../../settings/server'; import { normalizeTransferredByData } from '../../lib/Helper'; -import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; import { closeRoom } from '../../lib/closeRoom'; +import { saveGuest } from '../../lib/guests'; import type { CloseRoomParams } from '../../lib/localTypes'; import { livechatLogger } from '../../lib/logger'; import { createRoom, saveRoomInfo } from '../../lib/rooms'; +import { transfer } from '../../lib/transfer'; import { findGuest, findRoom, settings, findAgent, onCheckRoomParams } from '../lib/livechat'; const isAgentWithInfo = (agentObj: ILivechatAgent | { hiddenInfo: boolean }): agentObj is ILivechatAgent => !('hiddenInfo' in agentObj); @@ -78,7 +79,7 @@ API.v1.addRoute( const roomInfo = { source: { ...(isWidget(this.request.headers) - ? { type: OmnichannelSourceType.WIDGET, destination: this.request.headers.host } + ? { type: OmnichannelSourceType.WIDGET, destination: this.request.headers.get('host')! } : { type: OmnichannelSourceType.API }), }, }; @@ -243,7 +244,7 @@ API.v1.addRoute( const { _id, username, name } = guest; const transferredBy = normalizeTransferredByData({ _id, username, name, userType: 'visitor' }, room); - if (!(await LivechatTyped.transfer(room, guest, { departmentId: department, transferredBy }))) { + if (!(await transfer(room, guest, { departmentId: department, transferredBy }))) { return API.v1.failure(); } @@ -339,7 +340,7 @@ API.v1.addRoute( } } - const chatForwardedResult = await LivechatTyped.transfer(room, guest, transferData); + const chatForwardedResult = await transfer(room, guest, transferData); if (!chatForwardedResult) { throw new Error('error-forwarding-chat'); } @@ -410,7 +411,7 @@ API.v1.addRoute( } // We want this both operations to be concurrent, so we have to go with Promise.allSettled - const result = await Promise.allSettled([LivechatTyped.saveGuest(guestData, this.userId), saveRoomInfo(roomData)]); + const result = await Promise.allSettled([saveGuest(guestData, this.userId), saveRoomInfo(roomData)]); const firstError = result.find((item) => item.status === 'rejected'); if (firstError) { diff --git a/apps/meteor/app/livechat/server/api/v1/videoCall.ts b/apps/meteor/app/livechat/server/api/v1/videoCall.ts index 01bda33724d11..ee71543b75aba 100644 --- a/apps/meteor/app/livechat/server/api/v1/videoCall.ts +++ b/apps/meteor/app/livechat/server/api/v1/videoCall.ts @@ -8,7 +8,7 @@ import { API } from '../../../../api/server'; import { canSendMessageAsync } from '../../../../authorization/server/functions/canSendMessage'; import { notifyOnRoomChangedById, notifyOnSettingChanged } from '../../../../lib/server/lib/notifyListener'; import { settings as rcSettings } from '../../../../settings/server'; -import { Livechat } from '../../lib/LivechatTyped'; +import { updateCallStatus } from '../../lib/utils'; import { settings } from '../lib/livechat'; API.v1.addRoute( @@ -70,6 +70,7 @@ API.v1.addRoute( }, ); +// TODO: investigate if we can deprecate this functionality API.v1.addRoute( 'livechat/webrtc.call/:callId', { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isPUTWebRTCCallId }, @@ -100,7 +101,7 @@ API.v1.addRoute( throw new Error('invalid-callId'); } - await Livechat.updateCallStatus(callId, rid, status, this.user); + await updateCallStatus(callId, rid, status, this.user); return API.v1.success({ status }); }, diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index def6ef84edc9b..e2195738966b0 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -1,14 +1,16 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { LivechatVisitors as VisitorsRaw, LivechatCustomField, LivechatRooms } from '@rocket.chat/models'; +import type { IRoom, ILivechatCustomField } from '@rocket.chat/core-typings'; +import { LivechatVisitors as VisitorsRaw, LivechatCustomField, LivechatRooms, LivechatContacts } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../../lib/callbacks'; import { API } from '../../../../api/server'; import { settings } from '../../../../settings/server'; -import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; +import { updateContactsCustomFields, validateRequiredCustomFields } from '../../lib/custom-fields'; +import { registerGuest, removeGuest, notifyGuestStatusChanged } from '../../lib/guests'; +import { livechatLogger } from '../../lib/logger'; import { saveRoomInfo } from '../../lib/rooms'; -import { validateRequiredCustomFields } from '../../lib/validateRequiredCustomFields'; +import { updateCallStatus } from '../../lib/utils'; import { findGuest, normalizeHttpHeaderData } from '../lib/livechat'; API.v1.addRoute( @@ -57,7 +59,7 @@ API.v1.addRoute( connectionData: normalizeHttpHeaderData(this.request.headers), }; - const visitor = await LivechatTyped.registerGuest(guest); + const visitor = await registerGuest(guest); if (!visitor) { throw new Meteor.Error('error-livechat-visitor-registration', 'Error registering visitor', { method: 'livechat/visitor', @@ -91,9 +93,9 @@ API.v1.addRoute( ).toArray(); validateRequiredCustomFields(keys, livechatCustomFields); - const matchingCustomFields = livechatCustomFields.filter((field) => keys.includes(field._id)); + const matchingCustomFields = livechatCustomFields.filter((field: ILivechatCustomField) => keys.includes(field._id)); const processedKeys = await Promise.all( - matchingCustomFields.map(async (field) => { + matchingCustomFields.map(async (field: ILivechatCustomField) => { const customField = customFields.find((f) => f.key === field._id); if (!customField) { return; @@ -105,12 +107,18 @@ API.v1.addRoute( errors.push(key); } + // TODO deduplicate this code and the one at the function setCustomFields (apps/meteor/app/livechat/server/lib/custom-fields.ts) + const contacts = await LivechatContacts.findAllByVisitorId(visitor._id).toArray(); + if (contacts.length > 0) { + await Promise.all(contacts.map((contact) => updateContactsCustomFields(contact, key, value, overwrite))); + } + return key; }), ); if (processedKeys.length !== keys.length) { - LivechatTyped.logger.warn({ + livechatLogger.warn({ msg: 'Some custom fields were not processed', visitorId: visitor._id, missingKeys: keys.filter((key) => !processedKeys.includes(key)), @@ -118,7 +126,7 @@ API.v1.addRoute( } if (errors.length > 0) { - LivechatTyped.logger.error({ + livechatLogger.error({ msg: 'Error updating custom fields', visitorId: visitor._id, errors, @@ -183,7 +191,7 @@ API.v1.addRoute('livechat/visitor/:token', { } const { _id } = visitor; - const result = await LivechatTyped.removeGuest(_id); + const result = await removeGuest(_id); if (!result.modifiedCount) { throw new Meteor.Error('error-removing-visitor', 'An error ocurred while deleting visitor'); } @@ -236,7 +244,7 @@ API.v1.addRoute('livechat/visitor.callStatus', { if (!guest) { throw new Meteor.Error('invalid-token'); } - await LivechatTyped.updateCallStatus(callId, rid, callStatus, guest); + await updateCallStatus(callId, rid, callStatus, guest); return API.v1.success({ token, callStatus }); }, }); @@ -255,7 +263,7 @@ API.v1.addRoute('livechat/visitor.status', { throw new Meteor.Error('invalid-token'); } - await LivechatTyped.notifyGuestStatusChanged(token, status); + await notifyGuestStatusChanged(token, status); return API.v1.success({ token, status }); }, diff --git a/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts b/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts index 940954dfa2a79..1a61cca494935 100644 --- a/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts +++ b/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts @@ -1,4 +1,5 @@ import type { AtLeast, ILivechatAgentStatus, ILivechatBusinessHour, ILivechatDepartment } from '@rocket.chat/core-typings'; +import { UserStatus } from '@rocket.chat/core-typings'; import type { ILivechatBusinessHoursModel, IUsersModel } from '@rocket.chat/model-typings'; import { LivechatBusinessHours, Users } from '@rocket.chat/models'; import type { IWorkHoursCronJobsWrapper } from '@rocket.chat/models'; @@ -55,7 +56,7 @@ export abstract class AbstractBusinessHourBehavior { status, // Why this works: statusDefault is the property set when a user manually changes their status // So if it's set to offline, we can be sure the user will be offline after login and we can skip the update - { livechatStatusSystemModified: true, statusDefault: { $ne: 'offline' } }, + { livechatStatusSystemModified: true, statusDefault: { $ne: UserStatus.OFFLINE } }, { livechatStatusSystemModified: true }, ); diff --git a/apps/meteor/app/livechat/server/business-hour/Helper.ts b/apps/meteor/app/livechat/server/business-hour/Helper.ts index d21b51ce0184d..e5b16865d8f91 100644 --- a/apps/meteor/app/livechat/server/business-hour/Helper.ts +++ b/apps/meteor/app/livechat/server/business-hour/Helper.ts @@ -49,7 +49,7 @@ export const createDefaultBusinessHourIfNotExists = async (): Promise => { } }; -export async function makeAgentsUnavailableBasedOnBusinessHour(agentIds: string[] | null = null) { +export async function makeAgentsUnavailableBasedOnBusinessHour(agentIds?: string[]) { const results = await Users.findAgentsAvailableWithoutBusinessHours(agentIds).toArray(); const update = await Users.updateLivechatStatusByAgentIds( @@ -75,7 +75,7 @@ export async function makeAgentsUnavailableBasedOnBusinessHour(agentIds: string[ ); } -export async function makeOnlineAgentsAvailable(agentIds: string[] | null = null) { +export async function makeOnlineAgentsAvailable(agentIds?: string[]) { const results = await Users.findOnlineButNotAvailableAgents(agentIds).toArray(); const update = await Users.updateLivechatStatusByAgentIds( diff --git a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts index 2d0120c93c821..026eb8224d8c4 100644 --- a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts +++ b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts @@ -2,7 +2,7 @@ import { type IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; -import { Livechat as LivechatTyped } from '../lib/LivechatTyped'; +import { afterAgentUserActivated, afterAgentAdded, afterRemoveAgent } from '../lib/hooks'; type IAfterSaveUserProps = { user: IUser; @@ -16,18 +16,18 @@ const handleAgentUpdated = async (userData: IAfterSaveUserProps) => { const { user: newUser, oldUser } = userData; if (wasAgent(oldUser) && !isAgent(newUser)) { - await LivechatTyped.afterRemoveAgent(newUser); + await afterRemoveAgent(newUser); } if (!wasAgent(oldUser) && isAgent(newUser)) { - await LivechatTyped.afterAgentAdded(newUser); + await afterAgentAdded(newUser); } }; const handleAgentCreated = async (user: IUser) => { // created === no prev roles :) if (isAgent(user)) { - await LivechatTyped.afterAgentAdded(user); + await afterAgentAdded(user); } }; @@ -39,7 +39,7 @@ const handleDeactivateUser = async (user: IUser) => { const handleActivateUser = async (user: IUser) => { if (isAgent(user) && user.username) { - await LivechatTyped.afterAgentUserActivated(user); + await afterAgentUserActivated(user); } }; diff --git a/apps/meteor/app/livechat/server/hooks/sendToCRM.ts b/apps/meteor/app/livechat/server/hooks/sendToCRM.ts index 4189f84cbfd8a..f3b6dbccbf937 100644 --- a/apps/meteor/app/livechat/server/hooks/sendToCRM.ts +++ b/apps/meteor/app/livechat/server/hooks/sendToCRM.ts @@ -5,7 +5,7 @@ import { LivechatRooms, Messages } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; import { settings } from '../../../settings/server'; import { normalizeMessageFileUpload } from '../../../utils/server/functions/normalizeMessageFileUpload'; -import { Livechat as LivechatTyped } from '../lib/LivechatTyped'; +import { getLivechatRoomGuestInfo } from '../lib/guests'; import { sendRequest } from '../lib/webhooks'; type AdditionalFields = @@ -97,11 +97,11 @@ async function sendToCRM( return room; } - const postData: Awaited> & { + const postData: Awaited> & { type: string; messages: IOmnichannelSystemMessage[]; } = { - ...(await LivechatTyped.getLivechatRoomGuestInfo(room)), + ...(await getLivechatRoomGuestInfo(room)), type, messages: [], }; diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index b5d1b4569a430..6db7408aeef0d 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -31,17 +31,18 @@ import { Users, LivechatContacts, } from '@rocket.chat/models'; +import { removeEmpty } from '@rocket.chat/tools'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import type { ClientSession } from 'mongodb'; import { ObjectId } from 'mongodb'; -import { Livechat as LivechatTyped } from './LivechatTyped'; import { queueInquiry, saveQueueInquiry } from './QueueManager'; import { RoutingManager } from './RoutingManager'; import { isVerifiedChannelInSource } from './contacts/isVerifiedChannelInSource'; import { migrateVisitorIfMissingContact } from './contacts/migrateVisitorIfMissingContact'; -import { getOnlineAgents } from './getOnlineAgents'; +import { checkOnlineAgents, getOnlineAgents } from './service-status'; +import { saveTransferHistory } from './transfer'; import { callbacks } from '../../../../lib/callbacks'; import { validateEmail as validatorFunc } from '../../../../lib/emailValidator'; import { i18n } from '../../../../server/lib/i18n'; @@ -139,7 +140,6 @@ export const prepareLivechatRoom = async ( alias: 'unknown', }, queuedAt: newRoomAt, - livechatData: undefined, priorityWeight: LivechatPriorityWeight.NOT_SPECIFIED, estimatedWaitingTimeQueue: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE, ...extraRoomInfo, @@ -148,7 +148,7 @@ export const prepareLivechatRoom = async ( export const createLivechatRoom = async (room: InsertionModel, session: ClientSession) => { const result = await LivechatRooms.findOneAndUpdate( - room, + removeEmpty(room), { $set: {}, }, @@ -212,7 +212,7 @@ export const createLivechatInquiry = async ({ }); const result = await LivechatInquiry.findOneAndUpdate( - { + removeEmpty({ rid, name, ts, @@ -231,7 +231,7 @@ export const createLivechatInquiry = async ({ estimatedWaitingTimeQueue: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE, ...extraInquiryInfo, - }, + }), { $set: { _id: new ObjectId().toHexString(), @@ -516,7 +516,7 @@ export const forwardRoomToAgent = async (room: IOmnichannelRoom, transferData: T return false; } - await LivechatTyped.saveTransferHistory(room, transferData); + await saveTransferHistory(room, transferData); const { servedBy } = roomTaken; if (servedBy) { @@ -629,10 +629,10 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi if ( !RoutingManager.getConfig()?.autoAssignAgent || !(await Omnichannel.isWithinMACLimit(room)) || - (department?.allowReceiveForwardOffline && !(await LivechatTyped.checkOnlineAgents(departmentId))) + (department?.allowReceiveForwardOffline && !(await checkOnlineAgents(departmentId))) ) { logger.debug(`Room ${room._id} will be on department queue`); - await LivechatTyped.saveTransferHistory(room, transferData); + await saveTransferHistory(room, transferData); return RoutingManager.unassignAgent(inquiry, departmentId, true); } @@ -692,7 +692,7 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi ); } - await LivechatTyped.saveTransferHistory(room, transferData); + await saveTransferHistory(room, transferData); if (oldServedBy) { // if chat is queued then we don't ignore the new servedBy agent bcs at this // point the chat is not assigned to him/her and it is still in the queue diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts deleted file mode 100644 index 67230078847f6..0000000000000 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ /dev/null @@ -1,700 +0,0 @@ -import { Apps, AppEvents } from '@rocket.chat/apps'; -import { Message, VideoConf, api } from '@rocket.chat/core-services'; -import type { - IOmnichannelRoom, - IUser, - ILivechatVisitor, - ILivechatAgent, - ILivechatDepartment, - AtLeast, - TransferData, - IOmnichannelAgent, - UserStatus, -} from '@rocket.chat/core-typings'; -import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; -import { Logger } from '@rocket.chat/logger'; -import { - LivechatDepartment, - LivechatInquiry, - LivechatRooms, - Subscriptions, - LivechatVisitors, - Messages, - Users, - LivechatDepartmentAgents, - ReadReceipts, - Rooms, - LivechatCustomField, -} from '@rocket.chat/models'; -import { Match, check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; -import type { Filter } from 'mongodb'; -import UAParser from 'ua-parser-js'; - -import { callbacks } from '../../../../lib/callbacks'; -import { trim } from '../../../../lib/utils/stringUtils'; -import { i18n } from '../../../../server/lib/i18n'; -import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; -import { removeUserFromRolesAsync } from '../../../../server/lib/roles/removeUserFromRoles'; -import { canAccessRoomAsync } from '../../../authorization/server'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; -import { updateMessage } from '../../../lib/server/functions/updateMessage'; -import { - notifyOnLivechatInquiryChanged, - notifyOnLivechatInquiryChangedByToken, - notifyOnUserChange, - notifyOnSubscriptionChanged, -} from '../../../lib/server/lib/notifyListener'; -import { settings } from '../../../settings/server'; -import { businessHourManager } from '../business-hour'; -import { parseAgentCustomFields, updateDepartmentAgents, normalizeTransferredByData } from './Helper'; -import { RoutingManager } from './RoutingManager'; -import { Visitors, type RegisterGuestType } from './Visitors'; -import { registerGuestData } from './contacts/registerGuestData'; -import { cleanGuestHistory } from './tracking'; - -type AKeyOf = { - [K in keyof T]?: T[K]; -}; - -type ICRMData = { - _id: string; - label?: string; - topic?: string; - createdAt: Date; - lastMessageAt?: Date; - tags?: string[]; - customFields?: IOmnichannelRoom['livechatData']; - visitor: Pick & { - email?: ILivechatVisitor['visitorEmails']; - os?: string; - browser?: string; - customFields: ILivechatVisitor['livechatData']; - }; - agent?: Pick & { - email?: NonNullable[number]['address']; - }; - crmData?: IOmnichannelRoom['crmData']; -}; - -class LivechatClass { - logger: Logger; - - constructor() { - this.logger = new Logger('Livechat'); - } - - async online(department?: string, skipNoAgentSetting = false, skipFallbackCheck = false): Promise { - Livechat.logger.debug(`Checking online agents ${department ? `for department ${department}` : ''}`); - if (!skipNoAgentSetting && settings.get('Livechat_accept_chats_with_no_agents')) { - Livechat.logger.debug('Can accept without online agents: true'); - return true; - } - - if (settings.get('Livechat_assign_new_conversation_to_bot')) { - Livechat.logger.debug(`Fetching online bot agents for department ${department}`); - const botAgents = await Livechat.getBotAgents(department); - if (botAgents) { - const onlineBots = await Livechat.countBotAgents(department); - this.logger.debug(`Found ${onlineBots} online`); - if (onlineBots > 0) { - return true; - } - } - } - - const agentsOnline = await this.checkOnlineAgents(department, undefined, skipFallbackCheck); - Livechat.logger.debug(`Are online agents ${department ? `for department ${department}` : ''}?: ${agentsOnline}`); - return agentsOnline; - } - - async checkOnlineAgents(department?: string, agent?: { agentId: string }, skipFallbackCheck = false): Promise { - if (agent?.agentId) { - return Users.checkOnlineAgents(agent.agentId, settings.get('Livechat_enabled_when_agent_idle')); - } - - if (department) { - const onlineForDep = await LivechatDepartmentAgents.checkOnlineForDepartment(department); - if (onlineForDep || skipFallbackCheck) { - return onlineForDep; - } - - const dep = await LivechatDepartment.findOneById>(department, { - projection: { fallbackForwardDepartment: 1 }, - }); - if (!dep?.fallbackForwardDepartment) { - return onlineForDep; - } - - return this.checkOnlineAgents(dep?.fallbackForwardDepartment); - } - - return Users.checkOnlineAgents(undefined, settings.get('Livechat_enabled_when_agent_idle')); - } - - async removeRoom(rid: string) { - Livechat.logger.debug(`Deleting room ${rid}`); - check(rid, String); - const room = await LivechatRooms.findOneById(rid); - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room'); - } - - const inquiry = await LivechatInquiry.findOneByRoomId(rid); - - const result = await Promise.allSettled([ - Messages.removeByRoomId(rid), - ReadReceipts.removeByRoomId(rid), - Subscriptions.removeByRoomId(rid, { - async onTrash(doc) { - void notifyOnSubscriptionChanged(doc, 'removed'); - }, - }), - LivechatInquiry.removeByRoomId(rid), - LivechatRooms.removeById(rid), - ]); - - if (result[3]?.status === 'fulfilled' && result[3].value?.deletedCount && inquiry) { - void notifyOnLivechatInquiryChanged(inquiry, 'removed'); - } - - for (const r of result) { - if (r.status === 'rejected') { - this.logger.error(`Error removing room ${rid}: ${r.reason}`); - throw new Meteor.Error('error-removing-room', 'Error removing room'); - } - } - } - - async registerGuest(newData: RegisterGuestType): Promise { - const result = await Visitors.registerGuest(newData); - - if (result) { - await registerGuestData(newData, result); - } - - return result; - } - - private async getBotAgents(department?: string) { - if (department) { - return LivechatDepartmentAgents.getBotsForDepartment(department); - } - - return Users.findBotAgents(); - } - - private async countBotAgents(department?: string) { - if (department) { - return LivechatDepartmentAgents.countBotsForDepartment(department); - } - - return Users.countBotAgents(); - } - - async saveAgentInfo(_id: string, agentData: any, agentDepartments: string[]) { - check(_id, String); - check(agentData, Object); - check(agentDepartments, [String]); - - const user = await Users.findOneById(_id); - if (!user || !(await hasRoleAsync(_id, 'livechat-agent'))) { - throw new Meteor.Error('error-user-is-not-agent', 'User is not a livechat agent'); - } - - await Users.setLivechatData(_id, agentData); - - const currentDepartmentsForAgent = await LivechatDepartmentAgents.findByAgentId(_id).toArray(); - - const toRemoveIds = currentDepartmentsForAgent - .filter((dept) => !agentDepartments.includes(dept.departmentId)) - .map((dept) => dept.departmentId); - const toAddIds = agentDepartments.filter((d) => !currentDepartmentsForAgent.some((c) => c.departmentId === d)); - - await Promise.all( - await LivechatDepartment.findInIds([...toRemoveIds, ...toAddIds], { - projection: { - _id: 1, - enabled: 1, - }, - }) - .map((dep) => { - return updateDepartmentAgents( - dep._id, - { - ...(toRemoveIds.includes(dep._id) ? { remove: [{ agentId: _id }] } : { upsert: [{ agentId: _id, count: 0, order: 0 }] }), - }, - dep.enabled, - ); - }) - .toArray(), - ); - - return true; - } - - async updateCallStatus(callId: string, rid: string, status: 'ended' | 'declined', user: IUser | ILivechatVisitor) { - await Rooms.setCallStatus(rid, status); - if (status === 'ended' || status === 'declined') { - if (await VideoConf.declineLivechatCall(callId)) { - return; - } - - return updateMessage({ _id: callId, msg: status, actionLinks: [], webRtcCallEndTs: new Date(), rid }, user as unknown as IUser); - } - } - - notifyRoomVisitorChange(roomId: string, visitor: ILivechatVisitor) { - void api.broadcast('omnichannel.room', roomId, { - type: 'visitorData', - visitor, - }); - } - - async changeRoomVisitor(userId: string, room: IOmnichannelRoom, visitor: ILivechatVisitor) { - const user = await Users.findOneById(userId, { projection: { _id: 1 } }); - if (!user) { - throw new Error('error-user-not-found'); - } - - if (!(await canAccessRoomAsync(room, user))) { - throw new Error('error-not-allowed'); - } - - await LivechatRooms.changeVisitorByRoomId(room._id, visitor); - - this.notifyRoomVisitorChange(room._id, visitor); - - return LivechatRooms.findOneById(room._id); - } - - async notifyAgentStatusChanged(userId: string, status?: UserStatus) { - if (!status) { - return; - } - - void callbacks.runAsync('livechat.agentStatusChanged', { userId, status }); - if (!settings.get('Livechat_show_agent_info')) { - return; - } - - await LivechatRooms.findOpenByAgent(userId).forEach((room) => { - void api.broadcast('omnichannel.room', room._id, { - type: 'agentStatus', - status, - }); - }); - } - - async transfer(room: IOmnichannelRoom, guest: ILivechatVisitor, transferData: TransferData) { - this.logger.debug(`Transfering room ${room._id} [Transfered by: ${transferData?.transferredBy?._id}]`); - if (room.onHold) { - throw new Error('error-room-onHold'); - } - - if (transferData.departmentId) { - const department = await LivechatDepartment.findOneById>(transferData.departmentId, { - projection: { name: 1 }, - }); - if (!department) { - throw new Error('error-invalid-department'); - } - - transferData.department = department; - this.logger.debug(`Transfering room ${room._id} to department ${transferData.department?._id}`); - } - - return RoutingManager.transferRoom(room, guest, transferData); - } - - async forwardOpenChats(userId: string) { - this.logger.debug(`Transferring open chats for user ${userId}`); - const user = await Users.findOneById(userId); - if (!user) { - throw new Error('error-invalid-user'); - } - - const { _id, username, name } = user; - for await (const room of LivechatRooms.findOpenByAgent(userId)) { - const guest = await LivechatVisitors.findOneEnabledById(room.v._id); - if (!guest) { - continue; - } - - const transferredBy = normalizeTransferredByData({ _id, username, name }, room); - await this.transfer(room, guest, { - transferredBy, - departmentId: guest.department, - }); - } - } - - async removeGuest(_id: string) { - const guest = await LivechatVisitors.findOneEnabledById(_id, { projection: { _id: 1, token: 1 } }); - if (!guest) { - throw new Error('error-invalid-guest'); - } - - await cleanGuestHistory(guest); - return LivechatVisitors.disableById(_id); - } - - async setUserStatusLivechatIf(userId: string, status: ILivechatAgentStatus, condition?: Filter, fields?: AKeyOf) { - const result = await Users.setLivechatStatusIf(userId, status, condition, fields); - - if (result.modifiedCount > 0) { - void notifyOnUserChange({ - id: userId, - clientAction: 'updated', - diff: { ...fields, statusLivechat: status }, - }); - } - - callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status }); - return result; - } - - async returnRoomAsInquiry(room: IOmnichannelRoom, departmentId?: string, overrideTransferData: any = {}) { - this.logger.debug({ msg: `Transfering room to ${departmentId ? 'department' : ''} queue`, room }); - if (!room.open) { - throw new Meteor.Error('room-closed'); - } - - if (room.onHold) { - throw new Meteor.Error('error-room-onHold'); - } - - if (!room.servedBy) { - return false; - } - - const user = await Users.findOneById(room.servedBy._id); - if (!user?._id) { - throw new Meteor.Error('error-invalid-user'); - } - - // find inquiry corresponding to room - const inquiry = await LivechatInquiry.findOne({ rid: room._id }); - if (!inquiry) { - return false; - } - - const transferredBy = normalizeTransferredByData(user, room); - this.logger.debug(`Transfering room ${room._id} by user ${transferredBy._id}`); - const transferData = { roomId: room._id, scope: 'queue', departmentId, transferredBy, ...overrideTransferData }; - try { - await this.saveTransferHistory(room, transferData); - await RoutingManager.unassignAgent(inquiry, departmentId); - } catch (e) { - this.logger.error(e); - throw new Meteor.Error('error-returning-inquiry'); - } - - callbacks.runAsync('livechat:afterReturnRoomAsInquiry', { room }); - - return true; - } - - async saveTransferHistory(room: IOmnichannelRoom, transferData: TransferData) { - const { departmentId: previousDepartment } = room; - const { department: nextDepartment, transferredBy, transferredTo, scope, comment } = transferData; - - check( - transferredBy, - Match.ObjectIncluding({ - _id: String, - username: String, - name: Match.Maybe(String), - userType: String, - }), - ); - - const { _id, username } = transferredBy; - const scopeData = scope || (nextDepartment ? 'department' : 'agent'); - this.logger.info(`Storing new chat transfer of ${room._id} [Transfered by: ${_id} to ${scopeData}]`); - - const transferMessage = { - ...(transferData.transferredBy.userType === 'visitor' && { token: room.v.token }), - transferData: { - transferredBy, - ts: new Date(), - scope: scopeData, - comment, - ...(previousDepartment && { previousDepartment }), - ...(nextDepartment && { nextDepartment }), - ...(transferredTo && { transferredTo }), - }, - }; - - await Message.saveSystemMessageAndNotifyUser('livechat_transfer_history', room._id, '', { _id, username }, transferMessage); - } - - async saveGuest(guestData: Pick & { email?: string; phone?: string }, userId: string) { - const { _id, name, email, phone, livechatData = {} } = guestData; - - const visitor = await LivechatVisitors.findOneById(_id, { projection: { _id: 1 } }); - if (!visitor) { - throw new Error('error-invalid-visitor'); - } - - this.logger.debug({ msg: 'Saving guest', guestData }); - const updateData: { - name?: string | undefined; - username?: string | undefined; - email?: string | undefined; - phone?: string | undefined; - livechatData: { - [k: string]: any; - }; - } = { livechatData: {} }; - - if (name) { - updateData.name = name; - } - if (email) { - updateData.email = email; - } - if (phone) { - updateData.phone = phone; - } - - const customFields: Record = {}; - - if ((!userId || (await hasPermissionAsync(userId, 'edit-livechat-room-customfields'))) && Object.keys(livechatData).length) { - this.logger.debug({ msg: `Saving custom fields for visitor ${_id}`, livechatData }); - for await (const field of LivechatCustomField.findByScope('visitor')) { - if (!livechatData.hasOwnProperty(field._id)) { - continue; - } - const value = trim(livechatData[field._id]); - if (value !== '' && field.regexp !== undefined && field.regexp !== '') { - const regexp = new RegExp(field.regexp); - if (!regexp.test(value)) { - throw new Error(i18n.t('error-invalid-custom-field-value')); - } - } - customFields[field._id] = value; - } - updateData.livechatData = customFields; - Livechat.logger.debug(`About to update ${Object.keys(customFields).length} custom fields for visitor ${_id}`); - } - const ret = await LivechatVisitors.saveGuestById(_id, updateData); - - setImmediate(() => { - void Apps.self?.triggerEvent(AppEvents.IPostLivechatGuestSaved, _id); - }); - - return ret; - } - - async setCustomFields({ token, key, value, overwrite }: { key: string; value: string; overwrite: boolean; token: string }) { - Livechat.logger.debug(`Setting custom fields data for visitor with token ${token}`); - - const customField = await LivechatCustomField.findOneById(key); - if (!customField) { - throw new Error('invalid-custom-field'); - } - - if (customField.regexp !== undefined && customField.regexp !== '') { - const regexp = new RegExp(customField.regexp); - if (!regexp.test(value)) { - throw new Error(i18n.t('error-invalid-custom-field-value', { field: key })); - } - } - - let result; - if (customField.scope === 'room') { - result = await LivechatRooms.updateDataByToken(token, key, value, overwrite); - } else { - result = await LivechatVisitors.updateLivechatDataByToken(token, key, value, overwrite); - } - - if (typeof result === 'boolean') { - // Note: this only happens when !overwrite is passed, in this case we don't do any db update - return 0; - } - - return result.modifiedCount; - } - - async afterRemoveAgent(user: AtLeast) { - await callbacks.run('livechat.afterAgentRemoved', { agent: user }); - return true; - } - - async removeAgent(username: string) { - const user = await Users.findOneByUsername(username, { projection: { _id: 1, username: 1 } }); - - if (!user) { - throw new Error('error-invalid-user'); - } - - const { _id } = user; - - if (await removeUserFromRolesAsync(_id, ['livechat-agent'])) { - return this.afterRemoveAgent(user); - } - - return false; - } - - async removeManager(username: string) { - const user = await Users.findOneByUsername(username, { projection: { _id: 1 } }); - - if (!user) { - throw new Error('error-invalid-user'); - } - - return removeUserFromRolesAsync(user._id, ['livechat-manager']); - } - - async getLivechatRoomGuestInfo(room: IOmnichannelRoom) { - const visitor = await LivechatVisitors.findOneEnabledById(room.v._id); - if (!visitor) { - throw new Error('error-invalid-visitor'); - } - - const agent = room.servedBy?._id ? await Users.findOneById(room.servedBy?._id) : null; - - const ua = new UAParser(); - ua.setUA(visitor.userAgent || ''); - - const postData: ICRMData = { - _id: room._id, - label: room.fname || room.label, // using same field for compatibility - topic: room.topic, - createdAt: room.ts, - lastMessageAt: room.lm, - tags: room.tags, - customFields: room.livechatData, - visitor: { - _id: visitor._id, - token: visitor.token, - name: visitor.name, - username: visitor.username, - department: visitor.department, - ip: visitor.ip, - os: ua.getOS().name && `${ua.getOS().name} ${ua.getOS().version}`, - browser: ua.getBrowser().name && `${ua.getBrowser().name} ${ua.getBrowser().version}`, - customFields: visitor.livechatData, - }, - }; - - if (agent) { - const customFields = parseAgentCustomFields(agent.customFields); - - postData.agent = { - _id: agent._id, - username: agent.username, - name: agent.name, - ...(customFields && { customFields }), - }; - - if (agent.emails && agent.emails.length > 0) { - postData.agent.email = agent.emails[0].address; - } - } - - if (room.crmData) { - postData.crmData = room.crmData; - } - - if (visitor.visitorEmails && visitor.visitorEmails.length > 0) { - postData.visitor.email = visitor.visitorEmails; - } - if (visitor.phone && visitor.phone.length > 0) { - postData.visitor.phone = visitor.phone; - } - - return postData; - } - - async allowAgentChangeServiceStatus(statusLivechat: ILivechatAgentStatus, agentId: string) { - if (statusLivechat !== ILivechatAgentStatus.AVAILABLE) { - return true; - } - - return businessHourManager.allowAgentChangeServiceStatus(agentId); - } - - async notifyGuestStatusChanged(token: string, status: UserStatus) { - await LivechatRooms.updateVisitorStatus(token, status); - - const inquiryVisitorStatus = await LivechatInquiry.updateVisitorStatus(token, status); - - if (inquiryVisitorStatus.modifiedCount) { - void notifyOnLivechatInquiryChangedByToken(token, 'updated', { v: { status } }); - } - } - - async setUserStatusLivechat(userId: string, status: ILivechatAgentStatus) { - const user = await Users.setLivechatStatus(userId, status); - callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status }); - - if (user.modifiedCount > 0) { - void notifyOnUserChange({ - id: userId, - clientAction: 'updated', - diff: { - statusLivechat: status, - livechatStatusSystemModified: false, - }, - }); - } - - return user; - } - - async afterAgentAdded(user: IUser) { - await Promise.all([ - Users.setOperator(user._id, true), - this.setUserStatusLivechat(user._id, user.status !== 'offline' ? ILivechatAgentStatus.AVAILABLE : ILivechatAgentStatus.NOT_AVAILABLE), - ]); - callbacks.runAsync('livechat.onNewAgentCreated', user._id); - - return user; - } - - async addAgent(username: string) { - check(username, String); - - const user = await Users.findOneByUsername(username, { projection: { _id: 1, username: 1 } }); - - if (!user) { - throw new Meteor.Error('error-invalid-user'); - } - - if (await addUserRolesAsync(user._id, ['livechat-agent'])) { - return this.afterAgentAdded(user); - } - - return false; - } - - async afterAgentUserActivated(user: IUser) { - if (!user.roles.includes('livechat-agent')) { - throw new Error('invalid-user-role'); - } - await Users.setOperator(user._id, true); - callbacks.runAsync('livechat.onNewAgentCreated', user._id); - } - - async addManager(username: string) { - check(username, String); - - const user = await Users.findOneByUsername(username, { projection: { _id: 1, username: 1 } }); - - if (!user) { - throw new Meteor.Error('error-invalid-user'); - } - - if (await addUserRolesAsync(user._id, ['livechat-manager'])) { - return user; - } - - return false; - } -} - -export const Livechat = new LivechatClass(); diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index 280836d1fea86..7460bc6bc8247 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -20,10 +20,9 @@ import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { createLivechatRoom, createLivechatInquiry, allowAgentSkipQueue, prepareLivechatRoom } from './Helper'; -import { Livechat } from './LivechatTyped'; import { RoutingManager } from './RoutingManager'; import { isVerifiedChannelInSource } from './contacts/isVerifiedChannelInSource'; -import { getOnlineAgents } from './getOnlineAgents'; +import { checkOnlineAgents, getOnlineAgents } from './service-status'; import { getInquirySortMechanismSetting } from './settings'; import { dispatchInquiryPosition } from '../../../../ee/app/livechat-enterprise/server/lib/Helper'; import { callbacks } from '../../../../lib/callbacks'; @@ -328,7 +327,7 @@ export class QueueManager { throw new Meteor.Error('no-agent-online', 'Sorry, no online agents'); } - if (!agent && !guest.department && !(await Livechat.checkOnlineAgents())) { + if (!agent && !guest.department && !(await checkOnlineAgents())) { throw new Meteor.Error('no-agent-online', 'Sorry, no online agents'); } } diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index a62c7dfea46be..11140feb08a10 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -175,7 +175,7 @@ export const RoutingManager: Routing = { const { servedBy } = room; if (shouldQueue) { - const queuedInquiry = await LivechatInquiry.queueInquiry(inquiry._id); + const queuedInquiry = await LivechatInquiry.queueInquiry(inquiry._id, room.lastMessage); if (queuedInquiry) { inquiry = queuedInquiry; void notifyOnLivechatInquiryChanged(inquiry, 'updated', { diff --git a/apps/meteor/app/livechat/server/lib/Visitors.ts b/apps/meteor/app/livechat/server/lib/Visitors.ts index 83e37e76d12a0..6ff046b3fc211 100644 --- a/apps/meteor/app/livechat/server/lib/Visitors.ts +++ b/apps/meteor/app/livechat/server/lib/Visitors.ts @@ -64,7 +64,7 @@ export const Visitors = { const agent = await Users.findOneOnlineAgentById(contact.contactManager, shouldConsiderIdleAgent, { projection: { _id: 1, username: 1, name: 1, emails: 1 }, }); - if (agent && agent.username && agent.name && agent.emails) { + if (agent?.username && agent.name && agent.emails) { visitorDataToUpdate.contactManager = { _id: agent._id, username: agent.username, diff --git a/apps/meteor/app/livechat/server/lib/contacts/createContact.ts b/apps/meteor/app/livechat/server/lib/contacts/createContact.ts index 98cf238d9b5e3..44efef5b2e220 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/createContact.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/createContact.ts @@ -40,12 +40,12 @@ export async function createContact({ return LivechatContacts.insertContact({ name, - emails: emails?.map((address) => ({ address })), - phones: phones?.map((phoneNumber) => ({ phoneNumber })), - contactManager, - channels, - customFields, - lastChat, + ...(emails && { emails: emails?.map((address) => ({ address })) }), + ...(phones && { phones: phones?.map((phoneNumber) => ({ phoneNumber })) }), + ...(contactManager && { contactManager }), + ...(channels && { channels }), + ...(customFields && { customFields }), + ...(lastChat && { lastChat }), unknown, ...(importIds?.length && { importIds }), }); diff --git a/apps/meteor/app/livechat/server/lib/contacts/getContacts.ts b/apps/meteor/app/livechat/server/lib/contacts/getContacts.ts index 09b7a2545a1cc..ad9a4cbadf174 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/getContacts.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/getContacts.ts @@ -1,4 +1,5 @@ import type { IUser } from '@rocket.chat/core-typings'; +import { isNotUndefined } from '@rocket.chat/core-typings'; import { LivechatContacts, Users } from '@rocket.chat/models'; import type { PaginatedResult, ILivechatContactWithManagerData } from '@rocket.chat/rest-typings'; import type { FindCursor, Sort } from 'mongodb'; @@ -25,7 +26,7 @@ export async function getContacts(params: GetContactsParams): Promise contactManager))]; + const managerIds = [...new Set(rawContacts.map(({ contactManager }) => contactManager))].filter(isNotUndefined); const managersCursor: FindCursor<[string, Pick]> = Users.findByIds(managerIds, { projection: { name: 1, username: 1 }, }).map((manager) => [manager._id, manager]); diff --git a/apps/meteor/app/livechat/server/lib/contacts/registerContact.spec.ts b/apps/meteor/app/livechat/server/lib/contacts/registerContact.spec.ts index 03562a881a9e4..9eb3499a8b175 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/registerContact.spec.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/registerContact.spec.ts @@ -37,13 +37,6 @@ const { registerContact } = proxyquire.noCallThru().load('./registerContact', { '@rocket.chat/models': modelsMock, '@rocket.chat/tools': { wrapExceptions: sinon.stub() }, './Helper': { validateEmail: sinon.stub() }, - './LivechatTyped': { - Livechat: { - logger: { - debug: sinon.stub(), - }, - }, - }, }); describe('registerContact', () => { diff --git a/apps/meteor/app/livechat/server/lib/contacts/registerGuestData.ts b/apps/meteor/app/livechat/server/lib/contacts/registerGuestData.ts deleted file mode 100644 index 471104aecae9d..0000000000000 --- a/apps/meteor/app/livechat/server/lib/contacts/registerGuestData.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { AtLeast, ILivechatVisitor } from '@rocket.chat/core-typings'; -import { LivechatContacts } from '@rocket.chat/models'; -import { wrapExceptions } from '@rocket.chat/tools'; - -import { validateEmail } from '../Helper'; -import type { RegisterGuestType } from '../Visitors'; -import { ContactMerger, type FieldAndValue } from './ContactMerger'; - -export async function registerGuestData( - { name, phone, email, username }: Pick, - visitor: AtLeast, -): Promise { - const validatedEmail = - email && - wrapExceptions(() => { - const trimmedEmail = email.trim().toLowerCase(); - validateEmail(trimmedEmail); - return trimmedEmail; - }).suppress(); - - const fields = [ - { type: 'name', value: name }, - { type: 'phone', value: phone?.number }, - { type: 'email', value: validatedEmail }, - { type: 'username', value: username || visitor.username }, - ].filter((field) => Boolean(field.value)) as FieldAndValue[]; - - if (!fields.length) { - return; - } - - // If a visitor was updated who already had contacts, load up the contacts and update that information as well - const contacts = await LivechatContacts.findAllByVisitorId(visitor._id).toArray(); - for await (const contact of contacts) { - await ContactMerger.mergeFieldsIntoContact({ - fields, - contact, - conflictHandlingMode: contact.unknown ? 'overwrite' : 'conflict', - }); - } -} diff --git a/apps/meteor/app/livechat/server/lib/contacts/updateContact.ts b/apps/meteor/app/livechat/server/lib/contacts/updateContact.ts index 6c344274386e6..cabb0359796a3 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/updateContact.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/updateContact.ts @@ -73,11 +73,11 @@ export async function updateContact(params: UpdateContactParams): Promise ({ address })), - phones: phones?.map((phoneNumber) => ({ phoneNumber })), - contactManager, - channels, - customFields: customFieldsToUpdate, + ...(emails && { emails: emails?.map((address) => ({ address })) }), + ...(phones && { phones: phones?.map((phoneNumber) => ({ phoneNumber })) }), + ...(contactManager && { contactManager }), + ...(channels && { channels }), + ...(customFieldsToUpdate && { customFields: customFieldsToUpdate }), ...(wipeConflicts && { conflictingFields: [] }), }); diff --git a/apps/meteor/app/livechat/server/lib/custom-fields.ts b/apps/meteor/app/livechat/server/lib/custom-fields.ts new file mode 100644 index 0000000000000..e63fb2e43c6dc --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/custom-fields.ts @@ -0,0 +1,82 @@ +import type { ILivechatContact, ILivechatCustomField } from '@rocket.chat/core-typings'; +import { LivechatContacts, LivechatCustomField, LivechatRooms, LivechatVisitors } from '@rocket.chat/models'; + +import { livechatLogger } from './logger'; +import { i18n } from '../../../utils/lib/i18n'; + +export const validateRequiredCustomFields = (customFields: string[], livechatCustomFields: ILivechatCustomField[]) => { + const errors: string[] = []; + const requiredCustomFields = livechatCustomFields.filter((field) => field.required); + + requiredCustomFields.forEach((field) => { + if (!customFields.find((f) => f === field._id)) { + errors.push(field._id); + } + }); + + if (errors.length > 0) { + throw new Error(`Missing required custom fields: ${errors.join(', ')}`); + } +}; + +export async function updateContactsCustomFields(contact: ILivechatContact, key: string, value: string, overwrite: boolean): Promise { + if (overwrite || !contact.customFields || !contact.customFields[key]) { + contact.customFields ??= {}; + contact.customFields[key] = value; + } else { + contact.conflictingFields ??= []; + contact.conflictingFields.push({ field: `customFields.${key}`, value }); + } + + await LivechatContacts.updateContact(contact._id, { customFields: contact.customFields, conflictingFields: contact.conflictingFields }); + + livechatLogger.debug({ msg: `Contact ${contact._id} updated with custom fields` }); +} + +export async function setCustomFields({ + token, + key, + value, + overwrite, +}: { + key: string; + value: string; + overwrite: boolean; + token: string; +}): Promise { + livechatLogger.debug(`Setting custom fields data for visitor with token ${token}`); + + const customField = await LivechatCustomField.findOneById(key); + if (!customField) { + throw new Error('invalid-custom-field'); + } + + if (customField.regexp !== undefined && customField.regexp !== '') { + const regexp = new RegExp(customField.regexp); + if (!regexp.test(value)) { + throw new Error(i18n.t('error-invalid-custom-field-value', { field: key })); + } + } + + let result; + if (customField.scope === 'room') { + result = await LivechatRooms.updateDataByToken(token, key, value, overwrite); + } else { + result = await LivechatVisitors.updateLivechatDataByToken(token, key, value, overwrite); + + const visitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } }); + if (visitor) { + const contacts = await LivechatContacts.findAllByVisitorId(visitor._id).toArray(); + if (contacts.length > 0) { + await Promise.all(contacts.map((contact) => updateContactsCustomFields(contact, key, value, overwrite))); + } + } + } + + if (typeof result === 'boolean') { + // Note: this only happens when !overwrite is passed, in this case we don't do any db update + return 0; + } + + return result.modifiedCount; +} diff --git a/apps/meteor/app/livechat/server/lib/departmentsLib.ts b/apps/meteor/app/livechat/server/lib/departmentsLib.ts index 7dec370768f0d..2a9a505064b25 100644 --- a/apps/meteor/app/livechat/server/lib/departmentsLib.ts +++ b/apps/meteor/app/livechat/server/lib/departmentsLib.ts @@ -1,3 +1,4 @@ +import { AppEvents, Apps } from '@rocket.chat/apps'; import type { LivechatDepartmentDTO, ILivechatDepartment, ILivechatDepartmentAgents } from '@rocket.chat/core-typings'; import { LivechatDepartment, LivechatDepartmentAgents, LivechatVisitors, LivechatRooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; @@ -133,6 +134,10 @@ export async function saveDepartment( // Disable event if (department?.enabled && !departmentDB?.enabled) { await callbacks.run('livechat.afterDepartmentDisabled', departmentDB); + void Apps.self + ?.getBridges() + ?.getListenerBridge() + .livechatEvent(AppEvents.IPostLivechatDepartmentDisabled, { department: departmentDB }); } if (departmentUnit) { @@ -269,7 +274,7 @@ export async function removeDepartment(departmentId: string) { } }); - const { deletedCount } = await removeByDept; + const { deletedCount } = promiseResponses[0].status === 'fulfilled' ? promiseResponses[0].value : { deletedCount: 0 }; if (deletedCount > 0) { removedAgents.forEach(({ _id: docId, agentId }) => { @@ -285,6 +290,7 @@ export async function removeDepartment(departmentId: string) { } await callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds: removedAgents.map(({ agentId }) => agentId) }); + void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatDepartmentRemoved, { department }); return ret; } diff --git a/apps/meteor/app/livechat/server/lib/getOnlineAgents.ts b/apps/meteor/app/livechat/server/lib/getOnlineAgents.ts deleted file mode 100644 index 69b89cdc1c907..0000000000000 --- a/apps/meteor/app/livechat/server/lib/getOnlineAgents.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ILivechatAgent, SelectedAgent } from '@rocket.chat/core-typings'; -import { Users, LivechatDepartmentAgents } from '@rocket.chat/models'; -import type { FindCursor } from 'mongodb'; - -import { settings } from '../../../settings/server'; - -export async function getOnlineAgents(department?: string, agent?: SelectedAgent | null): Promise | undefined> { - if (agent?.agentId) { - return Users.findOnlineAgents(agent.agentId, settings.get('Livechat_enabled_when_agent_idle')); - } - - if (department) { - const departmentAgents = await LivechatDepartmentAgents.getOnlineForDepartment(department); - if (!departmentAgents) { - return; - } - - const agentIds = await departmentAgents.map(({ agentId }) => agentId).toArray(); - if (!agentIds.length) { - return; - } - - return Users.findByIds([...new Set(agentIds)]); - } - return Users.findOnlineAgents(undefined, settings.get('Livechat_enabled_when_agent_idle')); -} diff --git a/apps/meteor/app/livechat/server/lib/guests.ts b/apps/meteor/app/livechat/server/lib/guests.ts new file mode 100644 index 0000000000000..92a6dbf9b7c15 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/guests.ts @@ -0,0 +1,241 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; +import type { ILivechatVisitor, IOmnichannelRoom, UserStatus } from '@rocket.chat/core-typings'; +import { + LivechatVisitors, + LivechatCustomField, + LivechatInquiry, + LivechatRooms, + Messages, + ReadReceipts, + Subscriptions, + LivechatContacts, + Users, +} from '@rocket.chat/models'; +import { wrapExceptions } from '@rocket.chat/tools'; +import UAParser from 'ua-parser-js'; + +import { parseAgentCustomFields, validateEmail } from './Helper'; +import type { RegisterGuestType } from './Visitors'; +import { Visitors } from './Visitors'; +import { ContactMerger, type FieldAndValue } from './contacts/ContactMerger'; +import type { ICRMData } from './localTypes'; +import { livechatLogger } from './logger'; +import { trim } from '../../../../lib/utils/stringUtils'; +import { i18n } from '../../../../server/lib/i18n'; +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { FileUpload } from '../../../file-upload/server'; +import { + notifyOnSubscriptionChanged, + notifyOnLivechatInquiryChanged, + notifyOnLivechatInquiryChangedByToken, +} from '../../../lib/server/lib/notifyListener'; + +export async function saveGuest( + guestData: Pick & { email?: string; phone?: string }, + userId: string, +) { + const { _id, name, email, phone, livechatData = {} } = guestData; + + const visitor = await LivechatVisitors.findOneById(_id, { projection: { _id: 1 } }); + if (!visitor) { + throw new Error('error-invalid-visitor'); + } + + livechatLogger.debug({ msg: 'Saving guest', guestData }); + const updateData: { + name?: string | undefined; + username?: string | undefined; + email?: string | undefined; + phone?: string | undefined; + livechatData: { + [k: string]: any; + }; + } = { livechatData: {} }; + + if (name) { + updateData.name = name; + } + if (email) { + updateData.email = email; + } + if (phone) { + updateData.phone = phone; + } + + const customFields: Record = {}; + + if ((!userId || (await hasPermissionAsync(userId, 'edit-livechat-room-customfields'))) && Object.keys(livechatData).length) { + livechatLogger.debug({ msg: `Saving custom fields for visitor ${_id}`, livechatData }); + for await (const field of LivechatCustomField.findByScope('visitor')) { + if (!livechatData.hasOwnProperty(field._id)) { + continue; + } + const value = trim(livechatData[field._id]); + if (value !== '' && field.regexp !== undefined && field.regexp !== '') { + const regexp = new RegExp(field.regexp); + if (!regexp.test(value)) { + throw new Error(i18n.t('error-invalid-custom-field-value')); + } + } + customFields[field._id] = value; + } + updateData.livechatData = customFields; + livechatLogger.debug(`About to update ${Object.keys(customFields).length} custom fields for visitor ${_id}`); + } + const ret = await LivechatVisitors.saveGuestById(_id, updateData); + + setImmediate(() => { + void Apps.self?.triggerEvent(AppEvents.IPostLivechatGuestSaved, _id); + }); + + return ret; +} + +export async function removeGuest(_id: string) { + const guest = await LivechatVisitors.findOneEnabledById(_id, { projection: { _id: 1, token: 1 } }); + if (!guest) { + throw new Error('error-invalid-guest'); + } + + await cleanGuestHistory(guest.token); + return LivechatVisitors.disableById(_id); +} + +export async function registerGuest(newData: RegisterGuestType): Promise { + const visitor = await Visitors.registerGuest(newData); + if (!visitor) { + return null; + } + + const { name, phone, email, username } = newData; + + const validatedEmail = + email && + wrapExceptions(() => { + const trimmedEmail = email.trim().toLowerCase(); + validateEmail(trimmedEmail); + return trimmedEmail; + }).suppress(); + + const fields = [ + { type: 'name', value: name }, + { type: 'phone', value: phone?.number }, + { type: 'email', value: validatedEmail }, + { type: 'username', value: username || visitor.username }, + ].filter((field) => Boolean(field.value)) as FieldAndValue[]; + + if (!fields.length) { + return null; + } + + // If a visitor was updated who already had contacts, load up the contacts and update that information as well + const contacts = await LivechatContacts.findAllByVisitorId(visitor._id).toArray(); + for await (const contact of contacts) { + await ContactMerger.mergeFieldsIntoContact({ + fields, + contact, + conflictHandlingMode: contact.unknown ? 'overwrite' : 'conflict', + }); + } + + return visitor; +} + +async function cleanGuestHistory(token: string) { + // This shouldn't be possible, but just in case + if (!token) { + throw new Error('error-invalid-guest'); + } + + const cursor = LivechatRooms.findByVisitorToken(token); + for await (const room of cursor) { + await Promise.all([ + Subscriptions.removeByRoomId(room._id, { + async onTrash(doc) { + void notifyOnSubscriptionChanged(doc, 'removed'); + }, + }), + FileUpload.removeFilesByRoomId(room._id), + Messages.removeByRoomId(room._id), + ReadReceipts.removeByRoomId(room._id), + ]); + } + + await LivechatRooms.removeByVisitorToken(token); + + const livechatInquiries = await LivechatInquiry.findIdsByVisitorToken(token).toArray(); + await LivechatInquiry.removeByIds(livechatInquiries.map(({ _id }) => _id)); + void notifyOnLivechatInquiryChanged(livechatInquiries, 'removed'); +} + +export async function getLivechatRoomGuestInfo(room: IOmnichannelRoom) { + const visitor = await LivechatVisitors.findOneEnabledById(room.v._id); + if (!visitor) { + throw new Error('error-invalid-visitor'); + } + + const agent = room.servedBy?._id ? await Users.findOneById(room.servedBy?._id) : null; + + const ua = new UAParser(); + ua.setUA(visitor.userAgent || ''); + + const postData: ICRMData = { + _id: room._id, + label: room.fname || room.label, // using same field for compatibility + topic: room.topic, + createdAt: room.ts, + lastMessageAt: room.lm, + tags: room.tags, + customFields: room.livechatData, + visitor: { + _id: visitor._id, + token: visitor.token, + name: visitor.name, + username: visitor.username, + department: visitor.department, + ip: visitor.ip, + os: ua.getOS().name && `${ua.getOS().name} ${ua.getOS().version}`, + browser: ua.getBrowser().name && `${ua.getBrowser().name} ${ua.getBrowser().version}`, + customFields: visitor.livechatData, + }, + }; + + if (agent) { + const customFields = parseAgentCustomFields(agent.customFields); + + postData.agent = { + _id: agent._id, + username: agent.username, + name: agent.name, + ...(customFields && { customFields }), + }; + + if (agent.emails && agent.emails.length > 0) { + postData.agent.email = agent.emails[0].address; + } + } + + if (room.crmData) { + postData.crmData = room.crmData; + } + + if (visitor.visitorEmails && visitor.visitorEmails.length > 0) { + postData.visitor.email = visitor.visitorEmails; + } + if (visitor.phone && visitor.phone.length > 0) { + postData.visitor.phone = visitor.phone; + } + + return postData; +} + +export async function notifyGuestStatusChanged(token: string, status: UserStatus) { + // TODO: a promise.all maybe? + await LivechatRooms.updateVisitorStatus(token, status); + + const inquiryVisitorStatus = await LivechatInquiry.updateVisitorStatus(token, status); + + if (inquiryVisitorStatus.modifiedCount) { + void notifyOnLivechatInquiryChangedByToken(token, 'updated', { v: { status } }); + } +} diff --git a/apps/meteor/app/livechat/server/lib/hooks.ts b/apps/meteor/app/livechat/server/lib/hooks.ts new file mode 100644 index 0000000000000..d877ce13b8f6e --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/hooks.ts @@ -0,0 +1,30 @@ +import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; +import type { AtLeast, IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; + +import { setUserStatusLivechat } from './utils'; +import { callbacks } from '../../../../lib/callbacks'; + +export async function afterAgentUserActivated(user: IUser) { + if (!user.roles.includes('livechat-agent')) { + throw new Error('invalid-user-role'); + } + // TODO: deprecate this `operator` property + await Users.setOperator(user._id, true); + callbacks.runAsync('livechat.onNewAgentCreated', user._id); +} + +export async function afterAgentAdded(user: IUser) { + await Promise.all([ + Users.setOperator(user._id, true), + setUserStatusLivechat(user._id, user.status !== 'offline' ? ILivechatAgentStatus.AVAILABLE : ILivechatAgentStatus.NOT_AVAILABLE), + ]); + callbacks.runAsync('livechat.onNewAgentCreated', user._id); + + return user; +} + +export async function afterRemoveAgent(user: AtLeast) { + await callbacks.run('livechat.afterAgentRemoved', { agent: user }); + return true; +} diff --git a/apps/meteor/app/livechat/server/lib/isMessageFromBot.ts b/apps/meteor/app/livechat/server/lib/isMessageFromBot.ts index 8c53bf7c555d0..e394366cba7ad 100644 --- a/apps/meteor/app/livechat/server/lib/isMessageFromBot.ts +++ b/apps/meteor/app/livechat/server/lib/isMessageFromBot.ts @@ -1,6 +1,6 @@ -import type { IMessage } from '@rocket.chat/core-typings'; +import type { IMessage, IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; -export async function isMessageFromBot(message: IMessage): Promise { +export async function isMessageFromBot(message: IMessage): Promise | null> { return Users.isUserInRole(message.u._id, 'bot'); } diff --git a/apps/meteor/app/livechat/server/lib/localTypes.ts b/apps/meteor/app/livechat/server/lib/localTypes.ts index b82ad13b05313..2676443e6e5a2 100644 --- a/apps/meteor/app/livechat/server/lib/localTypes.ts +++ b/apps/meteor/app/livechat/server/lib/localTypes.ts @@ -1,4 +1,12 @@ -import type { IOmnichannelRoom, IUser, ILivechatVisitor, IMessage, MessageAttachment, IMessageInbox } from '@rocket.chat/core-typings'; +import type { + IOmnichannelRoom, + IUser, + ILivechatVisitor, + IMessage, + MessageAttachment, + IMessageInbox, + IOmnichannelAgent, +} from '@rocket.chat/core-typings'; type GenericCloseRoomParams = { room: IOmnichannelRoom; @@ -54,3 +62,27 @@ export interface ILivechatMessage { blocks?: IMessage['blocks']; email?: IMessageInbox['email']; } + +export type ICRMData = { + _id: string; + label?: string; + topic?: string; + createdAt: Date; + lastMessageAt?: Date; + tags?: string[]; + customFields?: IOmnichannelRoom['livechatData']; + visitor: Pick & { + email?: ILivechatVisitor['visitorEmails']; + os?: string; + browser?: string; + customFields: ILivechatVisitor['livechatData']; + }; + agent?: Pick & { + email?: NonNullable[number]['address']; + }; + crmData?: IOmnichannelRoom['crmData']; +}; + +export type AKeyOf = { + [K in keyof T]?: T[K]; +}; diff --git a/apps/meteor/app/livechat/server/lib/omni-users.ts b/apps/meteor/app/livechat/server/lib/omni-users.ts new file mode 100644 index 0000000000000..3b144196b7b1e --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/omni-users.ts @@ -0,0 +1,133 @@ +import { api } from '@rocket.chat/core-services'; +import type { UserStatus } from '@rocket.chat/core-typings'; +import { LivechatDepartment, LivechatDepartmentAgents, LivechatRooms, Users } from '@rocket.chat/models'; +import { removeEmpty } from '@rocket.chat/tools'; + +import { updateDepartmentAgents } from './Helper'; +import { afterAgentAdded, afterRemoveAgent } from './hooks'; +import { callbacks } from '../../../../lib/callbacks'; +import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; +import { removeUserFromRolesAsync } from '../../../../server/lib/roles/removeUserFromRoles'; +import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; +import { settings } from '../../../settings/server'; + +export async function notifyAgentStatusChanged(userId: string, status?: UserStatus) { + if (!status) { + return; + } + + void callbacks.runAsync('livechat.agentStatusChanged', { userId, status }); + if (!settings.get('Livechat_show_agent_info')) { + return; + } + + await LivechatRooms.findOpenByAgent(userId).forEach((room) => { + void api.broadcast('omnichannel.room', room._id, { + type: 'agentStatus', + status, + }); + }); +} + +export async function addManager(username: string) { + // TODO: remove 'check' function call + check(username, String); + + const user = await Users.findOneByUsername(username, { projection: { _id: 1, username: 1 } }); + + if (!user) { + throw new Meteor.Error('error-invalid-user'); + } + + if (await addUserRolesAsync(user._id, ['livechat-manager'])) { + return user; + } + + return false; +} + +export async function addAgent(username: string) { + check(username, String); + + const user = await Users.findOneByUsername(username, { projection: { _id: 1, username: 1 } }); + + if (!user) { + throw new Meteor.Error('error-invalid-user'); + } + + if (await addUserRolesAsync(user._id, ['livechat-agent'])) { + return afterAgentAdded(user); + } + + return false; +} + +export async function removeAgent(username: string) { + // TODO: we already validated user exists at this point, remove this check + const user = await Users.findOneByUsername(username, { projection: { _id: 1, username: 1 } }); + + if (!user) { + throw new Error('error-invalid-user'); + } + + const { _id } = user; + + if (await removeUserFromRolesAsync(_id, ['livechat-agent'])) { + return afterRemoveAgent(user); + } + + return false; +} + +export async function removeManager(username: string) { + // TODO: we already validated user exists at this point, remove this check + const user = await Users.findOneByUsername(username, { projection: { _id: 1 } }); + + if (!user) { + throw new Error('error-invalid-user'); + } + + return removeUserFromRolesAsync(user._id, ['livechat-manager']); +} + +export async function saveAgentInfo(_id: string, agentData: any, agentDepartments: string[]) { + // TODO: check if these 'check' functions are necessary + check(_id, String); + check(agentData, Object); + check(agentDepartments, [String]); + + const user = await Users.findOneById(_id); + if (!user || !(await hasRoleAsync(_id, 'livechat-agent'))) { + throw new Meteor.Error('error-user-is-not-agent', 'User is not a livechat agent'); + } + + await Users.setLivechatData(_id, removeEmpty(agentData)); + + const currentDepartmentsForAgent = await LivechatDepartmentAgents.findByAgentId(_id).toArray(); + + const toRemoveIds = currentDepartmentsForAgent + .filter((dept) => !agentDepartments.includes(dept.departmentId)) + .map((dept) => dept.departmentId); + const toAddIds = agentDepartments.filter((d) => !currentDepartmentsForAgent.some((c) => c.departmentId === d)); + + await Promise.all( + await LivechatDepartment.findInIds([...toRemoveIds, ...toAddIds], { + projection: { + _id: 1, + enabled: 1, + }, + }) + .map((dep) => { + return updateDepartmentAgents( + dep._id, + { + ...(toRemoveIds.includes(dep._id) ? { remove: [{ agentId: _id }] } : { upsert: [{ agentId: _id, count: 0, order: 0 }] }), + }, + dep.enabled, + ); + }) + .toArray(), + ); + + return true; +} diff --git a/apps/meteor/app/livechat/server/lib/rooms.ts b/apps/meteor/app/livechat/server/lib/rooms.ts index 4db8f526e7c4c..ee231f899b875 100644 --- a/apps/meteor/app/livechat/server/lib/rooms.ts +++ b/apps/meteor/app/livechat/server/lib/rooms.ts @@ -1,11 +1,31 @@ import { AppEvents, Apps } from '@rocket.chat/apps'; -import type { ILivechatVisitor, IMessage, IOmnichannelRoomInfo, SelectedAgent, IOmnichannelRoomExtraData } from '@rocket.chat/core-typings'; -import { LivechatRooms, LivechatContacts, Messages, LivechatCustomField, LivechatInquiry, Rooms, Subscriptions } from '@rocket.chat/models'; +import type { + ILivechatVisitor, + IMessage, + IOmnichannelRoomInfo, + SelectedAgent, + IOmnichannelRoomExtraData, + IOmnichannelRoom, +} from '@rocket.chat/core-typings'; +import { + LivechatRooms, + LivechatContacts, + Messages, + LivechatCustomField, + LivechatInquiry, + Rooms, + Subscriptions, + Users, + ReadReceipts, +} from '@rocket.chat/models'; +import { normalizeTransferredByData } from './Helper'; import { QueueManager } from './QueueManager'; +import { RoutingManager } from './RoutingManager'; import { Visitors } from './Visitors'; import { getRequiredDepartment } from './departmentsLib'; import { livechatLogger } from './logger'; +import { saveTransferHistory } from './transfer'; import { callbacks } from '../../../../lib/callbacks'; import { trim } from '../../../../lib/utils/stringUtils'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; @@ -13,6 +33,8 @@ import { notifyOnLivechatInquiryChangedByRoom, notifyOnSubscriptionChangedByRoomId, notifyOnRoomChangedById, + notifyOnLivechatInquiryChanged, + notifyOnSubscriptionChanged, } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { i18n } from '../../../utils/lib/i18n'; @@ -182,3 +204,83 @@ export async function saveRoomInfo( return true; } + +export async function returnRoomAsInquiry(room: IOmnichannelRoom, departmentId?: string, overrideTransferData: any = {}) { + livechatLogger.debug({ msg: `Transfering room to ${departmentId ? 'department' : ''} queue`, room }); + if (!room.open) { + throw new Meteor.Error('room-closed'); + } + + if (room.onHold) { + throw new Meteor.Error('error-room-onHold'); + } + + if (!room.servedBy) { + return false; + } + + const user = await Users.findOneById(room.servedBy._id); + if (!user?._id) { + throw new Meteor.Error('error-invalid-user'); + } + + const inquiry = await LivechatInquiry.findOne({ rid: room._id }); + if (!inquiry) { + return false; + } + + // update inquiry's last message with room's last message to correctly display in the queue + // because we stop updating the inquiry when it's been taken + if (room.lastMessage) { + await LivechatInquiry.setLastMessageById(inquiry._id, room.lastMessage); + } + + const transferredBy = normalizeTransferredByData(user, room); + livechatLogger.debug(`Transfering room ${room._id} by user ${transferredBy._id}`); + const transferData = { roomId: room._id, scope: 'queue', departmentId, transferredBy, ...overrideTransferData }; + try { + await saveTransferHistory(room, transferData); + await RoutingManager.unassignAgent(inquiry, departmentId); + } catch (e) { + livechatLogger.error(e); + throw new Meteor.Error('error-returning-inquiry'); + } + + callbacks.runAsync('livechat:afterReturnRoomAsInquiry', { room }); + + return true; +} + +export async function removeOmnichannelRoom(rid: string) { + livechatLogger.debug(`Deleting room ${rid}`); + check(rid, String); + const room = await LivechatRooms.findOneById(rid); + if (!room) { + throw new Meteor.Error('error-invalid-room', 'Invalid room'); + } + + const inquiry = await LivechatInquiry.findOneByRoomId(rid); + + const result = await Promise.allSettled([ + Messages.removeByRoomId(rid), + ReadReceipts.removeByRoomId(rid), + Subscriptions.removeByRoomId(rid, { + async onTrash(doc) { + void notifyOnSubscriptionChanged(doc, 'removed'); + }, + }), + LivechatInquiry.removeByRoomId(rid), + LivechatRooms.removeById(rid), + ]); + + if (result[3]?.status === 'fulfilled' && result[3].value?.deletedCount && inquiry) { + void notifyOnLivechatInquiryChanged(inquiry, 'removed'); + } + + for (const r of result) { + if (r.status === 'rejected') { + livechatLogger.error(`Error removing room ${rid}: ${r.reason}`); + throw new Meteor.Error('error-removing-room', 'Error removing room'); + } + } +} diff --git a/apps/meteor/app/livechat/server/lib/service-status.ts b/apps/meteor/app/livechat/server/lib/service-status.ts new file mode 100644 index 0000000000000..65b9b606d6713 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/service-status.ts @@ -0,0 +1,83 @@ +import type { ILivechatAgent, ILivechatDepartment, SelectedAgent } from '@rocket.chat/core-typings'; +import { Users, LivechatDepartmentAgents, LivechatDepartment } from '@rocket.chat/models'; +import type { FindCursor } from 'mongodb'; + +import { livechatLogger } from './logger'; +import { settings } from '../../../settings/server'; + +export async function getOnlineAgents(department?: string, agent?: SelectedAgent | null): Promise | undefined> { + if (agent?.agentId) { + return Users.findOnlineAgents(agent.agentId, settings.get('Livechat_enabled_when_agent_idle')); + } + + if (department) { + const departmentAgents = await LivechatDepartmentAgents.getOnlineForDepartment(department); + if (!departmentAgents) { + return; + } + + const agentIds = await departmentAgents.map(({ agentId }) => agentId).toArray(); + if (!agentIds.length) { + return; + } + + return Users.findByIds([...new Set(agentIds)]); + } + return Users.findOnlineAgents(undefined, settings.get('Livechat_enabled_when_agent_idle')); +} + +export async function online(department?: string, skipNoAgentSetting = false, skipFallbackCheck = false): Promise { + livechatLogger.debug(`Checking online agents ${department ? `for department ${department}` : ''}`); + if (!skipNoAgentSetting && settings.get('Livechat_accept_chats_with_no_agents')) { + livechatLogger.debug('Can accept without online agents: true'); + return true; + } + + if (settings.get('Livechat_assign_new_conversation_to_bot')) { + livechatLogger.debug(`Fetching online bot agents for department ${department}`); + // get & count where doing the same, but get was getting data, while count was only counting. We only need the count here + const botAgents = await countBotAgents(department); + if (botAgents) { + livechatLogger.debug(`Found ${botAgents} online`); + if (botAgents > 0) { + return true; + } + } + } + + const agentsOnline = await checkOnlineAgents(department, undefined, skipFallbackCheck); + livechatLogger.debug(`Are online agents ${department ? `for department ${department}` : ''}?: ${agentsOnline}`); + return agentsOnline; +} + +export async function checkOnlineAgents(department?: string, agent?: { agentId: string }, skipFallbackCheck = false): Promise { + if (agent?.agentId) { + return Users.checkOnlineAgents(agent.agentId, settings.get('Livechat_enabled_when_agent_idle')); + } + + if (department) { + const onlineForDep = await LivechatDepartmentAgents.checkOnlineForDepartment(department); + if (onlineForDep || skipFallbackCheck) { + return onlineForDep; + } + + const dep = await LivechatDepartment.findOneById>(department, { + projection: { fallbackForwardDepartment: 1 }, + }); + if (!dep?.fallbackForwardDepartment) { + return onlineForDep; + } + + return checkOnlineAgents(dep?.fallbackForwardDepartment); + } + + return Users.checkOnlineAgents(undefined, settings.get('Livechat_enabled_when_agent_idle')); +} + +async function countBotAgents(department?: string) { + if (department) { + return LivechatDepartmentAgents.countBotsForDepartment(department); + } + + return Users.countBotAgents(); +} diff --git a/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts b/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts index 71bc21f600323..d2862c93847fa 100644 --- a/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts +++ b/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts @@ -1,8 +1,8 @@ import { Logger } from '@rocket.chat/logger'; import { settings } from '../../../../settings/server'; -import { Livechat } from '../LivechatTyped'; import { closeOpenChats } from '../closeRoom'; +import { forwardOpenChats } from '../transfer'; const logger = new Logger('AgentStatusWatcher'); @@ -73,7 +73,7 @@ export const onlineAgents = { } if (action === 'forward') { - return await Livechat.forwardOpenChats(userId); + return await forwardOpenChats(userId); } } catch (e) { logger.error({ diff --git a/apps/meteor/app/livechat/server/lib/tracking.ts b/apps/meteor/app/livechat/server/lib/tracking.ts index 5e21bb4c38e46..bfbcf9912212e 100644 --- a/apps/meteor/app/livechat/server/lib/tracking.ts +++ b/apps/meteor/app/livechat/server/lib/tracking.ts @@ -1,10 +1,7 @@ import { Message } from '@rocket.chat/core-services'; -import type { ILivechatVisitor } from '@rocket.chat/core-typings'; -import { LivechatInquiry, LivechatRooms, Messages, ReadReceipts, Subscriptions, Users } from '@rocket.chat/models'; +import { Users } from '@rocket.chat/models'; import { livechatLogger } from './logger'; -import { FileUpload } from '../../../file-upload/server'; -import { notifyOnSubscriptionChanged, notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; type PageInfo = { title: string; location: { href: string }; change: string }; @@ -55,32 +52,3 @@ export async function savePageHistory(token: string, roomId: string | undefined, // @ts-expect-error: Investigating on which case we won't receive a roomId and where that history is supposed to be stored return Message.saveSystemMessage('livechat_navigation_history', roomId, `${pageTitle} - ${pageUrl}`, user, extraData); } - -export async function cleanGuestHistory(guest: ILivechatVisitor) { - const { token } = guest; - - // This shouldn't be possible, but just in case - if (!token) { - throw new Error('error-invalid-guest'); - } - - const cursor = LivechatRooms.findByVisitorToken(token); - for await (const room of cursor) { - await Promise.all([ - Subscriptions.removeByRoomId(room._id, { - async onTrash(doc) { - void notifyOnSubscriptionChanged(doc, 'removed'); - }, - }), - FileUpload.removeFilesByRoomId(room._id), - Messages.removeByRoomId(room._id), - ReadReceipts.removeByRoomId(room._id), - ]); - } - - await LivechatRooms.removeByVisitorToken(token); - - const livechatInquiries = await LivechatInquiry.findIdsByVisitorToken(token).toArray(); - await LivechatInquiry.removeByIds(livechatInquiries.map(({ _id }) => _id)); - void notifyOnLivechatInquiryChanged(livechatInquiries, 'removed'); -} diff --git a/apps/meteor/app/livechat/server/lib/transfer.ts b/apps/meteor/app/livechat/server/lib/transfer.ts new file mode 100644 index 0000000000000..74dcca09893ae --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/transfer.ts @@ -0,0 +1,84 @@ +import { Message } from '@rocket.chat/core-services'; +import type { ILivechatDepartment, ILivechatVisitor, IOmnichannelRoom, TransferData } from '@rocket.chat/core-typings'; +import { Users, LivechatRooms, LivechatVisitors, LivechatDepartment } from '@rocket.chat/models'; + +import { normalizeTransferredByData } from './Helper'; +import { RoutingManager } from './RoutingManager'; +import { livechatLogger } from './logger'; + +export async function saveTransferHistory(room: IOmnichannelRoom, transferData: TransferData) { + const { departmentId: previousDepartment } = room; + const { department: nextDepartment, transferredBy, transferredTo, scope, comment } = transferData; + + check( + transferredBy, + Match.ObjectIncluding({ + _id: String, + username: String, + name: Match.Maybe(String), + userType: String, + }), + ); + + const { _id, username } = transferredBy; + const scopeData = scope || (nextDepartment ? 'department' : 'agent'); + livechatLogger.info(`Storing new chat transfer of ${room._id} [Transfered by: ${_id} to ${scopeData}]`); + + const transferMessage = { + ...(transferData.transferredBy.userType === 'visitor' && { token: room.v.token }), + transferData: { + transferredBy, + ts: new Date(), + scope: scopeData, + comment, + ...(previousDepartment && { previousDepartment }), + ...(nextDepartment && { nextDepartment }), + ...(transferredTo && { transferredTo }), + }, + }; + + await Message.saveSystemMessageAndNotifyUser('livechat_transfer_history', room._id, '', { _id, username }, transferMessage); +} + +export async function forwardOpenChats(userId: string) { + livechatLogger.debug(`Transferring open chats for user ${userId}`); + const user = await Users.findOneById(userId); + if (!user) { + throw new Error('error-invalid-user'); + } + + const { _id, username, name } = user; + for await (const room of LivechatRooms.findOpenByAgent(userId)) { + const guest = await LivechatVisitors.findOneEnabledById(room.v._id); + if (!guest) { + continue; + } + + const transferredBy = normalizeTransferredByData({ _id, username, name }, room); + await transfer(room, guest, { + transferredBy, + departmentId: guest.department, + }); + } +} + +export async function transfer(room: IOmnichannelRoom, guest: ILivechatVisitor, transferData: TransferData) { + livechatLogger.debug(`Transfering room ${room._id} [Transfered by: ${transferData?.transferredBy?._id}]`); + if (room.onHold) { + throw new Error('error-room-onHold'); + } + + if (transferData.departmentId) { + const department = await LivechatDepartment.findOneById>(transferData.departmentId, { + projection: { name: 1 }, + }); + if (!department) { + throw new Error('error-invalid-department'); + } + + transferData.department = department; + livechatLogger.debug(`Transfering room ${room._id} to department ${transferData.department?._id}`); + } + + return RoutingManager.transferRoom(room, guest, transferData); +} diff --git a/apps/meteor/app/livechat/server/lib/utils.ts b/apps/meteor/app/livechat/server/lib/utils.ts index 21a7b9e91d672..dd03d1e3c9c46 100644 --- a/apps/meteor/app/livechat/server/lib/utils.ts +++ b/apps/meteor/app/livechat/server/lib/utils.ts @@ -1,5 +1,75 @@ +import { VideoConf } from '@rocket.chat/core-services'; +import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; +import type { ILivechatAgent, ILivechatVisitor, IUser } from '@rocket.chat/core-typings'; +import { Rooms, Users } from '@rocket.chat/models'; +import type { Filter } from 'mongodb'; + import { RoutingManager } from './RoutingManager'; +import type { AKeyOf } from './localTypes'; +import { callbacks } from '../../../../lib/callbacks'; +import { updateMessage } from '../../../lib/server/functions/updateMessage'; +import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; +import { businessHourManager } from '../business-hour'; export function showConnecting() { return RoutingManager.getConfig()?.showConnecting || false; } + +export async function setUserStatusLivechat(userId: string, status: ILivechatAgentStatus) { + const user = await Users.setLivechatStatus(userId, status); + // TODO: shouldnt this callback run if the modified count is > 0 too? + callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status }); + + if (user.modifiedCount > 0) { + void notifyOnUserChange({ + id: userId, + clientAction: 'updated', + diff: { + statusLivechat: status, + livechatStatusSystemModified: false, + }, + }); + } + + return user; +} + +export async function setUserStatusLivechatIf( + userId: string, + status: ILivechatAgentStatus, + condition?: Filter, + fields?: AKeyOf, +) { + const result = await Users.setLivechatStatusIf(userId, status, condition, fields); + + if (result.modifiedCount > 0) { + void notifyOnUserChange({ + id: userId, + clientAction: 'updated', + diff: { ...fields, statusLivechat: status }, + }); + } + + // TODO: shouldnt this callback run if the modified count is > 0 too? + callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status }); + return result; +} + +export async function allowAgentChangeServiceStatus(statusLivechat: ILivechatAgentStatus, agentId: string) { + if (statusLivechat !== ILivechatAgentStatus.AVAILABLE) { + return true; + } + + return businessHourManager.allowAgentChangeServiceStatus(agentId); +} + +export async function updateCallStatus(callId: string, rid: string, status: 'ended' | 'declined', user: IUser | ILivechatVisitor) { + await Rooms.setCallStatus(rid, status); + if (status === 'ended' || status === 'declined') { + if (await VideoConf.declineLivechatCall(callId)) { + return; + } + + return updateMessage({ _id: callId, msg: status, actionLinks: [], webRtcCallEndTs: new Date(), rid }, user as unknown as IUser); + } +} diff --git a/apps/meteor/app/livechat/server/lib/validateRequiredCustomFields.ts b/apps/meteor/app/livechat/server/lib/validateRequiredCustomFields.ts deleted file mode 100644 index 008a4b13c7824..0000000000000 --- a/apps/meteor/app/livechat/server/lib/validateRequiredCustomFields.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { ILivechatCustomField } from '@rocket.chat/core-typings'; - -export const validateRequiredCustomFields = (customFields: string[], livechatCustomFields: ILivechatCustomField[]) => { - const errors: string[] = []; - const requiredCustomFields = livechatCustomFields.filter((field) => field.required); - - requiredCustomFields.forEach((field) => { - if (!customFields.find((f) => f === field._id)) { - errors.push(field._id); - } - }); - - if (errors.length > 0) { - throw new Error(`Missing required custom fields: ${errors.join(', ')}`); - } -}; diff --git a/apps/meteor/app/livechat/server/methods/changeLivechatStatus.ts b/apps/meteor/app/livechat/server/methods/changeLivechatStatus.ts index f45588cad6d37..e82b2e4d24687 100644 --- a/apps/meteor/app/livechat/server/methods/changeLivechatStatus.ts +++ b/apps/meteor/app/livechat/server/methods/changeLivechatStatus.ts @@ -5,7 +5,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { Livechat as LivechatTS } from '../lib/LivechatTyped'; +import { setUserStatusLivechat, allowAgentChangeServiceStatus } from '../lib/utils'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -59,15 +59,15 @@ Meteor.methods({ method: 'livechat:changeLivechatStatus', }); } - return LivechatTS.setUserStatusLivechat(agentId, newStatus); + return setUserStatusLivechat(agentId, newStatus); } - if (!(await LivechatTS.allowAgentChangeServiceStatus(newStatus, agentId))) { + if (!(await allowAgentChangeServiceStatus(newStatus, agentId))) { throw new Meteor.Error('error-business-hours-are-closed', 'Not allowed', { method: 'livechat:changeLivechatStatus', }); } - return LivechatTS.setUserStatusLivechat(agentId, newStatus); + return setUserStatusLivechat(agentId, newStatus); }, }); diff --git a/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.ts b/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.ts index ba3939bb8573e..e154667c3c188 100644 --- a/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.ts +++ b/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.ts @@ -6,7 +6,7 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../lib/callbacks'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { Livechat } from '../lib/LivechatTyped'; +import { removeOmnichannelRoom } from '../lib/rooms'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -32,7 +32,7 @@ Meteor.methods({ const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); const promises: Promise[] = []; await LivechatRooms.findClosedRooms(departmentIds, {}, extraQuery).forEach(({ _id }: IOmnichannelRoom) => { - promises.push(Livechat.removeRoom(_id)); + promises.push(removeOmnichannelRoom(_id)); }); await Promise.all(promises); diff --git a/apps/meteor/app/livechat/server/methods/removeRoom.ts b/apps/meteor/app/livechat/server/methods/removeRoom.ts index 751d51d4f019f..7d659a15985e1 100644 --- a/apps/meteor/app/livechat/server/methods/removeRoom.ts +++ b/apps/meteor/app/livechat/server/methods/removeRoom.ts @@ -4,7 +4,7 @@ import { LivechatRooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { Livechat } from '../lib/LivechatTyped'; +import { removeOmnichannelRoom } from '../lib/rooms'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -40,6 +40,6 @@ Meteor.methods({ }); } - await Livechat.removeRoom(rid); + await removeOmnichannelRoom(rid); }, }); diff --git a/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts b/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts index bf76519a5afb7..df4f4f2a5f76b 100644 --- a/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts +++ b/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts @@ -5,7 +5,7 @@ import { LivechatRooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { Livechat } from '../lib/LivechatTyped'; +import { returnRoomAsInquiry } from '../lib/rooms'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -38,6 +38,6 @@ Meteor.methods({ throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:returnAsInquiry' }); } - return Livechat.returnRoomAsInquiry(room, departmentId); + return returnRoomAsInquiry(room, departmentId); }, }); diff --git a/apps/meteor/app/livechat/server/methods/saveAgentInfo.ts b/apps/meteor/app/livechat/server/methods/saveAgentInfo.ts index fe542b67156e7..215cb2b891a45 100644 --- a/apps/meteor/app/livechat/server/methods/saveAgentInfo.ts +++ b/apps/meteor/app/livechat/server/methods/saveAgentInfo.ts @@ -4,7 +4,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; -import { Livechat } from '../lib/LivechatTyped'; +import { saveAgentInfo } from '../lib/omni-users'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -29,6 +29,6 @@ Meteor.methods({ }); } - return Livechat.saveAgentInfo(_id, agentData, agentDepartments); + return saveAgentInfo(_id, agentData, agentDepartments); }, }); diff --git a/apps/meteor/app/livechat/server/methods/setUpConnection.ts b/apps/meteor/app/livechat/server/methods/setUpConnection.ts index 21ce09acaa229..d6f05c9af03f1 100644 --- a/apps/meteor/app/livechat/server/methods/setUpConnection.ts +++ b/apps/meteor/app/livechat/server/methods/setUpConnection.ts @@ -3,7 +3,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { Livechat } from '../lib/LivechatTyped'; +import { notifyGuestStatusChanged } from '../lib/guests'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -33,7 +33,7 @@ Meteor.methods({ if (this.connection && !this.connection.livechatToken) { this.connection.livechatToken = token; this.connection.onClose(async () => { - await Livechat.notifyGuestStatusChanged(token, UserStatus.OFFLINE); + await notifyGuestStatusChanged(token, UserStatus.OFFLINE); }); } }, diff --git a/apps/meteor/app/livechat/server/methods/transfer.ts b/apps/meteor/app/livechat/server/methods/transfer.ts index e0516fa279817..d4ce08b0a5619 100644 --- a/apps/meteor/app/livechat/server/methods/transfer.ts +++ b/apps/meteor/app/livechat/server/methods/transfer.ts @@ -8,7 +8,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { normalizeTransferredByData } from '../lib/Helper'; -import { Livechat } from '../lib/LivechatTyped'; +import { transfer } from '../lib/transfer'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -100,6 +100,6 @@ Meteor.methods({ }; } - return Livechat.transfer(room, guest, normalizedTransferData); + return transfer(room, guest, normalizedTransferData); }, }); diff --git a/apps/meteor/app/livechat/server/startup.ts b/apps/meteor/app/livechat/server/startup.ts index b41fc425bf066..1df1245655bab 100644 --- a/apps/meteor/app/livechat/server/startup.ts +++ b/apps/meteor/app/livechat/server/startup.ts @@ -7,7 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { businessHourManager } from './business-hour'; import { createDefaultBusinessHourIfNotExists } from './business-hour/Helper'; -import { Livechat as LivechatTyped } from './lib/LivechatTyped'; +import { setUserStatusLivechatIf } from './lib/utils'; import { LivechatAgentActivityMonitor } from './statistics/LivechatAgentActivityMonitor'; import { callbacks } from '../../../lib/callbacks'; import { beforeLeaveRoomCallback } from '../../../lib/callbacks/beforeLeaveRoomCallback'; @@ -88,13 +88,9 @@ Meteor.startup(async () => { return; } - void LivechatTyped.setUserStatusLivechatIf( - user._id, - ILivechatAgentStatus.NOT_AVAILABLE, - {}, - { livechatStatusSystemModified: true }, - ).catch(); + void setUserStatusLivechatIf(user._id, ILivechatAgentStatus.NOT_AVAILABLE, {}, { livechatStatusSystemModified: true }).catch(); + // TODO: Shouldn't this notifier be the same as the one inside setUserStatusLivechatIf? void notifyOnUserChange({ id: user._id, clientAction: 'updated', 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 407ba73806864..202dde93d062d 100644 --- a/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts +++ b/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts @@ -9,11 +9,11 @@ import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../lib/server/l declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - unreadMessages(firstUnreadMessage?: IMessage, room?: IRoom['_id']): void; + unreadMessages(firstUnreadMessage?: Pick, room?: IRoom['_id']): void; } } -export const unreadMessages = async (userId: string, firstUnreadMessage?: IMessage, room?: IRoom['_id']): Promise => { +export const unreadMessages = async (userId: string, firstUnreadMessage?: Pick, room?: IRoom['_id']): Promise => { if (room && typeof room === 'string') { const lastMessage = ( await Messages.findVisibleByRoomId(room, { @@ -65,7 +65,7 @@ export const unreadMessages = async (userId: string, firstUnreadMessage?: IMessa }); } - if (firstUnreadMessage.ts >= lastSeen) { + if (originalMessage.ts >= lastSeen) { return logger.debug('Provided message is already marked as unread'); } diff --git a/apps/meteor/app/meteor-accounts-saml/server/listener.ts b/apps/meteor/app/meteor-accounts-saml/server/listener.ts index 92a0c520ab651..8cdf7c9e6f636 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/listener.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/listener.ts @@ -1,7 +1,7 @@ import type { IncomingMessage, ServerResponse } from 'http'; -import type { IIncomingMessage } from '@rocket.chat/core-typings'; import bodyParser from 'body-parser'; +import express from 'express'; import { Meteor } from 'meteor/meteor'; import { RoutePolicy } from 'meteor/routepolicy'; import { WebApp } from 'meteor/webapp'; @@ -38,11 +38,11 @@ const samlUrlToObject = function (url: string | undefined): ISAMLAction | null { return result; }; -const middleware = async function (req: IIncomingMessage, res: ServerResponse, next: (err?: any) => void): Promise { +const middleware = async function (req: express.Request, res: ServerResponse, next: (err?: any) => void): Promise { // Make sure to catch any exceptions because otherwise we'd crash // the runner try { - const samlObject = samlUrlToObject(req.url); + const samlObject = samlUrlToObject(req.originalUrl); if (!samlObject?.serviceName) { next(); return; @@ -72,6 +72,12 @@ const middleware = async function (req: IIncomingMessage, res: ServerResponse, n }; // Listen to incoming SAML http requests -WebApp.connectHandlers - .use(bodyParser.json()) - .use(async (req: IncomingMessage, res: ServerResponse, next: (err?: any) => void) => middleware(req as IIncomingMessage, res, next)); +WebApp.connectHandlers.use( + /^\/_saml/, + bodyParser.json(), + express.urlencoded({ + extended: true, + limit: '50mb', + }), + async (req: IncomingMessage, res: ServerResponse, next: (err?: any) => void) => middleware(req as express.Request, res, next), +); diff --git a/apps/meteor/app/models/client/index.ts b/apps/meteor/app/models/client/index.ts index e719791066154..d985275faaa8f 100644 --- a/apps/meteor/app/models/client/index.ts +++ b/apps/meteor/app/models/client/index.ts @@ -3,18 +3,14 @@ import { CachedChatSubscription } from './models/CachedChatSubscription'; import { Messages } from './models/Messages'; import { AuthzCachedCollection, Permissions } from './models/Permissions'; import { Roles } from './models/Roles'; -import { RoomRoles } from './models/RoomRoles'; import { Rooms } from './models/Rooms'; import { Subscriptions } from './models/Subscriptions'; -import { UserRoles } from './models/UserRoles'; import { Users } from './models/Users'; export { Roles, CachedChatRoom, CachedChatSubscription, - RoomRoles, - UserRoles, AuthzCachedCollection, Permissions, /** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ diff --git a/apps/meteor/app/models/client/models/Base.ts b/apps/meteor/app/models/client/models/Base.ts deleted file mode 100644 index 68f16d110d00c..0000000000000 --- a/apps/meteor/app/models/client/models/Base.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { check } from 'meteor/check'; -import { Mongo } from 'meteor/mongo'; -import type { Document } from 'mongodb'; - -export abstract class Base { - private model: Mongo.Collection; - - protected _baseName() { - return 'rocketchat_'; - } - - protected _initModel(name: string) { - check(name, String); - this.model = new Mongo.Collection(this._baseName() + name); - return this.model; - } - - find(...args: Parameters['find']>) { - return this.model.find(...args); - } - - findOne(...args: Parameters['findOne']>) { - return this.model.findOne(...args); - } - - insert(...args: Parameters['insert']>) { - return this.model.insert(...args); - } - - update(...args: Parameters['update']>) { - return this.model.update(...args); - } - - upsert(...args: Parameters['upsert']>) { - return this.model.upsert(...args); - } - - remove(...args: Parameters['remove']>) { - return this.model.remove(...args); - } - - allow(...args: Parameters['allow']>) { - return this.model.allow(...args); - } - - deny(...args: Parameters['deny']>) { - return this.model.deny(...args); - } - - ensureIndex() { - // do nothing - } - - dropIndex() { - // do nothing - } - - tryEnsureIndex() { - // do nothing - } - - tryDropIndex() { - // do nothing - } -} diff --git a/apps/meteor/app/models/client/models/CachedChatRoom.ts b/apps/meteor/app/models/client/models/CachedChatRoom.ts index 784503a1b0afc..ce09e9da0ee58 100644 --- a/apps/meteor/app/models/client/models/CachedChatRoom.ts +++ b/apps/meteor/app/models/client/models/CachedChatRoom.ts @@ -2,11 +2,14 @@ import type { IOmnichannelRoom, IRoom, IRoomWithRetentionPolicy } from '@rocket. import { DEFAULT_SLA_CONFIG, LivechatPriorityWeight } from '@rocket.chat/core-typings'; import { CachedChatSubscription } from './CachedChatSubscription'; -import { CachedCollection } from '../../../../client/lib/cachedCollections/CachedCollection'; +import { PrivateCachedCollection } from '../../../../client/lib/cachedCollections/CachedCollection'; -class CachedChatRoom extends CachedCollection { +class CachedChatRoom extends PrivateCachedCollection { constructor() { - super({ name: 'rooms' }); + super({ + name: 'rooms', + eventType: 'notify-user', + }); } protected handleLoadFromServer(record: IRoom) { diff --git a/apps/meteor/app/models/client/models/CachedChatSubscription.ts b/apps/meteor/app/models/client/models/CachedChatSubscription.ts index 28b55a70197d4..077d3107b3e0f 100644 --- a/apps/meteor/app/models/client/models/CachedChatSubscription.ts +++ b/apps/meteor/app/models/client/models/CachedChatSubscription.ts @@ -3,7 +3,7 @@ import { DEFAULT_SLA_CONFIG, LivechatPriorityWeight } from '@rocket.chat/core-ty import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; import { CachedChatRoom } from './CachedChatRoom'; -import { CachedCollection } from '../../../../client/lib/cachedCollections/CachedCollection'; +import { PrivateCachedCollection } from '../../../../client/lib/cachedCollections/CachedCollection'; declare module '@rocket.chat/core-typings' { interface ISubscription { @@ -12,9 +12,12 @@ declare module '@rocket.chat/core-typings' { } } -class CachedChatSubscription extends CachedCollection { +class CachedChatSubscription extends PrivateCachedCollection { constructor() { - super({ name: 'subscriptions' }); + super({ + name: 'subscriptions', + eventType: 'notify-user', + }); } protected handleLoadFromServer(record: ISubscription) { diff --git a/apps/meteor/app/models/client/models/Permissions.ts b/apps/meteor/app/models/client/models/Permissions.ts index 5793ab3e897de..18898d07e0b9f 100644 --- a/apps/meteor/app/models/client/models/Permissions.ts +++ b/apps/meteor/app/models/client/models/Permissions.ts @@ -1,8 +1,8 @@ import type { IPermission } from '@rocket.chat/core-typings'; -import { CachedCollection } from '../../../../client/lib/cachedCollections'; +import { PrivateCachedCollection } from '../../../../client/lib/cachedCollections'; -export const AuthzCachedCollection = new CachedCollection({ +export const AuthzCachedCollection = new PrivateCachedCollection({ name: 'permissions', eventType: 'notify-logged', }); diff --git a/apps/meteor/app/models/client/models/RoomRoles.ts b/apps/meteor/app/models/client/models/RoomRoles.ts deleted file mode 100644 index ab347a59eadac..0000000000000 --- a/apps/meteor/app/models/client/models/RoomRoles.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { ISubscription } from '@rocket.chat/core-typings'; -import { Mongo } from 'meteor/mongo'; - -/** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ -export const RoomRoles = new Mongo.Collection>(null); diff --git a/apps/meteor/app/models/client/models/UserRoles.ts b/apps/meteor/app/models/client/models/UserRoles.ts deleted file mode 100644 index 04a1710e8b9c0..0000000000000 --- a/apps/meteor/app/models/client/models/UserRoles.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { IRocketChatRecord, IRole } from '@rocket.chat/core-typings'; -import { Mongo } from 'meteor/mongo'; - -/** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ -export const UserRoles = new Mongo.Collection< - IRocketChatRecord & { - roles?: IRole['_id'][]; - } ->(null); diff --git a/apps/meteor/app/nextcloud/client/useNextcloud.ts b/apps/meteor/app/nextcloud/client/useNextcloud.ts index 22151c7ff8c29..580d72e92a42a 100644 --- a/apps/meteor/app/nextcloud/client/useNextcloud.ts +++ b/apps/meteor/app/nextcloud/client/useNextcloud.ts @@ -17,7 +17,7 @@ const config: OauthConfig = { }, }; -const Nextcloud = new CustomOAuth('nextcloud', config); +const Nextcloud = CustomOAuth.configureOAuthService('nextcloud', config); export const useNextcloud = (): void => { const nextcloudURL = useSetting('Accounts_OAuth_Nextcloud_URL') as string; diff --git a/apps/meteor/app/notification-queue/server/NotificationQueue.ts b/apps/meteor/app/notification-queue/server/NotificationQueue.ts index 8bfcead0e47f9..6690bea41f521 100644 --- a/apps/meteor/app/notification-queue/server/NotificationQueue.ts +++ b/apps/meteor/app/notification-queue/server/NotificationQueue.ts @@ -169,7 +169,7 @@ class NotificationClass { rid, mid, ts: new Date(), - schedule, + ...(schedule && { schedule }), items, }); } 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 9c5cbf6fd9ae6..c123ce07072db 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 @@ -12,28 +12,32 @@ declare module '@rocket.chat/ddp-client' { } } +export const deleteOAuthApp = async (userId: string, applicationId: IOAuthApps['_id']): Promise => { + if (!(await hasPermissionAsync(userId, 'manage-oauth-apps'))) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'deleteOAuthApp' }); + } + + const application = await OAuthApps.findOneById(applicationId); + if (!application) { + throw new Meteor.Error('error-application-not-found', 'Application not found', { + method: 'deleteOAuthApp', + }); + } + + await OAuthApps.deleteOne({ _id: applicationId }); + + await OAuthAccessTokens.deleteMany({ clientId: application.clientId }); + await OAuthAuthCodes.deleteMany({ clientId: application.clientId }); + + return true; +}; + Meteor.methods({ async deleteOAuthApp(applicationId) { if (!this.userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'deleteOAuthApp' }); } - if (!(await hasPermissionAsync(this.userId, 'manage-oauth-apps'))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'deleteOAuthApp' }); - } - - const application = await OAuthApps.findOneById(applicationId); - if (!application) { - throw new Meteor.Error('error-application-not-found', 'Application not found', { - method: 'deleteOAuthApp', - }); - } - - await OAuthApps.deleteOne({ _id: applicationId }); - - await OAuthAccessTokens.deleteMany({ clientId: application.clientId }); - await OAuthAuthCodes.deleteMany({ clientId: application.clientId }); - - return true; + return deleteOAuthApp(this.userId, applicationId); }, }); 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 f2daca1885c9a..f6d2e26575e37 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 @@ -16,63 +16,71 @@ declare module '@rocket.chat/ddp-client' { } } -Meteor.methods({ - async updateOAuthApp(applicationId, application) { - if (!this.userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateOAuthApp' }); - } +export const updateOAuthApp = async ( + userId: string, + applicationId: IOAuthApps['_id'], + application: Pick, +): Promise => { + if (!(await hasPermissionAsync(userId, 'manage-oauth-apps'))) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'updateOAuthApp' }); + } - if (!(await hasPermissionAsync(this.userId, 'manage-oauth-apps'))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'updateOAuthApp' }); - } + if (!application.name || typeof application.name.valueOf() !== 'string' || application.name.trim() === '') { + throw new Meteor.Error('error-invalid-name', 'Invalid name', { method: 'updateOAuthApp' }); + } - if (!application.name || typeof application.name.valueOf() !== 'string' || application.name.trim() === '') { - throw new Meteor.Error('error-invalid-name', 'Invalid name', { method: 'updateOAuthApp' }); - } + if (!application.redirectUri || typeof application.redirectUri.valueOf() !== 'string' || application.redirectUri.trim() === '') { + throw new Meteor.Error('error-invalid-redirectUri', 'Invalid redirectUri', { + method: 'updateOAuthApp', + }); + } - if (!application.redirectUri || typeof application.redirectUri.valueOf() !== 'string' || application.redirectUri.trim() === '') { - throw new Meteor.Error('error-invalid-redirectUri', 'Invalid redirectUri', { - method: 'updateOAuthApp', - }); - } + if (typeof application.active !== 'boolean') { + throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', { + method: 'updateOAuthApp', + }); + } - if (typeof application.active !== 'boolean') { - throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', { - method: 'updateOAuthApp', - }); - } + const currentApplication = await OAuthApps.findOneById(applicationId); + if (currentApplication == null) { + throw new Meteor.Error('error-application-not-found', 'Application not found', { + method: 'updateOAuthApp', + }); + } - const currentApplication = await OAuthApps.findOneById(applicationId); - if (currentApplication == null) { - throw new Meteor.Error('error-application-not-found', 'Application not found', { - method: 'updateOAuthApp', - }); - } + const redirectUri = parseUriList(application.redirectUri); - const redirectUri = parseUriList(application.redirectUri); + if (redirectUri.length === 0) { + throw new Meteor.Error('error-invalid-redirectUri', 'Invalid redirectUri', { + method: 'updateOAuthApp', + }); + } + + await OAuthApps.updateOne( + { _id: applicationId }, + { + $set: { + name: application.name, + active: application.active, + redirectUri, + _updatedAt: new Date(), + _updatedBy: await Users.findOneById(userId, { + projection: { + username: 1, + }, + }), + }, + }, + ); + return OAuthApps.findOneById(applicationId); +}; - if (redirectUri.length === 0) { - throw new Meteor.Error('error-invalid-redirectUri', 'Invalid redirectUri', { - method: 'updateOAuthApp', - }); +Meteor.methods({ + async updateOAuthApp(applicationId, application) { + if (!this.userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateOAuthApp' }); } - await OAuthApps.updateOne( - { _id: applicationId }, - { - $set: { - name: application.name, - active: application.active, - redirectUri, - _updatedAt: new Date(), - _updatedBy: await Users.findOneById(this.userId, { - projection: { - username: 1, - }, - }), - }, - }, - ); - return OAuthApps.findOneById(applicationId); + return updateOAuthApp(this.userId, applicationId, application); }, }); diff --git a/apps/meteor/app/otr/client/OTRRoom.ts b/apps/meteor/app/otr/client/OTRRoom.ts index 8291ec840f814..83485a7442cfc 100644 --- a/apps/meteor/app/otr/client/OTRRoom.ts +++ b/apps/meteor/app/otr/client/OTRRoom.ts @@ -1,4 +1,4 @@ -import type { IRoom, IMessage, IUser } from '@rocket.chat/core-typings'; +import type { IRoom, IMessage, IUser, UserPresence } from '@rocket.chat/core-typings'; import { UserStatus } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; import EJSON from 'ejson'; @@ -8,7 +8,6 @@ import { Tracker } from 'meteor/tracker'; import GenericModal from '../../../client/components/GenericModal'; import { imperativeModal } from '../../../client/lib/imperativeModal'; -import type { UserPresence } from '../../../client/lib/presence'; import { Presence } from '../../../client/lib/presence'; import { dispatchToastMessage } from '../../../client/lib/toast'; import { getUidDirectMessage } from '../../../client/lib/utils/getUidDirectMessage'; diff --git a/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.ts b/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.ts index ddac51b4eca94..ea2a2d4545192 100644 --- a/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.ts +++ b/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.ts @@ -10,27 +10,126 @@ import { getUserNotificationPreference } from '../../../utils/server/getUserNoti const saveAudioNotificationValue = (subId: ISubscription['_id'], value: string) => value === 'default' ? Subscriptions.clearAudioNotificationValueById(subId) : Subscriptions.updateAudioNotificationValueById(subId, value); +export type NotificationFieldType = + | 'desktopNotifications' + | 'mobilePushNotifications' + | 'emailNotifications' + | 'unreadAlert' + | 'disableNotifications' + | 'hideUnreadStatus' + | 'hideMentionStatus' + | 'muteGroupMentions' + | 'audioNotificationValue'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - saveNotificationSettings( - roomId: string, - field: - | 'desktopNotifications' - | 'mobilePushNotifications' - | 'emailNotifications' - | 'unreadAlert' - | 'disableNotifications' - | 'hideUnreadStatus' - | 'hideMentionStatus' - | 'muteGroupMentions' - | 'audioNotificationValue', - value: string, - ): boolean; + saveNotificationSettings(roomId: string, field: NotificationFieldType, value: string): boolean; saveAudioNotificationValue(subId: string, value: string): boolean; } } +export const saveNotificationSettingsMethod = async ( + userId: string, + roomId: string, + field: NotificationFieldType, + value: string, +): Promise => { + const getNotificationPrefValue = async (field: string, value: unknown) => { + if (value === 'default') { + if (!userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'saveNotificationSettings', + }); + } + + const userPref = await getUserNotificationPreference(userId, field); + return userPref?.origin === 'server' ? null : userPref; + } + return { value, origin: 'subscription' }; + }; + + const notifications = { + desktopNotifications: { + updateMethod: async (subscription: ISubscription, value: unknown) => + Subscriptions.updateNotificationsPrefById( + subscription._id, + await getNotificationPrefValue('desktop', value), + 'desktopNotifications', + 'desktopPrefOrigin', + ), + }, + mobilePushNotifications: { + updateMethod: async (subscription: ISubscription, value: unknown) => + Subscriptions.updateNotificationsPrefById( + subscription._id, + await getNotificationPrefValue('mobile', value), + 'mobilePushNotifications', + 'mobilePrefOrigin', + ), + }, + emailNotifications: { + updateMethod: async (subscription: ISubscription, value: unknown) => + Subscriptions.updateNotificationsPrefById( + subscription._id, + await getNotificationPrefValue('email', value), + 'emailNotifications', + 'emailPrefOrigin', + ), + }, + unreadAlert: { + // @ts-expect-error - Check types of model. The way the method is defined makes difficult to type it, check proper types for `value` + updateMethod: (subscription: ISubscription, value: string) => Subscriptions.updateUnreadAlertById(subscription._id, value), + }, + disableNotifications: { + updateMethod: (subscription: ISubscription, value: unknown) => + Subscriptions.updateDisableNotificationsById(subscription._id, value === '1'), + }, + hideUnreadStatus: { + updateMethod: (subscription: ISubscription, value: string) => + Subscriptions.updateHideUnreadStatusById(subscription._id, value === '1'), + }, + hideMentionStatus: { + updateMethod: (subscription: ISubscription, value: unknown) => + Subscriptions.updateHideMentionStatusById(subscription._id, value === '1'), + }, + muteGroupMentions: { + updateMethod: (subscription: ISubscription, value: unknown) => Subscriptions.updateMuteGroupMentions(subscription._id, value === '1'), + }, + audioNotificationValue: { + updateMethod: (subscription: ISubscription, value: string) => saveAudioNotificationValue(subscription._id, value), + }, + }; + const isInvalidNotification = !Object.keys(notifications).includes(field); + const basicValuesForNotifications = ['all', 'mentions', 'nothing', 'default']; + const fieldsMustHaveBasicValues = ['emailNotifications', 'mobilePushNotifications', 'desktopNotifications']; + + if (isInvalidNotification) { + throw new Meteor.Error('error-invalid-settings', 'Invalid settings field', { + method: 'saveNotificationSettings', + }); + } + + if (fieldsMustHaveBasicValues.includes(field) && !basicValuesForNotifications.includes(value)) { + throw new Meteor.Error('error-invalid-settings', 'Invalid settings value', { + method: 'saveNotificationSettings', + }); + } + + const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, userId); + if (!subscription) { + throw new Meteor.Error('error-invalid-subscription', 'Invalid subscription', { + method: 'saveNotificationSettings', + }); + } + + const updateResponse = await notifications[field].updateMethod(subscription, value); + if (updateResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } + + return true; +}; + Meteor.methods({ async saveNotificationSettings(roomId, field, value) { const userId = Meteor.userId(); @@ -43,102 +142,7 @@ Meteor.methods({ check(field, String); check(value, String); - const getNotificationPrefValue = async (field: string, value: unknown) => { - if (value === 'default') { - const userId = Meteor.userId(); - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'saveNotificationSettings', - }); - } - - const userPref = await getUserNotificationPreference(userId, field); - return userPref?.origin === 'server' ? null : userPref; - } - return { value, origin: 'subscription' }; - }; - - const notifications = { - desktopNotifications: { - updateMethod: async (subscription: ISubscription, value: unknown) => - Subscriptions.updateNotificationsPrefById( - subscription._id, - await getNotificationPrefValue('desktop', value), - 'desktopNotifications', - 'desktopPrefOrigin', - ), - }, - mobilePushNotifications: { - updateMethod: async (subscription: ISubscription, value: unknown) => - Subscriptions.updateNotificationsPrefById( - subscription._id, - await getNotificationPrefValue('mobile', value), - 'mobilePushNotifications', - 'mobilePrefOrigin', - ), - }, - emailNotifications: { - updateMethod: async (subscription: ISubscription, value: unknown) => - Subscriptions.updateNotificationsPrefById( - subscription._id, - await getNotificationPrefValue('email', value), - 'emailNotifications', - 'emailPrefOrigin', - ), - }, - unreadAlert: { - // @ts-expect-error - Check types of model. The way the method is defined makes difficult to type it, check proper types for `value` - updateMethod: (subscription: ISubscription, value: string) => Subscriptions.updateUnreadAlertById(subscription._id, value), - }, - disableNotifications: { - updateMethod: (subscription: ISubscription, value: unknown) => - Subscriptions.updateDisableNotificationsById(subscription._id, value === '1'), - }, - hideUnreadStatus: { - updateMethod: (subscription: ISubscription, value: string) => - Subscriptions.updateHideUnreadStatusById(subscription._id, value === '1'), - }, - hideMentionStatus: { - updateMethod: (subscription: ISubscription, value: unknown) => - Subscriptions.updateHideMentionStatusById(subscription._id, value === '1'), - }, - muteGroupMentions: { - updateMethod: (subscription: ISubscription, value: unknown) => - Subscriptions.updateMuteGroupMentions(subscription._id, value === '1'), - }, - audioNotificationValue: { - updateMethod: (subscription: ISubscription, value: string) => saveAudioNotificationValue(subscription._id, value), - }, - }; - const isInvalidNotification = !Object.keys(notifications).includes(field); - const basicValuesForNotifications = ['all', 'mentions', 'nothing', 'default']; - const fieldsMustHaveBasicValues = ['emailNotifications', 'mobilePushNotifications', 'desktopNotifications']; - - if (isInvalidNotification) { - throw new Meteor.Error('error-invalid-settings', 'Invalid settings field', { - method: 'saveNotificationSettings', - }); - } - - if (fieldsMustHaveBasicValues.includes(field) && !basicValuesForNotifications.includes(value)) { - throw new Meteor.Error('error-invalid-settings', 'Invalid settings value', { - method: 'saveNotificationSettings', - }); - } - - const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, userId); - if (!subscription) { - throw new Meteor.Error('error-invalid-subscription', 'Invalid subscription', { - method: 'saveNotificationSettings', - }); - } - - const updateResponse = await notifications[field].updateMethod(subscription, value); - if (updateResponse.modifiedCount) { - void notifyOnSubscriptionChangedById(subscription._id); - } - - return true; + return saveNotificationSettingsMethod(userId, roomId, field, value); }, async saveAudioNotificationValue(rid, value) { diff --git a/apps/meteor/app/push/server/methods.ts b/apps/meteor/app/push/server/methods.ts index 1f1e261eccae0..47d0eb4dfacd1 100644 --- a/apps/meteor/app/push/server/methods.ts +++ b/apps/meteor/app/push/server/methods.ts @@ -9,21 +9,107 @@ import { Meteor } from 'meteor/meteor'; import { logger } from './logger'; import { _matchToken } from './push'; +type PushUpdateOptions = { + id?: string; + token: IAppsTokens['token']; + authToken: string; + appName: string; + userId: string | null; + metadata?: Record; +}; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - 'raix:push-update'(options: { - id?: string; - token: IAppsTokens['token']; - authToken: string; - appName: string; - userId?: string; - metadata?: Record; - }): Promise>; + 'raix:push-update'(options: PushUpdateOptions): Promise>; 'raix:push-setuser'(options: { id: string; userId: string }): Promise; } } +export const pushUpdate = async (options: PushUpdateOptions): Promise> => { + // we always store the hashed token to protect users + const hashedToken = Accounts._hashLoginToken(options.authToken); + + let doc; + + // lookup app by id if one was included + if (options.id) { + doc = await AppsTokens.findOne({ _id: options.id }); + } else if (options.userId) { + doc = await AppsTokens.findOne({ userId: options.userId }); + } + + // No doc was found - we check the database to see if + // we can find a match for the app via token and appName + if (!doc) { + doc = await AppsTokens.findOne({ + $and: [ + { token: options.token }, // Match token + { appName: options.appName }, // Match appName + { token: { $exists: true } }, // Make sure token exists + ], + }); + } + + // if we could not find the id or token then create it + if (!doc) { + // Rig default doc + doc = { + token: options.token, + authToken: hashedToken, + appName: options.appName, + userId: options.userId, + enabled: true, + createdAt: new Date(), + updatedAt: new Date(), + metadata: options.metadata || {}, + + // XXX: We might want to check the id - Why isnt there a match for id + // in the Meteor check... Normal length 17 (could be larger), and + // numbers+letters are used in Random.id() with exception of 0 and 1 + _id: options.id || Random.id(), + // The user wanted us to use a specific id, we didn't find this while + // searching. The client could depend on the id eg. as reference so + // we respect this and try to create a document with the selected id; + }; + + await AppsTokens.insertOne(doc); + } else { + // We found the app so update the updatedAt and set the token + await AppsTokens.updateOne( + { _id: doc._id }, + { + $set: { + updatedAt: new Date(), + token: options.token, + authToken: hashedToken, + }, + }, + ); + } + + if (doc.token) { + const removed = ( + await AppsTokens.deleteMany({ + $and: [ + { _id: { $ne: doc._id } }, + { token: doc.token }, // Match token + { appName: doc.appName }, // Match appName + { token: { $exists: true } }, // Make sure token exists + ], + }) + ).deletedCount; + + if (removed) { + logger.debug(`Removed ${removed} existing app items`); + } + } + + logger.debug('updated', doc); + + // Return the doc we want to use + return doc; +}; + Meteor.methods({ async 'raix:push-update'(options) { logger.debug('Got push token from app:', options); @@ -42,88 +128,7 @@ Meteor.methods({ throw new Meteor.Error(403, 'Forbidden access'); } - // we always store the hashed token to protect users - const hashedToken = Accounts._hashLoginToken(options.authToken); - - let doc; - - // lookup app by id if one was included - if (options.id) { - doc = await AppsTokens.findOne({ _id: options.id }); - } else if (options.userId) { - doc = await AppsTokens.findOne({ userId: options.userId }); - } - - // No doc was found - we check the database to see if - // we can find a match for the app via token and appName - if (!doc) { - doc = await AppsTokens.findOne({ - $and: [ - { token: options.token }, // Match token - { appName: options.appName }, // Match appName - { token: { $exists: true } }, // Make sure token exists - ], - }); - } - - // if we could not find the id or token then create it - if (!doc) { - // Rig default doc - doc = { - token: options.token, - authToken: hashedToken, - appName: options.appName, - userId: options.userId, - enabled: true, - createdAt: new Date(), - updatedAt: new Date(), - metadata: options.metadata || {}, - - // XXX: We might want to check the id - Why isnt there a match for id - // in the Meteor check... Normal length 17 (could be larger), and - // numbers+letters are used in Random.id() with exception of 0 and 1 - _id: options.id || Random.id(), - // The user wanted us to use a specific id, we didn't find this while - // searching. The client could depend on the id eg. as reference so - // we respect this and try to create a document with the selected id; - }; - - await AppsTokens.insertOne(doc); - } else { - // We found the app so update the updatedAt and set the token - await AppsTokens.updateOne( - { _id: doc._id }, - { - $set: { - updatedAt: new Date(), - token: options.token, - authToken: hashedToken, - }, - }, - ); - } - - if (doc.token) { - const removed = ( - await AppsTokens.deleteMany({ - $and: [ - { _id: { $ne: doc._id } }, - { token: doc.token }, // Match token - { appName: doc.appName }, // Match appName - { token: { $exists: true } }, // Make sure token exists - ], - }) - ).deletedCount; - - if (removed) { - logger.debug(`Removed ${removed} existing app items`); - } - } - - logger.debug('updated', doc); - - // Return the doc we want to use - return doc; + return pushUpdate(options); }, // Deprecated async 'raix:push-setuser'(id) { diff --git a/apps/meteor/app/tokenpass/client/hooks/useTokenPassAuth.tsx b/apps/meteor/app/tokenpass/client/hooks/useTokenPassAuth.tsx index 24e31db422260..26dbe3a900e2d 100644 --- a/apps/meteor/app/tokenpass/client/hooks/useTokenPassAuth.tsx +++ b/apps/meteor/app/tokenpass/client/hooks/useTokenPassAuth.tsx @@ -20,7 +20,7 @@ const config: OauthConfig = { accessTokenParam: 'access_token', }; -const Tokenpass = new CustomOAuth('tokenpass', config); +const Tokenpass = CustomOAuth.configureOAuthService('tokenpass', config); export const useTokenPassAuth = () => { const setting = useSetting('API_Tokenpass_URL') as string | undefined; diff --git a/apps/meteor/app/ui-message/client/popup/messagePopupConfig.ts b/apps/meteor/app/ui-message/client/popup/messagePopupConfig.ts deleted file mode 100644 index 1d220f8bce8d9..0000000000000 --- a/apps/meteor/app/ui-message/client/popup/messagePopupConfig.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; -import { Mongo } from 'meteor/mongo'; -import { Tracker } from 'meteor/tracker'; - -import { RoomManager } from '../../../../client/lib/RoomManager'; -import { asReactiveSource } from '../../../../client/lib/tracker'; -import { Messages } from '../../../models/client'; - -export const usersFromRoomMessages = new Mongo.Collection<{ - _id: string; - username: string; - name: string | undefined; - ts: Date; - suggestion?: boolean; -}>(null); - -Meteor.startup(() => { - Tracker.autorun(() => { - const uid = Meteor.userId(); - const rid = asReactiveSource( - (cb) => RoomManager.on('changed', cb), - () => RoomManager.opened, - ); - const user = uid ? (Meteor.users.findOne(uid, { fields: { username: 1 } }) as IUser | undefined) : undefined; - if (!rid || !user) { - return; - } - - usersFromRoomMessages.remove({}); - - const uniqueMessageUsersControl: Record = {}; - - Messages.find( - { - rid, - 'u.username': { $ne: user.username }, - 't': { $exists: false }, - }, - { - fields: { - 'u.username': 1, - 'u.name': 1, - 'u._id': 1, - 'ts': 1, - }, - sort: { ts: -1 }, - }, - ) - .fetch() - .filter(({ u: { username } }) => { - const notMapped = !uniqueMessageUsersControl[username]; - uniqueMessageUsersControl[username] = true; - return notMapped; - }) - .forEach(({ u: { username, name, _id }, ts }) => - usersFromRoomMessages.upsert(_id, { - _id, - username, - name, - ts, - }), - ); - }); -}); diff --git a/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts b/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts index fa3445b6ad940..dd72504575509 100644 --- a/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts @@ -50,7 +50,7 @@ function close(typeName: string) { if (rid) { RoomManager.close(rid); - return RoomHistoryManager.clear(rid); + return RoomHistoryManager.close(rid); } } } @@ -93,8 +93,6 @@ const computation = Tracker.autorun(() => { const room = roomCoordinator.getRoomDirectives(type).findRoom(name); - void RoomHistoryManager.getMoreIfIsEmpty(record.rid); - if (room) { if (record.streamActive !== true) { void sdk diff --git a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts index 67777b346718b..cd19f954ed534 100644 --- a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts @@ -14,6 +14,8 @@ import { waitForElement } from '../../../../client/lib/utils/waitForElement'; import { Messages, Subscriptions } from '../../../models/client'; import { getUserPreference } from '../../../utils/client'; +const waitAfterFlush = () => new Promise((resolve) => Tracker.afterFlush(() => resolve(void 0))); + export async function upsertMessage( { msg, @@ -40,24 +42,26 @@ export async function upsertMessage( return collection.upsert({ _id }, msg); } -export function upsertMessageBulk( +export async function upsertMessageBulk( { msgs, subscription }: { msgs: IMessage[]; subscription?: ISubscription }, collection: MinimongoCollection = Messages, ) { const { queries } = collection; collection.queries = []; - msgs.forEach((msg, index) => { - if (index === msgs.length - 1) { - collection.queries = queries; - } - void upsertMessage({ msg, subscription }, collection); - }); + const lastMessage = msgs.pop(); + + for await (const msg of msgs) { + await upsertMessage({ msg, subscription }, collection); + } + + if (lastMessage) { + collection.queries = queries; + await upsertMessage({ msg: lastMessage, subscription }, collection); + } } const defaultLimit = parseInt(getConfig('roomListLimit') ?? '50') || 50; -const waitAfterFlush = (fn: () => void) => setTimeout(() => Tracker.afterFlush(fn), 10); - class RoomHistoryManagerClass extends Emitter { private lastRequest?: Date; @@ -71,6 +75,10 @@ class RoomHistoryManagerClass extends Emitter { firstUnread: ReactiveVar; loaded: number | undefined; oldestTs?: Date; + scroll?: { + scrollHeight: number; + scrollTop: number; + }; } > = {}; @@ -114,6 +122,11 @@ class RoomHistoryManagerClass extends Emitter { return setTimeout(fn, 500 - difference); } + public isLoaded(rid: IRoom['_id']) { + const room = this.getRoom(rid); + return room.loaded !== undefined; + } + private unqueue() { const requestId = this.requestsList.pop(); if (!requestId) { @@ -156,8 +169,6 @@ class RoomHistoryManagerClass extends Emitter { this.unqueue(); - let previousHeight: number | undefined; - let scroll: number | undefined; const { messages = [] } = result; room.unreadNotLoaded.set(result.unreadNotLoaded); room.firstUnread.set(result.firstUnread); @@ -168,16 +179,18 @@ class RoomHistoryManagerClass extends Emitter { const wrapper = await waitForElement('.messages-box .wrapper [data-overlayscrollbars-viewport]'); - if (wrapper) { - previousHeight = wrapper.scrollHeight; - scroll = wrapper.scrollTop; - } + room.scroll = { + scrollHeight: wrapper.scrollHeight, + scrollTop: wrapper.scrollTop, + }; - upsertMessageBulk({ + await upsertMessageBulk({ msgs: messages.filter((msg) => msg.t !== 'command'), subscription, }); + this.emit('loaded-messages'); + if (!room.loaded) { room.loaded = 0; } @@ -194,13 +207,27 @@ class RoomHistoryManagerClass extends Emitter { return this.getMore(rid); } - waitAfterFlush(() => { - this.emit('loaded-messages'); - const heightDiff = wrapper.scrollHeight - (previousHeight ?? NaN); - wrapper.scrollTop = (scroll ?? NaN) + heightDiff; - }); + this.emit('loaded-messages'); room.isLoading.set(false); + await waitAfterFlush(); + } + + public restoreScroll(rid: IRoom['_id']) { + const room = this.getRoom(rid); + const wrapper = document.querySelector('.messages-box .wrapper [data-overlayscrollbars-viewport]'); + + if (room.scroll === undefined) { + return; + } + + if (!wrapper) { + return; + } + + const heightDiff = wrapper.scrollHeight - (room.scroll.scrollHeight ?? NaN); + wrapper.scrollTop = room.scroll.scrollTop + heightDiff; + room.scroll = undefined; } public async getMoreNext(rid: IRoom['_id'], atBottomRef: MutableRefObject) { @@ -221,11 +248,13 @@ class RoomHistoryManagerClass extends Emitter { if (lastMessage?.ts) { const { ts } = lastMessage; const result = await callWithErrorHandling('loadNextMessages', rid, ts, defaultLimit); - upsertMessageBulk({ + await upsertMessageBulk({ msgs: Array.from(result.messages).filter((msg) => msg.t !== 'command'), subscription, }); + this.emit('loaded-messages'); + room.isLoading.set(false); if (!room.loaded) { room.loaded = 0; @@ -262,10 +291,15 @@ class RoomHistoryManagerClass extends Emitter { return room.isLoading.get(); } - public async clear(rid: IRoom['_id']) { + public close(rid: IRoom['_id']) { + Messages.remove({ rid }); + delete this.histories[rid]; + } + + public clear(rid: IRoom['_id']) { const room = this.getRoom(rid); Messages.remove({ rid }); - room.isLoading.set(true); + room.isLoading.set(false); room.hasMore.set(true); room.hasMoreNext.set(false); room.oldestTs = undefined; @@ -284,17 +318,23 @@ class RoomHistoryManagerClass extends Emitter { } const room = this.getRoom(message.rid); - void this.clear(message.rid); const subscription = Subscriptions.findOne({ rid: message.rid }); const result = await callWithErrorHandling('loadSurroundingMessages', message, defaultLimit); + this.clear(message.rid); + if (!result) { return; } + const { messages = [] } = result; + + if (messages.length > 0) { + room.oldestTs = messages[messages.length - 1].ts; + } - upsertMessageBulk({ msgs: Array.from(result.messages).filter((msg) => msg.t !== 'command'), subscription }); + await upsertMessageBulk({ msgs: Array.from(result.messages).filter((msg) => msg.t !== 'command'), subscription }); Tracker.afterFlush(async () => { this.emit('loaded-messages'); diff --git a/apps/meteor/app/ui/client/lib/KonchatNotification.ts b/apps/meteor/app/ui/client/lib/KonchatNotification.ts index 2b463886fdac2..9d6f3dc6c9c1f 100644 --- a/apps/meteor/app/ui/client/lib/KonchatNotification.ts +++ b/apps/meteor/app/ui/client/lib/KonchatNotification.ts @@ -1,238 +1 @@ -import type { INotificationDesktop, IRoom, IUser } from '@rocket.chat/core-typings'; -import { Random } from '@rocket.chat/random'; -import { Meteor } from 'meteor/meteor'; -import { ReactiveVar } from 'meteor/reactive-var'; - -import { RoomManager } from '../../../../client/lib/RoomManager'; -import { onClientMessageReceived } from '../../../../client/lib/onClientMessageReceived'; -import { getAvatarAsPng } from '../../../../client/lib/utils/getAvatarAsPng'; -import { router } from '../../../../client/providers/RouterProvider'; -import { stripTags } from '../../../../lib/utils/stringUtils'; -import { CustomSounds } from '../../../custom-sounds/client/lib/CustomSounds'; -import { e2e } from '../../../e2e/client'; -import { Subscriptions, Users } from '../../../models/client'; -import { getUserPreference } from '../../../utils/client'; -import { getUserAvatarURL } from '../../../utils/client/getUserAvatarURL'; -import { getUserNotificationsSoundVolume } from '../../../utils/client/getUserNotificationsSoundVolume'; -import { sdk } from '../../../utils/client/lib/SDKClient'; - -declare global { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface NotificationEventMap { - reply: { response: string }; - } -} - -class KonchatNotification { - public notificationStatus = new ReactiveVar(undefined); - - public getDesktopPermission() { - if (window.Notification && Notification.permission !== 'granted') { - return Notification.requestPermission((status) => { - this.notificationStatus.set(status); - }); - } - } - - public async notify(notification: INotificationDesktop) { - if (typeof window.Notification === 'undefined' || Notification.permission !== 'granted') { - return; - } - - if (!notification.payload) { - return; - } - - const { rid } = notification.payload; - - if (!rid) { - return; - } - const message = await onClientMessageReceived({ - rid, - msg: notification.text, - notification: true, - } as any); - - const requireInteraction = getUserPreference(Meteor.userId(), 'desktopNotificationRequireInteraction'); - const n = new Notification(notification.title, { - icon: notification.icon || getUserAvatarURL(notification.payload.sender?.username as string), - body: stripTags(message.msg), - tag: notification.payload._id, - canReply: true, - silent: true, - requireInteraction, - } as NotificationOptions & { - canReply?: boolean; // TODO is this still needed for the desktop app? - }); - - const notificationDuration = !requireInteraction ? (notification.duration ?? 0) - 0 || 10 : -1; - if (notificationDuration > 0) { - setTimeout(() => n.close(), notificationDuration * 1000); - } - - if (n.addEventListener) { - n.addEventListener( - 'reply', - ({ response }) => - void sdk.call('sendMessage', { - _id: Random.id(), - rid, - msg: response, - }), - ); - } - - n.onclick = function () { - this.close(); - window.focus(); - - if (!notification.payload._id || !notification.payload.rid || !notification.payload.name) { - return; - } - - switch (notification.payload?.type) { - case 'd': - return router.navigate({ - pattern: '/direct/:rid/:tab?/:context?', - params: { - rid: notification.payload.rid, - ...(notification.payload.tmid && { - tab: 'thread', - context: notification.payload.tmid, - }), - }, - search: { ...router.getSearchParameters(), jump: notification.payload._id }, - }); - case 'c': - return router.navigate({ - pattern: '/channel/:name/:tab?/:context?', - params: { - name: notification.payload.name, - ...(notification.payload.tmid && { - tab: 'thread', - context: notification.payload.tmid, - }), - }, - search: { ...router.getSearchParameters(), jump: notification.payload._id }, - }); - case 'p': - return router.navigate({ - pattern: '/group/:name/:tab?/:context?', - params: { - name: notification.payload.name, - ...(notification.payload.tmid && { - tab: 'thread', - context: notification.payload.tmid, - }), - }, - search: { ...router.getSearchParameters(), jump: notification.payload._id }, - }); - case 'l': - return router.navigate({ - pattern: '/live/:id/:tab?/:context?', - params: { - id: notification.payload.rid, - tab: 'room-info', - }, - search: { ...router.getSearchParameters(), jump: notification.payload._id }, - }); - } - }; - } - - public async showDesktop(notification: INotificationDesktop) { - if (!notification.payload.rid) { - return; - } - - if ( - notification.payload?.rid === RoomManager.opened && - (typeof window.document.hasFocus === 'function' ? window.document.hasFocus() : undefined) - ) { - return; - } - - if ((Meteor.user() as IUser | null)?.status === 'busy') { - return; - } - - if (notification.payload?.message?.t === 'e2e') { - const e2eRoom = await e2e.getInstanceByRoomId(notification.payload.rid); - if (e2eRoom) { - notification.text = (await e2eRoom.decrypt(notification.payload.message.msg)).text; - } - } - - return getAvatarAsPng(notification.payload?.sender?.username, (avatarAsPng) => { - notification.icon = avatarAsPng; - return this.notify(notification); - }); - } - - public async newMessage(rid: IRoom['_id'] | undefined) { - if ((Meteor.user() as IUser | null)?.status === 'busy') { - return; - } - - const userId = Meteor.userId(); - const newMessageNotification = getUserPreference(userId, 'newMessageNotification'); - const audioVolume = getUserNotificationsSoundVolume(userId); - - if (!rid) { - return; - } - - const sub = Subscriptions.findOne({ rid }, { fields: { audioNotificationValue: 1 } }); - - if (!sub || sub.audioNotificationValue === 'none') { - return; - } - - try { - if (sub.audioNotificationValue && sub.audioNotificationValue !== '0') { - void CustomSounds.play(sub.audioNotificationValue, { - volume: Number((audioVolume / 100).toPrecision(2)), - }); - return; - } - - if (newMessageNotification && newMessageNotification !== 'none') { - void CustomSounds.play(newMessageNotification, { - volume: Number((audioVolume / 100).toPrecision(2)), - }); - } - } catch (e) { - // do nothing - } - } - - public newRoom() { - Tracker.nonreactive(() => { - const uid = Meteor.userId(); - if (!uid) { - return; - } - const user = Users.findOne(uid, { - fields: { - 'settings.preferences.newRoomNotification': 1, - 'settings.preferences.notificationsSoundVolume': 1, - }, - }); - const newRoomNotification = getUserPreference(user, 'newRoomNotification'); - const audioVolume = getUserNotificationsSoundVolume(user?._id); - - if (!newRoomNotification) { - return; - } - - void CustomSounds.play(newRoomNotification, { - volume: Number((audioVolume / 100).toPrecision(2)), - }); - }); - } -} - -const instance = new KonchatNotification(); - -export { instance as KonchatNotification }; +// KonchatNotification in memoriam diff --git a/apps/meteor/app/ui/client/views/app/lib/scrolling.ts b/apps/meteor/app/ui/client/views/app/lib/scrolling.ts index c3e95535281d4..c4f7afb7f4531 100644 --- a/apps/meteor/app/ui/client/views/app/lib/scrolling.ts +++ b/apps/meteor/app/ui/client/views/app/lib/scrolling.ts @@ -1,3 +1,20 @@ export function isAtBottom(element: HTMLElement, scrollThreshold = 0): boolean { return element.scrollTop + scrollThreshold >= element.scrollHeight - element.clientHeight; } + +// Mainly used for allow mock during testing + +export const getBoundingClientRect = (ref: HTMLElement) => { + const { top, bottom, left, right } = ref.getBoundingClientRect(); + const { scrollTop, scrollHeight, clientHeight } = ref; + + return { + top, + bottom, + left, + right, + scrollTop, + scrollHeight, + clientHeight, + }; +}; diff --git a/apps/meteor/app/user-status/server/methods/deleteCustomUserStatus.ts b/apps/meteor/app/user-status/server/methods/deleteCustomUserStatus.ts index 416bc6f678ede..72ccb9ccbd07d 100644 --- a/apps/meteor/app/user-status/server/methods/deleteCustomUserStatus.ts +++ b/apps/meteor/app/user-status/server/methods/deleteCustomUserStatus.ts @@ -12,20 +12,28 @@ declare module '@rocket.chat/ddp-client' { } } +export const deleteCustomUserStatus = async (userId: string, userStatusID: string): Promise => { + if (!(await hasPermissionAsync(userId, 'manage-user-status'))) { + throw new Meteor.Error('not_authorized'); + } + + const userStatus = await CustomUserStatus.findOneById(userStatusID); + if (userStatus == null) { + throw new Meteor.Error('Custom_User_Status_Error_Invalid_User_Status', 'Invalid user status', { method: 'deleteCustomUserStatus' }); + } + + await CustomUserStatus.removeById(userStatusID); + void api.broadcast('user.deleteCustomStatus', userStatus); + + return true; +}; + Meteor.methods({ async deleteCustomUserStatus(userStatusID) { - if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-user-status'))) { + if (!this.userId) { throw new Meteor.Error('not_authorized'); } - const userStatus = await CustomUserStatus.findOneById(userStatusID); - if (userStatus == null) { - throw new Meteor.Error('Custom_User_Status_Error_Invalid_User_Status', 'Invalid user status', { method: 'deleteCustomUserStatus' }); - } - - await CustomUserStatus.removeById(userStatusID); - void api.broadcast('user.deleteCustomStatus', userStatus); - - return true; + return deleteCustomUserStatus(this.userId, userStatusID); }, }); diff --git a/apps/meteor/app/user-status/server/methods/insertOrUpdateUserStatus.ts b/apps/meteor/app/user-status/server/methods/insertOrUpdateUserStatus.ts index 6e034f0306797..16f8dc716b06a 100644 --- a/apps/meteor/app/user-status/server/methods/insertOrUpdateUserStatus.ts +++ b/apps/meteor/app/user-status/server/methods/insertOrUpdateUserStatus.ts @@ -8,96 +8,106 @@ import { Meteor } from 'meteor/meteor'; import { trim } from '../../../../lib/utils/stringUtils'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +type InsertOrUpdateUserStatus = { + _id?: string; + name: string; + statusType: string; + status?: string; + emoji?: string; + message?: string; + order?: number; + previousName?: string; + previousStatusType?: string; +}; + declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - insertOrUpdateUserStatus(userStatusData: { - _id?: string; - name: string; - statusType: string; - status: string; - emoji: string; - message: string; - order: number; - previousName?: string; - previousStatusType?: string; - }): string | boolean; + insertOrUpdateUserStatus(userStatusData: InsertOrUpdateUserStatus): string | boolean; } } -Meteor.methods({ - async insertOrUpdateUserStatus(userStatusData) { - if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-user-status'))) { - throw new Meteor.Error('not_authorized'); - } +export const insertOrUpdateUserStatus = async (userId: string, userStatusData: InsertOrUpdateUserStatus): Promise => { + if (!(await hasPermissionAsync(userId, 'manage-user-status'))) { + throw new Meteor.Error('not_authorized'); + } - if (!trim(userStatusData.name)) { - throw new Meteor.Error('error-the-field-is-required', 'The field Name is required', { - method: 'insertOrUpdateUserStatus', - field: 'Name', - }); - } + if (!trim(userStatusData.name)) { + throw new Meteor.Error('error-the-field-is-required', 'The field Name is required', { + method: 'insertOrUpdateUserStatus', + field: 'Name', + }); + } - // allow all characters except >, <, &, ", ' - // more practical than allowing specific sets of characters; also allows foreign languages - const nameValidation = /[><&"']/; + // allow all characters except >, <, &, ", ' + // more practical than allowing specific sets of characters; also allows foreign languages + const nameValidation = /[><&"']/; - if (nameValidation.test(userStatusData.name)) { - throw new Meteor.Error('error-input-is-not-a-valid-field', `${userStatusData.name} is not a valid name`, { - method: 'insertOrUpdateUserStatus', - input: userStatusData.name, - field: 'Name', - }); - } + if (nameValidation.test(userStatusData.name)) { + throw new Meteor.Error('error-input-is-not-a-valid-field', `${userStatusData.name} is not a valid name`, { + method: 'insertOrUpdateUserStatus', + input: userStatusData.name, + field: 'Name', + }); + } - let matchingResults = []; + let matchingResults = []; - if (userStatusData._id) { - matchingResults = await CustomUserStatus.findByNameExceptId(userStatusData.name, userStatusData._id).toArray(); - } else { - matchingResults = await CustomUserStatus.findByName(userStatusData.name).toArray(); - } + if (userStatusData._id) { + matchingResults = await CustomUserStatus.findByNameExceptId(userStatusData.name, userStatusData._id).toArray(); + } else { + matchingResults = await CustomUserStatus.findByName(userStatusData.name).toArray(); + } - if (matchingResults.length > 0) { - throw new Meteor.Error('Custom_User_Status_Error_Name_Already_In_Use', 'The custom user status name is already in use', { - method: 'insertOrUpdateUserStatus', - }); - } + if (matchingResults.length > 0) { + throw new Meteor.Error('Custom_User_Status_Error_Name_Already_In_Use', 'The custom user status name is already in use', { + method: 'insertOrUpdateUserStatus', + }); + } - const validStatusTypes = ['online', 'away', 'busy', 'offline']; - if (userStatusData.statusType && validStatusTypes.indexOf(userStatusData.statusType) < 0) { - throw new Meteor.Error('error-input-is-not-a-valid-field', `${userStatusData.statusType} is not a valid status type`, { - method: 'insertOrUpdateUserStatus', - input: userStatusData.statusType, - field: 'StatusType', - }); - } + const validStatusTypes = ['online', 'away', 'busy', 'offline']; + if (userStatusData.statusType && validStatusTypes.indexOf(userStatusData.statusType) < 0) { + throw new Meteor.Error('error-input-is-not-a-valid-field', `${userStatusData.statusType} is not a valid status type`, { + method: 'insertOrUpdateUserStatus', + input: userStatusData.statusType, + field: 'StatusType', + }); + } - if (!userStatusData._id) { - // insert user status - const createUserStatus: InsertionModel = { - name: userStatusData.name, - statusType: userStatusData.statusType, - }; + if (!userStatusData._id) { + // insert user status + const createUserStatus: InsertionModel = { + name: userStatusData.name, + statusType: userStatusData.statusType, + }; - const _id = (await CustomUserStatus.create(createUserStatus)).insertedId; + const _id = (await CustomUserStatus.create(createUserStatus)).insertedId; - void api.broadcast('user.updateCustomStatus', { ...createUserStatus, _id }); + void api.broadcast('user.updateCustomStatus', { ...createUserStatus, _id }); - return _id; - } + return _id; + } - // update User status - if (userStatusData.name !== userStatusData.previousName) { - await CustomUserStatus.setName(userStatusData._id, userStatusData.name); - } + // update User status + if (userStatusData.name !== userStatusData.previousName) { + await CustomUserStatus.setName(userStatusData._id, userStatusData.name); + } - if (userStatusData.statusType !== userStatusData.previousStatusType) { - await CustomUserStatus.setStatusType(userStatusData._id, userStatusData.statusType); - } + if (userStatusData.statusType !== userStatusData.previousStatusType) { + await CustomUserStatus.setStatusType(userStatusData._id, userStatusData.statusType); + } + + void api.broadcast('user.updateCustomStatus', { ...userStatusData, _id: userStatusData._id }); - void api.broadcast('user.updateCustomStatus', { ...userStatusData, _id: userStatusData._id }); + return true; +}; + +Meteor.methods({ + async insertOrUpdateUserStatus(userStatusData) { + if (!this.userId) { + throw new Meteor.Error('not_authorized'); + } - return true; + return insertOrUpdateUserStatus(this.userId, userStatusData); }, }); diff --git a/apps/meteor/app/utils/client/getUserNotificationsSoundVolume.tsx b/apps/meteor/app/utils/client/getUserNotificationsSoundVolume.tsx deleted file mode 100644 index ea89cd51d4e3b..0000000000000 --- a/apps/meteor/app/utils/client/getUserNotificationsSoundVolume.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; - -import { getUserPreference } from './lib/getUserPreference'; - -export const getUserNotificationsSoundVolume = (userId: IUser['_id'] | null | undefined) => { - const masterVolume = getUserPreference(userId, 'masterVolume', 100); - const notificationsSoundVolume = getUserPreference(userId, 'notificationsSoundVolume', 100); - - return (notificationsSoundVolume * masterVolume) / 100; -}; diff --git a/apps/meteor/app/utils/client/lib/SDKClient.ts b/apps/meteor/app/utils/client/lib/SDKClient.ts index 9113976435377..d064f52e9ef74 100644 --- a/apps/meteor/app/utils/client/lib/SDKClient.ts +++ b/apps/meteor/app/utils/client/lib/SDKClient.ts @@ -249,12 +249,22 @@ export const createSDK = (rest: RestClientInterface) => { return Meteor.callAsync(method, ...args); }; + const disconnect = () => { + Meteor.disconnect(); + }; + + const reconnect = () => { + Meteor.reconnect(); + }; + return { rest, stop: stopAll, stream, publish, call, + disconnect, + reconnect, }; }; diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 4ec635611df81..19c2c4bb18695 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "7.5.1" + "version": "7.6.0-rc.8" } diff --git a/apps/meteor/app/webdav/server/lib/uploadFileToWebdav.ts b/apps/meteor/app/webdav/server/lib/uploadFileToWebdav.ts index 7b4c7a0951481..e7dc0df2cc632 100644 --- a/apps/meteor/app/webdav/server/lib/uploadFileToWebdav.ts +++ b/apps/meteor/app/webdav/server/lib/uploadFileToWebdav.ts @@ -11,7 +11,7 @@ export const uploadFileToWebdav = async (accountId: IWebdavAccount['_id'], fileD } const uploadFolder = 'Rocket.Chat Uploads/'; - const buffer = Buffer.from(fileData); + const buffer = Buffer.isBuffer(fileData) ? fileData : Buffer.from(fileData); const cred = getWebdavCredentials(account); const client = new WebdavClientAdapter(account.serverURL, cred); diff --git a/apps/meteor/app/webrtc/client/WebRTCClass.ts b/apps/meteor/app/webrtc/client/WebRTCClass.ts index 5d942c3656346..b99bea6cd28ba 100644 --- a/apps/meteor/app/webrtc/client/WebRTCClass.ts +++ b/apps/meteor/app/webrtc/client/WebRTCClass.ts @@ -3,7 +3,6 @@ import type { StreamKeys, StreamNames, StreamerCallbackArgs } from '@rocket.chat import { Emitter } from '@rocket.chat/emitter'; import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; -import { Tracker } from 'meteor/tracker'; import { ChromeScreenShare } from './screenShare'; import GenericModal from '../../../client/components/GenericModal'; @@ -65,9 +64,9 @@ type EventData; type CallData = EventData<'notify-room-users', `${string}/webrtc`, 'call'>; -type CandidateData = EventData<'notify-user', `${string}/webrtc`, 'candidate'>; -type DescriptionData = EventData<'notify-user', `${string}/webrtc`, 'description'>; -type JoinData = EventData<'notify-user', `${string}/webrtc`, 'join'>; +export type CandidateData = EventData<'notify-user', `${string}/webrtc`, 'candidate'>; +export type DescriptionData = EventData<'notify-user', `${string}/webrtc`, 'description'>; +export type JoinData = EventData<'notify-user', `${string}/webrtc`, 'join'>; type RemoteItem = { id: string; @@ -1074,31 +1073,4 @@ const WebRTC = new (class { } })(); -Meteor.startup(() => { - Tracker.autorun(() => { - const uid = Meteor.userId(); - - if (uid) { - sdk.stream('notify-user', [`${uid}/${WEB_RTC_EVENTS.WEB_RTC}`], (type, data) => { - if (data.room == null) { - return; - } - const webrtc = WebRTC.getInstanceByRoomId(data.room); - - switch (type) { - case 'candidate': - webrtc?.onUserStream('candidate', data); - break; - case 'description': - webrtc?.onUserStream('description', data); - break; - case 'join': - webrtc?.onUserStream('join', data); - break; - } - }); - } - }); -}); - export { WebRTC }; diff --git a/apps/meteor/app/wordpress/client/index.ts b/apps/meteor/app/wordpress/client/index.ts index 555f9f19df3c7..1015ce303cc5b 100644 --- a/apps/meteor/app/wordpress/client/index.ts +++ b/apps/meteor/app/wordpress/client/index.ts @@ -1,2 +1 @@ -import './lib'; import './wordpress-login-button.css'; diff --git a/apps/meteor/app/wordpress/client/lib.ts b/apps/meteor/app/wordpress/client/lib.ts deleted file mode 100644 index b213d5fb88c2a..0000000000000 --- a/apps/meteor/app/wordpress/client/lib.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { OauthConfig } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; -import _ from 'underscore'; - -import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth'; -import { settings } from '../../settings/client'; - -const config: OauthConfig = { - serverURL: '', - identityPath: '/oauth/me', - - addAutopublishFields: { - forLoggedInUser: ['services.wordpress'], - forOtherUsers: ['services.wordpress.user_login'], - }, - accessTokenParam: 'access_token', -}; - -const WordPress = new CustomOAuth('wordpress', config); - -const fillSettings = _.debounce(async (): Promise => { - config.serverURL = settings.get('API_Wordpress_URL'); - if (!config.serverURL) { - if (config.serverURL === undefined) { - return fillSettings(); - } - return; - } - - delete config.identityPath; - delete config.identityTokenSentVia; - delete config.authorizePath; - delete config.tokenPath; - delete config.scope; - - const serverType = settings.get('Accounts_OAuth_Wordpress_server_type'); - switch (serverType) { - case 'custom': - if (settings.get('Accounts_OAuth_Wordpress_identity_path')) { - config.identityPath = settings.get('Accounts_OAuth_Wordpress_identity_path'); - } - - if (settings.get('Accounts_OAuth_Wordpress_identity_token_sent_via')) { - config.identityTokenSentVia = settings.get('Accounts_OAuth_Wordpress_identity_token_sent_via'); - } - - if (settings.get('Accounts_OAuth_Wordpress_token_path')) { - config.tokenPath = settings.get('Accounts_OAuth_Wordpress_token_path'); - } - - if (settings.get('Accounts_OAuth_Wordpress_authorize_path')) { - config.authorizePath = settings.get('Accounts_OAuth_Wordpress_authorize_path'); - } - - if (settings.get('Accounts_OAuth_Wordpress_scope')) { - config.scope = settings.get('Accounts_OAuth_Wordpress_scope'); - } - break; - case 'wordpress-com': - config.identityPath = 'https://public-api.wordpress.com/rest/v1/me'; - config.identityTokenSentVia = 'header'; - config.authorizePath = 'https://public-api.wordpress.com/oauth2/authorize'; - config.tokenPath = 'https://public-api.wordpress.com/oauth2/token'; - config.scope = 'auth'; - break; - default: - config.identityPath = '/oauth/me'; - break; - } - - const result = WordPress.configure(config); - return result; -}, 100); - -Meteor.startup(() => { - return Tracker.autorun(() => { - return fillSettings(); - }); -}); diff --git a/apps/meteor/client/NavBarV2/NavBar.tsx b/apps/meteor/client/NavBarV2/NavBar.tsx index 26a281452c54e..497a90c9a1f58 100644 --- a/apps/meteor/client/NavBarV2/NavBar.tsx +++ b/apps/meteor/client/NavBarV2/NavBar.tsx @@ -1,85 +1,18 @@ -import { useToolbar } from '@react-aria/toolbar'; -import { NavBar as NavBarComponent, NavBarSection, NavBarGroup, NavBarDivider } from '@rocket.chat/fuselage'; -import { usePermission, useTranslation, useUser } from '@rocket.chat/ui-contexts'; -import { useVoipState } from '@rocket.chat/ui-voip'; -import { useRef } from 'react'; +import { NavBar as NavBarComponent } from '@rocket.chat/fuselage'; +import { useLayout } from '@rocket.chat/ui-contexts'; -import { - NavBarItemOmniChannelCallDialPad, - NavBarItemOmnichannelContact, - NavBarItemOmnichannelLivechatToggle, - NavBarItemOmnichannelQueue, - NavBarItemOmnichannelCallToggle, -} from './NavBarOmnichannelToolbar'; -import { NavBarItemMarketPlaceMenu, NavBarItemAuditMenu, NavBarItemDirectoryPage, NavBarItemHomePage } from './NavBarPagesToolbar'; -import { NavBarItemLoginPage, NavBarItemAdministrationMenu, UserMenu } from './NavBarSettingsToolbar'; -import { NavBarItemVoipDialer } from './NavBarVoipToolbar'; -import { useIsCallEnabled, useIsCallReady } from '../contexts/CallContext'; -import { useOmnichannelEnabled } from '../hooks/omnichannel/useOmnichannelEnabled'; -import { useOmnichannelShowQueueLink } from '../hooks/omnichannel/useOmnichannelShowQueueLink'; -import { useHasLicenseModule } from '../hooks/useHasLicenseModule'; +import NavBarControlsSection from './NavBarControls/NavBarControlsSection'; +import NavBarNavigation from './NavBarNavigation'; +import NavBarPagesSection from './NavBarPagesSection'; const NavBar = () => { - const t = useTranslation(); - const user = useUser(); - - const hasAuditLicense = useHasLicenseModule('auditing') === true; - - const showOmnichannel = useOmnichannelEnabled(); - const hasManageAppsPermission = usePermission('manage-apps'); - const hasAccessMarketplacePermission = usePermission('access-marketplace'); - const showMarketplace = hasAccessMarketplacePermission || hasManageAppsPermission; - - const showOmnichannelQueueLink = useOmnichannelShowQueueLink(); - const isCallEnabled = useIsCallEnabled(); - const isCallReady = useIsCallReady(); - const { isEnabled: showVoip } = useVoipState(); - - const pagesToolbarRef = useRef(null); - const { toolbarProps: pagesToolbarProps } = useToolbar({ 'aria-label': t('Pages') }, pagesToolbarRef); - - const omnichannelToolbarRef = useRef(null); - const { toolbarProps: omnichannelToolbarProps } = useToolbar({ 'aria-label': t('Omnichannel') }, omnichannelToolbarRef); - - const voipToolbarRef = useRef(null); - const { toolbarProps: voipToolbarProps } = useToolbar({ 'aria-label': t('Voice_Call') }, voipToolbarRef); + const { navbar } = useLayout(); return ( - - - - - {showMarketplace && } - {hasAuditLicense && } - - {showOmnichannel && ( - <> - - - {showOmnichannelQueueLink && } - {isCallReady && } - - {isCallEnabled && } - - - - )} - {showVoip && ( - <> - - - - - - )} - - - - - {user ? : } - - + {!navbar.searchExpanded && } + + {!navbar.searchExpanded && } ); }; diff --git a/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsMenu.tsx b/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsMenu.tsx new file mode 100644 index 0000000000000..8c92510d88fcf --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsMenu.tsx @@ -0,0 +1,49 @@ +import { NavBarItem } from '@rocket.chat/fuselage'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { GenericMenu } from '@rocket.chat/ui-client'; +import { useVoipState } from '@rocket.chat/ui-voip'; +import type { HTMLAttributes } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useOmnichannelEnabled } from '../../hooks/omnichannel/useOmnichannelEnabled'; + +type NavBarControlsMenuProps = Omit, 'is'> & { + voipItems: GenericMenuItemProps[]; + omnichannelItems: GenericMenuItemProps[]; + isPressed: boolean; +}; + +const NavBarControlsMenu = ({ voipItems, omnichannelItems, isPressed, ...props }: NavBarControlsMenuProps) => { + const { t } = useTranslation(); + const { isEnabled: showVoip } = useVoipState(); + const showOmnichannel = useOmnichannelEnabled(); + + const sections = [ + { + title: t('Voice_Call'), + items: showVoip ? voipItems : [], + }, + { + title: t('Omnichannel'), + items: showOmnichannel ? omnichannelItems : [], + }, + ].filter((section) => section.items.length > 0); + + if (sections.length === 0) { + return null; + } + + return ( + + ); +}; + +export default NavBarControlsMenu; diff --git a/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsSection.tsx b/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsSection.tsx new file mode 100644 index 0000000000000..75b3431fc5066 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsSection.tsx @@ -0,0 +1,41 @@ +import { NavBarSection, NavBarGroup, NavBarDivider } from '@rocket.chat/fuselage'; +import { useUser, useLayout } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; + +import NavBarControlsWithData from './NavBarControlsWithData'; +import NavBarOmnichannelGroup from '../NavBarOmnichannelGroup'; +import { NavBarItemLoginPage, NavBarItemAdministrationMenu, UserMenu } from '../NavBarSettingsToolbar'; +import NavBarVoipGroup from '../NavBarVoipGroup'; + +const NavBarControlsSection = () => { + const { t } = useTranslation(); + const user = useUser(); + + const { isMobile } = useLayout(); + + if (isMobile) { + return ( + + + + + + {user ? : } + + + ); + } + + return ( + + + + + + {user ? : } + + + ); +}; + +export default NavBarControlsSection; diff --git a/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsWithCall.tsx b/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsWithCall.tsx new file mode 100644 index 0000000000000..716279688c679 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsWithCall.tsx @@ -0,0 +1,50 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import type { HTMLAttributes } from 'react'; + +import NavBarControlsMenu from './NavBarControlsMenu'; +import { useOmnichannelCallDialPadAction } from '../NavBarOmnichannelGroup/hooks/useOmnichannelCallDialPadAction'; +import { useOmnichannelCallToggleAction } from '../NavBarOmnichannelGroup/hooks/useOmnichannelCallToggleAction'; + +type NavBarControlsMenuProps = Omit, 'is'> & { + voipItems: GenericMenuItemProps[]; + omnichannelItems: GenericMenuItemProps[]; + isPressed: boolean; +}; + +const NavBarControlsWithCall = ({ voipItems, omnichannelItems, isPressed, ...props }: NavBarControlsMenuProps) => { + const { + icon: omnichannelCallIcon, + title: omnichannelCallTitle, + handleOpenDialModal, + isDisabled: callDialPadDisabled, + } = useOmnichannelCallDialPadAction(); + + const { + title: omnichannelCallTogglerTitle, + icon: omnichannelCallTogglerIcon, + handleToggleCall, + isDisabled: callTogglerDisabled, + } = useOmnichannelCallToggleAction(); + + const omnichannelItemsWithCall = [ + ...omnichannelItems, + { + id: 'omnichannelCallDialPad', + icon: omnichannelCallIcon, + content: omnichannelCallTitle, + onClick: handleOpenDialModal, + disabled: callDialPadDisabled, + }, + { + id: 'omnichannelCallToggler', + icon: omnichannelCallTogglerIcon, + content: omnichannelCallTogglerTitle, + onClick: handleToggleCall, + disabled: callTogglerDisabled, + }, + ] as GenericMenuItemProps[]; + + return ; +}; + +export default NavBarControlsWithCall; diff --git a/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsWithData.tsx b/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsWithData.tsx new file mode 100644 index 0000000000000..ce4c7a9a3081f --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsWithData.tsx @@ -0,0 +1,90 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import type { HTMLAttributes } from 'react'; + +import NavBarControlsMenu from './NavBarControlsMenu'; +import NavbarControlsWithCall from './NavBarControlsWithCall'; +import { useIsCallEnabled } from '../../contexts/CallContext'; +import { useOmnichannelContactAction } from '../NavBarOmnichannelGroup/hooks/useOmnichannelContactAction'; +import { useOmnichannelLivechatToggle } from '../NavBarOmnichannelGroup/hooks/useOmnichannelLivechatToggle'; +import { useOmnichannelQueueAction } from '../NavBarOmnichannelGroup/hooks/useOmnichannelQueueAction'; +import { useVoipDialerAction } from '../NavBarVoipGroup/hooks/useVoipDialerAction'; +import { useVoipTogglerAction } from '../NavBarVoipGroup/hooks/useVoipTogglerAction'; + +type NavBarControlsMenuProps = Omit, 'is'>; + +const NavBarControlsWithData = (props: NavBarControlsMenuProps) => { + const isCallEnabled = useIsCallEnabled(); + + const { title: dialerTitle, handleToggleDialer, isPressed: isVoipDialerPressed, isDisabled: dialerDisabled } = useVoipDialerAction(); + const { isRegistered, title: togglerTitle, handleToggleVoip, isDisabled: togglerDisabled } = useVoipTogglerAction(); + + const { + isEnabled: queueEnabled, + icon: queueIcon, + title: queueTitle, + handleGoToQueue, + isPressed: isQueuePressed, + } = useOmnichannelQueueAction(); + + const { + title: contactCenterTitle, + icon: contactCenterIcon, + handleGoToContactCenter, + isPressed: isContactPressed, + } = useOmnichannelContactAction(); + + const { + title: omnichannelLivechatTogglerTitle, + icon: omnichannelLivechatTogglerIcon, + handleAvailableStatusChange, + } = useOmnichannelLivechatToggle(); + + const voipItems = [ + { + id: 'voipDialer', + icon: 'dialpad', + content: dialerTitle, + onClick: handleToggleDialer, + disabled: dialerDisabled, + }, + { + id: 'voipToggler', + icon: isRegistered ? 'phone-disabled' : 'phone', + content: togglerTitle, + onClick: handleToggleVoip, + disabled: togglerDisabled, + }, + ].filter(Boolean) as GenericMenuItemProps[]; + + const omnichannelItems = [ + queueEnabled && { + id: 'omnichannelQueue', + icon: queueIcon, + content: queueTitle, + onClick: handleGoToQueue, + disabled: dialerDisabled, + }, + { + id: 'omnichannelContact', + icon: contactCenterIcon, + content: contactCenterTitle, + onClick: handleGoToContactCenter, + }, + { + id: 'omnichannelLivechatToggler', + icon: omnichannelLivechatTogglerIcon, + content: omnichannelLivechatTogglerTitle, + onClick: handleAvailableStatusChange, + }, + ].filter(Boolean) as GenericMenuItemProps[]; + + const isPressed = isVoipDialerPressed || isQueuePressed || isContactPressed; + + if (isCallEnabled) { + return ; + } + + return ; +}; + +export default NavBarControlsWithData; diff --git a/apps/meteor/client/NavBarV2/NavBarNavigation.tsx b/apps/meteor/client/NavBarV2/NavBarNavigation.tsx new file mode 100644 index 0000000000000..e29f1d194e443 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarNavigation.tsx @@ -0,0 +1,28 @@ +import { NavBarGroup, NavBarItem, Box } from '@rocket.chat/fuselage'; +import { useLayout, useRouter } from '@rocket.chat/ui-contexts'; +import { FocusScope } from 'react-aria'; +import { useTranslation } from 'react-i18next'; + +import NavBarSearch from './NavBarSearch'; + +const NavbarNavigation = () => { + const { t } = useTranslation(); + const { navigate } = useRouter(); + const { isMobile } = useLayout(); + + return ( + + + + + {!isMobile && ( + + navigate(-1)} icon='chevron-right' small /> + navigate(1)} icon='chevron-left' small /> + + )} + + ); +}; + +export default NavbarNavigation; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmniChannelCallDialPad.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmniChannelCallDialPad.tsx new file mode 100644 index 0000000000000..7206bbea812ac --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmniChannelCallDialPad.tsx @@ -0,0 +1,19 @@ +import { NavBarItem } from '@rocket.chat/fuselage'; +import type { ComponentPropsWithoutRef } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useOmnichannelCallDialPadAction } from './hooks/useOmnichannelCallDialPadAction'; + +type NavBarItemOmniChannelCallDialPadProps = ComponentPropsWithoutRef; + +const NavBarItemOmniChannelCallDialPad = (props: NavBarItemOmniChannelCallDialPadProps) => { + const { t } = useTranslation(); + + const { title, icon, handleOpenDialModal, isDisabled } = useOmnichannelCallDialPadAction(); + + return ( + + ); +}; + +export default NavBarItemOmniChannelCallDialPad; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelCallToggle.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelCallToggle.tsx new file mode 100644 index 0000000000000..d6589117f6fbb --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelCallToggle.tsx @@ -0,0 +1,28 @@ +import { NavBarItem } from '@rocket.chat/fuselage'; +import type { HTMLAttributes } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useOmnichannelCallToggleAction } from './hooks/useOmnichannelCallToggleAction'; + +type NavBarItemOmnichannelCallToggleProps = Omit, 'is'>; + +const NavBarItemOmnichannelCallToggle = (props: NavBarItemOmnichannelCallToggleProps) => { + const { t } = useTranslation(); + const { icon, title, handleToggleCall, isSuccess, isWarning, isDanger, isDisabled } = useOmnichannelCallToggleAction(); + + return ( + + ); +}; + +export default NavBarItemOmnichannelCallToggle; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelContact.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelContact.tsx new file mode 100644 index 0000000000000..f0f25efb78d56 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelContact.tsx @@ -0,0 +1,14 @@ +import { NavBarItem } from '@rocket.chat/fuselage'; +import type { HTMLAttributes } from 'react'; + +import { useOmnichannelContactAction } from './hooks/useOmnichannelContactAction'; + +type NavBarItemOmnichannelContactProps = Omit, 'is'>; + +const NavBarItemOmnichannelContact = (props: NavBarItemOmnichannelContactProps) => { + const { icon, isPressed, title, handleGoToContactCenter } = useOmnichannelContactAction(); + + return ; +}; + +export default NavBarItemOmnichannelContact; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelLivechatToggle.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelLivechatToggle.tsx new file mode 100644 index 0000000000000..47252f0e9f856 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelLivechatToggle.tsx @@ -0,0 +1,23 @@ +import { NavBarItem } from '@rocket.chat/fuselage'; +import type { HTMLAttributes } from 'react'; + +import { useOmnichannelLivechatToggle } from './hooks/useOmnichannelLivechatToggle'; + +type NavBarItemOmnichannelLivechatToggleProps = Omit, 'is'>; + +const NavBarItemOmnichannelLivechatToggle = (props: NavBarItemOmnichannelLivechatToggleProps) => { + const { handleAvailableStatusChange, title, icon, isSuccess } = useOmnichannelLivechatToggle(); + + return ( + + ); +}; + +export default NavBarItemOmnichannelLivechatToggle; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelQueue.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelQueue.tsx new file mode 100644 index 0000000000000..c31ce4a296cd0 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelQueue.tsx @@ -0,0 +1,18 @@ +import { NavBarItem } from '@rocket.chat/fuselage'; +import type { HTMLAttributes } from 'react'; + +import { useOmnichannelQueueAction } from './hooks/useOmnichannelQueueAction'; + +type NavBarItemOmnichannelQueueProps = Omit, 'is'>; + +const NavBarItemOmnichannelQueue = (props: NavBarItemOmnichannelQueueProps) => { + const { isEnabled, title, icon, isPressed, handleGoToQueue } = useOmnichannelQueueAction(); + + if (!isEnabled) { + return null; + } + + return ; +}; + +export default NavBarItemOmnichannelQueue; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarOmnichannelGroup.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarOmnichannelGroup.tsx new file mode 100644 index 0000000000000..d7eeccea40091 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarOmnichannelGroup.tsx @@ -0,0 +1,37 @@ +import { NavBarDivider, NavBarGroup } from '@rocket.chat/fuselage'; +import { useTranslation } from 'react-i18next'; + +import NavBarItemOmniChannelCallDialPad from './NavBarItemOmniChannelCallDialPad'; +import NavBarItemOmnichannelCallToggle from './NavBarItemOmnichannelCallToggle'; +import NavBarItemOmnichannelContact from './NavBarItemOmnichannelContact'; +import NavBarItemOmnichannelLivechatToggle from './NavBarItemOmnichannelLivechatToggle'; +import NavBarItemOmnichannelQueue from './NavBarItemOmnichannelQueue'; +import { useIsCallEnabled, useIsCallReady } from '../../contexts/CallContext'; +import { useOmnichannelEnabled } from '../../hooks/omnichannel/useOmnichannelEnabled'; + +const NavBarOmnichannelGroup = () => { + const { t } = useTranslation(); + const showOmnichannel = useOmnichannelEnabled(); + + const isCallEnabled = useIsCallEnabled(); + const isCallReady = useIsCallReady(); + + if (!showOmnichannel) { + return null; + } + + return ( + <> + + + {isCallReady && } + + {isCallEnabled && } + + + + + ); +}; + +export default NavBarOmnichannelGroup; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelCallDialPadAction.ts b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelCallDialPadAction.ts new file mode 100644 index 0000000000000..6cf14e43446a0 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelCallDialPadAction.ts @@ -0,0 +1,20 @@ +import type { Keys } from '@rocket.chat/icons'; +import { useTranslation } from 'react-i18next'; + +import { useVoipOutboundStates } from '../../../contexts/CallContext'; +import { useDialModal } from '../../../hooks/useDialModal'; + +export const useOmnichannelCallDialPadAction = () => { + const { t } = useTranslation(); + + const { openDialModal } = useDialModal(); + + const { outBoundCallsAllowed, outBoundCallsEnabledForUser } = useVoipOutboundStates(); + + return { + isDisabled: !outBoundCallsEnabledForUser, + handleOpenDialModal: () => openDialModal(), + icon: 'dialpad' as Keys, + title: outBoundCallsAllowed ? t('New_Call') : t('New_Call_Premium_Only'), + }; +}; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelCallToggleAction.ts b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelCallToggleAction.ts new file mode 100644 index 0000000000000..a58f3de3cebe4 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelCallToggleAction.ts @@ -0,0 +1,75 @@ +import type { Keys as IconName } from '@rocket.chat/icons'; +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { + useIsCallReady, + useIsCallError, + useCallerInfo, + useCallRegisterClient, + useCallUnregisterClient, + useVoipNetworkStatus, +} from '../../../contexts/CallContext'; + +export const useOmnichannelCallToggleAction = () => { + const { t } = useTranslation(); + const isCallReady = useIsCallReady(); + const isCallError = useIsCallError(); + + const caller = useCallerInfo(); + const unregister = useCallUnregisterClient(); + const register = useCallRegisterClient(); + + const networkStatus = useVoipNetworkStatus(); + const registered = !['ERROR', 'INITIAL', 'UNREGISTERED'].includes(caller.state); + const inCall = ['IN_CALL'].includes(caller.state); + + const handleToggleCall = useCallback(() => { + if (registered) { + unregister(); + return; + } + register(); + }, [registered, register, unregister]); + + const title = useMemo(() => { + if (isCallError) { + return t('Error'); + } + + if (!isCallReady) { + return t('Loading'); + } + + if (networkStatus === 'offline') { + return t('Waiting_for_server_connection'); + } + + if (inCall) { + return t('Cannot_disable_while_on_call'); + } + + if (registered) { + return t('Turn_off_answer_calls'); + } + + return t('Turn_on_answer_calls'); + }, [inCall, isCallError, isCallReady, networkStatus, registered, t]); + + const icon: IconName = useMemo(() => { + if (networkStatus === 'offline') { + return 'phone-issue'; + } + return registered ? 'phone' : 'phone-disabled'; + }, [networkStatus, registered]); + + return { + handleToggleCall, + title, + icon, + isDisabled: inCall || isCallError || !isCallReady, + isDanger: isCallError, + isSuccess: registered, + isWarning: networkStatus === 'offline', + }; +}; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelContactAction.ts b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelContactAction.ts new file mode 100644 index 0000000000000..a9ecbd09a96d8 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelContactAction.ts @@ -0,0 +1,16 @@ +import type { Keys } from '@rocket.chat/icons'; +import { useCurrentRoutePath, useRouter } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; + +export const useOmnichannelContactAction = () => { + const { t } = useTranslation(); + const router = useRouter(); + const currentRoute = useCurrentRoutePath(); + + return { + icon: 'address-book' as Keys, + title: t('Contact_Center'), + handleGoToContactCenter: () => router.navigate('/omnichannel-directory'), + isPressed: currentRoute?.includes('/omnichannel-directory') || false, + }; +}; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelLivechatToggle.ts b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelLivechatToggle.ts new file mode 100644 index 0000000000000..45fb208f05382 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelLivechatToggle.ts @@ -0,0 +1,28 @@ +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import type { Keys } from '@rocket.chat/icons'; +import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; + +import { useOmnichannelAgentAvailable } from '../../../hooks/omnichannel/useOmnichannelAgentAvailable'; + +export const useOmnichannelLivechatToggle = () => { + const { t } = useTranslation(); + const agentAvailable = useOmnichannelAgentAvailable(); + const changeAgentStatus = useEndpoint('POST', '/v1/livechat/agent.status'); + const dispatchToastMessage = useToastMessageDispatch(); + + const handleAvailableStatusChange = useEffectEvent(async () => { + try { + await changeAgentStatus({}); + } catch (error: unknown) { + dispatchToastMessage({ type: 'error', message: error }); + } + }); + + return { + title: agentAvailable ? t('Turn_off_answer_chats') : t('Turn_on_answer_chats'), + isSuccess: agentAvailable, + icon: (agentAvailable ? 'message' : 'message-disabled') as Keys, + handleAvailableStatusChange, + }; +}; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelQueueAction.ts b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelQueueAction.ts new file mode 100644 index 0000000000000..307d01372fa5a --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelQueueAction.ts @@ -0,0 +1,21 @@ +import type { Keys } from '@rocket.chat/icons'; +import { useCurrentRoutePath, useRouter } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; + +import { useOmnichannelShowQueueLink } from '../../../hooks/omnichannel/useOmnichannelShowQueueLink'; + +export const useOmnichannelQueueAction = () => { + const { t } = useTranslation(); + const showOmnichannelQueueLink = useOmnichannelShowQueueLink(); + + const router = useRouter(); + const currentRoute = useCurrentRoutePath(); + + return { + isEnabled: showOmnichannelQueueLink, + icon: 'queue' as Keys, + title: t('Queue'), + handleGoToQueue: () => router.navigate('/livechat-queue'), + isPressed: currentRoute?.includes('/livechat-queue') || false, + }; +}; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/index.ts b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/index.ts new file mode 100644 index 0000000000000..d60b0385dd5e5 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/index.ts @@ -0,0 +1 @@ +export { default } from './NavBarOmnichannelGroup'; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmniChannelCallDialPad.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmniChannelCallDialPad.tsx deleted file mode 100644 index a1abf735a1191..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmniChannelCallDialPad.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { NavBarItem } from '@rocket.chat/fuselage'; -import type { ComponentPropsWithoutRef } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { useVoipOutboundStates } from '../../contexts/CallContext'; -import { useDialModal } from '../../hooks/useDialModal'; - -type NavBarItemOmniChannelCallDialPadProps = ComponentPropsWithoutRef; - -const NavBarItemOmniChannelCallDialPad = (props: NavBarItemOmniChannelCallDialPadProps) => { - const { t } = useTranslation(); - - const { openDialModal } = useDialModal(); - - const { outBoundCallsAllowed, outBoundCallsEnabledForUser } = useVoipOutboundStates(); - - return ( - openDialModal()} - disabled={!outBoundCallsEnabledForUser} - aria-label={t('Open_Dialpad')} - data-tooltip={outBoundCallsAllowed ? t('New_Call') : t('New_Call_Premium_Only')} - {...props} - /> - ); -}; - -export default NavBarItemOmniChannelCallDialPad; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggle.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggle.tsx deleted file mode 100644 index 83c0da87964dd..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggle.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { ComponentPropsWithoutRef } from 'react'; - -import NavBarItemOmnichannelCallToggleError from './NavBarItemOmnichannelCallToggleError'; -import NavBarItemOmnichannelCallToggleLoading from './NavBarItemOmnichannelCallToggleLoading'; -import NavBarItemOmnichannelCallToggleReady from './NavBarItemOmnichannelCallToggleReady'; -import { useIsCallReady, useIsCallError } from '../../contexts/CallContext'; - -type NavBarItemOmnichannelCallToggleProps = ComponentPropsWithoutRef< - typeof NavBarItemOmnichannelCallToggleError | typeof NavBarItemOmnichannelCallToggleLoading | typeof NavBarItemOmnichannelCallToggleReady ->; - -const NavBarItemOmnichannelCallToggle = (props: NavBarItemOmnichannelCallToggleProps) => { - const isCallReady = useIsCallReady(); - const isCallError = useIsCallError(); - if (isCallError) { - return ; - } - - if (!isCallReady) { - return ; - } - - return ; -}; - -export default NavBarItemOmnichannelCallToggle; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleError.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleError.tsx deleted file mode 100644 index a713310d6d831..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleError.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { NavBarItem } from '@rocket.chat/fuselage'; -import type { ComponentPropsWithoutRef } from 'react'; -import { useTranslation } from 'react-i18next'; - -type NavBarItemOmnichannelCallToggleErrorProps = ComponentPropsWithoutRef; - -const NavBarItemOmnichannelCallToggleError = (props: NavBarItemOmnichannelCallToggleErrorProps) => { - const { t } = useTranslation(); - return ; -}; - -export default NavBarItemOmnichannelCallToggleError; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleLoading.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleLoading.tsx deleted file mode 100644 index b39a2541513eb..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleLoading.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { NavBarItem } from '@rocket.chat/fuselage'; -import type { ComponentPropsWithoutRef } from 'react'; -import { useTranslation } from 'react-i18next'; - -type NavBarItemOmnichannelCallToggleLoadingProps = ComponentPropsWithoutRef; - -const NavBarItemOmnichannelCallToggleLoading = (props: NavBarItemOmnichannelCallToggleLoadingProps) => { - const { t } = useTranslation(); - return ; -}; - -export default NavBarItemOmnichannelCallToggleLoading; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleReady.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleReady.tsx deleted file mode 100644 index 104c6a2cc7b30..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleReady.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { NavBarItem } from '@rocket.chat/fuselage'; -import type { ComponentPropsWithoutRef } from 'react'; -import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { useCallerInfo, useCallRegisterClient, useCallUnregisterClient, useVoipNetworkStatus } from '../../contexts/CallContext'; - -type NavBarItemOmnichannelCallToggleReadyProps = ComponentPropsWithoutRef; - -const NavBarItemOmnichannelCallToggleReady = (props: NavBarItemOmnichannelCallToggleReadyProps) => { - const { t } = useTranslation(); - - const caller = useCallerInfo(); - const unregister = useCallUnregisterClient(); - const register = useCallRegisterClient(); - - const networkStatus = useVoipNetworkStatus(); - const registered = !['ERROR', 'INITIAL', 'UNREGISTERED'].includes(caller.state); - const inCall = ['IN_CALL'].includes(caller.state); - - const onClickVoipButton = useCallback((): void => { - if (registered) { - unregister(); - return; - } - register(); - }, [registered, register, unregister]); - - const getTitle = (): string => { - if (networkStatus === 'offline') { - return t('Waiting_for_server_connection'); - } - - if (inCall) { - return t('Cannot_disable_while_on_call'); - } - - if (registered) { - return t('Turn_off_answer_calls'); - } - - return t('Turn_on_answer_calls'); - }; - - const getIcon = (): 'phone-issue' | 'phone' | 'phone-disabled' => { - if (networkStatus === 'offline') { - return 'phone-issue'; - } - return registered ? 'phone' : 'phone-disabled'; - }; - - return ( - - ); -}; - -export default NavBarItemOmnichannelCallToggleReady; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelContact.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelContact.tsx deleted file mode 100644 index 08eaeaabacb4e..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelContact.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { NavBarItem } from '@rocket.chat/fuselage'; -import { useRouter, useCurrentRoutePath } from '@rocket.chat/ui-contexts'; -import type { HTMLAttributes } from 'react'; - -type NavBarItemOmnichannelContactProps = Omit, 'is'>; - -const NavBarItemOmnichannelContact = (props: NavBarItemOmnichannelContactProps) => { - const router = useRouter(); - const currentRoute = useCurrentRoutePath(); - - return ( - router.navigate('/omnichannel-directory')} - pressed={currentRoute?.includes('/omnichannel-directory')} - /> - ); -}; - -export default NavBarItemOmnichannelContact; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelLivechatToggle.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelLivechatToggle.tsx deleted file mode 100644 index f67c21647a064..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelLivechatToggle.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Sidebar } from '@rocket.chat/fuselage'; -import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; -import type { ReactElement, ComponentProps } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { useOmnichannelAgentAvailable } from '../../hooks/omnichannel/useOmnichannelAgentAvailable'; - -type NavBarItemOmnichannelLivechatToggleProps = Omit, 'icon'>; - -const NavBarItemOmnichannelLivechatToggle = (props: NavBarItemOmnichannelLivechatToggleProps): ReactElement => { - const { t } = useTranslation(); - const agentAvailable = useOmnichannelAgentAvailable(); - const changeAgentStatus = useEndpoint('POST', '/v1/livechat/agent.status'); - const dispatchToastMessage = useToastMessageDispatch(); - - const handleAvailableStatusChange = useEffectEvent(async () => { - try { - await changeAgentStatus({}); - } catch (error: unknown) { - dispatchToastMessage({ type: 'error', message: error }); - } - }); - - return ( - - ); -}; - -export default NavBarItemOmnichannelLivechatToggle; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelQueue.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelQueue.tsx deleted file mode 100644 index 471a1293ddbc0..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelQueue.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { NavBarItem } from '@rocket.chat/fuselage'; -import { useRouter, useCurrentRoutePath } from '@rocket.chat/ui-contexts'; -import type { HTMLAttributes } from 'react'; - -type NavBarItemOmnichannelQueueProps = Omit, 'is'>; - -const NavBarItemOmnichannelQueue = (props: NavBarItemOmnichannelQueueProps) => { - const router = useRouter(); - const currentRoute = useCurrentRoutePath(); - - return ( - router.navigate('/livechat-queue')} - pressed={currentRoute?.includes('/livechat-queue')} - /> - ); -}; - -export default NavBarItemOmnichannelQueue; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/index.ts b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/index.ts deleted file mode 100644 index 8dacb885deb3b..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { default as NavBarItemOmniChannelCallDialPad } from './NavBarItemOmniChannelCallDialPad'; -export { default as NavBarItemOmnichannelCallToggle } from './NavBarItemOmnichannelCallToggle'; -export { default as NavBarItemOmnichannelContact } from './NavBarItemOmnichannelContact'; -export { default as NavBarItemOmnichannelLivechatToggle } from './NavBarItemOmnichannelLivechatToggle'; -export { default as NavBarItemOmnichannelQueue } from './NavBarItemOmnichannelQueue'; diff --git a/apps/meteor/client/sidebarv2/header/actions/CreateRoom.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemCreateNew.tsx similarity index 50% rename from apps/meteor/client/sidebarv2/header/actions/CreateRoom.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemCreateNew.tsx index c720b8a768f40..f2b3c2956e97d 100644 --- a/apps/meteor/client/sidebarv2/header/actions/CreateRoom.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemCreateNew.tsx @@ -3,16 +3,16 @@ import { GenericMenu } from '@rocket.chat/ui-client'; import type { HTMLAttributes } from 'react'; import { useTranslation } from 'react-i18next'; -import { useCreateRoom } from './hooks/useCreateRoomMenu'; +import { useCreateNewMenu } from './hooks/useCreateNewMenu'; type CreateRoomProps = Omit, 'is'>; -const CreateRoom = (props: CreateRoomProps) => { +const NavBarItemCreateNew = (props: CreateRoomProps) => { const { t } = useTranslation(); - const sections = useCreateRoom(); + const sections = useCreateNewMenu(); - return ; + return ; }; -export default CreateRoom; +export default NavBarItemCreateNew; diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemDirectoryPage.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemDirectoryPage.tsx similarity index 100% rename from apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemDirectoryPage.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemDirectoryPage.tsx diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemHomePage.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemHomePage.tsx similarity index 100% rename from apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemHomePage.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemHomePage.tsx diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemMarketPlaceMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemMarketPlaceMenu.tsx similarity index 100% rename from apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemMarketPlaceMenu.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemMarketPlaceMenu.tsx diff --git a/apps/meteor/client/sidebarv2/header/actions/Sort.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemSort.tsx similarity index 74% rename from apps/meteor/client/sidebarv2/header/actions/Sort.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemSort.tsx index 5f0a19b2c66da..430718e036820 100644 --- a/apps/meteor/client/sidebarv2/header/actions/Sort.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemSort.tsx @@ -5,9 +5,9 @@ import { useTranslation } from 'react-i18next'; import { useSortMenu } from './hooks/useSortMenu'; -type SortProps = Omit, 'is'>; +type NavBarItemSortProps = Omit, 'is'>; -const Sort = (props: SortProps) => { +const NavBarItemSort = (props: NavBarItemSortProps) => { const { t } = useTranslation(); const sections = useSortMenu(); @@ -15,4 +15,4 @@ const Sort = (props: SortProps) => { return ; }; -export default Sort; +export default NavBarItemSort; diff --git a/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarPagesGroup.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarPagesGroup.tsx new file mode 100644 index 0000000000000..f4d4a89068bac --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarPagesGroup.tsx @@ -0,0 +1,36 @@ +import { NavBarGroup } from '@rocket.chat/fuselage'; +import { useLayout, usePermission } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; + +import NavBarItemCreateNew from './NavBarItemCreateNew'; +import NavBarItemDirectoryPage from './NavBarItemDirectoryPage'; +import NavBarItemHomePage from './NavBarItemHomePage'; +import NavBarItemMarketPlaceMenu from './NavBarItemMarketPlaceMenu'; +import NavBarItemSort from './NavBarItemSort'; +import NavBarPagesStackMenu from './NavBarPagesStackMenu'; + +const NavBarPagesGroup = () => { + const { t } = useTranslation(); + const { isTablet, isMobile } = useLayout(); + + const hasManageAppsPermission = usePermission('manage-apps'); + const hasAccessMarketplacePermission = usePermission('access-marketplace'); + const showMarketplace = hasAccessMarketplacePermission || hasManageAppsPermission; + + return ( + + {isTablet && } + {!isTablet && ( + <> + + + + )} + {showMarketplace && !isMobile && } + + {!isMobile && } + + ); +}; + +export default NavBarPagesGroup; diff --git a/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarPagesStackMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarPagesStackMenu.tsx new file mode 100644 index 0000000000000..16e7b81e4378e --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarPagesStackMenu.tsx @@ -0,0 +1,46 @@ +import { NavBarItem } from '@rocket.chat/fuselage'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { GenericMenu } from '@rocket.chat/ui-client'; +import { useCurrentRoutePath, useLayout, useRouter, useSetting } from '@rocket.chat/ui-contexts'; +import type { HTMLAttributes } from 'react'; +import { useTranslation } from 'react-i18next'; + +type NavBarPagesStackMenuProps = Omit, 'is'>; + +const NavBarPagesStackMenu = (props: NavBarPagesStackMenuProps) => { + const { t } = useTranslation(); + + const showHome = useSetting('Layout_Show_Home_Button'); + const { sidebar } = useLayout(); + const router = useRouter(); + + const handleGoToHome = useEffectEvent(() => { + sidebar.toggle(); + router.navigate('/home'); + }); + + const currentRoute = useCurrentRoutePath(); + const pressed = currentRoute?.includes('/directory') || currentRoute?.includes('/home'); + + const items = [ + showHome && { + id: 'home', + icon: 'home', + content: t('Home'), + onClick: handleGoToHome, + }, + { + id: 'directory', + icon: 'notebook-hashtag', + content: t('Directory'), + onClick: () => router.navigate('/directory'), + }, + ].filter(Boolean) as GenericMenuItemProps[]; + + return ( + + ); +}; + +export default NavBarPagesStackMenu; diff --git a/apps/meteor/client/sidebarv2/header/CreateChannelModal.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.tsx similarity index 97% rename from apps/meteor/client/sidebarv2/header/CreateChannelModal.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.tsx index b279993cd9229..b5cdfe223002a 100644 --- a/apps/meteor/client/sidebarv2/header/CreateChannelModal.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.tsx @@ -28,10 +28,10 @@ import type { ComponentProps, ReactElement } from 'react'; import { useId, useEffect, useMemo } from 'react'; import { useForm, Controller } from 'react-hook-form'; -import { useEncryptedRoomDescription } from './hooks/useEncryptedRoomDescription'; -import UserAutoCompleteMultipleFederated from '../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; -import { useHasLicenseModule } from '../../hooks/useHasLicenseModule'; -import { goToRoomById } from '../../lib/utils/goToRoomById'; +import { useEncryptedRoomDescription } from './useEncryptedRoomDescription'; +import UserAutoCompleteMultipleFederated from '../../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; +import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; +import { goToRoomById } from '../../../lib/utils/goToRoomById'; type CreateChannelModalProps = { teamId?: string; diff --git a/apps/meteor/client/sidebarv2/header/CreateDirectMessage.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateDirectMessage.tsx similarity index 94% rename from apps/meteor/client/sidebarv2/header/CreateDirectMessage.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateDirectMessage.tsx index cdb02d364b05c..1a7a5a5f5b7ff 100644 --- a/apps/meteor/client/sidebarv2/header/CreateDirectMessage.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateDirectMessage.tsx @@ -5,8 +5,8 @@ import { useMutation } from '@tanstack/react-query'; import { useId, memo } from 'react'; import { useForm, Controller } from 'react-hook-form'; -import UserAutoCompleteMultipleFederated from '../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; -import { goToRoomById } from '../../lib/utils/goToRoomById'; +import UserAutoCompleteMultipleFederated from '../../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; +import { goToRoomById } from '../../../lib/utils/goToRoomById'; type CreateDirectMessageProps = { onClose: () => void }; diff --git a/apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateTeamModal.tsx similarity index 98% rename from apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateTeamModal.tsx index 84ad009c96f91..29585f2cee1ec 100644 --- a/apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateTeamModal.tsx @@ -30,9 +30,9 @@ import type { ComponentProps, ReactElement } from 'react'; import { useId, memo, useEffect, useMemo } from 'react'; import { Controller, useForm } from 'react-hook-form'; -import { useEncryptedRoomDescription } from './hooks/useEncryptedRoomDescription'; -import UserAutoCompleteMultiple from '../../components/UserAutoCompleteMultiple'; -import { goToRoomById } from '../../lib/utils/goToRoomById'; +import { useEncryptedRoomDescription } from './useEncryptedRoomDescription'; +import UserAutoCompleteMultiple from '../../../components/UserAutoCompleteMultiple'; +import { goToRoomById } from '../../../lib/utils/goToRoomById'; type CreateTeamModalInputs = { name: string; diff --git a/apps/meteor/client/sidebarv2/header/hooks/useEncryptedRoomDescription.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/useEncryptedRoomDescription.tsx similarity index 100% rename from apps/meteor/client/sidebarv2/header/hooks/useEncryptedRoomDescription.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/useEncryptedRoomDescription.tsx diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomItems.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useCreateNewItems.tsx similarity index 84% rename from apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomItems.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useCreateNewItems.tsx index 4d313975d6f54..7ac76f3f7815e 100644 --- a/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomItems.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useCreateNewItems.tsx @@ -1,18 +1,18 @@ import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation, useSetting, useAtLeastOnePermission } from '@rocket.chat/ui-contexts'; -import CreateDiscussion from '../../../../components/CreateDiscussion'; -import CreateChannelModal from '../../CreateChannelModal'; -import CreateDirectMessage from '../../CreateDirectMessage'; -import CreateTeamModal from '../../CreateTeamModal'; -import { useCreateRoomModal } from '../../hooks/useCreateRoomModal'; +import { useCreateRoomModal } from './useCreateRoomModal'; +import CreateDiscussion from '../../../components/CreateDiscussion'; +import CreateChannelModal from '../actions/CreateChannelModal'; +import CreateDirectMessage from '../actions/CreateDirectMessage'; +import CreateTeamModal from '../actions/CreateTeamModal'; const CREATE_CHANNEL_PERMISSIONS = ['create-c', 'create-p']; const CREATE_TEAM_PERMISSIONS = ['create-team']; const CREATE_DIRECT_PERMISSIONS = ['create-d']; const CREATE_DISCUSSION_PERMISSIONS = ['start-discussion', 'start-discussion-other-user']; -export const useCreateRoomItems = (): GenericMenuItemProps[] => { +export const useCreateNewItems = (): GenericMenuItemProps[] => { const t = useTranslation(); const discussionEnabled = useSetting('Discussion_enabled'); diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useCreateNewMenu.tsx similarity index 81% rename from apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomMenu.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useCreateNewMenu.tsx index 795944b8d819a..781f7728bcfd7 100644 --- a/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useCreateNewMenu.tsx @@ -1,20 +1,20 @@ import { useAtLeastOnePermission, useSetting } from '@rocket.chat/ui-contexts'; import { useTranslation } from 'react-i18next'; -import { useCreateRoomItems } from './useCreateRoomItems'; +import { useCreateNewItems } from './useCreateNewItems'; import { useMatrixFederationItems } from './useMatrixFederationItems'; -import { useIsEnterprise } from '../../../../hooks/useIsEnterprise'; +import { useIsEnterprise } from '../../../hooks/useIsEnterprise'; const CREATE_ROOM_PERMISSIONS = ['create-c', 'create-p', 'create-d', 'start-discussion', 'start-discussion-other-user']; -export const useCreateRoom = () => { +export const useCreateNewMenu = () => { const { t } = useTranslation(); const showCreate = useAtLeastOnePermission(CREATE_ROOM_PERMISSIONS); const { data } = useIsEnterprise(); const isMatrixEnabled = useSetting('Federation_Matrix_enabled') && data?.isEnterprise; - const createRoomItems = useCreateRoomItems(); + const createRoomItems = useCreateNewItems(); const matrixFederationSearchItems = useMatrixFederationItems({ isMatrixEnabled }); const sections = [ diff --git a/apps/meteor/client/sidebarv2/header/hooks/useCreateRoomModal.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useCreateRoomModal.tsx similarity index 100% rename from apps/meteor/client/sidebarv2/header/hooks/useCreateRoomModal.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useCreateRoomModal.tsx diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.spec.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useGroupingListItems.spec.tsx similarity index 100% rename from apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.spec.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useGroupingListItems.spec.tsx diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useGroupingListItems.tsx similarity index 100% rename from apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useGroupingListItems.tsx diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useMarketPlaceMenu.spec.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useMarketPlaceMenu.spec.tsx similarity index 100% rename from apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useMarketPlaceMenu.spec.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useMarketPlaceMenu.spec.tsx diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useMarketPlaceMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useMarketPlaceMenu.tsx similarity index 100% rename from apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useMarketPlaceMenu.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useMarketPlaceMenu.tsx diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useMatrixFederationItems.ts b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useMatrixFederationItems.ts similarity index 82% rename from apps/meteor/client/sidebarv2/header/actions/hooks/useMatrixFederationItems.ts rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useMatrixFederationItems.ts index cd0abc9bdfb20..2a90219e03fd7 100644 --- a/apps/meteor/client/sidebarv2/header/actions/hooks/useMatrixFederationItems.ts +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useMatrixFederationItems.ts @@ -1,8 +1,8 @@ import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation } from 'react-i18next'; -import MatrixFederationSearch from '../../MatrixFederationSearch'; -import { useCreateRoomModal } from '../../hooks/useCreateRoomModal'; +import { useCreateRoomModal } from './useCreateRoomModal'; +import MatrixFederationSearch from '../../../sidebarv2/header/MatrixFederationSearch'; export const useMatrixFederationItems = ({ isMatrixEnabled, diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useSortMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useSortMenu.tsx similarity index 100% rename from apps/meteor/client/sidebarv2/header/actions/hooks/useSortMenu.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useSortMenu.tsx diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.spec.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useSortModeItems.spec.tsx similarity index 100% rename from apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.spec.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useSortModeItems.spec.tsx diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useSortModeItems.tsx similarity index 95% rename from apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useSortModeItems.tsx index b0f77eab7a0a6..c1dfb4f3d71ec 100644 --- a/apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useSortModeItems.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'; import { OmnichannelSortingDisclaimer, useOmnichannelSortingDisclaimer, -} from '../../../../components/Omnichannel/OmnichannelSortingDisclaimer'; +} from '../../../components/Omnichannel/OmnichannelSortingDisclaimer'; export const useSortModeItems = (): GenericMenuItemProps[] => { const { t } = useTranslation(); diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.spec.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useViewModeItems.spec.tsx similarity index 100% rename from apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.spec.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useViewModeItems.spec.tsx diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useViewModeItems.tsx similarity index 100% rename from apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useViewModeItems.tsx diff --git a/apps/meteor/client/NavBarV2/NavBarPagesGroup/index.ts b/apps/meteor/client/NavBarV2/NavBarPagesGroup/index.ts new file mode 100644 index 0000000000000..b8f036c41fb61 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/index.ts @@ -0,0 +1 @@ +export { default } from './NavBarPagesGroup'; diff --git a/apps/meteor/client/NavBarV2/NavBarPagesSection.tsx b/apps/meteor/client/NavBarV2/NavBarPagesSection.tsx new file mode 100644 index 0000000000000..f3f949753a903 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarPagesSection.tsx @@ -0,0 +1,25 @@ +import { NavBarDivider, NavBarGroup, NavBarSection } from '@rocket.chat/fuselage'; +import { useLayout } from '@rocket.chat/ui-contexts'; + +import NavBarPagesGroup from './NavBarPagesGroup'; +import { SidebarTogglerV2 } from '../components/SidebarTogglerV2'; + +const NavBarPagesSection = () => { + const { isTablet } = useLayout(); + + return ( + + {isTablet && ( + <> + + + + + + )} + + + ); +}; + +export default NavBarPagesSection; diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemAuditMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemAuditMenu.tsx deleted file mode 100644 index 2da6f7529be03..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemAuditMenu.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { NavBarItem } from '@rocket.chat/fuselage'; -import { GenericMenu } from '@rocket.chat/ui-client'; -import { useCurrentRoutePath } from '@rocket.chat/ui-contexts'; -import type { HTMLAttributes } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { useAuditMenu } from './hooks/useAuditMenu'; - -type NavBarItemAuditMenuProps = Omit, 'is'>; - -const NavBarItemAuditMenu = (props: NavBarItemAuditMenuProps) => { - const { t } = useTranslation(); - const sections = useAuditMenu(); - const currentRoute = useCurrentRoutePath(); - - return ( - - ); -}; - -export default NavBarItemAuditMenu; diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/index.ts b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/index.ts deleted file mode 100644 index 2b334cab4b2d2..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { default as NavBarItemAuditMenu } from './NavBarItemAuditMenu'; -export { default as NavBarItemHomePage } from './NavBarItemHomePage'; -export { default as NavBarItemMarketPlaceMenu } from './NavBarItemMarketPlaceMenu'; -export { default as NavBarItemDirectoryPage } from './NavBarItemDirectoryPage'; diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearch.tsx b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearch.tsx new file mode 100644 index 0000000000000..23b7493789b76 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearch.tsx @@ -0,0 +1,97 @@ +import { Box, Icon, TextInput } from '@rocket.chat/fuselage'; +import { useEffectEvent, useMergedRefs } from '@rocket.chat/fuselage-hooks'; +import { useCallback, useEffect, useRef } from 'react'; +import { useFocusManager, useOverlayTrigger } from 'react-aria'; +import { FormProvider, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { useOverlayTriggerState } from 'react-stately'; +import tinykeys from 'tinykeys'; + +import NavBarSearchListBox from './NavBarSearchListbox'; +import { getShortcutLabel } from './getShortcutLabel'; +import { useSearchClick } from './hooks/useSearchClick'; +import { useSearchFocus } from './hooks/useSearchFocus'; +import { useSearchInputNavigation } from './hooks/useSearchNavigation'; + +const NavBarSearch = () => { + const { t } = useTranslation(); + const focusManager = useFocusManager(); + const shortcut = getShortcutLabel(); + + const placeholder = [t('Search_rooms'), shortcut].filter(Boolean).join(' '); + + const methods = useForm({ defaultValues: { filterText: '' } }); + const { + formState: { isDirty }, + register, + resetField, + setFocus, + } = methods; + + const { ref: filterRef, ...rest } = register('filterText'); + + const triggerRef = useRef(null); + const mergedRefs = useMergedRefs(filterRef, triggerRef); + + const state = useOverlayTriggerState({}); + const { triggerProps, overlayProps } = useOverlayTrigger({ type: 'listbox' }, state, triggerRef); + delete triggerProps.onPress; + + const handleKeyDown = useSearchInputNavigation(state); + const handleFocus = useSearchFocus(state); + const handleClick = useSearchClick(state); + + const handleEscSearch = useCallback(() => { + resetField('filterText'); + state.close(); + }, [resetField, state]); + + const handleClearText = useEffectEvent(() => { + resetField('filterText'); + setFocus('filterText'); + }); + + useEffect(() => { + const unsubscribe = tinykeys(window, { + '$mod+K': (event) => { + event.preventDefault(); + setFocus('filterText'); + }, + '$mod+P': (event) => { + event.preventDefault(); + setFocus('filterText'); + }, + 'Escape': (event) => { + event.preventDefault(); + handleEscSearch(); + }, + }); + + return (): void => { + unsubscribe(); + }; + }, [focusManager, handleEscSearch, setFocus]); + + return ( + + + } + /> + {state.isOpen && } + + + ); +}; + +export default NavBarSearch; diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchItem.tsx b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchItem.tsx new file mode 100644 index 0000000000000..7c26ad4febcee --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchItem.tsx @@ -0,0 +1,28 @@ +import { SidebarV2Item, SidebarV2ItemAvatarWrapper, SidebarV2ItemTitle } from '@rocket.chat/fuselage'; +import type { HTMLAttributes, ReactElement, ReactNode } from 'react'; + +type NavBarSearchItemProps = { + title: string; + avatar: ReactElement; + icon: ReactNode; + actions?: ReactElement; + href?: string; + unread?: boolean; + selected?: boolean; + badges?: ReactElement; + clickable?: boolean; +} & Omit, 'is'>; + +const NavBarSearchItem = ({ icon, title, avatar, actions, unread, badges, ...props }: NavBarSearchItemProps) => { + return ( + + {avatar && {avatar}} + {icon && icon} + {title} + {badges && badges} + {actions && actions} + + ); +}; + +export default NavBarSearchItem; diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchItemWithData.tsx b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchItemWithData.tsx new file mode 100644 index 0000000000000..da83a9ca1c129 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchItemWithData.tsx @@ -0,0 +1,59 @@ +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { SidebarV2ItemBadge, SidebarV2ItemIcon } from '@rocket.chat/fuselage'; +import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; +import type { ComponentProps, ReactElement } from 'react'; +import { useTranslation } from 'react-i18next'; + +import NavBarSearchItem from './NavBarSearchItem'; +import { RoomIcon } from '../../components/RoomIcon'; +import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; +import { OmnichannelBadges } from '../../sidebarv2/badges/OmnichannelBadges'; +import { useUnreadDisplay } from '../../sidebarv2/hooks/useUnreadDisplay'; + +type NavBarSearchItemWithDataProps = { + room: SubscriptionWithRoom; + id: string; + AvatarTemplate: ReactElement; +} & Partial>; + +const NavBarSearchItemWithData = ({ room, AvatarTemplate, ...props }: NavBarSearchItemWithDataProps) => { + const { t } = useTranslation(); + + const href = roomCoordinator.getRouteLink(room.t, room) || ''; + const title = roomCoordinator.getRoomName(room.t, room) || ''; + + const { unreadTitle, unreadVariant, showUnread, unreadCount, highlightUnread: highlighted } = useUnreadDisplay(room); + + const icon = } />; + + const badges = ( + <> + {showUnread && ( + + {unreadCount.total} + + )} + {isOmnichannelRoom(room) && } + + ); + + return ( + + ); +}; + +export default NavBarSearchItemWithData; diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchListbox.tsx b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchListbox.tsx new file mode 100644 index 0000000000000..f99f1e2ab54e8 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchListbox.tsx @@ -0,0 +1,75 @@ +import { Box, Tile } from '@rocket.chat/fuselage'; +import { useDebouncedValue, useEffectEvent, useOutsideClick } from '@rocket.chat/fuselage-hooks'; +import { useRef } from 'react'; +import type { OverlayTriggerAria } from 'react-aria'; +import { useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import type { OverlayTriggerState } from 'react-stately'; + +import NavBarSearchNoResults from './NavBarSearchNoResults'; +import NavBarSearchRow from './NavBarSearchRow'; +import { useSearchItems } from './hooks/useSearchItems'; +import { useListboxNavigation } from './hooks/useSearchNavigation'; +import { CustomScrollbars } from '../../components/CustomScrollbars'; + +type NavBarSearchListBoxProps = { + state: OverlayTriggerState; + overlayProps: OverlayTriggerAria['overlayProps']; +}; + +const NavBarSearchListBox = ({ state, overlayProps }: NavBarSearchListBoxProps) => { + const { t } = useTranslation(); + const containerRef = useRef(null); + + const handleKeyDown = useListboxNavigation(state); + useOutsideClick([containerRef], state.close); + + const { resetField, watch } = useFormContext(); + const { filterText } = watch(); + + const debouncedFilter = useDebouncedValue(filterText, 500); + + const handleSelect = useEffectEvent(() => { + state.close(); + resetField('filterText'); + }); + + const { data: items = [], isLoading } = useSearchItems(debouncedFilter); + + return ( + + +
+ {items.length === 0 && !isLoading && } + {items.length > 0 && ( + + {filterText ? t('Results') : t('Recent')} + + )} + {items.map((item) => ( +
+ +
+ ))} +
+
+
+ ); +}; + +export default NavBarSearchListBox; diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchNoResults.tsx b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchNoResults.tsx new file mode 100644 index 0000000000000..ef468da5d166b --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchNoResults.tsx @@ -0,0 +1,10 @@ +import { useTranslation } from 'react-i18next'; + +import GenericNoResults from '../../components/GenericNoResults'; + +const NavBarSearchNoResults = () => { + const { t } = useTranslation(); + return ; +}; + +export default NavBarSearchNoResults; diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchRow.tsx b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchRow.tsx new file mode 100644 index 0000000000000..e60d9c6ae5c77 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchRow.tsx @@ -0,0 +1,24 @@ +import { RoomAvatar } from '@rocket.chat/ui-avatar'; +import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import { memo } from 'react'; + +import NavBarSearchItemWithData from './NavBarSearchItemWithData'; +import NavBarSearchUserRow from './NavBarSearchUserRow'; + +type NavBarSearchRowProps = { + room: SubscriptionWithRoom; + onClick: () => void; +}; + +const NavBarSearchRow = ({ room, onClick }: NavBarSearchRowProps): ReactElement => { + const Avatar = ; + + if (room.t === 'd' && !room.u) { + return ; + } + + return ; +}; + +export default memo(NavBarSearchRow); diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchUserRow.tsx b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchUserRow.tsx new file mode 100644 index 0000000000000..4dab2f976d936 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchUserRow.tsx @@ -0,0 +1,26 @@ +import { SidebarV2ItemIcon } from '@rocket.chat/fuselage'; +import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; +import { useSetting } from '@rocket.chat/ui-contexts'; +import type { ComponentProps, ReactElement } from 'react'; +import { memo } from 'react'; + +import NavBarSearchItem from './NavBarSearchItem'; +import { ReactiveUserStatus } from '../../components/UserStatus'; +import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; + +type NavBarSearchUserRowProps = { + room: SubscriptionWithRoom; + id: string; + AvatarTemplate: ReactElement; +} & Partial>; + +const NavBarSearchUserRow = ({ room, id, AvatarTemplate, ...props }: NavBarSearchUserRowProps) => { + const useRealName = useSetting('UI_Use_Real_Name'); + const title = useRealName ? room.fname || room.name : room.name || room.fname || ''; + const icon = } />; + const href = roomCoordinator.getRouteLink(room.t, { name: room.name }) || ''; + + return ; +}; + +export default memo(NavBarSearchUserRow); diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/getShortcutLabel.ts b/apps/meteor/client/NavBarV2/NavBarSearch/getShortcutLabel.ts new file mode 100644 index 0000000000000..e041f944d25b6 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/getShortcutLabel.ts @@ -0,0 +1,25 @@ +const mobileCheck = function () { + let check = false; + (function (a: string) { + if ( + /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test( + a, + ) || + /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( + a.substr(0, 4), + ) + ) + check = true; + })(navigator.userAgent || navigator.vendor || window.opera || ''); + return check; +}; + +export const getShortcutLabel = (): string => { + if (navigator.userAgentData?.mobile || mobileCheck()) { + return ''; + } + if (window.navigator.platform.toLowerCase().includes('mac')) { + return '(\u2318+K)'; + } + return '(Ctrl+K)'; +}; diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchClick.ts b/apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchClick.ts new file mode 100644 index 0000000000000..887d41bf27294 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchClick.ts @@ -0,0 +1,14 @@ +import { useCallback } from 'react'; +import type { OverlayTriggerState } from 'react-stately'; + +export const useSearchClick = (state: OverlayTriggerState) => { + const handleClick = useCallback(() => { + if (state.isOpen) { + return; + } + + state.setOpen(true); + }, [state]); + + return handleClick; +}; diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchFocus.ts b/apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchFocus.ts new file mode 100644 index 0000000000000..c4d348e252a0c --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchFocus.ts @@ -0,0 +1,20 @@ +import { useLayout } from '@rocket.chat/ui-contexts'; +import { useCallback, useEffect } from 'react'; +import type { OverlayTriggerState } from 'react-stately'; + +export const useSearchFocus = (state: OverlayTriggerState) => { + const { navbar } = useLayout(); + + useEffect(() => { + if (!state.isOpen) { + navbar.collapseSearch?.(); + } + }, [navbar, state.isOpen]); + + const handleFocus = useCallback(() => { + navbar.expandSearch?.(); + state.setOpen(true); + }, [navbar, state]); + + return handleFocus; +}; diff --git a/apps/meteor/client/sidebarv2/header/hooks/useSearchItems.ts b/apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchItems.ts similarity index 100% rename from apps/meteor/client/sidebarv2/header/hooks/useSearchItems.ts rename to apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchItems.ts diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchNavigation.ts b/apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchNavigation.ts new file mode 100644 index 0000000000000..b1ee47279e68a --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchNavigation.ts @@ -0,0 +1,64 @@ +import type { KeyboardEvent } from 'react'; +import { useCallback } from 'react'; +import { useFocusManager } from 'react-aria'; +import type { OverlayTriggerState } from 'react-stately'; + +export const isOption = (node: Element) => node.getAttribute('role') === 'option'; + +export const useListboxNavigation = (state: OverlayTriggerState) => { + const focusManager = useFocusManager(); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.code === 'Tab') { + state.close(); + } + + if (e.code === 'ArrowUp' || e.code === 'ArrowDown') { + e.preventDefault(); + + if (e.code === 'ArrowUp') { + return focusManager?.focusPrevious({ + wrap: true, + accept: (node) => isOption(node), + }); + } + + if (e.code === 'ArrowDown') { + focusManager?.focusNext({ + wrap: true, + accept: (node) => isOption(node), + }); + } + } + }, + [focusManager, state], + ); + + return handleKeyDown; +}; + +export const useSearchInputNavigation = (state: OverlayTriggerState) => { + const focusManager = useFocusManager(); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + state.setOpen(true); + + if ((e.code === 'Tab' && e.shiftKey) || e.key === 'Escape') { + state.close(); + } + + if (e.code === 'ArrowUp' || e.code === 'ArrowDown') { + e.preventDefault(); + + focusManager?.focusNext({ + accept: (node) => isOption(node), + }); + } + }, + [focusManager, state], + ); + + return handleKeyDown; +}; diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/index.ts b/apps/meteor/client/NavBarV2/NavBarSearch/index.ts new file mode 100644 index 0000000000000..4cee7a4b8ea42 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/index.ts @@ -0,0 +1 @@ +export { default } from './NavBarSearch'; diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.spec.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.spec.tsx new file mode 100644 index 0000000000000..b933d520a9630 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.spec.tsx @@ -0,0 +1,69 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import NavBarItemAdministrationMenu from './NavBarItemAdministrationMenu'; + +const handleMenuClick = async () => { + const menuButton = await screen.findByRole('button', { name: 'Manage' }); + await userEvent.click(menuButton); +}; + +it('should not display the menu if no permission is set', async () => { + render(, { wrapper: mockAppRoot().build() }); + + expect(screen.queryByRole('button', { name: 'Manage' })).not.toBeInTheDocument(); +}); + +it('should display the workspace menu item if at least one admin permission is set', async () => { + render(, { wrapper: mockAppRoot().withPermission('access-permissions').build() }); + + await handleMenuClick(); + expect(await screen.findByRole('menuitem', { name: 'Workspace' })).toBeInTheDocument(); +}); + +it('should display the omnichannel menu item if view-livechat-manager permission is set', async () => { + render(, { + wrapper: mockAppRoot().withPermission('view-livechat-manager').withPermission('access-permissions').build(), + }); + + await handleMenuClick(); + expect(await screen.findByRole('menuitem', { name: 'Omnichannel' })).toBeInTheDocument(); +}); + +it('should not display any audit items if has at least one admin permission, some audit permission and the auditing module is not enabled', async () => { + render(, { + wrapper: mockAppRoot().withPermission('access-permissions').withPermission('can-audit').build(), + }); + + await handleMenuClick(); + expect(screen.queryByRole('menuitem', { name: 'Messages' })).not.toBeInTheDocument(); +}); + +it('should display audit items if has at least one admin permission, both audit permission and the auditing module is enabled', async () => { + render(, { + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/licenses.info', () => ({ + license: { + license: { + // @ts-expect-error: just for testing + grantedModules: [{ module: 'auditing' }], + }, + // @ts-expect-error: just for testing + activeModules: ['auditing'], + }, + })) + .withJohnDoe() + .withPermission('can-audit') + .withPermission('can-audit-log') + .build(), + }); + + await handleMenuClick(); + await waitFor(() => { + expect(screen.getByText('Messages')).toBeInTheDocument(); + }); + + expect(await screen.findByRole('menuitem', { name: 'Messages' })).toBeInTheDocument(); + expect(await screen.findByRole('menuitem', { name: 'Logs' })).toBeInTheDocument(); +}); diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.tsx index d4ff95ae9f632..ed451873a741e 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.tsx @@ -5,6 +5,7 @@ import type { HTMLAttributes } from 'react'; import { useTranslation } from 'react-i18next'; import { useAdministrationMenu } from './hooks/useAdministrationMenu'; +import { useAuditMenu } from './hooks/useAuditMenu'; type NavBarItemAdministrationMenuProps = Omit, 'is'>; @@ -12,21 +13,20 @@ const NavBarItemAdministrationMenu = (props: NavBarItemAdministrationMenuProps) const { t } = useTranslation(); const currentRoute = useCurrentRoutePath(); - const sections = useAdministrationMenu(); + const adminSection = useAdministrationMenu(); + const auditSection = useAuditMenu(); - if (!sections[0].items.length) { + const adminRoutesRegex = new RegExp(['/omnichannel/', '/admin', '/audit'].join('|')); + const pressed = adminRoutesRegex.test(currentRoute || ''); + + const sections = [adminSection, auditSection].filter((section) => section.items.length > 0); + + if (sections.length === 0) { return null; } + return ( - + ); }; diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useUserMenu.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useUserMenu.tsx index 437a35c73833e..126f1665c5e7f 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useUserMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useUserMenu.tsx @@ -7,14 +7,12 @@ import { useTranslation } from 'react-i18next'; import UserMenuHeader from '../UserMenuHeader'; import { useAccountItems } from './useAccountItems'; import { useStatusItems } from './useStatusItems'; -import { useVoipItemsSection } from './useVoipItemsSection'; export const useUserMenu = (user: IUser) => { const { t } = useTranslation(); const statusItems = useStatusItems(); const accountItems = useAccountItems(); - const voipSection = useVoipItemsSection(); const logout = useLogout(); const handleLogout = useEffectEvent(() => { @@ -37,7 +35,6 @@ export const useUserMenu = (user: IUser) => { title: t('Status'), items: statusItems, }, - voipSection, { title: t('Account'), items: accountItems, diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.spec.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.spec.tsx index 55f92cde95762..6eb991ca61afb 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.spec.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.spec.tsx @@ -20,7 +20,7 @@ it('should return omnichannel item if has `view-livechat-manager` permission ', }); await waitFor(() => - expect(result.current[0]?.items[0]).toEqual( + expect(result.current.items[0]).toEqual( expect.objectContaining({ id: 'omnichannel', }), @@ -45,7 +45,7 @@ it('should show administration item if has at least one admin permission', async }); await waitFor(() => - expect(result.current[0]?.items[0]).toEqual( + expect(result.current.items[0]).toEqual( expect.objectContaining({ id: 'workspace', }), diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.tsx index 01357109c0fe9..7d4190bd00b8e 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.tsx @@ -48,10 +48,8 @@ export const useAdministrationMenu = () => { onClick: () => router.navigate('/omnichannel/current'), }; - return [ - { - title: t('Manage'), - items: [isAdmin && workspace, isOmnichannel && omnichannel].filter(Boolean) as GenericMenuItemProps[], - }, - ]; + return { + title: t('Manage'), + items: [isAdmin && workspace, isOmnichannel && omnichannel].filter(Boolean) as GenericMenuItemProps[], + }; }; diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.spec.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.spec.tsx similarity index 91% rename from apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.spec.tsx rename to apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.spec.tsx index dbf16bc4dfb24..de197ab30f09f 100644 --- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.spec.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.spec.tsx @@ -18,7 +18,7 @@ it('should return an empty array of items if doesn`t have license', async () => .build(), }); - await waitFor(() => expect(result.current).toEqual([])); + await waitFor(() => expect(result.current.items).toEqual([])); }); it('should return an empty array of items if have license and not have permissions', async () => { @@ -39,7 +39,7 @@ it('should return an empty array of items if have license and not have permissio .build(), }); - await waitFor(() => expect(result.current).toEqual([])); + await waitFor(() => expect(result.current.items).toEqual([])); }); it('should return auditItems if have license and permissions', async () => { @@ -62,14 +62,14 @@ it('should return auditItems if have license and permissions', async () => { }); await waitFor(() => - expect(result.current[0]?.items[0]).toEqual( + expect(result.current.items[0]).toEqual( expect.objectContaining({ id: 'messages', }), ), ); - expect(result.current[0].items[1]).toEqual( + expect(result.current.items[1]).toEqual( expect.objectContaining({ id: 'auditLog', }), @@ -95,7 +95,7 @@ it('should return auditMessages item if have license and can-audit permission', }); await waitFor(() => - expect(result.current[0]?.items[0]).toEqual( + expect(result.current.items[0]).toEqual( expect.objectContaining({ id: 'messages', }), @@ -122,7 +122,7 @@ it('should return audiLogs item if have license and can-audit-log permission', a }); await waitFor(() => - expect(result.current[0]?.items[0]).toEqual( + expect(result.current.items[0]).toEqual( expect.objectContaining({ id: 'auditLog', }), diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.tsx similarity index 74% rename from apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.tsx rename to apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.tsx index 7c0c36dc9f24a..be412480c3007 100644 --- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.tsx @@ -13,27 +13,20 @@ export const useAuditMenu = () => { const hasAuditPermission = usePermission('can-audit') && hasAuditLicense; const hasAuditLogPermission = usePermission('can-audit-log') && hasAuditLicense; - if (!hasAuditPermission && !hasAuditLogPermission) { - return []; - } - const auditMessageItem: GenericMenuItemProps = { id: 'messages', - icon: 'document-eye', content: t('Messages'), onClick: () => router.navigate('/audit'), }; + const auditLogItem: GenericMenuItemProps = { id: 'auditLog', - icon: 'document-eye', content: t('Logs'), onClick: () => router.navigate('/audit-log'), }; - return [ - { - title: t('Audit'), - items: [hasAuditPermission && auditMessageItem, hasAuditLogPermission && auditLogItem].filter(Boolean) as GenericMenuItemProps[], - }, - ]; + return { + title: t('Audit'), + items: [hasAuditPermission && auditMessageItem, hasAuditLogPermission && auditLogItem].filter(Boolean) as GenericMenuItemProps[], + }; }; diff --git a/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarItemVoipDialer.tsx b/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarItemVoipDialer.tsx new file mode 100644 index 0000000000000..d3c857b712a25 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarItemVoipDialer.tsx @@ -0,0 +1,16 @@ +import { NavBarItem } from '@rocket.chat/fuselage'; +import type { HTMLAttributes } from 'react'; + +import { useVoipDialerAction } from './hooks/useVoipDialerAction'; + +type NavBarItemVoipDialerProps = Omit, 'is'> & { + primary?: boolean; +}; + +const NavBarItemVoipDialer = (props: NavBarItemVoipDialerProps) => { + const { title, handleToggleDialer, isPressed, isDisabled } = useVoipDialerAction(); + + return ; +}; + +export default NavBarItemVoipDialer; diff --git a/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarItemVoipToggler.tsx b/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarItemVoipToggler.tsx new file mode 100644 index 0000000000000..d4b508709f40b --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarItemVoipToggler.tsx @@ -0,0 +1,16 @@ +import { NavBarItem } from '@rocket.chat/fuselage'; +import type { HTMLAttributes } from 'react'; + +import { useVoipTogglerAction } from './hooks/useVoipTogglerAction'; + +type NavBarItemVoipDialerProps = Omit, 'is'> & { + primary?: boolean; +}; + +const NavBarItemVoipToggler = (props: NavBarItemVoipDialerProps) => { + const { title, icon, isDisabled, handleToggleVoip } = useVoipTogglerAction(); + + return ; +}; + +export default NavBarItemVoipToggler; diff --git a/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarVoipGroup.tsx b/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarVoipGroup.tsx new file mode 100644 index 0000000000000..7b16053b30566 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarVoipGroup.tsx @@ -0,0 +1,29 @@ +import { NavBarDivider, NavBarGroup } from '@rocket.chat/fuselage'; +import { useVoipState } from '@rocket.chat/ui-voip'; +import { useTranslation } from 'react-i18next'; + +import NavBarItemVoipDialer from './NavBarItemVoipDialer'; +import NavBarItemVoipToggler from './NavBarItemVoipToggler'; +import { useIsCallEnabled } from '../../contexts/CallContext'; + +const NavBarVoipGroup = () => { + const { t } = useTranslation(); + const { isEnabled: showVoip } = useVoipState(); + const isCallEnabled = useIsCallEnabled(); + + if (!showVoip) { + return null; + } + + return ( + <> + + + + + + + ); +}; + +export default NavBarVoipGroup; diff --git a/apps/meteor/client/NavBarV2/NavBarVoipToolbar/NavBarItemVoipDialer.tsx b/apps/meteor/client/NavBarV2/NavBarVoipGroup/hooks/useVoipDialerAction.ts similarity index 57% rename from apps/meteor/client/NavBarV2/NavBarVoipToolbar/NavBarItemVoipDialer.tsx rename to apps/meteor/client/NavBarV2/NavBarVoipGroup/hooks/useVoipDialerAction.ts index ce3f0b8294587..765c6af619d79 100644 --- a/apps/meteor/client/NavBarV2/NavBarVoipToolbar/NavBarItemVoipDialer.tsx +++ b/apps/meteor/client/NavBarV2/NavBarVoipGroup/hooks/useVoipDialerAction.ts @@ -1,19 +1,13 @@ -import { NavBarItem } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useLayout } from '@rocket.chat/ui-contexts'; import { useVoipDialer, useVoipState } from '@rocket.chat/ui-voip'; -import type { HTMLAttributes } from 'react'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -type NavBarItemVoipDialerProps = Omit, 'is'> & { - primary?: boolean; -}; - -const NavBarItemVoipDialer = (props: NavBarItemVoipDialerProps) => { +export const useVoipDialerAction = () => { const { t } = useTranslation(); const { sidebar } = useLayout(); - const { clientError, isEnabled, isReady, isRegistered } = useVoipState(); + const { clientError, isReady, isRegistered } = useVoipState(); const { open: isDialerOpen, openDialer, closeDialer } = useVoipDialer(); const handleToggleDialer = useEffectEvent(() => { @@ -33,16 +27,5 @@ const NavBarItemVoipDialer = (props: NavBarItemVoipDialerProps) => { return t('New_Call'); }, [clientError, isReady, isRegistered, t]); - return isEnabled ? ( - - ) : null; + return { handleToggleDialer, title, isPressed: isDialerOpen, isDisabled: !isReady || !isRegistered }; }; - -export default NavBarItemVoipDialer; diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useVoipItemsSection.tsx b/apps/meteor/client/NavBarV2/NavBarVoipGroup/hooks/useVoipTogglerAction.ts similarity index 56% rename from apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useVoipItemsSection.tsx rename to apps/meteor/client/NavBarV2/NavBarVoipGroup/hooks/useVoipTogglerAction.ts index 9a2584869b13c..b60e13804644e 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useVoipItemsSection.tsx +++ b/apps/meteor/client/NavBarV2/NavBarVoipGroup/hooks/useVoipTogglerAction.ts @@ -1,16 +1,15 @@ -import { Box } from '@rocket.chat/fuselage'; -import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import type { Keys } from '@rocket.chat/icons'; import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useVoipAPI, useVoipState } from '@rocket.chat/ui-voip'; import { useMutation } from '@tanstack/react-query'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -export const useVoipItemsSection = (): { items: GenericMenuItemProps[] } | undefined => { +export const useVoipTogglerAction = () => { const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); - const { clientError, isEnabled, isReady, isRegistered } = useVoipState(); + const { clientError, isReady, isRegistered, isReconnecting } = useVoipState(); const { register, unregister, onRegisteredOnce, onUnregisteredOnce } = useVoipAPI(); const toggleVoip = useMutation({ @@ -35,7 +34,7 @@ export const useVoipItemsSection = (): { items: GenericMenuItemProps[] } | undef }, }); - const tooltip = useMemo(() => { + const title = useMemo(() => { if (clientError) { return t(clientError.message); } @@ -44,30 +43,18 @@ export const useVoipItemsSection = (): { items: GenericMenuItemProps[] } | undef return t('Loading'); } - return ''; - }, [clientError, isReady, toggleVoip.isPending, t]); - - return useMemo(() => { - if (!isEnabled) { - return; + if (isReconnecting) { + return t('Reconnecting'); } - return { - items: [ - { - id: 'toggle-voip', - icon: isRegistered ? 'phone-disabled' : 'phone', - disabled: !isReady || toggleVoip.isPending, - onClick: () => toggleVoip.mutate(), - content: ( - - {isRegistered ? t('Disable_voice_calling') : t('Enable_voice_calling')} - - ), - }, - ], - }; - }, [isEnabled, isRegistered, isReady, tooltip, t, toggleVoip]); -}; + return isRegistered ? t('Disable_voice_calling') : t('Enable_voice_calling'); + }, [clientError, isRegistered, isReconnecting, isReady, toggleVoip.isPending, t]); -export default useVoipItemsSection; + return { + handleToggleVoip: () => toggleVoip.mutate(), + title, + icon: (isRegistered ? 'phone' : 'phone-disabled') as Keys, + isRegistered, + isDisabled: !isReady || toggleVoip.isPending || isReconnecting, + }; +}; diff --git a/apps/meteor/client/NavBarV2/NavBarVoipGroup/index.ts b/apps/meteor/client/NavBarV2/NavBarVoipGroup/index.ts new file mode 100644 index 0000000000000..a9d8cb47bd8a4 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarVoipGroup/index.ts @@ -0,0 +1 @@ +export { default } from './NavBarVoipGroup'; diff --git a/apps/meteor/client/NavBarV2/NavBarVoipToolbar/index.ts b/apps/meteor/client/NavBarV2/NavBarVoipToolbar/index.ts deleted file mode 100644 index 7f6d317af2298..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarVoipToolbar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as NavBarItemVoipDialer } from './NavBarItemVoipDialer'; diff --git a/apps/meteor/client/components/AutoCompleteAgent.tsx b/apps/meteor/client/components/AutoCompleteAgent.tsx index fad5b1a15d575..f20c688ef169f 100644 --- a/apps/meteor/client/components/AutoCompleteAgent.tsx +++ b/apps/meteor/client/components/AutoCompleteAgent.tsx @@ -1,13 +1,13 @@ import { PaginatedSelectFiltered } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import type { ReactElement } from 'react'; +import type { AriaAttributes, ReactElement } from 'react'; import { memo, useMemo, useState } from 'react'; import { useRecordList } from '../hooks/lists/useRecordList'; import { AsyncStatePhase } from '../lib/asyncState'; import { useAgentsList } from './Omnichannel/hooks/useAgentsList'; -type AutoCompleteAgentProps = { +type AutoCompleteAgentProps = Pick & { value: string; error?: string; placeholder?: string; @@ -31,6 +31,7 @@ const AutoCompleteAgent = ({ onlyAvailable = false, withTitle = false, onChange, + 'aria-labelledby': ariaLabelledBy, }: AutoCompleteAgentProps): ReactElement => { const [agentsFilter, setAgentsFilter] = useState(''); @@ -57,6 +58,7 @@ const AutoCompleteAgent = ({ setFilter={setAgentsFilter as (value: string | number | undefined) => void} options={agentsItems} data-qa='autocomplete-agent' + aria-labelledby={ariaLabelledBy} endReached={ agentsPhase === AsyncStatePhase.LOADING ? (): void => undefined : (start): void => loadMoreAgents(start, Math.min(50, agentsTotal)) } diff --git a/apps/meteor/client/components/AutoupdateToastMessage.tsx b/apps/meteor/client/components/AutoupdateToastMessage.tsx index 84e2fe45d66d2..a3cd43d47cbc4 100644 --- a/apps/meteor/client/components/AutoupdateToastMessage.tsx +++ b/apps/meteor/client/components/AutoupdateToastMessage.tsx @@ -2,16 +2,13 @@ import { css } from '@rocket.chat/css-in-js'; import { Box, Button } from '@rocket.chat/fuselage'; import { useTranslation } from 'react-i18next'; -import { useIdleDetection } from '../hooks/useIdleDetection'; +import { useIdleActiveEvents } from '../hooks/useIdleActiveEvents'; export const AutoupdateToastMessage = () => { const { t } = useTranslation(); - useIdleDetection( - () => { - window.location.reload(); - }, - { awayOnWindowBlur: true }, - ); + useIdleActiveEvents({ id: 'autoupdate', awayOnWindowBlur: true }, () => { + window.location.reload(); + }); return ( { const { t } = useTranslation(); + const tagsFieldId = useId(); const { data: tagsResult, isLoading } = useLivechatTags({ department, @@ -66,13 +67,14 @@ const Tags = ({ tags = [], handler, error, tagRequired, department }: TagsProps) return ( <> - + {t('Tags')} {tagsResult?.tags && tagsResult?.tags.length ? ( { handler(tags.map((tag) => tag.label)); @@ -87,6 +89,7 @@ const Tags = ({ tags = [], handler, error, tagRequired, department }: TagsProps) ): void => handleTagValue(currentTarget.value)} flexGrow={1} placeholder={t('Enter_a_tag')} diff --git a/apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts b/apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts index d81f1b1f3e5a7..0c40ddf0d5826 100644 --- a/apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts +++ b/apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts @@ -76,7 +76,7 @@ export const useAgentsList = ( return { items, - itemCount: total + 1, + itemCount: total, }; }, [excludeId, getAgents, haveAll, haveNoAgentsSelectedOption, onlyAvailable, showIdleAgents, t, text], diff --git a/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx b/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx index 09b0e62c8f115..58188d1346508 100644 --- a/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx +++ b/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx @@ -1,5 +1,5 @@ import { Box, IconButton } from '@rocket.chat/fuselage'; -import { useDocumentTitle } from '@rocket.chat/ui-client'; +import { useDocumentTitle, FeaturePreview, FeaturePreviewOn, FeaturePreviewOff } from '@rocket.chat/ui-client'; import { useLayout } from '@rocket.chat/ui-contexts'; import type { ComponentPropsWithoutRef, ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; @@ -33,9 +33,14 @@ const PageHeaderNoShadow = ({ children = undefined, title, onClickBack, ...props color='default' > {isMobile && ( - - - + + + + + + + {null} + )} {onClickBack && } diff --git a/apps/meteor/client/components/SidebarTogglerV2/SidebarToggler.tsx b/apps/meteor/client/components/SidebarTogglerV2/SidebarToggler.tsx new file mode 100644 index 0000000000000..6f4762e858b7a --- /dev/null +++ b/apps/meteor/client/components/SidebarTogglerV2/SidebarToggler.tsx @@ -0,0 +1,25 @@ +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useLayout, useSession } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import { memo } from 'react'; + +import SidebarTogglerButton from './SidebarTogglerButton'; +import { useEmbeddedLayout } from '../../hooks/useEmbeddedLayout'; + +const SideBarToggler = (): ReactElement => { + const { sidebar } = useLayout(); + const isLayoutEmbedded = useEmbeddedLayout(); + const unreadMessagesBadge = useSession('unread') as number | string | undefined; + + const toggleSidebar = useEffectEvent(() => sidebar.toggle()); + + return ( + + ); +}; + +export default memo(SideBarToggler); diff --git a/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerBadge.tsx b/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerBadge.tsx new file mode 100644 index 0000000000000..6de65e701bc89 --- /dev/null +++ b/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerBadge.tsx @@ -0,0 +1,22 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box, Badge } from '@rocket.chat/fuselage'; +import type { ReactNode } from 'react'; + +type SidebarTogglerBadgeProps = { + children?: ReactNode; +}; + +const SidebarTogglerBadge = ({ children }: SidebarTogglerBadgeProps) => ( + + {children} + +); + +export default SidebarTogglerBadge; diff --git a/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerButton.stories.tsx b/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerButton.stories.tsx new file mode 100644 index 0000000000000..4c1aa58ab7650 --- /dev/null +++ b/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerButton.stories.tsx @@ -0,0 +1,24 @@ +import { action } from '@storybook/addon-actions'; +import type { Meta, StoryFn } from '@storybook/react'; + +import SidebarTogglerButton from './SidebarTogglerButton'; + +export default { + title: 'Components/SidebarToggler/SidebarTogglerButtonV2', + component: SidebarTogglerButton, + parameters: { + layout: 'centered', + controls: { hideNoControlsWarning: true }, + actions: { argTypesRegex: '^on.*' }, + }, +} satisfies Meta; + +export const Example: StoryFn = () => ; + +const Template: StoryFn = (args) => ; + +export const Default = Template.bind({}); +export const WithBadge = Template.bind({}); +WithBadge.args = { + badge: 99, +}; diff --git a/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerButton.tsx b/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerButton.tsx new file mode 100644 index 0000000000000..6da63f4786a78 --- /dev/null +++ b/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerButton.tsx @@ -0,0 +1,24 @@ +import { Box, IconButton } from '@rocket.chat/fuselage'; +import type { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; + +import SidebarTogglerBadge from './SidebarTogglerBadge'; + +type SideBarTogglerButtonProps = { + pressed?: boolean; + badge?: ReactNode; + onClick: () => void; +}; + +const SideBarTogglerButton = ({ pressed, badge, onClick }: SideBarTogglerButtonProps) => { + const { t } = useTranslation(); + + return ( + + + {badge && {badge}} + + ); +}; + +export default SideBarTogglerButton; diff --git a/apps/meteor/client/components/SidebarTogglerV2/index.ts b/apps/meteor/client/components/SidebarTogglerV2/index.ts new file mode 100644 index 0000000000000..4698327ba2938 --- /dev/null +++ b/apps/meteor/client/components/SidebarTogglerV2/index.ts @@ -0,0 +1 @@ +export { default as SidebarTogglerV2 } from './SidebarToggler'; diff --git a/apps/meteor/client/components/TextCopy.tsx b/apps/meteor/client/components/TextCopy.tsx index f2cddcd85d7dd..3a6481ec7ce43 100644 --- a/apps/meteor/client/components/TextCopy.tsx +++ b/apps/meteor/client/components/TextCopy.tsx @@ -31,7 +31,7 @@ const TextCopy = ({ text, wrapper = defaultWrapperRenderer, ...props }: TextCopy justifyContent='stretch' alignItems='flex-start' flexGrow={1} - padding={16} + pb={16} backgroundColor='surface' width='full' {...props} diff --git a/apps/meteor/client/components/UserStatus/ReactiveUserStatus.tsx b/apps/meteor/client/components/UserStatus/ReactiveUserStatus.tsx index caff7b8d08e09..51b683b608e4d 100644 --- a/apps/meteor/client/components/UserStatus/ReactiveUserStatus.tsx +++ b/apps/meteor/client/components/UserStatus/ReactiveUserStatus.tsx @@ -1,16 +1,15 @@ import type { IUser } from '@rocket.chat/core-typings'; import { UserStatus } from '@rocket.chat/ui-client'; +import { useUserPresence } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement } from 'react'; import { memo } from 'react'; -import { usePresence } from '../../hooks/usePresence'; - type ReactiveUserStatusProps = { uid: IUser['_id']; } & ComponentProps; const ReactiveUserStatus = ({ uid, ...props }: ReactiveUserStatusProps): ReactElement => { - const status = usePresence(uid)?.status; + const status = useUserPresence(uid)?.status; return ; }; diff --git a/apps/meteor/client/components/dashboards/getClosedPeriod.spec.ts b/apps/meteor/client/components/dashboards/getClosedPeriod.spec.ts index 518f5843fa0d4..ec61665a39abb 100644 --- a/apps/meteor/client/components/dashboards/getClosedPeriod.spec.ts +++ b/apps/meteor/client/components/dashboards/getClosedPeriod.spec.ts @@ -1,101 +1,62 @@ import { getClosedPeriod } from './periods'; -jest.mock('moment', () => { - return () => jest.requireActual('moment')('2024-05-19T12:00:00.000Z'); -}); +jest.useFakeTimers(); +jest.setSystemTime(Date.parse('2024-05-19T12:00:00.000Z')); it('should return the correct period range for this month', () => { - const monthExpectedReturn = { - start: new Date('5/1/2024').toISOString().split('T')[0], - end: new Date('5/19/2024').toISOString().split('T')[0], - }; - const period = getClosedPeriod({ startOf: 'month' })(true); - expect(period.start.toISOString().split('T')[0]).toEqual(monthExpectedReturn.start); - expect(period.end.toISOString().split('T')[0]).toEqual(monthExpectedReturn.end); + expect(period.start).toEqual(new Date('2024-05-01T00:00:00.000Z')); + expect(period.end).toEqual(new Date('2024-05-19T23:59:59.999Z')); }); it('should return the correct period range for this year', () => { - const yearExpectedReturn = { - start: new Date('1/1/2024').toISOString().split('T')[0], - end: new Date('5/19/2024').toISOString().split('T')[0], - }; - const period = getClosedPeriod({ startOf: 'year' })(true); - expect(period.start.toISOString().split('T')[0]).toEqual(yearExpectedReturn.start); - expect(period.end.toISOString().split('T')[0]).toEqual(yearExpectedReturn.end); + expect(period.start).toEqual(new Date('2024-01-01T00:00:00.000Z')); + expect(period.end).toEqual(new Date('2024-05-19T23:59:59.999Z')); }); it('should return the correct period range for last 6 months', () => { - const last6MonthsExpectedReturn = { - start: new Date('11/1/2023').toISOString().split('T')[0], - end: new Date('5/19/2024').toISOString().split('T')[0], - }; - const period = getClosedPeriod({ startOf: 'month', subtract: { amount: 6, unit: 'months' } })(true); - expect(period.start.toISOString().split('T')[0]).toEqual(last6MonthsExpectedReturn.start); - expect(period.end.toISOString().split('T')[0]).toEqual(last6MonthsExpectedReturn.end); + expect(period.start).toEqual(new Date('2023-11-01T00:00:00.000Z')); + expect(period.end).toEqual(new Date('2024-05-19T23:59:59.999Z')); }); it('should return the correct period range for this week', () => { - const weekExpectedReturn = { - start: new Date('5/19/2024').toISOString().split('T')[0], - end: new Date('5/19/2024').toISOString().split('T')[0], - }; - const period = getClosedPeriod({ startOf: 'week' })(true); - expect(period.start.toISOString().split('T')[0]).toEqual(weekExpectedReturn.start); - expect(period.end.toISOString().split('T')[0]).toEqual(weekExpectedReturn.end); + expect(period.start).toEqual(new Date('2024-05-19T00:00:00.000Z')); + expect(period.end).toEqual(new Date('2024-05-19T23:59:59.999Z')); }); -it('should return the correct period range for this month using local time', () => { - const monthExpectedReturn = { - start: new Date('5/1/2024').toISOString().split('T')[0], - end: new Date('5/19/2024').toISOString().split('T')[0], - }; - - const period = getClosedPeriod({ startOf: 'month' })(false); +describe('using local time', () => { + it('should return the correct period range for this month', () => { + const period = getClosedPeriod({ startOf: 'month' })(false); - expect(period.start.toISOString().split('T')[0]).toEqual(monthExpectedReturn.start); - expect(period.end.toISOString().split('T')[0]).toEqual(monthExpectedReturn.end); -}); + expect(period.start).toEqual(new Date('2024-05-01T00:00:00.000')); + expect(period.end).toEqual(new Date('2024-05-19T23:59:59.999')); + }); -it('should return the correct period range for this year using local time', () => { - const yearExpectedReturn = { - start: new Date('1/1/2024').toISOString().split('T')[0], - end: new Date('5/19/2024').toISOString().split('T')[0], - }; + it('should return the correct period range for this year', () => { + const period = getClosedPeriod({ startOf: 'year' })(false); - const period = getClosedPeriod({ startOf: 'year' })(false); + expect(period.start).toEqual(new Date('2024-01-01T00:00:00.000')); + expect(period.end).toEqual(new Date('2024-05-19T23:59:59.999')); + }); - expect(period.start.toISOString().split('T')[0]).toEqual(yearExpectedReturn.start); - expect(period.end.toISOString().split('T')[0]).toEqual(yearExpectedReturn.end); -}); - -it('should return the correct period range for last 6 months using local time', () => { - const last6MonthsExpectedReturn = { - start: new Date('11/1/2023').toISOString().split('T')[0], - end: new Date('5/19/2024').toISOString().split('T')[0], - }; - - const period = getClosedPeriod({ startOf: 'month', subtract: { amount: 6, unit: 'months' } })(true); - - expect(period.start.toISOString().split('T')[0]).toEqual(last6MonthsExpectedReturn.start); - expect(period.end.toISOString().split('T')[0]).toEqual(last6MonthsExpectedReturn.end); -}); + it('should return the correct period range for last 6 months', () => { + const period = getClosedPeriod({ startOf: 'month', subtract: { amount: 6, unit: 'months' } })(false); -it('should return the correct period range for this week using local time', () => { - const weekExpectedReturn = { - start: new Date('5/19/2024').toISOString().split('T')[0], - end: new Date('5/19/2024').toISOString().split('T')[0], - }; + expect(period.start).toEqual(new Date('2023-11-01T00:00:00.000')); + expect(period.end).toEqual(new Date('2024-05-19T23:59:59.999')); + }); - const period = getClosedPeriod({ startOf: 'week' })(false); + it('should return the correct period range for this week', () => { + const period = getClosedPeriod({ startOf: 'week' })(false); - expect(period.start.toISOString().split('T')[0]).toEqual(weekExpectedReturn.start); - expect(period.end.toISOString().split('T')[0]).toEqual(weekExpectedReturn.end); + expect(period.start).toEqual(new Date('2024-05-19T00:00:00.000')); + expect(period.end).toEqual(new Date('2024-05-19T23:59:59.999')); + }); }); diff --git a/apps/meteor/client/components/dashboards/periods.ts b/apps/meteor/client/components/dashboards/periods.ts index 5ae1626e976cf..637a2e0ac0b9f 100644 --- a/apps/meteor/client/components/dashboards/periods.ts +++ b/apps/meteor/client/components/dashboards/periods.ts @@ -16,27 +16,17 @@ export const getClosedPeriod = end: Date; }) => (utc): { start: Date; end: Date } => { - const date = new Date(); - const offsetForMoment = -(date.getTimezoneOffset() / 60); - let start = moment(date).utc(); - let end = moment(date).utc(); + const start = utc ? moment().utc() : moment(); + const end = utc ? moment().utc() : moment(); if (subtract) { const { amount, unit } = subtract; start.subtract(amount, unit); } - if (!utc) { - start = start.utcOffset(offsetForMoment); - end = end.utcOffset(offsetForMoment); - } - - // moment.toDate() can only return the date in localtime, that's why we do the new Date conversion - // https://github.com/moment/moment-timezone/issues/644 - return { - start: new Date(start.startOf(startOf).format('YYYY-MM-DD HH:mm:ss')), - end: new Date(end.endOf('day').format('YYYY-MM-DD HH:mm:ss')), + start: start.startOf(startOf).toDate(), + end: end.endOf('day').toDate(), }; }; diff --git a/apps/meteor/client/components/message/MessageHeader.tsx b/apps/meteor/client/components/message/MessageHeader.tsx index 8a259a8817865..61ba13171d7d2 100644 --- a/apps/meteor/client/components/message/MessageHeader.tsx +++ b/apps/meteor/client/components/message/MessageHeader.tsx @@ -8,6 +8,7 @@ import { MessageNameContainer, } from '@rocket.chat/fuselage'; import { useUserDisplayName } from '@rocket.chat/ui-client'; +import { useUserPresence } from '@rocket.chat/ui-contexts'; import type { KeyboardEvent, ReactElement } from 'react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -17,8 +18,6 @@ import MessageRoles from './header/MessageRoles'; import { useMessageListShowUsername, useMessageListShowRealName, useMessageListShowRoles } from './list/MessageListContext'; import { useFormatDateAndTime } from '../../hooks/useFormatDateAndTime'; import { useFormatTime } from '../../hooks/useFormatTime'; -import { useUserData } from '../../hooks/useUserData'; -import type { UserPresence } from '../../lib/presence'; import { useMessageRoles } from './header/hooks/useMessageRoles'; import { useUserCard } from '../../views/room/contexts/UserCardContext'; @@ -34,7 +33,7 @@ const MessageHeader = ({ message }: MessageHeaderProps): ReactElement => { const { triggerProps, openUserCard } = useUserCard(); const showRealName = useMessageListShowRealName(); - const user: UserPresence = { ...message.u, roles: [], ...useUserData(message.u._id) }; + const user = { ...message.u, roles: [], ...useUserPresence(message.u._id) }; const usernameAndRealNameAreSame = !user.name || user.username === user.name; const showUsername = useMessageListShowUsername() && showRealName && !usernameAndRealNameAreSame; const displayName = useUserDisplayName(user); diff --git a/apps/meteor/client/components/message/header/hooks/useMessageRoles.ts b/apps/meteor/client/components/message/header/hooks/useMessageRoles.ts index f39a866d9e1e9..d73fb5c07a3c4 100644 --- a/apps/meteor/client/components/message/header/hooks/useMessageRoles.ts +++ b/apps/meteor/client/components/message/header/hooks/useMessageRoles.ts @@ -1,23 +1,31 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; import { useCallback } from 'react'; -import { RoomRoles, UserRoles, Roles } from '../../../../../app/models/client'; +import { Roles } from '../../../../../app/models/client'; import { useReactiveValue } from '../../../../hooks/useReactiveValue'; +import type { RoomRoles } from '../../../../hooks/useRoomRolesQuery'; +import { useRoomRolesQuery } from '../../../../hooks/useRoomRolesQuery'; +import type { UserRoles } from '../../../../hooks/useUserRolesQuery'; +import { useUserRolesQuery } from '../../../../hooks/useUserRolesQuery'; -export const useMessageRoles = (userId: IUser['_id'] | undefined, roomId: IRoom['_id'], shouldLoadRoles: boolean): Array => - useReactiveValue( +export const useMessageRoles = (userId: IUser['_id'] | undefined, roomId: IRoom['_id'], shouldLoadRoles: boolean): Array => { + const { data: userRoles } = useUserRolesQuery({ + select: useCallback((records: UserRoles[]) => records.find((record) => record.uid === userId)?.roles ?? [], [userId]), + enabled: shouldLoadRoles && !!userId, + }); + + const { data: roomRoles } = useRoomRolesQuery(roomId, { + select: useCallback((records: RoomRoles[]) => records.find((record) => record.u._id === userId)?.roles ?? [], [userId]), + enabled: shouldLoadRoles && !!userId, + }); + + return useReactiveValue( useCallback(() => { if (!shouldLoadRoles || !userId) { return []; } - const userRoles = UserRoles.findOne(userId); - const roomRoles = RoomRoles.findOne({ - 'u._id': userId, - 'rid': roomId, - }); - - const roles = [...(userRoles?.roles || []), ...(roomRoles?.roles || [])]; + const roles = [...(userRoles ?? []), ...(roomRoles ?? [])]; const result = Roles.find( { @@ -36,5 +44,6 @@ export const useMessageRoles = (userId: IUser['_id'] | undefined, roomId: IRoom[ }, ).fetch(); return result.map(({ description }) => description); - }, [userId, roomId, shouldLoadRoles]), + }, [userId, shouldLoadRoles, userRoles, roomRoles]), ); +}; diff --git a/apps/meteor/client/components/message/hooks/useMarkAsUnreadMutation.tsx b/apps/meteor/client/components/message/hooks/useMarkAsUnreadMutation.tsx index cd52c5e34dd85..a8d2c0e4f9ba6 100644 --- a/apps/meteor/client/components/message/hooks/useMarkAsUnreadMutation.tsx +++ b/apps/meteor/client/components/message/hooks/useMarkAsUnreadMutation.tsx @@ -1,17 +1,30 @@ import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; -import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; import { LegacyRoomManager } from '../../../../app/ui-utils/client'; -import { sdk } from '../../../../app/utils/client/lib/SDKClient'; export const useMarkAsUnreadMutation = () => { const dispatchToastMessage = useToastMessageDispatch(); + const unreadMessages = useEndpoint('POST', '/v1/subscriptions.unread'); return useMutation({ - mutationFn: async ({ message, subscription }: { message: IMessage; subscription: ISubscription }) => { + mutationFn: async ({ + subscription, + ...props + }: + | { message: IMessage; subscription: ISubscription } + | { + roomId: string; + subscription: ISubscription; + }) => { await LegacyRoomManager.close(subscription.t + subscription.name); - await sdk.call('unreadMessages', message); + if ('message' in props) { + const { message } = props; + await unreadMessages({ firstUnreadMessage: { _id: message._id } }); + return; + } + await unreadMessages({ roomId: props.roomId }); }, onError: (error) => { dispatchToastMessage({ type: 'error', message: error }); diff --git a/apps/meteor/client/components/message/list/MessageListContext.tsx b/apps/meteor/client/components/message/list/MessageListContext.tsx index 64899357b2c5b..c175dac1baf8b 100644 --- a/apps/meteor/client/components/message/list/MessageListContext.tsx +++ b/apps/meteor/client/components/message/list/MessageListContext.tsx @@ -1,5 +1,5 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import type { KeyboardEvent, MouseEvent, RefObject } from 'react'; +import type { KeyboardEvent, MouseEvent, RefCallback } from 'react'; import { createContext, useContext } from 'react'; export type MessageListContextValue = { @@ -25,7 +25,7 @@ export type MessageListContextValue = { showColors: boolean; jumpToMessageParam?: string; username: string | undefined; - messageListRef?: RefObject; + messageListRef?: RefCallback; }; export const MessageListContext = createContext({ @@ -43,7 +43,7 @@ export const MessageListContext = createContext({ showUsername: false, showColors: false, username: undefined, - messageListRef: { current: null }, + messageListRef: undefined, }); export const useShowTranslated: MessageListContextValue['useShowTranslated'] = (...args) => diff --git a/apps/meteor/client/components/message/variants/SystemMessage.tsx b/apps/meteor/client/components/message/variants/SystemMessage.tsx index 34e8b48ee2756..0d3de880908ea 100644 --- a/apps/meteor/client/components/message/variants/SystemMessage.tsx +++ b/apps/meteor/client/components/message/variants/SystemMessage.tsx @@ -14,6 +14,7 @@ import { import { UserAvatar } from '@rocket.chat/ui-avatar'; import { useUserDisplayName } from '@rocket.chat/ui-client'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import { useUserPresence } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement, KeyboardEvent } from 'react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -21,8 +22,6 @@ import { useTranslation } from 'react-i18next'; import { MessageTypes } from '../../../../app/ui-utils/client'; import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime'; import { useFormatTime } from '../../../hooks/useFormatTime'; -import { useUserData } from '../../../hooks/useUserData'; -import type { UserPresence } from '../../../lib/presence'; import { useIsSelecting, useToggleSelect, @@ -46,7 +45,7 @@ const SystemMessage = ({ message, showUserAvatar, ...props }: SystemMessageProps const { triggerProps, openUserCard } = useUserCard(); const showRealName = useMessageListShowRealName(); - const user: UserPresence = { ...message.u, roles: [], ...useUserData(message.u._id) }; + const user = { ...message.u, roles: [], ...useUserPresence(message.u._id) }; const usernameAndRealNameAreSame = !user.name || user.username === user.name; const showUsername = useMessageListShowUsername() && showRealName && !usernameAndRealNameAreSame; const displayName = useUserDisplayName(user); diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 428c212a8935d..eaf54b7260222 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -2,12 +2,10 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { isDiscussionMessage, isThreadMainMessage, isE2EEMessage, isQuoteAttachment } from '@rocket.chat/core-typings'; import { MessageBody } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useSetting, useTranslation, useUserId } from '@rocket.chat/ui-contexts'; +import { useSetting, useTranslation, useUserId, useUserPresence } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import { memo } from 'react'; -import { useUserData } from '../../../../hooks/useUserData'; -import type { UserPresence } from '../../../../lib/presence'; import { useChat } from '../../../../views/room/contexts/ChatContext'; import MessageContentBody from '../../MessageContentBody'; import ReadReceiptIndicator from '../../ReadReceiptIndicator'; @@ -38,7 +36,7 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM const subscription = useSubscriptionFromMessageQuery(message).data ?? undefined; const broadcast = subscription?.broadcast ?? false; const uid = useUserId(); - const messageUser: UserPresence = { ...message.u, roles: [], ...useUserData(message.u._id) }; + const messageUser = { ...message.u, roles: [], ...useUserPresence(message.u._id) }; const readReceiptEnabled = useSetting('Message_Read_Receipt_Enabled', false); const chat = useChat(); const t = useTranslation(); diff --git a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx index 643c85e0c518c..a35d70075d67b 100644 --- a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx @@ -2,13 +2,11 @@ import type { IThreadMainMessage, IThreadMessage } from '@rocket.chat/core-typin import { isE2EEMessage, isQuoteAttachment } from '@rocket.chat/core-typings'; import { MessageBody } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useSetting, useUserId } from '@rocket.chat/ui-contexts'; +import { useSetting, useUserId, useUserPresence } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useUserData } from '../../../../hooks/useUserData'; -import type { UserPresence } from '../../../../lib/presence'; import MessageContentBody from '../../MessageContentBody'; import ReadReceiptIndicator from '../../ReadReceiptIndicator'; import Attachments from '../../content/Attachments'; @@ -32,7 +30,7 @@ const ThreadMessageContent = ({ message }: ThreadMessageContentProps): ReactElem const subscription = useSubscriptionFromMessageQuery(message).data ?? undefined; const broadcast = subscription?.broadcast ?? false; const uid = useUserId(); - const messageUser: UserPresence = { ...message.u, roles: [], ...useUserData(message.u._id) }; + const messageUser = { ...message.u, roles: [], ...useUserPresence(message.u._id) }; const readReceiptEnabled = useSetting('Message_Read_Receipt_Enabled', false); const { t } = useTranslation(); diff --git a/apps/meteor/client/contexts/UserPresenceContext.ts b/apps/meteor/client/contexts/UserPresenceContext.ts deleted file mode 100644 index 4cb45350b0d8b..0000000000000 --- a/apps/meteor/client/contexts/UserPresenceContext.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createContext } from 'react'; - -import type { Subscribable } from '../definitions/Subscribable'; -import type { UserPresence } from '../lib/presence'; - -type UserPresenceContextValue = { - queryUserData: (uid: string) => Subscribable; -}; - -export const UserPresenceContext = createContext(undefined); diff --git a/apps/meteor/client/definitions/global.d.ts b/apps/meteor/client/definitions/global.d.ts index 0916ef237119a..58e383ee58d8b 100644 --- a/apps/meteor/client/definitions/global.d.ts +++ b/apps/meteor/client/definitions/global.d.ts @@ -80,4 +80,8 @@ declare global { maxHeight: number; }; } + + interface NotificationEventMap { + reply: { response: string }; + } } diff --git a/apps/meteor/client/hooks/customEmoji/useCustomEmoji.ts b/apps/meteor/client/hooks/customEmoji/useCustomEmoji.ts new file mode 100644 index 0000000000000..8b8400145d97f --- /dev/null +++ b/apps/meteor/client/hooks/customEmoji/useCustomEmoji.ts @@ -0,0 +1,55 @@ +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; +import { useEffect } from 'react'; + +import { emoji } from '../../../app/emoji/client'; +import { customRender } from '../../lib/customEmoji'; + +export const useCustomEmoji = () => { + const getCustomEmojis = useEndpoint('GET', '/v1/emoji-custom.list'); + const result = useQuery({ + queryKey: ['emoji-custom.list'], + queryFn: () => getCustomEmojis({ query: '' }), + }); + + useEffect(() => { + emoji.packages.emojiCustom = { + emojiCategories: [{ key: 'rocket', i18n: 'Custom' }], + categoryIndex: 1, + toneList: {}, + list: [], + _regexpSignature: null, + _regexp: null, + emojisByCategory: { rocket: [] }, + render: customRender, + renderPicker: customRender, + }; + + if (result.isError) { + console.error('Error getting custom emoji ', result.error); + } + + if (result.isSuccess) { + const { + emojis: { update: customEmojis }, + } = result.data; + + const addCustomEmojis = () => { + for (const currentEmoji of customEmojis) { + emoji.packages.emojiCustom.emojisByCategory.rocket.push(currentEmoji.name); + emoji.packages.emojiCustom.list?.push(`:${currentEmoji.name}:`); + emoji.list[`:${currentEmoji.name}:`] = { ...currentEmoji, emojiPackage: 'emojiCustom' } as any; + for (const alias of currentEmoji.aliases) { + emoji.packages.emojiCustom.list?.push(`:${alias}:`); + emoji.list[`:${alias}:`] = { + emojiPackage: 'emojiCustom', + aliasOf: currentEmoji.name, + }; + } + } + emoji.dispatchUpdate(); + }; + addCustomEmojis(); + } + }, [result.data, result.error, result.isError, result.isSuccess]); +}; diff --git a/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts b/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts index a28c4424d313c..2134499ce4eee 100644 --- a/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts +++ b/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts @@ -1,10 +1,10 @@ import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import type { FieldExpression, Query } from '@rocket.chat/mongo-adapter'; +import { createFilterFromQuery } from '@rocket.chat/mongo-adapter'; import { useStream } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; import type { MessageList } from '../../lib/lists/MessageList'; -import type { FieldExpression, Query } from '../../lib/minimongo'; -import { createFilterFromQuery } from '../../lib/minimongo'; type NotifyRoomRidDeleteMessageBulkEvent = { rid: IMessage['rid']; diff --git a/apps/meteor/client/hooks/menuActions/useLeaveRoom.tsx b/apps/meteor/client/hooks/menuActions/useLeaveRoom.tsx index b5c90c8f42dac..2700bb676a1a4 100644 --- a/apps/meteor/client/hooks/menuActions/useLeaveRoom.tsx +++ b/apps/meteor/client/hooks/menuActions/useLeaveRoom.tsx @@ -52,7 +52,7 @@ export const useLeaveRoomAction = ({ rid, type, name, roomOpen }: LeaveRoomProps setModal( setModal(null)} cancelText={t('Cancel')} diff --git a/apps/meteor/client/hooks/menuActions/useToggleReadAction.ts b/apps/meteor/client/hooks/menuActions/useToggleReadAction.ts index 133acd9b0f789..d2bdfec8fe305 100644 --- a/apps/meteor/client/hooks/menuActions/useToggleReadAction.ts +++ b/apps/meteor/client/hooks/menuActions/useToggleReadAction.ts @@ -1,9 +1,10 @@ import type { ISubscription } from '@rocket.chat/core-typings'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useEndpoint, useMethod, useRouter, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useRouter, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useQueryClient } from '@tanstack/react-query'; import { LegacyRoomManager } from '../../../app/ui-utils/client'; +import { useMarkAsUnreadMutation } from '../../components/message/hooks/useMarkAsUnreadMutation'; type ToggleReadActionProps = { rid: string; @@ -17,7 +18,8 @@ export const useToggleReadAction = ({ rid, isUnread, subscription }: ToggleReadA const router = useRouter(); const readMessages = useEndpoint('POST', '/v1/subscriptions.read'); - const unreadMessages = useMethod('unreadMessages'); + + const unreadMessages = useMarkAsUnreadMutation(); const handleToggleRead = useEffectEvent(async () => { try { @@ -38,7 +40,7 @@ export const useToggleReadAction = ({ rid, isUnread, subscription }: ToggleReadA router.navigate('/home'); - await unreadMessages(undefined, rid); + await unreadMessages.mutateAsync({ roomId: rid, subscription }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } diff --git a/apps/meteor/client/hooks/notification/useDesktopNotification.ts b/apps/meteor/client/hooks/notification/useDesktopNotification.ts new file mode 100644 index 0000000000000..c58cccd59f878 --- /dev/null +++ b/apps/meteor/client/hooks/notification/useDesktopNotification.ts @@ -0,0 +1,39 @@ +import type { INotificationDesktop } from '@rocket.chat/core-typings'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useUser } from '@rocket.chat/ui-contexts'; + +import { useNotification } from './useNotification'; +import { e2e } from '../../../app/e2e/client'; +import { RoomManager } from '../../lib/RoomManager'; +import { getAvatarAsPng } from '../../lib/utils/getAvatarAsPng'; + +export const useDesktopNotification = () => { + const user = useUser(); + const notify = useNotification(); + + const notifyDesktop = useEffectEvent(async (notification: INotificationDesktop) => { + if ( + notification.payload.rid === RoomManager.opened && + (typeof window.document.hasFocus === 'function' ? window.document.hasFocus() : undefined) + ) { + return; + } + if (user?.status === 'busy') { + return; + } + + if (notification.payload.message?.t === 'e2e') { + const e2eRoom = await e2e.getInstanceByRoomId(notification.payload.rid); + if (e2eRoom) { + notification.text = (await e2eRoom.decrypt(notification.payload.message.msg)).text; + } + } + + return getAvatarAsPng(notification.payload.sender?.username, (avatarAsPng) => { + notification.icon = avatarAsPng; + return notify(notification); + }); + }); + + return notifyDesktop; +}; diff --git a/apps/meteor/client/hooks/notification/useNewMessageNotification.ts b/apps/meteor/client/hooks/notification/useNewMessageNotification.ts new file mode 100644 index 0000000000000..36b94568a27a5 --- /dev/null +++ b/apps/meteor/client/hooks/notification/useNewMessageNotification.ts @@ -0,0 +1,24 @@ +import type { AtLeast, ISubscription } from '@rocket.chat/core-typings'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useCustomSound } from '@rocket.chat/ui-contexts'; + +export const useNewMessageNotification = () => { + const { notificationSounds } = useCustomSound(); + + const notifyNewMessage = useEffectEvent((sub: AtLeast) => { + if (!sub || sub.audioNotificationValue === 'none') { + return; + } + // TODO: Fix this - Room Notifications Preferences > sound > desktop is not working. + // plays the user notificationSound preference + + // if (sub.audioNotificationValue && sub.audioNotificationValue !== '0') { + // void CustomSounds.play(sub.audioNotificationValue, { + // volume: Number((notificationsSoundVolume / 100).toPrecision(2)), + // }); + // } + + notificationSounds.playNewMessage(); + }); + return notifyNewMessage; +}; diff --git a/apps/meteor/client/hooks/notification/useNotification.ts b/apps/meteor/client/hooks/notification/useNotification.ts new file mode 100644 index 0000000000000..6caa2b2ecc966 --- /dev/null +++ b/apps/meteor/client/hooks/notification/useNotification.ts @@ -0,0 +1,121 @@ +import type { INotificationDesktop } from '@rocket.chat/core-typings'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { Random } from '@rocket.chat/random'; +import { useRouter, useUserPreference } from '@rocket.chat/ui-contexts'; + +import { useNotificationAllowed } from './useNotificationAllowed'; +import { getUserAvatarURL } from '../../../app/utils/client'; +import { sdk } from '../../../app/utils/client/lib/SDKClient'; +import { stripTags } from '../../../lib/utils/stringUtils'; +import { onClientMessageReceived } from '../../lib/onClientMessageReceived'; + +export const useNotification = () => { + const requireInteraction = useUserPreference('desktopNotificationRequireInteraction'); + const router = useRouter(); + const notificationAllowed = useNotificationAllowed(); + + const notify = useEffectEvent(async (notification: INotificationDesktop) => { + if (!notificationAllowed) { + return; + } + if (!notification.payload) { + return; + } + + const { rid } = notification.payload; + if (!rid) { + return; + } + const message = await onClientMessageReceived({ + rid, + msg: notification.text, + notification: true, + } as any); + + const n = new Notification(notification.title, { + icon: notification.icon || getUserAvatarURL(notification.payload.sender?.username as string), + body: stripTags(message?.msg), + tag: notification.payload._id, + canReply: true, + silent: true, + requireInteraction, + } as NotificationOptions & { + canReply?: boolean; + }); + const notificationDuration = !requireInteraction ? (notification.duration ?? 0) - 0 || 10 : -1; + if (notificationDuration > 0) { + setTimeout(() => n.close(), notificationDuration * 1000); + } + + if (n.addEventListener) { + n.addEventListener( + 'reply', + ({ response }) => + void sdk.call('sendMessage', { + _id: Random.id(), + rid, + msg: response, + }), + ); + } + + n.onclick = () => { + n.close(); + window.focus(); + + if (!notification.payload._id || !notification.payload.rid || !notification.payload.name) { + return; + } + + switch (notification.payload?.type) { + case 'd': + router.navigate({ + pattern: '/direct/:rid/:tab?/:context?', + params: { + rid: notification.payload.rid, + ...(notification.payload.tmid && { + tab: 'thread', + context: notification.payload.tmid, + }), + }, + search: { ...router.getSearchParameters(), jump: notification.payload._id }, + }); + break; + case 'c': + return router.navigate({ + pattern: '/channel/:name/:tab?/:context?', + params: { + name: notification.payload.name, + ...(notification.payload.tmid && { + tab: 'thread', + context: notification.payload.tmid, + }), + }, + search: { ...router.getSearchParameters(), jump: notification.payload._id }, + }); + case 'p': + return router.navigate({ + pattern: '/group/:name/:tab?/:context?', + params: { + name: notification.payload.name, + ...(notification.payload.tmid && { + tab: 'thread', + context: notification.payload.tmid, + }), + }, + search: { ...router.getSearchParameters(), jump: notification.payload._id }, + }); + case 'l': + return router.navigate({ + pattern: '/live/:id/:tab?/:context?', + params: { + id: notification.payload.rid, + tab: 'room-info', + }, + search: { ...router.getSearchParameters(), jump: notification.payload._id }, + }); + } + }; + }); + return notify; +}; diff --git a/apps/meteor/client/hooks/notification/useNotificationAllowed.ts b/apps/meteor/client/hooks/notification/useNotificationAllowed.ts new file mode 100644 index 0000000000000..31f17b0cb0e1d --- /dev/null +++ b/apps/meteor/client/hooks/notification/useNotificationAllowed.ts @@ -0,0 +1,19 @@ +import { useCallback, useSyncExternalStore } from 'react'; + +import { notificationManager } from '../../lib/notificationManager'; + +export const useNotificationAllowed = (): boolean => { + const allowed = useSyncExternalStore( + useCallback( + (callback): (() => void) => + notificationManager.on('change', () => { + notificationManager.allowed = Notification.permission === 'granted'; + callback(); + }), + [], + ), + (): boolean => notificationManager.allowed, + ); + + return allowed; +}; diff --git a/apps/meteor/client/hooks/notification/useNotificationPermission.ts b/apps/meteor/client/hooks/notification/useNotificationPermission.ts new file mode 100644 index 0000000000000..abd6c95be9f71 --- /dev/null +++ b/apps/meteor/client/hooks/notification/useNotificationPermission.ts @@ -0,0 +1,21 @@ +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; + +import { notificationManager } from '../../lib/notificationManager'; + +export const useNotificationPermission = () => { + const requestPermission = useEffectEvent(async () => { + const response = await Notification.requestPermission(); + notificationManager.allowed = response === 'granted'; + notificationManager.emit('change'); + + const notifications = await navigator.permissions.query({ name: 'notifications' }); + notifications.onchange = () => { + notificationManager.allowed = notifications.state === 'granted'; + notificationManager.emit('change'); + }; + }); + + if ('Notification' in window) { + requestPermission(); + } +}; diff --git a/apps/meteor/client/hooks/notification/useNotificationUserCalendar.ts b/apps/meteor/client/hooks/notification/useNotificationUserCalendar.ts new file mode 100644 index 0000000000000..47af5ae6e06eb --- /dev/null +++ b/apps/meteor/client/hooks/notification/useNotificationUserCalendar.ts @@ -0,0 +1,43 @@ +import type { ICalendarNotification, IUser } from '@rocket.chat/core-typings'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useSetting, useStream, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +import { imperativeModal } from '../../lib/imperativeModal'; +import OutlookCalendarEventModal from '../../views/outlookCalendar/OutlookCalendarEventModal'; + +export const useNotificationUserCalendar = (user: IUser) => { + const requireInteraction = useUserPreference('desktopNotificationRequireInteraction'); + const outLookEnabled = useSetting('Outlook_Calendar_Enabled'); + const notifyUserStream = useStream('notify-user'); + + const notifyUserCalendar = useEffectEvent(async (notification: ICalendarNotification) => { + if (user.status === 'busy') { + return; + } + + const n = new Notification(notification.title, { + body: notification.text, + tag: notification.payload._id, + silent: true, + requireInteraction, + } as NotificationOptions); + + n.onclick = function () { + this.close(); + window.focus(); + imperativeModal.open({ + component: OutlookCalendarEventModal, + props: { id: notification.payload._id, onClose: imperativeModal.close, onCancel: imperativeModal.close }, + }); + }; + }); + + useEffect(() => { + if (!user?._id || !outLookEnabled) { + return; + } + + return notifyUserStream(`${user._id}/calendar`, notifyUserCalendar); + }, [notifyUserCalendar, notifyUserStream, outLookEnabled, user?._id]); +}; diff --git a/apps/meteor/client/hooks/useNotifyUser.ts b/apps/meteor/client/hooks/notification/useNotifyUser.ts similarity index 58% rename from apps/meteor/client/hooks/useNotifyUser.ts rename to apps/meteor/client/hooks/notification/useNotifyUser.ts index 440c979fed111..3ac30732dc8a4 100644 --- a/apps/meteor/client/hooks/useNotifyUser.ts +++ b/apps/meteor/client/hooks/notification/useNotifyUser.ts @@ -1,28 +1,30 @@ -import type { AtLeast, INotificationDesktop, ISubscription } from '@rocket.chat/core-typings'; +import type { AtLeast, INotificationDesktop, ISubscription, IUser } from '@rocket.chat/core-typings'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useRouter, useStream, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useCustomSound, useRouter, useStream, useUserPreference } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; -import { useEmbeddedLayout } from './useEmbeddedLayout'; -import { CachedChatSubscription } from '../../app/models/client'; -import { KonchatNotification } from '../../app/ui/client/lib/KonchatNotification'; -import { RoomManager } from '../lib/RoomManager'; -import { fireGlobalEvent } from '../lib/utils/fireGlobalEvent'; +import { useEmbeddedLayout } from '../useEmbeddedLayout'; +import { useDesktopNotification } from './useDesktopNotification'; +import { useNewMessageNotification } from './useNewMessageNotification'; +import { RoomManager } from '../../lib/RoomManager'; +import { fireGlobalEvent } from '../../lib/utils/fireGlobalEvent'; -export const useNotifyUser = () => { - const user = useUser(); +export const useNotifyUser = (user: IUser) => { const router = useRouter(); const isLayoutEmbedded = useEmbeddedLayout(); const notifyUserStream = useStream('notify-user'); const muteFocusedConversations = useUserPreference('muteFocusedConversations'); + const { notificationSounds } = useCustomSound(); + const newMessageNotification = useNewMessageNotification(); + const showDesktopNotification = useDesktopNotification(); const notifyNewRoom = useEffectEvent(async (sub: AtLeast): Promise => { - if (!user || user.status === 'busy') { + if (user.status === 'busy') { return; } if ((!router.getRouteParameters().name || router.getRouteParameters().name !== sub.name) && !sub.ls && sub.alert === true) { - KonchatNotification.newRoom(); + notificationSounds.playNewRoom(); } }); @@ -43,21 +45,17 @@ export const useNotifyUser = () => { if (isLayoutEmbedded) { if (!hasFocus && messageIsInOpenedRoom) { // Play a notification sound - void KonchatNotification.newMessage(rid); - void KonchatNotification.showDesktop(notification); + newMessageNotification(notification.payload); + showDesktopNotification(notification); } } else if (!hasFocus || !messageIsInOpenedRoom || !muteFocusedConversations) { // Play a notification sound - void KonchatNotification.newMessage(rid); - void KonchatNotification.showDesktop(notification); + newMessageNotification(notification.payload); + showDesktopNotification(notification); } }); useEffect(() => { - if (!user?._id) { - return; - } - const unsubNotification = notifyUserStream(`${user._id}/notification`, notifyNewMessageAudioAndDesktop); const unsubSubs = notifyUserStream(`${user._id}/subscriptions-changed`, (action, sub) => { @@ -68,16 +66,11 @@ export const useNotifyUser = () => { void notifyNewRoom(sub); }); - const handle = CachedChatSubscription.collection.find().observe({ - added: (sub) => { - void notifyNewRoom(sub); - }, - }); - return () => { unsubNotification(); unsubSubs(); - handle.stop(); }; - }, [isLayoutEmbedded, notifyNewMessageAudioAndDesktop, notifyNewRoom, notifyUserStream, router, user?._id]); + }, [notifyNewMessageAudioAndDesktop, notifyNewRoom, notifyUserStream, router, user._id]); + + useEffect(() => () => notificationSounds.stopNewRoom(), [notificationSounds]); }; diff --git a/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/index.ts b/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/index.ts deleted file mode 100644 index d01e5a6a5dffd..0000000000000 --- a/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useStartCallRoomAction'; diff --git a/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useStartCallRoomAction.tsx b/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useStartCallRoomAction.tsx deleted file mode 100644 index a4db8c7eddd6c..0000000000000 --- a/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useStartCallRoomAction.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { GenericMenu } from '@rocket.chat/ui-client'; -import { useMemo } from 'react'; - -import useVideoConfMenuOptions from './useVideoConfMenuOptions'; -import useVoipMenuOptions from './useVoipMenuOptions'; -import HeaderToolbarAction from '../../../components/Header/HeaderToolbarAction'; -import type { RoomToolboxActionConfig } from '../../../views/room/contexts/RoomToolboxContext'; - -export const useStartCallRoomAction = () => { - const videoCall = useVideoConfMenuOptions(); - const voipCall = useVoipMenuOptions(); - - return useMemo((): RoomToolboxActionConfig | undefined => { - if (!videoCall.allowed && !voipCall?.allowed) { - return undefined; - } - - return { - id: 'start-call', - title: 'Call', - icon: 'phone', - groups: [...videoCall.groups, ...(voipCall?.groups ?? [])], - disabled: videoCall.disabled && (voipCall?.disabled ?? true), - full: true, - order: Math.max(voipCall?.order ?? Number.NEGATIVE_INFINITY, videoCall.order), - featured: true, - renderToolboxItem: ({ id, icon, title, disabled, className }) => ( - } - key={id} - title={title} - disabled={disabled} - items={[...(voipCall?.allowed ? voipCall.items : []), ...videoCall.items]} - className={className} - placement='bottom-start' - icon={icon} - /> - ), - }; - }, [videoCall, voipCall]); -}; diff --git a/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useVideoConfMenuOptions.tsx b/apps/meteor/client/hooks/roomActions/useVideoCallRoomAction.tsx similarity index 70% rename from apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useVideoConfMenuOptions.tsx rename to apps/meteor/client/hooks/roomActions/useVideoCallRoomAction.tsx index 2281e02bbac71..9146a2fc651a0 100644 --- a/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useVideoConfMenuOptions.tsx +++ b/apps/meteor/client/hooks/roomActions/useVideoCallRoomAction.tsx @@ -1,7 +1,5 @@ -import { isOmnichannelRoom, isRoomFederated } from '@rocket.chat/core-typings'; -import { Box } from '@rocket.chat/fuselage'; +import { isRoomFederated } from '@rocket.chat/core-typings'; import { useEffectEvent, useStableArray } from '@rocket.chat/fuselage-hooks'; -import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { usePermission, useSetting, useUser } from '@rocket.chat/ui-contexts'; import { useVideoConfDispatchOutgoing, @@ -12,11 +10,11 @@ import { import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRoom } from '../../../views/room/contexts/RoomContext'; -import type { RoomToolboxActionConfig } from '../../../views/room/contexts/RoomToolboxContext'; -import { useVideoConfWarning } from '../../../views/room/contextualBar/VideoConference/hooks/useVideoConfWarning'; +import { useRoom } from '../../views/room/contexts/RoomContext'; +import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext'; +import { useVideoConfWarning } from '../../views/room/contextualBar/VideoConference/hooks/useVideoConfWarning'; -const useVideoConfMenuOptions = () => { +export const useVideoCallRoomAction = () => { const { t } = useTranslation(); const room = useRoom(); const user = useUser(); @@ -54,7 +52,6 @@ const useVideoConfMenuOptions = () => { const allowed = visible && permittedToCallManagement && (!user?.username || !room.muted?.includes(user.username)) && !ownUser; const disabled = federated || (!!room.ro && !permittedToPostReadonly); const tooltip = disabled ? t('core.Video_Call_unavailable_for_this_type_of_room') : ''; - const order = isOmnichannelRoom(room) ? -1 : 4; const handleOpenVideoConf = useEffectEvent(async () => { if (isCalling || isRinging) { @@ -69,29 +66,21 @@ const useVideoConfMenuOptions = () => { } }); - return useMemo(() => { - const items: GenericMenuItemProps[] = [ - { - id: 'start-video-call', - icon: 'video', - disabled, - onClick: handleOpenVideoConf, - content: ( - - {t('Video_call')} - - ), - }, - ]; + return useMemo((): RoomToolboxActionConfig | undefined => { + if (!allowed) { + return undefined; + } return { - items, - disabled, - allowed, - order, + id: 'start-video-call', + title: 'Video_call', + icon: 'video', + featured: true, + action: handleOpenVideoConf, + order: -1, groups, + disabled, + tooltip, }; - }, [allowed, disabled, groups, handleOpenVideoConf, order, t, tooltip]); + }, [allowed, groups, disabled, handleOpenVideoConf, tooltip]); }; - -export default useVideoConfMenuOptions; diff --git a/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useVoipMenuOptions.tsx b/apps/meteor/client/hooks/roomActions/useVoiceCallRoomAction.tsx similarity index 64% rename from apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useVoipMenuOptions.tsx rename to apps/meteor/client/hooks/roomActions/useVoiceCallRoomAction.tsx index b6f31ff5cedf2..c858a591387a7 100644 --- a/apps/meteor/client/hooks/roomActions/useStartCallRoomAction/useVoipMenuOptions.tsx +++ b/apps/meteor/client/hooks/roomActions/useVoiceCallRoomAction.tsx @@ -1,17 +1,16 @@ -import { Box } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { usePermission, useUserId } from '@rocket.chat/ui-contexts'; import { useVoipAPI, useVoipState } from '@rocket.chat/ui-voip'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useMediaPermissions } from '../../../views/room/composer/messageBox/hooks/useMediaPermissions'; -import { useRoom } from '../../../views/room/contexts/RoomContext'; -import { useUserInfoQuery } from '../../useUserInfoQuery'; -import { useVoipWarningModal } from '../../useVoipWarningModal'; +import { useMediaPermissions } from '../../views/room/composer/messageBox/hooks/useMediaPermissions'; +import { useRoom } from '../../views/room/contexts/RoomContext'; +import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext'; +import { useUserInfoQuery } from '../useUserInfoQuery'; +import { useVoipWarningModal } from '../useVoipWarningModal'; -const useVoipMenuOptions = () => { +export const useVoiceCallRoomAction = () => { const { t } = useTranslation(); const { uids = [] } = useRoom(); const ownUserId = useUserId(); @@ -32,10 +31,10 @@ const useVoipMenuOptions = () => { const isDM = members.length === 1; const disabled = isMicPermissionDenied || !isDM || isInCall || isPending; - const allowed = isDM && !isInCall && !isPending; + const allowed = canStartVoiceCall && isDM && !isInCall && !isPending; const canMakeVoipCall = allowed && isRemoteRegistered && isRegistered && isEnabled && !isMicPermissionDenied; - const title = useMemo(() => { + const tooltip = useMemo(() => { if (isMicPermissionDenied) { return t('Microphone_access_not_allowed'); } @@ -54,33 +53,20 @@ const useVoipMenuOptions = () => { dispatchWarning(); }); - return useMemo(() => { - if (!canStartVoiceCall) { + return useMemo((): RoomToolboxActionConfig | undefined => { + if (!allowed) { return undefined; } - const items: GenericMenuItemProps[] = [ - { - id: 'start-voip-call', - icon: 'phone', - disabled, - onClick: handleOnClick, - content: ( - - {t('Voice_call')} - - ), - }, - ]; - return { - items, + id: 'start-voice-call', + title: 'Voice_Call', + icon: 'phone', + featured: true, + action: handleOnClick, groups: ['direct'] as const, disabled, - order: 4, - allowed, + tooltip, }; - }, [disabled, title, t, handleOnClick, allowed, canStartVoiceCall]); + }, [allowed, disabled, handleOnClick, tooltip]); }; - -export default useVoipMenuOptions; diff --git a/apps/meteor/app/analytics/client/loadScript.ts b/apps/meteor/client/hooks/useAnalytics.ts similarity index 79% rename from apps/meteor/app/analytics/client/loadScript.ts rename to apps/meteor/client/hooks/useAnalytics.ts index e6dc12cd77da0..67a2d4d43b819 100644 --- a/apps/meteor/app/analytics/client/loadScript.ts +++ b/apps/meteor/client/hooks/useAnalytics.ts @@ -1,9 +1,6 @@ -import { Meteor } from 'meteor/meteor'; +import { useSetting, useUserId } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; -import { useReactiveValue } from '../../../client/hooks/useReactiveValue'; -import { settings } from '../../settings/client'; - declare global { // eslint-disable-next-line @typescript-eslint/naming-convention interface Window { @@ -20,10 +17,19 @@ declare global { } export const useAnalytics = (): void => { - const uid = useReactiveValue(() => Meteor.userId()); + const uid = useUserId(); + + const googleAnalyticsEnabled = useSetting('GoogleAnalytics_enabled', false); + const googleId = useSetting('GoogleAnalytics_ID', ''); + + const piwiEnabled = useSetting('PiwikAnalytics_enabled', false); + const piwikUrl = useSetting('PiwikAnalytics_url', ''); - const googleId = useReactiveValue(() => settings.get('GoogleAnalytics_enabled') && settings.get('GoogleAnalytics_ID')); - const piwikUrl = useReactiveValue(() => settings.get('PiwikAnalytics_enabled') && settings.get('PiwikAnalytics_url')); + const piwikSiteId = useSetting('PiwikAnalytics_siteId', ''); + const piwikPrependDomain = useSetting('PiwikAnalytics_prependDomain', ''); + const piwikCookieDomain = useSetting('PiwikAnalytics_cookieDomain', ''); + const piwikDomains = useSetting('PiwikAnalytics_domains', ''); + const piwikAdditionalTracker = useSetting('PiwikAdditionalTrackers', ''); useEffect(() => { if (uid) { @@ -33,7 +39,7 @@ export const useAnalytics = (): void => { }, [uid]); useEffect(() => { - if (!googleId) { + if (!googleAnalyticsEnabled || !googleId) { return; } if (googleId.startsWith('G-')) { @@ -73,20 +79,15 @@ export const useAnalytics = (): void => { window.ga?.('create', googleId, 'auto'); window.ga?.('send', 'pageview'); } - }, [googleId, uid]); + }, [googleAnalyticsEnabled, googleId, uid]); useEffect(() => { - if (!piwikUrl) { + if (!piwiEnabled || !piwikUrl) { document.getElementById('piwik-analytics')?.remove(); window._paq = []; return; } - const piwikSiteId = piwikUrl && settings.get('PiwikAnalytics_siteId'); - const piwikPrependDomain = piwikUrl && settings.get('PiwikAnalytics_prependDomain'); - const piwikCookieDomain = piwikUrl && settings.get('PiwikAnalytics_cookieDomain'); - const piwikDomains = piwikUrl && settings.get('PiwikAnalytics_domains'); - const piwikAdditionalTracker = piwikUrl && settings.get('PiwikAdditionalTrackers'); window._paq = window._paq || []; window._paq.push(['trackPageView']); @@ -136,5 +137,5 @@ export const useAnalytics = (): void => { g.src = `${piwikUrl}js/`; s.parentNode?.insertBefore(g, s); })(); - }, [piwikUrl]); + }, [piwiEnabled, piwikAdditionalTracker, piwikCookieDomain, piwikDomains, piwikPrependDomain, piwikSiteId, piwikUrl]); }; diff --git a/apps/meteor/client/hooks/useHideRoomAction.tsx b/apps/meteor/client/hooks/useHideRoomAction.tsx index b6ee1439645d3..2d45b40a2db31 100644 --- a/apps/meteor/client/hooks/useHideRoomAction.tsx +++ b/apps/meteor/client/hooks/useHideRoomAction.tsx @@ -85,7 +85,7 @@ export const useHideRoomAction = ({ rid: roomId, type, name }: HideRoomProps, { label: t('Hide_room'), }} > - {t(warnText as TranslationKey, { postProcess: 'sprintf', sprintf: [name] })} + {t(warnText as TranslationKey, { roomName: name })} , ); }); diff --git a/apps/meteor/client/hooks/useIdleActiveEvents.ts b/apps/meteor/client/hooks/useIdleActiveEvents.ts new file mode 100644 index 0000000000000..8d570a6c69847 --- /dev/null +++ b/apps/meteor/client/hooks/useIdleActiveEvents.ts @@ -0,0 +1,47 @@ +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useEffect } from 'react'; + +import { useIdleDetection } from './useIdleDetection'; + +/** + * useIdleActiveEvents is a custom hook that triggers a callback function when the user is detected to be idle, and another callback function when the user is detected to be active. + * The idle state is determined based on the absence of certain user interactions for a specified time period. + * + * @param options - A configuration object. + * @param options.id - A unique identifier for the idle detection mechanism. + * @param options.time - The time in milliseconds to consider the user idle. Optional. Defaults to 600000 ms (10 minutes). + * @param options.awayOnWindowBlur - A boolean flag to trigger the callback when the window loses focus. Optional. Defaults to false. + * @param onIdleCallback - The callback function to be called when the user is detected to be idle. + * @param onActiveCallback - The callback function to be called when the user is detected to be active. + * + */ + +export const useIdleActiveEvents = ( + { + id, + time, + awayOnWindowBlur, + }: { + id: string; + time?: number; + awayOnWindowBlur?: boolean; + }, + onIdleCallback: () => void, + onActiveCallback?: () => void, +) => { + const stableIdleCallback = useEffectEvent(onIdleCallback); + const stableActiveCallback = useEffectEvent(onActiveCallback || (() => undefined)); + + useEffect(() => { + document.addEventListener(`${id}_idle`, stableIdleCallback); + + onActiveCallback && document.addEventListener(`${id}_active`, stableActiveCallback); + + return () => { + document.removeEventListener(`${id}_idle`, stableIdleCallback); + document.removeEventListener(`${id}_active`, stableActiveCallback); + }; + }, [id, onActiveCallback, stableActiveCallback, stableIdleCallback]); + + return useIdleDetection({ id, time, awayOnWindowBlur }); +}; diff --git a/apps/meteor/client/hooks/useIdleConnection.ts b/apps/meteor/client/hooks/useIdleConnection.ts new file mode 100644 index 0000000000000..a058941c80433 --- /dev/null +++ b/apps/meteor/client/hooks/useIdleConnection.ts @@ -0,0 +1,27 @@ +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { ServerContext, useConnectionStatus, useSetting } from '@rocket.chat/ui-contexts'; +import { useContext } from 'react'; + +import { useIdleActiveEvents } from './useIdleActiveEvents'; + +export const useIdleConnection = (uid: string | null) => { + const { status } = useConnectionStatus(); + const allowAnonymousRead = useSetting('Accounts_AllowAnonymousRead'); + const { disconnect: disconnectServer, reconnect: reconnectServer } = useContext(ServerContext); + + const disconnect = useEffectEvent(() => { + if (status !== 'offline') { + if (!uid && allowAnonymousRead !== true) { + disconnectServer(); + } + } + }); + + const reconnect = useEffectEvent(() => { + if (status === 'offline') { + reconnectServer(); + } + }); + + useIdleActiveEvents({ id: 'useLoginPresence', time: 60 * 1000, awayOnWindowBlur: true }, disconnect, reconnect); +}; diff --git a/apps/meteor/client/hooks/useIdleDetection.ts b/apps/meteor/client/hooks/useIdleDetection.ts index 326d0f3d5b85e..16dbed9570658 100644 --- a/apps/meteor/client/hooks/useIdleDetection.ts +++ b/apps/meteor/client/hooks/useIdleDetection.ts @@ -1,28 +1,58 @@ -import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useEffect } from 'react'; +import { useEffect, useReducer } from 'react'; const events = ['mousemove', 'mousedown', 'touchend', 'touchstart', 'keypress']; +type UseIdleDetectionOptions = { + id?: string; + time?: number; + awayOnWindowBlur?: boolean; +}; + /** - * useIdleDetection is a custom hook that triggers a callback function when the user is detected to be idle. - * The idle state is determined based on the absence of certain user interactions for a specified time period. + * A hook that detects when the user is idle. * - * @param callback - The callback function to be called when the user is detected to be idle. - * @param options - An optional configuration object. - * @param options.time - The time in milliseconds to consider the user idle. Defaults to 600000 ms (10 minutes). - * @param options.awayOnWindowBlur - A boolean flag to trigger the callback when the window loses focus. Defaults to false. + * This hook listens for mousemove, mousedown, touchend, touchstart, and keypress events. + * When any of these events are triggered, the user is considered active. + * If no events are triggered for a specified period of time, the user is considered idle. * + * @param {object} options - An object with the following properties: + * @param {string} options.id - A unique identifier for the idle detection mechanism. Defaults to 'useIdleDetection'. + * @param {number} options.time - The time in milliseconds to consider the user idle. Defaults to 600000 ms (10 minutes). + * @param {boolean} options.awayOnWindowBlur - A boolean flag to trigger the idle state when the window loses focus. Defaults to false. + * + * @returns {boolean} A boolean indicating whether the user is idle or not. */ -export const useIdleDetection = (callback: () => void, { time = 600000, awayOnWindowBlur = false } = {}) => { - const stableCallback = useEffectEvent(callback); +export const useIdleDetection = ({ id = 'useIdleDetection', time = 600000, awayOnWindowBlur = false }: UseIdleDetectionOptions = {}) => { + const [isIdle, dispatch] = useReducer((state: boolean, action: boolean) => { + if (state === action) { + return state; + } + + if (action) { + document.dispatchEvent(new Event(`${id}_idle`)); + } + + if (!action) { + document.dispatchEvent(new Event(`${id}_active`)); + } + + document.dispatchEvent( + new CustomEvent(`${id}_change`, { + detail: { isIdle: action }, + }), + ); + + return action; + }, false); useEffect(() => { let interval: ReturnType; const handleIdle = () => { + dispatch(false); clearTimeout(interval); interval = setTimeout(() => { - document.dispatchEvent(new Event('idle')); + dispatch(true); }, time); }; @@ -33,24 +63,19 @@ export const useIdleDetection = (callback: () => void, { time = 600000, awayOnWi clearTimeout(interval); events.forEach((key) => document.removeEventListener(key, handleIdle)); }; - }, [stableCallback, time]); + }, [time]); useEffect(() => { if (!awayOnWindowBlur) { return; } - window.addEventListener('blur', stableCallback); + const dispatchIdle = () => dispatch(true); + window.addEventListener('blur', dispatchIdle); return () => { - window.removeEventListener('blur', stableCallback); + window.removeEventListener('blur', dispatchIdle); }; - }, [awayOnWindowBlur, stableCallback]); + }, [awayOnWindowBlur]); - useEffect(() => { - document.addEventListener('idle', stableCallback); - - return () => { - document.removeEventListener('idle', stableCallback); - }; - }, [stableCallback]); + return isIdle; }; diff --git a/apps/meteor/client/hooks/useLivechatInquiryStore.ts b/apps/meteor/client/hooks/useLivechatInquiryStore.ts new file mode 100644 index 0000000000000..ba33752c3bbbf --- /dev/null +++ b/apps/meteor/client/hooks/useLivechatInquiryStore.ts @@ -0,0 +1,40 @@ +import type { ILivechatInquiryRecord, IRoom } from '@rocket.chat/core-typings'; +import { create } from 'zustand'; + +export const useLivechatInquiryStore = create<{ + records: (ILivechatInquiryRecord & { alert?: boolean })[]; + add: (record: ILivechatInquiryRecord & { alert?: boolean }) => void; + merge: (record: ILivechatInquiryRecord & { alert?: boolean }) => void; + discard: (id: ILivechatInquiryRecord['_id']) => void; + discardForRoom: (rid: IRoom['_id']) => void; + discardAll: () => void; +}>()((set) => ({ + records: [], + + add: (record) => { + set(({ records }) => ({ records: [...records, record] })); + }, + + merge: (record) => { + set(({ records }) => { + const index = records.findIndex((r) => r._id === record._id); + if (index === -1) { + return { records: [...records, record] }; + } + records[index] = record; + return { records: [...records] }; + }); + }, + + discard: (id) => { + set(({ records }) => ({ records: records.filter((r) => r._id !== id) })); + }, + + discardForRoom: (rid) => { + set(({ records }) => ({ records: records.filter((r) => r.rid !== rid) })); + }, + + discardAll: () => { + set(() => ({ records: [] })); + }, +})); diff --git a/apps/meteor/client/hooks/useMergedRefsV2.ts b/apps/meteor/client/hooks/useMergedRefsV2.ts new file mode 100644 index 0000000000000..beb3e45c28fa7 --- /dev/null +++ b/apps/meteor/client/hooks/useMergedRefsV2.ts @@ -0,0 +1,30 @@ +import type { MutableRefObject, Ref, RefCallback } from 'react'; +import { useCallback } from 'react'; + +const isRefCallback = (x: unknown): x is RefCallback => typeof x === 'function'; +const isMutableRefObject = (x: unknown): x is MutableRefObject => typeof x === 'object'; + +export const setRef = (ref: Ref | undefined, refValue: T) => { + if (isRefCallback(ref)) { + ref(refValue); + return; + } + + if (isMutableRefObject(ref)) { + ref.current = refValue; + } +}; + +// TODO: backport to fuselage-hooks +/** + * Merges multiple refs into a single ref callback + * + * @param refs The refs to merge. + * @returns The merged ref callback. + */ +export const useMergedRefsV2 = (...refs: (Ref | undefined)[]): RefCallback => { + return useCallback((refValue: T) => { + refs.forEach((ref) => setRef(ref, refValue)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, refs); +}; diff --git a/apps/meteor/client/hooks/useOmnichannelContinuousSoundNotification.ts b/apps/meteor/client/hooks/useOmnichannelContinuousSoundNotification.ts index 639b4cb7af22e..d565aa74902d8 100644 --- a/apps/meteor/client/hooks/useOmnichannelContinuousSoundNotification.ts +++ b/apps/meteor/client/hooks/useOmnichannelContinuousSoundNotification.ts @@ -1,54 +1,28 @@ -import type { ICustomSound } from '@rocket.chat/core-typings'; -import { useSetting, useUserPreference, useUserSubscriptions } from '@rocket.chat/ui-contexts'; +import { useCustomSound, useSetting, useUserSubscriptions } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; -import { useUserSoundPreferences } from './useUserSoundPreferences'; -import { CustomSounds } from '../../app/custom-sounds/client/lib/CustomSounds'; - const query = { t: 'l', ls: { $exists: false }, open: true }; export const useOmnichannelContinuousSoundNotification = (queue: T[]) => { const userSubscriptions = useUserSubscriptions(query); + const { notificationSounds } = useCustomSound(); const playNewRoomSoundContinuously = useSetting('Livechat_continuous_sound_notification_new_livechat_room'); - const newRoomNotification = useUserPreference('newRoomNotification'); - const { notificationsSoundVolume } = useUserSoundPreferences(); - - const continuousCustomSoundId = newRoomNotification && `${newRoomNotification}-continuous`; - const hasUnreadRoom = userSubscriptions.length > 0 || queue.length > 0; useEffect(() => { - let audio: ICustomSound; - if (playNewRoomSoundContinuously && continuousCustomSoundId) { - audio = { ...CustomSounds.getSound(newRoomNotification), _id: continuousCustomSoundId }; - CustomSounds.add(audio); - } - - return () => { - if (audio) { - CustomSounds.remove(audio); - } - }; - }, [continuousCustomSoundId, newRoomNotification, playNewRoomSoundContinuously]); - - useEffect(() => { - if (!continuousCustomSoundId) { - return; - } if (!playNewRoomSoundContinuously) { - CustomSounds.pause(continuousCustomSoundId); return; } if (!hasUnreadRoom) { - CustomSounds.pause(continuousCustomSoundId); return; } - CustomSounds.play(continuousCustomSoundId, { - volume: notificationsSoundVolume / 100, - loop: true, - }); - }, [continuousCustomSoundId, playNewRoomSoundContinuously, userSubscriptions, notificationsSoundVolume, hasUnreadRoom]); + notificationSounds.playNewMessageLoop(); + + return () => { + notificationSounds.stopNewRoom(); + }; + }, [playNewRoomSoundContinuously, userSubscriptions, hasUnreadRoom, notificationSounds]); }; diff --git a/apps/meteor/client/hooks/usePresence.ts b/apps/meteor/client/hooks/usePresence.ts deleted file mode 100644 index 555b3b4c8a9d2..0000000000000 --- a/apps/meteor/client/hooks/usePresence.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useCallback, useSyncExternalStore } from 'react'; - -import type { UserPresence } from '../lib/presence'; -import { Presence } from '../lib/presence'; - -/** - * @deprecated - * Hook to fetch and subscribe users presence - * - * @param uid - User Id - * @returns UserPresence - * @public - */ -export const usePresence = (uid: string | undefined): UserPresence | undefined => { - const subscribe = useCallback( - (callback: any): any => { - uid && Presence.listen(uid, callback); - return (): void => { - uid && Presence.stop(uid, callback); - }; - }, - [uid], - ); - - const getSnapshot = (): UserPresence | undefined => (uid ? Presence.store.get(uid) : undefined); - - return useSyncExternalStore(subscribe, getSnapshot); -}; diff --git a/apps/meteor/client/hooks/useRoomRolesQuery.ts b/apps/meteor/client/hooks/useRoomRolesQuery.ts new file mode 100644 index 0000000000000..ff0980e71ae66 --- /dev/null +++ b/apps/meteor/client/hooks/useRoomRolesQuery.ts @@ -0,0 +1,122 @@ +import type { IUser, IRole, IRoom } from '@rocket.chat/core-typings'; +import { useMethod, useStream, useUserId } from '@rocket.chat/ui-contexts'; +import { useQuery, useQueryClient, type UseQueryOptions } from '@tanstack/react-query'; +import { useEffect } from 'react'; + +import { roomsQueryKeys } from '../lib/queryKeys'; + +export type RoomRoles = { + rid: IRoom['_id']; + u: Pick; + roles: IRole['_id'][]; +}; + +type UseRoomRolesQueryOptions = Omit< + UseQueryOptions>, + 'queryKey' | 'queryFn' +>; + +export const useRoomRolesQuery = (rid: IRoom['_id'], options?: UseRoomRolesQueryOptions) => { + const queryClient = useQueryClient(); + + const uid = useUserId(); + + const subscribeToNotifyLogged = useStream('notify-logged'); + + const enabled = !!uid && (options?.enabled ?? true); + + useEffect(() => { + if (!enabled) return; + + return subscribeToNotifyLogged('roles-change', (role) => { + switch (role.type) { + case 'added': { + const { _id: roleId, scope, u } = role; + if (!scope || !u) return; + + queryClient.setQueryData(roomsQueryKeys.roles(rid), (data: RoomRoles[] | undefined = []): RoomRoles[] => { + const index = data?.findIndex((record) => record.rid === rid && record.u._id === u._id) ?? -1; + + if (index < 0) { + return [...data, { rid, u, roles: [roleId] }]; + } + + const roles = new Set(data[index].roles); + roles.add(roleId); + data[index] = { ...data[index], roles: [...roles] }; + + return [...data]; + }); + break; + } + + case 'removed': { + const { _id: roleId, scope, u } = role; + if (!!scope || !u) return; + + queryClient.setQueryData(roomsQueryKeys.roles(rid), (data: RoomRoles[] | undefined = []) => { + const index = data?.findIndex((record) => record.rid === rid && record.u._id === u._id) ?? -1; + + if (index < 0) return data; + + const roles = new Set(data[index].roles); + roles.delete(roleId); + data[index] = { ...data[index], roles: [...roles] }; + + return [...data]; + }); + break; + } + } + }); + }, [enabled, queryClient, rid, subscribeToNotifyLogged, uid]); + + useEffect(() => { + if (!enabled) return; + + return subscribeToNotifyLogged('Users:NameChanged', ({ _id: uid, username, name }: Partial) => { + if (!uid) { + return; + } + + queryClient.setQueryData(roomsQueryKeys.roles(rid), (data: RoomRoles[] | undefined = []) => { + const index = data?.findIndex((record) => record.rid === rid && record.u._id === uid) ?? -1; + + if (index < 0) { + return [...data, { rid, u: { _id: uid, username, name }, roles: [] }]; + } + + data[index] = { + ...data[index], + u: { + ...data[index].u, + username, + name, + }, + }; + + return [...data]; + }); + }); + }, [enabled, queryClient, rid, subscribeToNotifyLogged]); + + const getRoomRoles = useMethod('getRoomRoles'); + + return useQuery({ + queryKey: roomsQueryKeys.roles(rid), + queryFn: async () => { + const results = await getRoomRoles(rid); + + return results.map( + (record): RoomRoles => ({ + rid: record.rid, + u: record.u, + roles: record.roles ?? [], + }), + ); + }, + staleTime: Infinity, + ...options, + enabled, + }); +}; diff --git a/apps/meteor/client/hooks/useUserRolesQuery.ts b/apps/meteor/client/hooks/useUserRolesQuery.ts new file mode 100644 index 0000000000000..854d4afbc029c --- /dev/null +++ b/apps/meteor/client/hooks/useUserRolesQuery.ts @@ -0,0 +1,92 @@ +import type { IRole, IUser } from '@rocket.chat/core-typings'; +import { useMethod, useStream, useUserId } from '@rocket.chat/ui-contexts'; +import type { UseQueryOptions } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useEffect } from 'react'; + +import { rolesQueryKeys } from '../lib/queryKeys'; + +export type UserRoles = { + uid: IUser['_id']; + roles: IRole['_id'][]; +}; + +type UseUserRolesQueryOptions = Omit< + UseQueryOptions>, + 'queryKey' | 'queryFn' +>; + +export const useUserRolesQuery = (options?: UseUserRolesQueryOptions) => { + const queryClient = useQueryClient(); + + const uid = useUserId(); + + const subscribeToNotifyLogged = useStream('notify-logged'); + + const enabled = !!uid && (options?.enabled ?? true); + + useEffect(() => { + if (!enabled) return; + + return subscribeToNotifyLogged('roles-change', (role) => { + switch (role.type) { + case 'added': { + const { _id: roleId, scope, u } = role; + if (!!scope || !u) return; + + queryClient.setQueryData(rolesQueryKeys.userRoles(), (data: UserRoles[] | undefined = []): UserRoles[] => { + const index = data?.findIndex((record) => record.uid === u._id) ?? -1; + + if (index < 0) { + return [...data, { uid: u._id, roles: [roleId] }]; + } + + const roles = new Set(data[index].roles); + roles.add(roleId); + data[index] = { ...data[index], roles: [...roles] }; + + return [...data]; + }); + break; + } + + case 'removed': { + const { _id: roleId, scope, u } = role; + if (!!scope || !u) return; + + queryClient.setQueryData(rolesQueryKeys.userRoles(), (data: UserRoles[] | undefined = []): UserRoles[] => { + const index = data?.findIndex((record) => record.uid === u._id) ?? -1; + + if (index < 0) return data; + + const roles = new Set(data[index].roles); + roles.delete(roleId); + data[index] = { ...data[index], roles: [...roles] }; + + return [...data]; + }); + break; + } + } + }); + }, [enabled, queryClient, subscribeToNotifyLogged, uid]); + + const getUserRoles = useMethod('getUserRoles'); + + return useQuery({ + queryKey: rolesQueryKeys.userRoles(), + queryFn: async () => { + const results = await getUserRoles(); + + return results.map( + (record): UserRoles => ({ + uid: record._id, + roles: record.roles, + }), + ); + }, + staleTime: Infinity, + ...options, + enabled, + }); +}; diff --git a/apps/meteor/client/importPackages.ts b/apps/meteor/client/importPackages.ts index ad7f06e15603a..556f77a4146ae 100644 --- a/apps/meteor/client/importPackages.ts +++ b/apps/meteor/client/importPackages.ts @@ -1,11 +1,8 @@ import '../app/apple/client'; import '../app/authorization/client'; import '../app/autotranslate/client'; -import '../app/canned-responses/client'; -import '../app/custom-sounds/client'; import '../app/emoji/client'; import '../app/emoji-emojione/client'; -import '../app/emoji-custom/client'; import '../app/gitlab/client'; import '../app/iframe-login/client'; import '../app/license/client'; diff --git a/apps/meteor/client/lib/cachedCollections/CachedCollection.ts b/apps/meteor/client/lib/cachedCollections/CachedCollection.ts index 9e9e5107d3f2e..c3903ff532c63 100644 --- a/apps/meteor/client/lib/cachedCollections/CachedCollection.ts +++ b/apps/meteor/client/lib/cachedCollections/CachedCollection.ts @@ -36,8 +36,8 @@ const hasUnserializedUpdatedAt = (record: T): record is T & { _updatedAt: Con localforage.config({ name: baseURI }); -export class CachedCollection { - private static MAX_CACHE_TIME = 60 * 60 * 24 * 30; +export abstract class CachedCollection { + private static readonly MAX_CACHE_TIME = 60 * 60 * 24 * 30; public collection: MinimongoCollection; @@ -47,22 +47,19 @@ export class CachedCollection { protected eventType: StreamNames; - protected version = 18; + private readonly version = 18; - protected userRelated: boolean; - - protected updatedAt = new Date(0); + private updatedAt = new Date(0); protected log: (...args: any[]) => void; private timer: ReturnType; - constructor({ name, eventType = 'notify-user', userRelated = true }: { name: Name; eventType?: StreamNames; userRelated?: boolean }) { + constructor({ name, eventType }: { name: Name; eventType: StreamNames }) { this.collection = new Mongo.Collection(null) as MinimongoCollection; this.name = name; this.eventType = eventType; - this.userRelated = userRelated; this.log = [getConfig(`debugCachedCollection-${this.name}`), getConfig('debugCachedCollection'), getConfig('debug')].includes('true') ? console.log.bind(console, `%cCachedCollection ${this.name}`, `color: navy; font-weight: bold;`) @@ -78,13 +75,7 @@ export class CachedCollection { return `${this.name}-changed`; } - getToken() { - if (this.userRelated === false) { - return undefined; - } - - return Accounts._storedLoginToken(); - } + protected abstract getToken(): unknown; private async loadFromCache() { const data = await localforage.getItem<{ version: number; token: unknown; records: unknown[]; updatedAt: Date | string }>(this.name); @@ -195,7 +186,7 @@ export class CachedCollection { await this.save(); } - save = withDebouncing({ wait: 1000 })(async () => { + private save = withDebouncing({ wait: 1000 })(async () => { this.log('saving cache'); const data = this.collection.find().fetch(); await localforage.setItem(this.name, { @@ -207,19 +198,15 @@ export class CachedCollection { this.log('saving cache (done)'); }); - clearCacheOnLogout() { - if (this.userRelated === true) { - void this.clearCache(); - } - } + abstract clearCacheOnLogout(): void; - async clearCache() { + protected async clearCache() { this.log('clearing cache'); await localforage.removeItem(this.name); this.collection.remove({}); } - async setupListener() { + protected async setupListener() { sdk.stream(this.eventType, [this.eventName], (async (action: 'removed' | 'changed', record: any) => { this.log('record received', action, record); await this.handleRecordEvent(action, record); @@ -245,7 +232,7 @@ export class CachedCollection { await this.save(); } - trySync(delay = 10) { + private trySync(delay = 10) { clearTimeout(this.timer); // Wait for an empty queue to load data again and sync this.timer = setTimeout(async () => { @@ -256,7 +243,7 @@ export class CachedCollection { }, delay); } - async sync() { + protected async sync() { if (!this.updatedAt || this.updatedAt.getTime() === 0 || Meteor.connection._outstandingMethodBlocks.length !== 0) { return false; } @@ -355,13 +342,28 @@ export class CachedCollection { } private reconnectionComputation: Tracker.Computation | undefined; +} - listen() { - if (!this.userRelated) { - void this.init(); - return; - } +export class PublicCachedCollection extends CachedCollection { + protected getToken() { + return undefined; + } + clearCacheOnLogout() { + // do nothing + } +} + +export class PrivateCachedCollection extends CachedCollection { + protected getToken() { + return Accounts._storedLoginToken(); + } + + clearCacheOnLogout() { + void this.clearCache(); + } + + listen() { if (process.env.NODE_ENV === 'test') { return; } diff --git a/apps/meteor/client/lib/cachedCollections/index.ts b/apps/meteor/client/lib/cachedCollections/index.ts index fb99c0d3feead..849e96c1cf5b2 100644 --- a/apps/meteor/client/lib/cachedCollections/index.ts +++ b/apps/meteor/client/lib/cachedCollections/index.ts @@ -1,2 +1,2 @@ -export { CachedCollection } from './CachedCollection'; +export { PrivateCachedCollection, PublicCachedCollection } from './CachedCollection'; export { CachedCollectionManager } from './CachedCollectionManager'; diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index dbdaa1b04ac7b..058870cfbd286 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -1,10 +1,9 @@ -import type { IMessage, IRoom, ISubscription, IE2EEMessage, IUpload } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, ISubscription, IE2EEMessage, IUpload, Subscribable } from '@rocket.chat/core-typings'; import type { IActionManager } from '@rocket.chat/ui-contexts'; import type { Upload } from './Upload'; import type { ReadStateManager } from './readStateManager'; import type { FormattingButton } from '../../../app/ui-message/client/messageBox/messageBoxFormatting'; -import type { Subscribable } from '../../definitions/Subscribable'; export type ComposerAPI = { release(): void; diff --git a/apps/meteor/client/lib/chats/readStateManager.ts b/apps/meteor/client/lib/chats/readStateManager.ts index 89b85c52401f9..fbb0c849e9680 100644 --- a/apps/meteor/client/lib/chats/readStateManager.ts +++ b/apps/meteor/client/lib/chats/readStateManager.ts @@ -38,6 +38,10 @@ export class ReadStateManager extends Emitter { return this.firstUnreadRecordId; }; + public subscribeToMessages() { + return RoomHistoryManager.on('loaded-messages', () => this.updateFirstUnreadRecordId()); + } + public updateSubscription(subscription?: ISubscription) { if (!subscription) { return; @@ -88,8 +92,6 @@ export class ReadStateManager extends Emitter { ); this.setFirstUnreadRecordId(firstUnreadRecord?._id); - - RoomHistoryManager.once('loaded-messages', () => this.updateFirstUnreadRecordId()); } private setFirstUnreadRecordId(firstUnreadRecordId: string | undefined) { diff --git a/apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts b/apps/meteor/client/lib/customEmoji.ts similarity index 76% rename from apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts rename to apps/meteor/client/lib/customEmoji.ts index bb76f7388c179..e1f040af64912 100644 --- a/apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts +++ b/apps/meteor/client/lib/customEmoji.ts @@ -1,11 +1,8 @@ import type { IEmoji } from '@rocket.chat/core-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { Meteor } from 'meteor/meteor'; -import { onLoggedIn } from '../../../../client/lib/loggedIn'; -import { emoji, removeFromRecent, replaceEmojiInRecent } from '../../../emoji/client'; -import { getURL } from '../../../utils/client'; -import { sdk } from '../../../utils/client/lib/SDKClient'; +import { emoji, removeFromRecent, replaceEmojiInRecent } from '../../app/emoji/client'; +import { getURL } from '../../app/utils/client'; const isSetNotNull = (fn: () => unknown) => { let value; @@ -17,38 +14,6 @@ const isSetNotNull = (fn: () => unknown) => { return value !== null && value !== undefined; }; -const getEmojiUrlFromName = (name: string, extension: string, etag?: string) => { - if (!name) { - return; - } - - return getURL(`/emoji-custom/${encodeURIComponent(name)}.${extension}${etag ? `?etag=${etag}` : ''}`); -}; - -export const deleteEmojiCustom = (emojiData: IEmoji) => { - delete emoji.list[`:${emojiData.name}:`]; - const arrayIndex = emoji.packages.emojiCustom.emojisByCategory.rocket.indexOf(emojiData.name); - if (arrayIndex !== -1) { - emoji.packages.emojiCustom.emojisByCategory.rocket.splice(arrayIndex, 1); - } - const arrayIndexList = emoji.packages.emojiCustom.list?.indexOf(`:${emojiData.name}:`) ?? -1; - if (arrayIndexList !== -1) { - emoji.packages.emojiCustom.list?.splice(arrayIndexList, 1); - } - if (emojiData.aliases) { - for (const alias of emojiData.aliases) { - delete emoji.list[`:${alias}:`]; - const aliasIndex = emoji.packages.emojiCustom.list?.indexOf(`:${alias}:`) ?? -1; - if (aliasIndex !== -1) { - emoji.packages.emojiCustom.list?.splice(aliasIndex, 1); - } - } - } - - removeFromRecent(emojiData.name, emoji.packages.base.emojisByCategory.recent); - emoji.dispatchUpdate(); -}; - export const updateEmojiCustom = (emojiData: IEmoji) => { const previousExists = isSetNotNull(() => emojiData.previousName); const currentAliases = isSetNotNull(() => emojiData.aliases); @@ -98,7 +63,39 @@ export const updateEmojiCustom = (emojiData: IEmoji) => { emoji.dispatchUpdate(); }; -const customRender = (html: string) => { +export const deleteEmojiCustom = (emojiData: IEmoji) => { + delete emoji.list[`:${emojiData.name}:`]; + const arrayIndex = emoji.packages.emojiCustom.emojisByCategory.rocket.indexOf(emojiData.name); + if (arrayIndex !== -1) { + emoji.packages.emojiCustom.emojisByCategory.rocket.splice(arrayIndex, 1); + } + const arrayIndexList = emoji.packages.emojiCustom.list?.indexOf(`:${emojiData.name}:`) ?? -1; + if (arrayIndexList !== -1) { + emoji.packages.emojiCustom.list?.splice(arrayIndexList, 1); + } + if (emojiData.aliases) { + for (const alias of emojiData.aliases) { + delete emoji.list[`:${alias}:`]; + const aliasIndex = emoji.packages.emojiCustom.list?.indexOf(`:${alias}:`) ?? -1; + if (aliasIndex !== -1) { + emoji.packages.emojiCustom.list?.splice(aliasIndex, 1); + } + } + } + + removeFromRecent(emojiData.name, emoji.packages.base.emojisByCategory.recent); + emoji.dispatchUpdate(); +}; + +const getEmojiUrlFromName = (name: string, extension: string, etag?: string) => { + if (!name) { + return; + } + + return getURL(`/emoji-custom/${encodeURIComponent(name)}.${extension}${etag ? `?etag=${etag}` : ''}`); +}; + +export const customRender = (html: string) => { const emojisMatchGroup = emoji.packages.emojiCustom.list?.map(escapeRegExp).join('|'); if (emojisMatchGroup !== emoji.packages.emojiCustom._regexpSignature) { emoji.packages.emojiCustom._regexpSignature = emojisMatchGroup; @@ -131,42 +128,3 @@ const customRender = (html: string) => { return html; }; - -emoji.packages.emojiCustom = { - emojiCategories: [{ key: 'rocket', i18n: 'Custom' }], - categoryIndex: 1, - toneList: {}, - list: [], - _regexpSignature: null, - _regexp: null, - emojisByCategory: {}, - render: customRender, - renderPicker: customRender, -}; - -Meteor.startup(() => { - onLoggedIn(async () => { - try { - const { - emojis: { update: emojis }, - } = await sdk.rest.get('/v1/emoji-custom.list', { query: '' }); - - emoji.packages.emojiCustom.emojisByCategory = { rocket: [] }; - for (const currentEmoji of emojis) { - emoji.packages.emojiCustom.emojisByCategory.rocket.push(currentEmoji.name); - emoji.packages.emojiCustom.list?.push(`:${currentEmoji.name}:`); - emoji.list[`:${currentEmoji.name}:`] = { ...currentEmoji, emojiPackage: 'emojiCustom' } as any; - for (const alias of currentEmoji.aliases) { - emoji.packages.emojiCustom.list?.push(`:${alias}:`); - emoji.list[`:${alias}:`] = { - emojiPackage: 'emojiCustom', - aliasOf: currentEmoji.name, - }; - } - } - emoji.dispatchUpdate(); - } catch (e) { - console.error('Error getting custom emoji', e); - } - }); -}); diff --git a/apps/meteor/client/lib/federation/Federation.spec.ts b/apps/meteor/client/lib/federation/Federation.spec.ts index e4753f7e54220..e6a4ef3fc3845 100644 --- a/apps/meteor/client/lib/federation/Federation.spec.ts +++ b/apps/meteor/client/lib/federation/Federation.spec.ts @@ -1,17 +1,12 @@ import type { IRoom, ISubscription, IUser, ValueOf } from '@rocket.chat/core-typings'; import * as Federation from './Federation'; -import { RoomRoles } from '../../../app/models/client'; import { RoomMemberActions, RoomSettingsEnum } from '../../../definition/IRoomTypeConfig'; - -jest.mock('../../../app/models/client', () => ({ - RoomRoles: { - findOne: jest.fn(), - }, -})); +import { queryClient } from '../queryClient'; +import { roomsQueryKeys } from '../queryKeys'; afterEach(() => { - (RoomRoles.findOne as jest.Mock).mockClear(); + queryClient.resetQueries(); }); describe('#actionAllowed()', () => { @@ -51,8 +46,14 @@ describe('#actionAllowed()', () => { describe('Seeing another owners', () => { const theirRole = ['owner']; + beforeEach(() => { + queryClient.setQueryData(roomsQueryKeys.roles('room-id'), [ + { rid: 'room-id', u: { _id: me }, roles: myRole }, + { rid: 'room-id', u: { _id: them }, roles: theirRole }, + ]); + }); + it('should return true if the user want to remove himself as an owner', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { u: { _id: me }, @@ -62,7 +63,6 @@ describe('#actionAllowed()', () => { }); it('should return true if the user want to add himself as a moderator (Demoting himself to moderator)', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, me, { u: { _id: me }, @@ -72,20 +72,22 @@ describe('#actionAllowed()', () => { }); it('should return false if the user want to remove another owners as an owner', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); + queryClient.setQueryData(roomsQueryKeys.roles('room-id'), [ + { rid: 'room-id', u: { _id: me }, roles: myRole }, + { rid: 'room-id', u: { _id: them }, roles: theirRole }, + ]); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { - u: { _id: them }, + Federation.actionAllowed({ _id: 'room-id', federated: true }, RoomMemberActions.SET_AS_OWNER, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(false); }); it('should return false if the user want to remove another owners from the room', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, me, { - u: { _id: them }, + Federation.actionAllowed({ _id: 'room-id', federated: true }, RoomMemberActions.REMOVE_USER, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(false); @@ -95,31 +97,35 @@ describe('#actionAllowed()', () => { describe('Seeing moderators', () => { const theirRole = ['moderator']; + beforeEach(() => { + queryClient.setQueryData(roomsQueryKeys.roles('room-id'), [ + { rid: 'room-id', u: { _id: me }, roles: myRole }, + { rid: 'room-id', u: { _id: them }, roles: theirRole }, + ]); + }); + it('should return true if the user want to add/remove moderators as an owner', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { - u: { _id: them }, + Federation.actionAllowed({ _id: 'room-id', federated: true }, RoomMemberActions.SET_AS_OWNER, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(true); }); it('should return true if the user want to remove moderators as a moderator', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(true); }); it('should return true if the user want to remove moderators from the room', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(true); @@ -128,30 +134,27 @@ describe('#actionAllowed()', () => { describe('Seeing normal users', () => { it('should return true if the user want to add/remove normal users as an owner', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue(undefined); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(true); }); it('should return true if the user want to add/remove normal users as a moderator', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue(undefined); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(true); }); it('should return true if the user want to remove normal users from the room', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue(undefined); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(true); @@ -165,8 +168,14 @@ describe('#actionAllowed()', () => { describe('Seeing owners', () => { const theirRole = ['owner']; + beforeEach(() => { + queryClient.setQueryData(roomsQueryKeys.roles('room-id'), [ + { rid: 'room-id', u: { _id: me }, roles: myRole }, + { rid: 'room-id', u: { _id: them }, roles: theirRole }, + ]); + }); + it('should return false if the user want to add/remove owners as a moderator', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { u: { _id: me }, @@ -176,9 +185,8 @@ describe('#actionAllowed()', () => { }); it('should return false if the user want to add/remove owners as a moderator', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, me, { + Federation.actionAllowed({ _id: 'room-id', federated: true }, RoomMemberActions.SET_AS_MODERATOR, them, { u: { _id: me }, roles: myRole, } as ISubscription), @@ -186,9 +194,8 @@ describe('#actionAllowed()', () => { }); it('should return false if the user want to add/remove owners as a moderator', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, me, { + Federation.actionAllowed({ _id: 'room-id', federated: true }, RoomMemberActions.SET_AS_MODERATOR, them, { u: { _id: me }, roles: myRole, } as ISubscription), @@ -196,9 +203,8 @@ describe('#actionAllowed()', () => { }); it('should return false if the user want to remove owners from the room', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, me, { + Federation.actionAllowed({ _id: 'room-id', federated: true }, RoomMemberActions.REMOVE_USER, them, { u: { _id: me }, roles: myRole, } as ISubscription), @@ -209,20 +215,25 @@ describe('#actionAllowed()', () => { describe('Seeing another moderators', () => { const theirRole = ['moderator']; + beforeEach(() => { + queryClient.setQueryData(roomsQueryKeys.roles('room-id'), [ + { rid: 'room-id', u: { _id: me }, roles: myRole }, + { rid: 'room-id', u: { _id: them }, roles: theirRole }, + ]); + }); + it('should return false if the user want to add/remove moderator as an owner', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(false); }); it('should return true if the user want to remove himself as a moderator (Demoting himself)', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, me, { + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, them, { u: { _id: me }, roles: myRole, } as ISubscription), @@ -230,9 +241,8 @@ describe('#actionAllowed()', () => { }); it('should return false if the user want to promote himself as an owner', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, them, { u: { _id: me }, roles: myRole, } as ISubscription), @@ -240,20 +250,18 @@ describe('#actionAllowed()', () => { }); it('should return false if the user want to remove another moderator from their role', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, me, { - u: { _id: them }, + Federation.actionAllowed({ _id: 'room-id', federated: true }, RoomMemberActions.SET_AS_MODERATOR, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(false); }); it('should return false if the user want to remove another moderator from the room', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, me, { - u: { _id: them }, + Federation.actionAllowed({ _id: 'room-id', federated: true }, RoomMemberActions.REMOVE_USER, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(false); @@ -262,30 +270,27 @@ describe('#actionAllowed()', () => { describe('Seeing normal users', () => { it('should return false if the user want to add/remove normal users as an owner', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue(undefined); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(false); }); it('should return true if the user want to add/remove normal users as a moderator', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue(undefined); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(true); }); it('should return true if the user want to remove normal users from the room', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue(undefined); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(true); @@ -297,29 +302,30 @@ describe('#actionAllowed()', () => { describe('Seeing owners', () => { const theirRole = ['owner']; + beforeEach(() => { + queryClient.setQueryData(roomsQueryKeys.roles('room-id'), [{ rid: 'room-id', u: { _id: them }, roles: theirRole }]); + }); + it('should return false if the user want to add/remove owners as a normal user', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, them, { + u: { _id: me }, } as ISubscription), ).toBe(false); }); it('should return false if the user want to add/remove moderators as a normal user', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, them, { + u: { _id: me }, } as ISubscription), ).toBe(false); }); it('should return false if the user want to remove owners from the room', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, them, { + u: { _id: me }, } as ISubscription), ).toBe(false); }); @@ -328,29 +334,30 @@ describe('#actionAllowed()', () => { describe('Seeing moderators', () => { const theirRole = ['owner']; + beforeEach(() => { + queryClient.setQueryData(roomsQueryKeys.roles('room-id'), [{ rid: 'room-id', u: { _id: them }, roles: theirRole }]); + }); + it('should return false if the user want to add/remove owner as a normal user', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, them, { + u: { _id: me }, } as ISubscription), ).toBe(false); }); it('should return false if the user want to remove a moderator from their role', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, them, { + u: { _id: me }, } as ISubscription), ).toBe(false); }); it('should return false if the user want to remove a moderator from the room', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, them, { + u: { _id: me }, } as ISubscription), ).toBe(false); }); @@ -358,28 +365,25 @@ describe('#actionAllowed()', () => { describe('Seeing another normal users', () => { it('should return false if the user want to add/remove owner as a normal user', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue(undefined); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, them, { + u: { _id: me }, } as ISubscription), ).toBe(false); }); it('should return false if the user want to add/remove moderator as a normal user', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue(undefined); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, them, { + u: { _id: me }, } as ISubscription), ).toBe(false); }); it('should return false if the user want to remove normal users from the room', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue(undefined); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, them, { + u: { _id: me }, } as ISubscription), ).toBe(false); }); @@ -387,7 +391,6 @@ describe('#actionAllowed()', () => { it.each([[RoomMemberActions.SET_AS_MODERATOR], [RoomMemberActions.SET_AS_OWNER], [RoomMemberActions.REMOVE_USER]])( 'should return false if the user want to %s for himself', (action) => { - (RoomRoles.findOne as jest.Mock).mockReturnValue(undefined); expect( Federation.actionAllowed({ federated: true }, action, me, { u: { _id: me }, diff --git a/apps/meteor/client/lib/federation/Federation.ts b/apps/meteor/client/lib/federation/Federation.ts index eefa053bac430..8e9391b18bf26 100644 --- a/apps/meteor/client/lib/federation/Federation.ts +++ b/apps/meteor/client/lib/federation/Federation.ts @@ -1,8 +1,10 @@ import type { IRoom, ISubscription, IUser, ValueOf } from '@rocket.chat/core-typings'; import { isRoomFederated, isDirectMessageRoom, isPublicRoom } from '@rocket.chat/core-typings'; -import { RoomRoles } from '../../../app/models/client'; import { RoomMemberActions, RoomSettingsEnum } from '../../../definition/IRoomTypeConfig'; +import type { RoomRoles } from '../../hooks/useRoomRolesQuery'; +import { queryClient } from '../queryClient'; +import { roomsQueryKeys } from '../queryKeys'; const allowedUserActionsInFederatedRooms: ValueOf[] = [ RoomMemberActions.REMOVE_USER, @@ -39,7 +41,11 @@ export const actionAllowed = ( return false; } - const displayingUserRoomRoles = RoomRoles.findOne({ 'rid': room._id, 'u._id': displayingUserId })?.roles || []; + // TODO: there is no guarantee that the room roles are already loaded + const displayingUserRoomRoles = + queryClient + .getQueryData(roomsQueryKeys.roles(room._id)) + ?.find((record) => record.rid === room._id && record.u._id === displayingUserId)?.roles || []; const loggedInUserRoomRoles = userSubscription.roles || []; if (loggedInUserRoomRoles.includes('owner')) { diff --git a/apps/meteor/client/lib/lists/RecordList.spec.ts b/apps/meteor/client/lib/lists/RecordList.spec.ts new file mode 100644 index 0000000000000..88d846970f773 --- /dev/null +++ b/apps/meteor/client/lib/lists/RecordList.spec.ts @@ -0,0 +1,138 @@ +import { AsyncStatePhase } from '../asyncState'; +import { RecordList } from './RecordList'; // Adjust the import path if necessary + +type TestItem = { + _id: string; + _updatedAt?: Date; +}; + +describe('RecordList', () => { + let recordList: RecordList; + + beforeEach(() => { + recordList = new RecordList(); + recordList.emit = jest.fn(); + }); + + test('should initialize with loading phase', () => { + expect(recordList.phase).toBe(AsyncStatePhase.LOADING); + expect(recordList.items).toEqual([]); + }); + + it('should insert a new item and emit an "inserted" event', async () => { + const item = { _id: '1', _updatedAt: new Date() }; + await recordList.handle(item); + + expect(recordList.items).toContainEqual(item); + expect(recordList.emit).toHaveBeenCalledWith('1/inserted', item); + }); + + it('should update an existing item and emit an "updated" event', async () => { + const item = { _id: '1', _updatedAt: new Date() }; + await recordList.handle(item); + + const updatedItem = { _id: '1', _updatedAt: new Date() }; + await recordList.handle(updatedItem); + + expect(recordList.items).toContainEqual(updatedItem); + expect(recordList.items.length).toBe(1); + expect(recordList.emit).toHaveBeenCalledWith('1/updated', updatedItem); + }); + + it('should delete an item and emit a "deleted" event', async () => { + const item = { _id: '1', _updatedAt: new Date() }; + await recordList.handle(item); + await recordList.remove('1'); + + expect(recordList.items).not.toContainEqual(item); + expect(recordList.emit).toHaveBeenCalledWith('1/deleted'); + }); + + it('should emit "errored" event if an error occurs during mutation', async () => { + const error = new Error('Mutation error'); + const getInfo = jest.fn().mockRejectedValue(error); + + await recordList.batchHandle(getInfo); + + expect(recordList.emit).toHaveBeenCalledWith('errored', error); + }); + + test('should batch handle multiple items', async () => { + const changes = { + items: [ + { _id: '1', _updatedAt: new Date() }, + { _id: '2', _updatedAt: new Date() }, + ], + itemCount: 2, + }; + + const getInfo = jest.fn().mockResolvedValue(changes); + + await recordList.batchHandle(getInfo); + + expect(recordList.items).toEqual(changes.items); + expect(recordList.itemCount).toBe(changes.itemCount); + expect(recordList.emit).toHaveBeenCalledWith('1/inserted', changes.items[0]); + expect(recordList.emit).toHaveBeenCalledWith('2/inserted', changes.items[1]); + expect(recordList.emit).toHaveBeenCalledWith('mutated', true); + }); + + test('should fallback to index count if itemCount is not present', async () => { + const batchData = async () => ({ + items: [ + { _id: '1', _updatedAt: new Date() }, + { _id: '2', _updatedAt: new Date() }, + ], + }); + await recordList.batchHandle(batchData); + expect(recordList.itemCount).toBe(2); + }); + + test('should consider itemCount even if value is 0', async () => { + const batchData = async () => ({ + items: [ + { _id: '1', _updatedAt: new Date() }, + { _id: '2', _updatedAt: new Date() }, + ], + itemCount: 0, + }); + await recordList.batchHandle(batchData); + expect(recordList.itemCount).toBe(0); + }); + + test('should clear all items and emit cleared event', async () => { + const item = { _id: '1', _updatedAt: new Date() }; + + await await recordList.handle(item); + await recordList.clear(); + + expect(recordList.items).toEqual([]); + expect(recordList.itemCount).toBe(0); + expect(recordList.items.length).toBe(0); + expect(recordList.emit).toHaveBeenCalledWith('cleared'); + }); + + it('should prune items based on match criteria and emit delete events', async () => { + const item1 = { _id: '1', _updatedAt: new Date() }; + const item2 = { _id: '2', _updatedAt: new Date() }; + await await recordList.handle(item1); + await await recordList.handle(item2); + + const matchCriteria = (item: TestItem) => item._id === '1'; + await recordList.prune(matchCriteria); + + expect(recordList.items).not.toContainEqual(item1); + expect(recordList.emit).toHaveBeenCalledWith('1/deleted'); + expect(recordList.items).toContainEqual(item2); + }); + + test('should sort items based on _updatedAt', async () => { + const oldItem = { _id: '2', _updatedAt: new Date(Date.now() - 1000) }; + await await recordList.handle(oldItem); + + const newItem = { _id: '1', _updatedAt: new Date() }; + await await recordList.handle(newItem); + + expect(recordList.items[0]).toBe(newItem); + }); +}); diff --git a/apps/meteor/client/lib/lists/RecordList.ts b/apps/meteor/client/lib/lists/RecordList.ts index d4f802f3267b6..fed03c272e85f 100644 --- a/apps/meteor/client/lib/lists/RecordList.ts +++ b/apps/meteor/client/lib/lists/RecordList.ts @@ -121,7 +121,7 @@ export class RecordList extends Em } } - if (info.itemCount) { + if (Number.isInteger(info.itemCount)) { this.#itemCount = info.itemCount; this.#hasChanges = true; } diff --git a/apps/meteor/client/lib/loginServices.ts b/apps/meteor/client/lib/loginServices.ts index ad5ee926ccc78..740e7a1f966c6 100644 --- a/apps/meteor/client/lib/loginServices.ts +++ b/apps/meteor/client/lib/loginServices.ts @@ -49,7 +49,11 @@ class LoginServices extends Emitter { if (state === 'loaded') { this.retries = 0; - this.emit('loaded', services); + try { + this.emit('loaded', services); + } catch (e) { + console.error('Failed to apply loaded listed of login services.', e); + } } } @@ -113,13 +117,15 @@ class LoginServices extends Emitter { return this.serviceButtons; } - public onLoad(callback: (services: LoginServiceConfiguration[]) => void) { + public onLoad(callback: (services: LoginServiceConfiguration[]) => void): () => void { if (this.ready) { - return callback(this.services); + callback(this.services); + return () => undefined; } void this.loadServices(); this.once('loaded', callback); + return () => this.off('loaded', callback); } public async loadServices(): Promise { diff --git a/apps/meteor/client/lib/notificationManager.ts b/apps/meteor/client/lib/notificationManager.ts new file mode 100644 index 0000000000000..4ee07ed7cea51 --- /dev/null +++ b/apps/meteor/client/lib/notificationManager.ts @@ -0,0 +1,6 @@ +import { Emitter } from '@rocket.chat/emitter'; + +class NotificationPermissionEmitter extends Emitter { + allowed: boolean; +} +export const notificationManager = new NotificationPermissionEmitter(); diff --git a/apps/meteor/client/lib/presence.ts b/apps/meteor/client/lib/presence.ts index dbaddcbe405b5..ebd42bc41abd9 100644 --- a/apps/meteor/client/lib/presence.ts +++ b/apps/meteor/client/lib/presence.ts @@ -1,4 +1,4 @@ -import type { IUser } from '@rocket.chat/core-typings'; +import type { IUser, UserPresence } from '@rocket.chat/core-typings'; import { UserStatus } from '@rocket.chat/core-typings'; import type { EventHandlerOf } from '@rocket.chat/emitter'; import { Emitter } from '@rocket.chat/emitter'; @@ -22,10 +22,6 @@ const emitter = new Emitter(); const store = new Map(); -export type UserPresence = Readonly< - Partial> & Required> ->; - const isUid = (eventType: keyof Events): eventType is UserPresence['_id'] => Boolean(eventType) && typeof eventType === 'string' && !['reset', 'restart', 'remove'].includes(eventType); diff --git a/apps/meteor/client/lib/queryKeys.ts b/apps/meteor/client/lib/queryKeys.ts index a9217935b8219..1691403d59fe5 100644 --- a/apps/meteor/client/lib/queryKeys.ts +++ b/apps/meteor/client/lib/queryKeys.ts @@ -8,9 +8,19 @@ export const roomsQueryKeys = { messages: (rid: IRoom['_id']) => [...roomsQueryKeys.room(rid), 'messages'] as const, message: (rid: IRoom['_id'], mid: IMessage['_id']) => [...roomsQueryKeys.messages(rid), mid] as const, threads: (rid: IRoom['_id']) => [...roomsQueryKeys.room(rid), 'threads'] as const, + roles: (rid: IRoom['_id']) => [...roomsQueryKeys.room(rid), 'roles'] as const, }; export const subscriptionsQueryKeys = { all: ['subscriptions'] as const, subscription: (rid: IRoom['_id']) => [...subscriptionsQueryKeys.all, { rid }] as const, }; + +export const cannedResponsesQueryKeys = { + all: ['canned-responses'] as const, +}; + +export const rolesQueryKeys = { + all: ['roles'] as const, + userRoles: () => [...rolesQueryKeys.all, 'user-roles'] as const, +}; diff --git a/apps/meteor/client/lib/rooms/roomTypes/livechat.ts b/apps/meteor/client/lib/rooms/roomTypes/livechat.ts index 23a813d6b81a6..4cede49f0065b 100644 --- a/apps/meteor/client/lib/rooms/roomTypes/livechat.ts +++ b/apps/meteor/client/lib/rooms/roomTypes/livechat.ts @@ -39,7 +39,7 @@ roomCoordinator.add( case UiTextContext.HIDE_WARNING: return 'Hide_Livechat_Warning'; case UiTextContext.LEAVE_WARNING: - return 'Hide_Livechat_Warning'; + return 'Leave_Livechat_Warning'; default: return ''; } diff --git a/apps/meteor/client/lib/settings/PrivateSettingsCachedCollection.ts b/apps/meteor/client/lib/settings/PrivateSettingsCachedCollection.ts index 1b170c82e7d87..74cb76ac9002a 100644 --- a/apps/meteor/client/lib/settings/PrivateSettingsCachedCollection.ts +++ b/apps/meteor/client/lib/settings/PrivateSettingsCachedCollection.ts @@ -1,9 +1,9 @@ import type { ISetting } from '@rocket.chat/core-typings'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; -import { CachedCollection } from '../cachedCollections'; +import { PrivateCachedCollection } from '../cachedCollections'; -class PrivateSettingsCachedCollection extends CachedCollection { +class PrivateSettingsCachedCollection extends PrivateCachedCollection { constructor() { super({ name: 'private-settings', diff --git a/apps/meteor/client/lib/settings/PublicSettingsCachedCollection.ts b/apps/meteor/client/lib/settings/PublicSettingsCachedCollection.ts index 6d01d13d96175..bc12c13997c22 100644 --- a/apps/meteor/client/lib/settings/PublicSettingsCachedCollection.ts +++ b/apps/meteor/client/lib/settings/PublicSettingsCachedCollection.ts @@ -1,13 +1,12 @@ import type { ISetting } from '@rocket.chat/core-typings'; -import { CachedCollection } from '../cachedCollections'; +import { PublicCachedCollection } from '../cachedCollections/CachedCollection'; -class PublicSettingsCachedCollection extends CachedCollection { +class PublicSettingsCachedCollection extends PublicCachedCollection { constructor() { super({ name: 'public-settings', eventType: 'notify-all', - userRelated: false, }); } } diff --git a/apps/meteor/client/lib/utils/legacyJumpToMessage.ts b/apps/meteor/client/lib/utils/legacyJumpToMessage.ts index 9ae917e8cae9a..8dc2e2fdde319 100644 --- a/apps/meteor/client/lib/utils/legacyJumpToMessage.ts +++ b/apps/meteor/client/lib/utils/legacyJumpToMessage.ts @@ -15,7 +15,6 @@ export const legacyJumpToMessage = async (message: IMessage) => { if (tab === 'thread' && (context === message.tmid || context === message._id)) { return; } - router.navigate( { name: router.getRouteName()!, @@ -32,17 +31,17 @@ export const legacyJumpToMessage = async (message: IMessage) => { }, { replace: false }, ); + await RoomHistoryManager.getSurroundingMessages(message); + return; } if (RoomManager.opened === message.rid) { - RoomHistoryManager.getSurroundingMessages(message); + await RoomHistoryManager.getSurroundingMessages(message); return; } await goToRoomById(message.rid); - setTimeout(() => { - RoomHistoryManager.getSurroundingMessages(message); - }, 400); + await RoomHistoryManager.getSurroundingMessages(message); }; diff --git a/apps/meteor/client/omnichannel/additionalForms/CurrentChatTags.tsx b/apps/meteor/client/omnichannel/additionalForms/CurrentChatTags.tsx index 08bb02b8fb0a2..b4c2dde91758a 100644 --- a/apps/meteor/client/omnichannel/additionalForms/CurrentChatTags.tsx +++ b/apps/meteor/client/omnichannel/additionalForms/CurrentChatTags.tsx @@ -2,13 +2,14 @@ import { useHasLicenseModule } from '../../hooks/useHasLicenseModule'; import AutoCompleteTagsMultiple from '../tags/AutoCompleteTagsMultiple'; type CurrentChatTagsProps = { + id?: string; value: Array<{ value: string; label: string }>; handler: (value: { label: string; value: string }[]) => void; department?: string; viewAll?: boolean; }; -const CurrentChatTags = ({ value, handler, department, viewAll }: CurrentChatTagsProps) => { +const CurrentChatTags = ({ id, value, handler, department, viewAll }: CurrentChatTagsProps) => { const hasLicense = useHasLicenseModule('livechat-enterprise'); if (!hasLicense) { @@ -17,6 +18,7 @@ const CurrentChatTags = ({ value, handler, department, viewAll }: CurrentChatTag return ( ({ _id: cannedResponseData?._id || '', shortcut: cannedResponseData?.shortcut || '', text: cannedResponseData?.text || '', - tags: - cannedResponseData?.tags && Array.isArray(cannedResponseData.tags) - ? cannedResponseData.tags.map((tag: string) => ({ label: tag, value: tag })) - : [], + tags: cannedResponseData?.tags || [], scope: cannedResponseData?.scope || 'user', departmentId: cannedResponseData?.departmentId || '', }); @@ -44,7 +32,7 @@ const CreateCannedResponseModal = ({ cannedResponseData, onClose, reloadCannedLi const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); - const methods = useForm({ defaultValues: getInitialData(cannedResponseData) }); + const methods = useForm({ defaultValues: getInitialData(cannedResponseData) }); const { handleSubmit, formState: { isDirty }, @@ -53,7 +41,7 @@ const CreateCannedResponseModal = ({ cannedResponseData, onClose, reloadCannedLi const saveCannedResponse = useEndpoint('POST', '/v1/canned-responses'); const handleCreate = useCallback( - async ({ departmentId, ...data }: CreateCannedResponseModalFormData) => { + async ({ departmentId, ...data }: CannedResponseEditFormData) => { try { await saveCannedResponse({ ...data, @@ -83,9 +71,11 @@ const CreateCannedResponseModal = ({ cannedResponseData, onClose, reloadCannedLi title={cannedResponseData?._id ? t('Edit_Canned_Response') : t('Create_canned_response')} wrapperFunction={(props) => } > - - - + }> + + + + ); }; diff --git a/apps/meteor/client/omnichannel/securityPrivacy/SecurityPrivacyRoute.tsx b/apps/meteor/client/omnichannel/securityPrivacy/SecurityPrivacyRoute.tsx index cef7a6200f4fe..412d7e7e631e7 100644 --- a/apps/meteor/client/omnichannel/securityPrivacy/SecurityPrivacyRoute.tsx +++ b/apps/meteor/client/omnichannel/securityPrivacy/SecurityPrivacyRoute.tsx @@ -4,7 +4,7 @@ import EditableSettingsProvider from '../../views/admin/settings/EditableSetting const SecurityPrivacyRoute = () => { return ( - + diff --git a/apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx b/apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx index d80cc2b913fb1..6a980e6de6bfb 100644 --- a/apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx +++ b/apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx @@ -9,6 +9,7 @@ import { AsyncStatePhase } from '../../hooks/useAsyncState'; import { useTagsList } from '../../hooks/useTagsList'; type AutoCompleteTagsMultipleProps = { + id?: string; value?: PaginatedMultiSelectOption[]; onlyMyTags?: boolean; onChange?: (value: PaginatedMultiSelectOption[]) => void; @@ -17,6 +18,7 @@ type AutoCompleteTagsMultipleProps = { }; const AutoCompleteTagsMultiple = ({ + id, value = [], onlyMyTags = false, onChange = () => undefined, @@ -44,6 +46,7 @@ const AutoCompleteTagsMultiple = ({ return ( { const { openDialModal } = useDialModal(); - const voipSounds = useVoipSounds(); + const { voipSounds } = useCustomSound(); const closeRoom = useCallback( async ( @@ -336,7 +336,9 @@ export const CallProvider = ({ children }: CallProviderProps) => { if (!callDetails.callInfo) { return; } + voipSounds.stopAll(); + if (callDetails.userState !== UserState.UAC) { return; } @@ -377,15 +379,15 @@ export const CallProvider = ({ children }: CallProviderProps) => { }; const onRinging = (): void => { - voipSounds.play('outbound-call-ringing'); + voipSounds.playDialer(); }; const onIncomingCallRinging = (): void => { - voipSounds.play('telephone'); + voipSounds.playRinger(); }; const onCallTerminated = (): void => { - voipSounds.play('call-ended', false); + voipSounds.playCallEnded(); voipSounds.stopAll(); }; diff --git a/apps/meteor/client/providers/CallProvider/hooks/useVoipSounds.ts b/apps/meteor/client/providers/CallProvider/hooks/useVoipSounds.ts deleted file mode 100644 index 7eea3b867f507..0000000000000 --- a/apps/meteor/client/providers/CallProvider/hooks/useVoipSounds.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useCustomSound } from '@rocket.chat/ui-contexts'; -import { useMemo } from 'react'; - -import { useUserSoundPreferences } from '../../../hooks/useUserSoundPreferences'; - -type VoipSound = 'telephone' | 'outbound-call-ringing' | 'call-ended'; - -export const useVoipSounds = () => { - const { play, pause } = useCustomSound(); - const { voipRingerVolume } = useUserSoundPreferences(); - - return useMemo( - () => ({ - play: (soundId: VoipSound, loop = true) => { - play(soundId, { - volume: Number((voipRingerVolume / 100).toPrecision(2)), - loop, - }); - }, - stop: (soundId: VoipSound) => pause(soundId), - stopAll: () => { - pause('telephone'); - pause('outbound-call-ringing'); - }, - }), - [play, pause, voipRingerVolume], - ); -}; diff --git a/apps/meteor/client/providers/CustomSoundProvider.tsx b/apps/meteor/client/providers/CustomSoundProvider.tsx deleted file mode 100644 index 454b16196422f..0000000000000 --- a/apps/meteor/client/providers/CustomSoundProvider.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { CustomSoundContext, useUserId, useStream } from '@rocket.chat/ui-contexts'; -import type { ReactNode } from 'react'; -import { useEffect } from 'react'; - -import { CustomSounds } from '../../app/custom-sounds/client/lib/CustomSounds'; - -type CustomSoundProviderProps = { - children?: ReactNode; -}; - -const CustomSoundProvider = ({ children }: CustomSoundProviderProps) => { - const userId = useUserId(); - useEffect(() => { - if (!userId) { - return; - } - void CustomSounds.fetchCustomSoundList(); - }, [userId]); - - const streamAll = useStream('notify-all'); - - useEffect(() => { - if (!userId) { - return; - } - - return streamAll('public-info', ([key, data]) => { - switch (key) { - case 'updateCustomSound': - CustomSounds.update(data[0].soundData); - break; - case 'deleteCustomSound': - CustomSounds.remove(data[0].soundData); - break; - } - }); - }, [userId, streamAll]); - return ; -}; - -export default CustomSoundProvider; diff --git a/apps/meteor/client/providers/CustomSoundProvider/CustomSoundProvider.tsx b/apps/meteor/client/providers/CustomSoundProvider/CustomSoundProvider.tsx new file mode 100644 index 0000000000000..82c12f4febb83 --- /dev/null +++ b/apps/meteor/client/providers/CustomSoundProvider/CustomSoundProvider.tsx @@ -0,0 +1,130 @@ +import type { ICustomSound } from '@rocket.chat/core-typings'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { CustomSoundContext, useStream, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useEffect, useMemo, useRef, type ReactNode } from 'react'; + +import { defaultSounds, formatVolume, getCustomSoundURL } from './lib/helpers'; +import { sdk } from '../../../app/utils/client/lib/SDKClient'; +import { useUserSoundPreferences } from '../../hooks/useUserSoundPreferences'; + +type CustomSoundProviderProps = { + children?: ReactNode; +}; + +const CustomSoundProvider = ({ children }: CustomSoundProviderProps) => { + const audioRefs = useRef([]); + + const queryClient = useQueryClient(); + const streamAll = useStream('notify-all'); + + const newRoomNotification = useUserPreference('newRoomNotification') || 'door'; + const newMessageNotification = useUserPreference('newMessageNotification') || 'chime'; + const { notificationsSoundVolume, voipRingerVolume } = useUserSoundPreferences(); + + const { data: list } = useQuery({ + queryFn: async () => { + const customSoundsList = await sdk.call('listCustomSounds'); + if (!customSoundsList.length) { + return defaultSounds; + } + return [...customSoundsList.map((sound) => ({ ...sound, src: getCustomSoundURL(sound) })), ...defaultSounds]; + }, + queryKey: ['listCustomSounds'], + initialData: defaultSounds, + }); + + const play = useEffectEvent((soundId: ICustomSound['_id'], { volume = 1, loop = false } = {}) => { + stop(soundId); + + const item = list?.find(({ _id }) => _id === soundId); + if (!item?.src) { + console.error('Unable to play sound', soundId); + return; + } + + const audio = new Audio(item.src); + audio.volume = volume; + audio.loop = loop; + audio.id = soundId; + audio.play(); + + audioRefs.current = [...audioRefs.current, audio]; + + return () => { + stop(soundId); + }; + }); + + const pause = useEffectEvent((soundId: ICustomSound['_id']) => { + const current = audioRefs.current?.find(({ id }) => id === soundId); + if (current) { + current.pause(); + audioRefs.current = audioRefs.current.filter(({ id }) => id !== soundId); + } + }); + + const stop = useEffectEvent((soundId: ICustomSound['_id']) => { + const current = audioRefs.current?.find(({ id }) => id === soundId); + if (current) { + current.load(); + audioRefs.current = audioRefs.current.filter(({ id }) => id !== soundId); + } + }); + + const contextValue = useMemo(() => { + const notificationSounds = { + playNewRoom: () => play(newRoomNotification, { loop: false, volume: formatVolume(notificationsSoundVolume) }), + playNewMessage: () => play(newMessageNotification, { loop: false, volume: formatVolume(notificationsSoundVolume) }), + playNewMessageLoop: () => play(newMessageNotification, { loop: true, volume: formatVolume(notificationsSoundVolume) }), + stopNewRoom: () => stop(newRoomNotification), + stopNewMessage: () => stop(newMessageNotification), + }; + const voipSounds = { + playRinger: () => play('telephone', { loop: true, volume: formatVolume(voipRingerVolume) }), + playDialer: () => play('outbound-call-ringing', { loop: true, volume: formatVolume(voipRingerVolume) }), + playCallEnded: () => play('call-ended', { loop: false, volume: formatVolume(voipRingerVolume) }), + stopRinger: () => stop('telephone'), + stopDialer: () => stop('outbound-call-ringing'), + stopCallEnded: () => stop('call-ended'), + stopAll: () => { + stop('telephone'); + stop('outbound-call-ringing'); + stop('call-ended'); + }, + }; + const callSounds = { + playRinger: () => play('ringtone', { loop: true, volume: formatVolume(voipRingerVolume) }), + playDialer: () => play('dialtone', { loop: true, volume: formatVolume(voipRingerVolume) }), + stopRinger: () => stop('ringtone'), + stopDialer: () => stop('dialtone'), + }; + return { + list, + notificationSounds, + callSounds, + voipSounds, + play, + pause, + stop, + }; + }, [list, newMessageNotification, newRoomNotification, notificationsSoundVolume, pause, play, stop, voipRingerVolume]); + + useEffect(() => { + return streamAll('public-info', ([key]) => { + switch (key) { + case 'updateCustomSound': + queryClient.invalidateQueries({ queryKey: ['listCustomSounds'] }); + break; + case 'deleteCustomSound': + queryClient.invalidateQueries({ queryKey: ['listCustomSounds'] }); + + break; + } + }); + }, [queryClient, streamAll]); + + return ; +}; + +export default CustomSoundProvider; diff --git a/apps/meteor/client/providers/CustomSoundProvider/index.ts b/apps/meteor/client/providers/CustomSoundProvider/index.ts new file mode 100644 index 0000000000000..a2e563004d267 --- /dev/null +++ b/apps/meteor/client/providers/CustomSoundProvider/index.ts @@ -0,0 +1 @@ +export { default } from './CustomSoundProvider'; diff --git a/apps/meteor/client/providers/CustomSoundProvider/lib/helpers.ts b/apps/meteor/client/providers/CustomSoundProvider/lib/helpers.ts new file mode 100644 index 0000000000000..48e955e5dc2ce --- /dev/null +++ b/apps/meteor/client/providers/CustomSoundProvider/lib/helpers.ts @@ -0,0 +1,29 @@ +import type { ICustomSound } from '@rocket.chat/core-typings'; + +import { getURL } from '../../../../app/utils/client'; + +export const getAssetUrl = (asset: string, params?: Record) => getURL(asset, params, undefined, true); + +export const getCustomSoundURL = (sound: ICustomSound) => { + return getAssetUrl(`/custom-sounds/${sound._id}.${sound.extension}`, { _dc: sound.random || 0 }); +}; + +export const defaultSounds: ICustomSound[] = [ + { _id: 'chime', name: 'Chime', extension: 'mp3', src: getAssetUrl('sounds/chime.mp3') }, + { _id: 'door', name: 'Door', extension: 'mp3', src: getAssetUrl('sounds/door.mp3') }, + { _id: 'beep', name: 'Beep', extension: 'mp3', src: getAssetUrl('sounds/beep.mp3') }, + { _id: 'chelle', name: 'Chelle', extension: 'mp3', src: getAssetUrl('sounds/chelle.mp3') }, + { _id: 'ding', name: 'Ding', extension: 'mp3', src: getAssetUrl('sounds/ding.mp3') }, + { _id: 'droplet', name: 'Droplet', extension: 'mp3', src: getAssetUrl('sounds/droplet.mp3') }, + { _id: 'highbell', name: 'Highbell', extension: 'mp3', src: getAssetUrl('sounds/highbell.mp3') }, + { _id: 'seasons', name: 'Seasons', extension: 'mp3', src: getAssetUrl('sounds/seasons.mp3') }, + { _id: 'telephone', name: 'Telephone', extension: 'mp3', src: getAssetUrl('sounds/telephone.mp3') }, + { _id: 'outbound-call-ringing', name: 'Outbound Call Ringing', extension: 'mp3', src: getAssetUrl('sounds/outbound-call-ringing.mp3') }, + { _id: 'call-ended', name: 'Call Ended', extension: 'mp3', src: getAssetUrl('sounds/call-ended.mp3') }, + { _id: 'dialtone', name: 'Dialtone', extension: 'mp3', src: getAssetUrl('sounds/dialtone.mp3') }, + { _id: 'ringtone', name: 'Ringtone', extension: 'mp3', src: getAssetUrl('sounds/ringtone.mp3') }, +]; + +export const formatVolume = (volume: number) => { + return Number((volume / 100).toPrecision(2)); +}; diff --git a/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.tsx b/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.tsx index bf3207bbb8d1d..70ed30697649b 100644 --- a/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.tsx +++ b/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.tsx @@ -1,4 +1,4 @@ -import { useDebouncedState, useLocalStorage } from '@rocket.chat/fuselage-hooks'; +import { useDebouncedState, useEffectEvent, useLocalStorage } from '@rocket.chat/fuselage-hooks'; import type { ReactNode, ReactElement, ContextType } from 'react'; import { useState, useCallback, useMemo, useSyncExternalStore } from 'react'; @@ -23,13 +23,14 @@ const EmojiPickerProvider = ({ children }: { children: ReactNode }): ReactElemen const [customItemsLimit, setCustomItemsLimit] = useState(DEFAULT_ITEMS_LIMIT); - const [quickReactions, setQuickReactions] = useState<{ emoji: string; image: string }[]>(() => + const [quickReactions, _setQuickReactions] = useState<{ emoji: string; image: string }[]>(() => getFrequentEmoji(frequentEmojis.map(([emoji]) => emoji)), ); + const setQuickReactions = useEffectEvent(() => _setQuickReactions(getFrequentEmoji(frequentEmojis.map(([emoji]) => emoji)))); const [sub, getSnapshot] = useMemo(() => { - return createEmojiListByCategorySubscription(customItemsLimit, actualTone, recentEmojis, setRecentEmojis); - }, [customItemsLimit, actualTone, recentEmojis, setRecentEmojis]); + return createEmojiListByCategorySubscription(customItemsLimit, actualTone, recentEmojis, setRecentEmojis, setQuickReactions); + }, [customItemsLimit, actualTone, recentEmojis, setRecentEmojis, setQuickReactions]); const [emojiListByCategory, categoriesIndexes] = useSyncExternalStore(sub, getSnapshot); @@ -46,7 +47,7 @@ const EmojiPickerProvider = ({ children }: { children: ReactNode }): ReactElemen .sort(([, frequentA], [, frequentB]) => frequentB - frequentA); setFrequentEmojis(sortedFrequent); - setQuickReactions(getFrequentEmoji(sortedFrequent.map(([emoji]) => emoji))); + _setQuickReactions(getFrequentEmoji(sortedFrequent.map(([emoji]) => emoji))); }, [frequentEmojis, setFrequentEmojis], ); diff --git a/apps/meteor/client/providers/EmojiPickerProvider/useUpdateCustomEmoji.ts b/apps/meteor/client/providers/EmojiPickerProvider/useUpdateCustomEmoji.ts index 67d9f5bd20780..19c13fbce33c2 100644 --- a/apps/meteor/client/providers/EmojiPickerProvider/useUpdateCustomEmoji.ts +++ b/apps/meteor/client/providers/EmojiPickerProvider/useUpdateCustomEmoji.ts @@ -1,7 +1,7 @@ import { useStream, useUserId } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; -import { updateEmojiCustom, deleteEmojiCustom } from '../../../app/emoji-custom/client/lib/emojiCustom'; +import { updateEmojiCustom, deleteEmojiCustom } from '../../lib/customEmoji'; export const useUpdateCustomEmoji = () => { const notify = useStream('notify-logged'); diff --git a/apps/meteor/client/providers/LayoutProvider.tsx b/apps/meteor/client/providers/LayoutProvider.tsx index 46fd033667561..92b5b5d7871ff 100644 --- a/apps/meteor/client/providers/LayoutProvider.tsx +++ b/apps/meteor/client/providers/LayoutProvider.tsx @@ -1,4 +1,5 @@ import { useBreakpoints } from '@rocket.chat/fuselage-hooks'; +import { useFeaturePreview } from '@rocket.chat/ui-client'; import { LayoutContext, useRouter, useSetting } from '@rocket.chat/ui-contexts'; import type { ReactNode } from 'react'; import { useMemo, useState, useEffect } from 'react'; @@ -17,18 +18,23 @@ type LayoutProviderProps = { const LayoutProvider = ({ children }: LayoutProviderProps) => { const showTopNavbarEmbeddedLayout = useSetting('UI_Show_top_navbar_embedded_layout', false); const [isCollapsed, setIsCollapsed] = useState(false); + const [navBarSearchExpanded, setNavBarSearchExpanded] = useState(false); const breakpoints = useBreakpoints(); // ["xs", "sm", "md", "lg", "xl", xxl"] const [hiddenActions, setHiddenActions] = useState(hiddenActionsDefaultValue); + const enhancedNavigationEnabled = useFeaturePreview('newNavigation'); const router = useRouter(); // Once the layout is embedded, it can't be changed const [isEmbedded] = useState(() => router.getSearchParameters().layout === 'embedded'); const isMobile = !breakpoints.includes('md'); + const isTablet = !breakpoints.includes('lg'); + + const shouldToggle = enhancedNavigationEnabled ? isTablet || isMobile : isMobile; useEffect(() => { - setIsCollapsed(isMobile); - }, [isMobile]); + setIsCollapsed(shouldToggle); + }, [shouldToggle]); useEffect(() => { const eventHandler = (event: MessageEvent) => { @@ -48,11 +54,17 @@ const LayoutProvider = ({ children }: LayoutProviderProps) => { value={useMemo( () => ({ isMobile, + isTablet, isEmbedded, showTopNavbarEmbeddedLayout, + navbar: { + searchExpanded: navBarSearchExpanded, + expandSearch: isMobile ? () => setNavBarSearchExpanded(true) : undefined, + collapseSearch: isMobile ? () => setNavBarSearchExpanded(false) : undefined, + }, sidebar: { isCollapsed, - toggle: isMobile ? () => setIsCollapsed((isCollapsed) => !isCollapsed) : () => undefined, + toggle: shouldToggle ? () => setIsCollapsed((isCollapsed) => !isCollapsed) : () => undefined, collapse: () => setIsCollapsed(true), expand: () => setIsCollapsed(false), close: () => (isEmbedded ? setIsCollapsed(true) : router.navigate('/home')), @@ -68,7 +80,18 @@ const LayoutProvider = ({ children }: LayoutProviderProps) => { contextualBarPosition: breakpoints.includes('sm') ? (breakpoints.includes('lg') ? 'relative' : 'absolute') : 'fixed', hiddenActions, }), - [isMobile, isEmbedded, showTopNavbarEmbeddedLayout, isCollapsed, breakpoints, router, hiddenActions], + [ + isMobile, + isTablet, + navBarSearchExpanded, + isEmbedded, + showTopNavbarEmbeddedLayout, + isCollapsed, + shouldToggle, + breakpoints, + hiddenActions, + router, + ], )} /> ); diff --git a/apps/meteor/client/providers/MeteorProvider.tsx b/apps/meteor/client/providers/MeteorProvider.tsx index 704a4dce2eece..b3bed574fd046 100644 --- a/apps/meteor/client/providers/MeteorProvider.tsx +++ b/apps/meteor/client/providers/MeteorProvider.tsx @@ -38,9 +38,9 @@ const MeteorProvider = ({ children }: MeteorProviderProps) => ( - - - + + + @@ -66,9 +66,9 @@ const MeteorProvider = ({ children }: MeteorProviderProps) => ( - - - + + + diff --git a/apps/meteor/client/providers/OmnichannelProvider.tsx b/apps/meteor/client/providers/OmnichannelProvider.tsx index ed06c15d68736..ee0f9e6fb0e22 100644 --- a/apps/meteor/client/providers/OmnichannelProvider.tsx +++ b/apps/meteor/client/providers/OmnichannelProvider.tsx @@ -2,25 +2,24 @@ import { type IOmnichannelAgent, type OmichannelRoutingConfig, OmnichannelSortingMechanismSettingType, - type ILivechatInquiryRecord, LivechatInquiryStatus, } from '@rocket.chat/core-typings'; import { useSafely } from '@rocket.chat/fuselage-hooks'; -import { useUser, useSetting, usePermission, useMethod, useEndpoint, useStream } from '@rocket.chat/ui-contexts'; +import { createComparatorFromSort } from '@rocket.chat/mongo-adapter'; +import { useUser, useSetting, usePermission, useMethod, useEndpoint, useStream, useCustomSound } from '@rocket.chat/ui-contexts'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import type { ReactNode } from 'react'; -import { useState, useEffect, useMemo, useCallback, memo, useRef } from 'react'; +import { useState, useEffect, useMemo, memo, useRef } from 'react'; +import { useShallow } from 'zustand/shallow'; -import { LivechatInquiry } from '../../app/livechat/client/collections/LivechatInquiry'; import { initializeLivechatInquiryStream } from '../../app/livechat/client/lib/stream/queueManager'; import { getOmniChatSortQuery } from '../../app/livechat/lib/inquiries'; -import { KonchatNotification } from '../../app/ui/client/lib/KonchatNotification'; import { ClientLogger } from '../../lib/ClientLogger'; import type { OmnichannelContextValue } from '../contexts/OmnichannelContext'; import { OmnichannelContext } from '../contexts/OmnichannelContext'; import { useHasLicenseModule } from '../hooks/useHasLicenseModule'; +import { useLivechatInquiryStore } from '../hooks/useLivechatInquiryStore'; import { useOmnichannelContinuousSoundNotification } from '../hooks/useOmnichannelContinuousSoundNotification'; -import { useReactiveValue } from '../hooks/useReactiveValue'; import { useShouldPreventAction } from '../hooks/useShouldPreventAction'; const emptyContextValue: OmnichannelContextValue = { @@ -76,6 +75,8 @@ const OmnichannelProvider = ({ children }: OmnichannelProviderProps) => { const isPrioritiesEnabled = isEnterprise && accessible; const enabled = accessible && !!user && !!routeConfig; + const { notificationSounds } = useCustomSound(); + const { data: { priorities = [] } = {}, isLoading: isLoadingPriorities, @@ -140,28 +141,29 @@ const OmnichannelProvider = ({ children }: OmnichannelProviderProps) => { return streamNotifyUser(`${user._id}/departmentAgentData`, handleDepartmentAgentData); }, [manuallySelected, streamNotifyUser, user?._id]); - const queue = useReactiveValue( - useCallback(() => { + const queue = useLivechatInquiryStore( + useShallow((state) => { if (!manuallySelected) { return undefined; } - return LivechatInquiry.find( - { status: LivechatInquiryStatus.QUEUED }, - { - sort: getOmniChatSortQuery(omnichannelSortingMechanism), - limit: omnichannelPoolMaxIncoming, - }, - ).fetch(); - }, [manuallySelected, omnichannelPoolMaxIncoming, omnichannelSortingMechanism]), + return state.records + .filter((inquiry) => inquiry.status === LivechatInquiryStatus.QUEUED) + .sort(createComparatorFromSort(getOmniChatSortQuery(omnichannelSortingMechanism))) + .slice(...(omnichannelPoolMaxIncoming > 0 ? [0, omnichannelPoolMaxIncoming] : [])); + }), ); useEffect(() => { if (lastQueueSize.current < (queue?.length ?? 0)) { - KonchatNotification.newRoom(); + notificationSounds.playNewRoom(); } lastQueueSize.current = queue?.length ?? 0; - }, [queue?.length]); + + return () => { + notificationSounds.stopNewRoom(); + }; + }, [notificationSounds, queue?.length]); useOmnichannelContinuousSoundNotification(queue ?? []); diff --git a/apps/meteor/client/providers/ServerProvider.tsx b/apps/meteor/client/providers/ServerProvider.tsx index d6f6920016c43..85df8de5f6e51 100644 --- a/apps/meteor/client/providers/ServerProvider.tsx +++ b/apps/meteor/client/providers/ServerProvider.tsx @@ -75,6 +75,8 @@ const contextValue = { callEndpoint, uploadToEndpoint, getStream, + disconnect: () => Meteor.disconnect(), + reconnect: () => Meteor.reconnect(), }; type ServerProviderProps = { children?: ReactNode }; diff --git a/apps/meteor/client/providers/SettingsProvider.tsx b/apps/meteor/client/providers/SettingsProvider.tsx index b426b283c74aa..96f7c9c970fed 100644 --- a/apps/meteor/client/providers/SettingsProvider.tsx +++ b/apps/meteor/client/providers/SettingsProvider.tsx @@ -4,52 +4,30 @@ import { SettingsContext, useAtLeastOnePermission, useMethod } from '@rocket.cha import { useQueryClient } from '@tanstack/react-query'; import { Tracker } from 'meteor/tracker'; import type { ReactNode } from 'react'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useMemo } from 'react'; import { createReactiveSubscriptionFactory } from '../lib/createReactiveSubscriptionFactory'; import { PrivateSettingsCachedCollection } from '../lib/settings/PrivateSettingsCachedCollection'; import { PublicSettingsCachedCollection } from '../lib/settings/PublicSettingsCachedCollection'; +const settingsManagementPermissions = ['view-privileged-setting', 'edit-privileged-setting', 'manage-selected-settings']; + type SettingsProviderProps = { children?: ReactNode; - privileged?: boolean; }; -const SettingsProvider = ({ children, privileged = false }: SettingsProviderProps) => { - const hasPrivilegedPermission = useAtLeastOnePermission( - useMemo(() => ['view-privileged-setting', 'edit-privileged-setting', 'manage-selected-settings'], []), - ); - - const hasPrivateAccess = privileged && hasPrivilegedPermission; - - const cachedCollection = useMemo( - () => (hasPrivateAccess ? PrivateSettingsCachedCollection : PublicSettingsCachedCollection), - [hasPrivateAccess], - ); - - const [isLoading, setLoading] = useState(() => Tracker.nonreactive(() => !cachedCollection.ready.get())); - - useEffect(() => { - let mounted = true; - - const initialize = async (): Promise => { - if (!Tracker.nonreactive(() => cachedCollection.ready.get())) { - await cachedCollection.init(); - } +const SettingsProvider = ({ children }: SettingsProviderProps) => { + const canManageSettings = useAtLeastOnePermission(settingsManagementPermissions); - if (!mounted) { - return; - } + const cachedCollection = canManageSettings ? PrivateSettingsCachedCollection : PublicSettingsCachedCollection; - setLoading(false); - }; + const isLoading = Tracker.nonreactive(() => !cachedCollection.ready.get()); - initialize(); - - return (): void => { - mounted = false; - }; - }, [cachedCollection]); + if (isLoading) { + throw (async () => { + await cachedCollection.init(); + })(); + } const querySetting = useMemo( () => @@ -82,6 +60,8 @@ const SettingsProvider = ({ children, privileged = false }: SettingsProviderProp sorter: 1, i18nLabel: 1, }, + ...('skip' in query && typeof query.skip === 'number' && { skip: query.skip }), + ...('limit' in query && typeof query.limit === 'number' && { limit: query.limit }), }, ) .fetch(), @@ -108,19 +88,15 @@ const SettingsProvider = ({ children, privileged = false }: SettingsProviderProp const contextValue = useMemo( () => ({ - hasPrivateAccess, - isLoading, + hasPrivateAccess: canManageSettings, querySetting, querySettings, dispatch, }), - [hasPrivateAccess, isLoading, querySetting, querySettings, dispatch], + [canManageSettings, querySetting, querySettings, dispatch], ); return ; }; export default SettingsProvider; - -// '[subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => {}]' -// '[subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => ISetting | undefined]' diff --git a/apps/meteor/client/providers/UserPresenceProvider.tsx b/apps/meteor/client/providers/UserPresenceProvider.tsx index 04274c5cdbe0b..6936b106e38fc 100644 --- a/apps/meteor/client/providers/UserPresenceProvider.tsx +++ b/apps/meteor/client/providers/UserPresenceProvider.tsx @@ -1,8 +1,8 @@ -import { useSetting } from '@rocket.chat/ui-contexts'; +import type { UserPresenceContextValue } from '@rocket.chat/ui-contexts'; +import { useSetting, UserPresenceContext } from '@rocket.chat/ui-contexts'; import type { ReactElement, ReactNode } from 'react'; import { useMemo, useEffect } from 'react'; -import { UserPresenceContext } from '../contexts/UserPresenceContext'; import { Presence } from '../lib/presence'; type UserPresenceProviderProps = { @@ -16,30 +16,30 @@ const UserPresenceProvider = ({ children }: UserPresenceProviderProps): ReactEle Presence.setStatus(usePresenceDisabled ? 'disabled' : 'enabled'); }, [usePresenceDisabled]); - return ( - ({ - queryUserData: (uid) => { - const subscribe = (callback: () => void) => { - Presence.listen(uid, callback); - - return () => { - Presence.stop(uid, callback); - }; - }; - - const get = () => Presence.store.get(uid); - - return { subscribe, get }; - }, - }), - [], - )} - > - {children} - + const contextValue: UserPresenceContextValue = useMemo( + () => ({ + queryUserData: (uid) => { + if (!uid) { + return { get: () => undefined, subscribe: () => () => undefined }; + } + + const subscribe = (callback: () => void) => { + Presence.listen(uid, callback); + + return () => { + Presence.stop(uid, callback); + }; + }; + + const get = () => Presence.store.get(uid); + + return { subscribe, get }; + }, + }), + [], ); + + return {children}; }; export default UserPresenceProvider; diff --git a/apps/meteor/client/providers/UserProvider/UserProvider.tsx b/apps/meteor/client/providers/UserProvider/UserProvider.tsx index adbbc07fae54f..f2fa76732f19f 100644 --- a/apps/meteor/client/providers/UserProvider/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider/UserProvider.tsx @@ -15,6 +15,7 @@ import { Subscriptions, Rooms } from '../../../app/models/client'; import { getUserPreference } from '../../../app/utils/client'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; import { afterLogoutCleanUpCallback } from '../../../lib/callbacks/afterLogoutCleanUpCallback'; +import { useIdleConnection } from '../../hooks/useIdleConnection'; import { useReactiveValue } from '../../hooks/useReactiveValue'; import { createReactiveSubscriptionFactory } from '../../lib/createReactiveSubscriptionFactory'; import { useCreateFontStyleElement } from '../../views/account/accessibility/hooks/useCreateFontStyleElement'; @@ -62,6 +63,7 @@ const UserProvider = ({ children }: UserProviderProps): ReactElement => { useDeleteUser(); useUpdateAvatar(); + useIdleConnection(userId); const contextValue = useMemo( (): ContextType => ({ diff --git a/apps/meteor/client/sidebar/Sidebar.stories.tsx b/apps/meteor/client/sidebar/Sidebar.stories.tsx index 944bd45943bb3..61b173a754ad1 100644 --- a/apps/meteor/client/sidebar/Sidebar.stories.tsx +++ b/apps/meteor/client/sidebar/Sidebar.stories.tsx @@ -31,7 +31,6 @@ const settings: Record = { const settingContextValue: ContextType = { hasPrivateAccess: true, - isLoading: false, querySetting: (_id) => [() => () => undefined, () => settings[_id]], querySettings: () => [() => () => undefined, () => []], dispatch: async () => undefined, diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx index 1f02f3da31fd5..5734ffc9cac8a 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx @@ -13,6 +13,7 @@ export const useAuditItems = (): GenericMenuItemProps[] => { const auditHomeRoute = useRoute('audit-home'); const auditSettingsRoute = useRoute('audit-log'); + const securityLogsRoute = useRoute('security-logs'); if (!hasAuditPermission && !hasAuditLogPermission) { return []; @@ -31,5 +32,14 @@ export const useAuditItems = (): GenericMenuItemProps[] => { onClick: () => auditSettingsRoute.push(), }; - return [hasAuditPermission && auditMessageItem, hasAuditLogPermission && auditLogItem].filter(Boolean) as GenericMenuItemProps[]; + const securityLogItem: GenericMenuItemProps = { + id: 'securityLog', + icon: 'document-eye', + content: t('Security_logs'), + onClick: () => securityLogsRoute.push(), + }; + + return [hasAuditPermission && auditMessageItem, hasAuditLogPermission && auditLogItem, hasAuditPermission && securityLogItem].filter( + Boolean, + ) as GenericMenuItemProps[]; }; diff --git a/apps/meteor/client/sidebar/header/hooks/useVoipItemsSection.tsx b/apps/meteor/client/sidebar/header/hooks/useVoipItemsSection.tsx index c8b7e89dd2298..d77a78deb457c 100644 --- a/apps/meteor/client/sidebar/header/hooks/useVoipItemsSection.tsx +++ b/apps/meteor/client/sidebar/header/hooks/useVoipItemsSection.tsx @@ -10,7 +10,7 @@ export const useVoipItemsSection = (): { items: GenericMenuItemProps[] } | undef const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); - const { clientError, isEnabled, isReady, isRegistered } = useVoipState(); + const { clientError, isEnabled, isReady, isRegistered, isReconnecting } = useVoipState(); const { register, unregister, onRegisteredOnce, onUnregisteredOnce } = useVoipAPI(); const toggleVoip = useMutation({ @@ -44,8 +44,12 @@ export const useVoipItemsSection = (): { items: GenericMenuItemProps[] } | undef return t('Loading'); } + if (isReconnecting) { + return t('Reconnecting'); + } + return ''; - }, [clientError, isReady, toggleVoip.isPending, t]); + }, [clientError, isReady, toggleVoip.isPending, t, isReconnecting]); return useMemo(() => { if (!isEnabled) { @@ -57,7 +61,7 @@ export const useVoipItemsSection = (): { items: GenericMenuItemProps[] } | undef { id: 'toggle-voip', icon: isRegistered ? 'phone-disabled' : 'phone', - disabled: !isReady || toggleVoip.isPending, + disabled: !isReady || toggleVoip.isPending || isReconnecting, onClick: () => toggleVoip.mutate(), content: ( @@ -67,5 +71,5 @@ export const useVoipItemsSection = (): { items: GenericMenuItemProps[] } | undef }, ], }; - }, [isEnabled, isRegistered, isReady, tooltip, t, toggleVoip]); + }, [isEnabled, isRegistered, isReady, tooltip, t, toggleVoip, isReconnecting]); }; diff --git a/apps/meteor/client/sidebar/hooks/useCustomOAuth.ts b/apps/meteor/client/sidebar/hooks/useCustomOAuth.ts index b6c4acf6176e4..174cb3173433f 100644 --- a/apps/meteor/client/sidebar/hooks/useCustomOAuth.ts +++ b/apps/meteor/client/sidebar/hooks/useCustomOAuth.ts @@ -4,19 +4,21 @@ import { CustomOAuth } from '../../../app/custom-oauth/client/CustomOAuth'; import { loginServices } from '../../lib/loginServices'; export const useCustomOAuth = () => { - useEffect(() => { - loginServices.onLoad((services) => { - for (const service of services) { - if (!('custom' in service && service.custom)) { - continue; - } + useEffect( + () => + loginServices.onLoad((services) => { + for (const service of services) { + if (!('custom' in service && service.custom)) { + continue; + } - new CustomOAuth(service.service, { - serverURL: service.serverURL, - authorizePath: service.authorizePath, - scope: service.scope, - }); - } - }); - }, []); + CustomOAuth.configureCustomOAuthService(service.service, { + serverURL: service.serverURL, + authorizePath: service.authorizePath, + scope: service.scope, + }); + } + }), + [], + ); }; diff --git a/apps/meteor/client/sidebarv2/Sidebar.stories.tsx b/apps/meteor/client/sidebarv2/Sidebar.stories.tsx index 561ca0aba8ac1..fcf7037c518cb 100644 --- a/apps/meteor/client/sidebarv2/Sidebar.stories.tsx +++ b/apps/meteor/client/sidebarv2/Sidebar.stories.tsx @@ -31,7 +31,6 @@ const settings: Record = { const settingContextValue: ContextType = { hasPrivateAccess: true, - isLoading: false, querySetting: (_id) => [() => () => undefined, () => settings[_id]], querySettings: () => [() => () => undefined, () => []], dispatch: async () => undefined, diff --git a/apps/meteor/client/sidebarv2/Sidebar.tsx b/apps/meteor/client/sidebarv2/Sidebar.tsx index e292a1a0da80d..278c8b4f9d589 100644 --- a/apps/meteor/client/sidebarv2/Sidebar.tsx +++ b/apps/meteor/client/sidebarv2/Sidebar.tsx @@ -4,7 +4,6 @@ import { memo } from 'react'; import SidebarRoomList from './RoomList'; import SidebarFooter from './footer'; -import SearchSection from './header/SearchSection'; import BannerSection from './sections/BannerSection'; const Sidebar = () => { @@ -18,7 +17,6 @@ const Sidebar = () => { .filter(Boolean) .join(' ')} > - diff --git a/apps/meteor/client/sidebarv2/SidebarRegion.tsx b/apps/meteor/client/sidebarv2/SidebarRegion.tsx index 9a09cabfa7be1..83eb4e750c6da 100644 --- a/apps/meteor/client/sidebarv2/SidebarRegion.tsx +++ b/apps/meteor/client/sidebarv2/SidebarRegion.tsx @@ -7,7 +7,7 @@ import { FocusScope } from 'react-aria'; import Sidebar from './Sidebar'; const SidebarRegion = () => { - const { isMobile, sidebar } = useLayout(); + const { isTablet, sidebar } = useLayout(); const sidebarMobileClass = css` position: absolute; @@ -93,14 +93,14 @@ const SidebarRegion = () => { - {isMobile && ( - sidebar.toggle()}> + {isTablet && ( + sidebar.toggle()} /> )} ); diff --git a/apps/meteor/client/sidebarv2/header/SearchList.tsx b/apps/meteor/client/sidebarv2/header/SearchList.tsx deleted file mode 100644 index 2f9d09b704ab2..0000000000000 --- a/apps/meteor/client/sidebarv2/header/SearchList.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { Box, SidebarV2GroupTitle } from '@rocket.chat/fuselage'; -import { useTranslation, useUserPreference, useSetting } from '@rocket.chat/ui-contexts'; -import type { MouseEventHandler, ReactElement } from 'react'; -import { useMemo, useRef } from 'react'; -import { Virtuoso } from 'react-virtuoso'; - -import { VirtualizedScrollbars } from '../../components/CustomScrollbars'; -import RoomListWrapper from '../RoomList/RoomListWrapper'; -import { useAvatarTemplate } from '../hooks/useAvatarTemplate'; -import { usePreventDefault } from '../hooks/usePreventDefault'; -import { useTemplateByViewMode } from '../hooks/useTemplateByViewMode'; -import Row from '../search/Row'; -import { useSearchItems } from './hooks/useSearchItems'; - -type SearchListProps = { filterText: string; onEscSearch: () => void; showRecentList?: boolean }; - -const SearchList = ({ filterText, onEscSearch, showRecentList }: SearchListProps) => { - const t = useTranslation(); - - const boxRef = useRef(null); - usePreventDefault(boxRef); - - const { data: items = [], isLoading } = useSearchItems(filterText); - - const sidebarViewMode = useUserPreference('sidebarViewMode'); - const useRealName = useSetting('UI_Use_Real_Name'); - - const sideBarItemTemplate = useTemplateByViewMode(); - const avatarTemplate = useAvatarTemplate(); - - const extended = sidebarViewMode === 'extended'; - - const itemData = useMemo( - () => ({ - items, - t, - SidebarItemTemplate: sideBarItemTemplate, - avatarTemplate, - useRealName, - extended, - sidebarViewMode, - }), - [avatarTemplate, extended, items, useRealName, sideBarItemTemplate, sidebarViewMode, t], - ); - - const handleClick: MouseEventHandler = (e): void => { - if (e.target instanceof Element && [e.target.tagName, e.target.parentElement?.tagName].includes('BUTTON')) { - return; - } - return onEscSearch(); - }; - - return ( - - {showRecentList && } - - room._id} - itemContent={(_, data): ReactElement => } - /> - - - ); -}; - -export default SearchList; diff --git a/apps/meteor/client/sidebarv2/header/SearchSection.tsx b/apps/meteor/client/sidebarv2/header/SearchSection.tsx deleted file mode 100644 index 2fcdceb4ec035..0000000000000 --- a/apps/meteor/client/sidebarv2/header/SearchSection.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { css } from '@rocket.chat/css-in-js'; -import { Box, Icon, TextInput, Palette, SidebarV2Section, IconButton } from '@rocket.chat/fuselage'; -import { useMergedRefs, useOutsideClick } from '@rocket.chat/fuselage-hooks'; -import { useTranslation, useUser } from '@rocket.chat/ui-contexts'; -import { useCallback, useEffect, useRef, useState } from 'react'; -import { FocusScope, useFocusManager } from 'react-aria'; -import { useForm } from 'react-hook-form'; -import tinykeys from 'tinykeys'; - -import SearchList from './SearchList'; -import CreateRoom from './actions/CreateRoom'; -import Sort from './actions/Sort'; - -const wrapperStyle = css` - position: absolute; - height: 100%; - width: 100%; - display: flex; - flex-direction: column; - z-index: 99; - top: 0; - left: 0; - background-color: ${Palette.surface['surface-sidebar']}; -`; - -const mobileCheck = function () { - let check = false; - (function (a: string) { - if ( - /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test( - a, - ) || - /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( - a.substr(0, 4), - ) - ) - check = true; - })(navigator.userAgent || navigator.vendor || window.opera || ''); - return check; -}; - -const shortcut = ((): string => { - if (navigator.userAgentData?.mobile || mobileCheck()) { - return ''; - } - if (window.navigator.platform.toLowerCase().includes('mac')) { - return '(\u2318+K)'; - } - return '(Ctrl+K)'; -})(); - -const isRecentButton = (node: EventTarget) => (node as HTMLElement).title === 'Recent'; - -const SearchSection = () => { - const t = useTranslation(); - const focusManager = useFocusManager(); - const user = useUser(); - const [recentButtonPressed, setRecentButtonPressed] = useState(false); - - const { - formState: { isDirty }, - register, - watch, - resetField, - setFocus, - } = useForm({ defaultValues: { filterText: '' } }); - const { filterText } = watch(); - const { ref: filterRef, ...rest } = register('filterText'); - - const showRecentList = Boolean(recentButtonPressed && !filterText); - - const inputRef = useRef(null); - const wrapperRef = useRef(null); - const mergedRefs = useMergedRefs(filterRef, inputRef); - - const handleEscSearch = useCallback(() => { - resetField('filterText'); - setRecentButtonPressed(false); - inputRef.current?.blur(); - }, [resetField]); - - useOutsideClick([wrapperRef], handleEscSearch); - - useEffect(() => { - const unsubscribe = tinykeys(window, { - '$mod+K': (event) => { - event.preventDefault(); - setFocus('filterText'); - }, - '$mod+P': (event) => { - event.preventDefault(); - setFocus('filterText'); - }, - 'Shift+$mod+K': (event) => { - event.preventDefault(); - setRecentButtonPressed(true); - focusManager?.focusNext({ accept: (node) => isRecentButton(node) }); - }, - 'Escape': (event) => { - event.preventDefault(); - handleEscSearch(); - }, - }); - - return (): void => { - unsubscribe(); - }; - }, [focusManager, handleEscSearch, setFocus]); - - const placeholder = [t('Search'), shortcut].filter(Boolean).join(' '); - - return ( - - - } - /> - - {user && !isDirty && ( - <> - setRecentButtonPressed(!recentButtonPressed)} - pressed={recentButtonPressed} - /> - {recentButtonPressed ? : } - - - )} - - {(isDirty || recentButtonPressed) && ( - - - - )} - - ); -}; - -export default SearchSection; diff --git a/apps/meteor/client/startup/audit.tsx b/apps/meteor/client/startup/audit.tsx index a0efee0c45d0f..30031114f2562 100644 --- a/apps/meteor/client/startup/audit.tsx +++ b/apps/meteor/client/startup/audit.tsx @@ -5,11 +5,13 @@ import { hasAllPermission } from '../../app/authorization/client'; import { appLayout } from '../lib/appLayout'; import { onToggledFeature } from '../lib/onToggledFeature'; import { router } from '../providers/RouterProvider'; +import SettingsProvider from '../providers/SettingsProvider'; import NotAuthorizedPage from '../views/notAuthorized/NotAuthorizedPage'; import MainLayout from '../views/root/MainLayout'; const AuditPage = lazy(() => import('../views/audit/AuditPage')); const AuditLogPage = lazy(() => import('../views/audit/AuditLogPage')); +const SecurityLogsPage = lazy(() => import('../views/audit/SecurityLogsPage')); declare module '@rocket.chat/ui-contexts' { interface IRouterPaths { @@ -21,6 +23,10 @@ declare module '@rocket.chat/ui-contexts' { pathname: '/audit-log'; pattern: '/audit-log'; }; + 'security-logs': { + pathname: '/security-logs'; + pattern: '/security-logs'; + }; } } @@ -57,6 +63,19 @@ onToggledFeature('auditing', { , ), }, + { + path: '/security-logs', + id: 'security-logs', + element: appLayout.wrap( + + + + + + + , + ), + }, ]); }, down: () => { diff --git a/apps/meteor/client/startup/forceLogout.ts b/apps/meteor/client/startup/forceLogout.ts deleted file mode 100644 index f882354062cda..0000000000000 --- a/apps/meteor/client/startup/forceLogout.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Session } from 'meteor/session'; -import { Tracker } from 'meteor/tracker'; - -import { sdk } from '../../app/utils/client/lib/SDKClient'; - -Meteor.startup(() => { - Tracker.autorun(() => { - const userId = Meteor.userId(); - - if (!userId) { - return; - } - Session.set('force_logout', false); - sdk.stream('notify-user', [`${userId}/force_logout`], () => { - Session.set('force_logout', true); - }); - }); -}); diff --git a/apps/meteor/client/startup/index.ts b/apps/meteor/client/startup/index.ts index 63980c31561e0..dc217a109b80c 100644 --- a/apps/meteor/client/startup/index.ts +++ b/apps/meteor/client/startup/index.ts @@ -6,14 +6,10 @@ import './audit'; import './callbacks'; import './deviceManagement'; import './e2e'; -import './forceLogout'; import './iframeCommands'; import './incomingMessages'; -import './loadMissedMessages'; -import './loginViaQuery'; import './messageObserve'; import './messageTypes'; -import './notifications'; import './reloadRoomAfterLogin'; import './roles'; import './rootUrlChange'; @@ -22,4 +18,3 @@ import './slashCommands'; import './startup'; import './streamMessage'; import './unread'; -import './userRoles'; diff --git a/apps/meteor/client/startup/loadMissedMessages.ts b/apps/meteor/client/startup/loadMissedMessages.ts deleted file mode 100644 index 37bd437d8af27..0000000000000 --- a/apps/meteor/client/startup/loadMissedMessages.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { Messages, Subscriptions } from '../../app/models/client'; -import { LegacyRoomManager, upsertMessage } from '../../app/ui-utils/client'; -import { callWithErrorHandling } from '../lib/utils/callWithErrorHandling'; - -const loadMissedMessages = async function (rid: IRoom['_id']): Promise { - const lastMessage = Messages.findOne({ rid, _hidden: { $ne: true }, temp: { $exists: false } }, { sort: { ts: -1 }, limit: 1 }); - - if (!lastMessage) { - return; - } - - try { - const result = await callWithErrorHandling('loadMissedMessages', rid, lastMessage.ts); - if (result) { - const subscription = Subscriptions.findOne({ rid }); - await Promise.all(Array.from(result).map((msg) => upsertMessage({ msg, subscription }))); - } - } catch (error) { - console.error(error); - } -}; - -Meteor.startup(() => { - let connectionWasOnline = true; - Tracker.autorun(() => { - const { connected } = Meteor.connection.status(); - - if (connected === true && connectionWasOnline === false && LegacyRoomManager.openedRooms) { - Object.keys(LegacyRoomManager.openedRooms).forEach((key) => { - const value = LegacyRoomManager.openedRooms[key]; - if (value.rid) { - loadMissedMessages(value.rid); - } - }); - } - connectionWasOnline = connected; - }); -}); diff --git a/apps/meteor/client/startup/loginViaQuery.ts b/apps/meteor/client/startup/loginViaQuery.ts deleted file mode 100644 index f22fb80d1c713..0000000000000 --- a/apps/meteor/client/startup/loginViaQuery.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { router } from '../providers/RouterProvider'; - -Meteor.startup(() => { - Tracker.afterFlush(() => { - const { resumeToken } = router.getSearchParameters(); - if (!resumeToken) { - return; - } - - Meteor.loginWithToken(resumeToken, () => { - const routeName = router.getRouteName(); - - if (!routeName) { - router.navigate('/home'); - } - - const { resumeToken: _, userId: __, ...search } = router.getSearchParameters(); - - router.navigate( - { - pathname: router.getLocationPathname(), - search, - }, - { replace: true }, - ); - }); - }); -}); diff --git a/apps/meteor/client/startup/notifications/index.ts b/apps/meteor/client/startup/notifications/index.ts deleted file mode 100644 index a3d13d46c5fbb..0000000000000 --- a/apps/meteor/client/startup/notifications/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './konchatNotifications'; diff --git a/apps/meteor/client/startup/notifications/konchatNotifications.ts b/apps/meteor/client/startup/notifications/konchatNotifications.ts deleted file mode 100644 index 494e6ab08d5ba..0000000000000 --- a/apps/meteor/client/startup/notifications/konchatNotifications.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { IUser, ICalendarNotification } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; -import { lazy } from 'react'; - -import { settings } from '../../../app/settings/client'; -import { getUserPreference } from '../../../app/utils/client'; -import { sdk } from '../../../app/utils/client/lib/SDKClient'; -import { imperativeModal } from '../../lib/imperativeModal'; - -const OutlookCalendarEventModal = lazy(() => import('../../views/outlookCalendar/OutlookCalendarEventModal')); - -Meteor.startup(() => { - const notifyUserCalendar = async function (notification: ICalendarNotification): Promise { - const user = Meteor.user() as IUser | null; - if (!user || user.status === 'busy') { - return; - } - - const requireInteraction = getUserPreference(Meteor.userId(), 'desktopNotificationRequireInteraction'); - - const n = new Notification(notification.title, { - body: notification.text, - tag: notification.payload._id, - silent: true, - requireInteraction, - } as NotificationOptions); - - n.onclick = function () { - this.close(); - window.focus(); - imperativeModal.open({ - component: OutlookCalendarEventModal, - props: { id: notification.payload._id, onClose: imperativeModal.close, onCancel: imperativeModal.close }, - }); - }; - }; - - Tracker.autorun(() => { - if (!Meteor.userId() || !settings.get('Outlook_Calendar_Enabled')) { - sdk.stop('notify-user', `${Meteor.userId()}/calendar`); - return; - } - - sdk.stream('notify-user', [`${Meteor.userId()}/calendar`], notifyUserCalendar); - }); -}); diff --git a/apps/meteor/client/startup/startup.ts b/apps/meteor/client/startup/startup.ts index 45c102a72bf49..e75afdabc7a74 100644 --- a/apps/meteor/client/startup/startup.ts +++ b/apps/meteor/client/startup/startup.ts @@ -5,8 +5,6 @@ import { Session } from 'meteor/session'; import { Tracker } from 'meteor/tracker'; import moment from 'moment'; -import { register } from '../../app/markdown/lib/hljs'; -import { settings } from '../../app/settings/client'; import { getUserPreference } from '../../app/utils/client'; import 'hljs9/styles/github.css'; import { sdk } from '../../app/utils/client/lib/SDKClient'; @@ -63,11 +61,3 @@ Meteor.startup(() => { } }); }); -Meteor.startup(() => { - Tracker.autorun(() => { - const code = settings.get('Message_Code_highlight') as string | undefined; - code?.split(',').forEach((language: string) => { - language.trim() && register(language.trim()); - }); - }); -}); diff --git a/apps/meteor/client/startup/userRoles.ts b/apps/meteor/client/startup/userRoles.ts deleted file mode 100644 index 1359dfa4d423c..0000000000000 --- a/apps/meteor/client/startup/userRoles.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { UserRoles, Messages } from '../../app/models/client'; -import { sdk } from '../../app/utils/client/lib/SDKClient'; -import { dispatchToastMessage } from '../lib/toast'; - -Meteor.startup(() => { - Tracker.autorun(() => { - if (Meteor.userId()) { - sdk - .call('getUserRoles') - .then((results) => { - for (const record of results) { - UserRoles.upsert({ _id: record._id }, record); - } - }) - .catch((error) => { - dispatchToastMessage({ type: 'error', message: error }); - }); - - sdk.stream('notify-logged', ['roles-change'], (role) => { - if (role.type === 'added') { - if (!role.scope) { - if (!role.u) { - return; - } - UserRoles.upsert({ _id: role.u._id }, { $addToSet: { roles: role._id }, $set: { username: role.u.username } }); - Messages.update({ 'u._id': role.u._id }, { $addToSet: { roles: role._id } }, { multi: true }); - } - - return; - } - - if (role.type === 'removed') { - if (!role.scope) { - if (!role.u) { - return; - } - UserRoles.update({ _id: role.u._id }, { $pull: { roles: role._id } }); - Messages.update({ 'u._id': role.u._id }, { $pull: { roles: role._id } }, { multi: true }); - } - - return; - } - - if (role.type === 'changed') { - Messages.update({ roles: role._id }, { $inc: { rerender: 1 } }, { multi: true }); - } - }); - } - }); -}); diff --git a/apps/meteor/client/ui.ts b/apps/meteor/client/ui.ts index f5e55d64833b5..2bad21ebd2fae 100644 --- a/apps/meteor/client/ui.ts +++ b/apps/meteor/client/ui.ts @@ -25,14 +25,15 @@ import { usePushNotificationsRoomAction } from './hooks/roomActions/usePushNotif import { useRocketSearchRoomAction } from './hooks/roomActions/useRocketSearchRoomAction'; import { useRoomInfoRoomAction } from './hooks/roomActions/useRoomInfoRoomAction'; import { useStarredMessagesRoomAction } from './hooks/roomActions/useStarredMessagesRoomAction'; -import { useStartCallRoomAction } from './hooks/roomActions/useStartCallRoomAction'; import { useTeamChannelsRoomAction } from './hooks/roomActions/useTeamChannelsRoomAction'; import { useTeamInfoRoomAction } from './hooks/roomActions/useTeamInfoRoomAction'; import { useThreadRoomAction } from './hooks/roomActions/useThreadRoomAction'; import { useUploadedFilesListRoomAction } from './hooks/roomActions/useUploadedFilesListRoomAction'; import { useUserInfoGroupRoomAction } from './hooks/roomActions/useUserInfoGroupRoomAction'; import { useUserInfoRoomAction } from './hooks/roomActions/useUserInfoRoomAction'; +import { useVideoCallRoomAction } from './hooks/roomActions/useVideoCallRoomAction'; import { useVoIPRoomInfoRoomAction } from './hooks/roomActions/useVoIPRoomInfoRoomAction'; +import { useVoiceCallRoomAction } from './hooks/roomActions/useVoiceCallRoomAction'; import { useWebRTCVideoRoomAction } from './hooks/roomActions/useWebRTCVideoRoomAction'; import type { RoomToolboxActionConfig } from './views/room/contexts/RoomToolboxContext'; import type { QuickActionsActionConfig } from './views/room/lib/quickActions'; @@ -63,12 +64,13 @@ export const roomActionHooks = [ useRocketSearchRoomAction, useRoomInfoRoomAction, useStarredMessagesRoomAction, - useStartCallRoomAction, useTeamChannelsRoomAction, useUploadedFilesListRoomAction, useVoIPRoomInfoRoomAction, useWebRTCVideoRoomAction, useAppsRoomStarActions, + useVideoCallRoomAction, + useVoiceCallRoomAction, ] satisfies (() => RoomToolboxActionConfig | undefined)[]; export const quickActionHooks = [ diff --git a/apps/meteor/client/views/account/AccountSidebar.tsx b/apps/meteor/client/views/account/AccountSidebar.tsx index b6d065fe3683b..a651061f7a41f 100644 --- a/apps/meteor/client/views/account/AccountSidebar.tsx +++ b/apps/meteor/client/views/account/AccountSidebar.tsx @@ -16,7 +16,7 @@ const AccountSidebar = () => { // TODO: uplift this provider return ( - + diff --git a/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx index 0f081a83532f6..8d159a81e7a5d 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx @@ -7,7 +7,7 @@ import { useId, useCallback, useEffect, useState, useMemo } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import { KonchatNotification } from '../../../../app/ui/client/lib/KonchatNotification'; +import { useNotification } from '../../../hooks/notification/useNotification'; const notificationOptionsLabelMap = { all: 'All_messages', @@ -20,7 +20,6 @@ const emailNotificationOptionsLabelMap = { nothing: 'Email_Notification_Mode_Disabled', }; -// TODO: Test Notification Button not working const PreferencesNotificationsSection = () => { const { t, i18n } = useTranslation(); @@ -39,18 +38,19 @@ const PreferencesNotificationsSection = () => { const showNewLoginEmailPreference = loginEmailEnabled && allowLoginEmailPreference; const showCalendarPreference = useSetting('Outlook_Calendar_Enabled'); const showMobileRinging = useSetting('VideoConf_Mobile_Ringing'); + const notify = useNotification(); const userEmailNotificationMode = useUserPreference('emailNotificationMode') as keyof typeof emailNotificationOptionsLabelMap; useEffect(() => setNotificationsPermission(window.Notification && Notification.permission), []); const onSendNotification = useCallback(() => { - KonchatNotification.notify({ + notify({ payload: { sender: { _id: 'rocket.cat', username: 'rocket.cat' }, rid: 'GENERAL' } as INotificationDesktop['payload'], title: t('Desktop_Notification_Test'), text: t('This_is_a_desktop_notification'), }); - }, [t]); + }, [notify, t]); const onAskNotificationPermission = useCallback(() => { window.Notification && Notification.requestPermission().then((val) => setNotificationsPermission(val)); diff --git a/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx index 153d03dd078dc..3599e3d381d9c 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx @@ -8,7 +8,7 @@ const PreferencesSoundSection = () => { const t = useTranslation(); const customSound = useCustomSound(); - const soundsList: SelectOption[] = customSound?.getList()?.map((value) => [value._id, value.name]) || []; + const soundsList: SelectOption[] = customSound.list?.map((value) => [value._id, value.name]) || []; const { control, watch } = useFormContext(); const { newMessageNotification, notificationsSoundVolume = 100, masterVolume = 100, voipRingerVolume = 100 } = watch(); diff --git a/apps/meteor/client/views/account/security/AccountSecurityPage.tsx b/apps/meteor/client/views/account/security/AccountSecurityPage.tsx index db79ea5b22e6e..3033161d1aca5 100644 --- a/apps/meteor/client/views/account/security/AccountSecurityPage.tsx +++ b/apps/meteor/client/views/account/security/AccountSecurityPage.tsx @@ -1,4 +1,4 @@ -import { Box, Accordion, AccordionItem, ButtonGroup, Button } from '@rocket.chat/fuselage'; +import { Box, Accordion, AccordionItem, ButtonGroup, Button, Callout } from '@rocket.chat/fuselage'; import { useSetting, useTranslation, useUser } from '@rocket.chat/ui-contexts'; import { useId } from 'react'; import type { ReactElement } from 'react'; @@ -9,6 +9,7 @@ import EndToEnd from './EndToEnd'; import TwoFactorEmail from './TwoFactorEmail'; import TwoFactorTOTP from './TwoFactorTOTP'; import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; +import { useRequire2faSetup } from '../../hooks/useRequire2faSetup'; const passwordDefaultValues = { password: '', confirmationPassword: '' }; @@ -38,6 +39,8 @@ const AccountSecurityPage = (): ReactElement => { const passwordFormId = useId(); + const require2faSetup = useRequire2faSetup(); + return ( @@ -46,7 +49,7 @@ const AccountSecurityPage = (): ReactElement => { {allowPasswordChange && ( - + @@ -54,7 +57,12 @@ const AccountSecurityPage = (): ReactElement => { )} {(twoFactorTOTP || showEmailTwoFactor) && twoFactorEnabled && ( - + + {require2faSetup && ( + + {t('Enable_two-factor_authentication_callout_description')} + + )} {twoFactorTOTP && } {showEmailTwoFactor && } diff --git a/apps/meteor/client/views/account/security/TwoFactorEmail.tsx b/apps/meteor/client/views/account/security/TwoFactorEmail.tsx index c77c395cdefc2..5ff4e6d063102 100644 --- a/apps/meteor/client/views/account/security/TwoFactorEmail.tsx +++ b/apps/meteor/client/views/account/security/TwoFactorEmail.tsx @@ -1,7 +1,7 @@ -import { Box, Button, Margins } from '@rocket.chat/fuselage'; +import { Box, Field, FieldLabel, FieldRow, Margins, ToggleSwitch } from '@rocket.chat/fuselage'; import { useUser } from '@rocket.chat/ui-contexts'; -import type { ComponentProps } from 'react'; -import { useCallback } from 'react'; +import type { ComponentProps, FormEvent } from 'react'; +import { useCallback, useId } from 'react'; import { useTranslation } from 'react-i18next'; import { useEndpointAction } from '../../../hooks/useEndpointAction'; @@ -9,6 +9,7 @@ import { useEndpointAction } from '../../../hooks/useEndpointAction'; const TwoFactorEmail = (props: ComponentProps) => { const { t } = useTranslation(); const user = useUser(); + const emailId = useId(); const isEnabled = user?.services?.email2fa?.enabled; @@ -19,30 +20,26 @@ const TwoFactorEmail = (props: ComponentProps) => { successMessage: t('Two-factor_authentication_disabled'), }); - const handleEnable = useCallback(async () => { - await enable2faAction(); - }, [enable2faAction]); - const handleDisable = useCallback(async () => { - await disable2faAction(); - }, [disable2faAction]); + const handleEnable = useCallback( + async (e: FormEvent) => { + if (e.currentTarget.checked) { + await enable2faAction(); + } else { + await disable2faAction(); + } + }, + [disable2faAction, enable2faAction], + ); return ( - {t('Two-factor_authentication_email')} - {isEnabled && ( - - )} - {!isEnabled && ( - <> - {t('Two-factor_authentication_email_is_currently_disabled')} - - - )} + + + {t('Two-factor_authentication_email')} + + + ); diff --git a/apps/meteor/client/views/account/security/TwoFactorTOTP.tsx b/apps/meteor/client/views/account/security/TwoFactorTOTP.tsx index a8d2230eec62e..1c7bac0937d0a 100644 --- a/apps/meteor/client/views/account/security/TwoFactorTOTP.tsx +++ b/apps/meteor/client/views/account/security/TwoFactorTOTP.tsx @@ -1,8 +1,8 @@ -import { Box, Button, TextInput, Margins } from '@rocket.chat/fuselage'; -import { useSafely } from '@rocket.chat/fuselage-hooks'; +import { Box, Button, TextInput, Margins, Field, FieldRow, FieldLabel, ToggleSwitch } from '@rocket.chat/fuselage'; +import { useEffectEvent, useSafely } from '@rocket.chat/fuselage-hooks'; import { useSetModal, useToastMessageDispatch, useUser, useMethod } from '@rocket.chat/ui-contexts'; -import type { ReactElement, ComponentPropsWithoutRef } from 'react'; -import { useState, useCallback, useEffect } from 'react'; +import type { ReactElement, ComponentPropsWithoutRef, FormEvent } from 'react'; +import { useState, useCallback, useEffect, useId } from 'react'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import qrcode from 'yaqrcode'; @@ -51,7 +51,7 @@ const TwoFactorTOTP = (props: TwoFactorTOTPProps): ReactElement => { updateCodesRemaining(); }, [checkCodesRemainingFn, setCodesRemaining, totpEnabled]); - const handleEnableTotp = useCallback(async () => { + const enableTotp = useEffectEvent(async () => { try { const result = await enableTotpFn(); @@ -62,26 +62,46 @@ const TwoFactorTOTP = (props: TwoFactorTOTPProps): ReactElement => { } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } - }, [dispatchToastMessage, enableTotpFn, setQrCode, setRegisteringTotp, setTotpSecret]); + }); + + const disableTotp = useEffectEvent(async () => { + if (!totpEnabled) { + setRegisteringTotp(false); + + return; + } - const handleDisableTotp = useCallback(async () => { const onDisable = async (authCode: string): Promise => { try { const result = await disableTotpFn(authCode); if (!result) { - return dispatchToastMessage({ type: 'error', message: t('Invalid_two_factor_code') }); + dispatchToastMessage({ type: 'error', message: t('Invalid_two_factor_code') }); + + return; } dispatchToastMessage({ type: 'success', message: t('Two-factor_authentication_disabled') }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } + closeModal(); }; setModal(); - }, [closeModal, disableTotpFn, dispatchToastMessage, setModal, t]); + }); + + const handleToggleTotp = useEffectEvent(async (e: FormEvent) => { + if (e.currentTarget?.checked) { + void enableTotp(); + } else { + void disableTotp(); + } + }); + + const totpId = useId(); + const totpCodeId = useId(); const handleVerifyCode = useCallback( async ({ authCode }: TwoFactorTOTPFormData) => { @@ -94,6 +114,8 @@ const TwoFactorTOTP = (props: TwoFactorTOTPProps): ReactElement => { setRegisteringTotp(false); setModal(); + + dispatchToastMessage({ type: 'success', message: t('Two-factor_authentication_enabled') }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } @@ -121,38 +143,35 @@ const TwoFactorTOTP = (props: TwoFactorTOTPProps): ReactElement => { return ( - {t('Two-factor_authentication_via_TOTP')} - {!totpEnabled && !registeringTotp && ( - <> - {t('Two-factor_authentication_is_currently_disabled')} - - - )} + + + {t('Two-factor_authentication_via_TOTP')} + + + {!totpEnabled && registeringTotp && ( <> {t('Scan_QR_code')} {t('Scan_QR_code_alternative_s')} - - {t('Application_Name')} + {t('Application_Name')} - + {t('Give_the_application_a_name_This_will_be_seen_by_your_users')} {errors?.name && {t('Required_field', { field: t('Name') })}} - {t('Redirect_URI')} + {t('Redirect_URI')} - + {t('After_OAuth2_authentication_users_will_be_redirected_to_this_URL')} {errors?.redirectUri && {t('Required_field', { field: t('Redirect_URI') })}} - {t('Client_ID')} + {t('Client_ID')} - + - {t('Client_Secret')} + {t('Client_Secret')} - + - {t('Authorization_URL')} + {t('Authorization_URL')} - + - {t('Access_Token_URL')} + {t('Access_Token_URL')} - + diff --git a/apps/meteor/client/views/admin/oauthApps/OAuthAddApp.tsx b/apps/meteor/client/views/admin/oauthApps/OAuthAddApp.tsx index 6689590ad9ccc..cdd6c0db8e7b9 100644 --- a/apps/meteor/client/views/admin/oauthApps/OAuthAddApp.tsx +++ b/apps/meteor/client/views/admin/oauthApps/OAuthAddApp.tsx @@ -13,7 +13,7 @@ import { } from '@rocket.chat/fuselage'; import { useToastMessageDispatch, useRoute, useEndpoint } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import { useCallback } from 'react'; +import { useCallback, useId } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -53,6 +53,9 @@ const OAuthAddApp = (): ReactElement => { } }; + const nameField = useId(); + const redirectUriField = useId(); + return ( @@ -68,17 +71,17 @@ const OAuthAddApp = (): ReactElement => { - {t('Application_Name')} + {t('Application_Name')} - + {t('Give_the_application_a_name_This_will_be_seen_by_your_users')} {errors?.name && {errors.name.message}} - {t('Redirect_URI')} + {t('Redirect_URI')} - + {t('After_OAuth2_authentication_users_will_be_redirected_to_this_URL')} {errors?.redirectUri && {t('Required_field', { field: t('Redirect_URI') })}} diff --git a/apps/meteor/client/views/admin/oauthApps/OAuthAppsTable.tsx b/apps/meteor/client/views/admin/oauthApps/OAuthAppsTable.tsx index 2d3733e9f1802..01398427eb180 100644 --- a/apps/meteor/client/views/admin/oauthApps/OAuthAppsTable.tsx +++ b/apps/meteor/client/views/admin/oauthApps/OAuthAppsTable.tsx @@ -62,7 +62,7 @@ const OAuthAppsTable = (): ReactElement => { )} {isSuccess && data?.oauthApps.length === 0 && } {isSuccess && data?.oauthApps.length > 0 && ( - + {headers} {data?.oauthApps.map(({ _id, name, _createdAt, _createdBy: { username: createdBy } }) => ( diff --git a/apps/meteor/client/views/admin/settings/EditableSettingsProvider.tsx b/apps/meteor/client/views/admin/settings/EditableSettingsProvider.tsx index 677332261697d..5124da92d4686 100644 --- a/apps/meteor/client/views/admin/settings/EditableSettingsProvider.tsx +++ b/apps/meteor/client/views/admin/settings/EditableSettingsProvider.tsx @@ -1,209 +1,89 @@ import type { ISetting } from '@rocket.chat/core-typings'; -import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import type { SettingsContextQuery } from '@rocket.chat/ui-contexts'; import { useSettings } from '@rocket.chat/ui-contexts'; -import { Mongo } from 'meteor/mongo'; -import { Tracker } from 'meteor/tracker'; -import type { FilterOperators } from 'mongodb'; -import type { MutableRefObject, ReactNode } from 'react'; -import { useEffect, useMemo, useRef } from 'react'; +import type { ReactNode } from 'react'; +import { useEffect, useMemo, useState } from 'react'; +import { create } from 'zustand'; -import { createReactiveSubscriptionFactory } from '../../../lib/createReactiveSubscriptionFactory'; -import type { EditableSetting, EditableSettingsContextValue } from '../EditableSettingsContext'; -import { EditableSettingsContext } from '../EditableSettingsContext'; +import type { EditableSetting, IEditableSettingsState } from '../EditableSettingsContext'; +import { EditableSettingsContext, performSettingQuery } from '../EditableSettingsContext'; -const defaultQuery: SettingsContextQuery = {}; -const defaultOmit: Array = []; +const defaultOmit: Array = ['Cloud_Workspace_AirGapped_Restrictions_Remaining_Days']; type EditableSettingsProviderProps = { children?: ReactNode; - query?: SettingsContextQuery; - omit?: Array; }; -const EditableSettingsProvider = ({ children, query = defaultQuery, omit = defaultOmit }: EditableSettingsProviderProps) => { - const settingsCollectionRef = useRef>(null) as MutableRefObject>; - const persistedSettings = useSettings(query); - - const getSettingsCollection = useEffectEvent(() => { - if (!settingsCollectionRef.current) { - settingsCollectionRef.current = new Mongo.Collection(null); - } - - return settingsCollectionRef.current; - }) as () => Mongo.Collection; - - useEffect(() => { - const settingsCollection = getSettingsCollection(); - - settingsCollection.remove({ _id: { $nin: persistedSettings.map(({ _id }) => _id) } }); - for (const { _id, ...fields } of persistedSettings) { - settingsCollection.upsert(_id, { $set: { ...fields }, $unset: { changed: true } }); - } - // TODO: Remove option to omit settings from admin pages manually - // This is a very wacky workaround due to lack of support to omit settings from the - // admin settings page while keeping them public. - if (omit.length > 0) { - settingsCollection.remove({ _id: { $in: omit } }); - } - }, [getSettingsCollection, persistedSettings, omit]); - - const queryEditableSetting = useMemo(() => { - const validateSettingQueries = ( - query: undefined | string | FilterOperators | FilterOperators[], - settingsCollection: Mongo.Collection, - ): boolean => { - if (!query) { - return true; - } - - const queries = [].concat(typeof query === 'string' ? JSON.parse(query) : query); - return queries.every((query) => settingsCollection.find(query).count() > 0); - }; - - return createReactiveSubscriptionFactory((_id: ISetting['_id']): EditableSetting | undefined => { - const settingsCollection = getSettingsCollection(); - const editableSetting = settingsCollection.findOne(_id); - - if (!editableSetting) { - return undefined; - } - - return { - ...editableSetting, - disabled: editableSetting.blocked || !validateSettingQueries(editableSetting.enableQuery, settingsCollection), - invisible: !validateSettingQueries(editableSetting.displayQuery, settingsCollection), - }; - }); - }, [getSettingsCollection]); - - const queryEditableSettings = useMemo( - () => - createReactiveSubscriptionFactory((query = {}) => - getSettingsCollection() - .find( - { - ...('_id' in query && { _id: { $in: query._id } }), - ...('group' in query && { group: query.group }), - ...('changed' in query && { changed: query.changed }), - $and: [ - { - ...('section' in query && - (query.section - ? { section: query.section } - : { - $or: [{ section: { $exists: false } }, { section: '' }], - })), - }, - { - ...('tab' in query && - (query.tab - ? { tab: query.tab } - : { - $or: [{ tab: { $exists: false } }, { tab: '' }], - })), - }, - ], - }, - { - sort: { - section: 1, - sorter: 1, - i18nLabel: 1, - }, - }, - ) - .fetch(), - ), - [getSettingsCollection], - ); - - const queryGroupSections = useMemo( - () => - createReactiveSubscriptionFactory((_id: ISetting['_id'], tab?: ISetting['_id']) => - Array.from( - new Set( - getSettingsCollection() - .find( - { - group: _id, - ...(tab !== undefined - ? { tab } - : { - $or: [{ tab: { $exists: false } }, { tab: '' }], - }), - }, - { - fields: { - section: 1, - }, - sort: { - sorter: 1, - section: 1, - i18nLabel: 1, - }, - }, - ) - .fetch() - .map(({ section }) => section || ''), - ), +// TODO: this component can be replaced by RHF state management +const EditableSettingsProvider = ({ children }: EditableSettingsProviderProps) => { + const persistedSettings = useSettings(); + + const [useEditableSettingsStore] = useState(() => + create()((set) => ({ + state: persistedSettings + .filter((x) => !defaultOmit.includes(x._id)) + .map( + (persisted): EditableSetting => ({ + ...persisted, + changed: false, + // TODO: This might not be needed anymore due to implementation of useEditableSettingVisibilityQuery + // This was left here to avoid unexpected breaking changes + disabled: persisted.blocked || !performSettingQuery(persisted.enableQuery, persistedSettings), + invisible: !performSettingQuery(persisted.displayQuery, persistedSettings), + }), ), - ), - [getSettingsCollection], + initialState: persistedSettings, + sync: (newInitialState) => { + set(({ state }) => ({ + state: newInitialState + .filter((x) => !defaultOmit.includes(x._id)) + .map( + (persisted): EditableSetting => ({ + ...state.find(({ _id }) => _id === persisted._id), + ...persisted, + changed: false, + // TODO: This might not be needed anymore due to implementation of useEditableSettingVisibilityQuery + // This was left here to avoid unexpected breaking changes + disabled: persisted.blocked || !performSettingQuery(persisted.enableQuery, state), + invisible: !performSettingQuery(persisted.displayQuery, state), + }), + ), + })); + }, + mutate: (changes) => { + set(({ state, initialState }) => ({ + state: initialState + .filter((x) => !defaultOmit.includes(x._id)) + .map((persisted): EditableSetting => { + const current = state.find(({ _id }) => _id === persisted._id); + if (!current) throw new Error(`Setting ${persisted._id} not found`); + + const change = changes.find(({ _id }) => _id === current._id); + + if (!change) { + return current; + } + + return { + ...current, + ...change, + }; + }), + })); + }, + })), ); - const queryGroupTabs = useMemo( - () => - createReactiveSubscriptionFactory((_id: ISetting['_id']) => - Array.from( - new Set( - getSettingsCollection() - .find( - { - group: _id, - }, - { - fields: { - tab: 1, - }, - sort: { - sorter: 1, - tab: 1, - i18nLabel: 1, - }, - }, - ) - .fetch() - .map(({ tab }) => tab || ''), - ), - ), - ), - [getSettingsCollection], - ); - - const dispatch = useEffectEvent((changes: Partial[]): void => { - for (const { _id, ...data } of changes) { - if (!_id) { - continue; - } + const sync = useEditableSettingsStore((state) => state.sync); - getSettingsCollection().update(_id, { $set: data }); - } - Tracker.flush(); - }); + useEffect(() => { + sync(persistedSettings); + }, [persistedSettings, sync]); - const contextValue = useMemo( - () => ({ - queryEditableSetting, - queryEditableSettings, - queryGroupSections, - queryGroupTabs, - dispatch, - }), - [queryEditableSetting, queryEditableSettings, queryGroupSections, queryGroupTabs, dispatch], + return ( + ({ useEditableSettingsStore }), [useEditableSettingsStore])}> + {children} + ); - - return ; }; export default EditableSettingsProvider; diff --git a/apps/meteor/client/views/admin/settings/Setting/Setting.tsx b/apps/meteor/client/views/admin/settings/Setting/Setting.tsx index 484eb34cacfa3..04f4e8cf061e9 100644 --- a/apps/meteor/client/views/admin/settings/Setting/Setting.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/Setting.tsx @@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'; import MemoizedSetting from './MemoizedSetting'; import MarkdownText from '../../../../components/MarkdownText'; -import { useEditableSetting, useEditableSettingsDispatch } from '../../EditableSettingsContext'; +import { useEditableSetting, useEditableSettingsDispatch, useEditableSettingVisibilityQuery } from '../../EditableSettingsContext'; import { useHasSettingModule } from '../hooks/useHasSettingModule'; type SettingProps = { @@ -96,7 +96,10 @@ function Setting({ className = undefined, settingId, sectionChanged }: SettingPr // eslint-disable-next-line react-hooks/exhaustive-deps }, [setting.value, (setting as ISettingColor).editor, update, persistedSetting]); - const { _id, disabled, readonly, type, packageValue, i18nLabel, i18nDescription, alert, invisible } = setting; + const { _id, readonly, type, packageValue, i18nLabel, i18nDescription, alert } = setting; + + const disabled = !useEditableSettingVisibilityQuery(persistedSetting.enableQuery); + const invisible = !useEditableSettingVisibilityQuery(persistedSetting.displayQuery); const labelText = (i18n.exists(i18nLabel) && t(i18nLabel)) || (i18n.exists(_id) && t(_id)) || i18nLabel || _id; @@ -162,7 +165,7 @@ function Setting({ className = undefined, settingId, sectionChanged }: SettingPr showUpgradeButton={showUpgradeButton} sectionChanged={sectionChanged} {...setting} - disabled={setting.disabled || shouldDisableEnterprise} + disabled={disabled || shouldDisableEnterprise} value={value} editor={editor} hasResetButton={hasResetButton} diff --git a/apps/meteor/client/views/admin/settings/Setting/inputs/CodeMirror/CodeMirror.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/CodeMirror/CodeMirror.tsx index 1f249c7373c64..bef8372819197 100644 --- a/apps/meteor/client/views/admin/settings/Setting/inputs/CodeMirror/CodeMirror.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/CodeMirror/CodeMirror.tsx @@ -1,7 +1,7 @@ import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import type { Editor, EditorFromTextArea } from 'codemirror'; import type { ReactElement } from 'react'; -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; const defaultGutters = ['CodeMirror-linenumbers', 'CodeMirror-foldgutter']; @@ -44,77 +44,69 @@ function CodeMirror({ ...props }: CodeMirrorProps): ReactElement { const [value, setValue] = useState(valueProp || defaultValue); - - const textAreaRef = useRef(null); - const editorRef = useRef(null); const handleChange = useEffectEvent(onChange); - useEffect(() => { - if (editorRef.current) { - return; - } - - const setupCodeMirror = async (): Promise => { - const { default: CodeMirror } = await import('codemirror'); - await Promise.all([ - import('../../../../../../../app/ui/client/lib/codeMirror/codeMirror'), - import('codemirror/addon/edit/matchbrackets'), - import('codemirror/addon/edit/closebrackets'), - import('codemirror/addon/edit/matchtags'), - import('codemirror/addon/edit/trailingspace'), - import('codemirror/addon/search/match-highlighter'), - import('codemirror/lib/codemirror.css'), - ]); - - if (!textAreaRef.current) { - return; - } - - editorRef.current = CodeMirror.fromTextArea(textAreaRef.current, { - lineNumbers, - lineWrapping, - mode, - gutters, - foldGutter, - matchBrackets, - autoCloseBrackets, - matchTags, - showTrailingSpace, - highlightSelectionMatches, - readOnly, - }); - - editorRef.current.on('change', (doc: Editor) => { - const value = doc.getValue(); - setValue(value); - handleChange(value); - }); - }; - - setupCodeMirror(); - - return (): void => { - if (!editorRef.current) { - return; + const editorRef = useRef(null); + const textAreaRef = useCallback( + async (node: HTMLTextAreaElement | null) => { + if (!node) return; + + try { + const { default: CodeMirror } = await import('codemirror'); + await Promise.all([ + import('../../../../../../../app/ui/client/lib/codeMirror/codeMirror'), + import('codemirror/addon/edit/matchbrackets'), + import('codemirror/addon/edit/closebrackets'), + import('codemirror/addon/edit/matchtags'), + import('codemirror/addon/edit/trailingspace'), + import('codemirror/addon/search/match-highlighter'), + import('codemirror/lib/codemirror.css'), + ]); + + editorRef.current = CodeMirror.fromTextArea(node, { + lineNumbers, + lineWrapping, + mode, + gutters, + foldGutter, + matchBrackets, + autoCloseBrackets, + matchTags, + showTrailingSpace, + highlightSelectionMatches, + readOnly, + }); + + editorRef.current.on('change', (doc: Editor) => { + const newValue = doc.getValue(); + setValue(newValue); + handleChange(newValue); + }); + + return () => { + if (node.parentNode) { + editorRef.current?.toTextArea(); + } + }; + } catch (error) { + console.error('CodeMirror initialization failed:', error); } - - editorRef.current.toTextArea(); - }; - }, [ - autoCloseBrackets, - foldGutter, - gutters, - highlightSelectionMatches, - lineNumbers, - lineWrapping, - matchBrackets, - matchTags, - mode, - handleChange, - readOnly, - textAreaRef, - showTrailingSpace, - ]); + }, + [ + autoCloseBrackets, + foldGutter, + gutters, + highlightSelectionMatches, + lineNumbers, + lineWrapping, + matchBrackets, + matchTags, + mode, + handleChange, + readOnly, + showTrailingSpace, + ], + ); useEffect(() => { setValue(valueProp); diff --git a/apps/meteor/client/views/admin/settings/SettingsPage.tsx b/apps/meteor/client/views/admin/settings/SettingsPage.tsx index 5bc6acb7160de..abcf72e76bff4 100644 --- a/apps/meteor/client/views/admin/settings/SettingsPage.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsPage.tsx @@ -1,7 +1,6 @@ -import { Icon, SearchInput, Skeleton, CardGrid } from '@rocket.chat/fuselage'; +import { Icon, SearchInput, CardGrid } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useIsSettingsContextLoading } from '@rocket.chat/ui-contexts'; import type { ChangeEvent, ReactElement } from 'react'; import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -18,7 +17,6 @@ const SettingsPage = (): ReactElement => { const handleChange = useCallback((e: ChangeEvent) => setFilter(e.currentTarget.value), []); const groups = useSettingsGroups(useDebouncedValue(filter, 400)); - const isLoadingGroups = useIsSettingsContextLoading(); return ( @@ -28,7 +26,6 @@ const SettingsPage = (): ReactElement => { - {isLoadingGroups && } { p: 8, }} > - {!isLoadingGroups && - !!groups.length && + {!!groups.length && groups.map((group) => ( { ))} - {!isLoadingGroups && !groups.length && } + {!groups.length && } ); diff --git a/apps/meteor/client/views/admin/settings/SettingsRoute.tsx b/apps/meteor/client/views/admin/settings/SettingsRoute.tsx index 70776dee4fe80..145fb72e3ef3e 100644 --- a/apps/meteor/client/views/admin/settings/SettingsRoute.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsRoute.tsx @@ -6,8 +6,6 @@ import SettingsGroupSelector from './SettingsGroupSelector'; import SettingsPage from './SettingsPage'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; -const omittedSettings = ['Cloud_Workspace_AirGapped_Restrictions_Remaining_Days']; - export const SettingsRoute = (): ReactElement => { const hasPermission = useIsPrivilegedSettingsContext(); const groupId = useRouteParameter('group'); @@ -22,7 +20,7 @@ export const SettingsRoute = (): ReactElement => { } return ( - + router.navigate('/admin/settings')} /> ); diff --git a/apps/meteor/client/views/admin/sidebar/AdminSidebar.tsx b/apps/meteor/client/views/admin/sidebar/AdminSidebar.tsx index 59b3c611f1af3..1bcc7161c067a 100644 --- a/apps/meteor/client/views/admin/sidebar/AdminSidebar.tsx +++ b/apps/meteor/client/views/admin/sidebar/AdminSidebar.tsx @@ -15,7 +15,7 @@ const AdminSidebar = () => { // TODO: uplift this provider return ( - + { + const { t } = useTranslation(); + + return ( + + + + + + + ); +}; + +export default SecurityLogsPage; diff --git a/apps/meteor/client/views/audit/components/AppInfoField.spec.tsx b/apps/meteor/client/views/audit/components/AppInfoField.spec.tsx new file mode 100644 index 0000000000000..cfa92559fdcfc --- /dev/null +++ b/apps/meteor/client/views/audit/components/AppInfoField.spec.tsx @@ -0,0 +1,21 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { composeStories } from '@storybook/react'; +import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; + +import * as stories from './AppInfoField.stories'; + +const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]); + +test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => { + const view = render(, { wrapper: mockAppRoot().build() }); + expect(view.baseElement).toMatchSnapshot(); +}); + +test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => { + const { container } = render(, { wrapper: mockAppRoot().build() }); + + const results = await axe(container); + expect(results).toHaveNoViolations(); +}); diff --git a/apps/meteor/client/views/audit/components/AppInfoField.stories.tsx b/apps/meteor/client/views/audit/components/AppInfoField.stories.tsx new file mode 100644 index 0000000000000..b5a29e47427be --- /dev/null +++ b/apps/meteor/client/views/audit/components/AppInfoField.stories.tsx @@ -0,0 +1,89 @@ +import type { AppSubscriptionStatus } from '@rocket.chat/core-typings'; +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import type { Meta, StoryFn } from '@storybook/react'; + +import { AppInfoField } from './AppInfoField'; + +export default { + title: 'views/Audit/AppInfoField', + component: AppInfoField, + args: { + appId: 'app-id', + }, + decorators: [ + mockAppRoot() + .withEndpoint('GET', '/apps/:id', () => ({ + app: { + name: 'App Name', + id: '', + iconFileData: '', + appRequestStats: { + appId: '', + totalSeen: 0, + totalUnseen: 0, + }, + author: { + name: '', + homepage: '', + support: '', + }, + description: '', + privacyPolicySummary: '', + detailedDescription: { + raw: '', + rendered: '', + }, + detailedChangelog: { + raw: '', + rendered: '', + }, + categories: [], + version: '', + price: 0, + purchaseType: 'buy' as const, + pricingPlans: [], + iconFileContent: '', + isSubscribed: false, + bundledIn: [], + marketplaceVersion: '', + // Recursive typem expect an App type here + latest: undefined as any, + subscriptionInfo: { + typeOf: '', + status: 'Active' as AppSubscriptionStatus, + statusFromBilling: false, + isSeatBased: false, + seats: 0, + maxSeats: 0, + license: { + license: '', + version: 0, + expireDate: '', + }, + startDate: '', + periodEnd: '', + endDate: '', + externallyManaged: false, + isSubscribedViaBundle: false, + }, + tosLink: '', + privacyLink: '', + modifiedAt: '', + permissions: [], + languages: [], + createdDate: '', + private: false, + documentationUrl: '', + migrated: false, + }, + })) + .withTranslations('en', 'core', { App_name: 'App Name' }) + .buildStoryDecorator(), + ], +} satisfies Meta; + +export const Default: StoryFn = (args) => ; + +export const NoAppInfo: StoryFn = (args) => ; + +NoAppInfo.decorators = [mockAppRoot().withTranslations('en', 'core', { App_id: 'App Id' }).buildStoryDecorator()]; diff --git a/apps/meteor/client/views/audit/components/AppInfoField.tsx b/apps/meteor/client/views/audit/components/AppInfoField.tsx new file mode 100644 index 0000000000000..18541b31fa002 --- /dev/null +++ b/apps/meteor/client/views/audit/components/AppInfoField.tsx @@ -0,0 +1,40 @@ +import { Skeleton } from '@rocket.chat/fuselage'; +import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; + +import AuditModalField from './AuditModalField'; +import AuditModalLabel from './AuditModalLabel'; +import AuditModalText from './AuditModalText'; + +type AppInfoFieldProps = { + appId: string; +}; + +// This is a separate component to encapsulate its logic and in the future expand it to a field that shows more info on the App +export const AppInfoField = ({ appId }: AppInfoFieldProps) => { + const t = useTranslation(); + + const getAppInfo = useEndpoint('GET', `/apps/:id`, { id: appId }); + + const { data, isLoading, isSuccess } = useQuery({ + queryKey: ['getAppInfo', appId], + + queryFn: async () => { + return getAppInfo(); + }, + }); + + return ( + <> + + {t('Actor')} + {t('App')} + + + {isSuccess && data ? t('App_name') : t('App_id')} + {isLoading && } + {isSuccess && data ? {data.app.name} : {appId}} + + + ); +}; diff --git a/apps/meteor/client/views/audit/components/AuditModalField.tsx b/apps/meteor/client/views/audit/components/AuditModalField.tsx new file mode 100644 index 0000000000000..178260d432f20 --- /dev/null +++ b/apps/meteor/client/views/audit/components/AuditModalField.tsx @@ -0,0 +1,8 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ComponentPropsWithoutRef } from 'react'; + +type AuditModalFieldProps = ComponentPropsWithoutRef; + +const AuditModalField = (props: AuditModalFieldProps) => ; + +export default AuditModalField; diff --git a/apps/meteor/client/views/audit/components/AuditModalLabel.tsx b/apps/meteor/client/views/audit/components/AuditModalLabel.tsx new file mode 100644 index 0000000000000..3b2c58cffeb85 --- /dev/null +++ b/apps/meteor/client/views/audit/components/AuditModalLabel.tsx @@ -0,0 +1,8 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ComponentPropsWithoutRef } from 'react'; + +type AuditModalLabelProps = ComponentPropsWithoutRef; + +const AuditModalLabel = (props: AuditModalLabelProps) => ; + +export default AuditModalLabel; diff --git a/apps/meteor/client/views/audit/components/AuditModalText.tsx b/apps/meteor/client/views/audit/components/AuditModalText.tsx new file mode 100644 index 0000000000000..04f898d578d81 --- /dev/null +++ b/apps/meteor/client/views/audit/components/AuditModalText.tsx @@ -0,0 +1,13 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box } from '@rocket.chat/fuselage'; +import type { ComponentPropsWithoutRef } from 'react'; + +const wordBreak = css` + word-break: break-word; +`; + +type AuditModalTextProps = ComponentPropsWithoutRef; + +const AuditModalText = (props: AuditModalTextProps) => ; + +export default AuditModalText; diff --git a/apps/meteor/client/views/audit/components/SecurityLogDisplayModal.spec.tsx b/apps/meteor/client/views/audit/components/SecurityLogDisplayModal.spec.tsx new file mode 100644 index 0000000000000..1a3898ee9b8c2 --- /dev/null +++ b/apps/meteor/client/views/audit/components/SecurityLogDisplayModal.spec.tsx @@ -0,0 +1,21 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { composeStories } from '@storybook/react'; +import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; + +import * as stories from './SecurityLogDisplayModal.stories'; + +const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]); + +test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => { + const view = render(, { wrapper: mockAppRoot().build() }); + expect(view.baseElement).toMatchSnapshot(); +}); + +test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => { + const { container } = render(, { wrapper: mockAppRoot().build() }); + + const results = await axe(container); + expect(results).toHaveNoViolations(); +}); diff --git a/apps/meteor/client/views/audit/components/SecurityLogDisplayModal.stories.tsx b/apps/meteor/client/views/audit/components/SecurityLogDisplayModal.stories.tsx new file mode 100644 index 0000000000000..bb9a7b952706c --- /dev/null +++ b/apps/meteor/client/views/audit/components/SecurityLogDisplayModal.stories.tsx @@ -0,0 +1,31 @@ +import type { Meta, StoryFn } from '@storybook/react'; + +import SecurityLogDisplayModal from './SecurityLogDisplayModal'; + +export default { + title: 'views/Audit/SecurityLogDisplay', + component: SecurityLogDisplayModal, + args: { + timestamp: 'Thursday, 20-Mar-25 17:17:46', + actor: { + type: 'user', + _id: 'user-id', + username: 'username', + useragent: 'useragent', + ip: '127.0.0.1', + }, + setting: 'Show_message_in_email_notification', + changedFrom: 'false', + changedTo: 'true', + }, +} satisfies Meta; + +export const Default: StoryFn = (args) => ; + +export const system: StoryFn = (args) => ( + +); + +export const app: StoryFn = (args) => ( + +); diff --git a/apps/meteor/client/views/audit/components/SecurityLogDisplayModal.tsx b/apps/meteor/client/views/audit/components/SecurityLogDisplayModal.tsx new file mode 100644 index 0000000000000..ae7d23ff9b401 --- /dev/null +++ b/apps/meteor/client/views/audit/components/SecurityLogDisplayModal.tsx @@ -0,0 +1,85 @@ +import type { IAuditServerUserActor, IAuditServerSystemActor, IAuditServerAppActor } from '@rocket.chat/core-typings'; +import { Box } from '@rocket.chat/fuselage'; +import { UserAvatar } from '@rocket.chat/ui-avatar'; +import { format } from 'date-fns'; +import { useTranslation } from 'react-i18next'; + +import { AppInfoField } from './AppInfoField'; +import AuditModalField from './AuditModalField'; +import AuditModalLabel from './AuditModalLabel'; +import AuditModalText from './AuditModalText'; +import GenericModal from '../../../components/GenericModal'; + +type SecurityLogDisplayProps = { + timestamp: string; + actor: IAuditServerUserActor | IAuditServerSystemActor | IAuditServerAppActor; + setting: string; + changedFrom: string; + changedTo: string; + onCancel: () => void; +}; + +const SecurityLogDisplayModal = ({ timestamp, actor, setting, changedFrom, changedTo, onCancel }: SecurityLogDisplayProps) => { + const { t } = useTranslation(); + + return ( + + + {t('Timestamp')} + {format(new Date(timestamp), 'MMMM d yyyy, h:mm:ss a')} + + + {actor.type === 'user' && ( + + {t('Actor')} + + {actor.type === 'user' && } + + {actor.username} + + + + )} + + {actor.type === 'app' && } + + {actor.type === 'system' && ( + <> + + {t('Actor')} + {t('System')} + + + + {t('Reason')} + {actor.reason} + + + )} + + + {t('Setting')} + {t(setting)} + + + + {t('Changed_from')} + {changedFrom} + + + + {t('Changed_to')} + {changedTo} + + + ); +}; + +export default SecurityLogDisplayModal; diff --git a/apps/meteor/client/views/audit/components/SecurityLogsTable.tsx b/apps/meteor/client/views/audit/components/SecurityLogsTable.tsx new file mode 100644 index 0000000000000..2286bd81cf6f5 --- /dev/null +++ b/apps/meteor/client/views/audit/components/SecurityLogsTable.tsx @@ -0,0 +1,219 @@ +import type { IAuditServerAppActor, IAuditServerSystemActor, IAuditServerUserActor } from '@rocket.chat/core-typings'; +import { Box, Button, ButtonGroup, Field, FieldLabel, Margins, Pagination } from '@rocket.chat/fuselage'; +import { UserAvatar } from '@rocket.chat/ui-avatar'; +import { useEndpoint, useSetModal } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; +import { format } from 'date-fns'; +import { useState, type ReactElement } from 'react'; +import { useTranslation } from 'react-i18next'; + +import SecurityLogDisplayModal from './SecurityLogDisplayModal'; +import { SettingSelect } from './SettingSelect'; +import DateRangePicker from './forms/DateRangePicker'; +import GenericNoResults from '../../../components/GenericNoResults'; +import { + GenericTable, + GenericTableBody, + GenericTableCell, + GenericTableHeader, + GenericTableHeaderCell, + GenericTableLoadingRow, + GenericTableRow, +} from '../../../components/GenericTable'; +import { usePagination } from '../../../components/GenericTable/hooks/usePagination'; +import type { DateRange } from '../utils/dateRange'; +import { getTypeTranslation } from '../utils/getAppTypeTranslation'; + +const SecurityLogsTable = (): ReactElement => { + const { t } = useTranslation(); + const [setting, setSetting] = useState(''); + + const setModal = useSetModal(); + + const [dateRange, setDateRange] = useState(() => ({ + start: undefined, + end: undefined, + })); + + const [query, setQuery] = useState({ + start: new Date(0).toISOString(), + end: new Date().toISOString(), + settingId: '', + }); + + const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination(); + + const handleClearFilters = () => { + setSetting(''); + setDateRange({ start: undefined, end: undefined }); + setQuery({ + start: new Date(0).toISOString(), + end: new Date().toISOString(), + settingId: '', + }); + onSetCurrent(0); + }; + + const handleApplyFilters = () => { + const { start, end } = dateRange; + setQuery({ + start: start?.toISOString() ?? new Date(0).toISOString(), + end: end?.toISOString() ?? new Date().toISOString(), + settingId: setting, + }); + onSetCurrent(0); + }; + + const handleItemClick = ({ + actor, + timestamp, + setting, + changedFrom, + changedTo, + }: { + actor: IAuditServerUserActor | IAuditServerSystemActor | IAuditServerAppActor; + timestamp: string; + setting: unknown; + changedFrom: string; + changedTo: string; + }) => { + setModal( + setModal(null)} + />, + ); + }; + + const getAudits = useEndpoint('GET', '/v1/audit.settings'); + + const { data, isLoading, isSuccess } = useQuery({ + queryKey: ['audit.settings', query, itemsPerPage, current], + + queryFn: async () => { + return getAudits({ ...query, ...(itemsPerPage && { count: itemsPerPage }), ...(current && { offset: current }) }); + }, + }); + + return ( + <> + + + + {t('Date')} + + + + + + {t('Setting')} + + + + + + + + + + + + {isLoading && ( + + + {t('Actor')} + {t('Timestamp')} + {t('Setting')} + {t('Changed_from')} + {t('Changed_to')} + + + + + + )} + {isSuccess && data.total === 0 && ( + + )} + {isSuccess && data.total > 0 && ( + + + {t('Actor')} + {t('Timestamp')} + {t('Setting')} + {t('Changed_from')} + {t('Changed_to')} + + + {data.events.map((item) => { + const setting = item.data.find((item) => item.key === 'id')?.value; + const previous = item.data.find((item) => item.key === 'previous')?.value || t('Empty'); + const current = item.data.find((item) => item.key === 'current')?.value || t('Empty'); + return ( + + handleItemClick({ + actor: item.actor, + timestamp: item.ts, + setting, + changedFrom: String(previous), + changedTo: String(current), + }) + } + > + + + {item.actor.type === 'user' && ( + + + + )} + + {item.actor.type === 'user' ? item.actor.username : t(getTypeTranslation(item.actor.type))} + + + + {format(new Date(item.ts), 'MMMM d yyyy, h:mm:ss a')} + + {setting && String(setting)} + + {String(previous)} + {String(current)} + + ); + })} + + + )} + + + ); +}; + +export default SecurityLogsTable; diff --git a/apps/meteor/client/views/audit/components/SettingSelect.tsx b/apps/meteor/client/views/audit/components/SettingSelect.tsx new file mode 100644 index 0000000000000..2baa9c4958d75 --- /dev/null +++ b/apps/meteor/client/views/audit/components/SettingSelect.tsx @@ -0,0 +1,34 @@ +import { Option, PaginatedSelectFiltered } from '@rocket.chat/fuselage'; +import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useSettingSelectOptions } from '../hooks/useSettingSelectOptions'; + +export const SettingSelect = ({ value, onChange }: { value: string; onChange: (value: string) => void }) => { + const { t } = useTranslation(); + const [filter, setFilter] = useState(''); + + const debouncedFilter = useDebouncedValue(filter, 500); + + const { data, fetchNextPage, isFetchingNextPage } = useSettingSelectOptions(debouncedFilter); + const flattenedData = data?.pages.flatMap((page) => page) || []; + + return ( + onChange(val)} + placeholder={t('All_Settings')} + filter={filter} + setFilter={setFilter as (value: string | number | undefined) => void} + options={flattenedData} + endReached={() => !isFetchingNextPage && fetchNextPage({ cancelRefetch: true })} + renderItem={({ label, ...props }) => ( + + )} + /> + ); +}; diff --git a/apps/meteor/client/views/audit/components/__snapshots__/AppInfoField.spec.tsx.snap b/apps/meteor/client/views/audit/components/__snapshots__/AppInfoField.spec.tsx.snap new file mode 100644 index 0000000000000..872c71313ed24 --- /dev/null +++ b/apps/meteor/client/views/audit/components/__snapshots__/AppInfoField.spec.tsx.snap @@ -0,0 +1,77 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders Default without crashing 1`] = ` + +
+
+
+ Actor +
+
+ App +
+
+
+
+ App_id +
+ +
+ app-id +
+
+
+ +`; + +exports[`renders NoAppInfo without crashing 1`] = ` + +
+
+
+ Actor +
+
+ App +
+
+
+
+ App Id +
+ +
+ app-id +
+
+
+ +`; diff --git a/apps/meteor/client/views/audit/components/__snapshots__/SecurityLogDisplayModal.spec.tsx.snap b/apps/meteor/client/views/audit/components/__snapshots__/SecurityLogDisplayModal.spec.tsx.snap new file mode 100644 index 0000000000000..50fc75fba27a3 --- /dev/null +++ b/apps/meteor/client/views/audit/components/__snapshots__/SecurityLogDisplayModal.spec.tsx.snap @@ -0,0 +1,412 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders Default without crashing 1`] = ` + +
+ +
+
+
+
+

+ Setting_change +

+
+
+
+
+
+
+
+ Timestamp +
+
+ March 20 2025, 5:17:46 PM +
+
+
+
+ Actor +
+
+
+ +
+
+ username +
+
+
+
+
+ Setting +
+
+ Show_message_in_email_notification +
+
+
+
+ Changed_from +
+
+ false +
+
+
+
+ Changed_to +
+
+ true +
+
+
+
+ +
+
+ +`; + +exports[`renders app without crashing 1`] = ` + +
+ +
+
+
+
+

+ Setting_change +

+
+
+
+
+
+
+
+ Timestamp +
+
+ March 20 2025, 5:17:46 PM +
+
+
+
+ Actor +
+
+ App +
+
+
+
+ App_id +
+ +
+ app-id +
+
+
+
+ Setting +
+
+ Show_message_in_email_notification +
+
+
+
+ Changed_from +
+
+ false +
+
+
+
+ Changed_to +
+
+ true +
+
+
+
+ +
+
+ +`; + +exports[`renders system without crashing 1`] = ` + +
+ +
+
+
+
+

+ Setting_change +

+
+
+
+
+
+
+
+ Timestamp +
+
+ March 20 2025, 5:17:46 PM +
+
+
+
+ Actor +
+
+ System +
+
+
+
+ Reason +
+
+ update +
+
+
+
+ Setting +
+
+ Show_message_in_email_notification +
+
+
+
+ Changed_from +
+
+ false +
+
+
+
+ Changed_to +
+
+ true +
+
+
+
+ +
+
+ +`; diff --git a/apps/meteor/client/views/audit/hooks/useSettingSelectOptions.spec.ts b/apps/meteor/client/views/audit/hooks/useSettingSelectOptions.spec.ts new file mode 100644 index 0000000000000..3a72f659c91e3 --- /dev/null +++ b/apps/meteor/client/views/audit/hooks/useSettingSelectOptions.spec.ts @@ -0,0 +1,78 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { renderHook, waitFor } from '@testing-library/react'; + +import { useSettingSelectOptions } from './useSettingSelectOptions'; + +// TODO: check if the return of items matches the settings we mocked +describe('useSettingSelectOptions', () => { + it('should return the ordered list of options', async () => { + const { result } = renderHook(() => useSettingSelectOptions(), { + wrapper: mockAppRoot() + .withSetting('test1', true) + .withSetting('test2', true) + .withSetting('test3', true) + .withSetting('test4', true) + .withSetting('test5', true) + .withSetting('test6', true) + .withSetting('test7', true) + .withSetting('test8', true) + .withSetting('test9', true) + .withSetting('test10', true) + .withSetting('test11', true) + .withSetting('test12', true) + .withSetting('test13', true) + .withSetting('test14', true) + .withSetting('test15', true) + .build(), + }); + + await waitFor(() => expect(result.current?.data?.pages[0][0]).toEqual({ _id: 'test1', label: 'test1', value: 'test1' })); + await waitFor(() => expect(result.current?.data?.pages[0][1]).toEqual({ _id: 'test2', label: 'test2', value: 'test2' })); + await waitFor(() => expect(result.current?.data?.pages[0][2]).toEqual({ _id: 'test3', label: 'test3', value: 'test3' })); + await waitFor(() => expect(result.current?.data?.pages[0][3]).toEqual({ _id: 'test4', label: 'test4', value: 'test4' })); + await waitFor(() => expect(result.current?.data?.pages[0][4]).toEqual({ _id: 'test5', label: 'test5', value: 'test5' })); + await waitFor(() => expect(result.current?.data?.pages[0][5]).toEqual({ _id: 'test6', label: 'test6', value: 'test6' })); + await waitFor(() => expect(result.current?.data?.pages[0][6]).toEqual({ _id: 'test7', label: 'test7', value: 'test7' })); + await waitFor(() => expect(result.current?.data?.pages[0][7]).toEqual({ _id: 'test8', label: 'test8', value: 'test8' })); + await waitFor(() => expect(result.current?.data?.pages[0][8]).toEqual({ _id: 'test9', label: 'test9', value: 'test9' })); + await waitFor(() => expect(result.current?.data?.pages[0][9]).toEqual({ _id: 'test10', label: 'test10', value: 'test10' })); + await waitFor(() => expect(result.current?.data?.pages[0][10]).toEqual({ _id: 'test11', label: 'test11', value: 'test11' })); + await waitFor(() => expect(result.current?.data?.pages[0][11]).toEqual({ _id: 'test12', label: 'test12', value: 'test12' })); + await waitFor(() => expect(result.current?.data?.pages[0][12]).toEqual({ _id: 'test13', label: 'test13', value: 'test13' })); + await waitFor(() => expect(result.current?.data?.pages[0][13]).toEqual({ _id: 'test14', label: 'test14', value: 'test14' })); + await waitFor(() => expect(result.current?.data?.pages[0][14]).toEqual({ _id: 'test15', label: 'test15', value: 'test15' })); + + await waitFor(() => expect(result.current?.data?.pages[0]).toHaveLength(15)); + }); + + it('should return the list of filtered options', async () => { + const { result } = renderHook(() => useSettingSelectOptions('TeSt1'), { + wrapper: mockAppRoot() + .withSetting('test1', true) + .withSetting('test2', true) + .withSetting('test3', true) + .withSetting('test4', true) + .withSetting('test5', true) + .withSetting('test6', true) + .withSetting('test7', true) + .withSetting('test8', true) + .withSetting('test9', true) + .withSetting('test10', true) + .withSetting('test11', true) + .withSetting('test12', true) + .withSetting('test13', true) + .withSetting('test14', true) + .withSetting('test15', true) + .build(), + }); + + await waitFor(() => expect(result.current?.data?.pages[0][0]).toEqual({ _id: 'test1', label: 'test1', value: 'test1' })); + await waitFor(() => expect(result.current?.data?.pages[0][1]).toEqual({ _id: 'test10', label: 'test10', value: 'test10' })); + await waitFor(() => expect(result.current?.data?.pages[0][2]).toEqual({ _id: 'test11', label: 'test11', value: 'test11' })); + await waitFor(() => expect(result.current?.data?.pages[0][3]).toEqual({ _id: 'test12', label: 'test12', value: 'test12' })); + await waitFor(() => expect(result.current?.data?.pages[0][4]).toEqual({ _id: 'test13', label: 'test13', value: 'test13' })); + await waitFor(() => expect(result.current?.data?.pages[0][5]).toEqual({ _id: 'test14', label: 'test14', value: 'test14' })); + await waitFor(() => expect(result.current?.data?.pages[0][6]).toEqual({ _id: 'test15', label: 'test15', value: 'test15' })); + await waitFor(() => expect(result.current?.data?.pages[0]).toHaveLength(7)); + }); +}); diff --git a/apps/meteor/client/views/audit/hooks/useSettingSelectOptions.ts b/apps/meteor/client/views/audit/hooks/useSettingSelectOptions.ts new file mode 100644 index 0000000000000..e7342690b13a7 --- /dev/null +++ b/apps/meteor/client/views/audit/hooks/useSettingSelectOptions.ts @@ -0,0 +1,41 @@ +import { useSettings } from '@rocket.chat/ui-contexts'; +import { useInfiniteQuery } from '@tanstack/react-query'; +import { useCallback } from 'react'; + +type SettingSelectOption = { + label: string; + value: string; + _id: string; +}; + +export const useSettingSelectOptions = (filter = '') => { + const settings = useSettings(); + + const fetchData = useCallback( + async (start = 0): Promise => { + return settings + .map(({ _id }) => ({ label: _id, value: _id, _id })) + .filter(({ label }) => label.toUpperCase().includes(filter.toUpperCase())) + .slice(start, start + 50); + }, + [filter, settings], + ); + + return useInfiniteQuery({ + queryKey: ['settings', filter], + queryFn: ({ pageParam }) => fetchData(pageParam), + getNextPageParam: (lastPage, _allPages, lastPageParam) => { + if (lastPage.length === 0) { + return undefined; + } + return lastPageParam + 1; + }, + getPreviousPageParam: (_firstPage, _allPages, firstPageParam) => { + if (firstPageParam <= 1) { + return undefined; + } + return firstPageParam - 1; + }, + initialPageParam: 0, + }); +}; diff --git a/apps/meteor/client/views/audit/utils/getAppTypeTranslation.ts b/apps/meteor/client/views/audit/utils/getAppTypeTranslation.ts new file mode 100644 index 0000000000000..e225053a8afdc --- /dev/null +++ b/apps/meteor/client/views/audit/utils/getAppTypeTranslation.ts @@ -0,0 +1 @@ +export const getTypeTranslation = (type: 'app' | 'system') => (type === 'app' ? 'App' : 'System'); diff --git a/apps/meteor/client/views/home/HomePage.tsx b/apps/meteor/client/views/home/HomePage.tsx index a8a0544e3d870..1cc4654eeea12 100644 --- a/apps/meteor/client/views/home/HomePage.tsx +++ b/apps/meteor/client/views/home/HomePage.tsx @@ -1,16 +1,10 @@ import { useSetting } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import { useEffect } from 'react'; import CustomHomePage from './CustomHomePage'; import DefaultHomePage from './DefaultHomePage'; -import { KonchatNotification } from '../../../app/ui/client/lib/KonchatNotification'; const HomePage = (): ReactElement => { - useEffect(() => { - KonchatNotification.getDesktopPermission(); - }, []); - const customOnly = useSetting('Layout_Custom_Body_Only'); if (customOnly) { diff --git a/apps/meteor/client/views/hooks/useRequire2faSetup.ts b/apps/meteor/client/views/hooks/useRequire2faSetup.ts new file mode 100644 index 0000000000000..3aa34450e671e --- /dev/null +++ b/apps/meteor/client/views/hooks/useRequire2faSetup.ts @@ -0,0 +1,22 @@ +import { useSetting, useUser } from '@rocket.chat/ui-contexts'; +import { useCallback } from 'react'; + +import { Roles } from '../../../app/models/client'; +import { useReactiveValue } from '../../hooks/useReactiveValue'; + +export const useRequire2faSetup = () => { + const user = useUser(); + const tfaEnabled = useSetting('Accounts_TwoFactorAuthentication_Enabled'); + + return useReactiveValue( + useCallback(() => { + // User is already using 2fa + if (!user || user?.services?.totp?.enabled || user?.services?.email2fa?.enabled) { + return false; + } + + const mandatoryRole = Roles.findOne({ _id: { $in: user.roles ?? [] }, mandatory2fa: true }); + return !!(mandatoryRole !== undefined && tfaEnabled); + }, [tfaEnabled, user]), + ); +}; diff --git a/apps/meteor/client/views/invite/InvitePage.tsx b/apps/meteor/client/views/invite/InvitePage.tsx index 97689a832f89b..569d11c4cde04 100644 --- a/apps/meteor/client/views/invite/InvitePage.tsx +++ b/apps/meteor/client/views/invite/InvitePage.tsx @@ -21,9 +21,13 @@ const InvitePage = (): ReactElement => { const getInviteRoomMutation = useInviteTokenMutation(); useEffect(() => { + // TODO: this is so hacky, get from the url and set the storage setToken(token || null); - if (userId && token) { - getInviteRoomMutation(token); + }, [setToken, token]); + + useEffect(() => { + if (userId && token && !getInviteRoomMutation.submittedAt) { + getInviteRoomMutation.mutate(token); } }, [getInviteRoomMutation, setToken, token, userId]); diff --git a/apps/meteor/client/views/invite/hooks/useInviteTokenMutation.ts b/apps/meteor/client/views/invite/hooks/useInviteTokenMutation.ts index f6e0c58e7f102..057118e636bf7 100644 --- a/apps/meteor/client/views/invite/hooks/useInviteTokenMutation.ts +++ b/apps/meteor/client/views/invite/hooks/useInviteTokenMutation.ts @@ -10,7 +10,7 @@ export const useInviteTokenMutation = () => { const getInviteRoom = useEndpoint('POST', '/v1/useInviteToken'); - const { mutate } = useMutation({ + return useMutation({ mutationFn: (token: string) => getInviteRoom({ token }), onSuccess: (result) => { if (!result.room.name) { @@ -31,6 +31,4 @@ export const useInviteTokenMutation = () => { router.navigate('/home'); }, }); - - return mutate; }; diff --git a/apps/meteor/client/views/invite/hooks/useValidateInviteQuery.ts b/apps/meteor/client/views/invite/hooks/useValidateInviteQuery.ts index 869f707f1cf3b..fa8e42cf1d458 100644 --- a/apps/meteor/client/views/invite/hooks/useValidateInviteQuery.ts +++ b/apps/meteor/client/views/invite/hooks/useValidateInviteQuery.ts @@ -41,7 +41,7 @@ export const useValidateInviteQuery = (userId: string | null, token: string | un return; } - return getInviteRoomMutation(token); + return getInviteRoomMutation.mutate(token); }; onSuccess(); diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.spec.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.spec.tsx new file mode 100644 index 0000000000000..d6bae87ccc599 --- /dev/null +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.spec.tsx @@ -0,0 +1,150 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import AppDetailsPage from './AppDetailsPage'; +import { AppClientOrchestratorInstance } from '../../../apps/orchestrator'; +import { useAppInfo } from '../hooks/useAppInfo'; + +jest.mock('../hooks/useAppInfo', () => ({ + useAppInfo: jest.fn(), +})); + +jest.mock('@rocket.chat/ui-contexts', () => { + const originalModule = jest.requireActual('@rocket.chat/ui-contexts'); + return { + ...originalModule, + useRouter: () => ({ navigate: jest.fn() }), + useToastMessageDispatch: () => jest.fn(), + usePermission: () => true, + useRouteParameter: () => 'settings', + }; +}); + +jest.mock('../../../components/Page', () => { + const originalModule = jest.requireActual('../../../components/Page'); + return { + ...originalModule, + PageHeader: ({ children }: { children: React.ReactNode }) =>
{children}
, + PageFooter: ({ children, isDirty }: { children: React.ReactNode; isDirty: boolean }) => isDirty &&
{children}
, + }; +}); + +jest.mock('./AppDetailsPageHeader', () => ({ + __esModule: true, + default: () =>
AppDetailsPageHeader
, +})); + +jest.mock('../../../apps/orchestrator', () => ({ + AppClientOrchestratorInstance: { + setAppSettings: jest.fn(), + }, +})); + +const wrapper = mockAppRoot().withTranslations('en', 'core', { Save_changes: 'Save changes' }); +describe('AppDetailsPage', () => { + beforeEach(() => { + (useAppInfo as jest.Mock).mockReturnValue({ + id: 'app123', + name: 'Test App', + installed: true, + settings: { + setting1: { id: 'setting1', value: 'old-value', packageValue: 'default-value', type: 'string' }, + }, + privacyPolicySummary: '', + permissions: [], + tosLink: '', + privacyLink: '', + }); + (AppClientOrchestratorInstance.setAppSettings as jest.Mock).mockReset(); + }); + + it('should not display the Save button initially', async () => { + render(, { + wrapper: wrapper.build(), + legacyRoot: true, + }); + + await waitFor(() => { + expect(screen.queryByRole('button', { name: 'Save changes' })).not.toBeInTheDocument(); + }); + }); + + it('should display the Save button when a setting is changed', async () => { + render(, { + wrapper: wrapper.build(), + legacyRoot: true, + }); + + const settingInput = screen.getByLabelText('setting1'); + await userEvent.clear(settingInput); + await userEvent.type(settingInput, 'new-value'); + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Save changes' })).toBeVisible(); + }); + }); + + it('should disable the Save button during submission', async () => { + (AppClientOrchestratorInstance.setAppSettings as jest.Mock).mockResolvedValueOnce(new Promise((resolve) => setTimeout(resolve, 500))); + + render(, { + wrapper: wrapper.build(), + legacyRoot: true, + }); + + const settingInput = screen.getByLabelText('setting1'); + await userEvent.clear(settingInput); + await userEvent.type(settingInput, 'new-value'); + + const saveButton = screen.getByRole('button', { name: 'Save changes' }); + + await userEvent.click(saveButton); + + await waitFor(() => { + expect(saveButton).toBeDisabled(); + }); + }); + + it('should hide the Save button after successful save', async () => { + (AppClientOrchestratorInstance.setAppSettings as jest.Mock).mockResolvedValueOnce(new Promise((resolve) => setTimeout(resolve, 500))); + + render(, { + wrapper: wrapper.build(), + legacyRoot: true, + }); + + const settingInput = screen.getByLabelText('setting1'); + await userEvent.clear(settingInput); + await userEvent.type(settingInput, 'new-value'); + + const saveButton = screen.getByRole('button', { name: 'Save changes' }); + await userEvent.click(saveButton); + + await waitFor(() => { + expect(screen.queryByRole('button', { name: 'Save changes' })).not.toBeInTheDocument(); + }); + }); + + it('should call setAppSettings with updated setting value', async () => { + (AppClientOrchestratorInstance.setAppSettings as jest.Mock).mockResolvedValueOnce(new Promise((resolve) => setTimeout(resolve, 500))); + + render(, { + wrapper: wrapper.build(), + legacyRoot: true, + }); + + const settingInput = screen.getByLabelText('setting1'); + await userEvent.clear(settingInput); + await userEvent.type(settingInput, 'new-value'); + + const saveButton = screen.getByRole('button', { name: 'Save changes' }); + await userEvent.click(saveButton); + + await waitFor(() => { + expect(AppClientOrchestratorInstance.setAppSettings as jest.Mock).toHaveBeenCalledWith('app123', [ + { id: 'setting1', packageValue: 'default-value', type: 'string', value: 'new-value' }, + ]); + }); + }); +}); diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx index 45aeef3212a65..2b384e27082f1 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx @@ -51,6 +51,20 @@ const AppDetailsPage = ({ id }: AppDetailsPageProps): ReactElement => { const { installed, settings, privacyPolicySummary, permissions, tosLink, privacyLink, name } = appData || {}; const isSecurityVisible = Boolean(privacyPolicySummary || permissions || tosLink || privacyLink); + const reducedSettings = useMemo((): AppDetailsPageFormData => { + return Object.values(settings || {}).reduce( + (ret: AppDetailsPageFormData, { id, value, packageValue }) => ({ ...ret, [id]: value ?? packageValue }), + {}, + ); + }, [settings]); + + const methods = useForm({ values: reducedSettings }); + const { + handleSubmit, + reset, + formState: { isDirty, isSubmitting }, + } = methods; + const saveAppSettings = useCallback( async (data: AppDetailsPageFormData) => { try { @@ -61,29 +75,15 @@ const AppDetailsPage = ({ id }: AppDetailsPageProps): ReactElement => { value: data[setting.id], })), ); - - dispatchToastMessage({ type: 'success', message: `${name} settings saved succesfully` }); + reset(data); + dispatchToastMessage({ type: 'success', message: t('App_Settings_Saved_Successfully', { appName: name }) }); } catch (e: any) { handleAPIError(e); } }, - [dispatchToastMessage, id, name, settings], + [dispatchToastMessage, id, name, settings, reset], ); - const reducedSettings = useMemo((): AppDetailsPageFormData => { - return Object.values(settings || {}).reduce( - (ret: AppDetailsPageFormData, { id, value, packageValue }) => ({ ...ret, [id]: value ?? packageValue }), - {}, - ); - }, [settings]); - - const methods = useForm({ values: reducedSettings }); - const { - handleSubmit, - reset, - formState: { isDirty, isSubmitting, isSubmitted }, - } = methods; - return ( @@ -125,7 +125,7 @@ const AppDetailsPage = ({ id }: AppDetailsPageProps): ReactElement => { {installed && isAdminUser && ( - )} diff --git a/apps/meteor/client/views/marketplace/MarketplaceSidebar.tsx b/apps/meteor/client/views/marketplace/MarketplaceSidebar.tsx index 9232d660d8ad2..a10e01383fb62 100644 --- a/apps/meteor/client/views/marketplace/MarketplaceSidebar.tsx +++ b/apps/meteor/client/views/marketplace/MarketplaceSidebar.tsx @@ -16,7 +16,7 @@ const MarketplaceSidebar = (): ReactElement => { const currentPath = useCurrentRoutePath(); return ( - + diff --git a/apps/meteor/client/views/omnichannel/businessHours/EditBusinessHours.tsx b/apps/meteor/client/views/omnichannel/businessHours/EditBusinessHours.tsx index 7d459cfe5a96e..63f7ece3d600c 100644 --- a/apps/meteor/client/views/omnichannel/businessHours/EditBusinessHours.tsx +++ b/apps/meteor/client/views/omnichannel/businessHours/EditBusinessHours.tsx @@ -25,7 +25,7 @@ const getInitialData = (businessHourData: Serialized | un open, })), departmentsToApplyBusinessHour: '', - active: businessHourData?.active || true, + active: businessHourData?.active ?? true, departments: businessHourData?.departments?.map(({ _id, name }) => ({ value: _id, label: name })) || [], }); diff --git a/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/ReviewContactModal.tsx b/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/ReviewContactModal.tsx index bb5eacfe17d9a..7ca34aa3c4dc7 100644 --- a/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/ReviewContactModal.tsx +++ b/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/ReviewContactModal.tsx @@ -47,7 +47,7 @@ const ReviewContactModal = ({ contact, onCancel }: ReviewContactModalProps) => { const payload = { name, contactManager, - ...(customFields && { ...customFields }), + ...(customFields && { customFields }), wipeConflicts: true, }; @@ -86,7 +86,7 @@ const ReviewContactModal = ({ contact, onCancel }: ReviewContactModalProps) => { return ( - {t(label as TranslationKey)} + {t(label as TranslationKey)} { rules={{ required: isContactManagerField ? undefined : t('Required_field', { field: t(label as TranslationKey) }), }} - render={({ field: { value, onChange } }) => } + render={({ field: { value, onChange } }) => ( + + )} /> - + {t('different_values_found', { number: values.length })} - {errors?.[name] && {errors?.[name]?.message}} + {errors?.[name] && {errors?.[name]?.message}} ); })} diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AddAgent.tsx b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AddAgent.tsx index 86cb80c7c4d5c..7ddbdb66a6b41 100644 --- a/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AddAgent.tsx +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AddAgent.tsx @@ -1,6 +1,7 @@ import { Box, Button } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import type { AriaAttributes } from 'react'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,7 +9,12 @@ import AutoCompleteAgent from '../../../../components/AutoCompleteAgent'; import { useEndpointAction } from '../../../../hooks/useEndpointAction'; import type { IDepartmentAgent } from '../definitions'; -function AddAgent({ agentList, onAdd }: { agentList: IDepartmentAgent[]; onAdd: (agent: IDepartmentAgent) => void }) { +type AddAgentProps = Pick & { + agentList: IDepartmentAgent[]; + onAdd: (agent: IDepartmentAgent) => void; +}; + +function AddAgent({ agentList, onAdd, 'aria-labelledby': ariaLabelledBy }: AddAgentProps) { const { t } = useTranslation(); const [userId, setUserId] = useState(''); @@ -37,7 +43,7 @@ function AddAgent({ agentList, onAdd }: { agentList: IDepartmentAgent[]; onAdd: }); return ( - + - -
- - - )} - - {(authEnabled || !hasOutlookMethods) && ( - <> - - {(total === 0 || calendarListResult.isError) && ( - - {calendarListResult.isError && ( - - - {t('Something_went_wrong')} - {getErrorMessage(calendarListResult.error)} - - )} - {!calendarListResult.isError && total === 0 && ( - - - {t('No_history')} - - )} - - )} - {calendarListResult.isSuccess && calendarListResult.data.length > 0 && ( - - - } - /> - - - )} - - + + + {calendarListResult.isPending && } + {calendarListResult.isError && ( + + + {t('Something_went_wrong')} + {getErrorMessage(calendarListResult.error)} + + )} + {!calendarListResult.isPending && total === 0 && ( + + + {t('No_history')} + + )} + {calendarListResult.isSuccess && calendarListResult.data.length > 0 && ( + + } + /> + + )} + + + + + {authEnabled && } + {outlookUrl && ( + + )} + + {hasOutlookMethods && ( + - {authEnabled && } - {outlookUrl && ( - - )} + - {hasOutlookMethods && ( - - - - - - )} - - - )} +
+ )} + ); }; diff --git a/apps/meteor/client/views/room/Announcement/Announcement.stories.tsx b/apps/meteor/client/views/room/Announcement/Announcement.stories.tsx deleted file mode 100644 index 383375a72a8b8..0000000000000 --- a/apps/meteor/client/views/room/Announcement/Announcement.stories.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { action } from '@storybook/addon-actions'; -import type { Meta, StoryFn } from '@storybook/react'; - -import Announcement from '.'; - -export default { - title: 'Room/Announcement', - component: Announcement, -} satisfies Meta; - -export const Default: StoryFn = (args) => ; -Default.storyName = 'Announcement'; -Default.args = { - announcement: 'Lorem Ipsum Indolor', - announcementDetails: action('announcementDetails'), -}; diff --git a/apps/meteor/client/views/room/Announcement/Announcement.tsx b/apps/meteor/client/views/room/Announcement/Announcement.tsx deleted file mode 100644 index 1ccc1c5cc217f..0000000000000 --- a/apps/meteor/client/views/room/Announcement/Announcement.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useSetModal } from '@rocket.chat/ui-contexts'; -import type { MouseEvent } from 'react'; -import { useTranslation } from 'react-i18next'; - -import AnnouncementComponent from './AnnouncementComponent'; -import GenericModal from '../../../components/GenericModal'; -import MarkdownText from '../../../components/MarkdownText'; - -type AnnouncementProps = { - announcement: string; - announcementDetails?: () => void; -}; - -const Announcement = ({ announcement, announcementDetails }: AnnouncementProps) => { - const { t } = useTranslation(); - const setModal = useSetModal(); - const closeModal = useEffectEvent(() => setModal(null)); - const handleClick = (e: MouseEvent): void => { - if ((e.target as HTMLAnchorElement).href) { - return; - } - - if (window?.getSelection()?.toString() !== '') { - return; - } - - announcementDetails - ? announcementDetails() - : setModal( - - - - - , - ); - }; - - return announcement ? ( - ): void => handleClick(e)}> - - - ) : null; -}; - -export default Announcement; diff --git a/apps/meteor/client/views/room/Announcement/index.tsx b/apps/meteor/client/views/room/Announcement/index.tsx deleted file mode 100644 index 396eed77f20ee..0000000000000 --- a/apps/meteor/client/views/room/Announcement/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Announcement'; diff --git a/apps/meteor/client/views/room/Header/DirectRoomHeader.tsx b/apps/meteor/client/views/room/Header/DirectRoomHeader.tsx index 457626a7f91cc..359f566afc823 100644 --- a/apps/meteor/client/views/room/Header/DirectRoomHeader.tsx +++ b/apps/meteor/client/views/room/Header/DirectRoomHeader.tsx @@ -1,9 +1,8 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { useUserId } from '@rocket.chat/ui-contexts'; +import { useUserId, useUserPresence } from '@rocket.chat/ui-contexts'; import type { ReactElement, ReactNode } from 'react'; import RoomHeader from './RoomHeader'; -import { usePresence } from '../../../hooks/usePresence'; type DirectRoomHeaderProps = { room: IRoom; @@ -24,7 +23,7 @@ type DirectRoomHeaderProps = { const DirectRoomHeader = ({ room, slots }: DirectRoomHeaderProps): ReactElement => { const userId = useUserId(); const directUserId = room.uids?.filter((uid) => uid !== userId).shift(); - const directUserData = usePresence(directUserId); + const directUserData = useUserPresence(directUserId); return ; }; diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx index 7f1c20e343d92..ded93aa4741be 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx @@ -16,7 +16,6 @@ import { useCallback, useState, useEffect } from 'react'; import { usePutChatOnHoldMutation } from './usePutChatOnHoldMutation'; import { useReturnChatToQueueMutation } from './useReturnChatToQueueMutation'; -import { LivechatInquiry } from '../../../../../../../app/livechat/client/collections/LivechatInquiry'; import PlaceChatOnHoldModal from '../../../../../../../app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal'; import { LegacyRoomManager } from '../../../../../../../app/ui-utils/client'; import CloseChatModal from '../../../../../../components/Omnichannel/modals/CloseChatModal'; @@ -27,6 +26,7 @@ import TranscriptModal from '../../../../../../components/Omnichannel/modals/Tra import { useIsRoomOverMacLimit } from '../../../../../../hooks/omnichannel/useIsRoomOverMacLimit'; import { useOmnichannelRouteConfig } from '../../../../../../hooks/omnichannel/useOmnichannelRouteConfig'; import { useHasLicenseModule } from '../../../../../../hooks/useHasLicenseModule'; +import { useLivechatInquiryStore } from '../../../../../../hooks/useLivechatInquiryStore'; import { quickActionHooks } from '../../../../../../ui'; import { useOmnichannelRoom } from '../../../../contexts/RoomContext'; import type { QuickActionsActionConfig } from '../../../../lib/quickActions'; @@ -177,6 +177,8 @@ export const useQuickActions = (): { const closeChat = useEndpoint('POST', '/v1/livechat/room.closeByUser'); + const discardForRoom = useLivechatInquiryStore((state) => state.discardForRoom); + const handleClose = useCallback( async ( comment?: string, @@ -199,14 +201,14 @@ export const useQuickActions = (): { } : { transcriptEmail: { sendToVisitor: false } }), }); - LivechatInquiry.remove({ rid }); + discardForRoom(rid); closeModal(); dispatchToastMessage({ type: 'success', message: t('Chat_closed_successfully') }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } }, - [closeChat, closeModal, dispatchToastMessage, rid, t], + [closeChat, closeModal, dispatchToastMessage, rid, t, discardForRoom], ); const returnChatToQueueMutation = useReturnChatToQueueMutation({ diff --git a/apps/meteor/client/views/room/Header/RoomTitle.tsx b/apps/meteor/client/views/room/Header/RoomTitle.tsx index 7937051cc2ba3..2ff1a21650156 100644 --- a/apps/meteor/client/views/room/Header/RoomTitle.tsx +++ b/apps/meteor/client/views/room/Header/RoomTitle.tsx @@ -41,6 +41,7 @@ const RoomTitle = ({ room }: { room: IRoom }): ReactElement => { onClick={() => handleOpenRoomInfo()} tabIndex={0} role='button' + mie={4} > {room.name} diff --git a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolboxE2EESetup.tsx b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolboxE2EESetup.tsx index 36bf5701f2152..1d87a4887084e 100644 --- a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolboxE2EESetup.tsx +++ b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolboxE2EESetup.tsx @@ -1,4 +1,6 @@ +import type { Box } from '@rocket.chat/fuselage'; import { useStableArray } from '@rocket.chat/fuselage-hooks'; +import type { ComponentProps } from 'react'; import { useTranslation } from 'react-i18next'; import { HeaderToolbarAction } from '../../../../components/Header'; @@ -8,7 +10,11 @@ import type { RoomToolboxActionConfig } from '../../contexts/RoomToolboxContext' import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; import { getRoomGroup } from '../../lib/getRoomGroup'; -const RoomToolboxE2EESetup = () => { +type RoomToolboxE2EESetupProps = { + className?: ComponentProps['className']; +}; + +const RoomToolboxE2EESetup = ({ className }: RoomToolboxE2EESetupProps) => { const { t } = useTranslation(); const toolbox = useRoomToolbox(); const room = useRoom(); @@ -29,6 +35,7 @@ const RoomToolboxE2EESetup = () => { {actions.map(({ id, icon, title, action, disabled, tooltip }, index) => ( ); }; diff --git a/apps/meteor/client/views/room/HeaderV2/Header.tsx b/apps/meteor/client/views/room/HeaderV2/Header.tsx index 6501dad41bd40..067abcd88f05c 100644 --- a/apps/meteor/client/views/room/HeaderV2/Header.tsx +++ b/apps/meteor/client/views/room/HeaderV2/Header.tsx @@ -2,10 +2,7 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { isVoipRoom } from '@rocket.chat/core-typings'; import { useLayout, useSetting } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import { lazy, memo, useMemo } from 'react'; - -import { HeaderToolbar } from '../../../components/Header'; -import SidebarToggler from '../../../components/SidebarToggler'; +import { lazy, memo } from 'react'; const OmnichannelRoomHeader = lazy(() => import('./Omnichannel/OmnichannelRoomHeader')); const VoipRoomHeader = lazy(() => import('./Omnichannel/VoipRoomHeader')); @@ -17,39 +14,28 @@ type HeaderProps = { }; const Header = ({ room }: HeaderProps): ReactElement | null => { - const { isMobile, isEmbedded, showTopNavbarEmbeddedLayout } = useLayout(); + const { isEmbedded, showTopNavbarEmbeddedLayout } = useLayout(); const encrypted = Boolean(room.encrypted); const unencryptedMessagesAllowed = useSetting('E2E_Allow_Unencrypted_Messages', false); const shouldDisplayE2EESetup = encrypted && !unencryptedMessagesAllowed; - const slots = useMemo( - () => ({ - start: isMobile && ( - - - - ), - }), - [isMobile], - ); - if (isEmbedded && !showTopNavbarEmbeddedLayout) { return null; } if (room.t === 'l') { - return ; + return ; } if (isVoipRoom(room)) { - return ; + return ; } if (shouldDisplayE2EESetup) { - return ; + return ; } - return ; + return ; }; export default memo(Header); diff --git a/apps/meteor/client/views/room/HeaderV2/HeaderSkeleton.tsx b/apps/meteor/client/views/room/HeaderV2/HeaderSkeleton.tsx index 1839a64fb24a2..25d78f466024d 100644 --- a/apps/meteor/client/views/room/HeaderV2/HeaderSkeleton.tsx +++ b/apps/meteor/client/views/room/HeaderV2/HeaderSkeleton.tsx @@ -1,13 +1,10 @@ import { Skeleton } from '@rocket.chat/fuselage'; -import { Header, HeaderAvatar, HeaderContent, HeaderContentRow } from '../../../components/Header'; +import { Header, HeaderContent, HeaderContentRow } from '../../../components/Header'; const HeaderSkeleton = () => { return (
- - - diff --git a/apps/meteor/client/views/room/HeaderV2/Omnichannel/OmnichannelRoomHeader.tsx b/apps/meteor/client/views/room/HeaderV2/Omnichannel/OmnichannelRoomHeader.tsx index 0c5bc0458cf5c..a937bc93e7208 100644 --- a/apps/meteor/client/views/room/HeaderV2/Omnichannel/OmnichannelRoomHeader.tsx +++ b/apps/meteor/client/views/room/HeaderV2/Omnichannel/OmnichannelRoomHeader.tsx @@ -1,31 +1,14 @@ -import { useLayout, useRouter } from '@rocket.chat/ui-contexts'; -import type { ReactNode } from 'react'; +import { useRouter } from '@rocket.chat/ui-contexts'; import { useCallback, useMemo, useSyncExternalStore } from 'react'; import { HeaderToolbar } from '../../../../components/Header'; -import SidebarToggler from '../../../../components/SidebarToggler'; import { useOmnichannelRoom } from '../../contexts/RoomContext'; import RoomHeader from '../RoomHeader'; import BackButton from './BackButton'; import OmnichannelRoomHeaderTag from './OmnichannelRoomHeaderTag'; import QuickActions from './QuickActions'; -type OmnichannelRoomHeaderProps = { - slots: { - start?: ReactNode; - preContent?: ReactNode; - insideContent?: ReactNode; - posContent?: ReactNode; - end?: ReactNode; - toolbox?: { - pre?: ReactNode; - content?: ReactNode; - pos?: ReactNode; - }; - }; -}; - -const OmnichannelRoomHeader = ({ slots: parentSlot }: OmnichannelRoomHeaderProps) => { +const OmnichannelRoomHeader = () => { const router = useRouter(); const currentRouteName = useSyncExternalStore( @@ -33,22 +16,19 @@ const OmnichannelRoomHeader = ({ slots: parentSlot }: OmnichannelRoomHeaderProps useCallback(() => router.getRouteName(), [router]), ); - const { isMobile } = useLayout(); const room = useOmnichannelRoom(); const slots = useMemo( () => ({ - ...parentSlot, - start: (!!isMobile || currentRouteName === 'omnichannel-directory' || currentRouteName === 'omnichannel-current-chats') && ( + start: (currentRouteName === 'omnichannel-directory' || currentRouteName === 'omnichannel-current-chats') && ( - {isMobile && } ), insideContent: , posContent: , }), - [isMobile, currentRouteName, parentSlot], + [currentRouteName], ); return ; diff --git a/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/useQuickActions.tsx b/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/useQuickActions.tsx index a3a6509bd9a52..b454582f062dc 100644 --- a/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/useQuickActions.tsx +++ b/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/useQuickActions.tsx @@ -15,7 +15,6 @@ import { useCallback, useState, useEffect } from 'react'; import { usePutChatOnHoldMutation } from './usePutChatOnHoldMutation'; import { useReturnChatToQueueMutation } from './useReturnChatToQueueMutation'; -import { LivechatInquiry } from '../../../../../../../app/livechat/client/collections/LivechatInquiry'; import PlaceChatOnHoldModal from '../../../../../../../app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal'; import { LegacyRoomManager } from '../../../../../../../app/ui-utils/client'; import CloseChatModal from '../../../../../../components/Omnichannel/modals/CloseChatModal'; @@ -26,6 +25,7 @@ import TranscriptModal from '../../../../../../components/Omnichannel/modals/Tra import { useIsRoomOverMacLimit } from '../../../../../../hooks/omnichannel/useIsRoomOverMacLimit'; import { useOmnichannelRouteConfig } from '../../../../../../hooks/omnichannel/useOmnichannelRouteConfig'; import { useHasLicenseModule } from '../../../../../../hooks/useHasLicenseModule'; +import { useLivechatInquiryStore } from '../../../../../../hooks/useLivechatInquiryStore'; import { quickActionHooks } from '../../../../../../ui'; import { useOmnichannelRoom } from '../../../../contexts/RoomContext'; import type { QuickActionsActionConfig } from '../../../../lib/quickActions'; @@ -175,6 +175,8 @@ export const useQuickActions = (): { const closeChat = useEndpoint('POST', '/v1/livechat/room.closeByUser'); + const discardForRoom = useLivechatInquiryStore((state) => state.discardForRoom); + const handleClose = useCallback( async ( comment?: string, @@ -197,14 +199,14 @@ export const useQuickActions = (): { } : { transcriptEmail: { sendToVisitor: false } }), }); - LivechatInquiry.remove({ rid }); + discardForRoom(rid); closeModal(); dispatchToastMessage({ type: 'success', message: t('Chat_closed_successfully') }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } }, - [closeChat, closeModal, dispatchToastMessage, rid, t], + [closeChat, closeModal, dispatchToastMessage, rid, t, discardForRoom], ); const returnChatToQueueMutation = useReturnChatToQueueMutation({ diff --git a/apps/meteor/client/views/room/HeaderV2/Omnichannel/VoipRoomHeader.tsx b/apps/meteor/client/views/room/HeaderV2/Omnichannel/VoipRoomHeader.tsx index 9ec8d738d43ec..e8e492478bfc2 100644 --- a/apps/meteor/client/views/room/HeaderV2/Omnichannel/VoipRoomHeader.tsx +++ b/apps/meteor/client/views/room/HeaderV2/Omnichannel/VoipRoomHeader.tsx @@ -1,9 +1,8 @@ import type { IVoipRoom } from '@rocket.chat/core-typings'; -import { useLayout, useRouter } from '@rocket.chat/ui-contexts'; +import { useRouter } from '@rocket.chat/ui-contexts'; import { useCallback, useMemo, useSyncExternalStore } from 'react'; import { HeaderToolbar } from '../../../../components/Header'; -import SidebarToggler from '../../../../components/SidebarToggler'; import { parseOutboundPhoneNumber } from '../../../../lib/voip/parseOutboundPhoneNumber'; import type { RoomHeaderProps } from '../RoomHeader'; import RoomHeader from '../RoomHeader'; @@ -13,7 +12,7 @@ type VoipRoomHeaderProps = { room: IVoipRoom; } & Omit; -const VoipRoomHeader = ({ slots: parentSlot, room }: VoipRoomHeaderProps) => { +const VoipRoomHeader = ({ room }: VoipRoomHeaderProps) => { const router = useRouter(); const currentRouteName = useSyncExternalStore( @@ -21,19 +20,13 @@ const VoipRoomHeader = ({ slots: parentSlot, room }: VoipRoomHeaderProps) => { useCallback(() => router.getRouteName(), [router]), ); - const { isMobile } = useLayout(); - const slots = useMemo( () => ({ - ...parentSlot, - start: (!!isMobile || currentRouteName === 'omnichannel-directory') && ( - - {isMobile && } - {currentRouteName === 'omnichannel-directory' && } - + start: currentRouteName === 'omnichannel-directory' && ( + {currentRouteName === 'omnichannel-directory' && } ), }), - [isMobile, currentRouteName, parentSlot], + [currentRouteName], ); return ; }; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoom.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoom.tsx deleted file mode 100644 index 5a3df51894de8..0000000000000 --- a/apps/meteor/client/views/room/HeaderV2/ParentRoom.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; - -import { HeaderTag, HeaderTagIcon } from '../../../components/Header'; -import { useRoomIcon } from '../../../hooks/useRoomIcon'; -import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; - -type ParentRoomProps = { - room: Pick; -}; - -const ParentRoom = ({ room }: ParentRoomProps) => { - const icon = useRoomIcon(room); - - const handleRedirect = (): void => roomCoordinator.openRouteLink(room.t, { rid: room._id, ...room }); - - return ( - (e.code === 'Space' || e.code === 'Enter') && handleRedirect()} - onClick={handleRedirect} - > - - {roomCoordinator.getRoomName(room.t, room)} - - ); -}; - -export default ParentRoom; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussion.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussion.tsx new file mode 100644 index 0000000000000..09375e137d1de --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussion.tsx @@ -0,0 +1,20 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { useTranslation } from 'react-i18next'; + +import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator'; +import ParentRoomButton from '../ParentRoomButton'; + +type ParentDiscussionProps = { + loading?: boolean; + room: Pick; +}; + +const ParentDiscussion = ({ loading = false, room }: ParentDiscussionProps) => { + const { t } = useTranslation(); + const roomName = roomCoordinator.getRoomName(room.t, room); + const handleRedirect = (): void => roomCoordinator.openRouteLink(room.t, { rid: room._id, ...room }); + + return ; +}; + +export default ParentDiscussion; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussionRoute.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussionRoute.tsx new file mode 100644 index 0000000000000..5b407de6ba0c1 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussionRoute.tsx @@ -0,0 +1,27 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { useUserSubscription } from '@rocket.chat/ui-contexts'; + +import ParentDiscussion from './ParentDiscussion'; +import ParentDiscussionWithData from './ParentDiscussionWithData'; + +type ParentDiscussionRouteProps = { + room: Pick; +}; + +const ParentDiscussionRoute = ({ room }: ParentDiscussionRouteProps) => { + const { prid } = room; + + if (!prid) { + throw new Error('Parent room ID is missing'); + } + + const subscription = useUserSubscription(prid); + + if (subscription) { + return ; + } + + return ; +}; + +export default ParentDiscussionRoute; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussionWithData.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussionWithData.tsx new file mode 100644 index 0000000000000..d7b5f87944db5 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussionWithData.tsx @@ -0,0 +1,16 @@ +import type { IRoom } from '@rocket.chat/core-typings'; + +import ParentDiscussion from './ParentDiscussion'; +import { useRoomInfoEndpoint } from '../../../../../hooks/useRoomInfoEndpoint'; + +const ParentDiscussionWithData = ({ rid }: { rid: IRoom['_id'] }) => { + const { data, isPending, isError } = useRoomInfoEndpoint(rid); + + if (isError || !data?.room) { + return null; + } + + return ; +}; + +export default ParentDiscussionWithData; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/index.ts b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/index.ts new file mode 100644 index 0000000000000..6d70f6998d612 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/index.ts @@ -0,0 +1 @@ +export { default } from './ParentDiscussionRoute'; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentRoom.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentRoom.tsx new file mode 100644 index 0000000000000..38c6cc5f86597 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentRoom.tsx @@ -0,0 +1,22 @@ +import type { IRoom } from '@rocket.chat/core-typings'; + +import ParentDiscussion from './ParentDiscussion'; +import ParentTeam from './ParentTeam'; + +const ParentRoom = ({ room }: { room: IRoom }) => { + const parentRoomId = Boolean(room.prid || (room.teamId && !room.teamMain)); + + if (!parentRoomId) { + return null; + } + + if (room.prid) { + return ; + } + + if (room.teamId && !room.teamMain) { + return ; + } +}; + +export default ParentRoom; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentRoomButton.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentRoomButton.tsx new file mode 100644 index 0000000000000..05ce1a34b38c8 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentRoomButton.tsx @@ -0,0 +1,14 @@ +import { IconButton, Skeleton } from '@rocket.chat/fuselage'; +import type { ComponentProps } from 'react'; + +type ParentRoomButtonProps = Omit, 'icon'> & { loading: boolean }; + +const ParentRoomButton = ({ loading, ...props }: ParentRoomButtonProps) => { + if (loading) { + return ; + } + + return ; +}; + +export default ParentRoomButton; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentTeam.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentTeam.tsx similarity index 66% rename from apps/meteor/client/views/room/HeaderV2/ParentTeam.tsx rename to apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentTeam.tsx index 475054c07a9b2..9c9460f0a4820 100644 --- a/apps/meteor/client/views/room/HeaderV2/ParentTeam.tsx +++ b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentTeam.tsx @@ -2,9 +2,10 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { TEAM_TYPE } from '@rocket.chat/core-typings'; import { useUserId, useEndpoint } from '@rocket.chat/ui-contexts'; import { keepPreviousData, useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; -import { HeaderTag, HeaderTagIcon, HeaderTagSkeleton } from '../../../components/Header'; -import { goToRoomById } from '../../../lib/utils/goToRoomById'; +import ParentRoomButton from './ParentRoomButton'; +import { goToRoomById } from '../../../../lib/utils/goToRoomById'; type APIErrorResult = { success: boolean; error: string }; @@ -13,7 +14,9 @@ type ParentTeamProps = { }; const ParentTeam = ({ room }: ParentTeamProps) => { + const { t } = useTranslation(); const { teamId } = room; + const userId = useUserId(); if (!teamId) { @@ -43,8 +46,9 @@ const ParentTeam = ({ room }: ParentTeamProps) => { queryFn: async () => userTeamsListEndpoint({ userId }), }); - const userBelongsToTeam = userTeams?.teams?.find((team) => team._id === teamId) || false; - const isTeamPublic = teamInfoData?.teamInfo.type === TEAM_TYPE.PUBLIC; + const userBelongsToTeam = Boolean(userTeams?.teams?.find((team) => team._id === teamId)) || false; + const isPublicTeam = teamInfoData?.teamInfo.type === TEAM_TYPE.PUBLIC; + const shouldDisplayTeam = isPublicTeam || userBelongsToTeam; const redirectToMainRoom = (): void => { const rid = teamInfoData?.teamInfo.roomId; @@ -52,31 +56,19 @@ const ParentTeam = ({ room }: ParentTeamProps) => { return; } - if (!(isTeamPublic || userBelongsToTeam)) { - return; - } - goToRoomById(rid); }; - if (teamInfoLoading || userTeamsLoading) { - return ; - } - - if (teamInfoError) { + if (teamInfoError || !shouldDisplayTeam) { return null; } return ( - (e.code === 'Space' || e.code === 'Enter') && redirectToMainRoom()} + - - {teamInfoData?.teamInfo.name} - + title={t('Back_to__roomName__team', { roomName: teamInfoData?.teamInfo.name })} + /> ); }; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoom/index.ts b/apps/meteor/client/views/room/HeaderV2/ParentRoom/index.ts new file mode 100644 index 0000000000000..31d381a035f27 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/ParentRoom/index.ts @@ -0,0 +1 @@ +export { default } from './ParentRoom'; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoomWithData.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoomWithData.tsx deleted file mode 100644 index 903994533b50a..0000000000000 --- a/apps/meteor/client/views/room/HeaderV2/ParentRoomWithData.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { useUserSubscription } from '@rocket.chat/ui-contexts'; - -import ParentRoom from './ParentRoom'; -import ParentRoomWithEndpointData from './ParentRoomWithEndpointData'; - -type ParentRoomWithDataProps = { - room: IRoom; -}; - -const ParentRoomWithData = ({ room }: ParentRoomWithDataProps) => { - const { prid } = room; - - if (!prid) { - throw new Error('Parent room ID is missing'); - } - - const subscription = useUserSubscription(prid); - - if (subscription) { - return ; - } - - return ; -}; - -export default ParentRoomWithData; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoomWithEndpointData.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoomWithEndpointData.tsx deleted file mode 100644 index 0f8cb89bc4b78..0000000000000 --- a/apps/meteor/client/views/room/HeaderV2/ParentRoomWithEndpointData.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; - -import ParentRoom from './ParentRoom'; -import { HeaderTagSkeleton } from '../../../components/Header'; -import { useRoomInfoEndpoint } from '../../../hooks/useRoomInfoEndpoint'; - -type ParentRoomWithEndpointDataProps = { - rid: IRoom['_id']; -}; - -const ParentRoomWithEndpointData = ({ rid }: ParentRoomWithEndpointDataProps) => { - const { data, isPending, isError } = useRoomInfoEndpoint(rid); - - if (isPending) { - return ; - } - - if (isError || !data?.room) { - return null; - } - - return ; -}; - -export default ParentRoomWithEndpointData; diff --git a/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx b/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx index 1683873a7c5cf..3fd1677675216 100644 --- a/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx +++ b/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx @@ -1,23 +1,22 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; -import { RoomAvatar } from '@rocket.chat/ui-avatar'; import type { ReactNode } from 'react'; import { Suspense } from 'react'; import { useTranslation } from 'react-i18next'; import FederatedRoomOriginServer from './FederatedRoomOriginServer'; -import ParentRoomWithData from './ParentRoomWithData'; -import ParentTeam from './ParentTeam'; +import ParentRoom from './ParentRoom'; import RoomTitle from './RoomTitle'; import RoomToolbox from './RoomToolbox'; +import RoomTopic from './RoomTopic'; import Encrypted from './icons/Encrypted'; import Favorite from './icons/Favorite'; import Translate from './icons/Translate'; -import { Header, HeaderAvatar, HeaderContent, HeaderContentRow, HeaderToolbar } from '../../../components/Header'; +import { Header, HeaderContent, HeaderContentRow, HeaderToolbar } from '../../../components/Header'; export type RoomHeaderProps = { room: IRoom; - slots: { + slots?: { start?: ReactNode; preContent?: ReactNode; insideContent?: ReactNode; @@ -38,19 +37,16 @@ const RoomHeader = ({ room, slots = {}, roomToolbox }: RoomHeaderProps) => { return (
{slots?.start} - - - + {slots?.preContent} - {room.prid && } - {room.teamId && !room.teamMain && } {isRoomFederated(room) && } + {slots?.insideContent} diff --git a/apps/meteor/client/views/room/HeaderV2/RoomHeaderE2EESetup.tsx b/apps/meteor/client/views/room/HeaderV2/RoomHeaderE2EESetup.tsx index f1b959cd0ce94..be1ac31e733e9 100644 --- a/apps/meteor/client/views/room/HeaderV2/RoomHeaderE2EESetup.tsx +++ b/apps/meteor/client/views/room/HeaderV2/RoomHeaderE2EESetup.tsx @@ -9,15 +9,15 @@ import { useE2EEState } from '../hooks/useE2EEState'; const RoomToolboxE2EESetup = lazy(() => import('./RoomToolbox/RoomToolboxE2EESetup')); -const RoomHeaderE2EESetup = ({ room, slots = {} }: RoomHeaderProps) => { +const RoomHeaderE2EESetup = ({ room }: RoomHeaderProps) => { const e2eeState = useE2EEState(); const e2eRoomState = useE2EERoomState(room._id); if (e2eeState === E2EEState.SAVE_PASSWORD || e2eeState === E2EEState.ENTER_PASSWORD || e2eRoomState === E2ERoomState.WAITING_KEYS) { - return } />; + return } />; } - return ; + return ; }; export default RoomHeaderE2EESetup; diff --git a/apps/meteor/client/views/room/HeaderV2/RoomTitle.tsx b/apps/meteor/client/views/room/HeaderV2/RoomTitle.tsx index 6e45ce2fa94f4..77b9ecafbd3e8 100644 --- a/apps/meteor/client/views/room/HeaderV2/RoomTitle.tsx +++ b/apps/meteor/client/views/room/HeaderV2/RoomTitle.tsx @@ -43,9 +43,10 @@ const RoomTitle = ({ room }: RoomTitleProps) => { onClick={() => handleOpenRoomInfo()} tabIndex={0} role='button' + mie={4} > - {room.name} + {room.name} ); }; diff --git a/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolboxE2EESetup.tsx b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolboxE2EESetup.tsx index 4fb37de2e3365..75cc0aca7c800 100644 --- a/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolboxE2EESetup.tsx +++ b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolboxE2EESetup.tsx @@ -1,4 +1,6 @@ +import type { Box } from '@rocket.chat/fuselage'; import { useStableArray } from '@rocket.chat/fuselage-hooks'; +import type { ComponentProps } from 'react'; import { useTranslation } from 'react-i18next'; import { HeaderToolbarAction } from '../../../../components/Header'; @@ -6,7 +8,11 @@ import { roomActionHooksForE2EESetup } from '../../../../ui'; import type { RoomToolboxActionConfig } from '../../contexts/RoomToolboxContext'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; -const RoomToolboxE2EESetup = () => { +type RoomToolboxE2EESetupProps = { + className?: ComponentProps['className']; +}; + +const RoomToolboxE2EESetup = ({ className }: RoomToolboxE2EESetupProps) => { const { t } = useTranslation(); const toolbox = useRoomToolbox(); @@ -23,6 +29,7 @@ const RoomToolboxE2EESetup = () => { {actions.map(({ id, icon, title, action, disabled, tooltip }, index) => ( { + it('should render Add Topic when no topic is present and user can edit room', () => { + const room = createFakeRoom({ topic: '', t: 'c', u: { _id: user._id, username: user.username, name: user.name } }); + const subscription = createFakeSubscription({ t: 'c', rid: room._id, u: room.u, roles: ['owner'] }); + + render( + + + , + { + wrapper: mockAppRoot() + .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) + .withPermission('edit-room') + .withUser(user) + .build(), + }, + ); + + expect(screen.getByText('Add_topic')).toBeInTheDocument(); + }); + + it('should not render Add Topic when no topic is present and user cannot edit room', () => { + const room = createFakeRoom({ topic: '', t: 'c' }); + const subscription = createFakeSubscription({ t: 'c', rid: room._id }); + + render( + + + , + { + wrapper: mockAppRoot() + .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) + .withUser(user) + .build(), + }, + ); + + expect(screen.queryByText('Add_topic')).not.toBeInTheDocument(); + }); + + it('should not render Add Topic when no statusText is present, user can edit room and room is a direct message room', () => { + const room = createFakeRoom({ topic: '', t: 'd', uids: [user._id, user3._id] }); + const subscription = createFakeSubscription({ t: 'd', rid: room._id }); + + render( + + + , + { + wrapper: mockAppRoot() + .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) + .withUser(user) + .withUsers([user3]) + .withPermission('edit-room') + .build(), + }, + ); + + expect(screen.queryByText('Add_topic')).not.toBeInTheDocument(); + }); + + it('should render topic when topic is present for rooms', () => { + const room = createFakeRoom({ topic: 'Sample Topic', t: 'c' }); + const subscription = createFakeSubscription({ t: 'c', rid: room._id }); + + render( + + + , + { + wrapper: mockAppRoot() + .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) + .withUser(user) + .build(), + }, + ); + + expect(screen.getByText('Sample Topic')).toBeInTheDocument(); + }); + + it('should render statusText when statusText is present for direct message user and users length < 3', () => { + const room = createFakeRoom({ topic: '', t: 'd', uids: [user._id, user2._id] }); + const subscription = createFakeSubscription({ t: 'd', rid: room._id }); + + render( + + + , + { + wrapper: mockAppRoot() + .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) + .withPermission('edit-room') + .withUser(user) + .withUsers([user2]) + .build(), + }, + ); + + expect(screen.queryByText('Add_topic')).not.toBeInTheDocument(); + expect(screen.getByText('Sample Status')).toBeInTheDocument(); + }); + + it('should not render statusText when statusText is present for direct message user and users length >= 3', () => { + const room = createFakeRoom({ topic: '', t: 'd', uids: [user._id, user2._id, user3._id] }); + const subscription = createFakeSubscription({ t: 'd', rid: room._id }); + + render( + + + , + { + wrapper: mockAppRoot() + .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) + .withPermission('edit-room') + .withUser(user) + .withUsers([user2, user3]) + .build(), + }, + ); + + expect(screen.queryByText('Add_topic')).not.toBeInTheDocument(); + expect(screen.queryByText('Sample Status')).not.toBeInTheDocument(); + }); + + it('should not render Add Topic for livechat rooms', () => { + const room = createFakeRoom({ topic: '', t: 'l' }); + const subscription = createFakeSubscription({ t: 'l', rid: room._id }); + + render( + + + , + { + wrapper: mockAppRoot() + .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) + .withUser(user) + .withPermission('edit-room') + .build(), + }, + ); + + expect(screen.queryByText('Add_topic')).not.toBeInTheDocument(); + }); + + it('should not render Add Topic for voip rooms', () => { + const room = createFakeRoom({ topic: '', t: 'v' }); + const subscription = createFakeSubscription({ t: 'v', rid: room._id }); + + render( + + + , + { + wrapper: mockAppRoot() + .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) + .withUser(user) + .withPermission('edit-room') + .build(), + }, + ); + + expect(screen.queryByText('Add_topic')).not.toBeInTheDocument(); + }); +}); diff --git a/apps/meteor/client/views/room/body/RoomTopic.tsx b/apps/meteor/client/views/room/HeaderV2/RoomTopic.tsx similarity index 54% rename from apps/meteor/client/views/room/body/RoomTopic.tsx rename to apps/meteor/client/views/room/HeaderV2/RoomTopic.tsx index 79983e2b86504..abe34ab0f3252 100644 --- a/apps/meteor/client/views/room/body/RoomTopic.tsx +++ b/apps/meteor/client/views/room/HeaderV2/RoomTopic.tsx @@ -1,24 +1,21 @@ -import type { IRoom, IUser } from '@rocket.chat/core-typings'; +import type { IRoom } from '@rocket.chat/core-typings'; import { isDirectMessageRoom, isPrivateRoom, isPublicRoom, isTeamRoom } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; -import { RoomBanner, RoomBannerContent } from '@rocket.chat/ui-client'; -import { useUserId, useTranslation, useRouter } from '@rocket.chat/ui-contexts'; +import { useUserId, useTranslation, useRouter, useUserPresence } from '@rocket.chat/ui-contexts'; import MarkdownText from '../../../components/MarkdownText'; -import { usePresence } from '../../../hooks/usePresence'; import { useCanEditRoom } from '../contextualBar/Info/hooks/useCanEditRoom'; type RoomTopicProps = { room: IRoom; - user: IUser | null; }; -export const RoomTopic = ({ room }: RoomTopicProps) => { +const RoomTopic = ({ room }: RoomTopicProps) => { const t = useTranslation(); const canEdit = useCanEditRoom(room); const userId = useUserId(); const directUserId = room.uids?.filter((uid) => uid !== userId).shift(); - const directUserData = usePresence(directUserId); + const directUserData = useUserPresence(directUserId); const router = useRouter(); const currentRoute = router.getLocationPathname(); @@ -31,17 +28,15 @@ export const RoomTopic = ({ room }: RoomTopicProps) => { return null; } - return ( - - - {!topic && canEditTopic ? ( - - {t('Add_topic')} - - ) : ( - - )} - - - ); + if (!topic && canEditTopic) { + return ( + + {t('Add_topic')} + + ); + } + + return ; }; + +export default RoomTopic; diff --git a/apps/meteor/client/views/room/HeaderV2/icons/Encrypted.tsx b/apps/meteor/client/views/room/HeaderV2/icons/Encrypted.tsx index 47a613225ebd1..3e376e3b9d661 100644 --- a/apps/meteor/client/views/room/HeaderV2/icons/Encrypted.tsx +++ b/apps/meteor/client/views/room/HeaderV2/icons/Encrypted.tsx @@ -9,7 +9,7 @@ import { HeaderState } from '../../../../components/Header'; const Encrypted = ({ room }: { room: IRoom }) => { const { t } = useTranslation(); const e2eEnabled = useSetting('E2E_Enable'); - return e2eEnabled && room?.encrypted ? : null; + return e2eEnabled && room?.encrypted ? : null; }; export default memo(Encrypted); diff --git a/apps/meteor/client/views/room/HeaderV2/icons/Favorite.tsx b/apps/meteor/client/views/room/HeaderV2/icons/Favorite.tsx index 3fd30a5d24912..5afbac0355c84 100644 --- a/apps/meteor/client/views/room/HeaderV2/icons/Favorite.tsx +++ b/apps/meteor/client/views/room/HeaderV2/icons/Favorite.tsx @@ -33,8 +33,7 @@ const Favorite = ({ room: { _id, f: favorite = false, t: type, name } }: { room: title={favoriteLabel} icon={favorite ? 'star-filled' : 'star'} onClick={handleFavoriteClick} - color={favorite ? 'status-font-on-warning' : null} - tiny + color={favorite ? 'status-font-on-warning' : undefined} /> ); }; diff --git a/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts b/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts index 2ec888f51d922..6554603fb7fee 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts +++ b/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts @@ -1,50 +1,103 @@ import type { IMessage } from '@rocket.chat/core-typings'; +import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; import { useRouter } from '@rocket.chat/ui-contexts'; -import { useCallback } from 'react'; +import { useCallback, useRef } from 'react'; import { useMessageListJumpToMessageParam, useMessageListRef } from '../../../../components/message/list/MessageListContext'; +import { useSafeRefCallback } from '../../../../hooks/useSafeRefCallback'; +import { setRef } from '../../composer/hooks/useMessageComposerMergedRefs'; import { setHighlightMessage, clearHighlightMessage } from '../providers/messageHighlightSubscription'; -// this is an arbitrary value so that there's a gap between the header and the message; -const SCROLL_EXTRA_OFFSET = 60; +/** + * That is completely messy, CustomScrollbars force us to initialize the scrollbars inside an effect + * all refCallbacks happen before the effect, more than that, the scrollbars also reset the scroll position + * so we need to check if the scrollbars are initialized and if there is any message to be highlighted + */ + +export const useJumpToMessageImperative = () => { + const jumpToRef = useRef(null); + const containerRef = useRef(null); + + const jumpToRefAction = useCallback(() => { + if (!jumpToRef.current || !containerRef.current) { + return; + } + + // calculate the scroll position to center the message + // avoiding scrollIntoView because it will can scroll parent elements + containerRef.current.scrollTop = + jumpToRef.current.offsetTop - containerRef.current.clientHeight / 2 + jumpToRef.current.offsetHeight / 2; + }, []); + + return { + jumpToRef: useMergedRefs(jumpToRef, jumpToRefAction), + innerRef: useMergedRefs(containerRef, jumpToRefAction), + }; +}; + +/** + * `listRef` is a reference to the message node in the message list. + * its shared between other hooks like `useLoadSurroundingMessages`, `useJumpToMessage`, `useGetMore`, `useListIsAtBottom` and `useRestoreScrollPosition` + * since each hook has a different concern, this ref helps each other aware if a message is being highlighted which changes the scroll position + + */ export const useJumpToMessage = (messageId: IMessage['_id']) => { const jumpToMessageParam = useMessageListJumpToMessageParam(); const listRef = useMessageListRef(); const router = useRouter(); - const ref = useCallback( - (node: HTMLElement | null) => { - if (!node || !scroll) { - return; - } - setTimeout(() => { - if (listRef?.current) { - const wrapper = listRef.current; - const containerRect = wrapper.getBoundingClientRect(); - const messageRect = node.getBoundingClientRect(); - - const offset = messageRect.top - containerRect.top; - const scrollPosition = wrapper.scrollTop; - const newScrollPosition = scrollPosition + offset - SCROLL_EXTRA_OFFSET; - - wrapper.scrollTo({ top: newScrollPosition, behavior: 'smooth' }); + const ref = useSafeRefCallback( + useCallback( + (node: HTMLElement | null) => { + if (!node || !scroll) { + return; } - const { msg: _, ...search } = router.getSearchParameters(); - router.navigate( + if (!listRef) { + return; + } + + setRef(listRef, node); + + const handleScroll = () => { + const { msg: _, ...search } = router.getSearchParameters(); + router.navigate( + { + pathname: router.getLocationPathname(), + search, + }, + { replace: true }, + ); + setTimeout(clearHighlightMessage, 2000); + }; + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + handleScroll(); + } + }); + }, { - pathname: router.getLocationPathname(), - search, + threshold: 0.1, }, - { replace: true }, ); + observer.observe(node); + setHighlightMessage(messageId); - setTimeout(clearHighlightMessage, 2000); - }, 500); - }, - [listRef, messageId, router], + + return () => { + observer.disconnect(); + if (listRef) { + setRef(listRef, undefined); + } + }; + }, + [listRef, messageId, router], + ), ); if (jumpToMessageParam !== messageId) { diff --git a/apps/meteor/client/views/room/MessageList/hooks/useLoadSurroundingMessages.ts b/apps/meteor/client/views/room/MessageList/hooks/useLoadSurroundingMessages.ts index 7c6d3c6994734..678420171a1e7 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useLoadSurroundingMessages.ts +++ b/apps/meteor/client/views/room/MessageList/hooks/useLoadSurroundingMessages.ts @@ -1,11 +1,14 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useSearchParameter } from '@rocket.chat/ui-contexts'; import { useQueryClient } from '@tanstack/react-query'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { legacyJumpToMessage } from '../../../../lib/utils/legacyJumpToMessage'; -export const useLoadSurroundingMessages = (msgId?: IMessage['_id']) => { +export const useLoadSurroundingMessages = () => { + const msgId = useSearchParameter('msg'); + const jumpToRef = useRef(undefined); + const queryClient = useQueryClient(); const getMessage = useEndpoint('GET', '/v1/chat.getMessage'); @@ -13,6 +16,11 @@ export const useLoadSurroundingMessages = (msgId?: IMessage['_id']) => { if (!msgId) { return; } + + if (jumpToRef.current) { + return; + } + const abort = new AbortController(); queryClient @@ -36,4 +44,6 @@ export const useLoadSurroundingMessages = (msgId?: IMessage['_id']) => { abort.abort(); }; }, [msgId, queryClient, getMessage]); + + return { jumpToRef }; }; diff --git a/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx b/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx index 682b8b5154ae2..b7f1dd8c3917f 100644 --- a/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx +++ b/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx @@ -1,6 +1,6 @@ import { isThreadMainMessage } from '@rocket.chat/core-typings'; import { useLayout, useUser, useUserPreference, useSetting, useEndpoint, useSearchParameter } from '@rocket.chat/ui-contexts'; -import type { ReactNode, RefObject } from 'react'; +import type { ReactNode, RefCallback } from 'react'; import { useMemo, memo } from 'react'; import { getRegexHighlight, getRegexHighlightUrl } from '../../../../../app/highlight-words/client/helper'; @@ -11,11 +11,10 @@ import { useChat } from '../../contexts/ChatContext'; import { useRoom, useRoomSubscription } from '../../contexts/RoomContext'; import { useAutoTranslate } from '../hooks/useAutoTranslate'; import { useKatex } from '../hooks/useKatex'; -import { useLoadSurroundingMessages } from '../hooks/useLoadSurroundingMessages'; type MessageListProviderProps = { children: ReactNode; - messageListRef?: RefObject; + messageListRef?: RefCallback; attachmentDimension?: { width?: number; height?: number; @@ -52,8 +51,6 @@ const MessageListProvider = ({ children, messageListRef, attachmentDimension }: const hasSubscription = Boolean(subscription); const msgParameter = useSearchParameter('msg'); - useLoadSurroundingMessages(msgParameter); - const chat = useChat(); const context: MessageListContextValue = useMemo( diff --git a/apps/meteor/client/views/room/NotSubscribedRoom.tsx b/apps/meteor/client/views/room/NotSubscribedRoom.tsx index 2cf97519a3018..f2ece26605932 100644 --- a/apps/meteor/client/views/room/NotSubscribedRoom.tsx +++ b/apps/meteor/client/views/room/NotSubscribedRoom.tsx @@ -1,6 +1,6 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { Box, States, StatesAction, StatesActions, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage'; -import { Header, HeaderToolbar } from '@rocket.chat/ui-client'; +import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn, Header, HeaderToolbar } from '@rocket.chat/ui-client'; import { useLayout } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import { Trans, useTranslation } from 'react-i18next'; @@ -26,11 +26,16 @@ const NotSubscribedRoom = ({ rid, reference, type }: NotSubscribedRoomProps): Re - - - -
+ + +
+ + + +
+
+ {null} +
) } body={ diff --git a/apps/meteor/client/views/room/RoomAnnouncement/AnnouncementComponent.tsx b/apps/meteor/client/views/room/RoomAnnouncement/AnnouncementComponent.tsx deleted file mode 100644 index 5f5c9b0519f10..0000000000000 --- a/apps/meteor/client/views/room/RoomAnnouncement/AnnouncementComponent.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { RoomBanner, RoomBannerContent } from '@rocket.chat/ui-client'; -import type { MouseEvent, ReactNode } from 'react'; - -type AnnouncementComponentProps = { - children: ReactNode; - onClickOpen: (e: MouseEvent) => void; -}; - -const AnnouncementComponent = ({ children, onClickOpen }: AnnouncementComponentProps) => ( - - {children} - -); - -export default AnnouncementComponent; diff --git a/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.stories.tsx b/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.stories.tsx index da55c54b06df3..da8818dd919e1 100644 --- a/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.stories.tsx +++ b/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.stories.tsx @@ -1,4 +1,3 @@ -import { action } from '@storybook/addon-actions'; import type { Meta, StoryFn } from '@storybook/react'; import RoomAnnouncement from '.'; @@ -9,8 +8,6 @@ export default { } satisfies Meta; export const Default: StoryFn = (args) => ; -Default.storyName = 'Announcement'; Default.args = { announcement: 'Lorem Ipsum Indolor', - announcementDetails: action('announcementDetails'), }; diff --git a/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.tsx b/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.tsx index a2dcca82cd37d..e32ef2a9c4974 100644 --- a/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.tsx +++ b/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.tsx @@ -1,23 +1,38 @@ import { Box } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { AnnouncementBanner } from '@rocket.chat/ui-client'; import { useSetModal } from '@rocket.chat/ui-contexts'; -import type { MouseEvent } from 'react'; +import type { KeyboardEvent, MouseEvent } from 'react'; import { useTranslation } from 'react-i18next'; -import AnnouncementComponent from './AnnouncementComponent'; import GenericModal from '../../../components/GenericModal'; import MarkdownText from '../../../components/MarkdownText'; type RoomAnnouncementParams = { announcement: string; - announcementDetails?: () => void; }; -const RoomAnnouncement = ({ announcement, announcementDetails }: RoomAnnouncementParams) => { +const RoomAnnouncement = ({ announcement }: RoomAnnouncementParams) => { const { t } = useTranslation(); const setModal = useSetModal(); - const closeModal = useEffectEvent(() => setModal(null)); - const handleClick = (e: MouseEvent): void => { + + const handleOpenAnnouncement = useEffectEvent(() => { + setModal( + setModal(null)} + onClose={() => setModal(null)} + > + + + + , + ); + }); + + const handleClick = (e: MouseEvent) => { if ((e.target as HTMLAnchorElement).href) { return; } @@ -26,21 +41,24 @@ const RoomAnnouncement = ({ announcement, announcementDetails }: RoomAnnouncemen return; } - announcementDetails - ? announcementDetails() - : setModal( - - - - - , - ); + handleOpenAnnouncement(); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.target as HTMLAnchorElement).href) { + return; + } + + if (e.code === 'Enter' || e.code === 'Space') { + e.preventDefault(); + handleOpenAnnouncement(); + } }; return announcement ? ( - + - + ) : null; }; diff --git a/apps/meteor/client/views/room/RoomNotFound.tsx b/apps/meteor/client/views/room/RoomNotFound.tsx index 6c3b849a60b35..ca4945ba96d58 100644 --- a/apps/meteor/client/views/room/RoomNotFound.tsx +++ b/apps/meteor/client/views/room/RoomNotFound.tsx @@ -1,5 +1,5 @@ import { Box } from '@rocket.chat/fuselage'; -import { Header, HeaderToolbar } from '@rocket.chat/ui-client'; +import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn, Header, HeaderToolbar } from '@rocket.chat/ui-client'; import { useLayout } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; @@ -16,11 +16,16 @@ const RoomNotFound = (): ReactElement => { - - - -
+ + +
+ + + +
+
+ {null} +
) } body={ diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index 6e4e3b3db3e80..54aee2f2c1578 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -1,22 +1,26 @@ import { Box } from '@rocket.chat/fuselage'; -import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; import { usePermission, useRole, useSetting, useTranslation, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; import type { MouseEvent, ReactElement } from 'react'; -import { memo, useCallback, useMemo, useRef } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import DropTargetOverlay from './DropTargetOverlay'; import JumpToRecentMessageButton from './JumpToRecentMessageButton'; +import LoadingMessagesIndicator from './LoadingMessagesIndicator'; +import RetentionPolicyWarning from './RetentionPolicyWarning'; +import RoomForeword from './RoomForeword/RoomForeword'; +import UnreadMessagesIndicator from './UnreadMessagesIndicator'; +import { UploadProgressContainer, UploadProgressIndicator } from './UploadProgress'; +import { MessageList } from '../MessageList'; +import { useReadMessageWindowEvents } from './hooks/useReadMessageWindowEvents'; import { isTruthy } from '../../../../lib/isTruthy'; import { CustomScrollbars } from '../../../components/CustomScrollbars'; import { useEmbeddedLayout } from '../../../hooks/useEmbeddedLayout'; -import Announcement from '../Announcement'; +import { useMergedRefsV2 } from '../../../hooks/useMergedRefsV2'; import { BubbleDate } from '../BubbleDate'; -import { MessageList } from '../MessageList'; -import LoadingMessagesIndicator from './LoadingMessagesIndicator'; -import RetentionPolicyWarning from './RetentionPolicyWarning'; -import UnreadMessagesIndicator from './UnreadMessagesIndicator'; import MessageListErrorBoundary from '../MessageList/MessageListErrorBoundary'; +import RoomAnnouncement from '../RoomAnnouncement'; import ComposerContainer from '../composer/ComposerContainer'; +import { useSelectAllAndScrollToTop } from './hooks/useSelectAllAndScrollToTop'; import RoomComposer from '../composer/RoomComposer/RoomComposer'; import { useChat } from '../contexts/ChatContext'; import { useRoom, useRoomSubscription, useRoomMessages } from '../contexts/RoomContext'; @@ -24,18 +28,16 @@ import { useRoomToolbox } from '../contexts/RoomToolboxContext'; import { useDateScroll } from '../hooks/useDateScroll'; import { useMessageListNavigation } from '../hooks/useMessageListNavigation'; import { useRetentionPolicy } from '../hooks/useRetentionPolicy'; -import RoomForeword from './RoomForeword/RoomForeword'; -import { UploadProgressContainer, UploadProgressIndicator } from './UploadProgress'; import { useFileUpload } from './hooks/useFileUpload'; import { useGetMore } from './hooks/useGetMore'; import { useGoToHomeOnRemoved } from './hooks/useGoToHomeOnRemoved'; import { useHasNewMessages } from './hooks/useHasNewMessages'; import { useListIsAtBottom } from './hooks/useListIsAtBottom'; import { useQuoteMessageByUrl } from './hooks/useQuoteMessageByUrl'; -import { useReadMessageWindowEvents } from './hooks/useReadMessageWindowEvents'; import { useRestoreScrollPosition } from './hooks/useRestoreScrollPosition'; -import { useSelectAllAndScrollToTop } from './hooks/useSelectAllAndScrollToTop'; import { useHandleUnread } from './hooks/useUnreadMessages'; +import { useJumpToMessageImperative } from '../MessageList/hooks/useJumpToMessage'; +import { useLoadSurroundingMessages } from '../MessageList/hooks/useLoadSurroundingMessages'; const RoomBody = (): ReactElement => { const chat = useChat(); @@ -81,7 +83,9 @@ const RoomBody = (): ReactElement => { return subscribed; }, [allowAnonymousRead, canPreviewChannelRoom, room, subscribed]); - const innerBoxRef = useRef(null); + const { jumpToRef: jumpToRefGetMoreImperative, innerRef: jumpToRefGetMoreImperativeInnerRef } = useJumpToMessageImperative(); + + const { jumpToRef: surroundingMessagesJumpTpRef } = useLoadSurroundingMessages(); const { wrapperRef: unreadBarWrapperRef, @@ -93,9 +97,26 @@ const RoomBody = (): ReactElement => { const { innerRef: dateScrollInnerRef, bubbleRef, listStyle, ...bubbleDate } = useDateScroll(); - const { innerRef: isAtBottomInnerRef, atBottomRef, sendToBottom, sendToBottomIfNecessary, isAtBottom } = useListIsAtBottom(); - - const { innerRef: getMoreInnerRef } = useGetMore(room._id, atBottomRef); + const { + innerRef: isAtBottomInnerRef, + atBottomRef, + sendToBottom, + sendToBottomIfNecessary, + isAtBottom, + jumpToRef: jumpToRefIsAtBottom, + } = useListIsAtBottom(); + + const { innerRef: getMoreInnerRef, jumpToRef: jumpToRefGetMore } = useGetMore(room._id, atBottomRef); + + const { innerRef: restoreScrollPositionInnerRef, jumpToRef: jumpToRefRestoreScrollPosition } = useRestoreScrollPosition(room._id); + + const jumpToRef = useMergedRefsV2( + jumpToRefGetMore, + jumpToRefIsAtBottom, + jumpToRefRestoreScrollPosition, + surroundingMessagesJumpTpRef, + jumpToRefGetMoreImperative, + ); const { uploads, @@ -104,8 +125,6 @@ const RoomBody = (): ReactElement => { targeDrop: [fileUploadTriggerProps, fileUploadOverlayProps], } = useFileUpload(); - const { innerRef: restoreScrollPositionInnerRef } = useRestoreScrollPosition(); - const { messageListRef } = useMessageListNavigation(); const { innerRef: selectAndScrollRef, selectAllAndScrollToTop } = useSelectAllAndScrollToTop(); @@ -116,9 +135,8 @@ const RoomBody = (): ReactElement => { isAtBottom, }); - const innerRef = useMergedRefs( + const innerRef = useMergedRefsV2( dateScrollInnerRef, - innerBoxRef, restoreScrollPositionInnerRef, isAtBottomInnerRef, newMessagesScrollRef, @@ -126,9 +144,10 @@ const RoomBody = (): ReactElement => { getMoreInnerRef, selectAndScrollRef, messageListRef, + jumpToRefGetMoreImperativeInnerRef, ); - const wrapperBoxRefs = useMergedRefs(unreadBarWrapperRef); + const wrapperBoxRefs = useMergedRefsV2(unreadBarWrapperRef); const handleNavigateToPreviousMessage = useCallback((): void => { chat.messageEditing.toPreviousMessage(); @@ -176,7 +195,7 @@ const RoomBody = (): ReactElement => { return ( <> - {!isLayoutEmbedded && room.announcement && } + {!isLayoutEmbedded && room.announcement && }
{ .join(' ')} > - +
    {canPreview ? ( <> @@ -245,7 +264,7 @@ const RoomBody = (): ReactElement => { )} ) : null} - + {hasMoreNextMessages ? (
  • {isLoadingMoreMessages ? : null}
  • ) : null} diff --git a/apps/meteor/client/views/room/body/RoomBodyV2.tsx b/apps/meteor/client/views/room/body/RoomBodyV2.tsx index 98232baa648d2..b8d5246ecf543 100644 --- a/apps/meteor/client/views/room/body/RoomBodyV2.tsx +++ b/apps/meteor/client/views/room/body/RoomBodyV2.tsx @@ -1,22 +1,23 @@ -import { css } from '@rocket.chat/css-in-js'; import { Box } from '@rocket.chat/fuselage'; -import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; import { usePermission, useRole, useSetting, useTranslation, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; import type { MouseEvent, ReactElement } from 'react'; -import { memo, useCallback, useMemo, useRef } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { isTruthy } from '../../../../lib/isTruthy'; import { CustomScrollbars } from '../../../components/CustomScrollbars'; import { useEmbeddedLayout } from '../../../hooks/useEmbeddedLayout'; +import { useMergedRefsV2 } from '../../../hooks/useMergedRefsV2'; import { BubbleDate } from '../BubbleDate'; import { MessageList } from '../MessageList'; import DropTargetOverlay from './DropTargetOverlay'; import JumpToRecentMessageButton from './JumpToRecentMessageButton'; -import MessageListErrorBoundary from '../MessageList/MessageListErrorBoundary'; -import RoomAnnouncement from '../RoomAnnouncement'; import LoadingMessagesIndicator from './LoadingMessagesIndicator'; import RetentionPolicyWarning from './RetentionPolicyWarning'; +import MessageListErrorBoundary from '../MessageList/MessageListErrorBoundary'; +import RoomAnnouncement from '../RoomAnnouncement'; import ComposerContainer from '../composer/ComposerContainer'; +import { useQuoteMessageByUrl } from './hooks/useQuoteMessageByUrl'; +import { useReadMessageWindowEvents } from './hooks/useReadMessageWindowEvents'; import RoomComposer from '../composer/RoomComposer/RoomComposer'; import { useChat } from '../contexts/ChatContext'; import { useRoom, useRoomSubscription, useRoomMessages } from '../contexts/RoomContext'; @@ -25,20 +26,18 @@ import { useDateScroll } from '../hooks/useDateScroll'; import { useMessageListNavigation } from '../hooks/useMessageListNavigation'; import { useRetentionPolicy } from '../hooks/useRetentionPolicy'; import RoomForeword from './RoomForeword/RoomForeword'; -import { RoomTopic } from './RoomTopic'; import UnreadMessagesIndicator from './UnreadMessagesIndicator'; import { UploadProgressContainer, UploadProgressIndicator } from './UploadProgress'; -import { useBannerSection } from './hooks/useBannerSection'; import { useFileUpload } from './hooks/useFileUpload'; import { useGetMore } from './hooks/useGetMore'; import { useGoToHomeOnRemoved } from './hooks/useGoToHomeOnRemoved'; import { useHasNewMessages } from './hooks/useHasNewMessages'; import { useListIsAtBottom } from './hooks/useListIsAtBottom'; -import { useQuoteMessageByUrl } from './hooks/useQuoteMessageByUrl'; -import { useReadMessageWindowEvents } from './hooks/useReadMessageWindowEvents'; import { useRestoreScrollPosition } from './hooks/useRestoreScrollPosition'; import { useSelectAllAndScrollToTop } from './hooks/useSelectAllAndScrollToTop'; import { useHandleUnread } from './hooks/useUnreadMessages'; +import { useJumpToMessageImperative } from '../MessageList/hooks/useJumpToMessage'; +import { useLoadSurroundingMessages } from '../MessageList/hooks/useLoadSurroundingMessages'; const RoomBody = (): ReactElement => { const chat = useChat(); @@ -84,7 +83,9 @@ const RoomBody = (): ReactElement => { return subscribed; }, [allowAnonymousRead, canPreviewChannelRoom, room, subscribed]); - const innerBoxRef = useRef(null); + const { jumpToRef: jumpToRefGetMoreImperative, innerRef: jumpToRefGetMoreImperativeInnerRef } = useJumpToMessageImperative(); + + const { jumpToRef: surroundingMessagesJumpTpRef } = useLoadSurroundingMessages(); const { wrapperRef, @@ -96,11 +97,26 @@ const RoomBody = (): ReactElement => { const { innerRef: dateScrollInnerRef, bubbleRef, listStyle, ...bubbleDate } = useDateScroll(); - const { innerRef: isAtBottomInnerRef, atBottomRef, sendToBottom, sendToBottomIfNecessary, isAtBottom } = useListIsAtBottom(); - - const { innerRef: getMoreInnerRef } = useGetMore(room._id, atBottomRef); - - const { wrapperRef: sectionWrapperRef, hideSection, innerRef: sectionScrollRef } = useBannerSection(); + const { + innerRef: isAtBottomInnerRef, + atBottomRef, + sendToBottom, + sendToBottomIfNecessary, + isAtBottom, + jumpToRef: jumpToRefIsAtBottom, + } = useListIsAtBottom(); + + const { innerRef: getMoreInnerRef, jumpToRef: jumpToRefGetMore } = useGetMore(room._id, atBottomRef); + + const { innerRef: restoreScrollPositionInnerRef, jumpToRef: jumpToRefRestoreScrollPosition } = useRestoreScrollPosition(room._id); + + const jumpToRef = useMergedRefsV2( + jumpToRefIsAtBottom, + jumpToRefGetMore, + jumpToRefRestoreScrollPosition, + jumpToRefGetMoreImperative, + surroundingMessagesJumpTpRef, + ); const { uploads, @@ -109,8 +125,6 @@ const RoomBody = (): ReactElement => { targeDrop: [fileUploadTriggerProps, fileUploadOverlayProps], } = useFileUpload(); - const { innerRef: restoreScrollPositionInnerRef } = useRestoreScrollPosition(); - const { messageListRef } = useMessageListNavigation(); const { innerRef: selectAndScrollRef, selectAllAndScrollToTop } = useSelectAllAndScrollToTop(); @@ -121,17 +135,16 @@ const RoomBody = (): ReactElement => { isAtBottom, }); - const innerRef = useMergedRefs( + const innerRef = useMergedRefsV2( dateScrollInnerRef, - innerBoxRef, restoreScrollPositionInnerRef, isAtBottomInnerRef, newMessagesScrollRef, - sectionScrollRef, unreadBarInnerRef, getMoreInnerRef, selectAndScrollRef, messageListRef, + jumpToRefGetMoreImperativeInnerRef, ); const handleNavigateToPreviousMessage = useCallback((): void => { @@ -178,26 +191,9 @@ const RoomBody = (): ReactElement => { useReadMessageWindowEvents(); useQuoteMessageByUrl(); - const wrapperStyle = css` - position: absolute; - width: 100%; - z-index: 5; - top: 0px; - - &.animated-hidden { - top: -88px; - } - `; - return ( <> - - - - {!isLayoutEmbedded && room.announcement && } - - - + {!isLayoutEmbedded && room.announcement && }
    { .join(' ')} > - +
      {canPreview ? ( <> @@ -271,7 +267,7 @@ const RoomBody = (): ReactElement => { )} ) : null} - + {hasMoreNextMessages ? (
    • {isLoadingMoreMessages ? : null}
    • ) : null} diff --git a/apps/meteor/client/views/room/body/hooks/useBannerSection.ts b/apps/meteor/client/views/room/body/hooks/useBannerSection.ts deleted file mode 100644 index bade71b7fae5a..0000000000000 --- a/apps/meteor/client/views/room/body/hooks/useBannerSection.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useCallback, useRef, useState } from 'react'; - -import { isAtBottom } from '../../../../../app/ui/client/views/app/lib/scrolling'; -import { withThrottling } from '../../../../../lib/utils/highOrderFunctions'; - -export const useBannerSection = () => { - const [hideSection, setHideSection] = useState(false); - - const wrapperBoxRef = useRef(null); - - const innerScrollRef = useCallback((node: HTMLElement | null) => { - if (!node) { - return; - } - let lastScrollTopRef = 0; - - wrapperBoxRef.current?.addEventListener('mouseover', () => setHideSection(false)); - - node.addEventListener( - 'scroll', - withThrottling({ wait: 100 })((event) => { - const bannerSection = wrapperBoxRef.current?.querySelector('.rcx-header-section'); - - if (bannerSection) { - if (isAtBottom(node, 0)) { - setHideSection(false); - } else if (event.target.scrollTop < lastScrollTopRef) { - setHideSection(true); - } else if (!isAtBottom(node, 100) && event.target.scrollTop > parseFloat(getComputedStyle(bannerSection).height)) { - setHideSection(true); - } - } - lastScrollTopRef = event.target.scrollTop; - }), - { passive: true }, - ); - }, []); - - return { - wrapperRef: wrapperBoxRef, - hideSection, - innerRef: innerScrollRef, - }; -}; diff --git a/apps/meteor/client/views/room/body/hooks/useGetMore.spec.ts b/apps/meteor/client/views/room/body/hooks/useGetMore.spec.ts deleted file mode 100644 index ca289f7f5772b..0000000000000 --- a/apps/meteor/client/views/room/body/hooks/useGetMore.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import React from 'react'; - -import { useGetMore } from './useGetMore'; -import { RoomHistoryManager } from '../../../../../app/ui-utils/client'; - -jest.mock('../../../../../app/ui-utils/client', () => ({ - RoomHistoryManager: { - isLoading: jest.fn(), - hasMore: jest.fn(), - hasMoreNext: jest.fn(), - getMore: jest.fn(), - getMoreNext: jest.fn(), - }, -})); - -const mockGetMore = jest.fn(); - -describe('useGetMore', () => { - it('should call getMore when scrolling near top and hasMore is true', () => { - (RoomHistoryManager.isLoading as jest.Mock).mockReturnValue(false); - (RoomHistoryManager.hasMore as jest.Mock).mockReturnValue(true); - (RoomHistoryManager.hasMoreNext as jest.Mock).mockReturnValue(false); - (RoomHistoryManager.getMore as jest.Mock).mockImplementation(mockGetMore); - const atBottomRef = { current: false }; - - const mockElement = { - addEventListener: jest.fn((event, handler) => { - if (event === 'scroll') { - handler({ - target: { - scrollTop: 10, - clientHeight: 300, - }, - }); - } - }), - removeEventListener: jest.fn(), - }; - - const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: mockElement }); - - const { unmount } = renderHook(() => useGetMore('room-id', atBottomRef)); - - expect(useRefSpy).toHaveBeenCalledWith(null); - expect(RoomHistoryManager.getMore).toHaveBeenCalledWith('room-id'); - - unmount(); - expect(mockElement.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function)); - }); - - it('should call getMoreNext when scrolling near bottom and hasMoreNext is true', () => { - (RoomHistoryManager.isLoading as jest.Mock).mockReturnValue(false); - (RoomHistoryManager.hasMore as jest.Mock).mockReturnValue(false); - (RoomHistoryManager.hasMoreNext as jest.Mock).mockReturnValue(true); - (RoomHistoryManager.getMoreNext as jest.Mock).mockImplementation(mockGetMore); - - const atBottomRef = { current: false }; - const mockElement = { - addEventListener: jest.fn((event, handler) => { - if (event === 'scroll') { - handler({ - target: { - scrollTop: 600, - clientHeight: 300, - scrollHeight: 800, - }, - }); - } - }), - removeEventListener: jest.fn(), - }; - const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: mockElement }); - - renderHook(() => useGetMore('room-id', atBottomRef)); - - expect(useRefSpy).toHaveBeenCalledWith(null); - expect(RoomHistoryManager.getMoreNext).toHaveBeenCalledWith('room-id', atBottomRef); - }); -}); diff --git a/apps/meteor/client/views/room/body/hooks/useGetMore.spec.tsx b/apps/meteor/client/views/room/body/hooks/useGetMore.spec.tsx new file mode 100644 index 0000000000000..5be7ae0494293 --- /dev/null +++ b/apps/meteor/client/views/room/body/hooks/useGetMore.spec.tsx @@ -0,0 +1,96 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { render, screen, waitFor } from '@testing-library/react'; +import React from 'react'; + +import { useGetMore } from './useGetMore'; +import { getBoundingClientRect } from '../../../../../app/ui/client/views/app/lib/scrolling'; +import { RoomHistoryManager } from '../../../../../app/ui-utils/client'; + +jest.mock('../../../../../app/ui-utils/client', () => ({ + RoomHistoryManager: { + isLoading: jest.fn(), + isLoadingNext: jest.fn(), + hasMore: jest.fn(), + hasMoreNext: jest.fn(), + getMore: jest.fn(), + getMoreNext: jest.fn(), + restoreScroll: jest.fn(), + }, +})); + +jest.mock('../../../../../app/ui/client/views/app/lib/scrolling', () => ({ + getBoundingClientRect: jest.fn(), +})); + +const mockGetMore = jest.fn(); + +describe('useGetMore', () => { + it('should call getMore when scrolling near top and hasMore is true', async () => { + const root = mockAppRoot(); + + const Test = () => { + const atBottomRef = React.useRef(false); + const { innerRef } = useGetMore('room-id', atBottomRef); + return ( +
      +
      +
      + ); + }; + (RoomHistoryManager.isLoading as jest.Mock).mockReturnValue(false); + (RoomHistoryManager.hasMore as jest.Mock).mockReturnValue(true); + (RoomHistoryManager.hasMoreNext as jest.Mock).mockReturnValue(false); + (RoomHistoryManager.getMore as jest.Mock).mockImplementation(mockGetMore); + + (getBoundingClientRect as jest.Mock).mockReturnValue({ + scrollTop: 10, + clientHeight: 100, + scrollHeight: 800, + }); + + render(, { + wrapper: root.build(), + }); + + const scrollableElement = screen.getByTestId('scrollable-element'); + scrollableElement.scrollTop = 10; + scrollableElement.dispatchEvent(new Event('scroll')); + + expect(screen.getByTestId('scrollable-element')).toBeInTheDocument(); + + await waitFor(() => { + expect(RoomHistoryManager.getMore).toHaveBeenCalledWith('room-id'); + }); + }); + + it('should call getMoreNext when scrolling near bottom and hasMoreNext is true', () => { + const root = mockAppRoot(); + (RoomHistoryManager.isLoading as jest.Mock).mockReturnValue(false); + (RoomHistoryManager.hasMore as jest.Mock).mockReturnValue(false); + (RoomHistoryManager.hasMoreNext as jest.Mock).mockReturnValue(true); + (RoomHistoryManager.getMoreNext as jest.Mock).mockImplementation(mockGetMore); + + const Test = () => { + const atBottomRef = React.useRef(false); + const { innerRef } = useGetMore('room-id', atBottomRef); + return ( +
      +
      +
      + ); + }; + (getBoundingClientRect as jest.Mock).mockReturnValue({ + scrollTop: 700, + clientHeight: 100, + scrollHeight: 800, + }); + render(, { + wrapper: root.build(), + }); + const scrollableElement = screen.getByTestId('scrollable-element'); + scrollableElement.scrollTop = 700; + scrollableElement.dispatchEvent(new Event('scroll')); + expect(screen.getByTestId('scrollable-element')).toBeInTheDocument(); + expect(RoomHistoryManager.getMoreNext).toHaveBeenCalledWith('room-id', expect.anything()); + }); +}); diff --git a/apps/meteor/client/views/room/body/hooks/useGetMore.ts b/apps/meteor/client/views/room/body/hooks/useGetMore.ts index 32a6b1fb78e72..3d54ac514026a 100644 --- a/apps/meteor/client/views/room/body/hooks/useGetMore.ts +++ b/apps/meteor/client/views/room/body/hooks/useGetMore.ts @@ -1,44 +1,103 @@ +import { useSearchParameter } from '@rocket.chat/ui-contexts'; import type { MutableRefObject } from 'react'; -import { useEffect, useRef } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; +import { flushSync } from 'react-dom'; +import { getBoundingClientRect } from '../../../../../app/ui/client/views/app/lib/scrolling'; import { RoomHistoryManager } from '../../../../../app/ui-utils/client'; import { withThrottling } from '../../../../../lib/utils/highOrderFunctions'; +import { useSafeRefCallback } from '../../../../hooks/useSafeRefCallback'; export const useGetMore = (rid: string, atBottomRef: MutableRefObject) => { - const ref = useRef(null); + const msgId = useSearchParameter('msg'); + const msgIdRef = useRef(msgId); + const jumpToRef = useRef(undefined); useEffect(() => { - if (!ref.current) { - return; - } - - const refValue = ref.current; - - const handleScroll = withThrottling({ wait: 100 })((event) => { - const lastScrollTopRef = event.target.scrollTop; - const height = event.target.clientHeight; - const isLoading = RoomHistoryManager.isLoading(rid); - const hasMore = RoomHistoryManager.hasMore(rid); - const hasMoreNext = RoomHistoryManager.hasMoreNext(rid); - - if ((isLoading === false && hasMore === true) || hasMoreNext === true) { - if (hasMore === true && lastScrollTopRef <= height / 3) { - RoomHistoryManager.getMore(rid); - } else if (hasMoreNext === true && Math.ceil(lastScrollTopRef) >= event.target.scrollHeight - height) { - RoomHistoryManager.getMoreNext(rid, atBottomRef); - atBottomRef.current = false; + msgIdRef.current = msgId; + }, [msgId]); + + const ref = useSafeRefCallback( + useCallback( + (element: HTMLElement | null) => { + if (!element) { + return; } - } - }); + const checkPositionAndGetMore = withThrottling({ wait: 100 })(async () => { + if (!element.isConnected) { + return; + } + + const { scrollTop, clientHeight, scrollHeight } = getBoundingClientRect(element); + + if (msgIdRef.current && !RoomHistoryManager.isLoaded(rid)) { + return; + } + + const lastScrollTopRef = scrollTop; + const height = clientHeight; + const isLoading = RoomHistoryManager.isLoading(rid); + const hasMore = RoomHistoryManager.hasMore(rid); + const hasMoreNext = RoomHistoryManager.hasMoreNext(rid); + + if (jumpToRef.current) { + return; + } + + if (isLoading) { + return; + } + + if (hasMore === true && lastScrollTopRef <= height / 3) { + await RoomHistoryManager.getMore(rid); + + if (jumpToRef.current) { + return; + } + flushSync(() => { + RoomHistoryManager.restoreScroll(rid); + }); + } else if (hasMoreNext === true && Math.ceil(lastScrollTopRef) >= scrollHeight - height) { + await RoomHistoryManager.getMoreNext(rid, atBottomRef); + atBottomRef.current = false; + } + }); + + const mutationObserver = new MutationObserver((mutations) => { + mutations.forEach(() => { + checkPositionAndGetMore(); + }); + }); + + mutationObserver.observe(element, { childList: true, subtree: true }); + + const observer = new ResizeObserver(() => { + checkPositionAndGetMore(); + }); + + observer.observe(element); + + const handleScroll = function () { + checkPositionAndGetMore(); + }; - refValue.addEventListener('scroll', handleScroll); + element.addEventListener('scroll', handleScroll, { + passive: true, + }); - return () => { - refValue.removeEventListener('scroll', handleScroll); - }; - }, [rid, atBottomRef]); + return () => { + observer.disconnect(); + mutationObserver.disconnect(); + checkPositionAndGetMore.cancel(); + element.removeEventListener('scroll', handleScroll); + }; + }, + [rid, atBottomRef], + ), + ); return { innerRef: ref, + jumpToRef, }; }; diff --git a/apps/meteor/client/views/room/body/hooks/useListIsAtBottom.ts b/apps/meteor/client/views/room/body/hooks/useListIsAtBottom.ts index fc67c50d9fd83..7425a43e53c42 100644 --- a/apps/meteor/client/views/room/body/hooks/useListIsAtBottom.ts +++ b/apps/meteor/client/views/room/body/hooks/useListIsAtBottom.ts @@ -9,6 +9,8 @@ import { useSafeRefCallback } from '../../../../hooks/useSafeRefCallback'; export const useListIsAtBottom = () => { const atBottomRef = useRef(true); + const jumpToRef = useRef(undefined); + const innerBoxRef = useRef(null); const sendToBottom = useCallback(() => { @@ -16,6 +18,9 @@ export const useListIsAtBottom = () => { }, []); const sendToBottomIfNecessary = useCallback(() => { + if (jumpToRef.current) { + atBottomRef.current = false; + } if (atBottomRef.current === true) { sendToBottom(); } @@ -42,6 +47,9 @@ export const useListIsAtBottom = () => { } const observer = new ResizeObserver(() => { + if (jumpToRef.current) { + atBottomRef.current = false; + } if (atBottomRef.current === true) { node.scrollTo({ left: 30, top: node.scrollHeight }); } @@ -72,5 +80,6 @@ export const useListIsAtBottom = () => { sendToBottom, sendToBottomIfNecessary, isAtBottom, + jumpToRef, }; }; diff --git a/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.spec.ts b/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.spec.ts deleted file mode 100644 index 5444ac71e9818..0000000000000 --- a/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.spec.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import React from 'react'; - -import { useRestoreScrollPosition } from './useRestoreScrollPosition'; -import { RoomManager } from '../../../../lib/RoomManager'; - -jest.mock('../../../../lib/RoomManager', () => ({ - RoomManager: { - getStore: jest.fn(), - }, - useOpenedRoom: jest.fn(() => 'room-id'), - useSecondLevelOpenedRoom: jest.fn(() => 'room-id'), -})); - -describe('useRestoreScrollPosition', () => { - it('should restore room scroll position based on store', () => { - (RoomManager.getStore as jest.Mock).mockReturnValue({ scroll: 100, atBottom: false }); - - const mockElement = { - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - }; - - const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: mockElement }); - - const { unmount } = renderHook(() => useRestoreScrollPosition()); - - expect(useRefSpy).toHaveBeenCalledWith(null); - expect(mockElement).toHaveProperty('scrollTop', 100); - expect(mockElement).toHaveProperty('scrollLeft', 30); - - unmount(); - expect(mockElement.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function)); - }); - - it('should not restore scroll position if already at bottom', () => { - (RoomManager.getStore as jest.Mock).mockReturnValue({ scroll: 100, atBottom: true }); - - const mockElement = { - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - scrollHeight: 800, - }; - - const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: mockElement }); - - const { unmount } = renderHook(() => useRestoreScrollPosition()); - - expect(useRefSpy).toHaveBeenCalledWith(null); - expect(mockElement).toHaveProperty('scrollTop', 800); - expect(mockElement).not.toHaveProperty('scrollLeft'); - - unmount(); - expect(mockElement.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function)); - }); - - it('should update store based on scroll position', () => { - const update = jest.fn(); - (RoomManager.getStore as jest.Mock).mockReturnValue({ update }); - - const mockElement = { - addEventListener: jest.fn((event, handler) => { - if (event === 'scroll') { - handler({ - target: { - scrollTop: 500, - }, - }); - } - }), - removeEventListener: jest.fn(), - }; - - const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: mockElement }); - - const { unmount } = renderHook(() => useRestoreScrollPosition()); - - expect(useRefSpy).toHaveBeenCalledWith(null); - expect(update).toHaveBeenCalledWith({ scroll: 500, atBottom: false }); - - unmount(); - expect(mockElement.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function)); - }); -}); diff --git a/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.spec.tsx b/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.spec.tsx new file mode 100644 index 0000000000000..fbf95db89753a --- /dev/null +++ b/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.spec.tsx @@ -0,0 +1,87 @@ +import { render, screen, waitFor } from '@testing-library/react'; + +import { useRestoreScrollPosition } from './useRestoreScrollPosition'; +import { RoomManager } from '../../../../lib/RoomManager'; + +jest.mock('../../../../lib/RoomManager', () => ({ + RoomManager: { + getStore: jest.fn(), + }, + useOpenedRoom: jest.fn(() => 'room-id'), + useSecondLevelOpenedRoom: jest.fn(() => 'room-id'), +})); + +describe('useRestoreScrollPosition', () => { + it('should restore room scroll position based on store', () => { + const store = { + scroll: 123, + atBottom: false, + update: jest.fn(), + }; + (RoomManager.getStore as jest.Mock).mockReturnValue(store); + + const Test = () => { + const { innerRef } = useRestoreScrollPosition('GENERAL'); + return ( +
      +
      +
      + ); + }; + + render(); + + expect(screen.getByTestId('scrollable-element')).toBeInTheDocument(); + expect(screen.getByTestId('scrollable-element')).toHaveStyle({ height: '100px', overflowY: 'scroll' }); + expect(screen.getByTestId('scrollable-element')).toHaveProperty('scrollTop', 123); + }); + + it('should do nothing if no previous scroll position is stored', () => { + const store = { + scroll: undefined, + atBottom: false, + update: jest.fn(), + }; + + (RoomManager.getStore as jest.Mock).mockReturnValue(store); + const Test = () => { + const { innerRef } = useRestoreScrollPosition('GENERAL'); + return ( +
      +
      +
      + ); + }; + + (RoomManager.getStore as jest.Mock).mockReturnValue({ scroll: undefined }); + render(); + expect(screen.getByTestId('scrollable-element')).toBeInTheDocument(); + expect(screen.getByTestId('scrollable-element')).toHaveStyle({ height: '100px', overflowY: 'scroll' }); + expect(screen.getByTestId('scrollable-element')).toHaveProperty('scrollTop', 0); + }); + + it('should update store based on scroll position', async () => { + const store = { + scroll: 1, + atBottom: false, + update: jest.fn(), + }; + (RoomManager.getStore as jest.Mock).mockReturnValue(store); + const Test = () => { + const { innerRef } = useRestoreScrollPosition('GENERAL', 0); + return ( +
      +
      +
      + ); + }; + render(); + const scrollableElement = screen.getByTestId('scrollable-element'); + scrollableElement.scrollTop = 50; + scrollableElement.dispatchEvent(new Event('scroll')); + + await waitFor(() => { + expect(store.update).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.ts b/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.ts index 77c03bbed39c9..325cff721059e 100644 --- a/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.ts +++ b/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.ts @@ -1,51 +1,39 @@ -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback, useRef } from 'react'; import { isAtBottom } from '../../../../../app/ui/client/views/app/lib/scrolling'; import { withThrottling } from '../../../../../lib/utils/highOrderFunctions'; -import { RoomManager, useOpenedRoom, useSecondLevelOpenedRoom } from '../../../../lib/RoomManager'; - -export function useRestoreScrollPosition() { - const ref = useRef(null); - const parentRoomId = useOpenedRoom(); - const roomId = useSecondLevelOpenedRoom() ?? parentRoomId; - - const handleRestoreScroll = useCallback(() => { - if (!ref.current || !roomId) { - return; - } - - const store = RoomManager.getStore(roomId); - - if (store?.scroll && !store.atBottom) { - ref.current.scrollTop = store.scroll; - ref.current.scrollLeft = 30; - } else { - ref.current.scrollTop = ref.current.scrollHeight; - } - }, [roomId]); - - useEffect(() => { - if (!ref.current || !roomId) { - return; - } - - handleRestoreScroll(); - - const refValue = ref.current; - const store = RoomManager.getStore(roomId); - - const handleWrapperScroll = withThrottling({ wait: 100 })((event) => { - store?.update({ scroll: event.target.scrollTop, atBottom: isAtBottom(event.target, 50) }); - }); - - refValue.addEventListener('scroll', handleWrapperScroll, { passive: true }); - - return () => { - refValue.removeEventListener('scroll', handleWrapperScroll); - }; - }, [roomId, handleRestoreScroll]); +import { useSafeRefCallback } from '../../../../hooks/useSafeRefCallback'; +import { RoomManager } from '../../../../lib/RoomManager'; + +export function useRestoreScrollPosition(rid: string, wait = 100) { + const jumpToRef = useRef(undefined); + const ref = useSafeRefCallback( + useCallback( + (node: HTMLElement | null) => { + if (!node) { + return; + } + const store = RoomManager.getStore(rid); + if (!jumpToRef.current && store?.scroll !== undefined && !store.atBottom) { + node.scrollTop = store.scroll; + node.scrollLeft = 30; + } + const handleWrapperScroll = withThrottling({ wait })((event) => { + const store = RoomManager.getStore(rid); + store?.update({ scroll: event.target.scrollTop, atBottom: isAtBottom(event.target, 50) }); + }); + node.addEventListener('scroll', handleWrapperScroll, { passive: true }); + return () => { + handleWrapperScroll.cancel(); + node.removeEventListener('scroll', handleWrapperScroll); + }; + }, + [rid, wait], + ), + ); return { + jumpToRef, innerRef: ref, }; } diff --git a/apps/meteor/client/views/room/body/hooks/useRoomRolesManagement.ts b/apps/meteor/client/views/room/body/hooks/useRoomRolesManagement.ts deleted file mode 100644 index 7b3716e28d4f6..0000000000000 --- a/apps/meteor/client/views/room/body/hooks/useRoomRolesManagement.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { IRoom, IUser } from '@rocket.chat/core-typings'; -import { useMethod, useStream } from '@rocket.chat/ui-contexts'; -import { useEffect } from 'react'; - -import { RoomRoles, Messages } from '../../../../../app/models/client'; - -// const roomRoles = RoomRoles as Mongo.Collection>; - -export const useRoomRolesManagement = (rid: IRoom['_id']): void => { - const getRoomRoles = useMethod('getRoomRoles'); - - useEffect(() => { - getRoomRoles(rid).then((results) => { - Array.from(results).forEach(({ _id, ...data }) => { - const { - rid, - u: { _id: uid }, - } = data; - RoomRoles.upsert({ rid, 'u._id': uid }, { $set: data }); - }); - }); - }, [getRoomRoles, rid]); - - useEffect(() => { - const rolesObserve = RoomRoles.find({ rid }).observe({ - added: (role) => { - if (!role.u?._id) { - return; - } - Messages.update({ rid, 'u._id': role.u._id }, { $addToSet: { roles: role._id } }, { multi: true }); - }, - changed: (role) => { - if (!role.u?._id) { - return; - } - Messages.update({ rid, 'u._id': role.u._id }, { $inc: { rerender: 1 } }, { multi: true }); - }, - removed: (role) => { - if (!role.u?._id) { - return; - } - Messages.update({ rid, 'u._id': role.u._id }, { $pull: { roles: role._id } }, { multi: true }); - }, - }); - - return (): void => { - rolesObserve.stop(); - }; - }, [getRoomRoles, rid]); - - const subscribeToNotifyLoggedIn = useStream('notify-logged'); - - useEffect( - () => - subscribeToNotifyLoggedIn('roles-change', ({ type, ...role }) => { - if (!role.scope) { - return; - } - - if (!role.u?._id) { - return; - } - - switch (type) { - case 'added': - RoomRoles.upsert({ 'rid': role.scope, 'u._id': role.u._id }, { $setOnInsert: { u: role.u }, $addToSet: { roles: role._id } }); - break; - - case 'removed': - RoomRoles.update({ 'rid': role.scope, 'u._id': role.u._id }, { $pull: { roles: role._id } }); - break; - } - }), - [subscribeToNotifyLoggedIn], - ); - - useEffect( - () => - subscribeToNotifyLoggedIn('Users:NameChanged', ({ _id: uid, name }: Partial) => { - RoomRoles.update( - { - 'u._id': uid, - }, - { - $set: { - 'u.name': name, - }, - }, - { - multi: true, - }, - ); - }), - [subscribeToNotifyLoggedIn], - ); -}; diff --git a/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.spec.tsx b/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.spec.tsx new file mode 100644 index 0000000000000..f246478e3b4b8 --- /dev/null +++ b/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.spec.tsx @@ -0,0 +1,75 @@ +import { faker } from '@faker-js/faker/locale/af_ZA'; +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import ComposerOmnichannelCallout from './ComposerOmnichannelCallout'; +import FakeRoomProvider from '../../../../../tests/mocks/client/FakeRoomProvider'; +import { createFakeContact, createFakeRoom } from '../../../../../tests/mocks/data'; + +jest.mock('../../../omnichannel/contactInfo/tabs/ContactInfoChannels/useBlockChannel', () => ({ + useBlockChannel: () => jest.fn(), +})); + +const fakeVisitor = { + _id: faker.string.uuid(), + token: faker.string.uuid(), + username: faker.internet.userName(), +}; + +const fakeRoom = createFakeRoom({ t: 'l', v: fakeVisitor }); +const fakeContact = createFakeContact(); + +it('should be displayed if contact is unknown', async () => { + const getContactMockFn = jest.fn().mockResolvedValue({ contact: fakeContact }); + const wrapper = mockAppRoot().withEndpoint('GET', '/v1/omnichannel/contacts.get', getContactMockFn).withRoom(fakeRoom); + + render( + + + , + { wrapper: wrapper.build() }, + ); + + await waitFor(() => expect(getContactMockFn).toHaveBeenCalled()); + expect(screen.getByText('Unknown_contact_callout_description')).toBeVisible(); + expect(screen.getByRole('button', { name: 'Add_contact' })).toBeVisible(); + expect(screen.getByRole('button', { name: 'Block' })).toBeVisible(); + expect(screen.getByRole('button', { name: 'Dismiss' })).toBeVisible(); +}); + +it('should not be displayed if contact is known', async () => { + const getContactMockFn = jest.fn().mockResolvedValue({ contact: createFakeContact({ unknown: false }) }); + const wrapper = mockAppRoot().withEndpoint('GET', '/v1/omnichannel/contacts.get', getContactMockFn).withRoom(fakeRoom); + + render( + + + , + { wrapper: wrapper.build() }, + ); + + await waitFor(() => expect(getContactMockFn).toHaveBeenCalled()); + expect(screen.queryByText('Unknown_contact_callout_description')).not.toBeInTheDocument(); +}); + +it('should hide callout on dismiss', async () => { + const getContactMockFn = jest.fn().mockResolvedValue({ contact: fakeContact }); + const wrapper = mockAppRoot().withEndpoint('GET', '/v1/omnichannel/contacts.get', getContactMockFn).withRoom(fakeRoom); + + render( + + + , + { wrapper: wrapper.build() }, + ); + + await waitFor(() => expect(getContactMockFn).toHaveBeenCalled()); + expect(screen.getByText('Unknown_contact_callout_description')).toBeVisible(); + + const btnDismiss = screen.getByRole('button', { name: 'Dismiss' }); + await userEvent.click(btnDismiss); + + expect(screen.queryByText('Unknown_contact_callout_description')).not.toBeInTheDocument(); +}); diff --git a/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.tsx b/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.tsx index b1ce6cc244d3d..23a42b0546ad3 100644 --- a/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.tsx +++ b/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.tsx @@ -1,26 +1,18 @@ -import { Box, Button, ButtonGroup, Callout } from '@rocket.chat/fuselage'; -import { useAtLeastOnePermission, useEndpoint, useRouter, useSetting } from '@rocket.chat/ui-contexts'; +import { Button, ButtonGroup, Callout, IconButton } from '@rocket.chat/fuselage'; +import { useSessionStorage } from '@rocket.chat/fuselage-hooks'; +import { useEndpoint, useRouter } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; -import { Trans, useTranslation } from 'react-i18next'; +import { useId } from 'react'; +import { useTranslation } from 'react-i18next'; import { isSameChannel } from '../../../../../app/livechat/lib/isSameChannel'; -import { useHasLicenseModule } from '../../../../hooks/useHasLicenseModule'; import { useBlockChannel } from '../../../omnichannel/contactInfo/tabs/ContactInfoChannels/useBlockChannel'; import { useOmnichannelRoom } from '../../contexts/RoomContext'; const ComposerOmnichannelCallout = () => { const { t } = useTranslation(); const room = useOmnichannelRoom(); - const { navigate, buildRoutePath } = useRouter(); - const hasLicense = useHasLicenseModule('contact-id-verification'); - const securityPrivacyRoute = buildRoutePath('/omnichannel/security-privacy'); - const shouldShowSecurityRoute = useSetting('Livechat_Require_Contact_Verification') !== 'never' || !hasLicense; - - const canViewSecurityPrivacy = useAtLeastOnePermission([ - 'view-privileged-setting', - 'edit-privileged-setting', - 'manage-selected-settings', - ]); + const { navigate } = useRouter(); const { _id, @@ -29,6 +21,9 @@ const ComposerOmnichannelCallout = () => { contactId, } = room; + const calloutDescriptionId = useId(); + const [dismissed, setDismissed] = useSessionStorage(`contact-unknown-callout-${contactId}`, false); + const getContactById = useEndpoint('GET', '/v1/omnichannel/contacts.get'); const { data } = useQuery({ queryKey: ['getContactById', contactId], queryFn: () => getContactById({ contactId }) }); @@ -37,14 +32,15 @@ const ComposerOmnichannelCallout = () => { const handleBlock = useBlockChannel({ blocked: currentChannel?.blocked || false, association }); - if (!data?.contact?.unknown) { + if (dismissed || !data?.contact?.unknown) { return null; } return ( + setDismissed(true)} /> } > - {shouldShowSecurityRoute ? ( - - Add to contact list manually and - - enable verification - - using multi-factor authentication. - - ) : ( - t('Add_to_contact_list_manually') - )} +

      {t('Unknown_contact_callout_description')}

      ); }; diff --git a/apps/meteor/client/views/room/composer/hooks/useMessageComposerMergedRefs.ts b/apps/meteor/client/views/room/composer/hooks/useMessageComposerMergedRefs.ts index 02f129df3b6db..aea0318ce831d 100644 --- a/apps/meteor/client/views/room/composer/hooks/useMessageComposerMergedRefs.ts +++ b/apps/meteor/client/views/room/composer/hooks/useMessageComposerMergedRefs.ts @@ -4,6 +4,17 @@ import { useCallback } from 'react'; const isRefCallback = (x: unknown): x is RefCallback => typeof x === 'function'; const isMutableRefObject = (x: unknown): x is MutableRefObject => typeof x === 'object'; +export const setRef = (ref: Ref | undefined, refValue: T) => { + if (isRefCallback(ref)) { + ref(refValue); + return; + } + + if (isMutableRefObject(ref)) { + ref.current = refValue; + } +}; + /** * Merges multiple refs into a single ref callback. * it was not meant to be used with in any different place than MessageBox @@ -13,16 +24,7 @@ const isMutableRefObject = (x: unknown): x is MutableRefObject => typeof x */ export const useMessageComposerMergedRefs = (...refs: (Ref | undefined)[]): RefCallback => { return useCallback((refValue: T) => { - refs.forEach((ref) => { - if (isRefCallback(ref)) { - ref(refValue); - return; - } - - if (isMutableRefObject(ref)) { - ref.current = refValue; - } - }); + refs.forEach((ref) => setRef(ref, refValue)); // eslint-disable-next-line react-hooks/exhaustive-deps }, refs); }; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts index a7542ba113273..46d72c824b4e1 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts @@ -55,7 +55,7 @@ export const useVideoMessageAction = (disabled: boolean): GenericMenuItemProps = return { id: 'video-message', content: getMediaActionTitle, - icon: 'video', + icon: 'video-message', disabled: !isAllowed || Boolean(disabled), onClick: handleOpenVideoMessage, }; diff --git a/apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomLeave.tsx b/apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomLeave.tsx index ebcb7abaf3e71..2f53b9d95ed37 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomLeave.tsx +++ b/apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomLeave.tsx @@ -37,7 +37,7 @@ export const useRoomLeave = (room: IRoom, joined = true) => { setModal( setModal(null)} cancelText={t('Cancel')} diff --git a/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferencesWithData.tsx b/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferencesWithData.tsx index 07d8732c5dd80..cbe465a60d7ec 100644 --- a/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferencesWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferencesWithData.tsx @@ -20,7 +20,7 @@ const NotificationPreferencesWithData = (): ReactElement => { successMessage: t('Room_updated_successfully'), }); - const customSoundAsset: SelectOption[] | undefined = customSound?.getList()?.map((value) => [value._id, value.name]); + const customSoundAsset: SelectOption[] | undefined = customSound.list?.map((value) => [value._id, value.name]); const defaultOption: SelectOption[] = [ ['default', t('Default')], diff --git a/apps/meteor/client/views/room/contextualBar/NotificationPreferences/components/NotificationByDevice.tsx b/apps/meteor/client/views/room/contextualBar/NotificationPreferences/components/NotificationByDevice.tsx index e88142e9ca008..f91e06acccdec 100644 --- a/apps/meteor/client/views/room/contextualBar/NotificationPreferences/components/NotificationByDevice.tsx +++ b/apps/meteor/client/views/room/contextualBar/NotificationPreferences/components/NotificationByDevice.tsx @@ -19,7 +19,6 @@ const NotificationByDevice = ({ device, icon, children }: NotificationByDevicePr } - data-qa-id={`${device}-notifications`} > {children} diff --git a/apps/meteor/client/views/room/contextualBar/OTR/OTRWithData.tsx b/apps/meteor/client/views/room/contextualBar/OTR/OTRWithData.tsx index 0fbae5ad6bffb..270483ec5e4f0 100644 --- a/apps/meteor/client/views/room/contextualBar/OTR/OTRWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/OTR/OTRWithData.tsx @@ -1,17 +1,17 @@ +import { useUserPresence } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import { useEffect } from 'react'; import OTRComponent from './OTR'; import { OtrRoomState } from '../../../../../app/otr/lib/OtrRoomState'; import { useOTR } from '../../../../hooks/useOTR'; -import { usePresence } from '../../../../hooks/usePresence'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; const OTRWithData = (): ReactElement => { const { otr, otrState } = useOTR(); const { closeTab } = useRoomToolbox(); - const peerUserPresence = usePresence(otr?.getPeerId()); + const peerUserPresence = useUserPresence(otr?.getPeerId()); const userStatus = peerUserPresence?.status; const peerUsername = peerUserPresence?.username; const isOnline = !['offline', 'loading'].includes(userStatus || ''); diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx index 8c1be447b989f..04eac38276040 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx @@ -12,13 +12,13 @@ import { MessageTypes } from '../../../../../../app/ui-utils/client'; import { isTruthy } from '../../../../../../lib/isTruthy'; import { CustomScrollbars } from '../../../../../components/CustomScrollbars'; import { BubbleDate } from '../../../BubbleDate'; +import { useJumpToMessageImperative } from '../../../MessageList/hooks/useJumpToMessage'; import { isMessageNewDay } from '../../../MessageList/lib/isMessageNewDay'; import MessageListProvider from '../../../MessageList/providers/MessageListProvider'; import LoadingMessagesIndicator from '../../../body/LoadingMessagesIndicator'; import { useDateScroll } from '../../../hooks/useDateScroll'; import { useFirstUnreadMessageId } from '../../../hooks/useFirstUnreadMessageId'; import { useMessageListNavigation } from '../../../hooks/useMessageListNavigation'; -import { useLegacyThreadMessageJump } from '../hooks/useLegacyThreadMessageJump'; import { useLegacyThreadMessageListScrolling } from '../hooks/useLegacyThreadMessageListScrolling'; import { useLegacyThreadMessages } from '../hooks/useLegacyThreadMessages'; import './threads.css'; @@ -55,12 +55,12 @@ const ThreadMessageList = ({ mainMessage }: ThreadMessageListProps): ReactElemen const { innerRef, bubbleRef, listStyle, ...bubbleDate } = useDateScroll(); const { messages, loading } = useLegacyThreadMessages(mainMessage._id); - const { - listWrapperRef: listWrapperScrollRef, - listRef: listScrollRef, - onScroll: handleScroll, - } = useLegacyThreadMessageListScrolling(mainMessage); - const { parentRef: listJumpRef } = useLegacyThreadMessageJump({ enabled: !loading }); + + const { innerRef: listScrollRef, jumpToRef } = useLegacyThreadMessageListScrolling(mainMessage); + + const { jumpToRef: jumpToRefGetMoreImperative, innerRef: jumpToRefGetMoreImperativeInnerRef } = useJumpToMessageImperative(); + + const customScrollbarsRef = useMergedRefs(listScrollRef, jumpToRefGetMoreImperativeInnerRef); const hideUsernames = useUserPreference('hideUsernames'); const showUserAvatar = !!useUserPreference('displayAvatars'); @@ -68,18 +68,17 @@ const ThreadMessageList = ({ mainMessage }: ThreadMessageListProps): ReactElemen const messageGroupingPeriod = useSetting('Message_GroupingPeriod', 300); const { messageListRef } = useMessageListNavigation(); - const listRef = useMergedRefs(listScrollRef, messageListRef); - const scrollRef = useMergedRefs(innerRef, listWrapperScrollRef, listJumpRef); + const jumpToRefMessageListProvider = useMergedRefs(jumpToRef, jumpToRefGetMoreImperative); return (
      - + @@ -88,7 +87,7 @@ const ThreadMessageList = ({ mainMessage }: ThreadMessageListProps): ReactElemen ) : ( - + {[mainMessage, ...messages].map((message, index, { [index - 1]: previous }) => { const sequential = isMessageSequential(message, previous, messageGroupingPeriod); const newDay = isMessageNewDay(message, previous); diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessageJump.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessageJump.ts deleted file mode 100644 index 551d08d5f1420..0000000000000 --- a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessageJump.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { useRouter, useSearchParameter } from '@rocket.chat/ui-contexts'; -import { useRef, useEffect } from 'react'; - -import { waitForElement } from '../../../../../lib/utils/waitForElement'; -import { clearHighlightMessage, setHighlightMessage } from '../../../MessageList/providers/messageHighlightSubscription'; - -export const useLegacyThreadMessageJump = ({ enabled = true }: { enabled?: boolean }) => { - const router = useRouter(); - const mid = useSearchParameter('msg'); - - const clearQueryStringParameter = () => { - const name = router.getRouteName(); - - if (!name) { - return; - } - - const { msg: _, ...search } = router.getSearchParameters(); - - router.navigate( - { - name, - params: router.getRouteParameters(), - search, - }, - { replace: true }, - ); - }; - - const parentRef = useRef(null); - const clearQueryStringParameterRef = useRef(clearQueryStringParameter); - clearQueryStringParameterRef.current = clearQueryStringParameter; - - useEffect(() => { - const parent = parentRef.current; - - if (!enabled || !mid || !parent) { - return; - } - - const abortController = new AbortController(); - - waitForElement(`[data-id='${mid}']`, { parent, signal: abortController.signal }).then((messageElement) => { - if (abortController.signal.aborted) { - return; - } - - setHighlightMessage(mid); - clearQueryStringParameterRef.current?.(); - - setTimeout(() => { - clearHighlightMessage(); - }, 1000); - - setTimeout(() => { - if (abortController.signal.aborted) { - return; - } - - messageElement.scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - }); - }, 300); - }); - - return () => { - abortController.abort(); - }; - }, [enabled, mid]); - - return { parentRef }; -}; diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessageListScrolling.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessageListScrolling.ts index e7b6dabfe7d69..51ccda787b542 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessageListScrolling.ts +++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessageListScrolling.ts @@ -1,36 +1,16 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { isEditedMessage } from '@rocket.chat/core-typings'; -import { useUser } from '@rocket.chat/ui-contexts'; -import { useCallback, useEffect, useRef } from 'react'; +import { useUserId } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; import { callbacks } from '../../../../../../lib/callbacks'; -import type { OverlayScrollbars } from '../../../../../components/CustomScrollbars'; +import { useListIsAtBottom } from '../../../body/hooks/useListIsAtBottom'; import { useRoom } from '../../../contexts/RoomContext'; export const useLegacyThreadMessageListScrolling = (mainMessage: IMessage) => { - const listWrapperRef = useRef(null); - const listRef = useRef(null); - - const atBottomRef = useRef(true); - - const onScroll = useCallback(({ elements }: OverlayScrollbars) => { - const { - viewport: { scrollTop, scrollHeight, clientHeight }, - } = elements(); - atBottomRef.current = scrollTop >= scrollHeight - clientHeight; - }, []); - - const sendToBottomIfNecessary = useCallback(() => { - if (atBottomRef.current === true) { - const listWrapper = listWrapperRef.current; - - listWrapper?.scrollTo(30, listWrapper.scrollHeight); - } - }, []); - + const { atBottomRef, innerRef, sendToBottom, sendToBottomIfNecessary, isAtBottom, jumpToRef } = useListIsAtBottom(); const room = useRoom(); - const user = useUser(); - + const uid = useUserId(); useEffect(() => { callbacks.add( 'streamNewMessage', @@ -39,7 +19,7 @@ export const useLegacyThreadMessageListScrolling = (mainMessage: IMessage) => { return; } - if (msg.u._id === user?._id) { + if (msg.u._id === uid) { atBottomRef.current = true; sendToBottomIfNecessary(); } @@ -51,20 +31,7 @@ export const useLegacyThreadMessageListScrolling = (mainMessage: IMessage) => { return () => { callbacks.remove('streamNewMessage', `thread-scroll-${room._id}`); }; - }, [room._id, sendToBottomIfNecessary, user?._id, mainMessage._id]); - - useEffect(() => { - const observer = new ResizeObserver(() => { - sendToBottomIfNecessary(); - }); - - if (listWrapperRef.current) observer.observe(listWrapperRef.current); - if (listRef.current) observer.observe(listRef.current); - - return () => { - observer.disconnect(); - }; - }, [sendToBottomIfNecessary]); + }, [room._id, atBottomRef, sendToBottomIfNecessary, uid, mainMessage._id]); - return { listWrapperRef, listRef, requestScrollToBottom: sendToBottomIfNecessary, onScroll }; + return { atBottomRef, innerRef, sendToBottom, sendToBottomIfNecessary, isAtBottom, jumpToRef }; }; diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessages.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessages.ts index 6545d85de16c2..ba0a18e69fe1f 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessages.ts +++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessages.ts @@ -36,12 +36,11 @@ export const useLegacyThreadMessages = ( }, [tmid]), ); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(messages.length === 0); const getThreadMessages = useMethod('getThreadMessages'); useEffect(() => { - setLoading(true); getThreadMessages({ tmid }).then((messages) => { upsertMessageBulk({ msgs: messages }, Messages); setLoading(false); diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts index eef1630d35337..6baed3917707d 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts +++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts @@ -1,4 +1,6 @@ import type { IMessage, IThreadMainMessage } from '@rocket.chat/core-typings'; +import type { FieldExpression, Query } from '@rocket.chat/mongo-adapter'; +import { createFilterFromQuery } from '@rocket.chat/mongo-adapter'; import { useStream } from '@rocket.chat/ui-contexts'; import type { UseQueryResult } from '@tanstack/react-query'; import { useQueryClient, useQuery } from '@tanstack/react-query'; @@ -6,8 +8,6 @@ import { useCallback, useEffect, useRef } from 'react'; import { useGetMessageByID } from './useGetMessageByID'; import { withDebouncing } from '../../../../../../lib/utils/highOrderFunctions'; -import type { FieldExpression, Query } from '../../../../../lib/minimongo'; -import { createFilterFromQuery } from '../../../../../lib/minimongo'; import { onClientMessageReceived } from '../../../../../lib/onClientMessageReceived'; import { useRoom } from '../../../contexts/RoomContext'; diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx index d028a2620741e..7ba4d58cbc705 100644 --- a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx +++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx @@ -11,15 +11,13 @@ import { useEffect, useMemo } from 'react'; import { FocusScope } from 'react-aria'; import VideoConfPopup from './VideoConfPopup'; -import { useUserSoundPreferences } from '../../../../../hooks/useUserSoundPreferences'; import VideoConfPopupPortal from '../../../../../portals/VideoConfPopupPortal'; const VideoConfPopups = ({ children }: { children?: VideoConfPopupPayload }): ReactElement => { - const customSound = useCustomSound(); + const { callSounds } = useCustomSound(); const incomingCalls = useVideoConfIncomingCalls(); const isRinging = useVideoConfIsRinging(); const isCalling = useVideoConfIsCalling(); - const { voipRingerVolume } = useUserSoundPreferences(); const popups = useMemo( () => @@ -31,18 +29,18 @@ const VideoConfPopups = ({ children }: { children?: VideoConfPopupPayload }): Re useEffect(() => { if (isRinging) { - customSound.play('ringtone', { loop: true, volume: voipRingerVolume / 100 }); + callSounds.playRinger(); } if (isCalling) { - customSound.play('dialtone', { loop: true, volume: voipRingerVolume / 100 }); + callSounds.playDialer(); } return (): void => { - customSound.stop('ringtone'); - customSound.stop('dialtone'); + callSounds.stopRinger(); + callSounds.stopDialer(); }; - }, [customSound, isRinging, isCalling, voipRingerVolume]); + }, [isRinging, isCalling, callSounds]); return ( <> diff --git a/apps/meteor/client/views/room/hooks/useOpenRoom.ts b/apps/meteor/client/views/room/hooks/useOpenRoom.ts index 8faad251dc8b0..3c2da5c0d01ee 100644 --- a/apps/meteor/client/views/room/hooks/useOpenRoom.ts +++ b/apps/meteor/client/views/room/hooks/useOpenRoom.ts @@ -1,4 +1,4 @@ -import { isOmnichannelRoom, type IRoom, type RoomType } from '@rocket.chat/core-typings'; +import { isPublicRoom, type IRoom, type RoomType } from '@rocket.chat/core-typings'; import { useMethod, usePermission, useRoute, useSetting, useUser } from '@rocket.chat/ui-contexts'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useEffect } from 'react'; @@ -90,7 +90,7 @@ export function useOpenRoom({ type, reference }: { type: RoomType; reference: st const sub = Subscriptions.findOne({ rid: room._id }); // if user doesn't exist at this point, anonymous read is enabled, otherwise an error would have been thrown - if (user && !sub && !hasPreviewPermission && !isOmnichannelRoom(room)) { + if (user && !sub && !hasPreviewPermission && isPublicRoom(room)) { throw new NotSubscribedToRoomError(undefined, { rid: room._id }); } diff --git a/apps/meteor/client/views/room/hooks/useUserHasRoomRole.ts b/apps/meteor/client/views/room/hooks/useUserHasRoomRole.ts index 508393c6c673a..363ee57667498 100644 --- a/apps/meteor/client/views/room/hooks/useUserHasRoomRole.ts +++ b/apps/meteor/client/views/room/hooks/useUserHasRoomRole.ts @@ -1,8 +1,13 @@ import type { IRole, IRoom, IUser } from '@rocket.chat/core-typings'; import { useCallback } from 'react'; -import { RoomRoles } from '../../../../app/models/client'; -import { useReactiveValue } from '../../../hooks/useReactiveValue'; +import type { RoomRoles } from '../../../hooks/useRoomRolesQuery'; +import { useRoomRolesQuery } from '../../../hooks/useRoomRolesQuery'; export const useUserHasRoomRole = (uid: IUser['_id'], rid: IRoom['_id'], role: IRole['name']): boolean => - useReactiveValue(useCallback(() => !!RoomRoles.findOne({ rid, 'u._id': uid, 'roles': role }), [uid, rid, role])); + useRoomRolesQuery(rid, { + select: useCallback( + (records: RoomRoles[]) => records.some((record) => record.u._id === uid && record.roles.includes(role)), + [role, uid], + ), + }).data ?? false; diff --git a/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx b/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx index 22557f4a6d15b..08f4a60b88737 100644 --- a/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx +++ b/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx @@ -2,17 +2,17 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { useLocalStorage } from '@rocket.chat/fuselage-hooks'; import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { useMethod, useSetting, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useMethod, useSetting, useUserId, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useQueryClient } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; import type { ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { hasAtLeastOnePermission } from '../../../../app/authorization/client'; -import { CannedResponse } from '../../../../app/canned-responses/client/collections/CannedResponse'; import { emoji } from '../../../../app/emoji/client'; -import { Subscriptions } from '../../../../app/models/client'; -import { usersFromRoomMessages } from '../../../../app/ui-message/client/popup/messagePopupConfig'; +import { Messages, Subscriptions } from '../../../../app/models/client'; import { slashCommands } from '../../../../app/utils/client'; +import { cannedResponsesQueryKeys } from '../../../lib/queryKeys'; import ComposerBoxPopupCannedResponse from '../composer/ComposerBoxPopupCannedResponse'; import type { ComposerBoxPopupEmojiProps } from '../composer/ComposerBoxPopupEmoji'; import ComposerBoxPopupEmoji from '../composer/ComposerBoxPopupEmoji'; @@ -24,14 +24,62 @@ import ComposerBoxPopupUser from '../composer/ComposerBoxPopupUser'; import type { ComposerBoxPopupUserProps } from '../composer/ComposerBoxPopupUser'; import type { ComposerPopupContextValue } from '../contexts/ComposerPopupContext'; import { ComposerPopupContext, createMessageBoxPopupConfig } from '../contexts/ComposerPopupContext'; +import useCannedResponsesQuery from './hooks/useCannedResponsesQuery'; + +export type CannedResponse = { _id: string; shortcut: string; text: string }; type ComposerPopupProviderProps = { children: ReactNode; room: IRoom; }; +const getLastRecentUsers = (rid: string, uid: string) => { + const uniqueUsers = new Map< + string, + { + _id: string; + username: string; + name?: string; + ts: Date; + suggestion?: boolean; + } + >(); + Messages.find( + { + rid, + 'u._id': { $ne: uid }, + 't': { $exists: false }, + 'ts': { $exists: true }, + }, + { + fields: { + 'u.username': 1, + 'u.name': 1, + 'u._id': 1, + 'ts': 1, + }, + sort: { ts: -1 }, + }, + ).forEach(({ u: { username, name, _id }, ts }) => { + if (!uniqueUsers.has(username)) { + uniqueUsers.set(username, { + _id, + username, + name, + ts, + }); + } + }); + + return Array.from(uniqueUsers.values()); +}; const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) => { const { _id: rid, encrypted: isRoomEncrypted } = room; + + // TODO: this is awful because we are just triggering the query to get the data + // and we are not using the data itself, we should find a better way to do this + useCannedResponsesQuery(room); + const userSpotlight = useMethod('spotlight'); const suggestionsCount = useSetting('Number_of_users_autocomplete_suggestions', 5); const cannedResponseEnabled = useSetting('Canned_Responses_Enable', true); @@ -43,8 +91,10 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = const e2eEnabled = useSetting('E2E_Enable', false); const unencryptedMessagesAllowed = useSetting('E2E_Allow_Unencrypted_Messages', false); const encrypted = isRoomEncrypted && e2eEnabled && !unencryptedMessagesAllowed; - + const queryClient = useQueryClient(); + const uid = useUserId(); const call = useMethod('getSlashCommandPreviews'); + const value: ComposerPopupContextValue = useMemo(() => { return [ createMessageBoxPopupConfig({ @@ -54,24 +104,18 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = const filterRegex = filter && new RegExp(escapeRegExp(filter), 'i'); const items: ComposerBoxPopupUserProps[] = []; - const users = usersFromRoomMessages - .find( - { - ts: { $exists: true }, - ...(filter && { - $or: [{ username: filterRegex }, { name: filterRegex }], - }), - }, - { - limit: suggestionsCount ?? 5, - sort: { ts: -1 }, - }, - ) - .fetch() - .map((u) => { - u.suggestion = true; - return u; - }); + const roomMessageUsers = getLastRecentUsers(rid, uid!) + .filter((u) => { + if (!filterRegex) return true; + return filterRegex.test(u.username) || (u.name && filterRegex.test(u.name)); + }) + .sort((a, b) => b.ts.getTime() - a.ts.getTime()) + .slice(0, suggestionsCount ?? 5) + .map((u) => ({ + ...u, + suggestion: true, + })); + if (!filterRegex || filterRegex.test('all')) { items.push({ _id: 'all', @@ -92,27 +136,19 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = }); } - return [...users, ...items]; + return [...roomMessageUsers, ...items]; }, getItemsFromServer: async (filter: string) => { const filterRegex = filter && new RegExp(escapeRegExp(filter), 'i'); - const usernames = usersFromRoomMessages - .find( - { - ts: { $exists: true }, - ...(filter && { - $or: [{ username: filterRegex }, { name: filterRegex }], - }), - }, - { - limit: suggestionsCount ?? 5, - sort: { ts: -1 }, - }, - ) - .fetch() - .map((u) => { - return u.username; - }); + const usernames = getLastRecentUsers(rid, uid!) + .filter((u) => { + if (!filterRegex) return true; + return filterRegex.test(u.username) || (u.name && filterRegex.test(u.name)); + }) + .sort((a, b) => b.ts.getTime() - a.ts.getTime()) + .slice(0, suggestionsCount ?? 5) + .map((u) => u.username); + const { users = [] } = await userSpotlight(filter, usernames, { users: true, mentions: true }, rid); return users.map(({ _id, username, nickname, name, status, avatarETag, outside }) => { @@ -334,18 +370,12 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = renderItem: ({ item }) => , getItemsFromLocal: async (filter: string) => { const exp = new RegExp(filter, 'i'); - return CannedResponse.find( - { - shortcut: exp, - }, - { - limit: 12, - sort: { - shortcut: -1, - }, - }, - ) - .fetch() + // TODO: this is bad, but can only be fixed by refactoring the whole thing + const cannedResponses = queryClient.getQueryData(cannedResponsesQueryKeys.all) ?? []; + return cannedResponses + .filter((record) => record.shortcut.match(exp)) + .sort((a, b) => a.shortcut.localeCompare(b.shortcut)) + .slice(0, 11) .map((record) => ({ _id: record._id, text: record.text, @@ -353,9 +383,7 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = })); }, getItemsFromServer: async () => [], - getValue: (item) => { - return item.text; - }, + getValue: (item) => item.text, }), createMessageBoxPopupConfig({ title: previewTitle, @@ -377,19 +405,20 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = }), ].filter(Boolean); }, [ - t, - useEmoji, - encrypted, + call, cannedResponseEnabled, + encrypted, + i18n, isOmnichannel, previewTitle, + queryClient, + recentEmojis, + rid, suggestionsCount, + t, + uid, + useEmoji, userSpotlight, - rid, - recentEmojis, - i18n, - call, - setPreviewTitle, ]); return ; diff --git a/apps/meteor/client/views/room/providers/RoomProvider.tsx b/apps/meteor/client/views/room/providers/RoomProvider.tsx index 380eb184851a4..cc40e6d51c8a4 100644 --- a/apps/meteor/client/views/room/providers/RoomProvider.tsx +++ b/apps/meteor/client/views/room/providers/RoomProvider.tsx @@ -23,7 +23,6 @@ import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; import ImageGalleryProvider from '../../../providers/ImageGalleryProvider'; import RoomNotFound from '../RoomNotFound'; import RoomSkeleton from '../RoomSkeleton'; -import { useRoomRolesManagement } from '../body/hooks/useRoomRolesManagement'; import type { IRoomWithFederationOriginalName } from '../contexts/RoomContext'; import { RoomContext } from '../contexts/RoomContext'; @@ -33,8 +32,6 @@ type RoomProviderProps = { }; const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { - useRoomRolesManagement(rid); - const resultFromServer = useRoomInfoEndpoint(rid); const resultFromLocal = useRoomQuery(rid); diff --git a/apps/meteor/client/views/room/providers/hooks/useCannedResponsesQuery.ts b/apps/meteor/client/views/room/providers/hooks/useCannedResponsesQuery.ts new file mode 100644 index 0000000000000..0150ecdf9ae50 --- /dev/null +++ b/apps/meteor/client/views/room/providers/hooks/useCannedResponsesQuery.ts @@ -0,0 +1,64 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { useEndpoint, usePermission, useSetting, useStream, useUserId } from '@rocket.chat/ui-contexts'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useEffect } from 'react'; + +import { cannedResponsesQueryKeys } from '../../../../lib/queryKeys'; + +type CannedResponse = { _id: string; shortcut: string; text: string }; + +const useCannedResponsesQuery = (room: IRoom) => { + const isOmnichannel = isOmnichannelRoom(room); + const uid = useUserId(); + const isCannedResponsesEnabled = useSetting('Canned_Responses_Enable', true); + const canViewCannedResponses = usePermission('view-canned-responses'); + const subscribeToCannedResponses = useStream('canned-responses'); + + const enabled = isOmnichannel && !!uid && isCannedResponsesEnabled && canViewCannedResponses; + + const getCannedResponses = useEndpoint('GET', '/v1/canned-responses.get'); + + const queryClient = useQueryClient(); + + useEffect(() => { + if (!enabled) return; + + return subscribeToCannedResponses('canned-responses', (...[response, options]) => { + const { agentsId } = options || {}; + if (Array.isArray(agentsId) && !agentsId.includes(uid)) { + return; + } + + switch (response.type) { + case 'changed': { + const { _id, shortcut, text } = response; + queryClient.setQueryData(cannedResponsesQueryKeys.all, (responses) => + responses?.filter((response) => response._id !== _id).concat([{ _id, shortcut, text }]), + ); + break; + } + + case 'removed': { + const { _id } = response; + queryClient.setQueryData(cannedResponsesQueryKeys.all, (responses) => + responses?.filter((response) => response._id !== _id), + ); + break; + } + } + }); + }, [enabled, getCannedResponses, queryClient, subscribeToCannedResponses, uid]); + + return useQuery({ + queryKey: cannedResponsesQueryKeys.all, + queryFn: async () => { + const { responses } = await getCannedResponses(); + return responses.map(({ _id, shortcut, text }) => ({ _id, shortcut, text })); + }, + enabled, + staleTime: Infinity, + }); +}; + +export default useCannedResponsesQuery; diff --git a/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.spec.ts b/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.spec.ts index b63056ec4e228..40401bfb1b992 100644 --- a/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.spec.ts +++ b/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.spec.ts @@ -35,6 +35,7 @@ jest.mock('../../../../../app/ui/client/lib/ChatMessages', () => { release: jest.fn(), readStateManager: { updateSubscription: updateSubscriptionMock, + subscribeToMessages: jest.fn(), }, }; }), diff --git a/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.ts b/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.ts index fbadf90e2947d..06bf65eebdf43 100644 --- a/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.ts +++ b/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.ts @@ -30,6 +30,12 @@ export function useChatMessagesInstance({ return [instance, () => instance.release()]; }, [rid, tmid, uid, encrypted, e2eRoomState]); + useEffect(() => { + if (subscription?.rid) { + return chatMessages?.readStateManager.subscribeToMessages(); + } + }, [subscription?.rid, chatMessages?.readStateManager]); + useEffect(() => { if (subscription) { chatMessages?.readStateManager.updateSubscription(subscription); diff --git a/apps/meteor/client/views/root/AppLayout.tsx b/apps/meteor/client/views/root/AppLayout.tsx index 23a1373f6cfba..e7afa4be398da 100644 --- a/apps/meteor/client/views/root/AppLayout.tsx +++ b/apps/meteor/client/views/root/AppLayout.tsx @@ -2,14 +2,14 @@ import { useEffect, Suspense, useSyncExternalStore } from 'react'; import DocumentTitleWrapper from './DocumentTitleWrapper'; import PageLoading from './PageLoading'; +import { useCodeHighlight } from './hooks/useCodeHighlight'; import { useEscapeKeyStroke } from './hooks/useEscapeKeyStroke'; import { useGoogleTagManager } from './hooks/useGoogleTagManager'; +import { useLoadMissedMessages } from './hooks/useLoadMissedMessages'; +import { useLoginViaQuery } from './hooks/useLoginViaQuery'; import { useMessageLinkClicks } from './hooks/useMessageLinkClicks'; -import { useOTRMessaging } from './hooks/useOTRMessaging'; import { useSettingsOnLoadSiteUrl } from './hooks/useSettingsOnLoadSiteUrl'; -import { useStoreCookiesOnLogin } from './hooks/useStoreCookiesOnLogin'; -import { useUpdateVideoConfUser } from './hooks/useUpdateVideoConfUser'; -import { useAnalytics } from '../../../app/analytics/client/loadScript'; +import { useWordPressOAuth } from './hooks/useWordPressOAuth'; import { useCorsSSLConfig } from '../../../app/cors/client/useCorsSSLConfig'; import { useDolphin } from '../../../app/dolphin/client/hooks/useDolphin'; import { useDrupal } from '../../../app/drupal/client/hooks/useDrupal'; @@ -19,10 +19,11 @@ import { useGitLabAuth } from '../../../app/gitlab/client/hooks/useGitLabAuth'; import { useLivechatEnterprise } from '../../../app/livechat-enterprise/hooks/useLivechatEnterprise'; import { useNextcloud } from '../../../app/nextcloud/client/useNextcloud'; import { useTokenPassAuth } from '../../../app/tokenpass/client/hooks/useTokenPassAuth'; +import { useNotificationPermission } from '../../hooks/notification/useNotificationPermission'; +import { useAnalytics } from '../../hooks/useAnalytics'; import { useAnalyticsEventTracking } from '../../hooks/useAnalyticsEventTracking'; import { useAutoupdate } from '../../hooks/useAutoupdate'; import { useLoadRoomForAllowedAnonymousRead } from '../../hooks/useLoadRoomForAllowedAnonymousRead'; -import { useNotifyUser } from '../../hooks/useNotifyUser'; import { appLayout } from '../../lib/appLayout'; import { useCustomOAuth } from '../../sidebar/hooks/useCustomOAuth'; import { useRedirectToSetupWizard } from '../../startup/useRedirectToSetupWizard'; @@ -42,7 +43,7 @@ const AppLayout = () => { useEscapeKeyStroke(); useAnalyticsEventTracking(); useLoadRoomForAllowedAnonymousRead(); - useNotifyUser(); + useNotificationPermission(); useEmojiOne(); useRedirectToSetupWizard(); useSettingsOnLoadSiteUrl(); @@ -53,12 +54,13 @@ const AppLayout = () => { useDrupal(); useDolphin(); useTokenPassAuth(); + useWordPressOAuth(); useCustomOAuth(); useCorsSSLConfig(); - useOTRMessaging(); - useUpdateVideoConfUser(); - useStoreCookiesOnLogin(); useAutoupdate(); + useCodeHighlight(); + useLoginViaQuery(); + useLoadMissedMessages(); const layout = useSyncExternalStore(appLayout.subscribe, appLayout.getSnapshot); diff --git a/apps/meteor/client/views/root/MainLayout/AuthenticationCheck.tsx b/apps/meteor/client/views/root/MainLayout/AuthenticationCheck.tsx index a10d59eff67b8..822acfc22562b 100644 --- a/apps/meteor/client/views/root/MainLayout/AuthenticationCheck.tsx +++ b/apps/meteor/client/views/root/MainLayout/AuthenticationCheck.tsx @@ -1,7 +1,8 @@ -import { useSession, useUserId, useSetting } from '@rocket.chat/ui-contexts'; +import { useSession, useUser, useSetting } from '@rocket.chat/ui-contexts'; import RegistrationRoute from '@rocket.chat/web-ui-registration'; import type { ReactElement, ReactNode } from 'react'; +import LoggedInArea from './LoggedInArea'; import LoginPage from './LoginPage'; import UsernameCheck from './UsernameCheck'; @@ -15,12 +16,16 @@ import UsernameCheck from './UsernameCheck'; * renders the page, without creating an user (not even an anonymous user) */ const AuthenticationCheck = ({ children, guest }: { children: ReactNode; guest?: boolean }): ReactElement => { - const uid = useUserId(); + const user = useUser(); const allowAnonymousRead = useSetting('Accounts_AllowAnonymousRead'); const forceLogin = useSession('forceLogin'); - if (uid) { - return {children}; + if (user) { + return ( + + {children} + + ); } if (!forceLogin && guest) { diff --git a/apps/meteor/client/views/root/MainLayout/LoggedInArea.tsx b/apps/meteor/client/views/root/MainLayout/LoggedInArea.tsx new file mode 100644 index 0000000000000..c86be332c7553 --- /dev/null +++ b/apps/meteor/client/views/root/MainLayout/LoggedInArea.tsx @@ -0,0 +1,32 @@ +import { useUser } from '@rocket.chat/ui-contexts'; +import type { ReactNode } from 'react'; + +import { useCustomEmoji } from '../../../hooks/customEmoji/useCustomEmoji'; +import { useNotificationUserCalendar } from '../../../hooks/notification/useNotificationUserCalendar'; +import { useNotifyUser } from '../../../hooks/notification/useNotifyUser'; +import { useForceLogout } from '../hooks/useForceLogout'; +import { useOTRMessaging } from '../hooks/useOTRMessaging'; +import { useStoreCookiesOnLogin } from '../hooks/useStoreCookiesOnLogin'; +import { useUpdateVideoConfUser } from '../hooks/useUpdateVideoConfUser'; +import { useWebRTC } from '../hooks/useWebRTC'; + +const LoggedInArea = ({ children }: { children: ReactNode }) => { + const user = useUser(); + + if (!user) { + throw new Error('User not logged'); + } + + useNotifyUser(user); + useUpdateVideoConfUser(user._id); + useWebRTC(user._id); + useOTRMessaging(user._id); + useNotificationUserCalendar(user); + useForceLogout(user._id); + useStoreCookiesOnLogin(user._id); + useCustomEmoji(); + + return children; +}; + +export default LoggedInArea; diff --git a/apps/meteor/client/views/root/MainLayout/TwoFactorAuthSetupCheck.tsx b/apps/meteor/client/views/root/MainLayout/TwoFactorAuthSetupCheck.tsx index fc3020da7d01e..0b9bf5701f9a8 100644 --- a/apps/meteor/client/views/root/MainLayout/TwoFactorAuthSetupCheck.tsx +++ b/apps/meteor/client/views/root/MainLayout/TwoFactorAuthSetupCheck.tsx @@ -1,30 +1,25 @@ import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; -import { useLayout, useUser, useSetting } from '@rocket.chat/ui-contexts'; +import { useLayout, useSetModal } from '@rocket.chat/ui-contexts'; import type { ReactElement, ReactNode } from 'react'; -import { lazy, useCallback } from 'react'; +import { lazy, useLayoutEffect } from 'react'; import LayoutWithSidebar from './LayoutWithSidebar'; import LayoutWithSidebarV2 from './LayoutWithSidebarV2'; -import { Roles } from '../../../../app/models/client'; -import { useReactiveValue } from '../../../hooks/useReactiveValue'; +import TwoFactorRequiredModal from './TwoFactorRequiredModal'; +import { useRequire2faSetup } from '../../hooks/useRequire2faSetup'; const AccountSecurityPage = lazy(() => import('../../account/security/AccountSecurityPage')); const TwoFactorAuthSetupCheck = ({ children }: { children: ReactNode }): ReactElement => { const { isEmbedded: embeddedLayout } = useLayout(); - const user = useUser(); - const tfaEnabled = useSetting('Accounts_TwoFactorAuthentication_Enabled'); - const require2faSetup = useReactiveValue( - useCallback(() => { - // User is already using 2fa - if (!user || user?.services?.totp?.enabled || user?.services?.email2fa?.enabled) { - return false; - } + const require2faSetup = useRequire2faSetup(); + const setModal = useSetModal(); - const mandatoryRole = Roles.findOne({ _id: { $in: user.roles ?? [] }, mandatory2fa: true }); - return mandatoryRole !== undefined && tfaEnabled; - }, [tfaEnabled, user]), - ); + useLayoutEffect(() => { + if (require2faSetup) { + setModal(); + } + }, [setModal, require2faSetup]); if (require2faSetup) { return ( diff --git a/apps/meteor/client/views/root/MainLayout/TwoFactorRequiredModal.tsx b/apps/meteor/client/views/root/MainLayout/TwoFactorRequiredModal.tsx new file mode 100644 index 0000000000000..fcc39d5b4b9e6 --- /dev/null +++ b/apps/meteor/client/views/root/MainLayout/TwoFactorRequiredModal.tsx @@ -0,0 +1,31 @@ +import { Button, Modal, ModalContent, ModalFooter, ModalFooterControllers, ModalHeader, ModalTitle } from '@rocket.chat/fuselage'; +import { useSetModal } from '@rocket.chat/ui-contexts'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +const TwoFactorRequiredModal = () => { + const { t } = useTranslation(); + const setModal = useSetModal(); + + const closeModal = useCallback(() => { + setModal(null); + }, [setModal]); + + return ( + + + {t('Two-factor_authentication_required')} + + {t('Enable_two-factor_authentication_callout_description')} + + + + + + + ); +}; + +export default TwoFactorRequiredModal; diff --git a/apps/meteor/client/views/root/hooks/useCodeHighlight.ts b/apps/meteor/client/views/root/hooks/useCodeHighlight.ts new file mode 100644 index 0000000000000..ad65adce3361d --- /dev/null +++ b/apps/meteor/client/views/root/hooks/useCodeHighlight.ts @@ -0,0 +1,19 @@ +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +import { register } from '../../../../app/markdown/lib/hljs'; + +export const useCodeHighlight = (): void => { + const codeHighlight = useSetting('Message_Code_highlight'); + + useEffect(() => { + if (typeof codeHighlight === 'string') { + codeHighlight.split(',').forEach((language: string) => { + const trimmedLanguage = language.trim(); + if (trimmedLanguage) { + register(trimmedLanguage); + } + }); + } + }, [codeHighlight]); +}; diff --git a/apps/meteor/client/views/root/hooks/useForceLogout.ts b/apps/meteor/client/views/root/hooks/useForceLogout.ts new file mode 100644 index 0000000000000..07390ddb0d4aa --- /dev/null +++ b/apps/meteor/client/views/root/hooks/useForceLogout.ts @@ -0,0 +1,17 @@ +import { useStream, useSessionDispatch } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +export const useForceLogout = (userId: string) => { + const getNotifyUserStream = useStream('notify-user'); + const setForceLogout = useSessionDispatch('forceLogout'); + + useEffect(() => { + setForceLogout(false); + + const unsubscribe = getNotifyUserStream(`${userId}/force_logout`, () => { + setForceLogout(true); + }); + + return unsubscribe; + }, [getNotifyUserStream, setForceLogout, userId]); +}; diff --git a/apps/meteor/client/views/root/hooks/useLoadMissedMessages.ts b/apps/meteor/client/views/root/hooks/useLoadMissedMessages.ts new file mode 100644 index 0000000000000..976b1b2bafbbc --- /dev/null +++ b/apps/meteor/client/views/root/hooks/useLoadMissedMessages.ts @@ -0,0 +1,50 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { useConnectionStatus } from '@rocket.chat/ui-contexts'; +import { useEffect, useRef } from 'react'; + +import { Messages, Subscriptions } from '../../../../app/models/client'; +import { LegacyRoomManager, upsertMessage } from '../../../../app/ui-utils/client'; +import { callWithErrorHandling } from '../../../lib/utils/callWithErrorHandling'; + +/** + * Loads missed messages for a room + * @param rid - Room ID + */ +const loadMissedMessages = async (rid: IRoom['_id']): Promise => { + const lastMessage = Messages.findOne({ rid, _hidden: { $ne: true }, temp: { $exists: false } }, { sort: { ts: -1 }, limit: 1 }); + + if (!lastMessage) { + return; + } + + try { + const result = await callWithErrorHandling('loadMissedMessages', rid, lastMessage.ts); + if (result) { + const subscription = Subscriptions.findOne({ rid }); + await Promise.all(Array.from(result).map((msg) => upsertMessage({ msg, subscription }))); + } + } catch (error) { + console.error('Error loading missed messages:', error); + } +}; + +/** + * React hook that loads missed messages when connection is restored + */ +export const useLoadMissedMessages = (): void => { + const { connected } = useConnectionStatus(); + const connectionWasOnlineRef = useRef(connected); + + useEffect(() => { + if (connected === true && connectionWasOnlineRef.current === false && LegacyRoomManager.openedRooms) { + Object.keys(LegacyRoomManager.openedRooms).forEach((key) => { + const value = LegacyRoomManager.openedRooms[key]; + if (value.rid) { + loadMissedMessages(value.rid); + } + }); + } + + connectionWasOnlineRef.current = connected; + }, [connected]); +}; diff --git a/apps/meteor/client/views/root/hooks/useLoginViaQuery.ts b/apps/meteor/client/views/root/hooks/useLoginViaQuery.ts new file mode 100644 index 0000000000000..a67739eb7b025 --- /dev/null +++ b/apps/meteor/client/views/root/hooks/useLoginViaQuery.ts @@ -0,0 +1,41 @@ +import { useRouter, useLoginWithToken } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +export const useLoginViaQuery = () => { + const router = useRouter(); + const loginWithToken = useLoginWithToken(); + + useEffect(() => { + const handleLogin = async () => { + const { resumeToken } = router.getSearchParameters(); + + if (!resumeToken) { + return; + } + + try { + await loginWithToken(resumeToken); + + const routeName = router.getRouteName(); + + if (!routeName) { + router.navigate('/home'); + } + + const { resumeToken: _, userId: __, ...search } = router.getSearchParameters(); + + router.navigate( + { + pathname: router.getLocationPathname(), + search, + }, + { replace: true }, + ); + } catch (error) { + console.error('Failed to login with token', error); + } + }; + + handleLogin(); + }, [loginWithToken, router]); +}; diff --git a/apps/meteor/client/views/root/hooks/useOTRMessaging.ts b/apps/meteor/client/views/root/hooks/useOTRMessaging.ts index 3b09e84b86692..b3195326fe8c6 100644 --- a/apps/meteor/client/views/root/hooks/useOTRMessaging.ts +++ b/apps/meteor/client/views/root/hooks/useOTRMessaging.ts @@ -1,6 +1,6 @@ import type { AtLeast, IMessage } from '@rocket.chat/core-typings'; import { isOTRMessage } from '@rocket.chat/core-typings'; -import { useMethod, useStream, useUserId } from '@rocket.chat/ui-contexts'; +import { useMethod, useStream } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; import OTR from '../../../../app/otr/client/OTR'; @@ -9,16 +9,11 @@ import { t } from '../../../../app/utils/lib/i18n'; import { onClientBeforeSendMessage } from '../../../lib/onClientBeforeSendMessage'; import { onClientMessageReceived } from '../../../lib/onClientMessageReceived'; -export const useOTRMessaging = () => { - const uid = useUserId(); +export const useOTRMessaging = (uid: string) => { const updateOTRAck = useMethod('updateOTRAck'); const notifyUser = useStream('notify-user'); useEffect(() => { - if (!uid) { - return; - } - const handleNotifyUser = (type: 'handshake' | 'acknowledge' | 'deny' | 'end', data: { roomId: string; userId: string }) => { if (!data.roomId || !data.userId || data.userId === uid) { return; diff --git a/apps/meteor/client/views/root/hooks/useStoreCookiesOnLogin.ts b/apps/meteor/client/views/root/hooks/useStoreCookiesOnLogin.ts index 1d43b408b81f6..712726bd9a063 100644 --- a/apps/meteor/client/views/root/hooks/useStoreCookiesOnLogin.ts +++ b/apps/meteor/client/views/root/hooks/useStoreCookiesOnLogin.ts @@ -1,15 +1,14 @@ -import { useConnectionStatus, useUserId } from '@rocket.chat/ui-contexts'; +import { useConnectionStatus } from '@rocket.chat/ui-contexts'; import { Accounts } from 'meteor/accounts-base'; import { useEffect } from 'react'; -export const useStoreCookiesOnLogin = () => { - const userId = useUserId(); +export const useStoreCookiesOnLogin = (userId: string) => { const { isLoggingIn } = useConnectionStatus(); useEffect(() => { // Check for isLoggingIn to be reactive and ensure it will process only after login finishes // preventing race condition setting the rc_token as null forever - if (userId && isLoggingIn === false) { + if (isLoggingIn === false) { const secure = location.protocol === 'https:' ? '; secure' : ''; document.cookie = `rc_uid=${encodeURI(userId)}; path=/${secure}`; diff --git a/apps/meteor/client/views/root/hooks/useUpdateVideoConfUser.ts b/apps/meteor/client/views/root/hooks/useUpdateVideoConfUser.ts index bc3113eeb7fc5..67e3e72dd3012 100644 --- a/apps/meteor/client/views/root/hooks/useUpdateVideoConfUser.ts +++ b/apps/meteor/client/views/root/hooks/useUpdateVideoConfUser.ts @@ -1,10 +1,9 @@ -import { useConnectionStatus, useUserId } from '@rocket.chat/ui-contexts'; +import { useConnectionStatus } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; import { VideoConfManager } from '../../../lib/VideoConfManager'; -export const useUpdateVideoConfUser = () => { - const userId = useUserId(); +export const useUpdateVideoConfUser = (userId: string) => { const { connected, isLoggingIn } = useConnectionStatus(); useEffect(() => { diff --git a/apps/meteor/client/views/root/hooks/useWebRTC.ts b/apps/meteor/client/views/root/hooks/useWebRTC.ts new file mode 100644 index 0000000000000..a6182f985d406 --- /dev/null +++ b/apps/meteor/client/views/root/hooks/useWebRTC.ts @@ -0,0 +1,36 @@ +import { useStream } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +import type { CandidateData, DescriptionData, JoinData } from '../../../../app/webrtc/client/WebRTCClass'; +import { WebRTC } from '../../../../app/webrtc/client/WebRTCClass'; +import { WEB_RTC_EVENTS } from '../../../../app/webrtc/lib/constants'; + +export const useWebRTC = (uid: string) => { + const notifyUser = useStream('notify-user'); + + useEffect(() => { + const handleNotifyUser = (type: 'candidate' | 'description' | 'join', data: CandidateData | DescriptionData | JoinData) => { + if (data.room == null) return; + + const webrtc = WebRTC.getInstanceByRoomId(data.room); + + if (!webrtc) return; + + switch (type) { + case 'candidate': + webrtc.onUserStream('candidate', data as CandidateData); + break; + case 'description': + webrtc.onUserStream('description', data as DescriptionData); + break; + case 'join': + webrtc.onUserStream('join', data as JoinData); + break; + default: + console.warn(`WebRTC: Received unexpected event type: ${type}`); + } + }; + + return notifyUser(`${uid}/${WEB_RTC_EVENTS.WEB_RTC}`, handleNotifyUser); + }, [notifyUser, uid]); +}; diff --git a/apps/meteor/client/views/root/hooks/useWordPressOAuth.ts b/apps/meteor/client/views/root/hooks/useWordPressOAuth.ts new file mode 100644 index 0000000000000..c6b90d741c178 --- /dev/null +++ b/apps/meteor/client/views/root/hooks/useWordPressOAuth.ts @@ -0,0 +1,72 @@ +import type { OauthConfig } from '@rocket.chat/core-typings'; +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +import { CustomOAuth } from '../../../../app/custom-oauth/client/CustomOAuth'; + +const configDefault: OauthConfig = { + serverURL: '', + addAutopublishFields: { + forLoggedInUser: ['services.wordpress'], + forOtherUsers: ['services.wordpress.user_login'], + }, + accessTokenParam: 'access_token', +}; + +const WordPress = CustomOAuth.configureOAuthService('wordpress', configDefault); + +const configureServerType = ( + serverType: string, + identityPath?: string, + identityTokenSentVia?: string, + tokenPath?: string, + authorizePath?: string, + scope?: string, +) => { + switch (serverType) { + case 'custom': { + return { + ...configDefault, + ...(identityPath && { identityPath }), + ...(identityTokenSentVia && { identityTokenSentVia }), + ...(tokenPath && { tokenPath }), + ...(authorizePath && { authorizePath }), + ...(scope && { scope }), + }; + } + + case 'wordpress-com': + return { + ...configDefault, + identityPath: 'https://public-api.wordpress.com/rest/v1/me', + identityTokenSentVia: 'header', + authorizePath: 'https://public-api.wordpress.com/oauth2/authorize', + tokenPath: 'https://public-api.wordpress.com/oauth2/token', + scope: 'auth', + }; + + default: + return { + ...configDefault, + identityPath: '/oauth/me', + }; + } +}; + +export const useWordPressOAuth = (): void => { + const wordpressURL = useSetting('API_Wordpress_URL') as string; + const serverType = useSetting('Accounts_OAuth_Wordpress_server_type') as string; + const identityPath = useSetting('Accounts_OAuth_Wordpress_identity_path') as string; + const identityTokenSentVia = useSetting('Accounts_OAuth_Wordpress_identity_token_sent_via') as string; + const tokenPath = useSetting('Accounts_OAuth_Wordpress_token_path') as string; + const authorizePath = useSetting('Accounts_OAuth_Wordpress_authorize_path') as string; + const scope = useSetting('Accounts_OAuth_Wordpress_scope') as string; + + useEffect(() => { + WordPress.configure({ + ...configDefault, + ...configureServerType(serverType, identityPath, identityTokenSentVia, tokenPath, authorizePath, scope), + serverURL: wordpressURL, + }); + }, [authorizePath, identityPath, identityTokenSentVia, scope, serverType, tokenPath, wordpressURL]); +}; diff --git a/apps/meteor/definition/externals/meteor/accounts-base.d.ts b/apps/meteor/definition/externals/meteor/accounts-base.d.ts index 29445a5a0218a..cffaa88ad8675 100644 --- a/apps/meteor/definition/externals/meteor/accounts-base.d.ts +++ b/apps/meteor/definition/externals/meteor/accounts-base.d.ts @@ -73,6 +73,8 @@ declare module 'meteor/accounts-base' { ): (credentialTokenOrError?: string | globalThis.Error | Meteor.Error | Meteor.TypedError) => void; function registerService(name: string): void; + + function serviceNames(): string[]; } } } diff --git a/apps/meteor/definition/externals/meteor/oauth.d.ts b/apps/meteor/definition/externals/meteor/oauth.d.ts index ff14ee1b85415..6592418b94677 100644 --- a/apps/meteor/definition/externals/meteor/oauth.d.ts +++ b/apps/meteor/definition/externals/meteor/oauth.d.ts @@ -2,6 +2,12 @@ declare module 'meteor/oauth' { import type { IRocketChatRecord } from '@rocket.chat/core-typings'; import type { Mongo } from 'meteor/mongo'; + // These functions may only be used on the client's Mongo.Collection + type MeteorServerMongoCollection = Omit< + Mongo.Collection, + 'remove' | 'findOne' | 'insert' | 'update' | 'upsert' + >; + interface IOauthCredentials extends IRocketChatRecord { key: string; credentialSecret: string; @@ -17,7 +23,8 @@ declare module 'meteor/oauth' { function openSecret(secret: string): string; function retrieveCredential(credentialToken: string, credentialSecret: string); function _retrieveCredentialSecret(credentialToken: string): string | null; - const _pendingCredentials: Mongo.Collection; + // luckily we don't have any reference to this collection on the client code, so let's type it according to what can be used on the server + const _pendingCredentials: MeteorServerMongoCollection; const _storageTokenPrefix: string; function launchLogin(options: { diff --git a/apps/meteor/ee/app/api-enterprise/server/canned-responses.ts b/apps/meteor/ee/app/api-enterprise/server/canned-responses.ts index 04afadb2b4bf9..afe018f9a3ff4 100644 --- a/apps/meteor/ee/app/api-enterprise/server/canned-responses.ts +++ b/apps/meteor/ee/app/api-enterprise/server/canned-responses.ts @@ -44,7 +44,7 @@ declare module '@rocket.chat/rest-typings' { API.v1.addRoute( 'canned-responses.get', - { authRequired: true, permissionsRequired: ['view-canned-responses'] }, + { authRequired: true, permissionsRequired: ['view-canned-responses'], license: ['canned-responses'] }, { async get() { return API.v1.success({ @@ -60,6 +60,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: { GET: ['view-canned-responses'], POST: ['save-canned-responses'], DELETE: ['remove-canned-responses'] }, validateParams: { POST: isPOSTCannedResponsesProps, DELETE: isDELETECannedResponsesProps, GET: isCannedResponsesProps }, + license: ['canned-responses'], }, { async get() { @@ -113,7 +114,7 @@ API.v1.addRoute( API.v1.addRoute( 'canned-responses/:_id', - { authRequired: true, permissionsRequired: ['view-canned-responses'] }, + { authRequired: true, permissionsRequired: ['view-canned-responses'], license: ['canned-responses'] }, { async get() { const { _id } = this.urlParams; diff --git a/apps/meteor/ee/app/api-enterprise/server/index.ts b/apps/meteor/ee/app/api-enterprise/server/index.ts index af62899e1f3a6..7a9a151aa1fc7 100644 --- a/apps/meteor/ee/app/api-enterprise/server/index.ts +++ b/apps/meteor/ee/app/api-enterprise/server/index.ts @@ -1,9 +1,2 @@ -import { License } from '@rocket.chat/license'; - -await License.onLicense('canned-responses', async () => { - await import('./canned-responses'); -}); - -License.onValidateLicense(async () => { - await import('./voip-freeswitch'); -}); +import './canned-responses'; +import './voip-freeswitch'; diff --git a/apps/meteor/ee/app/api-enterprise/server/middlewares/license.ts b/apps/meteor/ee/app/api-enterprise/server/middlewares/license.ts new file mode 100644 index 0000000000000..d285ab11ccdcb --- /dev/null +++ b/apps/meteor/ee/app/api-enterprise/server/middlewares/license.ts @@ -0,0 +1,32 @@ +import type { LicenseManager } from '@rocket.chat/license'; +import type { MiddlewareHandler } from 'hono'; + +import type { FailureResult, TypedOptions } from '../../../../../app/api/server/definition'; + +export const license = + (options: TypedOptions, licenseManager: LicenseManager): MiddlewareHandler => + async (c, next) => { + if (!options.license) { + return next(); + } + + const license = options.license.every((license) => licenseManager.hasModule(license)); + + const failure: FailureResult<{ + error: string; + errorType: string; + }> = { + statusCode: 400, + body: { + success: false, + error: 'This is an enterprise feature [error-action-not-allowed]', + errorType: 'error-action-not-allowed', + }, + }; + + if (!license) { + return c.json(failure.body, failure.statusCode); + } + + return next(); + }; diff --git a/apps/meteor/ee/app/api-enterprise/server/voip-freeswitch.ts b/apps/meteor/ee/app/api-enterprise/server/voip-freeswitch.ts index fdcfcc2ec6e6d..fd4f96471200a 100644 --- a/apps/meteor/ee/app/api-enterprise/server/voip-freeswitch.ts +++ b/apps/meteor/ee/app/api-enterprise/server/voip-freeswitch.ts @@ -13,7 +13,12 @@ import { settings } from '../../../../app/settings/server/cached'; API.v1.addRoute( 'voip-freeswitch.extension.list', - { authRequired: true, permissionsRequired: ['manage-voip-extensions'], validateParams: isVoipFreeSwitchExtensionListProps }, + { + authRequired: true, + permissionsRequired: ['manage-voip-extensions'], + validateParams: isVoipFreeSwitchExtensionListProps, + license: ['voip-enterprise'], + }, { async get() { if (!settings.get('VoIP_TeamCollab_Enabled')) { @@ -59,7 +64,12 @@ API.v1.addRoute( API.v1.addRoute( 'voip-freeswitch.extension.assign', - { authRequired: true, permissionsRequired: ['manage-voip-extensions'], validateParams: isVoipFreeSwitchExtensionAssignProps }, + { + authRequired: true, + permissionsRequired: ['manage-voip-extensions'], + validateParams: isVoipFreeSwitchExtensionAssignProps, + license: ['voip-enterprise'], + }, { async post() { if (!settings.get('VoIP_TeamCollab_Enabled')) { @@ -94,7 +104,12 @@ API.v1.addRoute( API.v1.addRoute( 'voip-freeswitch.extension.getDetails', - { authRequired: true, permissionsRequired: ['view-voip-extension-details'], validateParams: isVoipFreeSwitchExtensionGetDetailsProps }, + { + authRequired: true, + permissionsRequired: ['view-voip-extension-details'], + validateParams: isVoipFreeSwitchExtensionGetDetailsProps, + license: ['voip-enterprise'], + }, { async get() { if (!settings.get('VoIP_TeamCollab_Enabled')) { @@ -124,7 +139,12 @@ API.v1.addRoute( API.v1.addRoute( 'voip-freeswitch.extension.getRegistrationInfoByUserId', - { authRequired: true, permissionsRequired: ['view-user-voip-extension'], validateParams: isVoipFreeSwitchExtensionGetInfoProps }, + { + authRequired: true, + permissionsRequired: ['view-user-voip-extension'], + validateParams: isVoipFreeSwitchExtensionGetInfoProps, + license: ['voip-enterprise'], + }, { async get() { if (!settings.get('VoIP_TeamCollab_Enabled')) { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/agents.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/agents.ts index 9eceae08e3260..388a03dfcb890 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/agents.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/agents.ts @@ -14,7 +14,12 @@ import { API.v1.addRoute( 'livechat/analytics/agents/average-service-time', - { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsAgentsAverageServiceTimeProps }, + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: isLivechatAnalyticsAgentsAverageServiceTimeProps, + license: ['livechat-enterprise'], + }, { async get() { const { offset, count } = await getPaginationItems(this.queryParams); @@ -47,7 +52,12 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/agents/total-service-time', - { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsAgentsTotalServiceTimeProps }, + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: isLivechatAnalyticsAgentsTotalServiceTimeProps, + license: ['livechat-enterprise'], + }, { async get() { const { offset, count } = await getPaginationItems(this.queryParams); @@ -84,6 +94,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsAgentsAvailableForServiceHistoryProps, + license: ['livechat-enterprise'], }, { async get() { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/business-hours.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/business-hours.ts index 663c489e114a1..b7389fe87224e 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/business-hours.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/business-hours.ts @@ -21,7 +21,7 @@ declare module '@rocket.chat/rest-typings' { API.v1.addRoute( 'livechat/business-hours', - { authRequired: true, permissionsRequired: ['view-livechat-business-hours'] }, + { authRequired: true, permissionsRequired: ['view-livechat-business-hours'], license: ['livechat-enterprise'] }, { async get() { const { offset, count } = await getPaginationItems(this.queryParams); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/contacts.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/contacts.ts index 96d45de17955d..c740db2bf6872 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/contacts.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/contacts.ts @@ -43,6 +43,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['block-livechat-contact'], validateParams: isBlockContactProps, + license: ['livechat-enterprise'], }, { async post() { @@ -69,6 +70,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['unblock-livechat-contact'], validateParams: isBlockContactProps, + license: ['livechat-enterprise'], }, { async post() { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/departments.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/departments.ts index cd29d1b76c58d..7ec0930253d83 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/departments.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/departments.ts @@ -24,7 +24,12 @@ import { API.v1.addRoute( 'livechat/analytics/departments/amount-of-chats', - { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsDepartmentsAmountOfChatsProps }, + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: isLivechatAnalyticsDepartmentsAmountOfChatsProps, + license: ['livechat-enterprise'], + }, { async get() { const { offset, count } = await getPaginationItems(this.queryParams); @@ -64,6 +69,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsDepartmentsAverageServiceTimeProps, + license: ['livechat-enterprise'], }, { async get() { @@ -103,6 +109,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsDepartmentsAverageChatDurationTimeProps, + license: ['livechat-enterprise'], }, { async get() { @@ -142,6 +149,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsDepartmentsTotalServiceTimeProps, + license: ['livechat-enterprise'], }, { async get() { @@ -181,6 +189,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsDepartmentsAverageWaitingTimeProps, + license: ['livechat-enterprise'], }, { async get() { @@ -220,6 +229,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsDepartmentsTotalTransferredChatsProps, + license: ['livechat-enterprise'], }, { async get() { @@ -259,6 +269,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsDepartmentsTotalAbandonedChatsProps, + license: ['livechat-enterprise'], }, { async get() { @@ -298,6 +309,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsDepartmentsPercentageAbandonedChatsProps, + license: ['livechat-enterprise'], }, { async get() { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/inquiries.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/inquiries.ts index 667529bb10e10..6c2b995caa1f1 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/inquiries.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/inquiries.ts @@ -8,6 +8,7 @@ API.v1.addRoute( permissionsRequired: { PUT: { permissions: ['view-l-room', 'manage-livechat-sla'], operation: 'hasAny' }, }, + license: ['livechat-enterprise'], }, { async put() { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/monitors.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/monitors.ts index 62293a87ea631..5a1418799e538 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/monitors.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/monitors.ts @@ -9,6 +9,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['manage-livechat-monitors'], + license: ['livechat-enterprise'], }, { async get() { @@ -35,6 +36,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['manage-livechat-monitors'], + license: ['livechat-enterprise'], }, { async get() { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/priorities.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/priorities.ts index c3e2e84bb83d5..c12a7f84d6704 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/priorities.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/priorities.ts @@ -12,6 +12,7 @@ API.v1.addRoute( authRequired: true, validateParams: isGETLivechatPrioritiesParams, permissionsRequired: { GET: { permissions: ['manage-livechat-priorities', 'view-l-room'], operation: 'hasAny' } }, + license: ['livechat-enterprise'], }, { async get() { @@ -42,6 +43,7 @@ API.v1.addRoute( PUT: { permissions: ['manage-livechat-priorities'], operation: 'hasAny' }, }, validateParams: { PUT: isPUTLivechatPriority }, + license: ['livechat-enterprise'], }, { async get() { @@ -74,6 +76,7 @@ API.v1.addRoute( POST: { permissions: ['manage-livechat-priorities'], operation: 'hasAny' }, GET: { permissions: ['manage-livechat-priorities'], operation: 'hasAny' }, }, + license: ['livechat-enterprise'], }, { async post() { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/reports.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/reports.ts index bd36a919b35b6..31129cf80b1af 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/reports.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/reports.ts @@ -2,8 +2,6 @@ import { isGETDashboardConversationsByType } from '@rocket.chat/rest-typings'; import type { Moment } from 'moment'; import moment from 'moment'; -import { API } from '../../../../../app/api/server'; -import { restrictQuery } from '../hooks/applyRoomRestrictions'; import { findAllConversationsBySourceCached, findAllConversationsByStatusCached, @@ -11,6 +9,8 @@ import { findAllConversationsByTagsCached, findAllConversationsByAgentsCached, } from './lib/dashboards'; +import { API } from '../../../../../app/api/server'; +import { restrictQuery } from '../lib/restrictQuery'; const checkDates = (start: Moment, end: Moment) => { if (!start.isValid()) { @@ -32,7 +32,12 @@ const checkDates = (start: Moment, end: Moment) => { API.v1.addRoute( 'livechat/analytics/dashboards/conversations-by-source', - { authRequired: true, permissionsRequired: ['view-livechat-reports'], validateParams: isGETDashboardConversationsByType }, + { + authRequired: true, + permissionsRequired: ['view-livechat-reports'], + validateParams: isGETDashboardConversationsByType, + license: ['livechat-enterprise'], + }, { async get() { const { start, end } = this.queryParams; @@ -52,7 +57,12 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/dashboards/conversations-by-status', - { authRequired: true, permissionsRequired: ['view-livechat-reports'], validateParams: isGETDashboardConversationsByType }, + { + authRequired: true, + permissionsRequired: ['view-livechat-reports'], + validateParams: isGETDashboardConversationsByType, + license: ['livechat-enterprise'], + }, { async get() { const { start, end } = this.queryParams; @@ -71,7 +81,12 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/dashboards/conversations-by-department', - { authRequired: true, permissionsRequired: ['view-livechat-reports'], validateParams: isGETDashboardConversationsByType }, + { + authRequired: true, + permissionsRequired: ['view-livechat-reports'], + validateParams: isGETDashboardConversationsByType, + license: ['livechat-enterprise'], + }, { async get() { const { start, end } = this.queryParams; @@ -91,7 +106,12 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/dashboards/conversations-by-tags', - { authRequired: true, permissionsRequired: ['view-livechat-reports'], validateParams: isGETDashboardConversationsByType }, + { + authRequired: true, + permissionsRequired: ['view-livechat-reports'], + validateParams: isGETDashboardConversationsByType, + license: ['livechat-enterprise'], + }, { async get() { const { start, end } = this.queryParams; @@ -111,7 +131,12 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/dashboards/conversations-by-agent', - { authRequired: true, permissionsRequired: ['view-livechat-reports'], validateParams: isGETDashboardConversationsByType }, + { + authRequired: true, + permissionsRequired: ['view-livechat-reports'], + validateParams: isGETDashboardConversationsByType, + license: ['livechat-enterprise'], + }, { async get() { const { start, end } = this.queryParams; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts index 41c4d4b500f7c..53ae6c1c9275b 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts @@ -10,7 +10,12 @@ import { i18n } from '../../../../../server/lib/i18n'; API.v1.addRoute( 'livechat/room.onHold', - { authRequired: true, permissionsRequired: ['on-hold-livechat-room'], validateParams: isLivechatRoomOnHoldProps }, + { + authRequired: true, + permissionsRequired: ['on-hold-livechat-room'], + validateParams: isLivechatRoomOnHoldProps, + license: ['livechat-enterprise'], + }, { async post() { const { roomId } = this.bodyParams; @@ -43,7 +48,12 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/room.resumeOnHold', - { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isLivechatRoomResumeOnHoldProps }, + { + authRequired: true, + permissionsRequired: ['view-l-room'], + validateParams: isLivechatRoomResumeOnHoldProps, + license: ['livechat-enterprise'], + }, { async post() { const { roomId } = this.bodyParams; @@ -87,6 +97,7 @@ API.v1.addRoute( POST: { permissions: ['view-l-room'], operation: 'hasAny' }, DELETE: { permissions: ['view-l-room'], operation: 'hasAny' }, }, + license: ['livechat-enterprise'], }, { async post() { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/sla.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/sla.ts index 382518bd6e3ae..4ba7b66f1793e 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/sla.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/sla.ts @@ -18,6 +18,7 @@ API.v1.addRoute( GET: isLivechatPrioritiesProps, POST: isCreateOrUpdateLivechatSlaProps, }, + license: ['livechat-enterprise'], }, { async get() { @@ -62,6 +63,7 @@ API.v1.addRoute( validateParams: { PUT: isCreateOrUpdateLivechatSlaProps, }, + license: ['livechat-enterprise'], }, { async get() { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/tags.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/tags.ts index ab40e42aaf82f..d9506c24aa5d1 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/tags.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/tags.ts @@ -4,7 +4,11 @@ import { getPaginationItems } from '../../../../../app/api/server/helpers/getPag API.v1.addRoute( 'livechat/tags', - { authRequired: true, permissionsRequired: { GET: { permissions: ['view-l-room', 'manage-livechat-tags'], operation: 'hasAny' } } }, + { + authRequired: true, + permissionsRequired: { GET: { permissions: ['view-l-room', 'manage-livechat-tags'], operation: 'hasAny' } }, + license: ['livechat-enterprise'], + }, { async get() { const { offset, count } = await getPaginationItems(this.queryParams); @@ -30,7 +34,11 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/tags/:tagId', - { authRequired: true, permissionsRequired: { GET: { permissions: ['view-l-room', 'manage-livechat-tags'], operation: 'hasAny' } } }, + { + authRequired: true, + permissionsRequired: { GET: { permissions: ['view-l-room', 'manage-livechat-tags'], operation: 'hasAny' } }, + license: ['livechat-enterprise'], + }, { async get() { const { tagId } = this.urlParams; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/transcript.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/transcript.ts index 19c1c450f5e6e..27b1764bf665d 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/transcript.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/transcript.ts @@ -6,7 +6,7 @@ import { requestPdfTranscript } from '../lib/requestPdfTranscript'; API.v1.addRoute( 'omnichannel/:rid/request-transcript', - { authRequired: true, permissionsRequired: ['request-pdf-transcript'] }, + { authRequired: true, permissionsRequired: ['request-pdf-transcript'], license: ['livechat-enterprise'] }, { async post() { const room = await LivechatRooms.findOneById(this.urlParams.rid); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/triggers.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/triggers.ts index 094c02d729c05..03b72c816822b 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/triggers.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/triggers.ts @@ -14,6 +14,7 @@ API.v1.addRoute( permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatTriggerWebhookTestParams, rateLimiterOptions: { numRequestsAllowed: 15, intervalTimeInMS: 60000 }, + license: ['livechat-enterprise'], }, { async post() { @@ -68,6 +69,7 @@ API.v1.addRoute( intervalTimeInMS: 60000, }, validateParams: isLivechatTriggerWebhookCallParams, + license: ['livechat-enterprise'], }, { async post() { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/units.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/units.ts index 347eeaf187616..a45ef5d14f38d 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/units.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/units.ts @@ -28,7 +28,7 @@ declare module '@rocket.chat/rest-typings' { API.v1.addRoute( 'livechat/units/:unitId/monitors', - { authRequired: true, permissionsRequired: ['manage-livechat-monitors'] }, + { authRequired: true, permissionsRequired: ['manage-livechat-monitors'], license: ['livechat-enterprise'] }, { async get() { const { unitId } = this.urlParams; @@ -47,7 +47,7 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/units', - { authRequired: true, permissionsRequired: { POST: ['manage-livechat-units'], GET: [] } }, + { authRequired: true, permissionsRequired: { POST: ['manage-livechat-units'], GET: [] }, license: ['livechat-enterprise'] }, { async get() { const params = this.queryParams; @@ -79,7 +79,7 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/units/:id', - { authRequired: true, permissionsRequired: ['manage-livechat-units'] }, + { authRequired: true, permissionsRequired: ['manage-livechat-units'], license: ['livechat-enterprise'] }, { async get() { const { id } = this.urlParams; @@ -105,7 +105,7 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/units/:unitId/departments', - { authRequired: true, permissionsRequired: ['manage-livechat-units'] }, + { authRequired: true, permissionsRequired: ['manage-livechat-units'], license: ['livechat-enterprise'] }, { async get() { const { offset, count } = await getPaginationItems(this.queryParams); @@ -125,7 +125,7 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/units/:unitId/departments/available', - { authRequired: true, permissionsRequired: ['manage-livechat-units'] }, + { authRequired: true, permissionsRequired: ['manage-livechat-units'], license: ['livechat-enterprise'] }, { async get() { const { offset, count } = await getPaginationItems(this.queryParams); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts index 00f67d0d8fd00..45f19dc2eda77 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts @@ -157,6 +157,7 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior if (!department || !agentsId.length) { return options; } + return this.handleRemoveAgentsFromDepartments(department, agentsId, options); } @@ -325,7 +326,7 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior const [agentsWithDepartment, [agentsOfDepartment] = []] = await Promise.all([ LivechatDepartmentAgents.findByAgentIds(agentsIds, { projection: { agentId: 1 } }).toArray(), - LivechatDepartment.findAgentsByBusinessHourId(department.businessHourId).toArray(), + ...[department?.businessHourId ? LivechatDepartment.findAgentsByBusinessHourId(department.businessHourId).toArray() : []], ]); for (const agentId of agentsIds) { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts index 8e1ceb6581083..95b310f3a3f3a 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts @@ -1,42 +1,8 @@ import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; -import { LivechatDepartment } from '@rocket.chat/models'; import type { FilterOperators } from 'mongodb'; import { callbacks } from '../../../../../lib/callbacks'; -import { cbLogger } from '../lib/logger'; -import { getUnitsFromUser } from '../lib/units'; - -export const restrictQuery = async (originalQuery: FilterOperators = {}, unitsFilter?: string[]) => { - const query = { ...originalQuery }; - - let userUnits = await getUnitsFromUser(); - if (!Array.isArray(userUnits)) { - if (Array.isArray(unitsFilter) && unitsFilter.length) { - return { ...query, departmentAncestors: { $in: unitsFilter } }; - } - return query; - } - - if (Array.isArray(unitsFilter) && unitsFilter.length) { - const userUnit = new Set([...userUnits]); - const filteredUnits = new Set(unitsFilter); - - // IF user is trying to filter by a unit he doens't have access to, apply empty filter (no matches) - userUnits = [...userUnit.intersection(filteredUnits)]; - } - // TODO: units is meant to include units and departments, however, here were only using them as units - // We have to change the filter to something like { $or: [{ ancestors: {$in: units }}, {_id: {$in: units}}] } - const departments = await LivechatDepartment.find({ ancestors: { $in: userUnits } }, { projection: { _id: 1 } }).toArray(); - - const expressions = query.$and || []; - const condition = { - $or: [{ departmentAncestors: { $in: userUnits } }, { departmentId: { $in: departments.map(({ _id }) => _id) } }], - }; - query.$and = [condition, ...expressions]; - - cbLogger.debug({ msg: 'Applying room query restrictions', userUnits }); - return query; -}; +import { restrictQuery } from '../lib/restrictQuery'; callbacks.add( 'livechat.applyRoomRestrictions', diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applySimultaneousChatsRestrictions.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applySimultaneousChatsRestrictions.ts index beca9eb385fbb..2de13028e12e4 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applySimultaneousChatsRestrictions.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applySimultaneousChatsRestrictions.ts @@ -1,53 +1,43 @@ -import type { ILivechatDepartment } from '@rocket.chat/core-typings'; +import type { ILivechatDepartment, AvailableAgentsAggregation } from '@rocket.chat/core-typings'; import { LivechatDepartment } from '@rocket.chat/models'; +import type { Filter } from 'mongodb'; import { settings } from '../../../../../app/settings/server'; import { callbacks } from '../../../../../lib/callbacks'; -callbacks.add( - 'livechat.applySimultaneousChatRestrictions', - async (_: any, { departmentId }: { departmentId?: string } = {}) => { - if (departmentId) { - const departmentLimit = - ( - await LivechatDepartment.findOneById>(departmentId, { - projection: { maxNumberSimultaneousChat: 1 }, - }) - )?.maxNumberSimultaneousChat || 0; - if (departmentLimit > 0) { - return { $match: { 'queueInfo.chats': { $gte: Number(departmentLimit) } } }; - } +export async function getChatLimitsQuery(departmentId?: string): Promise> { + const limitFilter: Filter = []; + + if (departmentId) { + const departmentLimit = + ( + await LivechatDepartment.findOneById>(departmentId, { + projection: { maxNumberSimultaneousChat: 1 }, + }) + )?.maxNumberSimultaneousChat || 0; + if (departmentLimit > 0) { + limitFilter.push({ 'queueInfo.chatsForDepartment': { $gte: Number(departmentLimit) } }); } + } + + limitFilter.push({ + $and: [{ maxChatsForAgent: { $gt: 0 } }, { $expr: { $gte: ['$queueInfo.chats', '$maxChatsForAgent'] } }], + }); - const maxChatsPerSetting = settings.get('Livechat_maximum_chats_per_agent') as number; - const agentFilter = { - $and: [ - { 'livechat.maxNumberSimultaneousChat': { $gt: 0 } }, - { $expr: { $gte: ['queueInfo.chats', 'livechat.maxNumberSimultaneousChat'] } }, - ], - }; - // apply filter only if agent setting is 0 or is disabled - const globalFilter = - maxChatsPerSetting > 0 - ? { - $and: [ - { - $or: [ - { - 'livechat.maxNumberSimultaneousChat': { $exists: false }, - }, - { 'livechat.maxNumberSimultaneousChat': 0 }, - { 'livechat.maxNumberSimultaneousChat': '' }, - { 'livechat.maxNumberSimultaneousChat': null }, - ], - }, - { 'queueInfo.chats': { $gte: maxChatsPerSetting } }, - ], - } - : // dummy filter meaning: don't match anything - { _id: '' }; + const maxChatsPerSetting = settings.get('Livechat_maximum_chats_per_agent'); + if (maxChatsPerSetting > 0) { + limitFilter.push({ + $and: [{ maxChatsForAgent: { $eq: 0 } }, { 'queueInfo.chats': { $gte: maxChatsPerSetting } }], + }); + } - return { $match: { $or: [agentFilter, globalFilter] } }; + return { $match: { $or: limitFilter } }; +} + +callbacks.add( + 'livechat.applySimultaneousChatRestrictions', + async (_: any, { departmentId }: { departmentId?: string } = {}) => { + return getChatLimitsQuery(departmentId); }, callbacks.priority.HIGH, 'livechat-apply-simultaneous-restrictions', diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeJoinRoom.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeJoinRoom.ts index c99af4b0a36d0..b17f0281d5c00 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeJoinRoom.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeJoinRoom.ts @@ -1,11 +1,10 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import type { IUser, IRoom } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; import { settings } from '../../../../../app/settings/server'; import { callbacks } from '../../../../../lib/callbacks'; -import { getMaxNumberSimultaneousChat } from '../lib/Helper'; +import { isAgentWithinChatLimits } from '../lib/Helper'; callbacks.add( 'beforeJoinRoom', @@ -17,28 +16,18 @@ callbacks.add( if (!room || !isOmnichannelRoom(room)) { return user; } - const { departmentId } = room; - const maxNumberSimultaneousChat = await getMaxNumberSimultaneousChat({ - agentId: user._id, - departmentId, - }); - - if (maxNumberSimultaneousChat === 0) { - return user; - } - - const userSubs = await Users.getAgentAndAmountOngoingChats(user._id); + const userSubs = await Users.getAgentAndAmountOngoingChats(user._id, departmentId); if (!userSubs) { return user; } + const { queueInfo: { chats = 0, chatsForDepartment = 0 } = {} } = userSubs; - const { queueInfo: { chats = 0 } = {} } = userSubs; - if (maxNumberSimultaneousChat <= chats) { - throw new Meteor.Error('error-max-number-simultaneous-chats-reached', 'Not allowed'); + if (await isAgentWithinChatLimits({ agentId: user._id, departmentId, totalChats: chats, departmentChats: chatsForDepartment })) { + return user; } - return user; + throw new Error('error-max-number-simultaneous-chats-reached'); }, callbacks.priority.MEDIUM, 'livechat-before-join-room', diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.ts index 444d310b3c5f2..d2782ddee00e9 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.ts @@ -2,10 +2,10 @@ import { type ILivechatDepartment } from '@rocket.chat/core-typings'; import { LivechatDepartment, LivechatInquiry, LivechatRooms } from '@rocket.chat/models'; import { notifyOnLivechatInquiryChanged } from '../../../../../app/lib/server/lib/notifyListener'; -import { online } from '../../../../../app/livechat/server/api/lib/livechat'; import { allowAgentSkipQueue } from '../../../../../app/livechat/server/lib/Helper'; import { saveQueueInquiry } from '../../../../../app/livechat/server/lib/QueueManager'; import { setDepartmentForGuest } from '../../../../../app/livechat/server/lib/departmentsLib'; +import { online } from '../../../../../app/livechat/server/lib/service-status'; import { settings } from '../../../../../app/settings/server'; import { callbacks } from '../../../../../lib/callbacks'; import { cbLogger } from '../lib/logger'; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts index 9722cf25b2009..b0f874d4a174a 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts @@ -1,10 +1,10 @@ import { Users } from '@rocket.chat/models'; import { allowAgentSkipQueue } from '../../../../../app/livechat/server/lib/Helper'; -import { Livechat } from '../../../../../app/livechat/server/lib/LivechatTyped'; +import { checkOnlineAgents } from '../../../../../app/livechat/server/lib/service-status'; import { settings } from '../../../../../app/settings/server'; import { callbacks } from '../../../../../lib/callbacks'; -import { getMaxNumberSimultaneousChat } from '../lib/Helper'; +import { isAgentWithinChatLimits } from '../lib/Helper'; import { cbLogger } from '../lib/logger'; const validateMaxChats = async ({ @@ -32,7 +32,7 @@ const validateMaxChats = async ({ } const { agentId } = agent; - if (!(await Livechat.checkOnlineAgents(undefined, agent))) { + if (!(await checkOnlineAgents(undefined, agent))) { cbLogger.debug('Provided agent is not online'); throw new Error('Provided agent is not online'); } @@ -48,34 +48,18 @@ const validateMaxChats = async ({ } const { department: departmentId } = inquiry; - - const maxNumberSimultaneousChat = await getMaxNumberSimultaneousChat({ - agentId, - departmentId, - }); - - if (maxNumberSimultaneousChat === 0) { - cbLogger.debug(`Chat can be taken by Agent ${agentId}: max number simultaneous chats on range`); - return agent; - } - - const user = await Users.getAgentAndAmountOngoingChats(agentId); + const user = await Users.getAgentAndAmountOngoingChats(agentId, departmentId); if (!user) { cbLogger.debug({ msg: 'No valid agent found', agentId }); throw new Error('No valid agent found'); } - const { queueInfo: { chats = 0 } = {} } = user; - const maxChats = typeof maxNumberSimultaneousChat === 'number' ? maxNumberSimultaneousChat : parseInt(maxNumberSimultaneousChat, 10); + const { queueInfo: { chats = 0, chatsForDepartment = 0 } = {} } = user; - cbLogger.debug({ msg: 'Validating agent is within max number of chats', agentId, user, maxChats }); - if (maxChats <= chats) { - await callbacks.run('livechat.onMaxNumberSimultaneousChatsReached', inquiry); - throw new Error('error-max-number-simultaneous-chats-reached'); + if (await isAgentWithinChatLimits({ agentId, departmentId, totalChats: chats, departmentChats: chatsForDepartment })) { + return user; } - - cbLogger.debug(`Agent ${agentId} can take inquiry ${inquiry._id}`); - return agent; + throw new Error('error-max-number-simultaneous-chats-reached'); }; callbacks.add('livechat.checkAgentBeforeTakeInquiry', validateMaxChats, callbacks.priority.MEDIUM, 'livechat-before-take-inquiry'); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/index.ts b/apps/meteor/ee/app/livechat-enterprise/server/index.ts index 13676e7cbedbf..bde44d7e26f4c 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/index.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/index.ts @@ -27,10 +27,10 @@ import './lib/routing/LoadBalancing'; import './lib/routing/LoadRotation'; import './lib/AutoCloseOnHoldScheduler'; import './business-hour'; +import './api'; import { createDefaultPriorities } from './priorities'; await License.onLicense('livechat-enterprise', async () => { - require('./api'); require('./hooks'); await import('./startup'); const { createPermissions } = await import('./permissions'); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts index b58aee89f5401..aedc1a77ac3bc 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts @@ -7,8 +7,8 @@ import { MongoInternals } from 'meteor/mongo'; import { schedulerLogger } from './logger'; import { forwardRoomToAgent } from '../../../../../app/livechat/server/lib/Helper'; -import { Livechat as LivechatTyped } from '../../../../../app/livechat/server/lib/LivechatTyped'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; +import { returnRoomAsInquiry } from '../../../../../app/livechat/server/lib/rooms'; import { settings } from '../../../../../app/settings/server'; const SCHEDULER_NAME = 'omnichannel_scheduler'; @@ -91,7 +91,7 @@ export class AutoTransferChatSchedulerClass { if (!RoutingManager.getConfig()?.autoAssignAgent) { this.logger.debug(`Auto-assign agent is disabled, returning room ${roomId} as inquiry`); - await LivechatTyped.returnRoomAsInquiry(room, departmentId, { + await returnRoomAsInquiry(room, departmentId, { scope: 'autoTransferUnansweredChatsToQueue', comment: timeoutDuration, transferredBy: await this.getSchedulerUser(), diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/Helper.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/Helper.ts index ec2c9d40d689f..0fab57ac1ed3e 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/Helper.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/Helper.ts @@ -1,5 +1,10 @@ import { api } from '@rocket.chat/core-services'; -import type { IOmnichannelRoom, IOmnichannelServiceLevelAgreements, InquiryWithAgentInfo } from '@rocket.chat/core-typings'; +import type { + ILivechatDepartment, + IOmnichannelRoom, + IOmnichannelServiceLevelAgreements, + InquiryWithAgentInfo, +} from '@rocket.chat/core-typings'; import type { Updater } from '@rocket.chat/models'; import { Rooms as RoomRaw, @@ -33,24 +38,118 @@ type QueueInfo = { numberMostRecentChats: number; }; -export const getMaxNumberSimultaneousChat = async ({ agentId, departmentId }: { agentId?: string; departmentId?: string }) => { - if (departmentId) { - const department = await LivechatDepartmentRaw.findOneById(departmentId); - const { maxNumberSimultaneousChat = 0 } = department || { maxNumberSimultaneousChat: 0 }; - if (maxNumberSimultaneousChat > 0) { - return Number(maxNumberSimultaneousChat); +export const isAgentWithinChatLimits = async ({ + agentId, + departmentId, + totalChats, + departmentChats, +}: { + agentId: string; + departmentId?: string; + totalChats: number; + departmentChats: number; +}): Promise => { + let agentLimit = 0; + let globalLimit = 0; + + const user = await Users.getAgentInfo(agentId, settings.get('Livechat_show_agent_info')); + const rawAgentLimit = user?.livechat?.maxNumberSimultaneousChat; + + if (rawAgentLimit !== undefined && rawAgentLimit !== null) { + const numericAgentLimit = Number(rawAgentLimit); + if (numericAgentLimit > 0) { + agentLimit = numericAgentLimit; + } + } + + if (agentLimit === 0) { + const settingLimit = settings.get('Livechat_maximum_chats_per_agent'); + if (settingLimit > 0) { + globalLimit = settingLimit; } } - if (agentId) { - const user = await Users.getAgentInfo(agentId, settings.get('Livechat_show_agent_info')); - const { livechat: { maxNumberSimultaneousChat = 0 } = {} } = user || {}; - if (maxNumberSimultaneousChat > 0) { - return Number(maxNumberSimultaneousChat); + if (departmentId) { + const department = await LivechatDepartmentRaw.findOneById>(departmentId, { + projection: { maxNumberSimultaneousChat: 1 }, + }); + let departmentLimit = 0; + + if (department?.maxNumberSimultaneousChat !== undefined && department.maxNumberSimultaneousChat !== null) { + const numericDeptLimit = Number(department.maxNumberSimultaneousChat); + if (numericDeptLimit > 0) { + departmentLimit = numericDeptLimit; + } } + + if (departmentLimit > 0) { + if (agentLimit > 0) { + logger.debug({ + msg: 'Applying chat limits', + departmentId, + agentId, + limits: ['department', 'agent'], + totalChats, + departmentChats, + agentLimit, + departmentLimit, + }); + return departmentChats < departmentLimit && totalChats < agentLimit; + } + + if (globalLimit > 0) { + logger.debug({ + msg: 'Applying chat limits', + departmentId, + agentId, + limits: ['department', 'global'], + totalChats, + departmentChats, + globalLimit, + departmentLimit, + }); + return departmentChats < departmentLimit && totalChats < globalLimit; + } + + logger.debug({ + msg: 'Applying chat limits', + departmentId, + agentId, + limits: ['department'], + totalChats, + departmentChats, + departmentLimit, + }); + return departmentChats < departmentLimit; + } + } + + if (agentLimit > 0) { + logger.debug({ + msg: 'Applying chat limits', + departmentId, + agentId, + limits: ['agent'], + totalChats, + agentLimit, + }); + return totalChats < agentLimit; + } + + if (globalLimit > 0) { + logger.debug({ + msg: 'Applying chat limits', + departmentId, + agentId, + limits: ['global'], + totalChats, + globalLimit, + }); + return totalChats < globalLimit; } - return settings.get('Livechat_maximum_chats_per_agent'); + logger.debug({ msg: 'No applicable limit found for user', agentId }); + return true; }; const getWaitingQueueMessage = async (departmentId?: string) => { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/restrictQuery.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/restrictQuery.ts new file mode 100644 index 0000000000000..b777ccfb215c6 --- /dev/null +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/restrictQuery.ts @@ -0,0 +1,38 @@ +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { LivechatDepartment } from '@rocket.chat/models'; +import type { FilterOperators } from 'mongodb'; + +import { cbLogger } from './logger'; +import { getUnitsFromUser } from './units'; + +export const restrictQuery = async (originalQuery: FilterOperators = {}, unitsFilter?: string[]) => { + const query = { ...originalQuery }; + + let userUnits = await getUnitsFromUser(); + if (!Array.isArray(userUnits)) { + if (Array.isArray(unitsFilter) && unitsFilter.length) { + return { ...query, departmentAncestors: { $in: unitsFilter } }; + } + return query; + } + + if (Array.isArray(unitsFilter) && unitsFilter.length) { + const userUnit = new Set([...userUnits]); + const filteredUnits = new Set(unitsFilter); + + // IF user is trying to filter by a unit he doens't have access to, apply empty filter (no matches) + userUnits = [...userUnit.intersection(filteredUnits)]; + } + // TODO: units is meant to include units and departments, however, here were only using them as units + // We have to change the filter to something like { $or: [{ ancestors: {$in: units }}, {_id: {$in: units}}] } + const departments = await LivechatDepartment.find({ ancestors: { $in: userUnits } }, { projection: { _id: 1 } }).toArray(); + + const expressions = query.$and || []; + const condition = { + $or: [{ departmentAncestors: { $in: userUnits } }, { departmentId: { $in: departments.map(({ _id }) => _id) } }], + }; + query.$and = [condition, ...expressions]; + + cbLogger.debug({ msg: 'Applying room query restrictions', userUnits }); + return query; +}; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/routing/LoadBalancing.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/routing/LoadBalancing.ts index b909e00b27a54..ded9f383fe30f 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/routing/LoadBalancing.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/routing/LoadBalancing.ts @@ -3,6 +3,7 @@ import { Users } from '@rocket.chat/models'; import { RoutingManager } from '../../../../../../app/livechat/server/lib/RoutingManager'; import { settings } from '../../../../../../app/settings/server'; import type { IRoutingManagerConfig } from '../../../../../../definition/IRoutingManagerConfig'; +import { getChatLimitsQuery } from '../../hooks/applySimultaneousChatsRestrictions'; /* Load Balancing Queuing method: * @@ -29,10 +30,13 @@ class LoadBalancing { } async getNextAgent(department?: string, ignoreAgentId?: string) { + const extraQuery = await getChatLimitsQuery(department); + const unavailableUsers = await Users.getUnavailableAgents(department, extraQuery); const nextAgent = await Users.getNextLeastBusyAgent( department, ignoreAgentId, settings.get('Livechat_enabled_when_agent_idle'), + unavailableUsers.map((u) => u.username), ); if (!nextAgent) { return; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/routing/LoadRotation.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/routing/LoadRotation.ts index b520a1b2a2a35..dda0ac5c98861 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/routing/LoadRotation.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/routing/LoadRotation.ts @@ -4,6 +4,7 @@ import { Users } from '@rocket.chat/models'; import { RoutingManager } from '../../../../../../app/livechat/server/lib/RoutingManager'; import { settings } from '../../../../../../app/settings/server'; import type { IRoutingManagerConfig } from '../../../../../../definition/IRoutingManagerConfig'; +import { getChatLimitsQuery } from '../../hooks/applySimultaneousChatsRestrictions'; /* Load Rotation Queuing method: * Routing method where the agent with the oldest routing time is the next agent to serve incoming chats @@ -28,12 +29,15 @@ class LoadRotation { } public async getNextAgent(department?: string, ignoreAgentId?: string): Promise { + const extraQuery = await getChatLimitsQuery(department); + const unavailableUsers = await Users.getUnavailableAgents(department, extraQuery); const nextAgent = await Users.getLastAvailableAgentRouted( department, ignoreAgentId, settings.get('Livechat_enabled_when_agent_idle'), + unavailableUsers.map((user) => user.username), ); - if (!nextAgent) { + if (!nextAgent?.username) { return; } diff --git a/apps/meteor/ee/lib/misc/fetchAppsStatusFromCluster.ts b/apps/meteor/ee/lib/misc/fetchAppsStatusFromCluster.ts new file mode 100644 index 0000000000000..830971dceb3cd --- /dev/null +++ b/apps/meteor/ee/lib/misc/fetchAppsStatusFromCluster.ts @@ -0,0 +1,12 @@ +import { Apps } from '@rocket.chat/core-services'; + +import { isRunningMs } from '../../../server/lib/isRunningMs'; +import { Instance } from '../../server/sdk'; + +export async function fetchAppsStatusFromCluster() { + if (isRunningMs()) { + return Apps.getAppsStatusInNodes(); + } + + return Instance.getAppsStatusInInstances(); +} diff --git a/apps/meteor/ee/lib/misc/formatAppInstanceForRest.ts b/apps/meteor/ee/lib/misc/formatAppInstanceForRest.ts index bf096122c50fe..7819404525d5f 100644 --- a/apps/meteor/ee/lib/misc/formatAppInstanceForRest.ts +++ b/apps/meteor/ee/lib/misc/formatAppInstanceForRest.ts @@ -3,6 +3,8 @@ import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; import type { AppLicenseValidationResult } from '@rocket.chat/apps-engine/server/marketplace/license'; import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +import type { AppStatusReport } from '@rocket.chat/core-services'; +import type { App } from '@rocket.chat/core-typings'; import { getInstallationSourceFromAppStorageItem } from '../../../lib/apps/getInstallationSourceFromAppStorageItem'; @@ -12,9 +14,10 @@ interface IAppInfoRest extends IAppInfo { licenseValidation?: AppLicenseValidationResult; private: boolean; migrated: boolean; + clusterStatus?: App['clusterStatus']; } -export async function formatAppInstanceForRest(app: ProxiedApp): Promise { +export async function formatAppInstanceForRest(app: ProxiedApp, clusterStatus?: AppStatusReport): Promise { const appRest: IAppInfoRest = { ...app.getInfo(), status: await app.getStatus(), @@ -23,6 +26,10 @@ export async function formatAppInstanceForRest(app: ProxiedApp): Promise); + const { sort } = await this.parseJsonQuery(); + const _sort = { ts: sort?.ts ? sort?.ts : -1 }; + + const { cursor, totalCount } = ServerEvents.findPaginated( + { + ...(settingId && { 'data.key': 'id', 'data.value': settingId }), + ...(actor && { actor }), + ts: { + $gte: start ? new Date(start as string) : new Date(0), + $lte: end ? new Date(end as string) : new Date(), + }, + t: 'settings.changed', + }, + { + sort: _sort, + skip: offset, + limit: count, + allowDiskUse: true, + }, + ); + + const [events, total] = await Promise.all([cursor.toArray(), totalCount]); + + return API.v1.success({ + events, + count: events.length, + offset, + total, + }); + }, +); diff --git a/apps/meteor/ee/server/api/chat.ts b/apps/meteor/ee/server/api/chat.ts index e96e8ab9b1c80..568ebe8892788 100644 --- a/apps/meteor/ee/server/api/chat.ts +++ b/apps/meteor/ee/server/api/chat.ts @@ -22,7 +22,10 @@ declare module '@rocket.chat/rest-typings' { API.v1.addRoute( 'chat.getMessageReadReceipts', - { authRequired: true }, + { + authRequired: true, + // license: ['message-read-receipt'] + }, { async get() { if (!License.hasModule('message-read-receipt')) { diff --git a/apps/meteor/ee/server/api/engagementDashboard/channels.ts b/apps/meteor/ee/server/api/engagementDashboard/channels.ts index 0d2d140bd5750..ebd0924fb2d8a 100644 --- a/apps/meteor/ee/server/api/engagementDashboard/channels.ts +++ b/apps/meteor/ee/server/api/engagementDashboard/channels.ts @@ -38,6 +38,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-engagement-dashboard'], + license: ['engagement-dashboard'], }, { async get() { diff --git a/apps/meteor/ee/server/api/engagementDashboard/messages.ts b/apps/meteor/ee/server/api/engagementDashboard/messages.ts index 716aafd0f1010..d048369ca502a 100644 --- a/apps/meteor/ee/server/api/engagementDashboard/messages.ts +++ b/apps/meteor/ee/server/api/engagementDashboard/messages.ts @@ -51,6 +51,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-engagement-dashboard'], + license: ['engagement-dashboard'], }, { async get() { @@ -75,6 +76,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-engagement-dashboard'], + license: ['engagement-dashboard'], }, { async get() { @@ -99,6 +101,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-engagement-dashboard'], + license: ['engagement-dashboard'], }, { async get() { diff --git a/apps/meteor/ee/server/api/engagementDashboard/users.ts b/apps/meteor/ee/server/api/engagementDashboard/users.ts index 0302a36538520..a85148047ff26 100644 --- a/apps/meteor/ee/server/api/engagementDashboard/users.ts +++ b/apps/meteor/ee/server/api/engagementDashboard/users.ts @@ -75,6 +75,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-engagement-dashboard'], + license: ['engagement-dashboard'], }, { async get() { @@ -99,6 +100,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-engagement-dashboard'], + license: ['engagement-dashboard'], }, { async get() { @@ -123,6 +125,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-engagement-dashboard'], + license: ['engagement-dashboard'], }, { async get() { @@ -146,6 +149,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-engagement-dashboard'], + license: ['engagement-dashboard'], }, { async get() { @@ -169,6 +173,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-engagement-dashboard'], + license: ['engagement-dashboard'], }, { async get() { diff --git a/apps/meteor/ee/server/api/federation/rooms.ts b/apps/meteor/ee/server/api/federation/rooms.ts index dce9049afb375..496e4be90e664 100644 --- a/apps/meteor/ee/server/api/federation/rooms.ts +++ b/apps/meteor/ee/server/api/federation/rooms.ts @@ -14,6 +14,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isFederationSearchPublicRoomsProps, + license: ['federation'], }, { async get() { @@ -31,6 +32,7 @@ API.v1.addRoute( 'federation/listServersByUser', { authRequired: true, + license: ['federation'], }, { async get() { @@ -48,6 +50,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isFederationAddServerProps, + license: ['federation'], }, { async post() { @@ -65,6 +68,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isFederationRemoveServerProps, + license: ['federation'], }, { async post() { @@ -82,6 +86,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isFederationJoinExternalPublicRoomProps, + license: ['federation'], }, { async post() { diff --git a/apps/meteor/ee/server/api/index.ts b/apps/meteor/ee/server/api/index.ts index 96dc64c5ced41..93a4c99d8f1b8 100644 --- a/apps/meteor/ee/server/api/index.ts +++ b/apps/meteor/ee/server/api/index.ts @@ -4,3 +4,6 @@ import './licenses'; import './sessions'; import './chat'; import './roles'; +import '../apps/communication/uikit'; +import './engagementDashboard'; +import './audit'; diff --git a/apps/meteor/ee/server/api/ldap.ts b/apps/meteor/ee/server/api/ldap.ts index 12dea6d5685ee..b69a63407320c 100644 --- a/apps/meteor/ee/server/api/ldap.ts +++ b/apps/meteor/ee/server/api/ldap.ts @@ -9,6 +9,7 @@ API.v1.addRoute( authRequired: true, forceTwoFactorAuthenticationForNonEnterprise: true, twoFactorRequired: true, + // license: ['ldap-enterprise'], }, { async post() { diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index 5c686c0e532e1..30484a1301b0b 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -60,7 +60,7 @@ API.v1.addRoute( _id: this.userId, username: this.user.username!, ip: this.requestIp, - useragent: this.request.headers['user-agent'] || '', + useragent: this.request.headers.get('user-agent') || '', }); (await auditSettingOperation(Settings.updateValueById, 'Enterprise_License', license)).modifiedCount && diff --git a/apps/meteor/ee/server/api/roles.ts b/apps/meteor/ee/server/api/roles.ts index 7e8048387ec3b..5164eb0b31db1 100644 --- a/apps/meteor/ee/server/api/roles.ts +++ b/apps/meteor/ee/server/api/roles.ts @@ -93,7 +93,7 @@ declare module '@rocket.chat/rest-typings' { API.v1.addRoute( 'roles.create', - { authRequired: true }, + { authRequired: true, license: ['custom-roles'] }, { async post() { if (!License.hasModule('custom-roles')) { @@ -137,7 +137,7 @@ API.v1.addRoute( API.v1.addRoute( 'roles.update', - { authRequired: true }, + { authRequired: true, license: ['custom-roles'] }, { async post() { if (!isRoleUpdateProps(this.bodyParams)) { diff --git a/apps/meteor/ee/server/api/sessions.ts b/apps/meteor/ee/server/api/sessions.ts index f4ab45be6462a..3ab1ed31eec38 100644 --- a/apps/meteor/ee/server/api/sessions.ts +++ b/apps/meteor/ee/server/api/sessions.ts @@ -82,7 +82,7 @@ declare module '@rocket.chat/rest-typings' { API.v1.addRoute( 'sessions/list', - { authRequired: true, validateParams: isSessionsPaginateProps }, + { authRequired: true, validateParams: isSessionsPaginateProps, license: ['device-management'] }, { async get() { if (!License.hasModule('device-management')) { @@ -105,7 +105,7 @@ API.v1.addRoute( API.v1.addRoute( 'sessions/info', - { authRequired: true, validateParams: isSessionsProps }, + { authRequired: true, validateParams: isSessionsProps, license: ['device-management'] }, { async get() { if (!License.hasModule('device-management')) { @@ -124,7 +124,7 @@ API.v1.addRoute( API.v1.addRoute( 'sessions/logout.me', - { authRequired: true, validateParams: isSessionsProps }, + { authRequired: true, validateParams: isSessionsProps, license: ['device-management'] }, { async post() { if (!License.hasModule('device-management')) { @@ -150,7 +150,13 @@ API.v1.addRoute( API.v1.addRoute( 'sessions/list.all', - { authRequired: true, twoFactorRequired: true, validateParams: isSessionsPaginateProps, permissionsRequired: ['view-device-management'] }, + { + authRequired: true, + twoFactorRequired: true, + validateParams: isSessionsPaginateProps, + permissionsRequired: ['view-device-management'], + license: ['device-management'], + }, { async get() { if (!License.hasModule('device-management')) { @@ -190,7 +196,13 @@ API.v1.addRoute( API.v1.addRoute( 'sessions/info.admin', - { authRequired: true, twoFactorRequired: true, validateParams: isSessionsProps, permissionsRequired: ['view-device-management'] }, + { + authRequired: true, + twoFactorRequired: true, + validateParams: isSessionsProps, + permissionsRequired: ['view-device-management'], + license: ['device-management'], + }, { async get() { if (!License.hasModule('device-management')) { @@ -209,7 +221,13 @@ API.v1.addRoute( API.v1.addRoute( 'sessions/logout', - { authRequired: true, twoFactorRequired: true, validateParams: isSessionsProps, permissionsRequired: ['logout-device-management'] }, + { + authRequired: true, + twoFactorRequired: true, + validateParams: isSessionsProps, + permissionsRequired: ['logout-device-management'], + license: ['device-management'], + }, { async post() { if (!License.hasModule('device-management')) { diff --git a/apps/meteor/ee/server/apps/communication/rest.ts b/apps/meteor/ee/server/apps/communication/rest.ts index 32282c78d15cc..8af2aefe6be6d 100644 --- a/apps/meteor/ee/server/apps/communication/rest.ts +++ b/apps/meteor/ee/server/apps/communication/rest.ts @@ -2,13 +2,12 @@ import { AppStatus, AppStatusUtils } from '@rocket.chat/apps-engine/definition/A import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; import type { IMarketplaceInfo } from '@rocket.chat/apps-engine/server/marketplace'; +import type { AppStatusReport } from '@rocket.chat/core-services'; import type { IUser, IMessage } from '@rocket.chat/core-typings'; import { License } from '@rocket.chat/license'; import { Settings, Users } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; -import type express from 'express'; import { Meteor } from 'meteor/meteor'; -import { WebApp } from 'meteor/webapp'; import { ZodError } from 'zod'; import { actionButtonsHandler } from './endpoints/actionButtonsHandler'; @@ -24,6 +23,7 @@ import { Info } from '../../../../app/utils/rocketchat.info'; import { i18n } from '../../../../server/lib/i18n'; import { sendMessagesToAdmins } from '../../../../server/lib/sendMessagesToAdmins'; import { canEnableApp } from '../../../app/license/server/canEnableApp'; +import { fetchAppsStatusFromCluster } from '../../../lib/misc/fetchAppsStatusFromCluster'; import { formatAppInstanceForRest } from '../../../lib/misc/formatAppInstanceForRest'; import { notifyAppInstall } from '../marketplace/appInstall'; import { fetchMarketplaceApps } from '../marketplace/fetchMarketplaceApps'; @@ -55,14 +55,17 @@ export class AppsRestApi { async loadAPI() { this.api = new API.ApiClass({ - version: 'apps', - apiPath: '/api', + apiPath: '', useDefaultAuth: true, prettyJson: false, enableCors: false, + version: 'apps', }); + await this.addManagementRoutes(); - (WebApp.connectHandlers as unknown as ReturnType).use(this.api.router.router); + + // Using the same instance of the existing API for now, to be able to use the same api prefix(/api) + API.api.use(this.api.router); } addManagementRoutes() { @@ -203,7 +206,14 @@ export class AppsRestApi { { async get() { const apps = await manager.get(); - const formatted = await Promise.all(apps.map(formatAppInstanceForRest)); + let clusterStatus: AppStatusReport | undefined; + + if (this.queryParams.includeClusterStatus === 'true') { + clusterStatus = await fetchAppsStatusFromCluster(); + } + + const formatted = await Promise.all(apps.map((app) => formatAppInstanceForRest(app, clusterStatus))); + return API.v1.success({ apps: formatted }); }, }, @@ -297,7 +307,7 @@ export class AppsRestApi { apiDeprecationLogger.endpoint(this.request.route, '7.0.0', this.response, 'Use /apps/installed to get the installed apps list.'); const proxiedApps = await manager.get(); - const apps = await Promise.all(proxiedApps.map(formatAppInstanceForRest)); + const apps = await Promise.all(proxiedApps.map((app) => formatAppInstanceForRest(app))); return API.v1.success({ apps }); }, @@ -407,7 +417,12 @@ export class AppsRestApi { ?.get('users') ?.convertToApp(await Meteor.userAsync()); - const aff = await manager.add(buff, { marketplaceInfo, permissionsGranted, enable: false, user }); + const aff = await manager.add(buff, { + ...(marketplaceInfo && { marketplaceInfo }), + permissionsGranted, + enable: false, + user, + }); const info: IAppInfo & { status?: AppStatus } = aff.getAppInfo(); if (aff.hasStorageError()) { @@ -1262,12 +1277,25 @@ export class AppsRestApi { { authRequired: true, permissionsRequired: ['manage-apps'] }, { async get() { - const prl = manager.getOneById(this.urlParams.id); + const app = manager.getOneById(this.urlParams.id); - if (prl) { - return API.v1.success({ status: await prl.getStatus() }); + if (!app) { + return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`); } - return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`); + + const response: { status: AppStatus; clusterStatus?: AppStatusReport[string] } = { status: await app.getStatus() }; + + try { + const clusterStatus = await fetchAppsStatusFromCluster(); + + if (clusterStatus?.[app.getID()]) { + response.clusterStatus = clusterStatus[app.getID()]; + } + } catch (e) { + orchestrator.getRocketChatLogger().warn('App status endpoint: could not fetch status across cluster', e); + } + + return API.v1.success(response); }, async post() { const { id: appId } = this.urlParams; diff --git a/apps/meteor/ee/server/apps/communication/uikit.ts b/apps/meteor/ee/server/apps/communication/uikit.ts index 0392076704d72..7d490406d007f 100644 --- a/apps/meteor/ee/server/apps/communication/uikit.ts +++ b/apps/meteor/ee/server/apps/communication/uikit.ts @@ -1,6 +1,7 @@ import type { UiKitCoreAppPayload } from '@rocket.chat/core-services'; import { UiKitCoreApp } from '@rocket.chat/core-services'; import type { OperationParams, UrlParams } from '@rocket.chat/rest-typings'; +import bodyParser from 'body-parser'; import cors from 'cors'; import type { Request, Response } from 'express'; import express from 'express'; @@ -33,7 +34,7 @@ settings.watch('API_CORS_Origin', (value: string) => { : []; }); -WebApp.connectHandlers.use(apiServer); +WebApp.rawConnectHandlers.use(apiServer); // eslint-disable-next-line new-cap const router = express.Router(); @@ -89,7 +90,7 @@ const corsOptions: cors.CorsOptions = { }, }; -apiServer.use('/api/apps/ui.interaction/', cors(corsOptions), router); // didn't have the rateLimiter option +apiServer.use('/api/apps/ui.interaction/', bodyParser.json(), cors(corsOptions), router); // didn't have the rateLimiter option type UiKitUserInteractionRequest = Request< UrlParams<'/apps/ui.interaction/:id'>, diff --git a/apps/meteor/ee/server/apps/startup.ts b/apps/meteor/ee/server/apps/startup.ts index 683e40dbb6b1e..2b24859ff122f 100644 --- a/apps/meteor/ee/server/apps/startup.ts +++ b/apps/meteor/ee/server/apps/startup.ts @@ -24,7 +24,7 @@ export const startupApp = async function startupApp() { }, ], public: true, - hidden: false, + hidden: true, alert: 'Apps_Logs_TTL_Alert', }); @@ -69,33 +69,6 @@ export const startupApp = async function startupApp() { // Disable apps that depend on add-ons (external modules) if they are invalidated License.onModule(disableAppsWithAddonsCallback); - settings.watch('Apps_Logs_TTL', async (value) => { - // TODO: remove this feature, initialized is always false first time - if (!Apps.isInitialized()) { - return; - } - let expireAfterSeconds = 0; - - switch (value) { - case '7_days': - expireAfterSeconds = 604800; - break; - case '14_days': - expireAfterSeconds = 1209600; - break; - case '30_days': - expireAfterSeconds = 2592000; - break; - } - - if (!expireAfterSeconds) { - return; - } - - const model = Apps._logModel; - await model?.resetTTLIndex(expireAfterSeconds); - }); - Apps.initialize(); void Apps.load(); diff --git a/apps/meteor/ee/server/index.ts b/apps/meteor/ee/server/index.ts index 1671c15987a06..c9340d1f440a4 100644 --- a/apps/meteor/ee/server/index.ts +++ b/apps/meteor/ee/server/index.ts @@ -6,8 +6,8 @@ import '../app/canned-responses/server/index'; import '../app/livechat-enterprise/server/index'; import '../app/message-read-receipt/server/index'; import '../app/voip-enterprise/server/index'; -import '../app/settings/server/index'; import './api'; +import '../app/settings/server/index'; import './requestSeatsRoute'; import './configuration/index'; import './local-services/ldap/service'; diff --git a/apps/meteor/ee/server/lib/ldap/Manager.ts b/apps/meteor/ee/server/lib/ldap/Manager.ts index 61e0ba990082b..f74e5e43ab415 100644 --- a/apps/meteor/ee/server/lib/ldap/Manager.ts +++ b/apps/meteor/ee/server/lib/ldap/Manager.ts @@ -291,6 +291,10 @@ export class LDAPEEManager extends LDAPManager { const roomOwner = settings.get('LDAP_Sync_User_Data_Channels_Admin') || ''; const user = await Users.findOneByUsernameIgnoringCase(roomOwner); + if (!user) { + logger.error(`Unable to find user '${roomOwner}' to be the owner of the channel '${channel}'.`); + return; + } const room = await createRoom('c', channel, user, [], false, false, { customFields: { ldap: true }, diff --git a/apps/meteor/ee/server/local-services/federation/service.ts b/apps/meteor/ee/server/local-services/federation/service.ts index 0fc952d2b6f20..9e6b331874f1c 100644 --- a/apps/meteor/ee/server/local-services/federation/service.ts +++ b/apps/meteor/ee/server/local-services/federation/service.ts @@ -124,7 +124,6 @@ abstract class AbstractBaseFederationServiceEE extends AbstractFederationService this.bridge.logFederationStartupInfo('Running Federation Enterprise V2'); FederationFactoryEE.removeCEValidators(); await import('./infrastructure/rocket-chat/slash-commands'); - await import('../../api/federation'); } private async stopFederation(): Promise { diff --git a/apps/meteor/ee/server/local-services/instance/service.ts b/apps/meteor/ee/server/local-services/instance/service.ts index 93d6d0c45e980..0c6dfa8f1695d 100644 --- a/apps/meteor/ee/server/local-services/instance/service.ts +++ b/apps/meteor/ee/server/local-services/instance/service.ts @@ -1,6 +1,7 @@ import os from 'os'; -import { License, ServiceClassInternal } from '@rocket.chat/core-services'; +import type { AppStatusReport } from '@rocket.chat/core-services'; +import { Apps, License, ServiceClassInternal } from '@rocket.chat/core-services'; import { InstanceStatus, defaultPingInterval, indexExpire } from '@rocket.chat/instance-status'; import { InstanceStatus as InstanceStatusRaw } from '@rocket.chat/models'; import EJSON from 'ejson'; @@ -117,6 +118,11 @@ export class InstanceService extends ServiceClassInternal implements IInstanceSe } }, }, + actions: { + getAppsStatus(_ctx) { + return Apps.getAppsStatusLocal(); + }, + }, }); } @@ -176,4 +182,45 @@ export class InstanceService extends ServiceClassInternal implements IInstanceSe async getInstances(): Promise { return this.broker.call('$node.list', { onlyAvailable: true }); } + + async getAppsStatusInInstances(): Promise { + const instances = await this.getInstances(); + + const control: Promise[] = []; + const statusByApp: AppStatusReport = {}; + + instances.forEach((instance) => { + if (instance.local) { + return; + } + + const { id: instanceId } = instance; + + control.push( + (async () => { + const appsStatus = await this.broker.call>, null>( + 'matrix.getAppsStatus', + null, + { nodeID: instanceId }, + ); + + if (!appsStatus) { + throw new Error(`Failed to get apps status from instance ${instanceId}`); + } + + appsStatus.forEach(({ status, appId }) => { + if (!statusByApp[appId]) { + statusByApp[appId] = []; + } + + statusByApp[appId].push({ instanceId, status }); + }); + })(), + ); + }); + + await Promise.all(control); + + return statusByApp; + } } diff --git a/apps/meteor/ee/server/models/raw/CannedResponse.ts b/apps/meteor/ee/server/models/raw/CannedResponse.ts index fdfb4a02d97b5..2bae8abd5dd70 100644 --- a/apps/meteor/ee/server/models/raw/CannedResponse.ts +++ b/apps/meteor/ee/server/models/raw/CannedResponse.ts @@ -113,6 +113,6 @@ export class CannedResponseRaw extends BaseRaw imple }, }; - return this.updateMany({}, update); + return this.updateMany({ tags: tagId }, update); } } diff --git a/apps/meteor/ee/server/models/raw/Users.ts b/apps/meteor/ee/server/models/raw/Users.ts index 9f2ae33019ddf..d7324f7f3a08e 100644 --- a/apps/meteor/ee/server/models/raw/Users.ts +++ b/apps/meteor/ee/server/models/raw/Users.ts @@ -1,21 +1,15 @@ -import type { RocketChatRecordDeleted, IUser } from '@rocket.chat/core-typings'; +import type { RocketChatRecordDeleted, IUser, AvailableAgentsAggregation } from '@rocket.chat/core-typings'; import { UsersRaw } from '@rocket.chat/models'; -import type { Db, Collection } from 'mongodb'; +import type { Db, Collection, Filter } from 'mongodb'; import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; -type AgentMetadata = { - 'agentId'?: string; - 'username'?: string; - 'lastAssignTime'?: Date; - 'lastRoutingTime'?: Date; - 'queueInfo.chats'?: number; - [x: string]: any; -}; - declare module '@rocket.chat/model-typings' { interface IUsersModel { - getUnavailableAgents(departmentId: string, customFilter: { [k: string]: any }[]): Promise; + getUnavailableAgents( + departmentId: string, + customFilter: Filter, + ): Promise[]>; } } @@ -24,21 +18,24 @@ export class UsersEE extends UsersRaw { super(db, trash); } - // @ts-expect-error - typings are good, but JS is not helping - getUnavailableAgents(departmentId: string, customFilter: { [k: string]: any }[]): Promise { + getUnavailableAgents( + departmentId: string, + customFilter: Filter, + ): Promise[]> { // if department is provided, remove the agents that are not from the selected department const departmentFilter = departmentId ? [ { $lookup: { from: 'rocketchat_livechat_department_agents', - let: { departmentId: '$departmentId', agentId: '$agentId' }, + let: { userId: '$_id' }, pipeline: [ { - $match: { $expr: { $eq: ['$$agentId', '$_id'] } }, - }, - { - $match: { $expr: { $eq: ['$$departmentId', departmentId] } }, + $match: { + $expr: { + $and: [{ $eq: ['$$userId', '$agentId'] }, { $eq: ['$departmentId', departmentId] }], + }, + }, }, ], as: 'department', @@ -51,7 +48,7 @@ export class UsersEE extends UsersRaw { : []; return this.col - .aggregate( + .aggregate( [ { $match: { @@ -66,38 +63,37 @@ export class UsersEE extends UsersRaw { from: 'rocketchat_subscription', localField: '_id', foreignField: 'u._id', + pipeline: [{ $match: { $and: [{ t: 'l' }, { open: true }, { onHold: { $ne: true } }] } }], as: 'subs', }, }, { $project: { 'agentId': '$_id', - 'livechat.maxNumberSimultaneousChat': 1, + 'maxChatsForAgent': { $convert: { input: '$livechat.maxNumberSimultaneousChat', to: 'double', onError: 0, onNull: 0 } }, 'username': 1, - 'lastAssignTime': 1, - 'lastRoutingTime': 1, - 'queueInfo.chats': { - $size: { - $filter: { - input: '$subs', - as: 'sub', - cond: { - $and: [{ $eq: ['$$sub.t', 'l'] }, { $eq: ['$$sub.open', true] }, { $ne: ['$$sub.onHold', true] }], + ...(departmentId + ? { + 'queueInfo.chatsForDepartment': { + $size: { + $filter: { + input: '$subs', + as: 'sub', + cond: { + $and: [{ $eq: ['$$sub.department', departmentId] }], + }, + }, + }, }, - }, - }, + } + : {}), + 'queueInfo.chats': { + $size: '$subs', }, }, }, ...(customFilter ? [customFilter] : []), - { - $sort: { - 'queueInfo.chats': 1, - 'lastAssignTime': 1, - 'lastRoutingTime': 1, - 'username': 1, - }, - }, + { $project: { username: 1 } }, ], { allowDiskUse: true, readPreference: readSecondaryPreferred() }, ) diff --git a/apps/meteor/ee/server/sdk/types/IInstanceService.ts b/apps/meteor/ee/server/sdk/types/IInstanceService.ts index b5c54349dfa16..7ce7b4be285a3 100644 --- a/apps/meteor/ee/server/sdk/types/IInstanceService.ts +++ b/apps/meteor/ee/server/sdk/types/IInstanceService.ts @@ -1,5 +1,7 @@ +import type { AppStatusReport } from '@rocket.chat/core-services'; import type { BrokerNode } from 'moleculer'; export interface IInstanceService { getInstances(): Promise; + getAppsStatusInInstances(): Promise; } diff --git a/apps/meteor/ee/server/services/CHANGELOG.md b/apps/meteor/ee/server/services/CHANGELOG.md index 72cbc5f082832..e5ef569ecb3ed 100644 --- a/apps/meteor/ee/server/services/CHANGELOG.md +++ b/apps/meteor/ee/server/services/CHANGELOG.md @@ -1,11 +1,137 @@ # rocketchat-services -## 2.0.11 +## 2.0.12-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/rest-typings@7.6.0-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 + - @rocket.chat/network-broker@0.2.0-rc.8 +
      + +## 2.0.12-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/rest-typings@7.6.0-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 + - @rocket.chat/network-broker@0.2.0-rc.7 +
      + +## 2.0.12-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/rest-typings@7.6.0-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 + - @rocket.chat/network-broker@0.2.0-rc.6 +
      + +## 2.0.12-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/rest-typings@7.6.0-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 + - @rocket.chat/network-broker@0.2.0-rc.5 +
      + +## 2.0.12-rc.4 ### Patch Changes -
      Updated dependencies []: + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/rest-typings@7.6.0-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 + - @rocket.chat/network-broker@0.2.0-rc.4 +
      + +## 2.0.12-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/rest-typings@7.6.0-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 + - @rocket.chat/network-broker@0.2.0-rc.3 +
      + +## 2.0.12-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/rest-typings@7.6.0-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 + - @rocket.chat/network-broker@0.2.0-rc.2 +
      + +## 2.0.12-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/rest-typings@7.6.0-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 + - @rocket.chat/network-broker@0.2.0-rc.1 +
      + +## 2.0.12-rc.0 + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d649a761edd71e1325a635b757ef1df2e5a778a4, bbd14f84214b4785f2b58cfeb8e9117bdfbf18e8, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/apps-engine@1.51.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/network-broker@0.2.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 +
      + +## 2.0.11 + +### Patch Changes + +-
      Updated dependencies []: - @rocket.chat/core-typings@7.5.1 - @rocket.chat/rest-typings@7.5.1 - @rocket.chat/core-services@0.8.1 diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 23c654638a8ce..311dc3bc98769 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -1,7 +1,7 @@ { "name": "rocketchat-services", "private": true, - "version": "2.0.11", + "version": "2.0.12-rc.8", "description": "Rocket.Chat Authorization service", "main": "index.js", "scripts": { @@ -48,13 +48,13 @@ "ws": "^8.18.0" }, "devDependencies": { - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@types/cookie": "^0.5.4", "@types/cookie-parser": "^1.4.7", "@types/ejson": "^2.2.2", "@types/express": "^4.17.21", "@types/fibers": "^3.1.4", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "@types/ws": "^8.5.13", "npm-run-all": "^4.1.5", "pino-pretty": "^7.6.1", diff --git a/apps/meteor/ee/server/settings/voip.ts b/apps/meteor/ee/server/settings/voip.ts index e63fc1f13df26..50f6e4dbafe95 100644 --- a/apps/meteor/ee/server/settings/voip.ts +++ b/apps/meteor/ee/server/settings/voip.ts @@ -8,6 +8,8 @@ export function addSettings(): Promise { modules: ['teams-voip'], }, async function () { + const enableQuery = { _id: 'VoIP_TeamCollab_Enabled', value: true }; + await this.add('VoIP_TeamCollab_Enabled', false, { type: 'boolean', public: true, @@ -19,30 +21,49 @@ export function addSettings(): Promise { type: 'string', public: true, invalidValue: '', + enableQuery, }); await this.add('VoIP_TeamCollab_FreeSwitch_Port', 8021, { type: 'int', public: true, invalidValue: 8021, + enableQuery, }); await this.add('VoIP_TeamCollab_FreeSwitch_Password', '', { type: 'password', secret: true, invalidValue: '', + enableQuery, }); await this.add('VoIP_TeamCollab_FreeSwitch_Timeout', 3000, { type: 'int', public: true, invalidValue: 3000, + enableQuery, }); await this.add('VoIP_TeamCollab_FreeSwitch_WebSocket_Path', '', { type: 'string', public: true, invalidValue: '', + enableQuery, + }); + + await this.add('VoIP_TeamCollab_Ice_Servers', 'stun:stun.l.google.com:19302', { + type: 'string', + public: true, + invalidValue: '', + enableQuery, + }); + + await this.add('VoIP_TeamCollab_Ice_Gathering_Timeout', 5000, { + type: 'int', + public: true, + invalidValue: 5000, + enableQuery, }); }, ); diff --git a/apps/meteor/ee/server/startup/engagementDashboard.ts b/apps/meteor/ee/server/startup/engagementDashboard.ts index f7a18f1f347bf..359307365029e 100644 --- a/apps/meteor/ee/server/startup/engagementDashboard.ts +++ b/apps/meteor/ee/server/startup/engagementDashboard.ts @@ -5,7 +5,6 @@ License.onToggledFeature('engagement-dashboard', { const { prepareAnalytics, attachCallbacks } = await import('../lib/engagementDashboard/startup'); await prepareAnalytics(); attachCallbacks(); - await import('../api/engagementDashboard'); }, down: async () => { const { detachCallbacks } = await import('../lib/engagementDashboard/startup'); diff --git a/apps/meteor/ee/server/startup/index.ts b/apps/meteor/ee/server/startup/index.ts index 07fbab3961937..bb65bac500fca 100644 --- a/apps/meteor/ee/server/startup/index.ts +++ b/apps/meteor/ee/server/startup/index.ts @@ -14,7 +14,7 @@ export const registerEEBroker = async (): Promise => { const { startBroker } = await import('@rocket.chat/network-broker'); api.setBroker(startBroker()); - void api.start(); + await api.start(); } else { require('./presence'); } diff --git a/apps/meteor/ee/server/startup/services.ts b/apps/meteor/ee/server/startup/services.ts index efaf1ab8be684..c54831eb774ea 100644 --- a/apps/meteor/ee/server/startup/services.ts +++ b/apps/meteor/ee/server/startup/services.ts @@ -7,6 +7,7 @@ import { LicenseService } from '../../app/license/server/license.internalService import { OmnichannelEE } from '../../app/livechat-enterprise/server/services/omnichannel.internalService'; import { EnterpriseSettings } from '../../app/settings/server/settings.internalService'; import { FederationServiceEE } from '../local-services/federation/service'; +import '../api/federation'; import { InstanceService } from '../local-services/instance/service'; import { LDAPEEService } from '../local-services/ldap/service'; import { MessageReadsService } from '../local-services/message-reads/service'; diff --git a/apps/meteor/ee/tests/unit/apps/livechat-enterprise/lib/AutoTransferChatsScheduler.tests.ts b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/lib/AutoTransferChatsScheduler.tests.ts index fc51201c7b316..5bf2ad56a903e 100644 --- a/apps/meteor/ee/tests/unit/apps/livechat-enterprise/lib/AutoTransferChatsScheduler.tests.ts +++ b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/lib/AutoTransferChatsScheduler.tests.ts @@ -72,7 +72,7 @@ const mocks = { }, '../../../../../app/livechat/server/lib/RoutingManager': { RoutingManager: { getConfig: routingConfigMock, getNextAgent } }, '../../../../../app/livechat/server/lib/Helper': { forwardRoomToAgent }, - '../../../../../app/livechat/server/lib/LivechatTyped': { Livechat: { returnRoomAsInquiry: returnRoomAsInquiryMock } }, + '../../../../../app/livechat/server/lib/rooms': { returnRoomAsInquiry: returnRoomAsInquiryMock }, '../../../../../app/settings/server': { settings: { get: settingsGet } }, './logger': { schedulerLogger: mockLogger }, '@rocket.chat/models': { diff --git a/apps/meteor/ee/tests/unit/apps/livechat-enterprise/lib/Helper.tests.ts b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/lib/Helper.tests.ts new file mode 100644 index 0000000000000..a9c99df7f7cee --- /dev/null +++ b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/lib/Helper.tests.ts @@ -0,0 +1,111 @@ +import { expect } from 'chai'; +import { beforeEach, describe, it } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const settingGetMock = sinon.stub(); +const usersModelMock = { + getAgentInfo: sinon.stub(), +}; + +const departmentsMock = { findOneById: sinon.stub() }; + +const mocks = { + 'meteor/meteor': { Meteor: { startup: sinon.stub() } }, + './QueueInactivityMonitor': { stop: sinon.stub() }, + '../../../../../app/livechat/server/lib/settings': { getInquirySortMechanismSetting: sinon.stub() }, + '../../../../../app/livechat/lib/inquiries': { getOmniChatSortQuery: sinon.stub() }, + '../../../../../app/settings/server': { settings: { get: settingGetMock } }, + '@rocket.chat/models': { Users: usersModelMock, LivechatDepartment: departmentsMock }, +}; + +const { isAgentWithinChatLimits } = proxyquire.noCallThru().load('../../../../../app/livechat-enterprise/server/lib/Helper.ts', mocks); + +describe('isAgentWithinChatLimits', () => { + beforeEach(() => { + usersModelMock.getAgentInfo.reset(); + departmentsMock.findOneById.reset(); + settingGetMock.reset(); + }); + it('should return true if no limit is set', async () => { + const res = await isAgentWithinChatLimits({ agentId: 'kevs', totalChats: 10 }); + expect(res).to.be.true; + }); + it('should return true when agent is under the agent limit', async () => { + usersModelMock.getAgentInfo.resolves({ livechat: { maxNumberSimultaneousChat: 15 } }); + const res = await isAgentWithinChatLimits({ agentId: 'kevs', totalChats: 10 }); + expect(res).to.be.true; + }); + it('should honor agent limit over global limit', async () => { + usersModelMock.getAgentInfo.resolves({ livechat: { maxNumberSimultaneousChat: 15 } }); + settingGetMock.returns(5); + const res = await isAgentWithinChatLimits({ agentId: 'kevs', totalChats: 10 }); + expect(res).to.be.true; + }); + it('should use global limit if agent limit is not set', async () => { + usersModelMock.getAgentInfo.resolves({ livechat: { maxNumberSimultaneousChat: undefined } }); + settingGetMock.returns(5); + const res = await isAgentWithinChatLimits({ agentId: 'kevs', totalChats: 10 }); + expect(res).to.be.false; + }); + it('should consider a user with the same number of chats as the limit as over the limit', async () => { + usersModelMock.getAgentInfo.resolves({ livechat: { maxNumberSimultaneousChat: 15 } }); + settingGetMock.returns(5); + const res = await isAgentWithinChatLimits({ agentId: 'kevs', totalChats: 15 }); + expect(res).to.be.false; + }); + it('should honor both department and agent limit when departmentId is passed', async () => { + usersModelMock.getAgentInfo.resolves({ livechat: { maxNumberSimultaneousChat: 15 } }); + departmentsMock.findOneById.resolves({ maxNumberSimultaneousChat: 10 }); + settingGetMock.returns(5); + const res = await isAgentWithinChatLimits({ agentId: 'kevs', totalChats: 10, departmentId: 'dept1', departmentChats: 5 }); + expect(res).to.be.true; + }); + it('should return false for a user under their agent limit but above their department limit', async () => { + usersModelMock.getAgentInfo.resolves({ livechat: { maxNumberSimultaneousChat: 15 } }); + departmentsMock.findOneById.resolves({ maxNumberSimultaneousChat: 10 }); + settingGetMock.returns(5); + const res = await isAgentWithinChatLimits({ agentId: 'kevs', totalChats: 11, departmentId: 'dept1', departmentChats: 11 }); + expect(res).to.be.false; + }); + it('should return false for a user under their department limit but above their agent limit', async () => { + usersModelMock.getAgentInfo.resolves({ livechat: { maxNumberSimultaneousChat: 10 } }); + departmentsMock.findOneById.resolves({ maxNumberSimultaneousChat: 15 }); + settingGetMock.returns(5); + const res = await isAgentWithinChatLimits({ agentId: 'kevs', totalChats: 11, departmentId: 'dept1', departmentChats: 10 }); + expect(res).to.be.false; + }); + it('should honor both department and global when agent limit is not set', async () => { + departmentsMock.findOneById.resolves({ maxNumberSimultaneousChat: 15 }); + settingGetMock.returns(5); + const res = await isAgentWithinChatLimits({ agentId: 'kevs', totalChats: 11, departmentId: 'dept1', departmentChats: 10 }); + expect(res).to.be.false; + }); + it('should return false for a user under their global limit but above their department limit', async () => { + departmentsMock.findOneById.resolves({ maxNumberSimultaneousChat: 10 }); + settingGetMock.returns(20); + const res = await isAgentWithinChatLimits({ agentId: 'kevs', totalChats: 11, departmentId: 'dept1', departmentChats: 11 }); + expect(res).to.be.false; + }); + it('should return false for a user under their department limit but above the global limit', async () => { + departmentsMock.findOneById.resolves({ maxNumberSimultaneousChat: 10 }); + settingGetMock.returns(5); + const res = await isAgentWithinChatLimits({ agentId: 'kevs', totalChats: 11, departmentId: 'dept1', departmentChats: 3 }); + expect(res).to.be.false; + }); + it('should apply only the department limit if the other 2 limits are not set', async () => { + departmentsMock.findOneById.resolves({ maxNumberSimultaneousChat: 10 }); + const res = await isAgentWithinChatLimits({ agentId: 'kevs', totalChats: 11, departmentId: 'dept1', departmentChats: 3 }); + expect(res).to.be.true; + }); + it('should ignore agent limit if its not a valid number (or cast to number)', async () => { + usersModelMock.getAgentInfo.resolves({ livechat: { maxNumberSimultaneousChat: 'invalid' } }); + const res = await isAgentWithinChatLimits({ agentId: 'kevs', totalChats: 11 }); + expect(res).to.be.true; + }); + it('should ignore the department limit if it is not a valid number (or cast to number)', async () => { + departmentsMock.findOneById.resolves({ maxNumberSimultaneousChat: 'invalid' }); + const res = await isAgentWithinChatLimits({ agentId: 'kevs', totalChats: 11, departmentId: 'dept1', departmentChats: 11 }); + expect(res).to.be.true; + }); +}); diff --git a/apps/meteor/imports/personal-access-tokens/server/api/methods/regenerateToken.ts b/apps/meteor/imports/personal-access-tokens/server/api/methods/regenerateToken.ts index 0ee4cf018f38e..281ed55dc013c 100644 --- a/apps/meteor/imports/personal-access-tokens/server/api/methods/regenerateToken.ts +++ b/apps/meteor/imports/personal-access-tokens/server/api/methods/regenerateToken.ts @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Users } from '@rocket.chat/models'; +import { isPersonalAccessToken } from '@rocket.chat/core-typings'; import { hasPermissionAsync } from '../../../../../app/authorization/server/functions/hasPermission'; import { twoFactorRequired } from '../../../../../app/2fa/server/twoFactorRequired'; @@ -33,8 +34,14 @@ export const regeneratePersonalAccessTokenOfUser = async (tokenName: string, use await removePersonalAccessTokenOfUser(tokenName, userId); - return generatePersonalAccessTokenOfUser({ tokenName, userId, bypassTwoFactor: tokenExist.bypassTwoFactor || false }); -} + const tokenObject = tokenExist.services?.resume?.loginTokens?.find((token) => isPersonalAccessToken(token) && token.name === tokenName); + + return generatePersonalAccessTokenOfUser({ + tokenName, + userId, + bypassTwoFactor: (tokenObject && isPersonalAccessToken(tokenObject) && tokenObject.bypassTwoFactor) || false, + }); +}; Meteor.methods({ 'personalAccessTokens:regenerateToken': twoFactorRequired(async function ({ tokenName }) { @@ -44,7 +51,7 @@ Meteor.methods({ method: 'personalAccessTokens:regenerateToken', }); } - + return regeneratePersonalAccessTokenOfUser(tokenName, uid); }), }); diff --git a/apps/meteor/jest.config.ts b/apps/meteor/jest.config.ts index 5173506d9492b..c3a578ef18fe1 100644 --- a/apps/meteor/jest.config.ts +++ b/apps/meteor/jest.config.ts @@ -38,6 +38,7 @@ export default { '/ee/server/patches/**/*.spec.ts', '/app/cloud/server/functions/supportedVersionsToken/**.spec.ts', '/app/utils/lib/**.spec.ts', + '/server/lib/auditServerEvents/**.spec.ts', '/app/api/server/**.spec.ts', '/app/api/server/middlewares/**.spec.ts', ], diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 52935ec657e24..32e28f829000a 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "7.5.1", + "version": "7.6.0-rc.8", "private": true, "type": "commonjs", "author": { @@ -46,6 +46,7 @@ "test": "yarn testunit && yarn testapi", "translation-diff": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node .scripts/translation-diff.ts", "translation-check": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node .scripts/translation-check.ts", + "translation-replace-sprintf-params": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node .scripts/replaceTranslationSprintfParams.ts", "translation-fix-order": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node .scripts/translation-fix-order.ts", "version": "node .scripts/version.js", "set-version": "node .scripts/set-version.js", @@ -128,7 +129,7 @@ "@types/meteor-collection-hooks": "^0.8.9", "@types/mkdirp": "^1.0.2", "@types/mocha": "github:whitecolor/mocha-types", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "@types/node-rsa": "^1.1.4", "@types/nodemailer": "^6.4.16", "@types/oauth2-server": "^3.0.18", @@ -138,6 +139,7 @@ "@types/proxy-from-env": "^1.0.4", "@types/proxyquire": "^1.3.31", "@types/psl": "^1.1.3", + "@types/qs": "^6", "@types/react": "~18.3.17", "@types/react-dom": "~18.3.5", "@types/sanitize-html": "^2.13.0", @@ -246,7 +248,7 @@ "@rocket.chat/emitter": "~0.31.25", "@rocket.chat/favicon": "workspace:^", "@rocket.chat/freeswitch": "workspace:^", - "@rocket.chat/fuselage": "~0.61.0", + "@rocket.chat/fuselage": "~0.62.0", "@rocket.chat/fuselage-hooks": "~0.35.0", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/fuselage-toastbar": "^0.35.0", @@ -254,7 +256,7 @@ "@rocket.chat/fuselage-ui-kit": "workspace:^", "@rocket.chat/gazzodown": "workspace:^", "@rocket.chat/i18n": "workspace:^", - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@rocket.chat/instance-status": "workspace:^", "@rocket.chat/jwt": "workspace:^", "@rocket.chat/layout": "~0.32.0", @@ -266,10 +268,11 @@ "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/mongo-adapter": "workspace:^", "@rocket.chat/mp3-encoder": "^0.31.26", "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/omnichannel-services": "workspace:^", - "@rocket.chat/onboarding-ui": "~0.35.0", + "@rocket.chat/onboarding-ui": "^0.35.1", "@rocket.chat/password-policies": "workspace:^", "@rocket.chat/patch-injection": "workspace:^", "@rocket.chat/pdf-worker": "workspace:^", @@ -350,12 +353,13 @@ "he": "^1.2.0", "highlight.js": "11.8.0", "hljs9": "npm:highlight.js@^9.18.5", + "hono": "^4.6.19", "http-proxy-agent": "^7.0.2", "human-interval": "^2.0.1", "i18next-http-backend": "^1.4.5", "i18next-sprintf-postprocessor": "^0.2.2", "iconv-lite": "^0.6.3", - "image-size": "^1.1.1", + "image-size": "^1.2.1", "imap": "^0.8.19", "ip-range-check": "^0.2.0", "is-svg": "^5.1.0", @@ -404,6 +408,7 @@ "prometheus-gc-stats": "^0.6.5", "proxy-from-env": "^1.1.0", "psl": "^1.10.0", + "qs": "^6.14.0", "query-string": "^7.1.3", "queue-fifo": "^0.2.6", "re-resizable": "^6.10.1", @@ -443,7 +448,8 @@ "xml-encryption": "~3.1.0", "xml2js": "~0.6.2", "yaqrcode": "^0.2.1", - "zod": "^3.24.1" + "zod": "^3.24.1", + "zustand": "~5.0.3" }, "meteor": { "mainModule": { diff --git a/apps/meteor/packages/rocketchat-coverage/.npm/package/npm-shrinkwrap.json b/apps/meteor/packages/rocketchat-coverage/.npm/package/npm-shrinkwrap.json index 526c4cbd2d5c5..a7aac7b28101d 100644 --- a/apps/meteor/packages/rocketchat-coverage/.npm/package/npm-shrinkwrap.json +++ b/apps/meteor/packages/rocketchat-coverage/.npm/package/npm-shrinkwrap.json @@ -1,5 +1,5 @@ { - "lockfileVersion": 1, + "lockfileVersion": 4, "dependencies": { "has-flag": { "version": "4.0.0", @@ -12,36 +12,29 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" }, "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==" + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==" }, "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dependencies": { - "istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==" - } - } + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==" }, "istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==" + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==" }, "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==" }, "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==" }, "supports-color": { "version": "7.2.0", diff --git a/apps/meteor/packages/rocketchat-coverage/package.js b/apps/meteor/packages/rocketchat-coverage/package.js index 55dde32fec3d5..3e1a77ece13c3 100644 --- a/apps/meteor/packages/rocketchat-coverage/package.js +++ b/apps/meteor/packages/rocketchat-coverage/package.js @@ -12,7 +12,7 @@ Package.onUse(function (api) { }); Npm.depends({ - 'istanbul-lib-report': '3.0.0', - 'istanbul-reports': '3.0.2', - 'istanbul-lib-coverage': '3.0.0', + 'istanbul-lib-report': '3.0.1', + 'istanbul-reports': '3.1.7', + 'istanbul-lib-coverage': '3.2.2', }); diff --git a/apps/meteor/packages/rocketchat-mongo-config/server/index.js b/apps/meteor/packages/rocketchat-mongo-config/server/index.js index 63343ac640143..1c72f87dfc41e 100644 --- a/apps/meteor/packages/rocketchat-mongo-config/server/index.js +++ b/apps/meteor/packages/rocketchat-mongo-config/server/index.js @@ -19,7 +19,7 @@ tls.DEFAULT_ECDH_CURVE = 'auto'; const mongoConnectionOptions = { // add retryWrites=false if not present in MONGO_URL ...(!process.env.MONGO_URL.includes('retryWrites') && { retryWrites: false }), - // ignoreUndefined: false, // TODO evaluate adding this config + ignoreUndefined: false, // TODO ideally we should call isTracingEnabled(), but since this is a Meteor package we can't :/ monitorCommands: ['yes', 'true'].includes(String(process.env.TRACING_ENABLED).toLowerCase()), diff --git a/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts b/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts index 1825442be8e78..2fc4f164f9ed5 100644 --- a/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts +++ b/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts @@ -14,9 +14,9 @@ import { stripHtml } from 'string-strip-html'; import { logger } from './logger'; import { FileUpload } from '../../../app/file-upload/server'; import { notifyOnMessageChange } from '../../../app/lib/server/lib/notifyListener'; -import { Livechat as LivechatTyped } from '../../../app/livechat/server/lib/LivechatTyped'; import { QueueManager } from '../../../app/livechat/server/lib/QueueManager'; import { setDepartmentForGuest } from '../../../app/livechat/server/lib/departmentsLib'; +import { registerGuest } from '../../../app/livechat/server/lib/guests'; import { sendMessage } from '../../../app/livechat/server/lib/messages'; import { settings } from '../../../app/settings/server'; import { i18n } from '../../lib/i18n'; @@ -42,7 +42,7 @@ async function getGuestByEmail(email: string, name: string, department = ''): Pr return guest; } - const livechatVisitor = await LivechatTyped.registerGuest({ + const livechatVisitor = await registerGuest({ token: Random.id(), name: name || email, email, diff --git a/apps/meteor/server/lib/auditServerEvents/userChanged.spec.ts b/apps/meteor/server/lib/auditServerEvents/userChanged.spec.ts new file mode 100644 index 0000000000000..4d293b61152f8 --- /dev/null +++ b/apps/meteor/server/lib/auditServerEvents/userChanged.spec.ts @@ -0,0 +1,297 @@ +import { faker } from '@faker-js/faker'; +import type { IAuditServerUserActor, IUser } from '@rocket.chat/core-typings'; +import type { Updater } from '@rocket.chat/models'; +import { UpdaterImpl } from '@rocket.chat/models'; + +import { UserChangedAuditStore } from './userChanged'; +import { createFakeUser } from '../../../tests/mocks/data'; + +const makeFakeActor = (): Omit => { + return { + ip: faker.internet.ip(), + useragent: faker.internet.userAgent(), + _id: faker.database.mongodbObjectId(), + username: faker.internet.userName(), + }; +}; + +const createUserAndUpdater = (overrides?: Partial): [IUser, Updater, Omit] => { + const originalUser = createFakeUser(overrides); + const updater = new UpdaterImpl(); + + const actor = makeFakeActor(); + + return [originalUser, updater, actor]; +}; + +const createEmailsField = (address?: string, verified = true) => { + return { + emails: [ + { + address: address || faker.internet.email(), + verified, + }, + ], + }; +}; + +jest.mock('@rocket.chat/models', () => { + return { + UpdaterImpl: jest.requireActual('@rocket.chat/models').UpdaterImpl, + ServerEvents: { + createAuditServerEvent: (...args: any) => args, + }, + }; +}); + +const createObfuscatedFields = (_2faEnabled = true): Pick => { + return { + services: { + password: { + bcrypt: faker.string.uuid(), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore this field is not in IUser, but is present in DB + enroll: { + token: faker.string.uuid(), + email: faker.internet.email(), + when: faker.date.past(), + reason: 'enroll', + }, + }, + email2fa: { + enabled: _2faEnabled, + changedAt: faker.date.past(), + }, + email: { + verificationTokens: [ + { + token: faker.string.uuid(), + address: faker.internet.email(), + when: faker.date.past(), + }, + ], + }, + resume: { + loginTokens: [ + { + when: faker.date.past(), + hashedToken: faker.string.uuid(), + twoFactorAuthorizedHash: faker.string.uuid(), + twoFactorAuthorizedUntil: faker.date.past(), + }, + ], + }, + }, + e2e: { + private_key: faker.string.uuid(), + public_key: faker.string.uuid(), + }, + inviteToken: faker.string.uuid(), + oauth: { authorizedClients: [faker.string.uuid(), faker.string.uuid()] }, + }; +}; + +const getObfuscatedFields = (email2faState: { enabled: boolean; changedAt: Date }) => ({ + e2e: '****', + oauth: '****', + inviteToken: '****', + services: getObfuscatedServices(email2faState), +}); + +const getObfuscatedServices = (email2faState: { enabled: boolean; changedAt: Date }) => { + return { + password: '****', + email2fa: email2faState, + email: '****', + resume: '****', + }; +}; + +describe('userChanged audit module', () => { + it('should build event with only name and username fields', async () => { + const [user, updater, actor] = createUserAndUpdater({ ...createEmailsField() }); + + const store = new UserChangedAuditStore(actor); + + const [newUsername, newName] = [faker.internet.userName(), faker.person.fullName()]; + + updater.set('username', newUsername); + updater.set('name', newName); + + store.setOriginalUser(user as IUser); + store.setUpdateFilter(updater.getUpdateFilter()); + + const event = await store.commitAuditEvent(); + + expect(event).toEqual([ + 'user.changed', + { + user: { _id: user._id, username: user.username }, + user_data: { username: user.username, name: user.name }, + operation: { $set: { username: newUsername, name: newName } }, + }, + { ...actor, type: 'user' }, + ]); + }); + + it('should build event with only emails field', async () => { + const [user, updater, actor] = createUserAndUpdater({ ...createEmailsField() }); + + const store = new UserChangedAuditStore(actor); + + const newEmailsField = createEmailsField(); + + updater.set('emails', newEmailsField.emails); + + store.setOriginalUser(user as IUser); + store.setUpdateFilter(updater.getUpdateFilter()); + + const event = await store.commitAuditEvent(); + + expect(event).toEqual([ + 'user.changed', + { + user: { _id: user._id, username: user.username }, + operation: { $set: { ...newEmailsField } }, + user_data: { emails: user.emails }, + }, + { ...actor, type: 'user' }, + ]); + }); + + it('should build event with every changed field', async () => { + const [user, updater, actor] = createUserAndUpdater({ ...createEmailsField(), active: false }); + + const changes = { + ...createFakeUser(), + ...createEmailsField(), + type: 'bot', + active: true, + }; + + Object.entries(changes).forEach(([key, value]: any) => { + updater.set(key, value); + }); + + updater.addToSet('roles', 'user'); + updater.addToSet('roles', 'bot'); + + const store = new UserChangedAuditStore(actor); + + store.setOriginalUser(user as IUser); + store.setUpdateFilter(updater.getUpdateFilter()); + + const event = await store.commitAuditEvent(); + + expect(event).toEqual([ + 'user.changed', + { + user: { _id: user._id, username: user.username }, + user_data: user, + operation: { + $set: { + ...changes, + }, + $addToSet: { + roles: { $each: ['user', 'bot'] }, + }, + }, + }, + { + ...actor, + type: 'user', + }, + ]); + }); + it('should obfuscate sensitive fields', async () => { + const [user, updater, actor] = createUserAndUpdater({ ...createEmailsField(), ...createObfuscatedFields(false), active: false }); + + const store = new UserChangedAuditStore(actor); + + const changes = { + ...createFakeUser(), + ...createEmailsField(), + ...createObfuscatedFields(true), + type: 'bot', + active: true, + }; + + Object.entries(changes).forEach(([key, value]: any) => { + updater.set(key, value); + }); + + updater.addToSet('roles', 'user'); + updater.addToSet('roles', 'bot'); + + store.setOriginalUser(user as IUser); + store.setUpdateFilter(updater.getUpdateFilter()); + + const event = await store.commitAuditEvent(); + + expect(event).toEqual([ + 'user.changed', + { + user: { _id: user._id, username: user.username }, + user_data: { ...user, ...getObfuscatedFields(user.services?.email2fa as any) }, + operation: { + $set: { + ...changes, + ...getObfuscatedFields(changes.services?.email2fa as any), + }, + $addToSet: { + roles: { $each: ['user', 'bot'] }, + }, + }, + }, + { + ...actor, + type: 'user', + }, + ]); + }); + it('should obfuscate nested services', async () => { + const [user, updater, actor] = createUserAndUpdater({ ...createEmailsField(), ...createObfuscatedFields(false), active: false }); + + const store = new UserChangedAuditStore(actor); + + updater.set('services.password.bcrypt', faker.string.uuid()); + updater.set('services.resume.loginTokens', faker.string.uuid()); + + store.setOriginalUser(user as IUser); + store.setUpdateFilter(updater.getUpdateFilter()); + + const event = await store.commitAuditEvent(); + + expect(event).toEqual([ + 'user.changed', + { + user: { _id: user._id, username: user.username }, + user_data: { services: { password: '****', resume: '****' } }, + operation: { $set: { 'services.password.bcrypt': '****', 'services.resume.loginTokens': '****' } }, + }, + { ...actor, type: 'user' }, + ]); + }); + it('should obfuscate all services when they are set at once', async () => { + const [user, updater, actor] = createUserAndUpdater({ ...createEmailsField(), ...createObfuscatedFields(false), active: false }); + + const store = new UserChangedAuditStore(actor); + + updater.set('services', { password: { bcrypt: faker.string.uuid() } }); + + store.setOriginalUser(user as IUser); + store.setUpdateFilter(updater.getUpdateFilter()); + + const event = await store.commitAuditEvent(); + + expect(event).toEqual([ + 'user.changed', + { + user: { _id: user._id, username: user.username }, + user_data: { services: getObfuscatedServices(user.services?.email2fa as any) }, + operation: { $set: { services: { password: '****' } } }, + }, + { ...actor, type: 'user' }, + ]); + }); +}); diff --git a/apps/meteor/server/lib/auditServerEvents/userChanged.ts b/apps/meteor/server/lib/auditServerEvents/userChanged.ts new file mode 100644 index 0000000000000..ea71794952da8 --- /dev/null +++ b/apps/meteor/server/lib/auditServerEvents/userChanged.ts @@ -0,0 +1,168 @@ +import type { IAuditServerUserActor, IServerEvents, ExtractDataToParams, IUser } from '@rocket.chat/core-typings'; +import { ServerEvents } from '@rocket.chat/models'; +import type { UpdateFilter } from 'mongodb'; + +const userKeysToObfuscate = ['authorizedClients', 'e2e', 'inviteToken', 'oauth']; +const nestableKeysToObfuscate = ['services', 'password', 'bcrypt']; // ex: services.password.bcrypt + +const obfuscateServices = (services: Record): Record => { + return Object.fromEntries( + Object.keys(services).map((key) => { + // Email 2FA is okay, only tells if it's enabled + if (key === 'email2fa') { + return [key, services[key]]; + } + return [key, '****']; + }), + ); +}; +export class UserChangedAuditStore { + private originalUser: Partial | undefined; + + private updateFilter: UpdateFilter | undefined; + + private actor: IAuditServerUserActor; + + constructor(actor: Omit, type: IAuditServerUserActor['type'] = 'user') { + this.actor = { ...actor, type }; + } + + public setOriginalUser(user: Partial) { + this.originalUser = user; + } + + public setUpdateFilter(updateFilter: UpdateFilter) { + this.updateFilter = Object.fromEntries( + Object.entries(updateFilter).map(([key, value]) => { + const obfuscatedValue = Object.entries(value).reduce((acc, [k, v]) => { + if (userKeysToObfuscate.includes(k)) { + return { + ...acc, + [k]: '****', + }; + } + + // In case all services are set at once, we need to obfuscate them + if (k === 'services') { + return { + ...acc, + [k]: obfuscateServices(v as Record), + }; + } + + if (nestableKeysToObfuscate.some((key) => k.includes(key))) { + return { + ...acc, + [k]: '****', + }; + } + + return { ...acc, [k]: v }; + }, {}); + + return [key, obfuscatedValue]; + }), + ); + } + + private filterUserChangedProperties(originalUser: Partial, updateFilter: UpdateFilter): Partial { + if (Object.keys(updateFilter).length === 0) { + return {}; + } + + // extract keys from updateFilter (keys are nested in $set, $unset, $inc, etc) + const updateFilterKeys: string[] = Object.values(updateFilter).reduce((acc, current) => { + const keys = Object.keys(current); + if (keys.length === 0) { + return acc; + } + return [...acc, ...keys]; + }, []); + + return Object.entries(originalUser).reduce((acc, [key, value]) => { + if (!updateFilterKeys.some((k) => k.includes(key))) { + return acc; + } + + if (userKeysToObfuscate.includes(key)) { + return { + ...acc, + [key]: '****', + }; + } + + if (key === 'services') { + // In case all services are set at once we should + // obfuscate all user services, because they'll all change + if (updateFilterKeys.some((k) => k === 'services')) { + return { + ...acc, + [key]: obfuscateServices(value as Record), + }; + } + + const changedNestedServices = updateFilterKeys + .filter((k) => k.includes(key) && k.includes('.')) + .map((serviceKey) => { + // service key can be nested with dot notation + // ex: services.password.bcrypt + const serviceKeyParts = serviceKey.split('.'); + return [serviceKeyParts[1], value[serviceKey as keyof typeof value]]; + }) + .filter(Boolean); + + if (!changedNestedServices.length) { + return acc; + } + + return { + ...acc, + [key]: obfuscateServices(Object.fromEntries(changedNestedServices) as Record), + }; + } + + return { + ...acc, + [key]: value, + }; + }, {}); + } + + private getEventData( + originalUser: Partial, + updateFilter: UpdateFilter, + ): ExtractDataToParams { + const userData = this.filterUserChangedProperties(originalUser, updateFilter); + + return { + user: { _id: originalUser._id || '', username: originalUser.username }, + user_data: userData, + operation: updateFilter, + }; + } + + private buildEvent(): ['user.changed', ExtractDataToParams, IAuditServerUserActor] { + if (!this.updateFilter) { + throw new Error('UserChangedAuditStore - Updater is undefined'); + } + + if (!this.originalUser) { + throw new Error('UserChangedAuditStore - OriginalUser is undefined'); + } + + const eventData = this.getEventData(this.originalUser, this.updateFilter); + + if (Object.keys(eventData.user_data).length === 0 || Object.keys(eventData.operation).length === 0) { + // UpdaterImpl throws an error when trying to build the filter if no changes are detected + // so we should never get here + throw new Error('UserChangedAuditStore - No changes detected'); + } + + return ['user.changed', eventData, this.actor]; + } + + public async commitAuditEvent() { + const event = this.buildEvent(); + return ServerEvents.createAuditServerEvent(...event); + } +} diff --git a/apps/meteor/server/lib/dataExport/processDataDownloads.ts b/apps/meteor/server/lib/dataExport/processDataDownloads.ts index 0075d33c67bf7..1042e74b0422c 100644 --- a/apps/meteor/server/lib/dataExport/processDataDownloads.ts +++ b/apps/meteor/server/lib/dataExport/processDataDownloads.ts @@ -71,7 +71,7 @@ const generateUserFile = async (exportOperation: IExportOperation, userData?: IU return; } - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const stream = createWriteStream(fileName, { encoding: 'utf8' }); stream.on('finish', resolve); diff --git a/apps/meteor/server/lib/findUsersOfRoom.ts b/apps/meteor/server/lib/findUsersOfRoom.ts index 6441d5265d114..25c0a34f0198a 100644 --- a/apps/meteor/server/lib/findUsersOfRoom.ts +++ b/apps/meteor/server/lib/findUsersOfRoom.ts @@ -1,13 +1,13 @@ import type { IUser } from '@rocket.chat/core-typings'; import type { FindPaginated } from '@rocket.chat/model-typings'; import { Users } from '@rocket.chat/models'; -import type { FilterOperators, FindCursor } from 'mongodb'; +import type { FindCursor, FindOptions, Filter } from 'mongodb'; import { settings } from '../../app/settings/server'; type FindUsersParam = { rid: string; - status?: FilterOperators; + status?: Filter['status']; skip?: number; limit?: number; filter?: string; @@ -15,7 +15,7 @@ type FindUsersParam = { }; export function findUsersOfRoom({ rid, status, skip = 0, limit = 0, filter = '', sort }: FindUsersParam): FindPaginated> { - const options = { + const options: FindOptions = { projection: { name: 1, username: 1, diff --git a/apps/meteor/server/lib/ldap/Connection.ts b/apps/meteor/server/lib/ldap/Connection.ts index a6e3e69f75f2a..10dc2a82a46d2 100644 --- a/apps/meteor/server/lib/ldap/Connection.ts +++ b/apps/meteor/server/lib/ldap/Connection.ts @@ -6,10 +6,12 @@ import type { ILDAPCallback, ILDAPPageCallback, } from '@rocket.chat/core-typings'; +import { wrapExceptions } from '@rocket.chat/tools'; import ldapjs from 'ldapjs'; import { logger, connLogger, searchLogger, authLogger, bindLogger, mapLogger } from './Logger'; import { getLDAPConditionalSetting } from './getLDAPConditionalSetting'; +import { processLdapVariables, type LDAPVariableMap } from './processLdapVariables'; import { settings } from '../../../app/settings/server'; import { ensureArray } from '../../../lib/utils/arrayUtils'; @@ -50,6 +52,8 @@ export class LDAPConnection { private usingAuthentication: boolean; + private _variableMap: LDAPVariableMap; + constructor() { this.ldapjs = ldapjs; @@ -83,9 +87,18 @@ export class LDAPConnection { authentication: settings.get('LDAP_Authentication') ?? false, authenticationUserDN: settings.get('LDAP_Authentication_UserDN') ?? '', authenticationPassword: settings.get('LDAP_Authentication_Password') ?? '', + useVariables: settings.get('LDAP_DataSync_UseVariables') ?? false, + variableMap: settings.get('LDAP_DataSync_VariableMap') ?? '{}', attributesToQuery: this.parseAttributeList(settings.get('LDAP_User_Search_AttributesToQuery')), }; + this._variableMap = + (this.options.useVariables && + wrapExceptions(() => JSON.parse(this.options.variableMap)).suppress(() => { + mapLogger.error({ msg: 'Failed to parse LDAP Variable Map', map: this.options.variableMap }); + })) || + {}; + if (!this.options.host) { logger.warn('LDAP Host is not configured.'); } @@ -322,7 +335,7 @@ export class LDAPConnection { mapLogger.debug({ msg: 'Extracted Attribute', key, type: dataType, value: values[key] }); }); - return values; + return processLdapVariables(values, this._variableMap); } public async doCustomSearch(baseDN: string, searchOptions: ldapjs.SearchOptions, entryCallback: ILDAPEntryCallback): Promise { @@ -359,7 +372,6 @@ export class LDAPConnection { realEntries++; } catch (e) { searchLogger.error(e); - throw e; } }); @@ -531,7 +543,6 @@ export class LDAPConnection { entries.push(result as T); } catch (e) { searchLogger.error(e); - throw e; } }); @@ -609,7 +620,6 @@ export class LDAPConnection { } } catch (e) { searchLogger.error(e); - throw e; } }); diff --git a/apps/meteor/server/lib/ldap/Manager.ts b/apps/meteor/server/lib/ldap/Manager.ts index a0f474cfe5d89..ca0b86d1a5256 100644 --- a/apps/meteor/server/lib/ldap/Manager.ts +++ b/apps/meteor/server/lib/ldap/Manager.ts @@ -12,6 +12,9 @@ import { LDAPConnection } from './Connection'; import { logger, authLogger, connLogger } from './Logger'; import { LDAPUserConverter } from './UserConverter'; import { getLDAPConditionalSetting } from './getLDAPConditionalSetting'; +import { getLdapDynamicValue } from './getLdapDynamicValue'; +import { getLdapString } from './getLdapString'; +import { ldapKeyExists } from './ldapKeyExists'; import type { UserConverterOptions } from '../../../app/importer/server/classes/converters/UserConverter'; import { setUserAvatar } from '../../../app/lib/server/functions/setUserAvatar'; import { settings } from '../../../app/settings/server'; @@ -41,6 +44,11 @@ export class LDAPManager { return this.fallbackToDefaultLogin(username, password); } + const homeServer = this.getFederationHomeServer(ldapUser); + if (homeServer) { + return this.fallbackToDefaultLogin(username, password); + } + const slugifiedUsername = this.slugifyUsername(ldapUser, username); const user = await this.findExistingUser(ldapUser, slugifiedUsername); @@ -78,6 +86,11 @@ export class LDAPManager { return; } + const homeServer = this.getFederationHomeServer(ldapUser); + if (homeServer) { + return; + } + const slugifiedUsername = this.slugifyUsername(ldapUser, username); const user = await this.findExistingUser(ldapUser, slugifiedUsername); @@ -140,12 +153,12 @@ export class LDAPManager { } // This method will only find existing users that are already linked to LDAP - protected static async findExistingLDAPUser(ldapUser: ILDAPEntry): Promise { + protected static async findExistingLDAPUser(ldapUser: ILDAPEntry): Promise { const uniqueIdentifierField = this.getLdapUserUniqueID(ldapUser); if (uniqueIdentifierField) { logger.debug({ msg: 'Querying user', uniqueId: uniqueIdentifierField.value }); - return UsersRaw.findOneByLDAPId(uniqueIdentifierField.value, uniqueIdentifierField.attribute); + return UsersRaw.findOneByLDAPId(uniqueIdentifierField.value, uniqueIdentifierField.attribute); } } @@ -165,7 +178,8 @@ export class LDAPManager { const { attribute: idAttribute, value: id } = uniqueId; const username = this.slugifyUsername(ldapUser, usedUsername || id || '') || undefined; - const emails = this.getLdapEmails(ldapUser, username).map((email) => email.trim()); + const homeServer = this.getFederationHomeServer(ldapUser); + const emails = homeServer ? [] : this.getLdapEmails(ldapUser, username).map((email) => email.trim()); const name = this.getLdapName(ldapUser) || undefined; const voipExtension = this.getLdapExtension(ldapUser); @@ -182,6 +196,10 @@ export class LDAPManager { id, }, }, + ...(homeServer && { + username: `${username}:${homeServer}`, + federated: true, + }), }; this.onMapUserData(ldapUser, userData); @@ -341,7 +359,7 @@ export class LDAPManager { ldapUser: ILDAPEntry, existingUser?: IUser, usedUsername?: string | undefined, - ): Promise { + ): Promise { logger.debug({ msg: 'Syncing user data', ldapUser: omit(ldapUser, '_raw'), @@ -401,43 +419,9 @@ export class LDAPManager { connLogger.debug(ldapUser); } - private static ldapKeyExists(ldapUser: ILDAPEntry, key: string): boolean { - return !_.isEmpty(ldapUser[key.trim()]); - } - - private static getLdapString(ldapUser: ILDAPEntry, key: string): string { - return ldapUser[key.trim()]; - } - - private static getLdapDynamicValue(ldapUser: ILDAPEntry, attributeSetting: string | undefined): string | undefined { - if (!attributeSetting) { - return; - } - - // If the attribute setting is a template, then convert the variables in it - if (attributeSetting.includes('#{')) { - return attributeSetting.replace(/#{(.+?)}/g, (_match, field) => { - const key = field.trim(); - - if (this.ldapKeyExists(ldapUser, key)) { - return this.getLdapString(ldapUser, key); - } - - return ''; - }); - } - - // If it's not a template, then treat the setting as a CSV list of possible attribute names and return the first valid one. - const attributeList: string[] = attributeSetting.replace(/\s/g, '').split(','); - const key = attributeList.find((field) => this.ldapKeyExists(ldapUser, field)); - if (key) { - return this.getLdapString(ldapUser, key); - } - } - private static getLdapName(ldapUser: ILDAPEntry): string | undefined { const nameAttributes = getLDAPConditionalSetting('LDAP_Name_Field'); - return this.getLdapDynamicValue(ldapUser, nameAttributes); + return getLdapDynamicValue(ldapUser, nameAttributes); } private static getLdapExtension(ldapUser: ILDAPEntry): string | undefined { @@ -446,14 +430,14 @@ export class LDAPManager { return; } - return this.getLdapString(ldapUser, extensionAttribute); + return getLdapString(ldapUser, extensionAttribute); } private static getLdapEmails(ldapUser: ILDAPEntry, username?: string): string[] { const emailAttributes = getLDAPConditionalSetting('LDAP_Email_Field'); if (emailAttributes) { const attributeList: string[] = emailAttributes.replace(/\s/g, '').split(','); - const key = attributeList.find((field) => this.ldapKeyExists(ldapUser, field)); + const key = attributeList.find((field) => ldapKeyExists(ldapUser, field)); const emails: string[] = [].concat(key ? ldapUser[key.trim()] : []); const filteredEmails = emails.filter((email) => email.includes('@')); @@ -497,18 +481,51 @@ export class LDAPManager { protected static getLdapUsername(ldapUser: ILDAPEntry): string | undefined { const usernameField = getLDAPConditionalSetting('LDAP_Username_Field') as string; - return this.getLdapDynamicValue(ldapUser, usernameField); + return getLdapDynamicValue(ldapUser, usernameField); + } + + protected static getFederationHomeServer(ldapUser: ILDAPEntry): string | undefined { + if (!settings.get('Federation_Matrix_enabled')) { + return; + } + + const homeServerField = settings.get('LDAP_FederationHomeServer_Field'); + const homeServer = getLdapDynamicValue(ldapUser, homeServerField); + + if (!homeServer) { + return; + } + + logger.debug({ msg: 'User has a federation home server', homeServer }); + + const localServer = settings.get('Federation_Matrix_homeserver_domain'); + if (localServer === homeServer) { + return; + } + + return homeServer; + } + + protected static getFederatedUsername(ldapUser: ILDAPEntry, requestUsername: string): string { + const username = this.slugifyUsername(ldapUser, requestUsername); + const homeServer = this.getFederationHomeServer(ldapUser); + + if (homeServer) { + return `${username}:${homeServer}`; + } + + return username; } // This method will find existing users by LDAP id or by username. - private static async findExistingUser(ldapUser: ILDAPEntry, slugifiedUsername: string): Promise { + private static async findExistingUser(ldapUser: ILDAPEntry, slugifiedUsername: string): Promise { const user = await this.findExistingLDAPUser(ldapUser); if (user) { return user; } // If we don't have that ldap user linked yet, check if there's any non-ldap user with the same username - return UsersRaw.findOneWithoutLDAPByUsernameIgnoringCase(slugifiedUsername); + return UsersRaw.findOneWithoutLDAPByUsernameIgnoringCase(slugifiedUsername); } private static fallbackToDefaultLogin(username: LoginUsername, password: string): LDAPLoginResult { diff --git a/apps/meteor/server/lib/ldap/UserConverter.ts b/apps/meteor/server/lib/ldap/UserConverter.ts index 1d94db88db3c7..bb6087a971345 100644 --- a/apps/meteor/server/lib/ldap/UserConverter.ts +++ b/apps/meteor/server/lib/ldap/UserConverter.ts @@ -1,7 +1,9 @@ import type { IImportUser, IUser } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import type { Logger } from '@rocket.chat/logger'; import { Users } from '@rocket.chat/models'; +import { logger } from './Logger'; import type { ConverterCache } from '../../../app/importer/server/classes/converters/ConverterCache'; import { type RecordConverterOptions } from '../../../app/importer/server/classes/converters/RecordConverter'; import { UserConverter, type UserConverterOptions } from '../../../app/importer/server/classes/converters/UserConverter'; @@ -20,7 +22,7 @@ export class LDAPUserConverter extends UserConverter { this.mergeExistingUsers = settings.get('LDAP_Merge_Existing_Users') ?? true; } - async findExistingUser(data: IImportUser): Promise { + async findExistingUser(data: IImportUser): Promise { if (data.services?.ldap?.id) { const importedUser = await Users.findOneByLDAPId(data.services.ldap.id, data.services.ldap.idAttribute); if (importedUser) { @@ -41,10 +43,22 @@ export class LDAPUserConverter extends UserConverter { } if (data.username) { - return Users.findOneWithoutLDAPByUsernameIgnoringCase(data.username); + return Users.findOneWithoutLDAPByUsernameIgnoringCase(data.username); } } + async insertUser(userData: IImportUser): Promise { + if (!userData.deleted) { + // #TODO: Change the LDAP sync process to split the inserts and updates into two stages so that we can validate this only once for all insertions + if (await License.shouldPreventAction('activeUsers')) { + logger.warn({ msg: 'Max users allowed reached, creating new LDAP users in inactive state ', username: userData.username }); + userData.deleted = true; + } + } + + return super.insertUser(userData); + } + static async convertSingleUser(userData: IImportUser, options?: UserConverterOptions): Promise { const converter = new LDAPUserConverter(options); await converter.addObject(userData); diff --git a/apps/meteor/server/lib/ldap/getLdapDynamicValue.spec.ts b/apps/meteor/server/lib/ldap/getLdapDynamicValue.spec.ts new file mode 100644 index 0000000000000..90b25c9f98868 --- /dev/null +++ b/apps/meteor/server/lib/ldap/getLdapDynamicValue.spec.ts @@ -0,0 +1,65 @@ +import type { ILDAPEntry } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; +import 'mocha'; + +import { getLdapDynamicValue } from './getLdapDynamicValue'; + +describe('getLdapDynamicValue', () => { + const ldapUser: ILDAPEntry = { + _raw: {}, + displayName: 'John Doe', + email: 'john.doe@example.com', + uid: 'johndoe', + emptyField: '', + }; + + it('should return undefined if attributeSetting is undefined', () => { + const result = getLdapDynamicValue(ldapUser, undefined); + expect(result).to.be.undefined; + }); + + it('should return the correct value from a single valid attribute', () => { + const result = getLdapDynamicValue(ldapUser, 'displayName'); + expect(result).to.equal('John Doe'); + }); + + it('should return the correct value from a template attribute', () => { + const result = getLdapDynamicValue(ldapUser, 'Hello, #{displayName}!'); + expect(result).to.equal('Hello, John Doe!'); + }); + + it('should replace missing keys with an empty string in a template', () => { + const result = getLdapDynamicValue(ldapUser, 'Hello, #{nonExistentField}!'); + expect(result).to.equal('Hello, !'); + }); + + it('should return the first valid key from a CSV list of attributes', () => { + const result = getLdapDynamicValue(ldapUser, 'nonExistentField,email,uid'); + expect(result).to.equal('john.doe@example.com'); + }); + + it('should return undefined if none of the keys in CSV list exist', () => { + const result = getLdapDynamicValue(ldapUser, 'nonExistentField,anotherNonExistentField'); + expect(result).to.be.undefined; + }); + + it('should handle attribute keys with surrounding whitespace correctly', () => { + const result = getLdapDynamicValue(ldapUser, ' email '); + expect(result).to.equal('john.doe@example.com'); + }); + + it('should correctly resolve multiple variables in a template', () => { + const result = getLdapDynamicValue(ldapUser, 'User: #{displayName}, Email: #{email}, UID: #{uid}'); + expect(result).to.equal('User: John Doe, Email: john.doe@example.com, UID: johndoe'); + }); + + it('should return undefined if the attribute has an empty value', () => { + const result = getLdapDynamicValue(ldapUser, 'emptyField'); + expect(result).to.be.undefined; + }); + + it('should return an empty string if using only a template attribute that has an empty value', () => { + const result = getLdapDynamicValue(ldapUser, '#{emptyField}'); + expect(result).to.be.equal(''); + }); +}); diff --git a/apps/meteor/server/lib/ldap/getLdapDynamicValue.ts b/apps/meteor/server/lib/ldap/getLdapDynamicValue.ts new file mode 100644 index 0000000000000..a0f4c2eeac189 --- /dev/null +++ b/apps/meteor/server/lib/ldap/getLdapDynamicValue.ts @@ -0,0 +1,31 @@ +import type { ILDAPEntry } from '@rocket.chat/core-typings'; + +import { getLdapString } from './getLdapString'; +import { ldapKeyExists } from './ldapKeyExists'; + +export function getLdapDynamicValue(ldapUser: ILDAPEntry, attributeSetting: string | undefined): string | undefined { + if (!attributeSetting) { + return; + } + + // If the attribute setting is a template, then convert the variables in it + if (attributeSetting.includes('#{')) { + return attributeSetting.replace(/#{(.+?)}/g, (_match, field) => { + const key = field.trim(); + + if (ldapKeyExists(ldapUser, key)) { + // We've already validated so it won't ever return undefined, but add a fallback to ensure it doesn't break if something gets changed + return getLdapString(ldapUser, key) || ''; + } + + return ''; + }); + } + + // If it's not a template, then treat the setting as a CSV list of possible attribute names and return the first valid one. + const attributeList: string[] = attributeSetting.replace(/\s/g, '').split(','); + const key = attributeList.find((field) => ldapKeyExists(ldapUser, field)); + if (key) { + return getLdapString(ldapUser, key); + } +} diff --git a/apps/meteor/server/lib/ldap/getLdapString.spec.ts b/apps/meteor/server/lib/ldap/getLdapString.spec.ts new file mode 100644 index 0000000000000..9f684a7e8c002 --- /dev/null +++ b/apps/meteor/server/lib/ldap/getLdapString.spec.ts @@ -0,0 +1,47 @@ +import type { ILDAPEntry } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; +import 'mocha'; + +import { getLdapString } from './getLdapString'; + +const ldapUser: ILDAPEntry = { + _raw: {}, + username: 'john_doe', + email: 'john.doe@example.com', + phoneNumber: '123-456-7890', + memberOf: 'group1,group2', +}; + +describe('getLdapString', () => { + it('should return the correct value for a given key', () => { + expect(getLdapString(ldapUser, 'username')).to.equal('john_doe'); + expect(getLdapString(ldapUser, 'email')).to.equal('john.doe@example.com'); + expect(getLdapString(ldapUser, 'phoneNumber')).to.equal('123-456-7890'); + expect(getLdapString(ldapUser, 'memberOf')).to.equal('group1,group2'); + }); + + it('should trim the key and return the correct value', () => { + expect(getLdapString(ldapUser, ' username ')).to.equal('john_doe'); + expect(getLdapString(ldapUser, ' email ')).to.equal('john.doe@example.com'); + }); + + it('should return undefined for non-existing keys', () => { + expect(getLdapString(ldapUser, 'nonExistingKey')).to.be.undefined; + expect(getLdapString(ldapUser, 'foo')).to.be.undefined; + }); + + it('should handle empty keys and return an empty string', () => { + expect(getLdapString(ldapUser, '')).to.be.undefined; + expect(getLdapString(ldapUser, ' ')).to.be.undefined; + }); + + it('should handle keys with only whitespace', () => { + expect(getLdapString(ldapUser, ' ')).to.be.undefined; + expect(getLdapString(ldapUser, ' ')).to.be.undefined; + }); + + it('should handle case-sensitive keys accurately', () => { + expect(getLdapString(ldapUser, 'Username')).to.be.undefined; + expect(getLdapString(ldapUser, 'EMAIL')).to.be.undefined; + }); +}); diff --git a/apps/meteor/server/lib/ldap/getLdapString.ts b/apps/meteor/server/lib/ldap/getLdapString.ts new file mode 100644 index 0000000000000..caad9d22141af --- /dev/null +++ b/apps/meteor/server/lib/ldap/getLdapString.ts @@ -0,0 +1,5 @@ +import type { ILDAPEntry } from '@rocket.chat/core-typings'; + +export function getLdapString(ldapUser: ILDAPEntry, key: string): string | undefined { + return ldapUser[key.trim()]; +} diff --git a/apps/meteor/server/lib/ldap/ldapKeyExists.spec.ts b/apps/meteor/server/lib/ldap/ldapKeyExists.spec.ts new file mode 100644 index 0000000000000..933c661e0b81b --- /dev/null +++ b/apps/meteor/server/lib/ldap/ldapKeyExists.spec.ts @@ -0,0 +1,94 @@ +import type { ILDAPEntry } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { ldapKeyExists } from './ldapKeyExists'; + +describe('ldapKeyExists', () => { + it('should return true when key exists and is not empty', () => { + const ldapUser: ILDAPEntry = { + _raw: {}, + cn: 'John Doe', + mail: 'john.doe@example.com', + }; + + expect(ldapKeyExists(ldapUser, 'cn')).to.be.true; + expect(ldapKeyExists(ldapUser, 'mail')).to.be.true; + }); + + it('should return false when key exists but is empty', () => { + const ldapUser: ILDAPEntry = { + _raw: {}, + cn: '', + mail: '', + }; + + expect(ldapKeyExists(ldapUser, 'cn')).to.be.false; + expect(ldapKeyExists(ldapUser, 'mail')).to.be.false; + }); + + it('should return false when key does not exist', () => { + const ldapUser: ILDAPEntry = { + _raw: {}, + cn: 'John Doe', + }; + expect(ldapKeyExists(ldapUser, 'mail')).to.be.false; + }); + + it('should trim the key before checking', () => { + const ldapUser: ILDAPEntry = { + _raw: {}, + cn: 'John Doe', + }; + expect(ldapKeyExists(ldapUser, ' cn ')).to.be.true; + expect(ldapKeyExists(ldapUser, ' mail ')).to.be.false; + }); + + it('should return false for empty keys', () => { + const ldapUser: ILDAPEntry = { + _raw: {}, + cn: 'John Doe', + }; + expect(ldapKeyExists(ldapUser, '')).to.be.false; + expect(ldapKeyExists(ldapUser, ' ')).to.be.false; + }); + + it('should handle keys with different casing', () => { + const ldapUser: ILDAPEntry = { + _raw: {}, + CN: 'John Doe', + }; + + expect(ldapKeyExists(ldapUser, 'CN')).to.be.true; + expect(ldapKeyExists(ldapUser, 'cn')).to.be.false; + }); + + // #TODO: We only work with strings so this doesn't matter, but why are numbers and booleans being considered "empty"? + it('should treat primitive non-string values as empty', () => { + const ldapUser: ILDAPEntry = { + _raw: {}, + numberValue: 123, + booleanValue: true, + anotherBooleanValue: false, + }; + + expect(ldapKeyExists(ldapUser, 'numberValue')).to.be.false; + expect(ldapKeyExists(ldapUser, 'booleanValue')).to.be.false; + expect(ldapKeyExists(ldapUser, 'anotherBooleanValue')).to.be.false; + }); + + it('should treat non-string values as empty', () => { + const ldapUser: ILDAPEntry = { + _raw: {}, + nullValue: null, + undefinedValue: undefined, + objectValue: {}, + arrayValue: [], + }; + + expect(ldapKeyExists(ldapUser, 'nullValue')).to.be.false; + expect(ldapKeyExists(ldapUser, 'undefinedValue')).to.be.false; + expect(ldapKeyExists(ldapUser, 'objectValue')).to.be.false; + expect(ldapKeyExists(ldapUser, 'arrayValue')).to.be.false; + }); +}); diff --git a/apps/meteor/server/lib/ldap/ldapKeyExists.ts b/apps/meteor/server/lib/ldap/ldapKeyExists.ts new file mode 100644 index 0000000000000..1b68b71bdd8ba --- /dev/null +++ b/apps/meteor/server/lib/ldap/ldapKeyExists.ts @@ -0,0 +1,6 @@ +import type { ILDAPEntry } from '@rocket.chat/core-typings'; +import _ from 'underscore'; + +export function ldapKeyExists(ldapUser: ILDAPEntry, key: string): boolean { + return !_.isEmpty(ldapUser[key.trim()]); +} diff --git a/apps/meteor/server/lib/ldap/operations/executeOperation.ts b/apps/meteor/server/lib/ldap/operations/executeOperation.ts new file mode 100644 index 0000000000000..e3eb21323598a --- /dev/null +++ b/apps/meteor/server/lib/ldap/operations/executeOperation.ts @@ -0,0 +1,31 @@ +import type { ILDAPEntry } from '@rocket.chat/core-typings'; + +import { executeFallback, type LDAPVariableFallback } from './fallback'; +import { executeMatch, type LDAPVariableMatch } from './match'; +import { executeReplace, type LDAPVariableReplace } from './replace'; +import { executeSplit, type LDAPVariableSplit } from './split'; +import { executeSubstring, type LDAPVariableSubString } from './substring'; + +export type LDAPVariableOperation = + | LDAPVariableReplace + | LDAPVariableMatch + | LDAPVariableSubString + | LDAPVariableFallback + | LDAPVariableSplit; + +export function executeOperation(ldapUser: ILDAPEntry, input: string, operation?: LDAPVariableOperation): string | undefined { + switch (operation?.operation) { + case 'replace': + return executeReplace(input, operation); + case 'match': + return executeMatch(input, operation); + case 'substring': + return executeSubstring(input, operation); + case 'fallback': + return executeFallback(ldapUser, input, operation); + case 'split': + return executeSplit(input, operation); + } + + return input; +} diff --git a/apps/meteor/server/lib/ldap/operations/fallback.spec.ts b/apps/meteor/server/lib/ldap/operations/fallback.spec.ts new file mode 100644 index 0000000000000..8cbdc58912ecd --- /dev/null +++ b/apps/meteor/server/lib/ldap/operations/fallback.spec.ts @@ -0,0 +1,61 @@ +import type { ILDAPEntry } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; +import 'mocha'; + +import type { LDAPVariableFallback } from './fallback'; +import { executeFallback } from './fallback'; + +describe('executeFallback function', () => { + const mockUser: ILDAPEntry = { + _raw: {}, + defaultFallback: 'defaultFallbackValue', + }; + + it('should return the input value when it is valid and no minLength is provided', () => { + const input = 'validInput'; + const operation: LDAPVariableFallback = { operation: 'fallback', fallback: 'defaultFallback' }; + const result = executeFallback(mockUser, input, operation); + expect(result).to.equal(input); + }); + + it('should return the input value when it is valid and meets minLength requirement', () => { + const input = 'validInput'; + const operation: LDAPVariableFallback = { operation: 'fallback', fallback: 'defaultFallback', minLength: 5 }; + const result = executeFallback(mockUser, input, operation); + expect(result).to.equal(input); + }); + + it('should return fallback when input is invalid', () => { + const input = ''; + const operation: LDAPVariableFallback = { operation: 'fallback', fallback: 'defaultFallback' }; + const result = executeFallback(mockUser, input, operation); + expect(result).to.equal('defaultFallbackValue'); + }); + + it('should return fallback when input is too short', () => { + const input = 'short'; + const operation: LDAPVariableFallback = { operation: 'fallback', fallback: 'defaultFallback', minLength: 10 }; + const result = executeFallback(mockUser, input, operation); + expect(result).to.equal('defaultFallbackValue'); + }); + + it('should return fallback when input is undefined', () => { + const operation: LDAPVariableFallback = { operation: 'fallback', fallback: 'defaultFallback' }; + const result = executeFallback(mockUser, undefined as any, operation); + expect(result).to.equal('defaultFallbackValue'); + }); + + it('should return fallback when input is an empty string and minLength is zero', () => { + const input = ''; + const operation: LDAPVariableFallback = { operation: 'fallback', fallback: 'defaultFallback', minLength: 0 }; + const result = executeFallback(mockUser, input, operation); + expect(result).to.equal('defaultFallbackValue'); + }); + + it('should return fallback when input is an empty string and minLength is undefined', () => { + const input = ''; + const operation: LDAPVariableFallback = { operation: 'fallback', fallback: 'defaultFallback', minLength: undefined }; + const result = executeFallback(mockUser, input, operation); + expect(result).to.equal('defaultFallbackValue'); + }); +}); diff --git a/apps/meteor/server/lib/ldap/operations/fallback.ts b/apps/meteor/server/lib/ldap/operations/fallback.ts new file mode 100644 index 0000000000000..ecd8c199fbdb3 --- /dev/null +++ b/apps/meteor/server/lib/ldap/operations/fallback.ts @@ -0,0 +1,24 @@ +import type { ILDAPEntry } from '@rocket.chat/core-typings'; + +import { getLdapDynamicValue } from '../getLdapDynamicValue'; + +export type LDAPVariableFallback = { + operation: 'fallback'; + fallback: string; + + minLength?: number; +}; + +export function executeFallback(ldapUser: ILDAPEntry, input: string, operation: LDAPVariableFallback): string | undefined { + let valid = Boolean(input); + + if (valid && typeof operation.minLength === 'number') { + valid = input.length >= operation.minLength; + } + + if (valid) { + return input; + } + + return getLdapDynamicValue(ldapUser, operation.fallback); +} diff --git a/apps/meteor/server/lib/ldap/operations/match.spec.ts b/apps/meteor/server/lib/ldap/operations/match.spec.ts new file mode 100644 index 0000000000000..ad1deb1148d52 --- /dev/null +++ b/apps/meteor/server/lib/ldap/operations/match.spec.ts @@ -0,0 +1,106 @@ +import { expect } from 'chai'; +import 'mocha'; + +import type { LDAPVariableMatch } from './match'; +import { executeMatch } from './match'; + +describe('executeMatch function', () => { + describe('Validation', () => { + it('throws an error when pattern is missing', () => { + const operation: LDAPVariableMatch = { + operation: 'match', + pattern: '', + regex: true, + }; + expect(() => executeMatch('input', operation)).to.throw('Invalid MATCH operation.'); + }); + + it('throws an error when neither valueIfTrue nor indexToUse is provided', () => { + const operation: LDAPVariableMatch = { + operation: 'match', + pattern: 'pattern', + }; + expect(() => executeMatch('input', operation)).to.throw('Invalid MATCH operation.'); + }); + }); + + describe('Non-Regex Matching', () => { + it('returns valueIfTrue when input matches pattern', () => { + const operation: LDAPVariableMatch = { + operation: 'match', + pattern: 'hello', + valueIfTrue: 'matched', + valueIfFalse: 'not matched', + }; + expect(executeMatch('hello', operation)).to.be.equal('matched'); + }); + + it('returns valueIfFalse when input does not match pattern', () => { + const operation: LDAPVariableMatch = { + operation: 'match', + pattern: 'hello', + valueIfTrue: 'matched', + valueIfFalse: 'not matched', + }; + expect(executeMatch('world', operation)).to.be.equal('not matched'); + }); + }); + + describe('Regex Matching', () => { + it('returns valueIfTrue when input matches regex pattern', () => { + const operation: LDAPVariableMatch = { + operation: 'match', + pattern: '^hello$', + regex: true, + valueIfTrue: 'matched', + valueIfFalse: 'not matched', + }; + expect(executeMatch('hello', operation)).to.be.equal('matched'); + }); + + it('returns valueIfFalse when input does not match regex pattern', () => { + const operation: LDAPVariableMatch = { + operation: 'match', + pattern: '^hello$', + regex: true, + valueIfTrue: 'matched', + valueIfFalse: 'not matched', + }; + expect(executeMatch('world', operation)).to.be.equal('not matched'); + }); + + it('uses flags when provided', () => { + const operation: LDAPVariableMatch = { + operation: 'match', + pattern: '^HELLO$', + regex: true, + flags: 'i', + valueIfTrue: 'matched', + valueIfFalse: 'not matched', + }; + expect(executeMatch('hello', operation)).to.be.equal('matched'); + }); + }); + + describe('IndexToUse', () => { + it('returns the matched group at indexToUse', () => { + const operation: LDAPVariableMatch = { + operation: 'match', + pattern: '(hello) (world)', + regex: true, + indexToUse: 1, + }; + expect(executeMatch('hello world', operation)).to.be.equal('hello'); + }); + + it('returns undefined when indexToUse is out of range', () => { + const operation: LDAPVariableMatch = { + operation: 'match', + pattern: '(hello)', + regex: true, + indexToUse: 2, + }; + expect(executeMatch('hello', operation)).to.be.undefined; + }); + }); +}); diff --git a/apps/meteor/server/lib/ldap/operations/match.ts b/apps/meteor/server/lib/ldap/operations/match.ts new file mode 100644 index 0000000000000..422b13446d365 --- /dev/null +++ b/apps/meteor/server/lib/ldap/operations/match.ts @@ -0,0 +1,28 @@ +export type LDAPVariableMatch = { + operation: 'match'; + pattern: string; + regex?: boolean; + flags?: string; + indexToUse?: number; + valueIfTrue?: string; + valueIfFalse?: string; +}; + +export function executeMatch(input: string, operation: LDAPVariableMatch): string | undefined { + if (!operation.pattern || (typeof operation.valueIfTrue !== 'string' && typeof operation.indexToUse !== 'number')) { + throw new Error('Invalid MATCH operation.'); + } + + const pattern = operation.regex ? new RegExp(operation.pattern, operation.flags) : operation.pattern; + + const result = input.match(pattern); + if (!result) { + return operation.valueIfFalse; + } + + if (typeof operation.indexToUse === 'number' && result.length > operation.indexToUse) { + return result[operation.indexToUse]; + } + + return operation.valueIfTrue; +} diff --git a/apps/meteor/server/lib/ldap/operations/replace.spec.ts b/apps/meteor/server/lib/ldap/operations/replace.spec.ts new file mode 100644 index 0000000000000..b03bbf6796228 --- /dev/null +++ b/apps/meteor/server/lib/ldap/operations/replace.spec.ts @@ -0,0 +1,105 @@ +import { expect } from 'chai'; +import 'mocha'; + +import type { LDAPVariableReplace } from './replace'; +import { executeReplace } from './replace'; + +describe('executeReplace', () => { + describe('Validation', () => { + it('throws an error when pattern is missing', () => { + const operation: LDAPVariableReplace = { + operation: 'replace', + pattern: '', + replacement: 'new-value', + }; + expect(() => executeReplace('input', operation)).to.throw('Invalid REPLACE operation.'); + }); + + it('throws an error when replacement is not a string', () => { + const operation: LDAPVariableReplace = { + operation: 'replace', + pattern: 'old-value', + replacement: 123 as any, + }; + expect(() => executeReplace('input', operation)).to.throw('Invalid REPLACE operation.'); + }); + }); + + describe('String Replacement', () => { + it('replaces the first occurrence of the pattern', () => { + const operation: LDAPVariableReplace = { + operation: 'replace', + pattern: 'old', + replacement: 'new', + }; + expect(executeReplace('old-value old-value', operation)).to.be.equal('new-value old-value'); + }); + + it('replaces all occurrences of the pattern when `all` is true', () => { + const operation: LDAPVariableReplace = { + operation: 'replace', + pattern: 'old', + replacement: 'new', + all: true, + }; + expect(executeReplace('old-value old-value', operation)).to.be.equal('new-value new-value'); + }); + }); + + describe('Regex Replacement', () => { + it('replaces the first occurrence of the pattern', () => { + const operation: LDAPVariableReplace = { + operation: 'replace', + pattern: 'old', + replacement: 'new', + regex: true, + }; + expect(executeReplace('old-value old-value', operation)).to.be.equal('new-value old-value'); + }); + + it('replaces all occurrences of the pattern when `regex` and `all` are true', () => { + const operation: LDAPVariableReplace = { + operation: 'replace', + pattern: 'old', + replacement: 'new', + regex: true, + all: true, + }; + expect(executeReplace('old-value old-value', operation)).to.be.equal('new-value new-value'); + }); + + it('uses the provided flags', () => { + const operation: LDAPVariableReplace = { + operation: 'replace', + pattern: 'OLD', + replacement: 'new', + regex: true, + flags: 'i', + }; + expect(executeReplace('OLD-value old-value', operation)).to.be.equal('new-value old-value'); + }); + + it('adds the `g` flag when `all` is true', () => { + const operation: LDAPVariableReplace = { + operation: 'replace', + pattern: 'old', + replacement: 'new', + regex: true, + all: true, + }; + expect(executeReplace('old-value old-value', operation)).to.be.equal('new-value new-value'); + }); + + it('does not duplicate the `g` flag when already present', () => { + const operation: LDAPVariableReplace = { + operation: 'replace', + pattern: 'old', + replacement: 'new', + regex: true, + all: true, + flags: 'g', + }; + expect(executeReplace('old-value old-value', operation)).to.be.equal('new-value new-value'); + }); + }); +}); diff --git a/apps/meteor/server/lib/ldap/operations/replace.ts b/apps/meteor/server/lib/ldap/operations/replace.ts new file mode 100644 index 0000000000000..49460a8dd17fc --- /dev/null +++ b/apps/meteor/server/lib/ldap/operations/replace.ts @@ -0,0 +1,23 @@ +export type LDAPVariableReplace = { + operation: 'replace'; + pattern: string; + regex?: boolean; + flags?: string; + all?: boolean; + replacement: string; +}; + +export function executeReplace(input: string, operation: LDAPVariableReplace): string { + if (!operation.pattern || typeof operation.replacement !== 'string') { + throw new Error('Invalid REPLACE operation.'); + } + + const flags = operation.regex && operation.all ? `${operation.flags || ''}${operation.flags?.includes('g') ? '' : 'g'}` : operation.flags; + const pattern = operation.regex ? new RegExp(operation.pattern, flags) : operation.pattern; + + if (operation.all) { + return input.replaceAll(pattern, operation.replacement); + } + + return input.replace(pattern, operation.replacement); +} diff --git a/apps/meteor/server/lib/ldap/operations/split.spec.ts b/apps/meteor/server/lib/ldap/operations/split.spec.ts new file mode 100644 index 0000000000000..fa447a38752b9 --- /dev/null +++ b/apps/meteor/server/lib/ldap/operations/split.spec.ts @@ -0,0 +1,48 @@ +import { expect } from 'chai'; +import 'mocha'; + +import type { LDAPVariableSplit } from './split'; +import { executeSplit } from './split'; + +describe('executeSplit function', () => { + it('should throw an error if the pattern is empty', () => { + const operation: LDAPVariableSplit = { operation: 'split', pattern: '' }; + expect(() => executeSplit('input', operation)).to.throw('Invalid SPLIT operation.'); + }); + + it('should split the input string by the pattern and return the first element by default', () => { + const operation: LDAPVariableSplit = { operation: 'split', pattern: ',' }; + const result = executeSplit('hello,world', operation); + expect(result).to.be.equal('hello'); + }); + + it('should split the input string by the pattern and return the element at the specified index', () => { + const operation: LDAPVariableSplit = { operation: 'split', pattern: ',', indexToUse: 1 }; + const result = executeSplit('hello,world', operation); + expect(result).to.be.equal('world'); + }); + + it('should return undefined if the index is out of range', () => { + const operation: LDAPVariableSplit = { operation: 'split', pattern: ',', indexToUse: 2 }; + const result = executeSplit('hello,world', operation); + expect(result).to.be.undefined; + }); + + it('should return undefined if the input string does not contain the pattern', () => { + const operation: LDAPVariableSplit = { operation: 'split', pattern: ',' }; + const result = executeSplit('helloworld', operation); + expect(result).to.be.equal('helloworld'); + }); + + it('should return the first element if the input string is empty', () => { + const operation: LDAPVariableSplit = { operation: 'split', pattern: ',' }; + const result = executeSplit('', operation); + expect(result).to.be.equal(''); + }); + + it('should return undefined if the input string is undefined', () => { + const operation: LDAPVariableSplit = { operation: 'split', pattern: ',' }; + const result = executeSplit(undefined as any, operation); + expect(result).to.be.undefined; + }); +}); diff --git a/apps/meteor/server/lib/ldap/operations/split.ts b/apps/meteor/server/lib/ldap/operations/split.ts new file mode 100644 index 0000000000000..e6c68e78657f3 --- /dev/null +++ b/apps/meteor/server/lib/ldap/operations/split.ts @@ -0,0 +1,30 @@ +export type LDAPVariableSplit = { + operation: 'split'; + pattern: string; + indexToUse?: number; +}; + +export function executeSplit(input: string, operation: LDAPVariableSplit): string | undefined { + if (!operation.pattern) { + throw new Error('Invalid SPLIT operation.'); + } + + if (!input) { + return input; + } + + const result = input.split(operation.pattern); + if (!result) { + return; + } + + if (typeof operation.indexToUse === 'number') { + if (result.length > operation.indexToUse) { + return result[operation.indexToUse]; + } + + return; + } + + return result.shift(); +} diff --git a/apps/meteor/server/lib/ldap/operations/substring.spec.ts b/apps/meteor/server/lib/ldap/operations/substring.spec.ts new file mode 100644 index 0000000000000..18e55b6eebe9c --- /dev/null +++ b/apps/meteor/server/lib/ldap/operations/substring.spec.ts @@ -0,0 +1,48 @@ +import { expect } from 'chai'; +import 'mocha'; + +import type { LDAPVariableSubString } from './substring'; +import { executeSubstring } from './substring'; + +describe('executeSubstring function', () => { + it('should throw an error if the start is missing', () => { + const operation: LDAPVariableSubString = { operation: 'substring' } as unknown as LDAPVariableSubString; + expect(() => executeSubstring('input', operation)).to.throw('Invalid SUBSTRING operation.'); + }); + + it('should throw an error if the start is invalid', () => { + const operation: LDAPVariableSubString = { operation: 'substring', start: 0, end: null } as unknown as LDAPVariableSubString; + expect(() => executeSubstring('input', operation)).to.throw('Invalid SUBSTRING operation.'); + }); + + it('should get the substring of the input, using the start param', () => { + const result = executeSubstring('hello world', { operation: 'substring', start: 6 }); + expect(result).to.be.equal('world'); + }); + + it('should get the whole string when the start is zero', () => { + const result = executeSubstring('hello world', { operation: 'substring', start: 0 }); + expect(result).to.be.equal('hello world'); + }); + + it('should get the substring of the input, using the start and end param', () => { + const result = executeSubstring('hello world', { operation: 'substring', start: 6, end: 8 }); + expect(result).to.be.equal('wo'); + }); + + it('should work backwards if end is smaller than start', () => { + const result = executeSubstring('hello world', { operation: 'substring', start: 5, end: 0 }); + expect(result).to.be.equal('hello'); + }); + + it('should get an empty string if start and end are the same', () => { + expect(executeSubstring('hello world', { operation: 'substring', start: 0, end: 0 })).to.be.equal(''); + expect(executeSubstring('hello world', { operation: 'substring', start: 5, end: 5 })).to.be.equal(''); + }); + + it('should treat negative values as zero', () => { + expect(executeSubstring('hello world', { operation: 'substring', start: -4, end: 5 })).to.be.equal('hello'); + expect(executeSubstring('hello world', { operation: 'substring', start: 5, end: -4 })).to.be.equal('hello'); + expect(executeSubstring('hello world', { operation: 'substring', start: -5, end: -4 })).to.be.equal(''); + }); +}); diff --git a/apps/meteor/server/lib/ldap/operations/substring.ts b/apps/meteor/server/lib/ldap/operations/substring.ts new file mode 100644 index 0000000000000..3af2287c3120c --- /dev/null +++ b/apps/meteor/server/lib/ldap/operations/substring.ts @@ -0,0 +1,13 @@ +export type LDAPVariableSubString = { + operation: 'substring'; + start: number; + end?: number; +}; + +export function executeSubstring(input: string, operation: LDAPVariableSubString): string | undefined { + if (typeof operation.start !== 'number' || (operation.end !== undefined && typeof operation.end !== 'number')) { + throw new Error('Invalid SUBSTRING operation.'); + } + + return input.substring(operation.start, operation.end); +} diff --git a/apps/meteor/server/lib/ldap/processLdapVariables.ts b/apps/meteor/server/lib/ldap/processLdapVariables.ts new file mode 100644 index 0000000000000..4587bc6c7f902 --- /dev/null +++ b/apps/meteor/server/lib/ldap/processLdapVariables.ts @@ -0,0 +1,38 @@ +import type { ILDAPEntry } from '@rocket.chat/core-typings'; + +import { mapLogger } from './Logger'; +import { getLdapDynamicValue } from './getLdapDynamicValue'; +import { executeOperation, type LDAPVariableOperation } from './operations/executeOperation'; + +export type LDAPVariableConfiguration = { + input: string; + output?: LDAPVariableOperation; +}; +export type LDAPVariableMap = Record; + +export function processLdapVariables(entry: ILDAPEntry, variableMap: LDAPVariableMap): ILDAPEntry { + if (!variableMap || !Object.keys(variableMap).length) { + mapLogger.debug('No LDAP variables to process.'); + return entry; + } + + for (const variableName in variableMap) { + if (!variableMap.hasOwnProperty(variableName)) { + continue; + } + + const variableData = variableMap[variableName]; + if (!variableData?.input) { + continue; + } + + const input = getLdapDynamicValue(entry, variableData.input) || ''; + const output = executeOperation(entry, input, variableData.output) || ''; + + mapLogger.debug({ msg: 'Processed LDAP variable.', variableName, input, output }); + + entry[variableName] = output; + } + + return entry; +} diff --git a/apps/meteor/server/main.ts b/apps/meteor/server/main.ts index 90ee58fb8a6a1..10f724c745e79 100644 --- a/apps/meteor/server/main.ts +++ b/apps/meteor/server/main.ts @@ -10,6 +10,7 @@ import './settings'; import { configureServer } from './configuration'; import { registerServices } from './services/startup'; import { startup } from './startup'; +import { startRestAPI } from '../app/api/server/api'; import { settings } from '../app/settings/server'; import { startupApp } from '../ee/server'; import { startRocketChat } from '../startRocketChat'; @@ -27,3 +28,4 @@ await Promise.all([configureServer(settings), registerServices(), startup()]); await startRocketChat(); await startupApp(); +await startRestAPI(); diff --git a/apps/meteor/server/methods/browseChannels.ts b/apps/meteor/server/methods/browseChannels.ts index 5956fa4eaa8c3..bba6c6a173c22 100644 --- a/apps/meteor/server/methods/browseChannels.ts +++ b/apps/meteor/server/methods/browseChannels.ts @@ -6,6 +6,7 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; import mem from 'mem'; import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; import { Meteor } from 'meteor/meteor'; +import type { FindOptions, SortDirection } from 'mongodb'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; import { federationSearchUsers } from '../../app/federation/server/handler'; @@ -32,7 +33,7 @@ const sortChannels = (field: string, direction: 'asc' | 'desc'): Record { +const sortUsers = (field: string, direction: 'asc' | 'desc'): Record => { switch (field) { case 'email': return { @@ -183,7 +184,7 @@ const findUsers = async ({ viewFullOtherUserInfo, }: { text: string; - sort: Record; + sort: Record; pagination: { skip: number; limit: number; @@ -194,7 +195,7 @@ const findUsers = async ({ const searchFields = workspace === 'all' ? ['username', 'name', 'emails.address'] : settings.get('Accounts_SearchFields').trim().split(','); - const options = { + const options: FindOptions = { ...pagination, sort, projection: { @@ -264,7 +265,7 @@ const getUsers = async ( user: IUser | undefined, text: string, workspace: string, - sort: Record, + sort: Record, pagination: { skip: number; limit: number; @@ -289,6 +290,7 @@ const getUsers = async ( // Add the federated user to the results results.unshift({ + _id: user._id, username: user.username, name: user.name, bio: user.bio, diff --git a/apps/meteor/server/methods/getUsersOfRoom.ts b/apps/meteor/server/methods/getUsersOfRoom.ts index d3d69ce937542..915cfad4fe01f 100644 --- a/apps/meteor/server/methods/getUsersOfRoom.ts +++ b/apps/meteor/server/methods/getUsersOfRoom.ts @@ -1,4 +1,5 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; +import { UserStatus } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Subscriptions, Rooms } from '@rocket.chat/models'; import { check } from 'meteor/check'; @@ -54,7 +55,7 @@ Meteor.methods({ const { cursor } = findUsersOfRoom({ rid, - status: !showAll ? { $ne: 'offline' } : undefined, + status: !showAll ? { $ne: UserStatus.OFFLINE } : undefined, limit, skip, filter, diff --git a/apps/meteor/server/methods/removeUserFromRoom.ts b/apps/meteor/server/methods/removeUserFromRoom.ts index 8d1cb2e2ba2be..28e900667204d 100644 --- a/apps/meteor/server/methods/removeUserFromRoom.ts +++ b/apps/meteor/server/methods/removeUserFromRoom.ts @@ -55,6 +55,11 @@ export const removeUserFromRoomMethod = async (fromId: string, data: { rid: stri } const removedUser = await Users.findOneByUsernameIgnoringCase(data.username); + if (!removedUser) { + throw new Meteor.Error('error-user-not-in-room', 'User is not in this room', { + method: 'removeUserFromRoom', + }); + } await Room.beforeUserRemoved(room); diff --git a/apps/meteor/server/methods/toggleFavorite.ts b/apps/meteor/server/methods/toggleFavorite.ts index 912b9a8f3e5cc..251637253fb30 100644 --- a/apps/meteor/server/methods/toggleFavorite.ts +++ b/apps/meteor/server/methods/toggleFavorite.ts @@ -9,14 +9,29 @@ import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../app/lib/serv declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - toggleFavorite(rid: IRoom['_id'], f?: boolean): Promise; + toggleFavorite(rid: IRoom['_id'], favorite?: boolean): Promise; } } +export const toggleFavoriteMethod = async (userId: string, rid: IRoom['_id'], favorite?: boolean): Promise => { + const userSubscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userId); + if (!userSubscription) { + throw new Meteor.Error('error-invalid-subscription', 'You must be part of a room to favorite it', { method: 'toggleFavorite' }); + } + + const { modifiedCount } = await Subscriptions.setFavoriteByRoomIdAndUserId(rid, userId, favorite); + + if (modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, userId); + } + + return modifiedCount; +}; + Meteor.methods({ - async toggleFavorite(rid, f) { + async toggleFavorite(rid, favorite) { check(rid, String); - check(f, Match.Optional(Boolean)); + check(favorite, Match.Optional(Boolean)); const userId = Meteor.userId(); if (!userId) { @@ -25,17 +40,6 @@ Meteor.methods({ }); } - const userSubscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userId); - if (!userSubscription) { - throw new Meteor.Error('error-invalid-subscription', 'You must be part of a room to favorite it', { method: 'toggleFavorite' }); - } - - const { modifiedCount } = await Subscriptions.setFavoriteByRoomIdAndUserId(rid, userId, f); - - if (modifiedCount) { - void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, userId); - } - - return modifiedCount; + return toggleFavoriteMethod(userId, rid, favorite); }, }); diff --git a/apps/meteor/server/oauth2-server/oauth.ts b/apps/meteor/server/oauth2-server/oauth.ts index 7cf7b24d453d3..e52e5fabfac4f 100644 --- a/apps/meteor/server/oauth2-server/oauth.ts +++ b/apps/meteor/server/oauth2-server/oauth.ts @@ -21,6 +21,17 @@ export class OAuth2Server { this.config = config; this.app = express(); + this.app.use( + '/oauth/*', + express.json({ + limit: '50mb', + }), + express.urlencoded({ + extended: true, + limit: '50mb', + }), + express.query({}), + ); this.oauth = new OAuthServer({ model: new Model(this.config), diff --git a/apps/meteor/server/publications/room/index.ts b/apps/meteor/server/publications/room/index.ts index 07dd94be23d00..dcb9c741f2376 100644 --- a/apps/meteor/server/publications/room/index.ts +++ b/apps/meteor/server/publications/room/index.ts @@ -25,26 +25,29 @@ const roomMap = (record: IRoom | IOmnichannelRoom) => { return _.pick(record, ...Object.keys(roomFields)) as PublicRoom; }; -Meteor.methods({ - async 'rooms/get'(updatedAt) { - const options = { projection: roomFields }; - const user = Meteor.userId(); - - if (!user) { - if (settings.get('Accounts_AllowAnonymousRead')) { - return Rooms.findByDefaultAndTypes(true, ['c'], options).toArray(); - } - return []; - } +export const roomsGetMethod = async (userId?: string | null, updatedAt?: Date): Promise => { + const options = { projection: roomFields }; - if (updatedAt instanceof Date) { - return { - update: await (await Rooms.findBySubscriptionUserIdUpdatedAfter(user, updatedAt, options)).toArray(), - remove: await Rooms.trashFindDeletedAfter(updatedAt, {}, { projection: { _id: 1, _deletedAt: 1 } }).toArray(), - }; + if (!userId) { + if (settings.get('Accounts_AllowAnonymousRead')) { + return Rooms.findByDefaultAndTypes(true, ['c'], options).toArray(); } + return []; + } + + if (updatedAt instanceof Date) { + return { + update: await (await Rooms.findBySubscriptionUserIdUpdatedAfter(userId, updatedAt, options)).toArray(), + remove: await Rooms.trashFindDeletedAfter(updatedAt, {}, { projection: { _id: 1, _deletedAt: 1 } }).toArray(), + }; + } + + return (await Rooms.findBySubscriptionUserId(userId, options)).toArray(); +}; - return (await Rooms.findBySubscriptionUserId(user, options)).toArray(); +Meteor.methods({ + async 'rooms/get'(updatedAt) { + return roomsGetMethod(Meteor.userId(), updatedAt); }, async 'getRoomByTypeAndName'(type, name) { diff --git a/apps/meteor/server/services/apps-engine/service.ts b/apps/meteor/server/services/apps-engine/service.ts index 486a788563946..a858eead4c430 100644 --- a/apps/meteor/server/services/apps-engine/service.ts +++ b/apps/meteor/server/services/apps-engine/service.ts @@ -4,9 +4,10 @@ import { AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; import type { IGetAppsFilter } from '@rocket.chat/apps-engine/server/IGetAppsFilter'; import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; -import type { IAppsEngineService } from '@rocket.chat/core-services'; +import type { AppStatusReport, IAppsEngineService } from '@rocket.chat/core-services'; import { ServiceClassInternal } from '@rocket.chat/core-services'; +import { isRunningMs } from '../../lib/isRunningMs'; import { SystemLogger } from '../../lib/logger/system'; export class AppsEngineService extends ServiceClassInternal implements IAppsEngineService { @@ -133,4 +134,68 @@ export class AppsEngineService extends ServiceClassInternal implements IAppsEngi return app.getStorageItem(); } + + async getAppsStatusLocal(): Promise<{ status: AppStatus; appId: string }[]> { + const apps = await Apps.self?.getManager()?.get(); + + if (!apps) { + return []; + } + + return Promise.all( + apps.map(async (app) => ({ + status: await app.getStatus(), + appId: app.getID(), + })), + ); + } + + async getAppsStatusInNodes(): Promise { + if (!isRunningMs()) { + throw new Error('Getting apps status in cluster is only available in microservices mode'); + } + + if (!this.api) { + throw new Error('AppsEngineService is not initialized'); + } + + // If we are running MS AND this.api is defined, we KNOW there is a local node + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + const { id: localNodeId } = (await this.api.nodeList()).find((node) => node.local)!; + + const services: { name: string; nodes: string[] }[] = await this.api?.call('$node.services', { onlyActive: true }); + + // We can filter out the local node because we already know its status + const availableNodes = services?.find((service) => service.name === 'apps-engine')?.nodes.filter((node) => node !== localNodeId); + + if (!availableNodes || availableNodes.length < 1) { + throw new Error('Not enough Apps-Engine nodes in deployment'); + } + + const statusByApp: AppStatusReport = {}; + + const apps: Promise[] = availableNodes.map(async (nodeID) => { + const appsStatus: Awaited> | undefined = await this.api?.call( + 'apps-engine.getAppsStatusLocal', + [], + { nodeID }, + ); + + if (!appsStatus) { + throw new Error(`Failed to get apps status from node ${nodeID}`); + } + + appsStatus.forEach(({ status, appId }) => { + if (!statusByApp[appId]) { + statusByApp[appId] = []; + } + + statusByApp[appId].push({ instanceId: nodeID, status }); + }); + }); + + await Promise.all(apps); + + return statusByApp; + } } diff --git a/apps/meteor/server/services/authorization/service.ts b/apps/meteor/server/services/authorization/service.ts index d970068e23ba4..af9801c1da08e 100644 --- a/apps/meteor/server/services/authorization/service.ts +++ b/apps/meteor/server/services/authorization/service.ts @@ -126,7 +126,7 @@ export class Authorization extends ServiceClass implements IAuthorization { private getUserFromRoles = mem( async (roleIds: string[]) => { - const options = { + const users = await Users.findUsersInRoles(roleIds, null, { sort: { username: 1, }, @@ -134,9 +134,7 @@ export class Authorization extends ServiceClass implements IAuthorization { username: 1, roles: 1, }, - }; - - const users = await Users.findUsersInRoles(roleIds, null, options).toArray(); + }).toArray(); return users.map((user) => ({ ...user, diff --git a/apps/meteor/server/services/federation/application/AbstractFederationApplicationService.ts b/apps/meteor/server/services/federation/application/AbstractFederationApplicationService.ts index 0bda7529ebe46..5d2ecfbfbb613 100644 --- a/apps/meteor/server/services/federation/application/AbstractFederationApplicationService.ts +++ b/apps/meteor/server/services/federation/application/AbstractFederationApplicationService.ts @@ -70,7 +70,7 @@ export abstract class AbstractFederationApplicationService { return; } if (federatedUser.shouldUpdateDisplayName(displayName)) { - await this.internalUserAdapter.updateRealName(federatedUser.getInternalReference(), displayName); + await this.internalUserAdapter.updateRealName(federatedUser.getInternalReferenceCopy(), displayName); } } diff --git a/apps/meteor/server/services/federation/domain/FederatedUser.ts b/apps/meteor/server/services/federation/domain/FederatedUser.ts index fdd535588b52d..7d5818aa3cedb 100644 --- a/apps/meteor/server/services/federation/domain/FederatedUser.ts +++ b/apps/meteor/server/services/federation/domain/FederatedUser.ts @@ -70,6 +70,10 @@ export class FederatedUser { }); } + public getInternalReferenceCopy(): IUser { + return structuredClone(this.internalReference); + } + public getStorageRepresentation(): Readonly { return { _id: this.internalId, @@ -111,7 +115,23 @@ export class FederatedUser { } public shouldUpdateDisplayName(displayName: string): boolean { - return this.internalReference.name !== displayName; + // If there is no change, then we don't need to update + if (this.internalReference.name === displayName) { + return false; + } + + // If we don't have a name yet, then use whatever we're receiving + if (!this.internalReference.name) { + return true; + } + + // If the displayName received is based on the username, then ignore it and keep the existing name instead + if (this.internalReference.username?.includes(displayName) || this.externalId.includes(displayName)) { + return false; + } + + // It's a new value and an actual display name + return true; } public getInternalId(): string { diff --git a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Settings.ts b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Settings.ts index 3bbb803efcccd..d9708982c1c87 100644 --- a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Settings.ts +++ b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Settings.ts @@ -295,6 +295,7 @@ export class RocketChatSettingsAdapter { i18nLabel: 'Federation_Matrix_max_size_of_public_rooms_users', i18nDescription: 'Federation_Matrix_max_size_of_public_rooms_users_desc', alert: 'Federation_Matrix_max_size_of_public_rooms_users_Alert', + modules: ['federation'], public: true, enterprise: true, invalidValue: false, diff --git a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/User.ts b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/User.ts index 73bbdcadc77bf..14f938c91e4c3 100644 --- a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/User.ts +++ b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/User.ts @@ -93,8 +93,8 @@ export class RocketChatUserAdapter { return user; } - public async getInternalUserByUsername(username: string): Promise { - return Users.findOneByUsername(username); + public async getInternalUserByUsername(username: string): Promise { + return Users.findOneByUsername(username); } public async createFederatedUser(federatedUser: FederatedUser): Promise { diff --git a/apps/meteor/server/services/meteor/service.ts b/apps/meteor/server/services/meteor/service.ts index a3653f2c36356..99fdb9b49dfb5 100644 --- a/apps/meteor/server/services/meteor/service.ts +++ b/apps/meteor/server/services/meteor/service.ts @@ -8,7 +8,7 @@ import { Meteor } from 'meteor/meteor'; import { MongoInternals } from 'meteor/mongo'; import { triggerHandler } from '../../../app/integrations/server/lib/triggerHandler'; -import { Livechat } from '../../../app/livechat/server/lib/LivechatTyped'; +import { notifyGuestStatusChanged } from '../../../app/livechat/server/lib/guests'; import { onlineAgents, monitorAgents } from '../../../app/livechat/server/lib/stream/agentStatus'; import { metrics } from '../../../app/metrics/server'; import notifications from '../../../app/notifications/server/lib/Notifications'; @@ -262,23 +262,30 @@ export class MeteorService extends ServiceClassInternal implements IMeteor { return LoginServiceConfigurationModel.find({}, { projection: { secret: 0 } }).toArray(); } - async callMethodWithToken(userId: string, token: string, method: string, args: any[]): Promise { + async callMethodWithToken( + userId: string, + token: string, + method: string, + args: any[], + ): Promise<{ + result: unknown; + }> { const user = await Users.findOneByIdAndLoginHashedToken(userId, token, { projection: { _id: 1 }, }); if (!user) { return { - result: Meteor.callAsync(method, ...args), + result: await Meteor.callAsync(method, ...args), }; } return { - result: Meteor.runAsUser(userId, () => Meteor.callAsync(method, ...args)), + result: await Meteor.runAsUser(userId, () => Meteor.callAsync(method, ...args)), }; } async notifyGuestStatusChanged(token: string, status: UserStatus): Promise { - return Livechat.notifyGuestStatusChanged(token, status); + return notifyGuestStatusChanged(token, status); } async getURL(path: string, params: Record = {}, cloudDeepLinkUrl?: string): Promise { diff --git a/apps/meteor/server/services/omnichannel-integrations/providers/mobex.ts b/apps/meteor/server/services/omnichannel-integrations/providers/mobex.ts index d036345663cd4..c1e5e32018926 100644 --- a/apps/meteor/server/services/omnichannel-integrations/providers/mobex.ts +++ b/apps/meteor/server/services/omnichannel-integrations/providers/mobex.ts @@ -1,7 +1,6 @@ import { Base64 } from '@rocket.chat/base64'; import type { ISMSProvider, ServiceData, SMSProviderResult, SMSProviderResponse } from '@rocket.chat/core-typings'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; -import type { Request } from 'express'; import { settings } from '../../../../app/settings/server'; import { SystemLogger } from '../../../lib/logger/system'; @@ -197,7 +196,7 @@ export class Mobex implements ISMSProvider { }; } - validateRequest(_request: Request): boolean { + async validateRequest(_request: Request): Promise { return true; } diff --git a/apps/meteor/server/services/omnichannel-integrations/providers/twilio.ts b/apps/meteor/server/services/omnichannel-integrations/providers/twilio.ts index d2f89d35e7c5d..7d4c4a48e1a20 100644 --- a/apps/meteor/server/services/omnichannel-integrations/providers/twilio.ts +++ b/apps/meteor/server/services/omnichannel-integrations/providers/twilio.ts @@ -1,7 +1,6 @@ import { api } from '@rocket.chat/core-services'; import type { ISMSProvider, ServiceData, SMSProviderResponse, SMSProviderResult } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; -import type { Request } from 'express'; import filesize from 'filesize'; import twilio from 'twilio'; @@ -245,7 +244,7 @@ export class Twilio implements ISMSProvider { }; } - isRequestFromTwilio(signature: string, request: Request): boolean { + async isRequestFromTwilio(signature: string, request: Request): Promise { const authToken = settings.get('SMS_Twilio_authToken'); let siteUrl = settings.get('Site_Url'); if (siteUrl.endsWith('/')) { @@ -257,17 +256,23 @@ export class Twilio implements ISMSProvider { return false; } - const twilioUrl = request.originalUrl ? `${siteUrl}${request.originalUrl}` : `${siteUrl}/api/v1/livechat/sms-incoming/twilio`; + const twilioUrl = request.url ? `${siteUrl}${request.url}` : `${siteUrl}/api/v1/livechat/sms-incoming/twilio`; - return twilio.validateRequest(authToken, signature, twilioUrl, request.body); + let body = {}; + try { + body = await request.json(); + // eslint-disable-next-line no-empty + } catch {} + + return twilio.validateRequest(authToken, signature, twilioUrl, body); } - validateRequest(request: Request): boolean { + async validateRequest(request: Request): Promise { // We're not getting original twilio requests on CI :p if (process.env.TEST_MODE === 'true') { return true; } - const twilioHeader = request.headers['x-twilio-signature'] || ''; + const twilioHeader = request.headers.get('x-twilio-signature') || ''; const twilioSignature = Array.isArray(twilioHeader) ? twilioHeader[0] : twilioHeader; return this.isRequestFromTwilio(twilioSignature, request); } diff --git a/apps/meteor/server/services/omnichannel-integrations/providers/voxtelesys.ts b/apps/meteor/server/services/omnichannel-integrations/providers/voxtelesys.ts index aa42bacad6243..063070a30e7e8 100644 --- a/apps/meteor/server/services/omnichannel-integrations/providers/voxtelesys.ts +++ b/apps/meteor/server/services/omnichannel-integrations/providers/voxtelesys.ts @@ -2,7 +2,6 @@ import { api } from '@rocket.chat/core-services'; import type { ISMSProvider, ServiceData, SMSProviderResponse } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; -import type { Request } from 'express'; import filesize from 'filesize'; import { settings } from '../../../../app/settings/server'; @@ -163,7 +162,7 @@ export class Voxtelesys implements ISMSProvider { }; } - validateRequest(_request: Request): boolean { + async validateRequest(_request: Request): Promise { return true; } diff --git a/apps/meteor/server/services/omnichannel-voip/service.ts b/apps/meteor/server/services/omnichannel-voip/service.ts index 9c30202045cf6..d6d199ca47b38 100644 --- a/apps/meteor/server/services/omnichannel-voip/service.ts +++ b/apps/meteor/server/services/omnichannel-voip/service.ts @@ -15,7 +15,7 @@ import { isILivechatVisitor, OmnichannelSourceType, isVoipRoom, VoipClientEvents import { Logger } from '@rocket.chat/logger'; import { Users, VoipRoom, PbxEvents } from '@rocket.chat/models'; import type { PaginatedResult } from '@rocket.chat/rest-typings'; -import type { FindOptions } from 'mongodb'; +import type { FindOptions, SortDirection } from 'mongodb'; import _ from 'underscore'; import type { IOmniRoomClosingMessage } from './internalTypes'; @@ -183,7 +183,7 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn private async getAllocatedExtesionAllocationData(projection: Partial<{ [P in keyof IUser]: number }>): Promise { const roles: string[] = ['livechat-agent', 'livechat-manager', 'admin']; - const options = { + const options: FindOptions = { sort: { username: 1, }, @@ -458,9 +458,9 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn text?: string, count?: number, offset?: number, - sort?: Record, + sort?: Record, ): Promise<{ agents: ILivechatAgent[]; total: number }> { - const { cursor, totalCount } = Users.getAvailableAgentsIncludingExt(includeExtension, text, { count, skip: offset, sort }); + const { cursor, totalCount } = Users.getAvailableAgentsIncludingExt(includeExtension, text, { limit: count, skip: offset, sort }); const [agents, total] = await Promise.all([cursor.toArray(), totalCount]); diff --git a/apps/meteor/server/services/omnichannel/service.ts b/apps/meteor/server/services/omnichannel/service.ts index e5b21f4aae97b..239a759aac5c4 100644 --- a/apps/meteor/server/services/omnichannel/service.ts +++ b/apps/meteor/server/services/omnichannel/service.ts @@ -5,8 +5,8 @@ import { License } from '@rocket.chat/license'; import moment from 'moment'; import { OmnichannelQueue } from './queue'; -import { Livechat } from '../../../app/livechat/server/lib/LivechatTyped'; import { RoutingManager } from '../../../app/livechat/server/lib/RoutingManager'; +import { notifyAgentStatusChanged } from '../../../app/livechat/server/lib/omni-users'; import { settings } from '../../../app/settings/server'; export class OmnichannelService extends ServiceClassInternal implements IOmnichannelService { @@ -27,7 +27,7 @@ export class OmnichannelService extends ServiceClassInternal implements IOmnicha const hasRole = user.roles.some((role) => ['livechat-manager', 'livechat-monitor', 'livechat-agent'].includes(role)); if (hasRole) { // TODO change `Livechat.notifyAgentStatusChanged` to a service call - await Livechat.notifyAgentStatusChanged(user._id, user.status); + await notifyAgentStatusChanged(user._id, user.status); } }); } diff --git a/apps/meteor/server/settings/layout.ts b/apps/meteor/server/settings/layout.ts index 984c11b21a0ea..d869e7fcceec1 100644 --- a/apps/meteor/server/settings/layout.ts +++ b/apps/meteor/server/settings/layout.ts @@ -89,6 +89,7 @@ export const createLayoutSettings = () => invalidValue: false, enterprise: true, public: true, + modules: ['hide-watermark'], enableQuery: [ { _id: 'Layout_Home_Body', diff --git a/apps/meteor/server/settings/ldap.ts b/apps/meteor/server/settings/ldap.ts index d0d77d4ec3456..646bcf31ec2d1 100644 --- a/apps/meteor/server/settings/ldap.ts +++ b/apps/meteor/server/settings/ldap.ts @@ -214,6 +214,24 @@ export const createLdapSettings = () => type: 'string', enableQuery, }); + + await this.add('LDAP_FederationHomeServer_Field', '', { + type: 'string', + enableQuery, + }); + + await this.add('LDAP_DataSync_UseVariables', false, { + type: 'boolean', + enableQuery, + invalidValue: false, + }); + + await this.add('LDAP_DataSync_VariableMap', '{}', { + type: 'code', + multiline: true, + enableQuery: [enableQuery, { _id: 'LDAP_DataSync_UseVariables', value: true }], + invalidValue: '{}', + }); }); await this.section('LDAP_DataSync_Avatar', async function () { diff --git a/apps/meteor/tests/data/livechat/canned-responses.ts b/apps/meteor/tests/data/livechat/canned-responses.ts index 23cfac13e2369..55a6db0b76dfd 100644 --- a/apps/meteor/tests/data/livechat/canned-responses.ts +++ b/apps/meteor/tests/data/livechat/canned-responses.ts @@ -9,7 +9,7 @@ export const createCannedResponse = (): Promise => { .send({ message: JSON.stringify({ method: 'livechat:saveTag', - params: [undefined, { name: faker.person.firstName(), description: faker.lorem.sentence() }, departments], + params: [undefined, { name: faker.string.uuid(), description: faker.lorem.sentence() }, departments], id: '101', msg: 'method', }), diff --git a/apps/meteor/tests/data/livechat/users.ts b/apps/meteor/tests/data/livechat/users.ts index 9d0bfd04f170f..b4298ff962c80 100644 --- a/apps/meteor/tests/data/livechat/users.ts +++ b/apps/meteor/tests/data/livechat/users.ts @@ -2,7 +2,7 @@ import { faker } from '@faker-js/faker'; import type { Credentials } from '@rocket.chat/api-client'; import type { ILivechatAgent, IUser } from '@rocket.chat/core-typings'; -import { api, credentials, request } from '../api-data'; +import { api, credentials, request, methodCall } from '../api-data'; import { password } from '../user'; import { createUser, login } from '../users.helper'; import { createAgent, makeAgentAvailable, makeAgentUnavailable } from './rooms'; @@ -76,3 +76,23 @@ export const createAnOfflineAgent = async (): Promise<{ user: agent, }; }; + +export const updateLivechatSettingsForUser = async ( + agentId: string, + livechatSettings: Record, + agentDepartments: string[] = [], +): Promise => { + await request + .post(methodCall('livechat:saveAgentInfo')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:saveAgentInfo', + params: [agentId, livechatSettings, agentDepartments], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200); +}; diff --git a/apps/meteor/tests/e2e/account-profile.spec.ts b/apps/meteor/tests/e2e/account-profile.spec.ts index 2c451640e3a94..20d531d9d7385 100644 --- a/apps/meteor/tests/e2e/account-profile.spec.ts +++ b/apps/meteor/tests/e2e/account-profile.spec.ts @@ -81,22 +81,15 @@ test.describe.serial('settings-account-profile', () => { expect(results.violations).toEqual([]); }); - test('expect to disable email 2FA', async () => { + test('should disable and enable email 2FA', async () => { await poAccountProfile.security2FASection.click(); - await expect(poAccountProfile.disableEmail2FAButton).toBeVisible(); - await poAccountProfile.disableEmail2FAButton.click(); - + await expect(poAccountProfile.email2FASwitch).toBeVisible(); + await poAccountProfile.email2FASwitch.click(); await expect(poHomeChannel.toastSuccess).toBeVisible(); - await expect(poAccountProfile.enableEmail2FAButton).toBeVisible(); - }); - - test('expect to enable email 2FA', async () => { - await poAccountProfile.security2FASection.click(); - await expect(poAccountProfile.enableEmail2FAButton).toBeVisible(); - await poAccountProfile.enableEmail2FAButton.click(); + await poHomeChannel.dismissToast(); + await poAccountProfile.email2FASwitch.click(); await expect(poHomeChannel.toastSuccess).toBeVisible(); - await expect(poAccountProfile.disableEmail2FAButton).toBeVisible(); }); }); diff --git a/apps/meteor/tests/e2e/administration-settings.spec.ts b/apps/meteor/tests/e2e/administration-settings.spec.ts index d2996d6eac886..24ffa09d34c1c 100644 --- a/apps/meteor/tests/e2e/administration-settings.spec.ts +++ b/apps/meteor/tests/e2e/administration-settings.spec.ts @@ -1,6 +1,6 @@ import { Users } from './fixtures/userStates'; import { Admin } from './page-objects'; -import { getSettingValueById } from './utils'; +import { getSettingValueById, setSettingValueById } from './utils'; import { test, expect } from './utils/test'; test.use({ storageState: Users.admin.state }); @@ -42,11 +42,29 @@ test.describe.parallel('administration-settings', () => { await page.goto('/admin/settings/Layout'); }); - test('should code mirror full screen be displayed correctly', async ({ page }) => { + test.afterAll(async ({ api }) => setSettingValueById(api, 'theme-custom-css', '')); + + test('should display the code mirror correctly', async ({ page, api }) => { await poAdmin.getAccordionBtnByName('Custom CSS').click(); - await poAdmin.btnFullScreen.click(); - await expect(page.getByRole('code')).toHaveCSS('width', '920px'); + await test.step('should render only one code mirror element', async () => { + const codeMirrorParent = page.getByRole('code'); + await expect(codeMirrorParent.locator('.CodeMirror')).toHaveCount(1); + }); + + await test.step('should display full screen properly', async () => { + await poAdmin.btnFullScreen.click(); + await expect(page.getByRole('code')).toHaveCSS('width', '920px'); + await poAdmin.btnExitFullScreen.click(); + }); + + await test.step('should reflect updated value when valueProp changes after server update', async () => { + const codeValue = `.test-class-${Date.now()} { background-color: red; }`; + await setSettingValueById(api, 'theme-custom-css', codeValue); + + const codeMirrorParent = page.getByRole('code'); + await expect(codeMirrorParent.locator('.CodeMirror-line')).toHaveText(codeValue); + }); }); }); }); diff --git a/apps/meteor/tests/e2e/administration.spec.ts b/apps/meteor/tests/e2e/administration.spec.ts index dde8bfe392d06..69fd87d0c02cf 100644 --- a/apps/meteor/tests/e2e/administration.spec.ts +++ b/apps/meteor/tests/e2e/administration.spec.ts @@ -277,6 +277,66 @@ test.describe.parallel('administration', () => { }); }); + test.describe.serial('Third party login', () => { + const appName = faker.string.uuid(); + const appRedirectURI = faker.internet.url(); + + test.beforeEach(async ({ page }) => { + await page.goto('/admin/third-party-login'); + }); + + test('should show Third-party login page', async ({ page }) => { + await page.goto('/admin/third-party-login'); + + await expect(page.locator('h1 >> text="Third-party login"')).toBeVisible(); + }); + + test('should not be able to create a new application without application name', async ({ page }) => { + await poAdmin.btnNewApplication.click(); + await poAdmin.inputRedirectURI.fill(appRedirectURI); + await poAdmin.btnSave.click(); + + await expect(page.getByText('Name required')).toBeVisible(); + }); + + test('should not be able to create a new application without redirect URI', async ({ page }) => { + await poAdmin.btnNewApplication.click(); + await poAdmin.inputApplicationName.fill(appName); + await poAdmin.btnSave.click(); + + await expect(page.getByText('Redirect URI required')).toBeVisible(); + }); + + test('should be able to create a new application', async ({ page }) => { + await poAdmin.btnNewApplication.click(); + await poAdmin.inputApplicationName.fill(appName); + await poAdmin.inputRedirectURI.fill(appRedirectURI); + await poAdmin.btnSave.click(); + + await expect(poAdmin.getThirdPartyAppByName(appName)).toBeVisible(); + await expect(page.getByText('Application added')).toBeVisible(); + }); + + test('should be able see aplication fields', async () => { + await poAdmin.getThirdPartyAppByName(appName).click(); + await expect(poAdmin.inputApplicationName).toBeVisible(); + await expect(poAdmin.inputRedirectURI).toBeVisible(); + await expect(poAdmin.inputClientId).toBeVisible(); + await expect(poAdmin.inputClientSecret).toBeVisible(); + await expect(poAdmin.inputAuthUrl).toBeVisible(); + await expect(poAdmin.inputTokenUrl).toBeVisible(); + }); + + test('should be able to delete an application', async ({ page }) => { + await poAdmin.getThirdPartyAppByName(appName).click(); + await poAdmin.btnDelete.click(); + await poUtils.btnModalConfirmDelete.click(); + + await expect(page.getByText('Your entry has been deleted.')).toBeVisible(); + await expect(poAdmin.getIntegrationByName(appName)).not.toBeVisible(); + }); + }); + test.describe('Integrations', () => { const messageCodeHighlightDefault = 'javascript,css,markdown,dockerfile,json,go,rust,clean,bash,plaintext,powershell,scss,shell,yaml,vim'; diff --git a/apps/meteor/tests/e2e/channel-management.spec.ts b/apps/meteor/tests/e2e/channel-management.spec.ts index 71c310d9b6f40..a15320c25aff4 100644 --- a/apps/meteor/tests/e2e/channel-management.spec.ts +++ b/apps/meteor/tests/e2e/channel-management.spec.ts @@ -134,7 +134,7 @@ test.describe.serial('channel-management', () => { targetChannel = hugeName; await page.setViewportSize({ width: 640, height: 460 }); - await expect(page.getByRole('heading', { name: hugeName })).toHaveCSS('width', '419px'); + await expect(page.getByRole('heading', { name: hugeName })).toHaveCSS('width', '407px'); }); test('should open sidebar clicking on sidebar toggler', async ({ page }) => { diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 39ba5fe64d015..0a50877d6894a 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -884,6 +884,8 @@ test.describe.serial('e2ee room setup', () => { await page.goto('/home'); + await page.waitForSelector('.main-content'); + await expect(poHomeChannel.bannerSaveEncryptionPassword).toBeVisible(); const channelName = faker.string.uuid(); diff --git a/apps/meteor/tests/e2e/embedded-layout.spec.ts b/apps/meteor/tests/e2e/embedded-layout.spec.ts index 380a740b5e542..9561dadf7302b 100644 --- a/apps/meteor/tests/e2e/embedded-layout.spec.ts +++ b/apps/meteor/tests/e2e/embedded-layout.spec.ts @@ -74,8 +74,7 @@ test.describe.serial('embedded-layout', () => { }); }); - // TODO: Fix intermittent failure where direct messages sometimes shows "channel not joined" screen - test.fixme('direct message', () => { + test.describe('direct message', () => { test('should allow sending messages', async ({ page, api }) => { await createDirectMessage(api); await page.goto('/home'); diff --git a/apps/meteor/tests/e2e/enforce-2FA.spec.ts b/apps/meteor/tests/e2e/enforce-2FA.spec.ts index cb5671864e3d5..188447df67365 100644 --- a/apps/meteor/tests/e2e/enforce-2FA.spec.ts +++ b/apps/meteor/tests/e2e/enforce-2FA.spec.ts @@ -49,12 +49,14 @@ test.describe('enforce two factor authentication', () => { test('should redirect to 2FA setup page and setup email 2FA', async ({ page }) => { await page.goto('/home'); + await poAccountProfile.required2faModalSetUpButton.click(); await expect(poHomeChannel.sidenav.sidebarHomeAction).not.toBeVisible(); + await expect(poAccountProfile.securityHeader).toBeVisible(); - await poAccountProfile.security2FASection.click(); - await expect(poAccountProfile.enableEmail2FAButton).toBeVisible(); - await poAccountProfile.enableEmail2FAButton.click(); + await expect(poAccountProfile.security2FASection).toHaveAttribute('aria-expanded', 'true'); + await expect(poAccountProfile.email2FASwitch).toBeVisible(); + await poAccountProfile.email2FASwitch.click(); await expect(poHomeChannel.toastSuccess).toBeVisible(); await expect(poHomeChannel.sidenav.sidebarHomeAction).toBeVisible(); diff --git a/apps/meteor/tests/e2e/feature-preview.spec.ts b/apps/meteor/tests/e2e/feature-preview.spec.ts index 474e6d84c5225..72259e8b5665b 100644 --- a/apps/meteor/tests/e2e/feature-preview.spec.ts +++ b/apps/meteor/tests/e2e/feature-preview.spec.ts @@ -1,8 +1,18 @@ import { faker } from '@faker-js/faker'; +import type { Page } from '@playwright/test'; import { Users } from './fixtures/userStates'; import { AccountProfile, HomeChannel } from './page-objects'; -import { createTargetChannel, createTargetTeam, deleteChannel, deleteTeam, setSettingValueById } from './utils'; +import { + createTargetChannel, + createTargetTeam, + deleteChannel, + deleteTeam, + setSettingValueById, + createTargetDiscussion, + createChannelWithTeam, + deleteRoom, +} from './utils'; import { setUserPreferences } from './utils/setUserPreferences'; import { test, expect } from './utils/test'; @@ -12,17 +22,20 @@ test.describe.serial('feature preview', () => { let poHomeChannel: HomeChannel; let poAccountProfile: AccountProfile; let targetChannel: string; + let targetDiscussion: Record; let sidepanelTeam: string; const targetChannelNameInTeam = `channel-from-team-${faker.number.int()}`; test.beforeAll(async ({ api }) => { await setSettingValueById(api, 'Accounts_AllowFeaturePreview', true); targetChannel = await createTargetChannel(api, { members: ['user1'] }); + targetDiscussion = await createTargetDiscussion(api); }); test.afterAll(async ({ api }) => { await setSettingValueById(api, 'Accounts_AllowFeaturePreview', false); await deleteChannel(api, targetChannel); + await deleteRoom(api, targetDiscussion._id); }); test.beforeEach(async ({ page }) => { @@ -32,6 +45,7 @@ test.describe.serial('feature preview', () => { test('should show "Message" and "Navigation" feature sections', async ({ page }) => { await page.goto('/account/feature-preview'); + await page.waitForSelector('.main-content'); await expect(page.getByRole('button', { name: 'Message' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Navigation' })).toBeVisible(); @@ -77,11 +91,40 @@ test.describe.serial('feature preview', () => { await expect(poHomeChannel.navbar.navbar).toBeVisible(); }); - test('should display "Recent" button on sidebar search section, and display recent chats when clicked', async ({ page }) => { + test('should render global header navigation', async ({ page }) => { await page.goto('/home'); - await poHomeChannel.sidebar.btnRecent.click(); - await expect(poHomeChannel.sidebar.sidebar.getByRole('heading', { name: 'Recent' })).toBeVisible(); + await test.step('should display recent chats when navbar search is clicked', async () => { + await poHomeChannel.navbar.searchInput.click(); + await expect(poHomeChannel.navbar.searchList).toBeVisible(); + await poHomeChannel.navbar.searchInput.blur(); + }); + + await test.step('should display home and directory button', async () => { + await expect(poHomeChannel.navbar.homeButton).toBeVisible(); + await expect(poHomeChannel.navbar.btnDirectory).toBeVisible(); + }); + + await test.step('should display home and directory inside a menu and sidebar toggler in tablet view', async () => { + await page.setViewportSize({ width: 1023, height: 767 }); + await expect(poHomeChannel.navbar.btnMenuPages).toBeVisible(); + await expect(poHomeChannel.navbar.btnSidebarToggler).toBeVisible(); + }); + + await test.step('should display voice and omnichannel items inside a menu in mobile view', async () => { + await page.setViewportSize({ width: 767, height: 510 }); + await expect(poHomeChannel.navbar.btnVoiceAndOmnichannel).toBeVisible(); + }); + + await test.step('should hide everything else when navbar search is focused in mobile view', async () => { + await page.setViewportSize({ width: 767, height: 510 }); + await poHomeChannel.navbar.searchInput.click(); + + await expect(poHomeChannel.navbar.btnMenuPages).not.toBeVisible(); + await expect(poHomeChannel.navbar.btnSidebarToggler).not.toBeVisible(); + await expect(poHomeChannel.navbar.btnVoiceAndOmnichannel).not.toBeVisible(); + await expect(poHomeChannel.navbar.groupHistoryNavigation).not.toBeVisible(); + }); }); test('should not display room topic in direct message', async ({ page }) => { @@ -156,11 +199,13 @@ test.describe.serial('feature preview', () => { test('should show unread badge on collapser when group is collapsed and has unread items', async ({ page }) => { await page.goto('/home'); - await poHomeChannel.sidebar.openChat(targetChannel); + await poHomeChannel.navbar.openChat(targetChannel); await poHomeChannel.content.sendMessage('hello world'); - await poHomeChannel.sidebar.typeSearch(targetChannel); const item = poHomeChannel.sidebar.getSearchRoomByName(targetChannel); + + await expect(item).toBeVisible(); + await poHomeChannel.sidebar.markItemAsUnread(item); await poHomeChannel.sidebar.escSearch(); @@ -172,12 +217,64 @@ test.describe.serial('feature preview', () => { test('should not show NavBar in embedded layout', async ({ page }) => { await page.goto('/home'); - await poHomeChannel.sidebar.openChat(targetChannel); + await poHomeChannel.navbar.openChat(targetChannel); await expect(page.locator('role=navigation[name="header"]')).toBeVisible(); const embeddedLayoutURL = `${page.url()}?layout=embedded`; await page.goto(embeddedLayoutURL); await expect(page.locator('role=navigation[name="header"]')).not.toBeVisible(); }); + + test('should display the room header properly', async ({ page }) => { + await page.goto('/home'); + await poHomeChannel.navbar.openChat(targetDiscussion.fname); + + await test.step('should not display avatar in room header', async () => { + await expect(page.locator('main').locator('header').getByRole('figure')).not.toBeVisible(); + }); + + await test.step('should display the back button in the room header when accessing a room with parent', async () => { + await expect( + page + .locator('main') + .locator('header') + .getByRole('button', { name: /Back to/ }), + ).toBeVisible(); + }); + }); + + test.describe('user is not part of the team', () => { + let targetTeam: string; + let targetChannelWithTeam: string; + let user1Page: Page; + + test.beforeAll(async ({ api, browser }) => { + await setSettingValueById(api, 'Accounts_Default_User_Preferences_featuresPreview', [{ name: 'newNavigation', value: true }]); + + const { channelName, teamName } = await createChannelWithTeam(api); + targetTeam = teamName; + targetChannelWithTeam = channelName; + user1Page = await browser.newPage({ storageState: Users.user1.state }); + }); + + test.afterAll(async ({ api }) => { + await setSettingValueById(api, 'Accounts_Default_User_Preferences_featuresPreview', []); + + await deleteChannel(api, targetChannelWithTeam); + await deleteTeam(api, targetTeam); + await user1Page.close(); + }); + + test('should not display back to team button in the room header', async ({ page }) => { + await user1Page.goto(`/channel/${targetChannelWithTeam}`); + + await expect( + page + .locator('main') + .locator('header') + .getByRole('button', { name: /Back to/ }), + ).not.toBeVisible(); + }); + }); }); test.describe('Sidepanel', () => { @@ -265,8 +362,8 @@ test.describe.serial('feature preview', () => { await page.goto('/home'); const message = 'hello world'; - await poHomeChannel.sidebar.setDisplayMode('Extended'); - await poHomeChannel.sidebar.openChat(sidepanelTeam); + await poHomeChannel.navbar.setDisplayMode('Extended'); + await poHomeChannel.navbar.openChat(sidepanelTeam); await poHomeChannel.content.sendMessage(message); await expect(poHomeChannel.sidepanel.getExtendedItem(sidepanelTeam, message)).toBeVisible(); }); @@ -276,8 +373,8 @@ test.describe.serial('feature preview', () => { const message = 'hello > world'; const parsedWrong = 'hello > world'; - await poHomeChannel.sidebar.setDisplayMode('Extended'); - await poHomeChannel.sidebar.openChat(sidepanelTeam); + await poHomeChannel.navbar.setDisplayMode('Extended'); + await poHomeChannel.navbar.openChat(sidepanelTeam); await poHomeChannel.content.sendMessage(message); await expect(poHomeChannel.sidepanel.getExtendedItem(sidepanelTeam, message)).toBeVisible(); diff --git a/apps/meteor/tests/e2e/federation/page-objects/fragments/home-flextab.ts b/apps/meteor/tests/e2e/federation/page-objects/fragments/home-flextab.ts index 4c62a1199ac07..ad343b5b2a56d 100644 --- a/apps/meteor/tests/e2e/federation/page-objects/fragments/home-flextab.ts +++ b/apps/meteor/tests/e2e/federation/page-objects/fragments/home-flextab.ts @@ -44,6 +44,10 @@ export class FederationHomeFlextab { return this.page.locator('[data-qa-id="ToolBoxAction-phone"]'); } + get btnVideoCall(): Locator { + return this.page.locator('[role=toolbar][aria-label="Primary Room actions"]').getByRole('button', { name: 'Video call' }); + } + get btnDiscussion(): Locator { return this.page.locator('[data-qa-id="ToolBoxAction-discussion"]'); } diff --git a/apps/meteor/tests/e2e/federation/tests/channel/dm.spec.ts b/apps/meteor/tests/e2e/federation/tests/channel/dm.spec.ts index c724d8a39ded3..4d01e00f76435 100644 --- a/apps/meteor/tests/e2e/federation/tests/channel/dm.spec.ts +++ b/apps/meteor/tests/e2e/federation/tests/channel/dm.spec.ts @@ -770,9 +770,11 @@ test.describe.parallel('Federation - Direct Messages', () => { await poFederationChannelServer1.content.sendMessage('hello world'); await expect(poFederationChannelServer1.tabs.btnCall).toBeDisabled(); + await expect(poFederationChannelServer1.tabs.btnVideoCall).toBeDisabled(); await poFederationChannelServer2.sidenav.openChat(usernameWithDomainFromServer1); await expect(poFederationChannelServer2.tabs.btnCall).toBeDisabled(); + await expect(poFederationChannelServer1.tabs.btnVideoCall).toBeDisabled(); await pageForServer2.close(); }); diff --git a/apps/meteor/tests/e2e/federation/tests/channel/private.spec.ts b/apps/meteor/tests/e2e/federation/tests/channel/private.spec.ts index 1fcf6882bd393..29570e0372cb1 100644 --- a/apps/meteor/tests/e2e/federation/tests/channel/private.spec.ts +++ b/apps/meteor/tests/e2e/federation/tests/channel/private.spec.ts @@ -883,6 +883,8 @@ test.describe.parallel('Federation - Group Creation', () => { await expect(poFederationChannelServer1.tabs.btnCall).toBeDisabled(); await expect(poFederationChannelServer2.tabs.btnCall).toBeDisabled(); + await expect(poFederationChannelServer1.tabs.btnVideoCall).toBeDisabled(); + await expect(poFederationChannelServer2.tabs.btnVideoCall).toBeDisabled(); await pageForServer2.close(); }); diff --git a/apps/meteor/tests/e2e/federation/tests/channel/public.spec.ts b/apps/meteor/tests/e2e/federation/tests/channel/public.spec.ts index 0c79a309faba6..237a154c0d72c 100644 --- a/apps/meteor/tests/e2e/federation/tests/channel/public.spec.ts +++ b/apps/meteor/tests/e2e/federation/tests/channel/public.spec.ts @@ -889,6 +889,8 @@ test.describe.parallel('Federation - Channel Creation', () => { await expect(poFederationChannelServer1.tabs.btnCall).toBeDisabled(); await expect(poFederationChannelServer2.tabs.btnCall).toBeDisabled(); + await expect(poFederationChannelServer1.tabs.btnVideoCall).toBeDisabled(); + await expect(poFederationChannelServer2.tabs.btnVideoCall).toBeDisabled(); await pageForServer2.close(); }); diff --git a/apps/meteor/tests/e2e/image-gallery.spec.ts b/apps/meteor/tests/e2e/image-gallery.spec.ts index 526e695870df6..3afd299101226 100644 --- a/apps/meteor/tests/e2e/image-gallery.spec.ts +++ b/apps/meteor/tests/e2e/image-gallery.spec.ts @@ -120,7 +120,7 @@ test.describe.serial('Image Gallery', async () => { }); test.describe('When sending an image as a link', () => { - const imageLink = 'https://i0.wp.com/merithu.com.br/wp-content/uploads/2019/11/rocket-chat.png'; + const imageLink = 'https://raw.githubusercontent.com/RocketChat/Rocket.Chat.Artwork/master/Logos/2020/png/logo-horizontal-red.png'; test.beforeAll(async () => { await poHomeChannel.content.sendMessage(imageLink); diff --git a/apps/meteor/tests/e2e/jump-to-thread-message.spec.ts b/apps/meteor/tests/e2e/jump-to-thread-message.spec.ts index 5bcb23d76d342..fd92d933ae5a0 100644 --- a/apps/meteor/tests/e2e/jump-to-thread-message.spec.ts +++ b/apps/meteor/tests/e2e/jump-to-thread-message.spec.ts @@ -47,6 +47,11 @@ test.describe.serial('Threads', () => { const messageLink = `/channel/${targetChannel.name}?msg=${mainMessage._id}`; await page.goto(messageLink); + await expect(async () => { + await page.waitForSelector('.main-content'); + await page.waitForSelector(`[data-qa-rc-room="${targetChannel._id}"]`); + }).toPass(); + const message = page.locator(`[aria-label=\"Message list\"] [data-id=\"${mainMessage._id}\"]`); await expect(message).toBeVisible(); @@ -54,9 +59,29 @@ test.describe.serial('Threads', () => { }); test('expect to jump scroll to thread message on opening its message link', async ({ page }) => { + const threadMessageLink = `/channel/${targetChannel.name}?msg=${threadMessage._id}`; + await page.goto(threadMessageLink); + + await expect(async () => { + await page.waitForSelector('.main-content'); + await page.waitForSelector(`[data-qa-rc-room="${targetChannel._id}"]`); + }).toPass(); + + const message = await page.locator(`[aria-label=\"Thread message list\"] [data-id=\"${threadMessage._id}\"]`); + + await expect(message).toBeVisible(); + await expect(message).toBeInViewport(); + }); + + test('expect to jump scroll to thread message on opening its message link from a different channel', async ({ page }) => { const threadMessageLink = `/channel/general?msg=${threadMessage._id}`; await page.goto(threadMessageLink); + await expect(async () => { + await page.waitForSelector('.main-content'); + await page.waitForSelector(`[data-qa-rc-room="${targetChannel._id}"]`); + }).toPass(); + const message = await page.locator(`[aria-label=\"Thread message list\"] [data-id=\"${threadMessage._id}\"]`); await expect(message).toBeVisible(); diff --git a/apps/meteor/tests/e2e/mark-unread.spec.ts b/apps/meteor/tests/e2e/mark-unread.spec.ts index 46e5d206e5b6f..7dcc6762ff07a 100644 --- a/apps/meteor/tests/e2e/mark-unread.spec.ts +++ b/apps/meteor/tests/e2e/mark-unread.spec.ts @@ -1,68 +1,70 @@ -import { createAuxContext } from './fixtures/createAuxContext'; +import type { IRoom } from '@rocket.chat/core-typings'; + import { Users } from './fixtures/userStates'; import { HomeChannel } from './page-objects'; -import { createTargetChannel } from './utils'; +import { createTargetChannelAndReturnFullRoom } from './utils'; import { test, expect } from './utils/test'; test.use({ storageState: Users.admin.state }); test.describe.serial('mark-unread', () => { let poHomeChannel: HomeChannel; - let targetChannel: string; + let targetChannel: Required; test.beforeEach(async ({ page, api }) => { poHomeChannel = new HomeChannel(page); - targetChannel = await createTargetChannel(api, { members: ['user2'] }); + const result = await createTargetChannelAndReturnFullRoom(api, { members: ['user2'] }); + targetChannel = result.channel as Required; await page.emulateMedia({ reducedMotion: 'reduce' }); await page.goto('/home'); }); test.afterEach(async ({ api }) => { - await api.post('/channels.delete', { roomName: targetChannel }); + await api.post('/channels.delete', { roomName: targetChannel.name }); }); test.describe('Mark Unread - Sidebar Action', () => { test('should not mark empty room as unread', async () => { - await poHomeChannel.sidenav.selectMarkAsUnread(targetChannel); + await poHomeChannel.sidenav.selectMarkAsUnread(targetChannel.name); - await expect(poHomeChannel.sidenav.getSidebarItemBadge(targetChannel)).not.toBeVisible(); + await expect(poHomeChannel.sidenav.getSidebarItemBadge(targetChannel.name)).not.toBeVisible(); }); test('should mark a populated room as unread', async () => { - await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.sidenav.openChat(targetChannel.name); await poHomeChannel.content.sendMessage('this is a message for reply'); - await poHomeChannel.sidenav.selectMarkAsUnread(targetChannel); + await poHomeChannel.sidenav.selectMarkAsUnread(targetChannel.name); - await expect(poHomeChannel.sidenav.getSidebarItemBadge(targetChannel)).toBeVisible(); + await expect(poHomeChannel.sidenav.getSidebarItemBadge(targetChannel.name)).toBeVisible(); }); test('should mark a populated room as unread - search', async () => { - await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.sidenav.openChat(targetChannel.name); await poHomeChannel.content.sendMessage('this is a message for reply'); - await poHomeChannel.sidenav.selectMarkAsUnread(targetChannel); - await poHomeChannel.sidenav.searchRoom(targetChannel); + await poHomeChannel.sidenav.selectMarkAsUnread(targetChannel.name); + await poHomeChannel.sidenav.searchRoom(targetChannel.name); - await expect(poHomeChannel.sidenav.getSearchItemBadge(targetChannel)).toBeVisible(); + await expect(poHomeChannel.sidenav.getSearchItemBadge(targetChannel.name)).toBeVisible(); }); }); test.describe('Mark Unread - Message Action', () => { - let poHomeChannelUser2: HomeChannel; - - test('should mark a populated room as unread', async ({ browser }) => { - const { page: user2Page } = await createAuxContext(browser, Users.user2); - poHomeChannelUser2 = new HomeChannel(user2Page); + test.use({ storageState: Users.user2.state }); - await poHomeChannelUser2.sidenav.openChat(targetChannel); - await poHomeChannelUser2.content.sendMessage('this is a message for reply'); - await user2Page.close(); + test('should mark a populated room as unread', async ({ api }) => { + await api.post('/chat.sendMessage', { + message: { + rid: targetChannel._id, + msg: 'this is a message for reply', + }, + }); await expect(async () => { - await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.sidenav.openChat(targetChannel.name); await poHomeChannel.content.openLastMessageMenu(); await poHomeChannel.markUnread.click(); - await expect(poHomeChannel.sidenav.getSidebarItemBadge(targetChannel)).toBeVisible(); + await expect(poHomeChannel.sidenav.getSidebarItemBadge(targetChannel.name)).toBeVisible(); }).toPass(); }); }); diff --git a/apps/meteor/tests/e2e/message-mentions.spec.ts b/apps/meteor/tests/e2e/message-mentions.spec.ts index 15abd9c36b400..45f4bd657a643 100644 --- a/apps/meteor/tests/e2e/message-mentions.spec.ts +++ b/apps/meteor/tests/e2e/message-mentions.spec.ts @@ -56,7 +56,7 @@ test.describe.serial('Should not allow to send @all mention if permission to do await expect(page).toHaveURL(`/group/${targetChannel2}`); }); await test.step('receive notify message', async () => { - await adminPage.content.sendMessage('@all '); + await adminPage.content.sendMessage('@all ', false); await expect(adminPage.content.lastUserMessage).toContainText('Notify all in this room is not allowed'); }); }); @@ -98,7 +98,7 @@ test.describe.serial('Should not allow to send @here mention if permission to do await expect(page).toHaveURL(`/group/${targetChannel2}`); }); await test.step('receive notify message', async () => { - await adminPage.content.sendMessage('@here '); + await adminPage.content.sendMessage('@here ', false); await expect(adminPage.content.lastUserMessage).toContainText('Notify all in this room is not allowed'); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-agents.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-agents.spec.ts index 239978928126e..643e672af6e1b 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-agents.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-agents.spec.ts @@ -114,8 +114,21 @@ test.describe.serial('OC - Manage Agents', () => { await poOmnichannelAgents.btnEdit.click(); await poOmnichannelAgents.selectDepartment(department.data.name); + const reg = new RegExp(`/api/v1/method.call/${encodeURIComponent('livechat:saveAgentInfo')}`); + const response = page.waitForResponse(reg); await poOmnichannelAgents.btnSave.click(); + /** + * between saving and opening the agent info again it is necessary to + * wait for the agent to be saved, since after successfully saving + * the contextual bar is closed + * otherwise content will be closed even if the current one is not the editing one + */ + + await response; + + await expect(poOmnichannelAgents.editCtxBar).not.toBeVisible(); + await test.step('expect the selected department is visible', async () => { await poOmnichannelAgents.findRowByUsername('user1').click(); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-business-hours.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-business-hours.spec.ts index 2c5dc162e509c..ed179249d7c11 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-business-hours.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-business-hours.spec.ts @@ -19,6 +19,9 @@ test.describe('OC - Business Hours', () => { let department2: Awaited>; let agent: Awaited>; + const BHid = faker.string.uuid(); + const BHName = 'TEST Business Hours'; + test.beforeAll(async ({ api }) => { department = await createDepartment(api); department2 = await createDepartment(api); @@ -40,8 +43,6 @@ test.describe('OC - Business Hours', () => { }); test('OC - Manage Business Hours - Create Business Hours', async ({ page }) => { - const BHName = faker.string.uuid(); - await page.goto('/omnichannel'); await poOmnichannelBusinessHours.sidenav.linkBusinessHours.click(); @@ -86,11 +87,9 @@ test.describe('OC - Business Hours', () => { }); test('OC - Business hours - Edit BH departments', async ({ api, page }) => { - const BHName = faker.string.uuid(); - await test.step('expect to create new businessHours', async () => { const createBH = await createBusinessHour(api, { - id: '33', + id: BHid, name: BHName, departments: [department.data._id], }); @@ -136,4 +135,49 @@ test.describe('OC - Business Hours', () => { await expect(poOmnichannelBusinessHours.confirmDeleteModal).not.toBeVisible(); }); }); + + test('OC - Business hours - Toggle BH active status', async ({ api, page }) => { + await test.step('expect to create new businessHours', async () => { + const createBH = await createBusinessHour(api, { + id: BHid, + name: BHName, + departments: [department.data._id], + }); + + expect(createBH.status()).toBe(200); + }); + + await page.goto('/omnichannel'); + await poOmnichannelBusinessHours.sidenav.linkBusinessHours.click(); + + await test.step('expect to disable business hours', async () => { + await poOmnichannelBusinessHours.sidenav.linkBusinessHours.click(); + + await poOmnichannelBusinessHours.search(BHName); + await poOmnichannelBusinessHours.findRowByName(BHName).click(); + + await poOmnichannelBusinessHours.getCheckboxByLabel('Enabled').click(); + await expect(poOmnichannelBusinessHours.getCheckboxByLabel('Enabled')).not.toBeChecked(); + + await poOmnichannelBusinessHours.btnSave.click(); + }); + + await test.step('expect to enable business hours', async () => { + await poOmnichannelBusinessHours.sidenav.linkBusinessHours.click(); + + await poOmnichannelBusinessHours.search(BHName); + await poOmnichannelBusinessHours.findRowByName(BHName).click(); + + await poOmnichannelBusinessHours.getCheckboxByLabel('Enabled').click(); + await expect(poOmnichannelBusinessHours.getCheckboxByLabel('Enabled')).toBeChecked(); + + await poOmnichannelBusinessHours.btnSave.click(); + }); + + await test.step('expect delete business hours', async () => { + await poOmnichannelBusinessHours.btnDeleteByName(BHName).click(); + await expect(poOmnichannelBusinessHours.confirmDeleteModal).toBeVisible(); + await poOmnichannelBusinessHours.btnConfirmDeleteModal.click(); + }); + }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-canned-responses-sidebar.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-canned-responses-sidebar.spec.ts index 8d3c3a4283f25..2a60fbab02ee3 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-canned-responses-sidebar.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-canned-responses-sidebar.spec.ts @@ -1,19 +1,22 @@ +import { faker } from '@faker-js/faker'; import type { Page } from '@playwright/test'; import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChat, HomeChannel } from '../page-objects'; +import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; import { test } from '../utils/test'; -test.describe('Omnichannel Canned Responses Sidebar', () => { +test.describe.serial('OC - Canned Responses Sidebar', () => { test.skip(!IS_EE, 'Enterprise Only'); let poLiveChat: OmnichannelLiveChat; let newVisitor: { email: string; name: string }; - let agent: { page: Page; poHomeChannel: HomeChannel }; + let agent: { page: Page; poHomeChannel: HomeOmnichannel }; + + const cannedResponseName = faker.string.uuid(); test.beforeAll(async ({ api, browser }) => { newVisitor = createFakeVisitor(); @@ -23,34 +26,65 @@ test.describe('Omnichannel Canned Responses Sidebar', () => { await api.post('/livechat/users/manager', { username: 'user1' }); const { page } = await createAuxContext(browser, Users.user1); - agent = { page, poHomeChannel: new HomeChannel(page) }; + agent = { page, poHomeChannel: new HomeOmnichannel(page) }; }); + test.beforeEach(async ({ page, api }) => { poLiveChat = new OmnichannelLiveChat(page, api); }); + test.afterAll('close livechat conversation', async () => { + await agent.poHomeChannel.content.closeChat(); + }); + test.afterAll(async ({ api }) => { await api.delete('/livechat/users/agent/user1'); await api.delete('/livechat/users/manager/user1'); + await poLiveChat.page.close(); await agent.page.close(); }); - test('Receiving a message from visitor', async ({ page }) => { - await test.step('Expect send a message as a visitor', async () => { + test('OC - Canned Responses Sidebar - Create', async ({ page }) => { + await test.step('expect send a message as a visitor', async () => { await page.goto('/livechat'); await poLiveChat.openLiveChat(); await poLiveChat.sendMessage(newVisitor, false); - await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor'); + await poLiveChat.onlineAgentMessage.fill('this_a_test_message_from_visitor'); await poLiveChat.btnSendMessageToOnlineAgent.click(); }); - await test.step('Expect to have 1 omnichannel assigned to agent 1', async () => { + await test.step('expect to have 1 omnichannel assigned to agent 1', async () => { await agent.poHomeChannel.sidenav.openChat(newVisitor.name); }); - await test.step('Expect to be able to open canned responses sidebar and creation', async () => { + await test.step('expect to be able to open canned responses sidebar and creation', async () => { await agent.poHomeChannel.content.btnCannedResponses.click(); + }); + + await test.step('expect to create new canned response', async () => { await agent.poHomeChannel.content.btnNewCannedResponse.click(); + await agent.poHomeChannel.cannedResponses.inputShortcut.fill(cannedResponseName); + await agent.poHomeChannel.cannedResponses.inputMessage.fill(faker.lorem.paragraph()); + await agent.poHomeChannel.cannedResponses.addTag(faker.commerce.department()); + await agent.poHomeChannel.cannedResponses.radioPublic.click(); + await agent.poHomeChannel.cannedResponses.btnSave.click(); + }); + }); + + test('OC - Canned Responses Sidebar - Edit', async () => { + await test.step('expect to have 1 omnichannel assigned to agent 1', async () => { + await agent.poHomeChannel.sidenav.openChat(newVisitor.name); + }); + + await test.step('expect to be able to open canned responses sidebar and creation', async () => { + await agent.poHomeChannel.content.btnCannedResponses.click(); + }); + + await test.step('expect to edit canned response', async () => { + await agent.poHomeChannel.cannedResponses.listItem(cannedResponseName).click(); + await agent.poHomeChannel.cannedResponses.btnEdit.click(); + await agent.poHomeChannel.cannedResponses.radioPrivate.click(); + await agent.poHomeChannel.cannedResponses.btnSave.click(); }); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-conflict-review.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-conflict-review.spec.ts new file mode 100644 index 0000000000000..b808d28f7aca4 --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-conflict-review.spec.ts @@ -0,0 +1,93 @@ +import { faker } from '@faker-js/faker'; + +import { createFakeVisitor } from '../../mocks/data'; +import { IS_EE } from '../config/constants'; +import { Users } from '../fixtures/userStates'; +import { HomeOmnichannel } from '../page-objects'; +import { createCustomField } from '../utils/omnichannel/custom-field'; +import { createConversation } from '../utils/omnichannel/rooms'; +import { test, expect } from '../utils/test'; + +const visitor = createFakeVisitor(); + +test.skip(!IS_EE, 'Omnichannel Contact Review > Enterprise Only'); + +test.use({ storageState: Users.user1.state }); + +test.describe.serial('OC - Contact Review', () => { + let poHomeChannel: HomeOmnichannel; + + const customFieldName = faker.string.uuid(); + const visitorToken = faker.string.uuid(); + let conversation: Awaited>; + let customField: Awaited>; + + test.beforeAll(async ({ api }) => { + ( + await Promise.all([ + api.post('/livechat/users/agent', { username: 'user1' }), + api.post('/livechat/users/manager', { username: 'user1' }), + ]) + ).every((res) => expect(res.status()).toBe(200)); + + customField = await createCustomField(api, { field: customFieldName }); + }); + + test.beforeEach(async ({ page }) => { + poHomeChannel = new HomeOmnichannel(page); + }); + + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.locator('.main-content').waitFor(); + }); + + test.beforeEach(async ({ api }) => { + conversation = await createConversation(api, { visitorName: visitor.name, agentId: `user1`, visitorToken }); + }); + + test.beforeEach(async ({ api }) => { + const resCustomFieldA = await api.post('/livechat/custom.field', { + token: visitorToken, + key: customFieldName, + value: 'custom-field-value', + overwrite: true, + }); + + expect(resCustomFieldA.status()).toBe(200); + + const resCustomFieldB = await api.post('/livechat/custom.field', { + token: visitorToken, + key: customFieldName, + value: 'custom-field-value-2', + overwrite: false, + }); + + expect(resCustomFieldB.status()).toBe(200); + }); + + test.afterAll(async ({ api }) => { + (await Promise.all([api.delete('/livechat/users/agent/user1'), api.delete('/livechat/users/manager/user1')])).every((res) => + expect(res.status()).toBe(200), + ); + + await conversation.delete(); + await customField.delete(); + }); + + test('OC - Contact Review - Update custom field conflicting', async ({ page }) => { + await poHomeChannel.sidenav.getSidebarItemByName(visitor.name).click(); + await poHomeChannel.content.btnContactInformation.click(); + + await poHomeChannel.content.contactReviewModal.btnSeeConflicts.click(); + + await poHomeChannel.content.contactReviewModal.getFieldByName(customFieldName).click(); + await poHomeChannel.content.contactReviewModal.findOption('custom-field-value-2').click(); + const responseListener = page.waitForResponse('**/api/v1/omnichannel/contacts.update'); + await poHomeChannel.content.contactReviewModal.btnSave.click(); + const response = await responseListener; + await expect(response.status()).toBe(200); + + await expect(poHomeChannel.content.contactReviewModal.btnSeeConflicts).not.toBeVisible(); + }); +}); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-unknown-callout.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-unknown-callout.spec.ts new file mode 100644 index 0000000000000..44335abad6cab --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-unknown-callout.spec.ts @@ -0,0 +1,69 @@ +import type { Page } from '@playwright/test'; + +import { createFakeVisitor } from '../../mocks/data'; +import { IS_EE } from '../config/constants'; +import { createAuxContext } from '../fixtures/createAuxContext'; +import { Users } from '../fixtures/userStates'; +import { OmnichannelLiveChat, HomeChannel } from '../page-objects'; +import { expect, test } from '../utils/test'; + +test.describe('OC - Contact Unknown Callout', () => { + test.skip(!IS_EE, 'Enterprise Only'); + + let poLiveChat: OmnichannelLiveChat; + let newVisitor: { email: string; name: string }; + + let agent: { page: Page; poHomeChannel: HomeChannel }; + + test.beforeAll(async ({ api, browser }) => { + newVisitor = createFakeVisitor(); + + await api.post('/livechat/users/agent', { username: 'user1' }); + await api.post('/livechat/users/manager', { username: 'user1' }); + + const { page } = await createAuxContext(browser, Users.user1); + agent = { page, poHomeChannel: new HomeChannel(page) }; + }); + test.beforeEach(async ({ page, api }) => { + poLiveChat = new OmnichannelLiveChat(page, api); + }); + + test.beforeEach('create livechat conversation', async ({ page }) => { + await page.goto('/livechat'); + await poLiveChat.openLiveChat(); + await poLiveChat.sendMessage(newVisitor, false); + await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor'); + await poLiveChat.btnSendMessageToOnlineAgent.click(); + }); + + test.afterEach('close livechat conversation', async () => { + await poLiveChat.closeChat(); + }); + + test.afterAll(async ({ api }) => { + await api.delete('/livechat/users/agent/user1'); + await api.delete('/livechat/users/manager/user1'); + await agent.page.close(); + }); + + test('OC - Contact Unknown Callout - Dismiss callout', async () => { + await test.step('expect to open conversation', async () => { + await agent.poHomeChannel.sidenav.openChat(newVisitor.name); + }); + + await test.step('expect contact unknown callout to be visible', async () => { + await expect(agent.poHomeChannel.content.contactUnknownCallout).toBeVisible(); + }); + + await test.step('expect to hide callout when dismiss is clicked', async () => { + await agent.poHomeChannel.content.btnDismissContactUnknownCallout.click(); + await expect(agent.poHomeChannel.content.contactUnknownCallout).not.toBeVisible(); + }); + + await test.step('expect keep callout hidden after changing pages', async () => { + await agent.poHomeChannel.sidenav.sidebarHomeAction.click(); + await agent.poHomeChannel.sidenav.openChat(newVisitor.name); + await expect(agent.poHomeChannel.content.contactUnknownCallout).not.toBeVisible(); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments.spec.ts index 023d11de47570..f6c156847c2f5 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments.spec.ts @@ -4,6 +4,7 @@ import type { Page } from '@playwright/test'; import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; import { OmnichannelDepartments } from '../page-objects'; +import { createAgent } from '../utils/omnichannel/agents'; import { createDepartment, deleteDepartment } from '../utils/omnichannel/departments'; import { test, expect } from '../utils/test'; @@ -19,15 +20,18 @@ test.describe('OC - Manage Departments', () => { test.skip(!IS_EE, 'Enterprise Edition Only'); let poOmnichannelDepartments: OmnichannelDepartments; + let agent: Awaited>; test.beforeAll(async ({ api }) => { // turn on department removal await api.post('/settings/Omnichannel_enable_department_removal', { value: true }); + agent = await createAgent(api, 'user1'); }); test.afterAll(async ({ api }) => { // turn off department removal await api.post('/settings/Omnichannel_enable_department_removal', { value: false }); + await agent.delete(); }); test.describe('Create first department', async () => { @@ -38,7 +42,7 @@ test.describe('OC - Manage Departments', () => { await poOmnichannelDepartments.sidenav.linkDepartments.click(); }); - test('Create department', async () => { + test('Create department', async ({ page }) => { const departmentName = faker.string.uuid(); await poOmnichannelDepartments.headingButtonNew('Create department').click(); @@ -65,12 +69,40 @@ test.describe('OC - Manage Departments', () => { await expect(poOmnichannelDepartments.errorMessage(ERROR.requiredEmail)).not.toBeVisible(); }); - await test.step('expect create new department', async () => { + await test.step('expect to fill required fields', async () => { await poOmnichannelDepartments.btnEnabled.click(); await poOmnichannelDepartments.inputName.fill(departmentName); await poOmnichannelDepartments.inputEmail.fill(faker.internet.email()); - await poOmnichannelDepartments.btnSave.click(); + }); + + await test.step('expect to fetch agents a reasonable number of times', async () => { + let requestCount = 0; + + await page.route('**/v1/livechat/users/agent*', async (route) => { + requestCount++; + await route.continue(); + }); + + await poOmnichannelDepartments.inputAgents.click(); + await poOmnichannelDepartments.inputAgents.fill('user1'); + await page.waitForTimeout(1000); + await expect(requestCount).toBeGreaterThan(0); + await expect(requestCount).toBeLessThan(3); + + await poOmnichannelDepartments.inputAgents.click(); + }); + + await test.step('expect to add an agent', async () => { + await poOmnichannelDepartments.inputAgents.click(); + await poOmnichannelDepartments.inputAgents.fill('user1'); + await poOmnichannelDepartments.findOption('user1 (@user1)').click(); + await poOmnichannelDepartments.btnAddAgent.click(); + await expect(poOmnichannelDepartments.findAgentRow('user1')).toBeVisible(); + }); + + await test.step('expect create new department', async () => { + await poOmnichannelDepartments.btnSave.click(); await poOmnichannelDepartments.search(departmentName); await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); }); diff --git a/apps/meteor/tests/e2e/page-objects/account-profile.ts b/apps/meteor/tests/e2e/page-objects/account-profile.ts index d4c654604cb75..bdd3b8873c68e 100644 --- a/apps/meteor/tests/e2e/page-objects/account-profile.ts +++ b/apps/meteor/tests/e2e/page-objects/account-profile.ts @@ -145,11 +145,15 @@ export class AccountProfile { return this.page.getByRole('button', { name: 'Save changes', exact: true }); } - get enableEmail2FAButton(): Locator { - return this.page.locator('role=button[name="Enable two-factor authentication via Email"]'); + get email2FASwitch(): Locator { + return this.page.locator('label', { has: this.page.getByRole('checkbox', { name: 'Two-factor authentication via email' }) }); } - get disableEmail2FAButton(): Locator { - return this.page.locator('role=button[name="Disable two-factor authentication via Email"]'); + get totp2FASwitch(): Locator { + return this.page.locator('label', { has: this.page.getByRole('checkbox', { name: 'Two-factor authentication via TOTP' }) }); + } + + get required2faModalSetUpButton(): Locator { + return this.page.locator('dialog >> button'); } } diff --git a/apps/meteor/tests/e2e/page-objects/admin.ts b/apps/meteor/tests/e2e/page-objects/admin.ts index 49f86a59b6f9b..aaf0f194f6d1f 100644 --- a/apps/meteor/tests/e2e/page-objects/admin.ts +++ b/apps/meteor/tests/e2e/page-objects/admin.ts @@ -251,6 +251,10 @@ export class Admin { return this.page.getByRole('button', { name: 'New', exact: true }); } + get btnNewApplication(): Locator { + return this.page.getByRole('button', { name: 'New Application', exact: true }); + } + get btnDelete(): Locator { return this.page.getByRole('button', { name: 'Delete', exact: true }); } @@ -263,6 +267,26 @@ export class Admin { return this.page.getByRole('textbox', { name: 'Name' }); } + get inputApplicationName(): Locator { + return this.page.getByRole('textbox', { name: 'Application Name' }); + } + + get inputClientId(): Locator { + return this.page.getByRole('textbox', { name: 'Client ID' }); + } + + get inputClientSecret(): Locator { + return this.page.getByRole('textbox', { name: 'Client Secret' }); + } + + get inputAuthUrl(): Locator { + return this.page.getByRole('textbox', { name: 'Authorization URL' }); + } + + get inputTokenUrl(): Locator { + return this.page.getByRole('textbox', { name: 'Access Token URL' }); + } + get inputPostToChannel(): Locator { return this.page.getByRole('textbox', { name: 'Post to Channel' }); } @@ -271,6 +295,10 @@ export class Admin { return this.page.getByRole('textbox', { name: 'Post as' }); } + get inputRedirectURI(): Locator { + return this.page.getByRole('textbox', { name: 'Redirect URI' }); + } + codeExamplePayload(text: string): Locator { return this.page.locator('code', { hasText: text }); } @@ -279,6 +307,10 @@ export class Admin { return this.page.getByRole('table', { name: 'Integrations table' }).locator('tr', { hasText: name }); } + getThirdPartyAppByName(name: string): Locator { + return this.page.getByRole('table', { name: 'Third-party applications table' }).locator('tr', { hasText: name }); + } + get inputWebhookUrl(): Locator { return this.page.getByRole('textbox', { name: 'Webhook URL' }); } @@ -291,6 +323,10 @@ export class Admin { return this.page.getByRole('button', { name: 'Full Screen', exact: true }); } + get btnExitFullScreen(): Locator { + return this.page.getByRole('button', { name: 'Exit Full Screen', exact: true }); + } + async dropdownFilterRoomType(text = 'All rooms'): Promise { return this.page.locator(`div[role="button"]:has-text("${text}")`); } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 08806275ef42e..63e03a71e38aa 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -81,11 +81,25 @@ export class HomeContent { await this.joinRoom(); } - async sendMessage(text: string): Promise { + async sendMessage(text: string, enforce = true): Promise { await this.joinRoomIfNeeded(); await this.page.waitForSelector('[name="msg"]:not([disabled])'); await this.page.locator('[name="msg"]').fill(text); + const responsePromise = this.page.waitForResponse( + (response) => + /api\/v1\/method.call\/sendMessage/.test(response.url()) && response.status() === 200 && response.request().method() === 'POST', + ); await this.page.getByRole('button', { name: 'Send', exact: true }).click(); + + if (enforce) { + const response = await (await responsePromise).json(); + + const mid = JSON.parse(response.message).result._id; + const messageLocator = this.getMessageById(mid); + + await expect(messageLocator).toBeVisible(); + await expect(messageLocator).not.toHaveClass('rcx-message--pending'); + } } async dispatchSlashCommand(text: string): Promise { @@ -375,12 +389,8 @@ export class HomeContent { return this.page.locator('[data-qa-id="ToolBoxAction-pause-unfilled"]'); } - get btnCall(): Locator { - return this.page.locator('[data-qa-id="ToolBoxAction-phone"]'); - } - - get menuItemVideoCall(): Locator { - return this.page.locator('role=menuitem[name="Video call"]'); + get btnVideoCall(): Locator { + return this.page.locator('[role=toolbar][aria-label="Primary Room actions"]').getByRole('button', { name: 'Video call' }); } get btnStartVideoCall(): Locator { @@ -436,6 +446,10 @@ export class HomeContent { return this.page.locator('[role="listitem"][aria-roledescription="message"]', { hasText: text }); } + getMessageById(id: string): Locator { + return this.page.locator(`[data-qa-type="message"][id="${id}"]`); + } + async waitForChannel(): Promise { await this.page.locator('role=main').waitFor(); await this.page.locator('role=main >> role=heading[level=1]').waitFor(); @@ -463,4 +477,12 @@ export class HomeContent { get btnJoinChannel() { return this.page.getByRole('button', { name: 'Join channel' }); } + + get contactUnknownCallout() { + return this.page.getByRole('status', { name: 'Unknown contact. This contact is not on the contact list.' }); + } + + get btnDismissContactUnknownCallout() { + return this.contactUnknownCallout.getByRole('button', { name: 'Dismiss' }); + } } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-notificationPreferences.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-notificationPreferences.ts index f9ff7bff52d36..9a38571b32898 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-notificationPreferences.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-notificationPreferences.ts @@ -11,20 +11,24 @@ export class HomeFlextabNotificationPreferences { return this.page.locator('role=button[name="Save"]'); } + get dialogNotificationPreferences(): Locator { + return this.page.getByRole('dialog', { name: 'Notifications Preferences' }); + } + getPreferenceByDevice(device: string): Locator { return this.page.locator(`//div[@id="${device}Alert"]`); } async selectDropdownById(text: string): Promise { - await this.page.locator(`//div[@id="${text}"]`).click(); + await this.dialogNotificationPreferences.locator(`//div[@id="${text}"]`).click(); } async selectOptionByLabel(text: string): Promise { - await this.page.locator(`li.rcx-option >> text="${text}"`).click(); + await this.page.getByRole('listbox').getByRole('option', { name: text }).click(); } async selectDevice(text: string): Promise { - await this.page.locator(`[data-qa-id="${text}-notifications"]`).click(); + await this.dialogNotificationPreferences.getByRole('button', { name: text }).click(); } async updateDevicePreference(device: string): Promise { diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts index 37f35b8202de8..335cec483cb19 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts @@ -3,16 +3,20 @@ import type { Locator, Page } from '@playwright/test'; import { OmnichannelTransferChatModal } from '../omnichannel-transfer-chat-modal'; import { HomeContent } from './home-content'; import { OmnichannelCloseChatModal } from './omnichannel-close-chat-modal'; +import { OmnichannelContactReviewModal } from '../omnichannel-contact-review-modal'; export class HomeOmnichannelContent extends HomeContent { readonly closeChatModal: OmnichannelCloseChatModal; readonly forwardChatModal: OmnichannelTransferChatModal; + readonly contactReviewModal: OmnichannelContactReviewModal; + constructor(page: Page) { super(page); this.closeChatModal = new OmnichannelCloseChatModal(page); this.forwardChatModal = new OmnichannelTransferChatModal(page); + this.contactReviewModal = new OmnichannelContactReviewModal(page); } get btnReturnToQueue(): Locator { @@ -74,4 +78,10 @@ export class HomeOmnichannelContent extends HomeContent { get infoHeaderName(): Locator { return this.page.locator('.rcx-room-header').getByRole('heading'); } + + async closeChat() { + await this.btnCloseChat.click(); + await this.closeChatModal.inputComment.fill('any_comment'); + await this.closeChatModal.btnConfirm.click(); + } } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts index bf2eefeb11a37..872c6b46b7d92 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts @@ -61,6 +61,18 @@ export class HomeSidenav { return this.sidebarToolbar.getByRole('button', { name: 'Home' }); } + get btnDisplay(): Locator { + return this.sidebarToolbar.getByRole('button', { name: 'Display' }); + } + + get btnCreateNew(): Locator { + return this.sidebarToolbar.getByRole('button', { name: 'Create new' }); + } + + get btnAdministration(): Locator { + return this.sidebarToolbar.getByRole('button', { name: 'Administration' }); + } + async setDisplayMode(mode: 'Extended' | 'Medium' | 'Condensed'): Promise { await this.sidebarToolbar.getByRole('button', { name: 'Display', exact: true }).click(); await this.sidebarToolbar.getByRole('menuitemcheckbox', { name: mode }).click(); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/index.ts b/apps/meteor/tests/e2e/page-objects/fragments/index.ts index f80290a0d3734..5541c078b7e31 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/index.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/index.ts @@ -7,3 +7,4 @@ export * from './omnichannel-close-chat-modal'; export * from './navbar'; export * from './sidebar'; export * from './sidepanel'; +export * from './report-message-modal'; diff --git a/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts b/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts index 55cda22ed09ef..c9e432527d0b8 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts @@ -1,5 +1,7 @@ import type { Locator, Page } from '@playwright/test'; +import { expect } from '../../utils/test'; + export class Navbar { private readonly page: Page; @@ -11,11 +13,72 @@ export class Navbar { return this.page.getByRole('navigation', { name: 'header' }); } - get pagesToolbar(): Locator { - return this.navbar.getByRole('toolbar', { name: 'Pages' }); + get btnSidebarToggler(): Locator { + return this.navbar.getByRole('button', { name: 'Open sidebar' }); + } + + get btnVoiceAndOmnichannel(): Locator { + return this.navbar.getByRole('button', { name: 'Voice and omnichannel' }); + } + + get groupHistoryNavigation(): Locator { + return this.navbar.getByRole('group', { name: 'History navigation' }); + } + + get pagesGroup(): Locator { + return this.navbar.getByRole('group', { name: 'Pages and actions' }); } get homeButton(): Locator { - return this.pagesToolbar.getByRole('button', { name: 'Home' }); + return this.pagesGroup.getByRole('button', { name: 'Home' }); + } + + get btnDirectory(): Locator { + return this.pagesGroup.getByRole('button', { name: 'Directory' }); + } + + get btnMenuPages(): Locator { + return this.pagesGroup.getByRole('button', { name: 'Pages' }); + } + + get navbarSearchSection(): Locator { + return this.navbar.getByRole('search'); + } + + get searchInput(): Locator { + return this.navbarSearchSection.getByRole('combobox'); + } + + get searchList(): Locator { + return this.navbarSearchSection.getByRole('listbox', { name: 'Channels' }); + } + + async typeSearch(name: string): Promise { + return this.searchInput.fill(name); + } + + async waitForChannel(): Promise { + await this.page.locator('role=main').waitFor(); + await this.page.locator('role=main >> role=heading[level=1]').waitFor(); + const messageList = this.page.getByRole('main').getByRole('list', { name: 'Message list', exact: true }); + await messageList.waitFor(); + + await expect(messageList).not.toHaveAttribute('aria-busy', 'true'); + } + + getSearchRoomByName(name: string): Locator { + return this.searchList.getByRole('option', { name }); + } + + async openChat(name: string): Promise { + await this.typeSearch(name); + await this.getSearchRoomByName(name).click(); + await this.waitForChannel(); + } + + async setDisplayMode(mode: 'Extended' | 'Medium' | 'Condensed'): Promise { + await this.pagesGroup.getByRole('button', { name: 'Display', exact: true }).click(); + await this.pagesGroup.getByRole('menuitemcheckbox', { name: mode }).click(); + await this.pagesGroup.click(); } } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/report-message-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/report-message-modal.ts new file mode 100644 index 0000000000000..f88fcbff30282 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/report-message-modal.ts @@ -0,0 +1,29 @@ +import type { Locator, Page } from '@playwright/test'; + +export class ReportMessageModal { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + get inputReportDescription(): Locator { + return this.page.getByRole('dialog').getByRole('textbox', { name: 'Why do you want to report?' }); + } + + get btnSubmitReport(): Locator { + return this.page.getByRole('dialog').getByRole('button', { name: 'Report!' }); + } + + get btnCancelReport(): Locator { + return this.page.getByRole('dialog').getByRole('button', { name: 'Cancel' }); + } + + get reportDescriptionError(): Locator { + return this.page.getByRole('dialog').getByText('You need to write something!'); + } + + get modalTitle(): Locator { + return this.page.getByRole('dialog').getByText('Report this message?'); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts b/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts index 6c546b2fc9368..27ae799b4e4d9 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts @@ -1,7 +1,5 @@ import type { Locator, Page } from '@playwright/test'; -import { expect } from '../../utils/test'; - export class Sidebar { private readonly page: Page; @@ -14,20 +12,12 @@ export class Sidebar { return this.page.getByRole('navigation', { name: 'sidebar' }); } - get sidebarSearchSection(): Locator { - return this.sidebar.getByRole('search'); - } - - get btnRecent(): Locator { - return this.sidebarSearchSection.getByRole('button', { name: 'Recent' }); - } - get channelsList(): Locator { return this.sidebar.getByRole('list', { name: 'Channels' }); } - get searchList(): Locator { - return this.sidebar.getByRole('search').getByRole('list', { name: 'Channels' }); + getSearchRoomByName(name: string) { + return this.channelsList.getByRole('link', { name }); } get firstCollapser(): Locator { @@ -38,43 +28,10 @@ export class Sidebar { return this.channelsList.getByRole('listitem').first(); } - get searchInput(): Locator { - return this.sidebarSearchSection.getByRole('searchbox'); - } - - async setDisplayMode(mode: 'Extended' | 'Medium' | 'Condensed'): Promise { - await this.sidebarSearchSection.getByRole('button', { name: 'Display', exact: true }).click(); - await this.sidebarSearchSection.getByRole('menuitemcheckbox', { name: mode }).click(); - await this.sidebarSearchSection.click(); - } - async escSearch(): Promise { await this.page.keyboard.press('Escape'); } - async waitForChannel(): Promise { - await this.page.locator('role=main').waitFor(); - await this.page.locator('role=main >> role=heading[level=1]').waitFor(); - const messageList = this.page.getByRole('main').getByRole('list', { name: 'Message list', exact: true }); - await messageList.waitFor(); - - await expect(messageList).not.toHaveAttribute('aria-busy', 'true'); - } - - async typeSearch(name: string): Promise { - return this.searchInput.fill(name); - } - - getSearchRoomByName(name: string): Locator { - return this.searchList.getByRole('link', { name }); - } - - async openChat(name: string): Promise { - await this.typeSearch(name); - await this.getSearchRoomByName(name).click(); - await this.waitForChannel(); - } - async markItemAsUnread(item: Locator): Promise { await item.hover(); await item.focus(); diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-business-hours.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-business-hours.ts index a77784c4538ec..b411c9a2108ff 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-business-hours.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-business-hours.ts @@ -51,6 +51,10 @@ export class OmnichannelBusinessHours extends OmnichannelAdministration { return this.confirmDeleteModal.locator('role=button[name="Delete"]'); } + getCheckboxByLabel(name: string): Locator { + return this.page.locator('label', { has: this.page.getByRole('checkbox', { name }) }); + } + private selectOption(name: string): Locator { return this.page.locator(`[role=option][value="${name}"]`); } diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-canned-responses.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-canned-responses.ts index 0537090cc0c02..17713dd4b51e2 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-canned-responses.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-canned-responses.ts @@ -3,8 +3,49 @@ import type { Locator } from '@playwright/test'; import { OmnichannelAdministration } from './omnichannel-administration'; export class OmnichannelCannedResponses extends OmnichannelAdministration { - get radioPublic(): Locator { - return this.page.locator('[data-qa-id="canned-response-public-radio"]').first(); + get inputShortcut() { + return this.page.getByRole('textbox', { name: 'Shortcut', exact: true }); + } + + get inputMessage() { + return this.page.getByRole('textbox', { name: 'Message', exact: true }); + } + + get radioPublic() { + return this.page.locator('label', { has: this.page.getByRole('radio', { name: 'Public' }) }); + } + + get radioDepartment() { + return this.page.locator('label', { has: this.page.getByRole('radio', { name: 'Department' }) }); + } + + get radioPrivate() { + return this.page.locator('label', { has: this.page.getByRole('radio', { name: 'Private' }) }); + } + + get inputTags() { + return this.page.getByRole('textbox', { name: 'Tags', exact: true }); + } + + get btnAddTag() { + return this.page.getByRole('button', { name: 'Add', exact: true }); + } + + listItem(name: string) { + return this.page.getByText(`!${name}`, { exact: true }); + } + + async addTag(tag: string) { + await this.inputTags.fill(tag); + await this.btnAddTag.click(); + } + + get btnEdit() { + return this.page.getByRole('button', { name: 'Edit', exact: true }); + } + + get btnSave(): Locator { + return this.page.getByRole('button', { name: 'Save', exact: true }); } get btnNew(): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-contact-review-modal.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-contact-review-modal.ts new file mode 100644 index 0000000000000..520faa2891ada --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-contact-review-modal.ts @@ -0,0 +1,25 @@ +import type { Locator, Page } from '@playwright/test'; + +export class OmnichannelContactReviewModal { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + get btnSeeConflicts(): Locator { + return this.page.getByRole('button', { name: 'See conflicts', exact: true }); + } + + get btnSave(): Locator { + return this.page.getByRole('button', { name: 'Save', exact: true }); + } + + getFieldByName(name: string): Locator { + return this.page.getByLabel(name, { exact: true }); + } + + findOption(name: string): Locator { + return this.page.getByRole('option', { name, exact: true }); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts index 9bab98fcfe00a..dd2607719e5f2 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts @@ -170,4 +170,20 @@ export class OmnichannelDepartments { await this.inputUnit.click(); await this.findOption(unitName).click(); } + + get fieldGroupAgents() { + return this.page.getByLabel('Agents', { exact: true }); + } + + get inputAgents() { + return this.fieldGroupAgents.getByRole('textbox'); + } + + get btnAddAgent() { + return this.fieldGroupAgents.getByRole('button', { name: 'Add', exact: true }); + } + + findAgentRow(name: string) { + return this.page.locator('tr', { has: this.page.getByText(name, { exact: true }) }); + } } diff --git a/apps/meteor/tests/e2e/preview-public-channel.spec.ts b/apps/meteor/tests/e2e/preview-public-channel.spec.ts index fd24a2aae6202..774ab2d2ab782 100644 --- a/apps/meteor/tests/e2e/preview-public-channel.spec.ts +++ b/apps/meteor/tests/e2e/preview-public-channel.spec.ts @@ -2,7 +2,7 @@ import { IS_EE } from './config/constants'; import { Users } from './fixtures/userStates'; import { HomeChannel, Utils } from './page-objects'; import { Directory } from './page-objects/directory'; -import { createTargetChannel, sendTargetChannelMessage } from './utils'; +import { createDirectMessage, createTargetChannel, sendTargetChannelMessage } from './utils'; import { test, expect } from './utils/test'; test.use({ storageState: Users.admin.state }); @@ -44,6 +44,16 @@ test.describe('Preview public channel', () => { await expect(poHomeChannel.content.lastUserMessageBody).toContainText(targetChannelMessage); }); + test('should let user view direct rooms', async ({ api }) => { + await api.post('/permissions.update', { permissions: [{ _id: 'preview-c-room', roles: ['admin'] }] }); + await createDirectMessage(api); + + await poHomeChannel.sidenav.openChat(Users.user2.data.username); + + await expect(poHomeChannel.content.btnJoinChannel).not.toBeVisible(); + await expect(poHomeChannel.composer).toBeEnabled(); + }); + test('should not let user role preview public rooms', async ({ api }) => { await api.post('/permissions.update', { permissions: [{ _id: 'preview-c-room', roles: ['admin'] }] }); diff --git a/apps/meteor/tests/e2e/report-message.spec.ts b/apps/meteor/tests/e2e/report-message.spec.ts new file mode 100644 index 0000000000000..7d22c97db77c0 --- /dev/null +++ b/apps/meteor/tests/e2e/report-message.spec.ts @@ -0,0 +1,146 @@ +import { faker } from '@faker-js/faker'; +import type { Page } from '@playwright/test'; + +import { Users } from './fixtures/userStates'; +import { HomeChannel } from './page-objects'; +import { ReportMessageModal } from './page-objects/fragments'; +import { createTargetChannel, deleteChannel } from './utils'; +import { test, expect } from './utils/test'; + +test.use({ storageState: Users.user1.state }); + +test.describe.serial('report message', () => { + let poHomeChannel: HomeChannel; + let targetChannel: string; + let adminPage: Page; + let reportModal: ReportMessageModal; + + test.beforeAll(async ({ api, browser }) => { + targetChannel = await createTargetChannel(api, { members: ['user1', 'admin'] }); + adminPage = await browser.newPage({ storageState: Users.admin.state }); + reportModal = new ReportMessageModal(adminPage); + }); + + test.afterAll(async ({ api }) => { + await Promise.all([ + api.post('/moderation.user.deleteReportedMessages', { + userId: 'user1', + }), + deleteChannel(api, targetChannel), + adminPage.close(), + ]); + }); + + test.beforeEach(async ({ page }) => { + poHomeChannel = new HomeChannel(page); + + await page.goto('/home'); + await adminPage.goto('/home'); + }); + + test('should show report message option in message menu for other users messages', async () => { + await test.step('send message as user1', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + const testMessage = faker.lorem.sentence(); + await poHomeChannel.content.sendMessage(testMessage); + }); + + await test.step('verify report option is visible for the other user', async () => { + const adminHomeChannel = new HomeChannel(adminPage); + await adminHomeChannel.sidenav.openChat(targetChannel); + await adminHomeChannel.content.openLastMessageMenu(); + await expect(adminPage.getByRole('menuitem', { name: 'Report' })).toBeVisible(); + }); + }); + + test('should not show report message option in message menu for own messages', async ({ page }) => { + await test.step('send message as user1', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + const testMessage = faker.lorem.sentence(); + await poHomeChannel.content.sendMessage(testMessage); + }); + + await test.step('verify report option is not visible for own message', async () => { + await poHomeChannel.content.openLastMessageMenu(); + await expect(page.getByRole('menuitem', { name: 'Report' })).not.toBeVisible(); + }); + }); + + test('should validate empty report description', async () => { + await test.step('send message as user1', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + const testMessage = faker.lorem.sentence(); + await poHomeChannel.content.sendMessage(testMessage); + }); + + await test.step('try to submit empty report', async () => { + const adminHomeChannel = new HomeChannel(adminPage); + await adminHomeChannel.sidenav.openChat(targetChannel); + + await adminHomeChannel.content.openLastMessageMenu(); + await adminPage.getByRole('menuitem', { name: 'Report' }).click(); + + await reportModal.btnSubmitReport.click(); + await expect(reportModal.reportDescriptionError).toBeVisible(); + }); + }); + + test('should be able to cancel reporting a message', async () => { + await test.step('send message as user1', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + const testMessage = faker.lorem.sentence(); + await poHomeChannel.content.sendMessage(testMessage); + }); + + await test.step('open and cancel report modal', async () => { + const adminHomeChannel = new HomeChannel(adminPage); + await adminHomeChannel.sidenav.openChat(targetChannel); + + await adminHomeChannel.content.openLastMessageMenu(); + await adminPage.getByRole('menuitem', { name: 'Report' }).click(); + + await expect(reportModal.modalTitle).toBeVisible(); + await reportModal.btnCancelReport.click(); + await expect(reportModal.modalTitle).not.toBeVisible(); + }); + }); + + test('should successfully report a message and verify its appearance in moderation console', async () => { + let testMessage: string; + let reportDescription: string; + + await test.step('send message as user1', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + testMessage = faker.lorem.sentence(); + await poHomeChannel.content.sendMessage(testMessage); + }); + + await test.step('report message as the other user', async () => { + const adminHomeChannel = new HomeChannel(adminPage); + await adminHomeChannel.sidenav.openChat(targetChannel); + + await adminHomeChannel.content.openLastMessageMenu(); + await adminPage.getByRole('menuitem', { name: 'Report' }).click(); + + reportDescription = faker.lorem.sentence(); + await reportModal.inputReportDescription.fill(reportDescription); + + await reportModal.btnSubmitReport.click(); + + await expect(adminPage.getByText('Report has been sent')).toBeVisible(); + }); + + await test.step('verify report in moderation console', async () => { + await adminPage.goto('/admin/moderation/messages'); + + await expect(adminPage.getByRole('tab', { name: 'Reported messages' })).toBeVisible(); + await expect(adminPage.getByRole('link', { name: 'user1' })).toBeVisible(); + await adminPage.getByRole('link', { name: 'user1' }).click(); + + await expect(adminPage.getByText(testMessage)).toBeVisible(); + + await adminPage.getByRole('button', { name: 'Show reports' }).click(); + await expect(adminPage.getByText(reportDescription)).toBeVisible(); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/search-discussion.spec.ts b/apps/meteor/tests/e2e/search-discussion.spec.ts index 0d645432d777e..a2ec674805296 100644 --- a/apps/meteor/tests/e2e/search-discussion.spec.ts +++ b/apps/meteor/tests/e2e/search-discussion.spec.ts @@ -2,7 +2,7 @@ import type { Page } from '@playwright/test'; import { Users } from './fixtures/userStates'; import { HomeChannel } from './page-objects'; -import { createTargetDiscussion } from './utils'; +import { createTargetDiscussion, deleteRoom } from './utils'; import { getSettingValueById } from './utils/getSettingValueById'; import { setSettingValueById } from './utils/setSettingValueById'; import { test, expect } from './utils/test'; @@ -12,26 +12,27 @@ test.use({ storageState: Users.user1.state }); test.describe.serial('search-discussion', () => { let settingDefaultValue: unknown; let poHomeChannel: HomeChannel; - let discussionName: string; + let discussion: Record; test.beforeAll(async ({ api }) => { settingDefaultValue = await getSettingValueById(api, 'UI_Allow_room_names_with_special_chars'); }); test.beforeEach(async ({ page, api }) => { - discussionName = await createTargetDiscussion(api); + discussion = await createTargetDiscussion(api); poHomeChannel = new HomeChannel(page); await page.goto('/home'); }); test.afterAll(async ({ api }) => { await setSettingValueById(api, 'UI_Allow_room_names_with_special_chars', settingDefaultValue); + await deleteRoom(api, discussion._id); }); const testDiscussionSearch = async (page: Page) => { await poHomeChannel.sidenav.openSearch(); - await poHomeChannel.sidenav.inputSearch.type(discussionName); - const targetSearchItem = page.locator('role=listbox').getByText(discussionName).first(); + await poHomeChannel.sidenav.inputSearch.fill(discussion.fname); + const targetSearchItem = page.locator('role=listbox').getByText(discussion.fname).first(); await expect(targetSearchItem).toBeVisible(); }; diff --git a/apps/meteor/tests/e2e/sidebar.spec.ts b/apps/meteor/tests/e2e/sidebar.spec.ts index 29fd1a1bc0f59..b45f2caa7f89b 100644 --- a/apps/meteor/tests/e2e/sidebar.spec.ts +++ b/apps/meteor/tests/e2e/sidebar.spec.ts @@ -14,6 +14,14 @@ test.describe.serial('sidebar', () => { await page.waitForSelector('main'); }); + test('should sidebar`s toolbar buttons not be disabled in tablet view', async ({ page }) => { + await page.setViewportSize({ width: 1023, height: 767 }); + + await expect(poHomeChannel.sidenav.btnDisplay).not.toBeDisabled(); + await expect(poHomeChannel.sidenav.btnCreateNew).not.toBeDisabled(); + await expect(poHomeChannel.sidenav.btnAdministration).not.toBeDisabled(); + }); + test('should navigate on sidebar toolbar using arrow keys', async ({ page }) => { await poHomeChannel.sidenav.userProfileMenu.focus(); await page.keyboard.press('Tab'); diff --git a/apps/meteor/tests/e2e/utils/create-target-channel.ts b/apps/meteor/tests/e2e/utils/create-target-channel.ts index 777bb99e226d5..402d45a2731ba 100644 --- a/apps/meteor/tests/e2e/utils/create-target-channel.ts +++ b/apps/meteor/tests/e2e/utils/create-target-channel.ts @@ -15,6 +15,14 @@ export async function createTargetChannel(api: BaseTest['api'], options?: Omit, +): Promise<{ channel: IRoom }> { + const name = faker.string.uuid(); + return (await api.post('/channels.create', { name, ...options })).json(); +} + export async function sendTargetChannelMessage(api: BaseTest['api'], roomName: string, options?: Partial) { const response = await api.get(`/channels.info?roomName=${roomName}`); @@ -37,6 +45,10 @@ export async function deleteChannel(api: BaseTest['api'], roomName: string): Pro await api.post('/channels.delete', { roomName }); } +export async function deleteRoom(api: BaseTest['api'], roomId: string): Promise { + await api.post('/rooms.delete', { roomId }); +} + export async function createTargetPrivateChannel(api: BaseTest['api'], options?: Omit): Promise { const name = faker.string.uuid(); await api.post('/groups.create', { name, ...options }); @@ -64,13 +76,30 @@ export async function createDirectMessage(api: BaseTest['api']): Promise { }); } -export async function createTargetDiscussion(api: BaseTest['api']): Promise { +export async function createTargetDiscussion(api: BaseTest['api']): Promise> { const channelName = faker.string.uuid(); const discussionName = faker.string.uuid(); - const response = await api.post('/channels.create', { name: channelName }); - const { channel } = await response.json(); - await api.post('/rooms.createDiscussion', { t_name: discussionName, prid: channel._id }); + const channelResponse = await api.post('/channels.create', { name: channelName }); + const { channel } = await channelResponse.json(); + const discussionResponse = await api.post('/rooms.createDiscussion', { t_name: discussionName, prid: channel._id }); + const { discussion } = await discussionResponse.json(); + + if (!discussion) { + throw new Error('Discussion not created'); + } + + return discussion; +} + +export async function createChannelWithTeam(api: BaseTest['api']): Promise> { + const channelName = faker.string.uuid(); + const teamName = faker.string.uuid(); + + const teamResponse = await api.post('/teams.create', { name: teamName, type: 1, members: ['user2'] }); + const { team } = await teamResponse.json(); + + await api.post('/channels.create', { name: channelName, members: ['user1'], extraData: { teamId: team._id } }); - return discussionName; + return { channelName, teamName }; } diff --git a/apps/meteor/tests/e2e/utils/omnichannel/custom-field.ts b/apps/meteor/tests/e2e/utils/omnichannel/custom-field.ts new file mode 100644 index 0000000000000..bc476aae918c8 --- /dev/null +++ b/apps/meteor/tests/e2e/utils/omnichannel/custom-field.ts @@ -0,0 +1,54 @@ +import type { ILivechatCustomField } from '@rocket.chat/core-typings'; + +import { parseMeteorResponse } from '../parseMeteorResponse'; +import type { BaseTest } from '../test'; + +type CustomField = Omit & { field: string }; + +export const removeCustomField = (api: BaseTest['api'], id: string) => { + return api.post('/method.call/livechat:deleteCustomField', { + method: 'livechat:saveCustomField', + params: [id], + id: 'id', + msg: 'method', + }); +}; + +export const createCustomField = async (api: BaseTest['api'], overwrides: Partial) => { + const response = await api.post('/method.call/livechat:saveCustomField', { + message: JSON.stringify({ + method: 'livechat:saveCustomField', + params: [ + null, + { + field: overwrides.field, + label: overwrides.label || overwrides.field, + visibility: 'visible', + scope: 'visitor', + searchable: false, + regexp: '', + type: 'input', + required: false, + defaultValue: '', + options: '', + public: false, + ...overwrides, + }, + ], + id: 'id', + msg: 'method', + }), + }); + + if (!response.ok()) { + throw new Error(`Failed to create custom field [http status: ${response.status()}]`); + } + + const customField = await parseMeteorResponse(response); + + return { + response, + customField, + delete: () => removeCustomField(api, customField._id), + }; +}; diff --git a/apps/meteor/tests/e2e/video-conference-ring.spec.ts b/apps/meteor/tests/e2e/video-conference-ring.spec.ts index d5072b00b5cca..49b7d15a751a1 100644 --- a/apps/meteor/tests/e2e/video-conference-ring.spec.ts +++ b/apps/meteor/tests/e2e/video-conference-ring.spec.ts @@ -10,13 +10,11 @@ test.use({ storageState: Users.user1.state }); test.describe('video conference ringing', () => { let poHomeChannel: HomeChannel; - let poAccountProfile: AccountProfile; test.skip(!IS_EE, 'Enterprise Only'); test.beforeEach(async ({ page }) => { poHomeChannel = new HomeChannel(page); - poAccountProfile = new AccountProfile(page); await page.goto('/home'); }); @@ -36,8 +34,7 @@ test.describe('video conference ringing', () => { await auxContext.poHomeChannel.sidenav.openChat('user1'); await test.step('should user1 calls user2', async () => { - await poHomeChannel.content.btnCall.click(); - await poHomeChannel.content.menuItemVideoCall.click(); + await poHomeChannel.content.btnVideoCall.click(); await poHomeChannel.content.btnStartVideoCall.click(); await expect(poHomeChannel.content.getVideoConfPopupByName('Calling user2')).toBeVisible(); @@ -51,39 +48,4 @@ test.describe('video conference ringing', () => { await expect(poHomeChannel.content.getVideoConfPopupByName('Start a call with user2')).toBeVisible(); }); }); - - const changeCallRingerVolumeFromHome = async (poHomeChannel: HomeChannel, poAccountProfile: AccountProfile, volume: string) => { - await poHomeChannel.sidenav.userProfileMenu.click(); - await poHomeChannel.sidenav.accountPreferencesOption.click(); - - await poAccountProfile.preferencesSoundAccordionOption.click(); - await poAccountProfile.preferencesCallRingerVolumeSlider.fill(volume); - - await poAccountProfile.btnSaveChanges.click(); - await poAccountProfile.btnClose.click(); - }; - - test('should be ringing/dialing according to volume preference', async () => { - await changeCallRingerVolumeFromHome(poHomeChannel, poAccountProfile, '50'); - await changeCallRingerVolumeFromHome(auxContext.poHomeChannel, auxContext.poAccountProfile, '25'); - - await poHomeChannel.sidenav.openChat('user2'); - await auxContext.poHomeChannel.sidenav.openChat('user1'); - - await poHomeChannel.content.btnCall.click(); - await poHomeChannel.content.menuItemVideoCall.click(); - await poHomeChannel.content.btnStartVideoCall.click(); - - await expect(auxContext.poHomeChannel.content.getVideoConfPopupByName('Incoming call from user1')).toBeVisible(); - - const dialToneVolume = await poHomeChannel.audioVideoConfDialtone.evaluate((el: HTMLAudioElement) => el.volume); - const ringToneVolume = await auxContext.poHomeChannel.audioVideoConfRingtone.evaluate((el: HTMLAudioElement) => el.volume); - - expect(dialToneVolume).toBe(0.5); - expect(ringToneVolume).toBe(0.25); - - await auxContext.poHomeChannel.content.btnDeclineVideoCall.click(); - await changeCallRingerVolumeFromHome(poHomeChannel, poAccountProfile, '100'); - await changeCallRingerVolumeFromHome(auxContext.poHomeChannel, auxContext.poAccountProfile, '100'); - }); }); diff --git a/apps/meteor/tests/e2e/video-conference.spec.ts b/apps/meteor/tests/e2e/video-conference.spec.ts index 63ebdd2966dc0..2c81b7b66159f 100644 --- a/apps/meteor/tests/e2e/video-conference.spec.ts +++ b/apps/meteor/tests/e2e/video-conference.spec.ts @@ -29,8 +29,7 @@ test.describe('video conference', () => { test('expect create video conference in a "targetChannel"', async () => { await poHomeChannel.sidenav.openChat(targetChannel); - await poHomeChannel.content.btnCall.click(); - await poHomeChannel.content.menuItemVideoCall.click(); + await poHomeChannel.content.btnVideoCall.click(); await poHomeChannel.content.btnStartVideoCall.click(); await expect(poHomeChannel.content.videoConfMessageBlock.last()).toBeVisible(); }); @@ -64,8 +63,7 @@ test.describe('video conference', () => { test('expect create video conference in a direct', async () => { await poHomeChannel.sidenav.openChat('user2'); - await poHomeChannel.content.btnCall.click(); - await poHomeChannel.content.menuItemVideoCall.click(); + await poHomeChannel.content.btnVideoCall.click(); await poHomeChannel.content.btnStartVideoCall.click(); await expect(poHomeChannel.content.videoConfMessageBlock.last()).toBeVisible(); }); @@ -81,8 +79,7 @@ test.describe('video conference', () => { test('expect create video conference in a "targetTeam"', async () => { await poHomeChannel.sidenav.openChat(targetTeam); - await poHomeChannel.content.btnCall.click(); - await poHomeChannel.content.menuItemVideoCall.click(); + await poHomeChannel.content.btnVideoCall.click(); await poHomeChannel.content.btnStartVideoCall.click(); await expect(poHomeChannel.content.videoConfMessageBlock.last()).toBeVisible(); }); @@ -98,8 +95,7 @@ test.describe('video conference', () => { test('expect create video conference in a direct multiple', async () => { await poHomeChannel.sidenav.openChat('rocketchat.internal.admin.test, user2'); - await poHomeChannel.content.btnCall.click(); - await poHomeChannel.content.menuItemVideoCall.click(); + await poHomeChannel.content.btnVideoCall.click(); await poHomeChannel.content.btnStartVideoCall.click(); await expect(poHomeChannel.content.videoConfMessageBlock.last()).toBeVisible(); }); @@ -115,6 +111,6 @@ test.describe('video conference', () => { test('expect create video conference not available in a "targetReadOnlyChannel"', async () => { await poHomeChannel.sidenav.openChat(targetReadOnlyChannel); - await expect(poHomeChannel.content.btnCall).hasAttribute('disabled'); + await expect(poHomeChannel.content.btnVideoCall).hasAttribute('disabled'); }); }); diff --git a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts index 952d21b386843..4921230011576 100644 --- a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts +++ b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts @@ -1,5 +1,7 @@ import type { Credentials } from '@rocket.chat/api-client'; -import type { IIntegration, IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import { TEAM_TYPE } from '@rocket.chat/core-typings'; +import type { AtLeast, IIntegration, IMessage, IRoom, ITeam, IUser } from '@rocket.chat/core-typings'; +import { Random } from '@rocket.chat/random'; import { assert, expect } from 'chai'; import { after, before, describe, it } from 'mocha'; @@ -7,6 +9,7 @@ import { getCredentials, api, request, credentials } from '../../data/api-data'; import { createIntegration, removeIntegration } from '../../data/integration.helper'; import { updatePermission } from '../../data/permissions.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; +import { createTeam, deleteTeam } from '../../data/teams.helper'; import { password } from '../../data/user'; import type { TestUser } from '../../data/users.helper'; import { createUser, deleteUser, login } from '../../data/users.helper'; @@ -330,6 +333,146 @@ describe('[Incoming Integrations]', () => { }); }); }); + + it('should send a message if the payload is a application/x-www-form-urlencoded JSON AND the integration has a valid script', async () => { + const payload = { msg: `Message as x-www-form-urlencoded JSON sent successfully at #${Date.now()}` }; + let withScript: IIntegration | undefined; + + await updatePermission('manage-incoming-integrations', ['admin']); + await request + .post(api('integrations.create')) + .set(credentials) + .send({ + type: 'webhook-incoming', + name: 'Incoming test with script', + enabled: true, + alias: 'test', + username: 'rocket.cat', + scriptEnabled: true, + overrideDestinationChannelEnabled: false, + channel: '#general', + script: ` + class Script { + process_incoming_request({ request }) { + return { + content:{ + text: request.content.text + } + }; + } + } + `, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('integration').and.to.be.an('object'); + withScript = res.body.integration; + }); + + if (!withScript) { + throw new Error('Integration not created'); + } + + await request + .post(`/hooks/${withScript._id}/${withScript.token}`) + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(`payload=${JSON.stringify(payload)}`) + .expect(200) + .expect(async () => { + return request + .get(api('channels.messages')) + .set(credentials) + .query({ + roomId: 'GENERAL', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').and.to.be.an('array'); + expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === payload.msg)).to.be.true; + }); + }); + + await removeIntegration(withScript._id, 'incoming'); + }); + + it('should send a message if the payload is a application/x-www-form-urlencoded JSON(when not set, default one) but theres no "payload" key, its just a string, the integration has a valid script', async () => { + const payload = { test: 'test' }; + let withScript: IIntegration | undefined; + + await updatePermission('manage-incoming-integrations', ['admin']); + await request + .post(api('integrations.create')) + .set(credentials) + .send({ + type: 'webhook-incoming', + name: 'Incoming test with script and default content-type', + enabled: true, + alias: 'test', + username: 'rocket.cat', + scriptEnabled: true, + overrideDestinationChannelEnabled: false, + channel: '#general', + script: + 'const buildMessage = (obj) => {\n' + + ' \n' + + ' const template = `[#VALUE](${ obj.test })`;\n' + + ' \n' + + ' return {\n' + + ' text: template\n' + + ' };\n' + + ' };\n' + + ' \n' + + ' class Script {\n' + + ' process_incoming_request({ request }) {\n' + + ' msg = buildMessage(request.content);\n' + + ' \n' + + ' return {\n' + + ' content:{\n' + + ' text: msg.text\n' + + ' }\n' + + ' };\n' + + ' }\n' + + ' }\n' + + ' \n', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('integration').and.to.be.an('object'); + withScript = res.body.integration; + }); + + if (!withScript) { + throw new Error('Integration not created'); + } + + await request + .post(`/hooks/${withScript._id}/${withScript.token}`) + .send(JSON.stringify(payload)) + .expect(200) + .expect(async () => { + return request + .get(api('channels.messages')) + .set(credentials) + .query({ + roomId: 'GENERAL', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').and.to.be.an('array'); + expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === '[#VALUE](test)')).to.be.true; + }); + }); + + await removeIntegration(withScript._id, 'incoming'); + }); }); describe('[/integrations.history]', () => { @@ -541,6 +684,16 @@ describe('[Incoming Integrations]', () => { }); describe('[/integrations.update]', () => { + let senderUser: IUser; + let sendUserCredentials: Credentials; + + before(async () => { + senderUser = await createUser(); + sendUserCredentials = await login(senderUser.username, password); + }); + + after(() => deleteUser(senderUser)); + it('should update an integration by id and return the new data', (done) => { void request .put(api('integrations.update')) @@ -564,6 +717,7 @@ describe('[Incoming Integrations]', () => { expect(res.body.integration._id).to.be.equal(integration._id); expect(res.body.integration.name).to.be.equal('Incoming test updated'); expect(res.body.integration.alias).to.be.equal('test updated'); + integration = res.body.integration; }) .end(done); }); @@ -584,6 +738,60 @@ describe('[Incoming Integrations]', () => { }) .end(done); }); + + it("should update an integration's username and associated userId correctly and return the new data", async () => { + await request + .put(api('integrations.update')) + .set(credentials) + .send({ + type: 'webhook-incoming', + name: 'Incoming test updated x2', + enabled: true, + alias: 'test updated x2', + username: senderUser.username, + scriptEnabled: true, + overrideDestinationChannelEnabled: true, + channel: '#general', + integrationId: integration._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('integration'); + expect(res.body.integration._id).to.be.equal(integration._id); + expect(res.body.integration.name).to.be.equal('Incoming test updated x2'); + expect(res.body.integration.alias).to.be.equal('test updated x2'); + expect(res.body.integration.username).to.be.equal(senderUser.username); + expect(res.body.integration.userId).to.be.equal(sendUserCredentials['X-User-Id']); + integration = res.body.integration; + }); + }); + + it('should send messages to the channel under the updated username', async () => { + const successfulMesssage = `Message sent successfully at #${Random.id()}`; + await request + .post(`/hooks/${integration._id}/${integration.token}`) + .send({ + text: successfulMesssage, + }) + .expect(200); + + await request + .get(api('channels.messages')) + .set(credentials) + .query({ + roomId: 'GENERAL', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').and.to.be.an('array'); + const message = (res.body.messages as IMessage[]).find((m) => m.msg === successfulMesssage); + expect(message?.u).have.property('username', senderUser.username); + }); + }); }); describe('[/integrations.remove]', () => { @@ -680,4 +888,267 @@ describe('[Incoming Integrations]', () => { }); }); }); + + describe('Additional Tests for Message Delivery Permissions', () => { + let nonMemberUser: IUser; + let privateTeam: ITeam; + let publicChannelInPrivateTeam: IRoom; + let privateRoom: IRoom; + let publicRoom: IRoom; + let integration2: IIntegration; + let integration3: IIntegration; + let integration4: IIntegration; + + before(async () => { + nonMemberUser = await createUser({ username: `g_${Random.id()}` }); + privateTeam = await createTeam(credentials, `private.team.${Random.id()}`, TEAM_TYPE.PRIVATE); + + const [publicInPrivateResponse, privateRoomResponse, publicRoomResponse] = await Promise.all([ + createRoom({ + type: 'c', + name: `teamPrivate.publicChannel.${Random.id()}`, + credentials, + extraData: { + teamId: privateTeam._id, + }, + }), + createRoom({ + type: 'p', + name: `privateChannel.${Random.id()}`, + credentials, + }), + createRoom({ + type: 'c', + name: `publicChannel.${Random.id()}`, + credentials, + }), + ]); + + publicChannelInPrivateTeam = publicInPrivateResponse.body.channel; + privateRoom = privateRoomResponse.body.group; + publicRoom = publicRoomResponse.body.channel; + + await updatePermission('manage-incoming-integrations', ['admin']); + await request + .post(api('integrations.create')) + .set(credentials) + .send({ + type: 'webhook-incoming', + name: 'Incoming test 2 - Sending Messages', + enabled: true, + alias: 'Incoming test 2 - Sending Messages', + username: nonMemberUser.username as string, + scriptEnabled: false, + overrideDestinationChannelEnabled: false, + channel: `#${privateRoom.fname}`, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('integration').and.to.be.an('object'); + integration2 = res.body.integration; + }); + + await request + .post(api('integrations.create')) + .set(credentials) + .send({ + type: 'webhook-incoming', + name: 'Incoming test 3 - Sending Messages', + enabled: true, + alias: 'Incoming test 3 - Sending Messages', + username: nonMemberUser.username as string, + scriptEnabled: false, + overrideDestinationChannelEnabled: false, + channel: `#${publicChannelInPrivateTeam.fname}`, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('integration').and.to.be.an('object'); + integration3 = res.body.integration; + }); + await request + .post(api('integrations.create')) + .set(credentials) + .send({ + type: 'webhook-incoming', + name: 'Incoming test 4 - Sending Messages', + enabled: true, + alias: 'Incoming test 4 - Sending Messages', + username: nonMemberUser.username as string, + scriptEnabled: false, + overrideDestinationChannelEnabled: false, + channel: `#${publicRoom.fname}`, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('integration').and.to.be.an('object'); + integration4 = res.body.integration; + }); + }); + + after(async () => { + await Promise.all( + [publicRoom, privateRoom, publicChannelInPrivateTeam].map((room) => deleteRoom({ type: room.t as 'c' | 'p', roomId: room._id })), + ); + await deleteTeam(credentials, privateTeam.name); + await deleteUser(nonMemberUser); + await Promise.all([ + ...[integration2, integration3, integration4].map((integration) => + request.post(api('integrations.remove')).set(credentials).send({ + integrationId: integration._id, + type: integration.type, + }), + ), + updatePermission('manage-incoming-integrations', ['admin']), + ]); + }); + + it('should not send a message to a private rooms on behalf of a non member', async () => { + const successfulMesssage = `Message sent successfully at #${Random.id()}`; + await request + .post(`/hooks/${integration2._id}/${integration2.token}`) + .send({ + text: successfulMesssage, + }) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('error', 'error-not-allowed'); + }); + await request + .get(api('groups.messages')) + .set(credentials) + .query({ + roomId: privateRoom._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').and.to.be.an('array'); + expect((res.body.messages as IMessage[]).find((m) => m.msg === successfulMesssage)).to.be.undefined; + }); + }); + + it('should not add non member to private rooms when sending message', async () => { + const successfulMesssage = `Message sent successfully at #${Random.id()}`; + await request + .post(`/hooks/${integration2._id}/${integration2.token}`) + .send({ + text: successfulMesssage, + }) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('error', 'error-not-allowed'); + }); + await request + .get(api('groups.members')) + .set(credentials) + .query({ + roomId: privateRoom._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + expect((res.body.members as AtLeast[]).find((m) => m._id === nonMemberUser._id)).to.be.undefined; + }); + }); + + it('should not send a message to public channel of a private team on behalf of a non team member', async () => { + const successfulMesssage = `Message sent successfully at #${Random.id()}`; + await request + .post(`/hooks/${integration3._id}/${integration3.token}`) + .send({ + text: successfulMesssage, + }) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('error', 'error-not-allowed'); + }); + await request + .get(api('channels.messages')) + .set(credentials) + .query({ + roomId: publicChannelInPrivateTeam._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').and.to.be.an('array'); + expect((res.body.messages as IMessage[]).find((m) => m.msg === successfulMesssage)).to.be.undefined; + }); + }); + + it('should not add non team member to the public channel in a private team when sending message', async () => { + const successfulMesssage = `Message sent successfully at #${Random.id()}`; + await request + .post(`/hooks/${integration3._id}/${integration3.token}`) + .send({ + text: successfulMesssage, + }) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('error', 'error-not-allowed'); + }); + await request + .get(api('channels.members')) + .set(credentials) + .query({ + roomId: publicChannelInPrivateTeam._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + expect((res.body.members as AtLeast[]).find((m) => m._id === nonMemberUser._id)).to.be.undefined; + }); + }); + + it('should send messages from non-members to public rooms and add them as room members', async () => { + const successfulMesssage = `Message sent successfully at #${Random.id()}`; + await request + .post(`/hooks/${integration4._id}/${integration4.token}`) + .send({ + text: successfulMesssage, + }) + .expect(200); + + await request + .get(api('channels.messages')) + .set(credentials) + .query({ + roomId: publicRoom._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').and.to.be.an('array'); + expect((res.body.messages as IMessage[]).find((m) => m.msg === successfulMesssage)).not.to.be.undefined; + }); + + await request + .get(api('channels.members')) + .set(credentials) + .query({ + roomId: publicRoom._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + expect((res.body.members as AtLeast[]).find((m) => m._id === nonMemberUser._id)).not.to.be.undefined; + }); + }); + }); }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index fc1413cd870c4..bff5aa27b184f 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -40,6 +40,7 @@ import { makeAgentUnavailable, sendAgentMessage, fetchInquiry, + takeInquiry, } from '../../../data/livechat/rooms'; import { saveTags } from '../../../data/livechat/tags'; import { createMonitor, createUnit, deleteUnit } from '../../../data/livechat/units'; @@ -82,7 +83,7 @@ describe('LIVECHAT - rooms', () => { if (IS_EE) { // install the app await request - .post(apps('/')) + .post(apps()) .set(credentials) .send({ url: APP_URL }) .expect('Content-Type', 'application/json') @@ -380,6 +381,33 @@ describe('LIVECHAT - rooms', () => { expect(body.rooms.every((room: IOmnichannelRoom) => room.open)).to.be.true; expect(body.rooms.find((froom: IOmnichannelRoom) => froom._id === room._id)).to.be.undefined; }); + + describe('roomName filter', () => { + let room1: IOmnichannelRoom; + let room2: IOmnichannelRoom; + before(async () => { + const visitor = await createVisitor(undefined, 'TEST_1'); + const visitor2 = await createVisitor(undefined, 'TEST_2'); + room1 = await createLivechatRoom(visitor.token); + room2 = await createLivechatRoom(visitor2.token); + }); + + it('should return only rooms matching exact term when roomName is between quotes', async () => { + const { body } = await request.get(api('livechat/rooms')).query({ roomName: `"TEST_1"` }).set(credentials).expect(200); + expect(body.rooms[0].fname).to.equal('TEST_1'); + expect(body.rooms.find((r: IOmnichannelRoom) => r.fname === 'TEST_2')).to.be.undefined; + }); + it('should return rooms matching using regex when searching by roomName', async () => { + const { body } = await request.get(api('livechat/rooms')).query({ roomName: `TEST_` }).set(credentials).expect(200); + expect(body.rooms.find((froom: IOmnichannelRoom) => froom._id === room1._id)).to.be.not.undefined; + expect(body.rooms.find((froom: IOmnichannelRoom) => froom._id === room2._id)).to.be.not.undefined; + }); + it('should return empty if no room matches term', async () => { + const { body } = await request.get(api('livechat/rooms')).query({ roomName: `"TEST_"` }).set(credentials).expect(200); + expect(body.rooms).to.be.empty; + }); + }); + it('should return both closed/open when open param is not passed', async () => { // Create and close a room const visitor = await createVisitor(); @@ -1170,6 +1198,67 @@ describe('LIVECHAT - rooms', () => { await deleteDepartment(forwardToOfflineDepartment._id); }); + (IS_EE ? it : it.skip)( + 'should update inquiry last message when manager forward to offline department and the inquiry returns to queued', + async () => { + await updateSetting('Livechat_Routing_Method', 'Manual_Selection'); + const { department: initialDepartment, agent } = await createDepartmentWithAnOnlineAgent(); + const { department: forwardToOfflineDepartment, agent: offlineAgent } = await createDepartmentWithAnOfflineAgent({ + allowReceiveForwardOffline: true, + }); + + const newVisitor = await createVisitor(initialDepartment._id); + const newRoom = await createLivechatRoom(newVisitor.token); + + const inq = await fetchInquiry(newRoom._id); + await takeInquiry(inq._id, agent.credentials); + + const msgText = `return to queue ${Date.now()}`; + await request.post(api('livechat/message')).send({ token: newVisitor.token, rid: newRoom._id, msg: msgText }).expect(200); + + await makeAgentUnavailable(offlineAgent.credentials); + + const manager = await createUser(); + const managerCredentials = await login(manager.username, password); + await createManager(manager.username); + + await request.post(api('livechat/room.forward')).set(managerCredentials).send({ + roomId: newRoom._id, + departmentId: forwardToOfflineDepartment._id, + clientAction: true, + comment: 'test comment', + }); + + await request + .get(api(`livechat/queue`)) + .set(credentials) + .query({ + count: 1, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body.queue).to.be.an('array'); + expect(res.body.queue[0].chats).not.to.undefined; + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('count'); + }); + + const inquiry = await fetchInquiry(newRoom._id); + + expect(inquiry).to.have.property('_id', inquiry._id); + expect(inquiry).to.have.property('rid', newRoom._id); + expect(inquiry).to.have.property('lastMessage'); + expect(inquiry.lastMessage).to.have.property('msg', ''); + expect(inquiry.lastMessage).to.have.property('t', 'livechat_transfer_history'); + + await deleteDepartment(initialDepartment._id); + await deleteDepartment(forwardToOfflineDepartment._id); + }, + ); + let roomId: string; let visitorToken: string; (IS_EE ? it : it.skip)('should return a success message when transferring to a fallback department', async () => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/03-custom-fields.ts b/apps/meteor/tests/end-to-end/api/livechat/03-custom-fields.ts index 75c1eaa622906..d18e1aa4974ad 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/03-custom-fields.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/03-custom-fields.ts @@ -1,3 +1,4 @@ +import type { ILivechatVisitor } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { before, describe, it } from 'mocha'; import type { Response } from 'supertest'; @@ -124,7 +125,13 @@ describe('LIVECHAT - custom fields', () => { await request.post(api('livechat/custom.fields')).send({ token: 'invalid-token' }).expect(400); }); it('should fail if customFields is not an array', async () => { - await request.post(api('livechat/custom.fields')).send({ token: 'invalid-token', customFields: 'invalid-custom-fields' }).expect(400); + await request + .post(api('livechat/custom.fields')) + .send({ + token: 'invalid-token', + customFields: 'invalid-custom-fields', + }) + .expect(400); }); it('should fail if customFields is an empty array', async () => { await request.post(api('livechat/custom.fields')).send({ token: 'invalid-token', customFields: [] }).expect(400); @@ -138,7 +145,10 @@ describe('LIVECHAT - custom fields', () => { it('should fail if token is not a valid token', async () => { await request .post(api('livechat/custom.fields')) - .send({ token: 'invalid-token', customFields: [{ key: 'invalid-key', value: 'invalid-value', overwrite: true }] }) + .send({ + token: 'invalid-token', + customFields: [{ key: 'invalid-key', value: 'invalid-value', overwrite: true }], + }) .expect(400); }); it('should fail when customFields.key is invalid', async () => { @@ -166,7 +176,10 @@ describe('LIVECHAT - custom fields', () => { const { body } = await request .post(api('livechat/custom.fields')) - .send({ token: visitor.token, customFields: [{ key: customFieldName, value: 'test_address', overwrite: true }] }) + .send({ + token: visitor.token, + customFields: [{ key: customFieldName, value: 'test_address', overwrite: true }], + }) .expect(200); expect(body).to.have.property('success', true); @@ -176,4 +189,231 @@ describe('LIVECHAT - custom fields', () => { expect(body.fields[0]).to.have.property('value', 'test_address'); }); }); + + describe('livechat/custom.field [with Contacts]', () => { + let visitor: ILivechatVisitor; + let contactId: string; + + const customFieldName = `custom_field_${Date.now()}`; + const customFieldValue = 'custom-field-value'; + + before(async () => { + await updatePermission('create-livechat-contact', ['admin']); + await updatePermission('view-livechat-contact', ['admin']); + + // Create a Visitor + visitor = await createVisitor(); + + // Create a Contact and store id on var + await request + .post(api('omnichannel/contacts')) + .set(credentials) + .send({ + name: visitor.name, + emails: [visitor.visitorEmails?.[0].address], + phones: [visitor.phone?.[0].phoneNumber], + }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('contactId'); + + contactId = res.body.contactId; + }); + + await request.get(api('livechat/room')).query({ token: visitor.token }); + + // Create Custom Field + await createCustomField({ + searchable: true, + field: customFieldName, + label: customFieldName, + defaultValue: 'test_default_address', + scope: 'visitor', + visibility: 'public', + regexp: '', + }); + + await createCustomField({ + searchable: true, + field: `${customFieldName}_2`, + label: `${customFieldName}_2`, + defaultValue: 'test_default_address', + scope: 'visitor', + visibility: 'public', + regexp: '', + }); + }); + + it('should save the custom field on Contact when available', async () => { + // Save the custom field on Visitor/Contact + await request + .post(api('livechat/custom.field')) + .send({ token: visitor.token, key: customFieldName, value: customFieldValue, overwrite: true }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + }); + + // Fetch the visitor to validate custom fields are properly set. + await request + .get(api(`livechat/visitor/${visitor.token}`)) + .set(credentials) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body.visitor).to.have.property('livechatData'); + expect(res.body.visitor.livechatData).to.have.property(customFieldName, customFieldValue); + }); + + // Fetch the visitor's contact to validate custom fields are properly set. + await request + .get(api(`omnichannel/contacts.get`)) + .set(credentials) + .query({ contactId }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('contact'); + expect(res.body.contact).to.have.property('customFields'); + expect(res.body.contact.customFields).to.have.property(customFieldName, customFieldValue); + }); + }); + + it('Should save multiple custom fields on Contact when available', async () => { + // Save the custom field on Visitor/Contact + await request + .post(api('livechat/custom.field')) + .send({ token: visitor.token, key: customFieldName, value: customFieldValue, overwrite: true }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + }); + + await request + .post(api('livechat/custom.field')) + .send({ token: visitor.token, key: `${customFieldName}_2`, value: customFieldValue, overwrite: true }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + }); + + // Fetch the visitor to validate custom fields are properly set. + await request + .get(api(`livechat/visitor/${visitor.token}`)) + .set(credentials) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body.visitor).to.have.property('livechatData'); + expect(res.body.visitor.livechatData).to.have.property(customFieldName, customFieldValue); + expect(res.body.visitor.livechatData).to.have.property(`${customFieldName}_2`, customFieldValue); + }); + + // Fetch the visitor's contact to validate custom fields are properly set. + await request + .get(api(`omnichannel/contacts.get`)) + .set(credentials) + .query({ contactId }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('contact'); + expect(res.body.contact).to.have.property('customFields'); + expect(res.body.contact.customFields).to.have.property(customFieldName, customFieldValue); + expect(res.body.contact.customFields).to.have.property(`${customFieldName}_2`, customFieldValue); + }); + }); + + it('should add the custom field as conflict on Contact when overwrite is false', async () => { + const conflictingFieldValue = 'conflicting-custom-field-value'; + + // Save the custom field on Contact + await request + .post(api('livechat/custom.field')) + .send({ token: visitor.token, key: customFieldName, value: conflictingFieldValue, overwrite: false }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + }); + + // Fetch the visitor to validate custom fields are properly set. + await request + .get(api(`livechat/visitor/${visitor.token}`)) + .set(credentials) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body.visitor).to.have.property('livechatData'); + expect(res.body.visitor.livechatData).to.have.property(customFieldName, customFieldValue); + }); + + // Fetch the visitor's contact to validate custom fields are properly set. + await request + .get(api(`omnichannel/contacts.get`)) + .set(credentials) + .query({ contactId }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('contact'); + expect(res.body.contact).to.have.property('customFields'); + expect(res.body.contact.customFields).to.have.property(customFieldName, customFieldValue); + + // Validate custom fields contain both entries, indicating conflict criteria + expect(res.body.contact.conflictingFields).to.have.lengthOf(1); + expect(res.body.contact.conflictingFields[0]).to.have.property('field', `customFields.${customFieldName}`); + expect(res.body.contact.conflictingFields[0]).to.have.property('value', conflictingFieldValue); + }); + }); + + it('should not save the custom field as a conflict on Contact when overwrite is false but custom field is not registered yet', async () => { + await createCustomField({ + searchable: true, + field: `${customFieldName}_3`, + label: `${customFieldName}_3`, + defaultValue: 'test_default_address', + scope: 'visitor', + visibility: 'public', + regexp: '', + }); + + // Save the custom field on Contact + await request + .post(api('livechat/custom.field')) + .send({ token: visitor.token, key: `${customFieldName}_3`, value: customFieldValue, overwrite: false }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + }); + + // Fetch the visitor to validate custom fields are properly set. + await request + .get(api(`livechat/visitor/${visitor.token}`)) + .set(credentials) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body.visitor).to.have.property('livechatData'); + expect(res.body.visitor.livechatData).to.have.property(`${customFieldName}_3`, customFieldValue); + }); + + // Fetch the visitor's contact to validate custom fields are properly set. + await request + .get(api(`omnichannel/contacts.get`)) + .set(credentials) + .query({ contactId }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('contact'); + expect(res.body.contact).to.have.property('customFields'); + expect(res.body.contact.customFields).to.have.property(`${customFieldName}_3`, customFieldValue); + + // Validate custom fields contain both entries, indicating conflict criteria + expect(res.body.contact.conflictingFields).to.have.lengthOf(1); + expect(res.body.contact.conflictingFields[0]).to.not.have.property('field', `customFields.${customFieldName}_3`); + }); + }); + }); }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.ts b/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.ts index 05702deaaf709..b504de6da0be6 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.ts @@ -11,6 +11,7 @@ import { createDepartment, createLivechatRoom, createVisitor, + deleteVisitor, fetchInquiry, getLivechatRoomInfo, makeAgentAvailable, @@ -456,4 +457,88 @@ describe('LIVECHAT - inquiries', () => { expect(depInq.length).to.be.equal(1); }); }); + + describe('keep inquiry last message updated', () => { + let room: any; + let visitor: any; + let agent: any; + + before(async () => { + agent = await createAgent(); + visitor = await createVisitor(); + + await makeAgentAvailable(); + room = await createLivechatRoom(visitor.token); + }); + + after(async () => { + await deleteVisitor(visitor.token); + }); + + it('should update inquiry last message', async () => { + const msgText = `update inquiry ${Date.now()}`; + + await request.post(api('livechat/message')).send({ token: visitor.token, rid: room._id, msg: msgText }).expect(200); + + const inquiry = await fetchInquiry(room._id); + + expect(inquiry).to.have.property('_id', inquiry._id); + expect(inquiry).to.have.property('rid', room._id); + expect(inquiry).to.have.property('lastMessage'); + expect(inquiry.lastMessage).to.have.property('msg', msgText); + }); + + it('should update room last message after inquiry is taken', async () => { + const msgText = `update room ${Date.now()}`; + + const inquiry = await fetchInquiry(room._id); + + await request + .post(api('livechat/inquiries.take')) + .set(credentials) + .send({ + inquiryId: inquiry._id, + userId: agent._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + }); + + await request.post(api('livechat/message')).send({ token: visitor.token, rid: room._id, msg: msgText }).expect(200); + + // check room + const roomInfo = await getLivechatRoomInfo(room._id); + expect(roomInfo).to.have.property('lastMessage'); + expect(roomInfo.lastMessage).to.have.property('msg', msgText); + }); + + it('should have the correct last message when room is returned to queue', async () => { + const msgText = `return to queue ${Date.now()}`; + + await request.post(api('livechat/message')).send({ token: visitor.token, rid: room._id, msg: msgText }).expect(200); + + await request + .post(methodCall('livechat:returnAsInquiry')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:returnAsInquiry', + params: [room._id], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200); + + const inquiry = await fetchInquiry(room._id); + + expect(inquiry).to.have.property('_id', inquiry._id); + expect(inquiry).to.have.property('rid', room._id); + expect(inquiry).to.have.property('lastMessage'); + expect(inquiry.lastMessage).to.have.property('msg', msgText); + }); + }); }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/07-queue.ts b/apps/meteor/tests/end-to-end/api/livechat/07-queue.ts index 10b4df54667ce..b9ba85bc7825c 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/07-queue.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/07-queue.ts @@ -1,6 +1,7 @@ /* eslint-env mocha */ import { faker } from '@faker-js/faker'; +import type { Credentials } from '@rocket.chat/api-client'; import type { ILivechatDepartment, ILivechatVisitor, IOmnichannelRoom, IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; @@ -8,10 +9,22 @@ import type { Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; import { createDepartmentWithAnOnlineAgent, deleteDepartment, addOrRemoveAgentFromDepartment } from '../../../data/livechat/department'; -import { createVisitor, createLivechatRoom, closeOmnichannelRoom, deleteVisitor } from '../../../data/livechat/rooms'; -import { createAnOnlineAgent } from '../../../data/livechat/users'; +import { + createVisitor, + createLivechatRoom, + closeOmnichannelRoom, + deleteVisitor, + createAgent, + createDepartment, + getLivechatRoomInfo, + makeAgentAvailable, + updateDepartment, +} from '../../../data/livechat/rooms'; +import { createAnOnlineAgent, updateLivechatSettingsForUser } from '../../../data/livechat/users'; +import { sleep } from '../../../data/livechat/utils'; import { updateEESetting, updatePermission, updateSetting } from '../../../data/permissions.helper'; -import { deleteUser } from '../../../data/users.helper'; +import { password } from '../../../data/user'; +import { createUser, deleteUser, login } from '../../../data/users.helper'; import { IS_EE } from '../../../e2e/config/constants'; const cleanupRooms = async () => { @@ -372,3 +385,496 @@ describe('LIVECHAT - Queue', () => { }); }); }); + +(IS_EE ? describe : describe.skip)('Livechat - Chat limits - AutoSelection', () => { + let testUser: { user: IUser; credentials: Credentials }; + let testDepartment: ILivechatDepartment; + let testDepartment2: ILivechatDepartment; + + before((done) => getCredentials(done)); + + before(async () => + Promise.all([ + updateSetting('Livechat_enabled', true), + updateSetting('Livechat_Routing_Method', 'Auto_Selection'), + updateSetting('Omnichannel_enable_department_removal', true), + updateEESetting('Livechat_maximum_chats_per_agent', 0), + updateEESetting('Livechat_waiting_queue', true), + ]), + ); + + before(async () => { + const user = await createUser(); + await createAgent(user.username); + const credentials3 = await login(user.username, password); + await makeAgentAvailable(credentials3); + + testUser = { + user, + credentials: credentials3, + }; + }); + + before(async () => { + testDepartment = await createDepartment([{ agentId: testUser.user._id }], `${new Date().toISOString()}-department`, true, { + maxNumberSimultaneousChat: 2, + }); + testDepartment2 = await createDepartment([{ agentId: testUser.user._id }], `${new Date().toISOString()}-department2`, true, { + maxNumberSimultaneousChat: 2, + }); + await updateLivechatSettingsForUser(testUser.user._id, { maxNumberSimultaneousChat: 1 }, [testDepartment._id, testDepartment2._id]); + }); + + after(async () => { + await Promise.all([ + deleteUser(testUser.user), + updateEESetting('Livechat_maximum_chats_per_agent', 0), + updateEESetting('Livechat_waiting_queue', false), + deleteDepartment(testDepartment._id), + deleteDepartment(testDepartment2._id), + ]); + await updateSetting('Omnichannel_enable_department_removal', false); + }); + + it('should allow a user to take a chat on a department since agent limit is set to 1 and department limit is set to 2 (agent has 0 chats)', async () => { + const visitor = await createVisitor(testDepartment._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + let previousChat: string; + it('should not allow a user to take a chat on a department since agent limit is set to 1 and department limit is set to 2 (agent has 1 chat)', async () => { + const visitor = await createVisitor(testDepartment._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.undefined; + previousChat = room._id; + }); + + it('should allow a user to take a chat on a department when agent limit is increased to 2 and department limit is set to 2 (agent has 1 chat)', async () => { + await updateLivechatSettingsForUser(testUser.user._id, { maxNumberSimultaneousChat: 2 }, [testDepartment._id, testDepartment2._id]); + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should not allow user to take chat on department B when agent limit is 2 and already has 2 chats', async () => { + const visitor = await createVisitor(testDepartment2._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.undefined; + previousChat = room._id; + }); + + it('should allow user to take a chat on department B when agent limit is increased to 3 and user has 2 chats on department A', async () => { + await updateLivechatSettingsForUser(testUser.user._id, { maxNumberSimultaneousChat: 3 }, [testDepartment._id, testDepartment2._id]); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should allow a user to take a chat on department B when agent limit is 0 and department B limit is 2 (user has 3 chats)', async () => { + await updateLivechatSettingsForUser(testUser.user._id, { maxNumberSimultaneousChat: 0 }, [testDepartment._id, testDepartment2._id]); + const visitor = await createVisitor(testDepartment2._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should not allow user to take a chat on department B when is on the limit (user has 4 chats, 2 chats on department B)', async () => { + const visitor = await createVisitor(testDepartment2._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.undefined; + previousChat = room._id; + }); + + it('should not allow user to take a chat on department B even if global limit allows it (user has 4 chats)', async () => { + await updateEESetting('Livechat_maximum_chats_per_agent', 6); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.undefined; + }); + it('should allow user to take a chat when department B limit is removed and its below global limit (user has 4 chats)', async () => { + await updateDepartment({ departmentId: testDepartment2._id, opts: { maxNumberSimultaneousChat: 0 }, userCredentials: credentials }); + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should allow user to take a chat on department B (user has 5 chats, global limit is 6, department limit is 0)', async () => { + const visitor = await createVisitor(testDepartment2._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should not allow user to take a chat on department B (user has 6 chats, global limit is 6, department limit is 0)', async () => { + const visitor = await createVisitor(testDepartment2._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.undefined; + previousChat = room._id; + }); + + it('should allow user to take chat once the global limit is removed (user has 7 chats)', async () => { + await updateEESetting('Livechat_maximum_chats_per_agent', 0); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should not allow user to take chat on department A (as limit for it hasnt changed)', async () => { + const visitor = await createVisitor(testDepartment._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.undefined; + }); + + it('should allow user to take a chat on department A when its limit gets removed (no agent, global or department filter are applied)', async () => { + await updateDepartment({ departmentId: testDepartment._id, opts: { maxNumberSimultaneousChat: 0 }, userCredentials: credentials }); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should honor agent limit over global limit when both are set (user has 8 chats)', async () => { + await updateEESetting('Livechat_maximum_chats_per_agent', 100000); + await updateLivechatSettingsForUser(testUser.user._id, { maxNumberSimultaneousChat: 4 }, [testDepartment._id, testDepartment2._id]); + + const visitor = await createVisitor(testDepartment._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.undefined; + previousChat = room._id; + }); + + // We already tested this case but this way the queue ends up empty :) + it('should receive the chat after agent limit is removed', async () => { + await updateLivechatSettingsForUser(testUser.user._id, { maxNumberSimultaneousChat: 0 }, [testDepartment._id, testDepartment2._id]); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); +}); + +// Note: didn't add for LoadRotation as everything that changes is how the agent is selected +// but the limits applicable are the same as load balance and autoselection +(IS_EE ? describe : describe.skip)('Livechat - Chat limits - LoadBalance', () => { + let testUser: { user: IUser; credentials: Credentials }; + let testUser2: { user: IUser; credentials: Credentials }; + let testDepartment: ILivechatDepartment; + let testDepartment2: ILivechatDepartment; + let testDepartment3: ILivechatDepartment; + + before((done) => getCredentials(done)); + + before(async () => + Promise.all([ + updateSetting('Livechat_enabled', true), + updateSetting('Livechat_Routing_Method', 'Load_Balancing'), + updateSetting('Omnichannel_enable_department_removal', true), + updateEESetting('Livechat_maximum_chats_per_agent', 0), + updateEESetting('Livechat_waiting_queue', true), + ]), + ); + + before(async () => { + const user = await createUser(); + const user2 = await createUser(); + await createAgent(user.username); + await createAgent(user2.username); + const credentials3 = await login(user.username, password); + const credentials4 = await login(user2.username, password); + await makeAgentAvailable(credentials3); + await makeAgentAvailable(credentials4); + + testUser = { + user, + credentials: credentials3, + }; + testUser2 = { + user: user2, + credentials: credentials4, + }; + }); + + before(async () => { + testDepartment = await createDepartment([{ agentId: testUser.user._id }], `${new Date().toISOString()}-department`, true, { + maxNumberSimultaneousChat: 2, + }); + testDepartment2 = await createDepartment([{ agentId: testUser.user._id }], `${new Date().toISOString()}-department2`, true, { + maxNumberSimultaneousChat: 2, + }); + testDepartment3 = await createDepartment( + [{ agentId: testUser.user._id }, { agentId: testUser2.user._id }], + `${new Date().toISOString()}-department3`, + true, + { + maxNumberSimultaneousChat: 2, + }, + ); + + await updateLivechatSettingsForUser(testUser.user._id, { maxNumberSimultaneousChat: 1 }, [ + testDepartment._id, + testDepartment2._id, + testDepartment3._id, + ]); + await updateLivechatSettingsForUser(testUser2.user._id, { maxNumberSimultaneousChat: 1 }, [testDepartment3._id]); + }); + + after(async () => { + await Promise.all([ + deleteUser(testUser.user), + updateEESetting('Livechat_maximum_chats_per_agent', 0), + updateEESetting('Livechat_waiting_queue', false), + updateSetting('Livechat_Routing_Method', 'Auto_Selection'), + deleteDepartment(testDepartment._id), + deleteDepartment(testDepartment2._id), + deleteDepartment(testDepartment3._id), + ]); + await updateSetting('Omnichannel_enable_department_removal', false); + }); + + it('should allow a user to take a chat on a department since agent limit is set to 1 and department limit is set to 2 (agent has 0 chats)', async () => { + const visitor = await createVisitor(testDepartment._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + let previousChat: string; + it('should not allow a user to take a chat on a department since agent limit is set to 1 and department limit is set to 2 (agent has 1 chat)', async () => { + const visitor = await createVisitor(testDepartment._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.undefined; + previousChat = room._id; + }); + + it('should allow a user to take a chat on a department when agent limit is increased to 2 and department limit is set to 2 (agent has 1 chat)', async () => { + await updateLivechatSettingsForUser(testUser.user._id, { maxNumberSimultaneousChat: 2 }, [testDepartment._id, testDepartment2._id]); + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should not allow user to take chat on department B when agent limit is 2 and already has 2 chats', async () => { + const visitor = await createVisitor(testDepartment2._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.undefined; + previousChat = room._id; + }); + + it('should allow user to take a chat on department B when agent limit is increased to 3 and user has 2 chats on department A', async () => { + await updateLivechatSettingsForUser(testUser.user._id, { maxNumberSimultaneousChat: 3 }, [testDepartment._id, testDepartment2._id]); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should allow a user to take a chat on department B when agent limit is 0 and department B limit is 2 (user has 3 chats)', async () => { + await updateLivechatSettingsForUser(testUser.user._id, { maxNumberSimultaneousChat: 0 }, [testDepartment._id, testDepartment2._id]); + const visitor = await createVisitor(testDepartment2._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should not allow user to take a chat on department B when is on the limit (user has 4 chats, 2 chats on department B)', async () => { + const visitor = await createVisitor(testDepartment2._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.undefined; + previousChat = room._id; + }); + + it('should not allow user to take a chat on department B even if global limit allows it (user has 4 chats)', async () => { + await updateEESetting('Livechat_maximum_chats_per_agent', 6); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.undefined; + }); + it('should allow user to take a chat when department B limit is removed and its below global limit (user has 4 chats)', async () => { + await updateDepartment({ departmentId: testDepartment2._id, opts: { maxNumberSimultaneousChat: 0 }, userCredentials: credentials }); + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should allow user to take a chat on department B (user has 5 chats, global limit is 6, department limit is 0)', async () => { + const visitor = await createVisitor(testDepartment2._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should not allow user to take a chat on department B (user has 6 chats, global limit is 6, department limit is 0)', async () => { + const visitor = await createVisitor(testDepartment2._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.undefined; + previousChat = room._id; + }); + + it('should allow user to take chat once the global limit is removed (user has 7 chats)', async () => { + await updateEESetting('Livechat_maximum_chats_per_agent', 0); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should not allow user to take chat on department A (as limit for it hasnt changed)', async () => { + const visitor = await createVisitor(testDepartment._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.undefined; + }); + + it('should allow user to take a chat on department A when its limit gets removed (no agent, global or department filter are applied)', async () => { + await updateDepartment({ departmentId: testDepartment._id, opts: { maxNumberSimultaneousChat: 0 }, userCredentials: credentials }); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should honor agent limit over global limit when both are set (user has 8 chats)', async () => { + await updateEESetting('Livechat_maximum_chats_per_agent', 100000); + await updateLivechatSettingsForUser(testUser.user._id, { maxNumberSimultaneousChat: 4 }, [testDepartment._id, testDepartment2._id]); + + const visitor = await createVisitor(testDepartment._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.undefined; + previousChat = room._id; + }); + + // We already tested this case but this way the queue ends up empty :) + it('should receive the chat after agent limit is removed', async () => { + await updateLivechatSettingsForUser(testUser.user._id, { maxNumberSimultaneousChat: 0 }, [testDepartment._id, testDepartment2._id]); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should route the chat to another agent if limit for agent A is reached and agent B is available', async () => { + await updateLivechatSettingsForUser(testUser.user._id, { maxNumberSimultaneousChat: 1 }, [ + testDepartment._id, + testDepartment2._id, + testDepartment3._id, + ]); + + const visitor = await createVisitor(testDepartment3._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser2.user._id); + }); +}); diff --git a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts index 6b377674bc646..52ae177471c6a 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts @@ -1,5 +1,6 @@ import { faker } from '@faker-js/faker'; -import type { ILivechatDepartment } from '@rocket.chat/core-typings'; +import type { Credentials } from '@rocket.chat/api-client'; +import type { ILivechatDepartment, IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { before, describe, it, after } from 'mocha'; import type { Response } from 'supertest'; @@ -15,8 +16,9 @@ import { getLivechatRoomInfo, } from '../../../data/livechat/rooms'; import { createMonitor, createUnit } from '../../../data/livechat/units'; -import { restorePermissionToRoles, updatePermission, updateSetting } from '../../../data/permissions.helper'; -import { createUser, deleteUser } from '../../../data/users.helper'; +import { restorePermissionToRoles, updateEESetting, updatePermission, updateSetting } from '../../../data/permissions.helper'; +import { password } from '../../../data/user'; +import { createUser, deleteUser, login } from '../../../data/users.helper'; import { IS_EE } from '../../../e2e/config/constants'; (IS_EE ? describe.skip : describe)('LIVECHAT - Departments[CE]', () => { @@ -220,7 +222,7 @@ import { IS_EE } from '../../../e2e/config/constants'; .expect(400); }); - it('should return an error if requestTagsBeforeClosing is true but no tags are provided', async () => { + it('should return an error if requestTagBeforeClosing is true but no tags are provided', async () => { await request .post(api('livechat/department')) .set(credentials) @@ -238,7 +240,7 @@ import { IS_EE } from '../../../e2e/config/constants'; .expect(400); }); - it('should return an error if requestTagsBeforeClosing is true but tags are not an array', async () => { + it('should return an error if requestTagBeforeClosing is true but tags are not an array', async () => { await request .post(api('livechat/department')) .set(credentials) @@ -257,6 +259,31 @@ import { IS_EE } from '../../../e2e/config/constants'; .expect(400); }); + it('should create department if requestTagBeforeClosing is true and tags are an array', async () => { + const chatClosingTags = ['tagA', 'tagB']; + const { body } = await request + .post(api('livechat/department')) + .set(credentials) + .send({ + department: { + name: 'Test', + enabled: true, + showOnOfflineForm: true, + showOnRegistration: true, + email: 'bla@bla', + requestTagBeforeClosingChat: true, + chatClosingTags, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(body.department).to.have.property('requestTagBeforeClosingChat', true); + expect(body.department.chatClosingTags).to.deep.equal(chatClosingTags); + + await deleteDepartment(body.department._id); + }); + it('should return an error if fallbackForwardDepartment is present but is not a department id', async () => { await request .post(api('livechat/department')) @@ -919,4 +946,44 @@ import { IS_EE } from '../../../e2e/config/constants'; .expect(200); }); }); + + describe('With multiple bussines hours', () => { + before(async () => + Promise.all([updateEESetting('Livechat_enable_business_hours', true), updateEESetting('Livechat_business_hour_type', 'Multiple')]), + ); + after(async () => + Promise.all([updateEESetting('Livechat_enable_business_hours', false), updateEESetting('Livechat_business_hour_type', 'Single')]), + ); + + let testUser: { user: IUser; credentials: Credentials }; + let testDepartment: ILivechatDepartment; + before(async () => { + const user = await createUser(); + await createAgent(user.username); + const credentials3 = await login(user.username, password); + await makeAgentAvailable(credentials3); + + testUser = { + user, + credentials: credentials3, + }; + }); + + before(async () => { + testDepartment = await createDepartment([{ agentId: testUser.user._id }], `${new Date().toISOString()}-department`, true); + }); + + after(async () => { + await Promise.all([deleteUser(testUser.user), deleteDepartment(testDepartment._id)]); + }); + + it('should allow to remove an agent from a department when multiple business hours are enabled', async () => { + const res = await request + .post(api(`livechat/department/${testDepartment._id}/agents`)) + .set(credentials) + .send({ upsert: [], remove: [{ agentId: testUser.user._id, username: testUser.user.username }] }) + .expect(200); + expect(res.body).to.have.property('success', true); + }); + }); }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/15-canned-responses.ts b/apps/meteor/tests/end-to-end/api/livechat/15-canned-responses.ts index a71925531f4d5..8099990a6a72e 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/15-canned-responses.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/15-canned-responses.ts @@ -168,6 +168,7 @@ import { IS_EE } from '../../../e2e/config/constants'; }); describe('[POST] canned-responses', () => { + const dupshortcut = `shortcut-${faker.string.nanoid(6)}`; it('should fail if user dont have save-canned-responses permission', async () => { await updatePermission('save-canned-responses', []); return request @@ -197,7 +198,7 @@ import { IS_EE } from '../../../e2e/config/constants'; const { body } = await request .post(api('canned-responses')) .set(credentials) - .send({ shortcut: 'shortcutxx', scope: 'user', tags: ['tag'], text: 'text' }) + .send({ shortcut: dupshortcut, scope: 'user', tags: ['tag'], text: 'text' }) .expect(200); expect(body).to.have.property('success', true); }); @@ -205,7 +206,7 @@ import { IS_EE } from '../../../e2e/config/constants'; return request .post(api('canned-responses')) .set(credentials) - .send({ shortcut: 'shortcutxx', scope: 'user', tags: ['tag'], text: 'text' }) + .send({ shortcut: dupshortcut, scope: 'user', tags: ['tag'], text: 'text' }) .expect(400); }); it('should save a canned response related to an EE tag', async () => { @@ -214,7 +215,7 @@ import { IS_EE } from '../../../e2e/config/constants'; const { body } = await request .post(api('canned-responses')) .set(credentials) - .send({ shortcut: 'shortcutxxx', scope: 'user', tags: [tag.name], text: 'text' }) + .send({ shortcut: `eetag-${faker.string.nanoid(6)}`, scope: 'user', tags: [tag.name], text: 'text' }) .expect(200); expect(body).to.have.property('success', true); @@ -232,7 +233,7 @@ import { IS_EE } from '../../../e2e/config/constants'; const { body } = await request .post(api('canned-responses')) .set(credentials) - .send({ shortcut: 'shortcutxxxx', scope: 'user', tags: [tag.name], text: 'text' }) + .send({ shortcut: `remove-${faker.string.nanoid(6)}`, scope: 'user', tags: [tag.name], text: 'text' }) .expect(200); expect(body).to.have.property('success', true); diff --git a/apps/meteor/tests/end-to-end/api/settings.ts b/apps/meteor/tests/end-to-end/api/settings.ts index 6cd635e41e8f8..be301ed4a088a 100644 --- a/apps/meteor/tests/end-to-end/api/settings.ts +++ b/apps/meteor/tests/end-to-end/api/settings.ts @@ -4,6 +4,7 @@ import { before, describe, it, after } from 'mocha'; import { getCredentials, api, request, credentials } from '../../data/api-data'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; +import { IS_EE } from '../../e2e/config/constants'; describe('[Settings]', () => { before((done) => getCredentials(done)); @@ -274,7 +275,7 @@ describe('[Settings]', () => { }); }); - describe('/audit.settings', () => { + (IS_EE ? describe : describe.skip)('/audit.settings', () => { const formatDate = (date: Date) => date.toISOString().slice(0, 10).replace(/-/g, '/'); it('should return list of settings changed (no filters)', async () => { diff --git a/apps/meteor/tests/end-to-end/api/users.ts b/apps/meteor/tests/end-to-end/api/users.ts index 84c52e7596fe9..e149de6d83e37 100644 --- a/apps/meteor/tests/end-to-end/api/users.ts +++ b/apps/meteor/tests/end-to-end/api/users.ts @@ -2006,6 +2006,63 @@ describe('[Users]', () => { await deleteUser(user); }); + describe('email verification', () => { + let admin: TestUser; + let userToUpdate: TestUser; + let userCredentials: Credentials; + + beforeEach(async () => { + admin = await createUser({ roles: ['admin'] }); + userToUpdate = await createUser(); + userCredentials = await login(admin.username, password); + }); + + afterEach(async () => { + await deleteUser(userToUpdate); + await deleteUser(admin); + }); + + it("should update user's email verified correctly", async () => { + await request + .post(api('users.update')) + .set(userCredentials) + .send({ + userId: userToUpdate._id, + data: { + verified: true, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('user.emails[0].verified', true); + expect(res.body).to.not.have.nested.property('user.e2e'); + }); + }); + + it("should update user's email verified even if email is not changed", (done) => { + void request + .post(api('users.update')) + .set(userCredentials) + .send({ + userId: userToUpdate._id, + data: { + email: userToUpdate.emails[0].address, + verified: true, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('user.emails[0].verified', true); + expect(res.body).to.not.have.nested.property('user.e2e'); + }) + .end(done); + }); + }); + function failUpdateUser(name: string) { it(`should not update an user if the new username is the reserved word ${name}`, (done) => { void request diff --git a/apps/meteor/tests/mocks/client/ServerProviderMock.tsx b/apps/meteor/tests/mocks/client/ServerProviderMock.tsx index 5638bc39160de..5c04ed43d4d25 100644 --- a/apps/meteor/tests/mocks/client/ServerProviderMock.tsx +++ b/apps/meteor/tests/mocks/client/ServerProviderMock.tsx @@ -67,6 +67,8 @@ const contextValue = { callEndpoint, uploadToEndpoint, getStream, + reconnect: () => undefined, + disconnect: () => undefined, }; type ServerProviderMockProps = { diff --git a/apps/meteor/tests/mocks/data.ts b/apps/meteor/tests/mocks/data.ts index d059d941abd6f..a651c8494335b 100644 --- a/apps/meteor/tests/mocks/data.ts +++ b/apps/meteor/tests/mocks/data.ts @@ -1,7 +1,17 @@ import { faker } from '@faker-js/faker'; import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from '@rocket.chat/apps-engine/client/definition'; -import { AppSubscriptionStatus } from '@rocket.chat/core-typings'; -import type { LicenseInfo, App, IMessage, IRoom, ISubscription, IUser } from '@rocket.chat/core-typings'; +import type { ILivechatContact } from '@rocket.chat/apps-engine/definition/livechat'; +import { AppSubscriptionStatus, OmnichannelSourceType } from '@rocket.chat/core-typings'; +import type { + LicenseInfo, + App, + IMessage, + IRoom, + ISubscription, + IUser, + ILivechatContactChannel, + Serialized, +} from '@rocket.chat/core-typings'; import { parse } from '@rocket.chat/message-parser'; import type { MessageWithMdEnforced } from '../../client/lib/parseMessageTextToAstMarkdown'; @@ -21,21 +31,22 @@ export function createFakeUser(overrides?: Partial): IUser { }; } -export const createFakeRoom = (overrides?: Partial): IRoom => ({ - _id: faker.database.mongodbObjectId(), - _updatedAt: faker.date.recent(), - t: faker.helpers.arrayElement(['c', 'p', 'd']), - msgs: faker.number.int({ min: 0 }), - u: { +export const createFakeRoom = (overrides?: Partial): T => + ({ _id: faker.database.mongodbObjectId(), - username: faker.internet.userName(), - name: faker.person.fullName(), - ...overrides?.u, - }, - usersCount: faker.number.int({ min: 0 }), - autoTranslateLanguage: faker.helpers.arrayElement(['en', 'es', 'pt', 'ar', 'it', 'ru', 'fr']), - ...overrides, -}); + _updatedAt: faker.date.recent(), + t: faker.helpers.arrayElement(['c', 'p', 'd']), + msgs: faker.number.int({ min: 0 }), + u: { + _id: faker.database.mongodbObjectId(), + username: faker.internet.userName(), + name: faker.person.fullName(), + ...overrides?.u, + }, + usersCount: faker.number.int({ min: 0 }), + autoTranslateLanguage: faker.helpers.arrayElement(['en', 'es', 'pt', 'ar', 'it', 'ru', 'fr']), + ...overrides, + }) as T; export const createFakeSubscription = (overrides?: Partial): ISubscription => ({ _id: faker.database.mongodbObjectId(), @@ -284,3 +295,38 @@ export function createFakeVisitor() { email: faker.internet.email(), } as const; } + +export function createFakeContactChannel(overrides?: Partial>): Serialized { + return { + name: 'widget', + blocked: false, + verified: false, + ...overrides, + visitor: { + visitorId: faker.string.uuid(), + source: { + type: OmnichannelSourceType.WIDGET, + }, + ...overrides?.visitor, + }, + details: { + type: OmnichannelSourceType.WIDGET, + destination: '', + ...overrides?.details, + }, + }; +} + +export function createFakeContact(overrides?: Partial>): Serialized { + return { + _id: faker.string.uuid(), + _updatedAt: new Date().toISOString(), + name: pullNextVisitorName(), + phones: [{ phoneNumber: faker.phone.number() }], + emails: [{ address: faker.internet.email() }], + unknown: true, + channels: [createFakeContactChannel()], + createdAt: new Date().toISOString(), + ...overrides, + }; +} diff --git a/apps/meteor/tests/unit/app/apps/server/mocks/models/Rooms.mock.js b/apps/meteor/tests/unit/app/apps/server/mocks/models/Rooms.mock.js index 6cf0370f1754e..de59997324c67 100644 --- a/apps/meteor/tests/unit/app/apps/server/mocks/models/Rooms.mock.js +++ b/apps/meteor/tests/unit/app/apps/server/mocks/models/Rooms.mock.js @@ -115,6 +115,19 @@ export class RoomsMock extends BaseModelMock { updatedAt: new Date('2019-04-10T17:44:34.931Z'), }, + GENERALPartialWithOptionalProps: { + id: 'GENERAL', + slugifiedName: 'general', + displaySystemMessages: true, + updatedAt: new Date('2019-04-10T17:44:34.931Z'), + messageCount: 40, + type: 'c', + }, + + UpdatedRoom: { + customFields: { custom: 'field' }, + }, + LivechatRoom: { id: 'LivechatRoom', slugifiedName: undefined, diff --git a/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts b/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts index 718d79baef361..fa2dabcb20313 100644 --- a/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts +++ b/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts @@ -117,6 +117,61 @@ describe('The AppMessagesConverter instance', () => { expect(rocketchatRoom).to.not.have.property('msgs'); expect(rocketchatRoom).to.not.have.property('ro'); expect(rocketchatRoom).to.not.have.property('default'); + expect(rocketchatRoom).to.not.have.property('t'); + }); + + it('should return a proper schema when receiving a partial object', async () => { + const appRoom = RoomsMock.convertedData.GENERALPartialWithOptionalProps as unknown as IAppsRoom; + const rocketchatRoom = await roomConverter.convertAppRoom(appRoom, true); + + expect(rocketchatRoom).to.have.property('_id', appRoom.id); + expect(rocketchatRoom).to.have.property('name', appRoom.slugifiedName); + expect(rocketchatRoom).to.have.property('sysMes', appRoom.displaySystemMessages); + expect(rocketchatRoom).to.have.property('_updatedAt', appRoom.updatedAt); + expect(rocketchatRoom).to.have.property('msgs', appRoom.messageCount); + expect(rocketchatRoom).to.have.property('t', 'c'); + + expect(rocketchatRoom).to.not.have.property('ro'); + expect(rocketchatRoom).to.not.have.property('default'); + }); + + it('should not include properties that are not present in the app room', async () => { + const appRoom = RoomsMock.convertedData.UpdatedRoom as unknown as IAppsRoom; + const rocketchatRoom = await roomConverter.convertAppRoom(appRoom, true); + + expect(rocketchatRoom).to.have.property('customFields'); + expect(rocketchatRoom).to.not.have.property('_id'); + expect(rocketchatRoom).to.not.have.property('t'); + }); + + it('should not include name as undefined if the room doesnt have a name property', async () => { + const appRoom = RoomsMock.convertedData.UpdatedRoom as unknown as IAppsRoom; + const rocketchatRoom = await roomConverter.convertAppRoom(appRoom, true); + + expect(rocketchatRoom.name).to.be.undefined; + }); + + it('should include a name if the source room has slugifiedName property', async () => { + const appRoom = RoomsMock.convertedData.GENERALPartialWithOptionalProps as unknown as IAppsRoom; + const rocketchatRoom = await roomConverter.convertAppRoom(appRoom, true); + + expect(rocketchatRoom.name).to.equal(appRoom.slugifiedName); + }); + + it('should not use _unmappedProperties when the room is a partial object', async () => { + const appRoom = RoomsMock.convertedData.GENERALPartialWithOptionalProps as unknown as IAppsRoom; + // @ts-expect-error - _unmappedProperties + const rocketchatRoom = await roomConverter.convertAppRoom({ ...appRoom, _unmappedProperties_: { unmapped: 'property' } }, true); + + expect(rocketchatRoom).to.not.have.property('unmapped'); + }); + + it('should use _unmappedProperties when the room is a partial object', async () => { + const appRoom = RoomsMock.convertedData.GENERALPartialWithOptionalProps as unknown as IAppsRoom; + // @ts-expect-error - _unmappedProperties + const rocketchatRoom = await roomConverter.convertAppRoom({ ...appRoom, _unmappedProperties_: { unmapped: 'property' } }, false); + + expect(rocketchatRoom).to.have.property('unmapped', 'property'); }); }); }); diff --git a/apps/meteor/tests/unit/app/lib/server/functions/getModifiedHttpHeaders.tests.ts b/apps/meteor/tests/unit/app/lib/server/functions/getModifiedHttpHeaders.tests.ts index 5130bbe59a99f..119abd3e2f0ec 100644 --- a/apps/meteor/tests/unit/app/lib/server/functions/getModifiedHttpHeaders.tests.ts +++ b/apps/meteor/tests/unit/app/lib/server/functions/getModifiedHttpHeaders.tests.ts @@ -8,7 +8,7 @@ describe('getModifiedHttpHeaders', () => { 'x-auth-token': '12345', 'some-other-header': 'value', }; - const result = getModifiedHttpHeaders(inputHeaders); + const result = getModifiedHttpHeaders(new Headers(inputHeaders)); expect(result['x-auth-token']).to.equal('[redacted]'); expect(result['some-other-header']).to.equal('value'); }); @@ -17,7 +17,7 @@ describe('getModifiedHttpHeaders', () => { const inputHeaders = { 'some-other-header': 'value', }; - const result = getModifiedHttpHeaders(inputHeaders); + const result = getModifiedHttpHeaders(new Headers(inputHeaders)); expect(result).to.deep.equal(inputHeaders); }); @@ -26,7 +26,7 @@ describe('getModifiedHttpHeaders', () => { cookie: 'session_id=abc123; rc_token=98765; other_cookie=value', }; const expectedCookies = 'session_id=abc123; rc_token=[redacted]; other_cookie=value'; - const result = getModifiedHttpHeaders(inputHeaders); + const result = getModifiedHttpHeaders(new Headers(inputHeaders)); expect(result.cookie).to.equal(expectedCookies); }); @@ -34,7 +34,7 @@ describe('getModifiedHttpHeaders', () => { const inputHeaders = { cookie: 'session_id=abc123; other_cookie=value', }; - const result = getModifiedHttpHeaders(inputHeaders); + const result = getModifiedHttpHeaders(new Headers(inputHeaders)); expect(result.cookie).to.equal(inputHeaders.cookie); }); @@ -42,7 +42,7 @@ describe('getModifiedHttpHeaders', () => { const inputHeaders = { 'some-other-header': 'value', }; - const result = getModifiedHttpHeaders(inputHeaders); + const result = getModifiedHttpHeaders(new Headers(inputHeaders)); expect(result).to.deep.equal(inputHeaders); }); @@ -52,7 +52,7 @@ describe('getModifiedHttpHeaders', () => { 'cookie': 'session_id=abc123; rc_token=98765; other_cookie=value', }; const expectedCookies = 'session_id=abc123; rc_token=[redacted]; other_cookie=value'; - const result = getModifiedHttpHeaders(inputHeaders); + const result = getModifiedHttpHeaders(new Headers(inputHeaders)); expect(result['x-auth-token']).to.equal('[redacted]'); expect(result.cookie).to.equal(expectedCookies); }); diff --git a/apps/meteor/tests/unit/app/livechat/server/hooks/sendToCRM.tests.ts b/apps/meteor/tests/unit/app/livechat/server/hooks/sendToCRM.tests.ts index 70fbc42bc77dd..79651cea1a52c 100644 --- a/apps/meteor/tests/unit/app/livechat/server/hooks/sendToCRM.tests.ts +++ b/apps/meteor/tests/unit/app/livechat/server/hooks/sendToCRM.tests.ts @@ -1,5 +1,6 @@ import { expect } from 'chai'; import p from 'proxyquire'; +import sinon from 'sinon'; const resultObj = { result: true, @@ -16,10 +17,10 @@ const { sendMessageType, isOmnichannelNavigationMessage, isOmnichannelClosingMes }, }, '../../../utils/server/functions/normalizeMessageFileUpload': { - normalizeMessageFileUpload: (data: any) => data, + normalizeMessageFileUpload: sinon.stub().returnsArg(0), }, '../lib/webhooks': {}, - '../lib/LivechatTyped': { Livechat: {} }, + '../lib/guests': { getLivechatRoomGuestInfo: sinon.stub() }, }); describe('[OC] Send TO CRM', () => { diff --git a/apps/meteor/tests/unit/app/livechat/server/lib/validateRequiredCustomFields.spec.ts b/apps/meteor/tests/unit/app/livechat/server/lib/validateRequiredCustomFields.spec.ts index a8ad3f083812e..1009c2b76b929 100644 --- a/apps/meteor/tests/unit/app/livechat/server/lib/validateRequiredCustomFields.spec.ts +++ b/apps/meteor/tests/unit/app/livechat/server/lib/validateRequiredCustomFields.spec.ts @@ -1,7 +1,7 @@ import type { ILivechatCustomField } from '@rocket.chat/core-typings'; import { expect } from 'chai'; -import { validateRequiredCustomFields } from '../../../../../../app/livechat/server/lib/validateRequiredCustomFields'; +import { validateRequiredCustomFields } from '../../../../../../app/livechat/server/lib/custom-fields'; describe('validateRequiredCustomFields', () => { it('should throw an error if the required custom fields are not provided', async () => { diff --git a/apps/meteor/tests/unit/server/federation/application/user/sender/UserServiceSender.spec.ts b/apps/meteor/tests/unit/server/federation/application/user/sender/UserServiceSender.spec.ts index 1c5213c91acb9..c94758214992a 100644 --- a/apps/meteor/tests/unit/server/federation/application/user/sender/UserServiceSender.spec.ts +++ b/apps/meteor/tests/unit/server/federation/application/user/sender/UserServiceSender.spec.ts @@ -238,7 +238,7 @@ describe('Federation - Application - FederationUserServiceSender', () => { existsOnlyOnProxyServer: false, }), ); - bridge.getUserProfileInformation.resolves({ displayname: 'normalizedInviterId' }); + bridge.getUserProfileInformation.resolves({ displayName: 'normalizedInviterId' }); await service.afterUserRealNameChanged('id', 'name'); expect(bridge.setUserDisplayName.called).to.be.false; @@ -252,7 +252,7 @@ describe('Federation - Application - FederationUserServiceSender', () => { _id: '_id', }), ); - bridge.getUserProfileInformation.resolves({ displayname: 'different' }); + bridge.getUserProfileInformation.resolves({ displayName: 'different' }); await service.afterUserRealNameChanged('id', 'name'); expect(bridge.setUserDisplayName.calledWith('externalInviterId', 'name')).to.be.true; diff --git a/apps/meteor/tests/unit/server/services/apps-engine/service.tests.ts b/apps/meteor/tests/unit/server/services/apps-engine/service.tests.ts new file mode 100644 index 0000000000000..277b3011eddfe --- /dev/null +++ b/apps/meteor/tests/unit/server/services/apps-engine/service.tests.ts @@ -0,0 +1,231 @@ +import type { IAppsEngineService } from '@rocket.chat/core-services'; +import { expect } from 'chai'; +import { describe, it, beforeEach, afterEach } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const AppsMock = { + self: { + isInitialized: sinon.stub(), + getManager: sinon.stub(), + getStorage: sinon.stub(), + getAppSourceStorage: sinon.stub(), + getRocketChatLogger: sinon.stub(), + triggerEvent: sinon.stub(), + }, +}; + +const apiMock = { + call: sinon.stub(), + nodeList: sinon.stub(), +}; + +const isRunningMsMock = sinon.stub(); + +const serviceMocks = { + '@rocket.chat/apps': { Apps: AppsMock }, + '@rocket.chat/core-services': { + api: apiMock, + ServiceClassInternal: class { + onEvent = sinon.stub(); + }, + }, + '../../lib/isRunningMs': { isRunningMs: isRunningMsMock }, + '../../lib/logger/system': { SystemLogger: { error: sinon.stub() } }, +}; + +const { AppsEngineService } = proxyquire.noCallThru().load('../../../../../server/services/apps-engine/service', serviceMocks); + +describe('AppsEngineService', () => { + let service: IAppsEngineService; + + it('should instantiate properly', () => { + expect(new AppsEngineService()).to.be.instanceOf(AppsEngineService); + }); + + describe('#getAppsStatusInNode - part 1', () => { + it('should error if api is not available', async () => { + isRunningMsMock.returns(true); + + const service = new AppsEngineService(); + await expect(service.getAppsStatusInNodes()).to.be.rejectedWith('AppsEngineService is not initialized'); + }); + }); + + beforeEach(() => { + service = new AppsEngineService(); + (service as any).api = apiMock; + }); + + afterEach(() => { + apiMock.call.reset(); + apiMock.nodeList.reset(); + AppsMock.self.isInitialized.reset(); + AppsMock.self.getManager.reset(); + AppsMock.self.getStorage.reset(); + AppsMock.self.getAppSourceStorage.reset(); + AppsMock.self.getRocketChatLogger.reset(); + AppsMock.self.triggerEvent.reset(); + isRunningMsMock.reset(); + }); + + describe('#isInitialized', () => { + it('should return true when Apps is initialized', () => { + AppsMock.self.isInitialized.returns(true); + expect(service.isInitialized()).to.be.true; + }); + + it('should return false when Apps is not initialized', () => { + AppsMock.self.isInitialized.returns(false); + expect(service.isInitialized()).to.be.false; + }); + }); + + describe('#getApps', () => { + it('should return app info from manager', async () => { + const mockApps = [{ getInfo: () => ({ id: 'app1' }) }]; + const mockManager = { get: sinon.stub().resolves(mockApps) }; + AppsMock.self.getManager.returns(mockManager); + + const result = await service.getApps({}); + expect(result).to.deep.equal([{ id: 'app1' }]); + }); + + it('should return undefined when manager is not available', async () => { + AppsMock.self.getManager.returns(undefined); + const result = await service.getApps({}); + expect(result).to.be.undefined; + }); + }); + + describe('#getAppsStatusLocal', () => { + it('should return app status reports', async () => { + const mockApps = [ + { + getStatus: sinon.stub().resolves('enabled'), + getID: sinon.stub().returns('app1'), + }, + ]; + const mockManager = { get: sinon.stub().resolves(mockApps) }; + AppsMock.self.getManager.returns(mockManager); + + const result = await service.getAppsStatusLocal(); + expect(result).to.deep.equal([ + { + status: 'enabled', + appId: 'app1', + }, + ]); + }); + + it('should return empty array when manager is not available', async () => { + AppsMock.self.getManager.returns(undefined); + const result = await service.getAppsStatusLocal(); + expect(result).to.deep.equal([]); + }); + }); + + describe('#getAppStorageItemById', () => { + it('should return storage item for existing app', async () => { + const mockStorageItem = { id: 'app1' }; + const mockApp = { + getStorageItem: sinon.stub().returns(mockStorageItem), + }; + const mockManager = { getOneById: sinon.stub().returns(mockApp) }; + AppsMock.self.getManager.returns(mockManager); + + const result = await service.getAppStorageItemById('app1'); + expect(result).to.equal(mockStorageItem); + }); + + it('should return undefined for non-existent app', async () => { + const mockManager = { getOneById: sinon.stub().returns(undefined) }; + AppsMock.self.getManager.returns(mockManager); + + const result = await service.getAppStorageItemById('non-existent'); + expect(result).to.be.undefined; + }); + }); + + describe('#getAppsStatusInNode - part 2', () => { + it('should throw error when not in microservices mode', async () => { + isRunningMsMock.returns(false); + await expect(service.getAppsStatusInNodes()).to.be.rejectedWith( + 'Getting apps status in cluster is only available in microservices mode', + ); + }); + + it('should throw error when not enough apps-engine nodes are available', async () => { + isRunningMsMock.returns(true); + apiMock.nodeList.resolves([{ id: 'node1', local: true }]); + apiMock.call.resolves([{ name: 'apps-engine', nodes: ['node1'] }]); + + await expect(service.getAppsStatusInNodes()).to.be.rejectedWith('Not enough Apps-Engine nodes in deployment'); + }); + + it('should not call the service for the local node', async () => { + isRunningMsMock.returns(true); + apiMock.nodeList.resolves([{ id: 'node1', local: true }]); + apiMock.call + .onFirstCall() + .resolves([{ name: 'apps-engine', nodes: ['node1', 'node2'] }]) + .onSecondCall() + .resolves([ + { status: 'enabled', appId: 'app1' }, + { status: 'enabled', appId: 'app2' }, + ]) + .onThirdCall() + .rejects(new Error('Should not be called')); + + const result = await service.getAppsStatusInNodes(); + + expect(result).to.deep.equal({ + app1: [{ instanceId: 'node2', status: 'enabled' }], + app2: [{ instanceId: 'node2', status: 'enabled' }], + }); + }); + + it('should return status from all nodes', async () => { + isRunningMsMock.returns(true); + apiMock.nodeList.resolves([{ id: 'node1', local: true }]); + apiMock.call + .onFirstCall() + .resolves([{ name: 'apps-engine', nodes: ['node1', 'node2', 'node3'] }]) + .onSecondCall() + .resolves([ + { status: 'enabled', appId: 'app1' }, + { status: 'enabled', appId: 'app2' }, + ]) + .onThirdCall() + .resolves([ + { status: 'initialized', appId: 'app1' }, + { status: 'enabled', appId: 'app2' }, + ]); + + const result = await service.getAppsStatusInNodes(); + + expect(result).to.deep.equal({ + app1: [ + { instanceId: 'node2', status: 'enabled' }, + { instanceId: 'node3', status: 'initialized' }, + ], + app2: [ + { instanceId: 'node2', status: 'enabled' }, + { instanceId: 'node3', status: 'enabled' }, + ], + }); + }); + + it('should throw error when failed to get status from a node', async () => { + isRunningMsMock.returns(true); + apiMock.nodeList.resolves([{ id: 'node1', local: true }]); + apiMock.call + .onFirstCall() + .resolves([{ name: 'apps-engine', nodes: ['node1', 'node2'] }]) + .onSecondCall() + .resolves(undefined); + + await expect(service.getAppsStatusInNodes()).to.be.rejectedWith('Failed to get apps status from node node2'); + }); + }); +}); diff --git a/apps/meteor/tests/unit/server/services/instance/service.tests.ts b/apps/meteor/tests/unit/server/services/instance/service.tests.ts new file mode 100644 index 0000000000000..dac15385514a5 --- /dev/null +++ b/apps/meteor/tests/unit/server/services/instance/service.tests.ts @@ -0,0 +1,122 @@ +import { expect } from 'chai'; +import { describe, it, beforeEach, afterEach } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +import type { IInstanceService } from '../../../../../ee/server/sdk/types/IInstanceService'; + +const ServiceBrokerMock = { + call: sinon.stub(), +}; + +const AppsMock = { + getAppsStatusLocal: sinon.stub(), +}; + +const serviceMocks = { + '@rocket.chat/core-services': { + ServiceClassInternal: class { + onEvent = sinon.stub(); + }, + Apps: AppsMock, + }, + 'moleculer': { + ServiceBroker: sinon.stub().returns(ServiceBrokerMock), + Serializers: { + Base: class {}, + }, + }, +}; + +const { InstanceService } = proxyquire + .noPreserveCache() + .noCallThru() + .load('../../../../../ee/server/local-services/instance/service', serviceMocks); + +describe('InstanceService', () => { + let service: IInstanceService; + + beforeEach(() => { + service = new InstanceService(); + (service as any).broker = ServiceBrokerMock; + }); + + afterEach(() => { + ServiceBrokerMock.call.reset(); + AppsMock.getAppsStatusLocal.reset(); + }); + + describe('#getInstances', () => { + it('should return list of instances', async () => { + const mockInstances = [{ id: 'node1' }]; + ServiceBrokerMock.call.resolves(mockInstances); + + const instances = await service.getInstances(); + + expect(instances).to.deep.equal(mockInstances); + expect(ServiceBrokerMock.call.calledWith('$node.list', { onlyAvailable: true })).to.be.true; + }); + + it('should handle empty instance list', async () => { + ServiceBrokerMock.call.resolves([]); + + const instances = await service.getInstances(); + + expect(instances).to.deep.equal([]); + expect(ServiceBrokerMock.call.calledWith('$node.list', { onlyAvailable: true })).to.be.true; + }); + }); + + describe('#getAppsStatusInInstances', () => { + it('should return app status from all non-local instances', async () => { + const mockInstances = [ + { id: 'node1', local: true }, + { id: 'node2', local: false }, + { id: 'node3', local: false }, + ]; + + ServiceBrokerMock.call + .onFirstCall() + .resolves(mockInstances) + .onSecondCall() + .resolves([{ status: 'enabled', appId: 'app1' }]) + .onThirdCall() + .resolves([{ status: 'disabled', appId: 'app2' }]); + + const result = await service.getAppsStatusInInstances(); + + expect(result).to.deep.equal({ + app1: [{ instanceId: 'node2', status: 'enabled' }], + app2: [{ instanceId: 'node3', status: 'disabled' }], + }); + + expect(ServiceBrokerMock.call.calledThrice).to.be.true; + }); + + it('should handle empty app status response', async () => { + const mockInstances = [ + { id: 'node1', local: true }, + { id: 'node2', local: false }, + ]; + + ServiceBrokerMock.call.onFirstCall().resolves(mockInstances).onSecondCall().resolves([]); + + const result = await service.getAppsStatusInInstances(); + + expect(result).to.deep.equal({}); + expect(ServiceBrokerMock.call.calledTwice).to.be.true; + }); + + it('should handle undefined app status response', async () => { + const mockInstances = [ + { id: 'node1', local: true }, + { id: 'node2', local: false }, + ]; + + ServiceBrokerMock.call.onFirstCall().resolves(mockInstances).onSecondCall().resolves(undefined); + + await expect(service.getAppsStatusInInstances()).to.be.rejectedWith(`Failed to get apps status from instance node2`); + expect(ServiceBrokerMock.call.calledTwice).to.be.true; + }); + }); +}); diff --git a/apps/meteor/tests/unit/server/services/omnichannel-integrations/providers/twilio.spec.ts b/apps/meteor/tests/unit/server/services/omnichannel-integrations/providers/twilio.spec.ts index dd0a62da3ad1e..9ee9471d8b41b 100644 --- a/apps/meteor/tests/unit/server/services/omnichannel-integrations/providers/twilio.spec.ts +++ b/apps/meteor/tests/unit/server/services/omnichannel-integrations/providers/twilio.spec.ts @@ -53,7 +53,7 @@ describe('Twilio Request Validation', () => { twilioStub.isRequestFromTwilio.reset(); }); - it('should not validate a request when process.env.TEST_MODE is true', () => { + it('should not validate a request when process.env.TEST_MODE is true', async () => { process.env.TEST_MODE = 'true'; const twilio = new Twilio(); @@ -63,10 +63,10 @@ describe('Twilio Request Validation', () => { }, }; - expect(twilio.validateRequest(request)).to.be.true; + expect(await twilio.validateRequest(request)).to.be.true; }); - it('should validate a request when process.env.TEST_MODE is false', () => { + it('should validate a request when process.env.TEST_MODE is false', async () => { process.env.TEST_MODE = 'false'; settingsStub.get.withArgs('SMS_Twilio_authToken').returns('test'); @@ -81,15 +81,21 @@ describe('Twilio Request Validation', () => { const request = { headers: { - 'x-twilio-signature': getSignature('test', 'https://example.com/api/v1/livechat/sms-incoming/twilio', requestBody), + get: (param: string) => { + const headers: Record = { + 'x-twilio-signature': getSignature('test', 'https://example.com/api/v1/livechat/sms-incoming/twilio', requestBody), + }; + + return headers[param]; + }, }, - body: requestBody, + json: () => requestBody, }; - expect(twilio.validateRequest(request)).to.be.true; + expect(await twilio.validateRequest(request)).to.be.true; }); - it('should validate a request when query string is present', () => { + it('should validate a request when query string is present', async () => { process.env.TEST_MODE = 'false'; settingsStub.get.withArgs('SMS_Twilio_authToken').returns('test'); @@ -103,17 +109,23 @@ describe('Twilio Request Validation', () => { }; const request = { - originalUrl: '/api/v1/livechat/sms-incoming/twilio?department=1', + url: '/api/v1/livechat/sms-incoming/twilio?department=1', headers: { - 'x-twilio-signature': getSignature('test', 'https://example.com/api/v1/livechat/sms-incoming/twilio?department=1', requestBody), + get: (param: string) => { + const headers: Record = { + 'x-twilio-signature': getSignature('test', 'https://example.com/api/v1/livechat/sms-incoming/twilio?department=1', requestBody), + }; + + return headers[param]; + }, }, - body: requestBody, + json: () => requestBody, }; - expect(twilio.validateRequest(request)).to.be.true; + expect(await twilio.validateRequest(request)).to.be.true; }); - it('should reject a request where signature doesnt match', () => { + it('should reject a request where signature doesnt match', async () => { settingsStub.get.withArgs('SMS_Twilio_authToken').returns('test'); settingsStub.get.withArgs('Site_Url').returns('https://example.com'); @@ -126,15 +138,21 @@ describe('Twilio Request Validation', () => { const request = { headers: { - 'x-twilio-signature': getSignature('anotherAuthToken', 'https://example.com/api/v1/livechat/sms-incoming/twilio', requestBody), + get: (param: string) => { + const headers: Record = { + 'x-twilio-signature': getSignature('anotherAuthToken', 'https://example.com/api/v1/livechat/sms-incoming/twilio', requestBody), + }; + + return headers[param]; + }, }, - body: requestBody, + json: () => requestBody, }; - expect(twilio.validateRequest(request)).to.be.false; + expect(await twilio.validateRequest(request)).to.be.false; }); - it('should reject a request where signature is missing', () => { + it('should reject a request where signature is missing', async () => { settingsStub.get.withArgs('SMS_Twilio_authToken').returns('test'); settingsStub.get.withArgs('Site_Url').returns('https://example.com'); @@ -146,14 +164,16 @@ describe('Twilio Request Validation', () => { }; const request = { - headers: {}, - body: requestBody, + headers: { + get: () => null, + }, + json: () => requestBody, }; - expect(twilio.validateRequest(request)).to.be.false; + expect(await twilio.validateRequest(request)).to.be.false; }); - it('should reject a request where the signature doesnt correspond body', () => { + it('should reject a request where the signature doesnt correspond body', async () => { settingsStub.get.withArgs('SMS_Twilio_authToken').returns('test'); settingsStub.get.withArgs('Site_Url').returns('https://example.com'); @@ -166,15 +186,21 @@ describe('Twilio Request Validation', () => { const request = { headers: { - 'x-twilio-signature': getSignature('test', 'https://example.com/api/v1/livechat/sms-incoming/twilio', {}), + get: (param: string) => { + const headers: Record = { + 'x-twilio-signature': getSignature('test', 'https://example.com/api/v1/livechat/sms-incoming/twilio', {}), + }; + + return headers[param]; + }, }, - body: requestBody, + json: () => requestBody, }; - expect(twilio.validateRequest(request)).to.be.false; + expect(await twilio.validateRequest(request)).to.be.false; }); - it('should return false if URL is not provided', () => { + it('should return false if URL is not provided', async () => { process.env.TEST_MODE = 'false'; settingsStub.get.withArgs('SMS_Twilio_authToken').returns('test'); @@ -189,15 +215,21 @@ describe('Twilio Request Validation', () => { const request = { headers: { - 'x-twilio-signature': getSignature('test', 'https://example.com/api/v1/livechat/sms-incoming/twilio', requestBody), + get: (param: string) => { + const headers: Record = { + 'x-twilio-signature': getSignature('test', 'https://example.com/api/v1/livechat/sms-incoming/twilio', requestBody), + }; + + return headers[param]; + }, }, - body: requestBody, + json: () => requestBody, }; - expect(twilio.validateRequest(request)).to.be.false; + expect(await twilio.validateRequest(request)).to.be.false; }); - it('should return false if authToken is not provided', () => { + it('should return false if authToken is not provided', async () => { process.env.TEST_MODE = 'false'; settingsStub.get.withArgs('SMS_Twilio_authToken').returns(''); @@ -212,11 +244,17 @@ describe('Twilio Request Validation', () => { const request = { headers: { - 'x-twilio-signature': getSignature('test', 'https://example.com/api/v1/livechat/sms-incoming/twilio', requestBody), + get: (param: string) => { + const headers: Record = { + 'x-twilio-signature': getSignature('test', 'https://example.com/api/v1/livechat/sms-incoming/twilio', requestBody), + }; + + return headers[param]; + }, }, - body: requestBody, + json: () => requestBody, }; - expect(twilio.validateRequest(request)).to.be.false; + expect(await twilio.validateRequest(request)).to.be.false; }); }); diff --git a/apps/uikit-playground/CHANGELOG.md b/apps/uikit-playground/CHANGELOG.md index 7becb2554cd26..50dacc6171da0 100644 --- a/apps/uikit-playground/CHANGELOG.md +++ b/apps/uikit-playground/CHANGELOG.md @@ -1,5 +1,113 @@ # @rocket.chat/uikit-playground +## 0.6.12-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.8 + - @rocket.chat/ui-contexts@18.0.0-rc.8 + - @rocket.chat/ui-avatar@14.0.0-rc.8 +
      + +## 0.6.12-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.7 + - @rocket.chat/ui-contexts@18.0.0-rc.7 + - @rocket.chat/ui-avatar@14.0.0-rc.7 +
      + +## 0.6.12-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.6 + - @rocket.chat/ui-contexts@18.0.0-rc.6 + - @rocket.chat/ui-avatar@14.0.0-rc.6 +
      + +## 0.6.12-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.5 + - @rocket.chat/ui-contexts@18.0.0-rc.5 + - @rocket.chat/ui-avatar@14.0.0-rc.5 +
      + +## 0.6.12-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.4 + - @rocket.chat/ui-contexts@18.0.0-rc.4 + - @rocket.chat/ui-avatar@14.0.0-rc.4 +
      + +## 0.6.12-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.3 + - @rocket.chat/ui-contexts@18.0.0-rc.3 + - @rocket.chat/ui-avatar@14.0.0-rc.3 +
      + +## 0.6.12-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.2 + - @rocket.chat/ui-contexts@18.0.0-rc.2 + - @rocket.chat/ui-avatar@14.0.0-rc.2 +
      + +## 0.6.12-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.1 + - @rocket.chat/ui-contexts@18.0.0-rc.1 + - @rocket.chat/ui-avatar@14.0.0-rc.1 +
      + +## 0.6.12-rc.0 + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/ui-contexts@18.0.0-rc.0 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.0 + - @rocket.chat/ui-avatar@14.0.0-rc.0 +
      + ## 0.6.11 ### Patch Changes diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index e0d2d0d8a82df..c2dafd5f56df2 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/uikit-playground", "private": true, - "version": "0.6.11", + "version": "0.6.12-rc.8", "type": "module", "scripts": { "dev": "vite", @@ -18,13 +18,13 @@ "@lezer/highlight": "^1.2.1", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "~0.61.0", + "@rocket.chat/fuselage": "~0.62.0", "@rocket.chat/fuselage-hooks": "~0.35.0", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/fuselage-toastbar": "^0.35.0", "@rocket.chat/fuselage-tokens": "~0.33.2", "@rocket.chat/fuselage-ui-kit": "workspace:~", - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@rocket.chat/logo": "^0.32.0", "@rocket.chat/styled": "~0.32.0", "@rocket.chat/ui-avatar": "workspace:^", @@ -53,7 +53,7 @@ "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", "typescript": "~5.7.2", - "vite": "^6.1.0" + "vite": "^6.2.4" }, "volta": { "extends": "../../package.json" diff --git a/ee/apps/account-service/CHANGELOG.md b/ee/apps/account-service/CHANGELOG.md index 8ee9ab1b38590..9a7caf80b6e8c 100644 --- a/ee/apps/account-service/CHANGELOG.md +++ b/ee/apps/account-service/CHANGELOG.md @@ -1,5 +1,131 @@ # @rocket.chat/account-service +## 0.4.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/rest-typings@7.6.0-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 + - @rocket.chat/network-broker@0.2.0-rc.8 +
      + +## 0.4.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/rest-typings@7.6.0-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 + - @rocket.chat/network-broker@0.2.0-rc.7 +
      + +## 0.4.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/rest-typings@7.6.0-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 + - @rocket.chat/network-broker@0.2.0-rc.6 +
      + +## 0.4.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/rest-typings@7.6.0-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 + - @rocket.chat/network-broker@0.2.0-rc.5 +
      + +## 0.4.21-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/rest-typings@7.6.0-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 + - @rocket.chat/network-broker@0.2.0-rc.4 +
      + +## 0.4.21-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/rest-typings@7.6.0-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 + - @rocket.chat/network-broker@0.2.0-rc.3 +
      + +## 0.4.21-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/rest-typings@7.6.0-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 + - @rocket.chat/network-broker@0.2.0-rc.2 +
      + +## 0.4.21-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/rest-typings@7.6.0-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 + - @rocket.chat/network-broker@0.2.0-rc.1 +
      + +## 0.4.21-rc.0 + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/network-broker@0.2.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 +
      + ## 0.4.20 ### Patch Changes diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index 4976887826f85..bc7f1c718cdcd 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/account-service", "private": true, - "version": "0.4.20", + "version": "0.4.21-rc.8", "description": "Rocket.Chat Account service", "scripts": { "build": "tsc -p tsconfig.json", @@ -26,7 +26,7 @@ "@rocket.chat/string-helpers": "~0.31.25", "@rocket.chat/tools": "workspace:^", "@rocket.chat/tracing": "workspace:^", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "bcrypt": "^5.1.1", "ejson": "^2.2.3", "event-loop-stats": "^1.4.1", diff --git a/ee/apps/authorization-service/CHANGELOG.md b/ee/apps/authorization-service/CHANGELOG.md index 02091eb36c071..6b8425aba07c3 100644 --- a/ee/apps/authorization-service/CHANGELOG.md +++ b/ee/apps/authorization-service/CHANGELOG.md @@ -1,5 +1,131 @@ # @rocket.chat/authorization-service +## 0.4.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/rest-typings@7.6.0-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 + - @rocket.chat/network-broker@0.2.0-rc.8 +
      + +## 0.4.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/rest-typings@7.6.0-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 + - @rocket.chat/network-broker@0.2.0-rc.7 +
      + +## 0.4.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/rest-typings@7.6.0-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 + - @rocket.chat/network-broker@0.2.0-rc.6 +
      + +## 0.4.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/rest-typings@7.6.0-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 + - @rocket.chat/network-broker@0.2.0-rc.5 +
      + +## 0.4.21-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/rest-typings@7.6.0-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 + - @rocket.chat/network-broker@0.2.0-rc.4 +
      + +## 0.4.21-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/rest-typings@7.6.0-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 + - @rocket.chat/network-broker@0.2.0-rc.3 +
      + +## 0.4.21-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/rest-typings@7.6.0-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 + - @rocket.chat/network-broker@0.2.0-rc.2 +
      + +## 0.4.21-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/rest-typings@7.6.0-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 + - @rocket.chat/network-broker@0.2.0-rc.1 +
      + +## 0.4.21-rc.0 + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/network-broker@0.2.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 +
      + ## 0.4.20 ### Patch Changes diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index f23e16bab5240..3daf1a15b8042 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/authorization-service", "private": true, - "version": "0.4.20", + "version": "0.4.21-rc.8", "description": "Rocket.Chat Authorization service", "scripts": { "build": "tsc -p tsconfig.json", @@ -25,7 +25,7 @@ "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@rocket.chat/tracing": "workspace:^", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "ejson": "^2.2.3", "event-loop-stats": "^1.4.1", "eventemitter3": "^5.0.1", diff --git a/ee/apps/ddp-streamer/CHANGELOG.md b/ee/apps/ddp-streamer/CHANGELOG.md index 1bf4229a35d6b..134c40573dba5 100644 --- a/ee/apps/ddp-streamer/CHANGELOG.md +++ b/ee/apps/ddp-streamer/CHANGELOG.md @@ -1,5 +1,140 @@ # @rocket.chat/ddp-streamer +## 0.3.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/rest-typings@7.6.0-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 + - @rocket.chat/network-broker@0.2.0-rc.8 + - @rocket.chat/instance-status@0.1.21-rc.8 +
      + +## 0.3.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/rest-typings@7.6.0-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 + - @rocket.chat/network-broker@0.2.0-rc.7 + - @rocket.chat/instance-status@0.1.21-rc.7 +
      + +## 0.3.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/rest-typings@7.6.0-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 + - @rocket.chat/network-broker@0.2.0-rc.6 + - @rocket.chat/instance-status@0.1.21-rc.6 +
      + +## 0.3.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/rest-typings@7.6.0-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 + - @rocket.chat/network-broker@0.2.0-rc.5 + - @rocket.chat/instance-status@0.1.21-rc.5 +
      + +## 0.3.21-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/rest-typings@7.6.0-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 + - @rocket.chat/network-broker@0.2.0-rc.4 + - @rocket.chat/instance-status@0.1.21-rc.4 +
      + +## 0.3.21-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/rest-typings@7.6.0-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 + - @rocket.chat/network-broker@0.2.0-rc.3 + - @rocket.chat/instance-status@0.1.21-rc.3 +
      + +## 0.3.21-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/rest-typings@7.6.0-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 + - @rocket.chat/network-broker@0.2.0-rc.2 + - @rocket.chat/instance-status@0.1.21-rc.2 +
      + +## 0.3.21-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/rest-typings@7.6.0-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 + - @rocket.chat/network-broker@0.2.0-rc.1 + - @rocket.chat/instance-status@0.1.20-rc.1 +
      + +## 0.3.21-rc.0 + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/network-broker@0.2.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 + - @rocket.chat/instance-status@0.1.20-rc.0 +
      + ## 0.3.20 ### Patch Changes diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index f37dda1598fb0..0aa8302bcbcf7 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/ddp-streamer", "private": true, - "version": "0.3.20", + "version": "0.3.21-rc.8", "description": "Rocket.Chat DDP-Streamer service", "scripts": { "build": "tsc -p tsconfig.json", @@ -49,7 +49,7 @@ "@rocket.chat/eslint-config": "workspace:^", "@types/ejson": "^2.2.2", "@types/gc-stats": "^1.4.3", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "@types/polka": "^0.5.7", "@types/underscore": "^1.13.0", "@types/uuid": "^10.0.0", diff --git a/ee/apps/omnichannel-transcript/CHANGELOG.md b/ee/apps/omnichannel-transcript/CHANGELOG.md index 28010332c40ac..10efeecdcb03d 100644 --- a/ee/apps/omnichannel-transcript/CHANGELOG.md +++ b/ee/apps/omnichannel-transcript/CHANGELOG.md @@ -1,5 +1,140 @@ # @rocket.chat/omnichannel-transcript +## 0.4.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/omnichannel-services@0.3.18-rc.8 + - @rocket.chat/pdf-worker@0.3.0-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 + - @rocket.chat/network-broker@0.2.0-rc.8 +
      + +## 0.4.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/omnichannel-services@0.3.18-rc.7 + - @rocket.chat/pdf-worker@0.3.0-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 + - @rocket.chat/network-broker@0.2.0-rc.7 +
      + +## 0.4.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/omnichannel-services@0.3.18-rc.6 + - @rocket.chat/pdf-worker@0.3.0-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 + - @rocket.chat/network-broker@0.2.0-rc.6 +
      + +## 0.4.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/omnichannel-services@0.3.18-rc.5 + - @rocket.chat/pdf-worker@0.3.0-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 + - @rocket.chat/network-broker@0.2.0-rc.5 +
      + +## 0.4.21-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/omnichannel-services@0.3.18-rc.4 + - @rocket.chat/pdf-worker@0.3.0-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 + - @rocket.chat/network-broker@0.2.0-rc.4 +
      + +## 0.4.21-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/omnichannel-services@0.3.18-rc.3 + - @rocket.chat/pdf-worker@0.3.0-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 + - @rocket.chat/network-broker@0.2.0-rc.3 +
      + +## 0.4.21-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/omnichannel-services@0.3.18-rc.2 + - @rocket.chat/pdf-worker@0.3.0-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 + - @rocket.chat/network-broker@0.2.0-rc.2 +
      + +## 0.4.21-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/omnichannel-services@0.3.17-rc.1 + - @rocket.chat/pdf-worker@0.3.0-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 + - @rocket.chat/network-broker@0.2.0-rc.1 +
      + +## 0.4.21-rc.0 + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 2ee1a81de770a682f6e7a8590a896e76a32f4e3c, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/network-broker@0.2.0-rc.0 + - @rocket.chat/pdf-worker@0.3.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 + - @rocket.chat/omnichannel-services@0.3.17-rc.0 +
      + ## 0.4.20 ### Patch Changes diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index a2f5a579e4df9..af99458f62ade 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/omnichannel-transcript", "private": true, - "version": "0.4.20", + "version": "0.4.21-rc.8", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", @@ -27,7 +27,7 @@ "@rocket.chat/pdf-worker": "workspace:^", "@rocket.chat/tools": "workspace:^", "@rocket.chat/tracing": "workspace:^", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "ejson": "^2.2.3", "emoji-toolkit": "^7.0.1", "event-loop-stats": "^1.4.1", diff --git a/ee/apps/presence-service/CHANGELOG.md b/ee/apps/presence-service/CHANGELOG.md index 88db53c5479c6..b491534f3b981 100644 --- a/ee/apps/presence-service/CHANGELOG.md +++ b/ee/apps/presence-service/CHANGELOG.md @@ -1,5 +1,131 @@ # @rocket.chat/presence-service +## 0.4.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/presence@0.2.21-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 + - @rocket.chat/network-broker@0.2.0-rc.8 +
      + +## 0.4.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/presence@0.2.21-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 + - @rocket.chat/network-broker@0.2.0-rc.7 +
      + +## 0.4.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/presence@0.2.21-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 + - @rocket.chat/network-broker@0.2.0-rc.6 +
      + +## 0.4.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/presence@0.2.21-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 + - @rocket.chat/network-broker@0.2.0-rc.5 +
      + +## 0.4.21-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/presence@0.2.21-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 + - @rocket.chat/network-broker@0.2.0-rc.4 +
      + +## 0.4.21-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/presence@0.2.21-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 + - @rocket.chat/network-broker@0.2.0-rc.3 +
      + +## 0.4.21-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/presence@0.2.21-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 + - @rocket.chat/network-broker@0.2.0-rc.2 +
      + +## 0.4.21-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/presence@0.2.20-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 + - @rocket.chat/network-broker@0.2.0-rc.1 +
      + +## 0.4.21-rc.0 + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/network-broker@0.2.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 + - @rocket.chat/presence@0.2.20-rc.0 +
      + ## 0.4.20 ### Patch Changes diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index 38c5b3cb8819e..9f719ce088fdd 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/presence-service", "private": true, - "version": "0.4.20", + "version": "0.4.21-rc.8", "description": "Rocket.Chat Presence service", "scripts": { "build": "tsc -p tsconfig.json", @@ -25,7 +25,7 @@ "@rocket.chat/presence": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@rocket.chat/tracing": "workspace:^", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "ejson": "^2.2.3", "event-loop-stats": "^1.4.1", "eventemitter3": "^5.0.1", diff --git a/ee/apps/queue-worker/CHANGELOG.md b/ee/apps/queue-worker/CHANGELOG.md index 85a2f2272de17..2512dd35a7d47 100644 --- a/ee/apps/queue-worker/CHANGELOG.md +++ b/ee/apps/queue-worker/CHANGELOG.md @@ -1,5 +1,131 @@ # @rocket.chat/queue-worker +## 0.4.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/omnichannel-services@0.3.18-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 + - @rocket.chat/network-broker@0.2.0-rc.8 +
      + +## 0.4.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/omnichannel-services@0.3.18-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 + - @rocket.chat/network-broker@0.2.0-rc.7 +
      + +## 0.4.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/omnichannel-services@0.3.18-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 + - @rocket.chat/network-broker@0.2.0-rc.6 +
      + +## 0.4.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/omnichannel-services@0.3.18-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 + - @rocket.chat/network-broker@0.2.0-rc.5 +
      + +## 0.4.21-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/omnichannel-services@0.3.18-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 + - @rocket.chat/network-broker@0.2.0-rc.4 +
      + +## 0.4.21-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/omnichannel-services@0.3.18-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 + - @rocket.chat/network-broker@0.2.0-rc.3 +
      + +## 0.4.21-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/omnichannel-services@0.3.18-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 + - @rocket.chat/network-broker@0.2.0-rc.2 +
      + +## 0.4.21-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/omnichannel-services@0.3.17-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 + - @rocket.chat/network-broker@0.2.0-rc.1 +
      + +## 0.4.21-rc.0 + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/network-broker@0.2.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 + - @rocket.chat/omnichannel-services@0.3.17-rc.0 +
      + ## 0.4.20 ### Patch Changes diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index ad770eec65504..84bb3314ed7d2 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/queue-worker", "private": true, - "version": "0.4.20", + "version": "0.4.21-rc.8", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", @@ -24,7 +24,7 @@ "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/omnichannel-services": "workspace:^", "@rocket.chat/tracing": "workspace:^", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "ejson": "^2.2.3", "emoji-toolkit": "^7.0.1", "event-loop-stats": "^1.4.1", diff --git a/ee/apps/stream-hub-service/CHANGELOG.md b/ee/apps/stream-hub-service/CHANGELOG.md index 9faed274e9f99..be61167d12246 100644 --- a/ee/apps/stream-hub-service/CHANGELOG.md +++ b/ee/apps/stream-hub-service/CHANGELOG.md @@ -1,5 +1,122 @@ # @rocket.chat/stream-hub-service +## 0.4.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 + - @rocket.chat/network-broker@0.2.0-rc.8 +
      + +## 0.4.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 + - @rocket.chat/network-broker@0.2.0-rc.7 +
      + +## 0.4.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 + - @rocket.chat/network-broker@0.2.0-rc.6 +
      + +## 0.4.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 + - @rocket.chat/network-broker@0.2.0-rc.5 +
      + +## 0.4.21-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 + - @rocket.chat/network-broker@0.2.0-rc.4 +
      + +## 0.4.21-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 + - @rocket.chat/network-broker@0.2.0-rc.3 +
      + +## 0.4.21-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 + - @rocket.chat/network-broker@0.2.0-rc.2 +
      + +## 0.4.21-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 + - @rocket.chat/network-broker@0.2.0-rc.1 +
      + +## 0.4.21-rc.0 + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/network-broker@0.2.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 +
      + ## 0.4.20 ### Patch Changes diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index f491b0daef0c8..26feefba44419 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/stream-hub-service", "private": true, - "version": "0.4.20", + "version": "0.4.21-rc.8", "description": "Rocket.Chat Stream Hub service", "scripts": { "build": "tsc -p tsconfig.json", @@ -24,7 +24,7 @@ "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@rocket.chat/tracing": "workspace:^", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "ejson": "^2.2.3", "event-loop-stats": "^1.4.1", "eventemitter3": "^5.0.1", diff --git a/ee/packages/license/CHANGELOG.md b/ee/packages/license/CHANGELOG.md index 74c0bafe031dc..02aae794eb287 100644 --- a/ee/packages/license/CHANGELOG.md +++ b/ee/packages/license/CHANGELOG.md @@ -1,5 +1,86 @@ # @rocket.chat/license +## 1.0.12-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 +
      + +## 1.0.12-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 +
      + +## 1.0.12-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 +
      + +## 1.0.12-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 +
      + +## 1.0.12-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 +
      + +## 1.0.12-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 +
      + +## 1.0.12-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 +
      + +## 1.0.12-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 +
      + +## 1.0.12-rc.0 + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 +
      + ## 1.0.11 ### Patch Changes diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 5c8e20276d07e..ea7128f6ced9f 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/license", - "version": "1.0.11", + "version": "1.0.12-rc.8", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts index bb816ea4183bd..308fab6e1e4bb 100644 --- a/ee/packages/license/src/index.ts +++ b/ee/packages/license/src/index.ts @@ -1,5 +1,6 @@ export { DuplicatedLicenseError } from './errors/DuplicatedLicenseError'; export * from './licenseImp'; +export * from './license'; export * from './MockedLicenseBuilder'; export * from './applyLicense'; export * from './AirGappedRestriction'; diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 99433ca1a8e10..677be2bb5112f 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -14,17 +14,33 @@ import type { import { Emitter } from '@rocket.chat/emitter'; import { getLicenseLimit } from './deprecated'; +import type { getAppsConfig, getMaxActiveUsers, getUnmodifiedLicenseAndModules } from './deprecated'; import { DuplicatedLicenseError } from './errors/DuplicatedLicenseError'; import { InvalidLicenseError } from './errors/InvalidLicenseError'; import { NotReadyForValidation } from './errors/NotReadyForValidation'; +import type { onLicense } from './events/deprecated'; import { behaviorTriggered, behaviorTriggeredToggled, licenseInvalidated, licenseValidated } from './events/emitter'; +import type { + onBehaviorTriggered, + onInvalidFeature, + onInvalidateLicense, + onLimitReached, + onModule, + onToggledFeature, + onValidFeature, + onValidateLicense, +} from './events/listeners'; +import type { overwriteClassOnLicense } from './events/overwriteClassOnLicense'; import { logger } from './logger'; +import type { getModuleDefinition, hasModule } from './modules'; import { getExternalModules, getModules, invalidateAll, replaceModules } from './modules'; import { applyPendingLicense, clearPendingLicense, hasPendingLicense, isPendingLicense, setPendingLicense } from './pendingLicense'; +import type { getTags } from './tags'; import { replaceTags } from './tags'; import { decrypt } from './token'; import { convertToV3 } from './v2/convertToV3'; import { filterBehaviorsResult } from './validation/filterBehaviorsResult'; +import type { setLicenseLimitCounter } from './validation/getCurrentValueForLicenseLimit'; import { getCurrentValueForLicenseLimit } from './validation/getCurrentValueForLicenseLimit'; import { getModulesToDisable } from './validation/getModulesToDisable'; import { isBehaviorsInResult } from './validation/isBehaviorsInResult'; @@ -36,7 +52,55 @@ import { validateLicenseLimits } from './validation/validateLicenseLimits'; const globalLimitKinds: LicenseLimitKind[] = ['activeUsers', 'guestUsers', 'privateApps', 'marketplaceApps', 'monthlyActiveContacts']; -export class LicenseManager extends Emitter { +export abstract class LicenseManager extends Emitter { + abstract validateFormat: typeof validateFormat; + + abstract hasModule: typeof hasModule; + + abstract getModules: typeof getModules; + + abstract getModuleDefinition: typeof getModuleDefinition; + + abstract getExternalModules: typeof getExternalModules; + + abstract getTags: typeof getTags; + + abstract overwriteClassOnLicense: typeof overwriteClassOnLicense; + + abstract setLicenseLimitCounter: typeof setLicenseLimitCounter; + + abstract getCurrentValueForLicenseLimit: typeof getCurrentValueForLicenseLimit; + + abstract isLimitReached(action: T, context?: Partial>): Promise; + + abstract onValidFeature: typeof onValidFeature; + + abstract onInvalidFeature: typeof onInvalidFeature; + + abstract onToggledFeature: typeof onToggledFeature; + + abstract onModule: typeof onModule; + + abstract onValidateLicense: typeof onValidateLicense; + + abstract onInvalidateLicense: typeof onInvalidateLicense; + + abstract onLimitReached: typeof onLimitReached; + + abstract onBehaviorTriggered: typeof onBehaviorTriggered; + + // Deprecated: + abstract onLicense: typeof onLicense; + + // Deprecated: + abstract getMaxActiveUsers: typeof getMaxActiveUsers; + + // Deprecated: + abstract getAppsConfig: typeof getAppsConfig; + + // Deprecated: + abstract getUnmodifiedLicenseAndModules: typeof getUnmodifiedLicenseAndModules; + dataCounters = new Map) => Promise>(); pendingLicense = ''; diff --git a/ee/packages/license/src/licenseImp.ts b/ee/packages/license/src/licenseImp.ts index 8305665536501..e5795751942f5 100644 --- a/ee/packages/license/src/licenseImp.ts +++ b/ee/packages/license/src/licenseImp.ts @@ -1,4 +1,4 @@ -import type { LicenseLimitKind, LicenseInfo, LimitContext } from '@rocket.chat/core-typings'; +import type { LicenseLimitKind, LimitContext } from '@rocket.chat/core-typings'; import { getAppsConfig, getMaxActiveUsers, getUnmodifiedLicenseAndModules } from './deprecated'; import { onLicense } from './events/deprecated'; @@ -27,40 +27,7 @@ import { getCurrentValueForLicenseLimit, setLicenseLimitCounter } from './valida import { validateFormat } from './validation/validateFormat'; // eslint-disable-next-line @typescript-eslint/naming-convention -interface License { - validateFormat: typeof validateFormat; - hasModule: typeof hasModule; - getModules: typeof getModules; - getModuleDefinition: typeof getModuleDefinition; - getExternalModules: typeof getExternalModules; - getTags: typeof getTags; - overwriteClassOnLicense: typeof overwriteClassOnLicense; - setLicenseLimitCounter: typeof setLicenseLimitCounter; - getCurrentValueForLicenseLimit: typeof getCurrentValueForLicenseLimit; - isLimitReached: (action: T, context?: Partial>) => Promise; - onValidFeature: typeof onValidFeature; - onInvalidFeature: typeof onInvalidFeature; - onToggledFeature: typeof onToggledFeature; - onModule: typeof onModule; - onValidateLicense: typeof onValidateLicense; - onInvalidateLicense: typeof onInvalidateLicense; - onLimitReached: typeof onLimitReached; - onBehaviorTriggered: typeof onBehaviorTriggered; - revalidateLicense: () => Promise; - - getInfo: (info: { limits: boolean; currentValues: boolean; license: boolean }) => Promise; - - // Deprecated: - onLicense: typeof onLicense; - // Deprecated: - getMaxActiveUsers: typeof getMaxActiveUsers; - // Deprecated: - getAppsConfig: typeof getAppsConfig; - // Deprecated: - getUnmodifiedLicenseAndModules: typeof getUnmodifiedLicenseAndModules; -} - -export class LicenseImp extends LicenseManager implements License { +export class LicenseImp extends LicenseManager { constructor() { super(); this.onValidateLicense(() => showLicense.call(this, this.getLicense(), this.hasValidLicense())); diff --git a/ee/packages/network-broker/CHANGELOG.md b/ee/packages/network-broker/CHANGELOG.md index 663d22ee130e7..8ac59acc29ca8 100644 --- a/ee/packages/network-broker/CHANGELOG.md +++ b/ee/packages/network-broker/CHANGELOG.md @@ -1,5 +1,90 @@ # @rocket.chat/network-broker +## 0.2.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-services@0.9.0-rc.8 +
      + +## 0.2.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-services@0.9.0-rc.7 +
      + +## 0.2.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-services@0.9.0-rc.6 +
      + +## 0.2.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-services@0.9.0-rc.5 +
      + +## 0.2.0-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-services@0.9.0-rc.4 +
      + +## 0.2.0-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-services@0.9.0-rc.3 +
      + +## 0.2.0-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-services@0.9.0-rc.2 +
      + +## 0.2.0-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-services@0.9.0-rc.1 +
      + +## 0.2.0-rc.0 + +### Minor Changes + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +### Patch Changes + +-
      Updated dependencies [d8eb824d242cbbeafb11b1c4a806860e4541ba79, e868a6f6598b7eb2843ef79126d18abd1f604b4f]: + + - @rocket.chat/core-services@0.9.0-rc.0 +
      + ## 0.1.12 ### Patch Changes diff --git a/ee/packages/network-broker/package.json b/ee/packages/network-broker/package.json index be881a9067623..e35e35ab7e5a0 100644 --- a/ee/packages/network-broker/package.json +++ b/ee/packages/network-broker/package.json @@ -1,12 +1,12 @@ { "name": "@rocket.chat/network-broker", - "version": "0.1.12", + "version": "0.2.0-rc.8", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", "@types/chai": "~4.3.20", "@types/ejson": "^2.2.2", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "@types/sinon": "^10.0.20", "chai": "^4.5.0", "eslint": "~8.45.0", diff --git a/ee/packages/network-broker/src/NetworkBroker.ts b/ee/packages/network-broker/src/NetworkBroker.ts index c38650e9ad7de..99d8716222413 100644 --- a/ee/packages/network-broker/src/NetworkBroker.ts +++ b/ee/packages/network-broker/src/NetworkBroker.ts @@ -1,5 +1,5 @@ import { asyncLocalStorage } from '@rocket.chat/core-services'; -import type { IBroker, IBrokerNode, IServiceMetrics, IServiceClass, EventSignatures } from '@rocket.chat/core-services'; +import type { CallingOptions, IBroker, IBrokerNode, IServiceMetrics, IServiceClass, EventSignatures } from '@rocket.chat/core-services'; import { injectCurrentContext, tracerSpan } from '@rocket.chat/tracing'; import type { ServiceBroker, Context, ServiceSchema } from 'moleculer'; @@ -32,7 +32,7 @@ export class NetworkBroker implements IBroker { this.metrics = broker.metrics; } - async call(method: string, data: any): Promise { + async call(method: string, data: any, options?: CallingOptions): Promise { if (!(await this.started)) { return; } @@ -40,17 +40,19 @@ export class NetworkBroker implements IBroker { const context = asyncLocalStorage.getStore(); if (context?.ctx?.call) { - return context.ctx.call(method, data); + return context.ctx.call(method, data, options); } const services: { name: string }[] = await this.broker.call('$node.services', { onlyAvailable: true, }); + if (!services.find((service) => service.name === method.split('.')[0])) { return new Error('method-not-available'); } return this.broker.call(method, data, { + ...options, meta: { optl: injectCurrentContext(), }, diff --git a/ee/packages/omnichannel-services/CHANGELOG.md b/ee/packages/omnichannel-services/CHANGELOG.md index 3a61b6a8e3f84..a3037ae4f2199 100644 --- a/ee/packages/omnichannel-services/CHANGELOG.md +++ b/ee/packages/omnichannel-services/CHANGELOG.md @@ -1,5 +1,131 @@ # @rocket.chat/omnichannel-services +## 0.3.18-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/rest-typings@7.6.0-rc.8 + - @rocket.chat/pdf-worker@0.3.0-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 +
      + +## 0.3.18-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/rest-typings@7.6.0-rc.7 + - @rocket.chat/pdf-worker@0.3.0-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 +
      + +## 0.3.18-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/rest-typings@7.6.0-rc.6 + - @rocket.chat/pdf-worker@0.3.0-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 +
      + +## 0.3.18-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/rest-typings@7.6.0-rc.5 + - @rocket.chat/pdf-worker@0.3.0-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 +
      + +## 0.3.18-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/rest-typings@7.6.0-rc.4 + - @rocket.chat/pdf-worker@0.3.0-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 +
      + +## 0.3.18-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/rest-typings@7.6.0-rc.3 + - @rocket.chat/pdf-worker@0.3.0-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 +
      + +## 0.3.18-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/rest-typings@7.6.0-rc.2 + - @rocket.chat/pdf-worker@0.3.0-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 +
      + +## 0.3.18-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/rest-typings@7.6.0-rc.1 + - @rocket.chat/pdf-worker@0.3.0-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 +
      + +## 0.3.18-rc.0 + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 2ee1a81de770a682f6e7a8590a896e76a32f4e3c, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/pdf-worker@0.3.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 +
      + ## 0.3.17 ### Patch Changes diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index 39bf01bb46581..d67c0d7408df1 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/omnichannel-services", - "version": "0.3.17", + "version": "0.3.18-rc.8", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", @@ -21,7 +21,7 @@ "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@rocket.chat/tools": "workspace:^", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "date-fns": "^2.30.0", "ejson": "^2.2.3", "emoji-toolkit": "^7.0.1", diff --git a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts index 8f4d0e44b5544..20288a4427ca3 100644 --- a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts +++ b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts @@ -17,6 +17,7 @@ import type { ILivechatVisitor, ILivechatAgent, IOmnichannelSystemMessage, + AtLeast, } from '@rocket.chat/core-typings'; import { isQuoteAttachment, isFileAttachment, isFileImageAttachment } from '@rocket.chat/core-typings'; import type { Logger } from '@rocket.chat/logger'; @@ -64,7 +65,7 @@ export type MessageData = Pick< type WorkerData = { siteName: string; visitor: Pick | null; - agent: ILivechatAgent | undefined; + agent: ILivechatAgent | undefined | null; closedAt?: Date; messages: MessageData[]; timezone: string; @@ -101,7 +102,7 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT this.log = new loggerClass('OmnichannelTranscript'); } - async getTimezone(user?: { utcOffset?: string | number }): Promise { + async getTimezone(user?: AtLeast | null): Promise { const reportingTimezone = await settingsService.get('Default_Timezone_For_Reporting'); switch (reportingTimezone) { diff --git a/ee/packages/pdf-worker/CHANGELOG.md b/ee/packages/pdf-worker/CHANGELOG.md index da7fda06ef1c2..e52b030de3bbe 100644 --- a/ee/packages/pdf-worker/CHANGELOG.md +++ b/ee/packages/pdf-worker/CHANGELOG.md @@ -1,5 +1,92 @@ # @rocket.chat/pdf-worker +## 0.3.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 +
      + +## 0.3.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 +
      + +## 0.3.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 +
      + +## 0.3.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 +
      + +## 0.3.0-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 +
      + +## 0.3.0-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 +
      + +## 0.3.0-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 +
      + +## 0.3.0-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 +
      + +## 0.3.0-rc.0 + +### Minor Changes + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +### Patch Changes + +- ([#35705](https://github.com/RocketChat/Rocket.Chat/pull/35705)) Fixes an issue with PDF generation process that caused the server to hang when a single message consisted of too many (+30) markdown elements and was followed and preceded by more messages. + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 +
      + ## 0.2.17 ### Patch Changes diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index cfa0026c61fce..1051e6cc44304 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/pdf-worker", - "version": "0.2.17", + "version": "0.3.0-rc.8", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Message.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Message.tsx index 5b6bf2fb6bd92..78338f8ab55f2 100644 --- a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Message.tsx +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Message.tsx @@ -7,6 +7,7 @@ import { Divider } from './Divider'; import { Files } from './Files'; import { MessageHeader } from './MessageHeader'; import { Quotes } from './Quotes'; +import { isSystemMessage, markupEntriesGreaterThan10, messageLongerThanPage, splitByTens } from './utils'; const styles = StyleSheet.create({ wrapper: { @@ -23,20 +24,40 @@ const styles = StyleSheet.create({ }, }); -const messageLongerThanPage = (message: string | undefined) => (message?.length ?? 0) > 1200; +const processMd = (message: PDFMessage) => + splitByTens(message.md).map((chunk, index) => ( + + } /> + + )); -const isSystemMessage = (message: PDFMessage) => !!message.t; +const processMessage = (message: PDFMessage) => { + if (message.md) { + if (markupEntriesGreaterThan10(message.md)) { + return processMd(message); + } -const Message = ({ message, invalidFileMessage }: { message: PDFMessage; invalidFileMessage: string }) => ( - - - {message.divider && } - + return ( - {message.md ? : {message.msg}} + - {message.quotes && } + ); + } + + return ( + + {message.msg} + ); +}; + +const Message = ({ message, invalidFileMessage }: { message: PDFMessage; invalidFileMessage: string }) => ( + + {message.divider && } + + + {processMessage(message)} + {message.quotes && } {message.files && } diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/utils.spec.ts b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/utils.spec.ts new file mode 100644 index 0000000000000..0b5ceb101f4d1 --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/utils.spec.ts @@ -0,0 +1,48 @@ +import { messageLongerThanPage, splitByTens, isSystemMessage, markupEntriesGreaterThan10 } from './utils'; + +describe('utils', () => { + it('should return true if message is longer than MAX_MSG_SIZE', () => { + const message = 'f'.repeat(10000); + expect(messageLongerThanPage(message)).toBe(true); + }); + it('should return false if message is shorter than MAX_MSG_SIZE', () => { + const message = 'f'.repeat(1000); + expect(messageLongerThanPage(message)).toBe(false); + }); + it('should return false if message is exactly MAX_MSG_SIZE', () => { + const message = 'f'.repeat(1200); + expect(messageLongerThanPage(message)).toBe(false); + }); + it('should return true if message has more markdown elements than MAX_MD_ELEMENTS_PER_VIEW', () => { + const message = Array(11).fill({}); + expect(markupEntriesGreaterThan10(message)).toBe(true); + }); + it('should return false if message has less markdown elements than MAX_MD_ELEMENTS_PER_VIEW', () => { + const message = Array(10).fill({}); + expect(markupEntriesGreaterThan10(message)).toBe(false); + }); + it('should return false if message has exactly MAX_MD_ELEMENTS_PER_VIEW markdown elements', () => { + const message = Array(10).fill({}); + expect(markupEntriesGreaterThan10(message)).toBe(false); + }); + it('should split an array by groups of 10', () => { + const message = Array(11).fill({}); + expect(splitByTens(message)).toEqual([Array(10).fill({}), Array(1).fill({})]); + }); + it('should split an array by groups of 10', () => { + const message = Array(21).fill({}); + expect(splitByTens(message)).toEqual([Array(10).fill({}), Array(10).fill({}), Array(1).fill({})]); + }); + it('should split an array by groups of 10', () => { + const message = Array(8).fill({}); + expect(splitByTens(message)).toEqual([Array(8).fill({})]); + }); + it('should return true if message is a system message', () => { + const message = { t: 'system' }; + expect(isSystemMessage(message as any)).toBe(true); + }); + it('should return false if message is not a system message', () => { + const message = { msg: 'text' }; + expect(isSystemMessage(message as any)).toBe(false); + }); +}); diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/utils.ts b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/utils.ts new file mode 100644 index 0000000000000..099d61951465a --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/utils.ts @@ -0,0 +1,19 @@ +import type { PDFMessage } from '..'; + +const MAX_MD_ELEMENTS_PER_VIEW = 10; +const MAX_MSG_SIZE = 1200; + +export const messageLongerThanPage = (message: string | undefined) => (message?.length ?? 0) > MAX_MSG_SIZE; + +// When a markup list is greater than 10 (magic number, but a reasonable small/big number) we're gonna split the markdown into multiple element +// So react-pdf can split them evenly across pages +export const markupEntriesGreaterThan10 = (messageMd: unknown[] = []) => messageMd.length > MAX_MD_ELEMENTS_PER_VIEW; +export const splitByTens = (array: unknown[] = []): unknown[][] => { + const result = []; + for (let i = 0; i < array.length; i += 10) { + result.push(array.slice(i, i + 10)); + } + return result; +}; + +export const isSystemMessage = (message: PDFMessage) => !!message.t; diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/index.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/index.tsx index 36dc77fe4f1c3..8c0a8085c344b 100644 --- a/ee/packages/pdf-worker/src/templates/ChatTranscript/index.tsx +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/index.tsx @@ -65,7 +65,7 @@ export const ChatTranscriptPDF = ({ header, messages, t }: ChatTranscriptData) = return ( - +
      ( - + {lines.map((line, index) => ( {line.value?.value || ' '} diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/OrderedListBlock.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/OrderedListBlock.tsx index c394e0a72ebd7..fbfecc379651f 100644 --- a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/OrderedListBlock.tsx +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/OrderedListBlock.tsx @@ -21,7 +21,7 @@ type OrderedListBlockProps = { }; const OrderedListBlock = ({ items }: OrderedListBlockProps) => ( - + {items.map(({ value, number }, index) => ( {number}. diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/UnorderedListBlock.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/UnorderedListBlock.tsx index dbedb8a0b5e02..ec074fc77d9fb 100644 --- a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/UnorderedListBlock.tsx +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/UnorderedListBlock.tsx @@ -20,7 +20,7 @@ type UnorderedListBlockProps = { items: MessageParser.ListItem[]; }; const UnorderedListBlock = ({ items }: UnorderedListBlockProps) => ( - + {items.map(({ value }, index) => ( diff --git a/ee/packages/pdf-worker/src/worker.fixtures.ts b/ee/packages/pdf-worker/src/worker.fixtures.ts index a1a6cf388e4c5..2ccf74ed0ac18 100644 --- a/ee/packages/pdf-worker/src/worker.fixtures.ts +++ b/ee/packages/pdf-worker/src/worker.fixtures.ts @@ -770,3 +770,676 @@ export const dataWithASingleSystemMessage = { }, ], }; + +export const dataWith2ReallyBigMessages = { + agent: { + name: 'Juanito De Ponce', + username: 'juanito.ponce', + }, + visitor: { + name: 'Christian Castro', + username: 'christian.castro', + }, + siteName: 'Rocket.Chat', + closedAt: '2022-11-21T00:00:00.000Z', + dateFormat: 'MMM D, YYYY', + timeAndDateFormat: 'MMM D, YYYY H:mm:ss', + timezone: 'Etc/GMT+1', + translations: [ + { + key: 'Agent', + value: 'Agent', + }, + { + key: 'Date', + value: 'Date', + }, + { + key: 'Customer', + value: 'Customer', + }, + { + key: 'Chat_transcript', + value: 'Chat transcript', + }, + { + key: 'Time', + value: 'Time', + }, + { + key: 'This_attachment_is_not_supported', + value: 'Attachment format not supported', + }, + ], + messages: [ + { + _id: 'nDYb7NKuL3T7RL6Wg', + rid: 'Zyutf8db4pSn3qbW4', + msg: 'Guten Tag alle! Ich brauche eine kleine Hilfe mit der TechSuiteX Anwendung.', + ts: '2025-04-02T12:55:06.279Z', + u: { + _id: '67e6671d9ddc2fe11b73ec5b', + username: 'Guest', + name: 'Anonymous User', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Guten Tag alle! ', + }, + { + type: 'EMOJI', + value: { + type: 'PLAIN_TEXT', + value: ':)', + }, + shortCode: 'slight_smile', + }, + { + type: 'PLAIN_TEXT', + value: ' Ich brauche eine kleine Hilfe mit der TechSuiteX Anwendung.', + }, + ], + }, + ], + files: [], + quotes: [], + }, + { + _id: 'vM2j9MFa4aXQukWJG', + rid: 'Zyutf8db4pSn3qbW4', + msg: 'Könntet ihr mir die Mindestvoraussetzungen für V2.0 mitteilen? Und ebenso die Spezifikationen für V2.3? Wir planen die Anschaffung eines stärkeren Computers, und unser IT-Team hat nach den Details gefragt.', + ts: '2025-04-02T12:56:32.098Z', + u: { + _id: '67e6671d9ddc2fe11b73ec5b', + username: 'Guest', + name: 'Anonymous User', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: + 'Könntet ihr mir die Mindestvoraussetzungen für V2.0 mitteilen? Und ebenso die Spezifikationen für V2.3? Wir planen die Anschaffung eines stärkeren Computers, und unser IT-Team hat nach den Details gefragt.', + }, + ], + }, + ], + files: [], + quotes: [], + }, + { + _id: 'T8nHTGt6TnuoSJqCj', + rid: 'Zyutf8db4pSn3qbW4', + msg: 'Willkommen bei der TechSupport Kundenhotline!', + ts: '2025-04-02T12:58:36.380Z', + u: { + _id: 'K4hFYDc2aFXhcRPGj', + username: 'User123', + name: 'Anonymous User', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Willkommen bei der TechSupport Kundenhotline!', + }, + ], + }, + ], + files: [], + quotes: [], + }, + { + _id: 'YCXWJ32cFSPdxwuX8', + rid: 'Zyutf8db4pSn3qbW4', + msg: 'Guten Tag, danke für Ihre Nachricht. Ich stehe Ihnen im Support-Chat zur Verfügung.', + ts: '2025-04-02T12:58:50.921Z', + u: { + _id: 'K4hFYDc2aFXhcRPGj', + username: 'KosuchK', + name: 'Kosuch, Karl-Heinz', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Guten Tag, danke für Ihre Nachricht. Ich stehe Ihnen im Support-Chat zur Verfügung.', + }, + ], + }, + ], + files: [], + quotes: [], + }, + { + _id: 'QvoAfRg4AAXCCgFuE', + rid: 'Zyutf8db4pSn3qbW4', + msg: 'Here are the system requirements for the application:\n\n1. Hardware Requirements\n\nMinimum system requirements:\n• Standard PC with Intel processor, at least 3.1 GHz\n• 4 GB RAM\n• At least 10 GB of free disk space\n• Screen resolution of at least 1280 x 768 pixels and 65k colors\n• DVD drive for installation (USB installation possible)\n• Required interfaces for peripherals: RS-232, Ethernet, USB 2.0\n• Printer: Any OS-supported printer\n\n¹ Adapter required if no free RS-232 port is available.\n² Ethernet adapter required if no free port is available.\n\nRecommended system:\n• Intel Core i5, 3.4 GHz\n• 8 GB (preferably 16 GB) RAM\n• 500 GB SSD storage\n• Screen resolution of 1920 x 1080 pixels\n• DVD drive\n• 1 serial RS-232 interface\n• 2 × 1-Gbit Ethernet interfaces\n\nSpecial requirements apply for advanced peripherals.\n\n2. Software Requirements\n\n• Operating Systems:\n - Windows 7 / 8 / 8.1 / 10 (latest service pack recommended)\n - Future versions will support only Windows 10.\n - Graphics driver must support OpenGL V2.1 or higher.\n\n• Media Player:\n - Some OS versions do not include the default media player.\n\n• Office Integration:\n - Spreadsheet and document software must be installed to use export features.', + ts: '2025-04-02T13:01:04.324Z', + u: { + _id: 'K4hFYDc2aFXhcRPGj', + username: 'User123', + name: 'Anonymous User', + }, + md: [ + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: 'Here are the system requirements for the application' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '1. Hardware Requirements' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: 'Minimum system requirements:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Standard PC with Intel processor, at least 3.1 GHz' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• 4 GB RAM' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• At least 10 GB of free disk space' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Screen resolution of at least 1280 x 768 pixels and 65k colors' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• DVD drive for installation (USB installation possible)' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Required interfaces: RS-232, Ethernet, USB 2.0' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Printer: Any OS-supported printer' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '¹ Adapter required if no free RS-232 port is available.' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '² Ethernet adapter required if no free port is available.' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: 'Recommended system:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Intel Core i5, 3.4 GHz' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• 8 GB (preferably 16 GB) RAM' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• 500 GB SSD storage' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Screen resolution of 1920 x 1080 pixels' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• DVD drive' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• 1 serial RS-232 interface' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• 2 × 1-Gbit Ethernet interfaces' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: 'Special requirements apply for advanced peripherals.' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '2. Software Requirements' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Operating Systems:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: ' - Windows 7 / 8 / 8.1 / 10 (latest service pack recommended)' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: ' - Future versions will support only Windows 10.' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: ' - Graphics driver must support OpenGL V2.1 or higher.' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Media Player:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: ' - Some OS versions do not include the default media player.' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { + type: 'PARAGRAPH', + value: [{ type: 'PLAIN_TEXT', value: ' - Spreadsheet and document software must be installed to use export features.' }], + }, + ], + }, + { + _id: 'b8mMFBNoDe6umGP6e', + rid: 'Zyutf8db4pSn3qbW4', + msg: 'Here are the system requirements for Application X V1.91\n1. Hardware Requirements\n\nRecommended System:\n• Intel Core-i5, 3.4 GHz (Turbo > 4 GHz)\n• 16 GB RAM\n• 500 GB SSD\n• Screen resolution 1920 x 1080\n• 2 * 1-Gbit Ethernet interfaces (Communication with test machine and company network)\n(• USB 2.0 interfaces when using USB devices)\n(• RS-232 interfaces when using RS-232 devices; USB-RS-232 adapter possible)\n\nSpecial requirements apply when using additional peripherals or starting multiple devices at once.\nSince 1.9.2024, Application X is available as a download from the customer portal and can be downloaded. No DVD is included by default.\n(https://www.example.com/)\n\n2. Software Requirements\n\n• Operating Systems:\n - Microsoft Windows 11 from Application X V1.6\n - Microsoft Windows 7 up to Application X V1.5\n - Microsoft Windows 10 for all Application X versions\n\nIt is generally recommended to install the latest service pack for the operating system.\n\n• Required Programs:\n - Media Player\n - Microsoft Excel or Word if using optional export interfaces for these types', + ts: '2025-04-02T13:03:06.045Z', + u: { + _id: 'K4hFYDc2aFXhcRPGj', + username: 'User123', + name: 'Anonymous User', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Here are the system requirements for Application X V1.91', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '1. Hardware Requirements', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Recommended System:', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• Intel Core-i5, 3.4 GHz (Turbo > 4 GHz)', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• 16 GB RAM', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• 500 GB SSD', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• Screen resolution 1920 x 1080', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• 2 * 1-Gbit Ethernet interfaces (Communication with test machine and company network)', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '(• USB 2.0 interfaces when using USB devices)', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '(• RS-232 interfaces when using RS-232 devices; USB-RS-232 adapter possible)', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Special requirements apply when using additional peripherals or starting multiple devices at once.', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: + 'Since 1.9.2024, Application X is available as a download from the customer portal and can be downloaded. No DVD is included by default.', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '(https://www.example.com/)', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '2. Software Requirements', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• Operating Systems:', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: ' - Microsoft Windows 11 from Application X V1.6', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: ' - Microsoft Windows 7 up to Application X V1.5', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: ' - Microsoft Windows 10 for all Application X versions', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'It is generally recommended to install the latest service pack for the operating system.', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• Required Programs:', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: ' - Media Player', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '( - Microsoft Excel or Word if using optional export interfaces for these types)', + }, + ], + }, + ], + files: [], + quotes: [], + }, + { + _id: 'b8mMFBNoDe6umGP6e', + rid: 'Zyutf8db4pSn3qbW4', + msg: 'Here are the system requirements for Application X V1.91\n1. Hardware Requirements\n\nRecommended System:\n• Intel Core-i5, 3.4 GHz (Turbo > 4 GHz)\n• 16 GB RAM\n• 500 GB SSD\n• Screen resolution 1920 x 1080\n• 2 * 1-Gbit Ethernet interfaces (Communication with test machine and company network)\n(• USB 2.0 interfaces when using USB devices)\n(• RS-232 interfaces when using RS-232 devices; USB-RS-232 adapter possible)\n\nSpecial requirements apply when using additional peripherals or starting multiple devices at once.\nSince 1.9.2024, Application X is available as a download from the customer portal and can be downloaded. No DVD is included by default.\n(https://www.example.com/)\n\n2. Software Requirements\n\n• Operating Systems:\n - Microsoft Windows 11 from Application X V1.6\n - Microsoft Windows 7 up to Application X V1.5\n - Microsoft Windows 10 for all Application X versions\n\nIt is generally recommended to install the latest service pack for the operating system.\n\n• Required Programs:\n - Media Player\n - Microsoft Excel or Word if using optional export interfaces for these types', + ts: '2025-04-02T13:03:06.045Z', + u: { + _id: 'K4hFYDc2aFXhcRPGj', + username: 'User123', + name: 'Anonymous User', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Here are the system requirements for Application X V1.91', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '1. Hardware Requirements', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Recommended System:', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• Intel Core-i5, 3.4 GHz (Turbo > 4 GHz)', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• 16 GB RAM', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• 500 GB SSD', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• Screen resolution 1920 x 1080', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• 2 * 1-Gbit Ethernet interfaces (Communication with test machine and company network)', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '(• USB 2.0 interfaces when using USB devices)', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '(• RS-232 interfaces when using RS-232 devices; USB-RS-232 adapter possible)', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Special requirements apply when using additional peripherals or starting multiple devices at once.', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: + 'Since 1.9.2024, Application X is available as a download from the customer portal and can be downloaded. No DVD is included by default.', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '(https://www.example.com/)', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '2. Software Requirements', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• Operating Systems:', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: ' - Microsoft Windows 11 from Application X V1.6', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: ' - Microsoft Windows 7 up to Application X V1.5', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: ' - Microsoft Windows 10 for all Application X versions', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'It is generally recommended to install the latest service pack for the operating system.', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• Required Programs:', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: ' - Media Player', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '( - Microsoft Excel or Word if using optional export interfaces for these types)', + }, + ], + }, + ], + files: [], + quotes: [], + }, + ], +}; diff --git a/ee/packages/pdf-worker/src/worker.spec.ts b/ee/packages/pdf-worker/src/worker.spec.ts index d880700194beb..02c8af09a28b5 100644 --- a/ee/packages/pdf-worker/src/worker.spec.ts +++ b/ee/packages/pdf-worker/src/worker.spec.ts @@ -9,6 +9,7 @@ import { dataWithMultipleMessagesAndABigMessage, dataWithASingleMessageAndAnImage, dataWithASingleSystemMessage, + dataWith2ReallyBigMessages, } from './worker.fixtures'; const streamToBuffer = async (stream: NodeJS.ReadableStream) => { @@ -79,6 +80,13 @@ it('should generate a pdf transcript for a single system message', async () => { expect(buffer).toBeTruthy(); }); +it('should generate a pdf transcript for rooms with messages consisting of tons of markdown elements', async () => { + const stream = await pdfWorker.renderToStream({ data: dataWith2ReallyBigMessages }); + const buffer = await streamToBuffer(stream); + + expect(buffer).toBeTruthy(); +}); + describe('isMimeTypeValid', () => { it('should return true if mimeType is valid', () => { expect(pdfWorker.isMimeTypeValid('image/png')).toBe(true); diff --git a/ee/packages/presence/CHANGELOG.md b/ee/packages/presence/CHANGELOG.md index 9ae96511649d5..bc466f1b0549b 100644 --- a/ee/packages/presence/CHANGELOG.md +++ b/ee/packages/presence/CHANGELOG.md @@ -1,5 +1,104 @@ # @rocket.chat/presence +## 0.2.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 +
      + +## 0.2.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 +
      + +## 0.2.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 +
      + +## 0.2.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 +
      + +## 0.2.21-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 +
      + +## 0.2.21-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 +
      + +## 0.2.21-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 +
      + +## 0.2.21-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 +
      + +## 0.2.21-rc.0 + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 +
      + ## 0.2.20 ### Patch Changes diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index b73af6c949639..e721892b1515d 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/presence", - "version": "0.2.20", + "version": "0.2.21-rc.8", "private": true, "devDependencies": { "@babel/core": "~7.26.0", @@ -9,7 +9,7 @@ "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "babel-jest": "^29.7.0", "eslint": "~8.45.0", "jest": "~29.7.0", diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index 78b3ade346697..e7b19ed262df5 100644 --- a/ee/packages/ui-theming/package.json +++ b/ee/packages/ui-theming/package.json @@ -4,9 +4,9 @@ "private": true, "devDependencies": { "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "~0.61.0", + "@rocket.chat/fuselage": "~0.62.0", "@rocket.chat/fuselage-hooks": "~0.35.0", - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@rocket.chat/ui-contexts": "workspace:~", "@types/react": "~18.3.17", "eslint": "~8.45.0", diff --git a/package.json b/package.json index 83a9a8de57dd7..32378f60dda88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "7.5.1", + "version": "7.6.0-rc.8", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/api-client/CHANGELOG.md b/packages/api-client/CHANGELOG.md index 4efd94b4a5616..4d82cc2577803 100644 --- a/packages/api-client/CHANGELOG.md +++ b/packages/api-client/CHANGELOG.md @@ -1,5 +1,95 @@ # @rocket.chat/api-client +## 0.2.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/rest-typings@7.6.0-rc.8 +
      + +## 0.2.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/rest-typings@7.6.0-rc.7 +
      + +## 0.2.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/rest-typings@7.6.0-rc.6 +
      + +## 0.2.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/rest-typings@7.6.0-rc.5 +
      + +## 0.2.21-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/rest-typings@7.6.0-rc.4 +
      + +## 0.2.21-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/rest-typings@7.6.0-rc.3 +
      + +## 0.2.21-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/rest-typings@7.6.0-rc.2 +
      + +## 0.2.21-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/rest-typings@7.6.0-rc.1 +
      + +## 0.2.21-rc.0 + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 +
      + ## 0.2.20 ### Patch Changes diff --git a/packages/api-client/package.json b/packages/api-client/package.json index d433ecd981f88..35df2c8236fbf 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/api-client", - "version": "0.2.20", + "version": "0.2.21-rc.8", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/apps-engine/CHANGELOG.md b/packages/apps-engine/CHANGELOG.md index 60377b01a9048..6762906013c60 100644 --- a/packages/apps-engine/CHANGELOG.md +++ b/packages/apps-engine/CHANGELOG.md @@ -1,5 +1,13 @@ # @rocket.chat/apps-engine +## 1.51.0-rc.0 + +### Minor Changes + +- ([#35280](https://github.com/RocketChat/Rocket.Chat/pull/35280)) Allows apps to react to department status changes. + +- ([#35644](https://github.com/RocketChat/Rocket.Chat/pull/35644)) Adds the ability to dynamically add and remove options from select/multi-select settings in the Apps Engine to support more flexible configuration scenarios by exposing two new methods on the settings API. + ## 1.50.0 ### Minor Changes diff --git a/packages/apps-engine/package.json b/packages/apps-engine/package.json index eb383c0ee4244..625235b7a0bcd 100644 --- a/packages/apps-engine/package.json +++ b/packages/apps-engine/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps-engine", - "version": "1.50.0", + "version": "1.51.0-rc.0", "description": "The engine code for the Rocket.Chat Apps which manages, runs, translates, coordinates and all of that.", "main": "index", "typings": "index", @@ -72,7 +72,7 @@ "@types/debug": "^4.1.12", "@types/lodash.clonedeep": "^4.5.9", "@types/nedb": "^1.8.16", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "@types/semver": "^7.5.8", "@types/stack-trace": "0.0.33", "@types/uuid": "~10.0.0", @@ -93,6 +93,7 @@ }, "dependencies": { "@msgpack/msgpack": "3.0.0-beta2", + "@rocket.chat/tools": "workspace:^", "adm-zip": "^0.5.16", "debug": "^4.3.7", "esbuild": "^0.25.0", diff --git a/packages/apps-engine/src/definition/accessors/ISettingUpdater.ts b/packages/apps-engine/src/definition/accessors/ISettingUpdater.ts index 3826286df6c93..9cdc15c7b1464 100644 --- a/packages/apps-engine/src/definition/accessors/ISettingUpdater.ts +++ b/packages/apps-engine/src/definition/accessors/ISettingUpdater.ts @@ -2,4 +2,5 @@ import type { ISetting } from '../settings/ISetting'; export interface ISettingUpdater { updateValue(id: ISetting['id'], value: ISetting['value']): Promise; + updateSelectOptions(id: ISetting['id'], values: ISetting['values']): Promise; } diff --git a/packages/apps-engine/src/definition/livechat/ILivechatEventContext.ts b/packages/apps-engine/src/definition/livechat/ILivechatEventContext.ts index b94f07ef0250e..c7c554b8376b5 100644 --- a/packages/apps-engine/src/definition/livechat/ILivechatEventContext.ts +++ b/packages/apps-engine/src/definition/livechat/ILivechatEventContext.ts @@ -1,7 +1,12 @@ import type { IUser } from '../users'; +import type { IDepartment } from './IDepartment'; import type { ILivechatRoom } from './ILivechatRoom'; export interface ILivechatEventContext { agent: IUser; room: ILivechatRoom; } + +export interface ILivechatDepartmentEventContext { + department: IDepartment; +} diff --git a/packages/apps-engine/src/definition/livechat/IPostLivechatDepartmentDisabled.ts b/packages/apps-engine/src/definition/livechat/IPostLivechatDepartmentDisabled.ts new file mode 100644 index 0000000000000..a08b1d61e446f --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IPostLivechatDepartmentDisabled.ts @@ -0,0 +1,25 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; +import type { ILivechatDepartmentEventContext } from './ILivechatEventContext'; + +/** + * Handler called after the disablement of a livechat department. + */ +export interface IPostLivechatDepartmentDisabled { + /** + * Handler called *after* the disablement of a livechat department. + * + * @param data the livechat context data which contains the department disabled + * @param read An accessor to the environment + * @param http An accessor to the outside world + * @param persis An accessor to the App's persistence + * @param modify An accessor to the modifier + */ + [AppMethod.EXECUTE_POST_LIVECHAT_DEPARTMENT_DISABLED]( + context: ILivechatDepartmentEventContext, + read: IRead, + http: IHttp, + persis: IPersistence, + modify?: IModify, + ): Promise; +} diff --git a/packages/apps-engine/src/definition/livechat/IPostLivechatDepartmentRemoved.ts b/packages/apps-engine/src/definition/livechat/IPostLivechatDepartmentRemoved.ts new file mode 100644 index 0000000000000..51a08d81eb88e --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IPostLivechatDepartmentRemoved.ts @@ -0,0 +1,25 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; +import type { ILivechatDepartmentEventContext } from './ILivechatEventContext'; + +/** + * Handler called after the removal of a livechat department. + */ +export interface IPostLivechatDepartmentRemoved { + /** + * Handler called *after* the removal of a livechat department. + * + * @param data the livechat context data which contains the department removed + * @param read An accessor to the environment + * @param http An accessor to the outside world + * @param persis An accessor to the App's persistence + * @param modify An accessor to the modifier + */ + [AppMethod.EXECUTE_POST_LIVECHAT_DEPARTMENT_REMOVED]( + context: ILivechatDepartmentEventContext, + read: IRead, + http: IHttp, + persis: IPersistence, + modify?: IModify, + ): Promise; +} diff --git a/packages/apps-engine/src/definition/livechat/index.ts b/packages/apps-engine/src/definition/livechat/index.ts index c5045751d5bfd..4533df149fd20 100644 --- a/packages/apps-engine/src/definition/livechat/index.ts +++ b/packages/apps-engine/src/definition/livechat/index.ts @@ -8,6 +8,8 @@ import { ILivechatTransferData } from './ILivechatTransferData'; import { ILivechatTransferEventContext, LivechatTransferEventType } from './ILivechatTransferEventContext'; import { IPostLivechatAgentAssigned } from './IPostLivechatAgentAssigned'; import { IPostLivechatAgentUnassigned } from './IPostLivechatAgentUnassigned'; +import { IPostLivechatDepartmentDisabled } from './IPostLivechatDepartmentDisabled'; +import { IPostLivechatDepartmentRemoved } from './IPostLivechatDepartmentRemoved'; import { IPostLivechatGuestSaved } from './IPostLivechatGuestSaved'; import { IPostLivechatRoomClosed } from './IPostLivechatRoomClosed'; import { IPostLivechatRoomSaved } from './IPostLivechatRoomSaved'; @@ -39,4 +41,6 @@ export { IVisitorEmail, IVisitorPhone, LivechatTransferEventType, + IPostLivechatDepartmentRemoved, + IPostLivechatDepartmentDisabled, }; diff --git a/packages/apps-engine/src/definition/metadata/AppInterface.ts b/packages/apps-engine/src/definition/metadata/AppInterface.ts index 6c599bf56c153..79e73d26679da 100644 --- a/packages/apps-engine/src/definition/metadata/AppInterface.ts +++ b/packages/apps-engine/src/definition/metadata/AppInterface.ts @@ -49,6 +49,8 @@ export enum AppInterface { IPostLivechatRoomTransferred = 'IPostLivechatRoomTransferred', IPostLivechatGuestSaved = 'IPostLivechatGuestSaved', IPostLivechatRoomSaved = 'IPostLivechatRoomSaved', + IPostLivechatDepartmentRemoved = 'IPostLivechatDepartmentRemoved', + IPostLivechatDepartmentDisabled = 'IPostLivechatDepartmentDisabled', // FileUpload IPreFileUpload = 'IPreFileUpload', // Email diff --git a/packages/apps-engine/src/definition/metadata/AppMethod.ts b/packages/apps-engine/src/definition/metadata/AppMethod.ts index 50c4cde74f0d4..67e0611290f23 100644 --- a/packages/apps-engine/src/definition/metadata/AppMethod.ts +++ b/packages/apps-engine/src/definition/metadata/AppMethod.ts @@ -90,6 +90,8 @@ export enum AppMethod { EXECUTE_POST_LIVECHAT_ROOM_TRANSFERRED = 'executePostLivechatRoomTransferred', EXECUTE_POST_LIVECHAT_GUEST_SAVED = 'executePostLivechatGuestSaved', EXECUTE_POST_LIVECHAT_ROOM_SAVED = 'executePostLivechatRoomSaved', + EXECUTE_POST_LIVECHAT_DEPARTMENT_DISABLED = 'executePostLivechatDepartmentDisabled', + EXECUTE_POST_LIVECHAT_DEPARTMENT_REMOVED = 'executePostLivechatDepartmentRemoved', // FileUpload EXECUTE_PRE_FILE_UPLOAD = 'executePreFileUpload', // Email diff --git a/packages/apps-engine/src/server/AppManager.ts b/packages/apps-engine/src/server/AppManager.ts index bbc31eef5e630..5e5b1f96e97eb 100644 --- a/packages/apps-engine/src/server/AppManager.ts +++ b/packages/apps-engine/src/server/AppManager.ts @@ -1,5 +1,7 @@ import { Buffer } from 'buffer'; +import { removeEmpty } from '@rocket.chat/tools'; + import type { IGetAppsFilter } from './IGetAppsFilter'; import { ProxiedApp } from './ProxiedApp'; import type { PersistenceBridge, UserBridge } from './bridges'; @@ -607,7 +609,7 @@ export class AppManager { } descriptor.signature = await this.getSignatureManager().signApp(descriptor); - const created = await this.appMetadataStorage.create(descriptor); + const created = await this.appMetadataStorage.create(removeEmpty(descriptor)); if (!created) { aff.setStorageError('Failed to create the App, the storage did not return it.'); @@ -617,6 +619,8 @@ export class AppManager { return aff; } + app.getStorageItem()._id = created._id; + this.apps.set(app.getID(), app); aff.setApp(app); @@ -717,9 +721,9 @@ export class AppManager { languageContent: result.languageContent, settings: old.settings, implemented: result.implemented.getValues(), - marketplaceInfo: old.marketplaceInfo, - sourcePath: old.sourcePath, - permissionsGranted, + ...(old.marketplaceInfo && { marketplaceInfo: old.marketplaceInfo }), + ...(old.sourcePath && { sourcePath: old.sourcePath }), + ...(permissionsGranted && { permissionsGranted }), }; try { diff --git a/packages/apps-engine/src/server/accessors/SettingUpdater.ts b/packages/apps-engine/src/server/accessors/SettingUpdater.ts index 879c1282ee58a..8af37b4bccf75 100644 --- a/packages/apps-engine/src/server/accessors/SettingUpdater.ts +++ b/packages/apps-engine/src/server/accessors/SettingUpdater.ts @@ -3,23 +3,64 @@ import type { ISetting } from '../../definition/settings'; import type { ProxiedApp } from '../ProxiedApp'; import type { AppSettingsManager } from '../managers'; +/** + * Implementation of ISettingUpdater that provides methods to update app settings. + */ export class SettingUpdater implements ISettingUpdater { constructor( private readonly app: ProxiedApp, private readonly manager: AppSettingsManager, ) {} - public async updateValue(id: ISetting['id'], value: ISetting['value']) { - if (!this.app.getStorageItem().settings[id]) { - return; + /** + * Updates a single setting value + * @param id The setting ID to update + * @param value The new value to set + * @returns Promise that resolves when the update is complete + * @throws Error if the setting doesn't exist + */ + public async updateValue(id: ISetting['id'], value: ISetting['value']): Promise { + const appId = this.app.getID(); + const storageItem = this.app.getStorageItem(); + + if (!storageItem.settings?.[id]) { + throw new Error(`Setting "${id}" not found for app ${appId}`); } - const setting = this.manager.getAppSetting(this.app.getID(), id); + const setting = this.manager.getAppSetting(appId, id); - this.manager.updateAppSetting(this.app.getID(), { + this.manager.updateAppSetting(appId, { ...setting, updatedAt: new Date(), value, }); } + + /** + * Updates the values for a multi-value setting by overwriting them + * @param id The setting ID to update + * @param values The new values to set + * @returns Promise that resolves when the update is complete + * @throws Error if the setting doesn't exist + */ + public async updateSelectOptions(id: ISetting['id'], values: ISetting['values']): Promise { + const appId = this.app.getID(); + const storageItem = this.app.getStorageItem(); + + if (!storageItem.settings?.[id]) { + throw new Error(`Setting "${id}" not found for app ${appId}`); + } + + const setting = this.manager.getAppSetting(appId, id); + + // TODO: This operation completely overwrites existing values + // which could lead to loss of selected values. Consider: + // Adding warning logs when selected value will be removed + + this.manager.updateAppSetting(appId, { + ...setting, + updatedAt: new Date(), + values, // Overwrite the values instead of merging + }); + } } diff --git a/packages/apps-engine/src/server/managers/AppListenerManager.ts b/packages/apps-engine/src/server/managers/AppListenerManager.ts index 882a1fdc9d944..5236223be2987 100644 --- a/packages/apps-engine/src/server/managers/AppListenerManager.ts +++ b/packages/apps-engine/src/server/managers/AppListenerManager.ts @@ -3,6 +3,7 @@ import type { IEmailDescriptor, IPreEmailSentContext } from '../../definition/em import { EssentialAppDisabledException } from '../../definition/exceptions'; import type { IExternalComponent } from '../../definition/externalComponent'; import type { ILivechatEventContext, ILivechatRoom, ILivechatTransferEventContext, IVisitor } from '../../definition/livechat'; +import type { ILivechatDepartmentEventContext } from '../../definition/livechat/ILivechatEventContext'; import type { IMessage, IMessageDeleteContext, @@ -194,6 +195,14 @@ interface IListenerExecutor { args: [IVisitor]; result: void; }; + [AppInterface.IPostLivechatDepartmentRemoved]: { + args: [ILivechatDepartmentEventContext]; + result: void; + }; + [AppInterface.IPostLivechatDepartmentDisabled]: { + args: [ILivechatDepartmentEventContext]; + result: void; + }; // FileUpload [AppInterface.IPreFileUpload]: { args: [IFileUploadContext]; @@ -428,6 +437,10 @@ export class AppListenerManager { return this.executePostLivechatAgentUnassigned(data as ILivechatEventContext); case AppInterface.IPostLivechatRoomTransferred: return this.executePostLivechatRoomTransferred(data as ILivechatTransferEventContext); + case AppInterface.IPostLivechatDepartmentRemoved: + return this.executePostLivechatDepartmentRemoved(data as ILivechatDepartmentEventContext); + case AppInterface.IPostLivechatDepartmentDisabled: + return this.executePostLivechatDepartmentDisabled(data as ILivechatDepartmentEventContext); case AppInterface.IPostLivechatGuestSaved: return this.executePostLivechatGuestSaved(data as IVisitor); // FileUpload @@ -1137,6 +1150,22 @@ export class AppListenerManager { } } + private async executePostLivechatDepartmentRemoved(data: ILivechatDepartmentEventContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPostLivechatDepartmentRemoved)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_LIVECHAT_DEPARTMENT_REMOVED, data); + } + } + + private async executePostLivechatDepartmentDisabled(data: ILivechatDepartmentEventContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPostLivechatDepartmentDisabled)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_LIVECHAT_DEPARTMENT_DISABLED, data); + } + } + // FileUpload private async executePreFileUpload(data: IFileUploadContext): Promise { for (const appId of this.listeners.get(AppInterface.IPreFileUpload)) { diff --git a/packages/apps-engine/tests/server/accessors/SettingUpdater.spec.ts b/packages/apps-engine/tests/server/accessors/SettingUpdater.spec.ts new file mode 100644 index 0000000000000..ebd982640bb81 --- /dev/null +++ b/packages/apps-engine/tests/server/accessors/SettingUpdater.spec.ts @@ -0,0 +1,104 @@ +import { AsyncTest, Expect, SetupFixture, SpyOn } from 'alsatian'; + +import type { ProxiedApp } from '../../../src/server/ProxiedApp'; +import { SettingUpdater } from '../../../src/server/accessors'; +import type { AppSettingsManager } from '../../../src/server/managers'; +import type { IAppStorageItem } from '../../../src/server/storage'; +import { TestData } from '../../test-data/utilities'; + +export class SettingUpdaterAccessorTestFixture { + private mockStorageItem: IAppStorageItem; + + private mockProxiedApp: ProxiedApp; + + private mockSettingsManager: AppSettingsManager; + + @SetupFixture + public setupFixture() { + // Set up mock storage with test settings + this.mockStorageItem = { + settings: {}, + } as IAppStorageItem; + + this.mockStorageItem.settings.singleValue = TestData.getSetting('singleValue'); + this.mockStorageItem.settings.multiValue = { + ...TestData.getSetting('multiValue'), + values: [ + { key: 'key1', i18nLabel: 'value1' }, + { key: 'key2', i18nLabel: 'value2' }, + ], + }; + + // Mock ProxiedApp + const si = this.mockStorageItem; + this.mockProxiedApp = { + getStorageItem(): IAppStorageItem { + return si; + }, + getID(): string { + return 'test-app-id'; + }, + } as ProxiedApp; + + // Mock AppSettingsManager + this.mockSettingsManager = {} as AppSettingsManager; + this.mockSettingsManager.getAppSetting = (appId: string, settingId: string) => { + return this.mockStorageItem.settings[settingId]; + }; + this.mockSettingsManager.updateAppSetting = (appId: string, setting: any) => { + this.mockStorageItem.settings[setting.id] = setting; + return Promise.resolve(); + }; + + SpyOn(this.mockSettingsManager, 'getAppSetting'); + SpyOn(this.mockSettingsManager, 'updateAppSetting'); + } + + @AsyncTest() + public async updateValueSuccessfully() { + const settingUpdater = new SettingUpdater(this.mockProxiedApp, this.mockSettingsManager); + + await settingUpdater.updateValue('singleValue', 'updated value'); + + Expect(this.mockSettingsManager.updateAppSetting).toHaveBeenCalled(); + Expect(this.mockStorageItem.settings.singleValue.value).toBe('updated value'); + // Verify updatedAt was set + Expect(this.mockStorageItem.settings.singleValue.updatedAt).toBeDefined(); + } + + @AsyncTest() + public async updateValueThrowsErrorForNonExistentSetting() { + const settingUpdater = new SettingUpdater(this.mockProxiedApp, this.mockSettingsManager); + + await Expect(() => settingUpdater.updateValue('nonExistent', 'value')).toThrowErrorAsync(Error, 'Setting "nonExistent" not found for app test-app-id'); + } + + @AsyncTest() + public async updateSelectOptionsSuccessfully() { + const settingUpdater = new SettingUpdater(this.mockProxiedApp, this.mockSettingsManager); + const newValues = [ + { key: 'key3', i18nLabel: 'value3' }, + { key: 'key4', i18nLabel: 'value4' }, + ]; + + await settingUpdater.updateSelectOptions('multiValue', newValues); + + Expect(this.mockSettingsManager.updateAppSetting).toHaveBeenCalled(); + const updatedValues = this.mockStorageItem.settings.multiValue.values; + // Should completely replace old values + Expect((updatedValues ?? []).length).toBe(2); + Expect(updatedValues).toEqual(newValues); + // Verify updatedAt was set + Expect(this.mockStorageItem.settings.multiValue.updatedAt).toBeDefined(); + } + + @AsyncTest() + public async updateSelectOptionsThrowsErrorForNonExistentSetting() { + const settingUpdater = new SettingUpdater(this.mockProxiedApp, this.mockSettingsManager); + + await Expect(() => settingUpdater.updateSelectOptions('nonExistent', [{ key: 'test', i18nLabel: 'value' }])).toThrowErrorAsync( + Error, + 'Setting "nonExistent" not found for app test-app-id', + ); + } +} diff --git a/packages/apps/CHANGELOG.md b/packages/apps/CHANGELOG.md index e82d549046cb0..6bad5953285be 100644 --- a/packages/apps/CHANGELOG.md +++ b/packages/apps/CHANGELOG.md @@ -1,5 +1,100 @@ # @rocket.chat/apps +## 0.5.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 +
      + +## 0.5.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 +
      + +## 0.5.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 +
      + +## 0.5.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 +
      + +## 0.5.0-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 +
      + +## 0.5.0-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 +
      + +## 0.5.0-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 +
      + +## 0.5.0-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 +
      + +## 0.5.0-rc.0 + +### Minor Changes + +- ([#35280](https://github.com/RocketChat/Rocket.Chat/pull/35280)) Allows apps to react to department status changes. + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d649a761edd71e1325a635b757ef1df2e5a778a4, bbd14f84214b4785f2b58cfeb8e9117bdfbf18e8, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/apps-engine@1.51.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 +
      + ## 0.4.1 ### Patch Changes diff --git a/packages/apps/package.json b/packages/apps/package.json index e5ef1e59ae841..a885412150fcd 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps", - "version": "0.4.1", + "version": "0.5.0-rc.8", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/apps/src/bridges/IListenerBridge.ts b/packages/apps/src/bridges/IListenerBridge.ts index 264d86153dfe4..83c3910d152cc 100644 --- a/packages/apps/src/bridges/IListenerBridge.ts +++ b/packages/apps/src/bridges/IListenerBridge.ts @@ -29,7 +29,11 @@ declare module '@rocket.chat/apps-engine/server/bridges' { roomEvent(int: 'IPostRoomCreate' | 'IPostRoomDeleted', room: IRoom): Promise; livechatEvent( - int: 'IPostLivechatAgentAssigned' | 'IPostLivechatAgentUnassigned', + int: + | 'IPostLivechatAgentAssigned' + | 'IPostLivechatAgentUnassigned' + | 'IPostLivechatDepartmentRemoved' + | 'IPostLivechatDepartmentDisabled', data: { user: IUser; room: IOmnichannelRoom }, ): Promise; livechatEvent( diff --git a/packages/core-services/CHANGELOG.md b/packages/core-services/CHANGELOG.md index c5238aff6990e..989fa02520dba 100644 --- a/packages/core-services/CHANGELOG.md +++ b/packages/core-services/CHANGELOG.md @@ -1,5 +1,110 @@ # @rocket.chat/core-services +## 0.9.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/rest-typings@7.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 +
      + +## 0.9.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/rest-typings@7.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 +
      + +## 0.9.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/rest-typings@7.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 +
      + +## 0.9.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/rest-typings@7.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 +
      + +## 0.9.0-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/rest-typings@7.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 +
      + +## 0.9.0-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/rest-typings@7.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 +
      + +## 0.9.0-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/rest-typings@7.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 +
      + +## 0.9.0-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/rest-typings@7.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 +
      + +## 0.9.0-rc.0 + +### Minor Changes + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +### Patch Changes + +- ([#35757](https://github.com/RocketChat/Rocket.Chat/pull/35757)) Fixes an issue where the bypass to call methods over microservices always returns to `{}` + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 +
      + ## 0.8.1 ### Patch Changes diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 78d5a64af8941..b2b1dae9e028d 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/core-services", - "version": "0.8.1", + "version": "0.9.0-rc.8", "private": true, "devDependencies": { "@babel/core": "~7.26.0", @@ -34,7 +34,7 @@ }, "dependencies": { "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", diff --git a/packages/core-services/src/LocalBroker.ts b/packages/core-services/src/LocalBroker.ts index 9e319c4cdbdb0..be19791097658 100644 --- a/packages/core-services/src/LocalBroker.ts +++ b/packages/core-services/src/LocalBroker.ts @@ -6,7 +6,7 @@ import { injectCurrentContext, tracerActiveSpan } from '@rocket.chat/tracing'; import { asyncLocalStorage } from '.'; import type { EventSignatures } from './events/Events'; -import type { IBroker, IBrokerNode } from './types/IBroker'; +import type { CallingOptions, IBroker, IBrokerNode } from './types/IBroker'; import type { ServiceClass, IServiceClass } from './types/ServiceClass'; type ExtendedServiceClass = { instance: IServiceClass; dependencies: string[]; isStarted: boolean }; @@ -29,7 +29,11 @@ export class LocalBroker implements IBroker { private defaultDependencies = ['settings']; - async call(method: string, data: any): Promise { + async call(method: string, data: any, options?: CallingOptions): Promise { + if (options) { + logger.warn('Options are not supported in LocalBroker'); + } + return tracerActiveSpan( `action ${method}`, {}, diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index dedbe3571aaac..c59df5ee55e66 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -50,13 +50,14 @@ import type { IVideoConfService, VideoConferenceJoinOptions } from './types/IVid import type { IVoipFreeSwitchService } from './types/IVoipFreeSwitchService'; import type { IVoipService } from './types/IVoipService'; +export { AppStatusReport } from './types/IAppsEngineService'; export { asyncLocalStorage } from './lib/asyncLocalStorage'; export { MeteorError, isMeteorError } from './MeteorError'; export { api } from './api'; export { EventSignatures } from './events/Events'; export { LocalBroker } from './LocalBroker'; -export { IBroker, IBrokerNode, BaseMetricOptions, IServiceMetrics } from './types/IBroker'; +export { IBroker, IBrokerNode, BaseMetricOptions, CallingOptions, IServiceMetrics } from './types/IBroker'; export { IServiceContext, ServiceClass, IServiceClass, ServiceClassInternal } from './types/ServiceClass'; diff --git a/packages/core-services/src/lib/Api.ts b/packages/core-services/src/lib/Api.ts index de028dcafd1d5..86516e27b31e1 100644 --- a/packages/core-services/src/lib/Api.ts +++ b/packages/core-services/src/lib/Api.ts @@ -1,6 +1,6 @@ import type { EventSignatures } from '../events/Events'; import type { IApiService } from '../types/IApiService'; -import type { IBroker, IBrokerNode } from '../types/IBroker'; +import type { CallingOptions, IBroker, IBrokerNode } from '../types/IBroker'; import type { IServiceClass } from '../types/ServiceClass'; export class Api implements IApiService { @@ -37,8 +37,8 @@ export class Api implements IApiService { } } - async call(method: string, data?: unknown): Promise { - return this.broker?.call(method, data); + async call(method: string, data?: unknown, options?: CallingOptions): Promise { + return this.broker?.call(method, data, options); } async broadcast(event: T, ...args: Parameters): Promise { diff --git a/packages/core-services/src/types/IApiService.ts b/packages/core-services/src/types/IApiService.ts index bff4bc3a2d82a..ab01517f9a0eb 100644 --- a/packages/core-services/src/types/IApiService.ts +++ b/packages/core-services/src/types/IApiService.ts @@ -1,4 +1,4 @@ -import type { IBroker, IBrokerNode } from './IBroker'; +import type { CallingOptions, IBroker, IBrokerNode } from './IBroker'; import type { IServiceClass } from './ServiceClass'; import type { EventSignatures } from '../events/Events'; @@ -9,7 +9,7 @@ export interface IApiService { registerService(instance: IServiceClass): void; - call(method: string, data?: unknown): Promise; + call(method: string, data?: unknown, options?: CallingOptions): Promise; broadcast(event: T, ...args: Parameters): Promise; diff --git a/packages/core-services/src/types/IAppsEngineService.ts b/packages/core-services/src/types/IAppsEngineService.ts index 9158d2fe3b299..e3abbb350e934 100644 --- a/packages/core-services/src/types/IAppsEngineService.ts +++ b/packages/core-services/src/types/IAppsEngineService.ts @@ -1,9 +1,16 @@ +import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; import type { IGetAppsFilter } from '@rocket.chat/apps-engine/server/IGetAppsFilter'; import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +export type AppStatusReport = { + [appId: string]: { instanceId: string; status: AppStatus }[]; +}; + export interface IAppsEngineService { isInitialized(): boolean; getApps(query: IGetAppsFilter): Promise; getAppStorageItemById(appId: string): Promise; + getAppsStatusLocal(): Promise<{ appId: string; status: AppStatus }[]>; + getAppsStatusInNodes(): Promise; } diff --git a/packages/core-services/src/types/IBroker.ts b/packages/core-services/src/types/IBroker.ts index d72221a09cabe..9ce5b693eaa41 100644 --- a/packages/core-services/src/types/IBroker.ts +++ b/packages/core-services/src/types/IBroker.ts @@ -30,6 +30,20 @@ export type BaseMetricOptions = { [key: string]: unknown; }; +export type CallingOptions = { + nodeID?: string; + // timeout?: number; + // retries?: number; + // fallbackResponse?: FallbackResponse | FallbackResponse[] | FallbackResponseHandler; + // meta?: GenericObject; + // parentSpan?: ContextParentSpan; + // parentCtx?: Context; + // requestID?: string; + // tracking?: boolean; + // paramsCloning?: boolean; + // caller?: string; +}; + export interface IServiceMetrics { register(opts: BaseMetricOptions): void; @@ -50,7 +64,7 @@ export interface IBroker { metrics?: IServiceMetrics; destroyService(service: IServiceClass): Promise; createService(service: IServiceClass, serviceDependencies?: string[]): void; - call(method: string, data: any): Promise; + call(method: string, data: any, options?: CallingOptions): Promise; broadcastToServices( services: string[], event: T, diff --git a/packages/core-services/src/types/IMeteor.ts b/packages/core-services/src/types/IMeteor.ts index f905f7d7cddce..09f4e4470a20c 100644 --- a/packages/core-services/src/types/IMeteor.ts +++ b/packages/core-services/src/types/IMeteor.ts @@ -17,7 +17,14 @@ export type AutoUpdateRecord = { export interface IMeteor extends IServiceClass { getAutoUpdateClientVersions(): Promise>; getLoginServiceConfiguration(): Promise; - callMethodWithToken(userId: string | undefined, token: string | undefined, method: string, args: any[]): Promise; + callMethodWithToken( + userId: string | undefined, + token: string | undefined, + method: string, + args: any[], + ): Promise<{ + result: unknown; + }>; notifyGuestStatusChanged(token: string, status: string): Promise; getURL(path: string, params?: Record, cloudDeepLinkUrl?: string): Promise; } diff --git a/packages/core-typings/CHANGELOG.md b/packages/core-typings/CHANGELOG.md index 22b8fbd1b3acb..78ce955ca5fc2 100644 --- a/packages/core-typings/CHANGELOG.md +++ b/packages/core-typings/CHANGELOG.md @@ -1,5 +1,39 @@ # @rocket.chat/core-typings +## 7.6.0-rc.8 + +## 7.6.0-rc.7 + +## 7.6.0-rc.6 + +## 7.6.0-rc.5 + +## 7.6.0-rc.4 + +## 7.6.0-rc.3 + +## 7.6.0-rc.2 + +## 7.6.0-rc.1 + +## 7.6.0-rc.0 + +### Minor Changes + +- ([#35717](https://github.com/RocketChat/Rocket.Chat/pull/35717)) Adds new settings to allow configuring custom variables with string manipulation functions on the LDAP data mapper + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +- ([#34494](https://github.com/RocketChat/Rocket.Chat/pull/34494)) Implements auditing events for `/v1/users.update` API endpoint + +- ([#35718](https://github.com/RocketChat/Rocket.Chat/pull/35718)) Adds a new setting to allow syncing federated users data through LDAP + +### Patch Changes + +- ([#35790](https://github.com/RocketChat/Rocket.Chat/pull/35790)) Fixes an issue in `Admin > Settings` page where sometimes settings guarded by a license module would not be editable despite having the required modules. + +- ([#35832](https://github.com/RocketChat/Rocket.Chat/pull/35832)) Fixes an issue where Voice Calls were unable to gather Ice Servers + ## 7.5.1 ## 7.5.0 diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 844ff5bf25c14..ce8084c83ab7a 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", "private": true, - "version": "7.5.1", + "version": "7.6.0-rc.8", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", @@ -14,7 +14,7 @@ "scripts": { "lint": "eslint --ext .js,.jsx,.ts,.tsx .", "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", - "test": "echo \"Error: no test specified\" && exit 1", + "test": "echo \"no tests\" && exit 1", "dev": "tsc --watch --preserveWatchOutput -p tsconfig.json", "build": "rm -rf dist && tsc -p tsconfig.json" }, @@ -24,7 +24,7 @@ "/dist" ], "dependencies": { - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/ui-kit": "workspace:~", "@types/express": "^4.17.21" diff --git a/packages/core-typings/src/Apps.ts b/packages/core-typings/src/Apps.ts index 456aa987bb19c..06a18581cd442 100644 --- a/packages/core-typings/src/Apps.ts +++ b/packages/core-typings/src/Apps.ts @@ -129,6 +129,11 @@ export type App = { private: boolean; documentationUrl: string; migrated: boolean; + // Status of the app across the cluster (when deployment includes multiple instances) + clusterStatus?: { + instanceId: string; + status: AppStatus; + }[]; }; export type AppCategory = { diff --git a/packages/core-typings/src/ILivechatAgent.ts b/packages/core-typings/src/ILivechatAgent.ts index f106239400b17..4a67930942b2d 100644 --- a/packages/core-typings/src/ILivechatAgent.ts +++ b/packages/core-typings/src/ILivechatAgent.ts @@ -15,3 +15,10 @@ export interface ILivechatAgent extends IUser { livechatStatusSystemModified?: boolean; openBusinessHours?: string[]; } + +export type AvailableAgentsAggregation = { + agentId: string; + username: string; + maxChatsForAgent: number; + queueInfo: { chats: number; chatsForDepartment?: number }; +}; diff --git a/packages/core-typings/src/IServerEvent.ts b/packages/core-typings/src/IServerEvent.ts index 2f4348260ae81..4bf19d1236193 100644 --- a/packages/core-typings/src/IServerEvent.ts +++ b/packages/core-typings/src/IServerEvent.ts @@ -42,9 +42,9 @@ export interface IAuditServerAppActor { export type IAuditServerActor = IAuditServerUserActor | IAuditServerSystemActor | IAuditServerAppActor; interface IAuditServerEvent { + _id: string; t: string; ts: Date; - actor: IAuditServerActor; } diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index c1ba49b556e13..b6c2a742643d1 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -21,6 +21,9 @@ export interface IPersonalAccessToken extends ILoginToken { bypassTwoFactor?: boolean; } +export const isPersonalAccessToken = (token: LoginToken): token is IPersonalAccessToken => + 'type' in token && token.type === 'personalAccessToken'; + export interface IUserEmailVerificationToken { token: string; address: string; @@ -196,6 +199,7 @@ export interface IUser extends IRocketChatRecord { reason?: string; // TODO: move this to a specific federation user type federated?: boolean; + // @deprecated federation?: { avatarUrl?: string; searchedServerNames?: string[]; @@ -218,6 +222,7 @@ export interface IUser extends IRocketChatRecord { requirePasswordChangeReason?: string; roomRolePriorities?: Record; isOAuthUser?: boolean; // client only field + __rooms?: string[]; } export interface IRegisterUser extends IUser { @@ -250,6 +255,10 @@ export type IUserInRole = Pick< '_id' | 'name' | 'username' | 'emails' | 'avatarETag' | 'createdAt' | 'roles' | 'type' | 'active' | '_updatedAt' >; +export type UserPresence = Readonly< + Partial> & Required> +>; + export type AvatarUrlObj = { avatarUrl: string; }; diff --git a/packages/core-typings/src/ServerAudit/IAuditUserChangedEvent.ts b/packages/core-typings/src/ServerAudit/IAuditUserChangedEvent.ts new file mode 100644 index 0000000000000..4218dcbb7c311 --- /dev/null +++ b/packages/core-typings/src/ServerAudit/IAuditUserChangedEvent.ts @@ -0,0 +1,36 @@ +import type { UpdateFilter } from 'mongodb'; + +import type { IAuditServerEventType } from '../IServerEvent'; +import type { IUser } from '../IUser'; +import type { DeepPartial } from '../utils'; + +export type IServerEventAuditedUser = IUser & { + password: string; +}; + +interface IServerEventUserChanged + extends IAuditServerEventType< + | { + key: 'user'; + value: { + _id: IUser['_id']; + username: IUser['username']; + }; + } + | { + key: 'user_data'; + value: DeepPartial; + } + | { + key: 'operation'; + value: UpdateFilter; + } + > { + t: 'user.changed'; +} + +declare module '../IServerEvent' { + interface IServerEvents { + 'user.changed': IServerEventUserChanged; + } +} diff --git a/apps/meteor/client/definitions/Subscribable.ts b/packages/core-typings/src/Subscribable.ts similarity index 100% rename from apps/meteor/client/definitions/Subscribable.ts rename to packages/core-typings/src/Subscribable.ts diff --git a/packages/core-typings/src/import/IImportUser.ts b/packages/core-typings/src/import/IImportUser.ts index de3b7806a300d..66841937cd90b 100644 --- a/packages/core-typings/src/import/IImportUser.ts +++ b/packages/core-typings/src/import/IImportUser.ts @@ -19,4 +19,5 @@ export interface IImportUser { password?: string; voipExtension?: string; + federated?: boolean; } diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index 0f589b22ba229..5cb20c24ef859 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -1,5 +1,7 @@ import './ServerAudit/IAuditServerSettingEvent'; +import './ServerAudit/IAuditUserChangedEvent'; +export * from './ServerAudit/IAuditUserChangedEvent'; export * from './Apps'; export * from './AppOverview'; export * from './FeaturedApps'; @@ -9,6 +11,7 @@ export * from './IRoom'; export * from './IMessage'; export * from './federation'; export * from './Serialized'; +export * from './Subscribable'; export * from './ISetting'; export * from './ISubscription'; export * from './ITeam'; diff --git a/packages/core-typings/src/ldap/ILDAPOptions.ts b/packages/core-typings/src/ldap/ILDAPOptions.ts index e7721dcd041d3..ad673f595efb5 100644 --- a/packages/core-typings/src/ldap/ILDAPOptions.ts +++ b/packages/core-typings/src/ldap/ILDAPOptions.ts @@ -28,4 +28,6 @@ export interface ILDAPConnectionOptions { authenticationUserDN: string; authenticationPassword: string; attributesToQuery: Array; + useVariables: boolean; + variableMap: string; } diff --git a/packages/core-typings/src/omnichannel/sms.ts b/packages/core-typings/src/omnichannel/sms.ts index 49364da2b8c36..c29437910066d 100644 --- a/packages/core-typings/src/omnichannel/sms.ts +++ b/packages/core-typings/src/omnichannel/sms.ts @@ -1,5 +1,3 @@ -import type { Request } from 'express'; - type ServiceMedia = { url: string; contentType: string; @@ -29,7 +27,7 @@ export interface ISMSProviderConstructor { export interface ISMSProvider { parse(data: unknown): ServiceData; - validateRequest(request: Request): boolean; + validateRequest(request: Request): Promise; sendBatch?(from: string, to: string[], message: string): Promise; response(): SMSProviderResponse; diff --git a/packages/core-typings/src/utils.ts b/packages/core-typings/src/utils.ts index 2e20ebc48c842..3e8f4869ce3d2 100644 --- a/packages/core-typings/src/utils.ts +++ b/packages/core-typings/src/utils.ts @@ -48,3 +48,5 @@ export type DeepPartial = { ? DeepPartial : T[P]; }; + +export const isNotUndefined = (value: T | undefined): value is T => value !== undefined; diff --git a/packages/core-typings/src/voip/VoIPUserConfiguration.ts b/packages/core-typings/src/voip/VoIPUserConfiguration.ts index 01bca0a5409cb..7bade79310cad 100644 --- a/packages/core-typings/src/voip/VoIPUserConfiguration.ts +++ b/packages/core-typings/src/voip/VoIPUserConfiguration.ts @@ -45,6 +45,12 @@ export interface VoIPUserConfiguration { * @defaultValue undefined */ enableKeepAliveUsingOptionsForUnstableNetworks: boolean; + + /** + * Time to wait for Ice Gathering to complete + * @defaultValue 5000 + */ + iceGatheringTimeout?: number; } export interface IMediaStreamRenderer { diff --git a/packages/cron/CHANGELOG.md b/packages/cron/CHANGELOG.md index bd98cf019c696..cd9c9ef64e06e 100644 --- a/packages/cron/CHANGELOG.md +++ b/packages/cron/CHANGELOG.md @@ -1,5 +1,95 @@ # @rocket.chat/cron +## 0.1.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 +
      + +## 0.1.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 +
      + +## 0.1.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 +
      + +## 0.1.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 +
      + +## 0.1.21-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 +
      + +## 0.1.21-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 +
      + +## 0.1.21-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 +
      + +## 0.1.21-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 +
      + +## 0.1.21-rc.0 + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 +
      + ## 0.1.20 ### Patch Changes diff --git a/packages/cron/package.json b/packages/cron/package.json index 8a7f2786c707b..ad5a5ca4678e7 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/cron", - "version": "0.1.20", + "version": "0.1.21-rc.8", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/ddp-client/CHANGELOG.md b/packages/ddp-client/CHANGELOG.md index 0b0fdf092de62..d45a2a406eec1 100644 --- a/packages/ddp-client/CHANGELOG.md +++ b/packages/ddp-client/CHANGELOG.md @@ -1,5 +1,104 @@ # @rocket.chat/ddp-client +## 0.3.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/rest-typings@7.6.0-rc.8 + - @rocket.chat/api-client@0.2.21-rc.8 +
      + +## 0.3.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/rest-typings@7.6.0-rc.7 + - @rocket.chat/api-client@0.2.21-rc.7 +
      + +## 0.3.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/rest-typings@7.6.0-rc.6 + - @rocket.chat/api-client@0.2.21-rc.6 +
      + +## 0.3.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/rest-typings@7.6.0-rc.5 + - @rocket.chat/api-client@0.2.21-rc.5 +
      + +## 0.3.21-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/rest-typings@7.6.0-rc.4 + - @rocket.chat/api-client@0.2.21-rc.4 +
      + +## 0.3.21-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/rest-typings@7.6.0-rc.3 + - @rocket.chat/api-client@0.2.21-rc.3 +
      + +## 0.3.21-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/rest-typings@7.6.0-rc.2 + - @rocket.chat/api-client@0.2.21-rc.2 +
      + +## 0.3.21-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/rest-typings@7.6.0-rc.1 + - @rocket.chat/api-client@0.2.20-rc.1 +
      + +## 0.3.21-rc.0 + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 + - @rocket.chat/api-client@0.2.20-rc.0 +
      + ## 0.3.20 ### Patch Changes diff --git a/packages/ddp-client/package.json b/packages/ddp-client/package.json index 5f71eecbfce6e..8def562d76a28 100644 --- a/packages/ddp-client/package.json +++ b/packages/ddp-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ddp-client", - "version": "0.3.20", + "version": "0.3.21-rc.8", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/freeswitch/CHANGELOG.md b/packages/freeswitch/CHANGELOG.md index 3f7ef12c2c650..392c827f397d3 100644 --- a/packages/freeswitch/CHANGELOG.md +++ b/packages/freeswitch/CHANGELOG.md @@ -1,5 +1,86 @@ # @rocket.chat/freeswitch +## 1.2.8-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 +
      + +## 1.2.8-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 +
      + +## 1.2.8-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 +
      + +## 1.2.8-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 +
      + +## 1.2.8-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 +
      + +## 1.2.8-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 +
      + +## 1.2.8-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 +
      + +## 1.2.8-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 +
      + +## 1.2.8-rc.0 + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 +
      + ## 1.2.7 ### Patch Changes diff --git a/packages/freeswitch/package.json b/packages/freeswitch/package.json index 1dea3fbe1ec66..1776a21518074 100644 --- a/packages/freeswitch/package.json +++ b/packages/freeswitch/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/freeswitch", - "version": "1.2.7", + "version": "1.2.8-rc.8", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/fuselage-ui-kit/CHANGELOG.md b/packages/fuselage-ui-kit/CHANGELOG.md index 9e912c07e492e..520dc2040cfec 100644 --- a/packages/fuselage-ui-kit/CHANGELOG.md +++ b/packages/fuselage-ui-kit/CHANGELOG.md @@ -1,5 +1,123 @@ # Change Log +## 18.0.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/gazzodown@18.0.0-rc.8 + - @rocket.chat/ui-contexts@18.0.0-rc.8 + - @rocket.chat/ui-avatar@14.0.0-rc.8 + - @rocket.chat/ui-video-conf@18.0.0-rc.8 +
      + +## 18.0.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/gazzodown@18.0.0-rc.7 + - @rocket.chat/ui-contexts@18.0.0-rc.7 + - @rocket.chat/ui-avatar@14.0.0-rc.7 + - @rocket.chat/ui-video-conf@18.0.0-rc.7 +
      + +## 18.0.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/gazzodown@18.0.0-rc.6 + - @rocket.chat/ui-contexts@18.0.0-rc.6 + - @rocket.chat/ui-avatar@14.0.0-rc.6 + - @rocket.chat/ui-video-conf@18.0.0-rc.6 +
      + +## 18.0.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/gazzodown@18.0.0-rc.5 + - @rocket.chat/ui-contexts@18.0.0-rc.5 + - @rocket.chat/ui-avatar@14.0.0-rc.5 + - @rocket.chat/ui-video-conf@18.0.0-rc.5 +
      + +## 18.0.0-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/gazzodown@18.0.0-rc.4 + - @rocket.chat/ui-contexts@18.0.0-rc.4 + - @rocket.chat/ui-avatar@14.0.0-rc.4 + - @rocket.chat/ui-video-conf@18.0.0-rc.4 +
      + +## 18.0.0-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/gazzodown@18.0.0-rc.3 + - @rocket.chat/ui-contexts@18.0.0-rc.3 + - @rocket.chat/ui-avatar@14.0.0-rc.3 + - @rocket.chat/ui-video-conf@18.0.0-rc.3 +
      + +## 18.0.0-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/gazzodown@18.0.0-rc.2 + - @rocket.chat/ui-contexts@18.0.0-rc.2 + - @rocket.chat/ui-avatar@14.0.0-rc.2 + - @rocket.chat/ui-video-conf@18.0.0-rc.2 +
      + +## 18.0.0-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/gazzodown@18.0.0-rc.1 + - @rocket.chat/ui-contexts@18.0.0-rc.1 + - @rocket.chat/ui-avatar@14.0.0-rc.1 + - @rocket.chat/ui-video-conf@18.0.0-rc.1 +
      + +## 18.0.0-rc.0 + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d649a761edd71e1325a635b757ef1df2e5a778a4, bbd14f84214b4785f2b58cfeb8e9117bdfbf18e8, 1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/apps-engine@1.51.0-rc.0 + - @rocket.chat/ui-contexts@18.0.0-rc.0 + - @rocket.chat/gazzodown@18.0.0-rc.0 + - @rocket.chat/ui-avatar@14.0.0-rc.0 + - @rocket.chat/ui-video-conf@18.0.0-rc.0 +
      + ## 17.0.1 ### Patch Changes diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 07f2e5917ea22..c37a1b4210635 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/fuselage-ui-kit", - "version": "17.0.1", + "version": "18.0.0-rc.8", "private": true, "description": "UiKit elements for Rocket.Chat Apps built under Fuselage design system", "homepage": "https://rocketchat.github.io/Rocket.Chat.Fuselage/", @@ -52,10 +52,10 @@ "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "~0.61.0", + "@rocket.chat/fuselage": "~0.62.0", "@rocket.chat/fuselage-hooks": "~0.35.0", "@rocket.chat/fuselage-polyfills": "~0.31.25", - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/mock-providers": "workspace:^", "@rocket.chat/prettier-config": "~0.31.25", @@ -93,7 +93,7 @@ "typescript": "~5.7.2" }, "peerDependencies": { - "@rocket.chat/apps-engine": "1.50.0", + "@rocket.chat/apps-engine": "1.51.0-rc.0", "@rocket.chat/eslint-config": "0.7.0", "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", @@ -101,10 +101,10 @@ "@rocket.chat/icons": "*", "@rocket.chat/prettier-config": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-avatar": "13.0.1", - "@rocket.chat/ui-contexts": "17.0.1", - "@rocket.chat/ui-kit": "0.37.0", - "@rocket.chat/ui-video-conf": "17.0.1", + "@rocket.chat/ui-avatar": "workspace:^", + "@rocket.chat/ui-contexts": "workspace:^", + "@rocket.chat/ui-kit": "workspace:^", + "@rocket.chat/ui-video-conf": "workspace:^", "@tanstack/react-query": "*", "react": "*", "react-dom": "*" diff --git a/packages/gazzodown/CHANGELOG.md b/packages/gazzodown/CHANGELOG.md index bdab57ffc976f..c24ca4c8ecddd 100644 --- a/packages/gazzodown/CHANGELOG.md +++ b/packages/gazzodown/CHANGELOG.md @@ -1,5 +1,104 @@ # @rocket.chat/gazzodown +## 18.0.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/ui-contexts@18.0.0-rc.8 + - @rocket.chat/ui-client@18.0.0-rc.8 +
      + +## 18.0.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/ui-contexts@18.0.0-rc.7 + - @rocket.chat/ui-client@18.0.0-rc.7 +
      + +## 18.0.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/ui-contexts@18.0.0-rc.6 + - @rocket.chat/ui-client@18.0.0-rc.6 +
      + +## 18.0.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/ui-contexts@18.0.0-rc.5 + - @rocket.chat/ui-client@18.0.0-rc.5 +
      + +## 18.0.0-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/ui-contexts@18.0.0-rc.4 + - @rocket.chat/ui-client@18.0.0-rc.4 +
      + +## 18.0.0-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/ui-contexts@18.0.0-rc.3 + - @rocket.chat/ui-client@18.0.0-rc.3 +
      + +## 18.0.0-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/ui-contexts@18.0.0-rc.2 + - @rocket.chat/ui-client@18.0.0-rc.2 +
      + +## 18.0.0-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/ui-contexts@18.0.0-rc.1 + - @rocket.chat/ui-client@18.0.0-rc.1 +
      + +## 18.0.0-rc.0 + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, f545617c2ac3d67af533e64c2670d8d564a56d15, 6bf386dcc2a560963cf719fbc2d96569ce23a2de, 1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 5e3ab1a07163cd22ad4c41502ef232845d26bdc2, 47ae69912cd90743e7bf836fdee4be481a01bbba, 72725d391e79b44e7380ee2fe640e2e4426c77ca, 4b28126ac94cf1d3312b30ad9863ca02673f49d4, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/ui-client@18.0.0-rc.0 + - @rocket.chat/ui-contexts@18.0.0-rc.0 +
      + ## 17.0.1 ### Patch Changes diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 539ac3ea7e171..7c26ae320ceb6 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/gazzodown", - "version": "17.0.1", + "version": "18.0.0-rc.8", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", @@ -30,7 +30,7 @@ "@babel/core": "~7.26.0", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "~0.61.0", + "@rocket.chat/fuselage": "~0.62.0", "@rocket.chat/fuselage-tokens": "~0.33.2", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/message-parser": "workspace:^", @@ -76,8 +76,8 @@ "@rocket.chat/fuselage-tokens": "*", "@rocket.chat/message-parser": "0.31.32", "@rocket.chat/styled": "*", - "@rocket.chat/ui-client": "17.0.1", - "@rocket.chat/ui-contexts": "17.0.1", + "@rocket.chat/ui-client": "workspace:^", + "@rocket.chat/ui-contexts": "workspace:^", "katex": "*", "react": "*" }, diff --git a/packages/i18n/CHANGELOG.md b/packages/i18n/CHANGELOG.md index 5315f9378448f..7fe2303223406 100644 --- a/packages/i18n/CHANGELOG.md +++ b/packages/i18n/CHANGELOG.md @@ -1,5 +1,33 @@ # @rocket.chat/i18n +## 1.6.0-rc.0 + +### Minor Changes + +- ([#35717](https://github.com/RocketChat/Rocket.Chat/pull/35717)) Adds new settings to allow configuring custom variables with string manipulation functions on the LDAP data mapper + +- ([#35613](https://github.com/RocketChat/Rocket.Chat/pull/35613)) Replaces the parent room tag in room header in favor of a button to back to the parent room + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#35218](https://github.com/RocketChat/Rocket.Chat/pull/35218)) Adds a new admin page to audit settings changes in a server + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +- ([#35718](https://github.com/RocketChat/Rocket.Chat/pull/35718)) Adds a new setting to allow syncing federated users data through LDAP + +- ([#35807](https://github.com/RocketChat/Rocket.Chat/pull/35807)) Moves the room search functionality from the sidebar to the navbar and reorganize their relative actions + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#35703](https://github.com/RocketChat/Rocket.Chat/pull/35703)) Adds close action to contact unknown callout displayed within Livechat rooms + +### Patch Changes + +- ([#35568](https://github.com/RocketChat/Rocket.Chat/pull/35568)) Fixes an issue with the leave room confirmation modal not displaying the room's name. + +- ([#35832](https://github.com/RocketChat/Rocket.Chat/pull/35832)) Fixes an issue where Voice Calls were unable to gather Ice Servers + +- ([#35709](https://github.com/RocketChat/Rocket.Chat/pull/35709)) Improves UX for users with mandatory 2FA roles by clarifying required actions + +- ([#35733](https://github.com/RocketChat/Rocket.Chat/pull/35733)) Fixes a typo in the app update success toast + ## 1.5.0 ### Minor Changes diff --git a/packages/i18n/package.json b/packages/i18n/package.json index c15599b47fc2c..f58bf8beb6d65 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/i18n", - "version": "1.5.0", + "version": "1.6.0-rc.0", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/i18n/src/locales/af.i18n.json b/packages/i18n/src/locales/af.i18n.json index 3aa139a3d5344..1fe8b49162a6e 100644 --- a/packages/i18n/src/locales/af.i18n.json +++ b/packages/i18n/src/locales/af.i18n.json @@ -863,7 +863,6 @@ "Directory": "Gids", "Disable_Facebook_integration": "Deaktiveer Facebook integrasie", "Disable_Notifications": "Deaktiveer kennisgewings", - "Disable_two-factor_authentication": "Deaktiveer tweefaktor-verifikasie", "Disabled": "gestremde", "Disallow_reacting": "Verwerp reaksie", "Disallow_reacting_Description": "Moenie reageer nie", @@ -956,7 +955,6 @@ "Enable_Auto_Away": "Aktiveer outomaties weg", "Enable_Desktop_Notifications": "Aktiveer lessenaar kennisgewings", "Enable_Svg_Favicon": "Aktiveer SVG favicon", - "Enable_two-factor_authentication": "Aktiveer tweefaktor-verifikasie", "Enabled": "enabled", "Encrypted_message": "Geënkripteerde boodskap", "End_OTR": "Einde OTR", @@ -1218,12 +1216,12 @@ "Hide": "Versteek kamer", "Hide_counter": "Versteek toonbank", "Hide_flextab": "Versteek regterkantste zijbalk met klik", - "Hide_Group_Warning": "Is jy seker jy wil die groep \"%s\" versteek?", - "Hide_Livechat_Warning": "Is jy seker jy wil die livechat verberg met \"%s\"?", - "Hide_Private_Warning": "Is jy seker jy wil die bespreking met \"%s\" versteek?", + "Hide_Group_Warning": "Is jy seker jy wil die groep \"{{roomName}}\" versteek?", + "Hide_Livechat_Warning": "Is jy seker jy wil die livechat verberg met \"{{roomName}}\"?", + "Hide_Private_Warning": "Is jy seker jy wil die bespreking met \"{{roomName}}\" versteek?", "Hide_roles": "Versteek rolle", "Hide_room": "Versteek kamer", - "Hide_Room_Warning": "Is jy seker jy wil die kamer \"%s\" versteek?", + "Hide_Room_Warning": "Is jy seker jy wil die kamer \"{{roomName}}\" versteek?", "Hide_Unread_Room_Status": "Versteek Ongelees Kamer Status", "Hide_usernames": "Versteek gebruikersname", "Highlights": "hoogtepunte", @@ -1523,11 +1521,11 @@ "Lead_capture_email_regex": "Lei vang e-pos regex", "Lead_capture_phone_regex": "Lead capture phone regex", "Leave": "Los kamer", - "Leave_Group_Warning": "Is jy seker jy wil die groep \"%s\" verlaat?", - "Leave_Livechat_Warning": "Is jy seker jy wil die livechat met \"%s\" verlaat?", - "Leave_Private_Warning": "Is jy seker jy wil die gesprek met \"%s\" verlaat?", + "Leave_Group_Warning": "Is jy seker jy wil die groep \"{{roomName}}\" verlaat?", + "Leave_Livechat_Warning": "Is jy seker jy wil die livechat met \"{{roomName}}\" verlaat?", + "Leave_Private_Warning": "Is jy seker jy wil die gesprek met \"{{roomName}}\" verlaat?", "Leave_room": "Los kamer", - "Leave_Room_Warning": "Is jy seker jy wil die kamer \"%s\" verlaat?", + "Leave_Room_Warning": "Is jy seker jy wil die kamer \"{{roomName}}\" verlaat?", "Leave_the_current_channel": "Los die huidige kanaal", "leave-c": "Laat kanale", "leave-p": "Verlaat privaat groepe", @@ -2445,7 +2443,6 @@ "Two-factor_authentication": "Twee-faktor-verifikasie", "Two-factor_authentication_disabled": "Tweefaktor-verifikasie gedeaktiveer", "Two-factor_authentication_enabled": "Tweefaktor-verifikasie aangeskakel", - "Two-factor_authentication_is_currently_disabled": "Tweefaktor-verifikasie is tans gedeaktiveer", "Two-factor_authentication_native_mobile_app_warning": "WAARSKUWING: Sodra jy dit aangeskakel het, sal jy nie kan inskakel op die inheemse mobiele programme (Rocket.Chat +) met jou wagwoord totdat hulle die 2FA implementeer nie.", "Type": "tipe", "Type_your_email": "Tik jou e-pos", @@ -2753,4 +2750,4 @@ "registration.component.form.sendConfirmationEmail": "Stuur bevestiging e-pos", "Enterprise": "onderneming", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ar.i18n.json b/packages/i18n/src/locales/ar.i18n.json index 59c26ec6a414f..7df25ff2bc23a 100644 --- a/packages/i18n/src/locales/ar.i18n.json +++ b/packages/i18n/src/locales/ar.i18n.json @@ -599,7 +599,6 @@ "Back_to_integrations": "عودة إلى عمليات التكامل", "Back_to_login": "عودة إلى تسجيل الدخول", "Back_to_permissions": "عودة إلى الأذونات", - "Back_to_room": "عودة إلى Room", "Back_to_threads": "عودة إلى المواضيع", "Backup_codes": "أكواد التخزين الاحتياطي", "Belongs_To": "ينتمي إلى", @@ -1311,8 +1310,6 @@ "Disable": "تعطيل", "Disable_Facebook_integration": "تعطيل تكامل Facebook", "Disable_Notifications": "تعطيل الإشعارات", - "Disable_two-factor_authentication": "تعطيل المصادقة الثنائية عبر TOTP", - "Disable_two-factor_authentication_email": "تعطيل المصادقة الثنائية عبر البريد الإلكتروني", "Disabled": "تم التعطيل", "Disallow_reacting": "عدم السماح بالتفاعل", "Disallow_reacting_Description": "لا يسمح بالتفاعل", @@ -1442,8 +1439,6 @@ "Enable_Svg_Favicon": "تفعيل رمز مفضلة SVG ", "Enable_inquiry_fetch_by_stream": "تمكين جلب بيانات الاستعلام من الخادم باستخدام الدفق", "Enable_omnichannel_auto_close_abandoned_rooms": "تمكين الإغلاق التلقائي للغرف التي هجرها الزائر", - "Enable_two-factor_authentication": "تمكين المصادقة الثنائية عبر TOTP", - "Enable_two-factor_authentication_email": "تمكين المصادقة الثنائية عبر البريد الإلكتروني", "Enabled": "تم التمكين", "Encrypted": "مشفر", "Encrypted_channel_Description": "القناة المشفرة بين النهايات. لن يعمل البحث مع الفِرَق المشفرة وقد لا تعرض الإشعارات محتوى الرسائل.", @@ -1744,10 +1739,10 @@ "Hi_username": "مرحبًا [name]", "Hidden": "مخفي", "Hide": "إخفاء", - "Hide_Group_Warning": "هل تريد فعلاً إخفاء المجموعة \"‎%s\"؟", - "Hide_Livechat_Warning": "هل تريد فعلاً إخفاء الدردشة مع \"‎%s\"؟", - "Hide_Private_Warning": "هل تريد فعلاً إخفاء المناقشة مع \"‎%s\"؟", - "Hide_Room_Warning": "هل تريد فعلاً إخفاء القناة \"‎%s\"؟", + "Hide_Group_Warning": "هل تريد فعلاً إخفاء المجموعة \"‎{{roomName}}\"؟", + "Hide_Livechat_Warning": "هل تريد فعلاً إخفاء الدردشة مع \"‎{{roomName}}\"؟", + "Hide_Private_Warning": "هل تريد فعلاً إخفاء المناقشة مع \"‎{{roomName}}\"؟", + "Hide_Room_Warning": "هل تريد فعلاً إخفاء القناة \"‎{{roomName}}\"؟", "Hide_System_Messages": "إخفاء رسائل النظام", "Hide_Unread_Room_Status": "إخفاء حالة Room غير المقروءة", "Hide_counter": "إخفاء العداد", @@ -2215,10 +2210,10 @@ "Lead_capture_phone_regex": "التعبير النمطي للهاتف المحمول لالتقاط العملاء المتوقعين", "Least_recent_updated": "آخر تحديث", "Leave": "مغادرة أو ترك", - "Leave_Group_Warning": "هل تريد فعلاً مغادرة المجموعة \"%s\"؟", - "Leave_Livechat_Warning": "هل تريد فعلاً مغادرة القناة متعددة الاتجاهات مع \" %s\"؟", - "Leave_Private_Warning": "هل تريد فعلاً مغادرة المناقشة مع \"%s\"؟", - "Leave_Room_Warning": "هل تريد فعلاً مغادرة القناة \"%s\"؟", + "Leave_Group_Warning": "هل تريد فعلاً مغادرة المجموعة \"{{roomName}}\"؟", + "Leave_Livechat_Warning": "هل تريد فعلاً مغادرة القناة متعددة الاتجاهات مع \" {{roomName}}\"؟", + "Leave_Private_Warning": "هل تريد فعلاً مغادرة المناقشة مع \"{{roomName}}\"؟", + "Leave_Room_Warning": "هل تريد فعلاً مغادرة القناة \"{{roomName}}\"؟", "Leave_a_comment": "ترك تعليق", "Leave_room": "مغادرة", "Leave_the_current_channel": "مغادرة القناة الحالية", @@ -3757,9 +3752,7 @@ "Two-factor_authentication": "المصادقة الثنائية عبر TOTP", "Two-factor_authentication_disabled": "تم تعطيل المصادقة الثنائية", "Two-factor_authentication_email": "المصادقة الثنائية عبر البريد الإلكتروني", - "Two-factor_authentication_email_is_currently_disabled": "تم تعطيل المصادقة الثنائية عبر البريد الإلكتروني حاليًا", "Two-factor_authentication_enabled": "تم تمكين المصادقة الثنائية", - "Two-factor_authentication_is_currently_disabled": "تم تعطيل المصادقة الثنائية عبر TOTP حاليًا", "Two-factor_authentication_native_mobile_app_warning": "تحذير: بمجرد تمكين هذا، لن تتمكن من تسجيل الدخول إلى تطبيقات الهاتف الأصلية (Rocket.Chat+) باستخدام كلمة المرور الخاصة بك حتى يتم تنفيذ المصادقة الثنائية.", "Two-factor_authentication_via_TOTP": "المصادقة الثنائية عبر TOTP", "Type": "اكتب", diff --git a/packages/i18n/src/locales/az.i18n.json b/packages/i18n/src/locales/az.i18n.json index 30c42415f4fe8..2c049b67719a6 100644 --- a/packages/i18n/src/locales/az.i18n.json +++ b/packages/i18n/src/locales/az.i18n.json @@ -863,7 +863,6 @@ "Directory": "Directory", "Disable_Facebook_integration": "Facebook inteqrasiyasını dayandırın", "Disable_Notifications": "Bildirişləri işdən çıxarın", - "Disable_two-factor_authentication": "İki faktorlu autentifikasiyası aradan qaldırın", "Disabled": "Əlil", "Disallow_reacting": "Reaksiyaya icazə verməyin", "Disallow_reacting_Description": "Reaksiya edilməsinə mane olur", @@ -956,7 +955,6 @@ "Enable_Auto_Away": "Avtomatik Away Enable", "Enable_Desktop_Notifications": "Masaüstü bildirişlərini aktivləşdirin", "Enable_Svg_Favicon": "SVG favicon'u aktiv edin", - "Enable_two-factor_authentication": "İki faktorlu identifikasiyası təmin edin", "Enabled": "Etkin", "Encrypted_message": "Şifrələnmiş mesaj", "End_OTR": "End OTR", @@ -1218,12 +1216,12 @@ "Hide": "Otaqları gizlədin", "Hide_counter": "Saytı gizlət", "Hide_flextab": "Sağ Kenar Çubuğunu Kliklə Gizlət", - "Hide_Group_Warning": "\"%s\" qrupunu gizlətmək istədiyinizə əminsiniz?", - "Hide_Livechat_Warning": "Livechat'ı \"%s\" ilə gizlətmək istədiyinizə əminsiniz?", - "Hide_Private_Warning": "Müzakirə \"%s\" ilə gizlətmək istədiyinizə əminsiniz?", + "Hide_Group_Warning": "\"{{roomName}}\" qrupunu gizlətmək istədiyinizə əminsiniz?", + "Hide_Livechat_Warning": "Livechat'ı \"{{roomName}}\" ilə gizlətmək istədiyinizə əminsiniz?", + "Hide_Private_Warning": "Müzakirə \"{{roomName}}\" ilə gizlətmək istədiyinizə əminsiniz?", "Hide_roles": "Roles gizlət", "Hide_room": "Otaqları gizlədin", - "Hide_Room_Warning": "\"%s\" otağını gizlətmək istədiyinizə əminsiniz?", + "Hide_Room_Warning": "\"{{roomName}}\" otağını gizlətmək istədiyinizə əminsiniz?", "Hide_Unread_Room_Status": "Oxunmamış Otaq Statusını Gizlət", "Hide_usernames": "İstifadəçi adlarını gizlət", "Highlights": "Zirvələr", @@ -1523,11 +1521,11 @@ "Lead_capture_email_regex": "Nəzarət ələ e-poçt qeydiyyatdan keçirin", "Lead_capture_phone_regex": "Nəzarət aparan telefon regex", "Leave": "Otaq buraxın", - "Leave_Group_Warning": "\"%s\" qrupunu tərk etmək istədiyinizə əminsiniz?", - "Leave_Livechat_Warning": "Livechat'ı \"%s\" ilə tərk etmək istədiyinizə əminsiniz?", - "Leave_Private_Warning": "Müzakirə \"%s\" ilə tərk etmək istədiyinizə əminsiniz?", + "Leave_Group_Warning": "\"{{roomName}}\" qrupunu tərk etmək istədiyinizə əminsiniz?", + "Leave_Livechat_Warning": "Livechat'ı \"{{roomName}}\" ilə tərk etmək istədiyinizə əminsiniz?", + "Leave_Private_Warning": "Müzakirə \"{{roomName}}\" ilə tərk etmək istədiyinizə əminsiniz?", "Leave_room": "Otaq buraxın", - "Leave_Room_Warning": "\"%s\" otağından çıxmaq istəyirsiniz?", + "Leave_Room_Warning": "\"{{roomName}}\" otağından çıxmaq istəyirsiniz?", "Leave_the_current_channel": "Cari kanalı buraxın", "leave-c": "Kanallardan çıxın", "leave-p": "Şəxsi Qruplar buraxın", @@ -2445,7 +2443,6 @@ "Two-factor_authentication": "İki faktorlu identifikasiya", "Two-factor_authentication_disabled": "İki faktorlu kimlik doğrulaması aradan qaldırıldı", "Two-factor_authentication_enabled": "İki faktorlu autentifikasiya effektivdir", - "Two-factor_authentication_is_currently_disabled": "İki faktorlu kimlik doğrulaması hazırda əlil", "Two-factor_authentication_native_mobile_app_warning": "XƏBƏRDARLIQ: Bunu təmin etdikdən sonra, 2FA-nı tətbiq etməyincə, parolınızı istifadə edərək, doğma mobil tətbiqlərə (Rocket.Chat +) giriş yapa bilməyəcəksiniz.", "Type": "Tipi", "Type_your_email": "E-poçtunuzu yazın", @@ -2753,4 +2750,4 @@ "registration.component.form.sendConfirmationEmail": "Təsdiq e-poçt göndər", "Enterprise": "Müəssisə", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/be-BY.i18n.json b/packages/i18n/src/locales/be-BY.i18n.json index a5a0e2de9054d..716bf7cb7e150 100644 --- a/packages/i18n/src/locales/be-BY.i18n.json +++ b/packages/i18n/src/locales/be-BY.i18n.json @@ -882,7 +882,6 @@ "Directory": "каталог", "Disable_Facebook_integration": "Адключыць інтэграцыю Facebook", "Disable_Notifications": "адключыць апавяшчэння", - "Disable_two-factor_authentication": "Адключыць два фактары аўтэнтыфікацыі", "Disabled": "інвалід", "Disallow_reacting": "забараняе Рэагуючы", "Disallow_reacting_Description": "забараняе рэагуюць", @@ -975,7 +974,6 @@ "Enable_Auto_Away": "Enable Auto Away", "Enable_Desktop_Notifications": "Ўключэнне апавяшчэнняў на працоўным стале", "Enable_Svg_Favicon": "Ўключыць SVG фавиконки", - "Enable_two-factor_authentication": "Ўключэнне двухфакторную аўтэнтыфікацыі", "Enabled": "Уключана", "Encrypted_message": "зашыфраваныя паведамленне", "End_OTR": "канец ОТР", @@ -1234,12 +1232,12 @@ "Hide": "схаваць нумар", "Hide_counter": "схаваць лічыльнік", "Hide_flextab": "Схаваць правую бакавую панэль з шчылінкі", - "Hide_Group_Warning": "Вы ўпэўненыя, што хочаце схаваць групу «%s\"?", - "Hide_Livechat_Warning": "Вы ўпэўненыя, што хочаце схаваць звязаўшыся \"%s\"?", - "Hide_Private_Warning": "Вы ўпэўненыя, што хочаце схаваць дыскусію з \"%s\"?", + "Hide_Group_Warning": "Вы ўпэўненыя, што хочаце схаваць групу «{{roomName}}\"?", + "Hide_Livechat_Warning": "Вы ўпэўненыя, што хочаце схаваць звязаўшыся \"{{roomName}}\"?", + "Hide_Private_Warning": "Вы ўпэўненыя, што хочаце схаваць дыскусію з \"{{roomName}}\"?", "Hide_roles": "схаваць ролі", "Hide_room": "схаваць нумар", - "Hide_Room_Warning": "Вы ўпэўненыя, што хочаце схаваць нумар \"%s\"?", + "Hide_Room_Warning": "Вы ўпэўненыя, што хочаце схаваць нумар \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Схаваць Статус непрачытаных нумары", "Hide_usernames": "Схаваць імёны карыстальнікаў", "Highlights": "мелірованіе", @@ -1539,11 +1537,11 @@ "Lead_capture_email_regex": "Свінец захопу электроннай пошты рэгулярных выразаў", "Lead_capture_phone_regex": "Свінец захопу тэлефона рэгулярны выраз", "Leave": "Пакіньце нумар", - "Leave_Group_Warning": "Вы ўпэўненыя, што жадаеце выйсці з групы \"%s\"?", - "Leave_Livechat_Warning": "Вы ўпэўненыя, што жадаеце пакінуць звязаўшыся \"%s\"?", - "Leave_Private_Warning": "Вы ўпэўненыя, што жадаеце пакінуць дыскусію з \"%s\"?", + "Leave_Group_Warning": "Вы ўпэўненыя, што жадаеце выйсці з групы \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Вы ўпэўненыя, што жадаеце пакінуць звязаўшыся \"{{roomName}}\"?", + "Leave_Private_Warning": "Вы ўпэўненыя, што жадаеце пакінуць дыскусію з \"{{roomName}}\"?", "Leave_room": "Пакіньце нумар", - "Leave_Room_Warning": "Вы ўпэўненыя, што хочаце выйсці з пакоя \"%s\"?", + "Leave_Room_Warning": "Вы ўпэўненыя, што хочаце выйсці з пакоя \"{{roomName}}\"?", "Leave_the_current_channel": "Пакіньце бягучы канал", "leave-c": "Пакіньце каналы", "leave-p": "Пакіньце Прыватныя групы", @@ -2463,7 +2461,6 @@ "Two-factor_authentication": "двухфакторную аўтэнтыфікацыя", "Two-factor_authentication_disabled": "Двухфакторную аўтэнтыфікацыя адключаная", "Two-factor_authentication_enabled": "ўключана праверка сапраўднасці двухфакторную", - "Two-factor_authentication_is_currently_disabled": "Двухфакторную аўтэнтыфікацыя ў цяперашні час адключана", "Two-factor_authentication_native_mobile_app_warning": "УВАГА: Пасля таго, як вы уключыце гэта, вы не зможаце ўвайсці на натыўнымі мабільных прыкладаннях (Rocket.Chat +), выкарыстоўваючы свой пароль, пакуль яны не рэалізуюць 2fa.", "Type": "тып", "Type_your_email": "Увядзіце адрас электроннай пошты", @@ -2771,4 +2768,4 @@ "registration.component.form.sendConfirmationEmail": "Адправіць па электроннай пошце пацвярджэнне", "Enterprise": "прадпрыемства", "UpgradeToGetMore_engagement-dashboard_Title": "аналітыка" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/bg.i18n.json b/packages/i18n/src/locales/bg.i18n.json index 47ca415025f93..07fedb24df3f5 100644 --- a/packages/i18n/src/locales/bg.i18n.json +++ b/packages/i18n/src/locales/bg.i18n.json @@ -863,7 +863,6 @@ "Directory": "указател", "Disable_Facebook_integration": "Деактивиране на интеграцията във Facebook", "Disable_Notifications": "Деактивиране на известията", - "Disable_two-factor_authentication": "Деактивирайте двуфакторното удостоверяване", "Disabled": "хора с увреждания", "Disallow_reacting": "Забраняване на реагирането", "Disallow_reacting_Description": "Забранява реакцията", @@ -956,7 +955,6 @@ "Enable_Auto_Away": "Активиране на функцията Автоматично излизане", "Enable_Desktop_Notifications": "Включи известията за работният плот", "Enable_Svg_Favicon": "Активирайте SVG favicon", - "Enable_two-factor_authentication": "Активирайте двуфакторното удостоверяване", "Enabled": "Enabled", "Encrypted_message": "Криптирано съобщение", "End_OTR": "Край OTR", @@ -1218,12 +1216,12 @@ "Hide": "Скрий стая", "Hide_counter": "Скриване на брояч", "Hide_flextab": "Скриване на дясната странична лента с кликване", - "Hide_Group_Warning": "Наистина ли искате да скриете групата \"%s\"?", - "Hide_Livechat_Warning": "Наистина ли искате да скриете livechat с \"%s\"?", - "Hide_Private_Warning": "Наистина ли искате да скриете дискусията с \"%s\"?", + "Hide_Group_Warning": "Наистина ли искате да скриете групата \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Наистина ли искате да скриете livechat с \"{{roomName}}\"?", + "Hide_Private_Warning": "Наистина ли искате да скриете дискусията с \"{{roomName}}\"?", "Hide_roles": "Скриване на роли", "Hide_room": "Скрий стая", - "Hide_Room_Warning": "Наистина ли искате да скриете стаята \"%s\"?", + "Hide_Room_Warning": "Наистина ли искате да скриете стаята \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Скриване на състоянието на непрочетената стая", "Hide_usernames": "Скриване на потребителските имена", "Highlights": "Акценти", @@ -1521,11 +1519,11 @@ "Lead_capture_email_regex": "Водещ имейл регекс за улавяне", "Lead_capture_phone_regex": "Водещ телефонен регекс за улавяне", "Leave": "Излез от стаята", - "Leave_Group_Warning": "Наистина ли искате да напуснете групата \"%s\"?", - "Leave_Livechat_Warning": "Сигурни ли сте, че искате да оставите livechat с \"%s\"?", - "Leave_Private_Warning": "Наистина ли искате да оставите дискусията с \"%s\"?", + "Leave_Group_Warning": "Наистина ли искате да напуснете групата \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Сигурни ли сте, че искате да оставите livechat с \"{{roomName}}\"?", + "Leave_Private_Warning": "Наистина ли искате да оставите дискусията с \"{{roomName}}\"?", "Leave_room": "Излез от стаята", - "Leave_Room_Warning": "Наистина ли искате да излезете от стаята \"%s\"?", + "Leave_Room_Warning": "Наистина ли искате да излезете от стаята \"{{roomName}}\"?", "Leave_the_current_channel": "Оставете текущия канал", "leave-c": "Оставете канали", "leave-p": "Напускане на частни групи", @@ -2442,7 +2440,6 @@ "Two-factor_authentication": "Двуфакторна удостоверяване", "Two-factor_authentication_disabled": "Двуфакторното удостоверяване е деактивирано", "Two-factor_authentication_enabled": "Разрешено е двуфакторно удостоверяване", - "Two-factor_authentication_is_currently_disabled": "Двуфакторното удостоверяване понастоящем е деактивирано", "Two-factor_authentication_native_mobile_app_warning": "ПРЕДУПРЕЖДЕНИЕ: След като активирате това, няма да можете да влезете в родните мобилни приложения (Rocket.Chat +), като използвате паролата си, докато не внедрят 2FA.", "Type": "Тип", "Type_your_email": "Въведете имейла си", @@ -2746,4 +2743,4 @@ "registration.component.form.sendConfirmationEmail": "Изпратете имейл за потвърждение", "Enterprise": "начинание", "UpgradeToGetMore_engagement-dashboard_Title": "анализ" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/bs.i18n.json b/packages/i18n/src/locales/bs.i18n.json index 2ccdddbc6e06b..ff29cfbb36bf0 100644 --- a/packages/i18n/src/locales/bs.i18n.json +++ b/packages/i18n/src/locales/bs.i18n.json @@ -862,7 +862,6 @@ "Directory": "Imenik", "Disable_Facebook_integration": "Onemogućivanje integracije Facebooka", "Disable_Notifications": "Onemogućivanje obavijesti", - "Disable_two-factor_authentication": "Onemogućivanje provjere autentičnosti s dva faktora", "Disabled": "onesposobljen", "Disallow_reacting": "Ne dopusti reagiranje", "Disallow_reacting_Description": "Ne dopušta reagiranje", @@ -954,7 +953,6 @@ "Enable_Auto_Away": "Omogućite automatsko odjavljivanje", "Enable_Desktop_Notifications": "Omogući obavijesti na radnoj površini", "Enable_Svg_Favicon": "Omogući SVG favicon", - "Enable_two-factor_authentication": "Omogući autentifikaciju s dva faktora", "Enabled": "Omogućeno", "Encrypted_message": "Zaštićena poruka", "End_OTR": "Završi SP", @@ -1214,12 +1212,12 @@ "Hide": "Sakrij sobu", "Hide_counter": "Sakrij brojač", "Hide_flextab": "Sakrij desni izbornik klikom", - "Hide_Group_Warning": "Jeste li sigurni da želite sakriti grupu \"%s\"?", - "Hide_Livechat_Warning": "Jeste li sigurni da želite sakriti livechat s \"%s\"?", - "Hide_Private_Warning": "Jeste li sigurni da želite sakriti raspravu s \"%s\"?", + "Hide_Group_Warning": "Jeste li sigurni da želite sakriti grupu \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Jeste li sigurni da želite sakriti livechat s \"{{roomName}}\"?", + "Hide_Private_Warning": "Jeste li sigurni da želite sakriti raspravu s \"{{roomName}}\"?", "Hide_roles": "Sakrij uloge", "Hide_room": "Sakrij sobu", - "Hide_Room_Warning": "Jeste li sigurni da želite sakriti sobu \"%s\"?", + "Hide_Room_Warning": "Jeste li sigurni da želite sakriti sobu \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Sakrij status nepročitane sobe", "Hide_usernames": "Sakrij korisnička imena", "Highlights": "Istaknuto", @@ -1519,11 +1517,11 @@ "Lead_capture_email_regex": "Olovo za hvatanje e-pošte regex", "Lead_capture_phone_regex": "Olovo za hvatanje regex telefona", "Leave": "Izađi iz sobe", - "Leave_Group_Warning": "Jeste li sigurni da želite napustiti grupu \"%s\"?", - "Leave_Livechat_Warning": "Jeste li sigurni da želite napustiti livechat s \"%s\"?", - "Leave_Private_Warning": "Jeste li sigurni da želite napustiti razgovor s \"%s\"?", + "Leave_Group_Warning": "Jeste li sigurni da želite napustiti grupu \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Jeste li sigurni da želite napustiti livechat s \"{{roomName}}\"?", + "Leave_Private_Warning": "Jeste li sigurni da želite napustiti razgovor s \"{{roomName}}\"?", "Leave_room": "Izađi iz sobe", - "Leave_Room_Warning": "Jeste li sigurni da želite izaći iz sobe \"%s\"?", + "Leave_Room_Warning": "Jeste li sigurni da želite izaći iz sobe \"{{roomName}}\"?", "Leave_the_current_channel": "Napusti trenutnu sobu", "leave-c": "Ostavite kanale", "leave-p": "Napusti privatne grupe", @@ -2439,7 +2437,6 @@ "Two-factor_authentication": "Provjera autentičnosti s dva faktora", "Two-factor_authentication_disabled": "Autentifikacija s dva faktora je onemogućena", "Two-factor_authentication_enabled": "Omogućena je autentifikacija s dva faktora", - "Two-factor_authentication_is_currently_disabled": "Trenutačno je onemogućena autentikacija s dva faktora", "Two-factor_authentication_native_mobile_app_warning": "UPOZORENJE: nakon što omogućite ovo, nećete se moći prijaviti na izvorne mobilne aplikacije (Rocket.Chat +) pomoću svoje lozinke dok ne implementirate 2FA.", "Type": "Vrsta", "Type_your_email": "Upišite Vaš e-mail", @@ -2743,4 +2740,4 @@ "registration.component.form.sendConfirmationEmail": "Pošalji potvrdni email", "Enterprise": "Poduzeće", "UpgradeToGetMore_engagement-dashboard_Title": "Analitika" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ca.i18n.json b/packages/i18n/src/locales/ca.i18n.json index 039a76f53a1b2..224b471b71ac7 100644 --- a/packages/i18n/src/locales/ca.i18n.json +++ b/packages/i18n/src/locales/ca.i18n.json @@ -596,7 +596,6 @@ "Back_to_integrations": "Torna a les integracions", "Back_to_login": "Torna a identificar-me", "Back_to_permissions": "Torna a permisos", - "Back_to_room": "Tornar a Room", "Back_to_threads": "Torna als fils", "Backup_codes": "Codis de recuperació", "Belongs_To": "Pertany a", @@ -1305,8 +1304,6 @@ "Disable": "Desactivar", "Disable_Facebook_integration": "Desactiva la integració de Facebook", "Disable_Notifications": "Desactiva notificacions", - "Disable_two-factor_authentication": "Desactiva l'autenticació de dos factors", - "Disable_two-factor_authentication_email": "Desactivar l'autenticació de doble factor per correu electrònic", "Disabled": "Inactiu", "Disallow_reacting": "No permetre la reacció", "Disallow_reacting_Description": "No permet reaccionar", @@ -1434,8 +1431,6 @@ "Enable_Svg_Favicon": "Activa el favicon SVG", "Enable_inquiry_fetch_by_stream": "Habilitar l'obtenció de dades de consulta des del servidor mitjançant una seqüència", "Enable_omnichannel_auto_close_abandoned_rooms": "Habilita el tancament automàtic de sales abandonades pel visitant", - "Enable_two-factor_authentication": "Activa l'autenticació de dos factors mitjançant TOTP", - "Enable_two-factor_authentication_email": "Habilitar l'autenticació en 2 passos via correu electrònic", "Enabled": "Activa", "Encrypted": "Xifrat", "Encrypted_channel_Description": "Canal xifrat d'extrem a extrem. La cerca no funcionarà amb canals xifrats i és possible que les notificacions no mostrin el contingut dels missatges.", @@ -1724,10 +1719,10 @@ "Hi_username": "Hola [name]", "Hidden": "Ocult", "Hide": "Amagar", - "Hide_Group_Warning": "Segur que voleu ocultar el grup \"%s\"?", - "Hide_Livechat_Warning": "Estàs segur que vols amagar al xat amb \"%s\"?", - "Hide_Private_Warning": "Segur que voleu ocultar la discussió amb \"%s\"?", - "Hide_Room_Warning": "Segur que vols amagar la sala amb \"%s\"?", + "Hide_Group_Warning": "Segur que voleu ocultar el grup \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Estàs segur que vols amagar al xat amb \"{{roomName}}\"?", + "Hide_Private_Warning": "Segur que voleu ocultar la discussió amb \"{{roomName}}\"?", + "Hide_Room_Warning": "Segur que vols amagar la sala amb \"{{roomName}}\"?", "Hide_System_Messages": "Ocultar els missatges del sistema", "Hide_Unread_Room_Status": "Amaga l'estat de sales no llegides", "Hide_counter": "Amaga comptador", @@ -2193,10 +2188,10 @@ "Lead_capture_phone_regex": "Regex de telèfon de captura clients potencials", "Least_recent_updated": "Actualització menys recent", "Leave": "Sortir ", - "Leave_Group_Warning": "Segur que vols deixar el grup \"%s\"?", - "Leave_Livechat_Warning": "Segur que vols sortir de l'LiveChat amb \"%s\"?", - "Leave_Private_Warning": "Segur que vols sortir de la conversa amb \"%s\"?", - "Leave_Room_Warning": "Segur que vols sortir de la sala \"%s\"?", + "Leave_Group_Warning": "Segur que vols deixar el grup \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Segur que vols sortir de l'LiveChat amb \"{{roomName}}\"?", + "Leave_Private_Warning": "Segur que vols sortir de la conversa amb \"{{roomName}}\"?", + "Leave_Room_Warning": "Segur que vols sortir de la sala \"{{roomName}}\"?", "Leave_a_comment": "Deixar un comentari", "Leave_room": "Sortir ", "Leave_the_current_channel": "Surt del canal actual", @@ -3695,9 +3690,7 @@ "Two-factor_authentication": "Autenticació de dos factors a través de TOTP", "Two-factor_authentication_disabled": "Autenticació de dos factors desactivada", "Two-factor_authentication_email": "Autenticació de dos factors via correu electrònic", - "Two-factor_authentication_email_is_currently_disabled": "L'autenticació en 2 passos via correu electrònic està inhabilitada", "Two-factor_authentication_enabled": "Autenticació de dos factors activada", - "Two-factor_authentication_is_currently_disabled": "L'autenticació de dos factors a través d'TOTP està actualment inhabilitada", "Two-factor_authentication_native_mobile_app_warning": "ATENCIÓ: Un cop activat això, no es podrà fer login des de les aplicacions mòbils natives (Rocket.Chat+) utilitzant la contrasenya fins que aquestes implementin el 2FA.", "Two-factor_authentication_via_TOTP": "Autenticació de dos factors a través de TOTP", "Type": "Tipus", diff --git a/packages/i18n/src/locales/cs.i18n.json b/packages/i18n/src/locales/cs.i18n.json index 8f0a504a72e1e..5029de8c83d44 100644 --- a/packages/i18n/src/locales/cs.i18n.json +++ b/packages/i18n/src/locales/cs.i18n.json @@ -501,7 +501,6 @@ "Back_to_integrations": "Zpět k integracím", "Back_to_login": "Zpět na přihlašovací formulář", "Back_to_permissions": "Zpět na práva", - "Back_to_room": "Zpět do místnosti", "Backup_codes": "Záložní kódy", "Best_first_response_time": "Nejlepší doba první reakce", "Beta_feature_Depends_on_Video_Conference_to_be_enabled": "Beta funkcionalita. Videohovory musí být povoleny.", @@ -1110,8 +1109,6 @@ "Directory": "Adresář", "Disable_Facebook_integration": "Zakázat Facebook integraci", "Disable_Notifications": "Zakázat notifikace", - "Disable_two-factor_authentication": "Zakázat dvoufázové ověření pomocí TOTP", - "Disable_two-factor_authentication_email": "Zakázat dvougázové ověření přes Email", "Disabled": "Zakázáno", "Disallow_reacting": "Zakázat reakce", "Disallow_reacting_Description": "Zakáže reakce", @@ -1215,8 +1212,6 @@ "Enable_Svg_Favicon": "Povolit SVG favikonu", "Enable_inquiry_fetch_by_stream": "Povolit načítání dat ze serveru pomocí streamu", "Enable_omnichannel_auto_close_abandoned_rooms": "Povolit automatické uzavření místnosti opuštěné návštěvníkem", - "Enable_two-factor_authentication": "Povolit dvoufázové ověření pomocí TOTP", - "Enable_two-factor_authentication_email": "Povolit dvoufázové ověření přes Email", "Enabled": "Povoleno", "Encrypted": "Šifrováno", "Encrypted_channel_Description": "End-to-end šifrovaný kanál. Hledání nebude fungovat s šifrovanými kanály a oznámení nemusí zobrazovat obsah zpráv.", @@ -1470,10 +1465,10 @@ "Hi_username": "Ahoj [name]", "Hidden": "Schovaný", "Hide": "Skrýt", - "Hide_Group_Warning": "Jste si jisti, že chcete skrýt skupiny \"%s\"?", - "Hide_Livechat_Warning": "Opravdu chcete skrýt chat s \"%s\"?", - "Hide_Private_Warning": "Jste si jisti, že chcete skrýt diskusi s \"%s\"?", - "Hide_Room_Warning": "Jste si jisti, že chcete skrýt místnost \"%s\"?", + "Hide_Group_Warning": "Jste si jisti, že chcete skrýt skupiny \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Opravdu chcete skrýt chat s \"{{roomName}}\"?", + "Hide_Private_Warning": "Jste si jisti, že chcete skrýt diskusi s \"{{roomName}}\"?", + "Hide_Room_Warning": "Jste si jisti, že chcete skrýt místnost \"{{roomName}}\"?", "Hide_System_Messages": "Skrýt systémové zprávy", "Hide_Unread_Room_Status": "Schovat stav nepřečtených místností", "Hide_counter": "Schovat počítadlo", @@ -1863,10 +1858,10 @@ "Lead_capture_email_regex": "Regulární výraz pro zachycení Leadu na email", "Lead_capture_phone_regex": "Regulární výraz pro zachycení Leadu na telefon", "Leave": "Opustit", - "Leave_Group_Warning": "Jste si jisti, že chcete opustit skupinu \"%s\"?", - "Leave_Livechat_Warning": "Opravdu chcete opustit LiveChat s \"%s\"?", - "Leave_Private_Warning": "Jste si jisti, že chcete opustit diskusi s \"%s\"?", - "Leave_Room_Warning": "Jste si jisti, že chcete opustit místnost \"%s\"?", + "Leave_Group_Warning": "Jste si jisti, že chcete opustit skupinu \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Opravdu chcete opustit LiveChat s \"{{roomName}}\"?", + "Leave_Private_Warning": "Jste si jisti, že chcete opustit diskusi s \"{{roomName}}\"?", + "Leave_Room_Warning": "Jste si jisti, že chcete opustit místnost \"{{roomName}}\"?", "Leave_a_comment": "Zanechat komentář", "Leave_room": "Opustit", "Leave_the_current_channel": "Opustit aktuální místnost", @@ -3127,9 +3122,7 @@ "Two-factor_authentication": "Dvoufázové ověření pomocí TOTP", "Two-factor_authentication_disabled": "Dvoufázová ověření zakázáno", "Two-factor_authentication_email": "Dvoufázové ověření pomocí Email", - "Two-factor_authentication_email_is_currently_disabled": "Dvoufázové ověření pomocí Emailu zakázáno", "Two-factor_authentication_enabled": "Dvoufázové ověření povoleno", - "Two-factor_authentication_is_currently_disabled": "Dvoufázové ověření pomocí TOTP je momentálně zakázáno", "Two-factor_authentication_native_mobile_app_warning": "UPOZORNĚNÍ: Pokud povolíte dvoufázové ověření, nebudete se moci přihlásit přes nativní mobilní aplikace (Rocket.Chat+) dokud v těchto nebude 2FA implementována.", "Two-factor_authentication_via_TOTP": "Dvoufázové ověření pomocí TOTP", "Type": "Typ", diff --git a/packages/i18n/src/locales/cy.i18n.json b/packages/i18n/src/locales/cy.i18n.json index aeb8e3a30b7d3..3a3d24a0b0ebb 100644 --- a/packages/i18n/src/locales/cy.i18n.json +++ b/packages/i18n/src/locales/cy.i18n.json @@ -862,7 +862,6 @@ "Directory": "Cyfeiriadur", "Disable_Facebook_integration": "Analluogi integreiddio Facebook", "Disable_Notifications": "Analluogi Hysbysiadau", - "Disable_two-factor_authentication": "Analluoga dilysiad dau ffactor", "Disabled": "Anabl", "Disallow_reacting": "Gwrthod yn Ailddefnyddio", "Disallow_reacting_Description": "Yn gwrthod ymateb", @@ -955,7 +954,6 @@ "Enable_Auto_Away": "Galluogi Auto Away", "Enable_Desktop_Notifications": "Galluogi Hysbysiadau Penbwrdd", "Enable_Svg_Favicon": "Galluogi ffugicon SVG", - "Enable_two-factor_authentication": "Galluogi dilysu dau ffactor", "Enabled": "Wedi'i alluogi", "Encrypted_message": "Neges wedi'i amgryptio", "End_OTR": "Diwedd OTR", @@ -1214,12 +1212,12 @@ "Hide": "Ystafell Guddio", "Hide_counter": "Cuddio cownter", "Hide_flextab": "Cuddio Bar Barhau Cywir gyda Chliciwch", - "Hide_Group_Warning": "Ydych chi'n siŵr eich bod am guddio'r grŵp \"%s\"?", - "Hide_Livechat_Warning": "Ydych chi'n siŵr eich bod am guddio'r bywladwr gyda \"%s\"?", - "Hide_Private_Warning": "Ydych chi'n siŵr eich bod am guddio'r drafodaeth gyda \"%s\"?", + "Hide_Group_Warning": "Ydych chi'n siŵr eich bod am guddio'r grŵp \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Ydych chi'n siŵr eich bod am guddio'r bywladwr gyda \"{{roomName}}\"?", + "Hide_Private_Warning": "Ydych chi'n siŵr eich bod am guddio'r drafodaeth gyda \"{{roomName}}\"?", "Hide_roles": "Cuddio Rolau", "Hide_room": "Ystafell Guddio", - "Hide_Room_Warning": "Ydych chi'n siŵr eich bod am guddio'r ystafell \"%s\"?", + "Hide_Room_Warning": "Ydych chi'n siŵr eich bod am guddio'r ystafell \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Cuddio Statws Ystafell Heb ei Darllen", "Hide_usernames": "Cuddio Enwau Defnyddwyr", "Highlights": "Uchafbwyntiau", @@ -1519,11 +1517,11 @@ "Lead_capture_email_regex": "Regex e-bost dal yn arwain", "Lead_capture_phone_regex": "Regex ffôn dal yn arwain", "Leave": "Ystafell gadael", - "Leave_Group_Warning": "Ydych chi'n siŵr eich bod am adael y grŵp \"%s\"?", - "Leave_Livechat_Warning": "Ydych chi'n siŵr eich bod am adael y byw-fyw gyda \"%s\"?", - "Leave_Private_Warning": "Ydych chi'n siŵr eich bod am adael y drafodaeth gyda \"%s\"?", + "Leave_Group_Warning": "Ydych chi'n siŵr eich bod am adael y grŵp \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Ydych chi'n siŵr eich bod am adael y byw-fyw gyda \"{{roomName}}\"?", + "Leave_Private_Warning": "Ydych chi'n siŵr eich bod am adael y drafodaeth gyda \"{{roomName}}\"?", "Leave_room": "Ystafell gadael", - "Leave_Room_Warning": "Ydych chi'n siŵr eich bod am adael yr ystafell \"%s\"?", + "Leave_Room_Warning": "Ydych chi'n siŵr eich bod am adael yr ystafell \"{{roomName}}\"?", "Leave_the_current_channel": "Gadewch y sianel gyfredol", "leave-c": "Gadael Sianeli", "leave-p": "Gadewch Grwpiau Preifat", @@ -2440,7 +2438,6 @@ "Two-factor_authentication": "Dilysu dau ffactor", "Two-factor_authentication_disabled": "Dilysu dau ffactor anabl", "Two-factor_authentication_enabled": "Gall dilysu dau ffactor alluogi", - "Two-factor_authentication_is_currently_disabled": "Mae dilysiad dau ffactor ar hyn o bryd yn anabl", "Two-factor_authentication_native_mobile_app_warning": "RHYBUDD: Ar ôl i chi alluogi hyn, ni allwch fewngofnodi ar y apps symudol brodorol (Rocket.Chat +) gan ddefnyddio'ch cyfrinair nes iddynt weithredu'r 2FA.", "Type": "Math", "Type_your_email": "Teipiwch eich e-bost", @@ -2745,4 +2742,4 @@ "registration.component.form.sendConfirmationEmail": "Anfon ebost cadarnhad", "Enterprise": "Menter", "UpgradeToGetMore_engagement-dashboard_Title": "Dadansoddiadau" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/da.i18n.json b/packages/i18n/src/locales/da.i18n.json index 2049e6a8113cd..52c7af2042ca1 100644 --- a/packages/i18n/src/locales/da.i18n.json +++ b/packages/i18n/src/locales/da.i18n.json @@ -648,7 +648,6 @@ "Back_to_imports": "Tilbage til import", "Cancel": "Annullér", "Cancel_message_input": "Annullér", - "Back_to_room": "Tilbage til rum", "Canceled": "Annulleret", "Cannot_invite_users_to_direct_rooms": "Kan ikke invitere brugere til at direkte rum", "Cannot_open_conversation_with_yourself": "Kan ikke oprette direkte besked med dig selv", @@ -1251,7 +1250,6 @@ "Custom_Script_Logged_Out_Description": "Brugerdefineret script der ALTID kører og for ENHVER bruger der IKKE er logget ind. (F.eks. hver gang du kommer til login-siden)", "Disable_Notifications": "Deaktivér notifikationer", "Custom_Script_On_Logout": "Brugerdefineret script til Logout Flow", - "Disable_two-factor_authentication": "Deaktivér tofaktorgodkendelse", "Custom_Script_On_Logout_Description": "Brugerdefineret script, der KUN kører ved udførslen af Logout-flowet", "Disabled": "Deaktiveret", "Disallow_reacting": "Tillad ikke Reacting", @@ -1388,14 +1386,12 @@ "Emoji": "Emoji", "EmojiCustomFilesystem": "Brugerdefineret emoji-filsystem", "Empty_title": "Tom titel", - "Disable_two-factor_authentication_email": "Deaktiver to-faktor-godkendelse via e-mail", "Enable": "Aktivér", "Enable_Auto_Away": "Aktivér automatisk \"Ikke til stede\"", "Enable_Desktop_Notifications": "Aktivér skrivebordsnotifikationer", "Discard": "Kassér", "Discussion": "Diskussion", "Enable_Svg_Favicon": "Aktivér SVG favicon", - "Enable_two-factor_authentication": "Aktivér tofaktorgodkendelse vi TOTP", "Enabled": "Aktiveret", "Encrypted": "Krypteret", "Encrypted_channel_Description": "End-to-end krypteringskanal. Søgning vil ikke virke med krypterede kanaler and notifikationer vil muligvis ikke vise the beskeden korrekt.", @@ -1579,7 +1575,6 @@ "External_Queue_Service_URL": "URL for ekstern kø-service", "External_Service": "Ekstern service", "Facebook_Page": "Facebookside", - "Enable_two-factor_authentication_email": "Aktivér to-faktor-godkendelse via e-mail", "False": "Falsk", "Favorite": "Favorit", "Favorite_Rooms": "Aktivér foretrukne rum", @@ -1775,12 +1770,12 @@ "Hide": "Skul", "Hide_counter": "Skjul tæller", "Hide_flextab": "Skjul højre sidepanel med klik", - "Hide_Group_Warning": "Er du sikker på, at du vil skjule gruppen \"%s\"?", - "Hide_Livechat_Warning": "Er du sikker på at du vil skjule chatten med \"%s\"?", - "Hide_Private_Warning": "Er du sikker på, at du vil skjule diskussionen med \"%s\"?", + "Hide_Group_Warning": "Er du sikker på, at du vil skjule gruppen \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Er du sikker på at du vil skjule chatten med \"{{roomName}}\"?", + "Hide_Private_Warning": "Er du sikker på, at du vil skjule diskussionen med \"{{roomName}}\"?", "Hide_roles": "Skjul roller", "Hide_room": "Skjul", - "Hide_Room_Warning": "Er du sikker på, at du vil skjule kanalen \"%s\"?", + "Hide_Room_Warning": "Er du sikker på, at du vil skjule kanalen \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Skjul ulæst rumstatus", "Hide_usernames": "Skjul brugernavne", "Highlights": "Højdepunkter", @@ -2198,11 +2193,11 @@ "Lead_capture_email_regex": "Lead capture email regex", "Lead_capture_phone_regex": "Lead capture phone regex", "Leave": "Forlad", - "Leave_Group_Warning": "Er du sikker på, at du vil forlade gruppen \"%s\"?", - "Leave_Livechat_Warning": "Er du sikker på at du vil forlade Omnichannel'en med \"%s\"?", - "Leave_Private_Warning": "Er du sikker på at du vil forlade diskussionen med \"%s\"?", + "Leave_Group_Warning": "Er du sikker på, at du vil forlade gruppen \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Er du sikker på at du vil forlade Omnichannel'en med \"{{roomName}}\"?", + "Leave_Private_Warning": "Er du sikker på at du vil forlade diskussionen med \"{{roomName}}\"?", "Leave_room": "Forlad", - "Leave_Room_Warning": "Er du sikker på at du vil forlade kanalen \"%s\"?", + "Leave_Room_Warning": "Er du sikker på at du vil forlade kanalen \"{{roomName}}\"?", "Leave_the_current_channel": "Forlad den nuværende kanal", "leave-c": "Forlad kanaler", "Instance": "Instans", @@ -3518,7 +3513,6 @@ "Two-factor_authentication": "Tofaktorgodkendelse", "Two-factor_authentication_disabled": "Tofaktorgodkendelse er deaktiveret", "Two-factor_authentication_enabled": "Tofaktorgodkendelse er aktiveret", - "Two-factor_authentication_is_currently_disabled": "Tofaktorgodkendelse er ikke aktiveret", "Two-factor_authentication_native_mobile_app_warning": "ADVARSEL: Når du har aktiveret dette, kan du ikke logge ind på de indbyggede mobilapps (Rocket.Chat +) ved hjælp af dit kodeord, indtil de implementerer 2FA.", "Type": "Type", "Room_updated_successfully": "Rummet blev succesfuldt opdateret", @@ -3966,7 +3960,6 @@ "Try_now": "Forsøg nu", "Two-factor_authentication_via_TOTP": "Tofaktorgodkendelse", "Two-factor_authentication_email": "To-faktor-godkendelse via e-mail", - "Two-factor_authentication_email_is_currently_disabled": "To-faktor-godkendelse via e-mail er i øjeblikket deaktiveret", "UI_Show_top_navbar_embedded_layout": "Vis øverste navigationsbar i integreret layout", "unable-to-get-file": "Kan ikke hente fil", "unauthorized": "Ikke godkendt", @@ -4075,4 +4068,4 @@ "Enterprise": "Firma", "UpgradeToGetMore_engagement-dashboard_Title": "Analyse", "UpgradeToGetMore_auditing_Title": "Meddelelsesovervågning" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/de-AT.i18n.json b/packages/i18n/src/locales/de-AT.i18n.json index bc7ab2741a443..1b7944bbb4c3d 100644 --- a/packages/i18n/src/locales/de-AT.i18n.json +++ b/packages/i18n/src/locales/de-AT.i18n.json @@ -866,7 +866,6 @@ "Directory": "Verzeichnis", "Disable_Facebook_integration": "Deaktivieren Sie die Facebook-Integration", "Disable_Notifications": "Benachrichtigungen ausschalten", - "Disable_two-factor_authentication": "Deaktivieren Sie die Zwei-Faktor-Authentifizierung", "Disabled": "Behindert", "Disallow_reacting": "Reaktion nicht zulassen", "Disallow_reacting_Description": "Nicht reagieren", @@ -960,7 +959,6 @@ "Enable_Auto_Away": "Verfügbarkeit automatisch umstellen", "Enable_Desktop_Notifications": "Aktivieren", "Enable_Svg_Favicon": "Aktiviere SVG Favicon", - "Enable_two-factor_authentication": "Aktivieren Sie die Zwei-Faktor-Authentifizierung", "Enabled": "Aktiviert", "Encrypted_message": "Verschlüsselte Nachricht", "End_OTR": "OTR beenden", @@ -1222,12 +1220,12 @@ "Hide": "Chatraum verstecken", "Hide_counter": "Zähler ausblenden", "Hide_flextab": "Verstecken Sie die rechte Seitenleiste mit Klick", - "Hide_Group_Warning": "Sind sie sicher, die Gruppe\"%s\" zu verstecken?", - "Hide_Livechat_Warning": "Möchtest du den Livechat wirklich mit \"%s\" ausblenden?", - "Hide_Private_Warning": "Sind sie sicher, das Gespräch mit \"%s\" zu verstecken?", + "Hide_Group_Warning": "Sind sie sicher, die Gruppe\"{{roomName}}\" zu verstecken?", + "Hide_Livechat_Warning": "Möchtest du den Livechat wirklich mit \"{{roomName}}\" ausblenden?", + "Hide_Private_Warning": "Sind sie sicher, das Gespräch mit \"{{roomName}}\" zu verstecken?", "Hide_roles": "Rollen ausblenden", "Hide_room": "Chatraum verstecken", - "Hide_Room_Warning": "Sind sie sicher, den Raum \"%s\" zu verstecken?", + "Hide_Room_Warning": "Sind sie sicher, den Raum \"{{roomName}}\" zu verstecken?", "Hide_Unread_Room_Status": "Ungelesenen Zimmerstatus ausblenden", "Hide_usernames": "Benutzernamen ausblenden", "Highlights": "Hervorhebungen", @@ -1526,11 +1524,11 @@ "Lead_capture_email_regex": "Lead Capture E-Mail Regex", "Lead_capture_phone_regex": "Lead Capture Telefon Regex", "Leave": "Chatraum verlassen", - "Leave_Group_Warning": "Sind sie sicher, die Chatgruppe \"%s\" verlassen zu wollen?", - "Leave_Livechat_Warning": "Möchtest du den Livechat wirklich mit \"%s\" verlassen?", - "Leave_Private_Warning": "Sind sie sicher, das Gespräch mit \"%s\" zu verlassen?", + "Leave_Group_Warning": "Sind sie sicher, die Chatgruppe \"{{roomName}}\" verlassen zu wollen?", + "Leave_Livechat_Warning": "Möchtest du den Livechat wirklich mit \"{{roomName}}\" verlassen?", + "Leave_Private_Warning": "Sind sie sicher, das Gespräch mit \"{{roomName}}\" zu verlassen?", "Leave_room": "Chatraum verlassen", - "Leave_Room_Warning": "Sind sie sicher, den Raum \"%s\" zu verlassen?", + "Leave_Room_Warning": "Sind sie sicher, den Raum \"{{roomName}}\" zu verlassen?", "Leave_the_current_channel": "Verlasse den aktuellen Kanal", "leave-c": "Kanäle verlassen", "leave-p": "Verlassen Sie private Gruppen", @@ -2449,7 +2447,6 @@ "Two-factor_authentication": "Zwei-Faktor-Authentifizierung", "Two-factor_authentication_disabled": "Zwei-Faktor-Authentifizierung deaktiviert", "Two-factor_authentication_enabled": "Zwei-Faktor-Authentifizierung aktiviert", - "Two-factor_authentication_is_currently_disabled": "Die Zwei-Faktor-Authentifizierung ist derzeit deaktiviert", "Two-factor_authentication_native_mobile_app_warning": "WARNUNG: Sobald Sie dies aktiviert haben, können Sie sich nicht mehr mit den nativen mobilen Apps (Rocket.Chat +) mit Ihrem Passwort anmelden, bevor Sie das 2FA implementieren.", "Type": "Typ", "Type_your_email": "Geben Sie Ihre E-Mail-Adresse ein", @@ -2753,4 +2750,4 @@ "registration.component.form.sendConfirmationEmail": "Bestätigungsmail versenden", "Enterprise": "Unternehmen", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/de-IN.i18n.json b/packages/i18n/src/locales/de-IN.i18n.json index 93bacebaf2cf0..e47e9875c8802 100644 --- a/packages/i18n/src/locales/de-IN.i18n.json +++ b/packages/i18n/src/locales/de-IN.i18n.json @@ -991,7 +991,6 @@ "Directory": "Verzeichnis", "Disable_Facebook_integration": "Fakebook Integration deaktivieren", "Disable_Notifications": "Benachrichtigungen deaktivieren", - "Disable_two-factor_authentication": "Zwei-Faktor-Authentifizierung deaktivieren", "Disabled": "deaktiviert", "Disallow_reacting": "Reaktionen verbieten", "Disallow_reacting_Description": "Verhindert, dass ein Benutzer auf eine Nachricht mit Emojis reagiert", @@ -1105,7 +1104,6 @@ "Enable_Auto_Away": "\"Abwesend\" automatisch aktivieren", "Enable_Desktop_Notifications": "Desktop-Benachrichtigungen", "Enable_Svg_Favicon": "SVG Favicon", - "Enable_two-factor_authentication": "Zwei-Faktor-Authentifizierung aktivieren", "Enabled": "Aktiviert", "Encrypted": "Verschlüsselt", "Encrypted_channel_Description": "Ende-zu-Ende verschlüsselter Kanal. Die Suche funktioniert nicht mit verschlüsselten Kanälen. In Benachrichtigungen wird der Inhalt der Nachricht möglicherweise nicht angezeigt.", @@ -1409,12 +1407,12 @@ "Hide": "Verstecken", "Hide_counter": "Zähler verstecken", "Hide_flextab": "Rechte Seitenleiste über Klick verstecken", - "Hide_Group_Warning": "Bist Du sicher, dass Du den privaten Kanal \"%s\" verstecken möchtest?", - "Hide_Livechat_Warning": "Bist Du Dir sicher, dass Du den Livechat mit \"%s\" ausblenden möchtest?", - "Hide_Private_Warning": "Bist Du Dir sicher, dass Du das Gespräch mit \"%s\" verstecken möchtest?", + "Hide_Group_Warning": "Bist Du sicher, dass Du den privaten Kanal \"{{roomName}}\" verstecken möchtest?", + "Hide_Livechat_Warning": "Bist Du Dir sicher, dass Du den Livechat mit \"{{roomName}}\" ausblenden möchtest?", + "Hide_Private_Warning": "Bist Du Dir sicher, dass Du das Gespräch mit \"{{roomName}}\" verstecken möchtest?", "Hide_roles": "Rollen ausblenden", "Hide_room": "Raum verstecken", - "Hide_Room_Warning": "Bist Du Dir sicher, dass Du den Raum \"%s\" verstecken möchtest?", + "Hide_Room_Warning": "Bist Du Dir sicher, dass Du den Raum \"{{roomName}}\" verstecken möchtest?", "Hide_Unread_Room_Status": "Ungelesen-Status des Raums nicht anzeigen", "Hide_usernames": "Benutzernamen ausblenden", "Highlights": "Hervorhebungen", @@ -1751,11 +1749,11 @@ "Lead_capture_email_regex": "Lead Capture E-Mail Regex", "Lead_capture_phone_regex": "Lead Capture Telefon Regex", "Leave": "Verlassen", - "Leave_Group_Warning": "Bist Du Dir sicher, dass Du den privaten Kanal \"%s\" verlassen möchtest?", - "Leave_Livechat_Warning": "Bist Du Dir sicher, dass Du den Livechat mit \"%s\" verlassen möchtest?", - "Leave_Private_Warning": "Bist Du Dir sicher, dass Du das Gespräch mit \"%s\" verlassen möchtest?", + "Leave_Group_Warning": "Bist Du Dir sicher, dass Du den privaten Kanal \"{{roomName}}\" verlassen möchtest?", + "Leave_Livechat_Warning": "Bist Du Dir sicher, dass Du den Livechat mit \"{{roomName}}\" verlassen möchtest?", + "Leave_Private_Warning": "Bist Du Dir sicher, dass Du das Gespräch mit \"{{roomName}}\" verlassen möchtest?", "Leave_room": "Raum verlassen", - "Leave_Room_Warning": "Bist Du Dir sicher, dass Du den Kanal \"%s\" verlassen möchtest?", + "Leave_Room_Warning": "Bist Du Dir sicher, dass Du den Kanal \"{{roomName}}\" verlassen möchtest?", "Leave_the_current_channel": "Aktuellen Kanal verlassen", "leave-c": "Kanäle verlassen", "leave-p": "Verlasse private Gruppen", @@ -2778,7 +2776,6 @@ "Two-factor_authentication": "Zwei-Faktor-Authentifizierung", "Two-factor_authentication_disabled": "Zwei-Faktor-Authentifizierung deaktiviert", "Two-factor_authentication_enabled": "Zwei-Faktor-Authentifizierung aktiviert", - "Two-factor_authentication_is_currently_disabled": "Zwei-Faktor-Authentifizierung ist momentan deaktiviert", "Two-factor_authentication_native_mobile_app_warning": "WARNUNG: Nach der Aktivierung kannst du dich nicht mehr auf den mobilen Apps (Rocket.Chat+) einloggen, da dieses Feature dort noch nicht implementiert wurde.", "Type": "Typ", "Type_your_email": "Gib Deine E-Mail-Adresse ein", @@ -3071,4 +3068,4 @@ "Your_question": "Deine Frage", "Your_server_link": "Dein Server-Link", "Your_workspace_is_ready": "Dein Arbeitsbereich ist einsatzbereit 🎉" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/de.i18n.json b/packages/i18n/src/locales/de.i18n.json index 9f01a18645659..817cae76cbf43 100644 --- a/packages/i18n/src/locales/de.i18n.json +++ b/packages/i18n/src/locales/de.i18n.json @@ -747,7 +747,6 @@ "Back_to_imports": "Zurück zu den Importen", "Cancel": "Abbrechen", "Cancel_message_input": "Abbrechen", - "Back_to_room": "Zurück zu Room", "Canceled": "Abgebrochen", "Back_to_threads": "Zurück zu den Threads", "BBB_End_Meeting": "Meeting beenden", @@ -1499,7 +1498,6 @@ "Custom_Script_Logged_Out_Description": "Benutzerdefiniertes Skript, das IMMER und für JEDEN Benutzer ausgeführt wird, der NICHT angemeldet ist (bspw. beim Öffnen der Anmeldeseite betreten)", "Disable_Notifications": "Benachrichtigungen deaktivieren", "Custom_Script_On_Logout": "Benutzerdefiniertes Skript beim Abmelden", - "Disable_two-factor_authentication": "Zwei-Faktor-Authentifizierung deaktivieren", "Custom_Script_On_Logout_Description": "Benutzerdefiniertes Skript, das NUR beim Abmelden eines Benutzers ausgeführt wird", "Disabled": "Deaktiviert", "Disallow_reacting": "Reaktionen verbieten", @@ -1687,7 +1685,6 @@ "Disable": "Deaktivieren", "EmojiCustomFilesystem": "Dateisystem für eigene Emojis", "Empty_title": "Leerer Titel", - "Disable_two-factor_authentication_email": "Zwei-Faktor-Authentifizierung per E-Mail deaktivieren", "Enable": "Aktivieren", "Enable_Auto_Away": "\"Abwesend\" automatisch aktivieren", "Enable_Desktop_Notifications": "Desktop-Benachrichtigungen", @@ -1696,7 +1693,6 @@ "Discussion": "Diskussion", "Enable_Svg_Favicon": "SVG-Favicon", "Discussion_Description": "Diskussionen sind eine zusätzliche Möglichkeit, Unterhaltungen zu organisieren, die es erlaubt, Benutzer von außerhalb einzuladen, an bestimmten Unterhaltungen teilzunehmen.", - "Enable_two-factor_authentication": "Zwei-Faktor-Authentifizierung aktivieren", "Discussion_first_message_disabled_due_to_e2e": "Nach der Erstellung dieser Diskussion, können Sie mit dem Senden von Ende-zu-Ende-verschlüsselte Nachrichten beginnen", "Enabled": "Aktiviert", "Encrypted": "Verschlüsselt", @@ -1929,7 +1925,6 @@ "External_Queue_Service_URL": "URL der Queue des externen Dienstes", "External_Service": "Externer Dienst", "Facebook_Page": "Facebook Seite", - "Enable_two-factor_authentication_email": "Zwei-Faktor-Authentifizierung per E-Mail aktivieren", "Encrypted_not_available": "Für öffentliche Kanäle nicht verfügbar", "False": "Nein", "End": "Ende", @@ -2163,14 +2158,14 @@ "You_do_not_have_permission_to_do_this": "Sie haben keine Berechtigung, dies zu tun", "Hide_counter": "Zähler ausblenden", "Hide_flextab": "Rechte Seitenleiste mit Klick ausblenden", - "Hide_Group_Warning": "Sind Sie sicher, dass Sie die Gruppe \"%s\" ausblenden wollen?", - "Hide_Livechat_Warning": "Sind Sie sich sicher, dass Sie den Livechat mit \"%s\" ausblenden wollen?", + "Hide_Group_Warning": "Sind Sie sicher, dass Sie die Gruppe \"{{roomName}}\" ausblenden wollen?", + "Hide_Livechat_Warning": "Sind Sie sich sicher, dass Sie den Livechat mit \"{{roomName}}\" ausblenden wollen?", "Estimated_wait_time": "Geschätzte Wartezeit", "Estimated_wait_time_in_minutes": "Geschätzte Wartezeit (Zeit in Minuten)", - "Hide_Private_Warning": "Sind Sie sicher, dass Sie das Gespräch mit \"%s\" ausblenden wollen?", + "Hide_Private_Warning": "Sind Sie sicher, dass Sie das Gespräch mit \"{{roomName}}\" ausblenden wollen?", "Hide_roles": "Rollen ausblenden", "Hide_room": "Raum verstecken", - "Hide_Room_Warning": "Sind Sie sicher, dass Sie den Raum \"%s\" verstecken wollen?", + "Hide_Room_Warning": "Sind Sie sicher, dass Sie den Raum \"{{roomName}}\" verstecken wollen?", "Hide_Unread_Room_Status": "Ungelesen-Status des Rooms nicht anzeigen", "Hide_usernames": "Benutzernamen ausblenden", "every_30_seconds": "Einmal alle 30 Sekunden", @@ -2697,11 +2692,11 @@ "Inline_code": "Inline-Code", "Install_anyway": "Trotzdem installieren", "Leave": "Verlassen", - "Leave_Group_Warning": "Sind Sie sicher, dass Sie die Gruppe \"%s\" verlassen wollen?", - "Leave_Livechat_Warning": "Sind Sie sich sicher, dass Sie den Livechat mit \"%s\" verlassen wollen?", - "Leave_Private_Warning": "Sind Sie sicher, dass Sie die Diskussion mit \"%s\" verlassen wollen?", + "Leave_Group_Warning": "Sind Sie sicher, dass Sie die Gruppe \"{{roomName}}\" verlassen wollen?", + "Leave_Livechat_Warning": "Sind Sie sich sicher, dass Sie den Livechat mit \"{{roomName}}\" verlassen wollen?", + "Leave_Private_Warning": "Sind Sie sicher, dass Sie die Diskussion mit \"{{roomName}}\" verlassen wollen?", "Leave_room": "Verlassen", - "Leave_Room_Warning": "Sind Sie sicher, dass Sie den Raum \"%s\" verlassen wollen?", + "Leave_Room_Warning": "Sind Sie sicher, dass Sie den Raum \"{{roomName}}\" verlassen wollen?", "Leave_the_current_channel": "Aktuellen Channel verlassen", "leave-c": "Channels verlassen", "Instance": "Instanz", @@ -4459,7 +4454,6 @@ "room_removed_read_only_permission": "nur-Lese-Erlaubnis entfernt", "Two-factor_authentication_enabled": "Zwei-Faktor-Authentifizierung aktiviert", "room_set_read_only_permission": "Raum auf nur lesen setzen", - "Two-factor_authentication_is_currently_disabled": "Zwei-Faktor-Authentifizierung ist momentan deaktiviert", "Two-factor_authentication_native_mobile_app_warning": "WARNUNG: Nach der Aktivierung können Sie sich nicht mehr auf den mobilen Apps (Rocket.Chat+) einloggen, da dieses Feature dort noch nicht implementiert wurde.", "Type": "Typ", "Room_updated_successfully": "Room erfolgreich aktualisiert!", @@ -5103,7 +5097,6 @@ "Turn_off_video": "Video ausschalten", "Two-factor_authentication_via_TOTP": "Zwei-Faktor-Authentifizierung", "Two-factor_authentication_email": "Zwei-Faktor-Authentifizierung per E-Mail", - "Two-factor_authentication_email_is_currently_disabled": "Die Zwei-Faktor-Authentifizierung per E-Mail ist derzeit deaktiviert", "typing": "schreibt", "Types": "Typen", "Types_and_Distribution": "Typen und Verteilung", @@ -5503,4 +5496,4 @@ "Enterprise": "Unternehmen", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "UpgradeToGetMore_auditing_Title": "Nachrichtenüberprüfung" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/el.i18n.json b/packages/i18n/src/locales/el.i18n.json index 30c4c078b9a72..9e36f590cf4d3 100644 --- a/packages/i18n/src/locales/el.i18n.json +++ b/packages/i18n/src/locales/el.i18n.json @@ -870,7 +870,6 @@ "Directory": "Ευρετήριο", "Disable_Facebook_integration": "Απενεργοποίηση Ενσωμάτωσης Facebook", "Disable_Notifications": "Απενεργοποίηση Ειδοποιήσεων", - "Disable_two-factor_authentication": "Απενεργοποίηση ταυτοποίησης σε δύο βήματα", "Disabled": "Απενεργοποιημένο", "Disallow_reacting": "Απαγόρευση αντίδρασης", "Disallow_reacting_Description": "Δεν επιτρέπει την αντίδραση", @@ -963,7 +962,6 @@ "Enable_Auto_Away": "Ενεργοποίηση Αυτόματης Απουσίας", "Enable_Desktop_Notifications": "Ενεργοποίηση Ειδοποιήσεων Επιφάνειας Εργασίας", "Enable_Svg_Favicon": "Ενεργοποίηση SVG favicon", - "Enable_two-factor_authentication": "Ενεργοποίηση ταυτοποίησης δύο βημάτων", "Enabled": "Ενεργοποιήθηκε", "Encrypted_message": "Κρυπτογραφημένο μήνυμα", "End_OTR": "τέλος OTR", @@ -1225,12 +1223,12 @@ "Hide": "Απόκρυψη δωματίου", "Hide_counter": "Απόκρυψη μετρητή", "Hide_flextab": "Απόκρυψη της δεξιάς πλευρικής γραμμής με κλικ", - "Hide_Group_Warning": "Είστε σίγουροι ότι θέλετε να αποκρύψετε την ομάδα \"%s\";", - "Hide_Livechat_Warning": "Είστε βέβαιοι ότι θέλετε να αποκρύψετε το livechat με το \"%s\";", - "Hide_Private_Warning": "Είστε βέβαιοι ότι θέλετε να αποκρύψετε τη συζήτηση με το \"%s\";", + "Hide_Group_Warning": "Είστε σίγουροι ότι θέλετε να αποκρύψετε την ομάδα \"{{roomName}}\";", + "Hide_Livechat_Warning": "Είστε βέβαιοι ότι θέλετε να αποκρύψετε το livechat με το \"{{roomName}}\";", + "Hide_Private_Warning": "Είστε βέβαιοι ότι θέλετε να αποκρύψετε τη συζήτηση με το \"{{roomName}}\";", "Hide_roles": "Απόκρυψη ρόλων", "Hide_room": "Απόκρυψη δωματίου", - "Hide_Room_Warning": "Είστε σίγουροι ότι θέλετε να αποκρύψετε το δωμάτιο \"%s\";", + "Hide_Room_Warning": "Είστε σίγουροι ότι θέλετε να αποκρύψετε το δωμάτιο \"{{roomName}}\";", "Hide_Unread_Room_Status": "Απόκρυψη κατάστασης μη αναγνωσμένου δωματίου", "Hide_usernames": "Απόκρυψη ονόματα", "Highlights": "Ανταύγειες", @@ -1530,11 +1528,11 @@ "Lead_capture_email_regex": "Επικεφαλίδα λήψης μηνυμάτων ηλεκτρονικού ταχυδρομείου", "Lead_capture_phone_regex": "Επικεφαλής κύριου τηλεφώνου σύλληψης", "Leave": "Έξοδος από το δωμάτιο", - "Leave_Group_Warning": "Είστε σίγουροι ότι θέλετε να αποχωρήσετε από την ομάδα \"%s\";", - "Leave_Livechat_Warning": "Είστε βέβαιοι ότι θέλετε να αφήσετε το livechat με το \"%s\";", - "Leave_Private_Warning": "Είστε σίγουροι ότι θέλετε να αφήσετε τη συζήτηση με το \"%s\";", + "Leave_Group_Warning": "Είστε σίγουροι ότι θέλετε να αποχωρήσετε από την ομάδα \"{{roomName}}\";", + "Leave_Livechat_Warning": "Είστε βέβαιοι ότι θέλετε να αφήσετε το livechat με το \"{{roomName}}\";", + "Leave_Private_Warning": "Είστε σίγουροι ότι θέλετε να αφήσετε τη συζήτηση με το \"{{roomName}}\";", "Leave_room": "Έξοδος από το δωμάτιο", - "Leave_Room_Warning": "Είστε σίγουροι ότι θέλετε να φύγετε από το δωμάτιο \"%s\";", + "Leave_Room_Warning": "Είστε σίγουροι ότι θέλετε να φύγετε από το δωμάτιο \"{{roomName}}\";", "Leave_the_current_channel": "Αφήστε το τρέχον κανάλι", "leave-c": "Αφήστε τα κανάλια", "leave-p": "Αφήστε ιδιωτικές ομάδες", @@ -2453,7 +2451,6 @@ "Two-factor_authentication": "Έλεγχος ταυτότητας δύο παραγόντων", "Two-factor_authentication_disabled": "Ο έλεγχος ταυτότητας δύο στοιχείων είναι απενεργοποιημένος", "Two-factor_authentication_enabled": "Έχει ενεργοποιηθεί ο έλεγχος ταυτότητας δύο παραγόντων", - "Two-factor_authentication_is_currently_disabled": "Ο έλεγχος ταυτότητας δύο στοιχείων είναι επί του παρόντος απενεργοποιημένος", "Two-factor_authentication_native_mobile_app_warning": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Μόλις το ενεργοποιήσετε, δεν θα μπορείτε να συνδεθείτε στις εφαρμογές κινητής εφαρμογής (Rocket.Chat +) χρησιμοποιώντας τον κωδικό πρόσβασής σας μέχρι να εφαρμόσουν το 2FA.", "Type": "Τύπος", "Type_your_email": "Πληκτρολογήστε το email σας", @@ -2760,4 +2757,4 @@ "registration.component.form.sendConfirmationEmail": "Αποστολή email επιβεβαίωσης", "Enterprise": "Επιχείρηση", "UpgradeToGetMore_engagement-dashboard_Title": "Αναλυτικά στοιχεία" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index eebaae1d817ec..d20ebda4f568f 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -553,6 +553,7 @@ "Application_updated": "Application updated", "Apply": "Apply", "Apply_and_refresh_all_clients": "Apply and refresh all clients", + "Apply_filters": "Apply filters", "Apps": "Apps", "Apps_Engine_Version": "Apps Engine Version", "API_Enable_Rate_Limiter": "Enable Rate Limiter", @@ -562,6 +563,7 @@ "api-bypass-rate-limit_description": "Permission to call api without rate limitation", "APIs": "APIs", "App_Info": "App Info", + "App_Settings_Saved_Successfully": "{{appName}} saved successfully", "Apps_context_enterprise": "Enterprise", "App_has_been_disabled_addon_message_one": "The app {{appNames}} has been disabled because of an invalid add-on. A valid add-on subscription is required to re-enable it", "App_has_been_disabled_addon_message_other": "The apps {{appNames}} have been disabled because of invalid add-ons. A valid add-on subscription is required to re-enable them", @@ -759,6 +761,7 @@ "away": "away", "Away": "Away", "Back": "Back", + "Back_in_history": "Back in history", "Back_to_applications": "Back to applications", "Back_to_chat": "Back to chat", "Back_to_integration_detail": "Back to the integration detail", @@ -766,6 +769,8 @@ "Back_to_integrations": "Back to integrations", "Back_to_login": "Back to login", "Back_to_Manage_Apps": "Back to Manage Apps", + "Back_to__roomName__channel": "Back to {{roomName}} channel", + "Back_to__roomName__team": "Back to {{roomName}} team", "Back_to_permissions": "Back to permissions", "are_playing": "are playing", "is_playing": "is playing", @@ -860,7 +865,6 @@ "Back_to_imports": "Back to imports", "Cancel": "Cancel", "Cancel_message_input": "Cancel", - "Back_to_room": "Back to Room", "Canceled": "Canceled", "Back_to_threads": "Back to threads", "BBB_End_Meeting": "End Meeting", @@ -1093,6 +1097,8 @@ "color": "Color", "changed_room_announcement_to__room_announcement_": "changed room announcement to: {{room_announcement}}", "changed_room_description_to__room_description_": "changed room description to: {{room_description}}", + "Changed_from": "Changed from", + "Changed_to": "Changed to", "Color": "Color", "Colors": "Colors", "change-livechat-room-visitor": "Change Livechat Room Visitors", @@ -1678,8 +1684,9 @@ "Custom_Script_Logged_Out_Description": "Custom Script that will run ALWAYS and to ANY user that is NOT logged in. e.g. (whenever you enter the login page)", "Disable_Notifications": "Disable Notifications", "Custom_Script_On_Logout": "Custom Script for Logout Flow", - "Disable_two-factor_authentication": "Disable two-factor authentication via TOTP", "Custom_Script_On_Logout_Description": "Custom Script that will run on execute logout flow ONLY", + "Two-factor_authentication_required": "Two-factor authentication required", + "Set_up_2FA": "Set up 2FA", "Disabled": "Disabled", "Disallow_reacting": "Disallow Reacting", "Disallow_reacting_Description": "Disallows reacting", @@ -1715,6 +1722,7 @@ "Daily_Active_Users": "Daily Active Users", "Display_unread_counter": "Display room as unread when there are unread messages", "Displays_action_text": "Displays action text", + "Dismiss": "Dismiss", "Data_modified": "Data Modified", "Do_not_display_unread_counter": "Do not display any counter of this channel", "Do_you_want_to_accept": "Do you want to accept?", @@ -1878,7 +1886,6 @@ "Disable": "Disable", "EmojiCustomFilesystem": "Custom Emoji Filesystem", "Empty_title": "Empty title", - "Disable_two-factor_authentication_email": "Disable two-factor authentication via Email", "Enable": "Enable", "Enable_Auto_Away": "Enable Auto Away", "Disabled_apps_admin_message": "There are one or more disabled apps with valid licenses. Go to {{marketplace}} > {{installed}} to review.", @@ -1890,7 +1897,6 @@ "Discussion_info": "Discussion info", "Enable_Svg_Favicon": "Enable SVG favicon", "Discussion_Description": "Discussions are an additional way to organize conversations that allows inviting users from outside channels to participate in specific conversations.", - "Enable_two-factor_authentication": "Enable two-factor authentication via TOTP", "Discussion_first_message_disabled_due_to_e2e": "You can start sending End-to-end encrypted messages in this discussion after its creation.", "Enabled": "Enabled", "Encrypted": "Encrypted", @@ -1917,6 +1923,7 @@ "Enter_a_username": "Enter a username", "Enter_Alternative": "Alternative mode (send with Enter + Ctrl/Alt/Shift/CMD)", "Enter_authentication_code": "Enter authentication code", + "Enter_code_provided_by_authentication_app": "Enter code provided by authentication app", "Documentation": "Documentation", "Enter_Behaviour": "Enter key Behaviour", "Enter_Behaviour_Description": "This changes if the enter key will send a message or do a line break", @@ -2180,7 +2187,8 @@ "Enable_timestamp_description": "Render Unix timestamps inside messages in your local (system) timezone.", "Enable_to_bypass_email_verification": "Enable to bypass email verification", "Facebook_Page": "Facebook Page", - "Enable_two-factor_authentication_email": "Enable two-factor authentication via Email", + "Enable_two-factor_authentication": "Enable two-factor authentication", + "Enable_two-factor_authentication_callout_description": "Two-factor authentication (2FA) is now required for your account on this workspace. 2FA must be setup and enabled before you can proceed with other tasks.", "Enable_unlimited_apps": "Enable unlimited apps", "Enable_voice_calling": "Enable voice calling", "Encrypted_content_cannot_be_searched": "Encrypted content cannot be searched.", @@ -2389,6 +2397,7 @@ "Estimated_due_time": "Estimated due time", "error-password-in-history": "Entered password has been previously used", "Forward": "Forward", + "Forward_in_history": "Forward in history", "Estimated_due_time_in_minutes": "Estimated due time (time in minutes)", "Forward_chat": "Forward chat", "Forward_to_department": "Forward to department", @@ -2485,16 +2494,16 @@ "You_do_not_have_permission_to_execute_this_command": "You do not have enough permissions to execute command: `/{{command}}`", "Hide_flextab": "Hide Contextual Bar by clicking outside of it", "You_have_reached_the_limit_active_costumers_this_month": "You have reached the limit of active customers this month", - "Hide_Group_Warning": "Are you sure you want to hide the group \"%s\"?", - "Hide_Livechat_Warning": "Are you sure you want to hide the chat with \"%s\"?", + "Hide_Group_Warning": "Are you sure you want to hide the group \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Are you sure you want to hide the chat with \"{{roomName}}\"?", "Estimated_wait_time": "Estimated wait time", "Estimated_wait_time_in_minutes": "Estimated wait time (time in minutes)", - "Hide_Private_Warning": "Are you sure you want to hide the discussion with \"%s\"?", + "Hide_Private_Warning": "Are you sure you want to hide the discussion with \"{{roomName}}\"?", "Hide_roles": "Hide Roles", "Event_notifications": "Event notifications", "Event_notifications_description": "By disabling this setting you’ll prevent the app from notifying you of upcoming events.", "Hide_room": "Hide", - "Hide_Room_Warning": "Are you sure you want to hide the channel \"%s\"?", + "Hide_Room_Warning": "Are you sure you want to hide the channel \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Hide Unread Room Status", "Hide_usernames": "Hide Usernames", "every_30_seconds": "Once every 30 seconds", @@ -2503,6 +2512,7 @@ "Highlights_How_To": "To be notified when someone mentions a word or phrase, add it here. You can separate words or phrases with commas. Highlight Words are not case sensitive.", "Highlights_List": "Highlight words", "History": "History", + "History_navigation": "History navigation", "every_12_hours": "Once every 12 hours", "every_24_hours": "Once every 24 hours", "every_48_hours": "Once every 48 hours", @@ -3008,6 +3018,8 @@ "LDAP_Enable_Description": "Attempt to utilize LDAP for authentication.", "LDAP_Encryption": "Encryption", "LDAP_Encryption_Description": "The encryption method used to secure communications to the LDAP server. Examples include `plain` (no encryption), `SSL/LDAPS` (encrypted from the start), and `StartTLS` (upgrade to encrypted communication once connected).", + "LDAP_FederationHomeServer_Field": "Federation Home Server field", + "LDAP_FederationHomeServer_Field_Description": "The Home Server can only be assigned on user creation. Changing this will have no effect on users that were already synced.", "If_you_didnt_try_to_login_in_your_account_please_ignore_this_email": "If you didn't try to login in your account please ignore this email.", "LDAP_Find_User_After_Login": "Find user after login", "LDAP_Find_User_After_Login_Description": "Will perform a search of the user's DN after bind to ensure the bind was successful preventing login with empty passwords when allowed by the AD configuration.", @@ -3105,12 +3117,12 @@ "Install_anyway": "Install anyway", "Update_anyway": "Update anyway", "Leave": "Leave", - "Leave_Group_Warning": "Are you sure you want to leave the group \"%s\"?", - "Leave_Livechat_Warning": "Are you sure you want to leave the omnichannel with \"%s\"?", - "Leave_Private_Warning": "Are you sure you want to leave the discussion with \"%s\"?", + "Leave_Group_Warning": "Are you sure you want to leave the group \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Are you sure you want to leave the omnichannel with \"{{roomName}}\"?", + "Leave_Private_Warning": "Are you sure you want to leave the discussion with \"{{roomName}}\"?", "Installing": "Installing", "Leave_room": "Leave", - "Leave_Room_Warning": "Are you sure you want to leave the channel \"%s\"?", + "Leave_Room_Warning": "Are you sure you want to leave the channel \"{{roomName}}\"?", "Leave_the_current_channel": "Leave the current channel", "leave-c": "Leave Channels", "Instance": "Instance", @@ -3140,6 +3152,7 @@ "Livechat_offline": "Omnichannel offline", "Livechat_offline_message_sent": "Livechat offline message sent", "Integrations_table": "Integrations table", + "Third_party_applications_table": "Third-party applications table", "Livechat_online": "Omnichannel on-line", "Livechat_Queue": "Omnichannel Queue", "Invalid Canned Response": "Invalid Canned Response", @@ -3324,6 +3337,8 @@ "LDAP_UserSearch_GroupFilter": "Group Filter", "LDAP_DataSync": "Data Sync", "LDAP_DataSync_DataMap": "Mapping", + "LDAP_DataSync_UseVariables": "Use Variables", + "LDAP_DataSync_VariableMap": "Variables Configuration", "Members_List": "Members List", "mention-all": "Mention All", "LDAP_DataSync_Avatar": "Avatar", @@ -4144,6 +4159,7 @@ "Microphone": "Microphone", "Microphone_access_not_allowed": "Microphone access was not allowed, please check your browser settings.", "RealName_Change_Disabled": "Your Rocket.Chat administrator has disabled the changing of names", + "Reason": "Reason", "Reason_To_Join": "Reason to Join", "Mic_off": "Mic Off", "Receive_alerts": "Receive alerts", @@ -4153,6 +4169,7 @@ "Missing_configuration": "Missing configuration", "Recent_Import_History": "Recent Import History", "Record": "Record", + "Reconnecting": "Reconnecting", "Mobex_sms_gateway_address": "Mobex SMS Gateway Address", "Mobex_sms_gateway_address_desc": "IP or Host of your Mobex service with specified port. E.g. `http://192.168.1.1:1401` or `https://www.example.com:1401`", "Mobex_sms_gateway_from_number": "From", @@ -4510,7 +4527,7 @@ "Omnichannel_contact_manager_routing": "Assign new conversations to the contact manager", "Scan_QR_code": "Using an authenticator app like Google Authenticator, Authy or Duo, scan the QR code. It will display a 6 digit code which you need to enter below.", "Omnichannel_contact_manager_routing_Description": "This setting allocates a chat to the assigned Contact Manager, as long as the Contact Manager is online when the chat starts", - "Scan_QR_code_alternative_s": "If you can't scan the QR code, you may enter code manually instead:", + "Scan_QR_code_alternative_s": "If you cannot scan the QR code, you may enter the following code manually into the authenticator app instead:", "Omnichannel_External_Frame": "External Frame", "Scope": "Scope", "Omnichannel_External_Frame_Enabled": "External frame enabled", @@ -4670,9 +4687,12 @@ "set-react-when-readonly_description": "Permission to set the ability to react to messages in a read only channel", "set-readonly": "Set ReadOnly", "Pages": "Pages", + "Pages_and_actions": "Pages and actions", "set-readonly_description": "Permission to set a channel to read only channel", "Settings": "Settings", + "Setting": "Setting", "Parent_channel_or_team": "Parent channel or team", + "Setting_change": "Setting change", "Settings_updated": "Settings updated", "Participants": "Participants", "Setup_Wizard": "Setup Wizard", @@ -4910,6 +4930,7 @@ "Queue_delay_timeout": "Queue processing delay timeout", "Queue_Time": "Queue Time", "System_messages": "System Messages", + "System": "System", "Queue_management": "Queue Management", "Tag": "Tag", "Quick_reactions": "Quick reactions", @@ -5213,7 +5234,6 @@ "room_removed_read_only_permission": "removed read only permission", "Two-factor_authentication_enabled": "Two-factor authentication enabled", "room_set_read_only_permission": "set room to read only", - "Two-factor_authentication_is_currently_disabled": "Two-factor authentication via TOTP is currently disabled", "Two-factor_authentication_native_mobile_app_warning": "WARNING: Once you enable this, you will not be able to login on the native mobile apps (Rocket.Chat+) using your password until they implement the 2FA.", "Type": "Type", "Room_updated_successfully": "Room updated successfully!", @@ -5576,6 +5596,7 @@ "Show_mentions": "Show badge for mentions", "Accept_receive_inquiry_no_online_agents": "Allow department to receive forwarded inquiries even when there's no available agents", "Accept_receive_inquiry_no_online_agents_Hint": "This method is effective only with automatic assignment routing methods, and does not apply to Manual Selection.", + "Actor": "Actor", "view-livechat-manager": "View Omnichannel Manager", "Show_Only_This_Content": "Show only this content", "view-livechat-manager_description": "Permission to view other Omnichannel managers", @@ -5916,6 +5937,7 @@ "Timeout_in_miliseconds": "Timeout (in miliseconds)", "Timeout_in_miliseconds_cant_be_negative_number": "Timeout (in miliseconds) can't a negative number", "Timeout_in_miliseconds_hint": "The time in milliseconds to wait for an external service to respond before canceling the request.", + "Timestamp": "Timestamp", "Timezone": "Timezone", "To_prevent_seeing_this_message_again_allow_popups_from_workspace_URL": "To prevent seeing this message again, make sure your browser settings allow pop-ups to be opened from the workspace URL: ", "toggle-room-e2e-encryption": "Toggle Room E2E Encryption", @@ -5960,6 +5982,8 @@ "Troubleshoot_Force_Caching_Version": "Force browsers to clear networking cache based on version change", "Troubleshoot_Force_Caching_Version_Alert": "If the value provided is not empty and different from previous one the browsers will try to clear the cache. This setting should not be set for a long period since it affects the browser performance, please clear it as soon as possible.", "Try_now": "Try now", + "Try_entering_a_different_search_term": "Try entering a different search term.", + "Try_different_filters": "Try different filters", "Try_searching_in_the_marketplace_instead": "Try searching in the Marketplace instead", "Turn_on_video": "Turn on video", "Turn_on_answer_chats": "Turn on answer chats", @@ -5971,7 +5995,6 @@ "Turn_off_video": "Turn off video", "Two-factor_authentication_via_TOTP": "Two-factor authentication via TOTP", "Two-factor_authentication_email": "Two-factor authentication via email", - "Two-factor_authentication_email_is_currently_disabled": "Two-factor authentication via Email is currently disabled", "typing": "typing", "Types": "Types", "Types_and_Distribution": "Types and Distribution", @@ -5997,6 +6020,7 @@ "Unique_ID_change_detected": "Unique ID change detected", "Unknown_Import_State": "Unknown Import State", "Unknown_User": "Unknown User", + "Unknown_contact_callout_description": "Unknown contact. This contact is not on the contact list.", "Unlimited": "Unlimited", "Unmute": "Unmute", "unpinning-not-allowed": "Unpinning is not allowed", @@ -6071,6 +6095,9 @@ "Value_messages": "{{value}} messages", "Value_users": "{{value}} users", "Version_version": "Version {{version}}", + "App": "App", + "App_id": "App Id", + "App_name": "App name", "App_Request_Admin_Message": "Hi {{admin_name}}, {{user_name}} submitted a request to install {{app_name}} app on this workspace. \n \n This is the message they included: \n>{{message}} \n \n To learn more and install the {{app_name}} app, [click here]({{learn_more}}).", "App_version_incompatible_tooltip": "App incompatible with Rocket.Chat version", "App_request_enduser_message": "The app you requested, {{appName}}, has just been installed on this workspace. \n [Click here]({{learnmore}}) to learn about the app.", @@ -6195,6 +6222,7 @@ "Visitor_Name_Placeholder": "Please enter a visitor name...", "Visitor_not_found": "Visitor not found", "Visitor_does_not_exist": "Visitor does not exist!", + "Voice_and_omnichannel": "Voice and omnichannel", "Voice_Call": "Voice Call", "Voice_call": "Voice call", "Voice_call_extension": "Voice call extension", @@ -6248,6 +6276,10 @@ "VoIP_TeamCollab_FreeSwitch_Timeout": "FreeSwitch Request Timeout", "VoIP_TeamCollab_FreeSwitch_WebSocket_Path": "WebSocket Path", "VoIP_TeamCollab_Beta_Alert": "This feature is currently in Beta, please report any issues to Rocket.Chat support", + "VoIP_TeamCollab_Ice_Servers": "Ice Servers", + "VoIP_TeamCollab_Ice_Servers_Description": "A list of Ice Servers (STUN and/or TURN), separated by comma. \n Username, password and port are allowed in the format `username:password@stun:host:port` or `username:password@turn:host:port`. \n Both username and password may be html-encoded.", + "VoIP_TeamCollab_Ice_Gathering_Timeout": "Ice Gathering Timeout", + "VoIP_TeamCollab_Ice_Gathering_Timeout_Description": "Time to wait for Ice Gathering to complete before sending. Low values may prevent Ice Servers from being used, while high values may delay the start of VoIP calls if an invalid Ice Server is specified.", "VoIP_Toggle": "Enable/Disable VoIP", "Chat_opened_by_visitor": "Chat opened by the visitor", "Waiting_for_answer": "Waiting for answer", @@ -6567,6 +6599,7 @@ "Disconnect_workspace": "Disconnect workspace", "Awaiting_confirmation": "Awaiting confirmation", "Security_code": "Security code", + "Security_logs": "Security logs", "Registration_Token": "Registration Token", "RegisterWorkspace_Button": "Register workspace", "ConnectWorkspace_Button": "Connect workspace", @@ -6617,6 +6650,7 @@ "App_will_lose_grandfathered_status": "**This app will lose its app limit policy exemption.** \n \nWorkspaces on Community can have up to {{limit}} apps enabled. Uninstalling this app will cause it to lose its exemption policy.", "App_will_lose_grandfathered_status_private": "**This app will lose its app limit policy exemption.** \n \nBecause Community workspaces cannot enable private apps, this workspace will require a premium plan in order to enable this app again in future.", "All_rooms": "All rooms", + "All_Settings": "All Settings", "All_visible": "All visible", "all": "all", "Filter_by_room": "Filter by room type", @@ -6747,6 +6781,7 @@ "Zoom_out": "Zoom out", "Zoom_in": "Zoom in", "Close_gallery": "Close gallery", + "Close_sidebar": "Close sidebar", "Next_image": "Next Image", "Previous_image": "Previous image", "Image_gallery": "Image gallery", @@ -6756,13 +6791,15 @@ "You_cant_take_chats_offline": "You cannot take new conversations because you're offline", "New_navigation": "Enhanced navigation experience", "New_navigation_description": "Explore our improved navigation, designed with clear scopes for easy access to what you need. This change serves as the foundation for future advancements in navigation management.", - "Workspace_and_user_settings": "Workspace and user settings", + "Workspace_and_user_preferences": "Workspace and user preferences", "Sidebar_Sections_Order": "Sidebar sections order", "Sidebar_Sections_Order_Description": "Select the categories in your preferred order", "Incoming_Calls": "Incoming calls", "Advanced_settings": "Advanced settings", "Security_and_permissions": "Security and permissions", "Security_and_privacy": "Security and privacy", + "Security_Log_App": "App ( {{appId}} )", + "Security_Log_System": "System ( {{reason}} )", "Sidepanel_navigation": "Secondary navigation for teams", "Sidepanel_navigation_description": "Display channels and/or discussions associated with teams by default. This allows team owners to customize communication methods to best meet their team’s needs. This is currently in feature preview and will be a premium capability once fully released.", "Show_channels_description": "Show team channels in second sidebar", @@ -6776,11 +6813,8 @@ "Advanced_contact_profile": "Advanced contact profile", "Advanced_contact_profile_description": "Manage multiple emails and phone numbers for a single contact, enabling a comprehensive multi-channel history that keeps you well-informed and improves communication efficiency.", "Add_contact": "Add contact", - "Add_to_contact_list_manually": "Add to contact list manually", - "Add_to_contact_and_enable_verification_description": "Add to contact list manually and <1>enable verification using multi-factor authentication.", "Ask_enable_advanced_contact_profile": "Ask your workspace admin to enable advanced contact profile", "close-blocked-room-comment": "This channel has been blocked", - "Contact_unknown": "Contact unknown", "Review_contact": "Review contact", "See_conflicts": "See conflicts", "Conflicts_found": "Conflicts found", diff --git a/packages/i18n/src/locales/eo.i18n.json b/packages/i18n/src/locales/eo.i18n.json index 8602c1a38cf99..33a0c100d875b 100644 --- a/packages/i18n/src/locales/eo.i18n.json +++ b/packages/i18n/src/locales/eo.i18n.json @@ -863,7 +863,6 @@ "Directory": "Dosierujo", "Disable_Facebook_integration": "Malŝalti Facebook integriĝo", "Disable_Notifications": "Malebligi Sciigojn", - "Disable_two-factor_authentication": "Malŝalti du-faktoro aŭtentikigo", "Disabled": "Malebligita", "Disallow_reacting": "Malkonsentu Reagi", "Disallow_reacting_Description": "Malsukcesas reagi", @@ -956,7 +955,6 @@ "Enable_Auto_Away": "Ebligu Aŭtoman Away", "Enable_Desktop_Notifications": "Ebligu labortablajn sciigojn", "Enable_Svg_Favicon": "Ebligu SVG-bildsimboleton", - "Enable_two-factor_authentication": "Ebligu du-faktorajn aŭtentikiĝon", "Enabled": "Enabled", "Encrypted_message": "Ĉifrita mesaĝo", "End_OTR": "Pinto OTR", @@ -1218,12 +1216,12 @@ "Hide": "Kaŝi ĉambron", "Hide_counter": "Kaŝi nombrilon", "Hide_flextab": "Kaŝi dekstra flankmenuo per klako", - "Hide_Group_Warning": "Ĉu vi certas, ke vi volas kaŝi la grupon \"%s\"?", - "Hide_Livechat_Warning": "Ĉu vi certas, ke vi volas kaŝi la vivkaptanton kun \"%s\"?", - "Hide_Private_Warning": "Ĉu vi certas, ke vi volas kaŝi la diskuton kun \"%s\"?", + "Hide_Group_Warning": "Ĉu vi certas, ke vi volas kaŝi la grupon \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Ĉu vi certas, ke vi volas kaŝi la vivkaptanton kun \"{{roomName}}\"?", + "Hide_Private_Warning": "Ĉu vi certas, ke vi volas kaŝi la diskuton kun \"{{roomName}}\"?", "Hide_roles": "Kaŝi Rulojn", "Hide_room": "Kaŝi ĉambron", - "Hide_Room_Warning": "Ĉu vi certas, ke vi volas kaŝi la ĉambron \"%s\"?", + "Hide_Room_Warning": "Ĉu vi certas, ke vi volas kaŝi la ĉambron \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Kaŝi Nelegitan Ĉambron", "Hide_usernames": "Kaŝi uzulnomon", "Highlights": "Plej elstaraj", @@ -1523,11 +1521,11 @@ "Lead_capture_email_regex": "Kondukta kapta retpoŝta regex", "Lead_capture_phone_regex": "Plumbo kapti telefonan regex", "Leave": "Lasu ĉambron", - "Leave_Group_Warning": "Ĉu vi certas, ke vi volas forlasi la grupon \"%s\"?", - "Leave_Livechat_Warning": "Ĉu vi certas, ke vi volas lasi la vivkaptanton kun \"%s\"?", - "Leave_Private_Warning": "Ĉu vi certas, ke vi volas lasi la diskuton kun \"%s\"?", + "Leave_Group_Warning": "Ĉu vi certas, ke vi volas forlasi la grupon \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Ĉu vi certas, ke vi volas lasi la vivkaptanton kun \"{{roomName}}\"?", + "Leave_Private_Warning": "Ĉu vi certas, ke vi volas lasi la diskuton kun \"{{roomName}}\"?", "Leave_room": "Lasu ĉambron", - "Leave_Room_Warning": "Ĉu vi certas, ke vi volas lasi la ĉambron \"%s\"?", + "Leave_Room_Warning": "Ĉu vi certas, ke vi volas lasi la ĉambron \"{{roomName}}\"?", "Leave_the_current_channel": "Lasu la nunan kanalon", "leave-c": "Lasi Kanalojn", "leave-p": "Lasi privatajn grupojn", @@ -2445,7 +2443,6 @@ "Two-factor_authentication": "Du-faktora aŭtentigo", "Two-factor_authentication_disabled": "Du-faktora aŭtentigo malŝaltita", "Two-factor_authentication_enabled": "Du faktoro aŭtentigo ebligita", - "Two-factor_authentication_is_currently_disabled": "Du-faktora aŭtentigo estas nuntempe malebligita", "Two-factor_authentication_native_mobile_app_warning": "ADVERTTO: Unufoje vi ebligas tion, vi ne povos ensaluti en la denaskaj poŝtelefonoj (Rocket.Chat +) uzante vian pasvorton ĝis ili efektivigas la 2FA.", "Type": "Tajpu", "Type_your_email": "Tajpu vian retpoŝton", @@ -2754,4 +2751,4 @@ "registration.component.form.sendConfirmationEmail": "Sendu konfirman retpoŝton", "Enterprise": "Entrepreno", "UpgradeToGetMore_engagement-dashboard_Title": "Analitiko" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/es.i18n.json b/packages/i18n/src/locales/es.i18n.json index dbec30c7e692a..19b80353d1938 100644 --- a/packages/i18n/src/locales/es.i18n.json +++ b/packages/i18n/src/locales/es.i18n.json @@ -604,7 +604,6 @@ "Back_to_integrations": "Volver a las integraciones", "Back_to_login": "Volver al inicio de sesión", "Back_to_permissions": "Volver a los permisos", - "Back_to_room": "Volver a Room", "Back_to_threads": "Volver a los hilos", "Backup_codes": "Códigos de respaldo", "Belongs_To": "Pertenece a", @@ -1333,8 +1332,6 @@ "Disable_Facebook_integration": "Deshabilitar integración con Facebook", "Disable_Notifications": "Deshabilitar notificaciones", "Disable_at_least_more_apps": "Tendrás que desactivar al menos {{numberOfExceededApps}} aplicaciones o actualizar a un plan Premium para activar esta aplicación.", - "Disable_two-factor_authentication": "Deshabilitar la autenticación de dos factores vía TOTP", - "Disable_two-factor_authentication_email": "Deshabilitar autenticación de dos factores vía correo electrónico", "Disabled": "Deshabilitada", "Disabled_E2E_Encryption_for_this_room": "El cifrado de punto a punto fue deshabilitado para esta sala", "Disallow_reacting": "No permitir reacciones", @@ -1466,8 +1463,6 @@ "Enable_Svg_Favicon": "Habilitar favicono SVG", "Enable_inquiry_fetch_by_stream": "Habilitar la obtención de datos de consulta desde el servidor mediante una secuencia", "Enable_omnichannel_auto_close_abandoned_rooms": "Habilitar el cierre automático de las salas que haya abandonado el visitante", - "Enable_two-factor_authentication": "Habilitar la autenticación de dos factores vía TOTP", - "Enable_two-factor_authentication_email": "Habilitar autenticación de dos factores vía correo electrónico", "Enabled": "Habilitada", "Enabled_E2E_Encryption_for_this_room": "El cifrado de punto a punto fue habilitado para esta sala", "Encrypted": "Cifrado", @@ -1762,10 +1757,10 @@ "Hi_username": "Hola, [name]", "Hidden": "Oculto", "Hide": "Ocultar", - "Hide_Group_Warning": "¿Seguro que quieres ocultar el grupo \"%s\"?", - "Hide_Livechat_Warning": "¿Seguro que quieres ocultar el chat con \"%s\"?", - "Hide_Private_Warning": "¿Seguro que quieres ocultar la discusión con \"%s\"?", - "Hide_Room_Warning": "¿Seguro que quieres ocultar el canal \"%s\"?", + "Hide_Group_Warning": "¿Seguro que quieres ocultar el grupo \"{{roomName}}\"?", + "Hide_Livechat_Warning": "¿Seguro que quieres ocultar el chat con \"{{roomName}}\"?", + "Hide_Private_Warning": "¿Seguro que quieres ocultar la discusión con \"{{roomName}}\"?", + "Hide_Room_Warning": "¿Seguro que quieres ocultar el canal \"{{roomName}}\"?", "Hide_System_Messages": "Ocultar mensajes de sistema", "Hide_Unread_Room_Status": "Ocultar indicación de que la Room tiene elementos no leídos", "Hide_counter": "Ocultar contador", @@ -2239,10 +2234,10 @@ "Learn_how_to_unlock_the_myriad_possibilities_of_rocket_chat": "Aprenda a desbloquear las innumerables posibilidades de Rocket.Chat.", "Least_recent_updated": "Actualización menos reciente", "Leave": "Salir", - "Leave_Group_Warning": "¿Seguro que quieres salir del grupo \"%s\"?", - "Leave_Livechat_Warning": "¿Seguro que quieres salir de la sala de Omnichannel con \"%s\"?", - "Leave_Private_Warning": "¿Seguro que quieres salir de la discusión con \"%s\"?", - "Leave_Room_Warning": "¿Seguro que quieres salir del canal \"%s\"?", + "Leave_Group_Warning": "¿Seguro que quieres salir del grupo \"{{roomName}}\"?", + "Leave_Livechat_Warning": "¿Seguro que quieres salir de la sala de Omnichannel con \"{{roomName}}\"?", + "Leave_Private_Warning": "¿Seguro que quieres salir de la discusión con \"{{roomName}}\"?", + "Leave_Room_Warning": "¿Seguro que quieres salir del canal \"{{roomName}}\"?", "Leave_a_comment": "Dejar un comentario", "Leave_room": "Salir ", "Leave_the_current_channel": "Salir del canal actual", @@ -3857,9 +3852,7 @@ "Two-factor_authentication": "Autenticación de dos factores vía TOTP", "Two-factor_authentication_disabled": "Autenticación de dos factores deshabilitada", "Two-factor_authentication_email": "Autenticación de dos factores vía correo electrónico", - "Two-factor_authentication_email_is_currently_disabled": "La autenticación de dos factores vía correo electrónico está actualmente deshabilitada", "Two-factor_authentication_enabled": "Autenticación de dos factores habilitada", - "Two-factor_authentication_is_currently_disabled": "La autenticación de dos factores vía TOTP está actualmente deshabilitada", "Two-factor_authentication_native_mobile_app_warning": "ADVERTENCIA: Una vez que hayas habilitado esta función, no podrás iniciar sesión en las aplicaciones móviles nativas (Rocket.Chat+) usando tu contraseña hasta que implementen la 2FA.", "Two-factor_authentication_via_TOTP": "Autenticación de dos factores vía TOTP", "Type": "Tipo", diff --git a/packages/i18n/src/locales/fa.i18n.json b/packages/i18n/src/locales/fa.i18n.json index be20d35ce9719..2d9a0966617a9 100644 --- a/packages/i18n/src/locales/fa.i18n.json +++ b/packages/i18n/src/locales/fa.i18n.json @@ -442,7 +442,6 @@ "Back_to_integrations": "بازگشت به یکپارچگی ها", "Back_to_login": "بازگشت به صفحه ورود", "Back_to_permissions": "بازگشت به مجوزها", - "Back_to_room": "بازگشت به اتاق", "Backup_codes": "کد پشتیبان", "Best_first_response_time": "بهترین زمان پاسخ اول", "Beta_feature_Depends_on_Video_Conference_to_be_enabled": "ویژگی بتا وابسته به کنفرانس ویدیویی فعال می شود", @@ -998,7 +997,6 @@ "Directory": "راهنمای ارتباط", "Disable_Facebook_integration": "غیرفعال کردن ادغام فیس بوک", "Disable_Notifications": "غیر فعال کردن اعلانات", - "Disable_two-factor_authentication": "غیر فعال کردن تایید هویت دومرحله ای", "Disabled": "معلول", "Disallow_reacting": "نادیده گرفتن واکنش", "Disallow_reacting_Description": "واکنش نشان می دهد", @@ -1077,7 +1075,6 @@ "Enable_Desktop_Notifications": "هشدارهای دسکتاپ را فعال کن", "Enable_Svg_Favicon": "فعال کردن فاویکون SVG", "Enable_omnichannel_auto_close_abandoned_rooms": "فعال کردن بستن خودکار اتاق رها شده توسط مشاهده کننده", - "Enable_two-factor_authentication": "فعال کردن تایید هویت دومرحله ای", "Enabled": "فعال شد", "Encrypted_message": "پیام رمز شده", "End_OTR": "پایان مکالمه محرمانه", @@ -1242,10 +1239,10 @@ "Hex_Color_Preview": "پیش نمایش رنگ Hex", "Hidden": "پنهان", "Hide": "پنهان کردن اتاق", - "Hide_Group_Warning": "آیا بابت پنهان کردن گروه \"%s\" مطمئن هستید؟", - "Hide_Livechat_Warning": "آیا مطمئن هستید که میخواهید livechat را با «%s» پنهان کنید؟", - "Hide_Private_Warning": "آیا بابت پنهان کردن بحث با \"%s\" مطمئن هستید؟", - "Hide_Room_Warning": "آیا بابت پنهان کردن اتاق \"%s\" مطمئنید؟", + "Hide_Group_Warning": "آیا بابت پنهان کردن گروه \"{{roomName}}\" مطمئن هستید؟", + "Hide_Livechat_Warning": "آیا مطمئن هستید که میخواهید livechat را با «{{roomName}}» پنهان کنید؟", + "Hide_Private_Warning": "آیا بابت پنهان کردن بحث با \"{{roomName}}\" مطمئن هستید؟", + "Hide_Room_Warning": "آیا بابت پنهان کردن اتاق \"{{roomName}}\" مطمئنید؟", "Hide_Unread_Room_Status": "عدم نمایش وضعیت خوانده نشده برای این اتاق", "Hide_counter": "پنهان کردن شمارنده", "Hide_flextab": "پنهان کردن نوار کناری سمت راست با کلیک کنید", @@ -1557,10 +1554,10 @@ "Lead_capture_email_regex": "سرب گرفتن ایمیل regex", "Lead_capture_phone_regex": "سرب گرفتن مجدد خط تلفن", "Leave": "ترک اتاق", - "Leave_Group_Warning": "آیا واقعا می خواهید گروه \"%s\" را ترک کنید؟", - "Leave_Livechat_Warning": "آیا می خواهید کانال همه‌کاره را با \"%s\" ترک کنید؟", - "Leave_Private_Warning": "آیا واقعا می خواهید بحث با \"%s\" را ترک کنید؟", - "Leave_Room_Warning": "آیا واقعا می خواهید اتاق \"%s\" را ترک کنید؟", + "Leave_Group_Warning": "آیا واقعا می خواهید گروه \"{{roomName}}\" را ترک کنید؟", + "Leave_Livechat_Warning": "آیا می خواهید کانال همه‌کاره را با \"{{roomName}}\" ترک کنید؟", + "Leave_Private_Warning": "آیا واقعا می خواهید بحث با \"{{roomName}}\" را ترک کنید؟", + "Leave_Room_Warning": "آیا واقعا می خواهید اتاق \"{{roomName}}\" را ترک کنید؟", "Leave_room": "ترک اتاق", "Leave_the_current_channel": "کانال فعلی را ترک کنید", "List_of_Channels": "لیست Channelها", @@ -2443,7 +2440,6 @@ "Two-factor_authentication": "تایید هویت دومرحله ای", "Two-factor_authentication_disabled": "تایید هویت دومرحله ای غیر فعال است", "Two-factor_authentication_enabled": "تایید هویت دومرحله ای فعال است", - "Two-factor_authentication_is_currently_disabled": "تایید هویت دومرحله ای فعلا غیر فعال است", "Two-factor_authentication_native_mobile_app_warning": "هشدار: وقتی این را فعال کنید دیگر قادر به ورود از طریق برنامه های موبایل نخواهید بود.", "Two-factor_authentication_via_TOTP": "تایید هویت دومرحله ای", "Type": "نوع", diff --git a/packages/i18n/src/locales/fi.i18n.json b/packages/i18n/src/locales/fi.i18n.json index 7b010ea0fd581..f710745d3e56a 100644 --- a/packages/i18n/src/locales/fi.i18n.json +++ b/packages/i18n/src/locales/fi.i18n.json @@ -759,7 +759,6 @@ "Back_to_imports": "Takaisin tuotuihin", "Cancel": "Peruuta", "Cancel_message_input": "Peruuta", - "Back_to_room": "Takaisin huoneeseen", "Canceled": "Peruutettu", "Back_to_threads": "Takaisin viestiketjuihin", "BBB_End_Meeting": "Lopeta kokous", @@ -1515,7 +1514,6 @@ "Custom_Script_Logged_Out_Description": "Mukautettu komentosarja, joka suoritetaan AINA ja KAIKILLE käyttäjille, jotka EIVÄT ole kirjautuneet sisään (esim. aina kun saavut kirjautumissivulle)", "Disable_Notifications": "Poista käytöstä ilmoitukset", "Custom_Script_On_Logout": "Mukautettu komentosarja uloskirjautumista varten", - "Disable_two-factor_authentication": "Poista käytöstä kaksivaiheinen tunnistautuminen TOTP:n kautta ", "Custom_Script_On_Logout_Description": "Mukautettu komentosarja, joka suoritetaan AINOASTAAN uloskirjautumisen yhteydessä", "Disabled": "Ei käytössä", "Disallow_reacting": "Estä reagointi", @@ -1701,7 +1699,6 @@ "Disable": "Poista käytöstä", "EmojiCustomFilesystem": "Mukautettujen emojien tiedostojärjestelmä", "Empty_title": "Tyhjä otsikko", - "Disable_two-factor_authentication_email": "Poista käytöstä kaksivaiheinen tunnistautuminen sähköpostitse", "Enable": "Ota käyttöön", "Enable_Auto_Away": "Ota käyttöön automaattinen Poissa-tila", "Enable_Desktop_Notifications": "Ota käyttöön työpöytäilmoitukset", @@ -1710,7 +1707,6 @@ "Discussion": "Keskustelu", "Enable_Svg_Favicon": "Ota käyttöön SVG-favicon", "Discussion_Description": "Keskustelut on lisätapa järjestää keskusteluja. Sillä voidaan kutsua ulkopuolisia käyttäjiä osallistumaan tiettyihin keskusteluihin.", - "Enable_two-factor_authentication": "Ota käyttöön kaksivaiheinen tunnistautuminen TOTP:n kautta", "Discussion_first_message_disabled_due_to_e2e": "Voit aloittaa täysin salattujen viestien lähettämisen tässä keskustelussa sen luomisen jälkeen.", "Enabled": "Käytössä", "Encrypted": "Salattu", @@ -1949,7 +1945,6 @@ "External_Queue_Service_URL": "Ulkoisen jonopalvelun URL-osoite", "External_Service": "Ulkoinen palvelu", "Facebook_Page": "Facebook-sivu", - "Enable_two-factor_authentication_email": "Ota käyttöön kaksivaiheinen tunnistautuminen sähköpostitse", "Enable_unlimited_apps": "Ota käyttöön rajattomasti sovelluksia", "Encrypted_not_available": "Ei saatavilla ylesellä kanavalla Channel", "False": "Epätosi", @@ -2194,14 +2189,14 @@ "You_do_not_have_permission_to_do_this": "Sinulla ei ole oikeutta tähän", "Hide_counter": "Piilota laskuri", "Hide_flextab": "Piilota oikea sivupalkki napsauttamalla", - "Hide_Group_Warning": "Haluatko varmasti piilottaa ryhmän \"%s\"?", - "Hide_Livechat_Warning": "Haluatko varmasti piilottaa keskustelun käyttäjän \"%s\" kanssa?", + "Hide_Group_Warning": "Haluatko varmasti piilottaa ryhmän \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Haluatko varmasti piilottaa keskustelun käyttäjän \"{{roomName}}\" kanssa?", "Estimated_wait_time": "Arvioitu odotusaika", "Estimated_wait_time_in_minutes": "Arvioitu odotusaika (minuutteina)", - "Hide_Private_Warning": "Haluatko varmasti piilottaa keskustelun käyttäjän \"%s\" kanssa?", + "Hide_Private_Warning": "Haluatko varmasti piilottaa keskustelun käyttäjän \"{{roomName}}\" kanssa?", "Hide_roles": "Piilota roolit", "Hide_room": "Piilota", - "Hide_Room_Warning": "Haluatko varmasti piilottaa kanavan \"%s\"?", + "Hide_Room_Warning": "Haluatko varmasti piilottaa kanavan \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Piilota lukemattoman Room huoneen tila", "Hide_usernames": "Piilota käyttäjätunnukset", "Highlights": "Kohokohdat", @@ -2733,11 +2728,11 @@ "Inline_code": "Sisäinen koodi", "Install_anyway": "Asenna silti", "Leave": "Poistu", - "Leave_Group_Warning": "Haluatko varmasti poistua ryhmästä \"%s\"?", - "Leave_Livechat_Warning": "Haluatko varmasti poistua monikanavalta käyttäjän \"%s\" kanssa?", - "Leave_Private_Warning": "Haluatko varmasti poistua poistua keskustelusta käyttäjän \"%s\" kanssa?", + "Leave_Group_Warning": "Haluatko varmasti poistua ryhmästä \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Haluatko varmasti poistua monikanavalta käyttäjän \"{{roomName}}\" kanssa?", + "Leave_Private_Warning": "Haluatko varmasti poistua poistua keskustelusta käyttäjän \"{{roomName}}\" kanssa?", "Leave_room": "Poistu", - "Leave_Room_Warning": "Haluatko varmasti poistua kanavalta \"%s\"?", + "Leave_Room_Warning": "Haluatko varmasti poistua kanavalta \"{{roomName}}\"?", "Leave_the_current_channel": "Poistu nykyiseltä kanavalta", "leave-c": "Jätä kanavat Channel", "Instance": "Esiintymä", @@ -4531,7 +4526,6 @@ "room_removed_read_only_permission": "poisti vain lukuoikeuden", "Two-factor_authentication_enabled": "Kaksivaiheinen todennus käytössä", "room_set_read_only_permission": "asetti huoneen vain luku-tilaan", - "Two-factor_authentication_is_currently_disabled": "Kaksivaiheinen todennus on poissa käytöstä", "Two-factor_authentication_native_mobile_app_warning": "VAROITUS: jos otat tämän käyttöön, voit kirjautua mobiilisovelluksiin (Rocket.Chat+) salasanalla vasta, kun sovellukset ottavat 2FA-todennuksen käyttöön.", "Type": "Tyyppi", "Room_updated_successfully": "Huone on päivitetty!", @@ -5200,7 +5194,6 @@ "Turn_off_video": "Kytke video pois päältä", "Two-factor_authentication_via_TOTP": "Kaksivaiheinen tunnistautuminen", "Two-factor_authentication_email": "Kaksivaiheinen tunnistautuminen sähköpostitse", - "Two-factor_authentication_email_is_currently_disabled": "Sähköpostin kautta tapahtuva kaksivaiheinen tunnistautuminen on tällä hetkellä poistettu käytöstä", "typing": "kirjoittaa", "Types": "Tyypit", "Types_and_Distribution": "Tyypit ja jakelu", @@ -5720,4 +5713,4 @@ "Theme_Appearence": "Teeman ulkoasu", "Enterprise": "Yritys", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/fr.i18n.json b/packages/i18n/src/locales/fr.i18n.json index 249ea998cbf09..4e13ca67e4403 100644 --- a/packages/i18n/src/locales/fr.i18n.json +++ b/packages/i18n/src/locales/fr.i18n.json @@ -598,7 +598,6 @@ "Back_to_integrations": "Retour aux intégrations", "Back_to_login": "Retour à l'écran de connexion", "Back_to_permissions": "Retour aux autorisations", - "Back_to_room": "Retour au salon", "Back_to_threads": "Retour aux fils", "Backup_codes": "Codes de sauvegarde", "Be_the_first_to_join": "Soyez le premier à rejoindre", @@ -1316,8 +1315,6 @@ "Disable": "Désactiver", "Disable_Facebook_integration": "Désactiver l'intégration Facebook", "Disable_Notifications": "Désactiver les notifications", - "Disable_two-factor_authentication": "Désactiver l'authentification à deux facteurs par TOTP", - "Disable_two-factor_authentication_email": "Désactiver l'authentification à deux facteurs par e-mail", "Disabled": "Désactivé", "Disallow_reacting": "Interdire la réaction", "Disallow_reacting_Description": "Interdit de réagir", @@ -1447,8 +1444,6 @@ "Enable_Svg_Favicon": "Activer l'icône de site SVG", "Enable_inquiry_fetch_by_stream": "Activer la récupération des données de demande à partir du serveur à l'aide d'un flux", "Enable_omnichannel_auto_close_abandoned_rooms": "Activer la fermeture automatique des salons abandonnés par le visiteur", - "Enable_two-factor_authentication": "Activer l'authentification à deux facteurs via TOTP", - "Enable_two-factor_authentication_email": "Activer l'authentification à deux facteurs par e-mail", "Enabled": "Activé", "Encrypted": "Chiffré", "Encrypted_channel_Description": "Canal chiffré de bout en bout. La recherche ne fonctionne pas avec les canaux chiffrés et les notifications peuvent ne pas afficher le contenu des messages.", @@ -1738,10 +1733,10 @@ "Hi_username": "Bonjour [name]", "Hidden": "Caché", "Hide": "Masquer", - "Hide_Group_Warning": "Voulez-vous vraiment masquer le groupe \"%s\" ?", - "Hide_Livechat_Warning": "Voulez-vous vraiment masquer le chat avec \"%s\"?", - "Hide_Private_Warning": "Voulez-vous vraiment masquer la discussion avec \"%s\" ?", - "Hide_Room_Warning": "Voulez-vous vraiment masquer le canal \"%s\" ?", + "Hide_Group_Warning": "Voulez-vous vraiment masquer le groupe \"{{roomName}}\" ?", + "Hide_Livechat_Warning": "Voulez-vous vraiment masquer le chat avec \"{{roomName}}\"?", + "Hide_Private_Warning": "Voulez-vous vraiment masquer la discussion avec \"{{roomName}}\" ?", + "Hide_Room_Warning": "Voulez-vous vraiment masquer le canal \"{{roomName}}\" ?", "Hide_System_Messages": "Masquer les messages système", "Hide_Unread_Room_Status": "Masquer le statut non lu du salon", "Hide_counter": "Masquer le compteur", @@ -2210,10 +2205,10 @@ "Lead_capture_phone_regex": "Expression régulière de numéro de téléphone pour la capture de piste", "Least_recent_updated": "Mise à jour la moins récente", "Leave": "Quitter", - "Leave_Group_Warning": "Voulez-vous vraiment quitter le groupe \"%s\" ?", - "Leave_Livechat_Warning": "Voulez-vous vraiment quitter l'omnicanal avec \"%s\" ?", - "Leave_Private_Warning": "Voulez-vous vraiment quitter la discussion avec \"%s\" ?", - "Leave_Room_Warning": "Voulez-vous vraiment quitter le canal \"%s\" ?", + "Leave_Group_Warning": "Voulez-vous vraiment quitter le groupe \"{{roomName}}\" ?", + "Leave_Livechat_Warning": "Voulez-vous vraiment quitter l'omnicanal avec \"{{roomName}}\" ?", + "Leave_Private_Warning": "Voulez-vous vraiment quitter la discussion avec \"{{roomName}}\" ?", + "Leave_Room_Warning": "Voulez-vous vraiment quitter le canal \"{{roomName}}\" ?", "Leave_a_comment": "Laisser un commentaire", "Leave_room": "Quitter", "Leave_the_current_channel": "Quitter le canal actuel", @@ -3760,9 +3755,7 @@ "Two-factor_authentication": "Authentification à deux facteurs via TOTP", "Two-factor_authentication_disabled": "Authentification à deux facteurs désactivée", "Two-factor_authentication_email": "Authentification à deux facteurs par e-mail", - "Two-factor_authentication_email_is_currently_disabled": "L'authentification à 2 facteurs par e-mail est actuellement désactivée", "Two-factor_authentication_enabled": "Authentification à deux facteurs activée", - "Two-factor_authentication_is_currently_disabled": "L'authentification à deux facteurs via TOTP est actuellement désactivée", "Two-factor_authentication_native_mobile_app_warning": "ATTENTION : Une fois cette option activée, vous ne pourrez pas vous connecter aux applications mobiles natives (Rocket.Chat+) en utilisant votre mot de passe tant que l'authentification à 2 facteurs ne sera pas implémentée.", "Two-factor_authentication_via_TOTP": "Authentification à deux facteurs via TOTP", "Type": "Type", diff --git a/packages/i18n/src/locales/he.i18n.json b/packages/i18n/src/locales/he.i18n.json index 2097e7dfdcf42..c01d4496cfbed 100644 --- a/packages/i18n/src/locales/he.i18n.json +++ b/packages/i18n/src/locales/he.i18n.json @@ -629,11 +629,11 @@ "Hide": "להסתיר את החדר", "Hide_counter": "הסתר את המונה", "Hide_flextab": "הסתרת תפריט ימני בלחיצה", - "Hide_Group_Warning": "האם אתה בטוח שאתה מעוניין להסתיר את הקבוצה \"%s\"?", - "Hide_Private_Warning": "האם אתה בטוח שאתה רוצה להסתיר את השיחה עם \"%s\"?", + "Hide_Group_Warning": "האם אתה בטוח שאתה מעוניין להסתיר את הקבוצה \"{{roomName}}\"?", + "Hide_Private_Warning": "האם אתה בטוח שאתה רוצה להסתיר את השיחה עם \"{{roomName}}\"?", "Hide_roles": "הסתר תפקידים", "Hide_room": "להסתיר את החדר", - "Hide_Room_Warning": "האם אתה בטוח שאתה רוצה להסתיר את חדר \"%s\"?", + "Hide_Room_Warning": "האם אתה בטוח שאתה רוצה להסתיר את חדר \"{{roomName}}\"?", "Hide_usernames": "הסתרת שמות משתמשים", "Highlights": "עיקרי הדברים", "Highlights_How_To": "כדי לקבל הודעה כאשר מישהו מזכיר את המילה או הביטוי, להוסיף אותו כאן. ניתן להפריד מילים או ביטויים עם פסיקים. מילות דגש אינן תלויות-רישיות.", @@ -788,10 +788,10 @@ "LDAP_Username_Field": "שדה שם המשתמש", "LDAP_Username_Field_Description": "איזה שדה ישמש * שם משתמש * עבור משתמשים חדשים. השאר ריק להשתמש בשם המשתמש הודיע ​​על דף הכניסה. \n אתה יכול להשתמש בתגי תבנית מדי, כמו `#{givenName}.#{sn}`. \n ערך ברירת המחדל הוא `sAMAccountName`.", "Leave": "לעזוב את החדר", - "Leave_Group_Warning": "האם אתה בטוח שאתה רוצה לעזוב את הקבוצה \"%s\"?", - "Leave_Private_Warning": "האם אתה בטוח שאתה רוצה לעזוב את השיחה עם \"%s\"?", + "Leave_Group_Warning": "האם אתה בטוח שאתה רוצה לעזוב את הקבוצה \"{{roomName}}\"?", + "Leave_Private_Warning": "האם אתה בטוח שאתה רוצה לעזוב את השיחה עם \"{{roomName}}\"?", "Leave_room": "לעזוב את החדר", - "Leave_Room_Warning": "אתה בטוח שאתה מעוניין לעזוב את החדר \"%s\"", + "Leave_Room_Warning": "אתה בטוח שאתה מעוניין לעזוב את החדר \"{{roomName}}\"", "Leave_the_current_channel": "יציאה מהערוץ הנוכחי", "leave-p": "עזוב קבוצות פרטיות", "List_of_Channels": "רשימה של ערוצים", diff --git a/packages/i18n/src/locales/hi-IN.i18n.json b/packages/i18n/src/locales/hi-IN.i18n.json index 58f64802d9ee6..393ad6aa5d2ca 100644 --- a/packages/i18n/src/locales/hi-IN.i18n.json +++ b/packages/i18n/src/locales/hi-IN.i18n.json @@ -785,7 +785,6 @@ "Back_to_imports": "आयात पर वापस जाएँ", "Cancel": "रद्द करना", "Cancel_message_input": "रद्द करना", - "Back_to_room": "कक्ष में वापस", "Canceled": "रद्द", "Back_to_threads": "धागों पर वापस जाएँ", "BBB_End_Meeting": "बैठक समाप्त", @@ -1564,7 +1563,6 @@ "Custom_Script_Logged_Out_Description": "कस्टम स्क्रिप्ट जो हमेशा चलेगी और किसी भी उपयोगकर्ता के लिए जो लॉग इन नहीं है। (जब भी आप लॉगिन पेज दर्ज करें)", "Disable_Notifications": "नोटीफिकेशन निष्क्रिय किया गया", "Custom_Script_On_Logout": "लॉगआउट फ़्लो के लिए कस्टम स्क्रिप्ट", - "Disable_two-factor_authentication": "TOTP के माध्यम से दो-कारक प्रमाणीकरण अक्षम करें", "Custom_Script_On_Logout_Description": "कस्टम स्क्रिप्ट जो केवल निष्पादन लॉगआउट प्रवाह पर चलेगी", "Disabled": "उपयोग करने की अनुमति नहीं है", "Disallow_reacting": "प्रतिक्रिया करने की अनुमति न दें", @@ -1754,7 +1752,6 @@ "Disable": "अक्षम करना", "EmojiCustomFilesystem": "कस्टम इमोजी फ़ाइल सिस्टम", "Empty_title": "ख़ाली शीर्षक", - "Disable_two-factor_authentication_email": "ईमेल के माध्यम से दो-कारक प्रमाणीकरण अक्षम करें", "Enable": "सक्षम करें", "Enable_Auto_Away": "ऑटो अवे सक्षम करें", "Enable_Desktop_Notifications": "डेस्कटॉप सूचनाएं सक्षम करें", @@ -1763,7 +1760,6 @@ "Discussion": "बहस", "Enable_Svg_Favicon": "एसवीजी फ़ेविकॉन सक्षम करें", "Discussion_Description": "चर्चाएँ वार्तालापों को व्यवस्थित करने का एक अतिरिक्त तरीका है जो बाहरी चैनलों के उपयोगकर्ताओं को विशिष्ट वार्तालापों में भाग लेने के लिए आमंत्रित करने की अनुमति देता है।", - "Enable_two-factor_authentication": "TOTP के माध्यम से दो-कारक प्रमाणीकरण सक्षम करें", "Discussion_first_message_disabled_due_to_e2e": "आप इसके निर्माण के बाद इस चर्चा में एंड-टू-एंड एन्क्रिप्टेड संदेश भेजना शुरू कर सकते हैं।", "Enabled": "सक्रिय", "Encrypted": "कूट रूप दिया गया", @@ -1997,7 +1993,6 @@ "External_Queue_Service_URL": "बाहरी कतार सेवा यूआरएल", "External_Service": "बाह्य सेवा", "Facebook_Page": "फेसबुक पेज", - "Enable_two-factor_authentication_email": "ईमेल के माध्यम से दो-कारक प्रमाणीकरण सक्षम करें", "Enable_unlimited_apps": "असीमित ऐप्स सक्षम करें", "Encrypted_not_available": "सार्वजनिक चैनलों के लिए उपलब्ध नहीं है", "False": "असत्य", @@ -2267,16 +2262,16 @@ "You_do_not_have_permission_to_execute_this_command": "आपके पास कमांड निष्पादित करने के लिए पर्याप्त अनुमतियाँ नहीं हैं: `/{{command}}`", "Hide_flextab": "प्रासंगिक बार के बाहर क्लिक करके उसे छिपाएँ", "You_have_reached_the_limit_active_costumers_this_month": "आप इस महीने सक्रिय ग्राहकों की सीमा तक पहुंच गए हैं", - "Hide_Group_Warning": "क्या आप वाकई समूह \"%s\" को छिपाना चाहते हैं?", - "Hide_Livechat_Warning": "क्या आप वाकई \"%s\" के साथ चैट छिपाना चाहते हैं?", + "Hide_Group_Warning": "क्या आप वाकई समूह \"{{roomName}}\" को छिपाना चाहते हैं?", + "Hide_Livechat_Warning": "क्या आप वाकई \"{{roomName}}\" के साथ चैट छिपाना चाहते हैं?", "Estimated_wait_time": "अनुमानित प्रतीक्षा समय", "Estimated_wait_time_in_minutes": "अनुमानित प्रतीक्षा समय (मिनटों में समय)", - "Hide_Private_Warning": "क्या आप वाकई \"%s\" के साथ चर्चा छिपाना चाहते हैं?", + "Hide_Private_Warning": "क्या आप वाकई \"{{roomName}}\" के साथ चर्चा छिपाना चाहते हैं?", "Hide_roles": "भूमिकाएँ छिपाएँ", "Event_notifications": "घटना सूचनाएं", "Event_notifications_description": "इस सेटिंग को अक्षम करके आप ऐप को आगामी घटनाओं के बारे में सूचित करने से रोकेंगे।", "Hide_room": "छिपाना", - "Hide_Room_Warning": "क्या आप वाकई चैनल \"%s\" को छिपाना चाहते हैं?", + "Hide_Room_Warning": "क्या आप वाकई चैनल \"{{roomName}}\" को छिपाना चाहते हैं?", "Hide_Unread_Room_Status": "अपठित कक्ष की स्थिति छिपाएँ", "Hide_usernames": "उपयोक्तानाम छिपाएँ", "every_30_seconds": "हर 30 सेकंड में एक बार", @@ -2835,12 +2830,12 @@ "Inline_code": "इनलाइन कोड", "Install_anyway": "फिर भी इंस्टॉल करें", "Leave": "छुट्टी", - "Leave_Group_Warning": "क्या आप वाकई समूह \"%s\" छोड़ना चाहते हैं?", - "Leave_Livechat_Warning": "क्या आप वाकई \"%s\" के साथ ओमनीचैनल छोड़ना चाहते हैं?", - "Leave_Private_Warning": "क्या आप वाकई \"%s\" के साथ चर्चा छोड़ना चाहते हैं?", + "Leave_Group_Warning": "क्या आप वाकई समूह \"{{roomName}}\" छोड़ना चाहते हैं?", + "Leave_Livechat_Warning": "क्या आप वाकई \"{{roomName}}\" के साथ ओमनीचैनल छोड़ना चाहते हैं?", + "Leave_Private_Warning": "क्या आप वाकई \"{{roomName}}\" के साथ चर्चा छोड़ना चाहते हैं?", "Installing": "स्थापित कर रहा है", "Leave_room": "छुट्टी", - "Leave_Room_Warning": "क्या आप वाकई चैनल \"%s\" छोड़ना चाहते हैं?", + "Leave_Room_Warning": "क्या आप वाकई चैनल \"{{roomName}}\" छोड़ना चाहते हैं?", "Leave_the_current_channel": "वर्तमान चैनल छोड़ें", "leave-c": "चैनल छोड़ें", "Instance": "उदाहरण", @@ -4760,7 +4755,6 @@ "room_removed_read_only_permission": "केवल पढ़ने की अनुमति हटा दी गई", "Two-factor_authentication_enabled": "दो-कारक प्रमाणीकरण सक्षम किया गया", "room_set_read_only_permission": "केवल पढ़ने के लिए कमरा निर्धारित करें", - "Two-factor_authentication_is_currently_disabled": "टीओटीपी के माध्यम से दो-कारक प्रमाणीकरण वर्तमान में अक्षम है", "Two-factor_authentication_native_mobile_app_warning": "चेतावनी: एक बार जब आप इसे सक्षम कर लेते हैं, तो आप अपने पासवर्ड का उपयोग करके मूल मोबाइल ऐप्स (रॉकेट.चैट+) पर तब तक लॉगिन नहीं कर पाएंगे जब तक वे 2FA लागू नहीं कर देते।", "Type": "प्रकार", "Room_updated_successfully": "कमरा सफलतापूर्वक अपडेट किया गया!", @@ -5465,7 +5459,6 @@ "Turn_off_answer_calls": "उत्तर कॉल बंद करें", "Turn_off_video": "वीडियो बंद करें", "Two-factor_authentication_email": "ईमेल के माध्यम से दो-कारक प्रमाणीकरण", - "Two-factor_authentication_email_is_currently_disabled": "ईमेल के माध्यम से दो-कारक प्रमाणीकरण वर्तमान में अक्षम है", "typing": "टाइपिंग", "Types": "प्रकार", "Types_and_Distribution": "प्रकार और वितरण", @@ -6094,4 +6087,4 @@ "Unlimited_seats": "असीमित सीटें", "Unlimited_MACs": "असीमित एमएसी", "Unlimited_seats_MACs": "असीमित सीटें और एमएसी" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/hr.i18n.json b/packages/i18n/src/locales/hr.i18n.json index a8fdf2905949e..6d8c773a7afd2 100644 --- a/packages/i18n/src/locales/hr.i18n.json +++ b/packages/i18n/src/locales/hr.i18n.json @@ -974,7 +974,6 @@ "Directory": "Imenik", "Disable_Facebook_integration": "Onemogućivanje integracije Facebooka", "Disable_Notifications": "Onemogućivanje obavijesti", - "Disable_two-factor_authentication": "Onemogućivanje provjere autentičnosti s dva faktora", "Disabled": "onesposobljen", "Disallow_reacting": "Ne dopusti reagiranje", "Disallow_reacting_Description": "Ne dopušta reagiranje", @@ -1084,7 +1083,6 @@ "Enable_Auto_Away": "Omogućite automatsko odjavljivanje", "Enable_Desktop_Notifications": "Omogući obavijesti na radnoj površini", "Enable_Svg_Favicon": "Omogući SVG favicon", - "Enable_two-factor_authentication": "Omogući autentifikaciju s dva faktora", "Enabled": "Omogućeno", "Encrypted": "Kodirano", "Encrypted_message": "Zaštićena poruka", @@ -1347,12 +1345,12 @@ "Hide": "Sakrij sobu", "Hide_counter": "Sakrij brojač", "Hide_flextab": "Sakrij desni izbornik klikom", - "Hide_Group_Warning": "Jeste li sigurni da želite sakriti grupu \"%s\"?", - "Hide_Livechat_Warning": "Jeste li sigurni da želite sakriti livechat s \"%s\"?", - "Hide_Private_Warning": "Jeste li sigurni da želite sakriti raspravu s \"%s\"?", + "Hide_Group_Warning": "Jeste li sigurni da želite sakriti grupu \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Jeste li sigurni da želite sakriti livechat s \"{{roomName}}\"?", + "Hide_Private_Warning": "Jeste li sigurni da želite sakriti raspravu s \"{{roomName}}\"?", "Hide_roles": "Sakrij uloge", "Hide_room": "Sakrij sobu", - "Hide_Room_Warning": "Jeste li sigurni da želite sakriti sobu \"%s\"?", + "Hide_Room_Warning": "Jeste li sigurni da želite sakriti sobu \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Sakrij status nepročitane sobe", "Hide_usernames": "Sakrij korisnička imena", "Highlights": "Istaknuto", @@ -1653,11 +1651,11 @@ "Lead_capture_email_regex": "Olovo za hvatanje e-pošte regex", "Lead_capture_phone_regex": "Olovo za hvatanje regex telefona", "Leave": "Izađi iz sobe", - "Leave_Group_Warning": "Jeste li sigurni da želite napustiti grupu \"%s\"?", - "Leave_Livechat_Warning": "Jeste li sigurni da želite napustiti livechat s \"%s\"?", - "Leave_Private_Warning": "Jeste li sigurni da želite napustiti razgovor s \"%s\"?", + "Leave_Group_Warning": "Jeste li sigurni da želite napustiti grupu \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Jeste li sigurni da želite napustiti livechat s \"{{roomName}}\"?", + "Leave_Private_Warning": "Jeste li sigurni da želite napustiti razgovor s \"{{roomName}}\"?", "Leave_room": "Izađi iz sobe", - "Leave_Room_Warning": "Jeste li sigurni da želite izaći iz sobe \"%s\"?", + "Leave_Room_Warning": "Jeste li sigurni da želite izaći iz sobe \"{{roomName}}\"?", "Leave_the_current_channel": "Napusti trenutnu sobu", "leave-c": "Ostavite kanale", "leave-p": "Napusti privatne grupe", @@ -2576,7 +2574,6 @@ "Two-factor_authentication": "Provjera autentičnosti s dva faktora", "Two-factor_authentication_disabled": "Autentifikacija s dva faktora je onemogućena", "Two-factor_authentication_enabled": "Omogućena je autentifikacija s dva faktora", - "Two-factor_authentication_is_currently_disabled": "Trenutačno je onemogućena autentikacija s dva faktora", "Two-factor_authentication_native_mobile_app_warning": "UPOZORENJE: nakon što omogućite ovo, nećete se moći prijaviti na izvorne mobilne aplikacije (Rocket.Chat +) pomoću svoje lozinke dok ne implementirate 2FA.", "Type": "Vrsta", "Type_your_email": "Upišite Vaš e-mail", @@ -2886,4 +2883,4 @@ "registration.component.form.sendConfirmationEmail": "Pošalji potvrdni email", "Enterprise": "Poduzeće", "UpgradeToGetMore_engagement-dashboard_Title": "Analitika" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/hu.i18n.json b/packages/i18n/src/locales/hu.i18n.json index 8d2d483169192..669ce3dba1b31 100644 --- a/packages/i18n/src/locales/hu.i18n.json +++ b/packages/i18n/src/locales/hu.i18n.json @@ -730,7 +730,6 @@ "Back_to_imports": "Vissza az importálásokhoz", "Cancel": "Mégse", "Cancel_message_input": "Mégse", - "Back_to_room": "Vissza a szobához", "Canceled": "Megszakítva", "Back_to_threads": "Vissza a szálakhoz", "BBB_End_Meeting": "Értekezlet befejezése", @@ -1473,7 +1472,6 @@ "Custom_Script_Logged_Out_Description": "Egyéni parancsfájl, amely MINDIG lefut az ÖSSZES olyan felhasználónál, akik NINCSENEK bejelentkezve (például amikor megnyitja a bejelentkezési oldalt)", "Disable_Notifications": "Értesítések letiltása", "Custom_Script_On_Logout": "Egyéni parancsfájl a kijelentkezési folyamathoz", - "Disable_two-factor_authentication": "TOTP-n keresztüli kétfaktoros hitelesítés letiltása", "Custom_Script_On_Logout_Description": "Egyéni parancsfájl, amely CSAK a kijelentkezési folyamat végrehajtásakor fog lefutni", "Disabled": "Letiltva", "Disallow_reacting": "Reagálás letiltása", @@ -1653,7 +1651,6 @@ "Disable": "Letiltás", "EmojiCustomFilesystem": "Egyéni emodzsi fájlrendszere", "Empty_title": "Üres cím", - "Disable_two-factor_authentication_email": "E-mailen keresztüli kétfaktoros hitelesítés letiltása", "Enable": "Engedélyezés", "Enable_Auto_Away": "Automatikus távollét engedélyezése", "Enable_Desktop_Notifications": "Asztali értesítések engedélyezése", @@ -1662,7 +1659,6 @@ "Discussion": "Megbeszélés", "Enable_Svg_Favicon": "SVG böngészőikon engedélyezése", "Discussion_Description": "A megbeszélések egy további módja a beszélgetések szervezésének, amely lehetővé teszi a felhasználók meghívását külső csatornákról, hogy részt vegyenek bizonyos beszélgetésekben.", - "Enable_two-factor_authentication": "TOTP-n keresztüli kétfaktoros hitelesítés engedélyezése", "Discussion_first_message_disabled_due_to_e2e": "A végpontok között titkosított üzenetek küldését ebben a megbeszélésben a létrehozását követően kezdheti el.", "Enabled": "Engedélyezve", "Encrypted": "Titkosítva", @@ -1895,7 +1891,6 @@ "External_Queue_Service_URL": "Külső várólista szolgáltatásának URL-je", "External_Service": "Külső szolgáltatás", "Facebook_Page": "Facebook-oldal", - "Enable_two-factor_authentication_email": "E-mailen keresztüli kétfaktoros hitelesítés engedélyezése", "Encrypted_not_available": "Nem érhető el nyilvános csatornáknál", "False": "Hamis", "End": "Befejezés", @@ -2128,12 +2123,12 @@ "You_do_not_have_permission_to_do_this": "Nincs jogosultsága ahhoz, hogy ezt tegye", "Hide_counter": "Számláló elrejtése", "Hide_flextab": "Jobb oldalsáv elrejtése kattintással", - "Hide_Group_Warning": "Biztosan el szeretné rejteni a(z) „%s” csoportot?", - "Hide_Livechat_Warning": "Biztosan el szeretné rejteni a(z) „%s” felhasználóval történt csevegést?", - "Hide_Private_Warning": "Biztosan el szeretné rejteni a(z) „%s” felhasználóval történt megbeszélést?", + "Hide_Group_Warning": "Biztosan el szeretné rejteni a(z) „{{roomName}}” csoportot?", + "Hide_Livechat_Warning": "Biztosan el szeretné rejteni a(z) „{{roomName}}” felhasználóval történt csevegést?", + "Hide_Private_Warning": "Biztosan el szeretné rejteni a(z) „{{roomName}}” felhasználóval történt megbeszélést?", "Hide_roles": "Szerepek elrejtése", "Hide_room": "Elrejtés", - "Hide_Room_Warning": "Biztosan el szeretné rejteni a(z) „%s” csatornát?", + "Hide_Room_Warning": "Biztosan el szeretné rejteni a(z) „{{roomName}}” csatornát?", "Hide_Unread_Room_Status": "Olvasatlan szobaállapot elrejtése", "Hide_usernames": "Felhasználónevek elrejtése", "Highlights": "Kiemelések", @@ -2646,11 +2641,11 @@ "Lead_capture_email_regex": "Érdeklődő rögzítésének e-mail reguláris kifejezése", "Lead_capture_phone_regex": "Érdeklődő rögzítésének telefon reguláris kifejezése", "Leave": "Elhagyás", - "Leave_Group_Warning": "Biztosan el szeretné hagyni a(z) „%s” csoportot?", - "Leave_Livechat_Warning": "Biztosan el szeretné hagyni a(z) „%s” felhasználóval történt összcsatornát?", - "Leave_Private_Warning": "Biztosan el szeretné hagyni a(z) „%s” felhasználóval történt megbeszélést?", + "Leave_Group_Warning": "Biztosan el szeretné hagyni a(z) „{{roomName}}” csoportot?", + "Leave_Livechat_Warning": "Biztosan el szeretné hagyni a(z) „{{roomName}}” felhasználóval történt összcsatornát?", + "Leave_Private_Warning": "Biztosan el szeretné hagyni a(z) „{{roomName}}” felhasználóval történt megbeszélést?", "Leave_room": "Elhagyás", - "Leave_Room_Warning": "Biztosan el szeretné hagyni a(z) „%s” csatornát?", + "Leave_Room_Warning": "Biztosan el szeretné hagyni a(z) „{{roomName}}” csatornát?", "Leave_the_current_channel": "A jelenlegi csatorna elhagyása", "leave-c": "Csatornák elhagyása", "Instance": "Példány", @@ -4374,7 +4369,6 @@ "room_removed_read_only_permission": "csak olvasható jogosultság eltávolítva", "Two-factor_authentication_enabled": "A kétfaktoros hitelesítés engedélyezve van", "room_set_read_only_permission": "szoba beállítva csak olvashatóra", - "Two-factor_authentication_is_currently_disabled": "Az időalapú, egyszer használatos jelszóval történő kétfaktoros hitelesítés jelenleg le van tiltva", "Two-factor_authentication_native_mobile_app_warning": "FIGYELMEZTETÉS: ha engedélyezi ezt, akkor nem lesz képes bejelentkezni a natív mobilalkalmazásokon (Rocket.Chat+) a jelszava használatával, amíg nem valósítják meg a kétfaktoros hitelesítést.", "Type": "Típus", "Room_updated_successfully": "A szoba sikeresen frissítve!", @@ -5015,7 +5009,6 @@ "Turn_off_video": "Videó kikapcsolása", "Two-factor_authentication_via_TOTP": "Kétlépcsős azonosítás", "Two-factor_authentication_email": "E-mailen keresztüli kétfaktoros hitelesítés", - "Two-factor_authentication_email_is_currently_disabled": "Az e-mailen keresztüli kétfaktoros hitelesítés jelenleg le van tiltva", "typing": "ír", "Types": "Típusok", "Types_and_Distribution": "Típusok és disztribúció", @@ -5403,4 +5396,4 @@ "Enterprise": "Vállalati", "UpgradeToGetMore_engagement-dashboard_Title": "Analitika", "UpgradeToGetMore_auditing_Title": "Üzenet ellenőrzés" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/id.i18n.json b/packages/i18n/src/locales/id.i18n.json index 8cfecd55098ca..3d78f86d23344 100644 --- a/packages/i18n/src/locales/id.i18n.json +++ b/packages/i18n/src/locales/id.i18n.json @@ -864,7 +864,6 @@ "Directory": "Direktori", "Disable_Facebook_integration": "Nonaktifkan integrasi Facebook", "Disable_Notifications": "Nonaktifkan Pemberitahuan", - "Disable_two-factor_authentication": "Nonaktifkan autentikasi dua faktor", "Disabled": "Cacat", "Disallow_reacting": "Larang Beraksi", "Disallow_reacting_Description": "Dilarang bereaksi", @@ -957,7 +956,6 @@ "Enable_Auto_Away": "Aktifkan Auto Away", "Enable_Desktop_Notifications": "Hidupkan Notifikasi Desktop", "Enable_Svg_Favicon": "Aktifkan favicon SVG", - "Enable_two-factor_authentication": "Aktifkan autentikasi dua faktor", "Enabled": "Diaktifkan", "Encrypted_message": "pesan terenkripsi", "End_OTR": "akhir OTR", @@ -1219,12 +1217,12 @@ "Hide": "Sembunyikan room", "Hide_counter": "Sembunyikan kontra", "Hide_flextab": "Sembunyikan Bilah Kanan dengan Klik", - "Hide_Group_Warning": "Apakah Anda yakin Anda ingin menyembunyikan kelompok \"%s\"?", - "Hide_Livechat_Warning": "Apakah Anda yakin ingin menyembunyikan livechat dengan \"%s\"?", - "Hide_Private_Warning": "Apakah Anda yakin ingin menyembunyikan diskusi dengan \"%s\"?", + "Hide_Group_Warning": "Apakah Anda yakin Anda ingin menyembunyikan kelompok \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Apakah Anda yakin ingin menyembunyikan livechat dengan \"{{roomName}}\"?", + "Hide_Private_Warning": "Apakah Anda yakin ingin menyembunyikan diskusi dengan \"{{roomName}}\"?", "Hide_roles": "Sembunyikan Peran", "Hide_room": "Sembunyikan room", - "Hide_Room_Warning": "Apakah Anda yakin Anda ingin menyembunyikan ruang \"%s\"?", + "Hide_Room_Warning": "Apakah Anda yakin Anda ingin menyembunyikan ruang \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Sembunyikan Status Kamar Belum Dibaca", "Hide_usernames": "menyembunyikan nama pengguna", "Highlights": "Highlight", @@ -1523,11 +1521,11 @@ "Lead_capture_email_regex": "Memimpin menangkap email regex", "Lead_capture_phone_regex": "Memimpin menangkap regex telepon", "Leave": "Keluar dari room", - "Leave_Group_Warning": "Apakah Anda yakin ingin meninggalkan kelompok \"%s\"?", - "Leave_Livechat_Warning": "Apakah Anda yakin ingin meninggalkan livechat dengan \"%s\"?", - "Leave_Private_Warning": "Apakah Anda yakin ingin meninggalkan diskusi dengan \"%s\"?", + "Leave_Group_Warning": "Apakah Anda yakin ingin meninggalkan kelompok \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Apakah Anda yakin ingin meninggalkan livechat dengan \"{{roomName}}\"?", + "Leave_Private_Warning": "Apakah Anda yakin ingin meninggalkan diskusi dengan \"{{roomName}}\"?", "Leave_room": "Keluar dari room", - "Leave_Room_Warning": "Apakah Anda yakin ingin meninggalkan ruangan \"%s\"?", + "Leave_Room_Warning": "Apakah Anda yakin ingin meninggalkan ruangan \"{{roomName}}\"?", "Leave_the_current_channel": "Tinggalkan saluran saat ini", "leave-c": "Tinggalkan Saluran", "leave-p": "Tinggalkan Grup Pribadi", @@ -2453,7 +2451,6 @@ "Two-factor_authentication": "Autentikasi dua faktor", "Two-factor_authentication_disabled": "Autentikasi dua faktor dinonaktifkan", "Two-factor_authentication_enabled": "Autentikasi dua faktor diaktifkan", - "Two-factor_authentication_is_currently_disabled": "Autentikasi dua faktor saat ini dinonaktifkan", "Two-factor_authentication_native_mobile_app_warning": "PERINGATAN: Setelah mengaktifkannya, Anda tidak akan dapat masuk ke aplikasi seluler asli (Rocket.Chat +) menggunakan kata sandi Anda sampai mereka menerapkan 2FA.", "Type": "Mengetik", "Type_your_email": "Ketik email Anda", @@ -2761,4 +2758,4 @@ "registration.component.form.sendConfirmationEmail": "Kirim email konfirmasi", "Enterprise": "Perusahaan", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/it.i18n.json b/packages/i18n/src/locales/it.i18n.json index 78157d2ddba00..0b1ed66dd4569 100644 --- a/packages/i18n/src/locales/it.i18n.json +++ b/packages/i18n/src/locales/it.i18n.json @@ -1009,7 +1009,6 @@ "Directory": "Directory", "Disable_Facebook_integration": "Disabilitare l'integrazione di Facebook", "Disable_Notifications": "Disabilita notifiche", - "Disable_two-factor_authentication": "Disabilita autenticazione a due fattori", "Disabled": "Disabilitato", "Disabled_E2E_Encryption_for_this_room": "criptazione E2E disattiva per questo canale", "Disallow_reacting": "Disallow Reagire", @@ -1107,7 +1106,6 @@ "Enable_Desktop_Notifications": "Abilita notifiche desktop", "Enable_Svg_Favicon": "Abilita Favicon SVG", "Enable_business_hours": "Attiva orari di lavoro", - "Enable_two-factor_authentication": "Abilita autenticazione a due fattori", "Enable_unlimited_apps": "Abilita app illimitate", "Enabled": "Abilitato", "Enabled_E2E_Encryption_for_this_room": "criptazione E2E attiva per questo canale", @@ -1318,11 +1316,11 @@ "Hi_username": "Ciao [name]", "Hidden": "Nascosto", "Hide": "Nascondi", - "Hide_Group_Warning": "Sei sicuro di voler nascondere il gruppo \"%s\"?", - "Hide_Livechat_Warning": "Sei sicuro di voler nascondere il livechat con \"%s\"?", + "Hide_Group_Warning": "Sei sicuro di voler nascondere il gruppo \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Sei sicuro di voler nascondere il livechat con \"{{roomName}}\"?", "Hide_On_Workspace": "Nascondi nell'area di lavoro", - "Hide_Private_Warning": "Sei sicuro di voler nascondere la discussione con \"%s\"?", - "Hide_Room_Warning": "Sei sicuro di voler nascondere il canale \"%s\"?", + "Hide_Private_Warning": "Sei sicuro di voler nascondere la discussione con \"{{roomName}}\"?", + "Hide_Room_Warning": "Sei sicuro di voler nascondere il canale \"{{roomName}}\"?", "Hide_System_Messages": "Nascondi messaggi di sistema", "Hide_Unread_Room_Status": "Nascondi lo stato del canale non letto", "Hide_counter": "Nascondi contatore", @@ -1690,10 +1688,10 @@ "Learn_more": "Per saperne di più", "Least_recent_updated": "Aggiornamento più recente", "Leave": "Lascia", - "Leave_Group_Warning": "Sei sicuro di voler lasciare il gruppo \"%s\"?", - "Leave_Livechat_Warning": "Sei sicuro di voler lasciare il live con \"%s\"?", - "Leave_Private_Warning": "Sei sicuro di volere lasciare la discussione con \"%s\"?", - "Leave_Room_Warning": "Sei sicuro di voler abbandonare il canale \"%s\"?", + "Leave_Group_Warning": "Sei sicuro di voler lasciare il gruppo \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Sei sicuro di voler lasciare il live con \"{{roomName}}\"?", + "Leave_Private_Warning": "Sei sicuro di volere lasciare la discussione con \"{{roomName}}\"?", + "Leave_Room_Warning": "Sei sicuro di voler abbandonare il canale \"{{roomName}}\"?", "Leave_a_comment": "Lascia un commento", "Leave_room": "Lasciare il canale", "Leave_the_current_channel": "Abbandona il canale corrente", @@ -2641,7 +2639,6 @@ "Two-factor_authentication": "Autenticazione a due fattori", "Two-factor_authentication_disabled": "Autenticazione a due fattori disabilitata", "Two-factor_authentication_enabled": "Autenticazione a due fattori abilitata", - "Two-factor_authentication_is_currently_disabled": "L'autenticazione a due fattori è attualmente disabilitata", "Two-factor_authentication_native_mobile_app_warning": "ATTENZIONE: una volta abilitato, non potrai accedere alle app native native (Rocket.Chat +) usando la tua password fino a quando non implementeranno la 2FA.", "Two-factor_authentication_via_TOTP": "Autenticazione a due fattori", "Type": "Tipo", diff --git a/packages/i18n/src/locales/ja.i18n.json b/packages/i18n/src/locales/ja.i18n.json index 938bf7cfa4028..fa82a332213f5 100644 --- a/packages/i18n/src/locales/ja.i18n.json +++ b/packages/i18n/src/locales/ja.i18n.json @@ -668,7 +668,6 @@ "Back_to_imports": "インポートに戻る", "Cancel": "キャンセル", "Cancel_message_input": "キャンセル", - "Back_to_room": "Roomに戻る", "Canceled": "キャンセルしました", "Back_to_threads": "スレッドに戻る", "BBB_End_Meeting": "ミーティングの終了", @@ -1355,7 +1354,6 @@ "Custom_Script_Logged_Out_Description": "常に実行され、ログインしているすべてのユーザーに対して実行されるカスタムスクリプト(ログインページに入るときは常時)", "Disable_Notifications": "通知を無効にする", "Custom_Script_On_Logout": "ログアウトフロー用のカスタムスクリプト", - "Disable_two-factor_authentication": "TOTPによる2要素認証を無効にする", "Custom_Script_On_Logout_Description": "ログアウトフローの実行時のみに実行されるカスタムスクリプト", "Disabled": "無効", "Disallow_reacting": "応答の禁止", @@ -1502,14 +1500,12 @@ "Disable": "無効", "EmojiCustomFilesystem": "カスタム絵文字ファイルシステム", "Empty_title": "タイトルなし", - "Disable_two-factor_authentication_email": "メールによる2要素認証を無効にする", "Enable": "有効", "Enable_Auto_Away": "自動離席を有効にする", "Enable_Desktop_Notifications": "デスクトップ通知を有効にする", "Discard": "破棄", "Discussion": "ディスカッション", "Enable_Svg_Favicon": "SVGファビコンを有効にする", - "Enable_two-factor_authentication": "TOTPによる2要素認証を有効にする", "Discussion_first_message_disabled_due_to_e2e": "このディスカッションでは、作成後にエンドツーエンドの暗号化されたメッセージの送信を開始できます。", "Enabled": "有効", "Encrypted": "暗号化済み", @@ -1719,7 +1715,6 @@ "External_Queue_Service_URL": "外部キューサービスのURL", "External_Service": "外部サービス", "Facebook_Page": "Facebookのページ", - "Enable_two-factor_authentication_email": "メールを介した2要素認証を有効にする", "Encrypted_not_available": "パブリックChannelsには利用できません", "False": "False", "End": "終了", @@ -1943,12 +1938,12 @@ "Hide": "非表示", "Hide_counter": "カウンターを非表示", "Hide_flextab": "クリックと同時に右サイドバーを非表示", - "Hide_Group_Warning": "グループ「%s」を非表示にしてよろしいですか?", - "Hide_Livechat_Warning": "「%s」とのチャットを非表示にしてよろしいですか?", - "Hide_Private_Warning": "「%s」とのディスカッションを非表示にしてよろしいですか?", + "Hide_Group_Warning": "グループ「{{roomName}}」を非表示にしてよろしいですか?", + "Hide_Livechat_Warning": "「{{roomName}}」とのチャットを非表示にしてよろしいですか?", + "Hide_Private_Warning": "「{{roomName}}」とのディスカッションを非表示にしてよろしいですか?", "Hide_roles": "ロールを非表示", "Hide_room": "ルームを非表示", - "Hide_Room_Warning": "チャネル「%s」を非表示にしてよろしいですか?", + "Hide_Room_Warning": "チャネル「{{roomName}}」を非表示にしてよろしいですか?", "Hide_Unread_Room_Status": "未読のRoomステータスを非表示", "Hide_usernames": "ユーザー名を非表示", "Highlights": "ハイライト", @@ -2415,11 +2410,11 @@ "Lead_capture_email_regex": "リードキャプチャメールの正規表現", "Lead_capture_phone_regex": "リードキャプチャ電話の正規表現", "Leave": "退出", - "Leave_Group_Warning": "グループ「%s」から退出してよろしいですか?", - "Leave_Livechat_Warning": "「%s」とのオムニチャネルから退出してよろしいですか?", - "Leave_Private_Warning": "「%s」とのディスカッションから退出してよろしいですか?", + "Leave_Group_Warning": "グループ「{{roomName}}」から退出してよろしいですか?", + "Leave_Livechat_Warning": "「{{roomName}}」とのオムニチャネルから退出してよろしいですか?", + "Leave_Private_Warning": "「{{roomName}}」とのディスカッションから退出してよろしいですか?", "Leave_room": "退出", - "Leave_Room_Warning": "チャネル「%s」から退出してよろしいですか?", + "Leave_Room_Warning": "チャネル「{{roomName}}」から退出してよろしいですか?", "Leave_the_current_channel": "現在のチャネルから退出", "leave-c": "Channelから退出", "Instance": "インスタンス", @@ -3964,7 +3959,6 @@ "room_set_read_only": "Roomは{{user_by}}によって読み取り専用に設定されました", "Two-factor_authentication_disabled": "2要素認証が無効です", "Two-factor_authentication_enabled": "2要素認証が有効です", - "Two-factor_authentication_is_currently_disabled": "TOTPによる2要素認証は現在無効です", "Two-factor_authentication_native_mobile_app_warning": "警告:これを有効にすると、2FAを実装するまでは、パスワードを使ってネイティブモバイルアプリ(Rocket.Chat +)でログインすることはできません。", "Type": "種類", "Room_updated_successfully": "Roomが正常に更新されました!", @@ -4536,7 +4530,6 @@ "Turn_off_video": "ビデオをオフ", "Two-factor_authentication_via_TOTP": "TOTPによる2要素認証", "Two-factor_authentication_email": "メールによる2要素認証", - "Two-factor_authentication_email_is_currently_disabled": "メールによる2要素認証は現在無効になっています", "typing": "入力", "Types": "種類", "Types_and_Distribution": "種類と配布", @@ -4813,4 +4806,4 @@ "Enterprise": "エンタープライズ", "UpgradeToGetMore_engagement-dashboard_Title": "分析", "UpgradeToGetMore_auditing_Title": "メッセージ監査" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ka-GE.i18n.json b/packages/i18n/src/locales/ka-GE.i18n.json index 5a78d4c7534da..4d97626c7bb11 100644 --- a/packages/i18n/src/locales/ka-GE.i18n.json +++ b/packages/i18n/src/locales/ka-GE.i18n.json @@ -541,7 +541,6 @@ "Back_to_imports": "იმპორტში დაბრუნება", "Cancel": "გაუქმება", "Cancel_message_input": "გაუქმება", - "Back_to_room": "დაბრუნება Room -ში", "Canceled": "გაუქმდა", "Cannot_invite_users_to_direct_rooms": "მომხმარებლების პირდაპირ ოთახში მოწვევა შეუძლებელია", "Cannot_open_conversation_with_yourself": "თქვენ ვერ შეძლებთ პირდაპირ მესიჯის გაგზავნას საკუთარ თავთან", @@ -1114,7 +1113,6 @@ "Custom_Script_Logged_Out_Description": "პირადი სკრიპტი რომელიც გაეშვება ყოველთვის და ყველა მომხმარებლითვის რომელიც გასულია სისტემიდან", "Disable_Notifications": "შეტყობინებების გათიშვა", "Custom_Script_On_Logout": "პირადი სკრიპტი მათთვის ვინც გადის სისტემიდან", - "Disable_two-factor_authentication": "ორ ფაქტორინი ავტორიზაციის გათიშვა TOTP-ით ", "Custom_Script_On_Logout_Description": "პირადი სკრიპტი რომელიც გაეშვება და შესრულდება მხოლოდ სისტემიდან გამსვლელთათვის", "Disabled": "გათიშული", "Disallow_reacting": "რეაქციის აკრძალვა", @@ -1244,13 +1242,11 @@ "Emoji": "ემოჯი", "EmojiCustomFilesystem": "პერსონალური ემოჯი ფაილ-სისტემა", "Empty_title": "ცარიელი სათაური", - "Disable_two-factor_authentication_email": "ორ ფაქტორინი ავტორიზაციის გათიშვა ელ.ფოსტით", "Enable": "ჩართვა", "Enable_Auto_Away": "\"გასულია\" სტატუსის ავტომატურად ჩართვა", "Enable_Desktop_Notifications": "დესკტოპ შეტყობინებების ჩართვა", "Discussion": "განხილვა", "Enable_Svg_Favicon": "ჩართეთ SVG ფავორიტი ნიშანი", - "Enable_two-factor_authentication": "ჩართეთ 2 ფაქტორიანი ავტორიზაცია TOTP-ით", "Enabled": "ჩართული", "Encrypted": "დაშიფრულია", "Encrypted_channel_Description": "წერტილიდან ბოლო წერტილამდე დაშიფრული არხი. ძიება არ აჩვენებს დაშიფრულ არხებს და შეტყობინებებმა შეიძლება არ აცვენოს მესიჯის ტექსტი", @@ -1423,7 +1419,6 @@ "External_Queue_Service_URL": "გარე რიგის მომსახურების URL", "External_Service": "გარე მომსახურება", "Facebook_Page": "Facebook გვერდი", - "Enable_two-factor_authentication_email": "ჩართეთ 2 ფაქტორიანი ავტორიზაცია ელ.ფოსტით", "False": " მცდარი", "Favorite": "ფავორიტი", "Favorite_Rooms": "ჩართეთ ფავორიტი ოთახები", @@ -1608,12 +1603,12 @@ "Hide": "დამალვა", "Hide_counter": "მთვლელის დამალვა", "Hide_flextab": "დამალეთ გვერდითა ბარი კლიკით", - "Hide_Group_Warning": "დარწმუნებული ხართ, რომ გსურთ \"%s\" ჯგუფის დამალვა?", - "Hide_Livechat_Warning": "დარწმუნებული ხართ, რომ გსურთ \"%s\"-თან ჩატის დამალვა?", - "Hide_Private_Warning": "დარწმუნებული ხართ, რომ გსურთ \"%s\"-თან დისკუსიის დამალვა?", + "Hide_Group_Warning": "დარწმუნებული ხართ, რომ გსურთ \"{{roomName}}\" ჯგუფის დამალვა?", + "Hide_Livechat_Warning": "დარწმუნებული ხართ, რომ გსურთ \"{{roomName}}\"-თან ჩატის დამალვა?", + "Hide_Private_Warning": "დარწმუნებული ხართ, რომ გსურთ \"{{roomName}}\"-თან დისკუსიის დამალვა?", "Hide_roles": "როლების დამალვა", "Hide_room": "ოთახის დამალვა", - "Hide_Room_Warning": "დარწმუნებული ხართ, რომ გსურთ \"%s\" ოთახის დამალვა?", + "Hide_Room_Warning": "დარწმუნებული ხართ, რომ გსურთ \"{{roomName}}\" ოთახის დამალვა?", "Hide_Unread_Room_Status": "ოთახის წაუკითხავი სტატუსის დამალვა", "Hide_usernames": "მომხმარებლის სახელების დამალვა", "Highlights": "ჰაილაითები", @@ -1997,11 +1992,11 @@ "LDAP_User_Search_Scope": "სფერო", "LDAP_Username_Field": "მომხმარებლის სახელის ველი", "Leave": "დატოვე", - "Leave_Group_Warning": "დარწმუნებული ხართ, რომ გსურთ დატოვოთ ჯგუფი \"%s\"?", - "Leave_Livechat_Warning": "დარწმუნებული ხართ, რომ გსურთ Omnichannel- ის დატოვება \"%s\" - ით?", - "Leave_Private_Warning": "დარწმუნებული ხართ, რომ გსურთ დატოვოთ განხილვა \"%s\"-ით?", + "Leave_Group_Warning": "დარწმუნებული ხართ, რომ გსურთ დატოვოთ ჯგუფი \"{{roomName}}\"?", + "Leave_Livechat_Warning": "დარწმუნებული ხართ, რომ გსურთ Omnichannel- ის დატოვება \"{{roomName}}\" - ით?", + "Leave_Private_Warning": "დარწმუნებული ხართ, რომ გსურთ დატოვოთ განხილვა \"{{roomName}}\"-ით?", "Leave_room": "ოთახის დატოვება", - "Leave_Room_Warning": "დარწმუნებული ხართ, რომ გსურთ დატოვოთ ოთახი \"%s\"?", + "Leave_Room_Warning": "დარწმუნებული ხართ, რომ გსურთ დატოვოთ ოთახი \"{{roomName}}\"?", "Leave_the_current_channel": "დატოვეთ მიმდინარე არხი", "leave-c": "დატოვეთ არხები", "leave-p": "დატოვე პირადი ჯგუფები", @@ -3190,7 +3185,6 @@ "Two-factor_authentication": "ორ ფაქტორიანი ავტენტიფიკაცია TOTP-ით", "Two-factor_authentication_disabled": "ორ ფაქტორიანი ავტენტიფიკაცია გამორთულია", "Two-factor_authentication_enabled": "ორ ფაქტორიანი ავტენტიფიკაცია ჩართულია", - "Two-factor_authentication_is_currently_disabled": "ორ ფაქტორიანი ავტენტიფიკაცია TOTP-ით ამჟამად გამორთულია", "Two-factor_authentication_native_mobile_app_warning": "გაფრთხილება: ამის ჩართვის შემდეგ თქვენ ვეღარ შეძლებთ თავდაპირველი მობილური აპლიკაციების გამოყენებას (Rocket.Chat+) თქვენი პაროლით ვიდრე ისინი არ დაამატებენ 2FA-ს", "Type": "ტიპი", "Room_updated_successfully": "ოთახი წარმატებით განახლდა!", @@ -3577,7 +3571,6 @@ "Try_now": "სცადე ახლა", "Two-factor_authentication_via_TOTP": "ორ ფაქტორიანი ავტენტიფიკაცია TOTP-ით", "Two-factor_authentication_email": "ორ ფაქტორიანი ავტენტიფიკაცია ელ.ფოსტით", - "Two-factor_authentication_email_is_currently_disabled": "ორ ფაქტორიანი ავტენტიფიკაცია ელ.ფოსტით ამჟამად გამორთულია", "UI_Show_top_navbar_embedded_layout": "აჩვენეთ ზედა ნავიგაციის ჩასმული განლაგება", "unable-to-get-file": "ფაილის მიღება შეუძლებელია", "unauthorized": "არაა უფლებამოსილი", @@ -3664,4 +3657,4 @@ "onboarding.form.registerOfflineForm.title": "ხელით დარეგისტრირება", "UpgradeToGetMore_engagement-dashboard_Title": "ანალიტიკა", "UpgradeToGetMore_auditing_Title": "შეტყობინებების შემოწმება" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/km.i18n.json b/packages/i18n/src/locales/km.i18n.json index 1ff62dac295ea..4f25700ab0ec4 100644 --- a/packages/i18n/src/locales/km.i18n.json +++ b/packages/i18n/src/locales/km.i18n.json @@ -513,7 +513,6 @@ "Avg_of_waiting_time": "ជាមធ្យមនៃការរង់ចាំពេលវេលា", "Cancel": "បញ្ឈប់", "Cancel_message_input": "បញ្ឈប់", - "Back_to_room": "ត្រលប់ទៅ Room។", "Canceled": "បានបោះបង់", "Cannot_invite_users_to_direct_rooms": "មិនអាចអញ្ជើញអ្នកប្រើប្រាស់ដើម្បីដឹកនាំបន្ទប់", "Cannot_open_conversation_with_yourself": "មិនអាចបញ្ជូនសារផ្ទាល់ជាមួយខ្លួនអ្នកបានទេ", @@ -1054,7 +1053,6 @@ "Disable_Facebook_integration": "បិទដំណើរការសមាហរណកម្ម Facebook", "Custom_Script_Logged_Out_Description": "ស្គ្រីបផ្ទាល់ខ្លួនដែលដំណើរការជាប្រចាំនិងចំពោះអ្នកប្រើប្រាស់ណាដែលមិនបានចូល។ ឧ។ (ពេលណាអ្នកបញ្ចូលទំព័រចូល)", "Disable_Notifications": "បិទការជូនដំណឹង", - "Disable_two-factor_authentication": "បិទការផ្ទៀងផ្ទាត់ពីរកត្តា", "Disabled": "បានបិទ", "Disallow_reacting": "មិនអនុញ្ញាតឱ្យមានប្រតិកម្ម", "Disallow_reacting_Description": "មិនអនុញ្ញាតឱ្យមានប្រតិកម្ម", @@ -1162,7 +1160,6 @@ "Enable_Auto_Away": "បើកដំណើរការស្វ័យប្រវត្តិ", "Enable_Desktop_Notifications": "អនុញ្ញាតិជំនូនដំណឹងលើ Desktop", "Enable_Svg_Favicon": "បើកដំណើរការ SVG favicon", - "Enable_two-factor_authentication": "បើកការផ្ទៀងផ្ទាត់ពីរកត្តា", "Enabled": "បានបើក", "Encrypted": "បានបំលែងកូដ", "Encrypted_message": "សារដែលបានអ៊ិនគ្រីប", @@ -1469,12 +1466,12 @@ "Hide": " ", "Hide_counter": "លាក់រាប់", "Hide_flextab": "លាក់របារចំហៀងខាងស្តាំដោយចុច", - "Hide_Group_Warning": "តើអ្នកពិតជាចង់លាក់ក្រុម \"%s\" ទេ?", - "Hide_Livechat_Warning": "តើអ្នកប្រាកដជាចង់លាក់ livechat ជាមួយ \"%s\" មែនទេ?", - "Hide_Private_Warning": "តើអ្នកប្រាកដថាអ្នកចង់លាក់ការពិភាក្សាជាមួយ \"%s\" ទេ?", + "Hide_Group_Warning": "តើអ្នកពិតជាចង់លាក់ក្រុម \"{{roomName}}\" ទេ?", + "Hide_Livechat_Warning": "តើអ្នកប្រាកដជាចង់លាក់ livechat ជាមួយ \"{{roomName}}\" មែនទេ?", + "Hide_Private_Warning": "តើអ្នកប្រាកដថាអ្នកចង់លាក់ការពិភាក្សាជាមួយ \"{{roomName}}\" ទេ?", "Hide_roles": "លាក់តួនាទី", "Hide_room": "លាក់​បន្ទប់", - "Hide_Room_Warning": "តើអ្នកពិតជាចង់លាក់បន្ទប់ \"%s\"?", + "Hide_Room_Warning": "តើអ្នកពិតជាចង់លាក់បន្ទប់ \"{{roomName}}\"?", "Hide_Unread_Room_Status": "លាក់ស្ថានភាពបន្ទប់មិនទាន់អាន", "Hide_usernames": "លាក់ឈ្មោះអ្នកប្រើ", "Highlights": "ការរំលេច", @@ -1793,11 +1790,11 @@ "Lead_capture_email_regex": "នាំយកអ៊ីមែល regex", "Lead_capture_phone_regex": "នាំយកការហៅទូរស័ព្ទ regex", "Leave": "ចេញ​ពីបន្ទប់", - "Leave_Group_Warning": "តើអ្នកពិតជាចង់ចាកចេញពីក្រុម \"%s\" ទេ?", - "Leave_Livechat_Warning": "តើអ្នកប្រាកដជាចង់ចាកចេញពី livechat ជាមួយ \"%s\" មែនទេ?", - "Leave_Private_Warning": "តើអ្នកពិតជាចង់ទុកការពិភាក្សាជាមួយ \"%s\" ទេ?", + "Leave_Group_Warning": "តើអ្នកពិតជាចង់ចាកចេញពីក្រុម \"{{roomName}}\" ទេ?", + "Leave_Livechat_Warning": "តើអ្នកប្រាកដជាចង់ចាកចេញពី livechat ជាមួយ \"{{roomName}}\" មែនទេ?", + "Leave_Private_Warning": "តើអ្នកពិតជាចង់ទុកការពិភាក្សាជាមួយ \"{{roomName}}\" ទេ?", "Leave_room": "ចេញ​ពីបន្ទប់", - "Leave_Room_Warning": "តើអ្នកពិតជាចង់ចាកចេញពីបន្ទប់ \"%s\"?", + "Leave_Room_Warning": "តើអ្នកពិតជាចង់ចាកចេញពីបន្ទប់ \"{{roomName}}\"?", "Leave_the_current_channel": "ចាកចេញពីឆានែលបច្ចុប្បន្ន", "leave-c": "ចាកចេញពីឆានែល", "leave-p": "ចាកចេញពីក្រុមឯកជន", @@ -2783,7 +2780,6 @@ "Two-factor_authentication": "ការផ្ទៀងផ្ទាត់ពីរកត្តា", "Two-factor_authentication_disabled": "ការផ្ទៀងផ្ទាត់ពីរកត្តាត្រូវបានបិទ", "Two-factor_authentication_enabled": "ការផ្ទៀងផ្ទាត់ពីរកត្តាត្រូវបានបើក", - "Two-factor_authentication_is_currently_disabled": "ការសម្គាល់អត្តសញ្ញាណកត្តាពីរត្រូវបានបិទនាពេលបច្ចុប្បន្ន", "Two-factor_authentication_native_mobile_app_warning": "ព្រមាន: នៅពេលដែលអ្នកបើកវាអ្នកនឹងមិនអាចចូលក្នុងកម្មវិធីទូរស័ព្ទដើម (Rocket.Chat +) ដោយប្រើពាក្យសម្ងាត់របស់អ្នករហូតដល់ពួកគេអនុវត្តកម្មវិធី 2FA ។", "Type": "ប្រភេទ", "Type_your_email": "វាយបញ្ចូលអ៊ីមែលរបស់លោកអ្នក", @@ -3110,4 +3106,4 @@ "Enterprise": "សហគ្រាស", "UpgradeToGetMore_engagement-dashboard_Title": "វិភាគ", "UpgradeToGetMore_auditing_Title": "សវនកម្មសារ" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ko.i18n.json b/packages/i18n/src/locales/ko.i18n.json index fe3a773ae74a1..e831f94eb093c 100644 --- a/packages/i18n/src/locales/ko.i18n.json +++ b/packages/i18n/src/locales/ko.i18n.json @@ -542,7 +542,6 @@ "Back_to_integrations": "Integrations 로 돌아가기", "Back_to_login": "로그인으로 돌아가기", "Back_to_permissions": "권한 으로 돌아가기", - "Back_to_room": "Room으로 돌아가기", "Backup_codes": "백업 코드", "Best_first_response_time": "최상의 첫번째 응답 시간", "Beta_feature_Depends_on_Video_Conference_to_be_enabled": "베타 기능입니다. 화상 회의 설정을 따릅니다.", @@ -1160,8 +1159,6 @@ "Disable": "비활성화", "Disable_Facebook_integration": "Facebook 통합 사용중지", "Disable_Notifications": "알림 사용중지", - "Disable_two-factor_authentication": "2단계 인증(2FA) 사용중지", - "Disable_two-factor_authentication_email": "이메일을 통한 2단계 인증(2FA) 사용중지", "Disabled": "비활성화됨", "Disabled_E2E_Encryption_for_this_room": "이 방에 대해 E2E 암호화 비활성화", "Disallow_reacting": "반응을 허용하지 않음", @@ -1273,8 +1270,6 @@ "Enable_business_hours": "업무 시간 활성화", "Enable_inquiry_fetch_by_stream": "스트림을 사용한 서버에서 조회 데이터 가져오기 사용", "Enable_omnichannel_auto_close_abandoned_rooms": "방치된 대화방 자동잠금 사용", - "Enable_two-factor_authentication": "2단계 인증(2FA) 사용", - "Enable_two-factor_authentication_email": "이메일을 통한 2단계 인증(2FA) 인증 활성화", "Enabled": "활성화", "Enabled_E2E_Encryption_for_this_room": "이 방에 E2E 암호화 활성화", "Encrypted": "암호화됨", @@ -1528,10 +1523,10 @@ "Hi_username": "[name] 님 안녕하세요.", "Hidden": "비표시", "Hide": "숨기기", - "Hide_Group_Warning": "그룹 \"%s\"을(를) 숨기시겠습니까?", - "Hide_Livechat_Warning": "\"%s\" Livechat 대화방을 숨기시겠습니까?", - "Hide_Private_Warning": "\"%s\"님과의 대화를 비표시 하시겠습니까?", - "Hide_Room_Warning": "\"%s\" 대화방을 숨기시겠습니까?", + "Hide_Group_Warning": "그룹 \"{{roomName}}\"을(를) 숨기시겠습니까?", + "Hide_Livechat_Warning": "\"{{roomName}}\" Livechat 대화방을 숨기시겠습니까?", + "Hide_Private_Warning": "\"{{roomName}}\"님과의 대화를 비표시 하시겠습니까?", + "Hide_Room_Warning": "\"{{roomName}}\" 대화방을 숨기시겠습니까?", "Hide_System_Messages": "시스템 메시지 숨기기", "Hide_Unread_Room_Status": "읽지 않은 상태 숨기기", "Hide_counter": "카운터 숨기기", @@ -1921,10 +1916,10 @@ "Lead_capture_email_regex": "리드 캡쳐 이메일 정규식", "Lead_capture_phone_regex": "리드 캡처 전화 정규식", "Leave": "나가기", - "Leave_Group_Warning": "\"%s\" 대화방에서 나가시겠습니까?", - "Leave_Livechat_Warning": "\"%s\" 실시간상담 대화방을 나가시겠습니까?", - "Leave_Private_Warning": "\"%s\"님과의 대화를 종료하시겠습니까?", - "Leave_Room_Warning": "\"%s\" 채널에서 나가시겠습니까?", + "Leave_Group_Warning": "\"{{roomName}}\" 대화방에서 나가시겠습니까?", + "Leave_Livechat_Warning": "\"{{roomName}}\" 실시간상담 대화방을 나가시겠습니까?", + "Leave_Private_Warning": "\"{{roomName}}\"님과의 대화를 종료하시겠습니까?", + "Leave_Room_Warning": "\"{{roomName}}\" 채널에서 나가시겠습니까?", "Leave_a_comment": "코멘트를 남겨주세요", "Leave_room": "나가기", "Leave_the_current_channel": "현재 대화방에서 나가기", @@ -3192,9 +3187,7 @@ "Two-factor_authentication": "2단계 인증(2FA)", "Two-factor_authentication_disabled": "2단계 인증(2FA) 사용 안 함", "Two-factor_authentication_email": "이메일을 통한 2단계 인증(2FA)", - "Two-factor_authentication_email_is_currently_disabled": "이메일을 통한 2단계 인증(2FA)은 현재 사용할 수 없습니다.", "Two-factor_authentication_enabled": "2단계 인증(2FA) 사용", - "Two-factor_authentication_is_currently_disabled": "2단계 인증(2FA)은 현재 사용할 수 없습니다.", "Two-factor_authentication_native_mobile_app_warning": "경고: 이 기능을 사용하면, 2단계 인증(2FA)이 적용될 때까지 비밀번호를 사용하여 모바일 앱 (Rocket.Chat +)에 로그인 할 수 없습니다.", "Two-factor_authentication_via_TOTP": "2단계 인증(2FA)", "Type": "유형", diff --git a/packages/i18n/src/locales/ku.i18n.json b/packages/i18n/src/locales/ku.i18n.json index 1c0dc903b07df..d0b0767aa1c51 100644 --- a/packages/i18n/src/locales/ku.i18n.json +++ b/packages/i18n/src/locales/ku.i18n.json @@ -863,7 +863,6 @@ "Directory": "Directory", "Disable_Facebook_integration": "Întegrasyonê ya Facebookê", "Disable_Notifications": "Notification", - "Disable_two-factor_authentication": "Pevçûnek du faktor bikin", "Disabled": "Bêmecel", "Disallow_reacting": "Disallow Reacting", "Disallow_reacting_Description": "Nerazîkirina şaşkirinê", @@ -956,7 +955,6 @@ "Enable_Auto_Away": "Vebijêrk Otomobîl", "Enable_Desktop_Notifications": "Sermaseya Notifications", "Enable_Svg_Favicon": "SVG favicon çalak bike", - "Enable_two-factor_authentication": "Guherîna du-faktorê çalak bikin", "Enabled": "çalake", "Encrypted_message": "message şîfrekirin", "End_OTR": "End OTR", @@ -1215,12 +1213,12 @@ "Hide": "شاردنەوەی ژوور", "Hide_counter": "Counter counter", "Hide_flextab": "Bişkojka Right Sidebar Bişkojka Veşêre", - "Hide_Group_Warning": "Ma tu dizanî, tu dixwazî ​​ji bo veşartina koma \"%s\"?", - "Hide_Livechat_Warning": "Ma hûn bawer dikin ku hûn dixwazin ku bi \"%s\" livechat vekin veşêre?", - "Hide_Private_Warning": "Ma tu dizanî, tu dixwazî ​​ji bo veşartina gotûbêjeke bi \"%s\"?", + "Hide_Group_Warning": "Ma tu dizanî, tu dixwazî ​​ji bo veşartina koma \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Ma hûn bawer dikin ku hûn dixwazin ku bi \"{{roomName}}\" livechat vekin veşêre?", + "Hide_Private_Warning": "Ma tu dizanî, tu dixwazî ​​ji bo veşartina gotûbêjeke bi \"{{roomName}}\"?", "Hide_roles": "Roles veşêre", "Hide_room": "شاردنەوەی ژوور", - "Hide_Room_Warning": "Ma tu dizanî, tu dixwazî ​​ji bo veşartina room \"%s\"?", + "Hide_Room_Warning": "Ma tu dizanî, tu dixwazî ​​ji bo veşartina room \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Paqijkirina Bixweya Rewş", "Hide_usernames": "Naverokan veşêre bikarhêneran", "Highlights": "Highlights", @@ -1518,11 +1516,11 @@ "Lead_capture_email_regex": "Lead capture email regex", "Lead_capture_phone_regex": "Leşkerên kolektîfê girtinê regex", "Leave": "جێهێشتنی ژوور", - "Leave_Group_Warning": "Ma tu dizanî, tu dixwazî ​​ji koma \"%s\" ku herin?", - "Leave_Livechat_Warning": "Ma hûn bawer dikin ku hûn dixwazin ku \"%s\" bi livechat derkeve?", - "Leave_Private_Warning": "Ma tu dizanî, tu dixwazî ​​li gotûbêjeke bi \"%s\" ku herin?", + "Leave_Group_Warning": "Ma tu dizanî, tu dixwazî ​​ji koma \"{{roomName}}\" ku herin?", + "Leave_Livechat_Warning": "Ma hûn bawer dikin ku hûn dixwazin ku \"{{roomName}}\" bi livechat derkeve?", + "Leave_Private_Warning": "Ma tu dizanî, tu dixwazî ​​li gotûbêjeke bi \"{{roomName}}\" ku herin?", "Leave_room": "جێهێشتنی ژوور", - "Leave_Room_Warning": "Ma tu bawer î ku dixwazî ​​ji odê derkevin \"%s\"?", + "Leave_Room_Warning": "Ma tu bawer î ku dixwazî ​​ji odê derkevin \"{{roomName}}\"?", "Leave_the_current_channel": "Vê kanalek niha bistînin", "leave-c": "Channels", "leave-p": "Komên Taybet", @@ -2440,7 +2438,6 @@ "Two-factor_authentication": "Çewtiya du-faktorê", "Two-factor_authentication_disabled": "Çewtiya du-faktîf hate qedexekirin", "Two-factor_authentication_enabled": "Guherîna du-faktorê çalak kirin", - "Two-factor_authentication_is_currently_disabled": "Çewtiya duyem-faktîk niha hate qedexekirin", "Two-factor_authentication_native_mobile_app_warning": "WARNING: Piştî ku hûn vê çalak bikin, hûn ê nikarin li ser sepanên mobîl ên navnîşan (Bişkojka Rocket.Chat +) bikar bînin, heta ku ew 2FA bicîh bikin.", "Type": "Awa", "Type_your_email": "Type email te", @@ -2743,4 +2740,4 @@ "registration.component.form.sendConfirmationEmail": "ئیمەیڵی پشتڕاستکردنەوە بنێرە", "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "analytics" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/lo.i18n.json b/packages/i18n/src/locales/lo.i18n.json index 0aef041a9228a..7068bd7745397 100644 --- a/packages/i18n/src/locales/lo.i18n.json +++ b/packages/i18n/src/locales/lo.i18n.json @@ -881,7 +881,6 @@ "Directory": "Directory", "Disable_Facebook_integration": "ປິດການໃຊ້ວຽກ Facebook integration", "Disable_Notifications": "ປິດການແຈ້ງເຕືອນ", - "Disable_two-factor_authentication": "ປິດການໃຊ້ງານການກວດສອບສອງປັດໃຈ", "Disabled": "ຖືກປິດໃຊ້ງານ", "Disallow_reacting": "ບໍ່ອະນຸຍາດໃຫ້ປະຕິກິລິຍາ", "Disallow_reacting_Description": "ບໍ່ອະນຸຍາດໃຫ້ປະຕິບັດ", @@ -975,7 +974,6 @@ "Enable_Auto_Away": "ເປີດຕົວອັດຕະໂນມັດ", "Enable_Desktop_Notifications": "ເຮັດໃຫ້ການແຈ້ງເຕືອນ Desktop", "Enable_Svg_Favicon": "ເປີດໃຊ້ SVG favicon", - "Enable_two-factor_authentication": "ເປີດການກວດສອບສອງປັດໄຈ", "Enabled": "ເປີດການໃຊ້ງານ", "Encrypted_message": "ຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດ", "End_OTR": "End OTR", @@ -1251,12 +1249,12 @@ "Hide": "hide ຫ້ອງ", "Hide_counter": "Hide counter", "Hide_flextab": "ຊ່ອນແຖບດ້ານຂວາດ້ວຍກົດ", - "Hide_Group_Warning": "ທ່ານແນ່ໃຈວ່າທ່ານຕ້ອງການເພື່ອຊ່ອນກຸ່ມ \"%s\"?", - "Hide_Livechat_Warning": "ທ່ານແນ່ໃຈວ່າທ່ານຕ້ອງການເຊື່ອງ livechat ດ້ວຍ \"%s\"?", - "Hide_Private_Warning": "ທ່ານວ່າທ່ານແມ່ນແນ່ໃຈວ່າຕ້ອງການບໍ່ໄດ້ສົນທະນາກັບ \"%s\"?", + "Hide_Group_Warning": "ທ່ານແນ່ໃຈວ່າທ່ານຕ້ອງການເພື່ອຊ່ອນກຸ່ມ \"{{roomName}}\"?", + "Hide_Livechat_Warning": "ທ່ານແນ່ໃຈວ່າທ່ານຕ້ອງການເຊື່ອງ livechat ດ້ວຍ \"{{roomName}}\"?", + "Hide_Private_Warning": "ທ່ານວ່າທ່ານແມ່ນແນ່ໃຈວ່າຕ້ອງການບໍ່ໄດ້ສົນທະນາກັບ \"{{roomName}}\"?", "Hide_roles": "Hide Role", "Hide_room": "hide ຫ້ອງ", - "Hide_Room_Warning": "ທ່ານແນ່ໃຈວ່າທ່ານຕ້ອງການເພື່ອຊ່ອນຫ້ອງ \"%s\"?", + "Hide_Room_Warning": "ທ່ານແນ່ໃຈວ່າທ່ານຕ້ອງການເພື່ອຊ່ອນຫ້ອງ \"{{roomName}}\"?", "Hide_Unread_Room_Status": "ສະແດງສະຖານະຫ້ອງທີ່ບໍ່ໄດ້ອ່ານ", "Hide_usernames": "ຊ່ອນຊື່ຜູ້ໃຊ້", "Highlights": "Highlights", @@ -1557,11 +1555,11 @@ "Lead_capture_email_regex": "ນໍາການຈັບຕົວອີເມວຂອງອີເມວ", "Lead_capture_phone_regex": "ນໍາການຈັບພາບໂທລະສັບ regex", "Leave": "ຫ້ອງໃບ", - "Leave_Group_Warning": "ທ່ານແນ່ໃຈວ່າທ່ານຕ້ອງການທີ່ຈະອອກຈາກກຸ່ມ \"%s\"?", - "Leave_Livechat_Warning": "ທ່ານແນ່ໃຈແນ່ແທ້ວ່າທ່ານຕ້ອງການທີ່ຈະປ່ອຍ livechat ດ້ວຍ \"%s\"?", - "Leave_Private_Warning": "ທ່ານວ່າທ່ານແມ່ນແນ່ໃຈວ່າຕ້ອງການທີ່ຈະອອກຈາກການສົນທະນາທີ່ມີ \"%s\"?", + "Leave_Group_Warning": "ທ່ານແນ່ໃຈວ່າທ່ານຕ້ອງການທີ່ຈະອອກຈາກກຸ່ມ \"{{roomName}}\"?", + "Leave_Livechat_Warning": "ທ່ານແນ່ໃຈແນ່ແທ້ວ່າທ່ານຕ້ອງການທີ່ຈະປ່ອຍ livechat ດ້ວຍ \"{{roomName}}\"?", + "Leave_Private_Warning": "ທ່ານວ່າທ່ານແມ່ນແນ່ໃຈວ່າຕ້ອງການທີ່ຈະອອກຈາກການສົນທະນາທີ່ມີ \"{{roomName}}\"?", "Leave_room": "ຫ້ອງໃບ", - "Leave_Room_Warning": "ທ່ານແນ່ໃຈວ່າທ່ານຕ້ອງການທີ່ຈະອອກຈາກຫ້ອງ \"%s\"?", + "Leave_Room_Warning": "ທ່ານແນ່ໃຈວ່າທ່ານຕ້ອງການທີ່ຈະອອກຈາກຫ້ອງ \"{{roomName}}\"?", "Leave_the_current_channel": "ອອກຈາກຊ່ອງທາງປະຈຸບັນ", "leave-c": "ອອກຈາກຊ່ອງ", "leave-p": "ອອກຈາກກຸ່ມເອກະຊົນ", @@ -2483,7 +2481,6 @@ "Two-factor_authentication": "ການກວດສອບສອງປັດໄຈ", "Two-factor_authentication_disabled": "ການກວດສອບສອງປັດໃຈຖືກປະຕິເສດ", "Two-factor_authentication_enabled": "ການກວດສອບສອງປັດໄຈທີ່ຖືກເປີດໃຊ້", - "Two-factor_authentication_is_currently_disabled": "ການກວດສອບສອງປັດໄຈແມ່ນຖືກປິດໃຊ້ໃນປະຈຸບັນ", "Two-factor_authentication_native_mobile_app_warning": "ຄໍາເຕືອນ: ເມື່ອທ່ານເປີດໃຊ້ງານນີ້, ທ່ານຈະບໍ່ສາມາດເຂົ້າສູ່ລະບົບແອັບຯມືຖື (RocketChat +) ໂດຍໃຊ້ລະຫັດຜ່ານຂອງທ່ານຈົນກວ່າພວກເຂົາຈະປະຕິບັດການ 2FA.", "Type": "ປະເພດ", "Type_your_email": "ພິມອີເມວຂອງທ່ານ", @@ -2790,4 +2787,4 @@ "registration.component.form.sendConfirmationEmail": "ສົ່ງອີເມວການຢືນຢັນ", "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "ການວິເຄາະ" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/lt.i18n.json b/packages/i18n/src/locales/lt.i18n.json index 16a051ed78381..bccad97c9f7c0 100644 --- a/packages/i18n/src/locales/lt.i18n.json +++ b/packages/i18n/src/locales/lt.i18n.json @@ -457,7 +457,6 @@ "Back_to_imports": "Atgal į importus", "Cancel": "Atšaukti", "Cancel_message_input": "Atšaukti", - "Back_to_room": "Atgal į Room", "Back_to_threads": "Atgal į temas", "Cannot_invite_users_to_direct_rooms": "Negalima pakviesti naudotojų nukreipti kambarius", "Cannot_open_conversation_with_yourself": "Negalima tiesioginio pranešimo su savimi", @@ -918,7 +917,6 @@ "Directory": "Katalogas", "Disable_Facebook_integration": "Išjungti \"Facebook\" integraciją", "Disable_Notifications": "Išjungti pranešimus", - "Disable_two-factor_authentication": "Išjungti dviejų veiksnių autentifikavimą", "Disabled": "Neįgalus", "Disallow_reacting": "Neleiskite reaguoti", "Disallow_reacting_Description": "Neleidžia reaguoti", @@ -1011,7 +1009,6 @@ "Enable_Auto_Away": "Įjunkite automatinį išjungimą", "Enable_Desktop_Notifications": "Įgalinti kompiuterio pranešimus", "Enable_Svg_Favicon": "Įgalinti SVG piktogramą", - "Enable_two-factor_authentication": "Įgalinkite dviejų veiksnių autentifikavimą", "Enabled": "Įjungtas", "Encrypted_message": "Užšifruotas pranešimas", "End_OTR": "Baigti OTR", @@ -1273,12 +1270,12 @@ "Hide": "Slėpti kambarį", "Hide_counter": "Slėpti skaitiklį", "Hide_flextab": "Slėpti dešinę šoninę juostą su spustelėjimu", - "Hide_Group_Warning": "Ar tikrai norite paslėpti grupę \"%s\"?", - "Hide_Livechat_Warning": "Ar tikrai norite paslėpti livechat su \"%s\"?", - "Hide_Private_Warning": "Ar tikrai norite paslėpti diskusiją su \"%s\"?", + "Hide_Group_Warning": "Ar tikrai norite paslėpti grupę \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Ar tikrai norite paslėpti livechat su \"{{roomName}}\"?", + "Hide_Private_Warning": "Ar tikrai norite paslėpti diskusiją su \"{{roomName}}\"?", "Hide_roles": "Slėpti vaidmenis", "Hide_room": "Slėpti kambarį", - "Hide_Room_Warning": "Ar tikrai norite paslėpti kambarį \"%s\"?", + "Hide_Room_Warning": "Ar tikrai norite paslėpti kambarį \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Slėpti neskaitytą kambario būseną", "Hide_usernames": "Slėpti vartotojo vardus", "Highlights": "Pabrėžia", @@ -1578,11 +1575,11 @@ "Lead_capture_email_regex": "Švinas surenkite el. Pašto regex", "Lead_capture_phone_regex": "\"Lead\" užfiksuok telefoną regex", "Leave": "Palikite kambarį", - "Leave_Group_Warning": "Ar tikrai norite išeiti iš grupės \"%s\"?", - "Leave_Livechat_Warning": "Ar tikrai norite palikti livechat su \"%s\"?", - "Leave_Private_Warning": "Ar tikrai norite palikti diskusiją su \"%s\"?", + "Leave_Group_Warning": "Ar tikrai norite išeiti iš grupės \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Ar tikrai norite palikti livechat su \"{{roomName}}\"?", + "Leave_Private_Warning": "Ar tikrai norite palikti diskusiją su \"{{roomName}}\"?", "Leave_room": "Palikite kambarį", - "Leave_Room_Warning": "Ar tikrai norite palikti kambarį \"%s\"?", + "Leave_Room_Warning": "Ar tikrai norite palikti kambarį \"{{roomName}}\"?", "Leave_the_current_channel": "Palikite dabartinį kanalą", "leave-c": "Palikti kanalus", "leave-p": "Palikti privačias grupes", @@ -2500,7 +2497,6 @@ "Two-factor_authentication": "Dviejų veiksnių autentifikavimas", "Two-factor_authentication_disabled": "Dviejų veiksnių autentifikavimas išjungtas", "Two-factor_authentication_enabled": "Dviejų veiksnių autentifikavimas įjungtas", - "Two-factor_authentication_is_currently_disabled": "Dviejų veiksnių autentifikavimas šiuo metu yra išjungtas", "Two-factor_authentication_native_mobile_app_warning": "ĮSPĖJIMAS: įjungę šį veiksmą negalėsite prisijungti prie gimtojo mobiliojo ryšio programų (\"Rocket.Chat +\") naudodami savo slaptažodį, kol jie įgyvendins 2FA.", "Type": "Tipas", "Type_your_email": "Įveskite savo el", @@ -2808,4 +2804,4 @@ "registration.component.form.sendConfirmationEmail": "Siųsti patvirtinimo el. Laišką", "Enterprise": "Įmonė", "UpgradeToGetMore_engagement-dashboard_Title": "\"Analytics\"" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/lv.i18n.json b/packages/i18n/src/locales/lv.i18n.json index a97f9850c8cec..09b57737c010b 100644 --- a/packages/i18n/src/locales/lv.i18n.json +++ b/packages/i18n/src/locales/lv.i18n.json @@ -871,7 +871,6 @@ "Directory": "Direktorijs", "Disable_Facebook_integration": "Atspējot Facebook integrāciju", "Disable_Notifications": "Atspējot paziņojumus", - "Disable_two-factor_authentication": "Atspējojiet divu faktoru autentifikāciju", "Disabled": "Atspējots", "Disallow_reacting": "Neļaut reaģēt", "Disallow_reacting_Description": "Neļauj reaģēt", @@ -964,7 +963,6 @@ "Enable_Auto_Away": "Iespējot Auto Away", "Enable_Desktop_Notifications": "Iespējot datora paziņojumus", "Enable_Svg_Favicon": "Iespējot SVG favicon", - "Enable_two-factor_authentication": "Iespējojiet divu faktoru autentifikāciju", "Enabled": "Iespējots", "Encrypted_message": "Šifrēts ziņojums", "End_OTR": "Beigt OTR", @@ -1229,12 +1227,12 @@ "Hide": "Paslēpt istabu", "Hide_counter": "Paslēpt skaitītāju", "Hide_flextab": "Slēpt labo sānu joslu ar klikšķi", - "Hide_Group_Warning": "Vai tiešām vēlaties paslēpt grupu \"%s\"?", - "Hide_Livechat_Warning": "Vai tiešām vēlaties slēpt livechat ar \"%s\"?", - "Hide_Private_Warning": "Vai tiešām vēlaties paslēpt sarunu ar \"%s\"?", + "Hide_Group_Warning": "Vai tiešām vēlaties paslēpt grupu \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Vai tiešām vēlaties slēpt livechat ar \"{{roomName}}\"?", + "Hide_Private_Warning": "Vai tiešām vēlaties paslēpt sarunu ar \"{{roomName}}\"?", "Hide_roles": "Paslēpt lomas", "Hide_room": "Paslēpt istabu", - "Hide_Room_Warning": "Vai tiešām vēlaties paslēpt istabu \"%s\"?", + "Hide_Room_Warning": "Vai tiešām vēlaties paslēpt istabu \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Paslēpt nelasīto istabu statusu", "Hide_usernames": "Paslēpt lietotājvārdus", "Highlights": "Aktualitātes", @@ -1534,11 +1532,11 @@ "Lead_capture_email_regex": "Vadošais uztveršanas e-pasta regex", "Lead_capture_phone_regex": "Vadošais uztveršanas tālruņa regex", "Leave": "Pamest istabu", - "Leave_Group_Warning": "Vai tiešām vēlaties pamest grupu \"%s\"?", - "Leave_Livechat_Warning": "Vai tiešām vēlaties pamest livechat ar \"%s\"?", - "Leave_Private_Warning": "Vai tiešām vēlaties pamest diskusiju ar \"%s\"?", + "Leave_Group_Warning": "Vai tiešām vēlaties pamest grupu \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Vai tiešām vēlaties pamest livechat ar \"{{roomName}}\"?", + "Leave_Private_Warning": "Vai tiešām vēlaties pamest diskusiju ar \"{{roomName}}\"?", "Leave_room": "Pamest istabu", - "Leave_Room_Warning": "Vai tiešām vēlaties pamest istabu \"%s\"?", + "Leave_Room_Warning": "Vai tiešām vēlaties pamest istabu \"{{roomName}}\"?", "Leave_the_current_channel": "Pamest pašreizējo kanālu", "leave-c": "Pamest kanālus", "leave-p": "Pamest privātās grupas", @@ -2453,7 +2451,6 @@ "Two-factor_authentication": "Divu faktoru autentifikācija", "Two-factor_authentication_disabled": "Divu faktoru autentifikācija ir atspējota", "Two-factor_authentication_enabled": "Divu faktoru autentifikācija ir iespējota", - "Two-factor_authentication_is_currently_disabled": "Divu faktoru autentifikācija pašlaik ir atspējota", "Two-factor_authentication_native_mobile_app_warning": "BRĪDINĀJUMS: šo iespējojot, jūs nevarēsiet pieteikties vietējajās mobilā tālruņa lietotnēs (Rocket.Chat +), izmantojot savu paroli, līdz tās ieviesīs 2FA.", "Type": "Veids", "Type_your_email": "Ierakstiet savu e-pastu", @@ -2749,4 +2746,4 @@ "registration.component.form.sendConfirmationEmail": "Nosūtīt apstiprinājuma e-pastu", "Enterprise": "Uzņēmums", "UpgradeToGetMore_engagement-dashboard_Title": "Analītika" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/mn.i18n.json b/packages/i18n/src/locales/mn.i18n.json index 0e55f3579a8b9..ced759b4495f5 100644 --- a/packages/i18n/src/locales/mn.i18n.json +++ b/packages/i18n/src/locales/mn.i18n.json @@ -862,7 +862,6 @@ "Directory": "Лавлах", "Disable_Facebook_integration": "Facebook интеграцийг идэвхгүй болгох", "Disable_Notifications": "Мэдэгдэл идэвхгүй болгох", - "Disable_two-factor_authentication": "Хоёр хүчин зүйлийн баталгаажуулалтыг идэвхгүй болгох", "Disabled": "Хөгжлийн бэрхшээлтэй", "Disallow_reacting": "Хүчингүй болгох", "Disallow_reacting_Description": "Хариултыг зөвшөөрөхгүй байх", @@ -955,7 +954,6 @@ "Enable_Auto_Away": "Авто хөдөлгөөнийг идэвхжүүлнэ үү", "Enable_Desktop_Notifications": "Ширээний мэдэгдлүүдийг идэвхжүүлэх", "Enable_Svg_Favicon": "SVG favicon-г идэвхжүүлэх", - "Enable_two-factor_authentication": "Хоёр хүчин зүйлийн баталгаажуулалтыг идэвхжүүлнэ", "Enabled": "Идэвхжүүлсэн", "Encrypted_message": "Шифрлэгдсэн зурвас", "End_OTR": "OTR төгсгөл", @@ -1214,12 +1212,12 @@ "Hide": "Өрөө нуух", "Hide_counter": "Күүкийг нуух", "Hide_flextab": "Баруун товчлуурыг дарна уу", - "Hide_Group_Warning": "Та \"%s\" бүлгийг нуухыг хүсч байна уу?", - "Hide_Livechat_Warning": "Та livechat-г \"%s\" -тай нуухыг хүсч байгаадаа итгэлтэй байна уу?", - "Hide_Private_Warning": "Та хэлэлцүүлэгийг \"%s\" -г нуухыг хүсч байна уу?", + "Hide_Group_Warning": "Та \"{{roomName}}\" бүлгийг нуухыг хүсч байна уу?", + "Hide_Livechat_Warning": "Та livechat-г \"{{roomName}}\" -тай нуухыг хүсч байгаадаа итгэлтэй байна уу?", + "Hide_Private_Warning": "Та хэлэлцүүлэгийг \"{{roomName}}\" -г нуухыг хүсч байна уу?", "Hide_roles": "Үүрийг нуух", "Hide_room": "Өрөө нуух", - "Hide_Room_Warning": "Та \"%s\" өрөөг нуухыг хүсч байна уу?", + "Hide_Room_Warning": "Та \"{{roomName}}\" өрөөг нуухыг хүсч байна уу?", "Hide_Unread_Room_Status": "Тодорхойгүй уншлагын өрөөний байдлыг нуух", "Hide_usernames": "Хэрэглэгчийн нэрийг нуух", "Highlights": "Онцлогууд", @@ -1518,11 +1516,11 @@ "Lead_capture_email_regex": "Хар тугалга авах имэйл regex", "Lead_capture_phone_regex": "Хар тугалга барих утасны харьцаа", "Leave": "Өрөө орхи", - "Leave_Group_Warning": "Та \"%s\" бүлгийн үлдээхийг хүсч байгаадаа итгэлтэй байна уу?", - "Leave_Livechat_Warning": "Та \"livechat\" -ийг \"%s\" -тай орхих уу гэдэгт итгэлтэй байна уу?", - "Leave_Private_Warning": "Та хэлэлцүүлэгийг \"%s\" -тай орхих уу?", + "Leave_Group_Warning": "Та \"{{roomName}}\" бүлгийн үлдээхийг хүсч байгаадаа итгэлтэй байна уу?", + "Leave_Livechat_Warning": "Та \"livechat\" -ийг \"{{roomName}}\" -тай орхих уу гэдэгт итгэлтэй байна уу?", + "Leave_Private_Warning": "Та хэлэлцүүлэгийг \"{{roomName}}\" -тай орхих уу?", "Leave_room": "Өрөө орхи", - "Leave_Room_Warning": "Та \"%s\" өрөөнөөс гарахыг хүсч байна уу?", + "Leave_Room_Warning": "Та \"{{roomName}}\" өрөөнөөс гарахыг хүсч байна уу?", "Leave_the_current_channel": "Одоогийн сувгийг орхи", "leave-c": "Сувгийг орхи", "leave-p": "Хувийн бүлгүүдийг орхи", @@ -2438,7 +2436,6 @@ "Two-factor_authentication": "Хоёр хүчин зүйлийн баталгаажилт", "Two-factor_authentication_disabled": "Хоёр хүчин зүйлийн баталгаажуулалт идэвхгүй", "Two-factor_authentication_enabled": "Хоёр хүчин зүйлийн баталгаажуулалт идэвхжсэн", - "Two-factor_authentication_is_currently_disabled": "Хоёр хүчин зүйл таньж баталгаажуулах боломжгүй байна", "Two-factor_authentication_native_mobile_app_warning": "АНХААРУУЛГА: Үүнийг идэвхжүүлсний дараа та 2FA-ийг хэрэгжүүлтэл эх хэл дээрх мобайл апп (Rocket.Chat +) дээр нууц үгээ нэвтэрч чадахгүй.", "Type": "Төрөл", "Type_your_email": "Имэйлээ оруулна уу", @@ -2742,4 +2739,4 @@ "registration.component.form.sendConfirmationEmail": "Баталгаажуулах имэйл илгээх", "Enterprise": "Аж ахуйн нэгж", "UpgradeToGetMore_engagement-dashboard_Title": "Аналитик" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ms-MY.i18n.json b/packages/i18n/src/locales/ms-MY.i18n.json index 6c21de2bd1455..caaa359161dc1 100644 --- a/packages/i18n/src/locales/ms-MY.i18n.json +++ b/packages/i18n/src/locales/ms-MY.i18n.json @@ -864,7 +864,6 @@ "Directory": "Direktori", "Disable_Facebook_integration": "Lumpuhkan integrasi Facebook", "Disable_Notifications": "Lumpuhkan Pemberitahuan", - "Disable_two-factor_authentication": "Lumpuhkan pengesahan dua faktor", "Disabled": "Dilumpuhkan", "Disallow_reacting": "Tidak membenarkan Reacting", "Disallow_reacting_Description": "Melarang bertindak balas", @@ -957,7 +956,6 @@ "Enable_Auto_Away": "Dayakan Auto Away", "Enable_Desktop_Notifications": "Mengaktifkan Notifikasi Desktop", "Enable_Svg_Favicon": "Dayakan favicon SVG", - "Enable_two-factor_authentication": "Dayakan pengesahan dua faktor", "Enabled": "didayakan", "Encrypted_message": "mesej disulitkan", "End_OTR": "akhir OTR", @@ -1217,12 +1215,12 @@ "Hide": "Menyembunyikan bilik", "Hide_counter": "Sembunyikan kaunter", "Hide_flextab": "Sembunyikan Sidebar Kanan dengan Klik", - "Hide_Group_Warning": "Adakah anda pasti anda mahu menyembunyikan kumpulan \"%s\"?", - "Hide_Livechat_Warning": "Adakah anda pasti mahu menyembunyikan livechat dengan \"%s\"?", - "Hide_Private_Warning": "Adakah anda pasti anda mahu menyembunyikan perbincangan dengan \"%s\"?", + "Hide_Group_Warning": "Adakah anda pasti anda mahu menyembunyikan kumpulan \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Adakah anda pasti mahu menyembunyikan livechat dengan \"{{roomName}}\"?", + "Hide_Private_Warning": "Adakah anda pasti anda mahu menyembunyikan perbincangan dengan \"{{roomName}}\"?", "Hide_roles": "Sembunyikan Peranan", "Hide_room": "Menyembunyikan bilik", - "Hide_Room_Warning": "Adakah anda pasti anda mahu menyembunyikan bilik \"%s\"?", + "Hide_Room_Warning": "Adakah anda pasti anda mahu menyembunyikan bilik \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Sembunyikan Status Bilik Belum Dibaca", "Hide_usernames": "menyembunyikan nama pengguna", "Highlights": "Sorotan", @@ -1521,11 +1519,11 @@ "Lead_capture_email_regex": "Larangan emel tangkap utama", "Lead_capture_phone_regex": "Regex telefon menangkap utama", "Leave": "Meninggalkan bilik", - "Leave_Group_Warning": "Adakah anda pasti anda mahu meninggalkan kumpulan \"%s\"?", - "Leave_Livechat_Warning": "Adakah anda pasti mahu meninggalkan livechat dengan \"%s\"?", - "Leave_Private_Warning": "Adakah anda pasti anda mahu meninggalkan perbincangan dengan \"%s\"?", + "Leave_Group_Warning": "Adakah anda pasti anda mahu meninggalkan kumpulan \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Adakah anda pasti mahu meninggalkan livechat dengan \"{{roomName}}\"?", + "Leave_Private_Warning": "Adakah anda pasti anda mahu meninggalkan perbincangan dengan \"{{roomName}}\"?", "Leave_room": "Meninggalkan bilik", - "Leave_Room_Warning": "Adakah anda pasti anda mahu meninggalkan bilik \"%s\"?", + "Leave_Room_Warning": "Adakah anda pasti anda mahu meninggalkan bilik \"{{roomName}}\"?", "Leave_the_current_channel": "Tinggalkan saluran semasa", "leave-c": "Tinggalkan Saluran", "leave-p": "Tinggalkan Kumpulan Swasta", @@ -2452,7 +2450,6 @@ "Two-factor_authentication": "Pengesahan dua faktor", "Two-factor_authentication_disabled": "Pengesahan dua faktor dilumpuhkan", "Two-factor_authentication_enabled": "Pengesahan dua faktor didayakan", - "Two-factor_authentication_is_currently_disabled": "Pengesahan dua faktor kini dilumpuhkan", "Two-factor_authentication_native_mobile_app_warning": "AMARAN: Sebaik sahaja anda mendayakannya, anda tidak akan dapat log masuk pada aplikasi mudah alih asli (Rocket.Chat +) menggunakan kata laluan anda sehingga mereka melaksanakan 2FA.", "Type": "Jenis", "Type_your_email": "Taipkan e-mel anda", @@ -2757,4 +2754,4 @@ "registration.component.form.sendConfirmationEmail": "Hantar e-mel pengesahan", "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "Analisis" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/nb.i18n.json b/packages/i18n/src/locales/nb.i18n.json index 48f7af9afd2d7..e48ae441ee9cb 100644 --- a/packages/i18n/src/locales/nb.i18n.json +++ b/packages/i18n/src/locales/nb.i18n.json @@ -859,7 +859,6 @@ "Back_to_imports": "Tilbake til import", "Cancel": "Avbryt", "Cancel_message_input": "Avbryt", - "Back_to_room": "Tilbake til rommet", "Canceled": "Avbrutt", "Back_to_threads": "Tilbake til tråder", "BBB_End_Meeting": "Avslutt møte", @@ -1677,7 +1676,6 @@ "Custom_Script_Logged_Out_Description": "Egendefinert skript som vil kjøre ALLTID og til ENHVER bruker som IKKE er pålogget. f.eks. (hver gang du går inn på påloggingssiden)", "Disable_Notifications": "Deaktiver varslinger", "Custom_Script_On_Logout": "Egendefinert skript for utloggingsflyt", - "Disable_two-factor_authentication": "Deaktiver tofaktorautentisering via TOTP", "Custom_Script_On_Logout_Description": "Egendefinert skript som KUN kjøres på utloggingsflyt", "Disabled": "Deaktivert", "Disallow_reacting": "Ikke tillat reaksjoner", @@ -1877,7 +1875,6 @@ "Disable": "Deaktiver", "EmojiCustomFilesystem": "Egendefinert Emoji-filsystem", "Empty_title": "Tom tittel", - "Disable_two-factor_authentication_email": "Deaktiver tofaktorautentisering via e-post", "Enable": "Aktiver", "Enable_Auto_Away": "Aktiver Auto-borte", "Disabled_apps_admin_message": "Det er én eller flere deaktiverte apper med gyldige lisenser. Gå til {{marketplace}} > {{installed}} for å se gjennom.", @@ -1889,7 +1886,6 @@ "Discussion_info": "Diskusjonsinformasjon", "Enable_Svg_Favicon": "Aktiver SVG-favicon", "Discussion_Description": "Diskusjoner er en ekstra måte å organisere samtaler på, som gjør det mulig å invitere brukere fra eksterne kanaler til å delta i bestemte samtaler.", - "Enable_two-factor_authentication": "Aktiver tofaktorautentisering via TOTP", "Discussion_first_message_disabled_due_to_e2e": "Du kan begynne å sende ende-til-ende-krypterte meldinger i denne diskusjonen etter at den er opprettet.", "Enabled": "Aktivert", "Encrypted": "Kryptert", @@ -2178,7 +2174,6 @@ "Enable_timestamp_description": "Gjengi Unix-tidsstempler inne i meldinger i din lokale (system) tidssone.", "Enable_to_bypass_email_verification": "Aktiver for å omgå e-postbekreftelse", "Facebook_Page": "Facebook-side", - "Enable_two-factor_authentication_email": "Aktiver tofaktorautentisering via e-post", "Enable_unlimited_apps": "Aktiver ubegrensede apper", "Enable_voice_calling": "Aktiver taleanrop", "Encrypted_content_cannot_be_searched": "Kryptert innhold kan ikke søkes.", @@ -2483,16 +2478,16 @@ "You_do_not_have_permission_to_execute_this_command": "Du har ikke nødvendige tillatelser til å utføre kommandoen: `/{{command}}`", "Hide_flextab": "Skjul innholdslinjen ved å klikke utenfor den", "You_have_reached_the_limit_active_costumers_this_month": "Du har nådd grensen for aktive kunder denne måneden", - "Hide_Group_Warning": "Er du sikker på at du vil skjule gruppen \"%s\"?", - "Hide_Livechat_Warning": "Er du sikker på at du vil skjule chatten med \"%s\"?", + "Hide_Group_Warning": "Er du sikker på at du vil skjule gruppen \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Er du sikker på at du vil skjule chatten med \"{{roomName}}\"?", "Estimated_wait_time": "Beregnet ventetid", "Estimated_wait_time_in_minutes": "Beregnet ventetid (tid i minutter)", - "Hide_Private_Warning": "Er du sikker på at du vil skjule diskusjonen med \"%s\"?", + "Hide_Private_Warning": "Er du sikker på at du vil skjule diskusjonen med \"{{roomName}}\"?", "Hide_roles": "Skjul roller", "Event_notifications": "Hendelsesvarsler", "Event_notifications_description": "Ved å deaktivere denne innstillingen forhindrer du appen i å varsle deg om kommende arrangementer.", "Hide_room": "Skjul rom", - "Hide_Room_Warning": "Er du sikker på at du vil skjule kanalen \"%s\"?", + "Hide_Room_Warning": "Er du sikker på at du vil skjule kanalen \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Skjul ulest romstatus", "Hide_usernames": "Skjul brukernavn", "every_30_seconds": "En gang hvert 30. sekund", @@ -3103,12 +3098,12 @@ "Install_anyway": "Installer allikevel ", "Update_anyway": "Oppdater uansett", "Leave": "Forlat", - "Leave_Group_Warning": "Er du sikker på at du vil forlate gruppen \"%s\"?", - "Leave_Livechat_Warning": "Er du sikker på at du vil forlate omnikanalen med \"%s\"?", - "Leave_Private_Warning": "Er du sikker på at du vil forlate diskusjonen med \"%s\"?", + "Leave_Group_Warning": "Er du sikker på at du vil forlate gruppen \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Er du sikker på at du vil forlate omnikanalen med \"{{roomName}}\"?", + "Leave_Private_Warning": "Er du sikker på at du vil forlate diskusjonen med \"{{roomName}}\"?", "Installing": "Installerer", "Leave_room": "Forlat rom", - "Leave_Room_Warning": "Er du sikker på at du vil forlate kanalen \"%s\"?", + "Leave_Room_Warning": "Er du sikker på at du vil forlate kanalen \"{{roomName}}\"?", "Leave_the_current_channel": "Forlat gjeldende kanal", "leave-c": "Forlat kanaler", "Instance": "Forekomst", @@ -5086,7 +5081,6 @@ "Two-factor_authentication_disabled": "Tofaktorautentisering deaktivert", "Room_Status_Open": "Åpen", "Two-factor_authentication_enabled": "Tofaktorautentisering aktivert", - "Two-factor_authentication_is_currently_disabled": "Tofaktorautentisering via TOTP er for øyeblikket deaktivert", "Two-factor_authentication_native_mobile_app_warning": "ADVARSEL: Når du har aktivert dette, vil du ikke kunne logge på de opprinnelige mobilappene (Rocket.Chat+) med passordet ditt før de implementerer 2FA.", "Type": "Type", "Room_updated_successfully": "Rommet ble oppdatert!", @@ -5652,7 +5646,6 @@ "Turn_off_video": "Slå av video", "Two-factor_authentication_via_TOTP": "Tofaktorautentisering via TOTP", "Two-factor_authentication_email": "Tofaktorautentisering via e-post", - "Two-factor_authentication_email_is_currently_disabled": "Tofaktorautentisering via e-post er deaktivert for øyeblikket ", "Types": "Typer", "unable-to-get-file": "Kan ikke hente filen", "Unable_to_load_active_connections": "Kan ikke laste inn aktive tilkoblinger", @@ -6166,7 +6159,6 @@ "You_cant_take_chats_offline": "Du kan ikke ta nye samtaler fordi du er frakoblet", "New_navigation": "Forbedret navigasjonsopplevelse", "New_navigation_description": "Utforsk vår forbedrede navigasjon, designet med ett klart omfang for enkel tilgang til det du trenger. Denne endringen fungerer som grunnlaget for fremtidige fremskritt innen navigasjonsadministrasjon.", - "Workspace_and_user_settings": "Arbeidsområde og brukerinnstillinger", "Sidebar_Sections_Order": "Rekkefølge på sidefeltseksjoner", "Sidebar_Sections_Order_Description": "Velg kategoriene i din foretrukne rekkefølge", "Incoming_Calls": "Innkommende anrop", @@ -6186,11 +6178,8 @@ "Advanced_contact_profile": "Avansert kontaktprofil", "Advanced_contact_profile_description": "Administrer flere e-poster og telefonnumre for en enkelt kontakt, noe som muliggjør en omfattende flerkanalshistorikk som holder deg godt informert og forbedrer kommunikasjonseffektiviteten.", "Add_contact": "Legg til kontakt", - "Add_to_contact_list_manually": "Legg til i kontaktlisten manuelt", - "Add_to_contact_and_enable_verification_description": "Legg til i kontaktlisten manuelt og <1>aktiver verifisering ved hjelp av multifaktorautentisering.", "Ask_enable_advanced_contact_profile": "Be arbeidsområdeadministratoren din om å aktivere avansert kontaktprofil", "close-blocked-room-comment": "Denne kanalen er blokkert", - "Contact_unknown": "Ukjent kontakt", "Review_contact": "Gjennomgå kontakt", "See_conflicts": "Se konflikter", "Conflicts_found": "Konflikter funnet", diff --git a/packages/i18n/src/locales/nl.i18n.json b/packages/i18n/src/locales/nl.i18n.json index d8ab54e9f9059..4123701e3ba99 100644 --- a/packages/i18n/src/locales/nl.i18n.json +++ b/packages/i18n/src/locales/nl.i18n.json @@ -672,7 +672,6 @@ "Back_to_imports": "Terug naar imports", "Cancel": "Annuleren", "Cancel_message_input": "Annuleren", - "Back_to_room": "Terug naar kamer", "Canceled": "Geannuleerd", "Back_to_threads": "Terug naar discussies", "BBB_End_Meeting": "Vergadering beëindigen", @@ -1366,7 +1365,6 @@ "Custom_Script_Logged_Out_Description": "Aangepast script dat ALTIJD en voor ELKE gebruiker die NIET is aangemeld, wordt uitgevoerd. (Bijv. telkens wanneer u de inlogpagina opent)", "Disable_Notifications": "Meldingen uitschakelen", "Custom_Script_On_Logout": "Aangepaste script voor afmeldingsflow", - "Disable_two-factor_authentication": "Schakel tweefactorauthenticatie via TOTP uit", "Custom_Script_On_Logout_Description": "Aangepast script dat ALLEEN wordt uitgevoerd bij het uitvoeren van de afmeldingsflow", "Disabled": "Uitgeschakeld", "Disallow_reacting": "Reageren niet toestaan", @@ -1514,7 +1512,6 @@ "Disable": "Uitschakelen", "EmojiCustomFilesystem": "Aangepast Emoji-bestandssysteem", "Empty_title": "Lege titel", - "Disable_two-factor_authentication_email": "Schakel tweefactorauthenticatie via e-mail uit", "Enable": "Inschakelen", "Enable_Auto_Away": "Schakel automatische afwezigheid in", "Enable_Desktop_Notifications": "Bureaubladmeldingen inschakelen", @@ -1522,7 +1519,6 @@ "Discover_public_channels_and_teams_in_the_workspace_directory": "Ontdek openbare kanalen en teams in de werkruimtemap.", "Discussion": "Discussie", "Enable_Svg_Favicon": "Schakel SVG-favicon in", - "Enable_two-factor_authentication": "Schakel tweefactorauthenticatie via TOTP in", "Discussion_first_message_disabled_due_to_e2e": "U kunt beginnen met het verzenden van end-to-end versleutelde berichten in deze discussie nadat deze werd aangemaakt.", "Enabled": "Ingeschakeld", "Encrypted": "Versleuteld", @@ -1733,7 +1729,6 @@ "External_Queue_Service_URL": "URL externe wachtrijservice", "External_Service": "Externe dienst", "Facebook_Page": "Facebook pagina", - "Enable_two-factor_authentication_email": "Schakel tweefactorauthenticatie via e-mail in", "Encrypted_not_available": "Niet beschikbaar voor openbare kanalen", "False": "Valse", "End": "Einde", @@ -1957,12 +1952,12 @@ "Hide": "Verbergen", "Hide_counter": "Teller verbergen", "Hide_flextab": "Verberg de rechterzijbalk met klik", - "Hide_Group_Warning": "Weet je zeker dat je groep \"%s\" wilt verbergen?", - "Hide_Livechat_Warning": "Weet je zeker dat je de chat met \"%s\" wilt verbergen?", - "Hide_Private_Warning": "Weet je zeker dat je de discussie met \"%s\" wilt verbergen?", + "Hide_Group_Warning": "Weet je zeker dat je groep \"{{roomName}}\" wilt verbergen?", + "Hide_Livechat_Warning": "Weet je zeker dat je de chat met \"{{roomName}}\" wilt verbergen?", + "Hide_Private_Warning": "Weet je zeker dat je de discussie met \"{{roomName}}\" wilt verbergen?", "Hide_roles": "Rollen verbergen", "Hide_room": "Kamer verbergen", - "Hide_Room_Warning": "Weet je zeker dat je het kanaal \"%s\" wilt verbergen?", + "Hide_Room_Warning": "Weet je zeker dat je het kanaal \"{{roomName}}\" wilt verbergen?", "Hide_Unread_Room_Status": "Verberg ongelezen kamerstatus", "Hide_usernames": "Gebruikersnamen verbergen", "Highlights": "Hoogtepunten", @@ -2431,11 +2426,11 @@ "Lead_capture_email_regex": "Regex voor e-mailregistratie voor leads", "Lead_capture_phone_regex": "Lead capture telefoon regex", "Leave": "Verlaten", - "Leave_Group_Warning": "Weet je zeker dat je groep \"%s\" wilt verlaten?", - "Leave_Livechat_Warning": "Weet je zeker dat je het omnichannel wilt verlaten met \"%s\"?", - "Leave_Private_Warning": "Weet je zeker dat je de discussie met \"%s\" wilt verlaten?", + "Leave_Group_Warning": "Weet je zeker dat je groep \"{{roomName}}\" wilt verlaten?", + "Leave_Livechat_Warning": "Weet je zeker dat je het omnichannel wilt verlaten met \"{{roomName}}\"?", + "Leave_Private_Warning": "Weet je zeker dat je de discussie met \"{{roomName}}\" wilt verlaten?", "Leave_room": "Verlaten", - "Leave_Room_Warning": "Weet je zeker dat je het kanaal \"%s\" wilt verlaten?", + "Leave_Room_Warning": "Weet je zeker dat je het kanaal \"{{roomName}}\" wilt verlaten?", "Leave_the_current_channel": "Verlaat het huidige kanaal", "leave-c": "Kanalen verlaten", "Instance": "Instantie", @@ -3993,7 +3988,6 @@ "room_set_read_only": "Kamer is als Alleen-lezen ingesteld door {{user_by}}", "Two-factor_authentication_disabled": "Tweefactorauthenticatie uitgeschakeld", "Two-factor_authentication_enabled": "Tweefactorauthenticatie ingeschakeld", - "Two-factor_authentication_is_currently_disabled": "Tweefactorauthenticatie via TOTP is momenteel uitgeschakeld", "Two-factor_authentication_native_mobile_app_warning": "WAARSCHUWING: Zodra je dit hebt ingeschakeld, kun je niet inloggen op de native mobiele apps (Rocket.Chat+) met je wachtwoord totdat ze de 2FA implementeren.", "Type": "Type", "Room_updated_successfully": "Kamer succesvol bijgewerkt!", @@ -4575,7 +4569,6 @@ "Turn_off_video": "Video uitschakelen", "Two-factor_authentication_via_TOTP": "Tweefactorauthenticatie via TOTP", "Two-factor_authentication_email": "Tweefactorauthenticatie via e-mail", - "Two-factor_authentication_email_is_currently_disabled": "Tweefactorauthentificatie via e-mail is momenteel uitgeschakeld", "typing": "aan het typen", "Types": "Soorten", "Types_and_Distribution": "Types en distributie", @@ -4855,4 +4848,4 @@ "Enterprise": "Onderneming", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "UpgradeToGetMore_auditing_Title": "Bericht auditing" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/nn.i18n.json b/packages/i18n/src/locales/nn.i18n.json index 6d4a60a559807..7f79e1b825117 100644 --- a/packages/i18n/src/locales/nn.i18n.json +++ b/packages/i18n/src/locales/nn.i18n.json @@ -859,7 +859,6 @@ "Back_to_imports": "Tilbake til import", "Cancel": "Avbryt", "Cancel_message_input": "Avbryt", - "Back_to_room": "Tilbake til Room", "Canceled": "Avbrutt", "Back_to_threads": "Tilbake til tråder", "BBB_End_Meeting": "Avslutt møte", @@ -1677,7 +1676,6 @@ "Custom_Script_Logged_Out_Description": "Egendefinert skript som vil kjøre ALLTID og til ENHVER bruker som IKKE er pålogget. f.eks. (hver gang du går inn på påloggingssiden)", "Disable_Notifications": "Deaktiver varslinger", "Custom_Script_On_Logout": "Egendefinert skript for utloggingsflyt", - "Disable_two-factor_authentication": "Deaktiver tofaktorautentisering", "Custom_Script_On_Logout_Description": "Egendefinert skript som KUN kjøres på utloggingsflyt", "Disabled": "Funksjonshemmet", "Disallow_reacting": "Tillat ikke å reagere", @@ -1877,7 +1875,6 @@ "Disable": "Deaktiver", "EmojiCustomFilesystem": "Egendefinert Emoji-filsystem", "Empty_title": "Tom tittel", - "Disable_two-factor_authentication_email": "Deaktiver tofaktorautentisering via e-post", "Enable": "Aktiver", "Enable_Auto_Away": "Aktiver automatisk unna", "Disabled_apps_admin_message": "Det er én eller flere deaktiverte apper med gyldige lisenser. Gå til {{marketplace}} > {{installed}} for å se gjennom.", @@ -1889,7 +1886,6 @@ "Discussion_info": "Diskusjonsinformasjon", "Enable_Svg_Favicon": "Aktiver SVG favicon", "Discussion_Description": "Diskusjoner er en ekstra måte å organisere samtaler på, som gjør det mulig å invitere brukere fra eksterne kanaler til å delta i bestemte samtaler.", - "Enable_two-factor_authentication": "Aktiver tofaktorautentisering", "Discussion_first_message_disabled_due_to_e2e": "Du kan begynne å sende ende-til-ende-krypterte meldinger i denne diskusjonen etter at den er opprettet.", "Enabled": "aktivert", "Encrypted": "Kryptert", @@ -2178,7 +2174,6 @@ "Enable_timestamp_description": "Gjengi Unix-tidsstempler inne i meldinger i din lokale (system) tidssone.", "Enable_to_bypass_email_verification": "Aktiver for å omgå e-postbekreftelse", "Facebook_Page": "Facebook-side", - "Enable_two-factor_authentication_email": "Aktiver tofaktorautentisering via e-post", "Enable_unlimited_apps": "Aktiver ubegrensede apper", "Enable_voice_calling": "Aktiver taleanrop", "Encrypted_content_cannot_be_searched": "Kryptert innhold kan ikke søkes.", @@ -2483,16 +2478,16 @@ "You_do_not_have_permission_to_execute_this_command": "Du har ikke nødvendige tillatelser til å utføre kommandoen: `/{{command}}`", "Hide_flextab": "Skjul høyre sidefelt med klikk", "You_have_reached_the_limit_active_costumers_this_month": "Du har nådd grensen for aktive kunder denne måneden", - "Hide_Group_Warning": "Er du sikker på at du vil gjemme gruppen \"%s\"?", - "Hide_Livechat_Warning": "Er du sikker på at du vil gjemme livechat med \"%s\"?", + "Hide_Group_Warning": "Er du sikker på at du vil gjemme gruppen \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Er du sikker på at du vil gjemme livechat med \"{{roomName}}\"?", "Estimated_wait_time": "Beregnet ventetid", "Estimated_wait_time_in_minutes": "Beregnet ventetid (tid i minutter)", - "Hide_Private_Warning": "Er du sikker på at du vil gjemme diskusjonen med \"%s\"?", + "Hide_Private_Warning": "Er du sikker på at du vil gjemme diskusjonen med \"{{roomName}}\"?", "Hide_roles": "Skjul roller", "Event_notifications": "Hendelsesvarsler", "Event_notifications_description": "Ved å deaktivere denne innstillingen forhindrer du appen i å varsle deg om kommende arrangementer.", "Hide_room": "Skjul rom", - "Hide_Room_Warning": "Er du sikker på at du vil gjemme rommet \"%s\"?", + "Hide_Room_Warning": "Er du sikker på at du vil gjemme rommet \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Skjul ulest romstatus", "Hide_usernames": "Skjul brukernavn", "every_30_seconds": "En gang hvert 30. sekund", @@ -3103,12 +3098,12 @@ "Install_anyway": "Installer allikevel ", "Update_anyway": "Oppdater uansett", "Leave": "Forlat rom", - "Leave_Group_Warning": "Er du sikker på at du vil forlate gruppen \"%s\"?", - "Leave_Livechat_Warning": "Er du sikker på at du vil forlate livechat med \"%s\"?", - "Leave_Private_Warning": "Er du sikker på at du vil legge diskusjonen med \"%s\"?", + "Leave_Group_Warning": "Er du sikker på at du vil forlate gruppen \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Er du sikker på at du vil forlate livechat med \"{{roomName}}\"?", + "Leave_Private_Warning": "Er du sikker på at du vil legge diskusjonen med \"{{roomName}}\"?", "Installing": "Installerer", "Leave_room": "Forlat rom", - "Leave_Room_Warning": "Er du sikker på at du vil forlate rommet \"%s\"?", + "Leave_Room_Warning": "Er du sikker på at du vil forlate rommet \"{{roomName}}\"?", "Leave_the_current_channel": "La den nåværende kanalen gå", "leave-c": "La kanaler", "Instance": "Forekomst", @@ -5086,7 +5081,6 @@ "Two-factor_authentication_disabled": "Tofaktorautentisering deaktivert", "Room_Status_Open": "Åpen", "Two-factor_authentication_enabled": "Tofaktorautentisering aktivert", - "Two-factor_authentication_is_currently_disabled": "Tofaktorautentisering er for øyeblikket deaktivert", "Two-factor_authentication_native_mobile_app_warning": "ADVARSEL: Når du har aktivert dette, vil du ikke kunne logge på de innkommende mobilappene (Rocket.Chat +) ved hjelp av passordet ditt før de implementerer 2FA.", "Type": "Type", "Room_updated_successfully": "Rommet ble oppdatert!", @@ -5652,7 +5646,6 @@ "Turn_off_video": "Slå av video", "Two-factor_authentication_via_TOTP": "Tofaktorautentisering", "Two-factor_authentication_email": "Tofaktorautentisering via e-post", - "Two-factor_authentication_email_is_currently_disabled": "Tofaktorautentisering via e-post er deaktivert for øyeblikket ", "Types": "Typer", "unable-to-get-file": "Kan ikke hente filen", "Unable_to_load_active_connections": "Kan ikke laste inn aktive tilkoblinger", @@ -6166,7 +6159,6 @@ "You_cant_take_chats_offline": "Du kan ikke ta nye samtaler fordi du er frakoblet", "New_navigation": "Forbedret navigasjonsopplevelse", "New_navigation_description": "Utforsk vår forbedrede navigasjon, designet med ett klart omfang for enkel tilgang til det du trenger. Denne endringen fungerer som grunnlaget for fremtidige fremskritt innen navigasjonsadministrasjon.", - "Workspace_and_user_settings": "Arbeidsområde og brukerinnstillinger", "Sidebar_Sections_Order": "Rekkefølge på sidefeltseksjoner", "Sidebar_Sections_Order_Description": "Velg kategoriene i din foretrukne rekkefølge", "Incoming_Calls": "Innkommende anrop", @@ -6186,11 +6178,8 @@ "Advanced_contact_profile": "Avansert kontaktprofil", "Advanced_contact_profile_description": "Administrer flere e-poster og telefonnumre for en enkelt kontakt, noe som muliggjør en omfattende flerkanalshistorikk som holder deg godt informert og forbedrer kommunikasjonseffektiviteten.", "Add_contact": "Legg til kontakt", - "Add_to_contact_list_manually": "Legg til i kontaktlisten manuelt", - "Add_to_contact_and_enable_verification_description": "Legg til i kontaktlisten manuelt og <1>aktiver verifisering ved hjelp av multifaktorautentisering.", "Ask_enable_advanced_contact_profile": "Be arbeidsområdeadministratoren din om å aktivere avansert kontaktprofil", "close-blocked-room-comment": "Denne kanalen er blokkert", - "Contact_unknown": "Ukjent kontakt", "Review_contact": "Gjennomgå kontakt", "See_conflicts": "Se konflikter", "Conflicts_found": "Konflikter funnet", @@ -6199,4 +6188,4 @@ "Recent": "Nylig", "On_All_Contacts": "På alle kontakter", "Once": "En gang" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/pl.i18n.json b/packages/i18n/src/locales/pl.i18n.json index 843c5ce238c7d..353b2a990f077 100644 --- a/packages/i18n/src/locales/pl.i18n.json +++ b/packages/i18n/src/locales/pl.i18n.json @@ -648,7 +648,6 @@ "Back_to_integrations": "Wróć do integracji", "Back_to_login": "Wróć do strony logowania", "Back_to_permissions": "Wróć do uprawnień", - "Back_to_room": "Wróć do pokoju Room", "Back_to_threads": "Wróć do wątków", "Backup_codes": "Kody zapasowe", "Be_the_first_to_join": "Bądź pierwszym, który dołączy", @@ -1433,8 +1432,6 @@ "Disable": "Wyłącz", "Disable_Facebook_integration": "Wyłącz integrację z Facebookiem", "Disable_Notifications": "Wyłącz powiadomienia", - "Disable_two-factor_authentication": "Wyłącz uwierzytelnianie dwuskładnikowe za pomocą TOTP", - "Disable_two-factor_authentication_email": "Wyłącz uwierzytelnienie dwuskładnikowe za pomocą poczty e-mail", "Disabled": "Wyłączone", "Disabled_E2E_Encryption_for_this_room": "wyłączone szyfrowanie E2E dla tego pokoju", "Disallow_reacting": "Nie zezwalaj na reagowanie", @@ -1578,8 +1575,6 @@ "Enable_business_hours": "Włącz godziny pracy", "Enable_inquiry_fetch_by_stream": "Włączanie pobierania danych zapytania z serwera za pomocą strumienia", "Enable_omnichannel_auto_close_abandoned_rooms": "Włącz automatyczne zamykanie pokoi opuszczonych przez odwiedzającego", - "Enable_two-factor_authentication": "Włącz uwierzytelnianie dwuskładnikowe", - "Enable_two-factor_authentication_email": "Włącz uwierzytelnienie dwuskładnikowe przez e-mail", "Enabled": "Włączone", "Enabled_E2E_Encryption_for_this_room": "włączone szyfrowanie E2E dla tego pokoju", "Encrypted": "Szyfrowane", @@ -1908,10 +1903,10 @@ "Hi_username": "Witaj [name]", "Hidden": "Ukryty", "Hide": "Ukryj", - "Hide_Group_Warning": "Czy na pewno chcesz ukryć grupę \"%s\"?", - "Hide_Livechat_Warning": "Czy na pewno chcesz ukryć livechat z \"%s\"?", - "Hide_Private_Warning": "Czy na pewno chcesz ukryć dyskusję z \"%s\"?", - "Hide_Room_Warning": "Czy na pewno chcesz ukryć pokój \"%s\"?", + "Hide_Group_Warning": "Czy na pewno chcesz ukryć grupę \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Czy na pewno chcesz ukryć livechat z \"{{roomName}}\"?", + "Hide_Private_Warning": "Czy na pewno chcesz ukryć dyskusję z \"{{roomName}}\"?", + "Hide_Room_Warning": "Czy na pewno chcesz ukryć pokój \"{{roomName}}\"?", "Hide_System_Messages": "Ukryj wiadomości systemowe", "Hide_Unread_Room_Status": "Ukryj nieprzeczytany stan pokoju", "Hide_counter": "Ukryj licznik", @@ -2399,10 +2394,10 @@ "Learn_more_about_accessibility": "Dowiedz się więcej o naszym zaangażowaniu w dostępność tutaj:", "Least_recent_updated": "Najstarsza aktualizacja", "Leave": "Opuść", - "Leave_Group_Warning": "Czy na pewno chcesz opuścić grupę \"%s\"?", - "Leave_Livechat_Warning": "Czy na pewno chcesz opuścić livechat za pomocą \"%s\"?", - "Leave_Private_Warning": "Czy na pewno chcesz opuścić dyskusję z \"%s\"?", - "Leave_Room_Warning": "Czy na pewno chcesz opuścić pokój \"%s\"?", + "Leave_Group_Warning": "Czy na pewno chcesz opuścić grupę \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Czy na pewno chcesz opuścić livechat za pomocą \"{{roomName}}\"?", + "Leave_Private_Warning": "Czy na pewno chcesz opuścić dyskusję z \"{{roomName}}\"?", + "Leave_Room_Warning": "Czy na pewno chcesz opuścić pokój \"{{roomName}}\"?", "Leave_a_comment": "Zostaw komentarz", "Leave_room": "Opuść pokój", "Leave_the_current_channel": "Opuść aktualny kanał", @@ -4078,9 +4073,7 @@ "Two-factor_authentication": "Uwierzytelnianie dwuskładnikowe", "Two-factor_authentication_disabled": "Wyłączono uwierzytelnianie dwuskładnikowe", "Two-factor_authentication_email": "Dwustopniowa autoryzacja poprzez email", - "Two-factor_authentication_email_is_currently_disabled": "Dwustopniowa autoryzacja poprzez email jest aktualnie wyłączona", "Two-factor_authentication_enabled": "Włączono uwierzytelnianie dwuskładnikowe", - "Two-factor_authentication_is_currently_disabled": "Obecnie uwierzytelnianie dwuskładnikowe jest wyłączone", "Two-factor_authentication_native_mobile_app_warning": "OSTRZEŻENIE: Po włączeniu tej opcji nie będziesz mógł zalogować się w macierzystych aplikacjach mobilnych (Rocket.Chat+) za pomocą hasła, dopóki nie wdrożą 2FA.", "Two-factor_authentication_via_TOTP": "Uwierzytelnianie dwuskładnikowe", "Type": "Rodzaj", diff --git a/packages/i18n/src/locales/pt-BR.i18n.json b/packages/i18n/src/locales/pt-BR.i18n.json index 3f6d61ee40fd6..c1fbb7c6095d2 100644 --- a/packages/i18n/src/locales/pt-BR.i18n.json +++ b/packages/i18n/src/locales/pt-BR.i18n.json @@ -72,6 +72,7 @@ "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "Um novo proprietário será atribuído automaticamente a estas {{count}} salas:
      {{rooms}}.", "A_secure_and_highly_private_self-managed_solution_for_conference_calls": "Uma solução autogerenciada segura e altamente privada para chamadas em conferência.", "A_workspace_admin_needs_to_install_and_configure_a_conference_call_app": "Um administrador do workspace precisa instalar e configurar um aplicativo de chamada de vídeo.", + "Actor": "Autor", "Accept": "Aceitar", "Accept_Call": "Aceitar chamada", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Aceitar solicitações de omnichannel de entrada mesmo que não tenham agentes online", @@ -324,8 +325,6 @@ "Add_members": "Adicionar membros", "Add_monitor": "Adicionar monitor", "Add_phone": "Adicionar número de telefone", - "Add_to_contact_and_enable_verification_description": "Adicione o contato na lista manualmente e <1>habilite a verificação usando autenticação de múltiplos fatores.", - "Add_to_contact_list_manually": "Adicione o contato na lista manualmente", "Add_user": "Adicionar usuário", "Add_users": "Adicionar usuários", "Added__username__to_team": "@{{user_added}} adicionado a esta equipe", @@ -402,6 +401,7 @@ "Answer_call": "Receber chamada", "Apiai_Key": "Api.ai Key", "Apiai_Language": "Idioma Api.ai", + "App": "Aplicativo", "App_Details": "Detalhes do aplicativo", "App_Info": "Informação do aplicativo", "App_Information": "Informações do aplicativo", @@ -631,7 +631,6 @@ "Back_to_integrations": "Voltar para integrações", "Back_to_login": "Voltar para o login", "Back_to_permissions": "Voltar para permissões", - "Back_to_room": "Voltar para Sala", "Back_to_threads": "Voltar para tópicos", "Backup_codes": "Códigos de backup", "Belongs_To": "Pertence a", @@ -761,6 +760,8 @@ "Categories*": "Categorias*", "Certificates_and_Keys": "Certificados e chaves", "Change_Room_Type": "Mudando o Tipo de Sala", + "Changed_from": "Mudou de", + "Changed_to": "Mudou para", "Changing_email": "Alterando e-mail", "Channel": "Canal", "Channel_Archived": "Canal com o nome `#%s` foi arquivado com sucesso", @@ -928,7 +929,6 @@ "Contact_identification": "Identificação de contato", "Contact_not_found": "Contato não encontrado", "Contact_unblocked": "Contato desbloqueado", - "Contact_unknown": "Contato desconhecido", "Contacts": "Contatos", "Contains_Security_Fixes": "Contém correções de segurança", "Content": "Conteúdo", @@ -1375,8 +1375,6 @@ "Disable": "Desabilitar", "Disable_Facebook_integration": "Desabilitar a integração do Facebook", "Disable_Notifications": "Desativar as notificações", - "Disable_two-factor_authentication": "Desativar a autenticação de dois fatores por TOTP", - "Disable_two-factor_authentication_email": "Desativar a autenticação de dois fatores por e-mail", "Disable_voice_calling": "Desabilitar chamadas de voz", "Disabled": "Desabilitado", "Disabled_E2E_Encryption_for_this_room": "Encriptação E2E desabilitada para essa sala", @@ -1403,6 +1401,7 @@ "Display_setting_permissions": "Exibir permissões para alterar configurações", "Display_unread_counter": "Exibir número de mensagens não lidas", "Displays_action_text": "Exibe texto da ação", + "Dismiss": "Dispensar", "Do_It_Later": "Fazer depois", "Do_Nothing": "Não fazer nada", "Do_not_display_unread_counter": "Não exibir nenhum contador desse canal", @@ -1511,8 +1510,6 @@ "Enable_encryption": "Ativar criptografia", "Enable_inquiry_fetch_by_stream": "Habilitar carga de dados de novas pesquisas de omnichannel utilizando stream", "Enable_omnichannel_auto_close_abandoned_rooms": "Habilitar o fechamento automático de salas abandonadas pelo visitante", - "Enable_two-factor_authentication": "Ativar autenticação de dois fatores por TOTP", - "Enable_two-factor_authentication_email": "Ativar autenticação de dois fatores por e-mail", "Enable_voice_calling": "Habilitar chamadas de voz", "Enabled": "Ativado", "Enabled_E2E_Encryption_for_this_room": "Encriptação E2E habilitada para essa sala", @@ -1817,10 +1814,10 @@ "Hi_username": "Oi [name]", "Hidden": "Oculto", "Hide": "Ocultar", - "Hide_Group_Warning": "Tem certeza de que deseja ocultar o grupo \"%s\"?", - "Hide_Livechat_Warning": "Tem certeza de que deseja ocultar a conversa com \"%s\"?", - "Hide_Private_Warning": "Tem certeza de que deseja ocultar a conversa com \"%s\"?", - "Hide_Room_Warning": "Tem certeza de que deseja ocultar o canal \"%s\"?", + "Hide_Group_Warning": "Tem certeza de que deseja ocultar o grupo \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Tem certeza de que deseja ocultar a conversa com \"{{roomName}}\"?", + "Hide_Private_Warning": "Tem certeza de que deseja ocultar a conversa com \"{{roomName}}\"?", + "Hide_Room_Warning": "Tem certeza de que deseja ocultar o canal \"{{roomName}}\"?", "Hide_System_Messages": "Ocultar mensagens do sistema", "Hide_Unread_Room_Status": "Ocultar status da sala não lida", "Hide_counter": "Ocultar contador", @@ -2297,10 +2294,10 @@ "Lead_capture_phone_regex": "Regex de telefone de captura de lead", "Least_recent_updated": "Atualizado há mais tempo", "Leave": "Sair", - "Leave_Group_Warning": "Tem certeza de que quer sair do grupo \"%s\"?", - "Leave_Livechat_Warning": "Tem certeza de que deseja sair do omnichannel com \"%s\"?", - "Leave_Private_Warning": "Tem certeza de que quer sair da conversa com \"%s\"?", - "Leave_Room_Warning": "Tem certeza de que deseja sair do canal \"%s\"?", + "Leave_Group_Warning": "Tem certeza de que quer sair do grupo \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Tem certeza de que deseja sair do omnichannel com \"{{roomName}}\"?", + "Leave_Private_Warning": "Tem certeza de que quer sair da conversa com \"{{roomName}}\"?", + "Leave_Room_Warning": "Tem certeza de que deseja sair do canal \"{{roomName}}\"?", "Leave_a_comment": "Deixe um comentário", "Leave_room": "Sair", "Leave_the_current_channel": "Sai do canal atual", @@ -3531,7 +3528,9 @@ "Set_as_owner": "Definir como proprietário", "Set_random_password_and_send_by_email": "Definir senha aleatória e enviar por e-mail", "Settings": "Configurações", + "Setting": "Configuração", "Settings_updated": "Configurações atualizadas", + "Setting_change": "Configuração alterada", "Setup_Wizard": "Assistente de configuração", "Setup_Wizard_Description": "Informações básicas do seu workspace como organização, nome e país.", "Setup_Wizard_Info": "Vamos apoiar na configuração do seu primeiro usuário administrador, na configuração da sua organização e no registro do servidor, para que possa receber notificações push gratuitas e muito mais.", @@ -3702,6 +3701,7 @@ "Sync_in_progress": "Sincronização em andamento", "Sync_success": "Sincronizado com sucesso", "System_messages": "Mensagens do sistema", + "System": "Sistema", "TOTP Invalid [totp-invalid]": "Código ou senha invalida", "TOTP_Reset_Other_Key_Warning": "Redefinir o TOTP de dois fatores atual vai desconectar o usuário. O usuário poderá definir os dois fatores mais tarde novamente.", "TOTP_reset_email": "Notificação de redefinição TOTP de dois fatores", @@ -3928,9 +3928,7 @@ "Two-factor_authentication": "Autenticação de dois fatores por TOTP", "Two-factor_authentication_disabled": "Autenticação de dois fatores desativada", "Two-factor_authentication_email": "Autenticação de dois fatores por e-mail", - "Two-factor_authentication_email_is_currently_disabled": "A autenticação de dois fatores por e-mail está atualmente desativada", "Two-factor_authentication_enabled": "Autenticação de dois fatores ativada", - "Two-factor_authentication_is_currently_disabled": "A autenticação de dois fatores por TOTP está atualmente desativada", "Two-factor_authentication_native_mobile_app_warning": "AVISO: Depois de ativar esta opção, você não poderá fazer login nos aplicativos móveis nativos (Rocket.Chat +) usando sua senha até implementar o 2FA.", "Two-factor_authentication_via_TOTP": "Autenticação de dois fatores por TOTP", "Type": "Tipo", @@ -3972,6 +3970,7 @@ "Uninstall": "Desinstalar", "Unit_removed": "Unidade removida", "Unknown_Import_State": "Estado de importação desconhecido", + "Unknown_contact_callout_description": "Contato desconhecido. Este contato não está na lista de contatos.", "Unlimited": "Ilimitado", "Unmute": "Ativar o som", "Unmute_microphone": "Ativar o som do microfone", diff --git a/packages/i18n/src/locales/pt.i18n.json b/packages/i18n/src/locales/pt.i18n.json index 0eeda558f10a7..a07cd012345df 100644 --- a/packages/i18n/src/locales/pt.i18n.json +++ b/packages/i18n/src/locales/pt.i18n.json @@ -502,7 +502,6 @@ "call-management": "Gestão de Chamadas", "Cancel": "Cancelar", "Cancel_message_input": "Cancelar", - "Back_to_room": "Regressar ao canal", "Canceled": "Cancelado", "Cannot_invite_users_to_direct_rooms": "Não é possível convidar pessoas para salas diretas", "Cannot_open_conversation_with_yourself": "Não é possível fazer uma mensagem directa com a mesma origem", @@ -1030,7 +1029,6 @@ "Directory": "Directório", "Disable_Facebook_integration": "Desactivar a integração com o Facebook", "Disable_Notifications": "Desactivar notificações", - "Disable_two-factor_authentication": "Desactivar autenticação de dois passos", "Disabled": "Desactivado", "Disallow_reacting": "Não permitir reagir", "Disallow_reacting_Description": "Não permite reagir", @@ -1146,7 +1144,6 @@ "Enable_Desktop_Notifications": "Habilitar Notificações de Ambiente de trabalho", "Discussion": "Discussão", "Enable_Svg_Favicon": "Habilitar favicon SVG", - "Enable_two-factor_authentication": "Habilitar autenticação de dois passos", "Enabled": "Activo", "Encrypted": "Criptografado", "Encrypted_channel_Description": "Canal criptografado de Ponta a ponta. A pesquisa não funcionará com canais criptografados e as notificações podem não mostrar o conteúdo das mensagens.", @@ -1457,12 +1454,12 @@ "Hide": "Ocultar", "Hide_counter": "Ocultar contador", "Hide_flextab": "Esconder barra da direita com clique", - "Hide_Group_Warning": "Tem certeza de que deseja ocultar o grupo \"%s\"?", - "Hide_Livechat_Warning": "Tem certeza de que deseja esconder o livechat com \"%s\"?", - "Hide_Private_Warning": "Tem certeza de que deseja ocultar a conversa com \"%s\"?", + "Hide_Group_Warning": "Tem certeza de que deseja ocultar o grupo \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Tem certeza de que deseja esconder o livechat com \"{{roomName}}\"?", + "Hide_Private_Warning": "Tem certeza de que deseja ocultar a conversa com \"{{roomName}}\"?", "Hide_roles": "Ocultar funções", "Hide_room": "Esconder sala", - "Hide_Room_Warning": "Tem certeza de que deseja ocultar a sala \"%s\"?", + "Hide_Room_Warning": "Tem certeza de que deseja ocultar a sala \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Ocultar status de sala não lida", "Hide_usernames": "Esconder nomes de utilizador", "Highlights": "Destaques", @@ -1783,11 +1780,11 @@ "Lead_capture_email_regex": "Regex de e-mail de captura de chumbo", "Lead_capture_phone_regex": "Regex de telefone de captura de chumbo", "Leave": "Sair", - "Leave_Group_Warning": "Tem a certeza de que quer sair do grupo \"%s\"?", - "Leave_Livechat_Warning": "Tem a certeza de que deseja deixar o Livechat com \"%s\"?", - "Leave_Private_Warning": "Tem a certeza de que quer sair da discussão com \"%s\"?", + "Leave_Group_Warning": "Tem a certeza de que quer sair do grupo \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Tem a certeza de que deseja deixar o Livechat com \"{{roomName}}\"?", + "Leave_Private_Warning": "Tem a certeza de que quer sair da discussão com \"{{roomName}}\"?", "Leave_room": "Sair da sala", - "Leave_Room_Warning": "Tem a certeza de que deseja sair da sala \"%s\"?", + "Leave_Room_Warning": "Tem a certeza de que deseja sair da sala \"{{roomName}}\"?", "Leave_the_current_channel": "Sai deste canal", "leave-c": "Sair dos canais", "leave-p": "Sair dos grupos privados", @@ -2828,7 +2825,6 @@ "Two-factor_authentication": "Autenticação em dois passos", "Two-factor_authentication_disabled": "Autenticação em dois passos desactivada", "Two-factor_authentication_enabled": "Autenticação em dois passos activada", - "Two-factor_authentication_is_currently_disabled": "A autenticação em dois passos está actualmente desactivada", "Two-factor_authentication_native_mobile_app_warning": "AVISO: depois de habilitar esta opção, não poderá fazer login nas aplicações móveis nativas (Rocket.Chat +) usando a sua senha até ser implementado o 2FA.", "Type": "Escreva", "Type_your_email": "Escreva o seu email", @@ -3169,4 +3165,4 @@ "registration.component.form.sendConfirmationEmail": "Enviar email de confirmação", "Enterprise": "Empreendimento", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ro.i18n.json b/packages/i18n/src/locales/ro.i18n.json index 2e530dd5a36fc..e751947dcaa0f 100644 --- a/packages/i18n/src/locales/ro.i18n.json +++ b/packages/i18n/src/locales/ro.i18n.json @@ -864,7 +864,6 @@ "Directory": "Director", "Disable_Facebook_integration": "Dezactivați integrarea Facebook", "Disable_Notifications": "Dezactivați notificările", - "Disable_two-factor_authentication": "Dezactivați autentificarea cu două factori", "Disabled": "Pentru persoane cu handicap", "Disallow_reacting": "Nu permiteți reacționarea", "Disallow_reacting_Description": "Nu permite reacționarea", @@ -957,7 +956,6 @@ "Enable_Auto_Away": "Activați Auto Away", "Enable_Desktop_Notifications": "Activați notificări pe desktop", "Enable_Svg_Favicon": "Activați faviconul SVG", - "Enable_two-factor_authentication": "Activați autentificarea cu două factori", "Enabled": "Activat", "Encrypted_message": "mesajul criptata", "End_OTR": "Sfârșitul OTR", @@ -1218,7 +1216,7 @@ "Hide_counter": "Ascundeți contorul", "Hide_flextab": "Ascundeți bara laterală din dreapta cu clic", "Hide_Group_Warning": "Sunetți sigur că vreți să ascundeți grupul?", - "Hide_Livechat_Warning": "Sigur doriți să ascundeți livechat-ul cu \"%s\"?", + "Hide_Livechat_Warning": "Sigur doriți să ascundeți livechat-ul cu \"{{roomName}}\"?", "Hide_Private_Warning": "Sunteți sigur că vreți să ascundeți discuția?", "Hide_roles": "Ascundeți rolurile", "Hide_room": "Ascunde camera", @@ -1523,7 +1521,7 @@ "Lead_capture_phone_regex": "Plătește regex telefoanele de captare", "Leave": "Părăsește camera", "Leave_Group_Warning": "Sunteți sigur că vreți să părăsiți grupul?", - "Leave_Livechat_Warning": "Sigur doriți să părăsiți livechat-ul cu \"%s\"?", + "Leave_Livechat_Warning": "Sigur doriți să părăsiți livechat-ul cu \"{{roomName}}\"?", "Leave_Private_Warning": "Sunteți sigur că vreți să părăsiți discuția?", "Leave_room": "Părăsește camera", "Leave_Room_Warning": "Sunteți sigur că vreți să părăsiți camera?", @@ -2445,7 +2443,6 @@ "Two-factor_authentication": "Autentificare în două factori", "Two-factor_authentication_disabled": "Autentificarea cu două factori este dezactivată", "Two-factor_authentication_enabled": "Autentificare cu două factori activată", - "Two-factor_authentication_is_currently_disabled": "Verificarea în doi factori este dezactivată în prezent", "Two-factor_authentication_native_mobile_app_warning": "AVERTISMENT: Odată ce activezi acest lucru, nu vei putea să te autentifici pe aplicațiile mobile native (Rocket.Chat +) folosind parola până când implementează 2FA.", "Type": "Tip", "Type_your_email": "Tastați un e-mail", @@ -2747,4 +2744,4 @@ "registration.component.form.sendConfirmationEmail": "Trimite email de confirmare", "Enterprise": "Afacere", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ru.i18n.json b/packages/i18n/src/locales/ru.i18n.json index 1b5c7929818e6..e6035981eb98b 100644 --- a/packages/i18n/src/locales/ru.i18n.json +++ b/packages/i18n/src/locales/ru.i18n.json @@ -656,7 +656,6 @@ "Back_to_integrations": "Назад", "Back_to_login": "На страницу авторизации", "Back_to_permissions": "Назад к настройкам прав", - "Back_to_room": "Вернуться в Room", "Back_to_threads": "Назад к тредам", "Backup_codes": "Коды резервного копирования", "Be_the_first_to_join": "Будьте первым, кто присоединится", @@ -1411,8 +1410,6 @@ "Disable": "Отключить", "Disable_Facebook_integration": "Отключить интеграцию с Facebook", "Disable_Notifications": "Отключить уведомления", - "Disable_two-factor_authentication": "Выключить двухфакторную аутентификацию", - "Disable_two-factor_authentication_email": "Отключить двухфакторную аутентификацию по Email", "Disabled": "Отключено", "Disabled_E2E_Encryption_for_this_room": "отключено шифрование E2E для этой комнаты", "Disallow_reacting": "Запретить реакции", @@ -1546,8 +1543,6 @@ "Enable_Svg_Favicon": "Включить векторную иконку (SVG favicon)", "Enable_inquiry_fetch_by_stream": "Включить сбор данных по запросу с сервера с помощью потока", "Enable_omnichannel_auto_close_abandoned_rooms": "Включить автоматическое закрытие чатов, покинутых посетителями", - "Enable_two-factor_authentication": "Включить двухфакторную авторизацию", - "Enable_two-factor_authentication_email": "Включить двухфакторную аутентификацию по электронной почте", "Enabled": "Включено", "Enabled_E2E_Encryption_for_this_room": "включено шифрование E2E для этой комнаты", "Encrypted": "Зашифрованный", @@ -1848,11 +1843,11 @@ "Hi_username": "Привет [name]", "Hidden": "Скрытый", "Hide": "Скрыть", - "Hide_Group_Warning": "Вы уверены, что хотите спрятать группу \"%s\"?", - "Hide_Livechat_Warning": "Вы уверены, что хотите спрятать Livechat с \"%s\"?", + "Hide_Group_Warning": "Вы уверены, что хотите спрятать группу \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Вы уверены, что хотите спрятать Livechat с \"{{roomName}}\"?", "Hide_On_Workspace": "Скрыть в рабочем пространстве", - "Hide_Private_Warning": "Вы уверены, что хотите спрятать беседу с \"%s\"?", - "Hide_Room_Warning": "Вы уверены, что хотите спрятать комнату \"%s\"?", + "Hide_Private_Warning": "Вы уверены, что хотите спрятать беседу с \"{{roomName}}\"?", + "Hide_Room_Warning": "Вы уверены, что хотите спрятать комнату \"{{roomName}}\"?", "Hide_System_Messages": "Скрыть Системные Сообщения", "Hide_Unread_Room_Status": "Скрыть статус \"непрочитанно\" у комнаты", "Hide_counter": "Скрыть счетчик", @@ -2324,10 +2319,10 @@ "Learn_how_to_unlock_the_myriad_possibilities_of_rocket_chat": "Узнайте о всех возможностях Rocket.Chat.", "Least_recent_updated": "Наименее недавнее обновление", "Leave": "Покинуть", - "Leave_Group_Warning": "Вы уверены, что хотите покинуть группу \"%s\"?", - "Leave_Livechat_Warning": "Вы уверены, что хотите покинуть Livechat с \"%s\"?", - "Leave_Private_Warning": "Вы уверены, что хотите покинуть беседу с \"%s\"?", - "Leave_Room_Warning": "Вы уверены, что хотите покинуть комнату \"%s\"?", + "Leave_Group_Warning": "Вы уверены, что хотите покинуть группу \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Вы уверены, что хотите покинуть Livechat с \"{{roomName}}\"?", + "Leave_Private_Warning": "Вы уверены, что хотите покинуть беседу с \"{{roomName}}\"?", + "Leave_Room_Warning": "Вы уверены, что хотите покинуть комнату \"{{roomName}}\"?", "Leave_a_comment": "Оставить комментарий", "Leave_room": "Покинуть чат", "Leave_the_current_channel": "Покинуть текущий канал", @@ -3905,9 +3900,7 @@ "Two-factor_authentication": "Двухфакторная аутентификация", "Two-factor_authentication_disabled": "Двухфакторная аутентификация выключена", "Two-factor_authentication_email": "Двухфакторная аутентификация по электронной почте", - "Two-factor_authentication_email_is_currently_disabled": "Двухфакторная аутентификация по Email в настоящее время отключена", "Two-factor_authentication_enabled": "Двухфакторная аутентификация включена", - "Two-factor_authentication_is_currently_disabled": "Двухфакторная аутентификация сейчас выключена", "Two-factor_authentication_native_mobile_app_warning": "Внимание: Если вы активируете эту функцию, то уже больше не сможете использовать нативные мобильные приложения (Rocket.Chat+) до момента поддержки ими 2FA.", "Two-factor_authentication_via_TOTP": "Двухфакторная аутентификация", "Type": "Тип", diff --git a/packages/i18n/src/locales/sk-SK.i18n.json b/packages/i18n/src/locales/sk-SK.i18n.json index 1aa632012aa4a..f5693e348ad8b 100644 --- a/packages/i18n/src/locales/sk-SK.i18n.json +++ b/packages/i18n/src/locales/sk-SK.i18n.json @@ -869,7 +869,6 @@ "Directory": "Adresár", "Disable_Facebook_integration": "Zakázať integráciu do služby Facebook", "Disable_Notifications": "Vypnutie upozornení", - "Disable_two-factor_authentication": "Zakázať dvojfaktorové overenie", "Disabled": "Vypnuté", "Disallow_reacting": "Zakázať reakciu", "Disallow_reacting_Description": "Zakáže reakciu", @@ -962,7 +961,6 @@ "Enable_Auto_Away": "Povoliť automatické prechod", "Enable_Desktop_Notifications": "Povoliť upozornenia na pracovnej ploche", "Enable_Svg_Favicon": "Povoliť favicon SVG", - "Enable_two-factor_authentication": "Povoliť dvojfaktorové overovanie", "Enabled": "povolené", "Encrypted_message": "Šifrovaná správa", "End_OTR": "Ukončiť OTR", @@ -1228,12 +1226,12 @@ "Hide": "Skryť miestnosť", "Hide_counter": "Skryť počítadlo", "Hide_flextab": "Skryť pravý bočný panel kliknutím", - "Hide_Group_Warning": "Naozaj chcete skryť skupinu \"%s\"?", - "Hide_Livechat_Warning": "Naozaj chcete skryť livechat s \"%s\"?", - "Hide_Private_Warning": "Naozaj chcete diskusiu skryť pomocou \"%s\"?", + "Hide_Group_Warning": "Naozaj chcete skryť skupinu \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Naozaj chcete skryť livechat s \"{{roomName}}\"?", + "Hide_Private_Warning": "Naozaj chcete diskusiu skryť pomocou \"{{roomName}}\"?", "Hide_roles": "Skryť role", "Hide_room": "Skryť miestnosť", - "Hide_Room_Warning": "Naozaj chcete skryť miestnosť \"%s\"?", + "Hide_Room_Warning": "Naozaj chcete skryť miestnosť \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Skryť nepresaný stav miestnosti", "Hide_usernames": "Skryť používateľské mená", "Highlights": "prednosti", @@ -1533,11 +1531,11 @@ "Lead_capture_email_regex": "Zachyťte e-mail regex", "Lead_capture_phone_regex": "Olovo zachytiť telefón regex", "Leave": "Nechajte miestnosť", - "Leave_Group_Warning": "Naozaj chcete opustiť skupinu \"%s\"?", - "Leave_Livechat_Warning": "Naozaj chcete opustiť livechat s \"%s\"?", - "Leave_Private_Warning": "Naozaj chcete diskusiu ponechať s názvom \"%s\"?", + "Leave_Group_Warning": "Naozaj chcete opustiť skupinu \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Naozaj chcete opustiť livechat s \"{{roomName}}\"?", + "Leave_Private_Warning": "Naozaj chcete diskusiu ponechať s názvom \"{{roomName}}\"?", "Leave_room": "Nechajte miestnosť", - "Leave_Room_Warning": "Naozaj chcete opustiť miestnosť \"%s\"?", + "Leave_Room_Warning": "Naozaj chcete opustiť miestnosť \"{{roomName}}\"?", "Leave_the_current_channel": "Nechajte aktuálny kanál", "leave-c": "Ponechajte kanály", "leave-p": "Nechajte súkromné ​​skupiny", @@ -2454,7 +2452,6 @@ "Two-factor_authentication": "Dvojfaktorové overenie", "Two-factor_authentication_disabled": "Zablokovanie dvoch faktorov", "Two-factor_authentication_enabled": "Dvojfaktorové overovanie povolené", - "Two-factor_authentication_is_currently_disabled": "Dvojfaktorové overenie je momentálne zakázané", "Two-factor_authentication_native_mobile_app_warning": "UPOZORNENIE: Ak to zapnete, nebudete sa môcť prihlásiť v natívnych mobilných aplikáciách (Rocket.Chat +) pomocou svojho hesla, kým implementujú 2FA.", "Type": "typ", "Type_your_email": "Zadajte svoj e-mail", @@ -2759,4 +2756,4 @@ "registration.component.form.sendConfirmationEmail": "Pošlite potvrdzovací e-mail", "Enterprise": "podnik", "UpgradeToGetMore_engagement-dashboard_Title": "Analytika" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/sl-SI.i18n.json b/packages/i18n/src/locales/sl-SI.i18n.json index 7d7b2c692de96..80743dd7e247d 100644 --- a/packages/i18n/src/locales/sl-SI.i18n.json +++ b/packages/i18n/src/locales/sl-SI.i18n.json @@ -860,7 +860,6 @@ "Directory": "Imenik", "Disable_Facebook_integration": "Onemogoči Facebook integracijo", "Disable_Notifications": "Onemogoči obvestila", - "Disable_two-factor_authentication": "Onemogočite dvojno preverjanje pristnosti", "Disabled": "Onemogočeno", "Disallow_reacting": "Zavrni reagiranje", "Disallow_reacting_Description": "Zavrača reagiranje", @@ -953,7 +952,6 @@ "Enable_Auto_Away": "Omogoči samodejno pot", "Enable_Desktop_Notifications": "Omogoči namizna obvestila", "Enable_Svg_Favicon": "Omogoči zaznamko SVG ", - "Enable_two-factor_authentication": "Omogoči dvojno preverjanje pristnosti", "Enabled": "Omogočeno", "Encrypted_message": "Šifrirano sporočilo", "End_OTR": "Končaj OTR", @@ -1209,12 +1207,12 @@ "Hide": "Skrij sobo", "Hide_counter": "Skrij števec", "Hide_flextab": "S klikom skrijte desno stransko vrstico", - "Hide_Group_Warning": "Ali ste prepričani, da želite skriti skupino \"%s\"?", - "Hide_Livechat_Warning": "Ali ste prepričani, da želite skriti klepet v živo z \"%s\"?", - "Hide_Private_Warning": "Ali ste prepričani, da želite skriti pogovor z \"%s\"?", + "Hide_Group_Warning": "Ali ste prepričani, da želite skriti skupino \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Ali ste prepričani, da želite skriti klepet v živo z \"{{roomName}}\"?", + "Hide_Private_Warning": "Ali ste prepričani, da želite skriti pogovor z \"{{roomName}}\"?", "Hide_roles": "Skrij vloge", "Hide_room": "Skrij sobo", - "Hide_Room_Warning": "Ali ste prepričani, da želite skriti sobo \"%s\"?", + "Hide_Room_Warning": "Ali ste prepričani, da želite skriti sobo \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Skrij neprebrani status sobe", "Hide_usernames": "Skrij uporabniška imena", "Highlights": "Zanimivosti", @@ -1513,11 +1511,11 @@ "Lead_capture_email_regex": "Regularni izraz za prepoznavanje e-poštnih naslovov", "Lead_capture_phone_regex": "Regularni izraz za prepoznavanje telefonskih številk", "Leave": "Zapusti sobo", - "Leave_Group_Warning": "Ali ste prepričani, da želite zapustiti skupino \"%s\"?", - "Leave_Livechat_Warning": "Ali ste prepričani, da želite zapustiti klepet v živo z osebo \"%s\"?", - "Leave_Private_Warning": "Ali ste prepričani, da želite zapustiti pogovor z osebo \"%s\"?", + "Leave_Group_Warning": "Ali ste prepričani, da želite zapustiti skupino \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Ali ste prepričani, da želite zapustiti klepet v živo z osebo \"{{roomName}}\"?", + "Leave_Private_Warning": "Ali ste prepričani, da želite zapustiti pogovor z osebo \"{{roomName}}\"?", "Leave_room": "Zapusti sobo", - "Leave_Room_Warning": "Ali ste prepričani, da želite zapustiti sobo \"%s\"?", + "Leave_Room_Warning": "Ali ste prepričani, da želite zapustiti sobo \"{{roomName}}\"?", "Leave_the_current_channel": "Zapusti trenutni kanal", "leave-c": "Zapusti kanale", "leave-p": "Zapustite zasebne skupine", @@ -2434,7 +2432,6 @@ "Two-factor_authentication": "Dvojno preverjanje pristnosti", "Two-factor_authentication_disabled": "Dvojno preverjanje pristnosti je onemogočeno", "Two-factor_authentication_enabled": "Dvojno preverjanje pristnosti je omogočeno", - "Two-factor_authentication_is_currently_disabled": "Dvojno preverjanje pristnosti je trenutno onemogočeno", "Two-factor_authentication_native_mobile_app_warning": "OPOZORILO: Ko to omogočite, se ne boste mogli prijaviti v izvorne aplikacije za mobilne naprave (Rocket.Chat+) z geslom, dokler ne uvedejo 2FA.", "Type": "Tip", "Type_your_email": "Vnesite vaš e-poštni naslov", @@ -2739,4 +2736,4 @@ "registration.component.form.sendConfirmationEmail": "Pošlji potrditveno e-poštno sporočilo", "Enterprise": "Podjetje", "UpgradeToGetMore_engagement-dashboard_Title": "Analiza" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/sq.i18n.json b/packages/i18n/src/locales/sq.i18n.json index 1ffb83c6172d5..7525ccab646b6 100644 --- a/packages/i18n/src/locales/sq.i18n.json +++ b/packages/i18n/src/locales/sq.i18n.json @@ -864,7 +864,6 @@ "Directory": "drejtori", "Disable_Facebook_integration": "Çaktivizo integrimin në Facebook", "Disable_Notifications": "Çaktivizo njoftimet", - "Disable_two-factor_authentication": "Çaktivizo legalizimin me dy faktorë", "Disabled": "i paaftë", "Disallow_reacting": "Mos lejoni reagimin", "Disallow_reacting_Description": "Nuk lejon reagimin", @@ -957,7 +956,6 @@ "Enable_Auto_Away": "Aktivizo Auto Away", "Enable_Desktop_Notifications": "Aktivizoni njoftimet në Desktop", "Enable_Svg_Favicon": "Aktivizo favicon SVG", - "Enable_two-factor_authentication": "Aktivizo legalizimin me dy faktorë", "Enabled": "enabled", "Encrypted_message": "mesazhi i koduar", "End_OTR": "End OTR", @@ -1219,12 +1217,12 @@ "Hide": "Fshihe dhomën", "Hide_counter": "Fshih kundër", "Hide_flextab": "Fshiheni Faqen e Djathtë me Klik", - "Hide_Group_Warning": "Jeni te sigurte qe doni te fshehur grupin \"%s\"?", - "Hide_Livechat_Warning": "Jeni i sigurt që doni të fshehni livechat me \"%s\"?", - "Hide_Private_Warning": "A jeni i sigurt që ju doni të fshehur diskutimin me \"%s\"?", + "Hide_Group_Warning": "Jeni te sigurte qe doni te fshehur grupin \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Jeni i sigurt që doni të fshehni livechat me \"{{roomName}}\"?", + "Hide_Private_Warning": "A jeni i sigurt që ju doni të fshehur diskutimin me \"{{roomName}}\"?", "Hide_roles": "Hide Roles", "Hide_room": "Fshihe dhomën", - "Hide_Room_Warning": "Jeni te sigurte qe doni te fshehur në dhomë \"%s\"?", + "Hide_Room_Warning": "Jeni te sigurte qe doni te fshehur në dhomë \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Fshihni statusin e pambrojtur të dhomës", "Hide_usernames": "Fshih përdoruesve", "Highlights": "Pikat kryesore", @@ -1522,11 +1520,11 @@ "Lead_capture_email_regex": "Plotësoni kapjen e regex-it të postës elektronike", "Lead_capture_phone_regex": "Regjimi i kapjes së telefonit të plumbit", "Leave": "Largohu nga dhoma", - "Leave_Group_Warning": "Jeni te sigurte qe doni te largohet nga grupi \"%s\"?", - "Leave_Livechat_Warning": "Je i sigurt që dëshiron të largosh livechat me \"%s\"?", - "Leave_Private_Warning": "Jeni te sigurte qe doni te largohet diskutimin me \"%s\"?", + "Leave_Group_Warning": "Jeni te sigurte qe doni te largohet nga grupi \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Je i sigurt që dëshiron të largosh livechat me \"{{roomName}}\"?", + "Leave_Private_Warning": "Jeni te sigurte qe doni te largohet diskutimin me \"{{roomName}}\"?", "Leave_room": "Largohu nga dhoma", - "Leave_Room_Warning": "Jeni te sigurte qe doni te largohen nga dhoma \"%s\"?", + "Leave_Room_Warning": "Jeni te sigurte qe doni te largohen nga dhoma \"{{roomName}}\"?", "Leave_the_current_channel": "Lëreni kanalin aktual", "leave-c": "Lërini kanalet", "leave-p": "Lini Grupet Private", @@ -2444,7 +2442,6 @@ "Two-factor_authentication": "Authentication me dy faktorë", "Two-factor_authentication_disabled": "Autentifikimi me dy faktorë është i paaftë", "Two-factor_authentication_enabled": "Aktivizimi i legalizimit me dy faktorë", - "Two-factor_authentication_is_currently_disabled": "Vertetimi me dy faktorë është aktualisht i çaktivizuar", "Two-factor_authentication_native_mobile_app_warning": "PARALAJMËRIM: Pasi ta keni mundësuar këtë, nuk do të jeni në gjendje të identifikoheni në aplikacionet celulare vendase (Rocket.Chat +) duke përdorur fjalëkalimin tuaj derisa të zbatojnë 2FA.", "Type": "lloj", "Type_your_email": "Lloji email-it tuaj", @@ -2748,4 +2745,4 @@ "registration.component.form.sendConfirmationEmail": "Dërgo email konfirmimi", "Enterprise": "Ndërmarrje", "UpgradeToGetMore_engagement-dashboard_Title": "Analitikë" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/sr.i18n.json b/packages/i18n/src/locales/sr.i18n.json index caf41c09fa8da..ded36b162ab1a 100644 --- a/packages/i18n/src/locales/sr.i18n.json +++ b/packages/i18n/src/locales/sr.i18n.json @@ -849,7 +849,6 @@ "Discard": "Одбаци", "Discussion": "Дискусија", "Enable_Svg_Favicon": "Омогући СВГ фавикон", - "Enable_two-factor_authentication": "Омогућите двоструку аутентификацију", "Enabled": "Оmogućeno", "Encrypted_message": "Шифрована порука", "Enter_a_regex": "Унесите регек", @@ -1084,12 +1083,12 @@ "Hidden": "Сакривен", "Hide": "Сакриј собу", "Hide_counter": "Сакриј бројач", - "Hide_Group_Warning": "Да ли заиста желите да сакријете групу \"%s\"?", - "Hide_Livechat_Warning": "Да ли заиста желите да сакријете ћаскање са \"%s\"?", - "Hide_Private_Warning": "Да ли заиста желите да сакријете расправу са \"%s\"?", + "Hide_Group_Warning": "Да ли заиста желите да сакријете групу \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Да ли заиста желите да сакријете ћаскање са \"{{roomName}}\"?", + "Hide_Private_Warning": "Да ли заиста желите да сакријете расправу са \"{{roomName}}\"?", "Hide_roles": "Сакриј улоге", "Hide_room": "Сакриј собу", - "Hide_Room_Warning": "Да ли заиста желите да сакријете собу \"%s\"?", + "Hide_Room_Warning": "Да ли заиста желите да сакријете собу \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Сакриј статус непрописане собе", "Hide_usernames": "Сакриј корисничка имена", "Highlights": "Наглашавања", @@ -1355,11 +1354,11 @@ "Lead_capture_email_regex": "Леад регек е-поште", "Lead_capture_phone_regex": "Оловно снимање регекса телефона", "Leave": "Напусти собу", - "Leave_Group_Warning": "Да ли сте сигурни да желите да напустите групу \"%s\"?", - "Leave_Livechat_Warning": "Јесте ли сигурни да желите да оставите ливецхат са \"%s\"?", - "Leave_Private_Warning": "Да ли сте сигурни да желите да напустите дискусију са \"%s\"?", + "Leave_Group_Warning": "Да ли сте сигурни да желите да напустите групу \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Јесте ли сигурни да желите да оставите ливецхат са \"{{roomName}}\"?", + "Leave_Private_Warning": "Да ли сте сигурни да желите да напустите дискусију са \"{{roomName}}\"?", "Leave_room": "Напусти собу", - "Leave_Room_Warning": "Да ли сте сигурни да желите да напустите просторију \"%s\"?", + "Leave_Room_Warning": "Да ли сте сигурни да желите да напустите просторију \"{{roomName}}\"?", "Leave_the_current_channel": "Напустите тренутни канал", "leave-c": "Леаве Цханнелс", "leave-p": "Оставите приватне групе", @@ -2254,7 +2253,6 @@ "Two-factor_authentication": "Два-факторска аутентикација", "Two-factor_authentication_disabled": "Два-факторска аутентификација је онемогућена", "Two-factor_authentication_enabled": "Два-факторска аутентификација је омогућена", - "Two-factor_authentication_is_currently_disabled": "Два-факторска аутентификација је тренутно онемогућена", "Two-factor_authentication_native_mobile_app_warning": "УПОЗОРЕЊЕ: Када то омогућите, нећете моћи да се пријавите на изворне мобилне апликације (Роцкет.Цхат +) користећи своју лозинку док не примене 2ФА.", "Type": "Тип", "Type_your_email": "Унесите вашу емаил", @@ -2544,4 +2542,4 @@ "registration.component.form.sendConfirmationEmail": "Пошаљи потврдну поруку", "Enterprise": "Предузеће", "UpgradeToGetMore_engagement-dashboard_Title": "Аналитика" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/sv.i18n.json b/packages/i18n/src/locales/sv.i18n.json index 2a84782f92534..709e9bf863e09 100644 --- a/packages/i18n/src/locales/sv.i18n.json +++ b/packages/i18n/src/locales/sv.i18n.json @@ -759,7 +759,6 @@ "Back_to_imports": "Tillbaka till importer", "Cancel": "Avbryt", "Cancel_message_input": "Avbryt", - "Back_to_room": "Tillbaka till Room", "Canceled": "Avbröt", "Back_to_threads": "Tillbaka till trådar", "BBB_End_Meeting": "Avsluta mötet", @@ -1516,7 +1515,6 @@ "Custom_Script_Logged_Out_Description": "Anpassat skript som alltid körs för alla icke inloggade användare (dvs. så fort någon öppnar inloggningssidan)", "Disable_Notifications": "Inaktivera notifieringar", "Custom_Script_On_Logout": "Anpassat skript för utloggningsflöde", - "Disable_two-factor_authentication": "Inaktivera tvåfaktorsautentisering via TOTP", "Custom_Script_On_Logout_Description": "Anpassat skript som endast ska köras i exekverade utloggningsflöden", "Disabled": "Inaktiverad", "Disallow_reacting": "Tillåt ej reaktioner", @@ -1705,7 +1703,6 @@ "Disable": "Inaktivera", "EmojiCustomFilesystem": "Anpassat emoji-filsystem", "Empty_title": "Tom titel", - "Disable_two-factor_authentication_email": "Inaktivera tvåfaktorsautentisering via e-post", "Enable": "Aktivera", "Enable_Auto_Away": "Aktivera automatiskt bort", "Enable_Desktop_Notifications": "Aktivera skrivbordsnotifieringar", @@ -1714,7 +1711,6 @@ "Discussion": "Diskussion", "Enable_Svg_Favicon": "Aktivera SVG favicon", "Discussion_Description": "Diskussioner är ett ytterligare sätt att organisera konversationer på. Med det kan inbjudna externa användare delta i specifikt angivna konversationer.", - "Enable_two-factor_authentication": "Aktivera tvåfaktorsautentisering", "Discussion_first_message_disabled_due_to_e2e": "När den har skapats kan du börja skicka end-to-end-krypterade meddelanden i diskussionen.", "Enabled": "Aktiverad", "Encrypted": "Krypterad", @@ -1951,7 +1947,6 @@ "External_Queue_Service_URL": "URL för extern kötjänst", "External_Service": "Extern tjänst", "Facebook_Page": "Facebooksida", - "Enable_two-factor_authentication_email": "Aktivera tvåfaktorsautentisering via e-post", "Enable_unlimited_apps": "Aktivera obegränsat antal appar", "Encrypted_not_available": "Inte tillgängligt för offentliga kanaler", "False": "Falskt", @@ -2198,14 +2193,14 @@ "You_do_not_have_permission_to_do_this": "Du har inte behörighet att göra det här", "Hide_counter": "Dölj räknare", "Hide_flextab": "Dölj höger sidofält med klick", - "Hide_Group_Warning": "Är du säker att du vill dölja gruppen \"%s\"?", - "Hide_Livechat_Warning": "Är du säker på att du vill dölja chat med \"%s\"?", + "Hide_Group_Warning": "Är du säker att du vill dölja gruppen \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Är du säker på att du vill dölja chat med \"{{roomName}}\"?", "Estimated_wait_time": "Beräknad väntetid", "Estimated_wait_time_in_minutes": "Beräknad väntetid (tid i minuter)", - "Hide_Private_Warning": "Är du säker att du vill dölja diskussionen med \"%s\"?", + "Hide_Private_Warning": "Är du säker att du vill dölja diskussionen med \"{{roomName}}\"?", "Hide_roles": "Dölj roller", "Hide_room": "Dölj rum", - "Hide_Room_Warning": "Är du säker att du vill dölja rummet \"%s\"?", + "Hide_Room_Warning": "Är du säker att du vill dölja rummet \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Dölj oläst rums status", "Hide_usernames": "Dölj användarnamn", "Highlights": "Markeringar", @@ -2739,11 +2734,11 @@ "Inline_code": "Inline-kod", "Install_anyway": "Installera ändå", "Leave": "Lämna", - "Leave_Group_Warning": "Är du säker på att du vill lämna gruppen \"%s\"?", - "Leave_Livechat_Warning": "Är du säker på att du vill lämna Omnichannel med \"%s\"?", - "Leave_Private_Warning": "Är du säker på att du vill lämna diskussionen med \"%s\"?", + "Leave_Group_Warning": "Är du säker på att du vill lämna gruppen \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Är du säker på att du vill lämna Omnichannel med \"{{roomName}}\"?", + "Leave_Private_Warning": "Är du säker på att du vill lämna diskussionen med \"{{roomName}}\"?", "Leave_room": "Lämna", - "Leave_Room_Warning": "Är du säker på att du vill lämna kanalen \"%s\"?", + "Leave_Room_Warning": "Är du säker på att du vill lämna kanalen \"{{roomName}}\"?", "Leave_the_current_channel": "Lämna den nuvarande kanalen", "leave-c": "Lämna kanaler", "Instance": "Instans", @@ -4543,7 +4538,6 @@ "room_removed_read_only_permission": "tog bort skrivskyddad behörighet", "Two-factor_authentication_enabled": "Tvåfaktorautentisering aktiverad", "room_set_read_only_permission": "ställde in rummet till skrivskyddat", - "Two-factor_authentication_is_currently_disabled": "Tvåfaktorsautentisering via TOTP är för närvarande inaktiverad", "Two-factor_authentication_native_mobile_app_warning": "VARNING: När du har aktiverat det här kan du inte logga in på de inbyggda mobilapparna (Rocket.Chat +) med ditt lösenord tills de implementerar 2FA.", "Type": "Typ", "Room_updated_successfully": "Rummet har uppdaterats.", @@ -5209,7 +5203,6 @@ "Turn_off_video": "Stäng av video", "Two-factor_authentication_via_TOTP": "Tvåfaktorsautentisering", "Two-factor_authentication_email": "Tvåfaktorsautentisering via e-post", - "Two-factor_authentication_email_is_currently_disabled": "Tvåfaktorsautentisering via e-post är inaktiverat", "typing": "skriver", "Types": "Typer", "Types_and_Distribution": "Typer och distribution", @@ -5725,4 +5718,4 @@ "Theme_Appearence": "Utseende för tema", "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ta-IN.i18n.json b/packages/i18n/src/locales/ta-IN.i18n.json index fd93c4a8e9e14..1c53ff2060d9f 100644 --- a/packages/i18n/src/locales/ta-IN.i18n.json +++ b/packages/i18n/src/locales/ta-IN.i18n.json @@ -864,7 +864,6 @@ "Directory": "அடைவு", "Disable_Facebook_integration": "பேஸ்புக் ஒருங்கிணைப்பு முடக்கவும்", "Disable_Notifications": "அறிவிப்புகளை முடக்கு", - "Disable_two-factor_authentication": "இரு-காரணி அங்கீகாரத்தை முடக்கு", "Disabled": "முடக்கப்பட்டது", "Disallow_reacting": "பதிலளிப்பதை அனுமதிக்காதீர்கள்", "Disallow_reacting_Description": "பதிலளிப்பதை அனுமதிக்காது", @@ -957,7 +956,6 @@ "Enable_Auto_Away": "தானாக வெளியேற்றவும்", "Enable_Desktop_Notifications": "டெஸ்க்டாப் அறிவிப்புகளை இயக்க", "Enable_Svg_Favicon": "SVG ஃபேவிகானை இயக்கு", - "Enable_two-factor_authentication": "இரு-காரணி அங்கீகாரத்தை இயக்கவும்", "Enabled": "இயக்கப்பட்டது", "Encrypted_message": "மறைக்கப்பட்ட செய்தி", "End_OTR": "முடிவு OTR", @@ -1218,12 +1216,12 @@ "Hide": "அறையை மறை", "Hide_counter": "எதிர் மறை", "Hide_flextab": "கிளிக் மூலம் வலது பக்க மறை", - "Hide_Group_Warning": "குழு \"%s\" பதில் மறைக்க வேண்டும் என்பதில் உறுதியாக இருக்கிறீர்களா?", - "Hide_Livechat_Warning": "\"%s\" உடன் livechat ஐ நிச்சயமாக மறைக்க விரும்புகிறீர்களா?", - "Hide_Private_Warning": "நீங்கள் \"%s\" பதில் மூலம் விவாதம் மறைக்க விரும்பவில்லை நீங்கள் உறுதியாக இருக்கிறீர்களா?", + "Hide_Group_Warning": "குழு \"{{roomName}}\" பதில் மறைக்க வேண்டும் என்பதில் உறுதியாக இருக்கிறீர்களா?", + "Hide_Livechat_Warning": "\"{{roomName}}\" உடன் livechat ஐ நிச்சயமாக மறைக்க விரும்புகிறீர்களா?", + "Hide_Private_Warning": "நீங்கள் \"{{roomName}}\" பதில் மூலம் விவாதம் மறைக்க விரும்பவில்லை நீங்கள் உறுதியாக இருக்கிறீர்களா?", "Hide_roles": "பாத்திரங்களை மறை", "Hide_room": "அறையை மறை", - "Hide_Room_Warning": "நீங்கள் அறையில் \"%s\" பதில் மறைக்க வேண்டும் என்பதில் உறுதியாக இருக்கிறீர்களா?", + "Hide_Room_Warning": "நீங்கள் அறையில் \"{{roomName}}\" பதில் மறைக்க வேண்டும் என்பதில் உறுதியாக இருக்கிறீர்களா?", "Hide_Unread_Room_Status": "படிக்காத அறையின் நிலையை மறை", "Hide_usernames": "பயனர் பெயர்கள் மறை", "Highlights": "ஹைலைட்ஸ்", @@ -1522,11 +1520,11 @@ "Lead_capture_email_regex": "கைப்பற்ற மின்னஞ்சல் regex ஐ முன்னணி", "Lead_capture_phone_regex": "கைப்பற்றும் தொலைபேசி regex முன்னணி", "Leave": "அறையை விட்டுச்செல்", - "Leave_Group_Warning": "குழு \"%s\" பதில் விட்டு வெளியேற வேண்டும் என்பதில் உறுதியாக இருக்கிறீர்களா?", - "Leave_Livechat_Warning": "\"%s\" உடன் livechat ஐ விட்டு வைக்க விரும்புகிறீர்களா?", - "Leave_Private_Warning": "நீங்கள் \"%s\" பதில் மூலம் விவாதம் விட்டு வெளியேற வேண்டும் நீங்கள் உறுதியாக இருக்கிறீர்களா?", + "Leave_Group_Warning": "குழு \"{{roomName}}\" பதில் விட்டு வெளியேற வேண்டும் என்பதில் உறுதியாக இருக்கிறீர்களா?", + "Leave_Livechat_Warning": "\"{{roomName}}\" உடன் livechat ஐ விட்டு வைக்க விரும்புகிறீர்களா?", + "Leave_Private_Warning": "நீங்கள் \"{{roomName}}\" பதில் மூலம் விவாதம் விட்டு வெளியேற வேண்டும் நீங்கள் உறுதியாக இருக்கிறீர்களா?", "Leave_room": "அறையை விட்டுச்செல்", - "Leave_Room_Warning": "நீங்கள் அறையில் \"%s\" பதில் விட்டு வெளியேற வேண்டும் என்பதில் உறுதியாக இருக்கிறீர்களா?", + "Leave_Room_Warning": "நீங்கள் அறையில் \"{{roomName}}\" பதில் விட்டு வெளியேற வேண்டும் என்பதில் உறுதியாக இருக்கிறீர்களா?", "Leave_the_current_channel": "நடப்பு சேனலை விட்டு விடுங்கள்", "leave-c": "சேனல்களை விடு", "leave-p": "தனியார் குழுக்களை விடு", @@ -2445,7 +2443,6 @@ "Two-factor_authentication": "இரண்டு காரணி அங்கீகாரம்", "Two-factor_authentication_disabled": "இரண்டு-காரணி அங்கீகாரம் முடக்கப்பட்டது", "Two-factor_authentication_enabled": "இரண்டு-காரணி அங்கீகாரம் இயக்கப்பட்டது", - "Two-factor_authentication_is_currently_disabled": "இரண்டு-காரணி அங்கீகாரம் தற்போது முடக்கப்பட்டுள்ளது", "Two-factor_authentication_native_mobile_app_warning": "எச்சரிக்கை: இதை இயக்கியவுடன், நீங்கள் 2FA ஐ செயல்படுத்தும் வரை உங்கள் கடவுச்சொல்லைப் பயன்படுத்தி இயல்பான மொபைல் பயன்பாடுகளில் (Rocket.Chat +) உள்நுழைய முடியாது.", "Type": "வகை", "Type_your_email": "உங்கள் மின்னஞ்சல் முகவரியை உள்ளிடவும்", @@ -2751,4 +2748,4 @@ "registration.component.form.sendConfirmationEmail": "உறுதிப்படுத்தும் மின்னஞ்சல் அனுப்பவும்", "Enterprise": "நிறுவன", "UpgradeToGetMore_engagement-dashboard_Title": "அனலிட்டிக்ஸ்" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/th-TH.i18n.json b/packages/i18n/src/locales/th-TH.i18n.json index 980f3ca8e5bac..ea63d22ef7176 100644 --- a/packages/i18n/src/locales/th-TH.i18n.json +++ b/packages/i18n/src/locales/th-TH.i18n.json @@ -862,7 +862,6 @@ "Directory": "ไดเรกทอรี", "Disable_Facebook_integration": "ปิดใช้งานการรวม Facebook", "Disable_Notifications": "ปิดการแจ้งเตือน", - "Disable_two-factor_authentication": "ปิดการตรวจสอบสิทธิ์แบบสองปัจจัย", "Disabled": "พิการ", "Disallow_reacting": "ไม่อนุญาตให้ทำปฏิกิริยา", "Disallow_reacting_Description": "ไม่ให้ทำปฏิกิริยา", @@ -955,7 +954,6 @@ "Enable_Auto_Away": "เปิดใช้งาน Auto Away", "Enable_Desktop_Notifications": "เปิดใช้งานการแจ้งเตือนบนเดสก์ท็อป", "Enable_Svg_Favicon": "เปิดใช้งาน SVG favicon", - "Enable_two-factor_authentication": "เปิดใช้งานการตรวจสอบสิทธิ์แบบสองปัจจัย", "Enabled": "เปิดการใช้งาน", "Encrypted_message": "ข้อความที่เข้ารหัส", "End_OTR": "End OTR", @@ -1213,12 +1211,12 @@ "Hide": "ซ่อนห้อง", "Hide_counter": "ซ่อนเคาน์เตอร์", "Hide_flextab": "ซ่อนแถบข้างขวาด้วยการคลิก", - "Hide_Group_Warning": "คุณแน่ใจหรือไม่ว่าต้องการซ่อนกลุ่ม \"%s\"?", - "Hide_Livechat_Warning": "คุณแน่ใจหรือไม่ว่าต้องการซ่อน livechat ด้วย \"%s\"?", - "Hide_Private_Warning": "คุณแน่ใจหรือไม่ว่าต้องการซ่อนการสนทนาด้วย \"%s\"?", + "Hide_Group_Warning": "คุณแน่ใจหรือไม่ว่าต้องการซ่อนกลุ่ม \"{{roomName}}\"?", + "Hide_Livechat_Warning": "คุณแน่ใจหรือไม่ว่าต้องการซ่อน livechat ด้วย \"{{roomName}}\"?", + "Hide_Private_Warning": "คุณแน่ใจหรือไม่ว่าต้องการซ่อนการสนทนาด้วย \"{{roomName}}\"?", "Hide_roles": "ซ่อนบทบาท", "Hide_room": "ซ่อนห้อง", - "Hide_Room_Warning": "คุณแน่ใจหรือไม่ว่าต้องการซ่อนห้อง \"%s\"", + "Hide_Room_Warning": "คุณแน่ใจหรือไม่ว่าต้องการซ่อนห้อง \"{{roomName}}\"", "Hide_Unread_Room_Status": "ซ่อนสถานะห้องพักที่ยังไม่ได้อ่าน", "Hide_usernames": "ซ่อนชื่อผู้ใช้", "Highlights": "ไฮไลท์", @@ -1517,11 +1515,11 @@ "Lead_capture_email_regex": "จับ regex อีเมลสำหรับจับภาพผู้นำ", "Lead_capture_phone_regex": "จับ regex โทรศัพท์ที่เป็นผู้นำ", "Leave": "ออกจากห้อง", - "Leave_Group_Warning": "คุณแน่ใจหรือไม่ว่าต้องการออกจากกลุ่ม \"%s\"?", - "Leave_Livechat_Warning": "คุณแน่ใจหรือไม่ว่าต้องการออกจาก livechat ด้วย \"%s\"?", - "Leave_Private_Warning": "คุณแน่ใจหรือว่าต้องการออกจากการสนทนากับ \"%s\"?", + "Leave_Group_Warning": "คุณแน่ใจหรือไม่ว่าต้องการออกจากกลุ่ม \"{{roomName}}\"?", + "Leave_Livechat_Warning": "คุณแน่ใจหรือไม่ว่าต้องการออกจาก livechat ด้วย \"{{roomName}}\"?", + "Leave_Private_Warning": "คุณแน่ใจหรือว่าต้องการออกจากการสนทนากับ \"{{roomName}}\"?", "Leave_room": "ออกจากห้อง", - "Leave_Room_Warning": "คุณแน่ใจหรือไม่ว่าต้องการออกจากห้อง \"%s\"?", + "Leave_Room_Warning": "คุณแน่ใจหรือไม่ว่าต้องการออกจากห้อง \"{{roomName}}\"?", "Leave_the_current_channel": "ออกจากช่องปัจจุบัน", "leave-c": "ออกจากช่อง", "leave-p": "ออกจากกลุ่มส่วนตัว", @@ -2436,7 +2434,6 @@ "Two-factor_authentication": "การตรวจสอบสิทธิ์แบบสองปัจจัย", "Two-factor_authentication_disabled": "การปิดใช้งานการพิสูจน์ตัวตนแบบสองปัจจัย", "Two-factor_authentication_enabled": "การเปิดใช้งานการตรวจสอบสิทธิ์แบบสองปัจจัย", - "Two-factor_authentication_is_currently_disabled": "การตรวจสอบสิทธิ์แบบสองปัจจัยถูกปิดใช้อยู่ในขณะนี้", "Two-factor_authentication_native_mobile_app_warning": "คำเตือน: เมื่อเปิดใช้งานแล้วคุณจะไม่สามารถเข้าสู่ระบบแอพพลิเคชันเคลื่อนที่ (Rocket.Chat +) โดยใช้รหัสผ่านของคุณจนกว่าจะใช้งาน 2FA", "Type": "ชนิด", "Type_your_email": "พิมพ์อีเมลของคุณ", @@ -2737,4 +2734,4 @@ "registration.component.form.sendConfirmationEmail": "ส่งอีเมลยืนยัน", "Enterprise": "องค์กร", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/tr.i18n.json b/packages/i18n/src/locales/tr.i18n.json index 2a51e02ab4659..98c99bb527c04 100644 --- a/packages/i18n/src/locales/tr.i18n.json +++ b/packages/i18n/src/locales/tr.i18n.json @@ -490,7 +490,6 @@ "call-management": "Arama Yönetimi", "Cancel": "İptal et", "Cancel_message_input": "İptal et", - "Back_to_room": "Odaya geri dön", "Canceled": "İptal edildi", "Cannot_invite_users_to_direct_rooms": "Odalar doğrudan kullanıcıları davet edemezsiniz", "Cannot_open_conversation_with_yourself": "Kendinize doğrudan ileti gönderemezsiniz", @@ -1024,7 +1023,6 @@ "Directory": "Dizin", "Disable_Facebook_integration": "Facebook entegrasyonunu devre dışı bırak", "Disable_Notifications": "Bildirimleri kapat", - "Disable_two-factor_authentication": "İki aşamalı kimlik doğrulamayı devre dışı bırak", "Disabled": "Pasif", "Disallow_reacting": "Tepki Vermeye İzin Verme", "Disallow_reacting_Description": "Tepkiye izin vermeyi kapatır", @@ -1154,7 +1152,6 @@ "Enable_Desktop_Notifications": "Masaüstü Bildirimlerini etkinleştir", "Discussion": "Tartışma", "Enable_Svg_Favicon": "SVG favicon'u etkinleştir", - "Enable_two-factor_authentication": "İki aşamalı kimlik doğrulamayı etkinleştir", "Enabled": "Etkin", "Encrypted": "Şifreli", "Encrypted_channel_Description": "Uçtan uca şifrelenmiş kanal. Arama şifrelenmiş kanallarda çalışmayacak ve bildirimlerde ileti içeriği gözükmeyecektir.", @@ -1473,12 +1470,12 @@ "Hide": "Gizle", "Hide_counter": "Sayacı gizle", "Hide_flextab": "Sağ Kenar Çubuğu Tıklanarak Gizlensin", - "Hide_Group_Warning": "\"%s\" grubunu gizlemek istediğinize emin misiniz?", - "Hide_Livechat_Warning": "\"%s\" ile canlı çekimi gizlemek istediğinize emin misiniz?", - "Hide_Private_Warning": "\"%s\" ile tartışmayı gizlemek istediğinize emin misiniz?", + "Hide_Group_Warning": "\"{{roomName}}\" grubunu gizlemek istediğinize emin misiniz?", + "Hide_Livechat_Warning": "\"{{roomName}}\" ile canlı çekimi gizlemek istediğinize emin misiniz?", + "Hide_Private_Warning": "\"{{roomName}}\" ile tartışmayı gizlemek istediğinize emin misiniz?", "Hide_roles": "Roller Gizlensin", "Hide_room": "Gizle", - "Hide_Room_Warning": "Oda \"%s\" gizlemek istediğinizden emin misiniz?", + "Hide_Room_Warning": "Oda \"{{roomName}}\" gizlemek istediğinizden emin misiniz?", "Hide_Unread_Room_Status": "Okunmamış Oda Durumunu Gizle", "Hide_usernames": "Kullanıcı Adları Gizlensin", "Highlights": "Vurgular", @@ -1811,11 +1808,11 @@ "Lead_capture_email_regex": "Kurşun yakalama e-posta regex'i", "Lead_capture_phone_regex": "Telefon yakalama regex'ini yönet", "Leave": "Ayrıl", - "Leave_Group_Warning": "\"%s\" grubundan ayrılmak istediğinize emin misiniz?", - "Leave_Livechat_Warning": "Canlı görüşme \"%s\" ile terk etmek istediğinize emin misiniz?", - "Leave_Private_Warning": "\"%s\" ile tartışmadan ayrılmak istediğinize emin misiniz?", + "Leave_Group_Warning": "\"{{roomName}}\" grubundan ayrılmak istediğinize emin misiniz?", + "Leave_Livechat_Warning": "Canlı görüşme \"{{roomName}}\" ile terk etmek istediğinize emin misiniz?", + "Leave_Private_Warning": "\"{{roomName}}\" ile tartışmadan ayrılmak istediğinize emin misiniz?", "Leave_room": "Odadan ayrıl", - "Leave_Room_Warning": "Eğer oda \"%s\" ayrılmak istediğinize emin misiniz?", + "Leave_Room_Warning": "Eğer oda \"{{roomName}}\" ayrılmak istediğinize emin misiniz?", "Leave_the_current_channel": "Geçerli kanalı bırak", "leave-c": "Kanallardan Çık", "leave-p": "Özel Grupları Bırak", @@ -2902,7 +2899,6 @@ "Two-factor_authentication": "İki aşamalı kimlik doğrulama", "Two-factor_authentication_disabled": "İki aşamalı kimlik doğrulama devre dışı", "Two-factor_authentication_enabled": "İki aşamalı kimlik doğrulama etkin", - "Two-factor_authentication_is_currently_disabled": "İki aşamalı kimlik doğrulama şu anda devre dışı", "Two-factor_authentication_native_mobile_app_warning": "UYARI: Bunu etkinleştirdiğinizde, yerel mobil uygulamalara (Rocket.Chat +) şifrenizi kullanarak 2FA'yı uygulayana kadar giriş yapamazsınız.", "Type": "Tür", "Type_your_email": "E-postanızı yazın", @@ -3258,4 +3254,4 @@ "RegisterWorkspace_Features_Omnichannel_Title": "Çoklu Kanal", "Enterprise": "Kuruluş", "UpgradeToGetMore_engagement-dashboard_Title": "Mantıksal Analiz" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ug.i18n.json b/packages/i18n/src/locales/ug.i18n.json index 2dfd2cd60c55e..75983706ed311 100644 --- a/packages/i18n/src/locales/ug.i18n.json +++ b/packages/i18n/src/locales/ug.i18n.json @@ -474,10 +474,10 @@ "Header": "باش", "Hidden": "يوشۇرۇلغان", "Hide": "پاراڭلىشىش ئۆيىنى يوشۇرۇش", - "Hide_Group_Warning": "يوشۇرۇشنى جەزملەشتۈرەمسىز ؟“%s”سىز ئەزا گۇرۇپپسى", - "Hide_Private_Warning": "بىلەن بولغان مۇنازىرىنى يوشۇرۇشنى جەزملەشتۈرەمسىز ؟“%s”سىز", + "Hide_Group_Warning": "يوشۇرۇشنى جەزملەشتۈرەمسىز ؟“{{roomName}}”سىز ئەزا گۇرۇپپسى", + "Hide_Private_Warning": "بىلەن بولغان مۇنازىرىنى يوشۇرۇشنى جەزملەشتۈرەمسىز ؟“{{roomName}}”سىز", "Hide_room": "پاراڭلىشىش ئۆيىنى يوشۇرۇش", - "Hide_Room_Warning": "ئۆينى يوشۇرۇشنى جەزملەشتۈرەمسىز؟“%s”سىز", + "Hide_Room_Warning": "ئۆينى يوشۇرۇشنى جەزملەشتۈرەمسىز؟“{{roomName}}”سىز", "Hide_usernames": "ئەزا ئىسمىنى يوشۇرۇش", "Highlights": "يۇقىرى ئېنىقلىق", "Highlights_How_To": "باشقىلار يوللىغان ئۇچۇرنىڭ ئىچىدە بۇيەردىكى ئاچقۇچلۇق سۆز بولغاندا ، سىز ئەسكەرتىش قوبۇل قىلىسىز.پەش بەلگىسى ئارقىلىق كۆپلىگەن ئاچقۇچلۇق سۆزنى ئايرىڭ . چوڭ-كىچىك يېزىلىشى پەرقلەنمەيدۇ.", @@ -606,10 +606,10 @@ "LDAP_Username_Field": "ئەزا ئىسمى خەت بۆلىكى", "LDAP_Username_Field_Description": "`sAMAccountName`بەلگىلەنگەن خەت بۆلىكى بولسا \n `#{givenName}.#{sn}`خەتكۈچ ئۈلگىسى ئىشلەتسىڭىز بولىدۇ ، مەسلەن \n *ئەزا ئىسمى* قايسى خەت بۆلەك قىممىتىنى قىلىپ ئىشلىتىش ئەگەر كىرىش بېتىدىكى ئەزا نامىنى ئىشلەتكەن بولسا بوش ئورۇن قويۇڭ .LDAP يېڭى ئەزانىڭ كىرىشىنى بېكىتكەندە", "Leave": "پاراڭلىشىش ئۆيىدىن ئايرىلىش", - "Leave_Group_Warning": "ئەزا گۇرۇپپىسىدىن ئايرىلىشنى جەزملەشتۈردىڭىزمۇ ؟ “%s”سىز", - "Leave_Private_Warning": "بىلەن سۆھبەتلىشىشتىن ئايرىلىشنى جەزملەشتۈرەمسىز ؟“%s”سىز", + "Leave_Group_Warning": "ئەزا گۇرۇپپىسىدىن ئايرىلىشنى جەزملەشتۈردىڭىزمۇ ؟ “{{roomName}}”سىز", + "Leave_Private_Warning": "بىلەن سۆھبەتلىشىشتىن ئايرىلىشنى جەزملەشتۈرەمسىز ؟“{{roomName}}”سىز", "Leave_room": "پاراڭلىشىش ئۆيىدىن ئايرىلىش", - "Leave_Room_Warning": "ئۆيدىن ئايرىلىشنى جەزملەشتۈرەمسىز ؟“%s” سىز", + "Leave_Room_Warning": "ئۆيدىن ئايرىلىشنى جەزملەشتۈرەمسىز ؟“{{roomName}}” سىز", "List_of_Channels": "قانال تىزىملىكى", "List_of_Direct_Messages": "بىۋاستە سۆھبەتلىشىش تىزىملىكى", "Livechat_agents": "توردىكى مۇلازىم", diff --git a/packages/i18n/src/locales/uk.i18n.json b/packages/i18n/src/locales/uk.i18n.json index 87ad77563a4a2..0bde9bea63c0b 100644 --- a/packages/i18n/src/locales/uk.i18n.json +++ b/packages/i18n/src/locales/uk.i18n.json @@ -474,7 +474,6 @@ "Back_to_integrations": "Повернутися до інтеграцій", "Back_to_login": "Повернутися до сторінки входу", "Back_to_permissions": "Повернутися до дозволів", - "Back_to_room": "Повернутися до кімнати", "Backup_codes": "Коди резервного копіювання", "Best_first_response_time": "Кращий час першої відповіді", "Beta_feature_Depends_on_Video_Conference_to_be_enabled": "Бета-функція. Залежить від включення відеоконференції.", @@ -1039,8 +1038,6 @@ "Directory": "Довідник", "Disable_Facebook_integration": "Вимкнути інтеграцію в Facebook", "Disable_Notifications": "Вимкнути сповіщення", - "Disable_two-factor_authentication": "Вимкнути двофакторну аутентифікацію", - "Disable_two-factor_authentication_email": "Вимкнути двофакторну аутентифікацію", "Disabled": "Вимкнено", "Disallow_reacting": "Заборонити реагування", "Disallow_reacting_Description": "Заборонено реагування", @@ -1138,8 +1135,6 @@ "Enable_Desktop_Notifications": "Увімкнути сповіщення на робочому столі", "Enable_Svg_Favicon": "Увімкнути значок SVG", "Enable_inquiry_fetch_by_stream": "Увімкнути отримання даних запитів із сервера за допомогою потоку", - "Enable_two-factor_authentication": "Увімкнути двофакторну аутентифікацію", - "Enable_two-factor_authentication_email": "Увімкніть двофакторну автентифікацію електронною поштою", "Enabled": "Увімкнено", "Encrypted": "Зашифровано", "Encrypted_channel_Description": "Повністю зашифрований канал. Пошук не працюватиме із зашифрованими каналами, а сповіщення можуть не відображати вміст повідомлень.", @@ -1365,10 +1360,10 @@ "Hi_username": "Привіт [name]", "Hidden": "Прихований", "Hide": "Сховати", - "Hide_Group_Warning": "Ви впевнені, що хочете приховати групу \"%s\"?", - "Hide_Livechat_Warning": "Ви впевнені, що хочете сховати livechat за допомогою \"%s\"?", - "Hide_Private_Warning": "Ви впевнені, що хочете приховати обговорення з \"%s\"?", - "Hide_Room_Warning": "Ви впевнені, що хочете приховати кімнату \"%s\"?", + "Hide_Group_Warning": "Ви впевнені, що хочете приховати групу \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Ви впевнені, що хочете сховати livechat за допомогою \"{{roomName}}\"?", + "Hide_Private_Warning": "Ви впевнені, що хочете приховати обговорення з \"{{roomName}}\"?", + "Hide_Room_Warning": "Ви впевнені, що хочете приховати кімнату \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Сховати статус непрочитаної кімнати", "Hide_counter": "Сховати лічильник", "Hide_flextab": "Сховати праву бічну панель за допомогою клацання", @@ -1722,10 +1717,10 @@ "Lead_capture_email_regex": "Провести захоплення регекса електронною поштою", "Lead_capture_phone_regex": "Провести захоплення телефону регулярним викликом", "Leave": "Залишити", - "Leave_Group_Warning": "Ви впевнені, що хочете залишити групу \"%s\"?", - "Leave_Livechat_Warning": "Ви впевнені, що хочете залишити livechat з \"%s\"?", - "Leave_Private_Warning": "Ви впевнені, що хочете залишити обговорення з \"%s\"?", - "Leave_Room_Warning": "Ви впевнені, що хочете вийти з кімнати \"%s\"?", + "Leave_Group_Warning": "Ви впевнені, що хочете залишити групу \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Ви впевнені, що хочете залишити livechat з \"{{roomName}}\"?", + "Leave_Private_Warning": "Ви впевнені, що хочете залишити обговорення з \"{{roomName}}\"?", + "Leave_Room_Warning": "Ви впевнені, що хочете вийти з кімнати \"{{roomName}}\"?", "Leave_room": "Покинути кімнату", "Leave_the_current_channel": "Залишити поточний канал", "Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "Залиште поле опису порожнім, якщо ви не хочете показувати роль", @@ -2646,7 +2641,6 @@ "Two-factor_authentication": "Двофакторна аутентифікація", "Two-factor_authentication_disabled": "Двохфакторна автентифікація вимкнена", "Two-factor_authentication_enabled": "Двохфакторна аутентифікація включена", - "Two-factor_authentication_is_currently_disabled": "Двофакторна автентифікація наразі відключена", "Two-factor_authentication_native_mobile_app_warning": "ПОПЕРЕДЖЕННЯ. Увімкнувши це, ви не зможете ввійти в рідні мобільні додатки (Rocket.Chat +), використовуючи свій пароль, доки вони не впровадуть 2FA.", "Two-factor_authentication_via_TOTP": "Двофакторна аутентифікація", "Type": "Тип", diff --git a/packages/i18n/src/locales/vi-VN.i18n.json b/packages/i18n/src/locales/vi-VN.i18n.json index ba24f622fb38a..61ab3a81bb83f 100644 --- a/packages/i18n/src/locales/vi-VN.i18n.json +++ b/packages/i18n/src/locales/vi-VN.i18n.json @@ -959,7 +959,6 @@ "Directory": "Danh mục", "Disable_Facebook_integration": "Vô hiệu hoá tích hợp trên Facebook", "Disable_Notifications": "Vô hiệu hóa thông báo", - "Disable_two-factor_authentication": "Tắt xác thực hai yếu tố", "Disabled": "Đã tắt", "Disallow_reacting": "Không cho phép biểu cảm", "Disallow_reacting_Description": "Không cho phép biểu cảm", @@ -1052,7 +1051,6 @@ "Enable_Auto_Away": "Bật Auto Away", "Enable_Desktop_Notifications": "Bật Thông báo trên màn hình", "Enable_Svg_Favicon": "Bật SVAV favicon", - "Enable_two-factor_authentication": "Bật xác thực hai yếu tố", "Enabled": "Đã bật", "Encrypted_message": "Tin nhắn được mã hóa", "End_OTR": "Kết thúc OTR", @@ -1313,12 +1311,12 @@ "Hide": "Ẩn phòng", "Hide_counter": "Ẩn bộ đếm", "Hide_flextab": "Ẩn Thanh bên Phải với Nhấp", - "Hide_Group_Warning": "Bạn có chắc chắn muốn ẩn nhóm \"%s\" không?", - "Hide_Livechat_Warning": "Bạn có chắc chắn muốn ẩn livechat với \"%s\" không?", - "Hide_Private_Warning": "Bạn có chắc chắn muốn ẩn thảo luận với \"%s\" không?", + "Hide_Group_Warning": "Bạn có chắc chắn muốn ẩn nhóm \"{{roomName}}\" không?", + "Hide_Livechat_Warning": "Bạn có chắc chắn muốn ẩn livechat với \"{{roomName}}\" không?", + "Hide_Private_Warning": "Bạn có chắc chắn muốn ẩn thảo luận với \"{{roomName}}\" không?", "Hide_roles": "Ẩn vai trò", "Hide_room": "Ẩn phòng", - "Hide_Room_Warning": "Bạn có chắc chắn muốn ẩn phòng \"%s\" không?", + "Hide_Room_Warning": "Bạn có chắc chắn muốn ẩn phòng \"{{roomName}}\" không?", "Hide_Unread_Room_Status": "Ẩn trạng thái phòng không đọc", "Hide_usernames": "Ẩn Tên người dùng", "Highlights": "Điểm nổi bật", @@ -1616,11 +1614,11 @@ "Lead_capture_email_regex": "Lead capture email regex", "Lead_capture_phone_regex": "Lead capture phone regex", "Leave": "Rời khỏi phòng", - "Leave_Group_Warning": "Bạn có chắc chắn muốn thoát khỏi nhóm \"%s\" không?", - "Leave_Livechat_Warning": "Bạn có chắc chắn muốn thoát khỏi livechat với \"%s\" không?", - "Leave_Private_Warning": "Bạn có chắc chắn muốn rời khỏi cuộc thảo luận với \"%s\" không?", + "Leave_Group_Warning": "Bạn có chắc chắn muốn thoát khỏi nhóm \"{{roomName}}\" không?", + "Leave_Livechat_Warning": "Bạn có chắc chắn muốn thoát khỏi livechat với \"{{roomName}}\" không?", + "Leave_Private_Warning": "Bạn có chắc chắn muốn rời khỏi cuộc thảo luận với \"{{roomName}}\" không?", "Leave_room": "Rời khỏi phòng", - "Leave_Room_Warning": "Bạn có chắc chắn muốn rời khỏi phòng \"%s\" không?", + "Leave_Room_Warning": "Bạn có chắc chắn muốn rời khỏi phòng \"{{roomName}}\" không?", "Leave_the_current_channel": "Rời khỏi kênh hiện tại", "leave-c": "Rời khỏi kênh", "leave-p": "Rời khỏi Nhóm Riêng tư", @@ -2544,7 +2542,6 @@ "Two-factor_authentication": "Xác thực hai yếu tố", "Two-factor_authentication_disabled": "Xác thực hai yếu tố bị vô hiệu hoá", "Two-factor_authentication_enabled": "Xác thực hai yếu tố được kích hoạt", - "Two-factor_authentication_is_currently_disabled": "Xác thực hai yếu tố hiện đang bị vô hiệu hóa", "Two-factor_authentication_native_mobile_app_warning": "CẢNH BÁO: Khi bạn kích hoạt tính năng này, bạn sẽ không thể đăng nhập vào các ứng dụng di động (Rocket.Chat +) sử dụng mật khẩu của bạn cho đến khi họ thực hiện 2FA.", "Type": "Kiểu", "Type_your_email": "Nhập email của bạn", @@ -2848,4 +2845,4 @@ "registration.component.form.sendConfirmationEmail": "Gửi email xác nhận", "Enterprise": "Doanh nghiệp", "UpgradeToGetMore_engagement-dashboard_Title": "phân tích" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/zh-HK.i18n.json b/packages/i18n/src/locales/zh-HK.i18n.json index 44370fa7d56a0..48ae0841fb5aa 100644 --- a/packages/i18n/src/locales/zh-HK.i18n.json +++ b/packages/i18n/src/locales/zh-HK.i18n.json @@ -885,7 +885,6 @@ "Directory": "目录", "Disable_Facebook_integration": "禁用Facebook集成", "Disable_Notifications": "禁用通知", - "Disable_two-factor_authentication": "禁用双因素身份验证", "Disabled": "残", "Disallow_reacting": "禁止反应", "Disallow_reacting_Description": "不允许反应", @@ -978,7 +977,6 @@ "Enable_Auto_Away": "启用自动离开", "Enable_Desktop_Notifications": "启用桌面通知", "Enable_Svg_Favicon": "启用S​​VG图标", - "Enable_two-factor_authentication": "启用双因素身份验证", "Enabled": "启用", "Encrypted_message": "加密的消息", "End_OTR": "结束OTR", @@ -2469,7 +2467,6 @@ "Two-factor_authentication": "双因素认证", "Two-factor_authentication_disabled": "双因素身份验证被禁用", "Two-factor_authentication_enabled": "启用双因素身份验证", - "Two-factor_authentication_is_currently_disabled": "双因素认证目前被禁用", "Two-factor_authentication_native_mobile_app_warning": "警告:启用此功能后,您将无法使用密码登录本机移动应用程序(Rocket.Chat +),直到他们实施2FA。", "Type": "类型", "Type_your_email": "输入你的邮箱", @@ -2772,4 +2769,4 @@ "registration.component.form.sendConfirmationEmail": "已发送确认电子邮件", "Enterprise": "企业", "UpgradeToGetMore_engagement-dashboard_Title": "分析" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/zh-TW.i18n.json b/packages/i18n/src/locales/zh-TW.i18n.json index 5ae51259c38ad..12a0bad6506e1 100644 --- a/packages/i18n/src/locales/zh-TW.i18n.json +++ b/packages/i18n/src/locales/zh-TW.i18n.json @@ -667,7 +667,6 @@ "Back_to_imports": "回到匯入", "Cancel": "取消", "Cancel_message_input": "取消", - "Back_to_room": "返回 Room", "Canceled": "已取消", "Back_to_threads": "返回主題", "BBB_End_Meeting": "結束會議", @@ -1353,7 +1352,6 @@ "Custom_Script_Logged_Out_Description": "自訂腳本讓所有未登入的使用者在登入時都會執行。例如. (無論何時登入頁面)", "Disable_Notifications": "停用通知", "Custom_Script_On_Logout": "登出流程的自訂腳本", - "Disable_two-factor_authentication": "透過 TOTP 停用2步驟驗證", "Custom_Script_On_Logout_Description": "自訂腳本只會執行在登出流程", "Disabled": "已停用", "Disallow_reacting": "不允許反應", @@ -1500,14 +1498,12 @@ "Disable": "停用", "EmojiCustomFilesystem": "自訂表情符號文件系統", "Empty_title": "空白標題", - "Disable_two-factor_authentication_email": "透過電子郵件停用2步驟身份驗證", "Enable": "啟用", "Enable_Auto_Away": "啟用自動離開", "Enable_Desktop_Notifications": "啟用桌面通知", "Discard": "丟棄", "Discussion": "討論", "Enable_Svg_Favicon": "啟用 SVG 圖示", - "Enable_two-factor_authentication": "啟用2步驟驗證", "Discussion_first_message_disabled_due_to_e2e": "您可以在這個建立後開始在這個論壇裡傳送點對點的加密訊息。", "Enabled": "已啟用", "Encrypted": "已加密", @@ -1716,7 +1712,6 @@ "External_Queue_Service_URL": "外部佇列服務 URL", "External_Service": "外部服務", "Facebook_Page": "Facebook 頁面", - "Enable_two-factor_authentication_email": "透過電子郵件啟用2步驟驗證", "Encrypted_not_available": "不可以用在公開 Channel", "False": "否", "End": "結束", @@ -1933,12 +1928,12 @@ "Hide": "隱藏", "Hide_counter": "隱藏計數器", "Hide_flextab": "點擊右鍵隱藏側邊欄", - "Hide_Group_Warning": "您確定要隱藏群組 “%s” 嗎?", - "Hide_Livechat_Warning": "您確定要隱藏 “%s” 的即時聊天嗎?", - "Hide_Private_Warning": "您確定您要隱藏用 “%s” 的討論?", + "Hide_Group_Warning": "您確定要隱藏群組 “{{roomName}}” 嗎?", + "Hide_Livechat_Warning": "您確定要隱藏 “{{roomName}}” 的即時聊天嗎?", + "Hide_Private_Warning": "您確定您要隱藏用 “{{roomName}}” 的討論?", "Hide_roles": "隱藏角色", "Hide_room": "隱藏", - "Hide_Room_Warning": "您確定您要隱藏的房間 “%s” 嗎?", + "Hide_Room_Warning": "您確定您要隱藏的房間 “{{roomName}}” 嗎?", "Hide_Unread_Room_Status": "隱藏未讀房間狀態", "Hide_usernames": "隱藏使用者名稱", "Highlights": "強調", @@ -2392,11 +2387,11 @@ "Lead_capture_email_regex": "最先抓取電子郵件正規表示法", "Lead_capture_phone_regex": "最先抓取手機號碼正規表示法", "Leave": "離開", - "Leave_Group_Warning": "你確定你要離開組 “%s” 嗎?", - "Leave_Livechat_Warning": "你確定要離開 “%s” 的即時聊天嗎?", - "Leave_Private_Warning": "你確定要離開 “%s” 的討論?", + "Leave_Group_Warning": "你確定你要離開組 “{{roomName}}” 嗎?", + "Leave_Livechat_Warning": "你確定要離開 “{{roomName}}” 的即時聊天嗎?", + "Leave_Private_Warning": "你確定要離開 “{{roomName}}” 的討論?", "Leave_room": "離開", - "Leave_Room_Warning": "您確定要離開頻道 “%s” 嗎?", + "Leave_Room_Warning": "您確定要離開頻道 “{{roomName}}” 嗎?", "Leave_the_current_channel": "離開目前頻道", "leave-c": "保留 Channel", "Instance": "實例", @@ -3870,7 +3865,6 @@ "Two-factor_authentication": "透過 TOTP 2步驟驗證", "Two-factor_authentication_disabled": "2步驟驗證被停用", "Two-factor_authentication_enabled": "啟用2步驟驗證", - "Two-factor_authentication_is_currently_disabled": "透過 TOTP 2步驟驗證目前被停用", "Two-factor_authentication_native_mobile_app_warning": "警告:啟用此功能後,您將無法使用密碼登錄本機移動應用程式(Rocket.Chat +),直到他們實施2步驟驗證。", "Type": "類型", "Room_updated_successfully": "Room 上傳成功!", @@ -4349,7 +4343,6 @@ "Try_now": "現在再試", "Two-factor_authentication_via_TOTP": "透過 TOTP 2步驟驗證", "Two-factor_authentication_email": "通過電子郵件進行2步驟驗證", - "Two-factor_authentication_email_is_currently_disabled": "目前已停用透過電子郵件進行的2步驟驗證", "Types": "類型", "UI_Show_top_navbar_embedded_layout": "在嵌入式介面中顯示頂部導航欄", "unable-to-get-file": "無法取得檔案", @@ -4568,4 +4561,4 @@ "Enterprise": "企業", "UpgradeToGetMore_engagement-dashboard_Title": "分析", "UpgradeToGetMore_auditing_Title": "訊息稽核" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/zh.i18n.json b/packages/i18n/src/locales/zh.i18n.json index 4e2ab8cd3480d..4a20bd3119b6f 100644 --- a/packages/i18n/src/locales/zh.i18n.json +++ b/packages/i18n/src/locales/zh.i18n.json @@ -611,7 +611,6 @@ "Back_to_imports": "返回导入", "Cancel": "取消", "Cancel_message_input": "取消", - "Back_to_room": "回到房间", "Canceled": "已取消", "Cannot_invite_users_to_direct_rooms": "不能邀请用户加入私聊", "Cannot_open_conversation_with_yourself": "不能和你自己私聊", @@ -1235,7 +1234,6 @@ "Custom_Script_Logged_Out_Description": "任何未登录的用户将运行的自定义脚本(例如进入登录页面时)。", "Disable_Notifications": "禁用通知", "Custom_Script_On_Logout": "登出流程中的自定义脚本", - "Disable_two-factor_authentication": "禁用基于 TOTP 的两步验证", "Custom_Script_On_Logout_Description": "仅登录流程中运行的自定义脚本", "Disabled": "已禁用", "Disallow_reacting": "不允许回应", @@ -1371,14 +1369,12 @@ "Emoji": "表情符号", "EmojiCustomFilesystem": "自定义表情文件系统", "Empty_title": "空标题", - "Disable_two-factor_authentication_email": "禁用基于邮件的两步验证", "Enable": "启用", "Enable_Auto_Away": "启用自动离开", "Enable_Desktop_Notifications": "启用桌面通知", "Discard": "放弃", "Discussion": "讨论", "Enable_Svg_Favicon": "启用 SVG 图标", - "Enable_two-factor_authentication": "启用两步验证", "Discussion_first_message_disabled_due_to_e2e": "在此讨论的创建完成后您可以发送端到端加密的消息", "Enabled": "已启用", "Encrypted": "加密的", @@ -1570,7 +1566,6 @@ "External_Queue_Service_URL": "外部队列服务 URL", "External_Service": "外部服务", "Facebook_Page": "Facebook页面", - "Enable_two-factor_authentication_email": "启用基于邮件的两步验证", "False": "否", "End": "结束", "Favorite": "喜爱", @@ -1773,12 +1768,12 @@ "Hide": "隐藏", "Hide_counter": "隐藏计数器", "Hide_flextab": "通过点击隐藏右边栏", - "Hide_Group_Warning": "您确定要隐藏用户组 “%s” 吗?", - "Hide_Livechat_Warning": "您确定要隐藏与 “%s” 的聊天吗?", - "Hide_Private_Warning": "您确定要隐藏与 “%s” 的讨论吗?", + "Hide_Group_Warning": "您确定要隐藏用户组 “{{roomName}}” 吗?", + "Hide_Livechat_Warning": "您确定要隐藏与 “{{roomName}}” 的聊天吗?", + "Hide_Private_Warning": "您确定要隐藏与 “{{roomName}}” 的讨论吗?", "Hide_roles": "隐藏角色", "Hide_room": "隐藏", - "Hide_Room_Warning": "您确定要隐藏频道 “%s” 吗?", + "Hide_Room_Warning": "您确定要隐藏频道 “{{roomName}}” 吗?", "Hide_Unread_Room_Status": "隐藏未读聊天室状态", "Hide_usernames": "隐藏用户名", "Highlights": "高亮", @@ -2195,11 +2190,11 @@ "Lead_capture_email_regex": "领导捕获电子邮件正则表达式", "Lead_capture_phone_regex": "领导捕获手机正则表达式", "Leave": "离开", - "Leave_Group_Warning": "您确定要离开用户组 “%s” 吗?", - "Leave_Livechat_Warning": "你确定要离开和 “%s” 的 omnichannel 吗?", - "Leave_Private_Warning": "您确定要离开和 “%s” 的讨论吗?", + "Leave_Group_Warning": "您确定要离开用户组 “{{roomName}}” 吗?", + "Leave_Livechat_Warning": "你确定要离开和 “{{roomName}}” 的 omnichannel 吗?", + "Leave_Private_Warning": "您确定要离开和 “{{roomName}}” 的讨论吗?", "Leave_room": "离开", - "Leave_Room_Warning": "您确定要离开聊天室 “%s” 吗?", + "Leave_Room_Warning": "您确定要离开聊天室 “{{roomName}}” 吗?", "Leave_the_current_channel": "离开当前频道", "leave-c": "保留频道", "Instance": "实例", @@ -3545,7 +3540,6 @@ "Two-factor_authentication": "基于 TOTP 的两步验证", "Two-factor_authentication_disabled": "两步验证被禁用", "Two-factor_authentication_enabled": "启用两步验证", - "Two-factor_authentication_is_currently_disabled": "基于 TOTP 的两步验证当前被禁用", "Two-factor_authentication_native_mobile_app_warning": "警告:一旦启用此功能,您将无法使用密码登录原生移动应用(Rocket.Chat +),直到他们实施2FA。", "Type": "类型", "Room_updated_successfully": "聊天室更新成功!", @@ -4010,7 +4004,6 @@ "Try_now": "立即尝试", "Two-factor_authentication_via_TOTP": "基于 TOTP 的两步验证", "Two-factor_authentication_email": "基于邮件的两步验证", - "Two-factor_authentication_email_is_currently_disabled": "基于邮件的两步验证当前被禁用", "Types_and_Distribution": "类型和分发", "UI_Show_top_navbar_embedded_layout": "在嵌入式界面中显示顶部导航栏", "unable-to-get-file": "无法取得文件", @@ -4128,4 +4121,4 @@ "Enterprise": "企业", "UpgradeToGetMore_engagement-dashboard_Title": "分析", "UpgradeToGetMore_auditing_Title": "消息审计" -} \ No newline at end of file +} diff --git a/packages/instance-status/CHANGELOG.md b/packages/instance-status/CHANGELOG.md index abfe67d27816e..75c42bbe71f00 100644 --- a/packages/instance-status/CHANGELOG.md +++ b/packages/instance-status/CHANGELOG.md @@ -1,5 +1,86 @@ # @rocket.chat/instance-status +## 0.1.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/models@1.5.0-rc.8 +
      + +## 0.1.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/models@1.5.0-rc.7 +
      + +## 0.1.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/models@1.5.0-rc.6 +
      + +## 0.1.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/models@1.5.0-rc.5 +
      + +## 0.1.21-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/models@1.5.0-rc.4 +
      + +## 0.1.21-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/models@1.5.0-rc.3 +
      + +## 0.1.21-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/models@1.5.0-rc.2 +
      + +## 0.1.21-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/models@1.5.0-rc.1 +
      + +## 0.1.21-rc.0 + +### Patch Changes + +-
      Updated dependencies [3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, 47ae69912cd90743e7bf836fdee4be481a01bbba]: + + - @rocket.chat/models@1.5.0-rc.0 +
      + ## 0.1.20 ### Patch Changes diff --git a/packages/instance-status/package.json b/packages/instance-status/package.json index 582b30db1d1c6..7bcb10f5a558e 100644 --- a/packages/instance-status/package.json +++ b/packages/instance-status/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/instance-status", - "version": "0.1.20", + "version": "0.1.21-rc.8", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/livechat/CHANGELOG.md b/packages/livechat/CHANGELOG.md index 9b5fc82ca696b..e88df49a38106 100644 --- a/packages/livechat/CHANGELOG.md +++ b/packages/livechat/CHANGELOG.md @@ -1,5 +1,86 @@ # @rocket.chat/livechat Change Log +## 1.22.8-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/gazzodown@18.0.0-rc.8 +
      + +## 1.22.8-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/gazzodown@18.0.0-rc.7 +
      + +## 1.22.8-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/gazzodown@18.0.0-rc.6 +
      + +## 1.22.8-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/gazzodown@18.0.0-rc.5 +
      + +## 1.22.8-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/gazzodown@18.0.0-rc.4 +
      + +## 1.22.8-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/gazzodown@18.0.0-rc.3 +
      + +## 1.22.8-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/gazzodown@18.0.0-rc.2 +
      + +## 1.22.8-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/gazzodown@18.0.0-rc.1 +
      + +## 1.22.8-rc.0 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/gazzodown@18.0.0-rc.0 +
      + ## 1.22.7 ### Patch Changes diff --git a/packages/livechat/package.json b/packages/livechat/package.json index 6af2d086ce411..ebc55aedcb787 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/livechat", - "version": "1.22.7", + "version": "1.22.8-rc.8", "files": [ "/build" ], diff --git a/packages/message-parser/package.json b/packages/message-parser/package.json index bd5e824829775..bcc3c18eedde7 100644 --- a/packages/message-parser/package.json +++ b/packages/message-parser/package.json @@ -56,7 +56,7 @@ "@rocket.chat/peggy-loader": "workspace:~", "@rocket.chat/prettier-config": "~0.31.25", "@types/jest": "~29.5.14", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "@typescript-eslint/parser": "~5.58.0", "babel-loader": "~9.2.1", "eslint": "~8.45.0", diff --git a/packages/mock-providers/CHANGELOG.md b/packages/mock-providers/CHANGELOG.md index f0f913141bc02..54af2e39efb80 100644 --- a/packages/mock-providers/CHANGELOG.md +++ b/packages/mock-providers/CHANGELOG.md @@ -1,5 +1,93 @@ # @rocket.chat/mock-providers +## 0.2.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.8 +
      + +## 0.2.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.7 +
      + +## 0.2.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.6 +
      + +## 0.2.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.5 +
      + +## 0.2.0-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.4 +
      + +## 0.2.0-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.3 +
      + +## 0.2.0-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.2 +
      + +## 0.2.0-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.1 +
      + +## 0.2.0-rc.0 + +### Minor Changes + +- ([#35218](https://github.com/RocketChat/Rocket.Chat/pull/35218)) Adds a new admin page to audit settings changes in a server + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +### Patch Changes + +-
      Updated dependencies [2c190740d0ff166a4cefe8e833b0b2682a41fab1, f545617c2ac3d67af533e64c2670d8d564a56d15, bffc49f426259925c415651c2b2a58083dac547a, 1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 4b28126ac94cf1d3312b30ad9863ca02673f49d4, cc344bea08c08501f50e9cee620b2926a322a4ee, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de, be67bb771294c337c28da5e61ae47ab4e32244d1, 895ea3fdbba1d0e3cf1bed03cb8d0abfcca5d351]: + + - @rocket.chat/i18n@1.6.0-rc.0 + - @rocket.chat/ui-contexts@18.0.0-rc.0 +
      + ## 0.1.9 ### Patch Changes diff --git a/packages/mock-providers/jest.config.ts b/packages/mock-providers/jest.config.ts new file mode 100644 index 0000000000000..0de4cfdd48e48 --- /dev/null +++ b/packages/mock-providers/jest.config.ts @@ -0,0 +1,8 @@ +import client from '@rocket.chat/jest-presets/client'; +import type { Config } from 'jest'; + +export default { + preset: client.preset, + modulePathIgnorePatterns: ['/__tests__/helpers'], + testMatch: ['/src/tests/**/**.spec.[jt]s?(x)'], +} satisfies Config; diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index 2476e82462ef6..c1284dd7a9abf 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -1,20 +1,27 @@ { "name": "@rocket.chat/mock-providers", - "version": "0.1.9", + "version": "0.2.0-rc.8", "private": true, "dependencies": { "@rocket.chat/emitter": "~0.31.25", "@rocket.chat/i18n": "workspace:~", + "@rocket.chat/ui-contexts": "workspace:^", "@storybook/react": "^8.6.4", "i18next": "~23.4.9", "react-i18next": "~13.2.2" }, "devDependencies": { "@rocket.chat/ddp-client": "workspace:~", - "@rocket.chat/ui-contexts": "workspace:*", + "@rocket.chat/mongo-adapter": "workspace:~", "@rocket.chat/ui-video-conf": "workspace:*", "@tanstack/react-query": "~5.65.1", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.2.0", + "@testing-library/react-hooks": "^8.0.1", + "@types/react": "~18.3.17", + "@types/react-dom": "~18.3.5", "eslint": "~8.45.0", + "jest": "^29.7.0", "react": "~18.3.1", "typescript": "~5.7.2" }, diff --git a/packages/mock-providers/src/MockedAppRootBuilder.tsx b/packages/mock-providers/src/MockedAppRootBuilder.tsx index 508f3a285e1d2..50815c1f692fa 100644 --- a/packages/mock-providers/src/MockedAppRootBuilder.tsx +++ b/packages/mock-providers/src/MockedAppRootBuilder.tsx @@ -11,8 +11,9 @@ import type { import type { ServerMethodName, ServerMethodParameters, ServerMethodReturn } from '@rocket.chat/ddp-client'; import { Emitter } from '@rocket.chat/emitter'; import languages from '@rocket.chat/i18n/dist/languages'; +import { createFilterFromQuery } from '@rocket.chat/mongo-adapter'; import type { Method, OperationParams, OperationResult, PathPattern, UrlParams } from '@rocket.chat/rest-typings'; -import type { Device, ModalContextValue, SubscriptionWithRoom, TranslationKey } from '@rocket.chat/ui-contexts'; +import type { Device, ModalContextValue, SettingsContextQuery, SubscriptionWithRoom, TranslationKey } from '@rocket.chat/ui-contexts'; import { AuthorizationContext, ConnectionStatusContext, @@ -23,6 +24,7 @@ import { UserContext, ActionManagerContext, ModalContext, + UserPresenceContext, } from '@rocket.chat/ui-contexts'; import type { VideoConfPopupPayload } from '@rocket.chat/ui-video-conf'; import { VideoConfContext } from '@rocket.chat/ui-video-conf'; @@ -45,7 +47,11 @@ interface MockedAppRootEvents { 'update-modal': void; } +const empty = [] as const; + export class MockedAppRootBuilder { + private _settings: Map = new Map(); + private wrappers: Array<(children: ReactNode) => ReactNode> = []; private connectionStatus: ContextType = { @@ -72,6 +78,8 @@ export class MockedAppRootBuilder { getStream: () => () => () => undefined, uploadToEndpoint: () => Promise.reject(new Error('not implemented')), callMethod: () => Promise.reject(new Error('not implemented')), + disconnect: () => Promise.reject(new Error('not implemented')), + reconnect: () => Promise.reject(new Error('not implemented')), info: undefined, }; @@ -92,9 +100,8 @@ export class MockedAppRootBuilder { private settings: Mutable> = { hasPrivateAccess: true, - isLoading: false, querySetting: (_id: string) => [() => () => undefined, () => undefined], - querySettings: () => [() => () => undefined, () => []], + querySettings: (_query: SettingsContextQuery) => [() => () => undefined, () => empty as unknown as ISetting[]], dispatch: async () => undefined, }; @@ -108,6 +115,10 @@ export class MockedAppRootBuilder { userId: null, }; + private userPresence: ContextType = { + queryUserData: (_uid) => ({ subscribe: () => () => undefined, get: () => undefined }), + }; + private videoConf: ContextType = { queryIncomingCalls: () => [() => () => undefined, () => []], queryRinging: () => [() => () => undefined, () => false], @@ -328,6 +339,14 @@ export class MockedAppRootBuilder { return this; } + withUsers(users: IUser[]): this { + users.forEach((user) => { + this.userPresence.queryUserData = (_uid) => ({ subscribe: () => () => undefined, get: () => user }); + }); + + return this; + } + withSubscriptions(subscriptions: SubscriptionWithRoom[]): this { this.subscriptions = subscriptions; @@ -373,7 +392,6 @@ export class MockedAppRootBuilder { } as ISetting; const innerFn = this.settings.querySetting; - const outerFn = ( innerSetting: string, ): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => ISetting | undefined] => { @@ -386,6 +404,26 @@ export class MockedAppRootBuilder { this.settings.querySetting = outerFn; + this._settings.set(id, setting); + + const cache = new WeakMap(); + + this.settings.querySettings = (query: SettingsContextQuery) => { + const filter = + cache.get(query) ?? + createFilterFromQuery({ + ...(query._id ? { _id: { $in: query._id } } : {}), + } as any); + cache.set(query, filter); + const arr = cache.get(filter) ?? Array.from(this._settings.values()).filter(filter); + return [ + () => () => undefined, + () => { + return arr; + }, + ]; + }; + return this; } @@ -489,6 +527,7 @@ export class MockedAppRootBuilder { router, settings, user, + userPresence, videoConf, i18n, authorization, @@ -578,36 +617,38 @@ export class MockedAppRootBuilder { {/* - */} - '', - emitInteraction: () => Promise.reject(new Error('not implemented')), - getInteractionPayloadByViewId: () => undefined, - handleServerInteraction: () => undefined, - off: () => undefined, - on: () => undefined, - openView: () => undefined, - disposeView: () => undefined, - notifyBusy: () => undefined, - notifyIdle: () => undefined, - }} - > - - {/* + */} + + '', + emitInteraction: () => Promise.reject(new Error('not implemented')), + getInteractionPayloadByViewId: () => undefined, + handleServerInteraction: () => undefined, + off: () => undefined, + on: () => undefined, + openView: () => undefined, + disposeView: () => undefined, + notifyBusy: () => undefined, + notifyIdle: () => undefined, + }} + > + + {/* */} - {wrappers.reduce( - (children, wrapper) => wrapper(children), - <> - {children} - {modal.currentModal.component} - , - )} - {/* + {wrappers.reduce( + (children, wrapper) => wrapper(children), + <> + {children} + {modal.currentModal.component} + , + )} + {/* */} - - - {/* + + + + {/* */} diff --git a/packages/mock-providers/src/MockedSettingsContext.tsx b/packages/mock-providers/src/MockedSettingsContext.tsx index 86414992e9e75..eb1181ce5dbc7 100644 --- a/packages/mock-providers/src/MockedSettingsContext.tsx +++ b/packages/mock-providers/src/MockedSettingsContext.tsx @@ -4,7 +4,6 @@ import type { ContextType, ReactNode } from 'react'; const settingContextValue: ContextType = { hasPrivateAccess: true, - isLoading: false, querySetting: (_id: string) => [() => () => undefined, () => undefined], querySettings: () => [() => () => undefined, () => []], dispatch: async () => undefined, diff --git a/packages/mock-providers/src/tests/useSetting.spec.tsx b/packages/mock-providers/src/tests/useSetting.spec.tsx new file mode 100644 index 0000000000000..90701e06bbe40 --- /dev/null +++ b/packages/mock-providers/src/tests/useSetting.spec.tsx @@ -0,0 +1,13 @@ +import { useSetting } from '@rocket.chat/ui-contexts'; +import { renderHook } from '@testing-library/react'; + +import { mockAppRoot } from '..'; + +describe('useSetting', () => { + it('should return settings from context', () => { + const { result } = renderHook(() => useSetting('asd'), { + wrapper: mockAppRoot().withSetting('asd', 'qwe').build(), + }); + expect(result.current).toEqual('qwe'); + }); +}); diff --git a/packages/mock-providers/src/tests/useSettings.spec.tsx b/packages/mock-providers/src/tests/useSettings.spec.tsx new file mode 100644 index 0000000000000..8354da97db1a6 --- /dev/null +++ b/packages/mock-providers/src/tests/useSettings.spec.tsx @@ -0,0 +1,38 @@ +import { useSettings } from '@rocket.chat/ui-contexts'; +import { renderHook } from '@testing-library/react'; + +import { mockAppRoot } from '..'; + +describe('useSettings', () => { + it('should return all settings', () => { + const query = {}; + const { result } = renderHook(() => useSettings(query), { + wrapper: mockAppRoot().withSetting('asd', 'qwe').withSetting('zxc', 'rty').build(), + }); + expect(result.current).toEqual([ + { + _id: 'asd', + value: 'qwe', + }, + { + _id: 'zxc', + value: 'rty', + }, + ]); + }); + + it('should return settings filtered by _id', () => { + const query = { + _id: ['asd'], + }; + const { result } = renderHook(() => useSettings(query), { + wrapper: mockAppRoot().withSetting('asd', 'qwe').withSetting('zxc', 'rty').build(), + }); + expect(result.current).toEqual([ + { + _id: 'asd', + value: 'qwe', + }, + ]); + }); +}); diff --git a/packages/mock-providers/src/tests/useUserPresence.spec.tsx b/packages/mock-providers/src/tests/useUserPresence.spec.tsx new file mode 100644 index 0000000000000..dbdfda827222f --- /dev/null +++ b/packages/mock-providers/src/tests/useUserPresence.spec.tsx @@ -0,0 +1,43 @@ +import { faker } from '@faker-js/faker'; +import type { IUser } from '@rocket.chat/core-typings'; +import { UserStatus } from '@rocket.chat/core-typings'; +import { useUserPresence } from '@rocket.chat/ui-contexts'; +import { renderHook } from '@testing-library/react'; + +import { mockAppRoot } from '..'; + +// TODO: this will live in `mock-providers` package +function createFakeUser(overrides?: Partial): IUser { + return { + _id: faker.database.mongodbObjectId(), + _updatedAt: faker.date.recent(), + username: faker.internet.userName(), + name: faker.person.fullName(), + createdAt: faker.date.recent(), + roles: ['user'], + active: faker.datatype.boolean(), + type: 'user', + ...overrides, + }; +} + +const SAMPLE_STATUS = 'Sample Status'; + +const user = createFakeUser({ + active: true, + roles: ['admin'], + type: 'user', + statusText: 'Sample Status', + status: UserStatus.ONLINE, +}); + +describe('useUserPresence', () => { + it('should return presence from context', () => { + const { result } = renderHook(() => useUserPresence(user._id), { + wrapper: mockAppRoot().withUsers([user]).build(), + }); + + expect(result.current?.status).toEqual(UserStatus.ONLINE); + expect(result.current?.statusText).toEqual(SAMPLE_STATUS); + }); +}); diff --git a/packages/mock-providers/tsconfig.json b/packages/mock-providers/tsconfig.json index e2be47cf5499f..8166e3acd42ab 100644 --- a/packages/mock-providers/tsconfig.json +++ b/packages/mock-providers/tsconfig.json @@ -4,5 +4,6 @@ "rootDir": "./src", "outDir": "./dist" }, - "include": ["./src/**/*"] + "include": ["./src/**/*"], + "exclude": ["./src/**/*.spec.ts"] } diff --git a/packages/model-typings/CHANGELOG.md b/packages/model-typings/CHANGELOG.md index a34b03cc7ac1c..39049f7d1d933 100644 --- a/packages/model-typings/CHANGELOG.md +++ b/packages/model-typings/CHANGELOG.md @@ -1,5 +1,103 @@ # @rocket.chat/model-typings +## 1.6.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 +
      + +## 1.6.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 +
      + +## 1.6.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 +
      + +## 1.6.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 +
      + +## 1.6.0-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 +
      + +## 1.6.0-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 +
      + +## 1.6.0-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 +
      + +## 1.6.0-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 +
      + +## 1.6.0-rc.0 + +### Minor Changes + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +- ([#34494](https://github.com/RocketChat/Rocket.Chat/pull/34494)) Implements auditing events for `/v1/users.update` API endpoint + +### Patch Changes + +- ([#35497](https://github.com/RocketChat/Rocket.Chat/pull/35497)) Fixes an issue where the app's logs index was not being created by default sometimes, also set to be always 30 days + +- ([#35722](https://github.com/RocketChat/Rocket.Chat/pull/35722)) Fixes the behavior of "Maximum number of simultaneous chats" settings, making them more predictable. Previously, we applied a single limit per operation, being the order: `Department > Agent > Global`. This caused the department limit to take prescedence over agent's specific limit, causing some unwanted side effects. + + The new way of applying the filter is as follows: + + - An agent can accept chats from multiple departments, respecting each department’s limit individually. + - The total number of active chats (across all departments) must not exceed the configured Agent-Level or Global limit. + - If neither the Agent-Level nor Global Limit is set, only department-specific limits apply. + - If no limits are set at any level, there is no restriction on the number of chats an agent can handle. + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 +
      + ## 1.5.1 ### Patch Changes diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index e118f2176e4e5..e5ebdc6867b86 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/model-typings", - "version": "1.5.1", + "version": "1.6.0-rc.8", "private": true, "devDependencies": { "@types/node-rsa": "^1.1.4", diff --git a/packages/model-typings/src/models/IAppLogsModel.ts b/packages/model-typings/src/models/IAppLogsModel.ts index 6a6bc765cd8ed..0ff8ae0a00cf5 100644 --- a/packages/model-typings/src/models/IAppLogsModel.ts +++ b/packages/model-typings/src/models/IAppLogsModel.ts @@ -4,6 +4,5 @@ import type { IBaseModel } from './IBaseModel'; // TODO: type for AppLogs export interface IAppLogsModel extends IBaseModel { - resetTTLIndex(expireAfterSeconds: number): Promise; remove(query: Filter): Promise; } diff --git a/packages/model-typings/src/models/IBaseModel.ts b/packages/model-typings/src/models/IBaseModel.ts index c3a541a1d1a75..e293dfa52d750 100644 --- a/packages/model-typings/src/models/IBaseModel.ts +++ b/packages/model-typings/src/models/IBaseModel.ts @@ -25,7 +25,7 @@ import type { import type { Updater } from '../updater'; -export type DefaultFields = Record | Record | void; +export type DefaultFields = Partial> | Partial> | void; export type ResultFields = Defaults extends void ? Base : Defaults[keyof Defaults] extends 1 diff --git a/packages/model-typings/src/models/ILivechatContactsModel.ts b/packages/model-typings/src/models/ILivechatContactsModel.ts index a61e731a79b30..b93ec3b304bef 100644 --- a/packages/model-typings/src/models/ILivechatContactsModel.ts +++ b/packages/model-typings/src/models/ILivechatContactsModel.ts @@ -68,4 +68,5 @@ export interface ILivechatContactsModel extends IBaseModel { countVerified(): Promise; countContactsWithoutChannels(): Promise; getStatistics(): AggregationCursor<{ totalConflicts: number; avgChannelsPerContact: number }>; + updateByVisitorId(visitorId: string, update: UpdateFilter, options?: UpdateOptions): Promise; } diff --git a/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts b/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts index 827b266421b32..5435f1ea4458f 100644 --- a/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts +++ b/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts @@ -1,4 +1,4 @@ -import type { ILivechatDepartmentAgents, IUser } from '@rocket.chat/core-typings'; +import type { AvailableAgentsAggregation, ILivechatDepartmentAgents } from '@rocket.chat/core-typings'; import type { DeleteResult, FindCursor, FindOptions, Document, UpdateResult, Filter, AggregationCursor } from 'mongodb'; import type { FindPaginated, IBaseModel } from './IBaseModel'; @@ -57,7 +57,7 @@ export interface ILivechatDepartmentAgentsModel extends IBaseModel, + extraQuery?: Filter, ): Promise | null | undefined>; checkOnlineForDepartment(departmentId: string): Promise; getOnlineForDepartment( diff --git a/packages/model-typings/src/models/ILivechatInquiryModel.ts b/packages/model-typings/src/models/ILivechatInquiryModel.ts index 19b52f3f90e9c..5b5283aec610c 100644 --- a/packages/model-typings/src/models/ILivechatInquiryModel.ts +++ b/packages/model-typings/src/models/ILivechatInquiryModel.ts @@ -16,6 +16,7 @@ export interface ILivechatInquiryModel extends IBaseModel; setDepartmentByInquiryId(inquiryId: string, department: string): Promise; setLastMessageByRoomId(rid: ILivechatInquiryRecord['rid'], message: IMessage): Promise; + setLastMessageById(inquiryId: string, lastMessage: IMessage): Promise; findNextAndLock( queueSortBy: FindOptions['sort'], department: string | null, @@ -31,7 +32,7 @@ export interface ILivechatInquiryModel extends IBaseModel): FindCursor; takeInquiry(inquiryId: string): Promise; openInquiry(inquiryId: string): Promise; - queueInquiry(inquiryId: string): Promise; + queueInquiry(inquiryId: string, lastMessage?: IMessage): Promise; queueInquiryAndRemoveDefaultAgent(inquiryId: string): Promise; readyInquiry(inquiryId: string): Promise; changeDepartmentIdByRoomId(rid: string, department: string): Promise; diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index e11ac154244f5..63039ab40d025 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -1,13 +1,14 @@ import type { + AvailableAgentsAggregation, IUser, IRole, - IRoom, ILivechatAgent, UserStatus, ILoginToken, IPersonalAccessToken, AtLeast, ILivechatAgentStatus, + IMeteorLoginToken, } from '@rocket.chat/core-typings'; import type { Document, @@ -19,131 +20,145 @@ import type { DeleteResult, WithId, UpdateOptions, - ClientSession, + UpdateFilter, } from 'mongodb'; import type { FindPaginated, IBaseModel } from './IBaseModel'; export interface IUsersModel extends IBaseModel { addRolesByUserId(uid: IUser['_id'], roles: IRole['_id'][]): Promise; - findUsersInRoles(roles: IRole['_id'][], scope?: null, options?: any): FindCursor; - findPaginatedUsersInRoles(roles: IRole['_id'][], options?: any): FindPaginated>; + findUsersInRoles(roles: IRole['_id'][] | IRole['_id'], _scope?: null, options?: FindOptions): FindCursor; + findPaginatedUsersInRoles(roles: IRole['_id'][] | IRole['_id'], options?: FindOptions): FindPaginated>; findOneByIdWithEmailAddress(uid: IUser['_id'], options?: FindOptions): Promise; - findOneByUsername(username: string, options?: any): Promise; - findOneAgentById(_id: string, options: any): Promise; - findUsersInRolesWithQuery(roles: IRole['_id'] | IRole['_id'][], query: any, options: any): FindCursor; - findPaginatedUsersInRolesWithQuery( - roles: IRole['_id'] | IRole['_id'][], - query: any, - options: any, - ): FindPaginated>; - findOneByUsernameAndRoomIgnoringCase(username: string, rid: IRoom['_id'], options: any): FindCursor; - findOneByIdAndLoginHashedToken(_id: string, token: any, options?: any): FindCursor; - findByActiveUsersExcept( - searchTerm: any, - exceptions: any, - options: any, - searchFields: any, - extraQuery?: any, - params?: { startsWith?: boolean; endsWith?: boolean }, - ): FindCursor; - findPaginatedByActiveUsersExcept( - searchTerm: any, - exceptions: any, - options: any, - searchFields: any, - extraQuery?: any, - params?: { startsWith?: boolean; endsWith?: boolean }, - ): FindPaginated>; - - findPaginatedByActiveLocalUsersExcept( - searchTerm: any, - exceptions: any, - options: any, - forcedSearchFields: any, - localDomain: any, - ): FindPaginated>; + findOneByUsername(username: string, options?: FindOptions): Promise; + findOneAgentById(_id: IUser['_id'], options?: FindOptions): Promise; + findUsersInRolesWithQuery(roles: IRole['_id'][] | IRole['_id'], query: Filter, options?: FindOptions): FindCursor; + findPaginatedUsersInRolesWithQuery( + roles: IRole['_id'][] | IRole['_id'], + query: Filter, + options?: FindOptions, + ): FindPaginated>>; + findOneByUsernameAndRoomIgnoringCase(username: string | RegExp, rid: string, options?: FindOptions): Promise; + findOneByIdAndLoginHashedToken(_id: IUser['_id'], token: string, options?: FindOptions): Promise; + findByActiveUsersExcept( + searchTerm: string, + exceptions: string[], + options?: FindOptions, + searchFields?: string[], + extraQuery?: Filter[], + extra?: { startsWith: boolean; endsWith: boolean }, + ): FindCursor; + findPaginatedByActiveUsersExcept( + searchTerm: string, + exceptions?: string[], + options?: FindOptions, + searchFields?: string[], + extraQuery?: Filter[], + extra?: { startsWith?: boolean; endsWith?: boolean }, + ): FindPaginated>>; + + findPaginatedByActiveLocalUsersExcept( + searchTerm: string, + exceptions?: string[], + options?: FindOptions, + forcedSearchFields?: string[], + localDomain?: string, + ): FindPaginated>>; - findPaginatedByActiveExternalUsersExcept( - searchTerm: any, - exceptions: any, - options: any, - forcedSearchFields: any, - localDomain: any, - ): FindPaginated>; + findPaginatedByActiveExternalUsersExcept( + searchTerm: string, + exceptions?: string[], + options?: FindOptions, + forcedSearchFields?: string[], + localDomain?: string, + ): FindPaginated>>; - findActive(options?: any): FindCursor; + findActive(query: Filter, options?: FindOptions): FindCursor; - findActiveByIds(userIds: any, options?: any): FindCursor; + findActiveByIds(userIds: IUser['_id'][], options?: FindOptions): FindCursor; - findByIds(userIds: any, options?: any): FindCursor; + findByIds(userIds: IUser['_id'][], options?: FindOptions): FindCursor; - findOneByUsernameIgnoringCase(username: any, options?: any): Promise; + findOneByUsernameIgnoringCase(username: IUser['username'], options?: FindOptions): Promise; - findOneWithoutLDAPByUsernameIgnoringCase(username: string, options?: any): Promise; + findOneWithoutLDAPByUsernameIgnoringCase(username: string, options?: FindOptions): Promise; - findOneByLDAPId(id: any, attribute?: any): Promise; + findOneByLDAPId(id: string, attribute?: string): Promise; - findOneByAppId(appId: string, options?: FindOptions): Promise; + findOneByAppId(appId: string, options?: FindOptions): Promise; - findLDAPUsers(options?: any): FindCursor; + findLDAPUsers(options?: FindOptions): FindCursor; - findLDAPUsersExceptIds(userIds: IUser['_id'][], options?: FindOptions): FindCursor; + findLDAPUsersExceptIds(userIds: IUser['_id'][], options?: FindOptions): FindCursor; - findConnectedLDAPUsers(options?: any): FindCursor; + findConnectedLDAPUsers(options?: FindOptions): FindCursor; - isUserInRole(userId: IUser['_id'], roleId: IRole['_id']): Promise; + isUserInRole(userId: IUser['_id'], roleId: IRole['_id']): Promise | null>; - getDistinctFederationDomains(): any; + getDistinctFederationDomains(): Promise; getNextLeastBusyAgent( - department: any, - ignoreAgentId: any, + department?: string, + ignoreAgentId?: string, isEnabledWhenAgentIdle?: boolean, - ): Promise<{ agentId: string; username: string; lastRoutingTime: Date; departments: any[]; count: number }>; + ignoreUsernames?: string[], + ): Promise<{ agentId: string; username?: string; lastRoutingTime?: Date; count: number }>; getLastAvailableAgentRouted( - department: any, - ignoreAgentId: any, + department?: string, + ignoreAgentId?: string, isEnabledWhenAgentIdle?: boolean, - ): Promise<{ agentId: string; username: string; lastRoutingTime: Date; departments: any[] }>; + ignoreUsernames?: string[], + ): Promise<{ agentId: string; username?: string; lastRoutingTime?: Date }>; - setLastRoutingTime(userId: any): Promise; + setLastRoutingTime(userId: IUser['_id']): Promise | null>; - setLivechatStatusIf(userId: string, status: ILivechatAgentStatus, conditions?: any, extraFields?: any): Promise; + setLivechatStatusIf( + userId: IUser['_id'], + status: ILivechatAgentStatus, + conditions?: Filter, + extraFields?: UpdateFilter['$set'], + ): Promise; getAgentAndAmountOngoingChats( - userId: any, - ): Promise<{ agentId: string; username: string; lastAssignTime: Date; lastRoutingTime: Date; queueInfo: { chats: number } }>; - - findAllResumeTokensByUserId(userId: any): any; - - findActiveByUsernameOrNameRegexWithExceptionsAndConditions( - termRegex: any, - exceptions: any, - conditions: any, - options: any, + userId: IUser['_id'], + departmentId?: string, + ): Promise<{ + agentId: string; + username?: string; + lastAssignTime?: Date; + lastRoutingTime?: Date; + queueInfo: { chats: number; chatsForDepartment?: number }; + }>; + + findAllResumeTokensByUserId(userId: IUser['_id']): Promise<{ tokens: IMeteorLoginToken[] }[]>; + + findActiveByUsernameOrNameRegexWithExceptionsAndConditions( + termRegex: { $regex: string; $options: string } | RegExp, + exceptions?: string[], + conditions?: Filter, + options?: FindOptions, ): FindCursor; - countAllAgentsStatus({ departmentId }: { departmentId?: any }): any; + countAllAgentsStatus({ + departmentId, + }: { + departmentId?: string; + }): Promise<{ offline: number; away: number; busy: number; available: number }[]>; - getTotalOfRegisteredUsersByDate({ start, end, options }: { start: any; end: any; options?: any }): Promise; - // TODO change back to below when convert model to TS - // Promise< - // { - // date: string; - // users: number; - // type: 'users'; - // }[] - // >; + getTotalOfRegisteredUsersByDate(params: { + start: Date; + end: Date; + options?: { count?: number; sort?: Record }; + }): Promise<{ date: string; users: number; type: 'users' }[]>; - getUserLanguages(): any; + getUserLanguages(): Promise<{ _id: string; total: number }[]>; - updateStatusText(_id: any, statusText: any, options?: { session?: ClientSession }): any; + updateStatusText(_id: IUser['_id'], statusText: string, options?: UpdateOptions): Promise; - updateStatusByAppId(appId: any, status: any): any; + updateStatusByAppId(appId: string, status: UserStatus): Promise; - openAgentsBusinessHoursByBusinessHourId(businessHourIds: any): any; + openAgentsBusinessHoursByBusinessHourId(businessHourIds: string[]): Promise; - openAgentBusinessHoursByBusinessHourIdsAndAgentId(businessHourIds: string[], agentId: string): Promise; + openAgentBusinessHoursByBusinessHourIdsAndAgentId(businessHourIds: string[], agentId: IUser['_id']): Promise; addBusinessHourByAgentIds(agentIds: string[], businessHourId: string): any; @@ -189,7 +204,11 @@ export interface IUsersModel extends IBaseModel { unsetExtension(userId: any): any; - getAvailableAgentsIncludingExt(includeExt: any, text: any, options: any): FindPaginated>; + getAvailableAgentsIncludingExt( + includeExt?: string, + text?: string, + options?: FindOptions, + ): FindPaginated>>; findActiveUsersTOTPEnable(options: any): any; @@ -199,7 +218,7 @@ export interface IUsersModel extends IBaseModel { countActiveUsersEmail2faEnable(options: any): Promise; - findActiveByIdsOrUsernames(userIds: string[], options?: any): FindCursor; + findActiveByIdsOrUsernames(userIds: IUser['_id'][], options?: FindOptions): FindCursor; setAsFederated(userId: string): any; @@ -208,69 +227,66 @@ export interface IUsersModel extends IBaseModel { findOneByResetToken(token: string, options: FindOptions): Promise; updateStatusById( - userId: string, + userId: IUser['_id'], { statusDefault, status, statusConnection, statusText, - }: { statusDefault?: string; status: UserStatus; statusConnection: UserStatus; statusText?: string }, + }: { statusDefault?: UserStatus; status: UserStatus; statusConnection: UserStatus; statusText?: string }, ): Promise; updateStatusAndStatusDefault(userId: string, status: UserStatus, statusDefault: UserStatus): Promise; setFederationAvatarUrlById(userId: IUser['_id'], federationAvatarUrl: string): Promise; - findSearchedServerNamesByUserId(userId: string): Promise; + setFederationAvatarUrlById(userId: IUser['_id'], federationAvatarUrl: string): Promise; + + findSearchedServerNamesByUserId(userId: IUser['_id']): Promise; addServerNameToSearchedServerNamesList(userId: string, serverName: string): Promise; removeServerNameFromSearchedServerNamesList(userId: string, serverName: string): Promise; countFederatedExternalUsers(): Promise; - findOnlineUserFromList(userList: string[], isLivechatEnabledWhenAgentIdle?: boolean): FindCursor; - countOnlineUserFromList(userList: string[], isLivechatEnabledWhenAgentIdle?: boolean): Promise; + findOnlineUserFromList( + userList: string | string[], + isLivechatEnabledWhenAgentIdle?: boolean, + ): FindCursor; + countOnlineUserFromList(userList: string | string[], isLivechatEnabledWhenAgentIdle?: boolean): Promise; getUnavailableAgents( departmentId?: string, - extraQuery?: Document, - ): Promise< - { - agentId: string; - username: string; - lastAssignTime: string; - lastRoutingTime: string; - livechat: { maxNumberSimultaneousChat: number }; - queueInfo: { chats: number }; - }[] - >; + extraQuery?: Filter, + ): Promise[]>; findOneOnlineAgentByUserList( userList: string[] | string, options?: FindOptions, isLivechatEnabledWhenAgentIdle?: boolean, ): Promise; - findBotAgents(usernameList?: string[]): FindCursor; - countBotAgents(usernameList?: string[]): Promise; + findBotAgents(usernameList?: string | string[]): FindCursor; + countBotAgents(usernameList?: string | string[]): Promise; removeAllRoomsByUserId(userId: string): Promise; removeRoomByUserId(userId: string, rid: string): Promise; addRoomByUserId(userId: string, rid: string): Promise; - addRoomByUserIds(uids: string[], rid: string): Promise; + addRoomByUserIds(uids: string[], rid: string): Promise; removeRoomByRoomIds(rids: string[]): Promise; addRoomRolePriorityByUserId(userId: string, rid: string, rolePriority: number): Promise; removeRoomRolePriorityByUserId(userId: string, rid: string): Promise; assignRoomRolePrioritiesByUserIdPriorityMap(rolePrioritiesMap: Record, rid: string): Promise; - unassignRoomRolePrioritiesByRoomId(rid: string): Promise; + unassignRoomRolePrioritiesByRoomId(rid: string): Promise; getLoginTokensByUserId(userId: string): FindCursor; addPersonalAccessTokenToUser(data: { userId: string; loginTokenObject: IPersonalAccessToken }): Promise; removePersonalAccessTokenOfUser(data: { userId: string; loginTokenObject: AtLeast; }): Promise; - findPersonalAccessTokenByTokenNameAndUserId(data: { userId: string; tokenName: string }): Promise; + findPersonalAccessTokenByTokenNameAndUserId({ userId, tokenName }: { userId: IUser['_id']; tokenName: string }): Promise; setOperator(userId: string, operator: boolean): Promise; checkOnlineAgents(agentId?: string, isLivechatEnabledWhenIdle?: boolean): Promise; - findOnlineAgents(agentId?: string, isLivechatEnabledWhenIdle?: boolean): FindCursor; - findOneBotAgent(): Promise; + findOnlineAgents(agentId?: IUser['_id'], isLivechatEnabledWhenIdle?: boolean): FindCursor; + countOnlineAgents(agentId: string): Promise; + findOneBotAgent(): Promise; findOneOnlineAgentById( agentId: string, isLivechatEnabledWhenAgentIdle?: boolean, @@ -280,23 +296,23 @@ export interface IUsersModel extends IBaseModel { countAgents(): Promise; getNextAgent( ignoreAgentId?: string, - extraQuery?: Filter, + extraQuery?: Filter, enabledWhenAgentIdle?: boolean, - ): Promise<{ agentId: string; username: string } | null>; - getNextBotAgent(ignoreAgentId?: string): Promise<{ agentId: string; username: string } | null>; + ): Promise<{ agentId: string; username?: string } | null>; + getNextBotAgent(ignoreAgentId?: string): Promise<{ agentId: string; username?: string } | null>; setLivechatStatus(userId: string, status: ILivechatAgentStatus): Promise; makeAgentUnavailableAndUnsetExtension(userId: string): Promise; setLivechatData(userId: string, data?: Record): Promise; closeOffice(): Promise; openOffice(): Promise; getAgentInfo( - agentId: string, + agentId: IUser['_id'], showAgentEmail?: boolean, - ): Promise | null>; + ): Promise | null>; roleBaseQuery(userId: string): { _id: string }; setE2EPublicAndPrivateKeysByUserId(userId: string, e2e: { public_key: string; private_key: string }): Promise; rocketMailUnsubscribe(userId: string, createdAt: string): Promise; - fetchKeysByUserId(userId: string): Promise<{ public_key: string; private_key: string } | Record>; + fetchKeysByUserId(userId: string): Promise<{ public_key: string; private_key: string } | object>; disable2FAAndSetTempSecretByUserId(userId: string, tempSecret: string): Promise; enable2FAAndSetSecretAndCodesByUserId(userId: string, secret: string, codes: string[]): Promise; disable2FAByUserId(userId: string): Promise; @@ -306,8 +322,6 @@ export interface IUsersModel extends IBaseModel { findByIdsWithPublicE2EKey(userIds: string[], options?: FindOptions): FindCursor; resetE2EKey(userId: string): Promise; removeExpiredEmailCodeOfUserId(userId: string): Promise; - removeEmailCodeByUserId(userId: string): Promise; - increaseInvalidEmailCodeAttempt(userId: string): Promise; maxInvalidEmailCodeAttemptsReached(userId: string, maxAttemtps: number): Promise; addEmailCodeByUserId(userId: string, code: string, expire: Date): Promise; findActiveUsersInRoles(roles: string[], options?: FindOptions): FindCursor; @@ -330,12 +344,12 @@ export interface IUsersModel extends IBaseModel { findOneByIdAndLoginToken(userId: string, loginToken: string, options?: FindOptions): Promise; findOneActiveById(userId: string, options?: FindOptions): Promise; findOneByIdOrUsername(userId: string, options?: FindOptions): Promise; - findOneByRolesAndType(roles: string[], type: string, options?: FindOptions): Promise; + findOneByRolesAndType(roles: IRole['_id'][], type: string, options?: FindOptions): Promise; findNotOfflineByIds(userIds: string[], options?: FindOptions): FindCursor; findUsersNotOffline(options?: FindOptions): FindCursor; countUsersNotOffline(options?: FindOptions): Promise; findNotIdUpdatedFrom(userId: string, updatedFrom: Date, options?: FindOptions): FindCursor; - findByRoomId(roomId: string, options?: FindOptions): FindCursor; + findByRoomId(roomId: string, options?: FindOptions): Promise>; findByUsername(username: string, options?: FindOptions): FindCursor; findByUsernames(usernames: string[], options?: FindOptions): FindCursor; findByUsernamesIgnoringCase(usernames: string[], options?: FindOptions): FindCursor; @@ -346,28 +360,27 @@ export interface IUsersModel extends IBaseModel { findByUsernameNameOrEmailAddress(nameOrUsernameOrEmail: string, options?: FindOptions): FindCursor; findCrowdUsers(options?: FindOptions): FindCursor; getLastLogin(options?: FindOptions): Promise; - findUsersByUsernames(usernames: string[], options?: FindOptions): FindCursor; + findUsersByUsernames(usernames: string[], options?: FindOptions): FindCursor; findUsersByIds(userIds: string[], options?: FindOptions): FindCursor; findUsersWithUsernameByIds(userIds: string[], options?: FindOptions): FindCursor; findUsersWithUsernameByIdsNotOffline(userIds: string[], options?: FindOptions): FindCursor; getOldest(options?: FindOptions): Promise; - findActiveRemoteUsers(options?: FindOptions): FindCursor; findActiveFederated(options?: FindOptions): FindCursor; getSAMLByIdAndSAMLProvider(userId: string, samlProvider: string): Promise; - findBySAMLNameIdOrIdpSession(samlNameId: string, idpSession: string): FindCursor; - findBySAMLInResponseTo(inResponseTo: string): FindCursor; + findBySAMLNameIdOrIdpSession(samlNameId: string, idpSession: string, options?: FindOptions): FindCursor; + findBySAMLInResponseTo(inResponseTo: string, options?: FindOptions): FindCursor; addImportIds(userId: string, importIds: string | string[]): Promise; updateInviteToken(userId: string, token: string): Promise; updateLastLoginById(userId: string): Promise; addPasswordToHistory(userId: string, password: string, passwordHistoryAmount: number): Promise; setServiceId(userId: string, serviceName: string, serviceId: string): Promise; - setUsername(userId: string, username: string, options?: { session?: ClientSession }): Promise; - setEmail(userId: string, email: string, verified?: boolean, options?: { session?: ClientSession }): Promise; + setUsername(userId: string, username: string, options?: UpdateOptions): Promise; + setEmail(userId: string, email: string, verified?: boolean, options?: UpdateOptions): Promise; setEmailVerified(userId: string, email: string): Promise; - setName(userId: string, name: string, options?: { session?: ClientSession }): Promise; - unsetName(userId: string, options?: { session?: ClientSession }): Promise; + setName(userId: string, name: string, options?: UpdateOptions): Promise; + unsetName(userId: string, options?: UpdateOptions): Promise; setCustomFields(userId: string, customFields: Record): Promise; - setAvatarData(userId: string, origin: string, etag?: Date | null | string, options?: { session?: ClientSession }): Promise; + setAvatarData(userId: string, origin: string, etag?: Date | null | string, options?: UpdateOptions): Promise; unsetAvatarData(userId: string): Promise; setUserActive(userId: string, active: boolean): Promise; setAllUsersActive(active: boolean): Promise; @@ -386,7 +399,6 @@ export interface IUsersModel extends IBaseModel { setPreferences(userId: string, preferences: Record): Promise; setTwoFactorAuthorizationHashAndUntilForUserIdAndToken(userId: string, token: string, hash: string, until: Date): Promise; setUtcOffset(userId: string, utcOffset: number): Promise; - saveUserById(userId: string, user: Partial): Promise; setReason(userId: string, reason: string): Promise; unsetReason(userId: string): Promise; bannerExistsById(userId: string, bannerId: string): Promise; @@ -407,24 +419,27 @@ export interface IUsersModel extends IBaseModel { updateCustomFieldsById(userId: string, customFields: Record): Promise; countRoomMembers(roomId: string): Promise; countRemote(options?: FindOptions): Promise; - findOneByImportId(importId: string, options?: FindOptions): Promise; + findOneByImportId(_id: IUser['_id'], options?: FindOptions): Promise; removeAgent(_id: string): Promise; - findAgentsWithDepartments( - role: string, + findAgentsWithDepartments( + role: IRole['_id'][] | IRole['_id'], query: Filter, - options: FindOptions, + options?: FindOptions, ): Promise<{ sortedResults: (T & { departments: string[] })[]; totalCount: { total: number }[] }[]>; countByRole(roleName: string): Promise; removeEmailCodeOfUserId(userId: string): Promise; incrementInvalidEmailCodeAttempt(userId: string): Promise | null>; - findOnlineButNotAvailableAgents(userIds: string[] | null): FindCursor>; - findAgentsAvailableWithoutBusinessHours(userIds: string[] | null): FindCursor>; - updateLivechatStatusByAgentIds(userIds: string[], status: ILivechatAgentStatus): Promise; - findOneByFreeSwitchExtension(extension: string, options?: FindOptions): Promise; - findOneByFreeSwitchExtensions(extensions: string[], options?: FindOptions): Promise; + findOnlineButNotAvailableAgents(userIds?: IUser['_id'][]): FindCursor; + findAgentsAvailableWithoutBusinessHours(userIds?: IUser['_id'][]): FindCursor>; + updateLivechatStatusByAgentIds(userIds: string[], status: ILivechatAgentStatus): Promise; + findOneByFreeSwitchExtension(freeSwitchExtension: string, options?: FindOptions): Promise; + findOneByFreeSwitchExtensions( + freeSwitchExtensions: string[], + options?: FindOptions, + ): Promise; setFreeSwitchExtension(userId: string, extension: string | undefined): Promise; - findAssignedFreeSwitchExtensions(): FindCursor; - findUsersWithAssignedFreeSwitchExtensions(options?: FindOptions): FindCursor; + findAssignedFreeSwitchExtensions(): FindCursor; + findUsersWithAssignedFreeSwitchExtensions(options?: FindOptions): FindCursor; countUsersInRoles(roles: IRole['_id'][]): Promise; countAllUsersWithPendingAvatar(): Promise; } diff --git a/packages/model-typings/src/updater.ts b/packages/model-typings/src/updater.ts index 7743430ad4b8b..d9ec9a84f57bf 100644 --- a/packages/model-typings/src/updater.ts +++ b/packages/model-typings/src/updater.ts @@ -8,6 +8,7 @@ export interface Updater { addToSet>(key: K, value: ArrayElementType[K]>): Updater; hasChanges(): boolean; getUpdateFilter(): UpdateFilter; + getRawUpdateFilter(): UpdateFilter; } type ArrayElementType = T extends (infer E)[] ? E : T; diff --git a/packages/models/CHANGELOG.md b/packages/models/CHANGELOG.md index c82f44ddfee33..5c77862c98291 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/CHANGELOG.md @@ -1,5 +1,116 @@ # @rocket.chat/models +## 1.5.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/rest-typings@7.6.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 +
      + +## 1.5.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/rest-typings@7.6.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 +
      + +## 1.5.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/rest-typings@7.6.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 +
      + +## 1.5.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/rest-typings@7.6.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 +
      + +## 1.5.0-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/rest-typings@7.6.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 +
      + +## 1.5.0-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/rest-typings@7.6.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 +
      + +## 1.5.0-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/rest-typings@7.6.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 +
      + +## 1.5.0-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/rest-typings@7.6.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 +
      + +## 1.5.0-rc.0 + +### Minor Changes + +- ([#34954](https://github.com/RocketChat/Rocket.Chat/pull/34954) by [@tapiarafael](https://github.com/tapiarafael)) Allows search omnichannel rooms by the exact visitor name using double quotes to have a faster response + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +- ([#34494](https://github.com/RocketChat/Rocket.Chat/pull/34494)) Implements auditing events for `/v1/users.update` API endpoint + +### Patch Changes + +- ([#35497](https://github.com/RocketChat/Rocket.Chat/pull/35497)) Fixes an issue where the app's logs index was not being created by default sometimes, also set to be always 30 days + +- ([#35722](https://github.com/RocketChat/Rocket.Chat/pull/35722)) Fixes the behavior of "Maximum number of simultaneous chats" settings, making them more predictable. Previously, we applied a single limit per operation, being the order: `Department > Agent > Global`. This caused the department limit to take prescedence over agent's specific limit, causing some unwanted side effects. + + The new way of applying the filter is as follows: + + - An agent can accept chats from multiple departments, respecting each department’s limit individually. + - The total number of active chats (across all departments) must not exceed the configured Agent-Level or Global limit. + - If neither the Agent-Level nor Global Limit is set, only department-specific limits apply. + - If no limits are set at any level, there is no restriction on the number of chats an agent can handle. + +- ([#35618](https://github.com/RocketChat/Rocket.Chat/pull/35618)) Fixes an issue when sending a message on an omnichannel room would cause the web client to fetch the room data again. + +-
      Updated dependencies [45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, d8eb824d242cbbeafb11b1c4a806860e4541ba79, 47ae69912cd90743e7bf836fdee4be481a01bbba]: + + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 +
      + ## 1.4.1 ### Patch Changes diff --git a/packages/models/package.json b/packages/models/package.json index c70518387a2f6..33c861e7e8439 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/models", - "version": "1.4.1", + "version": "1.5.0-rc.8", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/models/src/models/AppLogsModel.ts b/packages/models/src/models/AppLogsModel.ts index f9f7db79d86c5..4be9cc9e4dcbc 100644 --- a/packages/models/src/models/AppLogsModel.ts +++ b/packages/models/src/models/AppLogsModel.ts @@ -5,17 +5,21 @@ import { BaseRaw } from './BaseRaw'; export class AppsLogsModel extends BaseRaw implements IAppLogsModel { constructor(db: Db) { - super(db, 'apps_logs', undefined, { _updatedAtIndexOptions: { expireAfterSeconds: 60 * 60 * 24 * 30 } }); + super(db, 'apps_logs', undefined); } - remove(query: Filter): Promise { - return this.col.deleteMany(query); + protected modelIndexes() { + return [ + { + key: { + _updatedAt: 1, + }, + expireAfterSeconds: 60 * 60 * 24 * 30, + }, + ]; } - async resetTTLIndex(expireAfterSeconds: number): Promise { - if (await this.col.indexExists('_updatedAt_1')) { - await this.col.dropIndex('_updatedAt_1'); - } - await this.col.createIndex({ _updatedAt: 1 }, { expireAfterSeconds }); + remove(query: Filter): Promise { + return this.col.deleteMany(query); } } diff --git a/packages/models/src/models/BaseRaw.ts b/packages/models/src/models/BaseRaw.ts index 047c889ed1611..d6c75759c3ecf 100644 --- a/packages/models/src/models/BaseRaw.ts +++ b/packages/models/src/models/BaseRaw.ts @@ -44,7 +44,6 @@ type ModelOptions = { preventSetUpdatedAt?: boolean; collectionNameResolver?: (name: string) => string; collection?: CollectionOptions; - _updatedAtIndexOptions?: Omit; }; export abstract class BaseRaw< @@ -53,7 +52,7 @@ export abstract class BaseRaw< TDeleted extends RocketChatRecordDeleted = RocketChatRecordDeleted, > implements IBaseModel { - public readonly defaultFields: C | undefined; + protected defaultFields: C | undefined; public readonly col: Collection; @@ -74,7 +73,7 @@ export abstract class BaseRaw< private db: Db, protected name: string, protected trash?: Collection, - private options?: ModelOptions, + options?: ModelOptions, ) { this.collectionName = options?.collectionNameResolver ? options.collectionNameResolver(name) : getCollectionName(name); @@ -91,9 +90,6 @@ export abstract class BaseRaw< public async createIndexes() { const indexes = this.modelIndexes(); - if (this.options?._updatedAtIndexOptions) { - indexes?.push({ ...this.options._updatedAtIndexOptions, key: { _updatedAt: 1 } }); - } if (indexes?.length) { if (this.pendingIndexes) { diff --git a/packages/models/src/models/LivechatContacts.ts b/packages/models/src/models/LivechatContacts.ts index 5e0a30c8dd81b..927d7b82a6bef 100644 --- a/packages/models/src/models/LivechatContacts.ts +++ b/packages/models/src/models/LivechatContacts.ts @@ -374,4 +374,8 @@ export class LivechatContactsRaw extends BaseRaw implements IL { allowDiskUse: true, readPreference: readSecondaryPreferred() }, ); } + + updateByVisitorId(visitorId: string, update: UpdateFilter, options?: UpdateOptions): Promise { + return this.updateOne({ 'channels.visitor.visitorId': visitorId }, update, options); + } } diff --git a/packages/models/src/models/LivechatDepartmentAgents.ts b/packages/models/src/models/LivechatDepartmentAgents.ts index 064a59704154c..ddb5b7f40e518 100644 --- a/packages/models/src/models/LivechatDepartmentAgents.ts +++ b/packages/models/src/models/LivechatDepartmentAgents.ts @@ -1,4 +1,4 @@ -import type { ILivechatDepartmentAgents, RocketChatRecordDeleted, IUser } from '@rocket.chat/core-typings'; +import type { AvailableAgentsAggregation, ILivechatDepartmentAgents, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; import type { FindPaginated, ILivechatDepartmentAgentsModel } from '@rocket.chat/model-typings'; import type { Collection, @@ -177,7 +177,7 @@ export class LivechatDepartmentAgentsRaw extends BaseRaw, + extraQuery?: Filter, ): Promise | null | undefined> { const agents = await this.findByDepartmentId(departmentId).toArray(); diff --git a/packages/models/src/models/LivechatInquiry.ts b/packages/models/src/models/LivechatInquiry.ts index d1361f265494c..79d581ce888ff 100644 --- a/packages/models/src/models/LivechatInquiry.ts +++ b/packages/models/src/models/LivechatInquiry.ts @@ -153,8 +153,19 @@ export class LivechatInquiryRaw extends BaseRaw implemen return this.findOneAndUpdate({ _id: inquiryId }, { $set: { department } }, { returnDocument: 'after' }); } + /** + * Updates the `lastMessage` of inquiries that are not taken yet, after they're taken we only need to update room's `lastMessage` + */ async setLastMessageByRoomId(rid: ILivechatInquiryRecord['rid'], message: IMessage): Promise { - return this.findOneAndUpdate({ rid }, { $set: { lastMessage: message } }, { returnDocument: 'after' }); + return this.findOneAndUpdate( + { rid, status: { $ne: LivechatInquiryStatus.TAKEN } }, + { $set: { lastMessage: message } }, + { returnDocument: 'after' }, + ); + } + + async setLastMessageById(inquiryId: string, lastMessage: IMessage): Promise { + return this.updateOne({ _id: inquiryId }, { $set: { lastMessage } }); } async findNextAndLock( @@ -316,13 +327,17 @@ export class LivechatInquiryRaw extends BaseRaw implemen ); } - async queueInquiry(inquiryId: string): Promise { + async queueInquiry(inquiryId: string, lastMessage?: IMessage): Promise { return this.findOneAndUpdate( { _id: inquiryId, }, { - $set: { status: LivechatInquiryStatus.QUEUED, queuedAt: new Date() }, + $set: { + status: LivechatInquiryStatus.QUEUED, + queuedAt: new Date(), + ...(lastMessage && { lastMessage }), + }, $unset: { takenAt: 1 }, }, { returnDocument: 'after' }, diff --git a/packages/models/src/models/LivechatRooms.ts b/packages/models/src/models/LivechatRooms.ts index aef7c2e495082..366fcc02820d7 100644 --- a/packages/models/src/models/LivechatRooms.ts +++ b/packages/models/src/models/LivechatRooms.ts @@ -1275,11 +1275,16 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive options?: { offset?: number; count?: number; sort?: { [k: string]: SortDirection } }; extraQuery?: Filter; }) { + const isRoomNameExactTerm = roomName?.startsWith(`"`) && roomName?.endsWith(`"`); + const roomNameQuery = isRoomNameExactTerm ? roomName?.slice(1, -1) : roomName; + const query: Filter = { t: 'l', ...extraQuery, ...(agents && { 'servedBy._id': { $in: agents } }), - ...(roomName && { fname: new RegExp(escapeRegExp(roomName), 'i') }), + ...(roomName && isRoomNameExactTerm + ? { fname: roomNameQuery } // exact match + : roomName && { fname: new RegExp(escapeRegExp(roomName), 'i') }), // regex match ...(departmentId && departmentId !== 'undefined' && { departmentId: { $in: ([] as string[]).concat(departmentId) } }), ...(open !== undefined && { open: { $exists: open }, onHold: { $ne: true } }), ...(served !== undefined && { servedBy: { $exists: served } }), diff --git a/packages/models/src/models/Users.js b/packages/models/src/models/Users.ts similarity index 60% rename from packages/models/src/models/Users.js rename to packages/models/src/models/Users.ts index ad98e391f67f2..9fa764fd0e9da 100644 --- a/packages/models/src/models/Users.js +++ b/packages/models/src/models/Users.ts @@ -1,10 +1,38 @@ -import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; -import { Subscriptions } from '@rocket.chat/models'; +import type { + AvailableAgentsAggregation, + AtLeast, + DeepWritable, + ILivechatAgent, + ILoginToken, + IMeteorLoginToken, + IPersonalAccessToken, + IRole, + IRoom, + IUser, + RocketChatRecordDeleted, +} from '@rocket.chat/core-typings'; +import { ILivechatAgentStatus, UserStatus } from '@rocket.chat/core-typings'; +import type { DefaultFields, InsertionModel, IUsersModel } from '@rocket.chat/model-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; - +import type { + Collection, + Db, + Filter, + FindOptions, + IndexDescription, + Document, + UpdateFilter, + UpdateOptions, + FindCursor, + SortDirection, + UpdateResult, + FindOneAndUpdateOptions, +} from 'mongodb'; + +import { Subscriptions } from '../index'; import { BaseRaw } from './BaseRaw'; -const queryStatusAgentOnline = (extraFilters = {}, isLivechatEnabledWhenAgentIdle) => ({ +const queryStatusAgentOnline = (extraFilters = {}, isLivechatEnabledWhenAgentIdle?: boolean): Filter => ({ statusLivechat: 'available', roles: 'livechat-agent', // ignore deactivated users @@ -14,7 +42,7 @@ const queryStatusAgentOnline = (extraFilters = {}, isLivechatEnabledWhenAgentIdl { status: { $exists: true, - $ne: 'offline', + $ne: UserStatus.OFFLINE, }, roles: { $ne: 'bot', @@ -31,8 +59,8 @@ const queryStatusAgentOnline = (extraFilters = {}, isLivechatEnabledWhenAgentIdl }), }); -export class UsersRaw extends BaseRaw { - constructor(db, trash) { +export class UsersRaw extends BaseRaw> implements IUsersModel { + constructor(db: Db, trash?: Collection>) { super(db, 'users', trash, { collectionNameResolver(name) { return name; @@ -45,19 +73,19 @@ export class UsersRaw extends BaseRaw { } // Move index from constructor to here - modelIndexes() { + modelIndexes(): IndexDescription[] { return [ - { key: { __rooms: 1 }, sparse: 1 }, - { key: { roles: 1 }, sparse: 1 }, + { key: { __rooms: 1 }, sparse: true }, + { key: { roles: 1 }, sparse: true }, { key: { name: 1 } }, - { key: { bio: 1 }, sparse: 1 }, - { key: { nickname: 1 }, sparse: 1 }, + { key: { bio: 1 }, sparse: true }, + { key: { nickname: 1 }, sparse: true }, { key: { createdAt: 1 } }, { key: { lastLogin: 1 } }, { key: { status: 1 } }, { key: { statusText: 1 } }, - { key: { statusConnection: 1 }, sparse: 1 }, - { key: { appId: 1 }, sparse: 1 }, + { key: { statusConnection: 1 }, sparse: true }, + { key: { appId: 1 }, sparse: true }, { key: { type: 1 } }, { key: { federated: 1 }, sparse: true }, { key: { federation: 1 }, sparse: true }, @@ -106,7 +134,7 @@ export class UsersRaw extends BaseRaw { * @param {string} uid * @param {IRole['_id'][]} roles list of role ids */ - addRolesByUserId(uid, roles) { + addRolesByUserId(uid: string, roles: string | string[]) { if (!Array.isArray(roles)) { roles = [roles]; process.env.NODE_ENV === 'development' && console.warn('[WARN] Users.addRolesByUserId: roles should be an array'); @@ -129,8 +157,8 @@ export class UsersRaw extends BaseRaw { * @param {null} scope the value for the role scope (room id) - not used in the users collection * @param {any} options */ - findUsersInRoles(roles, scope, options) { - roles = [].concat(roles); + findUsersInRoles(roles: IRole['_id'][] | IRole['_id'], _scope?: null, options?: FindOptions) { + roles = ([] as string[]).concat(roles); const query = { roles: { $in: roles }, @@ -139,8 +167,8 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - countUsersInRoles(roles) { - roles = [].concat(roles); + countUsersInRoles(roles: IRole['_id'][] | IRole['_id']) { + roles = ([] as string[]).concat(roles); const query = { roles: { $in: roles }, @@ -149,8 +177,8 @@ export class UsersRaw extends BaseRaw { return this.countDocuments(query); } - findPaginatedUsersInRoles(roles, options) { - roles = [].concat(roles); + findPaginatedUsersInRoles(roles: IRole['_id'][] | IRole['_id'], options?: FindOptions) { + roles = ([] as string[]).concat(roles); const query = { roles: { $in: roles }, @@ -159,19 +187,19 @@ export class UsersRaw extends BaseRaw { return this.findPaginated(query, options); } - findOneByUsername(username, options = null) { + findOneByUsername(username: string, options?: FindOptions) { const query = { username }; - return this.findOne(query, options); + return this.findOne(query, options); } - findOneAgentById(_id, options) { + findOneAgentById(_id: IUser['_id'], options?: FindOptions) { const query = { _id, roles: 'livechat-agent', }; - return this.findOne(query, options); + return this.findOne(query, options); } /** @@ -179,8 +207,8 @@ export class UsersRaw extends BaseRaw { * @param {any} query * @param {any} options */ - findUsersInRolesWithQuery(roles, query, options) { - roles = [].concat(roles); + findUsersInRolesWithQuery(roles: IRole['_id'][] | IRole['_id'], query: Filter, options?: FindOptions) { + roles = ([] as string[]).concat(roles); Object.assign(query, { roles: { $in: roles } }); @@ -192,16 +220,24 @@ export class UsersRaw extends BaseRaw { * @param {any} query * @param {any} options */ - findPaginatedUsersInRolesWithQuery(roles, query, options) { - roles = [].concat(roles); + findPaginatedUsersInRolesWithQuery( + roles: IRole['_id'][] | IRole['_id'], + query: Filter, + options?: FindOptions, + ) { + roles = ([] as string[]).concat(roles); Object.assign(query, { roles: { $in: roles } }); - return this.findPaginated(query, options); + return this.findPaginated(query, options); } - findAgentsWithDepartments(role, query, options) { - const roles = [].concat(role); + findAgentsWithDepartments( + role: IRole['_id'][] | IRole['_id'], + query: Filter, + options?: FindOptions, + ): Promise<{ sortedResults: (T & { departments: string[] })[]; totalCount: { total: number }[] }[]> { + const roles = ([] as string[]).concat(role); Object.assign(query, { roles: { $in: roles } }); @@ -237,16 +273,16 @@ export class UsersRaw extends BaseRaw { }, { $facet: { - sortedResults: [{ $sort: options.sort }, { $skip: options.skip }, options.limit && { $limit: options.limit }], + sortedResults: [{ $sort: options?.sort }, { $skip: options?.skip }, options?.limit && { $limit: options.limit }], totalCount: [{ $group: { _id: null, total: { $sum: 1 } } }], }, }, ]; - return this.col.aggregate(aggregate).toArray(); + return this.col.aggregate<{ sortedResults: (T & { departments: string[] })[]; totalCount: { total: number }[] }>(aggregate).toArray(); } - findOneByUsernameAndRoomIgnoringCase(username, rid, options) { + findOneByUsernameAndRoomIgnoringCase(username: string | RegExp, rid: string, options?: FindOptions) { if (typeof username === 'string') { username = new RegExp(`^${escapeRegExp(username)}$`, 'i'); } @@ -259,7 +295,7 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - findOneByIdAndLoginHashedToken(_id, token, options = {}) { + findOneByIdAndLoginHashedToken(_id: IUser['_id'], token: string, options: FindOptions = {}) { const query = { _id, 'services.resume.loginTokens.hashedToken': token, @@ -268,7 +304,14 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - findByActiveUsersExcept(searchTerm, exceptions, options, searchFields, extraQuery = [], { startsWith = false, endsWith = false } = {}) { + findByActiveUsersExcept( + searchTerm: string, + exceptions: string[], + options?: FindOptions, + searchFields?: string[], + extraQuery: Filter[] = [], + { startsWith = false, endsWith = false } = {}, + ) { if (exceptions == null) { exceptions = []; } @@ -281,10 +324,13 @@ export class UsersRaw extends BaseRaw { const termRegex = new RegExp((startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : ''), 'i'); - const orStmt = (searchFields || []).reduce((acc, el) => { - acc.push({ [el.trim()]: termRegex }); - return acc; - }, []); + const orStmt = (searchFields || []).reduce( + (acc, el) => { + acc.push({ [el.trim()]: termRegex }); + return acc; + }, + [] as Record[], + ); const query = { $and: [ @@ -304,12 +350,12 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - findPaginatedByActiveUsersExcept( - searchTerm, - exceptions, - options, - searchFields, - extraQuery = [], + findPaginatedByActiveUsersExcept( + searchTerm: string, + exceptions?: string[], + options?: FindOptions, + searchFields: string[] = [], + extraQuery: Filter[] = [], { startsWith = false, endsWith = false } = {}, ) { if (exceptions == null) { @@ -324,10 +370,13 @@ export class UsersRaw extends BaseRaw { const termRegex = new RegExp((startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : ''), 'i'); - const orStmt = (searchFields || []).reduce((acc, el) => { - acc.push({ [el.trim()]: termRegex }); - return acc; - }, []); + const orStmt = (searchFields || []).reduce( + (acc, el) => { + acc.push({ [el.trim()]: termRegex }); + return acc; + }, + [] as Record[], + ); const query = { $and: [ @@ -344,30 +393,42 @@ export class UsersRaw extends BaseRaw { ], }; - return this.findPaginated(query, options); + return this.findPaginated(query, options); } - findPaginatedByActiveLocalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localDomain) { + findPaginatedByActiveLocalUsersExcept( + searchTerm: string, + exceptions?: string[], + options?: FindOptions, + forcedSearchFields?: string[], + localDomain?: string, + ) { const extraQuery = [ { $or: [{ federation: { $exists: false } }, { 'federation.origin': localDomain }], }, ]; - return this.findPaginatedByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery); + return this.findPaginatedByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery); } - findPaginatedByActiveExternalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localDomain) { + findPaginatedByActiveExternalUsersExcept( + searchTerm: string, + exceptions?: string[], + options?: FindOptions, + forcedSearchFields?: string[], + localDomain?: string, + ) { const extraQuery = [{ federation: { $exists: true } }, { 'federation.origin': { $ne: localDomain } }]; - return this.findPaginatedByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery); + return this.findPaginatedByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery); } - findActive(query, options = {}) { + findActive(query: Filter, options: FindOptions = {}) { Object.assign(query, { active: true }); return this.find(query, options); } - findActiveByIds(userIds, options = {}) { + findActiveByIds(userIds: IUser['_id'][], options: FindOptions = {}) { const query = { _id: { $in: userIds }, active: true, @@ -376,7 +437,7 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - findActiveByIdsOrUsernames(userIds, options = {}) { + findActiveByIdsOrUsernames(userIds: IUser['_id'][], options: FindOptions = {}) { const query = { $or: [{ _id: { $in: userIds } }, { username: { $in: userIds } }], active: true, @@ -385,32 +446,32 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - findByIds(userIds, options = {}) { + findByIds(userIds: IUser['_id'][], options: FindOptions = {}) { const query = { _id: { $in: userIds }, }; - return this.find(query, options); + return this.find(query, options); } - findOneByImportId(_id, options) { - return this.findOne({ importIds: _id }, options); + findOneByImportId(_id: IUser['_id'], options?: FindOptions) { + return this.findOne({ importIds: _id }, options); } - findOneByUsernameIgnoringCase(username, options) { + findOneByUsernameIgnoringCase(username: IUser['username'], options?: FindOptions) { if (!username) { throw new Error('invalid username'); } const query = { username }; - return this.findOne(query, { + return this.findOne(query, { collation: { locale: 'en', strength: 2 }, // Case insensitive ...options, }); } - findOneWithoutLDAPByUsernameIgnoringCase(username, options) { + findOneWithoutLDAPByUsernameIgnoringCase(username: string, options?: FindOptions) { const expression = new RegExp(`^${escapeRegExp(username)}$`, 'i'); const query = { @@ -420,34 +481,31 @@ export class UsersRaw extends BaseRaw { }, }; - return this.findOne(query, options); + return this.findOne(query, options); } - async findOneByLDAPId(id, attribute = undefined) { + async findOneByLDAPId(id: string, attribute?: string) { const query = { 'services.ldap.id': id, + ...(attribute && { 'services.ldap.idAttribute': attribute }), }; - if (attribute) { - query['services.ldap.idAttribute'] = attribute; - } - - return this.findOne(query); + return this.findOne(query); } - async findOneByAppId(appId, options) { + async findOneByAppId(appId: string, options?: FindOptions) { const query = { appId }; - return this.findOne(query, options); + return this.findOne(query, options); } - findLDAPUsers(options) { + findLDAPUsers(options?: FindOptions) { const query = { ldap: true }; - return this.find(query, options); + return this.find(query, options); } - findLDAPUsersExceptIds(userIds, options = {}) { + findLDAPUsersExceptIds(userIds: IUser['_id'][], options: FindOptions = {}) { const query = { ldap: true, _id: { @@ -455,10 +513,10 @@ export class UsersRaw extends BaseRaw { }, }; - return this.find(query, options); + return this.find(query, options); } - findConnectedLDAPUsers(options) { + findConnectedLDAPUsers(options?: FindOptions) { const query = { 'ldap': true, 'services.resume.loginTokens': { @@ -467,26 +525,60 @@ export class UsersRaw extends BaseRaw { }, }; - return this.find(query, options); + return this.find(query, options); } - isUserInRole(userId, roleId) { + isUserInRole(userId: IUser['_id'], roleId: IRole['_id']) { const query = { _id: userId, roles: roleId, }; - return this.findOne(query, { projection: { roles: 1 } }); + return this.findOne>(query, { projection: { roles: 1 } }); } getDistinctFederationDomains() { return this.col.distinct('federation.origin', { federation: { $exists: true } }); } - async getNextLeastBusyAgent(department, ignoreAgentId, isEnabledWhenAgentIdle) { - const match = queryStatusAgentOnline({ ...(ignoreAgentId && { _id: { $ne: ignoreAgentId } }) }, isEnabledWhenAgentIdle); - const aggregate = [ + async getNextLeastBusyAgent( + department?: string, + ignoreAgentId?: string, + isEnabledWhenAgentIdle?: boolean, + ignoreUsernames?: string[], + ): Promise<{ agentId: string; username?: string; lastRoutingTime?: Date; count: number; departments?: any[] }> { + const match = queryStatusAgentOnline( + { ...(ignoreAgentId && { _id: { $ne: ignoreAgentId } }), ...(ignoreUsernames?.length && { username: { $nin: ignoreUsernames } }) }, + isEnabledWhenAgentIdle, + ); + + const departmentFilter = department + ? [ + { + $lookup: { + from: 'rocketchat_livechat_department_agents', + let: { userId: '$_id' }, + pipeline: [ + { + $match: { + $expr: { + $and: [{ $eq: ['$$userId', '$agentId'] }, { $eq: ['$departmentId', department] }], + }, + }, + }, + ], + as: 'department', + }, + }, + { + $match: { department: { $size: 1 } }, + }, + ] + : []; + + const aggregate: Document[] = [ { $match: match }, + ...departmentFilter, { $lookup: { from: 'rocketchat_subscription', @@ -508,34 +600,21 @@ export class UsersRaw extends BaseRaw { as: 'subs', }, }, - { - $lookup: { - from: 'rocketchat_livechat_department_agents', - localField: '_id', - foreignField: 'agentId', - as: 'departments', - }, - }, { $project: { agentId: '$_id', username: 1, lastRoutingTime: 1, - departments: 1, count: { $size: '$subs' }, }, }, { $sort: { count: 1, lastRoutingTime: 1, username: 1 } }, + { $limit: 1 }, ]; - if (department) { - aggregate.push({ $unwind: '$departments' }); - aggregate.push({ $match: { 'departments.departmentId': department } }); - } - - aggregate.push({ $limit: 1 }); - - const [agent] = await this.col.aggregate(aggregate).toArray(); + const [agent] = await this.col + .aggregate<{ agentId: string; username?: string; lastRoutingTime?: Date; count: number }>(aggregate) + .toArray(); if (agent) { await this.setLastRoutingTime(agent.agentId); } @@ -543,30 +622,50 @@ export class UsersRaw extends BaseRaw { return agent; } - async getLastAvailableAgentRouted(department, ignoreAgentId, isEnabledWhenAgentIdle) { - const match = queryStatusAgentOnline({ ...(ignoreAgentId && { _id: { $ne: ignoreAgentId } }) }, isEnabledWhenAgentIdle); - const aggregate = [ + async getLastAvailableAgentRouted( + department?: string, + ignoreAgentId?: string, + isEnabledWhenAgentIdle?: boolean, + ignoreUsernames?: string[], + ): Promise<{ agentId: string; username?: string; lastRoutingTime?: Date; departments?: any[] }> { + const match = queryStatusAgentOnline( + { ...(ignoreAgentId && { _id: { $ne: ignoreAgentId } }), ...(ignoreUsernames?.length && { username: { $nin: ignoreUsernames } }) }, + isEnabledWhenAgentIdle, + ); + const departmentFilter = department + ? [ + { + $lookup: { + from: 'rocketchat_livechat_department_agents', + let: { userId: '$_id' }, + pipeline: [ + { + $match: { + $expr: { + $and: [{ $eq: ['$$userId', '$agentId'] }, { $eq: ['$departmentId', department] }], + }, + }, + }, + ], + as: 'department', + }, + }, + { + $match: { department: { $size: 1 } }, + }, + ] + : []; + + const aggregate: Document[] = [ { $match: match }, - { - $lookup: { - from: 'rocketchat_livechat_department_agents', - localField: '_id', - foreignField: 'agentId', - as: 'departments', - }, - }, - { $project: { agentId: '$_id', username: 1, lastRoutingTime: 1, departments: 1 } }, + ...departmentFilter, + { $project: { agentId: '$_id', username: 1, lastRoutingTime: 1 } }, { $sort: { lastRoutingTime: 1, username: 1 } }, ]; - if (department) { - aggregate.push({ $unwind: '$departments' }); - aggregate.push({ $match: { 'departments.departmentId': department } }); - } - aggregate.push({ $limit: 1 }); - const [agent] = await this.col.aggregate(aggregate).toArray(); + const [agent] = await this.col.aggregate<{ agentId: string; username?: string; lastRoutingTime?: Date }>(aggregate).toArray(); if (agent) { await this.setLastRoutingTime(agent.agentId); } @@ -574,7 +673,7 @@ export class UsersRaw extends BaseRaw { return agent; } - async setLastRoutingTime(userId) { + async setLastRoutingTime(userId: IUser['_id']) { const result = await this.findOneAndUpdate( { _id: userId }, { @@ -584,12 +683,17 @@ export class UsersRaw extends BaseRaw { }, { returnDocument: 'after' }, ); - return result.value; + return result; } - setLivechatStatusIf(userId, status, conditions = {}, extraFields = {}) { + setLivechatStatusIf( + userId: IUser['_id'], + status: ILivechatAgentStatus, + conditions: Filter = {}, + extraFields: UpdateFilter['$set'] = {}, + ) { // TODO: Create class Agent - const query = { + const query: Filter = { _id: userId, ...conditions, }; @@ -604,7 +708,16 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } - async getAgentAndAmountOngoingChats(userId) { + async getAgentAndAmountOngoingChats( + userId: IUser['_id'], + departmentId?: string, + ): Promise<{ + agentId: string; + username?: string; + lastAssignTime?: Date; + lastRoutingTime?: Date; + queueInfo: { chats: number; chatsForDepartment?: number }; + }> { const aggregate = [ { $match: { @@ -638,18 +751,46 @@ export class UsersRaw extends BaseRaw { }, }, }, + ...(departmentId + ? { + 'queueInfo.chatsForDepartment': { + $size: { + $filter: { + input: '$subs', + as: 'sub', + cond: { + $and: [ + { $eq: ['$$sub.t', 'l'] }, + { $eq: ['$$sub.open', true] }, + { $ne: ['$$sub.onHold', true] }, + { $eq: ['$$sub.department', departmentId] }, + ], + }, + }, + }, + }, + } + : {}), }, }, { $sort: { 'queueInfo.chats': 1, 'lastAssignTime': 1, 'lastRoutingTime': 1, 'username': 1 } }, ]; - const [agent] = await this.col.aggregate(aggregate).toArray(); + const [agent] = await this.col + .aggregate<{ + agentId: string; + username?: string; + lastAssignTime?: Date; + lastRoutingTime?: Date; + queueInfo: { chats: number }; + }>(aggregate) + .toArray(); return agent; } - findAllResumeTokensByUserId(userId) { + findAllResumeTokensByUserId(userId: IUser['_id']): Promise<{ tokens: IMeteorLoginToken[] }[]> { return this.col - .aggregate([ + .aggregate<{ tokens: IMeteorLoginToken[] }>([ { $match: { _id: userId, @@ -675,7 +816,12 @@ export class UsersRaw extends BaseRaw { .toArray(); } - findActiveByUsernameOrNameRegexWithExceptionsAndConditions(termRegex, exceptions, conditions, options) { + findActiveByUsernameOrNameRegexWithExceptionsAndConditions( + termRegex: { $regex: string; $options: string } | RegExp, + exceptions?: string[], + conditions?: Filter, + options?: FindOptions, + ) { if (exceptions == null) { exceptions = []; } @@ -722,16 +868,20 @@ export class UsersRaw extends BaseRaw { ], }; - return this.find(query, options); + return this.find(query, options); } - countAllAgentsStatus({ departmentId = undefined }) { - const match = { + countAllAgentsStatus({ + departmentId, + }: { + departmentId?: string; + }): Promise<{ offline: number; away: number; busy: number; available: number }[]> { + const match: Document = { $match: { roles: { $in: ['livechat-agent'] }, }, }; - const group = { + const group: Document = { $group: { _id: null, offline: { @@ -785,7 +935,7 @@ export class UsersRaw extends BaseRaw { }, }, }; - const lookup = { + const lookup: Document = { $lookup: { from: 'rocketchat_livechat_department_agents', localField: '_id', @@ -793,13 +943,13 @@ export class UsersRaw extends BaseRaw { as: 'departments', }, }; - const unwind = { + const unwind: Document = { $unwind: { path: '$departments', preserveNullAndEmptyArrays: true, }, }; - const departmentsMatch = { + const departmentsMatch: Document = { $match: { 'departments.departmentId': departmentId, }, @@ -811,11 +961,19 @@ export class UsersRaw extends BaseRaw { params.push(departmentsMatch); } params.push(group); - return this.col.aggregate(params).toArray(); + return this.col.aggregate<{ offline: number; away: number; busy: number; available: number }>(params).toArray(); } - getTotalOfRegisteredUsersByDate({ start, end, options = {} }) { - const params = [ + getTotalOfRegisteredUsersByDate({ + start, + end, + options = {}, + }: { + start: Date; + end: Date; + options?: { count?: number; sort?: Record }; + }): Promise<{ date: string; users: number; type: 'users' }[]> { + const params: Document[] = [ { $match: { createdAt: { $gte: start, $lte: end }, @@ -851,10 +1009,10 @@ export class UsersRaw extends BaseRaw { if (options.count) { params.push({ $limit: options.count }); } - return this.col.aggregate(params).toArray(); + return this.col.aggregate<{ date: string; users: number; type: 'users' }>(params).toArray(); } - getUserLanguages() { + getUserLanguages(): Promise<{ _id: string; total: number }[]> { const pipeline = [ { $match: { @@ -872,18 +1030,10 @@ export class UsersRaw extends BaseRaw { }, ]; - return this.col.aggregate(pipeline).toArray(); + return this.col.aggregate<{ _id: string; total: number }>(pipeline).toArray(); } - /** - * - * @param {string} _id - * @param {string} statusText - * @param {Object} options - * @param {ClientSession} options.session - * @returns {Promise} - */ - updateStatusText(_id, statusText, options) { + updateStatusText(_id: IUser['_id'], statusText: string, options?: UpdateOptions) { const update = { $set: { statusText, @@ -893,7 +1043,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update, { session: options?.session }); } - updateStatus(_id, status) { + updateStatus(_id: IUser['_id'], status: UserStatus) { const update = { $set: { status, @@ -903,7 +1053,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - updateStatusAndStatusDefault(_id, status, statusDefault) { + updateStatusAndStatusDefault(_id: IUser['_id'], status: UserStatus, statusDefault: UserStatus) { const update = { $set: { status, @@ -914,7 +1064,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - updateStatusByAppId(appId, status) { + updateStatusByAppId(appId: string, status: UserStatus) { const query = { appId, status: { $ne: status }, @@ -937,7 +1087,15 @@ export class UsersRaw extends BaseRaw { * @param {string} [status.statusDefault] * @param {string} [status.statusText] */ - updateStatusById(userId, { statusDefault, status, statusConnection, statusText }) { + updateStatusById( + userId: IUser['_id'], + { + statusDefault, + status, + statusConnection, + statusText, + }: { statusDefault?: UserStatus; status: UserStatus; statusConnection: UserStatus; statusText?: string }, + ) { const query = { _id: userId, }; @@ -958,7 +1116,7 @@ export class UsersRaw extends BaseRaw { return this.col.updateOne(query, update); } - openAgentsBusinessHoursByBusinessHourId(businessHourIds) { + openAgentsBusinessHoursByBusinessHourId(businessHourIds: string[]) { const query = { roles: 'livechat-agent', }; @@ -972,7 +1130,7 @@ export class UsersRaw extends BaseRaw { return this.updateMany(query, update); } - openAgentBusinessHoursByBusinessHourIdsAndAgentId(businessHourIds, agentId) { + openAgentBusinessHoursByBusinessHourIdsAndAgentId(businessHourIds: string[], agentId: IUser['_id']) { const query = { _id: agentId, roles: 'livechat-agent', @@ -987,7 +1145,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } - addBusinessHourByAgentIds(agentIds = [], businessHourId) { + addBusinessHourByAgentIds(agentIds: IUser['_id'][] = [], businessHourId: string) { const query = { _id: { $in: agentIds }, roles: 'livechat-agent', @@ -1002,20 +1160,20 @@ export class UsersRaw extends BaseRaw { return this.updateMany(query, update); } - findOnlineButNotAvailableAgents(userIds) { + findOnlineButNotAvailableAgents(userIds?: IUser['_id'][]) { const query = { ...(userIds && { _id: { $in: userIds } }), roles: 'livechat-agent', // Exclude away users - status: 'online', + status: UserStatus.ONLINE, // Exclude users that are already available, maybe due to other business hour - statusLivechat: 'not-available', + statusLivechat: ILivechatAgentStatus.NOT_AVAILABLE, }; - return this.find(query); + return this.find(query); } - removeBusinessHourByAgentIds(agentIds = [], businessHourId) { + removeBusinessHourByAgentIds(agentIds: IUser['_id'][] = [], businessHourId: string) { const query = { _id: { $in: agentIds }, roles: 'livechat-agent', @@ -1030,7 +1188,7 @@ export class UsersRaw extends BaseRaw { return this.updateMany(query, update); } - openBusinessHourToAgentsWithoutDepartment(agentIdsWithDepartment = [], businessHourId) { + openBusinessHourToAgentsWithoutDepartment(agentIdsWithDepartment: IUser['_id'][] = [], businessHourId: string) { const query = { _id: { $nin: agentIdsWithDepartment }, }; @@ -1044,7 +1202,7 @@ export class UsersRaw extends BaseRaw { return this.updateMany(query, update); } - closeBusinessHourToAgentsWithoutDepartment(agentIdsWithDepartment = [], businessHourId) { + closeBusinessHourToAgentsWithoutDepartment(agentIdsWithDepartment: IUser['_id'][] = [], businessHourId: string) { const query = { _id: { $nin: agentIdsWithDepartment }, }; @@ -1058,7 +1216,7 @@ export class UsersRaw extends BaseRaw { return this.updateMany(query, update); } - closeAgentsBusinessHoursByBusinessHourIds(businessHourIds) { + closeAgentsBusinessHoursByBusinessHourIds(businessHourIds: string[]) { const query = { roles: 'livechat-agent', }; @@ -1072,15 +1230,15 @@ export class UsersRaw extends BaseRaw { return this.updateMany(query, update); } - findAgentsAvailableWithoutBusinessHours(userIds = []) { - return this.find( + findAgentsAvailableWithoutBusinessHours(userIds: IUser['_id'][] = []) { + return this.find>( { $or: [{ openBusinessHours: { $exists: false } }, { openBusinessHours: { $size: 0 } }], $and: [{ roles: 'livechat-agent' }, { roles: { $ne: 'bot' } }], // exclude deactivated users active: true, // Avoid unnecessary updates - statusLivechat: 'available', + statusLivechat: ILivechatAgentStatus.AVAILABLE, ...(Array.isArray(userIds) && userIds.length > 0 && { _id: { $in: userIds } }), }, { @@ -1089,10 +1247,10 @@ export class UsersRaw extends BaseRaw { ); } - setLivechatStatusActiveBasedOnBusinessHours(userId) { + setLivechatStatusActiveBasedOnBusinessHours(userId: IUser['_id']) { const query = { _id: userId, - statusDefault: { $ne: 'offline' }, + statusDefault: { $ne: UserStatus.OFFLINE }, openBusinessHours: { $exists: true, $not: { $size: 0 }, @@ -1101,14 +1259,14 @@ export class UsersRaw extends BaseRaw { const update = { $set: { - statusLivechat: 'available', + statusLivechat: ILivechatAgentStatus.AVAILABLE, }, }; return this.updateOne(query, update); } - async isAgentWithinBusinessHours(agentId) { + async isAgentWithinBusinessHours(agentId: IUser['_id']) { const query = { _id: agentId, $or: [ @@ -1137,14 +1295,14 @@ export class UsersRaw extends BaseRaw { const update = { $unset: { - openBusinessHours: 1, + openBusinessHours: 1 as const, }, }; return this.updateMany(query, update); } - resetTOTPById(userId) { + resetTOTPById(userId: IUser['_id']) { return this.col.updateOne( { _id: userId, @@ -1157,7 +1315,7 @@ export class UsersRaw extends BaseRaw { ); } - unsetOneLoginToken(_id, token) { + unsetOneLoginToken(_id: IUser['_id'], token: string) { const update = { $pull: { 'services.resume.loginTokens': { hashedToken: token }, @@ -1167,7 +1325,7 @@ export class UsersRaw extends BaseRaw { return this.col.updateOne({ _id }, update); } - unsetLoginTokens(userId) { + unsetLoginTokens(userId: IUser['_id']) { return this.col.updateOne( { _id: userId, @@ -1180,7 +1338,7 @@ export class UsersRaw extends BaseRaw { ); } - removeNonPATLoginTokensExcept(userId, authToken) { + removeNonPATLoginTokensExcept(userId: IUser['_id'], authToken: string) { return this.col.updateOne( { _id: userId, @@ -1196,7 +1354,7 @@ export class UsersRaw extends BaseRaw { ); } - removeRoomsByRoomIdsAndUserId(rids, userId) { + removeRoomsByRoomIdsAndUserId(rids: IRoom['_id'][], userId: IUser['_id']) { return this.updateMany( { _id: userId, @@ -1204,10 +1362,13 @@ export class UsersRaw extends BaseRaw { }, { $pullAll: { __rooms: rids }, - $unset: rids.reduce((acc, rid) => { - acc[`roomRolePriorities.${rid}`] = ''; - return acc; - }, {}), + $unset: rids.reduce( + (acc, rid) => { + acc[`roomRolePriorities.${rid}`] = ''; + return acc; + }, + {} as Record, + ), }, ); } @@ -1216,7 +1377,7 @@ export class UsersRaw extends BaseRaw { * @param {string} uid * @param {IRole['_id']} roles the list of role ids to remove */ - removeRolesByUserId(uid, roles) { + removeRolesByUserId(uid: IUser['_id'], roles: IRole['_id'][]) { const query = { _id: uid, }; @@ -1230,7 +1391,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } - async isUserInRoleScope(uid) { + async isUserInRoleScope(uid: IUser['_id']) { const query = { _id: uid, }; @@ -1243,7 +1404,7 @@ export class UsersRaw extends BaseRaw { return !!found; } - addBannerById(_id, banner) { + addBannerById(_id: IUser['_id'], banner: { id: string }) { const query = { _id, [`banners.${banner.id}.read`]: { @@ -1261,13 +1422,13 @@ export class UsersRaw extends BaseRaw { } // Voip functions - findOneByAgentUsername(username, options) { + findOneByAgentUsername(username: string, options?: FindOptions) { const query = { username, roles: 'livechat-agent' }; return this.findOne(query, options); } - findOneByExtension(extension, options) { + findOneByExtension(extension: string, options?: FindOptions) { const query = { extension, }; @@ -1275,7 +1436,7 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - findByExtensions(extensions, options) { + findByExtensions(extensions: string[], options?: FindOptions) { const query = { extension: { $in: extensions, @@ -1285,7 +1446,7 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - getVoipExtensionByUserId(userId, options) { + getVoipExtensionByUserId(userId: IUser['_id'], options?: FindOptions) { const query = { _id: userId, extension: { $exists: true }, @@ -1293,7 +1454,7 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - setExtension(userId, extension) { + setExtension(userId: IUser['_id'], extension: string) { const query = { _id: userId, }; @@ -1306,33 +1467,33 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } - unsetExtension(userId) { + unsetExtension(userId: IUser['_id']) { const query = { _id: userId, }; const update = { $unset: { - extension: true, + extension: 1 as const, }, }; return this.updateOne(query, update); } - getAvailableAgentsIncludingExt(includeExt, text, options) { + getAvailableAgentsIncludingExt(includeExt?: string, text?: string, options?: FindOptions) { const query = { roles: { $in: ['livechat-agent'] }, $and: [ - ...(text && text.trim() + ...(text?.trim() ? [{ $or: [{ username: new RegExp(escapeRegExp(text), 'i') }, { name: new RegExp(escapeRegExp(text), 'i') }] }] : []), { $or: [{ extension: { $exists: false } }, ...(includeExt ? [{ extension: includeExt }] : [])] }, ], }; - return this.findPaginated(query, options); + return this.findPaginated(query, options); } - findActiveUsersTOTPEnable(options) { + findActiveUsersTOTPEnable(options?: FindOptions) { const query = { 'active': true, 'services.totp.enabled': true, @@ -1340,7 +1501,7 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - countActiveUsersTOTPEnable(options) { + countActiveUsersTOTPEnable(options?: FindOptions) { const query = { 'active': true, 'services.totp.enabled': true, @@ -1348,7 +1509,7 @@ export class UsersRaw extends BaseRaw { return this.countDocuments(query, options); } - findActiveUsersEmail2faEnable(options) { + findActiveUsersEmail2faEnable(options?: FindOptions) { const query = { 'active': true, 'services.email2fa.enabled': true, @@ -1356,7 +1517,7 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - countActiveUsersEmail2faEnable(options) { + countActiveUsersEmail2faEnable(options?: FindOptions) { const query = { 'active': true, 'services.email2fa.enabled': true, @@ -1364,7 +1525,7 @@ export class UsersRaw extends BaseRaw { return this.countDocuments(query, options); } - setAsFederated(uid) { + setAsFederated(uid: IUser['_id']) { const query = { _id: uid, }; @@ -1377,7 +1538,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } - removeRoomByRoomId(rid, options) { + removeRoomByRoomId(rid: IRoom['_id'], options?: UpdateOptions) { return this.updateMany( { __rooms: rid, @@ -1390,11 +1551,11 @@ export class UsersRaw extends BaseRaw { ); } - findOneByResetToken(token, options) { + findOneByResetToken(token: string, options?: FindOptions) { return this.findOne({ 'services.password.reset.token': token }, options); } - findOneByIdWithEmailAddress(userId, options) { + findOneByIdWithEmailAddress(userId: IUser['_id'], options?: FindOptions) { return this.findOne( { _id: userId, @@ -1404,7 +1565,7 @@ export class UsersRaw extends BaseRaw { ); } - setFederationAvatarUrlById(userId, federationAvatarUrl) { + setFederationAvatarUrlById(userId: IUser['_id'], federationAvatarUrl: string) { return this.updateOne( { _id: userId, @@ -1417,8 +1578,8 @@ export class UsersRaw extends BaseRaw { ); } - async findSearchedServerNamesByUserId(userId) { - const user = await this.findOne( + async findSearchedServerNamesByUserId(userId: IUser['_id']): Promise { + const user = await this.findOne>( { _id: userId, }, @@ -1429,10 +1590,10 @@ export class UsersRaw extends BaseRaw { }, ); - return user.federation?.searchedServerNames || []; + return user?.federation?.searchedServerNames || []; } - addServerNameToSearchedServerNamesList(userId, serverName) { + addServerNameToSearchedServerNamesList(userId: IUser['_id'], serverName: string) { return this.updateOne( { _id: userId, @@ -1445,7 +1606,7 @@ export class UsersRaw extends BaseRaw { ); } - removeServerNameFromSearchedServerNamesList(userId, serverName) { + removeServerNameFromSearchedServerNamesList(userId: IUser['_id'], serverName: string) { return this.updateOne( { _id: userId, @@ -1464,21 +1625,21 @@ export class UsersRaw extends BaseRaw { }); } - findOnlineUserFromList(userList, isLivechatEnabledWhenAgentIdle) { + findOnlineUserFromList(userList: string | string[], isLivechatEnabledWhenAgentIdle?: boolean) { // TODO: Create class Agent const username = { - $in: [].concat(userList), + $in: ([] as string[]).concat(userList), }; const query = queryStatusAgentOnline({ username }, isLivechatEnabledWhenAgentIdle); - return this.find(query); + return this.find(query); } - countOnlineUserFromList(userList, isLivechatEnabledWhenAgentIdle) { + countOnlineUserFromList(userList: string | string[], isLivechatEnabledWhenAgentIdle?: boolean) { // TODO: Create class Agent const username = { - $in: [].concat(userList), + $in: ([] as string[]).concat(userList), }; const query = queryStatusAgentOnline({ username }, isLivechatEnabledWhenAgentIdle); @@ -1486,10 +1647,10 @@ export class UsersRaw extends BaseRaw { return this.countDocuments(query); } - findOneOnlineAgentByUserList(userList, options, isLivechatEnabledWhenAgentIdle) { + findOneOnlineAgentByUserList(userList: string | string[], options?: FindOptions, isLivechatEnabledWhenAgentIdle?: boolean) { // TODO:: Create class Agent const username = { - $in: [].concat(userList), + $in: ([] as string[]).concat(userList), }; const query = queryStatusAgentOnline({ username }, isLivechatEnabledWhenAgentIdle); @@ -1497,11 +1658,14 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - getUnavailableAgents() { + async getUnavailableAgents( + _departmentId?: string, + _extraQuery?: Filter, + ): Promise[]> { return []; } - findBotAgents(usernameList) { + findBotAgents(usernameList?: string | string[]): FindCursor { // TODO:: Create class Agent const query = { roles: { @@ -1509,15 +1673,15 @@ export class UsersRaw extends BaseRaw { }, ...(usernameList && { username: { - $in: [].concat(usernameList), + $in: ([] as string[]).concat(usernameList), }, }), }; - return this.find(query); + return this.find(query); } - countBotAgents(usernameList) { + countBotAgents(usernameList?: string | string[]) { // TODO:: Create class Agent const query = { roles: { @@ -1525,7 +1689,7 @@ export class UsersRaw extends BaseRaw { }, ...(usernameList && { username: { - $in: [].concat(usernameList), + $in: ([] as string[]).concat(usernameList), }, }), }; @@ -1533,7 +1697,7 @@ export class UsersRaw extends BaseRaw { return this.countDocuments(query); } - removeAllRoomsByUserId(_id) { + removeAllRoomsByUserId(_id: IUser['_id']) { return this.updateOne( { _id, @@ -1545,7 +1709,7 @@ export class UsersRaw extends BaseRaw { ); } - removeRoomByUserId(_id, rid) { + removeRoomByUserId(_id: IUser['_id'], rid: IRoom['_id']) { return this.updateOne( { _id, @@ -1558,7 +1722,7 @@ export class UsersRaw extends BaseRaw { ); } - addRoomByUserId(_id, rid) { + addRoomByUserId(_id: IUser['_id'], rid: IRoom['_id']) { return this.updateOne( { _id, @@ -1570,7 +1734,7 @@ export class UsersRaw extends BaseRaw { ); } - addRoomByUserIds(uids, rid) { + addRoomByUserIds(uids: IUser['_id'][], rid: IRoom['_id']) { return this.updateMany( { _id: { $in: uids }, @@ -1582,22 +1746,25 @@ export class UsersRaw extends BaseRaw { ); } - removeRoomByRoomIds(rids) { + removeRoomByRoomIds(rids: IRoom['_id'][]) { return this.updateMany( { __rooms: { $in: rids }, }, { $pullAll: { __rooms: rids }, - $unset: rids.reduce((acc, rid) => { - acc[`roomRolePriorities.${rid}`] = ''; - return acc; - }, {}), + $unset: rids.reduce( + (acc, rid) => { + acc[`roomRolePriorities.${rid}`] = ''; + return acc; + }, + {} as Record, + ), }, ); } - addRoomRolePriorityByUserId(userId, rid, priority) { + addRoomRolePriorityByUserId(userId: IUser['_id'], rid: IRoom['_id'], priority: number) { return this.updateOne( { _id: userId, @@ -1610,7 +1777,7 @@ export class UsersRaw extends BaseRaw { ); } - removeRoomRolePriorityByUserId(userId, rid) { + removeRoomRolePriorityByUserId(userId: IUser['_id'], rid: IRoom['_id']) { return this.updateOne( { _id: userId, @@ -1623,7 +1790,7 @@ export class UsersRaw extends BaseRaw { ); } - async assignRoomRolePrioritiesByUserIdPriorityMap(userIdAndrolePriorityMap, rid) { + async assignRoomRolePrioritiesByUserIdPriorityMap(userIdAndrolePriorityMap: Record, rid: IRoom['_id']) { const bulk = this.col.initializeUnorderedBulkOp(); for (const [userId, priority] of Object.entries(userIdAndrolePriorityMap)) { @@ -1638,7 +1805,7 @@ export class UsersRaw extends BaseRaw { return 0; } - unassignRoomRolePrioritiesByRoomId(rid) { + unassignRoomRolePrioritiesByRoomId(rid: IRoom['_id']) { return this.updateMany( { __rooms: rid, @@ -1651,7 +1818,7 @@ export class UsersRaw extends BaseRaw { ); } - getLoginTokensByUserId(userId) { + getLoginTokensByUserId(userId: IUser['_id']) { const query = { 'services.resume.loginTokens.type': { $exists: true, @@ -1660,10 +1827,10 @@ export class UsersRaw extends BaseRaw { '_id': userId, }; - return this.find(query, { projection: { 'services.resume.loginTokens': 1 } }); + return this.find(query, { projection: { 'services.resume.loginTokens': 1 } }); } - addPersonalAccessTokenToUser({ userId, loginTokenObject }) { + addPersonalAccessTokenToUser({ userId, loginTokenObject }: { userId: IUser['_id']; loginTokenObject: ILoginToken }) { return this.updateOne( { _id: userId }, { @@ -1674,7 +1841,13 @@ export class UsersRaw extends BaseRaw { ); } - removePersonalAccessTokenOfUser({ userId, loginTokenObject }) { + removePersonalAccessTokenOfUser({ + userId, + loginTokenObject, + }: { + userId: IUser['_id']; + loginTokenObject: AtLeast; + }) { return this.updateOne( { _id: userId }, { @@ -1685,7 +1858,7 @@ export class UsersRaw extends BaseRaw { ); } - findPersonalAccessTokenByTokenNameAndUserId({ userId, tokenName }) { + findPersonalAccessTokenByTokenNameAndUserId({ userId, tokenName }: { userId: IUser['_id']; tokenName: string }) { const query = { 'services.resume.loginTokens': { $elemMatch: { name: tokenName, type: 'personalAccessToken' }, @@ -1696,7 +1869,8 @@ export class UsersRaw extends BaseRaw { return this.findOne(query); } - setOperator(_id, operator) { + // TODO: check if this is still valid/used for something + setOperator(_id: IUser['_id'], operator: boolean) { // TODO:: Create class Agent const update = { $set: { @@ -1707,21 +1881,28 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - async checkOnlineAgents(agentId, isLivechatEnabledWhenAgentIdle) { + async checkOnlineAgents(agentId: IUser['_id'], isLivechatEnabledWhenAgentIdle?: boolean) { // TODO:: Create class Agent const query = queryStatusAgentOnline(agentId && { _id: agentId }, isLivechatEnabledWhenAgentIdle); return !!(await this.findOne(query)); } - findOnlineAgents(agentId, isLivechatEnabledWhenAgentIdle) { + findOnlineAgents(agentId?: IUser['_id'], isLivechatEnabledWhenAgentIdle?: boolean) { // TODO:: Create class Agent const query = queryStatusAgentOnline(agentId && { _id: agentId }, isLivechatEnabledWhenAgentIdle); - return this.find(query); + return this.find(query); + } + + countOnlineAgents(agentId: IUser['_id']) { + // TODO:: Create class Agent + const query = queryStatusAgentOnline(agentId && { _id: agentId }); + + return this.col.countDocuments(query); } - findOneBotAgent() { + findOneBotAgent() { // TODO:: Create class Agent const query = { roles: { @@ -1729,23 +1910,27 @@ export class UsersRaw extends BaseRaw { }, }; - return this.findOne(query); + return this.findOne(query); } - findOneOnlineAgentById(_id, isLivechatEnabledWhenAgentIdle, options) { + findOneOnlineAgentById( + _id: IUser['_id'], + isLivechatEnabledWhenAgentIdle?: boolean, + options?: FindOptions, + ) { // TODO: Create class Agent const query = queryStatusAgentOnline({ _id }, isLivechatEnabledWhenAgentIdle); - return this.findOne(query, options); + return this.findOne(query, options); } - findAgents() { + findAgents() { // TODO: Create class Agent const query = { roles: 'livechat-agent', }; - return this.find(query); + return this.find(query); } countAgents() { @@ -1758,10 +1943,10 @@ export class UsersRaw extends BaseRaw { } // 2 - async getNextAgent(ignoreAgentId, extraQuery, enabledWhenAgentIdle) { + async getNextAgent(ignoreAgentId?: string, extraQuery?: Filter, enabledWhenAgentIdle?: boolean) { // TODO: Create class Agent // fetch all unavailable agents, and exclude them from the selection - const unavailableAgents = (await this.getUnavailableAgents(null, extraQuery)).map((u) => u.username); + const unavailableAgents = (await this.getUnavailableAgents(undefined, extraQuery)).map((u) => u.username); const extraFilters = { ...(ignoreAgentId && { _id: { $ne: ignoreAgentId } }), // limit query to remove booked agents @@ -1770,7 +1955,7 @@ export class UsersRaw extends BaseRaw { const query = queryStatusAgentOnline(extraFilters, enabledWhenAgentIdle); - const sort = { + const sort: Record = { livechatCount: 1, username: 1, }; @@ -1791,7 +1976,7 @@ export class UsersRaw extends BaseRaw { return null; } - async getNextBotAgent(ignoreAgentId) { + async getNextBotAgent(ignoreAgentId?: string) { // TODO: Create class Agent const query = { roles: { @@ -1811,7 +1996,7 @@ export class UsersRaw extends BaseRaw { }, }; - const user = await this.findOneAndUpdate(query, update, { sort, returnDocument: 'after' }); + const user = await this.findOneAndUpdate(query, update, { sort, returnDocument: 'after' } as FindOneAndUpdateOptions); if (user) { return { agentId: user._id, @@ -1821,7 +2006,7 @@ export class UsersRaw extends BaseRaw { return null; } - setLivechatStatus(userId, status) { + setLivechatStatus(userId: IUser['_id'], status: ILivechatAgentStatus) { // TODO: Create class Agent const query = { _id: userId, @@ -1837,7 +2022,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } - makeAgentUnavailableAndUnsetExtension(userId) { + makeAgentUnavailableAndUnsetExtension(userId: IUser['_id']) { const query = { _id: userId, roles: 'livechat-agent', @@ -1848,14 +2033,15 @@ export class UsersRaw extends BaseRaw { statusLivechat: ILivechatAgentStatus.NOT_AVAILABLE, }, $unset: { - extension: 1, + extension: 1 as const, }, }; return this.updateOne(query, update); } - setLivechatData(userId, data = {}) { + // TODO: improve type of livechatData + setLivechatData(userId: IUser['_id'], data: Record = {}) { // TODO: Create class Agent const query = { _id: userId, @@ -1870,21 +2056,31 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } + // TODO: why this needs to be one by one instead of an updateMany? async closeOffice() { // TODO: Create class Agent - const promises = []; - await this.findAgents().forEach((agent) => promises.push(this.setLivechatStatus(agent._id, 'not-available'))); + const promises: Promise>[] = []; + // TODO: limit the data returned by findAgents + await this.findAgents().forEach((agent) => { + promises.push(this.setLivechatStatus(agent._id, ILivechatAgentStatus.NOT_AVAILABLE)); + }); await Promise.all(promises); } + // Same todo's as the above async openOffice() { // TODO: Create class Agent - const promises = []; - await this.findAgents().forEach((agent) => promises.push(this.setLivechatStatus(agent._id, 'available'))); + const promises: Promise>[] = []; + await this.findAgents().forEach((agent) => { + promises.push(this.setLivechatStatus(agent._id, ILivechatAgentStatus.AVAILABLE)); + }); await Promise.all(promises); } - getAgentInfo(agentId, showAgentEmail = false) { + getAgentInfo( + agentId: IUser['_id'], + showAgentEmail = false, + ): Promise | null> { // TODO: Create class Agent const query = { _id: agentId, @@ -1905,11 +2101,12 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - roleBaseQuery(userId) { + roleBaseQuery(userId: IUser['_id']) { return { _id: userId }; } - setE2EPublicAndPrivateKeysByUserId(userId, { public_key, private_key }) { + // eslint-disable-next-line @typescript-eslint/naming-convention + setE2EPublicAndPrivateKeysByUserId(userId: IUser['_id'], { public_key, private_key }: { public_key: string; private_key: string }) { return this.updateOne( { _id: userId }, { @@ -1921,7 +2118,7 @@ export class UsersRaw extends BaseRaw { ); } - async rocketMailUnsubscribe(_id, createdAt) { + async rocketMailUnsubscribe(_id: IUser['_id'], createdAt: string) { const query = { _id, createdAt: new Date(parseInt(createdAt)), @@ -1931,11 +2128,11 @@ export class UsersRaw extends BaseRaw { 'mailer.unsubscribed': true, }, }; - const affectedRows = (await this.updateOne(query, update)).updatedCount; + const affectedRows = (await this.updateOne(query, update)).modifiedCount; return affectedRows; } - async fetchKeysByUserId(userId) { + async fetchKeysByUserId(userId: IUser['_id']) { const user = await this.findOne({ _id: userId }, { projection: { e2e: 1 } }); if (!user?.e2e?.public_key) { @@ -1948,7 +2145,7 @@ export class UsersRaw extends BaseRaw { }; } - disable2FAAndSetTempSecretByUserId(userId, tempToken) { + disable2FAAndSetTempSecretByUserId(userId: IUser['_id'], tempToken: string) { return this.updateOne( { _id: userId, @@ -1964,7 +2161,7 @@ export class UsersRaw extends BaseRaw { ); } - enable2FAAndSetSecretAndCodesByUserId(userId, secret, backupCodes) { + enable2FAAndSetSecretAndCodesByUserId(userId: IUser['_id'], secret: string, backupCodes: string[]) { return this.updateOne( { _id: userId, @@ -1982,7 +2179,7 @@ export class UsersRaw extends BaseRaw { ); } - disable2FAByUserId(userId) { + disable2FAByUserId(userId: IUser['_id']) { return this.updateOne( { _id: userId, @@ -1997,7 +2194,7 @@ export class UsersRaw extends BaseRaw { ); } - update2FABackupCodesByUserId(userId, backupCodes) { + update2FABackupCodesByUserId(userId: IUser['_id'], backupCodes: string[]) { return this.updateOne( { _id: userId, @@ -2010,7 +2207,7 @@ export class UsersRaw extends BaseRaw { ); } - enableEmail2FAByUserId(userId) { + enableEmail2FAByUserId(userId: IUser['_id']) { return this.updateOne( { _id: userId, @@ -2026,7 +2223,7 @@ export class UsersRaw extends BaseRaw { ); } - disableEmail2FAByUserId(userId) { + disableEmail2FAByUserId(userId: IUser['_id']) { return this.updateOne( { _id: userId, @@ -2042,7 +2239,7 @@ export class UsersRaw extends BaseRaw { ); } - findByIdsWithPublicE2EKey(ids, options) { + findByIdsWithPublicE2EKey(ids: IUser['_id'][], options?: FindOptions) { const query = { '_id': { $in: ids, @@ -2055,7 +2252,7 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - resetE2EKey(userId) { + resetE2EKey(userId: IUser['_id']) { return this.updateOne( { _id: userId }, { @@ -2066,7 +2263,7 @@ export class UsersRaw extends BaseRaw { ); } - removeExpiredEmailCodeOfUserId(userId) { + removeExpiredEmailCodeOfUserId(userId: IUser['_id']) { return this.updateOne( { '_id': userId, 'services.emailCode.expire': { $lt: new Date() } }, { @@ -2075,7 +2272,7 @@ export class UsersRaw extends BaseRaw { ); } - removeEmailCodeOfUserId(userId) { + removeEmailCodeOfUserId(userId: IUser['_id']) { return this.updateOne( { _id: userId }, { @@ -2084,7 +2281,7 @@ export class UsersRaw extends BaseRaw { ); } - incrementInvalidEmailCodeAttempt(userId) { + incrementInvalidEmailCodeAttempt(userId: IUser['_id']) { return this.findOneAndUpdate( { _id: userId }, { @@ -2099,7 +2296,7 @@ export class UsersRaw extends BaseRaw { ); } - async maxInvalidEmailCodeAttemptsReached(userId, maxAttempts) { + async maxInvalidEmailCodeAttemptsReached(userId: IUser['_id'], maxAttempts: number) { const result = await this.findOne( { '_id': userId, @@ -2114,7 +2311,7 @@ export class UsersRaw extends BaseRaw { return !!result?._id; } - addEmailCodeByUserId(userId, code, expire) { + addEmailCodeByUserId(userId: IUser['_id'], code: string, expire: Date) { return this.updateOne( { _id: userId }, { @@ -2133,8 +2330,8 @@ export class UsersRaw extends BaseRaw { * @param {IRole['_id'][]} roles the list of role ids * @param {any} options */ - findActiveUsersInRoles(roles, options) { - roles = [].concat(roles); + findActiveUsersInRoles(roles: IRole['_id'][], options?: FindOptions) { + roles = ([] as string[]).concat(roles); const query = { roles: { $in: roles }, @@ -2144,8 +2341,8 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - countActiveUsersInRoles(roles, options) { - roles = [].concat(roles); + countActiveUsersInRoles(roles: IRole['_id'][], options?: FindOptions) { + roles = ([] as string[]).concat(roles); const query = { roles: { $in: roles }, @@ -2155,7 +2352,12 @@ export class UsersRaw extends BaseRaw { return this.countDocuments(query, options); } - findOneByUsernameAndServiceNameIgnoringCase(username, userId, serviceName, options) { + findOneByUsernameAndServiceNameIgnoringCase( + username: string | RegExp, + userId: IUser['_id'], + serviceName: string, + options?: FindOptions, + ) { if (typeof username === 'string') { username = new RegExp(`^${escapeRegExp(username)}$`, 'i'); } @@ -2165,7 +2367,12 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - findOneByEmailAddressAndServiceNameIgnoringCase(emailAddress, userId, serviceName, options) { + findOneByEmailAddressAndServiceNameIgnoringCase( + emailAddress: string, + userId: IUser['_id'], + serviceName: string, + options?: FindOptions, + ) { const query = { 'emails.address': String(emailAddress).trim(), [`services.${serviceName}.id`]: userId, @@ -2177,7 +2384,7 @@ export class UsersRaw extends BaseRaw { }); } - findOneByEmailAddress(emailAddress, options) { + findOneByEmailAddress(emailAddress: string, options?: FindOptions) { const query = { 'emails.address': String(emailAddress).trim() }; return this.findOne(query, { @@ -2186,7 +2393,7 @@ export class UsersRaw extends BaseRaw { }); } - findOneWithoutLDAPByEmailAddress(emailAddress, options) { + findOneWithoutLDAPByEmailAddress(emailAddress: string, options?: FindOptions) { const query = { 'email.address': emailAddress.trim().toLowerCase(), 'services.ldap': { @@ -2197,13 +2404,13 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - findOneAdmin(userId, options) { + findOneAdmin(userId: IUser['_id'], options?: FindOptions) { const query = { roles: { $in: ['admin'] }, _id: userId }; return this.findOne(query, options); } - findOneByIdAndLoginToken(_id, token, options) { + findOneByIdAndLoginToken(_id: IUser['_id'], token: string, options?: FindOptions) { const query = { _id, 'services.resume.loginTokens.hashedToken': token, @@ -2212,13 +2419,13 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - findOneById(userId, options = {}) { + findOneById(userId: IUser['_id'], options: FindOptions = {}) { const query = { _id: userId }; return this.findOne(query, options); } - findOneActiveById(userId, options) { + findOneActiveById(userId?: IUser['_id'], options?: FindOptions) { const query = { _id: userId, active: true, @@ -2227,7 +2434,7 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - findOneByIdOrUsername(idOrUsername, options) { + findOneByIdOrUsername(idOrUsername: IUser['_id'] | IUser['username'], options?: FindOptions) { const query = { $or: [ { @@ -2242,53 +2449,53 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - findOneByRolesAndType(roles, type, options) { + findOneByRolesAndType(roles: IRole['_id'][], type: string, options?: FindOptions) { const query = { roles, type }; - return this.findOne(query, options); + return this.findOne(query, options); } - findNotOfflineByIds(users, options) { + findNotOfflineByIds(users?: IUser['_id'][], options?: FindOptions) { const query = { _id: { $in: users }, status: { - $in: ['online', 'away', 'busy'], + $in: [UserStatus.ONLINE, UserStatus.AWAY, UserStatus.BUSY], }, }; return this.find(query, options); } - findUsersNotOffline(options) { + findUsersNotOffline(options?: FindOptions) { const query = { username: { - $exists: 1, + $exists: true, }, status: { - $in: ['online', 'away', 'busy'], + $in: [UserStatus.ONLINE, UserStatus.AWAY, UserStatus.BUSY], }, }; return this.find(query, options); } - countUsersNotOffline(options) { + countUsersNotOffline(options?: FindOptions) { const query = { username: { - $exists: 1, + $exists: true, }, status: { - $in: ['online', 'away', 'busy'], + $in: [UserStatus.ONLINE, UserStatus.AWAY, UserStatus.BUSY], }, }; return this.col.countDocuments(query, options); } - findNotIdUpdatedFrom(uid, from, options) { - const query = { + findNotIdUpdatedFrom(uid: IUser['_id'], from: Date, options?: FindOptions) { + const query: Filter = { _id: { $ne: uid }, username: { - $exists: 1, + $exists: true, }, _updatedAt: { $gte: from }, }; @@ -2296,7 +2503,7 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - async findByRoomId(rid, options) { + async findByRoomId(rid: IRoom['_id'], options?: FindOptions) { const data = (await Subscriptions.findByRoomId(rid).toArray()).map((item) => item.u._id); const query = { _id: { @@ -2307,19 +2514,19 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - findByUsername(username, options) { + findByUsername(username: string, options?: FindOptions) { const query = { username }; return this.find(query, options); } - findByUsernames(usernames, options) { + findByUsernames(usernames: string[], options?: FindOptions) { const query = { username: { $in: usernames } }; return this.find(query, options); } - findByUsernamesIgnoringCase(usernames, options) { + findByUsernamesIgnoringCase(usernames: string[], options?: FindOptions) { const query = { username: { $in: usernames.filter(Boolean).map((u) => new RegExp(`^${escapeRegExp(u)}$`, 'i')), @@ -2329,7 +2536,7 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - findActiveByUserIds(ids, options = {}) { + findActiveByUserIds(ids: IUser['_id'][], options: FindOptions = {}) { return this.find( { active: true, @@ -2340,8 +2547,8 @@ export class UsersRaw extends BaseRaw { ); } - findActiveLocalGuests(idExceptions = [], options = {}) { - const query = { + findActiveLocalGuests(idExceptions: IUser['_id'] | IUser['_id'][] = [], options: FindOptions = {}) { + const query: Filter = { active: true, type: { $nin: ['app'] }, roles: { @@ -2362,8 +2569,8 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - countActiveLocalGuests(idExceptions = []) { - const query = { + countActiveLocalGuests(idExceptions: IUser['_id'] | IUser['_id'][] = []) { + const query: Filter = { active: true, type: { $nin: ['app'] }, roles: { @@ -2385,10 +2592,10 @@ export class UsersRaw extends BaseRaw { } // 4 - findUsersByNameOrUsername(nameOrUsername, options) { + findUsersByNameOrUsername(nameOrUsername: string, options?: FindOptions) { const query = { username: { - $exists: 1, + $exists: true, }, $or: [{ name: nameOrUsername }, { username: nameOrUsername }], @@ -2401,7 +2608,7 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - findByUsernameNameOrEmailAddress(usernameNameOrEmailAddress, options) { + findByUsernameNameOrEmailAddress(usernameNameOrEmailAddress: string, options?: FindOptions) { const query = { $or: [ { name: usernameNameOrEmailAddress }, @@ -2416,29 +2623,29 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - findCrowdUsers(options) { + findCrowdUsers(options?: FindOptions) { const query = { crowd: true }; return this.find(query, options); } - async getLastLogin(options = { projection: { _id: 0, lastLogin: 1 } }) { + async getLastLogin(options: FindOptions = { projection: { _id: 0, lastLogin: 1 } }) { options.sort = { lastLogin: -1 }; const user = await this.findOne({}, options); return user?.lastLogin; } - findUsersByUsernames(usernames, options) { + findUsersByUsernames(usernames: string[], options?: FindOptions) { const query = { username: { $in: usernames, }, }; - return this.find(query, options); + return this.find(query, options); } - findUsersByIds(ids, options) { + findUsersByIds(ids: IUser['_id'][], options?: FindOptions) { const query = { _id: { $in: ids, @@ -2447,29 +2654,29 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - findUsersWithUsernameByIds(ids, options) { + findUsersWithUsernameByIds(ids: IUser['_id'][], options?: FindOptions) { const query = { _id: { $in: ids, }, username: { - $exists: 1, + $exists: true, }, }; return this.find(query, options); } - findUsersWithUsernameByIdsNotOffline(ids, options) { + findUsersWithUsernameByIdsNotOffline(ids: IUser['_id'][], options?: FindOptions) { const query = { _id: { $in: ids, }, username: { - $exists: 1, + $exists: true, }, status: { - $in: ['online', 'away', 'busy'], + $in: [UserStatus.ONLINE, UserStatus.AWAY, UserStatus.BUSY], }, }; @@ -2479,14 +2686,14 @@ export class UsersRaw extends BaseRaw { /** * @param {import('mongodb').Filter} projection */ - getOldest(optionsParams) { + getOldest(optionsParams?: FindOptions) { const query = { _id: { $ne: 'rocket.cat', }, }; - const options = { + const options: FindOptions = { ...optionsParams, sort: { createdAt: 1, @@ -2496,11 +2703,11 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - countRemote(options = {}) { + countRemote(options: FindOptions = {}) { return this.countDocuments({ isRemote: true }, options); } - findActiveRemote(options = {}) { + findActiveRemote(options: FindOptions = {}) { return this.find( { active: true, @@ -2511,7 +2718,7 @@ export class UsersRaw extends BaseRaw { ); } - findActiveFederated(options = {}) { + findActiveFederated(options: FindOptions = {}) { return this.find( { active: true, @@ -2521,38 +2728,44 @@ export class UsersRaw extends BaseRaw { ); } - getSAMLByIdAndSAMLProvider(_id, provider) { + getSAMLByIdAndSAMLProvider(_id: IUser['_id'], provider: string) { return this.findOne( { _id, 'services.saml.provider': provider, }, { - 'services.saml': 1, + projection: { 'services.saml': 1 }, }, ); } - findBySAMLNameIdOrIdpSession(nameID, idpSession) { - return this.find({ - $or: [{ 'services.saml.nameID': nameID }, { 'services.saml.idpSession': idpSession }], - }); + findBySAMLNameIdOrIdpSession(nameID: string, idpSession: string, options?: FindOptions) { + return this.find( + { + $or: [{ 'services.saml.nameID': nameID }, { 'services.saml.idpSession': idpSession }], + }, + options, + ); } - countBySAMLNameIdOrIdpSession(nameID, idpSession) { + countBySAMLNameIdOrIdpSession(nameID: string, idpSession: string) { return this.countDocuments({ $or: [{ 'services.saml.nameID': nameID }, { 'services.saml.idpSession': idpSession }], }); } - findBySAMLInResponseTo(inResponseTo) { - return this.find({ - 'services.saml.inResponseTo': inResponseTo, - }); + findBySAMLInResponseTo(inResponseTo: string, options?: FindOptions) { + return this.find( + { + 'services.saml.inResponseTo': inResponseTo, + }, + options, + ); } - findOneByFreeSwitchExtension(freeSwitchExtension, options = {}) { - return this.findOne( + findOneByFreeSwitchExtension(freeSwitchExtension: string, options: FindOptions = {}) { + return this.findOne( { freeSwitchExtension, }, @@ -2560,8 +2773,8 @@ export class UsersRaw extends BaseRaw { ); } - findOneByFreeSwitchExtensions(freeSwitchExtensions, options = {}) { - return this.findOne( + findOneByFreeSwitchExtensions(freeSwitchExtensions: string[], options: FindOptions = {}) { + return this.findOne( { freeSwitchExtension: { $in: freeSwitchExtensions }, }, @@ -2577,11 +2790,11 @@ export class UsersRaw extends BaseRaw { }).map(({ freeSwitchExtension }) => freeSwitchExtension); } - findUsersWithAssignedFreeSwitchExtensions(options = {}) { - return this.find( + findUsersWithAssignedFreeSwitchExtensions(options: FindOptions = {}) { + return this.find( { freeSwitchExtension: { - $exists: 1, + $exists: true, }, }, options, @@ -2589,8 +2802,8 @@ export class UsersRaw extends BaseRaw { } // UPDATE - addImportIds(_id, importIds) { - importIds = [].concat(importIds); + addImportIds(_id: IUser['_id'], importIds: string[]) { + importIds = ([] as string[]).concat(importIds); const query = { _id }; @@ -2605,7 +2818,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } - updateInviteToken(_id, inviteToken) { + updateInviteToken(_id: IUser['_id'], inviteToken: string) { const update = { $set: { inviteToken, @@ -2615,7 +2828,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - updateLastLoginById(_id) { + updateLastLoginById(_id: IUser['_id']) { const update = { $set: { lastLogin: new Date(), @@ -2625,7 +2838,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - addPasswordToHistory(_id, password, passwordHistoryAmount) { + addPasswordToHistory(_id: IUser['_id'], password: string, passwordHistoryAmount: number) { const update = { $push: { 'services.passwordHistory': { @@ -2637,39 +2850,24 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - setServiceId(_id, serviceName, serviceId) { - const update = { $set: {} }; + setServiceId(_id: IUser['_id'], serviceName: string, serviceId: string) { + const update: UpdateFilter = { $set: {} }; const serviceIdKey = `services.${serviceName}.id`; - update.$set[serviceIdKey] = serviceId; + if (update.$set) { + update.$set[serviceIdKey] = serviceId; + } return this.updateOne({ _id }, update); } - /** - * - * @param {string} _id - * @param {string} username - * @param {Object} options - * @param {ClientSession} options.session - * @returns {Promise} - */ - setUsername(_id, username, options) { + setUsername(_id: IUser['_id'], username: string, options?: UpdateOptions) { const update = { $set: { username } }; return this.updateOne({ _id }, update, { session: options?.session }); } - /** - * - * @param {string} _id - * @param {string} email - * @param {boolean} verified - * @param {Object} options - * @param {ClientSession} options.session - * @returns {Promise} - */ - setEmail(_id, email, verified = false, options) { + setEmail(_id: IUser['_id'], email: string, verified = false, options?: UpdateOptions) { const update = { $set: { emails: [ @@ -2685,7 +2883,7 @@ export class UsersRaw extends BaseRaw { } // 5 - setEmailVerified(_id, email) { + setEmailVerified(_id: IUser['_id'], email: string) { const query = { _id, emails: { @@ -2705,15 +2903,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } - /** - * - * @param {string} _id - * @param {string} name - * @param {Object} options - * @param {ClientSession} options.session - * @returns {Promise} - */ - setName(_id, name, options) { + setName(_id: IUser['_id'], name: string, options?: UpdateOptions) { const update = { $set: { name, @@ -2723,25 +2913,18 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update, { session: options?.session }); } - /** - * - * @param {string} _id - * @param {Object} options - * @param {ClientSession} options.session - * @returns {Promise} - */ - unsetName(_id, options) { + unsetName(_id: IUser['_id'], options?: UpdateOptions) { const update = { $unset: { - name, + name: 1 as const, }, }; return this.updateOne({ _id }, update, { session: options?.session }); } - setCustomFields(_id, fields) { - const values = {}; + setCustomFields(_id: IUser['_id'], fields: Record) { + const values: Record = {}; Object.keys(fields).forEach((key) => { values[`customFields.${key}`] = fields[key]; }); @@ -2751,16 +2934,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - /** - * - * @param {string} _id - * @param {string} origin - * @param {string} etag - * @param {Object} options - * @param {ClientSession} options.session - * @returns {Promise} - */ - setAvatarData(_id, origin, etag, options) { + setAvatarData(_id: IUser['_id'], origin: string, etag: string, options?: UpdateOptions) { const update = { $set: { avatarOrigin: origin, @@ -2771,8 +2945,8 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update, { session: options?.session }); } - unsetAvatarData(_id) { - const update = { + unsetAvatarData(_id: IUser['_id']) { + const update: UpdateFilter = { $unset: { avatarOrigin: 1, avatarETag: 1, @@ -2782,7 +2956,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - setUserActive(_id, active) { + setUserActive(_id: IUser['_id'], active: boolean | null) { if (active == null) { active = true; } @@ -2795,7 +2969,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - setAllUsersActive(active) { + setAllUsersActive(active: boolean) { const update = { $set: { active, @@ -2810,8 +2984,8 @@ export class UsersRaw extends BaseRaw { * @param {IRole['_id']} role the role id * @param {boolean} active */ - setActiveNotLoggedInAfterWithRole(latestLastLoginDate, role = 'user', active = false) { - const neverActive = { lastLogin: { $exists: 0 }, createdAt: { $lte: latestLastLoginDate } }; + setActiveNotLoggedInAfterWithRole(latestLastLoginDate: Date, role: IRole['_id'] = 'user', active = false) { + const neverActive = { lastLogin: { $exists: false }, createdAt: { $lte: latestLastLoginDate } }; const idleTooLong = { lastLogin: { $lte: latestLastLoginDate } }; const query = { @@ -2829,8 +3003,8 @@ export class UsersRaw extends BaseRaw { return this.updateMany(query, update); } - unsetRequirePasswordChange(_id) { - const update = { + unsetRequirePasswordChange(_id: IUser['_id']) { + const update: UpdateFilter = { $unset: { requirePasswordChange: true, requirePasswordChangeReason: true, @@ -2840,10 +3014,10 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - resetPasswordAndSetRequirePasswordChange(_id, requirePasswordChange, requirePasswordChangeReason) { + resetPasswordAndSetRequirePasswordChange(_id: IUser['_id'], requirePasswordChange: boolean, requirePasswordChangeReason: string) { const update = { $unset: { - 'services.password': 1, + 'services.password': 1 as const, }, $set: { requirePasswordChange, @@ -2854,7 +3028,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - setLanguage(_id, language) { + setLanguage(_id: IUser['_id'], language: string) { const update = { $set: { language, @@ -2864,7 +3038,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - setProfile(_id, profile) { + setProfile(_id: IUser['_id'], profile: Record) { const update = { $set: { 'settings.profile': profile, @@ -2874,8 +3048,8 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - setBio(_id, bio = '') { - const update = { + setBio(_id: IUser['_id'], bio = '') { + const update: UpdateFilter = { ...(bio.trim() ? { $set: { @@ -2891,8 +3065,8 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - setNickname(_id, nickname = '') { - const update = { + setNickname(_id: IUser['_id'], nickname = '') { + const update: UpdateFilter = { ...(nickname.trim() ? { $set: { @@ -2908,7 +3082,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - clearSettings(_id) { + clearSettings(_id: IUser['_id']) { const update = { $set: { settings: {}, @@ -2918,26 +3092,26 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - setPreferences(_id, preferences) { + setPreferences(_id: IUser['_id'], preferences: Record) { const settingsObject = Object.assign( {}, ...Object.keys(preferences).map((key) => ({ - [`settings.preferences.${key}`]: preferences[key], + ...(preferences[key] !== undefined && { [`settings.preferences.${key}`]: preferences[key] }), })), ); - const update = { + const update: DeepWritable> = { $set: settingsObject, }; if (parseInt(preferences.clockMode) === 0) { - delete update.$set['settings.preferences.clockMode']; + delete update.$set?.['settings.preferences.clockMode']; update.$unset = { 'settings.preferences.clockMode': 1 }; } return this.updateOne({ _id }, update); } - setTwoFactorAuthorizationHashAndUntilForUserIdAndToken(_id, token, hash, until) { + setTwoFactorAuthorizationHashAndUntilForUserIdAndToken(_id: IUser['_id'], token: string, hash: string, until: Date) { return this.updateOne( { _id, @@ -2952,7 +3126,7 @@ export class UsersRaw extends BaseRaw { ); } - setUtcOffset(_id, utcOffset) { + setUtcOffset(_id: IUser['_id'], utcOffset: number) { const query = { _id, utcOffset: { @@ -2969,52 +3143,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } - saveUserById(_id, data) { - const setData = {}; - const unsetData = {}; - - if (data.name != null) { - if (data.name.trim()) { - setData.name = data.name.trim(); - } else { - unsetData.name = 1; - } - } - - if (data.email != null) { - if (data.email.trim()) { - setData.emails = [{ address: data.email.trim() }]; - } else { - unsetData.emails = 1; - } - } - - if (data.phone != null) { - if (data.phone.trim()) { - setData.phone = [{ phoneNumber: data.phone.trim() }]; - } else { - unsetData.phone = 1; - } - } - - const update = {}; - - if (setData) { - update.$set = setData; - } - - if (unsetData) { - update.$unset = unsetData; - } - - if (update) { - return true; - } - - return this.updateOne({ _id }, update); - } - - setReason(_id, reason) { + setReason(_id: IUser['_id'], reason: string) { const update = { $set: { reason, @@ -3024,17 +3153,17 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - unsetReason(_id) { + unsetReason(_id: IUser['_id']) { const update = { $unset: { - reason: true, + reason: true as const, }, }; return this.updateOne({ _id }, update); } - async bannerExistsById(_id, bannerId) { + async bannerExistsById(_id: IUser['_id'], bannerId: string) { const query = { _id, [`banners.${bannerId}`]: { @@ -3045,7 +3174,7 @@ export class UsersRaw extends BaseRaw { return (await this.countDocuments(query)) !== 0; } - setBannerReadById(_id, bannerId) { + setBannerReadById(_id: IUser['_id'], bannerId: string) { const update = { $set: { [`banners.${bannerId}.read`]: true, @@ -3055,27 +3184,27 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - removeBannerById(_id, bannerId) { + removeBannerById(_id: IUser['_id'], bannerId: string) { const update = { $unset: { - [`banners.${bannerId}`]: true, + [`banners.${bannerId}`]: true as const, }, }; return this.updateOne({ _id }, update); } - removeSamlServiceSession(_id) { + removeSamlServiceSession(_id: IUser['_id']) { const update = { $unset: { - 'services.saml.idpSession': '', + 'services.saml.idpSession': 1 as const, }, }; return this.updateOne({ _id }, update); } - updateDefaultStatus(_id, statusDefault) { + updateDefaultStatus(_id: IUser['_id'], statusDefault: UserStatus) { return this.updateOne( { _id, @@ -3089,8 +3218,8 @@ export class UsersRaw extends BaseRaw { ); } - setSamlInResponseTo(_id, inResponseTo) { - this.updateOne( + setSamlInResponseTo(_id: IUser['_id'], inResponseTo: string) { + return this.updateOne( { _id, }, @@ -3102,7 +3231,7 @@ export class UsersRaw extends BaseRaw { ); } - async setFreeSwitchExtension(_id, extension) { + async setFreeSwitchExtension(_id: IUser['_id'], extension?: string) { return this.updateOne( { _id, @@ -3114,11 +3243,11 @@ export class UsersRaw extends BaseRaw { } // INSERT - create(data) { + create(data: InsertionModel) { const user = { createdAt: new Date(), avatarOrigin: 'none', - }; + } as InsertionModel; Object.assign(user, data); @@ -3126,18 +3255,18 @@ export class UsersRaw extends BaseRaw { } // REMOVE - removeById(_id) { + removeById(_id: IUser['_id']) { return this.deleteOne({ _id }); } - removeLivechatData(userId) { + removeLivechatData(userId: IUser['_id']) { const query = { _id: userId, }; const update = { $unset: { - livechat: true, + livechat: 1 as const, }, }; @@ -3151,15 +3280,15 @@ export class UsersRaw extends BaseRaw { - has not disabled email notifications - `active` is equal to true (false means they were deactivated and can't login) */ - getUsersToSendOfflineEmail(usersIds) { + getUsersToSendOfflineEmail(usersIds: IUser['_id'][]) { const query = { '_id': { $in: usersIds, }, 'active': true, - 'status': 'offline', + 'status': UserStatus.OFFLINE, 'statusConnection': { - $ne: 'online', + $ne: UserStatus.ONLINE, }, 'emails.verified': true, }; @@ -3177,7 +3306,7 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - countActiveUsersByService(serviceName, options) { + countActiveUsersByService(serviceName: string, options?: FindOptions) { const query = { active: true, type: { $nin: ['app'] }, @@ -3189,7 +3318,7 @@ export class UsersRaw extends BaseRaw { } // here - getActiveLocalUserCount() { + async getActiveLocalUserCount() { return Promise.all([ // Count all active users (fast based on index) this.countDocuments({ @@ -3209,8 +3338,8 @@ export class UsersRaw extends BaseRaw { return this.countActiveLocalGuests(idExceptions); } - removeOlderResumeTokensByUserId(userId, fromDate) { - this.updateOne( + removeOlderResumeTokensByUserId(userId: IUser['_id'], fromDate: Date) { + return this.updateOne( { _id: userId }, { $pull: { @@ -3250,7 +3379,7 @@ export class UsersRaw extends BaseRaw { return this.countDocuments(query); } - updateCustomFieldsById(userId, customFields) { + updateCustomFieldsById(userId: IUser['_id'], customFields: Record) { return this.updateOne( { _id: userId }, { @@ -3261,12 +3390,12 @@ export class UsersRaw extends BaseRaw { ); } - countRoomMembers(roomId) { + countRoomMembers(roomId: IRoom['_id']) { return this.countDocuments({ __rooms: roomId, active: true }); } - removeAgent(_id) { - const update = { + removeAgent(_id: IUser['_id']) { + const update: UpdateFilter = { $set: { operator: false, }, @@ -3281,11 +3410,11 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - countByRole(role) { + countByRole(role: IRole['_id']) { return this.countDocuments({ roles: role }); } - updateLivechatStatusByAgentIds(userIds, status) { + updateLivechatStatusByAgentIds(userIds: IUser['_id'][], status: ILivechatAgentStatus) { return this.updateMany( { _id: { $in: userIds }, diff --git a/packages/models/src/updater.ts b/packages/models/src/updater.ts index 4f7ad271f397d..9babf5b5be20f 100644 --- a/packages/models/src/updater.ts +++ b/packages/models/src/updater.ts @@ -46,7 +46,7 @@ export class UpdaterImpl implements Updater { } hasChanges() { - const filter = this._getUpdateFilter(); + const filter = this.getRawUpdateFilter(); return this._hasChanges(filter); } @@ -54,7 +54,7 @@ export class UpdaterImpl implements Updater { return Object.keys(filter).length > 0; } - private _getUpdateFilter() { + public getRawUpdateFilter() { return { ...(this._set && { $set: Object.fromEntries(this._set) }), ...(this._unset && { $unset: Object.fromEntries([...this._unset.values()].map((k) => [k, 1])) }), @@ -68,7 +68,7 @@ export class UpdaterImpl implements Updater { throw new Error('Updater is dirty'); } this.dirty = true; - const filter = this._getUpdateFilter(); + const filter = this.getRawUpdateFilter(); if (!this._hasChanges(filter)) { throw new Error('No changes to update'); } diff --git a/packages/models/tsconfig.json b/packages/models/tsconfig.json index e28418fe4a248..52e9dd8c4976b 100644 --- a/packages/models/tsconfig.json +++ b/packages/models/tsconfig.json @@ -1,11 +1,9 @@ { "extends": "../../tsconfig.base.server.json", "compilerOptions": { - // TODO migrate Users to TS - "allowJs": true, "declaration": true, "rootDir": "./src", - "outDir": "./dist", + "outDir": "./dist" }, "include": ["./src/**/*"] } diff --git a/packages/mongo-adapter/.eslintrc.json b/packages/mongo-adapter/.eslintrc.json new file mode 100644 index 0000000000000..a83aeda48e66d --- /dev/null +++ b/packages/mongo-adapter/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/packages/mongo-adapter/CHANGELOG.md b/packages/mongo-adapter/CHANGELOG.md new file mode 100644 index 0000000000000..33beba0c5a98c --- /dev/null +++ b/packages/mongo-adapter/CHANGELOG.md @@ -0,0 +1,13 @@ +# @rocket.chat/account-utils + +## 0.0.2 + +### Patch Changes + +- ([#31138](https://github.com/RocketChat/Rocket.Chat/pull/31138)) feat(uikit): Move `@rocket.chat/ui-kit` package to the main monorepo + +## 0.0.2-rc.0 + +### Patch Changes + +- b223cbde14: feat(uikit): Move `@rocket.chat/ui-kit` package to the main monorepo diff --git a/packages/mongo-adapter/package.json b/packages/mongo-adapter/package.json new file mode 100644 index 0000000000000..3059f549d42e2 --- /dev/null +++ b/packages/mongo-adapter/package.json @@ -0,0 +1,20 @@ +{ + "name": "@rocket.chat/mongo-adapter", + "version": "0.0.2", + "private": true, + "devDependencies": { + "eslint": "~8.45.0", + "typescript": "~5.7.2" + }, + "scripts": { + "lint": "eslint --ext .js,.jsx,.ts,.tsx .", + "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "build": "rm -rf dist && tsc -p tsconfig.json", + "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" + }, + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "files": [ + "/dist" + ] +} diff --git a/apps/meteor/client/lib/minimongo/bson.spec.ts b/packages/mongo-adapter/src/bson.spec.ts similarity index 100% rename from apps/meteor/client/lib/minimongo/bson.spec.ts rename to packages/mongo-adapter/src/bson.spec.ts diff --git a/apps/meteor/client/lib/minimongo/bson.ts b/packages/mongo-adapter/src/bson.ts similarity index 100% rename from apps/meteor/client/lib/minimongo/bson.ts rename to packages/mongo-adapter/src/bson.ts diff --git a/apps/meteor/client/lib/minimongo/comparisons.spec.ts b/packages/mongo-adapter/src/comparisons.spec.ts similarity index 100% rename from apps/meteor/client/lib/minimongo/comparisons.spec.ts rename to packages/mongo-adapter/src/comparisons.spec.ts diff --git a/apps/meteor/client/lib/minimongo/comparisons.ts b/packages/mongo-adapter/src/comparisons.ts similarity index 100% rename from apps/meteor/client/lib/minimongo/comparisons.ts rename to packages/mongo-adapter/src/comparisons.ts diff --git a/apps/meteor/client/lib/minimongo/index.ts b/packages/mongo-adapter/src/index.ts similarity index 100% rename from apps/meteor/client/lib/minimongo/index.ts rename to packages/mongo-adapter/src/index.ts diff --git a/apps/meteor/client/lib/minimongo/lookups.spec.ts b/packages/mongo-adapter/src/lookups.spec.ts similarity index 100% rename from apps/meteor/client/lib/minimongo/lookups.spec.ts rename to packages/mongo-adapter/src/lookups.spec.ts diff --git a/apps/meteor/client/lib/minimongo/lookups.ts b/packages/mongo-adapter/src/lookups.ts similarity index 100% rename from apps/meteor/client/lib/minimongo/lookups.ts rename to packages/mongo-adapter/src/lookups.ts diff --git a/apps/meteor/client/lib/minimongo/query.ts b/packages/mongo-adapter/src/query.ts similarity index 100% rename from apps/meteor/client/lib/minimongo/query.ts rename to packages/mongo-adapter/src/query.ts diff --git a/apps/meteor/client/lib/minimongo/sort.ts b/packages/mongo-adapter/src/sort.ts similarity index 100% rename from apps/meteor/client/lib/minimongo/sort.ts rename to packages/mongo-adapter/src/sort.ts diff --git a/apps/meteor/client/lib/minimongo/types.ts b/packages/mongo-adapter/src/types.ts similarity index 100% rename from apps/meteor/client/lib/minimongo/types.ts rename to packages/mongo-adapter/src/types.ts diff --git a/packages/mongo-adapter/tsconfig.json b/packages/mongo-adapter/tsconfig.json new file mode 100644 index 0000000000000..e2be47cf5499f --- /dev/null +++ b/packages/mongo-adapter/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.client.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["./src/**/*"] +} diff --git a/packages/peggy-loader/package.json b/packages/peggy-loader/package.json index 18dfe84f7a8f2..a2e64f028058b 100644 --- a/packages/peggy-loader/package.json +++ b/packages/peggy-loader/package.json @@ -44,7 +44,7 @@ "devDependencies": { "@rocket.chat/eslint-config": "workspace:~", "@rocket.chat/prettier-config": "~0.31.25", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "eslint": "~8.45.0", "npm-run-all": "^4.1.5", "peggy": "4.1.1", diff --git a/packages/release-action/package.json b/packages/release-action/package.json index 9786588982e31..4b06a2af2dde0 100644 --- a/packages/release-action/package.json +++ b/packages/release-action/package.json @@ -10,7 +10,7 @@ "main": "dist/index.js", "packageManager": "yarn@3.5.1", "devDependencies": { - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "typescript": "~5.7.2" }, "dependencies": { diff --git a/packages/release-action/src/getMetadata.ts b/packages/release-action/src/getMetadata.ts index 5ca93ed5e8c57..fb56c1621b497 100644 --- a/packages/release-action/src/getMetadata.ts +++ b/packages/release-action/src/getMetadata.ts @@ -1,4 +1,5 @@ import { readFile } from 'fs/promises'; +import { EOL } from 'os'; import path from 'path'; import { readPackageJson } from './utils'; @@ -34,3 +35,20 @@ export async function getAppsEngineVersion(cwd: string) { console.error(e); } } + +export async function getDenoVersion(cwd: string) { + try { + const toolVersionsContent = await readFile(path.join(cwd, '.tool-versions')); + const data = toolVersionsContent.toString().replace(EOL, ' ').split(' '); + + for (let i = 0; i < data.length; i++) { + if (data[i] === 'deno') { + return data[i + 1]; + } + } + + return 'Not available'; + } catch (e) { + console.error(e); + } +} diff --git a/packages/release-action/src/utils.ts b/packages/release-action/src/utils.ts index ff7dc06318e1f..a5ae8c48b2c9c 100644 --- a/packages/release-action/src/utils.ts +++ b/packages/release-action/src/utils.ts @@ -7,7 +7,7 @@ import remarkParse from 'remark-parse'; import remarkStringify from 'remark-stringify'; import unified from 'unified'; -import { getAppsEngineVersion, getMongoVersion, getNodeNpmVersions } from './getMetadata'; +import { getAppsEngineVersion, getDenoVersion, getMongoVersion, getNodeNpmVersions } from './getMetadata'; export const BumpLevels = { dep: 0, @@ -110,11 +110,13 @@ Bump ${pkgName} version. export async function getEngineVersionsMd(cwd: string) { const { node } = await getNodeNpmVersions(cwd); const appsEngine = await getAppsEngineVersion(cwd); + const deno = await getDenoVersion(cwd); const mongo = await getMongoVersion(cwd); return `### Engine versions - Node: \`${node}\` +- Deno: \`${deno}\` - MongoDB: \`${mongo.join(', ')}\` - Apps-Engine: \`${appsEngine}\` diff --git a/packages/release-changelog/package.json b/packages/release-changelog/package.json index 031afdc5f576f..b448bdda1ca22 100644 --- a/packages/release-changelog/package.json +++ b/packages/release-changelog/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@changesets/types": "^6.0.0", "@rocket.chat/eslint-config": "workspace:^", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "eslint": "~8.45.0", "typescript": "~5.7.2" }, diff --git a/packages/rest-typings/CHANGELOG.md b/packages/rest-typings/CHANGELOG.md index 722eec57c3559..edd532bf843aa 100644 --- a/packages/rest-typings/CHANGELOG.md +++ b/packages/rest-typings/CHANGELOG.md @@ -1,5 +1,90 @@ # @rocket.chat/rest-typings +## 7.6.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 +
      + +## 7.6.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 +
      + +## 7.6.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 +
      + +## 7.6.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 +
      + +## 7.6.0-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 +
      + +## 7.6.0-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 +
      + +## 7.6.0-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 +
      + +## 7.6.0-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 +
      + +## 7.6.0-rc.0 + +### Minor Changes + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 +
      + ## 7.5.1 ### Patch Changes diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 04de9f0f60228..a260dcc60e981 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "7.5.1", + "version": "7.6.0-rc.8", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:~", diff --git a/packages/rest-typings/src/apps/index.ts b/packages/rest-typings/src/apps/index.ts index 528a1cc224078..d5f9de98d0ca8 100644 --- a/packages/rest-typings/src/apps/index.ts +++ b/packages/rest-typings/src/apps/index.ts @@ -123,6 +123,7 @@ export type AppsEndpoints = { '/apps/:id/status': { GET: () => { status: string; + clusterStatus: App['clusterStatus']; }; POST: (params: { status: AppStatus }) => { status: AppStatus; @@ -173,7 +174,7 @@ export type AppsEndpoints = { }; '/apps/installed': { - GET: () => { apps: App[] }; + GET: (params: { includeClusterStatus?: 'true' | 'false' }) => { success: true; apps: App[] } | { success: false; error: string }; }; '/apps/buildExternalAppRequest': { diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 834dc8efc459d..51e5934864c0f 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -590,7 +590,7 @@ const POSTLivechatDepartmentSchema = { showOnOfflineForm: { type: 'boolean', }, - requestTagsBeforeClosingChat: { + requestTagBeforeClosingChat: { type: 'boolean', nullable: true, }, diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index 410bd711d24a4..76dfbee33ddb0 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -8,3 +8,4 @@ export * from './timezone'; export * from './wrapExceptions'; export * from './getLoginExpiration'; export * from './converter'; +export * from './removeEmpty'; diff --git a/packages/tools/src/removeEmpty.spec.ts b/packages/tools/src/removeEmpty.spec.ts new file mode 100644 index 0000000000000..62c3c15593f1e --- /dev/null +++ b/packages/tools/src/removeEmpty.spec.ts @@ -0,0 +1,87 @@ +import { removeEmpty } from './removeEmpty'; + +describe('removeEmpty', () => { + it('should remove null props', () => { + const obj = { a: 1, b: null }; + + expect(removeEmpty(obj)).toEqual({ a: 1 }); + }); + + it('should remove undefined props', () => { + const obj = { a: 1, b: undefined }; + + expect(removeEmpty(obj)).toEqual({ a: 1 }); + }); + + it('should not remove empty strings', () => { + const obj = { a: 1, b: '' }; + + expect(removeEmpty(obj)).toEqual({ a: 1, b: '' }); + }); + + it('should not remove empty arrays', () => { + const obj = { a: 1, b: [] }; + + expect(removeEmpty(obj)).toEqual({ a: 1, b: [] }); + }); + + it('should not remove empty objects', () => { + const obj = { a: 1, b: {} }; + + expect(removeEmpty(obj)).toEqual({ a: 1, b: {} }); + }); + + it('should not remove 0', () => { + const obj = { a: 1, b: 0 }; + + expect(removeEmpty(obj)).toEqual({ a: 1, b: 0 }); + }); + + it('should not remove false', () => { + const obj = { a: 1, b: false }; + + expect(removeEmpty(obj)).toEqual({ a: 1, b: false }); + }); + + it('should not remove NaN', () => { + const obj = { a: 1, b: NaN }; + + expect(removeEmpty(obj)).toEqual({ a: 1, b: NaN }); + }); + + it('should not remove Infinity', () => { + const obj = { a: 1, b: Infinity }; + + expect(removeEmpty(obj)).toEqual({ a: 1, b: Infinity }); + }); + + it('should not remove functions', () => { + const fn = () => { + // noop + }; + + const obj = { + a: 1, + fn, + }; + + expect(removeEmpty(obj)).toEqual({ + a: 1, + fn, + }); + }); + + it('should not remove symbols', () => { + const b = Symbol('test'); + + const obj = { a: 1, b }; + + expect(removeEmpty(obj)).toEqual({ a: 1, b }); + }); + + it('should not remove objects with non-empty props', () => { + const obj = { a: 1, b: { c: 2 } }; + + expect(removeEmpty(obj)).toEqual({ a: 1, b: { c: 2 } }); + }); +}); diff --git a/packages/tools/src/removeEmpty.ts b/packages/tools/src/removeEmpty.ts new file mode 100644 index 0000000000000..2e8bfc3197362 --- /dev/null +++ b/packages/tools/src/removeEmpty.ts @@ -0,0 +1,7 @@ +type NonEmpty = { + [K in keyof T]: Exclude; +}; + +export function removeEmpty>(obj: T): NonEmpty { + return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v != null)) as NonEmpty; +} diff --git a/packages/ui-avatar/CHANGELOG.md b/packages/ui-avatar/CHANGELOG.md index 9c95e4b70a7e9..df654755c455d 100644 --- a/packages/ui-avatar/CHANGELOG.md +++ b/packages/ui-avatar/CHANGELOG.md @@ -1,5 +1,86 @@ # @rocket.chat/ui-avatar +## 14.0.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.8 +
      + +## 14.0.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.7 +
      + +## 14.0.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.6 +
      + +## 14.0.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.5 +
      + +## 14.0.0-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.4 +
      + +## 14.0.0-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.3 +
      + +## 14.0.0-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.2 +
      + +## 14.0.0-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.1 +
      + +## 14.0.0-rc.0 + +### Patch Changes + +-
      Updated dependencies [1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de]: + + - @rocket.chat/ui-contexts@18.0.0-rc.0 +
      + ## 13.0.1 ### Patch Changes diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index 022ce6599fbf7..28c58d121091e 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -1,10 +1,10 @@ { "name": "@rocket.chat/ui-avatar", - "version": "13.0.1", + "version": "14.0.0-rc.8", "private": true, "devDependencies": { "@babel/core": "~7.26.0", - "@rocket.chat/fuselage": "~0.61.0", + "@rocket.chat/fuselage": "~0.62.0", "@rocket.chat/ui-contexts": "workspace:^", "@types/react": "~18.3.17", "@types/react-dom": "~18.3.5", @@ -30,7 +30,7 @@ ], "peerDependencies": { "@rocket.chat/fuselage": "*", - "@rocket.chat/ui-contexts": "17.0.1", + "@rocket.chat/ui-contexts": "workspace:^", "react": "~17.0.2" }, "volta": { diff --git a/packages/ui-client/.storybook/DocsContainer.tsx b/packages/ui-client/.storybook/DocsContainer.tsx new file mode 100644 index 0000000000000..217b6b05910d1 --- /dev/null +++ b/packages/ui-client/.storybook/DocsContainer.tsx @@ -0,0 +1,21 @@ +import { DocsContainer as BaseContainer } from '@storybook/blocks'; +import { addons } from '@storybook/preview-api'; +import { themes } from '@storybook/theming'; +import type { ComponentPropsWithoutRef } from 'react'; +import { useEffect, useState } from 'react'; +import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode'; + +const channel = addons.getChannel(); + +const DocsContainer = (props: ComponentPropsWithoutRef) => { + const [isDark, setDark] = useState(false); + + useEffect(() => { + channel.on(DARK_MODE_EVENT_NAME, setDark); + return () => channel.removeListener(DARK_MODE_EVENT_NAME, setDark); + }, [setDark]); + + return ; +}; + +export default DocsContainer; diff --git a/packages/ui-client/.storybook/logo.svg b/packages/ui-client/.storybook/logo.svg new file mode 100644 index 0000000000000..9f732657872d4 --- /dev/null +++ b/packages/ui-client/.storybook/logo.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/ui-client/.storybook/logo.svg.d.ts b/packages/ui-client/.storybook/logo.svg.d.ts new file mode 100644 index 0000000000000..27c0914b230f0 --- /dev/null +++ b/packages/ui-client/.storybook/logo.svg.d.ts @@ -0,0 +1,3 @@ +declare const path: string; + +export = path; diff --git a/packages/ui-client/.storybook/main.ts b/packages/ui-client/.storybook/main.ts index 53ce127e65afb..0dae84a3b1798 100644 --- a/packages/ui-client/.storybook/main.ts +++ b/packages/ui-client/.storybook/main.ts @@ -5,7 +5,9 @@ import type { StorybookConfig } from '@storybook/react-webpack5'; const config: StorybookConfig = { stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'], addons: [ + getAbsolutePath('@storybook/addon-a11y'), getAbsolutePath('@storybook/addon-essentials'), + getAbsolutePath('storybook-dark-mode'), getAbsolutePath('@storybook/addon-webpack5-compiler-babel'), getAbsolutePath('@storybook/addon-styling-webpack'), ], diff --git a/packages/ui-client/.storybook/preview.tsx b/packages/ui-client/.storybook/preview.tsx index 75d4937b1beda..dbd4c788135d6 100644 --- a/packages/ui-client/.storybook/preview.tsx +++ b/packages/ui-client/.storybook/preview.tsx @@ -1,28 +1,61 @@ +import { PaletteStyleTag } from '@rocket.chat/fuselage'; +import surface from '@rocket.chat/fuselage-tokens/dist/surface.json'; import type { Decorator, Parameters } from '@storybook/react'; +import { themes } from '@storybook/theming'; +import { useDarkMode } from 'storybook-dark-mode'; -import '../../../apps/meteor/app/theme/client/main.css'; -import 'highlight.js/styles/github.css'; +import manifest from '../package.json'; +import DocsContainer from './DocsContainer'; +import logo from './logo.svg'; +import '@rocket.chat/fuselage/dist/fuselage.css'; import '@rocket.chat/icons/dist/rocketchat.css'; export const parameters: Parameters = { - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/, + backgrounds: { + grid: { + cellSize: 4, + cellAmount: 4, + opacity: 0.5, + }, + }, + options: { + storySort: { + method: 'alphabetical', + }, + }, + layout: 'fullscreen', + docs: { + container: DocsContainer, + }, + darkMode: { + dark: { + ...themes.dark, + appBg: surface.surface.dark.sidebar, + appContentBg: surface.surface.dark.light, + appPreviewBg: 'transparent', + barBg: surface.surface.dark.light, + brandTitle: manifest.name, + brandImage: logo, + }, + light: { + ...themes.normal, + appPreviewBg: 'transparent', + brandTitle: manifest.name, + brandImage: logo, }, }, }; export const decorators: Decorator[] = [ - (Story) => ( -
      - - -
      - ), + (Story) => { + const dark = useDarkMode(); + + return ( + <> + + + + ); + }, ]; export const tags = ['autodocs']; diff --git a/packages/ui-client/CHANGELOG.md b/packages/ui-client/CHANGELOG.md index c98cfc7f452a9..332a157940c92 100644 --- a/packages/ui-client/CHANGELOG.md +++ b/packages/ui-client/CHANGELOG.md @@ -1,5 +1,110 @@ # @rocket.chat/ui-client +## 18.0.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.8 + - @rocket.chat/ui-avatar@14.0.0-rc.8 +
      + +## 18.0.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.7 + - @rocket.chat/ui-avatar@14.0.0-rc.7 +
      + +## 18.0.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.6 + - @rocket.chat/ui-avatar@14.0.0-rc.6 +
      + +## 18.0.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.5 + - @rocket.chat/ui-avatar@14.0.0-rc.5 +
      + +## 18.0.0-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.4 + - @rocket.chat/ui-avatar@14.0.0-rc.4 +
      + +## 18.0.0-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.3 + - @rocket.chat/ui-avatar@14.0.0-rc.3 +
      + +## 18.0.0-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.2 + - @rocket.chat/ui-avatar@14.0.0-rc.2 +
      + +## 18.0.0-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.1 + - @rocket.chat/ui-avatar@14.0.0-rc.1 +
      + +## 18.0.0-rc.0 + +### Minor Changes + +- ([#35613](https://github.com/RocketChat/Rocket.Chat/pull/35613)) Replaces the parent room tag in room header in favor of a button to back to the parent room + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#35615](https://github.com/RocketChat/Rocket.Chat/pull/35615)) Removes the avatar in the room header + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#35218](https://github.com/RocketChat/Rocket.Chat/pull/35218)) Adds a new admin page to audit settings changes in a server + +- ([#35631](https://github.com/RocketChat/Rocket.Chat/pull/35631)) Places the room topic next to the room title + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#35672](https://github.com/RocketChat/Rocket.Chat/pull/35672)) Restores the previous room announcement style + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#35807](https://github.com/RocketChat/Rocket.Chat/pull/35807)) Moves the room search functionality from the sidebar to the navbar and reorganize their relative actions + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it + +### Patch Changes + +-
      Updated dependencies [1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de]: + + - @rocket.chat/ui-contexts@18.0.0-rc.0 + - @rocket.chat/ui-avatar@14.0.0-rc.0 +
      + ## 17.0.1 ### Patch Changes diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 34b4dab1f85c0..de31109761248 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-client", - "version": "17.0.1", + "version": "18.0.0-rc.8", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", @@ -21,13 +21,14 @@ "@babel/core": "~7.26.0", "@react-aria/toolbar": "^3.0.0-nightly.5042", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "~0.61.0", + "@rocket.chat/fuselage": "~0.62.0", "@rocket.chat/fuselage-hooks": "~0.35.0", - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/mock-providers": "workspace:^", "@rocket.chat/ui-avatar": "workspace:~", "@rocket.chat/ui-contexts": "workspace:~", + "@storybook/addon-a11y": "^8.6.4", "@storybook/addon-actions": "^8.6.4", "@storybook/addon-docs": "^8.6.4", "@storybook/addon-essentials": "^8.6.4", @@ -53,6 +54,7 @@ "react-dom": "~18.3.1", "react-hook-form": "~7.45.4", "storybook": "^8.6.4", + "storybook-dark-mode": "^4.0.2", "typescript": "~5.7.2" }, "peerDependencies": { @@ -61,8 +63,8 @@ "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", - "@rocket.chat/ui-avatar": "13.0.1", - "@rocket.chat/ui-contexts": "17.0.1", + "@rocket.chat/ui-avatar": "workspace:^", + "@rocket.chat/ui-contexts": "workspace:^", "react": "*", "react-i18next": "*" }, diff --git a/packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.spec.tsx b/packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.spec.tsx new file mode 100644 index 0000000000000..0c58ce6d4876a --- /dev/null +++ b/packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.spec.tsx @@ -0,0 +1,23 @@ +import { composeStories } from '@storybook/react'; +import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; + +import * as stories from './AnnouncementBanner.stories'; + +const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]); + +test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => { + const tree = render(); + expect(tree.baseElement).toMatchSnapshot(); +}); + +test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => { + const { container } = render(); + + /** + ** We are disabling the rule in this case because ideally we should not have nested interactive + ** but in this case we need to open a modal when clicking in the `AnnouncementBanner` + **/ + const results = await axe(container, { rules: { 'nested-interactive': { enabled: false } } }); + expect(results).toHaveNoViolations(); +}); diff --git a/packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.stories.tsx b/packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.stories.tsx new file mode 100644 index 0000000000000..165c0154d3361 --- /dev/null +++ b/packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.stories.tsx @@ -0,0 +1,28 @@ +import { action } from '@storybook/addon-actions'; +import { Meta, StoryFn } from '@storybook/react'; + +import AnnouncementBanner from './AnnouncementBanner'; + +export default { + title: 'Components/AnnouncementBanner', + component: AnnouncementBanner, + args: { + onClick: action('clicked'), + }, +} satisfies Meta; + +const Template: StoryFn = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + children: 'Announcement', +}; + +export const WithLink = Template.bind({}); +WithLink.args = { + children: ( + + Announcement + + ), +}; diff --git a/apps/meteor/client/views/room/Announcement/AnnouncementComponent.tsx b/packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.tsx similarity index 55% rename from apps/meteor/client/views/room/Announcement/AnnouncementComponent.tsx rename to packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.tsx index 6772d1d9c6ed9..fcdd254f75853 100644 --- a/apps/meteor/client/views/room/Announcement/AnnouncementComponent.tsx +++ b/packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.tsx @@ -1,13 +1,13 @@ import { css } from '@rocket.chat/css-in-js'; import { Box, Palette } from '@rocket.chat/fuselage'; -import type { MouseEvent, ReactNode } from 'react'; +import type { AllHTMLAttributes, ReactNode, MouseEvent } from 'react'; -type AnnouncementComponentProps = { - children?: ReactNode; - onClickOpen: (e: MouseEvent) => void; -}; +type AnnouncementBannerProps = { + children: ReactNode; + onClick?: (e: MouseEvent) => void; +} & Omit, 'is'>; -const AnnouncementComponent = ({ children, onClickOpen }: AnnouncementComponentProps) => { +const AnnouncementBanner = ({ children, className, onClick, ...props }: AnnouncementBannerProps) => { const announcementBar = css` background-color: ${Palette.status['status-background-info'].theme('announcement-background')}; color: ${Palette.text['font-pure-black'].theme('announcement-text')}; @@ -20,28 +20,32 @@ const AnnouncementComponent = ({ children, onClickOpen }: AnnouncementComponentP > * { flex: auto; } - &:hover, - &:focus { + &:hover { text-decoration: underline; } `; return ( - + {children} ); }; -export default AnnouncementComponent; +export default AnnouncementBanner; diff --git a/packages/ui-client/src/components/AnnouncementBanner/__snapshots__/AnnouncementBanner.spec.tsx.snap b/packages/ui-client/src/components/AnnouncementBanner/__snapshots__/AnnouncementBanner.spec.tsx.snap new file mode 100644 index 0000000000000..d2fc70f9f1cc3 --- /dev/null +++ b/packages/ui-client/src/components/AnnouncementBanner/__snapshots__/AnnouncementBanner.spec.tsx.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders Default without crashing 1`] = ` + +
      +
      +
      + Announcement +
      +
      +
      + +`; + +exports[`renders WithLink without crashing 1`] = ` + +
      + +
      + +`; diff --git a/packages/ui-client/src/components/AnnouncementBanner/index.ts b/packages/ui-client/src/components/AnnouncementBanner/index.ts new file mode 100644 index 0000000000000..22e294d3a3e3f --- /dev/null +++ b/packages/ui-client/src/components/AnnouncementBanner/index.ts @@ -0,0 +1 @@ +export { default } from './AnnouncementBanner'; diff --git a/packages/ui-client/src/components/Header/Header.stories.tsx b/packages/ui-client/src/components/Header/Header.stories.tsx index e811675b68c9b..cc129f191601a 100644 --- a/packages/ui-client/src/components/Header/Header.stories.tsx +++ b/packages/ui-client/src/components/Header/Header.stories.tsx @@ -40,7 +40,6 @@ export default { [ () => () => undefined, () => ({ diff --git a/packages/ui-client/src/components/Header/HeaderState.tsx b/packages/ui-client/src/components/Header/HeaderState.tsx index 01fe2a162129e..3a21eb949550e 100644 --- a/packages/ui-client/src/components/Header/HeaderState.tsx +++ b/packages/ui-client/src/components/Header/HeaderState.tsx @@ -1,6 +1,17 @@ import { Icon, IconButton } from '@rocket.chat/fuselage'; -import type { FC } from 'react'; +import type { Keys as IconName } from '@rocket.chat/icons'; +import type { AllHTMLAttributes, ComponentPropsWithoutRef, FC, MouseEventHandler } from 'react'; -const HeaderState: FC = (props) => (props.onClick ? : ); +type HeaderStateProps = + | (Pick, 'color' | 'title' | 'icon'> & { + onClick: MouseEventHandler; + } & Omit, 'is'>) + | (Omit, 'name' | 'onClick'> & { + icon: IconName; + onClick?: undefined; + }); + +const HeaderState: FC = (props) => + props.onClick ? : ; export default HeaderState; diff --git a/packages/ui-client/src/components/HeaderV2/Header.stories.tsx b/packages/ui-client/src/components/HeaderV2/Header.stories.tsx index bf91e16a7be4b..2438dfb90bb7a 100644 --- a/packages/ui-client/src/components/HeaderV2/Header.stories.tsx +++ b/packages/ui-client/src/components/HeaderV2/Header.stories.tsx @@ -1,5 +1,5 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { Avatar } from '@rocket.chat/fuselage'; +import { Avatar, Box } from '@rocket.chat/fuselage'; import { SettingsContext } from '@rocket.chat/ui-contexts'; import { action } from '@storybook/addon-actions'; import type { Meta } from '@storybook/react'; @@ -17,8 +17,7 @@ import { HeaderV2Title as HeaderTitle, HeaderV2State as HeaderState, } from '.'; -import { RoomBanner } from '../RoomBanner'; -import { RoomBannerContent } from '../RoomBanner/RoomBannerContent'; +import AnnouncementBanner from '../AnnouncementBanner'; const avatarUrl = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAoACgDASIAAhEBAxEB/8QAGwAAAgIDAQAAAAAAAAAAAAAAAAcEBgIDBQj/xAAuEAACAQQAAwcEAQUAAAAAAAABAgMABAUREiExBhMUIkFRYQcWcYGhFTJSgpH/xAAYAQADAQEAAAAAAAAAAAAAAAACAwQBAP/EAB4RAAIBBQEBAQAAAAAAAAAAAAABAgMREiExE0HR/9oADAMBAAIRAxEAPwBuXuIkhBuMe5ib/AHQP49q4L3mLitryTLTSpOiHQI5k/HzXa/qbFOEudVTu1dumWvcTaNCZYZ7vU6g6LxqjOU/24dfs1Ouh9FnkMpd3Reeyx83hAxZZEhkdV9/MBrX71WGPvJcqrJBGveKATtuXXqNU0pu02bTHXD/AGvJAluyxxRd6F4x00o+NdKoVrjbzJdvVe1t5cVLc2ck8qjnohgpPtz2v7G6JtPQ2VJwjlcw+37mchpnK6GtIuv5NFWeTsLNPvxWTvpfjvOEfwKKzEVkSct2vscS/BIzSN0YRkeX81UpPqO8masJETu7OOccY4dswYFQeftv096XV5knuJGdm2T1+agvMXj8jEaHX905QihabvcbuS7X566mLWLwSY8PuRnk/u4eZ0deTl71Ef6hY+0yM88TzeNZY4luYwpVYyduOfrvhPTnr0pXSX9y5mCsyJMdyxxvwq599em+taItqCSNc90ChvZRUruUcT0JiO18Elpk7t8v41LWzacxkBSuvjQ/FFJayjDWrCTepAQ2vUH0oo/Jk3ovpwJJeVCP5CN+lFFaaMqy+nAyuChvrTI2kN9JAsi2ZOy4IBHMnkSCP+iqBexSWdxLazoUljJVlPUH2oorkV10pRc7b1zXb/hZOzuJvM86QWEXeELxOzHSIPcmiiiunVlF2RNTpRkrs//Z'; @@ -41,7 +40,6 @@ export default { [ () => () => undefined, () => ({ @@ -74,6 +72,8 @@ const room: IRoom = { t: 'c', name: 'general general general general general general general general general general general general general general general general general general general', _id: 'GENERAL', + topic: 'lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum', + announcement: 'Announcement', encrypted: true, autoTranslate: true, autoTranslateLanguage: 'pt-BR', @@ -164,7 +164,7 @@ export const WithActionBadge = () => (
      ); -export const WithTopicBanner = () => ( +export const WithTopic = () => ( <>
      @@ -177,6 +177,7 @@ export const WithTopicBanner = () => ( + {room.topic} @@ -185,8 +186,30 @@ export const WithTopicBanner = () => (
      - - Topic {room.name} - + +); + +export const WithAnnouncement = () => ( + <> +
      + + + + + + {icon && } + {room.name} + + + + + + + + + + +
      + {room.announcement} ); diff --git a/packages/ui-client/src/components/HeaderV2/Header.tsx b/packages/ui-client/src/components/HeaderV2/Header.tsx index 4ee887e93cfbd..fe36431541265 100644 --- a/packages/ui-client/src/components/HeaderV2/Header.tsx +++ b/packages/ui-client/src/components/HeaderV2/Header.tsx @@ -1,31 +1,35 @@ import { Box } from '@rocket.chat/fuselage'; -import { useLayout } from '@rocket.chat/ui-contexts'; import type { ComponentPropsWithoutRef } from 'react'; import HeaderDivider from './HeaderDivider'; type HeaderProps = ComponentPropsWithoutRef; -const Header = (props: HeaderProps) => { - const { isMobile } = useLayout(); - - return ( - - - - - ); -}; +const Header = (props: HeaderProps) => ( + + + + +); export default Header; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderState.tsx b/packages/ui-client/src/components/HeaderV2/HeaderState.tsx index a5f29d77b28e4..7c7f64fe86cbb 100644 --- a/packages/ui-client/src/components/HeaderV2/HeaderState.tsx +++ b/packages/ui-client/src/components/HeaderV2/HeaderState.tsx @@ -1,17 +1,17 @@ import { Icon, IconButton } from '@rocket.chat/fuselage'; import type { Keys as IconName } from '@rocket.chat/icons'; -import type { ComponentPropsWithoutRef, MouseEventHandler } from 'react'; +import type { AllHTMLAttributes, ComponentPropsWithoutRef, MouseEventHandler } from 'react'; type HeaderStateProps = - | (Omit, 'onClick'> & { + | (Pick, 'color' | 'title' | 'icon'> & { onClick: MouseEventHandler; - }) + } & Omit, 'is'>) | (Omit, 'name' | 'onClick'> & { icon: IconName; onClick?: undefined; }); const HeaderState = (props: HeaderStateProps) => - props.onClick ? : ; + props.onClick ? : ; export default HeaderState; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderTitle.tsx b/packages/ui-client/src/components/HeaderV2/HeaderTitle.tsx index 88ebadc2ca11b..82c1233a41184 100644 --- a/packages/ui-client/src/components/HeaderV2/HeaderTitle.tsx +++ b/packages/ui-client/src/components/HeaderV2/HeaderTitle.tsx @@ -3,6 +3,6 @@ import type { ComponentPropsWithoutRef } from 'react'; type HeaderTitleProps = ComponentPropsWithoutRef; -const HeaderTitle = (props: HeaderTitleProps) => ; +const HeaderTitle = (props: HeaderTitleProps) => ; export default HeaderTitle; diff --git a/packages/ui-client/src/components/RoomBanner/RoomBanner.stories.tsx b/packages/ui-client/src/components/RoomBanner/RoomBanner.stories.tsx deleted file mode 100644 index 90e14b18b3e2a..0000000000000 --- a/packages/ui-client/src/components/RoomBanner/RoomBanner.stories.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Avatar, Box, IconButton } from '@rocket.chat/fuselage'; -import { ComponentProps } from 'react'; - -import { RoomBanner } from './RoomBanner'; -import { RoomBannerContent } from './RoomBannerContent'; - -export default { - title: 'Components/RoomBanner', - component: RoomBanner, -}; -const avatarUrl = - 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAoACgDASIAAhEBAxEB/8QAGwAAAgIDAQAAAAAAAAAAAAAAAAcEBgIDBQj/xAAuEAACAQQAAwcEAQUAAAAAAAABAgMABAUREiExBhMUIkFRYQcWcYGhFTJSgpH/xAAYAQADAQEAAAAAAAAAAAAAAAACAwQBAP/EAB4RAAIBBQEBAQAAAAAAAAAAAAABAgMREiExE0HR/9oADAMBAAIRAxEAPwBuXuIkhBuMe5ib/AHQP49q4L3mLitryTLTSpOiHQI5k/HzXa/qbFOEudVTu1dumWvcTaNCZYZ7vU6g6LxqjOU/24dfs1Ouh9FnkMpd3Reeyx83hAxZZEhkdV9/MBrX71WGPvJcqrJBGveKATtuXXqNU0pu02bTHXD/AGvJAluyxxRd6F4x00o+NdKoVrjbzJdvVe1t5cVLc2ck8qjnohgpPtz2v7G6JtPQ2VJwjlcw+37mchpnK6GtIuv5NFWeTsLNPvxWTvpfjvOEfwKKzEVkSct2vscS/BIzSN0YRkeX81UpPqO8masJETu7OOccY4dswYFQeftv096XV5knuJGdm2T1+agvMXj8jEaHX905QihabvcbuS7X566mLWLwSY8PuRnk/u4eZ0deTl71Ef6hY+0yM88TzeNZY4luYwpVYyduOfrvhPTnr0pXSX9y5mCsyJMdyxxvwq599em+taItqCSNc90ChvZRUruUcT0JiO18Elpk7t8v41LWzacxkBSuvjQ/FFJayjDWrCTepAQ2vUH0oo/Jk3ovpwJJeVCP5CN+lFFaaMqy+nAyuChvrTI2kN9JAsi2ZOy4IBHMnkSCP+iqBexSWdxLazoUljJVlPUH2oorkV10pRc7b1zXb/hZOzuJvM86QWEXeELxOzHSIPcmiiiunVlF2RNTpRkrs//Z'; - -const CustomAvatar = (props: Omit, 'url'>) => ; - -export const Default = () => ( - - - Plain text long long long loooooooooooooong loooong loooong loooong loooong loooong loooong teeeeeeext - - - - - Will Bell - - - - -); - -export const WithoutTopic = () => ( - - - - Add topic - - - - - - Will Bell - - - - -); - -export const TopicAndAnnouncement = () => ( -
      - - - Topic long long long loooooooooooooong loooong loooong loooong loooong loooong loooong loooong loooong teeeeeeext - - - - - Will Bell - - - - - - - Announcement banner google.com - - -
      -); diff --git a/packages/ui-client/src/components/RoomBanner/RoomBanner.tsx b/packages/ui-client/src/components/RoomBanner/RoomBanner.tsx deleted file mode 100644 index 8b9bda9e40207..0000000000000 --- a/packages/ui-client/src/components/RoomBanner/RoomBanner.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { css } from '@rocket.chat/css-in-js'; -import { Box, Divider, Palette } from '@rocket.chat/fuselage'; -import { useLayout } from '@rocket.chat/ui-contexts'; -import { ComponentProps } from 'react'; - -const clickable = css` - cursor: pointer; - &:focus-visible { - outline: ${Palette.stroke['stroke-highlight']} solid 1px; - } -`; - -const style = css` - background-color: ${Palette.surface['surface-room']}; -`; - -export const RoomBanner = ({ onClick, className, ...props }: ComponentProps) => { - const { isMobile } = useLayout(); - - return ( - <> - - - - ); -}; diff --git a/packages/ui-client/src/components/RoomBanner/RoomBannerContent.tsx b/packages/ui-client/src/components/RoomBanner/RoomBannerContent.tsx deleted file mode 100644 index 36b5d74569eab..0000000000000 --- a/packages/ui-client/src/components/RoomBanner/RoomBannerContent.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { css } from '@rocket.chat/css-in-js'; -import { Box, Palette } from '@rocket.chat/fuselage'; -import { HTMLAttributes } from 'react'; - -export const RoomBannerContent = (props: Omit, 'is'>) => ( - -); diff --git a/packages/ui-client/src/components/RoomBanner/index.ts b/packages/ui-client/src/components/RoomBanner/index.ts deleted file mode 100644 index 9c79ab469b624..0000000000000 --- a/packages/ui-client/src/components/RoomBanner/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './RoomBanner'; -export * from './RoomBannerContent'; diff --git a/packages/ui-client/src/components/index.ts b/packages/ui-client/src/components/index.ts index 4b637518ebd5f..dd3d037772a46 100644 --- a/packages/ui-client/src/components/index.ts +++ b/packages/ui-client/src/components/index.ts @@ -11,6 +11,6 @@ export * from './Header'; export * from './HeaderV2'; export * from './MultiSelectCustom/MultiSelectCustom'; export * from './FeaturePreview'; -export * from './RoomBanner'; +export { default as AnnouncementBanner } from './AnnouncementBanner'; export { default as UserAutoComplete } from './UserAutoComplete'; export * from './GenericMenu'; diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index e0793b3d3dc04..69d178384750e 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -21,8 +21,8 @@ "@babel/core": "~7.26.0", "@react-aria/toolbar": "^3.0.0-nightly.5042", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "~0.61.0", - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/fuselage": "~0.62.0", + "@rocket.chat/icons": "^0.42.0", "@storybook/addon-actions": "^8.6.4", "@storybook/addon-docs": "^8.6.4", "@storybook/addon-essentials": "^8.6.4", diff --git a/packages/ui-contexts/CHANGELOG.md b/packages/ui-contexts/CHANGELOG.md index e7d5bf6f2500f..30dac1c163578 100644 --- a/packages/ui-contexts/CHANGELOG.md +++ b/packages/ui-contexts/CHANGELOG.md @@ -1,5 +1,114 @@ # @rocket.chat/ui-contexts +## 18.0.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/rest-typings@7.6.0-rc.8 + - @rocket.chat/ddp-client@0.3.21-rc.8 +
      + +## 18.0.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/rest-typings@7.6.0-rc.7 + - @rocket.chat/ddp-client@0.3.21-rc.7 +
      + +## 18.0.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/rest-typings@7.6.0-rc.6 + - @rocket.chat/ddp-client@0.3.21-rc.6 +
      + +## 18.0.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/rest-typings@7.6.0-rc.5 + - @rocket.chat/ddp-client@0.3.21-rc.5 +
      + +## 18.0.0-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/rest-typings@7.6.0-rc.4 + - @rocket.chat/ddp-client@0.3.21-rc.4 +
      + +## 18.0.0-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/rest-typings@7.6.0-rc.3 + - @rocket.chat/ddp-client@0.3.21-rc.3 +
      + +## 18.0.0-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/rest-typings@7.6.0-rc.2 + - @rocket.chat/ddp-client@0.3.21-rc.2 +
      + +## 18.0.0-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/rest-typings@7.6.0-rc.1 + - @rocket.chat/ddp-client@0.3.20-rc.1 +
      + +## 18.0.0-rc.0 + +### Minor Changes + +- ([#35218](https://github.com/RocketChat/Rocket.Chat/pull/35218)) Adds a new admin page to audit settings changes in a server + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +- ([#35807](https://github.com/RocketChat/Rocket.Chat/pull/35807)) Moves the room search functionality from the sidebar to the navbar and reorganize their relative actions + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it + +### Patch Changes + +-
      Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, f545617c2ac3d67af533e64c2670d8d564a56d15, bffc49f426259925c415651c2b2a58083dac547a, 1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4, cc344bea08c08501f50e9cee620b2926a322a4ee, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de, be67bb771294c337c28da5e61ae47ab4e32244d1, 895ea3fdbba1d0e3cf1bed03cb8d0abfcca5d351]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/i18n@1.6.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 + - @rocket.chat/ddp-client@0.3.20-rc.0 +
      + ## 17.0.1 ### Patch Changes diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 900cef021c4f6..2870a25e0eedc 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-contexts", - "version": "17.0.1", + "version": "18.0.0-rc.8", "private": true, "devDependencies": { "@rocket.chat/core-typings": "workspace:^", diff --git a/packages/ui-contexts/src/CustomSoundContext.ts b/packages/ui-contexts/src/CustomSoundContext.ts index 30a48b603d5d4..5c3838ba47c6a 100644 --- a/packages/ui-contexts/src/CustomSoundContext.ts +++ b/packages/ui-contexts/src/CustomSoundContext.ts @@ -2,17 +2,67 @@ import type { ICustomSound } from '@rocket.chat/core-typings'; import { createContext } from 'react'; export type CustomSoundContextValue = { - play: (sound: ICustomSound['_id'], options?: { volume?: number; loop?: boolean }) => Promise; + play: ( + soundId: string, + options?: + | { + volume?: number | undefined; + loop?: boolean | undefined; + } + | undefined, + ) => void; pause: (sound: ICustomSound['_id']) => void; stop: (sound: ICustomSound['_id']) => void; - getList: () => ICustomSound[] | undefined; - isPlaying: (sound: ICustomSound['_id']) => boolean | null; + callSounds: { + playRinger: () => void; + playDialer: () => void; + stopRinger: () => void; + stopDialer: () => void; + }; + voipSounds: { + playRinger: () => void; + playDialer: () => void; + playCallEnded: () => void; + stopRinger: () => void; + stopDialer: () => void; + stopCallEnded: () => void; + stopAll: () => void; + }; + notificationSounds: { + playNewRoom: () => void; + playNewMessage: () => void; + playNewMessageLoop: () => void; + stopNewRoom: () => void; + stopNewMessage: () => void; + }; + list: ICustomSound[]; }; export const CustomSoundContext = createContext({ play: () => new Promise(() => undefined), pause: () => undefined, stop: () => undefined, - getList: () => undefined, - isPlaying: () => false, + callSounds: { + playRinger: () => undefined, + playDialer: () => undefined, + stopRinger: () => undefined, + stopDialer: () => undefined, + }, + voipSounds: { + playRinger: () => undefined, + playDialer: () => undefined, + playCallEnded: () => undefined, + stopRinger: () => undefined, + stopDialer: () => undefined, + stopCallEnded: () => undefined, + stopAll: () => undefined, + }, + notificationSounds: { + playNewRoom: () => undefined, + playNewMessage: () => undefined, + playNewMessageLoop: () => undefined, + stopNewRoom: () => undefined, + stopNewMessage: () => undefined, + }, + list: [], }); diff --git a/packages/ui-contexts/src/LayoutContext.ts b/packages/ui-contexts/src/LayoutContext.ts index 2d900b5a7612e..3e4c9a665a864 100644 --- a/packages/ui-contexts/src/LayoutContext.ts +++ b/packages/ui-contexts/src/LayoutContext.ts @@ -8,6 +8,7 @@ export type SizeLayout = { export type LayoutContextValue = { isEmbedded: boolean; showTopNavbarEmbeddedLayout: boolean; + isTablet: boolean; isMobile: boolean; roomToolboxExpanded: boolean; sidebar: { @@ -17,6 +18,11 @@ export type LayoutContextValue = { expand: () => void; close: () => void; }; + navbar: { + searchExpanded: boolean; + expandSearch?: () => void; + collapseSearch?: () => void; + }; size: SizeLayout; contextualBarExpanded: boolean; contextualBarPosition: 'absolute' | 'relative' | 'fixed'; @@ -31,8 +37,14 @@ export type LayoutContextValue = { export const LayoutContext = createContext({ isEmbedded: false, showTopNavbarEmbeddedLayout: false, + isTablet: false, isMobile: false, roomToolboxExpanded: true, + navbar: { + searchExpanded: false, + expandSearch: () => undefined, + collapseSearch: () => undefined, + }, sidebar: { isCollapsed: false, toggle: () => undefined, diff --git a/packages/ui-contexts/src/ServerContext/ServerContext.ts b/packages/ui-contexts/src/ServerContext/ServerContext.ts index e5b7fd63c1c5d..c4ae8c3ebcde3 100644 --- a/packages/ui-contexts/src/ServerContext/ServerContext.ts +++ b/packages/ui-contexts/src/ServerContext/ServerContext.ts @@ -44,6 +44,8 @@ export type ServerContextValue = { retransmitToSelf?: boolean | undefined; }, ) => (eventName: K, callback: (...args: StreamerCallbackArgs) => void) => () => void; + disconnect: () => void; + reconnect: () => void; }; export const ServerContext = createContext({ @@ -56,4 +58,10 @@ export const ServerContext = createContext({ throw new Error('not implemented'); }, getStream: () => () => (): void => undefined, + disconnect: () => { + throw new Error('not implemented'); + }, + reconnect: () => { + throw new Error('not implemented'); + }, }); diff --git a/packages/ui-contexts/src/SettingsContext.ts b/packages/ui-contexts/src/SettingsContext.ts index f0a005344d468..95e364de2c748 100644 --- a/packages/ui-contexts/src/SettingsContext.ts +++ b/packages/ui-contexts/src/SettingsContext.ts @@ -6,11 +6,12 @@ export type SettingsContextQuery = { readonly group?: ISetting['_id']; readonly section?: string; readonly tab?: ISetting['_id']; + readonly skip?: number; + readonly limit?: number; }; export type SettingsContextValue = { readonly hasPrivateAccess: boolean; - readonly isLoading: boolean; readonly querySetting: ( _id: ISetting['_id'], ) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => ISetting | undefined]; @@ -22,7 +23,6 @@ export type SettingsContextValue = { export const SettingsContext = createContext({ hasPrivateAccess: false, - isLoading: false, querySetting: () => [(): (() => void) => (): void => undefined, (): undefined => undefined], querySettings: () => [(): (() => void) => (): void => undefined, (): ISetting[] => []], dispatch: async () => undefined, diff --git a/packages/ui-contexts/src/UserPresenceContext.ts b/packages/ui-contexts/src/UserPresenceContext.ts new file mode 100644 index 0000000000000..58ccfd6d676e3 --- /dev/null +++ b/packages/ui-contexts/src/UserPresenceContext.ts @@ -0,0 +1,10 @@ +import type { Subscribable, UserPresence } from '@rocket.chat/core-typings'; +import { createContext } from 'react'; + +export type UserPresenceContextValue = { + queryUserData: (uid: string | undefined) => Subscribable; +}; + +export const UserPresenceContext = createContext({ + queryUserData: () => ({ get: () => undefined, subscribe: () => () => undefined }), +}); diff --git a/packages/ui-contexts/src/hooks/useIsSettingsContextLoading.ts b/packages/ui-contexts/src/hooks/useIsSettingsContextLoading.ts deleted file mode 100644 index fe5155e5abbfb..0000000000000 --- a/packages/ui-contexts/src/hooks/useIsSettingsContextLoading.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { useContext } from 'react'; - -import { SettingsContext } from '../SettingsContext'; - -export const useIsSettingsContextLoading = (): boolean => useContext(SettingsContext).isLoading; diff --git a/apps/meteor/client/hooks/useUserData.ts b/packages/ui-contexts/src/hooks/useUserPresence.ts similarity index 55% rename from apps/meteor/client/hooks/useUserData.ts rename to packages/ui-contexts/src/hooks/useUserPresence.ts index acac1fc4f553b..8a3857e858a5f 100644 --- a/apps/meteor/client/hooks/useUserData.ts +++ b/packages/ui-contexts/src/hooks/useUserPresence.ts @@ -1,16 +1,16 @@ +import type { UserPresence } from '@rocket.chat/core-typings'; import { useContext, useMemo, useSyncExternalStore } from 'react'; -import { UserPresenceContext } from '../contexts/UserPresenceContext'; -import type { UserPresence } from '../lib/presence'; +import { UserPresenceContext } from '../UserPresenceContext'; /** - * Hook to fetch and subscribe users data + * Hook to fetch and subscribe user presence data * * @param uid - User Id - * @returns Users data: status, statusText, username, name + * @returns status, statusText, username, name * @public */ -export const useUserData = (uid: string): UserPresence | undefined => { +export const useUserPresence = (uid: string | undefined): UserPresence | undefined => { const userPresence = useContext(UserPresenceContext); const { subscribe, get } = useMemo( diff --git a/packages/ui-contexts/src/index.ts b/packages/ui-contexts/src/index.ts index 313710bbfd4ef..314ce5bc372d5 100644 --- a/packages/ui-contexts/src/index.ts +++ b/packages/ui-contexts/src/index.ts @@ -14,6 +14,7 @@ export { ToastMessagesContext, ToastMessagesContextValue } from './ToastMessages export { TooltipContext, TooltipContextValue } from './TooltipContext'; export { TranslationContext, TranslationContextValue } from './TranslationContext'; export { UserContext, UserContextValue } from './UserContext'; +export { UserPresenceContext, UserPresenceContextValue } from './UserPresenceContext'; export { DeviceContext, Device, DeviceContextValue } from './DeviceContext'; export { ActionManagerContext, IActionManager } from './ActionManagerContext'; @@ -33,7 +34,6 @@ export { useEndpoint } from './hooks/useEndpoint'; export { useGoToRoom } from './hooks/useGoToRoom'; export type { EndpointFunction } from './hooks/useEndpoint'; export { useIsPrivilegedSettingsContext } from './hooks/useIsPrivilegedSettingsContext'; -export { useIsSettingsContextLoading } from './hooks/useIsSettingsContextLoading'; export { useLanguage } from './hooks/useLanguage'; export { useLanguages } from './hooks/useLanguages'; export { useLayout } from './hooks/useLayout'; @@ -91,6 +91,7 @@ export { useIsDeviceManagementEnabled } from './hooks/useIsDeviceManagementEnabl export { useSetOutputMediaDevice } from './hooks/useSetOutputMediaDevice'; export { useSetInputMediaDevice } from './hooks/useSetInputMediaDevice'; export { useAccountsCustomFields } from './hooks/useAccountsCustomFields'; +export { useUserPresence } from './hooks/useUserPresence'; export { UploadResult } from './ServerContext'; export { TranslationKey, TranslationLanguage } from './TranslationContext'; diff --git a/packages/ui-kit/package.json b/packages/ui-kit/package.json index 31e62196caec8..44248c15e4f75 100644 --- a/packages/ui-kit/package.json +++ b/packages/ui-kit/package.json @@ -40,7 +40,7 @@ "@babel/plugin-transform-runtime": "~7.25.9", "@babel/preset-env": "~7.26.0", "@rocket.chat/eslint-config": "workspace:~", - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", "babel-loader": "~9.2.1", diff --git a/packages/ui-video-conf/CHANGELOG.md b/packages/ui-video-conf/CHANGELOG.md index ae0b9a1ce69e0..ebecd4f7bb7f5 100644 --- a/packages/ui-video-conf/CHANGELOG.md +++ b/packages/ui-video-conf/CHANGELOG.md @@ -1,5 +1,95 @@ # @rocket.chat/ui-video-conf +## 18.0.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.8 + - @rocket.chat/ui-avatar@14.0.0-rc.8 +
      + +## 18.0.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.7 + - @rocket.chat/ui-avatar@14.0.0-rc.7 +
      + +## 18.0.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.6 + - @rocket.chat/ui-avatar@14.0.0-rc.6 +
      + +## 18.0.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.5 + - @rocket.chat/ui-avatar@14.0.0-rc.5 +
      + +## 18.0.0-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.4 + - @rocket.chat/ui-avatar@14.0.0-rc.4 +
      + +## 18.0.0-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.3 + - @rocket.chat/ui-avatar@14.0.0-rc.3 +
      + +## 18.0.0-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.2 + - @rocket.chat/ui-avatar@14.0.0-rc.2 +
      + +## 18.0.0-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.1 + - @rocket.chat/ui-avatar@14.0.0-rc.1 +
      + +## 18.0.0-rc.0 + +### Patch Changes + +-
      Updated dependencies [1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de]: + + - @rocket.chat/ui-contexts@18.0.0-rc.0 + - @rocket.chat/ui-avatar@14.0.0-rc.0 +
      + ## 17.0.1 ### Patch Changes diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 7c3113afe62d4..7411e43d6bd0e 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-video-conf", - "version": "17.0.1", + "version": "18.0.0-rc.8", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", @@ -24,9 +24,9 @@ "@babel/core": "~7.26.0", "@rocket.chat/css-in-js": "~0.31.25", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "~0.61.0", + "@rocket.chat/fuselage": "~0.62.0", "@rocket.chat/fuselage-hooks": "~0.35.0", - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/styled": "~0.32.0", "@rocket.chat/ui-avatar": "workspace:^", @@ -58,8 +58,8 @@ "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-avatar": "13.0.1", - "@rocket.chat/ui-contexts": "17.0.1", + "@rocket.chat/ui-avatar": "workspace:^", + "@rocket.chat/ui-contexts": "workspace:^", "react": "~17.0.2", "react-dom": "^17.0.2" }, diff --git a/packages/ui-voip/CHANGELOG.md b/packages/ui-voip/CHANGELOG.md index e46ccaccd1c8d..ea17859e5c493 100644 --- a/packages/ui-voip/CHANGELOG.md +++ b/packages/ui-voip/CHANGELOG.md @@ -1,5 +1,112 @@ # @rocket.chat/ui-voip +## 8.0.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.8 + - @rocket.chat/ui-avatar@14.0.0-rc.8 + - @rocket.chat/ui-client@18.0.0-rc.8 +
      + +## 8.0.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.7 + - @rocket.chat/ui-avatar@14.0.0-rc.7 + - @rocket.chat/ui-client@18.0.0-rc.7 +
      + +## 8.0.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.6 + - @rocket.chat/ui-avatar@14.0.0-rc.6 + - @rocket.chat/ui-client@18.0.0-rc.6 +
      + +## 8.0.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.5 + - @rocket.chat/ui-avatar@14.0.0-rc.5 + - @rocket.chat/ui-client@18.0.0-rc.5 +
      + +## 8.0.0-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.4 + - @rocket.chat/ui-avatar@14.0.0-rc.4 + - @rocket.chat/ui-client@18.0.0-rc.4 +
      + +## 8.0.0-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.3 + - @rocket.chat/ui-avatar@14.0.0-rc.3 + - @rocket.chat/ui-client@18.0.0-rc.3 +
      + +## 8.0.0-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.2 + - @rocket.chat/ui-avatar@14.0.0-rc.2 + - @rocket.chat/ui-client@18.0.0-rc.2 +
      + +## 8.0.0-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.1 + - @rocket.chat/ui-avatar@14.0.0-rc.1 + - @rocket.chat/ui-client@18.0.0-rc.1 +
      + +## 8.0.0-rc.0 + +### Minor Changes + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +### Patch Changes + +- ([#35765](https://github.com/RocketChat/Rocket.Chat/pull/35765)) Fixes an issue causing VoIP calls to no longer reach the client after a temporary disconnection + +- ([#35832](https://github.com/RocketChat/Rocket.Chat/pull/35832)) Fixes an issue where Voice Calls were unable to gather Ice Servers + +-
      Updated dependencies [f545617c2ac3d67af533e64c2670d8d564a56d15, 6bf386dcc2a560963cf719fbc2d96569ce23a2de, 1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, 5e3ab1a07163cd22ad4c41502ef232845d26bdc2, 72725d391e79b44e7380ee2fe640e2e4426c77ca, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de]: + + - @rocket.chat/ui-client@18.0.0-rc.0 + - @rocket.chat/ui-contexts@18.0.0-rc.0 + - @rocket.chat/ui-avatar@14.0.0-rc.0 +
      + ## 7.0.1 ### Patch Changes diff --git a/packages/ui-voip/package.json b/packages/ui-voip/package.json index 1a8a1e902970f..db15ccef619e8 100644 --- a/packages/ui-voip/package.json +++ b/packages/ui-voip/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-voip", - "version": "7.0.1", + "version": "8.0.0-rc.8", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", @@ -29,9 +29,9 @@ "@react-spectrum/test-utils": "~1.0.0-alpha.2", "@rocket.chat/css-in-js": "~0.31.25", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "~0.61.0", + "@rocket.chat/fuselage": "~0.62.0", "@rocket.chat/fuselage-hooks": "~0.35.0", - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/mock-providers": "workspace:~", "@rocket.chat/styled": "~0.32.0", @@ -67,9 +67,9 @@ "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-avatar": "13.0.1", - "@rocket.chat/ui-client": "17.0.1", - "@rocket.chat/ui-contexts": "17.0.1", + "@rocket.chat/ui-avatar": "workspace:^", + "@rocket.chat/ui-client": "workspace:^", + "@rocket.chat/ui-contexts": "workspace:^", "react": "~17.0.2", "react-aria": "~3.23.1", "react-dom": "^17.0.2" diff --git a/packages/ui-voip/src/hooks/useWebRtcServers.ts b/packages/ui-voip/src/hooks/useIceServers.ts similarity index 77% rename from packages/ui-voip/src/hooks/useWebRtcServers.ts rename to packages/ui-voip/src/hooks/useIceServers.ts index 9753098c0a659..1c32343a9246a 100644 --- a/packages/ui-voip/src/hooks/useWebRtcServers.ts +++ b/packages/ui-voip/src/hooks/useIceServers.ts @@ -4,8 +4,8 @@ import { useMemo } from 'react'; import type { IceServer } from '../definitions'; import { parseStringToIceServers } from '../utils/parseStringToIceServers'; -export const useWebRtcServers = (): IceServer[] => { - const servers = useSetting('WebRTC_Servers'); +export const useIceServers = (): IceServer[] => { + const servers = useSetting('VoIP_TeamCollab_Ice_Servers'); return useMemo(() => { if (typeof servers !== 'string' || !servers.trim()) { diff --git a/packages/ui-voip/src/hooks/useVoipClient.tsx b/packages/ui-voip/src/hooks/useVoipClient.tsx index 589f76d7952c0..fdda0587b7d52 100644 --- a/packages/ui-voip/src/hooks/useVoipClient.tsx +++ b/packages/ui-voip/src/hooks/useVoipClient.tsx @@ -1,8 +1,8 @@ -import { useUser, useEndpoint } from '@rocket.chat/ui-contexts'; +import { useUser, useEndpoint, useSetting } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import { useEffect, useRef } from 'react'; -import { useWebRtcServers } from './useWebRtcServers'; +import { useIceServers } from './useIceServers'; import VoipClient from '../lib/VoipClient'; type VoipClientParams = { @@ -20,8 +20,9 @@ export const useVoipClient = ({ enabled = true, autoRegister = true }: VoipClien const voipClientRef = useRef(null); const getRegistrationInfo = useEndpoint('GET', '/v1/voip-freeswitch.extension.getRegistrationInfoByUserId'); + const iceGatheringTimeout = useSetting('VoIP_TeamCollab_Ice_Gathering_Timeout', 5000); - const iceServers = useWebRtcServers(); + const iceServers = useIceServers(); const { data: voipClient, error } = useQuery({ queryKey: ['voip-client', enabled, userId, iceServers], @@ -59,6 +60,7 @@ export const useVoipClient = ({ enabled = true, autoRegister = true }: VoipClien webSocketURI: websocketPath, connectionRetryCount: Number(10), // TODO: get from settings enableKeepAliveUsingOptionsForUnstableNetworks: true, // TODO: get from settings + iceGatheringTimeout, }; const voipClient = await VoipClient.create(config); diff --git a/packages/ui-voip/src/hooks/useVoipSounds.ts b/packages/ui-voip/src/hooks/useVoipSounds.ts deleted file mode 100644 index f08ff00e3da5a..0000000000000 --- a/packages/ui-voip/src/hooks/useVoipSounds.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useCustomSound, useUserPreference } from '@rocket.chat/ui-contexts'; -import { useMemo } from 'react'; - -type VoipSound = 'telephone' | 'outbound-call-ringing' | 'call-ended'; - -export const useVoipSounds = () => { - const { play, pause } = useCustomSound(); - const masterVolume = useUserPreference('masterVolume', 100) || 100; - const voipRingerVolume = useUserPreference('voipRingerVolume', 100) || 100; - const audioVolume = Math.floor((voipRingerVolume * masterVolume) / 100); - - return useMemo( - () => ({ - play: (soundId: VoipSound, loop = true) => { - play(soundId, { - volume: Number((audioVolume / 100).toPrecision(2)), - loop, - }); - }, - stop: (soundId: VoipSound) => pause(soundId), - stopAll: () => { - pause('telephone'); - pause('outbound-call-ringing'); - }, - }), - [play, pause, audioVolume], - ); -}; diff --git a/packages/ui-voip/src/hooks/useVoipState.tsx b/packages/ui-voip/src/hooks/useVoipState.tsx index 66e1ea7538a06..b55f679dcef42 100644 --- a/packages/ui-voip/src/hooks/useVoipState.tsx +++ b/packages/ui-voip/src/hooks/useVoipState.tsx @@ -1,7 +1,7 @@ import { useContext, useMemo } from 'react'; -import { VoipContext } from '../contexts/VoipContext'; import { useVoipEffect } from './useVoipEffect'; +import { VoipContext } from '../contexts/VoipContext'; export type VoipState = { isEnabled: boolean; @@ -15,6 +15,7 @@ export type VoipState = { isError: boolean; error?: Error | null; clientError?: Error | null; + isReconnecting: boolean; }; const DEFAULT_STATE = { @@ -26,6 +27,7 @@ const DEFAULT_STATE = { isOngoing: false, isOutgoing: false, isError: false, + isReconnecting: false, }; export const useVoipState = (): VoipState => { diff --git a/packages/ui-voip/src/lib/VoipClient.ts b/packages/ui-voip/src/lib/VoipClient.ts index 7f8aa0e5e5393..7ab1fe3906eea 100644 --- a/packages/ui-voip/src/lib/VoipClient.ts +++ b/packages/ui-voip/src/lib/VoipClient.ts @@ -46,6 +46,8 @@ class VoipClient extends Emitter { private contactInfo: ContactInfo | null = null; + private reconnecting = false; + constructor(private readonly config: VoIPUserConfiguration) { super(); @@ -53,7 +55,7 @@ class VoipClient extends Emitter { } public async init() { - const { authPassword, authUserName, sipRegistrarHostnameOrIP, iceServers, webSocketURI } = this.config; + const { authPassword, authUserName, sipRegistrarHostnameOrIP, iceServers, webSocketURI, iceGatheringTimeout } = this.config; const transportOptions = { server: webSocketURI, @@ -62,10 +64,13 @@ class VoipClient extends Emitter { }; const sdpFactoryOptions = { - iceGatheringTimeout: 10, + ...(typeof iceGatheringTimeout === 'number' && { iceGatheringTimeout }), peerConnectionConfiguration: { iceServers }, }; + const searchParams = new URLSearchParams(window.location.search); + const debug = Boolean(searchParams.get('debug') || searchParams.get('debug-voip')); + this.userAgent = new UserAgent({ authorizationPassword: authPassword, authorizationUsername: authUserName, @@ -73,7 +78,7 @@ class VoipClient extends Emitter { transportOptions, sessionDescriptionHandlerFactoryOptions: sdpFactoryOptions, logConfiguration: false, - logLevel: 'error', + logLevel: debug ? 'debug' : 'error', delegate: { onInvite: this.onIncomingCall, onRefer: this.onTransferedCall, @@ -397,9 +402,17 @@ class VoipClient extends Emitter { } if (connectionRetryCount !== -1 && reconnectionAttempt > connectionRetryCount) { + console.error('VoIP reconnection limit reached.'); + this.reconnecting = false; + this.emit('stateChanged'); return; } + if (!this.reconnecting) { + this.reconnecting = true; + this.emit('stateChanged'); + } + const reconnectionDelay = Math.pow(2, reconnectionAttempt % 4); console.error(`Attempting to reconnect with backoff due to network loss. Backoff time [${reconnectionDelay}]`); @@ -518,6 +531,10 @@ class VoipClient extends Emitter { return !!this.error; } + public isReconnecting(): boolean { + return this.reconnecting; + } + public isOnline(): boolean { return this.online; } @@ -601,6 +618,7 @@ class VoipClient extends Emitter { isOutgoing: this.isOutgoing(), isInCall: this.isInCall(), isError: this.isError(), + isReconnecting: this.isReconnecting(), }; } @@ -757,15 +775,51 @@ class VoipClient extends Emitter { } private onUserAgentConnected = (): void => { + console.log('VoIP user agent connected.'); + + const wasReconnecting = this.reconnecting; + + this.reconnecting = false; this.networkEmitter.emit('connected'); this.emit('stateChanged'); + + if (!this.isReady() || !wasReconnecting) { + return; + } + + this.register() + .then(() => { + this.emit('stateChanged'); + }) + .catch((error?: any) => { + console.error('VoIP failed to register after user agent connection.'); + if (error) { + console.error(error); + } + }); }; private onUserAgentDisconnected = (error: any): void => { + console.log('VoIP user agent disconnected.'); + + this.reconnecting = !!error; this.networkEmitter.emit('disconnected'); this.emit('stateChanged'); if (error) { + if (this.isRegistered()) { + this.unregister() + .then(() => { + this.emit('stateChanged'); + }) + .catch((error?: any) => { + console.error('VoIP failed to unregister after user agent disconnection.'); + if (error) { + console.error(error); + } + }); + } + this.networkEmitter.emit('connectionerror', error); this.attemptReconnection(); } diff --git a/packages/ui-voip/src/providers/VoipProvider.tsx b/packages/ui-voip/src/providers/VoipProvider.tsx index d6d302fffd0e5..f752162999fe6 100644 --- a/packages/ui-voip/src/providers/VoipProvider.tsx +++ b/packages/ui-voip/src/providers/VoipProvider.tsx @@ -1,6 +1,7 @@ import { useEffectEvent, useLocalStorage } from '@rocket.chat/fuselage-hooks'; import type { Device } from '@rocket.chat/ui-contexts'; import { + useCustomSound, usePermission, useSetInputMediaDevice, useSetOutputMediaDevice, @@ -17,7 +18,6 @@ import VoipPopupPortal from '../components/VoipPopupPortal'; import type { VoipContextValue } from '../contexts/VoipContext'; import { VoipContext } from '../contexts/VoipContext'; import { useVoipClient } from '../hooks/useVoipClient'; -import { useVoipSounds } from '../hooks/useVoipSounds'; const VoipProvider = ({ children }: { children: ReactNode }) => { // Settings @@ -29,7 +29,7 @@ const VoipProvider = ({ children }: { children: ReactNode }) => { // Hooks const { t } = useTranslation(); - const voipSounds = useVoipSounds(); + const { voipSounds } = useCustomSound(); const { voipClient, error } = useVoipClient({ enabled: isVoipEnabled, autoRegister: isLocalRegistered, @@ -57,7 +57,8 @@ const VoipProvider = ({ children }: { children: ReactNode }) => { }; const onCallEstablished = async (): Promise => { - voipSounds.stopAll(); + voipSounds.stopDialer(); + voipSounds.stopRinger(); window.addEventListener('beforeunload', onBeforeUnload); }; @@ -68,16 +69,17 @@ const VoipProvider = ({ children }: { children: ReactNode }) => { }; const onOutgoingCallRinging = (): void => { - voipSounds.play('outbound-call-ringing'); + voipSounds.playDialer(); }; const onIncomingCallRinging = (): void => { - voipSounds.play('telephone'); + voipSounds.playRinger(); }; const onCallTerminated = (): void => { - voipSounds.play('call-ended', false); - voipSounds.stopAll(); + voipSounds.playCallEnded(); + voipSounds.stopDialer(); + voipSounds.stopRinger(); window.removeEventListener('beforeunload', onBeforeUnload); }; @@ -106,6 +108,7 @@ const VoipProvider = ({ children }: { children: ReactNode }) => { voipClient.networkEmitter.on('localnetworkoffline', onNetworkDisconnected); return (): void => { + voipSounds.stopCallEnded(); voipClient.off('incomingcall', onIncomingCallRinging); voipClient.off('outgoingcall', onOutgoingCallRinging); voipClient.off('callestablished', onCallEstablished); diff --git a/packages/web-ui-registration/CHANGELOG.md b/packages/web-ui-registration/CHANGELOG.md index 30346459d9e51..822f0f45f6c46 100644 --- a/packages/web-ui-registration/CHANGELOG.md +++ b/packages/web-ui-registration/CHANGELOG.md @@ -1,5 +1,86 @@ # @rocket.chat/web-ui-registration +## 18.0.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.8 +
      + +## 18.0.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.7 +
      + +## 18.0.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.6 +
      + +## 18.0.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.5 +
      + +## 18.0.0-rc.4 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.4 +
      + +## 18.0.0-rc.3 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.3 +
      + +## 18.0.0-rc.2 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.2 +
      + +## 18.0.0-rc.1 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.1 +
      + +## 18.0.0-rc.0 + +### Patch Changes + +-
      Updated dependencies [1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de]: + + - @rocket.chat/ui-contexts@18.0.0-rc.0 +
      + ## 17.0.1 ### Patch Changes diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index fcab236a3f302..c81da6286b533 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/web-ui-registration", - "version": "17.0.1", + "version": "18.0.0-rc.8", "private": true, "homepage": "https://rocket.chat", "main": "./dist/index.js", @@ -50,7 +50,7 @@ "peerDependencies": { "@rocket.chat/layout": "*", "@rocket.chat/tools": "0.2.2", - "@rocket.chat/ui-contexts": "17.0.1", + "@rocket.chat/ui-contexts": "workspace:^", "@tanstack/react-query": "*", "react": "*", "react-hook-form": "*", diff --git a/turbo.json b/turbo.json index 3211478850242..e47124a557988 100644 --- a/turbo.json +++ b/turbo.json @@ -46,6 +46,7 @@ }, "@rocket.chat/meteor#build:ci": { "dependsOn": ["^build"], + "env": ["BABEL_ENV"], "cache": false } } diff --git a/yarn.lock b/yarn.lock index 28b8bde99d513..3c66c7da3b9c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2727,13 +2727,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/aix-ppc64@npm:0.24.2" - conditions: os=aix & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/aix-ppc64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/aix-ppc64@npm:0.25.0" @@ -2748,13 +2741,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/android-arm64@npm:0.24.2" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/android-arm64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/android-arm64@npm:0.25.0" @@ -2769,13 +2755,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/android-arm@npm:0.24.2" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - "@esbuild/android-arm@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/android-arm@npm:0.25.0" @@ -2790,13 +2769,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/android-x64@npm:0.24.2" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - "@esbuild/android-x64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/android-x64@npm:0.25.0" @@ -2811,13 +2783,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/darwin-arm64@npm:0.24.2" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/darwin-arm64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/darwin-arm64@npm:0.25.0" @@ -2832,13 +2797,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/darwin-x64@npm:0.24.2" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@esbuild/darwin-x64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/darwin-x64@npm:0.25.0" @@ -2853,13 +2811,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/freebsd-arm64@npm:0.24.2" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/freebsd-arm64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/freebsd-arm64@npm:0.25.0" @@ -2874,13 +2825,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/freebsd-x64@npm:0.24.2" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/freebsd-x64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/freebsd-x64@npm:0.25.0" @@ -2895,13 +2839,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-arm64@npm:0.24.2" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/linux-arm64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/linux-arm64@npm:0.25.0" @@ -2916,13 +2853,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-arm@npm:0.24.2" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@esbuild/linux-arm@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/linux-arm@npm:0.25.0" @@ -2937,13 +2867,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-ia32@npm:0.24.2" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/linux-ia32@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/linux-ia32@npm:0.25.0" @@ -2958,13 +2881,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-loong64@npm:0.24.2" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - "@esbuild/linux-loong64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/linux-loong64@npm:0.25.0" @@ -2979,13 +2895,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-mips64el@npm:0.24.2" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - "@esbuild/linux-mips64el@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/linux-mips64el@npm:0.25.0" @@ -3000,13 +2909,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-ppc64@npm:0.24.2" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/linux-ppc64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/linux-ppc64@npm:0.25.0" @@ -3021,13 +2923,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-riscv64@npm:0.24.2" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - "@esbuild/linux-riscv64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/linux-riscv64@npm:0.25.0" @@ -3042,13 +2937,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-s390x@npm:0.24.2" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - "@esbuild/linux-s390x@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/linux-s390x@npm:0.25.0" @@ -3063,13 +2951,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-x64@npm:0.24.2" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - "@esbuild/linux-x64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/linux-x64@npm:0.25.0" @@ -3084,13 +2965,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/netbsd-arm64@npm:0.24.2" - conditions: os=netbsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/netbsd-arm64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/netbsd-arm64@npm:0.25.0" @@ -3105,13 +2979,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/netbsd-x64@npm:0.24.2" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/netbsd-x64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/netbsd-x64@npm:0.25.0" @@ -3126,13 +2993,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/openbsd-arm64@npm:0.24.2" - conditions: os=openbsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/openbsd-arm64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/openbsd-arm64@npm:0.25.0" @@ -3147,13 +3007,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/openbsd-x64@npm:0.24.2" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/openbsd-x64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/openbsd-x64@npm:0.25.0" @@ -3168,13 +3021,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/sunos-x64@npm:0.24.2" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - "@esbuild/sunos-x64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/sunos-x64@npm:0.25.0" @@ -3189,13 +3035,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/win32-arm64@npm:0.24.2" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/win32-arm64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/win32-arm64@npm:0.25.0" @@ -3210,13 +3049,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/win32-ia32@npm:0.24.2" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/win32-ia32@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/win32-ia32@npm:0.25.0" @@ -3231,13 +3063,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/win32-x64@npm:0.24.2" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@esbuild/win32-x64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/win32-x64@npm:0.25.0" @@ -7919,7 +7744,7 @@ __metadata: "@rocket.chat/tracing": "workspace:^" "@types/bcrypt": "npm:^5.0.2" "@types/gc-stats": "npm:^1.4.3" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/polka": "npm:^0.5.7" bcrypt: "npm:^5.1.1" ejson: "npm:^2.2.3" @@ -7990,12 +7815,13 @@ __metadata: dependencies: "@msgpack/msgpack": "npm:3.0.0-beta2" "@rocket.chat/eslint-config": "workspace:~" + "@rocket.chat/tools": "workspace:^" "@rocket.chat/ui-kit": "workspace:~" "@types/adm-zip": "npm:^0.5.6" "@types/debug": "npm:^4.1.12" "@types/lodash.clonedeep": "npm:^4.5.9" "@types/nedb": "npm:^1.8.16" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/semver": "npm:^7.5.8" "@types/stack-trace": "npm:0.0.33" "@types/uuid": "npm:~10.0.0" @@ -8053,7 +7879,7 @@ __metadata: "@rocket.chat/string-helpers": "npm:~0.31.25" "@rocket.chat/tracing": "workspace:^" "@types/gc-stats": "npm:^1.4.3" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/polka": "npm:^0.5.7" ejson: "npm:^2.2.3" eslint: "npm:~8.45.0" @@ -8108,7 +7934,7 @@ __metadata: "@rocket.chat/apps-engine": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/models": "workspace:^" @@ -8131,7 +7957,7 @@ __metadata: dependencies: "@rocket.chat/apps-engine": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/ui-kit": "workspace:~" "@types/express": "npm:^4.17.21" @@ -8218,7 +8044,7 @@ __metadata: "@rocket.chat/tracing": "workspace:^" "@types/ejson": "npm:^2.2.2" "@types/gc-stats": "npm:^1.4.3" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/polka": "npm:^0.5.7" "@types/underscore": "npm:^1.13.0" "@types/uuid": "npm:^10.0.0" @@ -8357,11 +8183,11 @@ __metadata: "@rocket.chat/apps-engine": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": "npm:~0.61.0" + "@rocket.chat/fuselage": "npm:~0.62.0" "@rocket.chat/fuselage-hooks": "npm:~0.35.0" "@rocket.chat/fuselage-polyfills": "npm:~0.31.25" "@rocket.chat/gazzodown": "workspace:^" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/mock-providers": "workspace:^" "@rocket.chat/prettier-config": "npm:~0.31.25" @@ -8398,7 +8224,7 @@ __metadata: storybook-dark-mode: "npm:^4.0.2" typescript: "npm:~5.7.2" peerDependencies: - "@rocket.chat/apps-engine": 1.50.0 + "@rocket.chat/apps-engine": 1.51.0-rc.0 "@rocket.chat/eslint-config": 0.7.0 "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" @@ -8406,19 +8232,19 @@ __metadata: "@rocket.chat/icons": "*" "@rocket.chat/prettier-config": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 13.0.0 - "@rocket.chat/ui-contexts": 17.0.0 - "@rocket.chat/ui-kit": 0.37.0 - "@rocket.chat/ui-video-conf": 17.0.0 + "@rocket.chat/ui-avatar": "workspace:^" + "@rocket.chat/ui-contexts": "workspace:^" + "@rocket.chat/ui-kit": "workspace:^" + "@rocket.chat/ui-video-conf": "workspace:^" "@tanstack/react-query": "*" react: "*" react-dom: "*" languageName: unknown linkType: soft -"@rocket.chat/fuselage@npm:~0.61.0": - version: 0.61.0 - resolution: "@rocket.chat/fuselage@npm:0.61.0" +"@rocket.chat/fuselage@npm:~0.62.0": + version: 0.62.0 + resolution: "@rocket.chat/fuselage@npm:0.62.0" dependencies: "@rocket.chat/css-in-js": "npm:^0.31.25" "@rocket.chat/css-supports": "npm:^0.31.25" @@ -8436,7 +8262,7 @@ __metadata: react: "*" react-dom: "*" react-virtuoso: 1.2.4 - checksum: 10/11e391d9fd8191e9e62073629430062365c13a85e406db417a2f06c24589176b83a23ec2abdbd80d5ea51bd8da87d011a9769362104ecc87f20c5eb15df072c3 + checksum: 10/493225ab219172fec6061735dab0e07f265adb991af0e66713a9771089cb917a472bd0147d4f4d48b6950002aef9e562c298288babd6118bc7083cd2681d99fe languageName: node linkType: hard @@ -8447,7 +8273,7 @@ __metadata: "@babel/core": "npm:~7.26.0" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": "npm:~0.31.25" - "@rocket.chat/fuselage": "npm:~0.61.0" + "@rocket.chat/fuselage": "npm:~0.62.0" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/message-parser": "workspace:^" @@ -8496,8 +8322,8 @@ __metadata: "@rocket.chat/fuselage-tokens": "*" "@rocket.chat/message-parser": 0.31.32 "@rocket.chat/styled": "*" - "@rocket.chat/ui-client": 17.0.0 - "@rocket.chat/ui-contexts": 17.0.0 + "@rocket.chat/ui-client": "workspace:^" + "@rocket.chat/ui-contexts": "workspace:^" katex: "*" react: "*" languageName: unknown @@ -8514,10 +8340,10 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/icons@npm:^0.40.0": - version: 0.40.0 - resolution: "@rocket.chat/icons@npm:0.40.0" - checksum: 10/9fb696db75919f3f0bacdb4637e226e0bb078ca620c1130ccb3b113144b140d040205dd232c69d0c951d5b26488b5d9295bedcbbe69c006feb1cc53eea370614 +"@rocket.chat/icons@npm:^0.42.0": + version: 0.42.0 + resolution: "@rocket.chat/icons@npm:0.42.0" + checksum: 10/0842e471fb0b6cc943421e2e728f489c3cc4d02d38add8503bda4e1905b3fa4f36c4de1d5589142074a79bb0b987faaf79b2b537ca5b8cd51f6c1f89e73e3aa5 languageName: node linkType: hard @@ -8755,7 +8581,7 @@ __metadata: "@rocket.chat/peggy-loader": "workspace:~" "@rocket.chat/prettier-config": "npm:~0.31.25" "@types/jest": "npm:~29.5.14" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@typescript-eslint/parser": "npm:~5.58.0" babel-loader: "npm:~9.2.1" eslint: "npm:~8.45.0" @@ -8818,7 +8644,7 @@ __metadata: "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/favicon": "workspace:^" "@rocket.chat/freeswitch": "workspace:^" - "@rocket.chat/fuselage": "npm:~0.61.0" + "@rocket.chat/fuselage": "npm:~0.62.0" "@rocket.chat/fuselage-hooks": "npm:~0.35.0" "@rocket.chat/fuselage-polyfills": "npm:~0.31.25" "@rocket.chat/fuselage-toastbar": "npm:^0.35.0" @@ -8826,7 +8652,7 @@ __metadata: "@rocket.chat/fuselage-ui-kit": "workspace:^" "@rocket.chat/gazzodown": "workspace:^" "@rocket.chat/i18n": "workspace:^" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/instance-status": "workspace:^" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/jwt": "workspace:^" @@ -8841,10 +8667,11 @@ __metadata: "@rocket.chat/mock-providers": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/mongo-adapter": "workspace:^" "@rocket.chat/mp3-encoder": "npm:^0.31.26" "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/omnichannel-services": "workspace:^" - "@rocket.chat/onboarding-ui": "npm:~0.35.0" + "@rocket.chat/onboarding-ui": "npm:^0.35.1" "@rocket.chat/password-policies": "workspace:^" "@rocket.chat/patch-injection": "workspace:^" "@rocket.chat/pdf-worker": "workspace:^" @@ -8923,7 +8750,7 @@ __metadata: "@types/meteor-collection-hooks": "npm:^0.8.9" "@types/mkdirp": "npm:^1.0.2" "@types/mocha": "github:whitecolor/mocha-types" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/node-rsa": "npm:^1.1.4" "@types/nodemailer": "npm:^6.4.16" "@types/oauth2-server": "npm:^3.0.18" @@ -8933,6 +8760,7 @@ __metadata: "@types/proxy-from-env": "npm:^1.0.4" "@types/proxyquire": "npm:^1.3.31" "@types/psl": "npm:^1.1.3" + "@types/qs": "npm:^6" "@types/react": "npm:~18.3.17" "@types/react-dom": "npm:~18.3.5" "@types/sanitize-html": "npm:^2.13.0" @@ -9030,13 +8858,14 @@ __metadata: he: "npm:^1.2.0" highlight.js: "npm:11.8.0" hljs9: "npm:highlight.js@^9.18.5" + hono: "npm:^4.6.19" http-proxy-agent: "npm:^7.0.2" human-interval: "npm:^2.0.1" i18next: "npm:~23.4.9" i18next-http-backend: "npm:^1.4.5" i18next-sprintf-postprocessor: "npm:^0.2.2" iconv-lite: "npm:^0.6.3" - image-size: "npm:^1.1.1" + image-size: "npm:^1.2.1" imap: "npm:^0.8.19" ip-range-check: "npm:^0.2.0" is-svg: "npm:^5.1.0" @@ -9102,6 +8931,7 @@ __metadata: proxy-from-env: "npm:^1.1.0" proxyquire: "npm:^2.1.3" psl: "npm:^1.10.0" + qs: "npm:^6.14.0" query-string: "npm:^7.1.3" queue-fifo: "npm:^0.2.6" raw-loader: "npm:~4.0.2" @@ -9156,6 +8986,7 @@ __metadata: xml2js: "npm:~0.6.2" yaqrcode: "npm:^0.2.1" zod: "npm:^3.24.1" + zustand: "npm:~5.0.3" languageName: unknown linkType: soft @@ -9166,12 +8997,19 @@ __metadata: "@rocket.chat/ddp-client": "workspace:~" "@rocket.chat/emitter": "npm:~0.31.25" "@rocket.chat/i18n": "workspace:~" - "@rocket.chat/ui-contexts": "workspace:*" + "@rocket.chat/mongo-adapter": "workspace:~" + "@rocket.chat/ui-contexts": "workspace:^" "@rocket.chat/ui-video-conf": "workspace:*" "@storybook/react": "npm:^8.6.4" "@tanstack/react-query": "npm:~5.65.1" + "@testing-library/jest-dom": "npm:^6.6.3" + "@testing-library/react": "npm:^16.2.0" + "@testing-library/react-hooks": "npm:^8.0.1" + "@types/react": "npm:~18.3.17" + "@types/react-dom": "npm:~18.3.5" eslint: "npm:~8.45.0" i18next: "npm:~23.4.9" + jest: "npm:^29.7.0" react: "npm:~18.3.1" react-i18next: "npm:~13.2.2" typescript: "npm:~5.7.2" @@ -9214,6 +9052,15 @@ __metadata: languageName: unknown linkType: soft +"@rocket.chat/mongo-adapter@workspace:^, @rocket.chat/mongo-adapter@workspace:packages/mongo-adapter, @rocket.chat/mongo-adapter@workspace:~": + version: 0.0.0-use.local + resolution: "@rocket.chat/mongo-adapter@workspace:packages/mongo-adapter" + dependencies: + eslint: "npm:~8.45.0" + typescript: "npm:~5.7.2" + languageName: unknown + linkType: soft + "@rocket.chat/mp3-encoder@npm:^0.31.26": version: 0.31.26 resolution: "@rocket.chat/mp3-encoder@npm:0.31.26" @@ -9229,7 +9076,7 @@ __metadata: "@rocket.chat/eslint-config": "workspace:^" "@types/chai": "npm:~4.3.20" "@types/ejson": "npm:^2.2.2" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/sinon": "npm:^10.0.20" chai: "npm:^4.5.0" ejson: "npm:^2.2.3" @@ -9259,7 +9106,7 @@ __metadata: "@rocket.chat/string-helpers": "npm:~0.31.25" "@rocket.chat/tools": "workspace:^" "@types/jest": "npm:~29.5.14" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" date-fns: "npm:^2.30.0" ejson: "npm:^2.2.3" emoji-toolkit: "npm:^7.0.1" @@ -9293,7 +9140,7 @@ __metadata: "@rocket.chat/tools": "workspace:^" "@rocket.chat/tracing": "workspace:^" "@types/gc-stats": "npm:^1.4.3" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/polka": "npm:^0.5.7" ejson: "npm:^2.2.3" emoji-toolkit: "npm:^7.0.1" @@ -9314,9 +9161,9 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/onboarding-ui@npm:~0.35.0": - version: 0.35.0 - resolution: "@rocket.chat/onboarding-ui@npm:0.35.0" +"@rocket.chat/onboarding-ui@npm:^0.35.1": + version: 0.35.1 + resolution: "@rocket.chat/onboarding-ui@npm:0.35.1" dependencies: i18next: "npm:~21.6.16" react-hook-form: "npm:~7.54.2" @@ -9331,7 +9178,7 @@ __metadata: react: "*" react-dom: "*" react-i18next: "*" - checksum: 10/eef2a48b76d9a9f96f55c87883c6b0748c85cf742920d47bddda06fc9284fe521fccd246300a9e77b5845989a6d6f88f0ba03d38fe1c31e228c0dd541a0d04cf + checksum: 10/e56caa9767ecd90ce4453439bda4832ce805b40dea4e73cfe10e8983066bb7e7ea8552025d46e21b31660c9fbd5f2e804cb44c15671d9eba17cb5c4e9d3f22bc languageName: node linkType: hard @@ -9396,7 +9243,7 @@ __metadata: dependencies: "@rocket.chat/eslint-config": "workspace:~" "@rocket.chat/prettier-config": "npm:~0.31.25" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" eslint: "npm:~8.45.0" npm-run-all: "npm:^4.1.5" peggy: "npm:4.1.1" @@ -9435,7 +9282,7 @@ __metadata: "@rocket.chat/string-helpers": "npm:~0.31.25" "@rocket.chat/tracing": "workspace:^" "@types/gc-stats": "npm:^1.4.3" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/polka": "npm:^0.5.7" ejson: "npm:^2.2.3" eslint: "npm:~8.45.0" @@ -9466,7 +9313,7 @@ __metadata: "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" babel-jest: "npm:^29.7.0" eslint: "npm:~8.45.0" jest: "npm:~29.7.0" @@ -9499,7 +9346,7 @@ __metadata: "@rocket.chat/omnichannel-services": "workspace:^" "@rocket.chat/tracing": "workspace:^" "@types/gc-stats": "npm:^1.4.3" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/polka": "npm:^0.5.7" ejson: "npm:^2.2.3" emoji-toolkit: "npm:^7.0.1" @@ -9545,7 +9392,7 @@ __metadata: "@actions/github": "npm:^6.0.0" "@octokit/plugin-throttling": "npm:^6.1.0" "@rocket.chat/eslint-config": "workspace:^" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" eslint: "npm:~8.45.0" mdast-util-to-string: "npm:2.0.0" remark-parse: "npm:9.0.0" @@ -9562,7 +9409,7 @@ __metadata: dependencies: "@changesets/types": "npm:^6.0.0" "@rocket.chat/eslint-config": "workspace:^" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" dataloader: "npm:^2.2.2" eslint: "npm:~8.45.0" node-fetch: "npm:^2.7.0" @@ -9647,7 +9494,7 @@ __metadata: "@rocket.chat/tracing": "workspace:^" "@types/bcrypt": "npm:^5.0.2" "@types/gc-stats": "npm:^1.4.3" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/polka": "npm:^0.5.7" ejson: "npm:^2.2.3" eslint: "npm:~8.45.0" @@ -9725,7 +9572,7 @@ __metadata: resolution: "@rocket.chat/ui-avatar@workspace:packages/ui-avatar" dependencies: "@babel/core": "npm:~7.26.0" - "@rocket.chat/fuselage": "npm:~0.61.0" + "@rocket.chat/fuselage": "npm:~0.62.0" "@rocket.chat/ui-contexts": "workspace:^" "@types/react": "npm:~18.3.17" "@types/react-dom": "npm:~18.3.5" @@ -9738,7 +9585,7 @@ __metadata: typescript: "npm:~5.7.2" peerDependencies: "@rocket.chat/fuselage": "*" - "@rocket.chat/ui-contexts": 17.0.0 + "@rocket.chat/ui-contexts": "workspace:^" react: ~17.0.2 languageName: unknown linkType: soft @@ -9750,13 +9597,14 @@ __metadata: "@babel/core": "npm:~7.26.0" "@react-aria/toolbar": "npm:^3.0.0-nightly.5042" "@rocket.chat/css-in-js": "npm:~0.31.25" - "@rocket.chat/fuselage": "npm:~0.61.0" + "@rocket.chat/fuselage": "npm:~0.62.0" "@rocket.chat/fuselage-hooks": "npm:~0.35.0" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/mock-providers": "workspace:^" "@rocket.chat/ui-avatar": "workspace:~" "@rocket.chat/ui-contexts": "workspace:~" + "@storybook/addon-a11y": "npm:^8.6.4" "@storybook/addon-actions": "npm:^8.6.4" "@storybook/addon-docs": "npm:^8.6.4" "@storybook/addon-essentials": "npm:^8.6.4" @@ -9783,6 +9631,7 @@ __metadata: react-dom: "npm:~18.3.1" react-hook-form: "npm:~7.45.4" storybook: "npm:^8.6.4" + storybook-dark-mode: "npm:^4.0.2" typescript: "npm:~5.7.2" peerDependencies: "@react-aria/toolbar": "*" @@ -9790,8 +9639,8 @@ __metadata: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" - "@rocket.chat/ui-avatar": 13.0.0 - "@rocket.chat/ui-contexts": 17.0.0 + "@rocket.chat/ui-avatar": "workspace:^" + "@rocket.chat/ui-contexts": "workspace:^" react: "*" react-i18next: "*" languageName: unknown @@ -9804,8 +9653,8 @@ __metadata: "@babel/core": "npm:~7.26.0" "@react-aria/toolbar": "npm:^3.0.0-nightly.5042" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": "npm:~0.61.0" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/fuselage": "npm:~0.62.0" + "@rocket.chat/icons": "npm:^0.42.0" "@storybook/addon-actions": "npm:^8.6.4" "@storybook/addon-docs": "npm:^8.6.4" "@storybook/addon-essentials": "npm:^8.6.4" @@ -9832,7 +9681,7 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/ui-contexts@workspace:*, @rocket.chat/ui-contexts@workspace:^, @rocket.chat/ui-contexts@workspace:packages/ui-contexts, @rocket.chat/ui-contexts@workspace:~": +"@rocket.chat/ui-contexts@workspace:^, @rocket.chat/ui-contexts@workspace:packages/ui-contexts, @rocket.chat/ui-contexts@workspace:~": version: 0.0.0-use.local resolution: "@rocket.chat/ui-contexts@workspace:packages/ui-contexts" dependencies: @@ -9870,7 +9719,7 @@ __metadata: "@babel/plugin-transform-runtime": "npm:~7.25.9" "@babel/preset-env": "npm:~7.26.0" "@rocket.chat/eslint-config": "workspace:~" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/jest-presets": "workspace:~" "@types/jest": "npm:~29.5.14" babel-loader: "npm:~9.2.1" @@ -9895,9 +9744,9 @@ __metadata: resolution: "@rocket.chat/ui-theming@workspace:ee/packages/ui-theming" dependencies: "@rocket.chat/css-in-js": "npm:~0.31.25" - "@rocket.chat/fuselage": "npm:~0.61.0" + "@rocket.chat/fuselage": "npm:~0.62.0" "@rocket.chat/fuselage-hooks": "npm:~0.35.0" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/ui-contexts": "workspace:~" "@types/react": "npm:~18.3.17" eslint: "npm:~8.45.0" @@ -9925,9 +9774,9 @@ __metadata: "@rocket.chat/css-in-js": "npm:~0.31.25" "@rocket.chat/emitter": "npm:~0.31.25" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": "npm:~0.61.0" + "@rocket.chat/fuselage": "npm:~0.62.0" "@rocket.chat/fuselage-hooks": "npm:~0.35.0" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/styled": "npm:~0.32.0" "@rocket.chat/ui-avatar": "workspace:^" @@ -9958,8 +9807,8 @@ __metadata: "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 13.0.0 - "@rocket.chat/ui-contexts": 17.0.0 + "@rocket.chat/ui-avatar": "workspace:^" + "@rocket.chat/ui-contexts": "workspace:^" react: ~17.0.2 react-dom: ^17.0.2 languageName: unknown @@ -9975,9 +9824,9 @@ __metadata: "@rocket.chat/css-in-js": "npm:~0.31.25" "@rocket.chat/emitter": "npm:~0.31.25" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": "npm:~0.61.0" + "@rocket.chat/fuselage": "npm:~0.62.0" "@rocket.chat/fuselage-hooks": "npm:~0.35.0" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/mock-providers": "workspace:~" "@rocket.chat/styled": "npm:~0.32.0" @@ -10015,9 +9864,9 @@ __metadata: "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 13.0.0 - "@rocket.chat/ui-client": 17.0.0 - "@rocket.chat/ui-contexts": 17.0.0 + "@rocket.chat/ui-avatar": "workspace:^" + "@rocket.chat/ui-client": "workspace:^" + "@rocket.chat/ui-contexts": "workspace:^" react: ~17.0.2 react-aria: ~3.23.1 react-dom: ^17.0.2 @@ -10034,13 +9883,13 @@ __metadata: "@lezer/highlight": "npm:^1.2.1" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": "npm:~0.31.25" - "@rocket.chat/fuselage": "npm:~0.61.0" + "@rocket.chat/fuselage": "npm:~0.62.0" "@rocket.chat/fuselage-hooks": "npm:~0.35.0" "@rocket.chat/fuselage-polyfills": "npm:~0.31.25" "@rocket.chat/fuselage-toastbar": "npm:^0.35.0" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" "@rocket.chat/fuselage-ui-kit": "workspace:~" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/logo": "npm:^0.32.0" "@rocket.chat/styled": "npm:~0.32.0" "@rocket.chat/ui-avatar": "workspace:^" @@ -10067,7 +9916,7 @@ __metadata: react-virtuoso: "npm:^4.12.0" reactflow: "npm:^11.11.4" typescript: "npm:~5.7.2" - vite: "npm:^6.1.0" + vite: "npm:^6.2.4" languageName: unknown linkType: soft @@ -10108,7 +9957,7 @@ __metadata: peerDependencies: "@rocket.chat/layout": "*" "@rocket.chat/tools": 0.2.2 - "@rocket.chat/ui-contexts": 17.0.0 + "@rocket.chat/ui-contexts": "workspace:^" "@tanstack/react-query": "*" react: "*" react-hook-form: "*" @@ -11269,7 +11118,7 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:~6.6.3": +"@testing-library/jest-dom@npm:^6.6.3, @testing-library/jest-dom@npm:~6.6.3": version: 6.6.3 resolution: "@testing-library/jest-dom@npm:6.6.3" dependencies: @@ -11284,6 +11133,48 @@ __metadata: languageName: node linkType: hard +"@testing-library/react-hooks@npm:^8.0.1": + version: 8.0.1 + resolution: "@testing-library/react-hooks@npm:8.0.1" + dependencies: + "@babel/runtime": "npm:^7.12.5" + react-error-boundary: "npm:^3.1.0" + peerDependencies: + "@types/react": ^16.9.0 || ^17.0.0 + react: ^16.9.0 || ^17.0.0 + react-dom: ^16.9.0 || ^17.0.0 + react-test-renderer: ^16.9.0 || ^17.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + react-dom: + optional: true + react-test-renderer: + optional: true + checksum: 10/f7b69373feebe99bc7d60595822cc5c00a1a5a4801bc4f99b597256a5c1d23c45a51f359051dd8a7bdffcc23b26f324c582e9433c25804934fd351a886812790 + languageName: node + linkType: hard + +"@testing-library/react@npm:^16.2.0": + version: 16.2.0 + resolution: "@testing-library/react@npm:16.2.0" + dependencies: + "@babel/runtime": "npm:^7.12.5" + peerDependencies: + "@testing-library/dom": ^10.0.0 + "@types/react": ^18.0.0 || ^19.0.0 + "@types/react-dom": ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10/cf10bfa9a363384e6861417696fff4a464a64f98ec6f0bb7f1fa7cbb550d075d23a2f6a943b7df85dded7bde3234f6ea6b6e36f95211f4544b846ea72c288289 + languageName: node + linkType: hard + "@testing-library/react@npm:~16.0.1": version: 16.0.1 resolution: "@testing-library/react@npm:16.0.1" @@ -12700,12 +12591,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:~20.17.8": - version: 20.17.8 - resolution: "@types/node@npm:20.17.8" +"@types/node@npm:~22.14.0": + version: 22.14.0 + resolution: "@types/node@npm:22.14.0" dependencies: - undici-types: "npm:~6.19.2" - checksum: 10/e3e968b327abc70fd437a223f8950dd4436047e954aa7db09abde5df1f58a5c49f33d6f14524e256d09719e1960d22bf072d62e4bda7375f7895a092c7eb2f9d + undici-types: "npm:~6.21.0" + checksum: 10/d0669a8a37a18532c886ccfa51eb3fe1e46088deb4d3d27ebcd5d7d68bd6343ad1c7a3fcb85164780a57629359c33a6c917ecff748ea232bceac7692acc96537 languageName: node linkType: hard @@ -12836,6 +12727,13 @@ __metadata: languageName: node linkType: hard +"@types/qs@npm:^6": + version: 6.9.18 + resolution: "@types/qs@npm:6.9.18" + checksum: 10/152fab96efd819cc82ae67c39f089df415da6deddb48f1680edaaaa4e86a2a597de7b2ff0ad391df66d11a07006a08d52c9405e86b8cb8f3d5ba15881fe56cc7 + languageName: node + linkType: hard + "@types/range-parser@npm:*": version: 1.2.4 resolution: "@types/range-parser@npm:1.2.4" @@ -20038,92 +19936,6 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.24.2": - version: 0.24.2 - resolution: "esbuild@npm:0.24.2" - dependencies: - "@esbuild/aix-ppc64": "npm:0.24.2" - "@esbuild/android-arm": "npm:0.24.2" - "@esbuild/android-arm64": "npm:0.24.2" - "@esbuild/android-x64": "npm:0.24.2" - "@esbuild/darwin-arm64": "npm:0.24.2" - "@esbuild/darwin-x64": "npm:0.24.2" - "@esbuild/freebsd-arm64": "npm:0.24.2" - "@esbuild/freebsd-x64": "npm:0.24.2" - "@esbuild/linux-arm": "npm:0.24.2" - "@esbuild/linux-arm64": "npm:0.24.2" - "@esbuild/linux-ia32": "npm:0.24.2" - "@esbuild/linux-loong64": "npm:0.24.2" - "@esbuild/linux-mips64el": "npm:0.24.2" - "@esbuild/linux-ppc64": "npm:0.24.2" - "@esbuild/linux-riscv64": "npm:0.24.2" - "@esbuild/linux-s390x": "npm:0.24.2" - "@esbuild/linux-x64": "npm:0.24.2" - "@esbuild/netbsd-arm64": "npm:0.24.2" - "@esbuild/netbsd-x64": "npm:0.24.2" - "@esbuild/openbsd-arm64": "npm:0.24.2" - "@esbuild/openbsd-x64": "npm:0.24.2" - "@esbuild/sunos-x64": "npm:0.24.2" - "@esbuild/win32-arm64": "npm:0.24.2" - "@esbuild/win32-ia32": "npm:0.24.2" - "@esbuild/win32-x64": "npm:0.24.2" - dependenciesMeta: - "@esbuild/aix-ppc64": - optional: true - "@esbuild/android-arm": - optional: true - "@esbuild/android-arm64": - optional: true - "@esbuild/android-x64": - optional: true - "@esbuild/darwin-arm64": - optional: true - "@esbuild/darwin-x64": - optional: true - "@esbuild/freebsd-arm64": - optional: true - "@esbuild/freebsd-x64": - optional: true - "@esbuild/linux-arm": - optional: true - "@esbuild/linux-arm64": - optional: true - "@esbuild/linux-ia32": - optional: true - "@esbuild/linux-loong64": - optional: true - "@esbuild/linux-mips64el": - optional: true - "@esbuild/linux-ppc64": - optional: true - "@esbuild/linux-riscv64": - optional: true - "@esbuild/linux-s390x": - optional: true - "@esbuild/linux-x64": - optional: true - "@esbuild/netbsd-arm64": - optional: true - "@esbuild/netbsd-x64": - optional: true - "@esbuild/openbsd-arm64": - optional: true - "@esbuild/openbsd-x64": - optional: true - "@esbuild/sunos-x64": - optional: true - "@esbuild/win32-arm64": - optional: true - "@esbuild/win32-ia32": - optional: true - "@esbuild/win32-x64": - optional: true - bin: - esbuild: bin/esbuild - checksum: 10/95425071c9f24ff88bf61e0710b636ec0eb24ddf8bd1f7e1edef3044e1221104bbfa7bbb31c18018c8c36fa7902c5c0b843f829b981ebc89160cf5eebdaa58f4 - languageName: node - linkType: hard - "esbuild@npm:^0.25.0": version: 0.25.0 resolution: "esbuild@npm:0.25.0" @@ -22912,6 +22724,13 @@ __metadata: languageName: node linkType: hard +"hono@npm:^4.6.19": + version: 4.6.19 + resolution: "hono@npm:4.6.19" + checksum: 10/b3e317bdaf868359b68271d5aa395b1561ceedf3ac97f2d76cc5b9e00e01a794f862bdb5d5574b96b284f5456e2be4d42e6f9511a479d1afdf8c09dff75150e7 + languageName: node + linkType: hard + "hosted-git-info@npm:^2.1.4": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -23474,14 +23293,14 @@ __metadata: languageName: node linkType: hard -"image-size@npm:^1.1.1": - version: 1.1.1 - resolution: "image-size@npm:1.1.1" +"image-size@npm:^1.2.1": + version: 1.2.1 + resolution: "image-size@npm:1.2.1" dependencies: queue: "npm:6.0.2" bin: image-size: bin/image-size.js - checksum: 10/f28966dd3f6d4feccc4028400bb7e8047c28b073ab0aa90c7c53039288139dd416c6bc254a976d4bf61113d4bc84871786804113099701cbfe9ccf377effdb54 + checksum: 10/b290c6cc5635565b1da51991472eb6522808430dbe3415823649723dc5f5fd8263f0f98f9bdec46184274ea24fe4f3f7a297c84b647b412e14d2208703dd8a19 languageName: node linkType: hard @@ -30610,14 +30429,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.5.1": - version: 8.5.1 - resolution: "postcss@npm:8.5.1" +"postcss@npm:^8.5.3": + version: 8.5.3 + resolution: "postcss@npm:8.5.3" dependencies: nanoid: "npm:^3.3.8" picocolors: "npm:^1.1.1" source-map-js: "npm:^1.2.1" - checksum: 10/1fbd28753143f7f03e4604813639918182b15343c7ad0f4e72f3875fc2cc0b8494c887f55dc05008fad5fbf1e1e908ce2edbbce364a91f84dcefb71edf7cd31d + checksum: 10/6d7e21a772e8b05bf102636918654dac097bac013f0dc8346b72ac3604fc16829646f94ea862acccd8f82e910b00e2c11c1f0ea276543565d278c7ca35516a7c languageName: node linkType: hard @@ -31127,6 +30946,15 @@ __metadata: languageName: node linkType: hard +"qs@npm:^6.14.0": + version: 6.14.0 + resolution: "qs@npm:6.14.0" + dependencies: + side-channel: "npm:^1.1.0" + checksum: 10/a60e49bbd51c935a8a4759e7505677b122e23bf392d6535b8fc31c1e447acba2c901235ecb192764013cd2781723dc1f61978b5fdd93cc31d7043d31cdc01974 + languageName: node + linkType: hard + "qs@npm:~6.5.2": version: 6.5.3 resolution: "qs@npm:6.5.3" @@ -31479,7 +31307,7 @@ __metadata: languageName: node linkType: hard -"react-error-boundary@npm:^3.1.4": +"react-error-boundary@npm:^3.1.0, react-error-boundary@npm:^3.1.4": version: 3.1.4 resolution: "react-error-boundary@npm:3.1.4" dependencies: @@ -32638,7 +32466,7 @@ __metadata: "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": "npm:~0.31.25" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" @@ -32651,7 +32479,7 @@ __metadata: "@types/ejson": "npm:^2.2.2" "@types/express": "npm:^4.17.21" "@types/fibers": "npm:^3.1.4" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/ws": "npm:^8.5.13" ajv: "npm:^8.17.1" bcrypt: "npm:^5.1.1" @@ -36365,6 +36193,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.21.0": + version: 6.21.0 + resolution: "undici-types@npm:6.21.0" + checksum: 10/ec8f41aa4359d50f9b59fa61fe3efce3477cc681908c8f84354d8567bb3701fafdddf36ef6bff307024d3feb42c837cf6f670314ba37fc8145e219560e473d14 + languageName: node + linkType: hard + "undici@npm:^5.25.4": version: 5.28.4 resolution: "undici@npm:5.28.4" @@ -36944,13 +36779,13 @@ __metadata: languageName: node linkType: hard -"vite@npm:^6.1.0": - version: 6.1.0 - resolution: "vite@npm:6.1.0" +"vite@npm:^6.2.4": + version: 6.2.4 + resolution: "vite@npm:6.2.4" dependencies: - esbuild: "npm:^0.24.2" + esbuild: "npm:^0.25.0" fsevents: "npm:~2.3.3" - postcss: "npm:^8.5.1" + postcss: "npm:^8.5.3" rollup: "npm:^4.30.1" peerDependencies: "@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0 @@ -36992,7 +36827,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10/5de360ac0ecb3cac85f796ec97d5347e2c8102a8845309af87f52296279464a6d5b880beb740bc42740936ec9de8bf0acce6a6ed3b3b24a733162a5d63d9f46b + checksum: 10/3734c8695b4d35a5b3ea617159594835e370b428745f37e90d9c1daf82b53af5248578c1f1d9977fc1460320c0cdd4aef135095d378b2eba2736c03e2cfa019e languageName: node linkType: hard @@ -38192,6 +38027,27 @@ __metadata: languageName: node linkType: hard +"zustand@npm:~5.0.3": + version: 5.0.3 + resolution: "zustand@npm:5.0.3" + peerDependencies: + "@types/react": ">=18.0.0" + immer: ">=9.0.6" + react: ">=18.0.0" + use-sync-external-store: ">=1.2.0" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + checksum: 10/35728fdaa68291ea3e469524316dda4fe1d8cc22d8be3df309ca99bda0dbc7e66a1c502f66c26f76abfb4bd49a6e1368160353eb3cb173c24042a5f252075462 + languageName: node + linkType: hard + "zwitch@npm:^1.0.0": version: 1.0.5 resolution: "zwitch@npm:1.0.5"