@apollohg/react-native-prose-editor 0.5.17 → 0.5.19

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.
@@ -390,6 +390,11 @@ class NativeEditorExpoView(
390
390
  val mentionQuery: String? = null
391
391
  )
392
392
 
393
+ private data class PendingEditorUpdateEvent(
394
+ val editorId: Long,
395
+ val updateJSON: String
396
+ )
397
+
393
398
  val richTextView: RichTextEditorView = RichTextEditorView(context)
394
399
  private val keyboardToolbarView = EditorKeyboardToolbarView(context)
395
400
  private val mainHandler = Handler(Looper.getMainLooper())
@@ -478,6 +483,9 @@ class NativeEditorExpoView(
478
483
  private var pendingNativeActionRetryGeneration = 0
479
484
  private var pendingNativeActionRetryAttempts = 0
480
485
  private var lastReadyEditorId: Long? = null
486
+ private val pendingEditorUpdateEvents = java.util.ArrayDeque<PendingEditorUpdateEvent>()
487
+ private var pendingEditorUpdateDispatchGeneration = 0
488
+ private var pendingEditorUpdateDispatchScheduled = false
481
489
 
482
490
  init {
483
491
  addView(richTextView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
@@ -1737,6 +1745,7 @@ class NativeEditorExpoView(
1737
1745
  isApplyingJSUpdate = true
1738
1746
  return try {
1739
1747
  richTextView.editorEditText.applyUpdateJSON(updateJson)
1748
+ clearPendingEditorUpdateDispatchQueue("jsUpdate")
1740
1749
  true
1741
1750
  } catch (error: Throwable) {
1742
1751
  Log.w(LOG_TAG, "Failed to apply JS editor update", error)
@@ -1812,27 +1821,114 @@ class NativeEditorExpoView(
1812
1821
  }
1813
1822
 
1814
1823
  override fun onEditorUpdate(updateJSON: String) {
1824
+ if (isApplyingJSUpdate) {
1825
+ dispatchEditorUpdate(
1826
+ PendingEditorUpdateEvent(
1827
+ editorId = richTextView.editorId,
1828
+ updateJSON = updateJSON
1829
+ ),
1830
+ emitToJS = false
1831
+ )
1832
+ return
1833
+ }
1834
+ pendingEditorUpdateEvents.addLast(
1835
+ PendingEditorUpdateEvent(
1836
+ editorId = richTextView.editorId,
1837
+ updateJSON = updateJSON
1838
+ )
1839
+ )
1840
+ richTextView.editorEditText.recordImeTraceForTesting(
1841
+ "nativeViewEditorUpdateQueued",
1842
+ "queue=${pendingEditorUpdateEvents.size} jsonLength=${updateJSON.length}"
1843
+ )
1844
+ schedulePendingEditorUpdateDispatch()
1845
+ }
1846
+
1847
+ internal fun pendingEditorUpdateEventCountForTesting(): Int =
1848
+ pendingEditorUpdateEvents.size
1849
+
1850
+ private fun schedulePendingEditorUpdateDispatch() {
1851
+ pendingEditorUpdateDispatchScheduled = true
1852
+ val generation = ++pendingEditorUpdateDispatchGeneration
1853
+ mainHandler.postDelayed({
1854
+ if (generation != pendingEditorUpdateDispatchGeneration) return@postDelayed
1855
+ pendingEditorUpdateDispatchScheduled = false
1856
+ drainPendingEditorUpdateEvents()
1857
+ }, EDITOR_UPDATE_EVENT_DEBOUNCE_MS)
1858
+ }
1859
+
1860
+ private fun drainPendingEditorUpdateEvents() {
1861
+ if (pendingEditorUpdateEvents.isEmpty()) return
1862
+ val startedAt = System.nanoTime()
1863
+ var drainedCount = 0
1864
+ while (pendingEditorUpdateEvents.isNotEmpty()) {
1865
+ val event = pendingEditorUpdateEvents.removeFirst()
1866
+ if (event.editorId != richTextView.editorId) {
1867
+ richTextView.editorEditText.recordImeTraceForTesting(
1868
+ "nativeViewEditorUpdateSkipped",
1869
+ "reason=staleEditor queuedEditor=${event.editorId} currentEditor=${richTextView.editorId}"
1870
+ )
1871
+ continue
1872
+ }
1873
+ dispatchEditorUpdate(event, emitToJS = true)
1874
+ drainedCount += 1
1875
+ }
1876
+ richTextView.editorEditText.recordImeTraceForTesting(
1877
+ "nativeViewEditorUpdateDrained",
1878
+ "count=$drainedCount totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
1879
+ )
1880
+ }
1881
+
1882
+ private fun clearPendingEditorUpdateDispatchQueue(reason: String) {
1883
+ if (pendingEditorUpdateEvents.isEmpty() && !pendingEditorUpdateDispatchScheduled) return
1884
+ val clearedCount = pendingEditorUpdateEvents.size
1885
+ pendingEditorUpdateEvents.clear()
1886
+ pendingEditorUpdateDispatchScheduled = false
1887
+ pendingEditorUpdateDispatchGeneration += 1
1888
+ richTextView.editorEditText.recordImeTraceForTesting(
1889
+ "nativeViewEditorUpdateQueueCleared",
1890
+ "reason=$reason count=$clearedCount"
1891
+ )
1892
+ }
1893
+
1894
+ private fun dispatchEditorUpdate(event: PendingEditorUpdateEvent, emitToJS: Boolean) {
1895
+ val updateJSON = event.updateJSON
1896
+ val startedAt = System.nanoTime()
1815
1897
  noteDocumentVersionFromUpdateJSON(updateJSON)
1898
+ val noteNanos = System.nanoTime() - startedAt
1899
+ val toolbarStartedAt = System.nanoTime()
1816
1900
  NativeToolbarState.fromUpdateJson(updateJSON)?.let { state ->
1817
1901
  toolbarState = state
1818
1902
  keyboardToolbarView.applyState(state)
1819
1903
  }
1904
+ val toolbarNanos = System.nanoTime() - toolbarStartedAt
1905
+ val mentionStartedAt = System.nanoTime()
1820
1906
  refreshMentionQuery()
1907
+ val mentionNanos = System.nanoTime() - mentionStartedAt
1908
+ val retryStartedAt = System.nanoTime()
1821
1909
  clearPendingNativeActionRetryIfScopeChanged()
1822
1910
  schedulePendingPreflightWake()
1823
1911
  richTextView.refreshRemoteSelections()
1912
+ val retryNanos = System.nanoTime() - retryStartedAt
1824
1913
  if (heightBehavior == EditorHeightBehavior.AUTO_GROW) {
1825
1914
  post {
1826
1915
  requestLayout()
1827
1916
  emitContentHeightIfNeeded(force = false)
1828
1917
  }
1829
1918
  }
1830
- if (isApplyingJSUpdate) return
1831
- val event = mapOf<String, Any>(
1832
- "updateJson" to updateJSON,
1833
- "editorId" to richTextView.editorId
1919
+ val emitStartedAt = System.nanoTime()
1920
+ if (emitToJS) {
1921
+ val payload = mapOf<String, Any>(
1922
+ "updateJson" to updateJSON,
1923
+ "editorId" to event.editorId
1924
+ )
1925
+ onEditorUpdate(payload)
1926
+ }
1927
+ val totalNanos = System.nanoTime() - startedAt
1928
+ richTextView.editorEditText.recordImeTraceForTesting(
1929
+ "nativeViewEditorUpdateDispatch",
1930
+ "emitToJS=$emitToJS jsonLength=${updateJSON.length} noteUs=${nanosToMicros(noteNanos)} toolbarUs=${nanosToMicros(toolbarNanos)} mentionUs=${nanosToMicros(mentionNanos)} retryUs=${nanosToMicros(retryNanos)} emitUs=${nanosToMicros(System.nanoTime() - emitStartedAt)} totalUs=${nanosToMicros(totalNanos)}"
1834
1931
  )
1835
- onEditorUpdate(event)
1836
1932
  }
1837
1933
 
1838
1934
  private fun installOutsideTapBlurHandlerIfNeeded() {
@@ -2082,10 +2178,13 @@ class NativeEditorExpoView(
2082
2178
  private const val TOOLBAR_FOCUS_PRESERVE_MS = 750L
2083
2179
  private const val OUTSIDE_TAP_BLUR_DELAY_MS = 100L
2084
2180
  private const val NATIVE_ACTION_RETRY_DELAY_MS = 16L
2181
+ private const val EDITOR_UPDATE_EVENT_DEBOUNCE_MS = 64L
2085
2182
  private const val PENDING_UPDATE_RECOVERY_RETRY_DELAY_MS = 250L
2086
2183
  private const val MAX_NATIVE_ACTION_RETRY_ATTEMPTS = 3
2087
2184
  private const val MAX_PENDING_UPDATE_RETRY_ATTEMPTS = 5
2088
2185
  private const val LOG_TAG = "NativeEditor"
2186
+
2187
+ private fun nanosToMicros(nanos: Long): Long = nanos / 1_000L
2089
2188
  }
2090
2189
 
2091
2190
  private fun resolveActivity(context: Context): Activity? {
@@ -567,6 +567,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
567
567
  const pendingControlledSyncAfterNativeUpdateRef = (0, react_1.useRef)(false);
568
568
  const pendingBlockedNativeCommandRetryRef = (0, react_1.useRef)(false);
569
569
  const pendingNativeCommandRetryRef = (0, react_1.useRef)(null);
570
+ const pendingBridgeRecreationContentRef = (0, react_1.useRef)(null);
570
571
  const blockedNativeCommandRetryTimerRef = (0, react_1.useRef)(null);
571
572
  const [autoGrowHeight, setAutoGrowHeight] = (0, react_1.useState)(null);
572
573
  // Toolbar state from EditorUpdate events
@@ -1039,6 +1040,10 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1039
1040
  }
1040
1041
  }, [syncNativeUpdateFromBridge]);
1041
1042
  (0, react_1.useEffect)(() => {
1043
+ const preservedUncontrolledContent = value == null && serializedValueJson == null
1044
+ ? pendingBridgeRecreationContentRef.current
1045
+ : null;
1046
+ pendingBridgeRecreationContentRef.current = null;
1042
1047
  const bridgeConfig = maxLength != null || serializedSchemaJson || allowBase64Images
1043
1048
  ? {
1044
1049
  maxLength,
@@ -1056,18 +1061,48 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1056
1061
  else if (serializedValueJson != null) {
1057
1062
  bridge.setJsonString(serializedValueJson);
1058
1063
  }
1064
+ else if (preservedUncontrolledContent != null) {
1065
+ bridge.setJsonString(preservedUncontrolledContent.jsonString);
1066
+ }
1059
1067
  else if (serializedInitialJson != null) {
1060
1068
  bridge.setJsonString(serializedInitialJson);
1061
1069
  }
1062
1070
  else if (initialContent) {
1063
1071
  bridge.setHtml(initialContent);
1064
1072
  }
1073
+ if (preservedUncontrolledContent != null) {
1074
+ const preservedSelection = preservedUncontrolledContent.selection;
1075
+ if (preservedSelection.type === 'text') {
1076
+ const anchor = preservedSelection.anchor ?? 0;
1077
+ const head = preservedSelection.head ?? anchor;
1078
+ bridge.setSelection(anchor, head);
1079
+ }
1080
+ else if (preservedSelection.type === 'node' &&
1081
+ typeof preservedSelection.pos === 'number') {
1082
+ bridge.setSelection(preservedSelection.pos, preservedSelection.pos);
1083
+ }
1084
+ }
1065
1085
  syncStateFromUpdate(bridge.getCurrentState());
1066
1086
  setIsReady(true);
1067
1087
  return () => {
1088
+ if (bridgeRef.current === bridge &&
1089
+ value == null &&
1090
+ serializedValueJson == null &&
1091
+ !bridge.isDestroyed) {
1092
+ try {
1093
+ pendingBridgeRecreationContentRef.current = {
1094
+ jsonString: bridge.getJsonString(),
1095
+ selection: selectionRef.current,
1096
+ };
1097
+ }
1098
+ catch {
1099
+ pendingBridgeRecreationContentRef.current = null;
1100
+ }
1101
+ }
1068
1102
  bridge.destroy();
1069
- bridgeRef.current = null;
1070
- nativeViewRef.current = null;
1103
+ if (bridgeRef.current === bridge) {
1104
+ bridgeRef.current = null;
1105
+ }
1071
1106
  pendingNativeUpdateInFlightRef.current = null;
1072
1107
  pendingDetachedControlledSyncRef.current = false;
1073
1108
  pendingControlledSyncAfterNativeUpdateRef.current = false;
@@ -131,6 +131,9 @@ func resolveMentionQueryState(
131
131
  }
132
132
 
133
133
  final class MentionSuggestionChipButton: UIButton {
134
+ private static let horizontalContentInset: CGFloat = 8
135
+ private static let verticalContentInset: CGFloat = 8
136
+
134
137
  private let titleLabelView = UILabel()
135
138
  private let subtitleLabelView = UILabel()
136
139
  private let stackView = UIStackView()
@@ -180,10 +183,10 @@ final class MentionSuggestionChipButton: UIButton {
180
183
  addSubview(stackView)
181
184
 
182
185
  NSLayoutConstraint.activate([
183
- stackView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
184
- stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
185
- stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
186
- stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
186
+ stackView.topAnchor.constraint(equalTo: topAnchor, constant: Self.verticalContentInset),
187
+ stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Self.horizontalContentInset),
188
+ stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Self.horizontalContentInset),
189
+ stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Self.verticalContentInset),
187
190
  heightAnchor.constraint(greaterThanOrEqualToConstant: 40),
188
191
  ])
189
192
 
@@ -213,6 +216,13 @@ final class MentionSuggestionChipButton: UIButton {
213
216
  }
214
217
  }
215
218
 
219
+ override func tintColorDidChange() {
220
+ super.tintColorDidChange()
221
+ if toolbarAppearance == .native {
222
+ updateAppearance(highlighted: isHighlighted)
223
+ }
224
+ }
225
+
216
226
  @objc private func handleTouchDown() {
217
227
  updateAppearance(highlighted: true)
218
228
  }
@@ -226,14 +236,46 @@ final class MentionSuggestionChipButton: UIButton {
226
236
  layer.cornerRadius = 18
227
237
  layer.borderColor = UIColor.clear.cgColor
228
238
  layer.borderWidth = 0
239
+ #if compiler(>=6.2)
240
+ if #available(iOS 26.0, *) {
241
+ stackView.isHidden = true
242
+ backgroundColor = .clear
243
+ var configuration = highlighted
244
+ ? UIButton.Configuration.prominentGlass()
245
+ : UIButton.Configuration.glass()
246
+ configuration.cornerStyle = .capsule
247
+ configuration.contentInsets = NSDirectionalEdgeInsets(
248
+ top: Self.verticalContentInset,
249
+ leading: Self.horizontalContentInset,
250
+ bottom: Self.verticalContentInset,
251
+ trailing: Self.horizontalContentInset
252
+ )
253
+ configuration.title = suggestion.label
254
+ configuration.subtitle = suggestion.subtitle
255
+ configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
256
+ var outgoing = incoming
257
+ outgoing.font = .systemFont(ofSize: 14, weight: .semibold)
258
+ return outgoing
259
+ }
260
+ self.configuration = configuration
261
+ return
262
+ }
263
+ #endif
264
+ stackView.isHidden = false
229
265
  backgroundColor = highlighted
230
266
  ? UIColor.white.withAlphaComponent(0.18)
231
267
  : .clear
232
- titleLabelView.textColor = .label
233
- subtitleLabelView.textColor = .secondaryLabel
268
+ titleLabelView.textColor = tintColor
269
+ subtitleLabelView.textColor = tintColor.withAlphaComponent(0.72)
234
270
  return
235
271
  }
236
272
 
273
+ stackView.isHidden = false
274
+ if #available(iOS 15.0, *) {
275
+ var configuration = UIButton.Configuration.plain()
276
+ configuration.contentInsets = .zero
277
+ self.configuration = configuration
278
+ }
237
279
  backgroundColor = highlighted
238
280
  ? (theme?.optionHighlightedBackgroundColor ?? UIColor.systemBlue.withAlphaComponent(0.12))
239
281
  : (theme?.backgroundColor ?? UIColor.secondarySystemBackground)
@@ -252,4 +294,34 @@ final class MentionSuggestionChipButton: UIButton {
252
294
  func usesNativeAppearanceForTesting() -> Bool {
253
295
  toolbarAppearance == .native
254
296
  }
297
+
298
+ func titleTextColorForTesting() -> UIColor? {
299
+ titleLabelView.textColor
300
+ }
301
+
302
+ func subtitleTextColorForTesting() -> UIColor? {
303
+ subtitleLabelView.textColor
304
+ }
305
+
306
+ func usesNativeGlassTextRenderingForTesting() -> Bool {
307
+ #if compiler(>=6.2)
308
+ if #available(iOS 26.0, *) {
309
+ return toolbarAppearance == .native
310
+ && stackView.isHidden
311
+ && configuration?.title == suggestion.label
312
+ }
313
+ #endif
314
+ return false
315
+ }
316
+
317
+ func usesNativeGlassSemiboldTitleForTesting() -> Bool {
318
+ #if compiler(>=6.2)
319
+ if #available(iOS 26.0, *) {
320
+ return toolbarAppearance == .native
321
+ && stackView.isHidden
322
+ && configuration?.titleTextAttributesTransformer != nil
323
+ }
324
+ #endif
325
+ return false
326
+ }
255
327
  }
@@ -8,32 +8,32 @@
8
8
  <key>BinaryPath</key>
9
9
  <string>libeditor_core.a</string>
10
10
  <key>LibraryIdentifier</key>
11
- <string>ios-arm64</string>
11
+ <string>ios-arm64_x86_64-simulator</string>
12
12
  <key>LibraryPath</key>
13
13
  <string>libeditor_core.a</string>
14
14
  <key>SupportedArchitectures</key>
15
15
  <array>
16
16
  <string>arm64</string>
17
+ <string>x86_64</string>
17
18
  </array>
18
19
  <key>SupportedPlatform</key>
19
20
  <string>ios</string>
21
+ <key>SupportedPlatformVariant</key>
22
+ <string>simulator</string>
20
23
  </dict>
21
24
  <dict>
22
25
  <key>BinaryPath</key>
23
26
  <string>libeditor_core.a</string>
24
27
  <key>LibraryIdentifier</key>
25
- <string>ios-arm64_x86_64-simulator</string>
28
+ <string>ios-arm64</string>
26
29
  <key>LibraryPath</key>
27
30
  <string>libeditor_core.a</string>
28
31
  <key>SupportedArchitectures</key>
29
32
  <array>
30
33
  <string>arm64</string>
31
- <string>x86_64</string>
32
34
  </array>
33
35
  <key>SupportedPlatform</key>
34
36
  <string>ios</string>
35
- <key>SupportedPlatformVariant</key>
36
- <string>simulator</string>
37
37
  </dict>
38
38
  </array>
39
39
  <key>CFBundlePackageType</key>
@@ -691,6 +691,8 @@ final class EditorAccessoryToolbarView: UIInputView {
691
691
  private static let contentSpacing: CGFloat = 6
692
692
  private static let defaultHorizontalInset: CGFloat = 0
693
693
  private static let defaultKeyboardOffset: CGFloat = 0
694
+ private static let chromeTransitionDuration: TimeInterval = 0.18
695
+ private static let nativeDisabledButtonOpacity: CGFloat = 0.46
694
696
 
695
697
  private struct ButtonBinding {
696
698
  let item: NativeToolbarItem
@@ -727,6 +729,7 @@ final class EditorAccessoryToolbarView: UIInputView {
727
729
  private var currentState = NativeToolbarState.empty
728
730
  private var theme: EditorToolbarTheme?
729
731
  private var mentionTheme: EditorMentionTheme?
732
+ private var didAnimateChromeTransition = false
730
733
  fileprivate var onPressItem: ((NativeToolbarItem) -> Void)?
731
734
  var onSelectMentionSuggestion: ((NativeMentionSuggestion) -> Void)?
732
735
  var isShowingMentionSuggestions: Bool {
@@ -746,6 +749,16 @@ final class EditorAccessoryToolbarView: UIInputView {
746
749
  var chromeBorderWidthForTesting: CGFloat {
747
750
  chromeView.layer.borderWidth
748
751
  }
752
+ var nativeChromeIsTransparentForTesting: Bool {
753
+ blurView.isHidden
754
+ && glassTintView.isHidden
755
+ && chromeView.layer.borderWidth == 0
756
+ && chromeView.layer.shadowOpacity == 0
757
+ && (chromeView.backgroundColor ?? .clear) == .clear
758
+ }
759
+ var didAnimateChromeTransitionForTesting: Bool {
760
+ didAnimateChromeTransition
761
+ }
749
762
  var nativeToolbarVisibleWidthForTesting: CGFloat {
750
763
  activeNativeToolbarScrollViewForTesting.bounds.width
751
764
  }
@@ -849,15 +862,42 @@ final class EditorAccessoryToolbarView: UIInputView {
849
862
  func apply(mentionTheme: EditorMentionTheme?) {
850
863
  self.mentionTheme = mentionTheme
851
864
  for button in mentionButtons {
852
- button.apply(theme: mentionTheme)
865
+ button.apply(theme: mentionTheme, toolbarAppearance: resolvedAppearance)
853
866
  }
854
867
  }
855
868
 
856
869
  func apply(theme: EditorToolbarTheme?) {
870
+ apply(theme: theme, animateChrome: false)
871
+ }
872
+
873
+ private func apply(theme: EditorToolbarTheme?, animateChrome: Bool) {
857
874
  self.theme = theme
858
875
  let usesNativeAppearance = resolvedAppearance == .native
876
+ let usesTransparentMentionChrome = self.usesTransparentMentionChrome
859
877
  let hasFloatingGlassButtons = self.usesFloatingGlassButtons
860
878
  let usesBarToolbar = usesNativeBarToolbar
879
+ let targetBlurHidden = usesTransparentMentionChrome || usesBarToolbar || !usesNativeAppearance
880
+ let targetBlurAlpha: CGFloat = usesNativeAppearance && !usesTransparentMentionChrome ? resolvedEffectAlpha : 0
881
+ let targetBlurEffect = usesNativeAppearance && !usesTransparentMentionChrome ? resolvedBlurEffect() : nil
882
+ let targetGlassHidden = usesTransparentMentionChrome || usesBarToolbar || !usesNativeAppearance
883
+ let targetGlassBackground = usesNativeAppearance && !usesTransparentMentionChrome
884
+ ? UIColor.systemBackground.withAlphaComponent(resolvedGlassTintAlpha)
885
+ : .clear
886
+ let targetGlassAlpha: CGFloat = targetGlassHidden ? 0 : 1
887
+ let targetBorderColor = usesTransparentMentionChrome ? UIColor.clear : resolvedBorderColor
888
+ let targetBorderWidth: CGFloat = usesTransparentMentionChrome || usesBarToolbar
889
+ ? 0
890
+ : (usesNativeAppearance
891
+ ? (1 / UIScreen.main.scale)
892
+ : resolvedBorderWidth)
893
+ let targetClipsToBounds =
894
+ !usesTransparentMentionChrome
895
+ && ((usesNativeAppearance && !hasFloatingGlassButtons && !usesBarToolbar) || resolvedBorderRadius > 0)
896
+ let targetShadowOpacity: Float =
897
+ usesNativeAppearance && !usesTransparentMentionChrome && !hasFloatingGlassButtons && !usesBarToolbar ? 0.08 : 0
898
+ let targetShadowRadius: CGFloat =
899
+ usesNativeAppearance && !usesTransparentMentionChrome && !hasFloatingGlassButtons && !usesBarToolbar ? 10 : 0
900
+
861
901
  chromeView.backgroundColor = usesNativeAppearance
862
902
  ? .clear
863
903
  : (theme?.backgroundColor ?? .systemBackground)
@@ -865,19 +905,6 @@ final class EditorAccessoryToolbarView: UIInputView {
865
905
  ? nil
866
906
  : (theme?.buttonColor ?? tintColor)
867
907
  chromeView.isOpaque = false
868
- blurView.isHidden = usesBarToolbar || !usesNativeAppearance
869
- blurView.effect = usesNativeAppearance ? resolvedBlurEffect() : nil
870
- blurView.alpha = usesNativeAppearance ? resolvedEffectAlpha : 1
871
- glassTintView.isHidden = usesBarToolbar || !usesNativeAppearance
872
- glassTintView.backgroundColor = usesNativeAppearance
873
- ? UIColor.systemBackground.withAlphaComponent(resolvedGlassTintAlpha)
874
- : .clear
875
- chromeView.layer.borderColor = resolvedBorderColor.cgColor
876
- chromeView.layer.borderWidth = usesBarToolbar
877
- ? 0
878
- : (usesNativeAppearance
879
- ? (1 / UIScreen.main.scale)
880
- : resolvedBorderWidth)
881
908
  chromeView.layer.cornerRadius = resolvedBorderRadius
882
909
  if #available(iOS 13.0, *) {
883
910
  chromeView.layer.cornerCurve = .continuous
@@ -892,11 +919,68 @@ final class EditorAccessoryToolbarView: UIInputView {
892
919
  glassTintView.cornerConfiguration = cornerConfig
893
920
  }
894
921
  #endif
895
- chromeView.clipsToBounds = (usesNativeAppearance && !hasFloatingGlassButtons && !usesBarToolbar) || resolvedBorderRadius > 0
896
- chromeView.layer.shadowOpacity = usesNativeAppearance && !hasFloatingGlassButtons && !usesBarToolbar ? 0.08 : 0
897
- chromeView.layer.shadowRadius = usesNativeAppearance && !hasFloatingGlassButtons && !usesBarToolbar ? 10 : 0
898
922
  chromeView.layer.shadowOffset = CGSize(width: 0, height: 2)
899
923
  chromeView.layer.shadowColor = UIColor.black.cgColor
924
+
925
+ let applyChromeProperties = {
926
+ self.blurView.alpha = targetBlurAlpha
927
+ self.glassTintView.alpha = targetGlassAlpha
928
+ self.chromeView.layer.borderColor = targetBorderColor.cgColor
929
+ self.chromeView.layer.borderWidth = targetBorderWidth
930
+ self.chromeView.layer.shadowOpacity = targetShadowOpacity
931
+ self.chromeView.layer.shadowRadius = targetShadowRadius
932
+ }
933
+ let finishChromeProperties = {
934
+ self.blurView.isHidden = targetBlurHidden
935
+ self.blurView.effect = targetBlurEffect
936
+ self.blurView.alpha = targetBlurAlpha
937
+ self.glassTintView.isHidden = targetGlassHidden
938
+ self.glassTintView.backgroundColor = targetGlassHidden ? .clear : targetGlassBackground
939
+ self.glassTintView.alpha = targetGlassAlpha
940
+ self.chromeView.layer.borderColor = targetBorderColor.cgColor
941
+ self.chromeView.layer.borderWidth = targetBorderWidth
942
+ self.chromeView.layer.shadowOpacity = targetShadowOpacity
943
+ self.chromeView.layer.shadowRadius = targetShadowRadius
944
+ self.chromeView.clipsToBounds = targetClipsToBounds
945
+ }
946
+
947
+ let shouldAnimateChrome = animateChrome && UIView.areAnimationsEnabled && window != nil
948
+ didAnimateChromeTransition = shouldAnimateChrome
949
+ if shouldAnimateChrome {
950
+ let blurWasHidden = blurView.isHidden
951
+ let glassWasHidden = glassTintView.isHidden
952
+ if !targetBlurHidden {
953
+ blurView.effect = targetBlurEffect
954
+ }
955
+ if !targetBlurHidden || !blurWasHidden {
956
+ blurView.isHidden = false
957
+ }
958
+ if blurWasHidden && !targetBlurHidden {
959
+ blurView.alpha = 0
960
+ }
961
+ if !targetGlassHidden {
962
+ glassTintView.backgroundColor = targetGlassBackground
963
+ }
964
+ if !targetGlassHidden || !glassWasHidden {
965
+ glassTintView.isHidden = false
966
+ }
967
+ if glassWasHidden && !targetGlassHidden {
968
+ glassTintView.alpha = 0
969
+ }
970
+ chromeView.clipsToBounds = targetClipsToBounds
971
+ UIView.animate(
972
+ withDuration: Self.chromeTransitionDuration,
973
+ delay: 0,
974
+ options: [.beginFromCurrentState, .allowUserInteraction, .curveEaseOut],
975
+ animations: applyChromeProperties,
976
+ completion: { _ in
977
+ finishChromeProperties()
978
+ }
979
+ )
980
+ } else {
981
+ finishChromeProperties()
982
+ }
983
+
900
984
  chromeLeadingConstraint?.constant = resolvedHorizontalInset
901
985
  chromeTrailingConstraint?.constant = -resolvedHorizontalInset
902
986
  chromeBottomConstraint?.constant = -resolvedKeyboardOffset
@@ -949,6 +1033,7 @@ final class EditorAccessoryToolbarView: UIInputView {
949
1033
  mentionScrollView.isHidden = !hasSuggestions
950
1034
  scrollView.isHidden = hasSuggestions
951
1035
  mentionRowHeightConstraint?.constant = hasSuggestions ? Self.mentionRowHeight : 0
1036
+ apply(theme: theme, animateChrome: hadSuggestions != hasSuggestions)
952
1037
  invalidateIntrinsicContentSize()
953
1038
  setNeedsLayout()
954
1039
  return hadSuggestions != hasSuggestions
@@ -988,6 +1073,9 @@ final class EditorAccessoryToolbarView: UIInputView {
988
1073
  var firstButtonTintColorForTesting: UIColor? {
989
1074
  buttonBindings.first?.button.tintColor
990
1075
  }
1076
+ func firstButtonTitleColorForTesting(_ state: UIControl.State) -> UIColor? {
1077
+ buttonBindings.first?.button.titleColor(for: state)
1078
+ }
991
1079
  var firstButtonTintAdjustmentModeForTesting: UIView.TintAdjustmentMode {
992
1080
  buttonBindings.first?.button.tintAdjustmentMode ?? .automatic
993
1081
  }
@@ -1552,7 +1640,10 @@ final class EditorAccessoryToolbarView: UIInputView {
1552
1640
  #endif
1553
1641
 
1554
1642
  if resolvedAppearance == .native {
1555
- button.tintColor = enabled ? nil : .systemGray
1643
+ let tintColor = enabled ? theme?.buttonColor : resolvedNativeDisabledButtonTintColor()
1644
+ button.tintColor = tintColor
1645
+ button.setTitleColor(tintColor, for: .normal)
1646
+ button.setTitleColor(resolvedNativeDisabledButtonTintColor(), for: .disabled)
1556
1647
  button.tintAdjustmentMode = enabled ? .automatic : .normal
1557
1648
  button.alpha = 1
1558
1649
  button.backgroundColor = active
@@ -1602,10 +1693,28 @@ final class EditorAccessoryToolbarView: UIInputView {
1602
1693
  theme?.resolvedButtonBorderRadius ?? 8
1603
1694
  }
1604
1695
 
1696
+ private func resolvedNativeDisabledButtonTintColor() -> UIColor {
1697
+ theme?.buttonDisabledColor ?? resolvedNativeButtonTintColor.withAlphaComponent(Self.nativeDisabledButtonOpacity)
1698
+ }
1699
+
1700
+ private var resolvedNativeButtonTintColor: UIColor {
1701
+ theme?.buttonColor ?? tintColor ?? .label
1702
+ }
1703
+
1605
1704
  private var usesFloatingGlassButtons: Bool {
1606
1705
  return false
1607
1706
  }
1608
1707
 
1708
+ private var usesTransparentMentionChrome: Bool {
1709
+ guard resolvedAppearance == .native, !mentionButtons.isEmpty else { return false }
1710
+ #if compiler(>=6.2)
1711
+ if #available(iOS 26.0, *) {
1712
+ return true
1713
+ }
1714
+ #endif
1715
+ return false
1716
+ }
1717
+
1609
1718
  private var usesNativeBarToolbar: Bool {
1610
1719
  return false
1611
1720
  }
@@ -1104,10 +1104,12 @@ final class RenderBridge {
1104
1104
  }
1105
1105
 
1106
1106
  let tagged = NSMutableAttributedString(attributedString: attributedString)
1107
+ let firstComposedCharacterRange = (tagged.string as NSString)
1108
+ .rangeOfComposedCharacterSequence(at: 0)
1107
1109
  tagged.addAttribute(
1108
1110
  RenderBridgeAttributes.topLevelChildIndex,
1109
1111
  value: NSNumber(value: topLevelChildIndex),
1110
- range: NSRange(location: 0, length: 1)
1112
+ range: firstComposedCharacterRange
1111
1113
  )
1112
1114
  return tagged
1113
1115
  }