mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 04:28:35 +00:00
Add CFV2 Scroll-to-position wiring.
This commit is contained in:
committed by
Cody Henthorne
parent
e32b81dc2a
commit
65e0fae3f4
@@ -52,9 +52,36 @@ class ScrollToPositionDelegate private constructor(
|
|||||||
recyclerView.doAfterNextLayout {
|
recyclerView.doAfterNextLayout {
|
||||||
handleScrollPositionRequest(position, recyclerView)
|
handleScrollPositionRequest(position, recyclerView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(recyclerView.isLayoutRequested || recyclerView.isInLayout)) {
|
||||||
|
recyclerView.requestLayout()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for requesting a specific scroll position.
|
||||||
|
*/
|
||||||
|
fun requestScrollPosition(position: Int, smooth: Boolean = true) {
|
||||||
|
scrollPositionRequested.onNext(ScrollToPositionRequest(position, smooth))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the scroll position to 0
|
||||||
|
*/
|
||||||
|
fun resetScrollPosition() {
|
||||||
|
requestScrollPosition(0, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This should be called every time a list is submitted to the RecyclerView's adapter.
|
||||||
|
*/
|
||||||
|
fun notifyListCommitted() {
|
||||||
|
listCommitted.onNext(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isListCommitted(): Boolean = listCommitted.value != null
|
||||||
|
|
||||||
private fun handleScrollPositionRequest(
|
private fun handleScrollPositionRequest(
|
||||||
request: ScrollToPositionRequest,
|
request: ScrollToPositionRequest,
|
||||||
recyclerView: RecyclerView
|
recyclerView: RecyclerView
|
||||||
@@ -87,28 +114,6 @@ class ScrollToPositionDelegate private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Entry point for requesting a specific scroll position.
|
|
||||||
*/
|
|
||||||
fun requestScrollPosition(position: Int, smooth: Boolean = true) {
|
|
||||||
scrollPositionRequested.onNext(ScrollToPositionRequest(position, smooth))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset the scroll position to 0
|
|
||||||
*/
|
|
||||||
fun resetScrollPosition() {
|
|
||||||
requestScrollPosition(0, true)
|
|
||||||
recyclerView.requestLayout()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This should be called every time a list is submitted to the RecyclerView's adapter.
|
|
||||||
*/
|
|
||||||
fun notifyListCommitted() {
|
|
||||||
listCommitted.onNext(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class ScrollToPositionRequest(
|
private data class ScrollToPositionRequest(
|
||||||
val position: Int,
|
val position: Int,
|
||||||
val smooth: Boolean
|
val smooth: Boolean
|
||||||
|
|||||||
@@ -402,6 +402,39 @@ public class ConversationAdapter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks a range around the given position for nulls.
|
||||||
|
*
|
||||||
|
* @param position The position we wish to jump to.
|
||||||
|
* @return true if we seem like we've paged in the right data, false if not so.
|
||||||
|
*/
|
||||||
|
public boolean canJumpToPosition(int position) {
|
||||||
|
position = isTypingViewEnabled() ? position - 1 : position;
|
||||||
|
if (position < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position > super.getItemCount()) {
|
||||||
|
Log.d(TAG, "Could not access corrected position " + position + " as it is out of bounds.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int start = Math.max(position - 10, 0);
|
||||||
|
int end = Math.min(position + 5, super.getItemCount());
|
||||||
|
|
||||||
|
for (int i = start; i < end; i++) {
|
||||||
|
if (super.getItem(i) == null) {
|
||||||
|
if (pagingController != null) {
|
||||||
|
pagingController.onDataNeededAroundIndex(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public void setPagingController(@Nullable PagingController pagingController) {
|
public void setPagingController(@Nullable PagingController pagingController) {
|
||||||
this.pagingController = pagingController;
|
this.pagingController = pagingController;
|
||||||
}
|
}
|
||||||
@@ -431,7 +464,7 @@ public class ConversationAdapter
|
|||||||
* an adjusted message position based on adapter state.
|
* an adjusted message position based on adapter state.
|
||||||
*/
|
*/
|
||||||
@MainThread
|
@MainThread
|
||||||
int getAdapterPositionForMessagePosition(int messagePosition) {
|
public int getAdapterPositionForMessagePosition(int messagePosition) {
|
||||||
return isTypingViewEnabled() ? messagePosition + 1 : messagePosition;
|
return isTypingViewEnabled() ? messagePosition + 1 : messagePosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,7 +613,7 @@ public class ConversationAdapter
|
|||||||
* Provided a pool, this will initialize it with view counts that make sense.
|
* Provided a pool, this will initialize it with view counts that make sense.
|
||||||
*/
|
*/
|
||||||
@MainThread
|
@MainThread
|
||||||
static void initializePool(@NonNull RecyclerView.RecycledViewPool pool) {
|
public static void initializePool(@NonNull RecyclerView.RecycledViewPool pool) {
|
||||||
pool.setMaxRecycledViews(MESSAGE_TYPE_INCOMING_TEXT, 25);
|
pool.setMaxRecycledViews(MESSAGE_TYPE_INCOMING_TEXT, 25);
|
||||||
pool.setMaxRecycledViews(MESSAGE_TYPE_INCOMING_MULTIMEDIA, 15);
|
pool.setMaxRecycledViews(MESSAGE_TYPE_INCOMING_MULTIMEDIA, 15);
|
||||||
pool.setMaxRecycledViews(MESSAGE_TYPE_OUTGOING_TEXT, 25);
|
pool.setMaxRecycledViews(MESSAGE_TYPE_OUTGOING_TEXT, 25);
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ data class ConversationData(
|
|||||||
return lastSeenPosition > 0
|
return lastSeenPosition > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getStartPosition(): Int {
|
||||||
|
return when {
|
||||||
|
shouldJumpToMessage() -> jumpToPosition
|
||||||
|
messageRequestData.isMessageRequestAccepted && shouldScrollToLastSeen() -> lastSeenPosition
|
||||||
|
messageRequestData.isMessageRequestAccepted -> lastScrolledPosition
|
||||||
|
else -> threadSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class MessageRequestData @JvmOverloads constructor(
|
data class MessageRequestData @JvmOverloads constructor(
|
||||||
val isMessageRequestAccepted: Boolean,
|
val isMessageRequestAccepted: Boolean,
|
||||||
val isHidden: Boolean,
|
val isHidden: Boolean,
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ import org.thoughtcrime.securesms.conversation.quotes.MessageQuotesBottomSheet;
|
|||||||
import org.thoughtcrime.securesms.conversation.ui.edit.EditMessageHistoryDialog;
|
import org.thoughtcrime.securesms.conversation.ui.edit.EditMessageHistoryDialog;
|
||||||
import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSettingsDialog;
|
import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSettingsDialog;
|
||||||
import org.thoughtcrime.securesms.conversation.v2.AddToContactsContract;
|
import org.thoughtcrime.securesms.conversation.v2.AddToContactsContract;
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.BubbleLayoutTransitionListener;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseObserver;
|
import org.thoughtcrime.securesms.database.DatabaseObserver;
|
||||||
import org.thoughtcrime.securesms.database.MessageTable;
|
import org.thoughtcrime.securesms.database.MessageTable;
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
@@ -258,8 +259,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||||||
private OnScrollListener conversationScrollListener;
|
private OnScrollListener conversationScrollListener;
|
||||||
private int lastSeenScrollOffset;
|
private int lastSeenScrollOffset;
|
||||||
private Stopwatch startupStopwatch;
|
private Stopwatch startupStopwatch;
|
||||||
private LayoutTransition layoutTransition;
|
|
||||||
private TransitionListener transitionListener;
|
|
||||||
private View reactionsShade;
|
private View reactionsShade;
|
||||||
private SignalBottomActionBar bottomActionBar;
|
private SignalBottomActionBar bottomActionBar;
|
||||||
private OpenableGiftItemDecoration openableGiftItemDecoration;
|
private OpenableGiftItemDecoration openableGiftItemDecoration;
|
||||||
@@ -309,8 +308,8 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||||||
list = view.findViewById(android.R.id.list);
|
list = view.findViewById(android.R.id.list);
|
||||||
composeDivider = view.findViewById(R.id.compose_divider);
|
composeDivider = view.findViewById(R.id.compose_divider);
|
||||||
|
|
||||||
layoutTransition = new LayoutTransition();
|
BubbleLayoutTransitionListener bubbleLayoutTransitionListener = new BubbleLayoutTransitionListener(list);
|
||||||
transitionListener = new TransitionListener(list);
|
getViewLifecycleOwner().getLifecycle().addObserver(bubbleLayoutTransitionListener);
|
||||||
|
|
||||||
scrollToBottomButton = view.findViewById(R.id.scroll_to_bottom);
|
scrollToBottomButton = view.findViewById(R.id.scroll_to_bottom);
|
||||||
scrollToMentionButton = view.findViewById(R.id.scroll_to_mention);
|
scrollToMentionButton = view.findViewById(R.id.scroll_to_mention);
|
||||||
@@ -514,7 +513,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||||||
super.onStart();
|
super.onStart();
|
||||||
initializeTypingObserver();
|
initializeTypingObserver();
|
||||||
SignalProxyUtil.startListeningToWebsocket();
|
SignalProxyUtil.startListeningToWebsocket();
|
||||||
layoutTransition.getAnimator(LayoutTransition.CHANGE_DISAPPEARING).addListener(transitionListener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -538,7 +536,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||||||
public void onStop() {
|
public void onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
ApplicationDependencies.getTypingStatusRepository().getTypists(threadId).removeObservers(getViewLifecycleOwner());
|
ApplicationDependencies.getTypingStatusRepository().getTypists(threadId).removeObservers(getViewLifecycleOwner());
|
||||||
layoutTransition.getAnimator(LayoutTransition.CHANGE_DISAPPEARING).removeListener(transitionListener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -2396,34 +2393,4 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class TransitionListener implements Animator.AnimatorListener {
|
|
||||||
|
|
||||||
private final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
|
|
||||||
|
|
||||||
TransitionListener(RecyclerView recyclerView) {
|
|
||||||
animator.addUpdateListener(unused -> recyclerView.invalidate());
|
|
||||||
animator.setDuration(100L);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationStart(Animator animation) {
|
|
||||||
animator.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animator animation) {
|
|
||||||
animator.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationCancel(Animator animation) {
|
|
||||||
// Do Nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationRepeat(Animator animation) {
|
|
||||||
// Do Nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.v2
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.LayoutTransition
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
class BubbleLayoutTransitionListener(
|
||||||
|
recyclerView: RecyclerView
|
||||||
|
) : DefaultLifecycleObserver {
|
||||||
|
|
||||||
|
private val layoutTransition = LayoutTransition()
|
||||||
|
private val transitionListener = TransitionListener(recyclerView)
|
||||||
|
|
||||||
|
override fun onStart(owner: LifecycleOwner) {
|
||||||
|
super.onStart(owner)
|
||||||
|
layoutTransition.getAnimator(LayoutTransition.CHANGE_DISAPPEARING).addListener(transitionListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
|
super.onStop(owner)
|
||||||
|
layoutTransition.getAnimator(LayoutTransition.CHANGE_DISAPPEARING).removeListener(transitionListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TransitionListener(recyclerView: RecyclerView) : Animator.AnimatorListener {
|
||||||
|
private val animator = ValueAnimator.ofFloat(0f, 1f)
|
||||||
|
|
||||||
|
init {
|
||||||
|
animator.addUpdateListener { recyclerView.invalidate() }
|
||||||
|
animator.duration = 100L
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationStart(animation: Animator) {
|
||||||
|
animator.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
|
animator.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationCancel(animation: Animator) = Unit
|
||||||
|
|
||||||
|
override fun onAnimationRepeat(animation: Animator) = Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.MainActivity
|
|||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.badges.gifts.viewgift.received.ViewReceivedGiftBottomSheet
|
import org.thoughtcrime.securesms.badges.gifts.viewgift.received.ViewReceivedGiftBottomSheet
|
||||||
import org.thoughtcrime.securesms.badges.gifts.viewgift.sent.ViewSentGiftBottomSheet
|
import org.thoughtcrime.securesms.badges.gifts.viewgift.sent.ViewSentGiftBottomSheet
|
||||||
|
import org.thoughtcrime.securesms.components.ScrollToPositionDelegate
|
||||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||||
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager
|
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager
|
||||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
|
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
|
||||||
@@ -48,6 +49,7 @@ import org.thoughtcrime.securesms.conversation.ConversationOptionsMenu
|
|||||||
import org.thoughtcrime.securesms.conversation.MarkReadHelper
|
import org.thoughtcrime.securesms.conversation.MarkReadHelper
|
||||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer
|
import org.thoughtcrime.securesms.conversation.colors.Colorizer
|
||||||
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
|
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
|
||||||
|
import org.thoughtcrime.securesms.conversation.mutiselect.ConversationItemAnimator
|
||||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectItemDecoration
|
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectItemDecoration
|
||||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
||||||
import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupCallViewModel
|
import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupCallViewModel
|
||||||
@@ -56,6 +58,7 @@ import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord
|
|||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.databinding.V2ConversationFragmentBinding
|
import org.thoughtcrime.securesms.databinding.V2ConversationFragmentBinding
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ItemDecoration
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ItemDecoration
|
||||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackController
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackController
|
||||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy
|
||||||
@@ -89,6 +92,7 @@ import org.thoughtcrime.securesms.util.ContextUtil
|
|||||||
import org.thoughtcrime.securesms.util.DrawableUtil
|
import org.thoughtcrime.securesms.util.DrawableUtil
|
||||||
import org.thoughtcrime.securesms.util.FullscreenHelper
|
import org.thoughtcrime.securesms.util.FullscreenHelper
|
||||||
import org.thoughtcrime.securesms.util.WindowUtil
|
import org.thoughtcrime.securesms.util.WindowUtil
|
||||||
|
import org.thoughtcrime.securesms.util.doAfterNextLayout
|
||||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||||
import org.thoughtcrime.securesms.util.hasGiftBadge
|
import org.thoughtcrime.securesms.util.hasGiftBadge
|
||||||
import org.thoughtcrime.securesms.util.visible
|
import org.thoughtcrime.securesms.util.visible
|
||||||
@@ -145,8 +149,12 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
|||||||
FullscreenHelper(requireActivity()).showSystemUI()
|
FullscreenHelper(requireActivity()).showSystemUI()
|
||||||
|
|
||||||
layoutManager = SmoothScrollingLinearLayoutManager(requireContext(), true)
|
layoutManager = SmoothScrollingLinearLayoutManager(requireContext(), true)
|
||||||
|
binding.conversationItemRecycler.setHasFixedSize(false)
|
||||||
binding.conversationItemRecycler.layoutManager = layoutManager
|
binding.conversationItemRecycler.layoutManager = layoutManager
|
||||||
|
|
||||||
|
val layoutTransitionListener = BubbleLayoutTransitionListener(binding.conversationItemRecycler)
|
||||||
|
viewLifecycleOwner.lifecycle.addObserver(layoutTransitionListener)
|
||||||
|
|
||||||
val recyclerViewColorizer = RecyclerViewColorizer(binding.conversationItemRecycler)
|
val recyclerViewColorizer = RecyclerViewColorizer(binding.conversationItemRecycler)
|
||||||
recyclerViewColorizer.setChatColors(args.chatColors)
|
recyclerViewColorizer.setChatColors(args.chatColors)
|
||||||
|
|
||||||
@@ -174,6 +182,10 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
|||||||
presentConversationTitle(it)
|
presentConversationTitle(it)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
disposables += viewModel.markReadRequests
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribeBy(onNext = markReadHelper::onViewsRevealed)
|
||||||
|
|
||||||
EventBus.getDefault().registerForLifecycle(groupCallViewModel, viewLifecycleOwner)
|
EventBus.getDefault().registerForLifecycle(groupCallViewModel, viewLifecycleOwner)
|
||||||
presentGroupCallJoinButton()
|
presentGroupCallJoinButton()
|
||||||
}
|
}
|
||||||
@@ -184,6 +196,15 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
|||||||
WindowUtil.setLightNavigationBarFromTheme(requireActivity())
|
WindowUtil.setLightNavigationBarFromTheme(requireActivity())
|
||||||
WindowUtil.setLightStatusBarFromTheme(requireActivity())
|
WindowUtil.setLightStatusBarFromTheme(requireActivity())
|
||||||
groupCallViewModel.peekGroupCall()
|
groupCallViewModel.peekGroupCall()
|
||||||
|
|
||||||
|
if (!args.conversationScreenType.isInBubble) {
|
||||||
|
ApplicationDependencies.getMessageNotifier().setVisibleThread(ConversationId.forConversation(args.threadId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
ApplicationDependencies.getMessageNotifier().clearVisibleThread()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun registerForResults() {
|
private fun registerForResults() {
|
||||||
@@ -204,23 +225,50 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
|||||||
colorizer
|
colorizer
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val scrollToPositionDelegate = ScrollToPositionDelegate(
|
||||||
|
binding.conversationItemRecycler,
|
||||||
|
adapter::canJumpToPosition,
|
||||||
|
adapter::getAdapterPositionForMessagePosition
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.conversationItemRecycler.itemAnimator = ConversationItemAnimator(
|
||||||
|
isInMultiSelectMode = adapter.selectedItems::isNotEmpty,
|
||||||
|
shouldPlayMessageAnimations = {
|
||||||
|
scrollToPositionDelegate.isListCommitted() && binding.conversationItemRecycler.scrollState == RecyclerView.SCROLL_STATE_IDLE
|
||||||
|
},
|
||||||
|
isParentFilled = {
|
||||||
|
binding.conversationItemRecycler.canScrollVertically(1) || binding.conversationItemRecycler.canScrollVertically(-1)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ConversationAdapter.initializePool(binding.conversationItemRecycler.recycledViewPool)
|
||||||
adapter.setPagingController(viewModel.pagingController)
|
adapter.setPagingController(viewModel.pagingController)
|
||||||
|
adapter.registerAdapterDataObserver(DataObserver(scrollToPositionDelegate))
|
||||||
viewLifecycleOwner.lifecycle.addObserver(LastSeenPositionUpdater(adapter, layoutManager, viewModel))
|
viewLifecycleOwner.lifecycle.addObserver(LastSeenPositionUpdater(adapter, layoutManager, viewModel))
|
||||||
binding.conversationItemRecycler.adapter = adapter
|
binding.conversationItemRecycler.adapter = adapter
|
||||||
giphyMp4ProjectionRecycler = initializeGiphyMp4()
|
giphyMp4ProjectionRecycler = initializeGiphyMp4()
|
||||||
|
|
||||||
binding.conversationItemRecycler.addItemDecoration(
|
val multiselectItemDecoration = MultiselectItemDecoration(
|
||||||
MultiselectItemDecoration(
|
requireContext()
|
||||||
requireContext()
|
) { viewModel.wallpaperSnapshot }
|
||||||
) { viewModel.wallpaperSnapshot }
|
|
||||||
)
|
binding.conversationItemRecycler.addItemDecoration(multiselectItemDecoration)
|
||||||
|
viewLifecycleOwner.lifecycle.addObserver(multiselectItemDecoration)
|
||||||
|
|
||||||
|
disposables += viewModel.conversationThreadState.subscribeBy {
|
||||||
|
scrollToPositionDelegate.requestScrollPosition(it.meta.getStartPosition(), false)
|
||||||
|
}
|
||||||
|
|
||||||
disposables += viewModel
|
disposables += viewModel
|
||||||
.conversationThreadState
|
.conversationThreadState
|
||||||
.flatMap { it.items.data }
|
.flatMapObservable { it.items.data }
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeBy(onNext = {
|
.subscribeBy(onNext = {
|
||||||
adapter.submitList(it)
|
adapter.submitList(it) {
|
||||||
|
binding.conversationItemRecycler.doAfterNextLayout {
|
||||||
|
scrollToPositionDelegate.notifyListCommitted()
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
disposables += viewModel
|
disposables += viewModel
|
||||||
@@ -231,6 +279,13 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
|||||||
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
binding.conversationItemRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
val timestamp = MarkReadHelper.getLatestTimestamp(adapter, layoutManager)
|
||||||
|
timestamp.ifPresent(viewModel::requestMarkRead)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
presentActionBarMenu()
|
presentActionBarMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,6 +408,18 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
|||||||
return callback
|
return callback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inner class DataObserver(
|
||||||
|
private val scrollToPositionDelegate: ScrollToPositionDelegate
|
||||||
|
) : 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.")
|
||||||
|
scrollToPositionDelegate.resetScrollPosition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private inner class ConversationItemClickListener : ConversationAdapter.ItemClickListener {
|
private inner class ConversationItemClickListener : ConversationAdapter.ItemClickListener {
|
||||||
override fun onQuoteClicked(messageRecord: MmsMessageRecord?) {
|
override fun onQuoteClicked(messageRecord: MmsMessageRecord?) {
|
||||||
// TODO [alex] - ("Not yet implemented")
|
// TODO [alex] - ("Not yet implemented")
|
||||||
|
|||||||
@@ -11,10 +11,8 @@ import org.signal.paging.PagingConfig
|
|||||||
import org.thoughtcrime.securesms.conversation.ConversationDataSource
|
import org.thoughtcrime.securesms.conversation.ConversationDataSource
|
||||||
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
|
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
|
||||||
import org.thoughtcrime.securesms.conversation.colors.NameColor
|
import org.thoughtcrime.securesms.conversation.colors.NameColor
|
||||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.threads
|
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.threads
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
@@ -50,16 +48,10 @@ class ConversationRepository(context: Context) {
|
|||||||
* Loads the details necessary to display the conversation thread.
|
* Loads the details necessary to display the conversation thread.
|
||||||
*/
|
*/
|
||||||
fun getConversationThreadState(threadId: Long, requestedStartPosition: Int): Single<ConversationThreadState> {
|
fun getConversationThreadState(threadId: Long, requestedStartPosition: Int): Single<ConversationThreadState> {
|
||||||
return Single.create { emitter ->
|
return Single.fromCallable {
|
||||||
val recipient = SignalDatabase.threads.getRecipientForThreadId(threadId)!!
|
val recipient = threads.getRecipientForThreadId(threadId)!!
|
||||||
val metadata = oldConversationRepository.getConversationData(threadId, recipient, requestedStartPosition)
|
val metadata = oldConversationRepository.getConversationData(threadId, recipient, requestedStartPosition)
|
||||||
val messageRequestData = metadata.messageRequestData
|
val messageRequestData = metadata.messageRequestData
|
||||||
val startPosition = when {
|
|
||||||
metadata.shouldJumpToMessage() -> metadata.jumpToPosition
|
|
||||||
messageRequestData.isMessageRequestAccepted && metadata.shouldScrollToLastSeen() -> metadata.lastSeenPosition
|
|
||||||
messageRequestData.isMessageRequestAccepted -> metadata.lastScrolledPosition
|
|
||||||
else -> metadata.threadSize
|
|
||||||
}
|
|
||||||
val dataSource = ConversationDataSource(
|
val dataSource = ConversationDataSource(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
threadId,
|
threadId,
|
||||||
@@ -69,36 +61,13 @@ class ConversationRepository(context: Context) {
|
|||||||
)
|
)
|
||||||
val config = PagingConfig.Builder().setPageSize(25)
|
val config = PagingConfig.Builder().setPageSize(25)
|
||||||
.setBufferPages(2)
|
.setBufferPages(2)
|
||||||
.setStartIndex(max(startPosition, 0))
|
.setStartIndex(max(metadata.getStartPosition(), 0))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val threadState = ConversationThreadState(
|
ConversationThreadState(
|
||||||
items = PagedData.createForObservable(dataSource, config),
|
items = PagedData.createForObservable(dataSource, config),
|
||||||
meta = metadata
|
meta = metadata
|
||||||
)
|
)
|
||||||
|
|
||||||
val controller = threadState.items.controller
|
|
||||||
val messageUpdateObserver = DatabaseObserver.MessageObserver {
|
|
||||||
controller.onDataItemChanged(it)
|
|
||||||
}
|
|
||||||
val messageInsertObserver = DatabaseObserver.MessageObserver {
|
|
||||||
controller.onDataItemInserted(it, 0)
|
|
||||||
}
|
|
||||||
val conversationObserver = DatabaseObserver.Observer {
|
|
||||||
controller.onDataInvalidated()
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(messageUpdateObserver)
|
|
||||||
ApplicationDependencies.getDatabaseObserver().registerMessageInsertObserver(threadId, messageInsertObserver)
|
|
||||||
ApplicationDependencies.getDatabaseObserver().registerConversationObserver(threadId, conversationObserver)
|
|
||||||
|
|
||||||
emitter.setCancellable {
|
|
||||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageUpdateObserver)
|
|
||||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver)
|
|
||||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver)
|
|
||||||
}
|
|
||||||
|
|
||||||
emitter.onSuccess(threadState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,23 @@ package org.thoughtcrime.securesms.conversation.v2
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
|
import io.reactivex.rxjava3.processors.PublishProcessor
|
||||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||||
import io.reactivex.rxjava3.subjects.Subject
|
import io.reactivex.rxjava3.subjects.Subject
|
||||||
import org.signal.paging.ProxyPagingController
|
import org.signal.paging.ProxyPagingController
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationIntents.Args
|
import org.thoughtcrime.securesms.conversation.ConversationIntents.Args
|
||||||
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
|
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
|
||||||
import org.thoughtcrime.securesms.conversation.colors.NameColor
|
import org.thoughtcrime.securesms.conversation.colors.NameColor
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||||
import org.thoughtcrime.securesms.database.model.MessageId
|
import org.thoughtcrime.securesms.database.model.MessageId
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.util.hasGiftBadge
|
import org.thoughtcrime.securesms.util.hasGiftBadge
|
||||||
@@ -35,7 +40,12 @@ class ConversationViewModel(
|
|||||||
val recipient: Observable<Recipient> = _recipient
|
val recipient: Observable<Recipient> = _recipient
|
||||||
|
|
||||||
private val _conversationThreadState: Subject<ConversationThreadState> = BehaviorSubject.create()
|
private val _conversationThreadState: Subject<ConversationThreadState> = BehaviorSubject.create()
|
||||||
val conversationThreadState: Observable<ConversationThreadState> = _conversationThreadState
|
val conversationThreadState: Single<ConversationThreadState> = _conversationThreadState.firstOrError()
|
||||||
|
|
||||||
|
private val _markReadProcessor: PublishProcessor<Long> = PublishProcessor.create()
|
||||||
|
val markReadRequests: Flowable<Long> = _markReadProcessor
|
||||||
|
.onBackpressureBuffer()
|
||||||
|
.distinct()
|
||||||
|
|
||||||
val pagingController = ProxyPagingController<MessageId>()
|
val pagingController = ProxyPagingController<MessageId>()
|
||||||
|
|
||||||
@@ -56,6 +66,31 @@ class ConversationViewModel(
|
|||||||
pagingController.set(it.items.controller)
|
pagingController.set(it.items.controller)
|
||||||
_conversationThreadState.onNext(it)
|
_conversationThreadState.onNext(it)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
disposables += _conversationThreadState.firstOrError().flatMapObservable { threadState ->
|
||||||
|
Observable.create<Unit> { emitter ->
|
||||||
|
val controller = threadState.items.controller
|
||||||
|
val messageUpdateObserver = DatabaseObserver.MessageObserver {
|
||||||
|
controller.onDataItemChanged(it)
|
||||||
|
}
|
||||||
|
val messageInsertObserver = DatabaseObserver.MessageObserver {
|
||||||
|
controller.onDataItemInserted(it, 0)
|
||||||
|
}
|
||||||
|
val conversationObserver = DatabaseObserver.Observer {
|
||||||
|
controller.onDataInvalidated()
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(messageUpdateObserver)
|
||||||
|
ApplicationDependencies.getDatabaseObserver().registerMessageInsertObserver(threadId, messageInsertObserver)
|
||||||
|
ApplicationDependencies.getDatabaseObserver().registerConversationObserver(threadId, conversationObserver)
|
||||||
|
|
||||||
|
emitter.setCancellable {
|
||||||
|
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageUpdateObserver)
|
||||||
|
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver)
|
||||||
|
ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
@@ -75,6 +110,9 @@ class ConversationViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun requestMarkRead(timestamp: Long) {
|
||||||
|
}
|
||||||
|
|
||||||
class Factory(
|
class Factory(
|
||||||
private val args: Args,
|
private val args: Args,
|
||||||
private val repository: ConversationRepository
|
private val repository: ConversationRepository
|
||||||
|
|||||||
@@ -37,9 +37,14 @@
|
|||||||
<org.thoughtcrime.securesms.conversation.mutiselect.MultiselectRecyclerView
|
<org.thoughtcrime.securesms.conversation.mutiselect.MultiselectRecyclerView
|
||||||
android:id="@+id/conversation_item_recycler"
|
android:id="@+id/conversation_item_recycler"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dip"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/conversation_bottom_panel_barrier"
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:overScrollMode="ifContentScrolls"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
android:splitMotionEvents="false"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/conversation_bottom_panel_barrier"
|
||||||
tools:itemCount="20"
|
tools:itemCount="20"
|
||||||
tools:listitem="@layout/conversation_item_sent_text_only" />
|
tools:listitem="@layout/conversation_item_sent_text_only" />
|
||||||
|
|
||||||
@@ -94,10 +99,10 @@
|
|||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
android:id="@+id/conversation_bottom_panel_barrier"
|
android:id="@+id/conversation_bottom_panel_barrier"
|
||||||
app:barrierDirection="top"
|
|
||||||
app:constraint_referenced_ids="conversation_input_panel"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content"
|
||||||
|
app:barrierDirection="top"
|
||||||
|
app:constraint_referenced_ids="conversation_input_panel" />
|
||||||
|
|
||||||
<include
|
<include
|
||||||
android:id="@+id/conversation_input_panel"
|
android:id="@+id/conversation_input_panel"
|
||||||
|
|||||||
Reference in New Issue
Block a user