Add mentions unread counter.
This commit is contained in:
parent
3c90dfa660
commit
06eadd0c15
30 changed files with 588 additions and 165 deletions
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -387,6 +387,7 @@ public class ThreadDatabase extends Database {
|
|||
db.endTransaction();
|
||||
}
|
||||
|
||||
notifyConversationListeners(new HashSet<>(threadIds));
|
||||
notifyConversationListListeners();
|
||||
return Util.concatenatedList(smsRecords, mmsRecords);
|
||||
}
|
||||
|
|
|
@ -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 |
9
app/src/main/res/drawable/ic_at_24.xml
Normal file
9
app/src/main/res/drawable/ic_at_24.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M22,10.88c0,3.93 -1.74,6.55 -4.62,6.55a2.6,2.6 0,0 1,-2.75 -2.06h-0.1a3.21,3.21 0,0 1,-3.11 2c-2.56,0 -4.28,-2.1 -4.28,-5.22S8.9,7 11.42,7a3.22,3.22 0,0 1,3 1.73h0.1V7.26h1.85v7.21a1.25,1.25 0,0 0,1.39 1.36c1.46,0 2.54,-1.79 2.54,-5 0,-4.46 -3.39,-7.47 -8.28,-7.47S3.74,7 3.74,12.16c0,5.56 3.71,8.62 8.65,8.62a11.93,11.93 0,0 0,4.11 -0.59v1.55a13,13 0,0 1,-4.14 0.6C6.37,22.34 2,18.69 2,12.09 2,6 6.18,1.85 12.15,1.85 17.86,1.85 22,5.47 22,10.88ZM9.11,12.15c0,2.13 1,3.44 2.59,3.44s2.76,-1.34 2.76,-3.44 -1.07,-3.43 -2.74,-3.43S9.11,10 9.11,12.15Z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_chevron_down_20.xml
Normal file
9
app/src/main/res/drawable/ic_chevron_down_20.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
38
app/src/main/res/layout/conversation_scroll_to.xml
Normal file
38
app/src/main/res/layout/conversation_scroll_to.xml
Normal 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>
|
|
@ -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"/>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue