Show name of message sender for groups in conversation list.
This commit is contained in:
parent
b5237848e9
commit
23303e5407
8 changed files with 147 additions and 49 deletions
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue