movius-chats 1.13.0 → 1.14.0

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.
Files changed (2) hide show
  1. package/README.md +710 -1206
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -4,9 +4,9 @@
4
4
 
5
5
  A production-ready React Native chat UI library providing message bubbles, WhatsApp-style media grids, audio playback, voice recording, typing indicators, message replies, full-screen media gallery, and comprehensive theming—all wrapped in a single, powerful `ChatScreen` component. Works with both bare React Native and Expo.
6
6
 
7
- **npm:** [`movius-chats`](https://www.npmjs.com/package/movius-chats)
8
- **Repo:** [github.com/David-Atueyi/Movius-Chats](https://github.com/David-Atueyi/Movius-Chats)
9
- **Version:** 1.6.0 | **License:** ISC
7
+ **npm:** [`movius-chats`](https://www.npmjs.com/package/movius-chats)
8
+ **Repo:** [github.com/David-Atueyi/Movius-Chats](https://github.com/David-Atueyi/Movius-Chats)
9
+ **Version:** 1.14.0 | **License:** ISC
10
10
 
11
11
  ---
12
12
 
@@ -18,6 +18,7 @@ A production-ready React Native chat UI library providing message bubbles, Whats
18
18
  4. [Quick start](#quick-start)
19
19
  5. [Core concepts](#core-concepts)
20
20
  - [Message data model](#message-data-model)
21
+ - [Audio items and bubble splitting](#audio-items-and-bubble-splitting)
21
22
  - [Message list order](#message-list-order)
22
23
  - [Context system](#context-system)
23
24
  6. [API reference](#api-reference)
@@ -67,6 +68,8 @@ A production-ready React Native chat UI library providing message bubbles, Whats
67
68
  - **Video autoplay control** (disabled in gallery by default)
68
69
  - **Video thumbnails** with play icon overlay
69
70
  - **Video playback** within the gallery with native controls
71
+ - **Mixed media + audio in one message** — images/videos render in the grid, with an attached audio note rendered underneath in the same bubble
72
+ - **Multiple audio clips in one message automatically split** into a primary inline audio bubble plus standalone bubbles for each additional clip, all sharing the same timestamp/status
70
73
 
71
74
  ### Audio & Voice
72
75
 
@@ -82,6 +85,7 @@ A production-ready React Native chat UI library providing message bubbles, Whats
82
85
  - Playback speed control (1.0x → 1.5x → 2.0x cycling)
83
86
  - Play/pause controls with status indicators
84
87
  - Sender avatar or current playback speed in the UI
88
+ - Natural compact sizing — audio never gets boxed into the image/video grid
85
89
  - **Audio context** ensuring only one audio plays at a time
86
90
  - **Microphone permission handling** (Android + iOS)
87
91
 
@@ -139,22 +143,23 @@ A production-ready React Native chat UI library providing message bubbles, Whats
139
143
 
140
144
  ## 📦 What is Included
141
145
 
142
- | Feature | Implementation |
143
- | ---------------------------- | -------------------------------------------------------------------- |
144
- | **Text rendering** | `react-native-parsed-text` (URLs, emails, phone numbers tappable) |
145
- | **Image / video albums** | `MediaGrid` — 1/2/3/4+ smart layout, 320px height |
146
- | **Full-screen media viewer** | `MediaViewer` — swipe navigation, counter, selective video autoplay |
147
- | **Audio playback** | `react-native-video` (hidden player) + custom waveform visualization |
148
- | **Variable playback speed** | 1.0x 1.5x 2.0x cycling during playback |
149
- | **Voice recording** | `react-native-audio-record` (optional peer) with gesture controls |
150
- | **File attachments** | Tappable rows with custom handler + default URL opener |
151
- | **Typing indicators** | Avatar stacking with "+N" badge for multiple typers |
152
- | **Input bar** | Growing TextField, emoji/clip/camera/send/mic buttons |
153
- | **Status indicators** | Sent/delivered/read animated checkmarks |
154
- | **Message replies** | Swipe-to-reply, inline display, edit tracking |
155
- | **Message actions** | Long-press menus with copy, forward, delete, edit |
156
- | **Selection mode** | Multi-select with batch operations |
157
- | **Theming** | Comprehensive dual-sided (sent/received) color system |
146
+ | Feature | Implementation |
147
+ | -------------------------------- | ---------------------------------------------------------------------------- |
148
+ | **Text rendering** | `react-native-parsed-text` (URLs, emails, phone numbers tappable) |
149
+ | **Image / video albums** | `MediaGrid` — 1/2/3/4+ smart layout, 320px height, images/videos only |
150
+ | **Full-screen media viewer** | `MediaViewer` — swipe navigation, counter, selective video autoplay |
151
+ | **Audio playback** | `react-native-video` (hidden player) + custom waveform visualization |
152
+ | **Multi-audio bubble splitting** | Extra audio items in one message split into standalone bubbles automatically |
153
+ | **Variable playback speed** | 1.0x 1.5x 2.0x cycling during playback |
154
+ | **Voice recording** | `react-native-audio-record` (optional peer) with gesture controls |
155
+ | **File attachments** | Tappable rows with custom handler + default URL opener |
156
+ | **Typing indicators** | Avatar stacking with "+N" badge for multiple typers |
157
+ | **Input bar** | Growing TextField, emoji/clip/camera/send/mic buttons |
158
+ | **Status indicators** | Sent/delivered/read animated checkmarks |
159
+ | **Message replies** | Swipe-to-reply, inline display, edit tracking |
160
+ | **Message actions** | Long-press menus with copy, forward, delete, edit |
161
+ | **Selection mode** | Multi-select with batch operations |
162
+ | **Theming** | Comprehensive dual-sided (sent/received) color system |
158
163
 
159
164
  ---
160
165
 
@@ -165,41 +170,87 @@ A production-ready React Native chat UI library providing message bubbles, Whats
165
170
  - **React Native 0.60+**
166
171
  - **Node 14+**
167
172
 
168
- ### Step 1: Install the package
173
+ ### 1. Install movius-chats
169
174
 
170
175
  ```bash
171
- npm install movius-chats
172
- # or
173
176
  yarn add movius-chats
177
+ # or: npm install movius-chats
178
+ # or: bun add movius-chats
174
179
  ```
175
180
 
176
- ### Step 2: Install peer dependencies
181
+ ### 2. Install peers
177
182
 
178
183
  ```bash
179
- npm install react react-native react-native-gesture-handler react-native-reanimated
184
+ yarn add react-native-reanimated react-native-video react-native-svg react-native-gesture-handler
180
185
  ```
181
186
 
182
- ### Step 3: Install optional dependencies (feature-specific)
187
+ `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.
188
+
189
+ | Peer | Version | Required | Purpose |
190
+ | ----------------------------------- | ------- | -------- | ------------------------------ |
191
+ | `react` | ≥16.8 | Yes | React hooks, context |
192
+ | `react-native` | \* | Yes | Core framework |
193
+ | `react-native-gesture-handler` | ≥2.0 | Yes | Swipe & long-press gestures |
194
+ | `react-native-reanimated` | \* | Yes | Gesture animations |
195
+ | `react-native-audio-record` | \* | Optional | Voice recording capture |
196
+ | `react-native-fs` | \* | Optional | File system access (recording) |
197
+ | `@react-native-clipboard/clipboard` | \* | Optional | Copy to clipboard |
183
198
 
184
- For **voice recording:**
199
+ If the optional voice-recording peers are missing, the UI still renders; starting a recording logs an install hint instead of crashing.
185
200
 
186
- ```bash
187
- npm install react-native-audio-record react-native-fs
201
+ **There is no `react-native-sound`, `expo-av`, `expo-file-system`, or other Expo package in this library.**
202
+
203
+ ### 3. Reanimated (Babel)
204
+
205
+ Put this plugin **last** in `babel.config.js`:
206
+
207
+ ```js
208
+ module.exports = {
209
+ presets: ['module:metro-react-native-babel-preset'],
210
+ plugins: [
211
+ // ...other plugins
212
+ 'react-native-reanimated/plugin',
213
+ ],
214
+ };
188
215
  ```
189
216
 
190
- For **clipboard support (copy message):**
217
+ ### 4. Voice recording (optional)
191
218
 
192
219
  ```bash
193
- npm install @react-native-clipboard/clipboard
220
+ yarn add react-native-audio-record react-native-fs
221
+ ```
222
+
223
+ **iOS** — add to `Info.plist`:
224
+
225
+ ```xml
226
+ <key>NSMicrophoneUsageDescription</key>
227
+ <string>This app needs the microphone to record voice messages.</string>
228
+ <key>NSCameraUsageDescription</key>
229
+ <string>This app needs camera access to take photos.</string>
230
+ <key>NSPhotoLibraryUsageDescription</key>
231
+ <string>This app needs photo library access.</string>
232
+ ```
233
+
234
+ **Android** — add to `AndroidManifest.xml`:
235
+
236
+ ```xml
237
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
238
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
239
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
240
+ <uses-permission android:name="android.permission.CAMERA" />
194
241
  ```
195
242
 
196
- For **file attachments** (already in React Native):
243
+ (`RECORD_AUDIO` is often added automatically by `react-native-audio-record`.)
244
+
245
+ Then rebuild native apps:
197
246
 
198
247
  ```bash
199
- # No additional packages needed; uses Linking API
248
+ cd ios && pod install && cd ..
249
+ npx react-native run-ios
250
+ npx react-native run-android
200
251
  ```
201
252
 
202
- ### Step 4: Link native modules
253
+ ### 5. Native linking
203
254
 
204
255
  **Bare React Native:**
205
256
 
@@ -217,82 +268,64 @@ android.useAndroidX=true
217
268
  android.enableJetifier=true
218
269
  ```
219
270
 
220
- ### Step 5: Configure permissions
221
-
222
- **Android** (`AndroidManifest.xml`):
271
+ ### 6. Android keyboard
223
272
 
224
- ```xml
225
- <uses-permission android:name="android.permission.RECORD_AUDIO" />
226
- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
227
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
228
- <uses-permission android:name="android.permission.CAMERA" />
229
- ```
230
-
231
- **iOS** (`Info.plist`):
273
+ In `android/app/src/main/AndroidManifest.xml` on your main activity:
232
274
 
233
275
  ```xml
234
- <key>NSMicrophoneUsageDescription</key>
235
- <string>This app needs microphone access to record voice messages.</string>
236
- <key>NSCameraUsageDescription</key>
237
- <string>This app needs camera access to take photos.</string>
238
- <key>NSPhotoLibraryUsageDescription</key>
239
- <string>This app needs photo library access.</string>
276
+ android:windowSoftInputMode="adjustResize"
240
277
  ```
241
278
 
242
279
  ---
243
280
 
244
281
  ## Quick Start
245
282
 
246
- ### Basic Usage
247
-
248
283
  ```tsx
249
284
  import React, { useState } from 'react';
250
- import { ChatScreen } from 'movius-chats';
251
- import type { Message } from 'movius-chats';
285
+ import { Platform, SafeAreaView, View } from 'react-native';
286
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
287
+ import ChatScreen from 'movius-chats';
288
+ import type { Message } from 'movius-chats/lib/typescript/types';
252
289
 
253
- export const MyChatApp = () => {
254
- const [messages, setMessages] = useState<Message[]>([
255
- {
256
- id: '1',
257
- text: 'Hey! How are you?',
258
- senderId: 'user-2',
259
- time: '10:30 AM',
260
- status: 'read',
261
- senderName: 'Alice',
262
- senderAvatar: 'https://example.com/avatar-alice.jpg',
263
- },
264
- {
265
- id: '2',
266
- text: "I'm doing great! How about you?",
267
- senderId: 'user-1',
268
- time: '10:31 AM',
269
- status: 'delivered',
270
- },
271
- ]);
272
-
273
- const currentUserId = 'user-1';
274
-
275
- const handleSendMessage = (messageText: string) => {
276
- const newMessage: Message = {
277
- id: String(Date.now()),
278
- text: messageText,
279
- senderId: currentUserId,
280
- time: new Date().toLocaleTimeString(),
281
- status: 'sent',
282
- };
283
- setMessages((prev) => [...prev, newMessage]);
284
- };
290
+ export default function ChatDetailScreen() {
291
+ const insets = useSafeAreaInsets();
292
+ const currentUserId = '1';
293
+ const [messages, setMessages] = useState<Message[]>([]);
285
294
 
286
295
  return (
287
- <ChatScreen
288
- messages={messages}
289
- currentUserId={currentUserId}
290
- onSendMessage={handleSendMessage}
291
- />
296
+ <SafeAreaView style={{ flex: 1 }}>
297
+ <View style={{ flex: 1 }}>
298
+ <ChatScreen
299
+ messages={messages}
300
+ currentUserId={currentUserId}
301
+ onSendMessage={({ text, senderId }) => {
302
+ setMessages((prev) => [
303
+ {
304
+ id: String(Date.now()),
305
+ text,
306
+ senderId,
307
+ time: new Date().toLocaleTimeString([], {
308
+ hour: '2-digit',
309
+ minute: '2-digit',
310
+ }),
311
+ status: 'sent',
312
+ },
313
+ ...prev,
314
+ ]);
315
+ }}
316
+ keyboardVerticalOffset={Platform.OS === 'ios' ? insets.top : 0}
317
+ showBubbleTail
318
+ showMessageStatus
319
+ showVoiceRecordButton
320
+ />
321
+ </View>
322
+ </SafeAreaView>
292
323
  );
293
- };
324
+ }
294
325
  ```
295
326
 
327
+ Wrap the screen in `flex: 1`. Load custom fonts in **your** app before passing `theme.fontFamily`.
328
+
296
329
  ---
297
330
 
298
331
  ## Core Concepts
@@ -305,24 +338,23 @@ Every message in movius-chats follows this **unified interface**:
305
338
  export interface Message {
306
339
  id: string; // Unique message identifier
307
340
  text?: string; // Text content
308
- image?: string; // Single image URI (legacy)
309
- video?: string; // Single video URI (legacy)
310
- audio?: string; // Audio file URI
311
341
  senderId: string; // ID of message sender
312
342
  time: string; // Timestamp (e.g., "10:30 AM")
313
343
  status: 'read' | 'delivered' | 'sent'; // Message status
314
344
  senderName?: string; // Sender display name
315
345
  senderAvatar?: string; // Sender avatar URI
316
- mediaItems?: MessageMediaItem[]; // Array of images, videos, or audio
346
+ mediaItems?: MessageMediaItem[]; // Images, videos, and/or audio — preferred way to attach media
317
347
  fileAttachments?: MessageFileAttachment[]; // Array of file attachments
318
348
  replyTo?: MessageReply; // Reply context
319
349
  edited?: boolean; // Whether message was edited
320
350
  }
321
351
 
322
- // Media item in album
352
+ // Media item — image, video, or audio, all in one array
323
353
  export interface MessageMediaItem {
324
354
  uri: string;
325
355
  kind: 'image' | 'video' | 'audio';
356
+ name?: string; // optional — filename, used when uploading
357
+ type?: string; // optional — MIME type, used when uploading
326
358
  }
327
359
 
328
360
  // File attachment
@@ -342,6 +374,56 @@ export interface MessageReply {
342
374
  }
343
375
  ```
344
376
 
377
+ ### Audio items and bubble splitting
378
+
379
+ A message's `mediaItems` array can contain any mix of `image`, `video`, and `audio` items. Rendering follows these rules:
380
+
381
+ - **All `image`/`video` items** render together in the media grid (1/2/3/4+ layout), exactly as before.
382
+ - **The first `audio` item** in `mediaItems` renders attached to the same bubble, directly under the media grid and/or text — this is what a voice note, or an image with an attached audio caption, looks like.
383
+ - **Any additional `audio` items** beyond the first automatically split into their own standalone bubbles, rendered immediately after the main bubble. Each split bubble inherits the same `time` and `status` as the parent message, so timestamps and read/delivered/sent checkmarks stay consistent across the whole group.
384
+ - **Audio never renders inside the image/video grid.** It always uses its own compact waveform UI (~240–280px wide) rather than being stretched into a 320px grid cell.
385
+
386
+ **Examples:**
387
+
388
+ ```tsx
389
+ // Single audio message
390
+ {
391
+ id: '123',
392
+ mediaItems: [{ uri: 'file:///path/to/audio.m4a', kind: 'audio' }],
393
+ senderId: 'user-1',
394
+ time: '10:45 AM',
395
+ status: 'sent',
396
+ }
397
+
398
+ // Images with an attached audio note — one bubble: grid on top, waveform below
399
+ {
400
+ id: '124',
401
+ mediaItems: [
402
+ { uri: 'https://example.com/photo1.jpg', kind: 'image' },
403
+ { uri: 'https://example.com/photo2.jpg', kind: 'image' },
404
+ { uri: 'file:///path/to/note.wav', kind: 'audio' },
405
+ ],
406
+ senderId: 'user-1',
407
+ time: '10:46 AM',
408
+ status: 'sent',
409
+ }
410
+
411
+ // Multiple audio clips — first attaches inline, rest split into their own bubbles
412
+ {
413
+ id: '125',
414
+ mediaItems: [
415
+ { uri: 'file:///path/to/clip1.wav', kind: 'audio' },
416
+ { uri: 'file:///path/to/clip2.wav', kind: 'audio' },
417
+ { uri: 'file:///path/to/clip3.wav', kind: 'audio' },
418
+ ],
419
+ senderId: 'user-1',
420
+ time: '10:47 AM',
421
+ status: 'sent',
422
+ }
423
+ ```
424
+
425
+ This means: sending images + one voice note produces a single bubble with the grid on top and the waveform underneath. Sending several voice notes together produces one bubble for the first clip plus a separate standalone bubble for each additional clip — visually indistinguishable from sending them as separate messages, but grouped under one logical `Message`.
426
+
345
427
  ### Message List Order
346
428
 
347
429
  By default, messages are displayed **newest-first** (reverse chronological). The component expects:
@@ -388,17 +470,18 @@ interface ChatScreenProps {
388
470
  // Data
389
471
  messages: Message[]; // Message array (newest first)
390
472
  currentUserId: string; // Current user's ID
391
- typingUsers?: Array<{ id: string; name: string }>; // Who's typing
473
+ typingUsers?: Array<{ id: string; avatar: string; name: string }>; // Who's typing
392
474
 
393
475
  // Callbacks
394
- onSendMessage: (text: string) => void;
476
+ onSendMessage: (message: Omit<Message, 'id' | 'time' | 'status'>) => void;
395
477
  onAttachmentPress?: () => void;
396
478
  onCameraPress?: () => void;
397
- onAudioRecordEnd?: (result: RecordingResult) => void;
479
+ onAudioRecordEnd?: (result?: RecordingResult) => void;
398
480
  onAudioRecordStart?: () => void;
399
481
  onMessageLongPress?: (message: Message) => void;
400
482
  onTypingStart?: () => void;
401
483
  onTypingEnd?: () => void;
484
+ onFileAttachmentPress?: (file: MessageFileAttachment) => void;
402
485
  onEndReached?: (info: { distanceFromEnd: number }) => void;
403
486
  onEndReachedThreshold?: number; // Default: 0.5
404
487
  isLoadingMoreMessages?: boolean;
@@ -410,42 +493,91 @@ interface ChatScreenProps {
410
493
  loadingMoreIndicatorSize?: number | 'small' | 'large';
411
494
 
412
495
  // UI Options
413
- showAvatars?: boolean; // Default: true
414
- showUserNames?: boolean; // Default: true
415
- showBubbleTail?: boolean; // Default: true
416
- renderCustomInput?: (props: InputProps) => React.ReactNode;
496
+ showAvatars?: boolean;
497
+ showUserNames?: boolean;
498
+ showBubbleTail?: boolean;
499
+ showMessageStatus?: boolean;
500
+ showEmojiButton?: boolean;
501
+ showAttachmentsButton?: boolean;
502
+ showCameraButton?: boolean;
503
+ showVoiceRecordButton?: boolean;
504
+ renderCustomInput?: () => React.ReactNode;
417
505
 
418
506
  // Custom Icons (optional)
419
- CustomEmojiIcon?: React.ComponentType;
420
- CustomAttachmentIcon?: React.ComponentType;
421
- CustomCameraIcon?: React.ComponentType;
422
- CustomMicrophoneIcon?: React.ComponentType;
423
- CustomSendIcon?: React.ComponentType;
424
- CustomFileIcon?: React.ComponentType;
425
- CustomImagePreview?: React.ComponentType;
426
- CustomVideoPreview?: React.ComponentType;
427
-
428
- // Feature Configs
429
- messageActionsConfig?: MessageActionsConfig;
507
+ CustomEmojiIcon?: () => React.ReactNode;
508
+ CustomAttachmentIcon?: () => React.ReactNode;
509
+ CustomCameraIcon?: () => React.ReactNode;
510
+ CustomMicrophoneIcon?: () => React.ReactNode;
511
+ CustomSendIcon?: () => React.ReactNode;
512
+ CustomFileIcon?: React.ComponentType<{ style?: any }>;
513
+ CustomImagePreview?: React.ComponentType<{ uri: string }>;
514
+ CustomVideoPreview?: React.ComponentType<{ uri: string }>;
515
+ CustomPlayIcon?: () => React.ReactNode;
516
+ CustomPauseIcon?: () => React.ReactNode;
517
+ CustomVoiceRecorder?: (state: VoiceRecorderExposedState) => React.ReactNode;
518
+
519
+ // Reply
520
+ onReplyMessage?: (message: Message) => void;
430
521
  replyProps?: ReplyConfig;
431
- voiceRecorderConfig?: VoiceRecorderConfig;
432
- recordingUIProps?: RecordingUIProps;
522
+ replyUI?: ReplyUIProps;
523
+ replyStyle?: ReplyStyleOverrides;
524
+ renderReplyPreview?: (
525
+ message: Message,
526
+ cancel: () => void
527
+ ) => React.ReactNode;
528
+ renderInlineReply?: (
529
+ reply: MessageReply,
530
+ isCurrentUser: boolean
531
+ ) => React.ReactNode;
532
+
533
+ // Long-press message actions
534
+ messageActionProps?: MessageActionFlags;
535
+ messageActionUI?: MessageActionUIProps;
536
+ messageActionLabels?: MessageActionLabels;
537
+ messageActionIcons?: MessageActionIconComponents;
538
+ renderMessageActions?: (
539
+ message: Message,
540
+ close: () => void,
541
+ anchor?: MessageActionAnchor
542
+ ) => React.ReactNode;
543
+ onCopyMessage?: (message: Message) => void;
544
+ onEditMessage?: (message: Message, newText: string) => void;
545
+ onDeleteMessage?: (message: Message) => void;
546
+ onForwardMessage?: (message: Message) => void;
547
+
548
+ // Multi-select mode
549
+ selectionUI?: SelectionUIProps;
550
+ onSelectionChange?: (selectedIds: string[]) => void;
551
+ onDeleteSelected?: (messages: Message[]) => void;
552
+ onForwardSelected?: (messages: Message[]) => void;
553
+ onCopySelected?: (messages: Message[]) => void;
554
+
555
+ // "edited" indicator
556
+ editedLabel?: string;
557
+ editedTextStyle?: TextStyle;
558
+
559
+ keyboardVerticalOffset?: number;
560
+ disableKeyboardAvoiding?: boolean;
561
+
562
+ // Composer preview
563
+ placeholder?: string;
564
+ previewData?: PreviewAttachment;
565
+ previewItems?: PreviewAttachment[];
566
+ closePreview?: () => void;
567
+ onRemovePreviewItem?: (uri: string) => void;
433
568
 
434
569
  // Theming
435
570
  theme?: ChatScreenTheme;
436
-
437
- // Selection
438
- selectionUI?: SelectionUIProps;
439
571
  }
440
572
  ```
441
573
 
442
574
  ### Message Interface
443
575
 
444
- See [Message Data Model](#message-data-model) section above for full details.
576
+ See [Message Data Model](#message-data-model) and [Audio items and bubble splitting](#audio-items-and-bubble-splitting) above for full details.
445
577
 
446
578
  ### Loading More Messages
447
579
 
448
- When the user reaches the top of the message list while older messages are being fetched, the component can show a loading indicator. This is controlled by the pagination props above.
580
+ When the user reaches the top of the message list while older messages are being fetched, the component can show a loading indicator:
449
581
 
450
582
  ```tsx
451
583
  <ChatScreen
@@ -466,63 +598,121 @@ When the user reaches the top of the message list while older messages are being
466
598
 
467
599
  ```tsx
468
600
  interface ChatScreenTheme {
601
+ fontFamily?: string;
602
+
469
603
  colors?: {
470
604
  // Sent messages
471
- sentBubbleBackground?: string;
472
- sentTextColor?: string;
605
+ sentBubbleBackgroundColor?: string;
606
+ sentMessageTextColor?: string;
473
607
  sentTimestampColor?: string;
474
- sentStatusIconColor?: string;
608
+ sentMessageTailColor?: string;
609
+ sentIconColor?: string;
610
+ deliveredIconColor?: string;
611
+ readIconColor?: string;
475
612
  sentAudioWaveformColor?: string;
476
613
  sentAudioWaveformActiveColor?: string;
614
+ sentAudioTimestampColor?: string;
615
+ sentAudioPlayIconColor?: string;
616
+ sentAudioPauseIconColor?: string;
617
+ sentAudioPlayButtonBackground?: string;
618
+ sentAudioSpeedTextColor?: string;
477
619
  sentFileAttachmentBackground?: string;
478
620
  sentFileAttachmentTextColor?: string;
621
+ sentFileAttachmentSubtitleColor?: string;
479
622
  sentMediaTimestampBackground?: string;
480
623
 
481
624
  // Received messages
482
- receivedBubbleBackground?: string;
483
- receivedTextColor?: string;
625
+ receivedBubbleBackgroundColor?: string;
626
+ receivedMessageTextColor?: string;
484
627
  receivedTimestampColor?: string;
628
+ receivedMessageTailColor?: string;
485
629
  receivedAudioWaveformColor?: string;
486
630
  receivedAudioWaveformActiveColor?: string;
631
+ receivedAudioTimestampColor?: string;
632
+ receivedAudioPlayIconColor?: string;
633
+ receivedAudioPauseIconColor?: string;
634
+ receivedAudioPlayButtonBackground?: string;
635
+ receivedAudioSpeedTextColor?: string;
487
636
  receivedFileAttachmentBackground?: string;
488
637
  receivedFileAttachmentTextColor?: string;
638
+ receivedFileAttachmentSubtitleColor?: string;
489
639
  receivedMediaTimestampBackground?: string;
490
640
 
491
- // UI elements
492
- inputBarBackground?: string;
493
- inputPlaceholderTextColor?: string;
641
+ // Shared / UI
642
+ videoPlayIconColor?: string;
494
643
  inputTextColor?: string;
495
- typingIndicatorDotColor?: string;
644
+ inputsIconsColor?: string;
645
+ sendIconsColor?: string;
646
+ placeholderTextColor?: string;
647
+ };
648
+
649
+ sizes?: {
650
+ inputIconSize?: string | number;
496
651
  };
497
652
 
498
- // Fonts & Sizes
499
- fontFamily?: string; // Default: system font
500
- fontSize?: {
501
- message?: number; // Default: 16
502
- timestamp?: number; // Default: 12
503
- typing?: number; // Default: 14
653
+ bubbleStyle?: {
654
+ sent?: ViewStyle;
655
+ received?: ViewStyle;
656
+ avatarTextStyle?: TextStyle;
657
+ userNameStyle?: TextStyle;
658
+ avatarImageStyle?: ImageStyle;
659
+ typingContainerStyle?: ViewStyle;
660
+ additionalTypingUsersContainerStyle?: ViewStyle;
661
+ additionalTypingUsersTextStyle?: TextStyle;
662
+ typingTextStyle?: TextStyle;
504
663
  };
505
664
 
506
- // Styling overrides
507
665
  messageStyle?: {
508
- sentBubbleStyle?: ViewStyle;
509
- receivedBubbleStyle?: ViewStyle;
666
+ sentTextStyle?: TextStyle;
667
+ receivedTextStyle?: TextStyle;
668
+ sentFileAttachmentStyle?: ViewStyle;
669
+ receivedFileAttachmentStyle?: ViewStyle;
670
+ sentFileAttachmentTextStyle?: TextStyle;
671
+ receivedFileAttachmentTextStyle?: TextStyle;
672
+ sentFileAttachmentSubtitleStyle?: TextStyle;
673
+ receivedFileAttachmentSubtitleStyle?: TextStyle;
510
674
  sentMediaTimestampContainerStyle?: ViewStyle;
511
675
  receivedMediaTimestampContainerStyle?: ViewStyle;
676
+ progressBarStyle?: ViewStyle;
677
+ activeProgressBarStyle?: ViewStyle;
678
+ audioDurationStyle?: TextStyle;
679
+ audioSpeedButtonStyle?: ViewStyle;
680
+ audioSpeedTextStyle?: TextStyle;
681
+ editedTextStyle?: TextStyle;
682
+ };
683
+
684
+ inputStyles?: {
685
+ inputSectionContainerStyle?: ViewStyle;
686
+ inputContainerStyle?: ViewStyle;
687
+ sendButtonStyle?: ViewStyle;
688
+ };
689
+
690
+ filePreviewStyle?: {
691
+ root?: ViewStyle;
692
+ container?: ViewStyle;
693
+ iconContainer?: ViewStyle;
694
+ nameContainer?: ViewStyle;
695
+ text?: TextStyle;
512
696
  };
513
697
 
514
- // Media grid
515
- mediaGrid?: {
516
- height?: number; // Default: 320
517
- borderRadius?: number; // Default: 12
698
+ voiceRecorder?: {
699
+ ui?: RecordingUIProps;
700
+ styles?: VoiceRecorderStyleOverrides;
701
+ config?: VoiceRecorderConfig;
518
702
  };
519
703
 
520
- // Input bar
521
- inputBar?: {
522
- maxHeight?: number; // Default: 118
523
- minHeight?: number; // Default: 32-50
524
- iconSize?: number; // Default: computed
704
+ reply?: {
705
+ ui?: ReplyUIProps;
706
+ styles?: ReplyStyleOverrides;
525
707
  };
708
+
709
+ messageActions?: {
710
+ ui?: MessageActionUIProps;
711
+ labels?: MessageActionLabels;
712
+ icons?: MessageActionIconComponents;
713
+ };
714
+
715
+ selection?: SelectionUIProps;
526
716
  }
527
717
  ```
528
718
 
@@ -532,7 +722,7 @@ interface ChatScreenTheme {
532
722
 
533
723
  ### Voice Recording
534
724
 
535
- Voice messages combine **gesture-based capture** with **multiple recording modes**:
725
+ Voice messages combine **gesture-based capture** with **multiple recording modes**. Requires **`react-native-audio-record`** and **`react-native-fs`** in the host app, plus a **native rebuild**.
536
726
 
537
727
  #### Recording Modes
538
728
 
@@ -541,30 +731,44 @@ Voice messages combine **gesture-based capture** with **multiple recording modes
541
731
  3. **Slide-to-Cancel** — Swipe left while recording to cancel
542
732
  4. **Lock Recording** — Tap lock icon to record hands-free
543
733
 
734
+ #### Gestures
735
+
736
+ | Action | Result |
737
+ | ------------------------- | ---------------------------------------------------------------------------- |
738
+ | **Tap** mic | Normal bar: trash, timer, waveform, play/pause preview, send |
739
+ | **Long-press** mic | Hold mode: "slide to cancel", lock column above send |
740
+ | Slide **left** | Cancel (file deleted via `react-native-fs`) |
741
+ | Slide **up** to lock | Switches to normal bar (`lockSlideDistance` in `theme.voiceRecorder.config`) |
742
+ | **Release** without slide | Auto-send (`onAudioRecordEnd`) |
743
+
544
744
  #### Configuration
545
745
 
546
746
  ```tsx
547
747
  <ChatScreen
548
- voiceRecorderConfig={{
549
- maxDuration: 300, // 5 minutes max (seconds)
550
- enableSlideToCancel: true, // Show slide-to-cancel hint
551
- enableLockRecording: true, // Show lock option
552
- enableWaveform: true, // Show waveform during recording
553
- }}
554
- recordingUIProps={{
555
- timerColor: '#FFFFFF',
556
- waveformColor: '#E9EDEF',
557
- recordingBackground: '#0B141A',
558
- lockPillBackground: '#1F2937',
559
- lockIconColor: '#FFFFFF',
560
- chevronIconColor: '#8696A0',
561
- cancelTextColor: '#F15C6D',
562
- waveformBarCount: 32,
748
+ theme={{
749
+ voiceRecorder: {
750
+ config: {
751
+ maxDuration: 300, // 5 minutes max (seconds)
752
+ enableSlideToCancel: true, // Show slide-to-cancel hint
753
+ enableLockRecording: true, // Show lock option
754
+ enableWaveform: true, // Show waveform during recording
755
+ },
756
+ ui: {
757
+ timerColor: '#FFFFFF',
758
+ waveformColor: '#E9EDEF',
759
+ recordingBackground: '#0B141A',
760
+ lockPillBackground: '#1F2937',
761
+ lockIconColor: '#FFFFFF',
762
+ chevronIconColor: '#8696A0',
763
+ cancelTextColor: '#F15C6D',
764
+ waveformBarCount: 32,
765
+ },
766
+ },
563
767
  }}
564
768
  onAudioRecordEnd={(result) => {
769
+ if (!result) return;
565
770
  console.log('Recording saved to:', result.uri);
566
- console.log('Duration:', result.duration, 'ms');
567
- // Upload to server, add to message, etc.
771
+ console.log('Duration:', result.duration, 'sec');
568
772
  }}
569
773
  />
570
774
  ```
@@ -573,19 +777,68 @@ Voice messages combine **gesture-based capture** with **multiple recording modes
573
777
 
574
778
  ```tsx
575
779
  interface RecordingResult {
576
- uri: string; // File system path
577
- duration: number; // Milliseconds
780
+ uri: string; // File system path (always file://...)
781
+ duration: number; // Seconds
782
+ mimeType?: string; // e.g. 'audio/wav'
578
783
  size?: number; // Bytes
579
- mimeType?: string; // e.g., 'audio/m4a'
784
+ name?: string; // e.g. 'movius_rec_1783269276396.wav'
580
785
  }
581
786
  ```
582
787
 
788
+ `name` and `mimeType` are populated automatically by the built-in recorder — pass them straight through when building your `mediaItems` entry so your upload logic has the correct filename and MIME type instead of guessing.
789
+
790
+ #### Wiring
791
+
792
+ ```tsx
793
+ <ChatScreen
794
+ showVoiceRecordButton
795
+ onAudioRecordEnd={(result) => {
796
+ if (!result) return;
797
+ setMessages((prev) => [
798
+ {
799
+ id: String(Date.now()),
800
+ senderId: currentUserId,
801
+ mediaItems: [
802
+ {
803
+ uri: result.uri,
804
+ kind: 'audio',
805
+ name: result.name, // ✅ pass through for upload
806
+ type: result.mimeType, // ✅ pass through for upload
807
+ },
808
+ ],
809
+ time: '10:56 PM',
810
+ status: 'sent',
811
+ senderAvatar: myAvatarUri,
812
+ senderName: myDisplayName,
813
+ },
814
+ ...prev,
815
+ ]);
816
+ }}
817
+ />
818
+ ```
819
+
820
+ #### Custom recorder UI
821
+
822
+ ```tsx
823
+ <ChatScreen
824
+ CustomVoiceRecorder={(state) => (
825
+ <MyRecorder
826
+ duration={state.duration}
827
+ onStop={state.stopRecording}
828
+ onCancel={state.cancelRecording}
829
+ />
830
+ )}
831
+ />
832
+ ```
833
+
834
+ ---
835
+
583
836
  ### Audio Message Playback
584
837
 
585
- Play audio messages with professional waveform visualization and playback speed control:
838
+ Audio is sent through `mediaItems` with `kind: 'audio'` not a separate top-level field. See [Audio items and bubble splitting](#audio-items-and-bubble-splitting) for the full rendering rules.
586
839
 
587
840
  ```tsx
588
- // Add audio to a message
841
+ // Single audio message
589
842
  const audioMessage: Message = {
590
843
  id: '123',
591
844
  mediaItems: [{ uri: 'file:///path/to/audio.m4a', kind: 'audio' }],
@@ -594,6 +847,32 @@ const audioMessage: Message = {
594
847
  status: 'sent',
595
848
  senderAvatar: 'https://...',
596
849
  };
850
+
851
+ // Image(s) with an attached audio note — one bubble, grid on top, waveform below
852
+ const imageWithAudio: Message = {
853
+ id: '124',
854
+ mediaItems: [
855
+ { uri: 'https://example.com/photo1.jpg', kind: 'image' },
856
+ { uri: 'https://example.com/photo2.jpg', kind: 'image' },
857
+ { uri: 'file:///path/to/note.wav', kind: 'audio' },
858
+ ],
859
+ senderId: 'user-1',
860
+ time: '10:46 AM',
861
+ status: 'sent',
862
+ };
863
+
864
+ // Multiple audio clips — first attaches inline, rest split into standalone bubbles
865
+ const multipleAudioClips: Message = {
866
+ id: '125',
867
+ mediaItems: [
868
+ { uri: 'file:///path/to/clip1.wav', kind: 'audio' },
869
+ { uri: 'file:///path/to/clip2.wav', kind: 'audio' },
870
+ { uri: 'file:///path/to/clip3.wav', kind: 'audio' },
871
+ ],
872
+ senderId: 'user-1',
873
+ time: '10:47 AM',
874
+ status: 'sent',
875
+ };
597
876
  ```
598
877
 
599
878
  **Features:**
@@ -602,8 +881,29 @@ const audioMessage: Message = {
602
881
  - **Playback speed cycling:** 1.0x → 1.5x → 2.0x → 1.0x
603
882
  - **Sender avatar** displayed or current speed overlay
604
883
  - **Pause/resume** during playback
605
- - **Automatic pause** when another audio starts
606
- - **Duration display** in bottom-right of bubble
884
+ - **Automatic pause** when another audio starts (only one plays at a time, via `AudioContext`)
885
+ - **Duration display** under the waveform
886
+ - **Natural sizing** — audio bubbles size to fit the compact waveform UI, never stretched into the image/video grid box
887
+ - **Multiple audio auto-splitting** — extra audio items beyond the first in one message become their own standalone bubbles, inheriting the parent message's `time` and `status`
888
+
889
+ WhatsApp-style row inside each audio bubble:
890
+
891
+ | Side | Layout (left → right) |
892
+ | ------------ | -------------------------------------------- |
893
+ | **Sent** | Avatar or speed pill → play/pause → waveform |
894
+ | **Received** | play/pause → waveform → avatar or speed pill |
895
+
896
+ | State | Avatar slot |
897
+ | --------------- | ------------------------------------------------------- |
898
+ | Idle / finished | `senderAvatar` or first letter of `senderName` |
899
+ | Playing | Pill showing **1x**, **1.5x**, or **2x** (tap to cycle) |
900
+ | Ended | Avatar again |
901
+
902
+ - Waveform bars with scrubber dot; tap or drag to seek
903
+ - Play/pause is icon-only (no filled circle)
904
+ - Video in the gallery pauses other audio
905
+
906
+ ---
607
907
 
608
908
  ### Media Grids & Gallery
609
909
 
@@ -624,12 +924,13 @@ const albumMessage: Message = {
624
924
  ```
625
925
 
626
926
  **Smart Layouts:**
627
- | Count | Layout |
628
- |-------|--------|
629
- | 1 | Full-width square |
630
- | 2 | 50/50 side-by-side |
631
- | 3 | Large left + 2 stacked right |
632
- | 4+ | 2×2 grid |
927
+
928
+ | Count | Layout | Height |
929
+ | ----- | ---------------------------- | ------ |
930
+ | 1 | Full-width square | 320px |
931
+ | 2 | 50/50 side-by-side | 320px |
932
+ | 3 | Large left + 2 stacked right | 320px |
933
+ | 4+ | 2×2 grid, `+N` on last cell | 320px |
633
934
 
634
935
  **Gallery Features:**
635
936
 
@@ -640,12 +941,17 @@ const albumMessage: Message = {
640
941
  - Close button (X icon)
641
942
  - Full-screen immersive viewing
642
943
 
944
+ **Audio is never included in the grid or gallery.** Only `image`/`video` items are eligible for grid layout and full-screen viewing — audio items are filtered out and rendered via the standalone `AudioPlayer` UI instead (see [Audio items and bubble splitting](#audio-items-and-bubble-splitting)).
945
+
946
+ **Legacy single fields:** Legacy `image`, `video`, and `audio` fields on `Message` are normalized into `mediaItems` internally via `collectMediaItems()`. **New integrations should use `mediaItems` directly** rather than the legacy fields — they're kept only for backward compatibility with older message data and will not receive new features (e.g. the multi-audio splitting behavior only applies to items in `mediaItems`).
947
+
948
+ ---
949
+
643
950
  ### Message Replies
644
951
 
645
952
  Enable conversations within conversations:
646
953
 
647
954
  ```tsx
648
- // Reply configuration
649
955
  <ChatScreen
650
956
  replyProps={{
651
957
  enableReply: true,
@@ -686,18 +992,19 @@ const repliedMessage: Message = {
686
992
  - Edit indication ("Edited" chip in reply context)
687
993
  - Customizable colors and backgrounds
688
994
 
689
- ### Typing Indicators
995
+ ---
690
996
 
691
- Show who's currently typing:
997
+ ### Typing Indicators
692
998
 
693
999
  ```tsx
694
1000
  <ChatScreen
695
1001
  typingUsers={[
696
- { id: 'alice', name: 'Alice' },
697
- { id: 'bob', name: 'Bob' },
1002
+ { id: 'alice', avatar: 'https://...', name: 'Alice' },
1003
+ { id: 'bob', avatar: 'https://...', name: 'Bob' },
698
1004
  // If 3+ users, shows "Alice, Bob, +2 more"
699
1005
  ]}
700
- // ... other props
1006
+ onTypingStart={() => console.log('Typing started')}
1007
+ onTypingEnd={() => console.log('Typing ended')}
701
1008
  />
702
1009
  ```
703
1010
 
@@ -707,11 +1014,11 @@ Show who's currently typing:
707
1014
  - Up to 2 avatars displayed inline
708
1015
  - "+N more" badge if 3+ users
709
1016
  - Customizable dot color
710
- - Auto-dismisses when onTypingEnd called
1017
+ - Auto-dismisses when `onTypingEnd` called
711
1018
 
712
- ### File Attachments
1019
+ ---
713
1020
 
714
- Handle any file type with custom handlers:
1021
+ ### File Attachments
715
1022
 
716
1023
  ```tsx
717
1024
  const fileMessage: Message = {
@@ -739,51 +1046,52 @@ const fileMessage: Message = {
739
1046
 
740
1047
  ```tsx
741
1048
  <ChatScreen
742
- onAttachmentPress={() => {
743
- // Custom file picker/handler
1049
+ onFileAttachmentPress={async (file) => {
1050
+ const uri = file.uri.startsWith('file://')
1051
+ ? file.uri
1052
+ : `file://${file.uri}`;
1053
+ await Linking.openURL(uri);
1054
+ // or use react-native-share, react-native-blob-util, etc.
744
1055
  }}
745
- // ... other props
746
1056
  />
747
1057
  ```
748
1058
 
749
- ### Message Selection & Actions
1059
+ ---
750
1060
 
751
- Enable multi-select and bulk operations:
1061
+ ### Message Selection & Actions
752
1062
 
753
1063
  ```tsx
754
1064
  <ChatScreen
755
1065
  onMessageLongPress={(message) => {
756
- // Long-press opens action menu with options:
757
- // - Copy
758
- // - Forward
759
- // - Reply
760
- // - Edit
761
- // - Delete
762
- // Customizable via messageActionsConfig
1066
+ // Long-press opens action menu with copy/forward/reply/edit/delete
763
1067
  }}
764
- messageActionsConfig={{
765
- actions: [
766
- 'copy', // Copy text to clipboard
767
- 'forward', // Forward message
768
- 'reply', // Reply to message
769
- 'edit', // Edit message content
770
- 'delete', // Delete message
771
- ],
772
- // Custom labels
773
- labels: {
774
- copy: 'Copy Text',
775
- forward: 'Share',
776
- delete: 'Remove',
777
- },
778
- // Custom icons (SVG/images)
779
- icons: {
780
- copy: <CustomCopyIcon />,
781
- },
1068
+ messageActionProps={{
1069
+ enableReply: true,
1070
+ enableCopy: true,
1071
+ enableEdit: true,
1072
+ enableDelete: true,
1073
+ enableForward: true,
1074
+ enableSelect: true,
1075
+ }}
1076
+ messageActionLabels={{
1077
+ copy: 'Copy Text',
1078
+ forward: 'Share',
1079
+ delete: 'Remove',
1080
+ }}
1081
+ messageActionIcons={{
1082
+ copy: CustomCopyIcon,
782
1083
  }}
783
1084
  selectionUI={{
784
- checkboxSize: 24,
785
- checkboxColor: '#22c55e',
786
- selectedBackground: 'rgba(34, 197, 94, 0.1)',
1085
+ rowBackgroundColor: 'rgba(34, 197, 94, 0.1)',
1086
+ }}
1087
+ onDeleteSelected={(messages) => {
1088
+ /* ... */
1089
+ }}
1090
+ onForwardSelected={(messages) => {
1091
+ /* ... */
1092
+ }}
1093
+ onCopySelected={(messages) => {
1094
+ /* ... */
787
1095
  }}
788
1096
  />
789
1097
  ```
@@ -801,18 +1109,18 @@ Enable multi-select and bulk operations:
801
1109
  theme={{
802
1110
  colors: {
803
1111
  // Sent messages (right-aligned, typically green)
804
- sentBubbleBackground: '#22c55e',
805
- sentTextColor: '#FFFFFF',
1112
+ sentBubbleBackgroundColor: '#22c55e',
1113
+ sentMessageTextColor: '#FFFFFF',
806
1114
  sentTimestampColor: 'rgba(255, 255, 255, 0.7)',
807
- sentStatusIconColor: '#FFFFFF',
1115
+ sentIconColor: '#FFFFFF',
808
1116
  sentAudioWaveformColor: 'rgba(255, 255, 255, 0.3)',
809
1117
  sentAudioWaveformActiveColor: 'rgba(255, 255, 255, 0.95)',
810
1118
  sentFileAttachmentBackground: 'rgba(255, 255, 255, 0.15)',
811
1119
  sentFileAttachmentTextColor: '#FFFFFF',
812
1120
 
813
1121
  // Received messages (left-aligned, typically gray)
814
- receivedBubbleBackground: '#E5E7EB',
815
- receivedTextColor: '#1F2937',
1122
+ receivedBubbleBackgroundColor: '#E5E7EB',
1123
+ receivedMessageTextColor: '#1F2937',
816
1124
  receivedTimestampColor: 'rgba(107, 114, 128, 0.85)',
817
1125
  receivedAudioWaveformColor: 'rgba(0, 0, 0, 0.2)',
818
1126
  receivedAudioWaveformActiveColor: 'rgba(0, 0, 0, 0.6)',
@@ -820,10 +1128,10 @@ Enable multi-select and bulk operations:
820
1128
  receivedFileAttachmentTextColor: '#1F2937',
821
1129
 
822
1130
  // Input & UI
823
- inputBarBackground: '#FFFFFF',
824
1131
  inputTextColor: '#1F2937',
825
- inputPlaceholderTextColor: '#9CA3AF',
826
- typingIndicatorDotColor: '#9CA3AF',
1132
+ placeholderTextColor: '#9CA3AF',
1133
+ inputsIconsColor: '#6B7280',
1134
+ sendIconsColor: '#FFFFFF',
827
1135
  },
828
1136
  }}
829
1137
  />
@@ -834,12 +1142,7 @@ Enable multi-select and bulk operations:
834
1142
  ```tsx
835
1143
  <ChatScreen
836
1144
  theme={{
837
- fontFamily: 'Segoe UI', // Custom font
838
- fontSize: {
839
- message: 16,
840
- timestamp: 12,
841
- typing: 14,
842
- },
1145
+ fontFamily: 'Inter-Regular', // Custom font — load it in your app first
843
1146
  }}
844
1147
  />
845
1148
  ```
@@ -849,38 +1152,26 @@ Enable multi-select and bulk operations:
849
1152
  ```tsx
850
1153
  <ChatScreen
851
1154
  theme={{
852
- messageStyle: {
853
- sentBubbleStyle: {
1155
+ bubbleStyle: {
1156
+ sent: {
854
1157
  borderRadius: 16,
855
1158
  shadowColor: '#000',
856
1159
  shadowOffset: { width: 0, height: 2 },
857
1160
  shadowOpacity: 0.1,
858
1161
  shadowRadius: 4,
859
1162
  elevation: 3,
860
- paddingHorizontal: 12,
861
- paddingVertical: 8,
862
1163
  },
863
- receivedBubbleStyle: {
1164
+ received: {
864
1165
  borderRadius: 16,
865
- paddingHorizontal: 12,
866
- paddingVertical: 8,
867
1166
  },
868
1167
  },
869
- mediaGrid: {
870
- height: 320,
871
- borderRadius: 12,
872
- },
873
1168
  }}
874
1169
  />
875
1170
  ```
876
1171
 
877
1172
  ### Custom Components & Icons
878
1173
 
879
- Override built-in UI icons:
880
-
881
1174
  ```tsx
882
- import { CustomIcon } from './CustomIcon';
883
-
884
1175
  <ChatScreen
885
1176
  // Input bar icons
886
1177
  CustomEmojiIcon={EmojiIcon}
@@ -892,11 +1183,29 @@ import { CustomIcon } from './CustomIcon';
892
1183
  // Media previews
893
1184
  CustomImagePreview={CustomImagePreview}
894
1185
  CustomVideoPreview={CustomVideoPreview}
1186
+ // Audio playback
1187
+ CustomPlayIcon={PlayIcon}
1188
+ CustomPauseIcon={PauseIcon}
895
1189
  // Custom input
896
- renderCustomInput={(props) => <YourCustomInput {...props} />}
897
- />;
1190
+ renderCustomInput={() => <YourCustomInput />}
1191
+ />
898
1192
  ```
899
1193
 
1194
+ | Prop | Replaces |
1195
+ | ------------------------------------------- | ----------------------------------------------------- |
1196
+ | `renderCustomInput` | Entire input + recorder (you handle preview yourself) |
1197
+ | `renderCustomTyping` | "Typing…" content |
1198
+ | `renderCustomVideoBubbleError` | Inline video error in grid |
1199
+ | `CustomVoiceRecorder` | Built-in recorder bars |
1200
+ | `CustomEmojiIcon` | Emoji button |
1201
+ | `CustomAttachmentIcon` | Paperclip |
1202
+ | `CustomCameraIcon` | Camera |
1203
+ | `CustomSendIcon` | Send |
1204
+ | `CustomMicrophoneIcon` | Mic |
1205
+ | `CustomPlayIcon` / `CustomPauseIcon` | Audio (and related) playback |
1206
+ | `CustomFileIcon` | Document chip icon |
1207
+ | `CustomImagePreview` / `CustomVideoPreview` | Composer thumbnails |
1208
+
900
1209
  ### Component Overrides
901
1210
 
902
1211
  The library exposes individual components for advanced customization:
@@ -904,11 +1213,16 @@ The library exposes individual components for advanced customization:
904
1213
  ```tsx
905
1214
  import {
906
1215
  ChatBubble,
907
- ChatInput,
908
- MediaViewer,
909
1216
  AudioPlayer,
910
- TypingIndicator,
911
1217
  VoiceRecorder,
1218
+ VoiceRecordingGesture,
1219
+ VoiceRecorderFlow,
1220
+ LongPressOverlay,
1221
+ MessageActionsPopover,
1222
+ MessageActionsSheet,
1223
+ InlineReply,
1224
+ ReplyPreview,
1225
+ SwipeableMessage,
912
1226
  } from 'movius-chats';
913
1227
 
914
1228
  // Use individually or extend
@@ -919,95 +1233,71 @@ const CustomChatBubble = (props) => <ChatBubble {...props} staticMode={true} />;
919
1233
 
920
1234
  ## Project Structure
921
1235
 
922
- ```
923
1236
  movius-chats/
924
1237
  ├── src/
925
- ├── index.tsx # Main ChatScreen export
926
- ├── types/
927
- └── index.ts # TypeScript interfaces
928
- ├── context/
929
- ├── ChatContext.tsx # Main chat state & props
930
- └── AudioContext.tsx # Audio playback coordination
931
- ├── hooks/
932
- ├── useKeyboardInset.ts # iOS/Android keyboard height
933
- └── useVoiceRecorder.ts # Voice recording hook
934
- ├── components/
935
- ├── ChatBubble/
936
- ├── ChatBubble.tsx # Main bubble component
937
- ├── MessageContent.tsx # Message text/media content
938
- ├── MessageStatus.tsx # Status checkmarks
939
- ├── MediaGrid.tsx # 1/2/3/4+ grid layouts
940
- └── types.ts
941
- ├── ChatInput/
942
- ├── ChatInput.tsx # Input bar with buttons
943
- ├── FilePreview.tsx # Selected file preview
944
- └── TruncateFileName.ts
945
- ├── AudioPlayer/
946
- ├── AudioPlayer.tsx # Audio playback UI
947
- └── types.ts
948
- ├── MediaViewer/
949
- └── MediaViewer.tsx # Full-screen gallery
950
- ├── MessageActions/
951
- ├── index.ts
952
- ├── LongPressOverlay.tsx # Action menu overlay
953
- ├── MessageActionsPopover.tsx # Tablet UI
954
- └── MessageActionsSheet.tsx # Mobile bottom sheet
955
- ├── Reply/
956
- ├── index.ts
957
- ├── SwipeableMessage.tsx # Swipe gesture handler
958
- ├── ReplyPreview.tsx # Input bar reply preview
959
- └── InlineReply.tsx # Bubble reply display
960
- ├── TypingComponent/
961
- └── TypingIndicator.tsx # Animated typing dots
962
- └── VoiceRecorder/
963
- ├── VoiceRecorder.tsx # Base recorder UI
964
- ├── VoiceRecordingGesture.tsx # Gesture handlers
965
- └── VoiceRecorderFlow/ # Recording modes & UI
966
- ├── utils/
967
- ├── bubbleTheme.ts # Color helpers for sent/received
968
- ├── messageMedia.ts # Media collection utilities
969
- ├── messageActions.ts # Action merging & defaults
970
- ├── replyTheme.ts # Reply styling helpers
971
- ├── theme.ts # Font & icon size helpers
972
- └── datefunc.ts # Duration formatting
973
- └── assets/
974
- └── Icons/
975
- │ ├── ArrowBack2RoundedIcon.tsx
976
- │ ├── CameraIcon.tsx
977
- │ ├── CheckAllIcon.tsx
978
- │ ├── CheckIcon.tsx
979
- │ ├── ChevronUpIcon.tsx
980
- │ ├── ClosePreviewIcon.tsx
981
- │ ├── CopyIcon.tsx
982
- │ ├── EditIcon.tsx
983
- │ ├── EmojiFunnySquareIcon.tsx
984
- │ ├── FileIcon.tsx
985
- │ ├── ForwardIcon.tsx
986
- │ ├── LoadingIcon.tsx
987
- │ ├── LockIcon.tsx
988
- │ ├── MicrophoneIcon.tsx
989
- │ ├── PaperClipIcon.tsx
990
- │ ├── PaperPlaneIcon.tsx
991
- │ ├── PauseIcon.tsx
992
- │ ├── PlayIcon.tsx
993
- │ ├── ReplyIcon.tsx
994
- │ ├── SelectIcon.tsx
995
- │ ├── TrashIcon.tsx
996
- │ └── XIcon.tsx
1238
+ ├── index.tsx # Main ChatScreen export
1239
+ ├── types/
1240
+ └── index.ts # TypeScript interfaces
1241
+ ├── context/
1242
+ ├── ChatContext.tsx # Main chat state & props
1243
+ └── AudioContext.tsx # Audio playback coordination
1244
+ ├── hooks/
1245
+ ├── useKeyboardInset.ts # iOS/Android keyboard height
1246
+ └── useVoiceRecorder.ts # Voice recording hook
1247
+ ├── components/
1248
+ ├── ChatBubble/
1249
+ ├── ChatBubble.tsx # Main bubble + audio-split rendering
1250
+ ├── MessageContent.tsx # Text/grid/primary-audio content
1251
+ ├── MessageStatus.tsx # Status checkmarks
1252
+ ├── MediaGrid.tsx # 1/2/3/4+ grid layouts (image/video only)
1253
+ └── types.ts
1254
+ ├── ChatInput/
1255
+ ├── ChatInput.tsx # Input bar with buttons
1256
+ ├── FilePreview.tsx # Selected file preview
1257
+ └── TruncateFileName.ts
1258
+ ├── AudioPlayer/
1259
+ ├── AudioPlayer.tsx # Audio playback UI
1260
+ └── types.ts
1261
+ ├── MediaViewer/
1262
+ └── MediaViewer.tsx # Full-screen gallery (image/video only)
1263
+ ├── MessageActions/
1264
+ ├── index.ts
1265
+ ├── LongPressOverlay.tsx # Action menu overlay
1266
+ ├── MessageActionsPopover.tsx # Tablet UI
1267
+ └── MessageActionsSheet.tsx # Mobile bottom sheet
1268
+ ├── Reply/
1269
+ ├── index.ts
1270
+ ├── SwipeableMessage.tsx # Swipe gesture handler
1271
+ ├── ReplyPreview.tsx # Input bar reply preview
1272
+ └── InlineReply.tsx # Bubble reply display
1273
+ ├── TypingComponent/
1274
+ └── TypingIndicator.tsx # Animated typing dots
1275
+ └── VoiceRecorder/
1276
+ ├── VoiceRecorder.tsx # Base recorder UI
1277
+ ├── VoiceRecordingGesture.tsx # Gesture handlers
1278
+ └── VoiceRecorderFlow/ # Recording modes & UI
1279
+ ├── utils/
1280
+ ├── bubbleTheme.ts # Color helpers for sent/received
1281
+ ├── messageMedia.ts # Media collection + audio-split utilities
1282
+ ├── messageActions.ts # Action merging & defaults
1283
+ ├── replyTheme.ts # Reply styling helpers
1284
+ ├── theme.ts # Font & icon size helpers
1285
+ └── datefunc.ts # Duration formatting
1286
+ └── assets/
1287
+ └── Icons/ # Built-in SVG icons
997
1288
  ├── lib/
998
- ├── commonjs/ # CommonJS build output
999
- ├── module/ # ES Module build output
1000
- └── typescript/ # TypeScript declarations
1289
+ ├── commonjs/ # CommonJS build output
1290
+ ├── module/ # ES Module build output
1291
+ └── typescript/ # TypeScript declarations
1001
1292
  ├── scripts/
1002
- └── patchSound.js # Audio module patching
1003
- ├── babel.config.js # Babel configuration
1004
- ├── rollup.config.mjs # Rollup bundler config
1005
- ├── tsconfig.json # TypeScript config
1006
- ├── tsconfig.types.json # TypeScript types config
1007
- ├── tsconfig.build.json # TypeScript build config
1293
+ └── patchSound.js # Audio module patching
1294
+ ├── babel.config.js
1295
+ ├── rollup.config.mjs
1296
+ ├── tsconfig.json
1297
+ ├── tsconfig.types.json
1298
+ ├── tsconfig.build.json
1008
1299
  ├── package.json
1009
1300
  └── README.md
1010
- ```
1011
1301
 
1012
1302
  ---
1013
1303
 
@@ -1045,31 +1335,26 @@ movius-chats/
1045
1335
 
1046
1336
  ## Keyboard Behavior
1047
1337
 
1048
- The library handles platform-specific keyboard behavior automatically:
1049
-
1050
- ### iOS
1051
-
1052
- - Soft keyboard slides up from bottom
1053
- - Input bar adjusts position via `KeyboardAvoidingView`
1054
- - Default offset: based on notch/safe area
1338
+ | Platform | Behavior |
1339
+ | ----------- | ------------------------------------------------------------------------------------------------- |
1340
+ | **Android** | `useKeyboardInset` sets `marginBottom` on the input row (= keyboard height) |
1341
+ | **iOS** | Same inset **plus** `KeyboardAvoidingView` with `behavior="padding"` and `keyboardVerticalOffset` |
1055
1342
 
1056
- ### Android
1343
+ If your navigator already avoids the keyboard:
1057
1344
 
1058
- - Keyboard dismisses text field focus automatically
1059
- - Input bar uses `KeyboardAvoidingView`
1060
- - Respects `android:windowSoftInputMode` setting
1345
+ ```tsx
1346
+ <ChatScreen disableKeyboardAvoiding />
1347
+ ```
1061
1348
 
1062
1349
  ### Customization
1063
1350
 
1064
1351
  ```tsx
1065
- // useKeyboardInset hook provides current inset
1352
+ // useKeyboardInset hook provides current inset (internal — usually not needed directly)
1066
1353
  const { keyboardHeight } = useKeyboardInset();
1067
1354
 
1068
- // Manual control via ChatInput props
1355
+ // Manual control via renderCustomInput
1069
1356
  <ChatScreen
1070
- renderCustomInput={(props) => (
1071
- <YourInput keyboardOffset={customOffset} {...props} />
1072
- )}
1357
+ renderCustomInput={() => <YourInput keyboardOffset={customOffset} />}
1073
1358
  />;
1074
1359
  ```
1075
1360
 
@@ -1080,21 +1365,28 @@ const { keyboardHeight } = useKeyboardInset();
1080
1365
  Full TypeScript support with exported types:
1081
1366
 
1082
1367
  ```tsx
1083
- import {
1084
- ChatScreen,
1368
+ import ChatScreen from 'movius-chats';
1369
+
1370
+ import type {
1085
1371
  Message,
1086
1372
  MessageMediaItem,
1087
1373
  MessageFileAttachment,
1088
1374
  MessageReply,
1375
+ PreviewAttachment,
1089
1376
  RecordingResult,
1090
1377
  ChatScreenProps,
1091
- ChatScreenTheme,
1378
+ VoiceRecorderExposedState,
1092
1379
  VoiceRecorderConfig,
1093
- MessageActionsConfig,
1380
+ VoiceRecorderStyleOverrides,
1381
+ RecordingUIProps,
1382
+ MessageActionFlags,
1383
+ MessageActionUIProps,
1384
+ MessageActionLabels,
1094
1385
  ReplyConfig,
1095
- } from 'movius-chats';
1386
+ ReplyUIProps,
1387
+ SelectionUIProps,
1388
+ } from 'movius-chats/lib/typescript/types';
1096
1389
 
1097
- // Use types in your code
1098
1390
  const message: Message = {
1099
1391
  id: '1',
1100
1392
  text: 'Hello',
@@ -1106,12 +1398,7 @@ const message: Message = {
1106
1398
  type Props = ChatScreenProps;
1107
1399
  ```
1108
1400
 
1109
- ### Type Definitions
1110
-
1111
- All types are exported from `lib/typescript/index.d.ts` and available in:
1112
-
1113
- - `lib/commonjs/index.d.ts` (CommonJS version)
1114
- - `lib/module/index.d.ts` (ES Module version)
1401
+ Source types while developing against the repo: `movius-chats/src/types` (field `"react-native": "src"` in `package.json`).
1115
1402
 
1116
1403
  ---
1117
1404
 
@@ -1120,41 +1407,40 @@ All types are exported from `lib/typescript/index.d.ts` and available in:
1120
1407
  ### Custom Message Actions
1121
1408
 
1122
1409
  ```tsx
1123
- import {
1124
- mergeMessageActionLabels,
1125
- mergeMessageActionIcons,
1126
- } from 'movius-chats';
1127
-
1128
1410
  <ChatScreen
1129
- messageActionsConfig={{
1130
- actions: ['copy', 'reply', 'customAction', 'delete'],
1131
- labels: mergeMessageActionLabels({
1132
- customAction: 'Pin Message',
1133
- delete: 'Remove',
1134
- }),
1135
- icons: mergeMessageActionIcons({
1136
- customAction: <PinIcon />,
1137
- }),
1411
+ messageActionProps={{
1412
+ enableReply: true,
1413
+ enableCopy: true,
1414
+ enableEdit: true,
1415
+ enableDelete: true,
1416
+ enableForward: true,
1417
+ enableSelect: false,
1138
1418
  }}
1139
- onMessageActionTap={(action, message) => {
1140
- if (action === 'customAction') {
1141
- // Handle custom action
1142
- }
1419
+ messageActionLabels={{
1420
+ delete: 'Remove',
1143
1421
  }}
1144
- />;
1422
+ onCopyMessage={(message) => {
1423
+ /* ... */
1424
+ }}
1425
+ onDeleteMessage={(message) => {
1426
+ /* ... */
1427
+ }}
1428
+ onForwardMessage={(message) => {
1429
+ /* ... */
1430
+ }}
1431
+ />
1145
1432
  ```
1146
1433
 
1147
1434
  ### Static Message Display (Read-Only)
1148
1435
 
1149
1436
  ```tsx
1150
- // Disable all interactivity
1151
1437
  import { ChatBubble } from 'movius-chats';
1152
1438
 
1153
1439
  <ChatBubble
1154
1440
  message={message}
1155
1441
  isCurrentUser={false}
1442
+ isFirstInSequence
1156
1443
  staticMode={true}
1157
- onLongPress={() => {}}
1158
1444
  />;
1159
1445
  ```
1160
1446
 
@@ -1163,9 +1449,9 @@ import { ChatBubble } from 'movius-chats';
1163
1449
  ```tsx
1164
1450
  <ChatScreen
1165
1451
  typingUsers={[
1166
- { id: 'alice', name: 'Alice' },
1167
- { id: 'bob', name: 'Bob' },
1168
- { id: 'charlie', name: 'Charlie' },
1452
+ { id: 'alice', avatar: 'https://...', name: 'Alice' },
1453
+ { id: 'bob', avatar: 'https://...', name: 'Bob' },
1454
+ { id: 'charlie', avatar: 'https://...', name: 'Charlie' },
1169
1455
  ]}
1170
1456
  onTypingStart={() => console.log('Typing started')}
1171
1457
  onTypingEnd={() => console.log('Typing ended')}
@@ -1175,15 +1461,14 @@ import { ChatBubble } from 'movius-chats';
1175
1461
  ### Custom Theme Colors (Dark Mode)
1176
1462
 
1177
1463
  ```tsx
1178
- const darkTheme: ChatScreenTheme = {
1464
+ const darkTheme = {
1179
1465
  colors: {
1180
- sentBubbleBackground: '#128C7E', // WhatsApp green
1181
- sentTextColor: '#FFFFFF',
1182
- receivedBubbleBackground: '#1F2937', // Dark gray
1183
- receivedTextColor: '#F3F4F6',
1184
- inputBarBackground: '#0F172A',
1466
+ sentBubbleBackgroundColor: '#128C7E', // WhatsApp green
1467
+ sentMessageTextColor: '#FFFFFF',
1468
+ receivedBubbleBackgroundColor: '#1F2937', // Dark gray
1469
+ receivedMessageTextColor: '#F3F4F6',
1185
1470
  inputTextColor: '#F3F4F6',
1186
- inputPlaceholderTextColor: '#6B7280',
1471
+ placeholderTextColor: '#6B7280',
1187
1472
  },
1188
1473
  fontFamily: 'Roboto',
1189
1474
  };
@@ -1193,258 +1478,45 @@ const darkTheme: ChatScreenTheme = {
1193
1478
 
1194
1479
  ### Handling Large Message Lists
1195
1480
 
1196
- For performance with 1000+ messages:
1481
+ For performance with large lists:
1197
1482
 
1198
1483
  ```tsx
1199
- // 1. Use virtualization (FlatList is already virtualized)
1200
- // 2. Minimize re-renders with useMemo
1201
- // 3. Lazy-load older messages
1484
+ // 1. The list is already virtualized via FlatList
1485
+ // 2. Paginate older messages with onEndReached
1486
+ // 3. Cache the most recent page locally for instant reopen (e.g. MMKV)
1202
1487
 
1203
1488
  const [messages, setMessages] = useState<Message[]>([
1204
- // Latest 50 messages
1489
+ // Latest page of messages
1205
1490
  ]);
1206
1491
 
1207
- const handleScroll = (offset: number) => {
1208
- if (offset > 500) {
1209
- // Load older messages from backend
1210
- loadMoreMessages();
1211
- }
1212
- };
1492
+ <ChatScreen
1493
+ messages={messages}
1494
+ onEndReached={loadOlderMessages}
1495
+ isLoadingMoreMessages={isFetchingOlder}
1496
+ />;
1213
1497
  ```
1214
1498
 
1215
1499
  ---
1216
1500
 
1217
1501
  ## Troubleshooting
1218
1502
 
1219
- ### Issue: Voice recording not working
1220
-
1221
- **Symptoms:** Recording button doesn't respond or crashes
1222
-
1223
- **Solutions:**
1224
-
1225
- 1. **Check permissions:**
1226
-
1227
- ```tsx
1228
- import { PermissionsAndroid } from 'react-native';
1229
- const granted = await PermissionsAndroid.request(
1230
- PermissionsAndroid.PERMISSIONS.RECORD_AUDIO
1231
- );
1232
- ```
1233
-
1234
- 2. **Install audio dependencies:**
1235
-
1236
- ```bash
1237
- npm install react-native-audio-record react-native-fs
1238
- ```
1239
-
1240
- 3. **Verify native linking (Android):**
1241
- ```bash
1242
- cd android && ./gradlew clean && cd ..
1243
- npx react-native run-android
1244
- ```
1245
-
1246
- ### Issue: Media gallery doesn't display
1247
-
1248
- **Symptoms:** Images/videos not showing in gallery
1249
-
1250
- **Solutions:**
1251
-
1252
- 1. **Verify URIs are valid:**
1253
-
1254
- ```tsx
1255
- const mediaItems = [
1256
- { uri: 'file:///path/to/image.jpg', kind: 'image' },
1257
- // Not 'file:///path/to/image.jpg/' (no trailing slash)
1258
- ];
1259
- ```
1260
-
1261
- 2. **Check MediaGrid height:**
1262
-
1263
- ```tsx
1264
- theme={{
1265
- mediaGrid: { height: 320 }
1266
- }}
1267
- ```
1268
-
1269
- 3. **Grant file permissions:**
1270
- - **Android:** Add `READ_EXTERNAL_STORAGE` permission
1271
- - **iOS:** Add `NSPhotoLibraryUsageDescription` to Info.plist
1272
-
1273
- ### Issue: Audio playback cutting off
1274
-
1275
- **Symptoms:** Audio stops playing prematurely or doesn't play at all
1276
-
1277
- **Solutions:**
1278
-
1279
- 1. **Check audio format:** Use supported formats (MP3, M4A, AAC)
1280
- 2. **Verify URI is accessible:**
1281
-
1282
- ```tsx
1283
- // Correct
1284
- {
1285
- mediaItems: [{ uri: 'file:///documents/voice.m4a', kind: 'audio' }],
1286
- }
1287
-
1288
- // Wrong
1289
- {
1290
- mediaItems: [{ uri: 'file://documents/voice.m4a', kind: 'audio' }],
1291
- } // Missing /
1292
- ```
1293
-
1294
- 3. **Clear audio cache:**
1295
- ```tsx
1296
- const { clearAudioCache } = useAudio();
1297
- clearAudioCache();
1298
- ```
1299
-
1300
- ### Issue: Keyboard overlaps input
1301
-
1302
- **Symptoms:** Chat input hidden when keyboard appears
1303
-
1304
- **Solutions:**
1305
-
1306
- 1. **Verify KeyboardAvoidingView:**
1307
-
1308
- ```tsx
1309
- // Already built-in, check if overridden
1310
- <KeyboardAvoidingView
1311
- behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
1312
- >
1313
- <ChatScreen ... />
1314
- </KeyboardAvoidingView>
1315
- ```
1316
-
1317
- 2. **Adjust offset:**
1318
-
1319
- ```tsx
1320
- <KeyboardAvoidingView keyboardVerticalOffset={100}>...</KeyboardAvoidingView>
1321
- ```
1322
-
1323
- 3. **Check AndroidManifest.xml:**
1324
- ```xml
1325
- <activity
1326
- android:windowSoftInputMode="adjustResize"
1327
- />
1328
- ```
1329
-
1330
- ### Issue: Memory leaks
1331
-
1332
- **Symptoms:** App crashes with large message lists, performance degrades
1333
-
1334
- **Solutions:**
1335
-
1336
- 1. **Cleanup contexts:**
1337
-
1338
- ```tsx
1339
- useEffect(() => {
1340
- return () => {
1341
- clearMediaViewerGallery?.();
1342
- // Cleanup
1343
- };
1344
- }, []);
1345
- ```
1346
-
1347
- 2. **Use FlatList optimizations:**
1348
-
1349
- ```tsx
1350
- <FlatList
1351
- removeClippedSubviews={true}
1352
- maxToRenderPerBatch={10}
1353
- windowSize={21}
1354
- updateCellsBatchingPeriod={50}
1355
- />
1356
- ```
1357
-
1358
- 3. **Avoid inline function callbacks:**
1359
-
1360
- ```tsx
1361
- // Bad
1362
- onSendMessage={(text) => { ... }}
1363
-
1364
- // Good
1365
- const handleSend = useCallback((text) => { ... }, []);
1366
- <ChatScreen onSendMessage={handleSend} />
1367
- ```
1368
-
1369
- ### Issue: TypeScript errors
1370
-
1371
- **Symptoms:** Type checking fails, missing types
1372
-
1373
- **Solutions:**
1374
-
1375
- 1. **Regenerate types:**
1376
-
1377
- ```bash
1378
- npm run build:types
1379
- ```
1380
-
1381
- 2. **Check tsconfig.json:**
1382
-
1383
- ```json
1384
- {
1385
- "compilerOptions": {
1386
- "strict": true,
1387
- "skipLibCheck": true
1388
- }
1389
- }
1390
- ```
1391
-
1392
- 3. **Update type definitions:**
1393
- ```bash
1394
- npm install --save-dev @types/react-native@latest
1395
- ```
1396
-
1397
- ### Issue: Android build fails
1398
-
1399
- **Symptoms:** Gradle/NDK errors during build
1400
-
1401
- **Solutions:**
1402
-
1403
- 1. **Enable AndroidX:**
1404
-
1405
- ```properties
1406
- # android/gradle.properties
1407
- android.useAndroidX=true
1408
- android.enableJetifier=true
1409
- ```
1410
-
1411
- 2. **Update Gradle:**
1412
-
1413
- ```properties
1414
- # android/gradle/wrapper/gradle-wrapper.properties
1415
- distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
1416
- ```
1417
-
1418
- 3. **Clean build:**
1419
- ```bash
1420
- cd android
1421
- ./gradlew clean
1422
- cd ..
1423
- npx react-native run-android
1424
- ```
1425
-
1426
- ### Issue: Reply gestures not working
1427
-
1428
- **Symptoms:** Swipe-to-reply doesn't trigger
1429
-
1430
- **Solutions:**
1431
-
1432
- 1. **Enable reply config:**
1433
-
1434
- ```tsx
1435
- replyProps={{
1436
- enableReply: true,
1437
- swipeThreshold: 60, // Pixels to swipe
1438
- }}
1439
- ```
1440
-
1441
- 2. **Check gesture handler:**
1442
-
1443
- ```bash
1444
- npm install react-native-gesture-handler@latest
1445
- ```
1446
-
1447
- 3. **Verify RectButton nesting** (GestureHandler requirement)
1503
+ | Problem | What to do |
1504
+ | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1505
+ | `Cannot read property 'init' of null` | Install `react-native-audio-record`, run `pod install`, **rebuild** the app |
1506
+ | Recording never starts | Mic permission; iOS `NSMicrophoneUsageDescription`; Android `RECORD_AUDIO` |
1507
+ | No audio playback | Ensure `react-native-video` is linked; URI must be `file://` or `http(s)://` |
1508
+ | Audio message shows a large empty box above the waveform | Audio items no longer render inside the image/video grid box — always uses a compact standalone UI. If seeing this, confirm you're not passing audio through the legacy `image`/`video` fields. |
1509
+ | Multiple audio clips squished into one grid cell | Fixed — any audio beyond the first in `mediaItems` splits into its own standalone bubble below the main one automatically. |
1510
+ | Standalone split audio bubble missing timestamp/checkmark | Split audio bubbles inherit `time` and `status` from the parent message automatically — no action needed. |
1511
+ | `NoSuchMethodError` `DefaultLoadControl` (Android) | Force `androidx.media3` to a consistent version (matching what `react-native-video` expects) in the app `android/build.gradle` `resolutionStrategy`, then clean-rebuild. |
1512
+ | Reanimated error | `react-native-reanimated/plugin` must be **last** in Babel plugins |
1513
+ | Font not applied | Register font in the **host** app; pass exact `fontFamily` string |
1514
+ | `inputIconSize` ignored on send/mic | By design |
1515
+ | Keyboard covers input (Android) | `android:windowSoftInputMode="adjustResize"`; parent `flex: 1` |
1516
+ | × clears all previews | Implement `onRemovePreviewItem` |
1517
+ | Wrong audio avatar | Set `senderAvatar` and `senderName` on the `Message` |
1518
+ | Messages upside down | Newest message must be at `messages[0]` |
1519
+ | Reply gestures not working | Ensure `replyProps.enableReply` is `true`, `react-native-gesture-handler` is up to date, and `GestureHandlerRootView` wraps your app |
1448
1520
 
1449
1521
  ---
1450
1522
 
@@ -1461,26 +1533,14 @@ Contributions welcome! Please:
1461
1533
  ### Development Setup
1462
1534
 
1463
1535
  ```bash
1464
- # Clone repo
1465
1536
  git clone https://github.com/David-Atueyi/Movius-Chats.git
1466
1537
  cd Movius-Chats
1467
1538
 
1468
- # Install dependencies
1469
1539
  npm install
1470
-
1471
- # Run tests
1472
1540
  npm run test
1473
-
1474
- # Type check
1475
1541
  npm run typescript
1476
-
1477
- # Lint
1478
1542
  npm run lint
1479
-
1480
- # Build
1481
1543
  npm run build
1482
-
1483
- # View types
1484
1544
  npm run build:types
1485
1545
  ```
1486
1546
 
@@ -1513,559 +1573,3 @@ ISC License — see [LICENSE](LICENSE) file for details.
1513
1573
  ---
1514
1574
 
1515
1575
  **Built with ❤️ for React Native developers.**
1516
- |---------|----------|
1517
- | `react` ≥ 16.8 | Yes |
1518
- | `react-native` | Yes |
1519
- | `react-native-reanimated` | Yes (voice recorder animations) |
1520
-
1521
- ### Optional peers (voice recording only)
1522
-
1523
- | Package | Use |
1524
- | --------------------------- | -------------------------------- |
1525
- | `react-native-audio-record` | Record microphone |
1526
- | `react-native-fs` | Delete cancelled recording files |
1527
-
1528
- If these are missing, the UI still renders; starting a recording logs an install hint.
1529
-
1530
- **There is no `react-native-sound`, `expo-av`, `expo-file-system`, or other Expo package in this library.**
1531
-
1532
- ---
1533
-
1534
- ## Installation
1535
-
1536
- ### 1. Install movius-chats
1537
-
1538
- ```bash
1539
- yarn add movius-chats
1540
- # or: npm install movius-chats
1541
- # or: bun add movius-chats
1542
- ```
1543
-
1544
- ### 2. Install peers
1545
-
1546
- ```bash
1547
- yarn add react-native-reanimated react-native-video react-native-svg
1548
- ```
1549
-
1550
- `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.
1551
-
1552
- ### 3. Reanimated (Babel)
1553
-
1554
- Put this plugin **last** in `babel.config.js`:
1555
-
1556
- ```js
1557
- module.exports = {
1558
- presets: ['module:metro-react-native-babel-preset'],
1559
- plugins: [
1560
- // ...other plugins
1561
- 'react-native-reanimated/plugin',
1562
- ],
1563
- };
1564
- ```
1565
-
1566
- ### 4. Voice recording (optional)
1567
-
1568
- ```bash
1569
- yarn add react-native-audio-record react-native-fs
1570
- ```
1571
-
1572
- **iOS** — add to `Info.plist`:
1573
-
1574
- ```xml
1575
- <key>NSMicrophoneUsageDescription</key>
1576
- <string>This app needs the microphone to record voice messages.</string>
1577
- ```
1578
-
1579
- **Android** — ensure `RECORD_AUDIO` is in `AndroidManifest.xml` (often added by the audio-record library).
1580
-
1581
- Then rebuild native apps:
1582
-
1583
- ```bash
1584
- cd ios && pod install && cd ..
1585
- npx react-native run-ios
1586
- npx react-native run-android
1587
- ```
1588
-
1589
- ### 5. Android keyboard
1590
-
1591
- In `android/app/src/main/AndroidManifest.xml` on your main activity:
1592
-
1593
- ```xml
1594
- android:windowSoftInputMode="adjustResize"
1595
- ```
1596
-
1597
- ---
1598
-
1599
- ## Quick start
1600
-
1601
- ```tsx
1602
- import React, { useState } from 'react';
1603
- import { Platform, SafeAreaView, View } from 'react-native';
1604
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
1605
- import ChatScreen from 'movius-chats';
1606
- import type { Message } from 'movius-chats/lib/typescript/types';
1607
-
1608
- export default function ChatDetailScreen() {
1609
- const insets = useSafeAreaInsets();
1610
- const currentUserId = '1';
1611
- const [messages, setMessages] = useState<Message[]>([]);
1612
-
1613
- return (
1614
- <SafeAreaView style={{ flex: 1 }}>
1615
- <View style={{ flex: 1 }}>
1616
- <ChatScreen
1617
- messages={messages}
1618
- currentUserId={currentUserId}
1619
- onSendMessage={({ text, senderId }) => {
1620
- setMessages((prev) => [
1621
- {
1622
- id: String(Date.now()),
1623
- text,
1624
- senderId,
1625
- time: new Date().toLocaleTimeString([], {
1626
- hour: '2-digit',
1627
- minute: '2-digit',
1628
- }),
1629
- status: 'sent',
1630
- },
1631
- ...prev,
1632
- ]);
1633
- }}
1634
- keyboardVerticalOffset={Platform.OS === 'ios' ? insets.top : 0}
1635
- showBubbleTail
1636
- showMessageStatus
1637
- showVoiceRecordButton
1638
- />
1639
- </View>
1640
- </SafeAreaView>
1641
- );
1642
- }
1643
- ```
1644
-
1645
- Wrap the screen in `flex: 1`. Load custom fonts in **your** app before passing `theme.fontFamily`.
1646
-
1647
- ---
1648
-
1649
- ## Message data model
1650
-
1651
- ### `Message`
1652
-
1653
- | Field | Type | Description |
1654
- | ----------------- | --------------------------------- | ------------------------------------------ |
1655
- | `id` | `string` | Unique id |
1656
- | `senderId` | `string` | Who sent it |
1657
- | `time` | `string` | Display time (you format it) |
1658
- | `status` | `'sent' \| 'delivered' \| 'read'` | Checkmarks on **your** messages only |
1659
- | `text` | `string` | Body text |
1660
- | `audio` | `string` | Audio file URI |
1661
- | `image` | `string` | Single image (legacy; prefer `mediaItems`) |
1662
- | `video` | `string` | Single video (legacy) |
1663
- | `mediaItems` | `MessageMediaItem[]` | Album in one bubble |
1664
- | `fileAttachments` | `MessageFileAttachment[]` | PDF, doc, etc. |
1665
- | `senderName` | `string` | Group name + audio avatar initial |
1666
- | `senderAvatar` | `string` | Image URI for audio bubble avatar |
1667
-
1668
- ### `MessageMediaItem`
1669
-
1670
- ```ts
1671
- {
1672
- uri: string;
1673
- kind: 'image' | 'video' | 'audio';
1674
- }
1675
- ```
1676
-
1677
- ### `MessageFileAttachment`
1678
-
1679
- ```ts
1680
- {
1681
- uri: string;
1682
- type: string;
1683
- name: string;
1684
- }
1685
- ```
1686
-
1687
- ### `PreviewAttachment` (composer)
1688
-
1689
- ```ts
1690
- { uri: string; type: string; name?: string }
1691
- ```
1692
-
1693
- ### `RecordingResult` (`onAudioRecordEnd`)
1694
-
1695
- ```ts
1696
- { uri: string; duration: number; mimeType?: string; size?: number }
1697
- ```
1698
-
1699
- ---
1700
-
1701
- ## Message list order
1702
-
1703
- The list is **inverted**. Newest message must be **index 0**:
1704
-
1705
- ```ts
1706
- setMessages((prev) => [newMessage, ...prev]);
1707
- ```
1708
-
1709
- Avatars and bubble tails show only on the **first** message in a consecutive run from the same `senderId`.
1710
-
1711
- ---
1712
-
1713
- ## ChatScreen API
1714
-
1715
- Default export: `ChatScreen`. All props are optional except `messages`, `currentUserId`, and `onSendMessage`.
1716
-
1717
- ### Core
1718
-
1719
- | Prop | Type | Description |
1720
- | ------------------------- | ----------------------------------------------------- | ----------------------------------------------- |
1721
- | `messages` | `Message[]` | Newest first |
1722
- | `currentUserId` | `string` | Sent vs received layout |
1723
- | `onSendMessage` | `(Omit<Message, 'id' \| 'time' \| 'status'>) => void` | Send button |
1724
- | `onMessageLongPress` | `(message: Message) => void` | Long-press bubble |
1725
- | `placeholder` | `string` | Input placeholder (default `"Message"`) |
1726
- | `keyboardVerticalOffset` | `number` | **iOS only** — passed to `KeyboardAvoidingView` |
1727
- | `disableKeyboardAvoiding` | `boolean` | Turn off built-in keyboard lift |
1728
-
1729
- ### Feature flags (default `false`)
1730
-
1731
- `showAvatars`, `showUserNames`, `showBubbleTail`, `showMessageStatus`, `showEmojiButton`, `showAttachmentsButton`, `showCameraButton`, `showVoiceRecordButton`
1732
-
1733
- ### Callbacks
1734
-
1735
- | Prop | Description |
1736
- | ------------------------------- | --------------------------------------------------- |
1737
- | `onTypingStart` / `onTypingEnd` | Input text empty ↔ non-empty |
1738
- | `onAttachmentPress` | Paperclip — open your picker |
1739
- | `onCameraPress` | Camera icon |
1740
- | `onAudioRecordStart` | Recording began |
1741
- | `onAudioRecordEnd` | `(RecordingResult?) => void` when done or cancelled |
1742
- | `onFileAttachmentPress` | File chip in bubble (default: `Linking.openURL`) |
1743
-
1744
- ### Composer preview
1745
-
1746
- | Prop | Description |
1747
- | --------------------- | -------------------------------------------- |
1748
- | `previewItems` | Multiple attachments before send |
1749
- | `previewData` | Single attachment (legacy) |
1750
- | `onRemovePreviewItem` | `(uri) => void` — remove **one** card by URI |
1751
- | `closePreview` | Clears all if `onRemovePreviewItem` not set |
1752
-
1753
- When preview or text exists, the **send** icon shows instead of the mic.
1754
-
1755
- ### Voice recorder customization
1756
-
1757
- | Prop | Type |
1758
- | ---------------------------- | --------------------------------------------------------------------------- |
1759
- | `CustomVoiceRecorder` | `(VoiceRecorderExposedState) => ReactNode` — replace entire recorder UI |
1760
- | `theme.voiceRecorder.config` | `VoiceRecorderConfig` — `maxDuration`, lock, slide-to-cancel, etc. |
1761
- | `theme.voiceRecorder.ui` | `RecordingUIProps` — colors/sizes for timer, lock pill, recorder play/pause |
1762
- | `theme.voiceRecorder.styles` | `VoiceRecorderStyleOverrides` |
1763
-
1764
- ### Typing
1765
-
1766
- | Prop | Type |
1767
- | ------------- | ------------------------ |
1768
- | `typingUsers` | `{ id, avatar, name }[]` |
1769
-
1770
- ---
1771
-
1772
- ## Voice recording
1773
-
1774
- Requires **`react-native-audio-record`** and **`react-native-fs`** in the host app, plus a **native rebuild**.
1775
-
1776
- ### Gestures
1777
-
1778
- | Action | Result |
1779
- | ------------------------- | ---------------------------------------------------------------------------- |
1780
- | **Tap** mic | Normal bar: trash, timer, waveform, play/pause preview, send |
1781
- | **Long-press** mic | Hold mode: “slide to cancel”, lock column above send |
1782
- | Slide **left** | Cancel (file deleted via `react-native-fs`) |
1783
- | Slide **up** to lock | Switches to normal bar (`lockSlideDistance` in `theme.voiceRecorder.config`) |
1784
- | **Release** without slide | Auto-send (`onAudioRecordEnd`) |
1785
-
1786
- ### Wiring
1787
-
1788
- ```tsx
1789
- <ChatScreen
1790
- showVoiceRecordButton
1791
- onAudioRecordEnd={(result) => {
1792
- if (!result) return;
1793
- setMessages((prev) => [
1794
- {
1795
- id: String(Date.now()),
1796
- senderId: currentUserId,
1797
- mediaItems: [{ uri: result.uri, kind: 'audio' }],
1798
- time: '10:56 PM',
1799
- status: 'sent',
1800
- senderAvatar: myAvatarUri,
1801
- senderName: myDisplayName,
1802
- },
1803
- ...prev,
1804
- ]);
1805
- }}
1806
- />
1807
- ```
1808
-
1809
- ### Custom recorder UI
1810
-
1811
- ```tsx
1812
- CustomVoiceRecorder={(state) => (
1813
- <MyRecorder
1814
- duration={state.duration}
1815
- onStop={state.stopRecording}
1816
- onCancel={state.cancelRecording}
1817
- />
1818
- )}
1819
- ```
1820
-
1821
- ---
1822
-
1823
- ## Audio message bubbles
1824
-
1825
- WhatsApp-style row inside the bubble:
1826
-
1827
- | Side | Layout (left → right) |
1828
- | ------------ | -------------------------------------------- |
1829
- | **Sent** | Avatar or speed pill → play/pause → waveform |
1830
- | **Received** | play/pause → waveform → avatar or speed pill |
1831
-
1832
- | State | Avatar slot |
1833
- | --------------- | ------------------------------------------------------- |
1834
- | Idle / finished | `senderAvatar` or first letter of `senderName` |
1835
- | Playing | Pill showing **1x**, **1.5x**, or **2x** (tap to cycle) |
1836
- | Ended | Avatar again |
1837
-
1838
- - Waveform bars with scrubber dot; tap or drag to seek
1839
- - Duration under the waveform
1840
- - Play/pause is icon-only (no filled circle)
1841
- - Only one audio plays at a time (`AudioContext`)
1842
- - Video in the gallery pauses other audio
1843
-
1844
- ```tsx
1845
- {
1846
- id: 'a1',
1847
- senderId: '2',
1848
- mediaItems: [{ uri: 'file:///data/user/0/.../voice.wav', kind: 'audio' }],
1849
- senderAvatar: 'https://cdn.example.com/u2.jpg',
1850
- senderName: 'Alex',
1851
- time: '10:23 pm',
1852
- status: 'read',
1853
- }
1854
- ```
1855
-
1856
- ---
1857
-
1858
- ## Media grids & gallery
1859
-
1860
- ### Grid (`mediaItems`)
1861
-
1862
- | Count | Layout | Height |
1863
- | ----- | ---------------------- | ------ |
1864
- | 1 | Full width, cover | 320px |
1865
- | 2 | Two columns | 320px |
1866
- | 3 | One top, two bottom | 320px |
1867
- | 4+ | 2×2, `+N` on last cell | 320px |
1868
-
1869
- Tap opens `MediaViewer`. Thumbnail `Video` uses `pointerEvents="none"` so presses reach the parent.
1870
-
1871
- ### Gallery behavior
1872
-
1873
- - Horizontal `FlatList`, `n / total` header
1874
- - **Videos** play only if that video was the tapped item and the page is active
1875
- - Tapping an **image** in a mixed album does not start other videos
1876
- - Composer video previews **do** autoplay in the small preview card
1877
-
1878
- ### Legacy single fields
1879
-
1880
- Legacy `image`, `video`, and `audio` fields on `Message` are normalized into `mediaItems` internally via `collectMediaItems()`.
1881
-
1882
- ---
1883
-
1884
- ## Composer attachment preview
1885
-
1886
- Controlled from your app state:
1887
-
1888
- ```tsx
1889
- const [previews, setPreviews] = useState<PreviewAttachment[]>([]);
1890
-
1891
- <ChatScreen
1892
- previewItems={previews}
1893
- onRemovePreviewItem={(uri) =>
1894
- setPreviews((p) => p.filter((x) => x.uri !== uri))
1895
- }
1896
- onAttachmentPress={openYourDocumentPicker}
1897
- onSendMessage={handleSend}
1898
- />;
1899
- ```
1900
-
1901
- | Preview type | UI |
1902
- | ------------- | ----------------------------------- |
1903
- | 1 image/video | Single thumb + × |
1904
- | 2–3 media | Fanned stack, × on each |
1905
- | 4+ media | Fan of 3 + `+N`, × per visible card |
1906
- | Documents | Chips; scrollable after 3 |
1907
-
1908
- Use any picker you want (`react-native-document-picker`, `react-native-image-picker`, etc.) — movius-chats only displays `previewItems`.
1909
-
1910
- ---
1911
-
1912
- ## Theme & styling
1913
-
1914
- Pass `theme` to `ChatScreen`. `theme.fontFamily` applies to **all** `Text` in the package (load the font in your app first).
1915
-
1916
- ### `theme.colors` — per side (`sent*` / `received*`)
1917
-
1918
- | Keys | Used for |
1919
- | ------------------------------------------------------------------------- | ------------------------------------- |
1920
- | `sentTimestampColor` / `receivedTimestampColor` | Message & file timestamps |
1921
- | `sentMessageTextColor` / `receivedMessageTextColor` | Bubble text |
1922
- | `sentBubbleBackgroundColor` / `receivedBubbleBackgroundColor` | Bubble background |
1923
- | `sentMessageTailColor` / `receivedMessageTailColor` | Corner tail (`ArrowBack2RoundedIcon`) |
1924
- | `sentFileAttachmentBackground` / `receivedFileAttachmentBackground` | File chip |
1925
- | `sentFileAttachmentTextColor` / `receivedFileAttachmentTextColor` | File name |
1926
- | `sentFileAttachmentSubtitleColor` / `receivedFileAttachmentSubtitleColor` | MIME line |
1927
- | `sentAudioWaveformColor` / `receivedAudioWaveformColor` | Inactive waveform bars |
1928
- | `sentAudioWaveformActiveColor` / `receivedAudioWaveformActiveColor` | Active bars + scrubber |
1929
- | `sentAudioTimestampColor` / `receivedAudioTimestampColor` | Duration under waveform |
1930
- | `sentAudioPlayIconColor` / `receivedAudioPlayIconColor` | Play icon |
1931
- | `sentAudioPauseIconColor` / `receivedAudioPauseIconColor` | Pause icon |
1932
- | `sentAudioSpeedTextColor` / `receivedAudioSpeedTextColor` | **1x / 1.5x / 2x** pill text |
1933
- | `sentMediaTimestampBackground` / `receivedMediaTimestampBackground` | Timestamp pill on file-only bubbles |
1934
-
1935
- **Not themeable:** image/video-only messages (no text, no audio) always use **white** (`#ffffff`) for the timestamp text.
1936
-
1937
- ### Shared colors
1938
-
1939
- `inputsIconsColor`, `sendIconsColor`, `placeholderTextColor`, `inputTextColor`, `sentIconColor`, `deliveredIconColor`, `readIconColor`, `videoPlayIconColor`
1940
-
1941
- ### `theme.sizes`
1942
-
1943
- `inputIconSize` — number (px) or twrnc class string; affects **emoji, paperclip, camera only** (not send/mic).
1944
-
1945
- ### `theme.bubbleStyle`
1946
-
1947
- `sent`, `received`, `avatarTextStyle`, `userNameStyle`, `avatarImageStyle`, typing styles.
1948
-
1949
- ### `theme.messageStyle`
1950
-
1951
- Text styles, file attachment styles, `progressBarStyle`, `activeProgressBarStyle`, `audioDurationStyle`, `audioSpeedButtonStyle`, `audioSpeedTextStyle`, media timestamp container styles.
1952
-
1953
- ### `theme.inputStyles` / `theme.filePreviewStyle`
1954
-
1955
- Input row, send button, preview strip.
1956
-
1957
- ### Example
1958
-
1959
- ```tsx
1960
- <ChatScreen
1961
- theme={{
1962
- fontFamily: 'Inter-Regular',
1963
- colors: {
1964
- sentBubbleBackgroundColor: '#005C4B',
1965
- receivedBubbleBackgroundColor: '#1F2C34',
1966
- sentAudioSpeedTextColor: '#FFFFFF',
1967
- receivedAudioSpeedTextColor: '#E5E7EB',
1968
- sentAudioWaveformActiveColor: '#53BDEB',
1969
- receivedAudioWaveformActiveColor: '#53BDEB',
1970
- },
1971
- sizes: { inputIconSize: 22 },
1972
- }}
1973
- />
1974
- ```
1975
-
1976
- ---
1977
-
1978
- ## Keyboard behavior
1979
-
1980
- Built into `ChatScreen`:
1981
-
1982
- | Platform | Behavior |
1983
- | ----------- | ------------------------------------------------------------------------------------------------- |
1984
- | **Android** | `useKeyboardInset` sets `marginBottom` on the input row (= keyboard height) |
1985
- | **iOS** | Same inset **plus** `KeyboardAvoidingView` with `behavior="padding"` and `keyboardVerticalOffset` |
1986
-
1987
- If your navigator already avoids the keyboard:
1988
-
1989
- ```tsx
1990
- <ChatScreen disableKeyboardAvoiding />
1991
- ```
1992
-
1993
- ---
1994
-
1995
- ## Custom components & icons
1996
-
1997
- | Prop | Replaces |
1998
- | ------------------------------------------- | ----------------------------------------------------- |
1999
- | `renderCustomInput` | Entire input + recorder (you handle preview yourself) |
2000
- | `renderCustomTyping` | “Typing…” content |
2001
- | `renderCustomVideoBubbleError` | Inline video error in grid |
2002
- | `CustomVoiceRecorder` | Built-in recorder bars |
2003
- | `CustomEmojiIcon` | Emoji button |
2004
- | `CustomAttachmentIcon` | Paperclip |
2005
- | `CustomCameraIcon` | Camera |
2006
- | `CustomSendIcon` | Send |
2007
- | `CustomMicrophoneIcon` | Mic |
2008
- | `CustomPlayIcon` / `CustomPauseIcon` | Audio (and related) playback |
2009
- | `CustomFileIcon` | Document chip icon |
2010
- | `CustomImagePreview` / `CustomVideoPreview` | Composer thumbnails |
2011
-
2012
- ### File attachments - Custom handlers
2013
-
2014
- Default tap uses React Native `Linking.openURL`. For local files or share sheets, you can implement custom handlers in your app:
2015
-
2016
- ```tsx
2017
- import { Linking } from 'react-native';
2018
- // or: react-native-share, react-native-blob-util, etc.
2019
-
2020
- onFileAttachmentPress={async (file) => {
2021
- const uri = file.uri.startsWith('file://') ? file.uri : `file://${file.uri}`;
2022
- await Linking.openURL(uri);
2023
- }}
2024
- ```
2025
-
2026
- ---
2027
-
2028
- ## TypeScript
2029
-
2030
- ```ts
2031
- import ChatScreen from 'movius-chats';
2032
-
2033
- import type {
2034
- Message,
2035
- MessageMediaItem,
2036
- MessageFileAttachment,
2037
- PreviewAttachment,
2038
- RecordingResult,
2039
- ChatScreenProps,
2040
- VoiceRecorderExposedState,
2041
- VoiceRecorderConfig,
2042
- VoiceRecorderStyleOverrides,
2043
- RecordingUIProps,
2044
- } from 'movius-chats/lib/typescript/types';
2045
- ```
2046
-
2047
- Source types while developing against the repo: `movius-chats/src/types` (field `"react-native": "src"` in package.json).
2048
-
2049
- ---
2050
-
2051
- ## Troubleshooting
2052
-
2053
- | Problem | What to do |
2054
- | -------------------------------------------------- | ------------------------------------------------------------------------------------------- |
2055
- | `Cannot read property 'init' of null` | Install `react-native-audio-record`, run `pod install`, **rebuild** the app |
2056
- | Recording never starts | Mic permission; iOS `NSMicrophoneUsageDescription`; Android `RECORD_AUDIO` |
2057
- | No audio playback | Ensure `react-native-video` is linked; URI must be `file://` or `http(s)://` |
2058
- | `NoSuchMethodError` `DefaultLoadControl` (Android) | Force `androidx.media3` to **1.3.1** in the app `android/build.gradle` `resolutionStrategy` |
2059
- | Reanimated error | `react-native-reanimated/plugin` must be **last** in Babel plugins |
2060
- | Font not applied | Register font in the **host** app; pass exact `fontFamily` string |
2061
- | `inputIconSize` ignored on send/mic | By design |
2062
- | Keyboard covers input (Android) | `android:windowSoftInputMode="adjustResize"`; parent `flex: 1` |
2063
- | × clears all previews | Implement `onRemovePreviewItem` |
2064
- | Wrong audio avatar | Set `senderAvatar` and `senderName` on the `Message` |
2065
- | Messages upside down | Newest at `messages[0]` |
2066
-
2067
- ---
2068
-
2069
- ## License
2070
-
2071
- ISC — see [package.json](./package.json).