From 4a098583a062aa1b20279834e8eeaf9cd1035b5f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 4 Jul 2023 13:27:08 +0200 Subject: [PATCH 1/6] Handle swipe to reply gesture. --- .../features/messages/impl/MessagesView.kt | 5 + .../impl/timeline/TimelinePresenter.kt | 8 +- .../messages/impl/timeline/TimelineState.kt | 1 + .../impl/timeline/TimelineStateProvider.kt | 1 + .../messages/impl/timeline/TimelineView.kt | 14 +++ .../components/ReplySwipeIndicator.kt | 72 ++++++++++++ .../components/TimelineItemEventRow.kt | 106 ++++++++++++++++-- 7 files changed, 197 insertions(+), 10 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ReplySwipeIndicator.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 037199aba8..95df2428a8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -156,6 +156,9 @@ fun MessagesView( }, onReactionClicked = ::onEmojiReactionClicked, onSendLocationClicked = onSendLocationClicked, + onSwipeToReply = { targetEvent -> + state.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, targetEvent)) + }, ) }, snackbarHost = { @@ -241,6 +244,7 @@ fun MessagesViewContent( onTimestampClicked: (TimelineItem.Event) -> Unit, onSendLocationClicked: () -> Unit, modifier: Modifier = Modifier, + onSwipeToReply: (TimelineItem.Event) -> Unit, ) { Column( modifier = modifier @@ -258,6 +262,7 @@ fun MessagesViewContent( onUserDataClicked = onUserDataClicked, onTimestampClicked = onTimestampClicked, onReactionClicked = onReactionClicked, + onSwipeToReply = onSwipeToReply, ) } if (state.userHasPermissionToSendMessage) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 071dc84116..765becfd11 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -30,7 +30,9 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.timeline.MatrixTimeline +import io.element.android.libraries.matrix.ui.room.canSendEventAsState import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.launchIn @@ -44,7 +46,7 @@ private const val backPaginationPageSize = 50 class TimelinePresenter @Inject constructor( private val timelineItemsFactory: TimelineItemsFactory, - room: MatrixRoom, + private val room: MatrixRoom, ) : Presenter { private val timeline = room.timeline @@ -62,6 +64,9 @@ class TimelinePresenter @Inject constructor( val timelineItems by timelineItemsFactory.collectItemsAsState() val paginationState by timeline.paginationState.collectAsState() + val syncUpdateFlow = room.syncUpdateFlow.collectAsState() + val userHasPermissionToSendMessage by room.canSendEventAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value) + fun handleEvents(event: TimelineEvents) { when (event) { TimelineEvents.LoadMore -> localCoroutineScope.loadMore(paginationState) @@ -92,6 +97,7 @@ class TimelinePresenter @Inject constructor( return TimelineState( highlightedEventId = highlightedEventId.value, + canReply = userHasPermissionToSendMessage, paginationState = paginationState, timelineItems = timelineItems, eventSink = ::handleEvents diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt index 0e86614bf3..0aa1bd0160 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt @@ -26,6 +26,7 @@ import kotlinx.collections.immutable.ImmutableList data class TimelineState( val timelineItems: ImmutableList, val highlightedEventId: EventId?, + val canReply: Boolean, val paginationState: MatrixTimeline.PaginationState, val eventSink: (TimelineEvents) -> Unit ) 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 fd7c4402f4..9dd01d60ad 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 @@ -43,6 +43,7 @@ fun aTimelineState(timelineItems: ImmutableList = persistentListOf timelineItems = timelineItems, paginationState = MatrixTimeline.PaginationState(isBackPaginating = false, canBackPaginate = true), highlightedEventId = null, + canReply = true, eventSink = {} ) 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 ab1adb97ae..443b77a8cb 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 @@ -80,6 +80,7 @@ fun TimelineView( onMessageClicked: (TimelineItem.Event) -> Unit, onMessageLongClicked: (TimelineItem.Event) -> Unit, onTimestampClicked: (TimelineItem.Event) -> Unit, + onSwipeToReply: (TimelineItem.Event) -> Unit, onReactionClicked: (emoji: String, TimelineItem.Event) -> Unit, modifier: Modifier = Modifier, ) { @@ -120,12 +121,14 @@ fun TimelineView( TimelineItemRow( timelineItem = timelineItem, highlightedItem = state.highlightedEventId?.value, + canReply = state.canReply, onClick = onMessageClicked, onLongClick = onMessageLongClicked, onUserDataClick = onUserDataClicked, inReplyToClick = ::inReplyToClicked, onReactionClick = onReactionClicked, onTimestampClicked = onTimestampClicked, + onSwipeToReply = onSwipeToReply, ) if (index == state.timelineItems.lastIndex) { onReachedLoadMore() @@ -145,12 +148,14 @@ fun TimelineView( fun TimelineItemRow( timelineItem: TimelineItem, highlightedItem: String?, + canReply: Boolean, onUserDataClick: (UserId) -> Unit, onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, inReplyToClick: (EventId) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit, onTimestampClicked: (TimelineItem.Event) -> Unit, + onSwipeToReply: (TimelineItem.Event) -> Unit, modifier: Modifier = Modifier ) { when (timelineItem) { @@ -165,6 +170,10 @@ fun TimelineItemRow( onClick(timelineItem) } + fun onSwipeToReply() { + onSwipeToReply(timelineItem) + } + fun onLongClick() { onLongClick(timelineItem) } @@ -181,12 +190,14 @@ fun TimelineItemRow( TimelineItemEventRow( event = timelineItem, isHighlighted = highlightedItem == timelineItem.identifier(), + canReply = canReply, onClick = ::onClick, onLongClick = ::onLongClick, onUserDataClick = onUserDataClick, inReplyToClick = inReplyToClick, onReactionClick = onReactionClick, onTimestampClicked = onTimestampClicked, + onSwipeToReply = ::onSwipeToReply, modifier = modifier, ) } @@ -215,12 +226,14 @@ fun TimelineItemRow( TimelineItemRow( timelineItem = subGroupEvent, highlightedItem = highlightedItem, + canReply = false, onClick = onClick, onLongClick = onLongClick, inReplyToClick = inReplyToClick, onUserDataClick = onUserDataClick, onTimestampClicked = onTimestampClicked, onReactionClick = onReactionClick, + onSwipeToReply = {}, ) } } @@ -322,5 +335,6 @@ private fun ContentToPreview(content: TimelineItemEventContent) { onUserDataClicked = {}, onMessageLongClicked = {}, onReactionClicked = { _, _ -> }, + onSwipeToReply = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ReplySwipeIndicator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ReplySwipeIndicator.kt new file mode 100644 index 0000000000..8e3fdf98cf --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ReplySwipeIndicator.kt @@ -0,0 +1,72 @@ +/* + * 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.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.VectorIcons +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Icon + +/** + * A swipe indicator that appears when swiping to reply to a message. + * + * @param swipeProgress the progress of the swipe, between 0 and X. When swipeProgress >= 1 the swipe will be detected. + */ +@Composable +fun RowScope.ReplySwipeIndicator(swipeProgress: Float) { + Icon( + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(start = 36.dp * swipeProgress.coerceAtMost(1f)) + .alpha(swipeProgress), + contentDescription = null, + resourceId = VectorIcons.Reply, + ) +} + +@Preview +@Composable +internal fun ReplySwipeIndicatorLightPreview() = + ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun ReplySwipeIndicatorDarkPreview() = + ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column(modifier = Modifier.fillMaxWidth()) { + for (i in 0..8) { + Row { ReplySwipeIndicator(swipeProgress = i / 8f) } + } + Row { ReplySwipeIndicator(swipeProgress = 1.5f) } + Row { ReplySwipeIndicator(swipeProgress = 2f) } + Row { ReplySwipeIndicator(swipeProgress = 3f) } + } +} 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 f2f7328821..9cbe258d4d 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 @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalMaterial3Api::class) + package io.element.android.features.messages.impl.timeline.components import androidx.compose.foundation.Canvas @@ -33,7 +35,13 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.DismissDirection +import androidx.compose.material3.DismissState +import androidx.compose.material3.DismissValue +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SwipeToDismiss +import androidx.compose.material3.rememberDismissState import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -86,12 +94,14 @@ import org.jsoup.Jsoup fun TimelineItemEventRow( event: TimelineItem.Event, isHighlighted: Boolean, + canReply: Boolean, onClick: () -> Unit, onLongClick: () -> Unit, onUserDataClick: (UserId) -> Unit, inReplyToClick: (EventId) -> Unit, onTimestampClicked: (TimelineItem.Event) -> Unit, onReactionClick: (emoji: String, eventId: TimelineItem.Event) -> Unit, + onSwipeToReply: () -> Unit, modifier: Modifier = Modifier ) { val interactionSource = remember { MutableInteractionSource() } @@ -108,6 +118,72 @@ fun TimelineItemEventRow( inReplyToClick(inReplyToEventId) } + if (canReply) { + val dismissState = rememberDismissState( + confirmValueChange = { + if (it == DismissValue.DismissedToEnd) { + onSwipeToReply() + } + // Do not dismiss the message, return false! + false + } + ) + SwipeToDismiss( + state = dismissState, + background = { + ReplySwipeIndicator(dismissState.toSwipeProgress()) + }, + directions = setOf(DismissDirection.StartToEnd), + dismissContent = { + TimelineItemEventRowContent( + modifier = Modifier, + event = event, + isHighlighted = isHighlighted, + interactionSource = interactionSource, + onClick = onClick, + onLongClick = onLongClick, + onTimestampClicked = onTimestampClicked, + inReplyToClicked = ::inReplyToClicked, + onUserDataClicked = ::onUserDataClicked, + onReactionClicked = ::onReactionClicked + ) + } + ) + } else { + TimelineItemEventRowContent( + modifier = Modifier, + event = event, + isHighlighted = isHighlighted, + interactionSource = interactionSource, + onClick = onClick, + onLongClick = onLongClick, + onTimestampClicked = onTimestampClicked, + inReplyToClicked = ::inReplyToClicked, + onUserDataClicked = ::onUserDataClicked, + onReactionClicked = ::onReactionClicked + ) + } + // This is assuming that we are in a ColumnScope, but this is OK, for both Preview and real usage. + if (event.groupPosition.isNew()) { + Spacer(modifier = modifier.height(16.dp)) + } else { + Spacer(modifier = modifier.height(2.dp)) + } +} + +@Composable +private fun TimelineItemEventRowContent( + modifier: Modifier, + event: TimelineItem.Event, + isHighlighted: Boolean, + interactionSource: MutableInteractionSource, + onClick: () -> Unit, + onLongClick: () -> Unit, + onTimestampClicked: (TimelineItem.Event) -> Unit, + inReplyToClicked: () -> Unit, + onUserDataClicked: () -> Unit, + onReactionClicked: (emoji: String) -> Unit +) { // To avoid using negative offset, we display in this Box a column with: // - Spacer to give room to the Sender information if they must be displayed; // - The message bubble; @@ -140,7 +216,7 @@ fun TimelineItemEventRow( interactionSource = interactionSource, onMessageClick = onClick, onMessageLongClick = onLongClick, - inReplyToClick = ::inReplyToClicked, + inReplyToClick = inReplyToClicked, onTimestampClicked = { onTimestampClicked(event) } @@ -158,25 +234,27 @@ fun TimelineItemEventRow( Modifier .padding(horizontal = 16.dp) .align(Alignment.TopStart) - .clickable(onClick = ::onUserDataClicked) + .clickable(onClick = onUserDataClicked) ) } // Align to the bottom of the box if (event.reactionsState.reactions.isNotEmpty()) { TimelineItemReactionsView( reactionsState = event.reactionsState, - onReactionClicked = ::onReactionClicked, + onReactionClicked = onReactionClicked, modifier = Modifier .align(if (event.isMine) Alignment.BottomEnd else Alignment.BottomStart) .padding(start = if (event.isMine) 16.dp else 36.dp, end = 16.dp) ) } } - // This is assuming that we are in a ColumnScope, but this is OK, for both Preview and real usage. - if (event.groupPosition.isNew()) { - Spacer(modifier = modifier.height(16.dp)) - } else { - Spacer(modifier = modifier.height(2.dp)) +} + +private fun DismissState.toSwipeProgress(): Float { + return when (targetValue) { + DismissValue.Default -> 0f + DismissValue.DismissedToEnd -> progress * 3 + DismissValue.DismissedToStart -> progress * 3 } } @@ -452,12 +530,14 @@ private fun ContentToPreview() { ) ), isHighlighted = false, + canReply = true, onClick = {}, onLongClick = {}, onUserDataClick = {}, inReplyToClick = {}, onReactionClick = { _, _ -> }, onTimestampClicked = {}, + onSwipeToReply = {}, ) TimelineItemEventRow( event = aTimelineItemEvent( @@ -467,12 +547,14 @@ private fun ContentToPreview() { ) ), isHighlighted = false, + canReply = true, onClick = {}, onLongClick = {}, onUserDataClick = {}, inReplyToClick = {}, onReactionClick = { _, _ -> }, onTimestampClicked = {}, + onSwipeToReply = {}, ) } } @@ -492,7 +574,7 @@ internal fun TimelineItemEventRowWithReplyDarkPreview() = private fun ContentToPreviewWithReply() { Column { sequenceOf(false, true).forEach { - val replyContent = if(it) { + val replyContent = if (it) { // Short "Message which are being replied." } else { @@ -509,12 +591,14 @@ private fun ContentToPreviewWithReply() { inReplyTo = aInReplyToReady(replyContent) ), isHighlighted = false, + canReply = true, onClick = {}, onLongClick = {}, onUserDataClick = {}, inReplyToClick = {}, onReactionClick = { _, _ -> }, onTimestampClicked = {}, + onSwipeToReply = {}, ) TimelineItemEventRow( event = aTimelineItemEvent( @@ -525,12 +609,14 @@ private fun ContentToPreviewWithReply() { inReplyTo = aInReplyToReady(replyContent) ), isHighlighted = false, + canReply = true, onClick = {}, onLongClick = {}, onUserDataClick = {}, inReplyToClick = {}, onReactionClick = { _, _ -> }, onTimestampClicked = {}, + onSwipeToReply = {}, ) } } @@ -578,12 +664,14 @@ private fun ContentTimestampToPreview(event: TimelineItem.Event) { senderDisplayName = if (useDocument) "Document case" else "Text case", ), isHighlighted = false, + canReply = true, onClick = {}, onLongClick = {}, onUserDataClick = {}, inReplyToClick = {}, onReactionClick = { _, _ -> }, onTimestampClicked = {}, + onSwipeToReply = {}, ) } } From e55ed2f10b2b5728c33ef9b2ef3bc16acfa30dde Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 4 Jul 2023 13:43:35 +0000 Subject: [PATCH 2/6] Update screenshots --- ...p_ReplySwipeIndicatorDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ ..._ReplySwipeIndicatorLightPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_ReplySwipeIndicatorDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_ReplySwipeIndicatorLightPreview_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_ReplySwipeIndicatorDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_ReplySwipeIndicatorDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f15ddfd706 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_ReplySwipeIndicatorDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6418097195ef3c6a166d216913469aa3adf30a4fb42fbda21bc27e321d431410 +size 9792 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_ReplySwipeIndicatorLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_ReplySwipeIndicatorLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8c614712a5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_ReplySwipeIndicatorLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f64649306919ac8dfeb6a7729f73a0617e8fc809c2d62f3aeabac6566bced1e +size 9151 From db3248441632b994754f3b0ce4bccc0eb2c7e0dc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Jul 2023 15:10:52 +0200 Subject: [PATCH 3/6] Fix issue around modifier. --- .../impl/timeline/components/ReplySwipeIndicator.kt | 7 +++++-- .../impl/timeline/components/TimelineItemEventRow.kt | 10 ++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ReplySwipeIndicator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ReplySwipeIndicator.kt index 8e3fdf98cf..7ca5955cd8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ReplySwipeIndicator.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ReplySwipeIndicator.kt @@ -38,9 +38,12 @@ import io.element.android.libraries.designsystem.theme.components.Icon * @param swipeProgress the progress of the swipe, between 0 and X. When swipeProgress >= 1 the swipe will be detected. */ @Composable -fun RowScope.ReplySwipeIndicator(swipeProgress: Float) { +fun RowScope.ReplySwipeIndicator( + swipeProgress: Float, + modifier: Modifier = Modifier, +) { Icon( - modifier = Modifier + modifier = modifier .align(Alignment.CenterVertically) .padding(start = 36.dp * swipeProgress.coerceAtMost(1f)) .alpha(swipeProgress), 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 9cbe258d4d..e64e753e98 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 @@ -136,7 +136,6 @@ fun TimelineItemEventRow( directions = setOf(DismissDirection.StartToEnd), dismissContent = { TimelineItemEventRowContent( - modifier = Modifier, event = event, isHighlighted = isHighlighted, interactionSource = interactionSource, @@ -145,13 +144,12 @@ fun TimelineItemEventRow( onTimestampClicked = onTimestampClicked, inReplyToClicked = ::inReplyToClicked, onUserDataClicked = ::onUserDataClicked, - onReactionClicked = ::onReactionClicked + onReactionClicked = ::onReactionClicked, ) } ) } else { TimelineItemEventRowContent( - modifier = Modifier, event = event, isHighlighted = isHighlighted, interactionSource = interactionSource, @@ -160,7 +158,7 @@ fun TimelineItemEventRow( onTimestampClicked = onTimestampClicked, inReplyToClicked = ::inReplyToClicked, onUserDataClicked = ::onUserDataClicked, - onReactionClicked = ::onReactionClicked + onReactionClicked = ::onReactionClicked, ) } // This is assuming that we are in a ColumnScope, but this is OK, for both Preview and real usage. @@ -173,7 +171,6 @@ fun TimelineItemEventRow( @Composable private fun TimelineItemEventRowContent( - modifier: Modifier, event: TimelineItem.Event, isHighlighted: Boolean, interactionSource: MutableInteractionSource, @@ -182,7 +179,8 @@ private fun TimelineItemEventRowContent( onTimestampClicked: (TimelineItem.Event) -> Unit, inReplyToClicked: () -> Unit, onUserDataClicked: () -> Unit, - onReactionClicked: (emoji: String) -> Unit + onReactionClicked: (emoji: String) -> Unit, + modifier: Modifier = Modifier, ) { // To avoid using negative offset, we display in this Box a column with: // - Spacer to give room to the Sender information if they must be displayed; From 9fb8900a5e1cc4057600c13cc80e1e8d2a18384e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Jul 2023 15:12:11 +0200 Subject: [PATCH 4/6] Fix outdated doc. --- .../messages/impl/timeline/components/ReplySwipeIndicator.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ReplySwipeIndicator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ReplySwipeIndicator.kt index 7ca5955cd8..9bab4f28ab 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ReplySwipeIndicator.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ReplySwipeIndicator.kt @@ -36,6 +36,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon * A swipe indicator that appears when swiping to reply to a message. * * @param swipeProgress the progress of the swipe, between 0 and X. When swipeProgress >= 1 the swipe will be detected. + * @param modifier the modifier to apply to this Composable root. */ @Composable fun RowScope.ReplySwipeIndicator( From 336564d8b548713a276d6ac330407d263cb61f36 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Jul 2023 15:20:34 +0200 Subject: [PATCH 5/6] Ensure the latest version of `timelineItem` is used. (other methods have been removed in #771) --- .../android/features/messages/impl/timeline/TimelineView.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 443b77a8cb..3e6ba59c8d 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 @@ -170,10 +170,6 @@ fun TimelineItemRow( onClick(timelineItem) } - fun onSwipeToReply() { - onSwipeToReply(timelineItem) - } - fun onLongClick() { onLongClick(timelineItem) } @@ -197,7 +193,7 @@ fun TimelineItemRow( inReplyToClick = inReplyToClick, onReactionClick = onReactionClick, onTimestampClicked = onTimestampClicked, - onSwipeToReply = ::onSwipeToReply, + onSwipeToReply = { onSwipeToReply(timelineItem) }, modifier = modifier, ) } From 570be3b73bbe8e83d8724f565a67b21a84163fa7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Jul 2023 16:42:46 +0200 Subject: [PATCH 6/6] Prefer using `graphicsLayer` for better performance issue (limit number of recompositions) --- .../components/ReplySwipeIndicator.kt | 19 ++++++++++--------- .../components/TimelineItemEventRow.kt | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ReplySwipeIndicator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ReplySwipeIndicator.kt index 9bab4f28ab..de40ae8dc1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ReplySwipeIndicator.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ReplySwipeIndicator.kt @@ -20,11 +20,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.VectorIcons @@ -40,14 +39,16 @@ import io.element.android.libraries.designsystem.theme.components.Icon */ @Composable fun RowScope.ReplySwipeIndicator( - swipeProgress: Float, + swipeProgress: () -> Float, modifier: Modifier = Modifier, ) { Icon( modifier = modifier .align(Alignment.CenterVertically) - .padding(start = 36.dp * swipeProgress.coerceAtMost(1f)) - .alpha(swipeProgress), + .graphicsLayer { + translationX = 36.dp.toPx() * swipeProgress().coerceAtMost(1f) + alpha = swipeProgress() + }, contentDescription = null, resourceId = VectorIcons.Reply, ) @@ -67,10 +68,10 @@ internal fun ReplySwipeIndicatorDarkPreview() = private fun ContentToPreview() { Column(modifier = Modifier.fillMaxWidth()) { for (i in 0..8) { - Row { ReplySwipeIndicator(swipeProgress = i / 8f) } + Row { ReplySwipeIndicator(swipeProgress = { i / 8f }) } } - Row { ReplySwipeIndicator(swipeProgress = 1.5f) } - Row { ReplySwipeIndicator(swipeProgress = 2f) } - Row { ReplySwipeIndicator(swipeProgress = 3f) } + Row { ReplySwipeIndicator(swipeProgress = { 1.5f }) } + Row { ReplySwipeIndicator(swipeProgress = { 2f }) } + Row { ReplySwipeIndicator(swipeProgress = { 3f }) } } } 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 e64e753e98..b46fb93e9c 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 @@ -131,7 +131,7 @@ fun TimelineItemEventRow( SwipeToDismiss( state = dismissState, background = { - ReplySwipeIndicator(dismissState.toSwipeProgress()) + ReplySwipeIndicator({ dismissState.toSwipeProgress() }) }, directions = setOf(DismissDirection.StartToEnd), dismissContent = {