Skip to content

Commit adc03c9

Browse files
committed
timeline : improve jumpTo precision (introducing animateScrollToItemCenter)
1 parent 88e01e7 commit adc03c9

File tree

14 files changed

+88
-72
lines changed

14 files changed

+88
-72
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,4 @@ interface MessagesModule {
3030

3131
@Binds
3232
fun bindTypingNotificationPresenter(presenter: TypingNotificationPresenter): Presenter<TypingNotificationState>
33-
3433
}

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,13 @@
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
1412
import kotlinx.coroutines.CompletableDeferred
1513
import kotlinx.coroutines.sync.Mutex
1614
import kotlinx.coroutines.sync.withLock
1715
import timber.log.Timber
1816
import javax.inject.Inject
1917

20-
@SingleIn(RoomScope::class)
2118
class TimelineItemIndexer @Inject constructor() {
2219
// This is a latch to wait for the first process call
2320
private val firstProcessLatch = CompletableDeferred<Unit>()

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

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,12 @@ import kotlinx.coroutines.flow.launchIn
5555
import kotlinx.coroutines.flow.onEach
5656
import kotlinx.coroutines.launch
5757
import kotlinx.coroutines.withContext
58+
import timber.log.Timber
5859

5960
const val FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS = 200L
6061

6162
class TimelinePresenter @AssistedInject constructor(
6263
timelineItemsFactoryCreator: TimelineItemsFactory.Creator,
63-
private val timelineItemIndexer: TimelineItemIndexer,
6464
private val room: MatrixRoom,
6565
private val dispatchers: CoroutineDispatchers,
6666
private val appScope: CoroutineScope,
@@ -70,6 +70,7 @@ class TimelinePresenter @AssistedInject constructor(
7070
private val endPollAction: EndPollAction,
7171
private val sessionPreferencesStore: SessionPreferencesStore,
7272
private val timelineController: TimelineController,
73+
private val timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(),
7374
private val resolveVerifiedUserSendFailurePresenter: Presenter<ResolveVerifiedUserSendFailureState>,
7475
private val typingNotificationPresenter: Presenter<TypingNotificationState>,
7576
) : Presenter<TimelineState> {
@@ -89,13 +90,7 @@ class TimelinePresenter @AssistedInject constructor(
8990
@Composable
9091
override fun present(): TimelineState {
9192
val localScope = rememberCoroutineScope()
92-
val focusRequestState: MutableState<FocusRequestState> = remember {
93-
mutableStateOf(FocusRequestState.None)
94-
}
95-
96-
LaunchedEffect(Unit) {
97-
timelineItemsFactory.timelineItems.collect { timelineItems = it }
98-
}
93+
var focusRequestState: FocusRequestState by remember { mutableStateOf(FocusRequestState.None) }
9994

10095
val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) }
10196

@@ -154,13 +149,13 @@ class TimelinePresenter @AssistedInject constructor(
154149
navigator.onEditPollClick(event.pollStartId)
155150
}
156151
is TimelineEvents.FocusOnEvent -> {
157-
focusRequestState.value = FocusRequestState.Requested(event.eventId, event.debounce)
152+
focusRequestState = FocusRequestState.Requested(event.eventId, event.debounce)
158153
}
159154
is TimelineEvents.OnFocusEventRender -> {
160-
focusRequestState.value = focusRequestState.value.onFocusEventRender()
155+
focusRequestState = focusRequestState.onFocusEventRender()
161156
}
162157
is TimelineEvents.ClearFocusRequestState -> {
163-
focusRequestState.value = FocusRequestState.None
158+
focusRequestState = FocusRequestState.None
164159
}
165160
is TimelineEvents.JumpToLive -> {
166161
timelineController.focusOnLive()
@@ -173,28 +168,46 @@ class TimelinePresenter @AssistedInject constructor(
173168
}
174169
}
175170

176-
LaunchedEffect(focusRequestState.value) {
177-
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) {
178193
is FocusRequestState.Requested -> {
179194
delay(currentFocusRequestState.debounce)
180195
if (timelineItemIndexer.isKnown(currentFocusRequestState.eventId)) {
181196
val index = timelineItemIndexer.indexOf(currentFocusRequestState.eventId)
182-
focusRequestState.value = FocusRequestState.Success(eventId = currentFocusRequestState.eventId, index = index)
197+
focusRequestState = FocusRequestState.Success(eventId = currentFocusRequestState.eventId, index = index)
183198
} else {
184-
focusRequestState.value = FocusRequestState.Loading(eventId = currentFocusRequestState.eventId)
199+
focusRequestState = FocusRequestState.Loading(eventId = currentFocusRequestState.eventId)
185200
}
186201
}
187202
is FocusRequestState.Loading -> {
188203
val eventId = currentFocusRequestState.eventId
189204
timelineController.focusOnEvent(eventId)
190-
.fold(
191-
onSuccess = {
192-
focusRequestState.value = FocusRequestState.Success(eventId = eventId)
193-
},
194-
onFailure = {
195-
focusRequestState.value = FocusRequestState.Failure(throwable = it)
196-
}
197-
)
205+
.onSuccess {
206+
focusRequestState = FocusRequestState.Success(eventId = eventId)
207+
}
208+
.onFailure {
209+
focusRequestState = FocusRequestState.Failure(it)
210+
}
198211
}
199212
else -> Unit
200213
}
@@ -204,29 +217,17 @@ class TimelinePresenter @AssistedInject constructor(
204217
computeNewItemState(timelineItems, prevMostRecentItemId, newEventState)
205218
}
206219

207-
LaunchedEffect(timelineItems.size, focusRequestState.value) {
208-
val currentFocusRequestState = focusRequestState.value
209-
if (currentFocusRequestState is FocusRequestState.Success && !currentFocusRequestState.isIndexed) {
220+
LaunchedEffect(timelineItems.size, focusRequestState) {
221+
val currentFocusRequestState = focusRequestState
222+
if (currentFocusRequestState is FocusRequestState.Success && !currentFocusRequestState.rendered) {
210223
val eventId = currentFocusRequestState.eventId
211224
if (timelineItemIndexer.isKnown(eventId)) {
212225
val index = timelineItemIndexer.indexOf(eventId)
213-
focusRequestState.value = FocusRequestState.Success(eventId = eventId, index = index)
226+
focusRequestState = FocusRequestState.Success(eventId = eventId, index = index)
214227
}
215228
}
216229
}
217230

218-
LaunchedEffect(Unit) {
219-
combine(timelineController.timelineItems(), room.membersStateFlow) { items, membersState ->
220-
timelineItemsFactory.replaceWith(
221-
timelineItems = items,
222-
roomMembers = membersState.roomMembers().orEmpty()
223-
)
224-
items
225-
}
226-
.onEach(redactedVoiceMessageManager::onEachMatrixTimelineItem)
227-
.launchIn(this)
228-
}
229-
230231
val typingNotificationState = typingNotificationPresenter.present()
231232
val timelineRoomInfo by remember(typingNotificationState) {
232233
derivedStateOf {
@@ -247,7 +248,7 @@ class TimelinePresenter @AssistedInject constructor(
247248
renderReadReceipts = renderReadReceipts,
248249
newEventState = newEventState.value,
249250
isLive = isLive,
250-
focusRequestState = focusRequestState.value,
251+
focusRequestState = focusRequestState,
251252
messageShield = messageShield.value,
252253
resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState,
253254
eventSink = { handleEvents(it) }

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,13 @@ data class TimelineState(
3131
val resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState,
3232
val eventSink: (TimelineEvents) -> Unit,
3333
) {
34-
val lastTimelineEvent = timelineItems.firstOrNull { it is TimelineItem.Event} as? TimelineItem.Event
34+
private val lastTimelineEvent = timelineItems.firstOrNull { it is TimelineItem.Event } as? TimelineItem.Event
3535
val hasAnyEvent = lastTimelineEvent != null
3636
val focusedEventId = focusRequestState.eventId()
3737

38-
3938
fun isLastOutgoingMessage(uniqueId: UniqueId): Boolean {
40-
return lastTimelineEvent != null && lastTimelineEvent.isMine && lastTimelineEvent.id == uniqueId
39+
return isLive && lastTimelineEvent != null && lastTimelineEvent.isMine && lastTimelineEvent.id == uniqueId
4140
}
42-
4341
}
4442

4543
@Immutable

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

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
6262
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
6363
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
6464
import io.element.android.libraries.designsystem.theme.components.Icon
65+
import io.element.android.libraries.designsystem.utils.animateScrollToItemCenter
6566
import io.element.android.libraries.matrix.api.core.EventId
6667
import io.element.android.libraries.matrix.api.core.UserId
6768
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
6869
import io.element.android.libraries.ui.strings.CommonStrings
6970
import kotlinx.coroutines.launch
70-
import kotlin.math.abs
7171

7272
@Composable
7373
fun TimelineView(
@@ -238,12 +238,8 @@ private fun BoxScope.TimelineScrollHelper(
238238

239239
val latestOnFocusEventRender by rememberUpdatedState(onFocusEventRender)
240240
LaunchedEffect(focusRequestState) {
241-
if (focusRequestState is FocusRequestState.Success && focusRequestState.isIndexed) {
242-
if (abs(lazyListState.firstVisibleItemIndex - focusRequestState.index) < 10) {
243-
lazyListState.animateScrollToItem(focusRequestState.index)
244-
} else {
245-
lazyListState.scrollToItem(focusRequestState.index)
246-
}
241+
if (focusRequestState is FocusRequestState.Success && focusRequestState.isIndexed && !focusRequestState.rendered) {
242+
lazyListState.animateScrollToItemCenter(focusRequestState.index)
247243
latestOnFocusEventRender()
248244
}
249245
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPr
1414
import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories
1515
import io.element.android.features.messages.impl.timeline.model.TimelineItem
1616
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
17-
import io.element.android.features.messages.impl.typing.aTypingNotificationState
1817
import io.element.android.libraries.designsystem.preview.ElementPreview
1918
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
2019
import kotlinx.collections.immutable.toImmutableList

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ package io.element.android.features.messages.impl.timeline.factories
1010
import dagger.assisted.Assisted
1111
import dagger.assisted.AssistedFactory
1212
import dagger.assisted.AssistedInject
13-
import io.element.android.features.messages.impl.timeline.TimelineItemIndexer
1413
import io.element.android.features.messages.impl.timeline.diff.TimelineItemsCacheInvalidator
1514
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemEventFactory
1615
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
@@ -36,7 +35,6 @@ class TimelineItemsFactory @AssistedInject constructor(
3635
private val dispatchers: CoroutineDispatchers,
3736
private val virtualItemFactory: TimelineItemVirtualFactory,
3837
private val timelineItemGrouper: TimelineItemGrouper,
39-
private val timelineItemIndexer: TimelineItemIndexer,
4038
) {
4139
@AssistedFactory
4240
interface Creator {
@@ -96,7 +94,6 @@ class TimelineItemsFactory @AssistedInject constructor(
9694
}
9795
}
9896
val result = timelineItemGrouper.group(newTimelineItemStates).toPersistentList()
99-
timelineItemIndexer.process(result)
10097
this._timelineItems.emit(result)
10198
}
10299

features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
4040
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
4141
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
4242
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
43+
import io.element.android.features.messages.impl.typing.aTypingNotificationState
4344
import io.element.android.features.messages.impl.utils.FakeTextPillificationHelper
4445
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer
4546
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
@@ -1047,6 +1048,7 @@ class MessagesPresenterTest {
10471048
timelineItemIndexer = TimelineItemIndexer(),
10481049
timelineController = TimelineController(matrixRoom),
10491050
resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() },
1051+
typingNotificationPresenter = { aTypingNotificationState() },
10501052
)
10511053
val timelinePresenterFactory = object : TimelinePresenter.Factory {
10521054
override fun create(navigator: MessagesNavigator): TimelinePresenter {

features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
package io.element.android.features.messages.impl.fixtures
99

10-
import io.element.android.features.messages.impl.timeline.TimelineItemIndexer
1110
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
1211
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig
1312
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFactory
@@ -40,19 +39,16 @@ import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorW
4039
import io.element.android.tests.testutils.testCoroutineDispatchers
4140
import kotlinx.coroutines.test.TestScope
4241

43-
internal fun TestScope.aTimelineItemsFactoryCreator(
44-
timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(),
45-
): TimelineItemsFactory.Creator {
42+
internal fun TestScope.aTimelineItemsFactoryCreator(): TimelineItemsFactory.Creator {
4643
return object : TimelineItemsFactory.Creator {
4744
override fun create(config: TimelineItemsFactoryConfig): TimelineItemsFactory {
48-
return aTimelineItemsFactory(config, timelineItemIndexer)
45+
return aTimelineItemsFactory(config)
4946
}
5047
}
5148
}
5249

5350
internal fun TestScope.aTimelineItemsFactory(
5451
config: TimelineItemsFactoryConfig,
55-
timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(),
5652
): TimelineItemsFactory {
5753
val timelineEventFormatter = aTimelineEventFormatter()
5854
val matrixClient = FakeMatrixClient()
@@ -96,7 +92,6 @@ internal fun TestScope.aTimelineItemsFactory(
9692
),
9793
),
9894
timelineItemGrouper = TimelineItemGrouper(),
99-
timelineItemIndexer = timelineItemIndexer,
10095
config = config
10196
)
10297
}

features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,7 @@ import kotlin.time.Duration.Companion.seconds
671671
timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(),
672672
): TimelinePresenter {
673673
return TimelinePresenter(
674-
timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(timelineItemIndexer),
674+
timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(),
675675
room = room,
676676
dispatchers = testCoroutineDispatchers(),
677677
appScope = this,

0 commit comments

Comments
 (0)