Add mentions unread counter.

master
Alex Hart 2020-08-13 15:37:15 -03:00 committed by Greyson Parrelli
parent 3c90dfa660
commit 06eadd0c15
30 changed files with 588 additions and 165 deletions

View File

@ -30,7 +30,7 @@ public interface BindableConversationItem extends Unbindable {
@NonNull Set<ConversationMessage> batchSelected,
@NonNull Recipient recipients,
@Nullable String searchQuery,
boolean pulseHighlight);
boolean pulseMention);
ConversationMessage getConversationMessage();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<ConversationMessage> batchSelected = new HashSet<>();
private @NonNull Outliner outliner = new Outliner();
private @NonNull Set<ConversationMessage> batchSelected = new HashSet<>();
private @NonNull Outliner outliner = new Outliner();
private @NonNull Outliner pulseOutliner = new Outliner();
private @NonNull List<Outliner> outliners = new ArrayList<>(2);
private LiveRecipient conversationRecipient;
private Stub<ConversationItemThumbnail> mediaThumbnailStub;
private Stub<AudioView> audioViewStub;
@ -249,7 +254,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
@NonNull Set<ConversationMessage> 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);
}
}

View File

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

View File

@ -93,7 +93,7 @@ public final class ConversationUpdateItem extends LinearLayout
@NonNull Set<ConversationMessage> batchSelected,
@NonNull Recipient conversationRecipient,
@Nullable String searchQuery,
boolean pulseUpdate)
boolean pulseMention)
{
this.batchSelected = batchSelected;

View File

@ -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<PagedList<ConversationMessage>> messages;
private final LiveData<ConversationData> conversationMetadata;
private final Invalidator invalidator;
private final MutableLiveData<Boolean> showScrollButtons;
private final MutableLiveData<Boolean> 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<ConversationData> metadata = Transformations.switchMap(threadId, thread -> {
LiveData<ConversationData> conversationData = conversationRepository.getConversationData(thread, jumpToPosition);
@ -109,6 +111,22 @@ class ConversationViewModel extends ViewModel {
this.threadId.postValue(-1L);
}
@NonNull LiveData<Boolean> getShowScrollToBottom() {
return Transformations.distinctUntilChanged(showScrollButtons);
}
@NonNull LiveData<Boolean> 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<List<Media>> getRecentMedia() {
return recentMedia;
}

View File

@ -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<Long> threadId = new MutableLiveData<>(-1L);
private final LiveData<Pair<Integer, Integer>> unreadCounts;
private ContentObserver observer;
public MessageCountsViewModel() {
this.context = ApplicationDependencies.getApplication();
this.unreadCounts = Transformations.switchMap(Transformations.distinctUntilChanged(threadId), id -> {
MutableLiveData<Pair<Integer, Integer>> 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<Integer> getUnreadMessagesCount() {
return Transformations.map(unreadCounts, Pair::first);
}
@NonNull LiveData<Integer> getUnreadMentionsCount() {
return Transformations.map(unreadCounts, Pair::second);
}
private Pair<Integer, Integer> 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);
}
}
}

View File

@ -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<RecipientId, Long> 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;

View File

@ -387,6 +387,7 @@ public class ThreadDatabase extends Database {
db.endTransaction();
}
notifyConversationListeners(new HashSet<>(threadIds));
notifyConversationListListeners();
return Util.concatenatedList(smsRecords, mmsRecords);
}

View File

@ -249,11 +249,6 @@ public class DefaultMessageNotifier implements MessageNotifier {
ThreadDatabase threads = DatabaseFactory.getThreadDatabase(context);
if (isVisible) {
List<MarkedMessageInfo> messageIds = threads.setRead(threadId, false);
MarkReadReceiver.process(context, messageIds);
}
if (!TextSecurePreferences.isNotificationsEnabled(context)) {
return;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 B

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="#FF000000"
android:pathData="M10,15.5l-8,-7.979l1.059,-1.062l6.941,6.923l6.941,-6.923l1.059,1.062l-8,7.979z"/>
</vector>

View File

@ -1,13 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android">
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:top="2px"
android:bottom="2px">
android:bottom="2px"
android:top="2px">
<shape android:shape="rectangle">
<corners android:radius="@dimen/message_corner_radius"/>
<corners android:radius="@dimen/message_corner_radius" />
<solid android:color="@color/white" />
</shape>
</item>
</layer-list>
</layer-list>

View File

@ -1,17 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android">
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:top="2px"
android:bottom="2px">
android:bottom="2px"
android:top="2px">
<shape android:shape="rectangle">
<corners
android:topLeftRadius="@dimen/message_corner_collapse_radius"
android:topRightRadius="@dimen/message_corner_radius"
android:bottomLeftRadius="@dimen/message_corner_radius"
android:bottomRightRadius="@dimen/message_corner_radius"
android:bottomLeftRadius="@dimen/message_corner_radius" />
android:topLeftRadius="@dimen/message_corner_collapse_radius"
android:topRightRadius="@dimen/message_corner_radius" />
<solid android:color="@color/white" />
</shape>
</item>
</layer-list>
</layer-list>

View File

@ -1,16 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android">
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:top="2px"
android:bottom="2px">
android:bottom="2px"
android:top="2px">
<shape android:shape="rectangle">
<corners
android:topLeftRadius="@dimen/message_corner_collapse_radius"
android:topRightRadius="@dimen/message_corner_radius"
android:bottomLeftRadius="@dimen/message_corner_collapse_radius"
android:bottomRightRadius="@dimen/message_corner_radius"
android:bottomLeftRadius="@dimen/message_corner_collapse_radius" />
android:topLeftRadius="@dimen/message_corner_collapse_radius"
android:topRightRadius="@dimen/message_corner_radius" />
<solid android:color="@color/white" />
</shape>
</item>

View File

@ -1,16 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android">
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:top="2px"
android:bottom="2px">
android:bottom="2px"
android:top="2px">
<shape android:shape="rectangle">
<corners
android:topLeftRadius="@dimen/message_corner_radius"
android:topRightRadius="@dimen/message_corner_radius"
android:bottomLeftRadius="@dimen/message_corner_collapse_radius"
android:bottomRightRadius="@dimen/message_corner_radius"
android:bottomLeftRadius="@dimen/message_corner_collapse_radius" />
android:topLeftRadius="@dimen/message_corner_radius"
android:topRightRadius="@dimen/message_corner_radius" />
<solid android:color="@color/white" />
</shape>
</item>

View File

@ -1,61 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="match_parent">
<include layout="@layout/conversation_item_banner"
<include
android:id="@+id/empty_conversation_banner"
layout="@layout/conversation_item_banner"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="2dp"
android:scrollbars="vertical"
android:cacheColorHint="?conversation_background"
android:clipChildren="false"
android:clipToPadding="false"/>
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" />
<TextView android:id="@+id/scroll_date_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_gravity="center_horizontal|top"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:layout_marginTop="8dp"
style="@style/Signal.Text.Caption"
android:textColor="?attr/conversation_item_sticky_date_text_color"
android:background="?attr/conversation_item_sticky_date_background"
android:elevation="9dp"
android:visibility="gone"
tools:text="March 1, 2015" />
<TextView
android:id="@+id/scroll_date_header"
style="@style/Signal.Text.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="?attr/conversation_item_sticky_date_background"
android:elevation="9dp"
android:gravity="center"
android:paddingStart="12dp"
android:paddingTop="4dp"
android:paddingEnd="12dp"
android:paddingBottom="4dp"
android:textColor="?attr/conversation_item_sticky_date_text_color"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="March 1, 2015" />
<View android:id="@+id/compose_divider"
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_gravity="bottom"
android:background="@drawable/compose_divider_background"
android:alpha="1"
android:visibility="invisible" />
<View
android:id="@+id/compose_divider"
android:layout_width="match_parent"
android:layout_height="2dp"
android:alpha="1"
android:background="@drawable/compose_divider_background"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent" />
<ImageButton
android:id="@+id/scroll_to_bottom_button"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginBottom="20dp"
android:padding="5dp"
android:layout_gravity="bottom|end"
android:background="?attr/conversation_scroll_to_bottom_background"
android:tint="?attr/conversation_scroll_to_bottom_foreground_color"
android:elevation="1dp"
android:contentDescription="@string/conversation_fragment__scroll_to_the_bottom_content_description"
android:src="@drawable/ic_scroll_down"/>
</FrameLayout>
<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_24"
app:layout_constraintBottom_toTopOf="@id/scroll_to_bottom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_goneMarginBottom="20dp" />
<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_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:parentTag="android.widget.FrameLayout">
<ImageButton
android:id="@+id/conversation_scroll_to_button"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="bottom|center"
android:layout_marginBottom="4dp"
android:background="?attr/conversation_scroll_to_bottom_background"
android:contentDescription="@string/conversation_fragment__scroll_to_the_bottom_content_description"
android:elevation="1dp"
android:scaleType="center"
android:tint="?attr/conversation_scroll_to_bottom_foreground_color"
tools:src="@drawable/ic_chevron_down_20" />
<TextView
android:id="@+id/conversation_scroll_to_count"
style="@style/Signal.Text.Caption"
android:layout_width="wrap_content"
android:layout_height="16dp"
android:layout_gravity="top|center"
android:layout_marginBottom="26dp"
android:background="?attr/conversation_list_item_unread_background"
android:elevation="1dp"
android:gravity="center"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:textColor="@color/core_white"
android:textSize="12dp"
tools:ignore="SpUsage"
tools:text="999+" />
</merge>

View File

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

View File

@ -152,6 +152,7 @@
<attr name="conversation_item_image_outline_color" format="color" />
<attr name="conversation_item_reveal_viewed_background_color" format="color" />
<attr name="conversation_item_delete_for_everyone_text_color" format="color" />
<attr name="conversation_item_mention_pulse_color" format="color" />
<attr name="conversation_scroll_to_bottom_background" format="reference" />
<attr name="conversation_scroll_to_bottom_foreground_color" format="color" />
@ -559,4 +560,8 @@
<attr name="state_speaker_selected" format="boolean" />
<attr name="state_handset_selected" format="boolean" />
</declare-styleable>
<declare-styleable name="ConversationScrollToView">
<attr name="cstv_scroll_button_src" format="reference" />
</declare-styleable>
</resources>

View File

@ -332,6 +332,7 @@
<item name="conversation_item_image_outline_color">@color/transparent_black_20</item>
<item name="conversation_item_reveal_viewed_background_color">?conversation_background</item>
<item name="conversation_item_delete_for_everyone_text_color">@color/core_grey_90</item>
<item name="conversation_item_mention_pulse_color">@color/transparent_black</item>
<item name="conversation_scroll_to_bottom_background">@drawable/scroll_to_bottom_background_light</item>
<item name="conversation_scroll_to_bottom_foreground_color">@color/grey_600</item>
@ -568,6 +569,7 @@
<item name="conversation_item_image_outline_color">@color/transparent_white_20</item>
<item name="conversation_item_reveal_viewed_background_color">?conversation_background</item>
<item name="conversation_item_delete_for_everyone_text_color">@color/core_grey_15</item>
<item name="conversation_item_mention_pulse_color">@color/transparent</item>
<item name="safety_number_change_dialog_button_background">@color/core_grey_75</item>
<item name="safety_number_change_dialog_button_text_color">@color/core_grey_05</item>