movius-chats 1.3.7 → 1.3.10
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 +519 -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 +18 -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 +22 -8
- package/lib/typescript/utils/messageMedia.d.ts +3 -0
- package/package.json +1 -2
- package/src/components/ChatBubble/ChatBubble.tsx +20 -13
- package/src/components/ChatBubble/MediaGrid.tsx +314 -0
- package/src/components/ChatBubble/MessageContent.tsx +38 -102
- package/src/components/ChatBubble/MessageStatus.tsx +8 -1
- package/src/components/ChatBubble/types.ts +5 -2
- package/src/components/ChatInput/ChatInput.tsx +22 -16
- package/src/components/ChatInput/FilePreview.tsx +295 -135
- 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 +25 -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,59 @@ 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
|
-
- [
|
|
34
|
-
- [
|
|
40
|
+
- [Opening file attachments (expo-sharing)](#opening-file-attachments-expo-sharing)
|
|
41
|
+
- [Long-press on a message](#long-press-on-a-message)
|
|
42
|
+
- [Full-screen gallery viewer](#full-screen-gallery-viewer)
|
|
35
43
|
- [Architecture overview](#architecture-overview)
|
|
44
|
+
- [TypeScript types](#typescript-types)
|
|
36
45
|
- [Troubleshooting](#troubleshooting)
|
|
37
|
-
- [
|
|
46
|
+
- [Publishing a new version](#publishing-a-new-version)
|
|
38
47
|
- [License](#license)
|
|
39
48
|
|
|
40
49
|
---
|
|
41
50
|
|
|
42
|
-
## Requirements
|
|
51
|
+
## Requirements & compatibility
|
|
43
52
|
|
|
44
53
|
| Dependency | Role |
|
|
45
54
|
|------------|------|
|
|
46
55
|
| `react` ≥ 16.8 | Peer dependency |
|
|
47
56
|
| `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 |
|
|
57
|
+
| `react-native-reanimated` | Audio scrubber animation (peer) |
|
|
51
58
|
| `react-native-sound` | Voice message playback |
|
|
52
59
|
| `react-native-svg` | Built-in icons |
|
|
53
|
-
| `react-native-video` | Video
|
|
54
|
-
| `
|
|
60
|
+
| `react-native-video` | Video thumbnails + full-screen video |
|
|
61
|
+
| `react-native-parsed-text` | Tappable URLs in text messages |
|
|
62
|
+
| `twrnc` | Internal Tailwind-style utilities |
|
|
55
63
|
|
|
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.
|
|
64
|
+
**Not compatible with Expo Go** — native modules require a development build or bare RN project.
|
|
63
65
|
|
|
64
66
|
---
|
|
65
67
|
|
|
@@ -72,55 +74,30 @@ npm install movius-chats
|
|
|
72
74
|
# or
|
|
73
75
|
yarn add movius-chats
|
|
74
76
|
# or
|
|
75
|
-
bun
|
|
77
|
+
bun add movius-chats
|
|
76
78
|
```
|
|
77
79
|
|
|
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):
|
|
80
|
+
### 2. Install peer / native dependencies
|
|
81
81
|
|
|
82
82
|
```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
|
|
83
|
+
npm install react-native-reanimated react-native-sound react-native-svg react-native-video twrnc
|
|
88
84
|
```
|
|
89
85
|
|
|
90
|
-
> `react-native-parsed-text` is pulled in transitively; no extra install step unless your bundler requires it.
|
|
91
|
-
|
|
92
86
|
### 3. Configure Reanimated
|
|
93
87
|
|
|
94
|
-
Add the Reanimated
|
|
88
|
+
Add the Reanimated plugin **last** in `babel.config.js`:
|
|
95
89
|
|
|
96
|
-
```
|
|
90
|
+
```js
|
|
97
91
|
module.exports = {
|
|
98
92
|
presets: ['module:metro-react-native-babel-preset'],
|
|
99
93
|
plugins: [
|
|
100
|
-
//
|
|
94
|
+
// other plugins
|
|
101
95
|
'react-native-reanimated/plugin',
|
|
102
96
|
],
|
|
103
97
|
};
|
|
104
98
|
```
|
|
105
99
|
|
|
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
|
|
100
|
+
### 4. Rebuild native code
|
|
124
101
|
|
|
125
102
|
**React Native CLI:**
|
|
126
103
|
|
|
@@ -130,19 +107,21 @@ npx react-native run-ios
|
|
|
130
107
|
npx react-native run-android
|
|
131
108
|
```
|
|
132
109
|
|
|
133
|
-
**Expo
|
|
110
|
+
**Expo development build:**
|
|
134
111
|
|
|
135
112
|
```bash
|
|
136
113
|
npx expo prebuild
|
|
137
|
-
npx expo run:ios
|
|
138
|
-
# or
|
|
139
|
-
npx expo run:android
|
|
114
|
+
npx expo run:ios # or run:android
|
|
140
115
|
```
|
|
141
116
|
|
|
142
|
-
|
|
117
|
+
### 5. Android keyboard mode (Expo)
|
|
143
118
|
|
|
144
|
-
|
|
145
|
-
|
|
119
|
+
In `app.json` / `app.config.js` add:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
"android": {
|
|
123
|
+
"softwareKeyboardLayoutMode": "resize"
|
|
124
|
+
}
|
|
146
125
|
```
|
|
147
126
|
|
|
148
127
|
---
|
|
@@ -151,45 +130,46 @@ npx expo prebuild --clean
|
|
|
151
130
|
|
|
152
131
|
```tsx
|
|
153
132
|
import React, { useState } from 'react';
|
|
154
|
-
import { SafeAreaView } from 'react-native';
|
|
133
|
+
import { Platform, SafeAreaView, View } from 'react-native';
|
|
155
134
|
import ChatScreen from 'movius-chats';
|
|
156
135
|
import type { Message } from 'movius-chats/lib/typescript/types';
|
|
136
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
157
137
|
|
|
158
|
-
export default function
|
|
159
|
-
const
|
|
138
|
+
export default function MyChatScreen() {
|
|
139
|
+
const insets = useSafeAreaInsets();
|
|
160
140
|
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
|
-
};
|
|
141
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
177
142
|
|
|
178
143
|
return (
|
|
179
144
|
<SafeAreaView style={{ flex: 1 }}>
|
|
180
|
-
<
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
145
|
+
<View style={{ flex: 1 }}>
|
|
146
|
+
<ChatScreen
|
|
147
|
+
messages={messages}
|
|
148
|
+
currentUserId={currentUserId}
|
|
149
|
+
onSendMessage={({ text, senderId }) => {
|
|
150
|
+
setMessages((prev) => [
|
|
151
|
+
{
|
|
152
|
+
id: String(Date.now()),
|
|
153
|
+
text,
|
|
154
|
+
senderId,
|
|
155
|
+
time: new Date().toLocaleTimeString([], {
|
|
156
|
+
hour: '2-digit',
|
|
157
|
+
minute: '2-digit',
|
|
158
|
+
}),
|
|
159
|
+
status: 'sent',
|
|
160
|
+
},
|
|
161
|
+
...prev,
|
|
162
|
+
]);
|
|
163
|
+
}}
|
|
164
|
+
keyboardVerticalOffset={Platform.OS === 'ios' ? insets.top + 44 : 0}
|
|
165
|
+
showAvatars
|
|
166
|
+
showBubbleTail
|
|
167
|
+
showMessageStatus
|
|
168
|
+
showAttachmentsButton
|
|
169
|
+
showCameraButton
|
|
170
|
+
showVoiceRecordButton
|
|
171
|
+
/>
|
|
172
|
+
</View>
|
|
193
173
|
</SafeAreaView>
|
|
194
174
|
);
|
|
195
175
|
}
|
|
@@ -197,125 +177,151 @@ export default function App() {
|
|
|
197
177
|
|
|
198
178
|
---
|
|
199
179
|
|
|
200
|
-
##
|
|
180
|
+
## Data model
|
|
181
|
+
|
|
182
|
+
### Message
|
|
201
183
|
|
|
202
|
-
|
|
184
|
+
Every item in the `messages` array must match this shape:
|
|
203
185
|
|
|
204
186
|
| Field | Type | Required | Description |
|
|
205
187
|
|-------|------|----------|-------------|
|
|
206
188
|
| `id` | `string` | Yes | Unique message id |
|
|
207
189
|
| `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
|
-
| `
|
|
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 |
|
|
214
198
|
| `senderName` | `string` | No | Shown when `showUserNames` is true |
|
|
215
199
|
| `senderAvatar` | `string` | No | Avatar image URI; falls back to first letter of `senderName` |
|
|
216
200
|
|
|
217
|
-
A message can combine fields (e.g. text +
|
|
201
|
+
A message can combine fields (e.g. `text` + `mediaItems`).
|
|
218
202
|
|
|
219
|
-
|
|
220
|
-
import type { Message } from 'movius-chats/lib/typescript/types';
|
|
203
|
+
### MessageMediaItem
|
|
221
204
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
text: 'Check this out https://example.com',
|
|
228
|
-
time: '10:42 AM',
|
|
229
|
-
status: 'read',
|
|
230
|
-
};
|
|
205
|
+
```ts
|
|
206
|
+
interface MessageMediaItem {
|
|
207
|
+
uri: string;
|
|
208
|
+
kind: 'image' | 'video';
|
|
209
|
+
}
|
|
231
210
|
```
|
|
232
211
|
|
|
212
|
+
Used in `message.mediaItems` for multi-media bubbles.
|
|
213
|
+
|
|
214
|
+
### MessageFileAttachment
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
interface MessageFileAttachment {
|
|
218
|
+
uri: string;
|
|
219
|
+
type: string; // MIME type, e.g. "application/pdf"
|
|
220
|
+
name: string;
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Each attachment is shown as a tappable chip that opens the file via `Linking.openURL`.
|
|
225
|
+
|
|
226
|
+
### PreviewAttachment
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
interface PreviewAttachment {
|
|
230
|
+
uri: string;
|
|
231
|
+
type: string; // MIME type
|
|
232
|
+
name?: string;
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Used in `previewData` / `previewItems` for the composer preview strip above the input.
|
|
237
|
+
|
|
233
238
|
---
|
|
234
239
|
|
|
235
240
|
## Message list ordering
|
|
236
241
|
|
|
237
|
-
`ChatScreen` uses an **inverted** `FlatList`.
|
|
242
|
+
`ChatScreen` uses an **inverted** `FlatList`. The **newest message must be at index `0`**:
|
|
238
243
|
|
|
239
|
-
```
|
|
240
|
-
setMessages((prev) => [newMessage, ...prev]); //
|
|
244
|
+
```ts
|
|
245
|
+
setMessages((prev) => [newMessage, ...prev]); // correct
|
|
241
246
|
```
|
|
242
247
|
|
|
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`).
|
|
248
|
+
Consecutive messages from the same sender are grouped — avatars and tails appear only on the first message in each group.
|
|
246
249
|
|
|
247
250
|
---
|
|
248
251
|
|
|
249
252
|
## ChatScreen API
|
|
250
253
|
|
|
251
|
-
`ChatScreen` is the **default export** from `movius-chats`.
|
|
254
|
+
`ChatScreen` is the **default export** from `movius-chats`.
|
|
252
255
|
|
|
253
256
|
### Core props
|
|
254
257
|
|
|
255
258
|
| Prop | Type | Required | Description |
|
|
256
259
|
|------|------|----------|-------------|
|
|
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
|
|
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 |
|
|
264
267
|
|
|
265
268
|
### Feature flags
|
|
266
269
|
|
|
267
|
-
All
|
|
270
|
+
All default to `false` (hidden) unless explicitly set to `true`:
|
|
268
271
|
|
|
269
272
|
| Prop | Description |
|
|
270
273
|
|------|-------------|
|
|
271
|
-
| `showAvatars` | Avatar (or initial) on received
|
|
274
|
+
| `showAvatars` | Avatar (or initial letter) on received bubbles and typing row |
|
|
272
275
|
| `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
|
|
276
|
+
| `showBubbleTail` | WhatsApp-style corner tail on first bubble in a sequence |
|
|
277
|
+
| `showMessageStatus` | Timestamp + checkmark icons on sent messages |
|
|
278
|
+
| `showEmojiButton` | Emoji button in the input bar (UI only — wire your own picker via `CustomEmojiIcon`) |
|
|
279
|
+
| `showAttachmentsButton` | Paperclip icon — triggers `onAttachmentPress` |
|
|
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 |
|
|
279
282
|
|
|
280
283
|
### Input & typing
|
|
281
284
|
|
|
282
285
|
| Prop | Type | Description |
|
|
283
286
|
|------|------|-------------|
|
|
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
|
-
| `
|
|
287
|
+
| `onTypingStart` | `() => void` | Called when the text input becomes non-empty |
|
|
288
|
+
| `onTypingEnd` | `() => void` | Called when the text input is cleared |
|
|
289
|
+
| `onAttachmentPress` | `() => void` | Paperclip tapped — open your file/image picker |
|
|
290
|
+
| `onCameraPress` | `() => void` | Camera icon tapped — open camera |
|
|
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) |
|
|
291
295
|
|
|
292
|
-
|
|
296
|
+
### Attachment preview (composer)
|
|
293
297
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
Show a file/image/video preview above the input before sending:
|
|
298
|
+
Show a preview strip above the input before the user taps send.
|
|
297
299
|
|
|
298
300
|
| Prop | Type | Description |
|
|
299
301
|
|------|------|-------------|
|
|
300
|
-
| `
|
|
301
|
-
| `
|
|
302
|
+
| `previewItems` | `PreviewAttachment[]` | **Multiple** attachments — images/videos shown as a fanned spread; documents shown as file chips |
|
|
303
|
+
| `previewData` | `PreviewAttachment` | **Single** attachment (kept for backward compatibility; `previewItems` takes precedence) |
|
|
304
|
+
| `closePreview` | `() => void` | Fallback called when no `onRemovePreviewItem` is provided |
|
|
305
|
+
| `onRemovePreviewItem` | `(uri: string) => void` | Called with the URI of whichever card the user tapped × on — removes only that item |
|
|
306
|
+
|
|
307
|
+
**Per-item removal:** Every media thumbnail and every document chip shows its own × button. When `onRemovePreviewItem` is provided, tapping × on a card calls it with that card's URI so you can filter it out of your state. `closePreview` is only used as a fallback when `onRemovePreviewItem` is not supplied.
|
|
302
308
|
|
|
303
|
-
When
|
|
309
|
+
When any preview is present, the send button appears regardless of text content.
|
|
304
310
|
|
|
305
|
-
###
|
|
311
|
+
### Theme
|
|
306
312
|
|
|
307
|
-
Pass a `theme` object to
|
|
313
|
+
All keys are optional. Pass a `theme` object to `ChatScreen`:
|
|
308
314
|
|
|
309
|
-
```
|
|
315
|
+
```ts
|
|
310
316
|
theme?: {
|
|
311
|
-
fontFamily?: string;
|
|
317
|
+
fontFamily?: string; // applied to every Text element in the package
|
|
312
318
|
|
|
313
319
|
colors?: {
|
|
314
320
|
sentMessageTailColor?: string;
|
|
315
321
|
receivedMessageTailColor?: string;
|
|
316
322
|
timestamp?: string;
|
|
317
|
-
inputsIconsColor?: string;
|
|
318
|
-
sendIconsColor?: string;
|
|
323
|
+
inputsIconsColor?: string; // emoji, clip, camera icons
|
|
324
|
+
sendIconsColor?: string; // send / mic icons
|
|
319
325
|
placeholderTextColor?: string;
|
|
320
326
|
inputTextColor?: string;
|
|
321
327
|
audioPlayIconColor?: string;
|
|
@@ -326,10 +332,10 @@ theme?: {
|
|
|
326
332
|
readIconColor?: string;
|
|
327
333
|
};
|
|
328
334
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
+
};
|
|
333
339
|
|
|
334
340
|
bubbleStyle?: {
|
|
335
341
|
sent?: ViewStyle;
|
|
@@ -353,9 +359,9 @@ theme?: {
|
|
|
353
359
|
};
|
|
354
360
|
|
|
355
361
|
inputStyles?: {
|
|
356
|
-
inputSectionContainerStyle?: ViewStyle;
|
|
357
|
-
inputContainerStyle?: ViewStyle;
|
|
358
|
-
sendButtonStyle?: ViewStyle;
|
|
362
|
+
inputSectionContainerStyle?: ViewStyle; // outer row (input + send button)
|
|
363
|
+
inputContainerStyle?: ViewStyle; // the pill/rounded box
|
|
364
|
+
sendButtonStyle?: ViewStyle; // the round send/mic button
|
|
359
365
|
};
|
|
360
366
|
|
|
361
367
|
filePreviewStyle?: {
|
|
@@ -368,282 +374,375 @@ theme?: {
|
|
|
368
374
|
}
|
|
369
375
|
```
|
|
370
376
|
|
|
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;
|
|
377
|
+
### Custom components & icons
|
|
390
378
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
379
|
+
| Prop | Type | Description |
|
|
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 |
|
|
399
394
|
|
|
400
|
-
|
|
395
|
+
---
|
|
401
396
|
|
|
402
|
-
|
|
397
|
+
## Usage examples
|
|
403
398
|
|
|
404
|
-
|
|
399
|
+
### Basic text chat
|
|
405
400
|
|
|
406
401
|
```tsx
|
|
407
402
|
<ChatScreen
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
403
|
+
messages={messages}
|
|
404
|
+
currentUserId="user-1"
|
|
405
|
+
onSendMessage={({ text, senderId }) => {
|
|
406
|
+
setMessages((prev) => [
|
|
407
|
+
{
|
|
408
|
+
id: String(Date.now()),
|
|
409
|
+
text,
|
|
410
|
+
senderId,
|
|
411
|
+
time: '10:00 AM',
|
|
412
|
+
status: 'sent',
|
|
413
|
+
},
|
|
414
|
+
...prev,
|
|
415
|
+
]);
|
|
413
416
|
}}
|
|
414
417
|
/>
|
|
415
418
|
```
|
|
416
419
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
The package lifts the chat when the keyboard opens (keyboard listeners + `KeyboardAvoidingView` on iOS).
|
|
420
|
+
### Multi-image / video album bubble
|
|
420
421
|
|
|
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.
|
|
422
|
+
Use `mediaItems` in the `Message` object. The bubble renders a WhatsApp-style grid:
|
|
424
423
|
|
|
425
|
-
|
|
426
|
-
|
|
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 |
|
|
427
430
|
|
|
428
|
-
|
|
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.
|
|
429
432
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
"softwareKeyboardLayoutMode": "resize"
|
|
443
|
-
}
|
|
433
|
+
```tsx
|
|
434
|
+
const message: Message = {
|
|
435
|
+
id: '1',
|
|
436
|
+
senderId: 'user-2',
|
|
437
|
+
time: '11:00 AM',
|
|
438
|
+
status: 'read',
|
|
439
|
+
mediaItems: [
|
|
440
|
+
{ uri: 'https://example.com/photo1.jpg', kind: 'image' },
|
|
441
|
+
{ uri: 'https://example.com/photo2.jpg', kind: 'image' },
|
|
442
|
+
{ uri: 'https://example.com/clip.mp4', kind: 'video' },
|
|
443
|
+
],
|
|
444
|
+
};
|
|
444
445
|
```
|
|
445
446
|
|
|
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
|
|
447
|
+
Legacy single-item syntax still works and is merged automatically:
|
|
471
448
|
|
|
472
449
|
```tsx
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}}
|
|
450
|
+
// These are equivalent for display purposes
|
|
451
|
+
{ image: 'https://...' }
|
|
452
|
+
{ mediaItems: [{ uri: 'https://...', kind: 'image' }] }
|
|
476
453
|
```
|
|
477
454
|
|
|
478
|
-
###
|
|
479
|
-
|
|
480
|
-
Add URIs when building the `Message` object (usually after upload):
|
|
455
|
+
### File attachment bubble
|
|
481
456
|
|
|
482
457
|
```tsx
|
|
483
|
-
const
|
|
458
|
+
const message: Message = {
|
|
484
459
|
id: '2',
|
|
485
|
-
senderId:
|
|
486
|
-
|
|
487
|
-
time: '11:00 AM',
|
|
460
|
+
senderId: 'user-1',
|
|
461
|
+
time: '11:05 AM',
|
|
488
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',
|
|
489
471
|
};
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Each attachment renders as a tappable row. By default tapping calls `Linking.openURL(uri)`. Supply `onFileAttachmentPress` to override this with your own handler (e.g. `expo-sharing`).
|
|
475
|
+
|
|
476
|
+
### Audio message bubble
|
|
490
477
|
|
|
491
|
-
|
|
478
|
+
```tsx
|
|
479
|
+
const message: Message = {
|
|
492
480
|
id: '3',
|
|
493
481
|
senderId: 'user-2',
|
|
494
482
|
audio: 'file:///path/to/recording.m4a',
|
|
495
|
-
time: '11:
|
|
483
|
+
time: '11:10 AM',
|
|
496
484
|
status: 'read',
|
|
497
485
|
};
|
|
498
486
|
```
|
|
499
487
|
|
|
500
|
-
|
|
488
|
+
The audio player has a scrubable progress bar, a play/pause button, and a duration counter.
|
|
489
|
+
Only one audio message plays at a time — starting a new one automatically stops the previous one.
|
|
501
490
|
|
|
502
|
-
###
|
|
491
|
+
### Composer attachment preview (single or multiple)
|
|
503
492
|
|
|
504
|
-
|
|
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
|
-
```
|
|
516
|
-
|
|
517
|
-
When your socket receives “user X is typing”, push into `typingUsers`. The component shows up to two avatars plus a “+N” badge.
|
|
518
|
-
|
|
519
|
-
### Attachments & camera (parent-controlled)
|
|
493
|
+
Use `previewItems` (array) for multi-select, or `previewData` (single) for back-compat:
|
|
520
494
|
|
|
521
495
|
```tsx
|
|
522
|
-
|
|
496
|
+
const [previews, setPreviews] = useState<PreviewAttachment[]>([]);
|
|
523
497
|
|
|
524
498
|
<ChatScreen
|
|
525
|
-
|
|
526
|
-
|
|
499
|
+
previewItems={previews}
|
|
500
|
+
closePreview={() => setPreviews([])}
|
|
501
|
+
// Remove only the card whose × was tapped:
|
|
502
|
+
onRemovePreviewItem={(uri) =>
|
|
503
|
+
setPreviews((prev) => prev.filter((p) => p.uri !== uri))
|
|
504
|
+
}
|
|
527
505
|
onAttachmentPress={async () => {
|
|
528
|
-
const
|
|
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
|
+
);
|
|
529
510
|
}}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
+
]);
|
|
535
|
+
|
|
536
|
+
setPreviews([]);
|
|
539
537
|
}}
|
|
540
538
|
/>
|
|
541
539
|
```
|
|
542
540
|
|
|
543
|
-
|
|
541
|
+
Preview UI:
|
|
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.
|
|
546
|
+
|
|
547
|
+
Tapping × on any card calls `onRemovePreviewItem(uri)` for that specific file. When the last item is removed the preview strip disappears automatically.
|
|
548
|
+
|
|
549
|
+
### Send button vs microphone
|
|
550
|
+
|
|
551
|
+
The send button (green circle, right of input) shows:
|
|
552
|
+
|
|
553
|
+
| Condition | Icon shown |
|
|
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 |
|
|
559
|
+
|
|
560
|
+
No extra work needed — the package handles this automatically.
|
|
561
|
+
|
|
562
|
+
### Typing indicators
|
|
544
563
|
|
|
545
564
|
```tsx
|
|
546
|
-
const [
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
} | null>(null);
|
|
565
|
+
const [typingUsers, setTypingUsers] = useState([]);
|
|
566
|
+
|
|
567
|
+
// When your socket receives "user X is typing":
|
|
568
|
+
setTypingUsers([{ id: 'user-2', avatar: 'https://...', name: 'Alex' }]);
|
|
551
569
|
|
|
552
570
|
<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
|
-
}}
|
|
571
|
+
typingUsers={typingUsers}
|
|
572
|
+
onTypingStart={() => socket.emit('typing-start')}
|
|
573
|
+
onTypingEnd={() => socket.emit('typing-end')}
|
|
580
574
|
/>
|
|
581
575
|
```
|
|
582
576
|
|
|
577
|
+
Up to two avatars are shown side-by-side; additional users appear as a `+N` badge.
|
|
578
|
+
|
|
583
579
|
### Custom theme
|
|
584
580
|
|
|
585
581
|
```tsx
|
|
586
582
|
<ChatScreen
|
|
587
583
|
theme={{
|
|
588
|
-
fontFamily: '
|
|
584
|
+
fontFamily: 'Outfit-Regular',
|
|
589
585
|
colors: {
|
|
590
586
|
sentMessageTailColor: '#007AFF',
|
|
591
|
-
receivedMessageTailColor: '#
|
|
587
|
+
receivedMessageTailColor: '#E5E5EA',
|
|
592
588
|
timestamp: '#8E8E93',
|
|
593
|
-
|
|
589
|
+
inputsIconsColor: '#6C808E',
|
|
590
|
+
sendIconsColor: '#FFFFFF',
|
|
594
591
|
inputTextColor: '#000000',
|
|
592
|
+
readIconColor: '#34C759',
|
|
595
593
|
},
|
|
596
594
|
bubbleStyle: {
|
|
597
595
|
sent: { backgroundColor: '#007AFF' },
|
|
598
|
-
received: { backgroundColor: '#
|
|
596
|
+
received: { backgroundColor: '#E5E5EA' },
|
|
599
597
|
},
|
|
600
598
|
messageStyle: {
|
|
601
599
|
sentTextStyle: { color: '#FFFFFF' },
|
|
602
600
|
receivedTextStyle: { color: '#000000' },
|
|
603
601
|
},
|
|
604
602
|
inputStyles: {
|
|
603
|
+
inputContainerStyle: { backgroundColor: '#F2F2F7' },
|
|
605
604
|
sendButtonStyle: { backgroundColor: '#007AFF' },
|
|
606
605
|
},
|
|
606
|
+
sizes: {
|
|
607
|
+
inputIconSize: 22, // emoji, clip, camera only — not send/mic
|
|
608
|
+
},
|
|
607
609
|
}}
|
|
608
|
-
// ...
|
|
609
610
|
/>
|
|
610
611
|
```
|
|
611
612
|
|
|
613
|
+
### Font family (all text)
|
|
614
|
+
|
|
615
|
+
`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.
|
|
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
|
+
```
|
|
630
|
+
|
|
631
|
+
Then pass the font name (usually the file name without `.ttf`):
|
|
632
|
+
|
|
633
|
+
```tsx
|
|
634
|
+
theme={{ fontFamily: 'Outfit-Regular' }}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
**Expo `useFonts` hook approach (custom name):**
|
|
638
|
+
|
|
639
|
+
```tsx
|
|
640
|
+
import { useFonts } from 'expo-font';
|
|
641
|
+
|
|
642
|
+
const [loaded] = useFonts({
|
|
643
|
+
'my-custom-font': require('./assets/fonts/MyFont.ttf'),
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
if (!loaded) return null;
|
|
647
|
+
|
|
648
|
+
return <ChatScreen theme={{ fontFamily: 'my-custom-font' }} />;
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### Keyboard avoiding
|
|
652
|
+
|
|
653
|
+
The package lifts the input bar automatically when the keyboard opens:
|
|
654
|
+
|
|
655
|
+
- **Android** — full keyboard height via a keyboard listener, applied as `marginBottom` on the input row.
|
|
656
|
+
- **iOS** — `KeyboardAvoidingView` with `behavior="padding"` plus the same keyboard listener.
|
|
657
|
+
|
|
658
|
+
In your screen:
|
|
659
|
+
|
|
660
|
+
```tsx
|
|
661
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
662
|
+
|
|
663
|
+
const insets = useSafeAreaInsets();
|
|
664
|
+
|
|
665
|
+
<View style={{ flex: 1 }}>
|
|
666
|
+
<ChatScreen
|
|
667
|
+
keyboardVerticalOffset={Platform.OS === 'ios' ? insets.top + 44 : 0}
|
|
668
|
+
// ...
|
|
669
|
+
/>
|
|
670
|
+
</View>
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
If your screen or navigator already handles the keyboard (e.g. wraps in its own `KeyboardAvoidingView`), pass `disableKeyboardAvoiding`:
|
|
674
|
+
|
|
675
|
+
```tsx
|
|
676
|
+
<ChatScreen disableKeyboardAvoiding />
|
|
677
|
+
```
|
|
678
|
+
|
|
612
679
|
### Custom input bar
|
|
613
680
|
|
|
614
681
|
```tsx
|
|
615
682
|
<ChatScreen
|
|
616
|
-
renderCustomInput={() => <
|
|
617
|
-
// Still use messages / currentUserId / onSendMessage via your own state
|
|
683
|
+
renderCustomInput={() => <MyOwnComposer onSend={handleSend} />}
|
|
618
684
|
/>
|
|
619
685
|
```
|
|
620
686
|
|
|
621
|
-
When
|
|
687
|
+
When `renderCustomInput` is provided the default `ChatInput` is not mounted. Preview props (`previewItems`, `closePreview`) are not wired automatically — handle them inside your custom component.
|
|
688
|
+
|
|
689
|
+
### Opening file attachments (expo-sharing)
|
|
690
|
+
|
|
691
|
+
By default tapping a file-attachment chip in a bubble calls `Linking.openURL`. For Expo apps that need to share or download local files, supply `onFileAttachmentPress`:
|
|
692
|
+
|
|
693
|
+
```bash
|
|
694
|
+
yarn add expo-sharing
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
```tsx
|
|
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
|
+
```
|
|
622
721
|
|
|
623
|
-
### Long-press
|
|
722
|
+
### Long-press on a message
|
|
624
723
|
|
|
625
724
|
```tsx
|
|
626
725
|
<ChatScreen
|
|
627
726
|
onMessageLongPress={(message) => {
|
|
727
|
+
// show action sheet: reply, copy, delete, etc.
|
|
728
|
+
Alert.alert(message.id, message.text ?? '');
|
|
628
729
|
}}
|
|
629
730
|
/>
|
|
630
731
|
```
|
|
631
732
|
|
|
632
733
|
---
|
|
633
734
|
|
|
634
|
-
##
|
|
735
|
+
## Full-screen gallery viewer
|
|
635
736
|
|
|
636
|
-
|
|
737
|
+
Tapping any image or video in a bubble opens a full-screen modal viewer:
|
|
637
738
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
} from 'movius-chats/lib/typescript/types';
|
|
644
|
-
```
|
|
739
|
+
- **Horizontal swipe** (`FlatList` paginated) moves between items in the same message.
|
|
740
|
+
- **Counter** `n / total` shown at the top when there are multiple items.
|
|
741
|
+
- **Images** fill the screen (`resizeMode: contain`).
|
|
742
|
+
- **Videos** use native controls (play, pause, seek, full-screen).
|
|
743
|
+
- **Close** with the × button in the top-right corner.
|
|
645
744
|
|
|
646
|
-
|
|
745
|
+
The viewer opens automatically — no extra code needed in your app.
|
|
647
746
|
|
|
648
747
|
---
|
|
649
748
|
|
|
@@ -651,41 +750,94 @@ import type {
|
|
|
651
750
|
|
|
652
751
|
```
|
|
653
752
|
ChatScreen
|
|
654
|
-
├── AudioProvider
|
|
655
|
-
├── ChatProvider
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
753
|
+
├── AudioProvider one audio plays at a time (context)
|
|
754
|
+
├── ChatProvider all props + gallery state (context)
|
|
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)
|
|
660
771
|
```
|
|
661
772
|
|
|
662
|
-
|
|
773
|
+
---
|
|
774
|
+
|
|
775
|
+
## TypeScript types
|
|
663
776
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
777
|
+
Import all types from the build output:
|
|
778
|
+
|
|
779
|
+
```ts
|
|
780
|
+
import ChatScreen from 'movius-chats';
|
|
781
|
+
|
|
782
|
+
import type {
|
|
783
|
+
Message,
|
|
784
|
+
MessageMediaItem,
|
|
785
|
+
MessageFileAttachment,
|
|
786
|
+
PreviewAttachment,
|
|
787
|
+
ChatScreenProps,
|
|
788
|
+
} from 'movius-chats/lib/typescript/types';
|
|
789
|
+
```
|
|
668
790
|
|
|
669
791
|
---
|
|
670
792
|
|
|
671
793
|
## Troubleshooting
|
|
672
794
|
|
|
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
|
-
|
|
|
795
|
+
| Symptom | Fix |
|
|
796
|
+
|---------|-----|
|
|
797
|
+
| `Native module not found` | Rebuild the app after install — run `pod install` (iOS) and rebuild Android |
|
|
798
|
+
| Crashes in Expo Go | Use a development build — this package uses native modules not in Expo Go |
|
|
799
|
+
| Audio silent on iOS | Add `[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil]` in your `AppDelegate` |
|
|
800
|
+
| Video/audio URL not loading | Check URI scheme (`https://`, `file://`) and `INTERNET` permission on Android |
|
|
801
|
+
| Reanimated worklet error | Move `'react-native-reanimated/plugin'` to the **last** position in Babel plugins |
|
|
802
|
+
| Font has no effect | Load the font in your app first (Expo: `expo-font` plugin or `useFonts`); use the exact registered name |
|
|
803
|
+
| Input icon size not changing | `inputIconSize` only affects emoji, clip, and camera — not the send or mic button |
|
|
804
|
+
| 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 |
|
|
805
|
+
| Keyboard overlaps input (Android) | Add `"softwareKeyboardLayoutMode": "resize"` to `app.json`; wrap `ChatScreen` in `flex: 1` |
|
|
806
|
+
| Keyboard offset wrong (iOS) | Tune `keyboardVerticalOffset` — it should equal your header height + status bar height |
|
|
807
|
+
| Messages appear in wrong order | Newest message must be at `messages[0]` (inverted FlatList) |
|
|
808
|
+
| Feature buttons missing | Feature flags (`showAttachmentsButton`, etc.) default to `false` — pass `true` to show them |
|
|
809
|
+
| Gallery does not swipe | Ensure `mediaItems` is an array; single `image`/`video` strings open the viewer for that single item |
|
|
810
|
+
| File attachment tap does nothing | Default is `Linking.openURL`. For local files on iOS/Android use `onFileAttachmentPress` with `expo-sharing` |
|
|
811
|
+
| Tapping × removes all previews | Supply `onRemovePreviewItem` — without it the fallback `closePreview` clears everything |
|
|
683
812
|
|
|
684
813
|
---
|
|
685
814
|
|
|
686
|
-
##
|
|
815
|
+
## Publishing a new version
|
|
816
|
+
|
|
817
|
+
```bash
|
|
818
|
+
# 1. Bump version in package.json (follow semver)
|
|
819
|
+
npm version patch # or minor / major
|
|
820
|
+
|
|
821
|
+
# 2. Build
|
|
822
|
+
yarn build # runs rollup + tsc
|
|
823
|
+
|
|
824
|
+
# 3. Dry run to confirm what's included
|
|
825
|
+
npm pack --dry-run
|
|
826
|
+
|
|
827
|
+
# 4. Publish (use --otp if 2FA is on)
|
|
828
|
+
npm publish --access public --otp=YOUR_CODE
|
|
829
|
+
|
|
830
|
+
# 5. Tag the release
|
|
831
|
+
git push && git push --tags
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
After publishing, update the package in your consumer app:
|
|
835
|
+
|
|
836
|
+
```bash
|
|
837
|
+
npm install movius-chats@latest
|
|
838
|
+
```
|
|
687
839
|
|
|
688
|
-
|
|
840
|
+
If native dependencies changed, run `pod install` and rebuild the app.
|
|
689
841
|
|
|
690
842
|
---
|
|
691
843
|
|