diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt index 11efaafd2c..ee1bd477fb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index bb2761f612..c77edadaa0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -221,6 +221,10 @@ internal class DefaultTimeline( postSnapshot() } + override fun getInitialEventId(): String? { + return initialEventId + } + override fun getTimelineEventAtIndex(index: Int): TimelineEvent? { return builtEvents.getOrNull(index) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index ef7d1c04fd..369c901baa 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt index 249618e12f..10cb1eb4a6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt @@ -28,8 +28,23 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, private val newTimelineEventIds = CopyOnWriteArrayList() private var forceScroll = false + var initialForceScroll = false + var initialForceScrollEventId: String? = null + get() = field ?: timelineEventController.timeline?.getInitialEventId() fun addNewTimelineEventIds(eventIds: List) { + // 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 }