diff --git a/.changeset/brave-brooms-invent.md b/.changeset/brave-brooms-invent.md deleted file mode 100644 index 35d32b485944..000000000000 --- a/.changeset/brave-brooms-invent.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixes a problem that caused visitor creation to fail when GDPR setting was enabled and visitor was created via Apps Engine or the deprecated `livechat:registerGuest` method. diff --git a/.changeset/brown-singers-appear.md b/.changeset/brown-singers-appear.md deleted file mode 100644 index 8a9a69f225ac..000000000000 --- a/.changeset/brown-singers-appear.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@rocket.chat/ui-client': minor -'@rocket.chat/i18n': minor -'@rocket.chat/meteor': minor ---- - -added `sidepanelNavigation` to feature preview list diff --git a/.changeset/bump-patch-1727212585363.md b/.changeset/bump-patch-1727212585363.md deleted file mode 100644 index e1eaa7980afb..000000000000 --- a/.changeset/bump-patch-1727212585363.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Bump @rocket.chat/meteor version. diff --git a/.changeset/chilled-files-relate.md b/.changeset/chilled-files-relate.md new file mode 100644 index 000000000000..a16cfda59217 --- /dev/null +++ b/.changeset/chilled-files-relate.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Adds methods to the Apps-Engine to interact with unread messages to enhance message capabilities on Apps. \ No newline at end of file diff --git a/.changeset/cyan-ladybugs-thank.md b/.changeset/cyan-ladybugs-thank.md deleted file mode 100644 index 377a014fcb72..000000000000 --- a/.changeset/cyan-ladybugs-thank.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed error during sendmessage client stub diff --git a/.changeset/dirty-stingrays-beg.md b/.changeset/dirty-stingrays-beg.md deleted file mode 100644 index cf5e3a4ca839..000000000000 --- a/.changeset/dirty-stingrays-beg.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/model-typings": minor -"@rocket.chat/rest-typings": minor ---- - -Added support for specifying a unit on departments' creation and update diff --git a/.changeset/e2ee-composer-freeze.md b/.changeset/e2ee-composer-freeze.md new file mode 100644 index 000000000000..8814a2ba90eb --- /dev/null +++ b/.changeset/e2ee-composer-freeze.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes E2EE composer freezing when the room state changes diff --git a/.changeset/five-coats-rhyme.md b/.changeset/five-coats-rhyme.md deleted file mode 100644 index c5359e3c978a..000000000000 --- a/.changeset/five-coats-rhyme.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/fuselage-ui-kit': patch ---- - -Fixed an error that incorrectly showed conference calls as not answered after they ended diff --git a/.changeset/four-cherries-kneel.md b/.changeset/four-cherries-kneel.md deleted file mode 100644 index 095d5af0aa76..000000000000 --- a/.changeset/four-cherries-kneel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Allow to use the token from `room.v` when requesting transcript instead of visitor token. Visitors may change their tokens at any time, rendering old conversations impossible to access for them (or for APIs depending on token) as the visitor token won't match the `room.v` token. diff --git a/.changeset/four-experts-compare.md b/.changeset/four-experts-compare.md new file mode 100644 index 000000000000..b9ce498e4a4b --- /dev/null +++ b/.changeset/four-experts-compare.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Introduces a new featured action on the room header for action buttons using the non-default category to enhance user accessibility. diff --git a/.changeset/gold-falcons-hear.md b/.changeset/gold-falcons-hear.md new file mode 100644 index 000000000000..0e47de679ae3 --- /dev/null +++ b/.changeset/gold-falcons-hear.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where a thread message was not scrolled to view when it was opened using its link. diff --git a/.changeset/great-humans-live.md b/.changeset/great-humans-live.md deleted file mode 100644 index 1d97d9da23ae..000000000000 --- a/.changeset/great-humans-live.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed a Federation callback not awaiting db call diff --git a/.changeset/grumpy-lamps-beg.md b/.changeset/grumpy-lamps-beg.md new file mode 100644 index 000000000000..c9c4d2d9faec --- /dev/null +++ b/.changeset/grumpy-lamps-beg.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed Twilio request validation by accepting URLs with query strings diff --git a/.changeset/healthy-rivers-nail.md b/.changeset/healthy-rivers-nail.md deleted file mode 100644 index a8da9bec846e..000000000000 --- a/.changeset/healthy-rivers-nail.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/i18n": minor -"@rocket.chat/livechat": minor ---- - -Added new setting `Allow visitors to finish conversations` that allows admins to decide if omnichannel visitors can close a conversation or not. This doesn't affect agent's capabilities of room closing, neither apps using the livechat bridge to close rooms. -However, if currently your integration relies on `livechat/room.close` endpoint for closing conversations, it's advised to use the authenticated version `livechat/room.closeByUser` of it before turning off this setting. diff --git a/.changeset/heavy-snails-help.md b/.changeset/heavy-snails-help.md deleted file mode 100644 index fb10bac9ea8f..000000000000 --- a/.changeset/heavy-snails-help.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/rest-typings": minor ---- - -Implemented "omnichannel/contacts.update" endpoint to update contacts diff --git a/.changeset/hot-balloons-travel.md b/.changeset/hot-balloons-travel.md deleted file mode 100644 index d6154babc49d..000000000000 --- a/.changeset/hot-balloons-travel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed issue where when you marked a room as unread and you were part of it, sometimes it would mark it as read right after diff --git a/.changeset/khaki-cameras-glow.md b/.changeset/khaki-cameras-glow.md deleted file mode 100644 index 87470d5c497b..000000000000 --- a/.changeset/khaki-cameras-glow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixes an issue where the retention policy warning keep displaying even if the retention is disabled inside the room diff --git a/.changeset/kind-llamas-grin.md b/.changeset/kind-llamas-grin.md deleted file mode 100644 index fd349e82d7f9..000000000000 --- a/.changeset/kind-llamas-grin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Changed the contextualbar behavior based on chat size instead the viewport diff --git a/.changeset/late-planes-sniff.md b/.changeset/late-planes-sniff.md deleted file mode 100644 index d702a938da78..000000000000 --- a/.changeset/late-planes-sniff.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/core-typings": patch -"@rocket.chat/i18n": patch ---- - -Added a new setting to enable mentions in end to end encrypted channels diff --git a/.changeset/many-balloons-scream.md b/.changeset/many-balloons-scream.md deleted file mode 100644 index f017cdb81137..000000000000 --- a/.changeset/many-balloons-scream.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@rocket.chat/uikit-playground': minor -'@rocket.chat/fuselage-ui-kit': minor -'@rocket.chat/ui-theming': minor -'@rocket.chat/ui-video-conf': minor -'@rocket.chat/ui-composer': minor -'@rocket.chat/gazzodown': minor -'@rocket.chat/ui-avatar': minor -'@rocket.chat/ui-client': minor -'@rocket.chat/meteor': minor ---- - -Replaced new `SidebarV2` components under feature preview diff --git a/.changeset/many-files-turn.md b/.changeset/many-files-turn.md new file mode 100644 index 000000000000..6f53374ddd86 --- /dev/null +++ b/.changeset/many-files-turn.md @@ -0,0 +1,11 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": patch +"@rocket.chat/ui-composer": minor +--- + +Adds a warning to inform users they are about to send unencrypted messages in an E2E Encrypted room if they have the `Unencrypted messages in encrypted rooms` setting enabled. + + + + diff --git a/.changeset/many-rules-shout.md b/.changeset/many-rules-shout.md deleted file mode 100644 index eacb88108a0f..000000000000 --- a/.changeset/many-rules-shout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) diff --git a/.changeset/mighty-drinks-hide.md b/.changeset/mighty-drinks-hide.md deleted file mode 100644 index 955d1ed760cc..000000000000 --- a/.changeset/mighty-drinks-hide.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/fuselage-ui-kit": patch ---- - -Fixes multiple selection for MultiStaticSelectElement in UiKit diff --git a/.changeset/nasty-tools-enjoy.md b/.changeset/nasty-tools-enjoy.md deleted file mode 100644 index b6e8dae3785a..000000000000 --- a/.changeset/nasty-tools-enjoy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed a code issue on NPS service. It was passing `startAt` as the expiration date when creating a banner. diff --git a/.changeset/pink-swans-teach.md b/.changeset/pink-swans-teach.md deleted file mode 100644 index 7c85572a78d5..000000000000 --- a/.changeset/pink-swans-teach.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -fixed retention policy max age settings not being respected after upgrade diff --git a/.changeset/pink-wombats-wait.md b/.changeset/pink-wombats-wait.md new file mode 100644 index 000000000000..5c8d4cb6a0b1 --- /dev/null +++ b/.changeset/pink-wombats-wait.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +--- + +Fixes an issue causing server to not notify users via websocket of new E2EE keys suggested by other users to them when running in development environments. diff --git a/.changeset/poor-falcons-doubt.md b/.changeset/poor-falcons-doubt.md new file mode 100644 index 000000000000..cb297885c5a9 --- /dev/null +++ b/.changeset/poor-falcons-doubt.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/livechat': patch +--- + +Fixes an issue where the unread message counter in the livechat widget does not update when a visitor receives their first response from an agent while the widget is minimized. diff --git a/.changeset/pre.json b/.changeset/pre.json deleted file mode 100644 index 7c415a6b0dde..000000000000 --- a/.changeset/pre.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "mode": "pre", - "tag": "rc", - "initialVersions": { - "@rocket.chat/meteor": "6.13.0-develop", - "rocketchat-services": "1.3.3", - "@rocket.chat/uikit-playground": "0.4.0", - "@rocket.chat/account-service": "0.4.6", - "@rocket.chat/authorization-service": "0.4.6", - "@rocket.chat/ddp-streamer": "0.3.6", - "@rocket.chat/omnichannel-transcript": "0.4.6", - "@rocket.chat/presence-service": "0.4.6", - "@rocket.chat/queue-worker": "0.4.6", - "@rocket.chat/stream-hub-service": "0.4.6", - "@rocket.chat/license": "0.2.6", - "@rocket.chat/omnichannel-services": "0.3.3", - "@rocket.chat/pdf-worker": "0.2.3", - "@rocket.chat/presence": "0.2.6", - "@rocket.chat/ui-theming": "0.2.1", - "@rocket.chat/account-utils": "0.0.2", - "@rocket.chat/agenda": "0.1.0", - "@rocket.chat/api-client": "0.2.6", - "@rocket.chat/apps": "0.1.6", - "@rocket.chat/base64": "1.0.13", - "@rocket.chat/cas-validate": "0.0.2", - "@rocket.chat/core-services": "0.6.0", - "@rocket.chat/core-typings": "6.13.0-develop", - "@rocket.chat/cron": "0.1.6", - "@rocket.chat/ddp-client": "0.3.6", - "@rocket.chat/eslint-config": "0.7.0", - "@rocket.chat/favicon": "0.0.2", - "@rocket.chat/fuselage-ui-kit": "10.0.0", - "@rocket.chat/gazzodown": "10.0.0", - "@rocket.chat/i18n": "0.7.0", - "@rocket.chat/instance-status": "0.1.6", - "@rocket.chat/jest-presets": "0.0.1", - "@rocket.chat/jwt": "0.1.1", - "@rocket.chat/livechat": "1.19.3", - "@rocket.chat/log-format": "0.0.2", - "@rocket.chat/logger": "0.0.2", - "@rocket.chat/message-parser": "0.31.29", - "@rocket.chat/mock-providers": "0.1.2", - "@rocket.chat/model-typings": "0.7.0", - "@rocket.chat/models": "0.2.3", - "@rocket.chat/poplib": "0.0.2", - "@rocket.chat/password-policies": "0.0.2", - "@rocket.chat/patch-injection": "0.0.1", - "@rocket.chat/peggy-loader": "0.31.25", - "@rocket.chat/random": "1.2.2", - "@rocket.chat/release-action": "2.2.3", - "@rocket.chat/release-changelog": "0.1.0", - "@rocket.chat/rest-typings": "6.13.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/ui-avatar": "6.0.0", - "@rocket.chat/ui-client": "10.0.0", - "@rocket.chat/ui-composer": "0.2.1", - "@rocket.chat/ui-contexts": "10.0.0", - "@rocket.chat/ui-kit": "0.36.1", - "@rocket.chat/ui-video-conf": "10.0.0", - "@rocket.chat/web-ui-registration": "10.0.0" - }, - "changesets": [ - "brave-brooms-invent", - "brown-singers-appear", - "bump-patch-1727212585363", - "cyan-ladybugs-thank", - "dirty-stingrays-beg", - "five-coats-rhyme", - "four-cherries-kneel", - "great-humans-live", - "healthy-rivers-nail", - "heavy-snails-help", - "hot-balloons-travel", - "khaki-cameras-glow", - "kind-llamas-grin", - "late-planes-sniff", - "many-balloons-scream", - "many-rules-shout", - "mighty-drinks-hide", - "nasty-tools-enjoy", - "pink-swans-teach", - "quick-rings-wave", - "quiet-cherries-punch", - "rich-toes-bow", - "rotten-rabbits-brush", - "short-drinks-itch", - "sixty-spoons-own", - "small-crabs-travel", - "soft-mirrors-remember", - "spicy-rocks-burn", - "strong-grapes-brake", - "stupid-pigs-share", - "sweet-nails-grin", - "tame-mayflies-press", - "tiny-geckos-kiss", - "wet-hats-walk", - "wise-avocados-taste", - "witty-lemons-type" - ] -} diff --git a/.changeset/proud-bugs-cry.md b/.changeset/proud-bugs-cry.md new file mode 100644 index 000000000000..854f9c6372fd --- /dev/null +++ b/.changeset/proud-bugs-cry.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/apps-engine': patch +--- + +Fixed a problem in the deno runtime controller where it would not handle undefined child process references correctly diff --git a/.changeset/quick-rings-wave.md b/.changeset/quick-rings-wave.md deleted file mode 100644 index 0ea22897ff45..000000000000 --- a/.changeset/quick-rings-wave.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/i18n": minor -"@rocket.chat/ui-client": minor ---- - -Added new Admin Feature Preview management view, this will allow the workspace admins to both enable feature previewing in the workspace as well as define which feature previews are enabled by default for the users in the workspace. diff --git a/.changeset/quiet-cherries-punch.md b/.changeset/quiet-cherries-punch.md deleted file mode 100644 index 25c08506db41..000000000000 --- a/.changeset/quiet-cherries-punch.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/core-services": minor -"@rocket.chat/rest-typings": minor ---- - -Return `parent` and `team` information when calling `rooms.info` endpoint diff --git a/.changeset/red-crews-behave.md b/.changeset/red-crews-behave.md new file mode 100644 index 000000000000..19608792e73d --- /dev/null +++ b/.changeset/red-crews-behave.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/ui-kit': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Introduces new property `category` for Rocket.Chat Apps to register UI action buttons. This property is used to group buttons in the UI. diff --git a/.changeset/rich-toes-bow.md b/.changeset/rich-toes-bow.md deleted file mode 100644 index a670f0756f1e..000000000000 --- a/.changeset/rich-toes-bow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Prevented uiInteraction to subscribe multiple times diff --git a/.changeset/rotten-rabbits-brush.md b/.changeset/rotten-rabbits-brush.md deleted file mode 100644 index 916f4cc8034a..000000000000 --- a/.changeset/rotten-rabbits-brush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Resolves the issue where outgoing integrations failed to trigger after the version 6.12.0 upgrade by correcting the parameter order from the `afterSaveMessage` callback to listener functions. This ensures the correct room information is passed, restoring the functionality of outgoing webhooks, IRC bridge, Autotranslate, and Engagement Dashboard. diff --git a/.changeset/seven-hotels-collect.md b/.changeset/seven-hotels-collect.md new file mode 100644 index 000000000000..7eb22980d29f --- /dev/null +++ b/.changeset/seven-hotels-collect.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes a problem with custom sounds causing files to not be playable when using filesystem storage. diff --git a/.changeset/sharp-adults-think.md b/.changeset/sharp-adults-think.md new file mode 100644 index 000000000000..399d3bae1ce2 --- /dev/null +++ b/.changeset/sharp-adults-think.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +--- + +Fixes the departments filter on the omnichannel current chats page by ensuring that the selected department is fetched and +added if it was not part of the initial department list. This prevents the filter from becoming blank and avoids potential +UX issues. diff --git a/.changeset/short-drinks-itch.md b/.changeset/short-drinks-itch.md deleted file mode 100644 index ee57330ffc86..000000000000 --- a/.changeset/short-drinks-itch.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@rocket.chat/message-parser': patch -'@rocket.chat/peggy-loader': patch ---- - -Improved the performance of the message parser diff --git a/.changeset/sixty-spoons-own.md b/.changeset/sixty-spoons-own.md deleted file mode 100644 index 0b717c3965ef..000000000000 --- a/.changeset/sixty-spoons-own.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/core-typings": minor -"@rocket.chat/model-typings": minor -"@rocket.chat/models": minor -"@rocket.chat/rest-typings": minor ---- - -Introduced "create contacts" endpoint to omnichannel diff --git a/.changeset/small-crabs-travel.md b/.changeset/small-crabs-travel.md deleted file mode 100644 index 201494a5b70f..000000000000 --- a/.changeset/small-crabs-travel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed avatar blob image setting in setUserAvatar method by correcting service handling logic. diff --git a/.changeset/soft-mirrors-remember.md b/.changeset/soft-mirrors-remember.md deleted file mode 100644 index 78b005ee6b6e..000000000000 --- a/.changeset/soft-mirrors-remember.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@rocket.chat/meteor": minor -"@rocket.chat/core-services": minor -"@rocket.chat/model-typings": minor -"@rocket.chat/rest-typings": minor ---- - -New `teams.listChildren` endpoint that allows users listing rooms & discussions from teams. Only the discussions from the team's main room are returned. diff --git a/.changeset/spicy-rocks-burn.md b/.changeset/spicy-rocks-burn.md deleted file mode 100644 index 6468dbbec241..000000000000 --- a/.changeset/spicy-rocks-burn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed `LivechatSessionTaken` webhook event being called without the `agent` param, which represents the agent serving the room. diff --git a/.changeset/strange-spies-clean.md b/.changeset/strange-spies-clean.md new file mode 100644 index 000000000000..68ec261509f6 --- /dev/null +++ b/.changeset/strange-spies-clean.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Applied category reorder from admin setting also on new sidebar - Enhanced navigation feature preview diff --git a/.changeset/strong-grapes-brake.md b/.changeset/strong-grapes-brake.md deleted file mode 100644 index c867600a8cd2..000000000000 --- a/.changeset/strong-grapes-brake.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed remaining direct references to external user avatar URLs - -Fixed local avatars having priority over external provider - -It mainly corrects the behavior of E2E encryption messages and desktop notifications. diff --git a/.changeset/strong-waves-add.md b/.changeset/strong-waves-add.md new file mode 100644 index 000000000000..c3b6bcf08cd2 --- /dev/null +++ b/.changeset/strong-waves-add.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue where a user may receive an indefinite amount of "suggested keys" from all the users on a room even when a user already suggested a key for them, duplicating the work on server. + +Now, the endpoint that returns users waiting for keys will return only users that don't have neither `E2EKey` nor `E2ESuggestedKey`. diff --git a/.changeset/stupid-pigs-share.md b/.changeset/stupid-pigs-share.md deleted file mode 100644 index 55d68c66d587..000000000000 --- a/.changeset/stupid-pigs-share.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': minor ---- - -Wraps some room settings in an accordion advanced settings section in room edit contextual bar to improve organization diff --git a/.changeset/sweet-nails-grin.md b/.changeset/sweet-nails-grin.md deleted file mode 100644 index de240bfc0e3f..000000000000 --- a/.changeset/sweet-nails-grin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed inconsistency between the markdown parser from the composer and the rest of the application when using bold and italics in a text. diff --git a/.changeset/tame-mayflies-press.md b/.changeset/tame-mayflies-press.md deleted file mode 100644 index e470306cbc25..000000000000 --- a/.changeset/tame-mayflies-press.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": minor ---- - -Implemented sending email via apps diff --git a/.changeset/tender-cheetahs-teach.md b/.changeset/tender-cheetahs-teach.md new file mode 100644 index 000000000000..bbb956d74715 --- /dev/null +++ b/.changeset/tender-cheetahs-teach.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/ui-client': patch +'@rocket.chat/meteor': patch +--- + +Fixed an issue where "Filter by room type" was selectable in the Rooms filter. diff --git a/.changeset/tiny-geckos-kiss.md b/.changeset/tiny-geckos-kiss.md deleted file mode 100644 index d38150970310..000000000000 --- a/.changeset/tiny-geckos-kiss.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@rocket.chat/i18n': minor -'@rocket.chat/meteor': minor ---- - -Added a new setting which allows workspace admins to disable email two factor authentication for SSO (OAuth) users. If enabled, SSO users won't be asked for email two factor authentication. diff --git a/.changeset/wet-hats-walk.md b/.changeset/wet-hats-walk.md deleted file mode 100644 index 4c3565e75523..000000000000 --- a/.changeset/wet-hats-walk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed issue that caused an infinite loading state when uploading a private app to Rocket.Chat diff --git a/.changeset/wise-avocados-taste.md b/.changeset/wise-avocados-taste.md deleted file mode 100644 index c4c9bce010b8..000000000000 --- a/.changeset/wise-avocados-taste.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixes an issue where multi-step modals were closing unexpectedly diff --git a/.changeset/witty-lemons-type.md b/.changeset/witty-lemons-type.md deleted file mode 100644 index a007cbe6260e..000000000000 --- a/.changeset/witty-lemons-type.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@rocket.chat/core-services': minor -'@rocket.chat/model-typings': minor -'@rocket.chat/core-typings': minor -'@rocket.chat/rest-typings': minor -'@rocket.chat/ui-client': minor -'@rocket.chat/meteor': minor ---- - -Implemented new feature preview for Sidepanel diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml index ae84e376a0d9..d0e9d3ce4f52 100644 --- a/.github/actions/build-docker/action.yml +++ b/.github/actions/build-docker/action.yml @@ -75,6 +75,12 @@ runs: install: true NPM_TOKEN: ${{ inputs.NPM_TOKEN }} + - name: Restore turbo build + uses: actions/download-artifact@v4 + with: + name: turbo-build + path: .turbo/cache + - run: yarn build if: inputs.setup == 'true' shell: bash diff --git a/.github/workflows/ci-code-check.yml b/.github/workflows/ci-code-check.yml index 41facad89a03..6712cc49356d 100644 --- a/.github/workflows/ci-code-check.yml +++ b/.github/workflows/ci-code-check.yml @@ -49,6 +49,11 @@ jobs: - uses: rharkor/caching-for-turbo@v1.5 + - name: Restore turbo build + uses: actions/download-artifact@v4 + with: + name: turbo-build + path: .turbo/cache - name: Cache TypeCheck uses: actions/cache@v3 if: matrix.check == 'ts' diff --git a/.github/workflows/ci-deploy-gh-pages-preview.yml b/.github/workflows/ci-deploy-gh-pages-preview.yml deleted file mode 100644 index 8a0905a174bb..000000000000 --- a/.github/workflows/ci-deploy-gh-pages-preview.yml +++ /dev/null @@ -1,42 +0,0 @@ -# .github/workflows/ci-preview.yml -name: Deploy PR previews -concurrency: preview-${{ github.ref }} -on: - pull_request: - types: - - opened - - reopened - - synchronize - - closed - -jobs: - deploy-preview: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - uses: rharkor/caching-for-turbo@v1.5 - if: github.event.action != 'closed' - - - name: Setup NodeJS - uses: ./.github/actions/setup-node - if: github.event.action != 'closed' - with: - node-version: 14.21.3 - deno-version: 1.37.1 - cache-modules: true - install: true - - - name: Build - if: github.event.action != 'closed' - run: | - yarn turbo run build-preview - yarn turbo run .:build-preview-move - npx indexifier .preview --html --extensions .html > .preview/index.html - - - uses: rossjrw/pr-preview-action@v1 - with: - source-dir: .preview - preview-branch: gh-pages - umbrella-dir: pr-preview - action: auto diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index f219f39c0614..d26bfdfe47f4 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -138,6 +138,11 @@ jobs: - uses: rharkor/caching-for-turbo@v1.5 + - name: Restore turbo build + uses: actions/download-artifact@v4 + with: + name: turbo-build + path: .turbo/cache - run: yarn build # if we are testing a PR from a fork, we need to build the docker image at this point - uses: ./.github/actions/build-docker diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml index 883212d0cf3d..3c02c933ddb4 100644 --- a/.github/workflows/ci-test-unit.yml +++ b/.github/workflows/ci-test-unit.yml @@ -47,6 +47,11 @@ jobs: - uses: rharkor/caching-for-turbo@v1.5 + - name: Restore turbo build + uses: actions/download-artifact@v4 + with: + name: turbo-build + path: .turbo/cache - name: Unit Test run: yarn testunit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b6fa426ca96..20e42203d45f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -174,6 +174,50 @@ jobs: - name: Build Rocket.Chat Packages run: yarn build + - name: Store turbo build + uses: actions/upload-artifact@v4 + with: + name: turbo-build + path: .turbo/cache + overwrite: true + include-hidden-files: true + + deploy-preview: + runs-on: ubuntu-latest + needs: [release-versions, packages-build] + steps: + - uses: actions/checkout@v3 + + - uses: rharkor/caching-for-turbo@v1.5 + if: github.event.action != 'closed' + + - name: Setup NodeJS + uses: ./.github/actions/setup-node + if: github.event.action != 'closed' + with: + node-version: 14.21.3 + deno-version: 1.37.1 + cache-modules: true + install: true + - name: Restore turbo build + uses: actions/download-artifact@v4 + with: + name: turbo-build + path: .turbo/cache + - name: Build + if: github.event.action != 'closed' + run: | + yarn turbo run build-preview + yarn turbo run .:build-preview-move + npx indexifier .preview --html --extensions .html > .preview/index.html + + - uses: rossjrw/pr-preview-action@v1 + with: + source-dir: .preview + preview-branch: gh-pages + umbrella-dir: pr-preview + action: auto + build: name: 📦 Meteor Build - coverage needs: [release-versions, packages-build] diff --git a/.github/workflows/update-version-durability.yml b/.github/workflows/update-version-durability.yml index 90c835577dc1..48ae2572c217 100644 --- a/.github/workflows/update-version-durability.yml +++ b/.github/workflows/update-version-durability.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v4 - name: Use Node.js - uses: actions/setup-node@v4.0.3 + uses: actions/setup-node@v4.0.4 with: node-version: '20.15.1' diff --git a/.yarn/patches/react-i18next-npm-15.0.1-0812bb73aa.patch b/.yarn/patches/react-i18next-npm-15.0.1-0812bb73aa.patch deleted file mode 100644 index cf5a292c0253..000000000000 --- a/.yarn/patches/react-i18next-npm-15.0.1-0812bb73aa.patch +++ /dev/null @@ -1,99 +0,0 @@ -diff --git a/dist/amd/react-i18next.js b/dist/amd/react-i18next.js -index 115ef3cc362e5ce38e875f9b35dfd1fe687cfd6c..2ba1b4d54295eeff49c8c650f214d7875d9219a5 100644 ---- a/dist/amd/react-i18next.js -+++ b/dist/amd/react-i18next.js -@@ -495,7 +495,7 @@ define(['exports', 'react'], (function (exports, react) { 'use strict'; - } - addUsedNamespaces(namespaces) { - namespaces.forEach(ns => { -- this.usedNamespaces[ns] ??= true; -+ this.usedNamespaces[ns] = this.usedNamespaces[ns] ?? true; - }); - } - getUsedNamespaces() { -diff --git a/dist/amd/react-i18next.min.js b/dist/amd/react-i18next.min.js -index c54c9114869feb14df7855be24237f85d64fe7e4..0cc2b09d6db9bfc6f94dffff4b9e0f3fd9108510 100644 ---- a/dist/amd/react-i18next.min.js -+++ b/dist/amd/react-i18next.min.js -@@ -1 +1 @@ --define(["exports","react"],(function(e,n){"use strict";function t(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var s=t({area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0}),a=/\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;function i(e){var n={type:"tag",name:"",voidElement:!1,attrs:{},children:[]},t=e.match(/<\/?([^\s]+?)[/\s>]/);if(t&&(n.name=t[1],(s[t[1]]||"/"===e.charAt(e.length-2))&&(n.voidElement=!0),n.name.startsWith("!--"))){var i=e.indexOf("--\x3e");return{type:"comment",comment:-1!==i?e.slice(4,i):""}}for(var r=new RegExp(a),o=null;null!==(o=r.exec(e));)if(o[0].trim())if(o[1]){var l=o[1].trim(),c=[l,""];l.indexOf("=")>-1&&(c=l.split("=")),n.attrs[c[0]]=c[1],r.lastIndex--}else o[2]&&(n.attrs[o[2]]=o[3].trim().substring(1,o[3].length-1));return n}var r=/<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g,o=/^\s*$/,l=Object.create(null);var c=function(e,n){n||(n={}),n.components||(n.components=l);var t,s=[],a=[],c=-1,u=!1;if(0!==e.indexOf("<")){var p=e.indexOf("<");s.push({type:"text",content:-1===p?e:e.substring(0,p)})}return e.replace(r,(function(r,l){if(u){if(r!=="")return;u=!1}var p,d="/"!==r.charAt(1),f=r.startsWith("\x3c!--"),h=l+r.length,m=e.charAt(h);if(f){var g=i(r);return c<0?(s.push(g),s):((p=a[c]).children.push(g),s)}if(d&&(c++,"tag"===(t=i(r)).type&&n.components[t.name]&&(t.type="component",u=!0),t.voidElement||u||!m||"<"===m||t.children.push({type:"text",content:e.slice(h,e.indexOf("<",h))}),0===c&&s.push(t),(p=a[c-1])&&p.children.push(t),a[c]=t),(!d||t.voidElement)&&(c>-1&&(t.voidElement||t.name===r.slice(2,-1))&&(c--,t=-1===c?s:a[c]),!u&&"<"!==m&&m)){p=-1===c?s:a[c].children;var y=e.indexOf("<",h),v=e.slice(h,-1===y?void 0:y);o.test(v)&&(v=" "),(y>-1&&c+p.length>=0||" "!==v)&&p.push({type:"text",content:v})}})),s};const u=function(){if(console?.warn){for(var e=arguments.length,n=new Array(e),t=0;t()=>{if(e.isInitialized)n();else{const t=()=>{setTimeout((()=>{e.off("initialized",t)}),0),n()};e.on("initialized",t)}},h=(e,n,t)=>{e.loadNamespaces(n,f(e,t))},m=(e,n,t,s)=>{y(t)&&(t=[t]),t.forEach((n=>{e.options.ns.indexOf(n)<0&&e.options.ns.push(n)})),e.loadLanguages(n,f(e,s))},g=e=>e.displayName||e.name||(y(e)&&e.length>0?e:"Unknown"),y=e=>"string"==typeof e,v=e=>"object"==typeof e&&null!==e,x=/&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34|nbsp|#160|copy|#169|reg|#174|hellip|#8230|#x2F|#47);/g,b={"&":"&","&":"&","<":"<","<":"<",">":">",">":">","'":"'","'":"'",""":'"',""":'"'," ":" "," ":" ","©":"©","©":"©","®":"®","®":"®","…":"…","…":"…","/":"/","/":"/"},E=e=>b[e];let O={bindI18n:"languageChanged",bindI18nStore:"",transEmptyNodeValue:"",transSupportBasicHtmlNodes:!0,transWrapTextNodes:"",transKeepBasicHtmlNodesFor:["br","strong","i","p"],useSuspense:!0,unescape:e=>e.replace(x,E)};const N=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};O={...O,...e}},$=()=>O;let w;const I=e=>{w=e},k=()=>w,S=(e,n)=>{if(!e)return!1;const t=e.props?.children??e.children;return n?t.length>0:!!t},j=e=>{if(!e)return[];const n=e.props?.children??e.children;return e.props?.i18nIsDynamicList?R(n):n},R=e=>Array.isArray(e)?e:[e],C=(e,t)=>{if(!e)return"";let s="";const a=R(e),i=t?.transSupportBasicHtmlNodes?t.transKeepBasicHtmlNodesFor??[]:[];return a.forEach(((e,a)=>{if(y(e))s+=`${e}`;else if(n.isValidElement(e)){const{props:n,type:r}=e,o=Object.keys(n).length,l=i.indexOf(r)>-1,c=n.children;if(c||!l||o)if(!c&&(!l||o)||n.i18nIsDynamicList)s+=`<${a}>`;else if(l&&1===o&&y(c))s+=`<${r}>${c}`;else{const e=C(c,t);s+=`<${a}>${e}`}else s+=`<${r}/>`}else if(null===e)u("Trans: the passed in value is invalid - seems you passed in a null child.");else if(v(e)){const{format:n,...t}=e,a=Object.keys(t);if(1===a.length){const e=n?`${a[0]}, ${n}`:a[0];s+=`{{${e}}}`}else u("react-i18next: the passed in object contained more than one variable - the object should look like {{ value, format }} where format is optional.",e)}else u("Trans: the passed in value is invalid - seems you passed in a variable like {number} - please pass in variables for interpolation as full objects like {{number}}.",e)})),s},T=(e,t,s,a,i,r)=>{if(""===t)return[];const o=a.transKeepBasicHtmlNodesFor||[],l=t&&new RegExp(o.map((e=>`<${e}`)).join("|")).test(t);if(!e&&!l&&!r)return[t];const u={},p=e=>{R(e).forEach((e=>{y(e)||(S(e)?p(j(e)):v(e)&&!n.isValidElement(e)&&Object.assign(u,e))}))};p(e);const d=c(`<0>${t}`),f={...u,...i},h=(e,t,s)=>{const a=j(e),i=g(a,t.children,s);return(e=>Array.isArray(e)&&e.every(n.isValidElement))(a)&&0===i.length||e.props?.i18nIsDynamicList?a:i},m=(e,t,s,a,i)=>{e.dummy?(e.children=t,s.push(n.cloneElement(e,{key:a},i?void 0:t))):s.push(...n.Children.map([e],(e=>{const s={...e.props};return delete s.i18nIsDynamicList,n.createElement(e.type,{...s,key:a,ref:e.ref},i?null:t)})))},g=(t,i,c)=>{const u=R(t);return R(i).reduce(((t,i,p)=>{const d=i.children?.[0]?.content&&s.services.interpolator.interpolate(i.children[0].content,f,s.language);if("tag"===i.type){let r=u[parseInt(i.name,10)];1!==c.length||r||(r=c[0][i.name]),r||(r={});const x=0!==Object.keys(i.attrs).length?((e,n)=>{const t={...n};return t.props=Object.assign(e.props,n.props),t})({props:i.attrs},r):r,b=n.isValidElement(x),E=b&&S(i,!0)&&!i.voidElement,O=l&&v(x)&&x.dummy&&!b,N=v(e)&&Object.hasOwnProperty.call(e,i.name);if(y(x)){const e=s.services.interpolator.interpolate(x,f,s.language);t.push(e)}else if(S(x)||E){const e=h(x,i,c);m(x,e,t,p)}else if(O){const e=g(u,i.children,c);m(x,e,t,p)}else if(Number.isNaN(parseFloat(i.name)))if(N){const e=h(x,i,c);m(x,e,t,p,i.voidElement)}else if(a.transSupportBasicHtmlNodes&&o.indexOf(i.name)>-1)if(i.voidElement)t.push(n.createElement(i.name,{key:`${i.name}-${p}`}));else{const e=g(u,i.children,c);t.push(n.createElement(i.name,{key:`${i.name}-${p}`},e))}else if(i.voidElement)t.push(`<${i.name} />`);else{const e=g(u,i.children,c);t.push(`<${i.name}>${e}`)}else if(v(x)&&!b){const e=i.children[0]?d:null;e&&t.push(e)}else m(x,d,t,p,1!==i.children.length||!d)}else if("text"===i.type){const e=a.transWrapTextNodes,o=r?a.unescape(s.services.interpolator.interpolate(i.content,f,s.language)):s.services.interpolator.interpolate(i.content,f,s.language);e?t.push(n.createElement(e,{key:`${i.name}-${p}`},o)):t.push(o)}return t}),[])},x=g([{dummy:!0,children:e||[]}],d,R(e||[]));return j(x[0])};function P(e){let{children:t,count:s,parent:a,i18nKey:i,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:f,t:h,shouldUnescape:m,...g}=e;const v=f||k();if(!v)return d("You will need to pass in an i18next instance by using i18nextReactModule"),t;const x=h||v.t.bind(v)||(e=>e),b={...$(),...v.options?.react};let E=p||x.ns||v.options?.defaultNS;E=y(E)?[E]:E||["translation"];const O=C(t,b),N=c||O||b.transEmptyNodeValue||i,{hashTransKey:w}=b,I=i||(w?w(O||N):O||N);v.options?.interpolation?.defaultVariables&&(l=l&&Object.keys(l).length>0?{...l,...v.options.interpolation.defaultVariables}:{...v.options.interpolation.defaultVariables});const S=l||void 0!==s||!t?o.interpolation:{interpolation:{...o.interpolation,prefix:"#$?",suffix:"?$#"}},j={...o,context:r||o.context,count:s,...l,...S,defaultValue:N,ns:E},R=I?x(I,j):N;u&&Object.keys(u).forEach((e=>{const t=u[e];"function"==typeof t.type||!t.props||!t.props.children||R.indexOf(`${e}/>`)<0&&R.indexOf(`${e} />`)<0||(u[e]=n.createElement((function(){return n.createElement(n.Fragment,null,t)})))}));const P=T(u||t,R,v,b,j,m),L=a??b.defaultTransParent;return L?n.createElement(L,g,P):P}const L={type:"3rdParty",init(e){N(e.options.react),I(e)}},A=n.createContext();class V{constructor(){this.usedNamespaces={}}addUsedNamespaces(e){e.forEach((e=>{this.usedNamespaces[e]??=!0}))}getUsedNamespaces(){return Object.keys(this.usedNamespaces)}}const z=e=>async n=>({...await(e.getInitialProps?.(n))??{},...F()}),F=()=>{const e=k(),n=e.reportNamespaces?.getUsedNamespaces()??[],t={},s={};return e.languages.forEach((t=>{s[t]={},n.forEach((n=>{s[t][n]=e.getResourceBundle(t,n)||{}}))})),t.initialI18nStore=s,t.initialLanguage=e.language,t};const U=(e,n,t,s)=>e.getFixedT(n,t,s),B=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{i18n:s}=t,{i18n:a,defaultNS:i}=n.useContext(A)||{},r=s||a||k();if(r&&!r.reportNamespaces&&(r.reportNamespaces=new V),!r){d("You will need to pass in an i18next instance by using initReactI18next");const e=(e,n)=>y(n)?n:v(n)&&y(n.defaultValue)?n.defaultValue:Array.isArray(e)?e[e.length-1]:e,n=[e,{},!1];return n.t=e,n.i18n={},n.ready=!1,n}r.options.react?.wait&&d("It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.");const o={...$(),...r.options.react,...t},{useSuspense:l,keyPrefix:c}=o;let u=e||i||r.options?.defaultNS;u=y(u)?[u]:u||["translation"],r.reportNamespaces.addUsedNamespaces?.(u);const p=(r.isInitialized||r.initializedStoreOnce)&&u.every((e=>function(e,n){let t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return n.languages&&n.languages.length?n.hasLoadedNamespace(e,{lng:t.lng,precheck:(n,s)=>{if(t.bindI18n?.indexOf("languageChanging")>-1&&n.services.backendConnector.backend&&n.isLanguageChangingTo&&!s(n.isLanguageChangingTo,e))return!1}}):(d("i18n.languages were undefined or empty",n.languages),!0)}(e,r,o))),f=((e,t,s,a)=>n.useCallback(U(e,t,s,a),[e,t,s,a]))(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),g=()=>f,x=()=>U(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),[b,E]=n.useState(g);let O=u.join();t.lng&&(O=`${t.lng}${O}`);const N=((e,t)=>{const s=n.useRef();return n.useEffect((()=>{s.current=e}),[e,t]),s.current})(O),w=n.useRef(!0);n.useEffect((()=>{const{bindI18n:e,bindI18nStore:n}=o;w.current=!0,p||l||(t.lng?m(r,t.lng,u,(()=>{w.current&&E(x)})):h(r,u,(()=>{w.current&&E(x)}))),p&&N&&N!==O&&w.current&&E(x);const s=()=>{w.current&&E(x)};return e&&r?.on(e,s),n&&r?.store.on(n,s),()=>{w.current=!1,r&&e?.split(" ").forEach((e=>r.off(e,s))),n&&r&&n.split(" ").forEach((e=>r.store.off(e,s)))}}),[r,O]),n.useEffect((()=>{w.current&&p&&E(g)}),[r,c,p]);const I=[b,r,p];if(I.t=b,I.i18n=r,I.ready=p,p)return I;if(!p&&!l)return I;throw new Promise((e=>{t.lng?m(r,t.lng,u,(()=>e())):h(r,u,(()=>e()))}))};const D=function(e,t){let s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const{i18n:a}=s,{i18n:i}=n.useContext(A)||{},r=a||i||k();r.options?.isClone||(e&&!r.initializedStoreOnce&&(r.services.resourceStore.data=e,r.options.ns=Object.values(e).reduce(((e,n)=>(Object.keys(n).forEach((n=>{e.indexOf(n)<0&&e.push(n)})),e)),r.options.ns),r.initializedStoreOnce=!0,r.isInitialized=!0),t&&!r.initializedLanguageOnce&&(r.changeLanguage(t),r.initializedLanguageOnce=!0))};e.I18nContext=A,e.I18nextProvider=function(e){let{i18n:t,defaultNS:s,children:a}=e;const i=n.useMemo((()=>({i18n:t,defaultNS:s})),[t,s]);return n.createElement(A.Provider,{value:i},a)},e.Trans=function(e){let{children:t,count:s,parent:a,i18nKey:i,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:d,t:f,shouldUnescape:h,...m}=e;const{i18n:g,defaultNS:y}=n.useContext(A)||{},v=d||g||k(),x=f||v?.t.bind(v);return P({children:t,count:s,parent:a,i18nKey:i,context:r,tOptions:o,values:l,defaults:c,components:u,ns:p||x?.ns||y||v?.options?.defaultNS,i18n:v,t:f,shouldUnescape:h,...m})},e.TransWithoutContext=P,e.Translation=e=>{let{ns:n,children:t,...s}=e;const[a,i,r]=B(n,s);return t(a,{i18n:i,lng:i.language},r)},e.composeInitialProps=z,e.date=()=>"",e.getDefaults=$,e.getI18n=k,e.getInitialProps=F,e.initReactI18next=L,e.number=()=>"",e.plural=()=>"",e.select=()=>"",e.selectOrdinal=()=>"",e.setDefaults=N,e.setI18n=I,e.time=()=>"",e.useSSR=D,e.useTranslation=B,e.withSSR=()=>function(e){function t(t){let{initialI18nStore:s,initialLanguage:a,...i}=t;return D(s,a),n.createElement(e,{...i})}return t.getInitialProps=z(e),t.displayName=`withI18nextSSR(${g(e)})`,t.WrappedComponent=e,t},e.withTranslation=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return function(s){function a(a){let{forwardedRef:i,...r}=a;const[o,l,c]=B(e,{...r,keyPrefix:t.keyPrefix}),u={...r,t:o,i18n:l,tReady:c};return t.withRef&&i?u.ref=i:!t.withRef&&i&&(u.forwardedRef=i),n.createElement(s,u)}a.displayName=`withI18nextTranslation(${g(s)})`,a.WrappedComponent=s;return t.withRef?n.forwardRef(((e,t)=>n.createElement(a,Object.assign({},e,{forwardedRef:t})))):a}}})); -+define(["exports","react"],(function(e,n){"use strict";function t(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var s=t({area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0}),a=/\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;function i(e){var n={type:"tag",name:"",voidElement:!1,attrs:{},children:[]},t=e.match(/<\/?([^\s]+?)[/\s>]/);if(t&&(n.name=t[1],(s[t[1]]||"/"===e.charAt(e.length-2))&&(n.voidElement=!0),n.name.startsWith("!--"))){var i=e.indexOf("--\x3e");return{type:"comment",comment:-1!==i?e.slice(4,i):""}}for(var r=new RegExp(a),o=null;null!==(o=r.exec(e));)if(o[0].trim())if(o[1]){var l=o[1].trim(),c=[l,""];l.indexOf("=")>-1&&(c=l.split("=")),n.attrs[c[0]]=c[1],r.lastIndex--}else o[2]&&(n.attrs[o[2]]=o[3].trim().substring(1,o[3].length-1));return n}var r=/<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g,o=/^\s*$/,l=Object.create(null);var c=function(e,n){n||(n={}),n.components||(n.components=l);var t,s=[],a=[],c=-1,u=!1;if(0!==e.indexOf("<")){var p=e.indexOf("<");s.push({type:"text",content:-1===p?e:e.substring(0,p)})}return e.replace(r,(function(r,l){if(u){if(r!=="")return;u=!1}var p,d="/"!==r.charAt(1),f=r.startsWith("\x3c!--"),h=l+r.length,m=e.charAt(h);if(f){var g=i(r);return c<0?(s.push(g),s):((p=a[c]).children.push(g),s)}if(d&&(c++,"tag"===(t=i(r)).type&&n.components[t.name]&&(t.type="component",u=!0),t.voidElement||u||!m||"<"===m||t.children.push({type:"text",content:e.slice(h,e.indexOf("<",h))}),0===c&&s.push(t),(p=a[c-1])&&p.children.push(t),a[c]=t),(!d||t.voidElement)&&(c>-1&&(t.voidElement||t.name===r.slice(2,-1))&&(c--,t=-1===c?s:a[c]),!u&&"<"!==m&&m)){p=-1===c?s:a[c].children;var y=e.indexOf("<",h),v=e.slice(h,-1===y?void 0:y);o.test(v)&&(v=" "),(y>-1&&c+p.length>=0||" "!==v)&&p.push({type:"text",content:v})}})),s};const u=function(){if(console?.warn){for(var e=arguments.length,n=new Array(e),t=0;t()=>{if(e.isInitialized)n();else{const t=()=>{setTimeout((()=>{e.off("initialized",t)}),0),n()};e.on("initialized",t)}},h=(e,n,t)=>{e.loadNamespaces(n,f(e,t))},m=(e,n,t,s)=>{y(t)&&(t=[t]),t.forEach((n=>{e.options.ns.indexOf(n)<0&&e.options.ns.push(n)})),e.loadLanguages(n,f(e,s))},g=e=>e.displayName||e.name||(y(e)&&e.length>0?e:"Unknown"),y=e=>"string"==typeof e,v=e=>"object"==typeof e&&null!==e,x=/&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34|nbsp|#160|copy|#169|reg|#174|hellip|#8230|#x2F|#47);/g,b={"&":"&","&":"&","<":"<","<":"<",">":">",">":">","'":"'","'":"'",""":'"',""":'"'," ":" "," ":" ","©":"©","©":"©","®":"®","®":"®","…":"…","…":"…","/":"/","/":"/"},E=e=>b[e];let O={bindI18n:"languageChanged",bindI18nStore:"",transEmptyNodeValue:"",transSupportBasicHtmlNodes:!0,transWrapTextNodes:"",transKeepBasicHtmlNodesFor:["br","strong","i","p"],useSuspense:!0,unescape:e=>e.replace(x,E)};const N=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};O={...O,...e}},$=()=>O;let w;const I=e=>{w=e},k=()=>w,S=(e,n)=>{if(!e)return!1;const t=e.props?.children??e.children;return n?t.length>0:!!t},j=e=>{if(!e)return[];const n=e.props?.children??e.children;return e.props?.i18nIsDynamicList?R(n):n},R=e=>Array.isArray(e)?e:[e],C=(e,t)=>{if(!e)return"";let s="";const a=R(e),i=t?.transSupportBasicHtmlNodes?t.transKeepBasicHtmlNodesFor??[]:[];return a.forEach(((e,a)=>{if(y(e))s+=`${e}`;else if(n.isValidElement(e)){const{props:n,type:r}=e,o=Object.keys(n).length,l=i.indexOf(r)>-1,c=n.children;if(c||!l||o)if(!c&&(!l||o)||n.i18nIsDynamicList)s+=`<${a}>`;else if(l&&1===o&&y(c))s+=`<${r}>${c}`;else{const e=C(c,t);s+=`<${a}>${e}`}else s+=`<${r}/>`}else if(null===e)u("Trans: the passed in value is invalid - seems you passed in a null child.");else if(v(e)){const{format:n,...t}=e,a=Object.keys(t);if(1===a.length){const e=n?`${a[0]}, ${n}`:a[0];s+=`{{${e}}}`}else u("react-i18next: the passed in object contained more than one variable - the object should look like {{ value, format }} where format is optional.",e)}else u("Trans: the passed in value is invalid - seems you passed in a variable like {number} - please pass in variables for interpolation as full objects like {{number}}.",e)})),s},T=(e,t,s,a,i,r)=>{if(""===t)return[];const o=a.transKeepBasicHtmlNodesFor||[],l=t&&new RegExp(o.map((e=>`<${e}`)).join("|")).test(t);if(!e&&!l&&!r)return[t];const u={},p=e=>{R(e).forEach((e=>{y(e)||(S(e)?p(j(e)):v(e)&&!n.isValidElement(e)&&Object.assign(u,e))}))};p(e);const d=c(`<0>${t}`),f={...u,...i},h=(e,t,s)=>{const a=j(e),i=g(a,t.children,s);return(e=>Array.isArray(e)&&e.every(n.isValidElement))(a)&&0===i.length||e.props?.i18nIsDynamicList?a:i},m=(e,t,s,a,i)=>{e.dummy?(e.children=t,s.push(n.cloneElement(e,{key:a},i?void 0:t))):s.push(...n.Children.map([e],(e=>{const s={...e.props};return delete s.i18nIsDynamicList,n.createElement(e.type,{...s,key:a,ref:e.ref},i?null:t)})))},g=(t,i,c)=>{const u=R(t);return R(i).reduce(((t,i,p)=>{const d=i.children?.[0]?.content&&s.services.interpolator.interpolate(i.children[0].content,f,s.language);if("tag"===i.type){let r=u[parseInt(i.name,10)];1!==c.length||r||(r=c[0][i.name]),r||(r={});const x=0!==Object.keys(i.attrs).length?((e,n)=>{const t={...n};return t.props=Object.assign(e.props,n.props),t})({props:i.attrs},r):r,b=n.isValidElement(x),E=b&&S(i,!0)&&!i.voidElement,O=l&&v(x)&&x.dummy&&!b,N=v(e)&&Object.hasOwnProperty.call(e,i.name);if(y(x)){const e=s.services.interpolator.interpolate(x,f,s.language);t.push(e)}else if(S(x)||E){const e=h(x,i,c);m(x,e,t,p)}else if(O){const e=g(u,i.children,c);m(x,e,t,p)}else if(Number.isNaN(parseFloat(i.name)))if(N){const e=h(x,i,c);m(x,e,t,p,i.voidElement)}else if(a.transSupportBasicHtmlNodes&&o.indexOf(i.name)>-1)if(i.voidElement)t.push(n.createElement(i.name,{key:`${i.name}-${p}`}));else{const e=g(u,i.children,c);t.push(n.createElement(i.name,{key:`${i.name}-${p}`},e))}else if(i.voidElement)t.push(`<${i.name} />`);else{const e=g(u,i.children,c);t.push(`<${i.name}>${e}`)}else if(v(x)&&!b){const e=i.children[0]?d:null;e&&t.push(e)}else m(x,d,t,p,1!==i.children.length||!d)}else if("text"===i.type){const e=a.transWrapTextNodes,o=r?a.unescape(s.services.interpolator.interpolate(i.content,f,s.language)):s.services.interpolator.interpolate(i.content,f,s.language);e?t.push(n.createElement(e,{key:`${i.name}-${p}`},o)):t.push(o)}return t}),[])},x=g([{dummy:!0,children:e||[]}],d,R(e||[]));return j(x[0])};function P(e){let{children:t,count:s,parent:a,i18nKey:i,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:f,t:h,shouldUnescape:m,...g}=e;const v=f||k();if(!v)return d("You will need to pass in an i18next instance by using i18nextReactModule"),t;const x=h||v.t.bind(v)||(e=>e),b={...$(),...v.options?.react};let E=p||x.ns||v.options?.defaultNS;E=y(E)?[E]:E||["translation"];const O=C(t,b),N=c||O||b.transEmptyNodeValue||i,{hashTransKey:w}=b,I=i||(w?w(O||N):O||N);v.options?.interpolation?.defaultVariables&&(l=l&&Object.keys(l).length>0?{...l,...v.options.interpolation.defaultVariables}:{...v.options.interpolation.defaultVariables});const S=l||void 0!==s||!t?o.interpolation:{interpolation:{...o.interpolation,prefix:"#$?",suffix:"?$#"}},j={...o,context:r||o.context,count:s,...l,...S,defaultValue:N,ns:E},R=I?x(I,j):N;u&&Object.keys(u).forEach((e=>{const t=u[e];"function"==typeof t.type||!t.props||!t.props.children||R.indexOf(`${e}/>`)<0&&R.indexOf(`${e} />`)<0||(u[e]=n.createElement((function(){return n.createElement(n.Fragment,null,t)})))}));const P=T(u||t,R,v,b,j,m),L=a??b.defaultTransParent;return L?n.createElement(L,g,P):P}const L={type:"3rdParty",init(e){N(e.options.react),I(e)}},A=n.createContext();class V{constructor(){this.usedNamespaces={}}addUsedNamespaces(e){e.forEach((e=>{this.usedNamespaces[e]=this.usedNamespaces[e]??!0}))}getUsedNamespaces(){return Object.keys(this.usedNamespaces)}}const z=e=>async n=>({...await(e.getInitialProps?.(n))??{},...F()}),F=()=>{const e=k(),n=e.reportNamespaces?.getUsedNamespaces()??[],t={},s={};return e.languages.forEach((t=>{s[t]={},n.forEach((n=>{s[t][n]=e.getResourceBundle(t,n)||{}}))})),t.initialI18nStore=s,t.initialLanguage=e.language,t};const U=(e,n,t,s)=>e.getFixedT(n,t,s),B=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{i18n:s}=t,{i18n:a,defaultNS:i}=n.useContext(A)||{},r=s||a||k();if(r&&!r.reportNamespaces&&(r.reportNamespaces=new V),!r){d("You will need to pass in an i18next instance by using initReactI18next");const e=(e,n)=>y(n)?n:v(n)&&y(n.defaultValue)?n.defaultValue:Array.isArray(e)?e[e.length-1]:e,n=[e,{},!1];return n.t=e,n.i18n={},n.ready=!1,n}r.options.react?.wait&&d("It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.");const o={...$(),...r.options.react,...t},{useSuspense:l,keyPrefix:c}=o;let u=e||i||r.options?.defaultNS;u=y(u)?[u]:u||["translation"],r.reportNamespaces.addUsedNamespaces?.(u);const p=(r.isInitialized||r.initializedStoreOnce)&&u.every((e=>function(e,n){let t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return n.languages&&n.languages.length?n.hasLoadedNamespace(e,{lng:t.lng,precheck:(n,s)=>{if(t.bindI18n?.indexOf("languageChanging")>-1&&n.services.backendConnector.backend&&n.isLanguageChangingTo&&!s(n.isLanguageChangingTo,e))return!1}}):(d("i18n.languages were undefined or empty",n.languages),!0)}(e,r,o))),f=((e,t,s,a)=>n.useCallback(U(e,t,s,a),[e,t,s,a]))(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),g=()=>f,x=()=>U(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),[b,E]=n.useState(g);let O=u.join();t.lng&&(O=`${t.lng}${O}`);const N=((e,t)=>{const s=n.useRef();return n.useEffect((()=>{s.current=e}),[e,t]),s.current})(O),w=n.useRef(!0);n.useEffect((()=>{const{bindI18n:e,bindI18nStore:n}=o;w.current=!0,p||l||(t.lng?m(r,t.lng,u,(()=>{w.current&&E(x)})):h(r,u,(()=>{w.current&&E(x)}))),p&&N&&N!==O&&w.current&&E(x);const s=()=>{w.current&&E(x)};return e&&r?.on(e,s),n&&r?.store.on(n,s),()=>{w.current=!1,r&&e?.split(" ").forEach((e=>r.off(e,s))),n&&r&&n.split(" ").forEach((e=>r.store.off(e,s)))}}),[r,O]),n.useEffect((()=>{w.current&&p&&E(g)}),[r,c,p]);const I=[b,r,p];if(I.t=b,I.i18n=r,I.ready=p,p)return I;if(!p&&!l)return I;throw new Promise((e=>{t.lng?m(r,t.lng,u,(()=>e())):h(r,u,(()=>e()))}))};const D=function(e,t){let s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const{i18n:a}=s,{i18n:i}=n.useContext(A)||{},r=a||i||k();r.options?.isClone||(e&&!r.initializedStoreOnce&&(r.services.resourceStore.data=e,r.options.ns=Object.values(e).reduce(((e,n)=>(Object.keys(n).forEach((n=>{e.indexOf(n)<0&&e.push(n)})),e)),r.options.ns),r.initializedStoreOnce=!0,r.isInitialized=!0),t&&!r.initializedLanguageOnce&&(r.changeLanguage(t),r.initializedLanguageOnce=!0))};e.I18nContext=A,e.I18nextProvider=function(e){let{i18n:t,defaultNS:s,children:a}=e;const i=n.useMemo((()=>({i18n:t,defaultNS:s})),[t,s]);return n.createElement(A.Provider,{value:i},a)},e.Trans=function(e){let{children:t,count:s,parent:a,i18nKey:i,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:d,t:f,shouldUnescape:h,...m}=e;const{i18n:g,defaultNS:y}=n.useContext(A)||{},v=d||g||k(),x=f||v?.t.bind(v);return P({children:t,count:s,parent:a,i18nKey:i,context:r,tOptions:o,values:l,defaults:c,components:u,ns:p||x?.ns||y||v?.options?.defaultNS,i18n:v,t:f,shouldUnescape:h,...m})},e.TransWithoutContext=P,e.Translation=e=>{let{ns:n,children:t,...s}=e;const[a,i,r]=B(n,s);return t(a,{i18n:i,lng:i.language},r)},e.composeInitialProps=z,e.date=()=>"",e.getDefaults=$,e.getI18n=k,e.getInitialProps=F,e.initReactI18next=L,e.number=()=>"",e.plural=()=>"",e.select=()=>"",e.selectOrdinal=()=>"",e.setDefaults=N,e.setI18n=I,e.time=()=>"",e.useSSR=D,e.useTranslation=B,e.withSSR=()=>function(e){function t(t){let{initialI18nStore:s,initialLanguage:a,...i}=t;return D(s,a),n.createElement(e,{...i})}return t.getInitialProps=z(e),t.displayName=`withI18nextSSR(${g(e)})`,t.WrappedComponent=e,t},e.withTranslation=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return function(s){function a(a){let{forwardedRef:i,...r}=a;const[o,l,c]=B(e,{...r,keyPrefix:t.keyPrefix}),u={...r,t:o,i18n:l,tReady:c};return t.withRef&&i?u.ref=i:!t.withRef&&i&&(u.forwardedRef=i),n.createElement(s,u)}a.displayName=`withI18nextTranslation(${g(s)})`,a.WrappedComponent=s;return t.withRef?n.forwardRef(((e,t)=>n.createElement(a,Object.assign({},e,{forwardedRef:t})))):a}}})); -diff --git a/dist/commonjs/context.js b/dist/commonjs/context.js -index 5c4506e4d3424e4ffd167fd5bb696dabd67b662b..dcc11b798da72771953acce3c1368f006c3c1478 100644 ---- a/dist/commonjs/context.js -+++ b/dist/commonjs/context.js -@@ -46,7 +46,7 @@ class ReportNamespaces { - } - addUsedNamespaces(namespaces) { - namespaces.forEach(ns => { -- this.usedNamespaces[ns] ??= true; -+ this.usedNamespaces[ns] = this.usedNamespaces[ns] ?? true; - }); - } - getUsedNamespaces() { -diff --git a/dist/es/context.js b/dist/es/context.js -index 89afe45d6cf480079598dd183521b32f28f77f06..a953078f44008ee751f51257536a31ceba2fd672 100644 ---- a/dist/es/context.js -+++ b/dist/es/context.js -@@ -10,7 +10,7 @@ export class ReportNamespaces { - } - addUsedNamespaces(namespaces) { - namespaces.forEach(ns => { -- this.usedNamespaces[ns] ??= true; -+ this.usedNamespaces[ns] = this.usedNamespaces[ns] ?? true; - }); - } - getUsedNamespaces() { -diff --git a/dist/umd/react-i18next.js b/dist/umd/react-i18next.js -index 3723bd7c0f5762bdb09e3226ac86ff255cbf9859..9178ae9f7cdb776f51a64ff6c79eed1d18fbd836 100644 ---- a/dist/umd/react-i18next.js -+++ b/dist/umd/react-i18next.js -@@ -499,7 +499,7 @@ - } - addUsedNamespaces(namespaces) { - namespaces.forEach(ns => { -- this.usedNamespaces[ns] ??= true; -+ this.usedNamespaces[ns] = this.usedNamespaces[ns] ?? true; - }); - } - getUsedNamespaces() { -diff --git a/dist/umd/react-i18next.min.js b/dist/umd/react-i18next.min.js -index 2eef624040aab6b4b9ba4699bf7e4777842bf0a2..69e17753d545df9dc26aa3411b477a4dff5e8361 100644 ---- a/dist/umd/react-i18next.min.js -+++ b/dist/umd/react-i18next.min.js -@@ -1 +1 @@ --!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports,require("react")):"function"==typeof define&&define.amd?define(["exports","react"],n):n((e="undefined"!=typeof globalThis?globalThis:e||self).ReactI18next={},e.React)}(this,(function(e,n){"use strict";function t(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var s=t({area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0}),i=/\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;function a(e){var n={type:"tag",name:"",voidElement:!1,attrs:{},children:[]},t=e.match(/<\/?([^\s]+?)[/\s>]/);if(t&&(n.name=t[1],(s[t[1]]||"/"===e.charAt(e.length-2))&&(n.voidElement=!0),n.name.startsWith("!--"))){var a=e.indexOf("--\x3e");return{type:"comment",comment:-1!==a?e.slice(4,a):""}}for(var r=new RegExp(i),o=null;null!==(o=r.exec(e));)if(o[0].trim())if(o[1]){var l=o[1].trim(),c=[l,""];l.indexOf("=")>-1&&(c=l.split("=")),n.attrs[c[0]]=c[1],r.lastIndex--}else o[2]&&(n.attrs[o[2]]=o[3].trim().substring(1,o[3].length-1));return n}var r=/<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g,o=/^\s*$/,l=Object.create(null);var c=function(e,n){n||(n={}),n.components||(n.components=l);var t,s=[],i=[],c=-1,u=!1;if(0!==e.indexOf("<")){var p=e.indexOf("<");s.push({type:"text",content:-1===p?e:e.substring(0,p)})}return e.replace(r,(function(r,l){if(u){if(r!=="")return;u=!1}var p,d="/"!==r.charAt(1),f=r.startsWith("\x3c!--"),h=l+r.length,m=e.charAt(h);if(f){var g=a(r);return c<0?(s.push(g),s):((p=i[c]).children.push(g),s)}if(d&&(c++,"tag"===(t=a(r)).type&&n.components[t.name]&&(t.type="component",u=!0),t.voidElement||u||!m||"<"===m||t.children.push({type:"text",content:e.slice(h,e.indexOf("<",h))}),0===c&&s.push(t),(p=i[c-1])&&p.children.push(t),i[c]=t),(!d||t.voidElement)&&(c>-1&&(t.voidElement||t.name===r.slice(2,-1))&&(c--,t=-1===c?s:i[c]),!u&&"<"!==m&&m)){p=-1===c?s:i[c].children;var y=e.indexOf("<",h),x=e.slice(h,-1===y?void 0:y);o.test(x)&&(x=" "),(y>-1&&c+p.length>=0||" "!==x)&&p.push({type:"text",content:x})}})),s};const u=function(){if(console?.warn){for(var e=arguments.length,n=new Array(e),t=0;t()=>{if(e.isInitialized)n();else{const t=()=>{setTimeout((()=>{e.off("initialized",t)}),0),n()};e.on("initialized",t)}},h=(e,n,t)=>{e.loadNamespaces(n,f(e,t))},m=(e,n,t,s)=>{y(t)&&(t=[t]),t.forEach((n=>{e.options.ns.indexOf(n)<0&&e.options.ns.push(n)})),e.loadLanguages(n,f(e,s))},g=e=>e.displayName||e.name||(y(e)&&e.length>0?e:"Unknown"),y=e=>"string"==typeof e,x=e=>"object"==typeof e&&null!==e,b=/&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34|nbsp|#160|copy|#169|reg|#174|hellip|#8230|#x2F|#47);/g,v={"&":"&","&":"&","<":"<","<":"<",">":">",">":">","'":"'","'":"'",""":'"',""":'"'," ":" "," ":" ","©":"©","©":"©","®":"®","®":"®","…":"…","…":"…","/":"/","/":"/"},E=e=>v[e];let O={bindI18n:"languageChanged",bindI18nStore:"",transEmptyNodeValue:"",transSupportBasicHtmlNodes:!0,transWrapTextNodes:"",transKeepBasicHtmlNodesFor:["br","strong","i","p"],useSuspense:!0,unescape:e=>e.replace(b,E)};const N=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};O={...O,...e}},$=()=>O;let w;const I=e=>{w=e},k=()=>w,S=(e,n)=>{if(!e)return!1;const t=e.props?.children??e.children;return n?t.length>0:!!t},j=e=>{if(!e)return[];const n=e.props?.children??e.children;return e.props?.i18nIsDynamicList?R(n):n},R=e=>Array.isArray(e)?e:[e],T=(e,t)=>{if(!e)return"";let s="";const i=R(e),a=t?.transSupportBasicHtmlNodes?t.transKeepBasicHtmlNodesFor??[]:[];return i.forEach(((e,i)=>{if(y(e))s+=`${e}`;else if(n.isValidElement(e)){const{props:n,type:r}=e,o=Object.keys(n).length,l=a.indexOf(r)>-1,c=n.children;if(c||!l||o)if(!c&&(!l||o)||n.i18nIsDynamicList)s+=`<${i}>`;else if(l&&1===o&&y(c))s+=`<${r}>${c}`;else{const e=T(c,t);s+=`<${i}>${e}`}else s+=`<${r}/>`}else if(null===e)u("Trans: the passed in value is invalid - seems you passed in a null child.");else if(x(e)){const{format:n,...t}=e,i=Object.keys(t);if(1===i.length){const e=n?`${i[0]}, ${n}`:i[0];s+=`{{${e}}}`}else u("react-i18next: the passed in object contained more than one variable - the object should look like {{ value, format }} where format is optional.",e)}else u("Trans: the passed in value is invalid - seems you passed in a variable like {number} - please pass in variables for interpolation as full objects like {{number}}.",e)})),s},C=(e,t,s,i,a,r)=>{if(""===t)return[];const o=i.transKeepBasicHtmlNodesFor||[],l=t&&new RegExp(o.map((e=>`<${e}`)).join("|")).test(t);if(!e&&!l&&!r)return[t];const u={},p=e=>{R(e).forEach((e=>{y(e)||(S(e)?p(j(e)):x(e)&&!n.isValidElement(e)&&Object.assign(u,e))}))};p(e);const d=c(`<0>${t}`),f={...u,...a},h=(e,t,s)=>{const i=j(e),a=g(i,t.children,s);return(e=>Array.isArray(e)&&e.every(n.isValidElement))(i)&&0===a.length||e.props?.i18nIsDynamicList?i:a},m=(e,t,s,i,a)=>{e.dummy?(e.children=t,s.push(n.cloneElement(e,{key:i},a?void 0:t))):s.push(...n.Children.map([e],(e=>{const s={...e.props};return delete s.i18nIsDynamicList,n.createElement(e.type,{...s,key:i,ref:e.ref},a?null:t)})))},g=(t,a,c)=>{const u=R(t);return R(a).reduce(((t,a,p)=>{const d=a.children?.[0]?.content&&s.services.interpolator.interpolate(a.children[0].content,f,s.language);if("tag"===a.type){let r=u[parseInt(a.name,10)];1!==c.length||r||(r=c[0][a.name]),r||(r={});const b=0!==Object.keys(a.attrs).length?((e,n)=>{const t={...n};return t.props=Object.assign(e.props,n.props),t})({props:a.attrs},r):r,v=n.isValidElement(b),E=v&&S(a,!0)&&!a.voidElement,O=l&&x(b)&&b.dummy&&!v,N=x(e)&&Object.hasOwnProperty.call(e,a.name);if(y(b)){const e=s.services.interpolator.interpolate(b,f,s.language);t.push(e)}else if(S(b)||E){const e=h(b,a,c);m(b,e,t,p)}else if(O){const e=g(u,a.children,c);m(b,e,t,p)}else if(Number.isNaN(parseFloat(a.name)))if(N){const e=h(b,a,c);m(b,e,t,p,a.voidElement)}else if(i.transSupportBasicHtmlNodes&&o.indexOf(a.name)>-1)if(a.voidElement)t.push(n.createElement(a.name,{key:`${a.name}-${p}`}));else{const e=g(u,a.children,c);t.push(n.createElement(a.name,{key:`${a.name}-${p}`},e))}else if(a.voidElement)t.push(`<${a.name} />`);else{const e=g(u,a.children,c);t.push(`<${a.name}>${e}`)}else if(x(b)&&!v){const e=a.children[0]?d:null;e&&t.push(e)}else m(b,d,t,p,1!==a.children.length||!d)}else if("text"===a.type){const e=i.transWrapTextNodes,o=r?i.unescape(s.services.interpolator.interpolate(a.content,f,s.language)):s.services.interpolator.interpolate(a.content,f,s.language);e?t.push(n.createElement(e,{key:`${a.name}-${p}`},o)):t.push(o)}return t}),[])},b=g([{dummy:!0,children:e||[]}],d,R(e||[]));return j(b[0])};function P(e){let{children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:f,t:h,shouldUnescape:m,...g}=e;const x=f||k();if(!x)return d("You will need to pass in an i18next instance by using i18nextReactModule"),t;const b=h||x.t.bind(x)||(e=>e),v={...$(),...x.options?.react};let E=p||b.ns||x.options?.defaultNS;E=y(E)?[E]:E||["translation"];const O=T(t,v),N=c||O||v.transEmptyNodeValue||a,{hashTransKey:w}=v,I=a||(w?w(O||N):O||N);x.options?.interpolation?.defaultVariables&&(l=l&&Object.keys(l).length>0?{...l,...x.options.interpolation.defaultVariables}:{...x.options.interpolation.defaultVariables});const S=l||void 0!==s||!t?o.interpolation:{interpolation:{...o.interpolation,prefix:"#$?",suffix:"?$#"}},j={...o,context:r||o.context,count:s,...l,...S,defaultValue:N,ns:E},R=I?b(I,j):N;u&&Object.keys(u).forEach((e=>{const t=u[e];"function"==typeof t.type||!t.props||!t.props.children||R.indexOf(`${e}/>`)<0&&R.indexOf(`${e} />`)<0||(u[e]=n.createElement((function(){return n.createElement(n.Fragment,null,t)})))}));const P=C(u||t,R,x,v,j,m),L=i??v.defaultTransParent;return L?n.createElement(L,g,P):P}const L={type:"3rdParty",init(e){N(e.options.react),I(e)}},A=n.createContext();class V{constructor(){this.usedNamespaces={}}addUsedNamespaces(e){e.forEach((e=>{this.usedNamespaces[e]??=!0}))}getUsedNamespaces(){return Object.keys(this.usedNamespaces)}}const z=e=>async n=>({...await(e.getInitialProps?.(n))??{},...F()}),F=()=>{const e=k(),n=e.reportNamespaces?.getUsedNamespaces()??[],t={},s={};return e.languages.forEach((t=>{s[t]={},n.forEach((n=>{s[t][n]=e.getResourceBundle(t,n)||{}}))})),t.initialI18nStore=s,t.initialLanguage=e.language,t};const U=(e,n,t,s)=>e.getFixedT(n,t,s),B=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{i18n:s}=t,{i18n:i,defaultNS:a}=n.useContext(A)||{},r=s||i||k();if(r&&!r.reportNamespaces&&(r.reportNamespaces=new V),!r){d("You will need to pass in an i18next instance by using initReactI18next");const e=(e,n)=>y(n)?n:x(n)&&y(n.defaultValue)?n.defaultValue:Array.isArray(e)?e[e.length-1]:e,n=[e,{},!1];return n.t=e,n.i18n={},n.ready=!1,n}r.options.react?.wait&&d("It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.");const o={...$(),...r.options.react,...t},{useSuspense:l,keyPrefix:c}=o;let u=e||a||r.options?.defaultNS;u=y(u)?[u]:u||["translation"],r.reportNamespaces.addUsedNamespaces?.(u);const p=(r.isInitialized||r.initializedStoreOnce)&&u.every((e=>function(e,n){let t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return n.languages&&n.languages.length?n.hasLoadedNamespace(e,{lng:t.lng,precheck:(n,s)=>{if(t.bindI18n?.indexOf("languageChanging")>-1&&n.services.backendConnector.backend&&n.isLanguageChangingTo&&!s(n.isLanguageChangingTo,e))return!1}}):(d("i18n.languages were undefined or empty",n.languages),!0)}(e,r,o))),f=((e,t,s,i)=>n.useCallback(U(e,t,s,i),[e,t,s,i]))(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),g=()=>f,b=()=>U(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),[v,E]=n.useState(g);let O=u.join();t.lng&&(O=`${t.lng}${O}`);const N=((e,t)=>{const s=n.useRef();return n.useEffect((()=>{s.current=e}),[e,t]),s.current})(O),w=n.useRef(!0);n.useEffect((()=>{const{bindI18n:e,bindI18nStore:n}=o;w.current=!0,p||l||(t.lng?m(r,t.lng,u,(()=>{w.current&&E(b)})):h(r,u,(()=>{w.current&&E(b)}))),p&&N&&N!==O&&w.current&&E(b);const s=()=>{w.current&&E(b)};return e&&r?.on(e,s),n&&r?.store.on(n,s),()=>{w.current=!1,r&&e?.split(" ").forEach((e=>r.off(e,s))),n&&r&&n.split(" ").forEach((e=>r.store.off(e,s)))}}),[r,O]),n.useEffect((()=>{w.current&&p&&E(g)}),[r,c,p]);const I=[v,r,p];if(I.t=v,I.i18n=r,I.ready=p,p)return I;if(!p&&!l)return I;throw new Promise((e=>{t.lng?m(r,t.lng,u,(()=>e())):h(r,u,(()=>e()))}))};const D=function(e,t){let s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const{i18n:i}=s,{i18n:a}=n.useContext(A)||{},r=i||a||k();r.options?.isClone||(e&&!r.initializedStoreOnce&&(r.services.resourceStore.data=e,r.options.ns=Object.values(e).reduce(((e,n)=>(Object.keys(n).forEach((n=>{e.indexOf(n)<0&&e.push(n)})),e)),r.options.ns),r.initializedStoreOnce=!0,r.isInitialized=!0),t&&!r.initializedLanguageOnce&&(r.changeLanguage(t),r.initializedLanguageOnce=!0))};e.I18nContext=A,e.I18nextProvider=function(e){let{i18n:t,defaultNS:s,children:i}=e;const a=n.useMemo((()=>({i18n:t,defaultNS:s})),[t,s]);return n.createElement(A.Provider,{value:a},i)},e.Trans=function(e){let{children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:d,t:f,shouldUnescape:h,...m}=e;const{i18n:g,defaultNS:y}=n.useContext(A)||{},x=d||g||k(),b=f||x?.t.bind(x);return P({children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o,values:l,defaults:c,components:u,ns:p||b?.ns||y||x?.options?.defaultNS,i18n:x,t:f,shouldUnescape:h,...m})},e.TransWithoutContext=P,e.Translation=e=>{let{ns:n,children:t,...s}=e;const[i,a,r]=B(n,s);return t(i,{i18n:a,lng:a.language},r)},e.composeInitialProps=z,e.date=()=>"",e.getDefaults=$,e.getI18n=k,e.getInitialProps=F,e.initReactI18next=L,e.number=()=>"",e.plural=()=>"",e.select=()=>"",e.selectOrdinal=()=>"",e.setDefaults=N,e.setI18n=I,e.time=()=>"",e.useSSR=D,e.useTranslation=B,e.withSSR=()=>function(e){function t(t){let{initialI18nStore:s,initialLanguage:i,...a}=t;return D(s,i),n.createElement(e,{...a})}return t.getInitialProps=z(e),t.displayName=`withI18nextSSR(${g(e)})`,t.WrappedComponent=e,t},e.withTranslation=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return function(s){function i(i){let{forwardedRef:a,...r}=i;const[o,l,c]=B(e,{...r,keyPrefix:t.keyPrefix}),u={...r,t:o,i18n:l,tReady:c};return t.withRef&&a?u.ref=a:!t.withRef&&a&&(u.forwardedRef=a),n.createElement(s,u)}i.displayName=`withI18nextTranslation(${g(s)})`,i.WrappedComponent=s;return t.withRef?n.forwardRef(((e,t)=>n.createElement(i,Object.assign({},e,{forwardedRef:t})))):i}}})); -+!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports,require("react")):"function"==typeof define&&define.amd?define(["exports","react"],n):n((e="undefined"!=typeof globalThis?globalThis:e||self).ReactI18next={},e.React)}(this,(function(e,n){"use strict";function t(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var s=t({area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0}),i=/\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;function a(e){var n={type:"tag",name:"",voidElement:!1,attrs:{},children:[]},t=e.match(/<\/?([^\s]+?)[/\s>]/);if(t&&(n.name=t[1],(s[t[1]]||"/"===e.charAt(e.length-2))&&(n.voidElement=!0),n.name.startsWith("!--"))){var a=e.indexOf("--\x3e");return{type:"comment",comment:-1!==a?e.slice(4,a):""}}for(var r=new RegExp(i),o=null;null!==(o=r.exec(e));)if(o[0].trim())if(o[1]){var l=o[1].trim(),c=[l,""];l.indexOf("=")>-1&&(c=l.split("=")),n.attrs[c[0]]=c[1],r.lastIndex--}else o[2]&&(n.attrs[o[2]]=o[3].trim().substring(1,o[3].length-1));return n}var r=/<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g,o=/^\s*$/,l=Object.create(null);var c=function(e,n){n||(n={}),n.components||(n.components=l);var t,s=[],i=[],c=-1,u=!1;if(0!==e.indexOf("<")){var p=e.indexOf("<");s.push({type:"text",content:-1===p?e:e.substring(0,p)})}return e.replace(r,(function(r,l){if(u){if(r!=="")return;u=!1}var p,d="/"!==r.charAt(1),f=r.startsWith("\x3c!--"),h=l+r.length,m=e.charAt(h);if(f){var g=a(r);return c<0?(s.push(g),s):((p=i[c]).children.push(g),s)}if(d&&(c++,"tag"===(t=a(r)).type&&n.components[t.name]&&(t.type="component",u=!0),t.voidElement||u||!m||"<"===m||t.children.push({type:"text",content:e.slice(h,e.indexOf("<",h))}),0===c&&s.push(t),(p=i[c-1])&&p.children.push(t),i[c]=t),(!d||t.voidElement)&&(c>-1&&(t.voidElement||t.name===r.slice(2,-1))&&(c--,t=-1===c?s:i[c]),!u&&"<"!==m&&m)){p=-1===c?s:i[c].children;var y=e.indexOf("<",h),x=e.slice(h,-1===y?void 0:y);o.test(x)&&(x=" "),(y>-1&&c+p.length>=0||" "!==x)&&p.push({type:"text",content:x})}})),s};const u=function(){if(console?.warn){for(var e=arguments.length,n=new Array(e),t=0;t()=>{if(e.isInitialized)n();else{const t=()=>{setTimeout((()=>{e.off("initialized",t)}),0),n()};e.on("initialized",t)}},h=(e,n,t)=>{e.loadNamespaces(n,f(e,t))},m=(e,n,t,s)=>{y(t)&&(t=[t]),t.forEach((n=>{e.options.ns.indexOf(n)<0&&e.options.ns.push(n)})),e.loadLanguages(n,f(e,s))},g=e=>e.displayName||e.name||(y(e)&&e.length>0?e:"Unknown"),y=e=>"string"==typeof e,x=e=>"object"==typeof e&&null!==e,b=/&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34|nbsp|#160|copy|#169|reg|#174|hellip|#8230|#x2F|#47);/g,v={"&":"&","&":"&","<":"<","<":"<",">":">",">":">","'":"'","'":"'",""":'"',""":'"'," ":" "," ":" ","©":"©","©":"©","®":"®","®":"®","…":"…","…":"…","/":"/","/":"/"},E=e=>v[e];let O={bindI18n:"languageChanged",bindI18nStore:"",transEmptyNodeValue:"",transSupportBasicHtmlNodes:!0,transWrapTextNodes:"",transKeepBasicHtmlNodesFor:["br","strong","i","p"],useSuspense:!0,unescape:e=>e.replace(b,E)};const N=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};O={...O,...e}},$=()=>O;let w;const I=e=>{w=e},k=()=>w,S=(e,n)=>{if(!e)return!1;const t=e.props?.children??e.children;return n?t.length>0:!!t},j=e=>{if(!e)return[];const n=e.props?.children??e.children;return e.props?.i18nIsDynamicList?R(n):n},R=e=>Array.isArray(e)?e:[e],T=(e,t)=>{if(!e)return"";let s="";const i=R(e),a=t?.transSupportBasicHtmlNodes?t.transKeepBasicHtmlNodesFor??[]:[];return i.forEach(((e,i)=>{if(y(e))s+=`${e}`;else if(n.isValidElement(e)){const{props:n,type:r}=e,o=Object.keys(n).length,l=a.indexOf(r)>-1,c=n.children;if(c||!l||o)if(!c&&(!l||o)||n.i18nIsDynamicList)s+=`<${i}>`;else if(l&&1===o&&y(c))s+=`<${r}>${c}`;else{const e=T(c,t);s+=`<${i}>${e}`}else s+=`<${r}/>`}else if(null===e)u("Trans: the passed in value is invalid - seems you passed in a null child.");else if(x(e)){const{format:n,...t}=e,i=Object.keys(t);if(1===i.length){const e=n?`${i[0]}, ${n}`:i[0];s+=`{{${e}}}`}else u("react-i18next: the passed in object contained more than one variable - the object should look like {{ value, format }} where format is optional.",e)}else u("Trans: the passed in value is invalid - seems you passed in a variable like {number} - please pass in variables for interpolation as full objects like {{number}}.",e)})),s},C=(e,t,s,i,a,r)=>{if(""===t)return[];const o=i.transKeepBasicHtmlNodesFor||[],l=t&&new RegExp(o.map((e=>`<${e}`)).join("|")).test(t);if(!e&&!l&&!r)return[t];const u={},p=e=>{R(e).forEach((e=>{y(e)||(S(e)?p(j(e)):x(e)&&!n.isValidElement(e)&&Object.assign(u,e))}))};p(e);const d=c(`<0>${t}`),f={...u,...a},h=(e,t,s)=>{const i=j(e),a=g(i,t.children,s);return(e=>Array.isArray(e)&&e.every(n.isValidElement))(i)&&0===a.length||e.props?.i18nIsDynamicList?i:a},m=(e,t,s,i,a)=>{e.dummy?(e.children=t,s.push(n.cloneElement(e,{key:i},a?void 0:t))):s.push(...n.Children.map([e],(e=>{const s={...e.props};return delete s.i18nIsDynamicList,n.createElement(e.type,{...s,key:i,ref:e.ref},a?null:t)})))},g=(t,a,c)=>{const u=R(t);return R(a).reduce(((t,a,p)=>{const d=a.children?.[0]?.content&&s.services.interpolator.interpolate(a.children[0].content,f,s.language);if("tag"===a.type){let r=u[parseInt(a.name,10)];1!==c.length||r||(r=c[0][a.name]),r||(r={});const b=0!==Object.keys(a.attrs).length?((e,n)=>{const t={...n};return t.props=Object.assign(e.props,n.props),t})({props:a.attrs},r):r,v=n.isValidElement(b),E=v&&S(a,!0)&&!a.voidElement,O=l&&x(b)&&b.dummy&&!v,N=x(e)&&Object.hasOwnProperty.call(e,a.name);if(y(b)){const e=s.services.interpolator.interpolate(b,f,s.language);t.push(e)}else if(S(b)||E){const e=h(b,a,c);m(b,e,t,p)}else if(O){const e=g(u,a.children,c);m(b,e,t,p)}else if(Number.isNaN(parseFloat(a.name)))if(N){const e=h(b,a,c);m(b,e,t,p,a.voidElement)}else if(i.transSupportBasicHtmlNodes&&o.indexOf(a.name)>-1)if(a.voidElement)t.push(n.createElement(a.name,{key:`${a.name}-${p}`}));else{const e=g(u,a.children,c);t.push(n.createElement(a.name,{key:`${a.name}-${p}`},e))}else if(a.voidElement)t.push(`<${a.name} />`);else{const e=g(u,a.children,c);t.push(`<${a.name}>${e}`)}else if(x(b)&&!v){const e=a.children[0]?d:null;e&&t.push(e)}else m(b,d,t,p,1!==a.children.length||!d)}else if("text"===a.type){const e=i.transWrapTextNodes,o=r?i.unescape(s.services.interpolator.interpolate(a.content,f,s.language)):s.services.interpolator.interpolate(a.content,f,s.language);e?t.push(n.createElement(e,{key:`${a.name}-${p}`},o)):t.push(o)}return t}),[])},b=g([{dummy:!0,children:e||[]}],d,R(e||[]));return j(b[0])};function P(e){let{children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:f,t:h,shouldUnescape:m,...g}=e;const x=f||k();if(!x)return d("You will need to pass in an i18next instance by using i18nextReactModule"),t;const b=h||x.t.bind(x)||(e=>e),v={...$(),...x.options?.react};let E=p||b.ns||x.options?.defaultNS;E=y(E)?[E]:E||["translation"];const O=T(t,v),N=c||O||v.transEmptyNodeValue||a,{hashTransKey:w}=v,I=a||(w?w(O||N):O||N);x.options?.interpolation?.defaultVariables&&(l=l&&Object.keys(l).length>0?{...l,...x.options.interpolation.defaultVariables}:{...x.options.interpolation.defaultVariables});const S=l||void 0!==s||!t?o.interpolation:{interpolation:{...o.interpolation,prefix:"#$?",suffix:"?$#"}},j={...o,context:r||o.context,count:s,...l,...S,defaultValue:N,ns:E},R=I?b(I,j):N;u&&Object.keys(u).forEach((e=>{const t=u[e];"function"==typeof t.type||!t.props||!t.props.children||R.indexOf(`${e}/>`)<0&&R.indexOf(`${e} />`)<0||(u[e]=n.createElement((function(){return n.createElement(n.Fragment,null,t)})))}));const P=C(u||t,R,x,v,j,m),L=i??v.defaultTransParent;return L?n.createElement(L,g,P):P}const L={type:"3rdParty",init(e){N(e.options.react),I(e)}},A=n.createContext();class V{constructor(){this.usedNamespaces={}}addUsedNamespaces(e){e.forEach((e=>{this.usedNamespaces[e]=this.usedNamespaces[e]??!0}))}getUsedNamespaces(){return Object.keys(this.usedNamespaces)}}const z=e=>async n=>({...await(e.getInitialProps?.(n))??{},...F()}),F=()=>{const e=k(),n=e.reportNamespaces?.getUsedNamespaces()??[],t={},s={};return e.languages.forEach((t=>{s[t]={},n.forEach((n=>{s[t][n]=e.getResourceBundle(t,n)||{}}))})),t.initialI18nStore=s,t.initialLanguage=e.language,t};const U=(e,n,t,s)=>e.getFixedT(n,t,s),B=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{i18n:s}=t,{i18n:i,defaultNS:a}=n.useContext(A)||{},r=s||i||k();if(r&&!r.reportNamespaces&&(r.reportNamespaces=new V),!r){d("You will need to pass in an i18next instance by using initReactI18next");const e=(e,n)=>y(n)?n:x(n)&&y(n.defaultValue)?n.defaultValue:Array.isArray(e)?e[e.length-1]:e,n=[e,{},!1];return n.t=e,n.i18n={},n.ready=!1,n}r.options.react?.wait&&d("It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.");const o={...$(),...r.options.react,...t},{useSuspense:l,keyPrefix:c}=o;let u=e||a||r.options?.defaultNS;u=y(u)?[u]:u||["translation"],r.reportNamespaces.addUsedNamespaces?.(u);const p=(r.isInitialized||r.initializedStoreOnce)&&u.every((e=>function(e,n){let t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return n.languages&&n.languages.length?n.hasLoadedNamespace(e,{lng:t.lng,precheck:(n,s)=>{if(t.bindI18n?.indexOf("languageChanging")>-1&&n.services.backendConnector.backend&&n.isLanguageChangingTo&&!s(n.isLanguageChangingTo,e))return!1}}):(d("i18n.languages were undefined or empty",n.languages),!0)}(e,r,o))),f=((e,t,s,i)=>n.useCallback(U(e,t,s,i),[e,t,s,i]))(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),g=()=>f,b=()=>U(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),[v,E]=n.useState(g);let O=u.join();t.lng&&(O=`${t.lng}${O}`);const N=((e,t)=>{const s=n.useRef();return n.useEffect((()=>{s.current=e}),[e,t]),s.current})(O),w=n.useRef(!0);n.useEffect((()=>{const{bindI18n:e,bindI18nStore:n}=o;w.current=!0,p||l||(t.lng?m(r,t.lng,u,(()=>{w.current&&E(b)})):h(r,u,(()=>{w.current&&E(b)}))),p&&N&&N!==O&&w.current&&E(b);const s=()=>{w.current&&E(b)};return e&&r?.on(e,s),n&&r?.store.on(n,s),()=>{w.current=!1,r&&e?.split(" ").forEach((e=>r.off(e,s))),n&&r&&n.split(" ").forEach((e=>r.store.off(e,s)))}}),[r,O]),n.useEffect((()=>{w.current&&p&&E(g)}),[r,c,p]);const I=[v,r,p];if(I.t=v,I.i18n=r,I.ready=p,p)return I;if(!p&&!l)return I;throw new Promise((e=>{t.lng?m(r,t.lng,u,(()=>e())):h(r,u,(()=>e()))}))};const D=function(e,t){let s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const{i18n:i}=s,{i18n:a}=n.useContext(A)||{},r=i||a||k();r.options?.isClone||(e&&!r.initializedStoreOnce&&(r.services.resourceStore.data=e,r.options.ns=Object.values(e).reduce(((e,n)=>(Object.keys(n).forEach((n=>{e.indexOf(n)<0&&e.push(n)})),e)),r.options.ns),r.initializedStoreOnce=!0,r.isInitialized=!0),t&&!r.initializedLanguageOnce&&(r.changeLanguage(t),r.initializedLanguageOnce=!0))};e.I18nContext=A,e.I18nextProvider=function(e){let{i18n:t,defaultNS:s,children:i}=e;const a=n.useMemo((()=>({i18n:t,defaultNS:s})),[t,s]);return n.createElement(A.Provider,{value:a},i)},e.Trans=function(e){let{children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:d,t:f,shouldUnescape:h,...m}=e;const{i18n:g,defaultNS:y}=n.useContext(A)||{},x=d||g||k(),b=f||x?.t.bind(x);return P({children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o,values:l,defaults:c,components:u,ns:p||b?.ns||y||x?.options?.defaultNS,i18n:x,t:f,shouldUnescape:h,...m})},e.TransWithoutContext=P,e.Translation=e=>{let{ns:n,children:t,...s}=e;const[i,a,r]=B(n,s);return t(i,{i18n:a,lng:a.language},r)},e.composeInitialProps=z,e.date=()=>"",e.getDefaults=$,e.getI18n=k,e.getInitialProps=F,e.initReactI18next=L,e.number=()=>"",e.plural=()=>"",e.select=()=>"",e.selectOrdinal=()=>"",e.setDefaults=N,e.setI18n=I,e.time=()=>"",e.useSSR=D,e.useTranslation=B,e.withSSR=()=>function(e){function t(t){let{initialI18nStore:s,initialLanguage:i,...a}=t;return D(s,i),n.createElement(e,{...a})}return t.getInitialProps=z(e),t.displayName=`withI18nextSSR(${g(e)})`,t.WrappedComponent=e,t},e.withTranslation=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return function(s){function i(i){let{forwardedRef:a,...r}=i;const[o,l,c]=B(e,{...r,keyPrefix:t.keyPrefix}),u={...r,t:o,i18n:l,tReady:c};return t.withRef&&a?u.ref=a:!t.withRef&&a&&(u.forwardedRef=a),n.createElement(s,u)}i.displayName=`withI18nextTranslation(${g(s)})`,i.WrappedComponent=s;return t.withRef?n.forwardRef(((e,t)=>n.createElement(i,Object.assign({},e,{forwardedRef:t})))):i}}})); -diff --git a/react-i18next.js b/react-i18next.js -index 3723bd7c0f5762bdb09e3226ac86ff255cbf9859..9178ae9f7cdb776f51a64ff6c79eed1d18fbd836 100644 ---- a/react-i18next.js -+++ b/react-i18next.js -@@ -499,7 +499,7 @@ - } - addUsedNamespaces(namespaces) { - namespaces.forEach(ns => { -- this.usedNamespaces[ns] ??= true; -+ this.usedNamespaces[ns] = this.usedNamespaces[ns] ?? true; - }); - } - getUsedNamespaces() { -diff --git a/react-i18next.min.js b/react-i18next.min.js -index 2eef624040aab6b4b9ba4699bf7e4777842bf0a2..69e17753d545df9dc26aa3411b477a4dff5e8361 100644 ---- a/react-i18next.min.js -+++ b/react-i18next.min.js -@@ -1 +1 @@ --!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports,require("react")):"function"==typeof define&&define.amd?define(["exports","react"],n):n((e="undefined"!=typeof globalThis?globalThis:e||self).ReactI18next={},e.React)}(this,(function(e,n){"use strict";function t(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var s=t({area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0}),i=/\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;function a(e){var n={type:"tag",name:"",voidElement:!1,attrs:{},children:[]},t=e.match(/<\/?([^\s]+?)[/\s>]/);if(t&&(n.name=t[1],(s[t[1]]||"/"===e.charAt(e.length-2))&&(n.voidElement=!0),n.name.startsWith("!--"))){var a=e.indexOf("--\x3e");return{type:"comment",comment:-1!==a?e.slice(4,a):""}}for(var r=new RegExp(i),o=null;null!==(o=r.exec(e));)if(o[0].trim())if(o[1]){var l=o[1].trim(),c=[l,""];l.indexOf("=")>-1&&(c=l.split("=")),n.attrs[c[0]]=c[1],r.lastIndex--}else o[2]&&(n.attrs[o[2]]=o[3].trim().substring(1,o[3].length-1));return n}var r=/<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g,o=/^\s*$/,l=Object.create(null);var c=function(e,n){n||(n={}),n.components||(n.components=l);var t,s=[],i=[],c=-1,u=!1;if(0!==e.indexOf("<")){var p=e.indexOf("<");s.push({type:"text",content:-1===p?e:e.substring(0,p)})}return e.replace(r,(function(r,l){if(u){if(r!=="")return;u=!1}var p,d="/"!==r.charAt(1),f=r.startsWith("\x3c!--"),h=l+r.length,m=e.charAt(h);if(f){var g=a(r);return c<0?(s.push(g),s):((p=i[c]).children.push(g),s)}if(d&&(c++,"tag"===(t=a(r)).type&&n.components[t.name]&&(t.type="component",u=!0),t.voidElement||u||!m||"<"===m||t.children.push({type:"text",content:e.slice(h,e.indexOf("<",h))}),0===c&&s.push(t),(p=i[c-1])&&p.children.push(t),i[c]=t),(!d||t.voidElement)&&(c>-1&&(t.voidElement||t.name===r.slice(2,-1))&&(c--,t=-1===c?s:i[c]),!u&&"<"!==m&&m)){p=-1===c?s:i[c].children;var y=e.indexOf("<",h),x=e.slice(h,-1===y?void 0:y);o.test(x)&&(x=" "),(y>-1&&c+p.length>=0||" "!==x)&&p.push({type:"text",content:x})}})),s};const u=function(){if(console?.warn){for(var e=arguments.length,n=new Array(e),t=0;t()=>{if(e.isInitialized)n();else{const t=()=>{setTimeout((()=>{e.off("initialized",t)}),0),n()};e.on("initialized",t)}},h=(e,n,t)=>{e.loadNamespaces(n,f(e,t))},m=(e,n,t,s)=>{y(t)&&(t=[t]),t.forEach((n=>{e.options.ns.indexOf(n)<0&&e.options.ns.push(n)})),e.loadLanguages(n,f(e,s))},g=e=>e.displayName||e.name||(y(e)&&e.length>0?e:"Unknown"),y=e=>"string"==typeof e,x=e=>"object"==typeof e&&null!==e,b=/&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34|nbsp|#160|copy|#169|reg|#174|hellip|#8230|#x2F|#47);/g,v={"&":"&","&":"&","<":"<","<":"<",">":">",">":">","'":"'","'":"'",""":'"',""":'"'," ":" "," ":" ","©":"©","©":"©","®":"®","®":"®","…":"…","…":"…","/":"/","/":"/"},E=e=>v[e];let O={bindI18n:"languageChanged",bindI18nStore:"",transEmptyNodeValue:"",transSupportBasicHtmlNodes:!0,transWrapTextNodes:"",transKeepBasicHtmlNodesFor:["br","strong","i","p"],useSuspense:!0,unescape:e=>e.replace(b,E)};const N=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};O={...O,...e}},$=()=>O;let w;const I=e=>{w=e},k=()=>w,S=(e,n)=>{if(!e)return!1;const t=e.props?.children??e.children;return n?t.length>0:!!t},j=e=>{if(!e)return[];const n=e.props?.children??e.children;return e.props?.i18nIsDynamicList?R(n):n},R=e=>Array.isArray(e)?e:[e],T=(e,t)=>{if(!e)return"";let s="";const i=R(e),a=t?.transSupportBasicHtmlNodes?t.transKeepBasicHtmlNodesFor??[]:[];return i.forEach(((e,i)=>{if(y(e))s+=`${e}`;else if(n.isValidElement(e)){const{props:n,type:r}=e,o=Object.keys(n).length,l=a.indexOf(r)>-1,c=n.children;if(c||!l||o)if(!c&&(!l||o)||n.i18nIsDynamicList)s+=`<${i}>`;else if(l&&1===o&&y(c))s+=`<${r}>${c}`;else{const e=T(c,t);s+=`<${i}>${e}`}else s+=`<${r}/>`}else if(null===e)u("Trans: the passed in value is invalid - seems you passed in a null child.");else if(x(e)){const{format:n,...t}=e,i=Object.keys(t);if(1===i.length){const e=n?`${i[0]}, ${n}`:i[0];s+=`{{${e}}}`}else u("react-i18next: the passed in object contained more than one variable - the object should look like {{ value, format }} where format is optional.",e)}else u("Trans: the passed in value is invalid - seems you passed in a variable like {number} - please pass in variables for interpolation as full objects like {{number}}.",e)})),s},C=(e,t,s,i,a,r)=>{if(""===t)return[];const o=i.transKeepBasicHtmlNodesFor||[],l=t&&new RegExp(o.map((e=>`<${e}`)).join("|")).test(t);if(!e&&!l&&!r)return[t];const u={},p=e=>{R(e).forEach((e=>{y(e)||(S(e)?p(j(e)):x(e)&&!n.isValidElement(e)&&Object.assign(u,e))}))};p(e);const d=c(`<0>${t}`),f={...u,...a},h=(e,t,s)=>{const i=j(e),a=g(i,t.children,s);return(e=>Array.isArray(e)&&e.every(n.isValidElement))(i)&&0===a.length||e.props?.i18nIsDynamicList?i:a},m=(e,t,s,i,a)=>{e.dummy?(e.children=t,s.push(n.cloneElement(e,{key:i},a?void 0:t))):s.push(...n.Children.map([e],(e=>{const s={...e.props};return delete s.i18nIsDynamicList,n.createElement(e.type,{...s,key:i,ref:e.ref},a?null:t)})))},g=(t,a,c)=>{const u=R(t);return R(a).reduce(((t,a,p)=>{const d=a.children?.[0]?.content&&s.services.interpolator.interpolate(a.children[0].content,f,s.language);if("tag"===a.type){let r=u[parseInt(a.name,10)];1!==c.length||r||(r=c[0][a.name]),r||(r={});const b=0!==Object.keys(a.attrs).length?((e,n)=>{const t={...n};return t.props=Object.assign(e.props,n.props),t})({props:a.attrs},r):r,v=n.isValidElement(b),E=v&&S(a,!0)&&!a.voidElement,O=l&&x(b)&&b.dummy&&!v,N=x(e)&&Object.hasOwnProperty.call(e,a.name);if(y(b)){const e=s.services.interpolator.interpolate(b,f,s.language);t.push(e)}else if(S(b)||E){const e=h(b,a,c);m(b,e,t,p)}else if(O){const e=g(u,a.children,c);m(b,e,t,p)}else if(Number.isNaN(parseFloat(a.name)))if(N){const e=h(b,a,c);m(b,e,t,p,a.voidElement)}else if(i.transSupportBasicHtmlNodes&&o.indexOf(a.name)>-1)if(a.voidElement)t.push(n.createElement(a.name,{key:`${a.name}-${p}`}));else{const e=g(u,a.children,c);t.push(n.createElement(a.name,{key:`${a.name}-${p}`},e))}else if(a.voidElement)t.push(`<${a.name} />`);else{const e=g(u,a.children,c);t.push(`<${a.name}>${e}`)}else if(x(b)&&!v){const e=a.children[0]?d:null;e&&t.push(e)}else m(b,d,t,p,1!==a.children.length||!d)}else if("text"===a.type){const e=i.transWrapTextNodes,o=r?i.unescape(s.services.interpolator.interpolate(a.content,f,s.language)):s.services.interpolator.interpolate(a.content,f,s.language);e?t.push(n.createElement(e,{key:`${a.name}-${p}`},o)):t.push(o)}return t}),[])},b=g([{dummy:!0,children:e||[]}],d,R(e||[]));return j(b[0])};function P(e){let{children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:f,t:h,shouldUnescape:m,...g}=e;const x=f||k();if(!x)return d("You will need to pass in an i18next instance by using i18nextReactModule"),t;const b=h||x.t.bind(x)||(e=>e),v={...$(),...x.options?.react};let E=p||b.ns||x.options?.defaultNS;E=y(E)?[E]:E||["translation"];const O=T(t,v),N=c||O||v.transEmptyNodeValue||a,{hashTransKey:w}=v,I=a||(w?w(O||N):O||N);x.options?.interpolation?.defaultVariables&&(l=l&&Object.keys(l).length>0?{...l,...x.options.interpolation.defaultVariables}:{...x.options.interpolation.defaultVariables});const S=l||void 0!==s||!t?o.interpolation:{interpolation:{...o.interpolation,prefix:"#$?",suffix:"?$#"}},j={...o,context:r||o.context,count:s,...l,...S,defaultValue:N,ns:E},R=I?b(I,j):N;u&&Object.keys(u).forEach((e=>{const t=u[e];"function"==typeof t.type||!t.props||!t.props.children||R.indexOf(`${e}/>`)<0&&R.indexOf(`${e} />`)<0||(u[e]=n.createElement((function(){return n.createElement(n.Fragment,null,t)})))}));const P=C(u||t,R,x,v,j,m),L=i??v.defaultTransParent;return L?n.createElement(L,g,P):P}const L={type:"3rdParty",init(e){N(e.options.react),I(e)}},A=n.createContext();class V{constructor(){this.usedNamespaces={}}addUsedNamespaces(e){e.forEach((e=>{this.usedNamespaces[e]??=!0}))}getUsedNamespaces(){return Object.keys(this.usedNamespaces)}}const z=e=>async n=>({...await(e.getInitialProps?.(n))??{},...F()}),F=()=>{const e=k(),n=e.reportNamespaces?.getUsedNamespaces()??[],t={},s={};return e.languages.forEach((t=>{s[t]={},n.forEach((n=>{s[t][n]=e.getResourceBundle(t,n)||{}}))})),t.initialI18nStore=s,t.initialLanguage=e.language,t};const U=(e,n,t,s)=>e.getFixedT(n,t,s),B=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{i18n:s}=t,{i18n:i,defaultNS:a}=n.useContext(A)||{},r=s||i||k();if(r&&!r.reportNamespaces&&(r.reportNamespaces=new V),!r){d("You will need to pass in an i18next instance by using initReactI18next");const e=(e,n)=>y(n)?n:x(n)&&y(n.defaultValue)?n.defaultValue:Array.isArray(e)?e[e.length-1]:e,n=[e,{},!1];return n.t=e,n.i18n={},n.ready=!1,n}r.options.react?.wait&&d("It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.");const o={...$(),...r.options.react,...t},{useSuspense:l,keyPrefix:c}=o;let u=e||a||r.options?.defaultNS;u=y(u)?[u]:u||["translation"],r.reportNamespaces.addUsedNamespaces?.(u);const p=(r.isInitialized||r.initializedStoreOnce)&&u.every((e=>function(e,n){let t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return n.languages&&n.languages.length?n.hasLoadedNamespace(e,{lng:t.lng,precheck:(n,s)=>{if(t.bindI18n?.indexOf("languageChanging")>-1&&n.services.backendConnector.backend&&n.isLanguageChangingTo&&!s(n.isLanguageChangingTo,e))return!1}}):(d("i18n.languages were undefined or empty",n.languages),!0)}(e,r,o))),f=((e,t,s,i)=>n.useCallback(U(e,t,s,i),[e,t,s,i]))(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),g=()=>f,b=()=>U(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),[v,E]=n.useState(g);let O=u.join();t.lng&&(O=`${t.lng}${O}`);const N=((e,t)=>{const s=n.useRef();return n.useEffect((()=>{s.current=e}),[e,t]),s.current})(O),w=n.useRef(!0);n.useEffect((()=>{const{bindI18n:e,bindI18nStore:n}=o;w.current=!0,p||l||(t.lng?m(r,t.lng,u,(()=>{w.current&&E(b)})):h(r,u,(()=>{w.current&&E(b)}))),p&&N&&N!==O&&w.current&&E(b);const s=()=>{w.current&&E(b)};return e&&r?.on(e,s),n&&r?.store.on(n,s),()=>{w.current=!1,r&&e?.split(" ").forEach((e=>r.off(e,s))),n&&r&&n.split(" ").forEach((e=>r.store.off(e,s)))}}),[r,O]),n.useEffect((()=>{w.current&&p&&E(g)}),[r,c,p]);const I=[v,r,p];if(I.t=v,I.i18n=r,I.ready=p,p)return I;if(!p&&!l)return I;throw new Promise((e=>{t.lng?m(r,t.lng,u,(()=>e())):h(r,u,(()=>e()))}))};const D=function(e,t){let s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const{i18n:i}=s,{i18n:a}=n.useContext(A)||{},r=i||a||k();r.options?.isClone||(e&&!r.initializedStoreOnce&&(r.services.resourceStore.data=e,r.options.ns=Object.values(e).reduce(((e,n)=>(Object.keys(n).forEach((n=>{e.indexOf(n)<0&&e.push(n)})),e)),r.options.ns),r.initializedStoreOnce=!0,r.isInitialized=!0),t&&!r.initializedLanguageOnce&&(r.changeLanguage(t),r.initializedLanguageOnce=!0))};e.I18nContext=A,e.I18nextProvider=function(e){let{i18n:t,defaultNS:s,children:i}=e;const a=n.useMemo((()=>({i18n:t,defaultNS:s})),[t,s]);return n.createElement(A.Provider,{value:a},i)},e.Trans=function(e){let{children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:d,t:f,shouldUnescape:h,...m}=e;const{i18n:g,defaultNS:y}=n.useContext(A)||{},x=d||g||k(),b=f||x?.t.bind(x);return P({children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o,values:l,defaults:c,components:u,ns:p||b?.ns||y||x?.options?.defaultNS,i18n:x,t:f,shouldUnescape:h,...m})},e.TransWithoutContext=P,e.Translation=e=>{let{ns:n,children:t,...s}=e;const[i,a,r]=B(n,s);return t(i,{i18n:a,lng:a.language},r)},e.composeInitialProps=z,e.date=()=>"",e.getDefaults=$,e.getI18n=k,e.getInitialProps=F,e.initReactI18next=L,e.number=()=>"",e.plural=()=>"",e.select=()=>"",e.selectOrdinal=()=>"",e.setDefaults=N,e.setI18n=I,e.time=()=>"",e.useSSR=D,e.useTranslation=B,e.withSSR=()=>function(e){function t(t){let{initialI18nStore:s,initialLanguage:i,...a}=t;return D(s,i),n.createElement(e,{...a})}return t.getInitialProps=z(e),t.displayName=`withI18nextSSR(${g(e)})`,t.WrappedComponent=e,t},e.withTranslation=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return function(s){function i(i){let{forwardedRef:a,...r}=i;const[o,l,c]=B(e,{...r,keyPrefix:t.keyPrefix}),u={...r,t:o,i18n:l,tReady:c};return t.withRef&&a?u.ref=a:!t.withRef&&a&&(u.forwardedRef=a),n.createElement(s,u)}i.displayName=`withI18nextTranslation(${g(s)})`,i.WrappedComponent=s;return t.withRef?n.forwardRef(((e,t)=>n.createElement(i,Object.assign({},e,{forwardedRef:t})))):i}}})); -+!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports,require("react")):"function"==typeof define&&define.amd?define(["exports","react"],n):n((e="undefined"!=typeof globalThis?globalThis:e||self).ReactI18next={},e.React)}(this,(function(e,n){"use strict";function t(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var s=t({area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0}),i=/\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;function a(e){var n={type:"tag",name:"",voidElement:!1,attrs:{},children:[]},t=e.match(/<\/?([^\s]+?)[/\s>]/);if(t&&(n.name=t[1],(s[t[1]]||"/"===e.charAt(e.length-2))&&(n.voidElement=!0),n.name.startsWith("!--"))){var a=e.indexOf("--\x3e");return{type:"comment",comment:-1!==a?e.slice(4,a):""}}for(var r=new RegExp(i),o=null;null!==(o=r.exec(e));)if(o[0].trim())if(o[1]){var l=o[1].trim(),c=[l,""];l.indexOf("=")>-1&&(c=l.split("=")),n.attrs[c[0]]=c[1],r.lastIndex--}else o[2]&&(n.attrs[o[2]]=o[3].trim().substring(1,o[3].length-1));return n}var r=/<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g,o=/^\s*$/,l=Object.create(null);var c=function(e,n){n||(n={}),n.components||(n.components=l);var t,s=[],i=[],c=-1,u=!1;if(0!==e.indexOf("<")){var p=e.indexOf("<");s.push({type:"text",content:-1===p?e:e.substring(0,p)})}return e.replace(r,(function(r,l){if(u){if(r!=="")return;u=!1}var p,d="/"!==r.charAt(1),f=r.startsWith("\x3c!--"),h=l+r.length,m=e.charAt(h);if(f){var g=a(r);return c<0?(s.push(g),s):((p=i[c]).children.push(g),s)}if(d&&(c++,"tag"===(t=a(r)).type&&n.components[t.name]&&(t.type="component",u=!0),t.voidElement||u||!m||"<"===m||t.children.push({type:"text",content:e.slice(h,e.indexOf("<",h))}),0===c&&s.push(t),(p=i[c-1])&&p.children.push(t),i[c]=t),(!d||t.voidElement)&&(c>-1&&(t.voidElement||t.name===r.slice(2,-1))&&(c--,t=-1===c?s:i[c]),!u&&"<"!==m&&m)){p=-1===c?s:i[c].children;var y=e.indexOf("<",h),x=e.slice(h,-1===y?void 0:y);o.test(x)&&(x=" "),(y>-1&&c+p.length>=0||" "!==x)&&p.push({type:"text",content:x})}})),s};const u=function(){if(console?.warn){for(var e=arguments.length,n=new Array(e),t=0;t()=>{if(e.isInitialized)n();else{const t=()=>{setTimeout((()=>{e.off("initialized",t)}),0),n()};e.on("initialized",t)}},h=(e,n,t)=>{e.loadNamespaces(n,f(e,t))},m=(e,n,t,s)=>{y(t)&&(t=[t]),t.forEach((n=>{e.options.ns.indexOf(n)<0&&e.options.ns.push(n)})),e.loadLanguages(n,f(e,s))},g=e=>e.displayName||e.name||(y(e)&&e.length>0?e:"Unknown"),y=e=>"string"==typeof e,x=e=>"object"==typeof e&&null!==e,b=/&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34|nbsp|#160|copy|#169|reg|#174|hellip|#8230|#x2F|#47);/g,v={"&":"&","&":"&","<":"<","<":"<",">":">",">":">","'":"'","'":"'",""":'"',""":'"'," ":" "," ":" ","©":"©","©":"©","®":"®","®":"®","…":"…","…":"…","/":"/","/":"/"},E=e=>v[e];let O={bindI18n:"languageChanged",bindI18nStore:"",transEmptyNodeValue:"",transSupportBasicHtmlNodes:!0,transWrapTextNodes:"",transKeepBasicHtmlNodesFor:["br","strong","i","p"],useSuspense:!0,unescape:e=>e.replace(b,E)};const N=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};O={...O,...e}},$=()=>O;let w;const I=e=>{w=e},k=()=>w,S=(e,n)=>{if(!e)return!1;const t=e.props?.children??e.children;return n?t.length>0:!!t},j=e=>{if(!e)return[];const n=e.props?.children??e.children;return e.props?.i18nIsDynamicList?R(n):n},R=e=>Array.isArray(e)?e:[e],T=(e,t)=>{if(!e)return"";let s="";const i=R(e),a=t?.transSupportBasicHtmlNodes?t.transKeepBasicHtmlNodesFor??[]:[];return i.forEach(((e,i)=>{if(y(e))s+=`${e}`;else if(n.isValidElement(e)){const{props:n,type:r}=e,o=Object.keys(n).length,l=a.indexOf(r)>-1,c=n.children;if(c||!l||o)if(!c&&(!l||o)||n.i18nIsDynamicList)s+=`<${i}>`;else if(l&&1===o&&y(c))s+=`<${r}>${c}`;else{const e=T(c,t);s+=`<${i}>${e}`}else s+=`<${r}/>`}else if(null===e)u("Trans: the passed in value is invalid - seems you passed in a null child.");else if(x(e)){const{format:n,...t}=e,i=Object.keys(t);if(1===i.length){const e=n?`${i[0]}, ${n}`:i[0];s+=`{{${e}}}`}else u("react-i18next: the passed in object contained more than one variable - the object should look like {{ value, format }} where format is optional.",e)}else u("Trans: the passed in value is invalid - seems you passed in a variable like {number} - please pass in variables for interpolation as full objects like {{number}}.",e)})),s},C=(e,t,s,i,a,r)=>{if(""===t)return[];const o=i.transKeepBasicHtmlNodesFor||[],l=t&&new RegExp(o.map((e=>`<${e}`)).join("|")).test(t);if(!e&&!l&&!r)return[t];const u={},p=e=>{R(e).forEach((e=>{y(e)||(S(e)?p(j(e)):x(e)&&!n.isValidElement(e)&&Object.assign(u,e))}))};p(e);const d=c(`<0>${t}`),f={...u,...a},h=(e,t,s)=>{const i=j(e),a=g(i,t.children,s);return(e=>Array.isArray(e)&&e.every(n.isValidElement))(i)&&0===a.length||e.props?.i18nIsDynamicList?i:a},m=(e,t,s,i,a)=>{e.dummy?(e.children=t,s.push(n.cloneElement(e,{key:i},a?void 0:t))):s.push(...n.Children.map([e],(e=>{const s={...e.props};return delete s.i18nIsDynamicList,n.createElement(e.type,{...s,key:i,ref:e.ref},a?null:t)})))},g=(t,a,c)=>{const u=R(t);return R(a).reduce(((t,a,p)=>{const d=a.children?.[0]?.content&&s.services.interpolator.interpolate(a.children[0].content,f,s.language);if("tag"===a.type){let r=u[parseInt(a.name,10)];1!==c.length||r||(r=c[0][a.name]),r||(r={});const b=0!==Object.keys(a.attrs).length?((e,n)=>{const t={...n};return t.props=Object.assign(e.props,n.props),t})({props:a.attrs},r):r,v=n.isValidElement(b),E=v&&S(a,!0)&&!a.voidElement,O=l&&x(b)&&b.dummy&&!v,N=x(e)&&Object.hasOwnProperty.call(e,a.name);if(y(b)){const e=s.services.interpolator.interpolate(b,f,s.language);t.push(e)}else if(S(b)||E){const e=h(b,a,c);m(b,e,t,p)}else if(O){const e=g(u,a.children,c);m(b,e,t,p)}else if(Number.isNaN(parseFloat(a.name)))if(N){const e=h(b,a,c);m(b,e,t,p,a.voidElement)}else if(i.transSupportBasicHtmlNodes&&o.indexOf(a.name)>-1)if(a.voidElement)t.push(n.createElement(a.name,{key:`${a.name}-${p}`}));else{const e=g(u,a.children,c);t.push(n.createElement(a.name,{key:`${a.name}-${p}`},e))}else if(a.voidElement)t.push(`<${a.name} />`);else{const e=g(u,a.children,c);t.push(`<${a.name}>${e}`)}else if(x(b)&&!v){const e=a.children[0]?d:null;e&&t.push(e)}else m(b,d,t,p,1!==a.children.length||!d)}else if("text"===a.type){const e=i.transWrapTextNodes,o=r?i.unescape(s.services.interpolator.interpolate(a.content,f,s.language)):s.services.interpolator.interpolate(a.content,f,s.language);e?t.push(n.createElement(e,{key:`${a.name}-${p}`},o)):t.push(o)}return t}),[])},b=g([{dummy:!0,children:e||[]}],d,R(e||[]));return j(b[0])};function P(e){let{children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:f,t:h,shouldUnescape:m,...g}=e;const x=f||k();if(!x)return d("You will need to pass in an i18next instance by using i18nextReactModule"),t;const b=h||x.t.bind(x)||(e=>e),v={...$(),...x.options?.react};let E=p||b.ns||x.options?.defaultNS;E=y(E)?[E]:E||["translation"];const O=T(t,v),N=c||O||v.transEmptyNodeValue||a,{hashTransKey:w}=v,I=a||(w?w(O||N):O||N);x.options?.interpolation?.defaultVariables&&(l=l&&Object.keys(l).length>0?{...l,...x.options.interpolation.defaultVariables}:{...x.options.interpolation.defaultVariables});const S=l||void 0!==s||!t?o.interpolation:{interpolation:{...o.interpolation,prefix:"#$?",suffix:"?$#"}},j={...o,context:r||o.context,count:s,...l,...S,defaultValue:N,ns:E},R=I?b(I,j):N;u&&Object.keys(u).forEach((e=>{const t=u[e];"function"==typeof t.type||!t.props||!t.props.children||R.indexOf(`${e}/>`)<0&&R.indexOf(`${e} />`)<0||(u[e]=n.createElement((function(){return n.createElement(n.Fragment,null,t)})))}));const P=C(u||t,R,x,v,j,m),L=i??v.defaultTransParent;return L?n.createElement(L,g,P):P}const L={type:"3rdParty",init(e){N(e.options.react),I(e)}},A=n.createContext();class V{constructor(){this.usedNamespaces={}}addUsedNamespaces(e){e.forEach((e=>{this.usedNamespaces[e]=this.usedNamespaces[e]??!0}))}getUsedNamespaces(){return Object.keys(this.usedNamespaces)}}const z=e=>async n=>({...await(e.getInitialProps?.(n))??{},...F()}),F=()=>{const e=k(),n=e.reportNamespaces?.getUsedNamespaces()??[],t={},s={};return e.languages.forEach((t=>{s[t]={},n.forEach((n=>{s[t][n]=e.getResourceBundle(t,n)||{}}))})),t.initialI18nStore=s,t.initialLanguage=e.language,t};const U=(e,n,t,s)=>e.getFixedT(n,t,s),B=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{i18n:s}=t,{i18n:i,defaultNS:a}=n.useContext(A)||{},r=s||i||k();if(r&&!r.reportNamespaces&&(r.reportNamespaces=new V),!r){d("You will need to pass in an i18next instance by using initReactI18next");const e=(e,n)=>y(n)?n:x(n)&&y(n.defaultValue)?n.defaultValue:Array.isArray(e)?e[e.length-1]:e,n=[e,{},!1];return n.t=e,n.i18n={},n.ready=!1,n}r.options.react?.wait&&d("It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.");const o={...$(),...r.options.react,...t},{useSuspense:l,keyPrefix:c}=o;let u=e||a||r.options?.defaultNS;u=y(u)?[u]:u||["translation"],r.reportNamespaces.addUsedNamespaces?.(u);const p=(r.isInitialized||r.initializedStoreOnce)&&u.every((e=>function(e,n){let t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return n.languages&&n.languages.length?n.hasLoadedNamespace(e,{lng:t.lng,precheck:(n,s)=>{if(t.bindI18n?.indexOf("languageChanging")>-1&&n.services.backendConnector.backend&&n.isLanguageChangingTo&&!s(n.isLanguageChangingTo,e))return!1}}):(d("i18n.languages were undefined or empty",n.languages),!0)}(e,r,o))),f=((e,t,s,i)=>n.useCallback(U(e,t,s,i),[e,t,s,i]))(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),g=()=>f,b=()=>U(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),[v,E]=n.useState(g);let O=u.join();t.lng&&(O=`${t.lng}${O}`);const N=((e,t)=>{const s=n.useRef();return n.useEffect((()=>{s.current=e}),[e,t]),s.current})(O),w=n.useRef(!0);n.useEffect((()=>{const{bindI18n:e,bindI18nStore:n}=o;w.current=!0,p||l||(t.lng?m(r,t.lng,u,(()=>{w.current&&E(b)})):h(r,u,(()=>{w.current&&E(b)}))),p&&N&&N!==O&&w.current&&E(b);const s=()=>{w.current&&E(b)};return e&&r?.on(e,s),n&&r?.store.on(n,s),()=>{w.current=!1,r&&e?.split(" ").forEach((e=>r.off(e,s))),n&&r&&n.split(" ").forEach((e=>r.store.off(e,s)))}}),[r,O]),n.useEffect((()=>{w.current&&p&&E(g)}),[r,c,p]);const I=[v,r,p];if(I.t=v,I.i18n=r,I.ready=p,p)return I;if(!p&&!l)return I;throw new Promise((e=>{t.lng?m(r,t.lng,u,(()=>e())):h(r,u,(()=>e()))}))};const D=function(e,t){let s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const{i18n:i}=s,{i18n:a}=n.useContext(A)||{},r=i||a||k();r.options?.isClone||(e&&!r.initializedStoreOnce&&(r.services.resourceStore.data=e,r.options.ns=Object.values(e).reduce(((e,n)=>(Object.keys(n).forEach((n=>{e.indexOf(n)<0&&e.push(n)})),e)),r.options.ns),r.initializedStoreOnce=!0,r.isInitialized=!0),t&&!r.initializedLanguageOnce&&(r.changeLanguage(t),r.initializedLanguageOnce=!0))};e.I18nContext=A,e.I18nextProvider=function(e){let{i18n:t,defaultNS:s,children:i}=e;const a=n.useMemo((()=>({i18n:t,defaultNS:s})),[t,s]);return n.createElement(A.Provider,{value:a},i)},e.Trans=function(e){let{children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:d,t:f,shouldUnescape:h,...m}=e;const{i18n:g,defaultNS:y}=n.useContext(A)||{},x=d||g||k(),b=f||x?.t.bind(x);return P({children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o,values:l,defaults:c,components:u,ns:p||b?.ns||y||x?.options?.defaultNS,i18n:x,t:f,shouldUnescape:h,...m})},e.TransWithoutContext=P,e.Translation=e=>{let{ns:n,children:t,...s}=e;const[i,a,r]=B(n,s);return t(i,{i18n:a,lng:a.language},r)},e.composeInitialProps=z,e.date=()=>"",e.getDefaults=$,e.getI18n=k,e.getInitialProps=F,e.initReactI18next=L,e.number=()=>"",e.plural=()=>"",e.select=()=>"",e.selectOrdinal=()=>"",e.setDefaults=N,e.setI18n=I,e.time=()=>"",e.useSSR=D,e.useTranslation=B,e.withSSR=()=>function(e){function t(t){let{initialI18nStore:s,initialLanguage:i,...a}=t;return D(s,i),n.createElement(e,{...a})}return t.getInitialProps=z(e),t.displayName=`withI18nextSSR(${g(e)})`,t.WrappedComponent=e,t},e.withTranslation=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return function(s){function i(i){let{forwardedRef:a,...r}=i;const[o,l,c]=B(e,{...r,keyPrefix:t.keyPrefix}),u={...r,t:o,i18n:l,tReady:c};return t.withRef&&a?u.ref=a:!t.withRef&&a&&(u.forwardedRef=a),n.createElement(s,u)}i.displayName=`withI18nextTranslation(${g(s)})`,i.WrappedComponent=s;return t.withRef?n.forwardRef(((e,t)=>n.createElement(i,Object.assign({},e,{forwardedRef:t})))):i}}})); -diff --git a/src/context.js b/src/context.js -index 167af9c50f47e34f7473df03bb2bb1e369725934..9b9a5570b0b765c9d7809f912dba2db759ebb68d 100644 ---- a/src/context.js -+++ b/src/context.js -@@ -14,7 +14,7 @@ export class ReportNamespaces { - - addUsedNamespaces(namespaces) { - namespaces.forEach((ns) => { -- this.usedNamespaces[ns] ??= true; -+ this.usedNamespaces[ns] = this.usedNamespaces[ns] ?? true; - }); - } - diff --git a/apps/meteor/.gitignore b/apps/meteor/.gitignore index a9fd54ab8711..6411fe002c51 100644 --- a/apps/meteor/.gitignore +++ b/apps/meteor/.gitignore @@ -70,7 +70,6 @@ Thumbs.db thumbs.db tramp ecosystem.json -pm2.json settings.json /public/livechat packages/rocketchat-i18n/i18n/livechat.* diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index c31645ca4ea4..2fd9252f1ba1 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,287 @@ # @rocket.chat/meteor +## 6.13.0 + +### Minor Changes + +- ([#33156](https://github.com/RocketChat/Rocket.Chat/pull/33156)) added `sidepanelNavigation` to feature preview list + +- ([#32682](https://github.com/RocketChat/Rocket.Chat/pull/32682)) Added support for specifying a unit on departments' creation and update + +- ([#33139](https://github.com/RocketChat/Rocket.Chat/pull/33139)) Added new setting `Allow visitors to finish conversations` that allows admins to decide if omnichannel visitors can close a conversation or not. This doesn't affect agent's capabilities of room closing, neither apps using the livechat bridge to close rooms. + However, if currently your integration relies on `livechat/room.close` endpoint for closing conversations, it's advised to use the authenticated version `livechat/room.closeByUser` of it before turning off this setting. +- ([#32729](https://github.com/RocketChat/Rocket.Chat/pull/32729)) Implemented "omnichannel/contacts.update" endpoint to update contacts + +- ([#32510](https://github.com/RocketChat/Rocket.Chat/pull/32510)) Added a new setting to enable mentions in end to end encrypted channels + +- ([#32821](https://github.com/RocketChat/Rocket.Chat/pull/32821)) Replaced new `SidebarV2` components under feature preview + +- ([#33212](https://github.com/RocketChat/Rocket.Chat/pull/33212)) Added new Admin Feature Preview management view, this will allow the workspace admins to both enable feature previewing in the workspace as well as define which feature previews are enabled by default for the users in the workspace. + +- ([#33011](https://github.com/RocketChat/Rocket.Chat/pull/33011)) Return `parent` and `team` information when calling `rooms.info` endpoint + +- ([#32693](https://github.com/RocketChat/Rocket.Chat/pull/32693)) Introduced "create contacts" endpoint to omnichannel + +- ([#33177](https://github.com/RocketChat/Rocket.Chat/pull/33177)) New `teams.listChildren` endpoint that allows users listing rooms & discussions from teams. Only the discussions from the team's main room are returned. + +- ([#33114](https://github.com/RocketChat/Rocket.Chat/pull/33114)) Wraps some room settings in an accordion advanced settings section in room edit contextual bar to improve organization + +- ([#33160](https://github.com/RocketChat/Rocket.Chat/pull/33160)) Implemented sending email via apps + +- ([#32945](https://github.com/RocketChat/Rocket.Chat/pull/32945)) Added a new setting which allows workspace admins to disable email two factor authentication for SSO (OAuth) users. If enabled, SSO users won't be asked for email two factor authentication. + +- ([#33225](https://github.com/RocketChat/Rocket.Chat/pull/33225)) Implemented new feature preview for Sidepanel + +### Patch Changes + +- ([#33339](https://github.com/RocketChat/Rocket.Chat/pull/33339)) Fixes a problem that caused visitor creation to fail when GDPR setting was enabled and visitor was created via Apps Engine or the deprecated `livechat:registerGuest` method. + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- ([#33317](https://github.com/RocketChat/Rocket.Chat/pull/33317)) Fixed error during sendmessage client stub + +- ([#33381](https://github.com/RocketChat/Rocket.Chat/pull/33381)) Fixes a race condition that causes livechat conversations to get stuck in the agent's sidebar panel after being forwarded. + +- ([#33211](https://github.com/RocketChat/Rocket.Chat/pull/33211)) Allow to use the token from `room.v` when requesting transcript instead of visitor token. Visitors may change their tokens at any time, rendering old conversations impossible to access for them (or for APIs depending on token) as the visitor token won't match the `room.v` token. + +- ([#33298](https://github.com/RocketChat/Rocket.Chat/pull/33298)) Fixed a Federation callback not awaiting db call + +- ([#32939](https://github.com/RocketChat/Rocket.Chat/pull/32939)) Fixed issue where when you marked a room as unread and you were part of it, sometimes it would mark it as read right after + +- ([#33197](https://github.com/RocketChat/Rocket.Chat/pull/33197)) Fixes an issue where the retention policy warning keep displaying even if the retention is disabled inside the room + +- ([#33321](https://github.com/RocketChat/Rocket.Chat/pull/33321)) Changed the contextualbar behavior based on chat size instead the viewport + +- ([#33246](https://github.com/RocketChat/Rocket.Chat/pull/33246)) Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) + +- ([#32999](https://github.com/RocketChat/Rocket.Chat/pull/32999)) Fixes multiple selection for MultiStaticSelectElement in UiKit + +- ([#33155](https://github.com/RocketChat/Rocket.Chat/pull/33155)) Fixed a code issue on NPS service. It was passing `startAt` as the expiration date when creating a banner. + +- ([#33237](https://github.com/RocketChat/Rocket.Chat/pull/33237)) fixed retention policy max age settings not being respected after upgrade + +- ([#33216](https://github.com/RocketChat/Rocket.Chat/pull/33216)) Prevented uiInteraction to subscribe multiple times + +- ([#33295](https://github.com/RocketChat/Rocket.Chat/pull/33295)) Resolves the issue where outgoing integrations failed to trigger after the version 6.12.0 upgrade by correcting the parameter order from the `afterSaveMessage` callback to listener functions. This ensures the correct room information is passed, restoring the functionality of outgoing webhooks, IRC bridge, Autotranslate, and Engagement Dashboard. + +- ([#33193](https://github.com/RocketChat/Rocket.Chat/pull/33193)) Fixed avatar blob image setting in setUserAvatar method by correcting service handling logic. + +- ([#33209](https://github.com/RocketChat/Rocket.Chat/pull/33209)) Fixed `LivechatSessionTaken` webhook event being called without the `agent` param, which represents the agent serving the room. + +- ([#33296](https://github.com/RocketChat/Rocket.Chat/pull/33296)) Fixed remaining direct references to external user avatar URLs + + Fixed local avatars having priority over external provider + + It mainly corrects the behavior of E2E encryption messages and desktop notifications. + +- ([#33157](https://github.com/RocketChat/Rocket.Chat/pull/33157) by [@csuadev](https://github.com/csuadev)) Fixed inconsistency between the markdown parser from the composer and the rest of the application when using bold and italics in a text. + +- ([#33181](https://github.com/RocketChat/Rocket.Chat/pull/33181)) Fixed issue that caused an infinite loading state when uploading a private app to Rocket.Chat + +- ([#33158](https://github.com/RocketChat/Rocket.Chat/pull/33158)) Fixes an issue where multi-step modals were closing unexpectedly + +-
Updated dependencies [bb94c9c67a, 9a38c8e13f, 599762739a, 7c14fd1a80, 9eaefdc892, 274f4f5881, cd0d50016e, 78e6ba4820, 2f9eea03d2, 532f08819e, 79c16d315a, 927710d778, 3a161c4310, 0f21fa01a3, 12d6307998]: + + - @rocket.chat/ui-client@11.0.0 + - @rocket.chat/i18n@0.8.0 + - @rocket.chat/model-typings@0.8.0 + - @rocket.chat/rest-typings@6.13.0 + - @rocket.chat/fuselage-ui-kit@11.0.0 + - @rocket.chat/core-typings@6.13.0 + - @rocket.chat/ui-theming@0.3.0 + - @rocket.chat/ui-video-conf@11.0.0 + - @rocket.chat/ui-composer@0.3.0 + - @rocket.chat/gazzodown@11.0.0 + - @rocket.chat/ui-avatar@7.0.0 + - @rocket.chat/core-services@0.7.0 + - @rocket.chat/message-parser@0.31.31 + - @rocket.chat/models@0.3.0 + - @rocket.chat/web-ui-registration@11.0.0 + - @rocket.chat/ui-contexts@11.0.0 + - @rocket.chat/omnichannel-services@0.3.5 + - @rocket.chat/apps@0.1.8 + - @rocket.chat/presence@0.2.8 + - @rocket.chat/api-client@0.2.8 + - @rocket.chat/license@0.2.8 + - @rocket.chat/pdf-worker@0.2.5 + - @rocket.chat/cron@0.1.8 + - @rocket.chat/instance-status@0.1.8 + - @rocket.chat/server-cloud-communication@0.0.2 +
+ +## 6.13.0-rc.6 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.6 + - @rocket.chat/rest-typings@6.13.0-rc.6 + - @rocket.chat/license@0.2.8-rc.6 + - @rocket.chat/omnichannel-services@0.3.5-rc.6 + - @rocket.chat/pdf-worker@0.2.5-rc.6 + - @rocket.chat/presence@0.2.8-rc.6 + - @rocket.chat/api-client@0.2.8-rc.6 + - @rocket.chat/apps@0.1.8-rc.6 + - @rocket.chat/core-services@0.7.0-rc.6 + - @rocket.chat/cron@0.1.8-rc.6 + - @rocket.chat/fuselage-ui-kit@11.0.0-rc.6 + - @rocket.chat/gazzodown@11.0.0-rc.6 + - @rocket.chat/model-typings@0.8.0-rc.6 + - @rocket.chat/ui-contexts@11.0.0-rc.6 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.3.0-rc.6 + - @rocket.chat/ui-theming@0.3.0-rc.0 + - @rocket.chat/ui-avatar@7.0.0-rc.6 + - @rocket.chat/ui-video-conf@11.0.0-rc.6 + - @rocket.chat/web-ui-registration@11.0.0-rc.6 + - @rocket.chat/instance-status@0.1.8-rc.6 +
+ +## 6.13.0-rc.5 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.5 + - @rocket.chat/rest-typings@6.13.0-rc.5 + - @rocket.chat/license@0.2.8-rc.5 + - @rocket.chat/omnichannel-services@0.3.5-rc.5 + - @rocket.chat/pdf-worker@0.2.5-rc.5 + - @rocket.chat/presence@0.2.8-rc.5 + - @rocket.chat/api-client@0.2.8-rc.5 + - @rocket.chat/apps@0.1.8-rc.5 + - @rocket.chat/core-services@0.7.0-rc.5 + - @rocket.chat/cron@0.1.8-rc.5 + - @rocket.chat/fuselage-ui-kit@11.0.0-rc.5 + - @rocket.chat/gazzodown@11.0.0-rc.5 + - @rocket.chat/model-typings@0.8.0-rc.5 + - @rocket.chat/ui-contexts@11.0.0-rc.5 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.3.0-rc.5 + - @rocket.chat/ui-theming@0.3.0-rc.0 + - @rocket.chat/ui-avatar@7.0.0-rc.5 + - @rocket.chat/ui-client@11.0.0-rc.5 + - @rocket.chat/ui-video-conf@11.0.0-rc.5 + - @rocket.chat/web-ui-registration@11.0.0-rc.5 + - @rocket.chat/instance-status@0.1.8-rc.5 +
+ +## 6.13.0-rc.4 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.4 + - @rocket.chat/rest-typings@6.13.0-rc.4 + - @rocket.chat/license@0.2.8-rc.4 + - @rocket.chat/omnichannel-services@0.3.5-rc.4 + - @rocket.chat/pdf-worker@0.2.5-rc.4 + - @rocket.chat/presence@0.2.8-rc.4 + - @rocket.chat/api-client@0.2.8-rc.4 + - @rocket.chat/apps@0.1.8-rc.4 + - @rocket.chat/core-services@0.7.0-rc.4 + - @rocket.chat/cron@0.1.8-rc.4 + - @rocket.chat/fuselage-ui-kit@11.0.0-rc.4 + - @rocket.chat/gazzodown@11.0.0-rc.4 + - @rocket.chat/model-typings@0.8.0-rc.4 + - @rocket.chat/ui-contexts@11.0.0-rc.4 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.3.0-rc.4 + - @rocket.chat/ui-theming@0.3.0-rc.0 + - @rocket.chat/ui-avatar@7.0.0-rc.4 + - @rocket.chat/ui-client@11.0.0-rc.4 + - @rocket.chat/ui-video-conf@11.0.0-rc.4 + - @rocket.chat/web-ui-registration@11.0.0-rc.4 + - @rocket.chat/instance-status@0.1.8-rc.4 +
+ +## 6.13.0-rc.3 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- ([#33381](https://github.com/RocketChat/Rocket.Chat/pull/33381)) Fixes a race condition that causes livechat conversations to get stuck in the agent's sidebar panel after being forwarded. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.3 + - @rocket.chat/rest-typings@6.13.0-rc.3 + - @rocket.chat/license@0.2.8-rc.3 + - @rocket.chat/omnichannel-services@0.3.5-rc.3 + - @rocket.chat/pdf-worker@0.2.5-rc.3 + - @rocket.chat/presence@0.2.8-rc.3 + - @rocket.chat/api-client@0.2.8-rc.3 + - @rocket.chat/apps@0.1.8-rc.3 + - @rocket.chat/core-services@0.7.0-rc.3 + - @rocket.chat/cron@0.1.8-rc.3 + - @rocket.chat/fuselage-ui-kit@11.0.0-rc.3 + - @rocket.chat/gazzodown@11.0.0-rc.3 + - @rocket.chat/model-typings@0.8.0-rc.3 + - @rocket.chat/ui-contexts@11.0.0-rc.3 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.3.0-rc.3 + - @rocket.chat/ui-theming@0.3.0-rc.0 + - @rocket.chat/ui-avatar@7.0.0-rc.3 + - @rocket.chat/ui-client@11.0.0-rc.3 + - @rocket.chat/ui-video-conf@11.0.0-rc.3 + - @rocket.chat/web-ui-registration@11.0.0-rc.3 + - @rocket.chat/instance-status@0.1.8-rc.3 +
+ +## 6.13.0-rc.2 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.2 + - @rocket.chat/rest-typings@6.13.0-rc.2 + - @rocket.chat/license@0.2.8-rc.2 + - @rocket.chat/omnichannel-services@0.3.5-rc.2 + - @rocket.chat/pdf-worker@0.2.5-rc.2 + - @rocket.chat/presence@0.2.8-rc.2 + - @rocket.chat/api-client@0.2.8-rc.2 + - @rocket.chat/apps@0.1.8-rc.2 + - @rocket.chat/core-services@0.7.0-rc.2 + - @rocket.chat/cron@0.1.8-rc.2 + - @rocket.chat/fuselage-ui-kit@11.0.0-rc.2 + - @rocket.chat/gazzodown@11.0.0-rc.2 + - @rocket.chat/model-typings@0.8.0-rc.2 + - @rocket.chat/ui-contexts@11.0.0-rc.2 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.3.0-rc.2 + - @rocket.chat/ui-theming@0.3.0-rc.0 + - @rocket.chat/ui-avatar@7.0.0-rc.2 + - @rocket.chat/ui-client@11.0.0-rc.2 + - @rocket.chat/ui-video-conf@11.0.0-rc.2 + - @rocket.chat/web-ui-registration@11.0.0-rc.2 + - @rocket.chat/instance-status@0.1.8-rc.2 +
+ ## 6.13.0-rc.1 ### Minor Changes diff --git a/apps/meteor/app/2fa/server/code/EmailCheck.ts b/apps/meteor/app/2fa/server/code/EmailCheck.ts index 5baf218a62bb..d947c1b30c2e 100644 --- a/apps/meteor/app/2fa/server/code/EmailCheck.ts +++ b/apps/meteor/app/2fa/server/code/EmailCheck.ts @@ -38,7 +38,7 @@ export class EmailCheck implements ICodeCheck { private async send2FAEmail(address: string, random: string, user: IUser): Promise { const language = user.language || settings.get('Language') || 'en'; - const t = i18n.getFixedT(language); + const t = (s: string): string => i18n.t(s, { lng: language }); await Mailer.send({ to: address, diff --git a/apps/meteor/app/2fa/server/functions/resetTOTP.ts b/apps/meteor/app/2fa/server/functions/resetTOTP.ts index 84426cd4f88d..3be8ec7c8060 100644 --- a/apps/meteor/app/2fa/server/functions/resetTOTP.ts +++ b/apps/meteor/app/2fa/server/functions/resetTOTP.ts @@ -22,7 +22,7 @@ const sendResetNotification = async function (uid: string): Promise { return; } - const t = i18n.getFixedT(language); + const t = (s: string): string => i18n.t(s, { lng: language }); const text = ` ${t('Your_TOTP_has_been_reset')} diff --git a/apps/meteor/app/api/server/lib/users.ts b/apps/meteor/app/api/server/lib/users.ts index 0289f1fe5ff5..164ea8c2b747 100644 --- a/apps/meteor/app/api/server/lib/users.ts +++ b/apps/meteor/app/api/server/lib/users.ts @@ -147,9 +147,6 @@ export async function findPaginatedUsersByStatus({ if (sort?.status) { actualSort.active = sort.status; } - if (sort?.name) { - actualSort.nameInsensitive = sort.name; - } const match: Filter> = {}; switch (status) { case 'active': @@ -198,6 +195,7 @@ export async function findPaginatedUsersByStatus({ if (roles?.length && !roles.includes('all')) { match.roles = { $in: roles }; } + const { cursor, totalCount } = await Users.findPaginated( { ...match, diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 4f4794591e02..821d1fdd60d5 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -118,6 +118,7 @@ export class AppLivechatBridge extends LivechatBridge { sidebarIcon: source.sidebarIcon, defaultIcon: source.defaultIcon, label: source.label, + destination: source.destination, }), }, }, diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts index 344acc74bda4..4743d72457d8 100644 --- a/apps/meteor/app/apps/server/bridges/rooms.ts +++ b/apps/meteor/app/apps/server/bridges/rooms.ts @@ -245,6 +245,57 @@ export class AppRoomBridge extends RoomBridge { return users.map((user: ICoreUser) => userConverter.convertToApp(user)); } + protected async getUnreadByUser(roomId: string, uid: string, options: GetMessagesOptions, appId: string): Promise> { + this.orch.debugLog(`The App ${appId} is getting the unread messages for the user: "${uid}" in the room: "${roomId}"`); + + const messageConverter = this.orch.getConverters()?.get('messages'); + if (!messageConverter) { + throw new Error('Message converter not found'); + } + + const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, uid, { projection: { ls: 1 } }); + + if (!subscription) { + const errorMessage = `No subscription found for user with ID "${uid}" in room with ID "${roomId}". This means the user is not subscribed to the room.`; + this.orch.debugLog(errorMessage); + throw new Error('User not subscribed to room'); + } + + const lastSeen = subscription?.ls; + if (!lastSeen) { + return []; + } + + const sort: Sort = options.sort?.createdAt ? { ts: options.sort.createdAt } : { ts: 1 }; + + const cursor = Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes(roomId, lastSeen, new Date(), [], { + ...options, + sort, + }); + + const messages = await cursor.toArray(); + return Promise.all(messages.map((msg) => messageConverter.convertMessageRaw(msg))); + } + + protected async getUserUnreadMessageCount(roomId: string, uid: string, appId: string): Promise { + this.orch.debugLog(`The App ${appId} is getting the unread messages count of the room: "${roomId}" for the user: "${uid}"`); + + const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, uid, { projection: { ls: 1 } }); + + if (!subscription) { + const errorMessage = `No subscription found for user with ID "${uid}" in room with ID "${roomId}". This means the user is not subscribed to the room.`; + this.orch.debugLog(errorMessage); + throw new Error('User not subscribed to room'); + } + + const lastSeen = subscription?.ls; + if (!lastSeen) { + return 0; + } + + return Messages.countVisibleByRoomIdBetweenTimestampsNotContainingTypes(roomId, lastSeen, new Date(), []); + } + protected async removeUsers(roomId: string, usernames: Array, appId: string): Promise { this.orch.debugLog(`The App ${appId} is removing users ${usernames} from room id: ${roomId}`); if (!roomId) { diff --git a/apps/meteor/app/apps/server/converters/visitors.js b/apps/meteor/app/apps/server/converters/visitors.js index c8fb0b7c4a21..32864e3e900e 100644 --- a/apps/meteor/app/apps/server/converters/visitors.js +++ b/apps/meteor/app/apps/server/converters/visitors.js @@ -36,6 +36,7 @@ export class AppVisitorsConverter { visitorEmails: 'visitorEmails', livechatData: 'livechatData', status: 'status', + contactId: 'contactId', }; return transformMappedData(visitor, map); @@ -54,6 +55,7 @@ export class AppVisitorsConverter { phone: visitor.phone, livechatData: visitor.livechatData, status: visitor.status || 'online', + contactId: visitor.contactId, ...(visitor.visitorEmails && { visitorEmails: visitor.visitorEmails }), ...(visitor.department && { department: visitor.department }), }; diff --git a/apps/meteor/app/authorization/server/constant/permissions.ts b/apps/meteor/app/authorization/server/constant/permissions.ts index f57943412fb4..bb6dfe14c6ff 100644 --- a/apps/meteor/app/authorization/server/constant/permissions.ts +++ b/apps/meteor/app/authorization/server/constant/permissions.ts @@ -105,6 +105,10 @@ export const permissions = [ _id: 'view-livechat-contact', roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'], }, + { + _id: 'view-livechat-contact-history', + roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'], + }, { _id: 'view-livechat-manager', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, { _id: 'view-omnichannel-contact-center', diff --git a/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js b/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js index 87d13f1deffd..2f4f77aa0bd2 100644 --- a/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js +++ b/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js @@ -76,6 +76,7 @@ Meteor.startup(() => { } else { res.setHeader('Last-Modified', new Date().toUTCString()); } + res.setHeader('Content-Type', file.contentType); res.setHeader('Content-Length', file.length); diff --git a/apps/meteor/app/e2e/server/functions/provideUsersSuggestedGroupKeys.ts b/apps/meteor/app/e2e/server/functions/provideUsersSuggestedGroupKeys.ts index 42408f398ecb..9f69f17920d1 100644 --- a/apps/meteor/app/e2e/server/functions/provideUsersSuggestedGroupKeys.ts +++ b/apps/meteor/app/e2e/server/functions/provideUsersSuggestedGroupKeys.ts @@ -2,6 +2,7 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions } from '@rocket.chat/models'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; +import { notifyOnSubscriptionChanged, notifyOnRoomChangedById } from '../../../lib/server/lib/notifyListener'; export const provideUsersSuggestedGroupKeys = async ( userId: IUser['_id'], @@ -21,13 +22,15 @@ export const provideUsersSuggestedGroupKeys = async ( const usersWithSuggestedKeys = []; for await (const user of usersSuggestedGroupKeys[roomId]) { - const { modifiedCount } = await Subscriptions.setGroupE2ESuggestedKey(user._id, roomId, user.key); - if (!modifiedCount) { + const { value } = await Subscriptions.setGroupE2ESuggestedKey(user._id, roomId, user.key); + if (!value) { continue; } + void notifyOnSubscriptionChanged(value); usersWithSuggestedKeys.push(user._id); } await Rooms.removeUsersFromE2EEQueueByRoomId(roomId, usersWithSuggestedKeys); + void notifyOnRoomChangedById(roomId); } }; diff --git a/apps/meteor/app/e2e/server/methods/updateGroupKey.ts b/apps/meteor/app/e2e/server/methods/updateGroupKey.ts index 87182f723e7d..6974ba225d9f 100644 --- a/apps/meteor/app/e2e/server/methods/updateGroupKey.ts +++ b/apps/meteor/app/e2e/server/methods/updateGroupKey.ts @@ -3,7 +3,7 @@ import { Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { notifyOnSubscriptionChangedById, notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../../lib/server/lib/notifyListener'; +import { notifyOnSubscriptionChangedById, notifyOnSubscriptionChanged } from '../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -34,9 +34,9 @@ Meteor.methods({ } // uid also has subscription to this room - const { modifiedCount } = await Subscriptions.setGroupE2ESuggestedKey(uid, rid, key); - if (modifiedCount) { - void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, uid); + const { value } = await Subscriptions.setGroupE2ESuggestedKey(uid, rid, key); + if (value) { + void notifyOnSubscriptionChanged(value); } } }, diff --git a/apps/meteor/app/file/server/file.server.ts b/apps/meteor/app/file/server/file.server.ts index e0190d06a729..3d4f6cf02609 100644 --- a/apps/meteor/app/file/server/file.server.ts +++ b/apps/meteor/app/file/server/file.server.ts @@ -6,6 +6,7 @@ import stream from 'stream'; import type { ObjectId } from 'bson'; import { MongoInternals } from 'meteor/mongo'; +import mime from 'mime-type/with-db'; import mkdirp from 'mkdirp'; import type { GridFSBucketReadStream } from 'mongodb'; import { GridFSBucket } from 'mongodb'; @@ -166,11 +167,13 @@ class FileSystem implements IRocketChatFileStore { const rs = this.createReadStream(fileName); return { readStream: rs, - // contentType: file.contentType + // We currently don't store the content type of uploaded custom sounds when using + // The filesystem storage. We will use mime to infer its type from the extension. + contentType: (mime.lookup(fileName) as string) || 'application/octet-stream', length: stat.size, }; } catch (error1) { - // + console.error(error1); } } @@ -197,7 +200,7 @@ class FileSystem implements IRocketChatFileStore { try { return await this.remove(fileName); } catch (error1) { - // + console.error(error1); } } } diff --git a/apps/meteor/app/importer-csv/server/CsvImporter.ts b/apps/meteor/app/importer-csv/server/CsvImporter.ts index 60c07c3288ce..a9844e747640 100644 --- a/apps/meteor/app/importer-csv/server/CsvImporter.ts +++ b/apps/meteor/app/importer-csv/server/CsvImporter.ts @@ -4,7 +4,7 @@ import { Random } from '@rocket.chat/random'; import { parse } from 'csv-parse/lib/sync'; import { Importer, ProgressStep, ImporterWebsocket } from '../../importer/server'; -import type { IConverterOptions } from '../../importer/server/classes/ImportDataConverter'; +import type { ConverterOptions } from '../../importer/server/classes/ImportDataConverter'; import type { ImporterProgress } from '../../importer/server/classes/ImporterProgress'; import type { ImporterInfo } from '../../importer/server/definitions/ImporterInfo'; import { notifyOnSettingChanged } from '../../lib/server/lib/notifyListener'; @@ -12,7 +12,7 @@ import { notifyOnSettingChanged } from '../../lib/server/lib/notifyListener'; export class CsvImporter extends Importer { private csvParser: (csv: string) => string[]; - constructor(info: ImporterInfo, importRecord: IImport, converterOptions: IConverterOptions = {}) { + constructor(info: ImporterInfo, importRecord: IImport, converterOptions: ConverterOptions = {}) { super(info, importRecord, converterOptions); this.csvParser = parse; diff --git a/apps/meteor/app/importer-hipchat-enterprise/server/HipChatEnterpriseImporter.js b/apps/meteor/app/importer-hipchat-enterprise/server/HipChatEnterpriseImporter.js index 663300e44154..ddabdfac4ee2 100644 --- a/apps/meteor/app/importer-hipchat-enterprise/server/HipChatEnterpriseImporter.js +++ b/apps/meteor/app/importer-hipchat-enterprise/server/HipChatEnterpriseImporter.js @@ -2,7 +2,7 @@ import fs from 'fs'; import path from 'path'; import { Readable } from 'stream'; -import { Settings } from '@rocket.chat/models'; +import { ImportData, Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { Importer, ProgressStep } from '../../importer/server'; @@ -89,6 +89,13 @@ export class HipChatEnterpriseImporter extends Importer { await super.addCountToTotal(count); } + async findDMForImportedUsers(...users) { + const record = await ImportData.findDMForImportedUsers(...users); + if (record) { + return record.data; + } + } + async prepareUserMessagesFile(file) { this.logger.debug(`preparing room with ${file.length} messages `); let count = 0; @@ -110,7 +117,7 @@ export class HipChatEnterpriseImporter extends Importer { const users = [senderId, receiverId].sort(); if (!dmRooms[receiverId]) { - dmRooms[receiverId] = await this.converter.findDMForImportedUsers(senderId, receiverId); + dmRooms[receiverId] = await this.findDMForImportedUsers(senderId, receiverId); if (!dmRooms[receiverId]) { const room = { diff --git a/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts b/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts index da85e9b73296..400a9856c4e7 100644 --- a/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts +++ b/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts @@ -8,12 +8,12 @@ import { Random } from '@rocket.chat/random'; import { FileUpload } from '../../file-upload/server'; import { Importer, ProgressStep, Selection } from '../../importer/server'; -import type { IConverterOptions } from '../../importer/server/classes/ImportDataConverter'; +import type { ConverterOptions } from '../../importer/server/classes/ImportDataConverter'; import type { ImporterProgress } from '../../importer/server/classes/ImporterProgress'; import type { ImporterInfo } from '../../importer/server/definitions/ImporterInfo'; export class PendingFileImporter extends Importer { - constructor(info: ImporterInfo, importRecord: IImport, converterOptions: IConverterOptions = {}) { + constructor(info: ImporterInfo, importRecord: IImport, converterOptions: ConverterOptions = {}) { super(info, importRecord, converterOptions); } diff --git a/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts b/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts index 95461820bf2d..ae8df1859086 100644 --- a/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts +++ b/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts @@ -6,7 +6,7 @@ import { parse } from 'csv-parse/lib/sync'; import { RocketChatFile } from '../../file/server'; import { Importer, ProgressStep } from '../../importer/server'; -import type { IConverterOptions } from '../../importer/server/classes/ImportDataConverter'; +import type { ConverterOptions } from '../../importer/server/classes/ImportDataConverter'; import type { ImporterProgress } from '../../importer/server/classes/ImporterProgress'; import type { ImporterInfo } from '../../importer/server/definitions/ImporterInfo'; import { notifyOnSettingChanged } from '../../lib/server/lib/notifyListener'; @@ -14,7 +14,7 @@ import { notifyOnSettingChanged } from '../../lib/server/lib/notifyListener'; export class SlackUsersImporter extends Importer { private csvParser: (csv: string) => string[]; - constructor(info: ImporterInfo, importRecord: IImport, converterOptions: IConverterOptions = {}) { + constructor(info: ImporterInfo, importRecord: IImport, converterOptions: ConverterOptions = {}) { super(info, importRecord, converterOptions); this.csvParser = parse; diff --git a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts index 6de47e33b2b6..64226f8752a1 100644 --- a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts +++ b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts @@ -1,1171 +1,119 @@ -import type { - IImportUser, - IImportMessage, - IImportMessageReaction, - IImportChannel, - IImportUserRecord, - IImportChannelRecord, - IImportMessageRecord, - IUser, - IUserEmail, - IImportData, - IImportRecordType, - IMessage as IDBMessage, -} from '@rocket.chat/core-typings'; +import type { IImportRecord, IImportUser, IImportMessage, IImportChannel } from '@rocket.chat/core-typings'; import type { Logger } from '@rocket.chat/logger'; -import { ImportData, Rooms, Users, Subscriptions } from '@rocket.chat/models'; -import { Random } from '@rocket.chat/random'; -import { SHA256 } from '@rocket.chat/sha256'; -import { hash as bcryptHash } from 'bcrypt'; -import { Accounts } from 'meteor/accounts-base'; -import { ObjectId } from 'mongodb'; +import { ImportData } from '@rocket.chat/models'; +import { pick } from '@rocket.chat/tools'; -import { callbacks } from '../../../../lib/callbacks'; -import { createDirectMessage } from '../../../../server/methods/createDirectMessage'; -import { saveRoomSettings } from '../../../channel-settings/server/methods/saveRoomSettings'; -import { addUserToDefaultChannels } from '../../../lib/server/functions/addUserToDefaultChannels'; -import { generateUsernameSuggestion } from '../../../lib/server/functions/getUsernameSuggestion'; -import { insertMessage } from '../../../lib/server/functions/insertMessage'; -import { saveUserIdentity } from '../../../lib/server/functions/saveUserIdentity'; -import { setUserActiveStatus } from '../../../lib/server/functions/setUserActiveStatus'; -import { notifyOnSubscriptionChangedByRoomId, notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; -import { createChannelMethod } from '../../../lib/server/methods/createChannel'; -import { createPrivateGroupMethod } from '../../../lib/server/methods/createPrivateGroup'; -import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; import type { IConversionCallbacks } from '../definitions/IConversionCallbacks'; +import { ConverterCache } from './converters/ConverterCache'; +import { type MessageConversionCallbacks, MessageConverter } from './converters/MessageConverter'; +import type { RecordConverter, RecordConverterOptions } from './converters/RecordConverter'; +import { RoomConverter } from './converters/RoomConverter'; +import { UserConverter, type UserConverterOptions } from './converters/UserConverter'; -type IRoom = Record; -type IMessage = Record; -type IUserIdentification = { - _id: string; - username: string | undefined; -}; -type IMentionedUser = { - _id: string; - username: string; - name?: string; -}; -type IMentionedChannel = { - _id: string; - name: string; -}; - -type IMessageReaction = { - name: string; - usernames: Array; -}; - -type IMessageReactions = Record; - -export type IConverterOptions = { - flagEmailsAsVerified?: boolean; - skipExistingUsers?: boolean; - skipNewUsers?: boolean; - skipUserCallbacks?: boolean; - skipDefaultChannels?: boolean; - - quickUserInsertion?: boolean; - enableEmail2fa?: boolean; -}; - -const guessNameFromUsername = (username: string): string => - username - .replace(/\W/g, ' ') - .replace(/\s(.)/g, (u) => u.toUpperCase()) - .replace(/^(.)/, (u) => u.toLowerCase()) - .replace(/^\w/, (u) => u.toUpperCase()); +export type ConverterOptions = UserConverterOptions & Omit; export class ImportDataConverter { - private _userCache: Map; + protected _options: ConverterOptions; - // display name uses a different cache because it's only used on mentions so we don't need to load it every time we load an user - private _userDisplayNameCache: Map; + protected _userConverter: UserConverter; - private _roomCache: Map; + protected _roomConverter: RoomConverter; - private _roomNameCache: Map; + protected _messageConverter: MessageConverter; - private _logger: Logger; + protected _cache = new ConverterCache(); - private _options: IConverterOptions; - - public get options(): IConverterOptions { + public get options(): ConverterOptions { return this._options; } - public aborted = false; - - constructor(options?: IConverterOptions) { - this._options = options || { - flagEmailsAsVerified: false, - skipExistingUsers: false, - skipNewUsers: false, - }; - this._userCache = new Map(); - this._userDisplayNameCache = new Map(); - this._roomCache = new Map(); - this._roomNameCache = new Map(); - } - - setLogger(logger: Logger): void { - this._logger = logger; - } - - addUserToCache(importId: string, _id: string, username: string | undefined): IUserIdentification { - const cache = { - _id, - username, + constructor(logger: Logger, options?: ConverterOptions) { + this._options = { + workInMemory: false, + ...(options || {}), }; - this._userCache.set(importId, cache); - return cache; + this.initializeUserConverter(logger); + this.initializeRoomConverter(logger); + this.initializeMessageConverter(logger); } - addUserDisplayNameToCache(importId: string, name: string): string { - this._userDisplayNameCache.set(importId, name); - return name; - } - - addRoomToCache(importId: string, rid: string): string { - this._roomCache.set(importId, rid); - return rid; - } - - addRoomNameToCache(importId: string, name: string): string { - this._roomNameCache.set(importId, name); - return name; - } - - addUserDataToCache(userData: IImportUser): void { - if (!userData._id) { - return; - } - if (!userData.importIds.length) { - return; - } - - this.addUserToCache(userData.importIds[0], userData._id, userData.username); - } - - protected async addObject(type: IImportRecordType, data: IImportData, options: Record = {}): Promise { - await ImportData.col.insertOne({ - _id: new ObjectId().toHexString(), - data, - dataType: type, - options, - }); - } - - async addUser(data: IImportUser): Promise { - await this.addObject('user', data); - } - - async addChannel(data: IImportChannel): Promise { - await this.addObject('channel', data); - } - - async addMessage(data: IImportMessage, useQuickInsert = false): Promise { - await this.addObject('message', data, { - useQuickInsert: useQuickInsert || undefined, - }); - } - - addUserImportId(updateData: Record, userData: IImportUser): void { - if (userData.importIds?.length) { - updateData.$addToSet = { - importIds: { - $each: userData.importIds, - }, - }; - } - } - - addUserEmails(updateData: Record, userData: IImportUser, existingEmails: Array): void { - if (!userData.emails?.length) { - return; - } - - const verifyEmails = Boolean(this.options.flagEmailsAsVerified); - const newEmailList: Array = []; - - for (const email of userData.emails) { - const verified = verifyEmails || existingEmails.find((ee) => ee.address === email)?.verified || false; - - newEmailList.push({ - address: email, - verified, - }); - } - - updateData.$set.emails = newEmailList; - } - - addUserServices(updateData: Record, userData: IImportUser): void { - if (!userData.services) { - return; - } - - for (const serviceKey in userData.services) { - if (!userData.services[serviceKey]) { - continue; - } - - const service = userData.services[serviceKey]; - - for (const key in service) { - if (!service[key]) { - continue; - } - - updateData.$set[`services.${serviceKey}.${key}`] = service[key]; - } - } - } - - addCustomFields(updateData: Record, userData: IImportUser): void { - if (!userData.customFields) { - return; - } - - const subset = (source: Record, currentPath: string): void => { - for (const key in source) { - if (!source.hasOwnProperty(key)) { - continue; - } - - const keyPath = `${currentPath}.${key}`; - if (typeof source[key] === 'object' && !Array.isArray(source[key])) { - subset(source[key], keyPath); - continue; - } - - updateData.$set = { - ...updateData.$set, - ...{ [keyPath]: source[key] }, - }; - } - }; - - subset(userData.customFields, 'customFields'); - } - - async updateUser(existingUser: IUser, userData: IImportUser): Promise { - const { _id } = existingUser; - if (!_id) { - return; - } - - userData._id = _id; - - if (!userData.roles && !existingUser.roles) { - userData.roles = ['user']; - } - if (!userData.type && !existingUser.type) { - userData.type = 'user'; - } - - const updateData: Record = Object.assign(Object.create(null), { - $set: Object.assign(Object.create(null), { - ...(userData.roles && { roles: userData.roles }), - ...(userData.type && { type: userData.type }), - ...(userData.statusText && { statusText: userData.statusText }), - ...(userData.bio && { bio: userData.bio }), - ...(userData.services?.ldap && { ldap: true }), - ...(userData.avatarUrl && { _pendingAvatarUrl: userData.avatarUrl }), - }), - }); - - this.addCustomFields(updateData, userData); - this.addUserServices(updateData, userData); - this.addUserImportId(updateData, userData); - this.addUserEmails(updateData, userData, existingUser.emails || []); - - if (Object.keys(updateData.$set).length === 0) { - delete updateData.$set; - } - if (Object.keys(updateData).length > 0) { - await Users.updateOne({ _id }, updateData); - } - - if (userData.utcOffset) { - await Users.setUtcOffset(_id, userData.utcOffset); - } - - if (userData.name || userData.username) { - await saveUserIdentity({ _id, name: userData.name, username: userData.username } as Parameters[0]); - } - - if (userData.importIds.length) { - this.addUserToCache(userData.importIds[0], existingUser._id, existingUser.username || userData.username); - } - - // Deleted users are 'inactive' users in Rocket.Chat - if (userData.deleted && existingUser?.active) { - await setUserActiveStatus(_id, false, true); - } else if (userData.deleted === false && existingUser?.active === false) { - await setUserActiveStatus(_id, true); - } - - void notifyOnUserChange({ clientAction: 'updated', id: _id, diff: updateData.$set }); - } - - private async hashPassword(password: string): Promise { - return bcryptHash(SHA256(password), Accounts._bcryptRounds()); - } - - private generateTempPassword(userData: IImportUser): string { - return `${Date.now()}${userData.name || ''}${userData.emails.length ? userData.emails[0].toUpperCase() : ''}`; - } - - private async buildNewUserObject(userData: IImportUser): Promise> { + protected getRecordConverterOptions(): RecordConverterOptions { return { - type: userData.type || 'user', - ...(userData.username && { username: userData.username }), - ...(userData.emails.length && { - emails: userData.emails.map((email) => ({ address: email, verified: !!this._options.flagEmailsAsVerified })), - }), - ...(userData.statusText && { statusText: userData.statusText }), - ...(userData.name && { name: userData.name }), - ...(userData.bio && { bio: userData.bio }), - ...(userData.avatarUrl && { _pendingAvatarUrl: userData.avatarUrl }), - ...(userData.utcOffset !== undefined && { utcOffset: userData.utcOffset }), - ...{ - services: { - // Add a password service if there's a password string, or if there's no service at all - ...((!!userData.password || !userData.services || !Object.keys(userData.services).length) && { - password: { bcrypt: await this.hashPassword(userData.password || this.generateTempPassword(userData)) }, - }), - ...(userData.services || {}), - }, - }, - ...(userData.services?.ldap && { ldap: true }), - ...(userData.importIds?.length && { importIds: userData.importIds }), - ...(!!userData.customFields && { customFields: userData.customFields }), - ...(userData.deleted !== undefined && { active: !userData.deleted }), + ...pick(this._options, 'workInMemory'), + // DbData is deleted by this class directly, so the converters don't need to do it individually + deleteDbData: false, }; } - private async buildUserBatch(usersData: IImportUser[]): Promise { - return Promise.all( - usersData.map(async (userData) => { - const user = await this.buildNewUserObject(userData); - return { - createdAt: new Date(), - _id: Random.id(), - - status: 'offline', - ...user, - roles: userData.roles?.length ? userData.roles : ['user'], - active: !userData.deleted, - services: { - ...user.services, - ...(this._options.enableEmail2fa - ? { - email2fa: { - enabled: true, - changedAt: new Date(), - }, - } - : {}), - }, - } as IUser; - }), - ); - } - - async insertUser(userData: IImportUser): Promise { - const user = await this.buildNewUserObject(userData); - - return Accounts.insertUserDoc( - { - joinDefaultChannels: false, - skipEmailValidation: true, - skipAdminCheck: true, - skipAdminEmail: true, - skipOnCreateUserCallback: this._options.skipUserCallbacks, - skipBeforeCreateUserCallback: this._options.skipUserCallbacks, - skipAfterCreateUserCallback: this._options.skipUserCallbacks, - skipDefaultAvatar: true, - skipAppsEngineEvent: !!process.env.IMPORTER_SKIP_APPS_EVENT, - }, - { - ...user, - ...(userData.roles?.length ? { globalRoles: userData.roles } : {}), - }, - ); - } - - protected async getUsersToImport(): Promise> { - return ImportData.getAllUsers().toArray(); - } - - async findExistingUser(data: IImportUser): Promise { - if (data.emails.length) { - const emailUser = await Users.findOneByEmailAddress(data.emails[0], {}); - - if (emailUser) { - return emailUser; - } - } - - // If we couldn't find one by their email address, try to find an existing user by their username - if (data.username) { - return Users.findOneByUsernameIgnoringCase(data.username, {}); - } - } - - private async insertUserBatch(users: IUser[], { afterBatchFn }: IConversionCallbacks): Promise { - let newIds: string[] | null = null; - - try { - newIds = Object.values((await Users.insertMany(users, { ordered: false })).insertedIds); - if (afterBatchFn) { - await afterBatchFn(newIds.length, 0); - } - } catch (e: any) { - newIds = (e.result?.result?.insertedIds || []) as string[]; - const errorCount = users.length - (e.result?.result?.nInserted || 0); - - if (afterBatchFn) { - await afterBatchFn(Math.min(newIds.length, users.length - errorCount), errorCount); - } - } - - return newIds; - } - - public async convertUsers({ beforeImportFn, afterImportFn, onErrorFn, afterBatchFn }: IConversionCallbacks = {}): Promise { - const users = (await this.getUsersToImport()) as IImportUserRecord[]; - - const insertedIds = new Set(); - const updatedIds = new Set(); - let skippedCount = 0; - let failedCount = 0; - - const batchToInsert = new Set(); - - for await (const record of users) { - const { data, _id } = record; - if (this.aborted) { - break; - } - - try { - if (beforeImportFn && !(await beforeImportFn(record))) { - await this.skipRecord(_id); - skippedCount++; - continue; - } - - const emails = data.emails.filter(Boolean).map((email) => ({ address: email })); - data.importIds = data.importIds.filter((item) => item); - - if (!data.emails.length && !data.username) { - throw new Error('importer-user-missing-email-and-username'); - } - - if (this.options.quickUserInsertion) { - batchToInsert.add(data); - - if (batchToInsert.size >= 50) { - const usersToInsert = await this.buildUserBatch([...batchToInsert]); - batchToInsert.clear(); - - const newIds = await this.insertUserBatch(usersToInsert, { afterBatchFn }); - newIds.forEach((id) => insertedIds.add(id)); - } - - continue; - } - - const existingUser = await this.findExistingUser(data); - if (existingUser && this._options.skipExistingUsers) { - await this.skipRecord(_id); - skippedCount++; - continue; - } - if (!existingUser && this._options.skipNewUsers) { - await this.skipRecord(_id); - skippedCount++; - continue; - } - - if (!data.username && !existingUser?.username) { - data.username = await generateUsernameSuggestion({ - name: data.name, - emails, - }); - } - - const isNewUser = !existingUser; - - if (existingUser) { - await this.updateUser(existingUser, data); - updatedIds.add(existingUser._id); - } else { - if (!data.name && data.username) { - data.name = guessNameFromUsername(data.username); - } - - const userId = await this.insertUser(data); - data._id = userId; - insertedIds.add(userId); - - if (!this._options.skipDefaultChannels) { - const insertedUser = await Users.findOneById(userId, {}); - if (!insertedUser) { - throw new Error(`User not found: ${userId}`); - } - - await addUserToDefaultChannels(insertedUser, true); - } - } - - if (afterImportFn) { - await afterImportFn(record, isNewUser); - } - } catch (e) { - this._logger.error(e); - await this.saveError(_id, e instanceof Error ? e : new Error(String(e))); - failedCount++; - - if (onErrorFn) { - await onErrorFn(); - } - } - } - - if (batchToInsert.size > 0) { - const usersToInsert = await this.buildUserBatch([...batchToInsert]); - const newIds = await this.insertUserBatch(usersToInsert, { afterBatchFn }); - newIds.forEach((id) => insertedIds.add(id)); - } - - await callbacks.run('afterUserImport', { - inserted: [...insertedIds], - updated: [...updatedIds], - skipped: skippedCount, - failed: failedCount, - }); - } - - protected async saveError(importId: string, error: Error): Promise { - this._logger.error(error); - await ImportData.updateOne( - { - _id: importId, - }, - { - $push: { - errors: { - message: error.message, - stack: error.stack, - }, - }, - }, - ); - } - - protected async skipRecord(_id: string): Promise { - await ImportData.updateOne( - { - _id, - }, - { - $set: { - skipped: true, - }, - }, - ); - } - - async convertMessageReactions(importedReactions: Record): Promise { - const reactions: IMessageReactions = {}; - - for await (const name of Object.keys(importedReactions)) { - if (!importedReactions.hasOwnProperty(name)) { - continue; - } - const { users } = importedReactions[name]; - - if (!users.length) { - continue; - } - - const reaction: IMessageReaction = { - name, - usernames: [], - }; - - for await (const importId of users) { - const username = await this.findImportedUsername(importId); - if (username && !reaction.usernames.includes(username)) { - reaction.usernames.push(username); - } - } - - if (reaction.usernames.length) { - reactions[name] = reaction; - } - } - - if (Object.keys(reactions).length > 0) { - return reactions; - } - } - - async convertMessageReplies(replies: Array): Promise> { - const result: Array = []; - for await (const importId of replies) { - const userId = await this.findImportedUserId(importId); - if (userId && !result.includes(userId)) { - result.push(userId); - } - } - return result; - } - - async convertMessageMentions(message: IImportMessage): Promise | undefined> { - const { mentions } = message; - if (!mentions) { - return undefined; - } - - const result: Array = []; - for await (const importId of mentions) { - if (importId === ('all' as 'string') || importId === 'here') { - result.push({ - _id: importId, - username: importId, - }); - continue; - } - - // Loading the name will also store the remaining data on the cache if it's missing, so this won't run two queries - const name = await this.findImportedUserDisplayName(importId); - const data = await this.findImportedUser(importId); - - if (!data) { - this._logger.warn(`Mentioned user not found: ${importId}`); - continue; - } - - if (!data.username) { - this._logger.debug(importId); - throw new Error('importer-message-mentioned-username-not-found'); - } - - message.msg = message.msg.replace(new RegExp(`\@${importId}`, 'gi'), `@${data.username}`); - - result.push({ - _id: data._id, - username: data.username as 'string', - name, - }); - } - return result; - } - - async getMentionedChannelData(importId: string): Promise { - // loading the name will also store the id on the cache if it's missing, so this won't run two queries - const name = await this.findImportedRoomName(importId); - const _id = await this.findImportedRoomId(importId); - - if (name && _id) { - return { - name, - _id, - }; - } - - // If the importId was not found, check if we have a room with that name - const roomName = await getValidRoomName(importId.trim(), undefined, { allowDuplicates: true }); - const room = await Rooms.findOneByNonValidatedName(roomName, { projection: { name: 1 } }); - if (room?.name) { - this.addRoomToCache(importId, room._id); - this.addRoomNameToCache(importId, room.name); - - return { - name: room.name, - _id: room._id, - }; - } - } - - async convertMessageChannels(message: IImportMessage): Promise { - const { channels } = message; - if (!channels) { - return; - } - - const result: Array = []; - for await (const importId of channels) { - const { name, _id } = (await this.getMentionedChannelData(importId)) || {}; - - if (!_id || !name) { - this._logger.warn(`Mentioned room not found: ${importId}`); - continue; - } - - message.msg = message.msg.replace(new RegExp(`\#${importId}`, 'gi'), `#${name}`); - - result.push({ - _id, - name, - }); - } - - return result; - } - - protected async getMessagesToImport(): Promise> { - return ImportData.getAllMessages().toArray(); - } - - async convertMessages({ - beforeImportFn, - afterImportFn, - onErrorFn, - afterImportAllMessagesFn, - }: IConversionCallbacks & { afterImportAllMessagesFn?: (roomIds: string[]) => Promise }): Promise { - const rids: Array = []; - const messages = await this.getMessagesToImport(); - - for await (const record of messages) { - const { data, _id } = record; - if (this.aborted) { - return; - } - - try { - if (beforeImportFn && !(await beforeImportFn(record))) { - await this.skipRecord(_id); - continue; - } - - if (!data.ts || isNaN(data.ts as unknown as number)) { - throw new Error('importer-message-invalid-timestamp'); - } - - const creator = await this.findImportedUser(data.u._id); - if (!creator) { - this._logger.warn(`Imported user not found: ${data.u._id}`); - throw new Error('importer-message-unknown-user'); - } - const rid = await this.findImportedRoomId(data.rid); - if (!rid) { - throw new Error('importer-message-unknown-room'); - } - if (!rids.includes(rid)) { - rids.push(rid); - } - - // Convert the mentions and channels first because these conversions can also modify the msg in the message object - const mentions = data.mentions && (await this.convertMessageMentions(data)); - const channels = data.channels && (await this.convertMessageChannels(data)); - - const msgObj: IMessage = { - rid, - u: { - _id: creator._id, - username: creator.username, - }, - msg: data.msg, - ts: data.ts, - t: data.t || undefined, - groupable: data.groupable, - tmid: data.tmid, - tlm: data.tlm, - tcount: data.tcount, - replies: data.replies && (await this.convertMessageReplies(data.replies)), - editedAt: data.editedAt, - editedBy: data.editedBy && ((await this.findImportedUser(data.editedBy)) || undefined), - mentions, - channels, - _importFile: data._importFile, - url: data.url, - attachments: data.attachments, - bot: data.bot, - emoji: data.emoji, - alias: data.alias, - }; - - if (data._id) { - msgObj._id = data._id; - } - - if (data.reactions) { - msgObj.reactions = await this.convertMessageReactions(data.reactions); - } - - try { - await insertMessage(creator, msgObj as unknown as IDBMessage, rid, true); - } catch (e) { - this._logger.warn(`Failed to import message with timestamp ${String(msgObj.ts)} to room ${rid}`); - this._logger.error(e); - } - - if (afterImportFn) { - await afterImportFn(record, true); - } - } catch (e) { - await this.saveError(_id, e instanceof Error ? e : new Error(String(e))); - if (onErrorFn) { - await onErrorFn(); - } - } - } - - for await (const rid of rids) { - try { - await Rooms.resetLastMessageById(rid, null); - } catch (e) { - this._logger.warn(`Failed to update last message of room ${rid}`); - this._logger.error(e); - } - } - if (afterImportAllMessagesFn) { - await afterImportAllMessagesFn(rids); - } - } - - async updateRoom(room: IRoom, roomData: IImportChannel, startedByUserId: string): Promise { - roomData._id = room._id; - - if ((roomData._id as string).toUpperCase() === 'GENERAL' && roomData.name !== room.name) { - await saveRoomSettings(startedByUserId, 'GENERAL', 'roomName', roomData.name); - } - - await this.updateRoomId(room._id, roomData); - } - - public async findDMForImportedUsers(...users: Array): Promise { - const record = await ImportData.findDMForImportedUsers(...users); - if (record) { - return record.data; - } - } - - async findImportedRoomId(importId: string): Promise { - if (this._roomCache.has(importId)) { - return this._roomCache.get(importId) as string; - } - - const options = { - projection: { - _id: 1, - }, - }; - - const room = await Rooms.findOneByImportId(importId, options); - if (room) { - return this.addRoomToCache(importId, room._id); - } - - return null; - } - - async findImportedRoomName(importId: string): Promise { - if (this._roomNameCache.has(importId)) { - return this._roomNameCache.get(importId) as string; - } + protected getUserConverterOptions(): UserConverterOptions { + return { + flagEmailsAsVerified: false, + skipExistingUsers: false, + skipNewUsers: false, - const options = { - projection: { - _id: 1, - name: 1, - }, + ...pick( + this._options, + 'flagEmailsAsVerified', + 'skipExistingUsers', + 'skipNewUsers', + 'skipUserCallbacks', + 'skipDefaultChannels', + 'quickUserInsertion', + 'enableEmail2fa', + ), }; - - const room = await Rooms.findOneByImportId(importId, options); - if (room) { - if (!this._roomCache.has(importId)) { - this.addRoomToCache(importId, room._id); - } - if (room?.name) { - return this.addRoomNameToCache(importId, room.name); - } - } } - async findImportedUser(importId: string): Promise { - const options = { - projection: { - _id: 1, - username: 1, - }, + protected initializeUserConverter(logger: Logger): void { + const userOptions = { + ...this.getRecordConverterOptions(), + ...this.getUserConverterOptions(), }; - if (importId === 'rocket.cat') { - return { - _id: 'rocket.cat', - username: 'rocket.cat', - }; - } - - if (this._userCache.has(importId)) { - return this._userCache.get(importId) as IUserIdentification; - } - - const user = await Users.findOneByImportId(importId, options); - if (user) { - return this.addUserToCache(importId, user._id, user.username); - } - - return null; + this._userConverter = new UserConverter(userOptions, logger, this._cache); } - async findImportedUserId(_id: string): Promise { - const data = await this.findImportedUser(_id); - return data?._id; - } - - async findImportedUsername(_id: string): Promise { - const data = await this.findImportedUser(_id); - return data?.username; - } - - async findImportedUserDisplayName(importId: string): Promise { - const options = { - projection: { - _id: 1, - name: 1, - username: 1, - }, + protected initializeRoomConverter(logger: Logger): void { + const roomOptions = { + ...this.getRecordConverterOptions(), }; - if (this._userDisplayNameCache.has(importId)) { - return this._userDisplayNameCache.get(importId); - } - - const user = - importId === 'rocket.cat' ? await Users.findOneById('rocket.cat', options) : await Users.findOneByImportId(importId, options); - if (user) { - if (!this._userCache.has(importId)) { - this.addUserToCache(importId, user._id, user.username); - } - - if (!user.name) { - return; - } - - return this.addUserDisplayNameToCache(importId, user.name); - } + this._roomConverter = new RoomConverter(roomOptions, logger, this._cache); } - async updateRoomId(_id: string, roomData: IImportChannel): Promise { - const set = { - ts: roomData.ts, - topic: roomData.topic, - description: roomData.description, + protected initializeMessageConverter(logger: Logger): void { + const messageOptions = { + ...this.getRecordConverterOptions(), }; - const roomUpdate: { $set?: Record; $addToSet?: Record } = {}; - - if (Object.keys(set).length > 0) { - roomUpdate.$set = set; - } - - if (roomData.importIds.length) { - roomUpdate.$addToSet = { - importIds: { - $each: roomData.importIds, - }, - }; - } - - if (roomUpdate.$set || roomUpdate.$addToSet) { - await Rooms.updateOne({ _id: roomData._id }, roomUpdate); - } + this._messageConverter = new MessageConverter(messageOptions, logger, this._cache); } - async getRoomCreatorId(roomData: IImportChannel, startedByUserId: string): Promise { - if (roomData.u) { - const creatorId = await this.findImportedUserId(roomData.u._id); - if (creatorId) { - return creatorId; - } - - if (roomData.t !== 'd') { - return startedByUserId; - } - - throw new Error('importer-channel-invalid-creator'); - } - - if (roomData.t === 'd') { - for await (const member of roomData.users) { - const userId = await this.findImportedUserId(member); - if (userId) { - return userId; - } - } - } - - throw new Error('importer-channel-invalid-creator'); - } - - async insertRoom(roomData: IImportChannel, startedByUserId: string): Promise { - // Find the rocketchatId of the user who created this channel - const creatorId = await this.getRoomCreatorId(roomData, startedByUserId); - const members = await this.convertImportedIdsToUsernames(roomData.users, roomData.t !== 'd' ? creatorId : undefined); - - if (roomData.t === 'd') { - if (members.length < roomData.users.length) { - this._logger.warn(`One or more imported users not found: ${roomData.users}`); - throw new Error('importer-channel-missing-users'); - } - } - - // Create the channel - try { - let roomInfo; - if (roomData.t === 'd') { - roomInfo = await createDirectMessage(members, startedByUserId, true); - } else { - if (!roomData.name) { - return; - } - if (roomData.t === 'p') { - const user = await Users.findOneById(creatorId); - if (!user) { - throw new Error('importer-channel-invalid-creator'); - } - roomInfo = await createPrivateGroupMethod(user, roomData.name, members, false, {}, {}); - } else { - roomInfo = await createChannelMethod(creatorId, roomData.name, members, false, {}, {}); - } - } - - roomData._id = roomInfo.rid; - } catch (e) { - this._logger.warn({ msg: 'Failed to create new room', name: roomData.name, members }); - this._logger.error(e); - throw e; - } - - await this.updateRoomId(roomData._id as 'string', roomData); + async addUser(data: IImportUser): Promise { + return this._userConverter.addObject(data); } - async convertImportedIdsToUsernames(importedIds: Array, idToRemove: string | undefined = undefined): Promise> { - return ( - await Promise.all( - importedIds.map(async (user) => { - if (user === 'rocket.cat') { - return user; - } - - if (this._userCache.has(user)) { - const cache = this._userCache.get(user); - if (cache) { - return cache.username; - } - } - - const obj = await Users.findOneByImportId(user, { projection: { _id: 1, username: 1 } }); - if (obj) { - this.addUserToCache(user, obj._id, obj.username); - - if (idToRemove && obj._id === idToRemove) { - return false; - } - - return obj.username; - } - - return false; - }), - ) - ).filter((user) => user) as string[]; + async addChannel(data: IImportChannel): Promise { + return this._roomConverter.addObject(data); } - async findExistingRoom(data: IImportChannel): Promise { - if (data._id && data._id.toUpperCase() === 'GENERAL') { - const room = await Rooms.findOneById('GENERAL', {}); - // Prevent the importer from trying to create a new general - if (!room) { - throw new Error('importer-channel-general-not-found'); - } - - return room; - } - - if (data.t === 'd') { - const users = await this.convertImportedIdsToUsernames(data.users); - if (users.length !== data.users.length) { - throw new Error('importer-channel-missing-users'); - } - - return Rooms.findDirectRoomContainingAllUsernames(users, {}); - } - - if (!data.name) { - return null; - } - - const roomName = await getValidRoomName(data.name.trim(), undefined, { allowDuplicates: true }); - return Rooms.findOneByNonValidatedName(roomName, {}); + async addMessage(data: IImportMessage, useQuickInsert = false): Promise { + return this._messageConverter.addObject(data, { + useQuickInsert: useQuickInsert || undefined, + }); } - protected async getChannelsToImport(): Promise> { - return ImportData.getAllChannels().toArray(); + async convertUsers(callbacks: IConversionCallbacks): Promise { + return this._userConverter.convertData(callbacks); } - async convertChannels(startedByUserId: string, { beforeImportFn, afterImportFn, onErrorFn }: IConversionCallbacks = {}): Promise { - const channels = await this.getChannelsToImport(); - for await (const record of channels) { - const { data, _id } = record; - if (this.aborted) { - return; - } - - try { - if (beforeImportFn && !(await beforeImportFn(record))) { - await this.skipRecord(_id); - continue; - } - - if (!data.name && data.t !== 'd') { - throw new Error('importer-channel-missing-name'); - } - - data.importIds = data.importIds.filter((item) => item); - data.users = [...new Set(data.users)]; - - if (!data.importIds.length) { - throw new Error('importer-channel-missing-import-id'); - } - - const existingRoom = await this.findExistingRoom(data); - - if (existingRoom) { - await this.updateRoom(existingRoom, data, startedByUserId); - } else { - await this.insertRoom(data, startedByUserId); - } - - if (data.archived && data._id) { - await this.archiveRoomById(data._id); - } - - if (afterImportFn) { - await afterImportFn(record, !existingRoom); - } - } catch (e) { - await this.saveError(_id, e instanceof Error ? e : new Error(String(e))); - if (onErrorFn) { - await onErrorFn(); - } - } - } + async convertChannels(startedByUserId: string, callbacks: IConversionCallbacks): Promise { + return this._roomConverter.convertChannels(startedByUserId, callbacks); } - async archiveRoomById(rid: string) { - const responses = await Promise.all([Rooms.archiveById(rid), Subscriptions.archiveByRoomId(rid)]); - - if (responses[1]?.modifiedCount) { - void notifyOnSubscriptionChangedByRoomId(rid); - } + async convertMessages(callbacks: MessageConversionCallbacks): Promise { + return this._messageConverter.convertData(callbacks); } async convertData(startedByUserId: string, callbacks: IConversionCallbacks = {}): Promise { @@ -1178,16 +126,34 @@ export class ImportDataConverter { }); } + protected getAllConverters(): RecordConverter[] { + return [this._userConverter, this._roomConverter, this._messageConverter]; + } + public async clearImportData(): Promise { - // Using raw collection since its faster - await ImportData.col.deleteMany({}); + if (!this._options.workInMemory) { + // Using raw collection since its faster + await ImportData.col.deleteMany({}); + } + + await Promise.all(this.getAllConverters().map((converter) => converter.clearImportData())); } async clearSuccessfullyImportedData(): Promise { - await ImportData.col.deleteMany({ - errors: { - $exists: false, - }, + if (!this._options.workInMemory) { + await ImportData.col.deleteMany({ + errors: { + $exists: false, + }, + }); + } + + await Promise.all(this.getAllConverters().map((converter) => converter.clearSuccessfullyImportedData())); + } + + public abort(): void { + this.getAllConverters().forEach((converter) => { + converter.aborted = true; }); } } diff --git a/apps/meteor/app/importer/server/classes/Importer.ts b/apps/meteor/app/importer/server/classes/Importer.ts index 846f9ef4b4f5..d89cb5f979f3 100644 --- a/apps/meteor/app/importer/server/classes/Importer.ts +++ b/apps/meteor/app/importer/server/classes/Importer.ts @@ -12,7 +12,7 @@ import { t } from '../../../utils/lib/i18n'; import { ProgressStep, ImportPreparingStartedStates } from '../../lib/ImporterProgressStep'; import type { ImporterInfo } from '../definitions/ImporterInfo'; import { ImportDataConverter } from './ImportDataConverter'; -import type { IConverterOptions } from './ImportDataConverter'; +import type { ConverterOptions } from './ImportDataConverter'; import { ImporterProgress } from './ImporterProgress'; import { ImporterWebsocket } from './ImporterWebsocket'; @@ -46,17 +46,15 @@ export class Importer { public progress: ImporterProgress; - constructor(info: ImporterInfo, importRecord: IImport, converterOptions: IConverterOptions = {}) { + constructor(info: ImporterInfo, importRecord: IImport, converterOptions: ConverterOptions = {}) { if (!info.key || !info.importer) { throw new Error('Information passed in must be a valid ImporterInfo instance.'); } - this.converter = new ImportDataConverter(converterOptions); - this.info = info; - this.logger = new Logger(`${this.info.name} Importer`); - this.converter.setLogger(this.logger); + + this.converter = new ImportDataConverter(this.logger, converterOptions); this.importRecord = importRecord; this.progress = new ImporterProgress(this.info.key, this.info.name); @@ -120,7 +118,7 @@ export class Importer { const beforeImportFn = async ({ data, dataType: type }: IImportRecord) => { if (this.importRecord.valid === false) { - this.converter.aborted = true; + this.converter.abort(); throw new Error('The import operation is no longer valid.'); } @@ -167,7 +165,7 @@ export class Importer { await this.addCountCompleted(1); if (this.importRecord.valid === false) { - this.converter.aborted = true; + this.converter.abort(); throw new Error('The import operation is no longer valid.'); } }; @@ -184,7 +182,7 @@ export class Importer { } if (this.importRecord.valid === false) { - this.converter.aborted = true; + this.converter.abort(); throw new Error('The import operation is no longer valid.'); } }; diff --git a/apps/meteor/app/importer/server/classes/VirtualDataConverter.ts b/apps/meteor/app/importer/server/classes/VirtualDataConverter.ts deleted file mode 100644 index ef850226be5c..000000000000 --- a/apps/meteor/app/importer/server/classes/VirtualDataConverter.ts +++ /dev/null @@ -1,169 +0,0 @@ -import type { - IImportUser, - IImportUserRecord, - IImportChannelRecord, - IImportMessageRecord, - IImportRecord, - IImportRecordType, - IImportData, - IImportChannel, -} from '@rocket.chat/core-typings'; -import { Random } from '@rocket.chat/random'; - -import { ImportDataConverter } from './ImportDataConverter'; -import type { IConverterOptions } from './ImportDataConverter'; - -export class VirtualDataConverter extends ImportDataConverter { - protected _userRecords: Array; - - protected _channelRecords: Array; - - protected _messageRecords: Array; - - protected useVirtual: boolean; - - constructor(virtual = true, options?: IConverterOptions) { - super(options); - - this.useVirtual = virtual; - if (virtual) { - this.clearVirtualData(); - } - } - - public async clearImportData(): Promise { - if (!this.useVirtual) { - return super.clearImportData(); - } - - this.clearVirtualData(); - } - - public async clearSuccessfullyImportedData(): Promise { - if (!this.useVirtual) { - return super.clearSuccessfullyImportedData(); - } - - this.clearVirtualData(); - } - - public async findDMForImportedUsers(...users: Array): Promise { - if (!this.useVirtual) { - return super.findDMForImportedUsers(...users); - } - - // The original method is only used by the hipchat importer so we probably don't need to implement this on the virtual converter. - return undefined; - } - - public addUserSync(data: IImportUser, options?: Record): void { - return this.addObjectSync('user', data, options); - } - - protected async addObject(type: IImportRecordType, data: IImportData, options: Record = {}): Promise { - if (!this.useVirtual) { - return super.addObject(type, data, options); - } - - this.addObjectSync(type, data, options); - } - - protected addObjectSync(type: IImportRecordType, data: IImportData, options: Record = {}): void { - if (!this.useVirtual) { - throw new Error('Sync operations can only be used on virtual converter'); - } - - const list = this.getObjectList(type); - - list.push({ - _id: Random.id(), - data, - dataType: type, - options, - }); - } - - protected async getUsersToImport(): Promise> { - if (!this.useVirtual) { - return super.getUsersToImport(); - } - - return this._userRecords; - } - - protected async saveError(importId: string, error: Error): Promise { - if (!this.useVirtual) { - return super.saveError(importId, error); - } - - const record = this.getVirtualRecordById(importId); - - if (!record) { - return; - } - - if (!record.errors) { - record.errors = []; - } - - record.errors.push({ - message: error.message, - stack: error.stack, - }); - } - - protected async skipRecord(_id: string): Promise { - if (!this.useVirtual) { - return super.skipRecord(_id); - } - - const record = this.getVirtualRecordById(_id); - - if (record) { - record.skipped = true; - } - } - - protected async getMessagesToImport(): Promise { - if (!this.useVirtual) { - return super.getMessagesToImport(); - } - - return this._messageRecords; - } - - protected async getChannelsToImport(): Promise { - if (!this.useVirtual) { - return super.getChannelsToImport(); - } - - return this._channelRecords; - } - - private clearVirtualData(): void { - this._userRecords = []; - this._channelRecords = []; - this._messageRecords = []; - } - - private getObjectList(type: IImportRecordType): Array { - switch (type) { - case 'user': - return this._userRecords; - case 'channel': - return this._channelRecords; - case 'message': - return this._messageRecords; - } - } - - private getVirtualRecordById(id: string): IImportRecord | undefined { - for (const store of [this._userRecords, this._channelRecords, this._messageRecords]) { - for (const record of store) { - if (record._id === id) { - return record; - } - } - } - } -} diff --git a/apps/meteor/app/importer/server/classes/converters/ConverterCache.ts b/apps/meteor/app/importer/server/classes/converters/ConverterCache.ts new file mode 100644 index 000000000000..cefbf9cc7dbb --- /dev/null +++ b/apps/meteor/app/importer/server/classes/converters/ConverterCache.ts @@ -0,0 +1,198 @@ +import type { IImportUser } from '@rocket.chat/core-typings'; +import { Rooms, Users } from '@rocket.chat/models'; + +export type UserIdentification = { + _id: string; + username: string | undefined; +}; + +export type MentionedChannel = { + _id: string; + name: string; +}; + +export class ConverterCache { + private _userCache = new Map(); + + // display name uses a different cache because it's only used on mentions so we don't need to load it every time we load an user + private _userDisplayNameCache = new Map(); + + private _roomCache = new Map(); + + private _roomNameCache = new Map(); + + addUser(importId: string, _id: string, username: string | undefined): UserIdentification { + const cache = { + _id, + username, + }; + + this._userCache.set(importId, cache); + return cache; + } + + addUserDisplayName(importId: string, name: string): string { + this._userDisplayNameCache.set(importId, name); + return name; + } + + addRoom(importId: string, rid: string): string { + this._roomCache.set(importId, rid); + return rid; + } + + addRoomName(importId: string, name: string): string { + this._roomNameCache.set(importId, name); + return name; + } + + addUserData(userData: IImportUser): void { + if (!userData._id) { + return; + } + if (!userData.importIds.length) { + return; + } + + this.addUser(userData.importIds[0], userData._id, userData.username); + } + + async findImportedRoomId(importId: string): Promise { + if (this._roomCache.has(importId)) { + return this._roomCache.get(importId) as string; + } + + const options = { + projection: { + _id: 1, + }, + }; + + const room = await Rooms.findOneByImportId(importId, options); + if (room) { + return this.addRoom(importId, room._id); + } + + return null; + } + + async findImportedRoomName(importId: string): Promise { + if (this._roomNameCache.has(importId)) { + return this._roomNameCache.get(importId) as string; + } + + const options = { + projection: { + _id: 1, + name: 1, + }, + }; + + const room = await Rooms.findOneByImportId(importId, options); + if (room) { + if (!this._roomCache.has(importId)) { + this.addRoom(importId, room._id); + } + if (room?.name) { + return this.addRoomName(importId, room.name); + } + } + } + + async findImportedUser(importId: string): Promise { + if (importId === 'rocket.cat') { + return { + _id: 'rocket.cat', + username: 'rocket.cat', + }; + } + + const options = { + projection: { + _id: 1, + username: 1, + }, + }; + + if (this._userCache.has(importId)) { + return this._userCache.get(importId) as UserIdentification; + } + + const user = await Users.findOneByImportId(importId, options); + if (user) { + return this.addUser(importId, user._id, user.username); + } + + return null; + } + + async findImportedUserId(_id: string): Promise { + const data = await this.findImportedUser(_id); + return data?._id; + } + + async findImportedUsername(_id: string): Promise { + const data = await this.findImportedUser(_id); + return data?.username; + } + + async findImportedUserDisplayName(importId: string): Promise { + const options = { + projection: { + _id: 1, + name: 1, + username: 1, + }, + }; + + if (this._userDisplayNameCache.has(importId)) { + return this._userDisplayNameCache.get(importId); + } + + const user = + importId === 'rocket.cat' ? await Users.findOneById('rocket.cat', options) : await Users.findOneByImportId(importId, options); + if (user) { + if (!this._userCache.has(importId)) { + this.addUser(importId, user._id, user.username); + } + + if (!user.name) { + return; + } + + return this.addUserDisplayName(importId, user.name); + } + } + + async convertImportedIdsToUsernames(importedIds: Array, idToRemove: string | undefined = undefined): Promise> { + return ( + await Promise.all( + importedIds.map(async (user) => { + if (user === 'rocket.cat') { + return user; + } + + if (this._userCache.has(user)) { + const cache = this._userCache.get(user); + if (cache) { + return cache.username; + } + } + + const obj = await Users.findOneByImportId(user, { projection: { _id: 1, username: 1 } }); + if (obj) { + this.addUser(user, obj._id, obj.username); + + if (idToRemove && obj._id === idToRemove) { + return false; + } + + return obj.username; + } + + return false; + }), + ) + ).filter((user) => user) as string[]; + } +} diff --git a/apps/meteor/app/importer/server/classes/converters/MessageConverter.ts b/apps/meteor/app/importer/server/classes/converters/MessageConverter.ts new file mode 100644 index 000000000000..b4540ed6182f --- /dev/null +++ b/apps/meteor/app/importer/server/classes/converters/MessageConverter.ts @@ -0,0 +1,263 @@ +import type { IImportMessageRecord, IMessage as IDBMessage, IImportMessage, IImportMessageReaction } from '@rocket.chat/core-typings'; +import { Rooms } from '@rocket.chat/models'; +import limax from 'limax'; + +import { insertMessage } from '../../../../lib/server/functions/insertMessage'; +import type { IConversionCallbacks } from '../../definitions/IConversionCallbacks'; +import type { UserIdentification, MentionedChannel } from './ConverterCache'; +import { RecordConverter } from './RecordConverter'; + +export type MessageConversionCallbacks = IConversionCallbacks & { afterImportAllMessagesFn?: (roomIds: string[]) => Promise }; + +type MessageObject = Record; + +type MentionedUser = { + _id: string; + username: string; + name?: string; +}; + +type IMessageReaction = { + name: string; + usernames: string[]; +}; + +type IMessageReactions = Record; + +export class MessageConverter extends RecordConverter { + private rids: string[] = []; + + async convertData({ afterImportAllMessagesFn, ...callbacks }: MessageConversionCallbacks = {}): Promise { + this.rids = []; + await super.convertData(callbacks); + + await this.resetLastMessages(); + if (afterImportAllMessagesFn) { + await afterImportAllMessagesFn(this.rids); + } + } + + protected async resetLastMessages(): Promise { + for await (const rid of this.rids) { + try { + await Rooms.resetLastMessageById(rid, null); + } catch (e) { + this._logger.warn(`Failed to update last message of room ${rid}`); + this._logger.error(e); + } + } + } + + protected async insertMessage(data: IImportMessage): Promise { + if (!data.ts || isNaN(data.ts as unknown as number)) { + throw new Error('importer-message-invalid-timestamp'); + } + + const creator = await this._cache.findImportedUser(data.u._id); + if (!creator) { + this._logger.warn(`Imported user not found: ${data.u._id}`); + throw new Error('importer-message-unknown-user'); + } + const rid = await this._cache.findImportedRoomId(data.rid); + if (!rid) { + throw new Error('importer-message-unknown-room'); + } + if (!this.rids.includes(rid)) { + this.rids.push(rid); + } + + const msgObj = await this.buildMessageObject(data, rid, creator); + + try { + await insertMessage(creator, msgObj as unknown as IDBMessage, rid, true); + } catch (e) { + this._logger.warn(`Failed to import message with timestamp ${String(msgObj.ts)} to room ${rid}`); + this._logger.error(e); + } + } + + protected async convertRecord(record: IImportMessageRecord): Promise { + await this.insertMessage(record.data); + return true; + } + + protected async buildMessageObject(data: IImportMessage, rid: string, creator: UserIdentification): Promise { + // Convert the mentions and channels first because these conversions can also modify the msg in the message object + const mentions = data.mentions && (await this.convertMessageMentions(data)); + const channels = data.channels && (await this.convertMessageChannels(data)); + + return { + rid, + u: { + _id: creator._id, + username: creator.username, + }, + msg: data.msg, + ts: data.ts, + t: data.t || undefined, + groupable: data.groupable, + tmid: data.tmid, + tlm: data.tlm, + tcount: data.tcount, + replies: data.replies && (await this.convertMessageReplies(data.replies)), + editedAt: data.editedAt, + editedBy: data.editedBy && ((await this._cache.findImportedUser(data.editedBy)) || undefined), + mentions, + channels, + _importFile: data._importFile, + url: data.url, + attachments: data.attachments, + bot: data.bot, + emoji: data.emoji, + alias: data.alias, + ...(data._id ? { _id: data._id } : {}), + ...(data.reactions ? { reactions: await this.convertMessageReactions(data.reactions) } : {}), + }; + } + + protected async convertMessageChannels(message: IImportMessage): Promise { + const { channels } = message; + if (!channels) { + return; + } + + const result: MentionedChannel[] = []; + for await (const importId of channels) { + const { name, _id } = (await this.getMentionedChannelData(importId)) || {}; + + if (!_id || !name) { + this._logger.warn(`Mentioned room not found: ${importId}`); + continue; + } + + message.msg = message.msg.replace(new RegExp(`\#${importId}`, 'gi'), `#${name}`); + + result.push({ + _id, + name, + }); + } + + return result; + } + + protected async convertMessageMentions(message: IImportMessage): Promise { + const { mentions } = message; + if (!mentions) { + return undefined; + } + + const result: MentionedUser[] = []; + for await (const importId of mentions) { + if (importId === ('all' as 'string') || importId === 'here') { + result.push({ + _id: importId, + username: importId, + }); + continue; + } + + // Loading the name will also store the remaining data on the cache if it's missing, so this won't run two queries + const name = await this._cache.findImportedUserDisplayName(importId); + const data = await this._cache.findImportedUser(importId); + + if (!data) { + this._logger.warn(`Mentioned user not found: ${importId}`); + continue; + } + + if (!data.username) { + this._logger.debug(importId); + throw new Error('importer-message-mentioned-username-not-found'); + } + + message.msg = message.msg.replace(new RegExp(`\@${importId}`, 'gi'), `@${data.username}`); + + result.push({ + _id: data._id, + username: data.username as 'string', + name, + }); + } + return result; + } + + protected async convertMessageReactions( + importedReactions: Record, + ): Promise { + const reactions: IMessageReactions = {}; + + for await (const name of Object.keys(importedReactions)) { + if (!importedReactions.hasOwnProperty(name)) { + continue; + } + const { users } = importedReactions[name]; + + if (!users.length) { + continue; + } + + const reaction: IMessageReaction = { + name, + usernames: [], + }; + + for await (const importId of users) { + const username = await this._cache.findImportedUsername(importId); + if (username && !reaction.usernames.includes(username)) { + reaction.usernames.push(username); + } + } + + if (reaction.usernames.length) { + reactions[name] = reaction; + } + } + + if (Object.keys(reactions).length > 0) { + return reactions; + } + } + + protected async convertMessageReplies(replies: string[]): Promise { + const result: string[] = []; + for await (const importId of replies) { + const userId = await this._cache.findImportedUserId(importId); + if (userId && !result.includes(userId)) { + result.push(userId); + } + } + return result; + } + + protected async getMentionedChannelData(importId: string): Promise { + // loading the name will also store the id on the cache if it's missing, so this won't run two queries + const name = await this._cache.findImportedRoomName(importId); + const _id = await this._cache.findImportedRoomId(importId); + + if (name && _id) { + return { + name, + _id, + }; + } + + // If the importId was not found, check if we have a room with that name + const roomName = limax(importId.trim(), { maintainCase: true }); + + const room = await Rooms.findOneByNonValidatedName(roomName, { projection: { name: 1 } }); + if (room?.name) { + this._cache.addRoom(importId, room._id); + this._cache.addRoomName(importId, room.name); + + return { + name: room.name, + _id: room._id, + }; + } + } + + protected getDataType(): 'message' { + return 'message'; + } +} diff --git a/apps/meteor/app/importer/server/classes/converters/RecordConverter.ts b/apps/meteor/app/importer/server/classes/converters/RecordConverter.ts new file mode 100644 index 000000000000..d0a6d60fa723 --- /dev/null +++ b/apps/meteor/app/importer/server/classes/converters/RecordConverter.ts @@ -0,0 +1,237 @@ +import type { IImportRecord } from '@rocket.chat/core-typings'; +import { Logger } from '@rocket.chat/logger'; +import { ImportData } from '@rocket.chat/models'; +import { Random } from '@rocket.chat/random'; +import { type FindCursor, ObjectId } from 'mongodb'; + +import type { IConversionCallbacks } from '../../definitions/IConversionCallbacks'; +import { ConverterCache } from './ConverterCache'; + +export type RecordConverterOptions = { + workInMemory?: boolean; + deleteDbData?: boolean; +}; + +export class RecordConverter { + protected _logger: Logger; + + protected _cache: ConverterCache; + + protected _converterOptions: RecordConverterOptions; + + protected _options: Omit; + + protected _records: R[]; + + protected skippedCount = 0; + + protected failedCount = 0; + + public aborted = false; + + constructor(options?: T, logger?: Logger, cache?: ConverterCache) { + const { workInMemory = false, deleteDbData = false, ...customOptions } = options || ({} as T); + this._converterOptions = { + workInMemory, + deleteDbData, + }; + this._options = customOptions; + + this._logger = logger || new Logger(`Data Importer - ${this.constructor.name}`); + this._cache = cache || new ConverterCache(); + this._records = []; + } + + private skipMemoryRecord(_id: string): void { + const record = this.getMemoryRecordById(_id); + if (!record) { + return; + } + + record.skipped = true; + } + + private async skipDatabaseRecord(_id: string): Promise { + await ImportData.updateOne( + { + _id, + }, + { + $set: { + skipped: true, + }, + }, + ); + } + + protected async skipRecord(_id: string): Promise { + this.skippedCount++; + this.skipMemoryRecord(_id); + if (!this._converterOptions.workInMemory) { + return this.skipDatabaseRecord(_id); + } + } + + private saveErrorToMemory(importId: string, error: Error): void { + const record = this.getMemoryRecordById(importId); + + if (!record) { + return; + } + + if (!record.errors) { + record.errors = []; + } + + record.errors.push({ + message: error.message, + stack: error.stack, + }); + } + + private async saveErrorToDatabase(importId: string, error: Error): Promise { + await ImportData.updateOne( + { + _id: importId, + }, + { + $push: { + errors: { + message: error.message, + stack: error.stack, + }, + }, + }, + ); + } + + protected async saveError(importId: string, error: Error): Promise { + this._logger.error(error); + this.saveErrorToMemory(importId, error); + + if (!this._converterOptions.workInMemory) { + return this.saveErrorToDatabase(importId, error); + } + } + + public async clearImportData(): Promise { + this._records = []; + + // On regular import operations this data will be deleted by the importer class with one single operation for all dataTypes (aka with no filter) + if (!this._converterOptions.workInMemory && this._converterOptions.deleteDbData) { + await ImportData.col.deleteMany({ dataType: this.getDataType() }); + } + } + + public async clearSuccessfullyImportedData(): Promise { + this._records = this._records.filter((record) => !record.errors?.length); + + // On regular import operations this data will be deleted by the importer class with one single operation for all dataTypes (aka with no filter) + if (!this._converterOptions.workInMemory && this._converterOptions.deleteDbData) { + await ImportData.col.deleteMany({ dataType: this.getDataType(), error: { $exists: false } }); + } + } + + private getMemoryRecordById(id: string): R | undefined { + for (const record of this._records) { + if (record._id === id) { + return record; + } + } + + return undefined; + } + + protected getDataType(): R['dataType'] { + throw new Error('Unspecified type'); + } + + protected async addObjectToDatabase(data: R['data'], options: R['options'] = {}): Promise { + await ImportData.col.insertOne({ + _id: new ObjectId().toHexString(), + data, + dataType: this.getDataType(), + options, + }); + } + + public addObjectToMemory(data: R['data'], options: R['options'] = {}): void { + this._records.push({ + _id: Random.id(), + data, + dataType: this.getDataType(), + options, + } as R); + } + + public async addObject(data: R['data'], options: R['options'] = {}): Promise { + if (this._converterOptions.workInMemory) { + return this.addObjectToMemory(data, options); + } + + return this.addObjectToDatabase(data, options); + } + + protected getDatabaseDataToImport(): Promise { + return (ImportData.find({ dataType: this.getDataType() }) as FindCursor).toArray(); + } + + protected async getDataToImport(): Promise { + if (this._converterOptions.workInMemory) { + return this._records; + } + + const dbRecords = await this.getDatabaseDataToImport(); + if (this._records.length) { + return [...this._records, ...dbRecords]; + } + + return dbRecords; + } + + protected async iterateRecords({ + beforeImportFn, + afterImportFn, + onErrorFn, + processRecord, + }: IConversionCallbacks & { processRecord?: (record: R) => Promise } = {}): Promise { + const records = await this.getDataToImport(); + + this.skippedCount = 0; + this.failedCount = 0; + + for await (const record of records) { + const { _id } = record; + if (this.aborted) { + return; + } + + try { + if (beforeImportFn && !(await beforeImportFn(record))) { + await this.skipRecord(_id); + continue; + } + + const isNew = await (processRecord || this.convertRecord).call(this, record); + + if (typeof isNew === 'boolean' && afterImportFn) { + await afterImportFn(record, isNew); + } + } catch (e) { + this.failedCount++; + await this.saveError(_id, e instanceof Error ? e : new Error(String(e))); + if (onErrorFn) { + await onErrorFn(); + } + } + } + } + + async convertData(callbacks: IConversionCallbacks = {}): Promise { + return this.iterateRecords(callbacks); + } + + protected async convertRecord(_record: R): Promise { + return undefined; + } +} diff --git a/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts b/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts new file mode 100644 index 000000000000..f57fa1a7cb88 --- /dev/null +++ b/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts @@ -0,0 +1,198 @@ +import type { IImportChannel, IImportChannelRecord, IRoom } from '@rocket.chat/core-typings'; +import { Subscriptions, Rooms, Users } from '@rocket.chat/models'; +import limax from 'limax'; + +import { createDirectMessage } from '../../../../../server/methods/createDirectMessage'; +import { saveRoomSettings } from '../../../../channel-settings/server/methods/saveRoomSettings'; +import { notifyOnSubscriptionChangedByRoomId } from '../../../../lib/server/lib/notifyListener'; +import { createChannelMethod } from '../../../../lib/server/methods/createChannel'; +import { createPrivateGroupMethod } from '../../../../lib/server/methods/createPrivateGroup'; +import type { IConversionCallbacks } from '../../definitions/IConversionCallbacks'; +import { RecordConverter } from './RecordConverter'; + +export class RoomConverter extends RecordConverter { + public startedByUserId: string; + + async convertChannels(startedByUserId: string, callbacks: IConversionCallbacks = {}): Promise { + this.startedByUserId = startedByUserId; + + return this.convertData(callbacks); + } + + protected async convertRecord(record: IImportChannelRecord): Promise { + const { data } = record; + + if (!data.name && data.t !== 'd') { + throw new Error('importer-channel-missing-name'); + } + + data.importIds = data.importIds.filter((item) => item); + data.users = [...new Set(data.users)]; + + if (!data.importIds.length) { + throw new Error('importer-channel-missing-import-id'); + } + + const existingRoom = await this.findExistingRoom(data); + await this.insertOrUpdateRoom(existingRoom, data, this.startedByUserId); + + return !existingRoom; + } + + async insertOrUpdateRoom(existingRoom: IRoom | null, data: IImportChannel, startedByUserId: string): Promise { + if (existingRoom) { + await this.updateRoom(existingRoom, data, startedByUserId); + } else { + await this.insertRoom(data, startedByUserId); + } + + if (data.archived && data._id) { + await this.archiveRoomById(data._id); + } + } + + async findExistingRoom(data: IImportChannel): Promise { + if (data._id && data._id.toUpperCase() === 'GENERAL') { + const room = await Rooms.findOneById('GENERAL', {}); + // Prevent the importer from trying to create a new general + if (!room) { + throw new Error('importer-channel-general-not-found'); + } + + return room; + } + + if (data.t === 'd') { + const users = await this._cache.convertImportedIdsToUsernames(data.users); + if (users.length !== data.users.length) { + throw new Error('importer-channel-missing-users'); + } + + return Rooms.findDirectRoomContainingAllUsernames(users, {}); + } + + if (!data.name) { + return null; + } + + // Imported room names always allow special chars + const roomName = limax(data.name.trim(), { maintainCase: true }); + return Rooms.findOneByNonValidatedName(roomName, {}); + } + + async updateRoom(room: IRoom, roomData: IImportChannel, startedByUserId: string): Promise { + roomData._id = room._id; + + if ((roomData._id as string).toUpperCase() === 'GENERAL' && roomData.name !== room.name) { + await saveRoomSettings(startedByUserId, 'GENERAL', 'roomName', roomData.name); + } + + await this.updateRoomId(room._id, roomData); + } + + async insertRoom(roomData: IImportChannel, startedByUserId: string): Promise { + // Find the rocketchatId of the user who created this channel + const creatorId = await this.getRoomCreatorId(roomData, startedByUserId); + const members = await this._cache.convertImportedIdsToUsernames(roomData.users, roomData.t !== 'd' ? creatorId : undefined); + + if (roomData.t === 'd') { + if (members.length < roomData.users.length) { + this._logger.warn(`One or more imported users not found: ${roomData.users}`); + throw new Error('importer-channel-missing-users'); + } + } + + // Create the channel + try { + let roomInfo; + if (roomData.t === 'd') { + roomInfo = await createDirectMessage(members, startedByUserId, true); + } else { + if (!roomData.name) { + return; + } + if (roomData.t === 'p') { + const user = await Users.findOneById(creatorId); + if (!user) { + throw new Error('importer-channel-invalid-creator'); + } + roomInfo = await createPrivateGroupMethod(user, roomData.name, members, false, {}, {}); + } else { + roomInfo = await createChannelMethod(creatorId, roomData.name, members, false, {}, {}); + } + } + + roomData._id = roomInfo.rid; + } catch (e) { + this._logger.warn({ msg: 'Failed to create new room', name: roomData.name, members }); + this._logger.error(e); + throw e; + } + + await this.updateRoomId(roomData._id as 'string', roomData); + } + + async archiveRoomById(rid: string) { + const responses = await Promise.all([Rooms.archiveById(rid), Subscriptions.archiveByRoomId(rid)]); + + if (responses[1]?.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } + } + + async updateRoomId(_id: string, roomData: IImportChannel): Promise { + const set = { + ts: roomData.ts, + topic: roomData.topic, + description: roomData.description, + }; + + const roomUpdate: { $set?: Record; $addToSet?: Record } = {}; + + if (Object.keys(set).length > 0) { + roomUpdate.$set = set; + } + + if (roomData.importIds.length) { + roomUpdate.$addToSet = { + importIds: { + $each: roomData.importIds, + }, + }; + } + + if (roomUpdate.$set || roomUpdate.$addToSet) { + await Rooms.updateOne({ _id: roomData._id }, roomUpdate); + } + } + + async getRoomCreatorId(roomData: IImportChannel, startedByUserId: string): Promise { + if (roomData.u) { + const creatorId = await this._cache.findImportedUserId(roomData.u._id); + if (creatorId) { + return creatorId; + } + + if (roomData.t !== 'd') { + return startedByUserId; + } + + throw new Error('importer-channel-invalid-creator'); + } + + if (roomData.t === 'd') { + for await (const member of roomData.users) { + const userId = await this._cache.findImportedUserId(member); + if (userId) { + return userId; + } + } + } + + throw new Error('importer-channel-invalid-creator'); + } + + protected getDataType(): 'channel' { + return 'channel'; + } +} diff --git a/apps/meteor/app/importer/server/classes/converters/UserConverter.ts b/apps/meteor/app/importer/server/classes/converters/UserConverter.ts new file mode 100644 index 000000000000..7401aea7c234 --- /dev/null +++ b/apps/meteor/app/importer/server/classes/converters/UserConverter.ts @@ -0,0 +1,419 @@ +import type { IImportUser, IImportUserRecord, IUser, IUserEmail } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; +import { Random } from '@rocket.chat/random'; +import { SHA256 } from '@rocket.chat/sha256'; +import { hash as bcryptHash } from 'bcrypt'; +import { Accounts } from 'meteor/accounts-base'; + +import { callbacks as systemCallbacks } from '../../../../../lib/callbacks'; +import { addUserToDefaultChannels } from '../../../../lib/server/functions/addUserToDefaultChannels'; +import { generateUsernameSuggestion } from '../../../../lib/server/functions/getUsernameSuggestion'; +import { saveUserIdentity } from '../../../../lib/server/functions/saveUserIdentity'; +import { setUserActiveStatus } from '../../../../lib/server/functions/setUserActiveStatus'; +import { notifyOnUserChange } from '../../../../lib/server/lib/notifyListener'; +import type { IConversionCallbacks } from '../../definitions/IConversionCallbacks'; +import { RecordConverter, type RecordConverterOptions } from './RecordConverter'; + +export type UserConverterOptions = { + flagEmailsAsVerified?: boolean; + skipExistingUsers?: boolean; + skipNewUsers?: boolean; + skipUserCallbacks?: boolean; + skipDefaultChannels?: boolean; + + quickUserInsertion?: boolean; + enableEmail2fa?: boolean; +}; + +export type ConvertUsersResult = { + inserted: string[]; + updated: string[]; + skipped: number; + failed: number; +}; + +export class UserConverter extends RecordConverter { + private insertedIds = new Set(); + + private updatedIds = new Set(); + + protected async convertRecord(record: IImportUserRecord): Promise { + const { data, _id } = record; + + data.importIds = data.importIds.filter((item) => item); + + if (!data.emails.length && !data.username) { + throw new Error('importer-user-missing-email-and-username'); + } + + const existingUser = await this.findExistingUser(data); + if (existingUser && this._options.skipExistingUsers) { + await this.skipRecord(_id); + return; + } + if (!existingUser && this._options.skipNewUsers) { + await this.skipRecord(_id); + return; + } + + await this.insertOrUpdateUser(existingUser, data); + return !existingUser; + } + + async convertData(userCallbacks: IConversionCallbacks = {}): Promise { + this.insertedIds.clear(); + this.updatedIds.clear(); + + if (this._options.quickUserInsertion) { + await this.batchConversion(userCallbacks); + } else { + await super.convertData(userCallbacks); + } + + await systemCallbacks.run('afterUserImport', { + inserted: [...this.insertedIds], + updated: [...this.updatedIds], + skipped: this.skippedCount, + failed: this.failedCount, + }); + } + + public async batchConversion({ afterBatchFn, ...callbacks }: IConversionCallbacks = {}): Promise { + const batchToInsert = new Set(); + + await this.iterateRecords({ + ...callbacks, + processRecord: async (record: IImportUserRecord) => { + const { data } = record; + + data.importIds = data.importIds.filter((item) => item); + + if (!data.emails.length && !data.username) { + throw new Error('importer-user-missing-email-and-username'); + } + + batchToInsert.add(data); + + if (batchToInsert.size >= 50) { + const usersToInsert = await this.buildUserBatch([...batchToInsert]); + batchToInsert.clear(); + + const newIds = await this.insertUserBatch(usersToInsert, { afterBatchFn }); + newIds.forEach((id) => this.insertedIds.add(id)); + } + + return undefined; + }, + }); + + if (batchToInsert.size > 0) { + const usersToInsert = await this.buildUserBatch([...batchToInsert]); + const newIds = await this.insertUserBatch(usersToInsert, { afterBatchFn }); + newIds.forEach((id) => this.insertedIds.add(id)); + } + } + + private async insertUserBatch(users: IUser[], { afterBatchFn }: IConversionCallbacks): Promise { + let newIds: string[] | null = null; + + try { + newIds = Object.values((await Users.insertMany(users, { ordered: false })).insertedIds); + if (afterBatchFn) { + await afterBatchFn(newIds.length, 0); + } + } catch (e: any) { + newIds = (e.result?.result?.insertedIds || []) as string[]; + const errorCount = users.length - (e.result?.result?.nInserted || 0); + + if (afterBatchFn) { + await afterBatchFn(Math.min(newIds.length, users.length - errorCount), errorCount); + } + } + + return newIds; + } + + async findExistingUser(data: IImportUser): Promise { + if (data.emails.length) { + const emailUser = await Users.findOneByEmailAddress(data.emails[0], {}); + + if (emailUser) { + return emailUser; + } + } + + // If we couldn't find one by their email address, try to find an existing user by their username + if (data.username) { + return Users.findOneByUsernameIgnoringCase(data.username, {}); + } + } + + addUserImportId(updateData: Record, userData: IImportUser): void { + if (userData.importIds?.length) { + updateData.$addToSet = { + importIds: { + $each: userData.importIds, + }, + }; + } + } + + addUserEmails(updateData: Record, userData: IImportUser, existingEmails: Array): void { + if (!userData.emails?.length) { + return; + } + + const verifyEmails = Boolean(this._options.flagEmailsAsVerified); + const newEmailList: Array = []; + + for (const email of userData.emails) { + const verified = verifyEmails || existingEmails.find((ee) => ee.address === email)?.verified || false; + + newEmailList.push({ + address: email, + verified, + }); + } + + updateData.$set.emails = newEmailList; + } + + addUserServices(updateData: Record, userData: IImportUser): void { + if (!userData.services) { + return; + } + + for (const serviceKey in userData.services) { + if (!userData.services[serviceKey]) { + continue; + } + + const service = userData.services[serviceKey]; + + for (const key in service) { + if (!service[key]) { + continue; + } + + updateData.$set[`services.${serviceKey}.${key}`] = service[key]; + } + } + } + + addCustomFields(updateData: Record, userData: IImportUser): void { + if (!userData.customFields) { + return; + } + + const subset = (source: Record, currentPath: string): void => { + for (const key in source) { + if (!source.hasOwnProperty(key)) { + continue; + } + + const keyPath = `${currentPath}.${key}`; + if (typeof source[key] === 'object' && !Array.isArray(source[key])) { + subset(source[key], keyPath); + continue; + } + + updateData.$set = { + ...updateData.$set, + ...{ [keyPath]: source[key] }, + }; + } + }; + + subset(userData.customFields, 'customFields'); + } + + async insertOrUpdateUser(existingUser: IUser | undefined, data: IImportUser): Promise { + if (!data.username && !existingUser?.username) { + const emails = data.emails.filter(Boolean).map((email) => ({ address: email })); + data.username = await generateUsernameSuggestion({ + name: data.name, + emails, + }); + } + + if (existingUser) { + await this.updateUser(existingUser, data); + this.updatedIds.add(existingUser._id); + } else { + if (!data.name && data.username) { + data.name = this.guessNameFromUsername(data.username); + } + + const userId = await this.insertUser(data); + data._id = userId; + this.insertedIds.add(userId); + + if (!this._options.skipDefaultChannels) { + const insertedUser = await Users.findOneById(userId, {}); + if (!insertedUser) { + throw new Error(`User not found: ${userId}`); + } + + await addUserToDefaultChannels(insertedUser, true); + } + } + } + + async updateUser(existingUser: IUser, userData: IImportUser): Promise { + const { _id } = existingUser; + if (!_id) { + return; + } + + userData._id = _id; + + if (!userData.roles && !existingUser.roles) { + userData.roles = ['user']; + } + if (!userData.type && !existingUser.type) { + userData.type = 'user'; + } + + const updateData: Record = Object.assign(Object.create(null), { + $set: Object.assign(Object.create(null), { + ...(userData.roles && { roles: userData.roles }), + ...(userData.type && { type: userData.type }), + ...(userData.statusText && { statusText: userData.statusText }), + ...(userData.bio && { bio: userData.bio }), + ...(userData.services?.ldap && { ldap: true }), + ...(userData.avatarUrl && { _pendingAvatarUrl: userData.avatarUrl }), + }), + }); + + this.addCustomFields(updateData, userData); + this.addUserServices(updateData, userData); + this.addUserImportId(updateData, userData); + this.addUserEmails(updateData, userData, existingUser.emails || []); + + if (Object.keys(updateData.$set).length === 0) { + delete updateData.$set; + } + if (Object.keys(updateData).length > 0) { + await Users.updateOne({ _id }, updateData); + } + + if (userData.utcOffset) { + await Users.setUtcOffset(_id, userData.utcOffset); + } + + if (userData.name || userData.username) { + await saveUserIdentity({ _id, name: userData.name, username: userData.username } as Parameters[0]); + } + + if (userData.importIds.length) { + this._cache.addUser(userData.importIds[0], existingUser._id, existingUser.username || userData.username); + } + + // Deleted users are 'inactive' users in Rocket.Chat + if (userData.deleted && existingUser?.active) { + await setUserActiveStatus(_id, false, true); + } else if (userData.deleted === false && existingUser?.active === false) { + await setUserActiveStatus(_id, true); + } + + void notifyOnUserChange({ clientAction: 'updated', id: _id, diff: updateData.$set }); + } + + private async hashPassword(password: string): Promise { + return bcryptHash(SHA256(password), Accounts._bcryptRounds()); + } + + private generateTempPassword(userData: IImportUser): string { + return `${Date.now()}${userData.name || ''}${userData.emails.length ? userData.emails[0].toUpperCase() : ''}`; + } + + private async buildNewUserObject(userData: IImportUser): Promise> { + return { + type: userData.type || 'user', + ...(userData.username && { username: userData.username }), + ...(userData.emails.length && { + emails: userData.emails.map((email) => ({ address: email, verified: !!this._options.flagEmailsAsVerified })), + }), + ...(userData.statusText && { statusText: userData.statusText }), + ...(userData.name && { name: userData.name }), + ...(userData.bio && { bio: userData.bio }), + ...(userData.avatarUrl && { _pendingAvatarUrl: userData.avatarUrl }), + ...(userData.utcOffset !== undefined && { utcOffset: userData.utcOffset }), + ...{ + services: { + // Add a password service if there's a password string, or if there's no service at all + ...((!!userData.password || !userData.services || !Object.keys(userData.services).length) && { + password: { bcrypt: await this.hashPassword(userData.password || this.generateTempPassword(userData)) }, + }), + ...(userData.services || {}), + }, + }, + ...(userData.services?.ldap && { ldap: true }), + ...(userData.importIds?.length && { importIds: userData.importIds }), + ...(!!userData.customFields && { customFields: userData.customFields }), + ...(userData.deleted !== undefined && { active: !userData.deleted }), + }; + } + + private async buildUserBatch(usersData: IImportUser[]): Promise { + return Promise.all( + usersData.map(async (userData) => { + const user = await this.buildNewUserObject(userData); + return { + createdAt: new Date(), + _id: Random.id(), + + status: 'offline', + ...user, + roles: userData.roles?.length ? userData.roles : ['user'], + active: !userData.deleted, + services: { + ...user.services, + ...(this._options.enableEmail2fa + ? { + email2fa: { + enabled: true, + changedAt: new Date(), + }, + } + : {}), + }, + } as IUser; + }), + ); + } + + async insertUser(userData: IImportUser): Promise { + const user = await this.buildNewUserObject(userData); + + return Accounts.insertUserDoc( + { + joinDefaultChannels: false, + skipEmailValidation: true, + skipAdminCheck: true, + skipAdminEmail: true, + skipOnCreateUserCallback: this._options.skipUserCallbacks, + skipBeforeCreateUserCallback: this._options.skipUserCallbacks, + skipAfterCreateUserCallback: this._options.skipUserCallbacks, + skipDefaultAvatar: true, + skipAppsEngineEvent: !!process.env.IMPORTER_SKIP_APPS_EVENT, + }, + { + ...user, + ...(userData.roles?.length ? { globalRoles: userData.roles } : {}), + }, + ); + } + + protected guessNameFromUsername(username: string): string { + return username + .replace(/\W/g, ' ') + .replace(/\s(.)/g, (u) => u.toUpperCase()) + .replace(/^(.)/, (u) => u.toLowerCase()) + .replace(/^\w/, (u) => u.toUpperCase()); + } + + protected getDataType(): 'user' { + return 'user'; + } +} diff --git a/apps/meteor/app/lib/server/methods/addUsersToRoom.ts b/apps/meteor/app/lib/server/methods/addUsersToRoom.ts index 119071e8ece0..73fbf6e51a04 100644 --- a/apps/meteor/app/lib/server/methods/addUsersToRoom.ts +++ b/apps/meteor/app/lib/server/methods/addUsersToRoom.ts @@ -98,11 +98,14 @@ export const addUsersToRoomMethod = async (userId: string, data: { rid: string; return; } void api.broadcast('notify.ephemeralMessage', userId, data.rid, { - msg: i18n.t('Username_is_already_in_here', { - postProcess: 'sprintf', - sprintf: [newUser.username], - lng: user?.language, - }), + msg: i18n.t( + 'Username_is_already_in_here', + { + postProcess: 'sprintf', + sprintf: [newUser.username], + }, + user?.language, + ), }); } }), diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index e004f2199fa0..56009f15fede 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -2,7 +2,6 @@ import { api } from '@rocket.chat/core-services'; import type { AtLeast, IMessage, IUser } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Messages, Users } from '@rocket.chat/models'; -import type { TOptions } from 'i18next'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import moment from 'moment'; @@ -99,9 +98,9 @@ export async function executeSendMessage(uid: IUser['_id'], message: AtLeast void; @@ -9,7 +9,7 @@ type PlaceChatOnHoldModalProps = { }; const PlaceChatOnHoldModal = ({ onCancel, onOnHoldChat, confirm = onOnHoldChat, ...props }: PlaceChatOnHoldModalProps) => { - const { t } = useTranslation(); + const t = useTranslation(); return ( diff --git a/apps/meteor/app/livechat/imports/server/rest/sms.ts b/apps/meteor/app/livechat/imports/server/rest/sms.ts index 6b2411cf8e3d..2fe3ce40eed1 100644 --- a/apps/meteor/app/livechat/imports/server/rest/sms.ts +++ b/apps/meteor/app/livechat/imports/server/rest/sms.ts @@ -121,13 +121,17 @@ API.v1.addRoute('livechat/sms-incoming/:service', { return API.v1.success(SMSService.error(new Error('Invalid visitor'))); } - const roomInfo = { + const roomInfo: { + source?: IOmnichannelRoom['source']; + [key: string]: unknown; + } = { sms: { from: sms.to, }, source: { type: OmnichannelSourceType.SMS, alias: service, + destination: sms.to, }, }; diff --git a/apps/meteor/app/livechat/server/api/lib/livechat.ts b/apps/meteor/app/livechat/server/api/lib/livechat.ts index 8041566d796e..01c4d9736c66 100644 --- a/apps/meteor/app/livechat/server/api/lib/livechat.ts +++ b/apps/meteor/app/livechat/server/api/lib/livechat.ts @@ -57,6 +57,7 @@ export function findGuest(token: string): Promise { visitorEmails: 1, department: 1, activity: 1, + contactId: 1, }, }); } diff --git a/apps/meteor/app/livechat/server/api/v1/contact.ts b/apps/meteor/app/livechat/server/api/v1/contact.ts index 0de3181708fb..5c68a475a952 100644 --- a/apps/meteor/app/livechat/server/api/v1/contact.ts +++ b/apps/meteor/app/livechat/server/api/v1/contact.ts @@ -3,6 +3,7 @@ import { isPOSTOmnichannelContactsProps, isPOSTUpdateOmnichannelContactsProps, isGETOmnichannelContactsProps, + isGETOmnichannelContactHistoryProps, isGETOmnichannelContactsSearchProps, } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -11,7 +12,7 @@ import { Meteor } from 'meteor/meteor'; import { API } from '../../../../api/server'; import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems'; -import { Contacts, createContact, updateContact, getContacts, isSingleContactEnabled } from '../../lib/Contacts'; +import { getContactHistory, Contacts, createContact, updateContact, getContacts, isSingleContactEnabled } from '../../lib/Contacts'; API.v1.addRoute( 'omnichannel/contact', @@ -161,3 +162,23 @@ API.v1.addRoute( }, }, ); + +API.v1.addRoute( + 'omnichannel/contacts.history', + { authRequired: true, permissionsRequired: ['view-livechat-contact-history'], validateParams: isGETOmnichannelContactHistoryProps }, + { + async get() { + if (!isSingleContactEnabled()) { + return API.v1.unauthorized(); + } + + const { contactId, source } = this.queryParams; + const { offset, count } = await getPaginationItems(this.queryParams); + const { sort } = await this.parseJsonQuery(); + + const history = await getContactHistory({ contactId, source, count, offset, sort }); + + return API.v1.success(history); + }, + }, +); diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 404f576ea513..9bda8b443eab 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -76,7 +76,9 @@ API.v1.addRoute( const roomInfo = { source: { - type: isWidget(this.request.headers) ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API, + ...(isWidget(this.request.headers) + ? { type: OmnichannelSourceType.WIDGET, destination: this.request.headers.host } + : { type: OmnichannelSourceType.API }), }, }; @@ -159,7 +161,7 @@ API.v1.addRoute( const visitorEmail = visitor.visitorEmails?.[0]?.address; const language = servingAgent.language || rcSettings.get('Language') || 'en'; - const t = i18n.getFixedT(language); + const t = (s: string): string => i18n.t(s, { lng: language }); const subject = t('Transcript_of_your_livechat_conversation'); options.emailTranscript = { diff --git a/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts b/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts index 9969f03bf8bb..d8014dd3ecc0 100644 --- a/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts +++ b/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts @@ -1,5 +1,5 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings'; -import { LivechatVisitors } from '@rocket.chat/models'; +import { LivechatContacts, LivechatVisitors } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; @@ -12,7 +12,7 @@ callbacks.add( const { _id, - v: { _id: guestId }, + v: { _id: guestId, contactId }, } = room; const lastChat = { @@ -20,6 +20,9 @@ callbacks.add( ts: new Date(), }; await LivechatVisitors.setLastChatById(guestId, lastChat); + if (contactId) { + await LivechatContacts.updateLastChatById(contactId, lastChat); + } }, callbacks.priority.MEDIUM, 'livechat-save-last-chat', diff --git a/apps/meteor/app/livechat/server/lib/Contacts.ts b/apps/meteor/app/livechat/server/lib/Contacts.ts index 8d440e297249..dd7fb0b99848 100644 --- a/apps/meteor/app/livechat/server/lib/Contacts.ts +++ b/apps/meteor/app/livechat/server/lib/Contacts.ts @@ -7,6 +7,7 @@ import type { IOmnichannelRoom, IUser, } from '@rocket.chat/core-typings'; +import type { InsertionModel } from '@rocket.chat/model-typings'; import { LivechatVisitors, Users, @@ -17,10 +18,10 @@ import { Subscriptions, LivechatContacts, } from '@rocket.chat/models'; -import type { PaginatedResult } from '@rocket.chat/rest-typings'; +import type { PaginatedResult, VisitorSearchChatsResult } from '@rocket.chat/rest-typings'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import type { MatchKeysAndValues, OnlyFieldsOfType, Sort } from 'mongodb'; +import type { MatchKeysAndValues, OnlyFieldsOfType, FindOptions, Sort } from 'mongodb'; import { callbacks } from '../../../../lib/callbacks'; import { trim } from '../../../../lib/utils/stringUtils'; @@ -71,6 +72,14 @@ type GetContactsParams = { sort: Sort; }; +type GetContactHistoryParams = { + contactId: string; + source?: string; + count: number; + offset: number; + sort: Sort; +}; + export const Contacts = { async registerContact({ token, @@ -183,6 +192,35 @@ export function isSingleContactEnabled(): boolean { return process.env.TEST_MODE?.toUpperCase() === 'TRUE'; } +export async function createContactFromVisitor(visitor: ILivechatVisitor): Promise { + if (visitor.contactId) { + throw new Error('error-contact-already-exists'); + } + + const contactData: InsertionModel = { + name: visitor.name || visitor.username, + emails: visitor.visitorEmails, + phones: visitor.phone || undefined, + unknown: true, + channels: [], + customFields: visitor.livechatData, + createdAt: new Date(), + }; + + if (visitor.contactManager) { + const contactManagerId = await Users.findOneByUsername>(visitor.contactManager.username, { projection: { _id: 1 } }); + if (contactManagerId) { + contactData.contactManager = contactManagerId._id; + } + } + + const { insertedId: contactId } = await LivechatContacts.insertOne(contactData); + + await LivechatVisitors.updateOne({ _id: visitor._id }, { $set: { contactId } }); + + return contactId; +} + export async function createContact(params: CreateContactParams): Promise { const { name, emails, phones, customFields: receivedCustomFields = {}, contactManager, channels, unknown } = params; @@ -195,12 +233,13 @@ export async function createContact(params: CreateContactParams): Promise ({ address })), + phones: phones?.map((phoneNumber) => ({ phoneNumber })), contactManager, channels, customFields, unknown, + createdAt: new Date(), }); return insertedId; @@ -223,8 +262,8 @@ export async function updateContact(params: UpdateContactParams): Promise ({ address })), + phones: phones?.map((phoneNumber) => ({ phoneNumber })), contactManager, channels, customFields, @@ -252,6 +291,57 @@ export async function getContacts(params: GetContactsParams): Promise> { + const { contactId, source, count, offset, sort } = params; + + const contact = await LivechatContacts.findOneById>(contactId, { projection: { channels: 1 } }); + + if (!contact) { + throw new Error('error-contact-not-found'); + } + + const visitorsIds = new Set(contact.channels?.map((channel: ILivechatContactChannel) => channel.visitorId)); + + if (!visitorsIds?.size) { + return { history: [], count: 0, offset, total: 0 }; + } + + const options: FindOptions = { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + projection: { + fname: 1, + ts: 1, + v: 1, + msgs: 1, + servedBy: 1, + closedAt: 1, + closedBy: 1, + closer: 1, + tags: 1, + source: 1, + }, + }; + + const { totalCount, cursor } = LivechatRooms.findPaginatedRoomsByVisitorsIdsAndSource({ + visitorsIds: Array.from(visitorsIds), + source, + options, + }); + + const [total, history] = await Promise.all([totalCount, cursor.toArray()]); + + return { + history, + count: history.length, + offset, + total, + }; +} + async function getAllowedCustomFields(): Promise[]> { return LivechatCustomField.findByScope( 'visitor', diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index 017eb516a487..1416ed8b028f 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -85,7 +85,7 @@ export const createLivechatRoom = async < ); const extraRoomInfo = await callbacks.run('livechat.beforeRoom', roomInfo, extraData); - const { _id, username, token, department: departmentId, status = 'online' } = guest; + const { _id, username, token, department: departmentId, status = 'online', contactId } = guest; const newRoomAt = new Date(); const { activity } = guest; @@ -109,6 +109,7 @@ export const createLivechatRoom = async < username, token, status, + contactId, ...(activity?.length && { activity }), }, cl: false, @@ -439,8 +440,8 @@ export const dispatchInquiryQueued = async (inquiry: ILivechatInquiryRecord, age hasMentionToHere: false, message: { _id: '', u: v, msg: '' }, // we should use server's language for this type of messages instead of user's - notificationMessage: i18n.t('User_started_a_new_conversation', { username: notificationUserName, lng: language }), - room: Object.assign(room, { name: i18n.t('New_chat_in_queue', { lng: language }) }), + notificationMessage: i18n.t('User_started_a_new_conversation', { username: notificationUserName }, language), + room: Object.assign(room, { name: i18n.t('New_chat_in_queue', {}, language) }), mentionIds: [], }); } diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 44ee46f04418..e521ac98fe71 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -20,10 +20,11 @@ import type { IOmnichannelAgent, ILivechatDepartmentAgents, LivechatDepartmentDTO, - OmnichannelSourceType, ILivechatInquiryRecord, + ILivechatContact, + ILivechatContactChannel, } from '@rocket.chat/core-typings'; -import { ILivechatAgentStatus, UserStatus, isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { OmnichannelSourceType, ILivechatAgentStatus, UserStatus, isOmnichannelRoom } from '@rocket.chat/core-typings'; import { Logger, type MainLogger } from '@rocket.chat/logger'; import { LivechatDepartment, @@ -37,6 +38,7 @@ import { ReadReceipts, Rooms, LivechatCustomField, + LivechatContacts, } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { Match, check } from 'meteor/check'; @@ -71,7 +73,7 @@ import * as Mailer from '../../../mailer/server/api'; import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; import { businessHourManager } from '../business-hour'; -import { createContact, isSingleContactEnabled } from './Contacts'; +import { createContact, createContactFromVisitor, isSingleContactEnabled } from './Contacts'; import { parseAgentCustomFields, updateDepartmentAgents, validateEmail, normalizeTransferredByData } from './Helper'; import { QueueManager } from './QueueManager'; import { RoutingManager } from './RoutingManager'; @@ -459,6 +461,55 @@ class LivechatClass { extraData, }); + if (isSingleContactEnabled()) { + let { contactId } = visitor; + + if (!contactId) { + const visitorContact = await LivechatVisitors.findOne< + Pick + >(visitor._id, { + projection: { + name: 1, + contactManager: 1, + livechatData: 1, + phone: 1, + visitorEmails: 1, + username: 1, + contactId: 1, + }, + }); + + contactId = visitorContact?.contactId; + } + + if (!contactId) { + // ensure that old visitors have a contact + contactId = await createContactFromVisitor(visitor); + } + + const contact = await LivechatContacts.findOneById>(contactId, { + projection: { _id: 1, channels: 1 }, + }); + + if (contact) { + const channel = contact.channels?.find( + (channel: ILivechatContactChannel) => channel.name === roomInfo.source?.type && channel.visitorId === visitor._id, + ); + + if (!channel) { + Livechat.logger.debug(`Adding channel for contact ${contact._id}`); + + await LivechatContacts.addChannel(contact._id, { + name: roomInfo.source?.label || roomInfo.source?.type.toString() || OmnichannelSourceType.OTHER, + visitorId: visitor._id, + blocked: false, + verified: false, + details: roomInfo.source, + }); + } + } + } + Livechat.logger.debug(`Room obtained for visitor ${visitor._id} -> ${room._id}`); await Messages.setRoomIdByToken(visitor.token, room._id); diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index c6728d470870..e1ea79d84163 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -371,8 +371,8 @@ export class QueueManager { hasMentionToHere: false, message: { _id: '', u: v, msg: '' }, // we should use server's language for this type of messages instead of user's - notificationMessage: i18n.t('User_started_a_new_conversation', { username: notificationUserName, lng: language }), - room: { ...room, name: i18n.t('New_chat_in_queue', { lng: language }) }, + notificationMessage: i18n.t('User_started_a_new_conversation', { username: notificationUserName }, language), + room: { ...room, name: i18n.t('New_chat_in_queue', {}, language) }, mentionIds: [], }); } diff --git a/apps/meteor/app/ui-message/client/messageBox/AddLinkComposerActionModal.tsx b/apps/meteor/app/ui-message/client/messageBox/AddLinkComposerActionModal.tsx index 21e502c90001..420bf93df66d 100644 --- a/apps/meteor/app/ui-message/client/messageBox/AddLinkComposerActionModal.tsx +++ b/apps/meteor/app/ui-message/client/messageBox/AddLinkComposerActionModal.tsx @@ -1,8 +1,8 @@ import { Field, FieldGroup, TextInput, FieldLabel, FieldRow, Box } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useEffect } from 'react'; import { useForm, Controller } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; import GenericModal from '../../../../client/components/GenericModal'; @@ -13,7 +13,7 @@ type AddLinkComposerActionModalProps = { }; const AddLinkComposerActionModal = ({ selectedText, onClose, onConfirm }: AddLinkComposerActionModalProps) => { - const { t } = useTranslation(); + const t = useTranslation(); const textField = useUniqueId(); const urlField = useUniqueId(); diff --git a/apps/meteor/app/utils/lib/i18n.ts b/apps/meteor/app/utils/lib/i18n.ts index 737b98666d0a..b69fe6b30513 100644 --- a/apps/meteor/app/utils/lib/i18n.ts +++ b/apps/meteor/app/utils/lib/i18n.ts @@ -1,5 +1,4 @@ import type { RocketchatI18nKeys } from '@rocket.chat/i18n'; -import type { TOptions } from 'i18next'; import i18next from 'i18next'; import sprintf from 'i18next-sprintf-postprocessor'; @@ -14,7 +13,7 @@ export const addSprinfToI18n = function (t: (typeof i18n)['t']) { } if (isObject(replaces[0]) && !Array.isArray(replaces[0])) { - return t(key, replaces[0] as TOptions); + return t(key, replaces[0]); } return t(key, { diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmniChannelCallDialPad.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmniChannelCallDialPad.tsx index 2693060578ed..af9b907df12e 100644 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmniChannelCallDialPad.tsx +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmniChannelCallDialPad.tsx @@ -1,7 +1,7 @@ import { NavBarItem } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentPropsWithoutRef } from 'react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; import { useVoipOutboundStates } from '../../contexts/CallContext'; import { useDialModal } from '../../hooks/useDialModal'; @@ -9,7 +9,7 @@ import { useDialModal } from '../../hooks/useDialModal'; type NavBarItemOmniChannelCallDialPadProps = ComponentPropsWithoutRef; const NavBarItemOmniChannelCallDialPad = (props: NavBarItemOmniChannelCallDialPadProps) => { - const { t } = useTranslation(); + const t = useTranslation(); const { openDialModal } = useDialModal(); diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleError.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleError.tsx index 7f2b6adc8691..cf4e7ec240b4 100644 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleError.tsx +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleError.tsx @@ -1,12 +1,12 @@ import { NavBarItem } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentPropsWithoutRef } from 'react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; type NavBarItemOmnichannelCallToggleErrorProps = ComponentPropsWithoutRef; const NavBarItemOmnichannelCallToggleError = (props: NavBarItemOmnichannelCallToggleErrorProps) => { - const { t } = useTranslation(); + const t = useTranslation(); return ; }; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleLoading.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleLoading.tsx index 149500050402..c4b53acefabb 100644 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleLoading.tsx +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleLoading.tsx @@ -1,12 +1,12 @@ import { NavBarItem } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentPropsWithoutRef } from 'react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; type NavBarItemOmnichannelCallToggleLoadingProps = ComponentPropsWithoutRef; const NavBarItemOmnichannelCallToggleLoading = (props: NavBarItemOmnichannelCallToggleLoadingProps) => { - const { t } = useTranslation(); + const t = useTranslation(); return ; }; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleReady.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleReady.tsx index 82f1c28350cd..8b51fc6c5b57 100644 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleReady.tsx +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleReady.tsx @@ -1,14 +1,14 @@ import { NavBarItem } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentPropsWithoutRef } from 'react'; import React, { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; import { useCallerInfo, useCallRegisterClient, useCallUnregisterClient, useVoipNetworkStatus } from '../../contexts/CallContext'; type NavBarItemOmnichannelCallToggleReadyProps = ComponentPropsWithoutRef; const NavBarItemOmnichannelCallToggleReady = (props: NavBarItemOmnichannelCallToggleReadyProps) => { - const { t } = useTranslation(); + const t = useTranslation(); const caller = useCallerInfo(); const unregister = useCallUnregisterClient(); diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/UserMenu.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/UserMenu.tsx index 149ad0ea585e..22895d55388f 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/UserMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/UserMenu.tsx @@ -1,9 +1,9 @@ import type { IUser } from '@rocket.chat/core-typings'; import { GenericMenu, useHandleMenuAction } from '@rocket.chat/ui-client'; import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import React, { memo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; import UserMenuButton from './UserMenuButton'; import { useUserMenu } from './hooks/useUserMenu'; @@ -11,7 +11,7 @@ import { useUserMenu } from './hooks/useUserMenu'; type UserMenuProps = { user: IUser } & Omit, 'sections' | 'items' | 'title'>; const UserMenu = function UserMenu({ user, ...props }: UserMenuProps) { - const { t } = useTranslation(); + const t = useTranslation(); const [isOpen, setIsOpen] = useState(false); const sections = useUserMenu(user); diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useUserMenu.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useUserMenu.tsx index fce9c3d14fd4..e554d3c16c82 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useUserMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useUserMenu.tsx @@ -7,14 +7,14 @@ import React from 'react'; import UserMenuHeader from '../UserMenuHeader'; import { useAccountItems } from './useAccountItems'; import { useStatusItems } from './useStatusItems'; -import { useVoipItems } from './useVoipItems'; +import { useVoipItemsSection } from './useVoipItemsSection'; export const useUserMenu = (user: IUser) => { const t = useTranslation(); const statusItems = useStatusItems(); const accountItems = useAccountItems(); - const voipItems = useVoipItems(); + const voipSection = useVoipItemsSection(); const logout = useLogout(); const handleLogout = useEffectEvent(() => { @@ -37,9 +37,7 @@ export const useUserMenu = (user: IUser) => { title: t('Status'), items: statusItems, }, - { - items: voipItems, - }, + voipSection, { title: t('Account'), items: accountItems, @@ -47,5 +45,5 @@ export const useUserMenu = (user: IUser) => { { items: [logoutItem], }, - ]; + ].filter((section) => section !== undefined); }; diff --git a/apps/meteor/client/sidebar/header/hooks/useVoipItems.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useVoipItemsSection.tsx similarity index 72% rename from apps/meteor/client/sidebar/header/hooks/useVoipItems.tsx rename to apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useVoipItemsSection.tsx index d7cbf2428c32..9b126e4eb1e5 100644 --- a/apps/meteor/client/sidebar/header/hooks/useVoipItems.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useVoipItemsSection.tsx @@ -6,7 +6,7 @@ import { useMutation } from '@tanstack/react-query'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -const useVoipItems = (): GenericMenuItemProps[] => { +export const useVoipItemsSection = (): { items: GenericMenuItemProps[] } | undefined => { const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); @@ -45,23 +45,25 @@ const useVoipItems = (): GenericMenuItemProps[] => { return useMemo(() => { if (!isEnabled) { - return []; + return; } - return [ - { - id: 'toggle-voip', - icon: isRegistered ? 'phone-disabled' : 'phone', - disabled: !isReady || toggleVoip.isLoading, - onClick: () => toggleVoip.mutate(), - content: ( - - {isRegistered ? t('Disable_voice_calling') : t('Enable_voice_calling')} - - ), - }, - ]; + return { + items: [ + { + id: 'toggle-voip', + icon: isRegistered ? 'phone-disabled' : 'phone', + disabled: !isReady || toggleVoip.isLoading, + onClick: () => toggleVoip.mutate(), + content: ( + + {isRegistered ? t('Disable_voice_calling') : t('Enable_voice_calling')} + + ), + }, + ], + }; }, [isEnabled, isRegistered, isReady, tooltip, t, toggleVoip]); }; -export default useVoipItems; +export default useVoipItemsSection; diff --git a/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx b/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx index dbaea02ace4a..f589dd21ed50 100644 --- a/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx @@ -1,7 +1,7 @@ import { Avatar } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; import { ContextualbarTitle, @@ -19,7 +19,7 @@ interface IGameCenterContainerProps { } const GameCenterContainer = ({ handleClose, handleBack, game }: IGameCenterContainerProps): ReactElement => { - const { t } = useTranslation(); + const t = useTranslation(); return ( <> diff --git a/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx b/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx index 871e82f3ff56..d0dcc6fad4fe 100644 --- a/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx @@ -1,8 +1,8 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useState } from 'react'; -import { useTranslation } from 'react-i18next'; import GenericModal from '../../components/GenericModal'; import UserAutoCompleteMultipleFederated from '../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; @@ -19,7 +19,7 @@ interface IGameCenterInvitePlayersModalProps { } const GameCenterInvitePlayersModal = ({ game, onClose }: IGameCenterInvitePlayersModalProps): ReactElement => { - const { t } = useTranslation(); + const t = useTranslation(); const [users, setUsers] = useState>([]); const { name } = game; diff --git a/apps/meteor/client/components/ActionManagerBusyState.tsx b/apps/meteor/client/components/ActionManagerBusyState.tsx index 932eb08ea502..1399c045271f 100644 --- a/apps/meteor/client/components/ActionManagerBusyState.tsx +++ b/apps/meteor/client/components/ActionManagerBusyState.tsx @@ -1,12 +1,12 @@ import { css } from '@rocket.chat/css-in-js'; import { Box } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; import { useUiKitActionManager } from '../uikit/hooks/useUiKitActionManager'; const ActionManagerBusyState = () => { - const { t } = useTranslation(); + const t = useTranslation(); const actionManager = useUiKitActionManager(); const [busy, setBusy] = useState(false); diff --git a/apps/meteor/client/components/AutoCompleteDepartment.tsx b/apps/meteor/client/components/AutoCompleteDepartment.tsx index 0c50f2254aac..7ff38c9c3ca4 100644 --- a/apps/meteor/client/components/AutoCompleteDepartment.tsx +++ b/apps/meteor/client/components/AutoCompleteDepartment.tsx @@ -1,8 +1,8 @@ import { PaginatedSelectFiltered } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement } from 'react'; import React, { memo, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; import { useRecordList } from '../hooks/lists/useRecordList'; import { AsyncStatePhase } from '../hooks/useAsyncState'; @@ -28,7 +28,7 @@ const AutoCompleteDepartment = ({ showArchived = false, ...props }: AutoCompleteDepartmentProps): ReactElement | null => { - const { t } = useTranslation(); + const t = useTranslation(); const [departmentsFilter, setDepartmentsFilter] = useState(''); const debouncedDepartmentsFilter = useDebouncedValue(departmentsFilter, 500); @@ -42,8 +42,9 @@ const AutoCompleteDepartment = ({ haveNone, excludeDepartmentId, showArchived, + selectedDepartment: value, }), - [debouncedDepartmentsFilter, onlyMyDepartments, haveAll, haveNone, excludeDepartmentId, showArchived], + [debouncedDepartmentsFilter, onlyMyDepartments, haveAll, haveNone, excludeDepartmentId, showArchived, value], ), ); diff --git a/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx b/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx index c15d480f3900..99af9a1f6a2c 100644 --- a/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx +++ b/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx @@ -1,9 +1,9 @@ import { CheckOption, PaginatedMultiSelectFiltered } from '@rocket.chat/fuselage'; import type { PaginatedMultiSelectOption } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import React, { memo, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; import { useRecordList } from '../hooks/lists/useRecordList'; import { AsyncStatePhase } from '../hooks/useAsyncState'; @@ -24,7 +24,7 @@ const AutoCompleteDepartmentMultiple = ({ enabled = false, onChange = () => undefined, }: AutoCompleteDepartmentMultipleProps) => { - const { t } = useTranslation(); + const t = useTranslation(); const [departmentsFilter, setDepartmentsFilter] = useState(''); const debouncedDepartmentsFilter = useDebouncedValue(departmentsFilter, 500); diff --git a/apps/meteor/client/components/ConfirmOwnerChangeModal.tsx b/apps/meteor/client/components/ConfirmOwnerChangeModal.tsx index 77135fad6230..349341baf003 100644 --- a/apps/meteor/client/components/ConfirmOwnerChangeModal.tsx +++ b/apps/meteor/client/components/ConfirmOwnerChangeModal.tsx @@ -1,7 +1,7 @@ import { Box } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentPropsWithoutRef } from 'react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; import GenericModal from './GenericModal'; import RawText from './RawText'; @@ -20,7 +20,7 @@ const ConfirmOwnerChangeModal = ({ onConfirm, onCancel, }: ConfirmOwnerChangeModalProps) => { - const { t } = useTranslation(); + const t = useTranslation(); let changeOwnerRooms = ''; if (shouldChangeOwner.length > 0) { diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarBack.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarBack.tsx index dcac448b1e92..c8e17ab88d80 100644 --- a/apps/meteor/client/components/Contextualbar/ContextualbarBack.tsx +++ b/apps/meteor/client/components/Contextualbar/ContextualbarBack.tsx @@ -1,13 +1,13 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ComponentProps } from 'react'; import React, { memo } from 'react'; -import { useTranslation } from 'react-i18next'; import ContextualbarAction from './ContextualbarAction'; type ContextualbarBackProps = Partial>; const ContextualbarBack = (props: ContextualbarBackProps): ReactElement => { - const { t } = useTranslation(); + const t = useTranslation(); return ; }; diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarClose.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarClose.tsx index 38db516476e3..1670c9be5895 100644 --- a/apps/meteor/client/components/Contextualbar/ContextualbarClose.tsx +++ b/apps/meteor/client/components/Contextualbar/ContextualbarClose.tsx @@ -1,13 +1,13 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement } from 'react'; import React, { memo } from 'react'; -import { useTranslation } from 'react-i18next'; import ContextualbarAction from './ContextualbarAction'; type ContextualbarCloseProps = Partial>; const ContextualbarClose = (props: ContextualbarCloseProps): ReactElement => { - const { t } = useTranslation(); + const t = useTranslation(); return ; }; diff --git a/apps/meteor/client/components/FilterByText.tsx b/apps/meteor/client/components/FilterByText.tsx index 25d8e225e3d8..5c5a3d599e2f 100644 --- a/apps/meteor/client/components/FilterByText.tsx +++ b/apps/meteor/client/components/FilterByText.tsx @@ -1,8 +1,8 @@ import { Box, Icon, TextInput, Margins } from '@rocket.chat/fuselage'; import { useAutoFocus, useMergedRefs } from '@rocket.chat/fuselage-hooks'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ChangeEvent, FormEvent, HTMLAttributes } from 'react'; import React, { forwardRef, memo, useCallback, useState } from 'react'; -import { useTranslation } from 'react-i18next'; type FilterByTextProps = { onChange: (filter: string) => void; @@ -13,7 +13,7 @@ const FilterByText = forwardRef(function Fi { placeholder, onChange: setFilter, shouldAutoFocus = false, children, ...props }, ref, ) { - const { t } = useTranslation(); + const t = useTranslation(); const [text, setText] = useState(''); const autoFocusRef = useAutoFocus(shouldAutoFocus); const mergedRefs = useMergedRefs(ref, autoFocusRef); diff --git a/apps/meteor/client/components/FingerprintChangeModal.tsx b/apps/meteor/client/components/FingerprintChangeModal.tsx index a45a17db8ccc..db4c33654a92 100644 --- a/apps/meteor/client/components/FingerprintChangeModal.tsx +++ b/apps/meteor/client/components/FingerprintChangeModal.tsx @@ -1,7 +1,7 @@ import { Box } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; import GenericModal from './GenericModal'; @@ -12,7 +12,7 @@ type FingerprintChangeModalProps = { }; const FingerprintChangeModal = ({ onConfirm, onCancel, onClose }: FingerprintChangeModalProps): ReactElement => { - const { t } = useTranslation(); + const t = useTranslation(); return ( { - const { t } = useTranslation(); + const t = useTranslation(); return ( { - const { t } = useTranslation(); + const t = useTranslation(); return ( diff --git a/apps/meteor/client/components/GenericModal/GenericModal.tsx b/apps/meteor/client/components/GenericModal/GenericModal.tsx index d91e3c066007..5d025e05827d 100644 --- a/apps/meteor/client/components/GenericModal/GenericModal.tsx +++ b/apps/meteor/client/components/GenericModal/GenericModal.tsx @@ -1,9 +1,9 @@ import { Button, Modal } from '@rocket.chat/fuselage'; import { useEffectEvent, useUniqueId } from '@rocket.chat/fuselage-hooks'; import type { Keys as IconName } from '@rocket.chat/icons'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement, ReactNode, ComponentPropsWithoutRef } from 'react'; import React, { useEffect, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; import type { RequiredModalProps } from './withDoNotAskAgain'; import { withDoNotAskAgain } from './withDoNotAskAgain'; @@ -75,7 +75,7 @@ const GenericModal = ({ annotation, ...props }: GenericModalProps) => { - const { t } = useTranslation(); + const t = useTranslation(); const genericModalId = useUniqueId(); const dismissedRef = useRef(true); diff --git a/apps/meteor/client/components/GenericNoResults/GenericNoResults.tsx b/apps/meteor/client/components/GenericNoResults/GenericNoResults.tsx index d21023024fb3..3fcfe2b0e0ac 100644 --- a/apps/meteor/client/components/GenericNoResults/GenericNoResults.tsx +++ b/apps/meteor/client/components/GenericNoResults/GenericNoResults.tsx @@ -1,7 +1,7 @@ import { Box, States, StatesIcon, StatesLink, StatesTitle, StatesSubtitle, StatesActions, StatesAction } from '@rocket.chat/fuselage'; import type { Keys as IconName } from '@rocket.chat/icons'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import { useTranslation } from 'react-i18next'; type LinkProps = { linkText: string; linkHref: string } | { linkText?: never; linkHref?: never }; type ButtonProps = { buttonTitle: string; buttonAction: () => void } | { buttonTitle?: never; buttonAction?: never }; @@ -23,7 +23,7 @@ const GenericNoResults = ({ linkHref, linkText, }: GenericNoResultsProps) => { - const { t } = useTranslation(); + const t = useTranslation(); return ( diff --git a/apps/meteor/client/components/GenericTable/hooks/useItemsPerPageLabel.ts b/apps/meteor/client/components/GenericTable/hooks/useItemsPerPageLabel.ts index 4c390041497e..0a8a8deb8262 100644 --- a/apps/meteor/client/components/GenericTable/hooks/useItemsPerPageLabel.ts +++ b/apps/meteor/client/components/GenericTable/hooks/useItemsPerPageLabel.ts @@ -1,7 +1,7 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; export const useItemsPerPageLabel = (): (() => string) => { - const { t } = useTranslation(); + const t = useTranslation(); return useCallback(() => t('Items_per_page:'), [t]); }; diff --git a/apps/meteor/client/components/GenericTable/hooks/useShowingResultsLabel.ts b/apps/meteor/client/components/GenericTable/hooks/useShowingResultsLabel.ts index 8ff7d2ac18cf..c610340f28bd 100644 --- a/apps/meteor/client/components/GenericTable/hooks/useShowingResultsLabel.ts +++ b/apps/meteor/client/components/GenericTable/hooks/useShowingResultsLabel.ts @@ -1,19 +1,15 @@ import type { Pagination } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; type Props['showingResultsLabel'] = ComponentProps['showingResultsLabel']> = T extends (...args: any[]) => any ? Parameters : never; export const useShowingResultsLabel = (): ((...params: Props) => string) => { - const { t } = useTranslation(); + const t = useTranslation(); return useCallback( - ({ count, current, itemsPerPage }) => - t('Showing_results_of', { - postProcess: 'sprintf', - sprintf: [current + 1, Math.min(current + itemsPerPage, count), count], - }), + ({ count, current, itemsPerPage }) => t('Showing_results_of', current + 1, Math.min(current + itemsPerPage, count), count), [t], ); }; diff --git a/apps/meteor/client/components/GenericUpsellModal/GenericUpsellModal.tsx b/apps/meteor/client/components/GenericUpsellModal/GenericUpsellModal.tsx index e7ce515ac496..3d68e3f4b6d3 100644 --- a/apps/meteor/client/components/GenericUpsellModal/GenericUpsellModal.tsx +++ b/apps/meteor/client/components/GenericUpsellModal/GenericUpsellModal.tsx @@ -1,8 +1,8 @@ import { Box, Button, Modal } from '@rocket.chat/fuselage'; import type { Keys as IconName } from '@rocket.chat/icons'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactNode, ReactElement, ComponentProps } from 'react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; type GenericUpsellModalProps = { children?: ReactNode; @@ -35,7 +35,7 @@ const GenericUpsellModal = ({ annotation, ...props }: GenericUpsellModalProps) => { - const { t } = useTranslation(); + const t = useTranslation(); return ( diff --git a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx index 6db61b2ec910..2cabfed460bd 100644 --- a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx +++ b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx @@ -1,10 +1,10 @@ import type { IUpload } from '@rocket.chat/core-typings'; import { css } from '@rocket.chat/css-in-js'; import { Box, ButtonGroup, IconButton, Palette, Throbber } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useRef, useState } from 'react'; import { FocusScope } from 'react-aria'; import { createPortal } from 'react-dom'; -import { useTranslation } from 'react-i18next'; import { Keyboard, Navigation, Zoom, A11y } from 'swiper'; import type { SwiperRef } from 'swiper/react'; import { type SwiperClass, Swiper, SwiperSlide } from 'swiper/react'; @@ -108,7 +108,7 @@ const swiperStyle = css` `; export const ImageGallery = ({ images, onClose, loadMore }: { images: IUpload[]; onClose: () => void; loadMore?: () => void }) => { - const { t } = useTranslation(); + const t = useTranslation(); const swiperRef = useRef(null); const [, setSwiperInst] = useState(); const [zoomScale, setZoomScale] = useState(1); diff --git a/apps/meteor/client/components/ImageGallery/ImageGalleryError.tsx b/apps/meteor/client/components/ImageGallery/ImageGalleryError.tsx index 8dcc55a93a48..97d91de95f62 100644 --- a/apps/meteor/client/components/ImageGallery/ImageGalleryError.tsx +++ b/apps/meteor/client/components/ImageGallery/ImageGalleryError.tsx @@ -1,8 +1,8 @@ import { css } from '@rocket.chat/css-in-js'; import { IconButton, ModalBackdrop } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; import { createPortal } from 'react-dom'; -import { useTranslation } from 'react-i18next'; import GenericError from '../GenericError/GenericError'; @@ -14,7 +14,7 @@ const closeButtonStyle = css` `; export const ImageGalleryError = ({ onClose }: { onClose: () => void }) => { - const { t } = useTranslation(); + const t = useTranslation(); return createPortal( diff --git a/apps/meteor/client/components/ImageGallery/ImageGalleryLoading.tsx b/apps/meteor/client/components/ImageGallery/ImageGalleryLoading.tsx index 588605786664..1c057584bd1f 100644 --- a/apps/meteor/client/components/ImageGallery/ImageGalleryLoading.tsx +++ b/apps/meteor/client/components/ImageGallery/ImageGalleryLoading.tsx @@ -1,8 +1,8 @@ import { css } from '@rocket.chat/css-in-js'; import { IconButton, ModalBackdrop, Throbber } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; import { createPortal } from 'react-dom'; -import { useTranslation } from 'react-i18next'; const closeButtonStyle = css` position: absolute; @@ -12,7 +12,7 @@ const closeButtonStyle = css` `; export const ImageGalleryLoading = ({ onClose }: { onClose: () => void }) => { - const { t } = useTranslation(); + const t = useTranslation(); return createPortal( diff --git a/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx index 6f94be4ebc90..cbefeb2c72c1 100644 --- a/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx +++ b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx @@ -1,14 +1,14 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { Callout } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import { useTranslation } from 'react-i18next'; import { usePruneWarningMessage } from '../../hooks/usePruneWarningMessage'; import { withErrorBoundary } from '../withErrorBoundary'; const RetentionPolicyCallout = ({ room }: { room: IRoom }) => { const message = usePruneWarningMessage(room); - const { t } = useTranslation(); + const t = useTranslation(); return ( diff --git a/apps/meteor/client/components/LocalTime.tsx b/apps/meteor/client/components/LocalTime.tsx index 498b2da9711e..100ba2ec6a62 100644 --- a/apps/meteor/client/components/LocalTime.tsx +++ b/apps/meteor/client/components/LocalTime.tsx @@ -1,6 +1,6 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { memo } from 'react'; -import { useTranslation } from 'react-i18next'; import { useUTCClock } from '../hooks/useUTCClock'; @@ -10,7 +10,7 @@ type LocalTimeProps = { const LocalTime = ({ utcOffset }: LocalTimeProps): ReactElement => { const time = useUTCClock(utcOffset); - const { t } = useTranslation(); + const t = useTranslation(); return <>{t('Local_Time_time', { time })}; }; diff --git a/apps/meteor/client/components/MarkdownText.spec.tsx b/apps/meteor/client/components/MarkdownText.spec.tsx index 86ebadad8463..69e7a8c25cd2 100644 --- a/apps/meteor/client/components/MarkdownText.spec.tsx +++ b/apps/meteor/client/components/MarkdownText.spec.tsx @@ -15,26 +15,30 @@ const markdownText = ` **Paragraph text**: *Bold with one asterisk* **Bold with two asterisks** Lorem ipsum dolor sit amet, consectetur adipiscing elit. ## Heading 2 _Italic Text_: _Italic with one underscore_ __Italic with two underscores__ Lorem ipsum dolor sit amet, consectetur adipiscing elit. - ### Heading 3 + ### Heading 3 Lists, Links and elements - **Unordered List** - - List Item 1 - - List Item 2 - - List Item 3 + **Unordered List** + - List Item 1 + - List Item 2 + - List Item 3 - List Item 4 - **Ordered List** + **Ordered List** 1. List Item 1 2. List Item 2 3. List Item 3 4. List Item 4 - **Links:** + **Links:** [Rocket.Chat](rocket.chat) - gabriel.engel@rocket.chat - +55991999999 + gabriel.engel@rocket.chat + +55991999999 \`Inline code\` - \`\`\`typescript + \`\`\`typescript const test = 'this is code' \`\`\` + **Bold text within __Italics__** + *Bold text with single asterik and underscore within _Italics_* + __Italics within **Bold** text__ + _Italics within *Bold* text with single underscore and asterik_ `; it('should render html elements as expected using default parser', async () => { @@ -54,13 +58,17 @@ it('should render html elements as expected using default parser', async () => { 'Italic Text: Italic with one underscore Italic with two underscores Lorem ipsum dolor sit amet', ); expect(normalizedHtml).toContain('

Heading 3

'); - expect(normalizedHtml).toContain('
  • List Item 1
  • List Item 2
  • List Item 3
  • List Item 4'); + expect(normalizedHtml).toContain('
    • List Item 1
    • List Item 2
    • List Item 3
    • List Item 4'); expect(normalizedHtml).toContain('
      1. List Item 1
      2. List Item 2
      3. List Item 3
      4. List Item 4'); expect(normalizedHtml).toContain('Rocket.Chat'); expect(normalizedHtml).toContain('gabriel.engel@rocket.chat'); expect(normalizedHtml).toContain('+55991999999'); expect(normalizedHtml).toContain('Inline code'); expect(normalizedHtml).toContain('
        const test = \'this is code\' 
        '); + expect(normalizedHtml).toContain('Bold text within Italics'); + expect(normalizedHtml).toContain('Bold text with single asterik and underscore within Italics'); + expect(normalizedHtml).toContain('Italics within Bold text'); + expect(normalizedHtml).toContain('Italics within Bold text with single underscore and asterik'); }); it('should render html elements as expected using inline parser', async () => { @@ -89,4 +97,8 @@ it('should render html elements as expected using inline parser', async () => { expect(normalizedHtml).toContain('+55991999999'); expect(normalizedHtml).toContain('Inline code'); expect(normalizedHtml).toContain(`typescript const test = 'this is code'`); + expect(normalizedHtml).toContain('Bold text within Italics'); + expect(normalizedHtml).toContain('Bold text with single asterik and underscore within Italics'); + expect(normalizedHtml).toContain('Italics within Bold text'); + expect(normalizedHtml).toContain('Italics within Bold text with single underscore and asterik'); }); diff --git a/apps/meteor/client/components/MarkdownText.tsx b/apps/meteor/client/components/MarkdownText.tsx index 0b7d2efa780e..16bd1b71430a 100644 --- a/apps/meteor/client/components/MarkdownText.tsx +++ b/apps/meteor/client/components/MarkdownText.tsx @@ -1,10 +1,10 @@ import { Box } from '@rocket.chat/fuselage'; import { isExternal, getBaseURI } from '@rocket.chat/ui-client'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import dompurify from 'dompurify'; import { marked } from 'marked'; import type { ComponentProps } from 'react'; import React, { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; import { renderMessageEmoji } from '../lib/utils/renderMessageEmoji'; @@ -23,10 +23,10 @@ const inlineWithoutBreaks = new marked.Renderer(); const walkTokens = (token: marked.Token) => { const boldPattern = /^\*[^*]+\*$|^\*\*[^*]+\*\*$/; const italicPattern = /^__(?=\S)([\s\S]*?\S)__(?!_)|^_(?=\S)([\s\S]*?\S)_(?!_)/; - if (boldPattern.test(token.raw)) { - token.type = 'strong'; - } else if (italicPattern.test(token.raw)) { - token.type = 'em'; + if (boldPattern.test(token.raw) && token.type === 'em') { + token.type = 'strong' as 'em'; + } else if (italicPattern.test(token.raw) && token.type === 'strong') { + token.type = 'em' as 'strong'; } }; @@ -95,7 +95,7 @@ const MarkdownText = ({ ...props }: MarkdownTextProps) => { const sanitizer = dompurify.sanitize; - const { t } = useTranslation(); + const t = useTranslation(); let markedOptions: marked.MarkedOptions; const schemes = 'http,https,notes,ftp,ftps,tel,mailto,sms,cid'; diff --git a/apps/meteor/client/components/Omnichannel/Definitions/DepartmentsDefinitions.ts b/apps/meteor/client/components/Omnichannel/Definitions/DepartmentsDefinitions.ts new file mode 100644 index 000000000000..8c6a2301bd66 --- /dev/null +++ b/apps/meteor/client/components/Omnichannel/Definitions/DepartmentsDefinitions.ts @@ -0,0 +1,5 @@ +export type DepartmentListItem = { + _id: string; + label: string; + value: string; +}; diff --git a/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.spec.ts b/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.spec.ts new file mode 100644 index 000000000000..173677799b08 --- /dev/null +++ b/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.spec.ts @@ -0,0 +1,111 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { renderHook, waitFor } from '@testing-library/react'; + +import { useDepartmentsList } from './useDepartmentsList'; + +const initialDepartmentsListMock = Array.from(Array(25)).map((_, index) => { + return { + _id: `${index}`, + name: `test_department_${index}`, + enabled: true, + email: `test${index}@email.com`, + showOnRegistration: false, + showOnOfflineForm: false, + type: 'd', + _updatedAt: '2024-09-26T20:05:31.330Z', + offlineMessageChannelName: '', + numAgents: 0, + ancestors: undefined, + parentId: undefined, + }; +}); + +it('should not fetch and add selected department if it is already in the departments list on first fetch', async () => { + const selectedDepartmentMappedToOption = { + _id: '5', + label: 'test_department_5', + value: '5', + }; + + const getDepartmentByIdCallback = jest.fn(); + + const { result } = renderHook( + () => + useDepartmentsList({ + filter: '', + onlyMyDepartments: true, + haveAll: true, + showArchived: true, + selectedDepartment: '5', + }), + { + legacyRoot: true, + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/livechat/department', () => ({ + count: 25, + offset: 0, + total: 25, + departments: initialDepartmentsListMock, + })) + .withEndpoint('GET', `/v1/livechat/department/:_id`, getDepartmentByIdCallback) + .build(), + }, + ); + + expect(getDepartmentByIdCallback).not.toHaveBeenCalled(); + await waitFor(() => expect(result.current.itemsList.items).toContainEqual(selectedDepartmentMappedToOption)); + // The expected length is 26 because the hook will add the 'All' item on run time + await waitFor(() => expect(result.current.itemsList.items.length).toBe(26)); +}); + +it('should fetch and add selected department if it is not part of departments list on first fetch', async () => { + const missingDepartmentRawMock = { + _id: '56f5be8bcf8cd67f9e9bcfdc', + name: 'test_department_25', + enabled: true, + email: 'test25@email.com', + showOnRegistration: false, + showOnOfflineForm: false, + type: 'd', + _updatedAt: '2024-09-26T20:05:31.330Z', + offlineMessageChannelName: '', + numAgents: 0, + ancestors: undefined, + parentId: undefined, + }; + + const missingDepartmentMappedToOption = { + _id: '56f5be8bcf8cd67f9e9bcfdc', + label: 'test_department_25', + value: '56f5be8bcf8cd67f9e9bcfdc', + }; + + const { result } = renderHook( + () => + useDepartmentsList({ + filter: '', + onlyMyDepartments: true, + haveAll: true, + showArchived: true, + selectedDepartment: '56f5be8bcf8cd67f9e9bcfdc', + }), + { + legacyRoot: true, + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/livechat/department', () => ({ + count: 25, + offset: 0, + total: 25, + departments: initialDepartmentsListMock, + })) + .withEndpoint('GET', `/v1/livechat/department/:_id`, () => ({ + department: missingDepartmentRawMock, + })) + .build(), + }, + ); + + await waitFor(() => expect(result.current.itemsList.items).toContainEqual(missingDepartmentMappedToOption)); + // The expected length is 27 because the hook will add the 'All' item and the missing department on run time + await waitFor(() => expect(result.current.itemsList.items.length).toBe(27)); +}); diff --git a/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts b/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts index fd3c0a29effe..d8e1071bf509 100644 --- a/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts +++ b/apps/meteor/client/components/Omnichannel/hooks/useDepartmentsList.ts @@ -4,6 +4,8 @@ import { useCallback, useState } from 'react'; import { useScrollableRecordList } from '../../../hooks/lists/useScrollableRecordList'; import { useComponentDidUpdate } from '../../../hooks/useComponentDidUpdate'; import { RecordList } from '../../../lib/lists/RecordList'; +import type { DepartmentListItem } from '../Definitions/DepartmentsDefinitions'; +import { normalizeDepartments } from '../utils/normalizeDepartments'; type DepartmentsListOptions = { filter: string; @@ -14,12 +16,7 @@ type DepartmentsListOptions = { excludeDepartmentId?: string; enabled?: boolean; showArchived?: boolean; -}; - -type DepartmentListItem = { - _id: string; - label: string; - value: string; + selectedDepartment?: string; }; export const useDepartmentsList = ( @@ -35,6 +32,7 @@ export const useDepartmentsList = ( const reload = useCallback(() => setItemsList(new RecordList()), []); const getDepartments = useEndpoint('GET', '/v1/livechat/department'); + const getDepartment = useEndpoint('GET', '/v1/livechat/department/:_id', { _id: options.selectedDepartment ?? '' }); useComponentDidUpdate(() => { options && reload(); @@ -60,30 +58,32 @@ export const useDepartmentsList = ( } return true; }) - .map(({ _id, name, _updatedAt, ...department }): DepartmentListItem => { - return { + .map( + ({ _id, name, ...department }): DepartmentListItem => ({ _id, label: department.archived ? `${name} [${t('Archived')}]` : name, value: _id, - }; - }); + }), + ); + + const normalizedItems = await normalizeDepartments(items, options.selectedDepartment ?? '', getDepartment); options.haveAll && - items.unshift({ + normalizedItems.unshift({ _id: '', label: t('All'), value: 'all', }); options.haveNone && - items.unshift({ + normalizedItems.unshift({ _id: '', label: t('None'), value: '', }); return { - items, + items: normalizedItems, itemCount: options.departmentId ? total - 1 : total, }; }, @@ -94,9 +94,11 @@ export const useDepartmentsList = ( options.excludeDepartmentId, options.enabled, options.showArchived, + options.selectedDepartment, options.haveAll, options.haveNone, options.departmentId, + getDepartment, t, ], ); diff --git a/apps/meteor/client/components/Omnichannel/modals/ReturnChatQueueModal.tsx b/apps/meteor/client/components/Omnichannel/modals/ReturnChatQueueModal.tsx index b19ccfee1769..04fcb29eed01 100644 --- a/apps/meteor/client/components/Omnichannel/modals/ReturnChatQueueModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/ReturnChatQueueModal.tsx @@ -1,6 +1,6 @@ import { Button, Modal } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import { useTranslation } from 'react-i18next'; type ReturnChatQueueModalProps = { onMoveChat: () => void; @@ -8,7 +8,7 @@ type ReturnChatQueueModalProps = { }; const ReturnChatQueueModal = ({ onCancel, onMoveChat, ...props }: ReturnChatQueueModalProps) => { - const { t } = useTranslation(); + const t = useTranslation(); return ( diff --git a/apps/meteor/client/components/Omnichannel/modals/TranscriptModal.tsx b/apps/meteor/client/components/Omnichannel/modals/TranscriptModal.tsx index de13ee3aa9a4..67deb558c1ed 100644 --- a/apps/meteor/client/components/Omnichannel/modals/TranscriptModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/TranscriptModal.tsx @@ -1,8 +1,8 @@ import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; import { Field, Button, TextInput, Modal, Box, FieldGroup, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useCallback, useEffect } from 'react'; import { useForm } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; type TranscriptModalProps = { email: string; @@ -14,7 +14,7 @@ type TranscriptModalProps = { }; const TranscriptModal = ({ email: emailDefault = '', room, onRequest, onSend, onCancel, onDiscard, ...props }: TranscriptModalProps) => { - const { t } = useTranslation(); + const t = useTranslation(); const { register, diff --git a/apps/meteor/client/components/Omnichannel/utils/normalizeDepartments.ts b/apps/meteor/client/components/Omnichannel/utils/normalizeDepartments.ts new file mode 100644 index 000000000000..6c27db246b1d --- /dev/null +++ b/apps/meteor/client/components/Omnichannel/utils/normalizeDepartments.ts @@ -0,0 +1,24 @@ +import type { EndpointFunction } from '@rocket.chat/ui-contexts'; + +import type { DepartmentListItem } from '../Definitions/DepartmentsDefinitions'; + +export const normalizeDepartments = async ( + departments: DepartmentListItem[], + selectedDepartment: string, + getDepartment: EndpointFunction<'GET', '/v1/livechat/department/:_id'>, +): Promise => { + const isSelectedDepartmentAlreadyOnList = () => departments.some((department) => department._id === selectedDepartment); + if (!selectedDepartment || selectedDepartment === 'all' || isSelectedDepartmentAlreadyOnList()) { + return departments; + } + + try { + const { department: missingDepartment } = await getDepartment({}); + + return missingDepartment + ? [...departments, { _id: missingDepartment._id, label: missingDepartment.name, value: missingDepartment._id }] + : departments; + } catch { + return departments; + } +}; diff --git a/apps/meteor/client/components/Sidebar/Header.tsx b/apps/meteor/client/components/Sidebar/Header.tsx index dcbf7f1cb505..e4bf5a5e7041 100644 --- a/apps/meteor/client/components/Sidebar/Header.tsx +++ b/apps/meteor/client/components/Sidebar/Header.tsx @@ -1,7 +1,7 @@ import { Box, IconButton } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactNode } from 'react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; type HeaderProps = { children?: ReactNode; @@ -10,7 +10,7 @@ type HeaderProps = { }; const Header = ({ title, onClose, children, ...props }: HeaderProps) => { - const { t } = useTranslation(); + const t = useTranslation(); return ( diff --git a/apps/meteor/client/components/Sidebar/SidebarItemsAssembler.tsx b/apps/meteor/client/components/Sidebar/SidebarItemsAssembler.tsx index b588e223d922..45eb6094572a 100644 --- a/apps/meteor/client/components/Sidebar/SidebarItemsAssembler.tsx +++ b/apps/meteor/client/components/Sidebar/SidebarItemsAssembler.tsx @@ -1,6 +1,6 @@ import { Divider } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { Fragment, memo } from 'react'; -import { useTranslation } from 'react-i18next'; import type { SidebarItem } from '../../lib/createSidebarItems'; import { isSidebarItem } from '../../lib/createSidebarItems'; @@ -12,7 +12,7 @@ type SidebarItemsAssemblerProps = { }; const SidebarItemsAssembler = ({ items, currentPath }: SidebarItemsAssemblerProps) => { - const { t, i18n } = useTranslation(); + const t = useTranslation(); return ( <> @@ -25,7 +25,7 @@ const SidebarItemsAssembler = ({ items, currentPath }: SidebarItemsAssemblerProp icon={props.icon} label={t((props.i18nLabel || props.name) as Parameters[0])} currentPath={currentPath} - tag={props.tag && i18n.exists(props.tag) ? t(props.tag) : props.tag} + tag={props.tag && t.has(props.tag) ? t(props.tag) : props.tag} externalUrl={props.externalUrl} badge={props.badge} /> diff --git a/apps/meteor/client/components/SidebarToggler/SidebarTogglerButton.tsx b/apps/meteor/client/components/SidebarToggler/SidebarTogglerButton.tsx index 1eefc50baabd..cd0c25f03d86 100644 --- a/apps/meteor/client/components/SidebarToggler/SidebarTogglerButton.tsx +++ b/apps/meteor/client/components/SidebarToggler/SidebarTogglerButton.tsx @@ -1,6 +1,6 @@ import { Box, IconButton } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import { useTranslation } from 'react-i18next'; import SidebarTogglerBadge from './SidebarTogglerBadge'; @@ -10,7 +10,7 @@ type SideBarTogglerButtonProps = { }; const SideBarTogglerButton = ({ badge, onClick }: SideBarTogglerButtonProps) => { - const { t } = useTranslation(); + const t = useTranslation(); return ( diff --git a/apps/meteor/client/components/TextCopy.tsx b/apps/meteor/client/components/TextCopy.tsx index f9e2ffc62284..467e954ddd65 100644 --- a/apps/meteor/client/components/TextCopy.tsx +++ b/apps/meteor/client/components/TextCopy.tsx @@ -1,7 +1,7 @@ import { Box, Button, Scrollable } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement } from 'react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; import useClipboardWithToast from '../hooks/useClipboardWithToast'; @@ -17,7 +17,7 @@ type TextCopyProps = { } & ComponentProps; const TextCopy = ({ text, wrapper = defaultWrapperRenderer, ...props }: TextCopyProps): ReactElement => { - const { t } = useTranslation(); + const t = useTranslation(); const { copy } = useClipboardWithToast(text); diff --git a/apps/meteor/client/components/TwoFactorModal/TwoFactorPasswordModal.tsx b/apps/meteor/client/components/TwoFactorModal/TwoFactorPasswordModal.tsx index 4867d171aee8..4c91e274de68 100644 --- a/apps/meteor/client/components/TwoFactorModal/TwoFactorPasswordModal.tsx +++ b/apps/meteor/client/components/TwoFactorModal/TwoFactorPasswordModal.tsx @@ -1,8 +1,8 @@ import { Box, PasswordInput, FieldGroup, Field, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage'; import { useAutoFocus, useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ChangeEvent, Ref, SyntheticEvent } from 'react'; import React, { useState } from 'react'; -import { useTranslation } from 'react-i18next'; import GenericModal from '../GenericModal'; import type { OnConfirm } from './TwoFactorModal'; @@ -15,7 +15,7 @@ type TwoFactorPasswordModalProps = { }; const TwoFactorPasswordModal = ({ onConfirm, onClose, invalidAttempt }: TwoFactorPasswordModalProps): ReactElement => { - const { t } = useTranslation(); + const t = useTranslation(); const [code, setCode] = useState(''); const ref = useAutoFocus(); diff --git a/apps/meteor/client/components/TwoFactorModal/TwoFactorTotpModal.tsx b/apps/meteor/client/components/TwoFactorModal/TwoFactorTotpModal.tsx index d6f167be8588..6f36c9c8ce26 100644 --- a/apps/meteor/client/components/TwoFactorModal/TwoFactorTotpModal.tsx +++ b/apps/meteor/client/components/TwoFactorModal/TwoFactorTotpModal.tsx @@ -1,8 +1,8 @@ import { Box, TextInput, Field, FieldGroup, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage'; import { useAutoFocus, useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ChangeEvent, SyntheticEvent } from 'react'; import React, { useState } from 'react'; -import { useTranslation } from 'react-i18next'; import GenericModal from '../GenericModal'; import type { OnConfirm } from './TwoFactorModal'; @@ -15,7 +15,7 @@ type TwoFactorTotpModalProps = { }; const TwoFactorTotpModal = ({ onConfirm, onClose, invalidAttempt }: TwoFactorTotpModalProps): ReactElement => { - const { t } = useTranslation(); + const t = useTranslation(); const [code, setCode] = useState(''); const ref = useAutoFocus(); diff --git a/apps/meteor/client/components/UrlChangeModal.tsx b/apps/meteor/client/components/UrlChangeModal.tsx index dbc152c7fbff..13d0523c92aa 100644 --- a/apps/meteor/client/components/UrlChangeModal.tsx +++ b/apps/meteor/client/components/UrlChangeModal.tsx @@ -1,7 +1,7 @@ import { Box } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; import GenericModal from './GenericModal'; @@ -13,17 +13,14 @@ type UrlChangeModalProps = { }; const UrlChangeModal = ({ onConfirm, siteUrl, currentUrl, onClose }: UrlChangeModalProps): ReactElement => { - const { t } = useTranslation(); + const t = useTranslation(); return (

        diff --git a/apps/meteor/client/components/UserCard/UserCard.tsx b/apps/meteor/client/components/UserCard/UserCard.tsx index 98e1cce2ab78..bf143229cf65 100644 --- a/apps/meteor/client/components/UserCard/UserCard.tsx +++ b/apps/meteor/client/components/UserCard/UserCard.tsx @@ -1,9 +1,9 @@ import { css } from '@rocket.chat/css-in-js'; import { Box, Button, IconButton } from '@rocket.chat/fuselage'; import { UserAvatar } from '@rocket.chat/ui-avatar'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactNode, ComponentProps } from 'react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; import { useEmbeddedLayout } from '../../hooks/useEmbeddedLayout'; import MarkdownText from '../MarkdownText'; @@ -52,7 +52,7 @@ const UserCard = ({ nickname, ...props }: UserCardProps) => { - const { t } = useTranslation(); + const t = useTranslation(); const isLayoutEmbedded = useEmbeddedLayout(); return ( diff --git a/apps/meteor/client/components/UserInfo/UserInfo.tsx b/apps/meteor/client/components/UserInfo/UserInfo.tsx index ac879d21738b..a7f32f82f454 100644 --- a/apps/meteor/client/components/UserInfo/UserInfo.tsx +++ b/apps/meteor/client/components/UserInfo/UserInfo.tsx @@ -1,9 +1,9 @@ import type { IUser, Serialized } from '@rocket.chat/core-typings'; import { Box, Margins, Tag } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ReactNode } from 'react'; import React, { memo } from 'react'; -import { useTranslation } from 'react-i18next'; import { useTimeAgo } from '../../hooks/useTimeAgo'; import { useUserCustomFields } from '../../hooks/useUserCustomFields'; @@ -72,7 +72,7 @@ const UserInfo = ({ reason, ...props }: UserInfoProps): ReactElement => { - const { t } = useTranslation(); + const t = useTranslation(); const timeAgo = useTimeAgo(); const userDisplayName = useUserDisplayName({ name, username }); const userCustomFields = useUserCustomFields(customFields); diff --git a/apps/meteor/client/components/WarningModal.tsx b/apps/meteor/client/components/WarningModal.tsx index db0697f78c0d..00ae92e1d3bd 100644 --- a/apps/meteor/client/components/WarningModal.tsx +++ b/apps/meteor/client/components/WarningModal.tsx @@ -1,7 +1,7 @@ import { Button, Modal } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ReactNode } from 'react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; type WarningModalProps = { text: ReactNode; @@ -13,7 +13,7 @@ type WarningModalProps = { }; const WarningModal = ({ text, confirmText, close, cancel, cancelText, confirm, ...props }: WarningModalProps): ReactElement => { - const { t } = useTranslation(); + const t = useTranslation(); return ( diff --git a/apps/meteor/client/components/dashboards/PeriodSelector.tsx b/apps/meteor/client/components/dashboards/PeriodSelector.tsx index d441fbf2f19a..6977e3334f4f 100644 --- a/apps/meteor/client/components/dashboards/PeriodSelector.tsx +++ b/apps/meteor/client/components/dashboards/PeriodSelector.tsx @@ -1,7 +1,7 @@ import { Select } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; import type { Period } from './periods'; import { getPeriod } from './periods'; @@ -14,9 +14,9 @@ type PeriodSelectorProps = { }; const PeriodSelector = ({ periods, value, name, onChange }: PeriodSelectorProps): ReactElement => { - const { t } = useTranslation(); + const t = useTranslation(); - const options = useMemo<[string, string][]>(() => periods.map((period) => [period, t(getPeriod(period).label)]), [periods, t]); + const options = useMemo<[string, string][]>(() => periods.map((period) => [period, t(...getPeriod(period).label)]), [periods, t]); return (