From d03edb840afef6094e6bd66c5ccadb635bca6a14 Mon Sep 17 00:00:00 2001 From: cristianoliveira Date: Wed, 17 Jun 2026 15:55:36 +0200 Subject: [PATCH 01/21] feat: add channel description section to conversation details panel - New ConversationDetailsDescription component with heading and body text - Renders between group header and add-participants button - Hidden for 1:1 conversations, shown for groups/channels - Uses mocked description text (API integration to follow) - Styled to match design: same-size bold heading, muted gray body, 1:2 spacing ratio - 6 new tests covering render, empty state, and integration --- apps/webapp/src/i18n/en-US.json | 1 + .../conversationDetailsDescription.test.tsx | 45 +++++++++++++++++++ .../conversationDetailsDescription.tsx | 41 +++++++++++++++++ .../conversationDetailsDescription/index.ts | 20 +++++++++ .../conversationDetails.test.tsx | 28 ++++++++++++ .../conversationDetails.tsx | 3 ++ .../src/style/panel/conversation-details.less | 22 +++++++++ apps/webapp/src/types/i18n.d.ts | 1 + 8 files changed, 161 insertions(+) create mode 100644 apps/webapp/src/script/page/RightSidebar/conversationDetails/components/conversationDetailsDescription/conversationDetailsDescription.test.tsx create mode 100644 apps/webapp/src/script/page/RightSidebar/conversationDetails/components/conversationDetailsDescription/conversationDetailsDescription.tsx create mode 100644 apps/webapp/src/script/page/RightSidebar/conversationDetails/components/conversationDetailsDescription/index.ts diff --git a/apps/webapp/src/i18n/en-US.json b/apps/webapp/src/i18n/en-US.json index d555ddea10a..eddca64cf21 100644 --- a/apps/webapp/src/i18n/en-US.json +++ b/apps/webapp/src/i18n/en-US.json @@ -702,6 +702,7 @@ "conversationDetailsCloseLabel": "Close image details view", "conversationDetailsGroupAdmin": "Group Admin", "conversationDetailsGroupAdminInfo": "When this is on, the admin can add or remove people and apps, update group settings, and change a participant’s role.", + "conversationDetailsDescription": "Channel description", "conversationDetailsOff": "Off", "conversationDetailsOn": "On", "conversationDetailsOptions": "Options", diff --git a/apps/webapp/src/script/page/RightSidebar/conversationDetails/components/conversationDetailsDescription/conversationDetailsDescription.test.tsx b/apps/webapp/src/script/page/RightSidebar/conversationDetails/components/conversationDetailsDescription/conversationDetailsDescription.test.tsx new file mode 100644 index 00000000000..810e536ebf2 --- /dev/null +++ b/apps/webapp/src/script/page/RightSidebar/conversationDetails/components/conversationDetailsDescription/conversationDetailsDescription.test.tsx @@ -0,0 +1,45 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {render, screen} from '@testing-library/react'; + +import {ConversationDetailsDescription} from './conversationDetailsDescription'; + +describe('ConversationDetailsDescription', () => { + it('renders the description heading and text', () => { + const description = 'This is the channel description'; + + render(); + + expect(screen.getByText('conversationDetailsDescription')).not.toBeNull(); + expect(screen.getByText(description)).not.toBeNull(); + }); + + it('does not render when description is empty', () => { + const {container} = render(); + + expect(container.firstChild).toBeNull(); + }); + + it('does not render when description is undefined', () => { + const {container} = render(); + + expect(container.firstChild).toBeNull(); + }); +}); diff --git a/apps/webapp/src/script/page/RightSidebar/conversationDetails/components/conversationDetailsDescription/conversationDetailsDescription.tsx b/apps/webapp/src/script/page/RightSidebar/conversationDetails/components/conversationDetailsDescription/conversationDetailsDescription.tsx new file mode 100644 index 00000000000..ab285d3acf6 --- /dev/null +++ b/apps/webapp/src/script/page/RightSidebar/conversationDetails/components/conversationDetailsDescription/conversationDetailsDescription.tsx @@ -0,0 +1,41 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {FC} from 'react'; + +import {t} from 'Util/localizerUtil'; + +interface ConversationDetailsDescriptionProps { + description?: string; +} + +const ConversationDetailsDescription: FC = ({description}) => { + if (!description) { + return null; + } + + return ( +
+

{t('conversationDetailsDescription')}

+

{description}

+
+ ); +}; + +export {ConversationDetailsDescription}; diff --git a/apps/webapp/src/script/page/RightSidebar/conversationDetails/components/conversationDetailsDescription/index.ts b/apps/webapp/src/script/page/RightSidebar/conversationDetails/components/conversationDetailsDescription/index.ts new file mode 100644 index 00000000000..bbf3b2305be --- /dev/null +++ b/apps/webapp/src/script/page/RightSidebar/conversationDetails/components/conversationDetailsDescription/index.ts @@ -0,0 +1,20 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +export {ConversationDetailsDescription} from './conversationDetailsDescription'; diff --git a/apps/webapp/src/script/page/RightSidebar/conversationDetails/conversationDetails.test.tsx b/apps/webapp/src/script/page/RightSidebar/conversationDetails/conversationDetails.test.tsx index 848fbad7b93..9e51a55cb94 100644 --- a/apps/webapp/src/script/page/RightSidebar/conversationDetails/conversationDetails.test.tsx +++ b/apps/webapp/src/script/page/RightSidebar/conversationDetails/conversationDetails.test.tsx @@ -115,6 +115,34 @@ const getDefaultParams = () => { }; describe('ConversationDetails', () => { + it('renders the channel description section for group conversations', () => { + const conversation = new Conversation(); + conversation.type(CONVERSATION_TYPE.REGULAR); + const otherUser = new User('other-user'); + jest.spyOn(otherUser as any, 'isConnected').mockReturnValue(true); + conversation.participating_user_ets([otherUser]); + + const defaultProps = getDefaultParams(); + + const {getByTestId} = render(); + + expect(getByTestId('conversation-details-description')).toBeDefined(); + }); + + it('does not render the channel description section for 1:1 conversations', () => { + const conversation = new Conversation(); + conversation.type(CONVERSATION_TYPE.ONE_TO_ONE); + const otherUser = new User('other-user'); + jest.spyOn(otherUser as any, 'isConnected').mockReturnValue(true); + conversation.participating_user_ets([otherUser]); + + const defaultProps = getDefaultParams(); + + const {queryByTestId} = render(); + + expect(queryByTestId('conversation-details-description')).toBeNull(); + }); + it("returns the right actions depending on the conversation's type for non group creators", () => { const conversation = new Conversation(); const otherUser = new User('other-user'); diff --git a/apps/webapp/src/script/page/RightSidebar/conversationDetails/conversationDetails.tsx b/apps/webapp/src/script/page/RightSidebar/conversationDetails/conversationDetails.tsx index 82f47345ad3..2ce077776d8 100644 --- a/apps/webapp/src/script/page/RightSidebar/conversationDetails/conversationDetails.tsx +++ b/apps/webapp/src/script/page/RightSidebar/conversationDetails/conversationDetails.tsx @@ -46,6 +46,7 @@ import {t} from 'Util/localizerUtil'; import {sortUsersByPriority} from 'Util/stringUtil'; import {formatDuration} from 'Util/timeUtil'; +import {ConversationDetailsDescription} from './components/conversationDetailsDescription'; import {ConversationDetailsHeader} from './components/conversationDetailsHeader'; import {ConversationDetailsOptions} from './components/conversationDetailsOptions'; import {ConversationDetailsParticipants} from './components/conversationDetailsParticipants'; @@ -324,6 +325,8 @@ const ConversationDetails = forwardRef conversation={activeConversation} /> + + {showActionAddParticipants && (