@ethora/chat-component-rn 26.4.1 → 26.5.1

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/LICENSE +21 -661
  3. package/README.md +259 -7
  4. package/index.js +7 -0
  5. package/package.json +47 -16
  6. package/src/components/Header/HeaderRoomList.tsx +8 -7
  7. package/src/components/InputComponents/Search.tsx +7 -4
  8. package/src/components/InputComponents/TextInput.tsx +8 -1
  9. package/src/components/MainComponents/ChatRoom.tsx +16 -4
  10. package/src/components/MainComponents/ChatWrapper.tsx +10 -14
  11. package/src/components/MainComponents/LoginWrapper.tsx +5 -2
  12. package/src/components/MainComponents/MessageList.tsx +3 -20
  13. package/src/components/MainComponents/RoomList.tsx +7 -2
  14. package/src/components/MessageBubble/Message.tsx +7 -3
  15. package/src/components/MessageBubble/MessageInteractions.tsx +67 -63
  16. package/src/components/Modals/AttachSheet/AttachSheet.tsx +195 -0
  17. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +3 -0
  18. package/src/components/Modals/FilePreviewModal/PdfView.tsx +71 -0
  19. package/src/components/Modals/Modal/Modal.tsx +2 -4
  20. package/src/components/Modals/ModalSelectMedia/ModalSelectMedia.tsx +0 -2
  21. package/src/components/styled/Button.tsx +9 -0
  22. package/src/components/styled/SendInput.tsx +191 -107
  23. package/src/components/styled/StyledInputComponents/StyledInputComponents.tsx +1 -1
  24. package/src/context/xmppProvider.tsx +51 -2
  25. package/src/helpers/constants/MODAL_COMPONENTS.tsx +35 -0
  26. package/src/helpers/constants/MODAL_TYPES.ts +10 -39
  27. package/src/helpers/historyPreloadScheduler.ts +15 -1
  28. package/src/helpers/updateMessagesTillLast.tsx +20 -17
  29. package/src/hooks/useComposing.tsx +0 -9
  30. package/src/hooks/useLogout.tsx +129 -9
  31. package/src/hooks/useSendMessage.tsx +114 -60
  32. package/src/networking/api-requests/auth.api.ts +1 -1
  33. package/src/networking/api-requests/user.api.ts +1 -0
  34. package/src/networking/apiClient.ts +80 -41
  35. package/src/networking/stanzaHandlers.ts +18 -7
  36. package/src/networking/xmpp/flushLastViewedToPrivateStore.ts +86 -0
  37. package/src/networking/xmpp/getHistory.xmpp.ts +6 -1
  38. package/src/networking/xmpp/presenceInRoom.xmpp.ts +8 -3
  39. package/src/networking/xmpp/sendMediaMessage.xmpp.ts +8 -10
  40. package/src/networking/xmpp/sendTextMessage.xmpp.ts +12 -12
  41. package/src/networking/xmppClient.ts +27 -0
  42. package/src/roomStore/Middleware/logoutMiddleware.tsx +17 -8
  43. package/src/roomStore/Middleware/unreadMidlleware.tsx +122 -28
  44. package/src/roomStore/persistence.ts +5 -2
  45. package/src/roomStore/roomsSlice.ts +115 -7
  46. package/src/services/pushSubscriptionService.ts +25 -2
  47. package/src/types/types.ts +10 -9
  48. package/src/utils/devLogger.ts +11 -0
  49. package/src/api.config.ts +0 -34
package/README.md CHANGED
@@ -4,7 +4,20 @@ React Native chat UI + chat core for iOS and Android, powered by the Ethora plat
4
4
 
5
5
  **Part of the [Ethora SDK ecosystem](https://github.com/dappros/ethora#ecosystem)** — see all SDKs, tools, and sample apps. Follow cross-SDK updates in the [Release Notes](https://github.com/dappros/ethora/blob/main/RELEASE-NOTES.md).
6
6
 
7
- > Looking for the React.js (web) version? See [`@ethora/chat-component`](https://github.com/dappros/ethora-chat-component).
7
+ > Looking for the React.js (web) version? See [`@ethora/chat-component`](https://github.com/dappros/ethora-chat-component) (npm: [`@ethora/chat-component`](https://www.npmjs.com/package/@ethora/chat-component)).
8
+
9
+ ## Table of contents
10
+
11
+ - [What you get](#what-you-get)
12
+ - [Default backend endpoints](#default-backend-endpoints)
13
+ - [Install](#install)
14
+ - [Quick start](#quick-start)
15
+ - [Configuration](#configuration) — full `IConfig` reference in [instructions.md](instructions.md)
16
+ - [Authentication modes](#authentication-modes)
17
+ - [Pinning a single room](#pinning-a-single-room)
18
+ - [Quality & test coverage](#quality--test-coverage)
19
+ - [Local development](#local-development)
20
+ - [Changelog](CHANGELOG.md)
8
21
 
9
22
  ## What you get
10
23
 
@@ -75,6 +88,61 @@ export default function App() {
75
88
 
76
89
  Sign up at [app.chat.ethora.com/register](https://app.chat.ethora.com/register) to get an `appId` (and optionally an app token / JWT for backend integrations). For a guided setup that writes config files into your project, run `npx @ethora/setup`.
77
90
 
91
+ ## Configuration
92
+
93
+ `<XmppProvider>` and `<Chat>` accept the same `config` object (shape: [`IConfig`](src/types/types.ts)). Real integrations should put the network/auth fields on the provider (single source of truth for the XMPP socket) and the UI/behavior toggles on the chat:
94
+
95
+ ```tsx
96
+ const baseConfig = {
97
+ customAppToken: token || '',
98
+ baseUrl: backend.base_url,
99
+ xmppSettings: {
100
+ devServer: backend.dev_server,
101
+ host: backend.host,
102
+ conference: backend.conference,
103
+ },
104
+ jwtLogin: { enabled: true, token: token || '' },
105
+ refreshTokens: { enabled: true },
106
+ initBeforeLoad: true,
107
+ };
108
+
109
+ <XmppProvider config={baseConfig}>
110
+ <Chat
111
+ roomJID={room_jid}
112
+ config={{
113
+ ...baseConfig,
114
+ newArch: true,
115
+ disableInteractions: true,
116
+ disableChatInfo: {
117
+ disableHeader: false,
118
+ disableDescription: true,
119
+ disableType: true,
120
+ disableMembers: true,
121
+ disableChatHeaderMenu: true,
122
+ },
123
+ chatHeaderSettings: {
124
+ hide: false,
125
+ disableCreate: true,
126
+ disableMenu: true,
127
+ hideSearch: singleRoomMode,
128
+ },
129
+ clearStoreBeforeInit: true,
130
+ disableNewChatButton: true,
131
+ disableRoomConfig: true,
132
+ disableProfilesInteractions: true,
133
+ disableRoomMenu: singleRoomMode,
134
+ disableRooms: singleRoomMode,
135
+ enableRoomsRetry: {
136
+ enabled: true,
137
+ helperText: 'Initializing your messages…',
138
+ },
139
+ }}
140
+ />
141
+ </XmppProvider>
142
+ ```
143
+
144
+ The complete field-by-field reference — every option in `IConfig`, grouped by purpose, plus the single-init contract and a per-room single-chat recipe — lives in **[instructions.md](instructions.md)**. It mirrors the structure of the web package's [`@ethora/chat-component` README](https://www.npmjs.com/package/@ethora/chat-component).
145
+
78
146
  ## Authentication modes
79
147
 
80
148
  ```tsx
@@ -112,21 +180,205 @@ Sign up at [app.chat.ethora.com/register](https://app.chat.ethora.com/register)
112
180
  />
113
181
  ```
114
182
 
183
+ ## Quality & test coverage
184
+
185
+ ### Jest (unit + integration)
186
+
187
+ ```bash
188
+ npm test # ~2s, full suite
189
+ ```
190
+
191
+ 45 files, 486 tests cover the SDK's substantive surface:
192
+
193
+ | Layer | Coverage |
194
+ |--------------------------------|----------|
195
+ | Redux slices (rooms, chatSettings, roomHeap) | reducers + slice contracts |
196
+ | Middleware (unread, new-message, reactions, logout) | dispatch wiring + edge cases |
197
+ | XMPP client | constructor, state machine, reconnect backoff, disconnect, QoS / coalesced MAM, delegating helpers |
198
+ | XMPP stanza builders (~25 files) | exact wire shape via real `@xmpp/client` `xml()` |
199
+ | REST API wrappers | URL + body + headers + redux side effects, 60s cache, 401 refresh interceptor with queue-during-refresh |
200
+ | Persistence | AsyncStorage rehydrate, debounced writes, key filtering, room cap |
201
+ | Helpers | parseMessageBody (markdown render), markdownParser, insertMessageWithDelimiter, createMessageFromXml, ensureScopedChatCache, scheduler, etc. |
202
+ | L2 components | ChatRoomItem (unread badge), TextInput, DeletedMessage, MessageReply, MessageReaction |
203
+ | L3 / e2e (jest) | `appLoginChatsRn` 3-tab testbed, JWT-login + room mount |
204
+
205
+ ### Maestro (live backend)
206
+
207
+ ```bash
208
+ npm run e2e:ios # boots iPhone 16 sim, runs auth-and-send flow
209
+ npm run e2e:android # ditto Pixel_6
210
+ ```
211
+
212
+ Uses any profile in `~/.ethora/profiles.json` (the same file the
213
+ `@ethora/setup` CLI writes to). The runner logs the test user via
214
+ REST, seeds the testbed's AsyncStorage with the resolved Creds, and
215
+ exercises the full pipeline against a real tenant:
216
+
217
+ > REST login → AsyncStorage persisted Creds → app boot →
218
+ > `/chats/my` → XMPP WebSocket → MUC presence join → MAM history →
219
+ > chat thread rendered with input + send button visible.
220
+
221
+ If any link in that chain breaks, Maestro fails — making this a
222
+ single high-signal smoke for the most-likely class of regressions
223
+ (auth flow, XMPP transport, room hydration).
224
+
225
+ ### Live deep test
226
+ A documented session log + screenshots from a side-by-side
227
+ alice/iOS ↔ bob/Android run against chat-qa.ethora.com lives at
228
+ `docs/260517_deep-test-chat-qa.md`. Useful as a reproducer recipe.
229
+
230
+ ### Bugs surfaced + fixed (full history on PR #4)
231
+ The test pass surfaced a handful of latent bugs that had been
232
+ silently shipping. All fixed in the same branch:
233
+ - **`apiClient` interceptor**: five separate bugs that combined to
234
+ swallow auth errors and hang the refresh-on-401 path.
235
+ - **`unreadMiddleware`**: didn't filter own messages → MAM-replayed
236
+ own messages bumped the unread badge on re-login.
237
+ - **`logoutMiddleware`**: filtered on the redux store key instead
238
+ of the slice's action prefix → the XMPP disconnect event never
239
+ fired on logout.
240
+ - **`updateMessagesTillLast`**: imported two reducer exports that
241
+ don't exist → any call site crashed at runtime.
242
+ - **`historyPreloadScheduler`**: the "skip if already preloaded"
243
+ check was dead — the batch-loading dispatch overwrote the state
244
+ the check read.
245
+ - **`TextInput`**: `editable={isLoading}` was inverted vs convention.
246
+ - **`MODAL_TYPES`** require cycle (logged on every app boot).
247
+
115
248
  ## Local development
116
249
 
117
- This repo is bootstrapped with `@react-native-community/cli`. To run it as its own RN sample app rather than as a library:
250
+ This repo doubles as an Expo testbed app: `App.tsx` mounts
251
+ `AppLoginChatsRn`, a 3-tab (Setup / Chat / Logs) shell that drives the
252
+ SDK end-to-end via either paste-a-JWT or email + app-token login. Run
253
+ it against the canonical Ethora Cloud endpoints, your QA tenant, or a
254
+ self-hosted Ethora instance — all configurable from the Setup tab at
255
+ runtime.
256
+
257
+ ### Prerequisites
258
+
259
+ - Node.js 18+
260
+ - Xcode 15+ (iOS), Android Studio with a working AVD (Android)
261
+ - Java JDK 17+ — Android Studio's bundled JBR works:
262
+ `export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"`
263
+ - Android SDK with platform-tools on `$PATH`:
264
+ `export ANDROID_HOME="$HOME/Library/Android/sdk"`
265
+ - **CocoaPods 1.13+** for iOS. macOS system Ruby (2.6) is too old for
266
+ the bundled Gemfile — install via Homebrew: `brew install cocoapods`
267
+
268
+ ### Build + run
118
269
 
119
270
  ```bash
120
271
  git clone https://github.com/dappros/ethora-chat-component-rn.git
121
272
  cd ethora-chat-component-rn
122
- yarn install
273
+ npm install
274
+
275
+ # iOS
123
276
  cd ios && pod install && cd ..
124
- yarn start # Metro
125
- yarn ios # in another terminal
126
- yarn android # in another terminal
277
+ npx expo run:ios --device "iPhone 16"
278
+
279
+ # Android — write local.properties if expo prebuild didn't create it
280
+ echo "sdk.dir=$ANDROID_HOME" > android/local.properties
281
+ npx expo run:android
282
+ ```
283
+
284
+ The first `expo run:*` will `expo prebuild` to generate `ios/` and
285
+ `android/` from `app.json`. Both directories are gitignored — the
286
+ source of truth is `app.json` + the config plugins it lists.
287
+
288
+ This repo uses **npm** (not yarn). `package-lock.json` is the
289
+ canonical lockfile and `yarn.lock` is intentionally absent so
290
+ `expo run:*` defaults to `npm install` for its post-prebuild
291
+ reinstall step. If you prefer yarn for your own work, that's fine —
292
+ just don't commit a `yarn.lock` back into the repo.
293
+
294
+ ### Known first-run gotchas
295
+
296
+ - **iOS `pod install` fails on system Ruby**: the Gemfile resolves
297
+ to ffi >= 1.17, which needs Ruby 3.0+. Install CocoaPods via
298
+ `brew install cocoapods` (uses brew's bundled Ruby) and run
299
+ `pod install` directly — skip Bundler.
300
+ - **`expo run:android` errors with "SDK location not found"**: write
301
+ `android/local.properties` with `sdk.dir=$ANDROID_HOME` (the
302
+ Expo prebuild flow doesn't currently generate this).
303
+ - **`expo run:ios` crashes at the very end on osascript**: the CLI
304
+ tries to count Simulator processes via AppleScript and fails if
305
+ Terminal lacks Automation permission. The build succeeds and the
306
+ app is installed — grant permission via System Settings →
307
+ Privacy & Security → Automation, or just open the Simulator
308
+ manually before the build.
309
+ - **First-bundle ANR on Android emulator**: the debug bundle is
310
+ ~1500 modules and the cold JS eval can briefly trip the watchdog
311
+ on a fresh AVD. Tap "Wait" — the Setup tab will render. Release
312
+ builds (Hermes precompiled) don't show this.
313
+
314
+ ### `expo prebuild` and the `dependencies` guard
315
+
316
+ `npm run prebuild|ios|android` each chain through to `expo prebuild`
317
+ on first run, and `expo prebuild` likes to hoist `expo`, `react`,
318
+ and `react-native` from `devDependencies` into `dependencies`.
319
+ That's wrong for a published library — consumers of
320
+ `@ethora/chat-component-rn` would install a duplicate copy of React
321
+ and crash at runtime with the "two copies of React" reconciler
322
+ error.
323
+
324
+ To prevent the regression, those three npm scripts each invoke
325
+ `scripts/fix-prebuild-deps.js` right after, which surgically strips
326
+ the offending lines back out of `package.json` (preserving the rest
327
+ of the file byte-for-byte). You can also run it manually:
328
+
329
+ ```bash
330
+ npm run fix-prebuild-deps
127
331
  ```
128
332
 
129
- > Make sure you have completed the [React Native environment setup](https://reactnative.dev/docs/environment-setup) before proceeding.
333
+ It's idempotent — runs are silent when there's nothing to fix.
334
+
335
+ ### Tests
336
+
337
+ ```bash
338
+ npm test # jest, ~2s for the full suite
339
+ npm test -- --watch # watch mode
340
+ npm test -- some.test.ts # single file
341
+ ```
342
+
343
+ ### E2E (Maestro)
344
+
345
+ `e2e/auth-and-send.yaml` drives the full Setup → Email auth → Chat
346
+ tab → enter room → send message → assert it appears flow against a
347
+ real backend. Credentials come from any profile in
348
+ `~/.ethora/profiles.json` (the file the `@ethora/setup` CLI writes
349
+ to), so you don't have to hardcode anything.
350
+
351
+ Prerequisites:
352
+
353
+ ```bash
354
+ # Install Maestro
355
+ curl -fsSL https://get.maestro.mobile.dev | bash
356
+ export PATH="$PATH:$HOME/.maestro/bin"
357
+
358
+ # JDK 17+ — Android Studio's bundled JBR works; the runner script
359
+ # auto-detects it on macOS so you don't have to set JAVA_HOME.
360
+ ```
361
+
362
+ Run against an already-built + installed app + a booted simulator:
363
+
364
+ ```bash
365
+ # Default profile "mychatapp QA", room "Main chat"
366
+ npm run e2e:ios
367
+ npm run e2e:android
368
+
369
+ # Or pass a different profile / room
370
+ scripts/run-e2e.sh ios "Vitall Dev2" "General"
371
+ ```
372
+
373
+ The flow targets stable `testID`s wired into the SDK
374
+ (`chat-message-input`, `chat-send-button`, plus `room-<jid-local>`
375
+ on each room row) — please keep those identifiers stable for
376
+ downstream e2e drivers (Detox, Appium) that rely on the same
377
+ contracts.
378
+
379
+ > Already have your RN environment set up? See the
380
+ > [React Native environment setup](https://reactnative.dev/docs/environment-setup)
381
+ > doc if any of the above feels unfamiliar.
130
382
 
131
383
  ## Related
132
384
 
package/index.js CHANGED
@@ -7,6 +7,13 @@
7
7
  */
8
8
 
9
9
  import 'react-native-gesture-handler';
10
+ // Polyfill `crypto.getRandomValues()` before anything that pulls in
11
+ // uuid — RN's JS runtime doesn't ship one, and uuid throws
12
+ // "crypto.getRandomValues() not supported" on the first send.
13
+ // The library entry (src/main.ts) loads this too, but the testbed
14
+ // here mounts the chat directly without going through that entry, so
15
+ // we need it again at the testbed root.
16
+ import 'react-native-get-random-values';
10
17
  import { registerRootComponent } from 'expo';
11
18
  import App from './App';
12
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethora/chat-component-rn",
3
- "version": "26.04.01",
3
+ "version": "26.05.01",
4
4
  "description": "Drop-in XMPP chat component for React Native / Expo. <Chat config={...}/> and you're done.",
5
5
  "main": "index.js",
6
6
  "react-native": "src/main.ts",
@@ -10,38 +10,60 @@
10
10
  "src",
11
11
  "metro.js",
12
12
  "empty-shim.js",
13
- "README.md"
13
+ "README.md",
14
+ "CHANGELOG.md"
14
15
  ],
15
16
  "peerDependencies": {
17
+ "@react-native-async-storage/async-storage": ">=1.21.0",
16
18
  "react": ">=18.0.0",
17
19
  "react-native": ">=0.73.0",
18
- "react-native-svg": ">=14.0.0",
19
- "react-native-reanimated": ">=3.0.0",
20
- "@react-native-async-storage/async-storage": ">=1.21.0",
20
+ "react-native-gesture-handler": ">=2.0.0",
21
21
  "react-native-get-random-values": ">=1.10.0",
22
- "react-native-gesture-handler": ">=2.0.0"
22
+ "react-native-reanimated": ">=3.0.0",
23
+ "react-native-svg": ">=14.0.0"
23
24
  },
24
25
  "peerDependenciesMeta": {
25
- "react-native-svg": { "optional": false },
26
- "react-native-reanimated": { "optional": false },
27
- "@react-native-async-storage/async-storage": { "optional": false },
28
- "react-native-get-random-values": { "optional": false },
29
- "react-native-gesture-handler": { "optional": true }
26
+ "react-native-svg": {
27
+ "optional": false
28
+ },
29
+ "react-native-reanimated": {
30
+ "optional": false
31
+ },
32
+ "@react-native-async-storage/async-storage": {
33
+ "optional": false
34
+ },
35
+ "react-native-get-random-values": {
36
+ "optional": false
37
+ },
38
+ "react-native-gesture-handler": {
39
+ "optional": true
40
+ }
30
41
  },
31
42
  "scripts": {
43
+ "postinstall": "patch-package",
32
44
  "start": "expo start",
33
- "android": "expo run:android",
34
- "ios": "expo run:ios",
45
+ "android": "node scripts/ensure-emulator.js && (expo run:android; node scripts/fix-prebuild-deps.js)",
46
+ "ios": "expo run:ios; node scripts/fix-prebuild-deps.js",
35
47
  "web": "expo start --web",
36
- "prebuild": "expo prebuild",
48
+ "prebuild": "expo prebuild; node scripts/fix-prebuild-deps.js",
49
+ "fix-prebuild-deps": "node scripts/fix-prebuild-deps.js",
37
50
  "lint": "eslint .",
38
- "test": "jest"
51
+ "test": "jest",
52
+ "e2e": "scripts/run-e2e.sh",
53
+ "e2e:ios": "scripts/run-e2e.sh ios",
54
+ "e2e:android": "scripts/run-e2e.sh android"
39
55
  },
40
56
  "dependencies": {
41
57
  "@reduxjs/toolkit": "^2.5.0",
42
58
  "@xmpp/client": "^0.13.1",
43
59
  "axios": "^1.7.9",
60
+ "expo-clipboard": "~8.0.8",
61
+ "expo-document-picker": "~14.0.0",
62
+ "expo-image-manipulator": "~14.0.0",
63
+ "expo-image-picker": "~17.0.0",
44
64
  "js-sha256": "^0.11.0",
65
+ "react-native-keyboard-controller": "^1.18.5",
66
+ "react-native-webview": "^13.16.1",
45
67
  "react-redux": "^9.2.0",
46
68
  "styled-components": "^6.1.13"
47
69
  },
@@ -60,6 +82,7 @@
60
82
  "expo-build-properties": "^55.0.14",
61
83
  "expo-status-bar": "~3.0.0",
62
84
  "jest": "^29.6.3",
85
+ "patch-package": "^8.0.1",
63
86
  "jest-expo": "~54.0.0",
64
87
  "prettier": "2.8.8",
65
88
  "react": "19.1.0",
@@ -79,7 +102,15 @@
79
102
  "node": ">=18"
80
103
  },
81
104
  "author": "Ethora",
82
- "license": "AGPL-3.0-only",
105
+ "homepage": "https://playground.chat.ethora.com",
106
+ "repository": {
107
+ "type": "git",
108
+ "url": "git+https://github.com/dappros/ethora-chat-component-rn.git"
109
+ },
110
+ "bugs": {
111
+ "url": "https://github.com/dappros/ethora-chat-component-rn/issues"
112
+ },
113
+ "license": "MIT",
83
114
  "keywords": [
84
115
  "chat",
85
116
  "messaging",
@@ -74,11 +74,10 @@ const styles = StyleSheet.create({
74
74
  headerContainer: {
75
75
  flexDirection: 'row',
76
76
  alignItems: 'center',
77
- paddingHorizontal: 8,
78
- paddingVertical: 2,
79
- paddingTop: 62, // 60px top padding + 2px for header content
80
- maxHeight: 24,
81
- minHeight: 24,
77
+ paddingHorizontal: 16,
78
+ paddingTop: 16,
79
+ paddingBottom: 8,
80
+ minHeight: 56,
82
81
  justifyContent: 'space-between',
83
82
  },
84
83
  menuButton: {
@@ -87,8 +86,9 @@ const styles = StyleSheet.create({
87
86
  backgroundColor: 'transparent',
88
87
  },
89
88
  leftContainer: {
90
- flex: 1,
89
+ width: 40,
91
90
  alignItems: 'flex-start',
91
+ justifyContent: 'center',
92
92
  },
93
93
  centerContainer: {
94
94
  position: 'absolute',
@@ -98,7 +98,8 @@ const styles = StyleSheet.create({
98
98
  justifyContent: 'center',
99
99
  },
100
100
  rightContainer: {
101
- flex: 1,
101
+ width: 40,
102
102
  alignItems: 'flex-end',
103
+ justifyContent: 'center',
103
104
  },
104
105
  });
@@ -20,9 +20,9 @@ const SearchInputWrapper = styled.View.withConfig({ shouldForwardProp })<{
20
20
  align-items: center;
21
21
  background-color: #fff;
22
22
  border-radius: 16px;
23
- height: 48px;
24
- padding: 0 16px;
25
- margin: 0 6px 16px 6px;
23
+ height: 44px;
24
+ padding: 0 12px;
25
+ margin: 0 0 12px 0;
26
26
  flex: 1;
27
27
  ${({ animated, expanded }) =>
28
28
  animated &&
@@ -36,7 +36,10 @@ const SearchIcon = styled(TouchableOpacity)<{
36
36
  animated?: boolean;
37
37
  expanded?: boolean;
38
38
  }>`
39
- padding: 3.5px;
39
+ padding: 0;
40
+ margin-right: 8px;
41
+ align-items: center;
42
+ justify-content: center;
40
43
  color: #999;
41
44
  `;
42
45
 
@@ -42,6 +42,11 @@ const TextInput: React.FC<TextInputProps> = ({
42
42
  return (
43
43
  <>
44
44
  <MessageInput
45
+ // Stable testIDs so Maestro / Detox / Appium can drive the
46
+ // chat-send flow reliably. accessibilityLabel mirrors the
47
+ // testID so iOS accessibility-id lookups also work.
48
+ testID="chat-message-input"
49
+ accessibilityLabel="chat-message-input"
45
50
  color={config?.colors?.primary}
46
51
  placeholder="Type message"
47
52
  value={message}
@@ -49,9 +54,11 @@ const TextInput: React.FC<TextInputProps> = ({
49
54
  onSubmitEditing={handleSendClick}
50
55
  onFocus={onFocus}
51
56
  onBlur={onBlur}
52
- editable={isLoading}
57
+ editable={!isLoading}
53
58
  />
54
59
  <Button
60
+ testID="chat-send-button"
61
+ accessibilityLabel="chat-send-button"
55
62
  onPress={handleSendClick}
56
63
  EndIcon={<SendIcon color={!message ? '#D4D4D8' : '#fff'} />}
57
64
  style={{
@@ -28,13 +28,13 @@ import CustomTypingIndicator from '../styled/StyledInputComponents/CustomTypingI
28
28
  // import {PanGestureHandler} from 'react-native-gesture-handler';
29
29
  import { FlatList } from 'react-native';
30
30
  import {
31
- KeyboardAvoidingView,
32
31
  Platform,
33
32
  TouchableWithoutFeedback,
34
33
  Keyboard,
35
34
  View,
36
35
  KeyboardEvent,
37
36
  } from 'react-native';
37
+ import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
38
38
  import useComposing from '../../hooks/useComposing';
39
39
 
40
40
  interface ChatRoomProps {
@@ -112,7 +112,10 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
112
112
 
113
113
  const sendMedia = useCallback(
114
114
  (data: any, type: string) => {
115
- sendMessageMedia(data, type, activeRoomJID || '');
115
+ // Return the promise so callers can sequence the follow-up
116
+ // text send AFTER the upload finishes (consumer expectation:
117
+ // media first, text second — never interleaved).
118
+ return sendMessageMedia(data, type, activeRoomJID || '');
116
119
  },
117
120
  [activeRoomJID],
118
121
  );
@@ -208,7 +211,16 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
208
211
  }
209
212
 
210
213
  return (
211
- <View style={{ flex: 1 }}>
214
+ <KeyboardAvoidingView
215
+ style={{ flex: 1 }}
216
+ // react-native-keyboard-controller drives a frame-perfect lift
217
+ // via Reanimated worklets on BOTH platforms, so `padding`
218
+ // behavior is correct everywhere — unlike RN's stock KAV
219
+ // which would double-handle against AndroidManifest's
220
+ // `adjustResize` and jitter.
221
+ behavior="padding"
222
+ keyboardVerticalOffset={configWithEventHandlers?.keyboardVerticalOffset ?? 0}
223
+ >
212
224
  <ChatContainer
213
225
  style={
214
226
  configWithEventHandlers?.chatRoomStyles
@@ -384,7 +396,7 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
384
396
  />
385
397
  )}
386
398
  </ChatContainer>
387
- </View>
399
+ </KeyboardAvoidingView>
388
400
  );
389
401
  },
390
402
  );
@@ -274,12 +274,6 @@ const ChatWrapper: FC<ChatWrapperProps> = ({
274
274
  }}>
275
275
  <ChatRoom
276
276
  CustomMessageComponent={CustomMessageComponent || Message}
277
- // Only show the back button when there's a room list to
278
- // return to. In single-room mode (`roomJID` preselected
279
- // by the consumer) or when the consumer disabled the
280
- // list (`disableRooms`), there's nowhere to go back to,
281
- // so we omit the handler and ChatHeader renders without
282
- // the back arrow.
283
277
  handleBackClick={
284
278
  roomJID || config?.disableRooms
285
279
  ? undefined
@@ -290,20 +284,22 @@ const ChatWrapper: FC<ChatWrapperProps> = ({
290
284
  />
291
285
  </ChatWrapperBox>
292
286
  )}
293
- {/*
294
- Mount the active-modal renderer so ChatHeader's onPress
295
- (dispatch setActiveModal(CHAT_PROFILE)) actually surfaces
296
- a modal. Without this, tapping the header was a no-op:
297
- redux state flipped but nothing was listening. Web's
298
- ChatWrapper mounts the same component (web ChatWrapper
299
- lines 331-336).
300
- */}
301
287
  <Modal
302
288
  modal={activeModal}
303
289
  setOpenModal={(value?: ModalType) =>
304
290
  dispatch(setActiveModal(value))
305
291
  }
306
292
  />
293
+ {deleteModal?.isDeleteModal && (
294
+ <ModalWrapper
295
+ title="Delete Message"
296
+ description="Are you sure you want to delete this message?"
297
+ buttonText="Delete"
298
+ backgroundColorButton="#E53935"
299
+ handleClick={handleDeleteClick}
300
+ handleCloseModal={handleCloseDeleteModal}
301
+ />
302
+ )}
307
303
  </ChatWrapperBox>
308
304
  ) : (
309
305
  <StyledLoaderWrapper>
@@ -24,9 +24,12 @@ const LoginWrapper: React.FC<LoginWrapperProps> = ({...props}) => {
24
24
 
25
25
  const loginUserFunction = useCallback(async () => {
26
26
  try {
27
+ // Per product-code-policy: no compiled-in dev credentials. If the
28
+ // consumer didn't pass `user.email` / `user.password`, the call
29
+ // below fails on empty strings, hits catch, and returns null.
27
30
  const authData = await loginEmail(
28
- props?.user?.email || 'yukiraze9@gmail.com',
29
- props?.user?.password || 'Qwerty123',
31
+ props?.user?.email || '',
32
+ props?.user?.password || '',
30
33
  );
31
34
 
32
35
  return {
@@ -126,36 +126,25 @@ const MessageList = <TMessage extends IMessage>({
126
126
  // even mid-stream — so the user sees them appear from oldest to
127
127
  // newest with no swap animation.
128
128
  const sorted = filtered.slice().sort((a, b) => {
129
- // Pull a numeric timestamp out of the id (server stamps a
130
- // microsecond timestamp into the stanza-id) — fallback to the
131
- // `date` ISO string when id isn't numeric (e.g. delimiter rows).
132
- const aNum = Number(a?.id) || new Date(a?.date as any).getTime() || 0;
133
- const bNum = Number(b?.id) || new Date(b?.date as any).getTime() || 0;
129
+ const aNum = new Date(a?.date as any).getTime() || 0;
130
+ const bNum = new Date(b?.date as any).getTime() || 0;
134
131
  return aNum - bNum;
135
132
  });
136
133
  return sorted;
137
134
  }, [addReplyMessages, isReply, roomJID, activeMessage]);
138
135
 
139
136
  const handleLoadMore = useCallback(async () => {
140
- console.log('🟠 [MessageList] handleLoadMore fired', {
141
- loading,
142
- isLoadingMore,
143
- msgCount: memoizedMessages.length,
144
- isScrollBlocked,
145
- });
146
137
  if (
147
138
  loading ||
148
139
  isLoadingMore ||
149
140
  !memoizedMessages.length ||
150
141
  isScrollBlocked
151
142
  ) {
152
- console.log('🟠 [MessageList] handleLoadMore gated out');
153
143
  return;
154
144
  }
155
145
 
156
146
  const oldestMessage = memoizedMessages[0];
157
147
  if (!oldestMessage || !oldestMessage.id) {
158
- console.log('🟠 [MessageList] no oldest msg');
159
148
  return;
160
149
  }
161
150
 
@@ -163,19 +152,13 @@ const MessageList = <TMessage extends IMessage>({
163
152
  setIsScrollBlocked(true);
164
153
 
165
154
  try {
166
- console.log('🟠 [MessageList] calling loadMoreMessages', {
167
- room: oldestMessage.roomJid || roomJID,
168
- beforeId: Number(oldestMessage.id),
169
- oldestId: oldestMessage.id,
170
- });
171
155
  await loadMoreMessages(
172
156
  oldestMessage.roomJid || roomJID,
173
157
  15,
174
158
  Number(oldestMessage.id)
175
159
  );
176
- console.log('🟠 [MessageList] loadMoreMessages returned');
177
160
  } catch (error) {
178
- console.error('🔴 [MessageList] loadMoreMessages threw:', error);
161
+ console.error(error);
179
162
  } finally {
180
163
  setTimeout(() => {
181
164
  setIsLoadingMore(false);