That fuck shit the fascists are using
1package org.tm.archive.components;
2
3import android.content.Context;
4import android.content.res.Configuration;
5import android.graphics.Canvas;
6import android.os.Build;
7import android.os.Bundle;
8import android.text.Annotation;
9import android.text.Editable;
10import android.text.InputType;
11import android.text.Selection;
12import android.text.SpannableString;
13import android.text.SpannableStringBuilder;
14import android.text.Spanned;
15import android.text.TextUtils;
16import android.text.TextUtils.TruncateAt;
17import android.util.AttributeSet;
18import android.view.ActionMode;
19import android.view.Menu;
20import android.view.MenuItem;
21import android.view.inputmethod.EditorInfo;
22import android.view.inputmethod.InputConnection;
23
24import androidx.annotation.IdRes;
25import androidx.annotation.NonNull;
26import androidx.annotation.Nullable;
27import androidx.core.content.ContextCompat;
28import androidx.core.view.inputmethod.EditorInfoCompat;
29import androidx.core.view.inputmethod.InputConnectionCompat;
30import androidx.core.view.inputmethod.InputContentInfoCompat;
31
32import org.signal.core.util.StringUtil;
33import org.signal.core.util.logging.Log;
34import org.tm.archive.R;
35import org.tm.archive.components.emoji.EmojiEditText;
36import org.tm.archive.components.mention.MentionAnnotation;
37import org.tm.archive.components.mention.MentionDeleter;
38import org.tm.archive.components.mention.MentionRendererDelegate;
39import org.tm.archive.components.mention.MentionValidatorWatcher;
40import org.tm.archive.components.spoiler.SpoilerRendererDelegate;
41import org.tm.archive.conversation.MessageSendType;
42import org.tm.archive.conversation.MessageStyler;
43import org.tm.archive.conversation.ui.inlinequery.InlineQuery;
44import org.tm.archive.conversation.ui.inlinequery.InlineQueryChangedListener;
45import org.tm.archive.conversation.ui.inlinequery.InlineQueryReplacement;
46import org.tm.archive.database.model.Mention;
47import org.tm.archive.database.model.databaseprotos.BodyRangeList;
48import org.tm.archive.keyvalue.SignalStore;
49import org.tm.archive.recipients.RecipientId;
50import org.tm.archive.util.TextSecurePreferences;
51
52import java.util.List;
53import java.util.Objects;
54import java.util.regex.Pattern;
55
56import static org.tm.archive.database.MentionUtil.MENTION_STARTER;
57
58public class ComposeText extends EmojiEditText {
59
60 private static final char EMOJI_STARTER = ':';
61
62 private static final Pattern TIME_PATTERN = Pattern.compile("^[0-9]{1,2}:[0-9]{1,2}$");
63
64 private CharSequence hint;
65 private MentionRendererDelegate mentionRendererDelegate;
66 private SpoilerRendererDelegate spoilerRendererDelegate;
67 private MentionValidatorWatcher mentionValidatorWatcher;
68
69 @Nullable private InputPanel.MediaListener mediaListener;
70 @Nullable private CursorPositionChangedListener cursorPositionChangedListener;
71 @Nullable private InlineQueryChangedListener inlineQueryChangedListener;
72 @Nullable private StylingChangedListener stylingChangedListener;
73
74 public ComposeText(Context context) {
75 super(context);
76 initialize();
77 }
78
79 public ComposeText(Context context, AttributeSet attrs) {
80 super(context, attrs);
81 initialize();
82 }
83
84 public ComposeText(Context context, AttributeSet attrs, int defStyleAttr) {
85 super(context, attrs, defStyleAttr);
86 initialize();
87 }
88
89 /**
90 * Trims and returns text while preserving potential spans like {@link MentionAnnotation}.
91 */
92 public @NonNull CharSequence getTextTrimmed() {
93 Editable text = getText();
94 if (text == null) {
95 return "";
96 }
97 return StringUtil.trimSequence(text);
98 }
99
100 @Override
101 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
102 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
103
104 if (getLayout() != null && !TextUtils.isEmpty(hint)) {
105 setHintWithChecks(ellipsizeToWidth(hint));
106 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
107 }
108 }
109
110 @Override
111 protected void onSelectionChanged(int selectionStart, int selectionEnd) {
112 super.onSelectionChanged(selectionStart, selectionEnd);
113
114 if (getText() != null) {
115 boolean selectionChanged = changeSelectionForPartialMentions(getText(), selectionStart, selectionEnd);
116 if (selectionChanged) {
117 return;
118 }
119
120 if (selectionStart == selectionEnd) {
121 doAfterCursorChange(getText());
122 } else {
123 clearInlineQuery();
124 }
125 }
126
127 if (cursorPositionChangedListener != null) {
128 cursorPositionChangedListener.onCursorPositionChanged(selectionStart, selectionEnd);
129 }
130 }
131
132 @Override
133 protected void onDraw(Canvas canvas) {
134 if (getText() != null && getLayout() != null) {
135 int checkpoint = canvas.save();
136
137 // Clip using same logic as TextView drawing
138 int maxScrollY = getLayout().getHeight() - getBottom() - getTop() - getCompoundPaddingBottom() - getCompoundPaddingTop();
139 float clipLeft = getCompoundPaddingLeft() + getScrollX();
140 float clipTop = (getScrollY() == 0) ? 0 : getExtendedPaddingTop() + getScrollY();
141 float clipRight = getRight() - getLeft() - getCompoundPaddingRight() + getScrollX();
142 float clipBottom = getBottom() - getTop() + getScrollY() - ((getScrollY() == maxScrollY) ? 0 : getExtendedPaddingBottom());
143
144 canvas.clipRect(clipLeft - 10, clipTop, clipRight + 10, clipBottom);
145 canvas.translate(getTotalPaddingLeft(), getTotalPaddingTop());
146
147 try {
148 mentionRendererDelegate.draw(canvas, getText(), getLayout());
149 if (spoilerRendererDelegate != null) {
150 spoilerRendererDelegate.draw(canvas, getText(), getLayout());
151 }
152 } finally {
153 canvas.restoreToCount(checkpoint);
154 }
155 }
156 super.onDraw(canvas);
157 }
158
159 private CharSequence ellipsizeToWidth(CharSequence text) {
160 return TextUtils.ellipsize(text,
161 getPaint(),
162 getWidth() - getPaddingLeft() - getPaddingRight(),
163 TruncateAt.END);
164 }
165
166 public void setHint(@NonNull String hint) {
167 this.hint = hint;
168 setHintWithChecks(ellipsizeToWidth(this.hint));
169 }
170
171 public void setDraftText(@Nullable CharSequence draftText) {
172 setText("");
173
174 if (draftText != null) {
175 append(draftText);
176 }
177 }
178
179 public void appendInvite(String invite) {
180 if (getText() == null) {
181 return;
182 }
183
184 if (!TextUtils.isEmpty(getText()) && !getText().toString().equals(" ")) {
185 append(" ");
186 }
187
188 append(invite);
189 setSelection(getText().length());
190 }
191
192 public void setCursorPositionChangedListener(@Nullable CursorPositionChangedListener listener) {
193 this.cursorPositionChangedListener = listener;
194 }
195
196 public void setInlineQueryChangedListener(@Nullable InlineQueryChangedListener listener) {
197 this.inlineQueryChangedListener = listener;
198 }
199
200 public void setMentionValidator(@Nullable MentionValidatorWatcher.MentionValidator mentionValidator) {
201 mentionValidatorWatcher.setMentionValidator(mentionValidator);
202 }
203
204 public void setStylingChangedListener(@Nullable StylingChangedListener listener) {
205 stylingChangedListener = listener;
206 }
207
208 private boolean isLandscape() {
209 return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
210 }
211
212 public void setMessageSendType(MessageSendType messageSendType) {
213 final boolean useSystemEmoji = SignalStore.settings().isPreferSystemEmoji();
214
215 int imeOptions = (getImeOptions() & ~EditorInfo.IME_MASK_ACTION) | EditorInfo.IME_ACTION_SEND;
216 int inputType = getInputType();
217
218 if (isLandscape()) setImeActionLabel(getContext().getString(messageSendType.getComposeHintRes()), EditorInfo.IME_ACTION_SEND);
219 else setImeActionLabel(null, 0);
220
221 if (useSystemEmoji) {
222 inputType = (inputType & ~InputType.TYPE_MASK_VARIATION) | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE;
223 }
224
225 setImeOptions(imeOptions);
226 setHint(getContext().getString(messageSendType.getComposeHintRes()));
227 setInputType(inputType);
228 }
229
230 @Override
231 public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
232 InputConnection inputConnection = super.onCreateInputConnection(editorInfo);
233
234 if (SignalStore.settings().isEnterKeySends()) {
235 editorInfo.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
236 }
237
238 if (mediaListener == null) {
239 return inputConnection;
240 }
241
242 if (inputConnection == null) {
243 return null;
244 }
245
246 EditorInfoCompat.setContentMimeTypes(editorInfo, new String[] { "image/jpeg", "image/png", "image/gif" });
247 return InputConnectionCompat.createWrapper(inputConnection, editorInfo, new CommitContentListener(mediaListener));
248 }
249
250 public void setMediaListener(@Nullable InputPanel.MediaListener mediaListener) {
251 this.mediaListener = mediaListener;
252 }
253
254 public boolean hasMentions() {
255 Editable text = getText();
256 if (text != null) {
257 return !MentionAnnotation.getMentionAnnotations(text).isEmpty();
258 }
259 return false;
260 }
261
262 public @NonNull List<Mention> getMentions() {
263 return MentionAnnotation.getMentionsFromAnnotations(getText());
264 }
265
266 public boolean hasStyling() {
267 CharSequence trimmed = getTextTrimmed();
268 return (trimmed instanceof Spanned) && MessageStyler.hasStyling((Spanned) trimmed);
269 }
270
271 public @Nullable BodyRangeList getStyling() {
272 return MessageStyler.getStyling(getTextTrimmed());
273 }
274
275 private void initialize() {
276 if (TextSecurePreferences.isIncognitoKeyboardEnabled(getContext())) {
277 setImeOptions(getImeOptions() | 16777216);
278 }
279
280 mentionRendererDelegate = new MentionRendererDelegate(getContext(), ContextCompat.getColor(getContext(), R.color.conversation_mention_background_color));
281
282 addTextChangedListener(new MentionDeleter());
283 mentionValidatorWatcher = new MentionValidatorWatcher();
284 addTextChangedListener(mentionValidatorWatcher);
285
286 spoilerRendererDelegate = new SpoilerRendererDelegate(this, true);
287
288 addTextChangedListener(new ComposeTextStyleWatcher());
289
290 setCustomSelectionActionModeCallback(new ActionMode.Callback() {
291 @Override
292 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
293 MenuItem copy = menu.findItem(android.R.id.copy);
294 MenuItem cut = menu.findItem(android.R.id.cut);
295 MenuItem paste = menu.findItem(android.R.id.paste);
296 int copyOrder = copy != null ? copy.getOrder() : 0;
297 int cutOrder = cut != null ? cut.getOrder() : 0;
298 int pasteOrder = paste != null ? paste.getOrder() : 0;
299 int largestOrder = Math.max(copyOrder, Math.max(cutOrder, pasteOrder));
300
301 menu.add(0, R.id.edittext_bold, largestOrder, getContext().getString(R.string.TextFormatting_bold));
302 menu.add(0, R.id.edittext_italic, largestOrder, getContext().getString(R.string.TextFormatting_italic));
303 menu.add(0, R.id.edittext_strikethrough, largestOrder, getContext().getString(R.string.TextFormatting_strikethrough));
304 menu.add(0, R.id.edittext_monospace, largestOrder, getContext().getString(R.string.TextFormatting_monospace));
305 menu.add(0, R.id.edittext_spoiler, largestOrder, getContext().getString(R.string.TextFormatting_spoiler));
306
307 Editable text = getText();
308
309 if (text != null) {
310 int start = getSelectionStart();
311 int end = getSelectionEnd();
312 if (MessageStyler.hasStyling(text, start, end)) {
313 menu.add(0, R.id.edittext_clear_formatting, largestOrder, getContext().getString(R.string.TextFormatting_clear_formatting));
314 }
315 }
316
317 return true;
318 }
319
320 @Override
321 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
322 boolean handled = handleFormatText(item.getItemId());
323 if (handled) {
324 mode.finish();
325 }
326 return handled;
327 }
328
329 @Override
330 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
331 return false;
332 }
333
334 @Override
335 public void onDestroyActionMode(ActionMode mode) {}
336 });
337 }
338
339 private void setHintWithChecks(@Nullable CharSequence newHint) {
340 if (getLayout() == null || Objects.equals(getHint(), newHint)) {
341 return;
342 }
343
344 setHint(newHint);
345 }
346
347 private boolean changeSelectionForPartialMentions(@NonNull Spanned spanned, int selectionStart, int selectionEnd) {
348 Annotation[] annotations = spanned.getSpans(0, spanned.length(), Annotation.class);
349 for (Annotation annotation : annotations) {
350 if (MentionAnnotation.isMentionAnnotation(annotation)) {
351 int spanStart = spanned.getSpanStart(annotation);
352 int spanEnd = spanned.getSpanEnd(annotation);
353
354 boolean startInMention = selectionStart > spanStart && selectionStart < spanEnd;
355 boolean endInMention = selectionEnd > spanStart && selectionEnd < spanEnd;
356
357 if (startInMention || endInMention) {
358 if (selectionStart == selectionEnd) {
359 setSelection(spanEnd, spanEnd);
360 } else {
361 int newStart = startInMention ? spanStart : selectionStart;
362 int newEnd = endInMention ? spanEnd : selectionEnd;
363 setSelection(newStart, newEnd);
364 }
365 return true;
366 }
367 }
368 }
369 return false;
370 }
371
372 private void doAfterCursorChange(@NonNull Editable text) {
373 if (enoughToFilter(text, false)) {
374 performFiltering(text, false);
375 } else {
376 clearInlineQuery();
377 }
378 }
379
380 private void performFiltering(@NonNull Editable text, boolean keywordEmojiSearch) {
381 int end = getSelectionEnd();
382 QueryStart queryStart = findQueryStart(text, end, keywordEmojiSearch);
383 int start = queryStart.index;
384 String query = text.subSequence(start, end).toString();
385
386 if (inlineQueryChangedListener != null) {
387 if (queryStart.isMentionQuery) {
388 inlineQueryChangedListener.onQueryChanged(new InlineQuery.Mention(query));
389 } else {
390 inlineQueryChangedListener.onQueryChanged(new InlineQuery.Emoji(query, keywordEmojiSearch));
391 }
392 }
393 }
394
395 private void clearInlineQuery() {
396 if (inlineQueryChangedListener != null) {
397 inlineQueryChangedListener.clearQuery();
398 }
399 }
400
401 private boolean enoughToFilter(@NonNull Editable text, boolean keywordEmojiSearch) {
402 int end = getSelectionEnd();
403 if (end < 0) {
404 return false;
405 }
406 return findQueryStart(text, end, keywordEmojiSearch).index != -1;
407 }
408
409 public void replaceTextWithMention(@NonNull String displayName, @NonNull RecipientId recipientId) {
410 replaceText(createReplacementToken(displayName, recipientId), false);
411 }
412
413 public void replaceText(@NonNull InlineQueryReplacement replacement) {
414 replaceText(replacement.toCharSequence(getContext()), replacement.isKeywordSearch());
415 }
416
417 private void replaceText(@NonNull CharSequence replacement, boolean keywordReplacement) {
418 Editable text = getText();
419 if (text == null) {
420 return;
421 }
422
423 clearComposingText();
424
425 int end = getSelectionEnd();
426 int start = findQueryStart(text, end, keywordReplacement).index - (keywordReplacement ? 0 : 1);
427
428 text.replace(start, end, "");
429 text.insert(start, replacement);
430 }
431
432 private @NonNull CharSequence createReplacementToken(@NonNull CharSequence text, @NonNull RecipientId recipientId) {
433 SpannableStringBuilder builder = new SpannableStringBuilder().append(MENTION_STARTER);
434 if (text instanceof Spanned) {
435 SpannableString spannableString = new SpannableString(text + " ");
436 TextUtils.copySpansFrom((Spanned) text, 0, text.length(), Object.class, spannableString, 0);
437 builder.append(spannableString);
438 } else {
439 builder.append(text).append(" ");
440 }
441
442 builder.setSpan(MentionAnnotation.mentionAnnotationForRecipientId(recipientId), 0, builder.length() - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
443
444 return builder;
445 }
446
447 private QueryStart findQueryStart(@NonNull CharSequence text, int inputCursorPosition, boolean keywordEmojiSearch) {
448 if (keywordEmojiSearch) {
449 int start = findQueryStart(text, inputCursorPosition, ' ');
450 if (start == -1 && inputCursorPosition != 0) {
451 start = 0;
452 } else if (start == inputCursorPosition) {
453 start = -1;
454 }
455 return new QueryStart(start, false);
456 }
457
458 QueryStart queryStart = new QueryStart(findQueryStart(text, inputCursorPosition, MENTION_STARTER), true);
459
460 if (queryStart.index < 0) {
461 queryStart = new QueryStart(findQueryStart(text, inputCursorPosition, EMOJI_STARTER), false);
462 }
463
464 return queryStart;
465 }
466
467 private int findQueryStart(@NonNull CharSequence text, int inputCursorPosition, char starter) {
468 if (inputCursorPosition == 0) {
469 return -1;
470 }
471
472 int delimiterSearchIndex = inputCursorPosition - 1;
473 while (delimiterSearchIndex >= 0 && (text.charAt(delimiterSearchIndex) != starter && !Character.isWhitespace(text.charAt(delimiterSearchIndex)))) {
474 delimiterSearchIndex--;
475 }
476
477 if (delimiterSearchIndex >= 0 && text.charAt(delimiterSearchIndex) == starter) {
478 if (couldBeTimeEntry(text, delimiterSearchIndex)) {
479 return -1;
480 } else {
481 return delimiterSearchIndex + 1;
482 }
483 }
484 return -1;
485 }
486
487 @Override
488 protected boolean shouldPersistSignalStylingWhenPasting() {
489 return true;
490 }
491
492 /**
493 * Return true if we think the user may be inputting a time.
494 */
495 private static boolean couldBeTimeEntry(@NonNull CharSequence text, int startIndex) {
496 if (startIndex <= 0 || startIndex + 1 >= text.length()) {
497 return false;
498 }
499
500 int startOfToken = startIndex;
501 while (startOfToken > 0 && !Character.isWhitespace(text.charAt(startOfToken))) {
502 startOfToken--;
503 }
504 startOfToken++;
505
506 int endOfToken = startIndex;
507 while (endOfToken < text.length() && !Character.isWhitespace(text.charAt(endOfToken))) {
508 endOfToken++;
509 }
510
511 return TIME_PATTERN.matcher(text.subSequence(startOfToken, endOfToken)).find();
512 }
513
514 public boolean isTextHighlighted() {
515 return getText() != null && getSelectionStart() < getSelectionEnd();
516 }
517
518 public boolean handleFormatText(@IdRes int id) {
519 Editable text = getText();
520
521 if (text == null) {
522 return false;
523 }
524
525 if (id != R.id.edittext_bold &&
526 id != R.id.edittext_italic &&
527 id != R.id.edittext_strikethrough &&
528 id != R.id.edittext_monospace &&
529 id != R.id.edittext_spoiler &&
530 id != R.id.edittext_clear_formatting)
531 {
532 return false;
533 }
534
535 int start = getSelectionStart();
536 int end = getSelectionEnd();
537 BodyRangeList.BodyRange.Style style = null;
538
539 if (id == R.id.edittext_bold) {
540 style = BodyRangeList.BodyRange.Style.BOLD;
541 } else if (id == R.id.edittext_italic) {
542 style = BodyRangeList.BodyRange.Style.ITALIC;
543 } else if (id == R.id.edittext_strikethrough) {
544 style = BodyRangeList.BodyRange.Style.STRIKETHROUGH;
545 } else if (id == R.id.edittext_monospace) {
546 style = BodyRangeList.BodyRange.Style.MONOSPACE;
547 } else if (id == R.id.edittext_spoiler) {
548 style = BodyRangeList.BodyRange.Style.SPOILER;
549 }
550
551 clearComposingText();
552
553 if (style != null) {
554 MessageStyler.toggleStyle(style, text, start, end);
555 } else {
556 MessageStyler.clearStyling(text, start, end);
557 }
558
559 Selection.setSelection(getText(), end);
560
561 if (stylingChangedListener != null) {
562 stylingChangedListener.onStylingChanged();
563 }
564
565 return true;
566 }
567
568 private static class CommitContentListener implements InputConnectionCompat.OnCommitContentListener {
569
570 private static final String TAG = Log.tag(CommitContentListener.class);
571
572 private final InputPanel.MediaListener mediaListener;
573
574 private CommitContentListener(@NonNull InputPanel.MediaListener mediaListener) {
575 this.mediaListener = mediaListener;
576 }
577
578 @Override
579 public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts) {
580 if (Build.VERSION.SDK_INT >= 25 && (flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
581 try {
582 inputContentInfo.requestPermission();
583 } catch (Exception e) {
584 Log.w(TAG, e);
585 return false;
586 }
587 }
588
589 if (inputContentInfo.getDescription().getMimeTypeCount() > 0) {
590 mediaListener.onMediaSelected(inputContentInfo.getContentUri(),
591 inputContentInfo.getDescription().getMimeType(0));
592
593 return true;
594 }
595
596 return false;
597 }
598 }
599
600 private static class QueryStart {
601 public int index;
602 public boolean isMentionQuery;
603
604 public QueryStart(int index, boolean isMentionQuery) {
605 this.index = index;
606 this.isMentionQuery = isMentionQuery;
607 }
608 }
609
610 public interface CursorPositionChangedListener {
611 void onCursorPositionChanged(int start, int end);
612 }
613
614 public interface StylingChangedListener {
615 void onStylingChanged();
616 }
617}