Live group update messages on conversation list and conversation.
parent
7446c2096d
commit
bd1c164d57
|
@ -359,10 +359,10 @@ dependencies {
|
|||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||
testImplementation 'org.mockito:mockito-core:1.9.5'
|
||||
testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-api-mockito:1.6.5'
|
||||
testImplementation 'org.powermock:powermock-module-junit4:1.6.5'
|
||||
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.5'
|
||||
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.5'
|
||||
|
||||
testImplementation 'androidx.test:core:1.2.0'
|
||||
testImplementation ('org.robolectric:robolectric:4.2') {
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context;
|
|||
import android.graphics.ColorFilter;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.text.SpannableString;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
@ -13,43 +14,54 @@ import android.widget.TextView;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.Transformations;
|
||||
|
||||
import org.thoughtcrime.securesms.BindableConversationItem;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.VerifyIdentityActivity;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.model.LiveUpdateMessage;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.UpdateDescription;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class ConversationUpdateItem extends LinearLayout
|
||||
implements RecipientForeverObserver, BindableConversationItem
|
||||
public final class ConversationUpdateItem extends LinearLayout
|
||||
implements RecipientForeverObserver,
|
||||
BindableConversationItem,
|
||||
Observer<SpannableString>
|
||||
{
|
||||
private static final String TAG = ConversationUpdateItem.class.getSimpleName();
|
||||
|
||||
private Set<MessageRecord> batchSelected;
|
||||
|
||||
private ImageView icon;
|
||||
private TextView title;
|
||||
private TextView body;
|
||||
private TextView date;
|
||||
private LiveRecipient sender;
|
||||
private MessageRecord messageRecord;
|
||||
private Locale locale;
|
||||
private ImageView icon;
|
||||
private TextView title;
|
||||
private TextView body;
|
||||
private TextView date;
|
||||
private LiveRecipient sender;
|
||||
private MessageRecord messageRecord;
|
||||
private Locale locale;
|
||||
private LiveData<SpannableString> displayBody;
|
||||
|
||||
private final Debouncer bodyClearDebouncer = new Debouncer(150);
|
||||
|
||||
public ConversationUpdateItem(Context context) {
|
||||
super(context);
|
||||
|
@ -108,9 +120,8 @@ public class ConversationUpdateItem extends LinearLayout
|
|||
this.sender.removeForeverObserver(this);
|
||||
}
|
||||
|
||||
if (this.messageRecord != null && messageRecord.isGroupAction()) {
|
||||
GroupUtil.getDescription(getContext(), messageRecord.getBody(), messageRecord.isGroupV2()).removeObserver(this);
|
||||
}
|
||||
observeDisplayBody(null);
|
||||
setBodyText(null);
|
||||
|
||||
this.messageRecord = messageRecord;
|
||||
this.sender = messageRecord.getIndividualRecipient().live();
|
||||
|
@ -118,23 +129,49 @@ public class ConversationUpdateItem extends LinearLayout
|
|||
|
||||
this.sender.observeForever(this);
|
||||
|
||||
if (this.messageRecord != null && messageRecord.isGroupAction()) {
|
||||
GroupUtil.getDescription(getContext(), messageRecord.getBody(), messageRecord.isGroupV2()).addObserver(this);
|
||||
}
|
||||
UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext()));
|
||||
LiveData<String> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(updateDescription);
|
||||
LiveData<SpannableString> spannableStringMessage = Transformations.map(liveUpdateMessage, SpannableString::new);
|
||||
|
||||
present(messageRecord);
|
||||
|
||||
observeDisplayBody(spannableStringMessage);
|
||||
}
|
||||
|
||||
private void observeDisplayBody(@Nullable LiveData<SpannableString> displayBody) {
|
||||
if (this.displayBody != displayBody) {
|
||||
if (this.displayBody != null) {
|
||||
this.displayBody.removeObserver(this);
|
||||
}
|
||||
|
||||
this.displayBody = displayBody;
|
||||
|
||||
if (this.displayBody != null) {
|
||||
this.displayBody.observeForever(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setBodyText(@Nullable CharSequence text) {
|
||||
if (text == null) {
|
||||
bodyClearDebouncer.publish(() -> body.setText(null));
|
||||
} else {
|
||||
bodyClearDebouncer.clear();
|
||||
body.setText(text);
|
||||
body.setVisibility(VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void present(MessageRecord messageRecord) {
|
||||
if (messageRecord.isGroupAction()) setGroupRecord(messageRecord);
|
||||
if (messageRecord.isGroupAction()) setGroupRecord();
|
||||
else if (messageRecord.isCallLog()) setCallRecord(messageRecord);
|
||||
else if (messageRecord.isJoined()) setJoinedRecord(messageRecord);
|
||||
else if (messageRecord.isJoined()) setJoinedRecord();
|
||||
else if (messageRecord.isExpirationTimerUpdate()) setTimerRecord(messageRecord);
|
||||
else if (messageRecord.isEndSession()) setEndSessionRecord(messageRecord);
|
||||
else if (messageRecord.isIdentityUpdate()) setIdentityRecord(messageRecord);
|
||||
else if (messageRecord.isEndSession()) setEndSessionRecord();
|
||||
else if (messageRecord.isIdentityUpdate()) setIdentityRecord();
|
||||
else if (messageRecord.isIdentityVerified() ||
|
||||
messageRecord.isIdentityDefault()) setIdentityVerifyUpdate(messageRecord);
|
||||
else if (messageRecord.isProfileChange()) setProfileNameChangeRecord(messageRecord);
|
||||
else if (messageRecord.isProfileChange()) setProfileNameChangeRecord();
|
||||
else throw new AssertionError("Neither group nor log nor joined.");
|
||||
|
||||
if (batchSelected.contains(messageRecord)) setSelected(true);
|
||||
|
@ -146,11 +183,9 @@ public class ConversationUpdateItem extends LinearLayout
|
|||
else if (messageRecord.isOutgoingCall()) icon.setImageResource(R.drawable.ic_call_made_grey600_24dp);
|
||||
else icon.setImageResource(R.drawable.ic_call_missed_grey600_24dp);
|
||||
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
date.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getDateReceived()));
|
||||
|
||||
title.setVisibility(GONE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
|
@ -163,10 +198,8 @@ public class ConversationUpdateItem extends LinearLayout
|
|||
|
||||
icon.setColorFilter(getIconTintFilter());
|
||||
title.setText(ExpirationUtil.getExpirationDisplayValue(getContext(), (int)(messageRecord.getExpiresIn() / 1000)));
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
|
||||
title.setVisibility(VISIBLE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
|
@ -174,13 +207,11 @@ public class ConversationUpdateItem extends LinearLayout
|
|||
return new PorterDuffColorFilter(ThemeUtil.getThemedColor(getContext(), R.attr.icon_tint), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
private void setIdentityRecord(final MessageRecord messageRecord) {
|
||||
private void setIdentityRecord() {
|
||||
icon.setImageDrawable(ThemeUtil.getThemedDrawable(getContext(), R.attr.safety_number_icon));
|
||||
icon.setColorFilter(getIconTintFilter());
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
|
||||
title.setVisibility(GONE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
|
@ -189,54 +220,40 @@ public class ConversationUpdateItem extends LinearLayout
|
|||
else icon.setImageResource(R.drawable.ic_info_outline_white_24dp);
|
||||
|
||||
icon.setColorFilter(getIconTintFilter());
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
|
||||
title.setVisibility(GONE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setProfileNameChangeRecord(MessageRecord messageRecord) {
|
||||
private void setProfileNameChangeRecord() {
|
||||
icon.setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.ic_profile_outline_20));
|
||||
icon.setColorFilter(getIconTintFilter());
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
|
||||
title.setVisibility(GONE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setGroupRecord(MessageRecord messageRecord) {
|
||||
private void setGroupRecord() {
|
||||
icon.setImageDrawable(ThemeUtil.getThemedDrawable(getContext(), R.attr.menu_group_icon));
|
||||
icon.clearColorFilter();
|
||||
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
|
||||
title.setVisibility(GONE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setJoinedRecord(MessageRecord messageRecord) {
|
||||
private void setJoinedRecord() {
|
||||
icon.setImageResource(R.drawable.ic_favorite_grey600_24dp);
|
||||
icon.clearColorFilter();
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
|
||||
title.setVisibility(GONE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setEndSessionRecord(MessageRecord messageRecord) {
|
||||
private void setEndSessionRecord() {
|
||||
icon.setImageResource(R.drawable.ic_refresh_white_24dp);
|
||||
icon.setColorFilter(getIconTintFilter());
|
||||
body.setText(messageRecord.getDisplayBody(getContext()));
|
||||
|
||||
title.setVisibility(GONE);
|
||||
body.setVisibility(VISIBLE);
|
||||
date.setVisibility(GONE);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onRecipientChanged(@NonNull Recipient recipient) {
|
||||
present(messageRecord);
|
||||
|
@ -252,9 +269,13 @@ public class ConversationUpdateItem extends LinearLayout
|
|||
if (sender != null) {
|
||||
sender.removeForeverObserver(this);
|
||||
}
|
||||
if (this.messageRecord != null && messageRecord.isGroupAction()) {
|
||||
GroupUtil.getDescription(getContext(), messageRecord.getBody(), messageRecord.isGroupV2()).removeObserver(this);
|
||||
}
|
||||
|
||||
observeDisplayBody(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(SpannableString update) {
|
||||
setBodyText(update);
|
||||
}
|
||||
|
||||
private class InternalClickListener implements View.OnClickListener {
|
||||
|
|
|
@ -21,7 +21,6 @@ import android.content.res.ColorStateList;
|
|||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.RippleDrawable;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.StyleSpan;
|
||||
|
@ -32,6 +31,9 @@ import android.widget.TextView;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.Transformations;
|
||||
|
||||
import org.thoughtcrime.securesms.BindableConversationListItem;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
@ -42,41 +44,49 @@ import org.thoughtcrime.securesms.components.DeliveryStatusView;
|
|||
import org.thoughtcrime.securesms.components.FromTextView;
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.components.TypingIndicatorView;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.LiveUpdateMessage;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.database.model.UpdateDescription;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.SearchUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public class ConversationListItem extends RelativeLayout
|
||||
implements RecipientForeverObserver,
|
||||
BindableConversationListItem, Unbindable
|
||||
import static org.thoughtcrime.securesms.database.model.LiveUpdateMessage.recipientToStringAsync;
|
||||
|
||||
public final class ConversationListItem extends RelativeLayout
|
||||
implements RecipientForeverObserver,
|
||||
BindableConversationListItem,
|
||||
Unbindable,
|
||||
Observer<SpannableString>
|
||||
{
|
||||
@SuppressWarnings("unused")
|
||||
private final static String TAG = ConversationListItem.class.getSimpleName();
|
||||
private final static String TAG = Log.tag(ConversationListItem.class);
|
||||
|
||||
private final static Typeface BOLD_TYPEFACE = Typeface.create("sans-serif-medium", Typeface.NORMAL);
|
||||
private final static Typeface LIGHT_TYPEFACE = Typeface.create("sans-serif", Typeface.NORMAL);
|
||||
|
||||
private static final int MAX_SNIPPET_LENGTH = 500;
|
||||
|
||||
private Set<Long> selectedThreads;
|
||||
private Set<Long> typingThreads;
|
||||
private LiveRecipient recipient;
|
||||
private LiveRecipient groupAddedBy;
|
||||
private long threadId;
|
||||
private GlideRequests glideRequests;
|
||||
private View subjectContainer;
|
||||
|
@ -96,13 +106,9 @@ public class ConversationListItem extends RelativeLayout
|
|||
private AvatarImageView contactPhotoImage;
|
||||
private ThumbnailView thumbnailView;
|
||||
|
||||
private int distributionType;
|
||||
private final Debouncer subjectViewClearDebouncer = new Debouncer(150);
|
||||
|
||||
private final RecipientForeverObserver groupAddedByObserver = adder -> {
|
||||
if (isAttachedToWindow() && subjectView != null && thread != null) {
|
||||
subjectView.setText(getThreadDisplayBody(getContext(), thread));
|
||||
}
|
||||
};
|
||||
private LiveData<SpannableString> displayBody;
|
||||
|
||||
public ConversationListItem(Context context) {
|
||||
this(context, null);
|
||||
|
@ -152,16 +158,16 @@ public class ConversationListItem extends RelativeLayout
|
|||
@Nullable String highlightSubstring)
|
||||
{
|
||||
if (this.recipient != null) this.recipient.removeForeverObserver(this);
|
||||
if (this.groupAddedBy != null) this.groupAddedBy.removeForeverObserver(groupAddedByObserver);
|
||||
observeDisplayBody(null);
|
||||
setSubjectViewText(null);
|
||||
|
||||
this.selectedThreads = selectedThreads;
|
||||
this.recipient = thread.getRecipient().live();
|
||||
this.threadId = thread.getThreadId();
|
||||
this.glideRequests = glideRequests;
|
||||
this.unreadCount = thread.getUnreadCount();
|
||||
this.distributionType = thread.getDistributionType();
|
||||
this.lastSeen = thread.getLastSeen();
|
||||
this.thread = thread;
|
||||
this.selectedThreads = selectedThreads;
|
||||
this.recipient = thread.getRecipient().live();
|
||||
this.threadId = thread.getThreadId();
|
||||
this.glideRequests = glideRequests;
|
||||
this.unreadCount = thread.getUnreadCount();
|
||||
this.lastSeen = thread.getLastSeen();
|
||||
this.thread = thread;
|
||||
|
||||
this.recipient.observeForever(this);
|
||||
if (highlightSubstring != null) {
|
||||
|
@ -172,14 +178,10 @@ public class ConversationListItem extends RelativeLayout
|
|||
this.fromView.setText(recipient.get(), thread.isRead());
|
||||
}
|
||||
|
||||
this.typingThreads = typingThreads;
|
||||
updateTypingIndicator(typingThreads);
|
||||
|
||||
this.subjectView.setText(getTrimmedSnippet(getThreadDisplayBody(getContext(), thread)));
|
||||
|
||||
if (thread.getGroupAddedBy() != null) {
|
||||
groupAddedBy = Recipient.live(thread.getGroupAddedBy());
|
||||
groupAddedBy.observeForever(groupAddedByObserver);
|
||||
}
|
||||
observeDisplayBody(getThreadDisplayBody(getContext(), thread));
|
||||
|
||||
this.subjectView.setTypeface(thread.isRead() ? LIGHT_TYPEFACE : BOLD_TYPEFACE);
|
||||
this.subjectView.setTextColor(thread.isRead() ? ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_subject_color)
|
||||
|
@ -213,7 +215,8 @@ public class ConversationListItem extends RelativeLayout
|
|||
@Nullable String highlightSubstring)
|
||||
{
|
||||
if (this.recipient != null) this.recipient.removeForeverObserver(this);
|
||||
if (this.groupAddedBy != null) this.groupAddedBy.removeForeverObserver(groupAddedByObserver);
|
||||
observeDisplayBody(null);
|
||||
setSubjectViewText(null);
|
||||
|
||||
this.selectedThreads = Collections.emptySet();
|
||||
this.recipient = contact.live();
|
||||
|
@ -242,7 +245,8 @@ public class ConversationListItem extends RelativeLayout
|
|||
@Nullable String highlightSubstring)
|
||||
{
|
||||
if (this.recipient != null) this.recipient.removeForeverObserver(this);
|
||||
if (this.groupAddedBy != null) this.groupAddedBy.removeForeverObserver(groupAddedByObserver);
|
||||
observeDisplayBody(null);
|
||||
setSubjectViewText(null);
|
||||
|
||||
this.selectedThreads = Collections.emptySet();
|
||||
this.recipient = messageResult.conversationRecipient.live();
|
||||
|
@ -274,10 +278,7 @@ public class ConversationListItem extends RelativeLayout
|
|||
contactPhotoImage.setAvatar(glideRequests, null, !batchMode);
|
||||
}
|
||||
|
||||
if (this.groupAddedBy != null) {
|
||||
this.groupAddedBy.removeForeverObserver(groupAddedByObserver);
|
||||
this.groupAddedBy = null;
|
||||
}
|
||||
observeDisplayBody(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -317,17 +318,30 @@ public class ConversationListItem extends RelativeLayout
|
|||
return unreadCount;
|
||||
}
|
||||
|
||||
public int getDistributionType() {
|
||||
return distributionType;
|
||||
}
|
||||
|
||||
public long getLastSeen() {
|
||||
return lastSeen;
|
||||
}
|
||||
|
||||
private static @NonNull CharSequence getTrimmedSnippet(@NonNull CharSequence snippet) {
|
||||
return snippet.length() <= MAX_SNIPPET_LENGTH ? snippet
|
||||
: snippet.subSequence(0, MAX_SNIPPET_LENGTH);
|
||||
private void observeDisplayBody(@Nullable LiveData<SpannableString> displayBody) {
|
||||
if (this.displayBody != null) {
|
||||
this.displayBody.removeObserver(this);
|
||||
}
|
||||
|
||||
this.displayBody = displayBody;
|
||||
|
||||
if (this.displayBody != null) {
|
||||
this.displayBody.observeForever(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void setSubjectViewText(@Nullable CharSequence text) {
|
||||
if (text == null) {
|
||||
subjectViewClearDebouncer.publish(() -> subjectView.setText(null));
|
||||
} else {
|
||||
subjectViewClearDebouncer.clear();
|
||||
subjectView.setText(text);
|
||||
subjectView.setVisibility(VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setThumbnailSnippet(ThreadRecord thread) {
|
||||
|
@ -371,7 +385,7 @@ public class ConversationListItem extends RelativeLayout
|
|||
}
|
||||
|
||||
private void setRippleColor(Recipient recipient) {
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
|
||||
if (VERSION.SDK_INT >= 21) {
|
||||
((RippleDrawable)(getBackground()).mutate())
|
||||
.setColor(ColorStateList.valueOf(recipient.getColor().toConversationColor(getContext())));
|
||||
}
|
||||
|
@ -394,16 +408,20 @@ public class ConversationListItem extends RelativeLayout
|
|||
setRippleColor(recipient);
|
||||
}
|
||||
|
||||
|
||||
private static SpannableString getThreadDisplayBody(@NonNull Context context, @NonNull ThreadRecord thread) {
|
||||
private static @NonNull LiveData<SpannableString> getThreadDisplayBody(@NonNull Context context, @NonNull ThreadRecord thread) {
|
||||
if (thread.getGroupAddedBy() != null) {
|
||||
return emphasisAdded(context.getString(thread.isGv2Invite() ? R.string.ThreadRecord_s_invited_you_to_the_group
|
||||
: R.string.ThreadRecord_s_added_you_to_the_group,
|
||||
Recipient.live(thread.getGroupAddedBy()).get().getDisplayName(context)));
|
||||
return emphasisAdded(recipientToStringAsync(thread.getGroupAddedBy(),
|
||||
r -> context.getString(thread.isGv2Invite() ? R.string.ThreadRecord_s_invited_you_to_the_group
|
||||
: R.string.ThreadRecord_s_added_you_to_the_group,
|
||||
r.getDisplayName(context))));
|
||||
} else if (!thread.isMessageRequestAccepted()) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_message_request));
|
||||
} else if (SmsDatabase.Types.isGroupUpdate(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated));
|
||||
if (thread.getRecipient().isPushV2Group()) {
|
||||
return emphasisAdded(MessageRecord.getGv2ChangeDescription(context, thread.getBody()));
|
||||
} else {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated));
|
||||
}
|
||||
} else if (SmsDatabase.Types.isGroupQuit(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_left_the_group));
|
||||
} else if (SmsDatabase.Types.isKeyExchangeType(thread.getType())) {
|
||||
|
@ -418,15 +436,15 @@ public class ConversationListItem extends RelativeLayout
|
|||
return emphasisAdded(context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported));
|
||||
} else if (MmsSmsColumns.Types.isDraftMessageType(thread.getType())) {
|
||||
String draftText = context.getString(R.string.ThreadRecord_draft);
|
||||
return emphasisAdded(draftText + " " + thread.getBody(), 0, draftText.length());
|
||||
return emphasisAdded(draftText + " " + thread.getBody());
|
||||
} else if (SmsDatabase.Types.isOutgoingCall(thread.getType())) {
|
||||
return emphasisAdded(context.getString(org.thoughtcrime.securesms.R.string.ThreadRecord_called));
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_called));
|
||||
} else if (SmsDatabase.Types.isIncomingCall(thread.getType())) {
|
||||
return emphasisAdded(context.getString(org.thoughtcrime.securesms.R.string.ThreadRecord_called_you));
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_called_you));
|
||||
} else if (SmsDatabase.Types.isMissedCall(thread.getType())) {
|
||||
return emphasisAdded(context.getString(org.thoughtcrime.securesms.R.string.ThreadRecord_missed_call));
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_missed_call));
|
||||
} else if (SmsDatabase.Types.isJoinedType(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_s_is_on_signal, thread.getRecipient().getDisplayName(context)));
|
||||
return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> context.getString(R.string.ThreadRecord_s_is_on_signal, r.getDisplayName(context))));
|
||||
} else if (SmsDatabase.Types.isExpirationTimerUpdate(thread.getType())) {
|
||||
int seconds = (int)(thread.getExpiresIn() / 1000);
|
||||
if (seconds <= 0) {
|
||||
|
@ -438,7 +456,7 @@ public class ConversationListItem extends RelativeLayout
|
|||
if (thread.getRecipient().isGroup()) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_safety_number_changed));
|
||||
} else {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, thread.getRecipient().getDisplayName(context)));
|
||||
return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context))));
|
||||
}
|
||||
} else if (SmsDatabase.Types.isIdentityVerified(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_verified));
|
||||
|
@ -449,11 +467,11 @@ public class ConversationListItem extends RelativeLayout
|
|||
} else {
|
||||
ThreadDatabase.Extra extra = thread.getExtra();
|
||||
if (extra != null && extra.isViewOnce()) {
|
||||
return new SpannableString(emphasisAdded(getViewOnceDescription(context, thread.getContentType())));
|
||||
return emphasisAdded(getViewOnceDescription(context, thread.getContentType()));
|
||||
} else if (extra != null && extra.isRemoteDelete()) {
|
||||
return new SpannableString(emphasisAdded(context.getString(thread.isOutgoing() ? R.string.ThreadRecord_you_deleted_this_message : R.string.ThreadRecord_this_message_was_deleted)));
|
||||
return emphasisAdded(context.getString(thread.isOutgoing() ? R.string.ThreadRecord_you_deleted_this_message : R.string.ThreadRecord_this_message_was_deleted));
|
||||
} else {
|
||||
return new SpannableString(removeNewlines(thread.getBody()));
|
||||
return LiveDataUtil.just(new SpannableString(removeNewlines(thread.getBody())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -470,17 +488,23 @@ public class ConversationListItem extends RelativeLayout
|
|||
}
|
||||
}
|
||||
|
||||
private static @NonNull SpannableString emphasisAdded(String sequence) {
|
||||
return emphasisAdded(sequence, 0, sequence.length());
|
||||
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull String string) {
|
||||
return emphasisAdded(UpdateDescription.staticDescription(string));
|
||||
}
|
||||
|
||||
private static @NonNull SpannableString emphasisAdded(String sequence, int start, int end) {
|
||||
SpannableString spannable = new SpannableString(sequence);
|
||||
spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC),
|
||||
start,
|
||||
end,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return spannable;
|
||||
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull UpdateDescription description) {
|
||||
return emphasisAdded(LiveUpdateMessage.fromMessageDescription(description));
|
||||
}
|
||||
|
||||
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull LiveData<String> description) {
|
||||
return Transformations.map(description, sequence -> {
|
||||
SpannableString spannable = new SpannableString(sequence);
|
||||
spannable.setSpan(new StyleSpan(Typeface.ITALIC),
|
||||
0,
|
||||
sequence.length(),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return spannable;
|
||||
});
|
||||
}
|
||||
|
||||
private static String getViewOnceDescription(@NonNull Context context, @Nullable String contentType) {
|
||||
|
@ -493,6 +517,15 @@ public class ConversationListItem extends RelativeLayout
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(SpannableString spannableString) {
|
||||
setSubjectViewText(spannableString);
|
||||
|
||||
if (typingThreads != null) {
|
||||
updateTypingIndicator(typingThreads);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ThumbnailPositioner implements Runnable {
|
||||
|
||||
private final View thumbnailView;
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database.model;
|
|||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
|
@ -21,6 +22,8 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
|||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
@ -37,8 +40,7 @@ final class GroupsV2UpdateMessageProducer {
|
|||
*/
|
||||
GroupsV2UpdateMessageProducer(@NonNull Context context,
|
||||
@NonNull DescribeMemberStrategy descriptionStrategy,
|
||||
@NonNull UUID selfUuid)
|
||||
{
|
||||
@NonNull UUID selfUuid) {
|
||||
this.context = context;
|
||||
this.descriptionStrategy = descriptionStrategy;
|
||||
this.selfUuid = selfUuid;
|
||||
|
@ -50,10 +52,10 @@ final class GroupsV2UpdateMessageProducer {
|
|||
* <p>
|
||||
* Invitation and groups you create are the most common cases where no change is available.
|
||||
*/
|
||||
String describeNewGroup(@NonNull DecryptedGroup group) {
|
||||
UpdateDescription describeNewGroup(@NonNull DecryptedGroup group) {
|
||||
Optional<DecryptedPendingMember> selfPending = DecryptedGroupUtil.findPendingByUuid(group.getPendingMembersList(), selfUuid);
|
||||
if (selfPending.isPresent()) {
|
||||
return context.getString(R.string.MessageRecord_s_invited_you_to_the_group, describe(selfPending.get().getAddedByUuid()));
|
||||
return updateDescription(selfPending.get().getAddedByUuid(), inviteBy -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, inviteBy));
|
||||
}
|
||||
|
||||
if (group.getRevision() == 0) {
|
||||
|
@ -61,22 +63,22 @@ final class GroupsV2UpdateMessageProducer {
|
|||
if (foundingMember.isPresent()) {
|
||||
ByteString foundingMemberUuid = foundingMember.get().getUuid();
|
||||
if (selfUuidBytes.equals(foundingMemberUuid)) {
|
||||
return context.getString(R.string.MessageRecord_you_created_the_group);
|
||||
return updateDescription(context.getString(R.string.MessageRecord_you_created_the_group));
|
||||
} else {
|
||||
return context.getString(R.string.MessageRecord_s_added_you, describe(foundingMemberUuid));
|
||||
return updateDescription(foundingMemberUuid, creator -> context.getString(R.string.MessageRecord_s_added_you, creator));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (DecryptedGroupUtil.findMemberByUuid(group.getMembersList(), selfUuid).isPresent()) {
|
||||
return context.getString(R.string.MessageRecord_you_joined_the_group);
|
||||
return updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group));
|
||||
} else {
|
||||
return context.getString(R.string.MessageRecord_group_updated);
|
||||
return updateDescription(context.getString(R.string.MessageRecord_group_updated));
|
||||
}
|
||||
}
|
||||
|
||||
List<String> describeChange(@NonNull DecryptedGroupChange change) {
|
||||
List<String> updates = new LinkedList<>();
|
||||
List<UpdateDescription> describeChanges(@NonNull DecryptedGroupChange change) {
|
||||
List<UpdateDescription> updates = new LinkedList<>();
|
||||
|
||||
if (change.getEditor().isEmpty() || UuidUtil.UNKNOWN_UUID.equals(UuidUtil.fromByteString(change.getEditor()))) {
|
||||
describeUnknownEditorMemberAdditions(change, updates);
|
||||
|
@ -119,21 +121,21 @@ final class GroupsV2UpdateMessageProducer {
|
|||
/**
|
||||
* Handles case of future protocol versions where we don't know what has changed.
|
||||
*/
|
||||
private void describeUnknownChange(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownChange(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_updated_group));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_updated_group)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_updated_group, describe(change.getEditor())));
|
||||
updates.add(updateDescription(change.getEditor(), (editor) -> context.getString(R.string.MessageRecord_s_updated_group, editor)));
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorUnknownChange(@NonNull List<String> updates) {
|
||||
updates.add(context.getString(R.string.MessageRecord_the_group_was_updated));
|
||||
private void describeUnknownEditorUnknownChange(@NonNull List<UpdateDescription> updates) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_was_updated)));
|
||||
}
|
||||
|
||||
private void describeMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
for (DecryptedMember member : change.getNewMembersList()) {
|
||||
|
@ -141,37 +143,37 @@ final class GroupsV2UpdateMessageProducer {
|
|||
|
||||
if (editorIsYou) {
|
||||
if (newMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_joined_the_group));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_added_s, describe(member.getUuid())));
|
||||
updates.add(updateDescription(member.getUuid(), added -> context.getString(R.string.MessageRecord_you_added_s, added)));
|
||||
}
|
||||
} else {
|
||||
if (newMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_added_you, describe(change.getEditor())));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_added_you, editor)));
|
||||
} else {
|
||||
if (member.getUuid().equals(change.getEditor())) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_joined_the_group, describe(member.getUuid())));
|
||||
updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group, newMember)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_added_s, describe(change.getEditor()), describe(member.getUuid())));
|
||||
updates.add(updateDescription(change.getEditor(), member.getUuid(), (editor, newMember) -> context.getString(R.string.MessageRecord_s_added_s, editor, newMember)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
for (DecryptedMember member : change.getNewMembersList()) {
|
||||
boolean newMemberIsYou = member.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (newMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_joined_the_group));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_joined_the_group, describe(member.getUuid())));
|
||||
updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group, newMember)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeMemberRemovals(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeMemberRemovals(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
for (ByteString member : change.getDeleteMembersList()) {
|
||||
|
@ -179,98 +181,98 @@ final class GroupsV2UpdateMessageProducer {
|
|||
|
||||
if (editorIsYou) {
|
||||
if (removedMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_left_the_group));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_left_the_group)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_removed_s, describe(member)));
|
||||
updates.add(updateDescription(member, removedMember -> context.getString(R.string.MessageRecord_you_removed_s, removedMember)));
|
||||
}
|
||||
} else {
|
||||
if (removedMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_removed_you_from_the_group, describe(change.getEditor())));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_removed_you_from_the_group, editor)));
|
||||
} else {
|
||||
if (member.equals(change.getEditor())) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_left_the_group, describe(member)));
|
||||
updates.add(updateDescription(member, leavingMember -> context.getString(R.string.MessageRecord_s_left_the_group, leavingMember)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_removed_s, describe(change.getEditor()), describe(member)));
|
||||
updates.add(updateDescription(change.getEditor(), member, (editor, removedMember) -> context.getString(R.string.MessageRecord_s_removed_s, editor, removedMember)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorMemberRemovals(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorMemberRemovals(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
for (ByteString member : change.getDeleteMembersList()) {
|
||||
boolean removedMemberIsYou = member.equals(selfUuidBytes);
|
||||
|
||||
if (removedMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_are_no_longer_in_the_group));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_no_longer_in_the_group)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_is_no_longer_in_the_group, describe(member)));
|
||||
updates.add(updateDescription(member, oldMember -> context.getString(R.string.MessageRecord_s_is_no_longer_in_the_group, oldMember)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeModifyMemberRoles(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeModifyMemberRoles(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
for (DecryptedModifyMemberRole roleChange : change.getModifyMemberRolesList()) {
|
||||
boolean changedMemberIsYou = roleChange.getUuid().equals(selfUuidBytes);
|
||||
if (roleChange.getRole() == Member.Role.ADMINISTRATOR) {
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_made_s_an_admin, describe(roleChange.getUuid())));
|
||||
updates.add(updateDescription(roleChange.getUuid(), newAdmin -> context.getString(R.string.MessageRecord_you_made_s_an_admin, newAdmin)));
|
||||
} else {
|
||||
if (changedMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_made_you_an_admin, describe(change.getEditor())));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_made_you_an_admin, editor)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_made_s_an_admin, describe(change.getEditor()), describe(roleChange.getUuid())));
|
||||
updates.add(updateDescription(change.getEditor(), roleChange.getUuid(), (editor, newAdmin) -> context.getString(R.string.MessageRecord_s_made_s_an_admin, editor, newAdmin)));
|
||||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_revoked_admin_privileges_from_s, describe(roleChange.getUuid())));
|
||||
updates.add(updateDescription(roleChange.getUuid(), oldAdmin -> context.getString(R.string.MessageRecord_you_revoked_admin_privileges_from_s, oldAdmin)));
|
||||
} else {
|
||||
if (changedMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_revoked_your_admin_privileges, describe(change.getEditor())));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_revoked_your_admin_privileges, editor)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_revoked_admin_privileges_from_s, describe(change.getEditor()), describe(roleChange.getUuid())));
|
||||
updates.add(updateDescription(change.getEditor(), roleChange.getUuid(), (editor, oldAdmin) -> context.getString(R.string.MessageRecord_s_revoked_admin_privileges_from_s, editor, oldAdmin)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorModifyMemberRoles(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorModifyMemberRoles(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
for (DecryptedModifyMemberRole roleChange : change.getModifyMemberRolesList()) {
|
||||
boolean changedMemberIsYou = roleChange.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (roleChange.getRole() == Member.Role.ADMINISTRATOR) {
|
||||
if (changedMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_are_now_an_admin));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_now_an_admin)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_is_now_an_admin, describe(roleChange.getUuid())));
|
||||
updates.add(updateDescription(roleChange.getUuid(), newAdmin -> context.getString(R.string.MessageRecord_s_is_now_an_admin, newAdmin)));
|
||||
}
|
||||
} else {
|
||||
if (changedMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_are_no_longer_an_admin));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_no_longer_an_admin)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_is_no_longer_an_admin, describe(roleChange.getUuid())));
|
||||
updates.add(updateDescription(roleChange.getUuid(), oldAdmin -> context.getString(R.string.MessageRecord_s_is_no_longer_an_admin, oldAdmin)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeInvitations(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
int notYouInviteCount = 0;
|
||||
private void describeInvitations(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
int notYouInviteCount = 0;
|
||||
|
||||
for (DecryptedPendingMember invitee : change.getNewPendingMembersList()) {
|
||||
boolean newMemberIsYou = invitee.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (newMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_invited_you_to_the_group, describe(change.getEditor())));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, editor)));
|
||||
} else {
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_invited_s_to_the_group, describe(invitee.getUuid())));
|
||||
updates.add(updateDescription(invitee.getUuid(), newInvitee -> context.getString(R.string.MessageRecord_you_invited_s_to_the_group, newInvitee)));
|
||||
} else {
|
||||
notYouInviteCount++;
|
||||
}
|
||||
|
@ -278,39 +280,40 @@ final class GroupsV2UpdateMessageProducer {
|
|||
}
|
||||
|
||||
if (notYouInviteCount > 0) {
|
||||
updates.add(context.getResources().getQuantityString(R.plurals.MessageRecord_s_invited_members, notYouInviteCount, describe(change.getEditor()), notYouInviteCount));
|
||||
final int notYouInviteCountFinalCopy = notYouInviteCount;
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getResources().getQuantityString(R.plurals.MessageRecord_s_invited_members, notYouInviteCountFinalCopy, editor, notYouInviteCountFinalCopy)));
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorInvitations(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorInvitations(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
int notYouInviteCount = 0;
|
||||
|
||||
for (DecryptedPendingMember invitee : change.getNewPendingMembersList()) {
|
||||
boolean newMemberIsYou = invitee.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (newMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_were_invited_to_the_group));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_were_invited_to_the_group)));
|
||||
} else {
|
||||
notYouInviteCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (notYouInviteCount > 0) {
|
||||
updates.add(context.getResources().getQuantityString(R.plurals.MessageRecord_d_people_were_invited_to_the_group, notYouInviteCount, notYouInviteCount));
|
||||
updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_d_people_were_invited_to_the_group, notYouInviteCount, notYouInviteCount)));
|
||||
}
|
||||
}
|
||||
|
||||
private void describeRevokedInvitations(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
int notDeclineCount = 0;
|
||||
private void describeRevokedInvitations(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
int notDeclineCount = 0;
|
||||
|
||||
for (DecryptedPendingMemberRemoval invitee : change.getDeletePendingMembersList()) {
|
||||
boolean decline = invitee.getUuid().equals(change.getEditor());
|
||||
if (decline) {
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_declined_the_invitation_to_the_group));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_declined_the_invitation_to_the_group)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_someone_declined_an_invitation_to_the_group));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_someone_declined_an_invitation_to_the_group)));
|
||||
}
|
||||
} else {
|
||||
notDeclineCount++;
|
||||
|
@ -319,176 +322,201 @@ final class GroupsV2UpdateMessageProducer {
|
|||
|
||||
if (notDeclineCount > 0) {
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getResources().getQuantityString(R.plurals.MessageRecord_you_revoked_invites, notDeclineCount, notDeclineCount));
|
||||
updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_you_revoked_invites, notDeclineCount, notDeclineCount)));
|
||||
} else {
|
||||
updates.add(context.getResources().getQuantityString(R.plurals.MessageRecord_s_revoked_invites, notDeclineCount, describe(change.getEditor()), notDeclineCount));
|
||||
final int notDeclineCountFinalCopy = notDeclineCount;
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getResources().getQuantityString(R.plurals.MessageRecord_s_revoked_invites, notDeclineCountFinalCopy, editor, notDeclineCountFinalCopy)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorRevokedInvitations(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorRevokedInvitations(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
int notDeclineCount = 0;
|
||||
|
||||
for (DecryptedPendingMemberRemoval invitee : change.getDeletePendingMembersList()) {
|
||||
boolean inviteeWasYou = invitee.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (inviteeWasYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_your_invitation_to_the_group_was_revoked));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_invitation_to_the_group_was_revoked)));
|
||||
} else {
|
||||
notDeclineCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (notDeclineCount > 0) {
|
||||
updates.add(context.getResources().getQuantityString(R.plurals.MessageRecord_d_invitations_were_revoked, notDeclineCount, notDeclineCount));
|
||||
updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_d_invitations_were_revoked, notDeclineCount, notDeclineCount)));
|
||||
}
|
||||
}
|
||||
|
||||
private void describePromotePending(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describePromotePending(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
for (DecryptedMember newMember : change.getPromotePendingMembersList()) {
|
||||
ByteString uuid = newMember.getUuid();
|
||||
boolean newMemberIsYou = uuid.equals(selfUuidBytes);
|
||||
ByteString uuid = newMember.getUuid();
|
||||
boolean newMemberIsYou = uuid.equals(selfUuidBytes);
|
||||
|
||||
if (editorIsYou) {
|
||||
if (newMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_accepted_invite));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_accepted_invite)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_added_invited_member_s, describe(uuid)));
|
||||
updates.add(updateDescription(uuid, newPromotedMember -> context.getString(R.string.MessageRecord_you_added_invited_member_s, newPromotedMember)));
|
||||
}
|
||||
} else {
|
||||
if (newMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_added_you, describe(change.getEditor())));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_added_you, editor)));
|
||||
} else {
|
||||
if (uuid.equals(change.getEditor())) {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_accepted_invite, describe(uuid)));
|
||||
updates.add(updateDescription(uuid, newAcceptedMember -> context.getString(R.string.MessageRecord_s_accepted_invite, newAcceptedMember)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_added_invited_member_s, describe(change.getEditor()), describe(uuid)));
|
||||
updates.add(updateDescription(change.getEditor(), uuid, (editor, newAcceptedMember) -> context.getString(R.string.MessageRecord_s_added_invited_member_s, editor, newAcceptedMember)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorPromotePending(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorPromotePending(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
for (DecryptedMember newMember : change.getPromotePendingMembersList()) {
|
||||
ByteString uuid = newMember.getUuid();
|
||||
boolean newMemberIsYou = uuid.equals(selfUuidBytes);
|
||||
ByteString uuid = newMember.getUuid();
|
||||
boolean newMemberIsYou = uuid.equals(selfUuidBytes);
|
||||
|
||||
if (newMemberIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_joined_the_group));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_joined_the_group, describe(uuid)));
|
||||
updates.add(updateDescription(uuid, newMemberName -> context.getString(R.string.MessageRecord_s_joined_the_group, newMemberName)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeNewTitle(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeNewTitle(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
if (change.hasNewTitle()) {
|
||||
String newTitle = change.getNewTitle().getValue();
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_changed_the_group_name_to_s, change.getNewTitle().getValue()));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_name_to_s, newTitle)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_changed_the_group_name_to_s, describe(change.getEditor()), change.getNewTitle().getValue()));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_the_group_name_to_s, editor, newTitle)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorNewTitle(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorNewTitle(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
if (change.hasNewTitle()) {
|
||||
updates.add(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, change.getNewTitle().getValue()));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, change.getNewTitle().getValue())));
|
||||
}
|
||||
}
|
||||
|
||||
private void describeNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
if (change.hasNewAvatar()) {
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_changed_the_group_avatar));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_avatar)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_changed_the_group_avatar, describe(change.getEditor())));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_the_group_avatar, editor)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
if (change.hasNewAvatar()) {
|
||||
updates.add(context.getString(R.string.MessageRecord_the_group_group_avatar_has_been_changed));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_group_avatar_has_been_changed)));
|
||||
}
|
||||
}
|
||||
|
||||
private void describeNewTimer(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeNewTimer(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
if (change.hasNewTimer()) {
|
||||
String time = ExpirationUtil.getExpirationDisplayValue(context, change.getNewTimer().getDuration());
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, describe(change.getEditor()), time));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, editor, time)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorNewTimer(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorNewTimer(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
if (change.hasNewTimer()) {
|
||||
String time = ExpirationUtil.getExpirationDisplayValue(context, change.getNewTimer().getDuration());
|
||||
updates.add(context.getString(R.string.MessageRecord_disappearing_message_time_set_to_s, time));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_disappearing_message_time_set_to_s, time)));
|
||||
}
|
||||
}
|
||||
|
||||
private void describeNewAttributeAccess(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeNewAttributeAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
if (change.getNewAttributeAccess() != AccessControl.AccessRequired.UNKNOWN) {
|
||||
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewAttributeAccess());
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_info_to_s, accessLevel));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_info_to_s, accessLevel)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_info_to_s, describe(change.getEditor()), accessLevel));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_info_to_s, editor, accessLevel)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorNewAttributeAccess(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorNewAttributeAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
if (change.getNewAttributeAccess() != AccessControl.AccessRequired.UNKNOWN) {
|
||||
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewAttributeAccess());
|
||||
updates.add(context.getString(R.string.MessageRecord_who_can_edit_group_info_has_been_changed_to_s, accessLevel));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_who_can_edit_group_info_has_been_changed_to_s, accessLevel)));
|
||||
}
|
||||
}
|
||||
|
||||
private void describeNewMembershipAccess(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeNewMembershipAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
if (change.getNewMemberAccess() != AccessControl.AccessRequired.UNKNOWN) {
|
||||
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewMemberAccess());
|
||||
if (editorIsYou) {
|
||||
updates.add(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_membership_to_s, accessLevel));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_membership_to_s, accessLevel)));
|
||||
} else {
|
||||
updates.add(context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_membership_to_s, describe(change.getEditor()), accessLevel));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_membership_to_s, editor, accessLevel)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorNewMembershipAccess(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
|
||||
private void describeUnknownEditorNewMembershipAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
if (change.getNewMemberAccess() != AccessControl.AccessRequired.UNKNOWN) {
|
||||
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewMemberAccess());
|
||||
updates.add(context.getString(R.string.MessageRecord_who_can_edit_group_membership_has_been_changed_to_s, accessLevel));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_who_can_edit_group_membership_has_been_changed_to_s, accessLevel)));
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull String describe(@NonNull ByteString uuid) {
|
||||
return descriptionStrategy.describe(UuidUtil.fromByteString(uuid));
|
||||
}
|
||||
|
||||
interface DescribeMemberStrategy {
|
||||
|
||||
/**
|
||||
* Map a UUID to a string that describes the group member.
|
||||
*/
|
||||
@NonNull String describe(@NonNull UUID uuid);
|
||||
@NonNull
|
||||
@WorkerThread
|
||||
String describe(@NonNull UUID uuid);
|
||||
}
|
||||
|
||||
private interface StringFactory1Arg {
|
||||
String create(String arg1);
|
||||
}
|
||||
|
||||
private interface StringFactory2Args {
|
||||
String create(String arg1, String arg2);
|
||||
}
|
||||
|
||||
private static UpdateDescription updateDescription(@NonNull String string) {
|
||||
return UpdateDescription.staticDescription(string);
|
||||
}
|
||||
|
||||
private UpdateDescription updateDescription(@NonNull ByteString uuid1Bytes, @NonNull StringFactory1Arg stringFactory) {
|
||||
UUID uuid1 = UuidUtil.fromByteStringOrUnknown(uuid1Bytes);
|
||||
|
||||
return UpdateDescription.mentioning(Collections.singletonList(uuid1), () -> stringFactory.create(descriptionStrategy.describe(uuid1)));
|
||||
}
|
||||
|
||||
private UpdateDescription updateDescription(@NonNull ByteString uuid1Bytes, @NonNull ByteString uuid2Bytes, @NonNull StringFactory2Args stringFactory) {
|
||||
UUID uuid1 = UuidUtil.fromByteStringOrUnknown(uuid1Bytes);
|
||||
UUID uuid2 = UuidUtil.fromByteStringOrUnknown(uuid2Bytes);
|
||||
|
||||
return UpdateDescription.mentioning(Arrays.asList(uuid1, uuid2), () -> stringFactory.create(descriptionStrategy.describe(uuid1), descriptionStrategy.describe(uuid2)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Function;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class LiveUpdateMessage {
|
||||
|
||||
/**
|
||||
* Creates a live data that observes the recipients mentioned in the {@link UpdateDescription} and
|
||||
* recreates the string asynchronously when they change.
|
||||
*/
|
||||
@AnyThread
|
||||
public static LiveData<String> fromMessageDescription(@NonNull UpdateDescription updateDescription) {
|
||||
if (updateDescription.isStringStatic()) {
|
||||
return LiveDataUtil.just(updateDescription.getStaticString());
|
||||
}
|
||||
|
||||
List<LiveData<Recipient>> allMentionedRecipients = Stream.of(updateDescription.getMentioned())
|
||||
.map(uuid -> Recipient.resolved(RecipientId.from(uuid, null)).live().getLiveData())
|
||||
.toList();
|
||||
|
||||
LiveData<?> mentionedRecipientChangeStream = allMentionedRecipients.isEmpty() ? LiveDataUtil.just(new Object())
|
||||
: LiveDataUtil.merge(allMentionedRecipients);
|
||||
|
||||
return LiveDataUtil.mapAsync(mentionedRecipientChangeStream, event -> updateDescription.getString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes a single recipient and recreates the string asynchronously when they change.
|
||||
*/
|
||||
public static LiveData<String> recipientToStringAsync(@NonNull RecipientId recipientId,
|
||||
@NonNull Function<Recipient, String> createStringInBackground)
|
||||
{
|
||||
return LiveDataUtil.mapAsync(Recipient.live(recipientId).getLiveData(), createStringInBackground);
|
||||
}
|
||||
|
||||
}
|
|
@ -23,8 +23,8 @@ import android.text.style.RelativeSizeSpan;
|
|||
import android.text.style.StyleSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
|
@ -39,9 +39,11 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
|||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Function;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
|
@ -109,78 +111,84 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||
|
||||
@Override
|
||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
||||
if (isGroupUpdate() && isGroupV2()) {
|
||||
return new SpannableString(getGv2Description(context));
|
||||
} else if (isGroupUpdate() && isOutgoing()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_you_updated_group));
|
||||
} else if (isGroupUpdate()) {
|
||||
return new SpannableString(GroupUtil.getDescription(context, getBody(), false).toString(getIndividualRecipient()));
|
||||
} else if (isGroupQuit() && isOutgoing()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_left_group));
|
||||
} else if (isGroupQuit()) {
|
||||
return new SpannableString(context.getString(R.string.ConversationItem_group_action_left, getIndividualRecipient().getDisplayName(context)));
|
||||
} else if (isIncomingCall()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_s_called_you, getIndividualRecipient().getDisplayName(context)));
|
||||
} else if (isOutgoingCall()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_you_called));
|
||||
} else if (isMissedCall()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_missed_call));
|
||||
} else if (isJoined()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_s_joined_signal, getIndividualRecipient().getDisplayName(context)));
|
||||
} else if (isExpirationTimerUpdate()) {
|
||||
int seconds = (int)(getExpiresIn() / 1000);
|
||||
if (seconds <= 0) {
|
||||
return isOutgoing() ? new SpannableString(context.getString(R.string.MessageRecord_you_disabled_disappearing_messages))
|
||||
: new SpannableString(context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, getIndividualRecipient().getDisplayName(context)));
|
||||
}
|
||||
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
|
||||
return isOutgoing() ? new SpannableString(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time))
|
||||
: new SpannableString(context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, getIndividualRecipient().getDisplayName(context), time));
|
||||
} else if (isIdentityUpdate()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_your_safety_number_with_s_has_changed, getIndividualRecipient().getDisplayName(context)));
|
||||
} else if (isIdentityVerified()) {
|
||||
if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified, getIndividualRecipient().getDisplayName(context)));
|
||||
else return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device, getIndividualRecipient().getDisplayName(context)));
|
||||
} else if (isIdentityDefault()) {
|
||||
if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified, getIndividualRecipient().getDisplayName(context)));
|
||||
else return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device, getIndividualRecipient().getDisplayName(context)));
|
||||
} else if (isProfileChange()) {
|
||||
return new SpannableString(getProfileChangeDescription(context));
|
||||
UpdateDescription updateDisplayBody = getUpdateDisplayBody(context);
|
||||
|
||||
if (updateDisplayBody != null) {
|
||||
return new SpannableString(updateDisplayBody.getString());
|
||||
}
|
||||
|
||||
return new SpannableString(getBody());
|
||||
}
|
||||
|
||||
private @NonNull String getGv2Description(@NonNull Context context) {
|
||||
if (!isGroupUpdate() || !isGroupV2()) {
|
||||
throw new AssertionError();
|
||||
public @Nullable UpdateDescription getUpdateDisplayBody(@NonNull Context context) {
|
||||
if (isGroupUpdate() && isGroupV2()) {
|
||||
return getGv2ChangeDescription(context, getBody());
|
||||
} else if (isGroupUpdate() && isOutgoing()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_you_updated_group));
|
||||
} else if (isGroupUpdate()) {
|
||||
return fromRecipient(getIndividualRecipient(), r -> GroupUtil.getNonV2GroupDescription(context, getBody()).toString(r));
|
||||
} else if (isGroupQuit() && isOutgoing()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_left_group));
|
||||
} else if (isGroupQuit()) {
|
||||
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.ConversationItem_group_action_left, r.getDisplayName(context)));
|
||||
} else if (isIncomingCall()) {
|
||||
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_called_you, r.getDisplayName(context)));
|
||||
} else if (isOutgoingCall()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_you_called));
|
||||
} else if (isMissedCall()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_missed_call));
|
||||
} else if (isJoined()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_s_joined_signal, getIndividualRecipient().getDisplayName(context)));
|
||||
} else if (isExpirationTimerUpdate()) {
|
||||
int seconds = (int)(getExpiresIn() / 1000);
|
||||
if (seconds <= 0) {
|
||||
return isOutgoing() ? staticUpdateDescription(context.getString(R.string.MessageRecord_you_disabled_disappearing_messages))
|
||||
: fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, r.getDisplayName(context)));
|
||||
}
|
||||
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
|
||||
return isOutgoing() ? staticUpdateDescription(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time))
|
||||
: fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, r.getDisplayName(context), time));
|
||||
} else if (isIdentityUpdate()) {
|
||||
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context)));
|
||||
} else if (isIdentityVerified()) {
|
||||
if (isOutgoing()) return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified, r.getDisplayName(context)));
|
||||
else return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device, r.getDisplayName(context)));
|
||||
} else if (isIdentityDefault()) {
|
||||
if (isOutgoing()) return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified, r.getDisplayName(context)));
|
||||
else return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device, r.getDisplayName(context)));
|
||||
} else if (isProfileChange()) {
|
||||
return staticUpdateDescription(getProfileChangeDescription(context));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static @NonNull UpdateDescription getGv2ChangeDescription(@NonNull Context context, @NonNull String body) {
|
||||
try {
|
||||
ShortStringDescriptionStrategy descriptionStrategy = new ShortStringDescriptionStrategy(context);
|
||||
byte[] decoded = Base64.decode(getBody());
|
||||
byte[] decoded = Base64.decode(body);
|
||||
DecryptedGroupV2Context decryptedGroupV2Context = DecryptedGroupV2Context.parseFrom(decoded);
|
||||
GroupsV2UpdateMessageProducer updateMessageProducer = new GroupsV2UpdateMessageProducer(context, descriptionStrategy, Recipient.self().getUuid().get());
|
||||
|
||||
if (decryptedGroupV2Context.hasChange() && decryptedGroupV2Context.getGroupState().getRevision() > 0) {
|
||||
DecryptedGroupChange change = decryptedGroupV2Context.getChange();
|
||||
List<String> strings = updateMessageProducer.describeChange(change);
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < strings.size(); i++) {
|
||||
if (i > 0) result.append('\n');
|
||||
result.append(strings.get(i));
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
return UpdateDescription.concatWithNewLines(updateMessageProducer.describeChanges(decryptedGroupV2Context.getChange()));
|
||||
} else {
|
||||
return updateMessageProducer.describeNewGroup(decryptedGroupV2Context.getGroupState());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "GV2 Message update detail could not be read", e);
|
||||
return context.getString(R.string.MessageRecord_group_updated);
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_group_updated));
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull UpdateDescription fromRecipient(@NonNull Recipient recipient, @NonNull Function<Recipient, String> stringFunction) {
|
||||
return UpdateDescription.mentioning(Collections.singletonList(recipient.getUuid().or(UuidUtil.UNKNOWN_UUID)), () -> stringFunction.apply(recipient.resolve()));
|
||||
}
|
||||
|
||||
private static @NonNull UpdateDescription staticUpdateDescription(@NonNull String string) {
|
||||
return UpdateDescription.staticDescription(string);
|
||||
}
|
||||
|
||||
private @NonNull String getProfileChangeDescription(@NonNull Context context) {
|
||||
try {
|
||||
byte[] decoded = Base64.decode(getBody());
|
||||
|
@ -316,7 +324,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||
return isFailed() && ((getRecipient().isPushGroup() && hasNetworkFailures()) || !isIdentityMismatchFailure());
|
||||
}
|
||||
|
||||
protected SpannableString emphasisAdded(String sequence) {
|
||||
protected static SpannableString emphasisAdded(String sequence) {
|
||||
SpannableString spannable = new SpannableString(sequence);
|
||||
spannable.setSpan(new RelativeSizeSpan(0.9f), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Contains a list of people mentioned in an update message and a function to create the update message.
|
||||
*/
|
||||
public final class UpdateDescription {
|
||||
|
||||
public interface StringFactory {
|
||||
@WorkerThread
|
||||
String create();
|
||||
}
|
||||
|
||||
private final Collection<UUID> mentioned;
|
||||
private final StringFactory stringFactory;
|
||||
private final String staticString;
|
||||
|
||||
private UpdateDescription(@NonNull Collection<UUID> mentioned,
|
||||
@Nullable StringFactory stringFactory,
|
||||
@Nullable String staticString)
|
||||
{
|
||||
if (staticString == null && stringFactory == null) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
this.mentioned = mentioned;
|
||||
this.stringFactory = stringFactory;
|
||||
this.staticString = staticString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an update description which has a string value created by a supplied factory method that
|
||||
* will be run on a background thread.
|
||||
*
|
||||
* @param mentioned UUIDs of recipients that are mentioned in the string.
|
||||
* @param stringFactory The background method for generating the string.
|
||||
*/
|
||||
public static UpdateDescription mentioning(@NonNull Collection<UUID> mentioned,
|
||||
@NonNull StringFactory stringFactory)
|
||||
{
|
||||
return new UpdateDescription(UuidUtil.filterKnown(mentioned),
|
||||
stringFactory,
|
||||
null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an update description that's string value is fixed.
|
||||
*/
|
||||
public static UpdateDescription staticDescription(@NonNull String staticString) {
|
||||
return new UpdateDescription(Collections.emptyList(), null, staticString);
|
||||
}
|
||||
|
||||
public boolean isStringStatic() {
|
||||
return staticString != null;
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public @NonNull String getStaticString() {
|
||||
if (staticString == null) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
return staticString;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public @NonNull String getString() {
|
||||
if (staticString != null) {
|
||||
return staticString;
|
||||
}
|
||||
|
||||
Util.assertNotMainThread();
|
||||
|
||||
//noinspection ConstantConditions
|
||||
return stringFactory.create();
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public Collection<UUID> getMentioned() {
|
||||
return mentioned;
|
||||
}
|
||||
|
||||
public static UpdateDescription concatWithNewLines(@NonNull List<UpdateDescription> updateDescriptions) {
|
||||
if (updateDescriptions.size() == 0) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
if (updateDescriptions.size() == 1) {
|
||||
return updateDescriptions.get(0);
|
||||
}
|
||||
|
||||
if (allAreStatic(updateDescriptions)) {
|
||||
return UpdateDescription.staticDescription(concatStaticLines(updateDescriptions));
|
||||
}
|
||||
|
||||
Set<UUID> allMentioned = new HashSet<>();
|
||||
|
||||
for (UpdateDescription updateDescription : updateDescriptions) {
|
||||
allMentioned.addAll(updateDescription.getMentioned());
|
||||
}
|
||||
|
||||
return UpdateDescription.mentioning(allMentioned, () -> concatLines(updateDescriptions));
|
||||
}
|
||||
|
||||
private static boolean allAreStatic(@NonNull Collection<UpdateDescription> updateDescriptions) {
|
||||
for (UpdateDescription description : updateDescriptions) {
|
||||
if (!description.isStringStatic()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static String concatLines(@NonNull List<UpdateDescription> updateDescriptions) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < updateDescriptions.size(); i++) {
|
||||
if (i > 0) result.append('\n');
|
||||
result.append(updateDescriptions.get(i).getString());
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
private static String concatStaticLines(@NonNull List<UpdateDescription> updateDescriptions) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < updateDescriptions.size(); i++) {
|
||||
if (i > 0) result.append('\n');
|
||||
result.append(updateDescriptions.get(i).getStaticString());
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
}
|
|
@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.groups.GroupId;
|
|||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MessageGroupContext;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
|
@ -67,13 +66,13 @@ public final class GroupUtil {
|
|||
return Optional.absent();
|
||||
}
|
||||
|
||||
public static @NonNull GroupDescription getDescription(@NonNull Context context, @Nullable String encodedGroup, boolean isV2) {
|
||||
public static @NonNull GroupDescription getNonV2GroupDescription(@NonNull Context context, @Nullable String encodedGroup) {
|
||||
if (encodedGroup == null) {
|
||||
return new GroupDescription(context, null);
|
||||
}
|
||||
|
||||
try {
|
||||
MessageGroupContext groupContext = new MessageGroupContext(encodedGroup, isV2);
|
||||
MessageGroupContext groupContext = new MessageGroupContext(encodedGroup, false);
|
||||
return new GroupDescription(context, groupContext);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
|
@ -117,7 +116,8 @@ public final class GroupUtil {
|
|||
}
|
||||
}
|
||||
|
||||
public String toString(Recipient sender) {
|
||||
@WorkerThread
|
||||
public String toString(@NonNull Recipient sender) {
|
||||
StringBuilder description = new StringBuilder();
|
||||
description.append(context.getString(R.string.MessageRecord_s_updated_group, sender.getDisplayName(context)));
|
||||
|
||||
|
@ -142,22 +142,6 @@ public final class GroupUtil {
|
|||
return description.toString();
|
||||
}
|
||||
|
||||
public void addObserver(RecipientForeverObserver listener) {
|
||||
if (this.members != null) {
|
||||
for (RecipientId member : this.members) {
|
||||
Recipient.live(member).observeForever(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeObserver(RecipientForeverObserver listener) {
|
||||
if (this.members != null) {
|
||||
for (RecipientId member : this.members) {
|
||||
Recipient.live(member).removeForeverObserver(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String toString(List<RecipientId> recipients) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
|
|
|
@ -11,6 +11,10 @@ import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor;
|
|||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.util.guava.Function;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public final class LiveDataUtil {
|
||||
|
@ -78,6 +82,34 @@ public final class LiveDataUtil {
|
|||
return new CombineLiveData<>(a, b, combine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the supplied live data streams.
|
||||
*/
|
||||
public static <T> LiveData<T> merge(@NonNull List<LiveData<T>> liveDataList) {
|
||||
Set<LiveData<T>> set = new LinkedHashSet<>(liveDataList);
|
||||
|
||||
set.addAll(liveDataList);
|
||||
|
||||
if (set.size() == 1) {
|
||||
return liveDataList.get(0);
|
||||
}
|
||||
|
||||
MediatorLiveData<T> mergedLiveData = new MediatorLiveData<>();
|
||||
|
||||
for (LiveData<T> liveDataSource : set) {
|
||||
mergedLiveData.addSource(liveDataSource, mergedLiveData::setValue);
|
||||
}
|
||||
|
||||
return mergedLiveData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Live data with just the initial value.
|
||||
*/
|
||||
public static <T> LiveData<T> just(@NonNull T item) {
|
||||
return new MutableLiveData<>(item);
|
||||
}
|
||||
|
||||
public interface Combine<A, B, R> {
|
||||
@NonNull R apply(@NonNull A a, @NonNull B b);
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="7dp"
|
||||
android:gravity="center"
|
||||
|
@ -50,7 +50,7 @@
|
|||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/conversation_update_body"
|
||||
style="@style/Signal.Text.Preview"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autoLink="all"
|
||||
android:gravity="center"
|
||||
|
|
|
@ -5,11 +5,16 @@ import android.app.Application;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.modules.junit4.rule.PowerMockRule;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
|
@ -22,19 +27,27 @@ import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
|||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedString;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
|
||||
import org.thoughtcrime.securesms.testutil.MainThreadUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(manifest = Config.NONE, application = Application.class)
|
||||
@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*", "androidx.*" })
|
||||
@PrepareForTest(Util.class)
|
||||
public final class GroupsV2UpdateMessageProducerTest {
|
||||
|
||||
private UUID you;
|
||||
|
@ -43,6 +56,9 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
|
||||
private GroupsV2UpdateMessageProducer producer;
|
||||
|
||||
@Rule
|
||||
public PowerMockRule powerMockRule = new PowerMockRule();
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
you = UUID.randomUUID();
|
||||
|
@ -57,7 +73,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
DecryptedGroupChange change = changeBy(alice)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Alice updated the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("Alice updated the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -65,7 +81,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
DecryptedGroupChange change = changeBy(you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You updated the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("You updated the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -73,7 +89,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
DecryptedGroupChange change = changeByUnknown()
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("The group was updated.")));
|
||||
assertThat(describeChange(change), is(singletonList("The group was updated.")));
|
||||
}
|
||||
|
||||
// Member additions
|
||||
|
@ -84,7 +100,16 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.addMember(bob)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Alice added Bob.")));
|
||||
assertThat(describeChange(change), is(singletonList("Alice added Bob.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_added_member_mentions_both() {
|
||||
DecryptedGroupChange change = changeBy(alice)
|
||||
.addMember(bob)
|
||||
.build();
|
||||
|
||||
assertSingleChangeMentioning(change, Arrays.asList(alice, bob));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -93,7 +118,16 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.addMember(bob)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You added Bob.")));
|
||||
assertThat(describeChange(change), is(singletonList("You added Bob.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void you_added_member_mentions_just_member() {
|
||||
DecryptedGroupChange change = changeBy(you)
|
||||
.addMember(bob)
|
||||
.build();
|
||||
|
||||
assertSingleChangeMentioning(change, singletonList(bob));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -102,7 +136,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.addMember(you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Alice added you to the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("Alice added you to the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -111,7 +145,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.addMember(you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You joined the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("You joined the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -120,7 +154,16 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.addMember(bob)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Bob joined the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("Bob joined the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_added_themselves_mentions_just_member() {
|
||||
DecryptedGroupChange change = changeBy(bob)
|
||||
.addMember(bob)
|
||||
.build();
|
||||
|
||||
assertSingleChangeMentioning(change, singletonList(bob));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -129,7 +172,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.addMember(you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You joined the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("You joined the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -138,18 +181,18 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.addMember(bob)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Bob joined the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("Bob joined the group.")));
|
||||
}
|
||||
|
||||
// Member removals
|
||||
|
||||
// Member removals
|
||||
@Test
|
||||
public void member_removed_member() {
|
||||
DecryptedGroupChange change = changeBy(alice)
|
||||
.deleteMember(bob)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Alice removed Bob.")));
|
||||
assertThat(describeChange(change), is(singletonList("Alice removed Bob.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -158,7 +201,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.deleteMember(bob)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You removed Bob.")));
|
||||
assertThat(describeChange(change), is(singletonList("You removed Bob.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -167,7 +210,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.deleteMember(you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Alice removed you from the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("Alice removed you from the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -176,7 +219,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.deleteMember(you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You left the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("You left the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -185,7 +228,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.deleteMember(bob)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Bob left the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("Bob left the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -194,7 +237,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.deleteMember(alice)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Alice is no longer in the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("Alice is no longer in the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -203,7 +246,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.deleteMember(you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You are no longer in the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("You are no longer in the group.")));
|
||||
}
|
||||
|
||||
// Member role modifications
|
||||
|
@ -214,7 +257,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.promoteToAdmin(alice)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You made Alice an admin.")));
|
||||
assertThat(describeChange(change), is(singletonList("You made Alice an admin.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -223,7 +266,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.promoteToAdmin(alice)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Bob made Alice an admin.")));
|
||||
assertThat(describeChange(change), is(singletonList("Bob made Alice an admin.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -232,7 +275,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.promoteToAdmin(you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Alice made you an admin.")));
|
||||
assertThat(describeChange(change), is(singletonList("Alice made you an admin.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -241,7 +284,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.demoteToMember(bob)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You revoked admin privileges from Bob.")));
|
||||
assertThat(describeChange(change), is(singletonList("You revoked admin privileges from Bob.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -250,7 +293,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.demoteToMember(alice)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Bob revoked admin privileges from Alice.")));
|
||||
assertThat(describeChange(change), is(singletonList("Bob revoked admin privileges from Alice.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -259,7 +302,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.demoteToMember(you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Alice revoked your admin privileges.")));
|
||||
assertThat(describeChange(change), is(singletonList("Alice revoked your admin privileges.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -268,7 +311,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.promoteToAdmin(alice)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Alice is now an admin.")));
|
||||
assertThat(describeChange(change), is(singletonList("Alice is now an admin.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -277,7 +320,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.promoteToAdmin(you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You are now an admin.")));
|
||||
assertThat(describeChange(change), is(singletonList("You are now an admin.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -286,7 +329,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.demoteToMember(alice)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Alice is no longer an admin.")));
|
||||
assertThat(describeChange(change), is(singletonList("Alice is no longer an admin.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -295,7 +338,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.demoteToMember(you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You are no longer an admin.")));
|
||||
assertThat(describeChange(change), is(singletonList("You are no longer an admin.")));
|
||||
}
|
||||
|
||||
// Member invitation
|
||||
|
@ -306,7 +349,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.invite(alice)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You invited Alice to the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("You invited Alice to the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -315,7 +358,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.invite(you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Alice invited you to the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("Alice invited you to the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -324,7 +367,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.invite(bob)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Alice invited 1 person to the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("Alice invited 1 person to the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -334,7 +377,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.invite(UUID.randomUUID())
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Alice invited 2 people to the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("Alice invited 2 people to the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -346,7 +389,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.invite(UUID.randomUUID())
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(Arrays.asList("Bob invited you to the group.", "Bob invited 3 people to the group.")));
|
||||
assertThat(describeChange(change), is(Arrays.asList("Bob invited you to the group.", "Bob invited 3 people to the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -355,7 +398,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.invite(you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You were invited to the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("You were invited to the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -364,7 +407,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.invite(alice)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("1 person was invited to the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("1 person was invited to the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -374,7 +417,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.invite(bob)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("2 people were invited to the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("2 people were invited to the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -386,7 +429,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.invite(UUID.randomUUID())
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(Arrays.asList("You were invited to the group.", "3 people were invited to the group.")));
|
||||
assertThat(describeChange(change), is(Arrays.asList("You were invited to the group.", "3 people were invited to the group.")));
|
||||
}
|
||||
|
||||
// Member invitation revocation
|
||||
|
@ -397,7 +440,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.uninvite(bob)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Alice revoked an invitation to the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("Alice revoked an invitation to the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -407,7 +450,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.uninvite(UUID.randomUUID())
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Alice revoked 2 invitations to the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("Alice revoked 2 invitations to the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -416,7 +459,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.uninvite(bob)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You revoked an invitation to the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("You revoked an invitation to the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -426,7 +469,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.uninvite(UUID.randomUUID())
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You revoked 2 invitations to the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("You revoked 2 invitations to the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -435,7 +478,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.uninvite(bob)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Someone declined an invitation to the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("Someone declined an invitation to the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -444,7 +487,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.uninvite(you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You declined the invitation to the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("You declined the invitation to the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -453,7 +496,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.uninvite(you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Your invitation to the group was revoked.")));
|
||||
assertThat(describeChange(change), is(singletonList("Your invitation to the group was revoked.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -462,7 +505,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.uninvite(bob)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("An invitation to the group was revoked.")));
|
||||
assertThat(describeChange(change), is(singletonList("An invitation to the group was revoked.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -472,7 +515,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.uninvite(UUID.randomUUID())
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("2 invitations to the group were revoked.")));
|
||||
assertThat(describeChange(change), is(singletonList("2 invitations to the group were revoked.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -484,7 +527,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.uninvite(UUID.randomUUID())
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(Arrays.asList("Your invitation to the group was revoked.", "3 invitations to the group were revoked.")));
|
||||
assertThat(describeChange(change), is(Arrays.asList("Your invitation to the group was revoked.", "3 invitations to the group were revoked.")));
|
||||
}
|
||||
|
||||
// Promote pending members
|
||||
|
@ -495,7 +538,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.promote(bob)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Bob accepted an invitation to the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("Bob accepted an invitation to the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -504,7 +547,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.promote(you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You accepted the invitation to the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("You accepted the invitation to the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -513,7 +556,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.promote(alice)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Bob added invited member Alice.")));
|
||||
assertThat(describeChange(change), is(singletonList("Bob added invited member Alice.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -522,7 +565,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.promote(bob)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You added invited member Bob.")));
|
||||
assertThat(describeChange(change), is(singletonList("You added invited member Bob.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -531,7 +574,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.promote(you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Bob added you to the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("Bob added you to the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -540,7 +583,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.promote(you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You joined the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("You joined the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -549,7 +592,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.promote(alice)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Alice joined the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("Alice joined the group.")));
|
||||
}
|
||||
|
||||
// Title change
|
||||
|
@ -560,7 +603,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.title("New title")
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Alice changed the group name to \"New title\".")));
|
||||
assertThat(describeChange(change), is(singletonList("Alice changed the group name to \"New title\".")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -569,7 +612,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.title("Title 2")
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You changed the group name to \"Title 2\".")));
|
||||
assertThat(describeChange(change), is(singletonList("You changed the group name to \"Title 2\".")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -578,7 +621,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.title("Title 3")
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("The group name has changed to \"Title 3\".")));
|
||||
assertThat(describeChange(change), is(singletonList("The group name has changed to \"Title 3\".")));
|
||||
}
|
||||
|
||||
// Avatar change
|
||||
|
@ -589,7 +632,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.avatar("Avatar1")
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Alice changed the group avatar.")));
|
||||
assertThat(describeChange(change), is(singletonList("Alice changed the group avatar.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -598,7 +641,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.avatar("Avatar2")
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You changed the group avatar.")));
|
||||
assertThat(describeChange(change), is(singletonList("You changed the group avatar.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -607,7 +650,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.avatar("Avatar3")
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("The group avatar has been changed.")));
|
||||
assertThat(describeChange(change), is(singletonList("The group avatar has been changed.")));
|
||||
}
|
||||
|
||||
// Timer change
|
||||
|
@ -618,7 +661,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.timer(10)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Bob set the disappearing message timer to 10 seconds.")));
|
||||
assertThat(describeChange(change), is(singletonList("Bob set the disappearing message timer to 10 seconds.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -627,7 +670,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.timer(60)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You set the disappearing message timer to 1 minute.")));
|
||||
assertThat(describeChange(change), is(singletonList("You set the disappearing message timer to 1 minute.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -636,7 +679,16 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.timer(120)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("The disappearing message timer has been set to 2 minutes.")));
|
||||
assertThat(describeChange(change), is(singletonList("The disappearing message timer has been set to 2 minutes.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_change_timer_mentions_no_one() {
|
||||
DecryptedGroupChange change = changeByUnknown()
|
||||
.timer(120)
|
||||
.build();
|
||||
|
||||
assertSingleChangeMentioning(change, emptyList());
|
||||
}
|
||||
|
||||
// Attribute access change
|
||||
|
@ -647,7 +699,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.attributeAccess(AccessControl.AccessRequired.MEMBER)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Bob changed who can edit group info to \"All members\".")));
|
||||
assertThat(describeChange(change), is(singletonList("Bob changed who can edit group info to \"All members\".")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -656,7 +708,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.attributeAccess(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You changed who can edit group info to \"Only admins\".")));
|
||||
assertThat(describeChange(change), is(singletonList("You changed who can edit group info to \"Only admins\".")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -665,7 +717,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.attributeAccess(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Who can edit group info has been changed to \"Only admins\".")));
|
||||
assertThat(describeChange(change), is(singletonList("Who can edit group info has been changed to \"Only admins\".")));
|
||||
}
|
||||
|
||||
// Membership access change
|
||||
|
@ -676,7 +728,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.membershipAccess(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Alice changed who can edit group membership to \"Only admins\".")));
|
||||
assertThat(describeChange(change), is(singletonList("Alice changed who can edit group membership to \"Only admins\".")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -685,7 +737,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.membershipAccess(AccessControl.AccessRequired.MEMBER)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("You changed who can edit group membership to \"All members\".")));
|
||||
assertThat(describeChange(change), is(singletonList("You changed who can edit group membership to \"All members\".")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -694,7 +746,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.membershipAccess(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(singletonList("Who can edit group membership has been changed to \"Only admins\".")));
|
||||
assertThat(describeChange(change), is(singletonList("Who can edit group membership has been changed to \"Only admins\".")));
|
||||
}
|
||||
|
||||
// Multiple changes
|
||||
|
@ -708,7 +760,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.timer(300)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(Arrays.asList(
|
||||
assertThat(describeChange(change), is(Arrays.asList(
|
||||
"Alice added Bob.",
|
||||
"Alice changed the group name to \"Title\".",
|
||||
"Alice set the disappearing message timer to 5 minutes.",
|
||||
|
@ -725,7 +777,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.timer(600)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeChange(change), is(Arrays.asList(
|
||||
assertThat(describeChange(change), is(Arrays.asList(
|
||||
"Bob joined the group.",
|
||||
"The group name has changed to \"Title 2\".",
|
||||
"The group avatar has been changed.",
|
||||
|
@ -740,7 +792,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
DecryptedGroup group = newGroupBy(you, 0)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeNewGroup(group), is("You created the group."));
|
||||
assertThat(describeNewGroup(group), is("You created the group."));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -749,7 +801,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.member(you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeNewGroup(group), is("Alice added you to the group."));
|
||||
assertThat(describeNewGroup(group), is("Alice added you to the group."));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -758,7 +810,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.member(you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeNewGroup(group), is("You joined the group."));
|
||||
assertThat(describeNewGroup(group), is("You joined the group."));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -767,7 +819,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.invite(bob, you)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeNewGroup(group), is("Bob invited you to the group."));
|
||||
assertThat(describeNewGroup(group), is("Bob invited you to the group."));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -775,13 +827,40 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
DecryptedGroup group = newGroupBy(alice, 1)
|
||||
.build();
|
||||
|
||||
assertThat(producer.describeNewGroup(group), is("Group updated."));
|
||||
assertThat(describeNewGroup(group), is("Group updated."));
|
||||
}
|
||||
|
||||
private @NonNull List<String> describeChange(@NonNull DecryptedGroupChange change) {
|
||||
MainThreadUtil.setMainThread(false);
|
||||
return Stream.of(producer.describeChanges(change))
|
||||
.map(UpdateDescription::getString)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private GroupStateBuilder newGroupBy(UUID foundingMember, int revision) {
|
||||
private @NonNull String describeNewGroup(@NonNull DecryptedGroup group) {
|
||||
MainThreadUtil.setMainThread(false);
|
||||
return producer.describeNewGroup(group).getString();
|
||||
}
|
||||
|
||||
private static GroupStateBuilder newGroupBy(UUID foundingMember, int revision) {
|
||||
return new GroupStateBuilder(foundingMember, revision);
|
||||
}
|
||||
|
||||
private void assertSingleChangeMentioning(DecryptedGroupChange change, List<UUID> expectedMentions) {
|
||||
List<UpdateDescription> changes = producer.describeChanges(change);
|
||||
|
||||
assertThat(changes.size(), is(1));
|
||||
|
||||
UpdateDescription description = changes.get(0);
|
||||
assertThat(description.getMentioned(), is(expectedMentions));
|
||||
|
||||
if (expectedMentions.isEmpty()) {
|
||||
assertTrue(description.isStringStatic());
|
||||
} else {
|
||||
assertFalse(description.isStringStatic());
|
||||
}
|
||||
}
|
||||
|
||||
private static class GroupStateBuilder {
|
||||
|
||||
private final DecryptedGroup.Builder builder;
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.modules.junit4.PowerMockRunner;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.thoughtcrime.securesms.testutil.MainThreadUtil.setMainThread;
|
||||
|
||||
@RunWith(PowerMockRunner.class)
|
||||
@PrepareForTest(Util.class)
|
||||
public final class UpdateDescriptionTest {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
setMainThread(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void staticDescription_byGetStaticString() {
|
||||
UpdateDescription description = UpdateDescription.staticDescription("update");
|
||||
|
||||
assertEquals("update", description.getStaticString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void staticDescription_has_empty_mentions() {
|
||||
UpdateDescription description = UpdateDescription.staticDescription("update");
|
||||
|
||||
assertTrue(description.getMentioned().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void staticDescription_byString() {
|
||||
UpdateDescription description = UpdateDescription.staticDescription("update");
|
||||
|
||||
assertEquals("update", description.getString());
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void stringFactory_cannot_run_on_main_thread() {
|
||||
UpdateDescription description = UpdateDescription.mentioning(Collections.singletonList(UUID.randomUUID()), () -> "update");
|
||||
|
||||
setMainThread(true);
|
||||
|
||||
description.getString();
|
||||
}
|
||||
|
||||
@Test(expected = UnsupportedOperationException.class)
|
||||
public void stringFactory_cannot_call_static_string() {
|
||||
UpdateDescription description = UpdateDescription.mentioning(Collections.singletonList(UUID.randomUUID()), () -> "update");
|
||||
|
||||
description.getStaticString();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stringFactory_not_evaluated_until_getString() {
|
||||
AtomicInteger factoryCalls = new AtomicInteger();
|
||||
|
||||
UpdateDescription.StringFactory stringFactory = () -> {
|
||||
factoryCalls.incrementAndGet();
|
||||
return "update";
|
||||
};
|
||||
|
||||
UpdateDescription description = UpdateDescription.mentioning(Collections.singletonList(UUID.randomUUID()), stringFactory);
|
||||
|
||||
assertEquals(0, factoryCalls.get());
|
||||
|
||||
setMainThread(false);
|
||||
|
||||
String string = description.getString();
|
||||
|
||||
assertEquals("update", string);
|
||||
assertEquals(1, factoryCalls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stringFactory_reevaluated_on_every_call() {
|
||||
AtomicInteger factoryCalls = new AtomicInteger();
|
||||
UpdateDescription.StringFactory stringFactory = () -> "call" + factoryCalls.incrementAndGet();
|
||||
UpdateDescription description = UpdateDescription.mentioning(Collections.singletonList(UUID.randomUUID()), stringFactory);
|
||||
|
||||
setMainThread(false);
|
||||
|
||||
assertEquals("call1", description.getString());
|
||||
assertEquals("call2", description.getString());
|
||||
assertEquals("call3", description.getString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concat_static_lines() {
|
||||
UpdateDescription description1 = UpdateDescription.staticDescription("update1");
|
||||
UpdateDescription description2 = UpdateDescription.staticDescription("update2");
|
||||
|
||||
UpdateDescription description = UpdateDescription.concatWithNewLines(Arrays.asList(description1, description2));
|
||||
|
||||
assertTrue(description.isStringStatic());
|
||||
assertEquals("update1\nupdate2", description.getStaticString());
|
||||
assertEquals("update1\nupdate2", description.getString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concat_single_does_not_make_new_object() {
|
||||
UpdateDescription description = UpdateDescription.staticDescription("update1");
|
||||
|
||||
UpdateDescription concat = UpdateDescription.concatWithNewLines(Collections.singletonList(description));
|
||||
|
||||
assertSame(description, concat);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concat_dynamic_lines() {
|
||||
AtomicInteger factoryCalls1 = new AtomicInteger();
|
||||
AtomicInteger factoryCalls2 = new AtomicInteger();
|
||||
UpdateDescription.StringFactory stringFactory1 = () -> "update." + factoryCalls1.incrementAndGet();
|
||||
UpdateDescription.StringFactory stringFactory2 = () -> "update." + factoryCalls2.incrementAndGet();
|
||||
UpdateDescription description1 = UpdateDescription.mentioning(Collections.singletonList(UUID.randomUUID()), stringFactory1);
|
||||
UpdateDescription description2 = UpdateDescription.mentioning(Collections.singletonList(UUID.randomUUID()), stringFactory2);
|
||||
|
||||
factoryCalls1.set(10);
|
||||
factoryCalls2.set(20);
|
||||
|
||||
UpdateDescription description = UpdateDescription.concatWithNewLines(Arrays.asList(description1, description2));
|
||||
|
||||
assertFalse(description.isStringStatic());
|
||||
|
||||
setMainThread(false);
|
||||
|
||||
assertEquals("update.11\nupdate.21", description.getString());
|
||||
assertEquals("update.12\nupdate.22", description.getString());
|
||||
assertEquals("update.13\nupdate.23", description.getString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concat_dynamic_lines_and_static_lines() {
|
||||
AtomicInteger factoryCalls1 = new AtomicInteger();
|
||||
AtomicInteger factoryCalls2 = new AtomicInteger();
|
||||
UpdateDescription.StringFactory stringFactory1 = () -> "update." + factoryCalls1.incrementAndGet();
|
||||
UpdateDescription.StringFactory stringFactory2 = () -> "update." + factoryCalls2.incrementAndGet();
|
||||
UpdateDescription description1 = UpdateDescription.mentioning(Collections.singletonList(UUID.randomUUID()), stringFactory1);
|
||||
UpdateDescription description2 = UpdateDescription.staticDescription("static");
|
||||
UpdateDescription description3 = UpdateDescription.mentioning(Collections.singletonList(UUID.randomUUID()), stringFactory2);
|
||||
|
||||
factoryCalls1.set(100);
|
||||
factoryCalls2.set(200);
|
||||
|
||||
UpdateDescription description = UpdateDescription.concatWithNewLines(Arrays.asList(description1, description2, description3));
|
||||
|
||||
assertFalse(description.isStringStatic());
|
||||
|
||||
setMainThread(false);
|
||||
|
||||
assertEquals("update.101\nstatic\nupdate.201", description.getString());
|
||||
assertEquals("update.102\nstatic\nupdate.202", description.getString());
|
||||
assertEquals("update.103\nstatic\nupdate.203", description.getString());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package org.thoughtcrime.securesms.testutil;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.powermock.api.mockito.PowerMockito.doCallRealMethod;
|
||||
import static org.powermock.api.mockito.PowerMockito.mockStatic;
|
||||
|
||||
public final class MainThreadUtil {
|
||||
|
||||
private MainThreadUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes {@link Util}'s Main thread assertions pass or fail during tests.
|
||||
* <p>
|
||||
* Use with {@link org.powermock.modules.junit4.PowerMockRunner} or robolectric with powermock
|
||||
* rule and {@code @PrepareForTest(Util.class)}
|
||||
*/
|
||||
public static void setMainThread(boolean isMainThread) {
|
||||
mockStatic(Util.class);
|
||||
when(Util.isMainThread()).thenReturn(isMainThread);
|
||||
try {
|
||||
doCallRealMethod().when(Util.class, "assertMainThread");
|
||||
doCallRealMethod().when(Util.class, "assertNotMainThread");
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ public final class LiveDataTestUtil {
|
|||
* <p>
|
||||
* This will therefore only work in conjunction with {@link LiveDataRule}.
|
||||
*/
|
||||
public static <T> T getValue(final LiveData<T> liveData) {
|
||||
public static <T> T observeAndGetOneValue(final LiveData<T> liveData) {
|
||||
AtomicReference<T> data = new AtomicReference<>();
|
||||
Observer<T> observer = data::set;
|
||||
|
||||
|
|
|
@ -10,9 +10,9 @@ import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
|||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.thoughtcrime.securesms.util.livedata.LiveDataTestUtil.assertNoValue;
|
||||
import static org.thoughtcrime.securesms.util.livedata.LiveDataTestUtil.getValue;
|
||||
import static org.thoughtcrime.securesms.util.livedata.LiveDataTestUtil.observeAndGetOneValue;
|
||||
|
||||
public final class LiveDataUtilTest {
|
||||
public final class LiveDataUtilTest_combineLatest {
|
||||
|
||||
@Rule
|
||||
public TestRule rule = new LiveDataRule();
|
||||
|
@ -61,7 +61,7 @@ public final class LiveDataUtilTest {
|
|||
liveDataA.setValue("Hello, ");
|
||||
liveDataB.setValue("World!");
|
||||
|
||||
assertEquals("Hello, World!", getValue(combined));
|
||||
assertEquals("Hello, World!", observeAndGetOneValue(combined));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -74,10 +74,10 @@ public final class LiveDataUtilTest {
|
|||
liveDataA.setValue("Hello, ");
|
||||
liveDataB.setValue("World!");
|
||||
|
||||
assertEquals("Hello, World!", getValue(combined));
|
||||
assertEquals("Hello, World!", observeAndGetOneValue(combined));
|
||||
|
||||
liveDataA.setValue("Welcome, ");
|
||||
assertEquals("Welcome, World!", getValue(combined));
|
||||
assertEquals("Welcome, World!", observeAndGetOneValue(combined));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -90,10 +90,10 @@ public final class LiveDataUtilTest {
|
|||
liveDataA.setValue("Hello, ");
|
||||
liveDataB.setValue("World!");
|
||||
|
||||
assertEquals("Hello, World!", getValue(combined));
|
||||
assertEquals("Hello, World!", observeAndGetOneValue(combined));
|
||||
|
||||
liveDataB.setValue("Joe!");
|
||||
assertEquals("Hello, Joe!", getValue(combined));
|
||||
assertEquals("Hello, Joe!", observeAndGetOneValue(combined));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -104,7 +104,7 @@ public final class LiveDataUtilTest {
|
|||
|
||||
liveDataA.setValue("Echo! ");
|
||||
|
||||
assertEquals("Echo! Echo! ", getValue(combined));
|
||||
assertEquals("Echo! Echo! ", observeAndGetOneValue(combined));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -118,7 +118,7 @@ public final class LiveDataUtilTest {
|
|||
|
||||
liveDataB.setValue("World!");
|
||||
|
||||
assertEquals("Hello, World!", getValue(combined));
|
||||
assertEquals("Hello, World!", observeAndGetOneValue(combined));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -128,6 +128,6 @@ public final class LiveDataUtilTest {
|
|||
|
||||
LiveData<Integer> combined = LiveDataUtil.combineLatest(liveDataA, liveDataB, (a, b) -> a * b);
|
||||
|
||||
assertEquals(Integer.valueOf(300), getValue(combined));
|
||||
assertEquals(Integer.valueOf(300), observeAndGetOneValue(combined));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
package org.thoughtcrime.securesms.util.livedata;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TestRule;
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.thoughtcrime.securesms.util.livedata.LiveDataTestUtil.assertNoValue;
|
||||
import static org.thoughtcrime.securesms.util.livedata.LiveDataTestUtil.observeAndGetOneValue;
|
||||
|
||||
public final class LiveDataUtilTest_merge {
|
||||
|
||||
@Rule
|
||||
public TestRule rule = new LiveDataRule();
|
||||
|
||||
@Test
|
||||
public void merge_nothing() {
|
||||
LiveData<String> combined = LiveDataUtil.merge(Collections.emptyList());
|
||||
|
||||
assertNoValue(combined);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void merge_one_is_a_no_op() {
|
||||
MutableLiveData<String> liveDataA = new MutableLiveData<>();
|
||||
|
||||
LiveData<String> combined = LiveDataUtil.merge(Collections.singletonList(liveDataA));
|
||||
|
||||
assertSame(liveDataA, combined);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initially_no_value() {
|
||||
MutableLiveData<String> liveDataA = new MutableLiveData<>();
|
||||
MutableLiveData<String> liveDataB = new MutableLiveData<>();
|
||||
|
||||
LiveData<String> combined = LiveDataUtil.merge(Arrays.asList(liveDataA, liveDataB));
|
||||
|
||||
assertNoValue(combined);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void value_on_first() {
|
||||
MutableLiveData<String> liveDataA = new MutableLiveData<>();
|
||||
MutableLiveData<String> liveDataB = new MutableLiveData<>();
|
||||
|
||||
LiveData<String> combined = LiveDataUtil.merge(Arrays.asList(liveDataA, liveDataB));
|
||||
|
||||
liveDataA.setValue("A");
|
||||
|
||||
assertEquals("A", observeAndGetOneValue(combined));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void value_on_second() {
|
||||
MutableLiveData<String> liveDataA = new MutableLiveData<>();
|
||||
MutableLiveData<String> liveDataB = new MutableLiveData<>();
|
||||
|
||||
LiveData<String> combined = LiveDataUtil.merge(Arrays.asList(liveDataA, liveDataB));
|
||||
|
||||
liveDataB.setValue("B");
|
||||
|
||||
assertEquals("B", observeAndGetOneValue(combined));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void value_on_third() {
|
||||
MutableLiveData<String> liveDataA = new MutableLiveData<>();
|
||||
MutableLiveData<String> liveDataB = new MutableLiveData<>();
|
||||
MutableLiveData<String> liveDataC = new MutableLiveData<>();
|
||||
|
||||
LiveData<String> combined = LiveDataUtil.merge(Arrays.asList(liveDataA, liveDataB, liveDataC));
|
||||
|
||||
liveDataC.setValue("C");
|
||||
|
||||
assertEquals("C", observeAndGetOneValue(combined));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void several_values_merged() {
|
||||
MutableLiveData<String> liveDataA = new MutableLiveData<>();
|
||||
MutableLiveData<String> liveDataB = new MutableLiveData<>();
|
||||
MutableLiveData<String> liveDataC = new MutableLiveData<>();
|
||||
|
||||
LiveData<String> combined = LiveDataUtil.merge(Arrays.asList(liveDataA, liveDataB, liveDataC));
|
||||
|
||||
liveDataC.setValue("C");
|
||||
|
||||
assertEquals("C", observeAndGetOneValue(combined));
|
||||
|
||||
liveDataA.setValue("A");
|
||||
|
||||
assertEquals("A", observeAndGetOneValue(combined));
|
||||
|
||||
liveDataB.setValue("B");
|
||||
|
||||
assertEquals("B", observeAndGetOneValue(combined));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void combined_same_instance() {
|
||||
MutableLiveData<String> liveDataA = new MutableLiveData<>();
|
||||
|
||||
LiveData<String> combined = LiveDataUtil.merge(Arrays.asList(liveDataA, liveDataA));
|
||||
|
||||
liveDataA.setValue("Echo! ");
|
||||
|
||||
assertSame(liveDataA, combined);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void combined_same_instances_repeated() {
|
||||
MutableLiveData<String> liveDataA = new MutableLiveData<>();
|
||||
MutableLiveData<String> liveDataB = new MutableLiveData<>();
|
||||
MutableLiveData<String> liveDataC = new MutableLiveData<>();
|
||||
|
||||
LiveData<String> combined = LiveDataUtil.merge(Arrays.asList(liveDataA, liveDataB, liveDataC, liveDataA, liveDataB, liveDataC));
|
||||
|
||||
liveDataC.setValue("C");
|
||||
|
||||
assertEquals("C", observeAndGetOneValue(combined));
|
||||
|
||||
liveDataA.setValue("A");
|
||||
|
||||
assertEquals("A", observeAndGetOneValue(combined));
|
||||
|
||||
liveDataB.setValue("B");
|
||||
|
||||
assertEquals("B", observeAndGetOneValue(combined));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void on_a_set_before_combine() {
|
||||
MutableLiveData<String> liveDataA = new MutableLiveData<>();
|
||||
MutableLiveData<String> liveDataB = new MutableLiveData<>();
|
||||
|
||||
liveDataA.setValue("A");
|
||||
|
||||
LiveData<String> combined = LiveDataUtil.merge(Arrays.asList(liveDataA, liveDataB));
|
||||
|
||||
assertEquals("A", observeAndGetOneValue(combined));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void on_default_values() {
|
||||
MutableLiveData<Integer> liveDataA = new DefaultValueLiveData<>(10);
|
||||
MutableLiveData<Integer> liveDataB = new DefaultValueLiveData<>(30);
|
||||
|
||||
LiveData<Integer> combined = LiveDataUtil.merge(Arrays.asList(liveDataA, liveDataB));
|
||||
|
||||
assertEquals(Integer.valueOf(30), observeAndGetOneValue(combined));
|
||||
}
|
||||
}
|
|
@ -63,6 +63,11 @@ public final class UuidUtil {
|
|||
return parseOrNull(bytes.toByteArray());
|
||||
}
|
||||
|
||||
public static UUID fromByteStringOrUnknown(ByteString bytes) {
|
||||
UUID uuid = fromByteStringOrNull(bytes);
|
||||
return uuid != null ? uuid : UNKNOWN_UUID;
|
||||
}
|
||||
|
||||
private static UUID parseOrNull(byte[] byteArray) {
|
||||
return byteArray != null && byteArray.length == 16 ? parseOrThrow(byteArray) : null;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue