SES-1145 - New messages are hidden under keyboard - MK3 (#1415)

* WIP

* Working - push before cleanup

* Fixes #1316

* Cleanup

* PR review adjustments

* Fixed scrolling when receiving an image based message while keyboard is up

* Prevent auto-scroll to last seen item pos in conversation view if <= 3

* Put back <=3 check to scroll

---------

Co-authored-by: = <=>
Co-authored-by: AL-Session <160798022+AL-Session@users.noreply.github.com>
This commit is contained in:
Al Lansley 2024-03-25 11:37:43 +11:00 committed by GitHub
parent 8c2aaa06d8
commit 1f249a6d5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 91 additions and 7 deletions

View File

@ -230,11 +230,13 @@
android:name="org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2" android:name="org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:parentActivityName="org.thoughtcrime.securesms.home.HomeActivity" android:parentActivityName="org.thoughtcrime.securesms.home.HomeActivity"
android:theme="@style/Theme.Session.DayNight.NoActionBar"> android:theme="@style/Theme.Session.DayNight.NoActionBar"
android:windowSoftInputMode="adjustResize" >
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.home.HomeActivity" /> android:value="org.thoughtcrime.securesms.home.HomeActivity" />
</activity> </activity>
<activity <activity
android:name="org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity" android:name="org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"

View File

@ -175,6 +175,7 @@ import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.SaveAttachmentTask import org.thoughtcrime.securesms.util.SaveAttachmentTask
import org.thoughtcrime.securesms.util.SimpleTextWatcher import org.thoughtcrime.securesms.util.SimpleTextWatcher
import org.thoughtcrime.securesms.util.isScrolledToBottom import org.thoughtcrime.securesms.util.isScrolledToBottom
import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom
import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show import org.thoughtcrime.securesms.util.show
import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.util.toPx
@ -281,6 +282,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private val isScrolledToBottom: Boolean private val isScrolledToBottom: Boolean
get() = binding?.conversationRecyclerView?.isScrolledToBottom ?: true get() = binding?.conversationRecyclerView?.isScrolledToBottom ?: true
private val isScrolledToWithin30dpOfBottom: Boolean
get() = binding?.conversationRecyclerView?.isScrolledToWithin30dpOfBottom ?: true
private val layoutManager: LinearLayoutManager? private val layoutManager: LinearLayoutManager?
get() { return binding?.conversationRecyclerView?.layoutManager as LinearLayoutManager? } get() { return binding?.conversationRecyclerView?.layoutManager as LinearLayoutManager? }
@ -336,6 +340,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
lifecycleCoroutineScope = lifecycleScope lifecycleCoroutineScope = lifecycleScope
) )
adapter.visibleMessageViewDelegate = this adapter.visibleMessageViewDelegate = this
// Register an AdapterDataObserver to scroll us to the bottom of the RecyclerView if we're
// already near the the bottom and the data changes.
adapter.registerAdapterDataObserver(ConversationAdapterDataObserver(binding?.conversationRecyclerView!!, adapter))
adapter adapter
} }
@ -352,6 +361,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private lateinit var reactionDelegate: ConversationReactionDelegate private lateinit var reactionDelegate: ConversationReactionDelegate
private val reactWithAnyEmojiStartPage = -1 private val reactWithAnyEmojiStartPage = -1
// Properties for what message indices are visible previously & now, as well as the scroll state
private var previousLastVisibleRecyclerViewIndex: Int = RecyclerView.NO_POSITION
private var currentLastVisibleRecyclerViewIndex: Int = RecyclerView.NO_POSITION
private var recyclerScrollState: Int = RecyclerView.SCROLL_STATE_IDLE
// region Settings // region Settings
companion object { companion object {
// Extras // Extras
@ -375,6 +389,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
binding = ActivityConversationV2Binding.inflate(layoutInflater) binding = ActivityConversationV2Binding.inflate(layoutInflater)
setContentView(binding!!.root) setContentView(binding!!.root)
// messageIdToScroll // messageIdToScroll
messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1)) messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1))
messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR)) messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR))
@ -390,6 +405,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
setUpLinkPreviewObserver() setUpLinkPreviewObserver()
restoreDraftIfNeeded() restoreDraftIfNeeded()
setUpUiStateObserver() setUpUiStateObserver()
binding!!.scrollToBottomButton.setOnClickListener { binding!!.scrollToBottomButton.setOnClickListener {
val layoutManager = (binding?.conversationRecyclerView?.layoutManager as? LinearLayoutManager) ?: return@setOnClickListener val layoutManager = (binding?.conversationRecyclerView?.layoutManager as? LinearLayoutManager) ?: return@setOnClickListener
val targetPosition = if (reverseMessageList) 0 else adapter.itemCount val targetPosition = if (reverseMessageList) 0 else adapter.itemCount
@ -419,9 +435,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
setUpBlockedBanner() setUpBlockedBanner()
binding!!.searchBottomBar.setEventListener(this) binding!!.searchBottomBar.setEventListener(this)
updateSendAfterApprovalText() updateSendAfterApprovalText()
showOrHideInputIfNeeded()
setUpMessageRequestsBar() setUpMessageRequestsBar()
// Note: Do not `showOrHideInputIfNeeded` here - we'll never start this activity w/ the
// keyboard visible and have no need to immediately display it.
val weakActivity = WeakReference(this) val weakActivity = WeakReference(this)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
@ -563,17 +581,45 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
binding!!.conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { binding!!.conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (recyclerScrollState == RecyclerView.SCROLL_STATE_IDLE) {
scrollToMostRecentMessageIfWeShould()
}
handleRecyclerViewScrolled() handleRecyclerViewScrolled()
} }
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
recyclerScrollState = newState
} }
}) })
binding!!.conversationRecyclerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
showScrollToBottomButtonIfApplicable()
} }
private fun scrollToMostRecentMessageIfWeShould() {
// Grab an initial 'previous' last visible message..
if (previousLastVisibleRecyclerViewIndex == RecyclerView.NO_POSITION) {
previousLastVisibleRecyclerViewIndex = layoutManager?.findLastVisibleItemPosition()!!
}
// ..and grab the 'current' last visible message.
currentLastVisibleRecyclerViewIndex = layoutManager?.findLastVisibleItemPosition()!!
// If the current last visible message index is less than the previous one (i.e. we've
// lost visibility of one or more messages due to showing the IME keyboard) AND we're
// at the bottom of the message feed..
val atBottomAndTrueLastNoLongerVisible = currentLastVisibleRecyclerViewIndex!! <= previousLastVisibleRecyclerViewIndex!! && !binding?.scrollToBottomButton?.isVisible!!
// ..OR we're at the last message or have received a new message..
val atLastOrReceivedNewMessage = currentLastVisibleRecyclerViewIndex == (adapter.itemCount - 1)
// ..then scroll the recycler view to the last message on resize. Note: We cannot just call
// scroll/smoothScroll - we have to `post` it or nothing happens!
if (atBottomAndTrueLastNoLongerVisible || atLastOrReceivedNewMessage) {
binding?.conversationRecyclerView?.post {
binding?.conversationRecyclerView?.smoothScrollToPosition(adapter.itemCount)
}
}
// Update our previous last visible view index to the current one
previousLastVisibleRecyclerViewIndex = currentLastVisibleRecyclerViewIndex
} }
// called from onCreate // called from onCreate
@ -760,13 +806,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// of the first unread message in the middle of the screen // of the first unread message in the middle of the screen
if (isFirstLoad && !reverseMessageList) { if (isFirstLoad && !reverseMessageList) {
layoutManager?.scrollToPositionWithOffset(lastSeenItemPosition, ((layoutManager?.height ?: 0) / 2)) layoutManager?.scrollToPositionWithOffset(lastSeenItemPosition, ((layoutManager?.height ?: 0) / 2))
if (shouldHighlight) { highlightViewAtPosition(lastSeenItemPosition) } if (shouldHighlight) { highlightViewAtPosition(lastSeenItemPosition) }
return lastSeenItemPosition return lastSeenItemPosition
} }
if (lastSeenItemPosition <= 3) { return lastSeenItemPosition } if (lastSeenItemPosition <= 3) { return lastSeenItemPosition }
binding?.conversationRecyclerView?.scrollToPosition(lastSeenItemPosition) binding?.conversationRecyclerView?.scrollToPosition(lastSeenItemPosition)
return lastSeenItemPosition return lastSeenItemPosition
} }
@ -1040,8 +1085,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun handleRecyclerViewScrolled() { private fun handleRecyclerViewScrolled() {
val binding = binding ?: return val binding = binding ?: return
// Note: The typing indicate is whether the other person / other people are typing - it has
// nothing to do with the IME keyboard state.
val wasTypingIndicatorVisibleBefore = binding.typingIndicatorViewContainer.isVisible val wasTypingIndicatorVisibleBefore = binding.typingIndicatorViewContainer.isVisible
binding.typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom binding.typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom
showScrollToBottomButtonIfApplicable() showScrollToBottomButtonIfApplicable()
val maybeTargetVisiblePosition = if (reverseMessageList) layoutManager?.findFirstVisibleItemPosition() else layoutManager?.findLastVisibleItemPosition() val maybeTargetVisiblePosition = if (reverseMessageList) layoutManager?.findFirstVisibleItemPosition() else layoutManager?.findLastVisibleItemPosition()
val targetVisiblePosition = maybeTargetVisiblePosition ?: RecyclerView.NO_POSITION val targetVisiblePosition = maybeTargetVisiblePosition ?: RecyclerView.NO_POSITION
@ -2107,4 +2156,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
} }
// AdapterDataObserver implementation to scroll us to the bottom of the ConversationRecyclerView
// when we're already near the bottom and we send or receive a message.
inner class ConversationAdapterDataObserver(val recyclerView: ConversationRecyclerView, val adapter: ConversationAdapter) : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
super.onChanged()
if (recyclerView.isScrolledToWithin30dpOfBottom) {
recyclerView.scrollToPosition(adapter.itemCount-1)
}
}
}
} }

View File

@ -37,3 +37,8 @@ val RecyclerView.isScrolledToBottom: Boolean
get() = computeVerticalScrollOffset().coerceAtLeast(0) + get() = computeVerticalScrollOffset().coerceAtLeast(0) +
computeVerticalScrollExtent() + computeVerticalScrollExtent() +
toPx(50, resources) >= computeVerticalScrollRange() toPx(50, resources) >= computeVerticalScrollRange()
val RecyclerView.isScrolledToWithin30dpOfBottom: Boolean
get() = computeVerticalScrollOffset().coerceAtLeast(0) +
computeVerticalScrollExtent() +
toPx(30, resources) >= computeVerticalScrollRange()

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="100dp"
android:height="100dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M0,0 L100,100 M0,100 L100,0"
android:strokeWidth="1"
android:strokeColor="@android:color/white" />
</vector>

View File

@ -23,6 +23,10 @@
</androidx.appcompat.widget.Toolbar> </androidx.appcompat.widget.Toolbar>
<!--
Add this to the below recycler view if you need to debug activity `adjustResize` issues:
android:background="@drawable/cross"
-->
<org.thoughtcrime.securesms.conversation.v2.ConversationRecyclerView <org.thoughtcrime.securesms.conversation.v2.ConversationRecyclerView
android:focusable="false" android:focusable="false"
android:id="@+id/conversationRecyclerView" android:id="@+id/conversationRecyclerView"
@ -31,6 +35,7 @@
android:layout_above="@+id/typingIndicatorViewContainer" android:layout_above="@+id/typingIndicatorViewContainer"
android:layout_below="@id/toolbar" /> android:layout_below="@id/toolbar" />
<org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorViewContainer <org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorViewContainer
android:focusable="false" android:focusable="false"
android:id="@+id/typingIndicatorViewContainer" android:id="@+id/typingIndicatorViewContainer"