@@ -15,6 +15,7 @@ import android.net.Uri
1515import android.os.AsyncTask
1616import android.os.Build
1717import android.os.Bundle
18+ import android.os.Parcelable
1819import android.os.Handler
1920import android.os.Looper
2021import android.os.SystemClock
@@ -459,6 +460,9 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
459460
460461 private var isKeyboardVisible = false
461462
463+ private var pendingRecyclerViewScrollState: Parcelable ? = null
464+ private var hasRestoredRecyclerViewScrollState: Boolean = false
465+
462466 private lateinit var reactionDelegate: ConversationReactionDelegate
463467 private val reactWithAnyEmojiStartPage = - 1
464468
@@ -529,6 +533,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
529533 // Extras
530534 private const val ADDRESS = " address"
531535 private const val SCROLL_MESSAGE_ID = " scroll_message_id"
536+ private const val CONVERSATION_SCROLL_STATE = " conversation_scroll_state"
532537
533538 const val SHOW_SEARCH = " show_search"
534539
@@ -570,6 +575,9 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
570575 override fun onCreate (savedInstanceState : Bundle ? , isReady : Boolean ) {
571576 super .onCreate(savedInstanceState, isReady)
572577
578+ pendingRecyclerViewScrollState = savedInstanceState?.getParcelable(CONVERSATION_SCROLL_STATE )
579+ hasRestoredRecyclerViewScrollState = false
580+
573581 // Check if address is null before proceeding with initialization
574582 if (
575583 IntentCompat .getParcelableExtra(
@@ -924,33 +932,27 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
924932 unreadCount = data.threadUnreadCount
925933 updateUnreadCountIndicator()
926934
935+ // If we have a saved RecyclerView scroll state (after rotation), restore it and skip first-load autoscroll.
936+ if (! hasRestoredRecyclerViewScrollState && pendingRecyclerViewScrollState != null ) {
937+ val lm = binding.conversationRecyclerView.layoutManager
938+ binding.conversationRecyclerView.runWhenLaidOut {
939+ lm?.onRestoreInstanceState(pendingRecyclerViewScrollState)
940+ hasRestoredRecyclerViewScrollState = true
941+ pendingRecyclerViewScrollState = null
942+ }
943+ }
944+
927945 if (messageToScrollTo != null ) {
928946 if (gotoMessageById(messageToScrollTo!! .id, smoothScroll = messageToScrollTo!! .smoothScroll, highlight = firstLoad)){
929947 messageToScrollTo = null
930948 }
931949 } else {
932- if (firstLoad) {
933- scrollToFirstUnreadMessageOrBottom()
934-
935- // On the first load, check if there unread messages
936- if (unreadCount == 0 && adapter.itemCount > 0 ) {
937- lifecycleScope.launch(Dispatchers .Default ) {
938- val isUnread = configFactory.withUserConfigs {
939- it.convoInfoVolatile.getConversationUnread(
940- viewModel.address,
941- )
942- }
950+ val shouldAutoScrollOnFirstLoad = firstLoad &&
951+ pendingRecyclerViewScrollState == null &&
952+ ! hasRestoredRecyclerViewScrollState
943953
944- viewModel.threadId?.let { threadId ->
945- if (isUnread) {
946- storage.markConversationAsRead(
947- threadId,
948- clock.currentTimeMillis()
949- )
950- }
951- }
952- }
953- }
954+ if (shouldAutoScrollOnFirstLoad) {
955+ scrollToFirstUnreadMessageOrBottom()
954956 } else {
955957 // If there are new data updated, we'll try to stay scrolled at the bottom (if we were at the bottom).
956958 // scrolled to bottom has a leniency of 50dp, so if we are within the 50dp but not fully at the bottom, scroll down
@@ -960,6 +962,26 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
960962 }
961963 }
962964
965+ // We should do this check regardless of whether we're restoring a saved scroll position.
966+ if (firstLoad && unreadCount == 0 && adapter.itemCount > 0 ) {
967+ lifecycleScope.launch(Dispatchers .Default ) {
968+ val isUnread = configFactory.withUserConfigs {
969+ it.convoInfoVolatile.getConversationUnread(
970+ viewModel.address,
971+ )
972+ }
973+
974+ viewModel.threadId?.let { threadId ->
975+ if (isUnread) {
976+ storage.markConversationAsRead(
977+ threadId,
978+ clock.currentTimeMillis()
979+ )
980+ }
981+ }
982+ }
983+ }
984+
963985 handleRecyclerViewScrolled()
964986 }
965987 }
@@ -973,6 +995,17 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
973995 }
974996 }
975997
998+ override fun onSaveInstanceState (outState : Bundle ) {
999+ super .onSaveInstanceState(outState)
1000+
1001+ // Persist the current scroll position so rotations (e.g., portrait <-> landscape) don't jump to bottom.
1002+ val lm = binding.conversationRecyclerView.layoutManager
1003+ val state = lm?.onSaveInstanceState()
1004+ if (state != null ) {
1005+ outState.putParcelable(CONVERSATION_SCROLL_STATE , state)
1006+ }
1007+ }
1008+
9761009 override fun onLoaderReset (cursor : Loader <ConversationLoader .Data >) = adapter.changeCursor(null )
9771010
9781011 // called from onCreate
0 commit comments