From 900cf1881fb77a9a9c1840f8ae0618a461646f13 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 16 Nov 2023 12:12:55 +0100 Subject: [PATCH] Read receipt: Bottom sheet --- .../messages/impl/MessagesPresenter.kt | 4 + .../features/messages/impl/MessagesState.kt | 2 + .../messages/impl/MessagesStateProvider.kt | 5 + .../features/messages/impl/MessagesView.kt | 20 ++-- .../receipt/ReadReceiptViewStateProvider.kt | 4 +- .../ReadReceiptBottomSheetEvents.kt | 24 ++++ .../ReadReceiptBottomSheetPresenter.kt | 52 +++++++++ .../ReadReceiptBottomSheetState.kt | 26 +++++ .../ReadReceiptBottomSheetStateProvider.kt | 44 +++++++ .../bottomsheet/ReadReceiptBottomSheetView.kt | 107 ++++++++++++++++++ .../event/TimelineItemEventFactory.kt | 6 +- .../model/TimelineItemReadReceipts.kt | 2 +- .../messages/fixtures/timelineItemsFactory.kt | 2 + .../components/avatar/AvatarSize.kt | 2 + .../matrix/ui/components/MatrixUserRow.kt | 2 + .../libraries/matrix/ui/components/UserRow.kt | 5 +- 16 files changed, 290 insertions(+), 17 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetEvents.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetPresenter.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetState.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetStateProvider.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetView.kt 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 a9a9e7fa50..2bb6e85df4 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 @@ -42,6 +42,7 @@ import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.TimelineState import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter +import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetPresenter import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent @@ -97,6 +98,7 @@ class MessagesPresenter @AssistedInject constructor( private val customReactionPresenter: CustomReactionPresenter, private val reactionSummaryPresenter: ReactionSummaryPresenter, private val retrySendMenuPresenter: RetrySendMenuPresenter, + private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter, private val networkMonitor: NetworkMonitor, private val snackbarDispatcher: SnackbarDispatcher, private val messageSummaryFormatter: MessageSummaryFormatter, @@ -124,6 +126,7 @@ class MessagesPresenter @AssistedInject constructor( val customReactionState = customReactionPresenter.present() val reactionSummaryState = reactionSummaryPresenter.present() val retryState = retrySendMenuPresenter.present() + val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value) @@ -201,6 +204,7 @@ class MessagesPresenter @AssistedInject constructor( customReactionState = customReactionState, reactionSummaryState = reactionSummaryState, retrySendMenuState = retryState, + readReceiptBottomSheetState = readReceiptBottomSheetState, hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online, snackbarMessage = snackbarMessage, showReinvitePrompt = showReinvitePrompt, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 5bce4f19a1..325c695988 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -22,6 +22,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer import io.element.android.features.messages.impl.timeline.TimelineState import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState +import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.libraries.architecture.Async @@ -43,6 +44,7 @@ data class MessagesState( val customReactionState: CustomReactionState, val reactionSummaryState: ReactionSummaryState, val retrySendMenuState: RetrySendMenuState, + val readReceiptBottomSheetState: ReadReceiptBottomSheetState, val hasNetworkConnection: Boolean, val snackbarMessage: SnackbarMessage?, val inviteProgress: Async, 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 38d6289a88..720c385f89 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 @@ -24,6 +24,7 @@ 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.components.customreaction.CustomReactionState import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState +import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState @@ -96,6 +97,10 @@ fun aMessagesState() = MessagesState( selectedEvent = null, eventSink = {}, ), + readReceiptBottomSheetState = ReadReceiptBottomSheetState( + selectedEvent = null, + eventSink = {}, + ), actionListState = anActionListState(), customReactionState = CustomReactionState( target = CustomReactionState.Target.None, 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 dbb8778131..c47bb99112 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 @@ -70,6 +70,8 @@ import io.element.android.features.messages.impl.timeline.components.customreact import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvents import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryView +import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents +import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetView import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMessageMenu import io.element.android.features.messages.impl.timeline.model.TimelineItem @@ -212,9 +214,8 @@ fun MessagesView( onReactionClicked = ::onEmojiReactionClicked, onReactionLongClicked = ::onEmojiReactionLongClicked, onMoreReactionsClicked = ::onMoreReactionsClicked, - onReadReceiptClick = { // targetEvent -> - // TODO Open bottom sheet with read receipts - // state.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ShowReadReceipts, targetEvent)) + onReadReceiptClick = { event -> + state.readReceiptBottomSheetState.eventSink(ReadReceiptBottomSheetEvents.EventSelected(event)) }, onSendLocationClicked = onSendLocationClicked, onCreatePollClicked = onCreatePollClicked, @@ -250,13 +251,9 @@ fun MessagesView( ) ReactionSummaryView(state = state.reactionSummaryState) - RetrySendMessageMenu( - state = state.retrySendMenuState - ) - - ReinviteDialog( - state = state - ) + RetrySendMessageMenu(state = state.retrySendMenuState) + ReadReceiptBottomSheetView(state = state.readReceiptBottomSheetState) + ReinviteDialog(state = state) // Since the textfield is now based on an Android view, this is no longer done automatically. // We need to hide the keyboard automatically when navigating out of this screen. @@ -412,7 +409,8 @@ private fun MessagesViewComposerBottomSheetContents( if (state.userHasPermissionToSendMessage) { Column(modifier = modifier.fillMaxWidth()) { MentionSuggestionsPickerView( - modifier = Modifier.heightIn(max = 230.dp) + modifier = Modifier + .heightIn(max = 230.dp) // Consume all scrolling, preventing the bottom sheet from being dragged when interacting with the list of suggestions .nestedScroll(object : NestedScrollConnection { override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateProvider.kt index 31134812f3..06520b1808 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateProvider.kt @@ -68,8 +68,8 @@ private fun aReadReceiptData( id = "$index", size = AvatarSize.TimelineReadReceipt ), - timestamp: Long = 1629780000000L, + formattedDate: String = "12:34", ) = ReadReceiptData( avatarData = avatarData, - timestamp = timestamp, + formattedDate = formattedDate, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetEvents.kt new file mode 100644 index 0000000000..213a43277d --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetEvents.kt @@ -0,0 +1,24 @@ +/* + * 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.receipt.bottomsheet + +import io.element.android.features.messages.impl.timeline.model.TimelineItem + +sealed interface ReadReceiptBottomSheetEvents { + data class EventSelected(val event: TimelineItem.Event) : ReadReceiptBottomSheetEvents + data object Dismiss : ReadReceiptBottomSheetEvents +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetPresenter.kt new file mode 100644 index 0000000000..a4a55cbc9e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetPresenter.kt @@ -0,0 +1,52 @@ +/* + * 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.receipt.bottomsheet + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.libraries.architecture.Presenter +import javax.inject.Inject + +class ReadReceiptBottomSheetPresenter @Inject constructor( +) : Presenter { + + @Composable + override fun present(): ReadReceiptBottomSheetState { + var selectedEvent: TimelineItem.Event? by remember { mutableStateOf(null) } + + fun handleEvent(event: ReadReceiptBottomSheetEvents) { + @Suppress("LiftReturnOrAssignment") + when (event) { + is ReadReceiptBottomSheetEvents.EventSelected -> { + selectedEvent = event.event + } + ReadReceiptBottomSheetEvents.Dismiss -> { + selectedEvent = null + } + } + } + + return ReadReceiptBottomSheetState( + selectedEvent = selectedEvent, + eventSink = { handleEvent(it) }, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetState.kt new file mode 100644 index 0000000000..34db5488fa --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetState.kt @@ -0,0 +1,26 @@ +/* + * 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.receipt.bottomsheet + +import androidx.compose.runtime.Immutable +import io.element.android.features.messages.impl.timeline.model.TimelineItem + +@Immutable +data class ReadReceiptBottomSheetState( + val selectedEvent: TimelineItem.Event?, + val eventSink: (ReadReceiptBottomSheetEvents) -> Unit, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetStateProvider.kt new file mode 100644 index 0000000000..9b51dac76e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetStateProvider.kt @@ -0,0 +1,44 @@ +/* + * 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.receipt.bottomsheet + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.messages.impl.timeline.aTimelineItemEvent +import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewStateProvider +import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState +import kotlinx.collections.immutable.toImmutableList + +class ReadReceiptBottomSheetStateProvider : PreviewParameterProvider { + // Reuse the provider ReadReceiptViewStateProvider + private val readReceiptViewStateProvider = ReadReceiptViewStateProvider() + override val values: Sequence = readReceiptViewStateProvider.values + .filter { it.sendState is LocalEventSendState.Sent } + .map { readReceiptViewState -> + ReadReceiptBottomSheetState( + selectedEvent = aTimelineItemEvent( + readReceiptState = TimelineItemReadReceipts.ReadReceipts( + receipts = readReceiptViewState.receipts.map { readReceiptData -> + readReceiptData + .copy(avatarData = readReceiptData.avatarData.copy(id = "@${readReceiptData.avatarData.id}:localhost")) + }.toImmutableList() + ) + ), + eventSink = {}, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetView.kt new file mode 100644 index 0000000000..bf46165cc6 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetView.kt @@ -0,0 +1,107 @@ +/* + * 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.receipt.bottomsheet + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.messages.impl.timeline.model.receipts +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.components.MatrixUserRow +import io.element.android.libraries.theme.ElementTheme +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun ReadReceiptBottomSheetView( + state: ReadReceiptBottomSheetState, + modifier: Modifier = Modifier, +) { + val isVisible = state.selectedEvent != null + + val sheetState = rememberModalBottomSheetState() + val coroutineScope = rememberCoroutineScope() + if (isVisible) { + ModalBottomSheet( + modifier = modifier, +// modifier = modifier.navigationBarsPadding() - FIXME after https://issuetracker.google.com/issues/275849044 +// .imePadding() + sheetState = sheetState, + onDismissRequest = { + coroutineScope.launch { + sheetState.hide() + state.eventSink(ReadReceiptBottomSheetEvents.Dismiss) + } + } + ) { + ReadReceiptBottomSheetContents( + state = state, + ) + // FIXME remove after https://issuetracker.google.com/issues/275849044 + Spacer(modifier = Modifier.height(32.dp)) + } + } +} + +@Composable +private fun ColumnScope.ReadReceiptBottomSheetContents( + state: ReadReceiptBottomSheetState, +) { + val receipts = state.selectedEvent?.readReceiptState?.receipts().orEmpty() + receipts.forEach { + MatrixUserRow( + matrixUser = MatrixUser( + UserId(it.avatarData.id), + it.avatarData.name, + it.avatarData.url, + ), + avatarSize = AvatarSize.ReadReceiptList, + trailingContent = { + Text( + text = it.formattedDate, + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + ) + } + ) + } +} + +@PreviewsDayNight +@Composable +internal fun ReadReceiptBottomSheetViewPreview(@PreviewParameter(ReadReceiptBottomSheetStateProvider::class) state: ReadReceiptBottomSheetState) = ElementPreview { + // TODO restore RetrySendMessageMenuBottomSheet once the issue with bottom sheet not being previewable is fixed + Column { + ReadReceiptBottomSheetContents( + state = state + ) + } +} 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 399eeec539..cde3f32d95 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 @@ -25,6 +25,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemGrou import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts import io.element.android.libraries.core.bool.orTrue +import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.MatrixClient @@ -39,6 +40,7 @@ import javax.inject.Inject class TimelineItemEventFactory @Inject constructor( private val contentFactory: TimelineItemContentFactory, private val matrixClient: MatrixClient, + private val lastMessageTimestampFormatter: LastMessageTimestampFormatter, ) { suspend fun create( @@ -140,11 +142,11 @@ class TimelineItemEventFactory @Inject constructor( ReadReceiptData( avatarData = AvatarData( id = receipt.userId.value, - name = roomMember?.displayName ?: receipt.userId.value, + name = roomMember?.displayName, url = roomMember?.avatarUrl, size = AvatarSize.TimelineReadReceipt, ), - timestamp = receipt.timestamp + formattedDate = lastMessageTimestampFormatter.format(receipt.timestamp) ) } .toImmutableList() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemReadReceipts.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemReadReceipts.kt index fca0040790..417fa415ee 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemReadReceipts.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemReadReceipts.kt @@ -31,7 +31,7 @@ sealed interface TimelineItemReadReceipts { data class ReadReceiptData( val avatarData: AvatarData, - val timestamp: Long + val formattedDate: String, ) fun TimelineItemReadReceipts.receipts(): ImmutableList = when (this) { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt index 18fa0e390e..972f105a4a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt @@ -35,6 +35,7 @@ import io.element.android.features.messages.impl.timeline.groups.TimelineItemGro import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractorWithoutValidation import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter +import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter import io.element.android.libraries.eventformatter.api.TimelineEventFormatter import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem @@ -65,6 +66,7 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory { failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory(), ), matrixClient = matrixClient, + lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(), ), virtualItemFactory = TimelineItemVirtualFactory( daySeparatorFactory = TimelineItemDaySeparatorFactory( diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt index cb3858fa47..0b4264e8e6 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt @@ -39,6 +39,8 @@ enum class AvatarSize(val dp: Dp) { TimelineSender(32.dp), TimelineReadReceipt(16.dp), + ReadReceiptList(32.dp), + MessageActionSender(32.dp), RoomInviteItem(52.dp), diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt index 1380d15e8b..947afd8bbd 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt @@ -31,11 +31,13 @@ fun MatrixUserRow( matrixUser: MatrixUser, modifier: Modifier = Modifier, avatarSize: AvatarSize = AvatarSize.UserListItem, + trailingContent: @Composable (() -> Unit)? = null, ) = UserRow( avatarData = matrixUser.getAvatarData(avatarSize), name = matrixUser.getBestName(), subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value, modifier = modifier, + trailingContent, ) @PreviewsDayNight diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UserRow.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UserRow.kt index 5e59ce7aef..7aa11ba193 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UserRow.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UserRow.kt @@ -38,6 +38,7 @@ internal fun UserRow( name: String, subtext: String?, modifier: Modifier = Modifier, + trailingContent: @Composable (() -> Unit)? = null, ) { Row( modifier = modifier @@ -49,7 +50,8 @@ internal fun UserRow( Avatar(avatarData) Column( modifier = Modifier - .padding(start = 12.dp), + .padding(start = 12.dp) + .weight(1f), ) { // Name Text( @@ -70,5 +72,6 @@ internal fun UserRow( ) } } + trailingContent?.invoke() } }