@poolse/react-native 2.12.1 → 2.13.1

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.
package/README.md CHANGED
@@ -1,238 +1,238 @@
1
- # `@poolse/react-native`
2
-
3
- Drop-in React Native chat SDK for Expo and bare React Native. `<ConversationView>` and `<PoolseInbox>` mount a complete chat surface with camera, attachments, markdown, typing indicators, reactions, threads, and AppState lifecycle baked in. Mirrors [`@poolse/react-ui`](https://www.npmjs.com/package/@poolse/react-ui) — same component names, same prop shapes — but implemented against native primitives (`View` / `Text` / `FlatList` / `TextInput` / `Pressable` / `Modal` / `KeyboardAvoidingView`).
4
-
5
- > **MIT-licensed alternative to `sendbird-chat-react-native`, `stream-chat-react-native`, and CometChat React Native UIKit.** Works out of the box with Expo (SDK 50+) and bare React Native (≥ 0.73). JS theme tokens, no native module setup beyond standard peer deps.
6
-
7
- Sits on top of [`@poolse/react`](https://www.npmjs.com/package/@poolse/react) (hooks) and [`@poolse/sdk`](https://www.npmjs.com/package/@poolse/sdk) (client).
8
-
9
- ## Install
10
-
11
- ```bash
12
- npx expo install \
13
- @poolse/sdk @poolse/react @poolse/react-native \
14
- react-native-svg \
15
- react-native-markdown-display \
16
- react-native-safe-area-context \
17
- expo-clipboard \
18
- expo-image-picker \
19
- expo-document-picker \
20
- expo-image-manipulator
21
- ```
22
-
23
- Use `npx expo install` (not plain `npm install`) so each module is
24
- pinned to a version compatible with your Expo SDK. On bare React
25
- Native without the Expo CLI, swap it for `npm install` and pin
26
- versions yourself.
27
-
28
- **All peer dependencies are required.** The chat surface eagerly
29
- imports each one at module-load time (Metro can't follow `try /
30
- require`, so we don't try — see `2.1.1` release notes). If any peer
31
- is missing the bundle blows up the first time you mount
32
- `<ConversationView>` / `<PoolseInbox>` with "Requiring unknown
33
- module …".
34
-
35
- | Peer | What it powers |
36
- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
37
- | `react-native-svg` | All `<PoolseIcon>` glyphs (send, reply, reactions, chevrons). |
38
- | `react-native-markdown-display` | Bold / italic / code / clickable links inside message bubbles. Disable with `markdown={false}` on `<ConversationView>` if you don't want it — the import still ships in the bundle. |
39
- | `react-native-safe-area-context` | Composer bottom inset when the keyboard is closed, and the `keyboardVerticalOffset` math `<PoolseInbox>` does for its header. Falls back to hardcoded iOS 50/34 constants if missing — usable, but wrong on devices with non-standard insets. |
40
- | `expo-clipboard` | The Copy action in the long-press message menu. |
41
- | `expo-image-picker` | Camera + Photo-library entries in `<AttachmentPicker>`. |
42
- | `expo-document-picker` | File entry in `<AttachmentPicker>`. |
43
- | `expo-image-manipulator` | WebP resize + compress on picked photos before upload (keeps payloads under ~250 KB). |
44
-
45
- **iOS Info.plist** (`app.json` → `ios.infoPlist`):
46
-
47
- ```json
48
- {
49
- "NSCameraUsageDescription": "Take photos to send in chat.",
50
- "NSPhotoLibraryUsageDescription": "Pick photos to send in chat."
51
- }
52
- ```
53
-
54
- Pass `camera={false}` to `<AttachmentPicker>` if you want to skip
55
- the camera entry (and Apple's review burden for `NSCameraUsageDescription`).
56
-
57
- **App root** — wrap once with `<SafeAreaProvider>` and `<PoolseProvider>`:
58
-
59
- ```tsx
60
- import { SafeAreaProvider } from 'react-native-safe-area-context';
61
- import { PoolseProvider } from '@poolse/react';
62
-
63
- export default function App() {
64
- return (
65
- <SafeAreaProvider>
66
- <PoolseProvider config={{ getToken: ..., apiUrl: ... }}>
67
- <YourScreens />
68
- </PoolseProvider>
69
- </SafeAreaProvider>
70
- );
71
- }
72
- ```
73
-
74
- ## Quick start
75
-
76
- The drop-in inbox — list + detail with a slide animation between them, edge-swipe back, plus built-in pickers when you provide a user directory:
77
-
78
- ```tsx
79
- import { PoolseProvider } from '@poolse/react';
80
- import { PoolseInbox, PoolseTheme } from '@poolse/react-native';
81
-
82
- const users = [
83
- { externalId: 'alice', name: 'Alice', avatarUrl: 'https://…' },
84
- { externalId: 'bob', name: 'Bob', avatarUrl: 'https://…' },
85
- ];
86
-
87
- export default function InboxScreen() {
88
- return (
89
- <PoolseProvider config={{ getToken, userResolver }}>
90
- <PoolseTheme>
91
- <PoolseInbox
92
- title="Chats"
93
- users={users}
94
- labelFor={(externalId) =>
95
- users.find((u) => u.externalId === externalId)?.name ?? externalId
96
- }
97
- avatarFor={(externalId) =>
98
- users.find((u) => u.externalId === externalId)?.avatarUrl ?? null
99
- }
100
- />
101
- </PoolseTheme>
102
- </PoolseProvider>
103
- );
104
- }
105
- ```
106
-
107
- If you'd rather render a single conversation (your app already owns the navigation), use `<ConversationView>` directly:
108
-
109
- ```tsx
110
- import { PoolseProvider } from '@poolse/react';
111
- import { PoolseTheme, ConversationView } from '@poolse/react-native';
112
-
113
- export default function ChatScreen({ conversationId }: { conversationId: string }) {
114
- return (
115
- <PoolseProvider config={{ getToken, userResolver }}>
116
- <PoolseTheme>
117
- <ConversationView conversationId={conversationId} />
118
- </PoolseTheme>
119
- </PoolseProvider>
120
- );
121
- }
122
- ```
123
-
124
- The same `<PoolseProvider>` you use on web. Wrap it once at the app root if you have multiple chat screens. Drop the `<PoolseTheme>` wrapper if you don't need to override the brand defaults — every component has a fallback theme baked in.
125
-
126
- ## What ships
127
-
128
- | Component | What it does |
129
- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
130
- | `<PoolseInbox>` | Full inbox shell — list + detail with an iOS-style slide animation between them, left-edge-swipe-back to return to the list, built-in `New chat` / `New group` picker sheets when `users` is provided. Imperative ref API: `open`, `openDirect`, `openGroup`, `promptNewChat`, `promptNewGroup`, `close`. |
131
- | `<ChatHeader>` | Title bar — direct chats resolve the other member's name + avatar via `labelFor` / `avatarFor`, groups show member count, presence-driven green dot. Optional `onBack` button. |
132
- | `<ConversationView>` | Composed chat surface — message list + composer + typing strip + thread modal. Single `KeyboardAvoidingView` covers the whole thing so the composer stays above the keyboard on both platforms. |
133
- | `<ThreadView>` | Bottom-sheet modal for thread replies. Reactions + attachments + pull-down-to-close via a 60-pt header drag strip. Composer reuses the same `UploadProvider` queue as the picker. |
134
- | `<GroupDetailsSheet>` | Bottom sheet showing a group's name, avatar, and member roster with presence. Opened from `<ChatHeader>`'s members button. |
135
- | `<MessageBubble>` / `<EditableMessageBubble>` | Single message render — body, meta, read receipt, quoted card, in-bubble attachments. |
136
- | `<MessageRow>` | Bubble + alignment + swipe-to-reply (capture-phase, 1:1 finger follow, sqrt-damped past 50pt threshold). |
137
- | `<MessageList>` | Inverted `FlatList` wrapper — stick-to-bottom, hold-position, auto-load-more. |
138
- | `<MessageComposer>` | Text input with auto-grow, send button, attach button, quote chip. |
139
- | `<MessageActions>` | Long-press menu (reply / quote / edit / delete / copy / react). |
140
- | `<TypingIndicator>` | Animated three-dot indicator + labels. |
141
- | `<ConversationList>` | `FlatList` of conversation rows. Direct rows pull the other member's name + avatar via `labelFor` / `avatarFor`; preview reads `last_message_preview` updated in realtime by the SDK's `conversation:updated` subscription. Unread badges. |
142
- | `<MemberList>` | Member roster with avatars + remove action. |
143
- | `<MentionInput>` | `TextInput` + autocomplete popover. |
144
- | `<ReactionStrip>` / `<ReactionPicker>` | Reaction summary + emoji picker sheet. |
145
- | `<AttachmentPreview>` / `<AttachmentPicker>` | Image or file card. Picker wraps `expo-image-picker` + `expo-document-picker`. |
146
- | `<UploadQueueStrip>` | Pending uploads with cancel. |
147
- | `<UserName>` / `useDisplayName(externalId)` | Display name resolution via the SDK's `userResolver`. |
148
- | `<Avatar>` | Initials + URL fallback, stable color per user. |
149
- | `<PoolseIcon>` / `<PoolseLogo>` | Brand chrome via `react-native-svg`. |
150
-
151
- ## Theming
152
-
153
- JS object via `<PoolseTheme theme={overrides}>`. The full token
154
- surface mirrors the web `--poolse-*` CSS variables, grouped by
155
- purpose:
156
-
157
- ```tsx
158
- <PoolseTheme
159
- theme={{
160
- colors: {
161
- brand: '#ff5733',
162
- brandSoft: '#ffe9e3',
163
- onBrand: '#ffffff',
164
- },
165
- radii: {
166
- bubble: 18,
167
- },
168
- }}
169
- >
170
- {/* ... */}
171
- </PoolseTheme>
172
- ```
173
-
174
- Pass nothing to use the brand defaults. Pass `mode="dark"` to swap
175
- to the warm dark palette. Deep-merges your overrides on top —
176
- unspecified tokens keep their default.
177
-
178
- ## Lifecycle
179
-
180
- ```tsx
181
- import { useAppStateLifecycle } from '@poolse/react-native';
182
-
183
- function App() {
184
- useAppStateLifecycle();
185
- return <PoolseProvider config={config}>{/* ... */}</PoolseProvider>;
186
- }
187
- ```
188
-
189
- Closes the SDK WebSocket cleanly when the app backgrounds (iOS /
190
- Android suspend sockets aggressively — keeping them open wastes
191
- battery and produces no benefit). Reopens on foreground and triggers
192
- a tail refetch on the currently-mounted `<ConversationView>` so the
193
- user lands on a fresh state instead of catching up via stale
194
- realtime.
195
-
196
- Pair with push notifications on your backend (subscribe to
197
- `message.created` / `mention.created` webhooks, fan out via APNs /
198
- FCM / Expo's push service) — that's how the user learns about new
199
- messages while the app is closed.
200
-
201
- ## Identity model
202
-
203
- Same SDK 2.0 contract as the web side. All component props that
204
- deal with users are keyed by your `external_id`:
205
-
206
- - `useUser(externalId)`, `useDisplayName(externalId)`
207
- - `<UserName externalId={message.sender_external_id} />`
208
- - `<MemberList labelFor={(externalId) => ...} avatarFor={(externalId) => ...} onlineExternalIds={set} />`
209
- - `<MessageBubble>` / `<MessageComposer>` / `<MentionInput>` / `<TypingIndicator>` — every `labelFor` is `(externalId) => string`.
210
-
211
- The wire still carries `user_id` (the poolse uuid) alongside
212
- `external_id`, but day-to-day app code never has to touch it.
213
-
214
- ## Differences from `@poolse/react-ui`
215
-
216
- | Concern | Web (`react-ui`) | Native (`react-native`) |
217
- | ------------------- | ----------------------------- | -------------------------------------------- |
218
- | Styling | CSS variables in `styles.css` | JS theme via `<PoolseTheme>` |
219
- | Action menu trigger | Hover | Long-press |
220
- | Reaction picker | Inline popover | Modal sheet |
221
- | Mention picker | Inline dropdown | Modal sheet |
222
- | Attachment picker | `<input type=file>` | `expo-image-picker` + `expo-document-picker` |
223
- | Message list | Reversed CSS flexbox | Inverted `FlatList` |
224
- | Markdown | `react-markdown` | Plain text |
225
-
226
- Everything else — feature flags, prop names, callback shapes, hook signatures — is intentionally identical so a web codebase ports across with mostly mechanical changes.
227
-
228
- ## Links
229
-
230
- - Docs: [poolse.dev/docs/sdk/react-native](https://poolse.dev/docs/sdk/react-native)
231
- - Source: [github.com/poolse-hq/js-sdk](https://github.com/poolse-hq/js-sdk/tree/main/packages/react-native)
232
- - Web sibling: [`@poolse/react-ui`](https://www.npmjs.com/package/@poolse/react-ui)
233
- - Hooks (used internally): [`@poolse/react`](https://www.npmjs.com/package/@poolse/react)
234
- - Core client: [`@poolse/sdk`](https://www.npmjs.com/package/@poolse/sdk)
235
-
236
- ## License
237
-
238
- MIT
1
+ # `@poolse/react-native`
2
+
3
+ Drop-in React Native chat SDK for Expo and bare React Native. `<ConversationView>` and `<PoolseInbox>` mount a complete chat surface with camera, attachments, markdown, typing indicators, reactions, threads, and AppState lifecycle baked in. Mirrors [`@poolse/react-ui`](https://www.npmjs.com/package/@poolse/react-ui) — same component names, same prop shapes — but implemented against native primitives (`View` / `Text` / `FlatList` / `TextInput` / `Pressable` / `Modal` / `KeyboardAvoidingView`).
4
+
5
+ > **MIT-licensed alternative to `sendbird-chat-react-native`, `stream-chat-react-native`, and CometChat React Native UIKit.** Works out of the box with Expo (SDK 50+) and bare React Native (≥ 0.73). JS theme tokens, no native module setup beyond standard peer deps.
6
+
7
+ Sits on top of [`@poolse/react`](https://www.npmjs.com/package/@poolse/react) (hooks) and [`@poolse/sdk`](https://www.npmjs.com/package/@poolse/sdk) (client).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npx expo install \
13
+ @poolse/sdk @poolse/react @poolse/react-native \
14
+ react-native-svg \
15
+ react-native-markdown-display \
16
+ react-native-safe-area-context \
17
+ expo-clipboard \
18
+ expo-image-picker \
19
+ expo-document-picker \
20
+ expo-image-manipulator
21
+ ```
22
+
23
+ Use `npx expo install` (not plain `npm install`) so each module is
24
+ pinned to a version compatible with your Expo SDK. On bare React
25
+ Native without the Expo CLI, swap it for `npm install` and pin
26
+ versions yourself.
27
+
28
+ **All peer dependencies are required.** The chat surface eagerly
29
+ imports each one at module-load time (Metro can't follow `try /
30
+ require`, so we don't try — see `2.1.1` release notes). If any peer
31
+ is missing the bundle blows up the first time you mount
32
+ `<ConversationView>` / `<PoolseInbox>` with "Requiring unknown
33
+ module …".
34
+
35
+ | Peer | What it powers |
36
+ | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
37
+ | `react-native-svg` | All `<PoolseIcon>` glyphs (send, reply, reactions, chevrons). |
38
+ | `react-native-markdown-display` | Bold / italic / code / clickable links inside message bubbles. Disable with `markdown={false}` on `<ConversationView>` if you don't want it — the import still ships in the bundle. |
39
+ | `react-native-safe-area-context` | Composer bottom inset when the keyboard is closed, and the `keyboardVerticalOffset` math `<PoolseInbox>` does for its header. Falls back to hardcoded iOS 50/34 constants if missing — usable, but wrong on devices with non-standard insets. |
40
+ | `expo-clipboard` | The Copy action in the long-press message menu. |
41
+ | `expo-image-picker` | Camera + Photo-library entries in `<AttachmentPicker>`. |
42
+ | `expo-document-picker` | File entry in `<AttachmentPicker>`. |
43
+ | `expo-image-manipulator` | WebP resize + compress on picked photos before upload (keeps payloads under ~250 KB). |
44
+
45
+ **iOS Info.plist** (`app.json` → `ios.infoPlist`):
46
+
47
+ ```json
48
+ {
49
+ "NSCameraUsageDescription": "Take photos to send in chat.",
50
+ "NSPhotoLibraryUsageDescription": "Pick photos to send in chat."
51
+ }
52
+ ```
53
+
54
+ Pass `camera={false}` to `<AttachmentPicker>` if you want to skip
55
+ the camera entry (and Apple's review burden for `NSCameraUsageDescription`).
56
+
57
+ **App root** — wrap once with `<SafeAreaProvider>` and `<PoolseProvider>`:
58
+
59
+ ```tsx
60
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
61
+ import { PoolseProvider } from '@poolse/react';
62
+
63
+ export default function App() {
64
+ return (
65
+ <SafeAreaProvider>
66
+ <PoolseProvider config={{ getToken: ..., apiUrl: ... }}>
67
+ <YourScreens />
68
+ </PoolseProvider>
69
+ </SafeAreaProvider>
70
+ );
71
+ }
72
+ ```
73
+
74
+ ## Quick start
75
+
76
+ The drop-in inbox — list + detail with a slide animation between them, edge-swipe back, plus built-in pickers when you provide a user directory:
77
+
78
+ ```tsx
79
+ import { PoolseProvider } from '@poolse/react';
80
+ import { PoolseInbox, PoolseTheme } from '@poolse/react-native';
81
+
82
+ const users = [
83
+ { externalId: 'alice', name: 'Alice', avatarUrl: 'https://…' },
84
+ { externalId: 'bob', name: 'Bob', avatarUrl: 'https://…' },
85
+ ];
86
+
87
+ export default function InboxScreen() {
88
+ return (
89
+ <PoolseProvider config={{ getToken, userResolver }}>
90
+ <PoolseTheme>
91
+ <PoolseInbox
92
+ title="Chats"
93
+ users={users}
94
+ labelFor={(externalId) =>
95
+ users.find((u) => u.externalId === externalId)?.name ?? externalId
96
+ }
97
+ avatarFor={(externalId) =>
98
+ users.find((u) => u.externalId === externalId)?.avatarUrl ?? null
99
+ }
100
+ />
101
+ </PoolseTheme>
102
+ </PoolseProvider>
103
+ );
104
+ }
105
+ ```
106
+
107
+ If you'd rather render a single conversation (your app already owns the navigation), use `<ConversationView>` directly:
108
+
109
+ ```tsx
110
+ import { PoolseProvider } from '@poolse/react';
111
+ import { PoolseTheme, ConversationView } from '@poolse/react-native';
112
+
113
+ export default function ChatScreen({ conversationId }: { conversationId: string }) {
114
+ return (
115
+ <PoolseProvider config={{ getToken, userResolver }}>
116
+ <PoolseTheme>
117
+ <ConversationView conversationId={conversationId} />
118
+ </PoolseTheme>
119
+ </PoolseProvider>
120
+ );
121
+ }
122
+ ```
123
+
124
+ The same `<PoolseProvider>` you use on web. Wrap it once at the app root if you have multiple chat screens. Drop the `<PoolseTheme>` wrapper if you don't need to override the brand defaults — every component has a fallback theme baked in.
125
+
126
+ ## What ships
127
+
128
+ | Component | What it does |
129
+ | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
130
+ | `<PoolseInbox>` | Full inbox shell — list + detail with an iOS-style slide animation between them, left-edge-swipe-back to return to the list, built-in `New chat` / `New group` picker sheets when `users` is provided. Imperative ref API: `open`, `openDirect`, `openGroup`, `promptNewChat`, `promptNewGroup`, `close`. |
131
+ | `<ChatHeader>` | Title bar — direct chats resolve the other member's name + avatar via `labelFor` / `avatarFor`, groups show member count, presence-driven green dot. Optional `onBack` button. |
132
+ | `<ConversationView>` | Composed chat surface — message list + composer + typing strip + thread modal. Single `KeyboardAvoidingView` covers the whole thing so the composer stays above the keyboard on both platforms. |
133
+ | `<ThreadView>` | Bottom-sheet modal for thread replies. Reactions + attachments + pull-down-to-close via a 60-pt header drag strip. Composer reuses the same `UploadProvider` queue as the picker. |
134
+ | `<GroupDetailsSheet>` | Bottom sheet showing a group's name, avatar, and member roster with presence. Opened from `<ChatHeader>`'s members button. |
135
+ | `<MessageBubble>` / `<EditableMessageBubble>` | Single message render — body, meta, read receipt, quoted card, in-bubble attachments. |
136
+ | `<MessageRow>` | Bubble + alignment + swipe-to-reply (capture-phase, 1:1 finger follow, sqrt-damped past 50pt threshold). |
137
+ | `<MessageList>` | Inverted `FlatList` wrapper — stick-to-bottom, hold-position, auto-load-more. |
138
+ | `<MessageComposer>` | Text input with auto-grow, send button, attach button, quote chip. |
139
+ | `<MessageActions>` | Long-press menu (reply / quote / edit / delete / copy / react). |
140
+ | `<TypingIndicator>` | Animated three-dot indicator + labels. |
141
+ | `<ConversationList>` | `FlatList` of conversation rows. Direct rows pull the other member's name + avatar via `labelFor` / `avatarFor`; preview reads `last_message_preview` updated in realtime by the SDK's `conversation:updated` subscription. Unread badges. |
142
+ | `<MemberList>` | Member roster with avatars + remove action. |
143
+ | `<MentionInput>` | `TextInput` + autocomplete popover. |
144
+ | `<ReactionStrip>` / `<ReactionPicker>` | Reaction summary + emoji picker sheet. |
145
+ | `<AttachmentPreview>` / `<AttachmentPicker>` | Image or file card. Picker wraps `expo-image-picker` + `expo-document-picker`. |
146
+ | `<UploadQueueStrip>` | Pending uploads with cancel. |
147
+ | `<UserName>` / `useDisplayName(externalId)` | Display name resolution via the SDK's `userResolver`. |
148
+ | `<Avatar>` | Initials + URL fallback, stable color per user. |
149
+ | `<PoolseIcon>` / `<PoolseLogo>` | Brand chrome via `react-native-svg`. |
150
+
151
+ ## Theming
152
+
153
+ JS object via `<PoolseTheme theme={overrides}>`. The full token
154
+ surface mirrors the web `--poolse-*` CSS variables, grouped by
155
+ purpose:
156
+
157
+ ```tsx
158
+ <PoolseTheme
159
+ theme={{
160
+ colors: {
161
+ brand: '#ff5733',
162
+ brandSoft: '#ffe9e3',
163
+ onBrand: '#ffffff',
164
+ },
165
+ radii: {
166
+ bubble: 18,
167
+ },
168
+ }}
169
+ >
170
+ {/* ... */}
171
+ </PoolseTheme>
172
+ ```
173
+
174
+ Pass nothing to use the brand defaults. Pass `mode="dark"` to swap
175
+ to the warm dark palette. Deep-merges your overrides on top —
176
+ unspecified tokens keep their default.
177
+
178
+ ## Lifecycle
179
+
180
+ ```tsx
181
+ import { useAppStateLifecycle } from '@poolse/react-native';
182
+
183
+ function App() {
184
+ useAppStateLifecycle();
185
+ return <PoolseProvider config={config}>{/* ... */}</PoolseProvider>;
186
+ }
187
+ ```
188
+
189
+ Closes the SDK WebSocket cleanly when the app backgrounds (iOS /
190
+ Android suspend sockets aggressively — keeping them open wastes
191
+ battery and produces no benefit). Reopens on foreground and triggers
192
+ a tail refetch on the currently-mounted `<ConversationView>` so the
193
+ user lands on a fresh state instead of catching up via stale
194
+ realtime.
195
+
196
+ Pair with push notifications on your backend (subscribe to
197
+ `message.created` / `mention.created` webhooks, fan out via APNs /
198
+ FCM / Expo's push service) — that's how the user learns about new
199
+ messages while the app is closed.
200
+
201
+ ## Identity model
202
+
203
+ Same SDK 2.0 contract as the web side. All component props that
204
+ deal with users are keyed by your `external_id`:
205
+
206
+ - `useUser(externalId)`, `useDisplayName(externalId)`
207
+ - `<UserName externalId={message.sender_external_id} />`
208
+ - `<MemberList labelFor={(externalId) => ...} avatarFor={(externalId) => ...} onlineExternalIds={set} />`
209
+ - `<MessageBubble>` / `<MessageComposer>` / `<MentionInput>` / `<TypingIndicator>` — every `labelFor` is `(externalId) => string`.
210
+
211
+ The wire still carries `user_id` (the poolse uuid) alongside
212
+ `external_id`, but day-to-day app code never has to touch it.
213
+
214
+ ## Differences from `@poolse/react-ui`
215
+
216
+ | Concern | Web (`react-ui`) | Native (`react-native`) |
217
+ | ------------------- | ----------------------------- | -------------------------------------------- |
218
+ | Styling | CSS variables in `styles.css` | JS theme via `<PoolseTheme>` |
219
+ | Action menu trigger | Hover | Long-press |
220
+ | Reaction picker | Inline popover | Modal sheet |
221
+ | Mention picker | Inline dropdown | Modal sheet |
222
+ | Attachment picker | `<input type=file>` | `expo-image-picker` + `expo-document-picker` |
223
+ | Message list | Reversed CSS flexbox | Inverted `FlatList` |
224
+ | Markdown | `react-markdown` | Plain text |
225
+
226
+ Everything else — feature flags, prop names, callback shapes, hook signatures — is intentionally identical so a web codebase ports across with mostly mechanical changes.
227
+
228
+ ## Links
229
+
230
+ - Docs: [poolse.dev/docs/sdk/react-native](https://poolse.dev/docs/sdk/react-native)
231
+ - Source: [github.com/poolse-hq/js-sdk](https://github.com/poolse-hq/js-sdk/tree/main/packages/react-native)
232
+ - Web sibling: [`@poolse/react-ui`](https://www.npmjs.com/package/@poolse/react-ui)
233
+ - Hooks (used internally): [`@poolse/react`](https://www.npmjs.com/package/@poolse/react)
234
+ - Core client: [`@poolse/sdk`](https://www.npmjs.com/package/@poolse/sdk)
235
+
236
+ ## License
237
+
238
+ MIT