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.res.TypedArray;
|
||||
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.ImageView;
|
||||
import android.widget.TextView;
|
||||
@@ -14,11 +17,14 @@ import androidx.annotation.Nullable;
|
||||
import com.airbnb.lottie.SimpleColorFilter;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public final class ConversationScrollToView extends FrameLayout {
|
||||
|
||||
private final TextView unreadCount;
|
||||
private final ImageView scrollButton;
|
||||
private final Animation inAnimation;
|
||||
private final Animation outAnimation;
|
||||
|
||||
public ConversationScrollToView(@NonNull Context context) {
|
||||
this(context, null);
|
||||
@@ -44,6 +50,20 @@ public final class ConversationScrollToView extends FrameLayout {
|
||||
|
||||
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) {
|
||||
|
||||
@@ -17,12 +17,8 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.Manifest;
|
||||
import android.animation.Animator;
|
||||
import android.animation.LayoutTransition;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.ActivityOptions;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
@@ -46,7 +42,6 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.ViewSwitcher;
|
||||
|
||||
import androidx.activity.result.ActivityResultCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -77,6 +72,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
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.SimpleTask;
|
||||
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.v2.GroupBlockJoinRequestResult;
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupDescriptionUtil;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceViewOnceOpenJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
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.FeatureFlags;
|
||||
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.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
@@ -253,10 +247,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
private ConversationGroupViewModel groupViewModel;
|
||||
private SnapToTopDataObserver snapToTopDataObserver;
|
||||
private MarkReadHelper markReadHelper;
|
||||
private Animation scrollButtonInAnimation;
|
||||
private Animation mentionButtonInAnimation;
|
||||
private Animation scrollButtonOutAnimation;
|
||||
private Animation mentionButtonOutAnimation;
|
||||
private OnScrollListener conversationScrollListener;
|
||||
private int lastSeenScrollOffset;
|
||||
private Stopwatch startupStopwatch;
|
||||
@@ -404,19 +394,11 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
}));
|
||||
|
||||
conversationViewModel.getShowMentionsButton().observe(getViewLifecycleOwner(), shouldShow -> {
|
||||
if (shouldShow) {
|
||||
ViewUtil.animateIn(scrollToMentionButton, mentionButtonInAnimation);
|
||||
} else {
|
||||
ViewUtil.animateOut(scrollToMentionButton, mentionButtonOutAnimation, View.INVISIBLE);
|
||||
}
|
||||
scrollToMentionButton.setShown(shouldShow);
|
||||
});
|
||||
|
||||
conversationViewModel.getShowScrollToBottom().observe(getViewLifecycleOwner(), shouldShow -> {
|
||||
if (shouldShow) {
|
||||
ViewUtil.animateIn(scrollToBottomButton, scrollButtonInAnimation);
|
||||
} else {
|
||||
ViewUtil.animateOut(scrollToBottomButton, scrollButtonOutAnimation, View.INVISIBLE);
|
||||
}
|
||||
scrollToBottomButton.setShown(shouldShow);
|
||||
});
|
||||
|
||||
scrollToBottomButton.setOnClickListener(v -> scrollToBottom());
|
||||
@@ -445,7 +427,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
|
||||
conversationViewModel.getActiveNotificationProfile().observe(getViewLifecycleOwner(), this::updateNotificationProfileStatus);
|
||||
|
||||
initializeScrollButtonAnimations();
|
||||
initializeResources();
|
||||
initializeMessageRequestViewModel();
|
||||
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() {
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
||||
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.ConversationOptionsMenu
|
||||
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.RecyclerViewColorizer
|
||||
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 lateinit var conversationOptionsMenuProvider: ConversationOptionsMenu.Provider
|
||||
private lateinit var layoutManager: SmoothScrollingLinearLayoutManager
|
||||
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 scrollToPositionDelegate: ScrollToPositionDelegate
|
||||
private lateinit var adapter: ConversationAdapter
|
||||
private lateinit var recyclerViewColorizer: RecyclerViewColorizer
|
||||
|
||||
private val jumpAndPulseScrollStrategy = object : ScrollToPositionDelegate.ScrollStrategy {
|
||||
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)
|
||||
binding.conversationItemRecycler.setHasFixedSize(false)
|
||||
binding.conversationItemRecycler.layoutManager = layoutManager
|
||||
binding.conversationItemRecycler.addOnScrollListener(ScrollListener())
|
||||
|
||||
binding.scrollToBottom.setOnClickListener {
|
||||
scrollToPositionDelegate.resetScrollPosition()
|
||||
}
|
||||
|
||||
binding.scrollToMention.setOnClickListener {
|
||||
scrollToNextMention()
|
||||
}
|
||||
|
||||
val layoutTransitionListener = BubbleLayoutTransitionListener(binding.conversationItemRecycler)
|
||||
viewLifecycleOwner.lifecycle.addObserver(layoutTransitionListener)
|
||||
|
||||
val recyclerViewColorizer = RecyclerViewColorizer(binding.conversationItemRecycler)
|
||||
recyclerViewColorizer = RecyclerViewColorizer(binding.conversationItemRecycler)
|
||||
recyclerViewColorizer.setChatColors(args.chatColors)
|
||||
|
||||
val conversationToolbarOnScrollHelper = ConversationToolbarOnScrollHelper(
|
||||
@@ -190,16 +202,15 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
presentWallpaper(args.wallpaper)
|
||||
disposables += viewModel.recipient
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy(onNext = {
|
||||
recyclerViewColorizer.setChatColors(it.chatColors)
|
||||
presentWallpaper(it.wallpaper)
|
||||
presentConversationTitle(it)
|
||||
})
|
||||
.subscribeBy(onNext = this::onRecipientChanged)
|
||||
|
||||
disposables += viewModel.markReadRequests
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy(onNext = markReadHelper::onViewsRevealed)
|
||||
|
||||
disposables += viewModel.scrollButtonState
|
||||
.subscribeBy(onNext = this::presentScrollButtons)
|
||||
|
||||
EventBus.getDefault().registerForLifecycle(groupCallViewModel, viewLifecycleOwner)
|
||||
presentGroupCallJoinButton()
|
||||
}
|
||||
@@ -294,16 +305,15 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
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()
|
||||
}
|
||||
|
||||
private fun onRecipientChanged(recipient: Recipient) {
|
||||
presentWallpaper(recipient.wallpaper)
|
||||
presentConversationTitle(recipient)
|
||||
presentChatColors(recipient.chatColors)
|
||||
}
|
||||
|
||||
private fun invalidateOptionsMenu() {
|
||||
// TODO [alex] -- Handle search... is there a better way to manage this state? Maybe an event system?
|
||||
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.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() {
|
||||
@@ -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 val scrollToPositionDelegate: ScrollToPositionDelegate
|
||||
) : RecyclerView.AdapterDataObserver() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.content.Context
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
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.colors.GroupAuthorNameColorHelper
|
||||
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.Companion.threads
|
||||
import org.thoughtcrime.securesms.database.model.Quote
|
||||
@@ -104,4 +106,36 @@ class ConversationRepository(context: Context) {
|
||||
SignalDatabase.messages.getQuotedMessagePosition(threadId, quote.id, quote.author)
|
||||
}.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.ViewModelProvider
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
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.RecipientId
|
||||
import org.thoughtcrime.securesms.util.hasGiftBadge
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
|
||||
|
||||
/**
|
||||
@@ -37,6 +39,11 @@ class ConversationViewModel(
|
||||
private val disposables = CompositeDisposable()
|
||||
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()
|
||||
val recipient: Observable<Recipient> = _recipient
|
||||
|
||||
@@ -92,16 +99,35 @@ class ConversationViewModel(
|
||||
}
|
||||
}
|
||||
}.subscribe()
|
||||
|
||||
disposables += scrollButtonStateStore.update(
|
||||
repository.getMessageCounts(threadId)
|
||||
) { counts, state ->
|
||||
state.copy(
|
||||
unreadCount = counts.unread,
|
||||
hasMentions = counts.mentions != 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun setShowScrollButtons(showScrollButtons: Boolean) {
|
||||
scrollButtonStateStore.update {
|
||||
it.copy(showScrollButtons = showScrollButtons)
|
||||
}
|
||||
}
|
||||
|
||||
fun getQuotedMessagePosition(quote: Quote): Single<Int> {
|
||||
return repository.getQuotedMessagePosition(threadId, quote)
|
||||
}
|
||||
|
||||
fun getNextMentionPosition(): Single<Int> {
|
||||
return repository.getNextMentionPosition(threadId)
|
||||
}
|
||||
|
||||
fun setLastScrolled(lastScrolledTimestamp: Long) {
|
||||
repository.setLastVisibleMessageTimestamp(
|
||||
threadId,
|
||||
|
||||
@@ -97,6 +97,32 @@
|
||||
|
||||
</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
|
||||
android:id="@+id/conversation_bottom_panel_barrier"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
Reference in New Issue
Block a user