Skip to content

Commit c1337de

Browse files
authored
Merge pull request #3597 from element-hq/feature/fga/timeline_better_jump_to_behaviours
Timeline better jump to behaviours
2 parents 4d6b37f + adc03c9 commit c1337de

File tree

26 files changed

+218
-142
lines changed

26 files changed

+218
-142
lines changed

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
4646
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
4747
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
4848
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
49-
import io.element.android.features.messages.impl.typing.TypingNotificationPresenter
5049
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
5150
import io.element.android.features.networkmonitor.api.NetworkMonitor
5251
import io.element.android.features.networkmonitor.api.NetworkStatus
@@ -91,7 +90,6 @@ class MessagesPresenter @AssistedInject constructor(
9190
private val composerPresenter: MessageComposerPresenter,
9291
private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter,
9392
timelinePresenterFactory: TimelinePresenter.Factory,
94-
private val typingNotificationPresenter: TypingNotificationPresenter,
9593
private val actionListPresenterFactory: ActionListPresenter.Factory,
9694
private val customReactionPresenter: CustomReactionPresenter,
9795
private val reactionSummaryPresenter: ReactionSummaryPresenter,
@@ -125,7 +123,6 @@ class MessagesPresenter @AssistedInject constructor(
125123
val composerState = composerPresenter.present()
126124
val voiceMessageComposerState = voiceMessageComposerPresenter.present()
127125
val timelineState = timelinePresenter.present()
128-
val typingNotificationState = typingNotificationPresenter.present()
129126
val actionListState = actionListPresenter.present()
130127
val customReactionState = customReactionPresenter.present()
131128
val reactionSummaryState = reactionSummaryPresenter.present()
@@ -216,7 +213,6 @@ class MessagesPresenter @AssistedInject constructor(
216213
userEventPermissions = userEventPermissions,
217214
voiceMessageComposerState = voiceMessageComposerState,
218215
timelineState = timelineState,
219-
typingNotificationState = typingNotificationState,
220216
actionListState = actionListState,
221217
customReactionState = customReactionState,
222218
reactionSummaryState = reactionSummaryState,

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import io.element.android.features.messages.impl.timeline.TimelineState
1515
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
1616
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
1717
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
18-
import io.element.android.features.messages.impl.typing.TypingNotificationState
1918
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
2019
import io.element.android.libraries.architecture.AsyncData
2120
import io.element.android.libraries.designsystem.components.avatar.AvatarData
@@ -33,7 +32,6 @@ data class MessagesState(
3332
val composerState: MessageComposerState,
3433
val voiceMessageComposerState: VoiceMessageComposerState,
3534
val timelineState: TimelineState,
36-
val typingNotificationState: TypingNotificationState,
3735
val actionListState: ActionListState,
3836
val customReactionState: CustomReactionState,
3937
val reactionSummaryState: ReactionSummaryState,

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import io.element.android.features.messages.impl.timeline.components.receipt.bot
2626
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
2727
import io.element.android.features.messages.impl.timeline.model.TimelineItem
2828
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
29-
import io.element.android.features.messages.impl.typing.aTypingNotificationState
3029
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
3130
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
3231
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessagePreviewState
@@ -122,7 +121,6 @@ fun aMessagesState(
122121
userEventPermissions = userEventPermissions,
123122
composerState = composerState,
124123
voiceMessageComposerState = voiceMessageComposerState,
125-
typingNotificationState = aTypingNotificationState(),
126124
timelineState = timelineState,
127125
readReceiptBottomSheetState = readReceiptBottomSheetState,
128126
actionListState = actionListState,

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,6 @@ private fun MessagesViewContent(
379379
val scrollBehavior = PinnedMessagesBannerViewDefaults.rememberExitOnScrollBehavior()
380380
TimelineView(
381381
state = state.timelineState,
382-
typingNotificationState = state.typingNotificationState,
383382
onUserDataClick = onUserDataClick,
384383
onLinkClick = onLinkClick,
385384
onMessageClick = onMessageClick,

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import io.element.android.features.messages.impl.crypto.sendfailure.resolve.Reso
1414
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState
1515
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerPresenter
1616
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
17+
import io.element.android.features.messages.impl.typing.TypingNotificationPresenter
18+
import io.element.android.features.messages.impl.typing.TypingNotificationState
1719
import io.element.android.libraries.architecture.Presenter
1820
import io.element.android.libraries.di.RoomScope
1921

@@ -25,4 +27,7 @@ interface MessagesModule {
2527

2628
@Binds
2729
fun bindResolveVerifiedUserSendFailurePresenter(presenter: ResolveVerifiedUserSendFailurePresenter): Presenter<ResolveVerifiedUserSendFailureState>
30+
31+
@Binds
32+
fun bindTypingNotificationPresenter(presenter: TypingNotificationPresenter): Presenter<TypingNotificationState>
2833
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
3030
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
3131
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig
3232
import io.element.android.features.messages.impl.timeline.model.TimelineItem
33+
import io.element.android.features.messages.impl.typing.TypingNotificationState
3334
import io.element.android.libraries.architecture.AsyncData
3435
import io.element.android.libraries.architecture.Presenter
3536
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@@ -44,6 +45,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
4445
import io.element.android.services.analytics.api.AnalyticsService
4546
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
4647
import kotlinx.collections.immutable.ImmutableList
48+
import kotlinx.collections.immutable.persistentListOf
4749
import kotlinx.coroutines.CoroutineScope
4850
import kotlinx.coroutines.flow.combine
4951
import kotlinx.coroutines.flow.flowOf
@@ -87,7 +89,12 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
8789
userHasPermissionToSendReaction = false,
8890
isCallOngoing = false,
8991
// don't compute this value or the pin icon will be shown
90-
pinnedEventIds = emptyList()
92+
pinnedEventIds = emptyList(),
93+
typingNotificationState = TypingNotificationState(
94+
renderTypingNotifications = false,
95+
typingMembers = persistentListOf(),
96+
reserveSpace = false,
97+
)
9198
)
9299
}
93100

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

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,39 @@
88
package io.element.android.features.messages.impl.timeline
99

1010
import io.element.android.features.messages.impl.timeline.model.TimelineItem
11-
import io.element.android.libraries.di.RoomScope
12-
import io.element.android.libraries.di.SingleIn
1311
import io.element.android.libraries.matrix.api.core.EventId
12+
import kotlinx.coroutines.CompletableDeferred
13+
import kotlinx.coroutines.sync.Mutex
14+
import kotlinx.coroutines.sync.withLock
1415
import timber.log.Timber
1516
import javax.inject.Inject
1617

17-
@SingleIn(RoomScope::class)
1818
class TimelineItemIndexer @Inject constructor() {
19+
// This is a latch to wait for the first process call
20+
private val firstProcessLatch = CompletableDeferred<Unit>()
1921
private val timelineEventsIndexes = mutableMapOf<EventId, Int>()
2022

21-
fun isKnown(eventId: EventId): Boolean {
22-
return timelineEventsIndexes.containsKey(eventId).also {
23-
Timber.d("$eventId isKnown = $it")
23+
private val mutex = Mutex()
24+
25+
suspend fun isKnown(eventId: EventId): Boolean {
26+
firstProcessLatch.await()
27+
return mutex.withLock {
28+
timelineEventsIndexes.containsKey(eventId).also {
29+
Timber.d("$eventId isKnown = $it")
30+
}
2431
}
2532
}
2633

27-
fun indexOf(eventId: EventId): Int {
28-
return (timelineEventsIndexes[eventId] ?: -1).also {
29-
Timber.d("indexOf $eventId= $it")
34+
suspend fun indexOf(eventId: EventId): Int {
35+
firstProcessLatch.await()
36+
return mutex.withLock {
37+
(timelineEventsIndexes[eventId] ?: -1).also {
38+
Timber.d("indexOf $eventId= $it")
39+
}
3040
}
3141
}
3242

33-
fun process(timelineItems: List<TimelineItem>) {
43+
suspend fun process(timelineItems: List<TimelineItem>) = mutex.withLock {
3444
Timber.d("process ${timelineItems.size} items")
3545
timelineEventsIndexes.clear()
3646
timelineItems.forEachIndexed { index, timelineItem ->
@@ -46,6 +56,7 @@ class TimelineItemIndexer @Inject constructor() {
4656
else -> Unit
4757
}
4858
}
59+
firstProcessLatch.complete(Unit)
4960
}
5061

5162
private fun processEvent(event: TimelineItem.Event, index: Int) {

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

Lines changed: 46 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import io.element.android.features.messages.impl.timeline.factories.TimelineItem
2828
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig
2929
import io.element.android.features.messages.impl.timeline.model.NewEventState
3030
import io.element.android.features.messages.impl.timeline.model.TimelineItem
31+
import io.element.android.features.messages.impl.typing.TypingNotificationState
3132
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
3233
import io.element.android.features.poll.api.actions.EndPollAction
3334
import io.element.android.features.poll.api.actions.SendPollResponseAction
@@ -54,12 +55,12 @@ import kotlinx.coroutines.flow.launchIn
5455
import kotlinx.coroutines.flow.onEach
5556
import kotlinx.coroutines.launch
5657
import kotlinx.coroutines.withContext
58+
import timber.log.Timber
5759

5860
const val FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS = 200L
5961

6062
class TimelinePresenter @AssistedInject constructor(
6163
timelineItemsFactoryCreator: TimelineItemsFactory.Creator,
62-
private val timelineItemIndexer: TimelineItemIndexer,
6364
private val room: MatrixRoom,
6465
private val dispatchers: CoroutineDispatchers,
6566
private val appScope: CoroutineScope,
@@ -69,7 +70,9 @@ class TimelinePresenter @AssistedInject constructor(
6970
private val endPollAction: EndPollAction,
7071
private val sessionPreferencesStore: SessionPreferencesStore,
7172
private val timelineController: TimelineController,
73+
private val timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(),
7274
private val resolveVerifiedUserSendFailurePresenter: Presenter<ResolveVerifiedUserSendFailureState>,
75+
private val typingNotificationPresenter: Presenter<TypingNotificationState>,
7376
) : Presenter<TimelineState> {
7477
@AssistedFactory
7578
interface Factory {
@@ -87,13 +90,7 @@ class TimelinePresenter @AssistedInject constructor(
8790
@Composable
8891
override fun present(): TimelineState {
8992
val localScope = rememberCoroutineScope()
90-
val focusRequestState: MutableState<FocusRequestState> = remember {
91-
mutableStateOf(FocusRequestState.None)
92-
}
93-
94-
LaunchedEffect(Unit) {
95-
timelineItemsFactory.timelineItems.collect { timelineItems = it }
96-
}
93+
var focusRequestState: FocusRequestState by remember { mutableStateOf(FocusRequestState.None) }
9794

9895
val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) }
9996

@@ -152,13 +149,13 @@ class TimelinePresenter @AssistedInject constructor(
152149
navigator.onEditPollClick(event.pollStartId)
153150
}
154151
is TimelineEvents.FocusOnEvent -> {
155-
focusRequestState.value = FocusRequestState.Requested(event.eventId, event.debounce)
152+
focusRequestState = FocusRequestState.Requested(event.eventId, event.debounce)
156153
}
157154
is TimelineEvents.OnFocusEventRender -> {
158-
focusRequestState.value = focusRequestState.value.onFocusEventRender()
155+
focusRequestState = focusRequestState.onFocusEventRender()
159156
}
160157
is TimelineEvents.ClearFocusRequestState -> {
161-
focusRequestState.value = FocusRequestState.None
158+
focusRequestState = FocusRequestState.None
162159
}
163160
is TimelineEvents.JumpToLive -> {
164161
timelineController.focusOnLive()
@@ -171,28 +168,46 @@ class TimelinePresenter @AssistedInject constructor(
171168
}
172169
}
173170

174-
LaunchedEffect(focusRequestState.value) {
175-
when (val currentFocusRequestState = focusRequestState.value) {
171+
LaunchedEffect(Unit) {
172+
timelineItemsFactory.timelineItems
173+
.onEach { newTimelineItems ->
174+
timelineItemIndexer.process(newTimelineItems)
175+
timelineItems = newTimelineItems
176+
}
177+
.launchIn(this)
178+
179+
combine(timelineController.timelineItems(), room.membersStateFlow) { items, membersState ->
180+
timelineItemsFactory.replaceWith(
181+
timelineItems = items,
182+
roomMembers = membersState.roomMembers().orEmpty()
183+
)
184+
items
185+
}
186+
.onEach(redactedVoiceMessageManager::onEachMatrixTimelineItem)
187+
.launchIn(this)
188+
}
189+
190+
LaunchedEffect(focusRequestState) {
191+
Timber.d("## focusRequestState: $focusRequestState")
192+
when (val currentFocusRequestState = focusRequestState) {
176193
is FocusRequestState.Requested -> {
177194
delay(currentFocusRequestState.debounce)
178195
if (timelineItemIndexer.isKnown(currentFocusRequestState.eventId)) {
179196
val index = timelineItemIndexer.indexOf(currentFocusRequestState.eventId)
180-
focusRequestState.value = FocusRequestState.Success(eventId = currentFocusRequestState.eventId, index = index)
197+
focusRequestState = FocusRequestState.Success(eventId = currentFocusRequestState.eventId, index = index)
181198
} else {
182-
focusRequestState.value = FocusRequestState.Loading(eventId = currentFocusRequestState.eventId)
199+
focusRequestState = FocusRequestState.Loading(eventId = currentFocusRequestState.eventId)
183200
}
184201
}
185202
is FocusRequestState.Loading -> {
186203
val eventId = currentFocusRequestState.eventId
187204
timelineController.focusOnEvent(eventId)
188-
.fold(
189-
onSuccess = {
190-
focusRequestState.value = FocusRequestState.Success(eventId = eventId)
191-
},
192-
onFailure = {
193-
focusRequestState.value = FocusRequestState.Failure(throwable = it)
194-
}
195-
)
205+
.onSuccess {
206+
focusRequestState = FocusRequestState.Success(eventId = eventId)
207+
}
208+
.onFailure {
209+
focusRequestState = FocusRequestState.Failure(it)
210+
}
196211
}
197212
else -> Unit
198213
}
@@ -202,30 +217,19 @@ class TimelinePresenter @AssistedInject constructor(
202217
computeNewItemState(timelineItems, prevMostRecentItemId, newEventState)
203218
}
204219

205-
LaunchedEffect(timelineItems.size, focusRequestState.value) {
206-
val currentFocusRequestState = focusRequestState.value
207-
if (currentFocusRequestState is FocusRequestState.Success && !currentFocusRequestState.isIndexed) {
220+
LaunchedEffect(timelineItems.size, focusRequestState) {
221+
val currentFocusRequestState = focusRequestState
222+
if (currentFocusRequestState is FocusRequestState.Success && !currentFocusRequestState.rendered) {
208223
val eventId = currentFocusRequestState.eventId
209224
if (timelineItemIndexer.isKnown(eventId)) {
210225
val index = timelineItemIndexer.indexOf(eventId)
211-
focusRequestState.value = FocusRequestState.Success(eventId = eventId, index = index)
226+
focusRequestState = FocusRequestState.Success(eventId = eventId, index = index)
212227
}
213228
}
214229
}
215230

216-
LaunchedEffect(Unit) {
217-
combine(timelineController.timelineItems(), room.membersStateFlow) { items, membersState ->
218-
timelineItemsFactory.replaceWith(
219-
timelineItems = items,
220-
roomMembers = membersState.roomMembers().orEmpty()
221-
)
222-
items
223-
}
224-
.onEach(redactedVoiceMessageManager::onEachMatrixTimelineItem)
225-
.launchIn(this)
226-
}
227-
228-
val timelineRoomInfo by remember {
231+
val typingNotificationState = typingNotificationPresenter.present()
232+
val timelineRoomInfo by remember(typingNotificationState) {
229233
derivedStateOf {
230234
TimelineRoomInfo(
231235
name = room.displayName,
@@ -234,6 +238,7 @@ class TimelinePresenter @AssistedInject constructor(
234238
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
235239
isCallOngoing = roomInfo?.hasRoomCall.orFalse(),
236240
pinnedEventIds = roomInfo?.pinnedEventIds.orEmpty(),
241+
typingNotificationState = typingNotificationState,
237242
)
238243
}
239244
}
@@ -243,7 +248,7 @@ class TimelinePresenter @AssistedInject constructor(
243248
renderReadReceipts = renderReadReceipts,
244249
newEventState = newEventState.value,
245250
isLive = isLive,
246-
focusRequestState = focusRequestState.value,
251+
focusRequestState = focusRequestState,
247252
messageShield = messageShield.value,
248253
resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState,
249254
eventSink = { handleEvents(it) }

0 commit comments

Comments
 (0)