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.
- package/README.md +710 -1206
- 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.
|
|
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
|
|
143
|
-
|
|
|
144
|
-
| **Text rendering**
|
|
145
|
-
| **Image / video albums**
|
|
146
|
-
| **Full-screen media viewer**
|
|
147
|
-
| **Audio playback**
|
|
148
|
-
| **
|
|
149
|
-
| **
|
|
150
|
-
| **
|
|
151
|
-
| **
|
|
152
|
-
| **
|
|
153
|
-
| **
|
|
154
|
-
| **
|
|
155
|
-
| **Message
|
|
156
|
-
| **
|
|
157
|
-
| **
|
|
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
|
-
###
|
|
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
|
-
###
|
|
181
|
+
### 2. Install peers
|
|
177
182
|
|
|
178
183
|
```bash
|
|
179
|
-
|
|
184
|
+
yarn add react-native-reanimated react-native-video react-native-svg react-native-gesture-handler
|
|
180
185
|
```
|
|
181
186
|
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
217
|
+
### 4. Voice recording (optional)
|
|
191
218
|
|
|
192
219
|
```bash
|
|
193
|
-
|
|
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
|
-
|
|
243
|
+
(`RECORD_AUDIO` is often added automatically by `react-native-audio-record`.)
|
|
244
|
+
|
|
245
|
+
Then rebuild native apps:
|
|
197
246
|
|
|
198
247
|
```bash
|
|
199
|
-
|
|
248
|
+
cd ios && pod install && cd ..
|
|
249
|
+
npx react-native run-ios
|
|
250
|
+
npx react-native run-android
|
|
200
251
|
```
|
|
201
252
|
|
|
202
|
-
###
|
|
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
|
-
###
|
|
221
|
-
|
|
222
|
-
**Android** (`AndroidManifest.xml`):
|
|
271
|
+
### 6. Android keyboard
|
|
223
272
|
|
|
224
|
-
|
|
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
|
-
|
|
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 {
|
|
251
|
-
import
|
|
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
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
<
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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[]; //
|
|
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
|
|
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: (
|
|
476
|
+
onSendMessage: (message: Omit<Message, 'id' | 'time' | 'status'>) => void;
|
|
395
477
|
onAttachmentPress?: () => void;
|
|
396
478
|
onCameraPress?: () => void;
|
|
397
|
-
onAudioRecordEnd?: (result
|
|
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;
|
|
414
|
-
showUserNames?: boolean;
|
|
415
|
-
showBubbleTail?: boolean;
|
|
416
|
-
|
|
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.
|
|
420
|
-
CustomAttachmentIcon?: React.
|
|
421
|
-
CustomCameraIcon?: React.
|
|
422
|
-
CustomMicrophoneIcon?: React.
|
|
423
|
-
CustomSendIcon?: React.
|
|
424
|
-
CustomFileIcon?: React.ComponentType
|
|
425
|
-
CustomImagePreview?: React.ComponentType
|
|
426
|
-
CustomVideoPreview?: React.ComponentType
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
432
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
472
|
-
|
|
605
|
+
sentBubbleBackgroundColor?: string;
|
|
606
|
+
sentMessageTextColor?: string;
|
|
473
607
|
sentTimestampColor?: string;
|
|
474
|
-
|
|
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
|
-
|
|
483
|
-
|
|
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
|
|
492
|
-
|
|
493
|
-
inputPlaceholderTextColor?: string;
|
|
641
|
+
// Shared / UI
|
|
642
|
+
videoPlayIconColor?: string;
|
|
494
643
|
inputTextColor?: string;
|
|
495
|
-
|
|
644
|
+
inputsIconsColor?: string;
|
|
645
|
+
sendIconsColor?: string;
|
|
646
|
+
placeholderTextColor?: string;
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
sizes?: {
|
|
650
|
+
inputIconSize?: string | number;
|
|
496
651
|
};
|
|
497
652
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
509
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
698
|
+
voiceRecorder?: {
|
|
699
|
+
ui?: RecordingUIProps;
|
|
700
|
+
styles?: VoiceRecorderStyleOverrides;
|
|
701
|
+
config?: VoiceRecorderConfig;
|
|
518
702
|
};
|
|
519
703
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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, '
|
|
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; //
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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**
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
|
630
|
-
|
|
|
631
|
-
|
|
|
632
|
-
|
|
|
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
|
-
|
|
995
|
+
---
|
|
690
996
|
|
|
691
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1019
|
+
---
|
|
713
1020
|
|
|
714
|
-
|
|
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
|
-
|
|
743
|
-
|
|
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
|
-
|
|
1059
|
+
---
|
|
750
1060
|
|
|
751
|
-
|
|
1061
|
+
### Message Selection & Actions
|
|
752
1062
|
|
|
753
1063
|
```tsx
|
|
754
1064
|
<ChatScreen
|
|
755
1065
|
onMessageLongPress={(message) => {
|
|
756
|
-
// Long-press opens action menu with
|
|
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
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
|
|
805
|
-
|
|
1112
|
+
sentBubbleBackgroundColor: '#22c55e',
|
|
1113
|
+
sentMessageTextColor: '#FFFFFF',
|
|
806
1114
|
sentTimestampColor: 'rgba(255, 255, 255, 0.7)',
|
|
807
|
-
|
|
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
|
-
|
|
815
|
-
|
|
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
|
-
|
|
826
|
-
|
|
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: '
|
|
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
|
-
|
|
853
|
-
|
|
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
|
-
|
|
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={(
|
|
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
|
-
│
|
|
926
|
-
│
|
|
927
|
-
│
|
|
928
|
-
│
|
|
929
|
-
│
|
|
930
|
-
│
|
|
931
|
-
│
|
|
932
|
-
│
|
|
933
|
-
│
|
|
934
|
-
│
|
|
935
|
-
│
|
|
936
|
-
│
|
|
937
|
-
│
|
|
938
|
-
│
|
|
939
|
-
│
|
|
940
|
-
│
|
|
941
|
-
│
|
|
942
|
-
│
|
|
943
|
-
│
|
|
944
|
-
│
|
|
945
|
-
│
|
|
946
|
-
│
|
|
947
|
-
│
|
|
948
|
-
│
|
|
949
|
-
│
|
|
950
|
-
│
|
|
951
|
-
│
|
|
952
|
-
│
|
|
953
|
-
│
|
|
954
|
-
│
|
|
955
|
-
│
|
|
956
|
-
│
|
|
957
|
-
│
|
|
958
|
-
│
|
|
959
|
-
│
|
|
960
|
-
│
|
|
961
|
-
│
|
|
962
|
-
│
|
|
963
|
-
│
|
|
964
|
-
│
|
|
965
|
-
│
|
|
966
|
-
│
|
|
967
|
-
│
|
|
968
|
-
│
|
|
969
|
-
│
|
|
970
|
-
│
|
|
971
|
-
│
|
|
972
|
-
│
|
|
973
|
-
│
|
|
974
|
-
│
|
|
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
|
-
│
|
|
999
|
-
│
|
|
1000
|
-
│
|
|
1289
|
+
│ ├── commonjs/ # CommonJS build output
|
|
1290
|
+
│ ├── module/ # ES Module build output
|
|
1291
|
+
│ └── typescript/ # TypeScript declarations
|
|
1001
1292
|
├── scripts/
|
|
1002
|
-
│
|
|
1003
|
-
├── babel.config.js
|
|
1004
|
-
├── rollup.config.mjs
|
|
1005
|
-
├── tsconfig.json
|
|
1006
|
-
├── tsconfig.types.json
|
|
1007
|
-
├── tsconfig.build.json
|
|
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
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
-
|
|
1343
|
+
If your navigator already avoids the keyboard:
|
|
1057
1344
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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
|
|
1355
|
+
// Manual control via renderCustomInput
|
|
1069
1356
|
<ChatScreen
|
|
1070
|
-
renderCustomInput={(
|
|
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
|
-
|
|
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
|
-
|
|
1378
|
+
VoiceRecorderExposedState,
|
|
1092
1379
|
VoiceRecorderConfig,
|
|
1093
|
-
|
|
1380
|
+
VoiceRecorderStyleOverrides,
|
|
1381
|
+
RecordingUIProps,
|
|
1382
|
+
MessageActionFlags,
|
|
1383
|
+
MessageActionUIProps,
|
|
1384
|
+
MessageActionLabels,
|
|
1094
1385
|
ReplyConfig,
|
|
1095
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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
|
-
|
|
1140
|
-
|
|
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
|
|
1464
|
+
const darkTheme = {
|
|
1179
1465
|
colors: {
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
inputBarBackground: '#0F172A',
|
|
1466
|
+
sentBubbleBackgroundColor: '#128C7E', // WhatsApp green
|
|
1467
|
+
sentMessageTextColor: '#FFFFFF',
|
|
1468
|
+
receivedBubbleBackgroundColor: '#1F2937', // Dark gray
|
|
1469
|
+
receivedMessageTextColor: '#F3F4F6',
|
|
1185
1470
|
inputTextColor: '#F3F4F6',
|
|
1186
|
-
|
|
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
|
|
1481
|
+
For performance with large lists:
|
|
1197
1482
|
|
|
1198
1483
|
```tsx
|
|
1199
|
-
// 1.
|
|
1200
|
-
// 2.
|
|
1201
|
-
// 3.
|
|
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
|
|
1489
|
+
// Latest page of messages
|
|
1205
1490
|
]);
|
|
1206
1491
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
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
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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).
|