Read receipt: model and UI.
This commit is contained in:
parent
dc0c9093be
commit
3e0971f189
17 changed files with 519 additions and 9 deletions
|
|
@ -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.appconfig
|
||||||
|
|
||||||
|
object TimelineConfig {
|
||||||
|
const val maxReadReceiptToDisplay = 3
|
||||||
|
}
|
||||||
|
|
@ -33,6 +33,7 @@ dependencies {
|
||||||
implementation(projects.anvilannotations)
|
implementation(projects.anvilannotations)
|
||||||
anvil(projects.anvilcodegen)
|
anvil(projects.anvilcodegen)
|
||||||
api(projects.features.messages.api)
|
api(projects.features.messages.api)
|
||||||
|
implementation(projects.appconfig)
|
||||||
implementation(projects.features.call)
|
implementation(projects.features.call)
|
||||||
implementation(projects.features.location.api)
|
implementation(projects.features.location.api)
|
||||||
implementation(projects.features.poll.api)
|
implementation(projects.features.poll.api)
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,10 @@ fun MessagesView(
|
||||||
onReactionClicked = ::onEmojiReactionClicked,
|
onReactionClicked = ::onEmojiReactionClicked,
|
||||||
onReactionLongClicked = ::onEmojiReactionLongClicked,
|
onReactionLongClicked = ::onEmojiReactionLongClicked,
|
||||||
onMoreReactionsClicked = ::onMoreReactionsClicked,
|
onMoreReactionsClicked = ::onMoreReactionsClicked,
|
||||||
|
onReadReceiptClick = { // targetEvent ->
|
||||||
|
// TODO Open bottom sheet with read receipts
|
||||||
|
// state.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ShowReadReceipts, targetEvent))
|
||||||
|
},
|
||||||
onSendLocationClicked = onSendLocationClicked,
|
onSendLocationClicked = onSendLocationClicked,
|
||||||
onCreatePollClicked = onCreatePollClicked,
|
onCreatePollClicked = onCreatePollClicked,
|
||||||
onSwipeToReply = { targetEvent ->
|
onSwipeToReply = { targetEvent ->
|
||||||
|
|
@ -310,6 +314,7 @@ private fun MessagesViewContent(
|
||||||
onReactionClicked: (key: String, TimelineItem.Event) -> Unit,
|
onReactionClicked: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onReactionLongClicked: (key: String, TimelineItem.Event) -> Unit,
|
onReactionLongClicked: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onMoreReactionsClicked: (TimelineItem.Event) -> Unit,
|
onMoreReactionsClicked: (TimelineItem.Event) -> Unit,
|
||||||
|
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
||||||
onMessageLongClicked: (TimelineItem.Event) -> Unit,
|
onMessageLongClicked: (TimelineItem.Event) -> Unit,
|
||||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||||
onSendLocationClicked: () -> Unit,
|
onSendLocationClicked: () -> Unit,
|
||||||
|
|
@ -381,6 +386,7 @@ private fun MessagesViewContent(
|
||||||
onReactionClicked = onReactionClicked,
|
onReactionClicked = onReactionClicked,
|
||||||
onReactionLongClicked = onReactionLongClicked,
|
onReactionLongClicked = onReactionLongClicked,
|
||||||
onMoreReactionsClicked = onMoreReactionsClicked,
|
onMoreReactionsClicked = onMoreReactionsClicked,
|
||||||
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
onSwipeToReply = onSwipeToReply,
|
onSwipeToReply = onSwipeToReply,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,14 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
import io.element.android.features.messages.impl.timeline.session.SessionState
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
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.room.MessageEventType
|
||||||
|
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
|
|
@ -63,6 +66,7 @@ class TimelinePresenter @Inject constructor(
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val verificationService: SessionVerificationService,
|
private val verificationService: SessionVerificationService,
|
||||||
private val encryptionService: EncryptionService,
|
private val encryptionService: EncryptionService,
|
||||||
|
private val featureFlagService: FeatureFlagService,
|
||||||
) : Presenter<TimelineState> {
|
) : Presenter<TimelineState> {
|
||||||
|
|
||||||
private val timeline = room.timeline
|
private val timeline = room.timeline
|
||||||
|
|
@ -97,6 +101,9 @@ class TimelinePresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val readReceiptsEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.ReadReceipts).collectAsState(initial = false)
|
||||||
|
val membersState by room.membersStateFlow.collectAsState()
|
||||||
|
|
||||||
fun handleEvents(event: TimelineEvents) {
|
fun handleEvents(event: TimelineEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
TimelineEvents.LoadMore -> localScope.paginateBackwards()
|
TimelineEvents.LoadMore -> localScope.paginateBackwards()
|
||||||
|
|
@ -136,7 +143,16 @@ class TimelinePresenter @Inject constructor(
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
timeline
|
timeline
|
||||||
.timelineItems
|
.timelineItems
|
||||||
.onEach(timelineItemsFactory::replaceWith)
|
.onEach {
|
||||||
|
timelineItemsFactory.replaceWith(
|
||||||
|
timelineItems = it,
|
||||||
|
roomMembers = if (readReceiptsEnabled) {
|
||||||
|
membersState.roomMembers()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
.onEach { timelineItems ->
|
.onEach { timelineItems ->
|
||||||
if (timelineItems.isEmpty()) {
|
if (timelineItems.isEmpty()) {
|
||||||
paginateBackwards()
|
paginateBackwards()
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.timeline
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
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.TimelineItemGroupPosition
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
|
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts
|
||||||
import io.element.android.features.messages.impl.timeline.model.anAggregatedReaction
|
import io.element.android.features.messages.impl.timeline.model.anAggregatedReaction
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||||
|
|
@ -118,11 +119,12 @@ internal fun aTimelineItemEvent(
|
||||||
senderDisplayName: String = "Sender",
|
senderDisplayName: String = "Sender",
|
||||||
content: TimelineItemEventContent = aTimelineItemTextContent(),
|
content: TimelineItemEventContent = aTimelineItemTextContent(),
|
||||||
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
||||||
sendState: LocalEventSendState = LocalEventSendState.Sent(eventId),
|
sendState: LocalEventSendState? = if (isMine) LocalEventSendState.Sent(eventId) else null,
|
||||||
inReplyTo: InReplyTo? = null,
|
inReplyTo: InReplyTo? = null,
|
||||||
isThreaded: Boolean = false,
|
isThreaded: Boolean = false,
|
||||||
debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(),
|
debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(),
|
||||||
timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(),
|
timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(),
|
||||||
|
readReceiptState: TimelineItemReadReceipts = TimelineItemReadReceipts.Hidden,
|
||||||
): TimelineItem.Event {
|
): TimelineItem.Event {
|
||||||
return TimelineItem.Event(
|
return TimelineItem.Event(
|
||||||
id = UUID.randomUUID().toString(),
|
id = UUID.randomUUID().toString(),
|
||||||
|
|
@ -132,6 +134,7 @@ internal fun aTimelineItemEvent(
|
||||||
senderAvatar = AvatarData("@senderId:domain", "sender", size = AvatarSize.TimelineSender),
|
senderAvatar = AvatarData("@senderId:domain", "sender", size = AvatarSize.TimelineSender),
|
||||||
content = content,
|
content = content,
|
||||||
reactionsState = timelineItemReactions,
|
reactionsState = timelineItemReactions,
|
||||||
|
readReceiptState = readReceiptState,
|
||||||
sentTime = "12:34",
|
sentTime = "12:34",
|
||||||
isMine = isMine,
|
isMine = isMine,
|
||||||
senderDisplayName = senderDisplayName,
|
senderDisplayName = senderDisplayName,
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ fun TimelineView(
|
||||||
onReactionClicked: (emoji: String, TimelineItem.Event) -> Unit,
|
onReactionClicked: (emoji: String, TimelineItem.Event) -> Unit,
|
||||||
onReactionLongClicked: (emoji: String, TimelineItem.Event) -> Unit,
|
onReactionLongClicked: (emoji: String, TimelineItem.Event) -> Unit,
|
||||||
onMoreReactionsClicked: (TimelineItem.Event) -> Unit,
|
onMoreReactionsClicked: (TimelineItem.Event) -> Unit,
|
||||||
|
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
fun onReachedLoadMore() {
|
fun onReachedLoadMore() {
|
||||||
|
|
@ -135,6 +136,7 @@ fun TimelineView(
|
||||||
onReactionClick = onReactionClicked,
|
onReactionClick = onReactionClicked,
|
||||||
onReactionLongClick = onReactionLongClicked,
|
onReactionLongClick = onReactionLongClicked,
|
||||||
onMoreReactionsClick = onMoreReactionsClicked,
|
onMoreReactionsClick = onMoreReactionsClicked,
|
||||||
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
onTimestampClicked = onTimestampClicked,
|
onTimestampClicked = onTimestampClicked,
|
||||||
sessionState = state.sessionState,
|
sessionState = state.sessionState,
|
||||||
eventSink = state.eventSink,
|
eventSink = state.eventSink,
|
||||||
|
|
@ -179,6 +181,7 @@ private fun TimelineItemRow(
|
||||||
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
||||||
|
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
||||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||||
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
||||||
eventSink: (TimelineEvents) -> Unit,
|
eventSink: (TimelineEvents) -> Unit,
|
||||||
|
|
@ -214,6 +217,7 @@ private fun TimelineItemRow(
|
||||||
onReactionClick = onReactionClick,
|
onReactionClick = onReactionClick,
|
||||||
onReactionLongClick = onReactionLongClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
onMoreReactionsClick = onMoreReactionsClick,
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
onTimestampClicked = onTimestampClicked,
|
onTimestampClicked = onTimestampClicked,
|
||||||
onSwipeToReply = { onSwipeToReply(timelineItem) },
|
onSwipeToReply = { onSwipeToReply(timelineItem) },
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
|
|
@ -255,6 +259,7 @@ private fun TimelineItemRow(
|
||||||
onReactionClick = onReactionClick,
|
onReactionClick = onReactionClick,
|
||||||
onReactionLongClick = onReactionLongClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
onMoreReactionsClick = onMoreReactionsClick,
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
)
|
)
|
||||||
|
|
@ -362,6 +367,7 @@ internal fun TimelineViewPreview(
|
||||||
onReactionLongClicked = { _, _ -> },
|
onReactionLongClicked = { _, _ -> },
|
||||||
onMoreReactionsClicked = {},
|
onMoreReactionsClicked = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,8 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||||
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
||||||
import io.element.android.features.messages.impl.timeline.components.event.toExtraPadding
|
import io.element.android.features.messages.impl.timeline.components.event.toExtraPadding
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
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.TimelineItemGroupPosition
|
||||||
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState
|
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState
|
||||||
|
|
@ -77,6 +79,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.aTimelineItemImageContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
|
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.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.receipts
|
||||||
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
|
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
|
||||||
import io.element.android.libraries.designsystem.components.EqualWidthColumn
|
import io.element.android.libraries.designsystem.components.EqualWidthColumn
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
|
@ -124,6 +127,7 @@ fun TimelineItemEventRow(
|
||||||
onReactionClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
|
onReactionClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
|
||||||
onReactionLongClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
|
onReactionLongClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
|
||||||
onMoreReactionsClick: (eventId: TimelineItem.Event) -> Unit,
|
onMoreReactionsClick: (eventId: TimelineItem.Event) -> Unit,
|
||||||
|
onReadReceiptClick: (event: TimelineItem.Event) -> Unit,
|
||||||
onSwipeToReply: () -> Unit,
|
onSwipeToReply: () -> Unit,
|
||||||
eventSink: (TimelineEvents) -> Unit,
|
eventSink: (TimelineEvents) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
|
|
@ -183,6 +187,7 @@ fun TimelineItemEventRow(
|
||||||
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
|
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
|
||||||
onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
|
onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
|
||||||
onMoreReactionsClicked = { onMoreReactionsClick(event) },
|
onMoreReactionsClicked = { onMoreReactionsClick(event) },
|
||||||
|
onReadReceiptsClicked = { onReadReceiptClick(event) },
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -200,6 +205,7 @@ fun TimelineItemEventRow(
|
||||||
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
|
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
|
||||||
onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
|
onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
|
||||||
onMoreReactionsClicked = { onMoreReactionsClick(event) },
|
onMoreReactionsClicked = { onMoreReactionsClick(event) },
|
||||||
|
onReadReceiptsClicked = { onReadReceiptClick(event) },
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -240,6 +246,7 @@ private fun TimelineItemEventRowContent(
|
||||||
inReplyToClicked: () -> Unit,
|
inReplyToClicked: () -> Unit,
|
||||||
onUserDataClicked: () -> Unit,
|
onUserDataClicked: () -> Unit,
|
||||||
onReactionClicked: (emoji: String) -> Unit,
|
onReactionClicked: (emoji: String) -> Unit,
|
||||||
|
onReadReceiptsClicked: () -> Unit,
|
||||||
onReactionLongClicked: (emoji: String) -> Unit,
|
onReactionLongClicked: (emoji: String) -> Unit,
|
||||||
onMoreReactionsClicked: (event: TimelineItem.Event) -> Unit,
|
onMoreReactionsClicked: (event: TimelineItem.Event) -> Unit,
|
||||||
eventSink: (TimelineEvents) -> Unit,
|
eventSink: (TimelineEvents) -> Unit,
|
||||||
|
|
@ -256,7 +263,12 @@ private fun TimelineItemEventRowContent(
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
val (sender, message, reactions) = createRefs()
|
val (
|
||||||
|
sender,
|
||||||
|
message,
|
||||||
|
reactions,
|
||||||
|
readReceipts,
|
||||||
|
) = createRefs()
|
||||||
|
|
||||||
// Sender
|
// Sender
|
||||||
val avatarStrokeSize = 3.dp
|
val avatarStrokeSize = 3.dp
|
||||||
|
|
@ -322,6 +334,23 @@ private fun TimelineItemEventRowContent(
|
||||||
.padding(start = if (event.isMine) 16.dp else 36.dp, end = 16.dp)
|
.padding(start = if (event.isMine) 16.dp else 36.dp, end = 16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read receipts / Send state
|
||||||
|
TimelineItemReadReceiptView(
|
||||||
|
state = ReadReceiptViewState(
|
||||||
|
sendState = event.localSendState,
|
||||||
|
receipts = event.readReceiptState.receipts(),
|
||||||
|
),
|
||||||
|
onReadReceiptsClicked = onReadReceiptsClicked,
|
||||||
|
modifier = Modifier
|
||||||
|
.constrainAs(readReceipts) {
|
||||||
|
if (event.reactionsState.reactions.isNotEmpty()) {
|
||||||
|
top.linkTo(reactions.bottom, margin = 4.dp)
|
||||||
|
} else {
|
||||||
|
top.linkTo(message.bottom, margin = 4.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -659,6 +688,7 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
|
|
@ -680,6 +710,7 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
|
|
@ -719,6 +750,7 @@ internal fun TimelineItemEventRowWithReplyPreview() = ElementPreview {
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
|
|
@ -742,6 +774,7 @@ internal fun TimelineItemEventRowWithReplyPreview() = ElementPreview {
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
|
|
@ -793,6 +826,7 @@ internal fun TimelineItemEventRowTimestampPreview(
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
|
|
@ -825,6 +859,7 @@ internal fun TimelineItemEventRowWithManyReactionsPreview() = ElementPreview {
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
|
|
@ -850,6 +885,7 @@ internal fun TimelineItemEventRowLongSenderNamePreview() = ElementPreviewLight {
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
|
|
@ -871,6 +907,7 @@ internal fun TimelineItemEventTimestampBelowPreview() = ElementPreviewLight {
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
data class ReadReceiptViewState(
|
||||||
|
val sendState: LocalEventSendState?,
|
||||||
|
val receipts: ImmutableList<ReadReceiptData>,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
class ReadReceiptViewStateProvider : PreviewParameterProvider<ReadReceiptViewState> {
|
||||||
|
override val values: Sequence<ReadReceiptViewState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aReadReceiptViewState(),
|
||||||
|
aReadReceiptViewState(sendState = LocalEventSendState.NotSentYet),
|
||||||
|
aReadReceiptViewState(sendState = LocalEventSendState.Sent(EventId("\$eventId"))),
|
||||||
|
aReadReceiptViewState(
|
||||||
|
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
||||||
|
receipts = mutableListOf<ReadReceiptData>().apply { repeat(1) { add(aReadReceiptData(it)) } },
|
||||||
|
),
|
||||||
|
aReadReceiptViewState(
|
||||||
|
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
||||||
|
receipts = mutableListOf<ReadReceiptData>().apply { repeat(2) { add(aReadReceiptData(it)) } },
|
||||||
|
),
|
||||||
|
aReadReceiptViewState(
|
||||||
|
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
||||||
|
receipts = mutableListOf<ReadReceiptData>().apply { repeat(3) { add(aReadReceiptData(it)) } },
|
||||||
|
),
|
||||||
|
aReadReceiptViewState(
|
||||||
|
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
||||||
|
receipts = mutableListOf<ReadReceiptData>().apply { repeat(4) { add(aReadReceiptData(it)) } },
|
||||||
|
),
|
||||||
|
aReadReceiptViewState(
|
||||||
|
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
||||||
|
receipts = mutableListOf<ReadReceiptData>().apply { repeat(5) { add(aReadReceiptData(it)) } },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aReadReceiptViewState(
|
||||||
|
sendState: LocalEventSendState? = null,
|
||||||
|
receipts: List<ReadReceiptData> = emptyList(),
|
||||||
|
) = ReadReceiptViewState(
|
||||||
|
sendState = sendState,
|
||||||
|
receipts = receipts.toImmutableList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun aReadReceiptData(
|
||||||
|
index: Int,
|
||||||
|
avatarData: AvatarData = anAvatarData(
|
||||||
|
id = "$index",
|
||||||
|
size = AvatarSize.TimelineReadReceipt
|
||||||
|
),
|
||||||
|
timestamp: Long = 1629780000000L,
|
||||||
|
) = ReadReceiptData(
|
||||||
|
avatarData = avatarData,
|
||||||
|
timestamp = timestamp,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import io.element.android.appconfig.TimelineConfig
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
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.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TimelineItemReadReceiptView(
|
||||||
|
state: ReadReceiptViewState,
|
||||||
|
onReadReceiptsClicked: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
when (state.sendState) {
|
||||||
|
LocalEventSendState.Canceled -> Unit
|
||||||
|
LocalEventSendState.NotSentYet -> {
|
||||||
|
ReadReceiptsRow(modifier) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.padding(2.dp),
|
||||||
|
resourceId = CommonDrawables.ic_sending,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = ElementTheme.colors.iconSecondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is LocalEventSendState.SendingFailed -> {
|
||||||
|
// Error? The timestamp is already displayed in red
|
||||||
|
}
|
||||||
|
is LocalEventSendState.Sent -> {
|
||||||
|
if (state.receipts.isEmpty()) {
|
||||||
|
ReadReceiptsRow(modifier = modifier) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.padding(2.dp),
|
||||||
|
resourceId = CommonDrawables.ic_sent,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = ElementTheme.colors.iconSecondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ReadReceiptsRow(modifier = modifier) {
|
||||||
|
ReadReceiptsAvatars(
|
||||||
|
receipts = state.receipts,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.clickable { onReadReceiptsClicked() }
|
||||||
|
.padding(2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
null -> {
|
||||||
|
if (state.receipts.isNotEmpty()) {
|
||||||
|
ReadReceiptsRow(modifier = modifier) {
|
||||||
|
ReadReceiptsAvatars(
|
||||||
|
receipts = state.receipts,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.clickable { onReadReceiptsClicked() }
|
||||||
|
.padding(2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReadReceiptsRow(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable () -> Unit = {},
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(AvatarSize.TimelineReadReceipt.dp + 8.dp)
|
||||||
|
.padding(horizontal = 18.dp),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 4.dp)
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReadReceiptsAvatars(
|
||||||
|
receipts: ImmutableList<ReadReceiptData>,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val avatarSize = AvatarSize.TimelineReadReceipt.dp
|
||||||
|
val avatarStrokeSize = 1.dp
|
||||||
|
val avatarStrokeColor = MaterialTheme.colorScheme.background
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp - avatarStrokeSize),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.CenterEnd,
|
||||||
|
) {
|
||||||
|
receipts
|
||||||
|
.take(TimelineConfig.maxReadReceiptToDisplay)
|
||||||
|
.reversed()
|
||||||
|
.forEachIndexed { index, it ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = (12.dp + avatarStrokeSize * 2) * index)
|
||||||
|
.size(size = avatarSize + avatarStrokeSize * 2)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(avatarStrokeColor)
|
||||||
|
.zIndex(index.toFloat()),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Avatar(
|
||||||
|
avatarData = it.avatarData,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (receipts.size > 3) {
|
||||||
|
Text(
|
||||||
|
text = "+" + (receipts.size - TimelineConfig.maxReadReceiptToDisplay),
|
||||||
|
style = ElementTheme.typography.fontBodyXsRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemReactionsViewPreview(
|
||||||
|
@PreviewParameter(ReadReceiptViewStateProvider::class) state: ReadReceiptViewState,
|
||||||
|
) = ElementPreview {
|
||||||
|
TimelineItemReadReceiptView(
|
||||||
|
state = state,
|
||||||
|
onReadReceiptsClicked = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
|
import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
|
||||||
import io.element.android.libraries.androidutils.diff.MutableListDiffCache
|
import io.element.android.libraries.androidutils.diff.MutableListDiffCache
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -66,19 +67,23 @@ class TimelineItemsFactory @Inject constructor(
|
||||||
|
|
||||||
suspend fun replaceWith(
|
suspend fun replaceWith(
|
||||||
timelineItems: List<MatrixTimelineItem>,
|
timelineItems: List<MatrixTimelineItem>,
|
||||||
|
roomMembers: List<RoomMember>?,
|
||||||
) = withContext(dispatchers.computation) {
|
) = withContext(dispatchers.computation) {
|
||||||
lock.withLock {
|
lock.withLock {
|
||||||
diffCacheUpdater.updateWith(timelineItems)
|
diffCacheUpdater.updateWith(timelineItems)
|
||||||
buildAndEmitTimelineItemStates(timelineItems)
|
buildAndEmitTimelineItemStates(timelineItems, roomMembers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun buildAndEmitTimelineItemStates(timelineItems: List<MatrixTimelineItem>) {
|
private suspend fun buildAndEmitTimelineItemStates(
|
||||||
|
timelineItems: List<MatrixTimelineItem>,
|
||||||
|
roomMembers: List<RoomMember>?,
|
||||||
|
) {
|
||||||
val newTimelineItemStates = ArrayList<TimelineItem>()
|
val newTimelineItemStates = ArrayList<TimelineItem>()
|
||||||
for (index in diffCache.indices().reversed()) {
|
for (index in diffCache.indices().reversed()) {
|
||||||
val cacheItem = diffCache.get(index)
|
val cacheItem = diffCache.get(index)
|
||||||
if (cacheItem == null) {
|
if (cacheItem == null) {
|
||||||
buildAndCacheItem(timelineItems, index)?.also { timelineItemState ->
|
buildAndCacheItem(timelineItems, index, roomMembers)?.also { timelineItemState ->
|
||||||
newTimelineItemStates.add(timelineItemState)
|
newTimelineItemStates.add(timelineItemState)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -91,11 +96,12 @@ class TimelineItemsFactory @Inject constructor(
|
||||||
|
|
||||||
private suspend fun buildAndCacheItem(
|
private suspend fun buildAndCacheItem(
|
||||||
timelineItems: List<MatrixTimelineItem>,
|
timelineItems: List<MatrixTimelineItem>,
|
||||||
index: Int
|
index: Int,
|
||||||
|
roomMembers: List<RoomMember>?,
|
||||||
): TimelineItem? {
|
): TimelineItem? {
|
||||||
val timelineItemState =
|
val timelineItemState =
|
||||||
when (val currentTimelineItem = timelineItems[index]) {
|
when (val currentTimelineItem = timelineItems[index]) {
|
||||||
is MatrixTimelineItem.Event -> eventItemFactory.create(currentTimelineItem, index, timelineItems)
|
is MatrixTimelineItem.Event -> eventItemFactory.create(currentTimelineItem, index, timelineItems, roomMembers)
|
||||||
is MatrixTimelineItem.Virtual -> virtualItemFactory.create(currentTimelineItem)
|
is MatrixTimelineItem.Virtual -> virtualItemFactory.create(currentTimelineItem)
|
||||||
MatrixTimelineItem.Other -> null
|
MatrixTimelineItem.Other -> null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,16 @@ package io.element.android.features.messages.impl.timeline.factories.event
|
||||||
import io.element.android.features.messages.impl.timeline.groups.canBeDisplayedInBubbleBlock
|
import io.element.android.features.messages.impl.timeline.groups.canBeDisplayedInBubbleBlock
|
||||||
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
|
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
|
||||||
import io.element.android.features.messages.impl.timeline.model.AggregatedReactionSender
|
import io.element.android.features.messages.impl.timeline.model.AggregatedReactionSender
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
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.TimelineItemGroupPosition
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
|
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.core.bool.orTrue
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
@ -42,6 +45,7 @@ class TimelineItemEventFactory @Inject constructor(
|
||||||
currentTimelineItem: MatrixTimelineItem.Event,
|
currentTimelineItem: MatrixTimelineItem.Event,
|
||||||
index: Int,
|
index: Int,
|
||||||
timelineItems: List<MatrixTimelineItem>,
|
timelineItems: List<MatrixTimelineItem>,
|
||||||
|
roomMembers: List<RoomMember>?,
|
||||||
): TimelineItem.Event {
|
): TimelineItem.Event {
|
||||||
val currentSender = currentTimelineItem.event.sender
|
val currentSender = currentTimelineItem.event.sender
|
||||||
val groupPosition =
|
val groupPosition =
|
||||||
|
|
@ -84,6 +88,7 @@ class TimelineItemEventFactory @Inject constructor(
|
||||||
sentTime = sentTime,
|
sentTime = sentTime,
|
||||||
groupPosition = groupPosition,
|
groupPosition = groupPosition,
|
||||||
reactionsState = currentTimelineItem.computeReactionsState(),
|
reactionsState = currentTimelineItem.computeReactionsState(),
|
||||||
|
readReceiptState = currentTimelineItem.computeReadReceiptState(roomMembers),
|
||||||
localSendState = currentTimelineItem.event.localSendState,
|
localSendState = currentTimelineItem.event.localSendState,
|
||||||
inReplyTo = currentTimelineItem.event.inReplyTo(),
|
inReplyTo = currentTimelineItem.event.inReplyTo(),
|
||||||
isThreaded = currentTimelineItem.event.isThreaded(),
|
isThreaded = currentTimelineItem.event.isThreaded(),
|
||||||
|
|
@ -102,7 +107,7 @@ class TimelineItemEventFactory @Inject constructor(
|
||||||
key = reaction.key,
|
key = reaction.key,
|
||||||
currentUserId = matrixClient.sessionId,
|
currentUserId = matrixClient.sessionId,
|
||||||
senders = reaction.senders
|
senders = reaction.senders
|
||||||
.sortedByDescending{ it.timestamp }
|
.sortedByDescending { it.timestamp }
|
||||||
.map {
|
.map {
|
||||||
val date = Date(it.timestamp)
|
val date = Date(it.timestamp)
|
||||||
AggregatedReactionSender(
|
AggregatedReactionSender(
|
||||||
|
|
@ -124,6 +129,28 @@ class TimelineItemEventFactory @Inject constructor(
|
||||||
return TimelineItemReactions(aggregatedReactions.toImmutableList())
|
return TimelineItemReactions(aggregatedReactions.toImmutableList())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun MatrixTimelineItem.Event.computeReadReceiptState(
|
||||||
|
roomMembers: List<RoomMember>?,
|
||||||
|
): TimelineItemReadReceipts {
|
||||||
|
if (roomMembers == null) return TimelineItemReadReceipts.Hidden
|
||||||
|
return TimelineItemReadReceipts.ReadReceipts(
|
||||||
|
receipts = event.receipts
|
||||||
|
.map { receipt ->
|
||||||
|
val roomMember = roomMembers.find { it.userId == receipt.userId }
|
||||||
|
ReadReceiptData(
|
||||||
|
avatarData = AvatarData(
|
||||||
|
id = receipt.userId.value,
|
||||||
|
name = roomMember?.displayName ?: receipt.userId.value,
|
||||||
|
url = roomMember?.avatarUrl,
|
||||||
|
size = AvatarSize.TimelineReadReceipt,
|
||||||
|
),
|
||||||
|
timestamp = receipt.timestamp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.toImmutableList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun computeGroupPosition(
|
private fun computeGroupPosition(
|
||||||
currentTimelineItem: MatrixTimelineItem.Event,
|
currentTimelineItem: MatrixTimelineItem.Event,
|
||||||
timelineItems: List<MatrixTimelineItem>,
|
timelineItems: List<MatrixTimelineItem>,
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ sealed interface TimelineItem {
|
||||||
val isMine: Boolean = false,
|
val isMine: Boolean = false,
|
||||||
val groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
val groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
||||||
val reactionsState: TimelineItemReactions,
|
val reactionsState: TimelineItemReactions,
|
||||||
|
val readReceiptState: TimelineItemReadReceipts,
|
||||||
val localSendState: LocalEventSendState?,
|
val localSendState: LocalEventSendState?,
|
||||||
val inReplyTo: InReplyTo?,
|
val inReplyTo: InReplyTo?,
|
||||||
val isThreaded: Boolean,
|
val isThreaded: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
sealed interface TimelineItemReadReceipts {
|
||||||
|
/** Value when the feature is disabled */
|
||||||
|
data object Hidden : TimelineItemReadReceipts
|
||||||
|
|
||||||
|
data class ReadReceipts(
|
||||||
|
val receipts: ImmutableList<ReadReceiptData>,
|
||||||
|
) : TimelineItemReadReceipts
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ReadReceiptData(
|
||||||
|
val avatarData: AvatarData,
|
||||||
|
val timestamp: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
fun TimelineItemReadReceipts.receipts(): ImmutableList<ReadReceiptData> = when (this) {
|
||||||
|
TimelineItemReadReceipts.Hidden -> persistentListOf()
|
||||||
|
is TimelineItemReadReceipts.ReadReceipts -> receipts
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,7 @@ enum class AvatarSize(val dp: Dp) {
|
||||||
|
|
||||||
TimelineRoom(32.dp),
|
TimelineRoom(32.dp),
|
||||||
TimelineSender(32.dp),
|
TimelineSender(32.dp),
|
||||||
|
TimelineReadReceipt(16.dp),
|
||||||
|
|
||||||
MessageActionSender(32.dp),
|
MessageActionSender(32.dp),
|
||||||
|
|
||||||
|
|
|
||||||
29
libraries/designsystem/src/main/res/drawable/ic_sending.xml
Normal file
29
libraries/designsystem/src/main/res/drawable/ic_sending.xml
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0h16v16h-16z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M8.006,16C6.905,16 5.868,15.792 4.896,15.375C3.924,14.958 3.073,14.385 2.344,13.656C1.615,12.927 1.042,12.077 0.625,11.105C0.208,10.133 0,9.095 0,7.99C0,6.886 0.208,5.851 0.625,4.885C1.042,3.92 1.615,3.073 2.344,2.344C3.073,1.615 3.923,1.042 4.895,0.625C5.867,0.208 6.905,0 8.01,0C9.114,0 10.149,0.208 11.115,0.625C12.08,1.042 12.927,1.615 13.656,2.344C14.385,3.073 14.958,3.922 15.375,4.89C15.792,5.858 16,6.893 16,7.994C16,9.095 15.792,10.132 15.375,11.104C14.958,12.076 14.385,12.927 13.656,13.656C12.927,14.385 12.078,14.958 11.11,15.375C10.142,15.792 9.107,16 8.006,16ZM8,14.5C9.806,14.5 11.34,13.868 12.604,12.604C13.868,11.34 14.5,9.806 14.5,8C14.5,6.194 13.868,4.66 12.604,3.396C11.34,2.132 9.806,1.5 8,1.5C6.194,1.5 4.66,2.132 3.396,3.396C2.132,4.66 1.5,6.194 1.5,8C1.5,9.806 2.132,11.34 3.396,12.604C4.66,13.868 6.194,14.5 8,14.5Z"
|
||||||
|
android:fillColor="@android:color/white"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
29
libraries/designsystem/src/main/res/drawable/ic_sent.xml
Normal file
29
libraries/designsystem/src/main/res/drawable/ic_sent.xml
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0h16v16h-16z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M6.938,8.875L5.688,7.646C5.535,7.493 5.361,7.417 5.167,7.417C4.972,7.417 4.799,7.493 4.646,7.646C4.493,7.799 4.417,7.976 4.417,8.177C4.417,8.378 4.493,8.556 4.646,8.708L6.417,10.479C6.569,10.632 6.743,10.708 6.938,10.708C7.132,10.708 7.306,10.632 7.458,10.479L11.354,6.583C11.507,6.431 11.583,6.253 11.583,6.052C11.583,5.851 11.507,5.674 11.354,5.521C11.201,5.368 11.028,5.292 10.833,5.292C10.639,5.292 10.465,5.368 10.313,5.521L6.938,8.875ZM8,16C6.903,16 5.868,15.792 4.896,15.375C3.924,14.958 3.073,14.385 2.344,13.656C1.615,12.927 1.042,12.076 0.625,11.104C0.208,10.132 0,9.097 0,8C0,6.889 0.208,5.851 0.625,4.885C1.042,3.92 1.615,3.073 2.344,2.344C3.073,1.615 3.924,1.042 4.896,0.625C5.868,0.208 6.903,0 8,0C9.111,0 10.149,0.208 11.115,0.625C12.08,1.042 12.927,1.615 13.656,2.344C14.385,3.073 14.958,3.92 15.375,4.885C15.792,5.851 16,6.889 16,8C16,9.097 15.792,10.132 15.375,11.104C14.958,12.076 14.385,12.927 13.656,13.656C12.927,14.385 12.08,14.958 11.115,15.375C10.149,15.792 9.111,16 8,16ZM8,14.5C9.806,14.5 11.34,13.868 12.604,12.604C13.868,11.34 14.5,9.806 14.5,8C14.5,6.194 13.868,4.66 12.604,3.396C11.34,2.132 9.806,1.5 8,1.5C6.194,1.5 4.66,2.132 3.396,3.396C2.132,4.66 1.5,6.194 1.5,8C1.5,9.806 2.132,11.34 3.396,12.604C4.66,13.868 6.194,14.5 8,14.5Z"
|
||||||
|
android:fillColor="@android:color/white"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue