Skip to content

Commit 004804a

Browse files
authored
Merge pull request #1834 from vector-im/feature/bma/readReceipts
Render send state and read receipts
2 parents 5d4313a + 5397ecd commit 004804a

File tree

118 files changed

+1159
-56
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

118 files changed

+1159
-56
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.appconfig
18+
19+
object TimelineConfig {
20+
const val maxReadReceiptToDisplay = 3
21+
}

features/messages/impl/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies {
3333
implementation(projects.anvilannotations)
3434
anvil(projects.anvilcodegen)
3535
api(projects.features.messages.api)
36+
implementation(projects.appconfig)
3637
implementation(projects.features.call)
3738
implementation(projects.features.location.api)
3839
implementation(projects.features.poll.api)

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import io.element.android.features.messages.impl.timeline.TimelinePresenter
4242
import io.element.android.features.messages.impl.timeline.TimelineState
4343
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
4444
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter
45+
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetPresenter
4546
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
4647
import io.element.android.features.messages.impl.timeline.model.TimelineItem
4748
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
@@ -97,6 +98,7 @@ class MessagesPresenter @AssistedInject constructor(
9798
private val customReactionPresenter: CustomReactionPresenter,
9899
private val reactionSummaryPresenter: ReactionSummaryPresenter,
99100
private val retrySendMenuPresenter: RetrySendMenuPresenter,
101+
private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter,
100102
private val networkMonitor: NetworkMonitor,
101103
private val snackbarDispatcher: SnackbarDispatcher,
102104
private val messageSummaryFormatter: MessageSummaryFormatter,
@@ -124,6 +126,7 @@ class MessagesPresenter @AssistedInject constructor(
124126
val customReactionState = customReactionPresenter.present()
125127
val reactionSummaryState = reactionSummaryPresenter.present()
126128
val retryState = retrySendMenuPresenter.present()
129+
val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present()
127130

128131
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
129132
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
@@ -201,6 +204,7 @@ class MessagesPresenter @AssistedInject constructor(
201204
customReactionState = customReactionState,
202205
reactionSummaryState = reactionSummaryState,
203206
retrySendMenuState = retryState,
207+
readReceiptBottomSheetState = readReceiptBottomSheetState,
204208
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
205209
snackbarMessage = snackbarMessage,
206210
showReinvitePrompt = showReinvitePrompt,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
2222
import io.element.android.features.messages.impl.timeline.TimelineState
2323
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
2424
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
25+
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
2526
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
2627
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
2728
import io.element.android.libraries.architecture.Async
@@ -43,6 +44,7 @@ data class MessagesState(
4344
val customReactionState: CustomReactionState,
4445
val reactionSummaryState: ReactionSummaryState,
4546
val retrySendMenuState: RetrySendMenuState,
47+
val readReceiptBottomSheetState: ReadReceiptBottomSheetState,
4648
val hasNetworkConnection: Boolean,
4749
val snackbarMessage: SnackbarMessage?,
4850
val inviteProgress: Async<Unit>,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemList
2424
import io.element.android.features.messages.impl.timeline.aTimelineState
2525
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
2626
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
27+
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
2728
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
2829
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
2930
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
@@ -96,6 +97,10 @@ fun aMessagesState() = MessagesState(
9697
selectedEvent = null,
9798
eventSink = {},
9899
),
100+
readReceiptBottomSheetState = ReadReceiptBottomSheetState(
101+
selectedEvent = null,
102+
eventSink = {},
103+
),
99104
actionListState = anActionListState(),
100105
customReactionState = CustomReactionState(
101106
target = CustomReactionState.Target.None,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ import io.element.android.features.messages.impl.timeline.components.customreact
7070
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents
7171
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvents
7272
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryView
73+
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheet
74+
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents
7375
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents
7476
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMessageMenu
7577
import io.element.android.features.messages.impl.timeline.model.TimelineItem
@@ -212,6 +214,9 @@ fun MessagesView(
212214
onReactionClicked = ::onEmojiReactionClicked,
213215
onReactionLongClicked = ::onEmojiReactionLongClicked,
214216
onMoreReactionsClicked = ::onMoreReactionsClicked,
217+
onReadReceiptClick = { event ->
218+
state.readReceiptBottomSheetState.eventSink(ReadReceiptBottomSheetEvents.EventSelected(event))
219+
},
215220
onSendLocationClicked = onSendLocationClicked,
216221
onCreatePollClicked = onCreatePollClicked,
217222
onSwipeToReply = { targetEvent ->
@@ -246,13 +251,12 @@ fun MessagesView(
246251
)
247252

248253
ReactionSummaryView(state = state.reactionSummaryState)
249-
RetrySendMessageMenu(
250-
state = state.retrySendMenuState
251-
)
252-
253-
ReinviteDialog(
254-
state = state
254+
RetrySendMessageMenu(state = state.retrySendMenuState)
255+
ReadReceiptBottomSheet(
256+
state = state.readReceiptBottomSheetState,
257+
onUserDataClicked = onUserDataClicked,
255258
)
259+
ReinviteDialog(state = state)
256260

257261
// Since the textfield is now based on an Android view, this is no longer done automatically.
258262
// We need to hide the keyboard automatically when navigating out of this screen.
@@ -310,6 +314,7 @@ private fun MessagesViewContent(
310314
onReactionClicked: (key: String, TimelineItem.Event) -> Unit,
311315
onReactionLongClicked: (key: String, TimelineItem.Event) -> Unit,
312316
onMoreReactionsClicked: (TimelineItem.Event) -> Unit,
317+
onReadReceiptClick: (TimelineItem.Event) -> Unit,
313318
onMessageLongClicked: (TimelineItem.Event) -> Unit,
314319
onTimestampClicked: (TimelineItem.Event) -> Unit,
315320
onSendLocationClicked: () -> Unit,
@@ -381,6 +386,7 @@ private fun MessagesViewContent(
381386
onReactionClicked = onReactionClicked,
382387
onReactionLongClicked = onReactionLongClicked,
383388
onMoreReactionsClicked = onMoreReactionsClicked,
389+
onReadReceiptClick = onReadReceiptClick,
384390
onSwipeToReply = onSwipeToReply,
385391
)
386392
},
@@ -406,7 +412,8 @@ private fun MessagesViewComposerBottomSheetContents(
406412
if (state.userHasPermissionToSendMessage) {
407413
Column(modifier = modifier.fillMaxWidth()) {
408414
MentionSuggestionsPickerView(
409-
modifier = Modifier.heightIn(max = 230.dp)
415+
modifier = Modifier
416+
.heightIn(max = 230.dp)
410417
// Consume all scrolling, preventing the bottom sheet from being dragged when interacting with the list of suggestions
411418
.nestedScroll(object : NestedScrollConnection {
412419
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,14 @@ import io.element.android.features.messages.impl.timeline.session.SessionState
3535
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
3636
import io.element.android.libraries.architecture.Presenter
3737
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
38+
import io.element.android.libraries.featureflag.api.FeatureFlagService
39+
import io.element.android.libraries.featureflag.api.FeatureFlags
3840
import io.element.android.libraries.matrix.api.core.EventId
3941
import io.element.android.libraries.matrix.api.encryption.BackupState
4042
import io.element.android.libraries.matrix.api.encryption.EncryptionService
4143
import io.element.android.libraries.matrix.api.room.MatrixRoom
4244
import io.element.android.libraries.matrix.api.room.MessageEventType
45+
import io.element.android.libraries.matrix.api.room.roomMembers
4346
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
4447
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
4548
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
@@ -64,6 +67,7 @@ class TimelinePresenter @Inject constructor(
6467
private val analyticsService: AnalyticsService,
6568
private val verificationService: SessionVerificationService,
6669
private val encryptionService: EncryptionService,
70+
private val featureFlagService: FeatureFlagService,
6771
private val redactedVoiceMessageManager: RedactedVoiceMessageManager,
6872
) : Presenter<TimelineState> {
6973

@@ -99,6 +103,9 @@ class TimelinePresenter @Inject constructor(
99103
}
100104
}
101105

106+
val readReceiptsEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.ReadReceipts).collectAsState(initial = false)
107+
val membersState by room.membersStateFlow.collectAsState()
108+
102109
fun handleEvents(event: TimelineEvents) {
103110
when (event) {
104111
TimelineEvents.LoadMore -> localScope.paginateBackwards()
@@ -138,7 +145,17 @@ class TimelinePresenter @Inject constructor(
138145
LaunchedEffect(Unit) {
139146
timeline
140147
.timelineItems
141-
.onEach(timelineItemsFactory::replaceWith)
148+
.onEach {
149+
timelineItemsFactory.replaceWith(
150+
timelineItems = it,
151+
roomMembers = if (readReceiptsEnabled) {
152+
membersState.roomMembers().orEmpty()
153+
} else {
154+
// Give an empty list to not affect performance
155+
emptyList()
156+
}
157+
)
158+
}
142159
.onEach { timelineItems ->
143160
if (timelineItems.isEmpty()) {
144161
paginateBackwards()
@@ -153,6 +170,7 @@ class TimelinePresenter @Inject constructor(
153170
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
154171
paginationState = paginationState,
155172
timelineItems = timelineItems,
173+
showReadReceipts = readReceiptsEnabled,
156174
hasNewItems = hasNewItems.value,
157175
sessionState = sessionState,
158176
eventSink = ::handleEvents

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import kotlinx.collections.immutable.ImmutableList
2626
@Immutable
2727
data class TimelineState(
2828
val timelineItems: ImmutableList<TimelineItem>,
29+
val showReadReceipts: Boolean,
2930
val highlightedEventId: EventId?,
3031
val userHasPermissionToSendMessage: Boolean,
3132
val paginationState: MatrixTimeline.PaginationState,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616

1717
package io.element.android.features.messages.impl.timeline
1818

19+
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
1920
import io.element.android.features.messages.impl.timeline.model.TimelineItem
2021
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
2122
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
23+
import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts
2224
import io.element.android.features.messages.impl.timeline.model.anAggregatedReaction
2325
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
2426
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
@@ -43,6 +45,7 @@ import kotlin.random.Random
4345

4446
fun aTimelineState(timelineItems: ImmutableList<TimelineItem> = persistentListOf()) = TimelineState(
4547
timelineItems = timelineItems,
48+
showReadReceipts = false,
4649
paginationState = MatrixTimeline.PaginationState(
4750
isBackPaginating = false,
4851
hasMoreToLoadBackwards = true,
@@ -118,11 +121,12 @@ internal fun aTimelineItemEvent(
118121
senderDisplayName: String = "Sender",
119122
content: TimelineItemEventContent = aTimelineItemTextContent(),
120123
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
121-
sendState: LocalEventSendState = LocalEventSendState.Sent(eventId),
124+
sendState: LocalEventSendState? = null,
122125
inReplyTo: InReplyTo? = null,
123126
isThreaded: Boolean = false,
124127
debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(),
125128
timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(),
129+
readReceiptState: TimelineItemReadReceipts = aTimelineItemReadReceipts(),
126130
): TimelineItem.Event {
127131
return TimelineItem.Event(
128132
id = UUID.randomUUID().toString(),
@@ -132,6 +136,7 @@ internal fun aTimelineItemEvent(
132136
senderAvatar = AvatarData("@senderId:domain", "sender", size = AvatarSize.TimelineSender),
133137
content = content,
134138
reactionsState = timelineItemReactions,
139+
readReceiptState = readReceiptState,
135140
sentTime = "12:34",
136141
isMine = isMine,
137142
senderDisplayName = senderDisplayName,
@@ -173,6 +178,12 @@ internal fun aTimelineItemDebugInfo(
173178
model, originalJson, latestEditedJson
174179
)
175180

181+
internal fun aTimelineItemReadReceipts(): TimelineItemReadReceipts {
182+
return TimelineItemReadReceipts(
183+
receipts = emptyList<ReadReceiptData>().toImmutableList(),
184+
)
185+
}
186+
176187
fun aGroupedEvents(id: Long = 0): TimelineItem.GroupedEvents {
177188
val event = aTimelineItemEvent(
178189
isMine = true,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ fun TimelineView(
9292
onReactionClicked: (emoji: String, TimelineItem.Event) -> Unit,
9393
onReactionLongClicked: (emoji: String, TimelineItem.Event) -> Unit,
9494
onMoreReactionsClicked: (TimelineItem.Event) -> Unit,
95+
onReadReceiptClick: (TimelineItem.Event) -> Unit,
9596
modifier: Modifier = Modifier,
9697
) {
9798
fun onReachedLoadMore() {
@@ -126,6 +127,9 @@ fun TimelineView(
126127
) { timelineItem ->
127128
TimelineItemRow(
128129
timelineItem = timelineItem,
130+
showReadReceipts = state.showReadReceipts,
131+
isLastOutgoingMessage = (timelineItem as? TimelineItem.Event)?.isMine == true
132+
&& state.timelineItems.first().identifier() == timelineItem.identifier(),
129133
highlightedItem = state.highlightedEventId?.value,
130134
userHasPermissionToSendMessage = state.userHasPermissionToSendMessage,
131135
onClick = onMessageClicked,
@@ -135,6 +139,7 @@ fun TimelineView(
135139
onReactionClick = onReactionClicked,
136140
onReactionLongClick = onReactionLongClicked,
137141
onMoreReactionsClick = onMoreReactionsClicked,
142+
onReadReceiptClick = onReadReceiptClick,
138143
onTimestampClicked = onTimestampClicked,
139144
sessionState = state.sessionState,
140145
eventSink = state.eventSink,
@@ -169,6 +174,8 @@ fun TimelineView(
169174
@Composable
170175
private fun TimelineItemRow(
171176
timelineItem: TimelineItem,
177+
showReadReceipts: Boolean,
178+
isLastOutgoingMessage: Boolean,
172179
highlightedItem: String?,
173180
userHasPermissionToSendMessage: Boolean,
174181
sessionState: SessionState,
@@ -179,6 +186,7 @@ private fun TimelineItemRow(
179186
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
180187
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
181188
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
189+
onReadReceiptClick: (TimelineItem.Event) -> Unit,
182190
onTimestampClicked: (TimelineItem.Event) -> Unit,
183191
onSwipeToReply: (TimelineItem.Event) -> Unit,
184192
eventSink: (TimelineEvents) -> Unit,
@@ -205,6 +213,8 @@ private fun TimelineItemRow(
205213
} else {
206214
TimelineItemEventRow(
207215
event = timelineItem,
216+
showReadReceipts = showReadReceipts,
217+
isLastOutgoingMessage = isLastOutgoingMessage,
208218
isHighlighted = highlightedItem == timelineItem.identifier(),
209219
canReply = userHasPermissionToSendMessage && timelineItem.content.canBeRepliedTo(),
210220
onClick = { onClick(timelineItem) },
@@ -214,6 +224,7 @@ private fun TimelineItemRow(
214224
onReactionClick = onReactionClick,
215225
onReactionLongClick = onReactionLongClick,
216226
onMoreReactionsClick = onMoreReactionsClick,
227+
onReadReceiptClick = onReadReceiptClick,
217228
onTimestampClicked = onTimestampClicked,
218229
onSwipeToReply = { onSwipeToReply(timelineItem) },
219230
eventSink = eventSink,
@@ -244,6 +255,8 @@ private fun TimelineItemRow(
244255
timelineItem.events.forEach { subGroupEvent ->
245256
TimelineItemRow(
246257
timelineItem = subGroupEvent,
258+
showReadReceipts = showReadReceipts,
259+
isLastOutgoingMessage = isLastOutgoingMessage,
247260
highlightedItem = highlightedItem,
248261
sessionState = sessionState,
249262
userHasPermissionToSendMessage = false,
@@ -255,6 +268,7 @@ private fun TimelineItemRow(
255268
onReactionClick = onReactionClick,
256269
onReactionLongClick = onReactionLongClick,
257270
onMoreReactionsClick = onMoreReactionsClick,
271+
onReadReceiptClick = onReadReceiptClick,
258272
eventSink = eventSink,
259273
onSwipeToReply = {},
260274
)
@@ -362,6 +376,7 @@ internal fun TimelineViewPreview(
362376
onReactionLongClicked = { _, _ -> },
363377
onMoreReactionsClicked = {},
364378
onSwipeToReply = {},
379+
onReadReceiptClick = {},
365380
)
366381
}
367382
}

0 commit comments

Comments
 (0)