@ethora/chat-component-rn 26.8.8 → 26.9.0
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 +12 -0
- package/README.md +2 -1
- package/lib/commonjs/components/Header/HeaderRoomList.js +34 -6
- package/lib/commonjs/components/Header/HeaderRoomList.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatHeader.js +15 -1
- package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/commonjs/components/MainComponents/RoomList.js +63 -11
- package/lib/commonjs/components/MainComponents/RoomList.js.map +1 -1
- package/lib/commonjs/components/Modals/ChatProfileModal/ReportChatModal.js +3 -2
- package/lib/commonjs/components/Modals/ChatProfileModal/ReportChatModal.js.map +1 -1
- package/lib/commonjs/components/Modals/NewChatModal/UpdNewChatModal.js +2 -0
- package/lib/commonjs/components/Modals/NewChatModal/UpdNewChatModal.js.map +1 -1
- package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js +1 -29
- package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/SwipeableRoomRow.js +133 -0
- package/lib/commonjs/components/RoomComponents/SwipeableRoomRow.js.map +1 -0
- package/lib/commonjs/context/xmppProvider.js +15 -3
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/helpers/updateMessagesTillLast.js +8 -0
- package/lib/commonjs/helpers/updateMessagesTillLast.js.map +1 -1
- package/lib/commonjs/hooks/useChatSettingState.js +2 -0
- package/lib/commonjs/hooks/useChatSettingState.js.map +1 -1
- package/lib/commonjs/hooks/useConnectionLabel.js +29 -0
- package/lib/commonjs/hooks/useConnectionLabel.js.map +1 -0
- package/lib/commonjs/i18n/strings.js +12 -0
- package/lib/commonjs/i18n/strings.js.map +1 -1
- package/lib/commonjs/networking/api-requests/rooms.api.js +32 -7
- package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/commonjs/networking/stanzaHandlers.js +12 -0
- package/lib/commonjs/networking/stanzaHandlers.js.map +1 -1
- package/lib/commonjs/networking/xmppClient.js +29 -1
- package/lib/commonjs/networking/xmppClient.js.map +1 -1
- package/lib/commonjs/roomStore/chatSettingsSlice.js +7 -2
- package/lib/commonjs/roomStore/chatSettingsSlice.js.map +1 -1
- package/lib/commonjs/roomStore/roomsSlice.js +4 -1
- package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
- package/lib/commonjs/services/pushSubscriptionService.js +19 -1
- package/lib/commonjs/services/pushSubscriptionService.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/components/Header/HeaderRoomList.js +34 -6
- package/lib/module/components/Header/HeaderRoomList.js.map +1 -1
- package/lib/module/components/MainComponents/ChatHeader.js +15 -1
- package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/module/components/MainComponents/RoomList.js +64 -12
- package/lib/module/components/MainComponents/RoomList.js.map +1 -1
- package/lib/module/components/Modals/ChatProfileModal/ReportChatModal.js +3 -2
- package/lib/module/components/Modals/ChatProfileModal/ReportChatModal.js.map +1 -1
- package/lib/module/components/Modals/NewChatModal/UpdNewChatModal.js +2 -0
- package/lib/module/components/Modals/NewChatModal/UpdNewChatModal.js.map +1 -1
- package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js +2 -30
- package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
- package/lib/module/components/RoomComponents/SwipeableRoomRow.js +126 -0
- package/lib/module/components/RoomComponents/SwipeableRoomRow.js.map +1 -0
- package/lib/module/context/xmppProvider.js +16 -4
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/helpers/updateMessagesTillLast.js +9 -1
- package/lib/module/helpers/updateMessagesTillLast.js.map +1 -1
- package/lib/module/hooks/useChatSettingState.js +2 -0
- package/lib/module/hooks/useChatSettingState.js.map +1 -1
- package/lib/module/hooks/useConnectionLabel.js +25 -0
- package/lib/module/hooks/useConnectionLabel.js.map +1 -0
- package/lib/module/i18n/strings.js +12 -0
- package/lib/module/i18n/strings.js.map +1 -1
- package/lib/module/networking/api-requests/rooms.api.js +32 -7
- package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/module/networking/stanzaHandlers.js +12 -0
- package/lib/module/networking/stanzaHandlers.js.map +1 -1
- package/lib/module/networking/xmppClient.js +29 -1
- package/lib/module/networking/xmppClient.js.map +1 -1
- package/lib/module/roomStore/chatSettingsSlice.js +5 -1
- package/lib/module/roomStore/chatSettingsSlice.js.map +1 -1
- package/lib/module/roomStore/roomsSlice.js +5 -1
- package/lib/module/roomStore/roomsSlice.js.map +1 -1
- package/lib/module/services/pushSubscriptionService.js +17 -0
- package/lib/module/services/pushSubscriptionService.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/typescript/components/Header/HeaderRoomList.d.ts +1 -0
- package/lib/typescript/components/Header/HeaderRoomList.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/RoomList.d.ts.map +1 -1
- package/lib/typescript/components/Modals/ChatProfileModal/ReportChatModal.d.ts +2 -0
- package/lib/typescript/components/Modals/ChatProfileModal/ReportChatModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/NewChatModal/UpdNewChatModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/UserProfileModal/UserProfileModal.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/SwipeableRoomRow.d.ts +19 -0
- package/lib/typescript/components/RoomComponents/SwipeableRoomRow.d.ts.map +1 -0
- package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
- package/lib/typescript/helpers/updateMessagesTillLast.d.ts.map +1 -1
- package/lib/typescript/hooks/useChatSettingState.d.ts +1 -0
- package/lib/typescript/hooks/useChatSettingState.d.ts.map +1 -1
- package/lib/typescript/hooks/useConnectionLabel.d.ts +8 -0
- package/lib/typescript/hooks/useConnectionLabel.d.ts.map +1 -0
- package/lib/typescript/i18n/strings.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/rooms.api.d.ts +11 -1
- package/lib/typescript/networking/api-requests/rooms.api.d.ts.map +1 -1
- package/lib/typescript/networking/stanzaHandlers.d.ts.map +1 -1
- package/lib/typescript/networking/xmppClient.d.ts +11 -1
- package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
- package/lib/typescript/roomStore/chatSettingsSlice.d.ts +11 -1
- package/lib/typescript/roomStore/chatSettingsSlice.d.ts.map +1 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
- package/lib/typescript/services/pushSubscriptionService.d.ts +8 -0
- package/lib/typescript/services/pushSubscriptionService.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +2 -0
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/lib/typescript/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/Header/HeaderRoomList.tsx +31 -6
- package/src/components/MainComponents/ChatHeader.tsx +16 -1
- package/src/components/MainComponents/RoomList.tsx +78 -12
- package/src/components/Modals/ChatProfileModal/ReportChatModal.tsx +4 -1
- package/src/components/Modals/NewChatModal/UpdNewChatModal.tsx +2 -0
- package/src/components/Modals/UserProfileModal/UserProfileModal.tsx +0 -28
- package/src/components/RoomComponents/SwipeableRoomRow.tsx +133 -0
- package/src/context/xmppProvider.tsx +21 -4
- package/src/helpers/updateMessagesTillLast.tsx +8 -1
- package/src/hooks/useChatSettingState.tsx +4 -0
- package/src/hooks/useConnectionLabel.ts +16 -0
- package/src/i18n/strings.ts +12 -0
- package/src/networking/api-requests/rooms.api.ts +33 -7
- package/src/networking/stanzaHandlers.ts +10 -0
- package/src/networking/xmppClient.ts +33 -1
- package/src/roomStore/chatSettingsSlice.ts +17 -0
- package/src/roomStore/roomsSlice.ts +4 -1
- package/src/services/pushSubscriptionService.ts +17 -0
- package/src/types/types.ts +2 -0
- package/src/version.ts +1 -1
|
@@ -14,6 +14,7 @@ import { RoomMember } from '../../../types/types';
|
|
|
14
14
|
import ModalContent from './ModalContent';
|
|
15
15
|
import { ModalBackground } from '../styledModalComponents';
|
|
16
16
|
import { addRoom, setCurrentRoom } from '../../../roomStore/roomsSlice';
|
|
17
|
+
import { subscribeRoomForPush } from '../../../services/pushSubscriptionService';
|
|
17
18
|
|
|
18
19
|
const NewChatModal: React.FC = () => {
|
|
19
20
|
const config = useSelector(
|
|
@@ -101,6 +102,7 @@ const NewChatModal: React.FC = () => {
|
|
|
101
102
|
dispatch(addRoom({ roomData: normalizedChat }));
|
|
102
103
|
dispatch(setCurrentRoom({ roomJID: normalizedChat.jid }));
|
|
103
104
|
client?.presenceInRoomStanza(normalizedChat.jid);
|
|
105
|
+
subscribeRoomForPush(client, normalizedChat.jid);
|
|
104
106
|
}
|
|
105
107
|
};
|
|
106
108
|
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
ChatIcon,
|
|
17
17
|
EditIcon,
|
|
18
18
|
IconDoc,
|
|
19
|
-
LeaveIcon,
|
|
20
19
|
LockIcon,
|
|
21
20
|
LogoutIcon,
|
|
22
21
|
ProfileIcon,
|
|
@@ -541,19 +540,6 @@ const UserProfileModal: React.FC<UserProfileModalProps> = ({
|
|
|
541
540
|
scrollY={scrollY}
|
|
542
541
|
actions={heroActions}
|
|
543
542
|
titleStyle={chatTextStyle(config?.typography?.profile?.title)}
|
|
544
|
-
titleAccessory={
|
|
545
|
-
isOwnProfile ? (
|
|
546
|
-
<TouchableOpacity
|
|
547
|
-
testID={`${PREFIX}-logout`}
|
|
548
|
-
activeOpacity={0.7}
|
|
549
|
-
style={styles.leaveButton}
|
|
550
|
-
onPress={handleLogout}
|
|
551
|
-
>
|
|
552
|
-
<LeaveIcon color="#FFFFFF" width={18} height={18} />
|
|
553
|
-
<Text style={styles.leaveLabel}>{t('action.leave')}</Text>
|
|
554
|
-
</TouchableOpacity>
|
|
555
|
-
) : undefined
|
|
556
|
-
}
|
|
557
543
|
/>
|
|
558
544
|
|
|
559
545
|
<View style={styles.body}>
|
|
@@ -685,20 +671,6 @@ const createStyles = (theme: ChatTheme) => StyleSheet.create({
|
|
|
685
671
|
fontSize: 16,
|
|
686
672
|
marginTop: 4,
|
|
687
673
|
},
|
|
688
|
-
leaveButton: {
|
|
689
|
-
flexDirection: 'row',
|
|
690
|
-
alignItems: 'center',
|
|
691
|
-
gap: 6,
|
|
692
|
-
paddingHorizontal: 12,
|
|
693
|
-
paddingVertical: 7,
|
|
694
|
-
borderRadius: 18,
|
|
695
|
-
backgroundColor: 'rgba(0,0,0,0.35)',
|
|
696
|
-
},
|
|
697
|
-
leaveLabel: {
|
|
698
|
-
color: '#FFFFFF',
|
|
699
|
-
fontSize: 14,
|
|
700
|
-
fontWeight: '500',
|
|
701
|
-
},
|
|
702
674
|
tabsRow: {
|
|
703
675
|
flexDirection: 'row',
|
|
704
676
|
gap: 20,
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useRef } from 'react';
|
|
2
|
+
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
3
|
+
import ReanimatedSwipeable, {
|
|
4
|
+
type SwipeableMethods,
|
|
5
|
+
} from 'react-native-gesture-handler/ReanimatedSwipeable';
|
|
6
|
+
import { LeaveIcon, ReportIcon } from '../../assets/icons';
|
|
7
|
+
import { useTheme } from '../../hooks/useTheme';
|
|
8
|
+
import { useT } from '../../i18n/useT';
|
|
9
|
+
import type { ChatTheme } from '../../theme/theme';
|
|
10
|
+
|
|
11
|
+
/** Width of one action button behind the row. */
|
|
12
|
+
export const ROOM_ACTION_WIDTH = 84;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Only one row shows its actions at a time, as in the messengers: opening
|
|
16
|
+
* another one, or the list being scrolled, folds the open one back.
|
|
17
|
+
*/
|
|
18
|
+
let openRow: SwipeableMethods | null = null;
|
|
19
|
+
|
|
20
|
+
export const closeOpenRoomRow = (): void => {
|
|
21
|
+
openRow?.close();
|
|
22
|
+
openRow = null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
interface SwipeableRoomRowProps {
|
|
26
|
+
/** Identifies the row to the host's callbacks. */
|
|
27
|
+
roomJid: string;
|
|
28
|
+
onReport: (roomJid: string) => void;
|
|
29
|
+
onLeave: (roomJid: string) => void;
|
|
30
|
+
children: React.ReactNode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A room-list row that slides left to show Report and Leave behind it,
|
|
35
|
+
* the way chats do in the big messengers. A tap on an action folds the
|
|
36
|
+
* row back first, so the list is tidy under whatever the action opens.
|
|
37
|
+
*/
|
|
38
|
+
const SwipeableRoomRow: React.FC<SwipeableRoomRowProps> = ({
|
|
39
|
+
roomJid,
|
|
40
|
+
onReport,
|
|
41
|
+
onLeave,
|
|
42
|
+
children,
|
|
43
|
+
}) => {
|
|
44
|
+
const theme = useTheme();
|
|
45
|
+
const t = useT();
|
|
46
|
+
const styles = useMemo(() => createStyles(theme), [theme]);
|
|
47
|
+
const ref = useRef<SwipeableMethods>(null);
|
|
48
|
+
|
|
49
|
+
const onWillOpen = useCallback(() => {
|
|
50
|
+
if (openRow && openRow !== ref.current) {openRow.close();}
|
|
51
|
+
openRow = ref.current;
|
|
52
|
+
}, []);
|
|
53
|
+
const onClose = useCallback(() => {
|
|
54
|
+
if (openRow === ref.current) {openRow = null;}
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const act = useCallback(
|
|
58
|
+
(what: (jid: string) => void) => {
|
|
59
|
+
ref.current?.close();
|
|
60
|
+
what(roomJid);
|
|
61
|
+
},
|
|
62
|
+
[roomJid]
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const renderRightActions = useCallback(
|
|
66
|
+
() => (
|
|
67
|
+
<View style={styles.actions}>
|
|
68
|
+
<TouchableOpacity
|
|
69
|
+
testID={`room-action-report-${roomJid.split('@')[0]}`}
|
|
70
|
+
accessibilityRole="button"
|
|
71
|
+
accessibilityLabel={t('action.report')}
|
|
72
|
+
activeOpacity={0.8}
|
|
73
|
+
style={[styles.action, { backgroundColor: theme.textSecondary }]}
|
|
74
|
+
onPress={() => act(onReport)}
|
|
75
|
+
>
|
|
76
|
+
<ReportIcon color={theme.textOnPrimary} width={22} height={22} />
|
|
77
|
+
<Text style={styles.label}>{t('action.report')}</Text>
|
|
78
|
+
</TouchableOpacity>
|
|
79
|
+
<TouchableOpacity
|
|
80
|
+
testID={`room-action-leave-${roomJid.split('@')[0]}`}
|
|
81
|
+
accessibilityRole="button"
|
|
82
|
+
accessibilityLabel={t('action.leave')}
|
|
83
|
+
activeOpacity={0.8}
|
|
84
|
+
style={[styles.action, { backgroundColor: theme.danger }]}
|
|
85
|
+
onPress={() => act(onLeave)}
|
|
86
|
+
>
|
|
87
|
+
<LeaveIcon color={theme.textOnPrimary} width={22} height={22} />
|
|
88
|
+
<Text style={styles.label}>{t('action.leave')}</Text>
|
|
89
|
+
</TouchableOpacity>
|
|
90
|
+
</View>
|
|
91
|
+
),
|
|
92
|
+
[act, onLeave, onReport, roomJid, styles, t, theme]
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<ReanimatedSwipeable
|
|
97
|
+
ref={ref}
|
|
98
|
+
testID={`room-swipe-${roomJid.split('@')[0]}`}
|
|
99
|
+
friction={2}
|
|
100
|
+
rightThreshold={ROOM_ACTION_WIDTH / 2}
|
|
101
|
+
overshootRight={false}
|
|
102
|
+
enableTrackpadTwoFingerGesture
|
|
103
|
+
renderRightActions={renderRightActions}
|
|
104
|
+
onSwipeableWillOpen={onWillOpen}
|
|
105
|
+
onSwipeableClose={onClose}
|
|
106
|
+
// The row must cover the actions until it moves.
|
|
107
|
+
childrenContainerStyle={{ backgroundColor: theme.listBackground }}
|
|
108
|
+
>
|
|
109
|
+
{children}
|
|
110
|
+
</ReanimatedSwipeable>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default SwipeableRoomRow;
|
|
115
|
+
|
|
116
|
+
const createStyles = (theme: ChatTheme) =>
|
|
117
|
+
StyleSheet.create({
|
|
118
|
+
actions: {
|
|
119
|
+
flexDirection: 'row',
|
|
120
|
+
width: ROOM_ACTION_WIDTH * 2,
|
|
121
|
+
},
|
|
122
|
+
action: {
|
|
123
|
+
width: ROOM_ACTION_WIDTH,
|
|
124
|
+
alignItems: 'center',
|
|
125
|
+
justifyContent: 'center',
|
|
126
|
+
gap: 4,
|
|
127
|
+
},
|
|
128
|
+
label: {
|
|
129
|
+
color: theme.textOnPrimary,
|
|
130
|
+
fontSize: 12,
|
|
131
|
+
fontWeight: '600',
|
|
132
|
+
},
|
|
133
|
+
});
|
|
@@ -39,7 +39,12 @@ import { getRooms as prefetchRoomsViaRest } from '../networking/api-requests/roo
|
|
|
39
39
|
import { allRoomPresences } from '../networking/xmpp/allRoomPresences.xmpp';
|
|
40
40
|
import { pushSubscriptionService } from '../services/pushSubscriptionService';
|
|
41
41
|
import { store } from '../roomStore';
|
|
42
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
logout,
|
|
44
|
+
setStoreClient,
|
|
45
|
+
setConfig,
|
|
46
|
+
setConnectionState,
|
|
47
|
+
} from '../roomStore/chatSettingsSlice';
|
|
43
48
|
import { logoutService } from '../hooks/useLogout';
|
|
44
49
|
import {
|
|
45
50
|
setLogoutState,
|
|
@@ -260,7 +265,12 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
260
265
|
created.setOnOnline(() => {
|
|
261
266
|
const underlying = (created as any).client;
|
|
262
267
|
if (!underlying) {return;}
|
|
263
|
-
|
|
268
|
+
// The headers say "Updating…" until the rooms are joined again and
|
|
269
|
+
// the archive caught up - a stream that is up is not yet a chat
|
|
270
|
+
// that is. Same tick as the client's own 'online', so the title
|
|
271
|
+
// never shows the plain state in between.
|
|
272
|
+
store.dispatch(setConnectionState('syncing'));
|
|
273
|
+
const rejoin = allRoomPresences(underlying)
|
|
264
274
|
.catch((e) =>
|
|
265
275
|
devPushLog('warn', 'reconnect: allRoomPresences re-join failed', e)
|
|
266
276
|
)
|
|
@@ -268,7 +278,7 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
268
278
|
// Also refresh the private store so unread / lastViewed markers
|
|
269
279
|
// are accurate after a long reconnect — the MUC re-join above only
|
|
270
280
|
// restores delivery, not unread state. Idempotent on first connect.
|
|
271
|
-
created
|
|
281
|
+
const markers = created
|
|
272
282
|
.getChatsPrivateStoreRequestStanza()
|
|
273
283
|
.catch((e: unknown) =>
|
|
274
284
|
devPushLog('warn', 'reconnect: privateStore refresh failed', e)
|
|
@@ -283,9 +293,16 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
283
293
|
// point, so this is a no-op until the bootstrap's own history load
|
|
284
294
|
// populates it. Customer-reported #32.
|
|
285
295
|
const rooms = store.getState().rooms?.rooms || {};
|
|
286
|
-
updateMessagesTillLast(rooms, created).catch((e: unknown) =>
|
|
296
|
+
const catchUp = updateMessagesTillLast(rooms, created).catch((e: unknown) =>
|
|
287
297
|
devPushLog('warn', 'reconnect: offline catch-up sync failed', e)
|
|
288
298
|
);
|
|
299
|
+
Promise.allSettled([rejoin, markers, catchUp]).then(() => {
|
|
300
|
+
// Only if this stream is still the live one: a drop meanwhile
|
|
301
|
+
// has already put the headers back to "Connecting…".
|
|
302
|
+
if (created.status === 'online') {
|
|
303
|
+
store.dispatch(setConnectionState('online'));
|
|
304
|
+
}
|
|
305
|
+
});
|
|
289
306
|
});
|
|
290
307
|
|
|
291
308
|
setClient(created);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import XmppClient from '../networking/xmppClient';
|
|
2
2
|
import { store } from '../roomStore';
|
|
3
|
-
import { setRoomMessages } from '../roomStore/roomsSlice';
|
|
3
|
+
import { addRoomMessages, setRoomMessages } from '../roomStore/roomsSlice';
|
|
4
4
|
import { IMessage, IRoom } from '../types/types';
|
|
5
5
|
import { checkUniqueUsers } from './checkUniqueUsers';
|
|
6
6
|
|
|
@@ -88,6 +88,13 @@ export const updateMessagesTillLast = async (
|
|
|
88
88
|
Number(message.id) === Number(lastCachedMessagesTimeStamp)
|
|
89
89
|
);
|
|
90
90
|
|
|
91
|
+
// Merge the page now, whatever comes next: what the cache
|
|
92
|
+
// already holds is skipped, the rest is what arrived while
|
|
93
|
+
// the stream was down.
|
|
94
|
+
store.dispatch(
|
|
95
|
+
addRoomMessages({ roomJID: jid, messages: fetchedMessages })
|
|
96
|
+
);
|
|
97
|
+
|
|
91
98
|
if (!isMessageFound && !(counter <= maxFetchAttempts - 1)) {
|
|
92
99
|
// Fire-and-forget — checkUniqueUsers resolves to an
|
|
93
100
|
// updated RoomMember list, but there's no slice
|
|
@@ -12,6 +12,9 @@ export const useChatSettingState = () => {
|
|
|
12
12
|
const uiLocale = useSelector(
|
|
13
13
|
(state: RootState) => state.chatSettingStore.uiLocale
|
|
14
14
|
);
|
|
15
|
+
const connection = useSelector(
|
|
16
|
+
(state: RootState) => state.chatSettingStore.connection
|
|
17
|
+
);
|
|
15
18
|
const config = useSelector(
|
|
16
19
|
(state: RootState) => state.chatSettingStore.config
|
|
17
20
|
);
|
|
@@ -33,6 +36,7 @@ export const useChatSettingState = () => {
|
|
|
33
36
|
|
|
34
37
|
return {
|
|
35
38
|
uiLocale,
|
|
39
|
+
connection,
|
|
36
40
|
user,
|
|
37
41
|
activeFile,
|
|
38
42
|
activeModal,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useChatSettingState } from './useChatSettingState';
|
|
2
|
+
import { useT } from '../i18n/useT';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What the headers say in place of their usual title while the session is
|
|
6
|
+
* not usable yet: "Connecting…" until the stream is up (first connect and
|
|
7
|
+
* every reconnect), "Updating…" while rooms are re-joined and the archive
|
|
8
|
+
* caught up. Nothing once the chat is live.
|
|
9
|
+
*/
|
|
10
|
+
export const useConnectionLabel = (): string | undefined => {
|
|
11
|
+
const { connection } = useChatSettingState();
|
|
12
|
+
const t = useT();
|
|
13
|
+
if (connection === 'connecting') {return t('connection.connecting');}
|
|
14
|
+
if (connection === 'syncing') {return t('connection.updating');}
|
|
15
|
+
return undefined;
|
|
16
|
+
};
|
package/src/i18n/strings.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type UiStringTable = Record<string, string>;
|
|
|
16
16
|
const en: UiStringTable = {
|
|
17
17
|
'search.placeholder': 'Search...',
|
|
18
18
|
'roomList.title': 'Chats',
|
|
19
|
+
'connection.connecting': 'Connecting…',
|
|
20
|
+
'connection.updating': 'Updating…',
|
|
19
21
|
'menu.newChat': 'New Chat',
|
|
20
22
|
'menu.profile': 'Profile',
|
|
21
23
|
'menu.settings': 'Settings',
|
|
@@ -335,6 +337,8 @@ const en: UiStringTable = {
|
|
|
335
337
|
const fr: UiStringTable = {
|
|
336
338
|
'search.placeholder': 'Rechercher...',
|
|
337
339
|
'roomList.title': 'Discussions',
|
|
340
|
+
'connection.connecting': 'Connexion…',
|
|
341
|
+
'connection.updating': 'Mise à jour…',
|
|
338
342
|
'menu.newChat': 'Nouvelle discussion',
|
|
339
343
|
'menu.profile': 'Profil',
|
|
340
344
|
'menu.settings': 'Paramètres',
|
|
@@ -654,6 +658,8 @@ const fr: UiStringTable = {
|
|
|
654
658
|
const es: UiStringTable = {
|
|
655
659
|
'search.placeholder': 'Buscar...',
|
|
656
660
|
'roomList.title': 'Conversaciones',
|
|
661
|
+
'connection.connecting': 'Conectando…',
|
|
662
|
+
'connection.updating': 'Actualizando…',
|
|
657
663
|
'menu.newChat': 'Nuevo chat',
|
|
658
664
|
'menu.profile': 'Perfil',
|
|
659
665
|
'menu.settings': 'Configuración',
|
|
@@ -973,6 +979,8 @@ const es: UiStringTable = {
|
|
|
973
979
|
const pt: UiStringTable = {
|
|
974
980
|
'search.placeholder': 'Pesquisar...',
|
|
975
981
|
'roomList.title': 'Conversas',
|
|
982
|
+
'connection.connecting': 'A ligar…',
|
|
983
|
+
'connection.updating': 'A atualizar…',
|
|
976
984
|
'menu.newChat': 'Nova conversa',
|
|
977
985
|
'menu.profile': 'Perfil',
|
|
978
986
|
'menu.settings': 'Definições',
|
|
@@ -1292,6 +1300,8 @@ const pt: UiStringTable = {
|
|
|
1292
1300
|
const ht: UiStringTable = {
|
|
1293
1301
|
'search.placeholder': 'Chèche...',
|
|
1294
1302
|
'roomList.title': 'Chat yo',
|
|
1303
|
+
'connection.connecting': 'Ap konekte…',
|
|
1304
|
+
'connection.updating': 'Ap mete ajou…',
|
|
1295
1305
|
'menu.newChat': 'Nouvo chat',
|
|
1296
1306
|
'menu.profile': 'Pwofil',
|
|
1297
1307
|
'menu.settings': 'Paramèt',
|
|
@@ -1611,6 +1621,8 @@ const ht: UiStringTable = {
|
|
|
1611
1621
|
const zh: UiStringTable = {
|
|
1612
1622
|
'search.placeholder': '搜索...',
|
|
1613
1623
|
'roomList.title': '聊天',
|
|
1624
|
+
'connection.connecting': '连接中…',
|
|
1625
|
+
'connection.updating': '更新中…',
|
|
1614
1626
|
'menu.newChat': '新聊天',
|
|
1615
1627
|
'menu.profile': '个人资料',
|
|
1616
1628
|
'menu.settings': '设置',
|
|
@@ -212,11 +212,25 @@ function dispatchRoomsFromRestItems(items: ApiRoom[]): void {
|
|
|
212
212
|
const ROOM_MEMBERS_TTL_MS = 5 * 60_000;
|
|
213
213
|
const roomMembersLoads = new Map<string, { at: number; done: Promise<boolean> }>();
|
|
214
214
|
|
|
215
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Fetches the room's own REST record (`/chats/my/{name}`) and applies what
|
|
217
|
+
* only the backend knows: the full roster, and whether the room is
|
|
218
|
+
* end-to-end encrypted. Skipped while the roster is already complete,
|
|
219
|
+
* unless `force` - a room that arrived through XMPP (the room list
|
|
220
|
+
* stanza, an invite) was never described by REST at all, so it has no
|
|
221
|
+
* `e2ee` and shows no padlock until this runs.
|
|
222
|
+
*/
|
|
223
|
+
export function loadRoomMembers(
|
|
224
|
+
roomJid: string,
|
|
225
|
+
options: { force?: boolean } = {}
|
|
226
|
+
): Promise<boolean> {
|
|
216
227
|
const jid = String(roomJid || '').split('/')[0]!;
|
|
217
228
|
const room = store.getState().rooms.rooms?.[jid];
|
|
218
229
|
if (!room) {return Promise.resolve(false);}
|
|
219
|
-
if (
|
|
230
|
+
if (
|
|
231
|
+
!options.force &&
|
|
232
|
+
(room.roomMembers?.length ?? 0) >= Number(room.usersCnt || 0)
|
|
233
|
+
) {
|
|
220
234
|
return Promise.resolve(false);
|
|
221
235
|
}
|
|
222
236
|
const running = roomMembersLoads.get(jid);
|
|
@@ -227,23 +241,35 @@ export function loadRoomMembers(roomJid: string): Promise<boolean> {
|
|
|
227
241
|
const response: any = await getRoomByName(jid.split('@')[0]!);
|
|
228
242
|
const item: ApiRoom = response?.result ?? response;
|
|
229
243
|
const members = Array.isArray(item?.members) ? item.members : [];
|
|
230
|
-
if (
|
|
244
|
+
if (!item || typeof item !== 'object') {return false;}
|
|
231
245
|
dispatchUsersSetFromRestItems([item]);
|
|
232
246
|
const current = store.getState().rooms.rooms?.[jid];
|
|
233
247
|
if (!current) {return false;}
|
|
248
|
+
const e2ee = item.e2ee === true || current.e2ee === true;
|
|
234
249
|
store.dispatch(
|
|
235
250
|
updateRoom({
|
|
236
251
|
jid,
|
|
237
252
|
updates: {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
253
|
+
...(members.length > 0
|
|
254
|
+
? {
|
|
255
|
+
roomMembers: toRoomMembers(members),
|
|
256
|
+
usersCnt: Math.max(memberCount(item), members.length),
|
|
257
|
+
}
|
|
258
|
+
: {}),
|
|
259
|
+
...(e2ee ? { e2ee: true } : {}),
|
|
260
|
+
...(e2ee && members.length > 0
|
|
241
261
|
? { members: toE2eeMembers(members, ownAppId()) }
|
|
242
262
|
: {}),
|
|
263
|
+
// What the stanza list did not carry.
|
|
264
|
+
...(!current.title && item.title ? { title: item.title } : {}),
|
|
265
|
+
...(!current.icon && (item.picture || item.icon)
|
|
266
|
+
? { icon: item.picture || item.icon }
|
|
267
|
+
: {}),
|
|
268
|
+
...(!current.type && (item as any).type ? { type: (item as any).type } : {}),
|
|
243
269
|
},
|
|
244
270
|
})
|
|
245
271
|
);
|
|
246
|
-
return
|
|
272
|
+
return members.length > 0;
|
|
247
273
|
} catch (error) {
|
|
248
274
|
roomMembersLoads.delete(jid);
|
|
249
275
|
console.warn('[ethora-rn] rooms.api: could not load room members', jid, error);
|
|
@@ -23,6 +23,8 @@ import { setDeleteModal } from '../roomStore/chatSettingsSlice';
|
|
|
23
23
|
import { messageNotificationManager } from '../utils/messageNotificationManager';
|
|
24
24
|
import { transformCallLogMessage } from '../helpers/callLogMessage';
|
|
25
25
|
import { translateKey } from '../i18n/strings';
|
|
26
|
+
import { loadRoomMembers } from './api-requests/rooms.api';
|
|
27
|
+
import { subscribeRoomForPush } from '../services/pushSubscriptionService';
|
|
26
28
|
|
|
27
29
|
// TO DO: we are thinking to refactor this code in the following way:
|
|
28
30
|
// each stanza will be parsed for 'type'
|
|
@@ -502,6 +504,14 @@ const onGetChatRooms = (stanza: Element, xmpp: any) => {
|
|
|
502
504
|
if (!store.getState().rooms.activeRoomJID) {
|
|
503
505
|
store.dispatch(setCurrentRoom({ roomJID: fresh[0].jid }));
|
|
504
506
|
}
|
|
507
|
+
// The stanza says nothing about encryption or the roster: ask REST
|
|
508
|
+
// for each new room, so an encrypted room gets its padlock (and
|
|
509
|
+
// its recipients) without waiting for the next full load.
|
|
510
|
+
for (const room of fresh) {
|
|
511
|
+
loadRoomMembers(room.jid, { force: true }).catch(() => {});
|
|
512
|
+
// ...and pushes for it: the bootstrap's MucSub pass is over.
|
|
513
|
+
subscribeRoomForPush(xmpp, room.jid);
|
|
514
|
+
}
|
|
505
515
|
}
|
|
506
516
|
for (const jid of jids) {
|
|
507
517
|
try {
|
|
@@ -29,6 +29,7 @@ import { pushLog as devPushLog, isDevLogActive } from '../utils/devLogger';
|
|
|
29
29
|
import { normalizeRoomJid } from '../helpers/normalizeRoomJid';
|
|
30
30
|
import { store } from '../roomStore';
|
|
31
31
|
import { applyPrivateStoreMarkers } from '../roomStore/roomsSlice';
|
|
32
|
+
import { setConnectionState } from '../roomStore/chatSettingsSlice';
|
|
32
33
|
import {
|
|
33
34
|
clearOutboundSends,
|
|
34
35
|
enqueueOutboundSend,
|
|
@@ -129,7 +130,38 @@ export class XmppClient {
|
|
|
129
130
|
service: string = '';
|
|
130
131
|
conference: string = '';
|
|
131
132
|
username: string;
|
|
132
|
-
|
|
133
|
+
private _status: 'offline' | 'connecting' | 'online' | 'error' = 'offline';
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The stream's state. Every change is mirrored into the store
|
|
137
|
+
* (`chatSettingStore.connection`) so the headers can say "Connecting…":
|
|
138
|
+
* a class field is nothing React can follow. `offline` and `error` are
|
|
139
|
+
* reported as `connecting` while a retry is scheduled - that is what the
|
|
140
|
+
* user is waiting for - and as `offline` only once nothing will try
|
|
141
|
+
* (logout, unmount).
|
|
142
|
+
*/
|
|
143
|
+
get status(): 'offline' | 'connecting' | 'online' | 'error' {
|
|
144
|
+
return this._status;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
set status(next: 'offline' | 'connecting' | 'online' | 'error') {
|
|
148
|
+
const changed = next !== this._status;
|
|
149
|
+
this._status = next;
|
|
150
|
+
if (!changed) {return;}
|
|
151
|
+
try {
|
|
152
|
+
const reported =
|
|
153
|
+
next === 'online'
|
|
154
|
+
? 'online'
|
|
155
|
+
: next === 'connecting' || !this.suppressReconnect
|
|
156
|
+
? 'connecting'
|
|
157
|
+
: 'offline';
|
|
158
|
+
if (store.getState().chatSettingStore.connection !== reported) {
|
|
159
|
+
store.dispatch(setConnectionState(reported));
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
/* no store in some test harnesses */
|
|
163
|
+
}
|
|
164
|
+
}
|
|
133
165
|
|
|
134
166
|
password = '';
|
|
135
167
|
reconnectAttempts = 0;
|
|
@@ -44,8 +44,18 @@ export interface ChatState {
|
|
|
44
44
|
* `config.i18n.locale`, which is the default for a user who never chose.
|
|
45
45
|
*/
|
|
46
46
|
uiLocale?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Where the XMPP session stands, for the headers: `connecting` until the
|
|
49
|
+
* stream is up (also while it is being re-established), `syncing` while
|
|
50
|
+
* rooms are re-joined and the archive caught up after a (re)connect,
|
|
51
|
+
* `online` once that is done, `offline` when the stream is down and
|
|
52
|
+
* nothing is trying (logged out).
|
|
53
|
+
*/
|
|
54
|
+
connection?: ConnectionState;
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
export type ConnectionState = 'connecting' | 'syncing' | 'online' | 'offline';
|
|
58
|
+
|
|
49
59
|
export const unpackAndTransform = (input?: User): User => {
|
|
50
60
|
return {
|
|
51
61
|
// Was hardcoded to '' — every setUser dropped the user's own bio, so
|
|
@@ -231,6 +241,12 @@ const reducers = {
|
|
|
231
241
|
) => {
|
|
232
242
|
state.uiLocale = action.payload || undefined;
|
|
233
243
|
},
|
|
244
|
+
setConnectionState: (
|
|
245
|
+
state: WritableDraft<ChatState>,
|
|
246
|
+
action: PayloadAction<ConnectionState>
|
|
247
|
+
) => {
|
|
248
|
+
state.connection = action.payload;
|
|
249
|
+
},
|
|
234
250
|
};
|
|
235
251
|
|
|
236
252
|
export const chatSlice: Slice<ChatState, typeof reducers, 'chat'> = createSlice({
|
|
@@ -255,6 +271,7 @@ export const {
|
|
|
255
271
|
setThemePreference,
|
|
256
272
|
setPushEnabled,
|
|
257
273
|
setUiLocale,
|
|
274
|
+
setConnectionState,
|
|
258
275
|
} = chatSlice.actions;
|
|
259
276
|
|
|
260
277
|
export default chatSlice.reducer;
|
|
@@ -4,6 +4,7 @@ import { EditAction, HistoryPreloadState, IMessage, IRoom } from '../types/types
|
|
|
4
4
|
import { insertMessageWithDelimiter } from '../helpers/insertMessageWithDelimiter';
|
|
5
5
|
import { msgSortableMs } from '../helpers/msgSortableMs';
|
|
6
6
|
import type XmppClient from '../networking/xmppClient';
|
|
7
|
+
import { subscribeRoomForPush } from '../services/pushSubscriptionService';
|
|
7
8
|
|
|
8
9
|
// Per-room runtime message cap. Mirrors the persistence layer's
|
|
9
10
|
// MESSAGE_LIMIT so what's in memory matches what's on disk; otherwise
|
|
@@ -509,11 +510,13 @@ const isValidRoomJid = (jid: unknown): jid is string => {
|
|
|
509
510
|
export const addRoomViaApi = createAsyncThunk(
|
|
510
511
|
'roomMessages/addRoomViaApi',
|
|
511
512
|
async (
|
|
512
|
-
{ room, xmpp
|
|
513
|
+
{ room, xmpp }: { room: IRoom; xmpp: XmppClient },
|
|
513
514
|
{ dispatch }
|
|
514
515
|
) => {
|
|
515
516
|
if (!room || !room.jid) return;
|
|
516
517
|
dispatch(roomsStore.actions.addRoomFromApi({ room }));
|
|
518
|
+
// A room made now was not in the bootstrap's MucSub pass.
|
|
519
|
+
subscribeRoomForPush(xmpp, room.jid);
|
|
517
520
|
}
|
|
518
521
|
);
|
|
519
522
|
|
|
@@ -114,3 +114,20 @@ export class PushSubscriptionService {
|
|
|
114
114
|
|
|
115
115
|
export const pushSubscriptionService = new PushSubscriptionService();
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Push for a room that turned up after the bootstrap (created here, an
|
|
119
|
+
* invite, the room list after a reconnect): the bootstrap subscribed only
|
|
120
|
+
* the rooms it had, so without this a new room is silent until the next
|
|
121
|
+
* app start. Best effort, never throws; `wrapper` is the SDK's XmppClient
|
|
122
|
+
* (its `client` the underlying stream).
|
|
123
|
+
*/
|
|
124
|
+
export const subscribeRoomForPush = (wrapper: any, roomJid: string): void => {
|
|
125
|
+
const client = wrapper?.client;
|
|
126
|
+
const jid = String(roomJid || '').split('/')[0];
|
|
127
|
+
if (!client || !jid.includes('@')) {return;}
|
|
128
|
+
const nick = client.jid?.getLocal?.();
|
|
129
|
+
pushSubscriptionService
|
|
130
|
+
.subscribeToRoom(client, jid, nick)
|
|
131
|
+
.catch((error) => console.warn('[PushService] late subscribe failed', jid, error));
|
|
132
|
+
};
|
|
133
|
+
|
package/src/types/types.ts
CHANGED
|
@@ -652,6 +652,8 @@ export interface IConfig {
|
|
|
652
652
|
};
|
|
653
653
|
disableProfilesInteractions?: boolean;
|
|
654
654
|
disableUserCount?: boolean;
|
|
655
|
+
/** No Report / Leave behind a room-list row swiped to the left. */
|
|
656
|
+
disableRoomSwipeActions?: boolean;
|
|
655
657
|
disableSentLogic?: boolean;
|
|
656
658
|
disableTypingIndicator?: boolean;
|
|
657
659
|
// Hide the full-screen "Connection error" overlay (the dark modal with
|
package/src/version.ts
CHANGED