diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 9249b808a1..beaba74a8c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -20,9 +20,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.textcomposer.AttachmentSourcePicker import io.element.android.features.messages.impl.textcomposer.aMessageComposerState -import io.element.android.features.messages.impl.timeline.aTimelineItemContent import io.element.android.features.messages.impl.timeline.aTimelineItemList import io.element.android.features.messages.impl.timeline.aTimelineState +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.RoomId @@ -48,7 +48,7 @@ fun aMessagesState() = MessagesState( mode = MessageComposerMode.Normal("Hello"), ), timelineState = aTimelineState().copy( - timelineItems = aTimelineItemList(aTimelineItemContent()), + timelineItems = aTimelineItemList(aTimelineItemTextContent()), ), actionListState = anActionListState(), hasNetworkConnection = 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 f6eaa55267..e119cb6e6c 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 @@ -21,7 +21,8 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId @@ -55,6 +56,12 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList content = content, groupPosition = TimelineItemGroupPosition.First ), + // A state event on top of it + aTimelineItemEvent( + isMine = false, + content = aTimelineItemStateEventContent(), + groupPosition = TimelineItemGroupPosition.None + ), // 3 items (First Middle Last) with isMine = true aTimelineItemEvent( isMine = true, @@ -71,12 +78,18 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList content = content, groupPosition = TimelineItemGroupPosition.First ), + // A state event on top of it + aTimelineItemEvent( + isMine = true, + content = aTimelineItemStateEventContent(), + groupPosition = TimelineItemGroupPosition.None + ), ) } internal fun aTimelineItemEvent( isMine: Boolean = false, - content: TimelineItemEventContent = aTimelineItemContent(), + content: TimelineItemEventContent = aTimelineItemTextContent(), groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.First ): TimelineItem.Event { val randomId = "\$" + Random.nextInt().toString() @@ -96,10 +109,3 @@ internal fun aTimelineItemEvent( groupPosition = groupPosition, ) } - -internal fun aTimelineItemContent(): TimelineItemEventContent { - return TimelineItemTextContent( - body = "Text", - htmlDocument = null - ) -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index a378c508f6..954b1321f7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -55,6 +55,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import io.element.android.features.messages.impl.timeline.components.MessageEventBubble +import io.element.android.features.messages.impl.timeline.components.MessageStateEventContainer import io.element.android.features.messages.impl.timeline.components.TimelineItemReactionsView import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView @@ -63,6 +64,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingModel import io.element.android.libraries.designsystem.components.avatar.Avatar @@ -130,11 +132,12 @@ fun TimelineItemRow( onLongClick: (TimelineItem.Event) -> Unit, ) { when (timelineItem) { - is TimelineItem.Virtual -> TimelineItemVirtualRow( - virtual = timelineItem - ) + is TimelineItem.Virtual -> { + TimelineItemVirtualRow( + virtual = timelineItem + ) + } is TimelineItem.Event -> { - fun onClick() { onClick(timelineItem) } @@ -143,12 +146,21 @@ fun TimelineItemRow( onLongClick(timelineItem) } - TimelineItemEventRow( - event = timelineItem, - isHighlighted = isHighlighted, - onClick = ::onClick, - onLongClick = ::onLongClick - ) + if (timelineItem.content is TimelineItemStateContent) { + TimelineItemStateEventRow( + event = timelineItem, + isHighlighted = isHighlighted, + onClick = ::onClick, + onLongClick = ::onLongClick + ) + } else { + TimelineItemEventRow( + event = timelineItem, + isHighlighted = isHighlighted, + onClick = ::onClick, + onLongClick = ::onLongClick + ) + } } } } @@ -232,6 +244,42 @@ fun TimelineItemEventRow( } } +@Composable +fun TimelineItemStateEventRow( + event: TimelineItem.Event, + isHighlighted: Boolean, + onClick: () -> Unit, + onLongClick: () -> Unit, + modifier: Modifier = Modifier +) { + val interactionSource = remember { MutableInteractionSource() } + Box( + modifier = modifier + .fillMaxWidth() + .wrapContentHeight(), + contentAlignment = Alignment.Center + ) { + MessageStateEventContainer( + isHighlighted = isHighlighted, + interactionSource = interactionSource, + onClick = onClick, + onLongClick = onLongClick, + modifier = Modifier + .zIndex(-1f) + .widthIn(max = 320.dp) + ) { + val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) + TimelineItemEventContentView( + content = event.content, + interactionSource = interactionSource, + onClick = onClick, + onLongClick = onLongClick, + modifier = contentModifier + ) + } + } +} + @Composable private fun MessageSenderInformation( sender: String, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt index b696b63f92..606d185568 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt @@ -84,6 +84,7 @@ fun MessageEventBubble( fun Modifier.offsetForItem(): Modifier { return if (state.isMine) { + // FIXME setting y offset to -12.dp can overlap a state event displayed above. offset(y = -(12.dp)) } else { offset(x = 20.dp, y = -(12.dp)) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageStateEventContainer.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageStateEventContainer.kt new file mode 100644 index 0000000000..fa1faaa93d --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageStateEventContainer.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.components + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.Surface + +private val CORNER_RADIUS = 8.dp + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun MessageStateEventContainer( + isHighlighted: Boolean, + interactionSource: MutableInteractionSource, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + onLongClick: () -> Unit = {}, + content: @Composable () -> Unit = {}, +) { + val backgroundColor = if (isHighlighted) { + ElementTheme.colors.messageHighlightedBackground + } else { + Color.Companion.Transparent + } + val shape = RoundedCornerShape(CORNER_RADIUS) + Surface( + modifier = modifier + .widthIn(min = 80.dp) + .clip(shape) + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + indication = rememberRipple(), + interactionSource = interactionSource + ), + color = backgroundColor, + shape = shape, + content = content + ) +} + +@Preview +@Composable +internal fun MessageStateEventContainerLightPreview() = + ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun MessageStateEventContainerDarkPreview() = + ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + MessageStateEventContainer( + isHighlighted = false, + interactionSource = MutableInteractionSource(), + ) { + Spacer(modifier = Modifier.size(width = 120.dp, height = 32.dp)) + } + MessageStateEventContainer( + isHighlighted = true, + interactionSource = MutableInteractionSource(), + ) { + Spacer(modifier = Modifier.size(width = 120.dp, height = 32.dp)) + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt index bfb1d7f0c7..103b444cf9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt @@ -23,6 +23,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent @@ -58,5 +59,9 @@ fun TimelineItemEventContentView( content = content, modifier = modifier ) + is TimelineItemStateContent -> TimelineItemStateView( + content = content, + modifier = modifier + ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStateView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStateView.kt new file mode 100644 index 0000000000..1402d59fd6 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStateView.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.components.event + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.sp +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Text + +@Composable +fun TimelineItemStateView( + content: TimelineItemStateContent, + modifier: Modifier = Modifier +) { + Text( + modifier = modifier, + color = MaterialTheme.colorScheme.secondary, + fontSize = 13.sp, + text = content.body, + textAlign = TextAlign.Center, + ) +} + +@Preview +@Composable +internal fun TimelineItemStateViewLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun TimelineItemStateViewDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + TimelineItemStateView( + content = aTimelineItemStateEventContent(), + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt index 7e4be1bbd1..433c69088e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt @@ -65,3 +65,7 @@ fun aTimelineItemTextContent() = TimelineItemTextContent( ) fun aTimelineItemUnknownContent() = TimelineItemUnknownContent + +fun aTimelineItemStateEventContent() = TimelineItemStateEventContent( + body = "A state event", +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemProfileChangeContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemProfileChangeContent.kt index 29836dc28c..7d56394893 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemProfileChangeContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemProfileChangeContent.kt @@ -16,11 +16,8 @@ package io.element.android.features.messages.impl.timeline.model.event -import org.jsoup.nodes.Document - data class TimelineItemProfileChangeContent( override val body: String, - override val htmlDocument: Document? = null -) : TimelineItemTextBasedContent { +) : TimelineItemStateContent { override val type: String = "TimelineItemProfileChangeContent" } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRoomMembershipContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRoomMembershipContent.kt index c18393b7a0..93607f01e6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRoomMembershipContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRoomMembershipContent.kt @@ -16,11 +16,8 @@ package io.element.android.features.messages.impl.timeline.model.event -import org.jsoup.nodes.Document - data class TimelineItemRoomMembershipContent( override val body: String, - override val htmlDocument: Document? = null -) : TimelineItemTextBasedContent { +) : TimelineItemStateContent { override val type: String = "TimelineItemRoomMembershipContent" } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateContent.kt new file mode 100644 index 0000000000..b136a602b2 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateContent.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.model.event + +sealed interface TimelineItemStateContent : TimelineItemEventContent { + val body: String +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateEventContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateEventContent.kt index 8ac3e5a40b..1c656c9b96 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateEventContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateEventContent.kt @@ -16,11 +16,8 @@ package io.element.android.features.messages.impl.timeline.model.event -import org.jsoup.nodes.Document - data class TimelineItemStateEventContent( override val body: String, - override val htmlDocument: Document? = null -) : TimelineItemTextBasedContent { +) : TimelineItemStateContent { override val type: String = "TimelineItemStateEventContent" }