From d923b1ece127896f315d8fc03e9f0a2812c0606e Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Wed, 30 Sep 2026 18:31:38 +0200 Subject: [PATCH 1/4] bump gesture handler to 3.3 --- config/eslint/eslint.seatbelt.tsv | 44 +++++++++++++++++++ ios/Podfile.lock | 5 ++- package-lock.json | 35 ++------------- package.json | 2 +- .../AttachmentCarouselView/index.tsx | 4 +- .../AvatarCropModal/ImageCropView.tsx | 4 +- .../MultiGestureCanvas/usePanGesture.ts | 4 +- .../MultiGestureCanvas/usePinchGesture.ts | 4 +- .../MultiGestureCanvas/useTapGestures.ts | 4 +- 9 files changed, 62 insertions(+), 44 deletions(-) diff --git a/config/eslint/eslint.seatbelt.tsv b/config/eslint/eslint.seatbelt.tsv index 09d318db701a..89516fa48356 100644 --- a/config/eslint/eslint.seatbelt.tsv +++ b/config/eslint/eslint.seatbelt.tsv @@ -68,26 +68,44 @@ "../../src/components/AttachmentPicker/index.native.tsx" "react-hooks/refs" 1 "../../src/components/AttachmentPicker/index.tsx" "react-hooks/refs" 1 "../../src/components/AttachmentPreview.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 +"../../src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx" "@typescript-eslint/no-deprecated/GestureType" 3 +"../../src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx" "@typescript-eslint/no-deprecated/LegacyComposedGesture" 1 "../../src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 "../../src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx" "react-hooks/refs" 3 "../../src/components/Attachments/AttachmentCarousel/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/Attachments/AttachmentCarousel/Pager/index.tsx" "@typescript-eslint/no-deprecated/Gesture.Native" 2 "../../src/components/Attachments/AttachmentCarousel/Pager/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/Attachments/AttachmentCarousel/Pager/types.ts" "@typescript-eslint/no-deprecated/GestureType" 2 "../../src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts" "no-restricted-syntax" 1 +"../../src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 "../../src/components/Attachments/AttachmentView/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4 "../../src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx" "no-restricted-syntax" 1 "../../src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx" "react-hooks/refs" 1 "../../src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/components/AutoCompleteSuggestions/index.tsx" "react-hooks/set-state-in-effect" 1 "../../src/components/AutoSubmitModal.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/AvatarCropModal/AvatarCropView.tsx" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 +"../../src/components/AvatarCropModal/AvatarCropView.tsx" "@typescript-eslint/no-deprecated/PanGestureChangeEventPayload" 1 +"../../src/components/AvatarCropModal/ImageCropView.tsx" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 +"../../src/components/AvatarCropModal/ImageCropView.tsx" "@typescript-eslint/no-deprecated/LegacyPanGesture" 1 "../../src/components/AvatarCropModal/ImageCropView.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/AvatarCropModal/Slider.tsx" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 +"../../src/components/AvatarCropModal/Slider.tsx" "@typescript-eslint/no-deprecated/PanGestureChangeEventPayload" 1 "../../src/components/AvatarCropModal/Slider.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/components/AvatarSelector.tsx" "no-restricted-syntax" 2 "../../src/components/AvatarWithImagePicker.tsx" "react-hooks/set-state-in-effect" 2 "../../src/components/BookTravelButton.tsx" "no-restricted-imports" 1 "../../src/components/ButtonWithDropdownMenu/index.tsx" "react-hooks/set-state-in-effect" 1 "../../src/components/CategoryPicker/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Charts/hooks/useChartInteractions.ts" "@typescript-eslint/no-deprecated/Gesture.Hover" 2 +"../../src/components/Charts/hooks/useChartInteractions.ts" "@typescript-eslint/no-deprecated/Gesture.Race" 2 +"../../src/components/Charts/hooks/useChartInteractions.ts" "@typescript-eslint/no-deprecated/Gesture.Tap" 2 "../../src/components/Charts/hooks/useChartLabelFormats.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/components/Charts/hooks/useChartLabelMeasurements.ts" "rulesdir/no-raw-typography" 1 +"../../src/components/Charts/PieChart/PieChartContent.tsx" "@typescript-eslint/no-deprecated/Gesture.Hover" 2 +"../../src/components/Charts/PieChart/PieChartContent.tsx" "@typescript-eslint/no-deprecated/Gesture.Race" 2 +"../../src/components/Charts/PieChart/PieChartContent.tsx" "@typescript-eslint/no-deprecated/Gesture.Tap" 2 "../../src/components/Charts/utils/chartFontAssets.native.ts" "@typescript-eslint/no-unsafe-type-assertion" 13 "../../src/components/Charts/utils/chartFontAssets.ts" "@typescript-eslint/no-unsafe-type-assertion" 13 "../../src/components/Charts/utils/chartFontsCache.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 @@ -144,6 +162,7 @@ "../../src/components/Form/InputWrapper.tsx" "react-hooks/refs" 1 "../../src/components/GPSInProgressModal/index.native.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1 "../../src/components/GPSTripStateChecker/index.native.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1 +"../../src/components/GrowlNotification/GrowlNotificationContent.tsx" "@typescript-eslint/no-deprecated/Gesture.Fling" 2 "../../src/components/HeaderWithBackButton/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 "../../src/components/Hoverable/ActiveHoverable.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/components/Hoverable/ActiveHoverable.tsx" "react-hooks/refs" 3 @@ -156,6 +175,7 @@ "../../src/components/HTMLEngineProvider/HTMLRenderers/ConciergeLinkRenderer.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/components/HTMLEngineProvider/HTMLRenderers/EditedRenderer.tsx" "@typescript-eslint/no-deprecated/fontSize" 3 "../../src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/HTMLEngineProvider/HTMLRenderers/HTMLTableScroll/index.native.tsx" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 "../../src/components/HTMLEngineProvider/HTMLRenderers/MentionHereRenderer.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 @@ -194,6 +214,8 @@ "../../src/components/Modal/BaseModal.tsx" "react-hooks/set-state-in-effect" 1 "../../src/components/Modal/Global/ConfirmModalWrapper.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1 "../../src/components/Modal/Global/ModalContext.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/Modal/ReanimatedModal/Container/GestureHandler.tsx" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 +"../../src/components/Modal/ReanimatedModal/Container/GestureHandler.tsx" "@typescript-eslint/no-deprecated/GestureType" 1 "../../src/components/Modal/ReanimatedModal/Container/index.web.tsx" "react-hooks/refs" 1 "../../src/components/MoneyReportHeaderActions/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 "../../src/components/MoneyRequestAmountInput.tsx" "react-hooks/immutability" 2 @@ -217,12 +239,23 @@ "../../src/components/MultifactorAuthentication/Context/stateReducer.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/components/MultifactorAuthentication/Context/usePromptContent.ts" "react-hooks/refs" 10 "../../src/components/MultifactorAuthentication/ValidateCodeResendButton.tsx" "no-restricted-syntax" 1 +"../../src/components/MultiGestureCanvas/index.tsx" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 +"../../src/components/MultiGestureCanvas/index.tsx" "@typescript-eslint/no-deprecated/Gesture.Race" 2 +"../../src/components/MultiGestureCanvas/index.tsx" "@typescript-eslint/no-deprecated/Gesture.Simultaneous" 2 +"../../src/components/MultiGestureCanvas/index.tsx" "@typescript-eslint/no-deprecated/GestureRef" 1 +"../../src/components/MultiGestureCanvas/index.tsx" "@typescript-eslint/no-deprecated/GestureType" 2 "../../src/components/MultiGestureCanvas/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/components/MultiGestureCanvas/index.tsx" "no-restricted-syntax" 1 "../../src/components/MultiGestureCanvas/index.tsx" "react-hooks/preserve-manual-memoization" 1 "../../src/components/MultiGestureCanvas/index.tsx" "react-hooks/refs" 2 +"../../src/components/MultiGestureCanvas/usePanGesture.ts" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 +"../../src/components/MultiGestureCanvas/usePanGesture.ts" "@typescript-eslint/no-deprecated/LegacyPanGesture" 1 "../../src/components/MultiGestureCanvas/usePanGesture.ts" "no-restricted-syntax" 1 +"../../src/components/MultiGestureCanvas/usePinchGesture.ts" "@typescript-eslint/no-deprecated/Gesture.Pinch" 2 +"../../src/components/MultiGestureCanvas/usePinchGesture.ts" "@typescript-eslint/no-deprecated/LegacyPinchGesture" 1 "../../src/components/MultiGestureCanvas/usePinchGesture.ts" "react-hooks/set-state-in-effect" 1 +"../../src/components/MultiGestureCanvas/useTapGestures.ts" "@typescript-eslint/no-deprecated/Gesture.Tap" 4 +"../../src/components/MultiGestureCanvas/useTapGestures.ts" "@typescript-eslint/no-deprecated/LegacyTapGesture" 2 "../../src/components/MultiGestureCanvas/useTapGestures.ts" "no-restricted-syntax" 1 "../../src/components/Navigation/QuickCreationActionsBar/index.tsx" "no-restricted-imports" 1 "../../src/components/NumberWithSymbolForm.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 @@ -250,6 +283,8 @@ "../../src/components/Reactions/AddReactionBubble.tsx" "react-hooks/refs" 1 "../../src/components/Reactions/MiniQuickEmojiReactions.tsx" "react-hooks/refs" 1 "../../src/components/Reactions/ReactionTooltipContent.tsx" "rulesdir/no-direct-personal-details-list" 1 +"../../src/components/ReceiptCropView/index.tsx" "@typescript-eslint/no-deprecated/Gesture.Pan" 4 +"../../src/components/ReceiptCropView/index.tsx" "@typescript-eslint/no-deprecated/PanGestureChangeEventPayload" 2 "../../src/components/ReceiptCropView/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/components/ReceiptHoverZoom/useReceiptHoverZoom.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/components/ReportActionAvatars/useReportPreviewSenderID.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 @@ -377,6 +412,7 @@ "../../src/components/TransactionItemRow/EditableCell/usePopoverEditState.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/components/UploadFile.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/components/UserInitialsAvatar.tsx" "@typescript-eslint/no-deprecated/fontSize" 1 +"../../src/components/ValidateCodeInput.tsx" "@typescript-eslint/no-deprecated/Gesture.Tap" 2 "../../src/components/ValidateCodeInput.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 "../../src/components/ValidateCodeInput.tsx" "react-hooks/refs" 1 "../../src/components/ValidateCodeInput.tsx" "react-hooks/set-state-in-effect" 3 @@ -384,6 +420,10 @@ "../../src/components/VideoPlayer/BaseVideoPlayer.tsx" "react-hooks/refs" 13 "../../src/components/VideoPlayer/BaseVideoPlayer.tsx" "react-hooks/set-state-in-effect" 2 "../../src/components/VideoPlayer/useHandleNativeVideoControls/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 +"../../src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx" "@typescript-eslint/no-deprecated/PanGestureChangeEventPayload" 1 +"../../src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 +"../../src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx" "@typescript-eslint/no-deprecated/PanGestureChangeEventPayload" 1 "../../src/components/VideoPlayerContexts/PlaybackContext/playbackContextReportIDUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/components/WalletStatementModal/index.native.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/components/WideRHPContextProvider/useShouldRenderOverlay.ts" "react-hooks/set-state-in-effect" 1 @@ -451,6 +491,7 @@ "../../src/hooks/useListKeyboardNav.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 "../../src/hooks/useMarkdownStyle.ts" "rulesdir/no-raw-typography" 3 "../../src/hooks/useMoneyReportHeaderStatusBar.ts" "rulesdir/no-direct-personal-details-list" 1 +"../../src/hooks/useNativeCamera.ts" "@typescript-eslint/no-deprecated/Gesture.Tap" 2 "../../src/hooks/useNewTransactions.ts" "react-hooks/refs" 2 "../../src/hooks/useOnyx.ts" "@typescript-eslint/no-unsafe-type-assertion" 10 "../../src/hooks/useOptimisticDraftTransactions.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 @@ -956,6 +997,7 @@ "../../src/pages/inbox/report/ReactionList/PopoverReactionList.tsx" "rulesdir/no-direct-personal-details-list" 1 "../../src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/pages/inbox/report/ReportActionCompose/ComposerLocalTime.tsx" "rulesdir/no-direct-personal-details-list" 1 +"../../src/pages/inbox/report/ReportActionCompose/ComposerSendButton.tsx" "@typescript-eslint/no-deprecated/Gesture.Tap" 2 "../../src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx" "@typescript-eslint/no-unsafe-type-assertion" 5 "../../src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx" "react-hooks/refs" 7 "../../src/pages/inbox/report/ReportActionCompose/SuggestionEmoji.tsx" "react-hooks/refs" 1 @@ -1019,7 +1061,9 @@ "../../src/pages/iou/request/step/IOURequestStepDistanceGPS/GPSButtons/index.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 3 "../../src/pages/iou/request/step/IOURequestStepDistanceGPS/index.native.tsx" "rulesdir/no-direct-personal-details-list" 1 "../../src/pages/iou/request/step/IOURequestStepDistanceMap.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/iou/request/step/IOURequestStepGPSTripEdit/Slider.tsx" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 "../../src/pages/iou/request/step/IOURequestStepOdometerImage/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/iou/request/step/IOURequestStepScan/components/CameraViewport.tsx" "@typescript-eslint/no-deprecated/GestureType" 1 "../../src/pages/iou/request/step/IOURequestStepScan/components/NavigationAwareCamera/WebCamera.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/pages/iou/request/step/IOURequestStepScan/components/ScanEditReceipt.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/pages/iou/request/step/IOURequestStepScan/components/ScanSkipConfirmation.tsx" "rulesdir/no-direct-personal-details-list" 1 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 307217ee3162..33bbc959c9fa 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2928,7 +2928,7 @@ PODS: - React-Core - RNFS (2.20.0): - React-Core - - RNGestureHandler (2.32.0): + - RNGestureHandler (3.3.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2950,6 +2950,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - ReactNativeDependencies + - RNWorklets (>= 0.8.0) - Yoga - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) @@ -4101,7 +4102,7 @@ SPEC CHECKSUMS: RNFBAnalytics: 2f451df50833a890974d55a93557da878a85b29e RNFBApp: db9c2e6d36fe579ab19b82c0a4a417ff7569db7e RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8 - RNGestureHandler: 7b07d9192bc65c6883fb37fdecc05dc6b61c814f + RNGestureHandler: 85e32defcf23dd3b023356355c5c1dd1d9d51bde RNGoogleSignin: 89877c73f0fbf6af2038fbdb7b73b5a25b8330cc RNLiveMarkdown: e1379ad7c01172909900127aff8108582c605200 RNLocalize: 82e7aa2857ce5dcd9e2a0c04f8c34c2ed50821df diff --git a/package-lock.json b/package-lock.json index d6ee52481174..8d3fc8d6c3e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -115,7 +115,7 @@ "react-native-device-info": "10.3.1", "react-native-draggable-flatlist": "^4.0.3", "react-native-fs": "^2.20.0", - "react-native-gesture-handler": "2.32.0", + "react-native-gesture-handler": "3.3.0", "react-native-google-places-autocomplete": "2.6.4", "react-native-haptic-feedback": "^2.3.3", "react-native-image-picker": "^7.1.2", @@ -5219,16 +5219,6 @@ "url": "https://github.com/sponsors/dword-design" } }, - "node_modules/@egjs/hammerjs": { - "version": "2.0.17", - "license": "MIT", - "dependencies": { - "@types/hammerjs": "^2.0.36" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/@emnapi/core": { "version": "1.11.2", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", @@ -19968,10 +19958,6 @@ "@types/node": "*" } }, - "node_modules/@types/hammerjs": { - "version": "2.0.41", - "license": "MIT" - }, "node_modules/@types/howler": { "version": "2.2.12", "dev": true, @@ -29317,17 +29303,6 @@ "hermes-estree": "0.36.1" } }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "license": "MIT" - }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -37486,14 +37461,12 @@ } }, "node_modules/react-native-gesture-handler": { - "version": "2.32.0", - "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.32.0.tgz", - "integrity": "sha512-uYIMOKlKENORq2SABE+jIjbPU+h5I/sQKcq2v16zRq848nwEp1fWRVwML4QWqijc8UcXJC25o54S8GQd4Mf2OA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-3.3.0.tgz", + "integrity": "sha512-ZCTlJ09nE3GPbm7J74lvwpxmx1TQ/7MDlHZNLARVYTN82k21mJUJ7BmI9eWjEi8eevHiAudI5O1Cpnt0rJfoJA==", "license": "MIT", "dependencies": { - "@egjs/hammerjs": "^2.0.17", "@types/react-test-renderer": "^19.1.0", - "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { diff --git a/package.json b/package.json index a604266d0900..1bd42524f836 100644 --- a/package.json +++ b/package.json @@ -191,7 +191,7 @@ "react-native-device-info": "10.3.1", "react-native-draggable-flatlist": "^4.0.3", "react-native-fs": "^2.20.0", - "react-native-gesture-handler": "2.32.0", + "react-native-gesture-handler": "3.3.0", "react-native-google-places-autocomplete": "2.6.4", "react-native-haptic-feedback": "^2.3.3", "react-native-image-picker": "^7.1.2", diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx index 3ab9ffb5a3ad..c207987a8ad7 100644 --- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx @@ -23,7 +23,7 @@ import CONST from '@src/CONST'; import type {RefObject} from 'react'; import type {ListRenderItemInfo} from 'react-native'; -import type {ComposedGesture, GestureType} from 'react-native-gesture-handler'; +import type {LegacyComposedGesture, GestureType} from 'react-native-gesture-handler'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {Keyboard, PixelRatio, View} from 'react-native'; @@ -42,7 +42,7 @@ const MIN_FLING_VELOCITY = 500; type DeviceAwareGestureDetectorProps = { canUseTouchScreen: boolean; - gesture: ComposedGesture | GestureType; + gesture: LegacyComposedGesture | GestureType; children: React.ReactNode; }; diff --git a/src/components/AvatarCropModal/ImageCropView.tsx b/src/components/AvatarCropModal/ImageCropView.tsx index adb26cfd55d4..cf7127f89563 100644 --- a/src/components/AvatarCropModal/ImageCropView.tsx +++ b/src/components/AvatarCropModal/ImageCropView.tsx @@ -9,7 +9,7 @@ import ControlSelection from '@libs/ControlSelection'; import type IconAsset from '@src/types/utils/IconAsset'; import type {ComponentRef} from 'react'; -import type {PanGesture} from 'react-native-gesture-handler'; +import type {LegacyPanGesture} from 'react-native-gesture-handler'; import type {SharedValue} from 'react-native-reanimated'; import React from 'react'; @@ -40,7 +40,7 @@ type ImageCropViewProps = { scale: SharedValue; /** Configuration object for pan gesture for handling image panning */ - panGesture?: PanGesture; + panGesture?: LegacyPanGesture; /** Image crop vector mask */ maskImage?: IconAsset; diff --git a/src/components/MultiGestureCanvas/usePanGesture.ts b/src/components/MultiGestureCanvas/usePanGesture.ts index e4dab5092a6d..9b1f5695cb7d 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.ts +++ b/src/components/MultiGestureCanvas/usePanGesture.ts @@ -1,6 +1,6 @@ import {isMobile} from '@libs/Browser'; -import type {PanGesture} from 'react-native-gesture-handler'; +import type {LegacyPanGesture} from 'react-native-gesture-handler'; import {useCallback} from 'react'; import {Dimensions} from 'react-native'; @@ -52,7 +52,7 @@ const usePanGesture = ({ isSwipingDownToClose, shouldDisableSwipeDownToClose, onSwipeDown, -}: UsePanGestureProps): PanGesture => { +}: UsePanGestureProps): LegacyPanGesture => { // The content size after fitting it to the canvas and zooming const zoomedContentWidth = useDerivedValue(() => contentSize.width * totalScale.get(), [contentSize.width]); const zoomedContentHeight = useDerivedValue(() => contentSize.height * totalScale.get(), [contentSize.height]); diff --git a/src/components/MultiGestureCanvas/usePinchGesture.ts b/src/components/MultiGestureCanvas/usePinchGesture.ts index b55fea09d09f..f2877fd3bd13 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.ts +++ b/src/components/MultiGestureCanvas/usePinchGesture.ts @@ -1,4 +1,4 @@ -import type {PinchGesture} from 'react-native-gesture-handler'; +import type {LegacyPinchGesture} from 'react-native-gesture-handler'; import {useCallback, useEffect, useState} from 'react'; import {Gesture} from 'react-native-gesture-handler'; @@ -38,7 +38,7 @@ const usePinchGesture = ({ isTransformGestureActive, stopAnimation, onScaleChanged, -}: UsePinchGestureProps): PinchGesture => { +}: UsePinchGestureProps): LegacyPinchGesture => { // The current pinch gesture event scale const currentPinchScale = useSharedValue(1); diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts index 1d2ab7aa3fc4..ebc3b64cabee 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.ts +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -1,4 +1,4 @@ -import type {TapGesture} from 'react-native-gesture-handler'; +import type {LegacyTapGesture} from 'react-native-gesture-handler'; import {useCallback, useMemo} from 'react'; import {Gesture} from 'react-native-gesture-handler'; @@ -45,7 +45,7 @@ const useTapGestures = ({ isTransformGestureActive, onScaleChanged, onTap, -}: UseTapGesturesProps): {singleTapGesture: TapGesture; doubleTapGesture: TapGesture} => { +}: UseTapGesturesProps): {singleTapGesture: LegacyTapGesture; doubleTapGesture: LegacyTapGesture} => { // The content size after scaling it with minimum scale to fit the content into the canvas const scaledContentWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); const scaledContentHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); From d688dd3fe4172bf54fafc426de6d3b7da7a6e949 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Thu, 1 Oct 2026 13:54:15 +0200 Subject: [PATCH 2/4] discard pre commit changes --- .../isDeployChecklistLocked/index.js | 151 ++++++++++++++---- .../javascript/proposalPoliceComment/index.js | 151 ++++++++++++++---- 2 files changed, 246 insertions(+), 56 deletions(-) diff --git a/.github/actions/javascript/isDeployChecklistLocked/index.js b/.github/actions/javascript/isDeployChecklistLocked/index.js index b1e68e9965bf..ebdf2bf6961c 100644 --- a/.github/actions/javascript/isDeployChecklistLocked/index.js +++ b/.github/actions/javascript/isDeployChecklistLocked/index.js @@ -23112,6 +23112,7 @@ var require_ExpensiMark = __commonJS({ var Constants = __importStar(require_CONST()); var UrlPatterns = __importStar(require_Url()); var Logger_1 = __importDefault(require_Logger()); + var tlds_1 = __importDefault(require_tlds()); var Utils = __importStar(require_utils()); var EXTRAS_DEFAULT = {}; var ASCII_DIGIT_START = "0".charCodeAt(0); @@ -23124,6 +23125,9 @@ var require_ExpensiMark = __commonJS({ var NON_BREAKING_SPACE_CODE = 160; var URL_PROTOCOLS = ["https://", "http://", "ftps://", "ftp://"]; var URL_CANDIDATE_PREFIX_CHARACTERS = "@_*~"; + var URL_TLD_LIST = tlds_1.default.toLowerCase().split("|"); + var URL_TLDS = new Set(URL_TLD_LIST); + var MAX_URL_TLD_LENGTH = Math.max(...URL_TLD_LIST.map((tld) => tld.length)); var PROTECTED_TAG_NAMES = /* @__PURE__ */ new Set(["a", "code", "pre", "video"]); var MARKDOWN_LINK_REGEX = new RegExp(`\\[((?:[^\\[\\]\\r\\n]*(?:\\[[^\\[\\]\\r\\n]*][^\\[\\]\\r\\n]*)*))]\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, "gi"); var MARKDOWN_IMAGE_REGEX = new RegExp(`\\!(?:\\[([^\\][]*(?:\\[[^\\][]*][^\\][]*)*)])?\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, "gi"); @@ -23169,6 +23173,9 @@ var require_ExpensiMark = __commonJS({ } return text.replace(regexp, replacement); } + function canUseCandidateScanning(text, shouldEscapeText) { + return shouldEscapeText || !text.includes("<") && !text.includes(">"); + } function isAsciiAlphaNumeric(character) { if (!character) { return false; @@ -23240,15 +23247,59 @@ var require_ExpensiMark = __commonJS({ } return URL_PROTOCOLS.find((protocol) => text.slice(position, position + protocol.length).toLowerCase() === protocol); } - function findHostnameEnd(text, hostnameStart, dotPosition) { - let hostnameEnd = dotPosition + 1; - while (hostnameEnd < text.length && (isAsciiAlphaNumeric(text[hostnameEnd]) || text[hostnameEnd] === "-")) { - hostnameEnd++; + function isValidHostnameLabel(text, start, end) { + if (start >= end || !isAsciiAlphaNumeric(text[start]) || !isAsciiAlphaNumeric(text[end - 1])) { + return false; } - if (hostnameStart === dotPosition || hostnameEnd === dotPosition + 1) { - return void 0; + for (let index = start + 1; index < end - 1; index++) { + if (!isAsciiAlphaNumeric(text[index]) && text[index] !== "-") { + return false; + } + } + return true; + } + function findHostnameStart(text, dotPosition) { + let hostnameStart = dotPosition; + let labelEnd = dotPosition; + while (labelEnd > 0) { + let rawLabelStart = labelEnd - 1; + while (rawLabelStart >= 0 && text[rawLabelStart] !== "." && isHostnameCharacter(text[rawLabelStart])) { + rawLabelStart--; + } + rawLabelStart++; + let labelStart = rawLabelStart; + while (labelStart < labelEnd && text[labelStart] === "-") { + labelStart++; + } + if (!isValidHostnameLabel(text, labelStart, labelEnd)) { + break; + } + hostnameStart = labelStart; + if (labelStart !== rawLabelStart) { + break; + } + const separatorPosition = labelStart - 1; + if (separatorPosition < 0 || text[separatorPosition] !== ".") { + break; + } + labelEnd = separatorPosition; } - return hostnameEnd; + return hostnameStart === dotPosition ? void 0 : hostnameStart; + } + function findKnownTldEnd(text, dotPosition) { + const maximumEnd = Math.min(text.length, dotPosition + 1 + MAX_URL_TLD_LENGTH); + for (let end = dotPosition + 2; end <= maximumEnd; end++) { + const currentCharacter = text[end - 1]; + if (!isAsciiAlphaNumeric(currentCharacter) && currentCharacter !== "-") { + break; + } + const nextCharacter = text[end]; + const hasValidBoundary = !nextCharacter || nextCharacter === ":" || nextCharacter === "_" || !isWordCharacter(nextCharacter); + if (hasValidBoundary && URL_TLDS.has(text.slice(dotPosition + 1, end).toLowerCase())) { + return end; + } + } + return void 0; } function extendUrlCandidateBoundaries(text, start, end) { let candidateStart = start; @@ -23261,24 +23312,65 @@ var require_ExpensiMark = __commonJS({ } return { start: candidateStart, end: candidateEnd }; } + function startsWithIgnoreCase(text, expected, position) { + return text.slice(position, position + expected.length).toLowerCase() === expected; + } + function filterUrlCandidatesBlockedByFollowingHtml(text, candidates) { + var _a; + if (candidates.length === 0 || !text.includes("<") && !text.includes(">")) { + return candidates; + } + const validCandidates = []; + let candidateIndex = candidates.length - 1; + let nextLessThan = text.length; + let nextGreaterThan = text.length; + let nextOpeningAnchor = text.length; + let nextClosingAnchor = text.length; + for (let index = text.length; index >= 0 && candidateIndex >= 0; index--) { + if (text[index] === "<") { + nextLessThan = index; + if (((_a = text[index + 1]) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === "a") { + nextOpeningAnchor = index; + } else if (startsWithIgnoreCase(text, "", index)) { + nextClosingAnchor = index; + } + } else if (text[index] === ">") { + nextGreaterThan = index; + } + while (candidateIndex >= 0 && candidates[candidateIndex].end === index) { + const firstHtmlBoundaryIsClosingTag = nextLessThan < nextGreaterThan && text.startsWith("", nextLessThan); + const firstTagIsProtectedClosingTag = startsWithIgnoreCase(text, "", nextLessThan) || startsWithIgnoreCase(text, "", nextLessThan); + const isBlockedByFollowingHtml = ( + // Mirrors `(?![^<]*>)`: reject when `>` appears before the next `<`. + nextGreaterThan < nextLessThan || // Mirrors `[^<>]*<\/(?!h1>)`: reject a later closing tag other than ``. + firstHtmlBoundaryIsClosingTag || // Mirrors `((?:(?!`: reject `` unless another `` appears first. + nextClosingAnchor < nextOpeningAnchor || // Mirrors `[^<]*(<\/pre>|<\/code>)`: reject a later protected closing tag. + firstTagIsProtectedClosingTag + ); + if (!isBlockedByFollowingHtml) { + validCandidates.push(candidates[candidateIndex]); + } + candidateIndex--; + } + } + return validCandidates.reverse(); + } function findUrlCandidates(text) { const candidates = []; const protectedTags = []; let index = 0; - let hostnameRunStart = 0; while (index < text.length) { if (text[index] === "<") { const nextIndex = updateProtectedTagStack(text, index, protectedTags); if (nextIndex === void 0) { - break; + index++; + continue; } index = nextIndex; - hostnameRunStart = index; continue; } if (protectedTags.length > 0) { index++; - hostnameRunStart = index; continue; } const matchedProtocol = getProtocolAt(text, index); @@ -23286,29 +23378,23 @@ var require_ExpensiMark = __commonJS({ const candidate2 = extendUrlCandidateBoundaries(text, index, index + matchedProtocol.length); candidates.push(candidate2); index = candidate2.end; - hostnameRunStart = candidate2.end; - continue; - } - if (!isHostnameCharacter(text[index])) { - hostnameRunStart = index + 1; - index++; continue; } if (text[index] !== ".") { index++; continue; } - const hostnameEnd = findHostnameEnd(text, hostnameRunStart, index); - if (hostnameEnd === void 0) { + const tldEnd = findKnownTldEnd(text, index); + const hostnameStart = tldEnd === void 0 ? void 0 : findHostnameStart(text, index); + if (tldEnd === void 0 || hostnameStart === void 0) { index++; continue; } - const candidate = extendUrlCandidateBoundaries(text, hostnameRunStart, hostnameEnd); + const candidate = extendUrlCandidateBoundaries(text, hostnameStart, tldEnd); candidates.push(candidate); index = candidate.end; - hostnameRunStart = candidate.end; } - return candidates; + return filterUrlCandidatesBlockedByFollowingHtml(text, candidates); } function replaceMarkdownCandidates(text, regexp, replacement, marker, canOpen) { if (!text.includes(marker)) { @@ -23381,6 +23467,14 @@ var require_ExpensiMark = __commonJS({ output.push(text.slice(outputStart)); return output.join(""); } + function processMarkdownRule(regex2, marker, canOpen) { + return (textToProcess, replacement, _shouldKeepRawInput, shouldEscapeText) => { + if (canUseCandidateScanning(textToProcess, shouldEscapeText)) { + return replaceMarkdownCandidates(textToProcess, regex2, replacement, marker, canOpen); + } + return replaceTextWithExtras(textToProcess, regex2, EXTRAS_DEFAULT, replacement); + }; + } function replaceBlockElementWithNewLine(htmlString) { let splitText = htmlString.replaceAll(/
> (|<\/div>||\n<\/comment>|<\/comment>|

|<\/h1>|

|<\/h2>|

|<\/h3>|

|<\/h4>|

|<\/h5>|
|<\/h6>|

|<\/p>|

  • |<\/li>)/gi, "
    > ").split(/|<\/div>||\n<\/comment>|<\/comment>|

    |<\/h1>|

    |<\/h2>|

    |<\/h3>|

    |<\/h4>|

    |<\/h5>|
    |<\/h6>|

    |<\/p>|

  • |<\/li>|
    |<\/blockquote>/); const stripHTML = (text) => str_1.default.stripHTML(text); @@ -23799,9 +23893,9 @@ var require_ExpensiMark = __commonJS({ */ { name: "autolink", - process: (textToProcess, replacement) => { + process: (textToProcess, replacement, _shouldKeepRawInput, shouldEscapeText) => { const regex2 = new RegExp(`(?![^<]*>|[^<>]*<\\/(?!h1>))([_*~]*?)${UrlPatterns.MARKDOWN_URL_REGEX}\\1(?!((?:(?!|[^<]*(<\\/pre>|<\\/code>))`, "gi"); - return this.modifyTextForUrlLinks(regex2, textToProcess, replacement, true); + return this.modifyTextForUrlLinks(regex2, textToProcess, replacement, canUseCandidateScanning(textToProcess, shouldEscapeText)); }, replacement: (_extras, _match, g1, g2) => { const href = str_1.default.sanitizeURL(g2); @@ -23870,7 +23964,8 @@ ${"
    ".repeat(i)}`, "\n"); name: "autoEmail", regex: new RegExp(`([^\\w'#%+-]|^)${Constants.CONST.REG_EXP.MARKDOWN_EMAIL}(?!((?:(?!|[^<>]*<\\/(?!em|h1|blockquote))`, "gim"), replacement: '$1$2', - rawInputReplacement: '$1$2' + rawInputReplacement: '$1$2', + shouldSkipProcessing: (textToCheck) => !textToCheck.includes("@") }, /** * This regex matches a short user mention in a string. @@ -23906,7 +24001,7 @@ ${"
    ".repeat(i)}`, "\n"); // \B will match everything that \b doesn't, so it works // for * and ~: https://www.rexegg.com/regex-boundaries.html#notb name: "bold", - process: (textToProcess, replacement) => replaceMarkdownCandidates(textToProcess, BOLD_MARKDOWN_REGEX, replacement, "*", canOpenBoldMarkdown), + process: processMarkdownRule(BOLD_MARKDOWN_REGEX, "*", canOpenBoldMarkdown), replacement: (_extras, match, g1, g2) => { if (g1.includes("_")) { return `${g1}${g2}`; @@ -23916,7 +24011,7 @@ ${"
    ".repeat(i)}`, "\n"); }, { name: "strikethrough", - process: (textToProcess, replacement) => replaceMarkdownCandidates(textToProcess, STRIKETHROUGH_MARKDOWN_REGEX, replacement, "~", canOpenStrikethroughMarkdown), + process: processMarkdownRule(STRIKETHROUGH_MARKDOWN_REGEX, "~", canOpenStrikethroughMarkdown), replacement: (_extras, match, g1) => g1.includes("") || containsNonPairTag(g1) ? match : `${g1}` }, { @@ -24285,7 +24380,7 @@ ${g2} } const replacement = shouldKeepRawInput && rule.rawInputReplacement ? rule.rawInputReplacement : rule.replacement; if ("process" in rule) { - replacedText = rule.process(replacedText, replacement, shouldKeepRawInput); + replacedText = rule.process(replacedText, replacement, shouldKeepRawInput, shouldEscapeText); } else { replacedText = replaceTextWithExtras(replacedText, rule.regex, extras, replacement); } diff --git a/.github/actions/javascript/proposalPoliceComment/index.js b/.github/actions/javascript/proposalPoliceComment/index.js index 6c66aba05ee9..126b34462df4 100644 --- a/.github/actions/javascript/proposalPoliceComment/index.js +++ b/.github/actions/javascript/proposalPoliceComment/index.js @@ -43926,6 +43926,7 @@ var require_ExpensiMark = __commonJS({ var Constants = __importStar(require_CONST()); var UrlPatterns = __importStar(require_Url()); var Logger_1 = __importDefault(require_Logger()); + var tlds_1 = __importDefault(require_tlds()); var Utils = __importStar(require_utils2()); var EXTRAS_DEFAULT = {}; var ASCII_DIGIT_START = "0".charCodeAt(0); @@ -43938,6 +43939,9 @@ var require_ExpensiMark = __commonJS({ var NON_BREAKING_SPACE_CODE = 160; var URL_PROTOCOLS = ["https://", "http://", "ftps://", "ftp://"]; var URL_CANDIDATE_PREFIX_CHARACTERS = "@_*~"; + var URL_TLD_LIST = tlds_1.default.toLowerCase().split("|"); + var URL_TLDS = new Set(URL_TLD_LIST); + var MAX_URL_TLD_LENGTH = Math.max(...URL_TLD_LIST.map((tld) => tld.length)); var PROTECTED_TAG_NAMES = /* @__PURE__ */ new Set(["a", "code", "pre", "video"]); var MARKDOWN_LINK_REGEX = new RegExp(`\\[((?:[^\\[\\]\\r\\n]*(?:\\[[^\\[\\]\\r\\n]*][^\\[\\]\\r\\n]*)*))]\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, "gi"); var MARKDOWN_IMAGE_REGEX = new RegExp(`\\!(?:\\[([^\\][]*(?:\\[[^\\][]*][^\\][]*)*)])?\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, "gi"); @@ -43983,6 +43987,9 @@ var require_ExpensiMark = __commonJS({ } return text.replace(regexp, replacement); } + function canUseCandidateScanning(text, shouldEscapeText) { + return shouldEscapeText || !text.includes("<") && !text.includes(">"); + } function isAsciiAlphaNumeric(character) { if (!character) { return false; @@ -44054,15 +44061,59 @@ var require_ExpensiMark = __commonJS({ } return URL_PROTOCOLS.find((protocol) => text.slice(position, position + protocol.length).toLowerCase() === protocol); } - function findHostnameEnd(text, hostnameStart, dotPosition) { - let hostnameEnd = dotPosition + 1; - while (hostnameEnd < text.length && (isAsciiAlphaNumeric(text[hostnameEnd]) || text[hostnameEnd] === "-")) { - hostnameEnd++; + function isValidHostnameLabel(text, start, end) { + if (start >= end || !isAsciiAlphaNumeric(text[start]) || !isAsciiAlphaNumeric(text[end - 1])) { + return false; } - if (hostnameStart === dotPosition || hostnameEnd === dotPosition + 1) { - return void 0; + for (let index = start + 1; index < end - 1; index++) { + if (!isAsciiAlphaNumeric(text[index]) && text[index] !== "-") { + return false; + } + } + return true; + } + function findHostnameStart(text, dotPosition) { + let hostnameStart = dotPosition; + let labelEnd = dotPosition; + while (labelEnd > 0) { + let rawLabelStart = labelEnd - 1; + while (rawLabelStart >= 0 && text[rawLabelStart] !== "." && isHostnameCharacter(text[rawLabelStart])) { + rawLabelStart--; + } + rawLabelStart++; + let labelStart = rawLabelStart; + while (labelStart < labelEnd && text[labelStart] === "-") { + labelStart++; + } + if (!isValidHostnameLabel(text, labelStart, labelEnd)) { + break; + } + hostnameStart = labelStart; + if (labelStart !== rawLabelStart) { + break; + } + const separatorPosition = labelStart - 1; + if (separatorPosition < 0 || text[separatorPosition] !== ".") { + break; + } + labelEnd = separatorPosition; } - return hostnameEnd; + return hostnameStart === dotPosition ? void 0 : hostnameStart; + } + function findKnownTldEnd(text, dotPosition) { + const maximumEnd = Math.min(text.length, dotPosition + 1 + MAX_URL_TLD_LENGTH); + for (let end = dotPosition + 2; end <= maximumEnd; end++) { + const currentCharacter = text[end - 1]; + if (!isAsciiAlphaNumeric(currentCharacter) && currentCharacter !== "-") { + break; + } + const nextCharacter = text[end]; + const hasValidBoundary = !nextCharacter || nextCharacter === ":" || nextCharacter === "_" || !isWordCharacter(nextCharacter); + if (hasValidBoundary && URL_TLDS.has(text.slice(dotPosition + 1, end).toLowerCase())) { + return end; + } + } + return void 0; } function extendUrlCandidateBoundaries(text, start, end) { let candidateStart = start; @@ -44075,24 +44126,65 @@ var require_ExpensiMark = __commonJS({ } return { start: candidateStart, end: candidateEnd }; } + function startsWithIgnoreCase(text, expected, position) { + return text.slice(position, position + expected.length).toLowerCase() === expected; + } + function filterUrlCandidatesBlockedByFollowingHtml(text, candidates) { + var _a3; + if (candidates.length === 0 || !text.includes("<") && !text.includes(">")) { + return candidates; + } + const validCandidates = []; + let candidateIndex = candidates.length - 1; + let nextLessThan = text.length; + let nextGreaterThan = text.length; + let nextOpeningAnchor = text.length; + let nextClosingAnchor = text.length; + for (let index = text.length; index >= 0 && candidateIndex >= 0; index--) { + if (text[index] === "<") { + nextLessThan = index; + if (((_a3 = text[index + 1]) === null || _a3 === void 0 ? void 0 : _a3.toLowerCase()) === "a") { + nextOpeningAnchor = index; + } else if (startsWithIgnoreCase(text, "", index)) { + nextClosingAnchor = index; + } + } else if (text[index] === ">") { + nextGreaterThan = index; + } + while (candidateIndex >= 0 && candidates[candidateIndex].end === index) { + const firstHtmlBoundaryIsClosingTag = nextLessThan < nextGreaterThan && text.startsWith("", nextLessThan); + const firstTagIsProtectedClosingTag = startsWithIgnoreCase(text, "", nextLessThan) || startsWithIgnoreCase(text, "", nextLessThan); + const isBlockedByFollowingHtml = ( + // Mirrors `(?![^<]*>)`: reject when `>` appears before the next `<`. + nextGreaterThan < nextLessThan || // Mirrors `[^<>]*<\/(?!h1>)`: reject a later closing tag other than `
  • `. + firstHtmlBoundaryIsClosingTag || // Mirrors `((?:(?!`: reject `` unless another `` appears first. + nextClosingAnchor < nextOpeningAnchor || // Mirrors `[^<]*(<\/pre>|<\/code>)`: reject a later protected closing tag. + firstTagIsProtectedClosingTag + ); + if (!isBlockedByFollowingHtml) { + validCandidates.push(candidates[candidateIndex]); + } + candidateIndex--; + } + } + return validCandidates.reverse(); + } function findUrlCandidates(text) { const candidates = []; const protectedTags = []; let index = 0; - let hostnameRunStart = 0; while (index < text.length) { if (text[index] === "<") { const nextIndex = updateProtectedTagStack(text, index, protectedTags); if (nextIndex === void 0) { - break; + index++; + continue; } index = nextIndex; - hostnameRunStart = index; continue; } if (protectedTags.length > 0) { index++; - hostnameRunStart = index; continue; } const matchedProtocol = getProtocolAt(text, index); @@ -44100,29 +44192,23 @@ var require_ExpensiMark = __commonJS({ const candidate2 = extendUrlCandidateBoundaries(text, index, index + matchedProtocol.length); candidates.push(candidate2); index = candidate2.end; - hostnameRunStart = candidate2.end; - continue; - } - if (!isHostnameCharacter(text[index])) { - hostnameRunStart = index + 1; - index++; continue; } if (text[index] !== ".") { index++; continue; } - const hostnameEnd = findHostnameEnd(text, hostnameRunStart, index); - if (hostnameEnd === void 0) { + const tldEnd = findKnownTldEnd(text, index); + const hostnameStart = tldEnd === void 0 ? void 0 : findHostnameStart(text, index); + if (tldEnd === void 0 || hostnameStart === void 0) { index++; continue; } - const candidate = extendUrlCandidateBoundaries(text, hostnameRunStart, hostnameEnd); + const candidate = extendUrlCandidateBoundaries(text, hostnameStart, tldEnd); candidates.push(candidate); index = candidate.end; - hostnameRunStart = candidate.end; } - return candidates; + return filterUrlCandidatesBlockedByFollowingHtml(text, candidates); } function replaceMarkdownCandidates(text, regexp, replacement, marker, canOpen) { if (!text.includes(marker)) { @@ -44195,6 +44281,14 @@ var require_ExpensiMark = __commonJS({ output.push(text.slice(outputStart)); return output.join(""); } + function processMarkdownRule(regex2, marker, canOpen) { + return (textToProcess, replacement, _shouldKeepRawInput, shouldEscapeText) => { + if (canUseCandidateScanning(textToProcess, shouldEscapeText)) { + return replaceMarkdownCandidates(textToProcess, regex2, replacement, marker, canOpen); + } + return replaceTextWithExtras(textToProcess, regex2, EXTRAS_DEFAULT, replacement); + }; + } function replaceBlockElementWithNewLine(htmlString) { let splitText = htmlString.replaceAll(/
    > (|<\/div>||\n<\/comment>|<\/comment>|

    |<\/h1>|

    |<\/h2>|

    |<\/h3>|

    |<\/h4>|

    |<\/h5>|
    |<\/h6>|

    |<\/p>|

  • |<\/li>)/gi, "
    > ").split(/|<\/div>||\n<\/comment>|<\/comment>|

    |<\/h1>|

    |<\/h2>|

    |<\/h3>|

    |<\/h4>|

    |<\/h5>|
    |<\/h6>|

    |<\/p>|

  • |<\/li>|
    |<\/blockquote>/); const stripHTML = (text) => str_1.default.stripHTML(text); @@ -44613,9 +44707,9 @@ var require_ExpensiMark = __commonJS({ */ { name: "autolink", - process: (textToProcess, replacement) => { + process: (textToProcess, replacement, _shouldKeepRawInput, shouldEscapeText) => { const regex2 = new RegExp(`(?![^<]*>|[^<>]*<\\/(?!h1>))([_*~]*?)${UrlPatterns.MARKDOWN_URL_REGEX}\\1(?!((?:(?!|[^<]*(<\\/pre>|<\\/code>))`, "gi"); - return this.modifyTextForUrlLinks(regex2, textToProcess, replacement, true); + return this.modifyTextForUrlLinks(regex2, textToProcess, replacement, canUseCandidateScanning(textToProcess, shouldEscapeText)); }, replacement: (_extras, _match, g1, g2) => { const href = str_1.default.sanitizeURL(g2); @@ -44684,7 +44778,8 @@ ${"
    ".repeat(i)}`, "\n"); name: "autoEmail", regex: new RegExp(`([^\\w'#%+-]|^)${Constants.CONST.REG_EXP.MARKDOWN_EMAIL}(?!((?:(?!|[^<>]*<\\/(?!em|h1|blockquote))`, "gim"), replacement: '$1$2', - rawInputReplacement: '$1$2' + rawInputReplacement: '$1$2', + shouldSkipProcessing: (textToCheck) => !textToCheck.includes("@") }, /** * This regex matches a short user mention in a string. @@ -44720,7 +44815,7 @@ ${"
    ".repeat(i)}`, "\n"); // \B will match everything that \b doesn't, so it works // for * and ~: https://www.rexegg.com/regex-boundaries.html#notb name: "bold", - process: (textToProcess, replacement) => replaceMarkdownCandidates(textToProcess, BOLD_MARKDOWN_REGEX, replacement, "*", canOpenBoldMarkdown), + process: processMarkdownRule(BOLD_MARKDOWN_REGEX, "*", canOpenBoldMarkdown), replacement: (_extras, match2, g1, g2) => { if (g1.includes("_")) { return `${g1}${g2}`; @@ -44730,7 +44825,7 @@ ${"
    ".repeat(i)}`, "\n"); }, { name: "strikethrough", - process: (textToProcess, replacement) => replaceMarkdownCandidates(textToProcess, STRIKETHROUGH_MARKDOWN_REGEX, replacement, "~", canOpenStrikethroughMarkdown), + process: processMarkdownRule(STRIKETHROUGH_MARKDOWN_REGEX, "~", canOpenStrikethroughMarkdown), replacement: (_extras, match2, g1) => g1.includes("") || containsNonPairTag(g1) ? match2 : `${g1}` }, { @@ -45099,7 +45194,7 @@ ${g2} } const replacement = shouldKeepRawInput && rule.rawInputReplacement ? rule.rawInputReplacement : rule.replacement; if ("process" in rule) { - replacedText = rule.process(replacedText, replacement, shouldKeepRawInput); + replacedText = rule.process(replacedText, replacement, shouldKeepRawInput, shouldEscapeText); } else { replacedText = replaceTextWithExtras(replacedText, rule.regex, extras, replacement); } From 1921e1d92adf4a4970f4c6e838fe57162e15c026 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Thu, 1 Oct 2026 14:22:44 +0200 Subject: [PATCH 3/4] fix tests --- jest/setup.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/jest/setup.ts b/jest/setup.ts index 44fbb6e59862..e51d539f9a45 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -38,6 +38,14 @@ Object.assign(global, {TextDecoder, TextEncoder}); // https://reactnavigation.org/docs/testing/#mocking-native-modules jest.mock('react-native/src/private/animated/NativeAnimatedHelper'); +// Gesture Handler 3 throws in __DEV__ when a GestureDetector has no GestureHandlerRootView above it. In the app every screen +// renders under the root view in App.tsx, but tests render screens on their own, so the context pretends a root view is always present. +// Jest resolves the package through `main` (lib/module), so the compiled file is the one to mock. +jest.mock('react-native-gesture-handler/lib/module/GestureHandlerRootViewContext', () => ({ + __esModule: true, + default: jest.requireActual('react').createContext(true), +})); + // Mock react-native-onyx storage layer because the SQLite storage layer doesn't work in jest. // Mocking this file in __mocks__ does not work because jest doesn't support mocking files that are not directly used in the testing project, // and we only want to mock the storage layer, not the whole Onyx module. @@ -144,6 +152,13 @@ jest.mock('react-native-reanimated', () => ({ // react-native-reanimated/mock leaves dispatchCommand out (see its own "ADD ME IF NEEDED" comment). forceClearInput // (src/libs/ComponentUtils) dispatches it from a UI-thread worklet, so any test exercising that path needs it mocked. dispatchCommand: jest.fn(), + // react-native-reanimated/mock also leaves out useComposedEventHandler, useHandler and isSharedValue, which Gesture Handler 3 + // calls from its detectors (including the ones behind its ScrollView / FlatList wrappers). These are hand-written stand-ins: + // useComposedEventHandler returns a no-op like the mocked useEvent, useHandler returns the same shape as the real hook, and + // isSharedValue recognizes the shared values created by the mocked useSharedValue (the real check relies on a flag they lack). + useComposedEventHandler: jest.fn(() => () => {}), + useHandler: jest.fn(() => ({context: {}, doDependenciesDiffer: false})), + isSharedValue: jest.fn((value: unknown) => typeof value === 'object' && value !== null && 'value' in value && 'get' in value && typeof value.get === 'function'), useReducedMotion: jest.fn, useScrollViewOffset: jest.fn(() => 0), useAnimatedRef: jest.fn(() => jest.fn()), From 61508f316d459762b7519109b3aaa1fc41ee701d Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Fri, 2 Oct 2026 11:05:32 +0200 Subject: [PATCH 4/4] fix comment --- jest/setup.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/jest/setup.ts b/jest/setup.ts index e51d539f9a45..9afaa8f219a1 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -153,9 +153,7 @@ jest.mock('react-native-reanimated', () => ({ // (src/libs/ComponentUtils) dispatches it from a UI-thread worklet, so any test exercising that path needs it mocked. dispatchCommand: jest.fn(), // react-native-reanimated/mock also leaves out useComposedEventHandler, useHandler and isSharedValue, which Gesture Handler 3 - // calls from its detectors (including the ones behind its ScrollView / FlatList wrappers). These are hand-written stand-ins: - // useComposedEventHandler returns a no-op like the mocked useEvent, useHandler returns the same shape as the real hook, and - // isSharedValue recognizes the shared values created by the mocked useSharedValue (the real check relies on a flag they lack). + // calls from its detectors (including the ones behind its ScrollView / FlatList wrappers). useComposedEventHandler: jest.fn(() => () => {}), useHandler: jest.fn(() => ({context: {}, doDependenciesDiffer: false})), isSharedValue: jest.fn((value: unknown) => typeof value === 'object' && value !== null && 'value' in value && 'get' in value && typeof value.get === 'function'),