Skip to content

spike: evaluate react-native-live-markdown for message input#5533

Draft
janicduplessis wants to merge 52 commits intojanic/expo-54from
spike-live-markdown-input
Draft

spike: evaluate react-native-live-markdown for message input#5533
janicduplessis wants to merge 52 commits intojanic/expo-54from
spike-live-markdown-input

Conversation

@janicduplessis
Copy link
Copy Markdown
Contributor

@janicduplessis janicduplessis commented Feb 26, 2026

Summary

Spike evaluating two native input candidates to replace the WebView/TipTap editor on mobile. Both can coexist — they serve different purposes:

  • react-native-enriched (Software Mansion) → rich text mode. Native rich-text input with full formatting parity (bold, italic, strike, headings 1-6, ordered/unordered/checkbox lists, code blocks, blockquotes, links, inline images) via imperative API. Priority to ship first. Key advantage: native paste support works reliably (the current WebView/TipTap editor can't receive paste events from the native side).
  • @expensify/react-native-live-markdownmarkdown mode. Live syntax highlighting for raw markdown input. Limited formatting surface. In the future, react-native-enriched-markdown (also Software Mansion) is a better candidate here — same native engine, consistent behavior across both modes. Could also potentially be used as a read-only markdown renderer (the app currently uses a custom BlockRenderer/InlineRenderer system in PostContent/).

Both are wired into BigInput behind feature flags (enrichedInput, liveMarkdownInput) alongside the existing TipTap WebView editor.

What's included

  • EnrichedNoteInput — wrapper around EnrichedTextInput exposing a TlonEditorBridge-compatible adapter so existing toolbar actions work
  • FormattingToolbar — standalone toolbar component (no tentap dependency) with explicit editor + editorState props
  • LiveMarkdownInput — thin wrapper around MarkdownTextInput with parseExpensiMark
  • BigInput integration with three switchable editor modes and debug label
  • Inline image upload support (image picker + clipboard paste) for the enriched editor
  • Import path cleanups in packages/api and packages/shared (barrel → direct imports)
  • Updated feasibility report at docs/spikes/live-markdown-feasibility.md

Next steps

  1. Ship rich text mode first — implement HTML → Story conversion for react-native-enriched send path
  2. Mention support (ship-mention parsing, popup, insertion)
  3. Draft format migration (TipTap JSON → plain text or HTML)
  4. Paste reference extraction
  5. Evaluate react-native-enriched-markdown for markdown mode and potentially read-only rendering
  6. Remove WebView/TipTap dependencies once feature-complete

Limitations (spike-level)

  • No mention popup integration
  • No paste reference extraction
  • Draft format is plain text (existing TipTap JSON drafts won't auto-migrate)
  • Enriched editor send path needs HTML → Story conversion for production use
  • live-markdown peer deps not yet added

Tests

Not run (spike only). See feasibility report for manual validation steps.

Demo

Rich text:

Screen.Recording.2026-03-07.at.2.08.03.PM.mov

Markdown:

Screen.Recording.2026-03-07.at.2.19.33.PM.mov

Major version upgrades:
- Expo 52 → 54
- React Native 0.73 → 0.81
- React 18 → 19
- React Navigation 6 → 7
- PostHog and other dependencies updated for compatibility
- Delete AppDelegate.h and AppDelegate.mm
- Add AppDelegate.swift with equivalent functionality
- Remove main.m (Swift uses @main attribute)
- Update Xcode project configuration
- Convert MainActivity.java to MainActivity.kt
- Convert MainApplication.java to MainApplication.kt
- Preserve all functionality including window insets handling
Mobile:
- Update app.config.ts for Expo 54 plugins
- Update babel.config.js for new architecture
- Update metro.config.js with new resolver config
- Update Android gradle and build configuration
- Update iOS Info.plist and bridging headers

Web:
- Update Vite config for React Native Worklets
Expo 54 supports TypeScript entry files
- Move cosmos.imports.ts from root to apps/tlon-mobile/
- Add cosmos.config.json to tlon-mobile app
- Remove root cosmos.imports.ts
- Refactor navigation logging to work with React Navigation v7
- Remove unnecessary React imports (React 19 auto-imports)
- Update TypeScript config for React 19
- Fix navigation context usage
- Refactor PostHog initialization to synchronous pattern
- Update telemetry provider for new PostHog API
- Fix type compatibility with new PostHog version
- Update Cosmos exports for ES module compatibility
- Fix TypeScript errors in signup context and API calls
- Update contacts API for new Expo Contacts module
- Fix attestation domain types
- Remove expo@52.0.47.patch (no longer needed in Expo 54)
- Add .expo directory to .gitignore
- Update pnpm-lock.yaml with new dependency versions
- Update iOS Podfile.lock
- Update Android gradle.lockfile
- Rename patch from @tamagui__sheet@1.126.12 to @tamagui__sheet@1.126.18
- Update pnpm.patchedDependencies reference
- Update lockfiles with new dependency resolutions
- Minor dependency version bumps from pnpm install
- Replace 'react-native bundle' with 'expo export:embed'
- Remove outdated entry-file path and dev flag
- Expo CLI automatically handles entry point detection
- Replace all imports with expo-clipboard
- Update Clipboard.setString() to Clipboard.setStringAsync()
- Update Clipboard.getString() to Clipboard.getStringAsync()
- Update Clipboard.hasImage() to Clipboard.hasImageAsync()
- Update Clipboard.getImagePNG/JPG() to Clipboard.getImageAsync({ format })
- Update test mocks to use expo-clipboard mock
- Make callbacks async where needed for await usage
- Revert @tamagui/sheet patch from 1.126.18 to 1.126.12
- Lock all Tamagui dependencies to exact 1.126.12 (no range)
- Update all package.json files to use strict version (removed ~)
- Regenerate lockfiles with clean install
- All Tamagui packages now on exact 1.126.12
- Change Tamagui versions from exact 1.126.12 to ~1.126.12 range
  - @tamagui/react-native-media-driver
  - @tamagui/babel-plugin
  - @tamagui/vite-plugin
- Add @react-native-picker/picker@^2.11.4 as explicit dependency
  - Was peer dependency of react-native-phone-input
  - Now explicitly managed for Expo 54 compatibility
- Update Android build scripts: productionDebugOptimized → productionDebug
- Regenerate lockfiles
- TelemetryProvider: Remove null check, use disabled option for tests
- AppInfoScreen: Format upload logs button
- tsconfig: Remove expo/tsconfig.base extend, add JSDoc comment
Changes applied:
- Android: Set DEFAULT_INTERVAL_MINUTES to 20 minutes (was 24 hours)
- iOS: Set intervalSeconds to 15 minutes (was 12 hours)
- iOS: Change from BGProcessingTaskRequest to BGAppRefreshTaskRequest
- iOS: Change background mode check from 'processing' to 'fetch'
- iOS: Set earliestBeginDate to nil for immediate scheduling
- iOS: Add debug print statements for task lifecycle
- Remove network/power requirements from iOS task requests

Removed obsolete patches:
- expo-background-task@0.1.4.patch
- @react-navigation__drawer@6.7.2.patch
- expo-localization@16.0.1.patch
- react-native-reanimated@3.8.1.patch
- react-native@0.73.4.patch

These were automatically removed as those versions are no longer installed.
- Upgrade @tamagui/* packages from ~1.126.12 to ~2.0.0-rc.0
- Remove @tamagui/sheet patch (no longer needed in v2)

Tamagui v2 API changes:
- Replace Stack with View/YStack (Stack removed in v2)
- Replace animation prop with transition on animated components
- Replace tag prop with render (renamed in v2)
- Replace editable prop with readOnly on TextArea/Input
- Replace onHoverIn/onHoverOut with onMouseEnter/onMouseLeave
- Remove textWrap/wordWrap non-existent props
- Remove fontWeight from Button (stack-based, not text)
- Fix TransitionProp by adding medium/slow animation keys to config

React 19 type compatibility:
- JSX.Element → ReactNode/ReactElement for children props
- RefObject<T> → RefObject<T | null> for ref nullability
- Fix generic types for forwardRef components

React Native API updates:
- BackHandler.addEventListener returns NativeEventSubscription
- headerBackTitleVisible → headerBackButtonDisplayMode: 'minimal'
- expo-contacts types: Contact → ExistingContact

Component fixes:
- Update ButtonContext/FloatingActionButton to use useStyledContext()
- Fix Pressable navigation props (href/action typing)
- Fix OverflowTriggerButton forwardRef generic type
- Cast web-only outlineStyle in BareChatInput
- Provide defaultTheme fallback in BaseProviderStack
- Remove duplicate clipboard image format attempt
- Update react-native-country-codes-picker patch for JSX.Element
- Add fontFamily to ListItemTitle for consistency
- Add position="relative" for absolute positioning contexts
Allow overriding the default hover background color by accepting
a hoverStyle prop, defaulting to the original behavior if not provided.
- Fix useRef initialization to explicitly pass undefined
- Update BackHandler event listener cleanup to use new API
- Fix ts-expect-error placement in PhoneNumberInput
- Add type cast for RawBottomSheetTextInput ref
Replace NodeJS.Timeout type with ReturnType<typeof setTimeout> to resolve
compilation error when DOM types are present in tsconfig. This makes the
type declaration work correctly in both DOM and Node.js environments.
- Add preview.proxy to vite config so vite preview proxies API requests
  to the Urbit ship (the urbit plugin only sets server.proxy for dev)
- Add resolveId hook to reactNativeWebPlugin to prefer .web.ts index
  files for directory imports in node_modules (fixes Rollup resolving
  expo-modules-core polyfill/index.ts noop instead of index.web.ts)
- Add explicit expo-polyfill.ts imported first in main.tsx to ensure
  globalThis.expo is set up before any expo modules load
- Add envPrefix ['VITE_', 'TAMAGUI_'] to vite config to prevent
  Tamagui plugin from overriding Vite's default VITE_ prefix
- Fix ActionSheet dialog: add disableRemoveScroll to prevent z-index
  stacking issues, change ScrollView flex to flexShrink to fix
  0-height content rendering
- Patch @tamagui/dialog to remove render: 'dialog' on DialogPortalFrame
  which causes stacking context issues with native <dialog> element
- Fix GroupTypeCard text overlap by disabling text trimming margins
The previous fix only addressed ActionSheet, but ConfirmDialog (used for
delete group confirmation) had the same issue. Add pointerEvents="none"
to Dialog.Overlay in both ActionSheet and ConfirmDialog so overlays never
intercept clicks, regardless of whether the pnpm patch is applied.

Also make e2e test cleanup more robust by pressing Escape to dismiss any
lingering dialogs before attempting to interact with background elements.
Fixes any-ascii ESM crash during Tamagui static extraction on EAS.
Node 22.12.0 had buggy require(esm) interop that caused uncatchable
errors with esbuild-register.
React Nav v7 changed navigate() to push new screens instead of popping
back to existing ones in a stack. This caused duplicate ChannelRoot
screens (and 2 MessageInput textareas) when navigating back to a channel
from GroupSettings via the sidebar.

- Add pop: true to getDesktopChannelRoute nested params so navigate()
  pops back to existing ChannelRoot instead of pushing a new one
- Add pop: true to useNavigateBackFromPost desktop path
- Fix navigateToGroupSettings to navigate directly to Channel >
  GroupSettings in a single call instead of the broken 2-step approach
  (navigateToGroup + setTimeout with stale navigation ref)
…ade issues

React Navigation v7 changed how nested navigator state is handled:
in v6, navigating with `params: { screen, params }` would reset the
nested navigator state. In v7, it dispatches `CommonActions.navigate()`
which pushes onto the existing stack, causing stale screens to
accumulate.

The fix uses `params: { state: { routes: [...], index: 0 } }` which
triggers `CommonActions.reset()` to fully replace the nested state,
matching v6 behavior. This is applied to all GroupSettings stack
navigations.

Also adds `pop: true` to navigate calls that should pop back to
existing screens (restoring v6 popTo behavior), and fixes several
other issues from the Expo 54 / RN v7 upgrade:

- Port @tamagui/sheet patch to v2.0.0-rc.0
- Move ForwardPostSheetProvider inside NavigationContainer
- Fix ActionSheet Popover z-index behind modals
- Add navigation state debug logging
- Refactor e2e tests to use navigateBack helper
NativeStack on web renders all stacked screens in the DOM, creating
many HeaderBackButton elements. The old helper only checked indices
[2, 1, 0] which missed the correct button when 5+ screens were stacked.

Now dynamically counts all back buttons and clicks the last visible one
(the topmost screen). Also fix roles-management test back-navigation
loops to stop once they reach the group channels view instead of
blindly navigating back a fixed number of times.
- Sheet `animation` prop renamed to `transition`
- Remove `estimatedItemSize` (FlashList 2.0 auto-calculates)
- Remove unused `editorIsFocused` prop from DetailView
- Fix expo-sensors version for Expo 54
- Update sheet patch for new version
@janicduplessis janicduplessis changed the base branch from develop to janic/expo-54 March 4, 2026 20:14
@janicduplessis janicduplessis force-pushed the spike-live-markdown-input branch from 52c19de to 156d323 Compare March 4, 2026 20:15
Change packages/api and packages/shared db files to import from
specific module paths instead of barrel re-exports to avoid circular
dependency issues with Metro/Vite.
…bility report

Add a second native input candidate using Software Mansion's
react-native-enriched, which provides full formatting parity (bold,
italic, strike, headings 1-6, lists, code blocks, blockquotes, links,
inline images) via imperative API.

- EnrichedNoteInput wraps EnrichedTextInput with a TlonEditorBridge adapter
- FormattingToolbar is a standalone toolbar (no tentap dependency)
- BigInput supports three editor modes behind feature flags
- Import path cleanups in packages/api and packages/shared
- Updated feasibility report with comparison and recommendation
…er notes

- Both libraries can coexist: react-native-enriched for rich text,
  markdown mode as an alternative
- Note react-native-enriched-markdown as future markdown input candidate
- Document current custom PostContent renderer and potential for
  enriched-markdown as read-only renderer
- Prioritize rich text mode (react-native-enriched) as first to ship
…itor

The current WebView/TipTap editor can't reliably receive paste events
from the native side. react-native-enriched solves this with a native
onPasteImages callback. Added WebView column to comparison table.
@janicduplessis janicduplessis force-pushed the spike-live-markdown-input branch from ba44e31 to 3bf36e4 Compare March 6, 2026 03:43
@janicduplessis janicduplessis force-pushed the janic/expo-54 branch 3 times, most recently from 9c81e3b to a988736 Compare March 21, 2026 06:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant