movius-chats 1.3.6 → 1.3.8
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 +473 -367
- 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/components/ChatBubble/MediaGrid.d.ts +8 -0
- package/lib/typescript/components/ChatBubble/types.d.ts +4 -1
- package/lib/typescript/components/ChatInput/FilePreview.d.ts +16 -2
- package/lib/typescript/components/MediaViewer/MediaViewer.d.ts +8 -1
- package/lib/typescript/context/ChatContext.d.ts +9 -9
- package/lib/typescript/types/index.d.ts +20 -8
- package/lib/typescript/utils/messageMedia.d.ts +3 -0
- package/package.json +1 -1
- package/src/components/ChatBubble/ChatBubble.tsx +20 -13
- package/src/components/ChatBubble/MediaGrid.tsx +316 -0
- package/src/components/ChatBubble/MessageContent.tsx +34 -102
- package/src/components/ChatBubble/MessageStatus.tsx +8 -1
- package/src/components/ChatBubble/types.ts +5 -2
- package/src/components/ChatInput/ChatInput.tsx +26 -22
- package/src/components/ChatInput/FilePreview.tsx +230 -115
- package/src/components/MediaViewer/MediaViewer.tsx +161 -83
- package/src/context/ChatContext.tsx +32 -6
- package/src/index.tsx +4 -9
- package/src/types/index.ts +23 -4
- package/src/utils/messageMedia.ts +16 -0
- package/lib/typescript/components/MediaViewer/types.d.ts +0 -5
- package/src/components/MediaViewer/types.ts +0 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# movius-chats
|
|
2
2
|
|
|
3
|
-
A highly customizable, feature-rich chat UI for **React Native**. Drop in a single `ChatScreen` component to get message bubbles, media
|
|
3
|
+
A highly customizable, feature-rich chat UI for **React Native**. Drop in a single `ChatScreen` component to get message bubbles, WhatsApp-style media grids, audio/video playback, a full-screen swipe gallery, typing indicators, file attachment previews, and a smart input bar — with deep theming and custom component hooks.
|
|
4
4
|
|
|
5
5
|
**npm:** [`movius-chats`](https://www.npmjs.com/package/movius-chats)
|
|
6
6
|
**Repository:** [github.com/David-Atueyi/Movius-Chats](https://github.com/David-Atueyi/Movius-Chats)
|
|
@@ -9,57 +9,58 @@ A highly customizable, feature-rich chat UI for **React Native**. Drop in a sing
|
|
|
9
9
|
|
|
10
10
|
## Table of contents
|
|
11
11
|
|
|
12
|
-
- [Requirements](#requirements)
|
|
13
|
-
- [Important: native modules & Expo](#important-native-modules--expo)
|
|
12
|
+
- [Requirements & compatibility](#requirements--compatibility)
|
|
14
13
|
- [Installation](#installation)
|
|
15
14
|
- [Quick start](#quick-start)
|
|
16
|
-
- [
|
|
15
|
+
- [Data model](#data-model)
|
|
16
|
+
- [Message](#message)
|
|
17
|
+
- [MessageMediaItem](#messagemediaitem)
|
|
18
|
+
- [MessageFileAttachment](#messagefileattachment)
|
|
19
|
+
- [PreviewAttachment](#previewattachment)
|
|
17
20
|
- [Message list ordering](#message-list-ordering)
|
|
18
21
|
- [ChatScreen API](#chatscreen-api)
|
|
19
22
|
- [Core props](#core-props)
|
|
20
23
|
- [Feature flags](#feature-flags)
|
|
21
24
|
- [Input & typing](#input--typing)
|
|
22
|
-
- [Attachment preview](#attachment-preview)
|
|
23
|
-
- [
|
|
25
|
+
- [Attachment preview (composer)](#attachment-preview-composer)
|
|
26
|
+
- [Theme](#theme)
|
|
24
27
|
- [Custom components & icons](#custom-components--icons)
|
|
25
28
|
- [Usage examples](#usage-examples)
|
|
26
|
-
- [
|
|
27
|
-
- [
|
|
29
|
+
- [Basic text chat](#basic-text-chat)
|
|
30
|
+
- [Multi-image / video album bubble](#multi-image--video-album-bubble)
|
|
31
|
+
- [File attachment bubble](#file-attachment-bubble)
|
|
32
|
+
- [Audio message bubble](#audio-message-bubble)
|
|
33
|
+
- [Composer attachment preview (single or multiple)](#composer-attachment-preview-single-or-multiple)
|
|
34
|
+
- [Send button vs microphone](#send-button-vs-microphone)
|
|
28
35
|
- [Typing indicators](#typing-indicators)
|
|
29
|
-
- [Attachments & camera (parent-controlled)](#attachments--camera-parent-controlled)
|
|
30
|
-
- [Attachment preview before send](#attachment-preview-before-send)
|
|
31
36
|
- [Custom theme](#custom-theme)
|
|
37
|
+
- [Font family (all text)](#font-family-all-text)
|
|
38
|
+
- [Keyboard avoiding](#keyboard-avoiding)
|
|
32
39
|
- [Custom input bar](#custom-input-bar)
|
|
33
|
-
- [Long-press
|
|
34
|
-
- [
|
|
40
|
+
- [Long-press on a message](#long-press-on-a-message)
|
|
41
|
+
- [Full-screen gallery viewer](#full-screen-gallery-viewer)
|
|
35
42
|
- [Architecture overview](#architecture-overview)
|
|
43
|
+
- [TypeScript types](#typescript-types)
|
|
36
44
|
- [Troubleshooting](#troubleshooting)
|
|
37
|
-
- [
|
|
45
|
+
- [Publishing a new version](#publishing-a-new-version)
|
|
38
46
|
- [License](#license)
|
|
39
47
|
|
|
40
48
|
---
|
|
41
49
|
|
|
42
|
-
## Requirements
|
|
50
|
+
## Requirements & compatibility
|
|
43
51
|
|
|
44
52
|
| Dependency | Role |
|
|
45
53
|
|------------|------|
|
|
46
54
|
| `react` ≥ 16.8 | Peer dependency |
|
|
47
55
|
| `react-native` | Peer dependency |
|
|
48
|
-
| `react-native-reanimated` | Audio scrubber
|
|
49
|
-
| `react-native-image-zoom-viewer` | Full-screen image viewer |
|
|
50
|
-
| `react-native-parsed-text` | Clickable URLs in messages |
|
|
56
|
+
| `react-native-reanimated` | Audio scrubber animation (peer) |
|
|
51
57
|
| `react-native-sound` | Voice message playback |
|
|
52
58
|
| `react-native-svg` | Built-in icons |
|
|
53
|
-
| `react-native-video` | Video
|
|
54
|
-
| `
|
|
59
|
+
| `react-native-video` | Video thumbnails + full-screen video |
|
|
60
|
+
| `react-native-parsed-text` | Tappable URLs in text messages |
|
|
61
|
+
| `twrnc` | Internal Tailwind-style utilities |
|
|
55
62
|
|
|
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
|
+
**Not compatible with Expo Go** — native modules require a development build or bare RN project.
|
|
63
64
|
|
|
64
65
|
---
|
|
65
66
|
|
|
@@ -72,55 +73,30 @@ npm install movius-chats
|
|
|
72
73
|
# or
|
|
73
74
|
yarn add movius-chats
|
|
74
75
|
# or
|
|
75
|
-
bun
|
|
76
|
+
bun add movius-chats
|
|
76
77
|
```
|
|
77
78
|
|
|
78
|
-
### 2. Install peer
|
|
79
|
-
|
|
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):
|
|
79
|
+
### 2. Install peer / native dependencies
|
|
81
80
|
|
|
82
81
|
```bash
|
|
83
|
-
npm install react-native-reanimated react-native-
|
|
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
|
|
82
|
+
npm install react-native-reanimated react-native-sound react-native-svg react-native-video twrnc
|
|
88
83
|
```
|
|
89
84
|
|
|
90
|
-
> `react-native-parsed-text` is pulled in transitively; no extra install step unless your bundler requires it.
|
|
91
|
-
|
|
92
85
|
### 3. Configure Reanimated
|
|
93
86
|
|
|
94
|
-
Add the Reanimated
|
|
87
|
+
Add the Reanimated plugin **last** in `babel.config.js`:
|
|
95
88
|
|
|
96
|
-
```
|
|
89
|
+
```js
|
|
97
90
|
module.exports = {
|
|
98
91
|
presets: ['module:metro-react-native-babel-preset'],
|
|
99
92
|
plugins: [
|
|
100
|
-
//
|
|
93
|
+
// other plugins
|
|
101
94
|
'react-native-reanimated/plugin',
|
|
102
95
|
],
|
|
103
96
|
};
|
|
104
97
|
```
|
|
105
98
|
|
|
106
|
-
### 4.
|
|
107
|
-
|
|
108
|
-
**iOS** — enable playback in silent mode (optional, in `AppDelegate`):
|
|
109
|
-
|
|
110
|
-
```objc
|
|
111
|
-
#import <AVFoundation/AVFoundation.h>
|
|
112
|
-
|
|
113
|
-
// inside didFinishLaunchingWithOptions:
|
|
114
|
-
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
**Android** — ensure internet permission if loading remote audio URLs in `AndroidManifest.xml`:
|
|
118
|
-
|
|
119
|
-
```xml
|
|
120
|
-
<uses-permission android:name="android.permission.INTERNET" />
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### 5. Rebuild the native app
|
|
99
|
+
### 4. Rebuild native code
|
|
124
100
|
|
|
125
101
|
**React Native CLI:**
|
|
126
102
|
|
|
@@ -130,19 +106,21 @@ npx react-native run-ios
|
|
|
130
106
|
npx react-native run-android
|
|
131
107
|
```
|
|
132
108
|
|
|
133
|
-
**Expo
|
|
109
|
+
**Expo development build:**
|
|
134
110
|
|
|
135
111
|
```bash
|
|
136
112
|
npx expo prebuild
|
|
137
|
-
npx expo run:ios
|
|
138
|
-
# or
|
|
139
|
-
npx expo run:android
|
|
113
|
+
npx expo run:ios # or run:android
|
|
140
114
|
```
|
|
141
115
|
|
|
142
|
-
|
|
116
|
+
### 5. Android keyboard mode (Expo)
|
|
143
117
|
|
|
144
|
-
|
|
145
|
-
|
|
118
|
+
In `app.json` / `app.config.js` add:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
"android": {
|
|
122
|
+
"softwareKeyboardLayoutMode": "resize"
|
|
123
|
+
}
|
|
146
124
|
```
|
|
147
125
|
|
|
148
126
|
---
|
|
@@ -151,45 +129,46 @@ npx expo prebuild --clean
|
|
|
151
129
|
|
|
152
130
|
```tsx
|
|
153
131
|
import React, { useState } from 'react';
|
|
154
|
-
import { SafeAreaView } from 'react-native';
|
|
132
|
+
import { Platform, SafeAreaView, View } from 'react-native';
|
|
155
133
|
import ChatScreen from 'movius-chats';
|
|
156
134
|
import type { Message } from 'movius-chats/lib/typescript/types';
|
|
135
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
157
136
|
|
|
158
|
-
export default function
|
|
159
|
-
const
|
|
137
|
+
export default function MyChatScreen() {
|
|
138
|
+
const insets = useSafeAreaInsets();
|
|
160
139
|
const currentUserId = 'user-1';
|
|
161
|
-
|
|
162
|
-
const handleSendMessage = (
|
|
163
|
-
payload: Omit<Message, 'id' | 'time' | 'status'>
|
|
164
|
-
) => {
|
|
165
|
-
const newMessage: Message = {
|
|
166
|
-
...payload,
|
|
167
|
-
id: String(Date.now()),
|
|
168
|
-
time: new Date().toLocaleTimeString([], {
|
|
169
|
-
hour: '2-digit',
|
|
170
|
-
minute: '2-digit',
|
|
171
|
-
}),
|
|
172
|
-
status: 'sent',
|
|
173
|
-
};
|
|
174
|
-
// Newest first — see "Message list ordering"
|
|
175
|
-
setMessages((prev) => [newMessage, ...prev]);
|
|
176
|
-
};
|
|
140
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
177
141
|
|
|
178
142
|
return (
|
|
179
143
|
<SafeAreaView style={{ flex: 1 }}>
|
|
180
|
-
<
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
144
|
+
<View style={{ flex: 1 }}>
|
|
145
|
+
<ChatScreen
|
|
146
|
+
messages={messages}
|
|
147
|
+
currentUserId={currentUserId}
|
|
148
|
+
onSendMessage={({ text, senderId }) => {
|
|
149
|
+
setMessages((prev) => [
|
|
150
|
+
{
|
|
151
|
+
id: String(Date.now()),
|
|
152
|
+
text,
|
|
153
|
+
senderId,
|
|
154
|
+
time: new Date().toLocaleTimeString([], {
|
|
155
|
+
hour: '2-digit',
|
|
156
|
+
minute: '2-digit',
|
|
157
|
+
}),
|
|
158
|
+
status: 'sent',
|
|
159
|
+
},
|
|
160
|
+
...prev,
|
|
161
|
+
]);
|
|
162
|
+
}}
|
|
163
|
+
keyboardVerticalOffset={Platform.OS === 'ios' ? insets.top + 44 : 0}
|
|
164
|
+
showAvatars
|
|
165
|
+
showBubbleTail
|
|
166
|
+
showMessageStatus
|
|
167
|
+
showAttachmentsButton
|
|
168
|
+
showCameraButton
|
|
169
|
+
showVoiceRecordButton
|
|
170
|
+
/>
|
|
171
|
+
</View>
|
|
193
172
|
</SafeAreaView>
|
|
194
173
|
);
|
|
195
174
|
}
|
|
@@ -197,125 +176,147 @@ export default function App() {
|
|
|
197
176
|
|
|
198
177
|
---
|
|
199
178
|
|
|
200
|
-
##
|
|
179
|
+
## Data model
|
|
180
|
+
|
|
181
|
+
### Message
|
|
201
182
|
|
|
202
|
-
|
|
183
|
+
Every item in the `messages` array must match this shape:
|
|
203
184
|
|
|
204
185
|
| Field | Type | Required | Description |
|
|
205
186
|
|-------|------|----------|-------------|
|
|
206
187
|
| `id` | `string` | Yes | Unique message id |
|
|
207
188
|
| `senderId` | `string` | Yes | User id of the sender |
|
|
208
|
-
| `time` | `string` | Yes | Display time (e.g. `"2:30 PM"`)
|
|
209
|
-
| `status` | `'sent' \| 'delivered' \| 'read'` | Yes |
|
|
210
|
-
| `text` | `string` | No | Plain text; URLs
|
|
211
|
-
| `
|
|
212
|
-
| `
|
|
213
|
-
| `
|
|
189
|
+
| `time` | `string` | Yes | Display time string — you format this (e.g. `"2:30 PM"`) |
|
|
190
|
+
| `status` | `'sent' \| 'delivered' \| 'read'` | Yes | Shown only for current user's messages |
|
|
191
|
+
| `text` | `string` | No | Plain text; URLs auto-linked |
|
|
192
|
+
| `audio` | `string` | No | Audio file URI — renders a playable audio bubble |
|
|
193
|
+
| `image` | `string` | No | Single image URI (legacy; prefer `mediaItems`) |
|
|
194
|
+
| `video` | `string` | No | Single video URI (legacy; prefer `mediaItems`) |
|
|
195
|
+
| `mediaItems` | `MessageMediaItem[]` | No | Album of images/videos — renders a WhatsApp-style grid |
|
|
196
|
+
| `fileAttachments` | `MessageFileAttachment[]` | No | PDFs, docs, etc. — rendered as tappable file rows |
|
|
214
197
|
| `senderName` | `string` | No | Shown when `showUserNames` is true |
|
|
215
198
|
| `senderAvatar` | `string` | No | Avatar image URI; falls back to first letter of `senderName` |
|
|
216
199
|
|
|
217
|
-
A message can combine fields (e.g. text +
|
|
200
|
+
A message can combine fields (e.g. `text` + `mediaItems`).
|
|
218
201
|
|
|
219
|
-
|
|
220
|
-
import type { Message } from 'movius-chats/lib/typescript/types';
|
|
202
|
+
### MessageMediaItem
|
|
221
203
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
204
|
+
```ts
|
|
205
|
+
interface MessageMediaItem {
|
|
206
|
+
uri: string;
|
|
207
|
+
kind: 'image' | 'video';
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Used in `message.mediaItems` for multi-media bubbles.
|
|
212
|
+
|
|
213
|
+
### MessageFileAttachment
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
interface MessageFileAttachment {
|
|
217
|
+
uri: string;
|
|
218
|
+
type: string; // MIME type, e.g. "application/pdf"
|
|
219
|
+
name: string;
|
|
220
|
+
}
|
|
231
221
|
```
|
|
232
222
|
|
|
223
|
+
Each attachment is shown as a tappable chip that opens the file via `Linking.openURL`.
|
|
224
|
+
|
|
225
|
+
### PreviewAttachment
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
interface PreviewAttachment {
|
|
229
|
+
uri: string;
|
|
230
|
+
type: string; // MIME type
|
|
231
|
+
name?: string;
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Used in `previewData` / `previewItems` for the composer preview strip above the input.
|
|
236
|
+
|
|
233
237
|
---
|
|
234
238
|
|
|
235
239
|
## Message list ordering
|
|
236
240
|
|
|
237
|
-
`ChatScreen` uses an **inverted** `FlatList`.
|
|
241
|
+
`ChatScreen` uses an **inverted** `FlatList`. The **newest message must be at index `0`**:
|
|
238
242
|
|
|
239
|
-
```
|
|
240
|
-
setMessages((prev) => [newMessage, ...prev]); //
|
|
243
|
+
```ts
|
|
244
|
+
setMessages((prev) => [newMessage, ...prev]); // correct
|
|
241
245
|
```
|
|
242
246
|
|
|
243
|
-
|
|
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`).
|
|
247
|
+
Consecutive messages from the same sender are grouped — avatars and tails appear only on the first message in each group.
|
|
246
248
|
|
|
247
249
|
---
|
|
248
250
|
|
|
249
251
|
## ChatScreen API
|
|
250
252
|
|
|
251
|
-
`ChatScreen` is the **default export** from `movius-chats`.
|
|
253
|
+
`ChatScreen` is the **default export** from `movius-chats`.
|
|
252
254
|
|
|
253
255
|
### Core props
|
|
254
256
|
|
|
255
257
|
| Prop | Type | Required | Description |
|
|
256
258
|
|------|------|----------|-------------|
|
|
257
|
-
| `messages` | `Message[]` | Yes |
|
|
258
|
-
| `currentUserId` | `string` | Yes | Logged-in user id
|
|
259
|
-
| `onSendMessage` | `(msg: Omit<Message, 'id' \| 'time' \| 'status'>) => void` | Yes |
|
|
260
|
-
| `onMessageLongPress` | `(message: Message) => void` | No |
|
|
261
|
-
| `placeholder` | `string` | No | Input placeholder (default: `"Message"`) |
|
|
262
|
-
| `keyboardVerticalOffset` | `number` | No | **iOS only
|
|
263
|
-
| `disableKeyboardAvoiding` | `boolean` | No | Set `true` if your screen already handles
|
|
259
|
+
| `messages` | `Message[]` | Yes | Message array, newest first |
|
|
260
|
+
| `currentUserId` | `string` | Yes | Logged-in user id — controls bubble alignment and status icons |
|
|
261
|
+
| `onSendMessage` | `(msg: Omit<Message, 'id' \| 'time' \| 'status'>) => void` | Yes | Called when the user taps send |
|
|
262
|
+
| `onMessageLongPress` | `(message: Message) => void` | No | Called on long-press of a bubble |
|
|
263
|
+
| `placeholder` | `string` | No | Input placeholder text (default: `"Message"`) |
|
|
264
|
+
| `keyboardVerticalOffset` | `number` | No | **iOS only** — header + status bar height offset for `KeyboardAvoidingView`. Android lifts the input by the full keyboard height automatically. Default: `0` |
|
|
265
|
+
| `disableKeyboardAvoiding` | `boolean` | No | Set `true` if your screen already handles keyboard avoidance |
|
|
264
266
|
|
|
265
267
|
### Feature flags
|
|
266
268
|
|
|
267
|
-
All
|
|
269
|
+
All default to `false` (hidden) unless explicitly set to `true`:
|
|
268
270
|
|
|
269
271
|
| Prop | Description |
|
|
270
272
|
|------|-------------|
|
|
271
|
-
| `showAvatars` | Avatar (or initial) on received
|
|
273
|
+
| `showAvatars` | Avatar (or initial letter) on received bubbles and typing row |
|
|
272
274
|
| `showUserNames` | Sender name above received bubbles |
|
|
273
|
-
| `showBubbleTail` | WhatsApp-style tail on first bubble in a sequence |
|
|
274
|
-
| `showMessageStatus` | Timestamp +
|
|
275
|
-
| `showEmojiButton` | Emoji button in input (UI only
|
|
276
|
-
| `showAttachmentsButton` | Paperclip
|
|
277
|
-
| `showCameraButton` | Camera icon when input is empty
|
|
278
|
-
| `showVoiceRecordButton` | Mic when
|
|
275
|
+
| `showBubbleTail` | WhatsApp-style corner tail on first bubble in a sequence |
|
|
276
|
+
| `showMessageStatus` | Timestamp + checkmark icons on sent messages |
|
|
277
|
+
| `showEmojiButton` | Emoji button in the input bar (UI only — wire your own picker via `CustomEmojiIcon`) |
|
|
278
|
+
| `showAttachmentsButton` | Paperclip icon — triggers `onAttachmentPress` |
|
|
279
|
+
| `showCameraButton` | Camera icon when input is empty — triggers `onCameraPress` |
|
|
280
|
+
| `showVoiceRecordButton` | Mic icon when there is no text and no preview; becomes send icon otherwise |
|
|
279
281
|
|
|
280
282
|
### Input & typing
|
|
281
283
|
|
|
282
284
|
| Prop | Type | Description |
|
|
283
285
|
|------|------|-------------|
|
|
284
|
-
| `onTypingStart` | `() => void` | Called when input
|
|
285
|
-
| `onTypingEnd` | `() => void` | Called when input is cleared |
|
|
286
|
-
| `onAttachmentPress` | `() => void` |
|
|
287
|
-
| `onCameraPress` | `() => void` |
|
|
288
|
-
| `onAudioRecordStart` | `() => void` | Mic
|
|
289
|
-
| `onAudioRecordEnd` | `() => void` | Mic
|
|
290
|
-
| `typingUsers` | `
|
|
286
|
+
| `onTypingStart` | `() => void` | Called when the text input becomes non-empty |
|
|
287
|
+
| `onTypingEnd` | `() => void` | Called when the text input is cleared |
|
|
288
|
+
| `onAttachmentPress` | `() => void` | Paperclip tapped — open your file/image picker |
|
|
289
|
+
| `onCameraPress` | `() => void` | Camera icon tapped — open camera |
|
|
290
|
+
| `onAudioRecordStart` | `() => void` | Mic pressed / long-pressed — start recording |
|
|
291
|
+
| `onAudioRecordEnd` | `() => void` | Mic released — stop recording, upload, add message |
|
|
292
|
+
| `typingUsers` | `{ id: string; avatar: string; name: string }[]` | Users currently typing (current user is excluded from display) |
|
|
291
293
|
|
|
292
|
-
|
|
294
|
+
### Attachment preview (composer)
|
|
293
295
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
Show a file/image/video preview above the input before sending:
|
|
296
|
+
Show a preview strip above the input before the user taps send.
|
|
297
297
|
|
|
298
298
|
| Prop | Type | Description |
|
|
299
299
|
|------|------|-------------|
|
|
300
|
-
| `
|
|
301
|
-
| `
|
|
300
|
+
| `previewItems` | `PreviewAttachment[]` | **Multiple** attachments — images/videos shown as a fanned spread; documents shown as file chips |
|
|
301
|
+
| `previewData` | `PreviewAttachment` | **Single** attachment (kept for backward compatibility; `previewItems` takes precedence) |
|
|
302
|
+
| `closePreview` | `() => void` | Called when the user taps the × on the preview |
|
|
302
303
|
|
|
303
|
-
When
|
|
304
|
+
When any preview is present, the send button appears regardless of text content.
|
|
304
305
|
|
|
305
|
-
###
|
|
306
|
+
### Theme
|
|
306
307
|
|
|
307
|
-
Pass a `theme` object to
|
|
308
|
+
All keys are optional. Pass a `theme` object to `ChatScreen`:
|
|
308
309
|
|
|
309
|
-
```
|
|
310
|
+
```ts
|
|
310
311
|
theme?: {
|
|
311
|
-
fontFamily?: string;
|
|
312
|
+
fontFamily?: string; // applied to every Text element in the package
|
|
312
313
|
|
|
313
314
|
colors?: {
|
|
314
315
|
sentMessageTailColor?: string;
|
|
315
316
|
receivedMessageTailColor?: string;
|
|
316
317
|
timestamp?: string;
|
|
317
|
-
inputsIconsColor?: string;
|
|
318
|
-
sendIconsColor?: string;
|
|
318
|
+
inputsIconsColor?: string; // emoji, clip, camera icons
|
|
319
|
+
sendIconsColor?: string; // send / mic icons
|
|
319
320
|
placeholderTextColor?: string;
|
|
320
321
|
inputTextColor?: string;
|
|
321
322
|
audioPlayIconColor?: string;
|
|
@@ -326,10 +327,10 @@ theme?: {
|
|
|
326
327
|
readIconColor?: string;
|
|
327
328
|
};
|
|
328
329
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
330
|
+
sizes?: {
|
|
331
|
+
// Applies only to emoji, paperclip, and camera icons (not send/mic)
|
|
332
|
+
inputIconSize?: string | number; // number = px, string = twrnc class e.g. "h-8 w-8"
|
|
333
|
+
};
|
|
333
334
|
|
|
334
335
|
bubbleStyle?: {
|
|
335
336
|
sent?: ViewStyle;
|
|
@@ -353,9 +354,9 @@ theme?: {
|
|
|
353
354
|
};
|
|
354
355
|
|
|
355
356
|
inputStyles?: {
|
|
356
|
-
inputSectionContainerStyle?: ViewStyle;
|
|
357
|
-
inputContainerStyle?: ViewStyle;
|
|
358
|
-
sendButtonStyle?: ViewStyle;
|
|
357
|
+
inputSectionContainerStyle?: ViewStyle; // outer row (input + send button)
|
|
358
|
+
inputContainerStyle?: ViewStyle; // the pill/rounded box
|
|
359
|
+
sendButtonStyle?: ViewStyle; // the round send/mic button
|
|
359
360
|
};
|
|
360
361
|
|
|
361
362
|
filePreviewStyle?: {
|
|
@@ -368,282 +369,336 @@ theme?: {
|
|
|
368
369
|
}
|
|
369
370
|
```
|
|
370
371
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
#### Custom font (`theme.fontFamily`)
|
|
374
|
-
|
|
375
|
-
`fontFamily` applies to **all text** in the package (messages, timestamps, typing label, input, file names, errors).
|
|
376
|
-
|
|
377
|
-
**You must load the font in your app first** — the library only sets the `fontFamily` style name.
|
|
378
|
-
|
|
379
|
-
**Expo:**
|
|
380
|
-
|
|
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;
|
|
372
|
+
### Custom components & icons
|
|
390
373
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
374
|
+
| Prop | Type | Description |
|
|
375
|
+
|------|------|-------------|
|
|
376
|
+
| `renderCustomInput` | `() => React.ReactNode` | Replace the entire input bar |
|
|
377
|
+
| `renderCustomTyping` | `() => React.ReactNode` | Replace the typing bubble content |
|
|
378
|
+
| `renderCustomVideoBubbleError` | `() => React.ReactNode` | Replace the inline video error state |
|
|
379
|
+
| `CustomEmojiIcon` | `() => React.ReactNode` | Emoji button icon |
|
|
380
|
+
| `CustomAttachmentIcon` | `() => React.ReactNode` | Paperclip icon |
|
|
381
|
+
| `CustomCameraIcon` | `() => React.ReactNode` | Camera icon |
|
|
382
|
+
| `CustomSendIcon` | `() => React.ReactNode` | Send button icon |
|
|
383
|
+
| `CustomMicrophoneIcon` | `() => React.ReactNode` | Microphone icon |
|
|
384
|
+
| `CustomPlayIcon` | `() => React.ReactNode` | Play icon in audio and video |
|
|
385
|
+
| `CustomPauseIcon` | `() => React.ReactNode` | Pause icon in audio player |
|
|
386
|
+
| `CustomFileIcon` | `React.ComponentType<{ style?: any }>` | Icon inside document file chips |
|
|
387
|
+
| `CustomImagePreview` | `React.ComponentType<{ uri: string }>` | Replaces the composer image thumbnail |
|
|
388
|
+
| `CustomVideoPreview` | `React.ComponentType<{ uri: string }>` | Replaces the composer video thumbnail |
|
|
399
389
|
|
|
400
|
-
|
|
390
|
+
---
|
|
401
391
|
|
|
402
|
-
|
|
392
|
+
## Usage examples
|
|
403
393
|
|
|
404
|
-
|
|
394
|
+
### Basic text chat
|
|
405
395
|
|
|
406
396
|
```tsx
|
|
407
397
|
<ChatScreen
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
398
|
+
messages={messages}
|
|
399
|
+
currentUserId="user-1"
|
|
400
|
+
onSendMessage={({ text, senderId }) => {
|
|
401
|
+
setMessages((prev) => [
|
|
402
|
+
{
|
|
403
|
+
id: String(Date.now()),
|
|
404
|
+
text,
|
|
405
|
+
senderId,
|
|
406
|
+
time: '10:00 AM',
|
|
407
|
+
status: 'sent',
|
|
408
|
+
},
|
|
409
|
+
...prev,
|
|
410
|
+
]);
|
|
413
411
|
}}
|
|
414
412
|
/>
|
|
415
413
|
```
|
|
416
414
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
The package lifts the chat when the keyboard opens (keyboard listeners + `KeyboardAvoidingView` on iOS).
|
|
415
|
+
### Multi-image / video album bubble
|
|
420
416
|
|
|
421
|
-
|
|
422
|
-
2. Wrap `ChatScreen` in `<View style={{ flex: 1 }}>` and give the screen root `flex: 1`.
|
|
423
|
-
3. **iOS only:** set `keyboardVerticalOffset` to header + status bar (e.g. `insets.top + 44`). **Android:** omit or use `0` — the package lifts the input by the full keyboard height.
|
|
417
|
+
Use `mediaItems` in the `Message` object. The bubble renders a WhatsApp-style grid:
|
|
424
418
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
<ChatScreen
|
|
432
|
-
keyboardVerticalOffset={Platform.OS === 'ios' ? insets.top + 44 : 0}
|
|
433
|
-
// ...
|
|
434
|
-
/>
|
|
435
|
-
</View>
|
|
436
|
-
```
|
|
419
|
+
| Count | Layout |
|
|
420
|
+
|-------|--------|
|
|
421
|
+
| 1 | Single full-width tile (cover) |
|
|
422
|
+
| 2 | Side by side |
|
|
423
|
+
| 3 | One on top, two below |
|
|
424
|
+
| 4+ | 2 × 2 grid; bottom-right cell shows `+N` overlay |
|
|
437
425
|
|
|
438
|
-
|
|
426
|
+
Tapping any cell opens the full-screen swipe gallery.
|
|
439
427
|
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
428
|
+
```tsx
|
|
429
|
+
const message: Message = {
|
|
430
|
+
id: '1',
|
|
431
|
+
senderId: 'user-2',
|
|
432
|
+
time: '11:00 AM',
|
|
433
|
+
status: 'read',
|
|
434
|
+
mediaItems: [
|
|
435
|
+
{ uri: 'https://example.com/photo1.jpg', kind: 'image' },
|
|
436
|
+
{ uri: 'https://example.com/photo2.jpg', kind: 'image' },
|
|
437
|
+
{ uri: 'https://example.com/clip.mp4', kind: 'video' },
|
|
438
|
+
],
|
|
439
|
+
};
|
|
444
440
|
```
|
|
445
441
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
### Custom components & icons
|
|
449
|
-
|
|
450
|
-
| Prop | Type | Description |
|
|
451
|
-
|------|------|-------------|
|
|
452
|
-
| `renderCustomInput` | `() => React.ReactNode` | Replace entire input bar |
|
|
453
|
-
| `renderCustomTyping` | `() => React.ReactNode` | Replace typing bubble content |
|
|
454
|
-
| `renderCustomVideoBubbleError` | `() => React.ReactNode` | Replace inline video error UI |
|
|
455
|
-
| `CustomEmojiIcon` | `() => React.ReactNode` | Emoji button |
|
|
456
|
-
| `CustomAttachmentIcon` | `() => React.ReactNode` | Attachment button |
|
|
457
|
-
| `CustomCameraIcon` | `() => React.ReactNode` | Camera button |
|
|
458
|
-
| `CustomSendIcon` | `() => React.ReactNode` | Send button |
|
|
459
|
-
| `CustomMicrophoneIcon` | `() => React.ReactNode` | Microphone button |
|
|
460
|
-
| `CustomPlayIcon` | `() => React.ReactNode` | Play icon in video/audio bubbles |
|
|
461
|
-
| `CustomPauseIcon` | `() => React.ReactNode` | Pause icon in audio player |
|
|
462
|
-
| `CustomFileIcon` | `React.ComponentType<{ style?: any }>` | Generic file preview icon |
|
|
463
|
-
| `CustomImagePreview` | `React.ComponentType<{ uri: string }>` | Image attachment preview |
|
|
464
|
-
| `CustomVideoPreview` | `React.ComponentType<{ uri: string }>` | Video attachment preview |
|
|
465
|
-
|
|
466
|
-
---
|
|
467
|
-
|
|
468
|
-
## Usage examples
|
|
469
|
-
|
|
470
|
-
### Text messages
|
|
442
|
+
Legacy single-item syntax still works and is merged automatically:
|
|
471
443
|
|
|
472
444
|
```tsx
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}}
|
|
445
|
+
// These are equivalent for display purposes
|
|
446
|
+
{ image: 'https://...' }
|
|
447
|
+
{ mediaItems: [{ uri: 'https://...', kind: 'image' }] }
|
|
476
448
|
```
|
|
477
449
|
|
|
478
|
-
###
|
|
479
|
-
|
|
480
|
-
Add URIs when building the `Message` object (usually after upload):
|
|
450
|
+
### File attachment bubble
|
|
481
451
|
|
|
482
452
|
```tsx
|
|
483
|
-
const
|
|
453
|
+
const message: Message = {
|
|
484
454
|
id: '2',
|
|
485
|
-
senderId:
|
|
486
|
-
|
|
487
|
-
time: '11:00 AM',
|
|
455
|
+
senderId: 'user-1',
|
|
456
|
+
time: '11:05 AM',
|
|
488
457
|
status: 'delivered',
|
|
458
|
+
fileAttachments: [
|
|
459
|
+
{
|
|
460
|
+
uri: 'https://example.com/report.pdf',
|
|
461
|
+
type: 'application/pdf',
|
|
462
|
+
name: 'Q2 Report.pdf',
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
text: 'Here is the report',
|
|
489
466
|
};
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
Each attachment renders as a tappable row. Tapping it calls `Linking.openURL(uri)`.
|
|
490
470
|
|
|
491
|
-
|
|
471
|
+
### Audio message bubble
|
|
472
|
+
|
|
473
|
+
```tsx
|
|
474
|
+
const message: Message = {
|
|
492
475
|
id: '3',
|
|
493
476
|
senderId: 'user-2',
|
|
494
477
|
audio: 'file:///path/to/recording.m4a',
|
|
495
|
-
time: '11:
|
|
478
|
+
time: '11:10 AM',
|
|
496
479
|
status: 'read',
|
|
497
480
|
};
|
|
498
481
|
```
|
|
499
482
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
### Typing indicators
|
|
503
|
-
|
|
504
|
-
```tsx
|
|
505
|
-
const [typingUsers, setTypingUsers] = useState<
|
|
506
|
-
{ id: string; avatar: string; name: string }[]
|
|
507
|
-
>([]);
|
|
508
|
-
|
|
509
|
-
<ChatScreen
|
|
510
|
-
typingUsers={typingUsers}
|
|
511
|
-
onTypingStart={() => notifyServer('typing-start')}
|
|
512
|
-
onTypingEnd={() => notifyServer('typing-end')}
|
|
513
|
-
// ...
|
|
514
|
-
/>
|
|
515
|
-
```
|
|
483
|
+
The audio player has a scrubable progress bar, a play/pause button, and a duration counter.
|
|
484
|
+
Only one audio message plays at a time — starting a new one automatically stops the previous one.
|
|
516
485
|
|
|
517
|
-
|
|
486
|
+
### Composer attachment preview (single or multiple)
|
|
518
487
|
|
|
519
|
-
|
|
488
|
+
Use `previewItems` (array) for multi-select, or `previewData` (single) for back-compat:
|
|
520
489
|
|
|
521
490
|
```tsx
|
|
522
|
-
|
|
491
|
+
const [previews, setPreviews] = useState<PreviewAttachment[]>([]);
|
|
523
492
|
|
|
524
493
|
<ChatScreen
|
|
525
|
-
|
|
526
|
-
|
|
494
|
+
previewItems={previews}
|
|
495
|
+
closePreview={() => setPreviews([])}
|
|
527
496
|
onAttachmentPress={async () => {
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}}
|
|
533
|
-
onAudioRecordStart={() => {
|
|
534
|
-
// start native recorder
|
|
497
|
+
const picked = await myPicker.pick(); // returns an array
|
|
498
|
+
setPreviews(
|
|
499
|
+
picked.map((f) => ({ uri: f.uri, type: f.type, name: f.name }))
|
|
500
|
+
);
|
|
535
501
|
}}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
502
|
+
onSendMessage={({ text, senderId }) => {
|
|
503
|
+
const media = previews
|
|
504
|
+
.filter((p) => p.type.startsWith('image/') || p.type.startsWith('video/'))
|
|
505
|
+
.map((p) => ({
|
|
506
|
+
uri: p.uri,
|
|
507
|
+
kind: p.type.startsWith('video/') ? 'video' : 'image',
|
|
508
|
+
} as MessageMediaItem));
|
|
509
|
+
|
|
510
|
+
const files = previews
|
|
511
|
+
.filter((p) => !p.type.startsWith('image/') && !p.type.startsWith('video/'))
|
|
512
|
+
.map((p) => ({ uri: p.uri, type: p.type, name: p.name ?? 'file' }));
|
|
513
|
+
|
|
514
|
+
setMessages((prev) => [
|
|
515
|
+
{
|
|
516
|
+
id: String(Date.now()),
|
|
517
|
+
senderId,
|
|
518
|
+
time: '...',
|
|
519
|
+
status: 'sent',
|
|
520
|
+
text: text || undefined,
|
|
521
|
+
mediaItems: media.length ? media : undefined,
|
|
522
|
+
fileAttachments: files.length ? files : undefined,
|
|
523
|
+
},
|
|
524
|
+
...prev,
|
|
525
|
+
]);
|
|
526
|
+
|
|
527
|
+
setPreviews([]);
|
|
539
528
|
}}
|
|
540
529
|
/>
|
|
541
530
|
```
|
|
542
531
|
|
|
543
|
-
|
|
532
|
+
Preview UI:
|
|
533
|
+
- **1 image/video** — single thumbnail.
|
|
534
|
+
- **2–3 images/videos** — overlapping fan spread.
|
|
535
|
+
- **4+ images/videos** — fan of 3 with a `+N` badge.
|
|
536
|
+
- **Documents** — file chip with name + icon.
|
|
537
|
+
|
|
538
|
+
### Send button vs microphone
|
|
539
|
+
|
|
540
|
+
The send button (green circle, right of input) shows:
|
|
541
|
+
|
|
542
|
+
| Condition | Icon shown |
|
|
543
|
+
|-----------|------------|
|
|
544
|
+
| Input has text | Send icon |
|
|
545
|
+
| `previewItems` / `previewData` is set | Send icon |
|
|
546
|
+
| Neither, `showVoiceRecordButton` true | Microphone icon |
|
|
547
|
+
| Neither, `showVoiceRecordButton` false | Send icon |
|
|
548
|
+
|
|
549
|
+
No extra work needed — the package handles this automatically.
|
|
550
|
+
|
|
551
|
+
### Typing indicators
|
|
544
552
|
|
|
545
553
|
```tsx
|
|
546
|
-
const [
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
} | null>(null);
|
|
554
|
+
const [typingUsers, setTypingUsers] = useState([]);
|
|
555
|
+
|
|
556
|
+
// When your socket receives "user X is typing":
|
|
557
|
+
setTypingUsers([{ id: 'user-2', avatar: 'https://...', name: 'Alex' }]);
|
|
551
558
|
|
|
552
559
|
<ChatScreen
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
const asset = await pickDocument();
|
|
557
|
-
setPreviewData({
|
|
558
|
-
uri: asset.uri,
|
|
559
|
-
type: asset.type ?? 'application/octet-stream',
|
|
560
|
-
name: asset.name ?? 'file',
|
|
561
|
-
});
|
|
562
|
-
}}
|
|
563
|
-
onSendMessage={({ text, senderId }) => {
|
|
564
|
-
const msg: Message = {
|
|
565
|
-
id: String(Date.now()),
|
|
566
|
-
senderId,
|
|
567
|
-
text: text || undefined,
|
|
568
|
-
image: previewData?.type.startsWith('image/')
|
|
569
|
-
? previewData.uri
|
|
570
|
-
: undefined,
|
|
571
|
-
video: previewData?.type.startsWith('video/')
|
|
572
|
-
? previewData.uri
|
|
573
|
-
: undefined,
|
|
574
|
-
time: formatTime(new Date()),
|
|
575
|
-
status: 'sent',
|
|
576
|
-
};
|
|
577
|
-
setMessages((prev) => [msg, ...prev]);
|
|
578
|
-
setPreviewData(null);
|
|
579
|
-
}}
|
|
560
|
+
typingUsers={typingUsers}
|
|
561
|
+
onTypingStart={() => socket.emit('typing-start')}
|
|
562
|
+
onTypingEnd={() => socket.emit('typing-end')}
|
|
580
563
|
/>
|
|
581
564
|
```
|
|
582
565
|
|
|
566
|
+
Up to two avatars are shown side-by-side; additional users appear as a `+N` badge.
|
|
567
|
+
|
|
583
568
|
### Custom theme
|
|
584
569
|
|
|
585
570
|
```tsx
|
|
586
571
|
<ChatScreen
|
|
587
572
|
theme={{
|
|
588
|
-
fontFamily: '
|
|
573
|
+
fontFamily: 'Outfit-Regular',
|
|
589
574
|
colors: {
|
|
590
575
|
sentMessageTailColor: '#007AFF',
|
|
591
|
-
receivedMessageTailColor: '#
|
|
576
|
+
receivedMessageTailColor: '#E5E5EA',
|
|
592
577
|
timestamp: '#8E8E93',
|
|
593
|
-
|
|
578
|
+
inputsIconsColor: '#6C808E',
|
|
579
|
+
sendIconsColor: '#FFFFFF',
|
|
594
580
|
inputTextColor: '#000000',
|
|
581
|
+
readIconColor: '#34C759',
|
|
595
582
|
},
|
|
596
583
|
bubbleStyle: {
|
|
597
584
|
sent: { backgroundColor: '#007AFF' },
|
|
598
|
-
received: { backgroundColor: '#
|
|
585
|
+
received: { backgroundColor: '#E5E5EA' },
|
|
599
586
|
},
|
|
600
587
|
messageStyle: {
|
|
601
588
|
sentTextStyle: { color: '#FFFFFF' },
|
|
602
589
|
receivedTextStyle: { color: '#000000' },
|
|
603
590
|
},
|
|
604
591
|
inputStyles: {
|
|
592
|
+
inputContainerStyle: { backgroundColor: '#F2F2F7' },
|
|
605
593
|
sendButtonStyle: { backgroundColor: '#007AFF' },
|
|
606
594
|
},
|
|
595
|
+
sizes: {
|
|
596
|
+
inputIconSize: 22, // emoji, clip, camera only — not send/mic
|
|
597
|
+
},
|
|
607
598
|
}}
|
|
608
|
-
// ...
|
|
609
599
|
/>
|
|
610
600
|
```
|
|
611
601
|
|
|
602
|
+
### Font family (all text)
|
|
603
|
+
|
|
604
|
+
`theme.fontFamily` applies to **every** `Text` element inside the package: bubble text, timestamps, sender names, audio duration, typing label, file names, error messages, and the input field.
|
|
605
|
+
|
|
606
|
+
**You must load the font in your app before using it.** The package only sets the style name.
|
|
607
|
+
|
|
608
|
+
**Expo `expo-font` plugin approach:**
|
|
609
|
+
|
|
610
|
+
In `app.json`:
|
|
611
|
+
|
|
612
|
+
```json
|
|
613
|
+
"plugins": [
|
|
614
|
+
["expo-font", {
|
|
615
|
+
"fonts": ["./assets/fonts/Outfit-Regular.ttf"]
|
|
616
|
+
}]
|
|
617
|
+
]
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
Then pass the font name (usually the file name without `.ttf`):
|
|
621
|
+
|
|
622
|
+
```tsx
|
|
623
|
+
theme={{ fontFamily: 'Outfit-Regular' }}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
**Expo `useFonts` hook approach (custom name):**
|
|
627
|
+
|
|
628
|
+
```tsx
|
|
629
|
+
import { useFonts } from 'expo-font';
|
|
630
|
+
|
|
631
|
+
const [loaded] = useFonts({
|
|
632
|
+
'my-custom-font': require('./assets/fonts/MyFont.ttf'),
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
if (!loaded) return null;
|
|
636
|
+
|
|
637
|
+
return <ChatScreen theme={{ fontFamily: 'my-custom-font' }} />;
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### Keyboard avoiding
|
|
641
|
+
|
|
642
|
+
The package lifts the input bar automatically when the keyboard opens:
|
|
643
|
+
|
|
644
|
+
- **Android** — full keyboard height via a keyboard listener, applied as `marginBottom` on the input row.
|
|
645
|
+
- **iOS** — `KeyboardAvoidingView` with `behavior="padding"` plus the same keyboard listener.
|
|
646
|
+
|
|
647
|
+
In your screen:
|
|
648
|
+
|
|
649
|
+
```tsx
|
|
650
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
651
|
+
|
|
652
|
+
const insets = useSafeAreaInsets();
|
|
653
|
+
|
|
654
|
+
<View style={{ flex: 1 }}>
|
|
655
|
+
<ChatScreen
|
|
656
|
+
keyboardVerticalOffset={Platform.OS === 'ios' ? insets.top + 44 : 0}
|
|
657
|
+
// ...
|
|
658
|
+
/>
|
|
659
|
+
</View>
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
If your screen or navigator already handles the keyboard (e.g. wraps in its own `KeyboardAvoidingView`), pass `disableKeyboardAvoiding`:
|
|
663
|
+
|
|
664
|
+
```tsx
|
|
665
|
+
<ChatScreen disableKeyboardAvoiding />
|
|
666
|
+
```
|
|
667
|
+
|
|
612
668
|
### Custom input bar
|
|
613
669
|
|
|
614
670
|
```tsx
|
|
615
671
|
<ChatScreen
|
|
616
|
-
renderCustomInput={() => <
|
|
617
|
-
// Still use messages / currentUserId / onSendMessage via your own state
|
|
672
|
+
renderCustomInput={() => <MyOwnComposer onSend={handleSend} />}
|
|
618
673
|
/>
|
|
619
674
|
```
|
|
620
675
|
|
|
621
|
-
When
|
|
676
|
+
When `renderCustomInput` is provided the default `ChatInput` is not mounted. Preview props (`previewItems`, `closePreview`) are not wired automatically — handle them inside your custom component.
|
|
622
677
|
|
|
623
|
-
### Long-press
|
|
678
|
+
### Long-press on a message
|
|
624
679
|
|
|
625
680
|
```tsx
|
|
626
681
|
<ChatScreen
|
|
627
682
|
onMessageLongPress={(message) => {
|
|
683
|
+
// show action sheet: reply, copy, delete, etc.
|
|
684
|
+
Alert.alert(message.id, message.text ?? '');
|
|
628
685
|
}}
|
|
629
686
|
/>
|
|
630
687
|
```
|
|
631
688
|
|
|
632
689
|
---
|
|
633
690
|
|
|
634
|
-
##
|
|
691
|
+
## Full-screen gallery viewer
|
|
635
692
|
|
|
636
|
-
|
|
693
|
+
Tapping any image or video in a bubble opens a full-screen modal viewer:
|
|
637
694
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
} from 'movius-chats/lib/typescript/types';
|
|
644
|
-
```
|
|
695
|
+
- **Horizontal swipe** (`FlatList` paginated) moves between items in the same message.
|
|
696
|
+
- **Counter** `n / total` shown at the top when there are multiple items.
|
|
697
|
+
- **Images** fill the screen (`resizeMode: contain`).
|
|
698
|
+
- **Videos** use native controls (play, pause, seek, full-screen).
|
|
699
|
+
- **Close** with the × button in the top-right corner.
|
|
645
700
|
|
|
646
|
-
|
|
701
|
+
The viewer opens automatically — no extra code needed in your app.
|
|
647
702
|
|
|
648
703
|
---
|
|
649
704
|
|
|
@@ -651,41 +706,92 @@ import type {
|
|
|
651
706
|
|
|
652
707
|
```
|
|
653
708
|
ChatScreen
|
|
654
|
-
├── AudioProvider
|
|
655
|
-
├── ChatProvider
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
709
|
+
├── AudioProvider one audio plays at a time (context)
|
|
710
|
+
├── ChatProvider all props + gallery state (context)
|
|
711
|
+
│
|
|
712
|
+
├── FlatList (inverted)
|
|
713
|
+
│ └── ChatBubble per message
|
|
714
|
+
│ ├── MediaGrid WhatsApp-style 1/2/3/4+ image-video grid
|
|
715
|
+
│ ├── MessageContent file chips, audio player, parsed text
|
|
716
|
+
│ └── MessageStatus timestamp + sent/delivered/read icons
|
|
717
|
+
│
|
|
718
|
+
├── ChatInput (optional)
|
|
719
|
+
│ ├── TextInput auto-grows, resets on clear or send
|
|
720
|
+
│ ├── EmojiFunnySquareIcon / PaperClipIcon / CameraIcon
|
|
721
|
+
│ │ (sized by theme.sizes.inputIconSize — not send/mic)
|
|
722
|
+
│ ├── PaperPlaneIcon / MicrophoneIcon (fixed size)
|
|
723
|
+
│ └── FilePreview fan spread for images/videos, chips for docs
|
|
724
|
+
│
|
|
725
|
+
└── MediaViewer full-screen horizontal pager (Modal)
|
|
726
|
+
└── ViewerPage Image (contain) or Video (native controls)
|
|
660
727
|
```
|
|
661
728
|
|
|
662
|
-
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
## TypeScript types
|
|
732
|
+
|
|
733
|
+
Import all types from the build output:
|
|
663
734
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
735
|
+
```ts
|
|
736
|
+
import ChatScreen from 'movius-chats';
|
|
737
|
+
|
|
738
|
+
import type {
|
|
739
|
+
Message,
|
|
740
|
+
MessageMediaItem,
|
|
741
|
+
MessageFileAttachment,
|
|
742
|
+
PreviewAttachment,
|
|
743
|
+
ChatScreenProps,
|
|
744
|
+
} from 'movius-chats/lib/typescript/types';
|
|
745
|
+
```
|
|
668
746
|
|
|
669
747
|
---
|
|
670
748
|
|
|
671
749
|
## Troubleshooting
|
|
672
750
|
|
|
673
|
-
|
|
|
674
|
-
|
|
675
|
-
| `Native module not found` | Rebuild
|
|
676
|
-
| Crashes in Expo Go | Use a development build
|
|
677
|
-
| Audio silent on iOS |
|
|
678
|
-
| Video/audio
|
|
679
|
-
| Reanimated worklet
|
|
680
|
-
|
|
|
681
|
-
|
|
|
682
|
-
|
|
|
751
|
+
| Symptom | Fix |
|
|
752
|
+
|---------|-----|
|
|
753
|
+
| `Native module not found` | Rebuild the app after install — run `pod install` (iOS) and rebuild Android |
|
|
754
|
+
| Crashes in Expo Go | Use a development build — this package uses native modules not in Expo Go |
|
|
755
|
+
| Audio silent on iOS | Add `[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil]` in your `AppDelegate` |
|
|
756
|
+
| Video/audio URL not loading | Check URI scheme (`https://`, `file://`) and `INTERNET` permission on Android |
|
|
757
|
+
| Reanimated worklet error | Move `'react-native-reanimated/plugin'` to the **last** position in Babel plugins |
|
|
758
|
+
| Font has no effect | Load the font in your app first (Expo: `expo-font` plugin or `useFonts`); use the exact registered name |
|
|
759
|
+
| Input icon size not changing | `inputIconSize` only affects emoji, clip, and camera — not the send or mic button |
|
|
760
|
+
| Input doesn't return to pill shape | Happens when text is deleted character by character. In the latest version the layout resets when the field empties and on send |
|
|
761
|
+
| Keyboard overlaps input (Android) | Add `"softwareKeyboardLayoutMode": "resize"` to `app.json`; wrap `ChatScreen` in `flex: 1` |
|
|
762
|
+
| Keyboard offset wrong (iOS) | Tune `keyboardVerticalOffset` — it should equal your header height + status bar height |
|
|
763
|
+
| Messages appear in wrong order | Newest message must be at `messages[0]` (inverted FlatList) |
|
|
764
|
+
| Feature buttons missing | Feature flags (`showAttachmentsButton`, etc.) default to `false` — pass `true` to show them |
|
|
765
|
+
| Gallery does not swipe | Ensure `mediaItems` is an array; single `image`/`video` strings open the viewer for that single item |
|
|
683
766
|
|
|
684
767
|
---
|
|
685
768
|
|
|
686
|
-
##
|
|
769
|
+
## Publishing a new version
|
|
770
|
+
|
|
771
|
+
```bash
|
|
772
|
+
# 1. Bump version in package.json (follow semver)
|
|
773
|
+
npm version patch # or minor / major
|
|
774
|
+
|
|
775
|
+
# 2. Build
|
|
776
|
+
yarn build # runs rollup + tsc
|
|
777
|
+
|
|
778
|
+
# 3. Dry run to confirm what's included
|
|
779
|
+
npm pack --dry-run
|
|
780
|
+
|
|
781
|
+
# 4. Publish (use --otp if 2FA is on)
|
|
782
|
+
npm publish --access public --otp=YOUR_CODE
|
|
783
|
+
|
|
784
|
+
# 5. Tag the release
|
|
785
|
+
git push && git push --tags
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
After publishing, update the package in your consumer app:
|
|
789
|
+
|
|
790
|
+
```bash
|
|
791
|
+
npm install movius-chats@latest
|
|
792
|
+
```
|
|
687
793
|
|
|
688
|
-
|
|
794
|
+
If native dependencies changed, run `pod install` and rebuild the app.
|
|
689
795
|
|
|
690
796
|
---
|
|
691
797
|
|