That fuck shit the fascists are using
at master 617 lines 21 kB view raw
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}