Add CFV2 Scroll-to-position wiring.

This commit is contained in:
Alex Hart
2023-04-19 09:45:19 -03:00
committed by Cody Henthorne
parent e32b81dc2a
commit 65e0fae3f4
9 changed files with 248 additions and 108 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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,

View File

@@ -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
}
}
} }

View File

@@ -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
}
}

View File

@@ -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")

View File

@@ -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)
} }
} }

View File

@@ -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

View File

@@ -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"