movius-chats 1.4.4 → 1.4.5

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,127 +1,180 @@
1
1
  # movius-chats
2
2
 
3
- A highly customizable, feature-rich chat UI for **React Native**. Drop in a single `ChatScreen` component to get message bubbles, WhatsApp-style media grids, audio/video playback, a full-screen swipe gallery, typing indicators, file attachment previews, and a smart input bar — with deep theming and custom component hooks.
3
+ A customizable React Native chat UI library. One `ChatScreen` component: message bubbles, WhatsApp-style media grids, audio playback, voice recording, composer previews, typing indicators, and a full-screen media gallery — with per-side theming and replaceable UI pieces.
4
4
 
5
5
  **npm:** [`movius-chats`](https://www.npmjs.com/package/movius-chats)
6
- **Repository:** [github.com/David-Atueyi/Movius-Chats](https://github.com/David-Atueyi/Movius-Chats)
6
+ **Repo:** [github.com/David-Atueyi/Movius-Chats](https://github.com/David-Atueyi/Movius-Chats)
7
+
8
+ This package is built for **plain React Native** (CLI / bare workflow). It does **not** depend on any Expo module.
7
9
 
8
10
  ---
9
11
 
10
12
  ## Table of contents
11
13
 
12
- - [Requirements & compatibility](#requirements--compatibility)
13
- - [Installation](#installation)
14
- - [Quick start](#quick-start)
15
- - [Data model](#data-model)
16
- - [Message](#message)
17
- - [MessageMediaItem](#messagemediaitem)
18
- - [MessageFileAttachment](#messagefileattachment)
19
- - [PreviewAttachment](#previewattachment)
20
- - [Message list ordering](#message-list-ordering)
21
- - [ChatScreen API](#chatscreen-api)
22
- - [Core props](#core-props)
23
- - [Feature flags](#feature-flags)
24
- - [Input & typing](#input--typing)
25
- - [Attachment preview (composer)](#attachment-preview-composer)
26
- - [Theme](#theme)
27
- - [Custom components & icons](#custom-components--icons)
28
- - [Usage examples](#usage-examples)
29
- - [Basic text chat](#basic-text-chat)
30
- - [Multi-image / video album bubble](#multi-image--video-album-bubble)
31
- - [File attachment bubble](#file-attachment-bubble)
32
- - [Audio message bubble](#audio-message-bubble)
33
- - [Composer attachment preview (single or multiple)](#composer-attachment-preview-single-or-multiple)
34
- - [Send button vs microphone](#send-button-vs-microphone)
35
- - [Typing indicators](#typing-indicators)
36
- - [Custom theme](#custom-theme)
37
- - [Font family (all text)](#font-family-all-text)
38
- - [Keyboard avoiding](#keyboard-avoiding)
39
- - [Custom input bar](#custom-input-bar)
40
- - [Opening file attachments (expo-sharing)](#opening-file-attachments-expo-sharing)
41
- - [Long-press on a message](#long-press-on-a-message)
42
- - [Full-screen gallery viewer](#full-screen-gallery-viewer)
43
- - [Architecture overview](#architecture-overview)
44
- - [TypeScript types](#typescript-types)
45
- - [Troubleshooting](#troubleshooting)
46
- - [Publishing a new version](#publishing-a-new-version)
47
- - [License](#license)
14
+ 1. [What is included](#what-is-included)
15
+ 2. [Package layout](#package-layout)
16
+ 3. [Dependencies](#dependencies)
17
+ 4. [Installation](#installation)
18
+ 5. [Quick start](#quick-start)
19
+ 6. [Message data model](#message-data-model)
20
+ 7. [Message list order](#message-list-order)
21
+ 8. [ChatScreen API](#chatscreen-api)
22
+ 9. [Voice recording](#voice-recording)
23
+ 10. [Audio message bubbles](#audio-message-bubbles)
24
+ 11. [Media grids & gallery](#media-grids--gallery)
25
+ 12. [Composer attachment preview](#composer-attachment-preview)
26
+ 13. [Theme & styling](#theme--styling)
27
+ 14. [Keyboard behavior](#keyboard-behavior)
28
+ 15. [Custom components & icons](#custom-components--icons)
29
+ 16. [TypeScript](#typescript)
30
+ 17. [Troubleshooting](#troubleshooting)
31
+ 18. [Publishing](#publishing)
32
+ 19. [License](#license)
33
+
34
+ ---
35
+
36
+ ## What is included
37
+
38
+ | Feature | Implementation |
39
+ |---------|----------------|
40
+ | Text messages | `react-native-parsed-text` (URLs tappable) |
41
+ | Image / video albums | `MediaGrid` — 1 / 2 / 3 / 4+ layout, 320px height |
42
+ | Full-screen viewer | `MediaViewer` — swipe, counter, selective video autoplay |
43
+ | Audio messages | `react-native-video` (hidden player) + waveform UI |
44
+ | Playback speed | 1x → 1.5x → 2x while playing |
45
+ | Voice recording | `react-native-audio-record` (optional peer) |
46
+ | File attachments | Tappable rows; default `Linking.openURL` |
47
+ | Typing indicator | Up to 2 avatars + `+N` badge |
48
+ | Input bar | Growing text field, emoji / clip / camera / send / mic |
49
+ | Status icons | Sent / delivered / read checkmarks |
50
+ | Theming | Separate `sent*` / `received*` colors for most bubble parts |
48
51
 
49
52
  ---
50
53
 
51
- ## Requirements & compatibility
54
+ ## Package layout
55
+
56
+ ```
57
+ src/
58
+ ├── index.tsx # ChatScreen entry
59
+ ├── types/index.ts # Message, ChatScreenProps, recorder types
60
+ ├── context/
61
+ │ ├── ChatContext.tsx # Props + gallery state
62
+ │ └── AudioContext.tsx # One audio plays at a time
63
+ ├── hooks/
64
+ │ ├── useKeyboardInset.ts # Keyboard height → input margin
65
+ │ └── useVoiceRecorder.ts # Mic capture (audio-record + fs)
66
+ ├── utils/
67
+ │ ├── bubbleTheme.ts # sent/received color helpers
68
+ │ ├── messageMedia.ts # collectMediaItems()
69
+ │ ├── theme.ts # fontFamily, input icon size
70
+ │ └── datefunc.ts # formatDuration()
71
+ ├── assets/Icons/ # SVG icons (play, mic, tail, etc.)
72
+ └── components/
73
+ ├── ChatBubble/ # Bubble, content, status, media grid
74
+ ├── ChatInput/ # Input, FilePreview, voice gestures
75
+ ├── AudioPlayer/ # WhatsApp-style audio UI
76
+ ├── MediaViewer/ # Full-screen gallery modal
77
+ ├── TypingComponent/
78
+ └── VoiceRecorder/ # Normal + long-press recording UI
79
+ ```
80
+
81
+ Published build output: `lib/commonjs`, `lib/module`, `lib/typescript`.
82
+
83
+ ---
52
84
 
53
- | Dependency | Role |
54
- |------------|------|
55
- | `react` 16.8 | Peer dependency |
56
- | `react-native` | Peer dependency |
57
- | `react-native-reanimated` | Audio scrubber animation (peer) |
58
- | `react-native-sound` | Voice message playback |
85
+ ## Dependencies
86
+
87
+ ### Bundled (installed with movius-chats)
88
+
89
+ | Package | Use |
90
+ |---------|-----|
91
+ | `react-native-video` | Video thumbnails, gallery video, **audio playback** |
59
92
  | `react-native-svg` | Built-in icons |
60
- | `react-native-video` | Video thumbnails + full-screen video |
61
- | `react-native-parsed-text` | Tappable URLs in text messages |
62
- | `twrnc` | Internal Tailwind-style utilities |
93
+ | `react-native-parsed-text` | Link detection in text |
94
+ | `twrnc` | Internal styles |
95
+
96
+ ### Peer dependencies (your app must install)
97
+
98
+ | Package | Required |
99
+ |---------|----------|
100
+ | `react` ≥ 16.8 | Yes |
101
+ | `react-native` | Yes |
102
+ | `react-native-reanimated` | Yes (voice recorder animations) |
63
103
 
64
- **Not compatible with Expo Go** — native modules require a development build or bare RN project.
104
+ ### Optional peers (voice recording only)
105
+
106
+ | Package | Use |
107
+ |---------|-----|
108
+ | `react-native-audio-record` | Record microphone |
109
+ | `react-native-fs` | Delete cancelled recording files |
110
+
111
+ If these are missing, the UI still renders; starting a recording logs an install hint.
112
+
113
+ **There is no `react-native-sound`, `expo-av`, `expo-file-system`, or other Expo package in this library.**
65
114
 
66
115
  ---
67
116
 
68
117
  ## Installation
69
118
 
70
- ### 1. Install the package
119
+ ### 1. Install movius-chats
71
120
 
72
121
  ```bash
73
- npm install movius-chats
74
- # or
75
122
  yarn add movius-chats
76
- # or
77
- bun add movius-chats
123
+ # or: npm install movius-chats
124
+ # or: bun add movius-chats
78
125
  ```
79
126
 
80
- ### 2. Install peer / native dependencies
127
+ ### 2. Install peers
81
128
 
82
129
  ```bash
83
- npm install react-native-reanimated react-native-sound react-native-svg react-native-video twrnc
130
+ yarn add react-native-reanimated react-native-video react-native-svg
84
131
  ```
85
132
 
86
- ### 3. Configure Reanimated
133
+ `react-native-video` and `react-native-svg` are also pulled in as movius-chats dependencies, but your app should list compatible versions and link native code.
134
+
135
+ ### 3. Reanimated (Babel)
87
136
 
88
- Add the Reanimated plugin **last** in `babel.config.js`:
137
+ Put this plugin **last** in `babel.config.js`:
89
138
 
90
139
  ```js
91
140
  module.exports = {
92
141
  presets: ['module:metro-react-native-babel-preset'],
93
142
  plugins: [
94
- // other plugins
143
+ // ...other plugins
95
144
  'react-native-reanimated/plugin',
96
145
  ],
97
146
  };
98
147
  ```
99
148
 
100
- ### 4. Rebuild native code
149
+ ### 4. Voice recording (optional)
101
150
 
102
- **React Native CLI:**
151
+ ```bash
152
+ yarn add react-native-audio-record react-native-fs
153
+ ```
103
154
 
104
- ```bash
105
- cd ios && pod install && cd ..
106
- npx react-native run-ios
107
- npx react-native run-android
155
+ **iOS** — add to `Info.plist`:
156
+
157
+ ```xml
158
+ <key>NSMicrophoneUsageDescription</key>
159
+ <string>This app needs the microphone to record voice messages.</string>
108
160
  ```
109
161
 
110
- **Expo development build:**
162
+ **Android** ensure `RECORD_AUDIO` is in `AndroidManifest.xml` (often added by the audio-record library).
163
+
164
+ Then rebuild native apps:
111
165
 
112
- ```bash
113
- npx expo prebuild
114
- npx expo run:ios # or run:android
166
+ ```bash
167
+ cd ios && pod install && cd ..
168
+ npx react-native run-ios
169
+ npx react-native run-android
115
170
  ```
116
171
 
117
- ### 5. Android keyboard mode (Expo)
172
+ ### 5. Android keyboard
118
173
 
119
- In `app.json` / `app.config.js` add:
174
+ In `android/app/src/main/AndroidManifest.xml` on your main activity:
120
175
 
121
- ```json
122
- "android": {
123
- "softwareKeyboardLayoutMode": "resize"
124
- }
176
+ ```xml
177
+ android:windowSoftInputMode="adjustResize"
125
178
  ```
126
179
 
127
180
  ---
@@ -131,20 +184,20 @@ In `app.json` / `app.config.js` add:
131
184
  ```tsx
132
185
  import React, { useState } from 'react';
133
186
  import { Platform, SafeAreaView, View } from 'react-native';
187
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
134
188
  import ChatScreen from 'movius-chats';
135
189
  import type { Message } from 'movius-chats/lib/typescript/types';
136
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
137
190
 
138
- export default function MyChatScreen() {
191
+ export default function ChatDetailScreen() {
139
192
  const insets = useSafeAreaInsets();
140
- const currentUserId = 'user-1';
193
+ const currentUserId = '1';
141
194
  const [messages, setMessages] = useState<Message[]>([]);
142
195
 
143
196
  return (
144
197
  <SafeAreaView style={{ flex: 1 }}>
145
198
  <View style={{ flex: 1 }}>
146
- <ChatScreen
147
- messages={messages}
199
+ <ChatScreen
200
+ messages={messages}
148
201
  currentUserId={currentUserId}
149
202
  onSendMessage={({ text, senderId }) => {
150
203
  setMessages((prev) => [
@@ -161,12 +214,9 @@ export default function MyChatScreen() {
161
214
  ...prev,
162
215
  ]);
163
216
  }}
164
- keyboardVerticalOffset={Platform.OS === 'ios' ? insets.top + 44 : 0}
165
- showAvatars
217
+ keyboardVerticalOffset={Platform.OS === 'ios' ? insets.top : 0}
166
218
  showBubbleTail
167
219
  showMessageStatus
168
- showAttachmentsButton
169
- showCameraButton
170
220
  showVoiceRecordButton
171
221
  />
172
222
  </View>
@@ -175,241 +225,156 @@ export default function MyChatScreen() {
175
225
  }
176
226
  ```
177
227
 
228
+ Wrap the screen in `flex: 1`. Load custom fonts in **your** app before passing `theme.fontFamily`.
229
+
178
230
  ---
179
231
 
180
- ## Data model
232
+ ## Message data model
181
233
 
182
- ### Message
234
+ ### `Message`
183
235
 
184
- Every item in the `messages` array must match this shape:
236
+ | Field | Type | Description |
237
+ |-------|------|-------------|
238
+ | `id` | `string` | Unique id |
239
+ | `senderId` | `string` | Who sent it |
240
+ | `time` | `string` | Display time (you format it) |
241
+ | `status` | `'sent' \| 'delivered' \| 'read'` | Checkmarks on **your** messages only |
242
+ | `text` | `string` | Body text |
243
+ | `audio` | `string` | Audio file URI |
244
+ | `image` | `string` | Single image (legacy; prefer `mediaItems`) |
245
+ | `video` | `string` | Single video (legacy) |
246
+ | `mediaItems` | `MessageMediaItem[]` | Album in one bubble |
247
+ | `fileAttachments` | `MessageFileAttachment[]` | PDF, doc, etc. |
248
+ | `senderName` | `string` | Group name + audio avatar initial |
249
+ | `senderAvatar` | `string` | Image URI for audio bubble avatar |
185
250
 
186
- | Field | Type | Required | Description |
187
- |-------|------|----------|-------------|
188
- | `id` | `string` | Yes | Unique message id |
189
- | `senderId` | `string` | Yes | User id of the sender |
190
- | `time` | `string` | Yes | Display time string — you format this (e.g. `"2:30 PM"`) |
191
- | `status` | `'sent' \| 'delivered' \| 'read'` | Yes | Shown only for current user's messages |
192
- | `text` | `string` | No | Plain text; URLs auto-linked |
193
- | `audio` | `string` | No | Audio file URI — renders a playable audio bubble |
194
- | `image` | `string` | No | Single image URI (legacy; prefer `mediaItems`) |
195
- | `video` | `string` | No | Single video URI (legacy; prefer `mediaItems`) |
196
- | `mediaItems` | `MessageMediaItem[]` | No | Album of images/videos — renders a WhatsApp-style grid |
197
- | `fileAttachments` | `MessageFileAttachment[]` | No | PDFs, docs, etc. — rendered as tappable file rows |
198
- | `senderName` | `string` | No | Shown when `showUserNames` is true |
199
- | `senderAvatar` | `string` | No | Avatar image URI; falls back to first letter of `senderName` |
251
+ ### `MessageMediaItem`
200
252
 
201
- A message can combine fields (e.g. `text` + `mediaItems`).
253
+ ```ts
254
+ { uri: string; kind: 'image' | 'video' }
255
+ ```
202
256
 
203
- ### MessageMediaItem
257
+ ### `MessageFileAttachment`
204
258
 
205
259
  ```ts
206
- interface MessageMediaItem {
207
- uri: string;
208
- kind: 'image' | 'video';
209
- }
260
+ { uri: string; type: string; name: string }
210
261
  ```
211
262
 
212
- Used in `message.mediaItems` for multi-media bubbles.
213
-
214
- ### MessageFileAttachment
263
+ ### `PreviewAttachment` (composer)
215
264
 
216
265
  ```ts
217
- interface MessageFileAttachment {
218
- uri: string;
219
- type: string; // MIME type, e.g. "application/pdf"
220
- name: string;
221
- }
266
+ { uri: string; type: string; name?: string }
222
267
  ```
223
268
 
224
- Each attachment is shown as a tappable chip that opens the file via `Linking.openURL`.
225
-
226
- ### PreviewAttachment
269
+ ### `RecordingResult` (`onAudioRecordEnd`)
227
270
 
228
271
  ```ts
229
- interface PreviewAttachment {
230
- uri: string;
231
- type: string; // MIME type
232
- name?: string;
233
- }
272
+ { uri: string; duration: number; mimeType?: string; size?: number }
234
273
  ```
235
274
 
236
- Used in `previewData` / `previewItems` for the composer preview strip above the input.
237
-
238
275
  ---
239
276
 
240
- ## Message list ordering
277
+ ## Message list order
241
278
 
242
- `ChatScreen` uses an **inverted** `FlatList`. The **newest message must be at index `0`**:
279
+ The list is **inverted**. Newest message must be **index 0**:
243
280
 
244
281
  ```ts
245
- setMessages((prev) => [newMessage, ...prev]); // correct
282
+ setMessages((prev) => [newMessage, ...prev]);
246
283
  ```
247
284
 
248
- Consecutive messages from the same sender are grouped — avatars and tails appear only on the first message in each group.
285
+ Avatars and bubble tails show only on the **first** message in a consecutive run from the same `senderId`.
249
286
 
250
287
  ---
251
288
 
252
289
  ## ChatScreen API
253
290
 
254
- `ChatScreen` is the **default export** from `movius-chats`.
291
+ Default export: `ChatScreen`. All props are optional except `messages`, `currentUserId`, and `onSendMessage`.
255
292
 
256
- ### Core props
293
+ ### Core
294
+
295
+ | Prop | Type | Description |
296
+ |------|------|-------------|
297
+ | `messages` | `Message[]` | Newest first |
298
+ | `currentUserId` | `string` | Sent vs received layout |
299
+ | `onSendMessage` | `(Omit<Message, 'id' \| 'time' \| 'status'>) => void` | Send button |
300
+ | `onMessageLongPress` | `(message: Message) => void` | Long-press bubble |
301
+ | `placeholder` | `string` | Input placeholder (default `"Message"`) |
302
+ | `keyboardVerticalOffset` | `number` | **iOS only** — passed to `KeyboardAvoidingView` |
303
+ | `disableKeyboardAvoiding` | `boolean` | Turn off built-in keyboard lift |
257
304
 
258
- | Prop | Type | Required | Description |
259
- |------|------|----------|-------------|
260
- | `messages` | `Message[]` | Yes | Message array, newest first |
261
- | `currentUserId` | `string` | Yes | Logged-in user id — controls bubble alignment and status icons |
262
- | `onSendMessage` | `(msg: Omit<Message, 'id' \| 'time' \| 'status'>) => void` | Yes | Called when the user taps send |
263
- | `onMessageLongPress` | `(message: Message) => void` | No | Called on long-press of a bubble |
264
- | `placeholder` | `string` | No | Input placeholder text (default: `"Message"`) |
265
- | `keyboardVerticalOffset` | `number` | No | **iOS only** — header + status bar height offset for `KeyboardAvoidingView`. Android lifts the input by the full keyboard height automatically. Default: `0` |
266
- | `disableKeyboardAvoiding` | `boolean` | No | Set `true` if your screen already handles keyboard avoidance |
305
+ ### Feature flags (default `false`)
267
306
 
268
- ### Feature flags
307
+ `showAvatars`, `showUserNames`, `showBubbleTail`, `showMessageStatus`, `showEmojiButton`, `showAttachmentsButton`, `showCameraButton`, `showVoiceRecordButton`
269
308
 
270
- All default to `false` (hidden) unless explicitly set to `true`:
309
+ ### Callbacks
271
310
 
272
311
  | Prop | Description |
273
312
  |------|-------------|
274
- | `showAvatars` | Avatar (or initial letter) on received bubbles and typing row |
275
- | `showUserNames` | Sender name above received bubbles |
276
- | `showBubbleTail` | WhatsApp-style corner tail on first bubble in a sequence |
277
- | `showMessageStatus` | Timestamp + checkmark icons on sent messages |
278
- | `showEmojiButton` | Emoji button in the input bar (UI only wire your own picker via `CustomEmojiIcon`) |
279
- | `showAttachmentsButton` | Paperclip icon triggers `onAttachmentPress` |
280
- | `showCameraButton` | Camera icon when input is empty — triggers `onCameraPress` |
281
- | `showVoiceRecordButton` | Mic icon when there is no text and no preview; becomes send icon otherwise |
282
-
283
- ### Input & typing
284
-
285
- | Prop | Type | Description |
286
- |------|------|-------------|
287
- | `onTypingStart` | `() => void` | Called when the text input becomes non-empty |
288
- | `onTypingEnd` | `() => void` | Called when the text input is cleared |
289
- | `onAttachmentPress` | `() => void` | Paperclip tapped — open your file/image picker |
290
- | `onCameraPress` | `() => void` | Camera icon tapped — open camera |
291
- | `onAudioRecordStart` | `() => void` | Mic pressed / long-pressed — start recording |
292
- | `onAudioRecordEnd` | `() => void` | Mic released — stop recording, upload, add message |
293
- | `onFileAttachmentPress` | `(file: MessageFileAttachment) => void` | Tapped a file-attachment chip in a bubble. Defaults to `Linking.openURL`. Supply this to use `expo-sharing` or a custom downloader. |
294
- | `typingUsers` | `{ id: string; avatar: string; name: string }[]` | Users currently typing (current user is excluded from display) |
313
+ | `onTypingStart` / `onTypingEnd` | Input text empty non-empty |
314
+ | `onAttachmentPress` | Paperclip open your picker |
315
+ | `onCameraPress` | Camera icon |
316
+ | `onAudioRecordStart` | Recording began |
317
+ | `onAudioRecordEnd` | `(RecordingResult?) => void` when done or cancelled |
318
+ | `onFileAttachmentPress` | File chip in bubble (default: `Linking.openURL`) |
295
319
 
296
- ### Attachment preview (composer)
320
+ ### Composer preview
297
321
 
298
- Show a preview strip above the input before the user taps send.
322
+ | Prop | Description |
323
+ |------|-------------|
324
+ | `previewItems` | Multiple attachments before send |
325
+ | `previewData` | Single attachment (legacy) |
326
+ | `onRemovePreviewItem` | `(uri) => void` — remove **one** card by URI |
327
+ | `closePreview` | Clears all if `onRemovePreviewItem` not set |
299
328
 
300
- | Prop | Type | Description |
301
- |------|------|-------------|
302
- | `previewItems` | `PreviewAttachment[]` | **Multiple** attachments — images/videos shown as a fanned spread; documents shown as file chips |
303
- | `previewData` | `PreviewAttachment` | **Single** attachment (kept for backward compatibility; `previewItems` takes precedence) |
304
- | `closePreview` | `() => void` | Fallback called when no `onRemovePreviewItem` is provided |
305
- | `onRemovePreviewItem` | `(uri: string) => void` | Called with the URI of whichever card the user tapped × on — removes only that item |
329
+ When preview or text exists, the **send** icon shows instead of the mic.
306
330
 
307
- **Per-item removal:** Every media thumbnail and every document chip shows its own × button. When `onRemovePreviewItem` is provided, tapping × on a card calls it with that card's URI so you can filter it out of your state. `closePreview` is only used as a fallback when `onRemovePreviewItem` is not supplied.
331
+ ### Voice recorder customization
308
332
 
309
- When any preview is present, the send button appears regardless of text content.
333
+ | Prop | Type |
334
+ |------|------|
335
+ | `renderVoiceRecorder` | `(VoiceRecorderExposedState) => ReactNode` — replace entire recorder UI |
336
+ | `voiceRecorderProps` | `VoiceRecorderConfig` — `maxDuration`, lock, slide-to-cancel, etc. |
337
+ | `voiceRecorderStyles` | `VoiceRecorderStyleOverrides` |
338
+ | `recordingUIProps` | Colors/sizes for timer, lock pill, recorder play/pause |
310
339
 
311
- ### Theme
340
+ ### Typing
312
341
 
313
- All keys are optional. Pass a `theme` object to `ChatScreen`:
342
+ | Prop | Type |
343
+ |------|------|
344
+ | `typingUsers` | `{ id, avatar, name }[]` |
314
345
 
315
- ```ts
316
- theme?: {
317
- fontFamily?: string; // applied to every Text element in the package
318
-
319
- colors?: {
320
- sentMessageTailColor?: string;
321
- receivedMessageTailColor?: string;
322
- timestamp?: string;
323
- inputsIconsColor?: string; // emoji, clip, camera icons
324
- sendIconsColor?: string; // send / mic icons
325
- placeholderTextColor?: string;
326
- inputTextColor?: string;
327
- audioPlayIconColor?: string;
328
- audioPauseIconColor?: string;
329
- videoPlayIconColor?: string;
330
- sentIconColor?: string;
331
- deliveredIconColor?: string;
332
- readIconColor?: string;
333
- };
334
-
335
- sizes?: {
336
- // Applies only to emoji, paperclip, and camera icons (not send/mic)
337
- inputIconSize?: string | number; // number = px, string = twrnc class e.g. "h-8 w-8"
338
- };
339
-
340
- bubbleStyle?: {
341
- sent?: ViewStyle;
342
- received?: ViewStyle;
343
- avatarTextStyle?: TextStyle;
344
- userNameStyle?: TextStyle;
345
- avatarImageStyle?: ImageStyle;
346
- typingContainerStyle?: ViewStyle;
347
- additionalTypingUsersContainerStyle?: ViewStyle;
348
- additionalTypingUsersTextStyle?: TextStyle;
349
- };
350
-
351
- messageStyle?: {
352
- sentTextStyle?: TextStyle;
353
- receivedTextStyle?: TextStyle;
354
- audioPlayButtonStyle?: ViewStyle;
355
- audioKnobStyle?: ViewStyle;
356
- progressBarStyle?: ViewStyle;
357
- activeProgressBarStyle?: ViewStyle;
358
- audioDurationStyle?: TextStyle;
359
- };
360
-
361
- inputStyles?: {
362
- inputSectionContainerStyle?: ViewStyle; // outer row (input + send button)
363
- inputContainerStyle?: ViewStyle; // the pill/rounded box
364
- sendButtonStyle?: ViewStyle; // the round send/mic button
365
- };
366
-
367
- filePreviewStyle?: {
368
- root?: ViewStyle;
369
- container?: ViewStyle;
370
- iconContainer?: ViewStyle;
371
- nameContainer?: ViewStyle;
372
- text?: TextStyle;
373
- };
374
- }
375
- ```
346
+ ---
376
347
 
377
- ### Custom components & icons
348
+ ## Voice recording
378
349
 
379
- | Prop | Type | Description |
380
- |------|------|-------------|
381
- | `renderCustomInput` | `() => React.ReactNode` | Replace the entire input bar |
382
- | `renderCustomTyping` | `() => React.ReactNode` | Replace the typing bubble content |
383
- | `renderCustomVideoBubbleError` | `() => React.ReactNode` | Replace the inline video error state |
384
- | `CustomEmojiIcon` | `() => React.ReactNode` | Emoji button icon |
385
- | `CustomAttachmentIcon` | `() => React.ReactNode` | Paperclip icon |
386
- | `CustomCameraIcon` | `() => React.ReactNode` | Camera icon |
387
- | `CustomSendIcon` | `() => React.ReactNode` | Send button icon |
388
- | `CustomMicrophoneIcon` | `() => React.ReactNode` | Microphone icon |
389
- | `CustomPlayIcon` | `() => React.ReactNode` | Play icon in audio and video |
390
- | `CustomPauseIcon` | `() => React.ReactNode` | Pause icon in audio player |
391
- | `CustomFileIcon` | `React.ComponentType<{ style?: any }>` | Icon inside document file chips |
392
- | `CustomImagePreview` | `React.ComponentType<{ uri: string }>` | Replaces the composer image thumbnail |
393
- | `CustomVideoPreview` | `React.ComponentType<{ uri: string }>` | Replaces the composer video thumbnail |
350
+ Requires **`react-native-audio-record`** and **`react-native-fs`** in the host app, plus a **native rebuild**.
394
351
 
395
- ---
352
+ ### Gestures
396
353
 
397
- ## Usage examples
354
+ | Action | Result |
355
+ |--------|--------|
356
+ | **Tap** mic | Normal bar: trash, timer, waveform, play/pause preview, send |
357
+ | **Long-press** mic | Hold mode: “slide to cancel”, lock column above send |
358
+ | Slide **left** | Cancel (file deleted via `react-native-fs`) |
359
+ | Slide **up** to lock | Switches to normal bar (`lockSlideDistance` in `recordingUIProps`) |
360
+ | **Release** without slide | Auto-send (`onAudioRecordEnd`) |
398
361
 
399
- ### Basic text chat
362
+ ### Wiring
400
363
 
401
364
  ```tsx
402
365
  <ChatScreen
403
- messages={messages}
404
- currentUserId="user-1"
405
- onSendMessage={({ text, senderId }) => {
366
+ showVoiceRecordButton
367
+ onAudioRecordEnd={(result) => {
368
+ if (!result) return;
406
369
  setMessages((prev) => [
407
370
  {
408
371
  id: String(Date.now()),
409
- text,
410
- senderId,
411
- time: '10:00 AM',
372
+ senderId: currentUserId,
373
+ audio: result.uri,
374
+ time: '10:56 PM',
412
375
  status: 'sent',
376
+ senderAvatar: myAvatarUri,
377
+ senderName: myDisplayName,
413
378
  },
414
379
  ...prev,
415
380
  ]);
@@ -417,364 +382,226 @@ All keys are optional. Pass a `theme` object to `ChatScreen`:
417
382
  />
418
383
  ```
419
384
 
420
- ### Multi-image / video album bubble
421
-
422
- Use `mediaItems` in the `Message` object. The bubble renders a WhatsApp-style grid:
423
-
424
- | Count | Layout | Height |
425
- |-------|--------|--------|
426
- | 1 | Single full-width tile (cover) | 320 px |
427
- | 2 | Side by side, two equal columns | 320 px |
428
- | 3 | One on top (55%), two below (45%) | 320 px |
429
- | 4+ | 2 × 2 grid; bottom-right cell shows `+N` overlay | 320 px |
430
-
431
- All layouts share the same fixed height so multi-image bubbles stay visually consistent with single-image bubbles. Tapping any cell opens the full-screen swipe gallery.
385
+ ### Custom recorder UI
432
386
 
433
387
  ```tsx
434
- const message: Message = {
435
- id: '1',
436
- senderId: 'user-2',
437
- time: '11:00 AM',
438
- status: 'read',
439
- mediaItems: [
440
- { uri: 'https://example.com/photo1.jpg', kind: 'image' },
441
- { uri: 'https://example.com/photo2.jpg', kind: 'image' },
442
- { uri: 'https://example.com/clip.mp4', kind: 'video' },
443
- ],
444
- };
388
+ renderVoiceRecorder={(state) => (
389
+ <MyRecorder
390
+ duration={state.duration}
391
+ onStop={state.stopRecording}
392
+ onCancel={state.cancelRecording}
393
+ />
394
+ )}
445
395
  ```
446
396
 
447
- Legacy single-item syntax still works and is merged automatically:
397
+ ---
448
398
 
449
- ```tsx
450
- // These are equivalent for display purposes
451
- { image: 'https://...' }
452
- { mediaItems: [{ uri: 'https://...', kind: 'image' }] }
453
- ```
399
+ ## Audio message bubbles
454
400
 
455
- ### File attachment bubble
401
+ WhatsApp-style row inside the bubble:
456
402
 
457
- ```tsx
458
- const message: Message = {
459
- id: '2',
460
- senderId: 'user-1',
461
- time: '11:05 AM',
462
- status: 'delivered',
463
- fileAttachments: [
464
- {
465
- uri: 'https://example.com/report.pdf',
466
- type: 'application/pdf',
467
- name: 'Q2 Report.pdf',
468
- },
469
- ],
470
- text: 'Here is the report',
471
- };
472
- ```
403
+ | Side | Layout (left → right) |
404
+ |------|------------------------|
405
+ | **Sent** | Avatar or speed pill → play/pause → waveform |
406
+ | **Received** | play/pause → waveform → avatar or speed pill |
473
407
 
474
- Each attachment renders as a tappable row. By default tapping calls `Linking.openURL(uri)`. Supply `onFileAttachmentPress` to override this with your own handler (e.g. `expo-sharing`).
408
+ | State | Avatar slot |
409
+ |-------|-------------|
410
+ | Idle / finished | `senderAvatar` or first letter of `senderName` |
411
+ | Playing | Pill showing **1x**, **1.5x**, or **2x** (tap to cycle) |
412
+ | Ended | Avatar again |
475
413
 
476
- ### Audio message bubble
414
+ - Waveform bars with scrubber dot; tap or drag to seek
415
+ - Duration under the waveform
416
+ - Play/pause is icon-only (no filled circle)
417
+ - Only one audio plays at a time (`AudioContext`)
418
+ - Video in the gallery pauses other audio
477
419
 
478
420
  ```tsx
479
- const message: Message = {
480
- id: '3',
481
- senderId: 'user-2',
482
- audio: 'file:///path/to/recording.m4a',
483
- time: '11:10 AM',
421
+ {
422
+ id: 'a1',
423
+ senderId: '2',
424
+ audio: 'file:///data/user/0/.../voice.wav',
425
+ senderAvatar: 'https://cdn.example.com/u2.jpg',
426
+ senderName: 'Alex',
427
+ time: '10:23 pm',
484
428
  status: 'read',
485
- };
429
+ }
486
430
  ```
487
431
 
488
- The audio player has a scrubable progress bar, a play/pause button, and a duration counter.
489
- Only one audio message plays at a time — starting a new one automatically stops the previous one.
490
-
491
- ### Composer attachment preview (single or multiple)
432
+ ---
492
433
 
493
- Use `previewItems` (array) for multi-select, or `previewData` (single) for back-compat:
434
+ ## Media grids & gallery
494
435
 
495
- ```tsx
496
- const [previews, setPreviews] = useState<PreviewAttachment[]>([]);
497
-
498
- <ChatScreen
499
- previewItems={previews}
500
- closePreview={() => setPreviews([])}
501
- // Remove only the card whose × was tapped:
502
- onRemovePreviewItem={(uri) =>
503
- setPreviews((prev) => prev.filter((p) => p.uri !== uri))
504
- }
505
- onAttachmentPress={async () => {
506
- const picked = await myPicker.pick(); // returns an array
507
- setPreviews(
508
- picked.map((f) => ({ uri: f.uri, type: f.type, name: f.name }))
509
- );
510
- }}
511
- onSendMessage={({ text, senderId }) => {
512
- const media = previews
513
- .filter((p) => p.type.startsWith('image/') || p.type.startsWith('video/'))
514
- .map((p) => ({
515
- uri: p.uri,
516
- kind: p.type.startsWith('video/') ? 'video' : 'image',
517
- } as MessageMediaItem));
518
-
519
- const files = previews
520
- .filter((p) => !p.type.startsWith('image/') && !p.type.startsWith('video/'))
521
- .map((p) => ({ uri: p.uri, type: p.type, name: p.name ?? 'file' }));
436
+ ### Grid (`mediaItems`)
522
437
 
523
- setMessages((prev) => [
524
- {
525
- id: String(Date.now()),
526
- senderId,
527
- time: '...',
528
- status: 'sent',
529
- text: text || undefined,
530
- mediaItems: media.length ? media : undefined,
531
- fileAttachments: files.length ? files : undefined,
532
- },
533
- ...prev,
534
- ]);
438
+ | Count | Layout | Height |
439
+ |-------|--------|--------|
440
+ | 1 | Full width, cover | 320px |
441
+ | 2 | Two columns | 320px |
442
+ | 3 | One top, two bottom | 320px |
443
+ | 4+ | 2×2, `+N` on last cell | 320px |
535
444
 
536
- setPreviews([]);
537
- }}
538
- />
539
- ```
445
+ Tap opens `MediaViewer`. Thumbnail `Video` uses `pointerEvents="none"` so presses reach the parent.
540
446
 
541
- Preview UI:
542
- - **1 image/video** — single thumbnail with × in the top-right corner.
543
- - **2–3 images/videos** — overlapping fan spread; each card has its own ×.
544
- - **4+ images/videos** — fan of 3 with a `+N` badge; each visible card has its own ×.
545
- - **Documents** — file chips, each with their own ×. If more than 3 documents are selected the list becomes scrollable.
447
+ ### Gallery behavior
546
448
 
547
- Tapping × on any card calls `onRemovePreviewItem(uri)` for that specific file. When the last item is removed the preview strip disappears automatically.
449
+ - Horizontal `FlatList`, `n / total` header
450
+ - **Videos** play only if that video was the tapped item and the page is active
451
+ - Tapping an **image** in a mixed album does not start other videos
452
+ - Composer video previews **do** autoplay in the small preview card
548
453
 
549
- ### Send button vs microphone
454
+ ### Legacy single fields
550
455
 
551
- The send button (green circle, right of input) shows:
456
+ `image` and `video` on `Message` are merged into `mediaItems` internally via `collectMediaItems()`.
552
457
 
553
- | Condition | Icon shown |
554
- |-----------|------------|
555
- | Input has text | Send icon |
556
- | `previewItems` / `previewData` is set | Send icon |
557
- | Neither, `showVoiceRecordButton` true | Microphone icon |
558
- | Neither, `showVoiceRecordButton` false | Send icon |
458
+ ---
559
459
 
560
- No extra work needed — the package handles this automatically.
460
+ ## Composer attachment preview
561
461
 
562
- ### Typing indicators
462
+ Controlled from your app state:
563
463
 
564
464
  ```tsx
565
- const [typingUsers, setTypingUsers] = useState([]);
566
-
567
- // When your socket receives "user X is typing":
568
- setTypingUsers([{ id: 'user-2', avatar: 'https://...', name: 'Alex' }]);
569
-
570
- <ChatScreen
571
- typingUsers={typingUsers}
572
- onTypingStart={() => socket.emit('typing-start')}
573
- onTypingEnd={() => socket.emit('typing-end')}
574
- />
575
- ```
576
-
577
- Up to two avatars are shown side-by-side; additional users appear as a `+N` badge.
578
-
579
- ### Custom theme
465
+ const [previews, setPreviews] = useState<PreviewAttachment[]>([]);
580
466
 
581
- ```tsx
582
467
  <ChatScreen
583
- theme={{
584
- fontFamily: 'Outfit-Regular',
585
- colors: {
586
- sentMessageTailColor: '#007AFF',
587
- receivedMessageTailColor: '#E5E5EA',
588
- timestamp: '#8E8E93',
589
- inputsIconsColor: '#6C808E',
590
- sendIconsColor: '#FFFFFF',
591
- inputTextColor: '#000000',
592
- readIconColor: '#34C759',
593
- },
594
- bubbleStyle: {
595
- sent: { backgroundColor: '#007AFF' },
596
- received: { backgroundColor: '#E5E5EA' },
597
- },
598
- messageStyle: {
599
- sentTextStyle: { color: '#FFFFFF' },
600
- receivedTextStyle: { color: '#000000' },
601
- },
602
- inputStyles: {
603
- inputContainerStyle: { backgroundColor: '#F2F2F7' },
604
- sendButtonStyle: { backgroundColor: '#007AFF' },
605
- },
606
- sizes: {
607
- inputIconSize: 22, // emoji, clip, camera only — not send/mic
608
- },
609
- }}
468
+ previewItems={previews}
469
+ onRemovePreviewItem={(uri) =>
470
+ setPreviews((p) => p.filter((x) => x.uri !== uri))
471
+ }
472
+ onAttachmentPress={openYourDocumentPicker}
473
+ onSendMessage={handleSend}
610
474
  />
611
475
  ```
612
476
 
613
- ### Font family (all text)
614
-
615
- `theme.fontFamily` applies to **every** `Text` element inside the package: bubble text, timestamps, sender names, audio duration, typing label, file names, error messages, and the input field.
616
-
617
- **You must load the font in your app before using it.** The package only sets the style name.
618
-
619
- **Expo `expo-font` plugin approach:**
620
-
621
- In `app.json`:
477
+ | Preview type | UI |
478
+ |--------------|-----|
479
+ | 1 image/video | Single thumb + × |
480
+ | 2–3 media | Fanned stack, × on each |
481
+ | 4+ media | Fan of 3 + `+N`, × per visible card |
482
+ | Documents | Chips; scrollable after 3 |
622
483
 
623
- ```json
624
- "plugins": [
625
- ["expo-font", {
626
- "fonts": ["./assets/fonts/Outfit-Regular.ttf"]
627
- }]
628
- ]
629
- ```
484
+ Use any picker you want (`react-native-document-picker`, `react-native-image-picker`, etc.) — movius-chats only displays `previewItems`.
630
485
 
631
- Then pass the font name (usually the file name without `.ttf`):
486
+ ---
632
487
 
633
- ```tsx
634
- theme={{ fontFamily: 'Outfit-Regular' }}
635
- ```
488
+ ## Theme & styling
636
489
 
637
- **Expo `useFonts` hook approach (custom name):**
490
+ Pass `theme` to `ChatScreen`. `theme.fontFamily` applies to **all** `Text` in the package (load the font in your app first).
638
491
 
639
- ```tsx
640
- import { useFonts } from 'expo-font';
492
+ ### `theme.colors` — per side (`sent*` / `received*`)
641
493
 
642
- const [loaded] = useFonts({
643
- 'my-custom-font': require('./assets/fonts/MyFont.ttf'),
644
- });
494
+ | Keys | Used for |
495
+ |------|----------|
496
+ | `sentTimestampColor` / `receivedTimestampColor` | Message & file timestamps |
497
+ | `sentMessageTextColor` / `receivedMessageTextColor` | Bubble text |
498
+ | `sentBubbleBackgroundColor` / `receivedBubbleBackgroundColor` | Bubble background |
499
+ | `sentMessageTailColor` / `receivedMessageTailColor` | Corner tail (`ArrowBack2RoundedIcon`) |
500
+ | `sentFileAttachmentBackground` / `receivedFileAttachmentBackground` | File chip |
501
+ | `sentFileAttachmentTextColor` / `receivedFileAttachmentTextColor` | File name |
502
+ | `sentFileAttachmentSubtitleColor` / `receivedFileAttachmentSubtitleColor` | MIME line |
503
+ | `sentAudioWaveformColor` / `receivedAudioWaveformColor` | Inactive waveform bars |
504
+ | `sentAudioWaveformActiveColor` / `receivedAudioWaveformActiveColor` | Active bars + scrubber |
505
+ | `sentAudioTimestampColor` / `receivedAudioTimestampColor` | Duration under waveform |
506
+ | `sentAudioPlayIconColor` / `receivedAudioPlayIconColor` | Play icon |
507
+ | `sentAudioPauseIconColor` / `receivedAudioPauseIconColor` | Pause icon |
508
+ | `sentAudioSpeedTextColor` / `receivedAudioSpeedTextColor` | **1x / 1.5x / 2x** pill text |
509
+ | `sentMediaTimestampBackground` / `receivedMediaTimestampBackground` | Timestamp pill on file-only bubbles |
645
510
 
646
- if (!loaded) return null;
511
+ **Not themeable:** image/video-only messages (no text, no audio) always use **white** (`#ffffff`) for the timestamp text.
647
512
 
648
- return <ChatScreen theme={{ fontFamily: 'my-custom-font' }} />;
649
- ```
513
+ ### Shared colors
650
514
 
651
- ### Keyboard avoiding
515
+ `inputsIconsColor`, `sendIconsColor`, `placeholderTextColor`, `inputTextColor`, `sentIconColor`, `deliveredIconColor`, `readIconColor`, `videoPlayIconColor`
652
516
 
653
- The package lifts the input bar automatically when the keyboard opens:
517
+ ### `theme.sizes`
654
518
 
655
- - **Android** full keyboard height via a keyboard listener, applied as `marginBottom` on the input row.
656
- - **iOS** — `KeyboardAvoidingView` with `behavior="padding"` plus the same keyboard listener.
519
+ `inputIconSize`number (px) or twrnc class string; affects **emoji, paperclip, camera only** (not send/mic).
657
520
 
658
- In your screen:
521
+ ### `theme.bubbleStyle`
659
522
 
660
- ```tsx
661
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
523
+ `sent`, `received`, `avatarTextStyle`, `userNameStyle`, `avatarImageStyle`, typing styles.
662
524
 
663
- const insets = useSafeAreaInsets();
525
+ ### `theme.messageStyle`
664
526
 
665
- <View style={{ flex: 1 }}>
666
- <ChatScreen
667
- keyboardVerticalOffset={Platform.OS === 'ios' ? insets.top + 44 : 0}
668
- // ...
669
- />
670
- </View>
671
- ```
527
+ Text styles, file attachment styles, `progressBarStyle`, `activeProgressBarStyle`, `audioDurationStyle`, `audioSpeedButtonStyle`, `audioSpeedTextStyle`, media timestamp container styles.
672
528
 
673
- If your screen or navigator already handles the keyboard (e.g. wraps in its own `KeyboardAvoidingView`), pass `disableKeyboardAvoiding`:
529
+ ### `theme.inputStyles` / `theme.filePreviewStyle`
674
530
 
675
- ```tsx
676
- <ChatScreen disableKeyboardAvoiding />
677
- ```
531
+ Input row, send button, preview strip.
678
532
 
679
- ### Custom input bar
533
+ ### Example
680
534
 
681
535
  ```tsx
682
536
  <ChatScreen
683
- renderCustomInput={() => <MyOwnComposer onSend={handleSend} />}
537
+ theme={{
538
+ fontFamily: 'Inter-Regular',
539
+ colors: {
540
+ sentBubbleBackgroundColor: '#005C4B',
541
+ receivedBubbleBackgroundColor: '#1F2C34',
542
+ sentAudioSpeedTextColor: '#FFFFFF',
543
+ receivedAudioSpeedTextColor: '#E5E7EB',
544
+ sentAudioWaveformActiveColor: '#53BDEB',
545
+ receivedAudioWaveformActiveColor: '#53BDEB',
546
+ },
547
+ sizes: { inputIconSize: 22 },
548
+ }}
684
549
  />
685
550
  ```
686
551
 
687
- When `renderCustomInput` is provided the default `ChatInput` is not mounted. Preview props (`previewItems`, `closePreview`) are not wired automatically — handle them inside your custom component.
552
+ ---
688
553
 
689
- ### Opening file attachments (expo-sharing)
554
+ ## Keyboard behavior
690
555
 
691
- By default tapping a file-attachment chip in a bubble calls `Linking.openURL`. For Expo apps that need to share or download local files, supply `onFileAttachmentPress`:
556
+ Built into `ChatScreen`:
692
557
 
693
- ```bash
694
- yarn add expo-sharing
695
- ```
558
+ | Platform | Behavior |
559
+ |----------|----------|
560
+ | **Android** | `useKeyboardInset` sets `marginBottom` on the input row (= keyboard height) |
561
+ | **iOS** | Same inset **plus** `KeyboardAvoidingView` with `behavior="padding"` and `keyboardVerticalOffset` |
696
562
 
697
- ```tsx
698
- import * as Sharing from 'expo-sharing';
699
- import { Linking } from 'react-native';
700
- import type { MessageFileAttachment } from 'movius-chats/lib/typescript/types';
701
-
702
- const handleFilePress = async (file: MessageFileAttachment) => {
703
- const uri =
704
- file.uri.startsWith('http') || file.uri.startsWith('file:')
705
- ? file.uri
706
- : `file://${file.uri}`;
707
-
708
- const available = await Sharing.isAvailableAsync();
709
- if (available) {
710
- await Sharing.shareAsync(uri, { dialogTitle: file.name });
711
- } else {
712
- Linking.openURL(uri);
713
- }
714
- };
715
-
716
- <ChatScreen
717
- onFileAttachmentPress={handleFilePress}
718
- // ...
719
- />
720
- ```
721
-
722
- ### Long-press on a message
563
+ If your navigator already avoids the keyboard:
723
564
 
724
565
  ```tsx
725
- <ChatScreen
726
- onMessageLongPress={(message) => {
727
- // show action sheet: reply, copy, delete, etc.
728
- Alert.alert(message.id, message.text ?? '');
729
- }}
730
- />
566
+ <ChatScreen disableKeyboardAvoiding />
731
567
  ```
732
568
 
733
569
  ---
734
570
 
735
- ## Full-screen gallery viewer
736
-
737
- Tapping any image or video in a bubble opens a full-screen modal viewer:
571
+ ## Custom components & icons
738
572
 
739
- - **Horizontal swipe** (`FlatList` paginated) moves between items in the same message.
740
- - **Counter** `n / total` shown at the top when there are multiple items.
741
- - **Images** fill the screen (`resizeMode: contain`).
742
- - **Videos** use native controls (play, pause, seek, full-screen).
743
- - **Close** with the × button in the top-right corner.
573
+ | Prop | Replaces |
574
+ |------|----------|
575
+ | `renderCustomInput` | Entire input + recorder (you handle preview yourself) |
576
+ | `renderCustomTyping` | “Typing…” content |
577
+ | `renderCustomVideoBubbleError` | Inline video error in grid |
578
+ | `renderVoiceRecorder` | Built-in recorder bars |
579
+ | `CustomEmojiIcon` | Emoji button |
580
+ | `CustomAttachmentIcon` | Paperclip |
581
+ | `CustomCameraIcon` | Camera |
582
+ | `CustomSendIcon` | Send |
583
+ | `CustomMicrophoneIcon` | Mic |
584
+ | `CustomPlayIcon` / `CustomPauseIcon` | Audio (and related) playback |
585
+ | `CustomFileIcon` | Document chip icon |
586
+ | `CustomImagePreview` / `CustomVideoPreview` | Composer thumbnails |
744
587
 
745
- The viewer opens automatically — no extra code needed in your app.
588
+ ### File attachments without Expo
746
589
 
747
- ---
590
+ Default tap uses React Native `Linking.openURL`. For local files or share sheets, use your own module in the host app:
748
591
 
749
- ## Architecture overview
592
+ ```tsx
593
+ import { Linking } from 'react-native';
594
+ // or: react-native-share, react-native-blob-util, etc.
750
595
 
751
- ```
752
- ChatScreen
753
- ├── AudioProvider one audio plays at a time (context)
754
- ├── ChatProvider all props + gallery state (context)
755
-
756
- ├── FlatList (inverted)
757
- │ └── ChatBubble per message
758
- │ ├── MediaGrid WhatsApp-style 1/2/3/4+ image-video grid
759
- │ ├── MessageContent file chips, audio player, parsed text
760
- │ └── MessageStatus timestamp + sent/delivered/read icons
761
-
762
- ├── ChatInput (optional)
763
- │ ├── TextInput auto-grows, resets on clear or send
764
- │ ├── EmojiFunnySquareIcon / PaperClipIcon / CameraIcon
765
- │ │ (sized by theme.sizes.inputIconSize — not send/mic)
766
- │ ├── PaperPlaneIcon / MicrophoneIcon (fixed size)
767
- │ └── FilePreview fan spread for images/videos, chips for docs
768
-
769
- └── MediaViewer full-screen horizontal pager (Modal)
770
- └── ViewerPage Image (contain) or Video (native controls)
596
+ onFileAttachmentPress={async (file) => {
597
+ const uri = file.uri.startsWith('file://') ? file.uri : `file://${file.uri}`;
598
+ await Linking.openURL(uri);
599
+ }}
771
600
  ```
772
601
 
773
602
  ---
774
603
 
775
- ## TypeScript types
776
-
777
- Import all types from the build output:
604
+ ## TypeScript
778
605
 
779
606
  ```ts
780
607
  import ChatScreen from 'movius-chats';
@@ -784,35 +611,38 @@ import type {
784
611
  MessageMediaItem,
785
612
  MessageFileAttachment,
786
613
  PreviewAttachment,
614
+ RecordingResult,
787
615
  ChatScreenProps,
616
+ VoiceRecorderExposedState,
617
+ VoiceRecorderConfig,
618
+ VoiceRecorderStyleOverrides,
619
+ RecordingUIProps,
788
620
  } from 'movius-chats/lib/typescript/types';
789
621
  ```
790
622
 
623
+ Source types while developing against the repo: `movius-chats/src/types` (field `"react-native": "src"` in package.json).
624
+
791
625
  ---
792
626
 
793
627
  ## Troubleshooting
794
628
 
795
- | Symptom | Fix |
796
- |---------|-----|
797
- | `resolveAssetSource is not a function` | Auto-patched by movius-chats `postinstall`. If it still occurs, delete `node_modules`, reinstall, then rebuild native code |
798
- | `Native module not found` | Rebuild the app after install — run `pod install` (iOS) and rebuild Android |
799
- | Crashes in Expo Go | Use a development build this package uses native modules not in Expo Go |
800
- | Audio silent on iOS | Add `[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil]` in your `AppDelegate` |
801
- | Video/audio URL not loading | Check URI scheme (`https://`, `file://`) and `INTERNET` permission on Android |
802
- | Reanimated worklet error | Move `'react-native-reanimated/plugin'` to the **last** position in Babel plugins |
803
- | Font has no effect | Load the font in your app first (Expo: `expo-font` plugin or `useFonts`); use the exact registered name |
804
- | Input icon size not changing | `inputIconSize` only affects emoji, clip, and camera — not the send or mic button |
805
- | Input doesn't return to pill shape | Happens when text is deleted character by character. In the latest version the layout resets when the field empties and on send |
806
- | Keyboard overlaps input (Android) | Add `"softwareKeyboardLayoutMode": "resize"` to `app.json`; wrap `ChatScreen` in `flex: 1` |
807
- | Keyboard offset wrong (iOS) | Tune `keyboardVerticalOffset` — it should equal your header height + status bar height |
808
- | Messages appear in wrong order | Newest message must be at `messages[0]` (inverted FlatList) |
809
- | Feature buttons missing | Feature flags (`showAttachmentsButton`, etc.) default to `false` — pass `true` to show them |
810
- | Gallery does not swipe | Ensure `mediaItems` is an array; single `image`/`video` strings open the viewer for that single item |
811
- | File attachment tap does nothing | Default is `Linking.openURL`. For local files on iOS/Android use `onFileAttachmentPress` with `expo-sharing` |
812
- | Tapping × removes all previews | Supply `onRemovePreviewItem` — without it the fallback `closePreview` clears everything |
629
+ | Problem | What to do |
630
+ |---------|------------|
631
+ | `Cannot read property 'init' of null` | Install `react-native-audio-record`, run `pod install`, **rebuild** the app |
632
+ | Recording never starts | Mic permission; iOS `NSMicrophoneUsageDescription`; Android `RECORD_AUDIO` |
633
+ | No audio playback | Ensure `react-native-video` is linked; URI must be `file://` or `http(s)://` |
634
+ | `NoSuchMethodError` `DefaultLoadControl` (Android) | Force `androidx.media3` to **1.3.1** in the app `android/build.gradle` `resolutionStrategy` |
635
+ | Reanimated error | `react-native-reanimated/plugin` must be **last** in Babel plugins |
636
+ | Font not applied | Register font in the **host** app; pass exact `fontFamily` string |
637
+ | `inputIconSize` ignored on send/mic | By design |
638
+ | Keyboard covers input (Android) | `android:windowSoftInputMode="adjustResize"`; parent `flex: 1` |
639
+ | × clears all previews | Implement `onRemovePreviewItem` |
640
+ | Wrong audio avatar | Set `senderAvatar` and `senderName` on the `Message` |
641
+ | Messages upside down | Newest at `messages[0]` |
813
642
 
814
643
  ---
815
644
 
645
+
816
646
  ## License
817
647
 
818
648
  ISC — see [package.json](./package.json).