diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 3594a33736..1ffa7af682 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -373,6 +373,11 @@ class ConversationFragment : adapter.unregisterAdapterDataObserver(it) } + scrollListener?.let { + _binding.conversationItemRecycler.removeOnScrollListener(it) + } + scrollListener = null + _binding.conversationItemRecycler.adapter = null textDraftSaveDebouncer.clear() @@ -473,6 +478,7 @@ class ConversationFragment : private var composeTextEventsListener: ComposeTextEventsListener? = null private var dataObserver: DataObserver? = null private var menuProvider: ConversationOptionsMenu.Provider? = null + private var scrollListener: ScrollListener? = null private val jumpAndPulseScrollStrategy = object : ScrollToPositionDelegate.ScrollStrategy { override fun performScroll(recyclerView: RecyclerView, layoutManager: LinearLayoutManager, position: Int, smooth: Boolean) { @@ -830,10 +836,6 @@ class ConversationFragment : .distinctUntilChanged { r1, r2 -> r1 === r2 || r1.hasSameContent(r2) } .subscribeBy(onNext = this::onRecipientChanged) - disposables += viewModel.markReadRequests - .observeOn(AndroidSchedulers.mainThread()) - .subscribeBy(onNext = markReadHelper::onViewsRevealed) - disposables += viewModel.scrollButtonState .subscribeBy(onNext = this::presentScrollButtons) @@ -1448,7 +1450,8 @@ class ConversationFragment : layoutManager = ConversationLayoutManager(requireContext()) binding.conversationItemRecycler.setHasFixedSize(false) binding.conversationItemRecycler.layoutManager = layoutManager - binding.conversationItemRecycler.addOnScrollListener(ScrollListener()) + scrollListener = ScrollListener() + binding.conversationItemRecycler.addOnScrollListener(scrollListener!!) adapter = ConversationAdapterV2( lifecycleOwner = viewLifecycleOwner, @@ -2248,6 +2251,10 @@ class ConversationFragment : return layoutManager.findFirstCompletelyVisibleItemPosition() > 4 } + private fun shouldScrollToBottom(): Boolean { + return isScrolledToBottom() || layoutManager.findFirstVisibleItemPosition() <= 0 + } + /** * Controls animation and visibility of the scrollDateHeader. */ @@ -2308,9 +2315,11 @@ class ConversationFragment : override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { if (isScrolledToBottom()) { - viewModel.setShowScrollButtons(false) + viewModel.setShowScrollButtonsForScrollPosition(showScrollButtons = false, willScrollToBottomOnNewMessage = true) } else if (isScrolledPastButtonThreshold()) { - viewModel.setShowScrollButtons(true) + viewModel.setShowScrollButtonsForScrollPosition(showScrollButtons = true, willScrollToBottomOnNewMessage = false) + } else { + viewModel.setShowScrollButtonsForScrollPosition(showScrollButtons = false, willScrollToBottomOnNewMessage = shouldScrollToBottom()) } presentComposeDivider() @@ -2344,10 +2353,9 @@ class ConversationFragment : private inner class DataObserver : RecyclerView.AdapterDataObserver() { override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { - Log.d(TAG, "onItemRangeInserted $positionStart $itemCount") - if (positionStart == 0 && itemCount == 1 && !binding.conversationItemRecycler.canScrollVertically(1)) { - Log.d(TAG, "Requesting scroll to bottom.") + if (positionStart == 0 && itemCount == 1 && shouldScrollToBottom()) { layoutManager.scrollToPositionWithOffset(0, 0) + scrollListener?.onScrolled(binding.conversationItemRecycler, 0, 0) } } @@ -2369,6 +2377,10 @@ class ConversationFragment : actionMode?.setTitle(calculateSelectedItemCount()) } } + + override fun onItemRangeChanged(positionStart: Int, itemCount: Int) { + scrollListener?.onScrolled(binding.conversationItemRecycler, 0, 0) + } } //endregion Scroll Handling @@ -2820,10 +2832,7 @@ class ConversationFragment : ViewUtil.hideKeyboard(requireContext(), itemView) - val showScrollButtons = viewModel.showScrollButtonsSnapshot - if (showScrollButtons) { - viewModel.setShowScrollButtons(false) - } + viewModel.setHideScrollButtonsForReactionOverlay(true) val targetViews: InteractiveConversationElement = target handleReaction( @@ -2861,9 +2870,7 @@ class ConversationFragment : ViewUtil.fadeIn(targetViews.quotedIndicatorView!!, 150) } - if (showScrollButtons) { - viewModel.setShowScrollButtons(true) - } + viewModel.setHideScrollButtonsForReactionOverlay(false) } } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt index ed290f5a52..b9a00bb494 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt @@ -288,15 +288,14 @@ class ConversationRepository( } fun getMessageCounts(threadId: Long): Flowable { - return RxDatabaseObserver.conversationList + return RxDatabaseObserver.conversation(threadId) .map { getUnreadCount(threadId) } .distinctUntilChanged() .map { MessageCounts(it, getUnreadMentionsCount(threadId)) } } private fun getUnreadCount(threadId: Long): Int { - val threadRecord = SignalDatabase.threads.getThreadRecord(threadId) - return threadRecord?.unreadCount ?: 0 + return SignalDatabase.messages.getUnreadCount(threadId) } private fun getUnreadMentionsCount(threadId: Long): Int { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationScrollButtonState.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationScrollButtonState.kt index d09f57b63a..c6ecab30a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationScrollButtonState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationScrollButtonState.kt @@ -1,7 +1,20 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + package org.thoughtcrime.securesms.conversation.v2 +/** + * State information used to display the scroll to next mention and scroll to bottom buttons. + */ data class ConversationScrollButtonState( - val showScrollButtons: Boolean = false, + val hideScrollButtonsForReactionOverlay: Boolean = false, + val showScrollButtonsForScrollPosition: Boolean = false, + val willScrollToBottomOnNewMessage: Boolean = true, val unreadCount: Int = 0, val hasMentions: Boolean = false -) +) { + val showScrollButtons: Boolean + get() = !hideScrollButtonsForReactionOverlay && (showScrollButtonsForScrollPosition || (!willScrollToBottomOnNewMessage && unreadCount > 0)) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index 7b672bd45f..66a34892c2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -20,7 +20,6 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.addTo import io.reactivex.rxjava3.kotlin.plusAssign import io.reactivex.rxjava3.kotlin.subscribeBy -import io.reactivex.rxjava3.processors.PublishProcessor import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.subjects.BehaviorSubject import io.reactivex.rxjava3.subjects.PublishSubject @@ -98,11 +97,6 @@ class ConversationViewModel( private val _conversationThreadState: Subject = BehaviorSubject.create() val conversationThreadState: Single = _conversationThreadState.firstOrError() - private val _markReadProcessor: PublishProcessor = PublishProcessor.create() - val markReadRequests: Flowable = _markReadProcessor - .onBackpressureBuffer() - .distinct() - val pagingController = ProxyPagingController() val groupMemberServiceIds: Observable> = recipientRepository @@ -249,9 +243,18 @@ class ConversationViewModel( disposables.clear() } - fun setShowScrollButtons(showScrollButtons: Boolean) { + fun setShowScrollButtonsForScrollPosition(showScrollButtons: Boolean, willScrollToBottomOnNewMessage: Boolean) { scrollButtonStateStore.update { - it.copy(showScrollButtons = showScrollButtons) + it.copy( + showScrollButtonsForScrollPosition = showScrollButtons, + willScrollToBottomOnNewMessage = willScrollToBottomOnNewMessage + ) + } + } + + fun setHideScrollButtonsForReactionOverlay(hide: Boolean) { + scrollButtonStateStore.update { + it.copy(hideScrollButtonsForReactionOverlay = hide) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RxDatabaseObserver.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RxDatabaseObserver.kt index c40c3aa1f3..65d8d517c2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RxDatabaseObserver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RxDatabaseObserver.kt @@ -22,6 +22,12 @@ object RxDatabaseObserver { } } + fun conversation(threadId: Long): Flowable { + return databaseFlowable { listener -> + ApplicationDependencies.getDatabaseObserver().registerVerboseConversationObserver(threadId, listener) + } + } + @Suppress("RedundantUnitExpression") private fun notificationProfilesFlowable(): Flowable { return Flowable.combineLatest(