diff --git a/changelog.d/1236.feature b/changelog.d/1236.feature new file mode 100644 index 0000000000..8ad652361a --- /dev/null +++ b/changelog.d/1236.feature @@ -0,0 +1 @@ +Display a thread decorator in timeline so we know when a message is coming from a thread. diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 50b3dca2d1..8c985d45d9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -209,7 +209,8 @@ class MessagesPresenter @AssistedInject constructor( TimelineItemAction.Copy -> handleCopyContents(targetEvent) TimelineItemAction.Redact -> handleActionRedact(targetEvent) TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState) - TimelineItemAction.Reply -> handleActionReply(targetEvent, composerState) + TimelineItemAction.Reply, + TimelineItemAction.ReplyInThread -> handleActionReply(targetEvent, composerState) TimelineItemAction.Developer -> handleShowDebugInfoAction(targetEvent) TimelineItemAction.Forward -> handleForwardAction(targetEvent) TimelineItemAction.ReportContent -> handleReportAction(targetEvent) @@ -312,6 +313,7 @@ class MessagesPresenter @AssistedInject constructor( is TimelineItemUnknownContent -> null } val composerMode = MessageComposerMode.Reply( + isThreaded = targetEvent.isThreaded, senderName = targetEvent.safeSenderName, eventId = targetEvent.eventId, attachmentThumbnailInfo = attachmentThumbnailInfo, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index b179261c72..e87523d702 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -130,7 +130,11 @@ class ActionListPresenter @Inject constructor( if (timelineItem.isRemote) { // Can only reply or forward messages already uploaded to the server if (userCanSendMessage) { - add(TimelineItemAction.Reply) + if (timelineItem.isThreaded) { + add(TimelineItemAction.ReplyInThread) + } else { + add(TimelineItemAction.Reply) + } } add(TimelineItemAction.Forward) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt index 7a8a1fa1db..331edfa1a5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt @@ -32,6 +32,7 @@ sealed class TimelineItemAction( data object Copy : TimelineItemAction(CommonStrings.action_copy, VectorIcons.Copy) data object Redact : TimelineItemAction(CommonStrings.action_remove, VectorIcons.Delete, destructive = true) data object Reply : TimelineItemAction(CommonStrings.action_reply, VectorIcons.Reply) + data object ReplyInThread : TimelineItemAction(CommonStrings.action_reply_in_thread, VectorIcons.Reply) data object Edit : TimelineItemAction(CommonStrings.action_edit, VectorIcons.Edit) data object Developer : TimelineItemAction(CommonStrings.action_view_source, VectorIcons.DeveloperMode) data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, VectorIcons.ReportContent, destructive = true) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index 8a4d45e40c..1374f4aef4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -111,6 +111,7 @@ internal fun aTimelineItemEvent( groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None, sendState: LocalEventSendState = LocalEventSendState.Sent(eventId), inReplyTo: InReplyTo? = null, + isThreaded: Boolean = false, debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(), ): TimelineItem.Event { @@ -129,6 +130,7 @@ internal fun aTimelineItemEvent( localSendState = sendState, inReplyTo = inReplyTo, debugInfo = debugInfo, + isThreaded = isThreaded, origin = null ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index 7e21ff649d..f492407690 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -75,6 +76,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent +import io.element.android.libraries.designsystem.VectorIcons import io.element.android.libraries.designsystem.colors.AvatarColorsProvider import io.element.android.libraries.designsystem.components.EqualWidthColumn import io.element.android.libraries.designsystem.components.avatar.Avatar @@ -84,6 +86,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.swipe.SwipeableActionsState import io.element.android.libraries.designsystem.swipe.rememberSwipeableActionsState import io.element.android.libraries.designsystem.text.toPx +import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId @@ -370,14 +373,6 @@ private fun MessageEventBubbleContent( onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, @SuppressLint("ModifierParameter") bubbleModifier: Modifier = Modifier, // need to rename this modifier to distinguish it from the following ones ) { - val timestampPosition = when (event.content) { - is TimelineItemImageContent, - is TimelineItemVideoContent, - is TimelineItemLocationContent -> TimestampPosition.Overlay - is TimelineItemPollContent -> TimestampPosition.Below - else -> TimestampPosition.Default - } - val replyToDetails = event.inReplyTo as? InReplyTo.Ready // Long clicks are not not automatically propagated from a `clickable` // to its `combinedClickable` parent so we do it manually @@ -398,6 +393,24 @@ private fun MessageEventBubbleContent( ) } + @Composable + fun ThreadDecoration( + modifier: Modifier = Modifier + ) { + Row( + modifier = modifier, + horizontalArrangement = spacedBy(4.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(resourceId = VectorIcons.ThreadDecoration, contentDescription = null, tint = ElementTheme.colors.iconSecondary) + Text( + text = stringResource(CommonStrings.common_thread), + style = ElementTheme.typography.fontBodyXsRegular, + color = ElementTheme.colors.textPrimary, + ) + } + } + @Composable fun ContentAndTimestampView( timestampPosition: TimestampPosition, @@ -450,47 +463,74 @@ private fun MessageEventBubbleContent( /** Groups the different components in a Column with some space between them. */ @Composable fun CommonLayout( + timestampPosition: TimestampPosition, + showThreadDecoration: Boolean, inReplyToDetails: InReplyTo.Ready?, modifier: Modifier = Modifier ) { - var modifierWithPadding: Modifier = Modifier - var contentModifier: Modifier = Modifier - EqualWidthColumn(modifier = modifier, spacing = 8.dp) { - when { - inReplyToDetails != null -> { - val senderName = inReplyToDetails.senderDisplayName ?: inReplyToDetails.senderId.value - val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToDetails) - val text = textForInReplyTo(inReplyToDetails) - ReplyToContent( - senderName = senderName, - text = text, - attachmentThumbnailInfo = attachmentThumbnailInfo, - modifier = Modifier - .padding(top = 8.dp, start = 8.dp, end = 8.dp) - .clip(RoundedCornerShape(6.dp)) - .clickable(enabled = true, onClick = inReplyToClick), - ) - if (timestampPosition == TimestampPosition.Overlay) { - modifierWithPadding = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp) - contentModifier = Modifier.clip(RoundedCornerShape(12.dp)) - } else { - contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 0.dp, bottom = 8.dp) - } - } - timestampPosition != TimestampPosition.Overlay -> { - contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp) + val modifierWithPadding: Modifier + val contentModifier: Modifier + when { + inReplyToDetails != null -> { + if (timestampPosition == TimestampPosition.Overlay) { + modifierWithPadding = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp) + contentModifier = Modifier.clip(RoundedCornerShape(12.dp)) + } else { + contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 0.dp, bottom = 8.dp) + modifierWithPadding = Modifier } } + timestampPosition != TimestampPosition.Overlay -> { + modifierWithPadding = Modifier + contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp) + } + else -> { + modifierWithPadding = Modifier + contentModifier = Modifier + } + } + EqualWidthColumn(modifier = modifier, spacing = 8.dp) { + if (showThreadDecoration) { + ThreadDecoration(modifier = Modifier.padding(top = 8.dp, start = 12.dp, end = 12.dp)) + } + if (inReplyToDetails != null) { + val senderName = inReplyToDetails.senderDisplayName ?: inReplyToDetails.senderId.value + val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToDetails) + val text = textForInReplyTo(inReplyToDetails) + val topPadding = if (showThreadDecoration) 0.dp else 8.dp + ReplyToContent( + senderName = senderName, + text = text, + attachmentThumbnailInfo = attachmentThumbnailInfo, + modifier = Modifier + .padding(top = topPadding, start = 8.dp, end = 8.dp) + .clip(RoundedCornerShape(6.dp)) + .clickable(enabled = true, onClick = inReplyToClick), + ) + } ContentAndTimestampView( timestampPosition = timestampPosition, - contentModifier = contentModifier, modifier = modifierWithPadding, + contentModifier = contentModifier, ) } } - CommonLayout(inReplyToDetails = replyToDetails, modifier = bubbleModifier) + val timestampPosition = when (event.content) { + is TimelineItemImageContent, + is TimelineItemVideoContent, + is TimelineItemLocationContent -> TimestampPosition.Overlay + is TimelineItemPollContent -> TimestampPosition.Below + else -> TimestampPosition.Default + } + val replyToDetails = event.inReplyTo as? InReplyTo.Ready + CommonLayout( + showThreadDecoration = event.isThreaded, + timestampPosition = timestampPosition, + inReplyToDetails = replyToDetails, + modifier = bubbleModifier + ) } @Composable @@ -694,6 +734,7 @@ private fun ContentToPreviewWithReply() { aspectRatio = 5f ), inReplyTo = aInReplyToReady(replyContent), + isThreaded = true, groupPosition = TimelineItemGroupPosition.Last, ), isHighlighted = false, @@ -714,11 +755,11 @@ private fun ContentToPreviewWithReply() { } private fun aInReplyToReady( - replyContent: String + replyContent: String, ): InReplyTo.Ready { return InReplyTo.Ready( eventId = EventId("\$event"), - content = MessageContent(replyContent, null, false, TextMessageType(replyContent, null)), + content = MessageContent(replyContent, null, false, false, TextMessageType(replyContent, null)), senderId = UserId("@Sender:domain"), senderDisplayName = "Sender", senderAvatarUrl = null, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt index 4cb249af72..14f8429c85 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt @@ -71,6 +71,7 @@ class TimelineItemEventFactory @Inject constructor( url = senderAvatarUrl, size = AvatarSize.TimelineSender ) + currentTimelineItem.event return TimelineItem.Event( id = currentTimelineItem.uniqueId.toString(), eventId = currentTimelineItem.eventId, @@ -85,6 +86,7 @@ class TimelineItemEventFactory @Inject constructor( reactionsState = currentTimelineItem.computeReactionsState(), localSendState = currentTimelineItem.event.localSendState, inReplyTo = currentTimelineItem.event.inReplyTo(), + isThreaded = currentTimelineItem.event.isThreaded(), debugInfo = currentTimelineItem.event.debugInfo, origin = currentTimelineItem.event.origin, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt index b1a5c245b9..8f00dfb0dd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt @@ -66,6 +66,7 @@ sealed interface TimelineItem { val reactionsState: TimelineItemReactions, val localSendState: LocalEventSendState?, val inReplyTo: InReplyTo?, + val isThreaded: Boolean, val debugInfo: TimelineItemDebugInfo, val origin: TimelineItemEventOrigin?, ) : TimelineItem { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt index 4f1edcb64f..6d944075d1 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt @@ -37,6 +37,7 @@ internal fun aMessageEvent( isMine: Boolean = true, content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false), inReplyTo: InReplyTo? = null, + isThreaded: Boolean = false, debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), sendState: LocalEventSendState = LocalEventSendState.Sent(AN_EVENT_ID), ) = TimelineItem.Event( @@ -52,5 +53,6 @@ internal fun aMessageEvent( localSendState = sendState, inReplyTo = inReplyTo, debugInfo = debugInfo, + isThreaded = isThreaded, origin = null ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt index a842162b26..3f66269fe1 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt @@ -632,7 +632,7 @@ fun anEditMode( transactionId: TransactionId? = null, ) = MessageComposerMode.Edit(eventId, message, transactionId) -fun aReplyMode() = MessageComposerMode.Reply(A_USER_NAME, null, AN_EVENT_ID, A_MESSAGE) +fun aReplyMode() = MessageComposerMode.Reply(A_USER_NAME, null, false, AN_EVENT_ID, A_MESSAGE) fun aQuoteMode() = MessageComposerMode.Quote(AN_EVENT_ID, A_MESSAGE) private fun String.toMessage() = Message( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt index d5ce31f87a..4c43a8552f 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt @@ -44,6 +44,7 @@ class TimelineItemGrouperTest { reactionsState = aTimelineItemReactions(count = 0), localSendState = LocalEventSendState.Sent(AN_EVENT_ID), inReplyTo = null, + isThreaded = false, debugInfo = aTimelineItemDebugInfo(), origin = null ) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt index 20ebb85615..0194e86518 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt @@ -41,4 +41,5 @@ object VectorIcons { val Quote = R.drawable.ic_quote val Strikethrough = R.drawable.ic_strikethrough val Underline = R.drawable.ic_underline + val ThreadDecoration = R.drawable.ic_thread_decoration } diff --git a/libraries/designsystem/src/main/res/drawable/ic_thread_decoration.xml b/libraries/designsystem/src/main/res/drawable/ic_thread_decoration.xml new file mode 100644 index 0000000000..09d4ad4ace --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_thread_decoration.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTests.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTests.kt index 494c63784d..0f22105635 100644 --- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTests.kt +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTests.kt @@ -153,7 +153,7 @@ class DefaultRoomLastMessageFormatterTests { fun `Message contents`() { val body = "Shared body" fun createMessageContent(type: MessageType): MessageContent { - return MessageContent(body, null, false, type) + return MessageContent(body, null, false, false,type) } val sharedContentMessagesTypes = arrayOf( diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt index b16e8d2694..a3edf80bee 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt @@ -32,6 +32,7 @@ data class MessageContent( val body: String, val inReplyTo: InReplyTo?, val isEdited: Boolean, + val isThreaded: Boolean, val type: MessageType? ) : EventContent diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt index 50bf5f8ce5..49108f8d54 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt @@ -40,6 +40,11 @@ data class EventTimelineItem( fun inReplyTo(): InReplyTo? { return (content as? MessageContent)?.inReplyTo } + + fun isThreaded(): Boolean { + return (content as? MessageContent)?.isThreaded ?: false + } + fun hasNotLoadedInReplyTo(): Boolean { val details = inReplyTo() return details is InReplyTo.NotLoaded diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt index 6e6efc67f5..0a59cfddab 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt @@ -68,6 +68,7 @@ class EventMessageMapper { body = it.body(), inReplyTo = inReplyToEvent, isEdited = it.isEdited(), + isThreaded = it.isThreaded(), type = type ) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt index 59bac7ad40..12b0325da5 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt @@ -147,6 +147,7 @@ fun aMessageContent( body: String = "body", inReplyTo: InReplyTo? = null, isEdited: Boolean = false, + isThreaded: Boolean = false, messageType: MessageType = TextMessageType( body = body, formatted = null @@ -155,6 +156,7 @@ fun aMessageContent( body = body, inReplyTo = inReplyTo, isEdited = isEdited, + isThreaded = isThreaded, type = messageType ) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt index aa3e745ea2..3dbc652aaf 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt @@ -41,6 +41,7 @@ sealed interface MessageComposerMode : Parcelable { class Reply( val senderName: String, val attachmentThumbnailInfo: AttachmentThumbnailInfo?, + val isThreaded: Boolean, override val eventId: EventId, override val defaultContent: String ) : Special(eventId, defaultContent) @@ -60,5 +61,5 @@ sealed interface MessageComposerMode : Parcelable { get() = this is Reply val inThread: Boolean - get() = false // TODO + get() = this is Reply && isThreaded } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index 983c84c45b..b7c2b8ea40 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -185,9 +185,13 @@ fun TextComposer( if (composerMode is MessageComposerMode.Special) { ComposerModeView(composerMode = composerMode, onResetComposerMode = onResetComposerMode) } - TextInput( state = state, + placeholder = if (composerMode.inThread) { + stringResource(id = CommonStrings.action_reply_in_thread) + } else { + stringResource(id = CommonStrings.rich_text_editor_composer_placeholder) + }, roundedCorners = roundedCorners, bgColor = bgColor, onError = onError, @@ -239,6 +243,7 @@ fun TextComposer( @Composable private fun TextInput( state: RichTextEditorState, + placeholder: String, roundedCorners: RoundedCornerShape, bgColor: Color, modifier: Modifier = Modifier, @@ -265,7 +270,7 @@ private fun TextInput( // Placeholder if (state.messageHtml.isEmpty()) { Text( - stringResource(CommonStrings.common_message), + placeholder, style = defaultTypography.copy( color = ElementTheme.colors.textDisabled, ), @@ -689,6 +694,23 @@ internal fun TextComposerReplyPreview() = ElementPreview { canSendMessage = false, onSendMessage = {}, composerMode = MessageComposerMode.Reply( + isThreaded = false, + senderName = "Alice", + eventId = EventId("$1234"), + attachmentThumbnailInfo = null, + defaultContent = "A message\n" + + "With several lines\n" + + "To preview larger textfields and long lines with overflow" + ), + onResetComposerMode = {}, + enableTextFormatting = true, + ) + TextComposer( + RichTextEditorState("", fake = true), + canSendMessage = false, + onSendMessage = {}, + composerMode = MessageComposerMode.Reply( + isThreaded = true, senderName = "Alice", eventId = EventId("$1234"), attachmentThumbnailInfo = null, @@ -704,6 +726,7 @@ internal fun TextComposerReplyPreview() = ElementPreview { canSendMessage = true, onSendMessage = {}, composerMode = MessageComposerMode.Reply( + isThreaded = true, senderName = "Alice", eventId = EventId("$1234"), attachmentThumbnailInfo = AttachmentThumbnailInfo( @@ -722,6 +745,7 @@ internal fun TextComposerReplyPreview() = ElementPreview { canSendMessage = true, onSendMessage = {}, composerMode = MessageComposerMode.Reply( + isThreaded = false, senderName = "Alice", eventId = EventId("$1234"), attachmentThumbnailInfo = AttachmentThumbnailInfo( @@ -740,6 +764,7 @@ internal fun TextComposerReplyPreview() = ElementPreview { canSendMessage = true, onSendMessage = {}, composerMode = MessageComposerMode.Reply( + isThreaded = false, senderName = "Alice", eventId = EventId("$1234"), attachmentThumbnailInfo = AttachmentThumbnailInfo( @@ -758,6 +783,7 @@ internal fun TextComposerReplyPreview() = ElementPreview { canSendMessage = true, onSendMessage = {}, composerMode = MessageComposerMode.Reply( + isThreaded = false, senderName = "Alice", eventId = EventId("$1234"), attachmentThumbnailInfo = AttachmentThumbnailInfo( diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewDark_0_null_0,NEXUS_5,1.0,en].png index ee0937d439..67b7525ddc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3ea303577f655368800debe9c40e1292dc08a20da7f2ea6ddddb87f8407b112 -size 10431 +oid sha256:6bf428927e9a3493284d9fa7ba307b51315ed52b317a60ac345e87ba70849d0f +size 10523 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewLight_0_null_0,NEXUS_5,1.0,en].png index e537e910e3..8007500f81 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b646c06e55b50b64eb7b566fbc0bcafa7f7e348398f87152d008a47e7448f4e0 -size 10736 +oid sha256:6eced1d7173c2d0100351f5bb9cd14c649a826b2e355e426b9c6d4add90015d9 +size 10833 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReplyDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReplyDark_0_null,NEXUS_5,1.0,en].png index c39b41db87..a41e57eecc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReplyDark_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReplyDark_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b991a34a137d82ff4dedf48316a05ebbc008233984b11af70e64889a5fb5eda8 -size 127438 +oid sha256:2bf08003a076d8a206782888cc2fc3df297e53478cd4c0e0aa5c8a26069bb7fa +size 128065 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReplyLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReplyLight_0_null,NEXUS_5,1.0,en].png index a6500ca748..9c01abd666 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReplyLight_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReplyLight_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43b6514716d18382c8962520928d9d2ff6d0f665b2471e8a24e5d2e44a25c88c -size 132295 +oid sha256:8f5f950fce40c10710eb7fe4b193b4623633acdedfcf9029d6e0920e0c8d43d6 +size 133043 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_2,NEXUS_5,1.0,en].png index eb75a98270..8d7e2dcf95 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd991a67e7d6c1169e08e62cb74ed6fa7d12ad7f91d69bbfa778e0053efebfec -size 52284 +oid sha256:c329165fa341a2130b43a448b8bba465f1e5f458136efa6da80f2cdeee9d6caa +size 52369 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_2,NEXUS_5,1.0,en].png index 9603a65588..ec09599aca 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d421656ae8ad316e65f406c74dffa2ceb4e47b868b7f033f9cc4139e3e6be788 -size 53909 +oid sha256:2b4e5e9d920ea2a7733453e030bb365c9e2af7263de8fe216ab56088e0861fa5 +size 53997 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_2_null,NEXUS_5,1.0,en].png index 760cf75800..f9dc3f8b9a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0e4c8da669ee5383a7d0f828a3300b923ec38ab5e04bc388da0e83f1cf1ccf3 -size 38291 +oid sha256:1f065f63fb37fa9a5441cfaeca04e867e4e8f6f744dd02a83c459fc128ee353d +size 38384 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_3_null,NEXUS_5,1.0,en].png index 0b9f9f3ab7..ca3a85e52c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f26aa7b13403cfaa864778a6ec5c26edde5932f3d887c7148d3a2b9f60f6e6a -size 36392 +oid sha256:a5ded1cbc536c544b0cb188ba0a333a541157eaf1772cd0e06c7338307549adc +size 36483 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_4_null,NEXUS_5,1.0,en].png index 37e8a8561f..0d9c311dcc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e65e15c721a939ed700719cacbdee57e1ccb89d984e88bdb5ed772c4b583472a -size 81494 +oid sha256:69e98a3521ae6545700e395bff211b06bb02095b01d9c254b3e0d2d0b8b88d26 +size 80484 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_5_null,NEXUS_5,1.0,en].png index 466f321959..ef035d6cd5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_5_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_5_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49f6d3b7f93008640abf30a5a65ddca6845e16343ef97c3450faf9dd6d9b9cec -size 78788 +oid sha256:27dee9eaae6736a128107c9fd93048a677287a40d24c334223243b3c55f1cf69 +size 77686 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_1_null,NEXUS_5,1.0,en].png index d6beb7c22c..06fbfc6c7f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a998d3db9e4454fe74f31028b1dc61e4d6c07c824192f5e75563234dedf0b26 -size 44108 +oid sha256:267f482ceddeadea4c3b580d6783fed1048015fa05a11e385dd547e60f72e1e8 +size 44206 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_2_null,NEXUS_5,1.0,en].png index 863e8b8628..a237bcce80 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97ab0a0b64ea7704a1557fc34b24d7caea34f9951948f5f5637f0bda596288dd -size 41455 +oid sha256:9f5e7ab52469b406509964d71f12475df546973f1382944ce7f2e1a437e4f880 +size 41536