diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java index 14186fc1e..e478eab91 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -55,15 +55,15 @@ public final class ConversationUpdateItem extends FrameLayout private Set batchSelected; - private TextView body; - private MaterialButton actionButton; - private View background; - private ConversationMessage conversationMessage; - private Recipient conversationRecipient; - private Optional nextMessageRecord; - private MessageRecord messageRecord; - private LiveData displayBody; - private EventListener eventListener; + private TextView body; + private MaterialButton actionButton; + private View background; + private ConversationMessage conversationMessage; + private Recipient conversationRecipient; + private Optional nextMessageRecord; + private MessageRecord messageRecord; + private LiveData 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 liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(getContext(), updateDescription, textColor); - LiveData spannableMessage = loading(liveUpdateMessage); + UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext())); + LiveData liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(getContext(), updateDescription, textColor); + LiveData 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 loading(@NonNull LiveData string) { + private @NonNull LiveData loading(@NonNull LiveData 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 displayBody) { + private void observeDisplayBody(@NonNull LifecycleOwner lifecycleOwner, @Nullable LiveData displayBody) { if (this.displayBody != displayBody) { if (this.displayBody != null) { this.displayBody.removeObserver(updateObserver); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java index cdb5733e6..316714854 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java @@ -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 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 whileLoadingShow(@NonNull String loading, @NonNull LiveData 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 emphasisAdded(@NonNull LiveData description) { + private static @NonNull LiveData emphasisAdded(@NonNull LiveData description) { return Transformations.map(description, sequence -> { SpannableString spannable = new SpannableString(sequence); spannable.setSpan(new StyleSpan(Typeface.ITALIC), diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 0fd803be8..406af7d2c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/LiveUpdateMessage.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/LiveUpdateMessage.java index 829be181a..f7c8cbe21 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/LiveUpdateMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/LiveUpdateMessage.java @@ -31,7 +31,7 @@ public final class LiveUpdateMessage { * recreates the string asynchronously when they change. */ @AnyThread - public static LiveData fromMessageDescription(@NonNull Context context, @NonNull UpdateDescription updateDescription, @ColorInt int defaultTint) { + public static LiveData 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 recipientToStringAsync(@NonNull RecipientId recipientId, - @NonNull Function createStringInBackground) + public static LiveData recipientToStringAsync(@NonNull RecipientId recipientId, + @NonNull Function 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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java index b9db4b392..619ea6c54 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java index e406b0b92..e015377d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java @@ -34,6 +34,7 @@ public final class LiveRecipient { private final Context context; private final MutableLiveData liveData; private final LiveData observableLiveData; + private final LiveData observableLiveDataResolved; private final Set observers; private final Observer foreverObserver; private final AtomicReference 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 getLiveDataResolved() { + return observableLiveDataResolved; + } + private @NonNull Recipient fetchAndCacheRecipientFromDisk(@NonNull RecipientId id) { RecipientSettings settings = recipientDatabase.getRecipientSettings(id); RecipientDetails details = settings.getGroupId() != null ? getGroupRecipientDetails(settings) diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientViewModel.java index ca501b6ef..b1348dd3f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientViewModel.java @@ -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, diff --git a/app/src/main/res/values/text_styles.xml b/app/src/main/res/values/text_styles.xml index ceecf87f2..045b082d4 100644 --- a/app/src/main/res/values/text_styles.xml +++ b/app/src/main/res/values/text_styles.xml @@ -22,6 +22,10 @@ 0.01 + +