diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java index 9b5e837d06..f9055812f4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java @@ -30,7 +30,7 @@ public interface BindableConversationItem extends Unbindable { @NonNull Set batchSelected, @NonNull Recipient recipients, @Nullable String searchQuery, - boolean pulseHighlight); + boolean pulseMention); ConversationMessage getConversationMessage(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java index 31ca68d826..563ad6d6e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java @@ -29,6 +29,7 @@ public class ConversationItemThumbnail extends FrameLayout { private ConversationItemFooter footer; private CornerMask cornerMask; private Outliner outliner; + private Outliner pulseOutliner; private boolean borderless; public ConversationItemThumbnail(Context context) { @@ -80,6 +81,14 @@ public class ConversationItemThumbnail extends FrameLayout { outliner.draw(canvas); } } + + if (pulseOutliner != null) { + pulseOutliner.draw(canvas); + } + } + + public void setPulseOutliner(@NonNull Outliner outliner) { + this.pulseOutliner = outliner; } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationScrollToView.java b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationScrollToView.java new file mode 100644 index 0000000000..1d86c50c11 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationScrollToView.java @@ -0,0 +1,60 @@ +package org.thoughtcrime.securesms.components; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.thoughtcrime.securesms.R; + +public final class ConversationScrollToView extends FrameLayout { + + private final TextView unreadCount; + private final ImageView scrollButton; + + public ConversationScrollToView(@NonNull Context context) { + this(context, null); + } + + public ConversationScrollToView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public ConversationScrollToView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + inflate(context, R.layout.conversation_scroll_to, this); + + unreadCount = findViewById(R.id.conversation_scroll_to_count); + scrollButton = findViewById(R.id.conversation_scroll_to_button); + + if (attrs != null) { + TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ConversationScrollToView); + Drawable src = array.getDrawable(R.styleable.ConversationScrollToView_cstv_scroll_button_src); + + scrollButton.setImageDrawable(src); + + array.recycle(); + } + } + + @Override + public void setOnClickListener(@Nullable OnClickListener l) { + scrollButton.setOnClickListener(l); + } + + public void setUnreadCount(int unreadCount) { + this.unreadCount.setText(formatUnreadCount(unreadCount)); + this.unreadCount.setVisibility(unreadCount > 0 ? VISIBLE : GONE); + } + + private @NonNull CharSequence formatUnreadCount(int unreadCount) { + return unreadCount > 999 ? "999+" : String.valueOf(unreadCount); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/Outliner.java b/app/src/main/java/org/thoughtcrime/securesms/components/Outliner.java index 9a4566245b..a88e64d687 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/Outliner.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/Outliner.java @@ -25,6 +25,14 @@ public class Outliner { outlinePaint.setColor(color); } + public void setStrokeWidth(float pixels) { + outlinePaint.setStrokeWidth(pixels); + } + + public void setAlpha(int alpha) { + outlinePaint.setAlpha(alpha); + } + public void draw(Canvas canvas) { draw(canvas, 0, canvas.getWidth(), canvas.getHeight(), 0); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java index ea677a3716..e33f1d57f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java @@ -39,7 +39,6 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.CachedInflater; -import org.thoughtcrime.securesms.util.Conversions; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.StickyHeaderDecoration; import org.thoughtcrime.securesms.util.Util; @@ -96,7 +95,7 @@ public class ConversationAdapter private final MessageDigest digest; private String searchQuery; - private ConversationMessage recordToPulseHighlight; + private ConversationMessage recordToPulse; private View headerView; private View footerView; @@ -228,10 +227,10 @@ public class ConversationAdapter selected, recipient, searchQuery, - conversationMessage == recordToPulseHighlight); + conversationMessage == recordToPulse); - if (conversationMessage == recordToPulseHighlight) { - recordToPulseHighlight = null; + if (conversationMessage == recordToPulse) { + recordToPulse = null; } break; case MESSAGE_TYPE_HEADER: @@ -384,13 +383,13 @@ public class ConversationAdapter } /** - * Momentarily highlights a row at the requested position. + * Momentarily highlights a mention at the requested position. */ - void pulseHighlightItem(int position) { + void pulseAtPosition(int position) { if (position >= 0 && position < getItemCount()) { int correctedPosition = isHeaderPosition(position) ? position + 1 : position; - recordToPulseHighlight = getItem(correctedPosition); + recordToPulse = getItem(correctedPosition); notifyItemChanged(correctedPosition); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 542fa0b084..f9d5457c0c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -66,6 +66,7 @@ import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.PassphraseRequiredActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.components.ConversationScrollToView; import org.thoughtcrime.securesms.components.ConversationTypingView; import org.thoughtcrime.securesms.components.TooltipPopup; import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager; @@ -77,6 +78,7 @@ import org.thoughtcrime.securesms.conversation.ConversationAdapter.StickyHeaderV import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessagingDatabase; +import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; @@ -133,7 +135,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; -import java.util.Objects; import java.util.Set; @SuppressLint("StaticFieldLeak") @@ -161,14 +162,22 @@ public class ConversationFragment extends LoggingFragment { private ConversationTypingView typingView; private UnknownSenderView unknownSenderView; private View composeDivider; - private View scrollToBottomButton; + private ConversationScrollToView scrollToBottomButton; + private ConversationScrollToView scrollToMentionButton; private TextView scrollDateHeader; private ConversationBannerView conversationBanner; private ConversationBannerView emptyConversationBanner; private MessageRequestViewModel messageRequestViewModel; + private MessageCountsViewModel messageCountsViewModel; private ConversationViewModel conversationViewModel; private SnapToTopDataObserver snapToTopDataObserver; private MarkReadHelper markReadHelper; + private Animation scrollButtonInAnimation; + private Animation mentionButtonInAnimation; + private Animation scrollButtonOutAnimation; + private Animation mentionButtonOutAnimation; + private OnScrollListener conversationScrollListener; + private int pulsePosition = -1; public static void prepare(@NonNull Context context) { FrameLayout parent = new FrameLayout(context); @@ -191,13 +200,13 @@ public class ConversationFragment extends LoggingFragment { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) { final View view = inflater.inflate(R.layout.conversation_fragment, container, false); - list = ViewUtil.findById(view, android.R.id.list); - composeDivider = ViewUtil.findById(view, R.id.compose_divider); - scrollToBottomButton = ViewUtil.findById(view, R.id.scroll_to_bottom_button); - scrollDateHeader = ViewUtil.findById(view, R.id.scroll_date_header); - emptyConversationBanner = ViewUtil.findById(view, R.id.empty_conversation_banner); + list = view.findViewById(android.R.id.list); + composeDivider = view.findViewById(R.id.compose_divider); - scrollToBottomButton.setOnClickListener(v -> scrollToBottom()); + scrollToBottomButton = view.findViewById(R.id.scroll_to_bottom); + scrollToMentionButton = view.findViewById(R.id.scroll_to_mention); + scrollDateHeader = view.findViewById(R.id.scroll_date_header); + emptyConversationBanner = view.findViewById(R.id.empty_conversation_banner); final LinearLayoutManager layoutManager = new SmoothScrollingLinearLayoutManager(getActivity(), true); list.setHasFixedSize(false); @@ -222,7 +231,9 @@ public class ConversationFragment extends LoggingFragment { setupListLayoutListeners(); - this.conversationViewModel = ViewModelProviders.of(requireActivity(), new ConversationViewModel.Factory()).get(ConversationViewModel.class); + this.messageCountsViewModel = ViewModelProviders.of(requireActivity()).get(MessageCountsViewModel.class); + this.conversationViewModel = ViewModelProviders.of(requireActivity(), new ConversationViewModel.Factory()).get(ConversationViewModel.class); + conversationViewModel.getMessages().observe(this, list -> { if (getListAdapter() != null && !list.getDataSource().isInvalid()) { Log.i(TAG, "submitList"); @@ -233,6 +244,25 @@ public class ConversationFragment extends LoggingFragment { }); conversationViewModel.getConversationMetadata().observe(this, this::presentConversationMetadata); + conversationViewModel.getShowMentionsButton().observe(this, shouldShow -> { + if (shouldShow) { + ViewUtil.animateIn(scrollToMentionButton, mentionButtonInAnimation); + } else { + ViewUtil.animateOut(scrollToMentionButton, mentionButtonOutAnimation, View.INVISIBLE); + } + }); + + conversationViewModel.getShowScrollToBottom().observe(this, shouldShow -> { + if (shouldShow) { + ViewUtil.animateIn(scrollToBottomButton, scrollButtonInAnimation); + } else { + ViewUtil.animateOut(scrollToBottomButton, scrollButtonOutAnimation, View.INVISIBLE); + } + }); + + scrollToBottomButton.setOnClickListener(v -> scrollToBottom()); + scrollToMentionButton.setOnClickListener(v -> scrollToNextMention()); + return view; } @@ -268,6 +298,7 @@ public class ConversationFragment extends LoggingFragment { public void onActivityCreated(Bundle bundle) { super.onActivityCreated(bundle); + initializeScrollButtonAnimations(); initializeResources(); initializeMessageRequestViewModel(); initializeListAdapter(); @@ -426,9 +457,16 @@ public class ConversationFragment extends LoggingFragment { this.markReadHelper = new MarkReadHelper(threadId, requireContext()); conversationViewModel.onConversationDataAvailable(threadId, startingPosition); + messageCountsViewModel.setThreadId(threadId); - OnScrollListener scrollListener = new ConversationScrollListener(getActivity()); - list.addOnScrollListener(scrollListener); + messageCountsViewModel.getUnreadMessagesCount().observe(getViewLifecycleOwner(), scrollToBottomButton::setUnreadCount); + messageCountsViewModel.getUnreadMentionsCount().observe(getViewLifecycleOwner(), count -> { + scrollToMentionButton.setUnreadCount(count); + conversationViewModel.setHasUnreadMentions(count > 0); + }); + + conversationScrollListener = new ConversationScrollListener(requireContext()); + list.addOnScrollListener(conversationScrollListener); if (oldThreadId != threadId) { ApplicationContext.getInstance(requireContext()).getTypingStatusRepository().getTypists(oldThreadId).removeObservers(this); @@ -566,6 +604,7 @@ public class ConversationFragment extends LoggingFragment { snapToTopDataObserver.requestScrollPosition(0); conversationViewModel.onConversationDataAvailable(threadId, -1); + messageCountsViewModel.setThreadId(threadId); initializeListAdapter(); } } @@ -655,6 +694,7 @@ public class ConversationFragment extends LoggingFragment { if (threadDeleted) { threadId = -1; conversationViewModel.clearThreadId(); + messageCountsViewModel.clearThreadId(); listener.setThreadId(threadId); } } @@ -695,6 +735,7 @@ public class ConversationFragment extends LoggingFragment { if (threadDeleted) { threadId = -1; conversationViewModel.clearThreadId(); + messageCountsViewModel.clearThreadId(); listener.setThreadId(threadId); } } @@ -920,6 +961,8 @@ public class ConversationFragment extends LoggingFragment { } listener.onCursorChanged(); + + conversationScrollListener.onScrolled(list, 0, 0); }; int lastSeenPosition = adapter.getAdapterPositionForMessagePosition(conversation.getLastSeenPosition()); @@ -931,7 +974,7 @@ public class ConversationFragment extends LoggingFragment { snapToTopDataObserver.buildScrollPosition(conversation.getJumpToPosition()) .withOnScrollRequestComplete(() -> { afterScroll.run(); - getListAdapter().pulseHighlightItem(conversation.getJumpToPosition()); + getListAdapter().pulseAtPosition(conversation.getJumpToPosition()); }) .submit(); } else if (conversation.isMessageRequestAccepted()) { @@ -969,27 +1012,40 @@ public class ConversationFragment extends LoggingFragment { } } + @SuppressWarnings("CodeBlock2Expr") public void jumpToMessage(@NonNull RecipientId author, long timestamp, @Nullable Runnable onMessageNotFound) { SimpleTask.run(getLifecycle(), () -> { return DatabaseFactory.getMmsSmsDatabase(getContext()) .getMessagePositionInConversation(threadId, timestamp, author); - }, p -> moveToMessagePosition(p + (isTypingIndicatorShowing() ? 1 : 0), onMessageNotFound)); + }, p -> moveToPosition(p + (isTypingIndicatorShowing() ? 1 : 0), onMessageNotFound)); } - private void moveToMessagePosition(int position, @Nullable Runnable onMessageNotFound) { + private void moveToPosition(int position, @Nullable Runnable onMessageNotFound) { conversationViewModel.onConversationDataAvailable(threadId, position); snapToTopDataObserver.buildScrollPosition(position) .withOnPerformScroll(((layoutManager, p) -> - list.post(() -> { - layoutManager.scrollToPosition(p); - getListAdapter().pulseHighlightItem(position); - }) + list.post(() -> { + if (Math.abs(layoutManager.findFirstVisibleItemPosition() - p) < SCROLL_ANIMATION_THRESHOLD) { + View child = layoutManager.findViewByPosition(position); + + if (child != null && layoutManager.isViewPartiallyVisible(child, true, false)) { + getListAdapter().pulseAtPosition(position); + } else { + pulsePosition = position; + } + + list.smoothScrollToPosition(p); + } else { + layoutManager.scrollToPosition(p); + getListAdapter().pulseAtPosition(position); + } + }) )) .withOnInvalidPosition(() -> { if (onMessageNotFound != null) { onMessageNotFound.run(); } - Log.w(TAG, "[moveToMessagePosition] Tried to navigate to message, but it wasn't found."); + Log.w(TAG, "[moveToMentionPosition] Tried to navigate to mention, but it wasn't found."); }) .submit(); } @@ -1008,6 +1064,48 @@ public class ConversationFragment extends LoggingFragment { } } + 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(), () -> { + MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(ApplicationDependencies.getApplication()); + return mmsDatabase.getOldestUnreadMentionDetails(threadId); + }, (pair) -> { + if (pair != null) { + jumpToMessage(pair.first, pair.second, () -> {}); + } + }); + } + + private void postMarkAsReadRequest() { + if (getListAdapter().hasNoConversationMessages()) { + return; + } + + int position = getListLayoutManager().findFirstVisibleItemPosition(); + if (position >= (isTypingIndicatorShowing() ? 1 : 0)) { + ConversationMessage item = getListAdapter().getItem(position); + if (item != null) { + long timestamp = item.getMessageRecord() + .getDateReceived(); + + markReadHelper.onViewsRevealed(timestamp); + } + } + } + public interface ConversationFragmentListener { void setThreadId(long threadId); void handleReplyMessage(ConversationMessage conversationMessage); @@ -1026,47 +1124,40 @@ public class ConversationFragment extends LoggingFragment { private class ConversationScrollListener extends OnScrollListener { - private final Animation scrollButtonInAnimation; - private final Animation scrollButtonOutAnimation; private final ConversationDateHeader conversationDateHeader; private boolean wasAtBottom = true; - private boolean wasAtZoomScrollHeight = false; private long lastPositionId = -1; ConversationScrollListener(@NonNull Context context) { - this.scrollButtonInAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_in); - this.scrollButtonOutAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_out); this.conversationDateHeader = new ConversationDateHeader(context, scrollDateHeader); - this.scrollButtonInAnimation.setDuration(100); - this.scrollButtonOutAnimation.setDuration(50); } @Override public void onScrolled(@NonNull final RecyclerView rv, final int dx, final int dy) { - boolean currentlyAtBottom = isAtBottom(); + boolean currentlyAtBottom = !rv.canScrollVertically(1); boolean currentlyAtZoomScrollHeight = isAtZoomScrollHeight(); int positionId = getHeaderPositionId(); if (currentlyAtBottom && !wasAtBottom) { ViewUtil.fadeOut(composeDivider, 50, View.INVISIBLE); - ViewUtil.animateOut(scrollToBottomButton, scrollButtonOutAnimation, View.INVISIBLE); } else if (!currentlyAtBottom && wasAtBottom) { ViewUtil.fadeIn(composeDivider, 500); } - if (currentlyAtZoomScrollHeight && !wasAtZoomScrollHeight) { - ViewUtil.animateIn(scrollToBottomButton, scrollButtonInAnimation); + if (currentlyAtBottom) { + conversationViewModel.setShowScrollButtons(false); + } else if (currentlyAtZoomScrollHeight) { + conversationViewModel.setShowScrollButtons(true); } if (positionId != lastPositionId) { bindScrollHeader(conversationDateHeader, positionId); } - wasAtBottom = currentlyAtBottom; - wasAtZoomScrollHeight = currentlyAtZoomScrollHeight; - lastPositionId = positionId; + wasAtBottom = currentlyAtBottom; + lastPositionId = positionId; postMarkAsReadRequest(); } @@ -1077,22 +1168,10 @@ public class ConversationFragment extends LoggingFragment { conversationDateHeader.show(); } else if (newState == RecyclerView.SCROLL_STATE_IDLE) { conversationDateHeader.hide(); - } - } - private void postMarkAsReadRequest() { - if (getListAdapter().hasNoConversationMessages()) { - return; - } - - int position = getListLayoutManager().findFirstVisibleItemPosition(); - if (position >= (isTypingIndicatorShowing() ? 1 : 0)) { - ConversationMessage item = getListAdapter().getItem(position); - if (item != null) { - long timestamp = item.getMessageRecord() - .getDateReceived(); - - markReadHelper.onViewsRevealed(timestamp); + if (pulsePosition != -1) { + getListAdapter().pulseAtPosition(pulsePosition); + pulsePosition = -1; } } } @@ -1175,7 +1254,7 @@ public class ConversationFragment extends LoggingFragment { .getQuotedMessagePosition(threadId, messageRecord.getQuote().getId(), messageRecord.getQuote().getAuthor()); - }, p -> moveToMessagePosition(p + (isTypingIndicatorShowing() ? 1 : 0), () -> { + }, p -> moveToPosition(p + (isTypingIndicatorShowing() ? 1 : 0), () -> { Toast.makeText(getContext(), R.string.ConversationFragment_quoted_message_no_longer_available, Toast.LENGTH_SHORT).show(); })); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index b7eadcd9b8..d338509f73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -16,6 +16,7 @@ */ package org.thoughtcrime.securesms.conversation; +import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.content.ActivityNotFoundException; import android.content.Context; @@ -119,6 +120,7 @@ import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.views.Stub; import org.whispersystems.libsignal.util.guava.Optional; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -151,6 +153,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati private boolean groupThread; private LiveRecipient recipient; private GlideRequests glideRequests; + private ValueAnimator pulseOutlinerAlphaAnimator; protected ConversationItemBodyBubble bodyBubble; protected View reply; @@ -167,8 +170,10 @@ public class ConversationItem extends LinearLayout implements BindableConversati private ViewGroup container; protected ReactionsConversationView reactionsView; - private @NonNull Set batchSelected = new HashSet<>(); - private @NonNull Outliner outliner = new Outliner(); + private @NonNull Set batchSelected = new HashSet<>(); + private @NonNull Outliner outliner = new Outliner(); + private @NonNull Outliner pulseOutliner = new Outliner(); + private @NonNull List outliners = new ArrayList<>(2); private LiveRecipient conversationRecipient; private Stub mediaThumbnailStub; private Stub audioViewStub; @@ -249,7 +254,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati @NonNull Set batchSelected, @NonNull Recipient conversationRecipient, @Nullable String searchQuery, - boolean pulseHighlight) + boolean pulse) { if (this.recipient != null) this.recipient.removeForeverObserver(this); if (this.conversationRecipient != null) this.conversationRecipient.removeForeverObserver(this); @@ -271,9 +276,9 @@ public class ConversationItem extends LinearLayout implements BindableConversati setGutterSizes(messageRecord, groupThread); setMessageShape(messageRecord, previousMessageRecord, nextMessageRecord, groupThread); setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, conversationRecipient, groupThread); - setInteractionState(conversationMessage, pulseHighlight); setBodyText(messageRecord, searchQuery); setBubbleState(messageRecord); + setInteractionState(conversationMessage, pulse); setStatusIcons(messageRecord); setContactPhoto(recipient.get()); setGroupMessageStatus(messageRecord, recipient.get()); @@ -387,6 +392,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati if (conversationRecipient != null) { conversationRecipient.removeForeverObserver(this); } + cancelPulseOutlinerAnimation(); } public ConversationMessage getConversationMessage() { @@ -411,7 +417,21 @@ public class ConversationItem extends LinearLayout implements BindableConversati } outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_sent_text_secondary_color)); - bodyBubble.setOutliner(shouldDrawBodyBubbleOutline(messageRecord) ? outliner : null); + + pulseOutliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_mention_pulse_color)); + pulseOutliner.setStrokeWidth(ViewUtil.dpToPx(4)); + + outliners.clear(); + if (shouldDrawBodyBubbleOutline(messageRecord)) { + outliners.add(outliner); + } + outliners.add(pulseOutliner); + + bodyBubble.setOutliners(outliners); + + if (mediaThumbnailStub.resolved()) { + mediaThumbnailStub.get().setPulseOutliner(pulseOutliner); + } if (audioViewStub.resolved()) { setAudioViewTint(messageRecord); @@ -432,14 +452,14 @@ public class ConversationItem extends LinearLayout implements BindableConversati } } - private void setInteractionState(ConversationMessage conversationMessage, boolean pulseHighlight) { + private void setInteractionState(ConversationMessage conversationMessage, boolean pulseMention) { if (batchSelected.contains(conversationMessage)) { setBackgroundResource(R.drawable.conversation_item_background); setSelected(true); - } else if (pulseHighlight) { - setBackgroundResource(R.drawable.conversation_item_background_animated); - setSelected(true); - postDelayed(() -> setSelected(false), 500); + } else if (pulseMention) { + setBackground(null); + setSelected(false); + startPulseOutlinerAnimation(); } else { setSelected(false); } @@ -462,6 +482,28 @@ public class ConversationItem extends LinearLayout implements BindableConversati } } + private void startPulseOutlinerAnimation() { + pulseOutlinerAlphaAnimator = ValueAnimator.ofInt(0, 0x66, 0).setDuration(600); + pulseOutlinerAlphaAnimator.addUpdateListener(animator -> { + pulseOutliner.setAlpha((Integer) animator.getAnimatedValue()); + bodyBubble.invalidate(); + + if (mediaThumbnailStub.resolved()) { + mediaThumbnailStub.get().invalidate(); + } + }); + pulseOutlinerAlphaAnimator.start(); + } + + private void cancelPulseOutlinerAnimation() { + if (pulseOutlinerAlphaAnimator != null) { + pulseOutlinerAlphaAnimator.cancel(); + pulseOutlinerAlphaAnimator = null; + } + + pulseOutliner.setAlpha(0); + } + private boolean shouldDrawBodyBubbleOutline(MessageRecord messageRecord) { boolean isIncomingViewedOnce = !messageRecord.isOutgoing() && isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord); return isIncomingViewedOnce || messageRecord.isRemoteDelete(); @@ -1097,33 +1139,41 @@ public class ConversationItem extends LinearLayout implements BindableConversati if (current.isOutgoing()) { background = R.drawable.message_bubble_background_sent_alone; outliner.setRadius(bigRadius); + pulseOutliner.setRadius(bigRadius); } else { background = R.drawable.message_bubble_background_received_alone; outliner.setRadius(bigRadius); + pulseOutliner.setRadius(bigRadius); } } else if (isStartOfMessageCluster(current, previous, isGroupThread)) { if (current.isOutgoing()) { background = R.drawable.message_bubble_background_sent_start; outliner.setRadii(bigRadius, bigRadius, smallRadius, bigRadius); + pulseOutliner.setRadii(bigRadius, bigRadius, smallRadius, bigRadius); } else { background = R.drawable.message_bubble_background_received_start; outliner.setRadii(bigRadius, bigRadius, bigRadius, smallRadius); + pulseOutliner.setRadii(bigRadius, bigRadius, bigRadius, smallRadius); } } else if (isEndOfMessageCluster(current, next, isGroupThread)) { if (current.isOutgoing()) { background = R.drawable.message_bubble_background_sent_end; outliner.setRadii(bigRadius, smallRadius, bigRadius, bigRadius); + pulseOutliner.setRadii(bigRadius, smallRadius, bigRadius, bigRadius); } else { background = R.drawable.message_bubble_background_received_end; outliner.setRadii(smallRadius, bigRadius, bigRadius, bigRadius); + pulseOutliner.setRadii(smallRadius, bigRadius, bigRadius, bigRadius); } } else { if (current.isOutgoing()) { background = R.drawable.message_bubble_background_sent_middle; outliner.setRadii(bigRadius, smallRadius, smallRadius, bigRadius); + pulseOutliner.setRadii(bigRadius, smallRadius, smallRadius, bigRadius); } else { background = R.drawable.message_bubble_background_received_middle; outliner.setRadii(smallRadius, bigRadius, bigRadius, smallRadius); + pulseOutliner.setRadii(smallRadius, bigRadius, bigRadius, smallRadius); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemBodyBubble.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemBodyBubble.java index 5719292bf3..af1b4be7c1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemBodyBubble.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemBodyBubble.java @@ -5,13 +5,18 @@ import android.graphics.Canvas; import android.util.AttributeSet; import android.widget.LinearLayout; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.thoughtcrime.securesms.components.Outliner; +import org.thoughtcrime.securesms.util.Util; + +import java.util.Collections; +import java.util.List; public class ConversationItemBodyBubble extends LinearLayout { - @Nullable private Outliner outliner; + @Nullable private List outliners = Collections.emptyList(); @Nullable private OnSizeChangedListener sizeChangedListener; public ConversationItemBodyBubble(Context context) { @@ -26,8 +31,8 @@ public class ConversationItemBodyBubble extends LinearLayout { super(context, attrs, defStyleAttr); } - public void setOutliner(@Nullable Outliner outliner) { - this.outliner = outliner; + public void setOutliners(@NonNull List outliners) { + this.outliners = outliners; } public void setOnSizeChangedListener(@Nullable OnSizeChangedListener listener) { @@ -38,9 +43,11 @@ public class ConversationItemBodyBubble extends LinearLayout { protected void onDraw(Canvas canvas) { super.onDraw(canvas); - if (outliner == null) return; + if (Util.isEmpty(outliners)) return; - outliner.draw(canvas, 0, getMeasuredWidth(), getMeasuredHeight(), 0); + for (Outliner outliner : outliners) { + outliner.draw(canvas, 0, getMeasuredWidth(), getMeasuredHeight(), 0); + } } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java index db53e2b232..e128b39859 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -93,7 +93,7 @@ public final class ConversationUpdateItem extends LinearLayout @NonNull Set batchSelected, @NonNull Recipient conversationRecipient, @Nullable String searchQuery, - boolean pulseUpdate) + boolean pulseMention) { this.batchSelected = batchSelected; diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java index 05b5c6a66e..91ee2e948f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java @@ -4,7 +4,6 @@ import android.app.Application; import androidx.annotation.MainThread; import androidx.annotation.NonNull; -import androidx.annotation.WorkerThread; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Transformations; @@ -14,7 +13,6 @@ import androidx.paging.DataSource; import androidx.paging.LivePagedListBuilder; import androidx.paging.PagedList; -import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mediasend.Media; @@ -38,6 +36,8 @@ class ConversationViewModel extends ViewModel { private final LiveData> messages; private final LiveData conversationMetadata; private final Invalidator invalidator; + private final MutableLiveData showScrollButtons; + private final MutableLiveData hasUnreadMentions; private int jumpToPosition; @@ -48,6 +48,8 @@ class ConversationViewModel extends ViewModel { this.recentMedia = new MutableLiveData<>(); this.threadId = new MutableLiveData<>(); this.invalidator = new Invalidator(); + this.showScrollButtons = new MutableLiveData<>(false); + this.hasUnreadMentions = new MutableLiveData<>(false); LiveData metadata = Transformations.switchMap(threadId, thread -> { LiveData conversationData = conversationRepository.getConversationData(thread, jumpToPosition); @@ -109,6 +111,22 @@ class ConversationViewModel extends ViewModel { this.threadId.postValue(-1L); } + @NonNull LiveData getShowScrollToBottom() { + return Transformations.distinctUntilChanged(showScrollButtons); + } + + @NonNull LiveData getShowMentionsButton() { + return Transformations.distinctUntilChanged(LiveDataUtil.combineLatest(showScrollButtons, hasUnreadMentions, (a, b) -> a && b)); + } + + void setHasUnreadMentions(boolean hasUnreadMentions) { + this.hasUnreadMentions.setValue(hasUnreadMentions); + } + + void setShowScrollButtons(boolean showScrollButtons) { + this.showScrollButtons.setValue(showScrollButtons); + } + @NonNull LiveData> getRecentMedia() { return recentMedia; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/MessageCountsViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/MessageCountsViewModel.java new file mode 100644 index 0000000000..0ac5daabe9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/MessageCountsViewModel.java @@ -0,0 +1,95 @@ +package org.thoughtcrime.securesms.conversation; + +import android.app.Application; +import android.content.Context; +import android.database.ContentObserver; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Transformations; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + +import org.thoughtcrime.securesms.database.DatabaseContentProviders; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.database.MmsSmsDatabase; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor; +import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; +import org.whispersystems.libsignal.util.Pair; + +import java.util.Objects; +import java.util.concurrent.Executor; + +public class MessageCountsViewModel extends ViewModel { + + private static final Executor EXECUTOR = new SerialMonoLifoExecutor(SignalExecutors.BOUNDED); + + private final Application context; + private final MutableLiveData threadId = new MutableLiveData<>(-1L); + private final LiveData> unreadCounts; + + private ContentObserver observer; + + public MessageCountsViewModel() { + this.context = ApplicationDependencies.getApplication(); + this.unreadCounts = Transformations.switchMap(Transformations.distinctUntilChanged(threadId), id -> { + + MutableLiveData> counts = new MutableLiveData<>(new Pair<>(0, 0)); + + if (id == -1L) { + return counts; + } + + observer = new ContentObserver(null) { + @Override + public void onChange(boolean selfChange) { + EXECUTOR.execute(() -> { + counts.postValue(getCounts(context, id)); + }); + } + }; + + observer.onChange(false); + + context.getContentResolver().registerContentObserver(DatabaseContentProviders.Conversation.getUriForThread(id), true, observer); + + return counts; + }); + + } + + void setThreadId(long threadId) { + this.threadId.setValue(threadId); + } + + void clearThreadId() { + this.threadId.postValue(-1L); + } + + @NonNull LiveData getUnreadMessagesCount() { + return Transformations.map(unreadCounts, Pair::first); + } + + @NonNull LiveData getUnreadMentionsCount() { + return Transformations.map(unreadCounts, Pair::second); + } + + private Pair getCounts(@NonNull Context context, long threadId) { + MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context); + MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context); + int unreadCount = mmsSmsDatabase.getUnreadCount(threadId); + int unreadMentionCount = mmsDatabase.getUnreadMentionCount(threadId); + + return new Pair<>(unreadCount, unreadMentionCount); + } + + @Override + protected void onCleared() { + if (observer != null) { + context.getContentResolver().unregisterContentObserver(observer); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index ea7e031e78..dd46f66d26 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -72,6 +72,7 @@ import org.thoughtcrime.securesms.revealable.ViewOnceExpirationInfo; import org.thoughtcrime.securesms.revealable.ViewOnceUtil; import org.thoughtcrime.securesms.util.CursorUtil; import org.thoughtcrime.securesms.util.JsonUtils; +import org.thoughtcrime.securesms.util.SqlUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; @@ -746,6 +747,36 @@ public class MmsDatabase extends MessagingDatabase { return expiring; } + public @Nullable Pair getOldestUnreadMentionDetails(long threadId) { + SQLiteDatabase database = databaseHelper.getReadableDatabase(); + String[] projection = new String[]{RECIPIENT_ID,DATE_RECEIVED}; + String selection = THREAD_ID + " = ? AND " + READ + " = 0 AND " + MENTIONS_SELF + " = 1"; + String[] args = SqlUtil.buildArgs(threadId); + + try (Cursor cursor = database.query(TABLE_NAME, projection, selection, args, null, null, DATE_RECEIVED + " ASC", "1")) { + if (cursor != null && cursor.moveToFirst()) { + return new Pair<>(RecipientId.from(CursorUtil.requireString(cursor, RECIPIENT_ID)), CursorUtil.requireLong(cursor, DATE_RECEIVED)); + } + } + + return null; + } + + public int getUnreadMentionCount(long threadId) { + SQLiteDatabase database = databaseHelper.getReadableDatabase(); + String[] projection = new String[]{"COUNT(*)"}; + String selection = THREAD_ID + " = ? AND " + READ + " = 0 AND " + MENTIONS_SELF + " = 1"; + String[] args = SqlUtil.buildArgs(threadId); + + try (Cursor cursor = database.query(TABLE_NAME, projection, selection, args, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + return cursor.getInt(0); + } + } + + return 0; + } + public void updateMessageBody(long messageId, String body) { long type = 0; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 7a210bb538..4f0931cbb0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -387,6 +387,7 @@ public class ThreadDatabase extends Database { db.endTransaction(); } + notifyConversationListeners(new HashSet<>(threadIds)); notifyConversationListListeners(); return Util.concatenatedList(smsRecords, mmsRecords); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java index e2bd4f9965..8ee1b46ea0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java @@ -249,11 +249,6 @@ public class DefaultMessageNotifier implements MessageNotifier { ThreadDatabase threads = DatabaseFactory.getThreadDatabase(context); - if (isVisible) { - List messageIds = threads.setRead(threadId, false); - MarkReadReceiver.process(context, messageIds); - } - if (!TextSecurePreferences.isNotificationsEnabled(context)) { return; } diff --git a/app/src/main/res/drawable-hdpi/ic_scroll_down.webp b/app/src/main/res/drawable-hdpi/ic_scroll_down.webp deleted file mode 100644 index 9d9423bc2b..0000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_scroll_down.webp and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_scroll_down.webp b/app/src/main/res/drawable-mdpi/ic_scroll_down.webp deleted file mode 100644 index 894569956a..0000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_scroll_down.webp and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_scroll_down.webp b/app/src/main/res/drawable-xhdpi/ic_scroll_down.webp deleted file mode 100644 index bd9bec6312..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_scroll_down.webp and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_scroll_down.webp b/app/src/main/res/drawable-xxhdpi/ic_scroll_down.webp deleted file mode 100644 index 18c21332e0..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_scroll_down.webp and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_scroll_down.webp b/app/src/main/res/drawable-xxxhdpi/ic_scroll_down.webp deleted file mode 100644 index 857f256706..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_scroll_down.webp and /dev/null differ diff --git a/app/src/main/res/drawable/ic_at_24.xml b/app/src/main/res/drawable/ic_at_24.xml new file mode 100644 index 0000000000..9891dd4bcc --- /dev/null +++ b/app/src/main/res/drawable/ic_at_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_chevron_down_20.xml b/app/src/main/res/drawable/ic_chevron_down_20.xml new file mode 100644 index 0000000000..2766d4df36 --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_down_20.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/message_bubble_background_received_alone.xml b/app/src/main/res/drawable/message_bubble_background_received_alone.xml index cda641b1da..b7186e4ff0 100644 --- a/app/src/main/res/drawable/message_bubble_background_received_alone.xml +++ b/app/src/main/res/drawable/message_bubble_background_received_alone.xml @@ -1,13 +1,11 @@ - + - + android:bottom="2px" + android:top="2px"> - + - + \ No newline at end of file diff --git a/app/src/main/res/drawable/message_bubble_background_received_end.xml b/app/src/main/res/drawable/message_bubble_background_received_end.xml index 3e4e2c0562..b5d1714e68 100644 --- a/app/src/main/res/drawable/message_bubble_background_received_end.xml +++ b/app/src/main/res/drawable/message_bubble_background_received_end.xml @@ -1,17 +1,15 @@ - + - + android:bottom="2px" + android:top="2px"> + android:topLeftRadius="@dimen/message_corner_collapse_radius" + android:topRightRadius="@dimen/message_corner_radius" /> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/message_bubble_background_received_middle.xml b/app/src/main/res/drawable/message_bubble_background_received_middle.xml index e3888978ab..627c3c6d97 100644 --- a/app/src/main/res/drawable/message_bubble_background_received_middle.xml +++ b/app/src/main/res/drawable/message_bubble_background_received_middle.xml @@ -1,16 +1,15 @@ - + + android:bottom="2px" + android:top="2px"> + android:topLeftRadius="@dimen/message_corner_collapse_radius" + android:topRightRadius="@dimen/message_corner_radius" /> diff --git a/app/src/main/res/drawable/message_bubble_background_received_start.xml b/app/src/main/res/drawable/message_bubble_background_received_start.xml index 4c30cc7565..f02feea0a1 100644 --- a/app/src/main/res/drawable/message_bubble_background_received_start.xml +++ b/app/src/main/res/drawable/message_bubble_background_received_start.xml @@ -1,16 +1,14 @@ - + - + android:bottom="2px" + android:top="2px"> + android:topLeftRadius="@dimen/message_corner_radius" + android:topRightRadius="@dimen/message_corner_radius" /> diff --git a/app/src/main/res/layout/conversation_fragment.xml b/app/src/main/res/layout/conversation_fragment.xml index bf82113024..96af2808f4 100644 --- a/app/src/main/res/layout/conversation_fragment.xml +++ b/app/src/main/res/layout/conversation_fragment.xml @@ -1,61 +1,76 @@ - + - + android:id="@android:id/list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:cacheColorHint="?conversation_background" + android:clipChildren="false" + android:clipToPadding="false" + android:paddingBottom="2dp" + android:scrollbars="vertical" /> - + - + - - + + + + + diff --git a/app/src/main/res/layout/conversation_scroll_to.xml b/app/src/main/res/layout/conversation_scroll_to.xml new file mode 100644 index 0000000000..ce77607ba1 --- /dev/null +++ b/app/src/main/res/layout/conversation_scroll_to.xml @@ -0,0 +1,38 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/submit_debug_log_activity.xml b/app/src/main/res/layout/submit_debug_log_activity.xml index c78c540f0f..dd600aad4e 100644 --- a/app/src/main/res/layout/submit_debug_log_activity.xml +++ b/app/src/main/res/layout/submit_debug_log_activity.xml @@ -63,7 +63,7 @@ android:background="@drawable/circle_tintable" android:tint="@color/grey_600" android:elevation="1dp" - android:src="@drawable/ic_scroll_down" + app:srcCompat="@drawable/ic_chevron_down_20" android:scaleY="-1" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/debug_log_warning_banner"/> @@ -78,7 +78,7 @@ android:background="@drawable/circle_tintable" android:tint="@color/grey_600" android:elevation="1dp" - android:src="@drawable/ic_scroll_down" + app:srcCompat="@drawable/ic_chevron_down_20" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toTopOf="@id/debug_log_submit_button"/> diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 0e6faf11e3..cd8aca9cc0 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -152,6 +152,7 @@ + @@ -559,4 +560,8 @@ + + + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index b762d8198f..a671a83f08 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -332,6 +332,7 @@ @color/transparent_black_20 ?conversation_background @color/core_grey_90 + @color/transparent_black @drawable/scroll_to_bottom_background_light @color/grey_600 @@ -568,6 +569,7 @@ @color/transparent_white_20 ?conversation_background @color/core_grey_15 + @color/transparent @color/core_grey_75 @color/core_grey_05