diff --git a/.changeset/angry-garlics-visit.md b/.changeset/angry-garlics-visit.md new file mode 100644 index 0000000000000..3a6464698e419 --- /dev/null +++ b/.changeset/angry-garlics-visit.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +--- + +Improved Retention Policy Warning messages diff --git a/.changeset/breezy-pens-sing.md b/.changeset/breezy-pens-sing.md new file mode 100644 index 0000000000000..0725999ef62b3 --- /dev/null +++ b/.changeset/breezy-pens-sing.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Removed "Unknown media type" errors on the client side by using `application/octet-stream` as a fallback media type (MIME type) for all files diff --git a/.changeset/brown-lobsters-join.md b/.changeset/brown-lobsters-join.md new file mode 100644 index 0000000000000..ac0e52c8f829c --- /dev/null +++ b/.changeset/brown-lobsters-join.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Resolved an issue with the room type filter not being reset after navigating between admin sections. diff --git a/.changeset/bump-patch-1719268507210.md b/.changeset/bump-patch-1719268507210.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1719268507210.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1719375980788.md b/.changeset/bump-patch-1719375980788.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1719375980788.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1719620501814.md b/.changeset/bump-patch-1719620501814.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1719620501814.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1719930428323.md b/.changeset/bump-patch-1719930428323.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1719930428323.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1719958830672.md b/.changeset/bump-patch-1719958830672.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1719958830672.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1720389361731.md b/.changeset/bump-patch-1720389361731.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1720389361731.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/bump-patch-1720558617362.md b/.changeset/bump-patch-1720558617362.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1720558617362.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/chilly-toys-hunt.md b/.changeset/chilly-toys-hunt.md new file mode 100644 index 0000000000000..79be3fcfc74c2 --- /dev/null +++ b/.changeset/chilly-toys-hunt.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed "File Upload > Accepted Media Types" setting to allow all type of files uploads diff --git a/.changeset/clean-moose-cover.md b/.changeset/clean-moose-cover.md new file mode 100644 index 0000000000000..39e6204ce9b4b --- /dev/null +++ b/.changeset/clean-moose-cover.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Introduced the use of the `API_User_Limit` setting to limit amount of members to simultaneously auto-join a room in a team diff --git a/.changeset/cuddly-cycles-nail.md b/.changeset/cuddly-cycles-nail.md new file mode 100644 index 0000000000000..ee49600ee865e --- /dev/null +++ b/.changeset/cuddly-cycles-nail.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/fuselage-ui-kit": minor +"@rocket.chat/ui-kit": minor +--- + +Introduced new elements for apps to select users diff --git a/.changeset/cuddly-maps-peel.md b/.changeset/cuddly-maps-peel.md new file mode 100644 index 0000000000000..1d4d8913ec7c0 --- /dev/null +++ b/.changeset/cuddly-maps-peel.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed streams being called when the user is not logged in diff --git a/.changeset/dry-shoes-tap.md b/.changeset/dry-shoes-tap.md new file mode 100644 index 0000000000000..f5abf51c0df0c --- /dev/null +++ b/.changeset/dry-shoes-tap.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes the supported versions problem, where in most cases the data chosen was the oldest diff --git a/.changeset/dull-knives-complain.md b/.changeset/dull-knives-complain.md new file mode 100644 index 0000000000000..02bfe810eaee0 --- /dev/null +++ b/.changeset/dull-knives-complain.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Added the allowDiskUse option to the users page queries so that if the mongodb memory threshold is exceeded it will use disk space instead of throwing an error. diff --git a/.changeset/eighty-wasps-kneel.md b/.changeset/eighty-wasps-kneel.md new file mode 100644 index 0000000000000..d8f297de64c34 --- /dev/null +++ b/.changeset/eighty-wasps-kneel.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed an issue with how the UI checked for permissions when deciding if editing or deleting a message by moderators users diff --git a/.changeset/famous-scissors-teach.md b/.changeset/famous-scissors-teach.md new file mode 100644 index 0000000000000..05d4cbbf8ea50 --- /dev/null +++ b/.changeset/famous-scissors-teach.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +--- + +Prevent usage of OTR messages with End-to-end Encryption, both feature shouldn't and can't work together. diff --git a/.changeset/five-monkeys-applaud.md b/.changeset/five-monkeys-applaud.md new file mode 100644 index 0000000000000..aaa1c0ae367cb --- /dev/null +++ b/.changeset/five-monkeys-applaud.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fixes not being able to reinstall app after installation failure diff --git a/.changeset/five-shoes-fly.md b/.changeset/five-shoes-fly.md new file mode 100644 index 0000000000000..da462e1508c7f --- /dev/null +++ b/.changeset/five-shoes-fly.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed codeBlock styles in light mode diff --git a/.changeset/forty-bikes-check.md b/.changeset/forty-bikes-check.md new file mode 100644 index 0000000000000..fdc42a68b0e40 --- /dev/null +++ b/.changeset/forty-bikes-check.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/core-typings': patch +'@rocket.chat/meteor': patch +--- + +Decrypt pinned encrypted messages in the chat and pinned messages contextual bar. diff --git a/.changeset/forty-ghosts-flow.md b/.changeset/forty-ghosts-flow.md new file mode 100644 index 0000000000000..743110f39e615 --- /dev/null +++ b/.changeset/forty-ghosts-flow.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed "Take it" button behavior disabling it when agent status is set to offline diff --git a/.changeset/four-onions-camp.md b/.changeset/four-onions-camp.md new file mode 100644 index 0000000000000..8068ac023638f --- /dev/null +++ b/.changeset/four-onions-camp.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +When using `DISABLE_DB_WATCHERS=true` this fixes message updates with URLs that were missing the link preview. diff --git a/.changeset/friendly-months-attack.md b/.changeset/friendly-months-attack.md new file mode 100644 index 0000000000000..90f0e58a9b324 --- /dev/null +++ b/.changeset/friendly-months-attack.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Implement E2EE warning callouts letting users know that encrypted messages can't be searched and auditted on search contextual bar and audit panel. diff --git a/.changeset/fuzzy-readers-bake.md b/.changeset/fuzzy-readers-bake.md new file mode 100644 index 0000000000000..a487096a312e3 --- /dev/null +++ b/.changeset/fuzzy-readers-bake.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes issues with loading license modules when loading the page while logged out diff --git a/.changeset/gold-flowers-shake.md b/.changeset/gold-flowers-shake.md new file mode 100644 index 0000000000000..26182d785c22f --- /dev/null +++ b/.changeset/gold-flowers-shake.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Added E2EE room setup header, with just limited functionality and room actions. diff --git a/.changeset/green-camels-repair.md b/.changeset/green-camels-repair.md new file mode 100644 index 0000000000000..58b0f6f1a00cc --- /dev/null +++ b/.changeset/green-camels-repair.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed 2 issues with `QueueInactivityMonitor` callback. This callback was in charge of scheduling the job that would close the inquiry, but it was checking on a property that didn't exist. This caused the callback to early return without scheduling the job, making the feature to not to work. diff --git a/.changeset/grumpy-games-greet.md b/.changeset/grumpy-games-greet.md new file mode 100644 index 0000000000000..1e7f03658ad37 --- /dev/null +++ b/.changeset/grumpy-games-greet.md @@ -0,0 +1,4 @@ +--- +'@rocket.chat/meteor': patch +--- +Changed streaming logic to prevent hidden system messages from being broadcasted through `stream-room-messages`. diff --git a/.changeset/happy-cameras-mix.md b/.changeset/happy-cameras-mix.md new file mode 100644 index 0000000000000..005d6742a686c --- /dev/null +++ b/.changeset/happy-cameras-mix.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed an issue where private encrypted room creation was being forced even when E2EE feature was disabled. diff --git a/.changeset/happy-windows-drum.md b/.changeset/happy-windows-drum.md new file mode 100644 index 0000000000000..3fc0c7ec8c3ae --- /dev/null +++ b/.changeset/happy-windows-drum.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed an issue that allowed saveSettings method to save NaN values on numeric settings. diff --git a/.changeset/healthy-clouds-hide.md b/.changeset/healthy-clouds-hide.md new file mode 100644 index 0000000000000..528a1bf275683 --- /dev/null +++ b/.changeset/healthy-clouds-hide.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/gazzodown": patch +--- + +Fixes long katex lines overflowing the message component diff --git a/.changeset/heavy-dolphins-lie.md b/.changeset/heavy-dolphins-lie.md new file mode 100644 index 0000000000000..aac6d0bc8e825 --- /dev/null +++ b/.changeset/heavy-dolphins-lie.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes the issue where the modal backdrop is overlapping the options of the `Select` component diff --git a/.changeset/hungry-waves-lick.md b/.changeset/hungry-waves-lick.md new file mode 100644 index 0000000000000..294ece663c32e --- /dev/null +++ b/.changeset/hungry-waves-lick.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed file name being incorrectly sent from the client when uploading assets diff --git a/.changeset/lastmessage-e2ee.md b/.changeset/lastmessage-e2ee.md new file mode 100644 index 0000000000000..b3c8642dcff60 --- /dev/null +++ b/.changeset/lastmessage-e2ee.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +--- + +Fixed last message preview in Sidebar for E2E Ecrypted channels diff --git a/.changeset/late-drinks-brake.md b/.changeset/late-drinks-brake.md new file mode 100644 index 0000000000000..7c1ba9ddd7f2b --- /dev/null +++ b/.changeset/late-drinks-brake.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +--- + +Fixed an issue that would not allow the user to dismiss the closeToSeatsLimit banner for old workspaces + diff --git a/.changeset/metal-candles-float.md b/.changeset/metal-candles-float.md new file mode 100644 index 0000000000000..256e6c3ac7d2f --- /dev/null +++ b/.changeset/metal-candles-float.md @@ -0,0 +1,13 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/core-typings": patch +"@rocket.chat/i18n": patch +--- + +Implemented a new "Pending Users" tab on the users page to list users who have not yet been activated and/or have not logged in for the first time. +Additionally, added a "Pending Action" column to aid administrators in identifying necessary actions for each user. Incorporated a "Reason for Joining" field +into the user info contextual bar, along with a callout for exceeding the seats cap in the users page header. Finally, introduced a new logic to disable user creation buttons upon surpassing the seats cap. + + + + diff --git a/.changeset/metal-cats-suffer.md b/.changeset/metal-cats-suffer.md new file mode 100644 index 0000000000000..73acebdca85d9 --- /dev/null +++ b/.changeset/metal-cats-suffer.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Support encrypted files on end-to-end encrypted rooms. diff --git a/.changeset/mighty-oranges-wait.md b/.changeset/mighty-oranges-wait.md new file mode 100644 index 0000000000000..888a013d54ae9 --- /dev/null +++ b/.changeset/mighty-oranges-wait.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Added a "LDAP group validation strategy" setting to LDAP channels and roles sync in order to enable faster syncs diff --git a/.changeset/nasty-windows-reply.md b/.changeset/nasty-windows-reply.md new file mode 100644 index 0000000000000..be62ea5587628 --- /dev/null +++ b/.changeset/nasty-windows-reply.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Moves the quotes to be on top of the message for better readability diff --git a/.changeset/nervous-wolves-collect.md b/.changeset/nervous-wolves-collect.md new file mode 100644 index 0000000000000..e32377f541794 --- /dev/null +++ b/.changeset/nervous-wolves-collect.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes the issue not allowing users without edit-room-retention-policy permission try to edit the room with the retention policy enabled diff --git a/.changeset/nice-zebras-admire.md b/.changeset/nice-zebras-admire.md new file mode 100644 index 0000000000000..74917c0a08cc6 --- /dev/null +++ b/.changeset/nice-zebras-admire.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Prevent E2EE key reset on startup due to possible race conditions diff --git a/.changeset/odd-goats-fix.md b/.changeset/odd-goats-fix.md new file mode 100644 index 0000000000000..9178620391be9 --- /dev/null +++ b/.changeset/odd-goats-fix.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where settings code mirror is not being displayed correctly in full screen mode diff --git a/.changeset/orange-clocks-raise.md b/.changeset/orange-clocks-raise.md new file mode 100644 index 0000000000000..81eac16e2a999 --- /dev/null +++ b/.changeset/orange-clocks-raise.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/model-typings': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +Async End-to-End Encrypted rooms key distribution process. Users now don't need to be online to get the keys of their subscribed encrypted rooms, the key distribution process is now async and users can recieve keys even when they are not online. diff --git a/.changeset/plenty-buses-kneel.md b/.changeset/plenty-buses-kneel.md new file mode 100644 index 0000000000000..5c21bdb0bb697 --- /dev/null +++ b/.changeset/plenty-buses-kneel.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Disable "Reply in direct message", "Copy link" and "Forward message" message menu items for encrypted messages as they don't apply to encrypted messages and also disable apps menu items and show a warning. diff --git a/.changeset/popular-bulldogs-accept.md b/.changeset/popular-bulldogs-accept.md new file mode 100644 index 0000000000000..b18e3382148b7 --- /dev/null +++ b/.changeset/popular-bulldogs-accept.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/i18n': patch +'@rocket.chat/meteor': patch +--- + +Disable slash commands in encrypted rooms and show a disabled warning. diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000000..2114b5521353c --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,132 @@ +{ + "mode": "pre", + "tag": "rc", + "initialVersions": { + "@rocket.chat/meteor": "6.10.0-develop", + "rocketchat-services": "1.1.35", + "@rocket.chat/account-service": "0.3.16", + "@rocket.chat/authorization-service": "0.3.17", + "@rocket.chat/ddp-streamer": "0.2.16", + "@rocket.chat/omnichannel-transcript": "0.3.17", + "@rocket.chat/presence-service": "0.3.17", + "@rocket.chat/queue-worker": "0.3.17", + "@rocket.chat/stream-hub-service": "0.3.17", + "@rocket.chat/api-client": "0.1.35", + "@rocket.chat/ddp-client": "0.2.26", + "@rocket.chat/license": "0.1.17", + "@rocket.chat/omnichannel-services": "0.1.17", + "@rocket.chat/pdf-worker": "0.0.41", + "@rocket.chat/presence": "0.1.17", + "@rocket.chat/ui-theming": "0.1.2", + "@rocket.chat/account-utils": "0.0.2", + "@rocket.chat/agenda": "0.1.0", + "@rocket.chat/apps": "0.0.8", + "@rocket.chat/base64": "1.0.13", + "@rocket.chat/cas-validate": "0.0.2", + "@rocket.chat/core-services": "0.3.17", + "@rocket.chat/core-typings": "6.10.0-develop", + "@rocket.chat/cron": "0.0.37", + "@rocket.chat/eslint-config": "0.7.0", + "@rocket.chat/favicon": "0.0.2", + "@rocket.chat/fuselage-ui-kit": "7.0.2", + "@rocket.chat/gazzodown": "7.0.2", + "@rocket.chat/i18n": "0.4.0", + "@rocket.chat/instance-status": "0.0.41", + "@rocket.chat/jwt": "0.1.1", + "@rocket.chat/livechat": "1.17.2", + "@rocket.chat/log-format": "0.0.2", + "@rocket.chat/logger": "0.0.2", + "@rocket.chat/message-parser": "0.31.29", + "@rocket.chat/mock-providers": "0.0.7", + "@rocket.chat/model-typings": "0.4.3", + "@rocket.chat/models": "0.0.41", + "@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.25", + "@rocket.chat/random": "1.2.2", + "@rocket.chat/release-action": "2.2.3", + "@rocket.chat/release-changelog": "0.1.0", + "@rocket.chat/rest-typings": "6.10.0-develop", + "@rocket.chat/server-cloud-communication": "0.0.2", + "@rocket.chat/server-fetch": "0.0.3", + "@rocket.chat/sha256": "1.0.10", + "@rocket.chat/tools": "0.2.1", + "@rocket.chat/ui-avatar": "3.0.2", + "@rocket.chat/ui-client": "7.0.2", + "@rocket.chat/ui-composer": "0.1.0", + "@rocket.chat/ui-contexts": "7.0.2", + "@rocket.chat/ui-kit": "0.34.0", + "@rocket.chat/ui-video-conf": "7.0.2", + "@rocket.chat/uikit-playground": "0.2.25", + "@rocket.chat/web-ui-registration": "7.0.2" + }, + "changesets": [ + "angry-garlics-visit", + "breezy-pens-sing", + "brown-lobsters-join", + "bump-patch-1719268507210", + "bump-patch-1719375980788", + "bump-patch-1719620501814", + "bump-patch-1719930428323", + "bump-patch-1719958830672", + "bump-patch-1720389361731", + "bump-patch-1720558617362", + "chilly-toys-hunt", + "clean-moose-cover", + "cuddly-cycles-nail", + "cuddly-maps-peel", + "dry-shoes-tap", + "dull-knives-complain", + "eighty-wasps-kneel", + "famous-scissors-teach", + "five-monkeys-applaud", + "five-shoes-fly", + "forty-bikes-check", + "forty-ghosts-flow", + "four-onions-camp", + "friendly-months-attack", + "fuzzy-readers-bake", + "gold-flowers-shake", + "green-camels-repair", + "grumpy-games-greet", + "happy-cameras-mix", + "happy-windows-drum", + "healthy-clouds-hide", + "heavy-dolphins-lie", + "hungry-waves-lick", + "lastmessage-e2ee", + "late-drinks-brake", + "metal-candles-float", + "metal-cats-suffer", + "mighty-oranges-wait", + "nasty-windows-reply", + "nervous-wolves-collect", + "nice-zebras-admire", + "odd-goats-fix", + "orange-clocks-raise", + "plenty-buses-kneel", + "popular-bulldogs-accept", + "proud-coats-repair", + "rare-colts-repair", + "rare-dancers-own", + "real-bobcats-train", + "red-cheetahs-heal", + "rude-llamas-notice", + "serious-bottles-tie", + "short-coins-enjoy", + "shy-eyes-march", + "slow-cars-press", + "smooth-knives-turn", + "spotty-seals-whisper", + "sweet-kiwis-scream", + "tame-weeks-shout", + "ten-stingrays-eat", + "thin-suns-invent", + "three-squids-brake", + "tidy-apes-fry", + "weak-books-tell", + "wild-carrots-know", + "witty-penguins-rush" + ] +} diff --git a/.changeset/proud-coats-repair.md b/.changeset/proud-coats-repair.md new file mode 100644 index 0000000000000..ba1e16b7e05d6 --- /dev/null +++ b/.changeset/proud-coats-repair.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fix the sorting by last chat in Contact Center table diff --git a/.changeset/rare-colts-repair.md b/.changeset/rare-colts-repair.md new file mode 100644 index 0000000000000..9011de6ff483c --- /dev/null +++ b/.changeset/rare-colts-repair.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue not rendering the proper error and empty state on users in role table diff --git a/.changeset/rare-dancers-own.md b/.changeset/rare-dancers-own.md new file mode 100644 index 0000000000000..358963661bef5 --- /dev/null +++ b/.changeset/rare-dancers-own.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Adds the missing `ignoreThreads` param fixing the issue not allowing ignoring threads when overriding retention policy diff --git a/.changeset/real-bobcats-train.md b/.changeset/real-bobcats-train.md new file mode 100644 index 0000000000000..6d51414c9fc4f --- /dev/null +++ b/.changeset/real-bobcats-train.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': patch +'@rocket.chat/meteor': patch +--- + +Don't show Join default channels option on edit user form. diff --git a/.changeset/red-cheetahs-heal.md b/.changeset/red-cheetahs-heal.md new file mode 100644 index 0000000000000..5b9934203da06 --- /dev/null +++ b/.changeset/red-cheetahs-heal.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes a cosmetic issue where emoji picker object and symbols category icon are swapped diff --git a/.changeset/rude-llamas-notice.md b/.changeset/rude-llamas-notice.md new file mode 100644 index 0000000000000..90c0ca3bd20ae --- /dev/null +++ b/.changeset/rude-llamas-notice.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +"@rocket.chat/omnichannel-services": patch +--- + +Added a new setting `Restrict files access to users who can access room` that controls file visibility. This new setting allows users that "can access a room" to also download the files that are there. This is specially important for users with livechat manager or monitor roles, or agents that have special permissions to view closed rooms, since this allows them to download files on the conversation even after the conversation is closed. +New setting is disabled by default and it is mutually exclusive with the setting `Restrict file access to room members` since this allows _more_ types of users to download files. diff --git a/.changeset/serious-bottles-tie.md b/.changeset/serious-bottles-tie.md new file mode 100644 index 0000000000000..e12bb94a53106 --- /dev/null +++ b/.changeset/serious-bottles-tie.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fix user not being set as online when setting "Use REST instead of websocket for Meteor calls" is disabled diff --git a/.changeset/short-coins-enjoy.md b/.changeset/short-coins-enjoy.md new file mode 100644 index 0000000000000..d47017030fcb4 --- /dev/null +++ b/.changeset/short-coins-enjoy.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed an issue where apps installed via the Marketplace would not be shown in the installed list if the app is unpublished diff --git a/.changeset/shy-eyes-march.md b/.changeset/shy-eyes-march.md new file mode 100644 index 0000000000000..398ab6eb11956 --- /dev/null +++ b/.changeset/shy-eyes-march.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed `EditRoomInfo` encrypted field placement diff --git a/.changeset/slow-cars-press.md b/.changeset/slow-cars-press.md new file mode 100644 index 0000000000000..de4d08ff52ff7 --- /dev/null +++ b/.changeset/slow-cars-press.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Introduced a new setting which doesn't allow users to access encrypted rooms until E2EE is configured and also doesn't allow users to send un-encrypted messages in encrypted rooms. + +New room setup for E2EE feature which helps users to setup their E2EE keys and introduced states to E2EE feature. diff --git a/.changeset/smooth-knives-turn.md b/.changeset/smooth-knives-turn.md new file mode 100644 index 0000000000000..3964ecc8481b8 --- /dev/null +++ b/.changeset/smooth-knives-turn.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +--- + +Executing a logout and login action in the same "tab/instance", some streams were not being recreated, causing countless types of bugs. + +PS: as a workaround reloading after logout or login in also solves the problem. diff --git a/.changeset/spotty-seals-whisper.md b/.changeset/spotty-seals-whisper.md new file mode 100644 index 0000000000000..242b5f6dde636 --- /dev/null +++ b/.changeset/spotty-seals-whisper.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/uikit-playground': minor +'@rocket.chat/meteor': minor +--- + +Upgrades fuselage-toastbar version in order to add RTL support to the component diff --git a/.changeset/sweet-kiwis-scream.md b/.changeset/sweet-kiwis-scream.md new file mode 100644 index 0000000000000..95a094f2ac402 --- /dev/null +++ b/.changeset/sweet-kiwis-scream.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Clicking on a message attachment link in the Desktop App will now initiate a direct download of the attachment only when the attachment is not a PDF file diff --git a/.changeset/tame-weeks-shout.md b/.changeset/tame-weeks-shout.md new file mode 100644 index 0000000000000..72bfc864274ff --- /dev/null +++ b/.changeset/tame-weeks-shout.md @@ -0,0 +1,41 @@ +--- +'@rocket.chat/omnichannel-services': minor +'rocketchat-services': minor +'@rocket.chat/omnichannel-transcript': minor +'@rocket.chat/authorization-service': minor +'@rocket.chat/web-ui-registration': minor +'@rocket.chat/stream-hub-service': minor +'@rocket.chat/uikit-playground': minor +'@rocket.chat/presence-service': minor +'@rocket.chat/fuselage-ui-kit': minor +'@rocket.chat/instance-status': minor +'@rocket.chat/account-service': minor +'@rocket.chat/mock-providers': minor +'@rocket.chat/api-client': minor +'@rocket.chat/ddp-client': minor +'@rocket.chat/pdf-worker': minor +'@rocket.chat/ui-theming': minor +'@rocket.chat/core-services': minor +'@rocket.chat/model-typings': minor +'@rocket.chat/ui-video-conf': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/ddp-streamer': minor +'@rocket.chat/queue-worker': minor +'@rocket.chat/presence': minor +'@rocket.chat/ui-composer': minor +'@rocket.chat/ui-contexts': minor +'@rocket.chat/license': minor +'@rocket.chat/gazzodown': minor +'@rocket.chat/ui-avatar': minor +'@rocket.chat/ui-client': minor +'@rocket.chat/livechat': minor +'@rocket.chat/models': minor +'@rocket.chat/ui-kit': minor +'@rocket.chat/apps': minor +'@rocket.chat/cron': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +New runtime for apps in the Apps-Engine based on the Deno platform diff --git a/.changeset/ten-stingrays-eat.md b/.changeset/ten-stingrays-eat.md new file mode 100644 index 0000000000000..8a8988bd77c2d --- /dev/null +++ b/.changeset/ten-stingrays-eat.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/core-typings": minor +"@rocket.chat/i18n": minor +--- + +Added the possibility to choose the time unit (days, hours, minutes) to the global retention policy settings diff --git a/.changeset/thin-suns-invent.md b/.changeset/thin-suns-invent.md new file mode 100644 index 0000000000000..945f44420797d --- /dev/null +++ b/.changeset/thin-suns-invent.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes issues causing nonstop sound notification when taking a chat from the `Current Chats` view diff --git a/.changeset/three-squids-brake.md b/.changeset/three-squids-brake.md new file mode 100644 index 0000000000000..89ed21f8048c8 --- /dev/null +++ b/.changeset/three-squids-brake.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed Encrypted thread main message reactivity issues. Earlier the encrypted thread main message was having some reactivity issues and flaky behavior. diff --git a/.changeset/tidy-apes-fry.md b/.changeset/tidy-apes-fry.md new file mode 100644 index 0000000000000..ee3922fa350c2 --- /dev/null +++ b/.changeset/tidy-apes-fry.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed inverted navigation direction in the image gallery diff --git a/.changeset/weak-books-tell.md b/.changeset/weak-books-tell.md new file mode 100644 index 0000000000000..675901263f31d --- /dev/null +++ b/.changeset/weak-books-tell.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Forces the highlight code language registration, preventing it to not being available when trying to use on the UI diff --git a/.changeset/wild-carrots-know.md b/.changeset/wild-carrots-know.md new file mode 100644 index 0000000000000..6403b5fd0bfd5 --- /dev/null +++ b/.changeset/wild-carrots-know.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Remove password change reason when the `request password change` option is set to false diff --git a/.changeset/witty-penguins-rush.md b/.changeset/witty-penguins-rush.md new file mode 100644 index 0000000000000..632026d6fe2e1 --- /dev/null +++ b/.changeset/witty-penguins-rush.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Security Hotfix (https://docs.rocket.chat/guides/security/security-updates) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 48b0af912d15f..a834776aeff5c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -28,6 +28,6 @@ apps/meteor/server/startup/migrations @RocketChat/Architecture /apps/meteor/ee/app/canned-responses @RocketChat/omnichannel /apps/meteor/ee/app/livechat @RocketChat/omnichannel /apps/meteor/ee/app/livechat-enterprise @RocketChat/omnichannel -/apps/meteor/ee/client/omnichannel @RocketChat/omnichannel +/apps/meteor/client/omnichannel @RocketChat/omnichannel /apps/meteor/client/components/omnichannel @RocketChat/omnichannel /apps/meteor/client/components/voip @RocketChat/omnichannel diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index 0e921e81f1f33..caa3c63e00f04 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -10,6 +10,10 @@ inputs: install: required: false type: boolean + deno-dir: + required: false + type: string + default: ~/.deno-cache outputs: node-version: @@ -19,6 +23,9 @@ runs: using: composite steps: + - run: echo 'DENO_DIR=${{ inputs.deno-dir }}' >> $GITHUB_ENV + shell: bash + - name: Cache Node Modules if: inputs.cache-modules id: cache-node-modules @@ -26,6 +33,7 @@ runs: with: path: | node_modules + ${{ env.DENO_DIR }} apps/meteor/node_modules apps/meteor/ee/server/services/node_modules key: node-modules-${{ hashFiles('yarn.lock') }} diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 1dc8993bfa870..b46c124d149be 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -90,6 +90,13 @@ jobs: name: MongoDB ${{ matrix.mongodb-version }}${{ inputs.db-watcher-disabled == 'true' && ' [no watchers]' || '' }} (${{ matrix.shard }}/${{ inputs.total-shard }})${{ matrix.mongodb-version == '6.0' && ' - Alpine' || '' }} steps: + - name: Collect Workflow Telemetry + uses: catchpoint/workflow-telemetry-action@v2 + with: + theme: dark + job_summary: true + comment_on_pr: false + - name: Login to GitHub Container Registry if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') uses: docker/login-action@v2 diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml index 066cc2e3773ea..bfb22ffa4e731 100644 --- a/.github/workflows/ci-test-unit.yml +++ b/.github/workflows/ci-test-unit.yml @@ -6,6 +6,9 @@ on: node-version: required: true type: string + enterprise-license: + required: false + type: string secrets: CODECOV_TOKEN: required: false @@ -13,6 +16,7 @@ on: env: MONGO_URL: mongodb://localhost:27017/rocketchat?replicaSet=rs0&directConnection=true TOOL_NODE_FLAGS: ${{ vars.TOOL_NODE_FLAGS }} + ENTERPRISE_LICENSE: ${{ inputs.enterprise-license }} jobs: test: @@ -21,6 +25,12 @@ jobs: name: Unit Tests steps: + - name: Collect Workflow Telemetry + uses: catchpoint/workflow-telemetry-action@v2 + with: + theme: dark + job_summary: true + comment_on_pr: false - uses: actions/checkout@v4 - name: Setup NodeJS diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b16ab459d6bd4..b542cfbf65232 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,8 +32,8 @@ jobs: rc-docker-tag-alpine: '${{ steps.docker.outputs.gh-docker-tag }}.alpine' node-version: ${{ steps.var.outputs.node-version }} # this is 100% intentional, secrets are not available for forks, so ee-tests will always fail - # to avoid this, we are using a dummy license, expiring at 2024-06-30 - enterprise-license: WMa5i+/t/LZbYOj8u3XUkivRhWBtWO6ycUjaZoVAw2DxMfdyBIAa2gMMI4x7Z2BrTZIZhFEImfOxcXcgD0QbXHGBJaMI+eYG+eofnVWi2VA7RWbpvWTULgPFgyJ4UEFeCOzVjcBLTQbmMSam3u0RlekWJkfAO0KnmLtsaEYNNA2rz1U+CLI/CdNGfdqrBu5PZZbGkH0KEzyIZMaykOjzvX+C6vd7fRxh23HecwhkBbqE8eQsCBt2ad0qC4MoVXsDaSOmSzGW+aXjuXt/9zjvrLlsmWQTSlkrEHdNkdywm0UkGxqz3+CP99n0WggUBioUiChjMuNMoceWvDvmxYP9Ml2NpYU7SnfhjmMFyXOah8ofzv8w509Y7XODvQBz+iB4Co9YnF3vT96HDDQyAV5t4jATE+0t37EAXmwjTi3qqyP7DLGK/revl+mlcwJ5kS4zZBsm1E4519FkXQOZSyWRnPdjqvh4mCLqoispZ49wKvklDvjPxCSP9us6cVXLDg7NTJr/4pfxLPOkvv7qCgugDvlDx17bXpQFPSDxmpw66FLzvb5Id0dkWjOzrRYSXb0bFWoUQjtHFzmcpFkyVhOKrQ9zA9+Zm7vXmU9Y2l2dK79EloOuHMSYAqsPEag8GMW6vI/cT4iIjHGGDePKnD0HblvTEKzql11cfT/abf2IiaY= + # to avoid this, we are using a dummy license, expiring at 2025-06-31 + enterprise-license: X/XumwIkgwQuld0alWKt37lVA90XjKOrfiMvMZ0/RtqsMtrdL9GoAk+4jXnaY1b2ePoG7XSzGhuxEDxFKIWJK3hIKGNTvrd980LgH5sM5+1T4P42ivSpd8UZi0bwjJkCFLIu9RozzYwslGG0IehMxe0S6VjcO0UYlUJtbMCBHuR2WmTAmO6YVU3ln+pZCbrPFaTPSS1RovhKaNCNkZwIx/CLWW8UTXUuFV/ML4PbKKVoa5nvvJwPeatgL7UCnlSD90lfCiiuikpzj/Y/JLkIL6velFbwNxsrxg9iRJ2k0sKheMMSmlTiGzSvZUm+na5WQq91aKGncih+DmaEZA7QGrjp4eoA0dqTk6OmItsy0fHmQhvZIOKNMeO7vNQiLbaSV6rqibrzu7WPpeIvsvL57T1h37USoCSB6+jDqkzdfoqIpz8BxTiJDj1d8xGPJFVrgxoqQqkj9qIP/gCaEz5DF39QFv5sovk4yK2O8fEQYod2d14V9yECYl4szZPMk1IBfCAC2w7czWGHHFonhL+CQGT403y5wmDmnsnjlCqMKF72odqfTPTI8XnCvJDriPMWohnQEAGtTTyciAhNokx/mjAVJ4NeZPcsbm4BjhvJvnjxx/BhYhBBTNWPaCSZzocfrGUj9Z+ZA7BEz+xAFQyGDx3xRzqIXfT0G7w8fvgYJMU= steps: - uses: Bhacaz/checkout-files@v2 with: @@ -168,6 +168,12 @@ jobs: runs-on: ubuntu-20.04 steps: + - name: Collect Workflow Telemetry + uses: catchpoint/workflow-telemetry-action@v2 + with: + theme: dark + job_summary: true + comment_on_pr: false - name: Github Info run: | echo "GITHUB_ACTION: $GITHUB_ACTION" @@ -192,6 +198,12 @@ jobs: runs-on: ubuntu-20.04 steps: + - name: Collect Workflow Telemetry + uses: catchpoint/workflow-telemetry-action@v2 + with: + theme: dark + job_summary: true + comment_on_pr: false - name: Github Info run: | echo "GITHUB_ACTION: $GITHUB_ACTION" @@ -289,6 +301,7 @@ jobs: uses: ./.github/workflows/ci-test-unit.yml with: node-version: ${{ needs.release-versions.outputs.node-version }} + enterprise-license: ${{ needs.release-versions.outputs.enterprise-license }} secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/pr-title-checker.yml b/.github/workflows/pr-title-checker.yml index 356ac10c97593..bc9d1f042d58a 100644 --- a/.github/workflows/pr-title-checker.yml +++ b/.github/workflows/pr-title-checker.yml @@ -12,6 +12,6 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: thehanimo/pr-title-checker@v1.3.7 + - uses: thehanimo/pr-title-checker@v1.4.1 with: GITHUB_TOKEN: ${{ secrets.RC_TITLE_CHECKER }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index b074212964ebc..0ee119fe43aa8 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -11,8 +11,8 @@ jobs: steps: - uses: actions/stale@v5 with: - days-before-issue-stale: 10 - days-before-issue-close: 4 + days-before-issue-stale: 14 + days-before-issue-close: 14 any-of-labels: 'stat: need more info,stat: waiting response' stale-issue-label: "stat: no response" stale-issue-message: "This issue has been marked as stale because there has been no further activity in the last 10 days. If the issue remains stale for the next 4 days (a total of 14 days with no activity), then it will be assumed that the question has been resolved and the issue will be automatically closed." diff --git a/.vscode/settings.json b/.vscode/settings.json index 4eaf1836d1fde..2dcd055310d14 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,6 +20,7 @@ "typescript.tsdk": "./node_modules/typescript/lib", "cSpell.words": [ "autotranslate", + "ciphertext", "Contextualbar", "fname", "Gazzodown", diff --git a/apps/meteor/.docker/Dockerfile b/apps/meteor/.docker/Dockerfile index 456ed4becafd9..1e9ed3f5e5922 100644 --- a/apps/meteor/.docker/Dockerfile +++ b/apps/meteor/.docker/Dockerfile @@ -13,12 +13,24 @@ RUN groupadd -g 65533 -r rocketchat \ # --chown requires Docker 17.12 and works only on Linux ADD --chown=rocketchat:rocketchat . /app +# needs a mongoinstance - defaults to container linking with alias 'mongo' +ENV DEPLOY_METHOD=docker \ + NODE_ENV=production \ + MONGO_URL=mongodb://mongo:27017/rocketchat \ + HOME=/tmp \ + PORT=3000 \ + ROOT_URL=http://localhost:3000 \ + Accounts_AvatarStorePath=/app/uploads \ + DENO_DIR=/usr/share/deno + RUN aptMark="$(apt-mark showmanual)" \ && apt-get install -y --no-install-recommends g++ make python3 ca-certificates \ && cd /app/bundle/programs/server \ && npm install \ - && cd npm/node_modules/isolated-vm \ - && npm install \ + && cd npm/node_modules/isolated-vm \ + && npm install \ + && cd /app/bundle/programs/server/npm/node_modules/@rocket.chat/apps-engine/deno-runtime \ + && ../../../deno-bin/bin/deno cache main.ts \ && apt-mark auto '.*' > /dev/null \ && apt-mark manual $aptMark > /dev/null \ && find /usr/local -type f -executable -exec ldd '{}' ';' \ @@ -37,15 +49,6 @@ VOLUME /app/uploads WORKDIR /app/bundle -# needs a mongoinstance - defaults to container linking with alias 'mongo' -ENV DEPLOY_METHOD=docker \ - NODE_ENV=production \ - MONGO_URL=mongodb://mongo:27017/rocketchat \ - HOME=/tmp \ - PORT=3000 \ - ROOT_URL=http://localhost:3000 \ - Accounts_AvatarStorePath=/app/uploads - EXPOSE 3000 CMD ["node", "main.js"] diff --git a/apps/meteor/.docker/Dockerfile.alpine b/apps/meteor/.docker/Dockerfile.alpine index 94baef8092174..feebf76a03e70 100644 --- a/apps/meteor/.docker/Dockerfile.alpine +++ b/apps/meteor/.docker/Dockerfile.alpine @@ -1,13 +1,68 @@ FROM node:14.21.3-alpine3.16 -RUN apk add --no-cache ttf-dejavu +ENV LANG=C.UTF-8 + +# Installing glibc deps required by Deno +# This replaces libc6-compat +# Copied from https://github.com/Docker-Hub-frolvlad/docker-alpine-glibc, which denoland/deno:alpine-1.37.1 uses +# NOTE: Glibc 2.35 package is broken: https://github.com/sgerrand/alpine-pkg-glibc/issues/176, so we stick to 2.34 for now +RUN ALPINE_GLIBC_BASE_URL="https://github.com/sgerrand/alpine-pkg-glibc/releases/download" && \ + ALPINE_GLIBC_PACKAGE_VERSION="2.34-r0" && \ + ALPINE_GLIBC_BASE_PACKAGE_FILENAME="glibc-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \ + ALPINE_GLIBC_BIN_PACKAGE_FILENAME="glibc-bin-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \ + ALPINE_GLIBC_I18N_PACKAGE_FILENAME="glibc-i18n-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \ + apk add --no-cache --virtual=.build-dependencies wget ca-certificates && \ + echo \ + "-----BEGIN PUBLIC KEY-----\ + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApZ2u1KJKUu/fW4A25y9m\ + y70AGEa/J3Wi5ibNVGNn1gT1r0VfgeWd0pUybS4UmcHdiNzxJPgoWQhV2SSW1JYu\ + tOqKZF5QSN6X937PTUpNBjUvLtTQ1ve1fp39uf/lEXPpFpOPL88LKnDBgbh7wkCp\ + m2KzLVGChf83MS0ShL6G9EQIAUxLm99VpgRjwqTQ/KfzGtpke1wqws4au0Ab4qPY\ + KXvMLSPLUp7cfulWvhmZSegr5AdhNw5KNizPqCJT8ZrGvgHypXyiFvvAH5YRtSsc\ + Zvo9GI2e2MaZyo9/lvb+LbLEJZKEQckqRj4P26gmASrZEPStwc+yqy1ShHLA0j6m\ + 1QIDAQAB\ + -----END PUBLIC KEY-----" | sed 's/ */\n/g' > "/etc/apk/keys/sgerrand.rsa.pub" && \ + wget \ + "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \ + "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \ + "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \ + mv /etc/nsswitch.conf /etc/nsswitch.conf.bak && \ + apk add --no-cache --force-overwrite \ + "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \ + "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \ + "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \ + \ + mv /etc/nsswitch.conf.bak /etc/nsswitch.conf && \ + rm "/etc/apk/keys/sgerrand.rsa.pub" && \ + (/usr/glibc-compat/bin/localedef --force --inputfile POSIX --charmap UTF-8 "$LANG" || true) && \ + echo "export LANG=$LANG" > /etc/profile.d/locale.sh && \ + \ + apk del glibc-i18n && \ + \ + rm "/root/.wget-hsts" && \ + apk del .build-dependencies && \ + rm \ + "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \ + "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \ + "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \ + apk add --no-cache ttf-dejavu ADD . /app LABEL maintainer="buildmaster@rocket.chat" +# needs a mongo instance - defaults to container linking with alias 'mongo' +ENV DEPLOY_METHOD=docker \ + NODE_ENV=production \ + MONGO_URL=mongodb://mongo:27017/rocketchat \ + HOME=/tmp \ + PORT=3000 \ + ROOT_URL=http://localhost:3000 \ + Accounts_AvatarStorePath=/app/uploads \ + DENO_DIR=/usr/share/deno + RUN set -x \ - && apk add --no-cache --virtual .fetch-deps python3 make g++ libc6-compat \ + && apk add --no-cache --virtual .fetch-deps python3 make g++ \ && cd /app/bundle/programs/server \ && npm install --production \ # Start hack for sharp... @@ -20,20 +75,14 @@ RUN set -x \ && npm install isolated-vm@4.4.2 \ && mv node_modules/isolated-vm npm/node_modules/isolated-vm \ # End hack for isolated-vm - && cd npm \ + # Cache Deno dependencies for Apps-Engine + && cd npm/node_modules/@rocket.chat/apps-engine/deno-runtime \ + && /app/bundle/programs/server/npm/node_modules/deno-bin/bin/deno cache main.ts \ + && cd /app/bundle/programs/server/npm \ && npm rebuild bcrypt --build-from-source \ && npm cache clear --force \ && apk del .fetch-deps -# needs a mongo instance - defaults to container linking with alias 'mongo' -ENV DEPLOY_METHOD=docker \ - NODE_ENV=production \ - MONGO_URL=mongodb://mongo:27017/rocketchat \ - HOME=/tmp \ - PORT=3000 \ - ROOT_URL=http://localhost:3000 \ - Accounts_AvatarStorePath=/app/uploads - VOLUME /app/uploads WORKDIR /app/bundle diff --git a/apps/meteor/.meteor/packages b/apps/meteor/.meteor/packages index 8107c249add2a..307b0d89eb0dc 100644 --- a/apps/meteor/.meteor/packages +++ b/apps/meteor/.meteor/packages @@ -15,12 +15,12 @@ rocketchat:streamer rocketchat:version rocketchat:user-presence -accounts-base@2.2.10 +accounts-base@2.2.11 accounts-facebook@1.3.3 accounts-github@1.5.0 accounts-google@1.4.0 accounts-meteor-developer@1.5.0 -accounts-oauth@1.4.3 +accounts-oauth@1.4.4 accounts-password@2.4.0 accounts-twitter@1.5.0 @@ -29,20 +29,20 @@ google-oauth@1.4.4 oauth@2.2.1 oauth2@1.3.2 -check@1.3.2 +check@1.4.1 ddp-rate-limiter@1.2.1 rate-limit@1.1.1 -email@2.2.5 +email@2.2.6 http@2.0.0 meteor-base@1.5.1 -ddp-common@1.4.0 +ddp-common@1.4.1 webapp@1.13.8 -mongo@1.16.8 +mongo@1.16.10 reload@1.3.1 -service-configuration@1.3.3 +service-configuration@1.3.4 session@1.2.1 shell-server@0.5.0 diff --git a/apps/meteor/.meteor/release b/apps/meteor/.meteor/release index 966586ce54fe9..5152abe9d5821 100644 --- a/apps/meteor/.meteor/release +++ b/apps/meteor/.meteor/release @@ -1 +1 @@ -METEOR@2.15 +METEOR@2.16 diff --git a/apps/meteor/.meteor/versions b/apps/meteor/.meteor/versions index a4483a5cf40e1..416ae456f05bd 100644 --- a/apps/meteor/.meteor/versions +++ b/apps/meteor/.meteor/versions @@ -1,9 +1,9 @@ -accounts-base@2.2.10 +accounts-base@2.2.11 accounts-facebook@1.3.3 accounts-github@1.5.0 accounts-google@1.4.0 accounts-meteor-developer@1.5.0 -accounts-oauth@1.4.3 +accounts-oauth@1.4.4 accounts-password@2.4.0 accounts-twitter@1.5.0 allow-deny@1.1.1 @@ -15,14 +15,14 @@ binary-heap@1.0.11 boilerplate-generator@1.7.2 caching-compiler@1.2.2 callback-hook@1.5.1 -check@1.3.2 +check@1.4.1 coffeescript@2.7.0 coffeescript-compiler@2.4.1 ddp@1.4.1 -ddp-client@2.6.1 -ddp-common@1.4.0 +ddp-client@2.6.2 +ddp-common@1.4.1 ddp-rate-limiter@1.2.1 -ddp-server@2.7.0 +ddp-server@2.7.1 diff-sequence@1.1.2 dispatch:run-as-user@1.1.1 dynamic-import@0.7.3 @@ -31,7 +31,7 @@ ecmascript-runtime@0.8.1 ecmascript-runtime-client@0.12.1 ecmascript-runtime-server@0.11.0 ejson@1.1.3 -email@2.2.5 +email@2.2.6 es5-shim@4.8.0 facebook-oauth@1.11.3 facts-base@1.0.1 @@ -45,17 +45,17 @@ id-map@1.1.1 inter-process-messaging@0.1.1 kadira:flow-router@2.12.1 localstorage@1.2.0 -logging@1.3.3 +logging@1.3.4 meteor@1.11.5 meteor-base@1.5.1 meteor-developer-oauth@1.3.2 meteorhacks:inject-initial@1.0.5 minifier-css@1.6.4 -minimongo@1.9.3 +minimongo@1.9.4 modern-browsers@0.1.10 modules@0.20.0 modules-runtime@0.13.1 -mongo@1.16.8 +mongo@1.16.10 mongo-decimal@0.1.3 mongo-dev-server@1.1.0 mongo-id@1.0.8 @@ -84,7 +84,7 @@ rocketchat:streamer@1.1.0 rocketchat:user-presence@2.6.3 rocketchat:version@1.0.0 routepolicy@1.1.1 -service-configuration@1.3.3 +service-configuration@1.3.4 session@1.2.1 sha@1.0.9 shell-server@0.5.0 @@ -93,10 +93,10 @@ standard-minifier-css@1.9.2 tracker@1.3.3 twitter-oauth@1.3.3 typescript@4.9.5 -underscore@1.6.0 +underscore@1.6.1 url@1.3.2 webapp@1.13.8 webapp-hashing@1.1.1 zodern:caching-minifier@0.5.0 zodern:standard-minifier-js@5.3.1 -zodern:types@1.0.11 +zodern:types@1.0.13 diff --git a/apps/meteor/.storybook/main.js b/apps/meteor/.storybook/main.js index 0e0b6db7c0e98..d70d3c5d7cc38 100644 --- a/apps/meteor/.storybook/main.js +++ b/apps/meteor/.storybook/main.js @@ -7,7 +7,6 @@ module.exports = { '../client/**/*.stories.{js,tsx}', '../app/**/*.stories.{js,tsx}', '../ee/app/**/*.stories.{js,tsx}', - '../ee/client/**/*.stories.{js,tsx}', ], addons: [ '@storybook/addon-essentials', diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 6b76f8532ea73..db944dc791774 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,11 +1,388 @@ # @rocket.chat/meteor +## 6.10.0-rc.7 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.10.0-rc.7 + - @rocket.chat/rest-typings@6.10.0-rc.7 + - @rocket.chat/api-client@0.2.0-rc.7 + - @rocket.chat/license@0.2.0-rc.7 + - @rocket.chat/omnichannel-services@0.2.0-rc.7 + - @rocket.chat/pdf-worker@0.1.0-rc.7 + - @rocket.chat/presence@0.2.0-rc.7 + - @rocket.chat/apps@0.1.0-rc.7 + - @rocket.chat/core-services@0.4.0-rc.7 + - @rocket.chat/cron@0.1.0-rc.7 + - @rocket.chat/fuselage-ui-kit@8.0.0-rc.7 + - @rocket.chat/gazzodown@8.0.0-rc.7 + - @rocket.chat/model-typings@0.5.0-rc.7 + - @rocket.chat/ui-contexts@8.0.0-rc.7 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.1.0-rc.7 + - @rocket.chat/ui-theming@0.2.0-rc.0 + - @rocket.chat/ui-avatar@4.0.0-rc.7 + - @rocket.chat/ui-client@8.0.0-rc.7 + - @rocket.chat/ui-video-conf@8.0.0-rc.7 + - @rocket.chat/web-ui-registration@8.0.0-rc.7 + - @rocket.chat/instance-status@0.1.0-rc.7 +
+ +## 6.10.0-rc.6 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- ([#32696](https://github.com/RocketChat/Rocket.Chat/pull/32696)) Added the allowDiskUse option to the users page queries so that if the mongodb memory threshold is exceeded it will use disk space instead of throwing an error. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.10.0-rc.6 + - @rocket.chat/rest-typings@6.10.0-rc.6 + - @rocket.chat/api-client@0.2.0-rc.6 + - @rocket.chat/license@0.2.0-rc.6 + - @rocket.chat/omnichannel-services@0.2.0-rc.6 + - @rocket.chat/pdf-worker@0.1.0-rc.6 + - @rocket.chat/presence@0.2.0-rc.6 + - @rocket.chat/apps@0.1.0-rc.6 + - @rocket.chat/core-services@0.4.0-rc.6 + - @rocket.chat/cron@0.1.0-rc.6 + - @rocket.chat/fuselage-ui-kit@8.0.0-rc.6 + - @rocket.chat/gazzodown@8.0.0-rc.6 + - @rocket.chat/model-typings@0.5.0-rc.6 + - @rocket.chat/ui-contexts@8.0.0-rc.6 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.1.0-rc.6 + - @rocket.chat/ui-theming@0.2.0-rc.0 + - @rocket.chat/ui-avatar@4.0.0-rc.6 + - @rocket.chat/ui-client@8.0.0-rc.6 + - @rocket.chat/ui-video-conf@8.0.0-rc.6 + - @rocket.chat/web-ui-registration@8.0.0-rc.6 + - @rocket.chat/instance-status@0.1.0-rc.6 +
+ +## 6.10.0-rc.5 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.10.0-rc.5 + - @rocket.chat/rest-typings@6.10.0-rc.5 + - @rocket.chat/api-client@0.2.0-rc.5 + - @rocket.chat/license@0.2.0-rc.5 + - @rocket.chat/omnichannel-services@0.2.0-rc.5 + - @rocket.chat/pdf-worker@0.1.0-rc.5 + - @rocket.chat/presence@0.2.0-rc.5 + - @rocket.chat/apps@0.1.0-rc.5 + - @rocket.chat/core-services@0.4.0-rc.5 + - @rocket.chat/cron@0.1.0-rc.5 + - @rocket.chat/fuselage-ui-kit@8.0.0-rc.5 + - @rocket.chat/gazzodown@8.0.0-rc.5 + - @rocket.chat/model-typings@0.5.0-rc.5 + - @rocket.chat/ui-contexts@8.0.0-rc.5 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.1.0-rc.5 + - @rocket.chat/ui-theming@0.2.0-rc.0 + - @rocket.chat/ui-avatar@4.0.0-rc.5 + - @rocket.chat/ui-client@8.0.0-rc.5 + - @rocket.chat/ui-video-conf@8.0.0-rc.5 + - @rocket.chat/web-ui-registration@8.0.0-rc.5 + - @rocket.chat/instance-status@0.1.0-rc.5 +
+ +## 6.10.0-rc.4 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- ([#32690](https://github.com/RocketChat/Rocket.Chat/pull/32690)) Security Hotfix (https://docs.rocket.chat/guides/security/security-updates) + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.10.0-rc.4 + - @rocket.chat/rest-typings@6.10.0-rc.4 + - @rocket.chat/api-client@0.2.0-rc.4 + - @rocket.chat/license@0.2.0-rc.4 + - @rocket.chat/omnichannel-services@0.2.0-rc.4 + - @rocket.chat/pdf-worker@0.1.0-rc.4 + - @rocket.chat/presence@0.2.0-rc.4 + - @rocket.chat/apps@0.1.0-rc.4 + - @rocket.chat/core-services@0.4.0-rc.4 + - @rocket.chat/cron@0.1.0-rc.4 + - @rocket.chat/fuselage-ui-kit@8.0.0-rc.4 + - @rocket.chat/gazzodown@8.0.0-rc.4 + - @rocket.chat/model-typings@0.5.0-rc.4 + - @rocket.chat/ui-contexts@8.0.0-rc.4 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.1.0-rc.4 + - @rocket.chat/ui-theming@0.2.0-rc.0 + - @rocket.chat/ui-avatar@4.0.0-rc.4 + - @rocket.chat/ui-client@8.0.0-rc.4 + - @rocket.chat/ui-video-conf@8.0.0-rc.4 + - @rocket.chat/web-ui-registration@8.0.0-rc.4 + - @rocket.chat/instance-status@0.1.0-rc.4 +
+ ## 6.9.3 ### Patch Changes - Bump @rocket.chat/meteor version. +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.10.0-rc.3 + - @rocket.chat/rest-typings@6.10.0-rc.3 + - @rocket.chat/api-client@0.2.0-rc.3 + - @rocket.chat/license@0.2.0-rc.3 + - @rocket.chat/omnichannel-services@0.2.0-rc.3 + - @rocket.chat/pdf-worker@0.1.0-rc.3 + - @rocket.chat/presence@0.2.0-rc.3 + - @rocket.chat/apps@0.1.0-rc.3 + - @rocket.chat/core-services@0.4.0-rc.3 + - @rocket.chat/cron@0.1.0-rc.3 + - @rocket.chat/fuselage-ui-kit@8.0.0-rc.3 + - @rocket.chat/gazzodown@8.0.0-rc.3 + - @rocket.chat/model-typings@0.5.0-rc.3 + - @rocket.chat/ui-contexts@8.0.0-rc.3 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.1.0-rc.3 + - @rocket.chat/ui-theming@0.2.0-rc.0 + - @rocket.chat/ui-avatar@4.0.0-rc.3 + - @rocket.chat/ui-client@8.0.0-rc.3 + - @rocket.chat/ui-video-conf@8.0.0-rc.3 + - @rocket.chat/web-ui-registration@8.0.0-rc.3 + - @rocket.chat/instance-status@0.1.0-rc.3 +
+ +## 6.10.0-rc.3 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +## 6.10.0-rc.2 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.10.0-rc.2 + - @rocket.chat/rest-typings@6.10.0-rc.2 + - @rocket.chat/api-client@0.2.0-rc.2 + - @rocket.chat/license@0.2.0-rc.2 + - @rocket.chat/omnichannel-services@0.2.0-rc.2 + - @rocket.chat/pdf-worker@0.1.0-rc.2 + - @rocket.chat/presence@0.2.0-rc.2 + - @rocket.chat/apps@0.1.0-rc.2 + - @rocket.chat/core-services@0.4.0-rc.2 + - @rocket.chat/cron@0.1.0-rc.2 + - @rocket.chat/fuselage-ui-kit@8.0.0-rc.2 + - @rocket.chat/gazzodown@8.0.0-rc.2 + - @rocket.chat/model-typings@0.5.0-rc.2 + - @rocket.chat/ui-contexts@8.0.0-rc.2 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.1.0-rc.2 + - @rocket.chat/ui-theming@0.2.0-rc.0 + - @rocket.chat/ui-avatar@4.0.0-rc.2 + - @rocket.chat/ui-client@8.0.0-rc.2 + - @rocket.chat/ui-video-conf@8.0.0-rc.2 + - @rocket.chat/web-ui-registration@8.0.0-rc.2 + - @rocket.chat/instance-status@0.1.0-rc.2 +
+ +## 6.10.0-rc.1 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.10.0-rc.1 + - @rocket.chat/rest-typings@6.10.0-rc.1 + - @rocket.chat/api-client@0.2.0-rc.1 + - @rocket.chat/license@0.2.0-rc.1 + - @rocket.chat/omnichannel-services@0.2.0-rc.1 + - @rocket.chat/pdf-worker@0.1.0-rc.1 + - @rocket.chat/presence@0.2.0-rc.1 + - @rocket.chat/apps@0.1.0-rc.1 + - @rocket.chat/core-services@0.4.0-rc.1 + - @rocket.chat/cron@0.1.0-rc.1 + - @rocket.chat/fuselage-ui-kit@8.0.0-rc.1 + - @rocket.chat/gazzodown@8.0.0-rc.1 + - @rocket.chat/model-typings@0.5.0-rc.1 + - @rocket.chat/ui-contexts@8.0.0-rc.1 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.1.0-rc.1 + - @rocket.chat/ui-theming@0.2.0-rc.0 + - @rocket.chat/ui-avatar@4.0.0-rc.1 + - @rocket.chat/ui-client@8.0.0-rc.1 + - @rocket.chat/ui-video-conf@8.0.0-rc.1 + - @rocket.chat/web-ui-registration@8.0.0-rc.1 + - @rocket.chat/instance-status@0.1.0-rc.1 +
+ +## 6.10.0-rc.0 + +### Minor Changes + +- ([#32471](https://github.com/RocketChat/Rocket.Chat/pull/32471)) Removed "Unknown media type" errors on the client side by using `application/octet-stream` as a fallback media type (MIME type) for all files + +- ([#31859](https://github.com/RocketChat/Rocket.Chat/pull/31859)) Introduced the use of the `API_User_Limit` setting to limit amount of members to simultaneously auto-join a room in a team + +- ([#32551](https://github.com/RocketChat/Rocket.Chat/pull/32551)) Implement E2EE warning callouts letting users know that encrypted messages can't be searched and auditted on search contextual bar and audit panel. + +- ([#32446](https://github.com/RocketChat/Rocket.Chat/pull/32446)) Added E2EE room setup header, with just limited functionality and room actions. + +- ([#32552](https://github.com/RocketChat/Rocket.Chat/pull/32552)) Fixed an issue that would not allow the user to dismiss the closeToSeatsLimit banner for old workspaces + +- ([#31987](https://github.com/RocketChat/Rocket.Chat/pull/31987)) Implemented a new "Pending Users" tab on the users page to list users who have not yet been activated and/or have not logged in for the first time. + Additionally, added a "Pending Action" column to aid administrators in identifying necessary actions for each user. Incorporated a "Reason for Joining" field + into the user info contextual bar, along with a callout for exceeding the seats cap in the users page header. Finally, introduced a new logic to disable user creation buttons upon surpassing the seats cap. +- ([#32316](https://github.com/RocketChat/Rocket.Chat/pull/32316)) Support encrypted files on end-to-end encrypted rooms. + +- ([#32436](https://github.com/RocketChat/Rocket.Chat/pull/32436)) Added a "LDAP group validation strategy" setting to LDAP channels and roles sync in order to enable faster syncs + +- ([#32605](https://github.com/RocketChat/Rocket.Chat/pull/32605)) Moves the quotes to be on top of the message for better readability + +- ([#32197](https://github.com/RocketChat/Rocket.Chat/pull/32197)) Async End-to-End Encrypted rooms key distribution process. Users now don't need to be online to get the keys of their subscribed encrypted rooms, the key distribution process is now async and users can recieve keys even when they are not online. + +- ([#32559](https://github.com/RocketChat/Rocket.Chat/pull/32559)) Disable "Reply in direct message", "Copy link" and "Forward message" message menu items for encrypted messages as they don't apply to encrypted messages and also disable apps menu items and show a warning. + +- ([#32040](https://github.com/RocketChat/Rocket.Chat/pull/32040)) Introduced a new setting which doesn't allow users to access encrypted rooms until E2EE is configured and also doesn't allow users to send un-encrypted messages in encrypted rooms. + + New room setup for E2EE feature which helps users to setup their E2EE keys and introduced states to E2EE feature. + +- ([#32604](https://github.com/RocketChat/Rocket.Chat/pull/32604)) Upgrades fuselage-toastbar version in order to add RTL support to the component + +- ([#31974](https://github.com/RocketChat/Rocket.Chat/pull/31974)) Clicking on a message attachment link in the Desktop App will now initiate a direct download of the attachment only when the attachment is not a PDF file + +- ([#31821](https://github.com/RocketChat/Rocket.Chat/pull/31821)) New runtime for apps in the Apps-Engine based on the Deno platform + +- ([#32425](https://github.com/RocketChat/Rocket.Chat/pull/32425)) Added the possibility to choose the time unit (days, hours, minutes) to the global retention policy settings + +### Patch Changes + +- ([#32579](https://github.com/RocketChat/Rocket.Chat/pull/32579)) Improved Retention Policy Warning messages + +- ([#32152](https://github.com/RocketChat/Rocket.Chat/pull/32152)) Resolved an issue with the room type filter not being reset after navigating between admin sections. + +- ([#32478](https://github.com/RocketChat/Rocket.Chat/pull/32478)) Fixed "File Upload > Accepted Media Types" setting to allow all type of files uploads + +- ([#32489](https://github.com/RocketChat/Rocket.Chat/pull/32489)) Fixed streams being called when the user is not logged in + +- ([#32610](https://github.com/RocketChat/Rocket.Chat/pull/32610)) Fixes the supported versions problem, where in most cases the data chosen was the oldest + +- ([#32376](https://github.com/RocketChat/Rocket.Chat/pull/32376)) Fixed an issue with how the UI checked for permissions when deciding if editing or deleting a message by moderators users + +- ([#32459](https://github.com/RocketChat/Rocket.Chat/pull/32459)) Prevent usage of OTR messages with End-to-end Encryption, both feature shouldn't and can't work together. + +- ([#32563](https://github.com/RocketChat/Rocket.Chat/pull/32563)) fixes not being able to reinstall app after installation failure + +- ([#32499](https://github.com/RocketChat/Rocket.Chat/pull/32499)) Fixed codeBlock styles in light mode + +- ([#32380](https://github.com/RocketChat/Rocket.Chat/pull/32380)) Decrypt pinned encrypted messages in the chat and pinned messages contextual bar. + +- ([#32175](https://github.com/RocketChat/Rocket.Chat/pull/32175)) Fixed "Take it" button behavior disabling it when agent status is set to offline + +- ([#32472](https://github.com/RocketChat/Rocket.Chat/pull/32472)) When using `DISABLE_DB_WATCHERS=true` this fixes message updates with URLs that were missing the link preview. + +- ([#32587](https://github.com/RocketChat/Rocket.Chat/pull/32587)) Fixes issues with loading license modules when loading the page while logged out + +- ([#32452](https://github.com/RocketChat/Rocket.Chat/pull/32452)) Fixed 2 issues with `QueueInactivityMonitor` callback. This callback was in charge of scheduling the job that would close the inquiry, but it was checking on a property that didn't exist. This caused the callback to early return without scheduling the job, making the feature to not to work. + +- ([#32522](https://github.com/RocketChat/Rocket.Chat/pull/32522)) Changed streaming logic to prevent hidden system messages from being broadcasted through `stream-room-messages`. + +- ([#32624](https://github.com/RocketChat/Rocket.Chat/pull/32624)) Fixed an issue where private encrypted room creation was being forced even when E2EE feature was disabled. + +- ([#32428](https://github.com/RocketChat/Rocket.Chat/pull/32428)) Fixed an issue that allowed saveSettings method to save NaN values on numeric settings. + +- ([#32640](https://github.com/RocketChat/Rocket.Chat/pull/32640)) Fixes the issue where the modal backdrop is overlapping the options of the `Select` component + +- ([#32636](https://github.com/RocketChat/Rocket.Chat/pull/32636)) Fixed file name being incorrectly sent from the client when uploading assets + +- ([#32431](https://github.com/RocketChat/Rocket.Chat/pull/32431)) Fixed last message preview in Sidebar for E2E Ecrypted channels + +- ([#32547](https://github.com/RocketChat/Rocket.Chat/pull/32547)) Fixes the issue not allowing users without edit-room-retention-policy permission try to edit the room with the retention policy enabled + +- ([#32653](https://github.com/RocketChat/Rocket.Chat/pull/32653)) Prevent E2EE key reset on startup due to possible race conditions + +- ([#32625](https://github.com/RocketChat/Rocket.Chat/pull/32625)) Fixes an issue where settings code mirror is not being displayed correctly in full screen mode + +- ([#32548](https://github.com/RocketChat/Rocket.Chat/pull/32548)) Disable slash commands in encrypted rooms and show a disabled warning. + +- ([#32566](https://github.com/RocketChat/Rocket.Chat/pull/32566)) Fix the sorting by last chat in Contact Center table + +- ([#32412](https://github.com/RocketChat/Rocket.Chat/pull/32412)) Fixes an issue not rendering the proper error and empty state on users in role table + +- ([#32485](https://github.com/RocketChat/Rocket.Chat/pull/32485)) Adds the missing `ignoreThreads` param fixing the issue not allowing ignoring threads when overriding retention policy + +- ([#31750](https://github.com/RocketChat/Rocket.Chat/pull/31750)) Don't show Join default channels option on edit user form. + +- ([#32612](https://github.com/RocketChat/Rocket.Chat/pull/32612)) Fixes a cosmetic issue where emoji picker object and symbols category icon are swapped + +- ([#32329](https://github.com/RocketChat/Rocket.Chat/pull/32329)) Added a new setting `Restrict files access to users who can access room` that controls file visibility. This new setting allows users that "can access a room" to also download the files that are there. This is specially important for users with livechat manager or monitor roles, or agents that have special permissions to view closed rooms, since this allows them to download files on the conversation even after the conversation is closed. + New setting is disabled by default and it is mutually exclusive with the setting `Restrict file access to room members` since this allows _more_ types of users to download files. +- ([#32500](https://github.com/RocketChat/Rocket.Chat/pull/32500)) Fix user not being set as online when setting "Use REST instead of websocket for Meteor calls" is disabled + +- ([#32534](https://github.com/RocketChat/Rocket.Chat/pull/32534)) Fixed an issue where apps installed via the Marketplace would not be shown in the installed list if the app is unpublished + +- ([#32458](https://github.com/RocketChat/Rocket.Chat/pull/32458)) Fixed `EditRoomInfo` encrypted field placement + +- ([#32479](https://github.com/RocketChat/Rocket.Chat/pull/32479)) Executing a logout and login action in the same "tab/instance", some streams were not being recreated, causing countless types of bugs. + + PS: as a workaround reloading after logout or login in also solves the problem. + +- ([#32572](https://github.com/RocketChat/Rocket.Chat/pull/32572)) Fixes issues causing nonstop sound notification when taking a chat from the `Current Chats` view + +- ([#32381](https://github.com/RocketChat/Rocket.Chat/pull/32381)) Fixed Encrypted thread main message reactivity issues. Earlier the encrypted thread main message was having some reactivity issues and flaky behavior. + +- ([#32106](https://github.com/RocketChat/Rocket.Chat/pull/32106)) Fixed inverted navigation direction in the image gallery + +- ([#32507](https://github.com/RocketChat/Rocket.Chat/pull/32507)) Forces the highlight code language registration, preventing it to not being available when trying to use on the UI + +- ([#31363](https://github.com/RocketChat/Rocket.Chat/pull/31363)) Remove password change reason when the `request password change` option is set to false + +-
Updated dependencies [d3c493b6da, 02dd87574b, 16b67aa0ff, a565999ae0, 1056f220df, 1240c874a5, 768cad6de5, 2ef71e8ea6, 59df102d0c, eaf2f11a6c, 5f95c4ec6b, 363a011487, 495628bce0, f75a2cb4bb, 45dc3d5f72, ee43f2c57c, 07c4ca0621, 30399688fc, 4fd9c4cbaa, 4f72d62aa7, dfa49bdbb2]: + + - @rocket.chat/i18n@0.5.0-rc.0 + - @rocket.chat/fuselage-ui-kit@8.0.0-rc.0 + - @rocket.chat/ui-kit@0.35.0-rc.0 + - @rocket.chat/core-typings@6.10.0-rc.0 + - @rocket.chat/gazzodown@8.0.0-rc.0 + - @rocket.chat/model-typings@0.5.0-rc.0 + - @rocket.chat/rest-typings@6.10.0-rc.0 + - @rocket.chat/omnichannel-services@0.2.0-rc.0 + - @rocket.chat/web-ui-registration@8.0.0-rc.0 + - @rocket.chat/instance-status@0.1.0-rc.0 + - @rocket.chat/api-client@0.2.0-rc.0 + - @rocket.chat/pdf-worker@0.1.0-rc.0 + - @rocket.chat/ui-theming@0.2.0-rc.0 + - @rocket.chat/core-services@0.4.0-rc.0 + - @rocket.chat/ui-video-conf@8.0.0-rc.0 + - @rocket.chat/presence@0.2.0-rc.0 + - @rocket.chat/ui-composer@0.2.0-rc.0 + - @rocket.chat/ui-contexts@8.0.0-rc.0 + - @rocket.chat/license@0.2.0-rc.0 + - @rocket.chat/ui-avatar@4.0.0-rc.0 + - @rocket.chat/ui-client@8.0.0-rc.0 + - @rocket.chat/models@0.1.0-rc.0 + - @rocket.chat/apps@0.1.0-rc.0 + - @rocket.chat/cron@0.1.0-rc.0 + - @rocket.chat/server-cloud-communication@0.0.2 + - Bump @rocket.chat/meteor version. - ([#32683](https://github.com/RocketChat/Rocket.Chat/pull/32683) by [@dionisio-bot](https://github.com/dionisio-bot)) livechat `setDepartment` livechat api fixes: diff --git a/apps/meteor/app/2fa/server/functions/resetTOTP.ts b/apps/meteor/app/2fa/server/functions/resetTOTP.ts index 85fe696babe10..3be8ec7c80602 100644 --- a/apps/meteor/app/2fa/server/functions/resetTOTP.ts +++ b/apps/meteor/app/2fa/server/functions/resetTOTP.ts @@ -4,6 +4,7 @@ import { Meteor } from 'meteor/meteor'; import { i18n } from '../../../../server/lib/i18n'; import { isUserIdFederated } from '../../../../server/lib/isUserIdFederated'; +import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; @@ -68,6 +69,14 @@ export async function resetTOTP(userId: string, notifyUser = false): Promise({ // Once the TOTP is validated we logout all other clients const { 'x-auth-token': xAuthToken } = this.connection?.httpHeaders ?? {}; - if (xAuthToken) { + if (xAuthToken && this.userId) { const hashedToken = Accounts._hashLoginToken(xAuthToken); - if (!(await Users.removeNonPATLoginTokensExcept(this.userId, hashedToken))) { - throw new Meteor.Error('error-logging-out-other-clients', 'Error logging out other clients'); + const { modifiedCount } = await Users.removeNonPATLoginTokensExcept(this.userId, hashedToken); + + if (modifiedCount > 0) { + // TODO this can be optmized so places that care about loginTokens being removed are invoked directly + // instead of having to listen to every watch.users event + void notifyOnUserChangeAsync(async () => { + if (!this.userId) { + return; + } + const userTokens = await Users.findOneById(this.userId, { projection: { 'services.resume.loginTokens': 1 } }); + return { + clientAction: 'updated', + id: this.userId, + diff: { 'services.resume.loginTokens': userTokens?.services?.resume?.loginTokens }, + }; + }); } } diff --git a/apps/meteor/app/api/server/api.ts b/apps/meteor/app/api/server/api.ts index 87153440cd28b..fef2f2165ef22 100644 --- a/apps/meteor/app/api/server/api.ts +++ b/apps/meteor/app/api/server/api.ts @@ -14,9 +14,11 @@ import { Restivus } from 'meteor/rocketchat:restivus'; import _ from 'underscore'; import { isObject } from '../../../lib/utils/isObject'; +import { getNestedProp } from '../../../server/lib/getNestedProp'; import { getRestPayload } from '../../../server/lib/logger/logPayloads'; import { checkCodeForUser } from '../../2fa/server/code'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; +import { notifyOnUserChangeAsync } from '../../lib/server/lib/notifyListener'; import { metrics } from '../../metrics/server'; import { settings } from '../../settings/server'; import { getDefaultUserFields } from '../../utils/server/functions/getDefaultUserFields'; @@ -61,6 +63,7 @@ interface IAPIDefaultFieldsToExclude { statusDefault: number; _updatedAt: number; settings: number; + inviteToken: number; } type RateLimiterOptions = { @@ -147,6 +150,7 @@ export class APIClass extends Restivus { public limitedUserFieldsToExcludeIfIsPrivilegedUser: { services: number; + inviteToken: number; }; constructor(properties: IAPIProperties) { @@ -174,10 +178,12 @@ export class APIClass extends Restivus { statusDefault: 0, _updatedAt: 0, settings: 0, + inviteToken: 0, }; this.limitedUserFieldsToExclude = this.defaultLimitedUserFieldsToExclude; this.limitedUserFieldsToExcludeIfIsPrivilegedUser = { services: 0, + inviteToken: 0, }; } @@ -848,6 +854,19 @@ export class APIClass extends Restivus { }, ); + // TODO this can be optmized so places that care about loginTokens being removed are invoked directly + // instead of having to listen to every watch.users event + void notifyOnUserChangeAsync(async () => { + const userTokens = await Users.findOneById(this.user._id, { projection: { [tokenPath]: 1 } }); + if (!userTokens) { + return; + } + + const diff = { [tokenPath]: getNestedProp(userTokens, tokenPath) }; + + return { clientAction: 'updated', id: this.user._id, diff }; + }); + const response = { status: 'success', data: { diff --git a/apps/meteor/app/api/server/lib/emailInbox.ts b/apps/meteor/app/api/server/lib/emailInbox.ts index 663459c7bc052..304d297261af7 100644 --- a/apps/meteor/app/api/server/lib/emailInbox.ts +++ b/apps/meteor/app/api/server/lib/emailInbox.ts @@ -1,6 +1,8 @@ import type { IEmailInbox } from '@rocket.chat/core-typings'; import { EmailInbox, Users } from '@rocket.chat/models'; -import type { Filter, InsertOneResult, Sort, UpdateResult, WithId } from 'mongodb'; +import type { DeleteResult, Filter, InsertOneResult, Sort } from 'mongodb'; + +import { notifyOnEmailInboxChanged } from '../../../lib/server/lib/notifyListener'; export const findEmailInboxes = async ({ query = {}, @@ -34,33 +36,31 @@ export const findEmailInboxes = async ({ }; }; -export const findOneEmailInbox = async ({ _id }: { _id: string }): Promise => { - return EmailInbox.findOneById(_id); -}; export const insertOneEmailInbox = async ( userId: string, emailInboxParams: Pick, -): Promise>> => { +): Promise> => { const obj = { ...emailInboxParams, _createdAt: new Date(), _updatedAt: new Date(), _createdBy: await Users.findOneById(userId, { projection: { username: 1 } }), }; - return EmailInbox.insertOne(obj); + + const response = await EmailInbox.create(obj); + + if (response.insertedId) { + void notifyOnEmailInboxChanged({ _id: response.insertedId, ...obj }, 'inserted'); + } + + return response; }; export const updateEmailInbox = async ( emailInboxParams: Pick, -): Promise> | UpdateResult> => { +): Promise | null> => { const { _id, active, name, email, description, senderInfo, department, smtp, imap } = emailInboxParams; - const emailInbox = await findOneEmailInbox({ _id }); - - if (!emailInbox) { - throw new Error('error-invalid-email-inbox'); - } - const updateEmailInbox = { $set: { active, @@ -76,5 +76,29 @@ export const updateEmailInbox = async ( ...(department === 'All' && { $unset: { department: 1 as const } }), }; - return EmailInbox.updateOne({ _id }, updateEmailInbox); + const updatedResponse = await EmailInbox.updateById(_id, updateEmailInbox); + + if (!updatedResponse.value) { + throw new Error('error-invalid-email-inbox'); + } + + void notifyOnEmailInboxChanged( + { + ...updatedResponse.value, + ...(department === 'All' && { department: undefined }), + }, + 'updated', + ); + + return updatedResponse.value; +}; + +export const removeEmailInbox = async (emailInboxId: IEmailInbox['_id']): Promise => { + const removeResponse = await EmailInbox.removeById(emailInboxId); + + if (removeResponse.deletedCount) { + void notifyOnEmailInboxChanged({ _id: emailInboxId }, 'removed'); + } + + return removeResponse; }; diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.ts b/apps/meteor/app/api/server/lib/getUploadFormData.ts index 9b8f69fb3a66d..85fc0658542d4 100644 --- a/apps/meteor/app/api/server/lib/getUploadFormData.ts +++ b/apps/meteor/app/api/server/lib/getUploadFormData.ts @@ -5,6 +5,8 @@ import type { ValidateFunction } from 'ajv'; import busboy from 'busboy'; import type { Request } from 'express'; +import { getMimeType } from '../../../utils/lib/mimeTypes'; + type UploadResult = { file: Readable & { truncated: boolean }; fieldname: string; @@ -61,7 +63,7 @@ export async function getUploadFormData< function onFile( fieldname: string, file: Readable & { truncated: boolean }, - { filename, encoding, mimeType: mimetype }: { filename: string; encoding: string; mimeType: string }, + { filename, encoding }: { filename: string; encoding: string }, ) { if (options.field && fieldname !== options.field) { file.resume(); @@ -83,7 +85,7 @@ export async function getUploadFormData< file, filename, encoding, - mimetype, + mimetype: getMimeType(filename), fieldname, fields, fileBuffer: Buffer.concat(fileChunks), diff --git a/apps/meteor/app/api/server/lib/rooms.ts b/apps/meteor/app/api/server/lib/rooms.ts index 14d3e20502fda..3f1353be8a6c2 100644 --- a/apps/meteor/app/api/server/lib/rooms.ts +++ b/apps/meteor/app/api/server/lib/rooms.ts @@ -98,6 +98,7 @@ export async function findAdminRoomsAutocomplete({ uid, selector }: { uid: strin name: 1, t: 1, avatarETag: 1, + encrypted: 1, }, limit: 10, sort: { diff --git a/apps/meteor/app/api/server/lib/users.ts b/apps/meteor/app/api/server/lib/users.ts index f80d662771dfd..f8e3d528f1634 100644 --- a/apps/meteor/app/api/server/lib/users.ts +++ b/apps/meteor/app/api/server/lib/users.ts @@ -154,6 +154,7 @@ export async function findPaginatedUsersByStatus({ lastLogin: 1, type: 1, reason: 1, + federated: 1, }; const actualSort: Record = sort || { username: 1 }; @@ -204,6 +205,7 @@ export async function findPaginatedUsersByStatus({ skip: offset, limit: count, projection, + allowDiskUse: true, }, ); const [users, total] = await Promise.all([cursor.toArray(), totalCount]); diff --git a/apps/meteor/app/api/server/v1/banners.ts b/apps/meteor/app/api/server/v1/banners.ts index 4dc74208153b3..48c94d3711bdd 100644 --- a/apps/meteor/app/api/server/v1/banners.ts +++ b/apps/meteor/app/api/server/v1/banners.ts @@ -52,9 +52,8 @@ import { API } from '../api'; */ API.v1.addRoute( 'banners.getNew', - { authRequired: true }, + { authRequired: true, deprecation: { version: '8.0.0', alternatives: ['banners/:id', 'banners'] } }, { - // deprecated async get() { check( this.queryParams, diff --git a/apps/meteor/app/api/server/v1/e2e.ts b/apps/meteor/app/api/server/v1/e2e.ts index 8cb3e8ab4236e..74bd85dded6a0 100644 --- a/apps/meteor/app/api/server/v1/e2e.ts +++ b/apps/meteor/app/api/server/v1/e2e.ts @@ -1,13 +1,18 @@ import type { IUser } from '@rocket.chat/core-typings'; +import { Subscriptions } from '@rocket.chat/models'; import { ise2eGetUsersOfRoomWithoutKeyParamsGET, ise2eSetRoomKeyIDParamsPOST, ise2eSetUserPublicAndPrivateKeysParamsPOST, ise2eUpdateGroupKeyParamsPOST, + isE2EProvideUsersGroupKeyProps, + isE2EFetchUsersWaitingForGroupKeyProps, } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; import { handleSuggestedGroupKey } from '../../../e2e/server/functions/handleSuggestedGroupKey'; +import { provideUsersSuggestedGroupKeys } from '../../../e2e/server/functions/provideUsersSuggestedGroupKeys'; +import { settings } from '../../../settings/server'; import { API } from '../api'; API.v1.addRoute( @@ -113,6 +118,8 @@ API.v1.addRoute( * type: string * private_key: * type: string + * force: + * type: boolean * responses: * 200: * content: @@ -135,11 +142,12 @@ API.v1.addRoute( { async post() { // eslint-disable-next-line @typescript-eslint/naming-convention - const { public_key, private_key } = this.bodyParams; + const { public_key, private_key, force } = this.bodyParams; await Meteor.callAsync('e2e.setUserPublicAndPrivateKeys', { public_key, private_key, + force, }); return API.v1.success(); @@ -185,6 +193,9 @@ API.v1.addRoute( { authRequired: true, validateParams: ise2eUpdateGroupKeyParamsPOST, + deprecation: { + version: '8.0.0', + }, }, { async post() { @@ -230,3 +241,46 @@ API.v1.addRoute( }, }, ); + +API.v1.addRoute( + 'e2e.fetchUsersWaitingForGroupKey', + { + authRequired: true, + validateParams: isE2EFetchUsersWaitingForGroupKeyProps, + }, + { + async get() { + if (!settings.get('E2E_Enable')) { + return API.v1.success({ usersWaitingForE2EKeys: {} }); + } + + const { roomIds = [] } = this.queryParams; + const usersWaitingForE2EKeys = (await Subscriptions.findUsersWithPublicE2EKeyByRids(roomIds, this.userId).toArray()).reduce< + Record + >((acc, { rid, users }) => ({ [rid]: users, ...acc }), {}); + + return API.v1.success({ + usersWaitingForE2EKeys, + }); + }, + }, +); + +API.v1.addRoute( + 'e2e.provideUsersSuggestedGroupKeys', + { + authRequired: true, + validateParams: isE2EProvideUsersGroupKeyProps, + }, + { + async post() { + if (!settings.get('E2E_Enable')) { + return API.v1.success(); + } + + await provideUsersSuggestedGroupKeys(this.userId, this.bodyParams.usersSuggestedGroupKeys); + + return API.v1.success(); + }, + }, +); diff --git a/apps/meteor/app/api/server/v1/email-inbox.ts b/apps/meteor/app/api/server/v1/email-inbox.ts index 5748565a0f773..89ede496b78ac 100644 --- a/apps/meteor/app/api/server/v1/email-inbox.ts +++ b/apps/meteor/app/api/server/v1/email-inbox.ts @@ -4,7 +4,7 @@ import { check, Match } from 'meteor/check'; import { sendTestEmailToInbox } from '../../../../server/features/EmailInbox/EmailInbox_Outgoing'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; -import { insertOneEmailInbox, findEmailInboxes, findOneEmailInbox, updateEmailInbox } from '../lib/emailInbox'; +import { insertOneEmailInbox, findEmailInboxes, updateEmailInbox, removeEmailInbox } from '../lib/emailInbox'; API.v1.addRoute( 'email-inbox.list', @@ -55,12 +55,23 @@ API.v1.addRoute( let _id: string; if (!emailInboxParams?._id) { - const emailInbox = await insertOneEmailInbox(this.userId, emailInboxParams); - _id = emailInbox.insertedId.toString(); + const { insertedId } = await insertOneEmailInbox(this.userId, emailInboxParams); + + if (!insertedId) { + return API.v1.failure('Failed to create email inbox'); + } + + _id = insertedId; } else { - _id = emailInboxParams._id; - await updateEmailInbox({ ...emailInboxParams, _id }); + const emailInbox = await updateEmailInbox({ ...emailInboxParams, _id: emailInboxParams._id }); + + if (!emailInbox?._id) { + return API.v1.failure('Failed to update email inbox'); + } + + _id = emailInbox._id; } + return API.v1.success({ _id }); }, }, @@ -79,7 +90,7 @@ API.v1.addRoute( if (!_id) { throw new Error('error-invalid-param'); } - const emailInbox = await findOneEmailInbox({ _id }); + const emailInbox = await EmailInbox.findOneById(_id); if (!emailInbox) { return API.v1.notFound(); @@ -97,11 +108,12 @@ API.v1.addRoute( throw new Error('error-invalid-param'); } - const emailInboxes = await EmailInbox.findOneById(_id); - if (!emailInboxes) { + const { deletedCount } = await removeEmailInbox(_id); + + if (!deletedCount) { return API.v1.notFound(); } - await EmailInbox.removeById(_id); + return API.v1.success({ _id }); }, }, @@ -120,7 +132,7 @@ API.v1.addRoute( // TODO: Chapter day backend - check if user has permission to view this email inbox instead of null values // TODO: Chapter day: Remove this endpoint and move search to GET /email-inbox - const emailInbox = await EmailInbox.findOne({ email }); + const emailInbox = await EmailInbox.findByEmail(email); return API.v1.success({ emailInbox }); }, @@ -140,7 +152,7 @@ API.v1.addRoute( if (!_id) { throw new Error('error-invalid-param'); } - const emailInbox = await findOneEmailInbox({ _id }); + const emailInbox = await EmailInbox.findOneById(_id); if (!emailInbox) { return API.v1.notFound(); diff --git a/apps/meteor/app/api/server/v1/misc.ts b/apps/meteor/app/api/server/v1/misc.ts index bdf6fa2dd1c61..dd4da47bff052 100644 --- a/apps/meteor/app/api/server/v1/misc.ts +++ b/apps/meteor/app/api/server/v1/misc.ts @@ -24,6 +24,7 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; import { getLogs } from '../../../../server/stream/stdout'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { passwordPolicy } from '../../../lib/server'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { getDefaultUserFields } from '../../../utils/server/functions/getDefaultUserFields'; import { isSMTPConfigured } from '../../../utils/server/functions/isSMTPConfigured'; @@ -687,27 +688,49 @@ API.v1.addRoute( setDeploymentAs: String, }); + const settingsIds: string[] = []; + if (this.bodyParams.setDeploymentAs === 'new-workspace') { - await Promise.all([ - Settings.resetValueById('uniqueID', process.env.DEPLOYMENT_ID || uuidv4()), - // Settings.resetValueById('Cloud_Url'), - Settings.resetValueById('Cloud_Service_Agree_PrivacyTerms'), - Settings.resetValueById('Cloud_Workspace_Id'), - Settings.resetValueById('Cloud_Workspace_Name'), - Settings.resetValueById('Cloud_Workspace_Client_Id'), - Settings.resetValueById('Cloud_Workspace_Client_Secret'), - Settings.resetValueById('Cloud_Workspace_Client_Secret_Expires_At'), - Settings.resetValueById('Cloud_Workspace_Registration_Client_Uri'), - Settings.resetValueById('Cloud_Workspace_PublicKey'), - Settings.resetValueById('Cloud_Workspace_License'), - Settings.resetValueById('Cloud_Workspace_Had_Trial'), - Settings.resetValueById('Cloud_Workspace_Access_Token'), - Settings.resetValueById('Cloud_Workspace_Access_Token_Expires_At', new Date(0)), - Settings.resetValueById('Cloud_Workspace_Registration_State'), - ]); + settingsIds.push( + 'Cloud_Service_Agree_PrivacyTerms', + 'Cloud_Workspace_Id', + 'Cloud_Workspace_Name', + 'Cloud_Workspace_Client_Id', + 'Cloud_Workspace_Client_Secret', + 'Cloud_Workspace_Client_Secret_Expires_At', + 'Cloud_Workspace_Registration_Client_Uri', + 'Cloud_Workspace_PublicKey', + 'Cloud_Workspace_License', + 'Cloud_Workspace_Had_Trial', + 'Cloud_Workspace_Access_Token', + 'uniqueID', + 'Cloud_Workspace_Access_Token_Expires_At', + ); } - await Settings.updateValueById('Deployment_FingerPrint_Verified', true); + settingsIds.push('Deployment_FingerPrint_Verified'); + + const promises = settingsIds.map((settingId) => { + if (settingId === 'uniqueID') { + return Settings.resetValueById('uniqueID', process.env.DEPLOYMENT_ID || uuidv4()); + } + + if (settingId === 'Cloud_Workspace_Access_Token_Expires_At') { + return Settings.resetValueById('Cloud_Workspace_Access_Token_Expires_At', new Date(0)); + } + + if (settingId === 'Deployment_FingerPrint_Verified') { + return Settings.updateValueById('Deployment_FingerPrint_Verified', true); + } + + return Settings.resetValueById(settingId); + }); + + (await Promise.all(promises)).forEach((value, index) => { + if (value?.modifiedCount) { + void notifyOnSettingChangedById(settingsIds[index]); + } + }); return API.v1.success({}); }, diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 40998201b03bd..e3296b98ef178 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -6,6 +6,7 @@ import { isGETRoomsNameExists, isRoomsImagesProps, isRoomsMuteUnmuteUserProps, i import { Meteor } from 'meteor/meteor'; import { isTruthy } from '../../../../lib/isTruthy'; +import { omit } from '../../../../lib/utils/omit'; import * as dataExport from '../../../../server/lib/dataExport'; import { eraseRoom } from '../../../../server/methods/eraseRoom'; import { muteUserInRoom } from '../../../../server/methods/muteUserInRoom'; @@ -141,7 +142,13 @@ API.v1.addRoute( API.v1.addRoute( 'rooms.upload/:rid', - { authRequired: true }, + { + authRequired: true, + deprecation: { + version: '8.0.0', + alternatives: ['rooms.media'], + }, + }, { async post() { if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { @@ -194,6 +201,112 @@ API.v1.addRoute( }, ); +API.v1.addRoute( + 'rooms.media/:rid', + { authRequired: true }, + { + async post() { + if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { + return API.v1.unauthorized(); + } + + const file = await getUploadFormData( + { + request: this.request, + }, + { field: 'file', sizeLimit: settings.get('FileUpload_MaxFileSize') }, + ); + + if (!file) { + throw new Meteor.Error('invalid-field'); + } + + let { fileBuffer } = file; + + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + 24); + + const { fields } = file; + + let content; + + if (fields.content) { + try { + content = JSON.parse(fields.content); + } catch (e) { + console.error(e); + throw new Meteor.Error('invalid-field-content'); + } + } + + const details = { + name: file.filename, + size: fileBuffer.length, + type: file.mimetype, + rid: this.urlParams.rid, + userId: this.userId, + content, + expiresAt, + }; + + const stripExif = settings.get('Message_Attachments_Strip_Exif'); + if (stripExif) { + // No need to check mime. Library will ignore any files without exif/xmp tags (like BMP, ico, PDF, etc) + fileBuffer = await Media.stripExifFromBuffer(fileBuffer); + } + + const fileStore = FileUpload.getStore('Uploads'); + const uploadedFile = await fileStore.insert(details, fileBuffer); + + uploadedFile.path = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); + + await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); + + return API.v1.success({ + file: { + _id: uploadedFile._id, + url: uploadedFile.path, + }, + }); + }, + }, +); + +API.v1.addRoute( + 'rooms.mediaConfirm/:rid/:fileId', + { authRequired: true }, + { + async post() { + if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { + return API.v1.unauthorized(); + } + + const file = await Uploads.findOneById(this.urlParams.fileId); + + if (!file) { + throw new Meteor.Error('invalid-file'); + } + + file.description = this.bodyParams.description; + delete this.bodyParams.description; + + await sendFileMessage( + this.userId, + { roomId: this.urlParams.rid, file, msgData: this.bodyParams }, + { parseAttachmentsForE2EE: false }, + ); + + await Uploads.confirmTemporaryFile(this.urlParams.fileId, this.userId); + + const message = await Messages.getMessageByFileIdAndUsername(file._id, this.userId); + + return API.v1.success({ + message, + }); + }, + }, +); + API.v1.addRoute( 'rooms.saveNotification', { authRequired: true }, diff --git a/apps/meteor/app/api/server/v1/settings.ts b/apps/meteor/app/api/server/v1/settings.ts index bccfc8d91fc7b..574f4ee64194f 100644 --- a/apps/meteor/app/api/server/v1/settings.ts +++ b/apps/meteor/app/api/server/v1/settings.ts @@ -13,6 +13,7 @@ import type { FindOptions } from 'mongodb'; import _ from 'underscore'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { notifyOnSettingChanged, notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { SettingsEvents, settings } from '../../../settings/server'; import { setValue } from '../../../settings/server/raw'; import { API } from '../api'; @@ -186,23 +187,34 @@ API.v1.addRoute( } if (isSettingColor(setting) && isSettingsUpdatePropsColor(this.bodyParams)) { - await Settings.updateOptionsById(this.urlParams._id, { - editor: this.bodyParams.editor, - }); - await Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value); + const updateOptionsPromise = Settings.updateOptionsById(this.urlParams._id, { editor: this.bodyParams.editor }); + const updateValuePromise = Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value); + + const [updateOptionsResult, updateValueResult] = await Promise.all([updateOptionsPromise, updateValuePromise]); + + if (updateOptionsResult.modifiedCount || updateValueResult.modifiedCount) { + await notifyOnSettingChangedById(this.urlParams._id); + } + return API.v1.success(); } - if ( - isSettingsUpdatePropDefault(this.bodyParams) && - (await Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value)) - ) { + if (isSettingsUpdatePropDefault(this.bodyParams)) { + const { matchedCount } = await Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value); + if (!matchedCount) { + return API.v1.failure(); + } + const s = await Settings.findOneNotHiddenById(this.urlParams._id); if (!s) { return API.v1.failure(); } + settings.set(s); setValue(this.urlParams._id, this.bodyParams.value); + + await notifyOnSettingChanged(s); + return API.v1.success(); } diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index ccca23f8ea823..c26957fa19910 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -43,6 +43,7 @@ import { setStatusText } from '../../../lib/server/functions/setStatusText'; import { setUserAvatar } from '../../../lib/server/functions/setUserAvatar'; import { setUsernameWithValidation } from '../../../lib/server/functions/setUsername'; import { validateCustomFields } from '../../../lib/server/functions/validateCustomFields'; +import { notifyOnUserChange, notifyOnUserChangeAsync } from '../../../lib/server/lib/notifyListener'; import { generateAccessToken } from '../../../lib/server/methods/createToken'; import { settings } from '../../../settings/server'; import { getURL } from '../../../utils/server/getURL'; @@ -387,7 +388,8 @@ API.v1.addRoute( const lastLoggedIn = new Date(); lastLoggedIn.setDate(lastLoggedIn.getDate() - daysIdle); - const count = (await Users.setActiveNotLoggedInAfterWithRole(lastLoggedIn, role, false)).modifiedCount; + // since we're deactiving users that are not logged in, there is no need to send data through WS + const { modifiedCount: count } = await Users.setActiveNotLoggedInAfterWithRole(lastLoggedIn, role, false); return API.v1.success({ count, @@ -861,14 +863,31 @@ API.v1.addRoute( // When 2FA is enable we logout all other clients const xAuthToken = this.request.headers['x-auth-token'] as string; - if (xAuthToken) { - const hashedToken = Accounts._hashLoginToken(xAuthToken); + if (!xAuthToken) { + return API.v1.success(); + } - if (!(await Users.removeNonPATLoginTokensExcept(this.userId, hashedToken))) { - throw new MeteorError('error-logging-out-other-clients', 'Error logging out other clients'); - } + const hashedToken = Accounts._hashLoginToken(xAuthToken); + + if (!(await Users.removeNonPATLoginTokensExcept(this.userId, hashedToken))) { + throw new MeteorError('error-logging-out-other-clients', 'Error logging out other clients'); } + // TODO this can be optmized so places that care about loginTokens being removed are invoked directly + // instead of having to listen to every watch.users event + void notifyOnUserChangeAsync(async () => { + const userTokens = await Users.findOneById(this.userId, { projection: { 'services.resume.loginTokens': 1 } }); + if (!userTokens) { + return; + } + + return { + clientAction: 'updated', + id: this.user._id, + diff: { 'services.resume.loginTokens': userTokens.services?.resume?.loginTokens }, + }; + }); + return API.v1.success(); }, }, @@ -1021,6 +1040,12 @@ API.v1.addRoute( const me = (await Users.findOneById(this.userId, { projection: { 'services.resume.loginTokens': 1 } })) as Pick; + void notifyOnUserChange({ + clientAction: 'updated', + id: this.userId, + diff: { 'services.resume.loginTokens': me.services?.resume?.loginTokens }, + }); + const token = me.services?.resume?.loginTokens?.find((token) => token.hashedToken === hashedToken); const tokenExpires = @@ -1172,6 +1197,8 @@ API.v1.addRoute( throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); } + void notifyOnUserChange({ clientAction: 'updated', id: this.userId, diff: { 'services.resume.loginTokens': [] } }); + return API.v1.success({ message: `User ${userId} has been logged out!`, }); @@ -1242,6 +1269,8 @@ API.v1.addRoute( return API.v1.unauthorized(); } + // TODO refactor to not update the user twice (one inside of `setStatusText` and then later just the status + statusDefault) + if (this.bodyParams.message || this.bodyParams.message === '') { await setStatusText(user._id, this.bodyParams.message); } diff --git a/apps/meteor/app/api/server/v1/voip/omnichannel.ts b/apps/meteor/app/api/server/v1/voip/omnichannel.ts index e1ee82d724789..e2128375ea42b 100644 --- a/apps/meteor/app/api/server/v1/voip/omnichannel.ts +++ b/apps/meteor/app/api/server/v1/voip/omnichannel.ts @@ -3,6 +3,7 @@ import type { IUser, IVoipExtensionWithAgentInfo } from '@rocket.chat/core-typin import { Users } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; +import { notifyOnUserChange } from '../../../../lib/server/lib/notifyListener'; import { API } from '../../api'; import { getPaginationItems } from '../../helpers/getPaginationItems'; import { logger } from './logger'; @@ -79,6 +80,15 @@ API.v1.addRoute( try { await Users.setExtension(user._id, extension); + + void notifyOnUserChange({ + clientAction: 'updated', + id: user._id, + diff: { + extension, + }, + }); + return API.v1.success(); } catch (e) { logger.error({ msg: 'Extension already in use' }); @@ -150,6 +160,15 @@ API.v1.addRoute( logger.debug(`Removing extension association for user ${user._id} (extension was ${user.extension})`); await Users.unsetExtension(user._id); + + void notifyOnUserChange({ + clientAction: 'updated', + id: user._id, + diff: { + extension: null, + }, + }); + return API.v1.success(); }, }, diff --git a/apps/meteor/app/apps/server/bridges/commands.ts b/apps/meteor/app/apps/server/bridges/commands.ts index 5e018c51de897..5ffef64730519 100644 --- a/apps/meteor/app/apps/server/bridges/commands.ts +++ b/apps/meteor/app/apps/server/bridges/commands.ts @@ -111,8 +111,8 @@ export class AppCommandsBridge extends CommandBridge { permission: command.permission, callback: this._appCommandExecutor.bind(this), providesPreview: command.providesPreview, - previewer: !command.previewer ? undefined : this._appCommandPreviewer.bind(this), - previewCallback: (!command.executePreviewItem ? undefined : this._appCommandPreviewExecutor.bind(this)) as + previewer: command.providesPreview ? this._appCommandPreviewer.bind(this) : undefined, + previewCallback: (command.providesPreview ? this._appCommandPreviewExecutor.bind(this) : undefined) as | (typeof slashCommands.commands)[string]['previewCallback'] | undefined, } as SlashCommand; @@ -155,10 +155,6 @@ export class AppCommandsBridge extends CommandBridge { if (typeof command.providesPreview !== 'boolean') { throw new Error('Invalid Slash Command parameter provided, it must be a valid ISlashCommand object.'); } - - if (typeof command.executor !== 'function') { - throw new Error('Invalid Slash Command parameter provided, it must be a valid ISlashCommand object.'); - } } private async _appCommandExecutor({ command, message, params, triggerId, userId }: SlashCommandCallbackParams): Promise { diff --git a/apps/meteor/app/apps/server/bridges/http.ts b/apps/meteor/app/apps/server/bridges/http.ts index 1535a18823c54..9d62769336a25 100644 --- a/apps/meteor/app/apps/server/bridges/http.ts +++ b/apps/meteor/app/apps/server/bridges/http.ts @@ -72,55 +72,51 @@ export class AppHttpBridge extends HttpBridge { this.orch.debugLog(`The App ${info.appId} is requesting from the outter webs:`, info); - try { - const response = await fetch( - url.href, - { - method, - body: content, - headers, - timeout, - }, - (request.hasOwnProperty('strictSSL') && !request.strictSSL) || - (request.hasOwnProperty('rejectUnauthorized') && request.rejectUnauthorized), - ); - - const result: IHttpResponse = { - url: info.url, - method: info.method, - statusCode: response.status, - headers: Object.fromEntries(response.headers as unknown as any), - }; - - const body = Buffer.from(await response.arrayBuffer()); - - if (request.encoding === null) { - /** - * The property `content` is not appropriately typed in the - * Apps-engine definition, and we can't simply change it there - * as it would be a breaking change. Thus, we're left with this - * type assertion. - */ - result.content = body as any; - } else { - result.content = body.toString(request.encoding as BufferEncoding); - result.data = ((): any => { - const contentType = (response.headers.get('content-type') || '').split(';')[0]; - if (!['application/json', 'text/javascript', 'application/javascript', 'application/x-javascript'].includes(contentType)) { - return null; - } - - try { - return JSON.parse(result.content); - } catch { - return null; - } - })(); - } - - return result; - } catch (e: any) { - return e.response; + const response = await fetch( + url.href, + { + method, + body: content, + headers, + timeout, + }, + (request.hasOwnProperty('strictSSL') && !request.strictSSL) || + (request.hasOwnProperty('rejectUnauthorized') && request.rejectUnauthorized), + ); + + const result: IHttpResponse = { + url: info.url, + method: info.method, + statusCode: response.status, + headers: Object.fromEntries(response.headers as unknown as any), + }; + + const body = Buffer.from(await response.arrayBuffer()); + + if (request.encoding === null) { + /** + * The property `content` is not appropriately typed in the + * Apps-engine definition, and we can't simply change it there + * as it would be a breaking change. Thus, we're left with this + * type assertion. + */ + result.content = body as any; + } else { + result.content = body.toString(request.encoding as BufferEncoding); + result.data = ((): any => { + const contentType = (response.headers.get('content-type') || '').split(';')[0]; + if (!['application/json', 'text/javascript', 'application/javascript', 'application/x-javascript'].includes(contentType)) { + return null; + } + + try { + return JSON.parse(result.content); + } catch { + return null; + } + })(); } + + return result; } } diff --git a/apps/meteor/app/apps/server/bridges/settings.ts b/apps/meteor/app/apps/server/bridges/settings.ts index e90171813df85..37803d4f94f3c 100644 --- a/apps/meteor/app/apps/server/bridges/settings.ts +++ b/apps/meteor/app/apps/server/bridges/settings.ts @@ -3,6 +3,8 @@ import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import { ServerSettingBridge } from '@rocket.chat/apps-engine/server/bridges/ServerSettingBridge'; import { Settings } from '@rocket.chat/models'; +import { notifyOnSettingChanged, notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; + export class AppSettingBridge extends ServerSettingBridge { constructor(private readonly orch: IAppServerOrchestrator) { super(); @@ -54,7 +56,7 @@ export class AppSettingBridge extends ServerSettingBridge { throw new Error(`The setting "${setting.id}" is not readable.`); } - await Settings.updateValueById(setting.id, setting.value); + (await Settings.updateValueById(setting.id, setting.value)).modifiedCount && void notifyOnSettingChangedById(setting.id); } protected async incrementValue(id: string, value: number, appId: string): Promise { @@ -64,6 +66,9 @@ export class AppSettingBridge extends ServerSettingBridge { throw new Error(`The setting "${id}" is not readable.`); } - await Settings.incrementValueById(id, value); + const { value: setting } = await Settings.incrementValueById(id, value, { returnDocument: 'after' }); + if (setting) { + void notifyOnSettingChanged(setting); + } } } diff --git a/apps/meteor/app/apps/server/bridges/users.ts b/apps/meteor/app/apps/server/bridges/users.ts index b0dfedd6273bc..13d2436c768c9 100644 --- a/apps/meteor/app/apps/server/bridges/users.ts +++ b/apps/meteor/app/apps/server/bridges/users.ts @@ -11,6 +11,7 @@ import { deleteUser } from '../../../lib/server/functions/deleteUser'; import { getUserCreatedByApp } from '../../../lib/server/functions/getUserCreatedByApp'; import { setUserActiveStatus } from '../../../lib/server/functions/setUserActiveStatus'; import { setUserAvatar } from '../../../lib/server/functions/setUserAvatar'; +import { notifyOnUserChange, notifyOnUserChangeById } from '../../../lib/server/lib/notifyListener'; export class AppUserBridge extends UserBridge { constructor(private readonly orch: IAppServerOrchestrator) { @@ -97,6 +98,8 @@ export class AppUserBridge extends UserBridge { throw new Error('Creating normal users is currently not supported'); } + void notifyOnUserChangeById({ clientAction: 'inserted', id: user._id }); + return user._id; } @@ -137,6 +140,8 @@ export class AppUserBridge extends UserBridge { await Users.updateOne({ _id: user.id }, { $set: fields as any }); + void notifyOnUserChange({ clientAction: 'updated', id: user.id, diff: fields }); + return true; } diff --git a/apps/meteor/app/assets/server/assets.ts b/apps/meteor/app/assets/server/assets.ts index 0acba139d5f44..b9653a2f1b9a9 100644 --- a/apps/meteor/app/assets/server/assets.ts +++ b/apps/meteor/app/assets/server/assets.ts @@ -13,6 +13,7 @@ import sharp from 'sharp'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; import { RocketChatFile } from '../../file/server'; import { methodDeprecationLogger } from '../../lib/server/lib/deprecationWarningLogger'; +import { notifyOnSettingChangedById } from '../../lib/server/lib/notifyListener'; import { settings, settingsRegistry } from '../../settings/server'; import { getExtension } from '../../utils/lib/mimeTypes'; import { getURL } from '../../utils/server/getURL'; @@ -261,7 +262,13 @@ class RocketChatAssetsClass { defaultUrl: assetInstance.defaultUrl, }; - void Settings.updateValueById(key, value); + void (async () => { + const { modifiedCount } = await Settings.updateValueById(key, value); + if (modifiedCount) { + void notifyOnSettingChangedById(key); + } + })(); + return RocketChatAssets.processAsset(key, value); }, 200); }); @@ -282,7 +289,13 @@ class RocketChatAssetsClass { defaultUrl: getAssetByKey(asset).defaultUrl, }; - void Settings.updateValueById(key, value); + void (async () => { + const { modifiedCount } = await Settings.updateValueById(key, value); + if (modifiedCount) { + void notifyOnSettingChangedById(key); + } + })(); + await RocketChatAssets.processAsset(key, value); } @@ -371,7 +384,8 @@ export async function addAssetToSetting(asset: string, value: IRocketChatAsset, if (currentValue && typeof currentValue === 'object' && currentValue.defaultUrl !== getAssetByKey(asset).defaultUrl) { currentValue.defaultUrl = getAssetByKey(asset).defaultUrl; - await Settings.updateValueById(key, currentValue); + + (await Settings.updateValueById(key, currentValue)).modifiedCount && void notifyOnSettingChangedById(key); } } diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index cc5a04c275b7b..bffbe1f9876dd 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -19,6 +19,7 @@ import { getNewUserRoles } from '../../../../server/services/user/lib/getNewUser import { getAvatarSuggestionForUser } from '../../../lib/server/functions/getAvatarSuggestionForUser'; import { joinDefaultChannels } from '../../../lib/server/functions/joinDefaultChannels'; import { setAvatarFromServiceWithValidation } from '../../../lib/server/functions/setUserAvatar'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; import { safeGetMeteorUser } from '../../../utils/server/functions/safeGetMeteorUser'; @@ -323,7 +324,8 @@ const insertUserDocAsync = async function (options, user) { if (!roles.includes('admin') && !hasAdmin) { roles.push('admin'); if (settings.get('Show_Setup_Wizard') === 'pending') { - await Settings.updateValueById('Show_Setup_Wizard', 'in_progress'); + (await Settings.updateValueById('Show_Setup_Wizard', 'in_progress')).modifiedCount && + void notifyOnSettingChangedById('Show_Setup_Wizard'); } } } diff --git a/apps/meteor/app/authorization/client/index.ts b/apps/meteor/app/authorization/client/index.ts index dd335c13030ea..7dc1a3466f49a 100644 --- a/apps/meteor/app/authorization/client/index.ts +++ b/apps/meteor/app/authorization/client/index.ts @@ -1,4 +1,5 @@ import { hasAllPermission, hasAtLeastOnePermission, hasPermission, userHasAllPermission } from './hasPermission'; import { hasRole, hasAnyRole } from './hasRole'; +import './restrictedRoles'; export { hasAllPermission, hasAtLeastOnePermission, hasRole, hasAnyRole, hasPermission, userHasAllPermission }; diff --git a/apps/meteor/app/authorization/client/restrictedRoles.ts b/apps/meteor/app/authorization/client/restrictedRoles.ts new file mode 100644 index 0000000000000..5aa5e426c2bda --- /dev/null +++ b/apps/meteor/app/authorization/client/restrictedRoles.ts @@ -0,0 +1,12 @@ +import { Meteor } from 'meteor/meteor'; + +import { sdk } from '../../utils/client/lib/SDKClient'; +import { AuthorizationUtils } from '../lib'; + +Meteor.startup(async () => { + const result = await sdk.call('license:isEnterprise'); + if (result) { + // #ToDo: Load this from the server with an API call instead of having a duplicate list + AuthorizationUtils.addRolePermissionWhiteList('guest', ['view-d-room', 'view-joined-room', 'view-p-room', 'start-discussion']); + } +}); diff --git a/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts b/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts index 91769e71270a1..7cd953a52bb2a 100644 --- a/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts +++ b/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts @@ -34,7 +34,7 @@ export const canDeleteMessageAsync = async ( if (!allowed) { return false; } - const bypassBlockTimeLimit = await hasPermissionAsync(uid, 'bypass-time-limit-edit-and-delete'); + const bypassBlockTimeLimit = await hasPermissionAsync(uid, 'bypass-time-limit-edit-and-delete', rid); if (!bypassBlockTimeLimit) { const blockDeleteInMinutes = await getValue('Message_AllowDeleting_BlockDeleteInMinutes'); diff --git a/apps/meteor/ee/app/canned-responses/client/collections/CannedResponse.ts b/apps/meteor/app/canned-responses/client/collections/CannedResponse.ts similarity index 100% rename from apps/meteor/ee/app/canned-responses/client/collections/CannedResponse.ts rename to apps/meteor/app/canned-responses/client/collections/CannedResponse.ts diff --git a/apps/meteor/ee/app/canned-responses/client/index.ts b/apps/meteor/app/canned-responses/client/index.ts similarity index 100% rename from apps/meteor/ee/app/canned-responses/client/index.ts rename to apps/meteor/app/canned-responses/client/index.ts diff --git a/apps/meteor/ee/app/canned-responses/client/startup/responses.js b/apps/meteor/app/canned-responses/client/startup/responses.js similarity index 84% rename from apps/meteor/ee/app/canned-responses/client/startup/responses.js rename to apps/meteor/app/canned-responses/client/startup/responses.js index 6d5834d91cc00..5959452832619 100644 --- a/apps/meteor/ee/app/canned-responses/client/startup/responses.js +++ b/apps/meteor/app/canned-responses/client/startup/responses.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { hasPermission } from '../../../../../app/authorization/client'; -import { settings } from '../../../../../app/settings/client'; -import { sdk } from '../../../../../app/utils/client/lib/SDKClient'; +import { hasPermission } from '../../../authorization/client'; +import { settings } from '../../../settings/client'; +import { sdk } from '../../../utils/client/lib/SDKClient'; import { CannedResponse } from '../collections/CannedResponse'; const events = { diff --git a/apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.ts b/apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.ts index 14cb2f4a57cef..0550a3d7f238e 100644 --- a/apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.ts +++ b/apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.ts @@ -1,6 +1,7 @@ import { Settings } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { userScopes } from '../oauthScopes'; import { getRedirectUri } from './getRedirectUri'; @@ -10,6 +11,8 @@ export async function getOAuthAuthorizationUrl() { await Settings.updateValueById('Cloud_Workspace_Registration_State', state); + void notifyOnSettingChangedById('Cloud_Workspace_Registration_State'); + const cloudUrl = settings.get('Cloud_Url'); const clientId = settings.get('Cloud_Workspace_Client_Id'); const redirectUri = getRedirectUri(); diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts index 7e970edfdfc23..1ea20812c0621 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts @@ -1,5 +1,6 @@ import { Settings } from '@rocket.chat/models'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { getWorkspaceAccessTokenWithScope } from './getWorkspaceAccessTokenWithScope'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; @@ -32,11 +33,13 @@ export async function getWorkspaceAccessToken(forceNew = false, scope = '', save const accessToken = await getWorkspaceAccessTokenWithScope(scope, throwOnError); if (save) { - await Promise.all([ - Settings.updateValueById('Cloud_Workspace_Access_Token', accessToken.token), - Settings.updateValueById('Cloud_Workspace_Access_Token_Expires_At', accessToken.expiresAt), - ]); + (await Settings.updateValueById('Cloud_Workspace_Access_Token', accessToken.token)).modifiedCount && + void notifyOnSettingChangedById('Cloud_Workspace_Access_Token'); + + (await Settings.updateValueById('Cloud_Workspace_Access_Token_Expires_At', accessToken.expiresAt)).modifiedCount && + void notifyOnSettingChangedById('Cloud_Workspace_Access_Token_Expires_At'); } + return accessToken.token; } diff --git a/apps/meteor/app/cloud/server/functions/removeWorkspaceRegistrationInfo.ts b/apps/meteor/app/cloud/server/functions/removeWorkspaceRegistrationInfo.ts index b4e5362f5ac7a..45e1738e11e62 100644 --- a/apps/meteor/app/cloud/server/functions/removeWorkspaceRegistrationInfo.ts +++ b/apps/meteor/app/cloud/server/functions/removeWorkspaceRegistrationInfo.ts @@ -1,5 +1,6 @@ import { Settings } from '@rocket.chat/models'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; export async function removeWorkspaceRegistrationInfo() { @@ -8,16 +9,30 @@ export async function removeWorkspaceRegistrationInfo() { return true; } - await Promise.all([ - Settings.resetValueById('Cloud_Workspace_Id', null), - Settings.resetValueById('Cloud_Workspace_Name', null), - Settings.resetValueById('Cloud_Workspace_Client_Id', null), - Settings.resetValueById('Cloud_Workspace_Client_Secret', null), - Settings.resetValueById('Cloud_Workspace_Client_Secret_Expires_At', null), - Settings.resetValueById('Cloud_Workspace_PublicKey', null), - Settings.resetValueById('Cloud_Workspace_Registration_Client_Uri', null), - ]); + const settingsIds = [ + 'Cloud_Workspace_Id', + 'Cloud_Workspace_Name', + 'Cloud_Workspace_Client_Id', + 'Cloud_Workspace_Client_Secret', + 'Cloud_Workspace_Client_Secret_Expires_At', + 'Cloud_Workspace_PublicKey', + 'Cloud_Workspace_Registration_Client_Uri', + 'Show_Setup_Wizard', + ]; + + const promises = settingsIds.map((settingId) => { + if (settingId === 'Show_Setup_Wizard') { + return Settings.updateValueById('Show_Setup_Wizard', 'in_progress'); + } + + return Settings.resetValueById(settingId, null); + }); + + (await Promise.all(promises)).forEach((value, index) => { + if (value?.modifiedCount) { + void notifyOnSettingChangedById(settingsIds[index]); + } + }); - await Settings.updateValueById('Show_Setup_Wizard', 'in_progress'); return true; } diff --git a/apps/meteor/app/cloud/server/functions/saveRegistrationData.ts b/apps/meteor/app/cloud/server/functions/saveRegistrationData.ts index cb2d19cfb92ed..d922733024426 100644 --- a/apps/meteor/app/cloud/server/functions/saveRegistrationData.ts +++ b/apps/meteor/app/cloud/server/functions/saveRegistrationData.ts @@ -1,6 +1,7 @@ import { applyLicense } from '@rocket.chat/license'; import { Settings } from '@rocket.chat/models'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { syncCloudData } from './syncWorkspace/syncCloudData'; @@ -33,7 +34,8 @@ export async function saveRegistrationData({ await syncCloudData(); } -function saveRegistrationDataBase({ + +async function saveRegistrationDataBase({ workspaceId, client_name, client_id, @@ -50,38 +52,47 @@ function saveRegistrationDataBase({ publicKey: string; registration_client_uri: string; }) { - return Promise.all([ - Settings.updateValueById('Register_Server', true), - Settings.updateValueById('Cloud_Workspace_Id', workspaceId), - Settings.updateValueById('Cloud_Workspace_Name', client_name), - Settings.updateValueById('Cloud_Workspace_Client_Id', client_id), - Settings.updateValueById('Cloud_Workspace_Client_Secret', client_secret), - Settings.updateValueById('Cloud_Workspace_Client_Secret_Expires_At', client_secret_expires_at), - Settings.updateValueById('Cloud_Workspace_PublicKey', publicKey), - Settings.updateValueById('Cloud_Workspace_Registration_Client_Uri', registration_client_uri), - ]).then(async (...results) => { - // wait until all the settings are updated before syncing the data - for await (const retry of Array.from({ length: 10 })) { - if ( - settings.get('Register_Server') === true && - settings.get('Cloud_Workspace_Id') === workspaceId && - settings.get('Cloud_Workspace_Name') === client_name && - settings.get('Cloud_Workspace_Client_Id') === client_id && - settings.get('Cloud_Workspace_Client_Secret') === client_secret && - settings.get('Cloud_Workspace_Client_Secret_Expires_At') === client_secret_expires_at && - settings.get('Cloud_Workspace_PublicKey') === publicKey && - settings.get('Cloud_Workspace_Registration_Client_Uri') === registration_client_uri - ) { - break; - } + const settingsData = [ + { _id: 'Register_Server', value: true }, + { _id: 'Cloud_Workspace_Id', value: workspaceId }, + { _id: 'Cloud_Workspace_Name', value: client_name }, + { _id: 'Cloud_Workspace_Client_Id', value: client_id }, + { _id: 'Cloud_Workspace_Client_Secret', value: client_secret }, + { _id: 'Cloud_Workspace_Client_Secret_Expires_At', value: client_secret_expires_at }, + { _id: 'Cloud_Workspace_PublicKey', value: publicKey }, + { _id: 'Cloud_Workspace_Registration_Client_Uri', value: registration_client_uri }, + ]; + + const promises = settingsData.map(({ _id, value }) => Settings.updateValueById(_id, value)); - if (retry === 9) { - throw new Error('Failed to save registration data'); - } - await new Promise((resolve) => setTimeout(resolve, 1000)); + (await Promise.all(promises)).forEach((value, index) => { + if (value?.modifiedCount) { + void notifyOnSettingChangedById(settingsData[index]._id); } - return results; }); + + // TODO: Why is this taking so long that needs a timeout? + for await (const retry of Array.from({ length: 10 })) { + const isSettingsUpdated = + settings.get('Register_Server') === true && + settings.get('Cloud_Workspace_Id') === workspaceId && + settings.get('Cloud_Workspace_Name') === client_name && + settings.get('Cloud_Workspace_Client_Id') === client_id && + settings.get('Cloud_Workspace_Client_Secret') === client_secret && + settings.get('Cloud_Workspace_Client_Secret_Expires_At') === client_secret_expires_at && + settings.get('Cloud_Workspace_PublicKey') === publicKey && + settings.get('Cloud_Workspace_Registration_Client_Uri') === registration_client_uri; + + if (isSettingsUpdated) { + return; + } + + if (retry === 9) { + throw new Error('Failed to save registration data'); + } + + await new Promise((resolve) => setTimeout(resolve, 1000)); + } } export async function saveRegistrationDataManual({ diff --git a/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts b/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts index 5f5df80d0d3db..1fb2dcc064493 100644 --- a/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts +++ b/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts @@ -2,6 +2,7 @@ import { Settings } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { SystemLogger } from '../../../../server/lib/logger/system'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { buildWorkspaceRegistrationData } from './buildRegistrationData'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; @@ -15,7 +16,7 @@ export async function startRegisterWorkspace(resend = false) { return true; } - await Settings.updateValueById('Register_Server', true); + (await Settings.updateValueById('Register_Server', true)).modifiedCount && void notifyOnSettingChangedById('Register_Server'); const regInfo = await buildWorkspaceRegistrationData(undefined); diff --git a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts index a543c0681f38b..f4334bd04d647 100644 --- a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts +++ b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts @@ -6,6 +6,7 @@ import type { Response } from '@rocket.chat/server-fetch'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { SystemLogger } from '../../../../../server/lib/logger/system'; +import { notifyOnSettingChangedById } from '../../../../lib/server/lib/notifyListener'; import { settings } from '../../../../settings/server'; import { supportedVersions as supportedVersionsFromBuild } from '../../../../utils/rocketchat-supported-versions.info'; import { buildVersionUpdateMessage } from '../../../../version-check/server/functions/buildVersionUpdateMessage'; @@ -65,7 +66,7 @@ const cacheValueInSettings = ( SystemLogger.debug(`Resetting cached value ${key} in settings`); const value = await fn(); - await Settings.updateValueById(key, value); + (await Settings.updateValueById(key, value)).modifiedCount && void notifyOnSettingChangedById(key); return value; }; diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/legacySyncWorkspace.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/legacySyncWorkspace.ts index 91202d9731708..f2e66bfdf9f77 100644 --- a/apps/meteor/app/cloud/server/functions/syncWorkspace/legacySyncWorkspace.ts +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/legacySyncWorkspace.ts @@ -5,6 +5,7 @@ import { v, compile } from 'suretype'; import { CloudWorkspaceConnectionError } from '../../../../../lib/errors/CloudWorkspaceConnectionError'; import { CloudWorkspaceRegistrationError } from '../../../../../lib/errors/CloudWorkspaceRegistrationError'; +import { notifyOnSettingChangedById } from '../../../../lib/server/lib/notifyListener'; import { settings } from '../../../../settings/server'; import type { WorkspaceRegistrationData } from '../buildRegistrationData'; import { buildWorkspaceRegistrationData } from '../buildRegistrationData'; @@ -126,11 +127,13 @@ const fetchWorkspaceClientPayload = async ({ /** @deprecated */ const consumeWorkspaceSyncPayload = async (result: Serialized) => { if (result.publicKey) { - await Settings.updateValueById('Cloud_Workspace_PublicKey', result.publicKey); + (await Settings.updateValueById('Cloud_Workspace_PublicKey', result.publicKey)).modifiedCount && + void notifyOnSettingChangedById('Cloud_Workspace_PublicKey'); } if (result.trial?.trialID) { - await Settings.updateValueById('Cloud_Workspace_Had_Trial', true); + (await Settings.updateValueById('Cloud_Workspace_Had_Trial', true)).modifiedCount && + void notifyOnSettingChangedById('Cloud_Workspace_Had_Trial'); } // add banners diff --git a/apps/meteor/app/crowd/server/crowd.ts b/apps/meteor/app/crowd/server/crowd.ts index e43c65c70f918..70f54dd7b7261 100644 --- a/apps/meteor/app/crowd/server/crowd.ts +++ b/apps/meteor/app/crowd/server/crowd.ts @@ -10,6 +10,7 @@ import { crowdIntervalValuesToCronMap } from '../../../server/settings/crowd'; import { deleteUser } from '../../lib/server/functions/deleteUser'; import { _setRealName } from '../../lib/server/functions/setRealName'; import { setUserActiveStatus } from '../../lib/server/functions/setUserActiveStatus'; +import { notifyOnUserChange, notifyOnUserChangeById, notifyOnUserChangeAsync } from '../../lib/server/lib/notifyListener'; import { settings } from '../../settings/server'; import { logger } from './logger'; @@ -215,6 +216,15 @@ export class CROWD { }, ); + void notifyOnUserChange({ + clientAction: 'updated', + id, + diff: { + ...user, + ...(crowdUser.displayname && { name: crowdUser.displayname }), + }, + }); + await setUserActiveStatus(id, crowdUser.active); } @@ -312,6 +322,21 @@ export class CROWD { }, ); + // TODO this can be optmized so places that care about loginTokens being removed are invoked directly + // instead of having to listen to every watch.users event + void notifyOnUserChangeAsync(async () => { + const userTokens = await Users.findOneById(crowdUser._id, { projection: { 'services.resume.loginTokens': 1 } }); + if (!userTokens) { + return; + } + + return { + clientAction: 'updated', + id: crowdUser._id, + diff: { 'services.resume.loginTokens': userTokens.services?.resume?.loginTokens }, + }; + }); + await this.syncDataToUser(crowdUser, user._id); return { @@ -324,6 +349,8 @@ export class CROWD { try { crowdUser._id = await Accounts.createUserAsync(crowdUser); + void notifyOnUserChangeById({ clientAction: 'inserted', id: crowdUser._id }); + // sync the user data await this.syncDataToUser(crowdUser, crowdUser._id); diff --git a/apps/meteor/app/custom-oauth/server/custom_oauth_server.js b/apps/meteor/app/custom-oauth/server/custom_oauth_server.js index 6b225069734d4..16407bf134de4 100644 --- a/apps/meteor/app/custom-oauth/server/custom_oauth_server.js +++ b/apps/meteor/app/custom-oauth/server/custom_oauth_server.js @@ -11,6 +11,7 @@ import _ from 'underscore'; import { callbacks } from '../../../lib/callbacks'; import { isURL } from '../../../lib/utils/isURL'; +import { notifyOnUserChange } from '../../lib/server/lib/notifyListener'; import { registerAccessTokenService } from '../../lib/server/oauth/oauth'; import { settings } from '../../settings/server'; import { normalizers, fromTemplate, renameInvalidProperties } from './transform_helpers'; @@ -374,6 +375,8 @@ export class CustomOAuth { }; await Users.update({ _id: user._id }, update); + + void notifyOnUserChange({ clientAction: 'updated', id: user._id, diff: update }); } }); diff --git a/apps/meteor/app/e2e/client/E2EEState.ts b/apps/meteor/app/e2e/client/E2EEState.ts new file mode 100644 index 0000000000000..0e505ec4a1bd3 --- /dev/null +++ b/apps/meteor/app/e2e/client/E2EEState.ts @@ -0,0 +1,9 @@ +export enum E2EEState { + NOT_STARTED = 'NOT_STARTED', + DISABLED = 'DISABLED', + LOADING_KEYS = 'LOADING_KEYS', + READY = 'READY', + SAVE_PASSWORD = 'SAVE_PASSWORD', + ENTER_PASSWORD = 'ENTER_PASSWORD', + ERROR = 'ERROR', +} diff --git a/apps/meteor/app/e2e/client/helper.js b/apps/meteor/app/e2e/client/helper.js index 2e0843ee33801..49b157c5ccf45 100644 --- a/apps/meteor/app/e2e/client/helper.js +++ b/apps/meteor/app/e2e/client/helper.js @@ -53,6 +53,10 @@ export async function encryptAES(vector, key, data) { return crypto.subtle.encrypt({ name: 'AES-CBC', iv: vector }, key, data); } +export async function encryptAESCTR(vector, key, data) { + return crypto.subtle.encrypt({ name: 'AES-CTR', counter: vector, length: 64 }, key, data); +} + export async function decryptRSA(key, data) { return crypto.subtle.decrypt({ name: 'RSA-OAEP' }, key, data); } @@ -65,6 +69,10 @@ export async function generateAESKey() { return crypto.subtle.generateKey({ name: 'AES-CBC', length: 128 }, true, ['encrypt', 'decrypt']); } +export async function generateAESCTRKey() { + return crypto.subtle.generateKey({ name: 'AES-CTR', length: 256 }, true, ['encrypt', 'decrypt']); +} + export async function generateRSAKey() { return crypto.subtle.generateKey( { diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index bd0863d691a9a..fe61156240a87 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -1,6 +1,5 @@ import { Base64 } from '@rocket.chat/base64'; import { Emitter } from '@rocket.chat/emitter'; -import { Random } from '@rocket.chat/random'; import EJSON from 'ejson'; import { RoomManager } from '../../../client/lib/RoomManager'; @@ -23,6 +22,8 @@ import { importAESKey, importRSAKey, readFileAsArrayBuffer, + encryptAESCTR, + generateAESCTRKey, } from './helper'; import { log, logError } from './logger'; import { e2e } from './rocketchat.e2e'; @@ -41,6 +42,7 @@ const permitedMutations = { E2ERoomState.ERROR, E2ERoomState.DISABLED, E2ERoomState.WAITING_KEYS, + E2ERoomState.CREATING_KEYS, ], }; @@ -92,6 +94,14 @@ export class E2ERoom extends Emitter { logError(`E2E ROOM { state: ${this.state}, rid: ${this.roomId} }`, ...msg); } + hasSessionKey() { + return !!this.groupSessionKey; + } + + getState() { + return this.state; + } + setState(requestedState) { const currentState = this.state; const nextState = filterMutation(currentState, requestedState); @@ -180,20 +190,20 @@ export class E2ERoom extends Emitter { async decryptSubscription() { const subscription = Subscriptions.findOne({ rid: this.roomId }); - const data = await (subscription.lastMessage?.msg && this.decrypt(subscription.lastMessage.msg)); - if (!data?.text) { + if (subscription.lastMessage?.t !== 'e2e') { this.log('decryptSubscriptions nothing to do'); return; } + const message = await this.decryptMessage(subscription.lastMessage); + Subscriptions.update( { _id: subscription._id, }, { $set: { - 'lastMessage.msg': data.text, - 'lastMessage.e2e': 'done', + lastMessage: message, }, }, ); @@ -208,6 +218,10 @@ export class E2ERoom extends Emitter { // Initiates E2E Encryption async handshake() { + if (!e2e.isReady()) { + return; + } + if (this.state !== E2ERoomState.KEYS_RECEIVED && this.state !== E2ERoomState.NOT_STARTED) { return; } @@ -307,17 +321,29 @@ export class E2ERoom extends Emitter { async encryptKeyForOtherParticipants() { // Encrypt generated session key for every user in room and publish to subscription model. try { - const { users } = await sdk.call('e2e.getUsersOfRoomWithoutKey', this.roomId); - users.forEach((user) => this.encryptForParticipant(user)); + const users = (await sdk.call('e2e.getUsersOfRoomWithoutKey', this.roomId)).users.filter((user) => user?.e2e?.public_key); + + if (!users.length) { + return; + } + + const usersSuggestedGroupKeys = { [this.roomId]: [] }; + for await (const user of users) { + const encryptedGroupKey = await this.encryptGroupKeyForParticipant(user.e2e.public_key); + + usersSuggestedGroupKeys[this.roomId].push({ _id: user._id, key: encryptedGroupKey }); + } + + await sdk.rest.post('/v1/e2e.provideUsersSuggestedGroupKeys', { usersSuggestedGroupKeys }); } catch (error) { return this.error('Error getting room users: ', error); } } - async encryptForParticipant(user) { + async encryptGroupKeyForParticipant(public_key) { let userKey; try { - userKey = await importRSAKey(JSON.parse(user.e2e.public_key), ['encrypt']); + userKey = await importRSAKey(JSON.parse(public_key), ['encrypt']); } catch (error) { return this.error('Error importing user key: ', error); } @@ -326,76 +352,109 @@ export class E2ERoom extends Emitter { // Encrypt session key for this user with his/her public key try { const encryptedUserKey = await encryptRSA(userKey, toArrayBuffer(this.sessionKeyExportedString)); - // Key has been encrypted. Publish to that user's subscription model for this room. - await sdk.call('e2e.updateGroupKey', this.roomId, user._id, this.keyID + Base64.encode(new Uint8Array(encryptedUserKey))); + const encryptedUserKeyToString = this.keyID + Base64.encode(new Uint8Array(encryptedUserKey)); + return encryptedUserKeyToString; } catch (error) { return this.error('Error encrypting user key: ', error); } } + async sha256Hash(arrayBuffer) { + const hashArray = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', arrayBuffer))); + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); + } + + async sha256HashText(text) { + const encoder = new TextEncoder(); + return this.sha256Hash(encoder.encode(text)); + } + // Encrypts files before upload. I/O is in arraybuffers. async encryptFile(file) { - if (!this.isSupportedRoomType(this.typeOfRoom)) { - return; - } + // if (!this.isSupportedRoomType(this.typeOfRoom)) { + // return; + // } const fileArrayBuffer = await readFileAsArrayBuffer(file); + const hash = await this.sha256Hash(new Uint8Array(fileArrayBuffer)); + const vector = crypto.getRandomValues(new Uint8Array(16)); + const key = await generateAESCTRKey(); let result; try { - result = await encryptAES(vector, this.groupSessionKey, fileArrayBuffer); + result = await encryptAESCTR(vector, key, fileArrayBuffer); } catch (error) { + console.log(error); return this.error('Error encrypting group key: ', error); } - const output = joinVectorAndEcryptedData(vector, result); + const exportedKey = await window.crypto.subtle.exportKey('jwk', key); - const encryptedFile = new File([toArrayBuffer(EJSON.stringify(output))], file.name); + const fileName = await this.sha256HashText(file.name); - return encryptedFile; + const encryptedFile = new File([toArrayBuffer(result)], fileName); + + return { + file: encryptedFile, + key: exportedKey, + iv: Base64.encode(vector), + type: file.type, + hash, + }; } // Decrypt uploaded encrypted files. I/O is in arraybuffers. - async decryptFile(message) { - if (message[0] !== '{') { - return; - } - - const [vector, cipherText] = splitVectorAndEcryptedData(EJSON.parse(message)); - - try { - return await decryptAES(vector, this.groupSessionKey, cipherText); - } catch (error) { - this.error('Error decrypting file: ', error); + async decryptFile(file, key, iv) { + const ivArray = Base64.decode(iv); + const cryptoKey = await window.crypto.subtle.importKey('jwk', key, { name: 'AES-CTR' }, true, ['encrypt', 'decrypt']); - return false; - } + return window.crypto.subtle.decrypt({ name: 'AES-CTR', counter: ivArray, length: 64 }, cryptoKey, file); } // Encrypts messages async encryptText(data) { - if (!(typeof data === 'function' || (typeof data === 'object' && !!data))) { - data = new TextEncoder('UTF-8').encode(EJSON.stringify({ text: data, ack: Random.id((Random.fraction() + 1) * 20) })); - } - - if (!this.isSupportedRoomType(this.typeOfRoom)) { - return data; - } - const vector = crypto.getRandomValues(new Uint8Array(16)); - let result; + try { - result = await encryptAES(vector, this.groupSessionKey, data); + const result = await encryptAES(vector, this.groupSessionKey, data); + return this.keyID + Base64.encode(joinVectorAndEcryptedData(vector, result)); } catch (error) { - return this.error('Error encrypting message: ', error); + this.error('Error encrypting message: ', error); + throw error; } + } + + // Helper function for encryption of content + async encryptMessageContent(contentToBeEncrypted) { + const data = new TextEncoder().encode(EJSON.stringify(contentToBeEncrypted)); + + return { + algorithm: 'rc.v1.aes-sha2', + ciphertext: await this.encryptText(data), + }; + } + + // Helper function for encryption of content + async encryptMessage(message) { + const { msg, attachments, ...rest } = message; + + const content = await this.encryptMessageContent({ msg, attachments }); - return this.keyID + Base64.encode(joinVectorAndEcryptedData(vector, result)); + return { + ...rest, + content, + t: 'e2e', + e2e: 'pending', + }; } // Helper function for encryption of messages encrypt(message) { + if (!this.isSupportedRoomType(this.typeOfRoom)) { + return; + } + const ts = new Date(); const data = new TextEncoder('UTF-8').encode( @@ -410,31 +469,38 @@ export class E2ERoom extends Emitter { return this.encryptText(data); } - // Decrypt messages + async decryptContent(data) { + if (data.content && data.content.algorithm === 'rc.v1.aes-sha2') { + const content = await this.decrypt(data.content.ciphertext); + Object.assign(data, content); + } + + return data; + } + // Decrypt messages async decryptMessage(message) { if (message.t !== 'e2e' || message.e2e === 'done') { return message; } - const data = await this.decrypt(message.msg); + if (message.msg) { + const data = await this.decrypt(message.msg); - if (!data?.text) { - return message; + if (data?.text) { + message.msg = data.text; + } } + message = await this.decryptContent(message); + return { ...message, - msg: data.text, e2e: 'done', }; } async decrypt(message) { - if (!this.isSupportedRoomType(this.typeOfRoom)) { - return message; - } - const keyID = message.slice(0, 12); if (keyID !== this.keyID) { @@ -459,5 +525,27 @@ export class E2ERoom extends Emitter { } this.encryptKeyForOtherParticipants(); + this.setState(E2ERoomState.READY); + } + + onStateChange(cb) { + this.on('STATE_CHANGED', cb); + return () => this.off('STATE_CHANGED', cb); + } + + async encryptGroupKeyForParticipantsWaitingForTheKeys(users) { + if (!this.isReady()) { + return; + } + + const usersWithKeys = await Promise.all( + users.map(async (user) => { + const { _id, public_key } = user; + const key = await this.encryptGroupKeyForParticipant(public_key); + return { _id, key }; + }), + ); + + return usersWithKeys; } } diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 1a98ce857f019..0cc344ff51527 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -1,13 +1,13 @@ import QueryString from 'querystring'; import URL from 'url'; -import type { IE2EEMessage, IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; +import type { IE2EEMessage, IMessage, IRoom, ISubscription, IUser, IUploadWithUser, MessageAttachment } from '@rocket.chat/core-typings'; import { isE2EEMessage } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import EJSON from 'ejson'; +import _ from 'lodash'; import { Meteor } from 'meteor/meteor'; -import type { ReactiveVar as ReactiveVarType } from 'meteor/reactive-var'; -import { ReactiveVar } from 'meteor/reactive-var'; +import { Tracker } from 'meteor/tracker'; import * as banners from '../../../client/lib/banners'; import type { LegacyBannerPayload } from '../../../client/lib/banners'; @@ -19,11 +19,13 @@ import EnterE2EPasswordModal from '../../../client/views/e2e/EnterE2EPasswordMod import SaveE2EPasswordModal from '../../../client/views/e2e/SaveE2EPasswordModal'; import { createQuoteAttachment } from '../../../lib/createQuoteAttachment'; import { getMessageUrlRegex } from '../../../lib/getMessageUrlRegex'; +import { isTruthy } from '../../../lib/isTruthy'; import { ChatRoom, Subscriptions, Messages } from '../../models/client'; import { settings } from '../../settings/client'; import { getUserAvatarURL } from '../../utils/client'; import { sdk } from '../../utils/client/lib/SDKClient'; import { t } from '../../utils/lib/i18n'; +import { E2EEState } from './E2EEState'; import { toString, toArrayBuffer, @@ -40,6 +42,7 @@ import { } from './helper'; import { log, logError } from './logger'; import { E2ERoom } from './rocketchat.e2e.room'; + import './events.js'; let failedToDecodeKey = false; @@ -49,36 +52,58 @@ type KeyPair = { private_key: string | null; }; +const ROOM_KEY_EXCHANGE_SIZE = 10; +const E2EEStateDependency = new Tracker.Dependency(); + class E2E extends Emitter { private started: boolean; - public enabled: ReactiveVarType; - - private _ready: ReactiveVarType; - private instancesByRoomId: Record; - private db_public_key: string | null; + private db_public_key: string | null | undefined; - private db_private_key: string | null; + private db_private_key: string | null | undefined; public privateKey: CryptoKey | undefined; + private keyDistributionInterval: ReturnType | null; + + private state: E2EEState; + + private observable: Meteor.LiveQueryHandle | undefined; + constructor() { super(); this.started = false; - this.enabled = new ReactiveVar(false); - this._ready = new ReactiveVar(false); this.instancesByRoomId = {}; + this.keyDistributionInterval = null; + this.observable = undefined; + + this.on('E2E_STATE_CHANGED', ({ prevState, nextState }) => { + this.log(`${prevState} -> ${nextState}`); + }); - this.on('ready', async () => { - this._ready.set(true); - this.log('startClient -> Done'); - this.log('decryptSubscriptions'); + this.on(E2EEState.READY, async () => { + await this.onE2EEReady(); + }); + + this.on(E2EEState.SAVE_PASSWORD, async () => { + await this.onE2EEReady(); + }); + + this.on(E2EEState.DISABLED, () => { + this.observable?.stop(); + }); - await this.decryptSubscriptions(); - this.log('decryptSubscriptions -> Done'); + this.on(E2EEState.NOT_STARTED, () => { + this.observable?.stop(); }); + + this.on(E2EEState.ERROR, () => { + this.observable?.stop(); + }); + + this.setState(E2EEState.NOT_STARTED); } log(...msg: unknown[]) { @@ -89,12 +114,140 @@ class E2E extends Emitter { logError('E2E', ...msg); } + getState() { + return this.state; + } + isEnabled(): boolean { - return this.enabled.get(); + return this.state !== E2EEState.DISABLED; } isReady(): boolean { - return this.enabled.get() && this._ready.get(); + E2EEStateDependency.depend(); + + // Save_Password state is also a ready state for E2EE + return this.state === E2EEState.READY || this.state === E2EEState.SAVE_PASSWORD; + } + + async onE2EEReady() { + this.log('startClient -> Done'); + this.initiateHandshake(); + await this.handleAsyncE2EESuggestedKey(); + this.log('decryptSubscriptions'); + await this.decryptSubscriptions(); + this.log('decryptSubscriptions -> Done'); + await this.initiateDecryptingPendingMessages(); + this.log('DecryptingPendingMessages -> Done'); + await this.initiateKeyDistribution(); + this.log('initiateKeyDistribution -> Done'); + this.observeSubscriptions(); + this.log('observing subscriptions'); + } + + observeSubscriptions() { + this.observable?.stop(); + + this.observable = Subscriptions.find().observe({ + changed: (sub: ISubscription) => { + setTimeout(async () => { + this.log('Subscription changed', sub); + if (!sub.encrypted && !sub.E2EKey) { + this.removeInstanceByRoomId(sub.rid); + return; + } + + const e2eRoom = await this.getInstanceByRoomId(sub.rid); + if (!e2eRoom) { + return; + } + + if (sub.E2ESuggestedKey) { + if (await e2eRoom.importGroupKey(sub.E2ESuggestedKey)) { + await this.acceptSuggestedKey(sub.rid); + e2eRoom.keyReceived(); + } else { + console.warn('Invalid E2ESuggestedKey, rejecting', sub.E2ESuggestedKey); + await this.rejectSuggestedKey(sub.rid); + } + } + + sub.encrypted ? e2eRoom.resume() : e2eRoom.pause(); + + // Cover private groups and direct messages + if (!e2eRoom.isSupportedRoomType(sub.t)) { + e2eRoom.disable(); + return; + } + + if (sub.E2EKey && e2eRoom.isWaitingKeys()) { + e2eRoom.keyReceived(); + return; + } + + if (!e2eRoom.isReady()) { + return; + } + + await e2eRoom.decryptSubscription(); + }, 0); + }, + added: (sub: ISubscription) => { + setTimeout(async () => { + this.log('Subscription added', sub); + if (!sub.encrypted && !sub.E2EKey) { + return; + } + return this.getInstanceByRoomId(sub.rid); + }, 0); + }, + removed: (sub: ISubscription) => { + this.log('Subscription removed', sub); + this.removeInstanceByRoomId(sub.rid); + }, + }); + } + + shouldAskForE2EEPassword() { + const { private_key } = this.getKeysFromLocalStorage(); + return this.db_private_key && !private_key; + } + + setState(nextState: E2EEState) { + const prevState = this.state; + + this.state = nextState; + + E2EEStateDependency.changed(); + + this.emit('E2E_STATE_CHANGED', { prevState, nextState }); + + this.emit(nextState); + } + + async handleAsyncE2EESuggestedKey() { + const subs = Subscriptions.find({ E2ESuggestedKey: { $exists: true } }).fetch(); + await Promise.all( + subs + .filter((sub) => sub.E2ESuggestedKey && !sub.E2EKey) + .map(async (sub) => { + const e2eRoom = await e2e.getInstanceByRoomId(sub.rid); + + if (!e2eRoom) { + return; + } + + if (await e2eRoom.importGroupKey(sub.E2ESuggestedKey)) { + this.log('Imported valid E2E suggested key'); + await e2e.acceptSuggestedKey(sub.rid); + e2eRoom.keyReceived(); + } else { + this.error('Invalid E2ESuggestedKey, rejecting', sub.E2ESuggestedKey); + await e2e.rejectSuggestedKey(sub.rid); + } + + sub.encrypted ? e2eRoom.resume() : e2eRoom.pause(); + }), + ); } async getInstanceByRoomId(rid: IRoom['_id']): Promise { @@ -119,7 +272,11 @@ class E2E extends Emitter { delete this.instancesByRoomId[rid]; } - async persistKeys({ public_key, private_key }: KeyPair, password: string): Promise { + async persistKeys( + { public_key, private_key }: KeyPair, + password: string, + { force }: { force: boolean } = { force: false }, + ): Promise { if (typeof public_key !== 'string' || typeof private_key !== 'string') { throw new Error('Failed to persist keys as they are not strings.'); } @@ -133,6 +290,7 @@ class E2E extends Emitter { await sdk.rest.post('/v1/e2e.setUserPublicAndPrivateKeys', { public_key, private_key: encodedPrivateKey, + force, }); } @@ -155,6 +313,35 @@ class E2E extends Emitter { }; } + initiateHandshake() { + Object.keys(this.instancesByRoomId).map((key) => this.instancesByRoomId[key].handshake()); + } + + async initiateDecryptingPendingMessages() { + await Promise.all(Object.keys(this.instancesByRoomId).map((key) => this.instancesByRoomId[key].decryptPendingMessages())); + } + + openSaveE2EEPasswordModal(randomPassword: string) { + imperativeModal.open({ + component: SaveE2EPasswordModal, + props: { + randomPassword, + onClose: imperativeModal.close, + onCancel: () => { + this.closeAlert(); + imperativeModal.close(); + }, + onConfirm: () => { + Meteor._localStorage.removeItem('e2e.randomPassword'); + this.setState(E2EEState.READY); + dispatchToastMessage({ type: 'success', message: t('End_To_End_Encryption_Enabled') }); + this.closeAlert(); + imperativeModal.close(); + }, + }, + }); + } + async startClient(): Promise { if (this.started) { return; @@ -172,9 +359,10 @@ class E2E extends Emitter { public_key = this.db_public_key; } - if (!private_key && this.db_private_key) { + if (this.shouldAskForE2EEPassword()) { try { - private_key = await this.decodePrivateKey(this.db_private_key); + this.setState(E2EEState.ENTER_PASSWORD); + private_key = await this.decodePrivateKey(this.db_private_key as string); } catch (error) { this.started = false; failedToDecodeKey = true; @@ -195,44 +383,29 @@ class E2E extends Emitter { if (public_key && private_key) { await this.loadKeys({ public_key, private_key }); + this.setState(E2EEState.READY); } else { await this.createAndLoadKeys(); + this.setState(E2EEState.READY); } if (!this.db_public_key || !this.db_private_key) { + this.setState(E2EEState.LOADING_KEYS); await this.persistKeys(this.getKeysFromLocalStorage(), await this.createRandomPassword()); } const randomPassword = Meteor._localStorage.getItem('e2e.randomPassword'); if (randomPassword) { + this.setState(E2EEState.SAVE_PASSWORD); this.openAlert({ title: () => t('Save_your_encryption_password'), html: () => t('Click_here_to_view_and_copy_your_password'), modifiers: ['large'], closable: false, icon: 'key', - action: () => { - imperativeModal.open({ - component: SaveE2EPasswordModal, - props: { - randomPassword, - onClose: imperativeModal.close, - onCancel: () => { - this.closeAlert(); - imperativeModal.close(); - }, - onConfirm: () => { - Meteor._localStorage.removeItem('e2e.randomPassword'); - this.closeAlert(); - dispatchToastMessage({ type: 'success', message: t('End_To_End_Encryption_Set') }); - imperativeModal.close(); - }, - }, - }); - }, + action: () => this.openSaveE2EEPasswordModal(randomPassword), }); } - this.emit('ready'); } async stopClient(): Promise { @@ -243,13 +416,14 @@ class E2E extends Emitter { Meteor._localStorage.removeItem('private_key'); this.instancesByRoomId = {}; this.privateKey = undefined; - this.enabled.set(false); - this._ready.set(false); this.started = false; + this.keyDistributionInterval && clearInterval(this.keyDistributionInterval); + this.keyDistributionInterval = null; + this.setState(E2EEState.DISABLED); } async changePassword(newPassword: string): Promise { - await this.persistKeys(this.getKeysFromLocalStorage(), newPassword); + await this.persistKeys(this.getKeysFromLocalStorage(), newPassword, { force: true }); if (Meteor._localStorage.getItem('e2e.randomPassword')) { Meteor._localStorage.setItem('e2e.randomPassword', newPassword); @@ -258,12 +432,17 @@ class E2E extends Emitter { async loadKeysFromDB(): Promise { try { + this.setState(E2EEState.LOADING_KEYS); const { public_key, private_key } = await sdk.rest.get('/v1/e2e.fetchMyKeys'); this.db_public_key = public_key; this.db_private_key = private_key; } catch (error) { - return this.error('Error fetching RSA keys: ', error); + this.setState(E2EEState.ERROR); + this.error('Error fetching RSA keys: ', error); + // Stop any process since we can't communicate with the server + // to get the keys. This prevents new key generation + throw error; } } @@ -275,17 +454,20 @@ class E2E extends Emitter { Meteor._localStorage.setItem('private_key', private_key); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error importing private key: ', error); } } async createAndLoadKeys(): Promise { // Could not obtain public-private keypair from server. + this.setState(E2EEState.LOADING_KEYS); let key; try { key = await generateRSAKey(); this.privateKey = key.privateKey; } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error generating key: ', error); } @@ -294,6 +476,7 @@ class E2E extends Emitter { Meteor._localStorage.setItem('public_key', JSON.stringify(publicKey)); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error exporting public key: ', error); } @@ -302,6 +485,7 @@ class E2E extends Emitter { Meteor._localStorage.setItem('private_key', JSON.stringify(privateKey)); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error exporting private key: ', error); } @@ -327,6 +511,7 @@ class E2E extends Emitter { return EJSON.stringify(joinVectorAndEcryptedData(vector, encodedPrivateKey)); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error encrypting encodedPrivateKey: ', error); } } @@ -341,6 +526,7 @@ class E2E extends Emitter { try { baseKey = await importRawKey(toArrayBuffer(password)); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error creating a key based on user password: ', error); } @@ -348,30 +534,34 @@ class E2E extends Emitter { try { return await deriveKey(toArrayBuffer(Meteor.userId()), baseKey); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error deriving baseKey: ', error); } } - async requestPassword(): Promise { + openEnterE2EEPasswordModal(onEnterE2EEPassword?: (password: string) => void) { + imperativeModal.open({ + component: EnterE2EPasswordModal, + props: { + onClose: imperativeModal.close, + onCancel: () => { + failedToDecodeKey = false; + dispatchToastMessage({ type: 'info', message: t('End_To_End_Encryption_Not_Enabled') }); + this.closeAlert(); + imperativeModal.close(); + }, + onConfirm: (password) => { + onEnterE2EEPassword?.(password); + this.closeAlert(); + imperativeModal.close(); + }, + }, + }); + } + + async requestPasswordAlert(): Promise { return new Promise((resolve) => { - const showModal = () => { - imperativeModal.open({ - component: EnterE2EPasswordModal, - props: { - onClose: imperativeModal.close, - onCancel: () => { - failedToDecodeKey = false; - this.closeAlert(); - imperativeModal.close(); - }, - onConfirm: (password) => { - resolve(password); - this.closeAlert(); - imperativeModal.close(); - }, - }, - }); - }; + const showModal = () => this.openEnterE2EEPasswordModal((password) => resolve(password)); const showAlert = () => { this.openAlert({ @@ -394,8 +584,42 @@ class E2E extends Emitter { }); } + async requestPasswordModal(): Promise { + return new Promise((resolve) => this.openEnterE2EEPasswordModal((password) => resolve(password))); + } + + async decodePrivateKeyFlow() { + const password = await this.requestPasswordModal(); + const masterKey = await this.getMasterKey(password); + + if (!this.db_private_key) { + return; + } + + const [vector, cipherText] = splitVectorAndEcryptedData(EJSON.parse(this.db_private_key)); + + try { + const privKey = await decryptAES(vector, masterKey, cipherText); + const privateKey = toString(privKey) as string; + + if (this.db_public_key && privateKey) { + await this.loadKeys({ public_key: this.db_public_key, private_key: privateKey }); + this.setState(E2EEState.READY); + } else { + await this.createAndLoadKeys(); + this.setState(E2EEState.READY); + } + dispatchToastMessage({ type: 'success', message: t('End_To_End_Encryption_Enabled') }); + } catch (error) { + this.setState(E2EEState.ENTER_PASSWORD); + dispatchToastMessage({ type: 'error', message: t('Your_E2EE_password_is_incorrect') }); + dispatchToastMessage({ type: 'info', message: t('End_To_End_Encryption_Not_Enabled') }); + throw new Error('E2E -> Error decrypting private key'); + } + } + async decodePrivateKey(privateKey: string): Promise { - const password = await this.requestPassword(); + const password = await this.requestPasswordAlert(); const masterKey = await this.getMasterKey(password); @@ -405,10 +629,27 @@ class E2E extends Emitter { const privKey = await decryptAES(vector, masterKey, cipherText); return toString(privKey); } catch (error) { + this.setState(E2EEState.ENTER_PASSWORD); + dispatchToastMessage({ type: 'error', message: t('Your_E2EE_password_is_incorrect') }); + dispatchToastMessage({ type: 'info', message: t('End_To_End_Encryption_Not_Enabled') }); throw new Error('E2E -> Error decrypting private key'); } } + async decryptFileContent(file: IUploadWithUser): Promise { + if (!file.rid) { + return file; + } + + const e2eRoom = await this.getInstanceByRoomId(file.rid); + + if (!e2eRoom) { + return file; + } + + return e2eRoom.decryptContent(file); + } + async decryptMessage(message: IMessage | IE2EEMessage): Promise { if (!isE2EEMessage(message) || message.e2e === 'done') { return message; @@ -420,21 +661,36 @@ class E2E extends Emitter { return message; } - const data = await e2eRoom.decrypt(message.msg); + const decryptedMessage: IE2EEMessage = await e2eRoom.decryptMessage(message); - if (!data) { + const decryptedMessageWithQuote = await this.parseQuoteAttachment(decryptedMessage); + + return decryptedMessageWithQuote; + } + + async decryptPinnedMessage(message: IMessage) { + const pinnedMessage = message?.attachments?.[0]?.text; + + if (!pinnedMessage) { return message; } - const decryptedMessage: IE2EEMessage = { - ...message, - msg: data.text, - e2e: 'done', - }; + const e2eRoom = await this.getInstanceByRoomId(message.rid); - const decryptedMessageWithQuote = await this.parseQuoteAttachment(decryptedMessage); + if (!e2eRoom) { + return message; + } - return decryptedMessageWithQuote; + const data = await e2eRoom.decrypt(pinnedMessage); + + if (!data) { + return message; + } + + const decryptedPinnedMessage = { ...message } as IMessage & { attachments: MessageAttachment[] }; + decryptedPinnedMessage.attachments[0].text = data.text; + + return decryptedPinnedMessage; } async decryptPendingMessages(): Promise { @@ -464,6 +720,9 @@ class E2E extends Emitter { } async parseQuoteAttachment(message: IE2EEMessage): Promise { + if (!message?.msg) { + return message; + } const urls = message.msg.match(getMessageUrlRegex()) || []; await Promise.all( @@ -509,6 +768,99 @@ class E2E extends Emitter { return message; } + + async getSuggestedE2EEKeys(usersWaitingForE2EKeys: Record) { + const roomIds = Object.keys(usersWaitingForE2EKeys); + return Object.fromEntries( + ( + await Promise.all( + roomIds.map(async (room) => { + const e2eRoom = await this.getInstanceByRoomId(room); + + if (!e2eRoom) { + return; + } + const usersWithKeys = await e2eRoom.encryptGroupKeyForParticipantsWaitingForTheKeys(usersWaitingForE2EKeys[room]); + + if (!usersWithKeys) { + return; + } + + return [room, usersWithKeys]; + }), + ) + ).filter(isTruthy), + ); + } + + async getSample(roomIds: string[], limit = 3): Promise { + if (limit === 0) { + return []; + } + + const randomRoomIds = _.sampleSize(roomIds, ROOM_KEY_EXCHANGE_SIZE); + + const sampleIds: string[] = []; + for await (const roomId of randomRoomIds) { + const e2eroom = await this.getInstanceByRoomId(roomId); + if (!e2eroom?.hasSessionKey()) { + continue; + } + + sampleIds.push(roomId); + } + + if (!sampleIds.length) { + return this.getSample(roomIds, limit - 1); + } + + return sampleIds; + } + + async initiateKeyDistribution() { + if (this.keyDistributionInterval) { + return; + } + + const keyDistribution = async () => { + const roomIds = ChatRoom.find({ + 'usersWaitingForE2EKeys': { $exists: true }, + 'usersWaitingForE2EKeys.userId': { $ne: Meteor.userId() }, + }).map((room) => room._id); + if (!roomIds.length) { + return; + } + + // Prevent function from running and doing nothing when theres something to do + const sampleIds = await this.getSample(roomIds); + + if (!sampleIds.length) { + return; + } + + const { usersWaitingForE2EKeys = {} } = await sdk.rest.get('/v1/e2e.fetchUsersWaitingForGroupKey', { roomIds: sampleIds }); + + if (!Object.keys(usersWaitingForE2EKeys).length) { + return; + } + + const userKeysWithRooms = await this.getSuggestedE2EEKeys(usersWaitingForE2EKeys); + + if (!Object.keys(userKeysWithRooms).length) { + return; + } + + try { + await sdk.rest.post('/v1/e2e.provideUsersSuggestedGroupKeys', { usersSuggestedGroupKeys: userKeysWithRooms }); + } catch (error) { + return this.error('Error providing group key to users: ', error); + } + }; + + // Run first call right away, then schedule for 10s in the future + await keyDistribution(); + this.keyDistributionInterval = setInterval(keyDistribution, 10000); + } } export const e2e = new E2E(); diff --git a/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts b/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts index dcd1f82edbc86..860051c04d4df 100644 --- a/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts +++ b/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts @@ -1,4 +1,4 @@ -import { Subscriptions } from '@rocket.chat/models'; +import { Rooms, Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; export async function handleSuggestedGroupKey( @@ -23,6 +23,11 @@ export async function handleSuggestedGroupKey( if (handle === 'accept') { await Subscriptions.setGroupE2EKey(sub._id, suggestedKey); + await Rooms.removeUsersFromE2EEQueueByRoomId(sub.rid, [userId]); + } + + if (handle === 'reject') { + await Rooms.addUserIdToE2EEQueueByRoomIds([sub.rid], userId); } await Subscriptions.unsetGroupE2ESuggestedKey(sub._id); diff --git a/apps/meteor/app/e2e/server/functions/provideUsersSuggestedGroupKeys.ts b/apps/meteor/app/e2e/server/functions/provideUsersSuggestedGroupKeys.ts new file mode 100644 index 0000000000000..42408f398ecb9 --- /dev/null +++ b/apps/meteor/app/e2e/server/functions/provideUsersSuggestedGroupKeys.ts @@ -0,0 +1,33 @@ +import type { IRoom, IUser } from '@rocket.chat/core-typings'; +import { Rooms, Subscriptions } from '@rocket.chat/models'; + +import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; + +export const provideUsersSuggestedGroupKeys = async ( + userId: IUser['_id'], + usersSuggestedGroupKeys: Record, +) => { + const roomIds = Object.keys(usersSuggestedGroupKeys); + + if (!roomIds.length) { + return; + } + + // Process should try to process all rooms i have access instead of dying if one is not + for await (const roomId of roomIds) { + if (!(await canAccessRoomIdAsync(roomId, userId))) { + continue; + } + + const usersWithSuggestedKeys = []; + for await (const user of usersSuggestedGroupKeys[roomId]) { + const { modifiedCount } = await Subscriptions.setGroupE2ESuggestedKey(user._id, roomId, user.key); + if (!modifiedCount) { + continue; + } + usersWithSuggestedKeys.push(user._id); + } + + await Rooms.removeUsersFromE2EEQueueByRoomId(roomId, usersWithSuggestedKeys); + } +}; diff --git a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts index c00b9b872466a..94d252601bc49 100644 --- a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts +++ b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts @@ -1,11 +1,11 @@ -import { Users } from '@rocket.chat/models'; +import { Rooms, Users } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - 'e2e.setUserPublicAndPrivateKeys'({ public_key, private_key }: { public_key: string; private_key: string }): void; + 'e2e.setUserPublicAndPrivateKeys'({ public_key, private_key }: { public_key: string; private_key: string; force?: boolean }): void; } } @@ -19,9 +19,22 @@ 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', + }); + } + } + 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); }, }); diff --git a/apps/meteor/app/e2e/server/methods/updateGroupKey.ts b/apps/meteor/app/e2e/server/methods/updateGroupKey.ts index 30053cc7164ac..c856f8cf708a6 100644 --- a/apps/meteor/app/e2e/server/methods/updateGroupKey.ts +++ b/apps/meteor/app/e2e/server/methods/updateGroupKey.ts @@ -2,6 +2,8 @@ import { Subscriptions } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; + declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -11,6 +13,7 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async 'e2e.updateGroupKey'(rid, uid, key) { + methodDeprecationLogger.method('e2e.updateGroupKey', '8.0.0'); const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'e2e.acceptSuggestedGroupKey' }); @@ -27,10 +30,7 @@ Meteor.methods({ } // uid also has subscription to this room - const userSub = await Subscriptions.findOneByRoomIdAndUserId(rid, uid); - if (userSub) { - await Subscriptions.setGroupE2ESuggestedKey(userSub._id, key); - } + await Subscriptions.setGroupE2ESuggestedKey(uid, rid, key); } }, }); diff --git a/apps/meteor/ee/app/ecdh/Session.ts b/apps/meteor/app/ecdh/Session.ts similarity index 100% rename from apps/meteor/ee/app/ecdh/Session.ts rename to apps/meteor/app/ecdh/Session.ts diff --git a/apps/meteor/ee/app/ecdh/client/ClientSession.ts b/apps/meteor/app/ecdh/client/ClientSession.ts similarity index 100% rename from apps/meteor/ee/app/ecdh/client/ClientSession.ts rename to apps/meteor/app/ecdh/client/ClientSession.ts diff --git a/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts b/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts index 99a7497f13c7f..264443a1378bc 100644 --- a/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts +++ b/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts @@ -3,10 +3,17 @@ import { Meteor } from 'meteor/meteor'; import { throttledCounter } from '../../../../lib/utils/throttledCounter'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; +import { notifyOnSettingChanged } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; const incException = throttledCounter((counter) => { - Settings.incrementValueById('Uncaught_Exceptions_Count', counter).catch(console.error); + Settings.incrementValueById('Uncaught_Exceptions_Count', counter, { returnDocument: 'after' }) + .then(({ value }) => { + if (value) { + void notifyOnSettingChanged(value); + } + }) + .catch(console.error); }, 10000); class ErrorHandler { diff --git a/apps/meteor/app/federation/server/functions/helpers.ts b/apps/meteor/app/federation/server/functions/helpers.ts index 3b9090311e011..c684b7b8f74a3 100644 --- a/apps/meteor/app/federation/server/functions/helpers.ts +++ b/apps/meteor/app/federation/server/functions/helpers.ts @@ -2,6 +2,7 @@ import { isDirectMessageRoom } from '@rocket.chat/core-typings'; import type { ISubscription, IUser, IRoom } from '@rocket.chat/core-typings'; import { Settings, Users, Subscriptions } from '@rocket.chat/models'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { STATUS_ENABLED, STATUS_REGISTERING } from '../constants'; export const getNameAndDomain = (fullyQualifiedName: string): string[] => fullyQualifiedName.split('@'); @@ -14,11 +15,12 @@ export async function isRegisteringOrEnabled(): Promise { } export async function updateStatus(status: string): Promise { + // No need to call ws listener because current function is called on startup await Settings.updateValueById('FEDERATION_Status', status); } export async function updateEnabled(enabled: boolean): Promise { - await Settings.updateValueById('FEDERATION_Enabled', enabled); + (await Settings.updateValueById('FEDERATION_Enabled', enabled)).modifiedCount && void notifyOnSettingChangedById('FEDERATION_Enabled'); } export const checkRoomType = (room: IRoom): boolean => room.t === 'p' || room.t === 'd'; diff --git a/apps/meteor/app/file-upload/client/lib/fileUploadHandler.ts b/apps/meteor/app/file-upload/client/lib/fileUploadHandler.ts index e0887888c478d..c8e6f90966fd4 100644 --- a/apps/meteor/app/file-upload/client/lib/fileUploadHandler.ts +++ b/apps/meteor/app/file-upload/client/lib/fileUploadHandler.ts @@ -5,7 +5,9 @@ import { Tracker } from 'meteor/tracker'; Tracker.autorun(() => { const userId = Meteor.userId(); - if (userId) { + // Check for Meteor.loggingIn to be reactive and ensure it will process only after login finishes + // preventing race condition setting the rc_token as null forever + if (userId && Meteor.loggingIn() === false) { const secure = location.protocol === 'https:' ? '; secure' : ''; document.cookie = `rc_uid=${escape(userId)}; path=/${secure}`; diff --git a/apps/meteor/app/file-upload/server/config/AmazonS3.ts b/apps/meteor/app/file-upload/server/config/AmazonS3.ts index 567e5e5d71ebf..0f551d3b90d1c 100644 --- a/apps/meteor/app/file-upload/server/config/AmazonS3.ts +++ b/apps/meteor/app/file-upload/server/config/AmazonS3.ts @@ -1,6 +1,5 @@ import http from 'http'; import https from 'https'; -import URL from 'url'; import _ from 'underscore'; @@ -8,12 +7,12 @@ import { settings } from '../../../settings/server'; import type { S3Options } from '../../ufs/AmazonS3/server'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; import '../../ufs/AmazonS3/server'; +import { forceDownload } from './helper'; const get: FileUploadClass['get'] = async function (this: FileUploadClass, file, req, res) { - const { query } = URL.parse(req.url || '', true); - const forceDownload = typeof query.download !== 'undefined'; + const forcedDownload = forceDownload(req); - const fileUrl = await this.store.getRedirectURL(file, forceDownload); + const fileUrl = await this.store.getRedirectURL(file, forcedDownload); if (!fileUrl || !file.store) { res.end(); return; @@ -23,7 +22,7 @@ const get: FileUploadClass['get'] = async function (this: FileUploadClass, file, if (settings.get(`FileUpload_S3_Proxy_${storeType}`)) { const request = /^https:/.test(fileUrl) ? https : http; - FileUpload.proxyFile(file.name || '', fileUrl, forceDownload, request, req, res); + FileUpload.proxyFile(file.name || '', fileUrl, forcedDownload, request, req, res); return; } diff --git a/apps/meteor/app/file-upload/server/config/FileSystem.ts b/apps/meteor/app/file-upload/server/config/FileSystem.ts index 98342daf2e46d..75fdb5afc8aeb 100644 --- a/apps/meteor/app/file-upload/server/config/FileSystem.ts +++ b/apps/meteor/app/file-upload/server/config/FileSystem.ts @@ -4,6 +4,7 @@ import { UploadFS } from '../../../../server/ufs'; import { settings } from '../../../settings/server'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; import { getFileRange, setRangeHeaders } from '../lib/ranges'; +import { getContentDisposition } from './helper'; const FileSystemUploads = new FileUploadClass({ name: 'FileSystem:Uploads', @@ -26,7 +27,8 @@ const FileSystemUploads = new FileUploadClass({ } file = FileUpload.addExtensionTo(file); - res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(file.name || '')}`); + + res.setHeader('Content-Disposition', `${getContentDisposition(req)}; filename*=UTF-8''${encodeURIComponent(file.name || '')}`); file.uploadedAt && res.setHeader('Last-Modified', file.uploadedAt.toUTCString()); res.setHeader('Content-Type', file.type || 'application/octet-stream'); diff --git a/apps/meteor/app/file-upload/server/config/GoogleStorage.ts b/apps/meteor/app/file-upload/server/config/GoogleStorage.ts index 41eb4350b8768..8fb901b5a1235 100644 --- a/apps/meteor/app/file-upload/server/config/GoogleStorage.ts +++ b/apps/meteor/app/file-upload/server/config/GoogleStorage.ts @@ -1,18 +1,17 @@ import http from 'http'; import https from 'https'; -import URL from 'url'; import _ from 'underscore'; import { settings } from '../../../settings/server'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; import '../../ufs/GoogleStorage/server'; +import { forceDownload } from './helper'; const get: FileUploadClass['get'] = async function (this: FileUploadClass, file, req, res) { - const { query } = URL.parse(req.url || '', true); - const forceDownload = typeof query.download !== 'undefined'; + const forcedDownload = forceDownload(req); - const fileUrl = await this.store.getRedirectURL(file, forceDownload); + const fileUrl = await this.store.getRedirectURL(file, forcedDownload); if (!fileUrl || !file.store) { res.end(); return; @@ -22,7 +21,7 @@ const get: FileUploadClass['get'] = async function (this: FileUploadClass, file, if (settings.get(`FileUpload_GoogleStorage_Proxy_${storeType}`)) { const request = /^https:/.test(fileUrl) ? https : http; - FileUpload.proxyFile(file.name || '', fileUrl, forceDownload, request, req, res); + FileUpload.proxyFile(file.name || '', fileUrl, forcedDownload, request, req, res); return; } diff --git a/apps/meteor/app/file-upload/server/config/GridFS.ts b/apps/meteor/app/file-upload/server/config/GridFS.ts index 629d177581bfc..3bb5f806f3a75 100644 --- a/apps/meteor/app/file-upload/server/config/GridFS.ts +++ b/apps/meteor/app/file-upload/server/config/GridFS.ts @@ -9,6 +9,7 @@ import { Logger } from '@rocket.chat/logger'; import { UploadFS } from '../../../../server/ufs'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; import { getFileRange, setRangeHeaders } from '../lib/ranges'; +import { getContentDisposition } from './helper'; const logger = new Logger('FileUpload'); @@ -161,7 +162,7 @@ new FileUploadClass({ async get(file, req, res) { file = FileUpload.addExtensionTo(file); - res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(file.name || '')}`); + res.setHeader('Content-Disposition', `${getContentDisposition(req)}; filename*=UTF-8''${encodeURIComponent(file.name || '')}`); file.uploadedAt && res.setHeader('Last-Modified', file.uploadedAt.toUTCString()); res.setHeader('Content-Type', file.type || 'application/octet-stream'); res.setHeader('Content-Length', file.size || 0); diff --git a/apps/meteor/app/file-upload/server/config/helper.ts b/apps/meteor/app/file-upload/server/config/helper.ts new file mode 100644 index 0000000000000..f1c465537255c --- /dev/null +++ b/apps/meteor/app/file-upload/server/config/helper.ts @@ -0,0 +1,21 @@ +import type http from 'http'; +import URL from 'url'; + +export const forceDownload = (req: http.IncomingMessage): boolean => { + const { query } = URL.parse(req.url || '', true); + + const forceDownload = typeof query.download !== 'undefined'; + if (forceDownload) { + return true; + } + + return query.contentDisposition === 'attachment'; +}; + +export const getContentDisposition = (req: http.IncomingMessage): string => { + const { query } = URL.parse(req.url || '', true); + if (query.contentDisposition === 'inline') { + return 'inline'; + } + return 'attachment'; +}; diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.ts b/apps/meteor/app/file-upload/server/lib/FileUpload.ts index 4458f9d618813..08e2ccb0a52be 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.ts +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.ts @@ -28,7 +28,7 @@ import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { UploadFS } from '../../../../server/ufs'; import { ufsComplete } from '../../../../server/ufs/ufs-methods'; import type { Store, StoreOptions } from '../../../../server/ufs/ufs-store'; -import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; +import { canAccessRoomAsync, canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { settings } from '../../../settings/server'; import { mime } from '../../../utils/lib/mimeTypes'; import { isValidJWT, generateJWT } from '../../../utils/server/lib/JWTHelper'; @@ -170,7 +170,7 @@ export const FileUpload = { throw new Meteor.Error('error-file-too-large', reason); } - if (!fileUploadIsValidContentType(file.type as string, '')) { + if (!fileUploadIsValidContentType(file?.type)) { const reason = i18n.t('File_type_is_not_accepted', { lng: language }); throw new Meteor.Error('error-invalid-file-type', reason); } @@ -420,7 +420,6 @@ export const FileUpload = { await Avatars.deleteFile(oldAvatar._id); } await Avatars.updateFileNameById(file._id, user.username); - // console.log('upload finished ->', file); }, async requestCanAccessFiles({ headers = {}, url }: http.IncomingMessage, file?: IUpload) { @@ -464,16 +463,26 @@ export const FileUpload = { return false; } - if (!settings.get('FileUpload_Restrict_to_room_members') || !file?.rid) { + if (!file?.rid) { return true; } - const subscription = await Subscriptions.findOneByRoomIdAndUserId(file.rid, user._id, { projection: { _id: 1 } }); + const fileUploadRestrictedToMembers = settings.get('FileUpload_Restrict_to_room_members'); + const fileUploadRestrictToUsersWhoCanAccessRoom = settings.get('FileUpload_Restrict_to_users_who_can_access_room'); - if (subscription) { + if (!fileUploadRestrictToUsersWhoCanAccessRoom && !fileUploadRestrictedToMembers) { return true; } + if (fileUploadRestrictedToMembers && !fileUploadRestrictToUsersWhoCanAccessRoom) { + const sub = await Subscriptions.findOneByRoomIdAndUserId(file.rid, user._id, { projection: { _id: 1 } }); + return !!sub; + } + + if (fileUploadRestrictToUsersWhoCanAccessRoom && !fileUploadRestrictedToMembers) { + return canAccessRoomIdAsync(file.rid, user._id); + } + return false; }, diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index e4ca2ab729d8b..485528a5e62fe 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -159,6 +159,13 @@ export const sendFileMessage = async ( file: Partial; msgData?: Record; }, + { + parseAttachmentsForE2EE, + }: { + parseAttachmentsForE2EE: boolean; + } = { + parseAttachmentsForE2EE: true, + }, ): Promise => { const user = await Users.findOneById(userId); if (!user) { @@ -186,22 +193,33 @@ export const sendFileMessage = async ( msg: Match.Optional(String), tmid: Match.Optional(String), customFields: Match.Optional(String), + t: Match.Optional(String), + content: Match.Optional( + Match.ObjectIncluding({ + algorithm: String, + ciphertext: String, + }), + ), }), ); - const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); - - const msg = await executeSendMessage(userId, { + const data = { rid: roomId, ts: new Date(), - file: files[0], - files, - attachments, ...(msgData as Partial), ...(msgData?.customFields && { customFields: JSON.parse(msgData.customFields) }), msg: msgData?.msg ?? '', groupable: msgData?.groupable ?? false, - }); + }; + + if (parseAttachmentsForE2EE || msgData?.t !== 'e2e') { + const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); + data.file = files[0]; + data.files = files; + data.attachments = attachments; + } + + const msg = await executeSendMessage(userId, data); callbacks.runAsync('afterFileUpload', { user, room, message: msg }); diff --git a/apps/meteor/app/importer-csv/server/CsvImporter.ts b/apps/meteor/app/importer-csv/server/CsvImporter.ts index 302aeb882ac59..60c07c3288ce0 100644 --- a/apps/meteor/app/importer-csv/server/CsvImporter.ts +++ b/apps/meteor/app/importer-csv/server/CsvImporter.ts @@ -7,6 +7,7 @@ import { Importer, ProgressStep, ImporterWebsocket } from '../../importer/server import type { IConverterOptions } from '../../importer/server/classes/ImportDataConverter'; import type { ImporterProgress } from '../../importer/server/classes/ImporterProgress'; import type { ImporterInfo } from '../../importer/server/definitions/ImporterInfo'; +import { notifyOnSettingChanged } from '../../lib/server/lib/notifyListener'; export class CsvImporter extends Importer { private csvParser: (csv: string) => string[]; @@ -236,7 +237,10 @@ export class CsvImporter extends Importer { } if (usersCount) { - await Settings.incrementValueById('CSV_Importer_Count', usersCount); + const { value } = await Settings.incrementValueById('CSV_Importer_Count', usersCount, { returnDocument: 'after' }); + if (value) { + void notifyOnSettingChanged(value); + } } // Check if any of the message usernames was not in the imported list of users diff --git a/apps/meteor/app/importer-hipchat-enterprise/server/HipChatEnterpriseImporter.js b/apps/meteor/app/importer-hipchat-enterprise/server/HipChatEnterpriseImporter.js index ac3d278d82abc..663300e44154e 100644 --- a/apps/meteor/app/importer-hipchat-enterprise/server/HipChatEnterpriseImporter.js +++ b/apps/meteor/app/importer-hipchat-enterprise/server/HipChatEnterpriseImporter.js @@ -6,6 +6,7 @@ import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { Importer, ProgressStep } from '../../importer/server'; +import { notifyOnSettingChanged } from '../../lib/server/lib/notifyListener'; /** @deprecated HipChat was discontinued at 2019-02-15 */ export class HipChatEnterpriseImporter extends Importer { @@ -54,7 +55,11 @@ export class HipChatEnterpriseImporter extends Importer { this.converter.addUser(newUser); } - await Settings.incrementValueById('Hipchat_Enterprise_Importer_Count', count); + const { value } = await Settings.incrementValueById('Hipchat_Enterprise_Importer_Count', count, { returnDocument: 'after' }); + if (value) { + void notifyOnSettingChanged(value); + } + await super.updateRecord({ 'count.users': count }); await super.addCountToTotal(count); } diff --git a/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts b/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts index 2c26531bd5c41..95461820bf2da 100644 --- a/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts +++ b/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts @@ -9,6 +9,7 @@ import { Importer, ProgressStep } from '../../importer/server'; import type { IConverterOptions } from '../../importer/server/classes/ImportDataConverter'; import type { ImporterProgress } from '../../importer/server/classes/ImporterProgress'; import type { ImporterInfo } from '../../importer/server/definitions/ImporterInfo'; +import { notifyOnSettingChanged } from '../../lib/server/lib/notifyListener'; export class SlackUsersImporter extends Importer { private csvParser: (csv: string) => string[]; @@ -93,7 +94,12 @@ export class SlackUsersImporter extends Importer { await super.updateProgress(ProgressStep.USER_SELECTION); await super.addCountToTotal(userCount); - await Settings.incrementValueById('Slack_Users_Importer_Count', userCount); + + const { value } = await Settings.incrementValueById('Slack_Users_Importer_Count', userCount, { returnDocument: 'after' }); + if (value) { + void notifyOnSettingChanged(value); + } + await super.updateRecord({ 'count.users': userCount }); return super.getProgress(); } diff --git a/apps/meteor/app/importer-slack/server/SlackImporter.ts b/apps/meteor/app/importer-slack/server/SlackImporter.ts index 0ef81c69a1e06..344db66565310 100644 --- a/apps/meteor/app/importer-slack/server/SlackImporter.ts +++ b/apps/meteor/app/importer-slack/server/SlackImporter.ts @@ -4,6 +4,7 @@ import type { IZipEntry } from 'adm-zip'; import { Importer, ProgressStep, ImporterWebsocket } from '../../importer/server'; import type { ImporterProgress } from '../../importer/server/classes/ImporterProgress'; +import { notifyOnSettingChanged } from '../../lib/server/lib/notifyListener'; import { MentionsParser } from '../../mentions/lib/MentionsParser'; import { settings } from '../../settings/server'; import { getUserAvatarURL } from '../../utils/server/getUserAvatarURL'; @@ -337,7 +338,10 @@ export class SlackImporter extends Importer { } if (userCount) { - await Settings.incrementValueById('Slack_Importer_Count', userCount); + const { value } = await Settings.incrementValueById('Slack_Importer_Count', userCount, { returnDocument: 'after' }); + if (value) { + void notifyOnSettingChanged(value); + } } const missedTypes: Record = {}; diff --git a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts index 493d14061bf23..7b1e71eaa0f03 100644 --- a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts +++ b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts @@ -28,6 +28,7 @@ import { generateUsernameSuggestion } from '../../../lib/server/functions/getUse import { insertMessage } from '../../../lib/server/functions/insertMessage'; import { saveUserIdentity } from '../../../lib/server/functions/saveUserIdentity'; import { setUserActiveStatus } from '../../../lib/server/functions/setUserActiveStatus'; +import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; import { createChannelMethod } from '../../../lib/server/methods/createChannel'; import { createPrivateGroupMethod } from '../../../lib/server/methods/createPrivateGroup'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; @@ -250,6 +251,9 @@ export class ImportDataConverter { async updateUser(existingUser: IUser, userData: IImportUser): Promise { const { _id } = existingUser; + if (!_id) { + return; + } userData._id = _id; @@ -297,10 +301,12 @@ export class ImportDataConverter { // Deleted users are 'inactive' users in Rocket.Chat if (userData.deleted && existingUser?.active) { - userData._id && (await setUserActiveStatus(userData._id, false, true)); + await setUserActiveStatus(_id, false, true); } else if (userData.deleted === false && existingUser?.active === false) { - userData._id && (await setUserActiveStatus(userData._id, true)); + await setUserActiveStatus(_id, true); } + + void notifyOnUserChange({ clientAction: 'updated', id: _id, diff: updateData.$set }); } private async hashPassword(password: string): Promise { diff --git a/apps/meteor/app/importer/server/classes/Importer.ts b/apps/meteor/app/importer/server/classes/Importer.ts index 68a12513a06c6..846f9ef4b4f54 100644 --- a/apps/meteor/app/importer/server/classes/Importer.ts +++ b/apps/meteor/app/importer/server/classes/Importer.ts @@ -7,6 +7,7 @@ import type { MatchKeysAndValues, MongoServerError } from 'mongodb'; import { Selection, SelectionChannel, SelectionUser } from '..'; import { callbacks } from '../../../../lib/callbacks'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { t } from '../../../utils/lib/i18n'; import { ProgressStep, ImportPreparingStartedStates } from '../../lib/ImporterProgressStep'; import type { ImporterInfo } from '../definitions/ImporterInfo'; @@ -245,10 +246,20 @@ export class Importer { } async applySettingValues(settingValues: OldSettings) { - await Settings.updateValueById('Accounts_AllowUsernameChange', settingValues.allowUsernameChange ?? true); - await Settings.updateValueById('FileUpload_MaxFileSize', settingValues.maxFileSize ?? -1); - await Settings.updateValueById('FileUpload_MediaTypeWhiteList', settingValues.mediaTypeWhiteList ?? '*'); - await Settings.updateValueById('FileUpload_MediaTypeBlackList', settingValues.mediaTypeBlackList ?? ''); + const settingsIds = [ + { _id: 'Accounts_AllowUsernameChange', value: settingValues.allowUsernameChange ?? true }, + { _id: 'FileUpload_MaxFileSize', value: settingValues.maxFileSize ?? -1 }, + { _id: 'FileUpload_MediaTypeWhiteList', value: settingValues.mediaTypeWhiteList ?? '*' }, + { _id: 'FileUpload_MediaTypeBlackList', value: settingValues.mediaTypeBlackList ?? '' }, + ]; + + const promises = settingsIds.map((setting) => Settings.updateValueById(setting._id, setting.value)); + + (await Promise.all(promises)).forEach((value, index) => { + if (value?.modifiedCount) { + void notifyOnSettingChangedById(settingsIds[index]._id); + } + }); } getProgress(): ImporterProgress { diff --git a/apps/meteor/app/integrations/server/lib/updateHistory.ts b/apps/meteor/app/integrations/server/lib/updateHistory.ts index ed304403e8c75..e8068ad82ac17 100644 --- a/apps/meteor/app/integrations/server/lib/updateHistory.ts +++ b/apps/meteor/app/integrations/server/lib/updateHistory.ts @@ -1,8 +1,8 @@ import type { IIntegrationHistory, OutgoingIntegrationEvent, IIntegration, IMessage, AtLeast } from '@rocket.chat/core-typings'; import { IntegrationHistory } from '@rocket.chat/models'; -import { Random } from '@rocket.chat/random'; import { omit } from '../../../../lib/utils/omit'; +import { notifyOnIntegrationHistoryChangedById, notifyOnIntegrationHistoryChanged } from '../../../lib/server/lib/notifyListener'; export const updateHistory = async ({ historyId, @@ -77,7 +77,12 @@ export const updateHistory = async ({ }; if (historyId) { - await IntegrationHistory.updateOne({ _id: historyId }, { $set: history }); + // Projecting just integration field to comply with existing listener behaviour + const integrationHistory = await IntegrationHistory.updateById(historyId, history, { projection: { 'integration._id': 1 } }); + if (!integrationHistory) { + throw new Error('error-updating-integration-history'); + } + void notifyOnIntegrationHistoryChanged(integrationHistory, 'updated', history); return historyId; } @@ -86,11 +91,15 @@ export const updateHistory = async ({ throw new Error('error-invalid-integration'); } - history._createdAt = new Date(); + // TODO: Had to force type cast here because of function's signature + // It would be easier if we separate into create and update functions + const { insertedId } = await IntegrationHistory.create(history as IIntegrationHistory); - const _id = Random.id(); + if (!insertedId) { + throw new Error('error-creating-integration-history'); + } - await IntegrationHistory.insertOne({ _id, ...history } as IIntegrationHistory); + void notifyOnIntegrationHistoryChangedById(insertedId, 'inserted'); - return _id; + return insertedId; }; diff --git a/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts b/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts index 2447683bd2914..5b8f13ef1a3ad 100644 --- a/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts +++ b/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts @@ -41,6 +41,7 @@ Meteor.methods({ }); } + // Don't sending to IntegrationHistory listener since it don't waits for 'removed' events. await IntegrationHistory.removeByIntegrationId(integrationId); notifications.streamIntegrationHistory.emit(integrationId, { type: 'removed', id: integrationId }); diff --git a/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts index cc3d138c554a3..c9f2211d835b5 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts @@ -41,6 +41,7 @@ export const deleteOutgoingIntegration = async (integrationId: string, userId: s } await Integrations.removeById(integrationId); + // Don't sending to IntegrationHistory listener since it don't waits for 'removed' events. await IntegrationHistory.removeByIntegrationId(integrationId); void notifyOnIntegrationChangedById(integrationId, 'removed'); }; diff --git a/apps/meteor/app/invites/server/functions/sendInvitationEmail.ts b/apps/meteor/app/invites/server/functions/sendInvitationEmail.ts index 1c00671ae41d4..87b8133e34917 100644 --- a/apps/meteor/app/invites/server/functions/sendInvitationEmail.ts +++ b/apps/meteor/app/invites/server/functions/sendInvitationEmail.ts @@ -3,6 +3,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { notifyOnSettingChanged } from '../../../lib/server/lib/notifyListener'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; @@ -53,7 +54,11 @@ export const sendInvitationEmail = async (userId: string, emails: string[]) => { }, }); - await Settings.incrementValueById('Invitation_Email_Count'); + const { value } = await Settings.incrementValueById('Invitation_Email_Count', 1, { returnDocument: 'after' }); + if (value) { + void notifyOnSettingChanged(value); + } + continue; } catch ({ message }: any) { throw new Meteor.Error('error-email-send-failed', `Error trying to send email: ${message}`, { diff --git a/apps/meteor/app/irc/server/irc-bridge/index.js b/apps/meteor/app/irc/server/irc-bridge/index.js index 9ab6c47987d03..09b7a3568362c 100644 --- a/apps/meteor/app/irc/server/irc-bridge/index.js +++ b/apps/meteor/app/irc/server/irc-bridge/index.js @@ -7,6 +7,7 @@ import { callbacks } from '../../../../lib/callbacks'; import { afterLeaveRoomCallback } from '../../../../lib/callbacks/afterLeaveRoomCallback'; import { afterLogoutCleanUpCallback } from '../../../../lib/callbacks/afterLogoutCleanUpCallback'; import { withThrottling } from '../../../../lib/utils/highOrderFunctions'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import * as servers from '../servers'; import * as localCommandHandlers from './localHandlers'; import * as peerCommandHandlers from './peerHandlers'; @@ -19,15 +20,13 @@ const updateLastPing = withThrottling({ wait: 10_000 })(() => { if (removed) { return; } - void Settings.updateOne( - { _id: 'IRC_Bridge_Last_Ping' }, - { - $set: { - value: new Date(), - }, - }, - { upsert: true }, - ); + + void (async () => { + const updatedValue = await Settings.updateValueById('IRC_Bridge_Last_Ping', new Date(), { upsert: true }); + if (updatedValue.modifiedCount || updatedValue.upsertedCount) { + void notifyOnSettingChangedById('IRC_Bridge_Last_Ping'); + } + })(); }); class Bridge { diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/disconnected.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/disconnected.js index ed6d635a5721e..3429a977fd1a6 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/disconnected.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/disconnected.js @@ -1,5 +1,7 @@ import { Users } from '@rocket.chat/models'; +import { notifyOnUserChange } from '../../../../lib/server/lib/notifyListener'; + export default async function handleQUIT(args) { const user = await Users.findOne({ 'profile.irc.nick': args.nick, @@ -13,4 +15,6 @@ export default async function handleQUIT(args) { }, }, ); + + void notifyOnUserChange({ id: user._id, clientAction: 'updated', diff: { status: 'offline' } }); } diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/nickChanged.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/nickChanged.js index fe5df0c9540e5..96a8ebcb3dc90 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/nickChanged.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/nickChanged.js @@ -1,5 +1,7 @@ import { Users } from '@rocket.chat/models'; +import { notifyOnUserChange } from '../../../../lib/server/lib/notifyListener'; + export default async function handleNickChanged(args) { const user = await Users.findOne({ 'profile.irc.nick': args.nick, @@ -21,4 +23,6 @@ export default async function handleNickChanged(args) { }, }, ); + + void notifyOnUserChange({ clientAction: 'updated', id: user._id, diff: { name: args.newNick } }); } diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/userRegistered.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/userRegistered.js index b4279ae008b4e..5e04d7b79407b 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/userRegistered.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/userRegistered.js @@ -1,5 +1,7 @@ import { Users } from '@rocket.chat/models'; +import { notifyOnUserChange } from '../../../../lib/server/lib/notifyListener'; + export default async function handleUserRegistered(args) { // Check if there is an user with the given username let user = await Users.findOne({ @@ -28,6 +30,8 @@ export default async function handleUserRegistered(args) { }; user = await Users.create(userToInsert); + + void notifyOnUserChange({ id: user._id, clientAction: 'inserted', data: user }); } else { // ...otherwise, log the user in and update the information this.log(`Logging in ${args.username} with nick: ${args.nick}`); @@ -43,5 +47,7 @@ export default async function handleUserRegistered(args) { }, }, ); + + void notifyOnUserChange({ id: user._id, clientAction: 'updated', diff: { status: 'online' } }); } } diff --git a/apps/meteor/app/irc/server/methods/resetIrcConnection.ts b/apps/meteor/app/irc/server/methods/resetIrcConnection.ts index 1bbc5b6a3ef82..24eef975d5d54 100644 --- a/apps/meteor/app/irc/server/methods/resetIrcConnection.ts +++ b/apps/meteor/app/irc/server/methods/resetIrcConnection.ts @@ -2,6 +2,7 @@ import { Settings } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import Bridge from '../irc-bridge'; @@ -16,29 +17,15 @@ Meteor.methods({ async resetIrcConnection() { const ircEnabled = Boolean(settings.get('IRC_Enabled')); - await Settings.updateOne( - { _id: 'IRC_Bridge_Last_Ping' }, - { - $set: { - value: new Date(0), - }, - }, - { - upsert: true, - }, - ); + const updatedLastPingValue = await Settings.updateValueById('IRC_Bridge_Last_Ping', new Date(0), { upsert: true }); + if (updatedLastPingValue.modifiedCount || updatedLastPingValue.upsertedCount) { + void notifyOnSettingChangedById('IRC_Bridge_Last_Ping'); + } - await Settings.updateOne( - { _id: 'IRC_Bridge_Reset_Time' }, - { - $set: { - value: new Date(), - }, - }, - { - upsert: true, - }, - ); + const updatedResetTimeValue = await Settings.updateValueById('IRC_Bridge_Reset_Time', new Date(), { upsert: true }); + if (updatedResetTimeValue.modifiedCount || updatedResetTimeValue.upsertedCount) { + void notifyOnSettingChangedById('IRC_Bridge_Last_Ping'); + } if (!ircEnabled) { return { diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts index e377ba3c46009..57ea20f00cb1e 100644 --- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts +++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts @@ -9,6 +9,7 @@ import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; import { callbacks } from '../../../../lib/callbacks'; import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; +import { settings } from '../../../settings/server'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; import { notifyOnRoomChangedById } from '../lib/notifyListener'; @@ -132,5 +133,9 @@ export const addUserToRoom = async function ( await Team.addMember(inviter || userToBeAdded, userToBeAdded._id, room.teamId); } + if (room.encrypted && settings.get('E2E_Enable') && userToBeAdded.e2e?.public_key) { + await Rooms.addUserIdToE2EEQueueByRoomIds([room._id], userToBeAdded._id); + } + return true; }; diff --git a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts index f53061995152f..2bfb1086c6357 100644 --- a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts +++ b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts @@ -111,7 +111,7 @@ export async function cleanRoomHistory({ } if (count) { - const lastMessage = await Messages.getLastVisibleMessageSentWithNoTypeByRoomId(rid); + const lastMessage = await Messages.getLastVisibleUserMessageSentByRoomId(rid); await Rooms.resetLastMessageById(rid, lastMessage, -count); diff --git a/apps/meteor/app/lib/server/functions/deleteMessage.ts b/apps/meteor/app/lib/server/functions/deleteMessage.ts index 4582a88823ecd..e977874b34540 100644 --- a/apps/meteor/app/lib/server/functions/deleteMessage.ts +++ b/apps/meteor/app/lib/server/functions/deleteMessage.ts @@ -81,7 +81,7 @@ export async function deleteMessage(message: IMessage, user: IUser): Promise 0) { + departmentAgents.forEach((depAgent) => { + void notifyOnLivechatDepartmentAgentChanged( + { + _id: depAgent._id, + agentId: userId, + departmentId: depAgent.departmentId, + }, + 'removed', + ); + }); + } } if (user.roles.includes('livechat-monitor')) { @@ -141,5 +161,7 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele // Refresh the servers list await FederationServers.refreshServers(); + void notifyOnUserChange({ clientAction: 'removed', id: user._id }); + await callbacks.run('afterDeleteUser', user); } diff --git a/apps/meteor/app/lib/server/functions/loadMessageHistory.ts b/apps/meteor/app/lib/server/functions/loadMessageHistory.ts index 2f6b7a1f694dd..fee7061cae963 100644 --- a/apps/meteor/app/lib/server/functions/loadMessageHistory.ts +++ b/apps/meteor/app/lib/server/functions/loadMessageHistory.ts @@ -1,7 +1,8 @@ -import type { IMessage } from '@rocket.chat/core-typings'; +import type { IMessage, MessageTypesValues } from '@rocket.chat/core-typings'; import { Messages, Rooms } from '@rocket.chat/models'; import type { FindOptions } from 'mongodb'; +import { settings } from '../../../settings/server/cached'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { getHiddenSystemMessages } from '../lib/getHiddenSystemMessages'; @@ -29,7 +30,9 @@ export async function loadMessageHistory({ throw new Error('error-invalid-room'); } - const hiddenMessageTypes = getHiddenSystemMessages(room); + const hiddenSystemMessages = settings.get('Hide_System_Messages'); + + const hiddenMessageTypes = getHiddenSystemMessages(room, hiddenSystemMessages); const options: FindOptions = { sort: { diff --git a/apps/meteor/app/lib/server/functions/notifications/email.js b/apps/meteor/app/lib/server/functions/notifications/email.js index dfc6a1716703a..8fa1eb02a29fc 100644 --- a/apps/meteor/app/lib/server/functions/notifications/email.js +++ b/apps/meteor/app/lib/server/functions/notifications/email.js @@ -21,7 +21,7 @@ Meteor.startup(() => { }); }); -async function getEmailContent({ message, user, room }) { +export async function getEmailContent({ message, user, room }) { const lng = (user && user.language) || settings.get('Language') || 'en'; const roomName = escapeHTML(`#${await roomCoordinator.getRoomName(room.t, room)}`); @@ -35,6 +35,10 @@ async function getEmailContent({ message, user, room }) { lng, }); + if (message.t === 'e2e' && !message.file) { + return settings.get('Email_notification_show_message') ? i18n.t('Encrypted_message_preview_unavailable', { lng }) : header; + } + if (message.msg !== '') { if (!settings.get('Email_notification_show_message')) { return header; @@ -42,10 +46,6 @@ async function getEmailContent({ message, user, room }) { let messageContent = escapeHTML(message.msg); - if (message.t === 'e2e') { - messageContent = i18n.t('Encrypted_message', { lng }); - } - message = await callbacks.run('renderMessage', message); if (message.tokens && message.tokens.length > 0) { message.tokens.forEach((token) => { diff --git a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts index e593b35080542..3b065c68f15c4 100644 --- a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts +++ b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { afterLeaveRoomCallback } from '../../../../lib/callbacks/afterLeaveRoomCallback'; import { beforeLeaveRoomCallback } from '../../../../lib/callbacks/beforeLeaveRoomCallback'; +import { settings } from '../../../settings/server'; import { notifyOnRoomChangedById } from '../lib/notifyListener'; export const removeUserFromRoom = async function ( @@ -65,6 +66,10 @@ export const removeUserFromRoom = async function ( await Team.removeMember(room.teamId, user._id); } + if (room.encrypted && settings.get('E2E_Enable')) { + await Rooms.removeUsersFromE2EEQueueByRoomId(room._id, [user._id]); + } + // TODO: CACHE: maybe a queue? await afterLeaveRoomCallback.run(user, room); diff --git a/apps/meteor/app/lib/server/functions/saveUser.js b/apps/meteor/app/lib/server/functions/saveUser.js index 3a2808b4171cc..1931333038b61 100644 --- a/apps/meteor/app/lib/server/functions/saveUser.js +++ b/apps/meteor/app/lib/server/functions/saveUser.js @@ -16,6 +16,7 @@ import { settings } from '../../../settings/server'; import { safeGetMeteorUser } from '../../../utils/server/functions/safeGetMeteorUser'; import { validateEmailDomain } from '../lib'; import { generatePassword } from '../lib/generatePassword'; +import { notifyOnUserChangeById, notifyOnUserChange } from '../lib/notifyListener'; import { passwordPolicy } from '../lib/passwordPolicy'; import { checkEmailAvailability } from './checkEmailAvailability'; import { checkUsernameAvailability } from './checkUsernameAvailability'; @@ -329,6 +330,8 @@ const saveNewUser = async function (userData, sendPassword) { } } + void notifyOnUserChangeById({ clientAction: 'inserted', id: _id }); + return _id; }; @@ -401,6 +404,7 @@ export const saveUser = async function (userId, userData) { const updateUser = { $set: {}, + $unset: {}, }; handleBio(updateUser, userData.bio); @@ -419,6 +423,9 @@ export const saveUser = async function (userId, userData) { if (typeof userData.requirePasswordChange !== 'undefined') { updateUser.$set.requirePasswordChange = userData.requirePasswordChange; + if (!userData.requirePasswordChange) { + updateUser.$unset.requirePasswordChangeReason = 1; + } } if (typeof userData.verified === 'boolean') { @@ -428,7 +435,7 @@ export const saveUser = async function (userId, userData) { await Users.updateOne({ _id: userData._id }, updateUser); // App IPostUserUpdated event hook - const userUpdated = await Users.findOneById(userId); + const userUpdated = await Users.findOneById(userData._id); await callbacks.run('afterSaveUser', { user: userUpdated, @@ -445,5 +452,17 @@ export const saveUser = async function (userId, userData) { await _sendUserEmail(settings.get('Password_Changed_Email_Subject'), passwordChangedHtml, userData); } + if (typeof userData.verified === 'boolean') { + delete userData.verified; + } + void notifyOnUserChange({ + clientAction: 'updated', + id: userData._id, + diff: { + ...userData, + emails: userUpdated.emails, + }, + }); + return true; }; diff --git a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts index fabf59669450d..e3104db280dd1 100644 --- a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts +++ b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts @@ -8,7 +8,7 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../lib/callbacks'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; -import { notifyOnRoomChangedById, notifyOnRoomChangedByUserDM } from '../lib/notifyListener'; +import { notifyOnRoomChangedById, notifyOnRoomChangedByUserDM, notifyOnUserChange } from '../lib/notifyListener'; import { closeOmnichannelConversations } from './closeOmnichannelConversations'; import { shouldRemoveOrChangeOwner, getSubscribedRoomsForUserWithDetails } from './getRoomsWithSingleOwner'; import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; @@ -107,11 +107,16 @@ export async function setUserActiveStatus(userId: string, active: boolean, confi if (active === false) { await Users.unsetLoginTokens(userId); await Rooms.setDmReadOnlyByUserId(userId, undefined, true, false); + + void notifyOnUserChange({ clientAction: 'updated', id: userId, diff: { 'services.resume.loginTokens': [], active } }); void notifyOnRoomChangedByUserDM(userId); } else { await Users.unsetReason(userId); + + void notifyOnUserChange({ clientAction: 'updated', id: userId, diff: { active } }); await reactivateDirectConversations(userId); } + if (active && !settings.get('Accounts_Send_Email_When_Activating')) { return true; } diff --git a/apps/meteor/app/lib/server/functions/setUsername.ts b/apps/meteor/app/lib/server/functions/setUsername.ts index 319202cefea4f..e19ef874db0fd 100644 --- a/apps/meteor/app/lib/server/functions/setUsername.ts +++ b/apps/meteor/app/lib/server/functions/setUsername.ts @@ -10,6 +10,7 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { settings } from '../../../settings/server'; import { RateLimiter } from '../lib'; +import { notifyOnUserChange } from '../lib/notifyListener'; import { addUserToRoom } from './addUserToRoom'; import { checkUsernameAvailability } from './checkUsernameAvailability'; import { getAvatarSuggestionForUser } from './getAvatarSuggestionForUser'; @@ -67,6 +68,8 @@ export const setUsernameWithValidation = async (userId: string, username: string await joinDefaultChannels(user._id, joinDefaultChannelsSilenced); setImmediate(async () => callbacks.run('afterCreateUser', user)); } + + void notifyOnUserChange({ clientAction: 'updated', id: user._id, diff: { username } }); }; export const _setUsername = async function (userId: string, u: string, fullUser: IUser): Promise { diff --git a/apps/meteor/app/lib/server/functions/updateMessage.ts b/apps/meteor/app/lib/server/functions/updateMessage.ts index 8327b0892b260..2954517fb0184 100644 --- a/apps/meteor/app/lib/server/functions/updateMessage.ts +++ b/apps/meteor/app/lib/server/functions/updateMessage.ts @@ -94,17 +94,21 @@ export const updateMessage = async function ( setImmediate(async () => { const msg = await Messages.findOneById(_id); - if (msg) { - await callbacks.run('afterSaveMessage', msg, room, user._id); + if (!msg) { + return; + } + + // although this is an "afterSave" kind callback, we know they can extend message's properties + // so we wait for it to run before broadcasting + const data = await callbacks.run('afterSaveMessage', msg, room, user._id); - void broadcastMessageFromData({ - id: msg._id, - data: msg, - }); + void broadcastMessageFromData({ + id: msg._id, + data: data as any, // TODO move "afterSaveMessage" type definition to specify a return value + }); - if (room?.lastMessage?._id === msg._id) { - void notifyOnRoomChangedById(message.rid); - } + if (room?.lastMessage?._id === msg._id) { + void notifyOnRoomChangedById(message.rid); } }); }; diff --git a/apps/meteor/app/lib/server/lib/getHiddenSystemMessages.ts b/apps/meteor/app/lib/server/lib/getHiddenSystemMessages.ts index 74b31b63da6a0..08f52620e0805 100644 --- a/apps/meteor/app/lib/server/lib/getHiddenSystemMessages.ts +++ b/apps/meteor/app/lib/server/lib/getHiddenSystemMessages.ts @@ -1,26 +1,10 @@ import type { MessageTypesValues, IRoom } from '@rocket.chat/core-typings'; -import { settings } from '../../../settings/server'; - -const hideMessagesOfTypeServer = new Set(); - -settings.watch('Hide_System_Messages', (values) => { - if (!values || !Array.isArray(values)) { - return; - } - - const hiddenTypes = values.reduce((array, value): MessageTypesValues[] => { +export const getHiddenSystemMessages = (room: IRoom, hiddenSystemMessages: MessageTypesValues[]): MessageTypesValues[] => { + const hiddenTypes = hiddenSystemMessages.reduce((array, value): MessageTypesValues[] => { const newValue: MessageTypesValues[] = value === 'mute_unmute' ? ['user-muted', 'user-unmuted'] : [value]; - return [...array, ...newValue]; }, [] as MessageTypesValues[]); - hideMessagesOfTypeServer.clear(); - - hiddenTypes.forEach((item) => hideMessagesOfTypeServer.add(item)); -}); - -// TODO probably remove on chained event system -export function getHiddenSystemMessages(room: IRoom): MessageTypesValues[] { - return Array.isArray(room?.sysMes) ? room.sysMes : [...hideMessagesOfTypeServer]; -} + return Array.isArray(room?.sysMes) ? room.sysMes : hiddenTypes; +}; diff --git a/apps/meteor/app/lib/server/lib/notifyListener.ts b/apps/meteor/app/lib/server/lib/notifyListener.ts index c019eba0db7ae..f4e948390c99b 100644 --- a/apps/meteor/app/lib/server/lib/notifyListener.ts +++ b/apps/meteor/app/lib/server/lib/notifyListener.ts @@ -9,9 +9,28 @@ import type { IIntegration, IPbxEvent, LoginServiceConfiguration as LoginServiceConfigurationData, + ILivechatInquiryRecord, ILivechatPriority, + ILivechatDepartmentAgents, + IEmailInbox, + IIntegrationHistory, + AtLeast, + ISettingColor, + IUser, } from '@rocket.chat/core-typings'; -import { Rooms, Permissions, Settings, PbxEvents, Roles, Integrations, LoginServiceConfiguration } from '@rocket.chat/models'; +import { + Rooms, + Permissions, + Settings, + PbxEvents, + Roles, + Integrations, + LoginServiceConfiguration, + IntegrationHistory, + LivechatInquiry, + LivechatDepartmentAgents, + Users, +} from '@rocket.chat/models'; type ClientAction = 'inserted' | 'updated' | 'removed'; @@ -91,14 +110,6 @@ export async function notifyOnRoomChangedByUserDM( } } -export async function notifyOnSettingChanged(setting: ISetting, clientAction: ClientAction = 'updated'): Promise { - if (!dbWatchersDisabled) { - return; - } - - void api.broadcast('watch.settings', { clientAction, setting }); -} - export async function notifyOnPermissionChanged(permission: IPermission, clientAction: ClientAction = 'updated'): Promise { if (!dbWatchersDisabled) { return; @@ -254,3 +265,239 @@ export async function notifyOnIntegrationChangedByChannels( + data: Pick | T, // TODO: improve typing + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('watch.emailInbox', { clientAction, id: data._id, data }); +} + +export async function notifyOnLivechatInquiryChanged( + data: ILivechatInquiryRecord | ILivechatInquiryRecord[], + clientAction: ClientAction = 'updated', + diff?: Partial & { queuedAt: unknown; takenAt: unknown }>, +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const items = Array.isArray(data) ? data : [data]; + + for (const item of items) { + void api.broadcast('watch.inquiries', { clientAction, inquiry: item, diff }); + } +} + +export async function notifyOnLivechatInquiryChangedById( + id: ILivechatInquiryRecord['_id'], + clientAction: ClientAction = 'updated', + diff?: Partial & { queuedAt: unknown; takenAt: unknown }>, +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const inquiry = clientAction === 'removed' ? await LivechatInquiry.trashFindOneById(id) : await LivechatInquiry.findOneById(id); + + if (!inquiry) { + return; + } + + void api.broadcast('watch.inquiries', { clientAction, inquiry, diff }); +} + +export async function notifyOnLivechatInquiryChangedByRoom( + rid: ILivechatInquiryRecord['rid'], + clientAction: ClientAction = 'updated', + diff?: Partial & { queuedAt: unknown; takenAt: unknown }>, +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const inquiry = await LivechatInquiry.findOneByRoomId(rid, {}); + + if (!inquiry) { + return; + } + + void api.broadcast('watch.inquiries', { clientAction, inquiry, diff }); +} + +export async function notifyOnLivechatInquiryChangedByToken( + token: ILivechatInquiryRecord['v']['token'], + clientAction: ClientAction = 'updated', + diff?: Partial & { queuedAt: unknown; takenAt: unknown }>, +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const inquiry = await LivechatInquiry.findOneByToken(token); + + if (!inquiry) { + return; + } + + void api.broadcast('watch.inquiries', { clientAction, inquiry, diff }); +} + +export async function notifyOnIntegrationHistoryChanged( + data: AtLeast, + clientAction: ClientAction = 'updated', + diff: Partial = {}, +): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('watch.integrationHistory', { clientAction, id: data._id, data, diff }); +} + +export async function notifyOnIntegrationHistoryChangedById( + id: T['_id'], + clientAction: ClientAction = 'updated', + diff: Partial = {}, +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const item = await IntegrationHistory.findOneById(id); + + if (!item) { + return; + } + + void api.broadcast('watch.integrationHistory', { clientAction, id: item._id, data: item, diff }); +} + +export async function notifyOnLivechatDepartmentAgentChanged( + data: Partial & Pick, + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('watch.livechatDepartmentAgents', { clientAction, id: data._id, data }); +} + +export async function notifyOnLivechatDepartmentAgentChangedByDepartmentId( + departmentId: T['departmentId'], + clientAction: 'inserted' | 'updated' = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const items = LivechatDepartmentAgents.findByDepartmentId(departmentId, { projection: { _id: 1, agentId: 1, departmentId: 1 } }); + + for await (const item of items) { + void api.broadcast('watch.livechatDepartmentAgents', { clientAction, id: item._id, data: item }); + } +} + +export async function notifyOnLivechatDepartmentAgentChangedByAgentsAndDepartmentId( + agentsIds: T['agentId'][], + departmentId: T['departmentId'], + clientAction: 'inserted' | 'updated' = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const items = LivechatDepartmentAgents.findByAgentsAndDepartmentId(agentsIds, departmentId, { + projection: { _id: 1, agentId: 1, departmentId: 1 }, + }); + + for await (const item of items) { + void api.broadcast('watch.livechatDepartmentAgents', { clientAction, id: item._id, data: item }); + } +} + +export async function notifyOnSettingChanged( + setting: ISetting & { editor?: ISettingColor['editor'] }, + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + void api.broadcast('watch.settings', { clientAction, setting }); +} + +export async function notifyOnSettingChangedById(id: ISetting['_id'], clientAction: ClientAction = 'updated'): Promise { + if (!dbWatchersDisabled) { + return; + } + const item = clientAction === 'removed' ? await Settings.trashFindOneById(id) : await Settings.findOneById(id); + + if (!item) { + return; + } + + void api.broadcast('watch.settings', { clientAction, setting: item }); +} + +type NotifyUserChange = { + id: IUser['_id']; + clientAction: 'inserted' | 'removed' | 'updated'; + data?: IUser; + diff?: Record; + unset?: Record; +}; + +export async function notifyOnUserChange({ clientAction, id, data, diff, unset }: NotifyUserChange) { + if (!dbWatchersDisabled) { + return; + } + if (clientAction === 'removed') { + void api.broadcast('watch.users', { clientAction, id }); + return; + } + if (clientAction === 'inserted') { + void api.broadcast('watch.users', { clientAction, id, data: data! }); + return; + } + + void api.broadcast('watch.users', { clientAction, diff: diff!, unset: unset || {}, id }); +} + +/** + * Calls the callback only if DB Watchers are disabled + */ +export async function notifyOnUserChangeAsync(cb: () => Promise) { + if (!dbWatchersDisabled) { + return; + } + + const result = await cb(); + if (!result) { + return; + } + + if (Array.isArray(result)) { + result.forEach((n) => notifyOnUserChange(n)); + return; + } + + return notifyOnUserChange(result); +} + +// TODO this may be only useful on 'inserted' +export async function notifyOnUserChangeById({ clientAction, id }: { id: IUser['_id']; clientAction: 'inserted' | 'removed' | 'updated' }) { + if (!dbWatchersDisabled) { + return; + } + const user = await Users.findOneById(id); + if (!user) { + return; + } + + void notifyOnUserChange({ id, clientAction, data: user }); +} diff --git a/apps/meteor/app/lib/server/methods/getChannelHistory.ts b/apps/meteor/app/lib/server/methods/getChannelHistory.ts index 3c68fb7a2bf29..00ff016395931 100644 --- a/apps/meteor/app/lib/server/methods/getChannelHistory.ts +++ b/apps/meteor/app/lib/server/methods/getChannelHistory.ts @@ -1,4 +1,4 @@ -import type { IMessage } from '@rocket.chat/core-typings'; +import type { IMessage, MessageTypesValues } from '@rocket.chat/core-typings'; import { Messages, Subscriptions, Rooms } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; @@ -7,6 +7,7 @@ import _ from 'underscore'; import { canAccessRoomAsync } from '../../../authorization/server'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { settings } from '../../../settings/server/cached'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { getHiddenSystemMessages } from '../lib/getHiddenSystemMessages'; @@ -67,7 +68,9 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-date', 'Invalid date', { method: 'getChannelHistory' }); } - const hiddenMessageTypes = getHiddenSystemMessages(room); + const hiddenSystemMessages = settings.get('Hide_System_Messages'); + + const hiddenMessageTypes = getHiddenSystemMessages(room, hiddenSystemMessages); const options: Record = { sort: { diff --git a/apps/meteor/app/lib/server/methods/removeOAuthService.ts b/apps/meteor/app/lib/server/methods/removeOAuthService.ts index 6d1bb688979d9..6e16dc8d2d5b5 100644 --- a/apps/meteor/app/lib/server/methods/removeOAuthService.ts +++ b/apps/meteor/app/lib/server/methods/removeOAuthService.ts @@ -5,6 +5,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { notifyOnSettingChangedById } from '../lib/notifyListener'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -31,37 +32,46 @@ Meteor.methods({ name = name.toLowerCase().replace(/[^a-z0-9_]/g, ''); name = capitalize(name); - await Promise.all([ - Settings.removeById(`Accounts_OAuth_Custom-${name}`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-url`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-token_path`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-identity_path`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-authorize_path`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-scope`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-access_token_param`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-token_sent_via`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-identity_token_sent_via`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-id`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-secret`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-button_label_text`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-button_label_color`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-button_color`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-login_style`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-key_field`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-username_field`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-email_field`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-name_field`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-avatar_field`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-roles_claim`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-merge_roles`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-roles_to_sync`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-merge_users`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-show_button`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-groups_claim`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-channels_admin`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-map_channels`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-groups_channel_map`), - Settings.removeById(`Accounts_OAuth_Custom-${name}-merge_users_distinct_services`), - ]); + + const settingsIds = [ + `Accounts_OAuth_Custom-${name}`, + `Accounts_OAuth_Custom-${name}-url`, + `Accounts_OAuth_Custom-${name}-token_path`, + `Accounts_OAuth_Custom-${name}-identity_path`, + `Accounts_OAuth_Custom-${name}-authorize_path`, + `Accounts_OAuth_Custom-${name}-scope`, + `Accounts_OAuth_Custom-${name}-access_token_param`, + `Accounts_OAuth_Custom-${name}-token_sent_via`, + `Accounts_OAuth_Custom-${name}-identity_token_sent_via`, + `Accounts_OAuth_Custom-${name}-id`, + `Accounts_OAuth_Custom-${name}-secret`, + `Accounts_OAuth_Custom-${name}-button_label_text`, + `Accounts_OAuth_Custom-${name}-button_label_color`, + `Accounts_OAuth_Custom-${name}-button_color`, + `Accounts_OAuth_Custom-${name}-login_style`, + `Accounts_OAuth_Custom-${name}-key_field`, + `Accounts_OAuth_Custom-${name}-username_field`, + `Accounts_OAuth_Custom-${name}-email_field`, + `Accounts_OAuth_Custom-${name}-name_field`, + `Accounts_OAuth_Custom-${name}-avatar_field`, + `Accounts_OAuth_Custom-${name}-roles_claim`, + `Accounts_OAuth_Custom-${name}-merge_roles`, + `Accounts_OAuth_Custom-${name}-roles_to_sync`, + `Accounts_OAuth_Custom-${name}-merge_users`, + `Accounts_OAuth_Custom-${name}-show_button`, + `Accounts_OAuth_Custom-${name}-groups_claim`, + `Accounts_OAuth_Custom-${name}-channels_admin`, + `Accounts_OAuth_Custom-${name}-map_channels`, + `Accounts_OAuth_Custom-${name}-groups_channel_map`, + `Accounts_OAuth_Custom-${name}-merge_users_distinct_services`, + ]; + + const promises = settingsIds.map((id) => Settings.removeById(id)); + + (await Promise.all(promises)).forEach((value, index) => { + if (value?.deletedCount) { + void notifyOnSettingChangedById(settingsIds[index], 'removed'); + } + }); }, }); diff --git a/apps/meteor/app/lib/server/methods/saveSetting.ts b/apps/meteor/app/lib/server/methods/saveSetting.ts index d6cd62dc74709..7f900d1751d82 100644 --- a/apps/meteor/app/lib/server/methods/saveSetting.ts +++ b/apps/meteor/app/lib/server/methods/saveSetting.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; import { getSettingPermissionId } from '../../../authorization/lib'; import { hasPermissionAsync, hasAllPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { notifyOnSettingChanged } from '../lib/notifyListener'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -56,7 +57,10 @@ Meteor.methods({ break; } - await Settings.updateValueAndEditorById(_id, value as SettingValue, editor); + (await Settings.updateValueAndEditorById(_id, value as SettingValue, editor)).modifiedCount && + setting && + void notifyOnSettingChanged({ ...setting, editor, value: value as SettingValue }); + return true; }), }); diff --git a/apps/meteor/app/lib/server/methods/saveSettings.ts b/apps/meteor/app/lib/server/methods/saveSettings.ts index 6d7c0927c3f89..8c4f92cfb88f0 100644 --- a/apps/meteor/app/lib/server/methods/saveSettings.ts +++ b/apps/meteor/app/lib/server/methods/saveSettings.ts @@ -9,6 +9,7 @@ import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; import { getSettingPermissionId } from '../../../authorization/lib'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { settings } from '../../../settings/server'; +import { notifyOnSettingChangedById } from '../lib/notifyListener'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -81,8 +82,15 @@ Meteor.methods({ case 'boolean': check(value, Boolean); break; + case 'timespan': case 'int': check(value, Number); + if (!Number.isInteger(value)) { + throw new Meteor.Error(`Invalid setting value ${value}`, 'Invalid setting value', { + method: 'saveSettings', + }); + } + break; case 'multiSelect': check(value, Array); @@ -107,7 +115,13 @@ Meteor.methods({ }); } - await Promise.all(params.map(({ _id, value }) => Settings.updateValueById(_id, value))); + const promises = params.map(({ _id, value }) => Settings.updateValueById(_id, value)); + + (await Promise.all(promises)).forEach((value, index) => { + if (value?.modifiedCount) { + void notifyOnSettingChangedById(params[index]._id); + } + }); return true; }, diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 5749daa980f39..a490b5c4c67f6 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -81,6 +81,14 @@ export async function executeSendMessage(uid: IUser['_id'], message: AtLeast('E2E_Enable') && !settings.get('E2E_Allow_Unencrypted_Messages')) { + if (message.t !== 'e2e') { + throw new Meteor.Error('error-not-allowed', 'Not allowed to send un-encrypted messages in an encrypted room', { + method: 'sendMessage', + }); + } + } + metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 return await sendMessage(user, message, room, false, previewUrls); } catch (err: any) { diff --git a/apps/meteor/app/lib/server/methods/updateMessage.ts b/apps/meteor/app/lib/server/methods/updateMessage.ts index 277841fd58e77..161a168a2cdcc 100644 --- a/apps/meteor/app/lib/server/methods/updateMessage.ts +++ b/apps/meteor/app/lib/server/methods/updateMessage.ts @@ -10,7 +10,7 @@ import { hasPermissionAsync } from '../../../authorization/server/functions/hasP import { settings } from '../../../settings/server'; import { updateMessage } from '../functions/updateMessage'; -const allowedEditedFields = ['tshow', 'alias', 'attachments', 'avatar', 'emoji', 'msg', 'customFields']; +const allowedEditedFields = ['tshow', 'alias', 'attachments', 'avatar', 'emoji', 'msg', 'customFields', 'content']; export async function executeUpdateMessage(uid: IUser['_id'], message: AtLeast, previewUrls?: string[]) { const originalMessage = await Messages.findOneById(message._id); @@ -53,7 +53,7 @@ export async function executeUpdateMessage(uid: IUser['_id'], message: AtLeast { try { diff --git a/apps/meteor/ee/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal.tsx b/apps/meteor/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal.tsx similarity index 100% rename from apps/meteor/ee/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal.tsx rename to apps/meteor/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal.tsx diff --git a/apps/meteor/ee/app/livechat-enterprise/client/index.ts b/apps/meteor/app/livechat-enterprise/client/index.ts similarity index 90% rename from apps/meteor/ee/app/livechat-enterprise/client/index.ts rename to apps/meteor/app/livechat-enterprise/client/index.ts index 1fc3ef7041392..7be8700392765 100644 --- a/apps/meteor/ee/app/livechat-enterprise/client/index.ts +++ b/apps/meteor/app/livechat-enterprise/client/index.ts @@ -1,5 +1,4 @@ import { hasLicense } from '../../license/client'; -import '../lib/messageTypes'; import './startup'; void hasLicense('livechat-enterprise').then((enabled) => { diff --git a/apps/meteor/ee/app/livechat-enterprise/lib/messageTypes.ts b/apps/meteor/app/livechat-enterprise/client/messageTypes.ts similarity index 67% rename from apps/meteor/ee/app/livechat-enterprise/lib/messageTypes.ts rename to apps/meteor/app/livechat-enterprise/client/messageTypes.ts index 9c15c277a1b27..90d390fe6be7b 100644 --- a/apps/meteor/ee/app/livechat-enterprise/lib/messageTypes.ts +++ b/apps/meteor/app/livechat-enterprise/client/messageTypes.ts @@ -1,7 +1,26 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import { MessageTypes } from '../../../../app/ui-utils/client'; -import { t } from '../../../../app/utils/lib/i18n'; +import { MessageTypes } from '../../ui-utils/client'; +import { t } from '../../utils/lib/i18n'; + +MessageTypes.registerType({ + id: 'livechat_transfer_history_fallback', + system: true, + message: 'New_chat_transfer_fallback', + data(message: any) { + if (!message.transferData) { + return { + fallback: 'SHOULD_NEVER_HAPPEN', + }; + } + const from = message.transferData.prevDepartment; + const to = message.transferData.department.name; + + return { + fallback: t('Livechat_transfer_failed_fallback', { from, to }), + }; + }, +}); MessageTypes.registerType({ id: 'omnichannel_priority_change_history', diff --git a/apps/meteor/ee/app/livechat-enterprise/client/startup.ts b/apps/meteor/app/livechat-enterprise/client/startup.ts similarity index 60% rename from apps/meteor/ee/app/livechat-enterprise/client/startup.ts rename to apps/meteor/app/livechat-enterprise/client/startup.ts index 3c3ec1c02139c..0535f8926a7d4 100644 --- a/apps/meteor/ee/app/livechat-enterprise/client/startup.ts +++ b/apps/meteor/app/livechat-enterprise/client/startup.ts @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; -import { businessHourManager } from '../../../../app/livechat/client/views/app/business-hours/BusinessHours'; -import type { IBusinessHourBehavior } from '../../../../app/livechat/client/views/app/business-hours/IBusinessHourBehavior'; -import { SingleBusinessHourBehavior } from '../../../../app/livechat/client/views/app/business-hours/Single'; -import { settings } from '../../../../app/settings/client'; import { hasLicense } from '../../license/client'; +import { businessHourManager } from '../../livechat/client/views/app/business-hours/BusinessHours'; +import type { IBusinessHourBehavior } from '../../livechat/client/views/app/business-hours/IBusinessHourBehavior'; +import { SingleBusinessHourBehavior } from '../../livechat/client/views/app/business-hours/Single'; +import { settings } from '../../settings/client'; import { MultipleBusinessHoursBehavior } from './views/business-hours/Multiple'; const businessHours: Record = { diff --git a/apps/meteor/ee/app/livechat-enterprise/client/views/business-hours/Multiple.ts b/apps/meteor/app/livechat-enterprise/client/views/business-hours/Multiple.ts similarity index 79% rename from apps/meteor/ee/app/livechat-enterprise/client/views/business-hours/Multiple.ts rename to apps/meteor/app/livechat-enterprise/client/views/business-hours/Multiple.ts index a57344da73dc1..698462dcaed2a 100644 --- a/apps/meteor/ee/app/livechat-enterprise/client/views/business-hours/Multiple.ts +++ b/apps/meteor/app/livechat-enterprise/client/views/business-hours/Multiple.ts @@ -1,7 +1,7 @@ import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; -import type { IBusinessHourBehavior } from '../../../../../../app/livechat/client/views/app/business-hours/IBusinessHourBehavior'; +import type { IBusinessHourBehavior } from '../../../../livechat/client/views/app/business-hours/IBusinessHourBehavior'; export class MultipleBusinessHoursBehavior implements IBusinessHourBehavior { getView(): string { diff --git a/apps/meteor/ee/app/livechat-enterprise/client/views/livechatSideNavItems.ts b/apps/meteor/app/livechat-enterprise/client/views/livechatSideNavItems.ts similarity index 91% rename from apps/meteor/ee/app/livechat-enterprise/client/views/livechatSideNavItems.ts rename to apps/meteor/app/livechat-enterprise/client/views/livechatSideNavItems.ts index c89931208451c..6d9d9f31e24c7 100644 --- a/apps/meteor/ee/app/livechat-enterprise/client/views/livechatSideNavItems.ts +++ b/apps/meteor/app/livechat-enterprise/client/views/livechatSideNavItems.ts @@ -1,5 +1,5 @@ -import { hasPermission, hasAtLeastOnePermission } from '../../../../../app/authorization/client'; -import { registerOmnichannelSidebarItem } from '../../../../../client/views/omnichannel/sidebarItems'; +import { registerOmnichannelSidebarItem } from '../../../../client/views/omnichannel/sidebarItems'; +import { hasPermission, hasAtLeastOnePermission } from '../../../authorization/client'; registerOmnichannelSidebarItem({ href: '/omnichannel/reports', diff --git a/apps/meteor/app/livechat/imports/server/rest/appearance.ts b/apps/meteor/app/livechat/imports/server/rest/appearance.ts index 02239bc0edc51..48863fc9e5d31 100644 --- a/apps/meteor/app/livechat/imports/server/rest/appearance.ts +++ b/apps/meteor/app/livechat/imports/server/rest/appearance.ts @@ -4,6 +4,7 @@ import { isPOSTLivechatAppearanceParams } from '@rocket.chat/rest-typings'; import { isTruthy } from '../../../../../lib/isTruthy'; import { API } from '../../../../api/server'; +import { notifyOnSettingChangedById } from '../../../../lib/server/lib/notifyListener'; import { findAppearance } from '../../../server/api/lib/appearance'; API.v1.addRoute( @@ -89,11 +90,13 @@ API.v1.addRoute( }) .toArray(); - await Promise.all( - dbSettings.filter(isTruthy).map((setting) => { - return Settings.updateValueById(setting._id, setting.value); - }), - ); + const eligibleSettings = dbSettings.filter(isTruthy); + const promises = eligibleSettings.map(({ _id, value }) => Settings.updateValueById(_id, value)); + (await Promise.all(promises)).forEach((value, index) => { + if (value?.modifiedCount) { + void notifyOnSettingChangedById(eligibleSettings[index]._id); + } + }); return API.v1.success(); }, diff --git a/apps/meteor/app/livechat/server/api/v1/integration.ts b/apps/meteor/app/livechat/server/api/v1/integration.ts index 6cf8ffd52192f..a1f9c59ffb87f 100644 --- a/apps/meteor/app/livechat/server/api/v1/integration.ts +++ b/apps/meteor/app/livechat/server/api/v1/integration.ts @@ -3,6 +3,7 @@ import { isPOSTomnichannelIntegrations } from '@rocket.chat/rest-typings'; import { trim } from '../../../../../lib/utils/stringUtils'; import { API } from '../../../../api/server'; +import { notifyOnSettingChangedById } from '../../../../lib/server/lib/notifyListener'; API.v1.addRoute( 'omnichannel/integrations', @@ -23,53 +24,40 @@ API.v1.addRoute( LivechatWebhookOnAgentMessage, } = this.bodyParams; - const promises = []; + const settingsIds = [ + typeof LivechatWebhookUrl !== 'undefined' && { _id: 'Livechat_webhookUrl', value: trim(LivechatWebhookUrl) }, + typeof LivechatSecretToken !== 'undefined' && { _id: 'Livechat_secret_token', value: trim(LivechatSecretToken) }, + typeof LivechatHttpTimeout !== 'undefined' && { _id: 'Livechat_http_timeout', value: LivechatHttpTimeout }, + typeof LivechatWebhookOnStart !== 'undefined' && { _id: 'Livechat_webhook_on_start', value: !!LivechatWebhookOnStart }, + typeof LivechatWebhookOnClose !== 'undefined' && { _id: 'Livechat_webhook_on_close', value: !!LivechatWebhookOnClose }, + typeof LivechatWebhookOnChatTaken !== 'undefined' && { _id: 'Livechat_webhook_on_chat_taken', value: !!LivechatWebhookOnChatTaken }, + typeof LivechatWebhookOnChatQueued !== 'undefined' && { + _id: 'Livechat_webhook_on_chat_queued', + value: !!LivechatWebhookOnChatQueued, + }, + typeof LivechatWebhookOnForward !== 'undefined' && { _id: 'Livechat_webhook_on_forward', value: !!LivechatWebhookOnForward }, + typeof LivechatWebhookOnOfflineMsg !== 'undefined' && { + _id: 'Livechat_webhook_on_offline_msg', + value: !!LivechatWebhookOnOfflineMsg, + }, + typeof LivechatWebhookOnVisitorMessage !== 'undefined' && { + _id: 'Livechat_webhook_on_visitor_message', + value: !!LivechatWebhookOnVisitorMessage, + }, + typeof LivechatWebhookOnAgentMessage !== 'undefined' && { + _id: 'Livechat_webhook_on_agent_message', + value: !!LivechatWebhookOnAgentMessage, + }, + ].filter(Boolean) as unknown as { _id: string; value: any }[]; - if (typeof LivechatWebhookUrl !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_webhookUrl', trim(LivechatWebhookUrl))); - } + const promises = settingsIds.map((setting) => Settings.updateValueById(setting._id, setting.value)); - if (typeof LivechatSecretToken !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_secret_token', trim(LivechatSecretToken))); - } + (await Promise.all(promises)).forEach((value, index) => { + if (value?.modifiedCount) { + void notifyOnSettingChangedById(settingsIds[index]._id); + } + }); - if (typeof LivechatHttpTimeout !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_http_timeout', LivechatHttpTimeout)); - } - - if (typeof LivechatWebhookOnStart !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_webhook_on_start', !!LivechatWebhookOnStart)); - } - - if (typeof LivechatWebhookOnClose !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_webhook_on_close', !!LivechatWebhookOnClose)); - } - - if (typeof LivechatWebhookOnChatTaken !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_webhook_on_chat_taken', !!LivechatWebhookOnChatTaken)); - } - - if (typeof LivechatWebhookOnChatQueued !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_webhook_on_chat_queued', !!LivechatWebhookOnChatQueued)); - } - - if (typeof LivechatWebhookOnForward !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_webhook_on_forward', !!LivechatWebhookOnForward)); - } - - if (typeof LivechatWebhookOnOfflineMsg !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_webhook_on_offline_msg', !!LivechatWebhookOnOfflineMsg)); - } - - if (typeof LivechatWebhookOnVisitorMessage !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_webhook_on_visitor_message', !!LivechatWebhookOnVisitorMessage)); - } - - if (typeof LivechatWebhookOnAgentMessage !== 'undefined') { - promises.push(Settings.updateValueById('Livechat_webhook_on_agent_message', !!LivechatWebhookOnAgentMessage)); - } - - await Promise.all(promises); return API.v1.success(); }, }, diff --git a/apps/meteor/app/livechat/server/api/v1/message.ts b/apps/meteor/app/livechat/server/api/v1/message.ts index 1dcf54e403a6a..97c92eeb530fb 100644 --- a/apps/meteor/app/livechat/server/api/v1/message.ts +++ b/apps/meteor/app/livechat/server/api/v1/message.ts @@ -98,7 +98,7 @@ API.v1.addRoute( throw new Error('invalid-room'); } - let message = await Messages.findOneById(_id); + let message = await Messages.findOneByRoomIdAndMessageId(rid, _id); if (!message) { throw new Error('invalid-message'); } diff --git a/apps/meteor/app/livechat/server/api/v1/videoCall.ts b/apps/meteor/app/livechat/server/api/v1/videoCall.ts index 8cbf9e100debe..dd9c701e64951 100644 --- a/apps/meteor/app/livechat/server/api/v1/videoCall.ts +++ b/apps/meteor/app/livechat/server/api/v1/videoCall.ts @@ -6,7 +6,7 @@ import { isGETWebRTCCall, isPUTWebRTCCallId } from '@rocket.chat/rest-typings'; import { i18n } from '../../../../../server/lib/i18n'; import { API } from '../../../../api/server'; import { canSendMessageAsync } from '../../../../authorization/server/functions/canSendMessage'; -import { notifyOnRoomChangedById } from '../../../../lib/server/lib/notifyListener'; +import { notifyOnRoomChangedById, notifyOnSettingChanged } from '../../../../lib/server/lib/notifyListener'; import { settings as rcSettings } from '../../../../settings/server'; import { Livechat } from '../../lib/LivechatTyped'; import { settings } from '../lib/livechat'; @@ -46,14 +46,20 @@ API.v1.addRoute( let { callStatus } = room; if (!callStatus || callStatus === 'ended' || callStatus === 'declined') { - await Settings.incrementValueById('WebRTC_Calls_Count'); + const { value } = await Settings.incrementValueById('WebRTC_Calls_Count', 1, { returnDocument: 'after' }); + if (value) { + void notifyOnSettingChanged(value); + } + callStatus = 'ringing'; - await Rooms.setCallStatusAndCallStartTime(room._id, callStatus); - void notifyOnRoomChangedById(room._id); + + (await Rooms.setCallStatusAndCallStartTime(room._id, callStatus)).modifiedCount && void notifyOnRoomChangedById(room._id); + await Message.saveSystemMessage('livechat_webrtc_video_call', room._id, i18n.t('Join_my_room_to_start_the_video_call'), this.user, { actionLinks: config.theme.actionLinks.webrtc, }); } + const videoCall = { rid: room._id, provider: 'webrtc', diff --git a/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts b/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts index a5f11caaab63e..9ddd273600d50 100644 --- a/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts +++ b/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts @@ -5,6 +5,7 @@ import moment from 'moment-timezone'; import type { UpdateFilter } from 'mongodb'; import type { IWorkHoursCronJobsWrapper } from '../../../../server/models/raw/LivechatBusinessHours'; +import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; export interface IBusinessHourBehavior { findHoursToCreateJobs(): Promise; @@ -49,7 +50,7 @@ export abstract class AbstractBusinessHourBehavior { } async changeAgentActiveStatus(agentId: string, status: ILivechatAgentStatus): Promise { - return this.UsersRepository.setLivechatStatusIf( + const result = await this.UsersRepository.setLivechatStatusIf( agentId, status, // Why this works: statusDefault is the property set when a user manually changes their status @@ -57,6 +58,16 @@ export abstract class AbstractBusinessHourBehavior { { livechatStatusSystemModified: true, statusDefault: { $ne: 'offline' } }, { livechatStatusSystemModified: true }, ); + + if (result.modifiedCount > 0) { + void notifyOnUserChange({ + clientAction: 'updated', + id: agentId, + diff: { statusLivechat: 'available', livechatStatusSystemModified: true }, + }); + } + + return result; } } diff --git a/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts b/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts index c893cb68ddf7c..ec21ff2de0677 100644 --- a/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts +++ b/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts @@ -5,6 +5,7 @@ import { LivechatBusinessHours, LivechatDepartment, Users } from '@rocket.chat/m import moment from 'moment'; import { callbacks } from '../../../../lib/callbacks'; +import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { businessHourLogger } from '../lib/logger'; import type { IBusinessHourBehavior, IBusinessHourType } from './AbstractBusinessHour'; @@ -126,7 +127,12 @@ export class BusinessHourManager { return this.behavior.changeAgentActiveStatus(agentId, 'available'); } - return Users.setLivechatStatusActiveBasedOnBusinessHours(agentId); + const result = await Users.setLivechatStatusActiveBasedOnBusinessHours(agentId); + if (result.updatedCount > 0) { + void notifyOnUserChange({ clientAction: 'updated', id: agentId, diff: { statusLivechat: 'available ' } }); + } + + return result; } async restartCronJobsIfNecessary(): Promise { diff --git a/apps/meteor/app/livechat/server/business-hour/Helper.ts b/apps/meteor/app/livechat/server/business-hour/Helper.ts index e50d866aa6b95..e19300691660a 100644 --- a/apps/meteor/app/livechat/server/business-hour/Helper.ts +++ b/apps/meteor/app/livechat/server/business-hour/Helper.ts @@ -1,8 +1,9 @@ import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; -import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; +import { ILivechatAgentStatus, LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; import { LivechatBusinessHours, Users } from '@rocket.chat/models'; import moment from 'moment'; +import { notifyOnUserChangeAsync } from '../../../lib/server/lib/notifyListener'; import { businessHourLogger } from '../lib/logger'; import { createDefaultBusinessHourRow } from './LivechatBusinessHours'; import { filterBusinessHoursThatMustBeOpened } from './filterBusinessHoursThatMustBeOpened'; @@ -32,13 +33,14 @@ export const openBusinessHourDefault = async (): Promise => { active: 1, }, }); + const businessHoursToOpenIds = (await filterBusinessHoursThatMustBeOpened(activeBusinessHours)).map((businessHour) => businessHour._id); businessHourLogger.debug({ msg: 'Opening default business hours', businessHoursToOpenIds }); await Users.openAgentsBusinessHoursByBusinessHourId(businessHoursToOpenIds); if (businessHoursToOpenIds.length) { - await Users.makeAgentsWithinBusinessHourAvailable(); + await makeOnlineAgentsAvailable(); } - await Users.updateLivechatStatusBasedOnBusinessHours(); + await makeAgentsUnavailableBasedOnBusinessHour(); }; export const createDefaultBusinessHourIfNotExists = async (): Promise => { @@ -46,3 +48,55 @@ export const createDefaultBusinessHourIfNotExists = async (): Promise => { await LivechatBusinessHours.insertOne(createDefaultBusinessHourRow()); } }; + +export async function makeAgentsUnavailableBasedOnBusinessHour(agentIds: string[] | null = null) { + const results = await Users.findAgentsAvailableWithoutBusinessHours(agentIds).toArray(); + + const update = await Users.updateLivechatStatusByAgentIds( + results.map(({ _id }) => _id), + ILivechatAgentStatus.NOT_AVAILABLE, + ); + + if (update.modifiedCount === 0) { + return; + } + + void notifyOnUserChangeAsync(async () => + results.map(({ _id, openBusinessHours }) => { + return { + id: _id, + clientAction: 'updated', + diff: { + statusLivechat: 'not-available', + openBusinessHours, + }, + }; + }), + ); +} + +export async function makeOnlineAgentsAvailable(agentIds: string[] | null = null) { + const results = await Users.findOnlineButNotAvailableAgents(agentIds).toArray(); + + const update = await Users.updateLivechatStatusByAgentIds( + results.map(({ _id }) => _id), + ILivechatAgentStatus.AVAILABLE, + ); + + if (update.modifiedCount === 0) { + return; + } + + void notifyOnUserChangeAsync(async () => + results.map(({ _id, openBusinessHours }) => { + return { + id: _id, + clientAction: 'updated', + diff: { + statusLivechat: 'available', + openBusinessHours, + }, + }; + }), + ); +} diff --git a/apps/meteor/app/livechat/server/business-hour/Single.ts b/apps/meteor/app/livechat/server/business-hour/Single.ts index 5d2730dba9a14..ea8166c75fa98 100644 --- a/apps/meteor/app/livechat/server/business-hour/Single.ts +++ b/apps/meteor/app/livechat/server/business-hour/Single.ts @@ -1,10 +1,11 @@ import { ILivechatAgentStatus, LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; import { LivechatBusinessHours, Users } from '@rocket.chat/models'; +import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; import { businessHourLogger } from '../lib/logger'; import type { IBusinessHourBehavior } from './AbstractBusinessHour'; import { AbstractBusinessHourBehavior } from './AbstractBusinessHour'; -import { filterBusinessHoursThatMustBeOpened, openBusinessHourDefault } from './Helper'; +import { filterBusinessHoursThatMustBeOpened, makeAgentsUnavailableBasedOnBusinessHour, openBusinessHourDefault } from './Helper'; export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior implements IBusinessHourBehavior { async openBusinessHoursByDayAndHour(): Promise { @@ -18,7 +19,8 @@ export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior imp }) ).map((businessHour) => businessHour._id); await this.UsersRepository.closeAgentsBusinessHoursByBusinessHourIds(businessHoursIds); - await this.UsersRepository.updateLivechatStatusBasedOnBusinessHours(); + + await makeAgentsUnavailableBasedOnBusinessHour(); } async onStartBusinessHours(): Promise { @@ -41,7 +43,19 @@ export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior imp agentId, newStatus: ILivechatAgentStatus.NOT_AVAILABLE, }); - await Users.setLivechatStatus(agentId, ILivechatAgentStatus.NOT_AVAILABLE); + + const { modifiedCount } = await Users.setLivechatStatus(agentId, ILivechatAgentStatus.NOT_AVAILABLE); + if (modifiedCount > 0) { + void notifyOnUserChange({ + id: agentId, + clientAction: 'updated', + diff: { + statusLivechat: ILivechatAgentStatus.NOT_AVAILABLE, + livechatStatusSystemModified: false, + }, + }); + } + return; } diff --git a/apps/meteor/app/livechat/server/business-hour/closeBusinessHour.ts b/apps/meteor/app/livechat/server/business-hour/closeBusinessHour.ts index a2295b529272c..976d8ec1705ea 100644 --- a/apps/meteor/app/livechat/server/business-hour/closeBusinessHour.ts +++ b/apps/meteor/app/livechat/server/business-hour/closeBusinessHour.ts @@ -3,6 +3,7 @@ import { Users } from '@rocket.chat/models'; import { makeFunction } from '@rocket.chat/patch-injection'; import { businessHourLogger } from '../lib/logger'; +import { makeAgentsUnavailableBasedOnBusinessHour } from './Helper'; import { getAgentIdsForBusinessHour } from './getAgentIdsForBusinessHour'; export const closeBusinessHourByAgentIds = async ( @@ -16,7 +17,8 @@ export const closeBusinessHourByAgentIds = async ( top10AgentIds: agentIds.slice(0, 10), }); await Users.removeBusinessHourByAgentIds(agentIds, businessHourId); - await Users.updateLivechatStatusBasedOnBusinessHours(); + + await makeAgentsUnavailableBasedOnBusinessHour(); }; export const closeBusinessHour = makeFunction(async (businessHour: Pick): Promise => { diff --git a/apps/meteor/app/livechat/server/hooks/afterAgentRemoved.ts b/apps/meteor/app/livechat/server/hooks/afterAgentRemoved.ts index 475eff9002fcc..5dcb9513ec2fc 100644 --- a/apps/meteor/app/livechat/server/hooks/afterAgentRemoved.ts +++ b/apps/meteor/app/livechat/server/hooks/afterAgentRemoved.ts @@ -1,13 +1,42 @@ import { LivechatDepartment, Users, LivechatDepartmentAgents, LivechatVisitors } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; +import { notifyOnLivechatDepartmentAgentChanged, notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; callbacks.add('livechat.afterAgentRemoved', async ({ agent }) => { - const departmentIds = (await LivechatDepartmentAgents.findByAgentId(agent._id).toArray()).map((department) => department.departmentId); - await Promise.all([ + const departments = await LivechatDepartmentAgents.findByAgentId(agent._id).toArray(); + + const [{ modifiedCount }, { deletedCount }] = await Promise.all([ Users.removeAgent(agent._id), LivechatDepartmentAgents.removeByAgentId(agent._id), agent.username && LivechatVisitors.removeContactManagerByUsername(agent.username), - departmentIds.length && LivechatDepartment.decreaseNumberOfAgentsByIds(departmentIds), + departments.length && LivechatDepartment.decreaseNumberOfAgentsByIds(departments.map(({ departmentId }) => departmentId)), ]); + + if (modifiedCount > 0) { + void notifyOnUserChange({ + id: agent._id, + clientAction: 'updated', + diff: { + operator: false, + livechat: null, + statusLivechat: null, + extension: null, + openBusinessHours: null, + }, + }); + } + + if (deletedCount > 0) { + departments.forEach((depAgent) => { + void notifyOnLivechatDepartmentAgentChanged( + { + _id: depAgent._id, + agentId: agent._id, + departmentId: depAgent.departmentId, + }, + 'removed', + ); + }); + } }); diff --git a/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts b/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts index 4a37dc4f0abeb..48ec985aa42cf 100644 --- a/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts +++ b/apps/meteor/app/livechat/server/hooks/markRoomResponded.ts @@ -4,6 +4,7 @@ import { LivechatRooms, LivechatVisitors, LivechatInquiry } from '@rocket.chat/m import moment from 'moment'; import { callbacks } from '../../../../lib/callbacks'; +import { notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener'; callbacks.add( 'afterSaveMessage', @@ -37,10 +38,13 @@ callbacks.add( } if (!room.v?.activity?.includes(monthYear)) { - await Promise.all([ + const [, livechatInquiry] = await Promise.all([ LivechatRooms.markVisitorActiveForPeriod(room._id, monthYear), LivechatInquiry.markInquiryActiveForPeriod(room._id, monthYear), ]); + if (livechatInquiry) { + void notifyOnLivechatInquiryChanged(livechatInquiry, 'updated', { v: livechatInquiry.v }); + } } if (room.responseBy) { diff --git a/apps/meteor/app/livechat/server/hooks/saveLastMessageToInquiry.ts b/apps/meteor/app/livechat/server/hooks/saveLastMessageToInquiry.ts index 648337a8c2a10..e65f1d99b8842 100644 --- a/apps/meteor/app/livechat/server/hooks/saveLastMessageToInquiry.ts +++ b/apps/meteor/app/livechat/server/hooks/saveLastMessageToInquiry.ts @@ -2,6 +2,7 @@ import { isOmnichannelRoom, isEditedMessage } from '@rocket.chat/core-typings'; import { LivechatInquiry } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; +import { notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { RoutingManager } from '../lib/RoutingManager'; @@ -21,7 +22,10 @@ callbacks.add( return message; } - await LivechatInquiry.setLastMessageByRoomId(room._id, message); + const livechatInquiry = await LivechatInquiry.setLastMessageByRoomId(room._id, message); + if (livechatInquiry) { + void notifyOnLivechatInquiryChanged(livechatInquiry, 'updated', { lastMessage: message }); + } return message; }, diff --git a/apps/meteor/app/livechat/server/lib/Contacts.ts b/apps/meteor/app/livechat/server/lib/Contacts.ts index 4105ae8f500fb..2e648b02f5dd4 100644 --- a/apps/meteor/app/livechat/server/lib/Contacts.ts +++ b/apps/meteor/app/livechat/server/lib/Contacts.ts @@ -6,7 +6,7 @@ import type { MatchKeysAndValues, OnlyFieldsOfType } from 'mongodb'; import { callbacks } from '../../../../lib/callbacks'; import { trim } from '../../../../lib/utils/stringUtils'; -import { notifyOnRoomChangedById } from '../../../lib/server/lib/notifyListener'; +import { notifyOnRoomChangedById, notifyOnLivechatInquiryChangedByRoom } from '../../../lib/server/lib/notifyListener'; import { i18n } from '../../../utils/lib/i18n'; type RegisterContactProps = { @@ -144,6 +144,7 @@ export const Contacts = { Subscriptions.updateDisplayNameByRoomId(rid, name), ]); + void notifyOnLivechatInquiryChangedByRoom(rid, 'updated', { name }); void notifyOnRoomChangedById(rid); } } diff --git a/apps/meteor/app/livechat/server/lib/Departments.ts b/apps/meteor/app/livechat/server/lib/Departments.ts index ed55a856e0b81..3dfa01e4f6b63 100644 --- a/apps/meteor/app/livechat/server/lib/Departments.ts +++ b/apps/meteor/app/livechat/server/lib/Departments.ts @@ -1,8 +1,9 @@ -import type { ILivechatDepartment, ILivechatDepartmentAgents } from '@rocket.chat/core-typings'; +import type { ILivechatDepartment } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; import { LivechatDepartment, LivechatDepartmentAgents, LivechatRooms } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; +import { notifyOnLivechatDepartmentAgentChanged } from '../../../lib/server/lib/notifyListener'; class DepartmentHelperClass { logger = new Logger('Omnichannel:DepartmentHelper'); @@ -24,29 +25,42 @@ class DepartmentHelperClass { throw new Error('error-failed-to-delete-department'); } - const agentsIds: string[] = await LivechatDepartmentAgents.findAgentsByDepartmentId>( - department._id, - { projection: { agentId: 1 } }, - ) - .cursor.map((agent) => agent.agentId) - .toArray(); + const removedAgents = await LivechatDepartmentAgents.findByDepartmentId(department._id, { projection: { agentId: 1 } }).toArray(); this.logger.debug( `Performing post-department-removal actions: ${_id}. Removing department agents, unsetting fallback department and removing department from rooms`, ); + const removeByDept = LivechatDepartmentAgents.removeByDepartmentId(_id); + const promiseResponses = await Promise.allSettled([ - LivechatDepartmentAgents.removeByDepartmentId(_id), + removeByDept, LivechatDepartment.unsetFallbackDepartmentByDepartmentId(_id), LivechatRooms.bulkRemoveDepartmentAndUnitsFromRooms(_id), ]); + promiseResponses.forEach((response, index) => { if (response.status === 'rejected') { this.logger.error(`Error while performing post-department-removal actions: ${_id}. Action No: ${index}. Error:`, response.reason); } }); - await callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds }); + const { deletedCount } = await removeByDept; + + if (deletedCount > 0) { + removedAgents.forEach(({ _id: docId, agentId }) => { + void notifyOnLivechatDepartmentAgentChanged( + { + _id: docId, + agentId, + departmentId: _id, + }, + 'removed', + ); + }); + } + + await callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds: removedAgents.map(({ agentId }) => agentId) }); return ret; } diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index 453869d4425ac..dacd99be00f92 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -37,6 +37,10 @@ import { i18n } from '../../../../server/lib/i18n'; import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; import { sendNotification } from '../../../lib/server'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; +import { + notifyOnLivechatDepartmentAgentChanged, + notifyOnLivechatDepartmentAgentChangedByAgentsAndDepartmentId, +} from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { Livechat as LivechatTyped } from './LivechatTyped'; import { queueInquiry, saveQueueInquiry } from './QueueManager'; @@ -697,14 +701,31 @@ export const updateDepartmentAgents = async ( }); const { upsert = [], remove = [] } = agents; - const agentsRemoved = []; + + const agentsUpdated = []; + const agentsRemoved = remove.map(({ agentId }: { agentId: string }) => agentId); const agentsAdded = []; - for await (const { agentId } of remove) { - await LivechatDepartmentAgents.removeByDepartmentIdAndAgentId(departmentId, agentId); - agentsRemoved.push(agentId); - } if (agentsRemoved.length > 0) { + const removedIds = await LivechatDepartmentAgents.findByAgentsAndDepartmentId(agentsRemoved, departmentId, { + projection: { agentId: 1 }, + }).toArray(); + + const { deletedCount } = await LivechatDepartmentAgents.removeByIds(removedIds.map(({ _id }) => _id)); + + if (deletedCount > 0) { + removedIds.forEach(({ _id, agentId }) => { + void notifyOnLivechatDepartmentAgentChanged( + { + _id, + agentId, + departmentId, + }, + 'removed', + ); + }); + } + callbacks.runAsync('livechat.removeAgentDepartment', { departmentId, agentsId: agentsRemoved }); } @@ -714,7 +735,7 @@ export const updateDepartmentAgents = async ( continue; } - await LivechatDepartmentAgents.saveAgent({ + const livechatDepartmentAgent = await LivechatDepartmentAgents.saveAgent({ agentId: agent.agentId, departmentId, username: agentFromDb.username || '', @@ -722,6 +743,20 @@ export const updateDepartmentAgents = async ( order: agent.order ? parseFromIntOrStr(agent.order) : 0, departmentEnabled, }); + + if (livechatDepartmentAgent.upsertedId) { + void notifyOnLivechatDepartmentAgentChanged( + { + _id: livechatDepartmentAgent.upsertedId as any, + agentId: agent.agentId, + departmentId, + }, + 'inserted', + ); + } else { + agentsUpdated.push(agent.agentId); + } + agentsAdded.push(agent.agentId); } @@ -732,6 +767,10 @@ export const updateDepartmentAgents = async ( }); } + if (agentsUpdated.length > 0) { + void notifyOnLivechatDepartmentAgentChangedByAgentsAndDepartmentId(agentsUpdated, departmentId); + } + if (agentsRemoved.length > 0 || agentsAdded.length > 0) { const numAgents = await LivechatDepartmentAgents.countByDepartmentId(departmentId); await LivechatDepartment.updateNumAgentsById(departmentId, numAgents); diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 8af9980eb5e63..bf5014b984f18 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -57,7 +57,14 @@ import { FileUpload } from '../../../file-upload/server'; import { deleteMessage } from '../../../lib/server/functions/deleteMessage'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; import { updateMessage } from '../../../lib/server/functions/updateMessage'; -import { notifyOnRoomChangedById } from '../../../lib/server/lib/notifyListener'; +import { + notifyOnLivechatInquiryChanged, + notifyOnLivechatInquiryChangedByRoom, + notifyOnRoomChangedById, + notifyOnLivechatInquiryChangedByToken, + notifyOnLivechatDepartmentAgentChangedByDepartmentId, + notifyOnUserChange, +} from '../../../lib/server/lib/notifyListener'; import * as Mailer from '../../../mailer/server/api'; import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; @@ -293,10 +300,15 @@ class LivechatClass { this.logger.debug(`Updating DB for room ${room._id} with close data`); + const inquiry = await LivechatInquiry.findOneByRoomId(rid); + const removedInquiry = await LivechatInquiry.removeByRoomId(rid); if (removedInquiry && removedInquiry.deletedCount !== 1) { throw new Error('Error removing inquiry'); } + if (inquiry) { + void notifyOnLivechatInquiryChanged(inquiry, 'removed'); + } const updatedRoom = await LivechatRooms.closeRoomById(rid, closeData); if (!updatedRoom || updatedRoom.modifiedCount !== 1) { @@ -500,6 +512,8 @@ class LivechatClass { 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), @@ -508,6 +522,10 @@ class LivechatClass { LivechatRooms.removeById(rid), ]); + if (inquiry) { + void notifyOnLivechatInquiryChanged(inquiry, 'removed'); + } + for (const r of result) { if (r.status === 'rejected') { this.logger.error(`Error removing room ${rid}: ${r.reason}`); @@ -1002,6 +1020,8 @@ class LivechatClass { await Promise.all([LivechatDepartmentAgents.disableAgentsByDepartmentId(_id), LivechatDepartment.archiveDepartment(_id)]); + void notifyOnLivechatDepartmentAgentChangedByDepartmentId(_id); + await callbacks.run('livechat.afterDepartmentArchived', department); } @@ -1014,6 +1034,9 @@ class LivechatClass { // TODO: these kind of actions should be on events instead of here await Promise.all([LivechatDepartmentAgents.enableAgentsByDepartmentId(_id), LivechatDepartment.unarchiveDepartment(_id)]); + + void notifyOnLivechatDepartmentAgentChangedByDepartmentId(_id); + return true; } @@ -1265,11 +1288,11 @@ class LivechatClass { ]); } - await Promise.all([ - Subscriptions.removeByVisitorToken(token), - LivechatRooms.removeByVisitorToken(token), - LivechatInquiry.removeByVisitorToken(token), - ]); + await Promise.all([Subscriptions.removeByVisitorToken(token), LivechatRooms.removeByVisitorToken(token)]); + + const livechatInquiries = await LivechatInquiry.findIdsByVisitorToken(token).toArray(); + await LivechatInquiry.removeByIds(livechatInquiries.map(({ _id }) => _id)); + void notifyOnLivechatInquiryChanged(livechatInquiries, 'removed'); } async deleteMessage({ guest, message }: { guest: ILivechatVisitor; message: IMessage }) { @@ -1286,9 +1309,18 @@ class LivechatClass { } async setUserStatusLivechatIf(userId: string, status: ILivechatAgentStatus, condition?: Filter, fields?: AKeyOf) { - const user = await Users.setLivechatStatusIf(userId, status, condition, fields); + 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 user; + return result; } async returnRoomAsInquiry(room: IOmnichannelRoom, departmentId?: string, overrideTransferData: any = {}) { @@ -1658,13 +1690,30 @@ class LivechatClass { } async notifyGuestStatusChanged(token: string, status: UserStatus) { - await LivechatInquiry.updateVisitorStatus(token, status); 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; } @@ -1804,14 +1853,19 @@ class LivechatClass { if (guestData?.name?.trim().length) { const { _id: rid } = roomData; const { name } = guestData; + await Promise.all([ Rooms.setFnameById(rid, name), LivechatInquiry.setNameByRoomId(rid, name), Subscriptions.updateDisplayNameByRoomId(rid, name), ]); + + void notifyOnLivechatInquiryChangedByRoom(rid, 'updated', { name }); } void notifyOnRoomChangedById(roomData._id); + + return true; } /** diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index 8be71aa4c9917..576b29990b33a 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -1,20 +1,42 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; import { Omnichannel } from '@rocket.chat/core-services'; -import type { ILivechatInquiryRecord, ILivechatVisitor, IMessage, IOmnichannelRoom, SelectedAgent } from '@rocket.chat/core-typings'; +import { + LivechatInquiryStatus, + type ILivechatInquiryRecord, + type ILivechatVisitor, + type IMessage, + type IOmnichannelRoom, + type SelectedAgent, +} from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; import { LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../lib/callbacks'; +import { + notifyOnLivechatInquiryChangedById, + notifyOnLivechatInquiryChanged, + notifyOnSettingChanged, +} from '../../../lib/server/lib/notifyListener'; import { checkServiceStatus, createLivechatRoom, createLivechatInquiry } from './Helper'; import { RoutingManager } from './RoutingManager'; const logger = new Logger('QueueManager'); export const saveQueueInquiry = async (inquiry: ILivechatInquiryRecord) => { - await LivechatInquiry.queueInquiry(inquiry._id); - await callbacks.run('livechat.afterInquiryQueued', inquiry); + const queuedInquiry = await LivechatInquiry.queueInquiry(inquiry._id); + if (!queuedInquiry) { + return; + } + + await callbacks.run('livechat.afterInquiryQueued', queuedInquiry); + + void notifyOnLivechatInquiryChanged(queuedInquiry, 'updated', { + status: LivechatInquiryStatus.QUEUED, + queuedAt: new Date(), + takenAt: undefined, + }); }; export const queueInquiry = async (inquiry: ILivechatInquiryRecord, defaultAgent?: SelectedAgent) => { @@ -106,7 +128,11 @@ export const QueueManager: queueManager = { } void Apps.self?.triggerEvent(AppEvents.IPostLivechatRoomStarted, room); - await LivechatRooms.updateRoomCount(); + + const livechatSetting = await LivechatRooms.updateRoomCount(); + if (livechatSetting) { + void notifyOnSettingChanged(livechatSetting); + } await queueInquiry(inquiry, agent); logger.debug(`Inquiry ${inquiry._id} queued`); @@ -137,6 +163,7 @@ export const QueueManager: queueManager = { if (oldInquiry) { logger.debug(`Removing old inquiry (${oldInquiry._id}) for room ${rid}`); await LivechatInquiry.removeByRoomId(rid); + void notifyOnLivechatInquiryChangedById(oldInquiry._id, 'removed'); } const guest = { diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index 19437d800ee21..5782d01e318fb 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -11,6 +11,7 @@ import type { InquiryWithAgentInfo, TransferData, } from '@rocket.chat/core-typings'; +import { LivechatInquiryStatus } from '@rocket.chat/core-typings'; import { License } from '@rocket.chat/license'; import { Logger } from '@rocket.chat/logger'; import { LivechatInquiry, LivechatRooms, Subscriptions, Rooms, Users } from '@rocket.chat/models'; @@ -18,6 +19,7 @@ import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../lib/callbacks'; +import { notifyOnLivechatInquiryChangedById, notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { createLivechatSubscription, @@ -182,7 +184,15 @@ export const RoutingManager: Routing = { const { servedBy } = room; if (shouldQueue) { - await LivechatInquiry.queueInquiry(inquiry._id); + const queuedInquiry = await LivechatInquiry.queueInquiry(inquiry._id); + if (queuedInquiry) { + inquiry = queuedInquiry; + void notifyOnLivechatInquiryChanged(inquiry, 'updated', { + status: LivechatInquiryStatus.QUEUED, + queuedAt: new Date(), + takenAt: undefined, + }); + } } if (servedBy) { @@ -192,6 +202,7 @@ export const RoutingManager: Routing = { } await dispatchInquiryQueued(inquiry); + return true; }, @@ -250,11 +261,20 @@ export const RoutingManager: Routing = { } await LivechatInquiry.takeInquiry(_id); + const inq = await this.assignAgent(inquiry as InquiryWithAgentInfo, agent); logger.info(`Inquiry ${inquiry._id} taken by agent ${agent.agentId}`); callbacks.runAsync('livechat.afterTakeInquiry', inq, agent); + void notifyOnLivechatInquiryChangedById(inquiry._id, 'updated', { + status: LivechatInquiryStatus.TAKEN, + takenAt: new Date(), + defaultAgent: undefined, + estimatedInactivityCloseTimeAt: undefined, + queuedAt: undefined, + }); + return LivechatRooms.findOneById(rid); }, @@ -282,6 +302,7 @@ export const RoutingManager: Routing = { if (defaultAgent) { logger.debug(`Delegating Inquiry ${inquiry._id} to agent ${defaultAgent.username}`); await LivechatInquiry.setDefaultAgentById(inquiry._id, defaultAgent); + void notifyOnLivechatInquiryChanged(inquiry, 'updated', { defaultAgent }); } logger.debug(`Queueing inquiry ${inquiry._id}`); diff --git a/apps/meteor/app/livechat/server/methods/loadHistory.ts b/apps/meteor/app/livechat/server/methods/loadHistory.ts index 8d747cad20d86..373ede1a36105 100644 --- a/apps/meteor/app/livechat/server/methods/loadHistory.ts +++ b/apps/meteor/app/livechat/server/methods/loadHistory.ts @@ -1,6 +1,7 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { LivechatVisitors, LivechatRooms } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; +import { check, Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { loadMessageHistory } from '../../../lib/server/functions/loadMessageHistory'; @@ -23,9 +24,11 @@ Meteor.methods({ async 'livechat:loadHistory'({ token, rid, end, limit = 20, ls }) { methodDeprecationLogger.method('livechat:loadHistory', '7.0.0'); - if (!token || typeof token !== 'string') { - return; - } + check(token, String); + check(rid, String); + check(end, Date); + check(ls, Match.OneOf(String, Date)); + check(limit, Number); const visitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } }); diff --git a/apps/meteor/app/livechat/server/methods/loginByToken.ts b/apps/meteor/app/livechat/server/methods/loginByToken.ts index 54ba97e899262..cae23e6d16f7c 100644 --- a/apps/meteor/app/livechat/server/methods/loginByToken.ts +++ b/apps/meteor/app/livechat/server/methods/loginByToken.ts @@ -14,6 +14,7 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async 'livechat:loginByToken'(token) { methodDeprecationLogger.method('livechat:loginByToken', '7.0.0'); + check(token, String); const visitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } }); if (!visitor) { diff --git a/apps/meteor/app/livechat/server/methods/saveAppearance.ts b/apps/meteor/app/livechat/server/methods/saveAppearance.ts index 35152d136afdc..cac5d34264d20 100644 --- a/apps/meteor/app/livechat/server/methods/saveAppearance.ts +++ b/apps/meteor/app/livechat/server/methods/saveAppearance.ts @@ -4,6 +4,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -15,6 +16,7 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async 'livechat:saveAppearance'(settings) { methodDeprecationLogger.method('livechat:saveAppearance', '7.0.0'); + const uid = Meteor.userId(); if (!uid || !(await hasPermissionAsync(uid, 'view-livechat-manager'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { @@ -50,10 +52,12 @@ Meteor.methods({ throw new Meteor.Error('invalid-setting'); } - await Promise.all( - settings.map((setting) => { - return Settings.updateValueById(setting._id, setting.value); - }), - ); + const promises = settings.map((setting) => Settings.updateValueById(setting._id, setting.value)); + + (await Promise.all(promises)).forEach((value, index) => { + if (value?.modifiedCount) { + void notifyOnSettingChangedById(settings[index]._id); + } + }); }, }); diff --git a/apps/meteor/app/livechat/server/methods/saveIntegration.ts b/apps/meteor/app/livechat/server/methods/saveIntegration.ts index 18bad34f0aea6..de7461d08e100 100644 --- a/apps/meteor/app/livechat/server/methods/saveIntegration.ts +++ b/apps/meteor/app/livechat/server/methods/saveIntegration.ts @@ -5,6 +5,7 @@ import { Meteor } from 'meteor/meteor'; import { trim } from '../../../../lib/utils/stringUtils'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -15,56 +16,59 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async 'livechat:saveIntegration'(values) { + methodDeprecationLogger.method('livechat:saveIntegration', '7.0.0'); + const uid = Meteor.userId(); if (!uid || !(await hasPermissionAsync(uid, 'view-livechat-manager'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:saveIntegration', }); } - methodDeprecationLogger.method('livechat:saveIntegration', '7.0.0'); - - if (typeof values.Livechat_webhookUrl !== 'undefined') { - await Settings.updateValueById('Livechat_webhookUrl', trim(values.Livechat_webhookUrl)); - } - - if (typeof values.Livechat_secret_token !== 'undefined') { - await Settings.updateValueById('Livechat_secret_token', trim(values.Livechat_secret_token)); - } - - if (typeof values.Livechat_http_timeout === 'number') { - await Settings.updateValueById('Livechat_http_timeout', values.Livechat_http_timeout); - } - - if (typeof values.Livechat_webhook_on_start !== 'undefined') { - await Settings.updateValueById('Livechat_webhook_on_start', !!values.Livechat_webhook_on_start); - } - if (typeof values.Livechat_webhook_on_close !== 'undefined') { - await Settings.updateValueById('Livechat_webhook_on_close', !!values.Livechat_webhook_on_close); - } - - if (typeof values.Livechat_webhook_on_chat_taken !== 'undefined') { - await Settings.updateValueById('Livechat_webhook_on_chat_taken', !!values.Livechat_webhook_on_chat_taken); - } + const settingsIds = [ + typeof values.Livechat_webhookUrl !== 'undefined' && { _id: 'Livechat_webhookUrl', value: trim(values.Livechat_webhookUrl) }, + typeof values.Livechat_secret_token !== 'undefined' && { _id: 'Livechat_secret_token', value: trim(values.Livechat_secret_token) }, + typeof values.Livechat_http_timeout !== 'undefined' && { _id: 'Livechat_http_timeout', value: values.Livechat_http_timeout }, + typeof values.Livechat_webhook_on_start !== 'undefined' && { + _id: 'Livechat_webhook_on_start', + value: !!values.Livechat_webhook_on_start, + }, + typeof values.Livechat_webhook_on_close !== 'undefined' && { + _id: 'Livechat_webhook_on_close', + value: !!values.Livechat_webhook_on_close, + }, + typeof values.Livechat_webhook_on_forward !== 'undefined' && { + _id: 'Livechat_webhook_on_forward', + value: !!values.Livechat_webhook_on_forward, + }, + typeof values.Livechat_webhook_on_chat_taken !== 'undefined' && { + _id: 'Livechat_webhook_on_chat_taken', + value: !!values.Livechat_webhook_on_chat_taken, + }, + typeof values.Livechat_webhook_on_chat_queued !== 'undefined' && { + _id: 'Livechat_webhook_on_chat_queued', + value: !!values.Livechat_webhook_on_chat_queued, + }, + typeof values.Livechat_webhook_on_offline_msg !== 'undefined' && { + _id: 'Livechat_webhook_on_offline_msg', + value: !!values.Livechat_webhook_on_offline_msg, + }, + typeof values.Livechat_webhook_on_visitor_message !== 'undefined' && { + _id: 'Livechat_webhook_on_visitor_message', + value: !!values.Livechat_webhook_on_visitor_message, + }, + typeof values.Livechat_webhook_on_agent_message !== 'undefined' && { + _id: 'Livechat_webhook_on_agent_message', + value: !!values.Livechat_webhook_on_agent_message, + }, + ].filter(Boolean) as unknown as { _id: string; value: any }[]; - if (typeof values.Livechat_webhook_on_chat_queued !== 'undefined') { - await Settings.updateValueById('Livechat_webhook_on_chat_queued', !!values.Livechat_webhook_on_chat_queued); - } - - if (typeof values.Livechat_webhook_on_forward !== 'undefined') { - await Settings.updateValueById('Livechat_webhook_on_forward', !!values.Livechat_webhook_on_forward); - } + const promises = settingsIds.map((setting) => Settings.updateValueById(setting._id, setting.value)); - if (typeof values.Livechat_webhook_on_offline_msg !== 'undefined') { - await Settings.updateValueById('Livechat_webhook_on_offline_msg', !!values.Livechat_webhook_on_offline_msg); - } - - if (typeof values.Livechat_webhook_on_visitor_message !== 'undefined') { - await Settings.updateValueById('Livechat_webhook_on_visitor_message', !!values.Livechat_webhook_on_visitor_message); - } - - if (typeof values.Livechat_webhook_on_agent_message !== 'undefined') { - await Settings.updateValueById('Livechat_webhook_on_agent_message', !!values.Livechat_webhook_on_agent_message); - } + (await Promise.all(promises)).forEach((value, index) => { + if (value?.modifiedCount) { + void notifyOnSettingChangedById(settingsIds[index]._id); + } + }); }, }); diff --git a/apps/meteor/app/livechat/server/startup.ts b/apps/meteor/app/livechat/server/startup.ts index aba8578dcea98..b61c85c4001ea 100644 --- a/apps/meteor/app/livechat/server/startup.ts +++ b/apps/meteor/app/livechat/server/startup.ts @@ -10,6 +10,7 @@ import { beforeLeaveRoomCallback } from '../../../lib/callbacks/beforeLeaveRoomC import { i18n } from '../../../server/lib/i18n'; import { roomCoordinator } from '../../../server/lib/rooms/roomCoordinator'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; +import { notifyOnUserChange } from '../../lib/server/lib/notifyListener'; import { settings } from '../../settings/server'; import { businessHourManager } from './business-hour'; import { createDefaultBusinessHourIfNotExists } from './business-hour/Helper'; @@ -64,29 +65,47 @@ Meteor.startup(async () => { }); await createDefaultBusinessHourIfNotExists(); - settings.watch('Livechat_enable_business_hours', async (value) => { - logger.debug(`Starting business hour manager ${value}`); - if (value) { - await businessHourManager.startManager(); - return; - } - await businessHourManager.stopManager(); - }); + settings.watch( + 'Livechat_enable_business_hours', + async (value) => { + logger.debug(`Starting business hour manager ${value}`); + if (value) { + await businessHourManager.startManager(); + return; + } + await businessHourManager.stopManager(); + }, + process.env.TEST_MODE === 'true' + ? { + debounce: 10, + } + : undefined, + ); settings.watch('Livechat_Routing_Method', () => { void RoutingManager.startQueue(); }); // Remove when accounts.onLogout is async - Accounts.onLogout( - ({ user }: { user: IUser }) => - user?.roles?.includes('livechat-agent') && - !user?.roles?.includes('bot') && - void LivechatTyped.setUserStatusLivechatIf( - user._id, - ILivechatAgentStatus.NOT_AVAILABLE, - {}, - { livechatStatusSystemModified: true }, - ).catch(), - ); + Accounts.onLogout(({ user }: { user: IUser }) => { + if (!user?.roles?.includes('livechat-agent') || user?.roles?.includes('bot')) { + return; + } + + void LivechatTyped.setUserStatusLivechatIf( + user._id, + ILivechatAgentStatus.NOT_AVAILABLE, + {}, + { livechatStatusSystemModified: true }, + ).catch(); + + void notifyOnUserChange({ + id: user._id, + clientAction: 'updated', + diff: { + statusLivechat: ILivechatAgentStatus.NOT_AVAILABLE, + livechatStatusSystemModified: true, + }, + }); + }); }); diff --git a/apps/meteor/app/mailer/server/api.ts b/apps/meteor/app/mailer/server/api.ts index e562fc8e7b39f..7e0ad0380b119 100644 --- a/apps/meteor/app/mailer/server/api.ts +++ b/apps/meteor/app/mailer/server/api.ts @@ -11,6 +11,7 @@ import _ from 'underscore'; import { validateEmail } from '../../../lib/emailValidator'; import { strLeft, strRightBack } from '../../../lib/utils/stringUtils'; import { i18n } from '../../../server/lib/i18n'; +import { notifyOnSettingChanged } from '../../lib/server/lib/notifyListener'; import { settings } from '../../settings/server'; import { replaceVariables } from './replaceVariables'; @@ -166,7 +167,10 @@ export const sendNoWrap = async ({ html = undefined; } - await Settings.incrementValueById('Triggered_Emails_Count'); + const { value } = await Settings.incrementValueById('Triggered_Emails_Count', 1, { returnDocument: 'after' }); + if (value) { + void notifyOnSettingChanged(value); + } const email = { to, from, replyTo, subject, html, text, headers }; diff --git a/apps/meteor/app/message-pin/server/pinMessage.ts b/apps/meteor/app/message-pin/server/pinMessage.ts index ffc3c9836f0c2..b0eab3f929d66 100644 --- a/apps/meteor/app/message-pin/server/pinMessage.ts +++ b/apps/meteor/app/message-pin/server/pinMessage.ts @@ -133,7 +133,9 @@ Meteor.methods({ // App IPostMessagePinned event hook await Apps.self?.triggerEvent(AppEvents.IPostMessagePinned, originalMessage, await Meteor.userAsync(), originalMessage.pinned); - const msgId = await Message.saveSystemMessage('message_pinned', originalMessage.rid, '', me, { + const pinMessageType = originalMessage.t === 'e2e' ? 'message_pinned_e2e' : 'message_pinned'; + + const msgId = await Message.saveSystemMessage(pinMessageType, originalMessage.rid, '', me, { attachments: [ { text: originalMessage.msg, diff --git a/apps/meteor/app/metrics/server/lib/collectMetrics.ts b/apps/meteor/app/metrics/server/lib/collectMetrics.ts index 136686f49c9e3..978b3d59ec98d 100644 --- a/apps/meteor/app/metrics/server/lib/collectMetrics.ts +++ b/apps/meteor/app/metrics/server/lib/collectMetrics.ts @@ -39,7 +39,7 @@ const setPrometheusData = async (): Promise => { metrics.ddpConnectedUsers.set(_.unique(authenticatedSessions.map((s) => s.userId)).length); // Apps metrics - const { totalInstalled, totalActive, totalFailed } = getAppsStatistics(); + const { totalInstalled, totalActive, totalFailed } = await getAppsStatistics(); metrics.totalAppsInstalled.set(totalInstalled || 0); metrics.totalAppsEnabled.set(totalActive || 0); diff --git a/apps/meteor/app/otr/client/OTR.ts b/apps/meteor/app/otr/client/OTR.ts index a855381bd9c05..9f3eea155384a 100644 --- a/apps/meteor/app/otr/client/OTR.ts +++ b/apps/meteor/app/otr/client/OTR.ts @@ -24,6 +24,16 @@ class OTR implements IOTR { this.instancesByRoomId[rid] = otrRoom; return this.instancesByRoomId[rid]; } + + closeAllInstances(): void { + // Resets state, but doesnt emit events + // Other party should receive event and fire events + Object.values(this.instancesByRoomId).forEach((instance) => { + instance.softReset(); + }); + + this.instancesByRoomId = {}; + } } export default new OTR(); diff --git a/apps/meteor/app/otr/client/OTRRoom.ts b/apps/meteor/app/otr/client/OTRRoom.ts index 8899de13b1908..ea5dc86b83195 100644 --- a/apps/meteor/app/otr/client/OTRRoom.ts +++ b/apps/meteor/app/otr/client/OTRRoom.ts @@ -1,4 +1,5 @@ import type { IRoom, IMessage, IUser } from '@rocket.chat/core-typings'; +import { UserStatus } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; import EJSON from 'ejson'; import { Meteor } from 'meteor/meteor'; @@ -7,6 +8,7 @@ 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'; @@ -47,6 +49,8 @@ export class OTRRoom implements IOTRRoom { private isFirstOTR: boolean; + private onPresenceEventHook: (event: UserPresence | undefined) => void; + protected constructor(uid: IUser['_id'], rid: IRoom['_id'], peerId: IUser['_id']) { this._userId = uid; this._roomId = rid; @@ -54,6 +58,7 @@ export class OTRRoom implements IOTRRoom { this._sessionKey = null; this.peerId = peerId; this.isFirstOTR = true; + this.onPresenceEventHook = this.onPresenceEvent.bind(this); } public static create(uid: IUser['_id'], rid: IRoom['_id']): OTRRoom | undefined { @@ -110,6 +115,35 @@ export class OTRRoom implements IOTRRoom { } } + onPresenceEvent(event: UserPresence | undefined): void { + if (!event) { + return; + } + if (event.status !== UserStatus.OFFLINE) { + return; + } + console.warn(`OTR Room ${this._roomId} ended because ${this.peerId} went offline`); + this.end(); + + imperativeModal.open({ + component: GenericModal, + props: { + variant: 'warning', + title: t('OTR'), + children: t('OTR_Session_ended_other_user_went_offline', { username: event.username }), + confirmText: t('Ok'), + onClose: imperativeModal.close, + onConfirm: imperativeModal.close, + }, + }); + } + + // Starts listening to other user's status changes and end OTR if any of the Users goes offline + // this should be called in 2 places: on acknowledge (meaning user accepted OTR) or on establish (meaning user initiated OTR) + listenToUserStatus(): void { + Presence.listen(this.peerId, this.onPresenceEventHook); + } + acknowledge(): void { void sdk.rest.post('/v1/statistics.telemetry', { params: [{ eventName: 'otrStats', timestamp: Date.now(), rid: this._roomId }] }); @@ -137,10 +171,19 @@ export class OTRRoom implements IOTRRoom { ]); } + softReset(): void { + this.isFirstOTR = true; + this.setState(OtrRoomState.NOT_STARTED); + this._keyPair = null; + this._exportedPublicKey = {}; + this._sessionKey = null; + } + end(): void { this.isFirstOTR = true; this.reset(); this.setState(OtrRoomState.NOT_STARTED); + Presence.stop(this.peerId, this.onPresenceEventHook); sdk.publish('notify-user', [ `${this.peerId}/otr`, 'end', @@ -285,6 +328,7 @@ export class OTRRoom implements IOTRRoom { setTimeout(async () => { this.setState(OtrRoomState.ESTABLISHED); this.acknowledge(); + this.listenToUserStatus(); if (data.refresh) { await sdk.rest.post('/v1/chat.otr', { @@ -362,6 +406,7 @@ export class OTRRoom implements IOTRRoom { this.setState(OtrRoomState.ESTABLISHED); if (this.isFirstOTR) { + this.listenToUserStatus(); await sdk.rest.post('/v1/chat.otr', { roomId: this._roomId, type: otrSystemMessages.USER_JOINED_OTR, diff --git a/apps/meteor/app/otr/client/events.ts b/apps/meteor/app/otr/client/events.ts new file mode 100644 index 0000000000000..9ff84c4651570 --- /dev/null +++ b/apps/meteor/app/otr/client/events.ts @@ -0,0 +1,7 @@ +import { Accounts } from 'meteor/accounts-base'; + +import OTR from './OTR'; + +Accounts.onLogout(() => { + OTR.closeAllInstances(); +}); diff --git a/apps/meteor/app/otr/client/index.ts b/apps/meteor/app/otr/client/index.ts index 74fea3c003e8e..fac7407f54fac 100644 --- a/apps/meteor/app/otr/client/index.ts +++ b/apps/meteor/app/otr/client/index.ts @@ -1,3 +1,4 @@ import './OTRRoom'; import './OTR'; import './messageTypes'; +import './events'; diff --git a/apps/meteor/app/push/server/fcm.ts b/apps/meteor/app/push/server/fcm.ts index 819e26e4f0034..87ced6e130df0 100644 --- a/apps/meteor/app/push/server/fcm.ts +++ b/apps/meteor/app/push/server/fcm.ts @@ -55,15 +55,17 @@ type FCMError = { }; /** - * Set at least a 10 second timeout on send requests before retrying. - * Most of FCM's internal Remote Procedure Calls use a 10 second timeout. + * Send a push notification using Firebase Cloud Messaging (FCM). + * implements the Firebase Cloud Messaging HTTP v1 API, and all of its retry logic, + * see: https://firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode * * Errors: - * - For 400, 401, 403, 404 errors: abort, and do not retry. + * - For 400, 401, 403 errors: abort, and do not retry. + * - For 404 errors: remove the token from the database. * - For 429 errors: retry after waiting for the duration set in the retry-after header. If no retry-after header is set, default to 60 seconds. * - For 500 errors: retry with exponential backoff. */ -async function fetchWithRetry(url: string, options: RequestInit, retries = 0): Promise { +async function fetchWithRetry(url: string, _removeToken: () => void, options: RequestInit, retries = 0): Promise { const MAX_RETRIES = 5; const response = await fetch(url, options); @@ -79,15 +81,20 @@ async function fetchWithRetry(url: string, options: RequestInit, retries = 0): P const retryAfter = response.headers.get('retry-after'); const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : 60; + if (response.status === 404) { + _removeToken(); + return response; + } + if (response.status === 429) { await new Promise((resolve) => setTimeout(resolve, retryAfterSeconds * 1000)); - return fetchWithRetry(url, options, retries + 1); + return fetchWithRetry(url, _removeToken, options, retries + 1); } if (response.status >= 500 && response.status < 600) { const backoff = Math.pow(2, retries) * 10000; await new Promise((resolve) => setTimeout(resolve, backoff)); - return fetchWithRetry(url, options, retries + 1); + return fetchWithRetry(url, _removeToken, options, retries + 1); } const error: FCMError = await response.json(); @@ -145,12 +152,7 @@ function getFCMMessagesFromPushData(userTokens: string[], notification: PendingP return userTokens.map((token) => ({ message: { ...message, token } })); } -export const sendFCM = function ({ userTokens, notification, _replaceToken, _removeToken, options }: NativeNotificationParameters): void { - // We don't use these parameters, but we need to keep them to keep the function signature - // TODO: Remove them when we remove the old sendGCM function - _replaceToken; - _removeToken; - +export const sendFCM = function ({ userTokens, notification, _removeToken, options }: NativeNotificationParameters): void { const tokens = typeof userTokens === 'string' ? [userTokens] : userTokens; if (!tokens.length) { logger.log('sendFCM no push tokens found'); @@ -173,9 +175,15 @@ export const sendFCM = function ({ userTokens, notification, _replaceToken, _rem const url = `https://fcm.googleapis.com/v1/projects/${options.gcm.projectNumber}/messages:send`; - for (const message of messages) { - logger.debug('sendFCM message', message); - const response = fetchWithRetry(url, { method: 'POST', headers, body: JSON.stringify(message) }); + for (const fcmRequest of messages) { + logger.debug('sendFCM message', fcmRequest); + + const removeToken = () => { + const { token } = fcmRequest.message; + token && _removeToken({ gcm: token }); + }; + + const response = fetchWithRetry(url, removeToken, { method: 'POST', headers, body: JSON.stringify(fcmRequest) }); response.catch((err) => { logger.error('sendFCM error', err); diff --git a/apps/meteor/app/retention-policy/server/cronPruneMessages.ts b/apps/meteor/app/retention-policy/server/cronPruneMessages.ts index fb0e691abd69f..337691bfbe57a 100644 --- a/apps/meteor/app/retention-policy/server/cronPruneMessages.ts +++ b/apps/meteor/app/retention-policy/server/cronPruneMessages.ts @@ -2,6 +2,7 @@ import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; import { cronJobs } from '@rocket.chat/cron'; import { Rooms } from '@rocket.chat/models'; +import { getCronAdvancedTimerFromPrecisionSetting } from '../../../lib/getCronAdvancedTimerFromPrecisionSetting'; import { cleanRoomHistory } from '../../lib/server/functions/cleanRoomHistory'; import { settings } from '../../settings/server'; @@ -29,7 +30,7 @@ async function job(): Promise { // get all rooms with default values for await (const type of types) { const maxAge = maxTimes[type] || 0; - const latest = new Date(now.getTime() - toDays(maxAge)); + const latest = new Date(now.getTime() - maxAge); const rooms = await Rooms.find( { @@ -79,19 +80,6 @@ async function job(): Promise { } } -function getSchedule(precision: '0' | '1' | '2' | '3'): string { - switch (precision) { - case '0': - return '*/30 * * * *'; // 30 minutes - case '1': - return '0 * * * *'; // hour - case '2': - return '0 */6 * * *'; // 6 hours - case '3': - return '0 0 * * *'; // day - } -} - const pruneCronName = 'Prune old messages by retention policy'; async function deployCron(precision: string): Promise { @@ -107,9 +95,9 @@ settings.watchMultiple( 'RetentionPolicy_AppliesToChannels', 'RetentionPolicy_AppliesToGroups', 'RetentionPolicy_AppliesToDMs', - 'RetentionPolicy_MaxAge_Channels', - 'RetentionPolicy_MaxAge_Groups', - 'RetentionPolicy_MaxAge_DMs', + 'RetentionPolicy_TTL_Channels', + 'RetentionPolicy_TTL_Groups', + 'RetentionPolicy_TTL_DMs', 'RetentionPolicy_Advanced_Precision', 'RetentionPolicy_Advanced_Precision_Cron', 'RetentionPolicy_Precision', @@ -132,13 +120,13 @@ settings.watchMultiple( types.push('d'); } - maxTimes.c = settings.get('RetentionPolicy_MaxAge_Channels'); - maxTimes.p = settings.get('RetentionPolicy_MaxAge_Groups'); - maxTimes.d = settings.get('RetentionPolicy_MaxAge_DMs'); + maxTimes.c = settings.get('RetentionPolicy_TTL_Channels'); + maxTimes.p = settings.get('RetentionPolicy_TTL_Groups'); + maxTimes.d = settings.get('RetentionPolicy_TTL_DMs'); const precision = (settings.get('RetentionPolicy_Advanced_Precision') && settings.get('RetentionPolicy_Advanced_Precision_Cron')) || - getSchedule(settings.get('RetentionPolicy_Precision')); + getCronAdvancedTimerFromPrecisionSetting(settings.get('RetentionPolicy_Precision')); return deployCron(precision); }, diff --git a/apps/meteor/app/slashcommands-inviteall/server/server.ts b/apps/meteor/app/slashcommands-inviteall/server/server.ts index 5376bd6ae64b8..bac4349ec72c1 100644 --- a/apps/meteor/app/slashcommands-inviteall/server/server.ts +++ b/apps/meteor/app/slashcommands-inviteall/server/server.ts @@ -10,6 +10,7 @@ import { Meteor } from 'meteor/meteor'; import { isTruthy } from '../../../lib/isTruthy'; import { i18n } from '../../../server/lib/i18n'; +import { canAccessRoomAsync } from '../../authorization/server'; import { addUsersToRoomMethod } from '../../lib/server/methods/addUsersToRoom'; import { createChannelMethod } from '../../lib/server/methods/createChannel'; import { createPrivateGroupMethod } from '../../lib/server/methods/createPrivateGroup'; @@ -55,6 +56,14 @@ function inviteAll(type: T): SlashCommand['callback'] { }); return; } + + if (!(await canAccessRoomAsync(baseChannel, user))) { + void api.broadcast('notify.ephemeralMessage', userId, message.rid, { + msg: i18n.t('Room_not_exist_or_not_permission', { lng }), + }); + return; + } + const cursor = Subscriptions.findByRoomIdWhenUsernameExists(baseChannel._id, { projection: { 'u.username': 1 }, }); diff --git a/apps/meteor/app/statistics/server/functions/updateStatsCounter.ts b/apps/meteor/app/statistics/server/functions/updateStatsCounter.ts index c9eb356841be6..af88e3c548adb 100644 --- a/apps/meteor/app/statistics/server/functions/updateStatsCounter.ts +++ b/apps/meteor/app/statistics/server/functions/updateStatsCounter.ts @@ -1,11 +1,17 @@ import { Settings } from '@rocket.chat/models'; +import { notifyOnSettingChanged } from '../../../lib/server/lib/notifyListener'; import telemetryEvent from '../lib/telemetryEvents'; type updateCounterDataType = { settingsId: string }; export function updateCounter(data: updateCounterDataType): void { - void Settings.incrementValueById(data.settingsId); + void (async () => { + const { value } = await Settings.incrementValueById(data.settingsId, 1, { returnDocument: 'after' }); + if (value) { + void notifyOnSettingChanged(value); + } + })(); } telemetryEvent.register('updateCounter', updateCounter); diff --git a/apps/meteor/app/statistics/server/lib/getAppsStatistics.js b/apps/meteor/app/statistics/server/lib/getAppsStatistics.js deleted file mode 100644 index 1d84bead3e85c..0000000000000 --- a/apps/meteor/app/statistics/server/lib/getAppsStatistics.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Apps } from '@rocket.chat/apps'; -import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; - -import { Info } from '../../../utils/rocketchat.info'; - -export function getAppsStatistics() { - return { - engineVersion: Info.marketplaceApiVersion, - totalInstalled: (Apps.self?.isInitialized() && Apps.getManager().get().length) ?? 0, - totalActive: (Apps.self?.isInitialized() && Apps.getManager().get({ enabled: true }).length) ?? 0, - totalFailed: - (Apps.self?.isInitialized() && - Apps.getManager() - .get({ disabled: true }) - .filter(({ app: { status } }) => status !== AppStatus.MANUALLY_DISABLED).length) ?? - 0, - }; -} diff --git a/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts b/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts new file mode 100644 index 0000000000000..5d6cb4ab94415 --- /dev/null +++ b/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts @@ -0,0 +1,66 @@ +import { Apps } from '@rocket.chat/apps'; +import { AppStatus, AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; +import mem from 'mem'; + +import { SystemLogger } from '../../../../server/lib/logger/system'; +import { Info } from '../../../utils/rocketchat.info'; + +export type AppsStatistics = { + engineVersion: string; + totalInstalled: number | false; + totalActive: number | false; + totalFailed: number | false; +}; + +async function _getAppsStatistics(): Promise { + if (!Apps.self?.isInitialized()) { + return { + engineVersion: Info.marketplaceApiVersion, + totalInstalled: false, + totalActive: false, + totalFailed: false, + }; + } + + try { + const apps = await Apps.getManager().get(); + + let totalInstalled = 0; + let totalActive = 0; + let totalFailed = 0; + + await Promise.all( + apps.map(async (app) => { + totalInstalled++; + + const status = await app.getStatus(); + + if (status === AppStatus.MANUALLY_DISABLED) { + totalFailed++; + } + + if (AppStatusUtils.isEnabled(status)) { + totalActive++; + } + }), + ); + + return { + engineVersion: Info.marketplaceApiVersion, + totalInstalled, + totalActive, + totalFailed, + }; + } catch (err: unknown) { + SystemLogger.error({ msg: 'Exception while getting Apps statistics', err }); + return { + engineVersion: Info.marketplaceApiVersion, + totalInstalled: false, + totalActive: false, + totalFailed: false, + }; + } +} + +// since this function is called every 5s by `setPrometheusData` we're memoizing the result since the result won't change that often +export const getAppsStatistics = mem(_getAppsStatistics, { maxAge: 60000 }); diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index a6fcc5b17b5b1..cff2aaefcc5ac 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -467,7 +467,7 @@ export const statistics = { }), ); - statistics.apps = getAppsStatistics(); + statistics.apps = await getAppsStatistics(); statistics.services = await getServicesStatistics(); statistics.importer = getImporterStatistics(); statistics.videoConf = await VideoConf.getStatistics(); diff --git a/apps/meteor/app/ui-utils/client/lib/MessageAction.ts b/apps/meteor/app/ui-utils/client/lib/MessageAction.ts index 6a3ddd45ca66d..c1f9590b98ee8 100644 --- a/apps/meteor/app/ui-utils/client/lib/MessageAction.ts +++ b/apps/meteor/app/ui-utils/client/lib/MessageAction.ts @@ -25,7 +25,7 @@ export type MessageActionContext = type MessageActionType = 'communication' | 'interaction' | 'duplication' | 'apps' | 'management'; -type MessageActionConditionProps = { +export type MessageActionConditionProps = { message: IMessage; user: IUser | undefined; room: IRoom; @@ -65,6 +65,7 @@ export type MessageActionConfig = { ) => any; condition?: (props: MessageActionConditionProps) => Promise | boolean; type?: MessageActionType; + disabled?: (props: MessageActionConditionProps) => boolean; }; class MessageAction { diff --git a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts index f3cf0be67ca99..2f2793f7493b9 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts @@ -1,5 +1,5 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import { isRoomFederated } from '@rocket.chat/core-typings'; +import { isE2EEMessage, isRoomFederated } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import moment from 'moment'; @@ -63,6 +63,9 @@ Meteor.startup(async () => { }, order: 0, group: 'menu', + disabled({ message }) { + return isE2EEMessage(message); + }, }); MessageAction.addButton({ @@ -87,6 +90,9 @@ Meteor.startup(async () => { }, order: 0, group: 'message', + disabled({ message }) { + return isE2EEMessage(message); + }, }); MessageAction.addButton({ @@ -139,6 +145,9 @@ Meteor.startup(async () => { }, order: 5, group: 'menu', + disabled({ message }) { + return isE2EEMessage(message); + }, }); MessageAction.addButton({ @@ -185,7 +194,7 @@ Meteor.startup(async () => { return false; } const blockEditInMinutes = settings.Message_AllowEditing_BlockEditInMinutes as number; - const bypassBlockTimeLimit = hasPermission('bypass-time-limit-edit-and-delete'); + const bypassBlockTimeLimit = hasPermission('bypass-time-limit-edit-and-delete', message.rid); if (!bypassBlockTimeLimit && blockEditInMinutes) { let msgTs; diff --git a/apps/meteor/app/utils/client/restrictions.ts b/apps/meteor/app/utils/client/restrictions.ts index 261eddf4467dc..d4c6b62d68dd9 100644 --- a/apps/meteor/app/utils/client/restrictions.ts +++ b/apps/meteor/app/utils/client/restrictions.ts @@ -1,7 +1,7 @@ import { settings } from '../../settings/client'; import { fileUploadIsValidContentTypeFromSettings } from '../lib/restrictions'; -export const fileUploadIsValidContentType = function (type: string, customWhiteList?: string): boolean { +export const fileUploadIsValidContentType = function (type: string | undefined, customWhiteList?: string): boolean { const blackList = settings.get('FileUpload_MediaTypeBlackList'); const whiteList = customWhiteList || settings.get('FileUpload_MediaTypeWhiteList'); diff --git a/apps/meteor/app/utils/lib/mimeTypes.ts b/apps/meteor/app/utils/lib/mimeTypes.ts index f2da185f84ba8..909a955d6724d 100644 --- a/apps/meteor/app/utils/lib/mimeTypes.ts +++ b/apps/meteor/app/utils/lib/mimeTypes.ts @@ -12,4 +12,9 @@ const getExtension = (param: string): string => { return !extension || typeof extension === 'boolean' ? '' : extension; }; -export { mime, getExtension }; +const getMimeType = (fileName: string): string => { + const fileMimeType = mime.lookup(fileName); + return typeof fileMimeType === 'string' ? fileMimeType : 'application/octet-stream'; +}; + +export { mime, getExtension, getMimeType }; diff --git a/apps/meteor/app/utils/lib/restrictions.ts b/apps/meteor/app/utils/lib/restrictions.ts index ebebe113f31e6..bf859e5b4700c 100644 --- a/apps/meteor/app/utils/lib/restrictions.ts +++ b/apps/meteor/app/utils/lib/restrictions.ts @@ -1,12 +1,10 @@ -import _ from 'underscore'; - export const fileUploadMediaWhiteList = function (customWhiteList: string): string[] | undefined { const mediaTypeWhiteList = customWhiteList; if (!mediaTypeWhiteList || mediaTypeWhiteList === '*') { return; } - return _.map(mediaTypeWhiteList.split(','), (item) => { + return mediaTypeWhiteList.split(',').map((item) => { return item.trim(); }); }; @@ -17,37 +15,47 @@ const fileUploadMediaBlackList = function (customBlackList: string): string[] | return; } - return _.map(blacklist.split(','), (item) => item.trim()); + return blacklist.split(',').map((item) => item.trim()); }; -const isTypeOnList = function (type: string, list: string[]): boolean | undefined { - if (_.contains(list, type)) { +const isTypeOnList = function (type?: string, list?: string[]): boolean { + if (!type || !list) { + return false; + } + + if (list.includes(type)) { return true; } const wildCardGlob = '/*'; - const wildcards = _.filter(list, (item) => { + const wildcards = list.filter((item) => { return item.indexOf(wildCardGlob) > 0; }); - if (_.contains(wildcards, type.replace(/(\/.*)$/, wildCardGlob))) { + if (wildcards.includes(type.replace(/(\/.*)$/, wildCardGlob))) { return true; } + + return false; }; -export const fileUploadIsValidContentTypeFromSettings = function (type: string, customWhiteList: string, customBlackList: string): boolean { +export const fileUploadIsValidContentTypeFromSettings = function ( + type: string | undefined, + customWhiteList: string, + customBlackList: string, +): boolean { const blackList = fileUploadMediaBlackList(customBlackList); const whiteList = fileUploadMediaWhiteList(customWhiteList); - if (!type && blackList) { + if (blackList && type && isTypeOnList(type, blackList)) { return false; } - if (blackList && isTypeOnList(type, blackList)) { - return false; + if (whiteList) { + return isTypeOnList(type, whiteList); } if (!whiteList) { return true; } - return !!isTypeOnList(type, whiteList); + return false; }; diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index b1f6fdff877cb..6d7a959218bf7 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "6.9.3" + "version": "6.10.0-rc.7" } diff --git a/apps/meteor/app/utils/server/restrictions.ts b/apps/meteor/app/utils/server/restrictions.ts index ca524b09d3510..6eb1c9a655d4a 100644 --- a/apps/meteor/app/utils/server/restrictions.ts +++ b/apps/meteor/app/utils/server/restrictions.ts @@ -1,7 +1,7 @@ import { settings } from '../../settings/server'; import { fileUploadIsValidContentTypeFromSettings } from '../lib/restrictions'; -export const fileUploadIsValidContentType = function (type: string, customWhiteList?: string): boolean { +export const fileUploadIsValidContentType = function (type: string | undefined, customWhiteList?: string): boolean { const blackList = settings.get('FileUpload_MediaTypeBlackList'); const whiteList = customWhiteList || settings.get('FileUpload_MediaTypeWhiteList'); diff --git a/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.ts b/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.ts index fb55d478b6f5f..4cca28f1d5a94 100644 --- a/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.ts +++ b/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.ts @@ -3,6 +3,7 @@ import semver from 'semver'; import { i18n } from '../../../../server/lib/i18n'; import { sendMessagesToAdmins } from '../../../../server/lib/sendMessagesToAdmins'; +import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { Info } from '../../../utils/rocketchat.info'; @@ -37,7 +38,8 @@ export const buildVersionUpdateMessage = async ( continue; } - await Settings.updateValueById('Update_LatestAvailableVersion', version.version); + (await Settings.updateValueById('Update_LatestAvailableVersion', version.version)).modifiedCount && + void notifyOnSettingChangedById('Update_LatestAvailableVersion'); await sendMessagesToAdmins({ msgs: async ({ adminUser }) => [ diff --git a/apps/meteor/app/version-check/server/methods/banner_dismiss.ts b/apps/meteor/app/version-check/server/methods/banner_dismiss.ts index 960379d1a99b0..5ffebcfbbd5a7 100644 --- a/apps/meteor/app/version-check/server/methods/banner_dismiss.ts +++ b/apps/meteor/app/version-check/server/methods/banner_dismiss.ts @@ -2,6 +2,8 @@ import { Users } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; +import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; + declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -17,5 +19,13 @@ Meteor.methods({ } await Users.setBannerReadById(userId, id); + + void notifyOnUserChange({ + id: userId, + clientAction: 'updated', + diff: { + [`banners.${id}.read`]: true, + }, + }); }, }); diff --git a/apps/meteor/ee/client/apps/@types/IOrchestrator.ts b/apps/meteor/client/apps/@types/IOrchestrator.ts similarity index 100% rename from apps/meteor/ee/client/apps/@types/IOrchestrator.ts rename to apps/meteor/client/apps/@types/IOrchestrator.ts diff --git a/apps/meteor/ee/client/apps/RealAppsEngineUIHost.js b/apps/meteor/client/apps/RealAppsEngineUIHost.js similarity index 78% rename from apps/meteor/ee/client/apps/RealAppsEngineUIHost.js rename to apps/meteor/client/apps/RealAppsEngineUIHost.js index bcd1a254a4bd3..4377f7c66abaa 100644 --- a/apps/meteor/ee/client/apps/RealAppsEngineUIHost.js +++ b/apps/meteor/client/apps/RealAppsEngineUIHost.js @@ -1,11 +1,11 @@ import { AppsEngineUIHost } from '@rocket.chat/apps-engine/client/AppsEngineUIHost'; import { Meteor } from 'meteor/meteor'; -import { ChatRoom } from '../../../app/models/client'; -import { getUserAvatarURL } from '../../../app/utils/client/getUserAvatarURL'; -import { sdk } from '../../../app/utils/client/lib/SDKClient'; -import { RoomManager } from '../../../client/lib/RoomManager'; -import { baseURI } from '../../../client/lib/baseURI'; +import { ChatRoom } from '../../app/models/client'; +import { getUserAvatarURL } from '../../app/utils/client/getUserAvatarURL'; +import { sdk } from '../../app/utils/client/lib/SDKClient'; +import { RoomManager } from '../lib/RoomManager'; +import { baseURI } from '../lib/baseURI'; export class RealAppsEngineUIHost extends AppsEngineUIHost { constructor() { diff --git a/apps/meteor/ee/client/apps/gameCenter/GameCenter.tsx b/apps/meteor/client/apps/gameCenter/GameCenter.tsx similarity index 87% rename from apps/meteor/ee/client/apps/gameCenter/GameCenter.tsx rename to apps/meteor/client/apps/gameCenter/GameCenter.tsx index 75f4882ce7476..3261d1e1c51e3 100644 --- a/apps/meteor/ee/client/apps/gameCenter/GameCenter.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenter.tsx @@ -3,8 +3,8 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import React, { useState } from 'react'; import type { ReactElement } from 'react'; -import { preventSyntheticEvent } from '../../../../client/lib/utils/preventSyntheticEvent'; -import { useRoomToolbox } from '../../../../client/views/room/contexts/RoomToolboxContext'; +import { preventSyntheticEvent } from '../../lib/utils/preventSyntheticEvent'; +import { useRoomToolbox } from '../../views/room/contexts/RoomToolboxContext'; import GameCenterContainer from './GameCenterContainer'; import GameCenterList from './GameCenterList'; import { useExternalComponentsQuery } from './hooks/useExternalComponentsQuery'; diff --git a/apps/meteor/ee/client/apps/gameCenter/GameCenterContainer.tsx b/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx similarity index 95% rename from apps/meteor/ee/client/apps/gameCenter/GameCenterContainer.tsx rename to apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx index 1f37e5d6358a2..f589dd21ed502 100644 --- a/apps/meteor/ee/client/apps/gameCenter/GameCenterContainer.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx @@ -9,7 +9,7 @@ import { ContextualbarBack, ContextualbarContent, ContextualbarClose, -} from '../../../../client/components/Contextualbar'; +} from '../../components/Contextualbar'; import type { IGame } from './GameCenter'; interface IGameCenterContainerProps { diff --git a/apps/meteor/ee/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx b/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx similarity index 79% rename from apps/meteor/ee/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx rename to apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx index e7afa0d9e6897..d0dcc6fad4fee 100644 --- a/apps/meteor/ee/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx @@ -4,11 +4,11 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useState } from 'react'; -import GenericModal from '../../../../client/components/GenericModal'; -import UserAutoCompleteMultipleFederated from '../../../../client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; -import { useOpenedRoom } from '../../../../client/lib/RoomManager'; -import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; -import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling'; +import GenericModal from '../../components/GenericModal'; +import UserAutoCompleteMultipleFederated from '../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; +import { useOpenedRoom } from '../../lib/RoomManager'; +import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; +import { callWithErrorHandling } from '../../lib/utils/callWithErrorHandling'; import type { IGame } from './GameCenter'; type Username = Exclude; diff --git a/apps/meteor/ee/client/apps/gameCenter/GameCenterList.tsx b/apps/meteor/client/apps/gameCenter/GameCenterList.tsx similarity index 91% rename from apps/meteor/ee/client/apps/gameCenter/GameCenterList.tsx rename to apps/meteor/client/apps/gameCenter/GameCenterList.tsx index f45ba934ba3bb..58a4f05f5362c 100644 --- a/apps/meteor/ee/client/apps/gameCenter/GameCenterList.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterList.tsx @@ -3,13 +3,8 @@ import { useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback } from 'react'; -import { - ContextualbarHeader, - ContextualbarTitle, - ContextualbarClose, - ContextualbarContent, -} from '../../../../client/components/Contextualbar'; -import { FormSkeleton } from '../../../../client/components/Skeleton'; +import { ContextualbarHeader, ContextualbarTitle, ContextualbarClose, ContextualbarContent } from '../../components/Contextualbar'; +import { FormSkeleton } from '../../components/Skeleton'; import type { IGame } from './GameCenter'; import GameCenterInvitePlayersModal from './GameCenterInvitePlayersModal'; diff --git a/apps/meteor/ee/client/apps/gameCenter/hooks/useExternalComponentsQuery.ts b/apps/meteor/client/apps/gameCenter/hooks/useExternalComponentsQuery.ts similarity index 100% rename from apps/meteor/ee/client/apps/gameCenter/hooks/useExternalComponentsQuery.ts rename to apps/meteor/client/apps/gameCenter/hooks/useExternalComponentsQuery.ts diff --git a/apps/meteor/ee/client/apps/orchestrator.ts b/apps/meteor/client/apps/orchestrator.ts similarity index 94% rename from apps/meteor/ee/client/apps/orchestrator.ts rename to apps/meteor/client/apps/orchestrator.ts index d16be3d0c8c75..f33807d25be4d 100644 --- a/apps/meteor/ee/client/apps/orchestrator.ts +++ b/apps/meteor/client/apps/orchestrator.ts @@ -4,10 +4,10 @@ import type { IPermission } from '@rocket.chat/apps-engine/definition/permission import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import type { Serialized } from '@rocket.chat/core-typings'; -import { hasAtLeastOnePermission } from '../../../app/authorization/client'; -import { sdk } from '../../../app/utils/client/lib/SDKClient'; -import { dispatchToastMessage } from '../../../client/lib/toast'; -import type { App } from '../../../client/views/marketplace/types'; +import { hasAtLeastOnePermission } from '../../app/authorization/client'; +import { sdk } from '../../app/utils/client/lib/SDKClient'; +import { dispatchToastMessage } from '../lib/toast'; +import type { App } from '../views/marketplace/types'; import type { IAppExternalURL, ICategory } from './@types/IOrchestrator'; import { RealAppsEngineUIHost } from './RealAppsEngineUIHost'; diff --git a/apps/meteor/client/components/CustomScrollbars/CustomScrollbars.tsx b/apps/meteor/client/components/CustomScrollbars/CustomScrollbars.tsx index 18bdbdead14e0..d443639794f15 100644 --- a/apps/meteor/client/components/CustomScrollbars/CustomScrollbars.tsx +++ b/apps/meteor/client/components/CustomScrollbars/CustomScrollbars.tsx @@ -1,8 +1,8 @@ import { Palette } from '@rocket.chat/fuselage'; import type { ScrollValues } from 'rc-scrollbars'; import { Scrollbars } from 'rc-scrollbars'; -import type { MutableRefObject, CSSProperties, ReactNode, ReactElement } from 'react'; -import React, { memo, forwardRef, useCallback } from 'react'; +import type { MutableRefObject, CSSProperties, ReactNode } from 'react'; +import React, { memo, forwardRef, useCallback, useMemo } from 'react'; export type CustomScrollbarsProps = { overflowX?: boolean; @@ -14,10 +14,17 @@ export type CustomScrollbarsProps = { autoHide?: boolean; }; +const styleDefault: CSSProperties = { + flexGrow: 1, + overflowY: 'hidden', +}; + const CustomScrollbars = forwardRef(function CustomScrollbars( - { children, onScroll, overflowX, renderView, ...props }, + { children, style, onScroll, overflowX, renderView, ...props }, ref, ) { + const scrollbarsStyle = useMemo(() => ({ ...style, ...styleDefault }), [style]); + const refSetter = useCallback( (scrollbarRef) => { if (ref && scrollbarRef) { @@ -38,12 +45,11 @@ const CustomScrollbars = forwardRef(function autoHide autoHideTimeout={2000} autoHideDuration={500} + style={scrollbarsStyle} onScrollFrame={onScroll} renderView={renderView} - renderTrackHorizontal={ - overflowX ? undefined : (props): ReactElement =>
- } - renderThumbVertical={({ style, ...props }): JSX.Element => ( + renderTrackHorizontal={overflowX ? undefined : (props) =>
} + renderThumbVertical={({ style, ...props }) => (
)} children={children} diff --git a/apps/meteor/client/components/CustomScrollbars/VirtuosoScrollbars.tsx b/apps/meteor/client/components/CustomScrollbars/VirtuosoScrollbars.tsx index a7d0371e4ab8b..b07083be1a036 100644 --- a/apps/meteor/client/components/CustomScrollbars/VirtuosoScrollbars.tsx +++ b/apps/meteor/client/components/CustomScrollbars/VirtuosoScrollbars.tsx @@ -1,4 +1,4 @@ -import type { ComponentProps, ReactElement, Ref } from 'react'; +import type { ComponentProps, Ref } from 'react'; import React, { forwardRef } from 'react'; import CustomScrollbars from './CustomScrollbars'; @@ -8,13 +8,9 @@ type VirtuosoScrollbarsProps = ComponentProps; const VirtuosoScrollbars = forwardRef(function VirtuosoScrollbars( { style, children, ...props }: VirtuosoScrollbarsProps, ref: Ref, -): ReactElement { +) { return ( -
} - > +
}> {children} ); diff --git a/apps/meteor/client/components/GenericMenu/GenericMenu.tsx b/apps/meteor/client/components/GenericMenu/GenericMenu.tsx index 9d8367f7ad982..294a987705237 100644 --- a/apps/meteor/client/components/GenericMenu/GenericMenu.tsx +++ b/apps/meteor/client/components/GenericMenu/GenericMenu.tsx @@ -11,6 +11,7 @@ type GenericMenuCommonProps = { title: string; icon?: ComponentProps['icon']; disabled?: boolean; + callbackAction?: () => void; }; type GenericMenuConditionalProps = | { @@ -28,7 +29,7 @@ type GenericMenuConditionalProps = type GenericMenuProps = GenericMenuCommonProps & GenericMenuConditionalProps & Omit, 'children'>; -const GenericMenu = ({ title, icon = 'menu', disabled, onAction, ...props }: GenericMenuProps) => { +const GenericMenu = ({ title, icon = 'menu', disabled, onAction, callbackAction, ...props }: GenericMenuProps) => { const t = useTranslation(); const sections = 'sections' in props && props.sections; @@ -37,11 +38,11 @@ const GenericMenu = ({ title, icon = 'menu', disabled, onAction, ...props }: Gen const itemsList = sections ? sections.reduce((acc, { items }) => [...acc, ...items], [] as GenericMenuItemProps[]) : items || []; const disabledKeys = itemsList.filter(({ disabled }) => disabled).map(({ id }) => id); - const handleAction = useHandleMenuAction(itemsList || []); + const handleAction = useHandleMenuAction(itemsList || [], callbackAction); const hasIcon = itemsList.some(({ icon }) => icon); const handleItems = (items: GenericMenuItemProps[]) => - hasIcon ? items.map((item) => ({ ...item, gap: !item.icon && !item.status })) : items; + hasIcon ? items.map((item) => ({ ...item, gap: item.gap ?? (!item.icon && !item.status) })) : items; const isMenuEmpty = !(sections && sections.length > 0) && !(items && items.length > 0); diff --git a/apps/meteor/client/components/GenericMenu/GenericMenuItem.tsx b/apps/meteor/client/components/GenericMenu/GenericMenuItem.tsx index ec987a1ee28d6..44feedf861154 100644 --- a/apps/meteor/client/components/GenericMenu/GenericMenuItem.tsx +++ b/apps/meteor/client/components/GenericMenu/GenericMenuItem.tsx @@ -12,14 +12,15 @@ export type GenericMenuItemProps = { disabled?: boolean; description?: ReactNode; gap?: boolean; + tooltip?: string; }; -const GenericMenuItem = ({ icon, content, addon, status, gap }: GenericMenuItemProps) => ( +const GenericMenuItem = ({ icon, content, addon, status, gap, tooltip }: GenericMenuItemProps) => ( <> {gap && } {icon && } {status && {status}} - {content && {content}} + {content && {content}} {addon && {addon}} ); diff --git a/apps/meteor/client/components/GenericMenu/hooks/useHandleMenuAction.tsx b/apps/meteor/client/components/GenericMenu/hooks/useHandleMenuAction.tsx index c67c8acd3c5e8..dd11157651943 100644 --- a/apps/meteor/client/components/GenericMenu/hooks/useHandleMenuAction.tsx +++ b/apps/meteor/client/components/GenericMenu/hooks/useHandleMenuAction.tsx @@ -1,13 +1,13 @@ -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import type { GenericMenuItemProps } from '../GenericMenuItem'; -export const useHandleMenuAction = (items: GenericMenuItemProps[], close?: () => void) => { - return useMutableCallback((id) => { +export const useHandleMenuAction = (items: GenericMenuItemProps[], callbackAction?: () => void) => { + return useEffectEvent((id) => { const item = items.find((item) => item.id === id && !!item.onClick); if (item) { item.onClick?.(); - close?.(); + callbackAction?.(); } }); }; diff --git a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx index 8f4f76a14afb0..2cabfed460bd7 100644 --- a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx +++ b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx @@ -112,6 +112,7 @@ export const ImageGallery = ({ images, onClose, loadMore }: { images: IUpload[]; const swiperRef = useRef(null); const [, setSwiperInst] = useState(); const [zoomScale, setZoomScale] = useState(1); + const [gridSize, setGridSize] = useState(images.length); const handleZoom = (ratio: number) => { if (swiperRef.current?.swiper.zoom) { @@ -174,16 +175,21 @@ export const ImageGallery = ({ images, onClose, loadMore }: { images: IUpload[]; onKeyPress={(_, keyCode) => String(keyCode) === '27' && onClose()} modules={[Navigation, Zoom, Keyboard, A11y]} onInit={(swiper) => setSwiperInst(swiper)} - onReachEnd={loadMore} + onSlidesGridLengthChange={(swiper) => { + swiper.slideTo(images.length - gridSize, 0); + setGridSize(images.length); + }} + onReachBeginning={loadMore} + initialSlide={images.length - 1} > - {images?.map(({ _id, url }) => ( + {[...images].reverse().map(({ _id, path, url }) => (
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions, - jsx-a11y/click-events-have-key-events - */} - + jsx-a11y/click-events-have-key-events + */} +
diff --git a/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx b/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx index 39242161ed469..1d8987995e8cf 100644 --- a/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx +++ b/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx @@ -2,6 +2,7 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; import InfoPanel from '.'; +import { createFakeRoom } from '../../../tests/mocks/data'; import RetentionPolicyCallout from './RetentionPolicyCallout'; export default { @@ -20,6 +21,8 @@ export default { }, } as ComponentMeta; +const fakeRoom = createFakeRoom(); + export const Default: ComponentStory = () => ( @@ -52,7 +55,7 @@ export const Default: ComponentStory = () => ( - + ); diff --git a/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.spec.tsx b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.spec.tsx new file mode 100644 index 0000000000000..9a2e7eac4c45a --- /dev/null +++ b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.spec.tsx @@ -0,0 +1,28 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import '@testing-library/jest-dom/extend-expect'; + +import { createRenteionPolicySettingsMock as createMock } from '../../../tests/mocks/client/mockRetentionPolicySettings'; +import { createFakeRoom } from '../../../tests/mocks/data'; +import { setDate } from '../../../tests/mocks/mockDate'; +import RetentionPolicyCallout from './RetentionPolicyCallout'; + +jest.useFakeTimers(); + +describe('RetentionPolicyCallout', () => { + it('Should render callout if settings are valid', () => { + setDate(); + const fakeRoom = createFakeRoom({ t: 'c' }); + render(, { wrapper: createMock({ appliesToChannels: true, TTLChannels: 60000 }) }); + expect(screen.getByRole('alert')).toHaveTextContent('a minute June 1, 2024, 12:30 AM'); + }); + + it('Should not render callout if settings are invalid', () => { + setDate(); + const fakeRoom = createFakeRoom({ t: 'c' }); + render(, { + wrapper: createMock({ appliesToChannels: true, TTLChannels: 60000, advancedPrecisionCron: '* * * 12 *', advancedPrecision: true }), + }); + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + }); +}); diff --git a/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx index 06f6ed133dc33..cbefeb2c72c1c 100644 --- a/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx +++ b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx @@ -1,31 +1,22 @@ +import type { IRoom } from '@rocket.chat/core-typings'; import { Callout } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import type { FC } from 'react'; import React from 'react'; -import { useFormattedRelativeTime } from '../../hooks/useFormattedRelativeTime'; -import { getMaxAgeInMS } from '../../views/room/hooks/useRetentionPolicy'; +import { usePruneWarningMessage } from '../../hooks/usePruneWarningMessage'; +import { withErrorBoundary } from '../withErrorBoundary'; -type RetentionPolicyCalloutProps = { - filesOnly: boolean; - excludePinned: boolean; - maxAge: number; -}; - -const RetentionPolicyCallout: FC = ({ filesOnly, excludePinned, maxAge }) => { +const RetentionPolicyCallout = ({ room }: { room: IRoom }) => { + const message = usePruneWarningMessage(room); const t = useTranslation(); - const time = useFormattedRelativeTime(getMaxAgeInMS(maxAge)); return (
- {filesOnly && excludePinned &&

{t('RetentionPolicy_RoomWarning_FilesOnly', { time })}

} - {filesOnly && !excludePinned &&

{t('RetentionPolicy_RoomWarning_UnpinnedFilesOnly', { time })}

} - {!filesOnly && excludePinned &&

{t('RetentionPolicy_RoomWarning', { time })}

} - {!filesOnly && !excludePinned &&

{t('RetentionPolicy_RoomWarning_Unpinned', { time })}

} +

{message}

); }; -export default RetentionPolicyCallout; +export default withErrorBoundary(RetentionPolicyCallout); diff --git a/apps/meteor/client/components/ModalBackdrop.tsx b/apps/meteor/client/components/ModalBackdrop.tsx index 18d930f9b1cc8..e1e6964961837 100644 --- a/apps/meteor/client/components/ModalBackdrop.tsx +++ b/apps/meteor/client/components/ModalBackdrop.tsx @@ -90,7 +90,7 @@ const ModalBackdrop = ({ children, onDismiss }: ModalBackdropProps): ReactElemen children={children} className='rcx-modal__backdrop' position='fixed' - zIndex={999999} + zIndex={9999} inset={0} display='flex' flexDirection='column' diff --git a/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx b/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx index 0fd882f2c5cc2..401448ceb3966 100644 --- a/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx @@ -18,7 +18,7 @@ import type { ReactElement } from 'react'; import React, { useCallback, useState, useEffect, useMemo } from 'react'; import { useForm } from 'react-hook-form'; -import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule'; +import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; import { dispatchToastMessage } from '../../../lib/toast'; import GenericModal from '../../GenericModal'; import Tags from '../Tags'; diff --git a/apps/meteor/client/components/Page/PageHeader.tsx b/apps/meteor/client/components/Page/PageHeader.tsx index 197592d3e98cc..4549c69dccec9 100644 --- a/apps/meteor/client/components/Page/PageHeader.tsx +++ b/apps/meteor/client/components/Page/PageHeader.tsx @@ -24,12 +24,20 @@ const PageHeader: FC = ({ children = undefined, title, onClickB - + {isMobile && ( diff --git a/apps/meteor/client/components/Page/PageScrollableContent.tsx b/apps/meteor/client/components/Page/PageScrollableContent.tsx index b1731f4eb0f6e..c3ac6869f277f 100644 --- a/apps/meteor/client/components/Page/PageScrollableContent.tsx +++ b/apps/meteor/client/components/Page/PageScrollableContent.tsx @@ -1,3 +1,4 @@ +import { css } from '@rocket.chat/css-in-js'; import type { Scrollable } from '@rocket.chat/fuselage'; import { Box } from '@rocket.chat/fuselage'; import type { ComponentProps } from 'react'; @@ -24,8 +25,18 @@ const PageScrollableContent = forwardRef - - + + ); diff --git a/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx b/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx index 10bea7cc0b60a..0da0e942b2e2a 100644 --- a/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx +++ b/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx @@ -1,9 +1,10 @@ +import type { IRoom } from '@rocket.chat/core-typings'; import { AutoComplete, Option, Box } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { RoomAvatar } from '@rocket.chat/ui-avatar'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; -import type { ComponentProps } from 'react'; +import type { ComponentProps, ReactElement } from 'react'; import React, { memo, useMemo, useState } from 'react'; const generateQuery = ( @@ -12,7 +13,11 @@ const generateQuery = ( selector: string; } => ({ selector: JSON.stringify({ name: term }) }); -type RoomAutoCompleteProps = Omit, 'filter'> & { scope?: 'admin' | 'regular' }; +type RoomAutoCompleteProps = Omit, 'filter'> & { + scope?: 'admin' | 'regular'; + renderRoomIcon?: (props: { encrypted: IRoom['encrypted']; type: IRoom['t'] }) => ReactElement | null; + setSelectedRoom?: React.Dispatch>; +}; const AVATAR_SIZE = 'x20'; @@ -27,7 +32,7 @@ const ROOM_AUTOCOMPLETE_PARAMS = { }, } as const; -const RoomAutoComplete = ({ value, onChange, scope = 'regular', ...props }: RoomAutoCompleteProps) => { +const RoomAutoComplete = ({ value, onChange, scope = 'regular', renderRoomIcon, setSelectedRoom, ...props }: RoomAutoCompleteProps) => { const [filter, setFilter] = useState(''); const filterDebounced = useDebouncedValue(filter, 300); const roomsAutoCompleteEndpoint = useEndpoint('GET', ROOM_AUTOCOMPLETE_PARAMS[scope].endpoint); @@ -43,9 +48,9 @@ const RoomAutoComplete = ({ value, onChange, scope = 'regular', ...props }: Room const options = useMemo( () => result.isSuccess - ? result.data.items.map(({ name, fname, _id, avatarETag, t }) => ({ + ? result.data.items.map(({ name, fname, _id, avatarETag, t, encrypted }) => ({ value: _id, - label: { name: fname || name, avatarETag, type: t }, + label: { name: fname || name, avatarETag, type: t, encrypted }, })) : [], [result.data?.items, result.isSuccess], @@ -55,7 +60,14 @@ const RoomAutoComplete = ({ value, onChange, scope = 'regular', ...props }: Room { + onChange(val); + + if (setSelectedRoom && typeof setSelectedRoom === 'function') { + const selectedRoom = result?.data?.items.find(({ _id }) => _id === val) as unknown as IRoom; + setSelectedRoom(selectedRoom); + } + }} filter={filter} setFilter={setFilter} renderSelected={({ selected: { value, label } }) => ( @@ -66,6 +78,7 @@ const RoomAutoComplete = ({ value, onChange, scope = 'regular', ...props }: Room {label?.name} + {renderRoomIcon?.({ ...label })} )} renderItem={({ value, label, ...props }) => ( diff --git a/apps/meteor/client/components/UserInfo/UserInfo.tsx b/apps/meteor/client/components/UserInfo/UserInfo.tsx index 025889edc7cd1..a4656373b4885 100644 --- a/apps/meteor/client/components/UserInfo/UserInfo.tsx +++ b/apps/meteor/client/components/UserInfo/UserInfo.tsx @@ -39,6 +39,7 @@ type UserInfoProps = UserInfoDataProps & { verified?: boolean; actions: ReactElement; roles: ReactElement[]; + reason?: string; }; const UserInfo = ({ @@ -59,6 +60,7 @@ const UserInfo = ({ customFields, canViewAllInfo, actions, + reason, ...props }: UserInfoProps): ReactElement => { const t = useTranslation(); @@ -79,53 +81,47 @@ const UserInfo = ({ {userDisplayName && } + {statusText && ( - + )} - {roles.length !== 0 && ( + {reason && ( - {t('Roles')} - {roles} + {t('Reason_for_joining')} + {reason} )} - {Number.isInteger(utcOffset) && ( - - {t('Local_Time')} - {utcOffset && } - - )} - - {username && username !== name && ( + {nickname && ( - {t('Username')} - {username} + {t('Nickname')} + {nickname} )} - {canViewAllInfo && ( + {roles.length !== 0 && ( - {t('Last_login')} - {lastLogin ? timeAgo(lastLogin) : t('Never')} + {t('Roles')} + {roles} )} - {name && ( + {username && username !== name && ( - {t('Full_Name')} - {name} + {t('Username')} + {username} )} - {nickname && ( + {Number.isInteger(utcOffset) && ( - {t('Nickname')} - {nickname} + {t('Local_Time')} + {utcOffset && } )} @@ -138,6 +134,13 @@ const UserInfo = ({ )} + {Number.isInteger(utcOffset) && canViewAllInfo && ( + + {t('Last_login')} + {lastLogin ? timeAgo(lastLogin) : t('Never')} + + )} + {phone && ( {t('Phone')} diff --git a/apps/meteor/ee/client/components/dashboards/DownloadDataButton.tsx b/apps/meteor/client/components/dashboards/DownloadDataButton.tsx similarity index 95% rename from apps/meteor/ee/client/components/dashboards/DownloadDataButton.tsx rename to apps/meteor/client/components/dashboards/DownloadDataButton.tsx index fb379021f7805..2edaf80237e30 100644 --- a/apps/meteor/ee/client/components/dashboards/DownloadDataButton.tsx +++ b/apps/meteor/client/components/dashboards/DownloadDataButton.tsx @@ -4,7 +4,7 @@ import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-context import type { ComponentProps, ReactElement } from 'react'; import React from 'react'; -import { downloadCsvAs } from '../../../../client/lib/download'; +import { downloadCsvAs } from '../../lib/download'; type RowFor = readonly unknown[] & { length: THeaders['length']; diff --git a/apps/meteor/ee/client/components/dashboards/PeriodSelector.tsx b/apps/meteor/client/components/dashboards/PeriodSelector.tsx similarity index 100% rename from apps/meteor/ee/client/components/dashboards/PeriodSelector.tsx rename to apps/meteor/client/components/dashboards/PeriodSelector.tsx diff --git a/apps/meteor/ee/client/components/dashboards/periods.ts b/apps/meteor/client/components/dashboards/periods.ts similarity index 100% rename from apps/meteor/ee/client/components/dashboards/periods.ts rename to apps/meteor/client/components/dashboards/periods.ts diff --git a/apps/meteor/ee/client/components/dashboards/usePeriodLabel.ts b/apps/meteor/client/components/dashboards/usePeriodLabel.ts similarity index 100% rename from apps/meteor/ee/client/components/dashboards/usePeriodLabel.ts rename to apps/meteor/client/components/dashboards/usePeriodLabel.ts diff --git a/apps/meteor/ee/client/components/dashboards/usePeriodSelectorState.ts b/apps/meteor/client/components/dashboards/usePeriodSelectorState.ts similarity index 100% rename from apps/meteor/ee/client/components/dashboards/usePeriodSelectorState.ts rename to apps/meteor/client/components/dashboards/usePeriodSelectorState.ts diff --git a/apps/meteor/ee/client/components/dashboards/usePeriodSelectorStorage.ts b/apps/meteor/client/components/dashboards/usePeriodSelectorStorage.ts similarity index 100% rename from apps/meteor/ee/client/components/dashboards/usePeriodSelectorStorage.ts rename to apps/meteor/client/components/dashboards/usePeriodSelectorStorage.ts diff --git a/apps/meteor/ee/client/components/deviceManagement/DeviceIcon.tsx b/apps/meteor/client/components/deviceManagement/DeviceIcon.tsx similarity index 100% rename from apps/meteor/ee/client/components/deviceManagement/DeviceIcon.tsx rename to apps/meteor/client/components/deviceManagement/DeviceIcon.tsx diff --git a/apps/meteor/ee/client/components/deviceManagement/DeviceManagementTable/DeviceManagementTable.tsx b/apps/meteor/client/components/deviceManagement/DeviceManagementTable/DeviceManagementTable.tsx similarity index 89% rename from apps/meteor/ee/client/components/deviceManagement/DeviceManagementTable/DeviceManagementTable.tsx rename to apps/meteor/client/components/deviceManagement/DeviceManagementTable/DeviceManagementTable.tsx index 40e550e1f4ba6..9bc4f11fd3cdc 100644 --- a/apps/meteor/ee/client/components/deviceManagement/DeviceManagementTable/DeviceManagementTable.tsx +++ b/apps/meteor/client/components/deviceManagement/DeviceManagementTable/DeviceManagementTable.tsx @@ -5,14 +5,9 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement } from 'react'; import React from 'react'; -import GenericNoResults from '../../../../../client/components/GenericNoResults/GenericNoResults'; -import { - GenericTable, - GenericTableHeader, - GenericTableBody, - GenericTableLoadingTable, -} from '../../../../../client/components/GenericTable'; -import { AsyncStatePhase } from '../../../../../client/lib/asyncState'; +import { AsyncStatePhase } from '../../../lib/asyncState'; +import GenericNoResults from '../../GenericNoResults/GenericNoResults'; +import { GenericTable, GenericTableHeader, GenericTableBody, GenericTableLoadingTable } from '../../GenericTable'; type DeviceManagementTableProps = { data?: Serialized>; diff --git a/apps/meteor/ee/client/components/deviceManagement/DeviceManagementTable/index.ts b/apps/meteor/client/components/deviceManagement/DeviceManagementTable/index.ts similarity index 100% rename from apps/meteor/ee/client/components/deviceManagement/DeviceManagementTable/index.ts rename to apps/meteor/client/components/deviceManagement/DeviceManagementTable/index.ts diff --git a/apps/meteor/ee/client/components/deviceManagement/LoggedOutBanner.tsx b/apps/meteor/client/components/deviceManagement/LoggedOutBanner.tsx similarity index 100% rename from apps/meteor/ee/client/components/deviceManagement/LoggedOutBanner.tsx rename to apps/meteor/client/components/deviceManagement/LoggedOutBanner.tsx diff --git a/apps/meteor/client/components/message/StatusIndicators.tsx b/apps/meteor/client/components/message/StatusIndicators.tsx index f47d25e7c7b2d..eb47fb040b210 100644 --- a/apps/meteor/client/components/message/StatusIndicators.tsx +++ b/apps/meteor/client/components/message/StatusIndicators.tsx @@ -1,5 +1,5 @@ import type { IMessage, ITranslatedMessage } from '@rocket.chat/core-typings'; -import { isEditedMessage, isE2EEMessage, isOTRMessage, isOTRAckMessage } from '@rocket.chat/core-typings'; +import { isEditedMessage, isE2EEMessage, isOTRMessage, isOTRAckMessage, isE2EEPinnedMessage } from '@rocket.chat/core-typings'; import { MessageStatusIndicator, MessageStatusIndicatorItem } from '@rocket.chat/fuselage'; import { useUserId, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -17,7 +17,7 @@ const StatusIndicators = ({ message }: StatusIndicatorsProps): ReactElement => { const starred = useShowStarred({ message }); const following = useShowFollowing({ message }); - const isEncryptedMessage = isE2EEMessage(message); + const isEncryptedMessage = isE2EEMessage(message) || isE2EEPinnedMessage(message); const isOtrMessage = isOTRMessage(message) || isOTRAckMessage(message); const uid = useUserId(); diff --git a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx index 7c2c2011cac92..394bf24995dc6 100644 --- a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx @@ -67,12 +67,12 @@ export const QuoteAttachment = ({ attachment }: QuoteAttachmentProps): ReactElem )} - {attachment.md ? : attachment.text.substring(attachment.text.indexOf('\n') + 1)} {attachment.attachments && ( )} + {attachment.md ? : attachment.text.substring(attachment.text.indexOf('\n') + 1)} diff --git a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx index 4301520c61738..b7bcd7d1e9dd8 100644 --- a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx @@ -6,11 +6,14 @@ import { MessageGenericPreviewTitle, MessageGenericPreviewDescription, } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useMediaUrl } from '@rocket.chat/ui-contexts'; import type { UIEvent } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { getFileExtension } from '../../../../../../lib/utils/getFileExtension'; +import { forAttachmentDownload, registerDownloadForUid } from '../../../../../hooks/useDownloadFromServiceWorker'; import MarkdownText from '../../../../MarkdownText'; import MessageCollapsible from '../../../MessageCollapsible'; import MessageContentBody from '../../../MessageContentBody'; @@ -31,18 +34,33 @@ const GenericFileAttachment = ({ collapsed, }: GenericFileAttachmentProps) => { const getURL = useMediaUrl(); + const uid = useUniqueId(); + const { t } = useTranslation(); const handleTitleClick = (event: UIEvent): void => { - if (openDocumentViewer && link && format === 'PDF') { + if (openDocumentViewer && link) { event.preventDefault(); - openDocumentViewer(getURL(link), format, ''); + + if (format === 'PDF') { + const url = new URL(getURL(link), window.location.origin); + url.searchParams.set('contentDisposition', 'inline'); + openDocumentViewer(url.toString(), format, ''); + return; + } + + registerDownloadForUid(uid, t, title); + forAttachmentDownload(uid, link); } }; const getExternalUrl = () => { if (!hasDownload || !link) return undefined; - if (openDocumentViewer) return `${getURL(link)}?download`; + if (openDocumentViewer) { + const url = new URL(getURL(link), window.location.origin); + url.searchParams.set('download', ''); + return url.toString(); + } return getURL(link); }; @@ -55,7 +73,12 @@ const GenericFileAttachment = ({ } > - + {title} {size && ( diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownload.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownload.tsx index de790f9e39363..b76cb268bb2f8 100644 --- a/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownload.tsx +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownload.tsx @@ -1,25 +1,20 @@ -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps, FC } from 'react'; import React from 'react'; -import Action from '../../Action'; +import type Action from '../../Action'; +import AttachmentDownloadBase from './AttachmentDownloadBase'; +import AttachmentEncryptedDownload from './AttachmentEncryptedDownload'; type AttachmentDownloadProps = Omit, 'icon'> & { title?: string | undefined; href: string }; const AttachmentDownload: FC = ({ title, href, ...props }) => { - const t = useTranslation(); - return ( - - ); + const isEncrypted = href.includes('/file-decrypt/'); + + if (isEncrypted) { + return ; + } + + return ; }; export default AttachmentDownload; diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownloadBase.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownloadBase.tsx new file mode 100644 index 0000000000000..48c078b9146c8 --- /dev/null +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownloadBase.tsx @@ -0,0 +1,26 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ComponentProps, FC } from 'react'; +import React from 'react'; + +import Action from '../../Action'; + +type AttachmentDownloadBaseProps = Omit, 'icon'> & { title?: string | undefined; href: string }; + +const AttachmentDownloadBase: FC = ({ title, href, ...props }) => { + const t = useTranslation(); + + return ( + + ); +}; + +export default AttachmentDownloadBase; diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentEncryptedDownload.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentEncryptedDownload.tsx new file mode 100644 index 0000000000000..1dc6752abdd0a --- /dev/null +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentEncryptedDownload.tsx @@ -0,0 +1,15 @@ +import type { ComponentProps, FC } from 'react'; +import React from 'react'; + +import { useDownloadFromServiceWorker } from '../../../../../hooks/useDownloadFromServiceWorker'; +import AttachmentDownloadBase from './AttachmentDownloadBase'; + +type AttachmentDownloadProps = ComponentProps; + +const AttachmentEncryptedDownload: FC = ({ title, href, ...props }) => { + const encryptedAnchorProps = useDownloadFromServiceWorker(href, title); + + return ; +}; + +export default AttachmentEncryptedDownload; diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx index 5b251ad6143f1..8195fdee5973c 100644 --- a/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentImage.tsx @@ -86,6 +86,7 @@ const AttachmentImage: FC = ({ id, previewUrl, dataSrc, lo alt='' width={dimensions.width} height={dimensions.height} + loading='lazy' /> diff --git a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts index 2ade3deb49267..715a84a2359bc 100644 --- a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts +++ b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts @@ -1,4 +1,12 @@ -import type { IMessage } from '@rocket.chat/core-typings'; +import { Base64 } from '@rocket.chat/base64'; +import type { IMessage, MessageAttachment } from '@rocket.chat/core-typings'; +import { + isFileImageAttachment, + isFileAttachment, + isFileAudioAttachment, + isFileVideoAttachment, + isQuoteAttachment, +} from '@rocket.chat/core-typings'; import type { Options } from '@rocket.chat/message-parser'; import { useSetting } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; @@ -10,6 +18,48 @@ import { useAutoTranslate } from '../../../views/room/MessageList/hooks/useAutoT import { useKatex } from '../../../views/room/MessageList/hooks/useKatex'; import { useSubscriptionFromMessageQuery } from './useSubscriptionFromMessageQuery'; +const normalizeAttachments = (attachments: MessageAttachment[], name?: string, type?: string): MessageAttachment[] => { + if (name) { + name = String.fromCharCode(...new TextEncoder().encode(name)); + } + + return attachments.map((attachment) => { + if (isQuoteAttachment(attachment) && attachment.attachments) { + attachment.attachments = normalizeAttachments(attachment.attachments); + return attachment; + } + + if (!attachment.encryption) { + return attachment; + } + + const key = Base64.encode( + JSON.stringify({ + ...attachment.encryption, + name, + type, + }), + ); + + if (isFileAttachment(attachment)) { + if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) { + attachment.title_link = `/file-decrypt${attachment.title_link}?key=${key}`; + } + if (isFileImageAttachment(attachment) && !attachment.image_url.startsWith('/file-decrypt/')) { + attachment.image_url = `/file-decrypt${attachment.image_url}?key=${key}`; + } + if (isFileAudioAttachment(attachment) && !attachment.audio_url.startsWith('/file-decrypt/')) { + attachment.audio_url = `/file-decrypt${attachment.audio_url}?key=${key}`; + } + if (isFileVideoAttachment(attachment) && !attachment.video_url.startsWith('/file-decrypt/')) { + attachment.video_url = `/file-decrypt${attachment.video_url}?key=${key}`; + } + } + + return attachment; + }); +}; + export const useNormalizedMessage = (message: TMessage): MessageWithMdEnforced => { const { katexEnabled, katexDollarSyntaxEnabled, katexParenthesisSyntaxEnabled } = useKatex(); const customDomains = useAutoLinkDomains(); @@ -30,6 +80,16 @@ export const useNormalizedMessage = (message: TMessag }), }; - return parseMessageTextToAstMarkdown(message, parseOptions, autoTranslateOptions); + const normalizedMessage = parseMessageTextToAstMarkdown(message, parseOptions, autoTranslateOptions); + + if (normalizedMessage.attachments) { + normalizedMessage.attachments = normalizeAttachments( + normalizedMessage.attachments, + normalizedMessage.file?.name, + normalizedMessage.file?.type, + ); + } + + return normalizedMessage; }, [showColors, customDomains, katexEnabled, katexDollarSyntaxEnabled, katexParenthesisSyntaxEnabled, message, autoTranslateOptions]); }; diff --git a/apps/meteor/client/components/message/toolbar/MessageActionMenu.tsx b/apps/meteor/client/components/message/toolbar/MessageActionMenu.tsx index 8d266fe35d0eb..6c35f7b73dbd7 100644 --- a/apps/meteor/client/components/message/toolbar/MessageActionMenu.tsx +++ b/apps/meteor/client/components/message/toolbar/MessageActionMenu.tsx @@ -1,8 +1,9 @@ +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { MouseEvent, ReactElement } from 'react'; import React from 'react'; -import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; +import type { MessageActionConditionProps, MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; import GenericMenu from '../../GenericMenu/GenericMenu'; import type { GenericMenuItemProps } from '../../GenericMenu/GenericMenuItem'; @@ -19,11 +20,13 @@ type MessageActionSection = { type MessageActionMenuProps = { onChangeMenuVisibility: (visible: boolean) => void; options: MessageActionConfigOption[]; + context: MessageActionConditionProps; + isMessageEncrypted: boolean; }; -const MessageActionMenu = ({ options, onChangeMenuVisibility }: MessageActionMenuProps): ReactElement => { +const MessageActionMenu = ({ options, onChangeMenuVisibility, context, isMessageEncrypted }: MessageActionMenuProps): ReactElement => { const t = useTranslation(); - + const id = useUniqueId(); const groupOptions = options .map((option) => ({ variant: option.color === 'alert' ? 'danger' : '', @@ -32,6 +35,9 @@ const MessageActionMenu = ({ options, onChangeMenuVisibility }: MessageActionMen content: t(option.label), onClick: option.action, type: option.type, + ...(option.disabled && { disabled: option?.disabled?.(context) }), + ...(option.disabled && + option?.disabled?.(context) && { tooltip: t('Action_not_available_encrypted_content', { action: t(option.label) }) }), })) .reduce((acc, option) => { const group = option.type ? option.type : ''; @@ -44,7 +50,31 @@ const MessageActionMenu = ({ options, onChangeMenuVisibility }: MessageActionMen acc.push(newSection); return acc; - }, [] as unknown as MessageActionSection[]); + }, [] as unknown as MessageActionSection[]) + .map((section) => { + if (section.id !== 'apps') { + return section; + } + + if (!isMessageEncrypted) { + return section; + } + + return { + id: 'apps', + title: t('Apps'), + items: [ + { + content: t('Unavailable'), + type: 'apps', + id, + disabled: true, + gap: false, + tooltip: t('Action_not_available_encrypted_content', { action: t('Apps') }), + }, + ], + }; + }); return ( action.action(e, { message, tabbar: toolbox, room, chat, autoTranslateOptions })} key={action.id} icon={action.icon} - title={t(action.label)} + title={ + action?.disabled?.({ message, room, user, subscription, settings: mapSettings, chat, context }) + ? t('Action_not_available_encrypted_content', { action: t(action.label) }) + : t(action.label) + } data-qa-id={action.label} data-qa-type='message-action-menu' + disabled={action?.disabled?.({ message, room, user, subscription, settings: mapSettings, chat, context })} /> ))} {actionsQueryResult.isSuccess && actionsQueryResult.data.menu.length > 0 && ( @@ -138,6 +143,8 @@ const MessageToolbar = ({ }))} onChangeMenuVisibility={onChangeMenuVisibility} data-qa-type='message-action-menu-options' + context={{ message, room, user, subscription, settings: mapSettings, chat, context }} + isMessageEncrypted={isE2EEMessage(message)} /> )} diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 84ea4039ca93b..89da3724cfbce 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -1,5 +1,6 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import { isDiscussionMessage, isThreadMainMessage, isE2EEMessage } 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 type { ReactElement } from 'react'; @@ -43,9 +44,18 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM const t = useTranslation(); const normalizedMessage = useNormalizedMessage(message); + const isMessageEncrypted = encrypted && normalizedMessage?.e2e === 'pending'; + + const quotes = normalizedMessage?.attachments?.filter(isQuoteAttachment) || []; + + const attachments = normalizedMessage?.attachments?.filter((attachment) => !isQuoteAttachment(attachment)) || []; return ( <> + {isMessageEncrypted && {t('E2E_message_encrypted_placeholder')}} + + {!!quotes?.length && } + {!normalizedMessage.blocks?.length && !!normalizedMessage.md?.length && ( <> {(!encrypted || normalizedMessage.e2e === 'done') && ( @@ -57,16 +67,15 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM searchText={searchText} /> )} - {encrypted && normalizedMessage.e2e === 'pending' && t('E2E_message_encrypted_placeholder')} )} + {!!attachments && } + {normalizedMessage.blocks && ( )} - {!!normalizedMessage?.attachments?.length && } - {oembedEnabled && !!normalizedMessage.urls?.length && } {normalizedMessage.actionLinks?.length && ( diff --git a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx index 7098a2709d5b2..b2f9f1d019e2c 100644 --- a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx @@ -1,5 +1,6 @@ import type { IThreadMainMessage, IThreadMessage } from '@rocket.chat/core-typings'; -import { isE2EEMessage } from '@rocket.chat/core-typings'; +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, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -37,14 +38,23 @@ const ThreadMessageContent = ({ message }: ThreadMessageContentProps): ReactElem const normalizedMessage = useNormalizedMessage(message); + const isMessageEncrypted = encrypted && normalizedMessage?.e2e === 'pending'; + + const quotes = normalizedMessage?.attachments?.filter(isQuoteAttachment) || []; + + const attachments = normalizedMessage?.attachments?.filter((attachment) => !isQuoteAttachment(attachment)) || []; + return ( <> + {isMessageEncrypted && {t('E2E_message_encrypted_placeholder')}} + + {!!quotes?.length && } + {!normalizedMessage.blocks?.length && !!normalizedMessage.md?.length && ( <> {(!encrypted || normalizedMessage.e2e === 'done') && ( )} - {encrypted && normalizedMessage.e2e === 'pending' && t('E2E_message_encrypted_placeholder')} )} @@ -52,7 +62,7 @@ const ThreadMessageContent = ({ message }: ThreadMessageContentProps): ReactElem )} - {normalizedMessage.attachments && } + {!!attachments && } {oembedEnabled && !!normalizedMessage.urls?.length && } diff --git a/apps/meteor/client/components/withErrorBoundary.tsx b/apps/meteor/client/components/withErrorBoundary.tsx new file mode 100644 index 0000000000000..598de5186bb41 --- /dev/null +++ b/apps/meteor/client/components/withErrorBoundary.tsx @@ -0,0 +1,19 @@ +import type { ComponentType, ReactNode, ComponentProps } from 'react'; +import React from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; + +function withErrorBoundary(Component: ComponentType, fallback: ReactNode = null) { + const WrappedComponent = function (props: ComponentProps) { + return ( + {fallback}}> + + + ); + }; + + WrappedComponent.displayName = `withErrorBoundary(${Component.displayName ?? Component.name ?? 'Component'})`; + + return WrappedComponent; +} + +export { withErrorBoundary }; diff --git a/apps/meteor/client/contexts/AppsContext.tsx b/apps/meteor/client/contexts/AppsContext.tsx index 1fbf7399d6482..2be8e74c2d672 100644 --- a/apps/meteor/client/contexts/AppsContext.tsx +++ b/apps/meteor/client/contexts/AppsContext.tsx @@ -4,7 +4,7 @@ import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import type { Serialized } from '@rocket.chat/core-typings'; import { createContext } from 'react'; -import type { IAppExternalURL, ICategory } from '../../ee/client/apps/@types/IOrchestrator'; +import type { IAppExternalURL, ICategory } from '../apps/@types/IOrchestrator'; import type { AsyncState } from '../lib/asyncState'; import { AsyncStatePhase } from '../lib/asyncState'; import type { App } from '../views/marketplace/types'; diff --git a/apps/meteor/client/contexts/CallContext.ts b/apps/meteor/client/contexts/CallContext.ts index ce9af784fb0df..316f5690cf69e 100644 --- a/apps/meteor/client/contexts/CallContext.ts +++ b/apps/meteor/client/contexts/CallContext.ts @@ -3,7 +3,7 @@ import type { Device } from '@rocket.chat/ui-contexts'; import { createContext, useContext, useMemo } from 'react'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; -import { useHasLicenseModule } from '../../ee/client/hooks/useHasLicenseModule'; +import { useHasLicenseModule } from '../hooks/useHasLicenseModule'; import type { VoIPUser } from '../lib/voip/VoIPUser'; export type CallContextValue = CallContextDisabled | CallContextReady | CallContextError | CallContextEnabled; diff --git a/apps/meteor/client/definitions/cron.d.ts b/apps/meteor/client/definitions/cron.d.ts new file mode 100644 index 0000000000000..25c4be4f97a7c --- /dev/null +++ b/apps/meteor/client/definitions/cron.d.ts @@ -0,0 +1,3 @@ +declare module 'cron' { + export declare function sendAt(precision: string): Moment; +} diff --git a/apps/meteor/ee/client/ecdh.ts b/apps/meteor/client/ecdh.ts similarity index 97% rename from apps/meteor/ee/client/ecdh.ts rename to apps/meteor/client/ecdh.ts index bbec5c19e0ad5..1e49f887e62b4 100644 --- a/apps/meteor/ee/client/ecdh.ts +++ b/apps/meteor/client/ecdh.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; -import { sdk } from '../../app/utils/client/lib/SDKClient'; import type { ClientSession } from '../app/ecdh/client/ClientSession'; +import { sdk } from '../app/utils/client/lib/SDKClient'; let resolveSession: (value: ClientSession | void) => void; const sessionPromise = new Promise((resolve) => { diff --git a/apps/meteor/ee/client/hooks/quickActions/useOnHoldChatQuickAction.ts b/apps/meteor/client/hooks/quickActions/useOnHoldChatQuickAction.ts similarity index 91% rename from apps/meteor/ee/client/hooks/quickActions/useOnHoldChatQuickAction.ts rename to apps/meteor/client/hooks/quickActions/useOnHoldChatQuickAction.ts index 7a3f5bbd51f2f..92c7ddf4c1c3d 100644 --- a/apps/meteor/ee/client/hooks/quickActions/useOnHoldChatQuickAction.ts +++ b/apps/meteor/client/hooks/quickActions/useOnHoldChatQuickAction.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react'; -import { QuickActionsEnum, type QuickActionsActionConfig } from '../../../../client/views/room/lib/quickActions'; +import { QuickActionsEnum, type QuickActionsActionConfig } from '../../views/room/lib/quickActions'; import { useHasLicenseModule } from '../useHasLicenseModule'; export const useOnHoldChatQuickAction = (): QuickActionsActionConfig | undefined => { diff --git a/apps/meteor/ee/client/hooks/roomActions/useCallsRoomAction.ts b/apps/meteor/client/hooks/roomActions/useCallsRoomAction.ts similarity index 74% rename from apps/meteor/ee/client/hooks/roomActions/useCallsRoomAction.ts rename to apps/meteor/client/hooks/roomActions/useCallsRoomAction.ts index 7c21ac9a47ff2..31be4b2300ee4 100644 --- a/apps/meteor/ee/client/hooks/roomActions/useCallsRoomAction.ts +++ b/apps/meteor/client/hooks/roomActions/useCallsRoomAction.ts @@ -2,11 +2,11 @@ import { isRoomFederated } from '@rocket.chat/core-typings'; import { lazy, useContext, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { RoomContext } from '../../../../client/views/room/contexts/RoomContext'; -import type { RoomToolboxActionConfig } from '../../../../client/views/room/contexts/RoomToolboxContext'; +import { RoomContext } from '../../views/room/contexts/RoomContext'; +import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext'; import { useHasLicenseModule } from '../useHasLicenseModule'; -const VideoConfList = lazy(() => import('../../../../client/views/room/contextualBar/VideoConference/VideoConfList')); +const VideoConfList = lazy(() => import('../../views/room/contextualBar/VideoConference/VideoConfList')); export const useCallsRoomAction = () => { const licensed = useHasLicenseModule('videoconference-enterprise') === true; diff --git a/apps/meteor/ee/client/hooks/roomActions/useCannedResponsesRoomAction.ts b/apps/meteor/client/hooks/roomActions/useCannedResponsesRoomAction.ts similarity index 88% rename from apps/meteor/ee/client/hooks/roomActions/useCannedResponsesRoomAction.ts rename to apps/meteor/client/hooks/roomActions/useCannedResponsesRoomAction.ts index 432a991697394..e5f44c67fbef9 100644 --- a/apps/meteor/ee/client/hooks/roomActions/useCannedResponsesRoomAction.ts +++ b/apps/meteor/client/hooks/roomActions/useCannedResponsesRoomAction.ts @@ -1,7 +1,7 @@ import { useSetting } from '@rocket.chat/ui-contexts'; import { lazy, useMemo } from 'react'; -import type { RoomToolboxActionConfig } from '../../../../client/views/room/contexts/RoomToolboxContext'; +import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext'; import { useHasLicenseModule } from '../useHasLicenseModule'; const CannedResponse = lazy(() => import('../../omnichannel/cannedResponses/contextualBar/CannedResponse/WrapCannedResponseList')); diff --git a/apps/meteor/client/hooks/roomActions/useE2EERoomAction.spec.ts b/apps/meteor/client/hooks/roomActions/useE2EERoomAction.spec.ts new file mode 100644 index 0000000000000..eb0cbe5b24f47 --- /dev/null +++ b/apps/meteor/client/hooks/roomActions/useE2EERoomAction.spec.ts @@ -0,0 +1,122 @@ +import { useSetting, usePermission, useEndpoint } from '@rocket.chat/ui-contexts'; +import { act, renderHook } from '@testing-library/react-hooks'; + +import { E2EEState } from '../../../app/e2e/client/E2EEState'; +import { e2e } from '../../../app/e2e/client/rocketchat.e2e'; +import { OtrRoomState } from '../../../app/otr/lib/OtrRoomState'; +import { dispatchToastMessage } from '../../lib/toast'; +import { useRoom, useRoomSubscription } from '../../views/room/contexts/RoomContext'; +import { useE2EEState } from '../../views/room/hooks/useE2EEState'; +import { useOTR } from '../useOTR'; +import { useE2EERoomAction } from './useE2EERoomAction'; + +jest.mock('@rocket.chat/ui-contexts', () => ({ + useSetting: jest.fn(), + usePermission: jest.fn(), + useEndpoint: jest.fn(), +})); + +jest.mock('../../lib/toast', () => ({ + dispatchToastMessage: jest.fn(), +})); + +jest.mock('../../views/room/contexts/RoomContext', () => ({ + useRoom: jest.fn(), + useRoomSubscription: jest.fn(), +})); + +jest.mock('../useOTR', () => ({ + useOTR: jest.fn(), +})); + +jest.mock('../../../app/e2e/client/rocketchat.e2e', () => ({ + e2e: { + isReady: jest.fn(), + }, +})); + +jest.mock('../../views/room/hooks/useE2EEState', () => ({ + useE2EEState: jest.fn(), +})); + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +jest.mock('meteor/tracker', () => ({ + Tracker: { + autorun: jest.fn(), + }, +})); + +describe('useE2EERoomAction', () => { + const mockRoom = { _id: 'roomId', encrypted: false, t: 'd', name: 'Test Room' }; + const mockSubscription = { autoTranslate: false }; + + beforeEach(() => { + (useSetting as jest.Mock).mockReturnValue(true); + (useRoom as jest.Mock).mockReturnValue(mockRoom); + (useRoomSubscription as jest.Mock).mockReturnValue(mockSubscription); + (useE2EEState as jest.Mock).mockReturnValue(E2EEState.READY); + (usePermission as jest.Mock).mockReturnValue(true); + (useEndpoint as jest.Mock).mockReturnValue(jest.fn().mockResolvedValue({ success: true })); + (e2e.isReady as jest.Mock).mockReturnValue(true); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should dispatch error toast message when otrState is ESTABLISHED', async () => { + (useOTR as jest.Mock).mockReturnValue({ otrState: OtrRoomState.ESTABLISHED }); + + const { result } = renderHook(() => useE2EERoomAction()); + + await act(async () => { + await result?.current?.action?.(); + }); + + expect(dispatchToastMessage).toHaveBeenCalledWith({ type: 'error', message: 'E2EE_not_available_OTR' }); + }); + + it('should dispatch error toast message when otrState is ESTABLISHING', async () => { + (useOTR as jest.Mock).mockReturnValue({ otrState: OtrRoomState.ESTABLISHING }); + + const { result } = renderHook(() => useE2EERoomAction()); + + await act(async () => { + await result?.current?.action?.(); + }); + + expect(dispatchToastMessage).toHaveBeenCalledWith({ type: 'error', message: 'E2EE_not_available_OTR' }); + }); + + it('should dispatch error toast message when otrState is REQUESTED', async () => { + (useOTR as jest.Mock).mockReturnValue({ otrState: OtrRoomState.REQUESTED }); + + const { result } = renderHook(() => useE2EERoomAction()); + + await act(async () => { + await result?.current?.action?.(); + }); + + expect(dispatchToastMessage).toHaveBeenCalledWith({ type: 'error', message: 'E2EE_not_available_OTR' }); + }); + + it('should dispatch success toast message when encryption is enabled', async () => { + (useOTR as jest.Mock).mockReturnValue({ otrState: OtrRoomState.NOT_STARTED }); + + const { result } = renderHook(() => useE2EERoomAction()); + + await act(async () => { + await result?.current?.action?.(); + }); + + expect(dispatchToastMessage).toHaveBeenCalledWith({ + type: 'success', + message: 'E2E_Encryption_enabled_for_room', + }); + }); +}); diff --git a/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts b/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts index 73b0f34836e17..bed73ab45c6b7 100644 --- a/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts +++ b/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts @@ -1,29 +1,40 @@ import { isRoomFederated } from '@rocket.chat/core-typings'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useSetting, usePermission, useEndpoint } from '@rocket.chat/ui-contexts'; -import { useCallback, useMemo } from 'react'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { e2e } from '../../../app/e2e/client/rocketchat.e2e'; +import { E2EEState } from '../../../app/e2e/client/E2EEState'; +import { OtrRoomState } from '../../../app/otr/lib/OtrRoomState'; import { dispatchToastMessage } from '../../lib/toast'; import { useRoom, useRoomSubscription } from '../../views/room/contexts/RoomContext'; import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext'; -import { useReactiveValue } from '../useReactiveValue'; +import { useE2EEState } from '../../views/room/hooks/useE2EEState'; +import { useOTR } from '../useOTR'; export const useE2EERoomAction = () => { const enabled = useSetting('E2E_Enable', false); const room = useRoom(); const subscription = useRoomSubscription(); - const readyToEncrypt = useReactiveValue(useCallback(() => e2e.isReady(), [])) || room.encrypted; + const e2eeState = useE2EEState(); + const isE2EEReady = e2eeState === E2EEState.READY || e2eeState === E2EEState.SAVE_PASSWORD; + const readyToEncrypt = isE2EEReady || room.encrypted; const permittedToToggleEncryption = usePermission('toggle-room-e2e-encryption', room._id); const permittedToEditRoom = usePermission('edit-room', room._id); const permitted = (room.t === 'd' || (permittedToEditRoom && permittedToToggleEncryption)) && readyToEncrypt; const federated = isRoomFederated(room); const { t } = useTranslation(); + const { otrState } = useOTR(); const toggleE2E = useEndpoint('POST', '/v1/rooms.saveRoomSettings'); - const action = useMutableCallback(async () => { + const action = useEffectEvent(async () => { + if (otrState === OtrRoomState.ESTABLISHED || otrState === OtrRoomState.ESTABLISHING || otrState === OtrRoomState.REQUESTED) { + dispatchToastMessage({ type: 'error', message: t('E2EE_not_available_OTR') }); + + return; + } + const { success } = await toggleE2E({ rid: room._id, encrypted: !room.encrypted }); if (!success) { return; diff --git a/apps/meteor/ee/client/hooks/roomActions/useGameCenterRoomAction.ts b/apps/meteor/client/hooks/roomActions/useGameCenterRoomAction.ts similarity index 86% rename from apps/meteor/ee/client/hooks/roomActions/useGameCenterRoomAction.ts rename to apps/meteor/client/hooks/roomActions/useGameCenterRoomAction.ts index 66a59d7fe81c2..f08e0fe03d0b9 100644 --- a/apps/meteor/ee/client/hooks/roomActions/useGameCenterRoomAction.ts +++ b/apps/meteor/client/hooks/roomActions/useGameCenterRoomAction.ts @@ -1,7 +1,7 @@ import { lazy, useMemo } from 'react'; -import type { RoomToolboxActionConfig } from '../../../../client/views/room/contexts/RoomToolboxContext'; import { useExternalComponentsQuery } from '../../apps/gameCenter/hooks/useExternalComponentsQuery'; +import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext'; const GameCenter = lazy(() => import('../../apps/gameCenter/GameCenter')); diff --git a/apps/meteor/ee/client/hooks/useDeviceLogout.tsx b/apps/meteor/client/hooks/useDeviceLogout.tsx similarity index 92% rename from apps/meteor/ee/client/hooks/useDeviceLogout.tsx rename to apps/meteor/client/hooks/useDeviceLogout.tsx index 0cc0df4630ac2..c9c089aa2ce0c 100644 --- a/apps/meteor/ee/client/hooks/useDeviceLogout.tsx +++ b/apps/meteor/client/hooks/useDeviceLogout.tsx @@ -1,8 +1,8 @@ import { useSetModal, useTranslation, useToastMessageDispatch, useRoute, useRouteParameter } from '@rocket.chat/ui-contexts'; import React, { useCallback } from 'react'; -import GenericModal from '../../../client/components/GenericModal'; -import { useEndpointAction } from '../../../client/hooks/useEndpointAction'; +import GenericModal from '../components/GenericModal'; +import { useEndpointAction } from './useEndpointAction'; export const useDeviceLogout = ( sessionId: string, diff --git a/apps/meteor/ee/client/hooks/useDevicesMenuOption.tsx b/apps/meteor/client/hooks/useDevicesMenuOption.tsx similarity index 100% rename from apps/meteor/ee/client/hooks/useDevicesMenuOption.tsx rename to apps/meteor/client/hooks/useDevicesMenuOption.tsx diff --git a/apps/meteor/client/hooks/useDialModal.tsx b/apps/meteor/client/hooks/useDialModal.tsx index ead4c9c070c7e..ef513e5212834 100644 --- a/apps/meteor/client/hooks/useDialModal.tsx +++ b/apps/meteor/client/hooks/useDialModal.tsx @@ -4,7 +4,7 @@ import React, { Suspense, lazy, useCallback, useMemo } from 'react'; import { useIsVoipEnterprise } from '../contexts/CallContext'; import { dispatchToastMessage } from '../lib/toast'; -const DialPadModal = lazy(() => import('../../ee/client/voip/modal/DialPad/DialPadModal')); +const DialPadModal = lazy(() => import('../voip/modal/DialPad/DialPadModal')); type DialModalProps = { initialValue?: string; diff --git a/apps/meteor/client/hooks/useDownloadFromServiceWorker.ts b/apps/meteor/client/hooks/useDownloadFromServiceWorker.ts new file mode 100644 index 0000000000000..5ab7f804fec78 --- /dev/null +++ b/apps/meteor/client/hooks/useDownloadFromServiceWorker.ts @@ -0,0 +1,56 @@ +import { Emitter } from '@rocket.chat/emitter'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useCallback, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { downloadAs } from '../lib/download'; + +const ee = new Emitter>(); + +navigator.serviceWorker.addEventListener('message', (event) => { + if (event.data.type === 'attachment-download-result') { + const { result } = event.data as { result: ArrayBuffer; id: string }; + + ee.emit(event.data.id, { result, id: event.data.id }); + } +}); + +export const registerDownloadForUid = (uid: string, t: ReturnType['t'], title?: string) => { + ee.once(uid, ({ result }) => { + downloadAs({ data: [new Blob([result])] }, title ?? t('Download')); + }); +}; + +export const forAttachmentDownload = (uid: string, href: string, controller?: ServiceWorker | null) => { + if (!controller) { + controller = navigator.serviceWorker.controller; + } + controller?.postMessage({ + type: 'attachment-download', + url: href, + id: uid, + }); +}; + +export const useDownloadFromServiceWorker = (href: string, title?: string) => { + const { controller } = navigator.serviceWorker; + + const uid = useUniqueId(); + + const { t } = useTranslation(); + + useEffect(() => registerDownloadForUid(uid, t, title), [title, t, uid]); + + return { + disabled: !controller, + onContextMenu: useCallback((e) => e.preventDefault(), []), + onClick: useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + + forAttachmentDownload(uid, href, controller); + }, + [href, uid, controller], + ), + }; +}; diff --git a/apps/meteor/ee/client/hooks/useHasLicenseModule.ts b/apps/meteor/client/hooks/useHasLicenseModule.ts similarity index 81% rename from apps/meteor/ee/client/hooks/useHasLicenseModule.ts rename to apps/meteor/client/hooks/useHasLicenseModule.ts index f2d75ab237464..ea40807308846 100644 --- a/apps/meteor/ee/client/hooks/useHasLicenseModule.ts +++ b/apps/meteor/client/hooks/useHasLicenseModule.ts @@ -1,6 +1,6 @@ import type { LicenseModule } from '@rocket.chat/core-typings'; -import { useLicenseBase } from '../../../client/hooks/useLicense'; +import { useLicenseBase } from './useLicense'; export const useHasLicenseModule = (licenseName: LicenseModule): 'loading' | boolean => { return ( diff --git a/apps/meteor/client/hooks/useHighlightedCode.ts b/apps/meteor/client/hooks/useHighlightedCode.ts index 43c4a5e6ea584..4e3405ca30342 100644 --- a/apps/meteor/client/hooks/useHighlightedCode.ts +++ b/apps/meteor/client/hooks/useHighlightedCode.ts @@ -1,7 +1,19 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; -import hljs from '../../app/markdown/lib/hljs'; +import hljs, { register } from '../../app/markdown/lib/hljs'; export function useHighlightedCode(language: string, text: string): string { - return useMemo(() => hljs.highlight(language, text).value, [language, text]); + const t = useTranslation(); + const { isLoading } = useQuery(['register-highlight-language', language], async () => { + try { + await register(language); + return true; + } catch (error) { + console.error('Not possible to register the provided language'); + } + }); + + return useMemo(() => (isLoading ? t('Loading') : hljs.highlight(language, text).value), [isLoading, language, text, t]); } diff --git a/apps/meteor/client/hooks/useLicenseLimitsByBehavior.ts b/apps/meteor/client/hooks/useLicenseLimitsByBehavior.ts new file mode 100644 index 0000000000000..7ccea2dd4bd4e --- /dev/null +++ b/apps/meteor/client/hooks/useLicenseLimitsByBehavior.ts @@ -0,0 +1,63 @@ +import type { LicenseBehavior, LicenseLimitKind } from '@rocket.chat/core-typings'; +import { validateWarnLimit } from '@rocket.chat/license/src/validation/validateLimit'; + +import { useLicense } from './useLicense'; + +type LicenseLimitsByBehavior = Record; + +export const useLicenseLimitsByBehavior = () => { + const result = useLicense({ loadValues: true }); + + if (result.isLoading || result.isError) { + return null; + } + + const { license, limits } = result.data; + + if (!license || !limits) { + return null; + } + + const keyLimits = Object.keys(limits) as Array; + + // Get the rule with the highest limit that applies to this key + const rules = keyLimits + .map((key) => { + const rule = license.limits[key] + ?.filter((limit) => validateWarnLimit(limit.max, limits[key].value ?? 0, limit.behavior)) + .reduce<{ max: number; behavior: LicenseBehavior } | null>( + (maxLimit, currentLimit) => (!maxLimit || currentLimit.max > maxLimit.max ? currentLimit : maxLimit), + null, + ); + + if (!rule) { + return undefined; + } + + if (rule.max === 0) { + return undefined; + } + + if (rule.max === -1) { + return undefined; + } + + return [key, rule.behavior]; + }) + .filter(Boolean) as Array<[keyof typeof limits, LicenseBehavior]>; + + if (!rules.length) { + return null; + } + + // Group by behavior + return rules.reduce((acc, [key, behavior]) => { + if (!acc[behavior]) { + acc[behavior] = []; + } + + acc[behavior].push(key); + + return acc; + }, {} as LicenseLimitsByBehavior); +}; diff --git a/apps/meteor/client/hooks/useOTR.spec.tsx b/apps/meteor/client/hooks/useOTR.spec.tsx new file mode 100644 index 0000000000000..0206d96ca176e --- /dev/null +++ b/apps/meteor/client/hooks/useOTR.spec.tsx @@ -0,0 +1,70 @@ +import { useUserId } from '@rocket.chat/ui-contexts'; +import { renderHook } from '@testing-library/react-hooks'; + +import OTR from '../../app/otr/client/OTR'; +import { OtrRoomState } from '../../app/otr/lib/OtrRoomState'; +import { useRoom } from '../views/room/contexts/RoomContext'; +import { useOTR } from './useOTR'; + +jest.mock('@rocket.chat/ui-contexts', () => ({ + useUserId: jest.fn(), +})); + +jest.mock('../views/room/contexts/RoomContext', () => ({ + useRoom: jest.fn(), +})); + +jest.mock('../../app/otr/client/OTR', () => ({ + getInstanceByRoomId: jest.fn(), +})); + +jest.mock('./useReactiveValue', () => ({ + useReactiveValue: jest.fn((fn) => fn()), +})); + +describe('useOTR', () => { + it('should return error state when user ID is not available', () => { + (useUserId as jest.Mock).mockReturnValue(undefined); + (useRoom as jest.Mock).mockReturnValue({ _id: 'roomId' }); + + const { result } = renderHook(() => useOTR()); + + expect(result.current.otr).toBeUndefined(); + expect(result.current.otrState).toBe(OtrRoomState.ERROR); + }); + + it('should return error state when room ID is not available', () => { + (useUserId as jest.Mock).mockReturnValue('userId'); + (useRoom as jest.Mock).mockReturnValue(undefined); + + const { result } = renderHook(() => useOTR()); + + expect(result.current.otr).toBeUndefined(); + expect(result.current.otrState).toBe(OtrRoomState.ERROR); + }); + + it('should return error state when OTR instance is not available', () => { + (useUserId as jest.Mock).mockReturnValue('userId'); + (useRoom as jest.Mock).mockReturnValue({ _id: 'roomId' }); + (OTR.getInstanceByRoomId as jest.Mock).mockReturnValue(undefined); + + const { result } = renderHook(() => useOTR()); + + expect(result.current.otr).toBeUndefined(); + expect(result.current.otrState).toBe(OtrRoomState.ERROR); + }); + + it('should return the correct OTR instance and state when available', () => { + const mockOtrInstance = { + getState: jest.fn().mockReturnValue(OtrRoomState.NOT_STARTED), + }; + (useUserId as jest.Mock).mockReturnValue('userId'); + (useRoom as jest.Mock).mockReturnValue({ _id: 'roomId' }); + (OTR.getInstanceByRoomId as jest.Mock).mockReturnValue(mockOtrInstance); + + const { result } = renderHook(() => useOTR()); + + expect(result.current.otr).toBe(mockOtrInstance); + expect(result.current.otrState).toBe(OtrRoomState.NOT_STARTED); + }); +}); diff --git a/apps/meteor/client/hooks/useOTR.ts b/apps/meteor/client/hooks/useOTR.ts new file mode 100644 index 0000000000000..65f9004cf323d --- /dev/null +++ b/apps/meteor/client/hooks/useOTR.ts @@ -0,0 +1,28 @@ +import { useUserId } from '@rocket.chat/ui-contexts'; +import { useMemo, useCallback } from 'react'; + +import OTR from '../../app/otr/client/OTR'; +import type { OTRRoom } from '../../app/otr/client/OTRRoom'; +import { OtrRoomState } from '../../app/otr/lib/OtrRoomState'; +import { useRoom } from '../views/room/contexts/RoomContext'; +import { useReactiveValue } from './useReactiveValue'; + +export const useOTR = (): { otr: OTRRoom | undefined; otrState: OtrRoomState } => { + const uid = useUserId(); + const room = useRoom(); + + const otr = useMemo(() => { + if (!uid || !room) { + return; + } + + return OTR.getInstanceByRoomId(uid, room._id); + }, [uid, room]); + + const otrState = useReactiveValue(useCallback(() => (otr ? otr.getState() : OtrRoomState.ERROR), [otr])); + + return { + otr, + otrState, + }; +}; diff --git a/apps/meteor/ee/client/hooks/useOutboundDialer.ts b/apps/meteor/client/hooks/useOutboundDialer.ts similarity index 78% rename from apps/meteor/ee/client/hooks/useOutboundDialer.ts rename to apps/meteor/client/hooks/useOutboundDialer.ts index fc7ae8a8522e7..2ffdc9b149694 100644 --- a/apps/meteor/ee/client/hooks/useOutboundDialer.ts +++ b/apps/meteor/client/hooks/useOutboundDialer.ts @@ -1,4 +1,4 @@ -import { useCallClient, useIsVoipEnterprise } from '../../../client/contexts/CallContext'; +import { useCallClient, useIsVoipEnterprise } from '../contexts/CallContext'; import { EEVoipClient } from '../lib/voip/EEVoipClient'; export const useOutboundDialer = (): EEVoipClient | null => { diff --git a/apps/meteor/client/hooks/usePruneWarningMessage.spec.ts b/apps/meteor/client/hooks/usePruneWarningMessage.spec.ts new file mode 100644 index 0000000000000..1ea3825c61ec9 --- /dev/null +++ b/apps/meteor/client/hooks/usePruneWarningMessage.spec.ts @@ -0,0 +1,190 @@ +import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; +import { renderHook } from '@testing-library/react-hooks'; + +import { createRenteionPolicySettingsMock as createMock } from '../../tests/mocks/client/mockRetentionPolicySettings'; +import { createFakeRoom } from '../../tests/mocks/data'; +import { setDate } from '../../tests/mocks/mockDate'; +import { usePruneWarningMessage } from './usePruneWarningMessage'; + +jest.useFakeTimers(); + +const getRetentionRoomProps = (props: Partial = {}) => { + return { + retention: { + enabled: true, + overrideGlobal: true, + maxAge: 30, + filesOnly: false, + excludePinned: false, + ignoreThreads: false, + ...props, + }, + }; +}; + +describe('usePruneWarningMessage hook', () => { + describe('Cron timer and precision', () => { + it('Should update the message after the nextRunDate has passaed', async () => { + setDate(); + const fakeRoom = createFakeRoom({ t: 'c' }); + const { result } = renderHook(() => usePruneWarningMessage(fakeRoom), { + wrapper: createMock({ + appliesToChannels: true, + TTLChannels: 60000, + }), + }); + expect(result.current).toEqual('a minute June 1, 2024, 12:30 AM'); + jest.advanceTimersByTime(31 * 60 * 1000); + expect(result.current).toEqual('a minute June 1, 2024, 1:00 AM'); + }); + + it('Should return the default warning with precision set to every_hour', () => { + const fakeRoom = createFakeRoom({ t: 'c' }); + setDate(); + const { result } = renderHook(() => usePruneWarningMessage(fakeRoom), { + wrapper: createMock({ + appliesToChannels: true, + TTLChannels: 60000, + precision: '1', + }), + }); + expect(result.current).toEqual('a minute June 1, 2024, 1:00 AM'); + }); + + it('Should return the default warning with precision set to every_six_hours', () => { + const fakeRoom = createFakeRoom({ t: 'c' }); + setDate(); + const { result } = renderHook(() => usePruneWarningMessage(fakeRoom), { + wrapper: createMock({ + appliesToChannels: true, + TTLChannels: 60000, + precision: '2', + }), + }); + expect(result.current).toEqual('a minute June 1, 2024, 6:00 AM'); + }); + + it('Should return the default warning with precision set to every_day', () => { + const fakeRoom = createFakeRoom({ t: 'c' }); + setDate(); + const { result } = renderHook(() => usePruneWarningMessage(fakeRoom), { + wrapper: createMock({ + appliesToChannels: true, + TTLChannels: 60000, + precision: '3', + }), + }); + expect(result.current).toEqual('a minute June 2, 2024, 12:00 AM'); + }); + + it('Should return the default warning with advanced precision', () => { + const fakeRoom = createFakeRoom({ t: 'c' }); + setDate(); + const { result } = renderHook(() => usePruneWarningMessage(fakeRoom), { + wrapper: createMock({ + appliesToChannels: true, + TTLChannels: 60000, + advancedPrecision: true, + advancedPrecisionCron: '0 0 1 */1 *', + }), + }); + expect(result.current).toEqual('a minute July 1, 2024, 12:00 AM'); + }); + }); + + describe('No override', () => { + it('Should return the default warning', () => { + const fakeRoom = createFakeRoom({ t: 'c' }); + setDate(); + const { result } = renderHook(() => usePruneWarningMessage(fakeRoom), { + wrapper: createMock({ + appliesToChannels: true, + TTLChannels: 60000, + }), + }); + expect(result.current).toEqual('a minute June 1, 2024, 12:30 AM'); + }); + + it('Should return the unpinned messages warning', () => { + const fakeRoom = createFakeRoom({ t: 'c' }); + setDate(); + const { result } = renderHook(() => usePruneWarningMessage(fakeRoom), { + wrapper: createMock({ + appliesToChannels: true, + TTLChannels: 60000, + doNotPrunePinned: true, + }), + }); + expect(result.current).toEqual('Unpinned a minute June 1, 2024, 12:30 AM'); + }); + + it('Should return the files only warning', () => { + const fakeRoom = createFakeRoom({ t: 'c' }); + setDate(); + + const { result } = renderHook(() => usePruneWarningMessage(fakeRoom), { + wrapper: createMock({ + appliesToChannels: true, + TTLChannels: 60000, + filesOnly: true, + }), + }); + expect(result.current).toEqual('FilesOnly a minute June 1, 2024, 12:30 AM'); + }); + + it('Should return the unpinned files only warning', () => { + const fakeRoom = createFakeRoom({ t: 'c' }); + setDate(); + + const { result } = renderHook(() => usePruneWarningMessage(fakeRoom), { + wrapper: createMock({ + appliesToChannels: true, + TTLChannels: 60000, + filesOnly: true, + doNotPrunePinned: true, + }), + }); + expect(result.current).toEqual('UnpinnedFilesOnly a minute June 1, 2024, 12:30 AM'); + }); + }); + + describe('Overriden', () => { + it('Should return the default warning', () => { + const fakeRoom = createFakeRoom({ t: 'p', ...getRetentionRoomProps() }); + setDate(); + const { result } = renderHook(() => usePruneWarningMessage(fakeRoom), { + wrapper: createMock(), + }); + expect(result.current).toEqual('30 days June 1, 2024, 12:30 AM'); + }); + + it('Should return the unpinned messages warning', () => { + const fakeRoom = createFakeRoom({ t: 'p', ...getRetentionRoomProps({ excludePinned: true }) }); + setDate(); + const { result } = renderHook(() => usePruneWarningMessage(fakeRoom), { + wrapper: createMock(), + }); + expect(result.current).toEqual('Unpinned 30 days June 1, 2024, 12:30 AM'); + }); + + it('Should return the files only warning', () => { + const fakeRoom = createFakeRoom({ t: 'p', ...getRetentionRoomProps({ filesOnly: true }) }); + setDate(); + + const { result } = renderHook(() => usePruneWarningMessage(fakeRoom), { + wrapper: createMock(), + }); + expect(result.current).toEqual('FilesOnly 30 days June 1, 2024, 12:30 AM'); + }); + + it('Should return the unpinned files only warning', () => { + const fakeRoom = createFakeRoom({ t: 'p', ...getRetentionRoomProps({ excludePinned: true, filesOnly: true }) }); + setDate(); + + const { result } = renderHook(() => usePruneWarningMessage(fakeRoom), { + wrapper: createMock(), + }); + expect(result.current).toEqual('UnpinnedFilesOnly 30 days June 1, 2024, 12:30 AM'); + }); + }); +}); diff --git a/apps/meteor/client/hooks/usePruneWarningMessage.ts b/apps/meteor/client/hooks/usePruneWarningMessage.ts new file mode 100644 index 0000000000000..bb1eec5a59891 --- /dev/null +++ b/apps/meteor/client/hooks/usePruneWarningMessage.ts @@ -0,0 +1,105 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { useSafely } from '@rocket.chat/fuselage-hooks'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import { useSetting, useTranslation, useLanguage } from '@rocket.chat/ui-contexts'; +import { sendAt } from 'cron'; +import intlFormat from 'date-fns/intlFormat'; +import { useEffect, useState } from 'react'; + +import { getCronAdvancedTimerFromPrecisionSetting } from '../../lib/getCronAdvancedTimerFromPrecisionSetting'; +import { useRetentionPolicy } from '../views/room/hooks/useRetentionPolicy'; +import { useFormattedRelativeTime } from './useFormattedRelativeTime'; + +const getMessage = ({ filesOnly, excludePinned }: { filesOnly: boolean; excludePinned: boolean }): TranslationKey => { + if (filesOnly) { + return excludePinned + ? 'RetentionPolicy_RoomWarning_UnpinnedFilesOnly_NextRunDate' + : 'RetentionPolicy_RoomWarning_FilesOnly_NextRunDate'; + } + + return excludePinned ? 'RetentionPolicy_RoomWarning_Unpinned_NextRunDate' : 'RetentionPolicy_RoomWarning_NextRunDate'; +}; + +type CronPrecisionSetting = '0' | '1' | '2' | '3'; +const getNextRunDate = ({ + enableAdvancedCronTimer, + cronPrecision, + advancedCronTimer, +}: { + enableAdvancedCronTimer: boolean; + cronPrecision: CronPrecisionSetting; + advancedCronTimer: string; +}) => { + if (enableAdvancedCronTimer) { + return sendAt(advancedCronTimer); + } + + return sendAt(getCronAdvancedTimerFromPrecisionSetting(cronPrecision)); +}; + +const useNextRunDate = ({ + enableAdvancedCronTimer, + advancedCronTimer, + cronPrecision, +}: { + enableAdvancedCronTimer: boolean; + cronPrecision: CronPrecisionSetting; + advancedCronTimer: string; +}) => { + const [nextRunDate, setNextRunDate] = useSafely(useState(getNextRunDate({ enableAdvancedCronTimer, advancedCronTimer, cronPrecision }))); + const lang = useLanguage(); + + useEffect(() => { + const timeoutBetweenRunAndNow = nextRunDate.valueOf() - Date.now(); + + const timeout = setTimeout( + () => setNextRunDate(getNextRunDate({ enableAdvancedCronTimer, advancedCronTimer, cronPrecision })), + timeoutBetweenRunAndNow, + ); + + return () => clearTimeout(timeout); + }, [advancedCronTimer, cronPrecision, enableAdvancedCronTimer, nextRunDate, setNextRunDate]); + + return intlFormat( + new Date(nextRunDate.valueOf()), + { + localeMatcher: 'best fit', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + }, + { + locale: lang, + }, + ); +}; + +export const usePruneWarningMessage = (room: IRoom) => { + const retention = useRetentionPolicy(room); + if (!retention) { + throw new Error('usePruneWarningMessage - No room provided'); + } + + const { maxAge, filesOnly, excludePinned } = retention; + + const cronPrecision = String(useSetting('RetentionPolicy_Precision')) as CronPrecisionSetting; + + const t = useTranslation(); + + const enableAdvancedCronTimer = Boolean(useSetting('RetentionPolicy_Advanced_Precision')); + const advancedCronTimer = String(useSetting('RetentionPolicy_Advanced_Precision_Cron')); + + const message = getMessage({ filesOnly, excludePinned }); + + const nextRunDate = useNextRunDate({ + enableAdvancedCronTimer, + advancedCronTimer, + cronPrecision, + }); + + const maxAgeFormatted = useFormattedRelativeTime(maxAge); + + return t(message, { maxAge: maxAgeFormatted, nextRunDate }); +}; diff --git a/apps/meteor/ee/client/hooks/useTagsList.ts b/apps/meteor/client/hooks/useTagsList.ts similarity index 86% rename from apps/meteor/ee/client/hooks/useTagsList.ts rename to apps/meteor/client/hooks/useTagsList.ts index 41062d1b9dceb..3aa80b4e07b3a 100644 --- a/apps/meteor/ee/client/hooks/useTagsList.ts +++ b/apps/meteor/client/hooks/useTagsList.ts @@ -1,9 +1,9 @@ import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useCallback, useState } from 'react'; -import { useScrollableRecordList } from '../../../client/hooks/lists/useScrollableRecordList'; -import { useComponentDidUpdate } from '../../../client/hooks/useComponentDidUpdate'; -import { RecordList } from '../../../client/lib/lists/RecordList'; +import { RecordList } from '../lib/lists/RecordList'; +import { useScrollableRecordList } from './lists/useScrollableRecordList'; +import { useComponentDidUpdate } from './useComponentDidUpdate'; type TagsListOptions = { filter: string; diff --git a/apps/meteor/ee/client/hooks/useVoipClient.ts b/apps/meteor/client/hooks/useVoipClient.ts similarity index 91% rename from apps/meteor/ee/client/hooks/useVoipClient.ts rename to apps/meteor/client/hooks/useVoipClient.ts index 93048c50016a8..a9a941b1a0e16 100644 --- a/apps/meteor/ee/client/hooks/useVoipClient.ts +++ b/apps/meteor/client/hooks/useVoipClient.ts @@ -5,9 +5,9 @@ import { useUser, useSetting, useEndpoint, useStream } from '@rocket.chat/ui-con import { KJUR } from 'jsrsasign'; import { useEffect, useState } from 'react'; -import { VoIPUser } from '../../../client/lib/voip/VoIPUser'; -import { useWebRtcServers } from '../../../client/providers/CallProvider/hooks/useWebRtcServers'; import { EEVoipClient } from '../lib/voip/EEVoipClient'; +import { VoIPUser } from '../lib/voip/VoIPUser'; +import { useWebRtcServers } from '../providers/CallProvider/hooks/useWebRtcServers'; import { useHasLicenseModule } from './useHasLicenseModule'; type UseVoipClientResult = { @@ -39,13 +39,13 @@ export const useVoipClient = (): UseVoipClientResult => { const isEE = useHasLicenseModule('voip-enterprise'); const voipEnabled = settingVoipEnabled && voipConnectorEnabled; - useEffect( - () => - subscribeToNotifyLoggedIn(`voip.statuschanged`, (enabled: boolean): void => { + useEffect(() => { + if (user) { + return subscribeToNotifyLoggedIn(`voip.statuschanged`, (enabled: boolean): void => { setVoipConnectorEnabled(enabled); - }), - [setResult, setVoipConnectorEnabled, subscribeToNotifyLoggedIn], - ); + }); + } + }, [setResult, setVoipConnectorEnabled, subscribeToNotifyLoggedIn, user]); useEffect(() => { const uid = user?._id; diff --git a/apps/meteor/ee/client/hooks/useVoipFooterMenu.tsx b/apps/meteor/client/hooks/useVoipFooterMenu.tsx similarity index 100% rename from apps/meteor/ee/client/hooks/useVoipFooterMenu.tsx rename to apps/meteor/client/hooks/useVoipFooterMenu.tsx diff --git a/apps/meteor/client/importPackages.ts b/apps/meteor/client/importPackages.ts index ffc81c35a9532..ddc173e631168 100644 --- a/apps/meteor/client/importPackages.ts +++ b/apps/meteor/client/importPackages.ts @@ -2,6 +2,7 @@ import '../app/cors/client'; import '../app/apple/client'; import '../app/authorization/client'; import '../app/autotranslate/client'; +import '../app/canned-responses/client'; import '../app/custom-sounds/client'; import '../app/dolphin/client'; import '../app/drupal/client'; @@ -12,7 +13,9 @@ import '../app/file-upload/client'; import '../app/github-enterprise/client'; import '../app/gitlab/client'; import '../app/iframe-login/client'; +import '../app/license/client'; import '../app/lib/client'; +import '../app/livechat-enterprise/client'; import '../app/message-mark-as-unread/client'; import '../app/nextcloud/client'; import '../app/notifications/client'; diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index ceffab987a649..30dd5909ee03b 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -1,4 +1,4 @@ -import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, ISubscription, IE2EEMessage, IUpload } from '@rocket.chat/core-typings'; import type { IActionManager } from '@rocket.chat/ui-contexts'; import type { FormattingButton } from '../../../app/ui-message/client/messageBox/messageBoxFormatting'; @@ -100,7 +100,12 @@ export type UploadsAPI = { subscribe(callback: () => void): () => void; wipeFailedOnes(): void; cancel(id: Upload['id']): void; - send(file: File, { description, msg }: { description?: string; msg?: string }): Promise; + send( + file: File, + { description, msg, t, e2e }: { description?: string; msg?: string; t?: IMessage['t']; e2e?: IMessage['e2e'] }, + getContent?: (fileId: string, fileUrl: string) => Promise, + fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, + ): Promise; }; export type ChatAPI = { @@ -140,7 +145,15 @@ export type ChatAPI = { readonly flows: { readonly uploadFiles: (files: readonly File[], resetFileInput?: () => void) => Promise; - readonly sendMessage: ({ text, tshow }: { text: string; tshow?: boolean; previewUrls?: string[] }) => Promise; + readonly sendMessage: ({ + text, + tshow, + }: { + text: string; + tshow?: boolean; + previewUrls?: string[]; + isSlashCommandAllowed?: boolean; + }) => Promise; readonly processSlashCommand: (message: IMessage, userId: string | null) => Promise; readonly processTooLongMessage: (message: IMessage) => Promise; readonly processMessageEditing: ( diff --git a/apps/meteor/client/lib/chats/data.ts b/apps/meteor/client/lib/chats/data.ts index f2c049ad04b14..445f61f27226e 100644 --- a/apps/meteor/client/lib/chats/data.ts +++ b/apps/meteor/client/lib/chats/data.ts @@ -7,7 +7,6 @@ import { Messages, ChatRoom, ChatSubscription } from '../../../app/models/client import { settings } from '../../../app/settings/client'; import { MessageTypes } from '../../../app/ui-utils/client'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; -import { onClientBeforeSendMessage } from '../onClientBeforeSendMessage'; import { prependReplies } from '../utils/prependReplies'; import type { DataAPI } from './ChatAPI'; @@ -21,7 +20,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage const effectiveRID = originalMessage?.rid ?? rid; const effectiveTMID = originalMessage ? originalMessage.tmid : tmid; - return (await onClientBeforeSendMessage({ + return { _id: originalMessage?._id ?? Random.id(), rid: effectiveRID, ...(effectiveTMID && { @@ -29,7 +28,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage ...(sendToChannel && { tshow: sendToChannel }), }), msg, - })) as IMessage; + } as IMessage; }; const findMessageByID = async (mid: IMessage['_id']): Promise => @@ -95,7 +94,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage } const blockEditInMinutes = settings.get('Message_AllowEditing_BlockEditInMinutes') as number | undefined; - const bypassBlockTimeLimit = hasPermission('bypass-time-limit-edit-and-delete'); + const bypassBlockTimeLimit = hasPermission('bypass-time-limit-edit-and-delete', message.rid); const elapsedMinutes = moment().diff(message.ts, 'minutes'); if (!bypassBlockTimeLimit && elapsedMinutes && blockEditInMinutes && elapsedMinutes > blockEditInMinutes) { @@ -208,7 +207,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage } const blockDeleteInMinutes = settings.get('Message_AllowDeleting_BlockDeleteInMinutes') as number | undefined; - const bypassBlockTimeLimit = hasPermission('bypass-time-limit-edit-and-delete'); + const bypassBlockTimeLimit = hasPermission('bypass-time-limit-edit-and-delete', message.rid); const elapsedMinutes = moment().diff(message.ts, 'minutes'); const onTimeForDelete = bypassBlockTimeLimit || !blockDeleteInMinutes || !elapsedMinutes || elapsedMinutes <= blockDeleteInMinutes; diff --git a/apps/meteor/client/lib/chats/flows/processMessageEditing.ts b/apps/meteor/client/lib/chats/flows/processMessageEditing.ts index 029c56ff6d28b..d152f47f40965 100644 --- a/apps/meteor/client/lib/chats/flows/processMessageEditing.ts +++ b/apps/meteor/client/lib/chats/flows/processMessageEditing.ts @@ -17,7 +17,7 @@ export const processMessageEditing = async ( return false; } - if (!message.msg && !message.attachments?.length) { + if (!message.msg && !message.attachments?.length && !message.content) { return false; } diff --git a/apps/meteor/client/lib/chats/flows/sendMessage.ts b/apps/meteor/client/lib/chats/flows/sendMessage.ts index 62adbd80eb3e2..dbac8d808e4f6 100644 --- a/apps/meteor/client/lib/chats/flows/sendMessage.ts +++ b/apps/meteor/client/lib/chats/flows/sendMessage.ts @@ -3,6 +3,7 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { KonchatNotification } from '../../../../app/ui/client/lib/KonchatNotification'; import { sdk } from '../../../../app/utils/client/lib/SDKClient'; import { t } from '../../../../app/utils/lib/i18n'; +import { onClientBeforeSendMessage } from '../../onClientBeforeSendMessage'; import { dispatchToastMessage } from '../../toast'; import type { ChatAPI } from '../ChatAPI'; import { processMessageEditing } from './processMessageEditing'; @@ -10,7 +11,7 @@ import { processSetReaction } from './processSetReaction'; import { processSlashCommand } from './processSlashCommand'; import { processTooLongMessage } from './processTooLongMessage'; -const process = async (chat: ChatAPI, message: IMessage, previewUrls?: string[]): Promise => { +const process = async (chat: ChatAPI, message: IMessage, previewUrls?: string[], isSlashCommandAllowed?: boolean): Promise => { KonchatNotification.removeRoomNotification(message.rid); if (await processSetReaction(chat, message)) { @@ -21,11 +22,16 @@ const process = async (chat: ChatAPI, message: IMessage, previewUrls?: string[]) return; } - if (await processMessageEditing(chat, message, previewUrls)) { + if (isSlashCommandAllowed && (await processSlashCommand(chat, message))) { return; } - if (await processSlashCommand(chat, message)) { + message = (await onClientBeforeSendMessage(message)) as IMessage; + + // e2e should be a client property only + delete message.e2e; + + if (await processMessageEditing(chat, message, previewUrls)) { return; } @@ -34,7 +40,12 @@ const process = async (chat: ChatAPI, message: IMessage, previewUrls?: string[]) export const sendMessage = async ( chat: ChatAPI, - { text, tshow, previewUrls }: { text: string; tshow?: boolean; previewUrls?: string[] }, + { + text, + tshow, + previewUrls, + isSlashCommandAllowed, + }: { text: string; tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean }, ): Promise => { if (!(await chat.data.isSubscribedToRoom())) { try { @@ -62,8 +73,23 @@ export const sendMessage = async ( originalMessage: chat.currentEditing ? await chat.data.findMessageByID(chat.currentEditing.mid) : null, }); + if (chat.currentEditing) { + const originalMessage = await chat.data.findMessageByID(chat.currentEditing.mid); + + if ( + originalMessage?.t === 'e2e' && + originalMessage.attachments && + originalMessage.attachments.length > 0 && + originalMessage.attachments[0].description !== undefined + ) { + originalMessage.attachments[0].description = message.msg; + message.attachments = originalMessage.attachments; + message.msg = originalMessage.msg; + } + } + try { - await process(chat, message, previewUrls); + await process(chat, message, previewUrls, isSlashCommandAllowed); chat.composer?.dismissAllQuotedMessages(); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 82572aa2dbf5c..32e17da8ac6fa 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -1,11 +1,27 @@ +import type { IMessage, FileAttachmentProps, IE2EEMessage, IUpload } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; +import { e2e } from '../../../../app/e2e/client'; import { fileUploadIsValidContentType } from '../../../../app/utils/client'; +import { getFileExtension } from '../../../../lib/utils/getFileExtension'; import FileUploadModal from '../../../views/room/modals/FileUploadModal'; import { imperativeModal } from '../../imperativeModal'; import { prependReplies } from '../../utils/prependReplies'; import type { ChatAPI } from '../ChatAPI'; +const getHeightAndWidthFromDataUrl = (dataURL: string): Promise<{ height: number; width: number }> => { + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + resolve({ + height: img.height, + width: img.width, + }); + }; + img.src = dataURL; + }); +}; + export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFileInput?: () => void): Promise => { const replies = chat.composer?.quotedMessages.get() ?? []; @@ -15,6 +31,26 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi const queue = [...files]; + const uploadFile = ( + file: File, + extraData?: Pick & { description?: string }, + getContent?: (fileId: string, fileUrl: string) => Promise, + fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, + ) => { + chat.uploads.send( + file, + { + msg, + ...extraData, + }, + getContent, + fileContent, + ); + chat.composer?.clear(); + imperativeModal.close(); + uploadNextFile(); + }; + const uploadNextFile = (): void => { const file = queue.pop(); if (!file) { @@ -33,20 +69,128 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi imperativeModal.close(); uploadNextFile(); }, - onSubmit: (fileName: string, description?: string): void => { + onSubmit: async (fileName: string, description?: string): Promise => { Object.defineProperty(file, 'name', { writable: true, value: fileName, }); - chat.uploads.send(file, { - description, - msg, - }); - chat.composer?.clear(); - imperativeModal.close(); - uploadNextFile(); + + // encrypt attachment description + const e2eRoom = await e2e.getInstanceByRoomId(room._id); + + if (!e2eRoom) { + uploadFile(file, { description }); + return; + } + + const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg }); + + if (!shouldConvertSentMessages) { + uploadFile(file, { description }); + return; + } + + const encryptedFile = await e2eRoom.encryptFile(file); + + if (encryptedFile) { + const getContent = async (_id: string, fileUrl: string): Promise => { + const attachments = []; + + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description, + title_link: fileUrl, + title_link_download: true, + encryption: { + key: encryptedFile.key, + iv: encryptedFile.iv, + }, + hashes: { + sha256: encryptedFile.hash, + }, + }; + + if (/^image\/.+/.test(file.type)) { + const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(file)); + + attachments.push({ + ...attachment, + image_url: fileUrl, + image_type: file.type, + image_size: file.size, + ...(dimensions && { + image_dimensions: dimensions, + }), + }); + } else if (/^audio\/.+/.test(file.type)) { + attachments.push({ + ...attachment, + audio_url: fileUrl, + audio_type: file.type, + audio_size: file.size, + }); + } else if (/^video\/.+/.test(file.type)) { + attachments.push({ + ...attachment, + video_url: fileUrl, + video_type: file.type, + video_size: file.size, + }); + } else { + attachments.push({ + ...attachment, + size: file.size, + format: getFileExtension(file.name), + }); + } + + const files = [ + { + _id, + name: file.name, + type: file.type, + size: file.size, + // "format": "png" + }, + ]; + + return e2eRoom.encryptMessageContent({ + attachments, + files, + file: files[0], + }); + }; + + const fileContentData = { + type: file.type, + typeGroup: file.type.split('/')[0], + name: fileName, + encryption: { + key: encryptedFile.key, + iv: encryptedFile.iv, + }, + hashes: { + sha256: encryptedFile.hash, + }, + }; + + const fileContent = { + raw: fileContentData, + encrypted: await e2eRoom.encryptMessageContent(fileContentData), + }; + + uploadFile( + encryptedFile.file, + { + t: 'e2e', + }, + getContent, + fileContent, + ); + } }, - invalidContentType: !(file.type && fileUploadIsValidContentType(file.type)), + invalidContentType: !fileUploadIsValidContentType(file?.type), }, }); }; diff --git a/apps/meteor/client/lib/chats/uploads.ts b/apps/meteor/client/lib/chats/uploads.ts index 2a06877807edb..8411a840c1d9b 100644 --- a/apps/meteor/client/lib/chats/uploads.ts +++ b/apps/meteor/client/lib/chats/uploads.ts @@ -1,4 +1,4 @@ -import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, IE2EEMessage, IUpload } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { Random } from '@rocket.chat/random'; @@ -36,12 +36,16 @@ const send = async ( msg, rid, tmid, + t, }: { description?: string; msg?: string; rid: string; tmid?: string; + t?: IMessage['t']; }, + getContent?: (fileId: string, fileUrl: string) => Promise, + fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, ): Promise => { const id = Random.id(); @@ -49,7 +53,7 @@ const send = async ( ...uploads, { id, - name: file.name, + name: fileContent?.raw.name || file.name, percentage: 0, }, ]); @@ -57,12 +61,12 @@ const send = async ( try { await new Promise((resolve, reject) => { const xhr = sdk.rest.upload( - `/v1/rooms.upload/${rid}`, + `/v1/rooms.media/${rid}`, { - msg, - tmid, file, - description, + ...(fileContent && { + content: JSON.stringify(fileContent.encrypted), + }), }, { load: (event) => { @@ -109,6 +113,24 @@ const send = async ( }, ); + xhr.onload = async () => { + if (xhr.readyState === xhr.DONE && xhr.status === 200) { + const result = JSON.parse(xhr.responseText); + let content; + if (getContent) { + content = await getContent(result.file._id, result.file.url); + } + + await sdk.rest.post(`/v1/rooms.mediaConfirm/${rid}/${result.file._id}`, { + msg, + tmid, + description, + t, + content, + }); + } + }; + if (uploads.length) { UserAction.performContinuously(rid, USER_ACTIVITIES.USER_UPLOADING, { tmid }); } @@ -146,6 +168,10 @@ export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMes subscribe, wipeFailedOnes, cancel, - send: (file: File, { description, msg }: { description?: string; msg?: string }): Promise => - send(file, { description, msg, rid, tmid }), + send: ( + file: File, + { description, msg, t }: { description?: string; msg?: string; t?: IMessage['t'] }, + getContent?: (fileId: string, fileUrl: string) => Promise, + fileContent?: { raw: Partial; encrypted: IE2EEMessage['content'] }, + ): Promise => send(file, { description, msg, rid, tmid, t }, getContent, fileContent), }); diff --git a/apps/meteor/client/lib/convertTimeUnit.spec.ts b/apps/meteor/client/lib/convertTimeUnit.spec.ts new file mode 100644 index 0000000000000..e781d2d953a0f --- /dev/null +++ b/apps/meteor/client/lib/convertTimeUnit.spec.ts @@ -0,0 +1,65 @@ +import { TIMEUNIT, timeUnitToMs, msToTimeUnit } from './convertTimeUnit'; + +describe('timeUnitToMs function', () => { + it('should correctly convert days to milliseconds', () => { + expect(timeUnitToMs(TIMEUNIT.days, 1)).toBe(86400000); + expect(timeUnitToMs(TIMEUNIT.days, 2)).toBe(172800000); + expect(timeUnitToMs(TIMEUNIT.days, 0.5)).toBe(43200000); + }); + + it('should correctly convert hours to milliseconds', () => { + expect(timeUnitToMs(TIMEUNIT.hours, 1)).toBe(3600000); + expect(timeUnitToMs(TIMEUNIT.hours, 2)).toBe(7200000); + expect(timeUnitToMs(TIMEUNIT.hours, 0.5)).toBe(1800000); + }); + + it('should correctly convert minutes to milliseconds', () => { + expect(timeUnitToMs(TIMEUNIT.minutes, 1)).toBe(60000); + expect(timeUnitToMs(TIMEUNIT.minutes, 2)).toBe(120000); + expect(timeUnitToMs(TIMEUNIT.minutes, 0.5)).toBe(30000); + }); + + it('should throw an error for invalid time units', () => { + expect(() => timeUnitToMs('invalidUnit' as TIMEUNIT, 1)).toThrow('timeUnitToMs - invalid time unit'); + }); + + it('should throw an error for invalid timespan', () => { + const errorMessage = 'timeUnitToMs - invalid timespan'; + expect(() => timeUnitToMs(TIMEUNIT.days, NaN)).toThrow(errorMessage); + expect(() => timeUnitToMs(TIMEUNIT.days, Infinity)).toThrow(errorMessage); + expect(() => timeUnitToMs(TIMEUNIT.days, -Infinity)).toThrow(errorMessage); + expect(() => timeUnitToMs(TIMEUNIT.days, -1)).toThrow(errorMessage); + }); +}); + +describe('msToTimeUnit function', () => { + it('should correctly convert milliseconds to days', () => { + expect(msToTimeUnit(TIMEUNIT.days, 86400000)).toBe(1); // 1 day + expect(msToTimeUnit(TIMEUNIT.days, 172800000)).toBe(2); // 2 days + expect(msToTimeUnit(TIMEUNIT.days, 43200000)).toBe(0.5); // .5 days + }); + + it('should correctly convert milliseconds to hours', () => { + expect(msToTimeUnit(TIMEUNIT.hours, 3600000)).toBe(1); // 1 hour + expect(msToTimeUnit(TIMEUNIT.hours, 7200000)).toBe(2); // 2 hours + expect(msToTimeUnit(TIMEUNIT.hours, 1800000)).toBe(0.5); // .5 hours + }); + + it('should correctly convert milliseconds to minutes', () => { + expect(msToTimeUnit(TIMEUNIT.minutes, 60000)).toBe(1); // 1 min + expect(msToTimeUnit(TIMEUNIT.minutes, 120000)).toBe(2); // 2 min + expect(msToTimeUnit(TIMEUNIT.minutes, 30000)).toBe(0.5); // .5 min + }); + + it('should throw an error for invalid time units', () => { + expect(() => msToTimeUnit('invalidUnit' as TIMEUNIT, 1)).toThrow('msToTimeUnit - invalid time unit'); + }); + + it('should throw an error for invalid timespan', () => { + const errorMessage = 'msToTimeUnit - invalid timespan'; + expect(() => msToTimeUnit(TIMEUNIT.days, NaN)).toThrow(errorMessage); + expect(() => msToTimeUnit(TIMEUNIT.days, Infinity)).toThrow(errorMessage); + expect(() => msToTimeUnit(TIMEUNIT.days, -Infinity)).toThrow(errorMessage); + expect(() => msToTimeUnit(TIMEUNIT.days, -1)).toThrow(errorMessage); + }); +}); diff --git a/apps/meteor/client/lib/convertTimeUnit.ts b/apps/meteor/client/lib/convertTimeUnit.ts new file mode 100644 index 0000000000000..b19af29ee122b --- /dev/null +++ b/apps/meteor/client/lib/convertTimeUnit.ts @@ -0,0 +1,58 @@ +export enum TIMEUNIT { + days = 'days', + hours = 'hours', + minutes = 'minutes', +} + +export const isValidTimespan = (timespan: number): boolean => { + if (Number.isNaN(timespan)) { + return false; + } + + if (!Number.isFinite(timespan)) { + return false; + } + + if (timespan < 0) { + return false; + } + + return true; +}; + +export const timeUnitToMs = (unit: TIMEUNIT, timespan: number) => { + if (!isValidTimespan(timespan)) { + throw new Error(`timeUnitToMs - invalid timespan:${timespan}`); + } + + switch (unit) { + case TIMEUNIT.days: + return timespan * 24 * 60 * 60 * 1000; + + case TIMEUNIT.hours: + return timespan * 60 * 60 * 1000; + + case TIMEUNIT.minutes: + return timespan * 60 * 1000; + + default: + throw new Error('timeUnitToMs - invalid time unit'); + } +}; + +export const msToTimeUnit = (unit: TIMEUNIT, timespan: number) => { + if (!isValidTimespan(timespan)) { + throw new Error(`msToTimeUnit - invalid timespan:${timespan}`); + } + + switch (unit) { + case TIMEUNIT.days: + return timespan / 24 / 60 / 60 / 1000; + case TIMEUNIT.hours: + return timespan / 60 / 60 / 1000; + case TIMEUNIT.minutes: + return timespan / 60 / 1000; + default: + throw new Error('msToTimeUnit - invalid time unit'); + } +}; diff --git a/apps/meteor/ee/client/lib/fetchFeatures.ts b/apps/meteor/client/lib/fetchFeatures.ts similarity index 57% rename from apps/meteor/ee/client/lib/fetchFeatures.ts rename to apps/meteor/client/lib/fetchFeatures.ts index 617fc9623e1a2..c481e8cf2f76d 100644 --- a/apps/meteor/ee/client/lib/fetchFeatures.ts +++ b/apps/meteor/client/lib/fetchFeatures.ts @@ -1,5 +1,5 @@ -import { CachedCollectionManager } from '../../../app/ui-cached-collection/client'; -import { sdk } from '../../../app/utils/client/lib/SDKClient'; +import { CachedCollectionManager } from '../../app/ui-cached-collection/client'; +import { sdk } from '../../app/utils/client/lib/SDKClient'; export const fetchFeatures = (): Promise => new Promise((resolve, reject) => { diff --git a/apps/meteor/client/lib/getRoomTypeTranslation.ts b/apps/meteor/client/lib/getRoomTypeTranslation.ts new file mode 100644 index 0000000000000..e0c8556bac367 --- /dev/null +++ b/apps/meteor/client/lib/getRoomTypeTranslation.ts @@ -0,0 +1,39 @@ +import { + isPublicRoom, + type IRoom, + isDirectMessageRoom, + isPrivateTeamRoom, + isPublicTeamRoom, + isPrivateDiscussion, + isPrivateRoom, +} from '@rocket.chat/core-typings'; + +import { t } from '../../app/utils/lib/i18n'; + +export const getRoomTypeTranslation = (room: IRoom) => { + if (isPublicRoom(room)) { + return t('Channel'); + } + + if (isPrivateDiscussion(room)) { + return t('Private_Discussion'); + } + + if (isPrivateRoom(room)) { + return t('Private_Group'); + } + + if (isDirectMessageRoom(room)) { + return t('Direct_Message'); + } + + if (isPrivateTeamRoom(room)) { + return t('Teams_Private_Team'); + } + + if (isPublicTeamRoom(room)) { + return t('Teams_Public_Team'); + } + + return t('Room'); +}; diff --git a/apps/meteor/ee/client/lib/onToggledFeature.ts b/apps/meteor/client/lib/onToggledFeature.ts similarity index 92% rename from apps/meteor/ee/client/lib/onToggledFeature.ts rename to apps/meteor/client/lib/onToggledFeature.ts index 3f47b35bdbe2d..0147706869b60 100644 --- a/apps/meteor/ee/client/lib/onToggledFeature.ts +++ b/apps/meteor/client/lib/onToggledFeature.ts @@ -1,8 +1,8 @@ import type { LicenseModule } from '@rocket.chat/core-typings'; import { QueryObserver } from '@tanstack/react-query'; -import { queryClient } from '../../../client/lib/queryClient'; import { fetchFeatures } from './fetchFeatures'; +import { queryClient } from './queryClient'; export const onToggledFeature = ( feature: LicenseModule, diff --git a/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts b/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts index 41cbfed957506..0a1de5049e2bf 100644 --- a/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts +++ b/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts @@ -1,4 +1,4 @@ -import type { IMessage, ITranslatedMessage, MessageAttachment, MessageQuoteAttachment } from '@rocket.chat/core-typings'; +import type { IMessage, ITranslatedMessage, MessageAttachment } from '@rocket.chat/core-typings'; import { isFileAttachment, isE2EEMessage, @@ -56,21 +56,35 @@ export const parseMessageTextToAstMarkdown = < }; }; -export const parseMessageQuoteAttachment = ( - quote: T, +export const parseMessageAttachment = ( + attachment: T, parseOptions: Options, autoTranslateOptions: { autoTranslateLanguage?: string; translated: boolean }, ): T => { const { translated, autoTranslateLanguage } = autoTranslateOptions; - if (quote.attachments && quote.attachments?.length > 0) { - quote.attachments = quote.attachments.map((attachment) => parseMessageQuoteAttachment(attachment, parseOptions, autoTranslateOptions)); + if (!attachment.text && !attachment.description) { + return attachment; } - const text = (isTranslatedAttachment(quote) && autoTranslateLanguage && quote?.translations?.[autoTranslateLanguage]) || quote.text || ''; + if (isQuoteAttachment(attachment) && attachment.attachments) { + attachment.attachments = parseMessageAttachments(attachment.attachments, parseOptions, autoTranslateOptions); + } + + const text = + (isTranslatedAttachment(attachment) && autoTranslateLanguage && attachment?.translations?.[autoTranslateLanguage]) || + attachment.text || + attachment.description || + ''; + + if (isFileAttachment(attachment) && attachment.description) { + attachment.descriptionMd = translated + ? textToMessageToken(text, parseOptions) + : attachment.descriptionMd ?? textToMessageToken(text, parseOptions); + } return { - ...quote, - md: translated ? textToMessageToken(text, parseOptions) : quote.md ?? textToMessageToken(text, parseOptions), + ...attachment, + md: translated ? textToMessageToken(text, parseOptions) : attachment.md ?? textToMessageToken(text, parseOptions), }; }; @@ -78,36 +92,7 @@ export const parseMessageAttachments = ( attachments: T[], parseOptions: Options, autoTranslateOptions: { autoTranslateLanguage?: string; translated: boolean }, -): T[] => - attachments.map((attachment) => { - const { translated, autoTranslateLanguage } = autoTranslateOptions; - if (!attachment.text && !attachment.description) { - return attachment; - } - - if (isQuoteAttachment(attachment) && attachment.attachments) { - attachment.attachments = attachment.attachments.map((quoteAttachment) => - parseMessageQuoteAttachment(quoteAttachment, parseOptions, autoTranslateOptions), - ); - } - - const text = - (isTranslatedAttachment(attachment) && autoTranslateLanguage && attachment?.translations?.[autoTranslateLanguage]) || - attachment.text || - attachment.description || - ''; - - if (isFileAttachment(attachment) && attachment.description) { - attachment.descriptionMd = translated - ? textToMessageToken(text, parseOptions) - : attachment.descriptionMd ?? textToMessageToken(text, parseOptions); - } - - return { - ...attachment, - md: translated ? textToMessageToken(text, parseOptions) : attachment.md ?? textToMessageToken(text, parseOptions), - }; - }); +): T[] => attachments.map((attachment) => parseMessageAttachment(attachment, parseOptions, autoTranslateOptions)); const isNotNullOrUndefined = (value: unknown): boolean => value !== null && value !== undefined; diff --git a/apps/meteor/ee/client/lib/voip/EEVoipClient.ts b/apps/meteor/client/lib/voip/EEVoipClient.ts similarity index 97% rename from apps/meteor/ee/client/lib/voip/EEVoipClient.ts rename to apps/meteor/client/lib/voip/EEVoipClient.ts index 6afa5b2a68354..5f3496ed317c5 100644 --- a/apps/meteor/ee/client/lib/voip/EEVoipClient.ts +++ b/apps/meteor/client/lib/voip/EEVoipClient.ts @@ -3,7 +3,7 @@ import { Operation, UserState } from '@rocket.chat/core-typings'; import { Inviter, UserAgent } from 'sip.js'; import type { IncomingResponse } from 'sip.js/lib/core'; -import { VoIPUser } from '../../../../client/lib/voip/VoIPUser'; +import { VoIPUser } from './VoIPUser'; export class EEVoipClient extends VoIPUser { constructor(config: VoIPUserConfiguration, mediaRenderer?: IMediaStreamRenderer) { diff --git a/apps/meteor/client/main.ts b/apps/meteor/client/main.ts index 0a35c44a10be9..d66b5bcec2de5 100644 --- a/apps/meteor/client/main.ts +++ b/apps/meteor/client/main.ts @@ -1,3 +1,4 @@ +import './serviceWorker'; import './startup/accounts'; import { FlowRouter } from 'meteor/kadira:flow-router'; @@ -10,8 +11,8 @@ FlowRouter.notFound = { import('./polyfills') .then(() => import('./meteorOverrides')) - .then(() => import('../ee/client/ecdh')) + .then(() => import('./ecdh')) .then(() => import('./importPackages')) .then(() => Promise.all([import('./methods'), import('./startup')])) - .then(() => import('../ee/client')) + .then(() => import('./omnichannel')) .then(() => Promise.all([import('./views/admin'), import('./views/marketplace'), import('./views/account')])); diff --git a/apps/meteor/client/methods/updateMessage.ts b/apps/meteor/client/methods/updateMessage.ts index deb2878072c52..719a036f870c2 100644 --- a/apps/meteor/client/methods/updateMessage.ts +++ b/apps/meteor/client/methods/updateMessage.ts @@ -50,7 +50,7 @@ Meteor.methods({ } const blockEditInMinutes = Number(settings.get('Message_AllowEditing_BlockEditInMinutes') as number | undefined); - const bypassBlockTimeLimit = hasPermission('bypass-time-limit-edit-and-delete'); + const bypassBlockTimeLimit = hasPermission('bypass-time-limit-edit-and-delete', message.rid); if (!bypassBlockTimeLimit && blockEditInMinutes !== 0) { if (originalMessage.ts) { diff --git a/apps/meteor/ee/client/omnichannel/ContactManagerInfo.js b/apps/meteor/client/omnichannel/ContactManagerInfo.js similarity index 67% rename from apps/meteor/ee/client/omnichannel/ContactManagerInfo.js rename to apps/meteor/client/omnichannel/ContactManagerInfo.js index 282ecc2b423c9..d7272b901e26a 100644 --- a/apps/meteor/ee/client/omnichannel/ContactManagerInfo.js +++ b/apps/meteor/client/omnichannel/ContactManagerInfo.js @@ -2,11 +2,11 @@ import { css } from '@rocket.chat/css-in-js'; import { UserAvatar } from '@rocket.chat/ui-avatar'; import React, { useMemo } from 'react'; -import { UserStatus } from '../../../client/components/UserStatus'; -import { AsyncStatePhase } from '../../../client/hooks/useAsyncState'; -import { useEndpointData } from '../../../client/hooks/useEndpointData'; -import AgentInfoDetails from '../../../client/views/omnichannel/components/AgentInfoDetails'; -import Info from '../../../client/views/omnichannel/components/Info'; +import { UserStatus } from '../components/UserStatus'; +import { AsyncStatePhase } from '../hooks/useAsyncState'; +import { useEndpointData } from '../hooks/useEndpointData'; +import AgentInfoDetails from '../views/omnichannel/components/AgentInfoDetails'; +import Info from '../views/omnichannel/components/Info'; const wordBreak = css` word-break: break-word; diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/BusinessHoursMultiple.stories.tsx b/apps/meteor/client/omnichannel/additionalForms/BusinessHoursMultiple.stories.tsx similarity index 100% rename from apps/meteor/ee/client/omnichannel/additionalForms/BusinessHoursMultiple.stories.tsx rename to apps/meteor/client/omnichannel/additionalForms/BusinessHoursMultiple.stories.tsx diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/BusinessHoursMultiple.tsx b/apps/meteor/client/omnichannel/additionalForms/BusinessHoursMultiple.tsx similarity index 95% rename from apps/meteor/ee/client/omnichannel/additionalForms/BusinessHoursMultiple.tsx rename to apps/meteor/client/omnichannel/additionalForms/BusinessHoursMultiple.tsx index f5fc4838ba7ec..bba1fd135782a 100644 --- a/apps/meteor/ee/client/omnichannel/additionalForms/BusinessHoursMultiple.tsx +++ b/apps/meteor/client/omnichannel/additionalForms/BusinessHoursMultiple.tsx @@ -5,7 +5,7 @@ import type { ComponentProps } from 'react'; import React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; -import AutoCompleteDepartmentMultiple from '../../../../client/components/AutoCompleteDepartmentMultiple'; +import AutoCompleteDepartmentMultiple from '../../components/AutoCompleteDepartmentMultiple'; import { useHasLicenseModule } from '../../hooks/useHasLicenseModule'; const BusinessHoursMultiple = ({ className }: { className?: ComponentProps['className'] }) => { diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/ContactManager.js b/apps/meteor/client/omnichannel/additionalForms/ContactManager.js similarity index 88% rename from apps/meteor/ee/client/omnichannel/additionalForms/ContactManager.js rename to apps/meteor/client/omnichannel/additionalForms/ContactManager.js index 2b688378753d6..52ab527ef8411 100644 --- a/apps/meteor/ee/client/omnichannel/additionalForms/ContactManager.js +++ b/apps/meteor/client/omnichannel/additionalForms/ContactManager.js @@ -2,7 +2,7 @@ import { Field } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import AutoCompleteAgent from '../../../../client/components/AutoCompleteAgent'; +import AutoCompleteAgent from '../../components/AutoCompleteAgent'; import { useHasLicenseModule } from '../../hooks/useHasLicenseModule'; export const ContactManager = ({ value: userId, handler }) => { diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/CurrentChatTags.tsx b/apps/meteor/client/omnichannel/additionalForms/CurrentChatTags.tsx similarity index 100% rename from apps/meteor/ee/client/omnichannel/additionalForms/CurrentChatTags.tsx rename to apps/meteor/client/omnichannel/additionalForms/CurrentChatTags.tsx diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/CustomFieldsAdditionalForm.tsx b/apps/meteor/client/omnichannel/additionalForms/CustomFieldsAdditionalForm.tsx similarity index 100% rename from apps/meteor/ee/client/omnichannel/additionalForms/CustomFieldsAdditionalForm.tsx rename to apps/meteor/client/omnichannel/additionalForms/CustomFieldsAdditionalForm.tsx diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/DepartmentBusinessHours.tsx b/apps/meteor/client/omnichannel/additionalForms/DepartmentBusinessHours.tsx similarity index 100% rename from apps/meteor/ee/client/omnichannel/additionalForms/DepartmentBusinessHours.tsx rename to apps/meteor/client/omnichannel/additionalForms/DepartmentBusinessHours.tsx diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/DepartmentForwarding.tsx b/apps/meteor/client/omnichannel/additionalForms/DepartmentForwarding.tsx similarity index 91% rename from apps/meteor/ee/client/omnichannel/additionalForms/DepartmentForwarding.tsx rename to apps/meteor/client/omnichannel/additionalForms/DepartmentForwarding.tsx index 4e99fee43acdb..47ee87f7ee2a8 100644 --- a/apps/meteor/ee/client/omnichannel/additionalForms/DepartmentForwarding.tsx +++ b/apps/meteor/client/omnichannel/additionalForms/DepartmentForwarding.tsx @@ -5,9 +5,9 @@ import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useMemo, useState } from 'react'; -import { useDepartmentsList } from '../../../../client/components/Omnichannel/hooks/useDepartmentsList'; -import { useRecordList } from '../../../../client/hooks/lists/useRecordList'; -import { AsyncStatePhase } from '../../../../client/hooks/useAsyncState'; +import { useDepartmentsList } from '../../components/Omnichannel/hooks/useDepartmentsList'; +import { useRecordList } from '../../hooks/lists/useRecordList'; +import { AsyncStatePhase } from '../../hooks/useAsyncState'; import { useHasLicenseModule } from '../../hooks/useHasLicenseModule'; type DepartmentForwardingProps = { diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/EeNumberInput.tsx b/apps/meteor/client/omnichannel/additionalForms/EeNumberInput.tsx similarity index 100% rename from apps/meteor/ee/client/omnichannel/additionalForms/EeNumberInput.tsx rename to apps/meteor/client/omnichannel/additionalForms/EeNumberInput.tsx diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/EeTextAreaInput.tsx b/apps/meteor/client/omnichannel/additionalForms/EeTextAreaInput.tsx similarity index 100% rename from apps/meteor/ee/client/omnichannel/additionalForms/EeTextAreaInput.tsx rename to apps/meteor/client/omnichannel/additionalForms/EeTextAreaInput.tsx diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/EeTextInput.tsx b/apps/meteor/client/omnichannel/additionalForms/EeTextInput.tsx similarity index 100% rename from apps/meteor/ee/client/omnichannel/additionalForms/EeTextInput.tsx rename to apps/meteor/client/omnichannel/additionalForms/EeTextInput.tsx diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgent.tsx b/apps/meteor/client/omnichannel/additionalForms/MaxChatsPerAgent.tsx similarity index 100% rename from apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgent.tsx rename to apps/meteor/client/omnichannel/additionalForms/MaxChatsPerAgent.tsx diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentDisplay.tsx b/apps/meteor/client/omnichannel/additionalForms/MaxChatsPerAgentDisplay.tsx similarity index 90% rename from apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentDisplay.tsx rename to apps/meteor/client/omnichannel/additionalForms/MaxChatsPerAgentDisplay.tsx index 286b1a344d3b9..91980f119316f 100644 --- a/apps/meteor/ee/client/omnichannel/additionalForms/MaxChatsPerAgentDisplay.tsx +++ b/apps/meteor/client/omnichannel/additionalForms/MaxChatsPerAgentDisplay.tsx @@ -1,7 +1,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import UserInfo from '../../../../client/components/UserInfo'; +import UserInfo from '../../components/UserInfo'; import { useHasLicenseModule } from '../../hooks/useHasLicenseModule'; const MaxChatsPerAgentDisplay = ({ maxNumberSimultaneousChat = 0 }) => { diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/PrioritiesSelect.tsx b/apps/meteor/client/omnichannel/additionalForms/PrioritiesSelect.tsx similarity index 100% rename from apps/meteor/ee/client/omnichannel/additionalForms/PrioritiesSelect.tsx rename to apps/meteor/client/omnichannel/additionalForms/PrioritiesSelect.tsx diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/SlaPoliciesSelect.tsx b/apps/meteor/client/omnichannel/additionalForms/SlaPoliciesSelect.tsx similarity index 100% rename from apps/meteor/ee/client/omnichannel/additionalForms/SlaPoliciesSelect.tsx rename to apps/meteor/client/omnichannel/additionalForms/SlaPoliciesSelect.tsx diff --git a/apps/meteor/ee/client/omnichannel/businessHours/BusinessHoursRow.tsx b/apps/meteor/client/omnichannel/businessHours/BusinessHoursRow.tsx similarity index 94% rename from apps/meteor/ee/client/omnichannel/businessHours/BusinessHoursRow.tsx rename to apps/meteor/client/omnichannel/businessHours/BusinessHoursRow.tsx index 0ab3a518075cb..fa6c9a40f1593 100644 --- a/apps/meteor/ee/client/omnichannel/businessHours/BusinessHoursRow.tsx +++ b/apps/meteor/client/omnichannel/businessHours/BusinessHoursRow.tsx @@ -5,7 +5,7 @@ import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import type { KeyboardEvent } from 'react'; import React, { memo, useMemo } from 'react'; -import { GenericTableRow, GenericTableCell } from '../../../../client/components/GenericTable'; +import { GenericTableRow, GenericTableCell } from '../../components/GenericTable'; import { useRemoveBusinessHour } from './useRemoveBusinessHour'; const BusinessHoursRow = ({ _id, name, timezone, workHours, active, type }: Serialized) => { diff --git a/apps/meteor/ee/client/omnichannel/businessHours/BusinessHoursTable.stories.tsx b/apps/meteor/client/omnichannel/businessHours/BusinessHoursTable.stories.tsx similarity index 100% rename from apps/meteor/ee/client/omnichannel/businessHours/BusinessHoursTable.stories.tsx rename to apps/meteor/client/omnichannel/businessHours/BusinessHoursTable.stories.tsx diff --git a/apps/meteor/ee/client/omnichannel/businessHours/BusinessHoursTable.tsx b/apps/meteor/client/omnichannel/businessHours/BusinessHoursTable.tsx similarity index 90% rename from apps/meteor/ee/client/omnichannel/businessHours/BusinessHoursTable.tsx rename to apps/meteor/client/omnichannel/businessHours/BusinessHoursTable.tsx index 2da634cff9d50..6fddd95859d82 100644 --- a/apps/meteor/ee/client/omnichannel/businessHours/BusinessHoursTable.tsx +++ b/apps/meteor/client/omnichannel/businessHours/BusinessHoursTable.tsx @@ -3,16 +3,16 @@ import { useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import React, { useMemo, useState } from 'react'; -import FilterByText from '../../../../client/components/FilterByText'; -import GenericNoResults from '../../../../client/components/GenericNoResults'; +import FilterByText from '../../components/FilterByText'; +import GenericNoResults from '../../components/GenericNoResults'; import { GenericTable, GenericTableBody, GenericTableHeaderCell, GenericTableHeader, GenericTableLoadingRow, -} from '../../../../client/components/GenericTable'; -import { usePagination } from '../../../../client/components/GenericTable/hooks/usePagination'; +} from '../../components/GenericTable'; +import { usePagination } from '../../components/GenericTable/hooks/usePagination'; import BusinessHoursRow from './BusinessHoursRow'; const BusinessHoursTable = () => { diff --git a/apps/meteor/ee/client/omnichannel/businessHours/useRemoveBusinessHour.tsx b/apps/meteor/client/omnichannel/businessHours/useRemoveBusinessHour.tsx similarity index 94% rename from apps/meteor/ee/client/omnichannel/businessHours/useRemoveBusinessHour.tsx rename to apps/meteor/client/omnichannel/businessHours/useRemoveBusinessHour.tsx index f30cb75b2977d..626c541069461 100644 --- a/apps/meteor/ee/client/omnichannel/businessHours/useRemoveBusinessHour.tsx +++ b/apps/meteor/client/omnichannel/businessHours/useRemoveBusinessHour.tsx @@ -3,7 +3,7 @@ import { useSetModal, useToastMessageDispatch, useMethod, useTranslation } from import { useQueryClient } from '@tanstack/react-query'; import React from 'react'; -import GenericModal from '../../../../client/components/GenericModal'; +import GenericModal from '../../components/GenericModal'; export const useRemoveBusinessHour = () => { const t = useTranslation(); diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponseEdit.tsx b/apps/meteor/client/omnichannel/cannedResponses/CannedResponseEdit.tsx similarity index 98% rename from apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponseEdit.tsx rename to apps/meteor/client/omnichannel/cannedResponses/CannedResponseEdit.tsx index 3c903d5715182..28116cb26a13c 100644 --- a/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponseEdit.tsx +++ b/apps/meteor/client/omnichannel/cannedResponses/CannedResponseEdit.tsx @@ -6,7 +6,7 @@ import { useQueryClient } from '@tanstack/react-query'; import React, { memo, useCallback } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../../client/components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../components/Page'; import CannedResponseForm from './components/cannedResponseForm'; import { useRemoveCannedResponse } from './useRemoveCannedResponse'; diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponseEditWithData.tsx b/apps/meteor/client/omnichannel/cannedResponses/CannedResponseEditWithData.tsx similarity index 94% rename from apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponseEditWithData.tsx rename to apps/meteor/client/omnichannel/cannedResponses/CannedResponseEditWithData.tsx index dd06afbcfa8dc..64cbe556db9cc 100644 --- a/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponseEditWithData.tsx +++ b/apps/meteor/client/omnichannel/cannedResponses/CannedResponseEditWithData.tsx @@ -4,7 +4,7 @@ import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import React from 'react'; -import { FormSkeleton } from '../../../../client/components/Skeleton'; +import { FormSkeleton } from '../../components/Skeleton'; import CannedResponseEdit from './CannedResponseEdit'; import CannedResponseEditWithDepartmentData from './CannedResponseEditWithDepartmentData'; diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponseEditWithDepartmentData.tsx b/apps/meteor/client/omnichannel/cannedResponses/CannedResponseEditWithDepartmentData.tsx similarity index 82% rename from apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponseEditWithDepartmentData.tsx rename to apps/meteor/client/omnichannel/cannedResponses/CannedResponseEditWithDepartmentData.tsx index 30f6639c054f6..90dd6bd4ed325 100644 --- a/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponseEditWithDepartmentData.tsx +++ b/apps/meteor/client/omnichannel/cannedResponses/CannedResponseEditWithDepartmentData.tsx @@ -3,9 +3,9 @@ import { Callout } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useMemo } from 'react'; -import { FormSkeleton } from '../../../../client/components/Skeleton'; -import { AsyncStatePhase } from '../../../../client/hooks/useAsyncState'; -import { useEndpointData } from '../../../../client/hooks/useEndpointData'; +import { FormSkeleton } from '../../components/Skeleton'; +import { AsyncStatePhase } from '../../hooks/useAsyncState'; +import { useEndpointData } from '../../hooks/useEndpointData'; import CannedResponseEdit from './CannedResponseEdit'; const CannedResponseEditWithDepartmentData = ({ cannedResponseData }: { cannedResponseData: Serialized }) => { diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponseFilter.tsx b/apps/meteor/client/omnichannel/cannedResponses/CannedResponseFilter.tsx similarity index 95% rename from apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponseFilter.tsx rename to apps/meteor/client/omnichannel/cannedResponses/CannedResponseFilter.tsx index fb379886318fc..4746e7df6bea3 100644 --- a/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponseFilter.tsx +++ b/apps/meteor/client/omnichannel/cannedResponses/CannedResponseFilter.tsx @@ -4,7 +4,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ChangeEvent } from 'react'; import React, { memo } from 'react'; -import AutoCompleteAgent from '../../../../client/components/AutoCompleteAgent'; +import AutoCompleteAgent from '../../components/AutoCompleteAgent'; type SharingValues = '' | 'user' | 'global' | 'department'; diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponsesPage.tsx b/apps/meteor/client/omnichannel/cannedResponses/CannedResponsesPage.tsx similarity index 92% rename from apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponsesPage.tsx rename to apps/meteor/client/omnichannel/cannedResponses/CannedResponsesPage.tsx index 9156cd7d678fc..f476868be27b9 100644 --- a/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponsesPage.tsx +++ b/apps/meteor/client/omnichannel/cannedResponses/CannedResponsesPage.tsx @@ -2,7 +2,7 @@ import { Button, ButtonGroup } from '@rocket.chat/fuselage'; import { useRouteParameter, useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import { Page, PageHeader, PageContent } from '../../../../client/components/Page'; +import { Page, PageHeader, PageContent } from '../../components/Page'; import CannedResponseEdit from './CannedResponseEdit'; import CannedResponseEditWithData from './CannedResponseEditWithData'; import CannedResponsesTable from './CannedResponsesTable'; diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponsesRoute.tsx b/apps/meteor/client/omnichannel/cannedResponses/CannedResponsesRoute.tsx similarity index 83% rename from apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponsesRoute.tsx rename to apps/meteor/client/omnichannel/cannedResponses/CannedResponsesRoute.tsx index a0f49c14124bc..9e90ac015a46d 100644 --- a/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponsesRoute.tsx +++ b/apps/meteor/client/omnichannel/cannedResponses/CannedResponsesRoute.tsx @@ -2,7 +2,7 @@ import { usePermission } from '@rocket.chat/ui-contexts'; import type { FC } from 'react'; import React from 'react'; -import NotAuthorizedPage from '../../../../client/views/notAuthorized/NotAuthorizedPage'; +import NotAuthorizedPage from '../../views/notAuthorized/NotAuthorizedPage'; import CannedResponsesPage from './CannedResponsesPage'; const CannedResponsesRoute: FC = () => { diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponsesTable.tsx b/apps/meteor/client/omnichannel/cannedResponses/CannedResponsesTable.tsx similarity index 94% rename from apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponsesTable.tsx rename to apps/meteor/client/omnichannel/cannedResponses/CannedResponsesTable.tsx index dccef4dfdf092..8bf1d52e5ca97 100644 --- a/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponsesTable.tsx +++ b/apps/meteor/client/omnichannel/cannedResponses/CannedResponsesTable.tsx @@ -5,7 +5,7 @@ import { useTranslation, usePermission, useToastMessageDispatch, useEndpoint, us import { useQuery, hashQueryKey } from '@tanstack/react-query'; import React, { useMemo, useState } from 'react'; -import GenericNoResults from '../../../../client/components/GenericNoResults'; +import GenericNoResults from '../../components/GenericNoResults'; import { GenericTable, GenericTableBody, @@ -14,10 +14,10 @@ import { GenericTableLoadingRow, GenericTableRow, GenericTableCell, -} from '../../../../client/components/GenericTable'; -import { usePagination } from '../../../../client/components/GenericTable/hooks/usePagination'; -import { useSort } from '../../../../client/components/GenericTable/hooks/useSort'; -import { useFormatDateAndTime } from '../../../../client/hooks/useFormatDateAndTime'; +} from '../../components/GenericTable'; +import { usePagination } from '../../components/GenericTable/hooks/usePagination'; +import { useSort } from '../../components/GenericTable/hooks/useSort'; +import { useFormatDateAndTime } from '../../hooks/useFormatDateAndTime'; import CannedResponseFilter from './CannedResponseFilter'; import { useRemoveCannedResponse } from './useRemoveCannedResponse'; diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposer.stories.tsx b/apps/meteor/client/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposer.stories.tsx similarity index 100% rename from apps/meteor/ee/client/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposer.stories.tsx rename to apps/meteor/client/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposer.stories.tsx diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposer.tsx b/apps/meteor/client/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposer.tsx similarity index 96% rename from apps/meteor/ee/client/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposer.tsx rename to apps/meteor/client/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposer.tsx index 087b2f3192ba6..0abc8d3bd203c 100644 --- a/apps/meteor/ee/client/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposer.tsx +++ b/apps/meteor/client/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposer.tsx @@ -11,8 +11,8 @@ import { useUserPreference, useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import React, { memo, useCallback, useRef, useState } from 'react'; -import { Backdrop } from '../../../../../../client/components/Backdrop'; -import { useEmojiPicker } from '../../../../../../client/contexts/EmojiPickerContext'; +import { Backdrop } from '../../../../components/Backdrop'; +import { useEmojiPicker } from '../../../../contexts/EmojiPickerContext'; import InsertPlaceholderDropdown from './InsertPlaceholderDropdown'; const CannedResponsesComposer = ({ onChange, ...props }: ComponentProps) => { diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposerPreview.tsx b/apps/meteor/client/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposerPreview.tsx similarity index 87% rename from apps/meteor/ee/client/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposerPreview.tsx rename to apps/meteor/client/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposerPreview.tsx index 840793db7e3cc..63dde93ac8d66 100644 --- a/apps/meteor/ee/client/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposerPreview.tsx +++ b/apps/meteor/client/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposerPreview.tsx @@ -2,7 +2,7 @@ import { Box } from '@rocket.chat/fuselage'; import type { FC } from 'react'; import React, { memo } from 'react'; -import MarkdownText from '../../../../../../client/components/MarkdownText'; +import MarkdownText from '../../../../components/MarkdownText'; const CannedResponsesComposerPreview: FC<{ text: string }> = ({ text }) => { const textM = text.split(/\n/).join(' \n'); diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/components/CannedResponsesComposer/InsertPlaceholderDropdown.tsx b/apps/meteor/client/omnichannel/cannedResponses/components/CannedResponsesComposer/InsertPlaceholderDropdown.tsx similarity index 100% rename from apps/meteor/ee/client/omnichannel/cannedResponses/components/CannedResponsesComposer/InsertPlaceholderDropdown.tsx rename to apps/meteor/client/omnichannel/cannedResponses/components/CannedResponsesComposer/InsertPlaceholderDropdown.tsx diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/components/cannedResponseForm.tsx b/apps/meteor/client/omnichannel/cannedResponses/components/cannedResponseForm.tsx similarity index 97% rename from apps/meteor/ee/client/omnichannel/cannedResponses/components/cannedResponseForm.tsx rename to apps/meteor/client/omnichannel/cannedResponses/components/cannedResponseForm.tsx index e07c8e2898450..51c993d2542f3 100644 --- a/apps/meteor/ee/client/omnichannel/cannedResponses/components/cannedResponseForm.tsx +++ b/apps/meteor/client/omnichannel/cannedResponses/components/cannedResponseForm.tsx @@ -5,8 +5,8 @@ import { usePermission, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useState } from 'react'; import { useFormContext, Controller } from 'react-hook-form'; -import AutoCompleteDepartment from '../../../../../client/components/AutoCompleteDepartment'; -import Tags from '../../../../../client/components/Omnichannel/Tags'; +import AutoCompleteDepartment from '../../../components/AutoCompleteDepartment'; +import Tags from '../../../components/Omnichannel/Tags'; import CannedResponsesComposer from './CannedResponsesComposer/CannedResponsesComposer'; import CannedResponsesComposerPreview from './CannedResponsesComposer/CannedResponsesComposerPreview'; diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponse.stories.tsx b/apps/meteor/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponse.stories.tsx similarity index 100% rename from apps/meteor/ee/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponse.stories.tsx rename to apps/meteor/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponse.stories.tsx diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponse.tsx b/apps/meteor/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponse.tsx similarity index 98% rename from apps/meteor/ee/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponse.tsx rename to apps/meteor/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponse.tsx index 90d6a4523cb7f..557d9672c027a 100644 --- a/apps/meteor/ee/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponse.tsx +++ b/apps/meteor/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponse.tsx @@ -11,7 +11,7 @@ import { ContextualbarAction, ContextualbarContent, ContextualbarFooter, -} from '../../../../../../client/components/Contextualbar'; +} from '../../../../components/Contextualbar'; import { useScopeDict } from '../../../hooks/useScopeDict'; const CannedResponse: FC<{ diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponseList.stories.tsx b/apps/meteor/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponseList.stories.tsx similarity index 96% rename from apps/meteor/ee/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponseList.stories.tsx rename to apps/meteor/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponseList.stories.tsx index a27dc589fd78a..544502bda87fa 100644 --- a/apps/meteor/ee/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponseList.stories.tsx +++ b/apps/meteor/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponseList.stories.tsx @@ -3,7 +3,7 @@ import { action } from '@storybook/addon-actions'; import type { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; -import { Contextualbar } from '../../../../../../client/components/Contextualbar'; +import { Contextualbar } from '../../../../components/Contextualbar'; import CannedResponseList from './CannedResponseList'; export default { diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponseList.tsx b/apps/meteor/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponseList.tsx similarity index 94% rename from apps/meteor/ee/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponseList.tsx rename to apps/meteor/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponseList.tsx index b9d3e57e31de1..cc1be1da33c8c 100644 --- a/apps/meteor/ee/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponseList.tsx +++ b/apps/meteor/client/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponseList.tsx @@ -13,9 +13,9 @@ import { ContextualbarContent, ContextualbarInnerContent, ContextualbarFooter, -} from '../../../../../../client/components/Contextualbar'; -import { VirtuosoScrollbars } from '../../../../../../client/components/CustomScrollbars'; -import { useRoomToolbox } from '../../../../../../client/views/room/contexts/RoomToolboxContext'; +} from '../../../../components/Contextualbar'; +import { VirtuosoScrollbars } from '../../../../components/CustomScrollbars'; +import { useRoomToolbox } from '../../../../views/room/contexts/RoomToolboxContext'; import Item from './Item'; import WrapCannedResponse from './WrapCannedResponse'; diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/contextualBar/CannedResponse/Item.stories.tsx b/apps/meteor/client/omnichannel/cannedResponses/contextualBar/CannedResponse/Item.stories.tsx similarity index 100% rename from apps/meteor/ee/client/omnichannel/cannedResponses/contextualBar/CannedResponse/Item.stories.tsx rename to apps/meteor/client/omnichannel/cannedResponses/contextualBar/CannedResponse/Item.stories.tsx diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/contextualBar/CannedResponse/Item.tsx b/apps/meteor/client/omnichannel/cannedResponses/contextualBar/CannedResponse/Item.tsx similarity index 100% rename from apps/meteor/ee/client/omnichannel/cannedResponses/contextualBar/CannedResponse/Item.tsx rename to apps/meteor/client/omnichannel/cannedResponses/contextualBar/CannedResponse/Item.tsx diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/contextualBar/CannedResponse/WrapCannedResponse.tsx b/apps/meteor/client/omnichannel/cannedResponses/contextualBar/CannedResponse/WrapCannedResponse.tsx similarity index 100% rename from apps/meteor/ee/client/omnichannel/cannedResponses/contextualBar/CannedResponse/WrapCannedResponse.tsx rename to apps/meteor/client/omnichannel/cannedResponses/contextualBar/CannedResponse/WrapCannedResponse.tsx diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/contextualBar/CannedResponse/WrapCannedResponseList.tsx b/apps/meteor/client/omnichannel/cannedResponses/contextualBar/CannedResponse/WrapCannedResponseList.tsx similarity index 82% rename from apps/meteor/ee/client/omnichannel/cannedResponses/contextualBar/CannedResponse/WrapCannedResponseList.tsx rename to apps/meteor/client/omnichannel/cannedResponses/contextualBar/CannedResponse/WrapCannedResponseList.tsx index 25108d0954186..6fd0ee420dbf7 100644 --- a/apps/meteor/ee/client/omnichannel/cannedResponses/contextualBar/CannedResponse/WrapCannedResponseList.tsx +++ b/apps/meteor/client/omnichannel/cannedResponses/contextualBar/CannedResponse/WrapCannedResponseList.tsx @@ -3,12 +3,12 @@ import { useSetModal, useRouter } from '@rocket.chat/ui-contexts'; import type { MouseEvent } from 'react'; import React, { memo, useCallback, useMemo, useState } from 'react'; -import { useRecordList } from '../../../../../../client/hooks/lists/useRecordList'; -import { useIsRoomOverMacLimit } from '../../../../../../client/hooks/omnichannel/useIsRoomOverMacLimit'; -import { AsyncStatePhase } from '../../../../../../client/lib/asyncState'; -import { useChat } from '../../../../../../client/views/room/contexts/ChatContext'; -import { useRoom } from '../../../../../../client/views/room/contexts/RoomContext'; -import { useRoomToolbox } from '../../../../../../client/views/room/contexts/RoomToolboxContext'; +import { useRecordList } from '../../../../hooks/lists/useRecordList'; +import { useIsRoomOverMacLimit } from '../../../../hooks/omnichannel/useIsRoomOverMacLimit'; +import { AsyncStatePhase } from '../../../../lib/asyncState'; +import { useChat } from '../../../../views/room/contexts/ChatContext'; +import { useRoom } from '../../../../views/room/contexts/RoomContext'; +import { useRoomToolbox } from '../../../../views/room/contexts/RoomToolboxContext'; import { useCannedResponseFilterOptions } from '../../../hooks/useCannedResponseFilterOptions'; import { useCannedResponseList } from '../../../hooks/useCannedResponseList'; import CreateCannedResponse from '../../modals/CreateCannedResponse'; diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/modals/CreateCannedResponse/CreateCannedResponseModal.stories.tsx b/apps/meteor/client/omnichannel/cannedResponses/modals/CreateCannedResponse/CreateCannedResponseModal.stories.tsx similarity index 100% rename from apps/meteor/ee/client/omnichannel/cannedResponses/modals/CreateCannedResponse/CreateCannedResponseModal.stories.tsx rename to apps/meteor/client/omnichannel/cannedResponses/modals/CreateCannedResponse/CreateCannedResponseModal.stories.tsx diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/modals/CreateCannedResponse/CreateCannedResponseModal.tsx b/apps/meteor/client/omnichannel/cannedResponses/modals/CreateCannedResponse/CreateCannedResponseModal.tsx similarity index 97% rename from apps/meteor/ee/client/omnichannel/cannedResponses/modals/CreateCannedResponse/CreateCannedResponseModal.tsx rename to apps/meteor/client/omnichannel/cannedResponses/modals/CreateCannedResponse/CreateCannedResponseModal.tsx index 15b65e6b42e39..9776564264cc5 100644 --- a/apps/meteor/ee/client/omnichannel/cannedResponses/modals/CreateCannedResponse/CreateCannedResponseModal.tsx +++ b/apps/meteor/client/omnichannel/cannedResponses/modals/CreateCannedResponse/CreateCannedResponseModal.tsx @@ -4,7 +4,7 @@ import { useEndpoint, useToastMessageDispatch, useTranslation } from '@rocket.ch import React, { memo, useCallback } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import GenericModal from '../../../../../../client/components/GenericModal'; +import GenericModal from '../../../../components/GenericModal'; import CannedResponseForm from '../../components/cannedResponseForm'; const getInitialData = ( diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/modals/CreateCannedResponse/index.tsx b/apps/meteor/client/omnichannel/cannedResponses/modals/CreateCannedResponse/index.tsx similarity index 100% rename from apps/meteor/ee/client/omnichannel/cannedResponses/modals/CreateCannedResponse/index.tsx rename to apps/meteor/client/omnichannel/cannedResponses/modals/CreateCannedResponse/index.tsx diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/useRemoveCannedResponse.tsx b/apps/meteor/client/omnichannel/cannedResponses/useRemoveCannedResponse.tsx similarity index 94% rename from apps/meteor/ee/client/omnichannel/cannedResponses/useRemoveCannedResponse.tsx rename to apps/meteor/client/omnichannel/cannedResponses/useRemoveCannedResponse.tsx index 2d0f5f36ab28a..aa23533b3e72e 100644 --- a/apps/meteor/ee/client/omnichannel/cannedResponses/useRemoveCannedResponse.tsx +++ b/apps/meteor/client/omnichannel/cannedResponses/useRemoveCannedResponse.tsx @@ -3,7 +3,7 @@ import { useSetModal, useToastMessageDispatch, useRouter, useMethod, useTranslat import { useQueryClient } from '@tanstack/react-query'; import React from 'react'; -import GenericModal from '../../../../client/components/GenericModal'; +import GenericModal from '../../components/GenericModal'; export const useRemoveCannedResponse = () => { const t = useTranslation(); diff --git a/apps/meteor/ee/client/omnichannel/components/RoomActivityIcon/index.tsx b/apps/meteor/client/omnichannel/components/RoomActivityIcon/index.tsx similarity index 86% rename from apps/meteor/ee/client/omnichannel/components/RoomActivityIcon/index.tsx rename to apps/meteor/client/omnichannel/components/RoomActivityIcon/index.tsx index 5db68f559fdb4..749f40d28a604 100644 --- a/apps/meteor/ee/client/omnichannel/components/RoomActivityIcon/index.tsx +++ b/apps/meteor/client/omnichannel/components/RoomActivityIcon/index.tsx @@ -4,7 +4,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import { useIsRoomOverMacLimit } from '../../../../../client/hooks/omnichannel/useIsRoomOverMacLimit'; +import { useIsRoomOverMacLimit } from '../../../hooks/omnichannel/useIsRoomOverMacLimit'; type RoomActivityIconProps = { room: IOmnichannelRoom; diff --git a/apps/meteor/ee/client/omnichannel/hooks/useCannedResponseFilterOptions.ts b/apps/meteor/client/omnichannel/hooks/useCannedResponseFilterOptions.ts similarity index 100% rename from apps/meteor/ee/client/omnichannel/hooks/useCannedResponseFilterOptions.ts rename to apps/meteor/client/omnichannel/hooks/useCannedResponseFilterOptions.ts diff --git a/apps/meteor/ee/client/omnichannel/hooks/useCannedResponseList.ts b/apps/meteor/client/omnichannel/hooks/useCannedResponseList.ts similarity index 88% rename from apps/meteor/ee/client/omnichannel/hooks/useCannedResponseList.ts rename to apps/meteor/client/omnichannel/hooks/useCannedResponseList.ts index 4f4e60e0f30a7..6e02f302e3e19 100644 --- a/apps/meteor/ee/client/omnichannel/hooks/useCannedResponseList.ts +++ b/apps/meteor/client/omnichannel/hooks/useCannedResponseList.ts @@ -1,9 +1,9 @@ import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useCallback, useEffect, useState } from 'react'; -import { useScrollableRecordList } from '../../../../client/hooks/lists/useScrollableRecordList'; -import { useComponentDidUpdate } from '../../../../client/hooks/useComponentDidUpdate'; -import { CannedResponseList } from '../../../../client/lib/lists/CannedResponseList'; +import { useScrollableRecordList } from '../../hooks/lists/useScrollableRecordList'; +import { useComponentDidUpdate } from '../../hooks/useComponentDidUpdate'; +import { CannedResponseList } from '../../lib/lists/CannedResponseList'; export const useCannedResponseList = ( options: any, diff --git a/apps/meteor/client/omnichannel/hooks/useOmnichannelPriorities.ts b/apps/meteor/client/omnichannel/hooks/useOmnichannelPriorities.ts new file mode 100644 index 0000000000000..9552a750e55b5 --- /dev/null +++ b/apps/meteor/client/omnichannel/hooks/useOmnichannelPriorities.ts @@ -0,0 +1,3 @@ +import { useOmnichannel } from '../../hooks/omnichannel/useOmnichannel'; + +export const useOmnichannelPriorities = () => useOmnichannel().livechatPriorities; diff --git a/apps/meteor/ee/client/omnichannel/hooks/useOmnichannelPrioritiesMenu.tsx b/apps/meteor/client/omnichannel/hooks/useOmnichannelPrioritiesMenu.tsx similarity index 97% rename from apps/meteor/ee/client/omnichannel/hooks/useOmnichannelPrioritiesMenu.tsx rename to apps/meteor/client/omnichannel/hooks/useOmnichannelPrioritiesMenu.tsx index ebfa9bf08a0be..2a99f6f1fada9 100644 --- a/apps/meteor/ee/client/omnichannel/hooks/useOmnichannelPrioritiesMenu.tsx +++ b/apps/meteor/client/omnichannel/hooks/useOmnichannelPrioritiesMenu.tsx @@ -6,7 +6,7 @@ import { useQueryClient } from '@tanstack/react-query'; import type { ComponentProps } from 'react'; import React, { useCallback, useMemo } from 'react'; -import { dispatchToastMessage } from '../../../../client/lib/toast'; +import { dispatchToastMessage } from '../../lib/toast'; import { PriorityIcon } from '../priorities/PriorityIcon'; import { useOmnichannelPriorities } from './useOmnichannelPriorities'; diff --git a/apps/meteor/ee/client/omnichannel/hooks/useScopeDict.ts b/apps/meteor/client/omnichannel/hooks/useScopeDict.ts similarity index 100% rename from apps/meteor/ee/client/omnichannel/hooks/useScopeDict.ts rename to apps/meteor/client/omnichannel/hooks/useScopeDict.ts diff --git a/apps/meteor/ee/client/omnichannel/index.ts b/apps/meteor/client/omnichannel/index.ts similarity index 100% rename from apps/meteor/ee/client/omnichannel/index.ts rename to apps/meteor/client/omnichannel/index.ts diff --git a/apps/meteor/ee/client/omnichannel/monitors/MonitorsPage.tsx b/apps/meteor/client/omnichannel/monitors/MonitorsPage.tsx similarity index 82% rename from apps/meteor/ee/client/omnichannel/monitors/MonitorsPage.tsx rename to apps/meteor/client/omnichannel/monitors/MonitorsPage.tsx index 4574f74bb2b71..dffbde494e7a2 100644 --- a/apps/meteor/ee/client/omnichannel/monitors/MonitorsPage.tsx +++ b/apps/meteor/client/omnichannel/monitors/MonitorsPage.tsx @@ -1,7 +1,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import { Page, PageHeader, PageContent } from '../../../../client/components/Page'; +import { Page, PageHeader, PageContent } from '../../components/Page'; import MonitorsTable from './MonitorsTable'; const MonitorsPage = () => { diff --git a/apps/meteor/ee/client/omnichannel/monitors/MonitorsPageContainer.tsx b/apps/meteor/client/omnichannel/monitors/MonitorsPageContainer.tsx similarity index 72% rename from apps/meteor/ee/client/omnichannel/monitors/MonitorsPageContainer.tsx rename to apps/meteor/client/omnichannel/monitors/MonitorsPageContainer.tsx index ce27ae47ae8c7..b86f8a1ced2cb 100644 --- a/apps/meteor/ee/client/omnichannel/monitors/MonitorsPageContainer.tsx +++ b/apps/meteor/client/omnichannel/monitors/MonitorsPageContainer.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import PageSkeleton from '../../../../client/components/PageSkeleton'; -import NotAuthorizedPage from '../../../../client/views/notAuthorized/NotAuthorizedPage'; +import PageSkeleton from '../../components/PageSkeleton'; import { useHasLicenseModule } from '../../hooks/useHasLicenseModule'; +import NotAuthorizedPage from '../../views/notAuthorized/NotAuthorizedPage'; import MonitorsPage from './MonitorsPage'; const MonitorsPageContainer = () => { diff --git a/apps/meteor/ee/client/omnichannel/monitors/MonitorsTable.tsx b/apps/meteor/client/omnichannel/monitors/MonitorsTable.tsx similarity index 91% rename from apps/meteor/ee/client/omnichannel/monitors/MonitorsTable.tsx rename to apps/meteor/client/omnichannel/monitors/MonitorsTable.tsx index 6a99ae93340d9..a67057b9add16 100644 --- a/apps/meteor/ee/client/omnichannel/monitors/MonitorsTable.tsx +++ b/apps/meteor/client/omnichannel/monitors/MonitorsTable.tsx @@ -17,9 +17,9 @@ import { useTranslation, useToastMessageDispatch, useMethod, useEndpoint, useSet import { useMutation, useQuery, hashQueryKey } from '@tanstack/react-query'; import React, { useMemo, useState } from 'react'; -import FilterByText from '../../../../client/components/FilterByText'; -import GenericModal from '../../../../client/components/GenericModal'; -import GenericNoResults from '../../../../client/components/GenericNoResults'; +import FilterByText from '../../components/FilterByText'; +import GenericModal from '../../components/GenericModal'; +import GenericNoResults from '../../components/GenericNoResults'; import { GenericTable, GenericTableBody, @@ -28,11 +28,11 @@ import { GenericTableHeaderCell, GenericTableLoadingTable, GenericTableRow, -} from '../../../../client/components/GenericTable'; -import { usePagination } from '../../../../client/components/GenericTable/hooks/usePagination'; -import { useSort } from '../../../../client/components/GenericTable/hooks/useSort'; -import UserAutoComplete from '../../../../client/components/UserAutoComplete'; -import { queryClient } from '../../../../client/lib/queryClient'; +} from '../../components/GenericTable'; +import { usePagination } from '../../components/GenericTable/hooks/usePagination'; +import { useSort } from '../../components/GenericTable/hooks/useSort'; +import UserAutoComplete from '../../components/UserAutoComplete'; +import { queryClient } from '../../lib/queryClient'; const MonitorsTable = () => { const t = useTranslation(); diff --git a/apps/meteor/ee/client/omnichannel/priorities/PrioritiesPage.tsx b/apps/meteor/client/omnichannel/priorities/PrioritiesPage.tsx similarity index 97% rename from apps/meteor/ee/client/omnichannel/priorities/PrioritiesPage.tsx rename to apps/meteor/client/omnichannel/priorities/PrioritiesPage.tsx index 8b87d0f811e34..fe694c1de8515 100644 --- a/apps/meteor/ee/client/omnichannel/priorities/PrioritiesPage.tsx +++ b/apps/meteor/client/omnichannel/priorities/PrioritiesPage.tsx @@ -5,7 +5,7 @@ import { useQueryClient } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { useMemo, useState } from 'react'; -import { Page, PageHeader, PageContent } from '../../../../client/components/Page'; +import { Page, PageHeader, PageContent } from '../../components/Page'; import { useOmnichannelPriorities } from '../hooks/useOmnichannelPriorities'; import { PrioritiesResetModal } from './PrioritiesResetModal'; import { PrioritiesTable } from './PrioritiesTable'; diff --git a/apps/meteor/ee/client/omnichannel/priorities/PrioritiesResetModal.tsx b/apps/meteor/client/omnichannel/priorities/PrioritiesResetModal.tsx similarity index 89% rename from apps/meteor/ee/client/omnichannel/priorities/PrioritiesResetModal.tsx rename to apps/meteor/client/omnichannel/priorities/PrioritiesResetModal.tsx index 0fdbd34f66aa3..a85dd72c868d6 100644 --- a/apps/meteor/ee/client/omnichannel/priorities/PrioritiesResetModal.tsx +++ b/apps/meteor/client/omnichannel/priorities/PrioritiesResetModal.tsx @@ -2,7 +2,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import GenericModal from '../../../../client/components/GenericModal'; +import GenericModal from '../../components/GenericModal'; type PrioritiesResetModalProps = { onReset: () => Promise; diff --git a/apps/meteor/ee/client/omnichannel/priorities/PrioritiesRoute.tsx b/apps/meteor/client/omnichannel/priorities/PrioritiesRoute.tsx similarity index 86% rename from apps/meteor/ee/client/omnichannel/priorities/PrioritiesRoute.tsx rename to apps/meteor/client/omnichannel/priorities/PrioritiesRoute.tsx index 7a7f6cafdb17e..fd3b73a3f4a5e 100644 --- a/apps/meteor/ee/client/omnichannel/priorities/PrioritiesRoute.tsx +++ b/apps/meteor/client/omnichannel/priorities/PrioritiesRoute.tsx @@ -2,7 +2,7 @@ import { usePermission, useRouteParameter } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import NotAuthorizedPage from '../../../../client/views/notAuthorized/NotAuthorizedPage'; +import NotAuthorizedPage from '../../views/notAuthorized/NotAuthorizedPage'; import { PrioritiesPage } from './PrioritiesPage'; const PrioritiesRoute = (): ReactElement => { diff --git a/apps/meteor/ee/client/omnichannel/priorities/PrioritiesTable.tsx b/apps/meteor/client/omnichannel/priorities/PrioritiesTable.tsx similarity index 93% rename from apps/meteor/ee/client/omnichannel/priorities/PrioritiesTable.tsx rename to apps/meteor/client/omnichannel/priorities/PrioritiesTable.tsx index 84c3051b6aea1..7e5368d7a0a55 100644 --- a/apps/meteor/ee/client/omnichannel/priorities/PrioritiesTable.tsx +++ b/apps/meteor/client/omnichannel/priorities/PrioritiesTable.tsx @@ -3,7 +3,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import GenericNoResults from '../../../../client/components/GenericNoResults'; +import GenericNoResults from '../../components/GenericNoResults'; import { GenericTable, GenericTableHeaderCell, @@ -12,7 +12,7 @@ import { GenericTableHeader, GenericTableBody, GenericTableLoadingTable, -} from '../../../../client/components/GenericTable'; +} from '../../components/GenericTable'; import { PriorityIcon } from './PriorityIcon'; type PrioritiesTableProps = { diff --git a/apps/meteor/ee/client/omnichannel/priorities/PriorityEditForm.tsx b/apps/meteor/client/omnichannel/priorities/PriorityEditForm.tsx similarity index 96% rename from apps/meteor/ee/client/omnichannel/priorities/PriorityEditForm.tsx rename to apps/meteor/client/omnichannel/priorities/PriorityEditForm.tsx index c39916369f5dc..d67f637a2b4e1 100644 --- a/apps/meteor/ee/client/omnichannel/priorities/PriorityEditForm.tsx +++ b/apps/meteor/client/omnichannel/priorities/PriorityEditForm.tsx @@ -7,7 +7,7 @@ import type { ReactElement } from 'react'; import React, { useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; -import StringSettingInput from '../../../../client/views/admin/settings/inputs/StringSettingInput'; +import StringSettingInput from '../../views/admin/settings/inputs/StringSettingInput'; export type PriorityFormData = { name: string; reset: boolean }; @@ -81,6 +81,7 @@ const PriorityEditForm = ({ data, onSave, onCancel }: PriorityEditFormProps): Re render={({ field: { value, onChange } }): ReactElement => ( void }) => { const t = useTranslation(); diff --git a/apps/meteor/ee/client/omnichannel/slaPolicies/SlaEdit.tsx b/apps/meteor/client/omnichannel/slaPolicies/SlaEdit.tsx similarity index 97% rename from apps/meteor/ee/client/omnichannel/slaPolicies/SlaEdit.tsx rename to apps/meteor/client/omnichannel/slaPolicies/SlaEdit.tsx index 83f2c4a36e72d..0b4ab02791da9 100644 --- a/apps/meteor/ee/client/omnichannel/slaPolicies/SlaEdit.tsx +++ b/apps/meteor/client/omnichannel/slaPolicies/SlaEdit.tsx @@ -6,7 +6,7 @@ import type { ReactElement } from 'react'; import React from 'react'; import { useController, useForm } from 'react-hook-form'; -import { ContextualbarScrollableContent } from '../../../../client/components/Contextualbar'; +import { ContextualbarScrollableContent } from '../../components/Contextualbar'; type SlaEditProps = { isNew?: boolean; diff --git a/apps/meteor/ee/client/omnichannel/slaPolicies/SlaEditWithData.tsx b/apps/meteor/client/omnichannel/slaPolicies/SlaEditWithData.tsx similarity index 92% rename from apps/meteor/ee/client/omnichannel/slaPolicies/SlaEditWithData.tsx rename to apps/meteor/client/omnichannel/slaPolicies/SlaEditWithData.tsx index 8a86255fa86b4..079e663f19c4b 100644 --- a/apps/meteor/ee/client/omnichannel/slaPolicies/SlaEditWithData.tsx +++ b/apps/meteor/client/omnichannel/slaPolicies/SlaEditWithData.tsx @@ -4,7 +4,7 @@ import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React from 'react'; -import { FormSkeleton } from '../../../../client/components/Skeleton'; +import { FormSkeleton } from '../../components/Skeleton'; import SlaEdit from './SlaEdit'; type SlaEditProps = { diff --git a/apps/meteor/ee/client/omnichannel/slaPolicies/SlaNew.tsx b/apps/meteor/client/omnichannel/slaPolicies/SlaNew.tsx similarity index 100% rename from apps/meteor/ee/client/omnichannel/slaPolicies/SlaNew.tsx rename to apps/meteor/client/omnichannel/slaPolicies/SlaNew.tsx diff --git a/apps/meteor/ee/client/omnichannel/slaPolicies/SlaPage.tsx b/apps/meteor/client/omnichannel/slaPolicies/SlaPage.tsx similarity index 93% rename from apps/meteor/ee/client/omnichannel/slaPolicies/SlaPage.tsx rename to apps/meteor/client/omnichannel/slaPolicies/SlaPage.tsx index cda40b7418680..7cbca605b712b 100644 --- a/apps/meteor/ee/client/omnichannel/slaPolicies/SlaPage.tsx +++ b/apps/meteor/client/omnichannel/slaPolicies/SlaPage.tsx @@ -9,8 +9,8 @@ import { ContextualbarHeader, ContextualbarClose, ContextualbarDialog, -} from '../../../../client/components/Contextualbar'; -import { Page, PageHeader, PageContent } from '../../../../client/components/Page'; +} from '../../components/Contextualbar'; +import { Page, PageHeader, PageContent } from '../../components/Page'; import SlaEditWithData from './SlaEditWithData'; import SlaNew from './SlaNew'; import SlaTable from './SlaTable'; diff --git a/apps/meteor/ee/client/omnichannel/slaPolicies/SlaRoute.tsx b/apps/meteor/client/omnichannel/slaPolicies/SlaRoute.tsx similarity index 77% rename from apps/meteor/ee/client/omnichannel/slaPolicies/SlaRoute.tsx rename to apps/meteor/client/omnichannel/slaPolicies/SlaRoute.tsx index 792fae8946d93..081c33fe3260f 100644 --- a/apps/meteor/ee/client/omnichannel/slaPolicies/SlaRoute.tsx +++ b/apps/meteor/client/omnichannel/slaPolicies/SlaRoute.tsx @@ -1,7 +1,7 @@ import { usePermission } from '@rocket.chat/ui-contexts'; import React from 'react'; -import NotAuthorizedPage from '../../../../client/views/notAuthorized/NotAuthorizedPage'; +import NotAuthorizedPage from '../../views/notAuthorized/NotAuthorizedPage'; import SlaPage from './SlaPage'; const SlaRoute = () => { diff --git a/apps/meteor/ee/client/omnichannel/slaPolicies/SlaTable.tsx b/apps/meteor/client/omnichannel/slaPolicies/SlaTable.tsx similarity index 91% rename from apps/meteor/ee/client/omnichannel/slaPolicies/SlaTable.tsx rename to apps/meteor/client/omnichannel/slaPolicies/SlaTable.tsx index 0a07f7468f014..d550118a5017d 100644 --- a/apps/meteor/ee/client/omnichannel/slaPolicies/SlaTable.tsx +++ b/apps/meteor/client/omnichannel/slaPolicies/SlaTable.tsx @@ -5,8 +5,8 @@ import { useQuery, hashQueryKey } from '@tanstack/react-query'; import type { MutableRefObject } from 'react'; import React, { useMemo, useState, useEffect } from 'react'; -import FilterByText from '../../../../client/components/FilterByText'; -import GenericNoResults from '../../../../client/components/GenericNoResults/GenericNoResults'; +import FilterByText from '../../components/FilterByText'; +import GenericNoResults from '../../components/GenericNoResults/GenericNoResults'; import { GenericTable, GenericTableHeaderCell, @@ -15,9 +15,9 @@ import { GenericTableBody, GenericTableRow, GenericTableCell, -} from '../../../../client/components/GenericTable'; -import { usePagination } from '../../../../client/components/GenericTable/hooks/usePagination'; -import { useSort } from '../../../../client/components/GenericTable/hooks/useSort'; +} from '../../components/GenericTable'; +import { usePagination } from '../../components/GenericTable/hooks/usePagination'; +import { useSort } from '../../components/GenericTable/hooks/useSort'; import RemoveSlaButton from './RemoveSlaButton'; const SlaTable = ({ reload }: { reload: MutableRefObject<() => void> }) => { diff --git a/apps/meteor/ee/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx b/apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx similarity index 92% rename from apps/meteor/ee/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx rename to apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx index f0baca991e2e6..9f0a210868686 100644 --- a/apps/meteor/ee/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx +++ b/apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx @@ -4,8 +4,8 @@ import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { memo, useMemo, useState } from 'react'; -import { useRecordList } from '../../../../client/hooks/lists/useRecordList'; -import { AsyncStatePhase } from '../../../../client/hooks/useAsyncState'; +import { useRecordList } from '../../hooks/lists/useRecordList'; +import { AsyncStatePhase } from '../../hooks/useAsyncState'; import { useTagsList } from '../../hooks/useTagsList'; type AutoCompleteTagsMultipleProps = { diff --git a/apps/meteor/ee/client/omnichannel/tags/TagEdit.tsx b/apps/meteor/client/omnichannel/tags/TagEdit.tsx similarity index 96% rename from apps/meteor/ee/client/omnichannel/tags/TagEdit.tsx rename to apps/meteor/client/omnichannel/tags/TagEdit.tsx index cd423c60b4bff..767b56e916751 100644 --- a/apps/meteor/ee/client/omnichannel/tags/TagEdit.tsx +++ b/apps/meteor/client/omnichannel/tags/TagEdit.tsx @@ -6,7 +6,7 @@ import { useQueryClient } from '@tanstack/react-query'; import React from 'react'; import { useForm, Controller } from 'react-hook-form'; -import AutoCompleteDepartmentMultiple from '../../../../client/components/AutoCompleteDepartmentMultiple'; +import AutoCompleteDepartmentMultiple from '../../components/AutoCompleteDepartmentMultiple'; import { ContextualbarScrollableContent, ContextualbarFooter, @@ -14,7 +14,7 @@ import { Contextualbar, ContextualbarHeader, ContextualbarClose, -} from '../../../../client/components/Contextualbar'; +} from '../../components/Contextualbar'; import { useRemoveTag } from './useRemoveTag'; type TagEditPayload = { diff --git a/apps/meteor/ee/client/omnichannel/tags/TagEditWithData.tsx b/apps/meteor/client/omnichannel/tags/TagEditWithData.tsx similarity index 92% rename from apps/meteor/ee/client/omnichannel/tags/TagEditWithData.tsx rename to apps/meteor/client/omnichannel/tags/TagEditWithData.tsx index 95a902055ed60..ae9a3259af4ad 100644 --- a/apps/meteor/ee/client/omnichannel/tags/TagEditWithData.tsx +++ b/apps/meteor/client/omnichannel/tags/TagEditWithData.tsx @@ -4,7 +4,7 @@ import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import React from 'react'; -import { ContextualbarSkeleton } from '../../../../client/components/Contextualbar'; +import { ContextualbarSkeleton } from '../../components/Contextualbar'; import TagEdit from './TagEdit'; import TagEditWithDepartmentData from './TagEditWithDepartmentData'; diff --git a/apps/meteor/ee/client/omnichannel/tags/TagEditWithDepartmentData.tsx b/apps/meteor/client/omnichannel/tags/TagEditWithDepartmentData.tsx similarity index 92% rename from apps/meteor/ee/client/omnichannel/tags/TagEditWithDepartmentData.tsx rename to apps/meteor/client/omnichannel/tags/TagEditWithDepartmentData.tsx index 4a65c31263c2f..ce4707417d1e9 100644 --- a/apps/meteor/ee/client/omnichannel/tags/TagEditWithDepartmentData.tsx +++ b/apps/meteor/client/omnichannel/tags/TagEditWithDepartmentData.tsx @@ -4,7 +4,7 @@ import { useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import React from 'react'; -import { ContextualbarSkeleton } from '../../../../client/components/Contextualbar'; +import { ContextualbarSkeleton } from '../../components/Contextualbar'; import TagEdit from './TagEdit'; const TagEditWithDepartmentData = ({ tagData }: { tagData: ILivechatTag }) => { diff --git a/apps/meteor/ee/client/omnichannel/tags/TagsPage.tsx b/apps/meteor/client/omnichannel/tags/TagsPage.tsx similarity index 85% rename from apps/meteor/ee/client/omnichannel/tags/TagsPage.tsx rename to apps/meteor/client/omnichannel/tags/TagsPage.tsx index 63934c95e945b..add1c64531d9e 100644 --- a/apps/meteor/ee/client/omnichannel/tags/TagsPage.tsx +++ b/apps/meteor/client/omnichannel/tags/TagsPage.tsx @@ -2,8 +2,8 @@ import { Button, ButtonGroup } from '@rocket.chat/fuselage'; import { useRouter, useTranslation, useRouteParameter } from '@rocket.chat/ui-contexts'; import React from 'react'; -import { ContextualbarDialog } from '../../../../client/components/Contextualbar'; -import { Page, PageHeader, PageContent } from '../../../../client/components/Page'; +import { ContextualbarDialog } from '../../components/Contextualbar'; +import { Page, PageHeader, PageContent } from '../../components/Page'; import TagEdit from './TagEdit'; import TagEditWithData from './TagEditWithData'; import TagsTable from './TagsTable'; diff --git a/apps/meteor/ee/client/omnichannel/tags/TagsRoute.tsx b/apps/meteor/client/omnichannel/tags/TagsRoute.tsx similarity index 77% rename from apps/meteor/ee/client/omnichannel/tags/TagsRoute.tsx rename to apps/meteor/client/omnichannel/tags/TagsRoute.tsx index 4860b0aeab542..11f3e4348a0bb 100644 --- a/apps/meteor/ee/client/omnichannel/tags/TagsRoute.tsx +++ b/apps/meteor/client/omnichannel/tags/TagsRoute.tsx @@ -1,7 +1,7 @@ import { usePermission } from '@rocket.chat/ui-contexts'; import React from 'react'; -import NotAuthorizedPage from '../../../../client/views/notAuthorized/NotAuthorizedPage'; +import NotAuthorizedPage from '../../views/notAuthorized/NotAuthorizedPage'; import TagsPage from './TagsPage'; const TagsRoute = () => { diff --git a/apps/meteor/ee/client/omnichannel/tags/TagsTable.tsx b/apps/meteor/client/omnichannel/tags/TagsTable.tsx similarity index 91% rename from apps/meteor/ee/client/omnichannel/tags/TagsTable.tsx rename to apps/meteor/client/omnichannel/tags/TagsTable.tsx index ce89fe2d764b6..3b9757134bedd 100644 --- a/apps/meteor/ee/client/omnichannel/tags/TagsTable.tsx +++ b/apps/meteor/client/omnichannel/tags/TagsTable.tsx @@ -4,8 +4,8 @@ import { useTranslation, useEndpoint, useRouter } from '@rocket.chat/ui-contexts import { useQuery, hashQueryKey } from '@tanstack/react-query'; import React, { useMemo, useState } from 'react'; -import FilterByText from '../../../../client/components/FilterByText'; -import GenericNoResults from '../../../../client/components/GenericNoResults'; +import FilterByText from '../../components/FilterByText'; +import GenericNoResults from '../../components/GenericNoResults'; import { GenericTable, GenericTableRow, @@ -14,9 +14,9 @@ import { GenericTableHeaderCell, GenericTableBody, GenericTableLoadingRow, -} from '../../../../client/components/GenericTable'; -import { usePagination } from '../../../../client/components/GenericTable/hooks/usePagination'; -import { useSort } from '../../../../client/components/GenericTable/hooks/useSort'; +} from '../../components/GenericTable'; +import { usePagination } from '../../components/GenericTable/hooks/usePagination'; +import { useSort } from '../../components/GenericTable/hooks/useSort'; import { useRemoveTag } from './useRemoveTag'; const TagsTable = () => { diff --git a/apps/meteor/ee/client/omnichannel/tags/useRemoveTag.tsx b/apps/meteor/client/omnichannel/tags/useRemoveTag.tsx similarity index 94% rename from apps/meteor/ee/client/omnichannel/tags/useRemoveTag.tsx rename to apps/meteor/client/omnichannel/tags/useRemoveTag.tsx index d1a5c60968907..31c3a910c7ea8 100644 --- a/apps/meteor/ee/client/omnichannel/tags/useRemoveTag.tsx +++ b/apps/meteor/client/omnichannel/tags/useRemoveTag.tsx @@ -3,7 +3,7 @@ import { useSetModal, useToastMessageDispatch, useRouter, useMethod, useTranslat import { useQueryClient } from '@tanstack/react-query'; import React from 'react'; -import GenericModal from '../../../../client/components/GenericModal'; +import GenericModal from '../../components/GenericModal'; export const useRemoveTag = () => { const t = useTranslation(); diff --git a/apps/meteor/ee/client/omnichannel/units/UnitEdit.tsx b/apps/meteor/client/omnichannel/units/UnitEdit.tsx similarity index 96% rename from apps/meteor/ee/client/omnichannel/units/UnitEdit.tsx rename to apps/meteor/client/omnichannel/units/UnitEdit.tsx index b618d2eac89e3..e4bc1c0efb505 100644 --- a/apps/meteor/ee/client/omnichannel/units/UnitEdit.tsx +++ b/apps/meteor/client/omnichannel/units/UnitEdit.tsx @@ -27,11 +27,11 @@ import { Contextualbar, ContextualbarHeader, ContextualbarClose, -} from '../../../../client/components/Contextualbar'; -import { useRecordList } from '../../../../client/hooks/lists/useRecordList'; -import { AsyncStatePhase } from '../../../../client/hooks/useAsyncState'; -import { useDepartmentsByUnitsList } from '../../../../client/views/hooks/useDepartmentsByUnitsList'; -import { useMonitorsList } from '../../../../client/views/hooks/useMonitorsList'; +} from '../../components/Contextualbar'; +import { useRecordList } from '../../hooks/lists/useRecordList'; +import { AsyncStatePhase } from '../../hooks/useAsyncState'; +import { useDepartmentsByUnitsList } from '../../views/hooks/useDepartmentsByUnitsList'; +import { useMonitorsList } from '../../views/hooks/useMonitorsList'; import { useRemoveUnit } from './useRemoveUnit'; type UnitEditProps = { diff --git a/apps/meteor/ee/client/omnichannel/units/UnitEditWithData.tsx b/apps/meteor/client/omnichannel/units/UnitEditWithData.tsx similarity index 95% rename from apps/meteor/ee/client/omnichannel/units/UnitEditWithData.tsx rename to apps/meteor/client/omnichannel/units/UnitEditWithData.tsx index 6f30453f62dc3..127c81c30032f 100644 --- a/apps/meteor/ee/client/omnichannel/units/UnitEditWithData.tsx +++ b/apps/meteor/client/omnichannel/units/UnitEditWithData.tsx @@ -4,7 +4,7 @@ import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import React from 'react'; -import { ContextualbarSkeleton } from '../../../../client/components/Contextualbar'; +import { ContextualbarSkeleton } from '../../components/Contextualbar'; import UnitEdit from './UnitEdit'; const UnitEditWithData = ({ unitId }: { unitId: IOmnichannelBusinessUnit['_id'] }) => { diff --git a/apps/meteor/ee/client/omnichannel/units/UnitsPage.tsx b/apps/meteor/client/omnichannel/units/UnitsPage.tsx similarity index 86% rename from apps/meteor/ee/client/omnichannel/units/UnitsPage.tsx rename to apps/meteor/client/omnichannel/units/UnitsPage.tsx index b3d900830e2f4..190c22991fb53 100644 --- a/apps/meteor/ee/client/omnichannel/units/UnitsPage.tsx +++ b/apps/meteor/client/omnichannel/units/UnitsPage.tsx @@ -2,8 +2,8 @@ import { Button, ButtonGroup } from '@rocket.chat/fuselage'; import { useTranslation, useRouteParameter, useRouter } from '@rocket.chat/ui-contexts'; import React from 'react'; -import { ContextualbarDialog } from '../../../../client/components/Contextualbar'; -import { Page, PageHeader, PageContent } from '../../../../client/components/Page'; +import { ContextualbarDialog } from '../../components/Contextualbar'; +import { Page, PageHeader, PageContent } from '../../components/Page'; import UnitEdit from './UnitEdit'; import UnitEditWithData from './UnitEditWithData'; import UnitsTable from './UnitsTable'; diff --git a/apps/meteor/ee/client/omnichannel/units/UnitsRoute.tsx b/apps/meteor/client/omnichannel/units/UnitsRoute.tsx similarity index 84% rename from apps/meteor/ee/client/omnichannel/units/UnitsRoute.tsx rename to apps/meteor/client/omnichannel/units/UnitsRoute.tsx index 9afa82c171be1..9490fd2d2048e 100644 --- a/apps/meteor/ee/client/omnichannel/units/UnitsRoute.tsx +++ b/apps/meteor/client/omnichannel/units/UnitsRoute.tsx @@ -1,8 +1,8 @@ import { usePermission } from '@rocket.chat/ui-contexts'; import React from 'react'; -import NotAuthorizedPage from '../../../../client/views/notAuthorized/NotAuthorizedPage'; import { useHasLicenseModule } from '../../hooks/useHasLicenseModule'; +import NotAuthorizedPage from '../../views/notAuthorized/NotAuthorizedPage'; import UnitsPage from './UnitsPage'; const UnitsRoute = () => { diff --git a/apps/meteor/ee/client/omnichannel/units/UnitsTable.tsx b/apps/meteor/client/omnichannel/units/UnitsTable.tsx similarity index 91% rename from apps/meteor/ee/client/omnichannel/units/UnitsTable.tsx rename to apps/meteor/client/omnichannel/units/UnitsTable.tsx index c88fb4ce4c677..fe95bc90d8a20 100644 --- a/apps/meteor/ee/client/omnichannel/units/UnitsTable.tsx +++ b/apps/meteor/client/omnichannel/units/UnitsTable.tsx @@ -4,8 +4,8 @@ import { useEndpoint, useRouter, useTranslation } from '@rocket.chat/ui-contexts import { useQuery, hashQueryKey } from '@tanstack/react-query'; import React, { useMemo, useState } from 'react'; -import FilterByText from '../../../../client/components/FilterByText'; -import GenericNoResults from '../../../../client/components/GenericNoResults/GenericNoResults'; +import FilterByText from '../../components/FilterByText'; +import GenericNoResults from '../../components/GenericNoResults/GenericNoResults'; import { GenericTable, GenericTableHeader, @@ -14,9 +14,9 @@ import { GenericTableCell, GenericTableLoadingRow, GenericTableRow, -} from '../../../../client/components/GenericTable'; -import { usePagination } from '../../../../client/components/GenericTable/hooks/usePagination'; -import { useSort } from '../../../../client/components/GenericTable/hooks/useSort'; +} from '../../components/GenericTable'; +import { usePagination } from '../../components/GenericTable/hooks/usePagination'; +import { useSort } from '../../components/GenericTable/hooks/useSort'; import { useRemoveUnit } from './useRemoveUnit'; const UnitsTable = () => { diff --git a/apps/meteor/ee/client/omnichannel/units/useRemoveUnit.tsx b/apps/meteor/client/omnichannel/units/useRemoveUnit.tsx similarity index 94% rename from apps/meteor/ee/client/omnichannel/units/useRemoveUnit.tsx rename to apps/meteor/client/omnichannel/units/useRemoveUnit.tsx index 61910dc37bc41..48eca28eb833f 100644 --- a/apps/meteor/ee/client/omnichannel/units/useRemoveUnit.tsx +++ b/apps/meteor/client/omnichannel/units/useRemoveUnit.tsx @@ -3,7 +3,7 @@ import { useSetModal, useToastMessageDispatch, useMethod, useTranslation, useRou import { useQueryClient } from '@tanstack/react-query'; import React from 'react'; -import GenericModal from '../../../../client/components/GenericModal'; +import GenericModal from '../../components/GenericModal'; export const useRemoveUnit = () => { const t = useTranslation(); diff --git a/apps/meteor/client/providers/AppsProvider/AppsProvider.spec.ts b/apps/meteor/client/providers/AppsProvider/AppsProvider.spec.ts new file mode 100644 index 0000000000000..8210fe22eec53 --- /dev/null +++ b/apps/meteor/client/providers/AppsProvider/AppsProvider.spec.ts @@ -0,0 +1,39 @@ +import type { App } from '@rocket.chat/core-typings'; +import type { UseQueryResult } from '@tanstack/react-query'; + +import { createFakeApp } from '../../../tests/mocks/data'; +import { createFakeAppInstalledMarketplace, createFakeAppPrivate } from '../../../tests/mocks/data/marketplace'; +import { storeQueryFunction } from './storeQueryFunction'; + +describe(`when an app installed from the Marketplace, but has since been unpublished`, () => { + it(`should still be present in the installed app data provided`, () => { + const marketplaceMockQuery = { + data: [createFakeApp({ id: 'marketplace-1' }), createFakeAppInstalledMarketplace({ id: 'marketplace-2' })], + isFetched: true, + } as unknown as UseQueryResult; + + const instanceMockQuery = { + data: [ + marketplaceMockQuery.data?.[1], + createFakeAppInstalledMarketplace({ id: 'marketplace-3' }), // This app has been installed via Marketplace but has been unpublished since + createFakeAppPrivate({ id: 'private-1' }), + ], + isFetched: true, + } as unknown as UseQueryResult; + + const [marketplaceList, installedList, privateList] = storeQueryFunction(marketplaceMockQuery, instanceMockQuery); + + expect(marketplaceList.find((app) => app.id === 'marketplace-1')).toBeTruthy(); + expect(marketplaceList.find((app) => app.id === 'marketplace-2')).toBeTruthy(); + expect(marketplaceList.find((app) => app.id === 'marketplace-3')).toBeUndefined(); + expect(marketplaceList).toHaveLength(2); + + expect(installedList.find((app) => app.id === 'marketplace-1')).toBeUndefined(); + expect(installedList.find((app) => app.id === 'marketplace-2')).toBeTruthy(); + expect(installedList.find((app) => app.id === 'marketplace-3')).toBeTruthy(); + expect(installedList).toHaveLength(2); + + expect(privateList.find((app) => app.id === 'private-1')).toBeTruthy(); + expect(privateList).toHaveLength(1); + }); +}); diff --git a/apps/meteor/client/providers/AppsProvider.tsx b/apps/meteor/client/providers/AppsProvider/AppsProvider.tsx similarity index 58% rename from apps/meteor/client/providers/AppsProvider.tsx rename to apps/meteor/client/providers/AppsProvider/AppsProvider.tsx index e521a05cafdcb..cf1d4d671d94b 100644 --- a/apps/meteor/client/providers/AppsProvider.tsx +++ b/apps/meteor/client/providers/AppsProvider/AppsProvider.tsx @@ -4,16 +4,15 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import type { ReactNode } from 'react'; import React, { useEffect } from 'react'; -import { AppClientOrchestratorInstance } from '../../ee/client/apps/orchestrator'; -import { AppsContext } from '../contexts/AppsContext'; -import { useIsEnterprise } from '../hooks/useIsEnterprise'; -import { useInvalidateLicense } from '../hooks/useLicense'; -import type { AsyncState } from '../lib/asyncState'; -import { AsyncStatePhase } from '../lib/asyncState'; -import { useInvalidateAppsCountQueryCallback } from '../views/marketplace/hooks/useAppsCountQuery'; -import type { App } from '../views/marketplace/types'; - -const sortByName = (apps: App[]): App[] => apps.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1)); +import { AppClientOrchestratorInstance } from '../../apps/orchestrator'; +import { AppsContext } from '../../contexts/AppsContext'; +import { useIsEnterprise } from '../../hooks/useIsEnterprise'; +import { useInvalidateLicense } from '../../hooks/useLicense'; +import type { AsyncState } from '../../lib/asyncState'; +import { AsyncStatePhase } from '../../lib/asyncState'; +import { useInvalidateAppsCountQueryCallback } from '../../views/marketplace/hooks/useAppsCountQuery'; +import type { App } from '../../views/marketplace/types'; +import { storeQueryFunction } from './storeQueryFunction'; const getAppState = ( loading: boolean, @@ -96,60 +95,10 @@ const AppsProvider = ({ children }: AppsProviderProps) => { }, ); - const store = useQuery( - ['marketplace', 'apps-stored', instance.data, marketplace.data], - () => { - if (!marketplace.isFetched && !instance.isFetched) { - throw new Error('Apps not loaded'); - } - - const marketplaceApps: App[] = []; - const installedApps: App[] = []; - const privateApps: App[] = []; - const clonedData = [...(instance.data || [])]; - - sortByName(marketplace.data || []).forEach((app) => { - const appIndex = clonedData.findIndex(({ id }) => id === app.id); - const [installedApp] = appIndex > -1 ? clonedData.splice(appIndex, 1) : []; - - const record = { - ...app, - ...(installedApp && { - private: installedApp.private, - installed: true, - status: installedApp.status, - version: installedApp.version, - licenseValidation: installedApp.licenseValidation, - migrated: installedApp.migrated, - }), - bundledIn: app.bundledIn, - marketplaceVersion: app.version, - }; - - if (installedApp) { - if (installedApp.private) { - privateApps.push(record); - } else { - installedApps.push(record); - } - } - - marketplaceApps.push(record); - }); - - sortByName(clonedData).forEach((app) => { - if (app.private) { - privateApps.push(app); - } - }); - - return [marketplaceApps, installedApps, privateApps]; - }, - { - enabled: marketplace.isFetched && instance.isFetched, - keepPreviousData: true, - }, - ); + const store = useQuery(['marketplace', 'apps-stored', instance.data, marketplace.data], () => storeQueryFunction(marketplace, instance), { + enabled: marketplace.isFetched && instance.isFetched, + keepPreviousData: true, + }); const [marketplaceAppsData, installedAppsData, privateAppsData] = store.data || []; const { isLoading } = store; diff --git a/apps/meteor/client/providers/AppsProvider/index.ts b/apps/meteor/client/providers/AppsProvider/index.ts new file mode 100644 index 0000000000000..94ae81e87e4cb --- /dev/null +++ b/apps/meteor/client/providers/AppsProvider/index.ts @@ -0,0 +1,3 @@ +import AppsProvider from './AppsProvider'; + +export default AppsProvider; diff --git a/apps/meteor/client/providers/AppsProvider/storeQueryFunction.ts b/apps/meteor/client/providers/AppsProvider/storeQueryFunction.ts new file mode 100644 index 0000000000000..a7c9d21f4df6c --- /dev/null +++ b/apps/meteor/client/providers/AppsProvider/storeQueryFunction.ts @@ -0,0 +1,64 @@ +import { type UseQueryResult } from '@tanstack/react-query'; + +import type { App } from '../../views/marketplace/types'; + +const sortByName = (apps: App[]): App[] => apps.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1)); + +/** + * Aggregates result data from marketplace request and instance installed into their appropriate lists + * + * Exporting for better testing + */ +export function storeQueryFunction( + marketplace: UseQueryResult, + instance: UseQueryResult, +): [App[], App[], App[]] { + if (!marketplace.isFetched && !instance.isFetched) { + throw new Error('Apps not loaded'); + } + + const marketplaceApps: App[] = []; + const installedApps: App[] = []; + const privateApps: App[] = []; + const clonedData = [...(instance.data || [])]; + + sortByName(marketplace.data || []).forEach((app) => { + const appIndex = clonedData.findIndex(({ id }) => id === app.id); + const [installedApp] = appIndex > -1 ? clonedData.splice(appIndex, 1) : []; + + const record = { + ...app, + ...(installedApp && { + private: installedApp.private, + installed: true, + status: installedApp.status, + version: installedApp.version, + licenseValidation: installedApp.licenseValidation, + migrated: installedApp.migrated, + }), + bundledIn: app.bundledIn, + marketplaceVersion: app.version, + }; + + if (installedApp) { + if (installedApp.private) { + privateApps.push(record); + } else { + installedApps.push(record); + } + } + + marketplaceApps.push(record); + }); + + sortByName(clonedData).forEach((app) => { + if (app.private) { + privateApps.push(app); + return; + } + + installedApps.push(app); + }); + + return [marketplaceApps, installedApps, privateApps]; +} diff --git a/apps/meteor/client/providers/CallProvider/CallProvider.tsx b/apps/meteor/client/providers/CallProvider/CallProvider.tsx index 0ede8072d8bc6..38b7c12791cd2 100644 --- a/apps/meteor/client/providers/CallProvider/CallProvider.tsx +++ b/apps/meteor/client/providers/CallProvider/CallProvider.tsx @@ -28,14 +28,14 @@ import React, { useMemo, useRef, useCallback, useEffect, useState } from 'react' import { createPortal } from 'react-dom'; import type { OutgoingByeRequest } from 'sip.js/lib/core'; -import { isOutboundClient, useVoipClient } from '../../../ee/client/hooks/useVoipClient'; -import { WrapUpCallModal } from '../../../ee/client/voip/components/modals/WrapUpCallModal'; import type { CallContextValue } from '../../contexts/CallContext'; import { CallContext, useIsVoipEnterprise } from '../../contexts/CallContext'; import { useDialModal } from '../../hooks/useDialModal'; +import { isOutboundClient, useVoipClient } from '../../hooks/useVoipClient'; import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; import type { QueueAggregator } from '../../lib/voip/QueueAggregator'; import { parseOutboundPhoneNumber } from '../../lib/voip/parseOutboundPhoneNumber'; +import { WrapUpCallModal } from '../../voip/components/modals/WrapUpCallModal'; import { useVoipSounds } from './hooks/useVoipSounds'; type NetworkState = 'online' | 'offline'; diff --git a/apps/meteor/client/providers/EmojiPickerProvider/useUpdateCustomEmoji.ts b/apps/meteor/client/providers/EmojiPickerProvider/useUpdateCustomEmoji.ts index a0a0946006dbb..67d9f5bd20780 100644 --- a/apps/meteor/client/providers/EmojiPickerProvider/useUpdateCustomEmoji.ts +++ b/apps/meteor/client/providers/EmojiPickerProvider/useUpdateCustomEmoji.ts @@ -1,11 +1,16 @@ -import { useStream } from '@rocket.chat/ui-contexts'; +import { useStream, useUserId } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; import { updateEmojiCustom, deleteEmojiCustom } from '../../../app/emoji-custom/client/lib/emojiCustom'; export const useUpdateCustomEmoji = () => { const notify = useStream('notify-logged'); + const uid = useUserId(); useEffect(() => { + if (!uid) { + return; + } + const unsubUpdate = notify('updateEmojiCustom', (data) => updateEmojiCustom(data.emojiData)); const unsubDelete = notify('deleteEmojiCustom', (data) => deleteEmojiCustom(data.emojiData)); @@ -13,5 +18,5 @@ export const useUpdateCustomEmoji = () => { unsubUpdate(); unsubDelete(); }; - }, [notify]); + }, [notify, uid]); }; diff --git a/apps/meteor/client/providers/OmnichannelProvider.tsx b/apps/meteor/client/providers/OmnichannelProvider.tsx index 47ccb3d39c880..881275e2fc2bb 100644 --- a/apps/meteor/client/providers/OmnichannelProvider.tsx +++ b/apps/meteor/client/providers/OmnichannelProvider.tsx @@ -14,10 +14,10 @@ import { LivechatInquiry } from '../../app/livechat/client/collections/LivechatI 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 { useHasLicenseModule } from '../../ee/client/hooks/useHasLicenseModule'; import { ClientLogger } from '../../lib/ClientLogger'; import type { OmnichannelContextValue } from '../contexts/OmnichannelContext'; import { OmnichannelContext } from '../contexts/OmnichannelContext'; +import { useHasLicenseModule } from '../hooks/useHasLicenseModule'; import { useReactiveValue } from '../hooks/useReactiveValue'; import { useShouldPreventAction } from '../hooks/useShouldPreventAction'; diff --git a/apps/meteor/client/providers/TranslationProvider.tsx b/apps/meteor/client/providers/TranslationProvider.tsx index 2f31d282a63b7..0c0ef00b5ac08 100644 --- a/apps/meteor/client/providers/TranslationProvider.tsx +++ b/apps/meteor/client/providers/TranslationProvider.tsx @@ -22,7 +22,7 @@ import { defaultTranslationNamespace, extractTranslationNamespaces, } from '../../app/utils/lib/i18n'; -import { AppClientOrchestratorInstance } from '../../ee/client/apps/orchestrator'; +import { AppClientOrchestratorInstance } from '../apps/orchestrator'; import { isRTLScriptLanguage } from '../lib/utils/isRTLScriptLanguage'; i18n.use(I18NextHttpBackend).use(initReactI18next); diff --git a/apps/meteor/client/providers/UserProvider/hooks/useDeleteUser.ts b/apps/meteor/client/providers/UserProvider/hooks/useDeleteUser.ts index e86fe9951a264..db7a7d548f9e5 100644 --- a/apps/meteor/client/providers/UserProvider/hooks/useDeleteUser.ts +++ b/apps/meteor/client/providers/UserProvider/hooks/useDeleteUser.ts @@ -1,4 +1,4 @@ -import { useStream } from '@rocket.chat/ui-contexts'; +import { useStream, useUserId } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; import { ChatMessage } from '../../../../app/models/client'; @@ -6,7 +6,11 @@ import { ChatMessage } from '../../../../app/models/client'; export const useDeleteUser = () => { const notify = useStream('notify-logged'); + const uid = useUserId(); useEffect(() => { + if (!uid) { + return; + } return notify('Users:Deleted', ({ userId, messageErasureType, replaceByUser }) => { if (messageErasureType === 'Unlink' && replaceByUser) { return ChatMessage.update( @@ -28,5 +32,5 @@ export const useDeleteUser = () => { 'u._id': userId, }); }); - }, [notify]); + }, [notify, uid]); }; diff --git a/apps/meteor/client/providers/UserProvider/hooks/useUpdateAvatar.ts b/apps/meteor/client/providers/UserProvider/hooks/useUpdateAvatar.ts index 292880e23da8c..0be90cd608b88 100644 --- a/apps/meteor/client/providers/UserProvider/hooks/useUpdateAvatar.ts +++ b/apps/meteor/client/providers/UserProvider/hooks/useUpdateAvatar.ts @@ -1,15 +1,19 @@ -import { useStream } from '@rocket.chat/ui-contexts'; +import { useUserId, useStream } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { useEffect } from 'react'; export const useUpdateAvatar = () => { const notify = useStream('notify-logged'); + const uid = useUserId(); useEffect(() => { + if (!uid) { + return; + } return notify('updateAvatar', (data) => { if ('username' in data) { const { username, etag } = data; username && Meteor.users.update({ username }, { $set: { avatarETag: etag } }); } }); - }, [notify]); + }, [notify, uid]); }; diff --git a/apps/meteor/client/serviceWorker.ts b/apps/meteor/client/serviceWorker.ts new file mode 100644 index 0000000000000..7d98747e59f78 --- /dev/null +++ b/apps/meteor/client/serviceWorker.ts @@ -0,0 +1,40 @@ +const KEY = 'sw_last_reload'; +const RELOAD_WINDOW = 1000 * 10; + +function reload() { + const lastReload = localStorage.getItem(KEY); + + if (lastReload) { + const last = Date.parse(lastReload); + + if (!isNaN(last)) { + const elapsed = Date.now() - last; + + if (elapsed < RELOAD_WINDOW) { + return; + } + } + } + + localStorage.setItem(KEY, new Date().toISOString()); + console.log('service worker: reloading to activate'); + window.location.reload(); +} + +if ('serviceWorker' in navigator) { + navigator.serviceWorker + .register('/enc.js', { + scope: '/', + }) + .then((reg) => { + if (reg.active) { + console.log('service worker: installed'); + if (!navigator.serviceWorker.controller) { + reload(); + } + } + }) + .catch((err) => { + console.log(`registration failed: ${err}`); + }); +} diff --git a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx index 4c51b8a3615b8..f9ec077e9e430 100644 --- a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx +++ b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx @@ -6,9 +6,9 @@ import { useLayout } from '@rocket.chat/ui-contexts'; import type { AllHTMLAttributes, ComponentType, ReactElement, ReactNode } from 'react'; import React, { memo, useMemo } from 'react'; -import { useOmnichannelPriorities } from '../../../ee/client/omnichannel/hooks/useOmnichannelPriorities'; import { RoomIcon } from '../../components/RoomIcon'; import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; +import { useOmnichannelPriorities } from '../../omnichannel/hooks/useOmnichannelPriorities'; import RoomMenu from '../RoomMenu'; import { OmnichannelBadges } from '../badges/OmnichannelBadges'; import type { useAvatarTemplate } from '../hooks/useAvatarTemplate'; diff --git a/apps/meteor/client/sidebar/RoomMenu.tsx b/apps/meteor/client/sidebar/RoomMenu.tsx index da9908b3ed5f7..8df55bd5d3594 100644 --- a/apps/meteor/client/sidebar/RoomMenu.tsx +++ b/apps/meteor/client/sidebar/RoomMenu.tsx @@ -18,11 +18,11 @@ import React, { memo, useMemo } from 'react'; import { LegacyRoomManager } from '../../app/ui-utils/client'; import { UiTextContext } from '../../definition/IRoomTypeConfig'; -import { useOmnichannelPrioritiesMenu } from '../../ee/client/omnichannel/hooks/useOmnichannelPrioritiesMenu'; import { GenericModalDoNotAskAgain } from '../components/GenericModal'; import WarningModal from '../components/WarningModal'; import { useDontAskAgain } from '../hooks/useDontAskAgain'; import { roomCoordinator } from '../lib/rooms/roomCoordinator'; +import { useOmnichannelPrioritiesMenu } from '../omnichannel/hooks/useOmnichannelPrioritiesMenu'; const fields: Fields = { f: true, diff --git a/apps/meteor/client/sidebar/badges/OmnichannelBadges.tsx b/apps/meteor/client/sidebar/badges/OmnichannelBadges.tsx index 32fff81d7bb38..9f2580b74b773 100644 --- a/apps/meteor/client/sidebar/badges/OmnichannelBadges.tsx +++ b/apps/meteor/client/sidebar/badges/OmnichannelBadges.tsx @@ -2,9 +2,9 @@ import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import React from 'react'; -import { RoomActivityIcon } from '../../../ee/client/omnichannel/components/RoomActivityIcon'; -import { useOmnichannelPriorities } from '../../../ee/client/omnichannel/hooks/useOmnichannelPriorities'; -import { PriorityIcon } from '../../../ee/client/omnichannel/priorities/PriorityIcon'; +import { RoomActivityIcon } from '../../omnichannel/components/RoomActivityIcon'; +import { useOmnichannelPriorities } from '../../omnichannel/hooks/useOmnichannelPriorities'; +import { PriorityIcon } from '../../omnichannel/priorities/PriorityIcon'; export const OmnichannelBadges = ({ room }: { room: ISubscription & IRoom }) => { const { enabled: isPriorityEnabled } = useOmnichannelPriorities(); diff --git a/apps/meteor/client/sidebar/footer/SidebarFooterDefault.tsx b/apps/meteor/client/sidebar/footer/SidebarFooterDefault.tsx index 7ecc7b8ab96d9..fbf987fa78af2 100644 --- a/apps/meteor/client/sidebar/footer/SidebarFooterDefault.tsx +++ b/apps/meteor/client/sidebar/footer/SidebarFooterDefault.tsx @@ -5,7 +5,7 @@ import { useThemeMode } from '@rocket.chat/ui-theming/src/hooks/useThemeMode'; import type { ReactElement } from 'react'; import React from 'react'; -import { SidebarFooterWatermark } from '../../../ee/client/sidebar/footer/SidebarFooterWatermark'; +import { SidebarFooterWatermark } from './SidebarFooterWatermark'; const SidebarFooterDefault = (): ReactElement => { const [, , theme] = useThemeMode(); diff --git a/apps/meteor/ee/client/sidebar/footer/SidebarFooterWatermark.tsx b/apps/meteor/client/sidebar/footer/SidebarFooterWatermark.tsx similarity index 91% rename from apps/meteor/ee/client/sidebar/footer/SidebarFooterWatermark.tsx rename to apps/meteor/client/sidebar/footer/SidebarFooterWatermark.tsx index 88b872ed46a31..bf7736b5899a4 100644 --- a/apps/meteor/ee/client/sidebar/footer/SidebarFooterWatermark.tsx +++ b/apps/meteor/client/sidebar/footer/SidebarFooterWatermark.tsx @@ -3,7 +3,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import { useLicense, useLicenseName } from '../../../../client/hooks/useLicense'; +import { useLicense, useLicenseName } from '../../hooks/useLicense'; export const SidebarFooterWatermark = (): ReactElement | null => { const t = useTranslation(); diff --git a/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx b/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx index 0b5ef3418e01a..119476ecd89a1 100644 --- a/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx +++ b/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx @@ -6,8 +6,8 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, MouseEvent, ReactNode } from 'react'; import React from 'react'; -import type { VoipFooterMenuOptions } from '../../../../ee/client/hooks/useVoipFooterMenu'; import type { CallActionsType } from '../../../contexts/CallContext'; +import type { VoipFooterMenuOptions } from '../../../hooks/useVoipFooterMenu'; import { useOmnichannelContactLabel } from './hooks/useOmnichannelContactLabel'; type VoipFooterPropsType = { diff --git a/apps/meteor/client/sidebar/footer/voip/index.tsx b/apps/meteor/client/sidebar/footer/voip/index.tsx index 9ae7d91c7c2b8..9f353628c982a 100644 --- a/apps/meteor/client/sidebar/footer/voip/index.tsx +++ b/apps/meteor/client/sidebar/footer/voip/index.tsx @@ -3,7 +3,6 @@ import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback, useState } from 'react'; -import { useVoipFooterMenu } from '../../../../ee/client/hooks/useVoipFooterMenu'; import { useCallActions, useCallCreateRoom, @@ -14,6 +13,7 @@ import { useQueueCounter, useQueueName, } from '../../../contexts/CallContext'; +import { useVoipFooterMenu } from '../../../hooks/useVoipFooterMenu'; import SidebarFooterDefault from '../SidebarFooterDefault'; import { VoipFooter as VoipFooterComponent } from './VoipFooter'; diff --git a/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx b/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx index 5738798f194e7..c45d300dc4c7d 100644 --- a/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx +++ b/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx @@ -27,8 +27,8 @@ import type { ComponentProps, ReactElement } from 'react'; import React, { useEffect, useMemo } from 'react'; import { useForm, Controller } from 'react-hook-form'; -import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule'; import UserAutoCompleteMultipleFederated from '../../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; +import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; import { goToRoomById } from '../../../lib/utils/goToRoomById'; import { useEncryptedRoomDescription } from '../hooks/useEncryptedRoomDescription'; @@ -66,7 +66,7 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal const namesValidation = useSetting('UTF8_Channel_Names_Validation'); const allowSpecialNames = useSetting('UI_Allow_room_names_with_special_chars'); const federationEnabled = useSetting('Federation_Matrix_enabled') || false; - const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms'); + const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms') && e2eEnabled; const canCreateChannel = usePermission('create-c'); const canCreatePrivateChannel = usePermission('create-p'); diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx index f506255806bfd..8e26407113478 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx @@ -1,7 +1,7 @@ import { useTranslation, useRoute, usePermission } from '@rocket.chat/ui-contexts'; -import { useHasLicenseModule } from '../../../../../ee/client/hooks/useHasLicenseModule'; import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; +import { useHasLicenseModule } from '../../../../hooks/useHasLicenseModule'; /** * @deprecated Feature preview diff --git a/apps/meteor/ee/client/startup/audit.tsx b/apps/meteor/client/startup/audit.tsx similarity index 79% rename from apps/meteor/ee/client/startup/audit.tsx rename to apps/meteor/client/startup/audit.tsx index 0f1e8b59ec255..99dfff9db3c1a 100644 --- a/apps/meteor/ee/client/startup/audit.tsx +++ b/apps/meteor/client/startup/audit.tsx @@ -1,11 +1,11 @@ import React, { lazy } from 'react'; -import { hasAllPermission } from '../../../app/authorization/client'; -import { appLayout } from '../../../client/lib/appLayout'; -import { router } from '../../../client/providers/RouterProvider'; -import NotAuthorizedPage from '../../../client/views/notAuthorized/NotAuthorizedPage'; -import MainLayout from '../../../client/views/root/MainLayout'; +import { hasAllPermission } from '../../app/authorization/client'; +import { appLayout } from '../lib/appLayout'; import { onToggledFeature } from '../lib/onToggledFeature'; +import { router } from '../providers/RouterProvider'; +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')); diff --git a/apps/meteor/ee/client/startup/deviceManagement.ts b/apps/meteor/client/startup/deviceManagement.ts similarity index 93% rename from apps/meteor/ee/client/startup/deviceManagement.ts rename to apps/meteor/client/startup/deviceManagement.ts index 3c22aa14384bf..f34a9796c304b 100644 --- a/apps/meteor/ee/client/startup/deviceManagement.ts +++ b/apps/meteor/client/startup/deviceManagement.ts @@ -1,7 +1,7 @@ import { lazy } from 'react'; -import { registerAccountRoute, registerAccountSidebarItem, unregisterSidebarItem } from '../../../client/views/account'; import { onToggledFeature } from '../lib/onToggledFeature'; +import { registerAccountRoute, registerAccountSidebarItem, unregisterSidebarItem } from '../views/account'; declare module '@rocket.chat/ui-contexts' { interface IRouterPaths { diff --git a/apps/meteor/client/startup/e2e.ts b/apps/meteor/client/startup/e2e.ts index f9cf156f8d8b3..de615e8f45deb 100644 --- a/apps/meteor/client/startup/e2e.ts +++ b/apps/meteor/client/startup/e2e.ts @@ -1,11 +1,12 @@ -import type { AtLeast, IMessage, ISubscription } from '@rocket.chat/core-typings'; +import type { IMessage } from '@rocket.chat/core-typings'; +import { isE2EEPinnedMessage } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; +import { E2EEState } from '../../app/e2e/client/E2EEState'; import { e2e } from '../../app/e2e/client/rocketchat.e2e'; -import { Subscriptions, ChatRoom } from '../../app/models/client'; +import { ChatRoom } from '../../app/models/client'; import { settings } from '../../app/settings/client'; -import { sdk } from '../../app/utils/client/lib/SDKClient'; import { onClientBeforeSendMessage } from '../lib/onClientBeforeSendMessage'; import { onClientMessageReceived } from '../lib/onClientMessageReceived'; import { isLayoutEmbedded } from '../lib/utils/isLayoutEmbedded'; @@ -15,10 +16,12 @@ import { router } from '../providers/RouterProvider'; Meteor.startup(() => { Tracker.autorun(() => { if (!Meteor.userId()) { + e2e.log('Not logged in'); return; } if (!window.crypto) { + e2e.error('No crypto support'); return; } @@ -27,102 +30,48 @@ Meteor.startup(() => { const adminEmbedded = isLayoutEmbedded() && router.getLocationPathname().startsWith('/admin'); if (enabled && !adminEmbedded) { + e2e.log('E2E enabled starting client'); e2e.startClient(); - e2e.enabled.set(true); } else { - e2e.enabled.set(false); + e2e.log('E2E disabled'); + e2e.setState(E2EEState.DISABLED); e2e.closeAlert(); } }); - let observable: Meteor.LiveQueryHandle | null = null; let offClientMessageReceived: undefined | (() => void); let offClientBeforeSendMessage: undefined | (() => void); - let unsubNotifyUser: undefined | (() => void); + let listenersAttached = false; + Tracker.autorun(() => { if (!e2e.isReady()) { + e2e.log('Not ready'); offClientMessageReceived?.(); - unsubNotifyUser?.(); - unsubNotifyUser = undefined; - observable?.stop(); offClientBeforeSendMessage?.(); + listenersAttached = false; return; } - unsubNotifyUser = sdk.stream('notify-user', [`${Meteor.userId()}/e2ekeyRequest`], async (roomId, keyId): Promise => { - const e2eRoom = await e2e.getInstanceByRoomId(roomId); - if (!e2eRoom) { - return; - } - - e2eRoom.provideKeyToUser(keyId); - }).stop; - - observable = Subscriptions.find().observe({ - changed: async (sub: ISubscription) => { - setTimeout(async () => { - if (!sub.encrypted && !sub.E2EKey) { - e2e.removeInstanceByRoomId(sub.rid); - return; - } - - const e2eRoom = await e2e.getInstanceByRoomId(sub.rid); - if (!e2eRoom) { - return; - } - - if (sub.E2ESuggestedKey) { - if (await e2eRoom.importGroupKey(sub.E2ESuggestedKey)) { - e2e.acceptSuggestedKey(sub.rid); - } else { - console.warn('Invalid E2ESuggestedKey, rejecting', sub.E2ESuggestedKey); - e2e.rejectSuggestedKey(sub.rid); - } - } - - sub.encrypted ? e2eRoom.resume() : e2eRoom.pause(); - - // Cover private groups and direct messages - if (!e2eRoom.isSupportedRoomType(sub.t)) { - e2eRoom.disable(); - return; - } - - if (sub.E2EKey && e2eRoom.isWaitingKeys()) { - e2eRoom.keyReceived(); - return; - } - - if (!e2eRoom.isReady()) { - return; - } - - e2eRoom.decryptSubscription(); - }, 0); - }, - added: async (sub: ISubscription) => { - setTimeout(async () => { - if (!sub.encrypted && !sub.E2EKey) { - return; - } - return e2e.getInstanceByRoomId(sub.rid); - }, 0); - }, - removed: (sub: ISubscription) => { - e2e.removeInstanceByRoomId(sub.rid); - }, - }); + if (listenersAttached) { + e2e.log('Listeners already attached'); + return; + } offClientMessageReceived = onClientMessageReceived.use(async (msg: IMessage) => { const e2eRoom = await e2e.getInstanceByRoomId(msg.rid); if (!e2eRoom?.shouldConvertReceivedMessages()) { return msg; } + + if (isE2EEPinnedMessage(msg)) { + return e2e.decryptPinnedMessage(msg); + } + return e2e.decryptMessage(msg); }); // Encrypt messages before sending - offClientBeforeSendMessage = onClientBeforeSendMessage.use(async (message: AtLeast) => { + offClientBeforeSendMessage = onClientBeforeSendMessage.use(async (message) => { const e2eRoom = await e2e.getInstanceByRoomId(message.rid); if (!e2eRoom) { @@ -140,12 +89,10 @@ Meteor.startup(() => { } // Should encrypt this message. - const msg = await e2eRoom.encrypt(message); - - message.msg = msg; - message.t = 'e2e'; - message.e2e = 'pending'; - return message; + return e2eRoom.encryptMessage(message); }); + + listenersAttached = true; + e2e.log('Listeners attached', listenersAttached); }); }); diff --git a/apps/meteor/client/startup/index.ts b/apps/meteor/client/startup/index.ts index bf6814617e4af..e2264d7954153 100644 --- a/apps/meteor/client/startup/index.ts +++ b/apps/meteor/client/startup/index.ts @@ -3,9 +3,11 @@ import './absoluteUrl'; import './actionButtons'; import './afterLogoutCleanUp'; import './appRoot'; +import './audit'; import './callbacks'; import './collections'; import './customOAuth'; +import './deviceManagement'; import './e2e'; import './forceLogout'; import './iframeCommands'; @@ -16,6 +18,7 @@ import './messageObserve'; import './messageTypes'; import './notifications'; import './otr'; +import './readReceipt'; import './reloadRoomAfterLogin'; import './roles'; import './rootUrlChange'; diff --git a/apps/meteor/client/startup/messageTypes.ts b/apps/meteor/client/startup/messageTypes.ts index 65ad6dabae236..481ab49568a2d 100644 --- a/apps/meteor/client/startup/messageTypes.ts +++ b/apps/meteor/client/startup/messageTypes.ts @@ -61,4 +61,10 @@ Meteor.startup(() => { system: true, message: 'Pinned_a_message', }); + + MessageTypes.registerType({ + id: 'message_pinned_e2e', + system: true, + message: 'Pinned_a_message', + }); }); diff --git a/apps/meteor/client/startup/notifications/konchatNotifications.ts b/apps/meteor/client/startup/notifications/konchatNotifications.ts index cd16d4264479d..3d5f26cae5472 100644 --- a/apps/meteor/client/startup/notifications/konchatNotifications.ts +++ b/apps/meteor/client/startup/notifications/konchatNotifications.ts @@ -72,6 +72,7 @@ Meteor.startup(() => { 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/ee/client/startup/readReceipt.ts b/apps/meteor/client/startup/readReceipt.ts similarity index 70% rename from apps/meteor/ee/client/startup/readReceipt.ts rename to apps/meteor/client/startup/readReceipt.ts index 6cd7c4835d462..36eb50b6bcbd0 100644 --- a/apps/meteor/ee/client/startup/readReceipt.ts +++ b/apps/meteor/client/startup/readReceipt.ts @@ -1,11 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { settings } from '../../../app/settings/client'; -import { MessageAction } from '../../../app/ui-utils/client'; -import { imperativeModal } from '../../../client/lib/imperativeModal'; -import { messageArgs } from '../../../client/lib/utils/messageArgs'; -import ReadReceiptsModal from '../../../client/views/room/modals/ReadReceiptsModal'; +import { settings } from '../../app/settings/client'; +import { MessageAction } from '../../app/ui-utils/client'; +import { imperativeModal } from '../lib/imperativeModal'; +import { messageArgs } from '../lib/utils/messageArgs'; +import ReadReceiptsModal from '../views/room/modals/ReadReceiptsModal'; Meteor.startup(() => { Tracker.autorun(() => { diff --git a/apps/meteor/client/ui.ts b/apps/meteor/client/ui.ts index 5dd438129c9aa..6c7971a8cca08 100644 --- a/apps/meteor/client/ui.ts +++ b/apps/meteor/client/ui.ts @@ -1,12 +1,11 @@ -import { useOnHoldChatQuickAction } from '../ee/client/hooks/quickActions/useOnHoldChatQuickAction'; -import { useCallsRoomAction } from '../ee/client/hooks/roomActions/useCallsRoomAction'; -import { useCannedResponsesRoomAction } from '../ee/client/hooks/roomActions/useCannedResponsesRoomAction'; -import { useGameCenterRoomAction } from '../ee/client/hooks/roomActions/useGameCenterRoomAction'; import { useChatForwardQuickAction } from './hooks/quickActions/useChatForwardQuickAction'; import { useCloseChatQuickAction } from './hooks/quickActions/useCloseChatQuickAction'; import { useMoveQueueQuickAction } from './hooks/quickActions/useMoveQueueQuickAction'; +import { useOnHoldChatQuickAction } from './hooks/quickActions/useOnHoldChatQuickAction'; import { useTranscriptQuickAction } from './hooks/quickActions/useTranscriptQuickAction'; import { useAutotranslateRoomAction } from './hooks/roomActions/useAutotranslateRoomAction'; +import { useCallsRoomAction } from './hooks/roomActions/useCallsRoomAction'; +import { useCannedResponsesRoomAction } from './hooks/roomActions/useCannedResponsesRoomAction'; import { useChannelSettingsRoomAction } from './hooks/roomActions/useChannelSettingsRoomAction'; import { useCleanHistoryRoomAction } from './hooks/roomActions/useCleanHistoryRoomAction'; import { useContactChatHistoryRoomAction } from './hooks/roomActions/useContactChatHistoryRoomAction'; @@ -14,6 +13,7 @@ import { useContactProfileRoomAction } from './hooks/roomActions/useContactProfi import { useDiscussionsRoomAction } from './hooks/roomActions/useDiscussionsRoomAction'; import { useE2EERoomAction } from './hooks/roomActions/useE2EERoomAction'; import { useExportMessagesRoomAction } from './hooks/roomActions/useExportMessagesRoomAction'; +import { useGameCenterRoomAction } from './hooks/roomActions/useGameCenterRoomAction'; import { useKeyboardShortcutListRoomAction } from './hooks/roomActions/useKeyboardShortcutListRoomAction'; import { useMembersListRoomAction } from './hooks/roomActions/useMembersListRoomAction'; import { useMentionsRoomAction } from './hooks/roomActions/useMentionsRoomAction'; @@ -78,3 +78,5 @@ export const quickActionHooks = [ useCloseChatQuickAction, useOnHoldChatQuickAction, ] satisfies (() => QuickActionsActionConfig | undefined)[]; + +export const roomActionHooksForE2EESetup = [useChannelSettingsRoomAction, useMembersListRoomAction, useE2EERoomAction]; diff --git a/apps/meteor/ee/client/views/account/deviceManagement/DeviceManagementAccountPage.tsx b/apps/meteor/client/views/account/deviceManagement/DeviceManagementAccountPage.tsx similarity index 85% rename from apps/meteor/ee/client/views/account/deviceManagement/DeviceManagementAccountPage.tsx rename to apps/meteor/client/views/account/deviceManagement/DeviceManagementAccountPage.tsx index 13722be69c694..30979e2a81d82 100644 --- a/apps/meteor/ee/client/views/account/deviceManagement/DeviceManagementAccountPage.tsx +++ b/apps/meteor/client/views/account/deviceManagement/DeviceManagementAccountPage.tsx @@ -2,7 +2,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import { Page, PageHeader, PageContent } from '../../../../../client/components/Page'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; import DeviceManagementAccountTable from './DeviceManagementAccountTable'; const DeviceManagementAccountPage = (): ReactElement => { diff --git a/apps/meteor/ee/client/views/account/deviceManagement/DeviceManagementAccountTable/DeviceManagementAccountRow.tsx b/apps/meteor/client/views/account/deviceManagement/DeviceManagementAccountTable/DeviceManagementAccountRow.tsx similarity index 89% rename from apps/meteor/ee/client/views/account/deviceManagement/DeviceManagementAccountTable/DeviceManagementAccountRow.tsx rename to apps/meteor/client/views/account/deviceManagement/DeviceManagementAccountTable/DeviceManagementAccountRow.tsx index 616b941de2a8a..e2532d0c6623c 100644 --- a/apps/meteor/ee/client/views/account/deviceManagement/DeviceManagementAccountTable/DeviceManagementAccountRow.tsx +++ b/apps/meteor/client/views/account/deviceManagement/DeviceManagementAccountTable/DeviceManagementAccountRow.tsx @@ -4,10 +4,10 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import { GenericTableCell, GenericTableRow } from '../../../../../../client/components/GenericTable'; -import { useFormatDateAndTime } from '../../../../../../client/hooks/useFormatDateAndTime'; +import { GenericTableCell, GenericTableRow } from '../../../../components/GenericTable'; import DeviceIcon from '../../../../components/deviceManagement/DeviceIcon'; import { useDeviceLogout } from '../../../../hooks/useDeviceLogout'; +import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime'; type DevicesRowProps = { _id: string; diff --git a/apps/meteor/ee/client/views/account/deviceManagement/DeviceManagementAccountTable/DeviceManagementAccountTable.tsx b/apps/meteor/client/views/account/deviceManagement/DeviceManagementAccountTable/DeviceManagementAccountTable.tsx similarity index 87% rename from apps/meteor/ee/client/views/account/deviceManagement/DeviceManagementAccountTable/DeviceManagementAccountTable.tsx rename to apps/meteor/client/views/account/deviceManagement/DeviceManagementAccountTable/DeviceManagementAccountTable.tsx index c6e32b0268188..5039521fa6226 100644 --- a/apps/meteor/ee/client/views/account/deviceManagement/DeviceManagementAccountTable/DeviceManagementAccountTable.tsx +++ b/apps/meteor/client/views/account/deviceManagement/DeviceManagementAccountTable/DeviceManagementAccountTable.tsx @@ -3,11 +3,11 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; -import { GenericTableHeaderCell } from '../../../../../../client/components/GenericTable'; -import { usePagination } from '../../../../../../client/components/GenericTable/hooks/usePagination'; -import { useSort } from '../../../../../../client/components/GenericTable/hooks/useSort'; -import { useEndpointData } from '../../../../../../client/hooks/useEndpointData'; +import { GenericTableHeaderCell } from '../../../../components/GenericTable'; +import { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; +import { useSort } from '../../../../components/GenericTable/hooks/useSort'; import DeviceManagementTable from '../../../../components/deviceManagement/DeviceManagementTable'; +import { useEndpointData } from '../../../../hooks/useEndpointData'; import DeviceManagementAccountRow from './DeviceManagementAccountRow'; const sortMapping = { diff --git a/apps/meteor/ee/client/views/account/deviceManagement/DeviceManagementAccountTable/index.ts b/apps/meteor/client/views/account/deviceManagement/DeviceManagementAccountTable/index.ts similarity index 100% rename from apps/meteor/ee/client/views/account/deviceManagement/DeviceManagementAccountTable/index.ts rename to apps/meteor/client/views/account/deviceManagement/DeviceManagementAccountTable/index.ts diff --git a/apps/meteor/client/views/account/omnichannel/PreferencesConversationTranscript.tsx b/apps/meteor/client/views/account/omnichannel/PreferencesConversationTranscript.tsx index 8cf34b6a56d40..11bf6634a0e9d 100644 --- a/apps/meteor/client/views/account/omnichannel/PreferencesConversationTranscript.tsx +++ b/apps/meteor/client/views/account/omnichannel/PreferencesConversationTranscript.tsx @@ -4,7 +4,7 @@ import { useTranslation, usePermission } from '@rocket.chat/ui-contexts'; import React from 'react'; import { useFormContext } from 'react-hook-form'; -import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule'; +import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; const PreferencesConversationTranscript = () => { const t = useTranslation(); diff --git a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementAdminPage.tsx b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminPage.tsx similarity index 84% rename from apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementAdminPage.tsx rename to apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminPage.tsx index 4360803188925..317a9f5190306 100644 --- a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementAdminPage.tsx +++ b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminPage.tsx @@ -2,8 +2,8 @@ import { useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useRef } from 'react'; -import { ContextualbarDialog } from '../../../../../client/components/Contextualbar'; -import { Page, PageHeader, PageContent } from '../../../../../client/components/Page'; +import { ContextualbarDialog } from '../../../components/Contextualbar'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; import DeviceManagementAdminTable from './DeviceManagementAdminTable'; import DeviceManagementInfo from './DeviceManagementInfo'; diff --git a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementAdminRoute.tsx b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminRoute.tsx similarity index 78% rename from apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementAdminRoute.tsx rename to apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminRoute.tsx index c250728988ec0..e7624ffd956ec 100644 --- a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementAdminRoute.tsx +++ b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminRoute.tsx @@ -2,12 +2,12 @@ import { usePermission, useRouter, useSetModal, useCurrentModal, useTranslation import type { ReactElement } from 'react'; import React, { useEffect } from 'react'; -import { getURL } from '../../../../../app/utils/client/getURL'; -import GenericUpsellModal from '../../../../../client/components/GenericUpsellModal'; -import { useUpsellActions } from '../../../../../client/components/GenericUpsellModal/hooks'; -import PageSkeleton from '../../../../../client/components/PageSkeleton'; -import NotAuthorizedPage from '../../../../../client/views/notAuthorized/NotAuthorizedPage'; +import { getURL } from '../../../../app/utils/client/getURL'; +import GenericUpsellModal from '../../../components/GenericUpsellModal'; +import { useUpsellActions } from '../../../components/GenericUpsellModal/hooks'; +import PageSkeleton from '../../../components/PageSkeleton'; import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import DeviceManagementAdminPage from './DeviceManagementAdminPage'; const DeviceManagementAdminRoute = (): ReactElement => { diff --git a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminRow.tsx b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminRow.tsx similarity index 93% rename from apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminRow.tsx rename to apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminRow.tsx index e3ac5680ed2e8..5420be9536015 100644 --- a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminRow.tsx +++ b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminRow.tsx @@ -4,10 +4,10 @@ import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; import type { KeyboardEvent, ReactElement } from 'react'; import React, { useCallback } from 'react'; -import { GenericTableRow, GenericTableCell } from '../../../../../../client/components/GenericTable'; -import { useFormatDateAndTime } from '../../../../../../client/hooks/useFormatDateAndTime'; +import { GenericTableRow, GenericTableCell } from '../../../../components/GenericTable'; import DeviceIcon from '../../../../components/deviceManagement/DeviceIcon'; import { useDeviceLogout } from '../../../../hooks/useDeviceLogout'; +import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime'; type DeviceRowProps = { _id: string; diff --git a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminTable.tsx b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminTable.tsx similarity index 89% rename from apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminTable.tsx rename to apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminTable.tsx index e9ac8be7da9fa..026f1cb0de30b 100644 --- a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminTable.tsx +++ b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminTable.tsx @@ -4,12 +4,12 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, MutableRefObject } from 'react'; import React, { useState, useMemo, useEffect } from 'react'; -import FilterByText from '../../../../../../client/components/FilterByText'; -import { GenericTableHeaderCell } from '../../../../../../client/components/GenericTable'; -import { usePagination } from '../../../../../../client/components/GenericTable/hooks/usePagination'; -import { useSort } from '../../../../../../client/components/GenericTable/hooks/useSort'; -import { useEndpointData } from '../../../../../../client/hooks/useEndpointData'; +import FilterByText from '../../../../components/FilterByText'; +import { GenericTableHeaderCell } from '../../../../components/GenericTable'; +import { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; +import { useSort } from '../../../../components/GenericTable/hooks/useSort'; import DeviceManagementTable from '../../../../components/deviceManagement/DeviceManagementTable'; +import { useEndpointData } from '../../../../hooks/useEndpointData'; import DeviceManagementAdminRow from './DeviceManagementAdminRow'; const sortMapping = { diff --git a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementAdminTable/index.ts b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminTable/index.ts similarity index 100% rename from apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementAdminTable/index.ts rename to apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminTable/index.ts diff --git a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfo.tsx b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfo.tsx similarity index 91% rename from apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfo.tsx rename to apps/meteor/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfo.tsx index 7820ee8349605..240e61e5878d4 100644 --- a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfo.tsx +++ b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfo.tsx @@ -12,11 +12,11 @@ import { ContextualbarScrollableContent, ContextualbarFooter, ContextualbarTitle, -} from '../../../../../../client/components/Contextualbar'; -import InfoPanel from '../../../../../../client/components/InfoPanel'; -import { useFormatDateAndTime } from '../../../../../../client/hooks/useFormatDateAndTime'; -import { usePresence } from '../../../../../../client/hooks/usePresence'; +} from '../../../../components/Contextualbar'; +import InfoPanel from '../../../../components/InfoPanel'; import { useDeviceLogout } from '../../../../hooks/useDeviceLogout'; +import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime'; +import { usePresence } from '../../../../hooks/usePresence'; type DeviceManagementInfoProps = DeviceManagementPopulatedSession & { onReload: () => void; diff --git a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfoWithData.tsx b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfoWithData.tsx similarity index 89% rename from apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfoWithData.tsx rename to apps/meteor/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfoWithData.tsx index d5e59ea027560..5f78269fca573 100644 --- a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfoWithData.tsx +++ b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfoWithData.tsx @@ -11,9 +11,9 @@ import { ContextualbarClose, ContextualbarContent, ContextualbarTitle, -} from '../../../../../../client/components/Contextualbar'; -import { useEndpointData } from '../../../../../../client/hooks/useEndpointData'; -import { AsyncStatePhase } from '../../../../../../client/lib/asyncState'; +} from '../../../../components/Contextualbar'; +import { useEndpointData } from '../../../../hooks/useEndpointData'; +import { AsyncStatePhase } from '../../../../lib/asyncState'; import DeviceManagementInfo from './DeviceManagementInfo'; const convertSessionFromAPI = ({ diff --git a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementInfo/index.ts b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementInfo/index.ts similarity index 100% rename from apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementInfo/index.ts rename to apps/meteor/client/views/admin/deviceManagement/DeviceManagementInfo/index.ts diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCard.tsx b/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardCard.tsx similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCard.tsx rename to apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardCard.tsx diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCardErrorBoundary.tsx b/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardCardErrorBoundary.tsx similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCardErrorBoundary.tsx rename to apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardCardErrorBoundary.tsx diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCardFilter.tsx b/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardCardFilter.tsx similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCardFilter.tsx rename to apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardCardFilter.tsx diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.stories.tsx b/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardPage.stories.tsx similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.stories.tsx rename to apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardPage.stories.tsx diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx b/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx similarity index 98% rename from apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx rename to apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx index de40e04dcfcac..78f1585f0f4bc 100644 --- a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx +++ b/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx @@ -3,7 +3,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback, useMemo, useState } from 'react'; -import { Page, PageHeader, PageScrollableContent } from '../../../../../client/components/Page'; +import { Page, PageHeader, PageScrollableContent } from '../../../components/Page'; import ChannelsTab from './channels/ChannelsTab'; import MessagesTab from './messages/MessagesTab'; import UsersTab from './users/UsersTab'; diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardRoute.tsx b/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardRoute.tsx similarity index 85% rename from apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardRoute.tsx rename to apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardRoute.tsx index fd42d428824dd..17f6713220f99 100644 --- a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardRoute.tsx +++ b/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardRoute.tsx @@ -10,12 +10,12 @@ import { import type { ReactElement } from 'react'; import React, { useEffect } from 'react'; -import { getURL } from '../../../../../app/utils/client/getURL'; -import GenericUpsellModal from '../../../../../client/components/GenericUpsellModal'; -import { useUpsellActions } from '../../../../../client/components/GenericUpsellModal/hooks'; -import PageSkeleton from '../../../../../client/components/PageSkeleton'; -import NotAuthorizedPage from '../../../../../client/views/notAuthorized/NotAuthorizedPage'; +import { getURL } from '../../../../app/utils/client/getURL'; +import GenericUpsellModal from '../../../components/GenericUpsellModal'; +import { useUpsellActions } from '../../../components/GenericUpsellModal/hooks'; +import PageSkeleton from '../../../components/PageSkeleton'; import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import EngagementDashboardPage from './EngagementDashboardPage'; const isValidTab = (tab: string | undefined): tab is 'users' | 'messages' | 'channels' => diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/channels/ChannelsOverview.tsx b/apps/meteor/client/views/admin/engagementDashboard/channels/ChannelsOverview.tsx similarity index 98% rename from apps/meteor/ee/client/views/admin/engagementDashboard/channels/ChannelsOverview.tsx rename to apps/meteor/client/views/admin/engagementDashboard/channels/ChannelsOverview.tsx index f901f11d3954d..a92b2c6d32952 100644 --- a/apps/meteor/ee/client/views/admin/engagementDashboard/channels/ChannelsOverview.tsx +++ b/apps/meteor/client/views/admin/engagementDashboard/channels/ChannelsOverview.tsx @@ -4,10 +4,10 @@ import moment from 'moment'; import type { ReactElement } from 'react'; import React, { useMemo, useState } from 'react'; -import Growth from '../../../../../../client/components/dataView/Growth'; import DownloadDataButton from '../../../../components/dashboards/DownloadDataButton'; import PeriodSelector from '../../../../components/dashboards/PeriodSelector'; import { usePeriodSelectorState } from '../../../../components/dashboards/usePeriodSelectorState'; +import Growth from '../../../../components/dataView/Growth'; import EngagementDashboardCardFilter from '../EngagementDashboardCardFilter'; import { useChannelsList } from './useChannelsList'; diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/channels/ChannelsTab.stories.tsx b/apps/meteor/client/views/admin/engagementDashboard/channels/ChannelsTab.stories.tsx similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/channels/ChannelsTab.stories.tsx rename to apps/meteor/client/views/admin/engagementDashboard/channels/ChannelsTab.stories.tsx diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/channels/ChannelsTab.tsx b/apps/meteor/client/views/admin/engagementDashboard/channels/ChannelsTab.tsx similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/channels/ChannelsTab.tsx rename to apps/meteor/client/views/admin/engagementDashboard/channels/ChannelsTab.tsx diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/channels/useChannelsList.ts b/apps/meteor/client/views/admin/engagementDashboard/channels/useChannelsList.ts similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/channels/useChannelsList.ts rename to apps/meteor/client/views/admin/engagementDashboard/channels/useChannelsList.ts diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/dataView/LegendSymbol.stories.tsx b/apps/meteor/client/views/admin/engagementDashboard/dataView/LegendSymbol.stories.tsx similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/dataView/LegendSymbol.stories.tsx rename to apps/meteor/client/views/admin/engagementDashboard/dataView/LegendSymbol.stories.tsx diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/dataView/LegendSymbol.tsx b/apps/meteor/client/views/admin/engagementDashboard/dataView/LegendSymbol.tsx similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/dataView/LegendSymbol.tsx rename to apps/meteor/client/views/admin/engagementDashboard/dataView/LegendSymbol.tsx diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/dataView/colors.ts b/apps/meteor/client/views/admin/engagementDashboard/dataView/colors.ts similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/dataView/colors.ts rename to apps/meteor/client/views/admin/engagementDashboard/dataView/colors.ts diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/messages/MessagesPerChannelSection.tsx b/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesPerChannelSection.tsx similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/messages/MessagesPerChannelSection.tsx rename to apps/meteor/client/views/admin/engagementDashboard/messages/MessagesPerChannelSection.tsx diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/messages/MessagesSentSection.tsx b/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesSentSection.tsx similarity index 98% rename from apps/meteor/ee/client/views/admin/engagementDashboard/messages/MessagesSentSection.tsx rename to apps/meteor/client/views/admin/engagementDashboard/messages/MessagesSentSection.tsx index aa12bf2a3d1ce..7cf46ebc476ac 100644 --- a/apps/meteor/ee/client/views/admin/engagementDashboard/messages/MessagesSentSection.tsx +++ b/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesSentSection.tsx @@ -6,11 +6,11 @@ import moment from 'moment'; import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; -import CounterSet from '../../../../../../client/components/dataView/CounterSet'; import DownloadDataButton from '../../../../components/dashboards/DownloadDataButton'; import PeriodSelector from '../../../../components/dashboards/PeriodSelector'; import { usePeriodLabel } from '../../../../components/dashboards/usePeriodLabel'; import { usePeriodSelectorState } from '../../../../components/dashboards/usePeriodSelectorState'; +import CounterSet from '../../../../components/dataView/CounterSet'; import EngagementDashboardCardFilter from '../EngagementDashboardCardFilter'; import { useMessagesSent } from './useMessagesSent'; diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/messages/MessagesTab.stories.tsx b/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesTab.stories.tsx similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/messages/MessagesTab.stories.tsx rename to apps/meteor/client/views/admin/engagementDashboard/messages/MessagesTab.stories.tsx diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/messages/MessagesTab.tsx b/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesTab.tsx similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/messages/MessagesTab.tsx rename to apps/meteor/client/views/admin/engagementDashboard/messages/MessagesTab.tsx diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/messages/useMessageOrigins.ts b/apps/meteor/client/views/admin/engagementDashboard/messages/useMessageOrigins.ts similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/messages/useMessageOrigins.ts rename to apps/meteor/client/views/admin/engagementDashboard/messages/useMessageOrigins.ts diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/messages/useMessagesSent.ts b/apps/meteor/client/views/admin/engagementDashboard/messages/useMessagesSent.ts similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/messages/useMessagesSent.ts rename to apps/meteor/client/views/admin/engagementDashboard/messages/useMessagesSent.ts diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/messages/useTopFivePopularChannels.ts b/apps/meteor/client/views/admin/engagementDashboard/messages/useTopFivePopularChannels.ts similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/messages/useTopFivePopularChannels.ts rename to apps/meteor/client/views/admin/engagementDashboard/messages/useTopFivePopularChannels.ts diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx b/apps/meteor/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx similarity index 98% rename from apps/meteor/ee/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx rename to apps/meteor/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx index 80af96d8f6691..80359837b6276 100644 --- a/apps/meteor/ee/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx +++ b/apps/meteor/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx @@ -6,9 +6,9 @@ import moment from 'moment'; import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; -import CounterSet from '../../../../../../client/components/dataView/CounterSet'; -import { useFormatDate } from '../../../../../../client/hooks/useFormatDate'; import DownloadDataButton from '../../../../components/dashboards/DownloadDataButton'; +import CounterSet from '../../../../components/dataView/CounterSet'; +import { useFormatDate } from '../../../../hooks/useFormatDate'; import EngagementDashboardCardFilter from '../EngagementDashboardCardFilter'; import LegendSymbol from '../dataView/LegendSymbol'; import { useActiveUsers } from './useActiveUsers'; diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/users/BusiestChatTimesSection.tsx b/apps/meteor/client/views/admin/engagementDashboard/users/BusiestChatTimesSection.tsx similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/users/BusiestChatTimesSection.tsx rename to apps/meteor/client/views/admin/engagementDashboard/users/BusiestChatTimesSection.tsx diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/users/ContentForDays.tsx b/apps/meteor/client/views/admin/engagementDashboard/users/ContentForDays.tsx similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/users/ContentForDays.tsx rename to apps/meteor/client/views/admin/engagementDashboard/users/ContentForDays.tsx diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/users/ContentForHours.tsx b/apps/meteor/client/views/admin/engagementDashboard/users/ContentForHours.tsx similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/users/ContentForHours.tsx rename to apps/meteor/client/views/admin/engagementDashboard/users/ContentForHours.tsx diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/users/NewUsersSection.tsx b/apps/meteor/client/views/admin/engagementDashboard/users/NewUsersSection.tsx similarity index 97% rename from apps/meteor/ee/client/views/admin/engagementDashboard/users/NewUsersSection.tsx rename to apps/meteor/client/views/admin/engagementDashboard/users/NewUsersSection.tsx index 1941b4fea718e..36f9c3de2cc27 100644 --- a/apps/meteor/ee/client/views/admin/engagementDashboard/users/NewUsersSection.tsx +++ b/apps/meteor/client/views/admin/engagementDashboard/users/NewUsersSection.tsx @@ -7,12 +7,12 @@ import moment from 'moment'; import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; -import CounterSet from '../../../../../../client/components/dataView/CounterSet'; -import { useFormatDate } from '../../../../../../client/hooks/useFormatDate'; import DownloadDataButton from '../../../../components/dashboards/DownloadDataButton'; import PeriodSelector from '../../../../components/dashboards/PeriodSelector'; import { usePeriodLabel } from '../../../../components/dashboards/usePeriodLabel'; import { usePeriodSelectorState } from '../../../../components/dashboards/usePeriodSelectorState'; +import CounterSet from '../../../../components/dataView/CounterSet'; +import { useFormatDate } from '../../../../hooks/useFormatDate'; import EngagementDashboardCardFilter from '../EngagementDashboardCardFilter'; import { useNewUsers } from './useNewUsers'; diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/users/UsersByTimeOfTheDaySection.tsx b/apps/meteor/client/views/admin/engagementDashboard/users/UsersByTimeOfTheDaySection.tsx similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/users/UsersByTimeOfTheDaySection.tsx rename to apps/meteor/client/views/admin/engagementDashboard/users/UsersByTimeOfTheDaySection.tsx diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/users/UsersTab.stories.tsx b/apps/meteor/client/views/admin/engagementDashboard/users/UsersTab.stories.tsx similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/users/UsersTab.stories.tsx rename to apps/meteor/client/views/admin/engagementDashboard/users/UsersTab.stories.tsx diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/users/UsersTab.tsx b/apps/meteor/client/views/admin/engagementDashboard/users/UsersTab.tsx similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/users/UsersTab.tsx rename to apps/meteor/client/views/admin/engagementDashboard/users/UsersTab.tsx diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/users/useActiveUsers.ts b/apps/meteor/client/views/admin/engagementDashboard/users/useActiveUsers.ts similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/users/useActiveUsers.ts rename to apps/meteor/client/views/admin/engagementDashboard/users/useActiveUsers.ts diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/users/useHourlyChatActivity.ts b/apps/meteor/client/views/admin/engagementDashboard/users/useHourlyChatActivity.ts similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/users/useHourlyChatActivity.ts rename to apps/meteor/client/views/admin/engagementDashboard/users/useHourlyChatActivity.ts diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/users/useNewUsers.ts b/apps/meteor/client/views/admin/engagementDashboard/users/useNewUsers.ts similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/users/useNewUsers.ts rename to apps/meteor/client/views/admin/engagementDashboard/users/useNewUsers.ts diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/users/useUsersByTimeOfTheDay.ts b/apps/meteor/client/views/admin/engagementDashboard/users/useUsersByTimeOfTheDay.ts similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/users/useUsersByTimeOfTheDay.ts rename to apps/meteor/client/views/admin/engagementDashboard/users/useUsersByTimeOfTheDay.ts diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/users/useWeeklyChatActivity.ts b/apps/meteor/client/views/admin/engagementDashboard/users/useWeeklyChatActivity.ts similarity index 100% rename from apps/meteor/ee/client/views/admin/engagementDashboard/users/useWeeklyChatActivity.ts rename to apps/meteor/client/views/admin/engagementDashboard/users/useWeeklyChatActivity.ts diff --git a/apps/meteor/client/views/admin/integrations/IntegrationsTable.tsx b/apps/meteor/client/views/admin/integrations/IntegrationsTable.tsx index 206458e09c9bc..e2942f384c9d9 100644 --- a/apps/meteor/client/views/admin/integrations/IntegrationsTable.tsx +++ b/apps/meteor/client/views/admin/integrations/IntegrationsTable.tsx @@ -107,7 +107,7 @@ const IntegrationsTable = ({ type }: { type?: string }) => { )} {isSuccess && data && data.integrations.length > 0 && ( <> - + {headers} {isSuccess && diff --git a/apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx b/apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx index 6de7cdda16751..805d89c54c37f 100644 --- a/apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx +++ b/apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx @@ -85,16 +85,6 @@ const ModerationConsoleTable: FC = () => { > {t('User')} , - - - {t('Moderation_Reported_message')} - , {t('Room')} , diff --git a/apps/meteor/client/views/admin/moderation/ModerationConsoleTableRow.tsx b/apps/meteor/client/views/admin/moderation/ModerationConsoleTableRow.tsx index 56419c61223c7..65bf7069e074e 100644 --- a/apps/meteor/client/views/admin/moderation/ModerationConsoleTableRow.tsx +++ b/apps/meteor/client/views/admin/moderation/ModerationConsoleTableRow.tsx @@ -13,7 +13,7 @@ export type ModerationConsoleRowProps = { }; const ModerationConsoleTableRow = ({ report, onClick, isDesktopOrLarger }: ModerationConsoleRowProps): JSX.Element => { - const { userId: _id, rooms, name, count, message, username, ts } = report; + const { userId: _id, rooms, name, count, username, ts } = report; const roomNames = rooms.map((room) => { if (room.t === 'd') { @@ -31,7 +31,6 @@ const ModerationConsoleTableRow = ({ report, onClick, isDesktopOrLarger }: Moder - {message} {concatenatedRoomNames} {formatDateAndTime(ts)} {count} diff --git a/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx b/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx index 51a2be0872764..afca74f7528cb 100644 --- a/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx +++ b/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx @@ -1,5 +1,5 @@ -import type { IMessage, MessageReport } from '@rocket.chat/core-typings'; -import { isE2EEMessage } from '@rocket.chat/core-typings'; +import type { IMessage, MessageReport, MessageAttachment } from '@rocket.chat/core-typings'; +import { isE2EEMessage, isQuoteAttachment } from '@rocket.chat/core-typings'; import { Message, MessageName, MessageToolbarItem, MessageToolbarWrapper, MessageUsername } from '@rocket.chat/fuselage'; import { UserAvatar } from '@rocket.chat/ui-avatar'; import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; @@ -47,6 +47,10 @@ const ContextMessage = ({ const displayName = useUserDisplayName({ name, username }); + const quotes = message?.attachments?.filter(isQuoteAttachment) || []; + + const attachments = message?.attachments?.filter((attachment: MessageAttachment) => !isQuoteAttachment(attachment)) || []; + return ( <> {formatDate(message._updatedAt)} @@ -65,6 +69,7 @@ const ContextMessage = ({ {room.name || room.fname || 'DM'} + {!!quotes?.length && } {!message.blocks?.length && !!message.md?.length ? ( <> {(!isEncryptedMessage || message.e2e === 'done') && ( @@ -76,8 +81,8 @@ const ContextMessage = ({ message.msg )} + {!!attachments && } {message.blocks && } - {message.attachments?.length > 0 && } diff --git a/apps/meteor/client/views/admin/permissions/EditRolePageWithData.tsx b/apps/meteor/client/views/admin/permissions/EditRolePageWithData.tsx index 32687a8385931..d08eab83a934e 100644 --- a/apps/meteor/client/views/admin/permissions/EditRolePageWithData.tsx +++ b/apps/meteor/client/views/admin/permissions/EditRolePageWithData.tsx @@ -4,8 +4,8 @@ import { useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule'; import PageSkeleton from '../../../components/PageSkeleton'; +import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; import EditRolePage from './EditRolePage'; import { useRole } from './hooks/useRole'; diff --git a/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx b/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx index ca9fefd54163e..3b8bea6097e4b 100644 --- a/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx +++ b/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx @@ -3,7 +3,6 @@ import { useRouteParameter, useRoute, useTranslation, useSetModal } from '@rocke import type { ReactElement } from 'react'; import React, { useEffect } from 'react'; -import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule'; import { Contextualbar, ContextualbarHeader, @@ -11,6 +10,7 @@ import { ContextualbarClose, ContextualbarDialog, } from '../../../components/Contextualbar'; +import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; import CustomRoleUpsellModal from './CustomRoleUpsellModal'; import EditRolePageWithData from './EditRolePageWithData'; diff --git a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePage.tsx b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePage.tsx index 9c418c74c3bc3..4bfb2afc81d35 100644 --- a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePage.tsx +++ b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePage.tsx @@ -1,9 +1,10 @@ import type { IRole, IRoom } from '@rocket.chat/core-typings'; -import { Box, Field, FieldLabel, FieldRow, Margins, ButtonGroup, Button, Callout } from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useToastMessageDispatch, useRoute, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { Box, Field, FieldLabel, FieldRow, Margins, ButtonGroup, Button, Callout, FieldError } from '@rocket.chat/fuselage'; +import { useEffectEvent, useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useToastMessageDispatch, useEndpoint, useTranslation, useRouter } from '@rocket.chat/ui-contexts'; +import { useQueryClient } from '@tanstack/react-query'; import type { ReactElement } from 'react'; -import React, { useRef } from 'react'; +import React from 'react'; import { useForm, Controller } from 'react-hook-form'; import { Page, PageHeader, PageContent } from '../../../../components/Page'; @@ -18,42 +19,35 @@ type UsersInRolePayload = { const UsersInRolePage = ({ role }: { role: IRole }): ReactElement => { const t = useTranslation(); - const reload = useRef<() => void>(() => undefined); const dispatchToastMessage = useToastMessageDispatch(); + const queryClient = useQueryClient(); const { control, handleSubmit, - formState: { isDirty }, - reset, - getValues, + formState: { errors, isDirty }, + watch, } = useForm({ defaultValues: { users: [] } }); const { _id, name, description } = role; - const router = useRoute('admin-permissions'); - const addUser = useEndpoint('POST', '/v1/roles.addUserToRole'); + const router = useRouter(); + const addUserToRoleEndpoint = useEndpoint('POST', '/v1/roles.addUserToRole'); - const rid = getValues('rid'); + const { rid } = watch(); + const roomFieldId = useUniqueId(); + const usersFieldId = useUniqueId(); - const handleReturn = useMutableCallback(() => { - router.push({ - context: 'edit', - _id, - }); - }); - - const handleAdd = useMutableCallback(async ({ users, rid }: UsersInRolePayload) => { + const handleAdd = useEffectEvent(async ({ users, rid }: UsersInRolePayload) => { try { await Promise.all( users.map(async (user) => { if (user) { - await addUser({ roleName: _id, username: user, roomId: rid }); + await addUserToRoleEndpoint({ roleName: _id, username: user, roomId: rid }); } }), ); dispatchToastMessage({ type: 'success', message: t('Users_added') }); - reload.current(); - reset(); + queryClient.invalidateQueries(['getUsersInRole']); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } @@ -63,7 +57,7 @@ const UsersInRolePage = ({ role }: { role: IRole }): ReactElement => { - + @@ -71,40 +65,68 @@ const UsersInRolePage = ({ role }: { role: IRole }): ReactElement => { {role.scope !== 'Users' && ( - {t('Choose_a_room')} + {t('Choose_a_room')} ( - + rules={{ required: t('error-the-field-is-required', { field: t('Room') }) }} + render={({ field: { onChange, value } }) => ( + )} /> + {errors.rid && ( + + {errors.rid.message} + + )} )} - {t('Add_users')} + {t('Add_users')} ( - + rules={{ required: t('error-the-field-is-required', { field: t('Users') }) }} + render={({ field: { onChange, value } }) => ( + )} /> - + {errors.users && ( + + + {errors.users.message} + + + )} - {(role.scope === 'Users' || rid) && ( - - )} + {(role.scope === 'Users' || rid) && } {role.scope !== 'Users' && !rid && {t('Select_a_room')}} diff --git a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTable.tsx b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTable.tsx index ab8c5926f97f2..ed7fdd93d79c9 100644 --- a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTable.tsx +++ b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTable.tsx @@ -1,79 +1,107 @@ -import type { IRole, IRoom, IUserInRole } from '@rocket.chat/core-typings'; +import type { IRole, IRoom } from '@rocket.chat/core-typings'; import { Pagination } from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useSetModal, useToastMessageDispatch, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import type { ReactElement } from 'react'; -import React from 'react'; +import React, { useMemo } from 'react'; +import GenericError from '../../../../../components/GenericError'; import GenericModal from '../../../../../components/GenericModal'; import GenericNoResults from '../../../../../components/GenericNoResults'; -import { GenericTable, GenericTableHeader, GenericTableHeaderCell, GenericTableBody } from '../../../../../components/GenericTable'; -import type { usePagination } from '../../../../../components/GenericTable/hooks/usePagination'; +import { + GenericTable, + GenericTableHeader, + GenericTableHeaderCell, + GenericTableBody, + GenericTableLoadingTable, +} from '../../../../../components/GenericTable'; +import { usePagination } from '../../../../../components/GenericTable/hooks/usePagination'; import UsersInRoleTableRow from './UsersInRoleTableRow'; type UsersInRoleTableProps = { - users: IUserInRole[]; - reload: () => void; roleName: IRole['name']; roleId: IRole['_id']; description: IRole['description']; - total: number; rid?: IRoom['_id']; - paginationData: ReturnType; }; -// TODO: Missing error state -const UsersInRoleTable = ({ - users, - reload, - roleName, - roleId, - description, - total, - rid, - paginationData, -}: UsersInRoleTableProps): ReactElement => { +const UsersInRoleTable = ({ rid, roleId, roleName, description }: UsersInRoleTableProps): ReactElement => { const t = useTranslation(); const setModal = useSetModal(); const dispatchToastMessage = useToastMessageDispatch(); - const removeUser = useEndpoint('POST', '/v1/roles.removeUserFromRole'); - const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = paginationData; + const queryClient = useQueryClient(); - const closeModal = (): void => setModal(); + const getUsersInRoleEndpoint = useEndpoint('GET', '/v1/roles.getUsersInRole'); + const removeUserFromRoleEndpoint = useEndpoint('POST', '/v1/roles.removeUserFromRole'); - const handleRemove = useMutableCallback((username) => { - const remove = async (): Promise => { + const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination(); + + const query = useMemo( + () => ({ + role: roleId, + ...(rid && { roomId: rid }), + ...(itemsPerPage && { count: itemsPerPage }), + ...(current && { offset: current }), + }), + [itemsPerPage, current, rid, roleId], + ); + + const { data, isLoading, isSuccess, refetch, isError } = useQuery(['getUsersInRole', roleId, query], async () => + getUsersInRoleEndpoint(query), + ); + + const users = + data?.users?.map((user) => ({ + ...user, + createdAt: new Date(user.createdAt), + _updatedAt: new Date(user._updatedAt), + })) || []; + + const handleRemove = useEffectEvent((username) => { + const remove = async () => { try { - await removeUser({ roleId, username, scope: rid }); + await removeUserFromRoleEndpoint({ roleId, username, scope: rid }); dispatchToastMessage({ type: 'success', message: t('User_removed') }); - } catch (error: unknown) { + queryClient.invalidateQueries(['getUsersInRole']); + } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } finally { - closeModal(); - reload(); + setModal(null); } }; setModal( - + setModal(null)} confirmText={t('Delete')}> {t('The_user_s_will_be_removed_from_role_s', username, description || roleName)} , ); }); + const headers = ( + <> + {t('Name')} + {t('Email')} + + + ); + return ( <> - {users.length === 0 && } - {users.length > 0 && ( + {isLoading && ( + + {headers} + + + + + )} + {isSuccess && users?.length > 0 && ( <> - - {t('Name')} - {t('Email')} - - + {headers} - {users.map((user) => ( + {users?.map((user) => ( ))} @@ -82,13 +110,15 @@ const UsersInRoleTable = ({ divider current={current} itemsPerPage={itemsPerPage} - count={total} + count={users.length || 0} onSetItemsPerPage={onSetItemsPerPage} onSetCurrent={onSetCurrent} {...paginationProps} /> )} + {users?.length === 0 && } + {isError && } ); }; diff --git a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTableRow.tsx b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTableRow.tsx index ea4b999d694eb..4e860ca8f7596 100644 --- a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTableRow.tsx +++ b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTableRow.tsx @@ -1,7 +1,8 @@ import type { IUserInRole } from '@rocket.chat/core-typings'; -import { Box, Button, Icon } from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { Box, IconButton } from '@rocket.chat/fuselage'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { UserAvatar } from '@rocket.chat/ui-avatar'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { memo } from 'react'; @@ -14,10 +15,11 @@ type UsersInRoleTableRowProps = { }; const UsersInRoleTableRow = ({ user, onRemove }: UsersInRoleTableRowProps): ReactElement => { + const t = useTranslation(); const { _id, name, username, avatarETag } = user; const email = getUserEmailAddress(user); - const handleRemove = useMutableCallback(() => { + const handleRemove = useEffectEvent(() => { onRemove(username); }); @@ -27,26 +29,20 @@ const UsersInRoleTableRow = ({ user, onRemove }: UsersInRoleTableRowProps): Reac - - - {name || username} - - {name && ( - - {' '} - {`@${username}`}{' '} - - )} + + {name || username} + {name && ( + + {`@${username}`} + + )} {email} - - {/* FIXME: Replace to IconButton */} - + + ); diff --git a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTableWithData.tsx b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTableWithData.tsx deleted file mode 100644 index a7fe837b6e72a..0000000000000 --- a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTableWithData.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import type { IRole, IRoom, IUserInRole } from '@rocket.chat/core-typings'; -import { useEndpoint, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; -import { useQuery } from '@tanstack/react-query'; -import type { ReactElement, MutableRefObject } from 'react'; -import React, { useEffect, useMemo } from 'react'; - -import { usePagination } from '../../../../../components/GenericTable/hooks/usePagination'; -import UsersInRoleTable from './UsersInRoleTable'; - -type UsersInRoleTableWithDataProps = { - rid?: IRoom['_id']; - roleId: IRole['_id']; - roleName: IRole['name']; - description: IRole['description']; - reloadRef: MutableRefObject<() => void>; -}; - -const UsersInRoleTableWithData = ({ - rid, - roleId, - roleName, - description, - reloadRef, -}: UsersInRoleTableWithDataProps): ReactElement | null => { - const { itemsPerPage, current, ...paginationData } = usePagination(); - const t = useTranslation(); - - const query = useMemo( - () => ({ - role: roleId, - ...(rid && { roomId: rid }), - ...(itemsPerPage && { count: itemsPerPage }), - ...(current && { offset: current }), - }), - [itemsPerPage, current, rid, roleId], - ); - - const getUsersInRole = useEndpoint('GET', '/v1/roles.getUsersInRole'); - - const dispatchToastMessage = useToastMessageDispatch(); - - const { refetch, ...result } = useQuery( - ['roles', roleId, 'users', query], - async () => { - const { users } = await getUsersInRole(query); - - if (users.length === 0) { - throw new Error(t('No_results_found')); - } - return users; - }, - { - onError: (error) => { - dispatchToastMessage({ type: 'error', message: error }); - }, - }, - ); - - useEffect(() => { - reloadRef.current = refetch; - }, [refetch, reloadRef]); - - if (result.isLoading || result.error || !result.data) { - return null; - } - - const users: IUserInRole[] = result.data.map((user) => ({ - ...user, - createdAt: new Date(user.createdAt), - _updatedAt: new Date(user._updatedAt), - })); - - return ( - - ); -}; - -export default UsersInRoleTableWithData; diff --git a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/index.ts b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/index.ts index e7195afe4fd16..1557d53dff12e 100644 --- a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/index.ts +++ b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/index.ts @@ -1 +1 @@ -export { default } from './UsersInRoleTableWithData'; +export { default } from './UsersInRoleTable'; diff --git a/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx b/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx index fcdaa29c9dffb..1ed21c1234a9a 100644 --- a/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx +++ b/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx @@ -2,10 +2,10 @@ import { Box, Icon, TextInput } from '@rocket.chat/fuselage'; import type { OptionProp } from '@rocket.chat/ui-client'; import { MultiSelectCustom } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import type { Dispatch, ReactElement, SetStateAction } from 'react'; -const roomTypeFilterStructure = [ +const initialRoomTypeFilterStructure = [ { id: 'filter_by_room', text: 'Filter_by_room', @@ -49,6 +49,13 @@ const RoomsTableFilters = ({ setFilters }: { setFilters: Dispatch([]); + const roomTypeFilterStructure = useMemo(() => { + return initialRoomTypeFilterStructure.map((option) => ({ + ...option, + checked: roomTypeSelectedOptions.some((selectedOption) => selectedOption.id === option.id), + })); + }, [roomTypeSelectedOptions]); + const handleSearchTextChange = useCallback( (event) => { const text = event.currentTarget.value; @@ -88,7 +95,7 @@ const RoomsTableFilters = ({ setFilters }: { setFilters: Dispatch import('../../../ee/client/views/admin/engagementDashboard/EngagementDashboardRoute')), + component: lazy(() => import('./engagementDashboard/EngagementDashboardRoute')), }); registerAdminRoute('/device-management/:context?/:id?', { name: 'device-management', - component: lazy(() => import('../../../ee/client/views/admin/deviceManagement/DeviceManagementAdminRoute')), + component: lazy(() => import('./deviceManagement/DeviceManagementAdminRoute')), }); registerAdminRoute('/subscription', { diff --git a/apps/meteor/client/views/admin/settings/MemoizedSetting.tsx b/apps/meteor/client/views/admin/settings/MemoizedSetting.tsx index d0b5c10e2c80a..5d08909d69501 100644 --- a/apps/meteor/client/views/admin/settings/MemoizedSetting.tsx +++ b/apps/meteor/client/views/admin/settings/MemoizedSetting.tsx @@ -20,6 +20,7 @@ import RoomPickSettingInput from './inputs/RoomPickSettingInput'; import SelectSettingInput from './inputs/SelectSettingInput'; import SelectTimezoneSettingInput from './inputs/SelectTimezoneSettingInput'; import StringSettingInput from './inputs/StringSettingInput'; +import TimespanSettingInput from './inputs/TimespanSettingInput'; // @todo: the props are loosely typed because `Setting` needs to typecheck them. const inputsByType: Record> = { @@ -39,6 +40,7 @@ const inputsByType: Record> = { roomPick: RoomPickSettingInput, timezone: SelectTimezoneSettingInput, lookup: LookupSettingInput, + timespan: TimespanSettingInput, date: GenericSettingInput, // @todo: implement group: GenericSettingInput, // @todo: implement }; @@ -46,6 +48,7 @@ const inputsByType: Record> = { type MemoizedSettingProps = { _id?: string; type: ISettingBase['type']; + packageValue: ISettingBase['packageValue']; hint?: ReactNode; callout?: ReactNode; value?: SettingValue; diff --git a/apps/meteor/client/views/admin/settings/Setting.stories.tsx b/apps/meteor/client/views/admin/settings/Setting.stories.tsx index e80db37d821e5..18ff3801dbc96 100644 --- a/apps/meteor/client/views/admin/settings/Setting.stories.tsx +++ b/apps/meteor/client/views/admin/settings/Setting.stories.tsx @@ -42,17 +42,17 @@ WithCallout.args = { export const types = () => ( - - - - - - - - - - - + + + + + + + + + + + ); diff --git a/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.tsx index 9ebde75e73166..14ba6f27b4c53 100644 --- a/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.tsx @@ -35,7 +35,7 @@ function AssetSettingInput({ _id, label, value, asset, required, disabled, fileC dispatchToastMessage({ type: 'info', message: t('Uploading_file') }); const fileData = new FormData(); - fileData.append('asset', blob, asset); + fileData.append('asset', blob, blob.name); fileData.append('assetName', asset); try { diff --git a/apps/meteor/client/views/admin/settings/inputs/CodeMirror/CodeMirrorBox.tsx b/apps/meteor/client/views/admin/settings/inputs/CodeMirror/CodeMirrorBox.tsx index ce6d2dfd60000..3f61417e4fa68 100644 --- a/apps/meteor/client/views/admin/settings/inputs/CodeMirror/CodeMirrorBox.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/CodeMirror/CodeMirrorBox.tsx @@ -39,7 +39,9 @@ const CodeMirrorBox = ({ label, children }: { label: ReactNode; children: ReactE {label} )} - {children} + + {children} +