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 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 (images, video, audio), typing indicators, attachment previews, and a full input bar—with deep theming and custom icon/component hooks.
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
- - [Message data model](#message-data-model)
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
- - [Theming](#theming)
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
- - [Text messages](#text-messages)
27
- - [Media messages](#media-messages)
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 actions](#long-press-actions)
34
- - [TypeScript](#typescript)
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
- - [Contributing](#contributing)
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 animations (peer) |
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 bubbles & preview |
54
- | `twrnc` | Tailwind-style utility classes |
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 install movius-chats
77
+ bun add movius-chats
76
78
  ```
77
79
 
78
- ### 2. Install peer & native dependencies
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-image-zoom-viewer react-native-sound react-native-svg react-native-video twrnc
84
- # or
85
- yarn add react-native-reanimated react-native-image-zoom-viewer react-native-sound react-native-svg react-native-video twrnc
86
- #or
87
- bun install react-native-reanimated react-native-image-zoom-viewer react-native-sound react-native-svg react-native-video twrnc
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 Babel plugin **last** in `babel.config.js`:
88
+ Add the Reanimated plugin **last** in `babel.config.js`:
95
89
 
96
- ```javascript
90
+ ```js
97
91
  module.exports = {
98
92
  presets: ['module:metro-react-native-babel-preset'],
99
93
  plugins: [
100
- // ...other plugins
94
+ // other plugins
101
95
  'react-native-reanimated/plugin',
102
96
  ],
103
97
  };
104
98
  ```
105
99
 
106
- ### 4. Configure react-native-sound (recommended)
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 (development build):**
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
- After native dependency updates:
117
+ ### 5. Android keyboard mode (Expo)
143
118
 
144
- ```bash
145
- npx expo prebuild --clean
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 App() {
159
- const [messages, setMessages] = useState<Message[]>([]);
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
- <ChatScreen
181
- messages={messages}
182
- currentUserId={currentUserId}
183
- onSendMessage={handleSendMessage}
184
- placeholder="Type a message..."
185
- showAvatars
186
- showBubbleTail
187
- showMessageStatus
188
- showEmojiButton
189
- showAttachmentsButton
190
- showCameraButton
191
- showVoiceRecordButton
192
- />
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
- ## Message data model
180
+ ## Data model
181
+
182
+ ### Message
201
183
 
202
- Each item in `messages` must match the `Message` interface:
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"`) — you format this |
209
- | `status` | `'sent' \| 'delivered' \| 'read'` | Yes | Delivery state (shown only for current user) |
210
- | `text` | `string` | No | Plain text; URLs are auto-linked |
211
- | `image` | `string` | No | Image URI |
212
- | `video` | `string` | No | Video URI |
213
- | `audio` | `string` | No | Audio file URI |
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 + image), but typically you use one primary content type per bubble.
201
+ A message can combine fields (e.g. `text` + `mediaItems`).
218
202
 
219
- ```typescript
220
- import type { Message } from 'movius-chats/lib/typescript/types';
203
+ ### MessageMediaItem
221
204
 
222
- const example: Message = {
223
- id: '1',
224
- senderId: 'user-2',
225
- senderName: 'Alex',
226
- senderAvatar: 'https://example.com/avatar.jpg',
227
- text: 'Check this out https://example.com',
228
- time: '10:42 AM',
229
- status: 'read',
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`. Put the **newest message at index `0`** of the `messages` array:
242
+ `ChatScreen` uses an **inverted** `FlatList`. The **newest message must be at index `0`**:
238
243
 
239
- ```typescript
240
- setMessages((prev) => [newMessage, ...prev]); // correct
244
+ ```ts
245
+ setMessages((prev) => [newMessage, ...prev]); // correct
241
246
  ```
242
247
 
243
- Older messages sit at higher indices and appear higher on screen.
244
-
245
- **Grouping:** consecutive messages from the same sender share bubble styling; avatars and bubble tails show on the first message of a sequence (`isFirstInSequence`).
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`. It wraps your chat in `AudioProvider` + `ChatProvider` and renders the message list, typing indicator, input (or custom input), and full-screen media viewer.
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 | Messages to render (newest first) |
258
- | `currentUserId` | `string` | Yes | Logged-in user id; used for bubble alignment & status |
259
- | `onSendMessage` | `(msg: Omit<Message, 'id' \| 'time' \| 'status'>) => void` | Yes | Fired when user taps send with text and/or `previewData` |
260
- | `onMessageLongPress` | `(message: Message) => void` | No | Long-press on a bubble (reply, delete, etc.) |
261
- | `placeholder` | `string` | No | Input placeholder (default: `"Message"`) |
262
- | `keyboardVerticalOffset` | `number` | No | **iOS only:** header offset for `KeyboardAvoidingView`. Default: `0` |
263
- | `disableKeyboardAvoiding` | `boolean` | No | Set `true` if your screen already handles the keyboard |
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 flags below default to **falsy** (hidden) unless you pass `true`:
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 messages & typing row |
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 + checkmarks for sent messages |
275
- | `showEmojiButton` | Emoji button in input (UI only; wire your own picker) |
276
- | `showAttachmentsButton` | Paperclip calls `onAttachmentPress` |
277
- | `showCameraButton` | Camera icon when input is empty `onCameraPress` |
278
- | `showVoiceRecordButton` | Mic when input empty; send when text present |
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 has non-empty text |
285
- | `onTypingEnd` | `() => void` | Called when input is cleared |
286
- | `onAttachmentPress` | `() => void` | User tapped attachment — open document picker, etc. |
287
- | `onCameraPress` | `() => void` | User tapped camera — open camera / image picker |
288
- | `onAudioRecordStart` | `() => void` | Mic press / long-press start |
289
- | `onAudioRecordEnd` | `() => void` | Mic releaseupload recorded audio and append to messages |
290
- | `typingUsers` | `Array<{ id: string; avatar: string; name: string }>` | Users currently typing (excludes `currentUserId` in UI) |
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 releasedstop 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
- **Note:** Built-in `onSendMessage` from the default input only includes `{ text, senderId }`. Recording, camera, and file picking are **intentionally delegated** to your app via the callbacks above.
296
+ ### Attachment preview (composer)
293
297
 
294
- ### Attachment preview
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
- | `previewData` | `{ uri: string; type: string; name: string }` | MIME type in `type` (e.g. `image/jpeg`, `video/mp4`, `application/pdf`) |
301
- | `closePreview` | `() => void` | Clear preview when user taps X |
302
+ | `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 `previewData` is set, send is enabled even if text is empty. Your `onSendMessage` handler should read `previewData` from closure/state and attach `image`, `video`, or file metadata to the outgoing message.
309
+ When any preview is present, the send button appears regardless of text content.
304
310
 
305
- ### Theming
311
+ ### Theme
306
312
 
307
- Pass a `theme` object to customize colors, typography, and styles. All keys are optional.
313
+ All keys are optional. Pass a `theme` object to `ChatScreen`:
308
314
 
309
- ```typescript
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
- sizes?: {
330
- /** Twrnc classes (`"h-8 w-8"`) or pixels (`28`) for input-bar icons */
331
- inputIconSize?: string | number;
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
- Default bubble colors (before `theme` overrides): sent ≈ green (`bg-green-500`), received ≈ white.
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
- return (
392
- <ChatScreen
393
- theme={{ fontFamily: 'InterRegular' }}
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
- **React Native CLI:** follow [react-native custom fonts](https://reactnative.dev/docs/custom-fonts) and use the exact font family name registered on each platform.
395
+ ---
401
396
 
402
- #### Input icon size (`theme.sizes.inputIconSize`)
397
+ ## Usage examples
403
398
 
404
- Use either **pixels** (recommended) or **twrnc classes**:
399
+ ### Basic text chat
405
400
 
406
401
  ```tsx
407
402
  <ChatScreen
408
- theme={{
409
- sizes: {
410
- inputIconSize: 28, // 28×28 px emoji, attachment, camera only
411
- // inputIconSize: 'h-8 w-8', // alternative: tailwind classes via twrnc
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
- #### Keyboard avoiding
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
- 1. Wrap `ChatScreen` in a parent with `flex: 1` (e.g. `SafeAreaView style={{ flex: 1 }}`).
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
- ```tsx
426
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
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
- const insets = useSafeAreaInsets();
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
- <View style={{ flex: 1 }}>
431
- <ChatScreen
432
- keyboardVerticalOffset={Platform.OS === 'ios' ? insets.top + 44 : 0}
433
- // ...
434
- />
435
- </View>
436
- ```
437
-
438
- 3. **Android (Expo):** in `app.json`:
439
-
440
- ```json
441
- "android": {
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
- 4. If your navigator already uses `KeyboardAvoidingView`, pass `disableKeyboardAvoiding` to avoid double offset.
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
- onSendMessage={({ text, senderId }) => {
474
- addMessage({ text, senderId, status: 'sent' });
475
- }}
450
+ // These are equivalent for display purposes
451
+ { image: 'https://...' }
452
+ { mediaItems: [{ uri: 'https://...', kind: 'image' }] }
476
453
  ```
477
454
 
478
- ### Media messages
479
-
480
- Add URIs when building the `Message` object (usually after upload):
455
+ ### File attachment bubble
481
456
 
482
457
  ```tsx
483
- const imageMessage: Message = {
458
+ const message: Message = {
484
459
  id: '2',
485
- senderId: currentUserId,
486
- image: 'https://cdn.example.com/photo.jpg',
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
- const audioMessage: Message = {
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:05 AM',
483
+ time: '11:10 AM',
496
484
  status: 'read',
497
485
  };
498
486
  ```
499
487
 
500
- Tap image/video bubbles to open the built-in **MediaViewer** (pinch-zoom for images, native controls for video).
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
- ### Typing indicators
491
+ ### Composer attachment preview (single or multiple)
503
492
 
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
- ```
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
- import { launchImageLibrary } from 'react-native-image-picker';
496
+ const [previews, setPreviews] = useState<PreviewAttachment[]>([]);
523
497
 
524
498
  <ChatScreen
525
- showAttachmentsButton
526
- showCameraButton
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 result = await launchImageLibrary({ mediaType: 'mixed' });
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
- onCameraPress={async () => {
531
- // open camera, then add message with image/video URI
532
- }}
533
- onAudioRecordStart={() => {
534
- // start native recorder
535
- }}
536
- onAudioRecordEnd={async () => {
537
- // stop recorder, upload, then:
538
- // addMessage({ audio: uploadedUrl, senderId: currentUserId, ... });
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
- ### Attachment preview before send
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 [previewData, setPreviewData] = useState<{
547
- uri: string;
548
- type: string;
549
- name: string;
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
- previewData={previewData ?? undefined}
554
- closePreview={() => setPreviewData(null)}
555
- onAttachmentPress={async () => {
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: 'Inter-Regular',
584
+ fontFamily: 'Outfit-Regular',
589
585
  colors: {
590
586
  sentMessageTailColor: '#007AFF',
591
- receivedMessageTailColor: '#E9E9EB',
587
+ receivedMessageTailColor: '#E5E5EA',
592
588
  timestamp: '#8E8E93',
593
- readIconColor: '#34C759',
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: '#E9E9EB' },
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={() => <MyComposer />}
617
- // Still use messages / currentUserId / onSendMessage via your own state
683
+ renderCustomInput={() => <MyOwnComposer onSend={handleSend} />}
618
684
  />
619
685
  ```
620
686
 
621
- When using `renderCustomInput`, you are responsible for calling your send logic; the default `ChatInput` is not mounted.
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 actions
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
- ## TypeScript
735
+ ## Full-screen gallery viewer
635
736
 
636
- The main export is the default `ChatScreen` component. Types live in the build output:
737
+ Tapping any image or video in a bubble opens a full-screen modal viewer:
637
738
 
638
- ```typescript
639
- import ChatScreen from 'movius-chats';
640
- import type {
641
- Message,
642
- ChatScreenProps,
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
- `ChatScreenProps` is the full props interface for `ChatScreen`.
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 # one audio message plays at a time
655
- ├── ChatProvider # props, theme, media viewer state
656
- ├── FlatList (inverted) # ChatBubble per message
657
- │ └── ListHeaderComponent → TypingIndicator
658
- ├── ChatInput (optional) # text, icons, file preview
659
- └── MediaViewer (Modal) # full-screen image / video
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
- Internal pieces (not exported from the package entry, but useful when reading source):
773
+ ---
774
+
775
+ ## TypeScript types
663
776
 
664
- - **ChatBubble** layout, tail, avatar, `MessageContent`, `MessageStatus`
665
- - **MessageContent** — image, video thumbnail, `AudioPlayer`, parsed text
666
- - **AudioPlayer** — `react-native-sound` + Reanimated scrubber
667
- - **FilePreview** pre-send attachment chip above input
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
- | Issue | What to try |
674
- |-------|-------------|
675
- | `Native module not found` | Rebuild iOS/Android after install; run `pod install` on iOS |
676
- | Crashes in Expo Go | Use a development build; native modules are not in Expo Go |
677
- | Audio silent on iOS | Set `AVAudioSession` category to playback (see installation) |
678
- | Video/audio won’t load | Check URI scheme (`https://`, `file://`) and Android `INTERNET` permission |
679
- | Reanimated worklet errors | Ensure `react-native-reanimated/plugin` is **last** in Babel config |
680
- | Types not found | Import from `movius-chats/lib/typescript/types` |
681
- | Messages appear in wrong order | Newest item must be `messages[0]` (inverted list) |
682
- | Icons/buttons missing | Pass feature flags (`showEmojiButton`, etc.) they default to off |
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
- ## Contributing
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
- Issues and pull requests are welcome on [GitHub](https://github.com/David-Atueyi/Movius-Chats/issues).
840
+ If native dependencies changed, run `pod install` and rebuild the app.
689
841
 
690
842
  ---
691
843