@ -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,19 +581,47 @@ 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 { _ , _ , _ , _ , _ , _ , _ , _ , _ ->
private fun scrollToMostRecentMessageIfWeShould ( ) {
showScrollToBottomButtonIfApplicable ( )
// 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
private fun setUpToolBar ( ) {
private fun setUpToolBar ( ) {
val binding = binding ?: return
val binding = binding ?: return
@ -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 )
}
}
}
}
}