package org.tm.archive.database; import android.content.Context; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; import com.annimon.stream.Stream; import com.annimon.stream.function.Function; import org.tm.archive.database.model.Mention; import org.tm.archive.database.model.MessageRecord; import org.tm.archive.database.model.databaseprotos.BodyRangeList; import org.tm.archive.recipients.Recipient; import org.tm.archive.recipients.RecipientId; import org.whispersystems.signalservice.api.push.ServiceId; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; import java.util.stream.Collectors; public final class MentionUtil { public static final char MENTION_STARTER = '@'; static final String MENTION_PLACEHOLDER = "\uFFFC"; private MentionUtil() { } @WorkerThread public static @NonNull UpdatedBodyAndMentions updateBodyWithDisplayNames(@NonNull Context context, @NonNull MessageRecord messageRecord) { return updateBodyWithDisplayNames(context, messageRecord, messageRecord.getDisplayBody(context)); } @WorkerThread public static @NonNull UpdatedBodyAndMentions updateBodyWithDisplayNames(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull CharSequence body) { List mentions = SignalDatabase.mentions().getMentionsForMessage(messageRecord.getId()); return updateBodyAndMentionsWithDisplayNames(context, body, mentions); } @WorkerThread public static @NonNull UpdatedBodyAndMentions updateBodyAndMentionsWithDisplayNames(@NonNull Context context, @NonNull CharSequence body, @NonNull List mentions) { return update(body, mentions, m -> MENTION_STARTER + Recipient.resolved(m.getRecipientId()).getMentionDisplayName(context)); } public static @NonNull UpdatedBodyAndMentions updateBodyAndMentionsWithPlaceholders(@Nullable CharSequence body, @NonNull List mentions) { return update(body, mentions, m -> MENTION_PLACEHOLDER); } @VisibleForTesting static @NonNull UpdatedBodyAndMentions update(@Nullable CharSequence body, @NonNull List mentions, @NonNull Function replacementTextGenerator) { if (body == null || mentions.isEmpty()) { return new UpdatedBodyAndMentions(body, mentions, Collections.emptyList()); } SortedSet sortedMentions = new TreeSet<>(mentions); SpannableStringBuilder updatedBody = new SpannableStringBuilder(); List updatedMentions = new ArrayList<>(); List bodyAdjustments = new ArrayList<>(); int bodyIndex = 0; for (Mention mention : sortedMentions) { if (invalidMention(body, mention) || bodyIndex > mention.getStart()) { continue; } updatedBody.append(body.subSequence(bodyIndex, mention.getStart())); CharSequence replaceWith = replacementTextGenerator.apply(mention); Mention updatedMention = new Mention(mention.getRecipientId(), updatedBody.length(), replaceWith.length()); updatedBody.append(replaceWith); updatedMentions.add(updatedMention); bodyAdjustments.add(new BodyAdjustment(mention.getStart(), mention.getLength(), updatedMention.getLength())); bodyIndex = mention.getStart() + mention.getLength(); } if (bodyIndex < body.length()) { updatedBody.append(body.subSequence(bodyIndex, body.length())); } return new UpdatedBodyAndMentions(updatedBody, updatedMentions, bodyAdjustments); } public static @Nullable BodyRangeList mentionsToBodyRangeList(@Nullable List mentions) { if (mentions == null || mentions.isEmpty()) { return null; } BodyRangeList.Builder builder = new BodyRangeList.Builder(); builder.ranges( mentions.stream() .map(mention -> { String uuid = Recipient.resolved(mention.getRecipientId()).requireAci().toString(); return new BodyRangeList.BodyRange.Builder() .mentionUuid(uuid) .start(mention.getStart()) .length(mention.getLength()) .build(); }) .collect(Collectors.toList()) ); return builder.build(); } public static @NonNull List bodyRangeListToMentions(@Nullable BodyRangeList bodyRanges) { if (bodyRanges != null) { return Stream.of(bodyRanges.ranges) .filter(bodyRange -> bodyRange.mentionUuid != null) .map(mention -> { RecipientId id = Recipient.externalPush(ServiceId.parseOrThrow(mention.mentionUuid)).getId(); return new Mention(id, mention.start, mention.length); }) .toList(); } else { return Collections.emptyList(); } } private static boolean invalidMention(@NonNull CharSequence body, @NonNull Mention mention) { int start = mention.getStart(); int length = mention.getLength(); return start < 0 || length < 0 || (start + length) > body.length(); } public static class UpdatedBodyAndMentions { @Nullable private final CharSequence body; @NonNull private final List mentions; @NonNull private final List bodyAdjustments; private UpdatedBodyAndMentions(@Nullable CharSequence body, @NonNull List mentions, @NonNull List bodyAdjustments) { this.body = body; this.mentions = mentions; this.bodyAdjustments = bodyAdjustments; } public @Nullable CharSequence getBody() { return body; } public @NonNull List getMentions() { return mentions; } public @NonNull List getBodyAdjustments() { return bodyAdjustments; } @Nullable String getBodyAsString() { return body != null ? body.toString() : null; } } }