react-native-enriched-markdown-scaffold 1.0.2-scaffold.8 → 1.0.2-scaffold.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -270,6 +270,8 @@ dependencies {
270
270
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
271
271
  // Baseline Profile installer for AOT compilation hints
272
272
  implementation "androidx.profileinstaller:profileinstaller:1.3.1"
273
+ // AppCompatTextView draws emoji through EmojiCompat; measurement must apply it too
274
+ implementation "androidx.emoji2:emoji2:1.4.0"
273
275
  // LaTeX math rendering (optional — set enrichedMarkdown.enableMath=false in gradle.properties to exclude)
274
276
  if (enableMath) {
275
277
  implementation("io.github.erweixin:ratex-android:0.1.10")
@@ -21,6 +21,7 @@ import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForCheckboxT
21
21
  import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForLinkTap
22
22
  import com.swmansion.enriched.markdown.utils.text.view.clearTextSelection
23
23
  import com.swmansion.enriched.markdown.utils.text.view.createSelectionActionModeCallback
24
+ import com.swmansion.enriched.markdown.utils.text.view.isRomSelectionPopupShower
24
25
  import com.swmansion.enriched.markdown.utils.text.view.reallowParentInterceptIfLinkReleased
25
26
  import com.swmansion.enriched.markdown.utils.text.view.setupAsMarkdownTextView
26
27
  import com.swmansion.enriched.markdown.views.BlockSegmentView
@@ -140,6 +141,15 @@ class EnrichedMarkdownInternalText
140
141
  return super.startActionMode(callback, type)
141
142
  }
142
143
 
144
+ // See isRomSelectionPopupShower: MIUI shows its own selection popup this way.
145
+ override fun postDelayed(
146
+ action: Runnable?,
147
+ delayMillis: Long,
148
+ ): Boolean {
149
+ if (contextMenuHidden && isRomSelectionPopupShower(action)) return false
150
+ return super.postDelayed(action, delayMillis)
151
+ }
152
+
143
153
  /** Clears the selection and ends its ActionMode. */
144
154
  fun clearSelection() {
145
155
  clearTextSelection()
@@ -44,6 +44,7 @@ import com.swmansion.enriched.markdown.utils.text.view.createSelectionActionMode
44
44
  import com.swmansion.enriched.markdown.utils.text.view.currentTextSelection
45
45
  import com.swmansion.enriched.markdown.utils.text.view.emitLinkLongPressEvent
46
46
  import com.swmansion.enriched.markdown.utils.text.view.emitLinkPressEvent
47
+ import com.swmansion.enriched.markdown.utils.text.view.isRomSelectionPopupShower
47
48
  import com.swmansion.enriched.markdown.utils.text.view.reallowParentInterceptIfLinkReleased
48
49
  import com.swmansion.enriched.markdown.utils.text.view.setupAsMarkdownTextView
49
50
  import java.util.concurrent.Executors
@@ -348,6 +349,15 @@ class EnrichedMarkdownText
348
349
  return super.startActionMode(callback, type)
349
350
  }
350
351
 
352
+ // See isRomSelectionPopupShower: MIUI shows its own selection popup this way.
353
+ override fun postDelayed(
354
+ action: Runnable?,
355
+ delayMillis: Long,
356
+ ): Boolean {
357
+ if (contextMenuHidden && isRomSelectionPopupShower(action)) return false
358
+ return super.postDelayed(action, delayMillis)
359
+ }
360
+
351
361
  /** Clears the selection (ending its ActionMode) and reports `active: false`. */
352
362
  fun clearSelection() {
353
363
  clearTextSelection()
@@ -7,6 +7,7 @@ import android.text.SpannableString
7
7
  import android.text.StaticLayout
8
8
  import android.text.TextPaint
9
9
  import android.util.Log
10
+ import androidx.emoji2.text.EmojiCompat
10
11
  import com.facebook.react.bridge.ReadableMap
11
12
  import com.facebook.react.uimanager.PixelUtil
12
13
  import com.facebook.yoga.YogaMeasureMode
@@ -92,6 +93,38 @@ object MeasurementStore {
92
93
  }
93
94
 
94
95
  private val measurePaint = TextPaint()
96
+
97
+ @Volatile
98
+ private var emojiInitCallbackRegistered = false
99
+
100
+ // A view measured before EmojiCompat loaded keeps a size without its emoji glyphs.
101
+ private val emojiInitCallback =
102
+ object : EmojiCompat.InitCallback() {
103
+ override fun onInitialized() {
104
+ data.clear()
105
+ }
106
+ }
107
+
108
+ // AppCompatTextView draws emoji the system font lacks through EmojiCompat, which can be
109
+ // wider than the fallback glyph and wrap a line earlier; measure the same glyphs.
110
+ private fun withEmojiSpans(text: CharSequence): CharSequence {
111
+ if (text.isEmpty() || !EmojiCompat.isConfigured()) return text
112
+ val emojiCompat = EmojiCompat.get()
113
+ if (emojiCompat.loadState != EmojiCompat.LOAD_STATE_SUCCEEDED) {
114
+ if (!emojiInitCallbackRegistered) {
115
+ emojiInitCallbackRegistered = true
116
+ emojiCompat.registerInitCallback(emojiInitCallback)
117
+ }
118
+ return text
119
+ }
120
+ return try {
121
+ emojiCompat.process(SpannableString(text)) ?: text
122
+ } catch (e: IllegalStateException) {
123
+ Log.w(TAG, "EmojiCompat processing failed, measuring raw text", e)
124
+ text
125
+ }
126
+ }
127
+
95
128
  private val measureRenderer = Renderer()
96
129
 
97
130
  @Volatile
@@ -550,9 +583,10 @@ object MeasurementStore {
550
583
  viewId: Int?,
551
584
  ): StaticLayout {
552
585
  measurePaint.textSize = fontSize
553
- prepareImageSpansForMeasurement(text, widthPx)
586
+ val content = withEmojiSpans(text)
587
+ prepareImageSpansForMeasurement(content, widthPx)
554
588
  return StaticLayout.Builder
555
- .obtain(text, 0, text.length, measurePaint, widthPx)
589
+ .obtain(content, 0, content.length, measurePaint, widthPx)
556
590
  .setIncludePad(false)
557
591
  .setLineSpacing(0f, 1f)
558
592
  .apply {
@@ -632,7 +666,7 @@ object MeasurementStore {
632
666
  paint: TextPaint,
633
667
  viewId: Int?,
634
668
  ): Long {
635
- val content = text ?: ""
669
+ val content = withEmojiSpans(text ?: "")
636
670
  val safeWidth = ceil(maxWidth).toInt().coerceAtLeast(1)
637
671
  prepareImageSpansForMeasurement(content, safeWidth)
638
672
 
@@ -672,7 +706,7 @@ object MeasurementStore {
672
706
  paint: TextPaint,
673
707
  viewId: Int?,
674
708
  ): Pair<Long, StaticLayout> {
675
- val content = text ?: ""
709
+ val content = withEmojiSpans(text ?: "")
676
710
  val widthPx = ceil(maxWidth).toInt().coerceAtLeast(1)
677
711
  prepareImageSpansForMeasurement(content, widthPx)
678
712
 
@@ -74,3 +74,15 @@ internal class HeadlessActionMode private constructor(
74
74
  }
75
75
  }
76
76
  }
77
+
78
+ /**
79
+ * MIUI 12 (Android 10) replaces android.widget.Editor: text selection never reaches
80
+ * startActionMode or the custom ActionMode callback, so HeadlessActionMode is never used.
81
+ * Its own popup is shown by Editor$HandleView.showActionPopupWindow, which posts a runnable
82
+ * on the TextView (Editor$HandleView$2). Dropping that runnable is the only hook left while
83
+ * `contextMenuHidden` is on.
84
+ */
85
+ private const val ROM_SELECTION_POPUP_SHOWER_PREFIX = "android.widget.Editor\$HandleView"
86
+
87
+ internal fun isRomSelectionPopupShower(action: Runnable?): Boolean =
88
+ action != null && action.javaClass.name.startsWith(ROM_SELECTION_POPUP_SHOWER_PREFIX)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-enriched-markdown-scaffold",
3
- "version": "1.0.2-scaffold.8",
3
+ "version": "1.0.2-scaffold.9",
4
4
  "description": "Markdown-based Rich Text solution for React Native",
5
5
  "main": "./lib/module/index",
6
6
  "module": "./lib/module/index",