Show name of message sender for groups in conversation list.

This commit is contained in:
Alan Evans 2021-01-27 11:53:31 -04:00 committed by GitHub
parent b5237848e9
commit 23303e5407
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 147 additions and 49 deletions

View file

@ -55,15 +55,15 @@ public final class ConversationUpdateItem extends FrameLayout
private Set<ConversationMessage> batchSelected;
private TextView body;
private MaterialButton actionButton;
private View background;
private ConversationMessage conversationMessage;
private Recipient conversationRecipient;
private Optional<MessageRecord> nextMessageRecord;
private MessageRecord messageRecord;
private LiveData<Spannable> displayBody;
private EventListener eventListener;
private TextView body;
private MaterialButton actionButton;
private View background;
private ConversationMessage conversationMessage;
private Recipient conversationRecipient;
private Optional<MessageRecord> nextMessageRecord;
private MessageRecord messageRecord;
private LiveData<SpannableString> displayBody;
private EventListener eventListener;
private final UpdateObserver updateObserver = new UpdateObserver();
@ -150,9 +150,9 @@ public final class ConversationUpdateItem extends FrameLayout
}
}
UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext()));
LiveData<Spannable> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(getContext(), updateDescription, textColor);
LiveData<Spannable> spannableMessage = loading(liveUpdateMessage);
UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext()));
LiveData<SpannableString> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(getContext(), updateDescription, textColor);
LiveData<SpannableString> spannableMessage = loading(liveUpdateMessage);
observeDisplayBody(lifecycleOwner, spannableMessage);
@ -172,7 +172,7 @@ public final class ConversationUpdateItem extends FrameLayout
}
/** After a short delay, if the main data hasn't shown yet, then a loading message is displayed. */
private @NonNull LiveData<Spannable> loading(@NonNull LiveData<Spannable> string) {
private @NonNull LiveData<SpannableString> loading(@NonNull LiveData<SpannableString> string) {
return LiveDataUtil.until(string, LiveDataUtil.delay(250, new SpannableString(getContext().getString(R.string.ConversationUpdateItem_loading))));
}
@ -208,7 +208,7 @@ public final class ConversationUpdateItem extends FrameLayout
}
}
private void observeDisplayBody(@NonNull LifecycleOwner lifecycleOwner, @Nullable LiveData<Spannable> displayBody) {
private void observeDisplayBody(@NonNull LifecycleOwner lifecycleOwner, @Nullable LiveData<SpannableString> displayBody) {
if (this.displayBody != displayBody) {
if (this.displayBody != null) {
this.displayBody.removeObserver(updateObserver);

View file

@ -24,6 +24,7 @@ import android.os.Build;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.StyleSpan;
import android.text.style.TextAppearanceSpan;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
@ -59,6 +60,7 @@ 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.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.ExpirationUtil;
@ -487,11 +489,47 @@ public final class ConversationListItem extends ConstraintLayout
} else if (extra != null && extra.isRemoteDelete()) {
return emphasisAdded(context, context.getString(thread.isOutgoing() ? R.string.ThreadRecord_you_deleted_this_message : R.string.ThreadRecord_this_message_was_deleted), defaultTint);
} else {
return LiveDataUtil.just(new SpannableString(removeNewlines(thread.getBody())));
String body = removeNewlines(thread.getBody());
if (thread.getRecipient().isGroup()) {
RecipientId groupMessageSender = thread.getGroupMessageSender();
if (!groupMessageSender.isUnknown()) {
return describeGroupMessage(context, body, groupMessageSender);
}
}
return LiveDataUtil.just(new SpannableString(body));
}
}
}
private static LiveData<SpannableString> describeGroupMessage(@NonNull Context context,
@NonNull String body,
@NonNull RecipientId groupMessageSender)
{
return whileLoadingShow(body, recipientToStringAsync(groupMessageSender,
r -> createGroupMessageUpdateString(context, body, r)));
}
private static SpannableString createGroupMessageUpdateString(@NonNull Context context,
@NonNull String body,
@NonNull Recipient recipient)
{
String sender = (recipient.isSelf() ? context.getString(R.string.MessageRecord_you)
: recipient.getShortDisplayName(context)) + ": ";
SpannableString spannable = new SpannableString(sender + body);
spannable.setSpan(new TextAppearanceSpan(context, R.style.Signal_Text_Preview_Medium),
0,
sender.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return spannable;
}
/** After a short delay, if the main data hasn't shown yet, then a loading message is displayed. */
private static @NonNull LiveData<SpannableString> whileLoadingShow(@NonNull String loading, @NonNull LiveData<SpannableString> string) {
return LiveDataUtil.until(string, LiveDataUtil.delay(250, new SpannableString(loading)));
}
private static @NonNull String removeNewlines(@Nullable String text) {
if (text == null) {
return "";
@ -512,7 +550,7 @@ public final class ConversationListItem extends ConstraintLayout
return emphasisAdded(LiveUpdateMessage.fromMessageDescription(context, description, defaultTint));
}
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull LiveData<Spannable> description) {
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull LiveData<SpannableString> description) {
return Transformations.map(description, sequence -> {
SpannableString spannable = new SpannableString(sequence);
spannable.setSpan(new StyleSpan(Typeface.ITALIC),

View file

@ -1381,6 +1381,7 @@ public class ThreadDatabase extends Database {
private @Nullable Extra getExtrasFor(MessageRecord record) {
boolean messageRequestAccepted = RecipientUtil.isMessageRequestAccepted(context, record.getThreadId());
RecipientId threadRecipientId = getRecipientIdForThreadId(record.getThreadId());
RecipientId individualRecipient = record.getIndividualRecipient().getId();
if (!messageRequestAccepted && threadRecipientId != null) {
Recipient resolved = Recipient.resolved(threadRecipientId);
@ -1391,35 +1392,42 @@ public class ThreadDatabase extends Database {
RecipientId from = RecipientId.from(inviteAddState.getAddedOrInvitedBy(), null);
if (inviteAddState.isInvited()) {
Log.i(TAG, "GV2 invite message request from " + from);
return Extra.forGroupV2invite(from);
return Extra.forGroupV2invite(from, individualRecipient);
} else {
Log.i(TAG, "GV2 message request from " + from);
return Extra.forGroupMessageRequest(from);
return Extra.forGroupMessageRequest(from, individualRecipient);
}
}
Log.w(TAG, "Falling back to unknown message request state for GV2 message");
return Extra.forMessageRequest();
return Extra.forMessageRequest(individualRecipient);
} else {
RecipientId recipientId = DatabaseFactory.getMmsSmsDatabase(context).getGroupAddedBy(record.getThreadId());
if (recipientId != null) {
return Extra.forGroupMessageRequest(recipientId);
return Extra.forGroupMessageRequest(recipientId, individualRecipient);
}
}
}
return Extra.forMessageRequest();
return Extra.forMessageRequest(individualRecipient);
}
if (record.isRemoteDelete()) {
return Extra.forRemoteDelete();
return Extra.forRemoteDelete(individualRecipient);
} else if (record.isViewOnce()) {
return Extra.forViewOnce();
return Extra.forViewOnce(individualRecipient);
} else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getStickerSlide() != null) {
StickerSlide slide = Objects.requireNonNull(((MmsMessageRecord) record).getSlideDeck().getStickerSlide());
return Extra.forSticker(slide.getEmoji());
return Extra.forSticker(slide.getEmoji(), individualRecipient);
} else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getSlides().size() > 1) {
return Extra.forAlbum();
return Extra.forAlbum(individualRecipient);
}
if (threadRecipientId != null) {
Recipient resolved = Recipient.resolved(threadRecipientId);
if (resolved.isGroup()) {
return Extra.forDefault(individualRecipient);
}
}
return null;
@ -1590,6 +1598,7 @@ public class ThreadDatabase extends Database {
@JsonProperty private final boolean isMessageRequestAccepted;
@JsonProperty private final boolean isGv2Invite;
@JsonProperty private final String groupAddedBy;
@JsonProperty private final String individualRecipientId;
public Extra(@JsonProperty("isRevealable") boolean isRevealable,
@JsonProperty("isSticker") boolean isSticker,
@ -1598,7 +1607,8 @@ public class ThreadDatabase extends Database {
@JsonProperty("isRemoteDelete") boolean isRemoteDelete,
@JsonProperty("isMessageRequestAccepted") boolean isMessageRequestAccepted,
@JsonProperty("isGv2Invite") boolean isGv2Invite,
@JsonProperty("groupAddedBy") String groupAddedBy)
@JsonProperty("groupAddedBy") String groupAddedBy,
@JsonProperty("individualRecipientId") String individualRecipientId)
{
this.isRevealable = isRevealable;
this.isSticker = isSticker;
@ -1608,34 +1618,39 @@ public class ThreadDatabase extends Database {
this.isMessageRequestAccepted = isMessageRequestAccepted;
this.isGv2Invite = isGv2Invite;
this.groupAddedBy = groupAddedBy;
this.individualRecipientId = individualRecipientId;
}
public static @NonNull Extra forViewOnce() {
return new Extra(true, false, null, false, false, true, false, null);
public static @NonNull Extra forViewOnce(@NonNull RecipientId individualRecipient) {
return new Extra(true, false, null, false, false, true, false, null, individualRecipient.serialize());
}
public static @NonNull Extra forSticker(@Nullable String emoji) {
return new Extra(false, true, emoji, false, false, true, false, null);
public static @NonNull Extra forSticker(@Nullable String emoji, @NonNull RecipientId individualRecipient) {
return new Extra(false, true, emoji, false, false, true, false, null, individualRecipient.serialize());
}
public static @NonNull Extra forAlbum() {
return new Extra(false, false, null, true, false, true, false, null);
public static @NonNull Extra forAlbum(@NonNull RecipientId individualRecipient) {
return new Extra(false, false, null, true, false, true, false, null, individualRecipient.serialize());
}
public static @NonNull Extra forRemoteDelete() {
return new Extra(false, false, null, false, true, true, false, null);
public static @NonNull Extra forRemoteDelete(@NonNull RecipientId individualRecipient) {
return new Extra(false, false, null, false, true, true, false, null, individualRecipient.serialize());
}
public static @NonNull Extra forMessageRequest() {
return new Extra(false, false, null, false, false, false, false, null);
public static @NonNull Extra forMessageRequest(@NonNull RecipientId individualRecipient) {
return new Extra(false, false, null, false, false, false, false, null, individualRecipient.serialize());
}
public static @NonNull Extra forGroupMessageRequest(RecipientId recipientId) {
return new Extra(false, false, null, false, false, false, false, recipientId.serialize());
public static @NonNull Extra forGroupMessageRequest(@NonNull RecipientId recipientId, @NonNull RecipientId individualRecipient) {
return new Extra(false, false, null, false, false, false, false, recipientId.serialize(), individualRecipient.serialize());
}
public static @NonNull Extra forGroupV2invite(RecipientId recipientId) {
return new Extra(false, false, null, false, false, false, true, recipientId.serialize());
public static @NonNull Extra forGroupV2invite(@NonNull RecipientId recipientId, @NonNull RecipientId individualRecipient) {
return new Extra(false, false, null, false, false, false, true, recipientId.serialize(), individualRecipient.serialize());
}
public static @NonNull Extra forDefault(@NonNull RecipientId individualRecipient) {
return new Extra(false, false, null, false, false, true, false, null, individualRecipient.serialize());
}
public boolean isViewOnce() {
@ -1669,6 +1684,10 @@ public class ThreadDatabase extends Database {
public @Nullable String getGroupAddedBy() {
return groupAddedBy;
}
public @Nullable String getIndividualRecipientId() {
return individualRecipientId;
}
}
enum ReadStatus {

View file

@ -31,7 +31,7 @@ public final class LiveUpdateMessage {
* recreates the string asynchronously when they change.
*/
@AnyThread
public static LiveData<Spannable> fromMessageDescription(@NonNull Context context, @NonNull UpdateDescription updateDescription, @ColorInt int defaultTint) {
public static LiveData<SpannableString> fromMessageDescription(@NonNull Context context, @NonNull UpdateDescription updateDescription, @ColorInt int defaultTint) {
if (updateDescription.isStringStatic()) {
return LiveDataUtil.just(toSpannable(context, updateDescription, updateDescription.getStaticString(), defaultTint));
}
@ -49,13 +49,13 @@ public final class LiveUpdateMessage {
/**
* Observes a single recipient and recreates the string asynchronously when they change.
*/
public static LiveData<Spannable> recipientToStringAsync(@NonNull RecipientId recipientId,
@NonNull Function<Recipient, Spannable> createStringInBackground)
public static LiveData<SpannableString> recipientToStringAsync(@NonNull RecipientId recipientId,
@NonNull Function<Recipient, SpannableString> createStringInBackground)
{
return LiveDataUtil.mapAsync(Recipient.live(recipientId).getLiveData(), createStringInBackground);
return LiveDataUtil.mapAsync(Recipient.live(recipientId).getLiveDataResolved(), createStringInBackground);
}
private static @NonNull Spannable toSpannable(@NonNull Context context, @NonNull UpdateDescription updateDescription, @NonNull String string, @ColorInt int defaultTint) {
private static @NonNull SpannableString toSpannable(@NonNull Context context, @NonNull UpdateDescription updateDescription, @NonNull String string, @ColorInt int defaultTint) {
boolean isDarkTheme = ThemeUtil.isDarkTheme(context);
int drawableResource = updateDescription.getIconResource();
int tint = isDarkTheme ? updateDescription.getDarkTint() : updateDescription.getLightTint();

View file

@ -40,6 +40,7 @@ public final class ThreadRecord {
private final long threadId;
private final String body;
private final Recipient recipient;
private final Recipient sender;
private final long type;
private final long date;
private final long deliveryStatus;
@ -61,6 +62,7 @@ public final class ThreadRecord {
this.threadId = builder.threadId;
this.body = builder.body;
this.recipient = builder.recipient;
this.sender = builder.sender;
this.date = builder.date;
this.type = builder.type;
this.deliveryStatus = builder.deliveryStatus;
@ -184,6 +186,29 @@ public final class ThreadRecord {
else return null;
}
public @NonNull RecipientId getIndividualRecipientId() {
if (extra != null && extra.getIndividualRecipientId() != null) {
return RecipientId.from(extra.getIndividualRecipientId());
} else {
if (getRecipient().isGroup()) {
return RecipientId.UNKNOWN;
} else {
return getRecipient().getId();
}
}
}
public @NonNull RecipientId getGroupMessageSender() {
RecipientId threadRecipientId = getRecipient().getId();
RecipientId individualRecipientId = getIndividualRecipientId();
if (threadRecipientId.equals(individualRecipientId)) {
return Recipient.self().getId();
} else {
return individualRecipientId;
}
}
public boolean isGv2Invite() {
return extra != null && extra.isGv2Invite();
}
@ -249,7 +274,8 @@ public final class ThreadRecord {
public static class Builder {
private long threadId;
private String body;
private Recipient recipient;
private Recipient recipient = Recipient.UNKNOWN;
private Recipient sender = Recipient.UNKNOWN;
private long type;
private long date;
private long deliveryStatus;
@ -281,6 +307,11 @@ public final class ThreadRecord {
return this;
}
public Builder setSender(@NonNull Recipient sender) {
this.sender = sender;
return this;
}
public Builder setType(long type) {
this.type = type;
return this;

View file

@ -34,6 +34,7 @@ public final class LiveRecipient {
private final Context context;
private final MutableLiveData<Recipient> liveData;
private final LiveData<Recipient> observableLiveData;
private final LiveData<Recipient> observableLiveDataResolved;
private final Set<RecipientForeverObserver> observers;
private final Observer<Recipient> foreverObserver;
private final AtomicReference<Recipient> recipient;
@ -53,10 +54,11 @@ public final class LiveRecipient {
o.onRecipientChanged(recipient);
}
};
this.refreshForceNotify = new MutableLiveData<>(System.currentTimeMillis());
this.refreshForceNotify = new MutableLiveData<>(new Object());
this.observableLiveData = LiveDataUtil.combineLatest(LiveDataUtil.distinctUntilChanged(liveData, Recipient::hasSameContent),
refreshForceNotify,
(recipient, force) -> recipient);
this.observableLiveDataResolved = LiveDataUtil.filter(this.observableLiveData, r -> !r.isResolving());
}
public @NonNull RecipientId getId() {
@ -183,6 +185,10 @@ public final class LiveRecipient {
return observableLiveData;
}
public @NonNull LiveData<Recipient> getLiveDataResolved() {
return observableLiveDataResolved;
}
private @NonNull Recipient fetchAndCacheRecipientFromDisk(@NonNull RecipientId id) {
RecipientSettings settings = recipientDatabase.getRecipientSettings(id);
RecipientDetails details = settings.getGroupId() != null ? getGroupRecipientDetails(settings)

View file

@ -285,13 +285,13 @@ public final class ManageRecipientViewModel extends ViewModel {
String profileKeyBase64 = recipient.getProfileKey() != null ? Base64.encodeBytes(recipient.getProfileKey()) : "None";
String profileKeyHex = recipient.getProfileKey() != null ? Hex.toStringCondensed(recipient.getProfileKey()) : "None";
return String.format("-- Profile Name --\n%s\n\n" +
return String.format("-- Profile Name --\n[%s] [%s]\n\n" +
"-- Profile Sharing --\n%s\n\n" +
"-- Profile Key (Base64) --\n%s\n\n" +
"-- Profile Key (Hex) --\n%s\n\n" +
"-- UUID --\n%s\n\n" +
"-- RecipientId --\n%s",
recipient.getProfileName().toString(),
recipient.getProfileName().getGivenName(), recipient.getProfileName().getFamilyName(),
recipient.isProfileSharing(),
profileKeyBase64,
profileKeyHex,

View file

@ -22,6 +22,10 @@
<item name="android:letterSpacing" tools:ignore="NewApi">0.01</item>
</style>
<style name="Signal.Text.Preview.Medium">
<item name="android:fontFamily">sans-serif-medium</item>
</style>
<style name="Signal.Text.Caption" parent="Base.TextAppearance.AppCompat.Caption">
<item name="android:textSize">12sp</item>
<item name="android:lineSpacingExtra">2sp</item>