Add keyboard tracking, automatic content scrolling, and a native toolbar with block type selection for the page editor. The toolbar appears above the keyboard with "+" button for block types and a dismiss button. Keyboard height is tracked via native events and communicated to the WebView for cursor scroll-into-view. Block type selection is inline (not modal) to prevent keyboard dismissal during interaction. - Track keyboard height in page screen with iOS/Android event listeners - Use KeyboardAvoidingView for proper layout when keyboard is visible - Add toolbar component with "+" (block types) and dismiss buttons - Implement inline block type selector (no modal to preserve keyboard) - Extend bridge protocol: keyboard.show/hide, editor.blur, block.command, editor.focus - Handle block command execution in WebView with correct TipTap node commands - Expose editor instance on window.__editorInstance for command execution - Add focus/blur callbacks to notify native of editor state - Send cursor position to scroll-into-view when keyboard shows - Increase ProseMirror padding-bottom from 40vh to 60vh for comfortable scrolling - Remove focus-restoration script that prevented cursor following Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Colanode Mobile
Status: Experimental and not production-ready. The mobile app exists to help the team iterate on Colanode's native experience, validate product decisions, and share as much logic as possible with the web and desktop clients.
Overview
Colanode Mobile is an Expo + React Native client for Colanode's local-first collaboration platform. It reuses the same shared data model and sync stack as the other apps:
@colanode/clientfor local database access, queries, mutations, and sync@colanode/corefor schemas, types, permissions, and business rules@colanode/crdtfor Yjs-backed collaborative documents@colanode/uifor shared editor behavior and UI building blocks where it makes sense
The app is mostly native React Native UI. The main exception is page editing: the rich-text editor runs as a small browser app inside a WebView, while the surrounding shell, routing, data access, and native integrations stay in React Native.
What Exists Today
- Authentication: server selection, OTP login, registration, password reset
- Local-first chat and channel browsing
- Spaces, folders, files, pages, and workspace navigation
- Page viewing and editing
- File picking and uploads from the device
- Workspace switching and basic settings flows
- Offline-aware reads from the local SQLite cache
Getting Started
Prerequisites
- Node.js (see the repository root for the expected version)
- npm
- Xcode for iOS Simulator and/or Android Studio for Android Emulator
- A running Colanode server
Install dependencies
From the repository root:
npm install
Run the mobile app
cd apps/mobile
npm run ios
cd apps/mobile
npm run android
cd apps/mobile
npm run start
The prestart, preios, and preandroid scripts prepare the embedded page editor asset before Expo starts.
High-Level Architecture
There are three important layers:
1. Native mobile shell
This is the real Expo / React Native app:
- file-based routes under
app/ - native screens and components under
src/ - Expo-backed services for filesystem, SQLite, paths, media picking, and platform integration
This layer owns:
- navigation
- auth flow
- workspace selection
- chat UI
- native sheets and action menus
- loading data from the local client services
- sending mutations and handling platform integrations
2. Shared local-first data layer
The mobile app follows the same local-first model as the desktop and web clients:
- Reads come from the local SQLite cache.
- Writes are applied locally first.
- Mutations sync to the server in the background.
- CRDT updates merge concurrent document edits.
- WebSocket-based synchronizers keep local state fresh.
This is why mobile can reuse so much from @colanode/client, @colanode/core, and @colanode/crdt instead of reimplementing business logic in the app.
3. Embedded page editor
Rich-text page editing is implemented as an embedded browser app loaded into a React Native WebView.
Why this exists:
- the shared editor stack is browser-oriented
- TipTap / ProseMirror depend on DOM APIs
- React Native cannot run DOM-based editor code directly
- a
WebViewgives the app a browser runtime inside the native screen
So the page editor is effectively "a tiny website shipped inside the app".
How Page Editing Works
The page screen is still a native screen, but the document body is rendered by the embedded editor.
High-level flow:
- The native page screen loads the page node, current document state, and pending document updates using the shared mobile hooks.
PageWebViewloads a local HTML asset intoreact-native-webview.- That HTML boots the embedded editor app, which uses
react-domand the shared editor stack. - The editor sends a
readymessage to the native shell. - The native shell sends initial state, theme, permissions, and document data into the WebView.
- When the editor needs queries or mutations, it sends bridge messages back to native.
- The native app executes those operations through the existing mediator and returns the results.
In practice:
- native owns the page screen, routing, theme, and app services
- the embedded browser app owns the DOM-based editor UI
- the bridge connects the two
Embedded Editor Setup
The editor lives in its own browser-oriented mini-app:
apps/mobile/webviews/editor/
That directory has its own:
package.jsontsconfig.jsonvite.config.tseditor.html- browser-side
src/
The editor is built into a single HTML file:
apps/mobile/webviews/editor/dist/editor.html
Then the copy script:
apps/mobile/scripts/copy-editor.js
copies the built file into:
apps/mobile/assets/editor-dist/editor.html
Metro bundles that copied HTML asset with the native app, and PageWebView loads it at runtime.
This split is intentional:
apps/mobile/src/**stays native-onlyapps/mobile/webviews/editor/**stays browser-only
Keeping those runtimes separate makes the build, TypeScript config, and mental model much clearer.
Project Structure
apps/mobile/
├── app/ # Expo Router routes
│ ├── (auth)/ # Authentication flows
│ └── (app)/ # Main application routes
├── src/
│ ├── components/ # Native React Native UI
│ ├── contexts/ # App, theme, workspace, and other providers
│ ├── hooks/ # Query, mutation, and platform hooks
│ ├── lib/ # Mobile-specific helpers
│ ├── mocks/ # Metro mocks for browser-only modules
│ └── services/ # Expo-backed implementations for client services
├── scripts/
│ └── copy-editor.js # Builds/copies the embedded editor asset
├── webviews/
│ └── editor/ # Browser app used inside the page WebView
├── assets/
│ └── editor-dist/ # Generated HTML asset loaded by the WebView
├── metro.config.js # Metro asset + module resolution customization
└── README.md
Key Mobile-Specific Pieces
Routing
Expo Router is used for file-based routing. The app is split into (auth) and (app) route groups, with nested stacks and tabs for the main experience.
Contexts and services
The mobile app wires shared client abstractions to Expo APIs:
- filesystem access
- local SQLite database access
- path resolution
- network state
- theming and workspace state
Queries and mutations
The hooks in src/hooks/ wrap the shared client mediator so screens and components can read from the local database and execute mutations without duplicating business logic.
Metro configuration
metro.config.js is customized so the mobile app can:
- bundle
.dbassets such as emoji/icon databases - bundle
.htmlassets for the embedded editor - mock browser-only dependencies that should not execute in the native runtime
Working On The Embedded Editor
If you are changing the page editor itself:
- browser-side editor code lives in
apps/mobile/webviews/editor/src/ - native hosting/bridge code lives in
apps/mobile/src/components/pages/page-webview.tsx - the native page screen lives in
apps/mobile/app/(app)/(spaces)/page/[pageId]/index.tsx
Useful commands:
cd apps/mobile/webviews/editor
npm run build
cd apps/mobile/webviews/editor
npm run compile
cd apps/mobile
npm run ios
Notes And Tradeoffs
- The app is still evolving quickly, so some flows are intentionally incomplete.
- The page title is managed by the native screen header, while the document body is managed by the embedded editor.
- Using a
WebViewfor the editor is a tradeoff: it adds build/bridge complexity, but it allows the app to reuse the mature shared web editor stack instead of maintaining a second rich-text editor implementation in pure React Native.