Add the ability to see replies.

fork-5.53.8
Greyson Parrelli 2022-06-20 16:20:03 -04:00 zatwierdzone przez Cody Henthorne
rodzic ee4f3abf22
commit 6ec7834046
28 zmienionych plików z 832 dodań i 91 usunięć

Wyświetl plik

@ -46,7 +46,8 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
boolean hasWallpaper,
boolean isMessageRequestAccepted,
boolean canPlayInline,
@NonNull Colorizer colorizer);
@NonNull Colorizer colorizer,
boolean isCondensedMode);
@NonNull ConversationMessage getConversationMessage();
@ -61,12 +62,13 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
}
default void updateSelectedState() {
// Intentionall Blank.
// Intentionally Blank.
}
interface EventListener {
void onQuoteClicked(MmsMessageRecord messageRecord);
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
void onQuotedIndicatorClicked(@NonNull MessageRecord messageRecord);
void onMoreTextClicked(@NonNull RecipientId conversationRecipientId, long messageId, boolean isMms);
void onStickerClicked(@NonNull StickerLocator stickerLocator);
void onViewOnceMessageClicked(@NonNull MmsMessageRecord messageRecord);

Wyświetl plik

@ -10,6 +10,7 @@ import android.widget.ImageView;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.UiThread;
import org.thoughtcrime.securesms.R;
@ -35,6 +36,7 @@ public class ConversationItemThumbnail extends FrameLayout {
private int[] normalBounds;
private int[] gifBounds;
private int minimumThumbnailWidth;
private int maximumThumbnailHeight;
public ConversationItemThumbnail(Context context) {
super(context);
@ -83,7 +85,8 @@ public class ConversationItemThumbnail extends FrameLayout {
Integer.MAX_VALUE
};
minimumThumbnailWidth = -1;
minimumThumbnailWidth = -1;
maximumThumbnailHeight = -1;
}
@SuppressWarnings("SuspiciousNameCombination")
@ -143,11 +146,16 @@ public class ConversationItemThumbnail extends FrameLayout {
cornerMask.setRadii(topLeft, topRight, bottomRight, bottomLeft);
}
public void setMinimumThumbnailWidth(int width) {
public void setMinimumThumbnailWidth(@Px int width) {
minimumThumbnailWidth = width;
thumbnail.setMinimumThumbnailWidth(width);
}
public void setMaximumThumbnailHeight(@Px int height) {
maximumThumbnailHeight = height;
thumbnail.setMaximumThumbnailHeight(height);
}
public void setBorderless(boolean borderless) {
this.borderless = borderless;
}
@ -170,6 +178,10 @@ public class ConversationItemThumbnail extends FrameLayout {
if (minimumThumbnailWidth != -1) {
thumbnail.setMinimumThumbnailWidth(minimumThumbnailWidth);
}
if (maximumThumbnailHeight != -1) {
thumbnail.setMaximumThumbnailHeight(maximumThumbnailHeight);
}
}
thumbnail.setVisibility(VISIBLE);

Wyświetl plik

@ -128,6 +128,10 @@ public class LinkPreviewView extends FrameLayout {
}
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
setLinkPreview(glideRequests, linkPreview, showThumbnail, true);
}
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail, boolean showDescription) {
spinner.setVisibility(GONE);
noPreview.setVisibility(GONE);
@ -138,7 +142,7 @@ public class LinkPreviewView extends FrameLayout {
title.setVisibility(GONE);
}
if (!Util.isEmpty(linkPreview.getDescription())) {
if (showDescription && !Util.isEmpty(linkPreview.getDescription())) {
description.setText(linkPreview.getDescription());
description.setVisibility(VISIBLE);
} else {

Wyświetl plik

@ -14,6 +14,7 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Px;
import androidx.annotation.UiThread;
import com.bumptech.glide.RequestBuilder;
@ -155,11 +156,16 @@ public class ThumbnailView extends FrameLayout {
captionIcon.setScaleY(captionIconScale);
}
public void setMinimumThumbnailWidth(int width) {
public void setMinimumThumbnailWidth(@Px int width) {
bounds[MIN_WIDTH] = width;
invalidate();
}
public void setMaximumThumbnailHeight(@Px int height) {
bounds[MAX_HEIGHT] = height;
invalidate();
}
@SuppressWarnings("SuspiciousNameCombination")
private void fillTargetDimensions(int[] targetDimens, int[] dimens, int[] bounds) {
int dimensFilledCount = getNonZeroCount(dimens);

Wyświetl plik

@ -26,6 +26,7 @@ import androidx.core.content.ContextCompat;
import androidx.core.view.ViewKt;
import androidx.core.widget.TextViewCompat;
import org.signal.core.util.StringUtil;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
@ -151,10 +152,10 @@ public class EmojiTextView extends AppCompatTextView {
// Android fails to ellipsize spannable strings. (https://issuetracker.google.com/issues/36991688)
// We ellipsize them ourselves by manually truncating the appropriate section.
if (getText() != null && getText().length() > 0 && isEllipsizedAtEnd()) {
if (maxLength > 0) {
ellipsizeAnyTextForMaxLength();
} else if (getMaxLines() > 0) {
if (getMaxLines() > 0) {
ellipsizeEmojiTextForMaxLines();
} else if (maxLength > 0) {
ellipsizeAnyTextForMaxLength();
}
}
@ -308,11 +309,17 @@ public class EmojiTextView extends AppCompatTextView {
int lineCount = getLineCount();
if (lineCount > maxLines) {
int overflowStart = getLayout().getLineStart(maxLines - 1);
int overflowStart = getLayout().getLineStart(maxLines - 1);
if (maxLength > 0 && overflowStart > maxLength) {
ellipsizeAnyTextForMaxLength();
return;
}
int overflowEnd = getLayout().getLineEnd(maxLines - 1);
CharSequence overflow = getText().subSequence(overflowStart, overflowEnd);
float adjust = overflowText != null ? getPaint().measureText(overflowText, 0, overflowText.length()) : 0f;
CharSequence ellipsized = TextUtils.ellipsize(overflow, getPaint(), getWidth() - adjust, TextUtils.TruncateAt.END);
CharSequence ellipsized = StringUtil.trim(TextUtils.ellipsize(overflow, getPaint(), getWidth() - adjust, TextUtils.TruncateAt.END));
SpannableStringBuilder newContent = new SpannableStringBuilder();
newContent.append(getText().subSequence(0, overflowStart))
@ -323,6 +330,8 @@ public class EmojiTextView extends AppCompatTextView {
CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this, isJumbomoji || forceJumboEmoji);
super.setText(emojified, BufferType.SPANNABLE);
} else if (maxLength > 0) {
ellipsizeAnyTextForMaxLength();
}
};

Wyświetl plik

@ -123,8 +123,9 @@ public class ConversationAdapter
private ConversationMessage inlineContent;
private Colorizer colorizer;
private boolean isTypingViewEnabled;
private boolean condensedMode;
ConversationAdapter(@NonNull Context context,
public ConversationAdapter(@NonNull Context context,
@NonNull LifecycleOwner lifecycleOwner,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@ -177,9 +178,9 @@ public class ConversationAdapter
} else if (messageRecord.isUpdate()) {
return MESSAGE_TYPE_UPDATE;
} else if (messageRecord.isOutgoing()) {
return MessageRecordUtil.isTextOnly(messageRecord, context) ? MESSAGE_TYPE_OUTGOING_TEXT : MESSAGE_TYPE_OUTGOING_MULTIMEDIA;
return MessageRecordUtil.isTextOnly(messageRecord, context) && !conversationMessage.hasBeenQuoted() ? MESSAGE_TYPE_OUTGOING_TEXT : MESSAGE_TYPE_OUTGOING_MULTIMEDIA;
} else {
return MessageRecordUtil.isTextOnly(messageRecord, context) ? MESSAGE_TYPE_INCOMING_TEXT : MESSAGE_TYPE_INCOMING_MULTIMEDIA;
return MessageRecordUtil.isTextOnly(messageRecord, context) && !conversationMessage.hasBeenQuoted() ? MESSAGE_TYPE_INCOMING_TEXT : MESSAGE_TYPE_INCOMING_MULTIMEDIA;
}
}
@ -259,6 +260,11 @@ public class ConversationAdapter
}
}
public void setCondensedMode(boolean condensedMode) {
this.condensedMode = condensedMode;
notifyDataSetChanged();
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
switch (getItemViewType(position)) {
@ -284,10 +290,11 @@ public class ConversationAdapter
recipient,
searchQuery,
conversationMessage == recordToPulse,
hasWallpaper,
hasWallpaper && !condensedMode,
isMessageRequestAccepted,
conversationMessage == inlineContent,
colorizer);
colorizer,
condensedMode);
if (conversationMessage == recordToPulse) {
recordToPulse = null;
@ -776,7 +783,7 @@ public class ConversationAdapter
}
}
interface ItemClickListener extends BindableConversationItem.EventListener {
public interface ItemClickListener extends BindableConversationItem.EventListener {
void onItemClick(MultiselectPart item);
void onItemLongClick(View itemView, MultiselectPart item);
}

Wyświetl plik

@ -25,7 +25,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Rect;
import android.net.Uri;
import android.os.AsyncTask;
@ -49,7 +48,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.WindowDecorActionBar;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
@ -104,6 +102,7 @@ import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardBottomSheet;
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment;
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs;
import org.thoughtcrime.securesms.conversation.quotes.MessageQuotesBottomSheet;
import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSettingsDialog;
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
import org.thoughtcrime.securesms.database.MessageDatabase;
@ -112,6 +111,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
@ -201,7 +201,7 @@ import java.util.concurrent.ExecutionException;
import kotlin.Unit;
@SuppressLint("StaticFieldLeak")
public class ConversationFragment extends LoggingFragment implements MultiselectForwardBottomSheet.Callback {
public class ConversationFragment extends LoggingFragment implements MultiselectForwardBottomSheet.Callback, MessageQuotesBottomSheet.Callback {
private static final String TAG = Log.tag(ConversationFragment.class);
private static final int SCROLL_ANIMATION_THRESHOLD = 50;
@ -1426,6 +1426,22 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
return true;
}
@Override
public @NonNull ItemClickListener getConversationAdapterListener() {
return selectionClickListener;
}
@Override
public void jumpToMessage(@NonNull MessageRecord messageRecord) {
SimpleTask.run(getLifecycle(), () -> {
return SignalDatabase.mmsSms().getMessagePositionInConversation(threadId,
messageRecord.getDateReceived(),
messageRecord.isOutgoing() ? Recipient.self().getId() : messageRecord.getRecipient().getId());
}, p -> moveToPosition(p + (isTypingIndicatorShowing() ? 1 : 0), () -> {
Toast.makeText(getContext(), R.string.ConversationFragment_failed_to_open_message, Toast.LENGTH_SHORT).show();
}));
}
public interface ConversationFragmentListener extends VoiceNoteMediaControllerOwner {
int getSendButtonTint();
boolean isKeyboardOpen();
@ -1704,6 +1720,17 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
}
}
@Override
public void onQuotedIndicatorClicked(@NonNull MessageRecord messageRecord) {
if (getContext() != null && getActivity() != null) {
MessageQuotesBottomSheet.show(
getChildFragmentManager(),
new MessageId(messageRecord.getId(), messageRecord.isMms()),
recipient.getId()
);
}
}
@Override
public void onMoreTextClicked(@NonNull RecipientId conversationRecipientId, long messageId, boolean isMms) {
if (getContext() != null && getActivity() != null) {

Wyświetl plik

@ -173,6 +173,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
public static final float LONG_PRESS_SCALE_FACTOR = 0.95f;
private static final int SHRINK_BUBBLE_DELAY_MILLIS = 100;
private static final long MAX_CLUSTERING_TIME_DIFF = TimeUnit.MINUTES.toMillis(3);
private static final int CONDENSED_MODE_MAX_LINES = 3;
private ConversationMessage conversationMessage;
private MessageRecord messageRecord;
@ -182,6 +183,13 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private LiveRecipient recipient;
private GlideRequests glideRequests;
private ValueAnimator pulseOutlinerAlphaAnimator;
private Optional<MessageRecord> previousMessage;
/**
* Whether or not we're rendering this item in a constrained space.
* Today this is only {@link org.thoughtcrime.securesms.conversation.quotes.MessageQuotesBottomSheet}.
*/
private boolean isCondensedMode;
protected ConversationItemBodyBubble bodyBubble;
protected View reply;
@ -199,6 +207,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
protected BadgeImageView badgeImageView;
private View storyReactionLabelWrapper;
private TextView storyReactionLabel;
protected View quotedIndicator;
private @NonNull Set<MultiselectPart> batchSelected = new HashSet<>();
private @NonNull Outliner outliner = new Outliner();
@ -228,6 +237,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private final SharedContactClickListener sharedContactClickListener = new SharedContactClickListener();
private final LinkPreviewClickListener linkPreviewClickListener = new LinkPreviewClickListener();
private final ViewOnceMessageClickListener revealableClickListener = new ViewOnceMessageClickListener();
private final QuotedIndicatorClickListener quotedIndicatorClickListener = new QuotedIndicatorClickListener();
private final UrlClickListener urlClickListener = new UrlClickListener();
private final Rect thumbnailMaskingRect = new Rect();
private final TouchDelegateChangedListener touchDelegateChangedListener = new TouchDelegateChangedListener();
@ -259,6 +269,12 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
reactionsView.animate()
.scaleX(LONG_PRESS_SCALE_FACTOR)
.scaleY(LONG_PRESS_SCALE_FACTOR);
if (quotedIndicator != null) {
quotedIndicator.animate()
.scaleX(LONG_PRESS_SCALE_FACTOR)
.scaleY(LONG_PRESS_SCALE_FACTOR);
}
}
};
@ -307,6 +323,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
this.storyReactionLabelWrapper = findViewById(R.id.story_reacted_label_holder);
this.storyReactionLabel = findViewById(R.id.story_reacted_label);
this.giftViewStub = new Stub<>(findViewById(R.id.gift_view_stub));
this.quotedIndicator = findViewById(R.id.quoted_indicator);
setOnClickListener(new ClickListener(null));
@ -329,7 +346,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
boolean hasWallpaper,
boolean isMessageRequestAccepted,
boolean allowedToPlayInline,
@NonNull Colorizer colorizer)
@NonNull Colorizer colorizer,
boolean isCondensedMode)
{
if (this.recipient != null) this.recipient.removeForeverObserver(this);
if (this.conversationRecipient != null) this.conversationRecipient.removeForeverObserver(this);
@ -350,6 +368,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
this.canPlayContent = false;
this.mediaItem = null;
this.colorizer = colorizer;
this.isCondensedMode = isCondensedMode;
this.previousMessage = previousMessageRecord;
this.recipient.observeForever(this);
this.conversationRecipient.observeForever(this);
@ -370,6 +390,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
setReactions(messageRecord);
setFooter(messageRecord, nextMessageRecord, locale, groupThread, hasWallpaper);
setStoryReactionLabel(messageRecord);
setHasBeenQuoted(conversationMessage);
if (audioViewStub.resolved()) {
audioViewStub.get().setOnLongClickListener(passthroughClickListener);
@ -388,6 +409,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (isCondensedMode) return super.dispatchTouchEvent(ev);
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getHandler().postDelayed(shrinkBubble, SHRINK_BUBBLE_DELAY_MILLIS);
@ -401,6 +424,12 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
reactionsView.animate()
.scaleX(1.0f)
.scaleY(1.0f);
if (quotedIndicator != null) {
quotedIndicator.animate()
.scaleX(1.0f)
.scaleY(1.0f);
}
break;
}
@ -864,6 +893,14 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
/**
* Whether or not we want to condense the actual content of the bubble. e.g. shorten image height, text content, etc.
* Today, we only want to do this for the first message when we're in condensed mode.
*/
private boolean isContentCondensed() {
return isCondensedMode && !previousMessage.isPresent();
}
private boolean isStoryReaction(MessageRecord messageRecord) {
return MessageRecordUtil.isStoryReaction(messageRecord);
}
@ -901,11 +938,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
private boolean hasExtraText(MessageRecord messageRecord) {
return MessageRecordUtil.hasExtraText(messageRecord);
return MessageRecordUtil.hasExtraText(messageRecord) || isContentCondensed();
}
private boolean hasQuote(MessageRecord messageRecord) {
return MessageRecordUtil.hasQuote(messageRecord);
return MessageRecordUtil.hasQuote(messageRecord) && (!isCondensedMode || !previousMessage.isPresent());
}
private boolean hasSharedContact(MessageRecord messageRecord) {
@ -917,7 +954,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
private boolean hasBigImageLinkPreview(MessageRecord messageRecord) {
return MessageRecordUtil.hasBigImageLinkPreview(messageRecord, context);
return MessageRecordUtil.hasBigImageLinkPreview(messageRecord, context) && !isContentCondensed();
}
private boolean isViewOnceMessage(MessageRecord messageRecord) {
@ -971,6 +1008,12 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
bodyText.setMentionBackgroundTint(ContextCompat.getColor(context, ThemeUtil.isDarkTheme(context) ? R.color.core_grey_60 : R.color.core_grey_20));
}
if (isContentCondensed()) {
bodyText.setMaxLines(CONDENSED_MODE_MAX_LINES);
} else {
bodyText.setMaxLines(Integer.MAX_VALUE);
}
bodyText.setText(StringUtil.trim(styledText));
bodyText.setVisibility(View.VISIBLE);
@ -1063,6 +1106,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
if (hasBigImageLinkPreview(messageRecord)) {
mediaThumbnailStub.require().setVisibility(VISIBLE);
mediaThumbnailStub.require().setMinimumThumbnailWidth(readDimen(R.dimen.media_bubble_min_width_with_content));
mediaThumbnailStub.require().setMaximumThumbnailHeight(readDimen(R.dimen.media_bubble_max_height));
mediaThumbnailStub.require().setImageResource(glideRequests, Collections.singletonList(new ImageSlide(context, linkPreview.getThumbnail().get())), showControls, false);
mediaThumbnailStub.require().setThumbnailClickListener(new LinkPreviewThumbnailClickListener());
mediaThumbnailStub.require().setDownloadClickListener(downloadClickListener);
@ -1077,7 +1121,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.setTopMargin(linkPreviewStub.get(), 0);
} else {
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, true);
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, true, !isContentCondensed());
linkPreviewStub.get().setDownloadClickedListener(downloadClickListener);
setLinkPreviewCorners(messageRecord, previousRecord, nextRecord, isGroupThread, false);
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
@ -1180,11 +1224,13 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE);
if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE);
List<Slide> thumbnailSlides = ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlides();
mediaThumbnailStub.require().setMinimumThumbnailWidth(readDimen(isCaptionlessMms(messageRecord) ? R.dimen.media_bubble_min_width_solo
: R.dimen.media_bubble_min_width_with_content));
: R.dimen.media_bubble_min_width_with_content));
mediaThumbnailStub.require().setMaximumThumbnailHeight(readDimen(isCondensedMode ? R.dimen.media_bubble_max_height_condensed
: R.dimen.media_bubble_max_height));
mediaThumbnailStub.require().setImageResource(glideRequests,
thumbnailSlides,
showControls,
@ -1453,7 +1499,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private void setQuote(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread, @NonNull ChatColors chatColors) {
boolean startOfCluster = isStartOfMessageCluster(current, previous, isGroupThread);
if (current.isMms() && !current.isMmsNotification() && ((MediaMmsMessageRecord)current).getQuote() != null) {
if (hasQuote(messageRecord)) {
if (quoteView == null) {
throw new AssertionError();
}
@ -1622,6 +1668,16 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
private void setHasBeenQuoted(@NonNull ConversationMessage message) {
if (message.hasBeenQuoted() && quotedIndicator != null) {
quotedIndicator.setVisibility(VISIBLE);
quotedIndicator.setOnClickListener(quotedIndicatorClickListener);
} else if (quotedIndicator != null) {
quotedIndicator.setVisibility(GONE);
quotedIndicator.setOnClickListener(null);
}
}
private boolean forceFooter(@NonNull MessageRecord messageRecord) {
return hasAudio(messageRecord);
}
@ -2169,6 +2225,16 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
private class QuotedIndicatorClickListener implements View.OnClickListener {
public void onClick(final View view) {
if (eventListener != null && batchSelected.isEmpty() && conversationMessage.hasBeenQuoted()) {
eventListener.onQuotedIndicatorClicked((messageRecord));
} else {
passthroughClickListener.onClick(view);
}
}
}
private class AttachmentDownloadClickListener implements SlidesClickedListener {
@Override
public void onClick(View v, final List<Slide> slides) {

Wyświetl plik

@ -32,16 +32,23 @@ public class ConversationMessage {
@Nullable private final SpannableString body;
@NonNull private final MultiselectCollection multiselectCollection;
@NonNull private final MessageStyler.Result styleResult;
private final int quotedCount;
private ConversationMessage(@NonNull MessageRecord messageRecord) {
this(messageRecord, null, null);
this(messageRecord, null, null, 0);
}
private ConversationMessage(@NonNull MessageRecord messageRecord, int quotedCount) {
this(messageRecord, null, null, quotedCount);
}
private ConversationMessage(@NonNull MessageRecord messageRecord,
@Nullable CharSequence body,
@Nullable List<Mention> mentions)
@Nullable List<Mention> mentions,
int quotedCount)
{
this.messageRecord = messageRecord;
this.quotedCount = quotedCount;
this.mentions = mentions != null ? mentions : Collections.emptyList();
if (body != null) {
@ -77,6 +84,14 @@ public class ConversationMessage {
return multiselectCollection;
}
public boolean hasBeenQuoted() {
return quotedCount > 0;
}
public int getQuoteCount() {
return quotedCount;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@ -119,8 +134,8 @@ public class ConversationMessage {
* heavy work performed as the message is assumed to not have any mentions.
*/
@AnyThread
public static @NonNull ConversationMessage createWithResolvedData(@NonNull MessageRecord messageRecord) {
return new ConversationMessage(messageRecord);
public static @NonNull ConversationMessage createWithResolvedData(@NonNull MessageRecord messageRecord, int quotedCount) {
return new ConversationMessage(messageRecord, quotedCount);
}
/**
@ -128,15 +143,16 @@ public class ConversationMessage {
* list of actual mentions. No database or heavy work performed as the body and mentions are assumed to be
* fully updated with display names.
*
* @param body Contains appropriate {@link MentionAnnotation}s and is updated with actual profile names.
* @param mentions List of actual mentions (i.e., not placeholder) matching annotation ranges in body.
* @param body Contains appropriate {@link MentionAnnotation}s and is updated with actual profile names.
* @param mentions List of actual mentions (i.e., not placeholder) matching annotation ranges in body.
* @param quotedCount The number of times a message has been quoted
*/
@AnyThread
public static @NonNull ConversationMessage createWithResolvedData(@NonNull MessageRecord messageRecord, @Nullable CharSequence body, @Nullable List<Mention> mentions) {
public static @NonNull ConversationMessage createWithResolvedData(@NonNull MessageRecord messageRecord, @Nullable CharSequence body, @Nullable List<Mention> mentions, int quotedCount) {
if (messageRecord.isMms() && mentions != null && !mentions.isEmpty()) {
return new ConversationMessage(messageRecord, body, mentions);
return new ConversationMessage(messageRecord, body, mentions, quotedCount);
}
return new ConversationMessage(messageRecord, body, null);
return new ConversationMessage(messageRecord, body, null, quotedCount);
}
/**
@ -147,11 +163,13 @@ public class ConversationMessage {
*/
@WorkerThread
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord, @Nullable List<Mention> mentions) {
int quotedCount = SignalDatabase.mmsSms().getQuotedCount(messageRecord);
if (messageRecord.isMms() && mentions != null && !mentions.isEmpty()) {
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, messageRecord, mentions);
return new ConversationMessage(messageRecord, updated.getBody(), updated.getMentions());
return new ConversationMessage(messageRecord, updated.getBody(), updated.getMentions(), quotedCount);
}
return createWithResolvedData(messageRecord);
return createWithResolvedData(messageRecord, quotedCount);
}
/**
@ -171,14 +189,33 @@ public class ConversationMessage {
*/
@WorkerThread
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull CharSequence body) {
int quotedCount = SignalDatabase.mmsSms().getQuotedCount(messageRecord);
if (messageRecord.isMms()) {
List<Mention> mentions = SignalDatabase.mentions().getMentionsForMessage(messageRecord.getId());
if (!mentions.isEmpty()) {
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, body, mentions);
return new ConversationMessage(messageRecord, updated.getBody(), updated.getMentions());
return new ConversationMessage(messageRecord, updated.getBody(), updated.getMentions(), quotedCount);
}
}
return createWithResolvedData(messageRecord, body, null);
return createWithResolvedData(messageRecord, body, null, quotedCount);
}
/**
* Creates a {@link ConversationMessage} wrapping the provided MessageRecord and body, and will query for potential mentions. If mentions
* are found, the body of the provided message will be updated and modified to match actual mentions. This will perform
* database operations to query for mentions and then to resolve mentions to display names.
*/
@WorkerThread
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull CharSequence body, int quotedCount) {
if (messageRecord.isMms()) {
List<Mention> mentions = SignalDatabase.mentions().getMentionsForMessage(messageRecord.getId());
if (!mentions.isEmpty()) {
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, body, mentions);
return new ConversationMessage(messageRecord, updated.getBody(), updated.getMentions(), quotedCount);
}
}
return createWithResolvedData(messageRecord, body, null, quotedCount);
}
}
}

Wyświetl plik

@ -25,6 +25,7 @@ final class ConversationSwipeAnimationHelper {
private static final Interpolator REPLY_TRANSITION_INTERPOLATOR = new ClampingLinearInterpolator(0f, dpToPx(10));
private static final Interpolator AVATAR_INTERPOLATOR = new ClampingLinearInterpolator(0f, dpToPx(8));
private static final Interpolator REPLY_SCALE_INTERPOLATOR = new ClampingLinearInterpolator(REPLY_SCALE_MIN, REPLY_SCALE_MAX);
private static final Interpolator QUOTED_ALPHA_INTERPOLATOR = new ClampingLinearInterpolator(1f, 0f, 3f);
private ConversationSwipeAnimationHelper() {
}
@ -34,6 +35,7 @@ final class ConversationSwipeAnimationHelper {
updateBodyBubbleTransition(conversationItem.bodyBubble, dx, sign);
updateReactionsTransition(conversationItem.reactionsView, dx, sign);
updateQuotedIndicatorTransition(conversationItem.quotedIndicator, dx, progress, sign);
updateReplyIconTransition(conversationItem.reply, dx, progress, sign);
updateContactPhotoHolderTransition(conversationItem.contactPhotoHolder, progress, sign);
updateContactPhotoHolderTransition(conversationItem.badgeImageView, progress, sign);
@ -51,6 +53,13 @@ final class ConversationSwipeAnimationHelper {
reactionsContainer.setTranslationX(BUBBLE_INTERPOLATOR.getInterpolation(dx) * sign);
}
private static void updateQuotedIndicatorTransition(@Nullable View quotedIndicator, float dx, float progress, float sign) {
if (quotedIndicator != null) {
quotedIndicator.setTranslationX(BUBBLE_INTERPOLATOR.getInterpolation(dx) * sign);
quotedIndicator.setAlpha(QUOTED_ALPHA_INTERPOLATOR.getInterpolation(progress) * sign);
}
}
private static void updateReplyIconTransition(@NonNull View replyIcon, float dx, float progress, float sign) {
if (progress > 0.05f) {
replyIcon.setAlpha(REPLY_ALPHA_INTERPOLATOR.getInterpolation(progress));

Wyświetl plik

@ -130,7 +130,8 @@ public final class ConversationUpdateItem extends FrameLayout
boolean hasWallpaper,
boolean isMessageRequestAccepted,
boolean allowedToPlayInline,
@NonNull Colorizer colorizer)
@NonNull Colorizer colorizer,
boolean isCondensedMode)
{
this.batchSelected = batchSelected;

Wyświetl plik

@ -13,12 +13,9 @@ import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import com.annimon.stream.Stream;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.MapUtil;
import org.signal.core.util.logging.Log;
import org.signal.paging.ObservablePagedData;
import org.signal.paging.PagedData;
@ -28,15 +25,12 @@ import org.signal.paging.ProxyPagingController;
import org.signal.libsignal.protocol.util.Pair;
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository;
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette;
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper;
import org.thoughtcrime.securesms.conversation.colors.NameColor;
import org.thoughtcrime.securesms.database.DatabaseObserver;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.StoryViewState;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mediasend.MediaRepository;
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile;
@ -53,19 +47,15 @@ import org.thoughtcrime.securesms.util.livedata.Store;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.BackpressureStrategy;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.BehaviorSubject;
@ -97,8 +87,7 @@ public class ConversationViewModel extends ViewModel {
private final Observer<ThreadAnimationState> threadAnimationStateStoreDriver;
private final NotificationProfilesRepository notificationProfilesRepository;
private final MutableLiveData<String> searchQuery;
private final Map<GroupId, Set<Recipient>> sessionMemberCache = new HashMap<>();
private final GroupAuthorNameColorHelper groupAuthorNameColorHelper;
private ConversationIntents.Args args;
private int jumpToPosition;
@ -123,6 +112,7 @@ public class ConversationViewModel extends ViewModel {
this.searchQuery = new MutableLiveData<>();
this.recipientId = BehaviorSubject.create();
this.threadId = BehaviorSubject.create();
this.groupAuthorNameColorHelper = new GroupAuthorNameColorHelper();
BehaviorSubject<Recipient> recipientCache = BehaviorSubject.create();
@ -345,35 +335,13 @@ public class ConversationViewModel extends ViewModel {
.observeOn(Schedulers.io())
.distinctUntilChanged()
.map(Recipient::resolved)
.map(Recipient::getGroupId)
.map(groupId -> {
if (groupId.isPresent()) {
List<Recipient> fullMembers = SignalDatabase.groups().getGroupMembers(groupId.get(), GroupDatabase.MemberSet.FULL_MEMBERS_INCLUDING_SELF);
Set<Recipient> cachedMembers = MapUtil.getOrDefault(sessionMemberCache, groupId.get(), new HashSet<>());
cachedMembers.addAll(fullMembers);
sessionMemberCache.put(groupId.get(), cachedMembers);
return cachedMembers;
.map(recipient -> {
if (recipient.getGroupId().isPresent()) {
return groupAuthorNameColorHelper.getColorMap(recipient.getGroupId().get());
} else {
return Collections.<Recipient>emptySet();
return Collections.<RecipientId, NameColor>emptyMap();
}
})
.map(members -> {
List<Recipient> sorted = Stream.of(members)
.filter(member -> !Objects.equals(member, Recipient.self()))
.sortBy(Recipient::requireStringId)
.toList();
List<NameColor> names = ChatColorsPalette.Names.getAll();
Map<RecipientId, NameColor> colors = new HashMap<>();
for (int i = 0; i < sorted.size(); i++) {
colors.put(sorted.get(i).getId(), names.get(i % names.size()));
}
return colors;
})
.observeOn(AndroidSchedulers.mainThread());
}

Wyświetl plik

@ -0,0 +1,47 @@
package org.thoughtcrime.securesms.conversation.colors
import androidx.annotation.NonNull
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
/**
* Class to assist managing the colors of author names in the UI in groups.
* We want to be able to map each group member to a color, and for that to
* remain constant throughout a "chat open lifecycle" (i.e. should never
* change while looking at a chat, but can change if you close and open).
*/
class GroupAuthorNameColorHelper {
/** Needed so that we have a full history of current *and* past members (so colors don't change when someone leaves) */
private val fullMemberCache: MutableMap<GroupId, Set<Recipient>> = mutableMapOf()
/**
* Given a [GroupId], returns a map of member -> name color.
*/
fun getColorMap(@NonNull groupId: GroupId): Map<RecipientId, NameColor> {
val dbMembers: Set<Recipient> = SignalDatabase
.groups
.getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_INCLUDING_SELF)
.toSet()
val cachedMembers: Set<Recipient> = fullMemberCache.getOrDefault(groupId, setOf())
val allMembers: Set<Recipient> = cachedMembers + dbMembers
fullMemberCache[groupId] = allMembers
val members: List<Recipient> = allMembers
.filter { member -> member != Recipient.self() }
.sortedBy { obj: Recipient -> obj.requireStringId() }
val allColors: List<NameColor> = ChatColorsPalette.Names.all
val colors: MutableMap<RecipientId, NameColor> = HashMap()
for (i in members.indices) {
colors[members[i].id] = allColors[i % allColors.size]
}
return colors.toMap()
}
}

Wyświetl plik

@ -0,0 +1,94 @@
package org.thoughtcrime.securesms.conversation.quotes
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.children
import androidx.core.view.marginEnd
import androidx.core.view.marginLeft
import androidx.core.view.marginStart
import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.ViewUtil
/**
* Serves as the separator between the original message and the messages that quote it in [MessageQuotesBottomSheet]
*/
class MessageQuoteHeaderDecoration(context: Context) : RecyclerView.ItemDecoration() {
private val dividerMargin = ViewUtil.dpToPx(context, 32)
private val dividerHeight = ViewUtil.dpToPx(context, 2)
private val dividerRect = Rect()
private val dividerPaint: Paint = Paint().apply {
style = Paint.Style.FILL
color = context.resources.getColor(R.color.signal_colorSurfaceVariant)
}
private var cachedHeader: View? = null
private val headerMargin = ViewUtil.dpToPx(24)
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val lastItem: View = parent.children.firstOrNull { child ->
parent.getChildAdapterPosition(child) == state.itemCount - 1
} ?: return
dividerRect.apply {
left = parent.left
top = lastItem.bottom + dividerMargin
right = parent.right
bottom = lastItem.bottom + dividerMargin + dividerHeight
}
canvas.drawRect(dividerRect, dividerPaint)
val header = getHeader(parent)
canvas.save()
canvas.translate((parent.left + header.marginLeft).toFloat(), (dividerRect.bottom + dividerMargin).toFloat())
header.draw(canvas)
canvas.restore()
}
private fun getHeader(parent: RecyclerView): View {
cachedHeader?.let {
return it
}
val header: View = LayoutInflater.from(parent.context).inflate(R.layout.message_quote_header_decoration, parent, false)
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width - header.marginStart - header.marginEnd, View.MeasureSpec.EXACTLY)
val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
val childWidth = ViewGroup.getChildMeasureSpec(
widthSpec,
parent.paddingLeft + parent.paddingRight,
header.layoutParams.width
)
val childHeight = ViewGroup.getChildMeasureSpec(
heightSpec,
parent.paddingTop + parent.paddingBottom,
header.layoutParams.height
)
header.measure(childWidth, childHeight)
header.layout(header.marginLeft, 0, header.measuredWidth, header.measuredHeight)
cachedHeader = header
return header
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
val currentPosition = parent.getChildAdapterPosition(view)
val lastPosition = state.itemCount - 1
if (currentPosition == lastPosition) {
outRect.bottom = ViewUtil.dpToPx(view.context, 110)
}
}
}

Wyświetl plik

@ -0,0 +1,223 @@
package org.thoughtcrime.securesms.conversation.quotes
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.doOnNextLayout
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager
import org.thoughtcrime.securesms.conversation.ConversationAdapter
import org.thoughtcrime.securesms.conversation.colors.Colorizer
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.StickyHeaderDecoration
import org.thoughtcrime.securesms.util.fragments.findListener
import java.util.Locale
class MessageQuotesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() {
override val peekHeightPercentage: Float = 0.66f
override val themeResId: Int = R.style.Widget_Signal_FixedRoundedCorners_Messages
private lateinit var messageAdapter: ConversationAdapter
private val viewModel: MessageQuotesViewModel by viewModels(
factoryProducer = {
val messageId = MessageId.deserialize(arguments?.getString(KEY_MESSAGE_ID, null) ?: throw IllegalArgumentException())
val conversationRecipientId = RecipientId.from(arguments?.getString(KEY_CONVERSATION_RECIPIENT_ID, null) ?: throw IllegalArgumentException())
MessageQuotesViewModel.Factory(ApplicationDependencies.getApplication(), messageId, conversationRecipientId)
}
)
private val disposables: LifecycleDisposable = LifecycleDisposable()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.message_quotes_bottom_sheet, container, false)
disposables.bindTo(viewLifecycleOwner)
return view
}
@SuppressLint("WrongThread")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val conversationRecipientId = RecipientId.from(arguments?.getString(KEY_CONVERSATION_RECIPIENT_ID, null) ?: throw IllegalArgumentException())
val conversationRecipient = Recipient.resolved(conversationRecipientId)
val colorizer = Colorizer()
messageAdapter = ConversationAdapter(requireContext(), viewLifecycleOwner, GlideApp.with(this), Locale.getDefault(), ConversationAdapterListener(), conversationRecipient, colorizer).apply {
setCondensedMode(true)
}
val list: RecyclerView = view.findViewById<RecyclerView>(R.id.quotes_list).apply {
layoutManager = SmoothScrollingLinearLayoutManager(requireContext(), true)
adapter = messageAdapter
itemAnimator = null
addItemDecoration(MessageQuoteHeaderDecoration(context))
doOnNextLayout {
// Adding this without waiting for a layout pass would result in an indeterminate amount of padding added to the top of the view
addItemDecoration(StickyHeaderDecoration(messageAdapter, false, false, ConversationAdapter.HEADER_TYPE_INLINE_DATE))
}
}
val recyclerViewColorizer = RecyclerViewColorizer(list)
disposables += viewModel.getMessages().subscribe { messages ->
messageAdapter.submitList(messages) {
(list.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(messages.size - 1, 100)
}
recyclerViewColorizer.setChatColors(conversationRecipient.chatColors)
}
disposables += viewModel.getNameColorsMap().subscribe { map ->
colorizer.onNameColorsChanged(map)
messageAdapter.notifyItemRangeChanged(0, messageAdapter.itemCount, ConversationAdapter.PAYLOAD_NAME_COLORS)
}
}
private fun getCallback(): Callback {
return findListener<Callback>() ?: throw IllegalStateException("Parent must implement callback interface!")
}
private fun getAdapterListener(): ConversationAdapter.ItemClickListener {
return getCallback().getConversationAdapterListener()
}
private inner class ConversationAdapterListener : ConversationAdapter.ItemClickListener by getAdapterListener() {
override fun onItemClick(item: MultiselectPart) {
dismiss()
getCallback().jumpToMessage(item.getMessageRecord())
}
override fun onItemLongClick(itemView: View, item: MultiselectPart) {
onItemClick(item)
}
override fun onQuoteClicked(messageRecord: MmsMessageRecord) {
dismiss()
getCallback().jumpToMessage(messageRecord)
}
override fun onLinkPreviewClicked(linkPreview: LinkPreview) {
dismiss()
getAdapterListener().onLinkPreviewClicked(linkPreview)
}
override fun onQuotedIndicatorClicked(messageRecord: MessageRecord) {
dismiss()
getAdapterListener().onQuotedIndicatorClicked(messageRecord)
}
override fun onReactionClicked(multiselectPart: MultiselectPart, messageId: Long, isMms: Boolean) {
dismiss()
getAdapterListener().onReactionClicked(multiselectPart, messageId, isMms)
}
override fun onGroupMemberClicked(recipientId: RecipientId, groupId: GroupId) {
dismiss()
getAdapterListener().onGroupMemberClicked(recipientId, groupId)
}
override fun onMessageWithRecaptchaNeededClicked(messageRecord: MessageRecord) {
dismiss()
getAdapterListener().onMessageWithRecaptchaNeededClicked(messageRecord)
}
override fun onGroupMigrationLearnMoreClicked(membershipChange: GroupMigrationMembershipChange) {
dismiss()
getAdapterListener().onGroupMigrationLearnMoreClicked(membershipChange)
}
override fun onChatSessionRefreshLearnMoreClicked() {
dismiss()
getAdapterListener().onChatSessionRefreshLearnMoreClicked()
}
override fun onBadDecryptLearnMoreClicked(author: RecipientId) {
dismiss()
getAdapterListener().onBadDecryptLearnMoreClicked(author)
}
override fun onSafetyNumberLearnMoreClicked(recipient: Recipient) {
dismiss()
getAdapterListener().onSafetyNumberLearnMoreClicked(recipient)
}
override fun onJoinGroupCallClicked() {
dismiss()
getAdapterListener().onJoinGroupCallClicked()
}
override fun onInviteFriendsToGroupClicked(groupId: GroupId.V2) {
dismiss()
getAdapterListener().onInviteFriendsToGroupClicked(groupId)
}
override fun onEnableCallNotificationsClicked() {
dismiss()
getAdapterListener().onEnableCallNotificationsClicked()
}
override fun onCallToAction(action: String) {
dismiss()
getAdapterListener().onCallToAction(action)
}
override fun onDonateClicked() {
dismiss()
getAdapterListener().onDonateClicked()
}
override fun onRecipientNameClicked(target: RecipientId) {
dismiss()
getAdapterListener().onRecipientNameClicked(target)
}
override fun onViewGiftBadgeClicked(messageRecord: MessageRecord) {
dismiss()
getAdapterListener().onViewGiftBadgeClicked(messageRecord)
}
}
interface Callback {
fun getConversationAdapterListener(): ConversationAdapter.ItemClickListener
fun jumpToMessage(messageRecord: MessageRecord)
}
companion object {
private const val KEY_MESSAGE_ID = "message_id"
private const val KEY_CONVERSATION_RECIPIENT_ID = "conversation_recipient_id"
@JvmStatic
fun show(fragmentManager: FragmentManager, messageId: MessageId, conversationRecipientId: RecipientId) {
val args = Bundle().apply {
putString(KEY_MESSAGE_ID, messageId.serialize())
putString(KEY_CONVERSATION_RECIPIENT_ID, conversationRecipientId.serialize())
}
val fragment = MessageQuotesBottomSheet().apply {
arguments = args
}
fragment.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
}

Wyświetl plik

@ -0,0 +1,71 @@
package org.thoughtcrime.securesms.conversation.quotes
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
import org.thoughtcrime.securesms.conversation.colors.NameColor
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
class MessageQuotesViewModel(
application: Application,
private val messageId: MessageId,
private val conversationRecipientId: RecipientId
) : AndroidViewModel(application) {
private val groupAuthorNameColorHelper = GroupAuthorNameColorHelper()
fun getMessages(): Observable<List<ConversationMessage>> {
return Observable.create<List<ConversationMessage>> { emitter ->
val quotes: List<ConversationMessage> = SignalDatabase
.mmsSms
.getAllMessagesThatQuote(messageId)
.map { ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(getApplication(), it) }
val originalRecord: MessageRecord? = if (messageId.mms) {
SignalDatabase.mms.getMessageRecordOrNull(messageId.id)
} else {
SignalDatabase.sms.getMessageRecordOrNull(messageId.id)
}
if (originalRecord != null) {
val originalMessage: ConversationMessage = ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(getApplication(), originalRecord, originalRecord.getDisplayBody(getApplication()), 0)
emitter.onNext(quotes + listOf(originalMessage))
} else {
emitter.onNext(quotes)
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
fun getNameColorsMap(): Observable<Map<RecipientId, NameColor>> {
return Observable.just(conversationRecipientId)
.map { conversationRecipientId ->
val conversationRecipient = Recipient.resolved(conversationRecipientId)
if (conversationRecipient.groupId.isPresent) {
groupAuthorNameColorHelper.getColorMap(conversationRecipient.groupId.get())
} else {
emptyMap()
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
class Factory(private val application: Application, private val messageId: MessageId, private val conversationRecipientId: RecipientId) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(MessageQuotesViewModel(application, messageId, conversationRecipientId)) as T
}
}
}

Wyświetl plik

@ -199,6 +199,7 @@ public class MmsDatabase extends MessageDatabase {
"CREATE INDEX IF NOT EXISTS mms_is_story_index ON " + TABLE_NAME + " (" + STORY_TYPE + ");",
"CREATE INDEX IF NOT EXISTS mms_parent_story_id_index ON " + TABLE_NAME + " (" + PARENT_STORY_ID + ");",
"CREATE INDEX IF NOT EXISTS mms_thread_story_parent_story_index ON " + TABLE_NAME + " (" + THREAD_ID + ", " + DATE_RECEIVED + "," + STORY_TYPE + "," + PARENT_STORY_ID + ");",
"CREATE INDEX IF NOT EXISTS mms_quote_id_quote_author_index ON " + TABLE_NAME + "(" + QUOTE_ID + ", " + QUOTE_AUTHOR + ");"
};
private static final String[] MMS_PROJECTION = new String[] {
@ -2180,7 +2181,11 @@ public class MmsDatabase extends MessageDatabase {
SignalDatabase.threads().updateLastSeenAndMarkSentAndLastScrolledSilenty(threadId);
if (!message.getStoryType().isStory()) {
ApplicationDependencies.getDatabaseObserver().notifyMessageInsertObservers(threadId, new MessageId(messageId, true));
if (message.getOutgoingQuote() == null) {
ApplicationDependencies.getDatabaseObserver().notifyMessageInsertObservers(threadId, new MessageId(messageId, true));
} else {
ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId);
}
} else {
ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(message.getRecipient().getId());
}

Wyświetl plik

@ -31,6 +31,7 @@ import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.util.Pair;
import org.thoughtcrime.securesms.database.MessageDatabase.MessageUpdate;
import org.thoughtcrime.securesms.database.MessageDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.notifications.v2.MessageNotifierV2;
@ -273,6 +274,45 @@ public class MmsSmsDatabase extends Database {
return queryTables(PROJECTION, selection, order, null, true);
}
/**
* The number of messages that quote the target message
*/
public int getQuotedCount(@NonNull MessageRecord messageRecord) {
RecipientId author = messageRecord.isOutgoing() ? Recipient.self().getId() : messageRecord.getRecipient().getId();
long timestamp = messageRecord.getDateSent();
String where = MmsDatabase.QUOTE_ID + " = ? AND " + MmsDatabase.QUOTE_AUTHOR + " = ?";
String[] whereArgs = SqlUtil.buildArgs(timestamp, author);
try (Cursor cursor = getReadableDatabase().query(MmsDatabase.TABLE_NAME, COUNT, where, whereArgs, null, null, null)) {
return cursor.moveToFirst() ? cursor.getInt(0) : 0;
}
}
public List<MessageRecord> getAllMessagesThatQuote(@NonNull MessageId id) {
MessageRecord targetMessage;
try {
targetMessage = id.isMms() ? SignalDatabase.mms().getMessageRecord(id.getId()) : SignalDatabase.sms().getMessageRecord(id.getId());
} catch (NoSuchMessageException e) {
throw new IllegalArgumentException("Invalid message ID!");
}
RecipientId author = targetMessage.isOutgoing() ? Recipient.self().getId() : targetMessage.getRecipient().getId();
String query = MmsDatabase.QUOTE_ID + " = " + targetMessage.getDateSent() + " AND " + MmsDatabase.QUOTE_AUTHOR + " = " + author.serialize();
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
List<MessageRecord> records = new ArrayList<>();
try (Reader reader = new Reader(queryTables(PROJECTION, query, order, null, true))) {
MessageRecord record;
while ((record = reader.getNext()) != null) {
records.add(record);
}
}
return records;
}
private @NonNull String getStickyWherePartForParentStoryId(@Nullable Long parentStoryId) {
if (parentStoryId == null) {
return " AND " + MmsDatabase.PARENT_STORY_ID + " <= 0";

Wyświetl plik

@ -200,8 +200,9 @@ object SignalDatabaseMigrations {
private const val GROUP_STORY_NOTIFICATIONS = 144
private const val GROUP_STORY_REPLY_CLEANUP = 145
private const val REMOTE_MEGAPHONE = 146
private const val QUOTE_INDEX = 147
const val DATABASE_VERSION = 146
const val DATABASE_VERSION = 147
@JvmStatic
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
@ -2613,6 +2614,14 @@ object SignalDatabaseMigrations {
"""
)
}
if (oldVersion < QUOTE_INDEX) {
db.execSQL(
"""
CREATE INDEX IF NOT EXISTS mms_quote_id_quote_author_index ON mms (quote_id, quote_author)
"""
)
}
}
@JvmStatic

Wyświetl plik

@ -111,7 +111,8 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder implements G
false,
false,
true,
colorizer);
colorizer,
false);
}
private void bindErrorState(MessageRecord messageRecord) {

Wyświetl plik

@ -0,0 +1,10 @@
<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="M7.583,2.833c-2.881,0 -5.083,2.093 -5.083,4.518 0,0.99 0.359,1.911 0.979,2.665 0.378,0.458 0.625,1.091 0.548,1.769l-0.104,0.921 1.161,-0.713c0.436,-0.267 0.93,-0.325 1.378,-0.235l0.084,0.016c-0.003,0.08 -0.005,0.16 -0.005,0.241 0,0.448 0.048,0.882 0.138,1.297 -0.173,-0.022 -0.343,-0.049 -0.512,-0.083 -0.124,-0.025 -0.224,-0.004 -0.299,0.042l-1.72,1.057c-0.449,0.276 -0.959,0.197 -1.307,-0.071 -0.338,-0.261 -0.526,-0.692 -0.474,-1.148l0.169,-1.492c0.024,-0.211 -0.052,-0.449 -0.215,-0.647 -0.825,-1.001 -1.322,-2.254 -1.322,-3.619C1,3.957 4.021,1.333 7.583,1.333c2.935,0 5.503,1.782 6.316,4.308 -0.16,-0.011 -0.321,-0.016 -0.483,-0.016 -0.371,0 -0.739,0.028 -1.1,0.083 -0.731,-1.656 -2.534,-2.875 -4.733,-2.875ZM17.333,12.063c0,-1.76 -1.605,-3.314 -3.75,-3.314s-3.75,1.554 -3.75,3.314 1.605,3.314 3.75,3.314c0.286,0 0.564,-0.028 0.831,-0.082 0.383,-0.077 0.809,-0.028 1.186,0.203l0.596,0.366 -0.041,-0.361c-0.065,-0.577 0.145,-1.11 0.461,-1.493 0.456,-0.553 0.716,-1.225 0.716,-1.947ZM13.583,7.249c2.826,0 5.25,2.085 5.25,4.814 0,1.095 -0.399,2.1 -1.059,2.901 -0.101,0.123 -0.14,0.262 -0.128,0.371l0.13,1.151c0.046,0.411 -0.123,0.802 -0.431,1.041 -0.319,0.246 -0.789,0.319 -1.202,0.065l-1.327,-0.815c-0.016,-0.01 -0.049,-0.022 -0.107,-0.011 -0.363,0.073 -0.74,0.111 -1.125,0.111 -2.826,0 -5.25,-2.085 -5.25,-4.814s2.424,-4.814 5.25,-4.814Z"
android:fillType="evenOdd"/>
</vector>

Wyświetl plik

@ -75,7 +75,8 @@
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical"
tools:backgroundTint="@color/conversation_blue">
tools:background="@drawable/message_bubble_background_received_alone"
tools:backgroundTint="@color/signal_colorSurfaceVariant">
<LinearLayout
android:id="@+id/group_sender_holder"
@ -140,6 +141,7 @@
app:message_type="incoming"
app:quote_colorPrimary="@color/conversation_item_quote_text_color"
app:quote_colorSecondary="@color/conversation_item_quote_text_color"
tools:background="@color/transparent_black_05"
tools:visibility="visible" />
<ViewStub
@ -225,8 +227,8 @@
android:id="@+id/conversation_item_call_to_action_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/conversation_item_call_to_action"
android:layout_margin="8dp" />
android:layout_margin="8dp"
android:layout="@layout/conversation_item_call_to_action" />
<org.thoughtcrime.securesms.components.ConversationItemFooter
android:id="@+id/conversation_item_footer"
@ -270,9 +272,31 @@
android:id="@+id/indicators_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_toStartOf="@id/quoted_indicator"
android:gravity="center_vertical"
android:orientation="vertical" />
android:orientation="vertical"
android:visibility="gone"/>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignEnd="@id/body_bubble"
android:layout_alignTop="@id/body_bubble"
android:layout_alignBottom="@id/body_bubble"
android:layout_marginEnd="-42dp" >
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/quoted_indicator"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_vertical"
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/signal_colorSurfaceVariant"
android:padding="6dp"
android:tint="@color/signal_colorOnSurfaceVariant"
app:srcCompat="@drawable/ic_replies_outline_20" />
</FrameLayout>
<org.thoughtcrime.securesms.reactions.ReactionsConversationView
android:id="@+id/reactions_view"

Wyświetl plik

@ -44,7 +44,8 @@
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical"
tools:backgroundTint="@color/core_grey_05">
tools:background="@drawable/message_bubble_background_received_alone"
tools:backgroundTint="@color/conversation_blue">
<LinearLayout
android:id="@+id/story_reacted_label_holder"
@ -82,6 +83,7 @@
app:message_type="outgoing"
app:quote_colorPrimary="@color/conversation_item_quote_text_color_sent"
app:quote_colorSecondary="@color/conversation_item_quote_text_color_sent"
tools:background="@color/transparent_white_10"
tools:visibility="visible" />
<ViewStub
@ -202,12 +204,33 @@
android:id="@+id/indicators_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/body_bubble"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_marginStart="8dp"
android:orientation="vertical"
android:padding="8dp" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignStart="@id/body_bubble"
android:layout_alignTop="@id/body_bubble"
android:layout_alignBottom="@id/body_bubble"
android:layout_marginStart="-42dp" >
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/quoted_indicator"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_vertical"
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/signal_colorSurfaceVariant"
android:padding="6dp"
android:tint="@color/signal_colorOnSurfaceVariant"
app:srcCompat="@drawable/ic_replies_outline_20" />
</FrameLayout>
<org.thoughtcrime.securesms.reactions.ReactionsConversationView
android:id="@+id/reactions_view"
android:layout_width="wrap_content"

Wyświetl plik

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:maxLines="2"
android:ellipsize="end"
android:text="@string/MessageQuotesBottomSheet_replies"
style="@style/Signal.Text.TitleSmall"/>

Wyświetl plik

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<View
android:id="@+id/anchor"
android:layout_width="48dp"
android:layout_height="2dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:background="@color/signal_icon_tint_tab_unselected" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/quotes_list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

Wyświetl plik

@ -54,6 +54,7 @@
<dimen name="media_bubble_max_width">240dp</dimen>
<dimen name="media_bubble_min_height">100dp</dimen>
<dimen name="media_bubble_max_height">320dp</dimen>
<dimen name="media_bubble_max_height_condensed">150dp</dimen>
<dimen name="media_bubble_sticker_dimens">175dp</dimen>
<dimen name="media_bubble_gif_width">240dp</dimen>
<dimen name="message_audio_width">242dp</dimen>

Wyświetl plik

@ -1041,6 +1041,9 @@
<string name="Megaphones_appearance">Appearance</string>
<string name="Megaphones_add_photo">Add photo</string>
<!-- Title of a bottom sheet to render messages that all quote a specific message -->
<string name="MessageQuotesBottomSheet_replies">Replies</string>
<!-- NotificationBarManager -->
<string name="NotificationBarManager_signal_call_in_progress">Signal call in progress</string>
<string name="NotificationBarManager__establishing_signal_call">Establishing Signal call</string>

Wyświetl plik

@ -470,6 +470,16 @@
<item name="android:elevation" tools:ignore="NewApi">0dp</item>
</style>
<style name="Widget.Signal.FixedRoundedCorners.Messages" parent="Widget.Signal.FixedRoundedCorners">
<item name="bottomSheetStyle">@style/Widget.Signal.FixedRoundedCorners.Messages.BottomSheet</item>
</style>
<style name="Widget.Signal.FixedRoundedCorners.Messages.BottomSheet" parent="Widget.Material3.BottomSheet">
<item name="shapeAppearanceOverlay">@style/ShapeAppearanceOverlay.Signal.BottomSheet.Rounded</item>
<item name="backgroundTint">@color/signal_colorSurface</item>
<item name="android:elevation" tools:ignore="NewApi">0dp</item>
</style>
<style name="Widget.Signal.FixedRoundedCorners.Stories">
<item name="bottomSheetStyle">@style/Widget.Signal.FixedRoundedCorners.BottomSheet.Stories</item>
<item name="android:navigationBarColor" tools:targetApi="lollipop">@color/signal_colorSurface</item>