Avoid chat position jumps during message loading

Sometimes, the chat list would jump without the user scrolling:
- During intial loading of a room content, i.e. when it is expected
  that the list stays scrolled to bottom
- During loading of messages after jumping to a linked message

With this commit, the target event is repeatedly scrolled to upon list
changes until the users scroll themselves, to avoid above scenarios.

Change-Id: Iabbe76832e7e68686431b0baed9356c88eb50901
This commit is contained in:
SpiritCroc 2021-07-02 13:08:50 +02:00
parent 4e9ab0c6f9
commit b808d8b464
4 changed files with 43 additions and 0 deletions

View file

@ -56,6 +56,11 @@ interface Timeline {
*/
fun restartWithEventId(eventId: String?)
/**
* Event that should be displayed first, before the user scrolls.
*/
fun getInitialEventId(): String?
/**
* Check if the timeline can be enriched by paginating.
* @param direction the direction to check in

View file

@ -221,6 +221,10 @@ internal class DefaultTimeline(
postSnapshot()
}
override fun getInitialEventId(): String? {
return initialEventId
}
override fun getTimelineEventAtIndex(index: Int): TimelineEvent? {
return builtEvents.getOrNull(index)
}

View file

@ -740,6 +740,8 @@ class RoomDetailFragment @Inject constructor(
}
private fun navigateToEvent(action: RoomDetailViewEvents.NavigateToEvent) {
scrollOnNewMessageCallback.initialForceScroll = true
scrollOnNewMessageCallback.initialForceScrollEventId = action.eventId
val scrollPosition = timelineEventController.searchPositionOfEvent(action.eventId)
if (scrollPosition == null) {
scrollOnHighlightedEventCallback.scheduleScrollTo(action.eventId)
@ -1067,6 +1069,8 @@ class RoomDetailFragment @Inject constructor(
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController)
// Force scroll until the user has scrolled to address the bug where the list would jump during initial loading
scrollOnNewMessageCallback.initialForceScroll = true
scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(views.timelineRecyclerView, layoutManager, timelineEventController)
views.timelineRecyclerView.layoutManager = layoutManager
views.timelineRecyclerView.itemAnimator = null
@ -1080,6 +1084,15 @@ class RoomDetailFragment @Inject constructor(
}
timelineEventController.addModelBuildListener(modelBuildListener)
views.timelineRecyclerView.adapter = timelineEventController.adapter
views.timelineRecyclerView.addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy != 0) {
// User has scrolled, stop force scrolling
scrollOnNewMessageCallback.initialForceScroll = false
}
super.onScrolled(recyclerView, dx, dy)
}
})
if (vectorPreferences.swipeToReplyIsEnabled()) {
val quickReplyHandler = object : RoomMessageTouchHelperCallback.QuickReplayHandler {

View file

@ -28,8 +28,23 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
private val newTimelineEventIds = CopyOnWriteArrayList<String>()
private var forceScroll = false
var initialForceScroll = false
var initialForceScrollEventId: String? = null
get() = field ?: timelineEventController.timeline?.getInitialEventId()
fun addNewTimelineEventIds(eventIds: List<String>) {
// Disable initial force scroll
initialForceScroll = false
// Update force scroll id when sticking to the bottom - TODO try this if staying at bottom is not reliable as well
/*
if (eventIds.isNotEmpty()) {
initialForceScrollEventId.let {
if (it != null && it == timelineEventController.timeline?.getInitialEventId()) {
initialForceScrollEventId = eventIds[0]
}
}
}
*/
newTimelineEventIds.addAll(0, eventIds)
}
@ -38,6 +53,12 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
}
override fun onInserted(position: Int, count: Int) {
if (initialForceScroll) {
timelineEventController.searchPositionOfEvent(initialForceScrollEventId)?.let {
layoutManager.scrollToPosition(it)
}
return
}
if (position != 0) {
return
}