Add scroll buttons to CFV2.

This commit is contained in:
Alex Hart
2023-04-21 10:49:30 -03:00
parent bff8fc8230
commit 694d8f1984
7 changed files with 182 additions and 50 deletions

View File

@@ -3,6 +3,9 @@ package org.thoughtcrime.securesms.components;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
@@ -14,11 +17,14 @@ import androidx.annotation.Nullable;
import com.airbnb.lottie.SimpleColorFilter; import com.airbnb.lottie.SimpleColorFilter;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ViewUtil;
public final class ConversationScrollToView extends FrameLayout { public final class ConversationScrollToView extends FrameLayout {
private final TextView unreadCount; private final TextView unreadCount;
private final ImageView scrollButton; private final ImageView scrollButton;
private final Animation inAnimation;
private final Animation outAnimation;
public ConversationScrollToView(@NonNull Context context) { public ConversationScrollToView(@NonNull Context context) {
this(context, null); this(context, null);
@@ -44,6 +50,20 @@ public final class ConversationScrollToView extends FrameLayout {
array.recycle(); array.recycle();
} }
inAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_in);
outAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_out);
inAnimation.setDuration(100);
outAnimation.setDuration(50);
}
public void setShown(boolean isShown) {
if (isShown) {
ViewUtil.animateIn(this, inAnimation);
} else {
ViewUtil.animateOut(this, outAnimation, View.INVISIBLE);
}
} }
public void setWallpaperEnabled(boolean hasWallpaper) { public void setWallpaperEnabled(boolean hasWallpaper) {

View File

@@ -17,12 +17,8 @@
package org.thoughtcrime.securesms.conversation; package org.thoughtcrime.securesms.conversation;
import android.Manifest; import android.Manifest;
import android.animation.Animator;
import android.animation.LayoutTransition;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.ActivityOptions; import android.app.ActivityOptions;
import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
@@ -46,7 +42,6 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import android.widget.ViewSwitcher; import android.widget.ViewSwitcher;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -77,6 +72,7 @@ import org.jetbrains.annotations.NotNull;
import org.signal.core.util.DimensionUnit; import org.signal.core.util.DimensionUnit;
import org.signal.core.util.Stopwatch; import org.signal.core.util.Stopwatch;
import org.signal.core.util.StreamUtil; import org.signal.core.util.StreamUtil;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.concurrent.SimpleTask; import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
@@ -138,7 +134,6 @@ import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupDescription
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInfoBottomSheetDialogFragment; import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInfoBottomSheetDialogFragment;
import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult; import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult;
import org.thoughtcrime.securesms.groups.v2.GroupDescriptionUtil; import org.thoughtcrime.securesms.groups.v2.GroupDescriptionUtil;
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceViewOnceOpenJob; import org.thoughtcrime.securesms.jobs.MultiDeviceViewOnceOpenJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.linkpreview.LinkPreview;
@@ -181,9 +176,8 @@ import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.HtmlUtil; import org.thoughtcrime.securesms.util.HtmlUtil;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.MessageConstraintsUtil; import org.thoughtcrime.securesms.util.MessageConstraintsUtil;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.Projection; import org.thoughtcrime.securesms.util.Projection;
import org.thoughtcrime.securesms.util.SaveAttachmentTask; import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SignalLocalMetrics; import org.thoughtcrime.securesms.util.SignalLocalMetrics;
@@ -253,10 +247,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
private ConversationGroupViewModel groupViewModel; private ConversationGroupViewModel groupViewModel;
private SnapToTopDataObserver snapToTopDataObserver; private SnapToTopDataObserver snapToTopDataObserver;
private MarkReadHelper markReadHelper; private MarkReadHelper markReadHelper;
private Animation scrollButtonInAnimation;
private Animation mentionButtonInAnimation;
private Animation scrollButtonOutAnimation;
private Animation mentionButtonOutAnimation;
private OnScrollListener conversationScrollListener; private OnScrollListener conversationScrollListener;
private int lastSeenScrollOffset; private int lastSeenScrollOffset;
private Stopwatch startupStopwatch; private Stopwatch startupStopwatch;
@@ -404,19 +394,11 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
})); }));
conversationViewModel.getShowMentionsButton().observe(getViewLifecycleOwner(), shouldShow -> { conversationViewModel.getShowMentionsButton().observe(getViewLifecycleOwner(), shouldShow -> {
if (shouldShow) { scrollToMentionButton.setShown(shouldShow);
ViewUtil.animateIn(scrollToMentionButton, mentionButtonInAnimation);
} else {
ViewUtil.animateOut(scrollToMentionButton, mentionButtonOutAnimation, View.INVISIBLE);
}
}); });
conversationViewModel.getShowScrollToBottom().observe(getViewLifecycleOwner(), shouldShow -> { conversationViewModel.getShowScrollToBottom().observe(getViewLifecycleOwner(), shouldShow -> {
if (shouldShow) { scrollToBottomButton.setShown(shouldShow);
ViewUtil.animateIn(scrollToBottomButton, scrollButtonInAnimation);
} else {
ViewUtil.animateOut(scrollToBottomButton, scrollButtonOutAnimation, View.INVISIBLE);
}
}); });
scrollToBottomButton.setOnClickListener(v -> scrollToBottom()); scrollToBottomButton.setOnClickListener(v -> scrollToBottom());
@@ -445,7 +427,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
conversationViewModel.getActiveNotificationProfile().observe(getViewLifecycleOwner(), this::updateNotificationProfileStatus); conversationViewModel.getActiveNotificationProfile().observe(getViewLifecycleOwner(), this::updateNotificationProfileStatus);
initializeScrollButtonAnimations();
initializeResources(); initializeResources();
initializeMessageRequestViewModel(); initializeMessageRequestViewModel();
initializeListAdapter(); initializeListAdapter();
@@ -1370,20 +1351,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
} }
} }
private void initializeScrollButtonAnimations() {
scrollButtonInAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.fade_scale_in);
scrollButtonOutAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.fade_scale_out);
mentionButtonInAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.fade_scale_in);
mentionButtonOutAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.fade_scale_out);
scrollButtonInAnimation.setDuration(100);
scrollButtonOutAnimation.setDuration(50);
mentionButtonInAnimation.setDuration(100);
mentionButtonOutAnimation.setDuration(50);
}
private void scrollToNextMention() { private void scrollToNextMention() {
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> { SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
return SignalDatabase.messages().getOldestUnreadMentionDetails(threadId); return SignalDatabase.messages().getOldestUnreadMentionDetails(threadId);

View File

@@ -50,6 +50,7 @@ import org.thoughtcrime.securesms.conversation.ConversationItem
import org.thoughtcrime.securesms.conversation.ConversationMessage import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.ConversationOptionsMenu import org.thoughtcrime.securesms.conversation.ConversationOptionsMenu
import org.thoughtcrime.securesms.conversation.MarkReadHelper import org.thoughtcrime.securesms.conversation.MarkReadHelper
import org.thoughtcrime.securesms.conversation.colors.ChatColors
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.ConversationItemAnimator
@@ -139,6 +140,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
) )
private val conversationTooltips = ConversationTooltips(this) private val conversationTooltips = ConversationTooltips(this)
private lateinit var conversationOptionsMenuProvider: ConversationOptionsMenu.Provider private lateinit var conversationOptionsMenuProvider: ConversationOptionsMenu.Provider
private lateinit var layoutManager: SmoothScrollingLinearLayoutManager private lateinit var layoutManager: SmoothScrollingLinearLayoutManager
private lateinit var markReadHelper: MarkReadHelper private lateinit var markReadHelper: MarkReadHelper
@@ -146,6 +148,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
private lateinit var addToContactsLauncher: ActivityResultLauncher<Intent> private lateinit var addToContactsLauncher: ActivityResultLauncher<Intent>
private lateinit var scrollToPositionDelegate: ScrollToPositionDelegate private lateinit var scrollToPositionDelegate: ScrollToPositionDelegate
private lateinit var adapter: ConversationAdapter private lateinit var adapter: ConversationAdapter
private lateinit var recyclerViewColorizer: RecyclerViewColorizer
private val jumpAndPulseScrollStrategy = object : ScrollToPositionDelegate.ScrollStrategy { private val jumpAndPulseScrollStrategy = object : ScrollToPositionDelegate.ScrollStrategy {
override fun performScroll(recyclerView: RecyclerView, layoutManager: LinearLayoutManager, position: Int, smooth: Boolean) { override fun performScroll(recyclerView: RecyclerView, layoutManager: LinearLayoutManager, position: Int, smooth: Boolean) {
@@ -165,11 +168,20 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
layoutManager = SmoothScrollingLinearLayoutManager(requireContext(), true) layoutManager = SmoothScrollingLinearLayoutManager(requireContext(), true)
binding.conversationItemRecycler.setHasFixedSize(false) binding.conversationItemRecycler.setHasFixedSize(false)
binding.conversationItemRecycler.layoutManager = layoutManager binding.conversationItemRecycler.layoutManager = layoutManager
binding.conversationItemRecycler.addOnScrollListener(ScrollListener())
binding.scrollToBottom.setOnClickListener {
scrollToPositionDelegate.resetScrollPosition()
}
binding.scrollToMention.setOnClickListener {
scrollToNextMention()
}
val layoutTransitionListener = BubbleLayoutTransitionListener(binding.conversationItemRecycler) val layoutTransitionListener = BubbleLayoutTransitionListener(binding.conversationItemRecycler)
viewLifecycleOwner.lifecycle.addObserver(layoutTransitionListener) viewLifecycleOwner.lifecycle.addObserver(layoutTransitionListener)
val recyclerViewColorizer = RecyclerViewColorizer(binding.conversationItemRecycler) recyclerViewColorizer = RecyclerViewColorizer(binding.conversationItemRecycler)
recyclerViewColorizer.setChatColors(args.chatColors) recyclerViewColorizer.setChatColors(args.chatColors)
val conversationToolbarOnScrollHelper = ConversationToolbarOnScrollHelper( val conversationToolbarOnScrollHelper = ConversationToolbarOnScrollHelper(
@@ -190,16 +202,15 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
presentWallpaper(args.wallpaper) presentWallpaper(args.wallpaper)
disposables += viewModel.recipient disposables += viewModel.recipient
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeBy(onNext = { .subscribeBy(onNext = this::onRecipientChanged)
recyclerViewColorizer.setChatColors(it.chatColors)
presentWallpaper(it.wallpaper)
presentConversationTitle(it)
})
disposables += viewModel.markReadRequests disposables += viewModel.markReadRequests
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeBy(onNext = markReadHelper::onViewsRevealed) .subscribeBy(onNext = markReadHelper::onViewsRevealed)
disposables += viewModel.scrollButtonState
.subscribeBy(onNext = this::presentScrollButtons)
EventBus.getDefault().registerForLifecycle(groupCallViewModel, viewLifecycleOwner) EventBus.getDefault().registerForLifecycle(groupCallViewModel, viewLifecycleOwner)
presentGroupCallJoinButton() presentGroupCallJoinButton()
} }
@@ -294,16 +305,15 @@ 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()
} }
private fun onRecipientChanged(recipient: Recipient) {
presentWallpaper(recipient.wallpaper)
presentConversationTitle(recipient)
presentChatColors(recipient.chatColors)
}
private fun invalidateOptionsMenu() { private fun invalidateOptionsMenu() {
// TODO [alex] -- Handle search... is there a better way to manage this state? Maybe an event system? // TODO [alex] -- Handle search... is there a better way to manage this state? Maybe an event system?
conversationOptionsMenuProvider.onCreateMenu(binding.toolbar.menu, requireActivity().menuInflater) conversationOptionsMenuProvider.onCreateMenu(binding.toolbar.menu, requireActivity().menuInflater)
@@ -352,6 +362,21 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
} }
binding.conversationWallpaper.visible = chatWallpaper != null binding.conversationWallpaper.visible = chatWallpaper != null
binding.scrollToBottom.setWallpaperEnabled(chatWallpaper != null)
binding.scrollToMention.setWallpaperEnabled(chatWallpaper != null)
}
private fun presentChatColors(chatColors: ChatColors) {
recyclerViewColorizer.setChatColors(chatColors)
binding.scrollToMention.setUnreadCountBackgroundTint(chatColors.asSingleColor())
binding.scrollToBottom.setUnreadCountBackgroundTint(chatColors.asSingleColor())
}
private fun presentScrollButtons(scrollButtonState: ConversationScrollButtonState) {
Log.d(TAG, "Update scroll state $scrollButtonState")
binding.scrollToBottom.setUnreadCount(scrollButtonState.unreadCount)
binding.scrollToMention.isShown = scrollButtonState.hasMentions && scrollButtonState.showScrollButtons
binding.scrollToBottom.isShown = scrollButtonState.showScrollButtons
} }
private fun presentGroupCallJoinButton() { private fun presentGroupCallJoinButton() {
@@ -444,6 +469,33 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
) )
} }
private fun scrollToNextMention() {
disposables += viewModel.getNextMentionPosition().subscribeBy {
moveToPosition(it)
}
}
private fun isScrolledToBottom(): Boolean {
return !binding.conversationItemRecycler.canScrollVertically(1)
}
private fun isScrolledPastButtonThreshold(): Boolean {
return layoutManager.findFirstCompletelyVisibleItemPosition() > 4
}
private inner class ScrollListener : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (isScrolledToBottom()) {
viewModel.setShowScrollButtons(false)
} else if (isScrolledPastButtonThreshold()) {
viewModel.setShowScrollButtons(true)
}
val timestamp = MarkReadHelper.getLatestTimestamp(adapter, layoutManager)
timestamp.ifPresent(viewModel::requestMarkRead)
}
}
private inner class DataObserver( private inner class DataObserver(
private val scrollToPositionDelegate: ScrollToPositionDelegate private val scrollToPositionDelegate: ScrollToPositionDelegate
) : RecyclerView.AdapterDataObserver() { ) : RecyclerView.AdapterDataObserver() {

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.conversation.v2 package org.thoughtcrime.securesms.conversation.v2
import android.content.Context import android.content.Context
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.core.Single
import io.reactivex.rxjava3.kotlin.subscribeBy import io.reactivex.rxjava3.kotlin.subscribeBy
@@ -11,6 +12,7 @@ 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.RxDatabaseObserver
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.database.model.Quote import org.thoughtcrime.securesms.database.model.Quote
@@ -104,4 +106,36 @@ class ConversationRepository(context: Context) {
SignalDatabase.messages.getQuotedMessagePosition(threadId, quote.id, quote.author) SignalDatabase.messages.getQuotedMessagePosition(threadId, quote.id, quote.author)
}.subscribeOn(Schedulers.io()) }.subscribeOn(Schedulers.io())
} }
fun getNextMentionPosition(threadId: Long): Single<Int> {
return Single.fromCallable {
val details = SignalDatabase.messages.getOldestUnreadMentionDetails(threadId)
if (details == null) {
-1
} else {
SignalDatabase.messages.getMessagePositionInConversation(threadId, details.second(), details.first())
}
}.subscribeOn(Schedulers.io())
}
fun getMessageCounts(threadId: Long): Flowable<MessageCounts> {
return RxDatabaseObserver.conversationList
.map { getUnreadCount(threadId) }
.distinctUntilChanged()
.map { MessageCounts(it, getUnreadMentionsCount(threadId)) }
}
private fun getUnreadCount(threadId: Long): Int {
val threadRecord = threads.getThreadRecord(threadId)
return threadRecord?.unreadCount ?: 0
}
private fun getUnreadMentionsCount(threadId: Long): Int {
return SignalDatabase.messages.getUnreadMentionCount(threadId)
}
data class MessageCounts(
val unread: Int,
val mentions: Int
)
} }

View File

@@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.conversation.v2
data class ConversationScrollButtonState(
val showScrollButtons: Boolean = false,
val unreadCount: Int = 0,
val hasMentions: Boolean = false
)

View File

@@ -2,6 +2,7 @@ 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.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable 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.core.Single
@@ -23,6 +24,7 @@ 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
import org.thoughtcrime.securesms.util.rx.RxStore
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
/** /**
@@ -37,6 +39,11 @@ class ConversationViewModel(
private val disposables = CompositeDisposable() private val disposables = CompositeDisposable()
private val groupAuthorNameColorHelper = GroupAuthorNameColorHelper() private val groupAuthorNameColorHelper = GroupAuthorNameColorHelper()
private val scrollButtonStateStore = RxStore(ConversationScrollButtonState()).addTo(disposables)
val scrollButtonState: Flowable<ConversationScrollButtonState> = scrollButtonStateStore.stateFlowable
.distinctUntilChanged()
.observeOn(AndroidSchedulers.mainThread())
private val _recipient: BehaviorSubject<Recipient> = BehaviorSubject.create() private val _recipient: BehaviorSubject<Recipient> = BehaviorSubject.create()
val recipient: Observable<Recipient> = _recipient val recipient: Observable<Recipient> = _recipient
@@ -92,16 +99,35 @@ class ConversationViewModel(
} }
} }
}.subscribe() }.subscribe()
disposables += scrollButtonStateStore.update(
repository.getMessageCounts(threadId)
) { counts, state ->
state.copy(
unreadCount = counts.unread,
hasMentions = counts.mentions != 0
)
}
} }
override fun onCleared() { override fun onCleared() {
disposables.clear() disposables.clear()
} }
fun setShowScrollButtons(showScrollButtons: Boolean) {
scrollButtonStateStore.update {
it.copy(showScrollButtons = showScrollButtons)
}
}
fun getQuotedMessagePosition(quote: Quote): Single<Int> { fun getQuotedMessagePosition(quote: Quote): Single<Int> {
return repository.getQuotedMessagePosition(threadId, quote) return repository.getQuotedMessagePosition(threadId, quote)
} }
fun getNextMentionPosition(): Single<Int> {
return repository.getNextMentionPosition(threadId)
}
fun setLastScrolled(lastScrolledTimestamp: Long) { fun setLastScrolled(lastScrolledTimestamp: Long) {
repository.setLastVisibleMessageTimestamp( repository.setLastVisibleMessageTimestamp(
threadId, threadId,

View File

@@ -97,6 +97,32 @@
</org.thoughtcrime.securesms.util.views.DarkOverflowToolbar> </org.thoughtcrime.securesms.util.views.DarkOverflowToolbar>
<org.thoughtcrime.securesms.components.ConversationScrollToView
android:id="@+id/scroll_to_mention"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginBottom="12dp"
android:visibility="invisible"
app:cstv_scroll_button_src="@drawable/ic_at_20"
app:layout_constraintBottom_toTopOf="@id/scroll_to_bottom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_goneMarginBottom="20dp"
tools:visibility="visible" />
<org.thoughtcrime.securesms.components.ConversationScrollToView
android:id="@+id/scroll_to_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginBottom="16dp"
android:visibility="invisible"
app:cstv_scroll_button_src="@drawable/ic_chevron_down_20"
app:layout_constraintBottom_toTopOf="@id/conversation_bottom_panel_barrier"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Barrier <androidx.constraintlayout.widget.Barrier
android:id="@+id/conversation_bottom_panel_barrier" android:id="@+id/conversation_bottom_panel_barrier"
android:layout_width="wrap_content" android:layout_width="wrap_content"