diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java index 0ccbb32cd8..1dd367f539 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java @@ -60,6 +60,10 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, // Intentionally Blank. } + default void updateSelectedState() { + // Intentionall Blank. + } + interface EventListener { void onQuoteClicked(MmsMessageRecord messageRecord); void onLinkPreviewClicked(@NonNull LinkPreview linkPreview); @@ -95,6 +99,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, void onCallToAction(@NonNull String action); void onDonateClicked(); void onBlockJoinRequest(@NonNull Recipient recipient); + void onRecipientNameClicked(@NonNull RecipientId target); /** @return true if handled, false if you want to let the normal url handling continue */ boolean onUrlClicked(@NonNull String url); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java index a6ff937984..59b97475a8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java @@ -104,6 +104,7 @@ public class ConversationAdapter private static final int PAYLOAD_TIMESTAMP = 0; public static final int PAYLOAD_NAME_COLORS = 1; + public static final int PAYLOAD_SELECTED = 2; private final ItemClickListener clickListener; private final Context context; @@ -229,7 +230,7 @@ public class ConversationAdapter } private boolean containsValidPayload(@NonNull List payloads) { - return payloads.contains(PAYLOAD_TIMESTAMP) || payloads.contains(PAYLOAD_NAME_COLORS); + return payloads.contains(PAYLOAD_TIMESTAMP) || payloads.contains(PAYLOAD_NAME_COLORS) || payloads.contains(PAYLOAD_SELECTED); } @Override @@ -250,6 +251,10 @@ public class ConversationAdapter conversationViewHolder.getBindable().updateContactNameColor(); } + if (payloads.contains(PAYLOAD_SELECTED)) { + conversationViewHolder.getBindable().updateSelectedState(); + } + default: return; } @@ -522,6 +527,7 @@ public class ConversationAdapter public void removeFromSelection(@NonNull Set parts) { selected.removeAll(parts); + updateSelected(); } /** @@ -529,6 +535,7 @@ public class ConversationAdapter */ void clearSelection() { selected.clear(); + updateSelected(); } /** @@ -540,6 +547,11 @@ public class ConversationAdapter } else { selected.add(multiselectPart); } + updateSelected(); + } + + private void updateSelected() { + notifyItemRangeChanged(0, getItemCount(), PAYLOAD_SELECTED); } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java index 1dc9879ac6..14f1cc58b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java @@ -106,7 +106,7 @@ class ConversationDataSource implements PagedDataSource handleBlockJoinRequest(recipient)) .show(); } + + @Override + public void onRecipientNameClicked(@NonNull RecipientId target) { + if (getParentFragment() == null) return; + + RecipientBottomSheetDialogFragment.create(target, recipient.get().getGroupId().orElse(null)).show(getParentFragmentManager(), "BOTTOM"); + } } public void refreshList() { 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 f4e5fb475c..2c71428870 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -4,6 +4,7 @@ import android.content.Context; import android.content.res.ColorStateList; import android.text.Spannable; import android.text.SpannableString; +import android.text.method.LinkMovementMethod; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; @@ -107,6 +108,9 @@ public final class ConversationUpdateItem extends FrameLayout this.donateButtonStub = ViewUtil.findStubById(this, R.id.conversation_update_donate_action); this.background = findViewById(R.id.conversation_update_background); + body.setOnClickListener(v -> performClick()); + body.setOnLongClickListener(v -> performLongClick()); + this.setOnClickListener(new InternalClickListener(null)); } @@ -179,7 +183,7 @@ public final class ConversationUpdateItem extends FrameLayout } } - UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext())); + UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext(), eventListener::onRecipientNameClicked)); LiveData liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(getContext(), updateDescription, textColor, true); LiveData spannableMessage = loading(liveUpdateMessage); @@ -190,6 +194,17 @@ public final class ConversationUpdateItem extends FrameLayout presentBackground(shouldCollapse(messageRecord, previousMessageRecord), shouldCollapse(messageRecord, nextMessageRecord), hasWallpaper); + + updateSelectedState(); + } + + @Override + public void updateSelectedState() { + if (batchSelected.size() > 0) { + body.setMovementMethod(null); + } else { + body.setMovementMethod(LinkMovementMethod.getInstance()); + } } private static boolean shouldCollapse(@NonNull MessageRecord current, @NonNull Optional candidate) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java index 27f71043ed..9d8df50bb9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java @@ -73,7 +73,7 @@ abstract class ConversationListDataSource implements PagedDataSource RecipientId.from(sid, null)).collect(Collectors.toList())); } } 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 8bd1aeb7f5..d83403cea5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java @@ -466,7 +466,7 @@ public final class ConversationListItem extends ConstraintLayout return emphasisAdded(context, context.getString(R.string.ThreadRecord_message_request), defaultTint); } else if (SmsDatabase.Types.isGroupUpdate(thread.getType())) { if (thread.getRecipient().isPushV2Group()) { - return emphasisAdded(context, MessageRecord.getGv2ChangeDescription(context, thread.getBody()), defaultTint); + return emphasisAdded(context, MessageRecord.getGv2ChangeDescription(context, thread.getBody(), null), defaultTint); } else { return emphasisAdded(context, context.getString(R.string.ThreadRecord_group_updated), R.drawable.ic_update_group_16, defaultTint); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupCallUpdateMessageFactory.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupCallUpdateMessageFactory.java index 812daeb405..3a3afd84ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupCallUpdateMessageFactory.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupCallUpdateMessageFactory.java @@ -1,6 +1,8 @@ package org.thoughtcrime.securesms.database.model; import android.content.Context; +import android.text.Spannable; +import android.text.SpannableString; import androidx.annotation.NonNull; @@ -21,7 +23,7 @@ import java.util.Objects; /** * Create a group call update message based on time and joined members. */ -public class GroupCallUpdateMessageFactory implements UpdateDescription.StringFactory { +public class GroupCallUpdateMessageFactory implements UpdateDescription.SpannableFactory { private final Context context; private final List joinedMembers; private final boolean withTime; @@ -46,7 +48,11 @@ public class GroupCallUpdateMessageFactory implements UpdateDescription.StringFa } @Override - public @NonNull String create() { + public @NonNull Spannable create() { + return new SpannableString(createString()); + } + + private @NonNull String createString() { String time = DateUtils.getTimeString(context, Locale.getDefault(), groupCallUpdateDetails.getStartedCallTimestamp()); switch (joinedMembers.size()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java index da0016e8c1..6156a41fcf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java @@ -1,10 +1,18 @@ package org.thoughtcrime.securesms.database.model; import android.content.Context; +import android.graphics.Color; +import android.text.PrecomputedText; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.widget.Toast; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.PluralsRes; +import androidx.annotation.StringRes; import com.google.protobuf.ByteString; @@ -21,12 +29,16 @@ import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember; import org.signal.storageservice.protos.groups.local.EnabledState; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.groups.GV2AccessLevelUtil; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.ExpirationUtil; import org.signal.core.util.StringUtil; +import org.thoughtcrime.securesms.util.SpanUtil; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.util.UuidUtil; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -35,25 +47,21 @@ import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.function.Consumer; import java.util.stream.Collectors; final class GroupsV2UpdateMessageProducer { @NonNull private final Context context; - @NonNull private final DescribeMemberStrategy descriptionStrategy; @NonNull private final UUID selfUuid; @NonNull private final ByteString selfUuidBytes; + @Nullable private final Consumer recipientClickHandler; - /** - * @param descriptionStrategy Strategy for member description. - */ - GroupsV2UpdateMessageProducer(@NonNull Context context, - @NonNull DescribeMemberStrategy descriptionStrategy, - @NonNull UUID selfUuid) { - this.context = context; - this.descriptionStrategy = descriptionStrategy; - this.selfUuid = selfUuid; - this.selfUuidBytes = UuidUtil.toByteString(selfUuid); + GroupsV2UpdateMessageProducer(@NonNull Context context, @NonNull UUID selfUuid, @Nullable Consumer recipientClickHandler) { + this.context = context; + this.selfUuid = selfUuid; + this.selfUuidBytes = UuidUtil.toByteString(selfUuid); + this.recipientClickHandler = recipientClickHandler; } /** @@ -68,7 +76,7 @@ final class GroupsV2UpdateMessageProducer { UpdateDescription describeNewGroup(@NonNull DecryptedGroup group, @NonNull DecryptedGroupChange decryptedGroupChange) { Optional selfPending = DecryptedGroupUtil.findPendingByUuid(group.getPendingMembersList(), selfUuid); if (selfPending.isPresent()) { - return updateDescription(selfPending.get().getAddedByUuid(), inviteBy -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, inviteBy), R.drawable.ic_update_group_add_16); + return updateDescription(R.string.MessageRecord_s_invited_you_to_the_group, selfPending.get().getAddedByUuid(), R.drawable.ic_update_group_add_16); } ByteString foundingMemberUuid = decryptedGroupChange.getEditor(); @@ -76,7 +84,7 @@ final class GroupsV2UpdateMessageProducer { if (selfUuidBytes.equals(foundingMemberUuid)) { return updateDescription(context.getString(R.string.MessageRecord_you_created_the_group), R.drawable.ic_update_group_16); } else { - return updateDescription(foundingMemberUuid, creator -> context.getString(R.string.MessageRecord_s_added_you, creator), R.drawable.ic_update_group_add_16); + return updateDescription(R.string.MessageRecord_s_added_you, foundingMemberUuid, R.drawable.ic_update_group_add_16); } } @@ -157,7 +165,7 @@ final class GroupsV2UpdateMessageProducer { if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_updated_group), R.drawable.ic_update_group_16)); } else { - updates.add(updateDescription(change.getEditor(), (editor) -> context.getString(R.string.MessageRecord_s_updated_group, editor), R.drawable.ic_update_group_16)); + updates.add(updateDescription(R.string.MessageRecord_s_updated_group, change.getEditor(), R.drawable.ic_update_group_16)); } } @@ -175,16 +183,16 @@ final class GroupsV2UpdateMessageProducer { if (newMemberIsYou) { updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group_via_the_group_link), R.drawable.ic_update_group_accept_16)); } else { - updates.add(updateDescription(member.getUuid(), added -> context.getString(R.string.MessageRecord_you_added_s, added), R.drawable.ic_update_group_add_16)); + updates.add(updateDescription(R.string.MessageRecord_you_added_s, member.getUuid(), R.drawable.ic_update_group_add_16)); } } else { if (newMemberIsYou) { - updates.add(0, updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_added_you, editor), R.drawable.ic_update_group_add_16)); + updates.add(0, updateDescription(R.string.MessageRecord_s_added_you, change.getEditor(), R.drawable.ic_update_group_add_16)); } else { if (member.getUuid().equals(change.getEditor())) { - updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group_via_the_group_link, newMember), R.drawable.ic_update_group_accept_16)); + updates.add(updateDescription(R.string.MessageRecord_s_joined_the_group_via_the_group_link, member.getUuid(), R.drawable.ic_update_group_accept_16)); } else { - updates.add(updateDescription(change.getEditor(), member.getUuid(), (editor, newMember) -> context.getString(R.string.MessageRecord_s_added_s, editor, newMember), R.drawable.ic_update_group_add_16)); + updates.add(updateDescription(R.string.MessageRecord_s_added_s, change.getEditor(), member.getUuid(), R.drawable.ic_update_group_add_16)); } } } @@ -198,7 +206,7 @@ final class GroupsV2UpdateMessageProducer { if (newMemberIsYou) { updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_16)); } else { - updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group, newMember), R.drawable.ic_update_group_add_16)); + updates.add(updateDescription(R.string.MessageRecord_s_joined_the_group, member.getUuid(), R.drawable.ic_update_group_add_16)); } } } @@ -213,16 +221,16 @@ final class GroupsV2UpdateMessageProducer { if (removedMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_left_the_group), R.drawable.ic_update_group_leave_16)); } else { - updates.add(updateDescription(member, removedMember -> context.getString(R.string.MessageRecord_you_removed_s, removedMember), R.drawable.ic_update_group_remove_16)); + updates.add(updateDescription(R.string.MessageRecord_you_removed_s, member, R.drawable.ic_update_group_remove_16)); } } else { if (removedMemberIsYou) { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_removed_you_from_the_group, editor), R.drawable.ic_update_group_remove_16)); + updates.add(updateDescription(R.string.MessageRecord_s_removed_you_from_the_group, change.getEditor(), R.drawable.ic_update_group_remove_16)); } else { if (member.equals(change.getEditor())) { - updates.add(updateDescription(member, leavingMember -> context.getString(R.string.MessageRecord_s_left_the_group, leavingMember), R.drawable.ic_update_group_leave_16)); + updates.add(updateDescription(R.string.MessageRecord_s_left_the_group, member, R.drawable.ic_update_group_leave_16)); } else { - updates.add(updateDescription(change.getEditor(), member, (editor, removedMember) -> context.getString(R.string.MessageRecord_s_removed_s, editor, removedMember), R.drawable.ic_update_group_remove_16)); + updates.add(updateDescription(R.string.MessageRecord_s_removed_s, change.getEditor(), member, R.drawable.ic_update_group_remove_16)); } } } @@ -236,7 +244,7 @@ final class GroupsV2UpdateMessageProducer { if (removedMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_no_longer_in_the_group), R.drawable.ic_update_group_leave_16)); } else { - updates.add(updateDescription(member, oldMember -> context.getString(R.string.MessageRecord_s_is_no_longer_in_the_group, oldMember), R.drawable.ic_update_group_leave_16)); + updates.add(updateDescription(R.string.MessageRecord_s_is_no_longer_in_the_group, member, R.drawable.ic_update_group_leave_16)); } } } @@ -248,23 +256,23 @@ final class GroupsV2UpdateMessageProducer { boolean changedMemberIsYou = roleChange.getUuid().equals(selfUuidBytes); if (roleChange.getRole() == Member.Role.ADMINISTRATOR) { if (editorIsYou) { - updates.add(updateDescription(roleChange.getUuid(), newAdmin -> context.getString(R.string.MessageRecord_you_made_s_an_admin, newAdmin), R.drawable.ic_update_group_role_16)); + updates.add(updateDescription(R.string.MessageRecord_you_made_s_an_admin, roleChange.getUuid(), R.drawable.ic_update_group_role_16)); } else { if (changedMemberIsYou) { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_made_you_an_admin, editor), R.drawable.ic_update_group_role_16)); + updates.add(updateDescription(R.string.MessageRecord_s_made_you_an_admin, change.getEditor(), R.drawable.ic_update_group_role_16)); } else { - updates.add(updateDescription(change.getEditor(), roleChange.getUuid(), (editor, newAdmin) -> context.getString(R.string.MessageRecord_s_made_s_an_admin, editor, newAdmin), R.drawable.ic_update_group_role_16)); + updates.add(updateDescription(R.string.MessageRecord_s_made_s_an_admin, change.getEditor(), roleChange.getUuid(), R.drawable.ic_update_group_role_16)); } } } else { if (editorIsYou) { - updates.add(updateDescription(roleChange.getUuid(), oldAdmin -> context.getString(R.string.MessageRecord_you_revoked_admin_privileges_from_s, oldAdmin), R.drawable.ic_update_group_role_16)); + updates.add(updateDescription(R.string.MessageRecord_you_revoked_admin_privileges_from_s, roleChange.getUuid(), R.drawable.ic_update_group_role_16)); } else { if (changedMemberIsYou) { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_revoked_your_admin_privileges, editor), R.drawable.ic_update_group_role_16)); + updates.add(updateDescription(R.string.MessageRecord_s_revoked_your_admin_privileges, change.getEditor(), R.drawable.ic_update_group_role_16)); } else { - updates.add(updateDescription(change.getEditor(), roleChange.getUuid(), (editor, oldAdmin) -> context.getString(R.string.MessageRecord_s_revoked_admin_privileges_from_s, editor, oldAdmin), R.drawable.ic_update_group_role_16)); + updates.add(updateDescription(R.string.MessageRecord_s_revoked_admin_privileges_from_s, change.getEditor(), roleChange.getUuid(), R.drawable.ic_update_group_role_16)); } } } @@ -279,13 +287,13 @@ final class GroupsV2UpdateMessageProducer { if (changedMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_now_an_admin), R.drawable.ic_update_group_role_16)); } else { - updates.add(updateDescription(roleChange.getUuid(), newAdmin -> context.getString(R.string.MessageRecord_s_is_now_an_admin, newAdmin), R.drawable.ic_update_group_role_16)); + updates.add(updateDescription(R.string.MessageRecord_s_is_now_an_admin, roleChange.getUuid(), R.drawable.ic_update_group_role_16)); } } else { if (changedMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_no_longer_an_admin), R.drawable.ic_update_group_role_16)); } else { - updates.add(updateDescription(roleChange.getUuid(), oldAdmin -> context.getString(R.string.MessageRecord_s_is_no_longer_an_admin, oldAdmin), R.drawable.ic_update_group_role_16)); + updates.add(updateDescription(R.string.MessageRecord_s_is_no_longer_an_admin, roleChange.getUuid(), R.drawable.ic_update_group_role_16)); } } } @@ -299,10 +307,10 @@ final class GroupsV2UpdateMessageProducer { boolean newMemberIsYou = invitee.getUuid().equals(selfUuidBytes); if (newMemberIsYou) { - updates.add(0, updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, editor), R.drawable.ic_update_group_add_16)); + updates.add(0, updateDescription(R.string.MessageRecord_s_invited_you_to_the_group, change.getEditor(), R.drawable.ic_update_group_add_16)); } else { if (editorIsYou) { - updates.add(updateDescription(invitee.getUuid(), newInvitee -> context.getString(R.string.MessageRecord_you_invited_s_to_the_group, newInvitee), R.drawable.ic_update_group_add_16)); + updates.add(updateDescription(R.string.MessageRecord_you_invited_s_to_the_group, invitee.getUuid(), R.drawable.ic_update_group_add_16)); } else { notYouInviteCount++; } @@ -311,7 +319,7 @@ final class GroupsV2UpdateMessageProducer { if (notYouInviteCount > 0) { final int notYouInviteCountFinalCopy = notYouInviteCount; - updates.add(updateDescription(change.getEditor(), editor -> context.getResources().getQuantityString(R.plurals.MessageRecord_s_invited_members, notYouInviteCountFinalCopy, editor, notYouInviteCountFinalCopy), R.drawable.ic_update_group_add_16)); + updates.add(updateDescription(R.plurals.MessageRecord_s_invited_members, notYouInviteCountFinalCopy, change.getEditor(), notYouInviteCountFinalCopy, R.drawable.ic_update_group_add_16)); } } @@ -327,7 +335,7 @@ final class GroupsV2UpdateMessageProducer { if (UuidUtil.UNKNOWN_UUID.equals(uuid)) { updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_were_invited_to_the_group), R.drawable.ic_update_group_add_16)); } else { - updates.add(0, updateDescription(invitee.getAddedByUuid(), editor -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, editor), R.drawable.ic_update_group_add_16)); + updates.add(0, updateDescription(R.string.MessageRecord_s_invited_you_to_the_group, invitee.getAddedByUuid(), R.drawable.ic_update_group_add_16)); } } else { notYouInviteCount++; @@ -352,7 +360,7 @@ final class GroupsV2UpdateMessageProducer { updates.add(updateDescription(context.getString(R.string.MessageRecord_someone_declined_an_invitation_to_the_group), R.drawable.ic_update_group_decline_16)); } } else if (invitee.getUuid().equals(selfUuidBytes)) { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_revoked_your_invitation_to_the_group, editor), R.drawable.ic_update_group_decline_16)); + updates.add(updateDescription(R.string.MessageRecord_s_revoked_your_invitation_to_the_group, change.getEditor(), R.drawable.ic_update_group_decline_16)); } else { notDeclineCount++; } @@ -363,7 +371,7 @@ final class GroupsV2UpdateMessageProducer { updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_you_revoked_invites, notDeclineCount, notDeclineCount), R.drawable.ic_update_group_decline_16)); } else { final int notDeclineCountFinalCopy = notDeclineCount; - updates.add(updateDescription(change.getEditor(), editor -> context.getResources().getQuantityString(R.plurals.MessageRecord_s_revoked_invites, notDeclineCountFinalCopy, editor, notDeclineCountFinalCopy), R.drawable.ic_update_group_decline_16)); + updates.add(updateDescription(R.plurals.MessageRecord_s_revoked_invites, notDeclineCountFinalCopy, change.getEditor(), notDeclineCountFinalCopy, R.drawable.ic_update_group_decline_16)); } } } @@ -397,16 +405,16 @@ final class GroupsV2UpdateMessageProducer { if (newMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_accepted_invite), R.drawable.ic_update_group_accept_16)); } else { - updates.add(updateDescription(uuid, newPromotedMember -> context.getString(R.string.MessageRecord_you_added_invited_member_s, newPromotedMember), R.drawable.ic_update_group_add_16)); + updates.add(updateDescription(R.string.MessageRecord_you_added_invited_member_s, uuid, R.drawable.ic_update_group_add_16)); } } else { if (newMemberIsYou) { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_added_you, editor), R.drawable.ic_update_group_add_16)); + updates.add(updateDescription(R.string.MessageRecord_s_added_you, change.getEditor(), R.drawable.ic_update_group_add_16)); } else { if (uuid.equals(change.getEditor())) { - updates.add(updateDescription(uuid, newAcceptedMember -> context.getString(R.string.MessageRecord_s_accepted_invite, newAcceptedMember), R.drawable.ic_update_group_accept_16)); + updates.add(updateDescription(R.string.MessageRecord_s_accepted_invite, uuid, R.drawable.ic_update_group_accept_16)); } else { - updates.add(updateDescription(change.getEditor(), uuid, (editor, newAcceptedMember) -> context.getString(R.string.MessageRecord_s_added_invited_member_s, editor, newAcceptedMember), R.drawable.ic_update_group_add_16)); + updates.add(updateDescription(R.string.MessageRecord_s_added_invited_member_s, change.getEditor(), uuid, R.drawable.ic_update_group_add_16)); } } } @@ -421,7 +429,7 @@ final class GroupsV2UpdateMessageProducer { if (newMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_16)); } else { - updates.add(updateDescription(uuid, newMemberName -> context.getString(R.string.MessageRecord_s_joined_the_group, newMemberName), R.drawable.ic_update_group_add_16)); + updates.add(updateDescription(R.string.MessageRecord_s_joined_the_group, uuid, R.drawable.ic_update_group_add_16)); } } } @@ -434,7 +442,7 @@ final class GroupsV2UpdateMessageProducer { if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_name_to_s, newTitle), R.drawable.ic_update_group_name_16)); } else { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_the_group_name_to_s, editor, newTitle), R.drawable.ic_update_group_name_16)); + updates.add(updateDescription(R.string.MessageRecord_s_changed_the_group_name_to_s, change.getEditor(), newTitle, R.drawable.ic_update_group_name_16)); } } } @@ -446,7 +454,7 @@ final class GroupsV2UpdateMessageProducer { if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_description), R.drawable.ic_update_group_name_16)); } else { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_the_group_description, editor), R.drawable.ic_update_group_name_16)); + updates.add(updateDescription(R.string.MessageRecord_s_changed_the_group_description, change.getEditor(), R.drawable.ic_update_group_name_16)); } } } @@ -470,7 +478,7 @@ final class GroupsV2UpdateMessageProducer { if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_avatar), R.drawable.ic_update_group_avatar_16)); } else { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_the_group_avatar, editor), R.drawable.ic_update_group_avatar_16)); + updates.add(updateDescription(R.string.MessageRecord_s_changed_the_group_avatar, change.getEditor(), R.drawable.ic_update_group_avatar_16)); } } } @@ -489,7 +497,7 @@ final class GroupsV2UpdateMessageProducer { if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time), R.drawable.ic_update_timer_16)); } else { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, editor, time), R.drawable.ic_update_timer_16)); + updates.add(updateDescription(R.string.MessageRecord_s_set_disappearing_message_time_to_s, change.getEditor(), time, R.drawable.ic_update_timer_16)); } } } @@ -509,7 +517,7 @@ final class GroupsV2UpdateMessageProducer { if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_info_to_s, accessLevel), R.drawable.ic_update_group_role_16)); } else { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_info_to_s, editor, accessLevel), R.drawable.ic_update_group_role_16)); + updates.add(updateDescription(R.string.MessageRecord_s_changed_who_can_edit_group_info_to_s, change.getEditor(), accessLevel, R.drawable.ic_update_group_role_16)); } } } @@ -529,7 +537,7 @@ final class GroupsV2UpdateMessageProducer { if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_membership_to_s, accessLevel), R.drawable.ic_update_group_role_16)); } else { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_membership_to_s, editor, accessLevel), R.drawable.ic_update_group_role_16)); + updates.add(updateDescription(R.string.MessageRecord_s_changed_who_can_edit_group_membership_to_s, change.getEditor(), accessLevel, R.drawable.ic_update_group_role_16)); } } } @@ -565,9 +573,9 @@ final class GroupsV2UpdateMessageProducer { } } else { if (previousAccessControl == AccessControl.AccessRequired.ADMINISTRATOR) { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_off_admin_approval_for_the_group_link, editor), R.drawable.ic_update_group_role_16)); + updates.add(updateDescription(R.string.MessageRecord_s_turned_off_admin_approval_for_the_group_link, change.getEditor(), R.drawable.ic_update_group_role_16)); } else { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_off, editor), R.drawable.ic_update_group_role_16)); + updates.add(updateDescription(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_off, change.getEditor(), R.drawable.ic_update_group_role_16)); } } break; @@ -581,9 +589,9 @@ final class GroupsV2UpdateMessageProducer { } } else { if (previousAccessControl == AccessControl.AccessRequired.ANY) { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_admin_approval_for_the_group_link, editor), R.drawable.ic_update_group_role_16)); + updates.add(updateDescription(R.string.MessageRecord_s_turned_on_admin_approval_for_the_group_link, change.getEditor(), R.drawable.ic_update_group_role_16)); } else { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_on, editor), R.drawable.ic_update_group_role_16)); + updates.add(updateDescription(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_on, change.getEditor(), R.drawable.ic_update_group_role_16)); } } break; @@ -591,7 +599,7 @@ final class GroupsV2UpdateMessageProducer { if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_off_the_group_link), R.drawable.ic_update_group_role_16)); } else { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_off_the_group_link, editor), R.drawable.ic_update_group_role_16)); + updates.add(updateDescription(R.string.MessageRecord_s_turned_off_the_group_link, change.getEditor(), R.drawable.ic_update_group_role_16)); } break; } @@ -600,7 +608,7 @@ final class GroupsV2UpdateMessageProducer { if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_reset_the_group_link), R.drawable.ic_update_group_role_16)); } else { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_reset_the_group_link, editor), R.drawable.ic_update_group_role_16)); + updates.add(updateDescription(R.string.MessageRecord_s_reset_the_group_link, change.getEditor(), R.drawable.ic_update_group_role_16)); } } } @@ -650,12 +658,13 @@ final class GroupsV2UpdateMessageProducer { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_sent_a_request_to_join_the_group), R.drawable.ic_update_group_16)); } else { if (deleteRequestingUuids.contains(member.getUuid())) { - updates.add(updateDescription(member.getUuid(), requesting -> context.getResources().getQuantityString(R.plurals.MessageRecord_s_requested_and_cancelled_their_request_to_join_via_the_group_link, - change.getDeleteRequestingMembersCount(), - requesting, - change.getDeleteRequestingMembersCount()), R.drawable.ic_update_group_16)); + updates.add(updateDescription(R.plurals.MessageRecord_s_requested_and_cancelled_their_request_to_join_via_the_group_link, + change.getDeleteRequestingMembersCount(), + member.getUuid(), + change.getDeleteRequestingMembersCount(), + R.drawable.ic_update_group_16)); } else { - updates.add(updateDescription(member.getUuid(), requesting -> context.getString(R.string.MessageRecord_s_requested_to_join_via_the_group_link, requesting), R.drawable.ic_update_group_16)); + updates.add(updateDescription(R.string.MessageRecord_s_requested_to_join_via_the_group_link, member.getUuid(), R.drawable.ic_update_group_16)); } } } @@ -666,14 +675,14 @@ final class GroupsV2UpdateMessageProducer { boolean requestingMemberIsYou = requestingMember.getUuid().equals(selfUuidBytes); if (requestingMemberIsYou) { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_approved_your_request_to_join_the_group, editor), R.drawable.ic_update_group_accept_16)); + updates.add(updateDescription(R.string.MessageRecord_s_approved_your_request_to_join_the_group, change.getEditor(), R.drawable.ic_update_group_accept_16)); } else { boolean editorIsYou = change.getEditor().equals(selfUuidBytes); if (editorIsYou) { - updates.add(updateDescription(requestingMember.getUuid(), requesting -> context.getString(R.string.MessageRecord_you_approved_a_request_to_join_the_group_from_s, requesting), R.drawable.ic_update_group_accept_16)); + updates.add(updateDescription(R.string.MessageRecord_you_approved_a_request_to_join_the_group_from_s, requestingMember.getUuid(), R.drawable.ic_update_group_accept_16)); } else { - updates.add(updateDescription(change.getEditor(), requestingMember.getUuid(), (editor, requesting) -> context.getString(R.string.MessageRecord_s_approved_a_request_to_join_the_group_from_s, editor, requesting), R.drawable.ic_update_group_accept_16)); + updates.add(updateDescription(R.string.MessageRecord_s_approved_a_request_to_join_the_group_from_s, change.getEditor(), requestingMember.getUuid(), R.drawable.ic_update_group_accept_16)); } } } @@ -686,7 +695,7 @@ final class GroupsV2UpdateMessageProducer { if (requestingMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_approved), R.drawable.ic_update_group_accept_16)); } else { - updates.add(updateDescription(requestingMember.getUuid(), requesting -> context.getString(R.string.MessageRecord_a_request_to_join_the_group_from_s_has_been_approved, requesting), R.drawable.ic_update_group_accept_16)); + updates.add(updateDescription(R.string.MessageRecord_a_request_to_join_the_group_from_s_has_been_approved, requestingMember.getUuid(), R.drawable.ic_update_group_accept_16)); } } } @@ -713,9 +722,9 @@ final class GroupsV2UpdateMessageProducer { boolean editorIsCanceledMember = change.getEditor().equals(requestingMember); if (editorIsCanceledMember) { - updates.add(updateDescription(requestingMember, editorRequesting -> context.getString(R.string.MessageRecord_s_canceled_their_request_to_join_the_group, editorRequesting), R.drawable.ic_update_group_decline_16)); + updates.add(updateDescription(R.string.MessageRecord_s_canceled_their_request_to_join_the_group, requestingMember, R.drawable.ic_update_group_decline_16)); } else { - updates.add(updateDescription(change.getEditor(), requestingMember, (editor, requesting) -> context.getString(R.string.MessageRecord_s_denied_a_request_to_join_the_group_from_s, editor, requesting), R.drawable.ic_update_group_decline_16)); + updates.add(updateDescription(R.string.MessageRecord_s_denied_a_request_to_join_the_group_from_s, change.getEditor(), requestingMember, R.drawable.ic_update_group_decline_16)); } } } @@ -728,7 +737,7 @@ final class GroupsV2UpdateMessageProducer { if (requestingMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin), R.drawable.ic_update_group_decline_16)); } else { - updates.add(updateDescription(requestingMember, requesting -> context.getString(R.string.MessageRecord_a_request_to_join_the_group_from_s_has_been_denied, requesting), R.drawable.ic_update_group_decline_16)); + updates.add(updateDescription(R.string.MessageRecord_a_request_to_join_the_group_from_s_has_been_denied, requestingMember, R.drawable.ic_update_group_decline_16)); } } } @@ -740,13 +749,13 @@ final class GroupsV2UpdateMessageProducer { if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_allow_only_admins_to_send), R.drawable.ic_update_group_role_16)); } else { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_allow_only_admins_to_send, editor), R.drawable.ic_update_group_role_16)); + updates.add(updateDescription(R.string.MessageRecord_s_allow_only_admins_to_send, change.getEditor(), R.drawable.ic_update_group_role_16)); } } else if (change.getNewIsAnnouncementGroup() == EnabledState.DISABLED) { if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_allow_all_members_to_send), R.drawable.ic_update_group_role_16)); } else { - updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_allow_all_members_to_send, editor), R.drawable.ic_update_group_role_16)); + updates.add(updateDescription(R.string.MessageRecord_s_allow_all_members_to_send, change.getEditor(), R.drawable.ic_update_group_role_16)); } } } @@ -759,46 +768,130 @@ final class GroupsV2UpdateMessageProducer { } } - interface DescribeMemberStrategy { - - /** - * Map a ServiceId to a string that describes the group member. - */ - @NonNull - String describe(@NonNull ServiceId serviceId); - } - - private interface StringFactory1Arg { - String create(String arg1); - } - - private interface StringFactory2Args { - String create(String arg1, String arg2); - } - - private static UpdateDescription updateDescription(@NonNull String string, - @DrawableRes int iconResource) - { + private static UpdateDescription updateDescription(@NonNull String string, @DrawableRes int iconResource) { return UpdateDescription.staticDescription(string, iconResource); } - private UpdateDescription updateDescription(@NonNull ByteString uuid1Bytes, - @NonNull StringFactory1Arg stringFactory, + private UpdateDescription updateDescription(@StringRes int stringRes, + @NonNull ByteString uuid1Bytes, @DrawableRes int iconResource) { - ServiceId serviceId = ServiceId.fromByteStringOrUnknown(uuid1Bytes); + ServiceId serviceId = ServiceId.fromByteStringOrUnknown(uuid1Bytes); + RecipientId recipientId = RecipientId.from(serviceId, null); - return UpdateDescription.mentioning(Collections.singletonList(serviceId), () -> stringFactory.create(descriptionStrategy.describe(serviceId)), iconResource); + return UpdateDescription.mentioning( + Collections.singletonList(serviceId), + () -> { + List recipientIdList = Collections.singletonList(recipientId); + String templateString = context.getString(stringRes, makePlaceholders(recipientIdList, null)); + + return makeRecipientsClickable(context, templateString, recipientIdList, recipientClickHandler); + }, + iconResource); } - private UpdateDescription updateDescription(@NonNull ByteString uuid1Bytes, + private UpdateDescription updateDescription(@StringRes int stringRes, + @NonNull ByteString uuid1Bytes, @NonNull ByteString uuid2Bytes, - @NonNull StringFactory2Args stringFactory, @DrawableRes int iconResource) { ServiceId sid1 = ServiceId.fromByteStringOrUnknown(uuid1Bytes); ServiceId sid2 = ServiceId.fromByteStringOrUnknown(uuid2Bytes); - return UpdateDescription.mentioning(Arrays.asList(sid1, sid2), () -> stringFactory.create(descriptionStrategy.describe(sid1), descriptionStrategy.describe(sid2)), iconResource); + RecipientId recipientId1 = RecipientId.from(sid1, null); + RecipientId recipientId2 = RecipientId.from(sid2, null); + + return UpdateDescription.mentioning( + Arrays.asList(sid1, sid2), + () -> { + List recipientIdList = Arrays.asList(recipientId1, recipientId2); + String templateString = context.getString(stringRes, makePlaceholders(recipientIdList, null)); + + return makeRecipientsClickable(context, templateString, recipientIdList, recipientClickHandler); + }, + iconResource + ); + } + + private UpdateDescription updateDescription(@StringRes int stringRes, + @NonNull ByteString uuid1Bytes, + @NonNull Object formatArg, + @DrawableRes int iconResource) + { + ServiceId serviceId = ServiceId.fromByteStringOrUnknown(uuid1Bytes); + RecipientId recipientId = RecipientId.from(serviceId, null); + + return UpdateDescription.mentioning( + Collections.singletonList(serviceId), + () -> { + List recipientIdList = Collections.singletonList(recipientId); + String templateString = context.getString(stringRes, makePlaceholders(recipientIdList, Collections.singletonList(formatArg))); + + return makeRecipientsClickable(context, templateString, recipientIdList, recipientClickHandler); + }, + iconResource + ); + } + + private UpdateDescription updateDescription(@PluralsRes int stringRes, + int quantity, + @NonNull ByteString uuid1Bytes, + @NonNull Object formatArg, + @DrawableRes int iconResource) + { + ServiceId serviceId = ServiceId.fromByteStringOrUnknown(uuid1Bytes); + RecipientId recipientId = RecipientId.from(serviceId, null); + + return UpdateDescription.mentioning( + Collections.singletonList(serviceId), + () -> { + List recipientIdList = Collections.singletonList(recipientId); + String templateString = context.getResources().getQuantityString(stringRes, quantity, makePlaceholders(recipientIdList, Collections.singletonList(formatArg))); + + return makeRecipientsClickable(context, templateString, recipientIdList, recipientClickHandler); + }, + iconResource + ); + } + + private static @NonNull Object[] makePlaceholders(@NonNull List recipientIds, @Nullable List formatArgs) { + List placeholders = recipientIds.stream().map(GroupsV2UpdateMessageProducer::makePlaceholder).collect(Collectors.toList()); + List args = new LinkedList<>(placeholders); + + if (formatArgs != null) { + args.addAll(formatArgs); + } + + return args.toArray(); + } + + private static @NonNull Spannable makeRecipientsClickable(@NonNull Context context, @NonNull String template, @NonNull List recipientIds, @Nullable Consumer clickHandler) { + SpannableStringBuilder builder = new SpannableStringBuilder(); + int startIndex = 0; + + for (RecipientId recipientId : recipientIds) { + String placeholder = makePlaceholder(recipientId); + int placeHolderStart = template.indexOf(placeholder); + String beforeChunk = template.substring(startIndex, placeHolderStart); + + builder.append(beforeChunk); + builder.append(SpanUtil.clickable(Recipient.resolved(recipientId).getDisplayName(context), 0, v -> { + if (!recipientId.isUnknown() && clickHandler != null) { + clickHandler.accept(recipientId); + } + })); + + startIndex = placeHolderStart + placeholder.length(); + } + + if (startIndex < template.length()) { + builder.append(template.substring(startIndex)); + } + + return builder; + } + + private static @NonNull String makePlaceholder(@NonNull RecipientId recipientId) { + return "{{SPAN_PLACEHOLDER_" + recipientId + "}}"; } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/InMemoryMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/InMemoryMessageRecord.java index 19f9e559d8..72bf5c70bc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/InMemoryMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/InMemoryMessageRecord.java @@ -9,9 +9,11 @@ import androidx.annotation.StringRes; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.ExpirationUtil; import java.util.Collections; +import java.util.function.Consumer; /** * In memory message record for use in temporary conversation messages. @@ -90,7 +92,7 @@ public class InMemoryMessageRecord extends MessageRecord { } @Override - public @Nullable UpdateDescription getUpdateDisplayBody(@NonNull Context context) { + public @Nullable UpdateDescription getUpdateDisplayBody(@NonNull Context context, @Nullable Consumer recipientClickHandler) { return UpdateDescription.staticDescription(context.getString(isGroup ? R.string.ConversationUpdateItem_no_contacts_in_this_group_review_requests_carefully : R.string.ConversationUpdateItem_no_groups_in_common_review_requests_carefully), R.drawable.ic_update_info_16); @@ -127,7 +129,7 @@ public class InMemoryMessageRecord extends MessageRecord { } @Override - public @Nullable UpdateDescription getUpdateDisplayBody(@NonNull Context context) { + public @Nullable UpdateDescription getUpdateDisplayBody(@NonNull Context context, @Nullable Consumer recipientClickHandler) { String update = context.getString(R.string.ConversationUpdateItem_the_disappearing_message_time_will_be_set_to_s_when_you_message_them, ExpirationUtil.getExpirationDisplayValue(context, SignalStore.settings().getUniversalExpireTimer())); 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 dc0e02d730..e8791f31ed 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 @@ -10,7 +10,6 @@ import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; -import androidx.annotation.AnyThread; import androidx.annotation.ColorInt; import androidx.annotation.MainThread; import androidx.annotation.NonNull; @@ -43,7 +42,7 @@ public final class LiveUpdateMessage { boolean adjustPosition) { if (updateDescription.isStringStatic()) { - return LiveDataUtil.just(toSpannable(context, updateDescription, updateDescription.getStaticString(), defaultTint, adjustPosition)); + return LiveDataUtil.just(toSpannable(context, updateDescription, updateDescription.getStaticSpannable(), defaultTint, adjustPosition)); } List> allMentionedRecipients = Stream.of(updateDescription.getMentioned()) @@ -53,7 +52,7 @@ public final class LiveUpdateMessage { LiveData mentionedRecipientChangeStream = allMentionedRecipients.isEmpty() ? LiveDataUtil.just(new Object()) : LiveDataUtil.merge(allMentionedRecipients); - return Transformations.map(mentionedRecipientChangeStream, event -> toSpannable(context, updateDescription, updateDescription.getString(), defaultTint, adjustPosition)); + return Transformations.map(mentionedRecipientChangeStream, event -> toSpannable(context, updateDescription, updateDescription.getSpannable(), defaultTint, adjustPosition)); } /** @@ -66,7 +65,7 @@ public final class LiveUpdateMessage { return Transformations.map(Recipient.live(recipientId).getLiveDataResolved(), createStringInBackground::apply); } - private static @NonNull SpannableString toSpannable(@NonNull Context context, @NonNull UpdateDescription updateDescription, @NonNull String string, @ColorInt int defaultTint, boolean adjustPosition) { + private static @NonNull SpannableString toSpannable(@NonNull Context context, @NonNull UpdateDescription updateDescription, @NonNull Spannable string, @ColorInt int defaultTint, boolean adjustPosition) { 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/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index 94ba3b8a6f..f172508216 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -71,6 +71,7 @@ import java.util.List; import java.util.Locale; import java.util.Set; import java.util.UUID; +import java.util.function.Consumer; import java.util.function.Function; /** @@ -143,21 +144,27 @@ public abstract class MessageRecord extends DisplayRecord { return MmsSmsColumns.Types.isLegacyType(type); } + @Override @WorkerThread public SpannableString getDisplayBody(@NonNull Context context) { - UpdateDescription updateDisplayBody = getUpdateDisplayBody(context); + return getDisplayBody(context, null); + } + + @WorkerThread + public SpannableString getDisplayBody(@NonNull Context context, @Nullable Consumer recipientClickHandler) { + UpdateDescription updateDisplayBody = getUpdateDisplayBody(context, recipientClickHandler); if (updateDisplayBody != null) { - return new SpannableString(updateDisplayBody.getString()); + return new SpannableString(updateDisplayBody.getSpannable()); } return new SpannableString(getBody()); } - public @Nullable UpdateDescription getUpdateDisplayBody(@NonNull Context context) { + public @Nullable UpdateDescription getUpdateDisplayBody(@NonNull Context context, @Nullable Consumer recipientClickHandler) { if (isGroupUpdate() && isGroupV2()) { - return getGv2ChangeDescription(context, getBody()); + return getGv2ChangeDescription(context, getBody(), recipientClickHandler); } else if (isGroupUpdate() && isOutgoing()) { return staticUpdateDescription(context.getString(R.string.MessageRecord_you_updated_group), R.drawable.ic_update_group_16); } else if (isGroupUpdate()) { @@ -222,7 +229,7 @@ public abstract class MessageRecord extends DisplayRecord { } public boolean isDisplayBodyEmpty(@NonNull Context context) { - return getUpdateDisplayBody(context) == null && getBody().isEmpty(); + return getUpdateDisplayBody(context, null) == null && getBody().isEmpty(); } public boolean isSelfCreatedGroup() { @@ -259,12 +266,11 @@ public abstract class MessageRecord extends DisplayRecord { change.getEditor().equals(UuidUtil.toByteString(SignalStore.account().requireAci().uuid())); } - public static @NonNull UpdateDescription getGv2ChangeDescription(@NonNull Context context, @NonNull String body) { + public static @NonNull UpdateDescription getGv2ChangeDescription(@NonNull Context context, @NonNull String body, @Nullable Consumer recipientClickHandler) { try { - ShortStringDescriptionStrategy descriptionStrategy = new ShortStringDescriptionStrategy(context); byte[] decoded = Base64.decode(body); DecryptedGroupV2Context decryptedGroupV2Context = DecryptedGroupV2Context.parseFrom(decoded); - GroupsV2UpdateMessageProducer updateMessageProducer = new GroupsV2UpdateMessageProducer(context, descriptionStrategy, SignalStore.account().requireAci().uuid()); + GroupsV2UpdateMessageProducer updateMessageProducer = new GroupsV2UpdateMessageProducer(context, SignalStore.account().requireAci().uuid(), recipientClickHandler); if (decryptedGroupV2Context.hasChange() && (decryptedGroupV2Context.getGroupState().getRevision() != 0 || decryptedGroupV2Context.hasPreviousGroupState())) { return UpdateDescription.concatWithNewLines(updateMessageProducer.describeChanges(decryptedGroupV2Context.getPreviousGroupState(), decryptedGroupV2Context.getChange())); @@ -318,7 +324,7 @@ public abstract class MessageRecord extends DisplayRecord { @DrawableRes int iconResource) { return UpdateDescription.mentioning(Collections.singletonList(recipient.getServiceId().orElse(ServiceId.UNKNOWN)), - () -> stringGenerator.apply(recipient.resolve()), + () -> new SpannableString(stringGenerator.apply(recipient.resolve())), iconResource); } @@ -391,7 +397,7 @@ public abstract class MessageRecord extends DisplayRecord { .map(ServiceId::from) .toList(); - UpdateDescription.StringFactory stringFactory = new GroupCallUpdateMessageFactory(context, joinedMembers, withTime, groupCallUpdateDetails); + UpdateDescription.SpannableFactory stringFactory = new GroupCallUpdateMessageFactory(context, joinedMembers, withTime, groupCallUpdateDetails); return UpdateDescription.mentioning(joinedMembers, stringFactory, R.drawable.ic_video_16); } @@ -460,26 +466,6 @@ public abstract class MessageRecord extends DisplayRecord { throw new AssertionError("Attempting to modify a message with no change"); } - /** - * Describes a UUID by it's corresponding recipient's {@link Recipient#getDisplayName(Context)}. - */ - private static class ShortStringDescriptionStrategy implements GroupsV2UpdateMessageProducer.DescribeMemberStrategy { - - private final Context context; - - ShortStringDescriptionStrategy(@NonNull Context context) { - this.context = context; - } - - @Override - public @NonNull String describe(@NonNull ServiceId serviceId) { - if (serviceId.isUnknown()) { - return context.getString(R.string.MessageRecord_unknown); - } - return Recipient.resolved(RecipientId.from(serviceId, null)).getDisplayName(context); - } - } - public long getId() { return id; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/UpdateDescription.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/UpdateDescription.java index 7a96feefcd..5f8e11c864 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/UpdateDescription.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/UpdateDescription.java @@ -1,5 +1,9 @@ package org.thoughtcrime.securesms.database.model; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; + import androidx.annotation.AnyThread; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; @@ -7,7 +11,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; -import org.signal.core.util.ThreadUtil; import org.whispersystems.signalservice.api.push.ServiceId; import java.util.Collection; @@ -21,20 +24,20 @@ import java.util.Set; */ public final class UpdateDescription { - public interface StringFactory { - String create(); + public interface SpannableFactory { + Spannable create(); } private final Collection mentioned; - private final StringFactory stringFactory; - private final String staticString; + private final SpannableFactory stringFactory; + private final Spannable staticString; private final int lightIconResource; private final int lightTint; private final int darkTint; private UpdateDescription(@NonNull Collection mentioned, - @Nullable StringFactory stringFactory, - @Nullable String staticString, + @Nullable SpannableFactory stringFactory, + @Nullable Spannable staticString, @DrawableRes int iconResource, @ColorInt int lightTint, @ColorInt int darkTint) @@ -58,7 +61,7 @@ public final class UpdateDescription { * @param stringFactory The background method for generating the string. */ public static UpdateDescription mentioning(@NonNull Collection mentioned, - @NonNull StringFactory stringFactory, + @NonNull SpannableFactory stringFactory, @DrawableRes int iconResource) { return new UpdateDescription(ServiceId.filterKnown(mentioned), @@ -74,6 +77,15 @@ public final class UpdateDescription { */ public static UpdateDescription staticDescription(@NonNull String staticString, @DrawableRes int iconResource) + { + return new UpdateDescription(Collections.emptyList(), null, new SpannableString(staticString), iconResource, 0, 0); + } + + /** + * Create an update description that's string value is fixed. + */ + public static UpdateDescription staticDescription(@NonNull Spannable staticString, + @DrawableRes int iconResource) { return new UpdateDescription(Collections.emptyList(), null, staticString, iconResource, 0, 0); } @@ -86,7 +98,7 @@ public final class UpdateDescription { @ColorInt int lightTint, @ColorInt int darkTint) { - return new UpdateDescription(Collections.emptyList(), null, staticString, iconResource, lightTint, darkTint); + return new UpdateDescription(Collections.emptyList(), null, new SpannableString(staticString), iconResource, lightTint, darkTint); } public boolean isStringStatic() { @@ -94,7 +106,7 @@ public final class UpdateDescription { } @AnyThread - public @NonNull String getStaticString() { + public @NonNull Spannable getStaticSpannable() { if (staticString == null) { throw new UnsupportedOperationException(); } @@ -103,7 +115,7 @@ public final class UpdateDescription { } @WorkerThread - public @NonNull String getString() { + public @NonNull Spannable getSpannable() { if (staticString != null) { return staticString; } @@ -166,26 +178,26 @@ public final class UpdateDescription { } @WorkerThread - private static String concatLines(@NonNull List updateDescriptions) { - StringBuilder result = new StringBuilder(); + private static Spannable concatLines(@NonNull List updateDescriptions) { + SpannableStringBuilder result = new SpannableStringBuilder(); for (int i = 0; i < updateDescriptions.size(); i++) { if (i > 0) result.append('\n'); - result.append(updateDescriptions.get(i).getString()); + result.append(updateDescriptions.get(i).getSpannable()); } - return result.toString(); + return result; } @AnyThread - private static String concatStaticLines(@NonNull List updateDescriptions) { - StringBuilder result = new StringBuilder(); + private static Spannable concatStaticLines(@NonNull List updateDescriptions) { + SpannableStringBuilder result = new SpannableStringBuilder(); for (int i = 0; i < updateDescriptions.size(); i++) { if (i > 0) result.append('\n'); - result.append(updateDescriptions.get(i).getStaticString()); + result.append(updateDescriptions.get(i).getStaticSpannable()); } - return result.toString(); + return result; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItemV2.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItemV2.kt index 776f48119f..dfd88b6543 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItemV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItemV2.kt @@ -185,7 +185,7 @@ class MessageNotification(threadRecipient: Recipient, record: MessageRecord) : N } else if (record.isMms && !record.isMmsNotification && (record as MmsMessageRecord).slideDeck.slides.isNotEmpty()) { ThreadBodyUtil.getFormattedBodyFor(context, record) } else if (record.isGroupCall) { - MessageRecord.getGroupCallUpdateDescription(context, record.body, false).string + MessageRecord.getGroupCallUpdateDescription(context, record.body, false).spannable } else { MentionUtil.updateBodyWithDisplayNames(context, record) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientId.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientId.java index b7b4a67614..48f14355dd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientId.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientId.java @@ -92,6 +92,10 @@ public class RecipientId implements Parcelable, Comparable, Databas @AnyThread @SuppressLint("WrongThread") private static @NonNull RecipientId from(@Nullable ServiceId serviceId, @Nullable String e164, boolean highTrust) { + if (serviceId != null && serviceId.isUnknown()) { + return RecipientId.UNKNOWN; + } + RecipientId recipientId = RecipientIdCache.INSTANCE.get(serviceId, e164); if (recipientId == null) { diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java b/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java index e6a3533aa6..84ce95db16 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java @@ -1,13 +1,13 @@ package org.thoughtcrime.securesms.database.model; import android.app.Application; +import android.text.Spannable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; 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; @@ -19,18 +19,18 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import org.signal.core.util.ThreadUtil; import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedMember; import org.signal.storageservice.protos.groups.local.DecryptedPendingMember; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.util.UuidUtil; import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; @@ -38,8 +38,11 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.thoughtcrime.securesms.groups.v2.ChangeBuilder.changeBy; import static org.thoughtcrime.securesms.groups.v2.ChangeBuilder.changeByUnknown; import static org.signal.core.util.StringUtil.isolateBidi; @@ -60,18 +63,38 @@ public final class GroupsV2UpdateMessageProducerTest { public MockitoRule rule = MockitoJUnit.rule(); @Mock - public MockedStatic threadUtilMockedStatic; + public MockedStatic recipientMockedStatic; + + @Mock + public MockedStatic recipientIdMockedStatic; @Before public void setup() { you = UUID.randomUUID(); alice = UUID.randomUUID(); bob = UUID.randomUUID(); - GroupsV2UpdateMessageProducer.DescribeMemberStrategy describeMember = createDescriber(ImmutableMap.of(alice, "Alice", bob, "Bob")); - producer = new GroupsV2UpdateMessageProducer(ApplicationProvider.getApplicationContext(), describeMember, you); - threadUtilMockedStatic.when(ThreadUtil::assertMainThread).thenCallRealMethod(); - threadUtilMockedStatic.when(ThreadUtil::assertNotMainThread).thenCallRealMethod(); + recipientIdMockedStatic.when(() -> RecipientId.from(anyLong())).thenCallRealMethod(); + + RecipientId aliceId = RecipientId.from(1); + RecipientId bobId = RecipientId.from(2); + + Recipient aliceRecipient = recipientWithName(aliceId, "Alice"); + Recipient bobRecipient = recipientWithName(bobId, "Bob"); + + producer = new GroupsV2UpdateMessageProducer(ApplicationProvider.getApplicationContext(), you, null); + + recipientIdMockedStatic.when(() -> RecipientId.from(ServiceId.from(alice), null)).thenReturn(aliceId); + recipientIdMockedStatic.when(() -> RecipientId.from(ServiceId.from(bob), null)).thenReturn(bobId); + recipientMockedStatic.when(() -> Recipient.resolved(aliceId)).thenReturn(aliceRecipient); + recipientMockedStatic.when(() -> Recipient.resolved(bobId)).thenReturn(bobRecipient); + } + + private static Recipient recipientWithName(RecipientId id, String name) { + Recipient recipient = mock(Recipient.class); + when(recipient.getId()).thenReturn(id); + when(recipient.getDisplayName(any())).thenReturn(name); + return recipient; } @Test @@ -1333,11 +1356,11 @@ public final class GroupsV2UpdateMessageProducerTest { } private @NonNull List describeChange(@Nullable DecryptedGroup previousGroupState, - @NonNull DecryptedGroupChange change) + @NonNull DecryptedGroupChange change) { - threadUtilMockedStatic.when(ThreadUtil::isMainThread).thenReturn(false); return Stream.of(producer.describeChanges(previousGroupState, change)) - .map(UpdateDescription::getString) + .map(UpdateDescription::getSpannable) + .map(Spannable::toString) .toList(); } @@ -1346,8 +1369,7 @@ public final class GroupsV2UpdateMessageProducerTest { } private @NonNull String describeNewGroup(@NonNull DecryptedGroup group, @NonNull DecryptedGroupChange groupChange) { - threadUtilMockedStatic.when(ThreadUtil::isMainThread).thenReturn(false); - return producer.describeNewGroup(group, groupChange).getString(); + return producer.describeNewGroup(group, groupChange).getSpannable().toString(); } private static GroupStateBuilder newGroupBy(UUID foundingMember, int revision) { @@ -1399,12 +1421,4 @@ public final class GroupsV2UpdateMessageProducerTest { return builder.build(); } } - - private static @NonNull GroupsV2UpdateMessageProducer.DescribeMemberStrategy createDescriber(@NonNull Map map) { - return serviceId -> { - String name = map.get(serviceId.uuid()); - assertNotNull(name); - return name; - }; - } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/model/UpdateDescriptionTest.java b/app/src/test/java/org/thoughtcrime/securesms/database/model/UpdateDescriptionTest.java index 07c654a74f..c3949c8eb0 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/model/UpdateDescriptionTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/database/model/UpdateDescriptionTest.java @@ -1,6 +1,12 @@ package org.thoughtcrime.securesms.database.model; +import android.app.Application; +import android.text.SpannableString; + import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; import org.whispersystems.signalservice.api.push.ServiceId; import java.util.Arrays; @@ -13,13 +19,15 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE, application = Application.class) public final class UpdateDescriptionTest { @Test public void staticDescription_byGetStaticString() { UpdateDescription description = UpdateDescription.staticDescription("update", 0); - assertEquals("update", description.getStaticString()); + assertEquals("update", description.getStaticSpannable().toString()); } @Test @@ -33,30 +41,30 @@ public final class UpdateDescriptionTest { public void staticDescription_byString() { UpdateDescription description = UpdateDescription.staticDescription("update", 0); - assertEquals("update", description.getString()); + assertEquals("update", description.getSpannable().toString()); } @Test(expected = UnsupportedOperationException.class) public void stringFactory_cannot_call_static_string() { - UpdateDescription description = UpdateDescription.mentioning(Collections.singletonList(ServiceId.from(UUID.randomUUID())), () -> "update", 0); + UpdateDescription description = UpdateDescription.mentioning(Collections.singletonList(ServiceId.from(UUID.randomUUID())), () -> new SpannableString("update"), 0); - description.getStaticString(); + description.getStaticSpannable(); } @Test public void stringFactory_not_evaluated_until_getString() { AtomicInteger factoryCalls = new AtomicInteger(); - UpdateDescription.StringFactory stringFactory = () -> { + UpdateDescription.SpannableFactory stringFactory = () -> { factoryCalls.incrementAndGet(); - return "update"; + return new SpannableString("update"); }; UpdateDescription description = UpdateDescription.mentioning(Collections.singletonList(ServiceId.from(UUID.randomUUID())), stringFactory, 0); assertEquals(0, factoryCalls.get()); - String string = description.getString(); + String string = description.getSpannable().toString(); assertEquals("update", string); assertEquals(1, factoryCalls.get()); @@ -64,13 +72,13 @@ public final class UpdateDescriptionTest { @Test public void stringFactory_reevaluated_on_every_call() { - AtomicInteger factoryCalls = new AtomicInteger(); - UpdateDescription.StringFactory stringFactory = () -> "call" + factoryCalls.incrementAndGet(); - UpdateDescription description = UpdateDescription.mentioning(Collections.singletonList(ServiceId.from(UUID.randomUUID())), stringFactory, 0); + AtomicInteger factoryCalls = new AtomicInteger(); + UpdateDescription.SpannableFactory stringFactory = () -> new SpannableString( "call" + factoryCalls.incrementAndGet()); + UpdateDescription description = UpdateDescription.mentioning(Collections.singletonList(ServiceId.from(UUID.randomUUID())), stringFactory, 0); - assertEquals("call1", description.getString()); - assertEquals("call2", description.getString()); - assertEquals("call3", description.getString()); + assertEquals("call1", description.getSpannable().toString()); + assertEquals("call2", description.getSpannable().toString()); + assertEquals("call3", description.getSpannable().toString()); } @Test @@ -81,8 +89,8 @@ public final class UpdateDescriptionTest { UpdateDescription description = UpdateDescription.concatWithNewLines(Arrays.asList(description1, description2)); assertTrue(description.isStringStatic()); - assertEquals("update1\nupdate2", description.getStaticString()); - assertEquals("update1\nupdate2", description.getString()); + assertEquals("update1\nupdate2", description.getStaticSpannable().toString()); + assertEquals("update1\nupdate2", description.getSpannable().toString()); } @Test @@ -96,12 +104,12 @@ public final class UpdateDescriptionTest { @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(ServiceId.from(UUID.randomUUID())), stringFactory1, 0); - UpdateDescription description2 = UpdateDescription.mentioning(Collections.singletonList(ServiceId.from(UUID.randomUUID())), stringFactory2, 0); + AtomicInteger factoryCalls1 = new AtomicInteger(); + AtomicInteger factoryCalls2 = new AtomicInteger(); + UpdateDescription.SpannableFactory stringFactory1 = () -> new SpannableString("update." + factoryCalls1.incrementAndGet()); + UpdateDescription.SpannableFactory stringFactory2 = () -> new SpannableString("update." + factoryCalls2.incrementAndGet()); + UpdateDescription description1 = UpdateDescription.mentioning(Collections.singletonList(ServiceId.from(UUID.randomUUID())), stringFactory1, 0); + UpdateDescription description2 = UpdateDescription.mentioning(Collections.singletonList(ServiceId.from(UUID.randomUUID())), stringFactory2, 0); factoryCalls1.set(10); factoryCalls2.set(20); @@ -110,20 +118,20 @@ public final class UpdateDescriptionTest { assertFalse(description.isStringStatic()); - assertEquals("update.11\nupdate.21", description.getString()); - assertEquals("update.12\nupdate.22", description.getString()); - assertEquals("update.13\nupdate.23", description.getString()); + assertEquals("update.11\nupdate.21", description.getSpannable().toString()); + assertEquals("update.12\nupdate.22", description.getSpannable().toString()); + assertEquals("update.13\nupdate.23", description.getSpannable().toString()); } @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(ServiceId.from(UUID.randomUUID())), stringFactory1, 0); - UpdateDescription description2 = UpdateDescription.staticDescription("static", 0); - UpdateDescription description3 = UpdateDescription.mentioning(Collections.singletonList(ServiceId.from(UUID.randomUUID())), stringFactory2, 0); + AtomicInteger factoryCalls1 = new AtomicInteger(); + AtomicInteger factoryCalls2 = new AtomicInteger(); + UpdateDescription.SpannableFactory stringFactory1 = () -> new SpannableString("update." + factoryCalls1.incrementAndGet()); + UpdateDescription.SpannableFactory stringFactory2 = () -> new SpannableString("update." + factoryCalls2.incrementAndGet()); + UpdateDescription description1 = UpdateDescription.mentioning(Collections.singletonList(ServiceId.from(UUID.randomUUID())), stringFactory1, 0); + UpdateDescription description2 = UpdateDescription.staticDescription("static", 0); + UpdateDescription description3 = UpdateDescription.mentioning(Collections.singletonList(ServiceId.from(UUID.randomUUID())), stringFactory2, 0); factoryCalls1.set(100); factoryCalls2.set(200); @@ -132,8 +140,8 @@ public final class UpdateDescriptionTest { assertFalse(description.isStringStatic()); - assertEquals("update.101\nstatic\nupdate.201", description.getString()); - assertEquals("update.102\nstatic\nupdate.202", description.getString()); - assertEquals("update.103\nstatic\nupdate.203", description.getString()); + assertEquals("update.101\nstatic\nupdate.201", description.getSpannable().toString()); + assertEquals("update.102\nstatic\nupdate.202", description.getSpannable().toString()); + assertEquals("update.103\nstatic\nupdate.203", description.getSpannable().toString()); } } \ No newline at end of file