@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.
- package/CHANGELOG.md +79 -0
- package/LICENSE +21 -661
- package/README.md +259 -7
- package/index.js +7 -0
- package/package.json +47 -16
- package/src/components/Header/HeaderRoomList.tsx +8 -7
- package/src/components/InputComponents/Search.tsx +7 -4
- package/src/components/InputComponents/TextInput.tsx +8 -1
- package/src/components/MainComponents/ChatRoom.tsx +16 -4
- package/src/components/MainComponents/ChatWrapper.tsx +10 -14
- package/src/components/MainComponents/LoginWrapper.tsx +5 -2
- package/src/components/MainComponents/MessageList.tsx +3 -20
- package/src/components/MainComponents/RoomList.tsx +7 -2
- package/src/components/MessageBubble/Message.tsx +7 -3
- package/src/components/MessageBubble/MessageInteractions.tsx +67 -63
- package/src/components/Modals/AttachSheet/AttachSheet.tsx +195 -0
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +3 -0
- package/src/components/Modals/FilePreviewModal/PdfView.tsx +71 -0
- package/src/components/Modals/Modal/Modal.tsx +2 -4
- package/src/components/Modals/ModalSelectMedia/ModalSelectMedia.tsx +0 -2
- package/src/components/styled/Button.tsx +9 -0
- package/src/components/styled/SendInput.tsx +191 -107
- package/src/components/styled/StyledInputComponents/StyledInputComponents.tsx +1 -1
- package/src/context/xmppProvider.tsx +51 -2
- package/src/helpers/constants/MODAL_COMPONENTS.tsx +35 -0
- package/src/helpers/constants/MODAL_TYPES.ts +10 -39
- package/src/helpers/historyPreloadScheduler.ts +15 -1
- package/src/helpers/updateMessagesTillLast.tsx +20 -17
- package/src/hooks/useComposing.tsx +0 -9
- package/src/hooks/useLogout.tsx +129 -9
- package/src/hooks/useSendMessage.tsx +114 -60
- package/src/networking/api-requests/auth.api.ts +1 -1
- package/src/networking/api-requests/user.api.ts +1 -0
- package/src/networking/apiClient.ts +80 -41
- package/src/networking/stanzaHandlers.ts +18 -7
- package/src/networking/xmpp/flushLastViewedToPrivateStore.ts +86 -0
- package/src/networking/xmpp/getHistory.xmpp.ts +6 -1
- package/src/networking/xmpp/presenceInRoom.xmpp.ts +8 -3
- package/src/networking/xmpp/sendMediaMessage.xmpp.ts +8 -10
- package/src/networking/xmpp/sendTextMessage.xmpp.ts +12 -12
- package/src/networking/xmppClient.ts +27 -0
- package/src/roomStore/Middleware/logoutMiddleware.tsx +17 -8
- package/src/roomStore/Middleware/unreadMidlleware.tsx +122 -28
- package/src/roomStore/persistence.ts +5 -2
- package/src/roomStore/roomsSlice.ts +115 -7
- package/src/services/pushSubscriptionService.ts +25 -2
- package/src/types/types.ts +10 -9
- package/src/utils/devLogger.ts +11 -0
- 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
|
|
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
|
-
|
|
273
|
+
npm install
|
|
274
|
+
|
|
275
|
+
# iOS
|
|
123
276
|
cd ios && pod install && cd ..
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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-
|
|
22
|
+
"react-native-reanimated": ">=3.0.0",
|
|
23
|
+
"react-native-svg": ">=14.0.0"
|
|
23
24
|
},
|
|
24
25
|
"peerDependenciesMeta": {
|
|
25
|
-
"react-native-svg": {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"react-native-
|
|
29
|
-
|
|
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
|
-
"
|
|
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:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
24
|
-
padding: 0
|
|
25
|
-
margin: 0
|
|
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:
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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 || '
|
|
29
|
-
props?.user?.password || '
|
|
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
|
-
|
|
130
|
-
|
|
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(
|
|
161
|
+
console.error(error);
|
|
179
162
|
} finally {
|
|
180
163
|
setTimeout(() => {
|
|
181
164
|
setIsLoadingMore(false);
|