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