From f3e8b6b5082934c6d142df442ee42a5ce968c969 Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Tue, 10 Sep 2024 18:59:26 +0000 Subject: [PATCH 01/39] Bump 6.12.1 --- .changeset/bump-patch-1725994766358.md | 5 +++++ yarn.lock | 22 +++++++++++----------- 2 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 .changeset/bump-patch-1725994766358.md diff --git a/.changeset/bump-patch-1725994766358.md b/.changeset/bump-patch-1725994766358.md new file mode 100644 index 000000000000..e1eaa7980afb --- /dev/null +++ b/.changeset/bump-patch-1725994766358.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/yarn.lock b/yarn.lock index d89ba8bce628..a4da870cdb2e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8934,10 +8934,10 @@ __metadata: "@rocket.chat/icons": "*" "@rocket.chat/prettier-config": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 6.0.0-rc.6 - "@rocket.chat/ui-contexts": 10.0.0-rc.6 - "@rocket.chat/ui-kit": 0.36.1-rc.0 - "@rocket.chat/ui-video-conf": 10.0.0-rc.6 + "@rocket.chat/ui-avatar": 6.0.0 + "@rocket.chat/ui-contexts": 10.0.0 + "@rocket.chat/ui-kit": 0.36.1 + "@rocket.chat/ui-video-conf": 10.0.0 "@tanstack/react-query": "*" react: "*" react-dom: "*" @@ -9021,8 +9021,8 @@ __metadata: "@rocket.chat/fuselage-tokens": "*" "@rocket.chat/message-parser": 0.31.29 "@rocket.chat/styled": "*" - "@rocket.chat/ui-client": 10.0.0-rc.6 - "@rocket.chat/ui-contexts": 10.0.0-rc.6 + "@rocket.chat/ui-client": 10.0.0 + "@rocket.chat/ui-contexts": 10.0.0 katex: "*" react: "*" languageName: unknown @@ -10228,7 +10228,7 @@ __metadata: typescript: ~5.3.3 peerDependencies: "@rocket.chat/fuselage": "*" - "@rocket.chat/ui-contexts": 10.0.0-rc.6 + "@rocket.chat/ui-contexts": 10.0.0 react: ~17.0.2 languageName: unknown linkType: soft @@ -10278,7 +10278,7 @@ __metadata: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" - "@rocket.chat/ui-contexts": 10.0.0-rc.6 + "@rocket.chat/ui-contexts": 10.0.0 react: ~17.0.2 languageName: unknown linkType: soft @@ -10448,8 +10448,8 @@ __metadata: "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 6.0.0-rc.6 - "@rocket.chat/ui-contexts": 10.0.0-rc.6 + "@rocket.chat/ui-avatar": 6.0.0 + "@rocket.chat/ui-contexts": 10.0.0 react: ^17.0.2 react-dom: ^17.0.2 languageName: unknown @@ -10536,7 +10536,7 @@ __metadata: peerDependencies: "@rocket.chat/layout": "*" "@rocket.chat/tools": 0.2.2 - "@rocket.chat/ui-contexts": 10.0.0-rc.6 + "@rocket.chat/ui-contexts": 10.0.0 "@tanstack/react-query": "*" react: "*" react-hook-form: "*" From 3cbb9f625292aa03fd30b48e44bd2bb8b3aaf0d9 Mon Sep 17 00:00:00 2001 From: "dionisio-bot[bot]" <117394943+dionisio-bot[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 19:15:29 +0000 Subject: [PATCH 02/39] fix: message parser being slow to process very long messages with too many symbols (#33254) Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> --- .changeset/short-drinks-itch.md | 6 + packages/message-parser/src/grammar.pegjs | 126 ++++---- packages/message-parser/src/utils.ts | 36 ++- packages/message-parser/tests/abuse.test.ts | 268 ++++++++++++++++++ .../message-parser/tests/emphasis.test.ts | 62 ++++ packages/message-parser/tests/link.test.ts | 24 ++ 6 files changed, 453 insertions(+), 69 deletions(-) create mode 100644 .changeset/short-drinks-itch.md create mode 100644 packages/message-parser/tests/abuse.test.ts diff --git a/.changeset/short-drinks-itch.md b/.changeset/short-drinks-itch.md new file mode 100644 index 000000000000..ee57330ffc86 --- /dev/null +++ b/.changeset/short-drinks-itch.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/message-parser': patch +'@rocket.chat/peggy-loader': patch +--- + +Improved the performance of the message parser diff --git a/packages/message-parser/src/grammar.pegjs b/packages/message-parser/src/grammar.pegjs index 182653a9c664..a6cae97facbf 100644 --- a/packages/message-parser/src/grammar.pegjs +++ b/packages/message-parser/src/grammar.pegjs @@ -10,6 +10,7 @@ emoji, emojiUnicode, emoticon, + extractFirstResult, heading, image, inlineCode, @@ -33,6 +34,11 @@ unorderedList, timestamp, } = require('./utils'); + +let skipBold = false; +let skipItalic = false; +let skipStrikethrough = false; +let skipReferences = false; }} Start @@ -212,7 +218,7 @@ Inline = value:(InlineItem / Any)+ EndOfLine? { return reducePlainTexts(value); InlineItem = Whitespace / Timestamp - / References + / MaybeReferences / AutolinkedPhone / AutolinkedEmail / AutolinkedURL @@ -240,7 +246,7 @@ References = "[" title:LinkTitle* "](" href:LinkRef ")" { return title.length ? link(href, reducePlainTexts(title)) : link(href); } / "<" href:LinkRef "|" title:LinkTitle2 ">" { return link(href, [plain(title)]); } -LinkTitle = (Whitespace / EmphasisForReferences) / anyTitle:$(!("](" .) .) { return plain(anyTitle) } +LinkTitle = (Whitespace / Emphasis) / anyTitle:$(!("](" .) .) { return plain(anyTitle) } LinkTitle2 = $([\x20-\x3B\x3D\x3F-\x60\x61-\x7B\x7D-\xFF] / NonASCII)+ @@ -349,14 +355,7 @@ AutoLinkURLBody = !(Extra* (Whitespace / EndOfLine)) . * Emphasis * */ -Emphasis = Bold / Italic / Strikethrough - -/** - * - * Emphasis for References - * -*/ -EmphasisForReferences = BoldForReferences / ItalicForReferences / StrikethroughForReferences +Emphasis = MaybeBold / MaybeItalic / MaybeStrikethrough /** * @@ -365,6 +364,63 @@ EmphasisForReferences = BoldForReferences / ItalicForReferences / StrikethroughF * */ +// This rule is used inside expressions that have a JS code ensuring they always fail, +// Without any pattern to match, peggy will think the rule may end up succedding without consuming any input, which could cause infinite loops +// So this unreachable rule is added to them to satisfy peggy's requirement. +BlockedByJavascript = 'unreachable' + +MaybeBold + = result:( + & { + if (skipBold) { return false; } + skipBold = true; + return true; + } + ( + (text:Bold { skipBold = false; return text; }) + / (& { skipBold = false; return false; } BlockedByJavascript) + ) + ) { return extractFirstResult(result); } + +MaybeStrikethrough + = result:( + & { + if (skipStrikethrough) { return false; } + skipStrikethrough = true; + return true; + } + ( + (text:Strikethrough { skipStrikethrough = false; return text; }) + / (& { skipStrikethrough = false; return false; } BlockedByJavascript) + ) + ) { return extractFirstResult(result); } + +MaybeItalic + = result:( + & { + if (skipItalic) { return false; } + skipItalic = true; + return true; + } + ( + (text:Italic { skipItalic = false; return text; }) + / (& { skipItalic = false; return false; } BlockedByJavascript) + ) + ) { return extractFirstResult(result); } + +MaybeReferences + = result:( + & { + if (skipReferences) { return false; } + skipReferences = true; + return true; + } + ( + (text:References { skipReferences = false; return text; }) + / (& { skipReferences = false; return false; } BlockedByJavascript) + ) + ) { return extractFirstResult(result); } + /* Italic */ Italic = value:$([a-zA-Z0-9]+ [\x5F] [\x5F]?) { return plain(value); } @@ -384,11 +440,11 @@ ItalicContentItems = text:ItalicContentItem+ { return reducePlainTexts(text); } ItalicContentItem = Whitespace / InlineCode - / References + / MaybeReferences / UserMention / ChannelMention - / Bold - / Strikethrough + / MaybeBold + / MaybeStrikethrough / Emoji / Emoticon / AnyItalic @@ -399,52 +455,12 @@ Bold = [\x2A] [\x2A] @BoldContent [\x2A] [\x2A] / [\x2A] @BoldContent [\x2A] BoldContent = text:BoldContentItem+ { return bold(reducePlainTexts(text)); } -BoldContentItem = Whitespace / InlineCode / References / UserMention / ChannelMention / Italic / Strikethrough / Emoji / Emoticon / AnyBold / Line +BoldContentItem = Whitespace / InlineCode / MaybeReferences / UserMention / ChannelMention / MaybeItalic / MaybeStrikethrough / Emoji / Emoticon / AnyBold / Line /* Strike */ Strikethrough = [\x7E] [\x7E] @StrikethroughContent [\x7E] [\x7E] / [\x7E] @StrikethroughContent [\x7E] -StrikethroughContent = text:(Timestamp / InlineCode / Whitespace / References / UserMention / ChannelMention / Italic / Bold / Emoji / Emoticon / AnyStrike / Line)+ { - return strike(reducePlainTexts(text)); - } - -/* Italic for References */ -ItalicForReferences - = value:$([a-zA-Z0-9]+ [\x5F] [\x5F]?) { return plain(value); } - / [\x5F] [\x5F] i:ItalicContentItemsForReferences [\x5F] [\x5F] t:$[a-zA-Z0-9]+ { - return reducePlainTexts([plain('__'), ...i, plain('__'), plain(t)])[0]; - } - / [\x5F] i:ItalicContentItemsForReferences [\x5F] t:$[a-zA-Z]+ { - return reducePlainTexts([plain('_'), ...i, plain('_'), plain(t)])[0]; - } - / [\x5F] [\x5F] @ItalicContentForReferences [\x5F] [\x5F] - / [\x5F] @ItalicContentForReferences [\x5F] - -ItalicContentForReferences = text:ItalicContentItemsForReferences { return italic(text); } - -ItalicContentItemsForReferences = text:ItalicContentItemForReferences+ { return reducePlainTexts(text); } - -ItalicContentItemForReferences - = Whitespace - / UserMention - / ChannelMention - / BoldForReferences - / StrikethroughForReferences - / Emoji - / Emoticon - / AnyItalic - / Line - / InlineCode - -/* Bold for References */ -BoldForReferences = [\x2A] [\x2A] @BoldContentForReferences [\x2A] [\x2A] / [\x2A] @BoldContentForReferences [\x2A] - -BoldContentForReferences = text:(Whitespace / UserMention / ChannelMention / ItalicForReferences / StrikethroughForReferences / Emoji / Emoticon / AnyBold / Line / InlineCode)+ { return bold(reducePlainTexts(text)); } - -/* Strike for References */ -StrikethroughForReferences = [\x7E] [\x7E] @StrikethroughContentForReferences [\x7E] [\x7E] / [\x7E] @StrikethroughContentForReferences [\x7E] - -StrikethroughContentForReferences = text:(Whitespace / UserMention / ChannelMention / ItalicForReferences / BoldForReferences / Emoji / Emoticon / AnyStrike / Line / InlineCode)+ { +StrikethroughContent = text:(Timestamp / Whitespace / InlineCode / MaybeReferences / UserMention / ChannelMention / MaybeItalic / MaybeBold / Emoji / Emoticon / AnyStrike / Line)+ { return strike(reducePlainTexts(text)); } diff --git a/packages/message-parser/src/utils.ts b/packages/message-parser/src/utils.ts index 1f684b56d6ed..6c5d605c5c7a 100644 --- a/packages/message-parser/src/utils.ts +++ b/packages/message-parser/src/utils.ts @@ -198,21 +198,19 @@ const joinEmoji = ( export const reducePlainTexts = ( values: Paragraph['value'] ): Paragraph['value'] => - values - .flatMap((item) => item) - .reduce((result, item, index, values) => { - const next = values[index + 1]; - const current = joinEmoji(item, values[index - 1], next); - const previous: Inlines = result[result.length - 1]; - - if (previous) { - if (current.type === 'PLAIN_TEXT' && current.type === previous.type) { - previous.value += current.value; - return result; - } + values.flat().reduce((result, item, index, values) => { + const next = values[index + 1]; + const current = joinEmoji(item, values[index - 1], next); + const previous: Inlines = result[result.length - 1]; + + if (previous) { + if (current.type === 'PLAIN_TEXT' && current.type === previous.type) { + previous.value += current.value; + return result; } - return [...result, current]; - }, [] as Paragraph['value']); + } + return [...result, current]; + }, [] as Paragraph['value']); export const lineBreak = (): LineBreak => ({ type: 'LINE_BREAK', value: undefined, @@ -249,3 +247,13 @@ export const timestamp = ( fallback: plain(``), }; }; + +export const extractFirstResult = ( + value: Types[keyof Types]['value'] +): Types[keyof Types]['value'] => { + if (typeof value !== 'object' || !Array.isArray(value)) { + return value; + } + + return value.filter((item) => item).shift() as Types[keyof Types]['value']; +}; diff --git a/packages/message-parser/tests/abuse.test.ts b/packages/message-parser/tests/abuse.test.ts new file mode 100644 index 000000000000..c280ee75b4a0 --- /dev/null +++ b/packages/message-parser/tests/abuse.test.ts @@ -0,0 +1,268 @@ +import { parse } from '../src'; +import { paragraph, plain, bold, italic, strike } from '../src/utils'; + +test.each([ + [ + `This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. , REPEATx2 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. REPEAT x3 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. REPEAT x4 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. REPEATx 5 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. , REPEAT x6 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. this can go long for some time, repeat x7 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. ,repeat x8 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some.`, + [ + paragraph([ + plain( + 'This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&' + ), + bold([ + plain('()'), + italic([ + plain( + `+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok` + ), + strike([ + plain( + `, from now on we repeat some. , REPEATx2 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok` + ), + ]), + plain( + ', from now on we repeat some. REPEAT x3 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()' + ), + ]), + plain( + `+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok` + ), + strike([ + plain( + ', from now on we repeat some. REPEAT x4 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()' + ), + italic([ + plain( + `+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. REPEATx 5 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()` + ), + ]), + plain( + `+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok` + ), + ]), + plain( + `, from now on we repeat some. , REPEAT x6 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&` + ), + ]), + plain( + `()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok` + ), + strike([ + plain( + `, from now on we repeat some. this can go long for some time, repeat x7 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()` + ), + italic([ + plain( + `+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. ,repeat x8 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()` + ), + ]), + plain( + `+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok` + ), + ]), + plain(', from now on we repeat some.'), + ]), + ], + ], + [ + '**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__', + [ + paragraph([ + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + plain('__'), + ]), + ], + ], +])('parses %p', (input, output) => { + expect(parse(input)).toMatchObject(output); +}); diff --git a/packages/message-parser/tests/emphasis.test.ts b/packages/message-parser/tests/emphasis.test.ts index e8e72a5882f1..b035999204ce 100644 --- a/packages/message-parser/tests/emphasis.test.ts +++ b/packages/message-parser/tests/emphasis.test.ts @@ -185,6 +185,68 @@ test.each([ ]), ], ], + [ + '**bold ~~and strike~~** **not bold ~~but strike** ~~ not strike~~', + [ + paragraph([ + bold([plain('bold '), strike([plain('and strike')])]), + plain(' **not bold '), + strike([plain('but strike** ')]), + plain(' not strike~~'), + ]), + ], + ], + [ + '**bold** **another bold** ~~strike~~ ~~another strike~~ **bold ~~and strike~~** **not bold ~~but strike** ~~ not strike~~', + [ + paragraph([ + bold([plain('bold')]), + plain(' '), + bold([plain('another bold')]), + plain(' '), + strike([plain('strike')]), + plain(' '), + strike([plain('another strike')]), + plain(' '), + bold([plain('bold '), strike([plain('and strike')])]), + plain(' **not bold '), + strike([plain('but strike** ')]), + plain(' not strike~~'), + ]), + ], + ], + [ + 'some_snake_case_text and even_more', + [paragraph([plain('some_snake_case_text and even_more')])], + ], + [ + 'some_snake_case_text and some __italic__ text', + [ + paragraph([ + plain('some_snake_case_text and some '), + italic([plain('italic')]), + plain(' text'), + ]), + ], + ], + [ + 'some__double__snake__case__text and even_more', + [paragraph([plain('some__double__snake__case__text and even_more')])], + ], + [ + 'some__double__snake__case__text and some __italic__ text', + [ + paragraph([ + plain('some__double__snake__case__text and some '), + italic([plain('italic')]), + plain(' text'), + ]), + ], + ], + [ + 'something__ __and italic__', + [paragraph([plain('something__ '), italic([plain('and italic')])])], + ], ])('parses %p', (input, output) => { expect(parse(input)).toMatchObject(output); }); diff --git a/packages/message-parser/tests/link.test.ts b/packages/message-parser/tests/link.test.ts index 1e083fde5d70..fcf371474bf2 100644 --- a/packages/message-parser/tests/link.test.ts +++ b/packages/message-parser/tests/link.test.ts @@ -584,6 +584,30 @@ Text after line break`, ]), ], ], + [ + '[test **bold** and __italic__](https://rocket.chat)', + [ + paragraph([ + link('https://rocket.chat', [ + plain('test '), + bold([plain('bold')]), + plain(' and '), + italic([plain('italic')]), + ]), + ]), + ], + ], + [ + '[test **bold with __italic__**](https://rocket.chat)', + [ + paragraph([ + link('https://rocket.chat', [ + plain('test '), + bold([plain('bold with '), italic([plain('italic')])]), + ]), + ]), + ], + ], ])('parses %p', (input, output) => { expect(parse(input)).toMatchObject(output); }); From ed7398d93067cf92b48791b4ca5f20cb94c89131 Mon Sep 17 00:00:00 2001 From: "dionisio-bot[bot]" <117394943+dionisio-bot[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 19:34:56 +0000 Subject: [PATCH 03/39] fix: Allow to use the token from `room.v` when requesting transcript instead of finding visitor (#33242) Co-authored-by: Kevin Aleman <11577696+KevLehman@users.noreply.github.com> --- .changeset/four-cherries-kneel.md | 5 +++ .../app/livechat/server/lib/sendTranscript.ts | 17 +++---- apps/meteor/tests/data/livechat/rooms.ts | 4 +- .../end-to-end/api/livechat/11-livechat.ts | 21 +++++++++ .../server/lib/sendTranscript.spec.ts | 45 +++++++++++++------ 5 files changed, 67 insertions(+), 25 deletions(-) create mode 100644 .changeset/four-cherries-kneel.md diff --git a/.changeset/four-cherries-kneel.md b/.changeset/four-cherries-kneel.md new file mode 100644 index 000000000000..095d5af0aa76 --- /dev/null +++ b/.changeset/four-cherries-kneel.md @@ -0,0 +1,5 @@ +--- +"@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/apps/meteor/app/livechat/server/lib/sendTranscript.ts b/apps/meteor/app/livechat/server/lib/sendTranscript.ts index 74032121ee50..bc7c06e0eaae 100644 --- a/apps/meteor/app/livechat/server/lib/sendTranscript.ts +++ b/apps/meteor/app/livechat/server/lib/sendTranscript.ts @@ -3,12 +3,13 @@ import { type IUser, type MessageTypesValues, type IOmnichannelSystemMessage, + type ILivechatVisitor, isFileAttachment, isFileImageAttachment, } from '@rocket.chat/core-typings'; import colors from '@rocket.chat/fuselage-tokens/colors'; import { Logger } from '@rocket.chat/logger'; -import { LivechatRooms, LivechatVisitors, Messages, Uploads, Users } from '@rocket.chat/models'; +import { LivechatRooms, Messages, Uploads, Users } from '@rocket.chat/models'; import { check } from 'meteor/check'; import moment from 'moment-timezone'; @@ -41,16 +42,12 @@ export async function sendTranscript({ const room = await LivechatRooms.findOneById(rid); - const visitor = await LivechatVisitors.getVisitorByToken(token, { - projection: { _id: 1, token: 1, language: 1, username: 1, name: 1 }, - }); - - if (!visitor) { - throw new Error('error-invalid-token'); + const visitor = room?.v as ILivechatVisitor; + if (token !== visitor?.token) { + throw new Error('error-invalid-visitor'); } - // @ts-expect-error - Visitor typings should include language? - const userLanguage = visitor?.language || settings.get('Language') || 'en'; + const userLanguage = settings.get('Language') || 'en'; const timezone = getTimezone(user); logger.debug(`Transcript will be sent using ${timezone} as timezone`); @@ -59,7 +56,7 @@ export async function sendTranscript({ } // allow to only user to send transcripts from their own chats - if (room.t !== 'l' || !room.v || room.v.token !== token) { + if (room.t !== 'l') { throw new Error('error-invalid-room'); } diff --git a/apps/meteor/tests/data/livechat/rooms.ts b/apps/meteor/tests/data/livechat/rooms.ts index 9532fd4214ab..b5d89762c614 100644 --- a/apps/meteor/tests/data/livechat/rooms.ts +++ b/apps/meteor/tests/data/livechat/rooms.ts @@ -33,10 +33,10 @@ export const createLivechatRoom = async (visitorToken: string, extraRoomParams?: return response.body.room; }; -export const createVisitor = (department?: string, visitorName?: string): Promise => +export const createVisitor = (department?: string, visitorName?: string, customEmail?: string): Promise => new Promise((resolve, reject) => { const token = getRandomVisitorToken(); - const email = `${token}@${token}.com`; + const email = customEmail || `${token}@${token}.com`; const phone = `${Math.floor(Math.random() * 10000000000)}`; void request.get(api(`livechat/visitor/${token}`)).end((err: Error, res: DummyResponse) => { if (!err && res && res.body && res.body.visitor) { diff --git a/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts b/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts index c07f7bcecc81..7ce582025538 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts @@ -283,6 +283,27 @@ describe('LIVECHAT - Utils', () => { .send({ token: visitor.token, rid: room._id, email: 'visitor@notadomain.com' }); expect(body).to.have.property('success', true); }); + it('should allow a visitor to get a transcript even if token changed by using an old token that matches room.v', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + await closeOmnichannelRoom(room._id); + const visitor2 = await createVisitor(undefined, undefined, visitor.visitorEmails?.[0].address); + const room2 = await createLivechatRoom(visitor2.token); + await closeOmnichannelRoom(room2._id); + + expect(visitor.token !== visitor2.token).to.be.true; + const { body } = await request + .post(api('livechat/transcript')) + .set(credentials) + .send({ token: visitor.token, rid: room._id, email: 'visitor@notadomain.com' }); + expect(body).to.have.property('success', true); + + const { body: body2 } = await request + .post(api('livechat/transcript')) + .set(credentials) + .send({ token: visitor2.token, rid: room2._id, email: 'visitor@notadomain.com' }); + expect(body2).to.have.property('success', true); + }); }); describe('livechat/transcript/:rid', () => { diff --git a/apps/meteor/tests/unit/app/livechat/server/lib/sendTranscript.spec.ts b/apps/meteor/tests/unit/app/livechat/server/lib/sendTranscript.spec.ts index 64da050cfd88..ca39a64c21a9 100644 --- a/apps/meteor/tests/unit/app/livechat/server/lib/sendTranscript.spec.ts +++ b/apps/meteor/tests/unit/app/livechat/server/lib/sendTranscript.spec.ts @@ -6,9 +6,6 @@ const modelsMock = { LivechatRooms: { findOneById: sinon.stub(), }, - LivechatVisitors: { - getVisitorByToken: sinon.stub(), - }, Messages: { findLivechatClosingMessage: sinon.stub(), findVisibleByRoomIdNotContainingTypesBeforeTs: sinon.stub(), @@ -75,7 +72,6 @@ describe('Send transcript', () => { beforeEach(() => { checkMock.reset(); modelsMock.LivechatRooms.findOneById.reset(); - modelsMock.LivechatVisitors.getVisitorByToken.reset(); modelsMock.Messages.findLivechatClosingMessage.reset(); modelsMock.Messages.findVisibleByRoomIdNotContainingTypesBeforeTs.reset(); modelsMock.Users.findOneById.reset(); @@ -87,11 +83,9 @@ describe('Send transcript', () => { await expect(sendTranscript({})).to.be.rejectedWith(Error); }); it('should throw error when visitor not found', async () => { - modelsMock.LivechatVisitors.getVisitorByToken.resolves(null); await expect(sendTranscript({ rid: 'rid', email: 'email', logger: mockLogger })).to.be.rejectedWith(Error); }); it('should attempt to send an email when params are valid using default subject', async () => { - modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null }); modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { token: 'token' } }); modelsMock.Messages.findVisibleByRoomIdNotContainingTypesBeforeTs.resolves([]); tStub.returns('Conversation Transcript'); @@ -117,7 +111,6 @@ describe('Send transcript', () => { ).to.be.true; }); it('should use provided subject', async () => { - modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null }); modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { token: 'token' } }); modelsMock.Messages.findVisibleByRoomIdNotContainingTypesBeforeTs.resolves([]); @@ -143,7 +136,6 @@ describe('Send transcript', () => { ).to.be.true; }); it('should use subject from setting (when configured) when no subject provided', async () => { - modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null }); modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { token: 'token' } }); modelsMock.Messages.findVisibleByRoomIdNotContainingTypesBeforeTs.resolves([]); mockSettingValues.Livechat_transcript_email_subject = 'A custom subject obtained from setting.get'; @@ -170,36 +162,63 @@ describe('Send transcript', () => { }); it('should fail if room provided is invalid', async () => { modelsMock.LivechatRooms.findOneById.resolves(null); - modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null }); await expect(sendTranscript({ rid: 'rid', email: 'email', logger: mockLogger })).to.be.rejectedWith(Error); }); it('should fail if room provided is of different type', async () => { modelsMock.LivechatRooms.findOneById.resolves({ t: 'c' }); - modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null }); await expect(sendTranscript({ rid: 'rid', email: 'email' })).to.be.rejectedWith(Error); }); it('should fail if room is of valid type, but doesnt doesnt have `v` property', async () => { - modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null }); modelsMock.LivechatRooms.findOneById.resolves({ t: 'l' }); await expect(sendTranscript({ rid: 'rid', email: 'email' })).to.be.rejectedWith(Error); }); it('should fail if room is of valid type, has `v` prop, but it doesnt contain `token`', async () => { - modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null }); modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { otherProp: 'xxx' } }); await expect(sendTranscript({ rid: 'rid', email: 'email' })).to.be.rejectedWith(Error); }); it('should fail if room is of valid type, has `v.token`, but its different from the one on param (room from another visitor)', async () => { - modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null }); modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { token: 'xxx' } }); await expect(sendTranscript({ rid: 'rid', email: 'email', token: 'xveasdf' })).to.be.rejectedWith(Error); }); + + it('should throw an error when token is not the one on room.v', async () => { + modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { token: 'xxx' } }); + + await expect(sendTranscript({ rid: 'rid', email: 'email', token: 'xveasdf' })).to.be.rejectedWith(Error); + }); + it('should work when token matches room.v', async () => { + modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { token: 'token-123' } }); + modelsMock.Messages.findVisibleByRoomIdNotContainingTypesBeforeTs.resolves([]); + delete mockSettingValues.Livechat_transcript_email_subject; + tStub.returns('Conversation Transcript'); + + await sendTranscript({ + rid: 'rid', + token: 'token-123', + email: 'email', + user: { _id: 'x', name: 'x', utcOffset: '-6', username: 'x' }, + }); + + expect(getTimezoneMock.calledWith({ _id: 'x', name: 'x', utcOffset: '-6', username: 'x' })).to.be.true; + expect(modelsMock.Messages.findLivechatClosingMessage.calledWith('rid', { projection: { ts: 1 } })).to.be.true; + expect(modelsMock.Messages.findVisibleByRoomIdNotContainingTypesBeforeTs.called).to.be.true; + expect( + mailerMock.calledWith({ + to: 'email', + from: 'test@rocket.chat', + subject: 'Conversation Transcript', + replyTo: 'test@rocket.chat', + html: '

', + }), + ).to.be.true; + }); }); From c2d7216c1c50254f028456f175675d909084b5ab Mon Sep 17 00:00:00 2001 From: "dionisio-bot[bot]" <117394943+dionisio-bot[bot]@users.noreply.github.com> Date: Thu, 12 Sep 2024 15:25:17 +0000 Subject: [PATCH 04/39] fix: Retention Policy cached settings not updated during upgrade procedure (#33265) Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com> --- .changeset/pink-swans-teach.md | 5 +++++ apps/meteor/server/startup/migrations/xrun.ts | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 .changeset/pink-swans-teach.md diff --git a/.changeset/pink-swans-teach.md b/.changeset/pink-swans-teach.md new file mode 100644 index 000000000000..7c85572a78d5 --- /dev/null +++ b/.changeset/pink-swans-teach.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fixed retention policy max age settings not being respected after upgrade diff --git a/apps/meteor/server/startup/migrations/xrun.ts b/apps/meteor/server/startup/migrations/xrun.ts index 7f6c8a68ad55..0344649f9993 100644 --- a/apps/meteor/server/startup/migrations/xrun.ts +++ b/apps/meteor/server/startup/migrations/xrun.ts @@ -2,6 +2,7 @@ import { Settings } from '@rocket.chat/models'; import type { UpdateResult } from 'mongodb'; import { upsertPermissions } from '../../../app/authorization/server/functions/upsertPermissions'; +import { settings } from '../../../app/settings/server'; import { migrateDatabase, onServerVersionChange } from '../../lib/migrations'; import { ensureCloudWorkspaceRegistered } from '../cloudRegistration'; @@ -23,10 +24,13 @@ const moveRetentionSetting = async () => { { _id: { $in: Array.from(maxAgeSettingMap.keys()) }, value: { $ne: -1 } }, { projection: { _id: 1, value: 1 } }, ).forEach(({ _id, value }) => { - if (!maxAgeSettingMap.has(_id)) { + const newSettingId = maxAgeSettingMap.get(_id); + if (!newSettingId) { throw new Error(`moveRetentionSetting - Setting ${_id} equivalent does not exist`); } + const newValue = convertDaysToMs(Number(value)); + promises.push( Settings.updateOne( { @@ -34,11 +38,17 @@ const moveRetentionSetting = async () => { }, { $set: { - value: convertDaysToMs(Number(value)), + value: newValue, }, }, ), ); + + const currentCache = settings.getSetting(newSettingId); + if (!currentCache) { + return; + } + settings.set({ ...currentCache, value: newValue }); }); await Promise.all(promises); From 4aeb619227b3330ac3a9bc508deafb52cc154d3f Mon Sep 17 00:00:00 2001 From: "dionisio-bot[bot]" <117394943+dionisio-bot[bot]@users.noreply.github.com> Date: Fri, 13 Sep 2024 12:35:41 +0000 Subject: [PATCH 05/39] fix: imported fixes (#33268) Co-authored-by: Julio A. <52619625+julio-cfa@users.noreply.github.com> --- .changeset/many-rules-shout.md | 5 + .../server/functions/canDeleteMessage.ts | 28 +- .../app/otr/server/methods/updateOTRAck.ts | 38 +- .../tabs/AppDetails/AppDetails.tsx | 11 +- .../tabs/AppReleases/AppReleasesItem.tsx | 4 +- .../views/marketplace/lib/purifyOptions.ts | 50 ++ apps/meteor/server/models/raw/Rooms.ts | 4 + apps/meteor/tests/end-to-end/api/methods.ts | 454 ++++++++++++++++++ packages/gazzodown/package.json | 2 + .../gazzodown/src/emoji/EmojiRenderer.tsx | 11 +- packages/gazzodown/src/katex/KatexBlock.tsx | 1 + packages/gazzodown/src/katex/KatexElement.tsx | 1 + .../model-typings/src/models/IRoomsModel.ts | 1 + 13 files changed, 595 insertions(+), 15 deletions(-) create mode 100644 .changeset/many-rules-shout.md create mode 100644 apps/meteor/client/views/marketplace/lib/purifyOptions.ts diff --git a/.changeset/many-rules-shout.md b/.changeset/many-rules-shout.md new file mode 100644 index 000000000000..eacb88108a0f --- /dev/null +++ b/.changeset/many-rules-shout.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) diff --git a/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts b/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts index 7cd953a52bb2..fea37fd1c2a5 100644 --- a/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts +++ b/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts @@ -1,7 +1,8 @@ -import type { IUser } from '@rocket.chat/core-typings'; +import type { IUser, IRoom } from '@rocket.chat/core-typings'; import { Rooms } from '@rocket.chat/models'; import { getValue } from '../../../settings/server/raw'; +import { canAccessRoomAsync } from './canAccessRoom'; import { hasPermissionAsync } from './hasPermission'; const elapsedTime = (ts: Date): number => { @@ -13,6 +14,25 @@ export const canDeleteMessageAsync = async ( uid: string, { u, rid, ts }: { u: Pick; rid: string; ts: Date }, ): Promise => { + const room = await Rooms.findOneById>(rid, { + projection: { + _id: 1, + ro: 1, + unmuted: 1, + t: 1, + teamId: 1, + prid: 1, + }, + }); + + if (!room) { + return false; + } + + if (!(await canAccessRoomAsync(room, { _id: uid }))) { + return false; + } + const forceDelete = await hasPermissionAsync(uid, 'force-delete-message', rid); if (forceDelete) { @@ -45,12 +65,6 @@ export const canDeleteMessageAsync = async ( } } - const room = await Rooms.findOneById(rid, { projection: { ro: 1, unmuted: 1 } }); - - if (!room) { - return false; - } - if (room.ro === true && !(await hasPermissionAsync(uid, 'post-readonly', rid))) { // Unless the user was manually unmuted if (u.username && !(room.unmuted || []).includes(u.username)) { diff --git a/apps/meteor/app/otr/server/methods/updateOTRAck.ts b/apps/meteor/app/otr/server/methods/updateOTRAck.ts index 4fbd182e9d27..64e5e97fa4e5 100644 --- a/apps/meteor/app/otr/server/methods/updateOTRAck.ts +++ b/apps/meteor/app/otr/server/methods/updateOTRAck.ts @@ -1,8 +1,12 @@ import { api } from '@rocket.chat/core-services'; import type { IOTRMessage } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; +import { Rooms } from '@rocket.chat/models'; +import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; + declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -11,10 +15,40 @@ declare module '@rocket.chat/ddp-client' { } Meteor.methods({ - updateOTRAck({ message, ack }) { - if (!Meteor.userId()) { + async updateOTRAck({ message, ack }) { + const uid = Meteor.userId(); + if (!uid) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateOTRAck' }); } + + check(ack, String); + check(message, { + _id: String, + rid: String, + msg: String, + t: String, + ts: Date, + u: { + _id: String, + username: String, + name: String, + }, + }); + + if (message?.t !== 'otr') { + throw new Meteor.Error('error-invalid-message', 'Invalid message type', { method: 'updateOTRAck' }); + } + + const room = await Rooms.findOneByIdAndType(message.rid, 'd', { projection: { t: 1, _id: 1, uids: 1 } }); + + if (!room) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'updateOTRAck' }); + } + + if (!(await canAccessRoomAsync(room, { _id: uid })) || (room.uids && (!message.u._id || !room.uids.includes(message.u._id)))) { + throw new Meteor.Error('error-invalid-user', 'Invalid user, not in room', { method: 'updateOTRAck' }); + } + const acknowledgeMessage: IOTRMessage = { ...message, otrAck: ack }; void api.broadcast('otrAckUpdate', { roomId: message.rid, acknowledgeMessage }); }, diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppDetails/AppDetails.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppDetails/AppDetails.tsx index 8d17f669db83..5f3e427b8391 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppDetails/AppDetails.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppDetails/AppDetails.tsx @@ -2,10 +2,12 @@ import { Box, Callout, Chip, Margins } from '@rocket.chat/fuselage'; import { ExternalLink } from '@rocket.chat/ui-client'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useTranslation } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import React from 'react'; import ScreenshotCarouselAnchor from '../../../components/ScreenshotCarouselAnchor'; import type { AppInfo } from '../../../definitions/AppInfo'; +import { purifyOptions } from '../../../lib/purifyOptions'; import AppDetailsAPIs from './AppDetailsAPIs'; import { normalizeUrl } from './normalizeUrl'; @@ -61,7 +63,14 @@ const AppDetails = ({ app }: AppDetailsProps) => { {t('Description')} - + diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppReleases/AppReleasesItem.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppReleases/AppReleasesItem.tsx index bc27053ea1d2..974b6d148e61 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppReleases/AppReleasesItem.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppReleases/AppReleasesItem.tsx @@ -1,9 +1,11 @@ import { Accordion, Box } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import React from 'react'; import { useTimeAgo } from '../../../../../hooks/useTimeAgo'; +import { purifyOptions } from '../../../lib/purifyOptions'; type IRelease = { version: string; @@ -36,7 +38,7 @@ const AppReleasesItem = ({ release, ...props }: ReleaseItemProps): ReactElement return ( {release.detailedChangelog?.rendered ? ( - + ) : ( {t('No_release_information_provided')} )} diff --git a/apps/meteor/client/views/marketplace/lib/purifyOptions.ts b/apps/meteor/client/views/marketplace/lib/purifyOptions.ts new file mode 100644 index 000000000000..cef1a2c8c707 --- /dev/null +++ b/apps/meteor/client/views/marketplace/lib/purifyOptions.ts @@ -0,0 +1,50 @@ +export const purifyOptions = { + ALLOWED_TAGS: [ + 'b', + 'i', + 'em', + 'strong', + 'br', + 'p', + 'ul', + 'ol', + 'li', + 'article', + 'aside', + 'figure', + 'section', + 'summary', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'hgroup', + 'div', + 'hr', + 'span', + 'wbr', + 'abbr', + 'acronym', + 'cite', + 'code', + 'dfn', + 'figcaption', + 'mark', + 's', + 'samp', + 'sub', + 'sup', + 'var', + 'time', + 'q', + 'del', + 'ins', + 'rp', + 'rt', + 'ruby', + 'bdi', + 'bdo', + ], +}; diff --git a/apps/meteor/server/models/raw/Rooms.ts b/apps/meteor/server/models/raw/Rooms.ts index 8f0c71623a96..8d2805becb3b 100644 --- a/apps/meteor/server/models/raw/Rooms.ts +++ b/apps/meteor/server/models/raw/Rooms.ts @@ -912,6 +912,10 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.findOne(query, options); } + findOneByIdAndType(roomId: IRoom['_id'], type: IRoom['t'], options: FindOptions = {}): Promise { + return this.findOne({ _id: roomId, t: type }, options); + } + setCallStatus(_id: IRoom['_id'], status: IRoom['callStatus']): Promise { const query: Filter = { _id, diff --git a/apps/meteor/tests/end-to-end/api/methods.ts b/apps/meteor/tests/end-to-end/api/methods.ts index 197a71f8e8f2..08945994e438 100644 --- a/apps/meteor/tests/end-to-end/api/methods.ts +++ b/apps/meteor/tests/end-to-end/api/methods.ts @@ -2602,6 +2602,158 @@ describe('Meteor.methods', () => { updateSetting('Message_AllowEditing_BlockEditInMinutes', 0), ]); }); + + describe('message deletion when user is not part of the room', () => { + let ridTestRoom: IRoom['_id']; + let messageIdTestRoom: IMessage['_id']; + let testUser: TestUser; + let testUserCredentials: Credentials; + + before('create room, add new owner, and leave room', async () => { + testUser = await createUser(); + testUserCredentials = await login(testUser.username, password); + const channelName = `methods-test-channel-${Date.now()}`; + + await request + .post(api('groups.create')) + .set(testUserCredentials) + .send({ + name: channelName, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group._id'); + expect(res.body).to.have.nested.property('group.name', channelName); + expect(res.body).to.have.nested.property('group.t', 'p'); + expect(res.body).to.have.nested.property('group.msgs', 0); + ridTestRoom = res.body.group._id; + }); + + await request + .post(methodCall('sendMessage')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'sendMessage', + params: [ + { + _id: `${Date.now() + Math.random()}`, + rid: ridTestRoom, + msg: 'just a random test message', + }, + ], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('result').that.is.an('object'); + messageIdTestRoom = data.result._id; + }); + + await request + .post(methodCall('addUsersToRoom')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'addUsersToRoom', + params: [ + { + rid: ridTestRoom, + users: ['rocket.cat'], + }, + ], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + }); + + await request + .post(api('groups.addOwner')) + .set(testUserCredentials) + .send({ + roomId: ridTestRoom, + userId: 'rocket.cat', + }) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + }); + + await request + .post(api('groups.leave')) + .set(testUserCredentials) + .send({ + roomId: ridTestRoom, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + }); + }); + + it('should not delete a message if the user is no longer member of the room', async () => { + await request + .post(methodCall('deleteMessage')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'deleteMessage', + params: [{ _id: messageIdTestRoom, rid: ridTestRoom }], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('msg', 'result'); + expect(data).to.have.a.property('id', 'id'); + expect(data.error).to.have.a.property('error', 'error-action-not-allowed'); + }); + }); + + it('should not delete a message if the user was never part of the room', async () => { + await request + .post(methodCall('deleteMessage')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'deleteMessage', + params: [{ _id: messageIdTestRoom, rid: ridTestRoom }], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('msg', 'result'); + expect(data).to.have.a.property('id', 'id'); + expect(data.error).to.have.a.property('error', 'error-action-not-allowed'); + }); + }); + + after(() => Promise.all([deleteRoom({ type: 'p', roomId: ridTestRoom }), deleteUser(testUser)])); + }); }); describe('[@setUserActiveStatus]', () => { @@ -3348,6 +3500,7 @@ describe('Meteor.methods', () => { .end(done); }); }); + (IS_EE ? describe : describe.skip)('[@auditGetAuditions] EE', () => { let testUser: TestUser; let testUserCredentials: Credentials; @@ -3451,4 +3604,305 @@ describe('Meteor.methods', () => { }); }); }); + + describe('UpdateOTRAck', () => { + let testUser: TestUser; + let testUser2: TestUser; + let testUserCredentials: Credentials; + let dmTestId: IRoom['_id']; + + before(async () => { + testUser = await createUser(); + testUser2 = await createUser(); + testUserCredentials = await login(testUser.username, password); + }); + + before('create direct conversation between both users', (done) => { + void request + .post(methodCall('createDirectMessage')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'createDirectMessage', + params: [testUser2.username], + id: 'id', + msg: 'method', + }), + }) + .end((_err, res) => { + const result = JSON.parse(res.body.message); + expect(result.result).to.be.an('object'); + expect(result.result).to.have.property('rid').that.is.an('string'); + + dmTestId = result.result.rid; + done(); + }); + }); + + after(() => Promise.all([deleteRoom({ type: 'd', roomId: dmTestId }), deleteUser(testUser), deleteUser(testUser2)])); + + it('should fail if required parameters are not present', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + // rid: 'test', + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: 'test', + username: 'test', + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', "Match error: Missing key 'rid'"); + }); + }); + + it('should fail if required parameters have a different type', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: { $ne: 'test' }, + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: 'test', + username: 'test', + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', 'Match error: Expected string, got object in field rid'); + }); + }); + + it('should fail if "t" is not "otr"', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: 'test', + msg: 'test', + t: 'notOTR', + ts: { $date: 1725447664093 }, + u: { + _id: 'test', + username: 'test', + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', 'Invalid message type [error-invalid-message]'); + }); + }); + + it('should fail if room does not exist', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: 'test', + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: 'test', + username: 'test', + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', 'Invalid room [error-invalid-room]'); + }); + }); + + it('should fail if room is not a DM', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: 'GENERAL', + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: 'test', + username: 'test', + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', 'Invalid room [error-invalid-room]'); + }); + }); + + it('should fail if user is not part of DM room', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: dmTestId, + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: testUser._id, + username: testUser.username, + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', 'Invalid user, not in room [error-invalid-user]'); + }); + }); + + it('should pass if all parameters are present and user is part of DM room', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: dmTestId, + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: testUser._id, + username: testUser.username, + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + expect(res.body).to.have.a.property('success', true); + }); + }); + }); }); diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 909be2dd192c..ec97461077f1 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -72,7 +72,9 @@ "react": "*" }, "dependencies": { + "@types/dompurify": "^3.0.5", "date-fns": "^3.3.1", + "dompurify": "^3.1.6", "highlight.js": "^11.5.1", "react-error-boundary": "^3.1.4" }, diff --git a/packages/gazzodown/src/emoji/EmojiRenderer.tsx b/packages/gazzodown/src/emoji/EmojiRenderer.tsx index 7a4ca5324930..84116361157c 100644 --- a/packages/gazzodown/src/emoji/EmojiRenderer.tsx +++ b/packages/gazzodown/src/emoji/EmojiRenderer.tsx @@ -1,5 +1,6 @@ import { MessageEmoji, ThreadMessageEmoji } from '@rocket.chat/fuselage'; import type * as MessageParser from '@rocket.chat/message-parser'; +import DOMPurify from 'dompurify'; import { ReactElement, useMemo, useContext, memo } from 'react'; import { MarkupInteractionContext } from '../MarkupInteractionContext'; @@ -14,10 +15,12 @@ const EmojiRenderer = ({ big = false, preview = false, ...emoji }: EmojiProps): const fallback = useMemo(() => ('unicode' in emoji ? emoji.unicode : `:${emoji.shortCode ?? emoji.value.value}:`), [emoji]); + const sanitizedFallback = DOMPurify.sanitize(fallback); + const descriptors = useMemo(() => { - const detected = detectEmoji?.(fallback); + const detected = detectEmoji?.(sanitizedFallback); return detected?.length !== 0 ? detected : undefined; - }, [detectEmoji, fallback]); + }, [detectEmoji, sanitizedFallback]); return ( <> @@ -34,8 +37,8 @@ const EmojiRenderer = ({ big = false, preview = false, ...emoji }: EmojiProps): )} )) ?? ( - - {fallback} + + {sanitizedFallback} )} diff --git a/packages/gazzodown/src/katex/KatexBlock.tsx b/packages/gazzodown/src/katex/KatexBlock.tsx index 5913185d3969..267b310b3897 100644 --- a/packages/gazzodown/src/katex/KatexBlock.tsx +++ b/packages/gazzodown/src/katex/KatexBlock.tsx @@ -15,6 +15,7 @@ const KatexBlock = ({ code }: KatexBlockProps): ReactElement => { macros: { '\\href': '\\@secondoftwo', }, + maxSize: 100, }), [code], ); diff --git a/packages/gazzodown/src/katex/KatexElement.tsx b/packages/gazzodown/src/katex/KatexElement.tsx index 3595f698f7ae..099c2f82cf8c 100644 --- a/packages/gazzodown/src/katex/KatexElement.tsx +++ b/packages/gazzodown/src/katex/KatexElement.tsx @@ -15,6 +15,7 @@ const KatexElement = ({ code }: KatexElementProps): ReactElement => { macros: { '\\href': '\\@secondoftwo', }, + maxSize: 100, }), [code], ); diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index fab810ccd30c..57a03631c1f0 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -195,6 +195,7 @@ export interface IRoomsModel extends IBaseModel { setE2eKeyId(roomId: string, e2eKeyId: string, options?: FindOptions): Promise; findOneByImportId(importId: string, options?: FindOptions): Promise; findOneByNameAndNotId(name: string, rid: string): Promise; + findOneByIdAndType(roomId: IRoom['_id'], type: IRoom['t'], options?: FindOptions): Promise; findOneByDisplayName(displayName: string, options?: FindOptions): Promise; findOneByNameAndType( name: string, From e22ea8f18a26aff15a1280cb6dce99510497b5ff Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Fri, 13 Sep 2024 15:15:24 +0000 Subject: [PATCH 06/39] Release 6.12.1 [no ci] --- .changeset/bump-patch-1725994766358.md | 5 --- .changeset/four-cherries-kneel.md | 5 --- .changeset/many-rules-shout.md | 5 --- .changeset/pink-swans-teach.md | 5 --- .changeset/short-drinks-itch.md | 6 --- apps/meteor/CHANGELOG.md | 41 +++++++++++++++++++ apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/ee/server/services/CHANGELOG.md | 14 +++++++ apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- ee/apps/account-service/CHANGELOG.md | 13 ++++++ ee/apps/account-service/package.json | 2 +- ee/apps/authorization-service/CHANGELOG.md | 13 ++++++ ee/apps/authorization-service/package.json | 2 +- ee/apps/ddp-streamer/CHANGELOG.md | 14 +++++++ ee/apps/ddp-streamer/package.json | 2 +- ee/apps/omnichannel-transcript/CHANGELOG.md | 14 +++++++ ee/apps/omnichannel-transcript/package.json | 2 +- ee/apps/presence-service/CHANGELOG.md | 13 ++++++ ee/apps/presence-service/package.json | 2 +- ee/apps/queue-worker/CHANGELOG.md | 13 ++++++ ee/apps/queue-worker/package.json | 2 +- ee/apps/stream-hub-service/CHANGELOG.md | 12 ++++++ ee/apps/stream-hub-service/package.json | 2 +- ee/packages/license/CHANGELOG.md | 9 ++++ ee/packages/license/package.json | 2 +- ee/packages/omnichannel-services/CHANGELOG.md | 14 +++++++ ee/packages/omnichannel-services/package.json | 2 +- ee/packages/pdf-worker/CHANGELOG.md | 9 ++++ ee/packages/pdf-worker/package.json | 2 +- ee/packages/presence/CHANGELOG.md | 11 +++++ ee/packages/presence/package.json | 2 +- package.json | 2 +- packages/api-client/CHANGELOG.md | 10 +++++ packages/api-client/package.json | 2 +- packages/apps/CHANGELOG.md | 10 +++++ packages/apps/package.json | 2 +- packages/core-services/CHANGELOG.md | 12 ++++++ packages/core-services/package.json | 2 +- packages/core-typings/CHANGELOG.md | 9 ++++ packages/core-typings/package.json | 2 +- packages/cron/CHANGELOG.md | 10 +++++ packages/cron/package.json | 2 +- packages/ddp-client/CHANGELOG.md | 11 +++++ packages/ddp-client/package.json | 2 +- packages/fuselage-ui-kit/CHANGELOG.md | 13 ++++++ packages/fuselage-ui-kit/package.json | 8 ++-- packages/gazzodown/CHANGELOG.md | 12 ++++++ packages/gazzodown/package.json | 8 ++-- packages/instance-status/CHANGELOG.md | 9 ++++ packages/instance-status/package.json | 2 +- packages/livechat/CHANGELOG.md | 10 +++++ packages/livechat/package.json | 2 +- packages/message-parser/CHANGELOG.md | 6 +++ packages/message-parser/package.json | 2 +- packages/model-typings/CHANGELOG.md | 9 ++++ packages/model-typings/package.json | 2 +- packages/models/CHANGELOG.md | 9 ++++ packages/models/package.json | 2 +- packages/peggy-loader/CHANGELOG.md | 6 +++ packages/peggy-loader/package.json | 2 +- packages/rest-typings/CHANGELOG.md | 10 +++++ packages/rest-typings/package.json | 2 +- packages/ui-avatar/CHANGELOG.md | 9 ++++ packages/ui-avatar/package.json | 4 +- packages/ui-client/CHANGELOG.md | 9 ++++ packages/ui-client/package.json | 4 +- packages/ui-contexts/CHANGELOG.md | 11 +++++ packages/ui-contexts/package.json | 2 +- packages/ui-video-conf/CHANGELOG.md | 10 +++++ packages/ui-video-conf/package.json | 6 +-- packages/uikit-playground/CHANGELOG.md | 11 +++++ packages/uikit-playground/package.json | 2 +- packages/web-ui-registration/CHANGELOG.md | 9 ++++ packages/web-ui-registration/package.json | 4 +- yarn.lock | 18 ++++++++ 76 files changed, 460 insertions(+), 73 deletions(-) delete mode 100644 .changeset/bump-patch-1725994766358.md delete mode 100644 .changeset/four-cherries-kneel.md delete mode 100644 .changeset/many-rules-shout.md delete mode 100644 .changeset/pink-swans-teach.md delete mode 100644 .changeset/short-drinks-itch.md diff --git a/.changeset/bump-patch-1725994766358.md b/.changeset/bump-patch-1725994766358.md deleted file mode 100644 index e1eaa7980afb..000000000000 --- a/.changeset/bump-patch-1725994766358.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Bump @rocket.chat/meteor version. 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/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/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/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/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 97a246abaca6..c7029b774cc0 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,46 @@ # @rocket.chat/meteor +## 6.12.1 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- Bump @rocket.chat/meteor version. + +- ([#33242](https://github.com/RocketChat/Rocket.Chat/pull/33242) by [@dionisio-bot](https://github.com/dionisio-bot)) 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. + +- ([#33268](https://github.com/RocketChat/Rocket.Chat/pull/33268) by [@dionisio-bot](https://github.com/dionisio-bot)) Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) + +- ([#33265](https://github.com/RocketChat/Rocket.Chat/pull/33265) by [@dionisio-bot](https://github.com/dionisio-bot)) fixed retention policy max age settings not being respected after upgrade + +-
Updated dependencies [3cbb9f6252]: + + - @rocket.chat/message-parser@0.31.30 + - @rocket.chat/core-services@0.6.1 + - @rocket.chat/core-typings@6.12.1 + - @rocket.chat/gazzodown@10.0.1 + - @rocket.chat/rest-typings@6.12.1 + - @rocket.chat/omnichannel-services@0.3.4 + - @rocket.chat/presence@0.2.7 + - @rocket.chat/license@0.2.7 + - @rocket.chat/pdf-worker@0.2.4 + - @rocket.chat/api-client@0.2.7 + - @rocket.chat/apps@0.1.7 + - @rocket.chat/cron@0.1.7 + - @rocket.chat/fuselage-ui-kit@10.0.1 + - @rocket.chat/model-typings@0.7.1 + - @rocket.chat/ui-contexts@10.0.1 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.2.4 + - @rocket.chat/ui-theming@0.2.1 + - @rocket.chat/ui-avatar@6.0.1 + - @rocket.chat/ui-client@10.0.1 + - @rocket.chat/ui-video-conf@10.0.1 + - @rocket.chat/web-ui-registration@10.0.1 + - @rocket.chat/instance-status@0.1.7 +
+ ## 6.12.0 ### Minor Changes diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index dce2b0cfa1e0..7285c1c94e10 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "6.12.0" + "version": "6.12.1" } diff --git a/apps/meteor/ee/server/services/CHANGELOG.md b/apps/meteor/ee/server/services/CHANGELOG.md index 1d6e058d7597..1b9cf6109764 100644 --- a/apps/meteor/ee/server/services/CHANGELOG.md +++ b/apps/meteor/ee/server/services/CHANGELOG.md @@ -1,5 +1,19 @@ # rocketchat-services +## 1.3.4 + +### Patch Changes + +-
Updated dependencies [3cbb9f6252]: + + - @rocket.chat/message-parser@0.31.30 + - @rocket.chat/core-services@0.6.1 + - @rocket.chat/core-typings@6.12.1 + - @rocket.chat/rest-typings@6.12.1 + - @rocket.chat/model-typings@0.7.1 + - @rocket.chat/models@0.2.4 +
+ ## 1.3.3 ### Patch Changes diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 5535234bfaa1..3d6b7decbf5c 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -1,7 +1,7 @@ { "name": "rocketchat-services", "private": true, - "version": "1.3.3", + "version": "1.3.4", "description": "Rocket.Chat Authorization service", "main": "index.js", "scripts": { diff --git a/apps/meteor/package.json b/apps/meteor/package.json index a5924efe23ce..5796920e8650 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "6.12.0", + "version": "6.12.1", "private": true, "author": { "name": "Rocket.Chat", diff --git a/ee/apps/account-service/CHANGELOG.md b/ee/apps/account-service/CHANGELOG.md index 8d306d7b1c17..5897afdf4f24 100644 --- a/ee/apps/account-service/CHANGELOG.md +++ b/ee/apps/account-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/account-service +## 0.4.7 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-services@0.6.1 + - @rocket.chat/core-typings@6.12.1 + - @rocket.chat/rest-typings@6.12.1 + - @rocket.chat/model-typings@0.7.1 + - @rocket.chat/models@0.2.4 +
+ ## 0.4.6 ### Patch Changes diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index 226f531264bb..0c2dccb08bc9 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/account-service", "private": true, - "version": "0.4.6", + "version": "0.4.7", "description": "Rocket.Chat Account service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/authorization-service/CHANGELOG.md b/ee/apps/authorization-service/CHANGELOG.md index 494c052e8cf8..0d505da757a9 100644 --- a/ee/apps/authorization-service/CHANGELOG.md +++ b/ee/apps/authorization-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/authorization-service +## 0.4.7 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-services@0.6.1 + - @rocket.chat/core-typings@6.12.1 + - @rocket.chat/rest-typings@6.12.1 + - @rocket.chat/model-typings@0.7.1 + - @rocket.chat/models@0.2.4 +
+ ## 0.4.6 ### Patch Changes diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index e614742754fe..7c4f3d8abafd 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/authorization-service", "private": true, - "version": "0.4.6", + "version": "0.4.7", "description": "Rocket.Chat Authorization service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/ddp-streamer/CHANGELOG.md b/ee/apps/ddp-streamer/CHANGELOG.md index 97565b8bde88..ebd5e0d3ce93 100644 --- a/ee/apps/ddp-streamer/CHANGELOG.md +++ b/ee/apps/ddp-streamer/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/ddp-streamer +## 0.3.7 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-services@0.6.1 + - @rocket.chat/core-typings@6.12.1 + - @rocket.chat/rest-typings@6.12.1 + - @rocket.chat/model-typings@0.7.1 + - @rocket.chat/models@0.2.4 + - @rocket.chat/instance-status@0.1.7 +
+ ## 0.3.6 ### Patch Changes diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 5b620add788a..8af959506d40 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/ddp-streamer", "private": true, - "version": "0.3.6", + "version": "0.3.7", "description": "Rocket.Chat DDP-Streamer service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/omnichannel-transcript/CHANGELOG.md b/ee/apps/omnichannel-transcript/CHANGELOG.md index 21fc879f9fd9..885b7746b929 100644 --- a/ee/apps/omnichannel-transcript/CHANGELOG.md +++ b/ee/apps/omnichannel-transcript/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/omnichannel-transcript +## 0.4.7 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-services@0.6.1 + - @rocket.chat/core-typings@6.12.1 + - @rocket.chat/omnichannel-services@0.3.4 + - @rocket.chat/pdf-worker@0.2.4 + - @rocket.chat/model-typings@0.7.1 + - @rocket.chat/models@0.2.4 +
+ ## 0.4.6 ### Patch Changes diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 057b8b2911bb..fb995abe4083 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/omnichannel-transcript", "private": true, - "version": "0.4.6", + "version": "0.4.7", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/presence-service/CHANGELOG.md b/ee/apps/presence-service/CHANGELOG.md index a1ed37bb6fde..9af1812bbfb6 100644 --- a/ee/apps/presence-service/CHANGELOG.md +++ b/ee/apps/presence-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/presence-service +## 0.4.7 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-services@0.6.1 + - @rocket.chat/core-typings@6.12.1 + - @rocket.chat/presence@0.2.7 + - @rocket.chat/model-typings@0.7.1 + - @rocket.chat/models@0.2.4 +
+ ## 0.4.6 ### Patch Changes diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index 94854d45cb40..1069cb43c155 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/presence-service", "private": true, - "version": "0.4.6", + "version": "0.4.7", "description": "Rocket.Chat Presence service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/queue-worker/CHANGELOG.md b/ee/apps/queue-worker/CHANGELOG.md index 16dc2590f38b..ae79b6e2e5d4 100644 --- a/ee/apps/queue-worker/CHANGELOG.md +++ b/ee/apps/queue-worker/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/queue-worker +## 0.4.7 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-services@0.6.1 + - @rocket.chat/core-typings@6.12.1 + - @rocket.chat/omnichannel-services@0.3.4 + - @rocket.chat/model-typings@0.7.1 + - @rocket.chat/models@0.2.4 +
+ ## 0.4.6 ### Patch Changes diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index 01ffc2cb4aa0..574b06ec6dcf 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/queue-worker", "private": true, - "version": "0.4.6", + "version": "0.4.7", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/stream-hub-service/CHANGELOG.md b/ee/apps/stream-hub-service/CHANGELOG.md index ccbb83e7de0b..98e7bcecc2e2 100644 --- a/ee/apps/stream-hub-service/CHANGELOG.md +++ b/ee/apps/stream-hub-service/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/stream-hub-service +## 0.4.7 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-services@0.6.1 + - @rocket.chat/core-typings@6.12.1 + - @rocket.chat/model-typings@0.7.1 + - @rocket.chat/models@0.2.4 +
+ ## 0.4.6 ### Patch Changes diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index 5f96b74cab8b..c74b037c256f 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/stream-hub-service", "private": true, - "version": "0.4.6", + "version": "0.4.7", "description": "Rocket.Chat Stream Hub service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/packages/license/CHANGELOG.md b/ee/packages/license/CHANGELOG.md index 181b6dfb0e7f..dacd1671ba3e 100644 --- a/ee/packages/license/CHANGELOG.md +++ b/ee/packages/license/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/license +## 0.2.7 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.12.1 +
+ ## 0.2.6 ### Patch Changes diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 35dfc7f8dc7a..cf95c63a7327 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/license", - "version": "0.2.6", + "version": "0.2.7", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/omnichannel-services/CHANGELOG.md b/ee/packages/omnichannel-services/CHANGELOG.md index f54493a31cd1..946060b52dd9 100644 --- a/ee/packages/omnichannel-services/CHANGELOG.md +++ b/ee/packages/omnichannel-services/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/omnichannel-services +## 0.3.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-services@0.6.1 + - @rocket.chat/core-typings@6.12.1 + - @rocket.chat/rest-typings@6.12.1 + - @rocket.chat/pdf-worker@0.2.4 + - @rocket.chat/model-typings@0.7.1 + - @rocket.chat/models@0.2.4 +
+ ## 0.3.3 ### Patch Changes diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index 2110c254bf06..2678bb9a09da 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/omnichannel-services", - "version": "0.3.3", + "version": "0.3.4", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/pdf-worker/CHANGELOG.md b/ee/packages/pdf-worker/CHANGELOG.md index ccc01c92e84f..92f2a8e72554 100644 --- a/ee/packages/pdf-worker/CHANGELOG.md +++ b/ee/packages/pdf-worker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/pdf-worker +## 0.2.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.12.1 +
+ ## 0.2.3 ### Patch Changes diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index cf96d08efc8b..1a47e3c09395 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/pdf-worker", - "version": "0.2.3", + "version": "0.2.4", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/presence/CHANGELOG.md b/ee/packages/presence/CHANGELOG.md index a4effccb5f2b..2628fcd41103 100644 --- a/ee/packages/presence/CHANGELOG.md +++ b/ee/packages/presence/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/presence +## 0.2.7 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-services@0.6.1 + - @rocket.chat/core-typings@6.12.1 + - @rocket.chat/models@0.2.4 +
+ ## 0.2.6 ### Patch Changes diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index cce1869e562f..4b6f39759dd5 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/presence", - "version": "0.2.6", + "version": "0.2.7", "private": true, "devDependencies": { "@babel/core": "~7.22.20", diff --git a/package.json b/package.json index 3f40f1d4ac3b..9be25273f5e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "6.12.0", + "version": "6.12.1", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/api-client/CHANGELOG.md b/packages/api-client/CHANGELOG.md index 50096d81e901..211a2eba796d 100644 --- a/packages/api-client/CHANGELOG.md +++ b/packages/api-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/api-client +## 0.2.7 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.12.1 + - @rocket.chat/rest-typings@6.12.1 +
+ ## 0.2.6 ### Patch Changes diff --git a/packages/api-client/package.json b/packages/api-client/package.json index 1ea3011a0010..3756f8526acf 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/api-client", - "version": "0.2.6", + "version": "0.2.7", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.12", diff --git a/packages/apps/CHANGELOG.md b/packages/apps/CHANGELOG.md index adb1b989bad0..ce7fc6c17d43 100644 --- a/packages/apps/CHANGELOG.md +++ b/packages/apps/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/apps +## 0.1.7 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.12.1 + - @rocket.chat/model-typings@0.7.1 +
+ ## 0.1.6 ### Patch Changes diff --git a/packages/apps/package.json b/packages/apps/package.json index 8b26679f7d5c..5a8bd648729e 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps", - "version": "0.1.6", + "version": "0.1.7", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/core-services/CHANGELOG.md b/packages/core-services/CHANGELOG.md index a73f804408ba..093ce016595e 100644 --- a/packages/core-services/CHANGELOG.md +++ b/packages/core-services/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/core-services +## 0.6.1 + +### Patch Changes + +-
Updated dependencies [3cbb9f6252]: + + - @rocket.chat/message-parser@0.31.30 + - @rocket.chat/core-typings@6.12.1 + - @rocket.chat/rest-typings@6.12.1 + - @rocket.chat/models@0.2.4 +
+ ## 0.6.0 ### Minor Changes diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 4de22804d3d0..09d63653d1cb 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/core-services", - "version": "0.6.0", + "version": "0.6.1", "private": true, "devDependencies": { "@babel/core": "~7.22.20", diff --git a/packages/core-typings/CHANGELOG.md b/packages/core-typings/CHANGELOG.md index ac2c861590c0..7c513ef0bdd5 100644 --- a/packages/core-typings/CHANGELOG.md +++ b/packages/core-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/core-typings +## 6.12.1 + +### Patch Changes + +-
Updated dependencies [3cbb9f6252]: + + - @rocket.chat/message-parser@0.31.30 +
+ ## 6.12.0 ### Minor Changes diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 9dddf874ed40..0970df8d81e4 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", - "version": "6.12.0", + "version": "6.12.1", "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", "eslint": "~8.45.0", diff --git a/packages/cron/CHANGELOG.md b/packages/cron/CHANGELOG.md index 9c0c43c6f16e..dd7e8a5734d8 100644 --- a/packages/cron/CHANGELOG.md +++ b/packages/cron/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/cron +## 0.1.7 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.12.1 + - @rocket.chat/models@0.2.4 +
+ ## 0.1.6 ### Patch Changes diff --git a/packages/cron/package.json b/packages/cron/package.json index 015ae8280045..472f88b8a84d 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/cron", - "version": "0.1.6", + "version": "0.1.7", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/ddp-client/CHANGELOG.md b/packages/ddp-client/CHANGELOG.md index 29e062a1374d..3844c4431e8e 100644 --- a/packages/ddp-client/CHANGELOG.md +++ b/packages/ddp-client/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ddp-client +## 0.3.7 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.12.1 + - @rocket.chat/rest-typings@6.12.1 + - @rocket.chat/api-client@0.2.7 +
+ ## 0.3.6 ### Patch Changes diff --git a/packages/ddp-client/package.json b/packages/ddp-client/package.json index 55c2d748bec4..8241a0a5e515 100644 --- a/packages/ddp-client/package.json +++ b/packages/ddp-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ddp-client", - "version": "0.3.6", + "version": "0.3.7", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.12", diff --git a/packages/fuselage-ui-kit/CHANGELOG.md b/packages/fuselage-ui-kit/CHANGELOG.md index 41cc87932393..6c5e6c9bbdd0 100644 --- a/packages/fuselage-ui-kit/CHANGELOG.md +++ b/packages/fuselage-ui-kit/CHANGELOG.md @@ -1,5 +1,18 @@ # Change Log +## 10.0.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.12.1 + - @rocket.chat/gazzodown@10.0.1 + - @rocket.chat/ui-contexts@10.0.1 + - @rocket.chat/ui-avatar@6.0.1 + - @rocket.chat/ui-video-conf@10.0.1 +
+ ## 10.0.0 ### Patch Changes diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index d84b1b595335..0dfb8ea74aa2 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/fuselage-ui-kit", "private": true, - "version": "10.0.0", + "version": "10.0.1", "description": "UiKit elements for Rocket.Chat Apps built under Fuselage design system", "homepage": "https://rocketchat.github.io/Rocket.Chat.Fuselage/", "author": { @@ -50,10 +50,10 @@ "@rocket.chat/icons": "*", "@rocket.chat/prettier-config": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-avatar": "6.0.0", - "@rocket.chat/ui-contexts": "10.0.0", + "@rocket.chat/ui-avatar": "6.0.1", + "@rocket.chat/ui-contexts": "10.0.1", "@rocket.chat/ui-kit": "0.36.1", - "@rocket.chat/ui-video-conf": "10.0.0", + "@rocket.chat/ui-video-conf": "10.0.1", "@tanstack/react-query": "*", "react": "*", "react-dom": "*" diff --git a/packages/gazzodown/CHANGELOG.md b/packages/gazzodown/CHANGELOG.md index 74cd44cc291c..1437387ad146 100644 --- a/packages/gazzodown/CHANGELOG.md +++ b/packages/gazzodown/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/gazzodown +## 10.0.1 + +### Patch Changes + +-
Updated dependencies [3cbb9f6252]: + + - @rocket.chat/message-parser@0.31.30 + - @rocket.chat/core-typings@6.12.1 + - @rocket.chat/ui-contexts@10.0.1 + - @rocket.chat/ui-client@10.0.1 +
+ ## 10.0.0 ### Patch Changes diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index ec97461077f1..622bfdc35c36 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/gazzodown", - "version": "10.0.0", + "version": "10.0.1", "private": true, "devDependencies": { "@babel/core": "~7.22.20", @@ -64,10 +64,10 @@ "@rocket.chat/css-in-js": "*", "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-tokens": "*", - "@rocket.chat/message-parser": "0.31.29", + "@rocket.chat/message-parser": "0.31.30", "@rocket.chat/styled": "*", - "@rocket.chat/ui-client": "10.0.0", - "@rocket.chat/ui-contexts": "10.0.0", + "@rocket.chat/ui-client": "10.0.1", + "@rocket.chat/ui-contexts": "10.0.1", "katex": "*", "react": "*" }, diff --git a/packages/instance-status/CHANGELOG.md b/packages/instance-status/CHANGELOG.md index bebd0fd56b85..71a1ba83c2ca 100644 --- a/packages/instance-status/CHANGELOG.md +++ b/packages/instance-status/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/instance-status +## 0.1.7 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/models@0.2.4 +
+ ## 0.1.6 ### Patch Changes diff --git a/packages/instance-status/package.json b/packages/instance-status/package.json index 686d4c710b1d..e95b6ef11489 100644 --- a/packages/instance-status/package.json +++ b/packages/instance-status/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/instance-status", - "version": "0.1.6", + "version": "0.1.7", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/livechat/CHANGELOG.md b/packages/livechat/CHANGELOG.md index 598fae50d68d..05ad7808e1b2 100644 --- a/packages/livechat/CHANGELOG.md +++ b/packages/livechat/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/livechat Change Log +## 1.19.4 + +### Patch Changes + +-
Updated dependencies [3cbb9f6252]: + + - @rocket.chat/message-parser@0.31.30 + - @rocket.chat/gazzodown@10.0.1 +
+ ## 1.19.3 ### Patch Changes diff --git a/packages/livechat/package.json b/packages/livechat/package.json index 564746ae9923..814913801dc5 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/livechat", - "version": "1.19.3", + "version": "1.19.4", "files": [ "/build" ], diff --git a/packages/message-parser/CHANGELOG.md b/packages/message-parser/CHANGELOG.md index 39c82e350b58..a00ffd3db882 100644 --- a/packages/message-parser/CHANGELOG.md +++ b/packages/message-parser/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 0.31.30 + +### Patch Changes + +- ([#33254](https://github.com/RocketChat/Rocket.Chat/pull/33254) by [@dionisio-bot](https://github.com/dionisio-bot)) Improved the performance of the message parser + ## 0.31.29 ### Patch Changes diff --git a/packages/message-parser/package.json b/packages/message-parser/package.json index fd7e88dc1d86..c566e6b2c5c7 100644 --- a/packages/message-parser/package.json +++ b/packages/message-parser/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/message-parser", "description": "Rocket.Chat parser for messages", - "version": "0.31.29", + "version": "0.31.30", "author": { "name": "Rocket.Chat", "url": "https://rocket.chat/" diff --git a/packages/model-typings/CHANGELOG.md b/packages/model-typings/CHANGELOG.md index b743a1132b48..4ae90df8e0c8 100644 --- a/packages/model-typings/CHANGELOG.md +++ b/packages/model-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/model-typings +## 0.7.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.12.1 +
+ ## 0.7.0 ### Minor Changes diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index 4819a451c82f..011fa9339812 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/model-typings", - "version": "0.7.0", + "version": "0.7.1", "private": true, "devDependencies": { "@types/node-rsa": "^1.1.3", diff --git a/packages/models/CHANGELOG.md b/packages/models/CHANGELOG.md index 1238c213ec4f..8a3f24035b14 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/models +## 0.2.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/model-typings@0.7.1 +
+ ## 0.2.3 ### Patch Changes diff --git a/packages/models/package.json b/packages/models/package.json index b3060782e983..6ff93a6c8e05 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/models", - "version": "0.2.3", + "version": "0.2.4", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/peggy-loader/CHANGELOG.md b/packages/peggy-loader/CHANGELOG.md index 0c1ace99d7d4..8fdec959414f 100644 --- a/packages/peggy-loader/CHANGELOG.md +++ b/packages/peggy-loader/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 0.31.26 + +### Patch Changes + +- ([#33254](https://github.com/RocketChat/Rocket.Chat/pull/33254) by [@dionisio-bot](https://github.com/dionisio-bot)) Improved the performance of the message parser + All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. diff --git a/packages/peggy-loader/package.json b/packages/peggy-loader/package.json index 5fa014da7f29..fab7c3c8f15a 100644 --- a/packages/peggy-loader/package.json +++ b/packages/peggy-loader/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/peggy-loader", - "version": "0.31.25", + "version": "0.31.26", "description": "Peggy loader for webpack", "keywords": [ "peggy", diff --git a/packages/rest-typings/CHANGELOG.md b/packages/rest-typings/CHANGELOG.md index 7cce8dcc6e99..0fde816d5729 100644 --- a/packages/rest-typings/CHANGELOG.md +++ b/packages/rest-typings/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/rest-typings +## 6.12.1 + +### Patch Changes + +-
Updated dependencies [3cbb9f6252]: + + - @rocket.chat/message-parser@0.31.30 + - @rocket.chat/core-typings@6.12.1 +
+ ## 6.12.0 ### Minor Changes diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 833326e26599..5817b2b515a7 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "6.12.0", + "version": "6.12.1", "devDependencies": { "@rocket.chat/eslint-config": "workspace:~", "@types/jest": "~29.5.12", diff --git a/packages/ui-avatar/CHANGELOG.md b/packages/ui-avatar/CHANGELOG.md index a9fa21cf7195..8c6aa8a5674c 100644 --- a/packages/ui-avatar/CHANGELOG.md +++ b/packages/ui-avatar/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/ui-avatar +## 6.0.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@10.0.1 +
+ ## 6.0.0 ### Patch Changes diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index 3bed718fbba5..047d59aef489 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-avatar", - "version": "6.0.0", + "version": "6.0.1", "private": true, "devDependencies": { "@babel/core": "~7.22.20", @@ -31,7 +31,7 @@ ], "peerDependencies": { "@rocket.chat/fuselage": "*", - "@rocket.chat/ui-contexts": "10.0.0", + "@rocket.chat/ui-contexts": "10.0.1", "react": "~17.0.2" }, "volta": { diff --git a/packages/ui-client/CHANGELOG.md b/packages/ui-client/CHANGELOG.md index 1d172fe1054f..f18e81b029f1 100644 --- a/packages/ui-client/CHANGELOG.md +++ b/packages/ui-client/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/ui-client +## 10.0.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@10.0.1 +
+ ## 10.0.0 ### Patch Changes diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 7ee4ded9bfee..e353242c849a 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-client", - "version": "10.0.0", + "version": "10.0.1", "private": true, "devDependencies": { "@babel/core": "~7.22.20", @@ -60,7 +60,7 @@ "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", - "@rocket.chat/ui-contexts": "10.0.0", + "@rocket.chat/ui-contexts": "10.0.1", "react": "~17.0.2" }, "volta": { diff --git a/packages/ui-contexts/CHANGELOG.md b/packages/ui-contexts/CHANGELOG.md index aa0f00bc0e83..fe429e751642 100644 --- a/packages/ui-contexts/CHANGELOG.md +++ b/packages/ui-contexts/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ui-contexts +## 10.0.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@6.12.1 + - @rocket.chat/rest-typings@6.12.1 + - @rocket.chat/ddp-client@0.3.7 +
+ ## 10.0.0 ### Patch Changes diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 854e12deff93..180a945c020f 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-contexts", - "version": "10.0.0", + "version": "10.0.1", "private": true, "devDependencies": { "@rocket.chat/core-typings": "workspace:^", diff --git a/packages/ui-video-conf/CHANGELOG.md b/packages/ui-video-conf/CHANGELOG.md index 0ebba8028576..46936bc366cf 100644 --- a/packages/ui-video-conf/CHANGELOG.md +++ b/packages/ui-video-conf/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/ui-video-conf +## 10.0.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@10.0.1 + - @rocket.chat/ui-avatar@6.0.1 +
+ ## 10.0.0 ### Patch Changes diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index ea97ec39ba58..68cc0e679087 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-video-conf", - "version": "10.0.0", + "version": "10.0.1", "private": true, "devDependencies": { "@babel/core": "~7.22.20", @@ -40,8 +40,8 @@ "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-avatar": "6.0.0", - "@rocket.chat/ui-contexts": "10.0.0", + "@rocket.chat/ui-avatar": "6.0.1", + "@rocket.chat/ui-contexts": "10.0.1", "react": "^17.0.2", "react-dom": "^17.0.2" }, diff --git a/packages/uikit-playground/CHANGELOG.md b/packages/uikit-playground/CHANGELOG.md index 63a8eb47d7c6..2c569add2627 100644 --- a/packages/uikit-playground/CHANGELOG.md +++ b/packages/uikit-playground/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/uikit-playground +## 0.4.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/fuselage-ui-kit@10.0.1 + - @rocket.chat/ui-contexts@10.0.1 + - @rocket.chat/ui-avatar@6.0.1 +
+ ## 0.4.0 ### Minor Changes diff --git a/packages/uikit-playground/package.json b/packages/uikit-playground/package.json index e3a029e181ec..228484c2b3d6 100644 --- a/packages/uikit-playground/package.json +++ b/packages/uikit-playground/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/uikit-playground", "private": true, - "version": "0.4.0", + "version": "0.4.1", "type": "module", "scripts": { "dev": "vite", diff --git a/packages/web-ui-registration/CHANGELOG.md b/packages/web-ui-registration/CHANGELOG.md index 601f9aa63494..73164cdb6722 100644 --- a/packages/web-ui-registration/CHANGELOG.md +++ b/packages/web-ui-registration/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/web-ui-registration +## 10.0.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@10.0.1 +
+ ## 10.0.0 ### Patch Changes diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index 7daa6e21e68d..618603e021b5 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/web-ui-registration", - "version": "10.0.0", + "version": "10.0.1", "private": true, "homepage": "https://rocket.chat", "main": "./dist/index.js", @@ -47,7 +47,7 @@ "peerDependencies": { "@rocket.chat/layout": "*", "@rocket.chat/tools": "0.2.2", - "@rocket.chat/ui-contexts": "10.0.0", + "@rocket.chat/ui-contexts": "10.0.1", "@tanstack/react-query": "*", "react": "*", "react-hook-form": "*", diff --git a/yarn.lock b/yarn.lock index a4da870cdb2e..430ef4b66e99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8992,6 +8992,7 @@ __metadata: "@storybook/react": ~6.5.16 "@storybook/testing-library": ~0.0.13 "@testing-library/react": ~16.0.0 + "@types/dompurify": ^3.0.5 "@types/jest": ~29.5.12 "@types/katex": ~0.16.5 "@types/react": ~17.0.69 @@ -9000,6 +9001,7 @@ __metadata: "@typescript-eslint/parser": ~5.60.1 babel-loader: ^8.3.0 date-fns: ^3.3.1 + dompurify: ^3.1.6 eslint: ~8.45.0 eslint-plugin-anti-trojan-source: ~1.1.1 eslint-plugin-react: ~7.32.2 @@ -13375,6 +13377,15 @@ __metadata: languageName: node linkType: hard +"@types/dompurify@npm:^3.0.5": + version: 3.0.5 + resolution: "@types/dompurify@npm:3.0.5" + dependencies: + "@types/trusted-types": "*" + checksum: ffc34eca6a4536e1c8c16a47cce2623c5a118a9785492e71230052d92933ff096d14326ff449031e8dfaac509413222372d8f2b28786a13159de6241df716185 + languageName: node + linkType: hard + "@types/ejson@npm:^2.2.1": version: 2.2.1 resolution: "@types/ejson@npm:2.2.1" @@ -21363,6 +21374,13 @@ __metadata: languageName: node linkType: hard +"dompurify@npm:^3.1.6": + version: 3.1.6 + resolution: "dompurify@npm:3.1.6" + checksum: cc4fc4ccd9261fbceb2a1627a985c70af231274a26ddd3f643fd0616a0a44099bd9e4480940ce3655612063be4a1fe9f5e9309967526f8c0a99f931602323866 + languageName: node + linkType: hard + "domutils@npm:^1.7.0": version: 1.7.0 resolution: "domutils@npm:1.7.0" From b919221b475c90789f665bcd13267a05ecd64f9c Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 16 Sep 2024 14:46:24 -0600 Subject: [PATCH 07/39] fix: Federation callback not awaiting model call (#33298) --- .changeset/great-humans-live.md | 5 +++++ .../meteor/app/federation/server/hooks/afterUnsetReaction.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/great-humans-live.md diff --git a/.changeset/great-humans-live.md b/.changeset/great-humans-live.md new file mode 100644 index 000000000000..1d97d9da23ae --- /dev/null +++ b/.changeset/great-humans-live.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed a Federation callback not awaiting db call diff --git a/apps/meteor/app/federation/server/hooks/afterUnsetReaction.js b/apps/meteor/app/federation/server/hooks/afterUnsetReaction.js index 51181d88ab9e..995146b290bf 100644 --- a/apps/meteor/app/federation/server/hooks/afterUnsetReaction.js +++ b/apps/meteor/app/federation/server/hooks/afterUnsetReaction.js @@ -6,7 +6,7 @@ import { getFederationDomain } from '../lib/getFederationDomain'; import { clientLogger } from '../lib/logger'; async function afterUnsetReaction(message, { user, reaction }) { - const room = Rooms.findOneById(message.rid, { fields: { federation: 1 } }); + const room = await Rooms.findOneById(message.rid, { projection: { federation: 1 } }); // If there are not federated users on this room, ignore it if (!hasExternalDomain(room)) { From 636d32d78374e943cb8b416d2172f9a6c4f68917 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Tue, 17 Sep 2024 11:06:11 -0300 Subject: [PATCH 08/39] fix: correct parameter order in afterSaveMessage to restore outgoing webhooks and related features (#33295) --- .changeset/rotten-rabbits-brush.md | 5 +++++ apps/meteor/app/autotranslate/server/autotranslate.ts | 7 ++++++- apps/meteor/app/integrations/server/triggers.ts | 7 ++++++- apps/meteor/app/irc/server/irc-bridge/index.js | 2 +- apps/meteor/ee/server/lib/engagementDashboard/startup.ts | 7 ++++++- 5 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 .changeset/rotten-rabbits-brush.md diff --git a/.changeset/rotten-rabbits-brush.md b/.changeset/rotten-rabbits-brush.md new file mode 100644 index 000000000000..916f4cc8034a --- /dev/null +++ b/.changeset/rotten-rabbits-brush.md @@ -0,0 +1,5 @@ +--- +'@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/apps/meteor/app/autotranslate/server/autotranslate.ts b/apps/meteor/app/autotranslate/server/autotranslate.ts index f3c6d9e55fdb..23e1b189a792 100644 --- a/apps/meteor/app/autotranslate/server/autotranslate.ts +++ b/apps/meteor/app/autotranslate/server/autotranslate.ts @@ -113,7 +113,12 @@ export class TranslationProviderRegistry { return; } - callbacks.add('afterSaveMessage', provider.translateMessage.bind(provider), callbacks.priority.MEDIUM, 'autotranslate'); + callbacks.add( + 'afterSaveMessage', + (message, { room }) => provider.translateMessage(message, { room }), + callbacks.priority.MEDIUM, + 'autotranslate', + ); } } diff --git a/apps/meteor/app/integrations/server/triggers.ts b/apps/meteor/app/integrations/server/triggers.ts index cdf8acda6a21..64b95827645f 100644 --- a/apps/meteor/app/integrations/server/triggers.ts +++ b/apps/meteor/app/integrations/server/triggers.ts @@ -8,7 +8,12 @@ const callbackHandler = function _callbackHandler(eventType: string) { }; }; -callbacks.add('afterSaveMessage', callbackHandler('sendMessage'), callbacks.priority.LOW, 'integrations-sendMessage'); +callbacks.add( + 'afterSaveMessage', + (message, { room }) => callbackHandler('sendMessage')(message, room), + callbacks.priority.LOW, + 'integrations-sendMessage', +); callbacks.add('afterCreateChannel', callbackHandler('roomCreated'), callbacks.priority.LOW, 'integrations-roomCreated'); callbacks.add('afterCreatePrivateGroup', callbackHandler('roomCreated'), callbacks.priority.LOW, 'integrations-roomCreated'); callbacks.add('afterCreateUser', callbackHandler('userCreated'), callbacks.priority.LOW, 'integrations-userCreated'); diff --git a/apps/meteor/app/irc/server/irc-bridge/index.js b/apps/meteor/app/irc/server/irc-bridge/index.js index 09b7a3568362..bc5b4f0bc33f 100644 --- a/apps/meteor/app/irc/server/irc-bridge/index.js +++ b/apps/meteor/app/irc/server/irc-bridge/index.js @@ -209,7 +209,7 @@ class Bridge { // Chatting callbacks.add( 'afterSaveMessage', - this.onMessageReceived.bind(this, 'local', 'onSaveMessage'), + (message, { room }) => this.onMessageReceived('local', 'onSaveMessage', message, room), callbacks.priority.LOW, 'irc-on-save-message', ); diff --git a/apps/meteor/ee/server/lib/engagementDashboard/startup.ts b/apps/meteor/ee/server/lib/engagementDashboard/startup.ts index 159b121f7043..415e0323d525 100644 --- a/apps/meteor/ee/server/lib/engagementDashboard/startup.ts +++ b/apps/meteor/ee/server/lib/engagementDashboard/startup.ts @@ -3,7 +3,12 @@ import { fillFirstDaysOfMessagesIfNeeded, handleMessagesDeleted, handleMessagesS import { fillFirstDaysOfUsersIfNeeded, handleUserCreated } from './users'; export const attachCallbacks = (): void => { - callbacks.add('afterSaveMessage', handleMessagesSent, callbacks.priority.MEDIUM, 'engagementDashboard.afterSaveMessage'); + callbacks.add( + 'afterSaveMessage', + (message, { room }) => handleMessagesSent(message, { room }), + callbacks.priority.MEDIUM, + 'engagementDashboard.afterSaveMessage', + ); callbacks.add('afterDeleteMessage', handleMessagesDeleted, callbacks.priority.MEDIUM, 'engagementDashboard.afterDeleteMessage'); callbacks.add('afterCreateUser', handleUserCreated, callbacks.priority.MEDIUM, 'engagementDashboard.afterCreateUser'); }; From 3a161c43103433553442ef1d27ada9f2fd88d531 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 17 Sep 2024 10:39:21 -0600 Subject: [PATCH 09/39] feat: New endpoint for listing rooms & discussions from teams (#33177) --- .changeset/soft-mirrors-remember.md | 8 + apps/meteor/app/api/server/v1/teams.ts | 39 +++ apps/meteor/server/models/raw/Rooms.ts | 81 +++++ apps/meteor/server/services/team/service.ts | 39 ++- apps/meteor/tests/data/teams.helper.ts | 19 +- apps/meteor/tests/end-to-end/api/teams.ts | 321 ++++++++++++++++++ .../core-services/src/types/ITeamService.ts | 11 +- .../model-typings/src/models/IRoomsModel.ts | 8 + .../src/v1/teams/TeamsListChildren.ts | 36 ++ packages/rest-typings/src/v1/teams/index.ts | 6 + 10 files changed, 559 insertions(+), 9 deletions(-) create mode 100644 .changeset/soft-mirrors-remember.md create mode 100644 packages/rest-typings/src/v1/teams/TeamsListChildren.ts diff --git a/.changeset/soft-mirrors-remember.md b/.changeset/soft-mirrors-remember.md new file mode 100644 index 000000000000..78b005ee6b6e --- /dev/null +++ b/.changeset/soft-mirrors-remember.md @@ -0,0 +1,8 @@ +--- +"@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/apps/meteor/app/api/server/v1/teams.ts b/apps/meteor/app/api/server/v1/teams.ts index f64f8c820575..acb6cba2bac7 100644 --- a/apps/meteor/app/api/server/v1/teams.ts +++ b/apps/meteor/app/api/server/v1/teams.ts @@ -11,6 +11,7 @@ import { isTeamsDeleteProps, isTeamsLeaveProps, isTeamsUpdateProps, + isTeamsListChildrenProps, } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Match, check } from 'meteor/check'; @@ -375,6 +376,44 @@ API.v1.addRoute( }, ); +const getTeamByIdOrNameOrParentRoom = async ( + params: { teamId: string } | { teamName: string } | { roomId: string }, +): Promise | null> => { + if ('teamId' in params && params.teamId) { + return Team.getOneById(params.teamId, { projection: { type: 1, roomId: 1 } }); + } + if ('teamName' in params && params.teamName) { + return Team.getOneByName(params.teamName, { projection: { type: 1, roomId: 1 } }); + } + if ('roomId' in params && params.roomId) { + return Team.getOneByRoomId(params.roomId, { projection: { type: 1, roomId: 1 } }); + } + return null; +}; + +// This should accept a teamId, filter (search by name on rooms collection) and sort/pagination +// should return a list of rooms/discussions from the team. the discussions will only be returned from the main room +API.v1.addRoute( + 'teams.listChildren', + { authRequired: true, validateParams: isTeamsListChildrenProps }, + { + async get() { + const { offset, count } = await getPaginationItems(this.queryParams); + const { sort } = await this.parseJsonQuery(); + const { filter, type } = this.queryParams; + + const team = await getTeamByIdOrNameOrParentRoom(this.queryParams); + if (!team) { + return API.v1.notFound(); + } + + const data = await Team.listChildren(this.userId, team, filter, type, sort, offset, count); + + return API.v1.success({ ...data, offset, count }); + }, + }, +); + API.v1.addRoute( 'teams.members', { authRequired: true }, diff --git a/apps/meteor/server/models/raw/Rooms.ts b/apps/meteor/server/models/raw/Rooms.ts index ec3bd6fe8d40..7d4a0a54dedf 100644 --- a/apps/meteor/server/models/raw/Rooms.ts +++ b/apps/meteor/server/models/raw/Rooms.ts @@ -2063,4 +2063,85 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.updateMany(query, update); } + + findChildrenOfTeam( + teamId: string, + teamRoomId: string, + userId: string, + filter?: string, + type?: 'channels' | 'discussions', + options?: FindOptions, + ): AggregationCursor<{ totalCount: { count: number }[]; paginatedResults: IRoom[] }> { + const nameFilter = filter ? new RegExp(escapeRegExp(filter), 'i') : undefined; + return this.col.aggregate<{ totalCount: { count: number }[]; paginatedResults: IRoom[] }>([ + { + $match: { + $and: [ + { + $or: [ + ...(!type || type === 'channels' ? [{ teamId }] : []), + ...(!type || type === 'discussions' ? [{ prid: teamRoomId }] : []), + ], + }, + ...(nameFilter ? [{ $or: [{ fname: nameFilter }, { name: nameFilter }] }] : []), + ], + }, + }, + { + $lookup: { + from: 'rocketchat_subscription', + let: { + roomId: '$_id', + }, + pipeline: [ + { + $match: { + $and: [ + { + $expr: { + $eq: ['$rid', '$$roomId'], + }, + }, + { + $expr: { + $eq: ['$u._id', userId], + }, + }, + { + $expr: { + $ne: ['$t', 'c'], + }, + }, + ], + }, + }, + { + $project: { _id: 1 }, + }, + ], + as: 'subscription', + }, + }, + { + $match: { + $or: [ + { t: 'c' }, + { + $expr: { + $ne: [{ $size: '$subscription' }, 0], + }, + }, + ], + }, + }, + { $project: { subscription: 0 } }, + { $sort: options?.sort || { ts: 1 } }, + { + $facet: { + totalCount: [{ $count: 'count' }], + paginatedResults: [{ $skip: options?.skip || 0 }, { $limit: options?.limit || 50 }], + }, + }, + ]); + } } diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index 27f7af1f1b1c..f5218c88402a 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -913,8 +913,8 @@ export class TeamService extends ServiceClassInternal implements ITeamService { }); } - async getOneByRoomId(roomId: string): Promise { - const room = await Rooms.findOneById(roomId); + async getOneByRoomId(roomId: string, options?: FindOptions): Promise { + const room = await Rooms.findOneById(roomId, { projection: { teamId: 1 } }); if (!room) { throw new Error('invalid-room'); @@ -924,7 +924,7 @@ export class TeamService extends ServiceClassInternal implements ITeamService { throw new Error('room-not-on-team'); } - return Team.findOneById(room.teamId); + return Team.findOneById(room.teamId, options); } async addRolesToMember(teamId: string, userId: string, roles: Array): Promise { @@ -1078,4 +1078,37 @@ export class TeamService extends ServiceClassInternal implements ITeamService { const parentRoom = await this.getParentRoom(team); return { team, ...(parentRoom && { parentRoom }) }; } + + // Returns the list of rooms and discussions a user has access to inside a team + // Rooms returned are a composition of the rooms the user is in + public rooms + discussions from the main room (if any) + async listChildren( + userId: string, + team: AtLeast, + filter?: string, + type?: 'channels' | 'discussions', + sort?: Record, + skip = 0, + limit = 10, + ): Promise<{ total: number; data: IRoom[] }> { + const mainRoom = await Rooms.findOneById(team.roomId, { projection: { _id: 1 } }); + if (!mainRoom) { + throw new Error('error-invalid-team-no-main-room'); + } + + const isMember = await TeamMember.findOneByUserIdAndTeamId(userId, team._id, { + projection: { _id: 1 }, + }); + + if (!isMember) { + throw new Error('error-invalid-team-not-a-member'); + } + + const [{ totalCount: [{ count: total }] = [], paginatedResults: data = [] }] = + (await Rooms.findChildrenOfTeam(team._id, mainRoom._id, userId, filter, type, { skip, limit, sort }).toArray()) || []; + + return { + total, + data, + }; + } } diff --git a/apps/meteor/tests/data/teams.helper.ts b/apps/meteor/tests/data/teams.helper.ts index 8fc60bd19fd4..f6cba25f86c9 100644 --- a/apps/meteor/tests/data/teams.helper.ts +++ b/apps/meteor/tests/data/teams.helper.ts @@ -2,11 +2,20 @@ import type { ITeam, TEAM_TYPE } from '@rocket.chat/core-typings'; import { api, request } from './api-data'; -export const createTeam = async (credentials: Record, teamName: string, type: TEAM_TYPE): Promise => { - const response = await request.post(api('teams.create')).set(credentials).send({ - name: teamName, - type, - }); +export const createTeam = async ( + credentials: Record, + teamName: string, + type: TEAM_TYPE, + members?: string[], +): Promise => { + const response = await request + .post(api('teams.create')) + .set(credentials) + .send({ + name: teamName, + type, + ...(members && { members }), + }); return response.body.team; }; diff --git a/apps/meteor/tests/end-to-end/api/teams.ts b/apps/meteor/tests/end-to-end/api/teams.ts index ca07d3e32679..b630a97b1727 100644 --- a/apps/meteor/tests/end-to-end/api/teams.ts +++ b/apps/meteor/tests/end-to-end/api/teams.ts @@ -2217,4 +2217,325 @@ describe('[Teams]', () => { }); }); }); + + describe('[teams.listChildren]', () => { + const teamName = `team-${Date.now()}`; + let testTeam: ITeam; + let testPrivateTeam: ITeam; + let testUser: IUser; + let testUserCredentials: Credentials; + + let privateRoom: IRoom; + let privateRoom2: IRoom; + let publicRoom: IRoom; + let publicRoom2: IRoom; + + let discussionOnPrivateRoom: IRoom; + let discussionOnPublicRoom: IRoom; + let discussionOnMainRoom: IRoom; + + before('Create test team', async () => { + testUser = await createUser(); + testUserCredentials = await login(testUser.username, password); + + const members = testUser.username ? [testUser.username] : []; + testTeam = await createTeam(credentials, teamName, 0, members); + testPrivateTeam = await createTeam(testUserCredentials, `${teamName}private`, 1, []); + }); + + before('make user owner', async () => { + await request + .post(api('teams.updateMember')) + .set(credentials) + .send({ + teamName: testTeam.name, + member: { + userId: testUser._id, + roles: ['member', 'owner'], + }, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }); + + before('create rooms', async () => { + privateRoom = (await createRoom({ type: 'p', name: `test-p-${Date.now()}` })).body.group; + privateRoom2 = (await createRoom({ type: 'p', name: `test-p2-${Date.now()}`, credentials: testUserCredentials })).body.group; + publicRoom = (await createRoom({ type: 'c', name: `test-c-${Date.now()}` })).body.channel; + publicRoom2 = (await createRoom({ type: 'c', name: `test-c2-${Date.now()}` })).body.channel; + + await Promise.all([ + request + .post(api('teams.addRooms')) + .set(credentials) + .send({ + rooms: [privateRoom._id, publicRoom._id, publicRoom2._id], + teamId: testTeam._id, + }) + .expect(200), + request + .post(api('teams.addRooms')) + .set(testUserCredentials) + .send({ + rooms: [privateRoom2._id], + teamId: testTeam._id, + }) + .expect(200), + ]); + }); + + before('Create discussions', async () => { + discussionOnPrivateRoom = ( + await request + .post(api('rooms.createDiscussion')) + .set(credentials) + .send({ + prid: privateRoom._id, + t_name: `test-d-${Date.now()}`, + }) + ).body.discussion; + discussionOnPublicRoom = ( + await request + .post(api('rooms.createDiscussion')) + .set(credentials) + .send({ + prid: publicRoom._id, + t_name: `test-d-${Date.now()}`, + }) + ).body.discussion; + discussionOnMainRoom = ( + await request + .post(api('rooms.createDiscussion')) + .set(credentials) + .send({ + prid: testTeam.roomId, + t_name: `test-d-${Date.now()}`, + }) + ).body.discussion; + }); + + after(async () => { + await Promise.all([ + deleteRoom({ type: 'p', roomId: privateRoom._id }), + deleteRoom({ type: 'p', roomId: privateRoom2._id }), + deleteRoom({ type: 'c', roomId: publicRoom._id }), + deleteRoom({ type: 'c', roomId: publicRoom2._id }), + deleteRoom({ type: 'p', roomId: discussionOnPrivateRoom._id }), + deleteRoom({ type: 'c', roomId: discussionOnPublicRoom._id }), + deleteRoom({ type: 'c', roomId: discussionOnMainRoom._id }), + deleteTeam(credentials, teamName), + deleteTeam(credentials, testPrivateTeam.name), + deleteUser({ _id: testUser._id }), + ]); + }); + + it('should fail if user is not logged in', async () => { + await request.get(api('teams.listChildren')).expect(401); + }); + + it('should fail if teamId is not passed as queryparam', async () => { + await request.get(api('teams.listChildren')).set(credentials).expect(400); + }); + + it('should fail if teamId is not valid', async () => { + await request.get(api('teams.listChildren')).set(credentials).query({ teamId: 'invalid' }).expect(404); + }); + + it('should fail if teamId is empty', async () => { + await request.get(api('teams.listChildren')).set(credentials).query({ teamId: '' }).expect(404); + }); + + it('should fail if both properties are passed', async () => { + await request.get(api('teams.listChildren')).set(credentials).query({ teamId: testTeam._id, teamName: testTeam.name }).expect(400); + }); + + it('should fail if teamName is empty', async () => { + await request.get(api('teams.listChildren')).set(credentials).query({ teamName: '' }).expect(404); + }); + + it('should fail if teamName is invalid', async () => { + await request.get(api('teams.listChildren')).set(credentials).query({ teamName: 'invalid' }).expect(404); + }); + + it('should fail if roomId is empty', async () => { + await request.get(api('teams.listChildren')).set(credentials).query({ roomId: '' }).expect(404); + }); + + it('should fail if roomId is invalid', async () => { + await request.get(api('teams.listChildren')).set(credentials).query({ teamName: 'invalid' }).expect(404); + }); + + it('should return a list of valid rooms for user', async () => { + const res = await request.get(api('teams.listChildren')).query({ teamId: testTeam._id }).set(credentials).expect(200); + + expect(res.body).to.have.property('total').to.be.equal(5); + expect(res.body).to.have.property('data').to.be.an('array'); + expect(res.body.data).to.have.lengthOf(5); + + const mainRoom = res.body.data.find((room: IRoom) => room._id === testTeam.roomId); + expect(mainRoom).to.be.an('object'); + + const publicChannel1 = res.body.data.find((room: IRoom) => room._id === publicRoom._id); + expect(publicChannel1).to.be.an('object'); + + const publicChannel2 = res.body.data.find((room: IRoom) => room._id === publicRoom2._id); + expect(publicChannel2).to.be.an('object'); + + const privateChannel1 = res.body.data.find((room: IRoom) => room._id === privateRoom._id); + expect(privateChannel1).to.be.an('object'); + + const privateChannel2 = res.body.data.find((room: IRoom) => room._id === privateRoom2._id); + expect(privateChannel2).to.be.undefined; + + const discussionOnP = res.body.data.find((room: IRoom) => room._id === discussionOnPrivateRoom._id); + expect(discussionOnP).to.be.undefined; + + const discussionOnC = res.body.data.find((room: IRoom) => room._id === discussionOnPublicRoom._id); + expect(discussionOnC).to.be.undefined; + + const mainDiscussion = res.body.data.find((room: IRoom) => room._id === discussionOnMainRoom._id); + expect(mainDiscussion).to.be.an('object'); + }); + + it('should return a valid list of rooms for non admin member too', async () => { + const res = await request.get(api('teams.listChildren')).query({ teamName: testTeam.name }).set(testUserCredentials).expect(200); + + expect(res.body).to.have.property('total').to.be.equal(5); + expect(res.body).to.have.property('data').to.be.an('array'); + expect(res.body.data).to.have.lengthOf(5); + + const mainRoom = res.body.data.find((room: IRoom) => room._id === testTeam.roomId); + expect(mainRoom).to.be.an('object'); + + const publicChannel1 = res.body.data.find((room: IRoom) => room._id === publicRoom._id); + expect(publicChannel1).to.be.an('object'); + + const publicChannel2 = res.body.data.find((room: IRoom) => room._id === publicRoom2._id); + expect(publicChannel2).to.be.an('object'); + + const privateChannel1 = res.body.data.find((room: IRoom) => room._id === privateRoom._id); + expect(privateChannel1).to.be.undefined; + + const privateChannel2 = res.body.data.find((room: IRoom) => room._id === privateRoom2._id); + expect(privateChannel2).to.be.an('object'); + + const discussionOnP = res.body.data.find((room: IRoom) => room._id === discussionOnPrivateRoom._id); + expect(discussionOnP).to.be.undefined; + + const discussionOnC = res.body.data.find((room: IRoom) => room._id === discussionOnPublicRoom._id); + expect(discussionOnC).to.be.undefined; + + const mainDiscussion = res.body.data.find((room: IRoom) => room._id === discussionOnMainRoom._id); + expect(mainDiscussion).to.be.an('object'); + }); + + it('should return a valid list of rooms for non admin member too when filtering by teams main room id', async () => { + const res = await request.get(api('teams.listChildren')).query({ roomId: testTeam.roomId }).set(testUserCredentials).expect(200); + + expect(res.body).to.have.property('total').to.be.equal(5); + expect(res.body).to.have.property('data').to.be.an('array'); + expect(res.body.data).to.have.lengthOf(5); + + const mainRoom = res.body.data.find((room: IRoom) => room._id === testTeam.roomId); + expect(mainRoom).to.be.an('object'); + + const publicChannel1 = res.body.data.find((room: IRoom) => room._id === publicRoom._id); + expect(publicChannel1).to.be.an('object'); + + const publicChannel2 = res.body.data.find((room: IRoom) => room._id === publicRoom2._id); + expect(publicChannel2).to.be.an('object'); + + const privateChannel1 = res.body.data.find((room: IRoom) => room._id === privateRoom._id); + expect(privateChannel1).to.be.undefined; + + const privateChannel2 = res.body.data.find((room: IRoom) => room._id === privateRoom2._id); + expect(privateChannel2).to.be.an('object'); + + const discussionOnP = res.body.data.find((room: IRoom) => room._id === discussionOnPrivateRoom._id); + expect(discussionOnP).to.be.undefined; + + const discussionOnC = res.body.data.find((room: IRoom) => room._id === discussionOnPublicRoom._id); + expect(discussionOnC).to.be.undefined; + + const mainDiscussion = res.body.data.find((room: IRoom) => room._id === discussionOnMainRoom._id); + expect(mainDiscussion).to.be.an('object'); + }); + + it('should return a list of rooms filtered by name using the filter parameter', async () => { + const res = await request + .get(api('teams.listChildren')) + .query({ teamId: testTeam._id, filter: 'test-p' }) + .set(credentials) + .expect(200); + + expect(res.body).to.have.property('total').to.be.equal(1); + expect(res.body).to.have.property('data').to.be.an('array'); + expect(res.body.data[0]._id).to.be.equal(privateRoom._id); + expect(res.body.data.find((room: IRoom) => room._id === privateRoom2._id)).to.be.undefined; + }); + + it('should paginate results', async () => { + const res = await request + .get(api('teams.listChildren')) + .query({ teamId: testTeam._id, offset: 1, count: 2 }) + .set(credentials) + .expect(200); + + expect(res.body).to.have.property('total').to.be.equal(5); + expect(res.body).to.have.property('data').to.be.an('array'); + expect(res.body.data).to.have.lengthOf(2); + }); + + it('should return only items of type channel', async () => { + const res = await request + .get(api('teams.listChildren')) + .query({ teamId: testTeam._id, type: 'channels' }) + .set(credentials) + .expect(200); + + expect(res.body).to.have.property('total').to.be.equal(4); + expect(res.body).to.have.property('data').to.be.an('array'); + expect(res.body.data).to.have.lengthOf(4); + expect(res.body.data.some((room: IRoom) => !!room.prid)).to.be.false; + }); + + it('should return only items of type discussion', async () => { + const res = await request + .get(api('teams.listChildren')) + .query({ teamId: testTeam._id, type: 'discussions' }) + .set(credentials) + .expect(200); + + expect(res.body).to.have.property('total').to.be.equal(1); + expect(res.body).to.have.property('data').to.be.an('array'); + expect(res.body.data).to.have.lengthOf(1); + expect(res.body.data.every((room: IRoom) => !!room.prid)).to.be.true; + }); + + it('should return both when type is not passed', async () => { + const res = await request.get(api('teams.listChildren')).query({ teamId: testTeam._id }).set(credentials).expect(200); + + expect(res.body).to.have.property('total').to.be.equal(5); + expect(res.body).to.have.property('data').to.be.an('array'); + expect(res.body.data).to.have.lengthOf(5); + expect(res.body.data.some((room: IRoom) => !!room.prid)).to.be.true; + expect(res.body.data.some((room: IRoom) => !room.prid)).to.be.true; + }); + + it('should fail if type is other than channel or discussion', async () => { + await request.get(api('teams.listChildren')).query({ teamId: testTeam._id, type: 'other' }).set(credentials).expect(400); + }); + + it('should properly list children of a private team', async () => { + const res = await request.get(api('teams.listChildren')).query({ teamId: testPrivateTeam._id }).set(testUserCredentials).expect(200); + + expect(res.body).to.have.property('total').to.be.equal(1); + expect(res.body).to.have.property('data').to.be.an('array'); + expect(res.body.data).to.have.lengthOf(1); + }); + + it('should throw an error when a non member user tries to fetch info for team', async () => { + await request.get(api('teams.listChildren')).query({ teamId: testPrivateTeam._id }).set(credentials).expect(400); + }); + }); }); diff --git a/packages/core-services/src/types/ITeamService.ts b/packages/core-services/src/types/ITeamService.ts index 3caa6a2e97df..132df89470ca 100644 --- a/packages/core-services/src/types/ITeamService.ts +++ b/packages/core-services/src/types/ITeamService.ts @@ -112,7 +112,7 @@ export interface ITeamService { getOneById

(teamId: string, options?: FindOptions

): Promise; getOneByName(teamName: string | RegExp, options?: FindOptions): Promise; getOneByMainRoomId(teamId: string): Promise | null>; - getOneByRoomId(teamId: string): Promise; + getOneByRoomId(teamId: string, options?: FindOptions): Promise; getMatchingTeamRooms(teamId: string, rids: Array): Promise>; autocomplete(uid: string, name: string): Promise; getAllPublicTeams(options?: FindOptions): Promise>; @@ -129,4 +129,13 @@ export interface ITeamService { getRoomInfo( room: AtLeast, ): Promise<{ team?: Pick; parentRoom?: Pick }>; + listChildren( + userId: string, + team: AtLeast, + filter?: string, + type?: 'channels' | 'discussions', + sort?: Record, + skip?: number, + limit?: number, + ): Promise<{ total: number; data: IRoom[] }>; } diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index 9097a89c1413..91802f836719 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -282,4 +282,12 @@ export interface IRoomsModel extends IBaseModel { getSubscribedRoomIdsWithoutE2EKeys(uid: IUser['_id']): Promise; removeUsersFromE2EEQueueByRoomId(roomId: IRoom['_id'], uids: IUser['_id'][]): Promise; removeUserFromE2EEQueue(uid: IUser['_id']): Promise; + findChildrenOfTeam( + teamId: string, + teamRoomId: string, + userId: string, + filter?: string, + type?: 'channels' | 'discussions', + options?: FindOptions, + ): AggregationCursor<{ totalCount: { count: number }[]; paginatedResults: IRoom[] }>; } diff --git a/packages/rest-typings/src/v1/teams/TeamsListChildren.ts b/packages/rest-typings/src/v1/teams/TeamsListChildren.ts new file mode 100644 index 000000000000..41128e18a05f --- /dev/null +++ b/packages/rest-typings/src/v1/teams/TeamsListChildren.ts @@ -0,0 +1,36 @@ +import type { ITeam } from '@rocket.chat/core-typings'; + +import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; +import { ajv } from '../Ajv'; + +type GeneralProps = { + filter?: string; + type?: 'channels' | 'discussions'; +}; + +export type TeamsListChildrenProps = + | PaginatedRequest< + { + teamId: ITeam['_id']; + } & GeneralProps + > + | PaginatedRequest<{ teamName: ITeam['name'] } & GeneralProps> + | PaginatedRequest<{ roomId: ITeam['roomId'] } & GeneralProps>; + +const TeamsListChildrenPropsSchema = { + type: 'object', + properties: { + teamId: { type: 'string' }, + teamName: { type: 'string' }, + type: { type: 'string', enum: ['channels', 'discussions'] }, + roomId: { type: 'string' }, + filter: { type: 'string' }, + offset: { type: 'number' }, + count: { type: 'number' }, + sort: { type: 'string' }, + }, + additionalProperties: false, + oneOf: [{ required: ['teamId'] }, { required: ['teamName'] }, { required: ['roomId'] }], +}; + +export const isTeamsListChildrenProps = ajv.compile(TeamsListChildrenPropsSchema); diff --git a/packages/rest-typings/src/v1/teams/index.ts b/packages/rest-typings/src/v1/teams/index.ts index d63e6da8bd8a..a4ae6c7bca0f 100644 --- a/packages/rest-typings/src/v1/teams/index.ts +++ b/packages/rest-typings/src/v1/teams/index.ts @@ -6,6 +6,7 @@ import type { TeamsAddMembersProps } from './TeamsAddMembersProps'; import type { TeamsConvertToChannelProps } from './TeamsConvertToChannelProps'; import type { TeamsDeleteProps } from './TeamsDeleteProps'; import type { TeamsLeaveProps } from './TeamsLeaveProps'; +import type { TeamsListChildrenProps } from './TeamsListChildren'; import type { TeamsRemoveMemberProps } from './TeamsRemoveMemberProps'; import type { TeamsRemoveRoomProps } from './TeamsRemoveRoomProps'; import type { TeamsUpdateMemberProps } from './TeamsUpdateMemberProps'; @@ -19,6 +20,7 @@ export * from './TeamsRemoveMemberProps'; export * from './TeamsRemoveRoomProps'; export * from './TeamsUpdateMemberProps'; export * from './TeamsUpdateProps'; +export * from './TeamsListChildren'; type ITeamAutocompleteResult = Pick; @@ -184,4 +186,8 @@ export type TeamsEndpoints = { room: IRoom; }; }; + + '/v1/teams.listChildren': { + GET: (params: TeamsListChildrenProps) => PaginatedResult<{ data: IRoom[] }>; + }; }; From f27092b94d5e4070a4eb74ce49b6b4eec191446e Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 17 Sep 2024 13:30:43 -0600 Subject: [PATCH 10/39] chore: Update typings on callbacks to accept less than a full room object (#33305) --- apps/meteor/app/lib/server/functions/isTheLastMessage.ts | 4 ++-- .../server/services/messages/hooks/BeforeFederationActions.ts | 3 ++- apps/meteor/server/services/messages/service.ts | 4 ++-- packages/core-services/src/types/IMessageService.ts | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/isTheLastMessage.ts b/apps/meteor/app/lib/server/functions/isTheLastMessage.ts index f8e5be94002c..f1f1fb4c1497 100644 --- a/apps/meteor/app/lib/server/functions/isTheLastMessage.ts +++ b/apps/meteor/app/lib/server/functions/isTheLastMessage.ts @@ -1,7 +1,7 @@ -import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, AtLeast } from '@rocket.chat/core-typings'; import { settings } from '../../../settings/server'; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export const isTheLastMessage = (room: IRoom, message: Pick) => +export const isTheLastMessage = (room: AtLeast, message: Pick) => settings.get('Store_Last_Message') && (!room.lastMessage || room.lastMessage._id === message._id); diff --git a/apps/meteor/server/services/messages/hooks/BeforeFederationActions.ts b/apps/meteor/server/services/messages/hooks/BeforeFederationActions.ts index a954e4899970..19f42626c216 100644 --- a/apps/meteor/server/services/messages/hooks/BeforeFederationActions.ts +++ b/apps/meteor/server/services/messages/hooks/BeforeFederationActions.ts @@ -1,9 +1,10 @@ +import type { AtLeast } from '@rocket.chat/core-typings'; import { type IMessage, type IRoom, isMessageFromMatrixFederation, isRoomFederated } from '@rocket.chat/core-typings'; import { isFederationEnabled, isFederationReady } from '../../federation/utils'; export class FederationActions { - public static shouldPerformAction(message: IMessage, room: IRoom): boolean { + public static shouldPerformAction(message: IMessage, room: AtLeast): boolean { if (isMessageFromMatrixFederation(message) || isRoomFederated(room)) { return isFederationEnabled() && isFederationReady(); } diff --git a/apps/meteor/server/services/messages/service.ts b/apps/meteor/server/services/messages/service.ts index b20b5236b7fe..85afcf394f28 100644 --- a/apps/meteor/server/services/messages/service.ts +++ b/apps/meteor/server/services/messages/service.ts @@ -1,6 +1,6 @@ import type { IMessageService } from '@rocket.chat/core-services'; import { Authorization, ServiceClassInternal } from '@rocket.chat/core-services'; -import { type IMessage, type MessageTypesValues, type IUser, type IRoom, isEditedMessage } from '@rocket.chat/core-typings'; +import { type IMessage, type MessageTypesValues, type IUser, type IRoom, isEditedMessage, type AtLeast } from '@rocket.chat/core-typings'; import { Messages, Rooms } from '@rocket.chat/models'; import { deleteMessage } from '../../../app/lib/server/functions/deleteMessage'; @@ -244,7 +244,7 @@ export class MessageService extends ServiceClassInternal implements IMessageServ // await Room.join({ room, user }); // } - async beforeReacted(message: IMessage, room: IRoom) { + async beforeReacted(message: IMessage, room: AtLeast) { if (!FederationActions.shouldPerformAction(message, room)) { throw new FederationMatrixInvalidConfigurationError('Unable to react to message'); } diff --git a/packages/core-services/src/types/IMessageService.ts b/packages/core-services/src/types/IMessageService.ts index ca84f78ea677..29da139ef63c 100644 --- a/packages/core-services/src/types/IMessageService.ts +++ b/packages/core-services/src/types/IMessageService.ts @@ -1,4 +1,4 @@ -import type { IMessage, MessageTypesValues, IUser, IRoom } from '@rocket.chat/core-typings'; +import type { IMessage, MessageTypesValues, IUser, IRoom, AtLeast } from '@rocket.chat/core-typings'; export interface IMessageService { sendMessage({ fromId, rid, msg }: { fromId: string; rid: string; msg: string }): Promise; @@ -21,6 +21,6 @@ export interface IMessageService { deleteMessage(user: IUser, message: IMessage): Promise; updateMessage(message: IMessage, user: IUser, originalMsg?: IMessage): Promise; reactToMessage(userId: string, reaction: string, messageId: IMessage['_id'], shouldReact?: boolean): Promise; - beforeReacted(message: IMessage, room: IRoom): Promise; + beforeReacted(message: IMessage, room: AtLeast): Promise; beforeDelete(message: IMessage, room: IRoom): Promise; } From 4202d6570cfa870cd196a5b10bc29041fbff50eb Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Tue, 17 Sep 2024 17:56:33 -0300 Subject: [PATCH 11/39] fix: resolve avatar download issue on setUsername by refining service selection logic (#33193) --- .changeset/small-crabs-travel.md | 5 + .../app/lib/server/functions/setUsername.ts | 23 +- .../lib/server/functions/setUsername.spec.ts | 271 ++++++++++++++++++ 3 files changed, 287 insertions(+), 12 deletions(-) create mode 100644 .changeset/small-crabs-travel.md create mode 100644 apps/meteor/tests/unit/app/lib/server/functions/setUsername.spec.ts diff --git a/.changeset/small-crabs-travel.md b/.changeset/small-crabs-travel.md new file mode 100644 index 000000000000..201494a5b70f --- /dev/null +++ b/.changeset/small-crabs-travel.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed avatar blob image setting in setUserAvatar method by correcting service handling logic. diff --git a/apps/meteor/app/lib/server/functions/setUsername.ts b/apps/meteor/app/lib/server/functions/setUsername.ts index 5b2b1923da75..c4d2c47c6d9d 100644 --- a/apps/meteor/app/lib/server/functions/setUsername.ts +++ b/apps/meteor/app/lib/server/functions/setUsername.ts @@ -102,23 +102,22 @@ export const _setUsername = async function (userId: string, u: string, fullUser: // Set new username* await Users.setUsername(user._id, username); user.username = username; + if (!previousUsername && settings.get('Accounts_SetDefaultAvatar') === true) { - // eslint-disable-next-line @typescript-eslint/ban-types - const avatarSuggestions = (await getAvatarSuggestionForUser(user)) as {}; - let gravatar; - for await (const service of Object.keys(avatarSuggestions)) { - const avatarData = avatarSuggestions[+service as keyof typeof avatarSuggestions]; + const avatarSuggestions = await getAvatarSuggestionForUser(user); + let avatarData; + let serviceName = 'gravatar'; + + for (const service of Object.keys(avatarSuggestions)) { + avatarData = avatarSuggestions[service]; if (service !== 'gravatar') { - // eslint-disable-next-line dot-notation - await setUserAvatar(user, avatarData['blob'], avatarData['contentType'], service); - gravatar = null; + serviceName = service; break; } - gravatar = avatarData; } - if (gravatar != null) { - // eslint-disable-next-line dot-notation - await setUserAvatar(user, gravatar['blob'], gravatar['contentType'], 'gravatar'); + + if (avatarData) { + await setUserAvatar(user, avatarData.blob, avatarData.contentType, serviceName); } } diff --git a/apps/meteor/tests/unit/app/lib/server/functions/setUsername.spec.ts b/apps/meteor/tests/unit/app/lib/server/functions/setUsername.spec.ts new file mode 100644 index 000000000000..c6b6f9a26fae --- /dev/null +++ b/apps/meteor/tests/unit/app/lib/server/functions/setUsername.spec.ts @@ -0,0 +1,271 @@ +import { expect } from 'chai'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +describe('setUsername', () => { + const userId = 'userId'; + const username = 'validUsername'; + + const stubs = { + Users: { + findOneById: sinon.stub(), + setUsername: sinon.stub(), + }, + Accounts: { + sendEnrollmentEmail: sinon.stub(), + }, + settings: { + get: sinon.stub(), + }, + api: { + broadcast: sinon.stub(), + }, + Invites: { + findOneById: sinon.stub(), + }, + callbacks: { + run: sinon.stub(), + }, + checkUsernameAvailability: sinon.stub(), + validateUsername: sinon.stub(), + saveUserIdentity: sinon.stub(), + joinDefaultChannels: sinon.stub(), + getAvatarSuggestionForUser: sinon.stub(), + setUserAvatar: sinon.stub(), + addUserToRoom: sinon.stub(), + notifyOnUserChange: sinon.stub(), + RateLimiter: { + limitFunction: sinon.stub(), + }, + underscore: { + escape: sinon.stub(), + }, + SystemLogger: sinon.stub(), + }; + + const { setUsernameWithValidation, _setUsername } = proxyquire + .noCallThru() + .load('../../../../../../app/lib/server/functions/setUsername', { + 'meteor/meteor': { Meteor: { Error } }, + '@rocket.chat/core-services': { api: stubs.api }, + '@rocket.chat/models': { Users: stubs.Users, Invites: stubs.Invites }, + 'meteor/accounts-base': { Accounts: stubs.Accounts }, + 'underscore': stubs.underscore, + '../../../settings/server': { settings: stubs.settings }, + '../lib': { notifyOnUserChange: stubs.notifyOnUserChange, RateLimiter: stubs.RateLimiter }, + './addUserToRoom': { addUserToRoom: stubs.addUserToRoom }, + './checkUsernameAvailability': { checkUsernameAvailability: stubs.checkUsernameAvailability }, + './getAvatarSuggestionForUser': { getAvatarSuggestionForUser: stubs.getAvatarSuggestionForUser }, + './joinDefaultChannels': { joinDefaultChannels: stubs.joinDefaultChannels }, + './saveUserIdentity': { saveUserIdentity: stubs.saveUserIdentity }, + './setUserAvatar': { setUserAvatar: stubs.setUserAvatar }, + './validateUsername': { validateUsername: stubs.validateUsername }, + '../../../../lib/callbacks': { callbacks: stubs.callbacks }, + '../../../../server/lib/logger/system': { SystemLogger: stubs.SystemLogger }, + }); + + afterEach(() => { + stubs.Users.findOneById.reset(); + stubs.Users.setUsername.reset(); + stubs.Accounts.sendEnrollmentEmail.reset(); + stubs.settings.get.reset(); + stubs.api.broadcast.reset(); + stubs.Invites.findOneById.reset(); + stubs.callbacks.run.reset(); + stubs.checkUsernameAvailability.reset(); + stubs.validateUsername.reset(); + stubs.saveUserIdentity.reset(); + stubs.joinDefaultChannels.reset(); + stubs.getAvatarSuggestionForUser.reset(); + stubs.setUserAvatar.reset(); + stubs.addUserToRoom.reset(); + stubs.notifyOnUserChange.reset(); + stubs.RateLimiter.limitFunction.reset(); + stubs.underscore.escape.reset(); + stubs.SystemLogger.reset(); + }); + + describe('setUsernameWithValidation', () => { + it('should throw an error if username is invalid', async () => { + try { + await setUsernameWithValidation(userId, ''); + } catch (error: any) { + expect(error.message).to.equal('error-invalid-username'); + } + }); + + it('should throw an error if user is not found', async () => { + stubs.Users.findOneById.withArgs(userId).returns(null); + + try { + await setUsernameWithValidation(userId, username); + } catch (error: any) { + expect(stubs.Users.findOneById.calledOnce).to.be.true; + expect(error.message).to.equal('error-invalid-user'); + } + }); + + it('should throw an error if username change is not allowed', async () => { + stubs.Users.findOneById.resolves({ username: 'oldUsername' }); + stubs.settings.get.withArgs('Accounts_AllowUsernameChange').returns(false); + + try { + await setUsernameWithValidation(userId, username); + } catch (error: any) { + expect(stubs.settings.get.calledOnce).to.be.true; + expect(error.message).to.equal('error-not-allowed'); + } + }); + + it('should throw an error if username is not valid', async () => { + stubs.Users.findOneById.resolves({ username: null }); + stubs.validateUsername.returns(false); + + try { + await setUsernameWithValidation(userId, 'invalid-username'); + } catch (error: any) { + expect(stubs.validateUsername.calledOnce).to.be.true; + expect(error.message).to.equal('username-invalid'); + } + }); + + it('should throw an error if username is already in use', async () => { + stubs.Users.findOneById.resolves({ username: null }); + stubs.validateUsername.returns(true); + stubs.checkUsernameAvailability.resolves(false); + + try { + await setUsernameWithValidation(userId, 'existingUsername'); + } catch (error: any) { + expect(stubs.checkUsernameAvailability.calledOnce).to.be.true; + expect(error.message).to.equal('error-field-unavailable'); + } + }); + + it('should save the user identity when valid username is set', async () => { + stubs.Users.findOneById.resolves({ _id: userId, username: null }); + stubs.settings.get.withArgs('Accounts_AllowUsernameChange').returns(true); + stubs.validateUsername.returns(true); + stubs.checkUsernameAvailability.resolves(true); + stubs.saveUserIdentity.resolves(true); + + await setUsernameWithValidation(userId, 'newUsername'); + + expect(stubs.saveUserIdentity.calledOnce).to.be.true; + expect(stubs.joinDefaultChannels.calledOnceWith(userId, undefined)).to.be.true; + }); + }); + + describe('_setUsername', () => { + it('should return false if userId or username is missing', async () => { + const result = await _setUsername(null, '', {}); + expect(result).to.be.false; + }); + + it('should return false if username is invalid', async () => { + stubs.validateUsername.returns(false); + + const result = await _setUsername(userId, 'invalid-username', {}); + expect(result).to.be.false; + }); + + it('should return user if username is already set', async () => { + stubs.validateUsername.returns(true); + const mockUser = { username }; + + const result = await _setUsername(userId, username, mockUser); + expect(result).to.equal(mockUser); + }); + + it('should set username when user has no previous username', async () => { + const mockUser = { _id: userId, emails: [{ address: 'test@example.com' }] }; + stubs.validateUsername.returns(true); + stubs.Users.findOneById.resolves(mockUser); + stubs.checkUsernameAvailability.resolves(true); + + await _setUsername(userId, username, mockUser); + + expect(stubs.Users.setUsername.calledOnceWith(userId, username)); + expect(stubs.checkUsernameAvailability.calledOnceWith(username)); + expect(stubs.api.broadcast.calledOnceWith('user.autoupdate', { user: mockUser })); + }); + + it('should set username when user has and old that is different from new', async () => { + const mockUser = { _id: userId, username: 'oldUsername', emails: [{ address: 'test@example.com' }] }; + stubs.validateUsername.returns(true); + stubs.Users.findOneById.resolves(mockUser); + stubs.checkUsernameAvailability.resolves(true); + + await _setUsername(userId, username, mockUser); + + expect(stubs.Users.setUsername.calledOnceWith(userId, username)); + expect(stubs.checkUsernameAvailability.calledOnceWith(username)); + expect(stubs.api.broadcast.calledOnceWith('user.autoupdate', { user: mockUser })); + }); + + it('should set username when user has and old that is different from new', async () => { + const mockUser = { _id: userId, username: 'oldUsername', emails: [{ address: 'test@example.com' }] }; + stubs.validateUsername.returns(true); + stubs.Users.findOneById.resolves(mockUser); + stubs.checkUsernameAvailability.resolves(true); + + await _setUsername(userId, username, mockUser); + + expect(stubs.Users.setUsername.calledOnceWith(userId, username)); + expect(stubs.checkUsernameAvailability.calledOnceWith(username)); + expect(stubs.api.broadcast.calledOnceWith('user.autoupdate', { user: mockUser })); + }); + + it('should set avatar if Accounts_SetDefaultAvatar is enabled', async () => { + const mockUser = { _id: userId, username: null }; + stubs.validateUsername.returns(true); + stubs.Users.findOneById.resolves(mockUser); + stubs.checkUsernameAvailability.resolves(true); + stubs.settings.get.withArgs('Accounts_SetDefaultAvatar').returns(true); + stubs.getAvatarSuggestionForUser.resolves({ gravatar: { blob: 'blobData', contentType: 'image/png' } }); + + await _setUsername(userId, username, mockUser); + + expect(stubs.setUserAvatar.calledOnceWith(mockUser, 'blobData', 'image/png', 'gravatar')).to.be.true; + }); + + it('should not set avatar if Accounts_SetDefaultAvatar is disabled', async () => { + const mockUser = { _id: userId, username: null }; + stubs.validateUsername.returns(true); + stubs.Users.findOneById.resolves(mockUser); + stubs.checkUsernameAvailability.resolves(true); + stubs.settings.get.withArgs('Accounts_SetDefaultAvatar').returns(false); + + await _setUsername(userId, username, mockUser); + + expect(stubs.setUserAvatar.called).to.be.false; + }); + + it('should not set avatar if no avatar suggestions are available', async () => { + const mockUser = { _id: userId, username: null }; + stubs.validateUsername.returns(true); + stubs.Users.findOneById.resolves(mockUser); + stubs.checkUsernameAvailability.resolves(true); + stubs.settings.get.withArgs('Accounts_SetDefaultAvatar').returns(true); + stubs.getAvatarSuggestionForUser.resolves({}); + + await _setUsername(userId, username, mockUser); + + expect(stubs.setUserAvatar.called).to.be.false; + }); + + it('should add user to room if inviteToken is present', async () => { + const mockUser = { _id: userId, username: null, inviteToken: 'invite token' }; + stubs.validateUsername.returns(true); + stubs.Users.findOneById.resolves(mockUser); + stubs.checkUsernameAvailability.resolves(true); + stubs.settings.get.withArgs('Accounts_SetDefaultAvatar').returns(true); + stubs.getAvatarSuggestionForUser.resolves({ gravatar: { blob: 'blobData', contentType: 'image/png' } }); + stubs.Invites.findOneById.resolves({ rid: 'room id' }); + + await _setUsername(userId, username, mockUser); + + expect(stubs.addUserToRoom.calledOnceWith('room id', mockUser)).to.be.true; + }); + }); +}); From 9a38c8e13f925d88ece6955333773bce45ba7536 Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Wed, 18 Sep 2024 09:31:32 -0300 Subject: [PATCH 12/39] feat: Allow managing association to business units on departments' creation and update (#32682) --- .changeset/dirty-stingrays-beg.md | 7 + .../imports/server/rest/departments.ts | 15 +- .../app/livechat/server/lib/LivechatTyped.ts | 25 +- .../livechat/server/methods/saveDepartment.ts | 5 +- .../livechat-enterprise/server/hooks/index.ts | 1 + .../server/hooks/manageDepartmentUnit.ts | 53 ++ .../ee/server/models/raw/LivechatUnit.ts | 29 +- .../server/hooks/manageDepartmentUnit.spec.ts | 183 ++++++ apps/meteor/lib/callbacks.ts | 2 + .../server/models/raw/LivechatDepartment.ts | 12 + apps/meteor/tests/data/livechat/department.ts | 40 +- apps/meteor/tests/data/livechat/rooms.ts | 47 +- apps/meteor/tests/data/livechat/units.ts | 38 +- .../tests/end-to-end/api/livechat/14-units.ts | 556 +++++++++++++++++- .../core-typings/src/ILivechatDepartment.ts | 1 + .../src/models/ILivechatDepartmentModel.ts | 3 + .../src/models/ILivechatUnitModel.ts | 2 + packages/rest-typings/src/v1/omnichannel.ts | 12 +- 18 files changed, 982 insertions(+), 49 deletions(-) create mode 100644 .changeset/dirty-stingrays-beg.md create mode 100644 apps/meteor/ee/app/livechat-enterprise/server/hooks/manageDepartmentUnit.ts create mode 100644 apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/hooks/manageDepartmentUnit.spec.ts diff --git a/.changeset/dirty-stingrays-beg.md b/.changeset/dirty-stingrays-beg.md new file mode 100644 index 000000000000..cf5e3a4ca839 --- /dev/null +++ b/.changeset/dirty-stingrays-beg.md @@ -0,0 +1,7 @@ +--- +"@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/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index 252a83855700..e56feeac2fa3 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -57,10 +57,18 @@ API.v1.addRoute( check(this.bodyParams, { department: Object, agents: Match.Maybe(Array), + departmentUnit: Match.Maybe({ _id: Match.Optional(String) }), }); const agents = this.bodyParams.agents ? { upsert: this.bodyParams.agents } : {}; - const department = await LivechatTs.saveDepartment(null, this.bodyParams.department as ILivechatDepartment, agents); + const { departmentUnit } = this.bodyParams; + const department = await LivechatTs.saveDepartment( + this.userId, + null, + this.bodyParams.department as ILivechatDepartment, + agents, + departmentUnit || {}, + ); if (department) { return API.v1.success({ @@ -112,17 +120,18 @@ API.v1.addRoute( check(this.bodyParams, { department: Object, agents: Match.Maybe(Array), + departmentUnit: Match.Maybe({ _id: Match.Optional(String) }), }); const { _id } = this.urlParams; - const { department, agents } = this.bodyParams; + const { department, agents, departmentUnit } = this.bodyParams; if (!permissionToSave) { throw new Error('error-not-allowed'); } const agentParam = permissionToAddAgents && agents ? { upsert: agents } : {}; - await LivechatTs.saveDepartment(_id, department, agentParam); + await LivechatTs.saveDepartment(this.userId, _id, department, agentParam, departmentUnit || {}); return API.v1.success({ department: await LivechatDepartment.findOneById(_id), diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 89d125033977..ade6726336ec 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -1789,18 +1789,37 @@ class LivechatClass { * @param {string|null} _id - The department id * @param {Partial} departmentData * @param {{upsert?: { agentId: string; count?: number; order?: number; }[], remove?: { agentId: string; count?: number; order?: number; }}} [departmentAgents] - The department agents + * @param {{_id?: string}} [departmentUnit] - The department's unit id */ async saveDepartment( + userId: string, _id: string | null, departmentData: LivechatDepartmentDTO, departmentAgents?: { upsert?: { agentId: string; count?: number; order?: number }[]; remove?: { agentId: string; count?: number; order?: number }; }, + departmentUnit?: { _id?: string }, ) { check(_id, Match.Maybe(String)); + if (departmentUnit?._id !== undefined && typeof departmentUnit._id !== 'string') { + throw new Meteor.Error('error-invalid-department-unit', 'Invalid department unit id provided', { + method: 'livechat:saveDepartment', + }); + } - const department = _id ? await LivechatDepartment.findOneById(_id, { projection: { _id: 1, archived: 1, enabled: 1 } }) : null; + const department = _id + ? await LivechatDepartment.findOneById(_id, { projection: { _id: 1, archived: 1, enabled: 1, parentId: 1 } }) + : null; + + if (departmentUnit && !departmentUnit._id && department && department.parentId) { + const isLastDepartmentInUnit = (await LivechatDepartment.countDepartmentsInUnit(department.parentId)) === 1; + if (isLastDepartmentInUnit) { + throw new Meteor.Error('error-unit-cant-be-empty', "The last department in a unit can't be removed", { + method: 'livechat:saveDepartment', + }); + } + } if (!department && !(await isDepartmentCreationAvailable())) { throw new Meteor.Error('error-max-departments-number-reached', 'Maximum number of departments reached', { @@ -1887,6 +1906,10 @@ class LivechatClass { await callbacks.run('livechat.afterDepartmentDisabled', departmentDB); } + if (departmentUnit) { + await callbacks.run('livechat.manageDepartmentUnit', { userId, departmentId: departmentDB._id, unitId: departmentUnit._id }); + } + return departmentDB; } } diff --git a/apps/meteor/app/livechat/server/methods/saveDepartment.ts b/apps/meteor/app/livechat/server/methods/saveDepartment.ts index b4833523ab3f..659f85f49945 100644 --- a/apps/meteor/app/livechat/server/methods/saveDepartment.ts +++ b/apps/meteor/app/livechat/server/methods/saveDepartment.ts @@ -30,12 +30,13 @@ declare module '@rocket.chat/ddp-client' { order?: number | undefined; }[] | undefined, + departmentUnit?: { _id?: string }, ) => ILivechatDepartment; } } Meteor.methods({ - async 'livechat:saveDepartment'(_id, departmentData, departmentAgents) { + async 'livechat:saveDepartment'(_id, departmentData, departmentAgents, departmentUnit) { const uid = Meteor.userId(); if (!uid || !(await hasPermissionAsync(uid, 'manage-livechat-departments'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { @@ -43,6 +44,6 @@ Meteor.methods({ }); } - return Livechat.saveDepartment(_id, departmentData, { upsert: departmentAgents }); + return Livechat.saveDepartment(uid, _id, departmentData, { upsert: departmentAgents }, departmentUnit); }, }); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts index c5bf0a5aa392..a4b66087be2e 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts @@ -26,3 +26,4 @@ import './afterInquiryQueued'; import './sendPdfTranscriptOnClose'; import './applyRoomRestrictions'; import './afterTagRemoved'; +import './manageDepartmentUnit'; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/manageDepartmentUnit.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/manageDepartmentUnit.ts new file mode 100644 index 000000000000..7de7ef0d6bf6 --- /dev/null +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/manageDepartmentUnit.ts @@ -0,0 +1,53 @@ +import type { ILivechatDepartment, IOmnichannelBusinessUnit } from '@rocket.chat/core-typings'; +import { LivechatDepartment, LivechatUnit } from '@rocket.chat/models'; + +import { hasAnyRoleAsync } from '../../../../../app/authorization/server/functions/hasRole'; +import { callbacks } from '../../../../../lib/callbacks'; +import { getUnitsFromUser } from '../methods/getUnitsFromUserRoles'; + +export const manageDepartmentUnit = async ({ userId, departmentId, unitId }: { userId: string; departmentId: string; unitId: string }) => { + const accessibleUnits = await getUnitsFromUser(userId); + const isLivechatManager = await hasAnyRoleAsync(userId, ['admin', 'livechat-manager']); + const department = await LivechatDepartment.findOneById>(departmentId, { + projection: { ancestors: 1, parentId: 1 }, + }); + + const isDepartmentAlreadyInUnit = unitId && department?.ancestors?.includes(unitId); + if (!department || isDepartmentAlreadyInUnit) { + return; + } + + const currentDepartmentUnitId = department.parentId; + const canManageNewUnit = !unitId || isLivechatManager || (Array.isArray(accessibleUnits) && accessibleUnits.includes(unitId)); + const canManageCurrentUnit = + !currentDepartmentUnitId || isLivechatManager || (Array.isArray(accessibleUnits) && accessibleUnits.includes(currentDepartmentUnitId)); + if (!canManageNewUnit || !canManageCurrentUnit) { + return; + } + + if (unitId) { + const unit = await LivechatUnit.findOneById>(unitId, { + projection: { ancestors: 1 }, + }); + + if (!unit) { + return; + } + + if (currentDepartmentUnitId) { + await LivechatUnit.decrementDepartmentsCount(currentDepartmentUnitId); + } + + await LivechatDepartment.addDepartmentToUnit(departmentId, unitId, [unitId, ...(unit.ancestors || [])]); + await LivechatUnit.incrementDepartmentsCount(unitId); + return; + } + + if (currentDepartmentUnitId) { + await LivechatUnit.decrementDepartmentsCount(currentDepartmentUnitId); + } + + await LivechatDepartment.removeDepartmentFromUnit(departmentId); +}; + +callbacks.add('livechat.manageDepartmentUnit', manageDepartmentUnit, callbacks.priority.HIGH, 'livechat-manage-department-unit'); diff --git a/apps/meteor/ee/server/models/raw/LivechatUnit.ts b/apps/meteor/ee/server/models/raw/LivechatUnit.ts index fcabf12fa4f8..c198ee04fbb0 100644 --- a/apps/meteor/ee/server/models/raw/LivechatUnit.ts +++ b/apps/meteor/ee/server/models/raw/LivechatUnit.ts @@ -11,7 +11,6 @@ const addQueryRestrictions = async (originalQuery: Filter implement // remove other departments for await (const departmentId of savedDepartments) { if (!departmentsToSave.includes(departmentId)) { - await LivechatDepartment.updateOne( - { _id: departmentId }, - { - $set: { - parentId: null, - ancestors: null, - }, - }, - ); + await LivechatDepartment.removeDepartmentFromUnit(departmentId); } } for await (const departmentId of departmentsToSave) { - await LivechatDepartment.updateOne( - { _id: departmentId }, - { - $set: { - parentId: _id, - ancestors, - }, - }, - ); + await LivechatDepartment.addDepartmentToUnit(departmentId, _id, ancestors); } await LivechatRooms.associateRoomsWithDepartmentToUnit(departmentsToSave, _id); @@ -154,6 +137,14 @@ export class LivechatUnitRaw extends BaseRaw implement return this.updateMany(query, update); } + incrementDepartmentsCount(_id: string): Promise { + return this.updateOne({ _id }, { $inc: { numDepartments: 1 } }); + } + + decrementDepartmentsCount(_id: string): Promise { + return this.updateOne({ _id }, { $inc: { numDepartments: -1 } }); + } + async removeById(_id: string): Promise { await LivechatUnitMonitors.removeByUnitId(_id); await this.removeParentAndAncestorById(_id); diff --git a/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/hooks/manageDepartmentUnit.spec.ts b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/hooks/manageDepartmentUnit.spec.ts new file mode 100644 index 000000000000..8fbf0dcf97a2 --- /dev/null +++ b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/hooks/manageDepartmentUnit.spec.ts @@ -0,0 +1,183 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const livechatDepartmentStub = { + findOneById: sinon.stub(), + addDepartmentToUnit: sinon.stub(), + removeDepartmentFromUnit: sinon.stub(), +}; + +const livechatUnitStub = { + findOneById: sinon.stub(), + decrementDepartmentsCount: sinon.stub(), + incrementDepartmentsCount: sinon.stub(), +}; + +const hasAnyRoleStub = sinon.stub(); +const getUnitsFromUserStub = sinon.stub(); + +const { manageDepartmentUnit } = proxyquire + .noCallThru() + .load('../../../../../../app/livechat-enterprise/server/hooks/manageDepartmentUnit.ts', { + '../methods/getUnitsFromUserRoles': { + getUnitsFromUser: getUnitsFromUserStub, + }, + '../../../../../app/authorization/server/functions/hasRole': { + hasAnyRoleAsync: hasAnyRoleStub, + }, + '@rocket.chat/models': { + LivechatDepartment: livechatDepartmentStub, + LivechatUnit: livechatUnitStub, + }, + }); + +describe('hooks/manageDepartmentUnit', () => { + beforeEach(() => { + livechatDepartmentStub.findOneById.reset(); + livechatDepartmentStub.addDepartmentToUnit.reset(); + livechatDepartmentStub.removeDepartmentFromUnit.reset(); + livechatUnitStub.findOneById.reset(); + livechatUnitStub.decrementDepartmentsCount.reset(); + livechatUnitStub.incrementDepartmentsCount.reset(); + hasAnyRoleStub.reset(); + }); + + it('should not perform any operation when an invalid department is provided', async () => { + livechatDepartmentStub.findOneById.resolves(null); + hasAnyRoleStub.resolves(true); + getUnitsFromUserStub.resolves(['unit-id']); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'unit-id' }); + expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true; + }); + + it('should not perform any operation if the provided department is already in unit', async () => { + livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' }); + hasAnyRoleStub.resolves(true); + getUnitsFromUserStub.resolves(['unit-id']); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'unit-id' }); + expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true; + }); + + it("should not perform any operation if user is a monitor and can't manage new unit", async () => { + livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' }); + hasAnyRoleStub.resolves(false); + getUnitsFromUserStub.resolves(['unit-id']); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'new-unit-id' }); + expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true; + }); + + it("should not perform any operation if user is a monitor and can't manage current unit", async () => { + livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' }); + hasAnyRoleStub.resolves(false); + getUnitsFromUserStub.resolves(['new-unit-id']); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'new-unit-id' }); + expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true; + }); + + it('should not perform any operation if user is an admin/manager but an invalid unit is provided', async () => { + livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' }); + livechatUnitStub.findOneById.resolves(undefined); + hasAnyRoleStub.resolves(true); + getUnitsFromUserStub.resolves(undefined); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'new-unit-id' }); + expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true; + }); + + it('should remove department from its current unit if user is an admin/manager', async () => { + livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' }); + hasAnyRoleStub.resolves(true); + getUnitsFromUserStub.resolves(undefined); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: undefined }); + expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.calledOnceWith('department-id')).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.calledOnceWith('unit-id')).to.be.true; + }); + + it('should add department to a unit if user is an admin/manager', async () => { + livechatDepartmentStub.findOneById.resolves({ _id: 'department-id' }); + livechatUnitStub.findOneById.resolves({ _id: 'unit-id' }); + hasAnyRoleStub.resolves(true); + getUnitsFromUserStub.resolves(undefined); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'unit-id' }); + expect(livechatDepartmentStub.addDepartmentToUnit.calledOnceWith('department-id', 'unit-id', ['unit-id'])).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.calledOnceWith('unit-id')).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true; + }); + + it('should move department to a new unit if user is an admin/manager', async () => { + livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' }); + livechatUnitStub.findOneById.resolves({ _id: 'new-unit-id' }); + hasAnyRoleStub.resolves(true); + getUnitsFromUserStub.resolves(undefined); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'new-unit-id' }); + expect(livechatDepartmentStub.addDepartmentToUnit.calledOnceWith('department-id', 'new-unit-id', ['new-unit-id'])).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.calledOnceWith('new-unit-id')).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.calledOnceWith('unit-id')).to.be.true; + }); + + it('should remove department from its current unit if user is a monitor that supervises the current unit', async () => { + livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' }); + hasAnyRoleStub.resolves(false); + getUnitsFromUserStub.resolves(['unit-id']); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: undefined }); + expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.calledOnceWith('department-id')).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.calledOnceWith('unit-id')).to.be.true; + }); + + it('should add department to a unit if user is a monitor that supervises the new unit', async () => { + livechatDepartmentStub.findOneById.resolves({ _id: 'department-id' }); + livechatUnitStub.findOneById.resolves({ _id: 'unit-id' }); + hasAnyRoleStub.resolves(false); + getUnitsFromUserStub.resolves(['unit-id']); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'unit-id' }); + expect(livechatDepartmentStub.addDepartmentToUnit.calledOnceWith('department-id', 'unit-id', ['unit-id'])).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.calledOnceWith('unit-id')).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true; + }); + + it('should move department to a new unit if user is a monitor that supervises the current and new units', async () => { + livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' }); + livechatUnitStub.findOneById.resolves({ _id: 'unit-id' }); + hasAnyRoleStub.resolves(false); + getUnitsFromUserStub.resolves(['unit-id', 'new-unit-id']); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'new-unit-id' }); + expect(livechatDepartmentStub.addDepartmentToUnit.calledOnceWith('department-id', 'new-unit-id', ['new-unit-id'])).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.calledOnceWith('new-unit-id')).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.calledOnceWith('unit-id')).to.be.true; + }); +}); diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 7eaa9ed7595d..57b8527d5008 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -225,6 +225,7 @@ type ChainedCallbackSignatures = { 'unarchiveRoom': (room: IRoom) => void; 'roomAvatarChanged': (room: IRoom) => void; 'beforeGetMentions': (mentionIds: string[], teamMentions: MessageMention[]) => Promise; + 'livechat.manageDepartmentUnit': (params: { userId: string; departmentId: string; unitId?: string }) => void; }; export type Hook = @@ -247,6 +248,7 @@ export type Hook = | 'livechat.offlineMessage' | 'livechat.onCheckRoomApiParams' | 'livechat.onLoadConfigApi' + | 'livechat.manageDepartmentUnit' | 'loginPageStateChange' | 'mapLDAPUserData' | 'onCreateUser' diff --git a/apps/meteor/server/models/raw/LivechatDepartment.ts b/apps/meteor/server/models/raw/LivechatDepartment.ts index b8263af030a8..9ecee34df5e9 100644 --- a/apps/meteor/server/models/raw/LivechatDepartment.ts +++ b/apps/meteor/server/models/raw/LivechatDepartment.ts @@ -222,6 +222,14 @@ export class LivechatDepartmentRaw extends BaseRaw implemen return this.updateOne({ _id }, { $set: { archived: true, enabled: false } }); } + addDepartmentToUnit(_id: string, unitId: string, ancestors: string[]): Promise { + return this.updateOne({ _id }, { $set: { parentId: unitId, ancestors } }); + } + + removeDepartmentFromUnit(_id: string): Promise { + return this.updateOne({ _id }, { $set: { parentId: null, ancestors: null } }); + } + async createOrUpdateDepartment( _id: string | null, data: { @@ -328,6 +336,10 @@ export class LivechatDepartmentRaw extends BaseRaw implemen return this.find(query, options); } + countDepartmentsInUnit(unitId: string): Promise { + return this.countDocuments({ parentId: unitId }); + } + findActiveByUnitIds(unitIds: string[], options: FindOptions = {}): FindCursor { const query = { enabled: true, diff --git a/apps/meteor/tests/data/livechat/department.ts b/apps/meteor/tests/data/livechat/department.ts index 72ab0af9f267..d7f22fca970b 100644 --- a/apps/meteor/tests/data/livechat/department.ts +++ b/apps/meteor/tests/data/livechat/department.ts @@ -42,36 +42,44 @@ const updateDepartment = async (departmentId: string, departmentData: Partial

  • +export const createDepartmentWithMethod = ({ + initialAgents = [], + allowReceiveForwardOffline = false, + fallbackForwardDepartment, + name, + departmentUnit, + userCredentials = credentials, + departmentId = '', +}: { + initialAgents?: { agentId: string; username: string }[]; + allowReceiveForwardOffline?: boolean; + fallbackForwardDepartment?: string; + name?: string; + departmentUnit?: { _id?: string }; + userCredentials?: Credentials; + departmentId?: string; +} = {}): Promise => new Promise((resolve, reject) => { void request .post(methodCall('livechat:saveDepartment')) - .set(credentials) + .set(userCredentials) .send({ message: JSON.stringify({ method: 'livechat:saveDepartment', params: [ - '', + departmentId, { enabled: true, email: faker.internet.email(), showOnRegistration: true, showOnOfflineForm: true, - name: `new department ${Date.now()}`, + name: name || `new department ${Date.now()}`, description: 'created from api', allowReceiveForwardOffline, fallbackForwardDepartment, }, initialAgents, + departmentUnit, ], id: 'id', msg: 'method', @@ -93,7 +101,7 @@ type OnlineAgent = { export const createDepartmentWithAnOnlineAgent = async (): Promise<{ department: ILivechatDepartment; agent: OnlineAgent }> => { const { user, credentials } = await createAnOnlineAgent(); - const department = (await createDepartmentWithMethod()) as ILivechatDepartment; + const department = await createDepartmentWithMethod(); await addOrRemoveAgentFromDepartment(department._id, { agentId: user._id, username: user.username }, true); @@ -108,7 +116,7 @@ export const createDepartmentWithAnOnlineAgent = async (): Promise<{ department: export const createDepartmentWithAgent = async (agent: OnlineAgent): Promise<{ department: ILivechatDepartment; agent: OnlineAgent }> => { const { user, credentials } = agent; - const department = (await createDepartmentWithMethod()) as ILivechatDepartment; + const department = await createDepartmentWithMethod(); await addOrRemoveAgentFromDepartment(department._id, { agentId: user._id, username: user.username }, true); @@ -153,7 +161,7 @@ export const createDepartmentWithAnOfflineAgent = async ({ }> => { const { user, credentials } = await createAnOfflineAgent(); - const department = (await createDepartmentWithMethod(undefined, { + const department = (await createDepartmentWithMethod({ allowReceiveForwardOffline, fallbackForwardDepartment, })) as ILivechatDepartment; diff --git a/apps/meteor/tests/data/livechat/rooms.ts b/apps/meteor/tests/data/livechat/rooms.ts index b5d89762c614..46e5cbe489a9 100644 --- a/apps/meteor/tests/data/livechat/rooms.ts +++ b/apps/meteor/tests/data/livechat/rooms.ts @@ -98,11 +98,55 @@ export const createDepartment = ( name?: string, enabled = true, opts: Record = {}, + departmentUnit?: { _id?: string }, + userCredentials: Credentials = credentials, ): Promise => { return new Promise((resolve, reject) => { void request .post(api('livechat/department')) - .set(credentials) + .set(userCredentials) + .send({ + department: { + name: name || `Department ${Date.now()}`, + enabled, + showOnOfflineForm: true, + showOnRegistration: true, + email: 'a@b.com', + ...opts, + }, + agents, + departmentUnit, + }) + .end((err: Error, res: DummyResponse) => { + if (err) { + return reject(err); + } + resolve(res.body.department); + }); + }); +}; + +export const updateDepartment = ({ + departmentId, + userCredentials, + agents, + name, + enabled = true, + opts = {}, + departmentUnit, +}: { + departmentId: string; + userCredentials: Credentials; + agents?: { agentId: string }[]; + name?: string; + enabled?: boolean; + opts?: Record; + departmentUnit?: { _id?: string }; +}): Promise => { + return new Promise((resolve, reject) => { + void request + .put(api(`livechat/department/${departmentId}`)) + .set(userCredentials) .send({ department: { name: name || `Department ${Date.now()}`, @@ -113,6 +157,7 @@ export const createDepartment = ( ...opts, }, agents, + departmentUnit, }) .end((err: Error, res: DummyResponse) => { if (err) { diff --git a/apps/meteor/tests/data/livechat/units.ts b/apps/meteor/tests/data/livechat/units.ts index 03ea578e654d..8a2d0f5a833a 100644 --- a/apps/meteor/tests/data/livechat/units.ts +++ b/apps/meteor/tests/data/livechat/units.ts @@ -1,7 +1,7 @@ import { faker } from '@faker-js/faker'; import type { IOmnichannelBusinessUnit } from '@rocket.chat/core-typings'; -import { methodCall, credentials, request } from '../api-data'; +import { methodCall, credentials, request, api } from '../api-data'; import type { DummyResponse } from './utils'; export const createMonitor = async (username: string): Promise<{ _id: string; username: string }> => { @@ -57,3 +57,39 @@ export const createUnit = async ( }); }); }; + +export const deleteUnit = async (unit: IOmnichannelBusinessUnit): Promise => { + return new Promise((resolve, reject) => { + void request + .post(methodCall(`livechat:removeUnit`)) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:removeUnit', + params: [unit._id], + id: '101', + msg: 'method', + }), + }) + .end((err: Error, res: DummyResponse) => { + if (err) { + return reject(err); + } + resolve(JSON.parse(res.body.message).result); + }); + }); +}; + +export const getUnit = (unitId: string): Promise => { + return new Promise((resolve, reject) => { + void request + .get(api(`livechat/units/${unitId}`)) + .set(credentials) + .end((err: Error, res: DummyResponse) => { + if (err) { + return reject(err); + } + resolve(res.body); + }); + }); +}; diff --git a/apps/meteor/tests/end-to-end/api/livechat/14-units.ts b/apps/meteor/tests/end-to-end/api/livechat/14-units.ts index 425c776fecdb..e0c079ece243 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/14-units.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/14-units.ts @@ -1,12 +1,14 @@ import type { ILivechatDepartment, IOmnichannelBusinessUnit } from '@rocket.chat/core-typings'; import { expect } from 'chai'; -import { before, describe, it } from 'mocha'; +import { before, after, describe, it } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../../data/api-data'; -import { createDepartment } from '../../../data/livechat/rooms'; -import { createMonitor, createUnit } from '../../../data/livechat/units'; +import { getCredentials, api, request, credentials, methodCall } from '../../../data/api-data'; +import { deleteDepartment, getDepartmentById, createDepartmentWithMethod } from '../../../data/livechat/department'; +import { createDepartment, updateDepartment } from '../../../data/livechat/rooms'; +import { createMonitor, createUnit, deleteUnit, getUnit } from '../../../data/livechat/units'; import { updatePermission, updateSetting } from '../../../data/permissions.helper'; -import { createUser, deleteUser } from '../../../data/users.helper'; +import { password } from '../../../data/user'; +import { createUser, deleteUser, login } from '../../../data/users.helper'; import { IS_EE } from '../../../e2e/config/constants'; (IS_EE ? describe : describe.skip)('[EE] LIVECHAT - Units', () => { @@ -14,6 +16,7 @@ import { IS_EE } from '../../../e2e/config/constants'; before(async () => { await updateSetting('Livechat_enabled', true); + await updatePermission('manage-livechat-departments', ['livechat-manager', 'livechat-monitor', 'admin']); }); describe('[GET] livechat/units', () => { @@ -409,4 +412,547 @@ import { IS_EE } from '../../../e2e/config/constants'; await deleteUser(user); }); }); + + describe('[POST] livechat/department', () => { + let monitor1: Awaited>; + let monitor1Credentials: Awaited>; + let monitor2: Awaited>; + let monitor2Credentials: Awaited>; + let unit: IOmnichannelBusinessUnit; + + before(async () => { + monitor1 = await createUser(); + monitor2 = await createUser(); + await createMonitor(monitor1.username); + monitor1Credentials = await login(monitor1.username, password); + await createMonitor(monitor2.username); + monitor2Credentials = await login(monitor2.username, password); + unit = await createUnit(monitor1._id, monitor1.username, []); + }); + + after(async () => Promise.all([deleteUser(monitor1), deleteUser(monitor2), deleteUnit(unit)])); + + it('should fail creating department when providing an invalid property in the department unit object', () => { + return request + .post(api('livechat/department')) + .set(credentials) + .send({ + department: { name: 'Fail-Test', enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' }, + departmentUnit: { invalidProperty: true }, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'invalid-params'); + }); + }); + + it('should fail creating a department into an existing unit that a monitor does not supervise', async () => { + const department = await createDepartment(undefined, undefined, undefined, undefined, { _id: unit._id }, monitor2Credentials); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 0); + + const fullDepartment = await getDepartmentById(department._id); + expect(fullDepartment).to.not.have.property('parentId'); + expect(fullDepartment).to.not.have.property('ancestors'); + + await deleteDepartment(department._id); + }); + + it('should succesfully create a department into an existing unit as an admin', async () => { + const department = await createDepartment(undefined, undefined, undefined, undefined, { _id: unit._id }); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 1); + + const fullDepartment = await getDepartmentById(department._id); + expect(fullDepartment).to.have.property('parentId', unit._id); + expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1); + expect(fullDepartment.ancestors?.[0]).to.equal(unit._id); + + await deleteDepartment(department._id); + }); + + it('should succesfully create a department into an existing unit that a monitor supervises', async () => { + const department = await createDepartment(undefined, undefined, undefined, undefined, { _id: unit._id }, monitor1Credentials); + + // Deleting a department currently does not decrease its unit's counter. We must adjust this check when this is fixed + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 2); + + const fullDepartment = await getDepartmentById(department._id); + expect(fullDepartment).to.have.property('parentId', unit._id); + expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1); + expect(fullDepartment.ancestors?.[0]).to.equal(unit._id); + + await deleteDepartment(department._id); + }); + }); + + describe('[PUT] livechat/department', () => { + let monitor1: Awaited>; + let monitor1Credentials: Awaited>; + let monitor2: Awaited>; + let monitor2Credentials: Awaited>; + let unit: IOmnichannelBusinessUnit; + let department: ILivechatDepartment; + let baseDepartment: ILivechatDepartment; + + before(async () => { + monitor1 = await createUser(); + monitor2 = await createUser(); + await createMonitor(monitor1.username); + monitor1Credentials = await login(monitor1.username, password); + await createMonitor(monitor2.username); + monitor2Credentials = await login(monitor2.username, password); + department = await createDepartment(); + baseDepartment = await createDepartment(); + unit = await createUnit(monitor1._id, monitor1.username, [baseDepartment._id]); + }); + + after(async () => + Promise.all([ + deleteUser(monitor1), + deleteUser(monitor2), + deleteUnit(unit), + deleteDepartment(department._id), + deleteDepartment(baseDepartment._id), + ]), + ); + + it("should fail updating a department's unit when providing an invalid property in the department unit object", () => { + const updatedName = 'updated-department-name'; + return request + .put(api(`livechat/department/${department._id}`)) + .set(credentials) + .send({ + department: { name: updatedName, enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' }, + departmentUnit: { invalidProperty: true }, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'Match error: Unknown key in field departmentUnit.invalidProperty'); + }); + }); + + it("should fail updating a department's unit when providing an invalid _id type in the department unit object", () => { + const updatedName = 'updated-department-name'; + return request + .put(api(`livechat/department/${department._id}`)) + .set(credentials) + .send({ + department: { name: updatedName, enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' }, + departmentUnit: { _id: true }, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'Match error: Expected string, got boolean in field departmentUnit._id'); + }); + }); + + it('should fail removing the last department from a unit', () => { + const updatedName = 'updated-department-name'; + return request + .put(api(`livechat/department/${baseDepartment._id}`)) + .set(credentials) + .send({ + department: { name: updatedName, enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' }, + departmentUnit: {}, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-unit-cant-be-empty'); + }); + }); + + it('should succesfully add an existing department to a unit as an admin', async () => { + const updatedName = 'updated-department-name'; + + const updatedDepartment = await updateDepartment({ + userCredentials: credentials, + departmentId: department._id, + name: updatedName, + departmentUnit: { _id: unit._id }, + }); + expect(updatedDepartment).to.have.property('name', updatedName); + expect(updatedDepartment).to.have.property('type', 'd'); + expect(updatedDepartment).to.have.property('_id', department._id); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 2); + + const fullDepartment = await getDepartmentById(department._id); + expect(fullDepartment).to.have.property('parentId', unit._id); + expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1); + expect(fullDepartment.ancestors?.[0]).to.equal(unit._id); + }); + + it('should succesfully remove an existing department from a unit as an admin', async () => { + const updatedName = 'updated-department-name'; + + const updatedDepartment = await updateDepartment({ + userCredentials: credentials, + departmentId: department._id, + name: updatedName, + departmentUnit: {}, + }); + expect(updatedDepartment).to.have.property('name', updatedName); + expect(updatedDepartment).to.have.property('type', 'd'); + expect(updatedDepartment).to.have.property('_id', department._id); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 1); + + const fullDepartment = await getDepartmentById(department._id); + expect(fullDepartment).to.have.property('parentId').that.is.null; + expect(fullDepartment).to.have.property('ancestors').that.is.null; + }); + + it('should fail adding a department into an existing unit that a monitor does not supervise', async () => { + const updatedName = 'updated-department-name2'; + + const updatedDepartment = await updateDepartment({ + userCredentials: monitor2Credentials, + departmentId: department._id, + name: updatedName, + departmentUnit: { _id: unit._id }, + }); + expect(updatedDepartment).to.have.property('name', updatedName); + expect(updatedDepartment).to.have.property('type', 'd'); + expect(updatedDepartment).to.have.property('_id', department._id); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 1); + + const fullDepartment = await getDepartmentById(department._id); + expect(fullDepartment).to.have.property('parentId').that.is.null; + expect(fullDepartment).to.have.property('ancestors').that.is.null; + }); + + it('should succesfully add a department into an existing unit that a monitor supervises', async () => { + const updatedName = 'updated-department-name3'; + + const updatedDepartment = await updateDepartment({ + userCredentials: monitor1Credentials, + departmentId: department._id, + name: updatedName, + departmentUnit: { _id: unit._id }, + }); + expect(updatedDepartment).to.have.property('name', updatedName); + expect(updatedDepartment).to.have.property('type', 'd'); + expect(updatedDepartment).to.have.property('_id', department._id); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 2); + + const fullDepartment = await getDepartmentById(department._id); + expect(fullDepartment).to.have.property('name', updatedName); + expect(fullDepartment).to.have.property('parentId', unit._id); + expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1); + expect(fullDepartment.ancestors?.[0]).to.equal(unit._id); + }); + + it('should fail removing a department from a unit that a monitor does not supervise', async () => { + const updatedName = 'updated-department-name4'; + + const updatedDepartment = await updateDepartment({ + userCredentials: monitor2Credentials, + departmentId: department._id, + name: updatedName, + departmentUnit: {}, + }); + expect(updatedDepartment).to.have.property('name', updatedName); + expect(updatedDepartment).to.have.property('type', 'd'); + expect(updatedDepartment).to.have.property('_id', department._id); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 2); + + const fullDepartment = await getDepartmentById(department._id); + expect(fullDepartment).to.have.property('name', updatedName); + expect(fullDepartment).to.have.property('parentId', unit._id); + expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1); + expect(fullDepartment.ancestors?.[0]).to.equal(unit._id); + }); + + it('should succesfully remove a department from a unit that a monitor supervises', async () => { + const updatedName = 'updated-department-name5'; + + const updatedDepartment = await updateDepartment({ + userCredentials: monitor1Credentials, + departmentId: department._id, + name: updatedName, + departmentUnit: {}, + }); + expect(updatedDepartment).to.have.property('name', updatedName); + expect(updatedDepartment).to.have.property('type', 'd'); + expect(updatedDepartment).to.have.property('_id', department._id); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 1); + + const fullDepartment = await getDepartmentById(department._id); + expect(fullDepartment).to.have.property('name', updatedName); + expect(fullDepartment).to.have.property('parentId').that.is.null; + expect(fullDepartment).to.have.property('ancestors').that.is.null; + }); + }); + + describe('[POST] livechat:saveDepartment', () => { + let monitor1: Awaited>; + let monitor1Credentials: Awaited>; + let monitor2: Awaited>; + let monitor2Credentials: Awaited>; + let unit: IOmnichannelBusinessUnit; + const departmentName = 'Test-Department-Livechat-Method'; + let testDepartmentId = ''; + let baseDepartment: ILivechatDepartment; + + before(async () => { + monitor1 = await createUser(); + monitor2 = await createUser(); + await createMonitor(monitor1.username); + monitor1Credentials = await login(monitor1.username, password); + await createMonitor(monitor2.username); + monitor2Credentials = await login(monitor2.username, password); + baseDepartment = await createDepartment(); + unit = await createUnit(monitor1._id, monitor1.username, [baseDepartment._id]); + }); + + after(async () => + Promise.all([ + deleteUser(monitor1), + deleteUser(monitor2), + deleteUnit(unit), + deleteDepartment(testDepartmentId), + deleteDepartment(baseDepartment._id), + ]), + ); + + it('should fail creating department when providing an invalid _id type in the department unit object', () => { + return request + .post(methodCall('livechat:saveDepartment')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:saveDepartment', + params: [ + '', + { name: 'Fail-Test', enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' }, + [], + { _id: true }, + ], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.property('error').that.is.an('object'); + expect(data.error).to.have.property('errorType', 'Meteor.Error'); + expect(data.error).to.have.property('error', 'error-invalid-department-unit'); + }); + }); + + it('should fail removing last department from unit', () => { + return request + .post(methodCall('livechat:saveDepartment')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:saveDepartment', + params: [ + baseDepartment._id, + { name: 'Fail-Test', enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' }, + [], + {}, + ], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.property('error').that.is.an('object'); + expect(data.error).to.have.property('errorType', 'Meteor.Error'); + expect(data.error).to.have.property('error', 'error-unit-cant-be-empty'); + }); + }); + + it('should fail creating a department into an existing unit that a monitor does not supervise', async () => { + const departmentName = 'Fail-Test'; + + const department = await createDepartmentWithMethod({ + userCredentials: monitor2Credentials, + name: departmentName, + departmentUnit: { _id: unit._id }, + }); + testDepartmentId = department._id; + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 1); + + const fullDepartment = await getDepartmentById(testDepartmentId); + expect(fullDepartment).to.not.have.property('parentId'); + expect(fullDepartment).to.not.have.property('ancestors'); + + await deleteDepartment(testDepartmentId); + }); + + it('should succesfully create a department into an existing unit as an admin', async () => { + const testDepartment = await createDepartmentWithMethod({ name: departmentName, departmentUnit: { _id: unit._id } }); + testDepartmentId = testDepartment._id; + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 2); + + const fullDepartment = await getDepartmentById(testDepartmentId); + expect(fullDepartment).to.have.property('parentId', unit._id); + expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1); + expect(fullDepartment.ancestors?.[0]).to.equal(unit._id); + }); + + it('should succesfully remove an existing department from a unit as an admin', async () => { + await createDepartmentWithMethod({ name: departmentName, departmentUnit: {}, departmentId: testDepartmentId }); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 1); + + const fullDepartment = await getDepartmentById(testDepartmentId); + expect(fullDepartment).to.have.property('parentId').that.is.null; + expect(fullDepartment).to.have.property('ancestors').that.is.null; + }); + + it('should succesfully add an existing department to a unit as an admin', async () => { + await createDepartmentWithMethod({ name: departmentName, departmentUnit: { _id: unit._id }, departmentId: testDepartmentId }); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 2); + + const fullDepartment = await getDepartmentById(testDepartmentId); + expect(fullDepartment).to.have.property('parentId', unit._id); + expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1); + expect(fullDepartment.ancestors?.[0]).to.equal(unit._id); + }); + + it('should succesfully remove a department from a unit that a monitor supervises', async () => { + await createDepartmentWithMethod({ + name: departmentName, + departmentUnit: {}, + departmentId: testDepartmentId, + userCredentials: monitor1Credentials, + }); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 1); + + const fullDepartment = await getDepartmentById(testDepartmentId); + expect(fullDepartment).to.have.property('parentId').that.is.null; + expect(fullDepartment).to.have.property('ancestors').that.is.null; + }); + + it('should succesfully add an existing department to a unit that a monitor supervises', async () => { + await createDepartmentWithMethod({ + name: departmentName, + departmentUnit: { _id: unit._id }, + departmentId: testDepartmentId, + userCredentials: monitor1Credentials, + }); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 2); + + const fullDepartment = await getDepartmentById(testDepartmentId); + expect(fullDepartment).to.have.property('parentId', unit._id); + expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1); + expect(fullDepartment.ancestors?.[0]).to.equal(unit._id); + }); + + it('should fail removing a department from a unit that a monitor does not supervise', async () => { + await createDepartmentWithMethod({ + name: departmentName, + departmentUnit: {}, + departmentId: testDepartmentId, + userCredentials: monitor2Credentials, + }); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 2); + + const fullDepartment = await getDepartmentById(testDepartmentId); + expect(fullDepartment).to.have.property('parentId', unit._id); + expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1); + expect(fullDepartment.ancestors?.[0]).to.equal(unit._id); + + await deleteDepartment(testDepartmentId); + }); + + it('should succesfully create a department in a unit that a monitor supervises', async () => { + const testDepartment = await createDepartmentWithMethod({ + name: departmentName, + departmentUnit: { _id: unit._id }, + userCredentials: monitor1Credentials, + }); + testDepartmentId = testDepartment._id; + + // Deleting a department currently does not decrease its unit's counter. We must adjust this check when this is fixed + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 3); + + const fullDepartment = await getDepartmentById(testDepartmentId); + expect(fullDepartment).to.have.property('parentId', unit._id); + expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1); + expect(fullDepartment.ancestors?.[0]).to.equal(unit._id); + }); + }); }); diff --git a/packages/core-typings/src/ILivechatDepartment.ts b/packages/core-typings/src/ILivechatDepartment.ts index a73cf55cb235..0138a88226fb 100644 --- a/packages/core-typings/src/ILivechatDepartment.ts +++ b/packages/core-typings/src/ILivechatDepartment.ts @@ -16,6 +16,7 @@ export interface ILivechatDepartment { archived?: boolean; departmentsAllowedToForward?: string[]; maxNumberSimultaneousChat?: number; + parentId?: string; ancestors?: string[]; allowReceiveForwardOffline?: boolean; // extra optional fields diff --git a/packages/model-typings/src/models/ILivechatDepartmentModel.ts b/packages/model-typings/src/models/ILivechatDepartmentModel.ts index 75fe0f54b2eb..fe366256eff7 100644 --- a/packages/model-typings/src/models/ILivechatDepartmentModel.ts +++ b/packages/model-typings/src/models/ILivechatDepartmentModel.ts @@ -59,6 +59,7 @@ export interface ILivechatDepartmentModel extends IBaseModel>; findOneByIdOrName(_idOrName: string, options?: FindOptions): Promise; findByUnitIds(unitIds: string[], options?: FindOptions): FindCursor; + countDepartmentsInUnit(unitId: string): Promise; findActiveByUnitIds(unitIds: string[], options?: FindOptions): FindCursor; findNotArchived(options?: FindOptions): FindCursor; getBusinessHoursWithDepartmentStatuses(): Promise< @@ -73,4 +74,6 @@ export interface ILivechatDepartmentModel extends IBaseModel): FindCursor; archiveDepartment(_id: string): Promise; unarchiveDepartment(_id: string): Promise; + addDepartmentToUnit(_id: string, unitId: string, ancestors: string[]): Promise; + removeDepartmentFromUnit(_id: string): Promise; } diff --git a/packages/model-typings/src/models/ILivechatUnitModel.ts b/packages/model-typings/src/models/ILivechatUnitModel.ts index 8858ee5b580e..24a482eccd0e 100644 --- a/packages/model-typings/src/models/ILivechatUnitModel.ts +++ b/packages/model-typings/src/models/ILivechatUnitModel.ts @@ -32,6 +32,8 @@ export interface ILivechatUnitModel extends IBaseModel departments: { departmentId: string }[], ): Promise>; removeParentAndAncestorById(parentId: string): Promise; + incrementDepartmentsCount(_id: string): Promise; + decrementDepartmentsCount(_id: string): Promise; removeById(_id: string): Promise; findOneByIdOrName(_idOrName: string, options: FindOptions): Promise; findByMonitorId(monitorId: string): Promise; diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index a1c714c013b8..b494e5d0e5a9 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -576,7 +576,8 @@ type POSTLivechatDepartmentProps = { chatClosingTags?: string[]; fallbackForwardDepartment?: string; }; - agents: { agentId: string; count?: number; order?: number }[]; + agents?: { agentId: string; count?: number; order?: number }[]; + departmentUnit?: { _id?: string }; }; const POSTLivechatDepartmentSchema = { @@ -645,6 +646,15 @@ const POSTLivechatDepartmentSchema = { }, nullable: true, }, + departmentUnit: { + type: 'object', + properties: { + _id: { + type: 'string', + }, + }, + additionalProperties: false, + }, }, required: ['department'], additionalProperties: false, From fdde637f92928f0f59d3b089c889fdfe979100d0 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Wed, 18 Sep 2024 11:29:22 -0300 Subject: [PATCH 13/39] fix: Local avatars prioritized over external avatar provider and remove remnant references on client of `Accounts_AvatarExternalProviderUrl` (#33296) Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com> --- .changeset/strong-grapes-brake.md | 9 +++++++++ apps/meteor/app/utils/client/getUserAvatarURL.ts | 5 ----- apps/meteor/app/utils/server/getUserAvatarURL.ts | 5 ----- apps/meteor/client/providers/AvatarUrlProvider.tsx | 8 ++------ apps/meteor/server/routes/avatar/user.js | 14 +++++++------- 5 files changed, 18 insertions(+), 23 deletions(-) create mode 100644 .changeset/strong-grapes-brake.md diff --git a/.changeset/strong-grapes-brake.md b/.changeset/strong-grapes-brake.md new file mode 100644 index 000000000000..c867600a8cd2 --- /dev/null +++ b/.changeset/strong-grapes-brake.md @@ -0,0 +1,9 @@ +--- +'@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/apps/meteor/app/utils/client/getUserAvatarURL.ts b/apps/meteor/app/utils/client/getUserAvatarURL.ts index 1a825a44fc27..d5fdd2b2d427 100644 --- a/apps/meteor/app/utils/client/getUserAvatarURL.ts +++ b/apps/meteor/app/utils/client/getUserAvatarURL.ts @@ -1,11 +1,6 @@ -import { settings } from '../../settings/client'; import { getAvatarURL } from './getAvatarURL'; export const getUserAvatarURL = function (username: string, cache = ''): string | undefined { - const externalSource = (settings.get('Accounts_AvatarExternalProviderUrl') || '').trim().replace(/\/$/, ''); - if (externalSource !== '') { - return externalSource.replace('{username}', username); - } if (username == null) { return; } diff --git a/apps/meteor/app/utils/server/getUserAvatarURL.ts b/apps/meteor/app/utils/server/getUserAvatarURL.ts index b83efea1d842..d5fdd2b2d427 100644 --- a/apps/meteor/app/utils/server/getUserAvatarURL.ts +++ b/apps/meteor/app/utils/server/getUserAvatarURL.ts @@ -1,11 +1,6 @@ -import { settings } from '../../settings/server'; import { getAvatarURL } from './getAvatarURL'; export const getUserAvatarURL = function (username: string, cache = ''): string | undefined { - const externalSource = (settings.get('Accounts_AvatarExternalProviderUrl') || '').trim().replace(/\/$/, ''); - if (externalSource !== '') { - return externalSource.replace('{username}', username); - } if (username == null) { return; } diff --git a/apps/meteor/client/providers/AvatarUrlProvider.tsx b/apps/meteor/client/providers/AvatarUrlProvider.tsx index b5a92c9117f2..606da39360d5 100644 --- a/apps/meteor/client/providers/AvatarUrlProvider.tsx +++ b/apps/meteor/client/providers/AvatarUrlProvider.tsx @@ -1,4 +1,4 @@ -import { AvatarUrlContext, useSetting } from '@rocket.chat/ui-contexts'; +import { AvatarUrlContext } from '@rocket.chat/ui-contexts'; import type { ReactNode } from 'react'; import React, { useMemo } from 'react'; @@ -10,19 +10,15 @@ type AvatarUrlProviderProps = { }; const AvatarUrlProvider = ({ children }: AvatarUrlProviderProps) => { - const cdnAvatarUrl = String(useSetting('CDN_PREFIX') || ''); const contextValue = useMemo( () => ({ getUserPathAvatar: ((): ((uid: string, etag?: string) => string) => { - if (cdnAvatarUrl) { - return (uid: string, etag?: string): string => `${cdnAvatarUrl}/avatar/${uid}${etag ? `?etag=${etag}` : ''}`; - } return (uid: string, etag?: string): string => getURL(`/avatar/${uid}${etag ? `?etag=${etag}` : ''}`); })(), getRoomPathAvatar: ({ type, ...room }: any): string => roomCoordinator.getRoomDirectives(type || room.t).getAvatarPath({ username: room._id, ...room }) || '', }), - [cdnAvatarUrl], + [], ); return ; diff --git a/apps/meteor/server/routes/avatar/user.js b/apps/meteor/server/routes/avatar/user.js index 269c2e90019a..1c92d24fe300 100644 --- a/apps/meteor/server/routes/avatar/user.js +++ b/apps/meteor/server/routes/avatar/user.js @@ -32,6 +32,13 @@ export const userAvatar = async function (req, res) { return; } + if (settings.get('Accounts_AvatarExternalProviderUrl')) { + const response = await fetch(settings.get('Accounts_AvatarExternalProviderUrl').replace('{username}', requestUsername)); + response.headers.forEach((value, key) => res.setHeader(key, value)); + response.body.pipe(res); + return; + } + const reqModifiedHeader = req.headers['if-modified-since']; const file = await Avatars.findOneByName(requestUsername); @@ -52,13 +59,6 @@ export const userAvatar = async function (req, res) { return FileUpload.get(file, req, res); } - if (settings.get('Accounts_AvatarExternalProviderUrl')) { - const response = await fetch(settings.get('Accounts_AvatarExternalProviderUrl').replace('{username}', requestUsername)); - response.headers.forEach((value, key) => res.setHeader(key, value)); - response.body.pipe(res); - return; - } - // if still using "letters fallback" if (!wasFallbackModified(reqModifiedHeader, res)) { res.writeHead(304); From ba8bee484b77fd83359f95a3c9c52a33e46b667b Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Wed, 18 Sep 2024 12:21:29 -0300 Subject: [PATCH 14/39] fix: Mark as unread not working (#32939) Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com> --- .changeset/hot-balloons-travel.md | 5 ++ .../client/actionButton.ts | 4 +- .../RoomList/SideBarItemTemplateWithData.tsx | 5 +- apps/meteor/client/sidebar/RoomMenu.tsx | 10 ++- .../RoomList/SidebarItemTemplateWithData.tsx | 3 +- apps/meteor/client/sidebarv2/RoomMenu.tsx | 10 ++- apps/meteor/tests/e2e/mark-unread.spec.ts | 71 +++++++++++++++++++ .../page-objects/fragments/home-sidenav.ts | 22 +++++- .../tests/e2e/page-objects/home-channel.ts | 4 ++ 9 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 .changeset/hot-balloons-travel.md create mode 100644 apps/meteor/tests/e2e/mark-unread.spec.ts diff --git a/.changeset/hot-balloons-travel.md b/.changeset/hot-balloons-travel.md new file mode 100644 index 000000000000..d6154babc49d --- /dev/null +++ b/.changeset/hot-balloons-travel.md @@ -0,0 +1,5 @@ +--- +"@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/apps/meteor/app/message-mark-as-unread/client/actionButton.ts b/apps/meteor/app/message-mark-as-unread/client/actionButton.ts index fc4a9d80c43c..e1e35d216029 100644 --- a/apps/meteor/app/message-mark-as-unread/client/actionButton.ts +++ b/apps/meteor/app/message-mark-as-unread/client/actionButton.ts @@ -19,7 +19,6 @@ Meteor.startup(() => { const { message = messageArgs(this).msg } = props; try { - await sdk.call('unreadMessages', message); const subscription = ChatSubscription.findOne({ rid: message.rid, }); @@ -27,8 +26,9 @@ Meteor.startup(() => { if (subscription == null) { return; } + router.navigate('/home'); await LegacyRoomManager.close(subscription.t + subscription.name); - return router.navigate('/home'); + await sdk.call('unreadMessages', message); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } diff --git a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx index cc7cdfbe7761..35d098151204 100644 --- a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx +++ b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx @@ -167,7 +167,7 @@ function SideBarItemTemplateWithData({ const badges = ( {showBadge && isUnread && ( - + {unread + tunread?.length} )} @@ -180,6 +180,7 @@ function SideBarItemTemplateWithData({ is='a' id={id} data-qa='sidebar-item' + data-unread={highlighted} unread={highlighted} selected={selected} href={href} @@ -205,7 +206,7 @@ function SideBarItemTemplateWithData({ threadUnread={threadUnread} rid={rid} unread={!!unread} - roomOpen={false} + roomOpen={selected} type={type} cl={cl} name={title} diff --git a/apps/meteor/client/sidebar/RoomMenu.tsx b/apps/meteor/client/sidebar/RoomMenu.tsx index 06b1352d2803..05f02220104b 100644 --- a/apps/meteor/client/sidebar/RoomMenu.tsx +++ b/apps/meteor/client/sidebar/RoomMenu.tsx @@ -13,6 +13,7 @@ import { useTranslation, useEndpoint, } from '@rocket.chat/ui-contexts'; +import { useQueryClient } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { memo, useMemo } from 'react'; @@ -100,6 +101,8 @@ const RoomMenu = ({ const isOmnichannelRoom = type === 'l'; const prioritiesMenu = useOmnichannelPrioritiesMenu(rid); + const queryClient = useQueryClient(); + const canLeave = ((): boolean => { if (type === 'c' && !canLeaveChannel) { return false; @@ -173,17 +176,22 @@ const RoomMenu = ({ const handleToggleRead = useMutableCallback(async () => { try { + queryClient.invalidateQueries(['sidebar/search/spotlight']); + if (isUnread) { await readMessages({ rid, readThreads: true }); return; } - await unreadMessages(undefined, rid); + if (subscription == null) { return; } + LegacyRoomManager.close(subscription.t + subscription.name); router.navigate('/home'); + + await unreadMessages(undefined, rid); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } diff --git a/apps/meteor/client/sidebarv2/RoomList/SidebarItemTemplateWithData.tsx b/apps/meteor/client/sidebarv2/RoomList/SidebarItemTemplateWithData.tsx index 51b8ce495af6..e1f66ba93b4e 100644 --- a/apps/meteor/client/sidebarv2/RoomList/SidebarItemTemplateWithData.tsx +++ b/apps/meteor/client/sidebarv2/RoomList/SidebarItemTemplateWithData.tsx @@ -180,6 +180,7 @@ const SidebarItemTemplateWithData = ({ is='a' id={id} data-qa='sidebar-item' + data-unread={highlighted} unread={highlighted} selected={selected} href={href} @@ -205,7 +206,7 @@ const SidebarItemTemplateWithData = ({ threadUnread={threadUnread} rid={rid} unread={!!unread} - roomOpen={false} + roomOpen={selected} type={type} cl={cl} name={title} diff --git a/apps/meteor/client/sidebarv2/RoomMenu.tsx b/apps/meteor/client/sidebarv2/RoomMenu.tsx index e88225df40ca..aac68b9ef79e 100644 --- a/apps/meteor/client/sidebarv2/RoomMenu.tsx +++ b/apps/meteor/client/sidebarv2/RoomMenu.tsx @@ -13,6 +13,7 @@ import { useTranslation, useEndpoint, } from '@rocket.chat/ui-contexts'; +import { useQueryClient } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { memo, useMemo } from 'react'; @@ -100,6 +101,8 @@ const RoomMenu = ({ const isOmnichannelRoom = type === 'l'; const prioritiesMenu = useOmnichannelPrioritiesMenu(rid); + const queryClient = useQueryClient(); + const canLeave = ((): boolean => { if (type === 'c' && !canLeaveChannel) { return false; @@ -173,17 +176,22 @@ const RoomMenu = ({ const handleToggleRead = useEffectEvent(async () => { try { + queryClient.invalidateQueries(['sidebar/search/spotlight']); + if (isUnread) { await readMessages({ rid, readThreads: true }); return; } - await unreadMessages(undefined, rid); + if (subscription == null) { return; } + LegacyRoomManager.close(subscription.t + subscription.name); router.navigate('/home'); + + await unreadMessages(undefined, rid); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } diff --git a/apps/meteor/tests/e2e/mark-unread.spec.ts b/apps/meteor/tests/e2e/mark-unread.spec.ts new file mode 100644 index 000000000000..81ae93965856 --- /dev/null +++ b/apps/meteor/tests/e2e/mark-unread.spec.ts @@ -0,0 +1,71 @@ +import { createAuxContext } from './fixtures/createAuxContext'; +import { Users } from './fixtures/userStates'; +import { HomeChannel } from './page-objects'; +import { createTargetChannel } from './utils'; +import { test, expect } from './utils/test'; + +test.use({ storageState: Users.admin.state }); + +test.describe.serial('mark-unread', () => { + let poHomeChannel: HomeChannel; + let targetChannel: string; + + test.beforeEach(async ({ page, api }) => { + poHomeChannel = new HomeChannel(page); + targetChannel = await createTargetChannel(api, { members: ['user2'] }); + + await page.emulateMedia({ reducedMotion: 'reduce' }); + await page.goto('/home'); + }); + + test.afterEach(async ({ api }) => { + await api.post('/channels.delete', { roomName: targetChannel }); + }); + + test.describe('Mark Unread - Sidebar Action', () => { + test('should not mark empty room as unread', async () => { + await poHomeChannel.sidenav.selectMarkAsUnread(targetChannel); + + await expect(poHomeChannel.sidenav.getRoomBadge(targetChannel)).not.toBeVisible(); + }); + + test('should mark a populated room as unread', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.content.sendMessage('this is a message for reply'); + await poHomeChannel.sidenav.selectMarkAsUnread(targetChannel); + + await expect(poHomeChannel.sidenav.getRoomBadge(targetChannel)).toBeVisible(); + }); + + test('should mark a populated room as unread - search', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.content.sendMessage('this is a message for reply'); + await poHomeChannel.sidenav.selectMarkAsUnread(targetChannel); + await poHomeChannel.sidenav.searchRoom(targetChannel); + + await expect(poHomeChannel.sidenav.getSearchChannelBadge(targetChannel)).toBeVisible(); + }); + }); + + test.describe('Mark Unread - Message Action', () => { + let poHomeChannelUser2: HomeChannel; + + test('should mark a populated room as unread', async ({ browser }) => { + const { page: user2Page } = await createAuxContext(browser, Users.user2); + poHomeChannelUser2 = new HomeChannel(user2Page); + + await poHomeChannelUser2.sidenav.openChat(targetChannel); + await poHomeChannelUser2.content.sendMessage('this is a message for reply'); + await user2Page.close(); + + await poHomeChannel.sidenav.openChat(targetChannel); + + // wait for the sidebar item to be read + await poHomeChannel.sidenav.getSidebarItemByName(targetChannel, true).waitFor(); + await poHomeChannel.content.openLastMessageMenu(); + await poHomeChannel.markUnread.click(); + + await expect(poHomeChannel.sidenav.getRoomBadge(targetChannel)).toBeVisible(); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts index d0bdd5028010..823469d02a96 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts @@ -68,8 +68,18 @@ export class HomeSidenav { return this.page.locator('role=menuitemcheckbox[name="Profile"]'); } - getSidebarItemByName(name: string): Locator { - return this.page.locator(`[data-qa="sidebar-item"][aria-label="${name}"]`); + // TODO: refactor getSidebarItemByName to not use data-qa + getSidebarItemByName(name: string, isRead?: boolean): Locator { + return this.page.locator( + ['[data-qa="sidebar-item"]', `[aria-label="${name}"]`, isRead && '[data-unread="false"]'].filter(Boolean).join(''), + ); + } + + async selectMarkAsUnread(name: string) { + const sidebarItem = this.getSidebarItemByName(name); + await sidebarItem.focus(); + await sidebarItem.locator('.rcx-sidebar-item__menu').click(); + await this.page.getByRole('option', { name: 'Mark Unread' }).click(); } async selectPriority(name: string, priority: string) { @@ -170,4 +180,12 @@ export class HomeSidenav { await this.checkboxEncryption.click(); await this.btnCreate.click(); } + + getRoomBadge(roomName: string): Locator { + return this.getSidebarItemByName(roomName).getByRole('status', { exact: true }); + } + + getSearchChannelBadge(name: string): Locator { + return this.page.locator(`[data-qa="sidebar-item"][aria-label="${name}"]`).first().getByRole('status', { exact: true }); + } } diff --git a/apps/meteor/tests/e2e/page-objects/home-channel.ts b/apps/meteor/tests/e2e/page-objects/home-channel.ts index a2784ea4c67b..ce51896eb449 100644 --- a/apps/meteor/tests/e2e/page-objects/home-channel.ts +++ b/apps/meteor/tests/e2e/page-objects/home-channel.ts @@ -59,4 +59,8 @@ export class HomeChannel { get roomHeaderToolbar(): Locator { return this.page.locator('[role=toolbar][aria-label="Primary Room actions"]'); } + + get markUnread(): Locator { + return this.page.locator('role=menuitem[name="Mark Unread"]'); + } } From 6a3c25c62c33e81fb286e364dedf084c64ad8ef8 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 18 Sep 2024 10:39:21 -0600 Subject: [PATCH 15/39] refactor: Remove old `setreaction` callbacks and use new after/before callbacks (#33309) --- .../reactions/client/methods/setReaction.ts | 4 ---- .../app/reactions/server/setReaction.ts | 2 -- .../app/slackbridge/server/RocketAdapter.js | 22 +++++++++---------- apps/meteor/lib/callbacks.ts | 2 -- 4 files changed, 10 insertions(+), 20 deletions(-) diff --git a/apps/meteor/app/reactions/client/methods/setReaction.ts b/apps/meteor/app/reactions/client/methods/setReaction.ts index ed15cda9ab8e..1744d49c0ceb 100644 --- a/apps/meteor/app/reactions/client/methods/setReaction.ts +++ b/apps/meteor/app/reactions/client/methods/setReaction.ts @@ -3,7 +3,6 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Meteor } from 'meteor/meteor'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; -import { callbacks } from '../../../../lib/callbacks'; import { emoji } from '../../../emoji/client'; import { Messages, ChatRoom, Subscriptions } from '../../../models/client'; @@ -55,10 +54,8 @@ Meteor.methods({ if (!message.reactions || typeof message.reactions !== 'object' || Object.keys(message.reactions).length === 0) { delete message.reactions; Messages.update({ _id: messageId }, { $unset: { reactions: 1 } }); - await callbacks.run('unsetReaction', messageId, reaction); } else { Messages.update({ _id: messageId }, { $set: { reactions: message.reactions } }); - await callbacks.run('setReaction', messageId, reaction); } } else { if (!message.reactions) { @@ -72,7 +69,6 @@ Meteor.methods({ message.reactions[reaction].usernames.push(user.username); Messages.update({ _id: messageId }, { $set: { reactions: message.reactions } }); - await callbacks.run('setReaction', messageId, reaction); } }, }); diff --git a/apps/meteor/app/reactions/server/setReaction.ts b/apps/meteor/app/reactions/server/setReaction.ts index d513c8dda6a5..8f9c24633407 100644 --- a/apps/meteor/app/reactions/server/setReaction.ts +++ b/apps/meteor/app/reactions/server/setReaction.ts @@ -85,7 +85,6 @@ async function setReaction(room: IRoom, user: IUser, message: IMessage, reaction await Rooms.setReactionsInLastMessage(room._id, message.reactions); } } - await callbacks.run('unsetReaction', message._id, reaction); await callbacks.run('afterUnsetReaction', message, { user, reaction, shouldReact, oldMessage }); isReacted = false; @@ -104,7 +103,6 @@ async function setReaction(room: IRoom, user: IUser, message: IMessage, reaction await Rooms.setReactionsInLastMessage(room._id, message.reactions); void notifyOnRoomChangedById(room._id); } - await callbacks.run('setReaction', message._id, reaction); await callbacks.run('afterSetReaction', message, { user, reaction, shouldReact }); isReacted = true; diff --git a/apps/meteor/app/slackbridge/server/RocketAdapter.js b/apps/meteor/app/slackbridge/server/RocketAdapter.js index f76c33fa1f81..8ba2a76dcbc2 100644 --- a/apps/meteor/app/slackbridge/server/RocketAdapter.js +++ b/apps/meteor/app/slackbridge/server/RocketAdapter.js @@ -45,16 +45,16 @@ export default class RocketAdapter { rocketLogger.debug('Register for events'); callbacks.add('afterSaveMessage', this.onMessage.bind(this), callbacks.priority.LOW, 'SlackBridge_Out'); callbacks.add('afterDeleteMessage', this.onMessageDelete.bind(this), callbacks.priority.LOW, 'SlackBridge_Delete'); - callbacks.add('setReaction', this.onSetReaction.bind(this), callbacks.priority.LOW, 'SlackBridge_SetReaction'); - callbacks.add('unsetReaction', this.onUnSetReaction.bind(this), callbacks.priority.LOW, 'SlackBridge_UnSetReaction'); + callbacks.add('afterSetReaction', this.onSetReaction.bind(this), callbacks.priority.LOW, 'SlackBridge_SetReaction'); + callbacks.add('afterUnsetReaction', this.onUnSetReaction.bind(this), callbacks.priority.LOW, 'SlackBridge_UnSetReaction'); } unregisterForEvents() { rocketLogger.debug('Unregister for events'); callbacks.remove('afterSaveMessage', 'SlackBridge_Out'); callbacks.remove('afterDeleteMessage', 'SlackBridge_Delete'); - callbacks.remove('setReaction', 'SlackBridge_SetReaction'); - callbacks.remove('unsetReaction', 'SlackBridge_UnSetReaction'); + callbacks.remove('afterSetReaction', 'SlackBridge_SetReaction'); + callbacks.remove('afterUnsetReaction', 'SlackBridge_UnSetReaction'); } async onMessageDelete(rocketMessageDeleted) { @@ -72,7 +72,7 @@ export default class RocketAdapter { } } - async onSetReaction(rocketMsgID, reaction) { + async onSetReaction(rocketMsg, { reaction }) { try { if (!this.slackBridge.isReactionsEnabled) { return; @@ -80,12 +80,11 @@ export default class RocketAdapter { rocketLogger.debug('onRocketSetReaction'); - if (rocketMsgID && reaction) { - if (this.slackBridge.reactionsMap.delete(`set${rocketMsgID}${reaction}`)) { + if (rocketMsg._id && reaction) { + if (this.slackBridge.reactionsMap.delete(`set${rocketMsg._id}${reaction}`)) { // This was a Slack reaction, we don't need to tell Slack about it return; } - const rocketMsg = await Messages.findOneById(rocketMsgID); if (rocketMsg) { for await (const slack of this.slackAdapters) { const slackChannel = slack.getSlackChannel(rocketMsg.rid); @@ -101,7 +100,7 @@ export default class RocketAdapter { } } - async onUnSetReaction(rocketMsgID, reaction) { + async onUnSetReaction(rocketMsg, { reaction }) { try { if (!this.slackBridge.isReactionsEnabled) { return; @@ -109,13 +108,12 @@ export default class RocketAdapter { rocketLogger.debug('onRocketUnSetReaction'); - if (rocketMsgID && reaction) { - if (this.slackBridge.reactionsMap.delete(`unset${rocketMsgID}${reaction}`)) { + if (rocketMsg._id && reaction) { + if (this.slackBridge.reactionsMap.delete(`unset${rocketMsg._id}${reaction}`)) { // This was a Slack unset reaction, we don't need to tell Slack about it return; } - const rocketMsg = await Messages.findOneById(rocketMsgID); if (rocketMsg) { for await (const slack of this.slackAdapters) { const slackChannel = slack.getSlackChannel(rocketMsg.rid); diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 57b8527d5008..dcfd7a021c5e 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -256,10 +256,8 @@ export type Hook = | 'onValidateLogin' | 'openBroadcast' | 'renderNotification' - | 'setReaction' | 'streamMessage' | 'streamNewMessage' - | 'unsetReaction' | 'userAvatarSet' | 'userConfirmationEmailRequested' | 'userForgotPasswordEmailRequested' From d0595a398050b55386eab8330752a24114a23bfa Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 18 Sep 2024 12:24:47 -0600 Subject: [PATCH 16/39] fix: `LivechatSessionTaken` webhook event called without `agent` param (#33209) --- .changeset/spicy-rocks-burn.md | 5 +++++ .../app/livechat/server/lib/RoutingManager.ts | 15 ++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 .changeset/spicy-rocks-burn.md diff --git a/.changeset/spicy-rocks-burn.md b/.changeset/spicy-rocks-burn.md new file mode 100644 index 000000000000..6468dbbec241 --- /dev/null +++ b/.changeset/spicy-rocks-burn.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed `LivechatSessionTaken` webhook event being called without the `agent` param, which represents the agent serving the room. diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index f4a2288305e5..28e5c72efc16 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -265,11 +265,20 @@ export const RoutingManager: Routing = { logger.info(`Inquiry ${inquiry._id} taken by agent ${agent.agentId}`); + // assignAgent changes the room data to add the agent serving the conversation. afterTakeInquiry expects room object to be updated + const inq = await this.assignAgent(inquiry as InquiryWithAgentInfo, room, agent); + const roomAfterUpdate = await LivechatRooms.findOneById(rid); + + if (!roomAfterUpdate) { + // This should never happen + throw new Error('error-room-not-found'); + } + callbacks.runAsync( 'livechat.afterTakeInquiry', { - inquiry: await this.assignAgent(inquiry as InquiryWithAgentInfo, room, agent), - room, + inquiry: inq, + room: roomAfterUpdate, }, agent, ); @@ -282,7 +291,7 @@ export const RoutingManager: Routing = { queuedAt: undefined, }); - return LivechatRooms.findOneById(rid); + return roomAfterUpdate; }, async transferRoom(room, guest, transferData) { From a541f64c8ca02e4448a4bfbff2699d6e96f17b69 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 19 Sep 2024 10:42:07 -0300 Subject: [PATCH 17/39] fix: error on sendmessage stub (#33317) --- .changeset/cyan-ladybugs-thank.md | 5 +++++ apps/meteor/app/lib/client/methods/sendMessage.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/cyan-ladybugs-thank.md diff --git a/.changeset/cyan-ladybugs-thank.md b/.changeset/cyan-ladybugs-thank.md new file mode 100644 index 000000000000..377a014fcb72 --- /dev/null +++ b/.changeset/cyan-ladybugs-thank.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed error during sendmessage client stub diff --git a/apps/meteor/app/lib/client/methods/sendMessage.ts b/apps/meteor/app/lib/client/methods/sendMessage.ts index bdaca587493a..19220f901458 100644 --- a/apps/meteor/app/lib/client/methods/sendMessage.ts +++ b/apps/meteor/app/lib/client/methods/sendMessage.ts @@ -43,7 +43,7 @@ Meteor.methods({ await onClientMessageReceived(message as IMessage).then((message) => { ChatMessage.insert(message); - return callbacks.run('afterSaveMessage', message, room); + return callbacks.run('afterSaveMessage', message, { room }); }); }, }); From 40339749b3682e0c7f804787fab6e49eb9cef080 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 19 Sep 2024 15:34:50 -0300 Subject: [PATCH 18/39] feat: contextualbar based on chat size (#33321) --- .changeset/kind-llamas-grin.md | 5 + .../Contextualbar/ContextualbarDialog.tsx | 6 +- .../client/views/room/layout/RoomLayout.tsx | 104 +++++++++++++----- 3 files changed, 83 insertions(+), 32 deletions(-) create mode 100644 .changeset/kind-llamas-grin.md diff --git a/.changeset/kind-llamas-grin.md b/.changeset/kind-llamas-grin.md new file mode 100644 index 000000000000..fd349e82d7f9 --- /dev/null +++ b/.changeset/kind-llamas-grin.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Changed the contextualbar behavior based on chat size instead the viewport diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarDialog.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarDialog.tsx index 23def16a94a1..4e4640270087 100644 --- a/apps/meteor/client/components/Contextualbar/ContextualbarDialog.tsx +++ b/apps/meteor/client/components/Contextualbar/ContextualbarDialog.tsx @@ -18,7 +18,7 @@ type ContextualbarDialogProps = AriaDialogProps & ComponentProps { const ref = useRef(null); const { dialogProps } = useDialog({ 'aria-labelledby': 'contextualbarTitle', ...props }, ref); - const sizes = useLayoutSizes(); + const { contextualBar } = useLayoutSizes(); const position = useLayoutContextualBarPosition(); const { closeTab } = useRoomToolbox(); @@ -42,12 +42,12 @@ const ContextualbarDialog = (props: ContextualbarDialogProps) => { - + - + diff --git a/apps/meteor/client/views/room/layout/RoomLayout.tsx b/apps/meteor/client/views/room/layout/RoomLayout.tsx index fd080b916b70..b5e29c05799f 100644 --- a/apps/meteor/client/views/room/layout/RoomLayout.tsx +++ b/apps/meteor/client/views/room/layout/RoomLayout.tsx @@ -1,7 +1,11 @@ +/* eslint-disable no-nested-ternary */ import { Box } from '@rocket.chat/fuselage'; +import { useResizeObserver } from '@rocket.chat/fuselage-hooks'; +import breakpointsDefinitions from '@rocket.chat/fuselage-tokens/breakpoints.json'; import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; +import { LayoutContext, useLayout } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement, ReactNode } from 'react'; -import React, { Suspense } from 'react'; +import React, { Suspense, useMemo } from 'react'; import { ContextualbarDialog } from '../../../components/Contextualbar'; import HeaderSkeleton from '../Header/HeaderSkeleton'; @@ -14,36 +18,78 @@ type RoomLayoutProps = { aside?: ReactNode; } & ComponentProps; -const RoomLayout = ({ header, body, footer, aside, ...props }: RoomLayoutProps): ReactElement => ( - - - - - - - - - - } +const useBreakpointsElement = () => { + const { ref, borderBoxSize } = useResizeObserver({ + debounceDelay: 30, + }); + + const breakpoints = useMemo( + () => + breakpointsDefinitions + .filter(({ minViewportWidth }) => minViewportWidth && borderBoxSize.inlineSize && borderBoxSize.inlineSize >= minViewportWidth) + .map(({ name }) => name), + [borderBoxSize], + ); + + return { + ref, + breakpoints, + }; +}; + +const RoomLayout = ({ header, body, footer, aside, ...props }: RoomLayoutProps): ReactElement => { + const { ref, breakpoints } = useBreakpointsElement(); + + const contextualbarPosition = breakpoints.includes('md') ? 'relative' : 'absolute'; + const contextualbarSize = breakpoints.includes('sm') ? (breakpoints.includes('xl') ? '38%' : '380px') : '100%'; + + const layout = useLayout(); + + return ( + ({ + ...layout, + contextualBarPosition: contextualbarPosition, + size: { + ...layout.size, + contextualBar: contextualbarSize, + }, + }), + [layout, contextualbarPosition, contextualbarSize], + )} > - {header} - - - - - {body} + + + + + + + + + + } + > + {header} + + + + + {body} + + {footer && {footer}} + + {aside && ( + + {aside} + + )} - {footer && {footer}} - {aside && ( - - {aside} - - )} - - -); + + ); +}; export default RoomLayout; From 12d630799827e75e4b3964881e79fe28d0ad81d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Thu, 19 Sep 2024 23:24:05 +0100 Subject: [PATCH 19/39] feat: `RoomSidepanel` (#33225) Co-authored-by: Guilherme Gazzo <5263975+ggazzo@users.noreply.github.com> --- .changeset/witty-lemons-type.md | 10 ++ apps/meteor/app/api/server/v1/rooms.ts | 2 +- .../FeaturePreviewSidePanelNavigation.tsx | 10 ++ .../client/hooks/useRoomInfoEndpoint.ts | 4 +- .../client/hooks/useSidePanelNavigation.ts | 14 +++ apps/meteor/client/lib/RoomManager.ts | 28 ++++- .../sidebarv2/header/CreateTeamModal.tsx | 62 ++++++++++ .../client/sidebarv2/header/SearchSection.tsx | 30 ++++- apps/meteor/client/views/room/RoomOpener.tsx | 80 +++++++------ .../views/room/Sidepanel/RoomSidepanel.tsx | 66 +++++++++++ .../Sidepanel/RoomSidepanelListWrapper.tsx | 19 ++++ .../room/Sidepanel/RoomSidepanelLoading.tsx | 20 ++++ .../SidepanelItem/RoomSidepanelItem.tsx | 29 +++++ .../room/Sidepanel/SidepanelItem/index.ts | 1 + .../room/Sidepanel/hooks/useItemData.tsx | 68 +++++++++++ .../Sidepanel/hooks/useTeamslistChildren.ts | 106 ++++++++++++++++++ .../client/views/room/Sidepanel/index.ts | 1 + .../Info/EditRoomInfo/EditRoomInfo.tsx | 68 ++++++++++- .../EditRoomInfo/useEditRoomInitialValues.ts | 18 ++- .../client/views/room/layout/RoomLayout.tsx | 4 +- .../views/room/providers/RoomProvider.tsx | 77 +++++++++++-- apps/meteor/lib/publishFields.ts | 1 + apps/meteor/server/services/team/service.ts | 6 +- packages/core-typings/src/IRoom.ts | 3 + packages/i18n/src/locales/en.i18n.json | 4 +- packages/rest-typings/src/v1/rooms.ts | 2 +- .../FeaturePreview/FeaturePreview.tsx | 12 +- .../src/hooks/useFeaturePreviewList.ts | 2 +- 28 files changed, 689 insertions(+), 58 deletions(-) create mode 100644 .changeset/witty-lemons-type.md create mode 100644 apps/meteor/client/components/FeaturePreviewSidePanelNavigation.tsx create mode 100644 apps/meteor/client/hooks/useSidePanelNavigation.ts create mode 100644 apps/meteor/client/views/room/Sidepanel/RoomSidepanel.tsx create mode 100644 apps/meteor/client/views/room/Sidepanel/RoomSidepanelListWrapper.tsx create mode 100644 apps/meteor/client/views/room/Sidepanel/RoomSidepanelLoading.tsx create mode 100644 apps/meteor/client/views/room/Sidepanel/SidepanelItem/RoomSidepanelItem.tsx create mode 100644 apps/meteor/client/views/room/Sidepanel/SidepanelItem/index.ts create mode 100644 apps/meteor/client/views/room/Sidepanel/hooks/useItemData.tsx create mode 100644 apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts create mode 100644 apps/meteor/client/views/room/Sidepanel/index.ts diff --git a/.changeset/witty-lemons-type.md b/.changeset/witty-lemons-type.md new file mode 100644 index 000000000000..a007cbe6260e --- /dev/null +++ b/.changeset/witty-lemons-type.md @@ -0,0 +1,10 @@ +--- +'@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/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 5da59d977fb1..3dc62e462ddf 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -420,7 +420,7 @@ API.v1.addRoute( const discussionParent = room.prid && (await Rooms.findOneById>(room.prid, { - projection: { name: 1, fname: 1, t: 1, prid: 1, u: 1 }, + projection: { name: 1, fname: 1, t: 1, prid: 1, u: 1, sidepanel: 1 }, })); const { team, parentRoom } = await Team.getRoomInfo(room); const parent = discussionParent || parentRoom; diff --git a/apps/meteor/client/components/FeaturePreviewSidePanelNavigation.tsx b/apps/meteor/client/components/FeaturePreviewSidePanelNavigation.tsx new file mode 100644 index 000000000000..f5d658ccb2f2 --- /dev/null +++ b/apps/meteor/client/components/FeaturePreviewSidePanelNavigation.tsx @@ -0,0 +1,10 @@ +import { FeaturePreview } from '@rocket.chat/ui-client'; +import type { ReactElement } from 'react'; +import React from 'react'; + +import { useSidePanelNavigationScreenSize } from '../hooks/useSidePanelNavigation'; + +export const FeaturePreviewSidePanelNavigation = ({ children }: { children: ReactElement[] }) => { + const disabled = !useSidePanelNavigationScreenSize(); + return ; +}; diff --git a/apps/meteor/client/hooks/useRoomInfoEndpoint.ts b/apps/meteor/client/hooks/useRoomInfoEndpoint.ts index 47ea84af20b6..0bac1d7eb413 100644 --- a/apps/meteor/client/hooks/useRoomInfoEndpoint.ts +++ b/apps/meteor/client/hooks/useRoomInfoEndpoint.ts @@ -1,6 +1,6 @@ import type { IRoom } from '@rocket.chat/core-typings'; import type { OperationResult } from '@rocket.chat/rest-typings'; -import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useUserId } from '@rocket.chat/ui-contexts'; import type { UseQueryResult } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; import { minutesToMilliseconds } from 'date-fns'; @@ -8,6 +8,7 @@ import type { Meteor } from 'meteor/meteor'; export const useRoomInfoEndpoint = (rid: IRoom['_id']): UseQueryResult> => { const getRoomInfo = useEndpoint('GET', '/v1/rooms.info'); + const uid = useUserId(); return useQuery(['/v1/rooms.info', rid], () => getRoomInfo({ roomId: rid }), { cacheTime: minutesToMilliseconds(15), staleTime: minutesToMilliseconds(5), @@ -17,5 +18,6 @@ export const useRoomInfoEndpoint = (rid: IRoom['_id']): UseQueryResult { + const isSidepanelFeatureEnabled = useFeaturePreview('sidepanelNavigation'); + // ["xs", "sm", "md", "lg", "xl", xxl"] + return useSidePanelNavigationScreenSize() && isSidepanelFeatureEnabled; +}; + +export const useSidePanelNavigationScreenSize = () => { + const breakpoints = useBreakpoints(); + // ["xs", "sm", "md", "lg", "xl", xxl"] + return breakpoints.includes('lg'); +}; diff --git a/apps/meteor/client/lib/RoomManager.ts b/apps/meteor/client/lib/RoomManager.ts index 34f64e4f4c78..840493aae406 100644 --- a/apps/meteor/client/lib/RoomManager.ts +++ b/apps/meteor/client/lib/RoomManager.ts @@ -55,6 +55,8 @@ export const RoomManager = new (class RoomManager extends Emitter<{ private rooms: Map = new Map(); + private parentRid?: IRoom['_id'] | undefined; + constructor() { super(); debugRoomManager && @@ -78,6 +80,13 @@ export const RoomManager = new (class RoomManager extends Emitter<{ } get opened(): IRoom['_id'] | undefined { + return this.parentRid ?? this.rid; + } + + get openedSecondLevel(): IRoom['_id'] | undefined { + if (!this.parentRid) { + return undefined; + } return this.rid; } @@ -106,20 +115,28 @@ export const RoomManager = new (class RoomManager extends Emitter<{ this.emit('changed', this.rid); } - open(rid: IRoom['_id']): void { + private _open(rid: IRoom['_id'], parent?: IRoom['_id']): void { if (rid === this.rid) { return; } - this.back(rid); if (!this.rooms.has(rid)) { this.rooms.set(rid, new RoomStore(rid)); } this.rid = rid; + this.parentRid = parent; this.emit('opened', this.rid); this.emit('changed', this.rid); } + open(rid: IRoom['_id']): void { + this._open(rid); + } + + openSecondLevel(parentId: IRoom['_id'], rid: IRoom['_id']): void { + this._open(rid, parentId); + } + getStore(rid: IRoom['_id']): RoomStore | undefined { return this.rooms.get(rid); } @@ -130,4 +147,11 @@ const subscribeOpenedRoom = [ (): IRoom['_id'] | undefined => RoomManager.opened, ] as const; +const subscribeOpenedSecondLevelRoom = [ + (callback: () => void): (() => void) => RoomManager.on('changed', callback), + (): IRoom['_id'] | undefined => RoomManager.openedSecondLevel, +] as const; + export const useOpenedRoom = (): IRoom['_id'] | undefined => useSyncExternalStore(...subscribeOpenedRoom); + +export const useSecondLevelOpenedRoom = (): IRoom['_id'] | undefined => useSyncExternalStore(...subscribeOpenedSecondLevelRoom); diff --git a/apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx b/apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx index a7e7b506de0f..9de721d8bbcd 100644 --- a/apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx +++ b/apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx @@ -1,3 +1,4 @@ +import type { SidepanelItem } from '@rocket.chat/core-typings'; import { Box, Button, @@ -16,6 +17,7 @@ import { AccordionItem, } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; import { useEndpoint, usePermission, @@ -40,6 +42,8 @@ type CreateTeamModalInputs = { encrypted: boolean; broadcast: boolean; members?: string[]; + showDiscussions?: boolean; + showChannels?: boolean; }; type CreateTeamModalProps = { onClose: () => void }; @@ -50,6 +54,7 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => { const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms'); const namesValidation = useSetting('UTF8_Channel_Names_Validation'); const allowSpecialNames = useSetting('UI_Allow_room_names_with_special_chars'); + const dispatchToastMessage = useToastMessageDispatch(); const canCreateTeam = usePermission('create-team'); const canSetReadOnly = usePermissionWithScopedRoles('set-readonly', ['owner']); @@ -94,6 +99,8 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => { encrypted: (e2eEnabledForPrivateByDefault as boolean) ?? false, broadcast: false, members: [], + showChannels: true, + showDiscussions: true, }, }); @@ -123,7 +130,10 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => { topic, broadcast, encrypted, + showChannels, + showDiscussions, }: CreateTeamModalInputs): Promise => { + const sidepanelItem = [showChannels && 'channels', showDiscussions && 'discussions'].filter(Boolean) as [SidepanelItem, SidepanelItem?]; const params = { name, members, @@ -136,6 +146,7 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => { encrypted, }, }, + ...((showChannels || showDiscussions) && { sidepanel: { items: sidepanelItem } }), }; try { @@ -157,6 +168,8 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => { const encryptedId = useUniqueId(); const broadcastId = useUniqueId(); const addMembersId = useUniqueId(); + const showChannelsId = useUniqueId(); + const showDiscussionsId = useUniqueId(); return ( { + + {null} + + + + {t('Navigation')} + + + + {t('Channels')} + ( + + )} + /> + + {t('Show_channels_description')} + + + + + {t('Discussions')} + ( + + )} + /> + + {t('Show_discussions_description')} + + + + {t('Security_and_permissions')} diff --git a/apps/meteor/client/sidebarv2/header/SearchSection.tsx b/apps/meteor/client/sidebarv2/header/SearchSection.tsx index 660b8ee19cd5..71b26b2e4053 100644 --- a/apps/meteor/client/sidebarv2/header/SearchSection.tsx +++ b/apps/meteor/client/sidebarv2/header/SearchSection.tsx @@ -22,6 +22,32 @@ const wrapperStyle = css` background-color: ${Palette.surface['surface-sidebar']}; `; +const mobileCheck = function () { + let check = false; + (function (a: string) { + if ( + /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test( + a, + ) || + /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( + a.substr(0, 4), + ) + ) + check = true; + })(navigator.userAgent || navigator.vendor || window.opera || ''); + return check; +}; + +const shortcut = ((): string => { + if (navigator.userAgentData?.mobile || mobileCheck()) { + return ''; + } + if (window.navigator.platform.toLowerCase().includes('mac')) { + return '(\u2318+K)'; + } + return '(Ctrl+K)'; +})(); + const SearchSection = () => { const t = useTranslation(); const user = useUser(); @@ -68,11 +94,13 @@ const SearchSection = () => { }; }, [handleEscSearch, setFocus]); + const placeholder = [t('Search'), shortcut].filter(Boolean).join(' '); + return ( import('./providers/RoomProvider')); @@ -23,46 +26,59 @@ type RoomOpenerProps = { reference: string; }; +const isDirectOrOmnichannelRoom = (type: RoomType) => type === 'd' || type === 'l'; + const RoomOpener = ({ type, reference }: RoomOpenerProps): ReactElement => { const { data, error, isSuccess, isError, isLoading } = useOpenRoom({ type, reference }); const { t } = useTranslation(); return ( - }> - {isLoading && } - {isSuccess && ( - - - + + {!isDirectOrOmnichannelRoom(type) && ( + + {null} + + + + )} - {isError && - (() => { - if (error instanceof OldUrlRoomError) { - return ; - } - if (error instanceof RoomNotFoundError) { - return ; - } + }> + {isLoading && } + {isSuccess && ( + + + + )} + {isError && + (() => { + if (error instanceof OldUrlRoomError) { + return ; + } + + if (error instanceof RoomNotFoundError) { + return ; + } - if (error instanceof NotAuthorizedError) { - return ; - } + if (error instanceof NotAuthorizedError) { + return ; + } - return ( - } - body={ - - - {t('core.Error')} - {getErrorMessage(error)} - - } - /> - ); - })()} - + return ( + } + body={ + + + {t('core.Error')} + {getErrorMessage(error)} + + } + /> + ); + })()} + + ); }; diff --git a/apps/meteor/client/views/room/Sidepanel/RoomSidepanel.tsx b/apps/meteor/client/views/room/Sidepanel/RoomSidepanel.tsx new file mode 100644 index 000000000000..27c45e2774e8 --- /dev/null +++ b/apps/meteor/client/views/room/Sidepanel/RoomSidepanel.tsx @@ -0,0 +1,66 @@ +/* eslint-disable react/no-multi-comp */ +import { Box, Sidepanel, SidepanelListItem } from '@rocket.chat/fuselage'; +import { useUserPreference } from '@rocket.chat/ui-contexts'; +import React, { memo } from 'react'; +import { Virtuoso } from 'react-virtuoso'; + +import { VirtuosoScrollbars } from '../../../components/CustomScrollbars'; +import { useRoomInfoEndpoint } from '../../../hooks/useRoomInfoEndpoint'; +import { useOpenedRoom, useSecondLevelOpenedRoom } from '../../../lib/RoomManager'; +import RoomSidepanelListWrapper from './RoomSidepanelListWrapper'; +import RoomSidepanelLoading from './RoomSidepanelLoading'; +import RoomSidepanelItem from './SidepanelItem'; +import { useTeamsListChildrenUpdate } from './hooks/useTeamslistChildren'; + +const RoomSidepanel = () => { + const parentRid = useOpenedRoom(); + const secondLevelOpenedRoom = useSecondLevelOpenedRoom() ?? parentRid; + + if (!parentRid || !secondLevelOpenedRoom) { + return null; + } + + return ; +}; + +const RoomSidepanelWithData = ({ parentRid, openedRoom }: { parentRid: string; openedRoom: string }) => { + const sidebarViewMode = useUserPreference<'extended' | 'medium' | 'condensed'>('sidebarViewMode'); + + const roomInfo = useRoomInfoEndpoint(parentRid); + const sidepanelItems = roomInfo.data?.room?.sidepanel?.items || roomInfo.data?.parent?.sidepanel?.items; + + const result = useTeamsListChildrenUpdate( + parentRid, + !roomInfo.data ? null : roomInfo.data.room?.teamId, + // eslint-disable-next-line no-nested-ternary + !sidepanelItems ? null : sidepanelItems?.length === 1 ? sidepanelItems[0] : undefined, + ); + if (roomInfo.isSuccess && !roomInfo.data.room?.sidepanel && !roomInfo.data.parent?.sidepanel) { + return null; + } + + if (roomInfo.isLoading || (roomInfo.isSuccess && result.isLoading)) { + return ; + } + + if (!result.isSuccess || !roomInfo.isSuccess) { + return null; + } + + return ( + + + ( + + )} + /> + + + ); +}; + +export default memo(RoomSidepanel); diff --git a/apps/meteor/client/views/room/Sidepanel/RoomSidepanelListWrapper.tsx b/apps/meteor/client/views/room/Sidepanel/RoomSidepanelListWrapper.tsx new file mode 100644 index 000000000000..dd9e6e6ec221 --- /dev/null +++ b/apps/meteor/client/views/room/Sidepanel/RoomSidepanelListWrapper.tsx @@ -0,0 +1,19 @@ +import { SidepanelList } from '@rocket.chat/fuselage'; +import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ForwardedRef, HTMLAttributes } from 'react'; +import React, { forwardRef } from 'react'; + +import { useSidebarListNavigation } from '../../../sidebar/RoomList/useSidebarListNavigation'; + +type RoomListWrapperProps = HTMLAttributes; + +const RoomSidepanelListWrapper = forwardRef(function RoomListWrapper(props: RoomListWrapperProps, ref: ForwardedRef) { + const t = useTranslation(); + const { sidebarListRef } = useSidebarListNavigation(); + const mergedRefs = useMergedRefs(ref, sidebarListRef); + + return ; +}); + +export default RoomSidepanelListWrapper; diff --git a/apps/meteor/client/views/room/Sidepanel/RoomSidepanelLoading.tsx b/apps/meteor/client/views/room/Sidepanel/RoomSidepanelLoading.tsx new file mode 100644 index 000000000000..00609ae6c496 --- /dev/null +++ b/apps/meteor/client/views/room/Sidepanel/RoomSidepanelLoading.tsx @@ -0,0 +1,20 @@ +import { SidebarV2Item as SidebarItem, Sidepanel, SidepanelList, Skeleton } from '@rocket.chat/fuselage'; +import React from 'react'; + +const RoomSidepanelLoading = () => ( + + + + + + + + + + + + + +); + +export default RoomSidepanelLoading; diff --git a/apps/meteor/client/views/room/Sidepanel/SidepanelItem/RoomSidepanelItem.tsx b/apps/meteor/client/views/room/Sidepanel/SidepanelItem/RoomSidepanelItem.tsx new file mode 100644 index 000000000000..dceb69e1aba3 --- /dev/null +++ b/apps/meteor/client/views/room/Sidepanel/SidepanelItem/RoomSidepanelItem.tsx @@ -0,0 +1,29 @@ +import type { IRoom, ISubscription, Serialized } from '@rocket.chat/core-typings'; +import { useUserSubscription } from '@rocket.chat/ui-contexts'; +import React, { memo } from 'react'; + +import { goToRoomById } from '../../../../lib/utils/goToRoomById'; +import { useTemplateByViewMode } from '../../../../sidebarv2/hooks/useTemplateByViewMode'; +import { useItemData } from '../hooks/useItemData'; + +export type RoomSidepanelItemProps = { + openedRoom?: string; + room: Serialized; + parentRid: string; + viewMode?: 'extended' | 'medium' | 'condensed'; +}; + +const RoomSidepanelItem = ({ room, openedRoom, viewMode }: RoomSidepanelItemProps) => { + const SidepanelItem = useTemplateByViewMode(); + const subscription = useUserSubscription(room._id); + + const itemData = useItemData({ ...room, ...subscription } as ISubscription & IRoom, { viewMode, openedRoom }); // as any because of divergent and overlaping timestamp types in subs and room (type Date vs type string) + + if (!subscription) { + return ; + } + + return ; +}; + +export default memo(RoomSidepanelItem); diff --git a/apps/meteor/client/views/room/Sidepanel/SidepanelItem/index.ts b/apps/meteor/client/views/room/Sidepanel/SidepanelItem/index.ts new file mode 100644 index 000000000000..5cfc0da3055b --- /dev/null +++ b/apps/meteor/client/views/room/Sidepanel/SidepanelItem/index.ts @@ -0,0 +1 @@ +export { default } from './RoomSidepanelItem'; diff --git a/apps/meteor/client/views/room/Sidepanel/hooks/useItemData.tsx b/apps/meteor/client/views/room/Sidepanel/hooks/useItemData.tsx new file mode 100644 index 000000000000..a6de01e22084 --- /dev/null +++ b/apps/meteor/client/views/room/Sidepanel/hooks/useItemData.tsx @@ -0,0 +1,68 @@ +import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; +import { SidebarV2ItemBadge as SidebarItemBadge, SidebarV2ItemIcon as SidebarItemIcon } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { useMemo } from 'react'; + +import { RoomIcon } from '../../../../components/RoomIcon'; +import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; +import { getBadgeTitle, getMessage } from '../../../../sidebarv2/RoomList/SidebarItemTemplateWithData'; +import { useAvatarTemplate } from '../../../../sidebarv2/hooks/useAvatarTemplate'; + +export const useItemData = ( + room: ISubscription & IRoom, + { openedRoom, viewMode }: { openedRoom: string | undefined; viewMode?: 'extended' | 'medium' | 'condensed' }, +) => { + const t = useTranslation(); + const AvatarTemplate = useAvatarTemplate(); + + const highlighted = Boolean(!room.hideUnreadStatus && (room.alert || room.unread)); + + const icon = useMemo( + () => } />, + [highlighted, room], + ); + const time = 'lastMessage' in room ? room.lastMessage?.ts : undefined; + const message = viewMode === 'extended' && getMessage(room, room.lastMessage, t); + + const threadUnread = Number(room.tunread?.length) > 0; + const isUnread = room.unread > 0 || threadUnread; + const showBadge = + !room.hideUnreadStatus || (!room.hideMentionStatus && (Boolean(room.userMentions) || Number(room.tunreadUser?.length) > 0)); + const badgeTitle = getBadgeTitle(room.userMentions, Number(room.tunread?.length), room.groupMentions, room.unread, t); + const variant = + ((room.userMentions || room.tunreadUser?.length) && 'danger') || + (threadUnread && 'primary') || + (room.groupMentions && 'warning') || + 'secondary'; + + const badges = useMemo( + () => ( + <> + {showBadge && isUnread && ( + + {room.unread + (room.tunread?.length || 0)} + + )} + + ), + [badgeTitle, isUnread, room.tunread?.length, room.unread, showBadge, variant], + ); + + const itemData = useMemo( + () => ({ + unread: highlighted, + selected: room.rid === openedRoom, + t, + href: roomCoordinator.getRouteLink(room.t, room) || '', + title: roomCoordinator.getRoomName(room.t, room) || '', + icon, + time, + badges, + avatar: AvatarTemplate && , + subtitle: message, + }), + [AvatarTemplate, badges, highlighted, icon, message, openedRoom, room, t, time], + ); + + return itemData; +}; diff --git a/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts b/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts new file mode 100644 index 000000000000..5791a6e5d547 --- /dev/null +++ b/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts @@ -0,0 +1,106 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useEffect, useMemo } from 'react'; + +import { ChatRoom } from '../../../../../app/models/client'; + +const sortRoomByLastMessage = (a: IRoom, b: IRoom) => { + if (!a.lm) { + return 1; + } + if (!b.lm) { + return -1; + } + return new Date(b.lm).toUTCString().localeCompare(new Date(a.lm).toUTCString()); +}; + +export const useTeamsListChildrenUpdate = ( + parentRid: string, + teamId?: string | null, + sidepanelItems?: 'channels' | 'discussions' | null, +) => { + const queryClient = useQueryClient(); + + const query = useMemo(() => { + const query: Parameters[0] = { + $or: [ + { + _id: parentRid, + }, + { + prid: parentRid, + }, + ], + }; + + if (teamId && query.$or) { + query.$or.push({ + teamId, + }); + } + return query; + }, [parentRid, teamId]); + + const teamList = useEndpoint('GET', '/v1/teams.listChildren'); + + const listRoomsAndDiscussions = useEndpoint('GET', '/v1/teams.listChildren'); + const result = useQuery({ + queryKey: ['sidepanel', 'list', parentRid, sidepanelItems], + queryFn: () => + listRoomsAndDiscussions({ + roomId: parentRid, + sort: JSON.stringify({ lm: -1 }), + type: sidepanelItems || undefined, + }), + enabled: sidepanelItems !== null && teamId !== null, + refetchInterval: 5 * 60 * 1000, + keepPreviousData: true, + }); + + const { mutate: update } = useMutation({ + mutationFn: async (params?: { action: 'add' | 'remove' | 'update'; data: IRoom }) => { + queryClient.setQueryData(['sidepanel', 'list', parentRid, sidepanelItems], (data: Awaited> | void) => { + if (!data) { + return; + } + + if (params?.action === 'add') { + data.data = [JSON.parse(JSON.stringify(params.data)), ...data.data].sort(sortRoomByLastMessage); + } + + if (params?.action === 'remove') { + data.data = data.data.filter((item) => item._id !== params.data?._id); + } + + if (params?.action === 'update') { + data.data = data.data + .map((item) => (item._id === params.data?._id ? JSON.parse(JSON.stringify(params.data)) : item)) + .sort(sortRoomByLastMessage); + } + + return { ...data }; + }); + }, + }); + + useEffect(() => { + const liveQueryHandle = ChatRoom.find(query).observe({ + added: (item) => { + queueMicrotask(() => update({ action: 'add', data: item })); + }, + changed: (item) => { + queueMicrotask(() => update({ action: 'update', data: item })); + }, + removed: (item) => { + queueMicrotask(() => update({ action: 'remove', data: item })); + }, + }); + + return () => { + liveQueryHandle.stop(); + }; + }, [update, query]); + + return result; +}; diff --git a/apps/meteor/client/views/room/Sidepanel/index.ts b/apps/meteor/client/views/room/Sidepanel/index.ts new file mode 100644 index 000000000000..c236142b8b0f --- /dev/null +++ b/apps/meteor/client/views/room/Sidepanel/index.ts @@ -0,0 +1 @@ +export { default } from './RoomSidepanel'; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx index b2aac49927a1..1233768a671c 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx @@ -1,5 +1,5 @@ /* eslint-disable complexity */ -import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; +import type { IRoomWithRetentionPolicy, SidepanelItem } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; import type { SelectOption } from '@rocket.chat/fuselage'; import { @@ -21,10 +21,13 @@ import { Box, TextAreaInput, AccordionItem, + Divider, } from '@rocket.chat/fuselage'; import { useEffectEvent, useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useSetting, useTranslation, useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQueryClient } from '@tanstack/react-query'; import type { ChangeEvent } from 'react'; import React, { useMemo } from 'react'; import { useForm, Controller } from 'react-hook-form'; @@ -72,11 +75,12 @@ const getRetentionSetting = (roomType: IRoomWithRetentionPolicy['t']): string => }; const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => { + const query = useQueryClient(); const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const isFederated = useMemo(() => isRoomFederated(room), [room]); // eslint-disable-next-line no-nested-ternary - const roomType = 'prid' in room ? 'discussion' : room.teamId ? 'team' : 'channel'; + const roomType = 'prid' in room ? 'discussion' : room.teamMain ? 'team' : 'channel'; const retentionPolicy = useRetentionPolicy(room); const retentionMaxAgeDefault = msToTimeUnit(TIMEUNIT.days, Number(useSetting(getRetentionSetting(room.t)))) ?? 30; @@ -118,6 +122,8 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => retentionOverrideGlobal, roomType: roomTypeP, reactWhenReadOnly, + showChannels, + showDiscussions, } = watch(); const { @@ -158,13 +164,23 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => retentionIgnoreThreads, ...formData }) => { - const data = getDirtyFields(formData, dirtyFields); + const data = getDirtyFields>(formData, dirtyFields); delete data.archived; + delete data.showChannels; + delete data.showDiscussions; + + const sidepanelItems = [showChannels && 'channels', showDiscussions && 'discussions'].filter(Boolean) as [ + SidepanelItem, + SidepanelItem?, + ]; + + const sidepanel = sidepanelItems.length > 0 ? { items: sidepanelItems } : null; try { await saveAction({ rid: room._id, ...data, + ...(roomType === 'team' ? { sidepanel } : null), ...((data.joinCode || 'joinCodeRequired' in data) && { joinCode: joinCodeRequired ? data.joinCode : '' }), ...((data.systemMessages || !hideSysMes) && { systemMessages: hideSysMes && data.systemMessages, @@ -180,6 +196,7 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => }), }); + await query.invalidateQueries(['/v1/rooms.info', room._id]); dispatchToastMessage({ type: 'success', message: t('Room_updated_successfully') }); onClickClose(); } catch (error) { @@ -224,6 +241,8 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => const retentionExcludePinnedField = useUniqueId(); const retentionFilesOnlyField = useUniqueId(); const retentionIgnoreThreads = useUniqueId(); + const showDiscussionsField = useUniqueId(); + const showChannelsField = useUniqueId(); const showAdvancedSettings = canViewEncrypted || canViewReadOnly || readOnly || canViewArchived || canViewJoinCode || canViewHideSysMes; const showRetentionPolicy = canEditRoomRetentionPolicy && retentionPolicy?.enabled; @@ -355,6 +374,49 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => {showAdvancedSettings && ( + {roomType === 'team' && ( + + {null} + + + + {t('Navigation')} + + + + {t('Channels')} + ( + + )} + /> + + + {t('Show_channels_description')} + + + + + {t('Discussions')} + ( + + )} + /> + + + {t('Show_discussions_description')} + + + + + + + )} {t('Security_and_permissions')} diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts index 1a002727358f..b71f8e1b5bc5 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts @@ -10,7 +10,20 @@ export const useEditRoomInitialValues = (room: IRoomWithRetentionPolicy) => { const retentionPolicy = useRetentionPolicy(room); const canEditRoomRetentionPolicy = usePermission('edit-room-retention-policy', room._id); - const { t, ro, archived, topic, description, announcement, joinCodeRequired, sysMes, encrypted, retention, reactWhenReadOnly } = room; + const { + t, + ro, + archived, + topic, + description, + announcement, + joinCodeRequired, + sysMes, + encrypted, + retention, + reactWhenReadOnly, + sidepanel, + } = room; return useMemo( () => ({ @@ -37,6 +50,8 @@ export const useEditRoomInitialValues = (room: IRoomWithRetentionPolicy) => { retentionFilesOnly: retention?.filesOnly ?? retentionPolicy.filesOnly, retentionIgnoreThreads: retention?.ignoreThreads ?? retentionPolicy.ignoreThreads, }), + showDiscussions: sidepanel?.items.includes('discussions'), + showChannels: sidepanel?.items.includes('channels'), }), [ announcement, @@ -53,6 +68,7 @@ export const useEditRoomInitialValues = (room: IRoomWithRetentionPolicy) => { encrypted, reactWhenReadOnly, canEditRoomRetentionPolicy, + sidepanel, ], ); }; diff --git a/apps/meteor/client/views/room/layout/RoomLayout.tsx b/apps/meteor/client/views/room/layout/RoomLayout.tsx index b5e29c05799f..3c0c4c313c99 100644 --- a/apps/meteor/client/views/room/layout/RoomLayout.tsx +++ b/apps/meteor/client/views/room/layout/RoomLayout.tsx @@ -59,7 +59,7 @@ const RoomLayout = ({ header, body, footer, aside, ...props }: RoomLayoutProps): [layout, contextualbarPosition, contextualbarSize], )} > - + @@ -82,7 +82,7 @@ const RoomLayout = ({ header, body, footer, aside, ...props }: RoomLayoutProps): {footer && {footer}} {aside && ( - + {aside} )} diff --git a/apps/meteor/client/views/room/providers/RoomProvider.tsx b/apps/meteor/client/views/room/providers/RoomProvider.tsx index 65c83100b1c0..d67c8566e3c6 100644 --- a/apps/meteor/client/views/room/providers/RoomProvider.tsx +++ b/apps/meteor/client/views/room/providers/RoomProvider.tsx @@ -8,12 +8,15 @@ import { RoomHistoryManager } from '../../../../app/ui-utils/client'; import { UserAction } from '../../../../app/ui/client/lib/UserAction'; import { useReactiveQuery } from '../../../hooks/useReactiveQuery'; import { useReactiveValue } from '../../../hooks/useReactiveValue'; +import { useRoomInfoEndpoint } from '../../../hooks/useRoomInfoEndpoint'; +import { useSidePanelNavigation } from '../../../hooks/useSidePanelNavigation'; import { RoomManager } from '../../../lib/RoomManager'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; import ImageGalleryProvider from '../../../providers/ImageGalleryProvider'; import RoomNotFound from '../RoomNotFound'; import RoomSkeleton from '../RoomSkeleton'; import { useRoomRolesManagement } from '../body/hooks/useRoomRolesManagement'; +import type { IRoomWithFederationOriginalName } from '../contexts/RoomContext'; import { RoomContext } from '../contexts/RoomContext'; import ComposerPopupProvider from './ComposerPopupProvider'; import RoomToolboxProvider from './RoomToolboxProvider'; @@ -30,15 +33,17 @@ type RoomProviderProps = { const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { useRoomRolesManagement(rid); - const { data: room, isSuccess } = useRoomQuery(rid); + const resultFromServer = useRoomInfoEndpoint(rid); + + const resultFromLocal = useRoomQuery(rid); // TODO: the following effect is a workaround while we don't have a general and definitive solution for it const router = useRouter(); useEffect(() => { - if (isSuccess && !room) { + if (resultFromLocal.isSuccess && !resultFromLocal.data) { router.navigate('/home'); } - }, [isSuccess, room, router]); + }, [resultFromLocal.data, resultFromLocal.isSuccess, resultFromServer, router]); const subscriptionQuery = useReactiveQuery(['subscriptions', { rid }], () => ChatSubscription.findOne({ rid }) ?? null); @@ -46,7 +51,8 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { useUsersNameChanged(); - const pseudoRoom = useMemo(() => { + const pseudoRoom: IRoomWithFederationOriginalName | null = useMemo(() => { + const room = resultFromLocal.data; if (!room) { return null; } @@ -57,7 +63,7 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { name: roomCoordinator.getRoomName(room.t, room), federationOriginalName: room.name, }; - }, [room, subscriptionQuery.data]); + }, [resultFromLocal.data, subscriptionQuery.data]); const { hasMorePreviousMessages, hasMoreNextMessages, isLoadingMoreMessages } = useReactiveValue( useCallback(() => { @@ -86,12 +92,69 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { }; }, [hasMoreNextMessages, hasMorePreviousMessages, isLoadingMoreMessages, pseudoRoom, rid, subscriptionQuery.data]); + const isSidepanelFeatureEnabled = useSidePanelNavigation(); + useEffect(() => { + if (isSidepanelFeatureEnabled) { + if (resultFromServer.isSuccess) { + if (resultFromServer.data.room?.teamMain) { + if ( + resultFromServer.data.room.sidepanel?.items.includes('channels') || + resultFromServer.data.room?.sidepanel?.items.includes('discussions') + ) { + RoomManager.openSecondLevel(rid, rid); + } else { + RoomManager.open(rid); + } + return (): void => { + RoomManager.back(rid); + }; + } + + switch (true) { + case resultFromServer.data.room?.prid && + resultFromServer.data.parent && + resultFromServer.data.parent.sidepanel?.items.includes('discussions'): + RoomManager.openSecondLevel(resultFromServer.data.parent._id, rid); + break; + case resultFromServer.data.team?.roomId && + !resultFromServer.data.room?.teamMain && + resultFromServer.data.parent?.sidepanel?.items.includes('channels'): + RoomManager.openSecondLevel(resultFromServer.data.team.roomId, rid); + break; + + default: + if ( + resultFromServer.data.parent?.sidepanel?.items.includes('channels') || + resultFromServer.data.parent?.sidepanel?.items.includes('discussions') + ) { + RoomManager.openSecondLevel(rid, rid); + } else { + RoomManager.open(rid); + } + break; + } + } + return (): void => { + RoomManager.back(rid); + }; + } + RoomManager.open(rid); return (): void => { RoomManager.back(rid); }; - }, [rid]); + }, [ + isSidepanelFeatureEnabled, + rid, + resultFromServer.data?.room?.prid, + resultFromServer.data?.room?.teamId, + resultFromServer.data?.room?.teamMain, + resultFromServer.isSuccess, + resultFromServer.data?.parent, + resultFromServer.data?.team?.roomId, + resultFromServer.data, + ]); const subscribed = !!subscriptionQuery.data; @@ -104,7 +167,7 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { }, [rid, subscribed]); if (!pseudoRoom) { - return isSuccess && !room ? : ; + return resultFromLocal.isSuccess && !resultFromLocal.data ? : ; } return ( diff --git a/apps/meteor/lib/publishFields.ts b/apps/meteor/lib/publishFields.ts index c1483ea86cd1..48d0f9c87d5c 100644 --- a/apps/meteor/lib/publishFields.ts +++ b/apps/meteor/lib/publishFields.ts @@ -74,6 +74,7 @@ export const roomFields = { avatarETag: 1, usersCount: 1, msgs: 1, + sidepanel: 1, // @TODO create an API to register this fields based on room type tags: 1, diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index f5218c88402a..4be96bff9866 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -1055,8 +1055,10 @@ export class TeamService extends ServiceClassInternal implements ITeamService { return rooms; } - private getParentRoom(team: AtLeast): Promise | null> { - return Rooms.findOneById>(team.roomId, { projection: { name: 1, fname: 1, t: 1 } }); + private getParentRoom(team: AtLeast): Promise | null> { + return Rooms.findOneById>(team.roomId, { + projection: { name: 1, fname: 1, t: 1, sidepanel: 1 }, + }); } async getRoomInfo( diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index 0ce1b1041de6..95f2836da1e7 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -99,6 +99,9 @@ export const isSidepanelItem = (item: any): item is SidepanelItem => { }; export const isValidSidepanel = (sidepanel: IRoom['sidepanel']) => { + if (sidepanel === null) { + return true; + } if (!sidepanel?.items) { return false; } diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 796e9d0519ff..728ca256952d 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -6541,8 +6541,8 @@ "Incoming_Calls": "Incoming calls", "Advanced_settings": "Advanced settings", "Security_and_permissions": "Security and permissions", - "Sidepanel_navigation": "Sidepanel navigation for teams", - "Sidepanel_navigation_description": "Option to open a sidepanel with channels and/or discussions associated with the team. This allows team owners to customize communication methods to best meet their team’s needs. This feature is only available when Enhanced navigation experience is enabled.", + "Sidepanel_navigation": "Secondary navigation for teams", + "Sidepanel_navigation_description": "Display channels and/or discussions associated with teams by default. This allows team owners to customize communication methods to best meet their team’s needs. This is currently in feature preview and will be a premium capability once fully released.", "Show_channels_description": "Show team channels in second sidebar", "Show_discussions_description": "Show team discussions in second sidebar" } diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index c837ba7186bd..16debe87e44c 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -626,7 +626,7 @@ export type RoomsEndpoints = { '/v1/rooms.info': { GET: (params: RoomsInfoProps) => { room: IRoom | undefined; - parent?: Pick; + parent?: Pick; team?: Pick; }; }; diff --git a/packages/ui-client/src/components/FeaturePreview/FeaturePreview.tsx b/packages/ui-client/src/components/FeaturePreview/FeaturePreview.tsx index 09ec0700cb79..c297cc839abc 100644 --- a/packages/ui-client/src/components/FeaturePreview/FeaturePreview.tsx +++ b/packages/ui-client/src/components/FeaturePreview/FeaturePreview.tsx @@ -5,8 +5,16 @@ import { Children, Suspense, cloneElement } from 'react'; import { useFeaturePreview } from '../../hooks/useFeaturePreview'; import { FeaturesAvailable } from '../../hooks/useFeaturePreviewList'; -export const FeaturePreview = ({ feature, children }: { feature: FeaturesAvailable; children: ReactElement[] }) => { - const featureToggleEnabled = useFeaturePreview(feature); +export const FeaturePreview = ({ + feature, + disabled = false, + children, +}: { + disabled?: boolean; + feature: FeaturesAvailable; + children: ReactElement[]; +}) => { + const featureToggleEnabled = useFeaturePreview(feature) && !disabled; const toggledChildren = Children.map(children, (child) => cloneElement(child, { diff --git a/packages/ui-client/src/hooks/useFeaturePreviewList.ts b/packages/ui-client/src/hooks/useFeaturePreviewList.ts index 172045197f8c..ff103a8d84ef 100644 --- a/packages/ui-client/src/hooks/useFeaturePreviewList.ts +++ b/packages/ui-client/src/hooks/useFeaturePreviewList.ts @@ -72,7 +72,7 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ description: 'Sidepanel_navigation_description', group: 'Navigation', value: false, - enabled: false, + enabled: true, enableQuery: { name: 'newNavigation', value: true, From 015c862c999517ad74710b951a7c47b2c00ed091 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 19 Sep 2024 20:02:19 -0300 Subject: [PATCH 20/39] ci: auto candidate releases (#33325) Co-authored-by: Diego Sampaio --- .github/workflows/release-candidate.yml | 35 +++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/release-candidate.yml diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml new file mode 100644 index 000000000000..4a1e67fca33a --- /dev/null +++ b/.github/workflows/release-candidate.yml @@ -0,0 +1,35 @@ +name: Release candidate cut +on: + schedule: + - cron: '28 0 20 * *' # run at minute 28 to avoid the chance of delay due to high load on GH +jobs: + new-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref_name }} + fetch-depth: 0 + token: ${{ secrets.CI_PAT }} + + - name: Setup NodeJS + uses: ./.github/actions/setup-node + with: + node-version: 14.21.3 + cache-modules: true + install: true + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + - uses: rharkor/caching-for-turbo@v1.5 + + - name: Build packages + run: yarn build + + - name: 'Start release candidate' + uses: ./packages/release-action + with: + action: next + base-ref: ${{ github.ref_name }} + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + GITHUB_TOKEN: ${{ secrets.CI_PAT }} From 7faba775f0967ce745bfbe48c51561e8c1c33c48 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 19 Sep 2024 17:57:54 -0600 Subject: [PATCH 21/39] refactor: Reactions set/unset (#32994) --- apps/meteor/app/api/server/v1/chat.ts | 2 +- .../app/reactions/server/setReaction.ts | 109 +++-- apps/meteor/server/models/raw/EmojiCustom.ts | 8 + .../app/reactions/server/setReaction.spec.ts | 428 ++++++++++++++++++ .../src/models/IEmojiCustomModel.ts | 1 + 5 files changed, 499 insertions(+), 49 deletions(-) create mode 100644 apps/meteor/tests/unit/app/reactions/server/setReaction.spec.ts diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 3ccc9caeafa0..d04d1a2418b5 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -376,7 +376,7 @@ API.v1.addRoute( throw new Meteor.Error('error-emoji-param-not-provided', 'The required "emoji" param is missing.'); } - await executeSetReaction(this.userId, emoji, msg._id, this.bodyParams.shouldReact); + await executeSetReaction(this.userId, emoji, msg, this.bodyParams.shouldReact); return API.v1.success(); }, diff --git a/apps/meteor/app/reactions/server/setReaction.ts b/apps/meteor/app/reactions/server/setReaction.ts index 8f9c24633407..be6e5aed4a54 100644 --- a/apps/meteor/app/reactions/server/setReaction.ts +++ b/apps/meteor/app/reactions/server/setReaction.ts @@ -4,7 +4,6 @@ import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Messages, EmojiCustom, Rooms, Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import _ from 'underscore'; import { callbacks } from '../../../lib/callbacks'; import { i18n } from '../../../server/lib/i18n'; @@ -12,26 +11,39 @@ import { canAccessRoomAsync } from '../../authorization/server'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; import { emoji } from '../../emoji/server'; import { isTheLastMessage } from '../../lib/server/functions/isTheLastMessage'; -import { notifyOnRoomChangedById, notifyOnMessageChange } from '../../lib/server/lib/notifyListener'; +import { notifyOnMessageChange } from '../../lib/server/lib/notifyListener'; -const removeUserReaction = (message: IMessage, reaction: string, username: string) => { +export const removeUserReaction = (message: IMessage, reaction: string, username: string) => { if (!message.reactions) { return message; } - message.reactions[reaction].usernames.splice(message.reactions[reaction].usernames.indexOf(username), 1); - if (message.reactions[reaction].usernames.length === 0) { + const idx = message.reactions[reaction].usernames.indexOf(username); + + // user not found in reaction array + if (idx === -1) { + return message; + } + + message.reactions[reaction].usernames.splice(idx, 1); + if (!message.reactions[reaction].usernames.length) { delete message.reactions[reaction]; } return message; }; -async function setReaction(room: IRoom, user: IUser, message: IMessage, reaction: string, shouldReact?: boolean) { - reaction = `:${reaction.replace(/:/g, '')}:`; +export async function setReaction( + room: Pick, + user: IUser, + message: IMessage, + reaction: string, + userAlreadyReacted?: boolean, +) { + await Message.beforeReacted(message, room); - if (!emoji.list[reaction] && (await EmojiCustom.findByNameOrAlias(reaction, {}).count()) === 0) { - throw new Meteor.Error('error-not-allowed', 'Invalid emoji provided.', { - method: 'setReaction', + if (Array.isArray(room.muted) && room.muted.includes(user.username as string)) { + throw new Meteor.Error('error-not-allowed', i18n.t('You_have_been_muted', { lng: user.language }), { + rid: room._id, }); } @@ -42,50 +54,23 @@ async function setReaction(room: IRoom, user: IUser, message: IMessage, reaction } } - if (Array.isArray(room.muted) && room.muted.indexOf(user.username as string) !== -1) { - throw new Meteor.Error('error-not-allowed', i18n.t('You_have_been_muted', { lng: user.language }), { - rid: room._id, - }); - } - - // if (!('reactions' in message)) { - // return; - // } - - await Message.beforeReacted(message, room); - - const userAlreadyReacted = - message.reactions && - Boolean(message.reactions[reaction]) && - message.reactions[reaction].usernames.indexOf(user.username as string) !== -1; - // When shouldReact was not informed, toggle the reaction. - if (shouldReact === undefined) { - shouldReact = !userAlreadyReacted; - } - - if (userAlreadyReacted === shouldReact) { - return; - } - let isReacted; - if (userAlreadyReacted) { const oldMessage = JSON.parse(JSON.stringify(message)); removeUserReaction(message, reaction, user.username as string); - if (_.isEmpty(message.reactions)) { + if (Object.keys(message.reactions || {}).length === 0) { delete message.reactions; + await Messages.unsetReactions(message._id); if (isTheLastMessage(room, message)) { await Rooms.unsetReactionsInLastMessage(room._id); - void notifyOnRoomChangedById(room._id); } - await Messages.unsetReactions(message._id); } else { await Messages.setReactions(message._id, message.reactions); if (isTheLastMessage(room, message)) { await Rooms.setReactionsInLastMessage(room._id, message.reactions); } } - await callbacks.run('afterUnsetReaction', message, { user, reaction, shouldReact, oldMessage }); + void callbacks.run('afterUnsetReaction', message, { user, reaction, shouldReact: false, oldMessage }); isReacted = false; } else { @@ -101,33 +86,61 @@ async function setReaction(room: IRoom, user: IUser, message: IMessage, reaction await Messages.setReactions(message._id, message.reactions); if (isTheLastMessage(room, message)) { await Rooms.setReactionsInLastMessage(room._id, message.reactions); - void notifyOnRoomChangedById(room._id); } - await callbacks.run('afterSetReaction', message, { user, reaction, shouldReact }); + + void callbacks.run('afterSetReaction', message, { user, reaction, shouldReact: true }); isReacted = true; } - await Apps.self?.triggerEvent(AppEvents.IPostMessageReacted, message, user, reaction, isReacted); + void Apps.self?.triggerEvent(AppEvents.IPostMessageReacted, message, user, reaction, isReacted); void notifyOnMessageChange({ id: message._id, }); } -export async function executeSetReaction(userId: string, reaction: string, messageId: IMessage['_id'], shouldReact?: boolean) { - const user = await Users.findOneById(userId); +export async function executeSetReaction( + userId: string, + reaction: string, + messageParam: IMessage['_id'] | IMessage, + shouldReact?: boolean, +) { + // Check if the emoji is valid before proceeding + const reactionWithoutColons = reaction.replace(/:/g, ''); + reaction = `:${reactionWithoutColons}:`; + + if (!emoji.list[reaction] && (await EmojiCustom.countByNameOrAlias(reactionWithoutColons)) === 0) { + throw new Meteor.Error('error-not-allowed', 'Invalid emoji provided.', { + method: 'setReaction', + }); + } + const user = await Users.findOneById(userId); if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setReaction' }); } - const message = await Messages.findOneById(messageId); + const message = typeof messageParam === 'string' ? await Messages.findOneById(messageParam) : messageParam; if (!message) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setReaction' }); } - const room = await Rooms.findOneById(message.rid); + const userAlreadyReacted = + message.reactions && Boolean(message.reactions[reaction]) && message.reactions[reaction].usernames.includes(user.username as string); + + // When shouldReact was not informed, toggle the reaction. + if (shouldReact === undefined) { + shouldReact = !userAlreadyReacted; + } + + if (userAlreadyReacted === shouldReact) { + return; + } + + const room = await Rooms.findOneById< + Pick + >(message.rid, { projection: { _id: 1, ro: 1, muted: 1, reactWhenReadOnly: 1, lastMessage: 1, t: 1, prid: 1, federated: 1 } }); if (!room) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setReaction' }); } @@ -136,7 +149,7 @@ export async function executeSetReaction(userId: string, reaction: string, messa throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'setReaction' }); } - return setReaction(room, user, message, reaction, shouldReact); + return setReaction(room, user, message, reaction, userAlreadyReacted); } declare module '@rocket.chat/ddp-client' { diff --git a/apps/meteor/server/models/raw/EmojiCustom.ts b/apps/meteor/server/models/raw/EmojiCustom.ts index 300721f60d7b..ec3c6390f64d 100644 --- a/apps/meteor/server/models/raw/EmojiCustom.ts +++ b/apps/meteor/server/models/raw/EmojiCustom.ts @@ -72,4 +72,12 @@ export class EmojiCustomRaw extends BaseRaw implements IEmojiCusto create(data: InsertionModel): Promise>> { return this.insertOne(data); } + + countByNameOrAlias(name: string): Promise { + const query = { + $or: [{ name }, { aliases: name }], + }; + + return this.countDocuments(query); + } } diff --git a/apps/meteor/tests/unit/app/reactions/server/setReaction.spec.ts b/apps/meteor/tests/unit/app/reactions/server/setReaction.spec.ts new file mode 100644 index 000000000000..e267825a6a18 --- /dev/null +++ b/apps/meteor/tests/unit/app/reactions/server/setReaction.spec.ts @@ -0,0 +1,428 @@ +import { expect } from 'chai'; +import { beforeEach, describe, it } from 'mocha'; +import p from 'proxyquire'; +import sinon from 'sinon'; + +const meteorMethodsMock = sinon.stub(); +const emojiList: Record = {}; +const modelsMock = { + EmojiCustom: { + countByNameOrAlias: sinon.stub(), + }, + Users: { + findOneById: sinon.stub(), + }, + Messages: { + findOneById: sinon.stub(), + setReactions: sinon.stub(), + unsetReactions: sinon.stub(), + }, + Rooms: { + findOneById: sinon.stub(), + unsetReactionsInLastMessage: sinon.stub(), + setReactionsInLastMessage: sinon.stub(), + }, +}; +const canAccessRoomAsyncMock = sinon.stub(); +const isTheLastMessageMock = sinon.stub(); +const notifyOnMessageChangeMock = sinon.stub(); +const hasPermissionAsyncMock = sinon.stub(); +const i18nMock = { t: sinon.stub() }; +const callbacksRunMock = sinon.stub(); +const meteorErrorMock = class extends Error { + constructor(message: string) { + super(message); + } +}; + +const { removeUserReaction, executeSetReaction, setReaction } = p.noCallThru().load('../../../../../app/reactions/server/setReaction.ts', { + '@rocket.chat/models': modelsMock, + '@rocket.chat/core-services': { Message: { beforeReacted: sinon.stub() } }, + 'meteor/meteor': { Meteor: { methods: meteorMethodsMock, Error: meteorErrorMock } }, + '../../../lib/callbacks': { callbacks: { run: callbacksRunMock } }, + '../../../server/lib/i18n': { i18n: i18nMock }, + '../../authorization/server': { canAccessRoomAsync: canAccessRoomAsyncMock }, + '../../authorization/server/functions/hasPermission': { hasPermissionAsync: hasPermissionAsyncMock }, + '../../emoji/server': { emoji: { list: emojiList } }, + '../../lib/server/functions/isTheLastMessage': { isTheLastMessage: isTheLastMessageMock }, + '../../lib/server/lib/notifyListener': { + notifyOnMessageChange: notifyOnMessageChangeMock, + }, +}); + +describe('Reactions', () => { + describe('removeUserReaction', () => { + it('should return the message unmodified when no reactions exist', () => { + const message = {}; + + const result = removeUserReaction(message as any, 'test', 'test'); + expect(result).to.equal(message); + }); + it('should remove the reaction from a message', () => { + const message = { + reactions: { + test: { + usernames: ['test', 'test2'], + }, + }, + }; + + const result = removeUserReaction(message as any, 'test', 'test'); + expect(result.reactions.test.usernames).to.not.include('test'); + expect(result.reactions.test.usernames).to.include('test2'); + }); + it('should remove the reaction from a message when the user is the last one on the array', () => { + const message = { + reactions: { + test: { + usernames: ['test'], + }, + }, + }; + + const result = removeUserReaction(message as any, 'test', 'test'); + expect(result.reactions.test).to.be.undefined; + }); + it('should remove username only from the reaction thats passed in', () => { + const message = { + reactions: { + test: { + usernames: ['test', 'test2'], + }, + other: { + usernames: ['test', 'test2'], + }, + }, + }; + + const result = removeUserReaction(message as any, 'test', 'test'); + expect(result.reactions.test.usernames).to.not.include('test'); + expect(result.reactions.test.usernames).to.include('test2'); + expect(result.reactions.other.usernames).to.include('test'); + expect(result.reactions.other.usernames).to.include('test2'); + }); + it('should do nothing if username is not in the reaction', () => { + const message = { + reactions: { + test: { + usernames: ['test', 'test2'], + }, + }, + }; + + const result = removeUserReaction(message as any, 'test', 'test3'); + expect(result.reactions.test.usernames).to.not.include('test3'); + expect(result.reactions.test.usernames).to.include('test'); + expect(result.reactions.test.usernames).to.include('test2'); + }); + }); + describe('executeSetReaction', () => { + beforeEach(() => { + modelsMock.EmojiCustom.countByNameOrAlias.reset(); + }); + it('should throw an error if reaction is not on emojione list', async () => { + modelsMock.EmojiCustom.countByNameOrAlias.resolves(0); + await expect(executeSetReaction('test', 'test', 'test')).to.be.rejectedWith('error-not-allowed'); + }); + it('should fail if user does not exist', async () => { + modelsMock.EmojiCustom.countByNameOrAlias.resolves(1); + await expect(executeSetReaction('test', 'test', 'test')).to.be.rejectedWith('error-invalid-user'); + }); + it('should fail if message does not exist', async () => { + modelsMock.EmojiCustom.countByNameOrAlias.resolves(1); + modelsMock.Users.findOneById.resolves({ username: 'test' }); + await expect(executeSetReaction('test', 'test', 'test')).to.be.rejectedWith('error-not-allowed'); + }); + it('should return nothing if user already reacted and its trying to react again', async () => { + modelsMock.EmojiCustom.countByNameOrAlias.resolves(1); + modelsMock.Users.findOneById.resolves({ username: 'test' }); + modelsMock.Messages.findOneById.resolves({ reactions: { ':test:': { usernames: ['test'] } } }); + expect(await executeSetReaction('test', 'test', 'test', true)).to.be.undefined; + }); + it('should return nothing if user hasnt reacted and its trying to unreact', async () => { + modelsMock.EmojiCustom.countByNameOrAlias.resolves(1); + modelsMock.Users.findOneById.resolves({ username: 'test' }); + modelsMock.Messages.findOneById.resolves({ reactions: { ':test:': { usernames: ['testxxxx'] } } }); + expect(await executeSetReaction('test', 'test', 'test', false)).to.be.undefined; + }); + it('should fail if room does not exist', async () => { + modelsMock.EmojiCustom.countByNameOrAlias.resolves(1); + modelsMock.Users.findOneById.resolves({ username: 'test' }); + modelsMock.Messages.findOneById.resolves({ reactions: { ':test:': { usernames: ['test'] } } }); + modelsMock.Rooms.findOneById.resolves(undefined); + await expect(executeSetReaction('test', 'test', 'test')).to.be.rejectedWith('error-not-allowed'); + }); + it('should fail if user doesnt have acccess to the room', async () => { + modelsMock.EmojiCustom.countByNameOrAlias.resolves(1); + modelsMock.Users.findOneById.resolves({ username: 'test' }); + modelsMock.Messages.findOneById.resolves({ reactions: { ':test:': { usernames: ['test'] } } }); + modelsMock.Rooms.findOneById.resolves({ t: 'd' }); + canAccessRoomAsyncMock.resolves(false); + await expect(executeSetReaction('test', 'test', 'test')).to.be.rejectedWith('not-authorized'); + }); + it('should call setReaction with correct params', async () => { + modelsMock.EmojiCustom.countByNameOrAlias.resolves(1); + modelsMock.Users.findOneById.resolves({ username: 'test' }); + modelsMock.Messages.findOneById.resolves({ reactions: { ':test:': { usernames: ['test'] } } }); + modelsMock.Rooms.findOneById.resolves({ t: 'c' }); + canAccessRoomAsyncMock.resolves(true); + + const res = await executeSetReaction('test', 'test', 'test'); + expect(res).to.be.undefined; + }); + it('should use the message from param when the type is not an string', async () => { + modelsMock.EmojiCustom.countByNameOrAlias.resolves(1); + modelsMock.Users.findOneById.resolves({ username: 'test' }); + modelsMock.Rooms.findOneById.resolves({ t: 'c' }); + canAccessRoomAsyncMock.resolves(true); + + await executeSetReaction('test', 'test', { reactions: { ':test:': { usernames: ['test'] } } }); + expect(modelsMock.Messages.findOneById.calledOnce).to.be.false; + }); + }); + describe('setReaction', () => { + beforeEach(() => { + canAccessRoomAsyncMock.reset(); + hasPermissionAsyncMock.reset(); + isTheLastMessageMock.reset(); + modelsMock.Messages.setReactions.reset(); + modelsMock.Rooms.setReactionsInLastMessage.reset(); + modelsMock.Rooms.unsetReactionsInLastMessage.reset(); + modelsMock.Messages.unsetReactions.reset(); + callbacksRunMock.reset(); + }); + it('should throw an error if user is muted from the room', async () => { + const room = { + muted: ['test'], + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + }; + await expect(setReaction(room, user, message, ':test:')).to.be.rejectedWith('error-not-allowed'); + }); + it('should throw an error if room is readonly and cannot be reacted when readonly and user trying doesnt have permissions and user is not unmuted from room', async () => { + const room = { + ro: true, + reactWhenReadOnly: false, + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + }; + canAccessRoomAsyncMock.resolves(false); + await expect(setReaction(room, user, message, ':test:')).to.be.rejectedWith("You can't send messages because the room is readonly."); + }); + it('should remove the user reaction if userAlreadyReacted is true and call unsetReaction if reaction is the last one on message', async () => { + const room = { + _id: 'test', + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + reactions: { + ':test:': { + usernames: ['test'], + }, + }, + }; + const reaction = ':test:'; + + await setReaction(room, user, message, reaction, true); + expect(modelsMock.Messages.unsetReactions.calledWith(message._id)).to.be.true; + }); + it('should call Rooms.unsetReactionsInLastMessage when userAlreadyReacted is true and reaction is the last one on message', async () => { + const room = { + _id: 'test', + lastMessage: 'test', + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + reactions: { + ':test:': { + usernames: ['test'], + }, + }, + }; + const reaction = ':test:'; + + isTheLastMessageMock.resolves(true); + + await setReaction(room, user, message, reaction, true); + expect(modelsMock.Messages.unsetReactions.calledWith(message._id)).to.be.true; + expect(modelsMock.Rooms.unsetReactionsInLastMessage.calledWith(room._id)).to.be.true; + }); + it('should update the reactions object when userAlreadyReacted is true and there is more reactions on message', async () => { + const room = { + _id: 'test', + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + reactions: { + ':test:': { + usernames: ['test'], + }, + ':test2:': { + usernames: ['test'], + }, + }, + }; + const reaction = ':test:'; + + await setReaction(room, user, message, reaction, true); + expect(modelsMock.Messages.setReactions.calledWith(message._id, sinon.match({ ':test2:': { usernames: ['test'] } }))).to.be.true; + }); + it('should call Rooms.setReactionsInLastMessage when userAlreadyReacted is true and reaction is not the last one on message', async () => { + const room = { + _id: 'test', + lastMessage: 'test', + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + reactions: { + ':test:': { + usernames: ['test'], + }, + ':test2:': { + usernames: ['test'], + }, + }, + }; + const reaction = ':test:'; + + isTheLastMessageMock.resolves(true); + + await setReaction(room, user, message, reaction, true); + expect(modelsMock.Messages.setReactions.calledWith(message._id, sinon.match({ ':test2:': { usernames: ['test'] } }))).to.be.true; + expect(modelsMock.Rooms.setReactionsInLastMessage.calledWith(room._id, sinon.match({ ':test2:': { usernames: ['test'] } }))).to.be + .true; + }); + it('should call afterUnsetReaction callback when userAlreadyReacted is true', async () => { + const room = { + _id: 'test', + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + reactions: { + ':test:': { + usernames: ['test'], + }, + }, + }; + const reaction = ':test:'; + + await setReaction(room, user, message, reaction, true); + expect( + callbacksRunMock.calledWith( + 'afterUnsetReaction', + sinon.match({ _id: 'test' }), + sinon.match({ user, reaction, shouldReact: false, oldMessage: message }), + ), + ).to.be.true; + }); + it('should set reactions when userAlreadyReacted is false', async () => { + const room = { + _id: 'test', + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + }; + const reaction = ':test:'; + await setReaction(room, user, message, reaction, false); + expect(modelsMock.Messages.setReactions.calledWith(message._id, sinon.match({ ':test:': { usernames: ['test'] } }))).to.be.true; + }); + it('should properly add username to the list of reactions when userAlreadyReacted is false', async () => { + const room = { + _id: 'test', + }; + const user = { + username: 'test2', + }; + const message = { + _id: 'test', + reactions: { + ':test:': { + usernames: ['test'], + }, + }, + }; + const reaction = ':test:'; + + await setReaction(room, user, message, reaction, false); + expect(modelsMock.Messages.setReactions.calledWith(message._id, sinon.match({ ':test:': { usernames: ['test', 'test2'] } }))).to.be + .true; + }); + it('should call Rooms.setReactionInLastMessage when userAlreadyReacted is false', async () => { + const room = { + _id: 'x5', + lastMessage: 'test', + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + }; + const reaction = ':test:'; + + isTheLastMessageMock.resolves(true); + + await setReaction(room, user, message, reaction, false); + expect(modelsMock.Messages.setReactions.calledWith(message._id, sinon.match({ ':test:': { usernames: ['test'] } }))).to.be.true; + expect(modelsMock.Rooms.setReactionsInLastMessage.calledWith(room._id, sinon.match({ ':test:': { usernames: ['test'] } }))).to.be + .true; + }); + it('should call afterSetReaction callback when userAlreadyReacted is false', async () => { + const room = { + _id: 'test', + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + }; + const reaction = ':test:'; + + await setReaction(room, user, message, reaction, false); + expect( + callbacksRunMock.calledWith('afterSetReaction', sinon.match({ _id: 'test' }), sinon.match({ user, reaction, shouldReact: true })), + ).to.be.true; + }); + it('should return undefined on a successful reaction', async () => { + const room = { + _id: 'test', + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + }; + const reaction = ':test:'; + + expect(await setReaction(room, user, message, reaction, false)).to.be.undefined; + }); + }); +}); diff --git a/packages/model-typings/src/models/IEmojiCustomModel.ts b/packages/model-typings/src/models/IEmojiCustomModel.ts index fba5f1c3ea10..30f0323c1ec7 100644 --- a/packages/model-typings/src/models/IEmojiCustomModel.ts +++ b/packages/model-typings/src/models/IEmojiCustomModel.ts @@ -10,4 +10,5 @@ export interface IEmojiCustomModel extends IBaseModel { setAliases(_id: string, aliases: string[]): Promise; setExtension(_id: string, extension: string): Promise; create(data: InsertionModel): Promise>>; + countByNameOrAlias(name: string): Promise; } From 274f4f58812cc0409c62380ca4292a64b6ab04b5 Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Thu, 19 Sep 2024 21:15:14 -0300 Subject: [PATCH 22/39] feat: E2EE messages mentions (#32510) --- .changeset/late-planes-sniff.md | 7 ++ .../app/lib/server/methods/updateMessage.ts | 2 +- apps/meteor/app/mentions/server/Mentions.ts | 20 ++++-- apps/meteor/client/startup/e2e.ts | 22 ++++++ apps/meteor/server/settings/e2e.ts | 6 ++ apps/meteor/tests/e2e/e2e-encryption.spec.ts | 71 +++++++++++++++++++ .../page-objects/fragments/home-content.ts | 2 +- .../core-typings/src/IMessage/IMessage.ts | 1 + packages/i18n/src/locales/en.i18n.json | 2 + 9 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 .changeset/late-planes-sniff.md diff --git a/.changeset/late-planes-sniff.md b/.changeset/late-planes-sniff.md new file mode 100644 index 000000000000..d702a938da78 --- /dev/null +++ b/.changeset/late-planes-sniff.md @@ -0,0 +1,7 @@ +--- +"@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/apps/meteor/app/lib/server/methods/updateMessage.ts b/apps/meteor/app/lib/server/methods/updateMessage.ts index 8cebe563cd23..c03208a438e9 100644 --- a/apps/meteor/app/lib/server/methods/updateMessage.ts +++ b/apps/meteor/app/lib/server/methods/updateMessage.ts @@ -10,7 +10,7 @@ import { hasPermissionAsync } from '../../../authorization/server/functions/hasP import { settings } from '../../../settings/server'; import { updateMessage } from '../functions/updateMessage'; -const allowedEditedFields = ['tshow', 'alias', 'attachments', 'avatar', 'emoji', 'msg', 'customFields', 'content']; +const allowedEditedFields = ['tshow', 'alias', 'attachments', 'avatar', 'emoji', 'msg', 'customFields', 'content', 'e2eMentions']; export async function executeUpdateMessage( uid: IUser['_id'], diff --git a/apps/meteor/app/mentions/server/Mentions.ts b/apps/meteor/app/mentions/server/Mentions.ts index 9eda56fea21c..779af2087932 100644 --- a/apps/meteor/app/mentions/server/Mentions.ts +++ b/apps/meteor/app/mentions/server/Mentions.ts @@ -2,7 +2,7 @@ * Mentions is a named function that will process Mentions * @param {Object} message - The message object */ -import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import { isE2EEMessage, type IMessage, type IRoom, type IUser } from '@rocket.chat/core-typings'; import { type MentionsParserArgs, MentionsParser } from '../lib/MentionsParser'; @@ -43,8 +43,13 @@ export class MentionsServer extends MentionsParser { }); } - async getUsersByMentions({ msg, rid, u: sender }: Pick): Promise { - const mentions = this.getUserMentions(msg); + async getUsersByMentions(message: IMessage): Promise { + const { msg, rid, u: sender, e2eMentions }: Pick = message; + + const mentions = + isE2EEMessage(message) && e2eMentions?.e2eUserMentions && e2eMentions?.e2eUserMentions.length > 0 + ? e2eMentions?.e2eUserMentions + : this.getUserMentions(msg); const mentionsAll: { _id: string; username: string }[] = []; const userMentions = []; @@ -67,8 +72,13 @@ export class MentionsServer extends MentionsParser { return [...mentionsAll, ...(userMentions.length ? await this.getUsers(userMentions) : [])]; } - async getChannelbyMentions({ msg }: Pick) { - const channels = this.getChannelMentions(msg); + async getChannelbyMentions(message: IMessage) { + const { msg, e2eMentions }: Pick = message; + + const channels = + isE2EEMessage(message) && e2eMentions?.e2eChannelMentions && e2eMentions?.e2eChannelMentions.length > 0 + ? e2eMentions?.e2eChannelMentions + : this.getChannelMentions(msg); return this.getChannels(channels.map((c) => c.trim().substr(1))); } diff --git a/apps/meteor/client/startup/e2e.ts b/apps/meteor/client/startup/e2e.ts index de615e8f45de..e45b62563726 100644 --- a/apps/meteor/client/startup/e2e.ts +++ b/apps/meteor/client/startup/e2e.ts @@ -5,6 +5,7 @@ import { Tracker } from 'meteor/tracker'; import { E2EEState } from '../../app/e2e/client/E2EEState'; import { e2e } from '../../app/e2e/client/rocketchat.e2e'; +import { MentionsParser } from '../../app/mentions/lib/MentionsParser'; import { ChatRoom } from '../../app/models/client'; import { settings } from '../../app/settings/client'; import { onClientBeforeSendMessage } from '../lib/onClientBeforeSendMessage'; @@ -88,6 +89,27 @@ Meteor.startup(() => { return message; } + const mentionsEnabled = settings.get('E2E_Enabled_Mentions'); + + if (mentionsEnabled) { + const me = Meteor.user()?.username || ''; + const pattern = settings.get('UTF8_User_Names_Validation'); + const useRealName = settings.get('UI_Use_Real_Name'); + + const mentions = new MentionsParser({ + pattern: () => pattern, + useRealName: () => useRealName, + me: () => me, + }); + + const e2eMentions: IMessage['e2eMentions'] = { + e2eUserMentions: mentions.getUserMentions(message.msg), + e2eChannelMentions: mentions.getChannelMentions(message.msg), + }; + + message.e2eMentions = e2eMentions; + } + // Should encrypt this message. return e2eRoom.encryptMessage(message); }); diff --git a/apps/meteor/server/settings/e2e.ts b/apps/meteor/server/settings/e2e.ts index 6f22784f1709..c8a69757128b 100644 --- a/apps/meteor/server/settings/e2e.ts +++ b/apps/meteor/server/settings/e2e.ts @@ -35,4 +35,10 @@ export const createE2ESettings = () => public: true, enableQuery: { _id: 'E2E_Enable', value: true }, }); + + await this.add('E2E_Enabled_Mentions', false, { + type: 'boolean', + public: true, + enableQuery: { _id: 'E2E_Enable', value: true }, + }); }); diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 8c6297e9975d..ad98df1aaa53 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -133,11 +133,13 @@ test.describe.serial('e2e-encryption', () => { test.beforeAll(async ({ api }) => { expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true })).status()).toBe(200); + expect((await api.post('/settings/E2E_Enabled_Mentions', { value: true })).status()).toBe(200); }); test.afterAll(async ({ api }) => { expect((await api.post('/settings/E2E_Enable', { value: false })).status()).toBe(200); expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); + expect((await api.post('/settings/E2E_Enabled_Mentions', { value: false })).status()).toBe(200); }); test('expect create a private channel encrypted and send an encrypted message', async ({ page }) => { @@ -265,6 +267,75 @@ test.describe.serial('e2e-encryption', () => { await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); }); + test('expect create a encrypted private channel and mention user', async ({ page }) => { + const channelName = faker.string.uuid(); + + await poHomeChannel.sidenav.createEncryptedChannel(channelName); + + await expect(page).toHaveURL(`/group/${channelName}`); + + await poHomeChannel.dismissToast(); + + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + await poHomeChannel.content.sendMessage('hello @user1'); + + const userMention = await page.getByRole('button', { + name: 'user1', + }); + + await expect(userMention).toBeVisible(); + }); + + test('expect create a encrypted private channel, mention a channel and navigate to it', async ({ page }) => { + const channelName = faker.string.uuid(); + + await poHomeChannel.sidenav.createEncryptedChannel(channelName); + + await expect(page).toHaveURL(`/group/${channelName}`); + + await poHomeChannel.dismissToast(); + + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + await poHomeChannel.content.sendMessage('Are you in the #general channel?'); + + const channelMention = await page.getByRole('button', { + name: 'general', + }); + + await expect(channelMention).toBeVisible(); + + await channelMention.click(); + + await expect(page).toHaveURL(`/channel/general`); + }); + + test('expect create a encrypted private channel, mention a channel and user', async ({ page }) => { + const channelName = faker.string.uuid(); + + await poHomeChannel.sidenav.createEncryptedChannel(channelName); + + await expect(page).toHaveURL(`/group/${channelName}`); + + await poHomeChannel.dismissToast(); + + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + await poHomeChannel.content.sendMessage('Are you in the #general channel, @user1 ?'); + + const channelMention = await page.getByRole('button', { + name: 'general', + }); + + const userMention = await page.getByRole('button', { + name: 'user1', + }); + + await expect(userMention).toBeVisible(); + await expect(channelMention).toBeVisible(); + }); + test('should encrypted field be available on edit room', async ({ page }) => { const channelName = faker.string.uuid(); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 9d5e2081ca93..519d9a4102aa 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -85,7 +85,7 @@ export class HomeContent { await this.joinRoomIfNeeded(); await this.page.waitForSelector('[name="msg"]:not([disabled])'); await this.page.locator('[name="msg"]').fill(text); - await this.page.keyboard.press('Enter'); + await this.page.getByLabel('Send').click(); } async dispatchSlashCommand(text: string): Promise { diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index 205cbaccd466..6c5511966ac8 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -170,6 +170,7 @@ export interface IMessage extends IRocketChatRecord { tcount?: number; t?: MessageTypesValues; e2e?: 'pending' | 'done'; + e2eMentions?: { e2eUserMentions?: string[]; e2eChannelMentions?: string[] }; otrAck?: string; urls?: MessageUrl[]; diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 728ca256952d..8ce6bea2e117 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1805,6 +1805,8 @@ "E2E_Enabled": "E2E Enabled", "E2E_Enabled_Default_DirectRooms": "Enable encryption for Direct Rooms by default", "E2E_Enabled_Default_PrivateRooms": "Enable encryption for Private Rooms by default", + "E2E_Enabled_Mentions": "Mentions", + "E2E_Enabled_Mentions_Description": "Notify people, and highlight user, channel, and team mentions in encrypted content.", "E2E_Enable_Encrypt_Files": "Encrypt files", "E2E_Enable_Encrypt_Files_Description": "Encrypt files sent inside encrypted rooms. Check for possible conflicts in [file upload settings.](admin/settings/FileUpload)", "E2E_Encryption_Password_Change": "Change Encryption Password", From 9c119a0abd9963b5ef22f64240cb118e65498c7e Mon Sep 17 00:00:00 2001 From: csuadev <72958726+csuadev@users.noreply.github.com> Date: Thu, 19 Sep 2024 19:16:24 -0500 Subject: [PATCH 23/39] fix: markdown inconsistency with bold and italics (#33157) --- .changeset/sweet-nails-grin.md | 5 + .../client/components/MarkdownText.spec.tsx | 92 +++++++++++++++++++ .../meteor/client/components/MarkdownText.tsx | 14 ++- 3 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 .changeset/sweet-nails-grin.md create mode 100644 apps/meteor/client/components/MarkdownText.spec.tsx diff --git a/.changeset/sweet-nails-grin.md b/.changeset/sweet-nails-grin.md new file mode 100644 index 000000000000..de240bfc0e3f --- /dev/null +++ b/.changeset/sweet-nails-grin.md @@ -0,0 +1,5 @@ +--- +"@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/apps/meteor/client/components/MarkdownText.spec.tsx b/apps/meteor/client/components/MarkdownText.spec.tsx new file mode 100644 index 000000000000..86ebadad8463 --- /dev/null +++ b/apps/meteor/client/components/MarkdownText.spec.tsx @@ -0,0 +1,92 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { render } from '@testing-library/react'; +import React from 'react'; + +import MarkdownText from './MarkdownText'; + +import '@testing-library/jest-dom'; + +const normalizeHtml = (html: any) => { + return html.replace(/\s+/g, ' ').trim(); +}; + +const markdownText = ` + # Heading 1 + **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 + Lists, Links and elements + **Unordered List** + - List Item 1 + - List Item 2 + - List Item 3 + - List Item 4 + **Ordered List** + 1. List Item 1 + 2. List Item 2 + 3. List Item 3 + 4. List Item 4 + **Links:** + [Rocket.Chat](rocket.chat) + gabriel.engel@rocket.chat + +55991999999 + \`Inline code\` + \`\`\`typescript + const test = 'this is code' + \`\`\` +`; + +it('should render html elements as expected using default parser', async () => { + const { container } = render(, { + wrapper: mockAppRoot().build(), + legacyRoot: true, + }); + + const normalizedHtml = normalizeHtml(container.innerHTML); + + expect(normalizedHtml).toContain('

    Heading 1

    '); + expect(normalizedHtml).toContain( + 'Paragraph text: Bold with one asterisk Bold with two asterisks Lorem ipsum dolor sit amet', + ); + expect(normalizedHtml).toContain('

    Heading 2

    '); + expect(normalizedHtml).toContain( + '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('
      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\' 
        '); +}); + +it('should render html elements as expected using inline parser', async () => { + const { container } = render(, { + wrapper: mockAppRoot().build(), + legacyRoot: true, + }); + + const normalizedHtml = normalizeHtml(container.innerHTML); + + expect(normalizedHtml).toContain('# Heading 1'); + expect(normalizedHtml).toContain( + 'Bold with one asterisk Bold with two asterisks Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + ); + expect(normalizedHtml).toContain('## Heading 2'); + expect(normalizedHtml).toContain( + 'Italic Text: Italic with one underscore Italic with two underscores Lorem ipsum dolor sit amet', + ); + expect(normalizedHtml).toContain('### Heading 3'); + expect(normalizedHtml).toContain('Unordered List - List Item 1 - List Item 2 - List Item 3 - List Item 4'); + expect(normalizedHtml).toContain('Ordered List 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(`typescript const test = 'this is code'`); +}); diff --git a/apps/meteor/client/components/MarkdownText.tsx b/apps/meteor/client/components/MarkdownText.tsx index 6426b24810ee..0b7d2efa780e 100644 --- a/apps/meteor/client/components/MarkdownText.tsx +++ b/apps/meteor/client/components/MarkdownText.tsx @@ -20,12 +20,18 @@ const documentRenderer = new marked.Renderer(); const inlineRenderer = new marked.Renderer(); const inlineWithoutBreaks = new marked.Renderer(); -marked.Lexer.rules.gfm = { - ...marked.Lexer.rules.gfm, - strong: /^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/, - em: /^__(?=\S)([\s\S]*?\S)__(?!_)|^_(?=\S)([\s\S]*?\S)_(?!_)/, +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'; + } }; +marked.use({ walkTokens }); + const linkMarked = (href: string | null, _title: string | null, text: string): string => `${text} `; const paragraphMarked = (text: string): string => text; From 599762739a71ee6b0f3a090db571a47498876a86 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Thu, 19 Sep 2024 21:20:48 -0300 Subject: [PATCH 24/39] fix: conference calls are shown as "not answered" after they end (#33179) --- .changeset/five-coats-rhyme.md | 5 ++++ apps/uikit-playground/package.json | 1 + apps/uikit-playground/vite.config.ts | 4 +-- .../VideoConferenceBlock.tsx | 25 +++++++++++++------ 4 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 .changeset/five-coats-rhyme.md diff --git a/.changeset/five-coats-rhyme.md b/.changeset/five-coats-rhyme.md new file mode 100644 index 000000000000..c5359e3c978a --- /dev/null +++ b/.changeset/five-coats-rhyme.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/fuselage-ui-kit': patch +--- + +Fixed an error that incorrectly showed conference calls as not answered after they ended diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index 791526c46f9d..4cca93dc00e9 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -15,6 +15,7 @@ "@codemirror/lang-json": "^6.0.1", "@codemirror/tooltip": "^0.19.16", "@lezer/highlight": "^1.1.6", + "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "~0.31.25", "@rocket.chat/fuselage": "^0.59.0", "@rocket.chat/fuselage-hooks": "^0.33.1", diff --git a/apps/uikit-playground/vite.config.ts b/apps/uikit-playground/vite.config.ts index 61a5ab30e647..4d382d652859 100644 --- a/apps/uikit-playground/vite.config.ts +++ b/apps/uikit-playground/vite.config.ts @@ -7,11 +7,11 @@ export default defineConfig(() => ({ esbuild: {}, plugins: [react()], optimizeDeps: { - include: ['@rocket.chat/ui-contexts', '@rocket.chat/message-parser'], + include: ['@rocket.chat/ui-contexts', '@rocket.chat/message-parser', '@rocket.chat/core-typings'], }, build: { commonjsOptions: { - include: [/ui-contexts/, /message-parser/, /node_modules/], + include: [/ui-contexts/, /core-typings/, /message-parser/, /node_modules/], }, }, })); diff --git a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx index 969ad0af1d7c..7125dbbf1bc4 100644 --- a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx +++ b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx @@ -1,3 +1,4 @@ +import { VideoConferenceStatus } from '@rocket.chat/core-typings'; import { useGoToRoom, useTranslation, @@ -133,9 +134,14 @@ const VideoConferenceBlock = ({ {isUserCaller ? t('Call_again') : t('Call_back')} - - {t('Call_was_not_answered')} - + {[ + VideoConferenceStatus.EXPIRED, + VideoConferenceStatus.DECLINED, + ].includes(data.status) && ( + + {t('Call_was_not_answered')} + + )} )} {data.type !== 'direct' && @@ -151,16 +157,21 @@ const VideoConferenceBlock = ({ ) : ( - - {t('Call_was_not_answered')} - + [ + VideoConferenceStatus.EXPIRED, + VideoConferenceStatus.DECLINED, + ].includes(data.status) && ( + + {t('Call_was_not_answered')} + + ) ))} ); } - if (data.type === 'direct' && data.status === 0) { + if (data.type === 'direct' && data.status === VideoConferenceStatus.CALLING) { return ( From 027183fc3e2ffe2b2c2b0875e43dd636aed99ae4 Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Fri, 20 Sep 2024 00:47:27 +0000 Subject: [PATCH 25/39] Release 6.13.0-rc.0 --- .changeset/pre.json | 100 +++++++++++++++++ apps/meteor/CHANGELOG.md | 101 ++++++++++++++++++ apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/ee/server/services/CHANGELOG.md | 14 +++ apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- apps/uikit-playground/CHANGELOG.md | 16 +++ apps/uikit-playground/package.json | 2 +- ee/apps/account-service/CHANGELOG.md | 13 +++ ee/apps/account-service/package.json | 2 +- ee/apps/authorization-service/CHANGELOG.md | 13 +++ ee/apps/authorization-service/package.json | 2 +- ee/apps/ddp-streamer/CHANGELOG.md | 14 +++ ee/apps/ddp-streamer/package.json | 2 +- ee/apps/omnichannel-transcript/CHANGELOG.md | 14 +++ ee/apps/omnichannel-transcript/package.json | 2 +- ee/apps/presence-service/CHANGELOG.md | 13 +++ ee/apps/presence-service/package.json | 2 +- ee/apps/queue-worker/CHANGELOG.md | 13 +++ ee/apps/queue-worker/package.json | 2 +- ee/apps/stream-hub-service/CHANGELOG.md | 12 +++ ee/apps/stream-hub-service/package.json | 2 +- ee/packages/license/CHANGELOG.md | 9 ++ ee/packages/license/package.json | 2 +- ee/packages/omnichannel-services/CHANGELOG.md | 14 +++ ee/packages/omnichannel-services/package.json | 2 +- ee/packages/pdf-worker/CHANGELOG.md | 9 ++ ee/packages/pdf-worker/package.json | 2 +- ee/packages/presence/CHANGELOG.md | 11 ++ ee/packages/presence/package.json | 2 +- ee/packages/ui-theming/CHANGELOG.md | 6 ++ ee/packages/ui-theming/package.json | 2 +- package.json | 2 +- packages/api-client/CHANGELOG.md | 10 ++ packages/api-client/package.json | 2 +- packages/apps/CHANGELOG.md | 10 ++ packages/apps/package.json | 2 +- packages/core-services/CHANGELOG.md | 20 ++++ packages/core-services/package.json | 2 +- packages/core-typings/CHANGELOG.md | 17 +++ packages/core-typings/package.json | 2 +- packages/cron/CHANGELOG.md | 10 ++ packages/cron/package.json | 2 +- packages/ddp-client/CHANGELOG.md | 11 ++ packages/ddp-client/package.json | 2 +- packages/fuselage-ui-kit/CHANGELOG.md | 21 ++++ packages/fuselage-ui-kit/package.json | 8 +- packages/gazzodown/CHANGELOG.md | 16 +++ packages/gazzodown/package.json | 8 +- packages/i18n/CHANGELOG.md | 14 +++ packages/i18n/package.json | 2 +- packages/instance-status/CHANGELOG.md | 9 ++ packages/instance-status/package.json | 2 +- packages/livechat/CHANGELOG.md | 15 +++ packages/livechat/package.json | 2 +- packages/message-parser/CHANGELOG.md | 6 ++ packages/message-parser/package.json | 2 +- packages/mock-providers/CHANGELOG.md | 9 ++ packages/mock-providers/package.json | 2 +- packages/model-typings/CHANGELOG.md | 19 ++++ packages/model-typings/package.json | 2 +- packages/models/CHANGELOG.md | 13 +++ packages/models/package.json | 2 +- packages/peggy-loader/CHANGELOG.md | 6 ++ packages/peggy-loader/package.json | 2 +- packages/rest-typings/CHANGELOG.md | 24 +++++ packages/rest-typings/package.json | 2 +- packages/ui-avatar/CHANGELOG.md | 13 +++ packages/ui-avatar/package.json | 4 +- packages/ui-client/CHANGELOG.md | 18 ++++ packages/ui-client/package.json | 6 +- packages/ui-composer/CHANGELOG.md | 6 ++ packages/ui-composer/package.json | 2 +- packages/ui-contexts/CHANGELOG.md | 12 +++ packages/ui-contexts/package.json | 2 +- packages/ui-video-conf/CHANGELOG.md | 14 +++ packages/ui-video-conf/package.json | 6 +- packages/web-ui-registration/CHANGELOG.md | 9 ++ packages/web-ui-registration/package.json | 4 +- yarn.lock | 1 + 80 files changed, 727 insertions(+), 52 deletions(-) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 000000000000..6976ce2b60aa --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,100 @@ +{ + "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": [ + "brown-singers-appear", + "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", + "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/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 97a246abaca6..dd941b905c3c 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,106 @@ # @rocket.chat/meteor +## 6.13.0-rc.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 + +- ([#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 + +- ([#33317](https://github.com/RocketChat/Rocket.Chat/pull/33317)) Fixed error during sendmessage client stub + +- ([#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, 532f08819e, 79c16d315a, 927710d778, 3a161c4310, 0f21fa01a3, 12d6307998]: + + - @rocket.chat/ui-client@11.0.0-rc.0 + - @rocket.chat/i18n@0.8.0-rc.0 + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/rest-typings@6.13.0-rc.0 + - @rocket.chat/fuselage-ui-kit@11.0.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/ui-theming@0.3.0-rc.0 + - @rocket.chat/ui-video-conf@11.0.0-rc.0 + - @rocket.chat/ui-composer@0.3.0-rc.0 + - @rocket.chat/gazzodown@11.0.0-rc.0 + - @rocket.chat/ui-avatar@7.0.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/message-parser@0.31.30-rc.0 + - @rocket.chat/models@0.3.0-rc.0 + - @rocket.chat/web-ui-registration@11.0.0-rc.0 + - @rocket.chat/ui-contexts@11.0.0-rc.0 + - @rocket.chat/omnichannel-services@0.3.4-rc.0 + - @rocket.chat/apps@0.1.7-rc.0 + - @rocket.chat/presence@0.2.7-rc.0 + - @rocket.chat/api-client@0.2.7-rc.0 + - @rocket.chat/license@0.2.7-rc.0 + - @rocket.chat/pdf-worker@0.2.4-rc.0 + - @rocket.chat/cron@0.1.7-rc.0 + - @rocket.chat/instance-status@0.1.7-rc.0 + - @rocket.chat/server-cloud-communication@0.0.2 +
        + ## 6.12.0 ### Minor Changes diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index cb3ad01b882a..fc41ef05cacd 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "6.13.0-develop" + "version": "6.13.0-rc.0" } diff --git a/apps/meteor/ee/server/services/CHANGELOG.md b/apps/meteor/ee/server/services/CHANGELOG.md index 1d6e058d7597..6d9cafc01648 100644 --- a/apps/meteor/ee/server/services/CHANGELOG.md +++ b/apps/meteor/ee/server/services/CHANGELOG.md @@ -1,5 +1,19 @@ # rocketchat-services +## 1.3.4-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 9eaefdc892, 274f4f5881, 532f08819e, 79c16d315a, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/rest-typings@6.13.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/message-parser@0.31.30-rc.0 + - @rocket.chat/models@0.3.0-rc.0 +
        + ## 1.3.3 ### Patch Changes diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 43659382eb67..82a55122337e 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -1,7 +1,7 @@ { "name": "rocketchat-services", "private": true, - "version": "1.3.3", + "version": "1.3.4-rc.0", "description": "Rocket.Chat Authorization service", "main": "index.js", "scripts": { diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 9ee7e48d1794..a09dcb657466 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "6.13.0-develop", + "version": "6.13.0-rc.0", "private": true, "author": { "name": "Rocket.Chat", diff --git a/apps/uikit-playground/CHANGELOG.md b/apps/uikit-playground/CHANGELOG.md index 63a8eb47d7c6..27cdcb061719 100644 --- a/apps/uikit-playground/CHANGELOG.md +++ b/apps/uikit-playground/CHANGELOG.md @@ -1,5 +1,21 @@ # @rocket.chat/uikit-playground +## 0.5.0-rc.0 + +### Minor Changes + +- ([#32821](https://github.com/RocketChat/Rocket.Chat/pull/32821)) Replaced new `SidebarV2` components under feature preview + +### Patch Changes + +-
        Updated dependencies [599762739a, 274f4f5881, cd0d50016e, 78e6ba4820, 927710d778, 12d6307998]: + + - @rocket.chat/fuselage-ui-kit@11.0.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/ui-avatar@7.0.0-rc.0 + - @rocket.chat/ui-contexts@11.0.0-rc.0 +
        + ## 0.4.0 ### Minor Changes diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index 4cca93dc00e9..48c099b8d9d1 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/uikit-playground", "private": true, - "version": "0.4.0", + "version": "0.5.0-rc.0", "type": "module", "scripts": { "dev": "vite", diff --git a/ee/apps/account-service/CHANGELOG.md b/ee/apps/account-service/CHANGELOG.md index 8d306d7b1c17..40d597827934 100644 --- a/ee/apps/account-service/CHANGELOG.md +++ b/ee/apps/account-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/account-service +## 0.4.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 9eaefdc892, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/rest-typings@6.13.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/models@0.3.0-rc.0 +
        + ## 0.4.6 ### Patch Changes diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index b9e45ed14eda..8438504f62f6 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/account-service", "private": true, - "version": "0.4.6", + "version": "0.4.7-rc.0", "description": "Rocket.Chat Account service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/authorization-service/CHANGELOG.md b/ee/apps/authorization-service/CHANGELOG.md index 494c052e8cf8..70479a00abdc 100644 --- a/ee/apps/authorization-service/CHANGELOG.md +++ b/ee/apps/authorization-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/authorization-service +## 0.4.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 9eaefdc892, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/rest-typings@6.13.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/models@0.3.0-rc.0 +
        + ## 0.4.6 ### Patch Changes diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index b7912b8bc94d..19cbbd4035cf 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/authorization-service", "private": true, - "version": "0.4.6", + "version": "0.4.7-rc.0", "description": "Rocket.Chat Authorization service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/ddp-streamer/CHANGELOG.md b/ee/apps/ddp-streamer/CHANGELOG.md index 97565b8bde88..2e13a89d03ec 100644 --- a/ee/apps/ddp-streamer/CHANGELOG.md +++ b/ee/apps/ddp-streamer/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/ddp-streamer +## 0.3.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 9eaefdc892, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/rest-typings@6.13.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/models@0.3.0-rc.0 + - @rocket.chat/instance-status@0.1.7-rc.0 +
        + ## 0.3.6 ### Patch Changes diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 87029e4b8993..d3ae93df7264 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/ddp-streamer", "private": true, - "version": "0.3.6", + "version": "0.3.7-rc.0", "description": "Rocket.Chat DDP-Streamer service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/omnichannel-transcript/CHANGELOG.md b/ee/apps/omnichannel-transcript/CHANGELOG.md index 21fc879f9fd9..b29eac976532 100644 --- a/ee/apps/omnichannel-transcript/CHANGELOG.md +++ b/ee/apps/omnichannel-transcript/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/omnichannel-transcript +## 0.4.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/models@0.3.0-rc.0 + - @rocket.chat/omnichannel-services@0.3.4-rc.0 + - @rocket.chat/pdf-worker@0.2.4-rc.0 +
        + ## 0.4.6 ### Patch Changes diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 27290070b653..f28dd6a6d783 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/omnichannel-transcript", "private": true, - "version": "0.4.6", + "version": "0.4.7-rc.0", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/presence-service/CHANGELOG.md b/ee/apps/presence-service/CHANGELOG.md index a1ed37bb6fde..a5ad6f38cfc5 100644 --- a/ee/apps/presence-service/CHANGELOG.md +++ b/ee/apps/presence-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/presence-service +## 0.4.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/models@0.3.0-rc.0 + - @rocket.chat/presence@0.2.7-rc.0 +
        + ## 0.4.6 ### Patch Changes diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index 5968631341b0..898de184e655 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/presence-service", "private": true, - "version": "0.4.6", + "version": "0.4.7-rc.0", "description": "Rocket.Chat Presence service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/queue-worker/CHANGELOG.md b/ee/apps/queue-worker/CHANGELOG.md index 16dc2590f38b..7517afb9c624 100644 --- a/ee/apps/queue-worker/CHANGELOG.md +++ b/ee/apps/queue-worker/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/queue-worker +## 0.4.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/models@0.3.0-rc.0 + - @rocket.chat/omnichannel-services@0.3.4-rc.0 +
        + ## 0.4.6 ### Patch Changes diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index b3d8b0aff94e..c661ab840b9b 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/queue-worker", "private": true, - "version": "0.4.6", + "version": "0.4.7-rc.0", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/stream-hub-service/CHANGELOG.md b/ee/apps/stream-hub-service/CHANGELOG.md index ccbb83e7de0b..3469df54a291 100644 --- a/ee/apps/stream-hub-service/CHANGELOG.md +++ b/ee/apps/stream-hub-service/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/stream-hub-service +## 0.4.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/models@0.3.0-rc.0 +
        + ## 0.4.6 ### Patch Changes diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index 9fd5cf0586f6..aecc6e9a9e99 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/stream-hub-service", "private": true, - "version": "0.4.6", + "version": "0.4.7-rc.0", "description": "Rocket.Chat Stream Hub service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/packages/license/CHANGELOG.md b/ee/packages/license/CHANGELOG.md index 181b6dfb0e7f..277ae4cbed4a 100644 --- a/ee/packages/license/CHANGELOG.md +++ b/ee/packages/license/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/license +## 0.2.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [274f4f5881, 927710d778, 12d6307998]: + + - @rocket.chat/core-typings@6.13.0-rc.0 +
        + ## 0.2.6 ### Patch Changes diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 3eceb35e27ff..4adcdae3826f 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/license", - "version": "0.2.6", + "version": "0.2.7-rc.0", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/omnichannel-services/CHANGELOG.md b/ee/packages/omnichannel-services/CHANGELOG.md index f54493a31cd1..9ee4cf76a087 100644 --- a/ee/packages/omnichannel-services/CHANGELOG.md +++ b/ee/packages/omnichannel-services/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/omnichannel-services +## 0.3.4-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 9eaefdc892, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/rest-typings@6.13.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/models@0.3.0-rc.0 + - @rocket.chat/pdf-worker@0.2.4-rc.0 +
        + ## 0.3.3 ### Patch Changes diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index 696ac6fee640..f6d4f34e8e1a 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/omnichannel-services", - "version": "0.3.3", + "version": "0.3.4-rc.0", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/pdf-worker/CHANGELOG.md b/ee/packages/pdf-worker/CHANGELOG.md index ccc01c92e84f..aa927a7b7c26 100644 --- a/ee/packages/pdf-worker/CHANGELOG.md +++ b/ee/packages/pdf-worker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/pdf-worker +## 0.2.4-rc.0 + +### Patch Changes + +-
        Updated dependencies [274f4f5881, 927710d778, 12d6307998]: + + - @rocket.chat/core-typings@6.13.0-rc.0 +
        + ## 0.2.3 ### Patch Changes diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index 234463087a4e..824f196df01c 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/pdf-worker", - "version": "0.2.3", + "version": "0.2.4-rc.0", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/presence/CHANGELOG.md b/ee/packages/presence/CHANGELOG.md index a4effccb5f2b..b4d74639e58f 100644 --- a/ee/packages/presence/CHANGELOG.md +++ b/ee/packages/presence/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/presence +## 0.2.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/models@0.3.0-rc.0 +
        + ## 0.2.6 ### Patch Changes diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index 4d599562b278..80c6afe06875 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/presence", - "version": "0.2.6", + "version": "0.2.7-rc.0", "private": true, "devDependencies": { "@babel/core": "~7.22.20", diff --git a/ee/packages/ui-theming/CHANGELOG.md b/ee/packages/ui-theming/CHANGELOG.md index 3a4956d146c3..b25ae24af33d 100644 --- a/ee/packages/ui-theming/CHANGELOG.md +++ b/ee/packages/ui-theming/CHANGELOG.md @@ -1,5 +1,11 @@ # @rocket.chat/ui-theming +## 0.3.0-rc.0 + +### Minor Changes + +- ([#32821](https://github.com/RocketChat/Rocket.Chat/pull/32821)) Replaced new `SidebarV2` components under feature preview + ## 0.2.1 ### Patch Changes diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index d6e3e93c01a4..0548f235c3fd 100644 --- a/ee/packages/ui-theming/package.json +++ b/ee/packages/ui-theming/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-theming", - "version": "0.2.1", + "version": "0.3.0-rc.0", "private": true, "devDependencies": { "@rocket.chat/css-in-js": "~0.31.25", diff --git a/package.json b/package.json index f3e3ce3d3e91..2f89cab55d2a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "6.13.0-develop", + "version": "6.13.0-rc.0", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/api-client/CHANGELOG.md b/packages/api-client/CHANGELOG.md index 50096d81e901..6ba6da8d6cb3 100644 --- a/packages/api-client/CHANGELOG.md +++ b/packages/api-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/api-client +## 0.2.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 9eaefdc892, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/rest-typings@6.13.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 +
        + ## 0.2.6 ### Patch Changes diff --git a/packages/api-client/package.json b/packages/api-client/package.json index b8d9ac155144..7e08a4d4f4d7 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/api-client", - "version": "0.2.6", + "version": "0.2.7-rc.0", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.13", diff --git a/packages/apps/CHANGELOG.md b/packages/apps/CHANGELOG.md index adb1b989bad0..4c7d0df77a29 100644 --- a/packages/apps/CHANGELOG.md +++ b/packages/apps/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/apps +## 0.1.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 274f4f5881, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 +
        + ## 0.1.6 ### Patch Changes diff --git a/packages/apps/package.json b/packages/apps/package.json index ba7d008543b7..b50ad6b74309 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps", - "version": "0.1.6", + "version": "0.1.7-rc.0", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/core-services/CHANGELOG.md b/packages/core-services/CHANGELOG.md index a73f804408ba..58b5d0df4f6e 100644 --- a/packages/core-services/CHANGELOG.md +++ b/packages/core-services/CHANGELOG.md @@ -1,5 +1,25 @@ # @rocket.chat/core-services +## 0.7.0-rc.0 + +### Minor Changes + +- ([#33011](https://github.com/RocketChat/Rocket.Chat/pull/33011)) Return `parent` and `team` information when calling `rooms.info` endpoint + +- ([#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. + +- ([#33225](https://github.com/RocketChat/Rocket.Chat/pull/33225)) Implemented new feature preview for Sidepanel + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 9eaefdc892, 274f4f5881, 532f08819e, 79c16d315a, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/rest-typings@6.13.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/message-parser@0.31.30-rc.0 + - @rocket.chat/models@0.3.0-rc.0 +
        + ## 0.6.0 ### Minor Changes diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 74250ffa8c16..7326f3cd3617 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/core-services", - "version": "0.6.0", + "version": "0.7.0-rc.0", "private": true, "devDependencies": { "@babel/core": "~7.22.20", diff --git a/packages/core-typings/CHANGELOG.md b/packages/core-typings/CHANGELOG.md index ac2c861590c0..1898fe2767c1 100644 --- a/packages/core-typings/CHANGELOG.md +++ b/packages/core-typings/CHANGELOG.md @@ -1,5 +1,22 @@ # @rocket.chat/core-typings +## 6.13.0-rc.0 + +### Minor Changes + +- ([#32693](https://github.com/RocketChat/Rocket.Chat/pull/32693)) Introduced "create contacts" endpoint to omnichannel + +- ([#33225](https://github.com/RocketChat/Rocket.Chat/pull/33225)) Implemented new feature preview for Sidepanel + +### Patch Changes + +- ([#32510](https://github.com/RocketChat/Rocket.Chat/pull/32510)) Added a new setting to enable mentions in end to end encrypted channels + +-
        Updated dependencies [79c16d315a]: + + - @rocket.chat/message-parser@0.31.30-rc.0 +
        + ## 6.12.0 ### Minor Changes diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 86123a156e75..2c5cb3f64a2d 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", - "version": "6.13.0-develop", + "version": "6.13.0-rc.0", "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", "eslint": "~8.45.0", diff --git a/packages/cron/CHANGELOG.md b/packages/cron/CHANGELOG.md index 9c0c43c6f16e..7dbf3c56c145 100644 --- a/packages/cron/CHANGELOG.md +++ b/packages/cron/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/cron +## 0.1.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [274f4f5881, 927710d778, 12d6307998]: + + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/models@0.3.0-rc.0 +
        + ## 0.1.6 ### Patch Changes diff --git a/packages/cron/package.json b/packages/cron/package.json index aa1226271bba..a9f6a0583f78 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/cron", - "version": "0.1.6", + "version": "0.1.7-rc.0", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/ddp-client/CHANGELOG.md b/packages/ddp-client/CHANGELOG.md index 29e062a1374d..82ccc01d5a24 100644 --- a/packages/ddp-client/CHANGELOG.md +++ b/packages/ddp-client/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ddp-client +## 0.3.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 9eaefdc892, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/rest-typings@6.13.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/api-client@0.2.7-rc.0 +
        + ## 0.3.6 ### Patch Changes diff --git a/packages/ddp-client/package.json b/packages/ddp-client/package.json index 0fc47dc9a122..5b78e5ebf1e9 100644 --- a/packages/ddp-client/package.json +++ b/packages/ddp-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ddp-client", - "version": "0.3.6", + "version": "0.3.7-rc.0", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.13", diff --git a/packages/fuselage-ui-kit/CHANGELOG.md b/packages/fuselage-ui-kit/CHANGELOG.md index 41cc87932393..085dedb33bd1 100644 --- a/packages/fuselage-ui-kit/CHANGELOG.md +++ b/packages/fuselage-ui-kit/CHANGELOG.md @@ -1,5 +1,26 @@ # Change Log +## 11.0.0-rc.0 + +### Minor Changes + +- ([#32821](https://github.com/RocketChat/Rocket.Chat/pull/32821)) Replaced new `SidebarV2` components under feature preview + +### Patch Changes + +- ([#33179](https://github.com/RocketChat/Rocket.Chat/pull/33179)) Fixed an error that incorrectly showed conference calls as not answered after they ended + +- ([#32999](https://github.com/RocketChat/Rocket.Chat/pull/32999)) Fixes multiple selection for MultiStaticSelectElement in UiKit + +-
        Updated dependencies [274f4f5881, cd0d50016e, 927710d778, 12d6307998]: + + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/ui-video-conf@11.0.0-rc.0 + - @rocket.chat/gazzodown@11.0.0-rc.0 + - @rocket.chat/ui-avatar@7.0.0-rc.0 + - @rocket.chat/ui-contexts@11.0.0-rc.0 +
        + ## 10.0.0 ### Patch Changes diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 80874491a951..8fb0ca254d68 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/fuselage-ui-kit", "private": true, - "version": "10.0.0", + "version": "11.0.0-rc.0", "description": "UiKit elements for Rocket.Chat Apps built under Fuselage design system", "homepage": "https://rocketchat.github.io/Rocket.Chat.Fuselage/", "author": { @@ -50,10 +50,10 @@ "@rocket.chat/icons": "*", "@rocket.chat/prettier-config": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-avatar": "*", - "@rocket.chat/ui-contexts": "*", + "@rocket.chat/ui-avatar": "7.0.0-rc.0", + "@rocket.chat/ui-contexts": "11.0.0-rc.0", "@rocket.chat/ui-kit": "*", - "@rocket.chat/ui-video-conf": "*", + "@rocket.chat/ui-video-conf": "11.0.0-rc.0", "@tanstack/react-query": "*", "react": "*", "react-dom": "*" diff --git a/packages/gazzodown/CHANGELOG.md b/packages/gazzodown/CHANGELOG.md index 74cd44cc291c..5216831e8365 100644 --- a/packages/gazzodown/CHANGELOG.md +++ b/packages/gazzodown/CHANGELOG.md @@ -1,5 +1,21 @@ # @rocket.chat/gazzodown +## 11.0.0-rc.0 + +### Minor Changes + +- ([#32821](https://github.com/RocketChat/Rocket.Chat/pull/32821)) Replaced new `SidebarV2` components under feature preview + +### Patch Changes + +-
        Updated dependencies [bb94c9c67a, 274f4f5881, cd0d50016e, 79c16d315a, 927710d778, 12d6307998]: + + - @rocket.chat/ui-client@11.0.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/message-parser@0.31.30-rc.0 + - @rocket.chat/ui-contexts@11.0.0-rc.0 +
        + ## 10.0.0 ### Patch Changes diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index c2fbf136b2d6..47bf2782226e 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/gazzodown", - "version": "10.0.0", + "version": "11.0.0-rc.0", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", @@ -72,10 +72,10 @@ "@rocket.chat/css-in-js": "*", "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-tokens": "*", - "@rocket.chat/message-parser": "0.31.29", + "@rocket.chat/message-parser": "0.31.30-rc.0", "@rocket.chat/styled": "*", - "@rocket.chat/ui-client": "*", - "@rocket.chat/ui-contexts": "*", + "@rocket.chat/ui-client": "11.0.0-rc.0", + "@rocket.chat/ui-contexts": "11.0.0-rc.0", "katex": "*", "react": "*" }, diff --git a/packages/i18n/CHANGELOG.md b/packages/i18n/CHANGELOG.md index 8a7e4f7317b9..27bb4b471c66 100644 --- a/packages/i18n/CHANGELOG.md +++ b/packages/i18n/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/i18n +## 0.8.0-rc.0 + +### Minor Changes + +- ([#33156](https://github.com/RocketChat/Rocket.Chat/pull/33156)) added `sidepanelNavigation` to feature preview list + +- ([#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. +- ([#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. + +### Patch Changes + +- ([#32510](https://github.com/RocketChat/Rocket.Chat/pull/32510)) Added a new setting to enable mentions in end to end encrypted channels + ## 0.7.0 ### Minor Changes diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 7c66e102d578..fe98e3b1f7fc 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/i18n", - "version": "0.7.0", + "version": "0.8.0-rc.0", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/packages/instance-status/CHANGELOG.md b/packages/instance-status/CHANGELOG.md index bebd0fd56b85..e98235550ef8 100644 --- a/packages/instance-status/CHANGELOG.md +++ b/packages/instance-status/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/instance-status +## 0.1.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [927710d778]: + + - @rocket.chat/models@0.3.0-rc.0 +
        + ## 0.1.6 ### Patch Changes diff --git a/packages/instance-status/package.json b/packages/instance-status/package.json index 5ea3b2cb6e0d..d092d649c80f 100644 --- a/packages/instance-status/package.json +++ b/packages/instance-status/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/instance-status", - "version": "0.1.6", + "version": "0.1.7-rc.0", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/livechat/CHANGELOG.md b/packages/livechat/CHANGELOG.md index 598fae50d68d..3c9bde71c23a 100644 --- a/packages/livechat/CHANGELOG.md +++ b/packages/livechat/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/livechat Change Log +## 1.20.0-rc.0 + +### Minor Changes + +- ([#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. + +### Patch Changes + +-
        Updated dependencies [cd0d50016e, 79c16d315a]: + + - @rocket.chat/gazzodown@11.0.0-rc.0 + - @rocket.chat/message-parser@0.31.30-rc.0 +
        + ## 1.19.3 ### Patch Changes diff --git a/packages/livechat/package.json b/packages/livechat/package.json index ce7bb92d6b78..ab4a2a9b3a11 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/livechat", - "version": "1.19.3", + "version": "1.20.0-rc.0", "files": [ "/build" ], diff --git a/packages/message-parser/CHANGELOG.md b/packages/message-parser/CHANGELOG.md index 39c82e350b58..f263b04e632c 100644 --- a/packages/message-parser/CHANGELOG.md +++ b/packages/message-parser/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 0.31.30-rc.0 + +### Patch Changes + +- ([#33227](https://github.com/RocketChat/Rocket.Chat/pull/33227)) Improved the performance of the message parser + ## 0.31.29 ### Patch Changes diff --git a/packages/message-parser/package.json b/packages/message-parser/package.json index e58727ba38c2..39fbb4bda5dd 100644 --- a/packages/message-parser/package.json +++ b/packages/message-parser/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/message-parser", "description": "Rocket.Chat parser for messages", - "version": "0.31.29", + "version": "0.31.30-rc.0", "author": { "name": "Rocket.Chat", "url": "https://rocket.chat/" diff --git a/packages/mock-providers/CHANGELOG.md b/packages/mock-providers/CHANGELOG.md index 3a2edfdf99ff..5bd539fda340 100644 --- a/packages/mock-providers/CHANGELOG.md +++ b/packages/mock-providers/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/mock-providers +## 0.1.3-rc.0 + +### Patch Changes + +-
        Updated dependencies [bb94c9c67a, 7c14fd1a80, 274f4f5881, 0f21fa01a3]: + + - @rocket.chat/i18n@0.8.0-rc.0 +
        + ## 0.1.2 ### Patch Changes diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index f9e9b4d06ab8..1a7051e87ff2 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/mock-providers", - "version": "0.1.2", + "version": "0.1.3-rc.0", "private": true, "dependencies": { "@rocket.chat/emitter": "~0.31.25", diff --git a/packages/model-typings/CHANGELOG.md b/packages/model-typings/CHANGELOG.md index b743a1132b48..f0aa631439e9 100644 --- a/packages/model-typings/CHANGELOG.md +++ b/packages/model-typings/CHANGELOG.md @@ -1,5 +1,24 @@ # @rocket.chat/model-typings +## 0.8.0-rc.0 + +### Minor Changes + +- ([#32682](https://github.com/RocketChat/Rocket.Chat/pull/32682)) Added support for specifying a unit on departments' creation and update + +- ([#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. + +- ([#33225](https://github.com/RocketChat/Rocket.Chat/pull/33225)) Implemented new feature preview for Sidepanel + +### Patch Changes + +-
        Updated dependencies [274f4f5881, 927710d778, 12d6307998]: + + - @rocket.chat/core-typings@6.13.0-rc.0 +
        + ## 0.7.0 ### Minor Changes diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index 30144dabb49a..10a2fd9b643c 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/model-typings", - "version": "0.7.0", + "version": "0.8.0-rc.0", "private": true, "devDependencies": { "@types/node-rsa": "^1.1.4", diff --git a/packages/models/CHANGELOG.md b/packages/models/CHANGELOG.md index 1238c213ec4f..1251d75001bb 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/models +## 0.3.0-rc.0 + +### Minor Changes + +- ([#32693](https://github.com/RocketChat/Rocket.Chat/pull/32693)) Introduced "create contacts" endpoint to omnichannel + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 +
        + ## 0.2.3 ### Patch Changes diff --git a/packages/models/package.json b/packages/models/package.json index e55ce8d5a0e1..35064171d9a6 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/models", - "version": "0.2.3", + "version": "0.3.0-rc.0", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/peggy-loader/CHANGELOG.md b/packages/peggy-loader/CHANGELOG.md index 0c1ace99d7d4..422c09e4bc9a 100644 --- a/packages/peggy-loader/CHANGELOG.md +++ b/packages/peggy-loader/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 0.31.26-rc.0 + +### Patch Changes + +- ([#33227](https://github.com/RocketChat/Rocket.Chat/pull/33227)) Improved the performance of the message parser + All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. diff --git a/packages/peggy-loader/package.json b/packages/peggy-loader/package.json index 6eb584f62d24..464be6fdc297 100644 --- a/packages/peggy-loader/package.json +++ b/packages/peggy-loader/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/peggy-loader", - "version": "0.31.25", + "version": "0.31.26-rc.0", "description": "Peggy loader for webpack", "keywords": [ "peggy", diff --git a/packages/rest-typings/CHANGELOG.md b/packages/rest-typings/CHANGELOG.md index 7cce8dcc6e99..b11458549219 100644 --- a/packages/rest-typings/CHANGELOG.md +++ b/packages/rest-typings/CHANGELOG.md @@ -1,5 +1,29 @@ # @rocket.chat/rest-typings +## 6.13.0-rc.0 + +### Minor Changes + +- ([#32682](https://github.com/RocketChat/Rocket.Chat/pull/32682)) Added support for specifying a unit on departments' creation and update + +- ([#32729](https://github.com/RocketChat/Rocket.Chat/pull/32729)) Implemented "omnichannel/contacts.update" endpoint to update contacts + +- ([#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. + +- ([#33225](https://github.com/RocketChat/Rocket.Chat/pull/33225)) Implemented new feature preview for Sidepanel + +### Patch Changes + +-
        Updated dependencies [274f4f5881, 79c16d315a, 927710d778, 12d6307998]: + + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/message-parser@0.31.30-rc.0 +
        + ## 6.12.0 ### Minor Changes diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 972577210f56..33aad6d64823 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "6.13.0-develop", + "version": "6.13.0-rc.0", "devDependencies": { "@rocket.chat/eslint-config": "workspace:~", "@types/jest": "~29.5.13", diff --git a/packages/ui-avatar/CHANGELOG.md b/packages/ui-avatar/CHANGELOG.md index a9fa21cf7195..cfb5f9e17d20 100644 --- a/packages/ui-avatar/CHANGELOG.md +++ b/packages/ui-avatar/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/ui-avatar +## 7.0.0-rc.0 + +### Minor Changes + +- ([#32821](https://github.com/RocketChat/Rocket.Chat/pull/32821)) Replaced new `SidebarV2` components under feature preview + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/ui-contexts@11.0.0-rc.0 +
        + ## 6.0.0 ### Patch Changes diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index 0423b68f2b43..a32a7a8be6bf 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-avatar", - "version": "6.0.0", + "version": "7.0.0-rc.0", "private": true, "devDependencies": { "@babel/core": "~7.22.20", @@ -30,7 +30,7 @@ ], "peerDependencies": { "@rocket.chat/fuselage": "*", - "@rocket.chat/ui-contexts": "*", + "@rocket.chat/ui-contexts": "11.0.0-rc.0", "react": "~17.0.2" }, "volta": { diff --git a/packages/ui-client/CHANGELOG.md b/packages/ui-client/CHANGELOG.md index 1d172fe1054f..5471cf97ea1b 100644 --- a/packages/ui-client/CHANGELOG.md +++ b/packages/ui-client/CHANGELOG.md @@ -1,5 +1,23 @@ # @rocket.chat/ui-client +## 11.0.0-rc.0 + +### Minor Changes + +- ([#33156](https://github.com/RocketChat/Rocket.Chat/pull/33156)) added `sidepanelNavigation` to feature preview list + +- ([#32821](https://github.com/RocketChat/Rocket.Chat/pull/32821)) Replaced new `SidebarV2` components under feature preview + +- ([#33225](https://github.com/RocketChat/Rocket.Chat/pull/33225)) Implemented new feature preview for Sidepanel + +### Patch Changes + +-
        Updated dependencies [cd0d50016e]: + + - @rocket.chat/ui-avatar@7.0.0-rc.0 + - @rocket.chat/ui-contexts@11.0.0-rc.0 +
        + ## 10.0.0 ### Patch Changes diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index a84317f37abc..82705ba026cb 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-client", - "version": "10.0.0", + "version": "11.0.0-rc.0", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", @@ -61,8 +61,8 @@ "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", - "@rocket.chat/ui-avatar": "*", - "@rocket.chat/ui-contexts": "*", + "@rocket.chat/ui-avatar": "7.0.0-rc.0", + "@rocket.chat/ui-contexts": "11.0.0-rc.0", "react": "*", "react-i18next": "*" }, diff --git a/packages/ui-composer/CHANGELOG.md b/packages/ui-composer/CHANGELOG.md index 757dee9a2549..20d5d6ac715d 100644 --- a/packages/ui-composer/CHANGELOG.md +++ b/packages/ui-composer/CHANGELOG.md @@ -1,5 +1,11 @@ # @rocket.chat/ui-composer +## 0.3.0-rc.0 + +### Minor Changes + +- ([#32821](https://github.com/RocketChat/Rocket.Chat/pull/32821)) Replaced new `SidebarV2` components under feature preview + ## 0.2.1 ### Patch Changes diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index 0f97fef22508..363299698bfa 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-composer", - "version": "0.2.1", + "version": "0.3.0-rc.0", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/ui-contexts/CHANGELOG.md b/packages/ui-contexts/CHANGELOG.md index aa0f00bc0e83..21aeff09a896 100644 --- a/packages/ui-contexts/CHANGELOG.md +++ b/packages/ui-contexts/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/ui-contexts +## 11.0.0-rc.0 + +### Patch Changes + +-
        Updated dependencies [bb94c9c67a, 9a38c8e13f, 7c14fd1a80, 9eaefdc892, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 0f21fa01a3, 12d6307998]: + + - @rocket.chat/i18n@0.8.0-rc.0 + - @rocket.chat/rest-typings@6.13.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/ddp-client@0.3.7-rc.0 +
        + ## 10.0.0 ### Patch Changes diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 3ef588432a7c..37f83e96a2aa 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-contexts", - "version": "10.0.0", + "version": "11.0.0-rc.0", "private": true, "devDependencies": { "@rocket.chat/core-typings": "workspace:^", diff --git a/packages/ui-video-conf/CHANGELOG.md b/packages/ui-video-conf/CHANGELOG.md index 0ebba8028576..fe47f06728bd 100644 --- a/packages/ui-video-conf/CHANGELOG.md +++ b/packages/ui-video-conf/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/ui-video-conf +## 11.0.0-rc.0 + +### Minor Changes + +- ([#32821](https://github.com/RocketChat/Rocket.Chat/pull/32821)) Replaced new `SidebarV2` components under feature preview + +### Patch Changes + +-
        Updated dependencies [cd0d50016e]: + + - @rocket.chat/ui-avatar@7.0.0-rc.0 + - @rocket.chat/ui-contexts@11.0.0-rc.0 +
        + ## 10.0.0 ### Patch Changes diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index eb363da1ceae..0f1f3e50eab8 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-video-conf", - "version": "10.0.0", + "version": "11.0.0-rc.0", "private": true, "devDependencies": { "@babel/core": "~7.22.20", @@ -39,8 +39,8 @@ "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-avatar": "*", - "@rocket.chat/ui-contexts": "*", + "@rocket.chat/ui-avatar": "7.0.0-rc.0", + "@rocket.chat/ui-contexts": "11.0.0-rc.0", "react": "^17.0.2", "react-dom": "^17.0.2" }, diff --git a/packages/web-ui-registration/CHANGELOG.md b/packages/web-ui-registration/CHANGELOG.md index 601f9aa63494..0a222203fdee 100644 --- a/packages/web-ui-registration/CHANGELOG.md +++ b/packages/web-ui-registration/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/web-ui-registration +## 11.0.0-rc.0 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/ui-contexts@11.0.0-rc.0 +
        + ## 10.0.0 ### Patch Changes diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index ae08e05ee218..23fd80c89842 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/web-ui-registration", - "version": "10.0.0", + "version": "11.0.0-rc.0", "private": true, "homepage": "https://rocket.chat", "main": "./dist/index.js", @@ -47,7 +47,7 @@ "peerDependencies": { "@rocket.chat/layout": "*", "@rocket.chat/tools": "0.2.2", - "@rocket.chat/ui-contexts": "*", + "@rocket.chat/ui-contexts": "11.0.0-rc.0", "@tanstack/react-query": "*", "react": "*", "react-hook-form": "*", diff --git a/yarn.lock b/yarn.lock index 6b1123701996..c0fb2733310e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10493,6 +10493,7 @@ __metadata: "@codemirror/lang-json": ^6.0.1 "@codemirror/tooltip": ^0.19.16 "@lezer/highlight": ^1.1.6 + "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": ~0.31.25 "@rocket.chat/fuselage": ^0.59.0 "@rocket.chat/fuselage-hooks": ^0.33.1 From 65d2a453f0f0a5236d710ae0ef0587e6aabcb19d Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Fri, 20 Sep 2024 10:56:48 -0300 Subject: [PATCH 26/39] chore: update E2EE setting text (#33226) --- packages/i18n/src/locales/en.i18n.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 8ce6bea2e117..9f37642263da 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1788,8 +1788,8 @@ "Duplicated_Email_address_will_be_ignored": "Duplicated email address will be ignored.", "Markdown_Marked_Tables": "Enable Marked Tables", "duplicated-account": "Duplicated account", - "E2E_Allow_Unencrypted_Messages": "Unencrypted messages in encrypted rooms", - "E2E_Allow_Unencrypted_Messages_Description": "Allow plain text messages to be sent in encrypted rooms. These messages will not be encrypted.", + "E2E_Allow_Unencrypted_Messages": "Access unencrypted content in encrypted rooms", + "E2E_Allow_Unencrypted_Messages_Description": "Allow access to encrypted rooms to people without room encryption keys. They'll be able to see and send unencrypted messages.", "E2E Encryption": "E2E Encryption", "E2E_Encryption_enabled_for_room": "End-to-end encryption enabled for #{{roomName}}", "E2E_Encryption_disabled_for_room": "End-to-end encryption disabled for #{{roomName}}", From 9bcb802fdcd2a458ae8f8cd69d94173b4f40861d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= <43561537+rique223@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:58:34 -0300 Subject: [PATCH 27/39] feat: Implement proper accessbility for report user modal (#33294) Co-authored-by: Tasso Evangelista <2263066+tassoevan@users.noreply.github.com> --- .changeset/late-hats-carry.md | 6 ++++ .../UserInfo/ReportUserModal.tsx | 31 +++++++++++-------- packages/i18n/src/locales/en.i18n.json | 2 ++ 3 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 .changeset/late-hats-carry.md diff --git a/.changeset/late-hats-carry.md b/.changeset/late-hats-carry.md new file mode 100644 index 000000000000..ec24c7cd5376 --- /dev/null +++ b/.changeset/late-hats-carry.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Improves the accessibility of the report user modal by adding an appropriate label, description, and ARIA attributes. diff --git a/apps/meteor/client/views/room/contextualBar/UserInfo/ReportUserModal.tsx b/apps/meteor/client/views/room/contextualBar/UserInfo/ReportUserModal.tsx index 5f94f7c407b0..86b4571d88d1 100644 --- a/apps/meteor/client/views/room/contextualBar/UserInfo/ReportUserModal.tsx +++ b/apps/meteor/client/views/room/contextualBar/UserInfo/ReportUserModal.tsx @@ -1,4 +1,4 @@ -import { Box, FieldGroup, Field, FieldLabel, FieldRow, FieldError, TextAreaInput } from '@rocket.chat/fuselage'; +import { Box, FieldGroup, Field, FieldLabel, FieldRow, FieldError, TextAreaInput, FieldDescription } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { UserAvatar } from '@rocket.chat/ui-avatar'; import type { ComponentProps } from 'react'; @@ -45,27 +45,32 @@ const ReportUserModal = ({ username, displayName, onConfirm, onClose }: ReportUs onCancel={onClose} confirmText={t('Report')} > + + + + {displayName} + + - - - - - {displayName} - - - + {t('Report_reason')} + {t('Let_moderators_know_what_the_issue_is')} - {errors.reasonForReport && {errors.reasonForReport.message}} + {errors.reasonForReport && ( + + {errors.reasonForReport.message} + + )} diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 9f37642263da..0e99c1bdc1d8 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -3199,6 +3199,7 @@ "leave-p": "Leave Private Groups", "leave-p_description": "Permission to leave private groups", "Lets_get_you_new_one_": "Let's get you a new one!", + "Let_moderators_know_what_the_issue_is": "Let moderators know what the issue is", "Let_them_know": "Let them know", "Left": "Left", "License": "License", @@ -4490,6 +4491,7 @@ "Report_exclamation_mark": "Report!", "Report_has_been_sent": "Report has been sent", "Report_Number": "Report Number", + "Report_reason": "Report reason", "Report_this_message_question_mark": "Report this message?", "Report_User": "Report user", "Reporting": "Reporting", From 827850d545043896722b53d2ed81af3e4d0738b7 Mon Sep 17 00:00:00 2001 From: "Julio A." <52619625+julio-cfa@users.noreply.github.com> Date: Mon, 23 Sep 2024 21:36:58 +0200 Subject: [PATCH 28/39] fix: imported fixes (#33330) --- .changeset/little-bottles-peel.md | 5 +++ apps/meteor/app/api/server/v1/rooms.ts | 9 +++- .../lib/server/methods/cleanRoomHistory.ts | 8 ++++ .../content/urlPreviews/OEmbedHtmlPreview.tsx | 11 ++++- apps/meteor/tests/end-to-end/api/methods.ts | 43 ++++++++++++++++++- apps/meteor/tests/end-to-end/api/rooms.ts | 28 ++++++++++++ 6 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 .changeset/little-bottles-peel.md diff --git a/.changeset/little-bottles-peel.md b/.changeset/little-bottles-peel.md new file mode 100644 index 000000000000..eacb88108a0f --- /dev/null +++ b/.changeset/little-bottles-peel.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 3dc62e462ddf..117ae3851c43 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -40,7 +40,7 @@ import { findRoomsAvailableForTeams, } from '../lib/rooms'; -async function findRoomByIdOrName({ +export async function findRoomByIdOrName({ params, checkedArchived = true, }: { @@ -365,7 +365,12 @@ API.v1.addRoute( { authRequired: true, validateParams: isRoomsCleanHistoryProps }, { async post() { - const { _id } = await findRoomByIdOrName({ params: this.bodyParams }); + const room = await findRoomByIdOrName({ params: this.bodyParams }); + const { _id } = room; + + if (!room || !(await canAccessRoomAsync(room, { _id: this.userId }))) { + return API.v1.failure('User does not have access to the room [error-not-allowed]', 'error-not-allowed'); + } const { latest, diff --git a/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts b/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts index d6136eee9131..c804128d27bd 100644 --- a/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts +++ b/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts @@ -2,6 +2,8 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { findRoomByIdOrName } from '../../../api/server/v1/rooms'; +import { canAccessRoomAsync } from '../../../authorization/server'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { cleanRoomHistory } from '../functions/cleanRoomHistory'; @@ -56,6 +58,12 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'cleanRoomHistory' }); } + const room = await findRoomByIdOrName({ params: { roomId } }); + + if (!room || !(await canAccessRoomAsync(room, { _id: userId }))) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'cleanRoomHistory' }); + } + return cleanRoomHistory({ rid: roomId, latest, diff --git a/apps/meteor/client/components/message/content/urlPreviews/OEmbedHtmlPreview.tsx b/apps/meteor/client/components/message/content/urlPreviews/OEmbedHtmlPreview.tsx index e8dd4e1ddcc2..518f1ebad203 100644 --- a/apps/meteor/client/components/message/content/urlPreviews/OEmbedHtmlPreview.tsx +++ b/apps/meteor/client/components/message/content/urlPreviews/OEmbedHtmlPreview.tsx @@ -1,12 +1,21 @@ import { Box } from '@rocket.chat/fuselage'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import React from 'react'; import OEmbedCollapsible from './OEmbedCollapsible'; import type { OEmbedPreviewMetadata } from './OEmbedPreviewMetadata'; +const purifyOptions = { + ADD_TAGS: ['iframe'], + ADD_ATTR: ['frameborder', 'allow', 'allowfullscreen', 'scrolling', 'src', 'style', 'referrerpolicy'], + ALLOW_UNKNOWN_PROTOCOLS: true, +}; + const OEmbedHtmlPreview = ({ html, ...props }: OEmbedPreviewMetadata): ReactElement => ( - {html && } + + {html && } + ); export default OEmbedHtmlPreview; diff --git a/apps/meteor/tests/end-to-end/api/methods.ts b/apps/meteor/tests/end-to-end/api/methods.ts index 08945994e438..e3c42389e506 100644 --- a/apps/meteor/tests/end-to-end/api/methods.ts +++ b/apps/meteor/tests/end-to-end/api/methods.ts @@ -616,9 +616,19 @@ describe('Meteor.methods', () => { describe('[@cleanRoomHistory]', () => { let rid: IRoom['_id']; - + let testUser: IUser; + let testUserCredentials: Credentials; let channelName: string; + before('update permissions', async () => { + await updatePermission('clean-channel-history', ['admin', 'user']); + }); + + before('create test user', async () => { + testUser = await createUser(); + testUserCredentials = await login(testUser.username, password); + }); + before('create room', (done) => { channelName = `methods-test-channel-${Date.now()}`; void request @@ -676,7 +686,36 @@ describe('Meteor.methods', () => { .end(done); }); - after(() => deleteRoom({ type: 'p', roomId: rid })); + after(() => + Promise.all([deleteRoom({ type: 'p', roomId: rid }), deleteUser(testUser), updatePermission('clean-channel-history', ['admin'])]), + ); + + it('should throw an error if user is not part of the room', async () => { + await request + .post(methodCall('cleanRoomHistory')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'cleanRoomHistory', + params: [ + { + roomId: rid, + oldest: { $date: new Date().getTime() }, + latest: { $date: new Date().getTime() }, + }, + ], + id: 'id', + msg: 'method', + }), + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error').that.is.an('object'); + expect(data.error).to.have.a.property('error', 'error-not-allowed'); + }); + }); it('should not change the _updatedAt value when nothing is changed on the room', async () => { const roomBefore = await request.get(api('groups.info')).set(credentials).query({ diff --git a/apps/meteor/tests/end-to-end/api/rooms.ts b/apps/meteor/tests/end-to-end/api/rooms.ts index 15f85964ffff..5047af7956d8 100644 --- a/apps/meteor/tests/end-to-end/api/rooms.ts +++ b/apps/meteor/tests/end-to-end/api/rooms.ts @@ -1133,6 +1133,34 @@ describe('[Rooms]', () => { }) .end(done); }); + describe('test user is not part of room', async () => { + beforeEach(async () => { + await updatePermission('clean-channel-history', ['admin', 'user']); + }); + + afterEach(async () => { + await updatePermission('clean-channel-history', ['admin']); + }); + + it('should return an error when the user with right privileges is not part of the room', async () => { + await request + .post(api('rooms.cleanHistory')) + .set(userCredentials) + .send({ + roomId: privateChannel._id, + latest: '9999-12-31T23:59:59.000Z', + oldest: '0001-01-01T00:00:00.000Z', + limit: 2000, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-not-allowed'); + expect(res.body).to.have.property('error', 'User does not have access to the room [error-not-allowed]'); + }); + }); + }); }); describe('[/rooms.info]', () => { let testChannel: IRoom; From a6b91525a8c28a640a6e378a912abc06c4ef6bae Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Mon, 23 Sep 2024 19:00:02 -0300 Subject: [PATCH 29/39] chore: create network broker package (#33338) --- _templates/service/new/package.json.ejs.t | 1 + _templates/service/new/service.ejs.t | 4 +- .../ee/server/services/ecdh-proxy/service.ts | 3 +- apps/meteor/ee/server/services/package.json | 1 + apps/meteor/ee/server/startup/index.ts | 2 +- apps/meteor/package.json | 1 + ee/apps/account-service/Dockerfile | 3 ++ ee/apps/account-service/package.json | 1 + ee/apps/account-service/src/service.ts | 2 +- ee/apps/authorization-service/Dockerfile | 3 ++ ee/apps/authorization-service/package.json | 1 + ee/apps/authorization-service/src/service.ts | 2 +- ee/apps/ddp-streamer/Dockerfile | 3 ++ ee/apps/ddp-streamer/package.json | 1 + ee/apps/ddp-streamer/src/service.ts | 2 +- ee/apps/omnichannel-transcript/Dockerfile | 3 ++ ee/apps/omnichannel-transcript/package.json | 1 + ee/apps/omnichannel-transcript/src/service.ts | 2 +- ee/apps/presence-service/Dockerfile | 3 ++ ee/apps/presence-service/package.json | 1 + ee/apps/presence-service/src/service.ts | 2 +- ee/apps/queue-worker/Dockerfile | 3 ++ ee/apps/queue-worker/package.json | 1 + ee/apps/queue-worker/src/service.ts | 2 +- ee/apps/stream-hub-service/Dockerfile | 3 ++ ee/apps/stream-hub-service/package.json | 1 + ee/apps/stream-hub-service/src/service.ts | 2 +- ee/packages/network-broker/.eslintrc.json | 4 ++ ee/packages/network-broker/jest.config.ts | 6 +++ ee/packages/network-broker/package.json | 39 +++++++++++++++++++ .../network-broker/src}/EnterpriseCheck.ts | 0 .../network-broker/src/NetworkBroker.test.ts | 4 +- .../network-broker/src}/NetworkBroker.ts | 16 +++++--- .../packages/network-broker/src/index.ts | 2 +- ee/packages/network-broker/tsconfig.json | 9 +++++ yarn.lock | 31 +++++++++++++++ 36 files changed, 145 insertions(+), 20 deletions(-) create mode 100644 ee/packages/network-broker/.eslintrc.json create mode 100644 ee/packages/network-broker/jest.config.ts create mode 100644 ee/packages/network-broker/package.json rename {apps/meteor/ee/server/lib => ee/packages/network-broker/src}/EnterpriseCheck.ts (100%) rename apps/meteor/ee/tests/unit/server/NetworkBroker.tests.ts => ee/packages/network-broker/src/NetworkBroker.test.ts (85%) rename {apps/meteor/ee/server => ee/packages/network-broker/src}/NetworkBroker.ts (94%) rename apps/meteor/ee/server/startup/broker.ts => ee/packages/network-broker/src/index.ts (98%) create mode 100644 ee/packages/network-broker/tsconfig.json diff --git a/_templates/service/new/package.json.ejs.t b/_templates/service/new/package.json.ejs.t index 2c74278d1ced..0aa1bd69e995 100644 --- a/_templates/service/new/package.json.ejs.t +++ b/_templates/service/new/package.json.ejs.t @@ -20,6 +20,7 @@ to: ee/apps/<%= name %>/package.json "dependencies": { "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/emitter": "next", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", diff --git a/_templates/service/new/service.ejs.t b/_templates/service/new/service.ejs.t index 54080d94cf08..77f02d2b7769 100644 --- a/_templates/service/new/service.ejs.t +++ b/_templates/service/new/service.ejs.t @@ -1,11 +1,11 @@ --- to: ee/apps/<%= name %>/src/service.ts --- +import { api } from '@rocket.chat/core-services'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; -import { api } from '@rocket.chat/core-services'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; diff --git a/apps/meteor/ee/server/services/ecdh-proxy/service.ts b/apps/meteor/ee/server/services/ecdh-proxy/service.ts index 7ef3e8d26dcc..a795f157c9a5 100755 --- a/apps/meteor/ee/server/services/ecdh-proxy/service.ts +++ b/apps/meteor/ee/server/services/ecdh-proxy/service.ts @@ -1,5 +1,4 @@ -import '../../startup/broker'; - +import '@rocket.chat/network-broker'; import { api } from '@rocket.chat/core-services'; import { ECDHProxy } from './ECDHProxy'; diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 43659382eb67..a2adb5872ad2 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -25,6 +25,7 @@ "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@rocket.chat/ui-kit": "workspace:~", diff --git a/apps/meteor/ee/server/startup/index.ts b/apps/meteor/ee/server/startup/index.ts index a8091f0e9a37..eb09ca6ed30f 100644 --- a/apps/meteor/ee/server/startup/index.ts +++ b/apps/meteor/ee/server/startup/index.ts @@ -13,7 +13,7 @@ import { isRunningMs } from '../../../server/lib/isRunningMs'; export const registerEEBroker = async (): Promise => { // only starts network broker if running in micro services mode if (isRunningMs()) { - const { broker } = await import('./broker'); + const { broker } = await import('@rocket.chat/network-broker'); api.setBroker(broker); void api.start(); diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 9ee7e48d1794..c7da95059475 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -262,6 +262,7 @@ "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/mp3-encoder": "0.24.0", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/omnichannel-services": "workspace:^", "@rocket.chat/onboarding-ui": "~0.33.3", "@rocket.chat/password-policies": "workspace:^", diff --git a/ee/apps/account-service/Dockerfile b/ee/apps/account-service/Dockerfile index acbc5b0371d2..c80d4f2eb376 100644 --- a/ee/apps/account-service/Dockerfile +++ b/ee/apps/account-service/Dockerfile @@ -37,6 +37,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index b9e45ed14eda..6f92b8eb9078 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -20,6 +20,7 @@ "@rocket.chat/emitter": "~0.31.25", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@rocket.chat/tools": "workspace:^", diff --git a/ee/apps/account-service/src/service.ts b/ee/apps/account-service/src/service.ts index f166233ca137..07ca30ed748f 100755 --- a/ee/apps/account-service/src/service.ts +++ b/ee/apps/account-service/src/service.ts @@ -1,10 +1,10 @@ import { api } from '@rocket.chat/core-services'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; const PORT = process.env.PORT || 3033; diff --git a/ee/apps/authorization-service/Dockerfile b/ee/apps/authorization-service/Dockerfile index c662d8765300..9a9e8ded922c 100644 --- a/ee/apps/authorization-service/Dockerfile +++ b/ee/apps/authorization-service/Dockerfile @@ -37,6 +37,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index b7912b8bc94d..262fb6789f9d 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -20,6 +20,7 @@ "@rocket.chat/emitter": "~0.31.25", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@types/node": "^14.18.63", diff --git a/ee/apps/authorization-service/src/service.ts b/ee/apps/authorization-service/src/service.ts index 29162b636229..4dcd466afa60 100755 --- a/ee/apps/authorization-service/src/service.ts +++ b/ee/apps/authorization-service/src/service.ts @@ -1,10 +1,10 @@ import { api } from '@rocket.chat/core-services'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; const PORT = process.env.PORT || 3034; diff --git a/ee/apps/ddp-streamer/Dockerfile b/ee/apps/ddp-streamer/Dockerfile index f556cbde6752..32103dc3528b 100644 --- a/ee/apps/ddp-streamer/Dockerfile +++ b/ee/apps/ddp-streamer/Dockerfile @@ -40,6 +40,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 87029e4b8993..84859add8d7b 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -23,6 +23,7 @@ "@rocket.chat/logger": "workspace:^", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "colorette": "^1.4.0", diff --git a/ee/apps/ddp-streamer/src/service.ts b/ee/apps/ddp-streamer/src/service.ts index b5cd20f7ec02..07666a265dbe 100755 --- a/ee/apps/ddp-streamer/src/service.ts +++ b/ee/apps/ddp-streamer/src/service.ts @@ -1,9 +1,9 @@ import { api } from '@rocket.chat/core-services'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; (async () => { const db = await getConnection(); diff --git a/ee/apps/omnichannel-transcript/Dockerfile b/ee/apps/omnichannel-transcript/Dockerfile index 6a93a8e5e8be..9b7e47968e68 100644 --- a/ee/apps/omnichannel-transcript/Dockerfile +++ b/ee/apps/omnichannel-transcript/Dockerfile @@ -37,6 +37,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 27290070b653..ced114bfb589 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -22,6 +22,7 @@ "@rocket.chat/logger": "workspace:^", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/omnichannel-services": "workspace:^", "@rocket.chat/pdf-worker": "workspace:^", "@rocket.chat/tools": "workspace:^", diff --git a/ee/apps/omnichannel-transcript/src/service.ts b/ee/apps/omnichannel-transcript/src/service.ts index 14cbc5b8438a..66456456fb74 100644 --- a/ee/apps/omnichannel-transcript/src/service.ts +++ b/ee/apps/omnichannel-transcript/src/service.ts @@ -1,11 +1,11 @@ import { api } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; const PORT = process.env.PORT || 3036; diff --git a/ee/apps/presence-service/Dockerfile b/ee/apps/presence-service/Dockerfile index aa9c1c0bd6c9..430880d29606 100644 --- a/ee/apps/presence-service/Dockerfile +++ b/ee/apps/presence-service/Dockerfile @@ -40,6 +40,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index 5968631341b0..d8e47cc5c5ea 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -20,6 +20,7 @@ "@rocket.chat/emitter": "~0.31.25", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/presence": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@types/node": "^14.18.63", diff --git a/ee/apps/presence-service/src/service.ts b/ee/apps/presence-service/src/service.ts index b7275a29106f..0e1c97f2daa2 100755 --- a/ee/apps/presence-service/src/service.ts +++ b/ee/apps/presence-service/src/service.ts @@ -1,10 +1,10 @@ import { api } from '@rocket.chat/core-services'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; const PORT = process.env.PORT || 3031; diff --git a/ee/apps/queue-worker/Dockerfile b/ee/apps/queue-worker/Dockerfile index 6a93a8e5e8be..9b7e47968e68 100644 --- a/ee/apps/queue-worker/Dockerfile +++ b/ee/apps/queue-worker/Dockerfile @@ -37,6 +37,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index b3d8b0aff94e..4270818a6eb3 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -22,6 +22,7 @@ "@rocket.chat/logger": "workspace:^", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/omnichannel-services": "workspace:^", "@types/node": "^14.18.63", "ejson": "^2.2.3", diff --git a/ee/apps/queue-worker/src/service.ts b/ee/apps/queue-worker/src/service.ts index 8583257a6860..4bc6c9642913 100644 --- a/ee/apps/queue-worker/src/service.ts +++ b/ee/apps/queue-worker/src/service.ts @@ -1,11 +1,11 @@ import { api } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; const PORT = process.env.PORT || 3038; diff --git a/ee/apps/stream-hub-service/Dockerfile b/ee/apps/stream-hub-service/Dockerfile index c662d8765300..9a9e8ded922c 100644 --- a/ee/apps/stream-hub-service/Dockerfile +++ b/ee/apps/stream-hub-service/Dockerfile @@ -37,6 +37,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index 9fd5cf0586f6..66443ebeec68 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -21,6 +21,7 @@ "@rocket.chat/logger": "workspace:^", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@types/node": "^14.18.63", "ejson": "^2.2.3", diff --git a/ee/apps/stream-hub-service/src/service.ts b/ee/apps/stream-hub-service/src/service.ts index 4975b5b306be..eade703321d2 100755 --- a/ee/apps/stream-hub-service/src/service.ts +++ b/ee/apps/stream-hub-service/src/service.ts @@ -1,11 +1,11 @@ import { api } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; import { DatabaseWatcher } from '../../../../apps/meteor/server/database/DatabaseWatcher'; import { StreamHub } from './StreamHub'; diff --git a/ee/packages/network-broker/.eslintrc.json b/ee/packages/network-broker/.eslintrc.json new file mode 100644 index 000000000000..a83aeda48e66 --- /dev/null +++ b/ee/packages/network-broker/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/ee/packages/network-broker/jest.config.ts b/ee/packages/network-broker/jest.config.ts new file mode 100644 index 000000000000..c18c8ae02465 --- /dev/null +++ b/ee/packages/network-broker/jest.config.ts @@ -0,0 +1,6 @@ +import server from '@rocket.chat/jest-presets/server'; +import type { Config } from 'jest'; + +export default { + preset: server.preset, +} satisfies Config; diff --git a/ee/packages/network-broker/package.json b/ee/packages/network-broker/package.json new file mode 100644 index 000000000000..c4d3ea1b284b --- /dev/null +++ b/ee/packages/network-broker/package.json @@ -0,0 +1,39 @@ +{ + "name": "@rocket.chat/network-broker", + "version": "0.1.0", + "private": true, + "devDependencies": { + "@rocket.chat/eslint-config": "workspace:^", + "@types/chai": "~4.3.19", + "@types/ejson": "^2.2.2", + "@types/node": "^14.18.63", + "@types/sinon": "^10.0.20", + "chai": "^4.3.10", + "eslint": "~8.45.0", + "jest": "~29.7.0", + "sinon": "^14.0.2", + "typescript": "~5.5.4" + }, + "scripts": { + "lint": "eslint src", + "lint:fix": "eslint src --fix", + "test": "jest", + "build": "tsc", + "testunit": "jest", + "typecheck": "tsc --noEmit --skipLibCheck" + }, + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "files": [ + "/dist" + ], + "volta": { + "extends": "../../../package.json" + }, + "dependencies": { + "@rocket.chat/core-services": "workspace:^", + "ejson": "^2.2.3", + "moleculer": "^0.14.34", + "pino": "^8.15.0" + } +} diff --git a/apps/meteor/ee/server/lib/EnterpriseCheck.ts b/ee/packages/network-broker/src/EnterpriseCheck.ts similarity index 100% rename from apps/meteor/ee/server/lib/EnterpriseCheck.ts rename to ee/packages/network-broker/src/EnterpriseCheck.ts diff --git a/apps/meteor/ee/tests/unit/server/NetworkBroker.tests.ts b/ee/packages/network-broker/src/NetworkBroker.test.ts similarity index 85% rename from apps/meteor/ee/tests/unit/server/NetworkBroker.tests.ts rename to ee/packages/network-broker/src/NetworkBroker.test.ts index 1aac9c33fc33..c79fb12e7049 100644 --- a/apps/meteor/ee/tests/unit/server/NetworkBroker.tests.ts +++ b/ee/packages/network-broker/src/NetworkBroker.test.ts @@ -2,8 +2,8 @@ import { ServiceClass } from '@rocket.chat/core-services'; import { expect } from 'chai'; import sinon from 'sinon'; -import { BrokerMocked } from '../../../../tests/mocks/server/BrokerMocked'; -import { NetworkBroker } from '../../../server/NetworkBroker'; +import { BrokerMocked } from '../../../../apps/meteor/tests/mocks/server/BrokerMocked'; +import { NetworkBroker } from './NetworkBroker'; class DelayedStopBroker extends BrokerMocked { async destroyService(name: string) { diff --git a/apps/meteor/ee/server/NetworkBroker.ts b/ee/packages/network-broker/src/NetworkBroker.ts similarity index 94% rename from apps/meteor/ee/server/NetworkBroker.ts rename to ee/packages/network-broker/src/NetworkBroker.ts index 0fed6fca542d..e326357cba0a 100644 --- a/apps/meteor/ee/server/NetworkBroker.ts +++ b/ee/packages/network-broker/src/NetworkBroker.ts @@ -2,7 +2,7 @@ import { asyncLocalStorage } from '@rocket.chat/core-services'; import type { IBroker, IBrokerNode, IServiceMetrics, IServiceClass, EventSignatures } from '@rocket.chat/core-services'; import type { ServiceBroker, Context, ServiceSchema } from 'moleculer'; -import { EnterpriseCheck } from './lib/EnterpriseCheck'; +import { EnterpriseCheck } from './EnterpriseCheck'; const events: { [k: string]: string } = { onNodeConnected: '$node.connected', @@ -25,7 +25,7 @@ const waitForServicesTimeout = parseInt(WAIT_FOR_SERVICES_TIMEOUT, 10) || 10000; export class NetworkBroker implements IBroker { private broker: ServiceBroker; - private started: Promise; + private started: Promise = Promise.resolve(false); metrics: IServiceMetrics; @@ -36,7 +36,9 @@ export class NetworkBroker implements IBroker { } async call(method: string, data: any): Promise { - await this.started; + if (!(await this.started)) { + return; + } const context = asyncLocalStorage.getStore(); @@ -54,7 +56,9 @@ export class NetworkBroker implements IBroker { } async waitAndCall(method: string, data: any): Promise { - await this.started; + if (!(await this.started)) { + return; + } try { await this.broker.waitForServices(method.split('.')[0], waitForServicesTimeout); @@ -182,6 +186,8 @@ export class NetworkBroker implements IBroker { } async start(): Promise { - this.started = this.broker.start(); + await this.broker.start(); + + this.started = Promise.resolve(true); } } diff --git a/apps/meteor/ee/server/startup/broker.ts b/ee/packages/network-broker/src/index.ts similarity index 98% rename from apps/meteor/ee/server/startup/broker.ts rename to ee/packages/network-broker/src/index.ts index daae4ace4e05..caa12890b514 100644 --- a/apps/meteor/ee/server/startup/broker.ts +++ b/ee/packages/network-broker/src/index.ts @@ -3,7 +3,7 @@ import EJSON from 'ejson'; import { Errors, Serializers, ServiceBroker } from 'moleculer'; import { pino } from 'pino'; -import { NetworkBroker } from '../NetworkBroker'; +import { NetworkBroker } from './NetworkBroker'; const { MS_NAMESPACE = '', diff --git a/ee/packages/network-broker/tsconfig.json b/ee/packages/network-broker/tsconfig.json new file mode 100644 index 000000000000..ada83b80ff89 --- /dev/null +++ b/ee/packages/network-broker/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.server.json", + "compilerOptions": { + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + }, + "files": ["./src/index.ts"] +} diff --git a/yarn.lock b/yarn.lock index 6b1123701996..a7d363694243 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8441,6 +8441,7 @@ __metadata: "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@rocket.chat/tools": "workspace:^" @@ -8556,6 +8557,7 @@ __metadata: "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@types/gc-stats": ^1.4.3 @@ -8719,6 +8721,7 @@ __metadata: "@rocket.chat/logger": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@types/ejson": ^2.2.2 @@ -9396,6 +9399,7 @@ __metadata: "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/mp3-encoder": 0.24.0 + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/omnichannel-services": "workspace:^" "@rocket.chat/onboarding-ui": ~0.33.3 "@rocket.chat/password-policies": "workspace:^" @@ -9771,6 +9775,27 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/network-broker@workspace:^, @rocket.chat/network-broker@workspace:ee/packages/network-broker": + version: 0.0.0-use.local + resolution: "@rocket.chat/network-broker@workspace:ee/packages/network-broker" + dependencies: + "@rocket.chat/core-services": "workspace:^" + "@rocket.chat/eslint-config": "workspace:^" + "@types/chai": ~4.3.19 + "@types/ejson": ^2.2.2 + "@types/node": ^14.18.63 + "@types/sinon": ^10.0.20 + chai: ^4.3.10 + ejson: ^2.2.3 + eslint: ~8.45.0 + jest: ~29.7.0 + moleculer: ^0.14.34 + pino: ^8.15.0 + sinon: ^14.0.2 + typescript: ~5.5.4 + languageName: unknown + linkType: soft + "@rocket.chat/omnichannel-services@workspace:^, @rocket.chat/omnichannel-services@workspace:ee/packages/omnichannel-services": version: 0.0.0-use.local resolution: "@rocket.chat/omnichannel-services@workspace:ee/packages/omnichannel-services" @@ -9817,6 +9842,7 @@ __metadata: "@rocket.chat/logger": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/omnichannel-services": "workspace:^" "@rocket.chat/pdf-worker": "workspace:^" "@rocket.chat/tools": "workspace:^" @@ -9954,6 +9980,7 @@ __metadata: "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/presence": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@types/gc-stats": ^1.4.3 @@ -10019,6 +10046,7 @@ __metadata: "@rocket.chat/logger": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/omnichannel-services": "workspace:^" "@types/gc-stats": ^1.4.3 "@types/node": ^14.18.63 @@ -10176,6 +10204,7 @@ __metadata: "@rocket.chat/logger": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@types/bcrypt": ^5.0.2 @@ -10493,6 +10522,7 @@ __metadata: "@codemirror/lang-json": ^6.0.1 "@codemirror/tooltip": ^0.19.16 "@lezer/highlight": ^1.1.6 + "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": ~0.31.25 "@rocket.chat/fuselage": ^0.59.0 "@rocket.chat/fuselage-hooks": ^0.33.1 @@ -37207,6 +37237,7 @@ __metadata: "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@rocket.chat/ui-kit": "workspace:~" From 1f89f780bd205d36b572689e1103c860f787a13e Mon Sep 17 00:00:00 2001 From: Lucas Pelegrino Date: Tue, 24 Sep 2024 09:54:31 -0300 Subject: [PATCH 30/39] feat: Adds new admin feature preview setting management (#33212) Co-authored-by: Guilherme Gazzo --- .changeset/quick-rings-wave.md | 7 + .../UserMenu/hooks/useAccountItems.tsx | 4 +- .../hooks/useFeaturePreviewEnableQuery.ts | 28 ++++ .../sidebar/header/hooks/useAccountItems.tsx | 4 +- .../AccountFeaturePreviewBadge.tsx | 21 --- .../AccountFeaturePreviewPage.tsx | 44 ++---- .../client/views/account/sidebarItems.tsx | 5 +- .../AdminFeaturePreviewPage.tsx | 127 ++++++++++++++++++ .../AdminFeaturePreviewRoute.tsx | 26 ++++ apps/meteor/client/views/admin/routes.tsx | 9 ++ .../meteor/client/views/admin/sidebarItems.ts | 8 ++ .../featurePreview/enhanced-navigation.png | Bin 0 -> 2372 bytes .../resizable-contextual-bar.png | Bin 0 -> 4776 bytes .../images/featurePreview/timestamp.png | Bin 0 -> 51432 bytes apps/meteor/server/settings/accounts.ts | 5 + .../tests/end-to-end/api/miscellaneous.ts | 1 + packages/i18n/src/locales/en.i18n.json | 16 ++- packages/i18n/src/locales/hi-IN.i18n.json | 6 +- .../FeaturePreview/FeaturePreviewBadge.tsx | 21 +++ .../src/components/FeaturePreview/index.ts | 2 + packages/ui-client/src/components/index.ts | 2 +- .../useDefaultSettingFeaturePreviewList.ts | 12 ++ .../ui-client/src/hooks/useFeaturePreview.ts | 5 +- .../src/hooks/useFeaturePreviewList.ts | 32 +++-- ... usePreferenceFeaturePreviewList.spec.tsx} | 13 +- .../hooks/usePreferenceFeaturePreviewList.ts | 16 +++ packages/ui-client/src/index.ts | 2 + 27 files changed, 324 insertions(+), 92 deletions(-) create mode 100644 .changeset/quick-rings-wave.md create mode 100644 apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts delete mode 100644 apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx create mode 100644 apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx create mode 100644 apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx create mode 100644 apps/meteor/public/images/featurePreview/enhanced-navigation.png create mode 100644 apps/meteor/public/images/featurePreview/resizable-contextual-bar.png create mode 100644 apps/meteor/public/images/featurePreview/timestamp.png create mode 100644 packages/ui-client/src/components/FeaturePreview/FeaturePreviewBadge.tsx create mode 100644 packages/ui-client/src/components/FeaturePreview/index.ts create mode 100644 packages/ui-client/src/hooks/useDefaultSettingFeaturePreviewList.ts rename packages/ui-client/src/hooks/{useFeaturePreviewList.spec.tsx => usePreferenceFeaturePreviewList.spec.tsx} (79%) create mode 100644 packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.ts diff --git a/.changeset/quick-rings-wave.md b/.changeset/quick-rings-wave.md new file mode 100644 index 000000000000..0ea22897ff45 --- /dev/null +++ b/.changeset/quick-rings-wave.md @@ -0,0 +1,7 @@ +--- +"@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/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx index 82c39c5c1b10..e54e2b72d675 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx @@ -1,7 +1,7 @@ import { Badge } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { defaultFeaturesPreview, useFeaturePreviewList } from '@rocket.chat/ui-client'; import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { defaultFeaturesPreview, usePreferenceFeaturePreviewList } from '@rocket.chat/ui-client'; import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -9,7 +9,7 @@ export const useAccountItems = (): GenericMenuItemProps[] => { const t = useTranslation(); const router = useRouter(); - const { unseenFeatures, featurePreviewEnabled } = useFeaturePreviewList(); + const { unseenFeatures, featurePreviewEnabled } = usePreferenceFeaturePreviewList(); const handleMyAccount = useEffectEvent(() => { router.navigate('/account'); diff --git a/apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts b/apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts new file mode 100644 index 000000000000..fd88f0237d29 --- /dev/null +++ b/apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts @@ -0,0 +1,28 @@ +import type { FeaturePreviewProps } from '@rocket.chat/ui-client'; +import { useMemo } from 'react'; + +const handleFeaturePreviewEnableQuery = (item: FeaturePreviewProps, _: any, features: FeaturePreviewProps[]) => { + if (item.enableQuery) { + const expected = item.enableQuery.value; + const received = features.find((el) => el.name === item.enableQuery?.name)?.value; + if (expected !== received) { + item.disabled = true; + item.value = false; + } else { + item.disabled = false; + } + } + return item; +}; + +const groupFeaturePreview = (features: FeaturePreviewProps[]) => + Object.entries( + features.reduce((result, currentValue) => { + (result[currentValue.group] = result[currentValue.group] || []).push(currentValue); + return result; + }, {} as Record), + ); + +export const useFeaturePreviewEnableQuery = (features: FeaturePreviewProps[]) => { + return useMemo(() => groupFeaturePreview(features.map(handleFeaturePreviewEnableQuery)), [features]); +}; diff --git a/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx b/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx index 2be6b2b1dea2..51ab7a198a67 100644 --- a/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx +++ b/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx @@ -1,7 +1,7 @@ import { Badge } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { defaultFeaturesPreview, useFeaturePreviewList } from '@rocket.chat/ui-client'; import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { defaultFeaturesPreview, usePreferenceFeaturePreviewList } from '@rocket.chat/ui-client'; import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -9,7 +9,7 @@ export const useAccountItems = (): GenericMenuItemProps[] => { const t = useTranslation(); const router = useRouter(); - const { unseenFeatures, featurePreviewEnabled } = useFeaturePreviewList(); + const { unseenFeatures, featurePreviewEnabled } = usePreferenceFeaturePreviewList(); const handleMyAccount = useMutableCallback(() => { router.navigate('/account'); diff --git a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx b/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx deleted file mode 100644 index c109ca1aefb5..000000000000 --- a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Badge } from '@rocket.chat/fuselage'; -import { useFeaturePreviewList } from '@rocket.chat/ui-client'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -const AccountFeaturePreviewBadge = () => { - const { t } = useTranslation(); - const { unseenFeatures } = useFeaturePreviewList(); - - if (!unseenFeatures) { - return null; - } - - return ( - - {unseenFeatures} - - ); -}; - -export default AccountFeaturePreviewBadge; diff --git a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx b/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx index dd9ab6a90959..358d2394003b 100644 --- a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx +++ b/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx @@ -1,4 +1,3 @@ -import { css } from '@rocket.chat/css-in-js'; import { ButtonGroup, Button, @@ -13,9 +12,10 @@ import { FieldLabel, FieldRow, FieldHint, + Callout, + Margins, } from '@rocket.chat/fuselage'; -import type { FeaturePreviewProps } from '@rocket.chat/ui-client'; -import { useFeaturePreviewList } from '@rocket.chat/ui-client'; +import { usePreferenceFeaturePreviewList } from '@rocket.chat/ui-client'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useToastMessageDispatch, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import type { ChangeEvent } from 'react'; @@ -23,26 +23,12 @@ import React, { useEffect, Fragment } from 'react'; import { useForm } from 'react-hook-form'; import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; +import { useFeaturePreviewEnableQuery } from '../../../hooks/useFeaturePreviewEnableQuery'; -const handleEnableQuery = (features: FeaturePreviewProps[]) => { - return features.map((item) => { - if (item.enableQuery) { - const expected = item.enableQuery.value; - const received = features.find((el) => el.name === item.enableQuery?.name)?.value; - if (expected !== received) { - item.disabled = true; - item.value = false; - } else { - item.disabled = false; - } - } - return item; - }); -}; const AccountFeaturePreviewPage = () => { const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); - const { features, unseenFeatures } = useFeaturePreviewList(); + const { features, unseenFeatures } = usePreferenceFeaturePreviewList(); const setUserPreferences = useEndpoint('POST', '/v1/users.setPreferences'); @@ -85,12 +71,7 @@ const AccountFeaturePreviewPage = () => { setValue('featuresPreview', updated, { shouldDirty: true }); }; - const grouppedFeaturesPreview = Object.entries( - handleEnableQuery(featuresPreview).reduce((result, currentValue) => { - (result[currentValue.group] = result[currentValue.group] || []).push(currentValue); - return result; - }, {} as Record), - ); + const grouppedFeaturesPreview = useFeaturePreviewEnableQuery(featuresPreview); return ( @@ -105,14 +86,11 @@ const AccountFeaturePreviewPage = () => { )} {featuresPreview.length > 0 && ( <> - - {t('Feature_preview_page_description')} + + + {t('Feature_preview_page_description')} + {t('Feature_preview_page_callout')} + {grouppedFeaturesPreview?.map(([group, features], index) => ( diff --git a/apps/meteor/client/views/account/sidebarItems.tsx b/apps/meteor/client/views/account/sidebarItems.tsx index ca0376be329d..fa2ab8bd5e40 100644 --- a/apps/meteor/client/views/account/sidebarItems.tsx +++ b/apps/meteor/client/views/account/sidebarItems.tsx @@ -1,10 +1,9 @@ -import { defaultFeaturesPreview } from '@rocket.chat/ui-client'; +import { defaultFeaturesPreview, FeaturePreviewBadge } from '@rocket.chat/ui-client'; import React from 'react'; import { hasPermission, hasAtLeastOnePermission } from '../../../app/authorization/client'; import { settings } from '../../../app/settings/client'; import { createSidebarItems } from '../../lib/createSidebarItems'; -import AccountFeaturePreviewBadge from './featurePreview/AccountFeaturePreviewBadge'; export const { registerSidebarItem: registerAccountSidebarItem, @@ -54,7 +53,7 @@ export const { href: '/account/feature-preview', i18nLabel: 'Feature_preview', icon: 'flask', - badge: () => , + badge: () => , permissionGranted: () => settings.get('Accounts_AllowFeaturePreview') && defaultFeaturesPreview?.length > 0, }, { diff --git a/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx new file mode 100644 index 000000000000..615fd20cf5a6 --- /dev/null +++ b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx @@ -0,0 +1,127 @@ +import { + ButtonGroup, + Button, + Box, + ToggleSwitch, + Accordion, + Field, + FieldGroup, + FieldLabel, + FieldRow, + FieldHint, + Callout, + Margins, +} from '@rocket.chat/fuselage'; +import { useDefaultSettingFeaturePreviewList } from '@rocket.chat/ui-client'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useTranslation, useSettingsDispatch } from '@rocket.chat/ui-contexts'; +import type { ChangeEvent } from 'react'; +import React, { Fragment } from 'react'; +import { useForm } from 'react-hook-form'; + +import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; +import { useFeaturePreviewEnableQuery } from '../../../hooks/useFeaturePreviewEnableQuery'; +import { useEditableSetting } from '../EditableSettingsContext'; +import Setting from '../settings/Setting'; +import SettingsGroupPageSkeleton from '../settings/SettingsGroupPage/SettingsGroupPageSkeleton'; + +const AdminFeaturePreviewPage = () => { + const t = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + const allowFeaturePreviewSetting = useEditableSetting('Accounts_AllowFeaturePreview'); + const { features } = useDefaultSettingFeaturePreviewList(); + + const { + watch, + formState: { isDirty }, + setValue, + handleSubmit, + reset, + } = useForm({ + defaultValues: { featuresPreview: features }, + }); + const { featuresPreview } = watch(); + const dispatch = useSettingsDispatch(); + + const handleSave = async () => { + try { + const featuresToBeSaved = featuresPreview.map((feature) => ({ name: feature.name, value: feature.value })); + + await dispatch([ + { _id: allowFeaturePreviewSetting!._id, value: allowFeaturePreviewSetting!.value }, + { _id: 'Accounts_Default_User_Preferences_featuresPreview', value: JSON.stringify(featuresToBeSaved) }, + ]); + dispatchToastMessage({ type: 'success', message: t('Preferences_saved') }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } finally { + reset({ featuresPreview }); + } + }; + + const handleFeatures = (e: ChangeEvent) => { + const updated = featuresPreview.map((item) => (item.name === e.target.name ? { ...item, value: e.target.checked } : item)); + setValue('featuresPreview', updated, { shouldDirty: true }); + }; + + const grouppedFeaturesPreview = useFeaturePreviewEnableQuery(featuresPreview); + + if (!allowFeaturePreviewSetting) { + // TODO: Implement FeaturePreviewSkeleton component + return ; + } + + return ( + + + + + + + {t('Feature_preview_admin_page_description')} + {t('Feature_preview_page_callout')} + {t('Feature_preview_admin_page_callout')} + + + + + {grouppedFeaturesPreview?.map(([group, features], index) => ( + + + {features.map((feature) => ( + + + + {t(feature.i18n)} + + + {feature.description && {t(feature.description)}} + + {feature.imageUrl && } + + ))} + + + ))} + + + + + + + + + + + ); +}; + +export default AdminFeaturePreviewPage; diff --git a/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx new file mode 100644 index 000000000000..a7d6bd77d136 --- /dev/null +++ b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx @@ -0,0 +1,26 @@ +import { usePermission } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React, { memo } from 'react'; + +import SettingsProvider from '../../../providers/SettingsProvider'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; +import EditableSettingsProvider from '../settings/EditableSettingsProvider'; +import AdminFeaturePreviewPage from './AdminFeaturePreviewPage'; + +const AdminFeaturePreviewRoute = (): ReactElement => { + const canViewFeaturesPreview = usePermission('manage-cloud'); + + if (!canViewFeaturesPreview) { + return ; + } + + return ( + + + + + + ); +}; + +export default memo(AdminFeaturePreviewRoute); diff --git a/apps/meteor/client/views/admin/routes.tsx b/apps/meteor/client/views/admin/routes.tsx index f70df1625871..d244d5e2f19b 100644 --- a/apps/meteor/client/views/admin/routes.tsx +++ b/apps/meteor/client/views/admin/routes.tsx @@ -104,6 +104,10 @@ declare module '@rocket.chat/ui-contexts' { pathname: `/admin/subscription`; pattern: '/admin/subscription'; }; + 'admin-feature-preview': { + pathname: '/admin/feature-preview'; + pattern: '/admin/feature-preview'; + }; } } @@ -237,3 +241,8 @@ registerAdminRoute('/subscription', { name: 'subscription', component: lazy(() => import('./subscription/SubscriptionRoute')), }); + +registerAdminRoute('/feature-preview', { + name: 'admin-feature-preview', + component: lazy(() => import('./featurePreview/AdminFeaturePreviewRoute')), +}); diff --git a/apps/meteor/client/views/admin/sidebarItems.ts b/apps/meteor/client/views/admin/sidebarItems.ts index 013206d9e9a8..fc7d307396d4 100644 --- a/apps/meteor/client/views/admin/sidebarItems.ts +++ b/apps/meteor/client/views/admin/sidebarItems.ts @@ -1,3 +1,5 @@ +import { defaultFeaturesPreview } from '@rocket.chat/ui-client'; + import { hasPermission, hasAtLeastOnePermission, hasAllPermission } from '../../../app/authorization/client'; import { createSidebarItems } from '../../lib/createSidebarItems'; @@ -129,6 +131,12 @@ export const { icon: 'emoji', permissionGranted: (): boolean => hasPermission('manage-emoji'), }, + { + href: '/admin/feature-preview', + i18nLabel: 'Feature_preview', + icon: 'flask', + permissionGranted: () => defaultFeaturesPreview?.length > 0, + }, { href: '/admin/settings', i18nLabel: 'Settings', diff --git a/apps/meteor/public/images/featurePreview/enhanced-navigation.png b/apps/meteor/public/images/featurePreview/enhanced-navigation.png new file mode 100644 index 0000000000000000000000000000000000000000..4240326ba985d0a054ae5d9c8521b24c8d93e6f7 GIT binary patch literal 2372 zcmeHIYgCg*8lKz;k(*0VynsLp8%mX01@Ra$34#GB2w8)n2Lz6kXazzALMRv_YP;xG zVrfx?0*kHdhC)CtVh|D##7hAYg%TP_P1$fufB*?0CfSL{AKkOvo<04szxKyBGw<`h z^UgExH}gzEL^#D}&89T~0Gp8DKq>$zF&wv8TEKSeq4Rv$;nISm(*aoTVg3*yRQGuZ zBGRdp08rcKGzASbgB(T%puW)RWda5O%aM>k@?kbYDJ7;mXg7)%7}|_U^u0g_?-vIh z3PLLno>;U^yRy;Ip#U%U`gz?3Q9dbXM|jd~=gzm4M_kLikKsE!!V_rWmyy}L8oj)= zS2fb?Im$rG8QwM!!;_UE8Rx|HP0%OX2>m!4{!$M9@ zmzycKXS|vvx3jAFOuN;#LOx%@xz-bPQB~L4+B$H!?W8hJu;wTX4KMQ=yEwA+byHJQ z4Srntp0Hr<_>yuo%x4&B_O`IRa^*|1XD(Z^^k)!1hMgXbR43&u9)Lcv`iBcipXe%2 zq_vq)1Omaa`(22J@|`C&yL#jc-3GtCI}p2cuqKsjiPzA?P2zh4jxQVm;07eyMCq?+ zYV8fOwUa-ksVNIq0FcPLSd^mM+Od*DToGaEjy4EpBb~T5rBg*r>d@@<%gYepB7JF0 zqq_ZhTTbt4e99^Fl9eP{muC7SQ?As-T7ZUt1HXW7`egSV>XAs%dT2+~9d#BA(P*)j z(D0-Wc6F=Xq;Ez*19MWIbzS0osT7X6ozKk>&N3dh)U{&Re&fo8P%s&w0N~{VcrIKq z2x~}d4RQmV;DFu^gWmrCgjZEc@Ze_b(=Aoh-_d=XJZ8+v)lKA~z%BN~S86rpYMt?0u zFkC3)tSau`mnY25=B{g)dM3owSbf=Vw0-aZz2T~fTHkJ@F6pcLmqLRS&;Cv>BWR!I z+RPN!dq?ck6I|axSa+4|B7lNzG$S`NkbWu8%wj3c6P@vb)pL!5@#dmB^TW{?vN zBGfH=G3iH<==4p-aBGihHuu9{(zqJ-xB51NXv62_%8>>T@awGan@0K^3i((iN+xo%#ouHt&I7j7;gA~ zaM|kYft59-NVWnweEfy~obuR!X;>4?FtKhWk9LQc+ATc4!u|H0t|p=C4#)UkIs1b* zU~l>r;^|xj-Rs_fd+|eK`kzi4=TVAO6y`vwsAMd7!IrRlB4EwT%Irr}B_*-~AjVn(;{-)H)yq<^yYV&jM#%$9>QVh%U z%UJHItG`bwI}ZY}o0&klMYUn~qrAe2#ymoOtW#gr9Dz%HofNmdj`t8&#p7n_pOvuZ z=F?L2tj|bH_{0E*sl4L|c!c+jV` zeXy-WWIHdBNTLXs_UIBMDy?(d=pS%;{K~$(^7C(zfUTgQC0m#>rkYzG!&0srmo+zP z%uIJAf4!wl*sSe43silWOG&I~w3z|7=J<^(E=H1Jo*@0}lR+k4a4{Z+7t=?Z<-^16 z#HH**nNR$)vabV9KteLt5UV>qWh13{~`%a65CIDvwi8xxILa6 z!t*MO@WlMq>6uJgtPOPTCQBxzspEI!dI! CCsuy| literal 0 HcmV?d00001 diff --git a/apps/meteor/public/images/featurePreview/resizable-contextual-bar.png b/apps/meteor/public/images/featurePreview/resizable-contextual-bar.png new file mode 100644 index 0000000000000000000000000000000000000000..c36c7e44a29d8c27c9c035a77026f1cd7e7dabd0 GIT binary patch literal 4776 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 a2K(3rHj$^VY%~U(889)lFep3k9Q`kU29J{f literal 0 HcmV?d00001 diff --git a/apps/meteor/public/images/featurePreview/timestamp.png b/apps/meteor/public/images/featurePreview/timestamp.png new file mode 100644 index 0000000000000000000000000000000000000000..7573f97db55b73e2d880af67e6705e3c4a2040c7 GIT binary patch literal 51432 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

        &c?(AAUaFYNC38B5NhzYmJ_RR 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 p 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 i18nLabel: 'Sidebar_Sections_Order', i18nDescription: 'Sidebar_Sections_Order_Description', }); + + await this.add('Accounts_Default_User_Preferences_featuresPreview', '[]', { + type: 'string', + public: true, + }); }); await this.section('Avatar', async function () { diff --git a/apps/meteor/tests/end-to-end/api/miscellaneous.ts b/apps/meteor/tests/end-to-end/api/miscellaneous.ts index b8341f7c0994..d933f1f3c4b3 100644 --- a/apps/meteor/tests/end-to-end/api/miscellaneous.ts +++ b/apps/meteor/tests/end-to-end/api/miscellaneous.ts @@ -186,6 +186,7 @@ describe('miscellaneous', () => { 'muteFocusedConversations', 'notifyCalendarEvents', 'enableMobileRinging', + 'featuresPreview', ].filter((p) => Boolean(p)); expect(res.body).to.have.property('success', true); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 0e99c1bdc1d8..47d2034e002e 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -85,6 +85,7 @@ "Accounts_AllowEmailChange": "Allow Email Change", "Accounts_AllowEmailNotifications": "Allow Email Notifications", "Accounts_AllowFeaturePreview": "Allow Feature Preview", + "Accounts_AllowFeaturePreview_Description": "Make feature preview available to all workspace members.", "Accounts_AllowPasswordChange": "Allow Password Change", "Accounts_AllowPasswordChangeForOAuthUsers": "Allow Password Change for OAuth Users", "Accounts_AllowRealNameChange": "Allow Name Change", @@ -1125,8 +1126,8 @@ "Common_Access": "Common Access", "Commit": "Commit", "Community": "Community", - "Contextualbar_resizable": "Contextual bar resizable", - "Contextualbar_resizable_description": "Allows you to adjust the size of the contextual bar by simply dragging, giving you instant customization and flexibility", + "Contextualbar_resizable": "Resizable contextual bar", + "Contextualbar_resizable_description": "Adjust the size of the contextual bar by clicking and dragging the edge, giving you instant customization and flexibility.", "Free_Edition": "Free edition", "Composer_not_available_phone_calls": "Messages are not available on phone calls", "Condensed": "Condensed", @@ -1949,8 +1950,8 @@ "Enable_Password_History": "Enable Password History", "Enable_Password_History_Description": "When enabled, users won't be able to update their passwords to some of their most recently used passwords.", "Enable_Svg_Favicon": "Enable SVG favicon", - "Enable_timestamp": "Enable timestamp parsing in messages", - "Enable_timestamp_description": "Enable timestamps to be parsed in messages", + "Enable_timestamp": "Timestamp in messages", + "Enable_timestamp_description": "Render Unix timestamps inside messages in your local (system) timezone.", "Enable_to_bypass_email_verification": "Enable to bypass email verification", "Enable_two-factor_authentication": "Enable two-factor authentication via TOTP", "Enable_two-factor_authentication_email": "Enable two-factor authentication via Email", @@ -2284,7 +2285,10 @@ "Favorite_Rooms": "Enable Favorite Rooms", "Favorites": "Favorites", "Feature_preview": "Feature preview", - "Feature_preview_page_description": "Welcome to the features preview page! Here, you can enable the latest cutting-edge features that are currently under development and not yet officially released.\n\nPlease note that these configurations are still in the testing phase and may not be stable or fully functional.", + "Feature_preview_page_description": "Enable the latest features that are currently under development.", + "Feature_preview_page_callout": "Feature previews are being tested and may not be stable or fully functional. Features may become premium capabilities once officially released.", + "Feature_preview_admin_page_description": "Choose what feature previews to make available to workspace members.", + "Feature_preview_admin_page_callout": "Features enabled here will be enabled to each user in their feature preview preferences.", "featured": "featured", "Featured": "Featured", "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "This feature depends on the above selected call provider to be enabled from the administration settings (Admin -> Video Conference).", @@ -4364,7 +4368,7 @@ "Queue_Time": "Queue Time", "Queue_management": "Queue Management", "Quick_reactions": "Quick reactions", - "Quick_reactions_description": "The three most used reactions get an easy access while your mouse is over the message", + "Quick_reactions_description": "Easily access your most used and most recent emoji message reactions by hovering on a message.", "quote": "quote", "Quote": "Quote", "Random": "Random", diff --git a/packages/i18n/src/locales/hi-IN.i18n.json b/packages/i18n/src/locales/hi-IN.i18n.json index 090e081e83fa..3110d6e82a67 100644 --- a/packages/i18n/src/locales/hi-IN.i18n.json +++ b/packages/i18n/src/locales/hi-IN.i18n.json @@ -2169,7 +2169,6 @@ "Favorite_Rooms": "पसंदीदा कमरे सक्षम करें", "Favorites": "पसंदीदा", "Feature_preview": "फ़ीचर पूर्वावलोकन", - "Feature_preview_page_description": "फीचर पूर्वावलोकन पृष्ठ पर आपका स्वागत है! यहां, आप नवीनतम अत्याधुनिक सुविधाओं को सक्षम कर सकते हैं जो वर्तमान में विकास के अधीन हैं और अभी तक आधिकारिक तौर पर जारी नहीं की गई हैं।\n\nकृपया ध्यान दें कि ये कॉन्फ़िगरेशन अभी भी परीक्षण चरण में हैं और स्थिर या पूरी तरह कार्यात्मक नहीं हो सकते हैं।", "featured": "प्रदर्शित", "Featured": "प्रदर्शित", "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "यह सुविधा प्रशासन सेटिंग्स (एडमिन -> वीडियो कॉन्फ्रेंस) से सक्षम होने के लिए उपरोक्त चयनित कॉल प्रदाता पर निर्भर करती है।", @@ -4128,7 +4127,6 @@ "Queue_Time": "कतार समय", "Queue_management": "कतार प्रबंधन", "Quick_reactions": "त्वरित प्रतिक्रियाएँ", - "Quick_reactions_description": "जब आपका माउस संदेश पर होता है तो सबसे अधिक उपयोग की जाने वाली तीन प्रतिक्रियाओं तक आसान पहुंच मिलती है", "quote": "उद्धरण", "Quote": "उद्धरण", "Random": "Random", @@ -4987,7 +4985,7 @@ "The_application_will_be_able_to": "<1>{{appName}} यह करने में सक्षम होगा:", "The_channel_name_is_required": "चैनल का नाम आवश्यक है", "The_emails_are_being_sent": "ईमेल भेजे जा रहे हैं.", - "The_empty_room__roomName__will_be_removed_automatically": "खाली कमरा {{roomName}} स्वचालित रूप से हटा दिया जाएगा।", + "The_empty_room__roomName__will_be_removed_automatically": "खाली कमरा {{roomName}} स्वचालित रूप से हटा दिया जाएगा।", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "छवि का आकार बदलना काम नहीं करेगा क्योंकि हम आपके सर्वर पर स्थापित ImageMagick या ग्राफ़िक्सMagick का पता नहीं लगा सकते हैं।", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "संदेश एक चर्चा है आप संदेशों को पुनर्प्राप्त नहीं कर पाएंगे!", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "मोबाइल सूचनाएं सभी उपयोगकर्ताओं के लिए अक्षम कर दी गई थीं, पुश गेटवे को फिर से सक्षम करने के लिए \"एडमिन > पुश\" पर जाएं", @@ -6134,4 +6132,4 @@ "Unlimited_seats": "असीमित सीटें", "Unlimited_MACs": "असीमित एमएसी", "Unlimited_seats_MACs": "असीमित सीटें और एमएसी" -} \ No newline at end of file +} diff --git a/packages/ui-client/src/components/FeaturePreview/FeaturePreviewBadge.tsx b/packages/ui-client/src/components/FeaturePreview/FeaturePreviewBadge.tsx new file mode 100644 index 000000000000..eece30cc7280 --- /dev/null +++ b/packages/ui-client/src/components/FeaturePreview/FeaturePreviewBadge.tsx @@ -0,0 +1,21 @@ +import { Badge } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; + +import { usePreferenceFeaturePreviewList } from '../../hooks/usePreferenceFeaturePreviewList'; + +const FeaturePreviewBadge = () => { + const t = useTranslation(); + const { unseenFeatures } = usePreferenceFeaturePreviewList(); + + if (!unseenFeatures) { + return null; + } + + return ( + + {unseenFeatures} + + ); +}; + +export default FeaturePreviewBadge; diff --git a/packages/ui-client/src/components/FeaturePreview/index.ts b/packages/ui-client/src/components/FeaturePreview/index.ts new file mode 100644 index 000000000000..f6b8e5f2071e --- /dev/null +++ b/packages/ui-client/src/components/FeaturePreview/index.ts @@ -0,0 +1,2 @@ +export { FeaturePreview, FeaturePreviewOn, FeaturePreviewOff } from './FeaturePreview'; +export { default as FeaturePreviewBadge } from './FeaturePreviewBadge'; diff --git a/packages/ui-client/src/components/index.ts b/packages/ui-client/src/components/index.ts index 8642983229aa..7308c8e75431 100644 --- a/packages/ui-client/src/components/index.ts +++ b/packages/ui-client/src/components/index.ts @@ -11,7 +11,7 @@ export * as UserStatus from './UserStatus'; export * from './Header'; export * from './HeaderV2'; export * from './MultiSelectCustom/MultiSelectCustom'; -export * from './FeaturePreview/FeaturePreview'; +export * from './FeaturePreview'; export * from './RoomBanner'; export { default as UserAutoComplete } from './UserAutoComplete'; export * from './GenericMenu'; diff --git a/packages/ui-client/src/hooks/useDefaultSettingFeaturePreviewList.ts b/packages/ui-client/src/hooks/useDefaultSettingFeaturePreviewList.ts new file mode 100644 index 000000000000..373862379cc1 --- /dev/null +++ b/packages/ui-client/src/hooks/useDefaultSettingFeaturePreviewList.ts @@ -0,0 +1,12 @@ +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { parseSetting, useFeaturePreviewList } from './useFeaturePreviewList'; + +export const useDefaultSettingFeaturePreviewList = () => { + const featurePreviewSettingJSON = useSetting('Accounts_Default_User_Preferences_featuresPreview'); + + const settingFeaturePreview = useMemo(() => parseSetting(featurePreviewSettingJSON), [featurePreviewSettingJSON]); + + return useFeaturePreviewList(settingFeaturePreview ?? []); +}; diff --git a/packages/ui-client/src/hooks/useFeaturePreview.ts b/packages/ui-client/src/hooks/useFeaturePreview.ts index 4bdda9c9251a..bd46adfdefff 100644 --- a/packages/ui-client/src/hooks/useFeaturePreview.ts +++ b/packages/ui-client/src/hooks/useFeaturePreview.ts @@ -1,7 +1,8 @@ -import { type FeaturesAvailable, useFeaturePreviewList } from './useFeaturePreviewList'; +import { type FeaturesAvailable } from './useFeaturePreviewList'; +import { usePreferenceFeaturePreviewList } from './usePreferenceFeaturePreviewList'; export const useFeaturePreview = (featureName: FeaturesAvailable) => { - const { features } = useFeaturePreviewList(); + const { features } = usePreferenceFeaturePreviewList(); const currentFeature = features?.find((feature) => feature.name === featureName); diff --git a/packages/ui-client/src/hooks/useFeaturePreviewList.ts b/packages/ui-client/src/hooks/useFeaturePreviewList.ts index ff103a8d84ef..08bda4ff81ff 100644 --- a/packages/ui-client/src/hooks/useFeaturePreviewList.ts +++ b/packages/ui-client/src/hooks/useFeaturePreviewList.ts @@ -1,5 +1,4 @@ import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useUserPreference, useSetting } from '@rocket.chat/ui-contexts'; export type FeaturesAvailable = | 'quickReactions' @@ -24,6 +23,7 @@ export type FeaturePreviewProps = { }; }; +// TODO: Move the features preview array to another directory to be accessed from both BE and FE. export const defaultFeaturesPreview: FeaturePreviewProps[] = [ { name: 'quickReactions', @@ -47,6 +47,7 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ i18n: 'Enable_timestamp', description: 'Enable_timestamp_description', group: 'Message', + imageUrl: 'images/featurePreview/timestamp.png', value: false, enabled: true, }, @@ -55,6 +56,7 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ i18n: 'Contextualbar_resizable', description: 'Contextualbar_resizable_description', group: 'Navigation', + imageUrl: 'images/featurePreview/resizable-contextual-bar.png', value: false, enabled: true, }, @@ -63,6 +65,7 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ i18n: 'New_navigation', description: 'New_navigation_description', group: 'Navigation', + imageUrl: 'images/featurePreview/enhanced-navigation.png', value: false, enabled: true, }, @@ -82,22 +85,27 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ export const enabledDefaultFeatures = defaultFeaturesPreview.filter((feature) => feature.enabled); -export const useFeaturePreviewList = () => { - const featurePreviewEnabled = useSetting('Accounts_AllowFeaturePreview'); - const userFeaturesPreview = useUserPreference('featuresPreview'); - - if (!featurePreviewEnabled) { - return { unseenFeatures: 0, features: [] as FeaturePreviewProps[], featurePreviewEnabled }; +// TODO: Remove this logic after we have a way to store object settings. +export const parseSetting = (setting?: FeaturePreviewProps[] | string) => { + if (typeof setting === 'string') { + try { + return JSON.parse(setting) as FeaturePreviewProps[]; + } catch (_) { + return; + } } + return setting; +}; +export const useFeaturePreviewList = (featuresList: Pick[]) => { const unseenFeatures = enabledDefaultFeatures.filter( - (feature) => !userFeaturesPreview?.find((userFeature) => userFeature.name === feature.name), + (defaultFeature) => !featuresList?.find((feature) => feature.name === defaultFeature.name), ).length; - const mergedFeatures = enabledDefaultFeatures.map((feature) => { - const userFeature = userFeaturesPreview?.find((userFeature) => userFeature.name === feature.name); - return { ...feature, ...userFeature }; + const mergedFeatures = enabledDefaultFeatures.map((defaultFeature) => { + const features = featuresList?.find((feature) => feature.name === defaultFeature.name); + return { ...defaultFeature, ...features }; }); - return { unseenFeatures, features: mergedFeatures, featurePreviewEnabled }; + return { unseenFeatures, features: mergedFeatures }; }; diff --git a/packages/ui-client/src/hooks/useFeaturePreviewList.spec.tsx b/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.spec.tsx similarity index 79% rename from packages/ui-client/src/hooks/useFeaturePreviewList.spec.tsx rename to packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.spec.tsx index e348cfb6a864..ac3d6f92d51a 100644 --- a/packages/ui-client/src/hooks/useFeaturePreviewList.spec.tsx +++ b/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.spec.tsx @@ -1,10 +1,11 @@ import { mockAppRoot } from '@rocket.chat/mock-providers'; import { renderHook } from '@testing-library/react'; -import { useFeaturePreviewList, enabledDefaultFeatures } from './useFeaturePreviewList'; +import { enabledDefaultFeatures } from './useFeaturePreviewList'; +import { usePreferenceFeaturePreviewList } from './usePreferenceFeaturePreviewList'; it('should return the number of unseen features and Accounts_AllowFeaturePreview enabled ', () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot().withSetting('Accounts_AllowFeaturePreview', true).build(), }); @@ -18,7 +19,7 @@ it('should return the number of unseen features and Accounts_AllowFeaturePreview }); it('should return the number of unseen features and Accounts_AllowFeaturePreview disabled ', () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot().withSetting('Accounts_AllowFeaturePreview', false).build(), }); @@ -32,7 +33,7 @@ it('should return the number of unseen features and Accounts_AllowFeaturePreview }); it('should return 0 unseen features', () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot() .withSetting('Accounts_AllowFeaturePreview', true) @@ -49,7 +50,7 @@ it('should return 0 unseen features', () => { }); it('should ignore removed feature previews', () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot() .withSetting('Accounts_AllowFeaturePreview', true) @@ -72,7 +73,7 @@ it('should ignore removed feature previews', () => { }); it('should turn off ignored feature previews', async () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot() .withSetting('Accounts_AllowFeaturePreview', true) diff --git a/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.ts b/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.ts new file mode 100644 index 000000000000..d7c4c13417d2 --- /dev/null +++ b/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.ts @@ -0,0 +1,16 @@ +import { useSetting, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { FeaturePreviewProps, parseSetting, useFeaturePreviewList } from './useFeaturePreviewList'; + +export const usePreferenceFeaturePreviewList = () => { + const featurePreviewEnabled = useSetting('Accounts_AllowFeaturePreview'); + const userFeaturesPreviewPreference = useUserPreference('featuresPreview'); + const userFeaturesPreview = useMemo(() => parseSetting(userFeaturesPreviewPreference), [userFeaturesPreviewPreference]); + const { unseenFeatures, features } = useFeaturePreviewList(userFeaturesPreview ?? []); + + if (!featurePreviewEnabled) { + return { unseenFeatures: 0, features: [] as FeaturePreviewProps[], featurePreviewEnabled }; + } + return { unseenFeatures, features, featurePreviewEnabled }; +}; diff --git a/packages/ui-client/src/index.ts b/packages/ui-client/src/index.ts index 3e640343da5b..a96ef265aadc 100644 --- a/packages/ui-client/src/index.ts +++ b/packages/ui-client/src/index.ts @@ -1,5 +1,7 @@ export * from './components'; export * from './hooks/useFeaturePreview'; +export * from './hooks/useDefaultSettingFeaturePreviewList'; export * from './hooks/useFeaturePreviewList'; +export * from './hooks/usePreferenceFeaturePreviewList'; export * from './hooks/useDocumentTitle'; export * from './helpers'; From a4fcd99e3006f28abb0b3f06985809b447365fc4 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 24 Sep 2024 12:45:44 -0300 Subject: [PATCH 31/39] chore: move common files to core-services (#33341) --- _templates/service/new/service.ejs.t | 8 ++------ ee/apps/account-service/src/service.ts | 8 ++------ ee/apps/authorization-service/src/service.ts | 8 ++------ ee/apps/ddp-streamer/src/service.ts | 8 ++------ ee/apps/omnichannel-transcript/src/service.ts | 8 ++------ ee/apps/presence-service/src/service.ts | 8 ++------ ee/apps/queue-worker/src/service.ts | 8 ++------ ee/apps/stream-hub-service/src/service.ts | 8 ++------ packages/core-services/src/index.ts | 2 ++ .../core-services/src/lib}/mongo.ts | 14 ++------------ 10 files changed, 20 insertions(+), 60 deletions(-) rename {apps/meteor/ee/server/services => packages/core-services/src/lib}/mongo.ts (72%) diff --git a/_templates/service/new/service.ejs.t b/_templates/service/new/service.ejs.t index 77f02d2b7769..699539365259 100644 --- a/_templates/service/new/service.ejs.t +++ b/_templates/service/new/service.ejs.t @@ -1,12 +1,10 @@ --- to: ee/apps/<%= name %>/src/service.ts --- -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; const PORT = process.env.PORT || <%= h.random() %>; @@ -14,9 +12,7 @@ const PORT = process.env.PORT || <%= h.random() %>; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/account-service/src/service.ts b/ee/apps/account-service/src/service.ts index 07ca30ed748f..c2f64e37bde3 100755 --- a/ee/apps/account-service/src/service.ts +++ b/ee/apps/account-service/src/service.ts @@ -1,19 +1,15 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; const PORT = process.env.PORT || 3033; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/authorization-service/src/service.ts b/ee/apps/authorization-service/src/service.ts index 4dcd466afa60..1698ef7a115c 100755 --- a/ee/apps/authorization-service/src/service.ts +++ b/ee/apps/authorization-service/src/service.ts @@ -1,19 +1,15 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; const PORT = process.env.PORT || 3034; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/ddp-streamer/src/service.ts b/ee/apps/ddp-streamer/src/service.ts index 07666a265dbe..58552240cadd 100755 --- a/ee/apps/ddp-streamer/src/service.ts +++ b/ee/apps/ddp-streamer/src/service.ts @@ -1,16 +1,12 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/omnichannel-transcript/src/service.ts b/ee/apps/omnichannel-transcript/src/service.ts index 66456456fb74..ad60687d5ba4 100644 --- a/ee/apps/omnichannel-transcript/src/service.ts +++ b/ee/apps/omnichannel-transcript/src/service.ts @@ -1,20 +1,16 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; const PORT = process.env.PORT || 3036; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/presence-service/src/service.ts b/ee/apps/presence-service/src/service.ts index 0e1c97f2daa2..0c51c30dc577 100755 --- a/ee/apps/presence-service/src/service.ts +++ b/ee/apps/presence-service/src/service.ts @@ -1,19 +1,15 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; const PORT = process.env.PORT || 3031; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/queue-worker/src/service.ts b/ee/apps/queue-worker/src/service.ts index 4bc6c9642913..c11376d56534 100644 --- a/ee/apps/queue-worker/src/service.ts +++ b/ee/apps/queue-worker/src/service.ts @@ -1,20 +1,16 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; const PORT = process.env.PORT || 3038; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/stream-hub-service/src/service.ts b/ee/apps/stream-hub-service/src/service.ts index eade703321d2..5e035548dc38 100755 --- a/ee/apps/stream-hub-service/src/service.ts +++ b/ee/apps/stream-hub-service/src/service.ts @@ -1,11 +1,9 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; import { DatabaseWatcher } from '../../../../apps/meteor/server/database/DatabaseWatcher'; import { StreamHub } from './StreamHub'; @@ -14,9 +12,7 @@ const PORT = process.env.PORT || 3035; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index 8eea19ea7405..85722c98839f 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -75,6 +75,8 @@ export { AnalyticsOverviewDataResult, } from './types/IOmnichannelAnalyticsService'; +export { getConnection, getTrashCollection } from './lib/mongo'; + export { AutoUpdateRecord, FindVoipRoomsParams, diff --git a/apps/meteor/ee/server/services/mongo.ts b/packages/core-services/src/lib/mongo.ts similarity index 72% rename from apps/meteor/ee/server/services/mongo.ts rename to packages/core-services/src/lib/mongo.ts index 27e9e931c039..fab1fd108d99 100644 --- a/apps/meteor/ee/server/services/mongo.ts +++ b/packages/core-services/src/lib/mongo.ts @@ -5,16 +5,6 @@ const { MONGO_URL = 'mongodb://localhost:27017/rocketchat' } = process.env; const name = /^mongodb:\/\/.*?(?::[0-9]+)?\/([^?]*)/.exec(MONGO_URL)?.[1]; -export enum Collections { - Subscriptions = 'rocketchat_subscription', - UserSession = 'usersSessions', - User = 'users', - Trash = 'rocketchat__trash', - Messages = 'rocketchat_message', - Rooms = 'rocketchat_room', - Settings = 'rocketchat_settings', -} - function connectDb(options?: MongoClientOptions): Promise { const client = new MongoClient(MONGO_URL, options); @@ -44,9 +34,9 @@ export const getConnection = ((): ((options?: MongoClientOptions) => Promise }; })(); -export async function getCollection(name: Collections): Promise> { +export async function getTrashCollection(): Promise> { if (!db) { db = await getConnection(); } - return db.collection(name); + return db.collection('rocketchat__trash'); } From d94a159126a757d156496f8ab2ce95f09ea45ff1 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 24 Sep 2024 12:50:10 -0300 Subject: [PATCH 32/39] regression: `Sidepanel` color highlight (#33342) --- apps/meteor/package.json | 2 +- apps/uikit-playground/package.json | 2 +- ee/packages/ui-theming/package.json | 2 +- packages/fuselage-ui-kit/package.json | 2 +- packages/gazzodown/package.json | 2 +- packages/ui-avatar/package.json | 2 +- packages/ui-client/package.json | 2 +- packages/ui-composer/package.json | 2 +- packages/ui-video-conf/package.json | 2 +- yarn.lock | 50 +++++++++++++-------------- 10 files changed, 34 insertions(+), 34 deletions(-) diff --git a/apps/meteor/package.json b/apps/meteor/package.json index a09dcb657466..a30ce30a7b6f 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -241,7 +241,7 @@ "@rocket.chat/favicon": "workspace:^", "@rocket.chat/forked-matrix-appservice-bridge": "^4.0.2", "@rocket.chat/forked-matrix-bot-sdk": "^0.6.0-beta.3", - "@rocket.chat/fuselage": "^0.59.0", + "@rocket.chat/fuselage": "^0.59.1", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/fuselage-toastbar": "^0.33.0", diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index 48c099b8d9d1..c5e7628001c6 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -17,7 +17,7 @@ "@lezer/highlight": "^1.1.6", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.59.0", + "@rocket.chat/fuselage": "^0.59.1", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/fuselage-toastbar": "^0.33.0", diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index 0548f235c3fd..29fe229ca532 100644 --- a/ee/packages/ui-theming/package.json +++ b/ee/packages/ui-theming/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.59.0", + "@rocket.chat/fuselage": "^0.59.1", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/icons": "~0.38.0", "@rocket.chat/ui-contexts": "workspace:~", diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 8fb0ca254d68..573d65dc215e 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -66,7 +66,7 @@ "@rocket.chat/apps-engine": "1.45.0-alpha.868", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.59.0", + "@rocket.chat/fuselage": "^0.59.1", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/icons": "~0.38.0", diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 47bf2782226e..a5883bbe3c0f 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -28,7 +28,7 @@ "@babel/core": "~7.22.20", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.59.0", + "@rocket.chat/fuselage": "^0.59.1", "@rocket.chat/fuselage-tokens": "^0.33.1", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/message-parser": "workspace:^", diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index a32a7a8be6bf..70f0efe2cff9 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@babel/core": "~7.22.20", - "@rocket.chat/fuselage": "^0.59.0", + "@rocket.chat/fuselage": "^0.59.1", "@rocket.chat/ui-contexts": "workspace:^", "@types/react": "~17.0.80", "@types/react-dom": "~17.0.25", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 82705ba026cb..40bccd8ca628 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -21,7 +21,7 @@ "@babel/core": "~7.22.20", "@react-aria/toolbar": "^3.0.0-beta.1", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.59.0", + "@rocket.chat/fuselage": "^0.59.1", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/icons": "~0.38.0", "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index 363299698bfa..c43f3485880e 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -19,7 +19,7 @@ "@babel/core": "~7.22.20", "@react-aria/toolbar": "^3.0.0-beta.1", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.59.0", + "@rocket.chat/fuselage": "^0.59.1", "@rocket.chat/icons": "~0.38.0", "@storybook/addon-actions": "~6.5.16", "@storybook/addon-docs": "~6.5.16", diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 0f1f3e50eab8..74c3ec43bcbb 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.20", "@rocket.chat/css-in-js": "~0.31.25", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.59.0", + "@rocket.chat/fuselage": "^0.59.1", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/icons": "~0.38.0", "@rocket.chat/jest-presets": "workspace:~", diff --git a/yarn.lock b/yarn.lock index c0fb2733310e..20c005cca203 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8910,7 +8910,7 @@ __metadata: "@rocket.chat/apps-engine": 1.45.0-alpha.868 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.59.0 + "@rocket.chat/fuselage": ^0.59.1 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/fuselage-polyfills": ~0.31.25 "@rocket.chat/gazzodown": "workspace:^" @@ -8959,19 +8959,19 @@ __metadata: "@rocket.chat/icons": "*" "@rocket.chat/prettier-config": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": "*" - "@rocket.chat/ui-contexts": "*" + "@rocket.chat/ui-avatar": 7.0.0-rc.0 + "@rocket.chat/ui-contexts": 11.0.0-rc.0 "@rocket.chat/ui-kit": "*" - "@rocket.chat/ui-video-conf": "*" + "@rocket.chat/ui-video-conf": 11.0.0-rc.0 "@tanstack/react-query": "*" react: "*" react-dom: "*" languageName: unknown linkType: soft -"@rocket.chat/fuselage@npm:^0.59.0": - version: 0.59.0 - resolution: "@rocket.chat/fuselage@npm:0.59.0" +"@rocket.chat/fuselage@npm:^0.59.1": + version: 0.59.1 + resolution: "@rocket.chat/fuselage@npm:0.59.1" dependencies: "@rocket.chat/css-in-js": ^0.31.25 "@rocket.chat/css-supports": ^0.31.25 @@ -8989,7 +8989,7 @@ __metadata: react: ^17.0.2 react-dom: ^17.0.2 react-virtuoso: 1.2.4 - checksum: 259dce5381a3c3e0d7c7f3dc7ab51346cb65a9f4906a5ca5d6a976627d05e01e7f8a3a940604d0ad1b2b4ed89c250a871ef3fb253f6bbb69d35bc931e193898d + checksum: 6ecceaefe8b2c6b9fe0ba3b6e19360f5d46af9697e7c97af16cce6c9eea2cb79255f38ccffdbc4766b4104c079281b4980fe7c924cd62fe5d2d341581fdf4f62 languageName: node linkType: hard @@ -9000,7 +9000,7 @@ __metadata: "@babel/core": ~7.22.20 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.59.0 + "@rocket.chat/fuselage": ^0.59.1 "@rocket.chat/fuselage-tokens": ^0.33.1 "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/message-parser": "workspace:^" @@ -9047,10 +9047,10 @@ __metadata: "@rocket.chat/css-in-js": "*" "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-tokens": "*" - "@rocket.chat/message-parser": 0.31.29 + "@rocket.chat/message-parser": 0.31.30-rc.0 "@rocket.chat/styled": "*" - "@rocket.chat/ui-client": "*" - "@rocket.chat/ui-contexts": "*" + "@rocket.chat/ui-client": 11.0.0-rc.0 + "@rocket.chat/ui-contexts": 11.0.0-rc.0 katex: "*" react: "*" languageName: unknown @@ -9372,7 +9372,7 @@ __metadata: "@rocket.chat/favicon": "workspace:^" "@rocket.chat/forked-matrix-appservice-bridge": ^4.0.2 "@rocket.chat/forked-matrix-bot-sdk": ^0.6.0-beta.3 - "@rocket.chat/fuselage": ^0.59.0 + "@rocket.chat/fuselage": ^0.59.1 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/fuselage-polyfills": ~0.31.25 "@rocket.chat/fuselage-toastbar": ^0.33.0 @@ -10244,7 +10244,7 @@ __metadata: resolution: "@rocket.chat/ui-avatar@workspace:packages/ui-avatar" dependencies: "@babel/core": ~7.22.20 - "@rocket.chat/fuselage": ^0.59.0 + "@rocket.chat/fuselage": ^0.59.1 "@rocket.chat/ui-contexts": "workspace:^" "@types/react": ~17.0.80 "@types/react-dom": ~17.0.25 @@ -10257,7 +10257,7 @@ __metadata: typescript: ~5.5.4 peerDependencies: "@rocket.chat/fuselage": "*" - "@rocket.chat/ui-contexts": "*" + "@rocket.chat/ui-contexts": 11.0.0-rc.0 react: ~17.0.2 languageName: unknown linkType: soft @@ -10269,7 +10269,7 @@ __metadata: "@babel/core": ~7.22.20 "@react-aria/toolbar": ^3.0.0-beta.1 "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.59.0 + "@rocket.chat/fuselage": ^0.59.1 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/icons": ~0.38.0 "@rocket.chat/jest-presets": "workspace:~" @@ -10308,8 +10308,8 @@ __metadata: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" - "@rocket.chat/ui-avatar": "*" - "@rocket.chat/ui-contexts": "*" + "@rocket.chat/ui-avatar": 7.0.0-rc.0 + "@rocket.chat/ui-contexts": 11.0.0-rc.0 react: "*" react-i18next: "*" languageName: unknown @@ -10322,7 +10322,7 @@ __metadata: "@babel/core": ~7.22.20 "@react-aria/toolbar": ^3.0.0-beta.1 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.59.0 + "@rocket.chat/fuselage": ^0.59.1 "@rocket.chat/icons": ~0.38.0 "@storybook/addon-actions": ~6.5.16 "@storybook/addon-docs": ~6.5.16 @@ -10416,7 +10416,7 @@ __metadata: resolution: "@rocket.chat/ui-theming@workspace:ee/packages/ui-theming" dependencies: "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.59.0 + "@rocket.chat/fuselage": ^0.59.1 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/icons": ~0.38.0 "@rocket.chat/ui-contexts": "workspace:~" @@ -10446,7 +10446,7 @@ __metadata: "@rocket.chat/css-in-js": ~0.31.25 "@rocket.chat/emitter": ~0.31.25 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.59.0 + "@rocket.chat/fuselage": ^0.59.1 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/icons": ~0.38.0 "@rocket.chat/jest-presets": "workspace:~" @@ -10478,8 +10478,8 @@ __metadata: "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": "*" - "@rocket.chat/ui-contexts": "*" + "@rocket.chat/ui-avatar": 7.0.0-rc.0 + "@rocket.chat/ui-contexts": 11.0.0-rc.0 react: ^17.0.2 react-dom: ^17.0.2 languageName: unknown @@ -10495,7 +10495,7 @@ __metadata: "@lezer/highlight": ^1.1.6 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.59.0 + "@rocket.chat/fuselage": ^0.59.1 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/fuselage-polyfills": ~0.31.25 "@rocket.chat/fuselage-toastbar": ^0.33.0 @@ -10567,7 +10567,7 @@ __metadata: peerDependencies: "@rocket.chat/layout": "*" "@rocket.chat/tools": 0.2.2 - "@rocket.chat/ui-contexts": "*" + "@rocket.chat/ui-contexts": 11.0.0-rc.0 "@tanstack/react-query": "*" react: "*" react-hook-form: "*" From 2f9eea03d2122e963bc89559e772bf00a54ad200 Mon Sep 17 00:00:00 2001 From: Lucas Pelegrino Date: Tue, 24 Sep 2024 09:54:31 -0300 Subject: [PATCH 33/39] feat: Adds new admin feature preview setting management (#33212) Co-authored-by: Guilherme Gazzo --- .changeset/quick-rings-wave.md | 7 + .../UserMenu/hooks/useAccountItems.tsx | 4 +- .../hooks/useFeaturePreviewEnableQuery.ts | 28 ++++ .../sidebar/header/hooks/useAccountItems.tsx | 4 +- .../AccountFeaturePreviewBadge.tsx | 21 --- .../AccountFeaturePreviewPage.tsx | 44 ++---- .../client/views/account/sidebarItems.tsx | 5 +- .../AdminFeaturePreviewPage.tsx | 127 ++++++++++++++++++ .../AdminFeaturePreviewRoute.tsx | 26 ++++ apps/meteor/client/views/admin/routes.tsx | 9 ++ .../meteor/client/views/admin/sidebarItems.ts | 8 ++ .../featurePreview/enhanced-navigation.png | Bin 0 -> 2372 bytes .../resizable-contextual-bar.png | Bin 0 -> 4776 bytes .../images/featurePreview/timestamp.png | Bin 0 -> 51432 bytes apps/meteor/server/settings/accounts.ts | 5 + .../tests/end-to-end/api/miscellaneous.ts | 1 + packages/i18n/src/locales/en.i18n.json | 16 ++- packages/i18n/src/locales/hi-IN.i18n.json | 6 +- .../FeaturePreview/FeaturePreviewBadge.tsx | 21 +++ .../src/components/FeaturePreview/index.ts | 2 + packages/ui-client/src/components/index.ts | 2 +- .../useDefaultSettingFeaturePreviewList.ts | 12 ++ .../ui-client/src/hooks/useFeaturePreview.ts | 5 +- .../src/hooks/useFeaturePreviewList.ts | 32 +++-- ... usePreferenceFeaturePreviewList.spec.tsx} | 13 +- .../hooks/usePreferenceFeaturePreviewList.ts | 16 +++ packages/ui-client/src/index.ts | 2 + 27 files changed, 324 insertions(+), 92 deletions(-) create mode 100644 .changeset/quick-rings-wave.md create mode 100644 apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts delete mode 100644 apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx create mode 100644 apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx create mode 100644 apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx create mode 100644 apps/meteor/public/images/featurePreview/enhanced-navigation.png create mode 100644 apps/meteor/public/images/featurePreview/resizable-contextual-bar.png create mode 100644 apps/meteor/public/images/featurePreview/timestamp.png create mode 100644 packages/ui-client/src/components/FeaturePreview/FeaturePreviewBadge.tsx create mode 100644 packages/ui-client/src/components/FeaturePreview/index.ts create mode 100644 packages/ui-client/src/hooks/useDefaultSettingFeaturePreviewList.ts rename packages/ui-client/src/hooks/{useFeaturePreviewList.spec.tsx => usePreferenceFeaturePreviewList.spec.tsx} (79%) create mode 100644 packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.ts diff --git a/.changeset/quick-rings-wave.md b/.changeset/quick-rings-wave.md new file mode 100644 index 000000000000..0ea22897ff45 --- /dev/null +++ b/.changeset/quick-rings-wave.md @@ -0,0 +1,7 @@ +--- +"@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/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx index 82c39c5c1b10..e54e2b72d675 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx @@ -1,7 +1,7 @@ import { Badge } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { defaultFeaturesPreview, useFeaturePreviewList } from '@rocket.chat/ui-client'; import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { defaultFeaturesPreview, usePreferenceFeaturePreviewList } from '@rocket.chat/ui-client'; import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -9,7 +9,7 @@ export const useAccountItems = (): GenericMenuItemProps[] => { const t = useTranslation(); const router = useRouter(); - const { unseenFeatures, featurePreviewEnabled } = useFeaturePreviewList(); + const { unseenFeatures, featurePreviewEnabled } = usePreferenceFeaturePreviewList(); const handleMyAccount = useEffectEvent(() => { router.navigate('/account'); diff --git a/apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts b/apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts new file mode 100644 index 000000000000..fd88f0237d29 --- /dev/null +++ b/apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts @@ -0,0 +1,28 @@ +import type { FeaturePreviewProps } from '@rocket.chat/ui-client'; +import { useMemo } from 'react'; + +const handleFeaturePreviewEnableQuery = (item: FeaturePreviewProps, _: any, features: FeaturePreviewProps[]) => { + if (item.enableQuery) { + const expected = item.enableQuery.value; + const received = features.find((el) => el.name === item.enableQuery?.name)?.value; + if (expected !== received) { + item.disabled = true; + item.value = false; + } else { + item.disabled = false; + } + } + return item; +}; + +const groupFeaturePreview = (features: FeaturePreviewProps[]) => + Object.entries( + features.reduce((result, currentValue) => { + (result[currentValue.group] = result[currentValue.group] || []).push(currentValue); + return result; + }, {} as Record), + ); + +export const useFeaturePreviewEnableQuery = (features: FeaturePreviewProps[]) => { + return useMemo(() => groupFeaturePreview(features.map(handleFeaturePreviewEnableQuery)), [features]); +}; diff --git a/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx b/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx index 2be6b2b1dea2..51ab7a198a67 100644 --- a/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx +++ b/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx @@ -1,7 +1,7 @@ import { Badge } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { defaultFeaturesPreview, useFeaturePreviewList } from '@rocket.chat/ui-client'; import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { defaultFeaturesPreview, usePreferenceFeaturePreviewList } from '@rocket.chat/ui-client'; import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -9,7 +9,7 @@ export const useAccountItems = (): GenericMenuItemProps[] => { const t = useTranslation(); const router = useRouter(); - const { unseenFeatures, featurePreviewEnabled } = useFeaturePreviewList(); + const { unseenFeatures, featurePreviewEnabled } = usePreferenceFeaturePreviewList(); const handleMyAccount = useMutableCallback(() => { router.navigate('/account'); diff --git a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx b/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx deleted file mode 100644 index c109ca1aefb5..000000000000 --- a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Badge } from '@rocket.chat/fuselage'; -import { useFeaturePreviewList } from '@rocket.chat/ui-client'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -const AccountFeaturePreviewBadge = () => { - const { t } = useTranslation(); - const { unseenFeatures } = useFeaturePreviewList(); - - if (!unseenFeatures) { - return null; - } - - return ( - - {unseenFeatures} - - ); -}; - -export default AccountFeaturePreviewBadge; diff --git a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx b/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx index dd9ab6a90959..358d2394003b 100644 --- a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx +++ b/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx @@ -1,4 +1,3 @@ -import { css } from '@rocket.chat/css-in-js'; import { ButtonGroup, Button, @@ -13,9 +12,10 @@ import { FieldLabel, FieldRow, FieldHint, + Callout, + Margins, } from '@rocket.chat/fuselage'; -import type { FeaturePreviewProps } from '@rocket.chat/ui-client'; -import { useFeaturePreviewList } from '@rocket.chat/ui-client'; +import { usePreferenceFeaturePreviewList } from '@rocket.chat/ui-client'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useToastMessageDispatch, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import type { ChangeEvent } from 'react'; @@ -23,26 +23,12 @@ import React, { useEffect, Fragment } from 'react'; import { useForm } from 'react-hook-form'; import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; +import { useFeaturePreviewEnableQuery } from '../../../hooks/useFeaturePreviewEnableQuery'; -const handleEnableQuery = (features: FeaturePreviewProps[]) => { - return features.map((item) => { - if (item.enableQuery) { - const expected = item.enableQuery.value; - const received = features.find((el) => el.name === item.enableQuery?.name)?.value; - if (expected !== received) { - item.disabled = true; - item.value = false; - } else { - item.disabled = false; - } - } - return item; - }); -}; const AccountFeaturePreviewPage = () => { const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); - const { features, unseenFeatures } = useFeaturePreviewList(); + const { features, unseenFeatures } = usePreferenceFeaturePreviewList(); const setUserPreferences = useEndpoint('POST', '/v1/users.setPreferences'); @@ -85,12 +71,7 @@ const AccountFeaturePreviewPage = () => { setValue('featuresPreview', updated, { shouldDirty: true }); }; - const grouppedFeaturesPreview = Object.entries( - handleEnableQuery(featuresPreview).reduce((result, currentValue) => { - (result[currentValue.group] = result[currentValue.group] || []).push(currentValue); - return result; - }, {} as Record), - ); + const grouppedFeaturesPreview = useFeaturePreviewEnableQuery(featuresPreview); return ( @@ -105,14 +86,11 @@ const AccountFeaturePreviewPage = () => { )} {featuresPreview.length > 0 && ( <> - - {t('Feature_preview_page_description')} + + + {t('Feature_preview_page_description')} + {t('Feature_preview_page_callout')} + {grouppedFeaturesPreview?.map(([group, features], index) => ( diff --git a/apps/meteor/client/views/account/sidebarItems.tsx b/apps/meteor/client/views/account/sidebarItems.tsx index ca0376be329d..fa2ab8bd5e40 100644 --- a/apps/meteor/client/views/account/sidebarItems.tsx +++ b/apps/meteor/client/views/account/sidebarItems.tsx @@ -1,10 +1,9 @@ -import { defaultFeaturesPreview } from '@rocket.chat/ui-client'; +import { defaultFeaturesPreview, FeaturePreviewBadge } from '@rocket.chat/ui-client'; import React from 'react'; import { hasPermission, hasAtLeastOnePermission } from '../../../app/authorization/client'; import { settings } from '../../../app/settings/client'; import { createSidebarItems } from '../../lib/createSidebarItems'; -import AccountFeaturePreviewBadge from './featurePreview/AccountFeaturePreviewBadge'; export const { registerSidebarItem: registerAccountSidebarItem, @@ -54,7 +53,7 @@ export const { href: '/account/feature-preview', i18nLabel: 'Feature_preview', icon: 'flask', - badge: () => , + badge: () => , permissionGranted: () => settings.get('Accounts_AllowFeaturePreview') && defaultFeaturesPreview?.length > 0, }, { diff --git a/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx new file mode 100644 index 000000000000..615fd20cf5a6 --- /dev/null +++ b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx @@ -0,0 +1,127 @@ +import { + ButtonGroup, + Button, + Box, + ToggleSwitch, + Accordion, + Field, + FieldGroup, + FieldLabel, + FieldRow, + FieldHint, + Callout, + Margins, +} from '@rocket.chat/fuselage'; +import { useDefaultSettingFeaturePreviewList } from '@rocket.chat/ui-client'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useTranslation, useSettingsDispatch } from '@rocket.chat/ui-contexts'; +import type { ChangeEvent } from 'react'; +import React, { Fragment } from 'react'; +import { useForm } from 'react-hook-form'; + +import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; +import { useFeaturePreviewEnableQuery } from '../../../hooks/useFeaturePreviewEnableQuery'; +import { useEditableSetting } from '../EditableSettingsContext'; +import Setting from '../settings/Setting'; +import SettingsGroupPageSkeleton from '../settings/SettingsGroupPage/SettingsGroupPageSkeleton'; + +const AdminFeaturePreviewPage = () => { + const t = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + const allowFeaturePreviewSetting = useEditableSetting('Accounts_AllowFeaturePreview'); + const { features } = useDefaultSettingFeaturePreviewList(); + + const { + watch, + formState: { isDirty }, + setValue, + handleSubmit, + reset, + } = useForm({ + defaultValues: { featuresPreview: features }, + }); + const { featuresPreview } = watch(); + const dispatch = useSettingsDispatch(); + + const handleSave = async () => { + try { + const featuresToBeSaved = featuresPreview.map((feature) => ({ name: feature.name, value: feature.value })); + + await dispatch([ + { _id: allowFeaturePreviewSetting!._id, value: allowFeaturePreviewSetting!.value }, + { _id: 'Accounts_Default_User_Preferences_featuresPreview', value: JSON.stringify(featuresToBeSaved) }, + ]); + dispatchToastMessage({ type: 'success', message: t('Preferences_saved') }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } finally { + reset({ featuresPreview }); + } + }; + + const handleFeatures = (e: ChangeEvent) => { + const updated = featuresPreview.map((item) => (item.name === e.target.name ? { ...item, value: e.target.checked } : item)); + setValue('featuresPreview', updated, { shouldDirty: true }); + }; + + const grouppedFeaturesPreview = useFeaturePreviewEnableQuery(featuresPreview); + + if (!allowFeaturePreviewSetting) { + // TODO: Implement FeaturePreviewSkeleton component + return ; + } + + return ( + + + + + + + {t('Feature_preview_admin_page_description')} + {t('Feature_preview_page_callout')} + {t('Feature_preview_admin_page_callout')} + + + + + {grouppedFeaturesPreview?.map(([group, features], index) => ( + + + {features.map((feature) => ( + + + + {t(feature.i18n)} + + + {feature.description && {t(feature.description)}} + + {feature.imageUrl && } + + ))} + + + ))} + + + + + + + + + + + ); +}; + +export default AdminFeaturePreviewPage; diff --git a/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx new file mode 100644 index 000000000000..a7d6bd77d136 --- /dev/null +++ b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx @@ -0,0 +1,26 @@ +import { usePermission } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React, { memo } from 'react'; + +import SettingsProvider from '../../../providers/SettingsProvider'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; +import EditableSettingsProvider from '../settings/EditableSettingsProvider'; +import AdminFeaturePreviewPage from './AdminFeaturePreviewPage'; + +const AdminFeaturePreviewRoute = (): ReactElement => { + const canViewFeaturesPreview = usePermission('manage-cloud'); + + if (!canViewFeaturesPreview) { + return ; + } + + return ( + + + + + + ); +}; + +export default memo(AdminFeaturePreviewRoute); diff --git a/apps/meteor/client/views/admin/routes.tsx b/apps/meteor/client/views/admin/routes.tsx index f70df1625871..d244d5e2f19b 100644 --- a/apps/meteor/client/views/admin/routes.tsx +++ b/apps/meteor/client/views/admin/routes.tsx @@ -104,6 +104,10 @@ declare module '@rocket.chat/ui-contexts' { pathname: `/admin/subscription`; pattern: '/admin/subscription'; }; + 'admin-feature-preview': { + pathname: '/admin/feature-preview'; + pattern: '/admin/feature-preview'; + }; } } @@ -237,3 +241,8 @@ registerAdminRoute('/subscription', { name: 'subscription', component: lazy(() => import('./subscription/SubscriptionRoute')), }); + +registerAdminRoute('/feature-preview', { + name: 'admin-feature-preview', + component: lazy(() => import('./featurePreview/AdminFeaturePreviewRoute')), +}); diff --git a/apps/meteor/client/views/admin/sidebarItems.ts b/apps/meteor/client/views/admin/sidebarItems.ts index 013206d9e9a8..fc7d307396d4 100644 --- a/apps/meteor/client/views/admin/sidebarItems.ts +++ b/apps/meteor/client/views/admin/sidebarItems.ts @@ -1,3 +1,5 @@ +import { defaultFeaturesPreview } from '@rocket.chat/ui-client'; + import { hasPermission, hasAtLeastOnePermission, hasAllPermission } from '../../../app/authorization/client'; import { createSidebarItems } from '../../lib/createSidebarItems'; @@ -129,6 +131,12 @@ export const { icon: 'emoji', permissionGranted: (): boolean => hasPermission('manage-emoji'), }, + { + href: '/admin/feature-preview', + i18nLabel: 'Feature_preview', + icon: 'flask', + permissionGranted: () => defaultFeaturesPreview?.length > 0, + }, { href: '/admin/settings', i18nLabel: 'Settings', diff --git a/apps/meteor/public/images/featurePreview/enhanced-navigation.png b/apps/meteor/public/images/featurePreview/enhanced-navigation.png new file mode 100644 index 0000000000000000000000000000000000000000..4240326ba985d0a054ae5d9c8521b24c8d93e6f7 GIT binary patch literal 2372 zcmeHIYgCg*8lKz;k(*0VynsLp8%mX01@Ra$34#GB2w8)n2Lz6kXazzALMRv_YP;xG zVrfx?0*kHdhC)CtVh|D##7hAYg%TP_P1$fufB*?0CfSL{AKkOvo<04szxKyBGw<`h z^UgExH}gzEL^#D}&89T~0Gp8DKq>$zF&wv8TEKSeq4Rv$;nISm(*aoTVg3*yRQGuZ zBGRdp08rcKGzASbgB(T%puW)RWda5O%aM>k@?kbYDJ7;mXg7)%7}|_U^u0g_?-vIh z3PLLno>;U^yRy;Ip#U%U`gz?3Q9dbXM|jd~=gzm4M_kLikKsE!!V_rWmyy}L8oj)= zS2fb?Im$rG8QwM!!;_UE8Rx|HP0%OX2>m!4{!$M9@ zmzycKXS|vvx3jAFOuN;#LOx%@xz-bPQB~L4+B$H!?W8hJu;wTX4KMQ=yEwA+byHJQ z4Srntp0Hr<_>yuo%x4&B_O`IRa^*|1XD(Z^^k)!1hMgXbR43&u9)Lcv`iBcipXe%2 zq_vq)1Omaa`(22J@|`C&yL#jc-3GtCI}p2cuqKsjiPzA?P2zh4jxQVm;07eyMCq?+ zYV8fOwUa-ksVNIq0FcPLSd^mM+Od*DToGaEjy4EpBb~T5rBg*r>d@@<%gYepB7JF0 zqq_ZhTTbt4e99^Fl9eP{muC7SQ?As-T7ZUt1HXW7`egSV>XAs%dT2+~9d#BA(P*)j z(D0-Wc6F=Xq;Ez*19MWIbzS0osT7X6ozKk>&N3dh)U{&Re&fo8P%s&w0N~{VcrIKq z2x~}d4RQmV;DFu^gWmrCgjZEc@Ze_b(=Aoh-_d=XJZ8+v)lKA~z%BN~S86rpYMt?0u zFkC3)tSau`mnY25=B{g)dM3owSbf=Vw0-aZz2T~fTHkJ@F6pcLmqLRS&;Cv>BWR!I z+RPN!dq?ck6I|axSa+4|B7lNzG$S`NkbWu8%wj3c6P@vb)pL!5@#dmB^TW{?vN zBGfH=G3iH<==4p-aBGihHuu9{(zqJ-xB51NXv62_%8>>T@awGan@0K^3i((iN+xo%#ouHt&I7j7;gA~ zaM|kYft59-NVWnweEfy~obuR!X;>4?FtKhWk9LQc+ATc4!u|H0t|p=C4#)UkIs1b* zU~l>r;^|xj-Rs_fd+|eK`kzi4=TVAO6y`vwsAMd7!IrRlB4EwT%Irr}B_*-~AjVn(;{-)H)yq<^yYV&jM#%$9>QVh%U z%UJHItG`bwI}ZY}o0&klMYUn~qrAe2#ymoOtW#gr9Dz%HofNmdj`t8&#p7n_pOvuZ z=F?L2tj|bH_{0E*sl4L|c!c+jV` zeXy-WWIHdBNTLXs_UIBMDy?(d=pS%;{K~$(^7C(zfUTgQC0m#>rkYzG!&0srmo+zP z%uIJAf4!wl*sSe43silWOG&I~w3z|7=J<^(E=H1Jo*@0}lR+k4a4{Z+7t=?Z<-^16 z#HH**nNR$)vabV9KteLt5UV>qWh13{~`%a65CIDvwi8xxILa6 z!t*MO@WlMq>6uJgtPOPTCQBxzspEI!dI! CCsuy| literal 0 HcmV?d00001 diff --git a/apps/meteor/public/images/featurePreview/resizable-contextual-bar.png b/apps/meteor/public/images/featurePreview/resizable-contextual-bar.png new file mode 100644 index 0000000000000000000000000000000000000000..c36c7e44a29d8c27c9c035a77026f1cd7e7dabd0 GIT binary patch literal 4776 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 a2K(3rHj$^VY%~U(889)lFep3k9Q`kU29J{f literal 0 HcmV?d00001 diff --git a/apps/meteor/public/images/featurePreview/timestamp.png b/apps/meteor/public/images/featurePreview/timestamp.png new file mode 100644 index 0000000000000000000000000000000000000000..7573f97db55b73e2d880af67e6705e3c4a2040c7 GIT binary patch literal 51432 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

        &c?(AAUaFYNC38B5NhzYmJ_RR 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 p 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 i18nLabel: 'Sidebar_Sections_Order', i18nDescription: 'Sidebar_Sections_Order_Description', }); + + await this.add('Accounts_Default_User_Preferences_featuresPreview', '[]', { + type: 'string', + public: true, + }); }); await this.section('Avatar', async function () { diff --git a/apps/meteor/tests/end-to-end/api/miscellaneous.ts b/apps/meteor/tests/end-to-end/api/miscellaneous.ts index b8341f7c0994..d933f1f3c4b3 100644 --- a/apps/meteor/tests/end-to-end/api/miscellaneous.ts +++ b/apps/meteor/tests/end-to-end/api/miscellaneous.ts @@ -186,6 +186,7 @@ describe('miscellaneous', () => { 'muteFocusedConversations', 'notifyCalendarEvents', 'enableMobileRinging', + 'featuresPreview', ].filter((p) => Boolean(p)); expect(res.body).to.have.property('success', true); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 8ce6bea2e117..27978d72e7dd 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -85,6 +85,7 @@ "Accounts_AllowEmailChange": "Allow Email Change", "Accounts_AllowEmailNotifications": "Allow Email Notifications", "Accounts_AllowFeaturePreview": "Allow Feature Preview", + "Accounts_AllowFeaturePreview_Description": "Make feature preview available to all workspace members.", "Accounts_AllowPasswordChange": "Allow Password Change", "Accounts_AllowPasswordChangeForOAuthUsers": "Allow Password Change for OAuth Users", "Accounts_AllowRealNameChange": "Allow Name Change", @@ -1125,8 +1126,8 @@ "Common_Access": "Common Access", "Commit": "Commit", "Community": "Community", - "Contextualbar_resizable": "Contextual bar resizable", - "Contextualbar_resizable_description": "Allows you to adjust the size of the contextual bar by simply dragging, giving you instant customization and flexibility", + "Contextualbar_resizable": "Resizable contextual bar", + "Contextualbar_resizable_description": "Adjust the size of the contextual bar by clicking and dragging the edge, giving you instant customization and flexibility.", "Free_Edition": "Free edition", "Composer_not_available_phone_calls": "Messages are not available on phone calls", "Condensed": "Condensed", @@ -1949,8 +1950,8 @@ "Enable_Password_History": "Enable Password History", "Enable_Password_History_Description": "When enabled, users won't be able to update their passwords to some of their most recently used passwords.", "Enable_Svg_Favicon": "Enable SVG favicon", - "Enable_timestamp": "Enable timestamp parsing in messages", - "Enable_timestamp_description": "Enable timestamps to be parsed in messages", + "Enable_timestamp": "Timestamp in messages", + "Enable_timestamp_description": "Render Unix timestamps inside messages in your local (system) timezone.", "Enable_to_bypass_email_verification": "Enable to bypass email verification", "Enable_two-factor_authentication": "Enable two-factor authentication via TOTP", "Enable_two-factor_authentication_email": "Enable two-factor authentication via Email", @@ -2284,7 +2285,10 @@ "Favorite_Rooms": "Enable Favorite Rooms", "Favorites": "Favorites", "Feature_preview": "Feature preview", - "Feature_preview_page_description": "Welcome to the features preview page! Here, you can enable the latest cutting-edge features that are currently under development and not yet officially released.\n\nPlease note that these configurations are still in the testing phase and may not be stable or fully functional.", + "Feature_preview_page_description": "Enable the latest features that are currently under development.", + "Feature_preview_page_callout": "Feature previews are being tested and may not be stable or fully functional. Features may become premium capabilities once officially released.", + "Feature_preview_admin_page_description": "Choose what feature previews to make available to workspace members.", + "Feature_preview_admin_page_callout": "Features enabled here will be enabled to each user in their feature preview preferences.", "featured": "featured", "Featured": "Featured", "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "This feature depends on the above selected call provider to be enabled from the administration settings (Admin -> Video Conference).", @@ -4363,7 +4367,7 @@ "Queue_Time": "Queue Time", "Queue_management": "Queue Management", "Quick_reactions": "Quick reactions", - "Quick_reactions_description": "The three most used reactions get an easy access while your mouse is over the message", + "Quick_reactions_description": "Easily access your most used and most recent emoji message reactions by hovering on a message.", "quote": "quote", "Quote": "Quote", "Random": "Random", diff --git a/packages/i18n/src/locales/hi-IN.i18n.json b/packages/i18n/src/locales/hi-IN.i18n.json index 090e081e83fa..3110d6e82a67 100644 --- a/packages/i18n/src/locales/hi-IN.i18n.json +++ b/packages/i18n/src/locales/hi-IN.i18n.json @@ -2169,7 +2169,6 @@ "Favorite_Rooms": "पसंदीदा कमरे सक्षम करें", "Favorites": "पसंदीदा", "Feature_preview": "फ़ीचर पूर्वावलोकन", - "Feature_preview_page_description": "फीचर पूर्वावलोकन पृष्ठ पर आपका स्वागत है! यहां, आप नवीनतम अत्याधुनिक सुविधाओं को सक्षम कर सकते हैं जो वर्तमान में विकास के अधीन हैं और अभी तक आधिकारिक तौर पर जारी नहीं की गई हैं।\n\nकृपया ध्यान दें कि ये कॉन्फ़िगरेशन अभी भी परीक्षण चरण में हैं और स्थिर या पूरी तरह कार्यात्मक नहीं हो सकते हैं।", "featured": "प्रदर्शित", "Featured": "प्रदर्शित", "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "यह सुविधा प्रशासन सेटिंग्स (एडमिन -> वीडियो कॉन्फ्रेंस) से सक्षम होने के लिए उपरोक्त चयनित कॉल प्रदाता पर निर्भर करती है।", @@ -4128,7 +4127,6 @@ "Queue_Time": "कतार समय", "Queue_management": "कतार प्रबंधन", "Quick_reactions": "त्वरित प्रतिक्रियाएँ", - "Quick_reactions_description": "जब आपका माउस संदेश पर होता है तो सबसे अधिक उपयोग की जाने वाली तीन प्रतिक्रियाओं तक आसान पहुंच मिलती है", "quote": "उद्धरण", "Quote": "उद्धरण", "Random": "Random", @@ -4987,7 +4985,7 @@ "The_application_will_be_able_to": "<1>{{appName}} यह करने में सक्षम होगा:", "The_channel_name_is_required": "चैनल का नाम आवश्यक है", "The_emails_are_being_sent": "ईमेल भेजे जा रहे हैं.", - "The_empty_room__roomName__will_be_removed_automatically": "खाली कमरा {{roomName}} स्वचालित रूप से हटा दिया जाएगा।", + "The_empty_room__roomName__will_be_removed_automatically": "खाली कमरा {{roomName}} स्वचालित रूप से हटा दिया जाएगा।", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "छवि का आकार बदलना काम नहीं करेगा क्योंकि हम आपके सर्वर पर स्थापित ImageMagick या ग्राफ़िक्सMagick का पता नहीं लगा सकते हैं।", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "संदेश एक चर्चा है आप संदेशों को पुनर्प्राप्त नहीं कर पाएंगे!", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "मोबाइल सूचनाएं सभी उपयोगकर्ताओं के लिए अक्षम कर दी गई थीं, पुश गेटवे को फिर से सक्षम करने के लिए \"एडमिन > पुश\" पर जाएं", @@ -6134,4 +6132,4 @@ "Unlimited_seats": "असीमित सीटें", "Unlimited_MACs": "असीमित एमएसी", "Unlimited_seats_MACs": "असीमित सीटें और एमएसी" -} \ No newline at end of file +} diff --git a/packages/ui-client/src/components/FeaturePreview/FeaturePreviewBadge.tsx b/packages/ui-client/src/components/FeaturePreview/FeaturePreviewBadge.tsx new file mode 100644 index 000000000000..eece30cc7280 --- /dev/null +++ b/packages/ui-client/src/components/FeaturePreview/FeaturePreviewBadge.tsx @@ -0,0 +1,21 @@ +import { Badge } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; + +import { usePreferenceFeaturePreviewList } from '../../hooks/usePreferenceFeaturePreviewList'; + +const FeaturePreviewBadge = () => { + const t = useTranslation(); + const { unseenFeatures } = usePreferenceFeaturePreviewList(); + + if (!unseenFeatures) { + return null; + } + + return ( + + {unseenFeatures} + + ); +}; + +export default FeaturePreviewBadge; diff --git a/packages/ui-client/src/components/FeaturePreview/index.ts b/packages/ui-client/src/components/FeaturePreview/index.ts new file mode 100644 index 000000000000..f6b8e5f2071e --- /dev/null +++ b/packages/ui-client/src/components/FeaturePreview/index.ts @@ -0,0 +1,2 @@ +export { FeaturePreview, FeaturePreviewOn, FeaturePreviewOff } from './FeaturePreview'; +export { default as FeaturePreviewBadge } from './FeaturePreviewBadge'; diff --git a/packages/ui-client/src/components/index.ts b/packages/ui-client/src/components/index.ts index 8642983229aa..7308c8e75431 100644 --- a/packages/ui-client/src/components/index.ts +++ b/packages/ui-client/src/components/index.ts @@ -11,7 +11,7 @@ export * as UserStatus from './UserStatus'; export * from './Header'; export * from './HeaderV2'; export * from './MultiSelectCustom/MultiSelectCustom'; -export * from './FeaturePreview/FeaturePreview'; +export * from './FeaturePreview'; export * from './RoomBanner'; export { default as UserAutoComplete } from './UserAutoComplete'; export * from './GenericMenu'; diff --git a/packages/ui-client/src/hooks/useDefaultSettingFeaturePreviewList.ts b/packages/ui-client/src/hooks/useDefaultSettingFeaturePreviewList.ts new file mode 100644 index 000000000000..373862379cc1 --- /dev/null +++ b/packages/ui-client/src/hooks/useDefaultSettingFeaturePreviewList.ts @@ -0,0 +1,12 @@ +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { parseSetting, useFeaturePreviewList } from './useFeaturePreviewList'; + +export const useDefaultSettingFeaturePreviewList = () => { + const featurePreviewSettingJSON = useSetting('Accounts_Default_User_Preferences_featuresPreview'); + + const settingFeaturePreview = useMemo(() => parseSetting(featurePreviewSettingJSON), [featurePreviewSettingJSON]); + + return useFeaturePreviewList(settingFeaturePreview ?? []); +}; diff --git a/packages/ui-client/src/hooks/useFeaturePreview.ts b/packages/ui-client/src/hooks/useFeaturePreview.ts index 4bdda9c9251a..bd46adfdefff 100644 --- a/packages/ui-client/src/hooks/useFeaturePreview.ts +++ b/packages/ui-client/src/hooks/useFeaturePreview.ts @@ -1,7 +1,8 @@ -import { type FeaturesAvailable, useFeaturePreviewList } from './useFeaturePreviewList'; +import { type FeaturesAvailable } from './useFeaturePreviewList'; +import { usePreferenceFeaturePreviewList } from './usePreferenceFeaturePreviewList'; export const useFeaturePreview = (featureName: FeaturesAvailable) => { - const { features } = useFeaturePreviewList(); + const { features } = usePreferenceFeaturePreviewList(); const currentFeature = features?.find((feature) => feature.name === featureName); diff --git a/packages/ui-client/src/hooks/useFeaturePreviewList.ts b/packages/ui-client/src/hooks/useFeaturePreviewList.ts index ff103a8d84ef..08bda4ff81ff 100644 --- a/packages/ui-client/src/hooks/useFeaturePreviewList.ts +++ b/packages/ui-client/src/hooks/useFeaturePreviewList.ts @@ -1,5 +1,4 @@ import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useUserPreference, useSetting } from '@rocket.chat/ui-contexts'; export type FeaturesAvailable = | 'quickReactions' @@ -24,6 +23,7 @@ export type FeaturePreviewProps = { }; }; +// TODO: Move the features preview array to another directory to be accessed from both BE and FE. export const defaultFeaturesPreview: FeaturePreviewProps[] = [ { name: 'quickReactions', @@ -47,6 +47,7 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ i18n: 'Enable_timestamp', description: 'Enable_timestamp_description', group: 'Message', + imageUrl: 'images/featurePreview/timestamp.png', value: false, enabled: true, }, @@ -55,6 +56,7 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ i18n: 'Contextualbar_resizable', description: 'Contextualbar_resizable_description', group: 'Navigation', + imageUrl: 'images/featurePreview/resizable-contextual-bar.png', value: false, enabled: true, }, @@ -63,6 +65,7 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ i18n: 'New_navigation', description: 'New_navigation_description', group: 'Navigation', + imageUrl: 'images/featurePreview/enhanced-navigation.png', value: false, enabled: true, }, @@ -82,22 +85,27 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ export const enabledDefaultFeatures = defaultFeaturesPreview.filter((feature) => feature.enabled); -export const useFeaturePreviewList = () => { - const featurePreviewEnabled = useSetting('Accounts_AllowFeaturePreview'); - const userFeaturesPreview = useUserPreference('featuresPreview'); - - if (!featurePreviewEnabled) { - return { unseenFeatures: 0, features: [] as FeaturePreviewProps[], featurePreviewEnabled }; +// TODO: Remove this logic after we have a way to store object settings. +export const parseSetting = (setting?: FeaturePreviewProps[] | string) => { + if (typeof setting === 'string') { + try { + return JSON.parse(setting) as FeaturePreviewProps[]; + } catch (_) { + return; + } } + return setting; +}; +export const useFeaturePreviewList = (featuresList: Pick[]) => { const unseenFeatures = enabledDefaultFeatures.filter( - (feature) => !userFeaturesPreview?.find((userFeature) => userFeature.name === feature.name), + (defaultFeature) => !featuresList?.find((feature) => feature.name === defaultFeature.name), ).length; - const mergedFeatures = enabledDefaultFeatures.map((feature) => { - const userFeature = userFeaturesPreview?.find((userFeature) => userFeature.name === feature.name); - return { ...feature, ...userFeature }; + const mergedFeatures = enabledDefaultFeatures.map((defaultFeature) => { + const features = featuresList?.find((feature) => feature.name === defaultFeature.name); + return { ...defaultFeature, ...features }; }); - return { unseenFeatures, features: mergedFeatures, featurePreviewEnabled }; + return { unseenFeatures, features: mergedFeatures }; }; diff --git a/packages/ui-client/src/hooks/useFeaturePreviewList.spec.tsx b/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.spec.tsx similarity index 79% rename from packages/ui-client/src/hooks/useFeaturePreviewList.spec.tsx rename to packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.spec.tsx index e348cfb6a864..ac3d6f92d51a 100644 --- a/packages/ui-client/src/hooks/useFeaturePreviewList.spec.tsx +++ b/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.spec.tsx @@ -1,10 +1,11 @@ import { mockAppRoot } from '@rocket.chat/mock-providers'; import { renderHook } from '@testing-library/react'; -import { useFeaturePreviewList, enabledDefaultFeatures } from './useFeaturePreviewList'; +import { enabledDefaultFeatures } from './useFeaturePreviewList'; +import { usePreferenceFeaturePreviewList } from './usePreferenceFeaturePreviewList'; it('should return the number of unseen features and Accounts_AllowFeaturePreview enabled ', () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot().withSetting('Accounts_AllowFeaturePreview', true).build(), }); @@ -18,7 +19,7 @@ it('should return the number of unseen features and Accounts_AllowFeaturePreview }); it('should return the number of unseen features and Accounts_AllowFeaturePreview disabled ', () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot().withSetting('Accounts_AllowFeaturePreview', false).build(), }); @@ -32,7 +33,7 @@ it('should return the number of unseen features and Accounts_AllowFeaturePreview }); it('should return 0 unseen features', () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot() .withSetting('Accounts_AllowFeaturePreview', true) @@ -49,7 +50,7 @@ it('should return 0 unseen features', () => { }); it('should ignore removed feature previews', () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot() .withSetting('Accounts_AllowFeaturePreview', true) @@ -72,7 +73,7 @@ it('should ignore removed feature previews', () => { }); it('should turn off ignored feature previews', async () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot() .withSetting('Accounts_AllowFeaturePreview', true) diff --git a/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.ts b/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.ts new file mode 100644 index 000000000000..d7c4c13417d2 --- /dev/null +++ b/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.ts @@ -0,0 +1,16 @@ +import { useSetting, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { FeaturePreviewProps, parseSetting, useFeaturePreviewList } from './useFeaturePreviewList'; + +export const usePreferenceFeaturePreviewList = () => { + const featurePreviewEnabled = useSetting('Accounts_AllowFeaturePreview'); + const userFeaturesPreviewPreference = useUserPreference('featuresPreview'); + const userFeaturesPreview = useMemo(() => parseSetting(userFeaturesPreviewPreference), [userFeaturesPreviewPreference]); + const { unseenFeatures, features } = useFeaturePreviewList(userFeaturesPreview ?? []); + + if (!featurePreviewEnabled) { + return { unseenFeatures: 0, features: [] as FeaturePreviewProps[], featurePreviewEnabled }; + } + return { unseenFeatures, features, featurePreviewEnabled }; +}; diff --git a/packages/ui-client/src/index.ts b/packages/ui-client/src/index.ts index 3e640343da5b..a96ef265aadc 100644 --- a/packages/ui-client/src/index.ts +++ b/packages/ui-client/src/index.ts @@ -1,5 +1,7 @@ export * from './components'; export * from './hooks/useFeaturePreview'; +export * from './hooks/useDefaultSettingFeaturePreviewList'; export * from './hooks/useFeaturePreviewList'; +export * from './hooks/usePreferenceFeaturePreviewList'; export * from './hooks/useDocumentTitle'; export * from './helpers'; From 519ed86b392219851c28b101b1219c22caf23c9e Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 24 Sep 2024 12:03:20 -0600 Subject: [PATCH 34/39] fix: Avoid destructuring `connectionData` when value is undefined (#33339) --- .changeset/brave-brooms-invent.md | 5 ++++ .../app/livechat/server/lib/LivechatTyped.ts | 8 ++++-- .../end-to-end/api/livechat/09-visitors.ts | 28 +++++++++++++++++-- 3 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 .changeset/brave-brooms-invent.md diff --git a/.changeset/brave-brooms-invent.md b/.changeset/brave-brooms-invent.md new file mode 100644 index 000000000000..35d32b485944 --- /dev/null +++ b/.changeset/brave-brooms-invent.md @@ -0,0 +1,5 @@ +--- +"@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/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index ade6726336ec..6c2d655f4c95 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -589,6 +589,10 @@ class LivechatClass { } } + isValidObject(obj: unknown): obj is Record { + return typeof obj === 'object' && obj !== null; + } + async registerGuest({ id, token, @@ -654,10 +658,10 @@ class LivechatClass { visitorDataToUpdate.status = status; visitorDataToUpdate.ts = new Date(); - if (settings.get('Livechat_Allow_collect_and_store_HTTP_header_informations')) { + if (settings.get('Livechat_Allow_collect_and_store_HTTP_header_informations') && Livechat.isValidObject(connectionData)) { Livechat.logger.debug(`Saving connection data for visitor ${token}`); const { httpHeaders, clientAddress } = connectionData; - if (httpHeaders) { + if (Livechat.isValidObject(httpHeaders)) { visitorDataToUpdate.userAgent = httpHeaders['user-agent']; visitorDataToUpdate.ip = httpHeaders['x-real-ip'] || httpHeaders['x-forwarded-for'] || clientAddress; visitorDataToUpdate.host = httpHeaders?.host; diff --git a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts index f02d9d1d1e95..31134412b8ec 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts @@ -1,11 +1,11 @@ import { faker } from '@faker-js/faker'; import type { ILivechatVisitor } from '@rocket.chat/core-typings'; import { expect } from 'chai'; -import { before, describe, it } from 'mocha'; +import { before, describe, it, after } from 'mocha'; import moment from 'moment'; import { type Response } from 'supertest'; -import { getCredentials, api, request, credentials } from '../../../data/api-data'; +import { getCredentials, api, request, credentials, methodCallAnon } from '../../../data/api-data'; import { createCustomField, deleteCustomField } from '../../../data/livechat/custom-fields'; import { makeAgentAvailable, @@ -217,6 +217,30 @@ describe('LIVECHAT - visitors', () => { expect(body.visitor).to.have.property('livechatData'); expect(body.visitor.livechatData).to.have.property(customFieldName, 'Not a real address :)'); }); + + describe('special cases', () => { + before(async () => { + await updateSetting('Livechat_Allow_collect_and_store_HTTP_header_informations', true); + }); + after(async () => { + await updateSetting('Livechat_Allow_collect_and_store_HTTP_header_informations', false); + }); + + // Note: this had to use the meteor method because the endpoint used `req.headers` which we cannot send as empty + // method doesn't pass them to the func allowing us to create a test for it + it('should allow to create a visitor without passing connectionData when GDPR setting is enabled', async () => { + const token = `${new Date().getTime()}-test`; + const response = await request + .post(methodCallAnon('livechat:registerGuest')) + .send({ message: `{"msg":"method","id":"23","method":"livechat:registerGuest","params":[{ "token": "${token}"}]}` }); + + expect(response.body).to.have.property('success', true); + const r = JSON.parse(response.body.message); + + expect(r.result).to.have.property('visitor'); + expect(r.result.visitor).to.have.property('token', token); + }); + }); }); describe('livechat/visitors.info', () => { From 2327ff58b2b06f127ac2d9fbc5b37ceb7202f752 Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Tue, 24 Sep 2024 21:16:36 +0000 Subject: [PATCH 35/39] Release 6.13.0-rc.1 [no ci] --- .changeset/bump-patch-1727212585363.md | 5 +++ .changeset/pre.json | 3 ++ apps/meteor/CHANGELOG.md | 39 +++++++++++++++++++ apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/ee/server/services/CHANGELOG.md | 14 +++++++ apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- apps/uikit-playground/CHANGELOG.md | 13 +++++++ apps/uikit-playground/package.json | 2 +- ee/apps/account-service/CHANGELOG.md | 14 +++++++ ee/apps/account-service/package.json | 2 +- ee/apps/authorization-service/CHANGELOG.md | 14 +++++++ ee/apps/authorization-service/package.json | 2 +- ee/apps/ddp-streamer/CHANGELOG.md | 15 +++++++ ee/apps/ddp-streamer/package.json | 2 +- ee/apps/omnichannel-transcript/CHANGELOG.md | 15 +++++++ ee/apps/omnichannel-transcript/package.json | 2 +- ee/apps/presence-service/CHANGELOG.md | 14 +++++++ ee/apps/presence-service/package.json | 2 +- ee/apps/queue-worker/CHANGELOG.md | 14 +++++++ ee/apps/queue-worker/package.json | 2 +- ee/apps/stream-hub-service/CHANGELOG.md | 12 ++++++ ee/apps/stream-hub-service/package.json | 2 +- ee/packages/license/CHANGELOG.md | 10 +++++ ee/packages/license/package.json | 2 +- ee/packages/omnichannel-services/CHANGELOG.md | 15 +++++++ ee/packages/omnichannel-services/package.json | 2 +- ee/packages/pdf-worker/CHANGELOG.md | 10 +++++ ee/packages/pdf-worker/package.json | 2 +- ee/packages/presence/CHANGELOG.md | 12 ++++++ ee/packages/presence/package.json | 2 +- package.json | 2 +- packages/api-client/CHANGELOG.md | 11 ++++++ packages/api-client/package.json | 2 +- packages/apps/CHANGELOG.md | 11 ++++++ packages/apps/package.json | 2 +- packages/core-services/CHANGELOG.md | 12 ++++++ packages/core-services/package.json | 2 +- packages/core-typings/CHANGELOG.md | 3 ++ packages/core-typings/package.json | 2 +- packages/cron/CHANGELOG.md | 11 ++++++ packages/cron/package.json | 2 +- packages/ddp-client/CHANGELOG.md | 12 ++++++ packages/ddp-client/package.json | 2 +- packages/fuselage-ui-kit/CHANGELOG.md | 14 +++++++ packages/fuselage-ui-kit/package.json | 8 ++-- packages/gazzodown/CHANGELOG.md | 12 ++++++ packages/gazzodown/package.json | 6 +-- packages/i18n/CHANGELOG.md | 6 +++ packages/i18n/package.json | 2 +- packages/instance-status/CHANGELOG.md | 10 +++++ packages/instance-status/package.json | 2 +- packages/livechat/CHANGELOG.md | 10 +++++ packages/livechat/package.json | 2 +- packages/mock-providers/CHANGELOG.md | 9 +++++ packages/mock-providers/package.json | 2 +- packages/model-typings/CHANGELOG.md | 10 +++++ packages/model-typings/package.json | 2 +- packages/models/CHANGELOG.md | 10 +++++ packages/models/package.json | 2 +- packages/rest-typings/CHANGELOG.md | 10 +++++ packages/rest-typings/package.json | 2 +- packages/ui-avatar/CHANGELOG.md | 9 +++++ packages/ui-avatar/package.json | 4 +- packages/ui-client/CHANGELOG.md | 15 +++++++ packages/ui-client/package.json | 6 +-- packages/ui-contexts/CHANGELOG.md | 13 +++++++ packages/ui-contexts/package.json | 2 +- packages/ui-video-conf/CHANGELOG.md | 11 ++++++ packages/ui-video-conf/package.json | 6 +-- packages/web-ui-registration/CHANGELOG.md | 9 +++++ packages/web-ui-registration/package.json | 4 +- 72 files changed, 474 insertions(+), 47 deletions(-) create mode 100644 .changeset/bump-patch-1727212585363.md diff --git a/.changeset/bump-patch-1727212585363.md b/.changeset/bump-patch-1727212585363.md new file mode 100644 index 000000000000..e1eaa7980afb --- /dev/null +++ b/.changeset/bump-patch-1727212585363.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/pre.json b/.changeset/pre.json index 6976ce2b60aa..7c415a6b0dde 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -63,7 +63,9 @@ "@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", @@ -80,6 +82,7 @@ "mighty-drinks-hide", "nasty-tools-enjoy", "pink-swans-teach", + "quick-rings-wave", "quiet-cherries-punch", "rich-toes-bow", "rotten-rabbits-brush", diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index ee5d553d85a5..c31645ca4ea4 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,44 @@ # @rocket.chat/meteor +## 6.13.0-rc.1 + +### Minor Changes + +- ([#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. + +### 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. + +-

        Updated dependencies [2f9eea03d2]: + + - @rocket.chat/i18n@0.8.0-rc.1 + - @rocket.chat/ui-client@11.0.0-rc.1 + - @rocket.chat/ui-contexts@11.0.0-rc.1 + - @rocket.chat/web-ui-registration@11.0.0-rc.1 + - @rocket.chat/gazzodown@11.0.0-rc.1 + - @rocket.chat/fuselage-ui-kit@11.0.0-rc.1 + - @rocket.chat/ui-theming@0.3.0-rc.0 + - @rocket.chat/ui-avatar@7.0.0-rc.1 + - @rocket.chat/ui-video-conf@11.0.0-rc.1 + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/rest-typings@6.13.0-rc.1 + - @rocket.chat/license@0.2.8-rc.1 + - @rocket.chat/omnichannel-services@0.3.5-rc.1 + - @rocket.chat/pdf-worker@0.2.5-rc.1 + - @rocket.chat/presence@0.2.8-rc.1 + - @rocket.chat/api-client@0.2.8-rc.1 + - @rocket.chat/apps@0.1.8-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/cron@0.1.8-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.3.0-rc.1 + - @rocket.chat/instance-status@0.1.8-rc.1 +
        + ## 6.13.0-rc.0 ### Minor Changes diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index fc41ef05cacd..cfec7fa33824 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "6.13.0-rc.0" + "version": "6.13.0-rc.1" } diff --git a/apps/meteor/ee/server/services/CHANGELOG.md b/apps/meteor/ee/server/services/CHANGELOG.md index fb0072738db2..59da7bfded9c 100644 --- a/apps/meteor/ee/server/services/CHANGELOG.md +++ b/apps/meteor/ee/server/services/CHANGELOG.md @@ -1,5 +1,18 @@ # rocketchat-services +## 1.3.5-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/rest-typings@6.13.0-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 1.3.5-rc.0 ### Patch Changes @@ -13,6 +26,7 @@ - @rocket.chat/message-parser@0.31.30-rc.0 - @rocket.chat/models@0.3.0-rc.0 + ## 1.3.4 ### Patch Changes diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 2cab26ca2d38..540a283be188 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -1,7 +1,7 @@ { "name": "rocketchat-services", "private": true, - "version": "1.3.5-rc.0", + "version": "1.3.5-rc.1", "description": "Rocket.Chat Authorization service", "main": "index.js", "scripts": { diff --git a/apps/meteor/package.json b/apps/meteor/package.json index a30ce30a7b6f..a36621eac547 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "6.13.0-rc.0", + "version": "6.13.0-rc.1", "private": true, "author": { "name": "Rocket.Chat", diff --git a/apps/uikit-playground/CHANGELOG.md b/apps/uikit-playground/CHANGELOG.md index d1528ca7502d..d2793509ff54 100644 --- a/apps/uikit-playground/CHANGELOG.md +++ b/apps/uikit-playground/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/uikit-playground +## 0.5.0-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/ui-contexts@11.0.0-rc.1 + - @rocket.chat/fuselage-ui-kit@11.0.0-rc.1 + - @rocket.chat/ui-avatar@7.0.0-rc.1 + - @rocket.chat/core-typings@6.13.0-rc.1 +
        + ## 0.5.0-rc.0 ### Minor Changes @@ -15,6 +27,7 @@ - @rocket.chat/ui-avatar@7.0.0-rc.0 - @rocket.chat/ui-contexts@11.0.0-rc.0 + ## 0.4.1 ### Patch Changes diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index c5e7628001c6..82ebd11ee8dc 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/uikit-playground", "private": true, - "version": "0.5.0-rc.0", + "version": "0.5.0-rc.1", "type": "module", "scripts": { "dev": "vite", diff --git a/ee/apps/account-service/CHANGELOG.md b/ee/apps/account-service/CHANGELOG.md index 929082c7bb9a..4c86f21b24c4 100644 --- a/ee/apps/account-service/CHANGELOG.md +++ b/ee/apps/account-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/account-service +## 0.4.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/rest-typings@6.13.0-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.4.8-rc.0 ### Patch Changes @@ -12,6 +25,7 @@ - @rocket.chat/core-services@0.7.0-rc.0 - @rocket.chat/models@0.3.0-rc.0 + ## 0.4.7 ### Patch Changes diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index f2c7dd0a8761..36ea1d9294c6 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/account-service", "private": true, - "version": "0.4.8-rc.0", + "version": "0.4.8-rc.1", "description": "Rocket.Chat Account service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/authorization-service/CHANGELOG.md b/ee/apps/authorization-service/CHANGELOG.md index 5be055a875d0..c587761cdb38 100644 --- a/ee/apps/authorization-service/CHANGELOG.md +++ b/ee/apps/authorization-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/authorization-service +## 0.4.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/rest-typings@6.13.0-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.4.8-rc.0 ### Patch Changes @@ -12,6 +25,7 @@ - @rocket.chat/core-services@0.7.0-rc.0 - @rocket.chat/models@0.3.0-rc.0 + ## 0.4.7 ### Patch Changes diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index eaa51c88691c..255b76d2db2e 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/authorization-service", "private": true, - "version": "0.4.8-rc.0", + "version": "0.4.8-rc.1", "description": "Rocket.Chat Authorization service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/ddp-streamer/CHANGELOG.md b/ee/apps/ddp-streamer/CHANGELOG.md index 4b61203020bf..975b9af1abf2 100644 --- a/ee/apps/ddp-streamer/CHANGELOG.md +++ b/ee/apps/ddp-streamer/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/ddp-streamer +## 0.3.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/rest-typings@6.13.0-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 + - @rocket.chat/instance-status@0.1.8-rc.1 +
        + ## 0.3.8-rc.0 ### Patch Changes @@ -13,6 +27,7 @@ - @rocket.chat/models@0.3.0-rc.0 - @rocket.chat/instance-status@0.1.7-rc.0 + ## 0.3.7 ### Patch Changes diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 16dcc7b6f507..7fd25289ca55 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/ddp-streamer", "private": true, - "version": "0.3.8-rc.0", + "version": "0.3.8-rc.1", "description": "Rocket.Chat DDP-Streamer service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/omnichannel-transcript/CHANGELOG.md b/ee/apps/omnichannel-transcript/CHANGELOG.md index b934e4037e8a..613d6ef3f377 100644 --- a/ee/apps/omnichannel-transcript/CHANGELOG.md +++ b/ee/apps/omnichannel-transcript/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/omnichannel-transcript +## 0.4.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/omnichannel-services@0.3.5-rc.1 + - @rocket.chat/pdf-worker@0.2.5-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.4.8-rc.0 ### Patch Changes @@ -13,6 +27,7 @@ - @rocket.chat/omnichannel-services@0.3.4-rc.0 - @rocket.chat/pdf-worker@0.2.4-rc.0 + ## 0.4.7 ### Patch Changes diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 2f83816f4650..b0f97ce6d562 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/omnichannel-transcript", "private": true, - "version": "0.4.8-rc.0", + "version": "0.4.8-rc.1", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/presence-service/CHANGELOG.md b/ee/apps/presence-service/CHANGELOG.md index 78b716888236..795b6a2df283 100644 --- a/ee/apps/presence-service/CHANGELOG.md +++ b/ee/apps/presence-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/presence-service +## 0.4.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/presence@0.2.8-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.4.8-rc.0 ### Patch Changes @@ -12,6 +25,7 @@ - @rocket.chat/models@0.3.0-rc.0 - @rocket.chat/presence@0.2.7-rc.0 + ## 0.4.7 ### Patch Changes diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index 7c21dbbdcbec..d1eb50267c52 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/presence-service", "private": true, - "version": "0.4.8-rc.0", + "version": "0.4.8-rc.1", "description": "Rocket.Chat Presence service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/queue-worker/CHANGELOG.md b/ee/apps/queue-worker/CHANGELOG.md index 9ac7c4bed05f..cca63889589c 100644 --- a/ee/apps/queue-worker/CHANGELOG.md +++ b/ee/apps/queue-worker/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/queue-worker +## 0.4.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/omnichannel-services@0.3.5-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.4.8-rc.0 ### Patch Changes @@ -12,6 +25,7 @@ - @rocket.chat/models@0.3.0-rc.0 - @rocket.chat/omnichannel-services@0.3.4-rc.0 + ## 0.4.7 ### Patch Changes diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index b800469a8274..634d49159ea8 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/queue-worker", "private": true, - "version": "0.4.8-rc.0", + "version": "0.4.8-rc.1", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/stream-hub-service/CHANGELOG.md b/ee/apps/stream-hub-service/CHANGELOG.md index 550a6a251252..05843d5502b1 100644 --- a/ee/apps/stream-hub-service/CHANGELOG.md +++ b/ee/apps/stream-hub-service/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/stream-hub-service +## 0.4.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.4.8-rc.0 ### Patch Changes diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index 1ffac9a8775c..dca9ad443b3d 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/stream-hub-service", "private": true, - "version": "0.4.8-rc.0", + "version": "0.4.8-rc.1", "description": "Rocket.Chat Stream Hub service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/packages/license/CHANGELOG.md b/ee/packages/license/CHANGELOG.md index 05c4edfb5e77..92b07995bb4b 100644 --- a/ee/packages/license/CHANGELOG.md +++ b/ee/packages/license/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/license +## 0.2.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 +
        + ## 0.2.8-rc.0 ### Patch Changes @@ -8,6 +17,7 @@ - @rocket.chat/core-typings@6.13.0-rc.0 + ## 0.2.7 ### Patch Changes diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 24b5676b8fc0..30a89b4334bd 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/license", - "version": "0.2.8-rc.0", + "version": "0.2.8-rc.1", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/omnichannel-services/CHANGELOG.md b/ee/packages/omnichannel-services/CHANGELOG.md index 3cfb53628c15..339277317d7f 100644 --- a/ee/packages/omnichannel-services/CHANGELOG.md +++ b/ee/packages/omnichannel-services/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/omnichannel-services +## 0.3.5-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/rest-typings@6.13.0-rc.1 + - @rocket.chat/pdf-worker@0.2.5-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.3.5-rc.0 ### Patch Changes @@ -13,6 +27,7 @@ - @rocket.chat/models@0.3.0-rc.0 - @rocket.chat/pdf-worker@0.2.4-rc.0 + ## 0.3.4 ### Patch Changes diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index 227ac2927974..98f507325586 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/omnichannel-services", - "version": "0.3.5-rc.0", + "version": "0.3.5-rc.1", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/pdf-worker/CHANGELOG.md b/ee/packages/pdf-worker/CHANGELOG.md index 785607cc6856..60f03a15a1de 100644 --- a/ee/packages/pdf-worker/CHANGELOG.md +++ b/ee/packages/pdf-worker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/pdf-worker +## 0.2.5-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 +
        + ## 0.2.5-rc.0 ### Patch Changes @@ -8,6 +17,7 @@ - @rocket.chat/core-typings@6.13.0-rc.0 + ## 0.2.4 ### Patch Changes diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index dbc1f4483d23..570b3baab2e2 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/pdf-worker", - "version": "0.2.5-rc.0", + "version": "0.2.5-rc.1", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/presence/CHANGELOG.md b/ee/packages/presence/CHANGELOG.md index 428dcbca5933..7b8bc5b28116 100644 --- a/ee/packages/presence/CHANGELOG.md +++ b/ee/packages/presence/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/presence +## 0.2.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.2.8-rc.0 ### Patch Changes @@ -10,6 +21,7 @@ - @rocket.chat/core-services@0.7.0-rc.0 - @rocket.chat/models@0.3.0-rc.0 + ## 0.2.7 ### Patch Changes diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index a0a9d059dead..912b4bf453fd 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/presence", - "version": "0.2.8-rc.0", + "version": "0.2.8-rc.1", "private": true, "devDependencies": { "@babel/core": "~7.22.20", diff --git a/package.json b/package.json index 2f89cab55d2a..1a87fa6034bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "6.13.0-rc.0", + "version": "6.13.0-rc.1", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/api-client/CHANGELOG.md b/packages/api-client/CHANGELOG.md index 8158e1f876d5..18b99f365728 100644 --- a/packages/api-client/CHANGELOG.md +++ b/packages/api-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/api-client +## 0.2.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/rest-typings@6.13.0-rc.1 +
        + ## 0.2.8-rc.0 ### Patch Changes @@ -9,6 +19,7 @@ - @rocket.chat/rest-typings@6.13.0-rc.0 - @rocket.chat/core-typings@6.13.0-rc.0 + ## 0.2.7 ### Patch Changes diff --git a/packages/api-client/package.json b/packages/api-client/package.json index 10492ffedab4..b87cdb6600d9 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/api-client", - "version": "0.2.8-rc.0", + "version": "0.2.8-rc.1", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.13", diff --git a/packages/apps/CHANGELOG.md b/packages/apps/CHANGELOG.md index 10681302f29e..fcf1229c306a 100644 --- a/packages/apps/CHANGELOG.md +++ b/packages/apps/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/apps +## 0.1.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 +
        + ## 0.1.8-rc.0 ### Patch Changes @@ -9,6 +19,7 @@ - @rocket.chat/model-typings@0.8.0-rc.0 - @rocket.chat/core-typings@6.13.0-rc.0 + ## 0.1.7 ### Patch Changes diff --git a/packages/apps/package.json b/packages/apps/package.json index 7dc954e5c3db..af88aff27664 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps", - "version": "0.1.8-rc.0", + "version": "0.1.8-rc.1", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/core-services/CHANGELOG.md b/packages/core-services/CHANGELOG.md index b1c5158d51a4..aafc8e1f0c6c 100644 --- a/packages/core-services/CHANGELOG.md +++ b/packages/core-services/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/core-services +## 0.7.0-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/rest-typings@6.13.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.7.0-rc.0 ### Minor Changes @@ -19,6 +30,7 @@ - @rocket.chat/message-parser@0.31.30-rc.0 - @rocket.chat/models@0.3.0-rc.0 + ## 0.6.1 ### Patch Changes diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 7326f3cd3617..2ee07033e732 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/core-services", - "version": "0.7.0-rc.0", + "version": "0.7.0-rc.1", "private": true, "devDependencies": { "@babel/core": "~7.22.20", diff --git a/packages/core-typings/CHANGELOG.md b/packages/core-typings/CHANGELOG.md index 4d686538954f..e7171dedd7b6 100644 --- a/packages/core-typings/CHANGELOG.md +++ b/packages/core-typings/CHANGELOG.md @@ -1,5 +1,7 @@ # @rocket.chat/core-typings +## 6.13.0-rc.1 + ## 6.13.0-rc.0 ### Minor Changes @@ -16,6 +18,7 @@ - @rocket.chat/message-parser@0.31.30-rc.0 + ## 6.12.1 ### Patch Changes diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 2c5cb3f64a2d..6fcd5a43d20d 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", - "version": "6.13.0-rc.0", + "version": "6.13.0-rc.1", "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", "eslint": "~8.45.0", diff --git a/packages/cron/CHANGELOG.md b/packages/cron/CHANGELOG.md index 27aa9beb2b65..297acb29b0cf 100644 --- a/packages/cron/CHANGELOG.md +++ b/packages/cron/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/cron +## 0.1.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.1.8-rc.0 ### Patch Changes @@ -9,6 +19,7 @@ - @rocket.chat/core-typings@6.13.0-rc.0 - @rocket.chat/models@0.3.0-rc.0 + ## 0.1.7 ### Patch Changes diff --git a/packages/cron/package.json b/packages/cron/package.json index 798e07348d3b..f9ac01ce7740 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/cron", - "version": "0.1.8-rc.0", + "version": "0.1.8-rc.1", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/ddp-client/CHANGELOG.md b/packages/ddp-client/CHANGELOG.md index ac4b3f76b11c..5e81a05f3804 100644 --- a/packages/ddp-client/CHANGELOG.md +++ b/packages/ddp-client/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ddp-client +## 0.3.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/rest-typings@6.13.0-rc.1 + - @rocket.chat/api-client@0.2.8-rc.1 +
        + ## 0.3.8-rc.0 ### Patch Changes @@ -10,6 +21,7 @@ - @rocket.chat/core-typings@6.13.0-rc.0 - @rocket.chat/api-client@0.2.7-rc.0 + ## 0.3.7 ### Patch Changes diff --git a/packages/ddp-client/package.json b/packages/ddp-client/package.json index 949fe4eebd65..c2dc8b95a4b9 100644 --- a/packages/ddp-client/package.json +++ b/packages/ddp-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ddp-client", - "version": "0.3.8-rc.0", + "version": "0.3.8-rc.1", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.13", diff --git a/packages/fuselage-ui-kit/CHANGELOG.md b/packages/fuselage-ui-kit/CHANGELOG.md index 895ffaf94ab4..14c2fea3c3c1 100644 --- a/packages/fuselage-ui-kit/CHANGELOG.md +++ b/packages/fuselage-ui-kit/CHANGELOG.md @@ -1,5 +1,18 @@ # Change Log +## 11.0.0-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/ui-contexts@11.0.0-rc.1 + - @rocket.chat/gazzodown@11.0.0-rc.1 + - @rocket.chat/ui-avatar@7.0.0-rc.1 + - @rocket.chat/ui-video-conf@11.0.0-rc.1 + - @rocket.chat/core-typings@6.13.0-rc.1 +
        + ## 11.0.0-rc.0 ### Minor Changes @@ -20,6 +33,7 @@ - @rocket.chat/ui-avatar@7.0.0-rc.0 - @rocket.chat/ui-contexts@11.0.0-rc.0 + ## 10.0.1 ### Patch Changes diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 573d65dc215e..975051db933f 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/fuselage-ui-kit", "private": true, - "version": "11.0.0-rc.0", + "version": "11.0.0-rc.1", "description": "UiKit elements for Rocket.Chat Apps built under Fuselage design system", "homepage": "https://rocketchat.github.io/Rocket.Chat.Fuselage/", "author": { @@ -50,10 +50,10 @@ "@rocket.chat/icons": "*", "@rocket.chat/prettier-config": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-avatar": "7.0.0-rc.0", - "@rocket.chat/ui-contexts": "11.0.0-rc.0", + "@rocket.chat/ui-avatar": "7.0.0-rc.1", + "@rocket.chat/ui-contexts": "11.0.0-rc.1", "@rocket.chat/ui-kit": "*", - "@rocket.chat/ui-video-conf": "11.0.0-rc.0", + "@rocket.chat/ui-video-conf": "11.0.0-rc.1", "@tanstack/react-query": "*", "react": "*", "react-dom": "*" diff --git a/packages/gazzodown/CHANGELOG.md b/packages/gazzodown/CHANGELOG.md index 9c4e6777a8a8..5ce5e8e7afed 100644 --- a/packages/gazzodown/CHANGELOG.md +++ b/packages/gazzodown/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/gazzodown +## 11.0.0-rc.1 + +### Patch Changes + +-
        Updated dependencies [2f9eea03d2]: + + - @rocket.chat/ui-client@11.0.0-rc.1 + - @rocket.chat/ui-contexts@11.0.0-rc.1 + - @rocket.chat/core-typings@6.13.0-rc.1 +
        + ## 11.0.0-rc.0 ### Minor Changes @@ -15,6 +26,7 @@ - @rocket.chat/message-parser@0.31.30-rc.0 - @rocket.chat/ui-contexts@11.0.0-rc.0 + ## 10.0.1 ### Patch Changes diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index b9a5023ce13c..cf4a9209d649 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/gazzodown", - "version": "11.0.0-rc.0", + "version": "11.0.0-rc.1", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", @@ -74,8 +74,8 @@ "@rocket.chat/fuselage-tokens": "*", "@rocket.chat/message-parser": "0.31.31-rc.0", "@rocket.chat/styled": "*", - "@rocket.chat/ui-client": "11.0.0-rc.0", - "@rocket.chat/ui-contexts": "11.0.0-rc.0", + "@rocket.chat/ui-client": "11.0.0-rc.1", + "@rocket.chat/ui-contexts": "11.0.0-rc.1", "katex": "*", "react": "*" }, diff --git a/packages/i18n/CHANGELOG.md b/packages/i18n/CHANGELOG.md index 27bb4b471c66..3fa9140d4e99 100644 --- a/packages/i18n/CHANGELOG.md +++ b/packages/i18n/CHANGELOG.md @@ -1,5 +1,11 @@ # @rocket.chat/i18n +## 0.8.0-rc.1 + +### Minor Changes + +- ([#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. + ## 0.8.0-rc.0 ### Minor Changes diff --git a/packages/i18n/package.json b/packages/i18n/package.json index fe98e3b1f7fc..b93b9e9926cc 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/i18n", - "version": "0.8.0-rc.0", + "version": "0.8.0-rc.1", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/packages/instance-status/CHANGELOG.md b/packages/instance-status/CHANGELOG.md index a2df17f053d2..dacdd5e1d383 100644 --- a/packages/instance-status/CHANGELOG.md +++ b/packages/instance-status/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/instance-status +## 0.1.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.1.8-rc.0 ### Patch Changes @@ -8,6 +17,7 @@ - @rocket.chat/models@0.3.0-rc.0 + ## 0.1.7 ### Patch Changes diff --git a/packages/instance-status/package.json b/packages/instance-status/package.json index a99c78a1ea78..79f0a19faf15 100644 --- a/packages/instance-status/package.json +++ b/packages/instance-status/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/instance-status", - "version": "0.1.8-rc.0", + "version": "0.1.8-rc.1", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/livechat/CHANGELOG.md b/packages/livechat/CHANGELOG.md index 480e9f27b435..b5e6fc40dff6 100644 --- a/packages/livechat/CHANGELOG.md +++ b/packages/livechat/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/livechat Change Log +## 1.20.0-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/gazzodown@11.0.0-rc.1 +
        + ## 1.20.0-rc.0 ### Minor Changes @@ -14,6 +23,7 @@ - @rocket.chat/gazzodown@11.0.0-rc.0 - @rocket.chat/message-parser@0.31.30-rc.0 + ## 1.19.4 ### Patch Changes diff --git a/packages/livechat/package.json b/packages/livechat/package.json index ab4a2a9b3a11..5663d8746eea 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/livechat", - "version": "1.20.0-rc.0", + "version": "1.20.0-rc.1", "files": [ "/build" ], diff --git a/packages/mock-providers/CHANGELOG.md b/packages/mock-providers/CHANGELOG.md index 5bd539fda340..a18b8b2e51fc 100644 --- a/packages/mock-providers/CHANGELOG.md +++ b/packages/mock-providers/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/mock-providers +## 0.1.3-rc.1 + +### Patch Changes + +-
        Updated dependencies [2f9eea03d2]: + + - @rocket.chat/i18n@0.8.0-rc.1 +
        + ## 0.1.3-rc.0 ### Patch Changes diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index 1a7051e87ff2..3cd4aef7325d 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/mock-providers", - "version": "0.1.3-rc.0", + "version": "0.1.3-rc.1", "private": true, "dependencies": { "@rocket.chat/emitter": "~0.31.25", diff --git a/packages/model-typings/CHANGELOG.md b/packages/model-typings/CHANGELOG.md index ecec03565e40..54c1a0769d1e 100644 --- a/packages/model-typings/CHANGELOG.md +++ b/packages/model-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/model-typings +## 0.8.0-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 +
        + ## 0.8.0-rc.0 ### Minor Changes @@ -18,6 +27,7 @@ - @rocket.chat/core-typings@6.13.0-rc.0 + ## 0.7.1 ### Patch Changes diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index 10a2fd9b643c..2c535eb950ad 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/model-typings", - "version": "0.8.0-rc.0", + "version": "0.8.0-rc.1", "private": true, "devDependencies": { "@types/node-rsa": "^1.1.4", diff --git a/packages/models/CHANGELOG.md b/packages/models/CHANGELOG.md index 5491066cea23..a332d48b4443 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/models +## 0.3.0-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/model-typings@0.8.0-rc.1 +
        + ## 0.3.0-rc.0 ### Minor Changes @@ -12,6 +21,7 @@ - @rocket.chat/model-typings@0.8.0-rc.0 + ## 0.2.4 ### Patch Changes diff --git a/packages/models/package.json b/packages/models/package.json index 35064171d9a6..aee39a8e841c 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/models", - "version": "0.3.0-rc.0", + "version": "0.3.0-rc.1", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/rest-typings/CHANGELOG.md b/packages/rest-typings/CHANGELOG.md index 8d999a8e7f5f..8dff96c33b7e 100644 --- a/packages/rest-typings/CHANGELOG.md +++ b/packages/rest-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/rest-typings +## 6.13.0-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 +
        + ## 6.13.0-rc.0 ### Minor Changes @@ -23,6 +32,7 @@ - @rocket.chat/core-typings@6.13.0-rc.0 - @rocket.chat/message-parser@0.31.30-rc.0 + ## 6.12.1 ### Patch Changes diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 33aad6d64823..6d435a93ccfc 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "6.13.0-rc.0", + "version": "6.13.0-rc.1", "devDependencies": { "@rocket.chat/eslint-config": "workspace:~", "@types/jest": "~29.5.13", diff --git a/packages/ui-avatar/CHANGELOG.md b/packages/ui-avatar/CHANGELOG.md index 8192d99f770b..b5fc9c4b46d3 100644 --- a/packages/ui-avatar/CHANGELOG.md +++ b/packages/ui-avatar/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/ui-avatar +## 7.0.0-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/ui-contexts@11.0.0-rc.1 +
        + ## 7.0.0-rc.0 ### Minor Changes diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index 70f0efe2cff9..5c319c8837a5 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-avatar", - "version": "7.0.0-rc.0", + "version": "7.0.0-rc.1", "private": true, "devDependencies": { "@babel/core": "~7.22.20", @@ -30,7 +30,7 @@ ], "peerDependencies": { "@rocket.chat/fuselage": "*", - "@rocket.chat/ui-contexts": "11.0.0-rc.0", + "@rocket.chat/ui-contexts": "11.0.0-rc.1", "react": "~17.0.2" }, "volta": { diff --git a/packages/ui-client/CHANGELOG.md b/packages/ui-client/CHANGELOG.md index 9be8c33c299d..0d5f01b17910 100644 --- a/packages/ui-client/CHANGELOG.md +++ b/packages/ui-client/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/ui-client +## 11.0.0-rc.1 + +### Minor Changes + +- ([#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. + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/ui-contexts@11.0.0-rc.1 + - @rocket.chat/ui-avatar@7.0.0-rc.1 +
        + ## 11.0.0-rc.0 ### Minor Changes @@ -17,6 +31,7 @@ - @rocket.chat/ui-avatar@7.0.0-rc.0 - @rocket.chat/ui-contexts@11.0.0-rc.0 + ## 10.0.1 ### Patch Changes diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 40bccd8ca628..3b551cf83681 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-client", - "version": "11.0.0-rc.0", + "version": "11.0.0-rc.1", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", @@ -61,8 +61,8 @@ "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", - "@rocket.chat/ui-avatar": "7.0.0-rc.0", - "@rocket.chat/ui-contexts": "11.0.0-rc.0", + "@rocket.chat/ui-avatar": "7.0.0-rc.1", + "@rocket.chat/ui-contexts": "11.0.0-rc.1", "react": "*", "react-i18next": "*" }, diff --git a/packages/ui-contexts/CHANGELOG.md b/packages/ui-contexts/CHANGELOG.md index 9dcb89eda51d..4e29a0d34993 100644 --- a/packages/ui-contexts/CHANGELOG.md +++ b/packages/ui-contexts/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/ui-contexts +## 11.0.0-rc.1 + +### Patch Changes + +-
        Updated dependencies [2f9eea03d2]: + + - @rocket.chat/i18n@0.8.0-rc.1 + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/rest-typings@6.13.0-rc.1 + - @rocket.chat/ddp-client@0.3.8-rc.1 +
        + ## 11.0.0-rc.0 ### Patch Changes @@ -11,6 +23,7 @@ - @rocket.chat/core-typings@6.13.0-rc.0 - @rocket.chat/ddp-client@0.3.7-rc.0 + ## 10.0.1 ### Patch Changes diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 37f83e96a2aa..c3b0d5465da2 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-contexts", - "version": "11.0.0-rc.0", + "version": "11.0.0-rc.1", "private": true, "devDependencies": { "@rocket.chat/core-typings": "workspace:^", diff --git a/packages/ui-video-conf/CHANGELOG.md b/packages/ui-video-conf/CHANGELOG.md index e058c3c4d257..450c89d74361 100644 --- a/packages/ui-video-conf/CHANGELOG.md +++ b/packages/ui-video-conf/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/ui-video-conf +## 11.0.0-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/ui-contexts@11.0.0-rc.1 + - @rocket.chat/ui-avatar@7.0.0-rc.1 +
        + ## 11.0.0-rc.0 ### Minor Changes @@ -13,6 +23,7 @@ - @rocket.chat/ui-avatar@7.0.0-rc.0 - @rocket.chat/ui-contexts@11.0.0-rc.0 + ## 10.0.1 ### Patch Changes diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 74c3ec43bcbb..fa14113988a2 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-video-conf", - "version": "11.0.0-rc.0", + "version": "11.0.0-rc.1", "private": true, "devDependencies": { "@babel/core": "~7.22.20", @@ -39,8 +39,8 @@ "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-avatar": "7.0.0-rc.0", - "@rocket.chat/ui-contexts": "11.0.0-rc.0", + "@rocket.chat/ui-avatar": "7.0.0-rc.1", + "@rocket.chat/ui-contexts": "11.0.0-rc.1", "react": "^17.0.2", "react-dom": "^17.0.2" }, diff --git a/packages/web-ui-registration/CHANGELOG.md b/packages/web-ui-registration/CHANGELOG.md index c53cb5a2623b..01be06de3c00 100644 --- a/packages/web-ui-registration/CHANGELOG.md +++ b/packages/web-ui-registration/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/web-ui-registration +## 11.0.0-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/ui-contexts@11.0.0-rc.1 +
        + ## 11.0.0-rc.0 ### Patch Changes diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index 23fd80c89842..c039b3fc6c94 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/web-ui-registration", - "version": "11.0.0-rc.0", + "version": "11.0.0-rc.1", "private": true, "homepage": "https://rocket.chat", "main": "./dist/index.js", @@ -47,7 +47,7 @@ "peerDependencies": { "@rocket.chat/layout": "*", "@rocket.chat/tools": "0.2.2", - "@rocket.chat/ui-contexts": "11.0.0-rc.0", + "@rocket.chat/ui-contexts": "11.0.0-rc.1", "@tanstack/react-query": "*", "react": "*", "react-hook-form": "*", From 7dc9c4154f672c26f046e8936f4cb02e86814355 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 25 Sep 2024 13:14:54 -0300 Subject: [PATCH 36/39] chore: replace Meteor._localStorage -> Accounts.storageLocation (#33356) --- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 27 ++++++++++--------- apps/meteor/app/ui-master/server/scripts.ts | 1 + .../client/messageBox/createComposerAPI.ts | 8 +++--- apps/meteor/app/ui-utils/server/Message.ts | 4 +-- .../app/utils/client/lib/RestApiClient.ts | 6 +++-- .../client/meteorOverrides/login/saml.ts | 2 +- .../providers/UserProvider/UserProvider.tsx | 6 ++--- apps/meteor/client/startup/accounts.ts | 16 +---------- .../components/AuthorizationFormPage.tsx | 3 +-- .../externals/meteor/accounts-base.d.ts | 1 + 10 files changed, 31 insertions(+), 43 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index bbd6f208f35a..50224cb89dbb 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -6,6 +6,7 @@ import { isE2EEMessage } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import EJSON from 'ejson'; import _ from 'lodash'; +import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; @@ -308,8 +309,8 @@ class E2E extends Emitter { getKeysFromLocalStorage(): KeyPair { return { - public_key: Meteor._localStorage.getItem('public_key'), - private_key: Meteor._localStorage.getItem('private_key'), + public_key: Accounts.storageLocation.getItem('public_key'), + private_key: Accounts.storageLocation.getItem('private_key'), }; } @@ -332,7 +333,7 @@ class E2E extends Emitter { imperativeModal.close(); }, onConfirm: () => { - Meteor._localStorage.removeItem('e2e.randomPassword'); + Accounts.storageLocation.removeItem('e2e.randomPassword'); this.setState(E2EEState.READY); dispatchToastMessage({ type: 'success', message: t('End_To_End_Encryption_Enabled') }); this.closeAlert(); @@ -394,7 +395,7 @@ class E2E extends Emitter { await this.persistKeys(this.getKeysFromLocalStorage(), await this.createRandomPassword()); } - const randomPassword = Meteor._localStorage.getItem('e2e.randomPassword'); + const randomPassword = Accounts.storageLocation.getItem('e2e.randomPassword'); if (randomPassword) { this.setState(E2EEState.SAVE_PASSWORD); this.openAlert({ @@ -412,8 +413,8 @@ class E2E extends Emitter { this.log('-> Stop Client'); this.closeAlert(); - Meteor._localStorage.removeItem('public_key'); - Meteor._localStorage.removeItem('private_key'); + Accounts.storageLocation.removeItem('public_key'); + Accounts.storageLocation.removeItem('private_key'); this.instancesByRoomId = {}; this.privateKey = undefined; this.started = false; @@ -425,8 +426,8 @@ class E2E extends Emitter { async changePassword(newPassword: string): Promise { await this.persistKeys(this.getKeysFromLocalStorage(), newPassword, { force: true }); - if (Meteor._localStorage.getItem('e2e.randomPassword')) { - Meteor._localStorage.setItem('e2e.randomPassword', newPassword); + if (Accounts.storageLocation.getItem('e2e.randomPassword')) { + Accounts.storageLocation.setItem('e2e.randomPassword', newPassword); } } @@ -447,12 +448,12 @@ class E2E extends Emitter { } async loadKeys({ public_key, private_key }: { public_key: string; private_key: string }): Promise { - Meteor._localStorage.setItem('public_key', public_key); + Accounts.storageLocation.setItem('public_key', public_key); try { this.privateKey = await importRSAKey(EJSON.parse(private_key), ['decrypt']); - Meteor._localStorage.setItem('private_key', private_key); + Accounts.storageLocation.setItem('private_key', private_key); } catch (error) { this.setState(E2EEState.ERROR); return this.error('Error importing private key: ', error); @@ -474,7 +475,7 @@ class E2E extends Emitter { try { const publicKey = await exportJWKKey(key.publicKey); - Meteor._localStorage.setItem('public_key', JSON.stringify(publicKey)); + Accounts.storageLocation.setItem('public_key', JSON.stringify(publicKey)); } catch (error) { this.setState(E2EEState.ERROR); return this.error('Error exporting public key: ', error); @@ -483,7 +484,7 @@ class E2E extends Emitter { try { const privateKey = await exportJWKKey(key.privateKey); - Meteor._localStorage.setItem('private_key', JSON.stringify(privateKey)); + Accounts.storageLocation.setItem('private_key', JSON.stringify(privateKey)); } catch (error) { this.setState(E2EEState.ERROR); return this.error('Error exporting private key: ', error); @@ -498,7 +499,7 @@ class E2E extends Emitter { async createRandomPassword(): Promise { const randomPassword = await generateMnemonicPhrase(5); - Meteor._localStorage.setItem('e2e.randomPassword', randomPassword); + Accounts.storageLocation.setItem('e2e.randomPassword', randomPassword); return randomPassword; } diff --git a/apps/meteor/app/ui-master/server/scripts.ts b/apps/meteor/app/ui-master/server/scripts.ts index 9edadb021d32..3e84a6e39c90 100644 --- a/apps/meteor/app/ui-master/server/scripts.ts +++ b/apps/meteor/app/ui-master/server/scripts.ts @@ -45,6 +45,7 @@ window.addEventListener('load', function() { }); window.localStorage.clear(); Meteor._localStorage = window.sessionStorage; + Accounts.config({ clientStorage: 'session' }); } }); ` diff --git a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts index a926f8540d27..741f7959fa90 100644 --- a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts +++ b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts @@ -1,6 +1,6 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; -import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; import type { ComposerAPI } from '../../../../client/lib/chats/ChatAPI'; import { withDebouncing } from '../../../../lib/utils/highOrderFunctions'; @@ -31,11 +31,11 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) const persist = withDebouncing({ wait: 300 })(() => { if (input.value) { - Meteor._localStorage.setItem(storageID, input.value); + Accounts.storageLocation.setItem(storageID, input.value); return; } - Meteor._localStorage.removeItem(storageID); + Accounts.storageLocation.removeItem(storageID); }); const notifyQuotedMessagesUpdate = (): void => { @@ -262,7 +262,7 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) const insertNewLine = (): void => insertText('\n'); - setText(Meteor._localStorage.getItem(storageID) ?? '', { + setText(Accounts.storageLocation.getItem(storageID) ?? '', { skipFocus: true, }); diff --git a/apps/meteor/app/ui-utils/server/Message.ts b/apps/meteor/app/ui-utils/server/Message.ts index 06ae59238b42..21d8886c70bc 100644 --- a/apps/meteor/app/ui-utils/server/Message.ts +++ b/apps/meteor/app/ui-utils/server/Message.ts @@ -1,6 +1,6 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { escapeHTML } from '@rocket.chat/string-helpers'; -import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; import { trim } from '../../../lib/utils/stringUtils'; import { i18n } from '../../../server/lib/i18n'; @@ -17,7 +17,7 @@ export const Message = { } if (messageType.message) { if (!language) { - language = Meteor._localStorage.getItem('userLanguage') || 'en'; + language = Accounts.storageLocation.getItem('userLanguage') || 'en'; } const data = (typeof messageType.data === 'function' && messageType.data(msg)) || {}; return i18n.t(messageType.message, { ...data, lng: language }); diff --git a/apps/meteor/app/utils/client/lib/RestApiClient.ts b/apps/meteor/app/utils/client/lib/RestApiClient.ts index c5e12250b441..53c95ee3e4fa 100644 --- a/apps/meteor/app/utils/client/lib/RestApiClient.ts +++ b/apps/meteor/app/utils/client/lib/RestApiClient.ts @@ -1,6 +1,5 @@ import { RestClient } from '@rocket.chat/api-client'; import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; import { invokeTwoFactorModal } from '../../../../client/lib/2fa/process2faReturn'; import { baseURI } from '../../../../client/lib/baseURI'; @@ -12,7 +11,10 @@ class RestApiClient extends RestClient { 'X-Auth-Token': string; } | undefined { - const [uid, token] = [Meteor._localStorage.getItem(Accounts.USER_ID_KEY), Meteor._localStorage.getItem(Accounts.LOGIN_TOKEN_KEY)]; + const [uid, token] = [ + Accounts.storageLocation.getItem(Accounts.USER_ID_KEY), + Accounts.storageLocation.getItem(Accounts.LOGIN_TOKEN_KEY), + ]; if (!uid || !token) { return; diff --git a/apps/meteor/client/meteorOverrides/login/saml.ts b/apps/meteor/client/meteorOverrides/login/saml.ts index 14dfcc694e5c..f2199af5c0c7 100644 --- a/apps/meteor/client/meteorOverrides/login/saml.ts +++ b/apps/meteor/client/meteorOverrides/login/saml.ts @@ -72,7 +72,7 @@ Meteor.logout = async function (...args) { // Remove the userId from the client to prevent calls to the server while the logout is processed. // If the logout fails, the userId will be reloaded on the resume call - Meteor._localStorage.removeItem(Accounts.USER_ID_KEY); + Accounts.storageLocation.removeItem(Accounts.USER_ID_KEY); // A nasty bounce: 'result' has the SAML LogoutRequest but we need a proper 302 to redirected from the server. window.location.replace(Meteor.absoluteUrl(`_saml/sloRedirect/${provider}/?redirect=${encodeURIComponent(result)}`)); diff --git a/apps/meteor/client/providers/UserProvider/UserProvider.tsx b/apps/meteor/client/providers/UserProvider/UserProvider.tsx index 27bba21eae95..53761fbef4e7 100644 --- a/apps/meteor/client/providers/UserProvider/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider/UserProvider.tsx @@ -19,8 +19,6 @@ import { useDeleteUser } from './hooks/useDeleteUser'; import { useEmailVerificationWarning } from './hooks/useEmailVerificationWarning'; import { useUpdateAvatar } from './hooks/useUpdateAvatar'; -const getUserId = (): string | null => Meteor.userId(); - const getUser = (): IUser | null => Meteor.user() as IUser | null; const logout = (): Promise => @@ -42,9 +40,9 @@ type UserProviderProps = { }; const UserProvider = ({ children }: UserProviderProps): ReactElement => { - const userId = useReactiveValue(getUserId); - const previousUserId = useRef(userId); const user = useReactiveValue(getUser); + const userId = user?._id ?? null; + const previousUserId = useRef(userId); const [userLanguage, setUserLanguage] = useLocalStorage('userLanguage', ''); const [preferedLanguage, setPreferedLanguage] = useLocalStorage('preferedLanguage', ''); diff --git a/apps/meteor/client/startup/accounts.ts b/apps/meteor/client/startup/accounts.ts index 88008a606656..50c033dc0596 100644 --- a/apps/meteor/client/startup/accounts.ts +++ b/apps/meteor/client/startup/accounts.ts @@ -2,7 +2,7 @@ import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { settings } from '../../app/settings/client'; +// import { settings } from '../../app/settings/client'; import { mainReady } from '../../app/ui-utils/client'; import { sdk } from '../../app/utils/client/lib/SDKClient'; import { t } from '../../app/utils/lib/i18n'; @@ -25,17 +25,3 @@ Accounts.onEmailVerificationLink((token: string) => { }); }); }); - -Meteor.startup(() => { - Tracker.autorun((computation) => { - const forgetUserSessionOnWindowClose = settings.get('Accounts_ForgetUserSessionOnWindowClose'); - - if (forgetUserSessionOnWindowClose === undefined) { - return; - } - - computation.stop(); - - Accounts.config({ clientStorage: forgetUserSessionOnWindowClose ? 'session' : 'local' }); - }); -}); diff --git a/apps/meteor/client/views/oauth/components/AuthorizationFormPage.tsx b/apps/meteor/client/views/oauth/components/AuthorizationFormPage.tsx index 14f251042abc..623214352372 100644 --- a/apps/meteor/client/views/oauth/components/AuthorizationFormPage.tsx +++ b/apps/meteor/client/views/oauth/components/AuthorizationFormPage.tsx @@ -4,7 +4,6 @@ import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { Form } from '@rocket.chat/layout'; import { useLogout, useRoute } from '@rocket.chat/ui-contexts'; import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; import React, { useEffect, useMemo, useRef } from 'react'; import { Trans, useTranslation } from 'react-i18next'; @@ -19,7 +18,7 @@ type AuthorizationFormPageProps = { }; const AuthorizationFormPage = ({ oauthApp, redirectUri, user }: AuthorizationFormPageProps) => { - const token = useMemo(() => Meteor._localStorage.getItem(Accounts.LOGIN_TOKEN_KEY) ?? undefined, []); + const token = useMemo(() => Accounts.storageLocation.getItem(Accounts.LOGIN_TOKEN_KEY) ?? undefined, []); const formLabelId = useUniqueId(); diff --git a/apps/meteor/definition/externals/meteor/accounts-base.d.ts b/apps/meteor/definition/externals/meteor/accounts-base.d.ts index 31b70f7b7154..0d30eed0430d 100644 --- a/apps/meteor/definition/externals/meteor/accounts-base.d.ts +++ b/apps/meteor/definition/externals/meteor/accounts-base.d.ts @@ -1,5 +1,6 @@ declare module 'meteor/accounts-base' { namespace Accounts { + const storageLocation: Window['localStorage']; function createUser( options: { username?: string; From b4c3e5cfee9f5a778b26c374b8b89a5a1ae79a26 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Wed, 25 Sep 2024 19:43:42 -0300 Subject: [PATCH 37/39] Bump rocket.chat to 6.14.0-develop (#33366) --- apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/package.json | 2 +- package.json | 2 +- packages/core-typings/package.json | 2 +- packages/rest-typings/package.json | 2 +- yarn.lock | 22 +++++++++++----------- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index cfec7fa33824..de3eb50fa5d1 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "6.13.0-rc.1" + "version": "6.14.0-develop" } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 766770427e0a..3767939a7e3c 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "6.13.0-rc.1", + "version": "6.14.0-develop", "private": true, "author": { "name": "Rocket.Chat", diff --git a/package.json b/package.json index 1a87fa6034bf..f06deed440ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "6.13.0-rc.1", + "version": "6.14.0-develop", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 6fcd5a43d20d..0db23760066b 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", - "version": "6.13.0-rc.1", + "version": "6.14.0-develop", "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", "eslint": "~8.45.0", diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 6d435a93ccfc..30a448980c9b 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "6.13.0-rc.1", + "version": "6.14.0-develop", "devDependencies": { "@rocket.chat/eslint-config": "workspace:~", "@types/jest": "~29.5.13", diff --git a/yarn.lock b/yarn.lock index 161420f46399..f6c5de3e9a44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8962,10 +8962,10 @@ __metadata: "@rocket.chat/icons": "*" "@rocket.chat/prettier-config": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 7.0.0-rc.0 - "@rocket.chat/ui-contexts": 11.0.0-rc.0 + "@rocket.chat/ui-avatar": 7.0.0-rc.1 + "@rocket.chat/ui-contexts": 11.0.0-rc.1 "@rocket.chat/ui-kit": "*" - "@rocket.chat/ui-video-conf": 11.0.0-rc.0 + "@rocket.chat/ui-video-conf": 11.0.0-rc.1 "@tanstack/react-query": "*" react: "*" react-dom: "*" @@ -9052,8 +9052,8 @@ __metadata: "@rocket.chat/fuselage-tokens": "*" "@rocket.chat/message-parser": 0.31.31-rc.0 "@rocket.chat/styled": "*" - "@rocket.chat/ui-client": 11.0.0-rc.0 - "@rocket.chat/ui-contexts": 11.0.0-rc.0 + "@rocket.chat/ui-client": 11.0.0-rc.1 + "@rocket.chat/ui-contexts": 11.0.0-rc.1 katex: "*" react: "*" languageName: unknown @@ -10286,7 +10286,7 @@ __metadata: typescript: ~5.5.4 peerDependencies: "@rocket.chat/fuselage": "*" - "@rocket.chat/ui-contexts": 11.0.0-rc.0 + "@rocket.chat/ui-contexts": 11.0.0-rc.1 react: ~17.0.2 languageName: unknown linkType: soft @@ -10337,8 +10337,8 @@ __metadata: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" - "@rocket.chat/ui-avatar": 7.0.0-rc.0 - "@rocket.chat/ui-contexts": 11.0.0-rc.0 + "@rocket.chat/ui-avatar": 7.0.0-rc.1 + "@rocket.chat/ui-contexts": 11.0.0-rc.1 react: "*" react-i18next: "*" languageName: unknown @@ -10507,8 +10507,8 @@ __metadata: "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 7.0.0-rc.0 - "@rocket.chat/ui-contexts": 11.0.0-rc.0 + "@rocket.chat/ui-avatar": 7.0.0-rc.1 + "@rocket.chat/ui-contexts": 11.0.0-rc.1 react: ^17.0.2 react-dom: ^17.0.2 languageName: unknown @@ -10596,7 +10596,7 @@ __metadata: peerDependencies: "@rocket.chat/layout": "*" "@rocket.chat/tools": 0.2.2 - "@rocket.chat/ui-contexts": 11.0.0-rc.0 + "@rocket.chat/ui-contexts": 11.0.0-rc.1 "@tanstack/react-query": "*" react: "*" react-hook-form: "*" From 6bee2a11a5ca3ed282311d0dd7f97d9c0a9392a0 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Thu, 26 Sep 2024 10:54:59 -0700 Subject: [PATCH 38/39] ci: use node20 for release action (#33343) --- packages/release-action/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/release-action/action.yml b/packages/release-action/action.yml index 2fc8fe35d565..23d7382aab6d 100644 --- a/packages/release-action/action.yml +++ b/packages/release-action/action.yml @@ -10,7 +10,7 @@ inputs: required: false runs: - using: "node16" + using: "node20" main: "dist/index.js" branding: From fc26d85b287e9f3c7b8b8f714316cfb72470048e Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Sep 2024 18:47:39 -0300 Subject: [PATCH 39/39] regression: `Sidepanel` sort requires refresh after room update (#33370) --- .../views/room/Sidepanel/hooks/useTeamslistChildren.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts b/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts index 5791a6e5d547..772c29509608 100644 --- a/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts +++ b/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts @@ -1,6 +1,7 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import type { Mongo } from 'meteor/mongo'; import { useEffect, useMemo } from 'react'; import { ChatRoom } from '../../../../../app/models/client'; @@ -12,7 +13,7 @@ const sortRoomByLastMessage = (a: IRoom, b: IRoom) => { if (!b.lm) { return -1; } - return new Date(b.lm).toUTCString().localeCompare(new Date(a.lm).toUTCString()); + return b.lm.getTime() - a.lm.getTime(); }; export const useTeamsListChildrenUpdate = ( @@ -23,7 +24,7 @@ export const useTeamsListChildrenUpdate = ( const queryClient = useQueryClient(); const query = useMemo(() => { - const query: Parameters[0] = { + const query: Mongo.Selector = { $or: [ { _id: parentRid,