That fuck shit the fascists are using
1package org.tm.archive.database;
2
3import android.content.Context;
4import android.text.SpannableStringBuilder;
5
6import androidx.annotation.NonNull;
7import androidx.annotation.Nullable;
8import androidx.annotation.VisibleForTesting;
9import androidx.annotation.WorkerThread;
10
11import com.annimon.stream.Stream;
12import com.annimon.stream.function.Function;
13
14import org.tm.archive.database.model.Mention;
15import org.tm.archive.database.model.MessageRecord;
16import org.tm.archive.database.model.databaseprotos.BodyRangeList;
17import org.tm.archive.recipients.Recipient;
18import org.tm.archive.recipients.RecipientId;
19import org.whispersystems.signalservice.api.push.ServiceId;
20
21import java.util.ArrayList;
22import java.util.Collections;
23import java.util.List;
24import java.util.SortedSet;
25import java.util.TreeSet;
26import java.util.stream.Collectors;
27
28public final class MentionUtil {
29
30 public static final char MENTION_STARTER = '@';
31 static final String MENTION_PLACEHOLDER = "\uFFFC";
32
33 private MentionUtil() { }
34
35 @WorkerThread
36 public static @NonNull UpdatedBodyAndMentions updateBodyWithDisplayNames(@NonNull Context context, @NonNull MessageRecord messageRecord) {
37 return updateBodyWithDisplayNames(context, messageRecord, messageRecord.getDisplayBody(context));
38 }
39
40 @WorkerThread
41 public static @NonNull UpdatedBodyAndMentions updateBodyWithDisplayNames(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull CharSequence body) {
42 List<Mention> mentions = SignalDatabase.mentions().getMentionsForMessage(messageRecord.getId());
43 return updateBodyAndMentionsWithDisplayNames(context, body, mentions);
44 }
45
46 @WorkerThread
47 public static @NonNull UpdatedBodyAndMentions updateBodyAndMentionsWithDisplayNames(@NonNull Context context, @NonNull CharSequence body, @NonNull List<Mention> mentions) {
48 return update(body, mentions, m -> MENTION_STARTER + Recipient.resolved(m.getRecipientId()).getMentionDisplayName(context));
49 }
50
51 public static @NonNull UpdatedBodyAndMentions updateBodyAndMentionsWithPlaceholders(@Nullable CharSequence body, @NonNull List<Mention> mentions) {
52 return update(body, mentions, m -> MENTION_PLACEHOLDER);
53 }
54
55 @VisibleForTesting
56 static @NonNull UpdatedBodyAndMentions update(@Nullable CharSequence body, @NonNull List<Mention> mentions, @NonNull Function<Mention, CharSequence> replacementTextGenerator) {
57 if (body == null || mentions.isEmpty()) {
58 return new UpdatedBodyAndMentions(body, mentions, Collections.emptyList());
59 }
60
61 SortedSet<Mention> sortedMentions = new TreeSet<>(mentions);
62 SpannableStringBuilder updatedBody = new SpannableStringBuilder();
63 List<Mention> updatedMentions = new ArrayList<>();
64 List<BodyAdjustment> bodyAdjustments = new ArrayList<>();
65
66 int bodyIndex = 0;
67
68 for (Mention mention : sortedMentions) {
69 if (invalidMention(body, mention) || bodyIndex > mention.getStart()) {
70 continue;
71 }
72
73 updatedBody.append(body.subSequence(bodyIndex, mention.getStart()));
74 CharSequence replaceWith = replacementTextGenerator.apply(mention);
75 Mention updatedMention = new Mention(mention.getRecipientId(), updatedBody.length(), replaceWith.length());
76
77 updatedBody.append(replaceWith);
78 updatedMentions.add(updatedMention);
79
80 bodyAdjustments.add(new BodyAdjustment(mention.getStart(), mention.getLength(), updatedMention.getLength()));
81
82 bodyIndex = mention.getStart() + mention.getLength();
83 }
84
85 if (bodyIndex < body.length()) {
86 updatedBody.append(body.subSequence(bodyIndex, body.length()));
87 }
88
89 return new UpdatedBodyAndMentions(updatedBody, updatedMentions, bodyAdjustments);
90 }
91
92 public static @Nullable BodyRangeList mentionsToBodyRangeList(@Nullable List<Mention> mentions) {
93 if (mentions == null || mentions.isEmpty()) {
94 return null;
95 }
96
97 BodyRangeList.Builder builder = new BodyRangeList.Builder();
98 builder.ranges(
99 mentions.stream()
100 .map(mention -> {
101 String uuid = Recipient.resolved(mention.getRecipientId()).requireAci().toString();
102 return new BodyRangeList.BodyRange.Builder()
103 .mentionUuid(uuid)
104 .start(mention.getStart())
105 .length(mention.getLength())
106 .build();
107 })
108 .collect(Collectors.toList())
109 );
110 return builder.build();
111 }
112
113 public static @NonNull List<Mention> bodyRangeListToMentions(@Nullable BodyRangeList bodyRanges) {
114 if (bodyRanges != null) {
115 return Stream.of(bodyRanges.ranges)
116 .filter(bodyRange -> bodyRange.mentionUuid != null)
117 .map(mention -> {
118 RecipientId id = Recipient.externalPush(ServiceId.parseOrThrow(mention.mentionUuid)).getId();
119 return new Mention(id, mention.start, mention.length);
120 })
121 .toList();
122 } else {
123 return Collections.emptyList();
124 }
125 }
126
127 private static boolean invalidMention(@NonNull CharSequence body, @NonNull Mention mention) {
128 int start = mention.getStart();
129 int length = mention.getLength();
130
131 return start < 0 || length < 0 || (start + length) > body.length();
132 }
133
134 public static class UpdatedBodyAndMentions {
135 @Nullable private final CharSequence body;
136 @NonNull private final List<Mention> mentions;
137 @NonNull private final List<BodyAdjustment> bodyAdjustments;
138
139 private UpdatedBodyAndMentions(@Nullable CharSequence body, @NonNull List<Mention> mentions, @NonNull List<BodyAdjustment> bodyAdjustments) {
140 this.body = body;
141 this.mentions = mentions;
142 this.bodyAdjustments = bodyAdjustments;
143 }
144
145 public @Nullable CharSequence getBody() {
146 return body;
147 }
148
149 public @NonNull List<Mention> getMentions() {
150 return mentions;
151 }
152
153 public @NonNull List<BodyAdjustment> getBodyAdjustments() {
154 return bodyAdjustments;
155 }
156
157 @Nullable String getBodyAsString() {
158 return body != null ? body.toString() : null;
159 }
160 }
161}