@ethora/chat-component-rn 26.4.1 → 26.4.2
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 +42 -0
- package/LICENSE +21 -661
- package/README.md +253 -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/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 +74 -38
- 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/types/types.ts +10 -9
- package/src/utils/devLogger.ts +11 -0
|
@@ -2,6 +2,25 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
|
2
2
|
import { EditAction, HistoryPreloadState, IMessage, IRoom } from '../types/types';
|
|
3
3
|
import { insertMessageWithDelimiter } from '../helpers/insertMessageWithDelimiter';
|
|
4
4
|
|
|
5
|
+
// Per-room runtime message cap. Mirrors the persistence layer's
|
|
6
|
+
// MESSAGE_LIMIT so what's in memory matches what's on disk; otherwise
|
|
7
|
+
// long sessions grow the array unbounded (memory leak) and reload
|
|
8
|
+
// shows fewer messages than the user saw last in the session.
|
|
9
|
+
//
|
|
10
|
+
// The cap fires on the APPEND path (new messages arriving) — paginated
|
|
11
|
+
// older history added via unshift/splice is exempt, so the user can
|
|
12
|
+
// page backward arbitrarily far without instantly losing what they
|
|
13
|
+
// just fetched. After cap eviction we drop from the head (oldest).
|
|
14
|
+
const RUNTIME_MESSAGE_LIMIT = 100;
|
|
15
|
+
|
|
16
|
+
const enforceMessageCap = (messages: IMessage[]): void => {
|
|
17
|
+
// Trim oldest until we're at/under the limit. Mutates in place
|
|
18
|
+
// (immer-compatible inside reducers).
|
|
19
|
+
while (messages.length > RUNTIME_MESSAGE_LIMIT) {
|
|
20
|
+
messages.shift();
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
5
24
|
export interface RoomPreloadPatch {
|
|
6
25
|
jid: string;
|
|
7
26
|
messages?: IMessage[];
|
|
@@ -37,7 +56,18 @@ export const roomsStore = createSlice({
|
|
|
37
56
|
reducers: {
|
|
38
57
|
addRoom(state, action: PayloadAction<{ roomData: IRoom }>) {
|
|
39
58
|
const { roomData } = action.payload;
|
|
40
|
-
state.rooms[roomData.jid]
|
|
59
|
+
const existing = state.rooms[roomData.jid];
|
|
60
|
+
// Preserve a pre-existing lastViewedTimestamp (the private-store
|
|
61
|
+
// pull may have landed before /chats/my finished). If neither
|
|
62
|
+
// source set one, stamp `Date.now()` so initial REST history
|
|
63
|
+
// counts as "seen" — without this default, every back-history
|
|
64
|
+
// message read by the unread middleware satisfies `>0` and the
|
|
65
|
+
// badge shows the entire room population on first paint.
|
|
66
|
+
const lastViewed =
|
|
67
|
+
roomData.lastViewedTimestamp ??
|
|
68
|
+
existing?.lastViewedTimestamp ??
|
|
69
|
+
Date.now();
|
|
70
|
+
state.rooms[roomData.jid] = { ...roomData, lastViewedTimestamp: lastViewed };
|
|
41
71
|
},
|
|
42
72
|
deleteRoom(state, action: PayloadAction<{ jid: string }>) {
|
|
43
73
|
const { jid } = action.payload;
|
|
@@ -65,7 +95,14 @@ export const roomsStore = createSlice({
|
|
|
65
95
|
) {
|
|
66
96
|
const { roomJID, messages } = action.payload;
|
|
67
97
|
if (state.rooms[roomJID]) {
|
|
68
|
-
|
|
98
|
+
// Cap to the runtime limit on replace too — guards against a
|
|
99
|
+
// single MAM page returning more than the limit (would balloon
|
|
100
|
+
// the array on its own).
|
|
101
|
+
const capped =
|
|
102
|
+
messages.length > RUNTIME_MESSAGE_LIMIT
|
|
103
|
+
? messages.slice(-RUNTIME_MESSAGE_LIMIT)
|
|
104
|
+
: messages;
|
|
105
|
+
state.rooms[roomJID].messages = capped;
|
|
69
106
|
}
|
|
70
107
|
},
|
|
71
108
|
deleteRoomMessage(
|
|
@@ -120,11 +157,22 @@ export const roomsStore = createSlice({
|
|
|
120
157
|
) {
|
|
121
158
|
const { roomJID, message, start } = action.payload;
|
|
122
159
|
|
|
123
|
-
|
|
160
|
+
// Guard against the "stanza arrived before /chats/my completed"
|
|
161
|
+
// race — without this, the optional-chain `?.messages` falls
|
|
162
|
+
// back to undefined, and the assignment below tries to set
|
|
163
|
+
// `.messages` on undefined → TypeError. Caller can safely
|
|
164
|
+
// ignore the message; the room will get its messages array on
|
|
165
|
+
// `addRoom` when the API response lands.
|
|
166
|
+
if (!state.rooms[roomJID]) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!state.rooms[roomJID].messages) {
|
|
124
171
|
state.rooms[roomJID].messages = [];
|
|
125
172
|
}
|
|
126
173
|
|
|
127
174
|
const roomMessages = state.rooms[roomJID].messages;
|
|
175
|
+
const lengthBefore = roomMessages.length;
|
|
128
176
|
|
|
129
177
|
if (roomMessages.length === 0 || start) {
|
|
130
178
|
roomMessages.unshift(message);
|
|
@@ -135,6 +183,14 @@ export const roomsStore = createSlice({
|
|
|
135
183
|
|
|
136
184
|
insertMessageWithDelimiter(roomMessages, message, lastViewedTimestamp);
|
|
137
185
|
}
|
|
186
|
+
|
|
187
|
+
// Apply the in-memory cap only when the array actually GREW
|
|
188
|
+
// (i.e. this wasn't a dedupe/merge that left length unchanged).
|
|
189
|
+
// Otherwise repeated echoes of the same message would chip away
|
|
190
|
+
// at the oldest history for no reason.
|
|
191
|
+
if (roomMessages.length > lengthBefore) {
|
|
192
|
+
enforceMessageCap(roomMessages);
|
|
193
|
+
}
|
|
138
194
|
},
|
|
139
195
|
deleteAllRooms(state) {
|
|
140
196
|
state.rooms = {};
|
|
@@ -309,17 +365,69 @@ export const roomsStore = createSlice({
|
|
|
309
365
|
});
|
|
310
366
|
|
|
311
367
|
// Count messages strictly newer than the given millisecond timestamp.
|
|
312
|
-
// Uses `msg.
|
|
313
|
-
//
|
|
368
|
+
// Uses `msg.id` (server-authoritative microsecond timestamp prefixed by
|
|
369
|
+
// 13-digit millis — see helpers/dateComparison `getHighResolutionTimestamp`)
|
|
370
|
+
// because `msg.date` can be derived client-side (createMessageFromXml
|
|
371
|
+
// falls back to `Date.now()` for realtime stanzas without a `date`
|
|
372
|
+
// attr), which makes the comparison drift vs what the server assigned.
|
|
373
|
+
// Excludes the "delimiter-new" sentinel, pending sends, and the current
|
|
374
|
+
// user's own messages (parity with unreadMiddleware's isOwnMessage
|
|
375
|
+
// filter — without this, the reducer and the middleware disagree about
|
|
376
|
+
// the count and we get a flicker as the badge gets written twice with
|
|
377
|
+
// different values on every message).
|
|
378
|
+
export const msgSortableMs = (msg: any): number => {
|
|
379
|
+
const id = String(msg?.id || '');
|
|
380
|
+
const m = /^(\d{13})/.exec(id);
|
|
381
|
+
if (m) {return Number(m[1]);}
|
|
382
|
+
if (msg?.date) {
|
|
383
|
+
const t = new Date(msg.date as any).getTime();
|
|
384
|
+
if (Number.isFinite(t)) {return t;}
|
|
385
|
+
}
|
|
386
|
+
return 0;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const norm = (s: any): string => {
|
|
390
|
+
if (s == null) {return '';}
|
|
391
|
+
let v = String(s).toLowerCase();
|
|
392
|
+
v = v.split('/')[0];
|
|
393
|
+
v = v.split('@')[0];
|
|
394
|
+
return v.replace(/_/g, '');
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const isOwn = (
|
|
398
|
+
msg: IMessage,
|
|
399
|
+
selfXmpp: string,
|
|
400
|
+
selfWallet: string
|
|
401
|
+
): boolean => {
|
|
402
|
+
if (!selfXmpp && !selfWallet) {return false;}
|
|
403
|
+
const candidates = [
|
|
404
|
+
norm((msg as any)?.user?.id),
|
|
405
|
+
norm((msg as any)?.user?.userJID),
|
|
406
|
+
norm((msg as any)?.user?.xmppUsername),
|
|
407
|
+
norm((msg as any)?.xmppFrom),
|
|
408
|
+
].filter(Boolean);
|
|
409
|
+
if (candidates.length === 0) {return false;}
|
|
410
|
+
const self = new Set(
|
|
411
|
+
[norm(selfXmpp), norm(selfWallet)].filter(Boolean)
|
|
412
|
+
);
|
|
413
|
+
for (const c of candidates) {
|
|
414
|
+
if (self.has(c)) {return true;}
|
|
415
|
+
}
|
|
416
|
+
return false;
|
|
417
|
+
};
|
|
418
|
+
|
|
314
419
|
const countNewerMessages = (
|
|
315
420
|
messages: IMessage[],
|
|
316
|
-
timestamp: number
|
|
421
|
+
timestamp: number,
|
|
422
|
+
selfXmpp: string = '',
|
|
423
|
+
selfWallet: string = ''
|
|
317
424
|
): number => {
|
|
318
425
|
if (!messages?.length || !timestamp) {return 0;}
|
|
319
426
|
let count = 0;
|
|
320
427
|
for (const message of messages) {
|
|
321
428
|
if (!message || message.id === 'delimiter-new' || message.pending) {continue;}
|
|
322
|
-
|
|
429
|
+
if (isOwn(message, selfXmpp, selfWallet)) {continue;}
|
|
430
|
+
const ms = msgSortableMs(message);
|
|
323
431
|
if (Number.isFinite(ms) && ms > timestamp) {count += 1;}
|
|
324
432
|
}
|
|
325
433
|
return count;
|
package/src/types/types.ts
CHANGED
|
@@ -60,6 +60,11 @@ export interface IRoom {
|
|
|
60
60
|
createdAt?: string | number | Date;
|
|
61
61
|
updatedAt?: string | number | Date;
|
|
62
62
|
|
|
63
|
+
// REST-side metadata populated by `/chats/my` hydration. Surfaced
|
|
64
|
+
// in ChatProfileModal under Description / Chat type fields.
|
|
65
|
+
description?: string;
|
|
66
|
+
type?: string;
|
|
67
|
+
|
|
63
68
|
roomMembers?: RoomMember[];
|
|
64
69
|
|
|
65
70
|
// QoS / preload state
|
|
@@ -349,6 +354,8 @@ export interface IConfig {
|
|
|
349
354
|
roomListStyles?: ViewStyle;
|
|
350
355
|
chatRoomStyles?: ViewStyle;
|
|
351
356
|
|
|
357
|
+
keyboardVerticalOffset?: number;
|
|
358
|
+
|
|
352
359
|
// ----- interactions / messages -----
|
|
353
360
|
disableInteractions?: boolean;
|
|
354
361
|
disableReactions?: boolean;
|
|
@@ -356,15 +363,9 @@ export interface IConfig {
|
|
|
356
363
|
disableUserCount?: boolean;
|
|
357
364
|
disableSentLogic?: boolean;
|
|
358
365
|
disableTypingIndicator?: boolean;
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
disableHeader?: boolean;
|
|
363
|
-
disableDescription?: boolean;
|
|
364
|
-
disableType?: boolean;
|
|
365
|
-
hideMembers?: boolean;
|
|
366
|
-
disableMembers?: boolean;
|
|
367
|
-
};
|
|
366
|
+
// disableChatInfo already declared above (line ~338) — granular gates
|
|
367
|
+
// (disableHeader / disableDescription / disableType / hideMembers /
|
|
368
|
+
// disableMembers / disableChatHeaderMenu). Don't redeclare here.
|
|
368
369
|
botMessageAutoScroll?: boolean;
|
|
369
370
|
blockMessageSendingWhenProcessing?:
|
|
370
371
|
| boolean
|
package/src/utils/devLogger.ts
CHANGED
|
@@ -106,6 +106,17 @@ function safeStringify(value: any): string {
|
|
|
106
106
|
let consolePatched = false;
|
|
107
107
|
|
|
108
108
|
export function installConsoleCapture() {
|
|
109
|
+
// Under Jest, patching `console.*` to fan out through `pushLog` causes
|
|
110
|
+
// a render-time feedback loop: React's dev build emits `console.error`
|
|
111
|
+
// during act() for various reasons (deprecated APIs, act-warnings),
|
|
112
|
+
// each call schedules a microtask notify → `useSyncExternalStore`
|
|
113
|
+
// subscribers re-render → next render may log again → "Maximum
|
|
114
|
+
// update depth exceeded" → the suite hangs. Skipping the patch in
|
|
115
|
+
// test environments eliminates the loop without changing dev/prod
|
|
116
|
+
// behaviour. Mirrors how the web build gates DEV-only diagnostics.
|
|
117
|
+
if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'test') {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
109
120
|
if (consolePatched) {return;}
|
|
110
121
|
consolePatched = true;
|
|
111
122
|
(['log', 'info', 'warn', 'error'] as const).forEach((level) => {
|