@ethora/chat-component-rn 26.7.6 → 26.7.8
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 +5 -0
- package/lib/commonjs/assets/icons.js +2 -2
- package/lib/commonjs/assets/icons.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatWrapper.js +2 -0
- package/lib/commonjs/components/MainComponents/ChatWrapper.js.map +1 -1
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +51 -1
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/commonjs/context/xmppProvider.js +9 -1
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/helpers/pushPayload.js +71 -0
- package/lib/commonjs/helpers/pushPayload.js.map +1 -0
- package/lib/commonjs/hooks/usePendingNotification.js +8 -11
- package/lib/commonjs/hooks/usePendingNotification.js.map +1 -1
- package/lib/commonjs/i18n/strings.js +18 -0
- package/lib/commonjs/i18n/strings.js.map +1 -1
- package/lib/commonjs/main.js +13 -0
- package/lib/commonjs/main.js.map +1 -1
- package/lib/commonjs/networking/api-requests/rooms.api.js +13 -0
- package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/commonjs/networking/xmpp/subscribeToRoomMessages.xmpp.js +5 -1
- package/lib/commonjs/networking/xmpp/subscribeToRoomMessages.xmpp.js.map +1 -1
- package/lib/commonjs/networking/xmppClient.js +55 -1
- package/lib/commonjs/networking/xmppClient.js.map +1 -1
- package/lib/commonjs/services/pushSubscriptionService.js +15 -13
- package/lib/commonjs/services/pushSubscriptionService.js.map +1 -1
- package/lib/module/assets/icons.js +1 -1
- package/lib/module/assets/icons.js.map +1 -1
- package/lib/module/components/MainComponents/ChatWrapper.js +2 -0
- package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +53 -3
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/module/context/xmppProvider.js +9 -1
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/helpers/pushPayload.js +63 -0
- package/lib/module/helpers/pushPayload.js.map +1 -0
- package/lib/module/hooks/usePendingNotification.js +8 -11
- package/lib/module/hooks/usePendingNotification.js.map +1 -1
- package/lib/module/i18n/strings.js +18 -0
- package/lib/module/i18n/strings.js.map +1 -1
- package/lib/module/main.js +1 -0
- package/lib/module/main.js.map +1 -1
- package/lib/module/networking/api-requests/rooms.api.js +12 -0
- package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/module/networking/xmpp/subscribeToRoomMessages.xmpp.js +5 -1
- package/lib/module/networking/xmpp/subscribeToRoomMessages.xmpp.js.map +1 -1
- package/lib/module/networking/xmppClient.js +55 -1
- package/lib/module/networking/xmppClient.js.map +1 -1
- package/lib/module/services/pushSubscriptionService.js +15 -13
- package/lib/module/services/pushSubscriptionService.js.map +1 -1
- package/lib/typescript/assets/icons.d.ts +8 -0
- package/lib/typescript/assets/icons.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
- package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
- package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
- package/lib/typescript/helpers/getAvatarColor.d.ts +1 -1
- package/lib/typescript/helpers/pushPayload.d.ts +7 -0
- package/lib/typescript/helpers/pushPayload.d.ts.map +1 -0
- package/lib/typescript/hooks/usePendingNotification.d.ts.map +1 -1
- package/lib/typescript/i18n/strings.d.ts.map +1 -1
- package/lib/typescript/main.d.ts +2 -0
- package/lib/typescript/main.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/rooms.api.d.ts +1 -0
- package/lib/typescript/networking/api-requests/rooms.api.d.ts.map +1 -1
- package/lib/typescript/networking/xmpp/subscribeToRoomMessages.xmpp.d.ts.map +1 -1
- package/lib/typescript/networking/xmppClient.d.ts +7 -0
- package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
- package/lib/typescript/services/pushSubscriptionService.d.ts +1 -1
- package/lib/typescript/services/pushSubscriptionService.d.ts.map +1 -1
- package/lib/typescript/types/models/room.model.d.ts +1 -0
- package/lib/typescript/types/models/room.model.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/package.json +1 -1
- package/src/assets/icons.tsx +24 -0
- package/src/components/MainComponents/ChatWrapper.tsx +3 -0
- package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +36 -2
- package/src/context/xmppProvider.tsx +9 -1
- package/src/helpers/pushPayload.ts +62 -0
- package/src/hooks/usePendingNotification.ts +5 -11
- package/src/i18n/strings.ts +18 -0
- package/src/main.ts +3 -0
- package/src/networking/api-requests/rooms.api.ts +9 -0
- package/src/networking/xmpp/subscribeToRoomMessages.xmpp.ts +12 -1
- package/src/networking/xmppClient.ts +58 -1
- package/src/services/pushSubscriptionService.ts +18 -13
- package/src/types/models/room.model.ts +1 -0
- package/src/types/types.ts +2 -0
|
@@ -30,6 +30,7 @@ import Modal from '../Modals/Modal/Modal';
|
|
|
30
30
|
import ThreadWrapper from '../Thread/ThreadWrapper';
|
|
31
31
|
import {ModalWrapper} from '../Modals/ModalWrapper/ModalWrapper';
|
|
32
32
|
import {useChatSettingState} from '../../hooks/useChatSettingState';
|
|
33
|
+
import { usePendingNotification } from '../../hooks/usePendingNotification';
|
|
33
34
|
import {DeviceEventEmitter, Pressable, Text, View} from 'react-native';
|
|
34
35
|
import {pushLog as devPushLog} from '../../utils/devLogger';
|
|
35
36
|
import {normalizeRoomJid} from '../../helpers/normalizeRoomJid';
|
|
@@ -61,6 +62,8 @@ const ChatWrapper: FC<ChatWrapperProps> = ({
|
|
|
61
62
|
client: storedClient,
|
|
62
63
|
} = useChatSettingState();
|
|
63
64
|
|
|
65
|
+
usePendingNotification();
|
|
66
|
+
|
|
64
67
|
const [isInited, setInited] = useState(false);
|
|
65
68
|
const [showModal, setShowModal] = useState(false);
|
|
66
69
|
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
|
@@ -26,6 +26,8 @@ import { deleteRoom, updateRoom } from '../../../roomStore/roomsSlice';
|
|
|
26
26
|
import Loader from '../../styled/Loader';
|
|
27
27
|
import {
|
|
28
28
|
AddNewIcon,
|
|
29
|
+
BellIcon,
|
|
30
|
+
BellOffIcon,
|
|
29
31
|
DeleteIcon,
|
|
30
32
|
EditIcon,
|
|
31
33
|
LeaveIcon,
|
|
@@ -36,7 +38,7 @@ import { useChatSettingState } from '../../../hooks/useChatSettingState';
|
|
|
36
38
|
import { chatTextStyle } from '../../../helpers/typography';
|
|
37
39
|
import { getElementFont } from '../../../helpers/getElementFont';
|
|
38
40
|
import { getIconColor } from '../../../helpers/getIconColor';
|
|
39
|
-
import { deleteRoomMember } from '../../../networking/api-requests/rooms.api';
|
|
41
|
+
import { deleteRoomMember, setRoomMuted } from '../../../networking/api-requests/rooms.api';
|
|
40
42
|
import { RoomMember } from '../../../types/models/room.model';
|
|
41
43
|
import { setActiveModal, setSelectedUser } from '../../../roomStore/chatSettingsSlice';
|
|
42
44
|
import { MODAL_TYPES } from '../../../helpers/constants/MODAL_TYPES';
|
|
@@ -355,8 +357,40 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
|
|
|
355
357
|
return () => sub.remove();
|
|
356
358
|
}, [isSearchOpen]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
357
359
|
|
|
360
|
+
const muteInFlightRef = useRef(false);
|
|
361
|
+
const toggleMuted = async () => {
|
|
362
|
+
if (!activeRoom?.jid || !activeRoom?.name || muteInFlightRef.current) {return;}
|
|
363
|
+
const next = !activeRoom.muted;
|
|
364
|
+
muteInFlightRef.current = true;
|
|
365
|
+
dispatch(updateRoom({ jid: activeRoom.jid, updates: { muted: next } }));
|
|
366
|
+
try {
|
|
367
|
+
const confirmed = await setRoomMuted(activeRoom.name, next);
|
|
368
|
+
if (confirmed !== next) {
|
|
369
|
+
dispatch(updateRoom({ jid: activeRoom.jid, updates: { muted: confirmed } }));
|
|
370
|
+
}
|
|
371
|
+
} catch (error) {
|
|
372
|
+
dispatch(updateRoom({ jid: activeRoom.jid, updates: { muted: !next } }));
|
|
373
|
+
console.warn('[ethora-rn] mute toggle failed', error);
|
|
374
|
+
showToast({
|
|
375
|
+
id: Date.now().toString(),
|
|
376
|
+
title: 'Error',
|
|
377
|
+
message: t('toast.muteFailed'),
|
|
378
|
+
type: 'error',
|
|
379
|
+
});
|
|
380
|
+
} finally {
|
|
381
|
+
muteInFlightRef.current = false;
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
358
385
|
const heroActions: HeroAction[] = useMemo(
|
|
359
386
|
() => [
|
|
387
|
+
{
|
|
388
|
+
key: 'mute',
|
|
389
|
+
label: activeRoom?.muted ? t('action.muted') : t('action.unmuted'),
|
|
390
|
+
icon: (color: string) =>
|
|
391
|
+
activeRoom?.muted ? <BellOffIcon color={color} /> : <BellIcon color={color} />,
|
|
392
|
+
onPress: toggleMuted,
|
|
393
|
+
},
|
|
360
394
|
{
|
|
361
395
|
key: 'search',
|
|
362
396
|
label: t('action.search'),
|
|
@@ -376,7 +410,7 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
|
|
|
376
410
|
onPress: () => setIsReportOpen(true),
|
|
377
411
|
},
|
|
378
412
|
],
|
|
379
|
-
[t, activeRoom?.jid] // eslint-disable-line react-hooks/exhaustive-deps
|
|
413
|
+
[t, activeRoom?.jid, activeRoom?.muted] // eslint-disable-line react-hooks/exhaustive-deps
|
|
380
414
|
);
|
|
381
415
|
|
|
382
416
|
// "…" menu. Edit is the chat picture — the only chat detail this SDK can
|
|
@@ -594,9 +594,14 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
594
594
|
// be re-marked visible on return. Lives in the effect closure (one per
|
|
595
595
|
// client) — persists across the listener's many invocations.
|
|
596
596
|
let visibleBeforeBackground: string | null = null;
|
|
597
|
+
const suspendInBackground = !config?.xmppSettings?.keepAliveInBackground;
|
|
597
598
|
const sub = AppState.addEventListener('change', (next) => {
|
|
598
599
|
const c = client;
|
|
599
600
|
if (next === 'active') {
|
|
601
|
+
if (c?.isSuspended) {
|
|
602
|
+
devPushLog('rn', 'AppState active: resuming suspended XMPP client');
|
|
603
|
+
c.resume();
|
|
604
|
+
}
|
|
600
605
|
// Returned to foreground. If the socket died while backgrounded
|
|
601
606
|
// (iOS suspends the WebSocket; long background kills it), the
|
|
602
607
|
// status may still read 'online' until xmpp.js notices the dead
|
|
@@ -624,6 +629,9 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
624
629
|
}
|
|
625
630
|
// Going to background/inactive (sleep / app switcher / tray):
|
|
626
631
|
if (!c) {return;}
|
|
632
|
+
if (next === 'background' && suspendInBackground) {
|
|
633
|
+
c.suspend().catch(() => undefined);
|
|
634
|
+
}
|
|
627
635
|
const state = store.getState();
|
|
628
636
|
const rooms = state.rooms?.rooms;
|
|
629
637
|
const visibleRoomJID = state.rooms?.visibleRoomJID || null;
|
|
@@ -655,7 +663,7 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
655
663
|
);
|
|
656
664
|
});
|
|
657
665
|
return () => sub.remove();
|
|
658
|
-
}, [client]);
|
|
666
|
+
}, [client, config?.xmppSettings?.keepAliveInBackground]);
|
|
659
667
|
|
|
660
668
|
// -----------------------------------------------------------
|
|
661
669
|
// Consumer visibility signal (`isVisible` prop). For hosts that keep
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
import { store } from '../roomStore';
|
|
3
|
+
import {
|
|
4
|
+
clearPendingNotificationJid,
|
|
5
|
+
setCurrentRoom,
|
|
6
|
+
setPendingNotificationJid,
|
|
7
|
+
} from '../roomStore/roomsSlice';
|
|
8
|
+
import { handleCallPush } from './incomingCallFromPush';
|
|
9
|
+
import { normalizeRoomJid } from './normalizeRoomJid';
|
|
10
|
+
|
|
11
|
+
export type PushPayloadOutcome = 'call' | 'room' | 'pending' | 'unknown';
|
|
12
|
+
|
|
13
|
+
export const PENDING_NOTIFICATION_JID_KEY = 'ethora_pending_notification_jid';
|
|
14
|
+
export const PENDING_NOTIFICATION_TTL_MS = 5 * 60 * 1000;
|
|
15
|
+
|
|
16
|
+
export const readPendingJid = async (): Promise<string | null> => {
|
|
17
|
+
try {
|
|
18
|
+
const raw = await AsyncStorage.getItem(PENDING_NOTIFICATION_JID_KEY);
|
|
19
|
+
if (!raw) {return null;}
|
|
20
|
+
if (raw.startsWith('{')) {
|
|
21
|
+
const { jid, at } = JSON.parse(raw) as { jid?: string; at?: number };
|
|
22
|
+
if (!jid || !at || Date.now() - at > PENDING_NOTIFICATION_TTL_MS) {
|
|
23
|
+
await AsyncStorage.removeItem(PENDING_NOTIFICATION_JID_KEY);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return jid;
|
|
27
|
+
}
|
|
28
|
+
return raw;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const openRoomFromPush = (jid: string): 'room' | 'pending' => {
|
|
35
|
+
const state = store.getState();
|
|
36
|
+
const conference = state.chatSettingStore?.config?.xmppSettings?.conference;
|
|
37
|
+
const full = normalizeRoomJid(jid, conference);
|
|
38
|
+
|
|
39
|
+
AsyncStorage.setItem(
|
|
40
|
+
PENDING_NOTIFICATION_JID_KEY,
|
|
41
|
+
JSON.stringify({ jid: full, at: Date.now() })
|
|
42
|
+
).catch(() => undefined);
|
|
43
|
+
if (state.rooms?.rooms?.[full]) {
|
|
44
|
+
store.dispatch(clearPendingNotificationJid());
|
|
45
|
+
store.dispatch(setCurrentRoom({ roomJID: full }));
|
|
46
|
+
return 'room';
|
|
47
|
+
}
|
|
48
|
+
store.dispatch(setPendingNotificationJid(full));
|
|
49
|
+
return 'pending';
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const handlePushPayload = (
|
|
53
|
+
data: Record<string, unknown> | null | undefined
|
|
54
|
+
): PushPayloadOutcome => {
|
|
55
|
+
if (!data) {return 'unknown';}
|
|
56
|
+
if (handleCallPush(data as Record<string, any>)) {return 'call';}
|
|
57
|
+
const jid = data.jid ?? data.chatJid ?? data.roomJid ?? data.roomJID;
|
|
58
|
+
if (typeof jid === 'string' && jid) {
|
|
59
|
+
return openRoomFromPush(jid);
|
|
60
|
+
}
|
|
61
|
+
return 'unknown';
|
|
62
|
+
};
|
|
@@ -4,7 +4,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
|
4
4
|
import { RootState } from '../roomStore';
|
|
5
5
|
import { setCurrentRoom, clearPendingNotificationJid } from '../roomStore/roomsSlice';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
import { PENDING_NOTIFICATION_JID_KEY, readPendingJid } from '../helpers/pushPayload';
|
|
8
8
|
|
|
9
9
|
export function usePendingNotification() {
|
|
10
10
|
const dispatch = useDispatch();
|
|
@@ -21,21 +21,15 @@ export function usePendingNotification() {
|
|
|
21
21
|
let jidToOpen = pendingNotificationJid;
|
|
22
22
|
|
|
23
23
|
if (!jidToOpen) {
|
|
24
|
-
|
|
25
|
-
const storedJid = await AsyncStorage.getItem(PENDING_NOTIFICATION_JID_KEY);
|
|
26
|
-
if (storedJid) {
|
|
27
|
-
jidToOpen = storedJid;
|
|
28
|
-
await AsyncStorage.removeItem(PENDING_NOTIFICATION_JID_KEY);
|
|
29
|
-
}
|
|
30
|
-
} catch (error) {
|
|
31
|
-
console.error('Error reading from AsyncStorage:', error);
|
|
32
|
-
}
|
|
24
|
+
jidToOpen = await readPendingJid();
|
|
33
25
|
}
|
|
34
26
|
|
|
35
27
|
if (jidToOpen) {
|
|
36
28
|
const room = rooms[jidToOpen];
|
|
37
29
|
if (room) {
|
|
38
|
-
|
|
30
|
+
dispatch(clearPendingNotificationJid());
|
|
31
|
+
AsyncStorage.removeItem(PENDING_NOTIFICATION_JID_KEY).catch(() => undefined);
|
|
32
|
+
dispatch(setCurrentRoom({ roomJID: jidToOpen }));
|
|
39
33
|
}
|
|
40
34
|
}
|
|
41
35
|
};
|
package/src/i18n/strings.ts
CHANGED
|
@@ -81,6 +81,9 @@ const en: UiStringTable = {
|
|
|
81
81
|
'action.save': 'Save',
|
|
82
82
|
'action.delete': 'Delete',
|
|
83
83
|
'action.leave': 'Leave',
|
|
84
|
+
'action.muted': 'Muted',
|
|
85
|
+
'action.unmuted': 'Unmuted',
|
|
86
|
+
'toast.muteFailed': 'Could not update notifications for this chat',
|
|
84
87
|
'action.create': 'Create',
|
|
85
88
|
'action.submit': 'Submit',
|
|
86
89
|
'action.translate': 'Translate',
|
|
@@ -309,6 +312,9 @@ const fr: UiStringTable = {
|
|
|
309
312
|
'action.save': 'Enregistrer',
|
|
310
313
|
'action.delete': 'Supprimer',
|
|
311
314
|
'action.leave': 'Quitter',
|
|
315
|
+
'action.muted': 'En sourdine',
|
|
316
|
+
'action.unmuted': 'Activé',
|
|
317
|
+
'toast.muteFailed': 'Impossible de modifier les notifications de ce chat',
|
|
312
318
|
'action.create': 'Créer',
|
|
313
319
|
'action.submit': 'Soumettre',
|
|
314
320
|
'action.translate': 'Traduire',
|
|
@@ -537,6 +543,9 @@ const es: UiStringTable = {
|
|
|
537
543
|
'action.save': 'Guardar',
|
|
538
544
|
'action.delete': 'Eliminar',
|
|
539
545
|
'action.leave': 'Salir',
|
|
546
|
+
'action.muted': 'Silenciado',
|
|
547
|
+
'action.unmuted': 'Activado',
|
|
548
|
+
'toast.muteFailed': 'No se pudieron cambiar las notificaciones de este chat',
|
|
540
549
|
'action.create': 'Crear',
|
|
541
550
|
'action.submit': 'Enviar',
|
|
542
551
|
'action.translate': 'Traducir',
|
|
@@ -765,6 +774,9 @@ const pt: UiStringTable = {
|
|
|
765
774
|
'action.save': 'Guardar',
|
|
766
775
|
'action.delete': 'Eliminar',
|
|
767
776
|
'action.leave': 'Sair',
|
|
777
|
+
'action.muted': 'Muted',
|
|
778
|
+
'action.unmuted': 'Unmuted',
|
|
779
|
+
'toast.muteFailed': 'Could not update notifications for this chat',
|
|
768
780
|
'action.create': 'Criar',
|
|
769
781
|
'action.submit': 'Enviar',
|
|
770
782
|
'action.translate': 'Traduzir',
|
|
@@ -993,6 +1005,9 @@ const ht: UiStringTable = {
|
|
|
993
1005
|
'action.save': 'Anrejistre',
|
|
994
1006
|
'action.delete': 'Efase',
|
|
995
1007
|
'action.leave': 'Kite',
|
|
1008
|
+
'action.muted': 'Muted',
|
|
1009
|
+
'action.unmuted': 'Unmuted',
|
|
1010
|
+
'toast.muteFailed': 'Could not update notifications for this chat',
|
|
996
1011
|
'action.create': 'Kreye',
|
|
997
1012
|
'action.submit': 'Soumèt',
|
|
998
1013
|
'action.translate': 'Tradui',
|
|
@@ -1221,6 +1236,9 @@ const zh: UiStringTable = {
|
|
|
1221
1236
|
'action.save': '保存',
|
|
1222
1237
|
'action.delete': '删除',
|
|
1223
1238
|
'action.leave': '退出',
|
|
1239
|
+
'action.muted': 'Muted',
|
|
1240
|
+
'action.unmuted': 'Unmuted',
|
|
1241
|
+
'toast.muteFailed': 'Could not update notifications for this chat',
|
|
1224
1242
|
'action.create': '创建',
|
|
1225
1243
|
'action.submit': '提交',
|
|
1226
1244
|
'action.translate': '翻译',
|
package/src/main.ts
CHANGED
|
@@ -31,6 +31,9 @@ export { XmppProvider } from './context/xmppProvider';
|
|
|
31
31
|
|
|
32
32
|
// Hooks that consumers usually need (badge counts, logout, push, etc.).
|
|
33
33
|
export { useUnread } from './hooks/useUnreadMessagesCounter';
|
|
34
|
+
|
|
35
|
+
export { handlePushPayload, openRoomFromPush } from './helpers/pushPayload';
|
|
36
|
+
export type { PushPayloadOutcome } from './helpers/pushPayload';
|
|
34
37
|
export { useChatRoomFocus } from './hooks/useChatRoomFocus';
|
|
35
38
|
export { logoutService, useLogout } from './hooks/useLogout';
|
|
36
39
|
export { useQRCodeChat, handleQRChatId } from './hooks/useQRCodeChatHandler';
|
|
@@ -130,6 +130,7 @@ function dispatchRoomsFromRestItems(items: ApiRoom[]): void {
|
|
|
130
130
|
icon: item.picture || item.icon,
|
|
131
131
|
createdAt: item.createdAt,
|
|
132
132
|
updatedAt: item.updatedAt,
|
|
133
|
+
muted: item.muted === true,
|
|
133
134
|
description: (item as any).description,
|
|
134
135
|
type: (item as any).type,
|
|
135
136
|
// ChatProfileModal renders this list under the description/type
|
|
@@ -254,6 +255,14 @@ export async function postPrivateRoom(
|
|
|
254
255
|
}
|
|
255
256
|
}
|
|
256
257
|
|
|
258
|
+
export async function setRoomMuted(chatName: string, muted: boolean): Promise<boolean> {
|
|
259
|
+
const token = store.getState().chatSettingStore.user.token || '';
|
|
260
|
+
const url = `/v1/chats/my/${encodeURIComponent(chatName)}/mute`;
|
|
261
|
+
const config = { headers: { Authorization: token } };
|
|
262
|
+
const response = muted ? await http.put(url, {}, config) : await http.delete(url, config);
|
|
263
|
+
return response.data?.result?.muted ?? muted;
|
|
264
|
+
}
|
|
265
|
+
|
|
257
266
|
export async function postReportRoom(data: PostReportRoom) {
|
|
258
267
|
const { chatName, category, text } = data;
|
|
259
268
|
const token = store.getState().chatSettingStore.user.token || '';
|
|
@@ -2,12 +2,14 @@ import { Client, xml } from '@xmpp/client';
|
|
|
2
2
|
import { Element } from '@xmpp/xml';
|
|
3
3
|
import { createTimeoutPromise } from './createTimeoutPromise.xmpp';
|
|
4
4
|
|
|
5
|
+
let subscribeIdCounter = 0;
|
|
6
|
+
|
|
5
7
|
export async function subscribeToRoomMessages(
|
|
6
8
|
client: Client,
|
|
7
9
|
roomJID: string,
|
|
8
10
|
userNick?: string
|
|
9
11
|
): Promise<boolean> {
|
|
10
|
-
const id = `newSubscription:${Date.now().toString()}`;
|
|
12
|
+
const id = `newSubscription:${Date.now().toString(36)}-${(++subscribeIdCounter).toString(36)}`;
|
|
11
13
|
let stanzaHandler: (stanza: Element) => void;
|
|
12
14
|
|
|
13
15
|
const unsubscribe = () => {
|
|
@@ -33,6 +35,15 @@ export async function subscribeToRoomMessages(
|
|
|
33
35
|
if (stanza.attrs.type === 'result') {
|
|
34
36
|
finish(resolve, true);
|
|
35
37
|
} else if (stanza.attrs.type === 'error') {
|
|
38
|
+
const errorEl = typeof stanza.getChild === 'function' ? stanza.getChild('error') : null;
|
|
39
|
+
const condition = errorEl?.children?.find(
|
|
40
|
+
(c: any) => c?.name && c.name !== 'text'
|
|
41
|
+
) as Element | undefined;
|
|
42
|
+
console.warn(
|
|
43
|
+
'[PushService] MucSub subscribe error for',
|
|
44
|
+
roomJID,
|
|
45
|
+
condition?.name ?? 'unknown'
|
|
46
|
+
);
|
|
36
47
|
finish(resolve, false);
|
|
37
48
|
}
|
|
38
49
|
}
|
|
@@ -342,6 +342,7 @@ export class XmppClient {
|
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
initializeClient() {
|
|
345
|
+
this.clientAlreadyStopped = false;
|
|
345
346
|
try {
|
|
346
347
|
const devServer = this.devServer || DEFAULT_DEV_SERVER;
|
|
347
348
|
const url = `wss://${devServer}/ws`;
|
|
@@ -752,7 +753,10 @@ export class XmppClient {
|
|
|
752
753
|
// `this.client` still points at the old client — i.e. before
|
|
753
754
|
// initializeClient() reassigns it.
|
|
754
755
|
const old = this.client;
|
|
755
|
-
if (old) {
|
|
756
|
+
if (old && this.clientAlreadyStopped) {
|
|
757
|
+
this.detachEventListeners();
|
|
758
|
+
this.clientAlreadyStopped = false;
|
|
759
|
+
} else if (old) {
|
|
756
760
|
this.detachEventListeners();
|
|
757
761
|
// NEVER await a bare stop(): on a half-dead socket (the exact case
|
|
758
762
|
// we're reconnecting for) @xmpp/client's stop() can hang forever
|
|
@@ -767,6 +771,9 @@ export class XmppClient {
|
|
|
767
771
|
await withTimeout(old.stop(), STOP_TIMEOUT_MS);
|
|
768
772
|
}
|
|
769
773
|
this.initializeClient();
|
|
774
|
+
if (this.suspendedForBackground) {
|
|
775
|
+
await this.stopClientQuietly();
|
|
776
|
+
}
|
|
770
777
|
} finally {
|
|
771
778
|
this.reconnecting = false;
|
|
772
779
|
}
|
|
@@ -804,6 +811,56 @@ export class XmppClient {
|
|
|
804
811
|
return this.credentialsRefreshInFlight;
|
|
805
812
|
}
|
|
806
813
|
|
|
814
|
+
private suspendedForBackground = false;
|
|
815
|
+
private clientAlreadyStopped = false;
|
|
816
|
+
|
|
817
|
+
private async stopClientQuietly(): Promise<void> {
|
|
818
|
+
const c: any = this.client;
|
|
819
|
+
if (c) {
|
|
820
|
+
const sock = c.socket;
|
|
821
|
+
if (sock && typeof sock.end === 'function') {
|
|
822
|
+
try {
|
|
823
|
+
sock.end();
|
|
824
|
+
} catch {}
|
|
825
|
+
try {
|
|
826
|
+
if (c.status !== 'offline') c._status?.('offline');
|
|
827
|
+
} catch {}
|
|
828
|
+
} else {
|
|
829
|
+
await withTimeout(
|
|
830
|
+
Promise.resolve(c.stop()).catch(() => undefined),
|
|
831
|
+
STOP_TIMEOUT_MS
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
this.clientAlreadyStopped = true;
|
|
835
|
+
}
|
|
836
|
+
this.status = 'offline';
|
|
837
|
+
this.presencesReady = false;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
async suspend(): Promise<void> {
|
|
841
|
+
if (this.suspendedForBackground) {return;}
|
|
842
|
+
this.suspendedForBackground = true;
|
|
843
|
+
this.suppressReconnect = true;
|
|
844
|
+
if (this.reconnectTimer) {
|
|
845
|
+
clearTimeout(this.reconnectTimer);
|
|
846
|
+
this.reconnectTimer = null;
|
|
847
|
+
}
|
|
848
|
+
await this.stopClientQuietly();
|
|
849
|
+
console.log('XMPP suspended (background).');
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/** Back in the foreground: allow reconnects again and reconnect at once. */
|
|
853
|
+
resume(): void {
|
|
854
|
+
if (!this.suspendedForBackground) {return;}
|
|
855
|
+
this.suspendedForBackground = false;
|
|
856
|
+
this.suppressReconnect = false;
|
|
857
|
+
this.forceReconnect();
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
get isSuspended(): boolean {
|
|
861
|
+
return this.suspendedForBackground;
|
|
862
|
+
}
|
|
863
|
+
|
|
807
864
|
async disconnect(options?: { suppressReconnect?: boolean }): Promise<void> {
|
|
808
865
|
if (options?.suppressReconnect) {this.suppressReconnect = true;}
|
|
809
866
|
return this.close();
|
|
@@ -81,7 +81,9 @@ export class PushSubscriptionService {
|
|
|
81
81
|
this.subscribedRooms.add(roomJID);
|
|
82
82
|
await this.saveSubscribedRoomsToStorage();
|
|
83
83
|
}
|
|
84
|
-
|
|
84
|
+
if (!result) {
|
|
85
|
+
console.warn('[PushService] MucSub subscribe refused for', roomJID);
|
|
86
|
+
}
|
|
85
87
|
return result;
|
|
86
88
|
} catch (error) {
|
|
87
89
|
console.error(`[PushService] Failed to subscribe to room ${roomJID}:`, error);
|
|
@@ -92,30 +94,33 @@ export class PushSubscriptionService {
|
|
|
92
94
|
async subscribeToRooms(
|
|
93
95
|
client: Client,
|
|
94
96
|
roomJIDs: string[],
|
|
95
|
-
userNick?: string
|
|
97
|
+
userNick?: string,
|
|
98
|
+
concurrency = 8
|
|
96
99
|
): Promise<void> {
|
|
97
100
|
let successful = 0;
|
|
98
101
|
let failed = 0;
|
|
99
102
|
|
|
100
|
-
|
|
103
|
+
await this.loadSubscribedRoomsFromStorage();
|
|
104
|
+
const pending = roomJIDs.filter((jid) => !this.subscribedRooms.has(jid));
|
|
105
|
+
if (!pending.length) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
101
108
|
|
|
102
|
-
for (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
for (let i = 0; i < pending.length; i += concurrency) {
|
|
110
|
+
const batch = pending.slice(i, i + concurrency);
|
|
111
|
+
const results = await Promise.allSettled(
|
|
112
|
+
batch.map((roomJID) => this.subscribeToRoom(client, roomJID, userNick))
|
|
113
|
+
);
|
|
114
|
+
for (const r of results) {
|
|
115
|
+
if (r.status === 'fulfilled' && r.value) {
|
|
107
116
|
successful++;
|
|
108
117
|
} else {
|
|
109
118
|
failed++;
|
|
110
119
|
}
|
|
111
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
112
|
-
} catch (error) {
|
|
113
|
-
failed++;
|
|
114
|
-
console.error(`Failed to subscribe to room ${roomJID}:`, error);
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
122
|
|
|
118
|
-
console.log(`
|
|
123
|
+
console.log(`[PushService] MucSub: ${successful} rooms subscribed, ${failed} failed`);
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
async reset(): Promise<void> {
|
package/src/types/types.ts
CHANGED
|
@@ -34,6 +34,7 @@ export interface IRoom {
|
|
|
34
34
|
lastViewedTimestamp?: number;
|
|
35
35
|
unreadMessages?: number;
|
|
36
36
|
noMessages?: boolean;
|
|
37
|
+
muted?: boolean;
|
|
37
38
|
role?: string;
|
|
38
39
|
createdAt?: string | number | Date;
|
|
39
40
|
updatedAt?: string | number | Date;
|
|
@@ -151,6 +152,7 @@ export interface xmppSettingsInterface {
|
|
|
151
152
|
disableLastRead?: boolean;
|
|
152
153
|
xmppPingOnSendEnabled?: boolean;
|
|
153
154
|
historyQoS?: HistoryQoSConfig;
|
|
155
|
+
keepAliveInBackground?: boolean;
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
export interface InAppNotificationConfig {
|