@ethora/chat-component-rn 26.5.16 → 26.5.18
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/lib/commonjs/assets/EmptyChatIllustration.js +106 -0
- package/lib/commonjs/assets/EmptyChatIllustration.js.map +1 -0
- package/lib/commonjs/assets/icons.js +13 -2
- package/lib/commonjs/assets/icons.js.map +1 -1
- package/lib/commonjs/components/DropdownMenu/DropdownMenu.js +2 -1
- package/lib/commonjs/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatHeader.js +2 -1
- package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatRoom.js +5 -2
- package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChooseChatMessage.js +9 -2
- package/lib/commonjs/components/MainComponents/ChooseChatMessage.js.map +1 -1
- package/lib/commonjs/components/MainComponents/EditWrapper.js +12 -4
- package/lib/commonjs/components/MainComponents/EditWrapper.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MessageList.js +2 -1
- package/lib/commonjs/components/MainComponents/MessageList.js.map +1 -1
- package/lib/commonjs/components/MainComponents/NoMessagesPlaceholder.js +11 -3
- package/lib/commonjs/components/MainComponents/NoMessagesPlaceholder.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ProfileImagePlaceholder.js +12 -7
- package/lib/commonjs/components/MainComponents/ProfileImagePlaceholder.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ReduxWrapper.js +4 -0
- package/lib/commonjs/components/MainComponents/ReduxWrapper.js.map +1 -1
- package/lib/commonjs/components/MainComponents/RoomList.js +2 -1
- package/lib/commonjs/components/MainComponents/RoomList.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/Avatar.js +16 -7
- package/lib/commonjs/components/MessageBubble/Avatar.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/Message.js +16 -1
- package/lib/commonjs/components/MessageBubble/Message.js.map +1 -1
- package/lib/commonjs/components/Modals/ModalWrapper/ModalWrapper.js +9 -0
- package/lib/commonjs/components/Modals/ModalWrapper/ModalWrapper.js.map +1 -1
- package/lib/commonjs/components/styled/Button.js +4 -2
- package/lib/commonjs/components/styled/Button.js.map +1 -1
- package/lib/commonjs/components/styled/DateLabel.js +12 -4
- package/lib/commonjs/components/styled/DateLabel.js.map +1 -1
- package/lib/commonjs/components/styled/SendInput.js +4 -3
- package/lib/commonjs/components/styled/SendInput.js.map +1 -1
- package/lib/commonjs/context/xmppProvider.js +12 -1
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/helpers/getAvatarColor.js +33 -0
- package/lib/commonjs/helpers/getAvatarColor.js.map +1 -0
- package/lib/commonjs/helpers/getDateLabelColor.js +9 -0
- package/lib/commonjs/helpers/getDateLabelColor.js.map +1 -0
- package/lib/commonjs/helpers/getIconColor.js +9 -0
- package/lib/commonjs/helpers/getIconColor.js.map +1 -0
- package/lib/commonjs/helpers/getSenderNameColor.js +9 -0
- package/lib/commonjs/helpers/getSenderNameColor.js.map +1 -0
- package/lib/commonjs/helpers/historyPreloadScheduler.js +118 -113
- package/lib/commonjs/helpers/historyPreloadScheduler.js.map +1 -1
- package/lib/commonjs/hooks/useChatFonts.js +137 -0
- package/lib/commonjs/hooks/useChatFonts.js.map +1 -0
- package/lib/commonjs/hooks/useSendMessage.js +72 -22
- package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
- package/lib/commonjs/hooks/useUnreadMessagesCounter.js +4 -2
- package/lib/commonjs/hooks/useUnreadMessagesCounter.js.map +1 -1
- package/lib/commonjs/main.js +7 -0
- package/lib/commonjs/main.js.map +1 -1
- package/lib/commonjs/networking/outboundQueue.js +22 -4
- package/lib/commonjs/networking/outboundQueue.js.map +1 -1
- package/lib/commonjs/networking/xmppClient.js +50 -0
- package/lib/commonjs/networking/xmppClient.js.map +1 -1
- package/lib/commonjs/roomStore/Middleware/newMessageMidlleware.js +22 -0
- package/lib/commonjs/roomStore/Middleware/newMessageMidlleware.js.map +1 -1
- package/lib/commonjs/roomStore/roomsSlice.js +22 -2
- package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
- package/lib/module/assets/EmptyChatIllustration.js +99 -0
- package/lib/module/assets/EmptyChatIllustration.js.map +1 -0
- package/lib/module/assets/icons.js +13 -2
- package/lib/module/assets/icons.js.map +1 -1
- package/lib/module/components/DropdownMenu/DropdownMenu.js +2 -1
- package/lib/module/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/lib/module/components/MainComponents/ChatHeader.js +2 -1
- package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/module/components/MainComponents/ChatRoom.js +5 -2
- package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/module/components/MainComponents/ChooseChatMessage.js +9 -2
- package/lib/module/components/MainComponents/ChooseChatMessage.js.map +1 -1
- package/lib/module/components/MainComponents/EditWrapper.js +12 -4
- package/lib/module/components/MainComponents/EditWrapper.js.map +1 -1
- package/lib/module/components/MainComponents/MessageList.js +2 -1
- package/lib/module/components/MainComponents/MessageList.js.map +1 -1
- package/lib/module/components/MainComponents/NoMessagesPlaceholder.js +11 -3
- package/lib/module/components/MainComponents/NoMessagesPlaceholder.js.map +1 -1
- package/lib/module/components/MainComponents/ProfileImagePlaceholder.js +12 -7
- package/lib/module/components/MainComponents/ProfileImagePlaceholder.js.map +1 -1
- package/lib/module/components/MainComponents/ReduxWrapper.js +4 -0
- package/lib/module/components/MainComponents/ReduxWrapper.js.map +1 -1
- package/lib/module/components/MainComponents/RoomList.js +2 -1
- package/lib/module/components/MainComponents/RoomList.js.map +1 -1
- package/lib/module/components/MessageBubble/Avatar.js +16 -7
- package/lib/module/components/MessageBubble/Avatar.js.map +1 -1
- package/lib/module/components/MessageBubble/Message.js +16 -1
- package/lib/module/components/MessageBubble/Message.js.map +1 -1
- package/lib/module/components/Modals/ModalWrapper/ModalWrapper.js +9 -0
- package/lib/module/components/Modals/ModalWrapper/ModalWrapper.js.map +1 -1
- package/lib/module/components/styled/Button.js +4 -2
- package/lib/module/components/styled/Button.js.map +1 -1
- package/lib/module/components/styled/DateLabel.js +12 -4
- package/lib/module/components/styled/DateLabel.js.map +1 -1
- package/lib/module/components/styled/SendInput.js +4 -3
- package/lib/module/components/styled/SendInput.js.map +1 -1
- package/lib/module/context/xmppProvider.js +13 -2
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/helpers/getAvatarColor.js +28 -0
- package/lib/module/helpers/getAvatarColor.js.map +1 -0
- package/lib/module/helpers/getDateLabelColor.js +4 -0
- package/lib/module/helpers/getDateLabelColor.js.map +1 -0
- package/lib/module/helpers/getIconColor.js +4 -0
- package/lib/module/helpers/getIconColor.js.map +1 -0
- package/lib/module/helpers/getSenderNameColor.js +4 -0
- package/lib/module/helpers/getSenderNameColor.js.map +1 -0
- package/lib/module/helpers/historyPreloadScheduler.js +119 -114
- package/lib/module/helpers/historyPreloadScheduler.js.map +1 -1
- package/lib/module/hooks/useChatFonts.js +132 -0
- package/lib/module/hooks/useChatFonts.js.map +1 -0
- package/lib/module/hooks/useSendMessage.js +72 -22
- package/lib/module/hooks/useSendMessage.js.map +1 -1
- package/lib/module/hooks/useUnreadMessagesCounter.js +4 -2
- package/lib/module/hooks/useUnreadMessagesCounter.js.map +1 -1
- package/lib/module/main.js +2 -0
- package/lib/module/main.js.map +1 -1
- package/lib/module/networking/outboundQueue.js +21 -4
- package/lib/module/networking/outboundQueue.js.map +1 -1
- package/lib/module/networking/xmppClient.js +50 -0
- package/lib/module/networking/xmppClient.js.map +1 -1
- package/lib/module/roomStore/Middleware/newMessageMidlleware.js +22 -0
- package/lib/module/roomStore/Middleware/newMessageMidlleware.js.map +1 -1
- package/lib/module/roomStore/roomsSlice.js +20 -1
- package/lib/module/roomStore/roomsSlice.js.map +1 -1
- package/lib/typescript/assets/EmptyChatIllustration.d.ts +10 -0
- package/lib/typescript/assets/EmptyChatIllustration.d.ts.map +1 -0
- package/lib/typescript/assets/icons.d.ts +3 -12
- package/lib/typescript/assets/icons.d.ts.map +1 -1
- package/lib/typescript/components/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChooseChatMessage.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/EditWrapper.d.ts +5 -1
- package/lib/typescript/components/MainComponents/EditWrapper.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MessageList.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/NoMessagesPlaceholder.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ProfileImagePlaceholder.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ReduxWrapper.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/RoomList.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble/Avatar.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble/Message.d.ts.map +1 -1
- package/lib/typescript/components/Modals/ModalWrapper/ModalWrapper.d.ts.map +1 -1
- package/lib/typescript/components/styled/Button.d.ts +1 -0
- package/lib/typescript/components/styled/Button.d.ts.map +1 -1
- package/lib/typescript/components/styled/DateLabel.d.ts +9 -8
- package/lib/typescript/components/styled/DateLabel.d.ts.map +1 -1
- package/lib/typescript/components/styled/SendInput.d.ts.map +1 -1
- package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
- package/lib/typescript/helpers/getAvatarColor.d.ts +7 -0
- package/lib/typescript/helpers/getAvatarColor.d.ts.map +1 -0
- package/lib/typescript/helpers/getDateLabelColor.d.ts +8 -0
- package/lib/typescript/helpers/getDateLabelColor.d.ts.map +1 -0
- package/lib/typescript/helpers/getIconColor.d.ts +8 -0
- package/lib/typescript/helpers/getIconColor.d.ts.map +1 -0
- package/lib/typescript/helpers/getSenderNameColor.d.ts +8 -0
- package/lib/typescript/helpers/getSenderNameColor.d.ts.map +1 -0
- package/lib/typescript/helpers/historyPreloadScheduler.d.ts.map +1 -1
- package/lib/typescript/hooks/useChatFonts.d.ts +23 -0
- package/lib/typescript/hooks/useChatFonts.d.ts.map +1 -0
- package/lib/typescript/hooks/useSendMessage.d.ts +1 -1
- package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
- package/lib/typescript/hooks/useUnreadMessagesCounter.d.ts +1 -0
- package/lib/typescript/hooks/useUnreadMessagesCounter.d.ts.map +1 -1
- package/lib/typescript/main.d.ts +2 -1
- package/lib/typescript/main.d.ts.map +1 -1
- package/lib/typescript/networking/outboundQueue.d.ts +16 -1
- package/lib/typescript/networking/outboundQueue.d.ts.map +1 -1
- package/lib/typescript/networking/xmppClient.d.ts +3 -0
- package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
- package/lib/typescript/roomStore/Middleware/newMessageMidlleware.d.ts.map +1 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts +3 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
- package/lib/typescript/types/models/message.model.d.ts +1 -0
- package/lib/typescript/types/models/message.model.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +53 -0
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/assets/EmptyChatIllustration.tsx +49 -0
- package/src/assets/icons.tsx +64 -81
- package/src/components/DropdownMenu/DropdownMenu.tsx +2 -1
- package/src/components/MainComponents/ChatHeader.tsx +2 -1
- package/src/components/MainComponents/ChatRoom.tsx +5 -2
- package/src/components/MainComponents/ChooseChatMessage.tsx +5 -2
- package/src/components/MainComponents/EditWrapper.tsx +9 -6
- package/src/components/MainComponents/MessageList.tsx +2 -1
- package/src/components/MainComponents/NoMessagesPlaceholder.tsx +4 -1
- package/src/components/MainComponents/ProfileImagePlaceholder.tsx +14 -7
- package/src/components/MainComponents/ReduxWrapper.tsx +4 -0
- package/src/components/MainComponents/RoomList.tsx +2 -1
- package/src/components/MessageBubble/Avatar.tsx +18 -9
- package/src/components/MessageBubble/Message.tsx +16 -1
- package/src/components/Modals/ModalWrapper/ModalWrapper.tsx +6 -0
- package/src/components/styled/Button.tsx +5 -1
- package/src/components/styled/DateLabel.tsx +10 -7
- package/src/components/styled/SendInput.tsx +4 -3
- package/src/context/xmppProvider.tsx +11 -1
- package/src/helpers/getAvatarColor.ts +31 -0
- package/src/helpers/getDateLabelColor.ts +3 -0
- package/src/helpers/getIconColor.ts +3 -0
- package/src/helpers/getSenderNameColor.ts +3 -0
- package/src/helpers/historyPreloadScheduler.ts +7 -1
- package/src/hooks/useChatFonts.ts +153 -0
- package/src/hooks/useSendMessage.tsx +101 -58
- package/src/hooks/useUnreadMessagesCounter.ts +5 -2
- package/src/main.ts +4 -0
- package/src/networking/outboundQueue.ts +25 -4
- package/src/networking/xmppClient.ts +47 -0
- package/src/roomStore/Middleware/newMessageMidlleware.tsx +25 -0
- package/src/roomStore/roomsSlice.ts +26 -1
- package/src/types/models/message.model.ts +3 -0
- package/src/types/types.ts +59 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { Text, TextInput } from 'react-native';
|
|
3
|
+
import type { TypographyConfig } from '../types/types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Loads and applies the host-provided font configuration for the chat UI.
|
|
7
|
+
*
|
|
8
|
+
* React Native has no web-style runtime `@font-face`, so a custom font must be
|
|
9
|
+
* registered with `expo-font` before it can be used. This hook:
|
|
10
|
+
* 1. loads every entry in `typography.fonts` via `expo-font` (remote URL or
|
|
11
|
+
* bundled `require(...)`), then
|
|
12
|
+
* 2. sets the resolved `fontFamily` as the default for all `<Text>` and
|
|
13
|
+
* `<TextInput>` rendered by the chat — mirroring how the web SDK sets a
|
|
14
|
+
* CSS variable. Individual components can still override per-style.
|
|
15
|
+
*
|
|
16
|
+
* `expo-font` is an optional peer dependency: if the host hasn't installed it,
|
|
17
|
+
* the hook degrades gracefully (font simply isn't loaded) instead of throwing.
|
|
18
|
+
*
|
|
19
|
+
* No-op when `typography` is undefined — the system font is kept, so existing
|
|
20
|
+
* integrations are unaffected.
|
|
21
|
+
*
|
|
22
|
+
* @returns `true` once fonts are ready (or when there's nothing to load), so
|
|
23
|
+
* callers can gate first paint to avoid a font "flash" if desired.
|
|
24
|
+
*/
|
|
25
|
+
export function useChatFonts(typography?: TypographyConfig): boolean {
|
|
26
|
+
const [ready, setReady] = useState(!typography?.fonts?.length);
|
|
27
|
+
|
|
28
|
+
applyDefaultFont(typography);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
let cancelled = false;
|
|
32
|
+
|
|
33
|
+
async function load() {
|
|
34
|
+
// Apply the default family immediately so already-available fonts
|
|
35
|
+
// (system, or host-loaded) take effect without waiting on a download.
|
|
36
|
+
applyDefaultFont(typography);
|
|
37
|
+
|
|
38
|
+
if (!typography?.fonts?.length) {
|
|
39
|
+
setReady(true);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// Optional dependency — resolve lazily so apps that don't use custom
|
|
45
|
+
// fonts don't need expo-font installed.
|
|
46
|
+
const ExpoFont = await import('expo-font').catch(() => null);
|
|
47
|
+
if (!ExpoFont) {
|
|
48
|
+
if (!cancelled) setReady(true);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const map: Record<string, string | number> = {};
|
|
52
|
+
for (const f of typography.fonts) map[f.family] = f.source as never;
|
|
53
|
+
await ExpoFont.loadAsync(map);
|
|
54
|
+
if (!cancelled) {
|
|
55
|
+
applyDefaultFont(typography);
|
|
56
|
+
setReady(true);
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// Loading failed (e.g. bad URL) — keep the fallback font, don't block UI.
|
|
60
|
+
if (!cancelled) setReady(true);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
load();
|
|
65
|
+
return () => {
|
|
66
|
+
cancelled = true;
|
|
67
|
+
};
|
|
68
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
69
|
+
}, [
|
|
70
|
+
typography?.fontFamily,
|
|
71
|
+
JSON.stringify(typography?.fonts ?? []),
|
|
72
|
+
JSON.stringify(typography?.weightFamilies ?? {}),
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
return ready;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The family currently applied as the chat-wide default. Read at render time
|
|
80
|
+
* by the patched components below, so updating it (then triggering a re-render)
|
|
81
|
+
* restyles the whole tree.
|
|
82
|
+
*/
|
|
83
|
+
let activeFamily: string | undefined;
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
function wrapFactory(orig: any): any {
|
|
87
|
+
if (typeof orig !== 'function') return orig;
|
|
88
|
+
if (orig.__ethoraFontWrapped) return orig;
|
|
89
|
+
const wrapped = function (this: any, type: any, props: any, ...rest: any[]) {
|
|
90
|
+
if (
|
|
91
|
+
activeFamily &&
|
|
92
|
+
props &&
|
|
93
|
+
(type === Text || type === TextInput)
|
|
94
|
+
) {
|
|
95
|
+
props = { ...props, style: [{ fontFamily: activeFamily }, props.style] };
|
|
96
|
+
}
|
|
97
|
+
return orig.call(this, type, props, ...rest);
|
|
98
|
+
};
|
|
99
|
+
wrapped.__ethoraFontWrapped = true;
|
|
100
|
+
return wrapped;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let factoriesPatched = false;
|
|
104
|
+
|
|
105
|
+
function ensurePatched(): void {
|
|
106
|
+
if (factoriesPatched) return;
|
|
107
|
+
|
|
108
|
+
let any = false;
|
|
109
|
+
|
|
110
|
+
const patchMod = (mod: any): void => {
|
|
111
|
+
if (!mod) return;
|
|
112
|
+
for (const fn of ['jsx', 'jsxs', 'jsxDEV']) {
|
|
113
|
+
if (typeof mod[fn] === 'function' && !mod[fn].__ethoraFontWrapped) {
|
|
114
|
+
mod[fn] = wrapFactory(mod[fn]);
|
|
115
|
+
any = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
patchMod(require('react/jsx-runtime'));
|
|
122
|
+
} catch {
|
|
123
|
+
/* ignore */
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
patchMod(require('react/jsx-dev-runtime'));
|
|
127
|
+
} catch {
|
|
128
|
+
/* ignore */
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const React = require('react');
|
|
133
|
+
if (
|
|
134
|
+
typeof React.createElement === 'function' &&
|
|
135
|
+
!React.createElement.__ethoraFontWrapped
|
|
136
|
+
) {
|
|
137
|
+
React.createElement = wrapFactory(React.createElement);
|
|
138
|
+
any = true;
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
/* ignore */
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (any) factoriesPatched = true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Set (or clear) the chat-wide default font family.
|
|
149
|
+
*/
|
|
150
|
+
function applyDefaultFont(typography?: TypographyConfig): void {
|
|
151
|
+
activeFamily = typography?.fontFamily;
|
|
152
|
+
if (activeFamily) ensurePatched();
|
|
153
|
+
}
|
|
@@ -20,11 +20,25 @@ import type { IConfig, IMessage } from '../types/types';
|
|
|
20
20
|
// long after the optimistic dispatch, mark it failed so the bubble
|
|
21
21
|
// flips out of the "sending..." state. Covers the silent-failure
|
|
22
22
|
// case (e.g. device offline before the message ever made it to the
|
|
23
|
-
// XMPP socket) — bug #18.
|
|
24
|
-
//
|
|
25
|
-
//
|
|
23
|
+
// XMPP socket) — bug #18.
|
|
24
|
+
//
|
|
25
|
+
// 5s window: a brief network blip that reconnects inside this window
|
|
26
|
+
// still resolves on its own — the offline send is buffered in the
|
|
27
|
+
// outbound queue (OUTBOUND_QUEUE_TTL_MS, kept in lockstep) and replayed
|
|
28
|
+
// the moment the stream comes back, so the server echo flips the bubble
|
|
29
|
+
// to delivered. Only a drop that outlasts 5s flips it to "Failed → tap
|
|
30
|
+
// to retry". If a slow send completes just after the window, the
|
|
31
|
+
// delivery clears the failed flag (see newMessageMidlleware) so it
|
|
32
|
+
// doesn't get stuck showing "Failed".
|
|
26
33
|
const PENDING_WATCHDOG_MS = 30_000;
|
|
27
34
|
|
|
35
|
+
// Media gets a longer window than text: the optimistic bubble appears before
|
|
36
|
+
// the file upload even starts, and a large file (up to the 50MB cap) over a
|
|
37
|
+
// slow link can legitimately take far longer than 5s to upload + send. Failing
|
|
38
|
+
// it at 5s mid-upload would flash a wrong "Failed" and let the user kick off a
|
|
39
|
+
// duplicate upload. The text watchdog stays at 5s per product spec.
|
|
40
|
+
const MEDIA_PENDING_WATCHDOG_MS = 30_000;
|
|
41
|
+
|
|
28
42
|
// Monotonic counter for stanza ids. Bumped on each send within a single
|
|
29
43
|
// process. Two rapid sends in the same millisecond used to collide on
|
|
30
44
|
// `send-text-message-${Date.now()}`; that collision made
|
|
@@ -76,7 +90,14 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
76
90
|
activeRoomJID: string,
|
|
77
91
|
isReply?: boolean,
|
|
78
92
|
isChecked?: boolean,
|
|
79
|
-
mainMessage?: string
|
|
93
|
+
mainMessage?: string,
|
|
94
|
+
// Set by retryMessage to the failed message's own id. When present we
|
|
95
|
+
// re-use that id (and its existing optimistic bubble) instead of minting
|
|
96
|
+
// a fresh one — mirroring sendMedia's `existingId`. Without this, a text
|
|
97
|
+
// retry created a second bubble while the original stayed pending
|
|
98
|
+
// forever, so a failed message that the user retried showed as two
|
|
99
|
+
// bubbles, one stuck on "sending...".
|
|
100
|
+
existingId?: string
|
|
80
101
|
) => {
|
|
81
102
|
if (editAction?.isEdit) {
|
|
82
103
|
try {
|
|
@@ -113,7 +134,7 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
113
134
|
// dedupes by id and flips `pending: false` → DoubleTick renders.
|
|
114
135
|
// Without this, the user taps send and sees nothing for ~200ms
|
|
115
136
|
// until the echo lands, which feels broken.
|
|
116
|
-
const optimisticId = nextStanzaId('send-text-message');
|
|
137
|
+
const optimisticId = existingId || nextStanzaId('send-text-message');
|
|
117
138
|
const optimisticDate = new Date().toISOString();
|
|
118
139
|
const selfId =
|
|
119
140
|
(user as any)?.xmppUsername || (user as any)?.walletAddress || '';
|
|
@@ -135,7 +156,11 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
135
156
|
name: `${user?.firstName || ''} ${user?.lastName || ''}`.trim() || selfId,
|
|
136
157
|
} as any,
|
|
137
158
|
} as IMessage;
|
|
138
|
-
|
|
159
|
+
// On retry (`existingId`) the original optimistic bubble is still in
|
|
160
|
+
// the room (pending:true) — clearMessageFailure just flipped it back out
|
|
161
|
+
// of the "failed" state — so re-adding would duplicate it. Only push a
|
|
162
|
+
// fresh bubble for a brand-new send.
|
|
163
|
+
if (!config?.disableSentLogic && !existingId) {
|
|
139
164
|
dispatch(
|
|
140
165
|
addRoomMessage({ roomJID: activeRoomJID, message: optimisticMessage })
|
|
141
166
|
);
|
|
@@ -177,15 +202,54 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
177
202
|
}, PENDING_WATCHDOG_MS);
|
|
178
203
|
|
|
179
204
|
try {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
205
|
+
// The single send closure, used both for the immediate send and for
|
|
206
|
+
// the buffered replay. Passing the optimistic id as the stanza id
|
|
207
|
+
// means the server echoes it back with the same value, so the
|
|
208
|
+
// reducer's dedupe matches and the bubble flips pending → delivered
|
|
209
|
+
// in-place (rather than rendering a second copy).
|
|
210
|
+
const replaySend = (c: any) =>
|
|
211
|
+
c.sendMessage(
|
|
212
|
+
activeRoomJID,
|
|
213
|
+
user.firstName,
|
|
214
|
+
user.lastName,
|
|
215
|
+
'',
|
|
216
|
+
user.walletAddress,
|
|
217
|
+
message,
|
|
218
|
+
'',
|
|
219
|
+
isReply || false,
|
|
220
|
+
isChecked || false,
|
|
221
|
+
mainMessage || '',
|
|
222
|
+
optimisticId
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Always buffer a copy BEFORE attempting the live send. The WebSocket
|
|
226
|
+
// can still read `status: 'online'` for a beat after the network
|
|
227
|
+
// actually drops (the WS error lands a moment later), so a stanza sent
|
|
228
|
+
// "while online" can vanish into a dying socket and never get echoed.
|
|
229
|
+
// Buffering every send means the onOnline reconnect flush replays
|
|
230
|
+
// anything that wasn't confirmed within the window. The server echo
|
|
231
|
+
// removes the entry (see newMessageMidlleware), so a healthy online
|
|
232
|
+
// send never actually replays — only genuinely-lost ones do.
|
|
233
|
+
enqueueOutboundSend({
|
|
234
|
+
optimisticId,
|
|
235
|
+
roomJID: activeRoomJID,
|
|
236
|
+
enqueuedAt: Date.now(),
|
|
237
|
+
send: replaySend,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Only treat a client as usable if its underlying stream is actually
|
|
241
|
+
// `online` — the instance survives a network drop with a stale
|
|
242
|
+
// 'connecting'/'offline' status, and sending through it would just
|
|
243
|
+
// lose the stanza.
|
|
244
|
+
const effectiveClient: any = [
|
|
245
|
+
client,
|
|
246
|
+
getGlobalXmppClient(),
|
|
247
|
+
store.getState()?.chatSettingStore?.client,
|
|
248
|
+
].find((c: any) => c && c.status === 'online');
|
|
187
249
|
|
|
188
250
|
if (!effectiveClient) {
|
|
251
|
+
// No live stream — kick a reconnect; the buffered copy above is
|
|
252
|
+
// replayed by the onOnline flush once the stream is back.
|
|
189
253
|
const anyClient: any =
|
|
190
254
|
client ||
|
|
191
255
|
getGlobalXmppClient() ||
|
|
@@ -193,45 +257,13 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
193
257
|
try {
|
|
194
258
|
anyClient?.forceReconnect?.();
|
|
195
259
|
} catch {}
|
|
196
|
-
enqueueOutboundSend({
|
|
197
|
-
optimisticId,
|
|
198
|
-
roomJID: activeRoomJID,
|
|
199
|
-
enqueuedAt: Date.now(),
|
|
200
|
-
send: (c) =>
|
|
201
|
-
c.sendMessage(
|
|
202
|
-
activeRoomJID,
|
|
203
|
-
user.firstName,
|
|
204
|
-
user.lastName,
|
|
205
|
-
'',
|
|
206
|
-
user.walletAddress,
|
|
207
|
-
message,
|
|
208
|
-
'',
|
|
209
|
-
isReply || false,
|
|
210
|
-
isChecked || false,
|
|
211
|
-
mainMessage || '',
|
|
212
|
-
optimisticId
|
|
213
|
-
),
|
|
214
|
-
});
|
|
215
260
|
return;
|
|
216
261
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
effectiveClient.sendMessage(
|
|
223
|
-
activeRoomJID,
|
|
224
|
-
user.firstName,
|
|
225
|
-
user.lastName,
|
|
226
|
-
'',
|
|
227
|
-
user.walletAddress,
|
|
228
|
-
message,
|
|
229
|
-
'',
|
|
230
|
-
isReply || false,
|
|
231
|
-
isChecked || false,
|
|
232
|
-
mainMessage || '',
|
|
233
|
-
optimisticId
|
|
234
|
-
);
|
|
262
|
+
|
|
263
|
+
replaySend(effectiveClient);
|
|
264
|
+
try {
|
|
265
|
+
effectiveClient.ensureStreamAlive?.();
|
|
266
|
+
} catch {}
|
|
235
267
|
await handleMessageSent({
|
|
236
268
|
message,
|
|
237
269
|
roomJID: activeRoomJID,
|
|
@@ -405,7 +437,7 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
405
437
|
messageType: 'media',
|
|
406
438
|
});
|
|
407
439
|
}
|
|
408
|
-
},
|
|
440
|
+
}, MEDIA_PENDING_WATCHDOG_MS);
|
|
409
441
|
|
|
410
442
|
// Help RN's FormData polyfill build a proper multipart/file part
|
|
411
443
|
// (instead of serialising the JS object as JSON). On iOS the
|
|
@@ -487,12 +519,14 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
487
519
|
// Stanza id == placeholder id so the MUC echo's outer
|
|
488
520
|
// <message id="..."> matches xmppId on the placeholder and
|
|
489
521
|
// insertMessageWithDelimiter merges in place + flips pending.
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
522
|
+
// Same online-only guard as the text path: a surviving-but-offline
|
|
523
|
+
// client must NOT be used (the stanza would vanish into a dead
|
|
524
|
+
// socket) — fall through to enqueue so the reconnect flush replays.
|
|
525
|
+
const effectiveMediaClient: any = [
|
|
526
|
+
client,
|
|
527
|
+
getGlobalXmppClient(),
|
|
528
|
+
store.getState()?.chatSettingStore?.client,
|
|
529
|
+
].find((c: any) => c && c.status === 'online');
|
|
496
530
|
if (effectiveMediaClient) {
|
|
497
531
|
effectiveMediaClient.setActiveRoomJid?.(activeRoomJID);
|
|
498
532
|
await Promise.resolve(
|
|
@@ -503,6 +537,9 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
503
537
|
messagePayload,
|
|
504
538
|
id
|
|
505
539
|
);
|
|
540
|
+
try {
|
|
541
|
+
effectiveMediaClient.ensureStreamAlive?.();
|
|
542
|
+
} catch {}
|
|
506
543
|
} else {
|
|
507
544
|
const anyMediaClient: any =
|
|
508
545
|
client ||
|
|
@@ -515,6 +552,9 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
515
552
|
optimisticId: id,
|
|
516
553
|
roomJID: activeRoomJID,
|
|
517
554
|
enqueuedAt: Date.now(),
|
|
555
|
+
// Match the media watchdog so a reconnect within the (longer)
|
|
556
|
+
// media window still auto-replays the buffered stanza.
|
|
557
|
+
ttlMs: MEDIA_PENDING_WATCHDOG_MS,
|
|
518
558
|
send: (c) =>
|
|
519
559
|
c.sendMediaMessageStanza(activeRoomJID, messagePayload, id),
|
|
520
560
|
});
|
|
@@ -605,7 +645,10 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
605
645
|
payload.roomJID,
|
|
606
646
|
payload.isReply,
|
|
607
647
|
payload.isChecked,
|
|
608
|
-
payload.mainMessage
|
|
648
|
+
payload.mainMessage,
|
|
649
|
+
// Re-use the failed message's id so the retry resends in place
|
|
650
|
+
// instead of spawning a duplicate bubble.
|
|
651
|
+
failedId
|
|
609
652
|
);
|
|
610
653
|
} else {
|
|
611
654
|
await sendMedia(
|
|
@@ -10,6 +10,7 @@ interface UnreadMessagesStats {
|
|
|
10
10
|
hasUnread: boolean;
|
|
11
11
|
totalCount: number;
|
|
12
12
|
unreadByRoom: UnreadMessagesMap;
|
|
13
|
+
isLoading: boolean;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export const useUnreadMessagesCounter = (): UnreadMessagesStats => {
|
|
@@ -26,11 +27,12 @@ export const useUnreadMessagesCounter = (): UnreadMessagesStats => {
|
|
|
26
27
|
// so non-rooms dispatches still don't trigger a re-render.)
|
|
27
28
|
const subscribe = (callback: () => void) => store.subscribe(callback);
|
|
28
29
|
|
|
29
|
-
const
|
|
30
|
+
const roomsState = useSyncExternalStore(
|
|
30
31
|
subscribe,
|
|
31
|
-
() => store.getState().rooms
|
|
32
|
+
() => store.getState().rooms
|
|
32
33
|
);
|
|
33
34
|
|
|
35
|
+
const rooms = roomsState.rooms;
|
|
34
36
|
const unreadByRoom: UnreadMessagesMap = {};
|
|
35
37
|
let totalCount = 0;
|
|
36
38
|
|
|
@@ -46,6 +48,7 @@ export const useUnreadMessagesCounter = (): UnreadMessagesStats => {
|
|
|
46
48
|
hasUnread: totalCount > 0,
|
|
47
49
|
totalCount,
|
|
48
50
|
unreadByRoom,
|
|
51
|
+
isLoading: !!roomsState.isUnreadSyncing || !!roomsState.isLoading,
|
|
49
52
|
};
|
|
50
53
|
};
|
|
51
54
|
|
package/src/main.ts
CHANGED
|
@@ -32,6 +32,8 @@ export { useChatRoomFocus } from './hooks/useChatRoomFocus';
|
|
|
32
32
|
export { logoutService } from './hooks/useLogout';
|
|
33
33
|
export { useQRCodeChat, handleQRChatId } from './hooks/useQRCodeChatHandler';
|
|
34
34
|
export { resendMessage } from './utils/resendMessage';
|
|
35
|
+
// Font loader — exposed so hosts can also load the font outside <Chat> if needed.
|
|
36
|
+
export { useChatFonts } from './hooks/useChatFonts';
|
|
35
37
|
|
|
36
38
|
// Public types — needed so consumers can type their `config` prop and
|
|
37
39
|
// any event handlers they pass in.
|
|
@@ -43,4 +45,6 @@ export type {
|
|
|
43
45
|
User,
|
|
44
46
|
ConfigUser,
|
|
45
47
|
MessageProps,
|
|
48
|
+
TypographyConfig,
|
|
49
|
+
RNFontSource,
|
|
46
50
|
} from './types/types';
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* Each queued item carries a `send(client)` closure that captures the
|
|
20
20
|
* original args (text or media), so flush is type-agnostic.
|
|
21
21
|
*
|
|
22
|
-
* TTL is deliberately aligned with the
|
|
22
|
+
* TTL is deliberately aligned with the pending-send watchdog in
|
|
23
23
|
* useSendMessage (PENDING_WATCHDOG_MS): a queued item older than that has
|
|
24
24
|
* already been — or is about to be — flipped to "Failed → tap to retry" by
|
|
25
25
|
* the watchdog, so replaying it on a late reconnect would duplicate the
|
|
@@ -37,11 +37,20 @@ export interface QueuedSend {
|
|
|
37
37
|
roomJID: string;
|
|
38
38
|
/** Wall-clock ms when first queued; drives the TTL drop on flush. */
|
|
39
39
|
enqueuedAt: number;
|
|
40
|
+
/**
|
|
41
|
+
* Per-item replay window. Kept in lockstep with the matching send
|
|
42
|
+
* watchdog so a reconnect inside the window replays the buffered send,
|
|
43
|
+
* while a longer drop lets the watchdog own the failure (no duplicate).
|
|
44
|
+
* Text uses 5s, media 30s (large uploads need longer). Falls back to
|
|
45
|
+
* OUTBOUND_QUEUE_TTL_MS when omitted.
|
|
46
|
+
*/
|
|
47
|
+
ttlMs?: number;
|
|
40
48
|
/** Replays the original send against a live, online client. */
|
|
41
49
|
send: (client: OutboundQueueClient) => void;
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
//
|
|
52
|
+
// Default replay window (text). Mirrors PENDING_WATCHDOG_MS in useSendMessage.
|
|
53
|
+
// Media enqueues a longer per-item ttlMs (see QueuedSend.ttlMs).
|
|
45
54
|
export const OUTBOUND_QUEUE_TTL_MS = 30_000;
|
|
46
55
|
|
|
47
56
|
let queue: QueuedSend[] = [];
|
|
@@ -74,8 +83,8 @@ export function flushOutboundSends(
|
|
|
74
83
|
const pending = queue;
|
|
75
84
|
queue = [];
|
|
76
85
|
for (const item of pending) {
|
|
77
|
-
if (now - item.enqueuedAt > OUTBOUND_QUEUE_TTL_MS) {
|
|
78
|
-
// Stale — the
|
|
86
|
+
if (now - item.enqueuedAt > (item.ttlMs ?? OUTBOUND_QUEUE_TTL_MS)) {
|
|
87
|
+
// Stale — the send watchdog owns the failure; don't duplicate.
|
|
79
88
|
continue;
|
|
80
89
|
}
|
|
81
90
|
try {
|
|
@@ -87,6 +96,18 @@ export function flushOutboundSends(
|
|
|
87
96
|
}
|
|
88
97
|
}
|
|
89
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Drop a single buffered send by its optimistic/stanza id. Called when a
|
|
101
|
+
* server-confirmed copy of the message arrives (see newMessageMidlleware):
|
|
102
|
+
* the send went through, so the buffered replay is no longer needed and must
|
|
103
|
+
* not fire on a later reconnect. No-op if the id isn't queued.
|
|
104
|
+
*/
|
|
105
|
+
export function removeOutboundSend(optimisticId: string): void {
|
|
106
|
+
if (!optimisticId) {return;}
|
|
107
|
+
const idx = queue.findIndex((q) => q.optimisticId === optimisticId);
|
|
108
|
+
if (idx >= 0) {queue.splice(idx, 1);}
|
|
109
|
+
}
|
|
110
|
+
|
|
90
111
|
/** Drop everything — called on permanent teardown (logout / close). */
|
|
91
112
|
export function clearOutboundSends(): void {
|
|
92
113
|
queue = [];
|
|
@@ -7,6 +7,7 @@ import { getChatsPrivateStoreRequest } from './xmpp/getChatsPrivateStoreRequest.
|
|
|
7
7
|
import { actionSetTimestampToPrivateStore } from './xmpp/actionSetTimestampToPrivateStore.xmpp';
|
|
8
8
|
import { flushLastViewedToPrivateStore } from './xmpp/flushLastViewedToPrivateStore';
|
|
9
9
|
import { sendTypingRequest } from './xmpp/sendTypingRequest.xmpp';
|
|
10
|
+
import { sendPing } from './xmpp/sendPing.xmpp';
|
|
10
11
|
import { getHistory } from './xmpp/getHistory.xmpp';
|
|
11
12
|
import { sendTextMessage } from './xmpp/sendTextMessage.xmpp';
|
|
12
13
|
import { deleteMessage } from './xmpp/deleteMessage.xmpp';
|
|
@@ -614,6 +615,52 @@ export class XmppClient {
|
|
|
614
615
|
this.reconnect();
|
|
615
616
|
}
|
|
616
617
|
|
|
618
|
+
private streamAliveProbe: Promise<boolean> | null = null;
|
|
619
|
+
|
|
620
|
+
verifyStreamAlive(timeoutMs: number = 4000): Promise<boolean> {
|
|
621
|
+
if (this.status !== 'online' || !this.client) {
|
|
622
|
+
return Promise.resolve(false);
|
|
623
|
+
}
|
|
624
|
+
if (this.streamAliveProbe) {return this.streamAliveProbe;}
|
|
625
|
+
const underlying: any = this.client;
|
|
626
|
+
this.streamAliveProbe = new Promise<boolean>((resolve) => {
|
|
627
|
+
let settled = false;
|
|
628
|
+
const done = (alive: boolean) => {
|
|
629
|
+
if (settled) {return;}
|
|
630
|
+
settled = true;
|
|
631
|
+
clearTimeout(timer);
|
|
632
|
+
try {
|
|
633
|
+
underlying.removeListener?.('stanza', onStanza);
|
|
634
|
+
} catch {}
|
|
635
|
+
resolve(alive);
|
|
636
|
+
};
|
|
637
|
+
const onStanza = () => done(true);
|
|
638
|
+
const timer = setTimeout(() => done(false), timeoutMs);
|
|
639
|
+
try {
|
|
640
|
+
underlying.on('stanza', onStanza);
|
|
641
|
+
sendPing(underlying, this.host, `alive-${Date.now()}`);
|
|
642
|
+
} catch {
|
|
643
|
+
// Couldn't even write the probe — treat as dead.
|
|
644
|
+
done(false);
|
|
645
|
+
}
|
|
646
|
+
}).finally(() => {
|
|
647
|
+
this.streamAliveProbe = null;
|
|
648
|
+
}) as Promise<boolean>;
|
|
649
|
+
return this.streamAliveProbe;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async ensureStreamAlive(timeoutMs: number = 4000): Promise<void> {
|
|
653
|
+
if (this.suppressReconnect || this.status !== 'online') {return;}
|
|
654
|
+
const alive = await this.verifyStreamAlive(timeoutMs);
|
|
655
|
+
// Re-check state: a reconnect may have started meanwhile.
|
|
656
|
+
if (!alive && this.status === 'online' && !this.suppressReconnect) {
|
|
657
|
+
try {
|
|
658
|
+
devPushLog('xmpp', 'stream is zombie (ping unanswered) → forceReconnect');
|
|
659
|
+
} catch {}
|
|
660
|
+
this.forceReconnect();
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
617
664
|
async reconnect() {
|
|
618
665
|
if (this.suppressReconnect) {return;}
|
|
619
666
|
// Single-flight: ignore overlapping triggers (AppState + NetInfo +
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Middleware } from '@reduxjs/toolkit';
|
|
2
2
|
import { updateRoom } from '../roomsSlice';
|
|
3
|
+
import { clearMessageFailure } from '../roomHeapSlice';
|
|
4
|
+
import { removeOutboundSend } from '../../networking/outboundQueue';
|
|
3
5
|
import { IRoom } from '../../types/types';
|
|
4
6
|
|
|
5
7
|
export const newMessageMidlleware: Middleware =
|
|
@@ -24,6 +26,29 @@ export const newMessageMidlleware: Middleware =
|
|
|
24
26
|
|
|
25
27
|
const { roomJID, message } = action.payload;
|
|
26
28
|
|
|
29
|
+
// Un-fail on confirmed delivery. A server-confirmed copy (echo or MAM
|
|
30
|
+
// history, never `pending`) arriving for a message we'd flagged failed
|
|
31
|
+
// means the send actually went through — e.g. a slow send that completed
|
|
32
|
+
// just after the 5s watchdog window, or a queued auto-resend landing on
|
|
33
|
+
// reconnect. The failed flag is keyed by our optimistic/stanza id, which
|
|
34
|
+
// the echo carries back as `id`/`xmppId`, so clear it here and let the
|
|
35
|
+
// bubble flip to delivered instead of staying stuck on "Failed".
|
|
36
|
+
if (message && !message.pending) {
|
|
37
|
+
// The confirmed copy carries our optimistic/stanza id back as id/xmppId.
|
|
38
|
+
// Drop any still-buffered replay for it so a later reconnect flush can't
|
|
39
|
+
// re-send an already-delivered message.
|
|
40
|
+
removeOutboundSend(message.id);
|
|
41
|
+
removeOutboundSend(message.xmppId);
|
|
42
|
+
|
|
43
|
+
const failedMap = state.roomHeapSlice?.failedMessages || {};
|
|
44
|
+
const failedKey = [message.id, message.xmppId].find(
|
|
45
|
+
(k: string | undefined) => k && failedMap[k]
|
|
46
|
+
);
|
|
47
|
+
if (failedKey) {
|
|
48
|
+
storeAPI.dispatch(clearMessageFailure(failedKey as string));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
27
52
|
if ((rooms[roomJID]?.lastMessageTimestamp ?? 0) <= Number(message.id)) {
|
|
28
53
|
storeAPI.dispatch(
|
|
29
54
|
updateRoom({
|
|
@@ -76,7 +76,17 @@ function mergeHistoryIntoCache(
|
|
|
76
76
|
// the freshest server state (reactions / edits / deletions).
|
|
77
77
|
const byId = new Map<string, IMessage>();
|
|
78
78
|
for (const m of realExisting) {byId.set(String(m.id), m);}
|
|
79
|
-
for (const m of realFetched) {
|
|
79
|
+
for (const m of realFetched) {
|
|
80
|
+
// Fetched wins on body/reactions/etc., but MAM doesn't carry our
|
|
81
|
+
// client-side "edited" flag (the archived stanza just has the corrected
|
|
82
|
+
// body). Preserve `isEdited` from the cached copy so a message edited
|
|
83
|
+
// before reload keeps its marker instead of losing it on history sync.
|
|
84
|
+
const prev = byId.get(String(m.id));
|
|
85
|
+
byId.set(
|
|
86
|
+
String(m.id),
|
|
87
|
+
prev?.isEdited && !m.isEdited ? { ...m, isEdited: true } : m
|
|
88
|
+
);
|
|
89
|
+
}
|
|
80
90
|
const merged = Array.from(byId.values()).sort(byMs);
|
|
81
91
|
return [...capTail(merged), ...pending];
|
|
82
92
|
}
|
|
@@ -96,6 +106,7 @@ export interface RoomMessagesState {
|
|
|
96
106
|
editAction?: EditAction;
|
|
97
107
|
isLoading: boolean;
|
|
98
108
|
loadingText?: string;
|
|
109
|
+
isUnreadSyncing?: boolean;
|
|
99
110
|
usersSet?: Record<string, any>;
|
|
100
111
|
pendingNotificationJid?: string | null;
|
|
101
112
|
// Server-side read markers fetched from the XMPP private store
|
|
@@ -111,6 +122,7 @@ const initialState: RoomMessagesState = {
|
|
|
111
122
|
activeRoomJID: null,
|
|
112
123
|
visibleRoomJID: null,
|
|
113
124
|
isLoading: false,
|
|
125
|
+
isUnreadSyncing: false,
|
|
114
126
|
editAction: {
|
|
115
127
|
isEdit: false,
|
|
116
128
|
roomJid: '',
|
|
@@ -305,6 +317,10 @@ const reducers = {
|
|
|
305
317
|
state.rooms[roomJID].messages.map((message) => {
|
|
306
318
|
if (message.id === messageId) {
|
|
307
319
|
message.body = text;
|
|
320
|
+
// Flag the correction so the bubble can render an "edited" marker.
|
|
321
|
+
// Covers both the author's own edit and edits from other users —
|
|
322
|
+
// every <replace> echo flows through here.
|
|
323
|
+
message.isEdited = true;
|
|
308
324
|
}
|
|
309
325
|
});
|
|
310
326
|
}
|
|
@@ -362,6 +378,7 @@ const reducers = {
|
|
|
362
378
|
state.rooms = {};
|
|
363
379
|
state.visibleRoomJID = null;
|
|
364
380
|
state.privateStoreMarkers = {};
|
|
381
|
+
state.isUnreadSyncing = false;
|
|
365
382
|
},
|
|
366
383
|
setComposing(
|
|
367
384
|
state: WritableDraft<RoomMessagesState>,
|
|
@@ -386,6 +403,12 @@ const reducers = {
|
|
|
386
403
|
state.isLoading = loading;
|
|
387
404
|
state.loadingText = loadingText;
|
|
388
405
|
},
|
|
406
|
+
setUnreadSyncing: (
|
|
407
|
+
state: WritableDraft<RoomMessagesState>,
|
|
408
|
+
action: PayloadAction<boolean>
|
|
409
|
+
) => {
|
|
410
|
+
state.isUnreadSyncing = action.payload;
|
|
411
|
+
},
|
|
389
412
|
setLastViewedTimestamp: (
|
|
390
413
|
state: WritableDraft<RoomMessagesState>,
|
|
391
414
|
action: PayloadAction<{ chatJID: string; timestamp: number }>
|
|
@@ -538,6 +561,7 @@ const reducers = {
|
|
|
538
561
|
state.activeRoomJID = null;
|
|
539
562
|
state.visibleRoomJID = null;
|
|
540
563
|
state.isLoading = false;
|
|
564
|
+
state.isUnreadSyncing = false;
|
|
541
565
|
state.privateStoreMarkers = {};
|
|
542
566
|
},
|
|
543
567
|
setActiveMessage: (
|
|
@@ -729,6 +753,7 @@ export const {
|
|
|
729
753
|
editRoomMessage,
|
|
730
754
|
setComposing,
|
|
731
755
|
setIsLoading,
|
|
756
|
+
setUnreadSyncing,
|
|
732
757
|
setLastViewedTimestamp,
|
|
733
758
|
applyPrivateStoreMarkers,
|
|
734
759
|
setRoomNoMessages,
|