Make names in group update descriptions tappable.

This commit is contained in:
Greyson Parrelli 2022-04-19 09:32:48 -04:00 committed by Alex Hart
parent 3b17a41415
commit e2cb535f3f
17 changed files with 378 additions and 215 deletions

View file

@ -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);

View file

@ -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<Object> 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<MultiselectPart> 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);
}
/**

View file

@ -106,7 +106,7 @@ class ConversationDataSource implements PagedDataSource<MessageId, ConversationM
reactionHelper.add(record);
attachmentHelper.add(record);
UpdateDescription description = record.getUpdateDisplayBody(context);
UpdateDescription description = record.getUpdateDisplayBody(context, null);
if (description != null) {
referencedIds.addAll(description.getMentioned());
}

View file

@ -1928,6 +1928,13 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
.setPositiveButton(R.string.ConversationFragment__block_request_button, (d, w) -> 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() {

View file

@ -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<SpannableString> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(getContext(), updateDescription, textColor, true);
LiveData<SpannableString> 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<MessageRecord> candidate)

View file

@ -73,7 +73,7 @@ abstract class ConversationListDataSource implements PagedDataSource<Long, Conve
if (!record.getRecipient().isPushV2Group()) {
needsResolve.add(record.getRecipient().getId());
} else if (SmsDatabase.Types.isGroupUpdate(record.getType())) {
UpdateDescription description = MessageRecord.getGv2ChangeDescription(ApplicationDependencies.getApplication(), record.getBody());
UpdateDescription description = MessageRecord.getGv2ChangeDescription(ApplicationDependencies.getApplication(), record.getBody(), null);
needsResolve.addAll(description.getMentioned().stream().map(sid -> RecipientId.from(sid, null)).collect(Collectors.toList()));
}
}

View file

@ -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);
}

View file

@ -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<ServiceId> 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()) {

View file

@ -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<RecipientId> 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<RecipientId> 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<DecryptedPendingMember> 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<RecipientId> 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<RecipientId> 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<RecipientId> 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<RecipientId> 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<RecipientId> recipientIds, @Nullable List<Object> formatArgs) {
List<String> placeholders = recipientIds.stream().map(GroupsV2UpdateMessageProducer::makePlaceholder).collect(Collectors.toList());
List<Object> 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<RecipientId> recipientIds, @Nullable Consumer<RecipientId> 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 + "}}";
}
}

View file

@ -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<RecipientId> 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<RecipientId> 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()));

View file

@ -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<LiveData<Recipient>> 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();

View file

@ -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<RecipientId> 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<RecipientId> 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<RecipientId> 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;
}

View file

@ -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<ServiceId> 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<ServiceId> 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<ServiceId> 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<UpdateDescription> updateDescriptions) {
StringBuilder result = new StringBuilder();
private static Spannable concatLines(@NonNull List<UpdateDescription> 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<UpdateDescription> updateDescriptions) {
StringBuilder result = new StringBuilder();
private static Spannable concatStaticLines(@NonNull List<UpdateDescription> 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;
}
}

View file

@ -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)
}

View file

@ -92,6 +92,10 @@ public class RecipientId implements Parcelable, Comparable<RecipientId>, 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) {

View file

@ -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<ThreadUtil> threadUtilMockedStatic;
public MockedStatic<Recipient> recipientMockedStatic;
@Mock
public MockedStatic<RecipientId> 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<String> 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<UUID, String> map) {
return serviceId -> {
String name = map.get(serviceId.uuid());
assertNotNull(name);
return name;
};
}
}

View file

@ -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());
}
}