movius-chats 1.3.0 → 1.3.2
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 +605 -197
- package/lib/commonjs/index.js +3 -3
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +3 -3
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/assets/Icons/CheckAllIcon.d.ts +5 -2
- package/lib/typescript/assets/Icons/CheckIcon.d.ts +5 -2
- package/lib/typescript/components/ChatInput/TruncateFileName.d.ts +2 -2
- package/lib/typescript/hooks/useKeyboardInset.d.ts +5 -0
- package/lib/typescript/types/index.d.ts +13 -0
- package/lib/typescript/utils/theme.d.ts +6 -0
- package/package.json +1 -1
- package/src/assets/Icons/CheckAllIcon.tsx +6 -1
- package/src/assets/Icons/CheckIcon.tsx +6 -2
- package/src/components/AudioPlayer/AudioPlayer.tsx +8 -4
- package/src/components/ChatBubble/ChatBubble.tsx +15 -8
- package/src/components/ChatBubble/MessageContent.tsx +23 -10
- package/src/components/ChatBubble/MessageStatus.tsx +19 -12
- package/src/components/ChatInput/ChatInput.tsx +26 -22
- package/src/components/ChatInput/FilePreview.tsx +9 -1
- package/src/components/ChatInput/TruncateFileName.tsx +9 -3
- package/src/components/MediaViewer/MediaViewer.tsx +9 -1
- package/src/components/TypingComponent/TypingIndicator.tsx +18 -9
- package/src/hooks/useKeyboardInset.ts +42 -0
- package/src/index.tsx +35 -3
- package/src/types/index.ts +14 -0
- package/src/utils/theme.ts +37 -0
package/README.md
CHANGED
|
@@ -1,283 +1,691 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
A highly customizable, feature-rich
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
1
|
+
# movius-chats
|
|
2
|
+
|
|
3
|
+
A highly customizable, feature-rich chat UI for **React Native**. Drop in a single `ChatScreen` component to get message bubbles, media (images, video, audio), typing indicators, attachment previews, and a full input bar—with deep theming and custom icon/component hooks.
|
|
4
|
+
|
|
5
|
+
**npm:** [`movius-chats`](https://www.npmjs.com/package/movius-chats)
|
|
6
|
+
**Repository:** [github.com/David-Atueyi/Movius-Chats](https://github.com/David-Atueyi/Movius-Chats)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Table of contents
|
|
11
|
+
|
|
12
|
+
- [Requirements](#requirements)
|
|
13
|
+
- [Important: native modules & Expo](#important-native-modules--expo)
|
|
14
|
+
- [Installation](#installation)
|
|
15
|
+
- [Quick start](#quick-start)
|
|
16
|
+
- [Message data model](#message-data-model)
|
|
17
|
+
- [Message list ordering](#message-list-ordering)
|
|
18
|
+
- [ChatScreen API](#chatscreen-api)
|
|
19
|
+
- [Core props](#core-props)
|
|
20
|
+
- [Feature flags](#feature-flags)
|
|
21
|
+
- [Input & typing](#input--typing)
|
|
22
|
+
- [Attachment preview](#attachment-preview)
|
|
23
|
+
- [Theming](#theming)
|
|
24
|
+
- [Custom components & icons](#custom-components--icons)
|
|
25
|
+
- [Usage examples](#usage-examples)
|
|
26
|
+
- [Text messages](#text-messages)
|
|
27
|
+
- [Media messages](#media-messages)
|
|
28
|
+
- [Typing indicators](#typing-indicators)
|
|
29
|
+
- [Attachments & camera (parent-controlled)](#attachments--camera-parent-controlled)
|
|
30
|
+
- [Attachment preview before send](#attachment-preview-before-send)
|
|
31
|
+
- [Custom theme](#custom-theme)
|
|
32
|
+
- [Custom input bar](#custom-input-bar)
|
|
33
|
+
- [Long-press actions](#long-press-actions)
|
|
34
|
+
- [TypeScript](#typescript)
|
|
35
|
+
- [Architecture overview](#architecture-overview)
|
|
36
|
+
- [Troubleshooting](#troubleshooting)
|
|
37
|
+
- [Contributing](#contributing)
|
|
38
|
+
- [License](#license)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Requirements
|
|
43
|
+
|
|
44
|
+
| Dependency | Role |
|
|
45
|
+
|------------|------|
|
|
46
|
+
| `react` ≥ 16.8 | Peer dependency |
|
|
47
|
+
| `react-native` | Peer dependency |
|
|
48
|
+
| `react-native-reanimated` | Audio scrubber animations (peer) |
|
|
49
|
+
| `react-native-image-zoom-viewer` | Full-screen image viewer |
|
|
50
|
+
| `react-native-parsed-text` | Clickable URLs in messages |
|
|
51
|
+
| `react-native-sound` | Voice message playback |
|
|
52
|
+
| `react-native-svg` | Built-in icons |
|
|
53
|
+
| `react-native-video` | Video bubbles & preview |
|
|
54
|
+
| `twrnc` | Tailwind-style utility classes |
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Important: native modules & Expo
|
|
59
|
+
|
|
60
|
+
- **Rebuild required** after install. This library uses native modules (`react-native-sound`, `react-native-video`, etc.).
|
|
61
|
+
- **Not compatible with Expo Go.** Use a [development build](https://docs.expo.dev/develop/development-builds/introduction/) or a bare React Native app.
|
|
62
|
+
- **iOS:** run `pod install` in the `ios` folder after adding dependencies.
|
|
63
|
+
|
|
64
|
+
---
|
|
28
65
|
|
|
29
66
|
## Installation
|
|
30
67
|
|
|
68
|
+
### 1. Install the package
|
|
69
|
+
|
|
31
70
|
```bash
|
|
32
71
|
npm install movius-chats
|
|
33
72
|
# or
|
|
34
73
|
yarn add movius-chats
|
|
74
|
+
# or
|
|
75
|
+
bun install movius-chats
|
|
35
76
|
```
|
|
36
77
|
|
|
37
|
-
###
|
|
78
|
+
### 2. Install peer & native dependencies
|
|
38
79
|
|
|
39
|
-
|
|
80
|
+
These are required in **your app** (some are bundled as dependencies of `movius-chats`, but you must still link/native-build them in the host app):
|
|
40
81
|
|
|
41
82
|
```bash
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
#
|
|
46
|
-
|
|
83
|
+
npm install react-native-reanimated react-native-image-zoom-viewer react-native-sound react-native-svg react-native-video twrnc
|
|
84
|
+
# or
|
|
85
|
+
yarn add react-native-reanimated react-native-image-zoom-viewer react-native-sound react-native-svg react-native-video twrnc
|
|
86
|
+
#or
|
|
87
|
+
bun install react-native-reanimated react-native-image-zoom-viewer react-native-sound react-native-svg react-native-video twrnc
|
|
47
88
|
```
|
|
48
89
|
|
|
49
|
-
|
|
90
|
+
> `react-native-parsed-text` is pulled in transitively; no extra install step unless your bundler requires it.
|
|
91
|
+
|
|
92
|
+
### 3. Configure Reanimated
|
|
50
93
|
|
|
51
|
-
|
|
94
|
+
Add the Reanimated Babel plugin **last** in `babel.config.js`:
|
|
52
95
|
|
|
53
96
|
```javascript
|
|
54
97
|
module.exports = {
|
|
55
|
-
|
|
98
|
+
presets: ['module:metro-react-native-babel-preset'],
|
|
99
|
+
plugins: [
|
|
100
|
+
// ...other plugins
|
|
101
|
+
'react-native-reanimated/plugin',
|
|
102
|
+
],
|
|
56
103
|
};
|
|
57
104
|
```
|
|
58
105
|
|
|
59
|
-
###
|
|
106
|
+
### 4. Configure react-native-sound (recommended)
|
|
60
107
|
|
|
61
|
-
|
|
108
|
+
**iOS** — enable playback in silent mode (optional, in `AppDelegate`):
|
|
62
109
|
|
|
63
|
-
|
|
110
|
+
```objc
|
|
111
|
+
#import <AVFoundation/AVFoundation.h>
|
|
64
112
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
npx react-native run-ios # Rebuild for iOS
|
|
69
|
-
```
|
|
113
|
+
// inside didFinishLaunchingWithOptions:
|
|
114
|
+
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
|
|
115
|
+
```
|
|
70
116
|
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
npx expo prebuild # Generate native code
|
|
74
|
-
npx expo run:android # Build and run on Android
|
|
75
|
-
npx expo run:ios # Build and run on iOS
|
|
76
|
-
```
|
|
117
|
+
**Android** — ensure internet permission if loading remote audio URLs in `AndroidManifest.xml`:
|
|
77
118
|
|
|
78
|
-
|
|
119
|
+
```xml
|
|
120
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
121
|
+
```
|
|
79
122
|
|
|
80
|
-
|
|
123
|
+
### 5. Rebuild the native app
|
|
124
|
+
|
|
125
|
+
**React Native CLI:**
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
cd ios && pod install && cd ..
|
|
129
|
+
npx react-native run-ios
|
|
130
|
+
npx react-native run-android
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Expo (development build):**
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npx expo prebuild
|
|
137
|
+
npx expo run:ios
|
|
138
|
+
# or
|
|
139
|
+
npx expo run:android
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
After native dependency updates:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npx expo prebuild --clean
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Quick start
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
import React, { useState } from 'react';
|
|
154
|
+
import { SafeAreaView } from 'react-native';
|
|
81
155
|
import ChatScreen from 'movius-chats';
|
|
82
|
-
import { Message } from 'movius-chats/lib/typescript/types';
|
|
83
|
-
import { useState } from 'react';
|
|
156
|
+
import type { Message } from 'movius-chats/lib/typescript/types';
|
|
84
157
|
|
|
85
|
-
|
|
158
|
+
export default function App() {
|
|
86
159
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
160
|
+
const currentUserId = 'user-1';
|
|
87
161
|
|
|
88
|
-
const handleSendMessage = (
|
|
89
|
-
|
|
162
|
+
const handleSendMessage = (
|
|
163
|
+
payload: Omit<Message, 'id' | 'time' | 'status'>
|
|
164
|
+
) => {
|
|
90
165
|
const newMessage: Message = {
|
|
91
|
-
...
|
|
92
|
-
id: Date.now()
|
|
93
|
-
time: new Date().toLocaleTimeString(
|
|
94
|
-
|
|
166
|
+
...payload,
|
|
167
|
+
id: String(Date.now()),
|
|
168
|
+
time: new Date().toLocaleTimeString([], {
|
|
169
|
+
hour: '2-digit',
|
|
170
|
+
minute: '2-digit',
|
|
171
|
+
}),
|
|
172
|
+
status: 'sent',
|
|
95
173
|
};
|
|
96
|
-
|
|
174
|
+
// Newest first — see "Message list ordering"
|
|
175
|
+
setMessages((prev) => [newMessage, ...prev]);
|
|
97
176
|
};
|
|
98
177
|
|
|
99
178
|
return (
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
179
|
+
<SafeAreaView style={{ flex: 1 }}>
|
|
180
|
+
<ChatScreen
|
|
181
|
+
messages={messages}
|
|
182
|
+
currentUserId={currentUserId}
|
|
183
|
+
onSendMessage={handleSendMessage}
|
|
184
|
+
placeholder="Type a message..."
|
|
185
|
+
showAvatars
|
|
186
|
+
showBubbleTail
|
|
187
|
+
showMessageStatus
|
|
188
|
+
showEmojiButton
|
|
189
|
+
showAttachmentsButton
|
|
190
|
+
showCameraButton
|
|
191
|
+
showVoiceRecordButton
|
|
192
|
+
/>
|
|
193
|
+
</SafeAreaView>
|
|
106
194
|
);
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Message data model
|
|
201
|
+
|
|
202
|
+
Each item in `messages` must match the `Message` interface:
|
|
203
|
+
|
|
204
|
+
| Field | Type | Required | Description |
|
|
205
|
+
|-------|------|----------|-------------|
|
|
206
|
+
| `id` | `string` | Yes | Unique message id |
|
|
207
|
+
| `senderId` | `string` | Yes | User id of the sender |
|
|
208
|
+
| `time` | `string` | Yes | Display time (e.g. `"2:30 PM"`) — you format this |
|
|
209
|
+
| `status` | `'sent' \| 'delivered' \| 'read'` | Yes | Delivery state (shown only for current user) |
|
|
210
|
+
| `text` | `string` | No | Plain text; URLs are auto-linked |
|
|
211
|
+
| `image` | `string` | No | Image URI |
|
|
212
|
+
| `video` | `string` | No | Video URI |
|
|
213
|
+
| `audio` | `string` | No | Audio file URI |
|
|
214
|
+
| `senderName` | `string` | No | Shown when `showUserNames` is true |
|
|
215
|
+
| `senderAvatar` | `string` | No | Avatar image URI; falls back to first letter of `senderName` |
|
|
216
|
+
|
|
217
|
+
A message can combine fields (e.g. text + image), but typically you use one primary content type per bubble.
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import type { Message } from 'movius-chats/lib/typescript/types';
|
|
221
|
+
|
|
222
|
+
const example: Message = {
|
|
223
|
+
id: '1',
|
|
224
|
+
senderId: 'user-2',
|
|
225
|
+
senderName: 'Alex',
|
|
226
|
+
senderAvatar: 'https://example.com/avatar.jpg',
|
|
227
|
+
text: 'Check this out https://example.com',
|
|
228
|
+
time: '10:42 AM',
|
|
229
|
+
status: 'read',
|
|
107
230
|
};
|
|
108
231
|
```
|
|
109
232
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Message list ordering
|
|
236
|
+
|
|
237
|
+
`ChatScreen` uses an **inverted** `FlatList`. Put the **newest message at index `0`** of the `messages` array:
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
setMessages((prev) => [newMessage, ...prev]); // ✅ correct
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Older messages sit at higher indices and appear higher on screen.
|
|
244
|
+
|
|
245
|
+
**Grouping:** consecutive messages from the same sender share bubble styling; avatars and bubble tails show on the first message of a sequence (`isFirstInSequence`).
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## ChatScreen API
|
|
250
|
+
|
|
251
|
+
`ChatScreen` is the **default export** from `movius-chats`. It wraps your chat in `AudioProvider` + `ChatProvider` and renders the message list, typing indicator, input (or custom input), and full-screen media viewer.
|
|
252
|
+
|
|
253
|
+
### Core props
|
|
254
|
+
|
|
255
|
+
| Prop | Type | Required | Description |
|
|
256
|
+
|------|------|----------|-------------|
|
|
257
|
+
| `messages` | `Message[]` | Yes | Messages to render (newest first) |
|
|
258
|
+
| `currentUserId` | `string` | Yes | Logged-in user id; used for bubble alignment & status |
|
|
259
|
+
| `onSendMessage` | `(msg: Omit<Message, 'id' \| 'time' \| 'status'>) => void` | Yes | Fired when user taps send with text and/or `previewData` |
|
|
260
|
+
| `onMessageLongPress` | `(message: Message) => void` | No | Long-press on a bubble (reply, delete, etc.) |
|
|
261
|
+
| `placeholder` | `string` | No | Input placeholder (default: `"Message"`) |
|
|
262
|
+
| `keyboardVerticalOffset` | `number` | No | Subtract from keyboard height (header + tab bar + safe area). Default: `0` |
|
|
263
|
+
| `disableKeyboardAvoiding` | `boolean` | No | Set `true` if your screen already handles the keyboard |
|
|
264
|
+
|
|
265
|
+
### Feature flags
|
|
266
|
+
|
|
267
|
+
All flags below default to **falsy** (hidden) unless you pass `true`:
|
|
268
|
+
|
|
269
|
+
| Prop | Description |
|
|
270
|
+
|------|-------------|
|
|
271
|
+
| `showAvatars` | Avatar (or initial) on received messages & typing row |
|
|
272
|
+
| `showUserNames` | Sender name above received bubbles |
|
|
273
|
+
| `showBubbleTail` | WhatsApp-style tail on first bubble in a sequence |
|
|
274
|
+
| `showMessageStatus` | Timestamp + checkmarks for sent messages |
|
|
275
|
+
| `showEmojiButton` | Emoji button in input (UI only; wire your own picker) |
|
|
276
|
+
| `showAttachmentsButton` | Paperclip → calls `onAttachmentPress` |
|
|
277
|
+
| `showCameraButton` | Camera icon when input is empty → `onCameraPress` |
|
|
278
|
+
| `showVoiceRecordButton` | Mic when input empty; send when text present |
|
|
279
|
+
|
|
280
|
+
### Input & typing
|
|
281
|
+
|
|
282
|
+
| Prop | Type | Description |
|
|
283
|
+
|------|------|-------------|
|
|
284
|
+
| `onTypingStart` | `() => void` | Called when input has non-empty text |
|
|
285
|
+
| `onTypingEnd` | `() => void` | Called when input is cleared |
|
|
286
|
+
| `onAttachmentPress` | `() => void` | User tapped attachment — open document picker, etc. |
|
|
287
|
+
| `onCameraPress` | `() => void` | User tapped camera — open camera / image picker |
|
|
288
|
+
| `onAudioRecordStart` | `() => void` | Mic press / long-press start |
|
|
289
|
+
| `onAudioRecordEnd` | `() => void` | Mic release — upload recorded audio and append to messages |
|
|
290
|
+
| `typingUsers` | `Array<{ id: string; avatar: string; name: string }>` | Users currently typing (excludes `currentUserId` in UI) |
|
|
291
|
+
|
|
292
|
+
**Note:** Built-in `onSendMessage` from the default input only includes `{ text, senderId }`. Recording, camera, and file picking are **intentionally delegated** to your app via the callbacks above.
|
|
293
|
+
|
|
294
|
+
### Attachment preview
|
|
295
|
+
|
|
296
|
+
Show a file/image/video preview above the input before sending:
|
|
297
|
+
|
|
298
|
+
| Prop | Type | Description |
|
|
299
|
+
|------|------|-------------|
|
|
300
|
+
| `previewData` | `{ uri: string; type: string; name: string }` | MIME type in `type` (e.g. `image/jpeg`, `video/mp4`, `application/pdf`) |
|
|
301
|
+
| `closePreview` | `() => void` | Clear preview when user taps X |
|
|
302
|
+
|
|
303
|
+
When `previewData` is set, send is enabled even if text is empty. Your `onSendMessage` handler should read `previewData` from closure/state and attach `image`, `video`, or file metadata to the outgoing message.
|
|
128
304
|
|
|
129
305
|
### Theming
|
|
130
306
|
|
|
131
|
-
|
|
307
|
+
Pass a `theme` object to customize colors, typography, and styles. All keys are optional.
|
|
132
308
|
|
|
133
309
|
```typescript
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
messageStyle?: {
|
|
157
|
-
sentTextStyle?: TextStyle;
|
|
158
|
-
receivedTextStyle?: TextStyle;
|
|
159
|
-
audioPlayButtonStyle?: ViewStyle;
|
|
160
|
-
audioKnobStyle?: ViewStyle;
|
|
161
|
-
progressBarStyle?: ViewStyle;
|
|
162
|
-
activeProgressBarStyle?: ViewStyle;
|
|
163
|
-
audioDurationStyle?: TextStyle;
|
|
164
|
-
};
|
|
165
|
-
inputStyles?: {
|
|
166
|
-
inputSectionContainerStyle?: ViewStyle;
|
|
167
|
-
inputContainerStyle?: ViewStyle;
|
|
168
|
-
sendButtonStyle?: ViewStyle;
|
|
169
|
-
};
|
|
170
|
-
filePreviewStyle?: {
|
|
171
|
-
root?: ViewStyle;
|
|
172
|
-
container?: ViewStyle;
|
|
173
|
-
iconContainer?: ViewStyle;
|
|
174
|
-
nameContainer?: ViewStyle;
|
|
175
|
-
text?: TextStyle;
|
|
310
|
+
theme?: {
|
|
311
|
+
fontFamily?: string;
|
|
312
|
+
|
|
313
|
+
colors?: {
|
|
314
|
+
sentMessageTailColor?: string;
|
|
315
|
+
receivedMessageTailColor?: string;
|
|
316
|
+
timestamp?: string;
|
|
317
|
+
inputsIconsColor?: string;
|
|
318
|
+
sendIconsColor?: string;
|
|
319
|
+
placeholderTextColor?: string;
|
|
320
|
+
inputTextColor?: string;
|
|
321
|
+
audioPlayIconColor?: string;
|
|
322
|
+
audioPauseIconColor?: string;
|
|
323
|
+
videoPlayIconColor?: string;
|
|
324
|
+
sentIconColor?: string;
|
|
325
|
+
deliveredIconColor?: string;
|
|
326
|
+
readIconColor?: string;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
sizes?: {
|
|
330
|
+
/** Twrnc classes (`"h-8 w-8"`) or pixels (`28`) for input-bar icons */
|
|
331
|
+
inputIconSize?: string | number;
|
|
176
332
|
};
|
|
333
|
+
|
|
334
|
+
bubbleStyle?: {
|
|
335
|
+
sent?: ViewStyle;
|
|
336
|
+
received?: ViewStyle;
|
|
337
|
+
avatarTextStyle?: TextStyle;
|
|
338
|
+
userNameStyle?: TextStyle;
|
|
339
|
+
avatarImageStyle?: ImageStyle;
|
|
340
|
+
typingContainerStyle?: ViewStyle;
|
|
341
|
+
additionalTypingUsersContainerStyle?: ViewStyle;
|
|
342
|
+
additionalTypingUsersTextStyle?: TextStyle;
|
|
177
343
|
};
|
|
344
|
+
|
|
345
|
+
messageStyle?: {
|
|
346
|
+
sentTextStyle?: TextStyle;
|
|
347
|
+
receivedTextStyle?: TextStyle;
|
|
348
|
+
audioPlayButtonStyle?: ViewStyle;
|
|
349
|
+
audioKnobStyle?: ViewStyle;
|
|
350
|
+
progressBarStyle?: ViewStyle;
|
|
351
|
+
activeProgressBarStyle?: ViewStyle;
|
|
352
|
+
audioDurationStyle?: TextStyle;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
inputStyles?: {
|
|
356
|
+
inputSectionContainerStyle?: ViewStyle;
|
|
357
|
+
inputContainerStyle?: ViewStyle;
|
|
358
|
+
sendButtonStyle?: ViewStyle;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
filePreviewStyle?: {
|
|
362
|
+
root?: ViewStyle;
|
|
363
|
+
container?: ViewStyle;
|
|
364
|
+
iconContainer?: ViewStyle;
|
|
365
|
+
nameContainer?: ViewStyle;
|
|
366
|
+
text?: TextStyle;
|
|
367
|
+
};
|
|
368
|
+
}
|
|
178
369
|
```
|
|
179
370
|
|
|
180
|
-
|
|
371
|
+
Default bubble colors (before `theme` overrides): sent ≈ green (`bg-green-500`), received ≈ white.
|
|
181
372
|
|
|
182
|
-
|
|
183
|
-
| ---------------------------- | ------------------------------------ | -------------------------- | ------------------------------- |
|
|
184
|
-
| renderCustomInput | () => React.ReactNode | Custom input component |
|
|
185
|
-
| renderCustomVideoBubbleError | () => React.ReactNode | Custom video error display |
|
|
186
|
-
| renderCustomTyping | () => React.ReactNode | Custom typing indicator |
|
|
187
|
-
| CustomEmojiIcon | () => React.ReactNode | Custom emoji picker icon |
|
|
188
|
-
| CustomAttachmentIcon | () => React.ReactNode | Custom attachment icon |
|
|
189
|
-
| CustomCameraIcon | () => React.ReactNode | Custom camera icon |
|
|
190
|
-
| CustomSendIcon | () => React.ReactNode | Custom send button icon |
|
|
191
|
-
| CustomMicrophoneIcon | () => React.ReactNode | Custom microphone icon |
|
|
192
|
-
| CustomPlayIcon | () => React.ReactNode | Custom play icon |
|
|
193
|
-
| CustomPauseIcon | () => React.ReactNode | Custom pause icon |
|
|
194
|
-
| CustomFileIcon | React.ComponentType<{ style?: any }> | Custom file icon |
|
|
195
|
-
| CustomImagePreview | React.ComponentType<{ uri: string }> | Custom image preview component. |
|
|
196
|
-
| CustomVideoPreview | React.ComponentType<{ uri: string }> | Custom video preview component. |
|
|
373
|
+
#### Custom font (`theme.fontFamily`)
|
|
197
374
|
|
|
375
|
+
`fontFamily` applies to **all text** in the package (messages, timestamps, typing label, input, file names, errors).
|
|
198
376
|
|
|
199
|
-
|
|
377
|
+
**You must load the font in your app first** — the library only sets the `fontFamily` style name.
|
|
200
378
|
|
|
201
|
-
|
|
379
|
+
**Expo:**
|
|
202
380
|
|
|
203
|
-
```
|
|
381
|
+
```tsx
|
|
382
|
+
import { useFonts } from 'expo-font';
|
|
383
|
+
|
|
384
|
+
export default function App() {
|
|
385
|
+
const [loaded] = useFonts({
|
|
386
|
+
InterRegular: require('./assets/fonts/Inter-Regular.ttf'),
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
if (!loaded) return null;
|
|
390
|
+
|
|
391
|
+
return (
|
|
392
|
+
<ChatScreen
|
|
393
|
+
theme={{ fontFamily: 'InterRegular' }}
|
|
394
|
+
// ...
|
|
395
|
+
/>
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**React Native CLI:** follow [react-native custom fonts](https://reactnative.dev/docs/custom-fonts) and use the exact font family name registered on each platform.
|
|
401
|
+
|
|
402
|
+
#### Input icon size (`theme.sizes.inputIconSize`)
|
|
403
|
+
|
|
404
|
+
Use either **pixels** (recommended) or **twrnc classes**:
|
|
405
|
+
|
|
406
|
+
```tsx
|
|
204
407
|
<ChatScreen
|
|
205
|
-
messages={messages}
|
|
206
|
-
currentUserId="user123"
|
|
207
|
-
onSendMessage={handleSendMessage}
|
|
208
408
|
theme={{
|
|
409
|
+
sizes: {
|
|
410
|
+
inputIconSize: 28, // 28×28 px — attachment, camera, emoji, send, mic
|
|
411
|
+
// inputIconSize: 'h-8 w-8', // alternative: tailwind classes via twrnc
|
|
412
|
+
},
|
|
413
|
+
}}
|
|
414
|
+
/>
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
#### Keyboard avoiding
|
|
418
|
+
|
|
419
|
+
The package lifts the chat when the keyboard opens (keyboard listeners + `KeyboardAvoidingView` on iOS).
|
|
420
|
+
|
|
421
|
+
1. Wrap `ChatScreen` in a parent with `flex: 1` (e.g. `SafeAreaView style={{ flex: 1 }}`).
|
|
422
|
+
2. Set `keyboardVerticalOffset` to your header + top inset (try `60`–`100` on iOS with a stack header):
|
|
423
|
+
|
|
424
|
+
```tsx
|
|
425
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
426
|
+
|
|
427
|
+
const insets = useSafeAreaInsets();
|
|
428
|
+
|
|
429
|
+
<ChatScreen
|
|
430
|
+
keyboardVerticalOffset={insets.top + 44}
|
|
431
|
+
// ...
|
|
432
|
+
/>
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
3. **Android (Expo):** in `app.json`:
|
|
436
|
+
|
|
437
|
+
```json
|
|
438
|
+
"android": {
|
|
439
|
+
"softwareKeyboardLayoutMode": "resize"
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
4. If your navigator already uses `KeyboardAvoidingView`, pass `disableKeyboardAvoiding` to avoid double offset.
|
|
444
|
+
|
|
445
|
+
### Custom components & icons
|
|
446
|
+
|
|
447
|
+
| Prop | Type | Description |
|
|
448
|
+
|------|------|-------------|
|
|
449
|
+
| `renderCustomInput` | `() => React.ReactNode` | Replace entire input bar |
|
|
450
|
+
| `renderCustomTyping` | `() => React.ReactNode` | Replace typing bubble content |
|
|
451
|
+
| `renderCustomVideoBubbleError` | `() => React.ReactNode` | Replace inline video error UI |
|
|
452
|
+
| `CustomEmojiIcon` | `() => React.ReactNode` | Emoji button |
|
|
453
|
+
| `CustomAttachmentIcon` | `() => React.ReactNode` | Attachment button |
|
|
454
|
+
| `CustomCameraIcon` | `() => React.ReactNode` | Camera button |
|
|
455
|
+
| `CustomSendIcon` | `() => React.ReactNode` | Send button |
|
|
456
|
+
| `CustomMicrophoneIcon` | `() => React.ReactNode` | Microphone button |
|
|
457
|
+
| `CustomPlayIcon` | `() => React.ReactNode` | Play icon in video/audio bubbles |
|
|
458
|
+
| `CustomPauseIcon` | `() => React.ReactNode` | Pause icon in audio player |
|
|
459
|
+
| `CustomFileIcon` | `React.ComponentType<{ style?: any }>` | Generic file preview icon |
|
|
460
|
+
| `CustomImagePreview` | `React.ComponentType<{ uri: string }>` | Image attachment preview |
|
|
461
|
+
| `CustomVideoPreview` | `React.ComponentType<{ uri: string }>` | Video attachment preview |
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## Usage examples
|
|
466
|
+
|
|
467
|
+
### Text messages
|
|
468
|
+
|
|
469
|
+
```tsx
|
|
470
|
+
onSendMessage={({ text, senderId }) => {
|
|
471
|
+
addMessage({ text, senderId, status: 'sent' });
|
|
472
|
+
}}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### Media messages
|
|
476
|
+
|
|
477
|
+
Add URIs when building the `Message` object (usually after upload):
|
|
478
|
+
|
|
479
|
+
```tsx
|
|
480
|
+
const imageMessage: Message = {
|
|
481
|
+
id: '2',
|
|
482
|
+
senderId: currentUserId,
|
|
483
|
+
image: 'https://cdn.example.com/photo.jpg',
|
|
484
|
+
time: '11:00 AM',
|
|
485
|
+
status: 'delivered',
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const audioMessage: Message = {
|
|
489
|
+
id: '3',
|
|
490
|
+
senderId: 'user-2',
|
|
491
|
+
audio: 'file:///path/to/recording.m4a',
|
|
492
|
+
time: '11:05 AM',
|
|
493
|
+
status: 'read',
|
|
494
|
+
};
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Tap image/video bubbles to open the built-in **MediaViewer** (pinch-zoom for images, native controls for video).
|
|
498
|
+
|
|
499
|
+
### Typing indicators
|
|
500
|
+
|
|
501
|
+
```tsx
|
|
502
|
+
const [typingUsers, setTypingUsers] = useState<
|
|
503
|
+
{ id: string; avatar: string; name: string }[]
|
|
504
|
+
>([]);
|
|
505
|
+
|
|
506
|
+
<ChatScreen
|
|
507
|
+
typingUsers={typingUsers}
|
|
508
|
+
onTypingStart={() => notifyServer('typing-start')}
|
|
509
|
+
onTypingEnd={() => notifyServer('typing-end')}
|
|
510
|
+
// ...
|
|
511
|
+
/>
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
When your socket receives “user X is typing”, push into `typingUsers`. The component shows up to two avatars plus a “+N” badge.
|
|
515
|
+
|
|
516
|
+
### Attachments & camera (parent-controlled)
|
|
517
|
+
|
|
518
|
+
```tsx
|
|
519
|
+
import { launchImageLibrary } from 'react-native-image-picker';
|
|
520
|
+
|
|
521
|
+
<ChatScreen
|
|
522
|
+
showAttachmentsButton
|
|
523
|
+
showCameraButton
|
|
524
|
+
onAttachmentPress={async () => {
|
|
525
|
+
const result = await launchImageLibrary({ mediaType: 'mixed' });
|
|
526
|
+
}}
|
|
527
|
+
onCameraPress={async () => {
|
|
528
|
+
// open camera, then add message with image/video URI
|
|
529
|
+
}}
|
|
530
|
+
onAudioRecordStart={() => {
|
|
531
|
+
// start native recorder
|
|
532
|
+
}}
|
|
533
|
+
onAudioRecordEnd={async () => {
|
|
534
|
+
// stop recorder, upload, then:
|
|
535
|
+
// addMessage({ audio: uploadedUrl, senderId: currentUserId, ... });
|
|
536
|
+
}}
|
|
537
|
+
/>
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Attachment preview before send
|
|
541
|
+
|
|
542
|
+
```tsx
|
|
543
|
+
const [previewData, setPreviewData] = useState<{
|
|
544
|
+
uri: string;
|
|
545
|
+
type: string;
|
|
546
|
+
name: string;
|
|
547
|
+
} | null>(null);
|
|
548
|
+
|
|
549
|
+
<ChatScreen
|
|
550
|
+
previewData={previewData ?? undefined}
|
|
551
|
+
closePreview={() => setPreviewData(null)}
|
|
552
|
+
onAttachmentPress={async () => {
|
|
553
|
+
const asset = await pickDocument();
|
|
554
|
+
setPreviewData({
|
|
555
|
+
uri: asset.uri,
|
|
556
|
+
type: asset.type ?? 'application/octet-stream',
|
|
557
|
+
name: asset.name ?? 'file',
|
|
558
|
+
});
|
|
559
|
+
}}
|
|
560
|
+
onSendMessage={({ text, senderId }) => {
|
|
561
|
+
const msg: Message = {
|
|
562
|
+
id: String(Date.now()),
|
|
563
|
+
senderId,
|
|
564
|
+
text: text || undefined,
|
|
565
|
+
image: previewData?.type.startsWith('image/')
|
|
566
|
+
? previewData.uri
|
|
567
|
+
: undefined,
|
|
568
|
+
video: previewData?.type.startsWith('video/')
|
|
569
|
+
? previewData.uri
|
|
570
|
+
: undefined,
|
|
571
|
+
time: formatTime(new Date()),
|
|
572
|
+
status: 'sent',
|
|
573
|
+
};
|
|
574
|
+
setMessages((prev) => [msg, ...prev]);
|
|
575
|
+
setPreviewData(null);
|
|
576
|
+
}}
|
|
577
|
+
/>
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### Custom theme
|
|
581
|
+
|
|
582
|
+
```tsx
|
|
583
|
+
<ChatScreen
|
|
584
|
+
theme={{
|
|
585
|
+
fontFamily: 'Inter-Regular',
|
|
209
586
|
colors: {
|
|
210
587
|
sentMessageTailColor: '#007AFF',
|
|
211
588
|
receivedMessageTailColor: '#E9E9EB',
|
|
212
589
|
timestamp: '#8E8E93',
|
|
590
|
+
readIconColor: '#34C759',
|
|
591
|
+
inputTextColor: '#000000',
|
|
213
592
|
},
|
|
214
593
|
bubbleStyle: {
|
|
215
|
-
sent: {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
borderRadius: 20,
|
|
222
|
-
},
|
|
594
|
+
sent: { backgroundColor: '#007AFF' },
|
|
595
|
+
received: { backgroundColor: '#E9E9EB' },
|
|
596
|
+
},
|
|
597
|
+
messageStyle: {
|
|
598
|
+
sentTextStyle: { color: '#FFFFFF' },
|
|
599
|
+
receivedTextStyle: { color: '#000000' },
|
|
223
600
|
},
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
container: { backgroundColor: '#f0f0f0', borderRadius: 16 },
|
|
227
|
-
iconContainer: { backgroundColor: '#333' },
|
|
228
|
-
nameContainer: { backgroundColor: '#eee' },
|
|
229
|
-
text: { color: 'red', fontWeight: 'bold' },
|
|
601
|
+
inputStyles: {
|
|
602
|
+
sendButtonStyle: { backgroundColor: '#007AFF' },
|
|
230
603
|
},
|
|
231
604
|
}}
|
|
605
|
+
// ...
|
|
232
606
|
/>
|
|
233
607
|
```
|
|
234
608
|
|
|
235
|
-
###
|
|
609
|
+
### Custom input bar
|
|
236
610
|
|
|
237
|
-
|
|
611
|
+
```tsx
|
|
612
|
+
<ChatScreen
|
|
613
|
+
renderCustomInput={() => <MyComposer />}
|
|
614
|
+
// Still use messages / currentUserId / onSendMessage via your own state
|
|
615
|
+
/>
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
When using `renderCustomInput`, you are responsible for calling your send logic; the default `ChatInput` is not mounted.
|
|
619
|
+
|
|
620
|
+
### Long-press actions
|
|
621
|
+
|
|
622
|
+
```tsx
|
|
623
|
+
<ChatScreen
|
|
624
|
+
onMessageLongPress={(message) => {
|
|
625
|
+
}}
|
|
626
|
+
/>
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
---
|
|
630
|
+
|
|
631
|
+
## TypeScript
|
|
238
632
|
|
|
239
|
-
|
|
633
|
+
The main export is the default `ChatScreen` component. Types live in the build output:
|
|
240
634
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
635
|
+
```typescript
|
|
636
|
+
import ChatScreen from 'movius-chats';
|
|
637
|
+
import type {
|
|
638
|
+
Message,
|
|
639
|
+
ChatScreenProps,
|
|
640
|
+
} from 'movius-chats/lib/typescript/types';
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
`ChatScreenProps` is the full props interface for `ChatScreen`.
|
|
644
|
+
|
|
645
|
+
---
|
|
244
646
|
|
|
245
|
-
|
|
647
|
+
## Architecture overview
|
|
246
648
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
649
|
+
```
|
|
650
|
+
ChatScreen
|
|
651
|
+
├── AudioProvider # one audio message plays at a time
|
|
652
|
+
├── ChatProvider # props, theme, media viewer state
|
|
653
|
+
├── FlatList (inverted) # ChatBubble per message
|
|
654
|
+
│ └── ListHeaderComponent → TypingIndicator
|
|
655
|
+
├── ChatInput (optional) # text, icons, file preview
|
|
656
|
+
└── MediaViewer (Modal) # full-screen image / video
|
|
657
|
+
```
|
|
252
658
|
|
|
253
|
-
|
|
254
|
-
```bash
|
|
255
|
-
npx expo prebuild --clean
|
|
256
|
-
```
|
|
659
|
+
Internal pieces (not exported from the package entry, but useful when reading source):
|
|
257
660
|
|
|
258
|
-
|
|
661
|
+
- **ChatBubble** — layout, tail, avatar, `MessageContent`, `MessageStatus`
|
|
662
|
+
- **MessageContent** — image, video thumbnail, `AudioPlayer`, parsed text
|
|
663
|
+
- **AudioPlayer** — `react-native-sound` + Reanimated scrubber
|
|
664
|
+
- **FilePreview** — pre-send attachment chip above input
|
|
259
665
|
|
|
260
|
-
|
|
261
|
-
- Avatar images are cached automatically
|
|
262
|
-
- Media messages use lazy loading
|
|
263
|
-
- Typing indicators are debounced
|
|
666
|
+
---
|
|
264
667
|
|
|
265
668
|
## Troubleshooting
|
|
266
669
|
|
|
267
|
-
|
|
670
|
+
| Issue | What to try |
|
|
671
|
+
|-------|-------------|
|
|
672
|
+
| `Native module not found` | Rebuild iOS/Android after install; run `pod install` on iOS |
|
|
673
|
+
| Crashes in Expo Go | Use a development build; native modules are not in Expo Go |
|
|
674
|
+
| Audio silent on iOS | Set `AVAudioSession` category to playback (see installation) |
|
|
675
|
+
| Video/audio won’t load | Check URI scheme (`https://`, `file://`) and Android `INTERNET` permission |
|
|
676
|
+
| Reanimated worklet errors | Ensure `react-native-reanimated/plugin` is **last** in Babel config |
|
|
677
|
+
| Types not found | Import from `movius-chats/lib/typescript/types` |
|
|
678
|
+
| Messages appear in wrong order | Newest item must be `messages[0]` (inverted list) |
|
|
679
|
+
| Icons/buttons missing | Pass feature flags (`showEmojiButton`, etc.) — they default to off |
|
|
268
680
|
|
|
269
|
-
|
|
270
|
-
- **Crashes in Expo Go**: This package uses native modules that are not compatible with Expo Go. Use a development build instead.
|
|
271
|
-
- **Audio/Video not working**: Check that you've installed all required dependencies and rebuilt the app.
|
|
681
|
+
---
|
|
272
682
|
|
|
273
683
|
## Contributing
|
|
274
684
|
|
|
275
|
-
|
|
685
|
+
Issues and pull requests are welcome on [GitHub](https://github.com/David-Atueyi/Movius-Chats/issues).
|
|
276
686
|
|
|
277
|
-
|
|
687
|
+
---
|
|
278
688
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
## Support
|
|
689
|
+
## License
|
|
282
690
|
|
|
283
|
-
|
|
691
|
+
ISC — see [package.json](./package.json).
|