mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-22 20:18:36 +00:00
Add scroll buttons to CFV2.
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user