diff --git a/.gitignore b/.gitignore index feb8d45a1ef..ecb3772cb70 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ lib/ storybook-static/ plans/ +output diff --git a/apps/webapp/src/i18n/en-US.json b/apps/webapp/src/i18n/en-US.json index d555ddea10a..a99e1a38bee 100644 --- a/apps/webapp/src/i18n/en-US.json +++ b/apps/webapp/src/i18n/en-US.json @@ -678,6 +678,11 @@ "conversationCreatedYou": "You started a conversation with {users}", "conversationCreatedYouMore": "You started a conversation with {users}, and [showmore]{count} more[/showmore]", "conversationDeleteTimestamp": "Deleted: {date}", + "conversationDescriptionAddedYou": " added a group description", + "conversationDescriptionEditedYou": " edited the group description", + "conversationDescriptionLabel": "Description:", + "conversationDescriptionOptionalLabel": "Description (optional)", + "conversationDescriptionPlaceholder": "Enter a description", "conversationDetails1to1ReceiptsFirst": "If both sides turn on read receipts, you can see when messages are read.", "conversationDetails1to1ReceiptsHeadDisabled": "You have disabled read receipts", "conversationDetails1to1ReceiptsHeadEnabled": "You have enabled read receipts", @@ -700,6 +705,8 @@ "conversationDetailsActionTimedMessagesDisabled": "Self-deleting messages are off", "conversationDetailsActionUnblock": "Unblock", "conversationDetailsCloseLabel": "Close image details view", + "conversationDetailsDescription": "Description (optional)", + "conversationDetailsDescriptionPlaceholder": "Enter a description (maximum 200 characters)", "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.", "conversationDetailsOff": "Off", diff --git a/apps/webapp/src/i18n/pt-BR.json b/apps/webapp/src/i18n/pt-BR.json index 926a8e32759..3a3388ba2f4 100644 --- a/apps/webapp/src/i18n/pt-BR.json +++ b/apps/webapp/src/i18n/pt-BR.json @@ -662,6 +662,7 @@ "conversationContextMenuReply": "Responder", "conversationContextMenuUnlike": "Descurtir", "conversationCreateReceiptsEnabled": "As confirmações de leitura estão ligadas", + "conversationDescriptionLabel": "Descrição:", "conversationCreateTeam": "com [showmore]todos os membros[/showmore]", "conversationCreateTeamGuest": "com [showmore]todos os membros e um convidado[/showmore]", "conversationCreateTeamGuests": "com [showmore]todos os membros e {count} convidados[/showmore]", @@ -699,6 +700,8 @@ "conversationDetailsCloseLabel": "Fechar visualização de detalhes da imagem", "conversationDetailsGroupAdmin": "Administrador do Grupo", "conversationDetailsGroupAdminInfo": "When this is on, the admin can add or remove people and apps, update group settings, and change a participant’s role.", + "conversationDescriptionAddedYou": " adicionou uma descrição ao grupo", + "conversationDescriptionEditedYou": " editou a descrição do grupo", "conversationDetailsOff": "Desligado", "conversationDetailsOn": "Ligado", "conversationDetailsOptions": "Opções", diff --git a/apps/webapp/src/script/components/Conversation/Conversation.tsx b/apps/webapp/src/script/components/Conversation/Conversation.tsx index fa5a4bb127b..f8dca354470 100644 --- a/apps/webapp/src/script/components/Conversation/Conversation.tsx +++ b/apps/webapp/src/script/components/Conversation/Conversation.tsx @@ -154,6 +154,15 @@ export const Conversation = ({ } }, [activeConversation, isVirtualizedMessagesListEnabled]); + useEffect(() => { + if (!activeConversation) { + return; + } + + // TODO: Remove when description is part of the API Conversation payload + void conversationRepository.loadConversationDescription(activeConversation); + }, [activeConversation, conversationRepository]); + const uploadImages = useCallback( (images: File[]) => { if (!activeConversation || isHittingUploadLimit(images, repositories.asset, translate)) { diff --git a/apps/webapp/src/script/components/MessagesList/Message/MemberMessage.test.tsx b/apps/webapp/src/script/components/MessagesList/Message/MemberMessage.test.tsx index 4693dcfed56..f1a0a23af6f 100644 --- a/apps/webapp/src/script/components/MessagesList/Message/MemberMessage.test.tsx +++ b/apps/webapp/src/script/components/MessagesList/Message/MemberMessage.test.tsx @@ -100,6 +100,66 @@ describe('MemberMessage', () => { expect(getByTestId('element-connected-message')).not.toBeNull(); }); + it('shows conversation description when group is created and description exists', () => { + const message = createMemberMessage({systemType: SystemMessageType.CONVERSATION_CREATE}, [generateUser()]); + const props = { + ...baseProps, + message, + conversationDescription: 'A useful group description', + }; + + const {getByTestId, getByText} = render(withTheme()); + expect(getByText('Description:')).toBeInTheDocument(); + expect(getByText('A useful group description')).toBeInTheDocument(); + expect(getByTestId('group-creation-description-label').nextElementSibling).toBe( + getByTestId('group-creation-description-text'), + ); + }); + + it('renders conversation description headings, line breaks, and links like chat messages', () => { + const message = createMemberMessage({systemType: SystemMessageType.CONVERSATION_CREATE}, [generateUser()]); + const props = { + ...baseProps, + message, + conversationDescription: '# First line\nhttps://wire.com', + }; + + const {getByRole, getByTestId} = render(withTheme()); + + const link = getByRole('link', {name: 'https://wire.com'}); + const description = getByTestId('group-creation-description-text'); + expect(link).toHaveAttribute('href', 'https://wire.com'); + expect(link).toHaveAttribute('target', '_blank'); + expect(description.querySelector('.md-heading--1')).toHaveTextContent('First line'); + expect(description.innerHTML).toContain('
'); + }); + + it('escapes html in the conversation description shown in chat', () => { + const message = createMemberMessage({systemType: SystemMessageType.CONVERSATION_CREATE}, [generateUser()]); + const props = { + ...baseProps, + message, + conversationDescription: '\nhttps://wire.com', + }; + + const {getByTestId, getByText} = render(withTheme()); + + expect(getByTestId('group-creation-description-text').querySelector('img')).toBeNull(); + expect(getByText('')).toBeInTheDocument(); + }); + + it('does not show conversation description when group is created without description', () => { + const message = createMemberMessage({systemType: SystemMessageType.CONVERSATION_CREATE}, [generateUser()]); + const props = { + ...baseProps, + message, + conversationDescription: '', + }; + + const {queryByText} = render(withTheme()); + expect(queryByText('Description:')).not.toBeInTheDocument(); + }); + it('shows self-deleting messages off banner when group is created and self-deleting messages are disabled', () => { const message = createMemberMessage({systemType: SystemMessageType.CONVERSATION_CREATE}, [generateUser()]); const props = { diff --git a/apps/webapp/src/script/components/MessagesList/Message/MemberMessage.tsx b/apps/webapp/src/script/components/MessagesList/Message/MemberMessage.tsx index 00050a42f05..f05643204cb 100644 --- a/apps/webapp/src/script/components/MessagesList/Message/MemberMessage.tsx +++ b/apps/webapp/src/script/components/MessagesList/Message/MemberMessage.tsx @@ -25,12 +25,19 @@ import {User} from 'Repositories/entity/User'; import {SystemMessageType} from 'src/script/message/SystemMessageType'; import {useApplicationContext} from 'src/script/page/RootProvider'; import {useKoSubscribableChildren} from 'Util/componentUtil'; +import {renderMessage} from 'Util/messageRenderer'; import {E2eEncryptionMessage} from './E2eEncryptionMessage/E2eEncryptionMessage'; import {ConnectedMessage} from './MemberMessage/ConnectedMessage'; import {MessageContent} from './MemberMessage/MessageContent'; import {MessageTime} from './MessageTime'; +const PinIcon = () => ( + +); + interface MemberMessageProps { classifiedDomains?: string[]; hasReadReceiptsTurnedOn: boolean; @@ -42,6 +49,7 @@ interface MemberMessageProps { onClickParticipants: (participants: User[]) => void; shouldShowInvitePeople: boolean; conversationName: string; + conversationDescription?: string; isCellsConversation: boolean; } @@ -56,6 +64,7 @@ export const MemberMessage = ({ onClickCancelRequest, classifiedDomains, conversationName, + conversationDescription, isCellsConversation, }: MemberMessageProps) => { const {translate} = useApplicationContext(); @@ -73,6 +82,9 @@ export const MemberMessage = ({ const cellsConversationLabel = translate('conversationCellsConversationEnabled'); const receiptsEnabledLabel = translate('conversationCreateReceiptsEnabled'); const timedMessagesDisabledLabel = translate('conversationDetailsActionTimedMessagesDisabled'); + const hasConversationDescription = conversationDescription !== undefined && conversationDescription.length > 0; + const shouldShowDescription = isGroupCreation && hasConversationDescription; + const renderedConversationDescription = renderMessage(conversationDescription ?? ''); const isConnectedMessage = [SystemMessageType.CONNECTION_ACCEPTED, SystemMessageType.CONNECTION_REQUEST].includes( message.memberMessageType, @@ -103,6 +115,31 @@ export const MemberMessage = ({ )} + {shouldShowDescription && ( +
+ +
+ + {translate('conversationDescriptionLabel')} + + +
+
+ )} + {hasUsers && (