movius-chats 1.3.7 → 1.3.8

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