diff --git a/docs/markdown-renderer.md b/docs/markdown-renderer.md index 219bec14b5..241fe5db0b 100644 --- a/docs/markdown-renderer.md +++ b/docs/markdown-renderer.md @@ -20,6 +20,8 @@ Mermaid fences use the existing isolated diagram runtime and fullscreen viewer. Native code blocks use the library's copy control and compiled grammars. Web code blocks use `HighlightedCodeBlock` through the library's `renderers.codeBlock` override. The patch also preserves web list numbering and link titles, marks pending parses with `aria-busy` so Find waits for rendered text, and carries link syntax through native and web tap events. Web inline code inside a link inherits only the link colour; its code font, size and background stay intact. Keep link variants colour-only: native pills replace text runs and prevent paths from wrapping; Android pills also use the surrounding block’s font size. The package patch is tracked by [upstream PR #997](https://github.com/software-mansion/enriched-markdown/pull/997); remove the patch when the pinned release includes it. +The package patch also keeps overflowing native horizontal blocks in control of their drags: Android claims parent interception at touch-down and releases it for vertical drags so the chat list can scroll; iOS keeps its scroll recognizer active for outward drags at the leading edge. This prevents compact sidebar pans from taking block scrolling without changing sidebar swipes on prose or non-overflowing blocks. Native changes require a client rebuild. + The library determines how incomplete Markdown appears during streaming. Completed text uses the same component and engine as history. Find and scroll-to-message address source message ids; selection-copy understands semantic HTML and the web code component's copy metadata. ## Verification diff --git a/patches/react-native-enriched-markdown+1.2.0-nightly-20261014-766fc0e67.patch b/patches/react-native-enriched-markdown+1.2.0-nightly-20261014-766fc0e67.patch index 7c1d282c7b..009ed49878 100644 --- a/patches/react-native-enriched-markdown+1.2.0-nightly-20261014-766fc0e67.patch +++ b/patches/react-native-enriched-markdown+1.2.0-nightly-20261014-766fc0e67.patch @@ -518,9 +518,25 @@ index c9644d1..58b2f4e 100644 val onTaskListItemPress: ((taskIndex: Int, checked: Boolean, itemText: String) -> Unit)?, val onContextMenuItemPress: ((itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit)?, diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/TableContainerView.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/TableContainerView.kt -index e190548..f9d959a 100644 +index e190548..b2e6cf2 100644 --- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/TableContainerView.kt +++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/TableContainerView.kt +@@ -19,7 +19,6 @@ import android.view.Gravity + import android.view.MotionEvent + import android.view.View + import android.widget.FrameLayout +-import android.widget.HorizontalScrollView + import androidx.core.view.ViewCompat + import com.swmansion.enriched.markdown.accessibility.AccessibilityLabels + import com.swmansion.enriched.markdown.parser.MarkdownASTNode +@@ -38,6 +37,7 @@ import com.swmansion.enriched.markdown.utils.text.span.prepareWidthAwareSpans + import com.swmansion.enriched.markdown.utils.text.view.LinkLongPressMovementMethod + import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForLinkTap + import com.swmansion.enriched.markdown.utils.text.view.reallowParentInterceptIfLinkReleased ++import com.swmansion.enriched.markdown.views.ClaimingHorizontalScrollView + import com.swmansion.enriched.markdown.views.ContextMenuPopup + import kotlin.math.ceil + import kotlin.math.max @@ -57,7 +57,7 @@ class TableContainerView( var allowFontScaling = true @@ -530,6 +546,15 @@ index e190548..f9d959a 100644 var onLinkLongPress: ((String) -> Unit)? = null var accessibilityLabels: AccessibilityLabels = AccessibilityLabels() set(value) { +@@ -70,7 +70,7 @@ class TableContainerView( + var dynamicProps: DynamicBlockProps = DynamicBlockProps() + + private val scrollView = +- HorizontalScrollView(context).apply { ++ ClaimingHorizontalScrollView(context).apply { + isHorizontalScrollBarEnabled = true + overScrollMode = View.OVER_SCROLL_NEVER + importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt index ff00069..f3e13aa 100644 --- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt @@ -2441,3 +2466,138 @@ index c8311d4..d1e698c 100644 } function LatexMathInlineRenderer({ node, +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/CodeBlockContainerView.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/CodeBlockContainerView.kt +index cba0825..21387f0 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/CodeBlockContainerView.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/CodeBlockContainerView.kt +@@ -21,7 +21,6 @@ import android.view.GestureDetector + import android.view.MotionEvent + import android.view.View + import android.widget.FrameLayout +-import android.widget.HorizontalScrollView + import android.widget.ImageView + import androidx.appcompat.widget.AppCompatImageButton + import androidx.appcompat.widget.AppCompatTextView +@@ -33,6 +32,7 @@ import com.swmansion.enriched.markdown.utils.common.CodeBlockHighlighter + import com.swmansion.enriched.markdown.utils.common.CodeBlockNode + import com.swmansion.enriched.markdown.utils.text.extensions.applyCodeBlockTextStyle + import com.swmansion.enriched.markdown.utils.text.span.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE ++import com.swmansion.enriched.markdown.views.ClaimingHorizontalScrollView + import com.swmansion.enriched.markdown.views.ContextMenuPopup + import com.swmansion.enriched.markdown.views.CopyGlyph + import kotlin.math.ceil +@@ -102,7 +102,7 @@ class CodeBlockContainerView( + } + + private val scrollView = +- HorizontalScrollView(context).apply { ++ ClaimingHorizontalScrollView(context).apply { + isHorizontalScrollBarEnabled = true + overScrollMode = View.OVER_SCROLL_NEVER + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/ClaimingHorizontalScrollView.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/ClaimingHorizontalScrollView.kt +new file mode 100644 +index 0000000..d80ecbd +--- /dev/null ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/ClaimingHorizontalScrollView.kt +@@ -0,0 +1,41 @@ ++package com.swmansion.enriched.markdown.views ++ ++import android.content.Context ++import android.view.MotionEvent ++import android.view.ViewConfiguration ++import android.widget.HorizontalScrollView ++import kotlin.math.abs ++ ++/** Claims overflowing blocks before ancestor gesture handlers can activate. */ ++internal class ClaimingHorizontalScrollView(context: Context) : HorizontalScrollView(context) { ++ private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop ++ private var downX = 0f ++ private var downY = 0f ++ private var claimed = false ++ ++ override fun dispatchTouchEvent(event: MotionEvent): Boolean { ++ if (event.actionMasked == MotionEvent.ACTION_DOWN) { ++ downX = event.x ++ downY = event.y ++ claimed = canScrollHorizontally(-1) || canScrollHorizontally(1) ++ if (claimed) parent.requestDisallowInterceptTouchEvent(true) ++ } ++ ++ val handled = super.dispatchTouchEvent(event) ++ when (event.actionMasked) { ++ MotionEvent.ACTION_MOVE -> { ++ if (claimed) { ++ val dx = abs(event.x - downX) ++ val dy = abs(event.y - downY) ++ if (dy > dx && dy > touchSlop) { ++ // Release after native dispatch, which can itself disallow interception. ++ parent.requestDisallowInterceptTouchEvent(false) ++ claimed = false ++ } ++ } ++ } ++ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> claimed = false ++ } ++ return handled ++ } ++} +diff --git a/node_modules/react-native-enriched-markdown/ios/utils/ENRMHorizontalScrollView.h b/node_modules/react-native-enriched-markdown/ios/utils/ENRMHorizontalScrollView.h +index a0c77f6..644e801 100644 +--- a/node_modules/react-native-enriched-markdown/ios/utils/ENRMHorizontalScrollView.h ++++ b/node_modules/react-native-enriched-markdown/ios/utils/ENRMHorizontalScrollView.h +@@ -1,30 +1,7 @@ + #import "ENRMUIKit.h" + + /// Horizontal scroll view shared by code blocks, tables and display math. +-/// +-/// A rightward pan that starts while the view is settled at its leading edge is +-/// refused, so a full-width back gesture (react-native-screens +-/// `fullScreenGestureEnabled`, iOS 26 content pop) can begin instead. Inward +-/// pans, pans from inside the content, and pans during deceleration or bounce +-/// scroll as usual. Cost: no bounce on a fresh rightward pan at the leading +-/// edge. Left edge only; RTL is not handled. macOS keeps RCTUIScrollView. +-/// +-/// TODO: move this decision to react-native-screens. RNSScreenStack already +-/// inspects competing scroll-view pans (`shouldRequireFailureOfGestureRecognizer:` +-/// on iOS 26, `shouldRecognizeSimultaneouslyWithGestureRecognizer:` before); +-/// treating a scroll view settled at its leading edge as not competing would +-/// cover every horizontal scroll view, keep the bounce when nothing can pop, +-/// and handle RTL. Then this subclass is only needed without RNS. ++/// Keep the native scroll recognizer at the leading edge so enclosing sidebar ++/// gestures cannot take an outward drag that starts in an overflowing block. + @interface ENRMHorizontalScrollView : RCTUIScrollView + @end +- +-/// Decision behind `-gestureRecognizerShouldBegin:`, kept pure so it can be +-/// tested without a live scroll view. `leadingBoundary` is the content offset +-/// at the leading edge, i.e. `-adjustedContentInset.left`. +-static inline BOOL ENRMShouldYieldOutwardPan(CGFloat contentOffsetX, CGFloat leadingBoundary, BOOL decelerating, +- CGPoint velocity) +-{ +- BOOL outward = velocity.x > fabs(velocity.y); +- BOOL settledAtLeadingEdge = !decelerating && fabs(contentOffsetX - leadingBoundary) <= 0.5; +- return outward && settledAtLeadingEdge; +-} +diff --git a/node_modules/react-native-enriched-markdown/ios/utils/ENRMHorizontalScrollView.m b/node_modules/react-native-enriched-markdown/ios/utils/ENRMHorizontalScrollView.m +index b98d10c..b645629 100644 +--- a/node_modules/react-native-enriched-markdown/ios/utils/ENRMHorizontalScrollView.m ++++ b/node_modules/react-native-enriched-markdown/ios/utils/ENRMHorizontalScrollView.m +@@ -1,17 +1,4 @@ + #import "ENRMHorizontalScrollView.h" + + @implementation ENRMHorizontalScrollView +- +-#if !TARGET_OS_OSX +-- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +-{ +- if (gestureRecognizer == self.panGestureRecognizer && +- ENRMShouldYieldOutwardPan(self.contentOffset.x, -self.adjustedContentInset.left, self.isDecelerating, +- [self.panGestureRecognizer velocityInView:self])) { +- return NO; +- } +- return [super gestureRecognizerShouldBegin:gestureRecognizer]; +-} +-#endif +- + @end