diff --git a/.changeset/big-timers-relax.md b/.changeset/big-timers-relax.md new file mode 100644 index 000000000000..651bcff90bb6 --- /dev/null +++ b/.changeset/big-timers-relax.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +Adds a new `contacts.checkExistence` endpoint, which allows identifying whether there's already a registered contact using a given email, phone, id or visitor to source association. diff --git a/.changeset/blue-items-raise.md b/.changeset/blue-items-raise.md new file mode 100644 index 000000000000..7b5592a9efb2 --- /dev/null +++ b/.changeset/blue-items-raise.md @@ -0,0 +1,22 @@ +--- +'@rocket.chat/fuselage-ui-kit': patch +'@rocket.chat/instance-status': patch +'@rocket.chat/ui-theming': patch +'@rocket.chat/model-typings': patch +'@rocket.chat/ui-video-conf': patch +'@rocket.chat/uikit-playground': patch +'@rocket.chat/core-typings': patch +'@rocket.chat/rest-typings': patch +'@rocket.chat/apps-engine': patch +'@rocket.chat/ui-composer': patch +'@rocket.chat/ui-contexts': patch +'@rocket.chat/gazzodown': patch +'@rocket.chat/ui-avatar': patch +'@rocket.chat/ui-client': patch +'@rocket.chat/livechat': patch +'@rocket.chat/ui-voip': patch +'@rocket.chat/i18n': patch +'@rocket.chat/meteor': patch +--- + +Fixes an error where the engine would not retry a subprocess restart if the last attempt failed diff --git a/.changeset/bump-patch-1732728542480.md b/.changeset/bump-patch-1732728542480.md deleted file mode 100644 index e1eaa7980afb..000000000000 --- a/.changeset/bump-patch-1732728542480.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Bump @rocket.chat/meteor version. diff --git a/.changeset/clean-flies-collect.md b/.changeset/clean-flies-collect.md deleted file mode 100644 index 4921355b51e8..000000000000 --- a/.changeset/clean-flies-collect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fix user highlights not matching only whole words diff --git a/.changeset/curvy-flies-greet.md b/.changeset/curvy-flies-greet.md deleted file mode 100644 index aeac8382b152..000000000000 --- a/.changeset/curvy-flies-greet.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Remove unused client side `setUserActiveStatus` meteor method. diff --git a/.changeset/empty-pans-love.md b/.changeset/empty-pans-love.md new file mode 100644 index 000000000000..a3ab8e26c8aa --- /dev/null +++ b/.changeset/empty-pans-love.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +--- + +Fixes the incorrect registration status shown on admin users page for federated remote users. diff --git a/.changeset/fair-carrots-trade.md b/.changeset/fair-carrots-trade.md new file mode 100644 index 000000000000..9c479c941ddf --- /dev/null +++ b/.changeset/fair-carrots-trade.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +--- + +Fixes "Average first response time" and "Best first response time" metrics being associated with the last agent who served the room (instead of the first one) diff --git a/.changeset/fair-colts-remain.md b/.changeset/fair-colts-remain.md deleted file mode 100644 index 7ce003e50fd5..000000000000 --- a/.changeset/fair-colts-remain.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/account-service": patch -"@rocket.chat/authorization-service": patch -"@rocket.chat/ddp-streamer": patch -"@rocket.chat/omnichannel-transcript": patch -"@rocket.chat/presence-service": patch -"@rocket.chat/queue-worker": patch -"@rocket.chat/stream-hub-service": patch -"rocketchat-services": patch ---- - -Bump meteor to 3.0.4 and Node version to 20.18.0 diff --git a/.changeset/fifty-parrots-wonder.md b/.changeset/fifty-parrots-wonder.md new file mode 100644 index 000000000000..f0078cd51136 --- /dev/null +++ b/.changeset/fifty-parrots-wonder.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue preventing the creation of normal direct message rooms due to an invalid federation configuration, allowing proper room creation under standard settings. diff --git a/.changeset/five-peaches-approve.md b/.changeset/five-peaches-approve.md new file mode 100644 index 000000000000..090e3716c025 --- /dev/null +++ b/.changeset/five-peaches-approve.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Adds support for `Use Full Name Initials to Generate Default Avatar` setting for the generated avatar preview button when editing an User's avatar diff --git a/.changeset/forty-gorillas-kneel.md b/.changeset/forty-gorillas-kneel.md deleted file mode 100644 index 42df0ed8c0e4..000000000000 --- a/.changeset/forty-gorillas-kneel.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/apps-engine": patch ---- - -Deprecated the `from` field in the apps email bridge and made it optional, using the server's settings when the field is omitted diff --git a/.changeset/friendly-ravens-teach.md b/.changeset/friendly-ravens-teach.md deleted file mode 100644 index 1c464a8679b6..000000000000 --- a/.changeset/friendly-ravens-teach.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': minor ---- - -adds unread badge to sidebar collapser diff --git a/.changeset/fuzzy-coins-lay.md b/.changeset/fuzzy-coins-lay.md new file mode 100644 index 000000000000..5929829e41ab --- /dev/null +++ b/.changeset/fuzzy-coins-lay.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes missing images in Twitter article links to ensure proper display by relying on meta tags. diff --git a/.changeset/giant-nails-trade.md b/.changeset/giant-nails-trade.md new file mode 100644 index 000000000000..76e92e999247 --- /dev/null +++ b/.changeset/giant-nails-trade.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/apps-engine': patch +--- + +Adds simple app subprocess metrics report diff --git a/.changeset/gold-comics-taste.md b/.changeset/gold-comics-taste.md new file mode 100644 index 000000000000..c9f864a0ffb5 --- /dev/null +++ b/.changeset/gold-comics-taste.md @@ -0,0 +1,22 @@ +--- +'@rocket.chat/fuselage-ui-kit': patch +'@rocket.chat/instance-status': patch +'@rocket.chat/ui-theming': patch +'@rocket.chat/model-typings': patch +'@rocket.chat/ui-video-conf': patch +'@rocket.chat/uikit-playground': patch +'@rocket.chat/core-typings': patch +'@rocket.chat/rest-typings': patch +'@rocket.chat/apps-engine': patch +'@rocket.chat/ui-composer': patch +'@rocket.chat/ui-contexts': patch +'@rocket.chat/gazzodown': patch +'@rocket.chat/ui-avatar': patch +'@rocket.chat/ui-client': patch +'@rocket.chat/livechat': patch +'@rocket.chat/ui-voip': patch +'@rocket.chat/i18n': patch +'@rocket.chat/meteor': patch +--- + +Fixes error propagation when trying to get the status of apps in some cases diff --git a/.changeset/green-papayas-thank.md b/.changeset/green-papayas-thank.md deleted file mode 100644 index 22547db942ef..000000000000 --- a/.changeset/green-papayas-thank.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@rocket.chat/model-typings': patch -'@rocket.chat/core-typings': patch -'@rocket.chat/meteor': patch ---- - -Fixes an issue where updating custom emojis didn’t work as expected, ensuring that uploaded emojis now update correctly and display without any caching problems. diff --git a/.changeset/happy-stingrays-provide.md b/.changeset/happy-stingrays-provide.md deleted file mode 100644 index fba25665133a..000000000000 --- a/.changeset/happy-stingrays-provide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixes issue that could cause multiple discussions to be created when creating it from a message action diff --git a/.changeset/honest-kings-allow.md b/.changeset/honest-kings-allow.md new file mode 100644 index 000000000000..7ab8d0abc1a8 --- /dev/null +++ b/.changeset/honest-kings-allow.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/apps-engine': patch +--- + +Attempts to restart an app subprocess if the spawn command fails diff --git a/.changeset/honest-pumpkins-joke.md b/.changeset/honest-pumpkins-joke.md deleted file mode 100644 index aa1abce9ad6d..000000000000 --- a/.changeset/honest-pumpkins-joke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -adds missing html attributes in sidebar item templates diff --git a/.changeset/kind-crabs-live.md b/.changeset/kind-crabs-live.md new file mode 100644 index 000000000000..93b2218f828b --- /dev/null +++ b/.changeset/kind-crabs-live.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +--- + +fixes "Change to language" button in login page not displaying the target language diff --git a/.changeset/lazy-avocados-whisper.md b/.changeset/lazy-avocados-whisper.md deleted file mode 100644 index b1296186c37c..000000000000 --- a/.changeset/lazy-avocados-whisper.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/i18n": minor ---- - -Improves thread metrics featuring user avatars, better titles and repositioned elements. diff --git a/.changeset/lemon-foxes-carry.md b/.changeset/lemon-foxes-carry.md deleted file mode 100644 index 7e14dda30747..000000000000 --- a/.changeset/lemon-foxes-carry.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/i18n": patch ---- - -Fixes message character limit not being applied to file upload descriptions \ No newline at end of file diff --git a/.changeset/lemon-stingrays-invite.md b/.changeset/lemon-stingrays-invite.md new file mode 100644 index 000000000000..eb57df68c636 --- /dev/null +++ b/.changeset/lemon-stingrays-invite.md @@ -0,0 +1,9 @@ +--- +'@rocket.chat/model-typings': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/freeswitch': minor +'@rocket.chat/models': minor +'@rocket.chat/meteor': minor +--- + +Allows Rocket.Chat to store call events. diff --git a/.changeset/light-terms-ring.md b/.changeset/light-terms-ring.md deleted file mode 100644 index 4437c5c4d596..000000000000 --- a/.changeset/light-terms-ring.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixes the issue where newly created teams are incorrectly displayed as channels on the sidebar when the DISABLE_DB_WATCHERS environment variable is enabled diff --git a/.changeset/lovely-beers-argue.md b/.changeset/lovely-beers-argue.md new file mode 100644 index 000000000000..a01a97535e48 --- /dev/null +++ b/.changeset/lovely-beers-argue.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes messages not being processed for all slack servers diff --git a/.changeset/lucky-wolves-turn.md b/.changeset/lucky-wolves-turn.md new file mode 100644 index 000000000000..0dbb08af9e0f --- /dev/null +++ b/.changeset/lucky-wolves-turn.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes a UI issue that showed the incorrect migration number on the `Information` page. This was caused by a function calculating the stats before the server had migrated the database and updated the control. diff --git a/.changeset/mean-cobras-sneeze.md b/.changeset/mean-cobras-sneeze.md deleted file mode 100644 index 39717f0c0d89..000000000000 --- a/.changeset/mean-cobras-sneeze.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@rocket.chat/rest-typings': minor -'@rocket.chat/meteor': minor ---- - -Adds cursor pagination on chat.syncMessages endpoint diff --git a/.changeset/metal-avocados-serve.md b/.changeset/metal-avocados-serve.md deleted file mode 100644 index 478407fcb97b..000000000000 --- a/.changeset/metal-avocados-serve.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/livechat": patch ---- - -Fixes the 'Finish Chat' option in Livechat appearing before the conversation is started, which caused the action to fail. diff --git a/.changeset/neat-flies-drive.md b/.changeset/neat-flies-drive.md deleted file mode 100644 index 27b5270f81f9..000000000000 --- a/.changeset/neat-flies-drive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Adds a divider on Create team modal - advanced settings diff --git a/.changeset/nervous-fireants-wash.md b/.changeset/nervous-fireants-wash.md new file mode 100644 index 000000000000..441202512864 --- /dev/null +++ b/.changeset/nervous-fireants-wash.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Allows default avatars to be generated with more than one inital (limited to first 3) when setting `Use Full Name Initials to Generate Default Avatar` is true. diff --git a/.changeset/nervous-rivers-fry.md b/.changeset/nervous-rivers-fry.md deleted file mode 100644 index 278259c53a86..000000000000 --- a/.changeset/nervous-rivers-fry.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@rocket.chat/apps-engine': patch -'@rocket.chat/meteor': patch ---- - -Fixed an issue that would grant network permission to app's processes in wrong cases diff --git a/.changeset/new-mails-add.md b/.changeset/new-mails-add.md new file mode 100644 index 000000000000..87446ac8b4c1 --- /dev/null +++ b/.changeset/new-mails-add.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/livechat": patch +--- + +Fixes issue that caused different sessions when opening a livechat popover in cross domain diff --git a/.changeset/ninety-bulldogs-dream.md b/.changeset/ninety-bulldogs-dream.md new file mode 100644 index 000000000000..1fe15fb79350 --- /dev/null +++ b/.changeset/ninety-bulldogs-dream.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue where removing the only message of a thread would keep the unread thread messages badge diff --git a/.changeset/old-coins-bow.md b/.changeset/old-coins-bow.md deleted file mode 100644 index 1790cc205160..000000000000 --- a/.changeset/old-coins-bow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/apps-engine': patch ---- - -Fixes an issue that would cause apps to appear disabled after a subprocess restart diff --git a/.changeset/perfect-ties-tell.md b/.changeset/perfect-ties-tell.md new file mode 100644 index 000000000000..e129fa0937c8 --- /dev/null +++ b/.changeset/perfect-ties-tell.md @@ -0,0 +1,21 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/core-typings": minor +"@rocket.chat/model-typings": minor +--- + +Adds statistics related to the new **Contact Identification** feature: +- `totalContacts`: Total number of contacts; +- `totalUnknownContacts`: Total number of unknown contacts; +- `totalMergedContacts`: Total number of merged contacts; +- `totalConflicts`: Total number of merge conflicts; +- `totalResolvedConflicts`: Total number of resolved conflicts; +- `totalBlockedContacts`: Total number of blocked contacts; +- `totalPartiallyBlockedContacts`: Total number of partially blocked contacts; +- `totalFullyBlockedContacts`: Total number of fully blocked contacts; +- `totalVerifiedContacts`: Total number of verified contacts; +- `avgChannelsPerContact`: Average number of channels per contact; +- `totalContactsWithoutChannels`: Number of contacts without channels; +- `totalImportedContacts`: Total number of imported contacts; +- `totalUpsellViews`: Total number of "Advanced Contact Management" Upsell CTA views; +- `totalUpsellClicks`: Total number of "Advanced Contact Management" Upsell CTA clicks; diff --git a/.changeset/pink-dodos-greet.md b/.changeset/pink-dodos-greet.md deleted file mode 100644 index f122a2e72fc7..000000000000 --- a/.changeset/pink-dodos-greet.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixes sidepanel not replicating sidebar sort preference diff --git a/.changeset/plenty-snakes-dream.md b/.changeset/plenty-snakes-dream.md deleted file mode 100644 index eecbf0cbb466..000000000000 --- a/.changeset/plenty-snakes-dream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": minor ---- - -Adds a new route to allow fetching avatars by the user's id `/avatar/uid/` diff --git a/.changeset/popular-queens-brake.md b/.changeset/popular-queens-brake.md deleted file mode 100644 index 5114920b8fde..000000000000 --- a/.changeset/popular-queens-brake.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@rocket.chat/model-typings': minor -'@rocket.chat/core-typings': minor -'@rocket.chat/rest-typings': minor -'@rocket.chat/apps-engine': minor -'@rocket.chat/i18n': minor -'@rocket.chat/meteor': minor ---- - -These changes aims to add: -- A brand-new omnichannel contact profile -- The ability to communicate with known contacts only -- Communicate with verified contacts only -- Merge verified contacts across different channels -- Block contact channels -- Resolve conflicting contact information when registered via different channels -- An advanced contact center filters diff --git a/.changeset/pre.json b/.changeset/pre.json deleted file mode 100644 index e552e7242eed..000000000000 --- a/.changeset/pre.json +++ /dev/null @@ -1,111 +0,0 @@ -{ - "mode": "pre", - "tag": "rc", - "initialVersions": { - "@rocket.chat/meteor": "7.1.0-develop", - "rocketchat-services": "2.0.0", - "@rocket.chat/uikit-playground": "0.6.0", - "@rocket.chat/account-service": "0.4.9", - "@rocket.chat/authorization-service": "0.4.9", - "@rocket.chat/ddp-streamer": "0.3.9", - "@rocket.chat/omnichannel-transcript": "0.4.9", - "@rocket.chat/presence-service": "0.4.9", - "@rocket.chat/queue-worker": "0.4.9", - "@rocket.chat/stream-hub-service": "0.4.9", - "@rocket.chat/license": "1.0.0", - "@rocket.chat/network-broker": "0.1.1", - "@rocket.chat/omnichannel-services": "0.3.6", - "@rocket.chat/pdf-worker": "0.2.6", - "@rocket.chat/presence": "0.2.9", - "@rocket.chat/ui-theming": "0.4.0", - "@rocket.chat/account-utils": "0.0.2", - "@rocket.chat/agenda": "0.1.0", - "@rocket.chat/api-client": "0.2.9", - "@rocket.chat/apps": "0.2.0", - "@rocket.chat/apps-engine": "1.47.0", - "@rocket.chat/base64": "1.0.13", - "@rocket.chat/cas-validate": "0.0.2", - "@rocket.chat/core-services": "0.7.1", - "@rocket.chat/core-typings": "7.1.0-develop", - "@rocket.chat/cron": "0.1.9", - "@rocket.chat/ddp-client": "0.3.9", - "@rocket.chat/eslint-config": "0.7.0", - "@rocket.chat/favicon": "0.0.2", - "@rocket.chat/freeswitch": "1.0.0", - "@rocket.chat/fuselage-ui-kit": "12.0.0", - "@rocket.chat/gazzodown": "12.0.0", - "@rocket.chat/i18n": "1.0.0", - "@rocket.chat/instance-status": "0.1.9", - "@rocket.chat/jest-presets": "0.0.1", - "@rocket.chat/jwt": "0.1.1", - "@rocket.chat/livechat": "1.20.1", - "@rocket.chat/log-format": "0.0.2", - "@rocket.chat/logger": "0.0.2", - "@rocket.chat/message-parser": "0.31.31", - "@rocket.chat/mock-providers": "0.1.4", - "@rocket.chat/model-typings": "1.0.0", - "@rocket.chat/models": "1.0.0", - "@rocket.chat/poplib": "0.0.2", - "@rocket.chat/password-policies": "0.0.2", - "@rocket.chat/patch-injection": "0.0.1", - "@rocket.chat/peggy-loader": "0.31.27", - "@rocket.chat/random": "1.2.2", - "@rocket.chat/release-action": "2.2.3", - "@rocket.chat/release-changelog": "0.1.0", - "@rocket.chat/rest-typings": "7.1.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.2", - "@rocket.chat/tracing": "0.0.1", - "@rocket.chat/ui-avatar": "8.0.0", - "@rocket.chat/ui-client": "12.0.0", - "@rocket.chat/ui-composer": "0.4.0", - "@rocket.chat/ui-contexts": "12.0.0", - "@rocket.chat/ui-kit": "0.37.0", - "@rocket.chat/ui-video-conf": "12.0.0", - "@rocket.chat/ui-voip": "2.0.0", - "@rocket.chat/web-ui-registration": "12.0.0" - }, - "changesets": [ - "bump-patch-1732728542480", - "clean-flies-collect", - "curvy-flies-greet", - "fair-colts-remain", - "forty-gorillas-kneel", - "friendly-ravens-teach", - "green-papayas-thank", - "happy-stingrays-provide", - "honest-pumpkins-joke", - "lazy-avocados-whisper", - "lemon-foxes-carry", - "light-terms-ring", - "mean-cobras-sneeze", - "metal-avocados-serve", - "neat-flies-drive", - "nervous-rivers-fry", - "old-coins-bow", - "pink-dodos-greet", - "plenty-snakes-dream", - "popular-queens-brake", - "real-jeans-worry", - "serious-mice-film", - "seven-berries-check", - "seven-otters-fold", - "silent-steaks-happen", - "smart-radios-reflect", - "spicy-spiders-search", - "spotty-ads-knock", - "stale-actors-enjoy", - "sweet-needles-melt", - "swift-suns-perform", - "three-dragons-brush", - "tricky-trees-destroy", - "twelve-horses-suffer", - "twenty-news-own", - "two-guests-tan", - "unlucky-kangaroos-yawn", - "unlucky-wasps-check", - "weak-trees-exercise" - ] -} diff --git a/.changeset/pretty-islands-wink.md b/.changeset/pretty-islands-wink.md new file mode 100644 index 000000000000..e9de264504b0 --- /dev/null +++ b/.changeset/pretty-islands-wink.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Disables OTR messages selection when exporting messages diff --git a/.changeset/proud-planets-applaud.md b/.changeset/proud-planets-applaud.md new file mode 100644 index 000000000000..7f692f780271 --- /dev/null +++ b/.changeset/proud-planets-applaud.md @@ -0,0 +1,22 @@ +--- +'@rocket.chat/fuselage-ui-kit': patch +'@rocket.chat/instance-status': patch +'@rocket.chat/ui-theming': patch +'@rocket.chat/model-typings': patch +'@rocket.chat/ui-video-conf': patch +'@rocket.chat/uikit-playground': patch +'@rocket.chat/core-typings': patch +'@rocket.chat/rest-typings': patch +'@rocket.chat/apps-engine': patch +'@rocket.chat/ui-composer': patch +'@rocket.chat/ui-contexts': patch +'@rocket.chat/gazzodown': patch +'@rocket.chat/ui-avatar': patch +'@rocket.chat/ui-client': patch +'@rocket.chat/livechat': patch +'@rocket.chat/ui-voip': patch +'@rocket.chat/i18n': patch +'@rocket.chat/meteor': patch +--- + +Fixes wrong data being reported to total failed apps metrics and statistics diff --git a/.changeset/quiet-lions-unite.md b/.changeset/quiet-lions-unite.md new file mode 100644 index 000000000000..723d773c0b02 --- /dev/null +++ b/.changeset/quiet-lions-unite.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes sidepanel list not sorting by last message inside thread diff --git a/.changeset/quiet-radios-fry.md b/.changeset/quiet-radios-fry.md new file mode 100644 index 000000000000..b3b7209cb041 --- /dev/null +++ b/.changeset/quiet-radios-fry.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/apps-engine': patch +--- + +Fixes an issue while collecting the error message from a failed restart attempt of an app subprocess diff --git a/.changeset/real-jeans-worry.md b/.changeset/real-jeans-worry.md deleted file mode 100644 index 9b16e7681a98..000000000000 --- a/.changeset/real-jeans-worry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixes banner breaking the UI with specific payloads diff --git a/.changeset/serious-mice-film.md b/.changeset/serious-mice-film.md deleted file mode 100644 index 35a2d6704071..000000000000 --- a/.changeset/serious-mice-film.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixes client-side updates for recent emoji list when custom emojis are modified. diff --git a/.changeset/seven-berries-check.md b/.changeset/seven-berries-check.md deleted file mode 100644 index b8cd3c49897f..000000000000 --- a/.changeset/seven-berries-check.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/i18n": patch ---- - -Adds "Master volume" and "Call ringer volume" to the user preferences sound section. diff --git a/.changeset/seven-otters-fold.md b/.changeset/seven-otters-fold.md deleted file mode 100644 index 7f2af2075f73..000000000000 --- a/.changeset/seven-otters-fold.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Sends server statistics only once a day despite multiple instance being started at different times. diff --git a/.changeset/seven-owls-tell.md b/.changeset/seven-owls-tell.md new file mode 100644 index 000000000000..0c2cda791671 --- /dev/null +++ b/.changeset/seven-owls-tell.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed an issue that added potencially infinite callbacks to the same event, degrading performance over time. diff --git a/.changeset/shaggy-bulldogs-beg.md b/.changeset/shaggy-bulldogs-beg.md new file mode 100644 index 000000000000..211d11d7b67c --- /dev/null +++ b/.changeset/shaggy-bulldogs-beg.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/ui-composer': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Introduces a new option when exporting messages, allowing users to select and download a JSON file directly from client diff --git a/.changeset/silent-steaks-happen.md b/.changeset/silent-steaks-happen.md deleted file mode 100644 index 1ae791c68177..000000000000 --- a/.changeset/silent-steaks-happen.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/i18n": minor ---- - -Improves the customizability of the naming of automatic Persistent video calls discussions, allowing the date of the call to be in different parts of the name, using the `[date]` keyword. diff --git a/.changeset/slow-readers-help.md b/.changeset/slow-readers-help.md new file mode 100644 index 000000000000..2fe4881b56a0 --- /dev/null +++ b/.changeset/slow-readers-help.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue where the update banner wasn't showing the new version number diff --git a/.changeset/smart-radios-reflect.md b/.changeset/smart-radios-reflect.md deleted file mode 100644 index 58ea7413e51c..000000000000 --- a/.changeset/smart-radios-reflect.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/core-services": patch ---- - -stops calling an object through proxy calling getQueueWorker diff --git a/.changeset/sour-roses-invite.md b/.changeset/sour-roses-invite.md new file mode 100644 index 000000000000..d560be689142 --- /dev/null +++ b/.changeset/sour-roses-invite.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +--- + +Improves the workspace and subscription admin pages by updating font scaling, centralizing elements, +enhancing responsiveness, and refactoring components to provide a better overall user experience. diff --git a/.changeset/spicy-spiders-search.md b/.changeset/spicy-spiders-search.md deleted file mode 100644 index d86bc93313c5..000000000000 --- a/.changeset/spicy-spiders-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed an issue where the installed apps list would go stale without a refresh in some cases diff --git a/.changeset/spotty-ads-knock.md b/.changeset/spotty-ads-knock.md deleted file mode 100644 index b40e70b74a98..000000000000 --- a/.changeset/spotty-ads-knock.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixes display of emoji aliases in custom emoji list by adding commas between aliases diff --git a/.changeset/stale-actors-enjoy.md b/.changeset/stale-actors-enjoy.md deleted file mode 100644 index baff2b19b667..000000000000 --- a/.changeset/stale-actors-enjoy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixes `waiting queue` feature. When `Livechat_waiting_queue` setting is enabled, incoming conversations should be sent to the queue instead of being assigned directly. diff --git a/.changeset/sweet-needles-melt.md b/.changeset/sweet-needles-melt.md deleted file mode 100644 index 51cd6e03d831..000000000000 --- a/.changeset/sweet-needles-melt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixes edge case of thread unread not being added to unread group diff --git a/.changeset/swift-suns-perform.md b/.changeset/swift-suns-perform.md deleted file mode 100644 index 2a52249d984c..000000000000 --- a/.changeset/swift-suns-perform.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/livechat": patch ---- - -Fixes livechat popout mode not working correctly in cross domain situations diff --git a/.changeset/three-dragons-brush.md b/.changeset/three-dragons-brush.md deleted file mode 100644 index d80c4dc83306..000000000000 --- a/.changeset/three-dragons-brush.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@rocket.chat/apps-engine': minor -'@rocket.chat/livechat': minor -'@rocket.chat/i18n': minor -'@rocket.chat/meteor': minor ---- - -Prevent apps' subprocesses from crashing on unhandled rejections or uncaught exceptions diff --git a/.changeset/tricky-trees-destroy.md b/.changeset/tricky-trees-destroy.md deleted file mode 100644 index 3d43cc5b571a..000000000000 --- a/.changeset/tricky-trees-destroy.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- -Adds login and permission validation for resetIrcConnection method diff --git a/.changeset/twelve-horses-suffer.md b/.changeset/twelve-horses-suffer.md deleted file mode 100644 index bc7f7d5b3ba4..000000000000 --- a/.changeset/twelve-horses-suffer.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@rocket.chat/i18n': minor -'@rocket.chat/meteor': minor ---- - -Adds a confirmation modal to the cancel subscription action diff --git a/.changeset/twenty-news-own.md b/.changeset/twenty-news-own.md deleted file mode 100644 index c48d06e0a05e..000000000000 --- a/.changeset/twenty-news-own.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/i18n": minor ---- - -Disables the possiblity to upload exempted apps diff --git a/.changeset/two-guests-tan.md b/.changeset/two-guests-tan.md deleted file mode 100644 index ff44f0ef493b..000000000000 --- a/.changeset/two-guests-tan.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/apps-engine': patch ---- - -Removed the 1 second timeout of `Pre` app events. Now they will follow the "global" configuration diff --git a/.changeset/unlucky-kangaroos-yawn.md b/.changeset/unlucky-kangaroos-yawn.md deleted file mode 100644 index 1aaa97cbd8d8..000000000000 --- a/.changeset/unlucky-kangaroos-yawn.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/i18n": patch ---- - -Updates VoIP field labels from 'Free Extension Numbers' to 'Available Extensions' to better describe the field's purpose and improve clarity. diff --git a/.changeset/unlucky-wasps-check.md b/.changeset/unlucky-wasps-check.md deleted file mode 100644 index fd7e8af17824..000000000000 --- a/.changeset/unlucky-wasps-check.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixes an issue where resizable handler renders over the expanded thread view while using contextualbarResizable feature preview diff --git a/.changeset/violet-pets-attend.md b/.changeset/violet-pets-attend.md new file mode 100644 index 000000000000..f93079c94fa4 --- /dev/null +++ b/.changeset/violet-pets-attend.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/ui-client': patch +'@rocket.chat/meteor': patch +--- + +Fixed the data structure of the features preview diff --git a/.changeset/weak-trees-exercise.md b/.changeset/weak-trees-exercise.md deleted file mode 100644 index 230c087ccd83..000000000000 --- a/.changeset/weak-trees-exercise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/apps-engine': minor ---- - -Add support to configure apps runtime timeout via the APPS_ENGINE_RUNTIME_TIMEOUT environment variable diff --git a/.changeset/wet-chicken-scream.md b/.changeset/wet-chicken-scream.md new file mode 100644 index 000000000000..610be5e8cd44 --- /dev/null +++ b/.changeset/wet-chicken-scream.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes contact update failing in case a custom field is removed from the workspace diff --git a/.changeset/wicked-socks-hide.md b/.changeset/wicked-socks-hide.md new file mode 100644 index 000000000000..3f425c3228e2 --- /dev/null +++ b/.changeset/wicked-socks-hide.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/core-typings": minor +"@rocket.chat/rest-typings": minor +--- + +Adds a new callout in the subscription page to inform users of subscription upgrade eligibility when applicable. diff --git a/.changeset/wise-queens-build.md b/.changeset/wise-queens-build.md new file mode 100644 index 000000000000..51e54de4a6bd --- /dev/null +++ b/.changeset/wise-queens-build.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where the notification sound was playing randomly diff --git a/.changeset/young-dots-cheat.md b/.changeset/young-dots-cheat.md new file mode 100644 index 000000000000..e8d3b6c5bff6 --- /dev/null +++ b/.changeset/young-dots-cheat.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/apps-engine': patch +--- + +Prevents app:getStatus requests from timing out in some cases diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index c4e935205c2b..d5319e745e35 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,192 @@ # @rocket.chat/meteor +## 7.1.0 + +### Minor Changes + +- ([#33897](https://github.com/RocketChat/Rocket.Chat/pull/33897)) adds unread badge to sidebar collapser + +- ([#32906](https://github.com/RocketChat/Rocket.Chat/pull/32906)) Improves thread metrics featuring user avatars, better titles and repositioned elements. + +- ([#33810](https://github.com/RocketChat/Rocket.Chat/pull/33810)) Adds cursor pagination on chat.syncMessages endpoint + +- ([#33214](https://github.com/RocketChat/Rocket.Chat/pull/33214)) Adds a new route to allow fetching avatars by the user's id `/avatar/uid/` + +- ([#32727](https://github.com/RocketChat/Rocket.Chat/pull/32727)) These changes aims to add: + - A brand-new omnichannel contact profile + - The ability to communicate with known contacts only + - Communicate with verified contacts only + - Merge verified contacts across different channels + - Block contact channels + - Resolve conflicting contact information when registered via different channels + - An advanced contact center filters +- ([#33920](https://github.com/RocketChat/Rocket.Chat/pull/33920)) Improves the customizability of the naming of automatic Persistent video calls discussions, allowing the date of the call to be in different parts of the name, using the `[date]` keyword. + +- ([#33997](https://github.com/RocketChat/Rocket.Chat/pull/33997)) Prevent apps' subprocesses from crashing on unhandled rejections or uncaught exceptions + +- ([#33814](https://github.com/RocketChat/Rocket.Chat/pull/33814)) Adds a confirmation modal to the cancel subscription action + +- ([#33949](https://github.com/RocketChat/Rocket.Chat/pull/33949)) Disables the possiblity to upload exempted apps + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- ([#33776](https://github.com/RocketChat/Rocket.Chat/pull/33776)) Fix user highlights not matching only whole words + +- ([#33818](https://github.com/RocketChat/Rocket.Chat/pull/33818)) Remove unused client side `setUserActiveStatus` meteor method. + +- ([#33596](https://github.com/RocketChat/Rocket.Chat/pull/33596)) Bump meteor to 3.0.4 and Node version to 20.18.0 + +- ([#33713](https://github.com/RocketChat/Rocket.Chat/pull/33713)) Deprecated the `from` field in the apps email bridge and made it optional, using the server's settings when the field is omitted + +- ([#32991](https://github.com/RocketChat/Rocket.Chat/pull/32991)) Fixes an issue where updating custom emojis didn’t work as expected, ensuring that uploaded emojis now update correctly and display without any caching problems. + +- ([#33985](https://github.com/RocketChat/Rocket.Chat/pull/33985)) Fixes issue that could cause multiple discussions to be created when creating it from a message action + +- ([#33904](https://github.com/RocketChat/Rocket.Chat/pull/33904)) adds missing html attributes in sidebar item templates + +- ([#33218](https://github.com/RocketChat/Rocket.Chat/pull/33218)) Fixes message character limit not being applied to file upload descriptions + +- ([#33908](https://github.com/RocketChat/Rocket.Chat/pull/33908)) Fixes the issue where newly created teams are incorrectly displayed as channels on the sidebar when the DISABLE_DB_WATCHERS environment variable is enabled + +- ([#33953](https://github.com/RocketChat/Rocket.Chat/pull/33953)) Adds a divider on Create team modal - advanced settings + +- ([#33786](https://github.com/RocketChat/Rocket.Chat/pull/33786)) Fixed an issue that would grant network permission to app's processes in wrong cases + +- ([#33986](https://github.com/RocketChat/Rocket.Chat/pull/33986)) Fixes sidepanel not replicating sidebar sort preference + +- ([#33689](https://github.com/RocketChat/Rocket.Chat/pull/33689)) Fixes banner breaking the UI with specific payloads + +- ([#33808](https://github.com/RocketChat/Rocket.Chat/pull/33808)) Fixes client-side updates for recent emoji list when custom emojis are modified. + +- ([#33902](https://github.com/RocketChat/Rocket.Chat/pull/33902)) Adds "Master volume" and "Call ringer volume" to the user preferences sound section. + +- ([#33311](https://github.com/RocketChat/Rocket.Chat/pull/33311)) Sends server statistics only once a day despite multiple instance being started at different times. + +- ([#33719](https://github.com/RocketChat/Rocket.Chat/pull/33719)) stops calling an object through proxy calling getQueueWorker + +- ([#33785](https://github.com/RocketChat/Rocket.Chat/pull/33785)) Fixed an issue where the installed apps list would go stale without a refresh in some cases + +- ([#33278](https://github.com/RocketChat/Rocket.Chat/pull/33278)) Fixes display of emoji aliases in custom emoji list by adding commas between aliases + +- ([#33772](https://github.com/RocketChat/Rocket.Chat/pull/33772)) Fixes `waiting queue` feature. When `Livechat_waiting_queue` setting is enabled, incoming conversations should be sent to the queue instead of being assigned directly. + +- ([#33963](https://github.com/RocketChat/Rocket.Chat/pull/33963)) Fixes edge case of thread unread not being added to unread group + +- ([#33994](https://github.com/RocketChat/Rocket.Chat/pull/33994)) Adds login and permission validation for resetIrcConnection method + +- ([#33880](https://github.com/RocketChat/Rocket.Chat/pull/33880)) Updates VoIP field labels from 'Free Extension Numbers' to 'Available Extensions' to better describe the field's purpose and improve clarity. + +- ([#33958](https://github.com/RocketChat/Rocket.Chat/pull/33958)) Fixes an issue where resizable handler renders over the expanded thread view while using contextualbarResizable feature preview + +-
Updated dependencies [82767d8fd8a52ac348e8aded1d238e688d36129b, 80e36bfc3938775eb26aa5576f1b9b98896e1cc4, 66ecc64fc1d4464ad2818ad04e23a09cdf221194, 6c83bf0657004ee9cf43d5c832f51826a6591165, e7edeac3bdd22da0a04b8e873d5a008e249fb4be, 3569b0a9c48f8b94ebaef2f8b607c52fdb8e570a, b4841cb7206d855d7a1bc7604683a5b4a48b7176, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0, d1e6a73796269824fb1aa7afcc7b8aa242e34e90, 661cc01237629ce83699d6c25df25d12985e88bf, 63ccadc012499e004445ad6bc6cd2ff777aecbd1, ce7024af36fcde97b1da5b2731f6edc4a4c236b8, 616655585cb1c5c60d7cee97e25b17af3dfda794, e5fe727f6a2f0e60cdf7ba225e1f6caa6db2045c, d398866dba725918017e3609807f9d0ab9b89b72, 322bafd4bd1fe91ed34610501b269e4d8951944c, d398866dba725918017e3609807f9d0ab9b89b72]: + + - @rocket.chat/apps-engine@1.48.0 + - @rocket.chat/model-typings@1.1.0 + - @rocket.chat/core-typings@7.1.0 + - @rocket.chat/i18n@1.1.0 + - @rocket.chat/rest-typings@7.1.0 + - @rocket.chat/core-services@0.7.2 + - @rocket.chat/presence@0.2.10 + - @rocket.chat/apps@0.2.1 + - @rocket.chat/fuselage-ui-kit@13.0.0 + - @rocket.chat/omnichannel-services@0.3.7 + - @rocket.chat/models@1.0.1 + - @rocket.chat/license@1.0.1 + - @rocket.chat/pdf-worker@0.2.7 + - @rocket.chat/api-client@0.2.10 + - @rocket.chat/cron@0.1.10 + - @rocket.chat/freeswitch@1.0.1 + - @rocket.chat/gazzodown@13.0.0 + - @rocket.chat/ui-contexts@13.0.0 + - @rocket.chat/web-ui-registration@13.0.0 + - @rocket.chat/network-broker@0.1.2 + - @rocket.chat/instance-status@0.1.10 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/ui-theming@0.4.0 + - @rocket.chat/ui-avatar@9.0.0 + - @rocket.chat/ui-client@13.0.0 + - @rocket.chat/ui-video-conf@13.0.0 + - @rocket.chat/ui-voip@3.0.0 +
+ +## 7.1.0-rc.3 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 + - @rocket.chat/rest-typings@7.1.0-rc.3 + - @rocket.chat/license@1.0.1-rc.3 + - @rocket.chat/omnichannel-services@0.3.7-rc.3 + - @rocket.chat/pdf-worker@0.2.7-rc.3 + - @rocket.chat/presence@0.2.10-rc.3 + - @rocket.chat/api-client@0.2.10-rc.3 + - @rocket.chat/apps@0.2.1-rc.3 + - @rocket.chat/core-services@0.7.2-rc.3 + - @rocket.chat/cron@0.1.10-rc.3 + - @rocket.chat/freeswitch@1.0.1-rc.3 + - @rocket.chat/fuselage-ui-kit@13.0.0-rc.3 + - @rocket.chat/gazzodown@13.0.0-rc.3 + - @rocket.chat/model-typings@1.1.0-rc.3 + - @rocket.chat/ui-contexts@13.0.0-rc.3 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.1.2-rc.3 + - @rocket.chat/models@1.0.1-rc.3 + - @rocket.chat/ui-theming@0.4.0 + - @rocket.chat/ui-avatar@9.0.0-rc.3 + - @rocket.chat/ui-client@13.0.0-rc.3 + - @rocket.chat/ui-video-conf@13.0.0-rc.3 + - @rocket.chat/ui-voip@3.0.0-rc.3 + - @rocket.chat/web-ui-registration@13.0.0-rc.3 + - @rocket.chat/instance-status@0.1.10-rc.3 +
+ +## 7.1.0-rc.2 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 + - @rocket.chat/rest-typings@7.1.0-rc.2 + - @rocket.chat/license@1.0.1-rc.2 + - @rocket.chat/omnichannel-services@0.3.7-rc.2 + - @rocket.chat/pdf-worker@0.2.7-rc.2 + - @rocket.chat/presence@0.2.10-rc.2 + - @rocket.chat/api-client@0.2.10-rc.2 + - @rocket.chat/apps@0.2.1-rc.2 + - @rocket.chat/core-services@0.7.2-rc.2 + - @rocket.chat/cron@0.1.10-rc.2 + - @rocket.chat/freeswitch@1.0.1-rc.2 + - @rocket.chat/fuselage-ui-kit@13.0.0-rc.2 + - @rocket.chat/gazzodown@13.0.0-rc.2 + - @rocket.chat/model-typings@1.1.0-rc.2 + - @rocket.chat/ui-contexts@13.0.0-rc.2 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.1.2-rc.2 + - @rocket.chat/models@1.0.1-rc.2 + - @rocket.chat/ui-theming@0.4.0 + - @rocket.chat/ui-avatar@9.0.0-rc.2 + - @rocket.chat/ui-client@13.0.0-rc.2 + - @rocket.chat/ui-video-conf@13.0.0-rc.2 + - @rocket.chat/ui-voip@3.0.0-rc.2 + - @rocket.chat/web-ui-registration@13.0.0-rc.2 + - @rocket.chat/instance-status@0.1.10-rc.2 +
+ ## 7.1.0-rc.1 ### Patch Changes diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.spec.ts b/apps/meteor/app/api/server/lib/getUploadFormData.spec.ts new file mode 100644 index 000000000000..dc7afb77bd19 --- /dev/null +++ b/apps/meteor/app/api/server/lib/getUploadFormData.spec.ts @@ -0,0 +1,178 @@ +import { Readable } from 'stream'; + +import { expect } from 'chai'; +import type { Request } from 'express'; + +import { getUploadFormData } from './getUploadFormData'; + +const createMockRequest = ( + fields: Record, + file?: { + fieldname: string; + filename: string; + content: string | Buffer; + mimetype?: string; + }, +): Readable & { headers: Record } => { + const boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW'; + const parts: string[] = []; + + if (file) { + parts.push( + `--${boundary}`, + `Content-Disposition: form-data; name="${file.fieldname}"; filename="${file.filename}"`, + `Content-Type: ${file.mimetype || 'application/octet-stream'}`, + '', + file.content.toString(), + ); + } + + for (const [name, value] of Object.entries(fields)) { + parts.push(`--${boundary}`, `Content-Disposition: form-data; name="${name}"`, '', value); + } + + parts.push(`--${boundary}--`); + + const mockRequest: any = new Readable({ + read() { + this.push(Buffer.from(parts.join('\r\n'))); + this.push(null); + }, + }); + + mockRequest.headers = { + 'content-type': `multipart/form-data; boundary=${boundary}`, + }; + + return mockRequest as Readable & { headers: Record }; +}; + +describe('getUploadFormData', () => { + it('should successfully parse a single file upload and fields', async () => { + const mockRequest = createMockRequest( + { fieldName: 'fieldValue' }, + { + fieldname: 'fileField', + filename: 'test.txt', + content: 'Hello, this is a test file!', + mimetype: 'text/plain', + }, + ); + + const result = await getUploadFormData({ request: mockRequest as Request }, { field: 'fileField' }); + + expect(result).to.deep.include({ + fieldname: 'fileField', + filename: 'test.txt', + mimetype: 'text/plain', + fields: { fieldName: 'fieldValue' }, + }); + + expect(result.fileBuffer).to.not.be.undefined; + expect(result.fileBuffer.toString()).to.equal('Hello, this is a test file!'); + }); + it('should parse a file upload with multiple additional fields', async () => { + const mockRequest = createMockRequest( + { + fieldName: 'fieldValue', + extraField1: 'extraValue1', + extraField2: 'extraValue2', + }, + { + fieldname: 'fileField', + filename: 'test_with_fields.txt', + content: 'This file has additional fields!', + mimetype: 'text/plain', + }, + ); + + const result = await getUploadFormData({ request: mockRequest as Request }, { field: 'fileField' }); + + expect(result).to.deep.include({ + fieldname: 'fileField', + filename: 'test_with_fields.txt', + mimetype: 'text/plain', + fields: { + fieldName: 'fieldValue', + extraField1: 'extraValue1', + extraField2: 'extraValue2', + }, + }); + + expect(result.fileBuffer).to.not.be.undefined; + expect(result.fileBuffer.toString()).to.equal('This file has additional fields!'); + }); + + it('should handle a file upload when fileOptional is true', async () => { + const mockRequest = createMockRequest( + { fieldName: 'fieldValue' }, + { + fieldname: 'fileField', + filename: 'optional.txt', + content: 'This file is optional!', + mimetype: 'text/plain', + }, + ); + + const result = await getUploadFormData({ request: mockRequest as Request }, { fileOptional: true }); + + expect(result).to.deep.include({ + fieldname: 'fileField', + filename: 'optional.txt', + mimetype: 'text/plain', + fields: { fieldName: 'fieldValue' }, + }); + + expect(result.fileBuffer).to.not.be.undefined; + expect(result.fileBuffer?.toString()).to.equal('This file is optional!'); + }); + + it('should throw an error when no file is uploaded and fileOptional is false', async () => { + const mockRequest = createMockRequest({ fieldName: 'fieldValue' }); + + try { + await getUploadFormData({ request: mockRequest as Request }, { fileOptional: false }); + throw new Error('Expected function to throw'); + } catch (error) { + expect((error as Error).message).to.equal('[No file uploaded]'); + } + }); + + it('should return fields without errors when no file is uploaded but fileOptional is true', async () => { + const mockRequest = createMockRequest({ fieldName: 'fieldValue' }); // No file + + const result = await getUploadFormData({ request: mockRequest as Request }, { fileOptional: true }); + + expect(result).to.deep.equal({ + fields: { fieldName: 'fieldValue' }, + file: undefined, + fileBuffer: undefined, + fieldname: undefined, + filename: undefined, + encoding: undefined, + mimetype: undefined, + }); + }); + + it('should reject an oversized file', async () => { + const mockRequest = createMockRequest( + {}, + { + fieldname: 'fileField', + filename: 'large.txt', + content: 'x'.repeat(1024 * 1024 * 2), // 2 MB file + mimetype: 'text/plain', + }, + ); + + try { + await getUploadFormData( + { request: mockRequest as Request }, + { sizeLimit: 1024 * 1024 }, // 1 MB limit + ); + throw new Error('Expected function to throw'); + } catch (error) { + expect((error as Error).message).to.equal('[error-file-too-large]'); + } + }); +}); diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.ts b/apps/meteor/app/api/server/lib/getUploadFormData.ts index 1630afe8cae2..93ceafdde92f 100644 --- a/apps/meteor/app/api/server/lib/getUploadFormData.ts +++ b/apps/meteor/app/api/server/lib/getUploadFormData.ts @@ -74,7 +74,15 @@ export async function getUploadFormData< const bb = busboy({ headers: request.headers, defParamCharset: 'utf8', limits }); const fields = Object.create(null) as K; - let uploadedFile: UploadResultWithOptionalFile | undefined; + let uploadedFile: UploadResultWithOptionalFile | undefined = { + fields, + encoding: undefined, + filename: undefined, + fieldname: undefined, + mimetype: undefined, + fileBuffer: undefined, + file: undefined, + }; let returnResult = (_value: UploadResultWithOptionalFile) => { // noop @@ -85,22 +93,13 @@ export async function getUploadFormData< function onField(fieldname: keyof K, value: K[keyof K]) { fields[fieldname] = value; - uploadedFile = { - fields, - encoding: undefined, - filename: undefined, - fieldname: undefined, - mimetype: undefined, - fileBuffer: undefined, - file: undefined, - }; } function onEnd() { if (!uploadedFile) { return returnError(new MeteorError('No file or fields were uploaded')); } - if (!('file' in uploadedFile) && !options.fileOptional) { + if (!options.fileOptional && !uploadedFile?.file) { return returnError(new MeteorError('No file uploaded')); } if (options.validate !== undefined && !options.validate(fields)) { diff --git a/apps/meteor/app/api/server/lib/maybeMigrateLivechatRoom.ts b/apps/meteor/app/api/server/lib/maybeMigrateLivechatRoom.ts index ac5b2cd866ca..3a66cef2e416 100644 --- a/apps/meteor/app/api/server/lib/maybeMigrateLivechatRoom.ts +++ b/apps/meteor/app/api/server/lib/maybeMigrateLivechatRoom.ts @@ -1,5 +1,6 @@ -import { isOmnichannelRoom, type IRoom } from '@rocket.chat/core-typings'; -import { Rooms } from '@rocket.chat/models'; +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import type { IOmnichannelRoom, IRoom } from '@rocket.chat/core-typings'; +import { LivechatRooms } from '@rocket.chat/models'; import type { FindOptions } from 'mongodb'; import { projectionAllowsAttribute } from './projectionAllowsAttribute'; @@ -9,7 +10,10 @@ import { migrateVisitorIfMissingContact } from '../../../livechat/server/lib/con * If the room is a livechat room and it doesn't yet have a contact, trigger the migration for its visitor and source * The migration will create/use a contact and assign it to every room that matches this visitorId and source. **/ -export async function maybeMigrateLivechatRoom(room: IRoom | null, options: FindOptions = {}): Promise { +export async function maybeMigrateLivechatRoom( + room: IOmnichannelRoom | null, + options: FindOptions = {}, +): Promise { if (!room || !isOmnichannelRoom(room)) { return room; } @@ -32,5 +36,5 @@ export async function maybeMigrateLivechatRoom(room: IRoom | null, options: Find } // Load the room again with the same options so it can be reloaded with the contactId in place - return Rooms.findOneById(room._id, options); + return LivechatRooms.findOneById(room._id, options); } diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 26a1ad022941..2944a82f3e63 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -34,7 +34,6 @@ import { composeRoomWithLastMessage } from '../helpers/composeRoomWithLastMessag import { getPaginationItems } from '../helpers/getPaginationItems'; import { getUserFromParams } from '../helpers/getUserFromParams'; import { getUploadFormData } from '../lib/getUploadFormData'; -import { maybeMigrateLivechatRoom } from '../lib/maybeMigrateLivechatRoom'; import { findAdminRoom, findAdminRooms, @@ -450,10 +449,8 @@ API.v1.addRoute( const { team, parentRoom } = await Team.getRoomInfo(room); const parent = discussionParent || parentRoom; - const options = { projection: fields }; - return API.v1.success({ - room: (await maybeMigrateLivechatRoom(await Rooms.findOneByIdOrName(room._id, options), options)) ?? undefined, + room: await Rooms.findOneByIdOrName(room._id, { projection: fields }), ...(team && { team }), ...(parent && { parent }), }); diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/fetchWorkspaceSyncPayload.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/fetchWorkspaceSyncPayload.ts new file mode 100644 index 000000000000..7d01f6305c83 --- /dev/null +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/fetchWorkspaceSyncPayload.ts @@ -0,0 +1,42 @@ +import type { Cloud, Serialized } from '@rocket.chat/core-typings'; +import { serverFetch as fetch } from '@rocket.chat/server-fetch'; +import { v, compile } from 'suretype'; + +import { CloudWorkspaceConnectionError } from '../../../../../lib/errors/CloudWorkspaceConnectionError'; +import { settings } from '../../../../settings/server'; + +const workspaceSyncPayloadSchema = v.object({ + workspaceId: v.string().required(), + publicKey: v.string(), + license: v.string().required(), +}); + +const assertWorkspaceSyncPayload = compile(workspaceSyncPayloadSchema); + +export async function fetchWorkspaceSyncPayload({ + token, + data, +}: { + token: string; + data: Cloud.WorkspaceSyncRequestPayload; +}): Promise> { + const workspaceRegistrationClientUri = settings.get('Cloud_Workspace_Registration_Client_Uri'); + const response = await fetch(`${workspaceRegistrationClientUri}/sync`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + }, + body: data, + }); + + if (!response.ok) { + const { error } = await response.json(); + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${error}`); + } + + const payload = await response.json(); + + assertWorkspaceSyncPayload(payload); + + return payload; +} diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/syncCloudData.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/syncCloudData.ts index fc55fc9e34fd..f28821b08387 100644 --- a/apps/meteor/app/cloud/server/functions/syncWorkspace/syncCloudData.ts +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/syncCloudData.ts @@ -1,57 +1,14 @@ -import type { Cloud, Serialized } from '@rocket.chat/core-typings'; import { DuplicatedLicenseError } from '@rocket.chat/license'; -import { serverFetch as fetch } from '@rocket.chat/server-fetch'; -import { v, compile } from 'suretype'; +import { Settings } from '@rocket.chat/models'; import { callbacks } from '../../../../../lib/callbacks'; import { CloudWorkspaceAccessError } from '../../../../../lib/errors/CloudWorkspaceAccessError'; -import { CloudWorkspaceConnectionError } from '../../../../../lib/errors/CloudWorkspaceConnectionError'; import { CloudWorkspaceRegistrationError } from '../../../../../lib/errors/CloudWorkspaceRegistrationError'; import { SystemLogger } from '../../../../../server/lib/logger/system'; -import { settings } from '../../../../settings/server'; import { buildWorkspaceRegistrationData } from '../buildRegistrationData'; import { CloudWorkspaceAccessTokenEmptyError, getWorkspaceAccessToken } from '../getWorkspaceAccessToken'; import { retrieveRegistrationStatus } from '../retrieveRegistrationStatus'; - -const workspaceSyncPayloadSchema = v.object({ - workspaceId: v.string().required(), - publicKey: v.string(), - license: v.string().required(), -}); - -const assertWorkspaceSyncPayload = compile(workspaceSyncPayloadSchema); - -const fetchWorkspaceSyncPayload = async ({ - token, - data, -}: { - token: string; - data: Cloud.WorkspaceSyncRequestPayload; -}): Promise> => { - const workspaceRegistrationClientUri = settings.get('Cloud_Workspace_Registration_Client_Uri'); - const response = await fetch(`${workspaceRegistrationClientUri}/sync`, { - method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - }, - body: data, - }); - - if (!response.ok) { - try { - const { error } = await response.json(); - throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${error}`); - } catch (error) { - throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${response.statusText}`); - } - } - - const payload = await response.json(); - - assertWorkspaceSyncPayload(payload); - - return payload; -}; +import { fetchWorkspaceSyncPayload } from './fetchWorkspaceSyncPayload'; export async function syncCloudData() { try { @@ -67,11 +24,17 @@ export async function syncCloudData() { const workspaceRegistrationData = await buildWorkspaceRegistrationData(undefined); - const { license, removeLicense = false } = await fetchWorkspaceSyncPayload({ + const { + license, + removeLicense = false, + cloudSyncAnnouncement, + } = await fetchWorkspaceSyncPayload({ token, data: workspaceRegistrationData, }); + await Settings.updateValueById('Cloud_Sync_Announcement_Payload', JSON.stringify(cloudSyncAnnouncement ?? null)); + if (removeLicense) { await callbacks.run('workspaceLicenseRemoved'); } else { diff --git a/apps/meteor/app/importer/server/classes/Importer.ts b/apps/meteor/app/importer/server/classes/Importer.ts index 49430c101d45..32834ed15c4a 100644 --- a/apps/meteor/app/importer/server/classes/Importer.ts +++ b/apps/meteor/app/importer/server/classes/Importer.ts @@ -18,7 +18,7 @@ import { ImportDataConverter } from './ImportDataConverter'; import type { ConverterOptions } from './ImportDataConverter'; import { ImporterProgress } from './ImporterProgress'; import { ImporterWebsocket } from './ImporterWebsocket'; -import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; +import { notifyOnSettingChanged, notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { t } from '../../../utils/lib/i18n'; import { ProgressStep, ImportPreparingStartedStates } from '../../lib/ImporterProgressStep'; import type { ImporterInfo } from '../definitions/ImporterInfo'; @@ -183,6 +183,13 @@ export class Importer { } }; + const afterContactsBatchFn = async (successCount: number) => { + const { value } = await Settings.incrementValueById('Contacts_Importer_Count', successCount, { returnDocument: 'after' }); + if (value) { + void notifyOnSettingChanged(value); + } + }; + const onErrorFn = async () => { await this.addCountCompleted(1); }; @@ -197,7 +204,7 @@ export class Importer { await this.converter.convertUsers({ beforeImportFn, afterImportFn, onErrorFn, afterBatchFn }); await this.updateProgress(ProgressStep.IMPORTING_CONTACTS); - await this.converter.convertContacts({ beforeImportFn, afterImportFn, onErrorFn }); + await this.converter.convertContacts({ beforeImportFn, afterImportFn, onErrorFn, afterBatchFn: afterContactsBatchFn }); await this.updateProgress(ProgressStep.IMPORTING_CHANNELS); await this.converter.convertChannels(startedByUserId, { beforeImportFn, afterImportFn, onErrorFn }); diff --git a/apps/meteor/app/importer/server/classes/converters/RecordConverter.ts b/apps/meteor/app/importer/server/classes/converters/RecordConverter.ts index 9003fe4bd416..d530b96212d7 100644 --- a/apps/meteor/app/importer/server/classes/converters/RecordConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/RecordConverter.ts @@ -27,6 +27,8 @@ export class RecordConverter Promise } = {}): Promise { const records = await this.getDataToImport(); this.skippedCount = 0; this.failedCount = 0; + this.newCount = 0; for await (const record of records) { const { _id } = record; @@ -214,8 +218,11 @@ export class RecordConverter { diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts index 2e74bf66cf4a..2997acaf5924 100644 --- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts +++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts @@ -20,9 +20,11 @@ export const addUserToRoom = async function ( { skipSystemMessage, skipAlertSound, + createAsHidden = false, }: { skipSystemMessage?: boolean; skipAlertSound?: boolean; + createAsHidden?: boolean; } = {}, ): Promise { const now = new Date(); @@ -84,8 +86,8 @@ export const addUserToRoom = async function ( const { insertedId } = await Subscriptions.createWithRoomAndUser(room, userToBeAdded as IUser, { ts: now, - open: true, - alert: !skipAlertSound, + open: !createAsHidden, + alert: createAsHidden ? false : !skipAlertSound, unread: 1, userMentions: 1, groupMentions: 0, diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index fda2314d3f2e..bb099af8cee7 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -74,9 +74,7 @@ async function createUsersSubscriptions({ memberIds.push(member._id); - const extra: Partial = options?.subscriptionExtra || {}; - - extra.open = true; + const extra: Partial = { open: true, ...options?.subscriptionExtra }; if (room.prid) { extra.prid = room.prid; diff --git a/apps/meteor/app/lib/server/functions/deleteMessage.ts b/apps/meteor/app/lib/server/functions/deleteMessage.ts index a91e77858043..30044dadb81c 100644 --- a/apps/meteor/app/lib/server/functions/deleteMessage.ts +++ b/apps/meteor/app/lib/server/functions/deleteMessage.ts @@ -1,14 +1,14 @@ import { AppEvents, Apps } from '@rocket.chat/apps'; import { api, Message } from '@rocket.chat/core-services'; -import type { AtLeast, IMessage, IUser } from '@rocket.chat/core-typings'; -import { Messages, Rooms, Uploads, Users, ReadReceipts } from '@rocket.chat/models'; +import { isThreadMessage, type AtLeast, type IMessage, type IRoom, type IThreadMessage, type IUser } from '@rocket.chat/core-typings'; +import { Messages, Rooms, Uploads, Users, ReadReceipts, Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../lib/callbacks'; import { canDeleteMessageAsync } from '../../../authorization/server/functions/canDeleteMessage'; import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; -import { notifyOnRoomChangedById, notifyOnMessageChange } from '../lib/notifyListener'; +import { notifyOnRoomChangedById, notifyOnMessageChange, notifyOnSubscriptionChangedByRoomIdAndUserIds } from '../lib/notifyListener'; export const deleteMessageValidatingPermission = async (message: AtLeast, userId: IUser['_id']): Promise => { if (!message?._id) { @@ -50,8 +50,8 @@ export async function deleteMessage(message: IMessage, user: IUser): Promise { + const { value: updatedParentMessage } = await Messages.decreaseReplyCountById(message.tmid, -1); + + if (room) { + const { modifiedCount } = await Subscriptions.removeUnreadThreadsByRoomId(room._id, [message.tmid]); + if (modifiedCount > 0) { + // The replies array contains the ids of all the users that are following the thread (everyone that is involved + the ones who are following) + // Technically, user._id is already in the message.replies array, but since we don't have any strong + // guarantees of it, we are adding again to make sure it is there. + const userIdsThatAreWatchingTheThread = [...new Set([user._id, ...(message.replies || [])])]; + // So they can decrement the unread threads count + void notifyOnSubscriptionChangedByRoomIdAndUserIds(room._id, userIdsThatAreWatchingTheThread); + } + } + + if (updatedParentMessage && updatedParentMessage.tcount === 0) { + void notifyOnMessageChange({ + id: message.tmid, + }); + } +} diff --git a/apps/meteor/app/lib/server/methods/createChannel.ts b/apps/meteor/app/lib/server/methods/createChannel.ts index 3b106ad5acff..210dada14971 100644 --- a/apps/meteor/app/lib/server/methods/createChannel.ts +++ b/apps/meteor/app/lib/server/methods/createChannel.ts @@ -25,7 +25,7 @@ export const createChannelMethod = async ( name: string, members: string[], readOnly = false, - customFields: Record = {}, + customFields?: Record, extraData: Record = {}, excludeSelf = false, ) => { @@ -53,7 +53,7 @@ export const createChannelMethod = async ( } return createRoom('c', name, user, members, excludeSelf, readOnly, { - customFields, + ...(customFields && Object.keys(customFields).length && { customFields }), ...extraData, }); }; diff --git a/apps/meteor/app/lib/server/methods/createPrivateGroup.ts b/apps/meteor/app/lib/server/methods/createPrivateGroup.ts index f07e37109901..413b1066a0cd 100644 --- a/apps/meteor/app/lib/server/methods/createPrivateGroup.ts +++ b/apps/meteor/app/lib/server/methods/createPrivateGroup.ts @@ -25,7 +25,7 @@ export const createPrivateGroupMethod = async ( name: string, members: string[], readOnly = false, - customFields: Record = {}, + customFields?: Record, extraData: Record = {}, excludeSelf = false, ): Promise< @@ -51,7 +51,7 @@ export const createPrivateGroupMethod = async ( } return createRoom('p', name, user, members, excludeSelf, readOnly, { - customFields, + ...(customFields && Object.keys(customFields).length && { customFields }), ...extraData, }); }; diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 6ab03df337ef..6c4ddda1b125 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -132,7 +132,7 @@ Meteor.methods({ ts: Match.Maybe(Date), t: Match.Maybe(String), otrAck: Match.Maybe(String), - bot: Match.Maybe(Boolean), + bot: Match.Maybe(Object), content: Match.Maybe(Object), e2e: Match.Maybe(String), e2eMentions: Match.Maybe(Object), diff --git a/apps/meteor/app/livechat/server/api/v1/contact.ts b/apps/meteor/app/livechat/server/api/v1/contact.ts index 0baa5584a243..03cc5ddeaabd 100644 --- a/apps/meteor/app/livechat/server/api/v1/contact.ts +++ b/apps/meteor/app/livechat/server/api/v1/contact.ts @@ -6,6 +6,7 @@ import { isGETOmnichannelContactHistoryProps, isGETOmnichannelContactsChannelsProps, isGETOmnichannelContactsSearchProps, + isGETOmnichannelContactsCheckExistenceProps, } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Match, check } from 'meteor/check'; @@ -14,7 +15,6 @@ import { Meteor } from 'meteor/meteor'; import { API } from '../../../../api/server'; import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems'; import { createContact } from '../../lib/contacts/createContact'; -import { getContactByChannel } from '../../lib/contacts/getContactByChannel'; import { getContactChannelsGrouped } from '../../lib/contacts/getContactChannelsGrouped'; import { getContactHistory } from '../../lib/contacts/getContactHistory'; import { getContacts } from '../../lib/contacts/getContacts'; @@ -133,13 +133,13 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-livechat-contact'], validateParams: isGETOmnichannelContactsProps }, { async get() { - const { contactId, visitor } = this.queryParams; + const { contactId } = this.queryParams; - if (!contactId && !visitor) { + if (!contactId) { return API.v1.notFound(); } - const contact = await (contactId ? LivechatContacts.findOneById(contactId) : getContactByChannel(visitor)); + const contact = await LivechatContacts.findOneById(contactId); if (!contact) { return API.v1.notFound(); @@ -166,6 +166,20 @@ API.v1.addRoute( }, ); +API.v1.addRoute( + 'omnichannel/contacts.checkExistence', + { authRequired: true, permissionsRequired: ['view-livechat-contact'], validateParams: isGETOmnichannelContactsCheckExistenceProps }, + { + async get() { + const { contactId, email, phone } = this.queryParams; + + const contact = await LivechatContacts.countByContactInfo({ contactId, email, phone }); + + return API.v1.success({ exists: contact > 0 }); + }, + }, +); + API.v1.addRoute( 'omnichannel/contacts.history', { authRequired: true, permissionsRequired: ['view-livechat-contact-history'], validateParams: isGETOmnichannelContactHistoryProps }, diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index 4f2887ab369c..7eaf800a05df 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -97,7 +97,7 @@ export const createLivechatRoom = async ( const source = extraRoomInfo.source || roomInfo.source; if (settings.get('Livechat_Require_Contact_Verification') === 'always') { - await LivechatContacts.updateContactChannel({ visitorId: _id, source }, { verified: false }); + await LivechatContacts.setChannelVerifiedStatus({ visitorId: _id, source }, false); } const contactId = await migrateVisitorIfMissingContact(_id, source); diff --git a/apps/meteor/app/livechat/server/lib/contacts/getContactByChannel.ts b/apps/meteor/app/livechat/server/lib/contacts/getContactByChannel.ts deleted file mode 100644 index 052c28206047..000000000000 --- a/apps/meteor/app/livechat/server/lib/contacts/getContactByChannel.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { ILivechatContact, ILivechatContactVisitorAssociation } from '@rocket.chat/core-typings'; -import { LivechatContacts, LivechatVisitors } from '@rocket.chat/models'; - -import { migrateVisitorToContactId } from './migrateVisitorToContactId'; - -export async function getContactByChannel(association: ILivechatContactVisitorAssociation): Promise { - // If a contact already exists for that visitor, return it - const linkedContact = await LivechatContacts.findOneByVisitor(association); - if (linkedContact) { - return linkedContact; - } - - // If the contact was not found, Load the visitor data so we can migrate it - const visitor = await LivechatVisitors.findOneById(association.visitorId); - - // If there is no visitor data, there's nothing we can do - if (!visitor) { - return null; - } - - const newContactId = await migrateVisitorToContactId({ visitor, source: association.source }); - - // If no contact was created by the migration, this visitor doesn't need a contact yet, so let's return null - if (!newContactId) { - return null; - } - - // Finally, let's return the data of the migrated contact - return LivechatContacts.findOneById(newContactId); -} diff --git a/apps/meteor/app/livechat/server/lib/contacts/updateContact.ts b/apps/meteor/app/livechat/server/lib/contacts/updateContact.ts index 8ec929ae4b34..023568bd11de 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/updateContact.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/updateContact.ts @@ -1,5 +1,5 @@ import type { ILivechatContact, ILivechatContactChannel } from '@rocket.chat/core-typings'; -import { LivechatContacts, LivechatInquiry, LivechatRooms, Subscriptions } from '@rocket.chat/models'; +import { LivechatContacts, LivechatInquiry, LivechatRooms, Settings, Subscriptions } from '@rocket.chat/models'; import { getAllowedCustomFields } from './getAllowedCustomFields'; import { validateContactManager } from './validateContactManager'; @@ -8,6 +8,7 @@ import { notifyOnSubscriptionChangedByVisitorIds, notifyOnRoomChangedByContactId, notifyOnLivechatInquiryChangedByVisitorIds, + notifyOnSettingChanged, } from '../../../../lib/server/lib/notifyListener'; export type UpdateContactParams = { @@ -24,9 +25,12 @@ export type UpdateContactParams = { export async function updateContact(params: UpdateContactParams): Promise { const { contactId, name, emails, phones, customFields: receivedCustomFields, contactManager, channels, wipeConflicts } = params; - const contact = await LivechatContacts.findOneById>(contactId, { - projection: { _id: 1, name: 1 }, - }); + const contact = await LivechatContacts.findOneById>( + contactId, + { + projection: { _id: 1, name: 1, customFields: 1, conflictingFields: 1 }, + }, + ); if (!contact) { throw new Error('error-contact-not-found'); @@ -36,7 +40,36 @@ export async function updateContact(params: UpdateContactParams): Promise customField._id); + const currentCustomFieldsIds = Object.keys(contact.customFields || {}); + const notRegisteredCustomFields = currentCustomFieldsIds + .filter((customFieldId) => !workspaceAllowedCustomFieldsIds.includes(customFieldId)) + .map((customFieldId) => ({ _id: customFieldId })); + + const customFieldsToUpdate = + receivedCustomFields && + validateCustomFields(workspaceAllowedCustomFields, receivedCustomFields, { + ignoreAdditionalFields: !!notRegisteredCustomFields.length, + }); + + if (receivedCustomFields && customFieldsToUpdate && notRegisteredCustomFields.length) { + const allowedCustomFields = [...workspaceAllowedCustomFields, ...notRegisteredCustomFields]; + validateCustomFields(allowedCustomFields, receivedCustomFields); + + notRegisteredCustomFields.forEach((notRegisteredCustomField) => { + customFieldsToUpdate[notRegisteredCustomField._id] = contact.customFields?.[notRegisteredCustomField._id] as string; + }); + } const updatedContact = await LivechatContacts.updateContact(contactId, { name, @@ -44,7 +77,7 @@ export async function updateContact(params: UpdateContactParams): Promise ({ phoneNumber })), contactManager, channels, - customFields, + customFields: customFieldsToUpdate, ...(wipeConflicts && { conflictingFields: [] }), }); diff --git a/apps/meteor/app/livechat/server/lib/contacts/validateCustomFields.ts b/apps/meteor/app/livechat/server/lib/contacts/validateCustomFields.ts index 4efede65b266..3ab981cf2513 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/validateCustomFields.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/validateCustomFields.ts @@ -4,7 +4,7 @@ import { trim } from '../../../../../lib/utils/stringUtils'; import { i18n } from '../../../../utils/lib/i18n'; export function validateCustomFields( - allowedCustomFields: AtLeast[], + allowedCustomFields: AtLeast[], customFields: Record, { ignoreAdditionalFields = false, @@ -16,7 +16,7 @@ export function validateCustomFields( for (const cf of allowedCustomFields) { if (!customFields.hasOwnProperty(cf._id)) { if (cf.required && !ignoreValidationErrors) { - throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label })); + throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label || cf._id })); } continue; } @@ -24,7 +24,7 @@ export function validateCustomFields( if (!cfValue || typeof cfValue !== 'string') { if (cf.required && !ignoreValidationErrors) { - throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label })); + throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label || cf._id })); } continue; } @@ -36,7 +36,7 @@ export function validateCustomFields( continue; } - throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label })); + throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label || cf._id })); } } diff --git a/apps/meteor/app/livechat/server/startup.ts b/apps/meteor/app/livechat/server/startup.ts index 5c02587f17e6..b41fc425bf06 100644 --- a/apps/meteor/app/livechat/server/startup.ts +++ b/apps/meteor/app/livechat/server/startup.ts @@ -13,6 +13,7 @@ import { callbacks } from '../../../lib/callbacks'; import { beforeLeaveRoomCallback } from '../../../lib/callbacks/beforeLeaveRoomCallback'; import { i18n } from '../../../server/lib/i18n'; import { roomCoordinator } from '../../../server/lib/rooms/roomCoordinator'; +import { maybeMigrateLivechatRoom } from '../../api/server/lib/maybeMigrateLivechatRoom'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; import { notifyOnUserChange } from '../../lib/server/lib/notifyListener'; import { settings } from '../../settings/server'; @@ -21,7 +22,7 @@ import './roomAccessValidator.internalService'; const logger = new Logger('LivechatStartup'); Meteor.startup(async () => { - roomCoordinator.setRoomFind('l', (_id) => LivechatRooms.findOneById(_id)); + roomCoordinator.setRoomFind('l', async (id) => maybeMigrateLivechatRoom(await LivechatRooms.findOneById(id))); beforeLeaveRoomCallback.add( (user, room) => { diff --git a/apps/meteor/app/models/client/models/CachedChatSubscription.ts b/apps/meteor/app/models/client/models/CachedChatSubscription.ts index 6ea6ea6917e9..28b55a70197d 100644 --- a/apps/meteor/app/models/client/models/CachedChatSubscription.ts +++ b/apps/meteor/app/models/client/models/CachedChatSubscription.ts @@ -125,6 +125,10 @@ class CachedChatSubscription extends CachedCollection { + return this.handleRecordEvent('changed', record); + } + protected deserializeFromCache(record: unknown) { const deserialized = super.deserializeFromCache(record); diff --git a/apps/meteor/app/oembed/server/providers.ts b/apps/meteor/app/oembed/server/providers.ts index 9760d521b15a..1d2bf89bbae8 100644 --- a/apps/meteor/app/oembed/server/providers.ts +++ b/apps/meteor/app/oembed/server/providers.ts @@ -3,6 +3,8 @@ import { camelCase } from 'change-case'; import { callbacks } from '../../../lib/callbacks'; import { SystemLogger } from '../../../server/lib/logger/system'; +import { settings } from '../../settings/server'; +import { Info } from '../../utils/rocketchat.info'; class Providers { private providers: OEmbedProvider[]; @@ -11,13 +13,20 @@ class Providers { this.providers = []; } - static getConsumerUrl(provider: OEmbedProvider, url: string): string { + static getConsumerUrl(provider: OEmbedProvider, url: string): string | undefined { + if (!provider.endPoint) { + return; + } const urlObj = new URL(provider.endPoint); urlObj.searchParams.set('url', url); return urlObj.toString(); } + static getCustomHeaders(provider: OEmbedProvider): { [k: string]: string } { + return provider.getHeaderOverrides?.() || {}; + } + registerProvider(provider: OEmbedProvider): number { return this.providers.push(provider); } @@ -75,7 +84,11 @@ providers.registerProvider({ providers.registerProvider({ urls: [new RegExp('https?://(twitter|x)\\.com/[^/]+/status/\\S+')], - endPoint: 'https://publish.twitter.com/oembed', + getHeaderOverrides: () => { + return { + 'User-Agent': `${settings.get('API_Embed_UserAgent')} Rocket.Chat/${Info.version} Googlebot/2.1`, + }; + }, }); providers.registerProvider({ @@ -104,7 +117,12 @@ callbacks.add( const consumerUrl = Providers.getConsumerUrl(provider, url); - return { ...data, urlObj: new URL(consumerUrl) }; + const headerOverrides = Providers.getCustomHeaders(provider); + if (!consumerUrl) { + return { ...data, headerOverrides }; + } + + return { ...data, headerOverrides, urlObj: new URL(consumerUrl) }; }, callbacks.priority.MEDIUM, 'oembed-providers-before', diff --git a/apps/meteor/app/oembed/server/server.ts b/apps/meteor/app/oembed/server/server.ts index 1e758b1371e8..d3cdb52c0a86 100644 --- a/apps/meteor/app/oembed/server/server.ts +++ b/apps/meteor/app/oembed/server/server.ts @@ -107,6 +107,7 @@ const getUrlContent = async (urlObj: URL, redirectCount = 5): Promise { totalInstalled++; const status = await app.getStatus(); - const storageItem = await app.getStorageItem(); + const storageItem = app.getStorageItem(); if (storageItem.installationSource === AppInstallationSource.PRIVATE) { totalPrivateApps++; @@ -51,12 +51,10 @@ async function _getAppsStatistics(): Promise { } } - if (status === AppStatus.MANUALLY_DISABLED) { - totalFailed++; - } - if (AppStatusUtils.isEnabled(status)) { totalActive++; + } else if (status !== AppStatus.MANUALLY_DISABLED) { + totalFailed++; } }), ); diff --git a/apps/meteor/app/statistics/server/lib/getContactVerificationStatistics.ts b/apps/meteor/app/statistics/server/lib/getContactVerificationStatistics.ts new file mode 100644 index 000000000000..f1072451c972 --- /dev/null +++ b/apps/meteor/app/statistics/server/lib/getContactVerificationStatistics.ts @@ -0,0 +1,41 @@ +import type { IStats } from '@rocket.chat/core-typings'; +import { LivechatContacts } from '@rocket.chat/models'; + +import { settings } from '../../../settings/server'; + +export async function getContactVerificationStatistics(): Promise { + const [ + totalContacts, + totalUnknownContacts, + [{ totalConflicts, avgChannelsPerContact } = { totalConflicts: 0, avgChannelsPerContact: 0 }], + totalBlockedContacts, + totalFullyBlockedContacts, + totalVerifiedContacts, + totalContactsWithoutChannels, + ] = await Promise.all([ + LivechatContacts.estimatedDocumentCount(), + LivechatContacts.countUnknown(), + LivechatContacts.getStatistics().toArray(), + LivechatContacts.countBlocked(), + LivechatContacts.countFullyBlocked(), + LivechatContacts.countVerified(), + LivechatContacts.countContactsWithoutChannels(), + ]); + + return { + totalContacts, + totalUnknownContacts, + totalMergedContacts: settings.get('Merged_Contacts_Count'), + totalConflicts, + totalResolvedConflicts: settings.get('Resolved_Conflicts_Count'), + totalBlockedContacts, + totalPartiallyBlockedContacts: totalBlockedContacts - totalFullyBlockedContacts, + totalFullyBlockedContacts, + totalVerifiedContacts, + avgChannelsPerContact, + totalContactsWithoutChannels, + totalImportedContacts: settings.get('Contacts_Importer_Count'), + totalUpsellViews: settings.get('Advanced_Contact_Upsell_Views_Count'), + totalUpsellClicks: settings.get('Advanced_Contact_Upsell_Clicks_Count'), + }; +} diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 25d93a6985c3..12f24cd3bc10 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -30,6 +30,7 @@ import { MongoInternals } from 'meteor/mongo'; import moment from 'moment'; import { getAppsStatistics } from './getAppsStatistics'; +import { getContactVerificationStatistics } from './getContactVerificationStatistics'; import { getStatistics as getEnterpriseStatistics } from './getEEStatistics'; import { getImporterStatistics } from './getImporterStatistics'; import { getServicesStatistics } from './getServicesStatistics'; @@ -477,6 +478,7 @@ export const statistics = { statistics.services = await getServicesStatistics(); statistics.importer = getImporterStatistics(); statistics.videoConf = await VideoConf.getStatistics(); + statistics.contactVerification = await getContactVerificationStatistics(); // If getSettingsStatistics() returns an error, save as empty object. statsPms.push( diff --git a/apps/meteor/app/theme/client/imports/general/base_old.css b/apps/meteor/app/theme/client/imports/general/base_old.css index 5b924491b54e..921bf3051d33 100644 --- a/apps/meteor/app/theme/client/imports/general/base_old.css +++ b/apps/meteor/app/theme/client/imports/general/base_old.css @@ -181,47 +181,6 @@ blockquote { animation: highlight 6s infinite; } -.page-settings { - & .settings-file-preview { - display: flex; - align-items: center; - - & input[type='file'] { - position: absolute !important; - z-index: 10000; - top: 0; - left: 0; - width: 100%; - height: 100%; - cursor: pointer; - opacity: 0; - - & * { - cursor: pointer; - } - } - - & .preview { - overflow: hidden; - width: 100px; - height: 40px; - margin-right: 0.75rem; - border-width: var(--input-border-width); - border-color: var(--input-border-color); - border-radius: var(--input-border-radius); - background-repeat: no-repeat; - background-position: center center; - background-size: contain; - - &.no-file { - display: flex; - align-items: center; - justify-content: center; - } - } - } -} - .room-not-found { display: flex; flex-direction: column; diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts b/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts index 3c35d0fee301..49391d94c4b6 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts +++ b/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts @@ -44,7 +44,7 @@ export const formattingButtons: ReadonlyArray = [ command: 'i', }, { - label: 'Strike', + label: 'Strikethrough', icon: 'strike', pattern: '~{{text}}~', }, @@ -54,7 +54,7 @@ export const formattingButtons: ReadonlyArray = [ pattern: '`{{text}}`', }, { - label: 'Multi_line', + label: 'Multi_line_code', icon: 'multiline', pattern: '```\n{{text}}\n``` ', }, diff --git a/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts b/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts index 5f82e47921f8..bf33821de4a4 100644 --- a/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts @@ -10,7 +10,7 @@ import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; import { fireGlobalEvent } from '../../../../client/lib/utils/fireGlobalEvent'; import { getConfig } from '../../../../client/lib/utils/getConfig'; import { callbacks } from '../../../../lib/callbacks'; -import { CachedChatRoom, Messages, Subscriptions, CachedChatSubscription } from '../../../models/client'; +import { Messages, Subscriptions, CachedChatSubscription } from '../../../models/client'; import { sdk } from '../../../utils/client/lib/SDKClient'; const maxRoomsOpen = parseInt(getConfig('maxRoomsOpen') ?? '5') || 5; @@ -79,8 +79,7 @@ function getOpenedRoomByRid(rid: IRoom['_id']) { } const computation = Tracker.autorun(() => { - const ready = CachedChatRoom.ready.get() && mainReady.get(); - if (ready !== true) { + if (!mainReady.get()) { return; } Tracker.nonreactive(() => diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index f7fff0b2a2aa..3745864061f4 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -31,7 +31,7 @@ export class ChatMessages implements ChatAPI { public composer: ComposerAPI | undefined; - public setComposerAPI = (composer: ComposerAPI): void => { + public setComposerAPI = (composer?: ComposerAPI): void => { this.composer?.release(); this.composer = composer; }; diff --git a/apps/meteor/app/ui/client/lib/KonchatNotification.ts b/apps/meteor/app/ui/client/lib/KonchatNotification.ts index ff67250c000b..2b463886fdac 100644 --- a/apps/meteor/app/ui/client/lib/KonchatNotification.ts +++ b/apps/meteor/app/ui/client/lib/KonchatNotification.ts @@ -10,7 +10,7 @@ import { router } from '../../../../client/providers/RouterProvider'; import { stripTags } from '../../../../lib/utils/stringUtils'; import { CustomSounds } from '../../../custom-sounds/client/lib/CustomSounds'; import { e2e } from '../../../e2e/client'; -import { Subscriptions } from '../../../models/client'; +import { Subscriptions, Users } from '../../../models/client'; import { getUserPreference } from '../../../utils/client'; import { getUserAvatarURL } from '../../../utils/client/getUserAvatarURL'; import { getUserNotificationsSoundVolume } from '../../../utils/client/getUserNotificationsSoundVolume'; @@ -207,27 +207,29 @@ class KonchatNotification { } } - public newRoom(rid: IRoom['_id']) { + public newRoom() { Tracker.nonreactive(() => { - let newRoomSound = Session.get('newRoomSound') as IRoom['_id'][] | undefined; - if (newRoomSound) { - newRoomSound = [...newRoomSound, rid]; - } else { - newRoomSound = [rid]; + const uid = Meteor.userId(); + if (!uid) { + return; } + const user = Users.findOne(uid, { + fields: { + 'settings.preferences.newRoomNotification': 1, + 'settings.preferences.notificationsSoundVolume': 1, + }, + }); + const newRoomNotification = getUserPreference(user, 'newRoomNotification'); + const audioVolume = getUserNotificationsSoundVolume(user?._id); - return Session.set('newRoomSound', newRoomSound); - }); - } - - public removeRoomNotification(rid: IRoom['_id']) { - let newRoomSound = (Session.get('newRoomSound') as IRoom['_id'][] | undefined) ?? []; - newRoomSound = newRoomSound.filter((_rid) => _rid !== rid); - Tracker.nonreactive(() => Session.set('newRoomSound', newRoomSound)); - - const link = document.querySelector(`.link-room-${rid}`); + if (!newRoomNotification) { + return; + } - link?.classList.remove('new-room-highlight'); + void CustomSounds.play(newRoomNotification, { + volume: Number((audioVolume / 100).toPrecision(2)), + }); + }); } } diff --git a/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx b/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx index 51feca504834..afc3891a5714 100644 --- a/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx +++ b/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx @@ -22,7 +22,7 @@ const PageHeaderNoShadow = ({ children = undefined, title, onClickBack, ...props useDocumentTitle(typeof title === 'string' ? title : undefined); return ( - + void; disabled?: boolean; etag: IUser['avatarETag']; + name: IUser['name']; }; -function UserAvatarEditor({ currentUsername, username, setAvatarObj, disabled, etag }: UserAvatarEditorProps): ReactElement { +function UserAvatarEditor({ currentUsername, username, setAvatarObj, name, disabled, etag }: UserAvatarEditorProps): ReactElement { const { t } = useTranslation(); + const useFullNameForDefaultAvatar = useSetting('UI_Use_Name_Avatar'); const rotateImages = useSetting('FileUpload_RotateImages'); const [avatarFromUrl, setAvatarFromUrl] = useState(''); const [newAvatarSource, setNewAvatarSource] = useState(); @@ -53,7 +55,7 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, disabled, e }; const clickReset = (): void => { - setNewAvatarSource(`/avatar/%40${username}`); + setNewAvatarSource(`/avatar/%40${useFullNameForDefaultAvatar ? name : username}`); setAvatarObj('reset'); }; @@ -91,7 +93,7 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, disabled, e { )} - {shouldShowRolesList && } + {shouldShowRolesList && } {formatTime(message.ts)} diff --git a/apps/meteor/client/components/message/variants/RoomMessage.tsx b/apps/meteor/client/components/message/variants/RoomMessage.tsx index 8dec6c9abbaa..54d3dcdea539 100644 --- a/apps/meteor/client/components/message/variants/RoomMessage.tsx +++ b/apps/meteor/client/components/message/variants/RoomMessage.tsx @@ -55,8 +55,10 @@ const RoomMessage = ({ const { openUserCard, triggerProps } = useUserCard(); const selecting = useIsSelecting(); + const isOTRMessage = message.t === 'otr' || message.t === 'otr-ack'; + const toggleSelected = useToggleSelect(message._id); - const selected = useIsSelectedMessage(message._id); + const selected = useIsSelectedMessage(message._id, isOTRMessage); useCountSelected(); @@ -67,10 +69,10 @@ const RoomMessage = ({ ref={messageRef} id={message._id} role='listitem' - aria-roledescription={sequential ? t('sequential_message') : t('message')} + aria-roledescription={t('message')} tabIndex={0} aria-labelledby={`${message._id}-displayName ${message._id}-time ${message._id}-content ${message._id}-read-status`} - onClick={selecting ? toggleSelected : undefined} + onClick={selecting && !isOTRMessage ? toggleSelected : undefined} isSelected={selected} isEditing={editing} isPending={message.temp} @@ -99,7 +101,7 @@ const RoomMessage = ({ {...triggerProps} /> )} - {selecting && } + {selecting && } {sequential && } diff --git a/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx b/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx index f7fa072a7406..146bda990d52 100644 --- a/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx +++ b/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx @@ -45,8 +45,10 @@ const ThreadMessagePreview = ({ message, showUserAvatar, sequential, ...props }: const { t } = useTranslation(); const isSelecting = useIsSelecting(); + const isOTRMessage = message.t === 'otr' || message.t === 'otr-ack'; + const toggleSelected = useToggleSelect(message._id); - const isSelected = useIsSelectedMessage(message._id); + const isSelected = useIsSelectedMessage(message._id, isOTRMessage); useCountSelected(); const messageType = parentMessage.isSuccess ? MessageTypes.getType(parentMessage.data) : null; @@ -65,6 +67,10 @@ const ThreadMessagePreview = ({ message, showUserAvatar, sequential, ...props }: return goToThread({ rid: message.rid, tmid: message.tmid, msg: message._id }); } + if (isOTRMessage) { + return; + } + return toggleSelected(); }; @@ -117,7 +123,7 @@ const ThreadMessagePreview = ({ message, showUserAvatar, sequential, ...props }: size='x18' /> )} - {isSelecting && } + {isSelecting && } diff --git a/apps/meteor/client/hooks/useAppSlashCommands.ts b/apps/meteor/client/hooks/useAppSlashCommands.ts index 3a925cb24690..baa11ca00a69 100644 --- a/apps/meteor/client/hooks/useAppSlashCommands.ts +++ b/apps/meteor/client/hooks/useAppSlashCommands.ts @@ -1,6 +1,7 @@ +import type { SlashCommand } from '@rocket.chat/core-typings'; import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks'; import { useEndpoint, useStream, useUserId } from '@rocket.chat/ui-contexts'; -import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useQueryClient, useQuery } from '@tanstack/react-query'; import { useEffect } from 'react'; import { slashCommands } from '../../app/utils/client/slashCommand'; @@ -35,10 +36,29 @@ export const useAppSlashCommands = () => { const getSlashCommands = useEndpoint('GET', '/v1/commands.list'); - useQuery(['apps', 'slashCommands'], () => getSlashCommands(), { - enabled: !!uid, - onSuccess(data) { - data.commands.forEach((command) => slashCommands.add(command)); + useQuery( + ['apps', 'slashCommands'], + async () => { + let allCommands: Pick[] = []; + let hasMore = true; + let offset = 0; + const count = 50; + + while (hasMore) { + // eslint-disable-next-line no-await-in-loop + const { commands, total } = await getSlashCommands({ offset, count }); + allCommands = allCommands.concat(commands); + hasMore = allCommands.length < total; + offset += count; + } + + return allCommands; }, - }); + { + enabled: !!uid, + onSuccess(data) { + data.forEach((command) => slashCommands.add(command)); + }, + }, + ); }; diff --git a/apps/meteor/client/hooks/useLicense.ts b/apps/meteor/client/hooks/useLicense.ts index 4ec8d29ec49c..6c50ff55fe74 100644 --- a/apps/meteor/client/hooks/useLicense.ts +++ b/apps/meteor/client/hooks/useLicense.ts @@ -49,7 +49,20 @@ export const useLicenseBase = ({ }; export const useLicense = (params?: LicenseParams) => { - return useLicenseBase({ params, select: (data) => data.license }); + return useLicenseBase({ + params, + select: (data) => data.license, + }); +}; + +export const useLicenseWithCloudAnnouncement = (params?: LicenseParams) => { + return useLicenseBase({ + params, + select: ({ license, cloudSyncAnnouncement }) => ({ + ...license, + cloudSyncAnnouncement, + }), + }); }; export const useHasLicense = (): UseQueryResult => { diff --git a/apps/meteor/client/hooks/useNotifyUser.ts b/apps/meteor/client/hooks/useNotifyUser.ts index 818017d93e97..440c979fed11 100644 --- a/apps/meteor/client/hooks/useNotifyUser.ts +++ b/apps/meteor/client/hooks/useNotifyUser.ts @@ -1,6 +1,7 @@ -import type { AtLeast, ISubscription } from '@rocket.chat/core-typings'; +import type { AtLeast, INotificationDesktop, ISubscription } from '@rocket.chat/core-typings'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useRouter, useStream, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; -import { useCallback, useEffect } from 'react'; +import { useEffect } from 'react'; import { useEmbeddedLayout } from './useEmbeddedLayout'; import { CachedChatSubscription } from '../../app/models/client'; @@ -15,79 +16,68 @@ export const useNotifyUser = () => { const notifyUserStream = useStream('notify-user'); const muteFocusedConversations = useUserPreference('muteFocusedConversations'); - const notifyNewRoom = useCallback( - async (sub: AtLeast): Promise => { - if (!user || user.status === 'busy') { - return; - } + const notifyNewRoom = useEffectEvent(async (sub: AtLeast): Promise => { + if (!user || user.status === 'busy') { + return; + } - if ((!router.getRouteParameters().name || router.getRouteParameters().name !== sub.name) && !sub.ls && sub.alert === true) { - KonchatNotification.newRoom(sub.rid); - } - }, - [router, user], - ); + if ((!router.getRouteParameters().name || router.getRouteParameters().name !== sub.name) && !sub.ls && sub.alert === true) { + KonchatNotification.newRoom(); + } + }); + + const notifyNewMessageAudioAndDesktop = useEffectEvent((notification: INotificationDesktop) => { + const hasFocus = document.hasFocus(); + + const openedRoomId = ['channel', 'group', 'direct'].includes(router.getRouteName() || '') ? RoomManager.opened : undefined; + + const { rid } = notification.payload; + const messageIsInOpenedRoom = openedRoomId === rid; - const notifyNewMessageAudio = useCallback( - (rid?: string) => { - const hasFocus = document.hasFocus(); - const messageIsInOpenedRoom = RoomManager.opened === rid; + fireGlobalEvent('notification', { + notification, + fromOpenedRoom: messageIsInOpenedRoom, + hasFocus, + }); - if (isLayoutEmbedded) { - if (!hasFocus && messageIsInOpenedRoom) { - // Play a notification sound - void KonchatNotification.newMessage(rid); - } - } else if (!hasFocus || !messageIsInOpenedRoom || !muteFocusedConversations) { + if (isLayoutEmbedded) { + if (!hasFocus && messageIsInOpenedRoom) { // Play a notification sound void KonchatNotification.newMessage(rid); + void KonchatNotification.showDesktop(notification); } - }, - [isLayoutEmbedded, muteFocusedConversations], - ); + } else if (!hasFocus || !messageIsInOpenedRoom || !muteFocusedConversations) { + // Play a notification sound + void KonchatNotification.newMessage(rid); + void KonchatNotification.showDesktop(notification); + } + }); useEffect(() => { if (!user?._id) { return; } - notifyUserStream(`${user?._id}/notification`, (notification) => { - const openedRoomId = ['channel', 'group', 'direct'].includes(router.getRouteName() || '') ? RoomManager.opened : undefined; - - const hasFocus = document.hasFocus(); - const messageIsInOpenedRoom = openedRoomId === notification.payload.rid; + const unsubNotification = notifyUserStream(`${user._id}/notification`, notifyNewMessageAudioAndDesktop); - fireGlobalEvent('notification', { - notification, - fromOpenedRoom: messageIsInOpenedRoom, - hasFocus, - }); - - if (isLayoutEmbedded) { - if (!hasFocus && messageIsInOpenedRoom) { - // Show a notification. - KonchatNotification.showDesktop(notification); - } - } else if (!hasFocus || !messageIsInOpenedRoom) { - // Show a notification. - KonchatNotification.showDesktop(notification); - } - - notifyNewMessageAudio(notification.payload.rid); - }); - - notifyUserStream(`${user?._id}/subscriptions-changed`, (action, sub) => { - if (action === 'removed') { + const unsubSubs = notifyUserStream(`${user._id}/subscriptions-changed`, (action, sub) => { + if (action !== 'inserted') { return; } void notifyNewRoom(sub); }); - CachedChatSubscription.collection.find().observe({ - changed: (sub) => { + const handle = CachedChatSubscription.collection.find().observe({ + added: (sub) => { void notifyNewRoom(sub); }, }); - }, [isLayoutEmbedded, notifyNewMessageAudio, notifyNewRoom, notifyUserStream, router, user?._id]); + + return () => { + unsubNotification(); + unsubSubs(); + handle.stop(); + }; + }, [isLayoutEmbedded, notifyNewMessageAudioAndDesktop, notifyNewRoom, notifyUserStream, router, user?._id]); }; diff --git a/apps/meteor/client/hooks/useContinuousSoundNotification.ts b/apps/meteor/client/hooks/useOmnichannelContinuousSoundNotification.ts similarity index 87% rename from apps/meteor/client/hooks/useContinuousSoundNotification.ts rename to apps/meteor/client/hooks/useOmnichannelContinuousSoundNotification.ts index 80abb492363b..bcfdf40ae0b8 100644 --- a/apps/meteor/client/hooks/useContinuousSoundNotification.ts +++ b/apps/meteor/client/hooks/useOmnichannelContinuousSoundNotification.ts @@ -6,7 +6,7 @@ import { useUserSoundPreferences } from './useUserSoundPreferences'; import { CustomSounds } from '../../app/custom-sounds/client/lib/CustomSounds'; const query = { t: 'l', ls: { $exists: false }, open: true }; -export const useContinuousSoundNotification = () => { +export const useOmnichannelContinuousSoundNotification = (queue: T[]) => { const userSubscriptions = useUserSubscriptions(query); const playNewRoomSoundContinuously = useSetting('Livechat_continuous_sound_notification_new_livechat_room'); @@ -16,6 +16,8 @@ export const useContinuousSoundNotification = () => { const continuousCustomSoundId = newRoomNotification && `${newRoomNotification}-continuous`; + const hasUnreadRoom = userSubscriptions.length > 0 || queue.length > 0; + useEffect(() => { let audio: ICustomSound; if (playNewRoomSoundContinuously && continuousCustomSoundId) { @@ -39,7 +41,7 @@ export const useContinuousSoundNotification = () => { return; } - if (userSubscriptions.length === 0) { + if (!hasUnreadRoom) { CustomSounds.pause(continuousCustomSoundId); return; } @@ -48,5 +50,5 @@ export const useContinuousSoundNotification = () => { volume: notificationsSoundVolume, loop: true, }); - }, [continuousCustomSoundId, playNewRoomSoundContinuously, userSubscriptions, notificationsSoundVolume]); + }, [continuousCustomSoundId, playNewRoomSoundContinuously, userSubscriptions, notificationsSoundVolume, hasUnreadRoom]); }; diff --git a/apps/meteor/client/lib/VideoConfManager.ts b/apps/meteor/client/lib/VideoConfManager.ts index 99ec760fba91..2fa07eba885f 100644 --- a/apps/meteor/client/lib/VideoConfManager.ts +++ b/apps/meteor/client/lib/VideoConfManager.ts @@ -84,9 +84,7 @@ type VideoConfEvents = { // When join call 'call/join': CurrentCallParams; - 'join/error': { error: string }; - - 'start/error': { error: string }; + 'error': { error: string }; 'capabilities/changed': void; }; @@ -112,6 +110,8 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter(); this.dismissedCalls = new Set(); this._preferences = { mic: true, cam: false }; @@ -161,18 +162,19 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter { if (!this.userId || this.isBusy()) { + this.emitError('error-videoconf-cant-start-call-with-manager-busy'); throw new Error('Video manager is busy.'); } - debug && console.log(`[VideoConf] Starting new call on room ${roomId}`); + this.debugLog(`[VideoConf] Starting new call on room ${roomId}`); this.startingNewCall = true; this.emit('calling/changed'); const { data } = await sdk.rest.post('/v1/video-conference.start', { roomId, title, allowRinging: true }).catch((e: any) => { - debug && console.error(`[VideoConf] Failed to start new call on room ${roomId}`); + console.error(`[VideoConf] Failed to start new call on room ${roomId}`, e); this.startingNewCall = false; this.emit('calling/changed'); - this.emit('start/error', { error: e?.xhr?.responseJSON?.error || 'unknown-error' }); + this.emitError(e?.xhr?.responseJSON?.error || 'error-videoconf-unexpected'); return Promise.reject(e); }); @@ -197,14 +199,15 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter { const { capabilities } = await sdk.rest.get('/v1/video-conference.capabilities').catch((e: any) => { - debug && console.error(`[VideoConf] Failed to load video conference capabilities`); + console.error(`[VideoConf] Failed to load video conference capabilities`, e); + this.emitError(); return Promise.reject(e); }); @@ -273,7 +278,7 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter this.dismissedCalls.delete(callId), CALL_TIMEOUT * 20); @@ -318,11 +327,11 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter { - debug && console.log(`[VideoConf] Joining call ${callId}.`); + this.debugLog(`[VideoConf] Joining call ${callId}.`); if (this.incomingDirectCalls.has(callId)) { const data = this.incomingDirectCalls.get(callId); if (data?.acceptTimeout) { - debug && console.log('[VideoConf] Clearing acceptance timeout'); + this.debugLog('[VideoConf] Clearing acceptance timeout'); clearTimeout(data.acceptTimeout); } this.removeIncomingCall(callId); @@ -368,17 +381,18 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter { - debug && console.error(`[VideoConf] Failed to join call ${callId}`); - this.emit('join/error', { error: e?.xhr?.responseJSON?.error || 'unknown-error' }); + console.error(`[VideoConf] Failed to join call ${callId}`, e); + this.emitError(e?.xhr?.responseJSON?.error || 'error-videoconf-join-failed'); return Promise.reject(e); }); if (!url) { + this.emitError('error-videoconf-missing-url'); throw new Error('Failed to get video conference URL.'); } - debug && console.log(`[VideoConf] Opening ${url}.`); + this.debugLog(`[VideoConf] Opening ${url}.`); this.emit('call/join', { url, callId, providerName }); } @@ -390,10 +404,22 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter= 1) && console.log(...args); + } + + private warnLog(...args: any[]): void { + (debug || this._logLevel >= 1) && console.warn(...args); + } + + private debugLog(...args: any[]): void { + (debug || this._logLevel >= 2) && console.log(...args); + } + private rejectIncomingCallsFromUser(userId: string): void { for (const [, { callId, uid }] of this.incomingDirectCalls) { if (userId === uid) { - debug && console.log(`[VideoConf] Rejecting old incoming call from user ${userId}`); + this.debugLog(`[VideoConf] Rejecting old incoming call from user ${userId}`); this.rejectIncomingCall(callId); } } @@ -401,6 +427,7 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter { if (this.currentCallHandler || this.currentCallData) { + this.emitError('error-videoconf-cant-start-call-with-manager-busy'); throw new Error('Video Conference State Error.'); } @@ -408,7 +435,7 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter { if (!this.currentCallHandler) { - debug && console.warn(`[VideoConf] Ringing interval was not properly cleared.`); + this.warnLog(`[VideoConf] Ringing interval was not properly cleared.`); return; } @@ -419,19 +446,19 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter { const joined = this.currentCallData?.joined; - debug && console.log(`[VideoConf] Stop ringing user ${uid}.`); + this.debugLog(`[VideoConf] Stop ringing user ${uid}.`); if (this.currentCallHandler) { clearInterval(this.currentCallHandler); this.currentCallHandler = undefined; @@ -439,7 +466,7 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter { if (!action || typeof action !== 'string') { - debug && console.error('[VideoConf] Invalid action received.'); + this.warnLog('[VideoConf] Invalid action received.', action, params); return; } if (!params || typeof params !== 'object' || !params.callId || !params.uid || !params.rid) { - debug && console.error('[VideoConf] Invalid params received.'); + this.warnLog('[VideoConf] Invalid params received.', action, params); return; } @@ -515,7 +542,7 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter { - debug && console.log(`[VideoConf] connecting user ${userId}`); + console.log(`[VideoConf] connecting user ${userId}`); this.userId = userId; const { stop, ready } = sdk.stream('notify-user', [`${userId}/video-conference`], (data) => this.onVideoConfNotification(data)); @@ -531,25 +558,25 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter { : () => undefined; CachedCollectionManager.register(this); - - if (!userRelated) { - void this.init(); - return; - } - - if (process.env.NODE_ENV === 'test') { - return; - } - - onLoggedIn(() => { - void this.init(); - }); - - Accounts.onLogout(() => { - this.ready.set(false); - }); } protected get eventName(): `${Name}-changed` | `${string}/${Name}-changed` { @@ -239,23 +222,27 @@ export class CachedCollection { async setupListener() { sdk.stream(this.eventType, [this.eventName], (async (action: 'removed' | 'changed', record: any) => { this.log('record received', action, record); - const newRecord = this.handleReceived(record, action); + await this.handleRecordEvent(action, record); + }) as (...args: unknown[]) => void); + } - if (!hasId(newRecord)) { - return; - } + protected async handleRecordEvent(action: 'removed' | 'changed', record: any) { + const newRecord = this.handleReceived(record, action); - if (action === 'removed') { - this.collection.remove(newRecord._id); - } else { - const { _id } = newRecord; - if (!_id) { - return; - } - this.collection.upsert({ _id } as any, newRecord); + if (!hasId(newRecord)) { + return; + } + + if (action === 'removed') { + this.collection.remove(newRecord._id); + } else { + const { _id } = newRecord; + if (!_id) { + return; } - await this.save(); - }) as (...args: unknown[]) => void); + this.collection.upsert({ _id } as any, newRecord); + } + await this.save(); } trySync(delay = 10) { @@ -368,4 +355,23 @@ export class CachedCollection { } private reconnectionComputation: Tracker.Computation | undefined; + + listen() { + if (!this.userRelated) { + void this.init(); + return; + } + + if (process.env.NODE_ENV === 'test') { + return; + } + + onLoggedIn(() => { + void this.init(); + }); + + Accounts.onLogout(() => { + this.ready.set(false); + }); + } } diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 6a782faafa1f..dbdaa1b04ac7 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -111,7 +111,7 @@ export type UploadsAPI = { export type ChatAPI = { readonly uid: string | null; readonly composer?: ComposerAPI; - readonly setComposerAPI: (composer: ComposerAPI) => void; + readonly setComposerAPI: (composer?: ComposerAPI) => void; readonly data: DataAPI; readonly uploads: UploadsAPI; readonly readStateManager: ReadStateManager; diff --git a/apps/meteor/client/lib/chats/flows/sendMessage.ts b/apps/meteor/client/lib/chats/flows/sendMessage.ts index e025730682d5..3d372900fb29 100644 --- a/apps/meteor/client/lib/chats/flows/sendMessage.ts +++ b/apps/meteor/client/lib/chats/flows/sendMessage.ts @@ -1,6 +1,5 @@ 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'; @@ -12,8 +11,6 @@ import { processSlashCommand } from './processSlashCommand'; import { processTooLongMessage } from './processTooLongMessage'; const process = async (chat: ChatAPI, message: IMessage, previewUrls?: string[], isSlashCommandAllowed?: boolean): Promise => { - KonchatNotification.removeRoomNotification(message.rid); - if (await processSetReaction(chat, message)) { return; } diff --git a/apps/meteor/client/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposer.tsx b/apps/meteor/client/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposer.tsx index 43d2b14d0628..f786328c0766 100644 --- a/apps/meteor/client/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposer.tsx +++ b/apps/meteor/client/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposer.tsx @@ -98,7 +98,7 @@ const CannedResponsesComposer = ({ onChange, ...props }: ComponentProps - + - + ); }; diff --git a/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx index b90d577524ba..2fb77f997e34 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx @@ -1,6 +1,6 @@ import type { INotificationDesktop } from '@rocket.chat/core-typings'; import type { SelectOption } from '@rocket.chat/fuselage'; -import { Accordion, Field, FieldLabel, FieldRow, FieldHint, Select, FieldGroup, ToggleSwitch, Button } from '@rocket.chat/fuselage'; +import { AccordionItem, Field, FieldLabel, FieldRow, FieldHint, Select, FieldGroup, ToggleSwitch, Button } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useUserPreference, useSetting } from '@rocket.chat/ui-contexts'; @@ -93,7 +93,7 @@ const PreferencesNotificationsSection = () => { const enableMobileRingingId = useUniqueId(); return ( - + {t('Desktop_Notifications')} @@ -231,7 +231,7 @@ const PreferencesNotificationsSection = () => { )} - + ); }; diff --git a/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx index d88ecaa99dbe..8e9979578ca7 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx @@ -1,5 +1,5 @@ import type { SelectOption } from '@rocket.chat/fuselage'; -import { Accordion, Field, FieldLabel, FieldRow, Select, FieldGroup, ToggleSwitch, FieldHint, Slider } from '@rocket.chat/fuselage'; +import { AccordionItem, Field, FieldLabel, FieldRow, Select, FieldGroup, ToggleSwitch, FieldHint, Slider } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useTranslation, useCustomSound } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -21,7 +21,7 @@ const PreferencesSoundSection = () => { const voipRingerVolumeId = useUniqueId(); return ( - + {t('Master_volume')} @@ -150,7 +150,7 @@ const PreferencesSoundSection = () => { - + ); }; diff --git a/apps/meteor/client/views/account/preferences/PreferencesUserPresenceSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesUserPresenceSection.tsx index edfece178e52..39ee0dbc747a 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesUserPresenceSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesUserPresenceSection.tsx @@ -1,4 +1,4 @@ -import { Accordion, Field, FieldLabel, FieldRow, NumberInput, FieldGroup, ToggleSwitch } from '@rocket.chat/fuselage'; +import { AccordionItem, Field, FieldLabel, FieldRow, NumberInput, FieldGroup, ToggleSwitch } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; @@ -12,7 +12,7 @@ const PreferencesUserPresenceSection = () => { const idleTimeLimit = useUniqueId(); return ( - + @@ -33,7 +33,7 @@ const PreferencesUserPresenceSection = () => { - + ); }; diff --git a/apps/meteor/client/views/account/profile/AccountProfileForm.tsx b/apps/meteor/client/views/account/profile/AccountProfileForm.tsx index 54875f0686cb..73e0910ef207 100644 --- a/apps/meteor/client/views/account/profile/AccountProfileForm.tsx +++ b/apps/meteor/client/views/account/profile/AccountProfileForm.tsx @@ -64,7 +64,7 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle formState: { errors }, } = useFormContext(); - const { email, avatar, username } = watch(); + const { email, avatar, username, name: userFullName } = watch(); const previousEmail = user ? getUserEmailAddress(user) : ''; const previousUsername = user?.username || ''; @@ -150,6 +150,7 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle { {allowPasswordChange && ( - + - + )} {(twoFactorTOTP || showEmailTwoFactor) && twoFactorEnabled && ( - + {twoFactorTOTP && } {showEmailTwoFactor && } - + )} {e2eEnabled && ( - - + )} diff --git a/apps/meteor/client/views/admin/emailInbox/EmailInboxForm.tsx b/apps/meteor/client/views/admin/emailInbox/EmailInboxForm.tsx index ab92971db5ba..b7f02c5c8260 100644 --- a/apps/meteor/client/views/admin/emailInbox/EmailInboxForm.tsx +++ b/apps/meteor/client/views/admin/emailInbox/EmailInboxForm.tsx @@ -1,6 +1,7 @@ import type { IEmailInboxPayload } from '@rocket.chat/core-typings'; import { Accordion, + AccordionItem, Button, ButtonGroup, TextInput, @@ -191,7 +192,7 @@ const EmailInboxForm = ({ inboxData }: { inboxData?: IEmailInboxPayload }): Reac - + @@ -304,8 +305,8 @@ const EmailInboxForm = ({ inboxData }: { inboxData?: IEmailInboxPayload }): Reac {t('Only_Members_Selected_Department_Can_View_Channel')} - - + + @@ -426,8 +427,8 @@ const EmailInboxForm = ({ inboxData }: { inboxData?: IEmailInboxPayload }): Reac - - + + @@ -575,7 +576,7 @@ const EmailInboxForm = ({ inboxData }: { inboxData?: IEmailInboxPayload }): Reac - + diff --git a/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx index 615fd20cf5a6..d4adff4c2a70 100644 --- a/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx +++ b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx @@ -4,6 +4,7 @@ import { Box, ToggleSwitch, Accordion, + AccordionItem, Field, FieldGroup, FieldLabel, @@ -86,7 +87,7 @@ const AdminFeaturePreviewPage = () => { {grouppedFeaturesPreview?.map(([group, features], index) => ( - + {features.map((feature) => ( @@ -107,7 +108,7 @@ const AdminFeaturePreviewPage = () => { ))} - + ))} diff --git a/apps/meteor/client/views/admin/import/NewImportPage.tsx b/apps/meteor/client/views/admin/import/NewImportPage.tsx index 54de54e095dd..34ba161de9a0 100644 --- a/apps/meteor/client/views/admin/import/NewImportPage.tsx +++ b/apps/meteor/client/views/admin/import/NewImportPage.tsx @@ -185,7 +185,7 @@ function NewImportPage() { undefined; return ( - + router.navigate('/admin/import')}> {importer && ( diff --git a/apps/meteor/client/views/admin/integrations/outgoing/history/HistoryItem.tsx b/apps/meteor/client/views/admin/integrations/outgoing/history/HistoryItem.tsx index e823172cba8a..319ab49600ad 100644 --- a/apps/meteor/client/views/admin/integrations/outgoing/history/HistoryItem.tsx +++ b/apps/meteor/client/views/admin/integrations/outgoing/history/HistoryItem.tsx @@ -1,5 +1,5 @@ import type { IIntegrationHistory, Serialized } from '@rocket.chat/core-typings'; -import { Button, Icon, Box, Accordion, Field, FieldGroup, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; +import { Button, Icon, Box, AccordionItem, Field, FieldGroup, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useMethod } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -51,7 +51,7 @@ const HistoryItem = ({ data }: { data: Serialized }) => { const errorStackCode = useHighlightedCode('json', JSON.stringify(errorStack || '', null, 2)); return ( - @@ -200,7 +200,7 @@ const HistoryItem = ({ data }: { data: Serialized }) => { )} - + ); }; diff --git a/apps/meteor/client/views/admin/settings/Setting/Setting.stories.tsx b/apps/meteor/client/views/admin/settings/Setting/Setting.stories.tsx index 7705c223491a..17aef33ab60a 100644 --- a/apps/meteor/client/views/admin/settings/Setting/Setting.stories.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/Setting.stories.tsx @@ -15,13 +15,6 @@ export default { argTypesRegex: '^on.*', }, }, - decorators: [ - (fn) => ( -
-
{fn()}
-
- ), - ], } satisfies Meta; export const Default: StoryFn = (args) => ; diff --git a/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.stories.tsx index 231d3d1a42b3..8c149c246506 100644 --- a/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.stories.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.stories.tsx @@ -10,7 +10,7 @@ export default { decorators: [ (fn) => (
-
+
{fn()}
diff --git a/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.tsx index 39604cd1146e..5475841a8940 100644 --- a/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/AssetSettingInput.tsx @@ -1,4 +1,5 @@ -import { Box, Button, Field, FieldLabel, FieldRow, Icon } from '@rocket.chat/fuselage'; +import { css } from '@rocket.chat/css-in-js'; +import { Box, Button, Field, FieldLabel, FieldRow, Icon, Palette } from '@rocket.chat/fuselage'; import { Random } from '@rocket.chat/random'; import { useToastMessageDispatch, useEndpoint, useTranslation, useUpload } from '@rocket.chat/ui-contexts'; import type { ChangeEventHandler, DragEvent, ReactElement, SyntheticEvent } from 'react'; @@ -54,13 +55,52 @@ function AssetSettingInput({ _id, label, value, asset, required, disabled, fileC } }; + const settingsFilePreview = css` + display: flex; + align-items: center; + + & input[type='file'] { + position: absolute !important; + z-index: 10000; + top: 0; + left: 0; + width: 100%; + height: 100%; + cursor: pointer; + opacity: 0; + + & * { + cursor: pointer; + } + } + + & .preview { + overflow: hidden; + width: 100px; + height: 40px; + margin-right: 0.75rem; + border-width: 1px; + border-color: ${Palette.stroke['stroke-light']}; + border-radius: 4px; + background-repeat: no-repeat; + background-position: center center; + background-size: contain; + + &.no-file { + display: flex; + align-items: center; + justify-content: center; + } + } + `; + return ( {label} -
+ {value?.url ? (
{t('Select_file')} - + )}
-
+
); diff --git a/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPage.tsx b/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPage.tsx index 662dd2d52c9f..54363a2df112 100644 --- a/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPage.tsx @@ -152,7 +152,7 @@ const SettingsGroupPage = ({ )} - {children} + {children} )} diff --git a/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPageSkeleton.tsx b/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPageSkeleton.tsx index ad6f93390c1d..a5c2e022299d 100644 --- a/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPageSkeleton.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPageSkeleton.tsx @@ -14,7 +14,7 @@ const SettingsGroupPageSkeleton = () => ( - + diff --git a/apps/meteor/client/views/admin/settings/SettingsSection/SettingsSection.tsx b/apps/meteor/client/views/admin/settings/SettingsSection/SettingsSection.tsx index de575dbad302..f1dc393b07b2 100644 --- a/apps/meteor/client/views/admin/settings/SettingsSection/SettingsSection.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsSection/SettingsSection.tsx @@ -1,5 +1,5 @@ import { isSetting, isSettingColor } from '@rocket.chat/core-typings'; -import { Accordion, Box, Button, FieldGroup } from '@rocket.chat/fuselage'; +import { AccordionItem, Box, Button, FieldGroup } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import type { ReactElement, ReactNode } from 'react'; @@ -71,7 +71,7 @@ function SettingsSection({ groupId, hasReset = true, sectionName, currentTab, so }; return ( - )} - + ); } diff --git a/apps/meteor/client/views/admin/settings/SettingsSection/SettingsSectionSkeleton.tsx b/apps/meteor/client/views/admin/settings/SettingsSection/SettingsSectionSkeleton.tsx index 69466eba374f..c47dd339170c 100644 --- a/apps/meteor/client/views/admin/settings/SettingsSection/SettingsSectionSkeleton.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsSection/SettingsSectionSkeleton.tsx @@ -1,4 +1,4 @@ -import { Accordion, Box, FieldGroup, Skeleton } from '@rocket.chat/fuselage'; +import { AccordionItem, Box, FieldGroup, Skeleton } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React from 'react'; @@ -6,7 +6,7 @@ import SettingSkeleton from '../Setting/SettingSkeleton'; function SettingsSectionSkeleton(): ReactElement { return ( - }> + }> @@ -16,7 +16,7 @@ function SettingsSectionSkeleton(): ReactElement { ))} - + ); } diff --git a/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/VoipGroupPage.tsx b/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/VoipGroupPage.tsx index 249bdab99616..63ea3c1d7c2b 100644 --- a/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/VoipGroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/VoipGroupPage.tsx @@ -57,7 +57,7 @@ function VoipGroupPage({ _id, onClickBack, ...group }: VoipGroupPageProps) { ) : ( - + {sections.map((sectionName) => ( ))} diff --git a/apps/meteor/client/views/admin/subscription/SubscriptionPage.tsx b/apps/meteor/client/views/admin/subscription/SubscriptionPage.tsx index e6f5beffe7ba..7f4635950a5a 100644 --- a/apps/meteor/client/views/admin/subscription/SubscriptionPage.tsx +++ b/apps/meteor/client/views/admin/subscription/SubscriptionPage.tsx @@ -1,4 +1,4 @@ -import { Accordion, Box, Button, ButtonGroup, Callout, Grid } from '@rocket.chat/fuselage'; +import { Accordion, AccordionItem, Box, Button, ButtonGroup, Callout, Grid } from '@rocket.chat/fuselage'; import { useDebouncedValue, useSessionStorage } from '@rocket.chat/fuselage-hooks'; import { useSearchParameter, useRouter } from '@rocket.chat/ui-contexts'; import { t } from 'i18next'; @@ -21,9 +21,12 @@ import PlanCardCommunity from './components/cards/PlanCard/PlanCardCommunity'; import SeatsCard from './components/cards/SeatsCard'; import { useCancelSubscriptionModal } from './hooks/useCancelSubscriptionModal'; import { useWorkspaceSync } from './hooks/useWorkspaceSync'; -import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; +import UiKitSubscriptionLicense from './surface/UiKitSubscriptionLicense'; +import { Page, PageScrollableContentWithShadow } from '../../../components/Page'; +import PageBlockWithBorder from '../../../components/Page/PageBlockWithBorder'; +import PageHeaderNoShadow from '../../../components/Page/PageHeaderNoShadow'; import { useIsEnterprise } from '../../../hooks/useIsEnterprise'; -import { useInvalidateLicense, useLicense } from '../../../hooks/useLicense'; +import { useInvalidateLicense, useLicenseWithCloudAnnouncement } from '../../../hooks/useLicense'; import { useRegistrationStatus } from '../../../hooks/useRegistrationStatus'; function useShowLicense() { @@ -48,7 +51,7 @@ const SubscriptionPage = () => { const router = useRouter(); const { data: enterpriseData } = useIsEnterprise(); const { isRegistered } = useRegistrationStatus(); - const { data: licensesData, isLoading: isLicenseLoading } = useLicense({ loadValues: true }); + const { data: licensesData, isLoading: isLicenseLoading } = useLicenseWithCloudAnnouncement({ loadValues: true }); const syncLicenseUpdate = useWorkspaceSync(); const invalidateLicenseQuery = useInvalidateLicense(); @@ -56,7 +59,7 @@ const SubscriptionPage = () => { const showSubscriptionCallout = useDebouncedValue(subscriptionSuccess || syncLicenseUpdate.isLoading, 10000); - const { license, limits, activeModules = [] } = licensesData || {}; + const { license, limits, activeModules = [], cloudSyncAnnouncement } = licensesData || {}; const { isEnterprise = true } = enterpriseData || {}; const getKeyLimit = (key: 'monthlyActiveContacts' | 'activeUsers') => { @@ -99,7 +102,7 @@ const SubscriptionPage = () => { return ( - + {isRegistered && ( + + + + ); +}; + +export default ComposerSelectMessages; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 6546a6be9245..0a52355ac58a 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -131,7 +131,11 @@ const MessageBox = ({ const callbackRef = useCallback( (node: HTMLTextAreaElement) => { - if (node === null || chat.composer) { + if (node === null && chat.composer) { + return chat.setComposerAPI(); + } + + if (chat.composer) { return; } chat.setComposerAPI(createComposerAPI(node, storageID)); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx index 2010a589edc6..5ab3804f2d3a 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx @@ -35,9 +35,9 @@ const FormattingToolbarDropdown = ({ composer, items, disabled }: FormattingTool }; }); - const sections = [{ title: t('Message_Formatting_Toolbox'), items: formattingItems }]; + const sections = [{ title: t('Message_Formatting_toolbox'), items: formattingItems }]; - return ; + return ; }; export default FormattingToolbarDropdown; diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx index f6a621200119..c05faece42e1 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx @@ -1,18 +1,43 @@ import type { SelectOption } from '@rocket.chat/fuselage'; -import { useUniqueId } from '@rocket.chat/fuselage-hooks'; -import React, { useMemo } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; +import { + FieldError, + Field, + FieldLabel, + FieldRow, + TextAreaInput, + TextInput, + ButtonGroup, + Button, + Icon, + FieldGroup, + Select, + InputBox, + Callout, +} from '@rocket.chat/fuselage'; +import { useAutoFocus, useUniqueId } from '@rocket.chat/fuselage-hooks'; +import React, { useContext, useEffect, useMemo } from 'react'; +import { Controller, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import FileExport from './FileExport'; -import MailExportForm from './MailExportForm'; -import { ContextualbarHeader, ContextualbarIcon, ContextualbarTitle, ContextualbarClose } from '../../../../components/Contextualbar'; +import { useDownloadExportMutation } from './useDownloadExportMutation'; +import { useRoomExportMutation } from './useRoomExportMutation'; +import { validateEmail } from '../../../../../lib/emailValidator'; +import { + ContextualbarHeader, + ContextualbarScrollableContent, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarClose, + ContextualbarFooter, +} from '../../../../components/Contextualbar'; +import UserAutoCompleteMultiple from '../../../../components/UserAutoCompleteMultiple'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; +import { SelectedMessageContext, useCountSelected } from '../../MessageList/contexts/SelectedMessagesContext'; import { useRoom } from '../../contexts/RoomContext'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; -export type MailExportFormValues = { - type: 'email' | 'file'; +export type ExportMessagesFormValues = { + type: 'email' | 'file' | 'download'; dateFrom: string; dateTo: string; format: 'html' | 'json'; @@ -24,16 +49,25 @@ export type MailExportFormValues = { const ExportMessages = () => { const { t } = useTranslation(); - const room = useRoom(); - const { closeTab } = useRoomToolbox(); + const formFocus = useAutoFocus(); + const room = useRoom(); + const isE2ERoom = room.encrypted; const roomName = room?.t && roomCoordinator.getRoomName(room.t, room); - const methods = useForm({ + const { + control, + formState: { errors, isSubmitting }, + watch, + register, + setValue, + handleSubmit, + clearErrors, + } = useForm({ mode: 'onBlur', defaultValues: { - type: 'email', + type: isE2ERoom ? 'download' : 'email', dateFrom: '', dateTo: '', toUsers: [], @@ -43,18 +77,94 @@ const ExportMessages = () => { postProcess: 'sprintf', sprintf: [roomName], }), - format: 'html', + format: isE2ERoom ? 'json' : 'html', }, }); + const exportOptions = useMemo( () => [ - ['email', t('Send_via_email')], - ['file', t('Export_as_file')], + ['email', t('Send_email')], + ['file', t('Send_file_via_email')], + ['download', t('Download_file')], ], [t], ); + const outputOptions = useMemo( + () => [ + ['html', t('HTML')], + ['json', t('JSON')], + ], + [t], + ); + + const roomExportMutation = useRoomExportMutation(); + const downloadExportMutation = useDownloadExportMutation(); + + const { selectedMessageStore } = useContext(SelectedMessageContext); + const messageCount = useCountSelected(); + + const { type, toUsers } = watch(); + + useEffect(() => { + if (type !== 'file') { + selectedMessageStore.setIsSelecting(true); + } + + return (): void => { + selectedMessageStore.reset(); + }; + }, [type, selectedMessageStore]); + + useEffect(() => { + if (type === 'email') { + setValue('format', 'html'); + } + + if (type === 'download') { + setValue('format', 'json'); + } + + setValue('messagesCount', messageCount); + }, [type, setValue, messageCount]); + + const handleExport = async ({ type, toUsers, dateFrom, dateTo, format, subject, additionalEmails }: ExportMessagesFormValues) => { + const messages = selectedMessageStore.getSelectedMessages(); + + if (type === 'download') { + return downloadExportMutation.mutateAsync({ + mids: messages, + }); + } + + if (type === 'file') { + return roomExportMutation.mutateAsync({ + rid: room._id, + type: 'file', + ...(dateFrom && { dateFrom }), + ...(dateTo && { dateTo }), + format, + }); + } + + roomExportMutation.mutateAsync({ + rid: room._id, + type: 'email', + toUsers, + toEmails: additionalEmails?.split(','), + subject, + messages, + }); + }; + const formId = useUniqueId(); + const methodField = useUniqueId(); + const formatField = useUniqueId(); + const toUsersField = useUniqueId(); + const dateFromField = useUniqueId(); + const dateToField = useUniqueId(); + const additionalEmailsField = useUniqueId(); + const subjectField = useUniqueId(); return ( <> @@ -63,14 +173,183 @@ const ExportMessages = () => { {t('Export_Messages')} - - {methods.watch('type') === 'email' && ( - - )} - {methods.watch('type') === 'file' && ( - - )} - + +
+ + {room.createdOTR && ( + + + {t('OTR_messages_cannot_be_exported')} + + + )} + + {t('Method')} + + ( + + )} + /> + + + {type === 'file' && ( + <> + + {t('Date_From')} + + } + /> + + + + {t('Date_to')} + + } + /> + + + + )} + {type === 'email' && ( + <> + + {t('To_users')} + + ( + { + onChange(value); + clearErrors('additionalEmails'); + }} + onBlur={onBlur} + name={name} + /> + )} + /> + + + + {t('To_additional_emails')} + + { + if (additionalEmails === '') { + return undefined; + } + + const emails = additionalEmails?.split(',').map((email) => email.trim()); + if (Array.isArray(emails) && emails.every((email) => validateEmail(email.trim()))) { + return undefined; + } + + return t('Mail_Message_Invalid_emails', { postProcess: 'sprintf', sprintf: [additionalEmails] }); + }, + validateToUsers: (additionalEmails) => { + if (additionalEmails !== '' || toUsers?.length > 0) { + return undefined; + } + + return t('Mail_Message_Missing_to'); + }, + }, + }} + render={({ field }) => ( + } + aria-describedby={`${additionalEmailsField}-error`} + aria-invalid={Boolean(errors?.additionalEmails?.message)} + error={errors?.additionalEmails?.message} + /> + )} + /> + + {errors?.additionalEmails && ( + + {errors.additionalEmails.message} + + )} + + + {t('Subject')} + + ( + } /> + )} + /> + + + + )} + {type !== 'file' && ( + <> + (messagesCount > 0 ? undefined : t('Mail_Message_No_messages_selected_select_all')), + })} + /> + {errors.messagesCount && ( + + + {errors.messagesCount.message} + + + )} + + )} + +
+
+ + + + + + ); }; diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/FileExport.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/FileExport.tsx deleted file mode 100644 index 2d4a3bf0030c..000000000000 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/FileExport.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import type { SelectOption } from '@rocket.chat/fuselage'; -import { Field, FieldLabel, FieldRow, Select, ButtonGroup, Button, FieldGroup, InputBox } from '@rocket.chat/fuselage'; -import { useAutoFocus, useUniqueId } from '@rocket.chat/fuselage-hooks'; -import React, { useMemo } from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; - -import type { MailExportFormValues } from './ExportMessages'; -import { useRoomExportMutation } from './useRoomExportMutation'; -import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../../components/Contextualbar'; - -type FileExportProps = { - formId: string; - rid: IRoom['_id']; - onCancel: () => void; - exportOptions: SelectOption[]; -}; - -const FileExport = ({ formId, rid, exportOptions, onCancel }: FileExportProps) => { - const { t } = useTranslation(); - const { control, handleSubmit } = useFormContext(); - const roomExportMutation = useRoomExportMutation(); - const formFocus = useAutoFocus(); - - const outputOptions = useMemo( - () => [ - ['html', t('HTML')], - ['json', t('JSON')], - ], - [t], - ); - - const handleExport = ({ dateFrom, dateTo, format }: MailExportFormValues) => { - roomExportMutation.mutateAsync({ - rid, - type: 'file', - ...(dateFrom && { dateFrom }), - ...(dateTo && { dateTo }), - format, - }); - }; - - const typeField = useUniqueId(); - const dateFromField = useUniqueId(); - const dateToField = useUniqueId(); - const formatField = useUniqueId(); - - return ( - <> - -
- - - {t('Method')} - - } - /> - - - -
-
- - - - - - - - ); -}; - -export default FileExport; diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/MailExportForm.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/MailExportForm.tsx deleted file mode 100644 index b6f0e4b88bf2..000000000000 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/MailExportForm.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { css } from '@rocket.chat/css-in-js'; -import type { SelectOption } from '@rocket.chat/fuselage'; -import { - FieldError, - Field, - FieldLabel, - FieldRow, - TextAreaInput, - TextInput, - ButtonGroup, - Button, - Box, - Icon, - Callout, - FieldGroup, - Select, -} from '@rocket.chat/fuselage'; -import { useAutoFocus, useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; -import React, { useEffect, useContext } from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; - -import type { MailExportFormValues } from './ExportMessages'; -import { useRoomExportMutation } from './useRoomExportMutation'; -import { validateEmail } from '../../../../../lib/emailValidator'; -import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../../components/Contextualbar'; -import UserAutoCompleteMultiple from '../../../../components/UserAutoCompleteMultiple'; -import { SelectedMessageContext, useCountSelected } from '../../MessageList/contexts/SelectedMessagesContext'; - -type MailExportFormProps = { - formId: string; - rid: IRoom['_id']; - onCancel: () => void; - exportOptions: SelectOption[]; -}; - -const MailExportForm = ({ formId, rid, onCancel, exportOptions }: MailExportFormProps) => { - const { t } = useTranslation(); - const formFocus = useAutoFocus(); - - const { - watch, - setValue, - control, - register, - formState: { errors, isDirty, isSubmitting }, - handleSubmit, - clearErrors, - } = useFormContext(); - const roomExportMutation = useRoomExportMutation(); - - const { selectedMessageStore } = useContext(SelectedMessageContext); - const messages = selectedMessageStore.getSelectedMessages(); - - const count = useCountSelected(); - - const clearSelection = useMutableCallback(() => { - selectedMessageStore.clearStore(); - }); - - useEffect(() => { - selectedMessageStore.setIsSelecting(true); - return (): void => { - selectedMessageStore.reset(); - }; - }, [selectedMessageStore]); - - const { toUsers } = watch(); - - useEffect(() => { - setValue('messagesCount', messages.length); - }, [setValue, messages.length]); - - const handleExport = async ({ toUsers, subject, additionalEmails }: MailExportFormValues) => { - roomExportMutation.mutateAsync({ - rid, - type: 'email', - toUsers, - toEmails: additionalEmails?.split(','), - subject, - messages, - }); - }; - - const clickable = css` - cursor: pointer; - `; - - const methodField = useUniqueId(); - const toUsersField = useUniqueId(); - const additionalEmailsField = useUniqueId(); - const subjectField = useUniqueId(); - - return ( - <> - -
- - - {t('Method')} - - (messagesCount > 0 ? undefined : t('Mail_Message_No_messages_selected_select_all')), - })} - /> - {errors.messagesCount && {errors.messagesCount.message}} - - - {t('To_users')} - - ( - { - onChange(value); - clearErrors('additionalEmails'); - }} - onBlur={onBlur} - name={name} - /> - )} - /> - - - - {t('To_additional_emails')} - - { - const emails = additionalEmails?.split(',').map((email) => email.trim()); - if (Array.isArray(emails) && emails.every((email) => validateEmail(email.trim()))) { - return undefined; - } - - return t('Mail_Message_Invalid_emails', { postProcess: 'sprintf', sprintf: [additionalEmails] }); - }, - validateToUsers: (additionalEmails) => { - if (additionalEmails !== '' || toUsers?.length > 0) { - return undefined; - } - - return t('Mail_Message_Missing_to'); - }, - }, - }} - render={({ field }) => ( - } - aria-describedby={`${additionalEmailsField}-error`} - aria-invalid={Boolean(errors?.additionalEmails?.message)} - error={errors?.additionalEmails?.message} - /> - )} - /> - - {errors?.additionalEmails && ( - - {errors.additionalEmails.message} - - )} - - - {t('Subject')} - - } />} - /> - - - -
-
- - - - - - - - ); -}; - -export default MailExportForm; diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts b/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts new file mode 100644 index 000000000000..f35f153da8bb --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts @@ -0,0 +1,50 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { useToastMessageDispatch, useUser } from '@rocket.chat/ui-contexts'; +import { useMutation } from '@tanstack/react-query'; +import type { FindOptions } from 'mongodb'; +import { useTranslation } from 'react-i18next'; + +import { Messages } from '../../../../../app/models/client'; +import { downloadJsonAs } from '../../../../lib/download'; +import { useRoom } from '../../contexts/RoomContext'; + +const messagesFields: FindOptions = { projection: { _id: 1, ts: 1, u: 1, msg: 1, _updatedAt: 1, tlm: 1, replies: 1, tmid: 1 } }; + +export const useDownloadExportMutation = () => { + const { t } = useTranslation(); + const room = useRoom(); + const user = useUser(); + const dispatchToastMessage = useToastMessageDispatch(); + + return useMutation({ + mutationFn: async ({ mids }: { mids: IMessage['_id'][] }) => { + const messages = Messages.find( + { + $or: [{ _id: { $in: mids } }, { tmid: { $in: mids } }], + }, + messagesFields, + ).fetch(); + + const fileData = { + roomId: room._id, + roomName: room.fname || room.name, + userExport: { + id: user?._id, + username: user?.username, + name: user?.name, + roles: user?.roles, + }, + exportDate: new Date().toISOString(), + messages, + }; + + return downloadJsonAs(fileData, `exportedMessages-${new Date().toISOString()}`); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + onSuccess: () => { + dispatchToastMessage({ type: 'success', message: t('Messages_exported_successfully') }); + }, + }); +}; diff --git a/apps/meteor/client/views/room/contextualBar/NotificationPreferences/components/NotificationByDevice.tsx b/apps/meteor/client/views/room/contextualBar/NotificationPreferences/components/NotificationByDevice.tsx index a0a7b56c2ef3..f741013f3c7f 100644 --- a/apps/meteor/client/views/room/contextualBar/NotificationPreferences/components/NotificationByDevice.tsx +++ b/apps/meteor/client/views/room/contextualBar/NotificationPreferences/components/NotificationByDevice.tsx @@ -1,4 +1,4 @@ -import { Box, Accordion, Icon, FieldGroup } from '@rocket.chat/fuselage'; +import { Box, AccordionItem, Icon, FieldGroup } from '@rocket.chat/fuselage'; import type { Keys as IconName } from '@rocket.chat/icons'; import type { ReactElement, ReactNode } from 'react'; import React, { memo } from 'react'; @@ -10,7 +10,7 @@ type NotificationByDeviceProps = { }; const NotificationByDevice = ({ device, icon, children }: NotificationByDeviceProps): ReactElement => ( - @@ -22,7 +22,7 @@ const NotificationByDevice = ({ device, icon, children }: NotificationByDevicePr data-qa-id={`${device}-notifications`} > {children} - + ); export default memo(NotificationByDevice); diff --git a/apps/meteor/client/views/room/modals/E2EEModals/DisableE2EEModal.tsx b/apps/meteor/client/views/room/modals/E2EEModals/DisableE2EEModal.tsx index e3b231fa9476..19626d708dcb 100644 --- a/apps/meteor/client/views/room/modals/E2EEModals/DisableE2EEModal.tsx +++ b/apps/meteor/client/views/room/modals/E2EEModals/DisableE2EEModal.tsx @@ -1,4 +1,4 @@ -import { Accordion, Box, Button } from '@rocket.chat/fuselage'; +import { Accordion, AccordionItem, Box, Button } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React from 'react'; import { Trans, useTranslation } from 'react-i18next'; @@ -36,14 +36,14 @@ const DisableE2EEModal = ({ onConfirm, onCancel, roomType, canResetRoomKey, onRe {t('E2E_disable_encryption_reset_keys_description')}
- + {t('E2E_reset_encryption_keys_description')} - + )} diff --git a/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx b/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx index 4100751037ff..e70156126df0 100644 --- a/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx +++ b/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx @@ -4,8 +4,6 @@ import React, { useMemo } from 'react'; import { SelectedMessageContext } from '../MessageList/contexts/SelectedMessagesContext'; -// data-qa-select - export const selectedMessageStore = new (class SelectMessageStore extends Emitter< { change: undefined; @@ -14,8 +12,20 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte > { store = new Set(); + availableMessages = new Set(); + isSelecting = false; + addAvailableMessage(mid: string): void { + this.availableMessages.add(mid); + this.emit('change'); + } + + removeAvailableMessage(mid: string): void { + this.availableMessages.delete(mid); + this.emit('change'); + } + setIsSelecting(isSelecting: boolean): void { this.isSelecting = isSelecting; this.emit('toggleIsSelecting', isSelecting); @@ -49,6 +59,10 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte return this.store.size; } + availableMessagesCount(): number { + return this.availableMessages.size; + } + clearStore(): void { const selectedMessages = this.getSelectedMessages(); this.store.clear(); @@ -61,6 +75,11 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte this.isSelecting = false; this.emit('toggleIsSelecting', false); } + + toggleAll(mids: string[]): void { + this.store = new Set([...this.store, ...mids]); + this.emit('change'); + } })(); type SelectedMessagesProviderProps = { diff --git a/apps/meteor/client/views/root/MainLayout/EmbeddedPreload.tsx b/apps/meteor/client/views/root/MainLayout/EmbeddedPreload.tsx new file mode 100644 index 000000000000..1de34cbf5f9f --- /dev/null +++ b/apps/meteor/client/views/root/MainLayout/EmbeddedPreload.tsx @@ -0,0 +1,36 @@ +import { useUserId } from '@rocket.chat/ui-contexts'; +import type { ReactElement, ReactNode } from 'react'; +import React, { useEffect } from 'react'; + +import { CachedChatRoom, CachedChatSubscription } from '../../../../app/models/client'; +import { settings } from '../../../../app/settings/client'; +import { mainReady } from '../../../../app/ui-utils/client'; +import { useReactiveVar } from '../../../hooks/useReactiveVar'; +import { isSyncReady } from '../../../lib/userData'; +import PageLoading from '../PageLoading'; + +const EmbeddedPreload = ({ children }: { children: ReactNode }): ReactElement => { + const uid = useUserId(); + const subscriptionsReady = useReactiveVar(CachedChatSubscription.ready); + const settingsReady = useReactiveVar(settings.cachedCollection.ready); + const userDataReady = useReactiveVar(isSyncReady); + + const ready = !uid || (userDataReady && subscriptionsReady && settingsReady); + + useEffect(() => { + mainReady.set(ready); + }, [ready]); + + useEffect(() => { + CachedChatSubscription.ready.set(true); + CachedChatRoom.ready.set(true); + }, [ready]); + + if (!ready) { + return ; + } + + return <>{children}; +}; + +export default EmbeddedPreload; diff --git a/apps/meteor/client/views/root/MainLayout/MainLayout.tsx b/apps/meteor/client/views/root/MainLayout/MainLayout.tsx index cd13000d4ec2..ddf9bfbac73b 100644 --- a/apps/meteor/client/views/root/MainLayout/MainLayout.tsx +++ b/apps/meteor/client/views/root/MainLayout/MainLayout.tsx @@ -2,8 +2,10 @@ import type { ReactElement, ReactNode } from 'react'; import React, { Suspense } from 'react'; import AuthenticationCheck from './AuthenticationCheck'; +import EmbeddedPreload from './EmbeddedPreload'; import Preload from './Preload'; import { useCustomScript } from './useCustomScript'; +import { useEmbeddedLayout } from '../../../hooks/useEmbeddedLayout'; type MainLayoutProps = { children?: ReactNode; @@ -12,6 +14,18 @@ type MainLayoutProps = { const MainLayout = ({ children = null }: MainLayoutProps): ReactElement => { useCustomScript(); + const isEmbeddedLayout = useEmbeddedLayout(); + + if (isEmbeddedLayout) { + return ( + + + {children} + + + ); + } + return ( diff --git a/apps/meteor/client/views/root/MainLayout/Preload.tsx b/apps/meteor/client/views/root/MainLayout/Preload.tsx index c1e233d8a8cc..a86d1d6d5414 100644 --- a/apps/meteor/client/views/root/MainLayout/Preload.tsx +++ b/apps/meteor/client/views/root/MainLayout/Preload.tsx @@ -2,7 +2,7 @@ import { useUserId } from '@rocket.chat/ui-contexts'; import type { ReactElement, ReactNode } from 'react'; import React, { useEffect } from 'react'; -import { CachedChatSubscription } from '../../../../app/models/client'; +import { CachedChatRoom, CachedChatSubscription } from '../../../../app/models/client'; import { settings } from '../../../../app/settings/client'; import { mainReady } from '../../../../app/ui-utils/client'; import { useReactiveVar } from '../../../hooks/useReactiveVar'; @@ -21,6 +21,11 @@ const Preload = ({ children }: { children: ReactNode }): ReactElement => { mainReady.set(ready); }, [ready]); + useEffect(() => { + CachedChatSubscription.listen(); + CachedChatRoom.listen(); + }, []); + if (!ready) { return ; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/lib/contacts.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/contacts.ts index cb8fd7e526e1..587a1704eee0 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/lib/contacts.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/contacts.ts @@ -6,7 +6,7 @@ import { Livechat } from '../../../../../../app/livechat/server/lib/LivechatType import { i18n } from '../../../../../../server/lib/i18n'; export async function changeContactBlockStatus({ block, visitor }: { visitor: ILivechatContactVisitorAssociation; block: boolean }) { - const result = await LivechatContacts.updateContactChannel(visitor, { blocked: block }); + const result = await LivechatContacts.setChannelBlockStatus(visitor, block); if (!result.modifiedCount) { throw new Error('error-contact-not-found'); diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index 1e5a2a90575c..5c686c0e532e 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -6,6 +6,7 @@ import { check } from 'meteor/check'; import { API } from '../../../app/api/server/api'; import { hasPermissionAsync } from '../../../app/authorization/server/functions/hasPermission'; import { notifyOnSettingChangedById } from '../../../app/lib/server/lib/notifyListener'; +import { settings } from '../../../app/settings/server'; import { updateAuditedByUser } from '../../../server/settings/lib/auditedSettingUpdates'; API.v1.addRoute( @@ -16,9 +17,27 @@ API.v1.addRoute( const unrestrictedAccess = await hasPermissionAsync(this.userId, 'view-privileged-setting'); const loadCurrentValues = unrestrictedAccess && Boolean(this.queryParams.loadValues); - const license = await License.getInfo({ limits: unrestrictedAccess, license: unrestrictedAccess, currentValues: loadCurrentValues }); + const license = await License.getInfo({ + limits: unrestrictedAccess, + license: unrestrictedAccess, + currentValues: loadCurrentValues, + }); + + try { + // TODO: Remove this logic after setting type object is implemented. + const cloudSyncAnnouncement = JSON.parse(settings.get('Cloud_Sync_Announcement_Payload') ?? null); + const canManageCloud = await hasPermissionAsync(this.userId, 'manage-cloud'); + return API.v1.success({ + license, + ...(canManageCloud && cloudSyncAnnouncement && { cloudSyncAnnouncement }), + }); + } catch (error) { + console.error('Unable to parse Cloud_Sync_Announcement_Payload'); + } - return API.v1.success({ license }); + return API.v1.success({ + license, + }); }, }, ); diff --git a/apps/meteor/ee/server/local-services/instance/service.ts b/apps/meteor/ee/server/local-services/instance/service.ts index a7e921511a62..93d6d0c45e98 100644 --- a/apps/meteor/ee/server/local-services/instance/service.ts +++ b/apps/meteor/ee/server/local-services/instance/service.ts @@ -1,7 +1,7 @@ import os from 'os'; import { License, ServiceClassInternal } from '@rocket.chat/core-services'; -import { InstanceStatus } from '@rocket.chat/instance-status'; +import { InstanceStatus, defaultPingInterval, indexExpire } from '@rocket.chat/instance-status'; import { InstanceStatus as InstanceStatusRaw } from '@rocket.chat/models'; import EJSON from 'ejson'; import type { BrokerNode } from 'moleculer'; @@ -33,8 +33,6 @@ export class InstanceService extends ServiceClassInternal implements IInstanceSe private transporter: Transporters.TCP | Transporters.NATS; - private isTransporterTCP = true; - private broker: ServiceBroker; private troubleshootDisableInstanceBroadcast = false; @@ -42,28 +40,6 @@ export class InstanceService extends ServiceClassInternal implements IInstanceSe constructor() { super(); - const tx = getTransporter({ transporter: process.env.TRANSPORTER, port: process.env.TCP_PORT, extra: process.env.TRANSPORTER_EXTRA }); - if (typeof tx === 'string') { - this.transporter = new Transporters.NATS({ url: tx }); - this.isTransporterTCP = false; - } else { - this.transporter = new Transporters.TCP(tx); - } - - if (this.isTransporterTCP) { - this.onEvent('watch.instanceStatus', async ({ clientAction, data }): Promise => { - if (clientAction === 'removed') { - (this.broker.transit?.tx as any).nodes.disconnected(data?._id, false); - (this.broker.transit?.tx as any).nodes.nodes.delete(data?._id); - return; - } - - if (clientAction === 'inserted' && data?.extraInformation?.tcpPort) { - this.connectNode(data); - } - }); - } - this.onEvent('license.module', async ({ module, valid }) => { if (module === 'scalability' && valid) { await this.startBroadcast(); @@ -93,17 +69,28 @@ export class InstanceService extends ServiceClassInternal implements IInstanceSe } async created() { + const transporter = getTransporter({ + transporter: process.env.TRANSPORTER, + port: process.env.TCP_PORT, + extra: process.env.TRANSPORTER_EXTRA, + }); + + const activeInstances = InstanceStatusRaw.getActiveInstancesAddress(); + + this.transporter = + typeof transporter !== 'string' + ? new Transporters.TCP({ ...transporter, urls: activeInstances }) + : new Transporters.NATS({ url: transporter }); + this.broker = new ServiceBroker({ nodeID: InstanceStatus.id(), transporter: this.transporter, serializer: new EJSONSerializer(), + heartbeatInterval: defaultPingInterval, + heartbeatTimeout: indexExpire, ...getLogger(process.env), }); - if ((this.broker.transit?.tx as any)?.nodes?.localNode) { - (this.broker.transit?.tx as any).nodes.localNode.ipList = [hostIP]; - } - this.broker.createService({ name: 'matrix', events: { @@ -176,31 +163,6 @@ export class InstanceService extends ServiceClassInternal implements IInstanceSe this.broadcastStarted = true; StreamerCentral.on('broadcast', this.sendBroadcast.bind(this)); - - if (this.isTransporterTCP) { - await InstanceStatusRaw.find( - { - 'extraInformation.tcpPort': { - $exists: true, - }, - }, - { - sort: { - _createdAt: -1, - }, - }, - ).forEach(this.connectNode.bind(this)); - } - } - - private connectNode(record: any) { - if (record._id === InstanceStatus.id()) { - return; - } - - const { host, tcpPort } = record.extraInformation; - - (this.broker?.transit?.tx as any).addOfflineNode(record._id, host, tcpPort); } private sendBroadcast(streamName: string, eventName: string, args: unknown[]) { diff --git a/apps/meteor/ee/server/local-services/voip-freeswitch/service.ts b/apps/meteor/ee/server/local-services/voip-freeswitch/service.ts index 8bc0f7c9b4fe..41010f41ad55 100644 --- a/apps/meteor/ee/server/local-services/voip-freeswitch/service.ts +++ b/apps/meteor/ee/server/local-services/voip-freeswitch/service.ts @@ -1,27 +1,78 @@ -import { type IVoipFreeSwitchService, ServiceClassInternal } from '@rocket.chat/core-services'; -import type { FreeSwitchExtension, ISetting, SettingValue } from '@rocket.chat/core-typings'; -import { getDomain, getUserPassword, getExtensionList, getExtensionDetails } from '@rocket.chat/freeswitch'; +import { type IVoipFreeSwitchService, ServiceClassInternal, ServiceStarter } from '@rocket.chat/core-services'; +import type { + DeepPartial, + IFreeSwitchEventCall, + IFreeSwitchEventCaller, + IFreeSwitchEvent, + FreeSwitchExtension, + IFreeSwitchCall, + IFreeSwitchCallEventType, + IFreeSwitchCallEvent, + AtLeast, +} from '@rocket.chat/core-typings'; +import { isKnownFreeSwitchEventType } from '@rocket.chat/core-typings'; +import { getDomain, getUserPassword, getExtensionList, getExtensionDetails, listenToEvents } from '@rocket.chat/freeswitch'; +import type { InsertionModel } from '@rocket.chat/model-typings'; +import { FreeSwitchCall, FreeSwitchEvent, Users } from '@rocket.chat/models'; +import { objectMap, wrapExceptions } from '@rocket.chat/tools'; +import type { WithoutId } from 'mongodb'; +import { MongoError } from 'mongodb'; + +import { settings } from '../../../../app/settings/server'; export class VoipFreeSwitchService extends ServiceClassInternal implements IVoipFreeSwitchService { protected name = 'voip-freeswitch'; - constructor(private getSetting: (id: ISetting['_id']) => T) { + private serviceStarter: ServiceStarter; + + constructor() { super(); + + this.serviceStarter = new ServiceStarter(() => Promise.resolve(this.startEvents())); + this.onEvent('watch.settings', async ({ setting }): Promise => { + if (setting._id === 'VoIP_TeamCollab_Enabled' && setting.value === true) { + void this.serviceStarter.start(); + } + }); + } + + private listening = false; + + public async started(): Promise { + void this.serviceStarter.start(); + } + + private startEvents(): void { + if (this.listening) { + return; + } + + try { + // #ToDo: Reconnection + // #ToDo: Only connect from one rocket.chat instance + void listenToEvents( + async (...args) => wrapExceptions(() => this.onFreeSwitchEvent(...args)).suppress(), + this.getConnectionSettings(), + ); + this.listening = true; + } catch (_e) { + this.listening = false; + } } private getConnectionSettings(): { host: string; port: number; password: string; timeout: number } { - if (!this.getSetting('VoIP_TeamCollab_Enabled') && !process.env.FREESWITCHIP) { + if (!settings.get('VoIP_TeamCollab_Enabled') && !process.env.FREESWITCHIP) { throw new Error('VoIP is disabled.'); } - const host = process.env.FREESWITCHIP || this.getSetting('VoIP_TeamCollab_FreeSwitch_Host'); + const host = process.env.FREESWITCHIP || settings.get('VoIP_TeamCollab_FreeSwitch_Host'); if (!host) { throw new Error('VoIP is not properly configured.'); } - const port = this.getSetting('VoIP_TeamCollab_FreeSwitch_Port') || 8021; - const timeout = this.getSetting('VoIP_TeamCollab_FreeSwitch_Timeout') || 3000; - const password = this.getSetting('VoIP_TeamCollab_FreeSwitch_Password'); + const port = settings.get('VoIP_TeamCollab_FreeSwitch_Port') || 8021; + const timeout = settings.get('VoIP_TeamCollab_FreeSwitch_Timeout') || 3000; + const password = settings.get('VoIP_TeamCollab_FreeSwitch_Password'); return { host, @@ -31,6 +82,494 @@ export class VoipFreeSwitchService extends ServiceClassInternal implements IVoip }; } + private async onFreeSwitchEvent(eventName: string, data: Record): Promise { + const uniqueId = data['Unique-ID']; + if (!uniqueId) { + return; + } + + // Using a set to avoid duplicates + const callIds = new Set( + [data['Channel-Call-UUID'], data.variable_call_uuid].filter((callId) => Boolean(callId) && callId !== '0') as string[], + ); + const event = await this.parseEventData(eventName, data); + + // If for some reason the event had different callIds, save a copy of it for each of them + if (callIds.size > 1) { + await Promise.all( + callIds.values().map((callId) => + this.registerEvent({ + ...event, + call: { + ...event.call, + UUID: callId, + }, + }), + ), + ); + return; + } + + await this.registerEvent(event); + } + + private getDetailedEventName(eventName: string, eventData: Record): string { + if (eventName === 'CHANNEL_STATE') { + return `CHANNEL_STATE=${eventData['Channel-State']}`; + } + + if (eventName === 'CHANNEL_CALLSTATE') { + return `CHANNEL_CALLSTATE=${eventData['Channel-Call-State']}`; + } + + return eventName; + } + + private filterOutMissingData>(data: T): DeepPartial { + return objectMap( + data, + ({ key, value }) => { + if (!value || value === '0') { + return; + } + + if (typeof value === 'object' && !Object.keys(value).length) { + return; + } + + return { key, value }; + }, + true, + ) as DeepPartial; + } + + private async parseEventData( + eventName: string, + eventData: Record, + ): Promise>> { + const filteredData: Record = Object.fromEntries( + Object.entries(eventData).filter(([_, value]) => value !== undefined), + ) as Record; + + const detaildEventName = this.getDetailedEventName(eventName, filteredData); + const state = eventData['Channel-State']; + const sequence = eventData['Event-Sequence']; + const previousCallState = eventData['Original-Channel-Call-State']; + const callState = eventData['Channel-Call-State']; + const answerState = eventData['Answer-State']; + const hangupCause = eventData['Hangup-Cause']; + const direction = eventData['Call-Direction']; + const channelName = eventData['Channel-Name']; + + const otherLegUniqueId = eventData['Other-Leg-Unique-ID']; + const loopbackLegUniqueId = eventData.variable_other_loopback_leg_uuid; + const loopbackFromUniqueId = eventData.variable_other_loopback_from_uuid; + const oldUniqueId = eventData['Old-Unique-ID']; + + const channelUniqueId = eventData['Unique-ID']; + const referencedIds = [otherLegUniqueId, loopbackLegUniqueId, loopbackFromUniqueId, oldUniqueId].filter((id) => + Boolean(id), + ) as string[]; + const timestamp = eventData['Event-Date-Timestamp']; + const firedAt = this.parseTimestamp(eventData['Event-Date-Timestamp']); + + const durationStr = eventData.variable_duration; + const duration = (durationStr && parseInt(durationStr)) || 0; + + const call: Partial = { + UUID: (eventData['Channel-Call-UUID'] !== '0' && eventData['Channel-Call-UUID']) || eventData.variable_call_uuid, + answerState, + state: callState, + previousState: previousCallState, + presenceId: eventData['Channel-Presence-ID'], + sipId: eventData.variable_sip_call_id, + authorized: eventData.variable_sip_authorized, + hangupCause, + duration, + + from: { + user: eventData.variable_sip_from_user, + stripped: eventData.variable_sip_from_user_stripped, + port: eventData.variable_sip_from_port, + uri: eventData.variable_sip_from_uri, + host: eventData.variable_sip_from_host, + full: eventData.variable_sip_full_from, + }, + + req: { + user: eventData.variable_sip_req_user, + port: eventData.variable_sip_req_port, + uri: eventData.variable_sip_req_uri, + host: eventData.variable_sip_req_host, + }, + + to: { + user: eventData.variable_sip_to_user, + port: eventData.variable_sip_to_port, + uri: eventData.variable_sip_to_uri, + full: eventData.variable_sip_full_to, + dialedExtension: eventData.variable_dialed_extension, + dialedUser: eventData.variable_dialed_user, + }, + + contact: { + user: eventData.variable_sip_contact_user, + uri: eventData.variable_sip_contact_uri, + host: eventData.variable_sip_contact_host, + }, + + via: { + full: eventData.variable_sip_full_via, + host: eventData.variable_sip_via_host, + rport: eventData.variable_sip_via_rport, + }, + }; + + const caller: Partial = { + uniqueId: eventData['Caller-Unique-ID'], + direction: eventData['Caller-Direction'], + username: eventData['Caller-Username'], + networkAddr: eventData['Caller-Network-Addr'], + ani: eventData['Caller-ANI'], + destinationNumber: eventData['Caller-Destination-Number'], + source: eventData['Caller-Source'], + context: eventData['Caller-Context'], + name: eventData['Caller-Caller-ID-Name'], + number: eventData['Caller-Caller-ID-Number'], + originalCaller: { + name: eventData['Caller-Orig-Caller-ID-Name'], + number: eventData['Caller-Orig-Caller-ID-Number'], + }, + privacy: { + hideName: eventData['Caller-Privacy-Hide-Name'], + hideNumber: eventData['Caller-Privacy-Hide-Number'], + }, + channel: { + name: eventData['Caller-Channel-Name'], + createdTime: eventData['Caller-Channel-Created-Time'], + }, + }; + + return this.filterOutMissingData({ + channelUniqueId, + eventName, + detaildEventName, + sequence, + state, + previousCallState, + callState, + timestamp, + firedAt, + answerState, + hangupCause, + referencedIds, + receivedAt: new Date(), + channelName, + direction, + caller, + call, + eventData: filteredData, + }) as InsertionModel>; + } + + private parseTimestamp(timestamp: string | undefined): Date | undefined { + if (!timestamp || timestamp === '0') { + return undefined; + } + + const value = parseInt(timestamp); + if (Number.isNaN(value)) { + return undefined; + } + + const timeValue = Math.floor(value / 1000); + return new Date(timeValue); + } + + private async registerEvent(event: InsertionModel>): Promise { + try { + await FreeSwitchEvent.registerEvent(event); + if (event.eventName === 'CHANNEL_DESTROY' && event.call?.UUID) { + await this.computeCall(event.call?.UUID); + } + } catch (error) { + // avoid logging that an event was duplicated from mongo + if (error instanceof MongoError && error.code === 11000) { + return; + } + + throw error; + } + } + + private getEventType(event: IFreeSwitchEvent): IFreeSwitchCallEventType { + const { eventName, state, callState } = event; + + const modifiedEventName = eventName.toUpperCase().replace('CHANNEL_', '').replace('_COMPLETE', ''); + + if (isKnownFreeSwitchEventType(modifiedEventName)) { + return modifiedEventName; + } + + if (modifiedEventName === 'STATE') { + if (!state) { + return 'OTHER_STATE'; + } + + const modifiedState = state.toUpperCase().replace('CS_', ''); + if (isKnownFreeSwitchEventType(modifiedState)) { + return modifiedState; + } + } + + if (modifiedEventName === 'CALLSTATE') { + if (!callState) { + return 'OTHER_CALL_STATE'; + } + + const modifiedCallState = callState.toUpperCase().replace('CS_', ''); + if (isKnownFreeSwitchEventType(modifiedCallState)) { + return modifiedCallState; + } + } + + return 'OTHER'; + } + + private identifyCallerFromEvent(event: IFreeSwitchEvent): string { + if (event.call?.from?.user) { + return event.call.from.user; + } + + if (event.caller?.username) { + return event.caller.username; + } + + if (event.caller?.number) { + return event.caller.number; + } + + if (event.caller?.ani) { + return event.caller.ani; + } + + return ''; + } + + private identifyCalleeFromEvent(event: IFreeSwitchEvent): string { + if (event.call?.to?.dialedExtension) { + return event.call.to.dialedExtension; + } + + if (event.call?.to?.dialedUser) { + return event.call.to.dialedUser; + } + + return ''; + } + + private isImportantEvent(event: IFreeSwitchEvent): boolean { + return Object.keys(event).some((key) => key.startsWith('variable_')); + } + + private async computeCall(callUUID: string): Promise { + const allEvents = await FreeSwitchEvent.findAllByCallUUID(callUUID).toArray(); + const call: InsertionModel = { + UUID: callUUID, + channels: [], + events: [], + }; + + // Sort events by both sequence and timestamp, but only when they are present + const sortedEvents = allEvents.sort((event1: IFreeSwitchEvent, event2: IFreeSwitchEvent) => { + if (event1.sequence && event2.sequence) { + return event1.sequence.localeCompare(event2.sequence); + } + + if (event1.firedAt && event2.firedAt) { + return event1.firedAt.valueOf() - event2.firedAt.valueOf(); + } + + if (event1.sequence || event2.sequence) { + return (event1.sequence || '').localeCompare(event2.sequence || ''); + } + + return (event1.firedAt?.valueOf() || 0) - (event2.firedAt?.valueOf() || 0); + }); + + const fromUser = new Set(); + const toUser = new Set(); + let isVoicemailCall = false; + for (const event of sortedEvents) { + if (event.channelUniqueId && !call.channels.includes(event.channelUniqueId)) { + call.channels.push(event.channelUniqueId); + } + + const eventType = this.getEventType(event); + fromUser.add(this.identifyCallerFromEvent(event)); + toUser.add(this.identifyCalleeFromEvent(event)); + + // when a call enters the voicemail, we receive one/or many events with the channelName = loopback/voicemail-x + // where X appears to be a letter + isVoicemailCall = event.channelName?.includes('voicemail') || isVoicemailCall; + + const hasUsefulCallData = this.isImportantEvent(event); + + const callEvent = this.filterOutMissingData({ + type: eventType, + caller: event.caller, + ...(hasUsefulCallData && { call: event.call }), + + otherType: event.eventData['Other-Type'], + otherChannelId: event.eventData['Other-Leg-Unique-ID'], + }) as AtLeast; + + if (call.events[call.events.length - 1]?.type === eventType) { + const previousEvent = call.events.pop() as IFreeSwitchCallEvent; + + call.events.push({ + ...previousEvent, + ...callEvent, + caller: { + ...previousEvent.caller, + ...callEvent.caller, + }, + ...((previousEvent.call || callEvent.call) && { + call: { + ...previousEvent.call, + ...callEvent.call, + from: { + ...previousEvent.call?.from, + ...callEvent.call?.from, + }, + req: { + ...previousEvent.call?.req, + ...callEvent.call?.req, + }, + to: { + ...previousEvent.call?.to, + ...callEvent.call?.to, + }, + contact: { + ...previousEvent.call?.contact, + ...callEvent.call?.contact, + }, + via: { + ...previousEvent.call?.via, + ...callEvent.call?.via, + }, + }, + }), + }); + continue; + } + + call.events.push({ + ...callEvent, + eventName: event.eventName, + sequence: event.sequence, + channelUniqueId: event.channelUniqueId, + timestamp: event.timestamp, + firedAt: event.firedAt, + }); + } + + if (fromUser.size) { + const callerIds = [...fromUser].filter((e) => !!e); + const user = await Users.findOneByFreeSwitchExtensions(callerIds, { + projection: { _id: 1, username: 1, name: 1, avatarETag: 1, freeSwitchExtension: 1 }, + }); + + if (user) { + call.from = { + _id: user._id, + username: user.username, + name: user.name, + avatarETag: user.avatarETag, + freeSwitchExtension: user.freeSwitchExtension, + }; + } + } + + if (toUser.size) { + const calleeIds = [...toUser].filter((e) => !!e); + const user = await Users.findOneByFreeSwitchExtensions(calleeIds, { + projection: { _id: 1, username: 1, name: 1, avatarETag: 1, freeSwitchExtension: 1 }, + }); + if (user) { + call.to = { + _id: user._id, + username: user.username, + name: user.name, + avatarETag: user.avatarETag, + freeSwitchExtension: user.freeSwitchExtension, + }; + } + } + + // A call has 2 channels at max + // If it has 3 or more channels, it's a forwarded call + if (call.channels.length >= 3) { + const originalCalls = await FreeSwitchCall.findAllByChannelUniqueIds(call.channels, { projection: { events: 0 } }).toArray(); + if (originalCalls.length) { + call.forwardedFrom = originalCalls; + } + } + + // Call originated from us but destination and destination is another user = internal + if (call.from && call.to) { + call.direction = 'internal'; + } + + // Call originated from us but destination is not on server = external outbound + if (call.from && !call.to) { + call.direction = 'external_outbound'; + } + + // Call originated from a user outside server but received by a user in our side = external inbound + if (!call.from && call.to) { + call.direction = 'external_inbound'; + } + + // Call ended up in voicemail of another user = voicemail + if (isVoicemailCall) { + call.voicemail = true; + } + + call.duration = this.computeCallDuration(call); + + await FreeSwitchCall.registerCall(call); + } + + private computeCallDuration(call: InsertionModel): number { + if (!call.events.length) { + return 0; + } + + const channelAnswerEvent = call.events.find((e) => e.eventName === 'CHANNEL_ANSWER'); + if (!channelAnswerEvent?.timestamp) { + return 0; + } + + const answer = this.parseTimestamp(channelAnswerEvent.timestamp); + if (!answer) { + return 0; + } + + const channelHangupEvent = call.events.find((e) => e.eventName === 'CHANNEL_HANGUP_COMPLETE'); + if (!channelHangupEvent?.timestamp) { + // We dont have a hangup but we have an answer, assume hangup is === destroy time + return new Date().getTime() - answer.getTime(); + } + + const hangup = this.parseTimestamp(channelHangupEvent.timestamp); + if (!hangup) { + return 0; + } + + return hangup.getTime() - answer.getTime(); + } + async getDomain(): Promise { const options = this.getConnectionSettings(); return getDomain(options); diff --git a/apps/meteor/ee/server/patches/mergeContacts.ts b/apps/meteor/ee/server/patches/mergeContacts.ts index 1f93e1731a82..30d5b03d0cfb 100644 --- a/apps/meteor/ee/server/patches/mergeContacts.ts +++ b/apps/meteor/ee/server/patches/mergeContacts.ts @@ -1,8 +1,9 @@ import type { ILivechatContact, ILivechatContactChannel, ILivechatContactVisitorAssociation } from '@rocket.chat/core-typings'; import { License } from '@rocket.chat/license'; -import { LivechatContacts, LivechatRooms } from '@rocket.chat/models'; +import { LivechatContacts, LivechatRooms, Settings } from '@rocket.chat/models'; import type { ClientSession } from 'mongodb'; +import { notifyOnSettingChanged } from '../../../app/lib/server/lib/notifyListener'; import { isSameChannel } from '../../../app/livechat/lib/isSameChannel'; import { ContactMerger } from '../../../app/livechat/server/lib/contacts/ContactMerger'; import { mergeContacts } from '../../../app/livechat/server/lib/contacts/mergeContacts'; @@ -41,10 +42,16 @@ export const runMergeContacts = async ( const similarContactIds = similarContacts.map((c) => c._id); const { deletedCount } = await LivechatContacts.deleteMany({ _id: { $in: similarContactIds } }, { session }); + + const { value } = await Settings.incrementValueById('Merged_Contacts_Count', similarContacts.length, { returnDocument: 'after' }); + if (value) { + void notifyOnSettingChanged(value); + } logger.info({ - msg: `${deletedCount} contacts have been deleted and merged`, - deletedContactIds: similarContactIds, - contactId, + msg: 'contacts have been deleted and merged with a contact', + similarContactIds, + deletedCount, + originalContactId: originalContact._id, }); logger.debug({ msg: 'Updating rooms with new contact id', contactId }); diff --git a/apps/meteor/ee/server/patches/verifyContactChannel.ts b/apps/meteor/ee/server/patches/verifyContactChannel.ts index f26419d57e18..8e7a2c05cf84 100644 --- a/apps/meteor/ee/server/patches/verifyContactChannel.ts +++ b/apps/meteor/ee/server/patches/verifyContactChannel.ts @@ -29,20 +29,10 @@ async function _verifyContactChannel( session.startTransaction(); logger.debug({ msg: 'Start verifying contact channel', contactId, visitorId, roomId }); - await LivechatContacts.updateContactChannel( - { - visitorId, - source: room.source, - }, - { - verified: true, - verifiedAt: new Date(), - field, - value: value.toLowerCase(), - }, - {}, - { session }, - ); + const updater = LivechatContacts.getUpdater(); + LivechatContacts.setVerifiedUpdateQuery(true, updater); + LivechatContacts.setFieldAndValueUpdateQuery(field, value.toLowerCase(), updater); + await LivechatContacts.updateFromUpdaterByAssociation({ visitorId, source: room.source }, updater, { session }); await LivechatRooms.update({ _id: roomId }, { $set: { verified: true } }, { session }); logger.debug({ msg: 'Merging contacts', contactId, visitorId, roomId }); diff --git a/apps/meteor/ee/server/services/CHANGELOG.md b/apps/meteor/ee/server/services/CHANGELOG.md index 6a0d6bc8afd2..2f19f8b81072 100644 --- a/apps/meteor/ee/server/services/CHANGELOG.md +++ b/apps/meteor/ee/server/services/CHANGELOG.md @@ -1,5 +1,50 @@ # rocketchat-services +## 2.0.1 + +### Patch Changes + +- ([#33596](https://github.com/RocketChat/Rocket.Chat/pull/33596)) Bump meteor to 3.0.4 and Node version to 20.18.0 + +-
Updated dependencies [82767d8fd8a52ac348e8aded1d238e688d36129b, 80e36bfc3938775eb26aa5576f1b9b98896e1cc4, e7edeac3bdd22da0a04b8e873d5a008e249fb4be, 3569b0a9c48f8b94ebaef2f8b607c52fdb8e570a, b4841cb7206d855d7a1bc7604683a5b4a48b7176, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0, 63ccadc012499e004445ad6bc6cd2ff777aecbd1, ce7024af36fcde97b1da5b2731f6edc4a4c236b8, d398866dba725918017e3609807f9d0ab9b89b72, d398866dba725918017e3609807f9d0ab9b89b72]: + + - @rocket.chat/apps-engine@1.48.0 + - @rocket.chat/model-typings@1.1.0 + - @rocket.chat/core-typings@7.1.0 + - @rocket.chat/rest-typings@7.1.0 + - @rocket.chat/core-services@0.7.2 + - @rocket.chat/models@1.0.1 + - @rocket.chat/network-broker@0.1.2 +
+ +## 2.0.1-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 + - @rocket.chat/rest-typings@7.1.0-rc.3 + - @rocket.chat/core-services@0.7.2-rc.3 + - @rocket.chat/model-typings@1.1.0-rc.3 + - @rocket.chat/network-broker@0.1.2-rc.3 + - @rocket.chat/models@1.0.1-rc.3 +
+ +## 2.0.1-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 + - @rocket.chat/rest-typings@7.1.0-rc.2 + - @rocket.chat/core-services@0.7.2-rc.2 + - @rocket.chat/model-typings@1.1.0-rc.2 + - @rocket.chat/network-broker@0.1.2-rc.2 + - @rocket.chat/models@1.0.1-rc.2 +
+ ## 2.0.1-rc.1 ### Patch Changes diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index fb836b217eaf..3e7b581401f4 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -1,7 +1,7 @@ { "name": "rocketchat-services", "private": true, - "version": "2.0.1-rc.1", + "version": "2.0.1", "description": "Rocket.Chat Authorization service", "main": "index.js", "scripts": { diff --git a/apps/meteor/ee/server/settings/contact-verification.ts b/apps/meteor/ee/server/settings/contact-verification.ts index 78d04b7dc1d7..b62b164a7b90 100644 --- a/apps/meteor/ee/server/settings/contact-verification.ts +++ b/apps/meteor/ee/server/settings/contact-verification.ts @@ -4,6 +4,31 @@ export const addSettings = async (): Promise => { const omnichannelEnabledQuery = { _id: 'Livechat_enabled', value: true }; return settingsRegistry.addGroup('Omnichannel', async function () { + await this.add('Merged_Contacts_Count', 0, { + type: 'int', + hidden: true, + }); + + await this.add('Resolved_Conflicts_Count', 0, { + type: 'int', + hidden: true, + }); + + await this.add('Contacts_Importer_Count', 0, { + type: 'int', + hidden: true, + }); + + await this.add('Advanced_Contact_Upsell_Views_Count', 0, { + type: 'int', + hidden: true, + }); + + await this.add('Advanced_Contact_Upsell_Clicks_Count', 0, { + type: 'int', + hidden: true, + }); + return this.with( { enterprise: true, diff --git a/apps/meteor/ee/server/startup/services.ts b/apps/meteor/ee/server/startup/services.ts index c7cd0491bda9..efaf1ab8be68 100644 --- a/apps/meteor/ee/server/startup/services.ts +++ b/apps/meteor/ee/server/startup/services.ts @@ -1,7 +1,6 @@ import { api } from '@rocket.chat/core-services'; import { License } from '@rocket.chat/license'; -import { settings } from '../../../app/settings/server/cached'; import { isRunningMs } from '../../../server/lib/isRunningMs'; import { FederationService } from '../../../server/services/federation/service'; import { LicenseService } from '../../app/license/server/license.internalService'; @@ -19,7 +18,7 @@ api.registerService(new LDAPEEService()); api.registerService(new LicenseService()); api.registerService(new MessageReadsService()); api.registerService(new OmnichannelEE()); -api.registerService(new VoipFreeSwitchService((id) => settings.get(id))); +api.registerService(new VoipFreeSwitchService()); // when not running micro services we want to start up the instance intercom if (!isRunningMs()) { diff --git a/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/mergeContacts.spec.ts b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/mergeContacts.spec.ts index 0fb40fa04bdd..5e4b285c7a3b 100644 --- a/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/mergeContacts.spec.ts +++ b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/mergeContacts.spec.ts @@ -11,6 +11,9 @@ const modelsMock = { LivechatRooms: { updateMergedContactIds: sinon.stub(), }, + Settings: { + incrementValueById: sinon.stub(), + }, }; const contactMergerStub = { @@ -22,6 +25,7 @@ const { runMergeContacts } = proxyquire.noCallThru().load('../../../../../../ser '../../../app/livechat/server/lib/contacts/mergeContacts': { mergeContacts: { patch: sinon.stub() } }, '../../../app/livechat/server/lib/contacts/ContactMerger': { ContactMerger: contactMergerStub }, '../../../app/livechat-enterprise/server/lib/logger': { logger: { info: sinon.stub(), debug: sinon.stub() } }, + '../../../app/lib/server/lib/notifyListener': { notifyOnSettingChanged: sinon.stub() }, '@rocket.chat/models': modelsMock, }); @@ -45,6 +49,7 @@ describe('mergeContacts', () => { modelsMock.LivechatContacts.findSimilarVerifiedContacts.reset(); modelsMock.LivechatContacts.deleteMany.reset(); modelsMock.LivechatRooms.updateMergedContactIds.reset(); + modelsMock.Settings.incrementValueById.reset(); contactMergerStub.getAllFieldsFromContact.reset(); contactMergerStub.mergeFieldsIntoContact.reset(); modelsMock.LivechatContacts.deleteMany.resolves({ deletedCount: 0 }); @@ -102,6 +107,7 @@ describe('mergeContacts', () => { modelsMock.LivechatContacts.findOneById.resolves(originalContact); modelsMock.LivechatContacts.findSimilarVerifiedContacts.resolves([similarContact]); + modelsMock.Settings.incrementValueById.resolves({ value: undefined }); await runMergeContacts(() => undefined, 'contactId', { visitorId: 'visitorId', source: { type: 'sms' } }); @@ -114,5 +120,6 @@ describe('mergeContacts', () => { expect(modelsMock.LivechatContacts.deleteMany.calledOnceWith({ _id: { $in: ['differentId'] } })).to.be.true; expect(modelsMock.LivechatRooms.updateMergedContactIds.calledOnceWith(['differentId'], 'contactId')).to.be.true; + expect(modelsMock.Settings.incrementValueById.calledOnceWith('Merged_Contacts_Count', 1)).to.be.true; }); }); diff --git a/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/verifyContactChannel.spec.ts b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/verifyContactChannel.spec.ts index 7e57f5d1a22a..cdf310b58521 100644 --- a/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/verifyContactChannel.spec.ts +++ b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/verifyContactChannel.spec.ts @@ -4,7 +4,10 @@ import sinon from 'sinon'; const modelsMock = { LivechatContacts: { - updateContactChannel: sinon.stub(), + getUpdater: sinon.stub(), + setVerifiedUpdateQuery: sinon.stub(), + setFieldAndValueUpdateQuery: sinon.stub(), + updateFromUpdaterByAssociation: sinon.stub(), }, LivechatRooms: { update: sinon.stub(), @@ -44,7 +47,10 @@ const { runVerifyContactChannel } = proxyquire.noCallThru().load('../../../../.. describe('verifyContactChannel', () => { beforeEach(() => { - modelsMock.LivechatContacts.updateContactChannel.reset(); + modelsMock.LivechatContacts.getUpdater.reset(); + modelsMock.LivechatContacts.setVerifiedUpdateQuery.reset(); + modelsMock.LivechatContacts.setFieldAndValueUpdateQuery.reset(); + modelsMock.LivechatContacts.updateFromUpdaterByAssociation.reset(); modelsMock.LivechatRooms.update.reset(); modelsMock.LivechatInquiry.findOneByRoomId.reset(); modelsMock.LivechatRooms.findOneById.reset(); @@ -55,6 +61,8 @@ describe('verifyContactChannel', () => { mergeContactsStub.reset(); queueManager.processNewInquiry.reset(); queueManager.verifyInquiry.reset(); + + modelsMock.LivechatContacts.getUpdater.returns({}); }); afterEach(() => { @@ -68,24 +76,23 @@ describe('verifyContactChannel', () => { await runVerifyContactChannel(() => undefined, { contactId: 'contactId', field: 'field', - value: 'value', + value: 'Value', visitorId: 'visitorId', roomId: 'roomId', }); + expect(modelsMock.LivechatContacts.getUpdater.calledOnce).to.be.true; + expect(modelsMock.LivechatContacts.setVerifiedUpdateQuery.calledOnceWith(true, {})).to.be.true; + expect(modelsMock.LivechatContacts.setFieldAndValueUpdateQuery.calledOnceWith('field', 'value', {})).to.be.true; expect( - modelsMock.LivechatContacts.updateContactChannel.calledOnceWith( + modelsMock.LivechatContacts.updateFromUpdaterByAssociation.calledOnceWith( sinon.match({ visitorId: 'visitorId', source: sinon.match({ type: 'sms', }), }), - sinon.match({ - verified: true, - field: 'field', - value: 'value', - }), + {}, ), ).to.be.true; expect(modelsMock.LivechatRooms.update.calledOnceWith({ _id: 'roomId' }, { $set: { verified: true } })).to.be.true; @@ -116,21 +123,21 @@ describe('verifyContactChannel', () => { roomId: 'roomId', }); + expect(modelsMock.LivechatContacts.getUpdater.calledOnce).to.be.true; + expect(modelsMock.LivechatContacts.setVerifiedUpdateQuery.calledOnceWith(true, {})).to.be.true; + expect(modelsMock.LivechatContacts.setFieldAndValueUpdateQuery.calledOnceWith('field', 'value', {})).to.be.true; expect( - modelsMock.LivechatContacts.updateContactChannel.calledOnceWith( + modelsMock.LivechatContacts.updateFromUpdaterByAssociation.calledOnceWith( sinon.match({ visitorId: 'visitorId', source: sinon.match({ type: 'sms', }), }), - sinon.match({ - verified: true, - field: 'field', - value: 'value', - }), + {}, ), ).to.be.true; + expect(modelsMock.LivechatRooms.update.calledOnceWith({ _id: 'roomId' }, { $set: { verified: true } })).to.be.true; expect( mergeContactsStub.calledOnceWith( @@ -160,7 +167,11 @@ describe('verifyContactChannel', () => { }), ).to.be.rejectedWith('error-invalid-room'); - expect(modelsMock.LivechatContacts.updateContactChannel.notCalled).to.be.true; + expect(modelsMock.LivechatContacts.getUpdater.notCalled).to.be.true; + expect(modelsMock.LivechatContacts.setVerifiedUpdateQuery.notCalled).to.be.true; + expect(modelsMock.LivechatContacts.setFieldAndValueUpdateQuery.notCalled).to.be.true; + expect(modelsMock.LivechatContacts.updateFromUpdaterByAssociation.notCalled).to.be.true; + expect(modelsMock.LivechatRooms.update.notCalled).to.be.true; expect(mergeContactsStub.notCalled).to.be.true; expect(queueManager.verifyInquiry.notCalled).to.be.true; @@ -180,21 +191,21 @@ describe('verifyContactChannel', () => { }), ).to.be.rejectedWith('error-invalid-inquiry'); + expect(modelsMock.LivechatContacts.getUpdater.calledOnce).to.be.true; + expect(modelsMock.LivechatContacts.setVerifiedUpdateQuery.calledOnceWith(true, {})).to.be.true; + expect(modelsMock.LivechatContacts.setFieldAndValueUpdateQuery.calledOnceWith('field', 'value', {})).to.be.true; expect( - modelsMock.LivechatContacts.updateContactChannel.calledOnceWith( + modelsMock.LivechatContacts.updateFromUpdaterByAssociation.calledOnceWith( sinon.match({ visitorId: 'visitorId', source: sinon.match({ type: 'sms', }), }), - sinon.match({ - verified: true, - field: 'field', - value: 'value', - }), + {}, ), ).to.be.true; + expect(modelsMock.LivechatRooms.update.calledOnceWith({ _id: 'roomId' }, { $set: { verified: true } })).to.be.true; expect( mergeContactsStub.calledOnceWith( diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index ca82683a04ef..901c8101e034 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -183,6 +183,7 @@ type ChainedCallbackSignatures = { 'renderMessage': (message: T) => T; 'oembed:beforeGetUrlContent': (data: { urlObj: URL }) => { urlObj: URL; + headerOverrides?: { [k: string]: string }; }; 'oembed:afterParseContent': (data: { url: string; meta: OEmbedMeta; headers: { [k: string]: string }; content: OEmbedUrlContent }) => { url: string; diff --git a/apps/meteor/lib/publishFields.ts b/apps/meteor/lib/publishFields.ts index c4965bb1c6b0..5f5170aed3a5 100644 --- a/apps/meteor/lib/publishFields.ts +++ b/apps/meteor/lib/publishFields.ts @@ -14,6 +14,7 @@ export const subscriptionFields = { roles: 1, unread: 1, prid: 1, + customFields: 1, userMentions: 1, groupMentions: 1, archived: 1, diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 5da653e2fa75..6120cc43b351 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -247,8 +247,8 @@ "@rocket.chat/emitter": "~0.31.25", "@rocket.chat/favicon": "workspace:^", "@rocket.chat/freeswitch": "workspace:^", - "@rocket.chat/fuselage": "^0.59.4", - "@rocket.chat/fuselage-hooks": "^0.33.1", + "@rocket.chat/fuselage": "^0.60.0", + "@rocket.chat/fuselage-hooks": "^0.34.0", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/fuselage-toastbar": "^0.33.0", "@rocket.chat/fuselage-tokens": "^0.33.2", @@ -262,12 +262,12 @@ "@rocket.chat/license": "workspace:^", "@rocket.chat/log-format": "workspace:^", "@rocket.chat/logger": "workspace:^", - "@rocket.chat/logo": "^0.31.30", + "@rocket.chat/logo": "^0.31.31", "@rocket.chat/memo": "~0.31.25", "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", - "@rocket.chat/mp3-encoder": "0.24.0", + "@rocket.chat/mp3-encoder": "^0.31.26", "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/omnichannel-services": "workspace:^", "@rocket.chat/onboarding-ui": "~0.34.0", diff --git a/apps/meteor/server/database/watchCollections.ts b/apps/meteor/server/database/watchCollections.ts index 6dd173d5d323..b7bdf13c28b8 100644 --- a/apps/meteor/server/database/watchCollections.ts +++ b/apps/meteor/server/database/watchCollections.ts @@ -29,10 +29,11 @@ const onlyCollections = DBWATCHER_ONLY_COLLECTIONS.split(',') .filter(Boolean); export function getWatchCollections(): string[] { - const collections = [InstanceStatus.getCollectionName()]; + const collections = []; // add back to the list of collections in case db watchers are enabled if (!dbWatchersDisabled) { + collections.push(InstanceStatus.getCollectionName()); collections.push(Users.getCollectionName()); collections.push(Messages.getCollectionName()); collections.push(LivechatInquiry.getCollectionName()); diff --git a/apps/meteor/server/lib/videoConfTypes.ts b/apps/meteor/server/lib/videoConfTypes.ts index d899539c3ba7..7529115745d5 100644 --- a/apps/meteor/server/lib/videoConfTypes.ts +++ b/apps/meteor/server/lib/videoConfTypes.ts @@ -1,4 +1,11 @@ -import type { AtLeast, IRoom, VideoConferenceCreateData, VideoConferenceType } from '@rocket.chat/core-typings'; +import type { + AtLeast, + ExternalVideoConference, + IRoom, + VideoConference, + VideoConferenceCreateData, + VideoConferenceType, +} from '@rocket.chat/core-typings'; type RoomRequiredFields = AtLeast; type VideoConferenceTypeCondition = (room: RoomRequiredFields, allowRinging: boolean) => Promise; @@ -34,6 +41,11 @@ export const videoConfTypes = { return { type: 'videoconference' }; }, + + isCallManagedByApp(call: VideoConference): call is ExternalVideoConference { + return call.type !== 'voip'; + }, }; +videoConfTypes.registerVideoConferenceType('voip', async () => false); videoConfTypes.registerVideoConferenceType({ type: 'livechat' }, async ({ t }) => t === 'l'); diff --git a/apps/meteor/server/models/FreeSwitchCall.ts b/apps/meteor/server/models/FreeSwitchCall.ts new file mode 100644 index 000000000000..97b470538e80 --- /dev/null +++ b/apps/meteor/server/models/FreeSwitchCall.ts @@ -0,0 +1,6 @@ +import { registerModel } from '@rocket.chat/models'; + +import { db } from '../database/utils'; +import { FreeSwitchCallRaw } from './raw/FreeSwitchCall'; + +registerModel('IFreeSwitchCallModel', new FreeSwitchCallRaw(db)); diff --git a/apps/meteor/server/models/FreeSwitchEvent.ts b/apps/meteor/server/models/FreeSwitchEvent.ts new file mode 100644 index 000000000000..cab4d7daa2e4 --- /dev/null +++ b/apps/meteor/server/models/FreeSwitchEvent.ts @@ -0,0 +1,6 @@ +import { registerModel } from '@rocket.chat/models'; + +import { db } from '../database/utils'; +import { FreeSwitchEventRaw } from './raw/FreeSwitchEvent'; + +registerModel('IFreeSwitchEventModel', new FreeSwitchEventRaw(db)); diff --git a/apps/meteor/server/models/raw/Avatars.ts b/apps/meteor/server/models/raw/Avatars.ts index c4935c18d6be..40fb14eab0a0 100644 --- a/apps/meteor/server/models/raw/Avatars.ts +++ b/apps/meteor/server/models/raw/Avatars.ts @@ -1,6 +1,6 @@ import type { IAvatar, RocketChatRecordDeleted, IUser } from '@rocket.chat/core-typings'; import type { IAvatarsModel } from '@rocket.chat/model-typings'; -import type { Collection, Db, FindOptions } from 'mongodb'; +import type { Collection, Db, IndexDescription, FindOptions } from 'mongodb'; import { BaseUploadModelRaw } from './BaseUploadModel'; @@ -9,6 +9,10 @@ export class AvatarsRaw extends BaseUploadModelRaw implements IAvatarsModel { super(db, 'avatars', trash); } + protected modelIndexes(): IndexDescription[] { + return [...super.modelIndexes(), { key: { userId: 1 }, sparse: true }]; + } + findOneByUserId(userId: IUser['_id'], options?: FindOptions) { return this.findOne({ userId }, options); } diff --git a/apps/meteor/server/models/raw/BaseRaw.ts b/apps/meteor/server/models/raw/BaseRaw.ts index e1caf93cf3a2..022576e8f490 100644 --- a/apps/meteor/server/models/raw/BaseRaw.ts +++ b/apps/meteor/server/models/raw/BaseRaw.ts @@ -124,9 +124,9 @@ export abstract class BaseRaw< return new UpdaterImpl(); } - public updateFromUpdater(query: Filter, updater: Updater): Promise { + public updateFromUpdater(query: Filter, updater: Updater, options: UpdateOptions = {}): Promise { const updateFilter = updater.getUpdateFilter(); - return this.updateOne(query, updateFilter).catch((e) => { + return this.updateOne(query, updateFilter, options).catch((e) => { console.warn(e, updateFilter); return Promise.reject(e); }); diff --git a/apps/meteor/server/models/raw/BaseUploadModel.ts b/apps/meteor/server/models/raw/BaseUploadModel.ts index 6d2760a19f78..4037566272b9 100644 --- a/apps/meteor/server/models/raw/BaseUploadModel.ts +++ b/apps/meteor/server/models/raw/BaseUploadModel.ts @@ -19,7 +19,6 @@ type T = IUpload; export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUploadsModel { protected modelIndexes(): IndexDescription[] { return [ - { key: { userId: 1 }, sparse: true }, { key: { name: 1 }, sparse: true }, { key: { rid: 1 }, sparse: true }, { key: { expiresAt: 1 }, sparse: true }, diff --git a/apps/meteor/server/models/raw/FreeSwitchCall.ts b/apps/meteor/server/models/raw/FreeSwitchCall.ts new file mode 100644 index 000000000000..2be10e0d29d3 --- /dev/null +++ b/apps/meteor/server/models/raw/FreeSwitchCall.ts @@ -0,0 +1,28 @@ +import type { IFreeSwitchCall, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; +import type { IFreeSwitchCallModel, InsertionModel } from '@rocket.chat/model-typings'; +import type { Collection, Db, FindCursor, FindOptions, IndexDescription, WithoutId } from 'mongodb'; + +import { BaseRaw } from './BaseRaw'; + +export class FreeSwitchCallRaw extends BaseRaw implements IFreeSwitchCallModel { + constructor(db: Db, trash?: Collection>) { + super(db, 'freeswitch_calls', trash); + } + + protected modelIndexes(): IndexDescription[] { + return [{ key: { UUID: 1 } }, { key: { channels: 1 } }]; + } + + public async registerCall(call: WithoutId>): Promise { + await this.findOneAndUpdate({ UUID: call.UUID }, { $set: call }, { upsert: true }); + } + + public findAllByChannelUniqueIds(uniqueIds: string[], options?: FindOptions): FindCursor { + return this.find( + { + channels: { $in: uniqueIds }, + }, + options, + ); + } +} diff --git a/apps/meteor/server/models/raw/FreeSwitchEvent.ts b/apps/meteor/server/models/raw/FreeSwitchEvent.ts new file mode 100644 index 000000000000..236f891cee0d --- /dev/null +++ b/apps/meteor/server/models/raw/FreeSwitchEvent.ts @@ -0,0 +1,40 @@ +import type { IFreeSwitchEvent, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; +import type { IFreeSwitchEventModel, InsertionModel } from '@rocket.chat/model-typings'; +import type { IndexDescription, Collection, Db, FindOptions, FindCursor, WithoutId, InsertOneResult } from 'mongodb'; + +import { BaseRaw } from './BaseRaw'; + +export class FreeSwitchEventRaw extends BaseRaw implements IFreeSwitchEventModel { + constructor(db: Db, trash?: Collection>) { + super(db, 'freeswitch_events', trash); + } + + protected modelIndexes(): IndexDescription[] { + return [ + { key: { channelUniqueId: 1, sequence: 1 }, unique: true }, + { key: { 'call.UUID': 1 } }, + // Allow 15 days of events to be saved + { key: { _updatedAt: 1 }, expireAfterSeconds: 30 * 24 * 60 * 15 }, + ]; + } + + public async registerEvent(event: WithoutId>): Promise> { + return this.insertOne(event); + } + + public findAllByCallUUID(callUUID: string, options?: FindOptions): FindCursor { + return this.find({ 'call.UUID': callUUID }, options); + } + + public findAllByChannelUniqueIds( + uniqueIds: string[], + options?: FindOptions, + ): FindCursor { + return this.find( + { + channelUniqueId: { $in: uniqueIds }, + }, + options, + ); + } +} diff --git a/apps/meteor/server/models/raw/InstanceStatus.ts b/apps/meteor/server/models/raw/InstanceStatus.ts index 039d8fa18afc..86aed7fbc128 100644 --- a/apps/meteor/server/models/raw/InstanceStatus.ts +++ b/apps/meteor/server/models/raw/InstanceStatus.ts @@ -1,6 +1,6 @@ import type { IInstanceStatus } from '@rocket.chat/core-typings'; import type { IInstanceStatusModel } from '@rocket.chat/model-typings'; -import type { Db } from 'mongodb'; +import type { Db, ModifyResult, UpdateResult, DeleteResult } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -17,4 +17,49 @@ export class InstanceStatusRaw extends BaseRaw implements IInst async getActiveInstanceCount(): Promise { return this.col.countDocuments({ _updatedAt: { $gt: new Date(Date.now() - process.uptime() * 1000 - 2000) } }); } + + async getActiveInstancesAddress(): Promise { + const instances = await this.find({}, { projection: { _id: 1, extraInformation: { host: 1, tcpPort: 1 } } }).toArray(); + return instances.map((instance) => `${instance.extraInformation.host}:${instance.extraInformation.tcpPort}/${instance._id}`); + } + + async removeInstanceById(_id: IInstanceStatus['_id']): Promise { + return this.deleteOne({ _id }); + } + + async setDocumentHeartbeat(documentId: string): Promise { + return this.updateOne({ _id: documentId }, { $currentDate: { _updatedAt: true } }); + } + + async upsertInstance(instance: Partial): Promise> { + return this.findOneAndUpdate( + { + _id: instance._id, + }, + { + $set: instance, + $currentDate: { + _createdAt: true, + _updatedAt: true, + }, + }, + { + upsert: true, + returnDocument: 'after', + }, + ); + } + + async updateConnections(_id: IInstanceStatus['_id'], conns: number) { + return this.updateOne( + { + _id, + }, + { + $set: { + 'extraInformation.conns': conns, + }, + }, + ); + } } diff --git a/apps/meteor/server/models/raw/LivechatContacts.ts b/apps/meteor/server/models/raw/LivechatContacts.ts index 4e80f23956e6..43a5f5204e60 100644 --- a/apps/meteor/server/models/raw/LivechatContacts.ts +++ b/apps/meteor/server/models/raw/LivechatContacts.ts @@ -6,7 +6,7 @@ import type { ILivechatVisitor, RocketChatRecordDeleted, } from '@rocket.chat/core-typings'; -import type { FindPaginated, ILivechatContactsModel, InsertionModel } from '@rocket.chat/model-typings'; +import type { FindPaginated, ILivechatContactsModel, InsertionModel, Updater } from '@rocket.chat/model-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import type { Document, @@ -21,9 +21,11 @@ import type { UpdateFilter, UpdateOptions, FindOneAndUpdateOptions, + AggregationCursor, } from 'mongodb'; import { BaseRaw } from './BaseRaw'; +import { readSecondaryPreferred } from '../../database/readSecondaryPreferred'; export class LivechatContactsRaw extends BaseRaw implements ILivechatContactsModel { constructor(db: Db, trash?: Collection>) { @@ -76,6 +78,22 @@ export class LivechatContactsRaw extends BaseRaw implements IL sparse: true, unique: false, }, + { + key: { channels: 1 }, + unique: false, + }, + { + key: { 'channels.blocked': 1 }, + sparse: true, + }, + { + key: { 'channels.verified': 1 }, + sparse: true, + }, + { + key: { unknown: 1 }, + unique: false, + }, ]; } @@ -198,24 +216,37 @@ export class LivechatContactsRaw extends BaseRaw implements IL return Boolean(await this.findOne(this.makeQueryForVisitor(visitor, { blocked: true }), { projection: { _id: 1 } })); } - async updateContactChannel( + setChannelBlockStatus(visitor: ILivechatContactVisitorAssociation, blocked: boolean): Promise { + return this.updateOne(this.makeQueryForVisitor(visitor), { $set: { 'channels.$.blocked': blocked } }); + } + + setChannelVerifiedStatus(visitor: ILivechatContactVisitorAssociation, verified: boolean): Promise { + return this.updateOne(this.makeQueryForVisitor(visitor), { + $set: { + 'channels.$.verified': verified, + ...(verified && { 'channels.$.verifiedAt': new Date() }), + }, + }); + } + + setVerifiedUpdateQuery(verified: boolean, contactUpdater: Updater): Updater { + if (verified) { + contactUpdater.set('channels.$.verifiedAt', new Date()); + } + return contactUpdater.set('channels.$.verified', verified); + } + + setFieldAndValueUpdateQuery(field: string, value: string, contactUpdater: Updater): Updater { + contactUpdater.set('channels.$.field', field); + return contactUpdater.set('channels.$.value', value); + } + + updateFromUpdaterByAssociation( visitor: ILivechatContactVisitorAssociation, - data: Partial, - contactData?: Partial>, + contactUpdater: Updater, options: UpdateOptions = {}, ): Promise { - return this.updateOne( - this.makeQueryForVisitor(visitor), - { - $set: { - ...contactData, - ...(Object.fromEntries( - Object.keys(data).map((key) => [`channels.$.${key}`, data[key as keyof ILivechatContactChannel]]), - ) as UpdateFilter['$set']), - }, - }, - options, - ); + return this.updateFromUpdater(this.makeQueryForVisitor(visitor), contactUpdater, options); } async findSimilarVerifiedContacts( @@ -249,4 +280,59 @@ export class LivechatContactsRaw extends BaseRaw implements IL return updatedContact.value; } + + countByContactInfo({ contactId, email, phone }: { contactId?: string; email?: string; phone?: string }): Promise { + const filter = { + ...(email && { 'emails.address': email }), + ...(phone && { 'phones.phoneNumber': phone }), + ...(contactId && { _id: contactId }), + }; + + return this.countDocuments(filter); + } + + countUnknown(): Promise { + return this.countDocuments({ unknown: true }, { readPreference: readSecondaryPreferred() }); + } + + countBlocked(): Promise { + return this.countDocuments({ 'channels.blocked': true }, { readPreference: readSecondaryPreferred() }); + } + + countFullyBlocked(): Promise { + return this.countDocuments( + { + 'channels.blocked': true, + 'channels': { $not: { $elemMatch: { $or: [{ blocked: false }, { blocked: { $exists: false } }] } } }, + }, + { readPreference: readSecondaryPreferred() }, + ); + } + + countVerified(): Promise { + return this.countDocuments({ 'channels.verified': true }, { readPreference: readSecondaryPreferred() }); + } + + countContactsWithoutChannels(): Promise { + return this.countDocuments({ channels: { $size: 0 } }, { readPreference: readSecondaryPreferred() }); + } + + getStatistics(): AggregationCursor<{ totalConflicts: number; avgChannelsPerContact: number }> { + return this.col.aggregate<{ totalConflicts: number; avgChannelsPerContact: number }>( + [ + { + $group: { + _id: null, + totalConflicts: { + $sum: { $size: { $cond: [{ $isArray: '$conflictingFields' }, '$conflictingFields', []] } }, + }, + avgChannelsPerContact: { + $avg: { $size: { $cond: [{ $isArray: '$channels' }, '$channels', []] } }, + }, + }, + }, + ], + { allowDiskUse: true, readPreference: readSecondaryPreferred() }, + ); + } } diff --git a/apps/meteor/server/models/raw/LivechatRooms.ts b/apps/meteor/server/models/raw/LivechatRooms.ts index e422615fecbd..de18a8ec3d22 100644 --- a/apps/meteor/server/models/raw/LivechatRooms.ts +++ b/apps/meteor/server/models/raw/LivechatRooms.ts @@ -2147,7 +2147,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive }; return this.find(query, { - projection: { ts: 1, departmentId: 1, open: 1, servedBy: 1, metrics: 1, msgs: 1 }, + projection: { ts: 1, departmentId: 1, open: 1, servedBy: 1, responseBy: 1, metrics: 1, msgs: 1 }, }); } diff --git a/apps/meteor/server/models/raw/Messages.ts b/apps/meteor/server/models/raw/Messages.ts index e84abc3ed481..bec0afaafe43 100644 --- a/apps/meteor/server/models/raw/Messages.ts +++ b/apps/meteor/server/models/raw/Messages.ts @@ -1779,13 +1779,13 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { return this.col.countDocuments(query); } - decreaseReplyCountById(_id: string, inc = -1): Promise { + decreaseReplyCountById(_id: string, inc = -1): Promise> { const query = { _id }; const update: UpdateFilter = { $inc: { tcount: inc, }, }; - return this.updateOne(query, update); + return this.findOneAndUpdate(query, update, { returnDocument: 'after' }); } } diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index 0e5832b2aad9..04df04af5939 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -2485,6 +2485,15 @@ export class UsersRaw extends BaseRaw { ); } + findOneByFreeSwitchExtensions(freeSwitchExtensions, options = {}) { + return this.findOne( + { + freeSwitchExtension: { $in: freeSwitchExtensions }, + }, + options, + ); + } + findAssignedFreeSwitchExtensions() { return this.findUsersWithAssignedFreeSwitchExtensions({ projection: { diff --git a/apps/meteor/server/models/raw/VideoConference.ts b/apps/meteor/server/models/raw/VideoConference.ts index 5d18d9892038..0f631cf2d301 100644 --- a/apps/meteor/server/models/raw/VideoConference.ts +++ b/apps/meteor/server/models/raw/VideoConference.ts @@ -5,6 +5,7 @@ import type { IUser, IRoom, RocketChatRecordDeleted, + IVoIPVideoConference, } from '@rocket.chat/core-typings'; import { VideoConferenceStatus } from '@rocket.chat/core-typings'; import type { FindPaginated, InsertionModel, IVideoConferenceModel } from '@rocket.chat/model-typings'; @@ -136,6 +137,13 @@ export class VideoConferenceRaw extends BaseRaw implements IVid return (await this.insertOne(call)).insertedId; } + public async createVoIP(call: InsertionModel): Promise { + const { externalId, ...data } = call; + + const doc = await this.findOneAndUpdate({ externalId }, { $set: data }, { upsert: true, returnDocument: 'after' }); + return doc.value?._id; + } + public updateOneById( _id: string, update: UpdateFilter | Partial, diff --git a/apps/meteor/server/models/startup.ts b/apps/meteor/server/models/startup.ts index c3ecc381f7f0..a03c2265c683 100644 --- a/apps/meteor/server/models/startup.ts +++ b/apps/meteor/server/models/startup.ts @@ -15,6 +15,8 @@ import './EmojiCustom'; import './ExportOperations'; import './FederationKeys'; import './FederationServers'; +import './FreeSwitchCall'; +import './FreeSwitchEvent'; import './ImportData'; import './InstanceStatus'; import './IntegrationHistory'; diff --git a/apps/meteor/server/modules/core-apps/cloudAnnouncements.module.ts b/apps/meteor/server/modules/core-apps/cloudAnnouncements.module.ts index 68599162cdef..569e9d639b17 100644 --- a/apps/meteor/server/modules/core-apps/cloudAnnouncements.module.ts +++ b/apps/meteor/server/modules/core-apps/cloudAnnouncements.module.ts @@ -67,9 +67,8 @@ export class CloudAnnouncementsModule implements IUiKitCoreApp { const type = announcement?.surface === 'banner' ? 'banner.close' : 'modal.close'; // for viewClosed we just need to let Cloud know that the banner was closed, no need to wait for the response - setImmediate(async () => { - await this.handlePayload(payload); - }); + + void this.handlePayload(payload); return { type, diff --git a/apps/meteor/server/modules/core-apps/cloudSubscriptionCommunication.module.ts b/apps/meteor/server/modules/core-apps/cloudSubscriptionCommunication.module.ts new file mode 100644 index 000000000000..8e5789f91099 --- /dev/null +++ b/apps/meteor/server/modules/core-apps/cloudSubscriptionCommunication.module.ts @@ -0,0 +1,37 @@ +import type { UiKitCoreAppPayload } from '@rocket.chat/core-services'; +import type * as UiKit from '@rocket.chat/ui-kit'; + +import { CloudAnnouncementsModule } from './cloudAnnouncements.module'; + +export class CloudSubscriptionCommunication extends CloudAnnouncementsModule { + appId = 'cloud-communication-core'; + + async viewClosed(payload: UiKitCoreAppPayload): Promise { + const { + payload: { view: { viewId } = {} }, + user: { _id: userId } = {}, + } = payload; + + if (!userId) { + throw new Error('invalid user'); + } + + if (!viewId) { + throw new Error('invalid view'); + } + + if (!payload.triggerId) { + throw new Error('invalid triggerId'); + } + + // for viewClosed we just need to let Cloud know that the banner was closed, no need to wait for the response + + void this.handlePayload(payload); + + return { + type: 'modal.close', + triggerId: payload.triggerId, + appId: payload.appId, + }; + } +} diff --git a/apps/meteor/server/routes/avatar/user.spec.ts b/apps/meteor/server/routes/avatar/user.spec.ts index f5e6d87dbb63..1604c545aa05 100644 --- a/apps/meteor/server/routes/avatar/user.spec.ts +++ b/apps/meteor/server/routes/avatar/user.spec.ts @@ -169,7 +169,9 @@ describe('#userAvatarById()', () => { await userAvatarById(request, response, next); expect(mocks.utils.setCacheAndDispositionHeaders.calledWith(request, response)).to.be.true; - expect(mocks.utils.serveSvgAvatarInRequestedFormat.calledWith({ nameOrUsername: 'Doe', req: request, res: response })).to.be.true; + expect( + mocks.utils.serveSvgAvatarInRequestedFormat.calledWith({ nameOrUsername: 'Doe', req: request, res: response, useAllInitials: true }), + ).to.be.true; }); }); @@ -234,13 +236,42 @@ describe('#userAvatarByUsername()', () => { expect(pipe.calledWith(response)).to.be.true; }); - it(`should serve svg if requestUsername starts with @`, async () => { - const request = { url: '/@jon' }; + describe('should serve svg if requestUsername starts with @', () => { + it('should serve SVG and useAllInitials should be false', async () => { + const request = { url: '/@jon' }; - await userAvatarByUsername(request, response, next); + mocks.settingsGet.returns(false); - expect(mocks.utils.setCacheAndDispositionHeaders.calledWith(request, response)).to.be.true; - expect(mocks.utils.serveSvgAvatarInRequestedFormat.calledWith({ nameOrUsername: 'jon', req: request, res: response })).to.be.true; + await userAvatarByUsername(request, response, next); + + expect(mocks.utils.setCacheAndDispositionHeaders.calledWith(request, response)).to.be.true; + expect( + mocks.utils.serveSvgAvatarInRequestedFormat.calledWith({ + nameOrUsername: 'jon', + req: request, + res: response, + useAllInitials: false, + }), + ).to.be.true; + }); + + it('should serve SVG and useAllInitials should be true', async () => { + const request = { url: '/@baba yaga' }; + + mocks.settingsGet.withArgs('UI_Use_Name_Avatar').returns(true); + + await userAvatarByUsername(request, response, next); + + expect(mocks.utils.setCacheAndDispositionHeaders.calledWith(request, response)).to.be.true; + expect( + mocks.utils.serveSvgAvatarInRequestedFormat.calledWith({ + nameOrUsername: 'baba yaga', + req: request, + res: response, + useAllInitials: true, + }), + ).to.be.true; + }); }); it(`should serve avatar file if found`, async () => { @@ -286,6 +317,8 @@ describe('#userAvatarByUsername()', () => { await userAvatarByUsername(request, response, next); expect(mocks.utils.setCacheAndDispositionHeaders.calledWith(request, response)).to.be.true; - expect(mocks.utils.serveSvgAvatarInRequestedFormat.calledWith({ nameOrUsername: 'Doe', req: request, res: response })).to.be.true; + expect( + mocks.utils.serveSvgAvatarInRequestedFormat.calledWith({ nameOrUsername: 'Doe', req: request, res: response, useAllInitials: true }), + ).to.be.true; }); }); diff --git a/apps/meteor/server/routes/avatar/user.ts b/apps/meteor/server/routes/avatar/user.ts index 7b20287ddac3..5241390e0650 100644 --- a/apps/meteor/server/routes/avatar/user.ts +++ b/apps/meteor/server/routes/avatar/user.ts @@ -40,7 +40,12 @@ export const userAvatarByUsername = async function (request: IncomingMessage, re // if request starts with @ always return the svg letters if (requestUsername[0] === '@') { - serveSvgAvatarInRequestedFormat({ nameOrUsername: requestUsername.slice(1), req, res }); + serveSvgAvatarInRequestedFormat({ + nameOrUsername: requestUsername.slice(1), + req, + res, + useAllInitials: settings.get('UI_Use_Name_Avatar'), + }); return; } @@ -66,7 +71,7 @@ export const userAvatarByUsername = async function (request: IncomingMessage, re }); if (user?.name) { - serveSvgAvatarInRequestedFormat({ nameOrUsername: user.name, req, res }); + serveSvgAvatarInRequestedFormat({ nameOrUsername: user.name, req, res, useAllInitials: true }); return; } } @@ -126,7 +131,7 @@ export const userAvatarById = async function (request: IncomingMessage, res: Ser // Use real name for SVG letters if (settings.get('UI_Use_Name_Avatar') && user?.name) { - serveSvgAvatarInRequestedFormat({ nameOrUsername: user.name, req, res }); + serveSvgAvatarInRequestedFormat({ nameOrUsername: user.name, req, res, useAllInitials: true }); return; } diff --git a/apps/meteor/server/routes/avatar/utils.spec.ts b/apps/meteor/server/routes/avatar/utils.spec.ts index 3822ac8caa90..7f6bc2c28fe0 100644 --- a/apps/meteor/server/routes/avatar/utils.spec.ts +++ b/apps/meteor/server/routes/avatar/utils.spec.ts @@ -165,6 +165,29 @@ describe('#renderSvgLetters', () => { expect(renderSVGLetters('Bob', 32)).to.include('viewBox="0 0 32 32"'); expect(renderSVGLetters('yan', 64)).to.include('viewBox="0 0 64 64"'); }); + it('should return a default size of 125 for a single letter', () => { + expect(renderSVGLetters('a', 200)).to.include('font-size="125"'); + }); + it('should render a single letter when useAllInitials is false', () => { + expect(renderSVGLetters('arthur', 16, false)).to.include('>\nA\n'); + }); + it('should render a single letter when useAllInitials is true but username has no spaces', () => { + expect(renderSVGLetters('arthur', 16, true)).to.include('>\nA\n'); + }); + it('should render more than one letter when useAllInitials is true', () => { + expect(renderSVGLetters('arthur void', 16, true)).to.include('>\nAV\n'); + expect(renderSVGLetters('arthur void jackson', 16, true)).to.include('>\nAVJ\n'); + }); + it('should cap generated avatar to 3 letters at most', () => { + expect(renderSVGLetters('arthur void jackson billie', 16, true)).to.include('>\nAVJ\n'); + expect(renderSVGLetters('arthur void jackson billie jean', 16, true)).to.include('>\nAVJ\n'); + }); + it('should decrease the font size when username has more than 1 word', () => { + expect(renderSVGLetters('arthur void', 200, true)).to.include('font-size="100"'); + }); + it('should decrease the font size when username has 3 words', () => { + expect(renderSVGLetters('this is three_words', 200, true)).to.include('font-size="80"'); + }); }); describe('#setCacheAndDispositionHeaders', () => { diff --git a/apps/meteor/server/routes/avatar/utils.ts b/apps/meteor/server/routes/avatar/utils.ts index 892bd959558d..377369f4a450 100644 --- a/apps/meteor/server/routes/avatar/utils.ts +++ b/apps/meteor/server/routes/avatar/utils.ts @@ -18,6 +18,7 @@ const cookie = new Cookies(); export const MAX_SVG_AVATAR_SIZE = 1024; export const MIN_SVG_AVATAR_SIZE = 16; +const MAX_SVG_AVATAR_INITIALS = 3; export const serveAvatarFile = (file: IUpload, req: IIncomingMessage, res: ServerResponse, next: NextFunction) => { res.setHeader('Content-Security-Policy', "default-src 'none'"); @@ -56,13 +57,15 @@ export const serveSvgAvatarInRequestedFormat = ({ nameOrUsername, req, res, + useAllInitials = false, }: { nameOrUsername: string; req: IIncomingMessage; res: ServerResponse; + useAllInitials?: boolean; }) => { const size = getAvatarSizeFromRequest(req); - const avatar = renderSVGLetters(nameOrUsername, size); + const avatar = renderSVGLetters(nameOrUsername, size, useAllInitials); res.setHeader('Last-Modified', FALLBACK_LAST_MODIFIED); const { format } = req.query; @@ -125,7 +128,9 @@ const getFirstLetter = (name: string) => .substr(0, 1) .toUpperCase(); -export const renderSVGLetters = (username: string, viewSize = 200) => { +const getInitials = (name: string) => name.split(' ').slice(0, MAX_SVG_AVATAR_INITIALS).map(getFirstLetter).join(''); + +export const renderSVGLetters = (username: string, viewSize = 200, useAllInitials = false) => { let color = ''; let initials = ''; @@ -134,10 +139,11 @@ export const renderSVGLetters = (username: string, viewSize = 200) => { initials = username; } else { color = getAvatarColor(username); - initials = getFirstLetter(username); + initials = !useAllInitials ? getFirstLetter(username) : getInitials(username); } - const fontSize = viewSize / 1.6; + const reductionFactor = initials.length > 1 ? Math.pow(initials.length, 2) / 10 : 0; + const fontSize = viewSize / (1.6 + reductionFactor); return `\n\n\n${initials}\n\n`; }; diff --git a/apps/meteor/server/services/federation/infrastructure/rocket-chat/hooks/index.ts b/apps/meteor/server/services/federation/infrastructure/rocket-chat/hooks/index.ts index 8cac9bc9ffb0..4d8f8cf298f0 100644 --- a/apps/meteor/server/services/federation/infrastructure/rocket-chat/hooks/index.ts +++ b/apps/meteor/server/services/federation/infrastructure/rocket-chat/hooks/index.ts @@ -74,12 +74,10 @@ export class FederationHooks { callbacks.add( 'federation.beforeCreateDirectMessage', async (members: IUser[]): Promise => { - if (!members) { + if (!members || !isFederationEnabled()) { return; } - throwIfFederationNotEnabledOrNotReady(); - await callback(members); }, callbacks.priority.HIGH, diff --git a/apps/meteor/server/services/omnichannel-analytics/AgentData.ts b/apps/meteor/server/services/omnichannel-analytics/AgentData.ts index 40ce0f1236cb..e92f7c7b6716 100644 --- a/apps/meteor/server/services/omnichannel-analytics/AgentData.ts +++ b/apps/meteor/server/services/omnichannel-analytics/AgentData.ts @@ -235,15 +235,15 @@ export class AgentOverviewData { data: [], }; - await this.roomsModel.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { - if (servedBy && metrics && metrics.response && metrics.response.ft) { - if (agentAvgRespTime.has(servedBy.username)) { - agentAvgRespTime.set(servedBy.username, { - frt: agentAvgRespTime.get(servedBy.username).frt + metrics.response.ft, - total: agentAvgRespTime.get(servedBy.username).total + 1, + await this.roomsModel.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, responseBy }) => { + if (responseBy && metrics && metrics.response && metrics.response.ft) { + if (agentAvgRespTime.has(responseBy.username)) { + agentAvgRespTime.set(responseBy.username, { + frt: agentAvgRespTime.get(responseBy.username).frt + metrics.response.ft, + total: agentAvgRespTime.get(responseBy.username).total + 1, }); } else { - agentAvgRespTime.set(servedBy.username, { + agentAvgRespTime.set(responseBy.username, { frt: metrics.response.ft, total: 1, }); @@ -267,7 +267,7 @@ export class AgentOverviewData { } async Best_first_response_time(from: moment.Moment, to: moment.Moment, departmentId?: string, extraQuery: Filter = {}) { - const agentFirstRespTime = new Map(); // stores avg response time for each agent + const agentFirstRespTime = new Map(); // stores best response time for each agent const date = { gte: from.toDate(), lte: to.toDate(), @@ -285,12 +285,12 @@ export class AgentOverviewData { data: [], }; - await this.roomsModel.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { - if (servedBy && metrics && metrics.response && metrics.response.ft) { - if (agentFirstRespTime.has(servedBy.username)) { - agentFirstRespTime.set(servedBy.username, Math.min(agentFirstRespTime.get(servedBy.username), metrics.response.ft)); + await this.roomsModel.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, responseBy }) => { + if (responseBy && metrics && metrics.response && metrics.response.ft) { + if (agentFirstRespTime.has(responseBy.username)) { + agentFirstRespTime.set(responseBy.username, Math.min(agentFirstRespTime.get(responseBy.username), metrics.response.ft)); } else { - agentFirstRespTime.set(servedBy.username, metrics.response.ft); + agentFirstRespTime.set(responseBy.username, metrics.response.ft); } } }); diff --git a/apps/meteor/server/services/video-conference/service.ts b/apps/meteor/server/services/video-conference/service.ts index 4b1456f37774..694f92d014a6 100644 --- a/apps/meteor/server/services/video-conference/service.ts +++ b/apps/meteor/server/services/video-conference/service.ts @@ -21,6 +21,8 @@ import type { VideoConferenceCapabilities, VideoConferenceCreateData, Optional, + ExternalVideoConference, + IVoIPVideoConference, } from '@rocket.chat/core-typings'; import { VideoConferenceStatus, @@ -29,6 +31,7 @@ import { isLivechatVideoConference, } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; +import type { InsertionModel } from '@rocket.chat/model-typings'; import { Users, VideoConference as VideoConferenceModel, Rooms, Messages, Subscriptions } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import type { PaginatedResult } from '@rocket.chat/rest-typings'; @@ -140,7 +143,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf public async join(uid: IUser['_id'] | undefined, callId: VideoConference['_id'], options: VideoConferenceJoinOptions): Promise { return wrapExceptions(async () => { const call = await VideoConferenceModel.findOneById(callId); - if (!call || call.endedAt) { + if (!call || call.endedAt || !videoConfTypes.isCallManagedByApp(call)) { throw new Error('invalid-call'); } @@ -175,6 +178,10 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf throw new Error('invalid-call'); } + if (!videoConfTypes.isCallManagedByApp(call)) { + return []; + } + if (!videoConfProviders.isProviderAvailable(call.providerName)) { throw new Error('video-conf-provider-unavailable'); } @@ -454,6 +461,16 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf return true; } + public async createVoIP(data: InsertionModel): Promise { + return wrapExceptions(async () => VideoConferenceModel.createVoIP(data)).catch((err) => { + logger.error({ + name: 'Error on VideoConf.createVoIP', + err, + }); + throw err; + }); + } + private notifyUser( userId: IUser['_id'], action: string, @@ -855,7 +872,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf } private async joinCall( - call: VideoConference, + call: ExternalVideoConference, user: AtLeast | undefined, options: VideoConferenceJoinOptions, ): Promise { @@ -885,7 +902,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf return room?.fname || room?.name || rid; } - private async generateNewUrl(call: VideoConference): Promise { + private async generateNewUrl(call: ExternalVideoConference): Promise { if (!videoConfProviders.isProviderAvailable(call.providerName)) { throw new Error('video-conf-provider-unavailable'); } @@ -944,7 +961,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf } private async getUrl( - call: VideoConference, + call: ExternalVideoConference, user?: AtLeast, options: VideoConferenceJoinOptions = {}, ): Promise { @@ -987,6 +1004,10 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf throw new Error('video-conf-data-not-found'); } + if (!videoConfTypes.isCallManagedByApp(call)) { + return; + } + if (!videoConfProviders.isProviderAvailable(call.providerName)) { throw new Error('video-conf-provider-unavailable'); } @@ -1001,6 +1022,10 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf throw new Error('video-conf-data-not-found'); } + if (!videoConfTypes.isCallManagedByApp(call)) { + return; + } + if (!videoConfProviders.isProviderAvailable(call.providerName)) { throw new Error('video-conf-provider-unavailable'); } @@ -1015,6 +1040,10 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf throw new Error('video-conf-data-not-found'); } + if (!videoConfTypes.isCallManagedByApp(call)) { + return; + } + if (!videoConfProviders.isProviderAvailable(call.providerName)) { throw new Error('video-conf-provider-unavailable'); } @@ -1159,6 +1188,9 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf }, { creator: user._id, + subscriptionExtra: { + open: false, + }, }, ); @@ -1190,7 +1222,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf private async addUserToDiscussion(rid: IRoom['_id'], uid: IUser['_id']): Promise { try { - await Room.addUserToRoom(rid, { _id: uid }, undefined, { skipAlertSound: true }); + await Room.addUserToRoom(rid, { _id: uid }, undefined, { skipSystemMessage: true, createAsHidden: true }); } catch (error) { // Ignore any errors here so that the subscription doesn't block the user from participating in the conference. logger.error({ diff --git a/apps/meteor/server/settings/setup-wizard.ts b/apps/meteor/server/settings/setup-wizard.ts index 91e3125d1280..13d76a6070d9 100644 --- a/apps/meteor/server/settings/setup-wizard.ts +++ b/apps/meteor/server/settings/setup-wizard.ts @@ -1342,5 +1342,10 @@ export const createSetupWSettings = () => }, secret: true, }); + await this.add('Cloud_Sync_Announcement_Payload', 'null', { + type: 'string', // TODO: replace setting type string for object once is implemented. + hidden: true, + secret: true, + }); }); }); diff --git a/apps/meteor/server/startup/coreApps.ts b/apps/meteor/server/startup/coreApps.ts index 9638d99ab559..38f95da93d20 100644 --- a/apps/meteor/server/startup/coreApps.ts +++ b/apps/meteor/server/startup/coreApps.ts @@ -1,11 +1,13 @@ import { BannerModule } from '../modules/core-apps/banner.module'; import { CloudAnnouncementsModule } from '../modules/core-apps/cloudAnnouncements.module'; +import { CloudSubscriptionCommunication } from '../modules/core-apps/cloudSubscriptionCommunication.module'; import { MentionModule } from '../modules/core-apps/mention.module'; import { Nps } from '../modules/core-apps/nps.module'; import { VideoConfModule } from '../modules/core-apps/videoconf.module'; import { registerCoreApp } from '../services/uikit-core-app/service'; registerCoreApp(new CloudAnnouncementsModule()); +registerCoreApp(new CloudSubscriptionCommunication()); registerCoreApp(new Nps()); registerCoreApp(new BannerModule()); registerCoreApp(new VideoConfModule()); diff --git a/apps/meteor/server/startup/cron.ts b/apps/meteor/server/startup/cron.ts index 308d1297a01a..8951038f80fe 100644 --- a/apps/meteor/server/startup/cron.ts +++ b/apps/meteor/server/startup/cron.ts @@ -1,5 +1,4 @@ import { Logger } from '@rocket.chat/logger'; -import { Meteor } from 'meteor/meteor'; import { federationCron } from '../cron/federation'; import { npsCron } from '../cron/nps'; @@ -12,14 +11,13 @@ import { videoConferencesCron } from '../cron/videoConferences'; const logger = new Logger('SyncedCron'); -Meteor.defer(async () => { +export const startCronJobs = async (): Promise => { await startCron(); - await oembedCron(); await usageReportCron(logger); await npsCron(); await temporaryUploadCleanupCron(); await federationCron(); await videoConferencesCron(); - await userDataDownloadsCron(); -}); + userDataDownloadsCron(); +}; diff --git a/apps/meteor/server/startup/index.ts b/apps/meteor/server/startup/index.ts index 001af2c12be5..048735bba752 100644 --- a/apps/meteor/server/startup/index.ts +++ b/apps/meteor/server/startup/index.ts @@ -1,6 +1,6 @@ import './appcache'; import './callbacks'; -import './cron'; +import { startCronJobs } from './cron'; import './initialData'; import './serverRunning'; import './coreApps'; @@ -13,6 +13,8 @@ import { isRunningMs } from '../lib/isRunningMs'; export const startup = async () => { await performMigrationProcedure(); + + setImmediate(() => startCronJobs()); // only starts network broker if running in micro services mode if (!isRunningMs()) { require('./localServices'); diff --git a/apps/meteor/server/startup/watchDb.ts b/apps/meteor/server/startup/watchDb.ts index 5a0dd0417f63..f60f1e5949fd 100644 --- a/apps/meteor/server/startup/watchDb.ts +++ b/apps/meteor/server/startup/watchDb.ts @@ -1,4 +1,4 @@ -import { api } from '@rocket.chat/core-services'; +import { api, dbWatchersDisabled } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; import { MongoInternals } from 'meteor/mongo'; @@ -19,12 +19,17 @@ watcher.watch().catch((err: Error) => { process.exit(1); }); -setInterval(function _checkDatabaseWatcher() { - if (watcher.isLastDocDelayed()) { - SystemLogger.error('No real time data received recently'); - } -}, 20000); +if (!dbWatchersDisabled) { + setInterval(function _checkDatabaseWatcher() { + if (watcher.isLastDocDelayed()) { + SystemLogger.error('No real time data received recently'); + } + }, 20000); +} export function isLastDocDelayed(): boolean { + if (dbWatchersDisabled) { + return true; + } return watcher.isLastDocDelayed(); } diff --git a/apps/meteor/tests/data/chat.helper.ts b/apps/meteor/tests/data/chat.helper.ts index 5df283831203..2d0588a7f158 100644 --- a/apps/meteor/tests/data/chat.helper.ts +++ b/apps/meteor/tests/data/chat.helper.ts @@ -7,10 +7,12 @@ export const sendSimpleMessage = ({ roomId, text = 'test message', tmid, + userCredentials = credentials, }: { roomId: IRoom['_id']; text?: string; tmid?: IMessage['_id']; + userCredentials?: Credentials; }) => { if (!roomId) { throw new Error('"roomId" is required in "sendSimpleMessage" test helper'); @@ -28,7 +30,7 @@ export const sendSimpleMessage = ({ message.tmid = tmid; } - return request.post(api('chat.sendMessage')).set(credentials).send({ message }); + return request.post(api('chat.sendMessage')).set(userCredentials).send({ message }); }; export const sendMessage = ({ @@ -87,3 +89,10 @@ export const getMessageById = ({ msgId }: { msgId: IMessage['_id'] }) => { }); }); }; + +export const followMessage = ({ msgId, requestCredentials }: { msgId: IMessage['_id']; requestCredentials?: Credentials }) => { + return request + .post(api('chat.followMessage')) + .set(requestCredentials ?? credentials) + .send({ mid: msgId }); +}; diff --git a/apps/meteor/tests/data/rooms.helper.ts b/apps/meteor/tests/data/rooms.helper.ts index 410e6e7ca48c..29059cf2f42b 100644 --- a/apps/meteor/tests/data/rooms.helper.ts +++ b/apps/meteor/tests/data/rooms.helper.ts @@ -1,7 +1,7 @@ import type { Credentials } from '@rocket.chat/api-client'; -import type { IRoom } from '@rocket.chat/core-typings'; +import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; -import { api, credentials, request } from './api-data'; +import { api, credentials, methodCall, request } from './api-data'; type CreateRoomParams = { name?: IRoom['name']; @@ -108,3 +108,36 @@ export function actionRoom({ action, type, roomId, overrideCredentials = credent export const deleteRoom = ({ type, roomId }: { type: ActionRoomParams['type']; roomId: IRoom['_id'] }) => actionRoom({ action: 'delete', type, roomId, overrideCredentials: credentials }); + +export const getSubscriptionByRoomId = (roomId: IRoom['_id'], userCredentials = credentials): Promise => + new Promise((resolve) => { + void request + .get(api('subscriptions.getOne')) + .set(userCredentials) + .query({ roomId }) + .end((_err, res) => { + resolve(res.body.subscription); + }); + }); + +export const addUserToRoom = ({ + usernames, + rid, + userCredentials, +}: { + usernames: string[]; + rid: IRoom['_id']; + userCredentials?: Credentials; +}) => { + return request + .post(methodCall('addUsersToRoom')) + .set(userCredentials ?? credentials) + .send({ + message: JSON.stringify({ + method: 'addUsersToRoom', + params: [{ rid, users: usernames }], + id: 'id', + msg: 'method', + }), + }); +}; diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 04d01d5a7c71..cecb6fd525d4 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -231,6 +231,20 @@ test.describe.serial('e2e-encryption initial setup', () => { ); await expect(poHomeChannel.content.nthMessage(0).locator('.rcx-icon--name-key')).toBeVisible(); }); + + test('should display only the download file method when exporting messages in an e2ee room', async ({ page }) => { + await page.goto('/home'); + const channelName = faker.string.uuid(); + await poHomeChannel.sidenav.createEncryptedChannel(channelName); + await expect(page).toHaveURL(`/group/${channelName}`); + + await poHomeChannel.dismissToast(); + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + await poHomeChannel.tabs.kebab.click({ force: true }); + await poHomeChannel.tabs.btnExportMessages.click(); + await expect(poHomeChannel.tabs.exportMessages.downloadFileMethod).toBeVisible(); + }); }); test.describe.serial('e2e-encryption', () => { diff --git a/apps/meteor/tests/e2e/export-messages.spec.ts b/apps/meteor/tests/e2e/export-messages.spec.ts new file mode 100644 index 000000000000..dffb6b4d5edb --- /dev/null +++ b/apps/meteor/tests/e2e/export-messages.spec.ts @@ -0,0 +1,72 @@ +import { Users } from './fixtures/userStates'; +import { HomeChannel, Utils } from './page-objects'; +import { createTargetChannel } from './utils'; +import { test, expect } from './utils/test'; + +test.use({ storageState: Users.admin.state }); + +test.describe.serial('export-messages', () => { + let poHomeChannel: HomeChannel; + let poUtils: Utils; + let targetChannel: string; + + test.beforeAll(async ({ api }) => { + targetChannel = await createTargetChannel(api); + }); + + test.beforeEach(async ({ page }) => { + poHomeChannel = new HomeChannel(page); + poUtils = new Utils(page); + + await page.goto('/home'); + }); + + test('should all export methods be available in targetChannel', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.tabs.kebab.click({ force: true }); + await poHomeChannel.tabs.btnExportMessages.click(); + await expect(poHomeChannel.tabs.exportMessages.sendEmailMethod).not.toBeDisabled(); + + await poHomeChannel.tabs.exportMessages.sendEmailMethod.click(); + await expect(poHomeChannel.tabs.exportMessages.getMethodByName('Send email')).toBeVisible(); + await expect(poHomeChannel.tabs.exportMessages.getMethodByName('Send file via email')).toBeVisible(); + await expect(poHomeChannel.tabs.exportMessages.getMethodByName('Download file')).toBeVisible(); + }); + + test('should display an error when trying to send email without filling to users or to additional emails', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.content.sendMessage('hello world'); + await poHomeChannel.tabs.kebab.click({ force: true }); + await poHomeChannel.tabs.btnExportMessages.click(); + + await poHomeChannel.content.getMessageByText('hello world').click(); + await poHomeChannel.tabs.exportMessages.btnSend.click(); + + await expect( + poUtils.getAlertByText('You must select one or more users or provide one or more email addresses, separated by commas'), + ).toBeVisible(); + }); + + test('should display an error when trying to send email without selecting any message', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.tabs.kebab.click({ force: true }); + await poHomeChannel.tabs.btnExportMessages.click(); + + await poHomeChannel.tabs.exportMessages.textboxAdditionalEmails.fill('mail@mail.com'); + await poHomeChannel.tabs.exportMessages.btnSend.click(); + + await expect(poUtils.getAlertByText(`You haven't selected any messages`)).toBeVisible(); + }); + + test('should be able to send messages after closing export messages', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.tabs.kebab.click({ force: true }); + await poHomeChannel.tabs.btnExportMessages.click(); + + await poHomeChannel.content.getMessageByText('hello world').click(); + await poHomeChannel.tabs.exportMessages.btnCancel.click(); + await poHomeChannel.content.sendMessage('hello export'); + + await expect(poHomeChannel.content.getMessageByText('hello export')).toBeVisible(); + }); +}); diff --git a/apps/meteor/tests/e2e/feature-preview.spec.ts b/apps/meteor/tests/e2e/feature-preview.spec.ts index 4e6b62827c85..85969debb8fc 100644 --- a/apps/meteor/tests/e2e/feature-preview.spec.ts +++ b/apps/meteor/tests/e2e/feature-preview.spec.ts @@ -280,8 +280,7 @@ test.describe.serial('feature preview', () => { await expect(poHomeChannel.sidepanel.getItemByName(targetChannel)).toBeVisible(); }); - // remove .fail after fix - test.fail('should sort by last message even if unread message is inside thread', async ({ page, browser }) => { + test('should sort by last message even if unread message is inside thread', async ({ page, browser }) => { const user1Page = await browser.newPage({ storageState: Users.user1.state }); const user1Channel = new HomeChannel(user1Page); diff --git a/apps/meteor/tests/e2e/login.spec.ts b/apps/meteor/tests/e2e/login.spec.ts index 63e4949c31d9..75340c64506b 100644 --- a/apps/meteor/tests/e2e/login.spec.ts +++ b/apps/meteor/tests/e2e/login.spec.ts @@ -2,6 +2,7 @@ import { faker } from '@faker-js/faker'; import { DEFAULT_USER_CREDENTIALS } from './config/constants'; import { Utils, Registration } from './page-objects'; +import { setSettingValueById } from './utils/setSettingValueById'; import { test, expect } from './utils/test'; test.describe.parallel('Login', () => { @@ -15,6 +16,10 @@ test.describe.parallel('Login', () => { await page.goto('/home'); }); + test.afterAll(async ({ api }) => { + await setSettingValueById(api, 'Language', 'en'); + }); + test('should not have any accessibility violations', async ({ makeAxeBuilder }) => { const results = await makeAxeBuilder().analyze(); expect(results.violations).toEqual([]); @@ -50,4 +55,18 @@ test.describe.parallel('Login', () => { await expect(poUtils.mainContent).toBeVisible(); }); }); + + test('Should correctly display switch language button', async ({ page, api }) => { + expect((await setSettingValueById(api, 'Language', 'pt-BR')).status()).toBe(200); + + const button = page.getByRole('button', { name: 'Change to português (Brasil)' }); + await button.click(); + + await expect(page.getByRole('button', { name: 'Fazer Login' })).toBeVisible(); + + const buttonEnglish = page.getByRole('button', { name: 'Change to English' }); + await buttonEnglish.click(); + + await expect(page.getByRole('button', { name: 'Login' })).toBeVisible(); + }); }); diff --git a/apps/meteor/tests/e2e/otr.spec.ts b/apps/meteor/tests/e2e/otr.spec.ts new file mode 100644 index 000000000000..b6ff4789e0c2 --- /dev/null +++ b/apps/meteor/tests/e2e/otr.spec.ts @@ -0,0 +1,46 @@ +import { Users } from './fixtures/userStates'; +import { HomeChannel } from './page-objects'; +import { createDirectMessage } from './utils'; +import { test, expect } from './utils/test'; + +test.use({ storageState: Users.admin.state }); + +test.describe.serial('OTR', () => { + let poHomeChannel: HomeChannel; + + test.beforeEach(async ({ page, api }) => { + await createDirectMessage(api); + poHomeChannel = new HomeChannel(page); + + await page.goto('/home'); + }); + + test('should not allow export OTR messages', async ({ browser }) => { + const user1Page = await browser.newPage({ storageState: Users.user1.state }); + const user1Channel = new HomeChannel(user1Page); + + await test.step('log in user1', async () => { + await user1Page.goto(`/direct/${Users.admin.data.username}`); + await user1Channel.content.waitForChannel(); + }); + + await test.step('invite OTR with user1', async () => { + await poHomeChannel.sidenav.openChat(Users.user1.data.username); + await poHomeChannel.tabs.kebab.click({ force: true }); + await poHomeChannel.tabs.btnEnableOTR.click({ force: true }); + await poHomeChannel.tabs.otr.btnStartOTR.click(); + }); + + await test.step('accept handshake with user1', async () => { + await user1Channel.tabs.otr.btnAcceptOTR.click(); + }); + + await poHomeChannel.content.sendMessage('hello OTR'); + await poHomeChannel.tabs.kebab.click({ force: true }); + await poHomeChannel.tabs.btnExportMessages.click(); + await poHomeChannel.content.getMessageByText('hello OTR').click(); + await expect(poHomeChannel.content.btnClearSelection).toBeDisabled(); + + await user1Page.close(); + }); +}); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 59df066d8163..2a66a899f3ce 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -391,7 +391,7 @@ export class HomeContent { } getSystemMessageByText(text: string): Locator { - return this.page.locator('[aria-roledescription="system message"]', { hasText: text }); + return this.page.locator('[role="listitem"][aria-roledescription="system message"]', { hasText: text }); } getMessageByText(text: string): Locator { @@ -417,4 +417,8 @@ export class HomeContent { await this.page.getByRole('dialog').getByRole('textbox', { name: 'Message' }).fill(text); await this.page.getByRole('dialog').getByRole('button', { name: 'Send', exact: true }).click(); } + + get btnClearSelection() { + return this.page.getByRole('button', { name: 'Clear selection' }); + } } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-exportMessages.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-exportMessages.ts new file mode 100644 index 000000000000..ddf78b7f4388 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-exportMessages.ts @@ -0,0 +1,33 @@ +import type { Page } from '@playwright/test'; + +export class HomeFlextabExportMessages { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + get sendEmailMethod() { + return this.page.getByLabel('Send email'); + } + + get downloadFileMethod() { + return this.page.getByLabel('Download file'); + } + + getMethodByName(name: string) { + return this.page.getByRole('option', { name }); + } + + get textboxAdditionalEmails() { + return this.page.getByRole('textbox', { name: 'To additional emails' }); + } + + get btnSend() { + return this.page.locator('role=button[name="Send"]'); + } + + get btnCancel() { + return this.page.locator('role=button[name="Cancel"]'); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-otr.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-otr.ts new file mode 100644 index 000000000000..1a310f76f2f4 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-otr.ts @@ -0,0 +1,21 @@ +import type { Locator, Page } from '@playwright/test'; + +export class HomeFlextabOtr { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + get otrDialog(): Locator { + return this.page.getByRole('dialog', { name: 'OTR' }); + } + + get btnStartOTR(): Locator { + return this.otrDialog.getByRole('button', { name: 'Start OTR' }); + } + + get btnAcceptOTR(): Locator { + return this.page.getByRole('dialog').getByRole('button', { name: 'Yes' }); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts index 6e22bea99faf..52dc165e9a43 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts @@ -1,8 +1,10 @@ import type { Locator, Page } from '@playwright/test'; import { HomeFlextabChannels } from './home-flextab-channels'; +import { HomeFlextabExportMessages } from './home-flextab-exportMessages'; import { HomeFlextabMembers } from './home-flextab-members'; import { HomeFlextabNotificationPreferences } from './home-flextab-notificationPreferences'; +import { HomeFlextabOtr } from './home-flextab-otr'; import { HomeFlextabRoom } from './home-flextab-room'; export class HomeFlextab { @@ -16,12 +18,18 @@ export class HomeFlextab { readonly notificationPreferences: HomeFlextabNotificationPreferences; + readonly otr: HomeFlextabOtr; + + readonly exportMessages: HomeFlextabExportMessages; + constructor(page: Page) { this.page = page; this.members = new HomeFlextabMembers(page); this.room = new HomeFlextabRoom(page); this.channels = new HomeFlextabChannels(page); this.notificationPreferences = new HomeFlextabNotificationPreferences(page); + this.otr = new HomeFlextabOtr(page); + this.exportMessages = new HomeFlextabExportMessages(page); } get btnTabMembers(): Locator { @@ -48,6 +56,10 @@ export class HomeFlextab { return this.page.locator('role=menuitem[name="Notifications Preferences"]'); } + get btnExportMessages(): Locator { + return this.page.locator('role=menuitem[name="Export messages"]'); + } + get btnE2EERoomSetupDisableE2E(): Locator { return this.page.locator('[data-qa-id=ToolBoxAction-key]'); } diff --git a/apps/meteor/tests/e2e/page-objects/utils.ts b/apps/meteor/tests/e2e/page-objects/utils.ts index 066c5eac153f..15fb0b88b986 100644 --- a/apps/meteor/tests/e2e/page-objects/utils.ts +++ b/apps/meteor/tests/e2e/page-objects/utils.ts @@ -26,4 +26,10 @@ export class Utils { get btnModalConfirmDelete() { return this.page.locator('.rcx-modal >> button >> text="Delete"'); } + + getAlertByText(text: string): Locator { + return this.page.locator('[role="alert"]', { + hasText: text, + }); + } } diff --git a/apps/meteor/tests/end-to-end/api/chat.ts b/apps/meteor/tests/end-to-end/api/chat.ts index 2e23e2613779..43df270bbec1 100644 --- a/apps/meteor/tests/end-to-end/api/chat.ts +++ b/apps/meteor/tests/end-to-end/api/chat.ts @@ -1,15 +1,15 @@ import type { Credentials } from '@rocket.chat/api-client'; -import type { IMessage, IRoom, IThreadMessage, IUser } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, ISubscription, IThreadMessage, IUser } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; import { expect } from 'chai'; import { after, before, beforeEach, describe, it } from 'mocha'; import type { Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../data/api-data'; -import { sendSimpleMessage, deleteMessage } from '../../data/chat.helper'; +import { followMessage, sendSimpleMessage, deleteMessage } from '../../data/chat.helper'; import { imgURL } from '../../data/interactions'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; -import { createRoom, deleteRoom } from '../../data/rooms.helper'; +import { addUserToRoom, createRoom, deleteRoom, getSubscriptionByRoomId } from '../../data/rooms.helper'; import { password } from '../../data/user'; import type { TestUser } from '../../data/users.helper'; import { createUser, deleteUser, login } from '../../data/users.helper'; @@ -2005,6 +2005,52 @@ describe('[Chat]', () => { }) .end(done); }); + + describe('when deleting a thread message', () => { + let otherUser: TestUser; + let otherUserCredentials: Credentials; + let parentThreadId: IMessage['_id']; + + before(async () => { + const username = `user${+new Date()}`; + otherUser = await createUser({ username }); + otherUserCredentials = await login(otherUser.username, password); + parentThreadId = (await sendSimpleMessage({ roomId: testChannel._id })).body.message._id; + await addUserToRoom({ rid: testChannel._id, usernames: [otherUser.username] }); + }); + + after(() => Promise.all([deleteUser(otherUser), deleteMessage({ msgId: parentThreadId, roomId: testChannel._id })])); + + const expectNoUnreadThreadMessages = (s: ISubscription) => { + expect(s).to.have.property('tunread'); + expect(s.tunread).to.be.an('array'); + expect(s.tunread).to.deep.equal([]); + }; + + it('should reset the unread counter if the message was removed', async () => { + const { body } = await sendSimpleMessage({ roomId: testChannel._id, tmid: parentThreadId, userCredentials: otherUserCredentials }); + const childrenMessageId = body.message._id; + + await followMessage({ msgId: parentThreadId, requestCredentials: otherUserCredentials }); + await deleteMessage({ msgId: childrenMessageId, roomId: testChannel._id }); + + const userWhoCreatedTheThreadSubscription = await getSubscriptionByRoomId(testChannel._id); + + expectNoUnreadThreadMessages(userWhoCreatedTheThreadSubscription); + }); + + it('should reset the unread counter of users who followed the thread', async () => { + const { body } = await sendSimpleMessage({ roomId: testChannel._id, tmid: parentThreadId }); + const childrenMessageId = body.message._id; + + await followMessage({ msgId: parentThreadId, requestCredentials: otherUserCredentials }); + await deleteMessage({ msgId: childrenMessageId, roomId: testChannel._id }); + + const userWhoWasFollowingTheThreadSubscription = await getSubscriptionByRoomId(testChannel._id, otherUserCredentials); + + expectNoUnreadThreadMessages(userWhoWasFollowingTheThreadSubscription); + }); + }); }); describe('/chat.search', () => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts b/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts index 33cb2f1f26b0..351abecff21c 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts @@ -922,6 +922,7 @@ describe('LIVECHAT - dashboards', function () { describe('[livechat/analytics/agent-overview] - Average first response time', () => { let agent: { credentials: Credentials; user: IUser & { username: string } }; + let forwardAgent: { credentials: Credentials; user: IUser & { username: string } }; let originalFirstResponseTimeInSeconds: number; let roomId: string; const firstDelayInSeconds = 4; @@ -929,11 +930,10 @@ describe('LIVECHAT - dashboards', function () { before(async () => { agent = await createAnOnlineAgent(); + forwardAgent = await createAnOnlineAgent(); }); - after(async () => { - await deleteUser(agent.user); - }); + after(async () => Promise.all([deleteUser(agent.user), deleteUser(forwardAgent.user)])); it('should return no average response time for an agent if no response has been sent in the period', async () => { await startANewLivechatRoomAndTakeIt({ agent: agent.credentials }); @@ -984,6 +984,62 @@ describe('LIVECHAT - dashboards', function () { expect(originalFirstResponseTimeInSeconds).to.be.greaterThanOrEqual(firstDelayInSeconds); }); + it('should correctly associate the first response time to the first agent who responded the room', async () => { + const response = await startANewLivechatRoomAndTakeIt({ agent: forwardAgent.credentials }); + roomId = response.room._id; + + await sendAgentMessage(roomId, 'first response from agent', forwardAgent.credentials); + + await request + .post(api('livechat/room.forward')) + .set(credentials) + .send({ + roomId, + userId: agent.user._id, + comment: 'test comment', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + }); + + await sendAgentMessage(roomId, 'first response from forwarded agent', agent.credentials); + + const today = moment().startOf('day').format('YYYY-MM-DD'); + const result = await request + .get(api('livechat/analytics/agent-overview')) + .query({ from: today, to: today, name: 'Avg_first_response_time' }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(result.body).to.have.property('success', true); + expect(result.body).to.have.property('head'); + expect(result.body).to.have.property('data'); + expect(result.body.data).to.be.an('array'); + + // The agent to whom the room has been forwarded shouldn't have their average first response time changed + const agentData = result.body.data.find( + (agentOverviewData: { name: string; value: string }) => agentOverviewData.name === agent.user.username, + ); + expect(agentData).to.not.be.undefined; + expect(agentData).to.have.property('name', agent.user.username); + expect(agentData).to.have.property('value'); + const averageFirstResponseTimeInSeconds = moment.duration(agentData.value).asSeconds(); + expect(originalFirstResponseTimeInSeconds).to.be.equal(averageFirstResponseTimeInSeconds); + + // A room's first response time should be attached to the agent who first responded to it even if it has been forwarded + const forwardAgentData = result.body.data.find( + (agentOverviewData: { name: string; value: string }) => agentOverviewData.name === forwardAgent.user.username, + ); + expect(forwardAgentData).to.not.be.undefined; + expect(forwardAgentData).to.have.property('name', forwardAgent.user.username); + expect(forwardAgentData).to.have.property('value'); + const forwardAgentAverageFirstResponseTimeInSeconds = moment.duration(forwardAgentData.value).asSeconds(); + expect(originalFirstResponseTimeInSeconds).to.be.greaterThan(forwardAgentAverageFirstResponseTimeInSeconds); + }); + it('should correctly calculate the average time of first responses for an agent', async () => { const response = await startANewLivechatRoomAndTakeIt({ agent: agent.credentials }); roomId = response.room._id; @@ -1019,14 +1075,16 @@ describe('LIVECHAT - dashboards', function () { describe('[livechat/analytics/agent-overview] - Best first response time', () => { let agent: { credentials: Credentials; user: IUser & { username: string } }; + let forwardAgent: { credentials: Credentials; user: IUser & { username: string } }; let originalBestFirstResponseTimeInSeconds: number; let roomId: string; before(async () => { agent = await createAnOnlineAgent(); + forwardAgent = await createAnOnlineAgent(); }); - after(() => deleteUser(agent.user)); + after(() => Promise.all([deleteUser(agent.user), deleteUser(forwardAgent.user)])); it('should return no best response time for an agent if no response has been sent in the period', async () => { await startANewLivechatRoomAndTakeIt({ agent: agent.credentials }); @@ -1110,6 +1168,62 @@ describe('LIVECHAT - dashboards', function () { const bestFirstResponseTimeInSeconds = moment.duration(agentData.value).asSeconds(); expect(bestFirstResponseTimeInSeconds).to.be.equal(originalBestFirstResponseTimeInSeconds); }); + + it('should correctly associate best first response time to the first agent who responded the room', async () => { + const response = await startANewLivechatRoomAndTakeIt({ agent: forwardAgent.credentials }); + roomId = response.room._id; + + await sendAgentMessage(roomId, 'first response from agent', forwardAgent.credentials); + + await request + .post(api('livechat/room.forward')) + .set(credentials) + .send({ + roomId, + userId: agent.user._id, + comment: 'test comment', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + }); + + await sendAgentMessage(roomId, 'first response from forwarded agent', agent.credentials); + + const today = moment().startOf('day').format('YYYY-MM-DD'); + const result = await request + .get(api('livechat/analytics/agent-overview')) + .query({ from: today, to: today, name: 'Best_first_response_time' }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(result.body).to.have.property('success', true); + expect(result.body).to.have.property('head'); + expect(result.body).to.have.property('data'); + expect(result.body.data).to.be.an('array'); + + // The agent to whom the room has been forwarded shouldn't have their best first response time changed + const agentData = result.body.data.find( + (agentOverviewData: { name: string; value: string }) => agentOverviewData.name === agent.user.username, + ); + expect(agentData).to.not.be.undefined; + expect(agentData).to.have.property('name', agent.user.username); + expect(agentData).to.have.property('value'); + const bestFirstResponseTimeInSeconds = moment.duration(agentData.value).asSeconds(); + expect(bestFirstResponseTimeInSeconds).to.be.equal(originalBestFirstResponseTimeInSeconds); + + // A room's first response time should be attached to the agent who first responded to it even if it has been forwarded + const forwardAgentData = result.body.data.find( + (agentOverviewData: { name: string; value: string }) => agentOverviewData.name === forwardAgent.user.username, + ); + expect(forwardAgentData).to.not.be.undefined; + expect(forwardAgentData).to.have.property('name', forwardAgent.user.username); + expect(forwardAgentData).to.have.property('value'); + const forwardAgentBestFirstResponseTimeInSeconds = moment.duration(forwardAgentData.value).asSeconds(); + expect(forwardAgentBestFirstResponseTimeInSeconds).to.be.lessThan(originalBestFirstResponseTimeInSeconds); + }); }); describe('livechat/analytics/overview', () => { @@ -1170,12 +1284,12 @@ describe('LIVECHAT - dashboards', function () { expect(result.body).to.be.an('array'); const expectedResult = [ - { title: 'Total_conversations', value: 13 }, - { title: 'Open_conversations', value: 10 }, + { title: 'Total_conversations', value: 15 }, + { title: 'Open_conversations', value: 12 }, { title: 'On_Hold_conversations', value: 1 }, // { title: 'Total_messages', value: 6 }, // { title: 'Busiest_day', value: moment().format('dddd') }, - { title: 'Conversations_per_day', value: '6.50' }, + { title: 'Conversations_per_day', value: '7.50' }, // { title: 'Busiest_time', value: '' }, ]; diff --git a/apps/meteor/tests/end-to-end/api/livechat/contacts.ts b/apps/meteor/tests/end-to-end/api/livechat/contacts.ts index daaffb1f9eb3..e19f5490b857 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/contacts.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/contacts.ts @@ -142,18 +142,37 @@ describe('LIVECHAT - contacts', () => { }); describe('Custom Fields', () => { + let contactId: string; before(async () => { - await createCustomField({ - field: 'cf1', - label: 'Custom Field 1', - scope: 'visitor', + const defaultProps = { + scope: 'visitor' as const, visibility: 'public', type: 'input', - required: true, regexp: '^[0-9]+$', searchable: true, public: true, - }); + }; + + await Promise.all([ + createCustomField({ + ...defaultProps, + field: 'cf1', + label: 'Custom Field 1', + required: true, + }), + createCustomField({ + ...defaultProps, + field: 'cf2', + label: 'Custom Field 2', + required: false, + }), + createCustomField({ + ...defaultProps, + field: 'cfOptional', + label: 'Optional Custom Field', + required: false, + }), + ]); }); after(async () => { @@ -211,6 +230,98 @@ describe('LIVECHAT - contacts', () => { expect(res.body).to.have.property('error'); expect(res.body.error).to.be.equal('Invalid value for Custom Field 1 field'); }); + + it('should keep a legacy custom field, but not update it, nor throw an error if it is specified on update', async () => { + const createRes = await request + .post(api('omnichannel/contacts')) + .set(credentials) + .send({ + name: faker.person.fullName(), + emails: [faker.internet.email().toLowerCase()], + phones: [faker.phone.number()], + customFields: { + cf1: '123', + cf2: '456', + }, + }); + expect(createRes.body).to.have.property('success', true); + expect(createRes.body).to.have.property('contactId').that.is.a('string'); + contactId = createRes.body.contactId; + + await deleteCustomField('cf2'); + + const updateRes = await request + .post(api('omnichannel/contacts.update')) + .set(credentials) + .send({ + contactId, + customFields: { + cf1: '456', + cf2: '789', + }, + }); + expect(updateRes.body).to.have.property('success', true); + expect(updateRes.body).to.have.property('contact').that.is.an('object'); + expect(updateRes.body.contact).to.have.property('_id', contactId); + expect(updateRes.body.contact).to.have.property('customFields').that.is.an('object'); + expect(updateRes.body.contact.customFields).to.have.property('cf1', '456'); + expect(updateRes.body.contact.customFields).to.have.property('cf2', '456'); + }); + + it('should keep a legacy custom field and not throw an error if it is not specified on update', async () => { + const updateRes = await request + .post(api('omnichannel/contacts.update')) + .set(credentials) + .send({ + contactId, + customFields: { + cf1: '789', + cfOptional: '567', + }, + }); + expect(updateRes.body).to.have.property('success', true); + expect(updateRes.body).to.have.property('contact').that.is.an('object'); + expect(updateRes.body.contact).to.have.property('_id', contactId); + expect(updateRes.body.contact).to.have.property('customFields').that.is.an('object'); + expect(updateRes.body.contact.customFields).to.have.property('cf1', '789'); + expect(updateRes.body.contact.customFields).to.have.property('cfOptional', '567'); + expect(updateRes.body.contact.customFields).to.have.property('cf2', '456'); + }); + + it('should keep a legacy custom field, but remove an optional registered custom field if it is not specified on update', async () => { + const updateRes = await request + .post(api('omnichannel/contacts.update')) + .set(credentials) + .send({ + contactId, + customFields: { + cf1: '789', + }, + }); + expect(updateRes.body).to.have.property('success', true); + expect(updateRes.body).to.have.property('contact').that.is.an('object'); + expect(updateRes.body.contact).to.have.property('_id', contactId); + expect(updateRes.body.contact).to.have.property('customFields').that.is.an('object'); + expect(updateRes.body.contact.customFields).to.have.property('cf1', '789'); + expect(updateRes.body.contact.customFields).to.have.property('cf2', '456'); + expect(updateRes.body.contact.customFields).to.not.have.property('cfOptional'); + }); + + it('should throw an error if trying to update a custom field that is not registered in the workspace and does not exist in the contact', async () => { + const updateRes = await request + .post(api('omnichannel/contacts.update')) + .set(credentials) + .send({ + contactId, + customFields: { + cf1: '123', + cf3: 'invalid', + }, + }); + expect(updateRes.body).to.have.property('success', false); + expect(updateRes.body).to.have.property('error'); + expect(updateRes.body.error).to.be.equal('Custom field cf3 is not allowed'); + }); }); describe('Fields Validation', () => { @@ -710,7 +821,6 @@ describe('LIVECHAT - contacts', () => { describe('[GET] omnichannel/contacts.get', () => { let contactId: string; let contactId2: string; - let association: ILivechatContactVisitorAssociation; const email = faker.internet.email().toLowerCase(); const phone = faker.phone.number(); @@ -743,14 +853,7 @@ describe('LIVECHAT - contacts', () => { const visitor = await createVisitor(undefined, contact.name, email, phone); - const room = await createLivechatRoom(visitor.token); - association = { - visitorId: visitor._id, - source: { - type: room.source.type, - id: room.source.id, - }, - }; + await createLivechatRoom(visitor.token); }); after(async () => { @@ -774,23 +877,6 @@ describe('LIVECHAT - contacts', () => { expect(res.body.contact.contactManager).to.be.equal(contact.contactManager); }); - it('should be able get a contact by visitor association', async () => { - const res = await request.get(api(`omnichannel/contacts.get`)).set(credentials).query({ visitor: association }); - - expect(res.status).to.be.equal(200); - expect(res.body).to.have.property('success', true); - expect(res.body.contact).to.have.property('createdAt'); - expect(res.body.contact._id).to.be.equal(contactId); - expect(res.body.contact.name).to.be.equal(contact.name); - expect(res.body.contact.emails).to.be.deep.equal([ - { - address: contact.emails[0], - }, - ]); - expect(res.body.contact.phones).to.be.deep.equal([{ phoneNumber: contact.phones[0] }]); - expect(res.body.contact.contactManager).to.be.equal(contact.contactManager); - }); - it('should return 404 if contact does not exist using contactId', async () => { const res = await request.get(api(`omnichannel/contacts.get`)).set(credentials).query({ contactId: 'invalid' }); @@ -799,28 +885,6 @@ describe('LIVECHAT - contacts', () => { expect(res.body).to.have.property('error', 'Resource not found'); }); - it('should return 404 if contact does not exist using visitor association', async () => { - const res = await request - .get(api(`omnichannel/contacts.get`)) - .set(credentials) - .query({ visitor: { ...association, visitorId: 'invalidId' } }); - - expect(res.status).to.be.equal(404); - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', 'Resource not found'); - }); - - it('should return 404 if contact does not exist using visitor source', async () => { - const res = await request - .get(api(`omnichannel/contacts.get`)) - .set(credentials) - .query({ visitor: { ...association, source: { type: 'email' } } }); - - expect(res.status).to.be.equal(404); - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', 'Resource not found'); - }); - it("should return an error if user doesn't have 'view-livechat-contact' permission", async () => { await removePermissionFromAllRoles('view-livechat-contact'); @@ -835,21 +899,7 @@ describe('LIVECHAT - contacts', () => { it('should return an error if contactId and visitor association is missing', async () => { const res = await request.get(api(`omnichannel/contacts.get`)).set(credentials); - expectInvalidParams(res, [ - "must have required property 'contactId'", - "must have required property 'visitor'", - 'must match exactly one schema in oneOf [invalid-params]', - ]); - }); - - it('should return an error if more than one field is provided', async () => { - const res = await request.get(api(`omnichannel/contacts.get`)).set(credentials).query({ contactId, visitor: association }); - - expectInvalidParams(res, [ - 'must NOT have additional properties', - 'must NOT have additional properties', - 'must match exactly one schema in oneOf [invalid-params]', - ]); + expectInvalidParams(res, ["must have required property 'contactId' [invalid-params]"]); }); describe('Contact Channels', () => { @@ -950,6 +1000,121 @@ describe('LIVECHAT - contacts', () => { }); }); + describe('[GET] omnichannel/contacts.checkExistence', () => { + let contactId: string; + let roomId: string; + + const email = faker.internet.email().toLowerCase(); + const phone = faker.phone.number(); + + const contact = { + name: faker.person.fullName(), + emails: [email], + phones: [phone], + contactManager: agentUser?._id, + }; + + before(async () => { + await updatePermission('view-livechat-contact', ['admin']); + const { body } = await request + .post(api('omnichannel/contacts')) + .set(credentials) + .send({ ...contact }); + contactId = body.contactId; + + const visitor = await createVisitor(undefined, contact.name, email, phone); + + const room = await createLivechatRoom(visitor.token); + roomId = room._id; + }); + + after(async () => Promise.all([restorePermissionToRoles('view-livechat-contact'), closeOmnichannelRoom(roomId)])); + + it('should confirm a contact exists when checking by contact id', async () => { + const res = await request.get(api(`omnichannel/contacts.checkExistence`)).set(credentials).query({ contactId }); + + expect(res.status).to.be.equal(200); + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('exists', true); + }); + + it('should confirm a contact does not exist when checking by contact id', async () => { + const res = await request.get(api(`omnichannel/contacts.checkExistence`)).set(credentials).query({ contactId: 'invalid-contact-id' }); + + expect(res.status).to.be.equal(200); + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('exists', false); + }); + + it('should confirm a contact exists when checking by email', async () => { + const res = await request.get(api(`omnichannel/contacts.checkExistence`)).set(credentials).query({ email }); + + expect(res.status).to.be.equal(200); + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('exists', true); + }); + + it('should confirm a contact does not exist when checking by email', async () => { + const res = await request + .get(api(`omnichannel/contacts.checkExistence`)) + .set(credentials) + .query({ email: 'invalid-email@example.com' }); + + expect(res.status).to.be.equal(200); + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('exists', false); + }); + + it('should confirm a contact exists when checking by phone', async () => { + const res = await request.get(api(`omnichannel/contacts.checkExistence`)).set(credentials).query({ phone }); + + expect(res.status).to.be.equal(200); + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('exists', true); + }); + + it('should confirm a contact does not exist when checking by phone', async () => { + const res = await request.get(api(`omnichannel/contacts.checkExistence`)).set(credentials).query({ phone: 'invalid-phone' }); + + expect(res.status).to.be.equal(200); + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('exists', false); + }); + + it("should return an error if user doesn't have 'view-livechat-contact' permission", async () => { + await removePermissionFromAllRoles('view-livechat-contact'); + + const res = await request.get(api(`omnichannel/contacts.checkExistence`)).set(credentials).query({ contactId }); + + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('User does not have the permissions required for this action [error-unauthorized]'); + + await restorePermissionToRoles('view-livechat-contact'); + }); + + it('should return an error if all query params are missing', async () => { + const res = await request.get(api(`omnichannel/contacts.checkExistence`)).set(credentials); + + expectInvalidParams(res, [ + "must have required property 'contactId'", + "must have required property 'email'", + "must have required property 'phone'", + 'must match exactly one schema in oneOf [invalid-params]', + ]); + }); + + it('should return an error if more than one field is provided', async () => { + const res = await request.get(api(`omnichannel/contacts.checkExistence`)).set(credentials).query({ contactId, email, phone }); + + expectInvalidParams(res, [ + 'must NOT have additional properties', + 'must NOT have additional properties', + 'must NOT have additional properties', + 'must match exactly one schema in oneOf [invalid-params]', + ]); + }); + }); + describe('[GET] omnichannel/contacts.search', () => { let contactId: string; let visitor: ILivechatVisitor; @@ -1258,7 +1423,7 @@ describe('LIVECHAT - contacts', () => { expect(res.status).to.be.equal(200); expect(res.body).to.have.property('success', true); - const { body } = await request.get(api('omnichannel/contacts.get')).set(credentials).query({ visitor: association }); + const { body } = await request.get(api('omnichannel/contacts.get')).set(credentials).query({ contactId: room.contactId }); expect(body.contact.channels).to.be.an('array'); expect(body.contact.channels.length).to.be.equal(1); @@ -1388,7 +1553,7 @@ describe('LIVECHAT - contacts', () => { it('should be able to unblock a contact channel', async () => { await request.post(api('omnichannel/contacts.block')).set(credentials).send({ visitor: association }); - const { body } = await request.get(api('omnichannel/contacts.get')).set(credentials).query({ visitor: association }); + const { body } = await request.get(api('omnichannel/contacts.get')).set(credentials).query({ contactId: room.contactId }); expect(body.contact.channels).to.be.an('array'); expect(body.contact.channels.length).to.be.equal(1); @@ -1399,7 +1564,7 @@ describe('LIVECHAT - contacts', () => { expect(res.status).to.be.equal(200); expect(res.body).to.have.property('success', true); - const { body: body2 } = await request.get(api('omnichannel/contacts.get')).set(credentials).query({ visitor: association }); + const { body: body2 } = await request.get(api('omnichannel/contacts.get')).set(credentials).query({ contactId: room.contactId }); expect(body2.contact.channels).to.be.an('array'); expect(body2.contact.channels.length).to.be.equal(1); diff --git a/apps/meteor/tests/end-to-end/api/methods.ts b/apps/meteor/tests/end-to-end/api/methods.ts index e3e8a193d799..8e4950be5127 100644 --- a/apps/meteor/tests/end-to-end/api/methods.ts +++ b/apps/meteor/tests/end-to-end/api/methods.ts @@ -2082,6 +2082,32 @@ describe('Meteor.methods', () => { }) .end(done); }); + + it('should accept message sent by js.SDK', (done) => { + void request + .post(methodCall('sendMessage')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'sendMessage', + params: [{ rid, msg: 'test message', bot: { i: 'js.SDK' } }], + id: 1000, + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + + const data = JSON.parse(res.body.message); + + expect(data).to.have.a.property('result').that.is.an('object'); + expect(data.result).to.have.a.property('bot').that.is.an('object'); + expect(data.result.bot).to.have.a.property('i', 'js.SDK'); + }) + .end(done); + }); }); describe('[@updateMessage]', () => { diff --git a/apps/meteor/tests/end-to-end/api/rooms.ts b/apps/meteor/tests/end-to-end/api/rooms.ts index 110eb22a83de..7e4846717656 100644 --- a/apps/meteor/tests/end-to-end/api/rooms.ts +++ b/apps/meteor/tests/end-to-end/api/rooms.ts @@ -1255,21 +1255,7 @@ describe('[Rooms]', () => { let testChannel: IRoom; let testGroup: IRoom; let testDM: IRoom; - const expectedKeys = [ - '_id', - 'name', - 'fname', - 't', - 'msgs', - 'usersCount', - 'u', - 'customFields', - 'ts', - 'ro', - 'sysMes', - 'default', - '_updatedAt', - ]; + const expectedKeys = ['_id', 'name', 'fname', 't', 'msgs', 'usersCount', 'u', 'ts', 'ro', 'sysMes', 'default', '_updatedAt']; const testChannelName = `channel.test.${Date.now()}-${Math.random()}`; const testGroupName = `group.test.${Date.now()}-${Math.random()}`; let user: TestUser; diff --git a/apps/meteor/tests/end-to-end/apps/video-conferences.ts b/apps/meteor/tests/end-to-end/apps/video-conferences.ts index 230ee8e9f80c..0f54c05bdc89 100644 --- a/apps/meteor/tests/end-to-end/apps/video-conferences.ts +++ b/apps/meteor/tests/end-to-end/apps/video-conferences.ts @@ -581,6 +581,33 @@ describe('Apps - Video Conferences', () => { .that.satisfies((msg: string) => msg.includes('Chat History')); }); }); + + it('should have created a subscription with open = false', async function () { + if (!process.env.IS_EE) { + this.skip(); + return; + } + + await request + .get(api('subscriptions.getOne')) + .set(credentials) + .query({ + roomId: discussionRid, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('subscription').and.to.be.an('object'); + expect(res.body.subscription).to.have.a.property('rid').equal(discussionRid); + expect(res.body.subscription) + .to.have.a.property('fname') + .that.is.a('string') + .that.satisfies((msg: string) => !msg.startsWith('Chat History')) + .that.satisfies((msg: string) => msg.includes('Chat History')); + expect(res.body.subscription).to.have.a.property('open', false); + expect(res.body.subscription).to.have.a.property('alert', false); + }); + }); }); describe('[Persistent Chat provider with the persistent chat feature enabled and custom discussion names]', () => { diff --git a/apps/meteor/tests/unit/app/cloud/server/functions/syncWorkspace/syncCloudData.spec.ts b/apps/meteor/tests/unit/app/cloud/server/functions/syncWorkspace/syncCloudData.spec.ts new file mode 100644 index 000000000000..f17cb5986da8 --- /dev/null +++ b/apps/meteor/tests/unit/app/cloud/server/functions/syncWorkspace/syncCloudData.spec.ts @@ -0,0 +1,101 @@ +import { expect } from 'chai'; +import { describe, it, beforeEach } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const models = { + Settings: { updateValueById: sinon.stub() }, +}; + +const mockedFetchWorkspaceSyncPayload = sinon.stub(); + +const { syncCloudData } = proxyquire.noCallThru().load('../../../../../../../app/cloud/server/functions/syncWorkspace/syncCloudData.ts', { + '@rocket.chat/license': { DuplicatedLicenseError: sinon.stub() }, + '@rocket.chat/models': models, + '../../../../../lib/callbacks': { callbacks: { run: sinon.stub() } }, + '../../../../../lib/errors/CloudWorkspaceAccessError': { CloudWorkspaceAccessError: sinon.stub() }, + '../../../../../lib/errors/CloudWorkspaceRegistrationError': { CloudWorkspaceRegistrationError: sinon.stub() }, + '../../../../../server/lib/logger/system': { SystemLogger: { info: sinon.stub(), error: sinon.stub() } }, + '../buildRegistrationData': { buildWorkspaceRegistrationData: sinon.stub().resolves({}) }, + '../getWorkspaceAccessToken': { + getWorkspaceAccessToken: sinon.stub().resolves('token'), + CloudWorkspaceAccessTokenEmptyError: sinon.stub(), + }, + '../retrieveRegistrationStatus': { retrieveRegistrationStatus: sinon.stub().resolves({ workspaceRegistered: true }) }, + './fetchWorkspaceSyncPayload': { fetchWorkspaceSyncPayload: mockedFetchWorkspaceSyncPayload }, +}); + +describe('SyncCloudData', () => { + beforeEach(() => { + models.Settings.updateValueById.reset(); + mockedFetchWorkspaceSyncPayload.reset(); + }); + + it('should save cloudSyncAnnouncement payload on Cloud_Sync_Announcement_Payload setting when present', async () => { + const workspaceSyncPayloadResponse = { + workspaceId: 'workspaceId', + publicKey: 'publicKey', + license: {}, + removeLicense: false, + cloudSyncAnnouncement: { + viewId: 'subscription-announcement', + appId: 'cloud-announcements-core', + blocks: [ + { + type: 'callout', + title: { + type: 'plain_text', + text: 'Workspace eligible for Starter Plan', + }, + text: { + type: 'plain_text', + text: 'Get free access to premium capabilities for up to 50 users', + }, + accessory: { + type: 'button', + text: { + type: 'plain_text', + text: 'Switch Plan', + }, + actionId: 'callout-action', + appId: 'cloud-announcements-core', + blockId: 'section-button', + }, + }, + ], + }, + }; + + mockedFetchWorkspaceSyncPayload.resolves(workspaceSyncPayloadResponse); + + await syncCloudData(); + + expect(mockedFetchWorkspaceSyncPayload.calledOnce).to.be.true; + + expect( + models.Settings.updateValueById.calledOnceWith( + 'Cloud_Sync_Announcement_Payload', + JSON.stringify(workspaceSyncPayloadResponse.cloudSyncAnnouncement), + ), + ).to.be.true; + }); + + it("Should save as 'null' the setting update if cloudSyncAnnouncement is not present", async () => { + const workspaceSyncPayloadResponse = { + workspaceId: 'workspaceId', + publicKey: 'publicKey', + license: {}, + removeLicense: false, + }; + + mockedFetchWorkspaceSyncPayload.resolves(workspaceSyncPayloadResponse); + + await syncCloudData(); + + expect(mockedFetchWorkspaceSyncPayload.calledOnce).to.be.true; + + expect(models.Settings.updateValueById.calledOnce).to.be.true; + + expect(models.Settings.updateValueById.calledWith('Cloud_Sync_Announcement_Payload', 'null')).to.be.true; + }); +}); diff --git a/apps/meteor/tests/unit/server/federation/infrastructure/rocket-chat/hooks/hooks.spec.ts b/apps/meteor/tests/unit/server/federation/infrastructure/rocket-chat/hooks/hooks.spec.ts index 94d8fa26bd9c..be1a935eb06c 100644 --- a/apps/meteor/tests/unit/server/federation/infrastructure/rocket-chat/hooks/hooks.spec.ts +++ b/apps/meteor/tests/unit/server/federation/infrastructure/rocket-chat/hooks/hooks.spec.ts @@ -279,9 +279,18 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => { it('should execute the callback when everything is correct', () => { const stub = sinon.stub(); FederationHooks.canCreateDirectMessageFromUI(stub); + isFederationEnabled.returns(true); hooks['federation-v2-can-create-direct-message-from-ui-ce']([]); expect(stub.calledWith([])).to.be.true; }); + + it('should not execute callback or throw error when federation is disabled', () => { + const stub = sinon.stub(); + FederationHooks.canCreateDirectMessageFromUI(stub); + isFederationEnabled.returns(false); + hooks['federation-v2-can-create-direct-message-from-ui-ce']([]); + expect(stub.calledWith([])).to.be.false; + }); }); describe('#afterMessageReacted()', () => { diff --git a/apps/meteor/tests/unit/server/lib/freeswitch.tests.ts b/apps/meteor/tests/unit/server/lib/freeswitch.tests.ts index bb020995c7f8..c78568dcee7b 100644 --- a/apps/meteor/tests/unit/server/lib/freeswitch.tests.ts +++ b/apps/meteor/tests/unit/server/lib/freeswitch.tests.ts @@ -1,11 +1,13 @@ import { expect } from 'chai'; import { describe } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; -import { settings } from '../../../../app/settings/server/cached'; -import { VoipFreeSwitchService } from '../../../../ee/server/local-services/voip-freeswitch/service'; - -const VoipFreeSwitch = new VoipFreeSwitchService((id) => settings.get(id)); +const { VoipFreeSwitchService } = proxyquire.noCallThru().load('../../../../ee/server/local-services/voip-freeswitch/service', { + '../../../../app/settings/server': { get: sinon.stub() }, +}); +const VoipFreeSwitch = new VoipFreeSwitchService(); // Those tests still need a proper freeswitch environment configured in order to run // So for now they are being deliberately skipped on CI describe.skip('VoIP', () => { diff --git a/apps/meteor/tests/unit/server/services/omnichannel-analytics/AgentData.tests.ts b/apps/meteor/tests/unit/server/services/omnichannel-analytics/AgentData.tests.ts index 4c9320163774..7e3003332d50 100644 --- a/apps/meteor/tests/unit/server/services/omnichannel-analytics/AgentData.tests.ts +++ b/apps/meteor/tests/unit/server/services/omnichannel-analytics/AgentData.tests.ts @@ -735,7 +735,7 @@ describe('AgentData Analytics', () => { getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) { return [ { - servedBy: { + responseBy: { username: 'agent 1', }, metrics: { @@ -772,7 +772,7 @@ describe('AgentData Analytics', () => { getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) { return [ { - servedBy: { + responseBy: { username: 'agent 1', }, metrics: { @@ -782,7 +782,7 @@ describe('AgentData Analytics', () => { }, }, { - servedBy: { + responseBy: { username: 'agent 2', }, metrics: { @@ -818,12 +818,15 @@ describe('AgentData Analytics', () => { ], }); }); - it('should calculate correctly when agents have multiple conversations', async () => { + it('should associate average first response time with the agent who first responded to the room', async () => { const modelMock = { getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) { return [ { servedBy: { + username: 'agent 3', + }, + responseBy: { username: 'agent 1', }, metrics: { @@ -834,6 +837,9 @@ describe('AgentData Analytics', () => { }, { servedBy: { + username: 'agent 4', + }, + responseBy: { username: 'agent 2', }, metrics: { @@ -844,6 +850,9 @@ describe('AgentData Analytics', () => { }, { servedBy: { + username: 'agent 5', + }, + responseBy: { username: 'agent 1', }, metrics: { @@ -879,12 +888,73 @@ describe('AgentData Analytics', () => { ], }); }); - it('should ignore conversations not being served by any agent', async () => { + it('should calculate correctly when agents have multiple conversations', async () => { const modelMock = { getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) { return [ { - servedBy: undefined, + responseBy: { + username: 'agent 1', + }, + metrics: { + response: { + ft: 100, + }, + }, + }, + { + responseBy: { + username: 'agent 2', + }, + metrics: { + response: { + ft: 200, + }, + }, + }, + { + responseBy: { + username: 'agent 1', + }, + metrics: { + response: { + ft: 200, + }, + }, + }, + ]; + }, + }; + + const agentOverview = new AgentOverviewData(modelMock as any); + + const result = await agentOverview.Avg_first_response_time(moment(), moment(), 'departmentId'); + + expect(result).to.be.deep.equal({ + data: [ + { + name: 'agent 1', + value: '00:02:30', + }, + { + name: 'agent 2', + value: '00:03:20', + }, + ], + head: [ + { + name: 'Agent', + }, + { name: 'Avg_first_response_time' }, + ], + }); + }); + it('should ignore conversations not responded by any agent', async () => { + const modelMock = { + getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) { + return [ + { + responseBy: undefined, metrics: { response: { ft: 100, @@ -909,7 +979,7 @@ describe('AgentData Analytics', () => { ], }); }); - it('should ignore conversations with no metrics', async () => { + it('should ignore conversations served, but not responded by any agent', async () => { const modelMock = { getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) { return [ @@ -917,6 +987,39 @@ describe('AgentData Analytics', () => { servedBy: { username: 'agent 1', }, + responseBy: undefined, + metrics: { + response: { + ft: 100, + }, + }, + }, + ]; + }, + }; + + const agentOverview = new AgentOverviewData(modelMock as any); + + const result = await agentOverview.Avg_first_response_time(moment(), moment(), 'departmentId'); + + expect(result).to.be.deep.equal({ + data: [], + head: [ + { + name: 'Agent', + }, + { name: 'Avg_first_response_time' }, + ], + }); + }); + it('should ignore conversations with no metrics', async () => { + const modelMock = { + getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) { + return [ + { + responseBy: { + username: 'agent 1', + }, metrics: undefined, }, ]; @@ -966,7 +1069,7 @@ describe('AgentData Analytics', () => { getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) { return [ { - servedBy: { + responseBy: { username: 'agent 1', }, metrics: { @@ -976,7 +1079,7 @@ describe('AgentData Analytics', () => { }, }, { - servedBy: { + responseBy: { username: 'agent 2', }, metrics: { @@ -986,7 +1089,7 @@ describe('AgentData Analytics', () => { }, }, { - servedBy: { + responseBy: { username: 'agent 3', }, metrics: { @@ -996,7 +1099,7 @@ describe('AgentData Analytics', () => { }, }, { - servedBy: { + responseBy: { username: 'agent 4', }, metrics: { @@ -1006,7 +1109,7 @@ describe('AgentData Analytics', () => { }, }, { - servedBy: { + responseBy: { username: 'agent 5', }, metrics: { @@ -1016,9 +1119,116 @@ describe('AgentData Analytics', () => { }, }, { + responseBy: { + username: 'agent 6', + }, + metrics: { + response: { + ft: 300, + }, + }, + }, + ]; + }, + }; + + const agentOverview = new AgentOverviewData(modelMock as any); + + const result = await agentOverview.Best_first_response_time(moment(), moment(), 'departmentId'); + + expect(result).to.be.deep.equal({ + data: [ + { name: 'agent 1', value: '00:01:40' }, + { name: 'agent 2', value: '00:03:20' }, + { name: 'agent 3', value: '00:00:50' }, + { name: 'agent 4', value: '00:02:30' }, + { name: 'agent 5', value: '00:04:10' }, + { name: 'agent 6', value: '00:05:00' }, + ], + head: [ + { + name: 'Agent', + }, + { name: 'Best_first_response_time' }, + ], + }); + }); + it('should associate best first response time with the agent who first responded to the room', async () => { + const modelMock = { + getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) { + return [ + { + responseBy: { + username: 'agent 1', + }, + servedBy: { + username: 'agent 2', + }, + metrics: { + response: { + ft: 100, + }, + }, + }, + { + responseBy: { + username: 'agent 2', + }, + servedBy: { + username: 'agent 3', + }, + metrics: { + response: { + ft: 200, + }, + }, + }, + { + responseBy: { + username: 'agent 3', + }, + servedBy: { + username: 'agent 4', + }, + metrics: { + response: { + ft: 50, + }, + }, + }, + { + responseBy: { + username: 'agent 4', + }, servedBy: { + username: 'agent 5', + }, + metrics: { + response: { + ft: 150, + }, + }, + }, + { + responseBy: { + username: 'agent 5', + }, + servedBy: { + username: 'agent 6', + }, + metrics: { + response: { + ft: 250, + }, + }, + }, + { + responseBy: { username: 'agent 6', }, + servedBy: { + username: 'agent 7', + }, metrics: { response: { ft: 300, @@ -1055,7 +1265,7 @@ describe('AgentData Analytics', () => { getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) { return [ { - servedBy: { + responseBy: { username: 'agent 1', }, metrics: { @@ -1065,7 +1275,7 @@ describe('AgentData Analytics', () => { }, }, { - servedBy: { + responseBy: { username: 'agent 2', }, metrics: { @@ -1075,7 +1285,7 @@ describe('AgentData Analytics', () => { }, }, { - servedBy: { + responseBy: { username: 'agent 3', }, metrics: { @@ -1085,7 +1295,7 @@ describe('AgentData Analytics', () => { }, }, { - servedBy: { + responseBy: { username: 'agent 4', }, metrics: { @@ -1095,7 +1305,7 @@ describe('AgentData Analytics', () => { }, }, { - servedBy: { + responseBy: { username: 'agent 5', }, metrics: { @@ -1105,7 +1315,7 @@ describe('AgentData Analytics', () => { }, }, { - servedBy: { + responseBy: { username: 'agent 6', }, metrics: { @@ -1115,7 +1325,7 @@ describe('AgentData Analytics', () => { }, }, { - servedBy: { + responseBy: { username: 'agent 1', }, metrics: { @@ -1149,10 +1359,31 @@ describe('AgentData Analytics', () => { ], }); }); - it('should ignore conversations not being served by any agent', async () => { + it('should ignore conversations not responded by any agent', async () => { + const modelMock = { + getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) { + return [{ responseBy: undefined, metrics: { response: { ft: 100 } } }]; + }, + }; + + const agentOverview = new AgentOverviewData(modelMock as any); + + const result = await agentOverview.Best_first_response_time(moment(), moment(), 'departmentId'); + + expect(result).to.be.deep.equal({ + data: [], + head: [ + { + name: 'Agent', + }, + { name: 'Best_first_response_time' }, + ], + }); + }); + it('should ignore conversations served, but not responded by any agent', async () => { const modelMock = { getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) { - return [{ servedBy: undefined, metrics: { response: { ft: 100 } } }]; + return [{ servedBy: { username: 'agent1' }, responseBy: undefined, metrics: { response: { ft: 100 } } }]; }, }; @@ -1173,7 +1404,7 @@ describe('AgentData Analytics', () => { it('should ignore conversations with no metrics', async () => { const modelMock = { getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) { - return [{ servedBy: { username: 'agent 1' }, metrics: undefined }]; + return [{ responseBy: { username: 'agent 1' }, metrics: undefined }]; }, }; diff --git a/apps/uikit-playground/CHANGELOG.md b/apps/uikit-playground/CHANGELOG.md index 889b0060bb79..dae98761b306 100644 --- a/apps/uikit-playground/CHANGELOG.md +++ b/apps/uikit-playground/CHANGELOG.md @@ -1,5 +1,41 @@ # @rocket.chat/uikit-playground +## 0.6.1 + +### Patch Changes + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0]: + + - @rocket.chat/core-typings@7.1.0 + - @rocket.chat/fuselage-ui-kit@13.0.0 + - @rocket.chat/ui-contexts@13.0.0 + - @rocket.chat/ui-avatar@9.0.0 +
+ +## 0.6.1-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 + - @rocket.chat/fuselage-ui-kit@13.0.0-rc.3 + - @rocket.chat/ui-contexts@13.0.0-rc.3 + - @rocket.chat/ui-avatar@9.0.0-rc.3 +
+ +## 0.6.1-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 + - @rocket.chat/fuselage-ui-kit@13.0.0-rc.2 + - @rocket.chat/ui-contexts@13.0.0-rc.2 + - @rocket.chat/ui-avatar@9.0.0-rc.2 +
+ ## 0.6.1-rc.1 ### Patch Changes diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index b70bf3040f9e..faf20592b821 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/uikit-playground", "private": true, - "version": "0.6.1-rc.1", + "version": "0.6.1", "type": "module", "scripts": { "dev": "vite", @@ -17,14 +17,14 @@ "@lezer/highlight": "^1.2.1", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.59.4", - "@rocket.chat/fuselage-hooks": "^0.33.1", + "@rocket.chat/fuselage": "^0.60.0", + "@rocket.chat/fuselage-hooks": "^0.34.0", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/fuselage-toastbar": "^0.33.0", "@rocket.chat/fuselage-tokens": "^0.33.2", "@rocket.chat/fuselage-ui-kit": "workspace:~", "@rocket.chat/icons": "~0.39.0", - "@rocket.chat/logo": "^0.31.30", + "@rocket.chat/logo": "^0.31.31", "@rocket.chat/styled": "~0.31.25", "@rocket.chat/ui-avatar": "workspace:^", "@rocket.chat/ui-contexts": "workspace:~", diff --git a/ee/apps/account-service/CHANGELOG.md b/ee/apps/account-service/CHANGELOG.md index 06bfc2b86e72..1736734c0d6d 100644 --- a/ee/apps/account-service/CHANGELOG.md +++ b/ee/apps/account-service/CHANGELOG.md @@ -1,5 +1,49 @@ # @rocket.chat/account-service +## 0.4.10 + +### Patch Changes + +- ([#33596](https://github.com/RocketChat/Rocket.Chat/pull/33596)) Bump meteor to 3.0.4 and Node version to 20.18.0 + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, e7edeac3bdd22da0a04b8e873d5a008e249fb4be, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0, 63ccadc012499e004445ad6bc6cd2ff777aecbd1]: + + - @rocket.chat/model-typings@1.1.0 + - @rocket.chat/core-typings@7.1.0 + - @rocket.chat/rest-typings@7.1.0 + - @rocket.chat/core-services@0.7.2 + - @rocket.chat/models@1.0.1 + - @rocket.chat/network-broker@0.1.2 +
+ +## 0.4.10-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 + - @rocket.chat/rest-typings@7.1.0-rc.3 + - @rocket.chat/core-services@0.7.2-rc.3 + - @rocket.chat/model-typings@1.1.0-rc.3 + - @rocket.chat/network-broker@0.1.2-rc.3 + - @rocket.chat/models@1.0.1-rc.3 +
+ +## 0.4.10-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 + - @rocket.chat/rest-typings@7.1.0-rc.2 + - @rocket.chat/core-services@0.7.2-rc.2 + - @rocket.chat/model-typings@1.1.0-rc.2 + - @rocket.chat/network-broker@0.1.2-rc.2 + - @rocket.chat/models@1.0.1-rc.2 +
+ ## 0.4.10-rc.1 ### Patch Changes diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index 7a1f8e836a5d..da2122998c23 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/account-service", "private": true, - "version": "0.4.10-rc.1", + "version": "0.4.10", "description": "Rocket.Chat Account service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/authorization-service/CHANGELOG.md b/ee/apps/authorization-service/CHANGELOG.md index 4c07109801f5..41364ae5daf1 100644 --- a/ee/apps/authorization-service/CHANGELOG.md +++ b/ee/apps/authorization-service/CHANGELOG.md @@ -1,5 +1,49 @@ # @rocket.chat/authorization-service +## 0.4.10 + +### Patch Changes + +- ([#33596](https://github.com/RocketChat/Rocket.Chat/pull/33596)) Bump meteor to 3.0.4 and Node version to 20.18.0 + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, e7edeac3bdd22da0a04b8e873d5a008e249fb4be, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0, 63ccadc012499e004445ad6bc6cd2ff777aecbd1]: + + - @rocket.chat/model-typings@1.1.0 + - @rocket.chat/core-typings@7.1.0 + - @rocket.chat/rest-typings@7.1.0 + - @rocket.chat/core-services@0.7.2 + - @rocket.chat/models@1.0.1 + - @rocket.chat/network-broker@0.1.2 +
+ +## 0.4.10-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 + - @rocket.chat/rest-typings@7.1.0-rc.3 + - @rocket.chat/core-services@0.7.2-rc.3 + - @rocket.chat/model-typings@1.1.0-rc.3 + - @rocket.chat/network-broker@0.1.2-rc.3 + - @rocket.chat/models@1.0.1-rc.3 +
+ +## 0.4.10-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 + - @rocket.chat/rest-typings@7.1.0-rc.2 + - @rocket.chat/core-services@0.7.2-rc.2 + - @rocket.chat/model-typings@1.1.0-rc.2 + - @rocket.chat/network-broker@0.1.2-rc.2 + - @rocket.chat/models@1.0.1-rc.2 +
+ ## 0.4.10-rc.1 ### Patch Changes diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index 96c3e6dc3356..83719a245bf5 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/authorization-service", "private": true, - "version": "0.4.10-rc.1", + "version": "0.4.10", "description": "Rocket.Chat Authorization service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/ddp-streamer/CHANGELOG.md b/ee/apps/ddp-streamer/CHANGELOG.md index 4972f83e54a5..16ae9fb059df 100644 --- a/ee/apps/ddp-streamer/CHANGELOG.md +++ b/ee/apps/ddp-streamer/CHANGELOG.md @@ -1,5 +1,52 @@ # @rocket.chat/ddp-streamer +## 0.3.10 + +### Patch Changes + +- ([#33596](https://github.com/RocketChat/Rocket.Chat/pull/33596)) Bump meteor to 3.0.4 and Node version to 20.18.0 + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, e7edeac3bdd22da0a04b8e873d5a008e249fb4be, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0, 63ccadc012499e004445ad6bc6cd2ff777aecbd1]: + + - @rocket.chat/model-typings@1.1.0 + - @rocket.chat/core-typings@7.1.0 + - @rocket.chat/rest-typings@7.1.0 + - @rocket.chat/core-services@0.7.2 + - @rocket.chat/models@1.0.1 + - @rocket.chat/network-broker@0.1.2 + - @rocket.chat/instance-status@0.1.10 +
+ +## 0.3.10-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 + - @rocket.chat/rest-typings@7.1.0-rc.3 + - @rocket.chat/core-services@0.7.2-rc.3 + - @rocket.chat/model-typings@1.1.0-rc.3 + - @rocket.chat/network-broker@0.1.2-rc.3 + - @rocket.chat/models@1.0.1-rc.3 + - @rocket.chat/instance-status@0.1.10-rc.3 +
+ +## 0.3.10-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 + - @rocket.chat/rest-typings@7.1.0-rc.2 + - @rocket.chat/core-services@0.7.2-rc.2 + - @rocket.chat/model-typings@1.1.0-rc.2 + - @rocket.chat/network-broker@0.1.2-rc.2 + - @rocket.chat/models@1.0.1-rc.2 + - @rocket.chat/instance-status@0.1.10-rc.2 +
+ ## 0.3.10-rc.1 ### Patch Changes diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 5d7b66166be7..8f33978fae06 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/ddp-streamer", "private": true, - "version": "0.3.10-rc.1", + "version": "0.3.10", "description": "Rocket.Chat DDP-Streamer service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/omnichannel-transcript/CHANGELOG.md b/ee/apps/omnichannel-transcript/CHANGELOG.md index 5e8d2e7c990e..78edb8863a9b 100644 --- a/ee/apps/omnichannel-transcript/CHANGELOG.md +++ b/ee/apps/omnichannel-transcript/CHANGELOG.md @@ -1,5 +1,52 @@ # @rocket.chat/omnichannel-transcript +## 0.4.10 + +### Patch Changes + +- ([#33596](https://github.com/RocketChat/Rocket.Chat/pull/33596)) Bump meteor to 3.0.4 and Node version to 20.18.0 + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0, 63ccadc012499e004445ad6bc6cd2ff777aecbd1]: + + - @rocket.chat/model-typings@1.1.0 + - @rocket.chat/core-typings@7.1.0 + - @rocket.chat/core-services@0.7.2 + - @rocket.chat/omnichannel-services@0.3.7 + - @rocket.chat/models@1.0.1 + - @rocket.chat/pdf-worker@0.2.7 + - @rocket.chat/network-broker@0.1.2 +
+ +## 0.4.10-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 + - @rocket.chat/omnichannel-services@0.3.7-rc.3 + - @rocket.chat/pdf-worker@0.2.7-rc.3 + - @rocket.chat/core-services@0.7.2-rc.3 + - @rocket.chat/model-typings@1.1.0-rc.3 + - @rocket.chat/network-broker@0.1.2-rc.3 + - @rocket.chat/models@1.0.1-rc.3 +
+ +## 0.4.10-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 + - @rocket.chat/omnichannel-services@0.3.7-rc.2 + - @rocket.chat/pdf-worker@0.2.7-rc.2 + - @rocket.chat/core-services@0.7.2-rc.2 + - @rocket.chat/model-typings@1.1.0-rc.2 + - @rocket.chat/network-broker@0.1.2-rc.2 + - @rocket.chat/models@1.0.1-rc.2 +
+ ## 0.4.10-rc.1 ### Patch Changes diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index fcd0dabd28ce..4ecd44666cd5 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/omnichannel-transcript", "private": true, - "version": "0.4.10-rc.1", + "version": "0.4.10", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/presence-service/CHANGELOG.md b/ee/apps/presence-service/CHANGELOG.md index 6f1d3f2ba644..e71fe0c8fdaa 100644 --- a/ee/apps/presence-service/CHANGELOG.md +++ b/ee/apps/presence-service/CHANGELOG.md @@ -1,5 +1,49 @@ # @rocket.chat/presence-service +## 0.4.10 + +### Patch Changes + +- ([#33596](https://github.com/RocketChat/Rocket.Chat/pull/33596)) Bump meteor to 3.0.4 and Node version to 20.18.0 + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0, 63ccadc012499e004445ad6bc6cd2ff777aecbd1]: + + - @rocket.chat/model-typings@1.1.0 + - @rocket.chat/core-typings@7.1.0 + - @rocket.chat/core-services@0.7.2 + - @rocket.chat/presence@0.2.10 + - @rocket.chat/models@1.0.1 + - @rocket.chat/network-broker@0.1.2 +
+ +## 0.4.10-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 + - @rocket.chat/presence@0.2.10-rc.3 + - @rocket.chat/core-services@0.7.2-rc.3 + - @rocket.chat/model-typings@1.1.0-rc.3 + - @rocket.chat/network-broker@0.1.2-rc.3 + - @rocket.chat/models@1.0.1-rc.3 +
+ +## 0.4.10-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 + - @rocket.chat/presence@0.2.10-rc.2 + - @rocket.chat/core-services@0.7.2-rc.2 + - @rocket.chat/model-typings@1.1.0-rc.2 + - @rocket.chat/network-broker@0.1.2-rc.2 + - @rocket.chat/models@1.0.1-rc.2 +
+ ## 0.4.10-rc.1 ### Patch Changes diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index dfc94c4847ba..de78e2bd8275 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/presence-service", "private": true, - "version": "0.4.10-rc.1", + "version": "0.4.10", "description": "Rocket.Chat Presence service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/queue-worker/CHANGELOG.md b/ee/apps/queue-worker/CHANGELOG.md index fdf742e855c0..cf1ea684ed82 100644 --- a/ee/apps/queue-worker/CHANGELOG.md +++ b/ee/apps/queue-worker/CHANGELOG.md @@ -1,5 +1,49 @@ # @rocket.chat/queue-worker +## 0.4.10 + +### Patch Changes + +- ([#33596](https://github.com/RocketChat/Rocket.Chat/pull/33596)) Bump meteor to 3.0.4 and Node version to 20.18.0 + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0, 63ccadc012499e004445ad6bc6cd2ff777aecbd1]: + + - @rocket.chat/model-typings@1.1.0 + - @rocket.chat/core-typings@7.1.0 + - @rocket.chat/core-services@0.7.2 + - @rocket.chat/omnichannel-services@0.3.7 + - @rocket.chat/models@1.0.1 + - @rocket.chat/network-broker@0.1.2 +
+ +## 0.4.10-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 + - @rocket.chat/omnichannel-services@0.3.7-rc.3 + - @rocket.chat/core-services@0.7.2-rc.3 + - @rocket.chat/model-typings@1.1.0-rc.3 + - @rocket.chat/network-broker@0.1.2-rc.3 + - @rocket.chat/models@1.0.1-rc.3 +
+ +## 0.4.10-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 + - @rocket.chat/omnichannel-services@0.3.7-rc.2 + - @rocket.chat/core-services@0.7.2-rc.2 + - @rocket.chat/model-typings@1.1.0-rc.2 + - @rocket.chat/network-broker@0.1.2-rc.2 + - @rocket.chat/models@1.0.1-rc.2 +
+ ## 0.4.10-rc.1 ### Patch Changes diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index 4da5781f59ff..3282b96bcb31 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/queue-worker", "private": true, - "version": "0.4.10-rc.1", + "version": "0.4.10", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/stream-hub-service/CHANGELOG.md b/ee/apps/stream-hub-service/CHANGELOG.md index 54b6aea3c8eb..467beccf5c2c 100644 --- a/ee/apps/stream-hub-service/CHANGELOG.md +++ b/ee/apps/stream-hub-service/CHANGELOG.md @@ -1,5 +1,46 @@ # @rocket.chat/stream-hub-service +## 0.4.10 + +### Patch Changes + +- ([#33596](https://github.com/RocketChat/Rocket.Chat/pull/33596)) Bump meteor to 3.0.4 and Node version to 20.18.0 + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0, 63ccadc012499e004445ad6bc6cd2ff777aecbd1]: + + - @rocket.chat/model-typings@1.1.0 + - @rocket.chat/core-typings@7.1.0 + - @rocket.chat/core-services@0.7.2 + - @rocket.chat/models@1.0.1 + - @rocket.chat/network-broker@0.1.2 +
+ +## 0.4.10-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 + - @rocket.chat/core-services@0.7.2-rc.3 + - @rocket.chat/model-typings@1.1.0-rc.3 + - @rocket.chat/network-broker@0.1.2-rc.3 + - @rocket.chat/models@1.0.1-rc.3 +
+ +## 0.4.10-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 + - @rocket.chat/core-services@0.7.2-rc.2 + - @rocket.chat/model-typings@1.1.0-rc.2 + - @rocket.chat/network-broker@0.1.2-rc.2 + - @rocket.chat/models@1.0.1-rc.2 +
+ ## 0.4.10-rc.1 ### Patch Changes diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index 7b50b753265f..5eee33643175 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/stream-hub-service", "private": true, - "version": "0.4.10-rc.1", + "version": "0.4.10", "description": "Rocket.Chat Stream Hub service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/packages/license/CHANGELOG.md b/ee/packages/license/CHANGELOG.md index 4dc7b58ef73c..87dfce1f1adb 100644 --- a/ee/packages/license/CHANGELOG.md +++ b/ee/packages/license/CHANGELOG.md @@ -1,5 +1,32 @@ # @rocket.chat/license +## 1.0.1 + +### Patch Changes + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0]: + + - @rocket.chat/core-typings@7.1.0 +
+ +## 1.0.1-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 +
+ +## 1.0.1-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 +
+ ## 1.0.1-rc.1 ### Patch Changes diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 99a0bbd66904..aa65a0f95c89 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/license", - "version": "1.0.1-rc.1", + "version": "1.0.1", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/network-broker/CHANGELOG.md b/ee/packages/network-broker/CHANGELOG.md index 9ab4e7f6e31d..ab846bc6fce7 100644 --- a/ee/packages/network-broker/CHANGELOG.md +++ b/ee/packages/network-broker/CHANGELOG.md @@ -1,5 +1,32 @@ # @rocket.chat/network-broker +## 0.1.2 + +### Patch Changes + +-
Updated dependencies [63ccadc012499e004445ad6bc6cd2ff777aecbd1]: + + - @rocket.chat/core-services@0.7.2 +
+ +## 0.1.2-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-services@0.7.2-rc.3 +
+ +## 0.1.2-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-services@0.7.2-rc.2 +
+ ## 0.1.2-rc.1 ### Patch Changes diff --git a/ee/packages/network-broker/package.json b/ee/packages/network-broker/package.json index 56574af9480f..1d06b2c6c40c 100644 --- a/ee/packages/network-broker/package.json +++ b/ee/packages/network-broker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/network-broker", - "version": "0.1.2-rc.1", + "version": "0.1.2", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/omnichannel-services/CHANGELOG.md b/ee/packages/omnichannel-services/CHANGELOG.md index b2c482394fbd..e985e00a61b4 100644 --- a/ee/packages/omnichannel-services/CHANGELOG.md +++ b/ee/packages/omnichannel-services/CHANGELOG.md @@ -1,5 +1,47 @@ # @rocket.chat/omnichannel-services +## 0.3.7 + +### Patch Changes + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, e7edeac3bdd22da0a04b8e873d5a008e249fb4be, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0, 63ccadc012499e004445ad6bc6cd2ff777aecbd1]: + + - @rocket.chat/model-typings@1.1.0 + - @rocket.chat/core-typings@7.1.0 + - @rocket.chat/rest-typings@7.1.0 + - @rocket.chat/core-services@0.7.2 + - @rocket.chat/models@1.0.1 + - @rocket.chat/pdf-worker@0.2.7 +
+ +## 0.3.7-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 + - @rocket.chat/rest-typings@7.1.0-rc.3 + - @rocket.chat/pdf-worker@0.2.7-rc.3 + - @rocket.chat/core-services@0.7.2-rc.3 + - @rocket.chat/model-typings@1.1.0-rc.3 + - @rocket.chat/models@1.0.1-rc.3 +
+ +## 0.3.7-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 + - @rocket.chat/rest-typings@7.1.0-rc.2 + - @rocket.chat/pdf-worker@0.2.7-rc.2 + - @rocket.chat/core-services@0.7.2-rc.2 + - @rocket.chat/model-typings@1.1.0-rc.2 + - @rocket.chat/models@1.0.1-rc.2 +
+ ## 0.3.7-rc.1 ### Patch Changes diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index 0ca5d7bdbbaa..91c7f58a835d 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/omnichannel-services", - "version": "0.3.7-rc.1", + "version": "0.3.7", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/pdf-worker/CHANGELOG.md b/ee/packages/pdf-worker/CHANGELOG.md index f94999eea02c..b8fe3d5e6690 100644 --- a/ee/packages/pdf-worker/CHANGELOG.md +++ b/ee/packages/pdf-worker/CHANGELOG.md @@ -1,5 +1,32 @@ # @rocket.chat/pdf-worker +## 0.2.7 + +### Patch Changes + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0]: + + - @rocket.chat/core-typings@7.1.0 +
+ +## 0.2.7-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 +
+ +## 0.2.7-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 +
+ ## 0.2.7-rc.1 ### Patch Changes diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index ef4a22ea0b5c..fa252b5292c4 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/pdf-worker", - "version": "0.2.7-rc.1", + "version": "0.2.7", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/ee/packages/presence/CHANGELOG.md b/ee/packages/presence/CHANGELOG.md index 4b1bf78d0349..962455fe593f 100644 --- a/ee/packages/presence/CHANGELOG.md +++ b/ee/packages/presence/CHANGELOG.md @@ -1,5 +1,38 @@ # @rocket.chat/presence +## 0.2.10 + +### Patch Changes + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0, 63ccadc012499e004445ad6bc6cd2ff777aecbd1]: + + - @rocket.chat/core-typings@7.1.0 + - @rocket.chat/core-services@0.7.2 + - @rocket.chat/models@1.0.1 +
+ +## 0.2.10-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 + - @rocket.chat/core-services@0.7.2-rc.3 + - @rocket.chat/models@1.0.1-rc.3 +
+ +## 0.2.10-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 + - @rocket.chat/core-services@0.7.2-rc.2 + - @rocket.chat/models@1.0.1-rc.2 +
+ ## 0.2.10-rc.1 ### Patch Changes diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index 404d34ec117b..032f374cd43c 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/presence", - "version": "0.2.10-rc.1", + "version": "0.2.10", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index efc29260f7f1..65aa842ba39c 100644 --- a/ee/packages/ui-theming/package.json +++ b/ee/packages/ui-theming/package.json @@ -4,8 +4,8 @@ "private": true, "devDependencies": { "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.59.4", - "@rocket.chat/fuselage-hooks": "^0.33.1", + "@rocket.chat/fuselage": "^0.60.0", + "@rocket.chat/fuselage-hooks": "^0.34.0", "@rocket.chat/icons": "~0.39.0", "@rocket.chat/ui-contexts": "workspace:~", "@types/react": "~17.0.80", diff --git a/package.json b/package.json index 103433af1e4e..db4c79e048e0 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@types/chart.js": "^2.9.41", "@types/js-yaml": "^4.0.9", "ts-node": "^10.9.2", - "turbo": "^2.2.3" + "turbo": "^2.3.3" }, "workspaces": [ "apps/*", diff --git a/packages/api-client/CHANGELOG.md b/packages/api-client/CHANGELOG.md index 3f3e0aefef65..068b95d2ad08 100644 --- a/packages/api-client/CHANGELOG.md +++ b/packages/api-client/CHANGELOG.md @@ -1,5 +1,35 @@ # @rocket.chat/api-client +## 0.2.10 + +### Patch Changes + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, e7edeac3bdd22da0a04b8e873d5a008e249fb4be, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0]: + + - @rocket.chat/core-typings@7.1.0 + - @rocket.chat/rest-typings@7.1.0 +
+ +## 0.2.10-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 + - @rocket.chat/rest-typings@7.1.0-rc.3 +
+ +## 0.2.10-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 + - @rocket.chat/rest-typings@7.1.0-rc.2 +
+ ## 0.2.10-rc.1 ### Patch Changes diff --git a/packages/api-client/package.json b/packages/api-client/package.json index 6de3fa38b133..ccdc3e95feb7 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/api-client", - "version": "0.2.10-rc.1", + "version": "0.2.10", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/apps-engine/CHANGELOG.md b/packages/apps-engine/CHANGELOG.md index 3d661f0a3599..c5f48de76637 100644 --- a/packages/apps-engine/CHANGELOG.md +++ b/packages/apps-engine/CHANGELOG.md @@ -1,5 +1,31 @@ # @rocket.chat/apps-engine +## 1.48.0 + +### Minor Changes + +- ([#32727](https://github.com/RocketChat/Rocket.Chat/pull/32727)) These changes aims to add: + - A brand-new omnichannel contact profile + - The ability to communicate with known contacts only + - Communicate with verified contacts only + - Merge verified contacts across different channels + - Block contact channels + - Resolve conflicting contact information when registered via different channels + - An advanced contact center filters +- ([#33997](https://github.com/RocketChat/Rocket.Chat/pull/33997)) Prevent apps' subprocesses from crashing on unhandled rejections or uncaught exceptions + +- ([#33690](https://github.com/RocketChat/Rocket.Chat/pull/33690)) Add support to configure apps runtime timeout via the APPS_ENGINE_RUNTIME_TIMEOUT environment variable + +### Patch Changes + +- ([#33713](https://github.com/RocketChat/Rocket.Chat/pull/33713)) Deprecated the `from` field in the apps email bridge and made it optional, using the server's settings when the field is omitted + +- ([#33786](https://github.com/RocketChat/Rocket.Chat/pull/33786)) Fixed an issue that would grant network permission to app's processes in wrong cases + +- ([#33865](https://github.com/RocketChat/Rocket.Chat/pull/33865)) Fixes an issue that would cause apps to appear disabled after a subprocess restart + +- ([#33690](https://github.com/RocketChat/Rocket.Chat/pull/33690)) Removed the 1 second timeout of `Pre` app events. Now they will follow the "global" configuration + ## 1.48.0-rc.0 ### Minor Changes diff --git a/packages/apps-engine/deno-runtime/deno.lock b/packages/apps-engine/deno-runtime/deno.lock index 86cebf98f63a..1154e7709f11 100644 --- a/packages/apps-engine/deno-runtime/deno.lock +++ b/packages/apps-engine/deno-runtime/deno.lock @@ -90,18 +90,7 @@ "https://deno.land/std@0.203.0/testing/bdd.ts": "3f446df5ef8e856a869e8eec54c8482590415741ff0b6358a00c43486cc15769", "https://deno.land/std@0.203.0/testing/mock.ts": "6576b4aa55ee20b1990d656a78fff83599e190948c00e9f25a7f3ac5e9d6492d", "https://deno.land/std@0.216.0/io/types.ts": "748bbb3ac96abda03594ef5a0db15ce5450dcc6c0d841c8906f8b10ac8d32c96", - "https://deno.land/std@0.216.0/io/write_all.ts": "24aac2312bb21096ae3ae0b102b22c26164d3249dff96dbac130958aa736f038" - }, - "workspace": { - "dependencies": [ - "npm:@msgpack/msgpack@3.0.0-beta2", - "npm:@rocket.chat/ui-kit@^0.31.22", - "npm:acorn-walk@8.2.0", - "npm:acorn@8.10.0", - "npm:astring@1.8.6", - "npm:jsonrpc-lite@2.2.0", - "npm:stack-trace@0.0.10", - "npm:uuid@8.3.2" - ] + "https://deno.land/std@0.216.0/io/write_all.ts": "24aac2312bb21096ae3ae0b102b22c26164d3249dff96dbac130958aa736f038", + "https://jsr.io/@std/cli/1.0.9/parse_args.ts": "29ac18602d8836d2723cab1d90111ff954acc369f184626a3f9f677e3185caef" } } diff --git a/packages/apps-engine/deno-runtime/handlers/app/handler.ts b/packages/apps-engine/deno-runtime/handlers/app/handler.ts index 2a44f34cb7fe..141e145df971 100644 --- a/packages/apps-engine/deno-runtime/handlers/app/handler.ts +++ b/packages/apps-engine/deno-runtime/handlers/app/handler.ts @@ -19,41 +19,41 @@ import handleOnUpdate from './handleOnUpdate.ts'; export default async function handleApp(method: string, params: unknown): Promise { const [, appMethod] = method.split(':'); - // We don't want the getStatus method to generate logs, so we handle it separately - if (appMethod === 'getStatus') { - return handleGetStatus(); - } + try { + // We don't want the getStatus method to generate logs, so we handle it separately + if (appMethod === 'getStatus') { + return await handleGetStatus(); + } - // `app` will be undefined if the method here is "app:construct" - const app = AppObjectRegistry.get('app'); + // `app` will be undefined if the method here is "app:construct" + const app = AppObjectRegistry.get('app'); - app?.getLogger().debug(`'${appMethod}' is being called...`); + app?.getLogger().debug(`'${appMethod}' is being called...`); - if (uikitInteractions.includes(appMethod)) { - return handleUIKitInteraction(appMethod, params).then((result) => { - if (result instanceof JsonRpcError) { - app?.getLogger().debug(`'${appMethod}' was unsuccessful.`, result.message); - } else { - app?.getLogger().debug(`'${appMethod}' was successfully called! The result is:`, result); - } + if (uikitInteractions.includes(appMethod)) { + return handleUIKitInteraction(appMethod, params).then((result) => { + if (result instanceof JsonRpcError) { + app?.getLogger().debug(`'${appMethod}' was unsuccessful.`, result.message); + } else { + app?.getLogger().debug(`'${appMethod}' was successfully called! The result is:`, result); + } - return result; - }); - } + return result; + }); + } - if (appMethod.startsWith('check') || appMethod.startsWith('execute')) { - return handleListener(appMethod, params).then((result) => { - if (result instanceof JsonRpcError) { - app?.getLogger().debug(`'${appMethod}' was unsuccessful.`, result.message); - } else { - app?.getLogger().debug(`'${appMethod}' was successfully called! The result is:`, result); - } + if (appMethod.startsWith('check') || appMethod.startsWith('execute')) { + return handleListener(appMethod, params).then((result) => { + if (result instanceof JsonRpcError) { + app?.getLogger().debug(`'${appMethod}' was unsuccessful.`, result.message); + } else { + app?.getLogger().debug(`'${appMethod}' was successfully called! The result is:`, result); + } - return result; - }); - } + return result; + }); + } - try { let result: Defined | JsonRpcError; switch (appMethod) { diff --git a/packages/apps-engine/deno-runtime/lib/messenger.ts b/packages/apps-engine/deno-runtime/lib/messenger.ts index 1e9ffe05c6c5..5881d408c01c 100644 --- a/packages/apps-engine/deno-runtime/lib/messenger.ts +++ b/packages/apps-engine/deno-runtime/lib/messenger.ts @@ -59,6 +59,10 @@ export const Queue = new (class Queue { this.queue.push(encoder.encode(message)); this.processQueue(); } + + public getCurrentSize() { + return this.queue.length; + } }); export const Transport = new (class Transporter { diff --git a/packages/apps-engine/deno-runtime/lib/metricsCollector.ts b/packages/apps-engine/deno-runtime/lib/metricsCollector.ts new file mode 100644 index 000000000000..273ef2463d59 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/metricsCollector.ts @@ -0,0 +1,31 @@ +import { writeAll } from "https://deno.land/std@0.216.0/io/write_all.ts"; +import { Queue } from "./messenger.ts"; + +export function collectMetrics() { + return { + pid: Deno.pid, + queueSize: Queue.getCurrentSize(), + } +}; + +const encoder = new TextEncoder(); + +export async function sendMetrics() { + const metrics = collectMetrics(); + + await writeAll(Deno.stderr, encoder.encode(JSON.stringify(metrics))); +} + +let intervalId: number; + +export function startMetricsReport(frequencyInMs = 5000) { + if (intervalId) { + throw new Error('There is already an active metrics report'); + } + + intervalId = setInterval(sendMetrics, frequencyInMs); +} + +export function abortMetricsReport() { + clearInterval(intervalId); +} diff --git a/packages/apps-engine/deno-runtime/lib/parseArgs.ts b/packages/apps-engine/deno-runtime/lib/parseArgs.ts new file mode 100644 index 000000000000..10c59cbca3a7 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/parseArgs.ts @@ -0,0 +1,11 @@ +import { parseArgs as $parseArgs } from "https://jsr.io/@std/cli/1.0.9/parse_args.ts"; + +export type ParsedArgs = { + subprocess: string; + spawnId: number; + metricsReportFrequencyInMs?: number; +} + +export function parseArgs(args: string[]): ParsedArgs { + return $parseArgs(args); +} diff --git a/packages/apps-engine/deno-runtime/main.ts b/packages/apps-engine/deno-runtime/main.ts index fa2822908954..596128952168 100644 --- a/packages/apps-engine/deno-runtime/main.ts +++ b/packages/apps-engine/deno-runtime/main.ts @@ -22,6 +22,8 @@ import apiHandler from './handlers/api-handler.ts'; import handleApp from './handlers/app/handler.ts'; import handleScheduler from './handlers/scheduler-handler.ts'; import registerErrorListeners from './error-handlers.ts'; +import { startMetricsReport } from "./lib/metricsCollector.ts"; +import { parseArgs } from "./lib/parseArgs.ts"; type Handlers = { app: typeof handleApp; @@ -127,6 +129,10 @@ async function main() { } } +const mainArgs = parseArgs(Deno.args); + registerErrorListeners(); main(); + +startMetricsReport(mainArgs.metricsReportFrequencyInMs); diff --git a/packages/apps-engine/package.json b/packages/apps-engine/package.json index b7d1919b989b..31bf6ddc182c 100644 --- a/packages/apps-engine/package.json +++ b/packages/apps-engine/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps-engine", - "version": "1.48.0-rc.0", + "version": "1.48.0", "description": "The engine code for the Rocket.Chat Apps which manages, runs, translates, coordinates and all of that.", "main": "index", "typings": "index", diff --git a/packages/apps-engine/src/server/AppManager.ts b/packages/apps-engine/src/server/AppManager.ts index 0bb930b723a1..0ea7e998e995 100644 --- a/packages/apps-engine/src/server/AppManager.ts +++ b/packages/apps-engine/src/server/AppManager.ts @@ -271,7 +271,7 @@ export class AppManager { const prl = new ProxiedApp(this, item, { // Maybe we should have an "EmptyRuntime" class for this? getStatus() { - return AppStatus.COMPILER_ERROR_DISABLED; + return Promise.resolve(AppStatus.COMPILER_ERROR_DISABLED); }, } as unknown as DenoRuntimeSubprocessController); diff --git a/packages/apps-engine/src/server/ProxiedApp.ts b/packages/apps-engine/src/server/ProxiedApp.ts index 4307f9c9fc93..7810ab362422 100644 --- a/packages/apps-engine/src/server/ProxiedApp.ts +++ b/packages/apps-engine/src/server/ProxiedApp.ts @@ -1,5 +1,5 @@ import type { AppManager } from './AppManager'; -import type { AppStatus } from '../definition/AppStatus'; +import { AppStatus } from '../definition/AppStatus'; import { AppsEngineException } from '../definition/exceptions'; import type { IAppAuthorInfo, IAppInfo } from '../definition/metadata'; import { AppMethod } from '../definition/metadata'; @@ -79,7 +79,7 @@ export class ProxiedApp { } public async getStatus(): Promise { - return this.appRuntime.getStatus(); + return this.appRuntime.getStatus().catch(() => AppStatus.UNKNOWN); } public async setStatus(status: AppStatus, silent?: boolean): Promise { diff --git a/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts b/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts index 22608962bcf7..fec78b835984 100644 --- a/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts +++ b/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts @@ -88,6 +88,11 @@ export class DenoRuntimeSubprocessController extends EventEmitter { private state: 'uninitialized' | 'ready' | 'invalid' | 'restarting' | 'unknown' | 'stopped'; + /** + * Incremental id that keeps track of how many times we've spawned a process for this app + */ + private spawnId = 0; + private readonly debug: debug.Debugger; private readonly options = { @@ -149,6 +154,8 @@ export class DenoRuntimeSubprocessController extends EventEmitter { denoWrapperPath, '--subprocess', this.appPackage.info.id, + '--spawnId', + String(this.spawnId++), ]; // If the app doesn't request any permissions, it gets the default set of permissions, which includes "networking" @@ -295,7 +302,8 @@ export class DenoRuntimeSubprocessController extends EventEmitter { logger.info('Successfully restarted app subprocess'); } catch (e) { - logger.error("Failed to restart app's subprocess", { error: e }); + logger.error("Failed to restart app's subprocess", { error: e.message || e }); + throw e; } finally { await this.logStorage.storeEntries(AppConsole.toStorageEntry(this.getAppId(), logger)); } @@ -322,18 +330,24 @@ export class DenoRuntimeSubprocessController extends EventEmitter { } private waitUntilReady(): Promise { + if (this.state === 'ready') { + return; + } + return new Promise((resolve, reject) => { - const timeoutId = setTimeout(() => reject(new Error(`[${this.getAppId()}] Timeout: app process not ready`)), this.options.timeout); + let timeoutId: NodeJS.Timeout; - if (this.state === 'ready') { + const handler = () => { clearTimeout(timeoutId); - return resolve(); - } + resolve(); + }; - this.once('ready', () => { - clearTimeout(timeoutId); - return resolve(); - }); + timeoutId = setTimeout(() => { + this.off('ready', handler); + reject(new Error(`[${this.getAppId()}] Timeout: app process not ready`)); + }, this.options.timeout); + + this.once('ready', handler); }); } @@ -637,6 +651,12 @@ export class DenoRuntimeSubprocessController extends EventEmitter { } private async parseError(chunk: Buffer): Promise { - console.error('Subprocess stderr', chunk.toString()); + try { + const data = JSON.parse(chunk.toString()); + + this.debug('Metrics received from subprocess: %o', data); + } catch (e) { + console.error('Subprocess stderr', chunk.toString()); + } } } diff --git a/packages/apps-engine/src/server/runtime/deno/LivenessManager.ts b/packages/apps-engine/src/server/runtime/deno/LivenessManager.ts index 2450b3f2ad50..3f363c5402f1 100644 --- a/packages/apps-engine/src/server/runtime/deno/LivenessManager.ts +++ b/packages/apps-engine/src/server/runtime/deno/LivenessManager.ts @@ -7,10 +7,11 @@ import type { ProcessMessenger } from './ProcessMessenger'; const COMMAND_PING = '_zPING'; const defaultOptions: LivenessManager['options'] = { - pingRequestTimeout: 10000, + pingRequestTimeout: 1000, pingFrequencyInMS: 10000, consecutiveTimeoutLimit: 4, maxRestarts: Infinity, + restartAttemptDelayInMS: 1000, }; /** @@ -36,6 +37,9 @@ export class LivenessManager { // Limit of times we can try to restart a process maxRestarts: number; + + // Time to delay the next restart attempt after a failed one + restartAttemptDelayInMS: number; }; private subprocess: ChildProcess; @@ -82,6 +86,7 @@ export class LivenessManager { this.controller.once('ready', () => this.ping()); this.subprocess.once('exit', this.handleExit.bind(this)); + this.subprocess.once('error', this.handleError.bind(this)); } /** @@ -155,6 +160,11 @@ export class LivenessManager { this.messenger.send(COMMAND_PING); } + private handleError(err: Error) { + this.debug('App has failed to start.`', err); + this.restartProcess(err.message); + } + private handleExit(exitCode: number, signal: string) { this.pingAbortController.emit('abort'); @@ -178,15 +188,13 @@ export class LivenessManager { this.restartProcess(reason); } - private restartProcess(reason: string) { + private async restartProcess(reason: string) { if (this.restartCount >= this.options.maxRestarts) { this.debug('Limit of restarts reached (%d). Aborting restart...', this.options.maxRestarts); this.controller.stopApp(); return; } - this.pingTimeoutConsecutiveCount = 0; - this.restartCount++; this.restartLog.push({ reason, restartedAt: new Date(), @@ -194,6 +202,14 @@ export class LivenessManager { pid: this.subprocess.pid, }); - this.controller.restartApp(); + try { + await this.controller.restartApp(); + } catch (e) { + this.debug('Restart attempt failed. Retrying in %dms', this.options.restartAttemptDelayInMS); + setTimeout(() => this.restartProcess('Failed restart attempt'), this.options.restartAttemptDelayInMS); + } + + this.pingTimeoutConsecutiveCount = 0; + this.restartCount++; } } diff --git a/packages/apps-engine/src/server/runtime/deno/ProcessMessenger.ts b/packages/apps-engine/src/server/runtime/deno/ProcessMessenger.ts index 03d03d125323..c919adb5f0bb 100644 --- a/packages/apps-engine/src/server/runtime/deno/ProcessMessenger.ts +++ b/packages/apps-engine/src/server/runtime/deno/ProcessMessenger.ts @@ -1,11 +1,11 @@ -import { ChildProcess } from 'child_process'; +import type { ChildProcess } from 'child_process'; import type { JsonRpc } from 'jsonrpc-lite'; import { encoder } from './codec'; export class ProcessMessenger { - private deno: ChildProcess; + private deno: ChildProcess | undefined; private _sendStrategy: (message: JsonRpc) => void; @@ -30,7 +30,7 @@ export class ProcessMessenger { } private switchStrategy() { - if (this.deno instanceof ChildProcess) { + if (this.deno?.stdin?.writable) { this._sendStrategy = this.strategySend.bind(this); } else { this._sendStrategy = this.strategyError.bind(this); diff --git a/packages/apps/CHANGELOG.md b/packages/apps/CHANGELOG.md index 4f12ae2ae69f..f57df93ace27 100644 --- a/packages/apps/CHANGELOG.md +++ b/packages/apps/CHANGELOG.md @@ -1,5 +1,36 @@ # @rocket.chat/apps +## 0.2.1 + +### Patch Changes + +-
Updated dependencies [82767d8fd8a52ac348e8aded1d238e688d36129b, 80e36bfc3938775eb26aa5576f1b9b98896e1cc4, 3569b0a9c48f8b94ebaef2f8b607c52fdb8e570a, b4841cb7206d855d7a1bc7604683a5b4a48b7176, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0, ce7024af36fcde97b1da5b2731f6edc4a4c236b8, d398866dba725918017e3609807f9d0ab9b89b72, d398866dba725918017e3609807f9d0ab9b89b72]: + + - @rocket.chat/apps-engine@1.48.0 + - @rocket.chat/model-typings@1.1.0 + - @rocket.chat/core-typings@7.1.0 +
+ +## 0.2.1-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 + - @rocket.chat/model-typings@1.1.0-rc.3 +
+ +## 0.2.1-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 + - @rocket.chat/model-typings@1.1.0-rc.2 +
+ ## 0.2.1-rc.1 ### Patch Changes diff --git a/packages/apps/package.json b/packages/apps/package.json index ee079ad5f0d0..ebfa6e50b620 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps", - "version": "0.2.1-rc.1", + "version": "0.2.1", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/core-services/CHANGELOG.md b/packages/core-services/CHANGELOG.md index 222f45e54fb0..02ca0c091757 100644 --- a/packages/core-services/CHANGELOG.md +++ b/packages/core-services/CHANGELOG.md @@ -1,5 +1,40 @@ # @rocket.chat/core-services +## 0.7.2 + +### Patch Changes + +- ([#33719](https://github.com/RocketChat/Rocket.Chat/pull/33719)) stops calling an object through proxy calling getQueueWorker + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, e7edeac3bdd22da0a04b8e873d5a008e249fb4be, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0]: + + - @rocket.chat/core-typings@7.1.0 + - @rocket.chat/rest-typings@7.1.0 + - @rocket.chat/models@1.0.1 +
+ +## 0.7.2-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 + - @rocket.chat/rest-typings@7.1.0-rc.3 + - @rocket.chat/models@1.0.1-rc.3 +
+ +## 0.7.2-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 + - @rocket.chat/rest-typings@7.1.0-rc.2 + - @rocket.chat/models@1.0.1-rc.2 +
+ ## 0.7.2-rc.1 ### Patch Changes diff --git a/packages/core-services/package.json b/packages/core-services/package.json index b37ccdaab011..cdd8523ef524 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/core-services", - "version": "0.7.2-rc.1", + "version": "0.7.2", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/packages/core-services/src/types/IRoomService.ts b/packages/core-services/src/types/IRoomService.ts index 36bf5dff2564..1334bfce4041 100644 --- a/packages/core-services/src/types/IRoomService.ts +++ b/packages/core-services/src/types/IRoomService.ts @@ -38,6 +38,7 @@ export interface IRoomService { options?: { skipSystemMessage?: boolean; skipAlertSound?: boolean; + createAsHidden?: boolean; }, ): Promise; removeUserFromRoom(roomId: string, user: IUser, options?: { byUser: Pick }): Promise; diff --git a/packages/core-services/src/types/IVideoConfService.ts b/packages/core-services/src/types/IVideoConfService.ts index 4f007229b98d..3c21db2f677b 100644 --- a/packages/core-services/src/types/IVideoConfService.ts +++ b/packages/core-services/src/types/IVideoConfService.ts @@ -2,11 +2,13 @@ import type { IRoom, IStats, IUser, + IVoIPVideoConference, VideoConference, VideoConferenceCapabilities, VideoConferenceCreateData, VideoConferenceInstructions, } from '@rocket.chat/core-typings'; +import type { InsertionModel } from '@rocket.chat/model-typings'; import type { PaginatedResult } from '@rocket.chat/rest-typings'; import type * as UiKit from '@rocket.chat/ui-kit'; @@ -41,4 +43,5 @@ export interface IVideoConfService { params: { callId: VideoConference['_id']; uid: IUser['_id']; rid: IRoom['_id'] }, ): Promise; assignDiscussionToConference(callId: VideoConference['_id'], rid: IRoom['_id'] | undefined): Promise; + createVoIP(data: InsertionModel): Promise; } diff --git a/packages/core-typings/CHANGELOG.md b/packages/core-typings/CHANGELOG.md index 231e7b53607b..a52c22ec8782 100644 --- a/packages/core-typings/CHANGELOG.md +++ b/packages/core-typings/CHANGELOG.md @@ -1,5 +1,26 @@ # @rocket.chat/core-typings +## 7.1.0 + +### Minor Changes + +- ([#32727](https://github.com/RocketChat/Rocket.Chat/pull/32727)) These changes aims to add: + - A brand-new omnichannel contact profile + - The ability to communicate with known contacts only + - Communicate with verified contacts only + - Merge verified contacts across different channels + - Block contact channels + - Resolve conflicting contact information when registered via different channels + - An advanced contact center filters + +### Patch Changes + +- ([#32991](https://github.com/RocketChat/Rocket.Chat/pull/32991)) Fixes an issue where updating custom emojis didn’t work as expected, ensuring that uploaded emojis now update correctly and display without any caching problems. + +## 7.1.0-rc.3 + +## 7.1.0-rc.2 + ## 7.1.0-rc.1 ## 7.1.0-rc.0 diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index 6c5511966ac8..ab2eee721d46 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -200,7 +200,7 @@ export interface IMessage extends IRocketChatRecord { private?: boolean; /* @deprecated */ - bot?: boolean; + bot?: Record; sentByEmail?: boolean; webRtcCallEndTs?: Date; role?: string; diff --git a/packages/core-typings/src/IOembed.ts b/packages/core-typings/src/IOembed.ts index 0b781aa07fc8..f3eee3904260 100644 --- a/packages/core-typings/src/IOembed.ts +++ b/packages/core-typings/src/IOembed.ts @@ -16,7 +16,8 @@ export type OEmbedUrlContent = { export type OEmbedProvider = { urls: RegExp[]; - endPoint: string; + endPoint?: string; + getHeaderOverrides?: () => { [k: string]: string }; }; export type OEmbedUrlContentResult = { diff --git a/packages/core-typings/src/IStats.ts b/packages/core-typings/src/IStats.ts index 9aab9cd96f87..df179989de95 100644 --- a/packages/core-typings/src/IStats.ts +++ b/packages/core-typings/src/IStats.ts @@ -237,4 +237,20 @@ export interface IStats { webRTCEnabledForOmnichannel: boolean; omnichannelWebRTCCalls: number; statsToken?: string; + contactVerification: { + totalContacts: number; + totalUnknownContacts: number; + totalMergedContacts: number; + totalConflicts: number; + totalResolvedConflicts: number; + totalBlockedContacts: number; + totalPartiallyBlockedContacts: number; + totalFullyBlockedContacts: number; + totalVerifiedContacts: number; + avgChannelsPerContact: number; + totalContactsWithoutChannels: number; + totalImportedContacts: number; + totalUpsellViews: number; + totalUpsellClicks: number; + }; } diff --git a/packages/core-typings/src/IVideoConference.ts b/packages/core-typings/src/IVideoConference.ts index 334e1fd6a0e2..c3c084865156 100644 --- a/packages/core-typings/src/IVideoConference.ts +++ b/packages/core-typings/src/IVideoConference.ts @@ -29,7 +29,7 @@ export type LivechatInstructions = { callId: string; }; -export type VideoConferenceType = DirectCallInstructions['type'] | ConferenceInstructions['type'] | LivechatInstructions['type']; +export type VideoConferenceType = DirectCallInstructions['type'] | ConferenceInstructions['type'] | LivechatInstructions['type'] | 'voip'; export interface IVideoConferenceUser extends Pick, '_id' | 'username' | 'name' | 'avatarETag'> { ts: Date; @@ -73,7 +73,32 @@ export interface ILivechatVideoConference extends IVideoConference { type: 'livechat'; } -export type VideoConference = IDirectVideoConference | IGroupVideoConference | ILivechatVideoConference; +export interface IVoIPVideoConferenceData {} + +export type IVoIPVideoConference = IVideoConference & { + type: 'voip'; + externalId: string; + + callerExtension?: string; + calleeExtension?: string; + external?: boolean; + transferred?: boolean; + duration?: number; + + events: { + outgoing?: boolean; + hold?: boolean; + park?: boolean; + bridge?: boolean; + answer?: boolean; + }; +}; + +export type ExternalVideoConference = IDirectVideoConference | IGroupVideoConference | ILivechatVideoConference; + +export type InternalVideoConference = IVoIPVideoConference; + +export type VideoConference = ExternalVideoConference | InternalVideoConference; export type VideoConferenceInstructions = DirectCallInstructions | ConferenceInstructions | LivechatInstructions; @@ -89,11 +114,16 @@ export const isLivechatVideoConference = (call: VideoConference | undefined | nu return call?.type === 'livechat'; }; +export const isVoIPVideoConference = (call: VideoConference | undefined | null): call is IVoIPVideoConference => { + return call?.type === 'voip'; +}; + type GroupVideoConferenceCreateData = Omit & { createdBy: IUser['_id'] }; type DirectVideoConferenceCreateData = Omit & { createdBy: IUser['_id'] }; type LivechatVideoConferenceCreateData = Omit & { createdBy: IUser['_id'] }; +type VoIPVideoConferenceCreateData = Omit & { createdBy: IUser['_id'] }; export type VideoConferenceCreateData = AtLeast< - DirectVideoConferenceCreateData | GroupVideoConferenceCreateData | LivechatVideoConferenceCreateData, + DirectVideoConferenceCreateData | GroupVideoConferenceCreateData | LivechatVideoConferenceCreateData | VoIPVideoConferenceCreateData, 'createdBy' | 'type' | 'rid' | 'providerName' | 'providerData' >; diff --git a/packages/core-typings/src/cloud/CloudSyncAnnouncement.ts b/packages/core-typings/src/cloud/CloudSyncAnnouncement.ts new file mode 100644 index 000000000000..96856538ff72 --- /dev/null +++ b/packages/core-typings/src/cloud/CloudSyncAnnouncement.ts @@ -0,0 +1,10 @@ +import type { CalloutBlock, ContextBlock, DividerBlock, ImageBlock, SectionBlock } from '@rocket.chat/ui-kit'; + +type CloudSyncAnnouncementLayoutBlock = ContextBlock | DividerBlock | ImageBlock | SectionBlock | CalloutBlock; +type CloudSyncAnnouncementLayout = CloudSyncAnnouncementLayoutBlock[]; + +export interface ICloudSyncAnnouncement { + viewId: string; + appId: string; + blocks: CloudSyncAnnouncementLayout; +} diff --git a/packages/core-typings/src/cloud/WorkspaceSyncPayload.ts b/packages/core-typings/src/cloud/WorkspaceSyncPayload.ts index 1a3e6c12cf5e..e8821e93ac13 100644 --- a/packages/core-typings/src/cloud/WorkspaceSyncPayload.ts +++ b/packages/core-typings/src/cloud/WorkspaceSyncPayload.ts @@ -49,6 +49,7 @@ export interface WorkspaceSyncResponse { publicKey: string; license: unknown; removeLicense?: boolean; + cloudSyncAnnouncement: unknown; } export interface WorkspaceCommsRequestPayload { diff --git a/packages/core-typings/src/cloud/index.ts b/packages/core-typings/src/cloud/index.ts index 8232ffce267f..63efbd0bcf18 100644 --- a/packages/core-typings/src/cloud/index.ts +++ b/packages/core-typings/src/cloud/index.ts @@ -9,3 +9,4 @@ export { WorkspaceCommsResponsePayload, WorkspaceInteractionResponsePayload, } from './WorkspaceSyncPayload'; +export { ICloudSyncAnnouncement } from './CloudSyncAnnouncement'; diff --git a/packages/core-typings/src/license/LicenseInfo.ts b/packages/core-typings/src/license/LicenseInfo.ts index 85e7c3fba506..855602283cd3 100644 --- a/packages/core-typings/src/license/LicenseInfo.ts +++ b/packages/core-typings/src/license/LicenseInfo.ts @@ -1,6 +1,7 @@ import type { ILicenseTag } from './ILicenseTag'; import type { ExternalModule, ILicenseV3, LicenseLimitKind } from './ILicenseV3'; import type { LicenseModule } from './LicenseModule'; +import type { ICloudSyncAnnouncement } from '../cloud'; export type LicenseInfo = { license?: ILicenseV3; @@ -10,4 +11,5 @@ export type LicenseInfo = { limits: Record; tags: ILicenseTag[]; trial: boolean; + cloudSyncAnnouncement?: ICloudSyncAnnouncement; }; diff --git a/packages/core-typings/src/utils.ts b/packages/core-typings/src/utils.ts index c00d8c3f5a7d..2e20ebc48c84 100644 --- a/packages/core-typings/src/utils.ts +++ b/packages/core-typings/src/utils.ts @@ -40,5 +40,11 @@ export type ValueOfUnion> = T extends any ? (K extends export type ValueOfOptional> = T extends undefined ? undefined : T extends object ? ValueOfUnion : null; export type DeepPartial = { - [P in keyof T]?: T[P] extends (infer U)[] ? DeepPartial[] : T[P] extends object | undefined ? DeepPartial : T[P]; + [P in keyof T]?: T[P] extends (infer U)[] | undefined + ? DeepPartial[] + : T[P] extends Date | undefined + ? T[P] + : T[P] extends object | undefined + ? DeepPartial + : T[P]; }; diff --git a/packages/core-typings/src/voip/IFreeSwitchCall.ts b/packages/core-typings/src/voip/IFreeSwitchCall.ts new file mode 100644 index 000000000000..b0f8043f78fc --- /dev/null +++ b/packages/core-typings/src/voip/IFreeSwitchCall.ts @@ -0,0 +1,64 @@ +import type { IRocketChatRecord } from '../IRocketChatRecord'; +import type { IUser } from '../IUser'; +import type { IFreeSwitchEventCall, IFreeSwitchEventCaller } from './IFreeSwitchEvent'; + +export interface IFreeSwitchCall extends IRocketChatRecord { + UUID: string; + channels: string[]; + events: IFreeSwitchCallEvent[]; + from?: Pick; + to?: Pick; + forwardedFrom?: Omit[]; + direction?: 'internal' | 'external_inbound' | 'external_outbound'; + voicemail?: boolean; + duration?: number; +} + +const knownEventTypes = [ + 'NEW', + 'INIT', + 'CREATE', + 'DESTROY', + 'ANSWER', + 'HANGUP', + 'BRIDGE', + 'UNBRIDGE', + 'OUTGOING', + 'PARK', + 'UNPARK', + 'HOLD', + 'UNHOLD', + 'ORIGINATE', + 'UUID', + 'REPORTING', + 'ROUTING', + 'RINGING', + 'ACTIVE', + 'EARLY', + 'RING_WAIT', + 'EXECUTE', + 'CONSUME_MEDIA', + 'EXCHANGE_MEDIA', + 'OTHER', + 'OTHER_STATE', + 'OTHER_CALL_STATE', +] as const; + +export type IFreeSwitchCallEventType = (typeof knownEventTypes)[number]; + +export const isKnownFreeSwitchEventType = (eventName: string): eventName is IFreeSwitchCallEventType => + knownEventTypes.includes(eventName as any); + +export type IFreeSwitchCallEvent = { + eventName: string; + type: IFreeSwitchCallEventType; + sequence?: string; + channelUniqueId?: string; + timestamp?: string; + firedAt?: Date; + caller?: IFreeSwitchEventCaller; + call?: IFreeSwitchEventCall; + + otherType?: string; + otherChannelId?: string; +}; diff --git a/packages/core-typings/src/voip/IFreeSwitchEvent.ts b/packages/core-typings/src/voip/IFreeSwitchEvent.ts new file mode 100644 index 000000000000..a1cc3e7eafe9 --- /dev/null +++ b/packages/core-typings/src/voip/IFreeSwitchEvent.ts @@ -0,0 +1,113 @@ +import type { IRocketChatRecord } from '../IRocketChatRecord'; + +export interface IFreeSwitchEvent extends IRocketChatRecord { + channelUniqueId?: string; + eventName: string; + detaildEventName: string; + + sequence?: string; + state?: string; + previousCallState?: string; + callState?: string; + timestamp?: string; + + firedAt?: Date; + answerState?: string; + hangupCause?: string; + + referencedIds?: string[]; + receivedAt?: Date; + + channelName?: string; + direction?: string; + + caller?: IFreeSwitchEventCaller; + call?: IFreeSwitchEventCall; + + eventData: Record; +} + +export interface IFreeSwitchEventCall { + UUID?: string; + answerState?: string; + state?: string; + previousState?: string; + presenceId?: string; + sipId?: string; + authorized?: string; + hangupCause?: string; + duration?: number; + + from?: { + user?: string; + stripped?: string; + port?: string; + uri?: string; + host?: string; + full?: string; + + userId?: string; + }; + + req?: { + user?: string; + port?: string; + uri?: string; + host?: string; + + userId?: string; + }; + + to?: { + user?: string; + port?: string; + uri?: string; + full?: string; + dialedExtension?: string; + dialedUser?: string; + + userId?: string; + }; + + contact?: { + user?: string; + uri?: string; + host?: string; + + userId?: string; + }; + + via?: { + full?: string; + host?: string; + rport?: string; + + userId?: string; + }; +} + +export interface IFreeSwitchEventCaller { + uniqueId?: string; + direction?: string; + username?: string; + networkAddr?: string; + ani?: string; + destinationNumber?: string; + source?: string; + context?: string; + name?: string; + number?: string; + + originalCaller?: { + name?: string; + number?: string; + }; + privacy?: { + hideName?: string; + hideNumber?: string; + }; + channel?: { + name?: string; + createdTime?: string; + }; +} diff --git a/packages/core-typings/src/voip/index.ts b/packages/core-typings/src/voip/index.ts index 0a83a01d70bc..edede37e6cdc 100644 --- a/packages/core-typings/src/voip/index.ts +++ b/packages/core-typings/src/voip/index.ts @@ -17,3 +17,5 @@ export * from './IVoipClientEvents'; export * from './VoIPUserConfiguration'; export * from './VoIpCallerInfo'; export * from './ICallDetails'; +export * from './IFreeSwitchCall'; +export * from './IFreeSwitchEvent'; diff --git a/packages/cron/CHANGELOG.md b/packages/cron/CHANGELOG.md index a8cd3e909d9b..07d6c9fc604a 100644 --- a/packages/cron/CHANGELOG.md +++ b/packages/cron/CHANGELOG.md @@ -1,5 +1,35 @@ # @rocket.chat/cron +## 0.1.10 + +### Patch Changes + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0]: + + - @rocket.chat/core-typings@7.1.0 + - @rocket.chat/models@1.0.1 +
+ +## 0.1.10-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 + - @rocket.chat/models@1.0.1-rc.3 +
+ +## 0.1.10-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 + - @rocket.chat/models@1.0.1-rc.2 +
+ ## 0.1.10-rc.1 ### Patch Changes diff --git a/packages/cron/package.json b/packages/cron/package.json index 5462a7b327bf..13b695799920 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/cron", - "version": "0.1.10-rc.1", + "version": "0.1.10", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/ddp-client/CHANGELOG.md b/packages/ddp-client/CHANGELOG.md index edbfcf320efb..100f96a24433 100644 --- a/packages/ddp-client/CHANGELOG.md +++ b/packages/ddp-client/CHANGELOG.md @@ -1,5 +1,38 @@ # @rocket.chat/ddp-client +## 0.3.10 + +### Patch Changes + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, e7edeac3bdd22da0a04b8e873d5a008e249fb4be, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0]: + + - @rocket.chat/core-typings@7.1.0 + - @rocket.chat/rest-typings@7.1.0 + - @rocket.chat/api-client@0.2.10 +
+ +## 0.3.10-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 + - @rocket.chat/rest-typings@7.1.0-rc.3 + - @rocket.chat/api-client@0.2.10-rc.3 +
+ +## 0.3.10-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 + - @rocket.chat/rest-typings@7.1.0-rc.2 + - @rocket.chat/api-client@0.2.10-rc.2 +
+ ## 0.3.10-rc.1 ### Patch Changes diff --git a/packages/ddp-client/package.json b/packages/ddp-client/package.json index c1e705f9d1d2..0163b61b51e0 100644 --- a/packages/ddp-client/package.json +++ b/packages/ddp-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ddp-client", - "version": "0.3.10-rc.1", + "version": "0.3.10", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/freeswitch/CHANGELOG.md b/packages/freeswitch/CHANGELOG.md index 2b43848eae79..c1e167da4429 100644 --- a/packages/freeswitch/CHANGELOG.md +++ b/packages/freeswitch/CHANGELOG.md @@ -1,5 +1,32 @@ # @rocket.chat/freeswitch +## 1.0.1 + +### Patch Changes + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0]: + + - @rocket.chat/core-typings@7.1.0 +
+ +## 1.0.1-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 +
+ +## 1.0.1-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 +
+ ## 1.0.1-rc.1 ### Patch Changes diff --git a/packages/freeswitch/package.json b/packages/freeswitch/package.json index d75579e49fd5..d83f68e9c909 100644 --- a/packages/freeswitch/package.json +++ b/packages/freeswitch/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/freeswitch", - "version": "1.0.1-rc.1", + "version": "1.0.1", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/freeswitch/src/connect.ts b/packages/freeswitch/src/connect.ts index 6ea3741edc42..2d6d74295af1 100644 --- a/packages/freeswitch/src/connect.ts +++ b/packages/freeswitch/src/connect.ts @@ -6,7 +6,12 @@ import { logger } from './logger'; const defaultPassword = 'ClueCon'; -export async function connect(options?: { host?: string; port?: number; password?: string }): Promise { +export type EventNames = Parameters; + +export async function connect( + options?: { host?: string; port?: number; password?: string }, + customEventNames: EventNames = [], +): Promise { const host = options?.host ?? '127.0.0.1'; const port = options?.port ?? 8021; const password = options?.password ?? defaultPassword; @@ -26,7 +31,7 @@ export async function connect(options?: { host?: string; port?: number; password await currentCall.onceAsync('freeswitch_auth_request', 20_000, 'FreeSwitchClient expected authentication request'); await currentCall.auth(password); currentCall.auto_cleanup(); - await currentCall.event_json('CHANNEL_EXECUTE_COMPLETE', 'BACKGROUND_JOB'); + await currentCall.event_json('CHANNEL_EXECUTE_COMPLETE', 'BACKGROUND_JOB', ...customEventNames); } catch (error) { logger.error('FreeSwitchClient: connect error', error); reject(error); diff --git a/packages/freeswitch/src/index.ts b/packages/freeswitch/src/index.ts index 30272ff42df9..6248f38c97d5 100644 --- a/packages/freeswitch/src/index.ts +++ b/packages/freeswitch/src/index.ts @@ -1 +1,2 @@ export * from './commands'; +export * from './listenToEvents'; diff --git a/packages/freeswitch/src/listenToEvents.ts b/packages/freeswitch/src/listenToEvents.ts new file mode 100644 index 000000000000..c108a9890baa --- /dev/null +++ b/packages/freeswitch/src/listenToEvents.ts @@ -0,0 +1,37 @@ +import type { FreeSwitchResponse } from 'esl'; + +import { connect, type EventNames } from './connect'; + +export async function listenToEvents( + callback: (eventName: string, data: Record) => Promise, + options?: { host?: string; port?: number; password?: string }, +): Promise { + const eventsToListen: EventNames = [ + 'CHANNEL_CALLSTATE', + 'CHANNEL_STATE', + 'CHANNEL_CREATE', + 'CHANNEL_DESTROY', + 'CHANNEL_ANSWER', + 'CHANNEL_HANGUP', + 'CHANNEL_HANGUP_COMPLETE', + 'CHANNEL_BRIDGE', + 'CHANNEL_UNBRIDGE', + 'CHANNEL_OUTGOING', + 'CHANNEL_PARK', + 'CHANNEL_UNPARK', + 'CHANNEL_HOLD', + 'CHANNEL_UNHOLD', + 'CHANNEL_ORIGINATE', + 'CHANNEL_UUID', + ]; + + const connection = await connect(options, eventsToListen); + + eventsToListen.forEach((eventName) => + connection.on(eventName, (event) => { + callback(eventName, event.body); + }), + ); + + return connection; +} diff --git a/packages/fuselage-ui-kit/CHANGELOG.md b/packages/fuselage-ui-kit/CHANGELOG.md index 4d33ff306ad7..3da9a288d5b3 100644 --- a/packages/fuselage-ui-kit/CHANGELOG.md +++ b/packages/fuselage-ui-kit/CHANGELOG.md @@ -1,5 +1,45 @@ # Change Log +## 13.0.0 + +### Patch Changes + +-
Updated dependencies [82767d8fd8a52ac348e8aded1d238e688d36129b, 80e36bfc3938775eb26aa5576f1b9b98896e1cc4, 3569b0a9c48f8b94ebaef2f8b607c52fdb8e570a, b4841cb7206d855d7a1bc7604683a5b4a48b7176, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0, ce7024af36fcde97b1da5b2731f6edc4a4c236b8, d398866dba725918017e3609807f9d0ab9b89b72, d398866dba725918017e3609807f9d0ab9b89b72]: + + - @rocket.chat/apps-engine@1.48.0 + - @rocket.chat/core-typings@7.1.0 + - @rocket.chat/gazzodown@13.0.0 + - @rocket.chat/ui-contexts@13.0.0 + - @rocket.chat/ui-avatar@9.0.0 + - @rocket.chat/ui-video-conf@13.0.0 +
+ +## 13.0.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 + - @rocket.chat/gazzodown@13.0.0-rc.3 + - @rocket.chat/ui-contexts@13.0.0-rc.3 + - @rocket.chat/ui-avatar@9.0.0-rc.3 + - @rocket.chat/ui-video-conf@13.0.0-rc.3 +
+ +## 13.0.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 + - @rocket.chat/gazzodown@13.0.0-rc.2 + - @rocket.chat/ui-contexts@13.0.0-rc.2 + - @rocket.chat/ui-avatar@9.0.0-rc.2 + - @rocket.chat/ui-video-conf@13.0.0-rc.2 +
+ ## 13.0.0-rc.1 ### Patch Changes diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index d3d5bc4e53fa..86000b6b877f 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/fuselage-ui-kit", - "version": "13.0.0-rc.1", + "version": "13.0.0", "private": true, "description": "UiKit elements for Rocket.Chat Apps built under Fuselage design system", "homepage": "https://rocketchat.github.io/Rocket.Chat.Fuselage/", @@ -52,8 +52,8 @@ "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.59.4", - "@rocket.chat/fuselage-hooks": "^0.33.1", + "@rocket.chat/fuselage": "^0.60.0", + "@rocket.chat/fuselage-hooks": "^0.34.0", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/icons": "~0.39.0", "@rocket.chat/jest-presets": "workspace:~", @@ -93,7 +93,7 @@ "typescript": "~5.7.2" }, "peerDependencies": { - "@rocket.chat/apps-engine": "1.48.0-rc.0", + "@rocket.chat/apps-engine": "1.48.0", "@rocket.chat/eslint-config": "0.7.0", "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", @@ -101,10 +101,10 @@ "@rocket.chat/icons": "*", "@rocket.chat/prettier-config": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-avatar": "9.0.0-rc.1", - "@rocket.chat/ui-contexts": "13.0.0-rc.1", + "@rocket.chat/ui-avatar": "9.0.0", + "@rocket.chat/ui-contexts": "13.0.0", "@rocket.chat/ui-kit": "0.37.0", - "@rocket.chat/ui-video-conf": "13.0.0-rc.1", + "@rocket.chat/ui-video-conf": "13.0.0", "@tanstack/react-query": "*", "react": "~17.0.2", "react-dom": "*" diff --git a/packages/gazzodown/CHANGELOG.md b/packages/gazzodown/CHANGELOG.md index be29cf95ebc7..707ff34838c5 100644 --- a/packages/gazzodown/CHANGELOG.md +++ b/packages/gazzodown/CHANGELOG.md @@ -1,5 +1,38 @@ # @rocket.chat/gazzodown +## 13.0.0 + +### Patch Changes + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0]: + + - @rocket.chat/core-typings@7.1.0 + - @rocket.chat/ui-contexts@13.0.0 + - @rocket.chat/ui-client@13.0.0 +
+ +## 13.0.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 + - @rocket.chat/ui-contexts@13.0.0-rc.3 + - @rocket.chat/ui-client@13.0.0-rc.3 +
+ +## 13.0.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 + - @rocket.chat/ui-contexts@13.0.0-rc.2 + - @rocket.chat/ui-client@13.0.0-rc.2 +
+ ## 13.0.0-rc.1 ### Patch Changes diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index d494a478ac8c..c63a256bc054 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/gazzodown", - "version": "13.0.0-rc.1", + "version": "13.0.0", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", @@ -29,7 +29,7 @@ "@babel/core": "~7.26.0", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.59.4", + "@rocket.chat/fuselage": "^0.60.0", "@rocket.chat/fuselage-tokens": "^0.33.2", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/message-parser": "workspace:^", @@ -74,8 +74,8 @@ "@rocket.chat/fuselage-tokens": "*", "@rocket.chat/message-parser": "0.31.31", "@rocket.chat/styled": "*", - "@rocket.chat/ui-client": "13.0.0-rc.1", - "@rocket.chat/ui-contexts": "13.0.0-rc.1", + "@rocket.chat/ui-client": "13.0.0", + "@rocket.chat/ui-contexts": "13.0.0", "katex": "*", "react": "*" }, diff --git a/packages/i18n/CHANGELOG.md b/packages/i18n/CHANGELOG.md index 05fa9e6037d0..6a4bec0347d8 100644 --- a/packages/i18n/CHANGELOG.md +++ b/packages/i18n/CHANGELOG.md @@ -1,5 +1,35 @@ # @rocket.chat/i18n +## 1.1.0 + +### Minor Changes + +- ([#32906](https://github.com/RocketChat/Rocket.Chat/pull/32906)) Improves thread metrics featuring user avatars, better titles and repositioned elements. + +- ([#32727](https://github.com/RocketChat/Rocket.Chat/pull/32727)) These changes aims to add: + - A brand-new omnichannel contact profile + - The ability to communicate with known contacts only + - Communicate with verified contacts only + - Merge verified contacts across different channels + - Block contact channels + - Resolve conflicting contact information when registered via different channels + - An advanced contact center filters +- ([#33920](https://github.com/RocketChat/Rocket.Chat/pull/33920)) Improves the customizability of the naming of automatic Persistent video calls discussions, allowing the date of the call to be in different parts of the name, using the `[date]` keyword. + +- ([#33997](https://github.com/RocketChat/Rocket.Chat/pull/33997)) Prevent apps' subprocesses from crashing on unhandled rejections or uncaught exceptions + +- ([#33814](https://github.com/RocketChat/Rocket.Chat/pull/33814)) Adds a confirmation modal to the cancel subscription action + +- ([#33949](https://github.com/RocketChat/Rocket.Chat/pull/33949)) Disables the possiblity to upload exempted apps + +### Patch Changes + +- ([#33218](https://github.com/RocketChat/Rocket.Chat/pull/33218)) Fixes message character limit not being applied to file upload descriptions + +- ([#33902](https://github.com/RocketChat/Rocket.Chat/pull/33902)) Adds "Master volume" and "Call ringer volume" to the user preferences sound section. + +- ([#33880](https://github.com/RocketChat/Rocket.Chat/pull/33880)) Updates VoIP field labels from 'Free Extension Numbers' to 'Available Extensions' to better describe the field's purpose and improve clarity. + ## 1.1.0-rc.0 ### Minor Changes diff --git a/packages/i18n/package.json b/packages/i18n/package.json index eb6d7902628e..5e56917e77c4 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/i18n", - "version": "1.1.0-rc.0", + "version": "1.1.0", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/i18n/src/locales/de.i18n.json b/packages/i18n/src/locales/de.i18n.json index f39ec20f1622..d3c5cb1eae9d 100644 --- a/packages/i18n/src/locales/de.i18n.json +++ b/packages/i18n/src/locales/de.i18n.json @@ -3257,7 +3257,6 @@ "Message_is_removed": "Nachricht entfernt", "Message_sent_by_email": "Nachricht per E-Mail versendet", "Message_ShowDeletedStatus": "Löschstatus anzeigen", - "Message_Formatting_Toolbox": "Formatierungs-Werkzeuge", "Message_starring": "Markieren von favorisierten Nachrichten", "Message_Time": "Zeitpunkt der Nachricht", "Message_TimeAndDateFormat": "Zeit- und Datumsformat", @@ -3273,7 +3272,6 @@ "Message_VideoRecorderEnabledDescription": "Erfordert, dass der Medientyp 'video/webm' in den \"Datei-Upload\"-Einstellungen als Medientyp akzeptiert wird", "messages": "Nachrichten", "Messages": "Nachrichten", - "Messages_selected": "Ausgewählte Nachrichten", "Messages_sent": "Nachrichten versandt", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Nachrichten, die an den eingehenden Webhook gesendet werden, werden hier veröffentlicht", "Meta": "Metadaten", @@ -3362,7 +3360,6 @@ "Move_queue": "In Warteschlange verschieben", "Msgs": "Nachrichten", "multi": "mehrere", - "Multi_line": "Mehrzeilig", "Multiple_monolith_instances_alert": "Sie betreiben mehrere Instanzen ohne eine aktive Unternehmenslizenz — einige Funktionen verhalten sich möglicherweise nicht wie vorgesehen", "Mute": "Stummschalten", "Mute_and_dismiss": "Stummschalten und Abweisen", @@ -4503,7 +4500,6 @@ "Stream_Cast": "Stream Cast", "Stream_Cast_Address": "Stream Cast-Adresse", "Stream_Cast_Address_Description": "IP oder Host Ihres zentralen Stream Cast-Servers inkl. Port, bspw. `192.168.1.1:3000` oder `localhost:4000`", - "Strike": "Durchgestrichen", "Style": "Stil", "Subject": "Betreff", "Submit": "Abssenden", @@ -5509,4 +5505,4 @@ "Enterprise": "Unternehmen", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "UpgradeToGetMore_auditing_Title": "Nachrichtenüberprüfung" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 3ba0b961b6ee..2b955fc6c327 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -26,7 +26,6 @@ "__roomName__encryption_keys_need_to_be_updated": "{{roomName}} encryption keys need to be updated to give you access. Another room member needs to be online for this to happen.", "removed__username__as__role_": "removed {{username}} as {{role}}", "set__username__as__role_": "set {{username}} as {{role}}", - "sequential_message": "sequential message", "This_room_encryption_has_been_enabled_by__username_": "This room's encryption has been enabled by {{username}}", "This_room_encryption_has_been_disabled_by__username_": "This room's encryption has been disabled by {{username}}", "Third_party_login": "Third-party login", @@ -1067,6 +1066,7 @@ "clean-channel-history_description": "Permission to Clear the history from channels", "clear": "Clear", "Clear_all_unreads_question": "Clear all unreads?", + "Clear_selection": "Clear selection", "clear_cache_now": "Clear Cache Now", "Clear_filters": "Clear filters", "clear_history": "Clear History", @@ -1813,6 +1813,7 @@ "Download": "Download", "Download_Destkop_App": "Download Desktop App", "Download_Disabled": "Download disabled", + "Download_file": "Download file", "Download_Info": "Download info", "Download_My_Data": "Download My Data (HTML)", "Download_Pending_Avatars": "Download Pending Avatars", @@ -2259,6 +2260,13 @@ "error-user-registration-disabled": "User registration is disabled", "error-user-registration-secret": "User registration is only allowed via Secret URL", "error-validating-department-chat-closing-tags": "At least one closing tag is required when the department requires tag(s) on closing conversations.", + "error-videoconf-cant-start-call-with-manager-busy": "Unable to start a new call due to the current state of other calls.", + "error-videoconf-direct-call-accept-timeout": "No response from remote user after notifying the call was accepted.", + "error-videoconf-direct-call-accept-canceled": "The remote user hang up before we had time to accept the call.", + "error-videoconf-direct-call-accept-ended": "The server ended the call before we had time to accept it.", + "error-videoconf-join-failed": "Unexpected Server Error while joining call.", + "error-videoconf-missing-url": "Failed to get the conference's URL.", + "error-videoconf-unexpected": "Unexpected Conference Call Error", "error-no-permission-team-channel": "You don't have permission to add this channel to the team", "error-no-owner-channel": "Only owners can add this channel to the team", "error-unable-to-update-priority": "Unable to update priority", @@ -2319,7 +2327,7 @@ "Expiration": "Expiration", "Expiration_(Days)": "Expiration (Days)", "Export_as_file": "Export as file", - "Export_Messages": "Export Messages", + "Export_Messages": "Export messages", "Export_My_Data": "Export My Data (JSON)", "expression": "Expression", "Extended": "Extended", @@ -2428,6 +2436,7 @@ "Federation_Matrix_serve_well_known_Alert": "Keep this off if using DNS srv records for federation, or use a reverse proxy to return static JSON if federation traffic is heavy. Read mode.", "Federation_Matrix_check_configuration": "Verify configuration", "Federation_Matrix_configuration_status": "Configuration status", + "Federated": "Federated", "Field": "Field", "Field_removed": "Field removed", "Field_required": "Field required", @@ -3755,7 +3764,7 @@ "Message_is_removed": "message removed", "Message_sent_by_email": "Message sent by Email", "Message_ShowDeletedStatus": "Show Deleted Status", - "Message_Formatting_Toolbox": "Formatting Toolbox", + "Message_Formatting_toolbox": "Formatting toolbox", "Message_composer_toolbox_primary_actions": "Composer Primary Actions", "Message_composer_toolbox_secondary_actions": "Composer Secondary Actions", "Message_starring": "Message starring", @@ -3773,7 +3782,8 @@ "Message_VideoRecorderEnabledDescription": "Requires 'video/webm' files to be an accepted media type within 'File Upload' settings.", "messages": "messages", "Messages": "Messages", - "Messages_selected": "Messages selected", + "__count__messages_selected": "{{count}} messages selected", + "Messages_exported_successfully": "Messages exported successfully", "Messages_sent": "Messages sent", "Message_sent": "Message sent", "Message_viewed": "Message viewed", @@ -3887,7 +3897,7 @@ "move-room-to-team_description": "Permission to add an existing room to a team", "Msgs": "Msgs", "multi": "multi", - "Multi_line": "Multi line", + "Multi_line_code": "Multi-line code", "Multiple_monolith_instances_alert": "You are operating multiple instances without an active Premium license - some features may not behave as designed", "Mute": "Mute", "Mute_and_dismiss": "Mute and dismiss", @@ -4221,6 +4231,7 @@ "others": "others", "Others": "Others", "OTR": "OTR", + "OTR_messages_cannot_be_exported": "OTR messages cannot be exported", "OTR_unavailable_for_federation": "OTR is unavailable for federated rooms", "OTR_Description": "Off-the-record chats are secure, private and disappear once ended.", "OTR_Chat_Declined_Title": "OTR Chat invite Declined", @@ -4974,8 +4985,9 @@ "Send_a_test_push_to_my_user": "Send a test push to my user", "Send_confirmation_email": "Send confirmation email", "Send_data_into_RocketChat_in_realtime": "Send data into Rocket.Chat in real-time.", - "Send_email": "Send Email", + "Send_email": "Send email", "Send_Email_SMTP_Warning": "Set up the SMTP server in email settings to enable.", + "Send_file_via_email": "Send file via email", "Send_invitation_email": "Send invitation email", "Send_invitation_email_error": "You haven't provided any valid email address.", "Send_invitation_email_info": "You can send multiple email invitations at once.", @@ -5276,7 +5288,7 @@ "Stream_Cast": "Stream Cast", "Stream_Cast_Address": "Stream Cast Address", "Stream_Cast_Address_Description": "IP or Host of your Rocket.Chat central Stream Cast. E.g. `192.168.1.1:3000` or `localhost:4000`", - "Strike": "Strike", + "Strikethrough": "Strikethrough", "Style": "Style", "Subject": "Subject", "Submit": "Submit", @@ -6304,7 +6316,7 @@ "registration.component.login": "Login", "registration.component.login.userNotFound": "User not found", "registration.component.login.incorrectPassword": "Incorrect password", - "registration.component.switchLanguage": "Change to <1>{{name}}", + "registration.component.switchLanguage": "Change to <2>{{name}}", "registration.component.resetPassword": "Reset password", "registration.component.form.emailOrUsername": "Email or username", "registration.component.form.username": "Username", @@ -6684,6 +6696,7 @@ "Go_to_href": "Go to: {{href}}", "Anyone_can_send_new_messages": "Anyone can send new messages", "Select_messages_to_hide": "Select messages to hide", + "Select__count__messages": "Select {{count}} messages", "Name_cannot_have_special_characters": "Name cannot have spaces or special characters", "Resize": "Resize", "Zoom_out": "Zoom out", diff --git a/packages/i18n/src/locales/es.i18n.json b/packages/i18n/src/locales/es.i18n.json index fc1362aa7eeb..31b3b41aefb1 100644 --- a/packages/i18n/src/locales/es.i18n.json +++ b/packages/i18n/src/locales/es.i18n.json @@ -4788,7 +4788,7 @@ "registration.component.login": "Iniciar sesión", "registration.component.login.userNotFound": "Usuario no encontrado", "registration.component.login.incorrectPassword": "Contraseña incorrecta", - "registration.component.switchLanguage": "Cambiar a <1>{{name}}", + "registration.component.switchLanguage": "Cambiar a <2>{{name}}", "registration.component.resetPassword": "Reestablecer contraseña", "registration.component.form.username": "Nombre de usuario", "registration.component.form.name": "Nombre", diff --git a/packages/i18n/src/locales/fi.i18n.json b/packages/i18n/src/locales/fi.i18n.json index 497484914396..b1435822c864 100644 --- a/packages/i18n/src/locales/fi.i18n.json +++ b/packages/i18n/src/locales/fi.i18n.json @@ -3302,7 +3302,6 @@ "Message_is_removed": "viesti poistettu", "Message_sent_by_email": "Viesti lähetetty sähköpostilla", "Message_ShowDeletedStatus": "Näytä Poistettu-tila", - "Message_Formatting_Toolbox": "Muotoilutyökalut", "Message_composer_toolbox_primary_actions": "Kirjoituksen ensisijaiset toiminnot", "Message_composer_toolbox_secondary_actions": "Kirjoituksen toissijaiset toiminnot", "Message_starring": "Viestin merkitseminen tähdellä", @@ -3320,7 +3319,6 @@ "Message_VideoRecorderEnabledDescription": "Vaatii 'video/webm'-tiedostot hyväksytyksi mediatyypiksi 'Tiedoston lataus'-asetuksissa.", "messages": "viestit", "Messages": "Viestit", - "Messages_selected": "Valitut viestit", "Messages_sent": "Lähetetyt viestit", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Saapuvaan WebHookiin lähetetyt viestit julkaistaan tässä.", "Meta": "Meta", @@ -3394,7 +3392,6 @@ "Move_queue": "Siirry jonoon", "Msgs": "Viestit", "multi": "monta", - "Multi_line": "Monirivinen", "Multiple_monolith_instances_alert": "Käytät useita instansseja... jotkut ominaisuudet eivät käyttäydy suunnitellulla tavalla.", "Mute": "Mykistä", "Mute_and_dismiss": "Mykistä ja poista", @@ -4587,7 +4584,6 @@ "Stream_Cast": "Stream Cast", "Stream_Cast_Address": "Stream Cast-osoite", "Stream_Cast_Address_Description": "Chatsovelluksen Central Stream Castin IP-osoite tai isäntä. Esim. `192.168.1.1:3000` tai `localhost:4000`", - "Strike": "Päälleviivaa", "Style": "Tyyli", "Subject": "Aihe", "Submit": "Lähetä", @@ -5509,7 +5505,7 @@ "registration.component.login": "Kirjaudu", "registration.component.login.userNotFound": "Käyttäjää ei löydy", "registration.component.login.incorrectPassword": "Väärä salasana", - "registration.component.switchLanguage": "Vaihda kieleksi <1>{{name}}", + "registration.component.switchLanguage": "Vaihda kieleksi <2>{{name}}", "registration.component.resetPassword": "Nollaa salasana", "registration.component.form.emailOrUsername": "Sähköpostiosoite tai käyttäjätunnus", "registration.component.form.username": "Käyttäjätunnus", @@ -5722,4 +5718,4 @@ "Theme_Appearence": "Teeman ulkoasu", "Enterprise": "Yritys", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/hi-IN.i18n.json b/packages/i18n/src/locales/hi-IN.i18n.json index 46d5a2325fc2..35dd69ec28ac 100644 --- a/packages/i18n/src/locales/hi-IN.i18n.json +++ b/packages/i18n/src/locales/hi-IN.i18n.json @@ -3437,7 +3437,6 @@ "Message_is_removed": "संदेश हटा दिया गया", "Message_sent_by_email": "ईमेल द्वारा भेजा गया संदेश", "Message_ShowDeletedStatus": "हटाई गई स्थिति दिखाएँ", - "Message_Formatting_Toolbox": "फ़ॉर्मेटिंग टूलबॉक्स", "Message_composer_toolbox_primary_actions": "संगीतकार प्राथमिक क्रियाएँ", "Message_composer_toolbox_secondary_actions": "संगीतकार माध्यमिक क्रियाएँ", "Message_starring": "संदेश अभिनीत", @@ -3455,7 +3454,6 @@ "Message_VideoRecorderEnabledDescription": "'फ़ाइल अपलोड' सेटिंग्स के अंतर्गत 'वीडियो/वेबएम' फ़ाइलों को एक स्वीकृत मीडिया प्रकार होना आवश्यक है।", "messages": "संदेशों", "Messages": "संदेशों", - "Messages_selected": "संदेश चयनित", "Messages_sent": "संदेश भेजे गए", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "इनकमिंग वेबहुक पर भेजे गए संदेश यहां पोस्ट किए जाएंगे।", "Meta": "मेटा", @@ -3558,7 +3556,6 @@ "Move_queue": "कतार में जाएँ", "Msgs": "संदेश", "multi": "बहु", - "Multi_line": "मल्टी लाइन", "Multiple_monolith_instances_alert": "आप सक्रिय प्रीमियम लाइसेंस के बिना कई इंस्टेंसेस का संचालन कर रहे हैं - हो सकता है कि कुछ सुविधाएँ डिज़ाइन के अनुसार व्यवहार न करें", "Mute": "आवाज़ बंद करना", "Mute_and_dismiss": "म्यूट करें और ख़ारिज करें", @@ -4847,7 +4844,6 @@ "Stream_Cast": "स्ट्रीम कास्ट", "Stream_Cast_Address": "स्ट्रीम कास्ट पता", "Stream_Cast_Address_Description": "आपके रॉकेट.चैट सेंट्रल स्ट्रीम कास्ट का आईपी या होस्ट। जैसे `192.168.1.1:3000` या `लोकलहोस्ट:4000`", - "Strike": "हड़ताल", "Style": "शैली", "Subject": "विषय", "Submit": "जमा करना", @@ -5796,7 +5792,7 @@ "registration.component.login": "लॉग इन करें", "registration.component.login.userNotFound": "उपयोगकर्ता नहीं मिला", "registration.component.login.incorrectPassword": "गलत पासवर्ड", - "registration.component.switchLanguage": "<1>{{name}} में बदलें", + "registration.component.switchLanguage": "<2>{{name}} में बदलें", "registration.component.resetPassword": "पासवर्ड रीसेट", "registration.component.form.emailOrUsername": "ईमेल या उपयोगकर्ता का नाम", "registration.component.form.username": "उपयोगकर्ता नाम", @@ -6106,4 +6102,4 @@ "Unlimited_seats": "असीमित सीटें", "Unlimited_MACs": "असीमित एमएसी", "Unlimited_seats_MACs": "असीमित सीटें और एमएसी" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/hu.i18n.json b/packages/i18n/src/locales/hu.i18n.json index e5b68aeddbee..9f2ed6f082aa 100644 --- a/packages/i18n/src/locales/hu.i18n.json +++ b/packages/i18n/src/locales/hu.i18n.json @@ -3199,7 +3199,6 @@ "Message_VideoRecorderEnabledDescription": "Azt igényli, hogy a „video/webm” fájlok elfogadott médiatípus legyen a „Fájlfeltöltés” beállításaiban.", "messages": "üzenetek", "Messages": "Üzenetek", - "Messages_selected": "Üzenetek kijelölve", "Messages_sent": "Üzenetek elküldve", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "A bejövő webhorogra küldött üzenetek itt lesznek beküldve.", "Meta": "Meta", @@ -5300,7 +5299,7 @@ "registration.component.login": "Bejelentkezés", "registration.component.login.userNotFound": "A felhasználó nem található", "registration.component.login.incorrectPassword": "Hibás jelszó", - "registration.component.switchLanguage": "Átváltás <1>{{name}}", + "registration.component.switchLanguage": "Átváltás <2>{{name}}", "registration.component.resetPassword": "Jelszó visszaállítása", "registration.component.form.emailOrUsername": "E-mail-cím vagy felhasználónév", "registration.component.form.username": "Felhasználónév", @@ -5407,4 +5406,4 @@ "Enterprise": "Vállalati", "UpgradeToGetMore_engagement-dashboard_Title": "Analitika", "UpgradeToGetMore_auditing_Title": "Üzenet ellenőrzés" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/nn.i18n.json b/packages/i18n/src/locales/nn.i18n.json index 27df7821fae0..6637364b8265 100644 --- a/packages/i18n/src/locales/nn.i18n.json +++ b/packages/i18n/src/locales/nn.i18n.json @@ -2855,7 +2855,6 @@ "Message_VideoRecorderEnabledDescription": "Krever at video / webm-filer skal være en akseptert medietype i \"Filopplastings\" -innstillinger.", "messages": "meldinger", "Messages": "meldinger", - "Messages_selected": "Meldinger er valgt", "Messages_sent": "Meldinger sendt", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Meldinger som sendes til Incoming WebHook vil bli lagt ut her.", "Meta": "Meta", @@ -2926,7 +2925,6 @@ "Move_queue": "Flytt til køen", "Msgs": "meld", "multi": "multi", - "Multi_line": "Flerlinje", "Mute": "Demp", "Mute_and_dismiss": "Demp og avvis", "Mute_all_notifications": "Slå av alle varsler", @@ -4477,7 +4475,7 @@ "registration.component.login": "Logg inn", "registration.component.login.userNotFound": "Bruker ikke funnet", "registration.component.login.incorrectPassword": "feil passord", - "registration.component.switchLanguage": "Bytt til <1>{{name}}", + "registration.component.switchLanguage": "Bytt til <2>{{name}}", "registration.component.resetPassword": "Tilbakestilling av passord", "registration.component.form.username": "Brukernavn", "registration.component.form.name": "Navn", @@ -4560,4 +4558,4 @@ "free_per_month_user": "$0 per måned per bruker", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "Buy_more": "Kjøp mer" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/no.i18n.json b/packages/i18n/src/locales/no.i18n.json index a053a75912d3..39a50fbad692 100644 --- a/packages/i18n/src/locales/no.i18n.json +++ b/packages/i18n/src/locales/no.i18n.json @@ -2855,7 +2855,6 @@ "Message_VideoRecorderEnabledDescription": "Krever at video / webm-filer skal være en akseptert medietype i \"Filopplastings\" -innstillinger.", "messages": "meldinger", "Messages": "meldinger", - "Messages_selected": "Meldinger er valgt", "Messages_sent": "Meldinger sendt", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Meldinger som sendes til Incoming WebHook vil bli lagt ut her.", "Meta": "Meta", @@ -2926,7 +2925,6 @@ "Move_queue": "Flytt til køen", "Msgs": "meld", "multi": "multi", - "Multi_line": "Flerlinje", "Mute": "Demp", "Mute_and_dismiss": "Demp og avvis", "Mute_all_notifications": "Slå av alle varsler", @@ -4477,7 +4475,7 @@ "registration.component.login": "Logg inn", "registration.component.login.userNotFound": "Bruker ikke funnet", "registration.component.login.incorrectPassword": "feil passord", - "registration.component.switchLanguage": "Bytt til <1>{{name}}", + "registration.component.switchLanguage": "Bytt til <2>{{name}}", "registration.component.resetPassword": "Tilbakestilling av passord", "registration.component.form.username": "Brukernavn", "registration.component.form.name": "Navn", @@ -4562,4 +4560,4 @@ "free_per_month_user": "$0 per måned per bruker", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "Buy_more": "Kjøp mer" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/pl.i18n.json b/packages/i18n/src/locales/pl.i18n.json index 3a58a1a4c4eb..f43c37775fa3 100644 --- a/packages/i18n/src/locales/pl.i18n.json +++ b/packages/i18n/src/locales/pl.i18n.json @@ -26,7 +26,6 @@ "__roomName__encryption_keys_need_to_be_updated": "{{roomName}} klucze szyfrowania muszą zostać zaktualizowane, aby umożliwić dostęp. Aby tak się stało, inny członek pokoju musi być online.", "removed__username__as__role_": "usunięto {{username}} jako {{role}}", "set__username__as__role_": "ustaw {{username}} jako {{role}}", - "sequential_message": "komunikat sekwencyjny", "This_room_encryption_has_been_enabled_by__username_": "Użytkownik {{username}} włączył szyfrowanie w tym pokoju", "This_room_encryption_has_been_disabled_by__username_": "Użytkownik {{username}} wyłączył szyfrowanie w tym pokoju", "Third_party_login": "Logowanie przez stronę trzecią", @@ -3204,7 +3203,6 @@ "Message_VideoRecorderEnabledDescription": "Wymaga plików \"wideo / webm\", aby były akceptowanym typem mediów w ustawieniach \"Przesyłanie pliku\".", "messages": "Wiadomości", "Messages": "Wiadomości", - "Messages_selected": "Wybrane wiadomości", "Messages_sent": "Wiadomości wysłane", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Wiadomości, które zostaną przesłane przez WebHook będą publikowane tutaj.", "Meta": "Meta", @@ -5405,4 +5403,4 @@ "Broadcast_hint_enabled": "Tylko właściciele {{roomType}} mogą pisać nowe wiadomości, ale każdy może odpowiadać w wątku", "Anyone_can_send_new_messages": "Każdy może wysyłać nowe wiadomości", "Select_messages_to_hide": "Wybierz wiadomości do ukrycia" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/se.i18n.json b/packages/i18n/src/locales/se.i18n.json index fac7195afff7..2c2512c9aba6 100644 --- a/packages/i18n/src/locales/se.i18n.json +++ b/packages/i18n/src/locales/se.i18n.json @@ -24,7 +24,6 @@ "__roomName__encryption_keys_need_to_be_updated": "{{roomName}} encryption keys need to be updated to give you access. Another room member needs to be online for this to happen.", "removed__username__as__role_": "removed {{username}} as {{role}}", "set__username__as__role_": "set {{username}} as {{role}}", - "sequential_message": "sequential message", "This_room_encryption_has_been_enabled_by__username_": "This room's encryption has been enabled by {{username}}", "This_room_encryption_has_been_disabled_by__username_": "This room's encryption has been disabled by {{username}}", "Third_party_login": "Third-party login", @@ -3662,7 +3661,6 @@ "Message_is_removed": "message removed", "Message_sent_by_email": "Message sent by Email", "Message_ShowDeletedStatus": "Show Deleted Status", - "Message_Formatting_Toolbox": "Formatting Toolbox", "Message_composer_toolbox_primary_actions": "Composer Primary Actions", "Message_composer_toolbox_secondary_actions": "Composer Secondary Actions", "Message_starring": "Message starring", @@ -3680,7 +3678,6 @@ "Message_VideoRecorderEnabledDescription": "Requires 'video/webm' files to be an accepted media type within 'File Upload' settings.", "messages": "messages", "Messages": "Messages", - "Messages_selected": "Messages selected", "Messages_sent": "Messages sent", "Message_sent": "Message sent", "Message_viewed": "Message viewed", @@ -3793,7 +3790,6 @@ "Move_queue": "Move to the queue", "Msgs": "Msgs", "multi": "multi", - "Multi_line": "Multi line", "Multiple_monolith_instances_alert": "You are operating multiple instances without an active Premium license - some features may not behave as designed", "Mute": "Mute", "Mute_and_dismiss": "Mute and dismiss", @@ -5165,7 +5161,6 @@ "Stream_Cast": "Stream Cast", "Stream_Cast_Address": "Stream Cast Address", "Stream_Cast_Address_Description": "IP or Host of your Rocket.Chat central Stream Cast. E.g. `192.168.1.1:3000` or `localhost:4000`", - "Strike": "Strike", "Style": "Style", "Subject": "Subject", "Submit": "Submit", @@ -6177,7 +6172,7 @@ "registration.component.login": "Login", "registration.component.login.userNotFound": "User not found", "registration.component.login.incorrectPassword": "Incorrect password", - "registration.component.switchLanguage": "Change to <1>{{name}}", + "registration.component.switchLanguage": "Change to <2>{{name}}", "registration.component.resetPassword": "Reset password", "registration.component.form.emailOrUsername": "Email or username", "registration.component.form.username": "Username", @@ -6579,4 +6574,4 @@ "Sidepanel_navigation_description": "Display channels and/or discussions associated with teams by default. This allows team owners to customize communication methods to best meet their team’s needs. This is currently in feature preview and will be a premium capability once fully released.", "Show_channels_description": "Show team channels in second sidebar", "Show_discussions_description": "Show team discussions in second sidebar" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/sv.i18n.json b/packages/i18n/src/locales/sv.i18n.json index 5bcb4e9af48f..d097e915b2e4 100644 --- a/packages/i18n/src/locales/sv.i18n.json +++ b/packages/i18n/src/locales/sv.i18n.json @@ -3307,7 +3307,6 @@ "Message_is_removed": "meddelande borttaget", "Message_sent_by_email": "Meddelande skickat via e-post", "Message_ShowDeletedStatus": "Visa borttagen status", - "Message_Formatting_Toolbox": "Verktygslåda för formatering", "Message_composer_toolbox_primary_actions": "Primär åtgärd för kompositör", "Message_composer_toolbox_secondary_actions": "Sekundär åtgärd för kompositör", "Message_starring": "Stjärnmarkera meddelanden", @@ -3325,7 +3324,6 @@ "Message_VideoRecorderEnabledDescription": "Kräver \"video/webm\"-filer för att vara en accepterad medietyp inom inställningarna \"Filuppladdning\".", "messages": "Meddelanden", "Messages": "Meddelanden", - "Messages_selected": "Valda meddelanden", "Messages_sent": "Skickade meddelanden", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Meddelanden som skickas till inkommande WebHook kommer att publiceras här.", "Meta": "Meta", @@ -3400,7 +3398,6 @@ "Move_queue": "Flytta till kön", "Msgs": "Meddelanden ", "multi": "mång", - "Multi_line": "Flera rader", "Multiple_monolith_instances_alert": "Du använder flera instanser utan en aktiv licens för Enterprise Edition. Alla funktioner kanske inte fungerar som avsett.", "Mute": "Tysta", "Mute_and_dismiss": "Tysta och ignorera", @@ -4594,7 +4591,6 @@ "Stream_Cast": "Stream Cast", "Stream_Cast_Address": "Stream Cast-adress", "Stream_Cast_Address_Description": "IP eller värd för din Rocket.Chat Central Stream Cast. T.ex. `192.168.1.1: 3000` eller` localhost: 4000`", - "Strike": "Genomstruken", "Style": "Stil", "Subject": "Ämne", "Submit": "Skicka", @@ -5517,7 +5513,7 @@ "registration.component.login": "Logga in", "registration.component.login.userNotFound": "Användare inte hittad", "registration.component.login.incorrectPassword": "Felaktigt lösenord", - "registration.component.switchLanguage": "Växla till <1>{{name}}", + "registration.component.switchLanguage": "Växla till <2>{{name}}", "registration.component.resetPassword": "Återställ lösenord", "registration.component.form.emailOrUsername": "E-postadress eller användarnamn", "registration.component.form.username": "Användarnamn", @@ -5724,4 +5720,4 @@ "Uninstall_grandfathered_app": "Avinstallera {{appName}}?", "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} +} \ No newline at end of file diff --git a/packages/instance-status/CHANGELOG.md b/packages/instance-status/CHANGELOG.md index 2c6646df5361..27f0fe2ed041 100644 --- a/packages/instance-status/CHANGELOG.md +++ b/packages/instance-status/CHANGELOG.md @@ -1,5 +1,32 @@ # @rocket.chat/instance-status +## 0.1.10 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/models@1.0.1 +
+ +## 0.1.10-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/models@1.0.1-rc.3 +
+ +## 0.1.10-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/models@1.0.1-rc.2 +
+ ## 0.1.10-rc.1 ### Patch Changes diff --git a/packages/instance-status/package.json b/packages/instance-status/package.json index 0a8fd83eade0..91c105ae4266 100644 --- a/packages/instance-status/package.json +++ b/packages/instance-status/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/instance-status", - "version": "0.1.10-rc.1", + "version": "0.1.10", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/instance-status/src/index.ts b/packages/instance-status/src/index.ts index 38109e01626d..86f638e089c3 100644 --- a/packages/instance-status/src/index.ts +++ b/packages/instance-status/src/index.ts @@ -1,26 +1,39 @@ -// import { IInstanceStatus } from '@rocket.chat/core-typings'; -import { EventEmitter } from 'events'; - +import type { IInstanceStatus } from '@rocket.chat/core-typings'; import { InstanceStatus as InstanceStatusModel } from '@rocket.chat/models'; -import { tracerSpan } from '@rocket.chat/tracing'; import { v4 as uuidv4 } from 'uuid'; -const events = new EventEmitter(); +export const defaultPingInterval = parseInt(String(process.env.MULTIPLE_INSTANCES_PING_INTERVAL)) || 10; +export const indexExpire = (parseInt(String(process.env.MULTIPLE_INSTANCES_EXPIRE)) || Math.ceil((defaultPingInterval * 3) / 60)) * 60; + +const ID = uuidv4(); +const id = (): IInstanceStatus['_id'] => ID; + +const currentInstance = { + name: '', + extraInformation: {}, +}; + +let pingInterval: NodeJS.Timeout | null; -const defaultPingInterval = parseInt(String(process.env.MULTIPLE_INSTANCES_PING_INTERVAL)) || 10; // default to 10s +function start() { + stop(); + pingInterval = setInterval(async () => ping(), defaultPingInterval * 1000); +} -// if not set via env var ensures at least 3 ticks before expiring (multiple of 60s) -const indexExpire = (parseInt(String(process.env.MULTIPLE_INSTANCES_EXPIRE)) || Math.ceil((defaultPingInterval * 3) / 60)) * 60; +function stop() { + if (!pingInterval) { + return; + } + clearInterval(pingInterval); + pingInterval = null; +} let createIndexes = async () => { await InstanceStatusModel.col .indexes() - .catch(function () { - // the collection should not exists yet, return empty then - return []; - }) - .then(function (result) { - return result.some(function (index) { + .catch(() => []) + .then((result) => + result.some((index) => { if (index.key && index.key._updatedAt === 1) { if (index.expireAfterSeconds !== indexExpire) { InstanceStatusModel.col.dropIndex(index.name); @@ -29,115 +42,55 @@ let createIndexes = async () => { return true; } return false; - }); - }) - .then(function (created) { + }), + ) + .then((created) => { if (!created) { InstanceStatusModel.col.createIndex({ _updatedAt: 1 }, { expireAfterSeconds: indexExpire }); } }); createIndexes = async () => { - // no op + // noop }; }; -const ID = uuidv4(); - -function id() { - return ID; -} - -const currentInstance = { - name: '', - extraInformation: {}, -}; - -async function registerInstance(name: string, extraInformation: Record): Promise { +async function registerInstance(name: string, extraInformation: Partial): Promise { createIndexes(); currentInstance.name = name; currentInstance.extraInformation = extraInformation; - // if (ID === undefined || ID === null) { - // return console.error('[multiple-instances-status] only can be called after Meteor.startup'); - // } - - const instance = { - $set: { - pid: process.pid, - name, - ...(extraInformation && { extraInformation }), - }, - $currentDate: { - _createdAt: true, - _updatedAt: true, - }, - }; - - try { - await InstanceStatusModel.updateOne({ _id: ID }, instance as any, { upsert: true }); - - const result = await InstanceStatusModel.findOne({ _id: ID }); + const result = await InstanceStatusModel.upsertInstance({ + _id: id(), + pid: process.pid, + name, + extraInformation: extraInformation as IInstanceStatus['extraInformation'], + }); - start(); + start(); + process.on('exit', onExit); - events.emit('registerInstance', result, instance); - - process.on('exit', onExit); - - return result; - } catch (e) { - return e; - } + return result; } async function unregisterInstance() { try { - const result = await InstanceStatusModel.deleteOne({ _id: ID }); + const result = await InstanceStatusModel.removeInstanceById(id()); stop(); - - events.emit('unregisterInstance', ID); - process.removeListener('exit', onExit); - return result; } catch (e) { return e; } } -let pingInterval: NodeJS.Timeout | null; - -function start(interval?: number) { - stop(); - - interval = interval || defaultPingInterval; - - pingInterval = setInterval(async function () { - await tracerSpan('InstanceStatus.ping', {}, () => ping()); - }, interval * 1000); -} - -function stop() { - if (!pingInterval) { - return; - } - clearInterval(pingInterval); - pingInterval = null; +async function updateConnections(connections: number) { + await InstanceStatusModel.updateConnections(id(), connections); } async function ping() { - const result = await InstanceStatusModel.updateOne( - { - _id: ID, - }, - { - $currentDate: { - _updatedAt: true, - }, - }, - ); + const result = await InstanceStatusModel.setDocumentHeartbeat(ID); if (result.modifiedCount === 0) { await registerInstance(currentInstance.name, currentInstance.extraInformation); @@ -148,21 +101,10 @@ async function onExit() { await unregisterInstance(); } -async function updateConnections(conns: number) { - await InstanceStatusModel.updateOne( - { - _id: ID, - }, - { - $set: { - 'extraInformation.conns': conns, - }, - }, - ); -} - export const InstanceStatus = { + defaultPingInterval, id, + indexExpire, registerInstance, updateConnections, }; diff --git a/packages/livechat/CHANGELOG.md b/packages/livechat/CHANGELOG.md index 1fcbed0e5c46..a5c85267d93a 100644 --- a/packages/livechat/CHANGELOG.md +++ b/packages/livechat/CHANGELOG.md @@ -1,5 +1,40 @@ # @rocket.chat/livechat Change Log +## 1.21.0 + +### Minor Changes + +- ([#33997](https://github.com/RocketChat/Rocket.Chat/pull/33997)) Prevent apps' subprocesses from crashing on unhandled rejections or uncaught exceptions + +### Patch Changes + +- ([#33911](https://github.com/RocketChat/Rocket.Chat/pull/33911)) Fixes the 'Finish Chat' option in Livechat appearing before the conversation is started, which caused the action to fail. + +- ([#33944](https://github.com/RocketChat/Rocket.Chat/pull/33944)) Fixes livechat popout mode not working correctly in cross domain situations + +-
Updated dependencies []: + + - @rocket.chat/gazzodown@13.0.0 +
+ +## 1.21.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/gazzodown@13.0.0-rc.3 +
+ +## 1.21.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/gazzodown@13.0.0-rc.2 +
+ ## 1.21.0-rc.1 ### Patch Changes diff --git a/packages/livechat/package.json b/packages/livechat/package.json index 96913f82c650..88bb6db91e63 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/livechat", - "version": "1.21.0-rc.1", + "version": "1.21.0", "files": [ "/build" ], @@ -30,9 +30,9 @@ "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/ddp-client": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage-hooks": "^0.33.1", + "@rocket.chat/fuselage-hooks": "^0.34.0", "@rocket.chat/fuselage-tokens": "^0.33.2", - "@rocket.chat/logo": "^0.31.30", + "@rocket.chat/logo": "^0.31.31", "@rocket.chat/ui-contexts": "workspace:^", "@storybook/addon-essentials": "^8.4.4", "@storybook/addon-styling-webpack": "~1.0.1", diff --git a/packages/livechat/src/components/Screen/ScreenProvider.tsx b/packages/livechat/src/components/Screen/ScreenProvider.tsx index aa1dd61c59be..2bccafd60c6f 100644 --- a/packages/livechat/src/components/Screen/ScreenProvider.tsx +++ b/packages/livechat/src/components/Screen/ScreenProvider.tsx @@ -4,6 +4,7 @@ import { useCallback, useContext, useEffect, useState } from 'preact/hooks'; import { parse } from 'query-string'; import { isActiveSession } from '../../helpers/isActiveSession'; +import { createOrUpdateGuest, evaluateChangesAndLoadConfigByFields } from '../../lib/hooks'; import { loadConfig } from '../../lib/main'; import { parentCall } from '../../lib/parentCall'; import { loadMessages } from '../../lib/room'; @@ -76,7 +77,7 @@ export const ScreenProvider: FunctionalComponent = ({ children }) => { } = useContext(StoreContext); const { department, name, email } = iframe.guest || {}; const { color, position: configPosition, background } = config.theme || {}; - const { livechatLogo, hideWatermark = false } = config.settings || {}; + const { livechatLogo, hideWatermark = false, registrationForm } = config.settings || {}; const { color: customColor, @@ -137,15 +138,26 @@ export const ScreenProvider: FunctionalComponent = ({ children }) => { const dismissNotification = () => !isActiveSession(); - const checkPoppedOutWindow = useCallback(() => { + const checkPoppedOutWindow = useCallback(async () => { // Checking if the window is poppedOut and setting parent minimized if yes for the restore purpose const poppedOut = parse(window.location.search).mode === 'popout'; + const { token = '' } = parse(window.location.search); setPopedOut(poppedOut); if (poppedOut) { dispatch({ minimized: false, undocked: true }); } - }, [dispatch]); + + if (token && typeof token === 'string') { + if (registrationForm && !name && !email) { + dispatch({ token }); + return; + } + await evaluateChangesAndLoadConfigByFields(async () => { + await createOrUpdateGuest({ token }); + }); + } + }, [dispatch, email, name, registrationForm]); useEffect(() => { checkPoppedOutWindow(); diff --git a/packages/livechat/src/lib/hooks.ts b/packages/livechat/src/lib/hooks.ts index e1a980fe65e3..cdfde40f16e5 100644 --- a/packages/livechat/src/lib/hooks.ts +++ b/packages/livechat/src/lib/hooks.ts @@ -11,7 +11,7 @@ import { createToken } from './random'; import { loadMessages } from './room'; import Triggers from './triggers'; -const evaluateChangesAndLoadConfigByFields = async (fn: () => Promise) => { +export const evaluateChangesAndLoadConfigByFields = async (fn: () => Promise) => { const oldStore = JSON.parse( JSON.stringify({ user: store.state.user || {}, @@ -42,7 +42,7 @@ const evaluateChangesAndLoadConfigByFields = async (fn: () => Promise) => } }; -const createOrUpdateGuest = async (guest: StoreState['guest']) => { +export const createOrUpdateGuest = async (guest: StoreState['guest']) => { if (!guest) { return; } diff --git a/packages/livechat/src/widget.ts b/packages/livechat/src/widget.ts index 234d61f87096..ca46ab944913 100644 --- a/packages/livechat/src/widget.ts +++ b/packages/livechat/src/widget.ts @@ -489,20 +489,13 @@ const api: InternalWidgetAPI = { if (!config.url) { throw new Error('Config.url is not set!'); } + const urlToken = token && `&token=${token}`; + api.popup = window.open( - `${config.url}${config.url.lastIndexOf('?') > -1 ? '&' : '?'}mode=popout`, + `${config.url}${config.url.lastIndexOf('?') > -1 ? '&' : '?'}mode=popout${urlToken}`, 'livechat-popout', `width=${WIDGET_OPEN_WIDTH}, height=${widgetHeight}, toolbars=no`, ); - - const data = { - src: 'rocketchat', - fn: 'setGuestToken', - args: [token], - }; - - api.popup?.postMessage(data, '*'); - api.popup?.focus(); }, removeWidget() { diff --git a/packages/mock-providers/CHANGELOG.md b/packages/mock-providers/CHANGELOG.md index aefa17c20991..28a34e6d19f6 100644 --- a/packages/mock-providers/CHANGELOG.md +++ b/packages/mock-providers/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/mock-providers +## 0.1.5 + +### Patch Changes + +-
Updated dependencies [66ecc64fc1d4464ad2818ad04e23a09cdf221194, 6c83bf0657004ee9cf43d5c832f51826a6591165, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0, d1e6a73796269824fb1aa7afcc7b8aa242e34e90, 661cc01237629ce83699d6c25df25d12985e88bf, ce7024af36fcde97b1da5b2731f6edc4a4c236b8, 616655585cb1c5c60d7cee97e25b17af3dfda794, e5fe727f6a2f0e60cdf7ba225e1f6caa6db2045c, 322bafd4bd1fe91ed34610501b269e4d8951944c]: + + - @rocket.chat/i18n@1.1.0 +
+ ## 0.1.5-rc.0 ### Patch Changes diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index 6c5d5a0acdfe..a4967947b95d 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/mock-providers", - "version": "0.1.5-rc.0", + "version": "0.1.5", "private": true, "dependencies": { "@rocket.chat/emitter": "~0.31.25", diff --git a/packages/model-typings/CHANGELOG.md b/packages/model-typings/CHANGELOG.md index 2cb9d9f81a89..93760f4984f7 100644 --- a/packages/model-typings/CHANGELOG.md +++ b/packages/model-typings/CHANGELOG.md @@ -1,5 +1,45 @@ # @rocket.chat/model-typings +## 1.1.0 + +### Minor Changes + +- ([#32727](https://github.com/RocketChat/Rocket.Chat/pull/32727)) These changes aims to add: + - A brand-new omnichannel contact profile + - The ability to communicate with known contacts only + - Communicate with verified contacts only + - Merge verified contacts across different channels + - Block contact channels + - Resolve conflicting contact information when registered via different channels + - An advanced contact center filters + +### Patch Changes + +- ([#32991](https://github.com/RocketChat/Rocket.Chat/pull/32991)) Fixes an issue where updating custom emojis didn’t work as expected, ensuring that uploaded emojis now update correctly and display without any caching problems. + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0]: + + - @rocket.chat/core-typings@7.1.0 +
+ +## 1.1.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 +
+ +## 1.1.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 +
+ ## 1.1.0-rc.1 ### Patch Changes diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index 37afc465f121..a9a3255546f9 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/model-typings", - "version": "1.1.0-rc.1", + "version": "1.1.0", "private": true, "devDependencies": { "@types/node-rsa": "^1.1.4", diff --git a/packages/model-typings/src/index.ts b/packages/model-typings/src/index.ts index 77fe8f012ec9..5482132e4e57 100644 --- a/packages/model-typings/src/index.ts +++ b/packages/model-typings/src/index.ts @@ -14,6 +14,8 @@ export * from './models/IEmojiCustomModel'; export * from './models/IExportOperationsModel'; export * from './models/IFederationKeysModel'; export * from './models/IFederationServersModel'; +export * from './models/IFreeSwitchCallModel'; +export * from './models/IFreeSwitchEventModel'; export * from './models/IInstanceStatusModel'; export * from './models/IIntegrationHistoryModel'; export * from './models/IIntegrationsModel'; diff --git a/packages/model-typings/src/models/IFreeSwitchCallModel.ts b/packages/model-typings/src/models/IFreeSwitchCallModel.ts new file mode 100644 index 000000000000..ef5b35860420 --- /dev/null +++ b/packages/model-typings/src/models/IFreeSwitchCallModel.ts @@ -0,0 +1,9 @@ +import type { IFreeSwitchCall } from '@rocket.chat/core-typings'; +import type { FindCursor, FindOptions, WithoutId } from 'mongodb'; + +import type { IBaseModel, InsertionModel } from './IBaseModel'; + +export interface IFreeSwitchCallModel extends IBaseModel { + registerCall(call: WithoutId>): Promise; + findAllByChannelUniqueIds(uniqueIds: string[], options?: FindOptions): FindCursor; +} diff --git a/packages/model-typings/src/models/IFreeSwitchEventModel.ts b/packages/model-typings/src/models/IFreeSwitchEventModel.ts new file mode 100644 index 000000000000..118a57f85410 --- /dev/null +++ b/packages/model-typings/src/models/IFreeSwitchEventModel.ts @@ -0,0 +1,10 @@ +import type { IFreeSwitchEvent } from '@rocket.chat/core-typings'; +import type { FindCursor, FindOptions, WithoutId, InsertOneResult } from 'mongodb'; + +import type { IBaseModel, InsertionModel } from './IBaseModel'; + +export interface IFreeSwitchEventModel extends IBaseModel { + registerEvent(event: WithoutId>): Promise>; + findAllByCallUUID(callUUID: string, options?: FindOptions): FindCursor; + findAllByChannelUniqueIds(uniqueIds: string[], options?: FindOptions): FindCursor; +} diff --git a/packages/model-typings/src/models/IInstanceStatusModel.ts b/packages/model-typings/src/models/IInstanceStatusModel.ts index dd6aa4d76bc7..f514ba074666 100644 --- a/packages/model-typings/src/models/IInstanceStatusModel.ts +++ b/packages/model-typings/src/models/IInstanceStatusModel.ts @@ -1,7 +1,13 @@ import type { IInstanceStatus } from '@rocket.chat/core-typings'; +import type { DeleteResult, ModifyResult, UpdateResult } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; export interface IInstanceStatusModel extends IBaseModel { getActiveInstanceCount(): Promise; + getActiveInstancesAddress(): Promise; + removeInstanceById(_id: IInstanceStatus['_id']): Promise; + setDocumentHeartbeat(documentId: string): Promise; + upsertInstance(instance: Partial): Promise>; + updateConnections(_id: IInstanceStatus['_id'], conns: number): Promise; } diff --git a/packages/model-typings/src/models/ILivechatContactsModel.ts b/packages/model-typings/src/models/ILivechatContactsModel.ts index 5cf68b15449c..00702018bc6a 100644 --- a/packages/model-typings/src/models/ILivechatContactsModel.ts +++ b/packages/model-typings/src/models/ILivechatContactsModel.ts @@ -5,8 +5,18 @@ import type { ILivechatContactVisitorAssociation, ILivechatVisitor, } from '@rocket.chat/core-typings'; -import type { Document, FindCursor, FindOneAndUpdateOptions, FindOptions, UpdateFilter, UpdateOptions, UpdateResult } from 'mongodb'; +import type { + AggregationCursor, + Document, + FindCursor, + FindOneAndUpdateOptions, + FindOptions, + UpdateFilter, + UpdateOptions, + UpdateResult, +} from 'mongodb'; +import type { Updater } from '../updater'; import type { FindPaginated, IBaseModel, InsertionModel } from './IBaseModel'; export interface ILivechatContactsModel extends IBaseModel { @@ -31,10 +41,9 @@ export interface ILivechatContactsModel extends IBaseModel { options?: FindOptions, ): Promise; isChannelBlocked(visitor: ILivechatContactVisitorAssociation): Promise; - updateContactChannel( + updateFromUpdaterByAssociation( visitor: ILivechatContactVisitorAssociation, - data: Partial, - contactData?: Partial>, + contactUpdater: Updater, options?: UpdateOptions, ): Promise; findSimilarVerifiedContacts( @@ -44,4 +53,15 @@ export interface ILivechatContactsModel extends IBaseModel { ): Promise; findAllByVisitorId(visitorId: string): FindCursor; addEmail(contactId: string, email: string): Promise; + setChannelBlockStatus(visitor: ILivechatContactVisitorAssociation, blocked: boolean): Promise; + setChannelVerifiedStatus(visitor: ILivechatContactVisitorAssociation, verified: boolean): Promise; + setVerifiedUpdateQuery(verified: boolean, contactUpdater: Updater): Updater; + setFieldAndValueUpdateQuery(field: string, value: string, contactUpdater: Updater): Updater; + countByContactInfo({ contactId, email, phone }: { contactId?: string; email?: string; phone?: string }): Promise; + countUnknown(): Promise; + countBlocked(): Promise; + countFullyBlocked(): Promise; + countVerified(): Promise; + countContactsWithoutChannels(): Promise; + getStatistics(): AggregationCursor<{ totalConflicts: number; avgChannelsPerContact: number }>; } diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index 7ba6f9e74a3b..00dea51969c7 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -238,7 +238,7 @@ export interface ILivechatRoomsModel extends IBaseModel { date: { gte: Date; lte: Date }, data?: { departmentId?: string }, extraQuery?: Filter, - ): FindCursor>; + ): FindCursor>; getAnalyticsMetricsBetweenDateWithMessages( t: string, date: { gte: Date; lte: Date }, diff --git a/packages/model-typings/src/models/IMessagesModel.ts b/packages/model-typings/src/models/IMessagesModel.ts index e50e71f179bc..f5452e957e8f 100644 --- a/packages/model-typings/src/models/IMessagesModel.ts +++ b/packages/model-typings/src/models/IMessagesModel.ts @@ -290,7 +290,7 @@ export interface IMessagesModel extends IBaseModel { removeThreadFollowerByThreadId(tmid: string, userId: string): Promise; findThreadsByRoomId(rid: string, skip: number, limit: number): FindCursor; - decreaseReplyCountById(_id: string, inc?: number): Promise; + decreaseReplyCountById(_id: string, inc?: number): Promise>; countPinned(options?: CountDocumentsOptions): Promise; countStarred(options?: CountDocumentsOptions): Promise; } diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index a2863aba8fe5..407006596ba6 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -405,6 +405,7 @@ export interface IUsersModel extends IBaseModel { findAgentsAvailableWithoutBusinessHours(userIds: string[] | null): FindCursor>; updateLivechatStatusByAgentIds(userIds: string[], status: ILivechatAgentStatus): Promise; findOneByFreeSwitchExtension(extension: string, options?: FindOptions): Promise; + findOneByFreeSwitchExtensions(extensions: string[], options?: FindOptions): Promise; setFreeSwitchExtension(userId: string, extension: string | undefined): Promise; findAssignedFreeSwitchExtensions(): FindCursor; findUsersWithAssignedFreeSwitchExtensions(options?: FindOptions): FindCursor; diff --git a/packages/model-typings/src/models/IVideoConferenceModel.ts b/packages/model-typings/src/models/IVideoConferenceModel.ts index 8ef775fb6082..66a082af85d2 100644 --- a/packages/model-typings/src/models/IVideoConferenceModel.ts +++ b/packages/model-typings/src/models/IVideoConferenceModel.ts @@ -5,10 +5,11 @@ import type { IUser, VideoConference, VideoConferenceStatus, + IVoIPVideoConference, } from '@rocket.chat/core-typings'; import type { FindCursor, UpdateOptions, UpdateFilter, UpdateResult, FindOptions } from 'mongodb'; -import type { FindPaginated, IBaseModel } from './IBaseModel'; +import type { FindPaginated, IBaseModel, InsertionModel } from './IBaseModel'; export interface IVideoConferenceModel extends IBaseModel { findPaginatedByRoomId( @@ -67,4 +68,6 @@ export interface IVideoConferenceModel extends IBaseModel { setDiscussionRidById(callId: string, discussionRid: IRoom['_id']): Promise; unsetDiscussionRid(discussionRid: IRoom['_id']): Promise; + + createVoIP(call: InsertionModel): Promise; } diff --git a/packages/models/CHANGELOG.md b/packages/models/CHANGELOG.md index 85b835a7cd39..bdba22919b89 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/CHANGELOG.md @@ -1,5 +1,32 @@ # @rocket.chat/models +## 1.0.1 + +### Patch Changes + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0]: + + - @rocket.chat/model-typings@1.1.0 +
+ +## 1.0.1-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/model-typings@1.1.0-rc.3 +
+ +## 1.0.1-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/model-typings@1.1.0-rc.2 +
+ ## 1.0.1-rc.1 ### Patch Changes diff --git a/packages/models/package.json b/packages/models/package.json index 9028d400fba8..2259a4d1566c 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/models", - "version": "1.0.1-rc.1", + "version": "1.0.1", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 67bb4dfbcd47..7f13e4c9a079 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -13,6 +13,8 @@ import type { IExportOperationsModel, IFederationKeysModel, IFederationServersModel, + IFreeSwitchCallModel, + IFreeSwitchEventModel, IInstanceStatusModel, IIntegrationHistoryModel, IIntegrationsModel, @@ -111,6 +113,8 @@ export const ExportOperations = proxify('IExportOperatio export const FederationServers = proxify('IFederationServersModel'); export const FederationKeys = proxify('IFederationKeysModel'); export const FederationRoomEvents = proxify('IFederationRoomEventsModel'); +export const FreeSwitchCall = proxify('IFreeSwitchCallModel'); +export const FreeSwitchEvent = proxify('IFreeSwitchEventModel'); export const ImportData = proxify('IImportDataModel'); export const Imports = proxify('IImportsModel'); export const InstanceStatus = proxify('IInstanceStatusModel'); diff --git a/packages/release-action/src/publishRelease.ts b/packages/release-action/src/publishRelease.ts index 78066649b16c..9b62af0097e3 100644 --- a/packages/release-action/src/publishRelease.ts +++ b/packages/release-action/src/publishRelease.ts @@ -4,6 +4,7 @@ import path from 'path'; import * as core from '@actions/core'; import { exec } from '@actions/exec'; import * as github from '@actions/github'; +import semver from 'semver'; import { createNpmFile } from './createNpmFile'; import { fixWorkspaceVersionsBeforePublish } from './fixWorkspaceVersionsBeforePublish'; @@ -87,12 +88,19 @@ export async function publishRelease({ await pushChanges(); + const { data: latestRelease } = await octokit.rest.repos.getLatestRelease({ + ...github.context.repo, + }); + + core.info(`latest release tag: ${latestRelease.tag_name}`); + core.info('create release'); await octokit.rest.repos.createRelease({ name: newVersion, tag_name: newVersion, body: releaseBody, prerelease: newVersion.includes('-'), + make_latest: semver.gt(newVersion, latestRelease.tag_name) ? 'true' : 'false', ...github.context.repo, }); } diff --git a/packages/rest-typings/CHANGELOG.md b/packages/rest-typings/CHANGELOG.md index 1b8e6e0fc9f8..df4445517aac 100644 --- a/packages/rest-typings/CHANGELOG.md +++ b/packages/rest-typings/CHANGELOG.md @@ -1,5 +1,45 @@ # @rocket.chat/rest-typings +## 7.1.0 + +### Minor Changes + +- ([#33810](https://github.com/RocketChat/Rocket.Chat/pull/33810)) Adds cursor pagination on chat.syncMessages endpoint + +- ([#32727](https://github.com/RocketChat/Rocket.Chat/pull/32727)) These changes aims to add: + - A brand-new omnichannel contact profile + - The ability to communicate with known contacts only + - Communicate with verified contacts only + - Merge verified contacts across different channels + - Block contact channels + - Resolve conflicting contact information when registered via different channels + - An advanced contact center filters + +### Patch Changes + +-
Updated dependencies [80e36bfc3938775eb26aa5576f1b9b98896e1cc4, 32d93a0666fa1cbe857d02889e93d9bbf45bd4f0]: + + - @rocket.chat/core-typings@7.1.0 +
+ +## 7.1.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.3 +
+ +## 7.1.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.1.0-rc.2 +
+ ## 7.1.0-rc.1 ### Patch Changes diff --git a/packages/rest-typings/src/v1/licenses.ts b/packages/rest-typings/src/v1/licenses.ts index 99ba936e3492..8240546ddc37 100644 --- a/packages/rest-typings/src/v1/licenses.ts +++ b/packages/rest-typings/src/v1/licenses.ts @@ -1,4 +1,4 @@ -import type { LicenseInfo } from '@rocket.chat/core-typings'; +import type { LicenseInfo, Cloud } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; const ajv = new Ajv({ @@ -43,6 +43,7 @@ export type LicensesEndpoints = { '/v1/licenses.info': { GET: (params: licensesInfoProps) => { license: LicenseInfo; + cloudSyncAnnouncement?: Cloud.ICloudSyncAnnouncement; }; }; '/v1/licenses.add': { diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 37a259187b66..b612a2697aa4 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -29,7 +29,6 @@ import type { SMSProviderResponse, ILivechatTriggerActionResponse, ILivechatContact, - ILivechatContactVisitorAssociation, ILivechatContactChannel, IUser, } from '@rocket.chat/core-typings'; @@ -1336,7 +1335,7 @@ const POSTUpdateOmnichannelContactsSchema = { export const isPOSTUpdateOmnichannelContactsProps = ajv.compile(POSTUpdateOmnichannelContactsSchema); -type GETOmnichannelContactsProps = { contactId?: string; visitor?: ILivechatContactVisitorAssociation }; +type GETOmnichannelContactsProps = { contactId?: string }; export const ContactVisitorAssociationSchema = { type: 'object', @@ -1362,28 +1361,16 @@ export const ContactVisitorAssociationSchema = { }; const GETOmnichannelContactsSchema = { - oneOf: [ - { - type: 'object', - properties: { - contactId: { - type: 'string', - nullable: false, - isNotEmpty: true, - }, - }, - required: ['contactId'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - visitor: ContactVisitorAssociationSchema, - }, - required: ['visitor'], - additionalProperties: false, + type: 'object', + properties: { + contactId: { + type: 'string', + nullable: false, + isNotEmpty: true, }, - ], + }, + required: ['contactId'], + additionalProperties: false, }; export const isGETOmnichannelContactsProps = ajv.compile(GETOmnichannelContactsSchema); @@ -1418,6 +1405,58 @@ const GETOmnichannelContactsSearchSchema = { export const isGETOmnichannelContactsSearchProps = ajv.compile(GETOmnichannelContactsSearchSchema); +type GETOmnichannelContactsCheckExistenceProps = { + contactId?: string; + email?: string; + phone?: string; +}; + +const GETOmnichannelContactsCheckExistenceSchema = { + oneOf: [ + { + type: 'object', + properties: { + contactId: { + type: 'string', + nullable: false, + isNotEmpty: true, + }, + }, + required: ['contactId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + email: { + type: 'string', + format: 'basic_email', + nullable: false, + isNotEmpty: true, + }, + }, + required: ['email'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + phone: { + type: 'string', + nullable: false, + isNotEmpty: true, + }, + }, + required: ['phone'], + additionalProperties: false, + }, + ], +}; + +export const isGETOmnichannelContactsCheckExistenceProps = ajv.compile( + GETOmnichannelContactsCheckExistenceSchema, +); + type GETOmnichannelContactHistoryProps = PaginatedRequest<{ contactId: string; source?: string; @@ -3867,6 +3906,9 @@ export type OmnichannelEndpoints = { '/v1/omnichannel/contacts.search': { GET: (params: GETOmnichannelContactsSearchProps) => PaginatedResult<{ contacts: ILivechatContactWithManagerData[] }>; }; + '/v1/omnichannel/contacts.checkExistence': { + GET: (params: GETOmnichannelContactsCheckExistenceProps) => { exists: boolean }; + }; '/v1/omnichannel/contacts.history': { GET: (params: GETOmnichannelContactHistoryProps) => PaginatedResult<{ history: ContactSearchChatsResult[] }>; }; diff --git a/packages/tools/src/convertSubObjectsIntoPaths.spec.ts b/packages/tools/src/convertSubObjectsIntoPaths.spec.ts new file mode 100644 index 000000000000..c595a17ae70e --- /dev/null +++ b/packages/tools/src/convertSubObjectsIntoPaths.spec.ts @@ -0,0 +1,117 @@ +import { expect } from 'chai'; + +import { convertSubObjectsIntoPaths } from './convertSubObjectsIntoPaths'; + +describe('convertSubObjectsIntoPaths', () => { + it('should flatten a simple object with no nested structure', () => { + const input = { a: 1, b: 2, c: 3 }; + const expected = { a: 1, b: 2, c: 3 }; + + expect(convertSubObjectsIntoPaths(input)).to.deep.equal(expected); + }); + + it('should flatten a nested object into paths', () => { + const input = { + a: 1, + b: { + c: 2, + d: { + e: 3, + }, + }, + }; + const expected = { + 'a': 1, + 'b.c': 2, + 'b.d.e': 3, + }; + + expect(convertSubObjectsIntoPaths(input)).to.deep.equal(expected); + }); + + it('should handle objects with array values', () => { + const input = { + a: [1, 2, 3], + b: { + c: [4, 5], + }, + }; + const expected = { + 'a': [1, 2, 3], + 'b.c': [4, 5], + }; + + expect(convertSubObjectsIntoPaths(input)).to.deep.equal(expected); + }); + + it('should handle deeply nested objects', () => { + const input = { + a: { + b: { + c: { + d: { + e: { + f: 6, + }, + }, + }, + }, + }, + }; + const expected = { + 'a.b.c.d.e.f': 6, + }; + + expect(convertSubObjectsIntoPaths(input)).to.deep.equal(expected); + }); + + it('should handle an empty object', () => { + const input = {}; + const expected = {}; + + expect(convertSubObjectsIntoPaths(input)).to.deep.equal(expected); + }); + + it('should handle objects with mixed types of values', () => { + const input = { + a: 1, + b: 'string', + c: true, + d: { + e: null, + f: undefined, + g: { + h: 2, + }, + }, + }; + const expected = { + 'a': 1, + + 'b': 'string', + + 'c': true, + 'd.e': null, + 'd.f': undefined, + 'd.g.h': 2, + }; + + expect(convertSubObjectsIntoPaths(input)).to.deep.equal(expected); + }); + + it('should respect the parentPath parameter', () => { + const input = { + a: 1, + b: { + c: 2, + }, + }; + const parentPath = 'root'; + const expected = { + 'root.a': 1, + 'root.b.c': 2, + }; + + expect(convertSubObjectsIntoPaths(input, parentPath)).to.deep.equal(expected); + }); +}); diff --git a/packages/tools/src/convertSubObjectsIntoPaths.ts b/packages/tools/src/convertSubObjectsIntoPaths.ts new file mode 100644 index 000000000000..8c128aa19f2c --- /dev/null +++ b/packages/tools/src/convertSubObjectsIntoPaths.ts @@ -0,0 +1,16 @@ +export function convertSubObjectsIntoPaths(object: Record, parentPath?: string): Record { + return Object.fromEntries( + Object.keys(object).flatMap((key) => { + const value = object[key]; + const fullKey = parentPath ? `${parentPath}.${key}` : key; + + if (typeof value === 'object' && !Array.isArray(value) && value !== null) { + const flattened = convertSubObjectsIntoPaths(value, fullKey); + + return Object.keys(flattened).map((newKey) => [newKey, flattened[newKey]]); + } + + return [[fullKey, value]]; + }) as [string, any][], + ); +} diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index 96faa4d55969..410bd711d24a 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -1,5 +1,7 @@ +export * from './convertSubObjectsIntoPaths'; export * from './getObjectKeys'; export * from './normalizeLanguage'; +export * from './objectMap'; export * from './pick'; export * from './stream'; export * from './timezone'; diff --git a/packages/tools/src/objectMap.spec.ts b/packages/tools/src/objectMap.spec.ts new file mode 100644 index 000000000000..15299b9614a0 --- /dev/null +++ b/packages/tools/src/objectMap.spec.ts @@ -0,0 +1,93 @@ +import { expect } from 'chai'; + +import { objectMap } from './objectMap'; + +describe('objectMap', () => { + it('should map a simple object non-recursively', () => { + const input = { a: 1, b: 2, c: 3 }; + const callback = ({ key, value }) => ({ key: key.toUpperCase(), value: value * 2 }); + const expected = { A: 2, B: 4, C: 6 }; + expect(objectMap(input, callback)).to.deep.equal(expected); + }); + it('should filter out undefined results from callback', () => { + const input = { a: 1, b: 2, c: 3 }; + const callback = ({ key, value }) => (value > 1 ? { key, value } : undefined); + const expected = { b: 2, c: 3 }; + expect(objectMap(input, callback)).to.deep.equal(expected); + }); + it('should map a nested object recursively', () => { + const input = { + a: 1, + b: { + c: 2, + d: { + e: 3, + }, + }, + }; + const callback = ({ key, value }) => ({ key: `mapped_${key}`, value: typeof value === 'number' ? value * 10 : value }); + const expected = { + mapped_a: 10, + mapped_b: { + mapped_c: 20, + mapped_d: { + mapped_e: 30, + }, + }, + }; + expect(objectMap(input, callback, true)).to.deep.equal(expected); + }); + it('should handle an empty object', () => { + const input = {}; + const callback = ({ key, value }) => ({ key: `mapped_${key}`, value }); + const expected = {}; + expect(objectMap(input, callback)).to.deep.equal(expected); + }); + it('should handle mixed value types in non-recursive mode', () => { + const input = { + a: 1, + b: 'string', + c: true, + d: null, + }; + const callback = ({ key, value }) => ({ key: key.toUpperCase(), value: typeof value === 'number' ? value * 2 : value }); + const expected = { + A: 2, + B: 'string', + C: true, + D: null, + }; + expect(objectMap(input, callback)).to.deep.equal(expected); + }); + it('should handle nested objects with mixed types recursively', () => { + const input = { + a: 1, + b: { + c: 'string', + d: { + e: true, + f: null, + }, + }, + }; + const callback = ({ key, value }) => ({ key: key.toUpperCase(), value }); + const expected = { + A: 1, + B: { + C: 'string', + D: { + E: true, + F: null, + }, + }, + }; + expect(objectMap(input, callback, true)).to.deep.equal(expected); + }); + it('should not modify the original object', () => { + const input = { a: 1, b: 2 }; + const original = { ...input }; + const callback = ({ key, value }) => ({ key, value: value * 2 }); + objectMap(input, callback); + expect(input).to.deep.equal(original); + }); +}); diff --git a/packages/tools/src/objectMap.ts b/packages/tools/src/objectMap.ts new file mode 100644 index 000000000000..a28b37ed5048 --- /dev/null +++ b/packages/tools/src/objectMap.ts @@ -0,0 +1,35 @@ +export function objectMap = Record, K extends keyof TObject | string = keyof TObject>( + object: TObject, + cb: (value: { key: K; value: TObject[K] }) => { key: string | number | symbol; value: any } | undefined, + recursive?: false, +): Record; +export function objectMap = Record>( + object: TObject, + cb: (value: { key: string | number | symbol; value: any }) => { key: string | number | symbol; value: any } | undefined, + recursive: true, +): Record; +export function objectMap = Record, K extends keyof TObject | string = keyof TObject>( + object: TObject, + cb: (value: { key: K; value: any }) => { key: string | number | symbol; value: any } | undefined, + recursive: false, +): Record; +export function objectMap = Record, K extends keyof TObject | string = keyof TObject>( + object: TObject, + cb: (value: { key: K | string; value: any }) => { key: string | number | symbol; value: any } | undefined, + recursive = false, +): Record { + return Object.fromEntries( + Object.keys(object) + .map((key) => { + const value = object[key as K]; + if (recursive && value && typeof value === 'object' && !Array.isArray(value) && !((value as any) instanceof Date)) { + const newValue = objectMap(value, cb as any, true); + return cb({ key, value: newValue }); + } + + return cb({ key, value }); + }) + .filter((item) => !!item) + .map((item) => [item.key, item.value]), + ); +} diff --git a/packages/ui-avatar/CHANGELOG.md b/packages/ui-avatar/CHANGELOG.md index 308ac76e886c..273699c706ac 100644 --- a/packages/ui-avatar/CHANGELOG.md +++ b/packages/ui-avatar/CHANGELOG.md @@ -1,5 +1,32 @@ # @rocket.chat/ui-avatar +## 9.0.0 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@13.0.0 +
+ +## 9.0.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@13.0.0-rc.3 +
+ +## 9.0.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@13.0.0-rc.2 +
+ ## 9.0.0-rc.1 ### Patch Changes diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index 642ea1391e8a..c4a216d909dd 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -1,10 +1,10 @@ { "name": "@rocket.chat/ui-avatar", - "version": "9.0.0-rc.1", + "version": "9.0.0", "private": true, "devDependencies": { "@babel/core": "~7.26.0", - "@rocket.chat/fuselage": "^0.59.4", + "@rocket.chat/fuselage": "^0.60.0", "@rocket.chat/ui-contexts": "workspace:^", "@types/react": "~17.0.80", "@types/react-dom": "~17.0.25", @@ -30,7 +30,7 @@ ], "peerDependencies": { "@rocket.chat/fuselage": "*", - "@rocket.chat/ui-contexts": "13.0.0-rc.1", + "@rocket.chat/ui-contexts": "13.0.0", "react": "~17.0.2" }, "volta": { diff --git a/packages/ui-avatar/src/components/BaseAvatar.tsx b/packages/ui-avatar/src/components/BaseAvatar.tsx index 80329946fd4b..72845a42ce5a 100644 --- a/packages/ui-avatar/src/components/BaseAvatar.tsx +++ b/packages/ui-avatar/src/components/BaseAvatar.tsx @@ -6,7 +6,7 @@ import { useState } from 'react'; export type BaseAvatarProps = Omit; -const BaseAvatar = ({ url, onLoad, onError, ...props }: BaseAvatarProps) => { +const BaseAvatar = ({ url, onLoad, onError, size, ...props }: BaseAvatarProps) => { const [unloaded, setUnloaded] = useState(false); const prevUrl = usePrevious(url); @@ -21,10 +21,10 @@ const BaseAvatar = ({ url, onLoad, onError, ...props }: BaseAvatarProps) => { }); if (unloaded && url === prevUrl) { - return
diff --git a/packages/ui-video-conf/src/VideoConfPopup/__snapshots__/VideoConfPopup.spec.tsx.snap b/packages/ui-video-conf/src/VideoConfPopup/__snapshots__/VideoConfPopup.spec.tsx.snap index 358cec7d7f7d..91cb7ba7f91b 100644 --- a/packages/ui-video-conf/src/VideoConfPopup/__snapshots__/VideoConfPopup.spec.tsx.snap +++ b/packages/ui-video-conf/src/VideoConfPopup/__snapshots__/VideoConfPopup.spec.tsx.snap @@ -33,7 +33,7 @@ exports[`renders StartCall without crashing 1`] = `