Skip to content

Commit e0fec90

Browse files
committed
fix: add error notification when loading members fails
1 parent 534828a commit e0fec90

16 files changed

Lines changed: 89 additions & 30 deletions

File tree

package/src/components/ChannelDetailsScreen/__tests__/members/useChannelAllMembers.test.tsx

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,28 @@
1+
import React from 'react';
2+
13
import { act, renderHook, waitFor } from '@testing-library/react-native';
24
import type { Channel, ChannelMemberResponse } from 'stream-chat';
35

6+
import { TranslationProvider } from '../../../../contexts/translationContext/TranslationContext';
47
import { generateMember } from '../../../../mock-builders/generator/member';
58
import { generateUser } from '../../../../mock-builders/generator/user';
9+
import { useNotificationApi } from '../../../Notifications/hooks/useNotificationApi';
610
import { useChannelAllMembers } from '../../hooks/members/useChannelAllMembers';
711

12+
jest.mock('../../../Notifications/hooks/useNotificationApi', () => ({
13+
useNotificationApi: jest.fn(() => ({ addNotification: jest.fn() })),
14+
}));
15+
16+
const t = ((key: string) => key) as never;
17+
18+
const translationWrapper = ({ children }: { children: React.ReactNode }) => (
19+
<TranslationProvider
20+
value={{ t, tDateTimeParser: ((input: unknown) => input) as never, userLanguage: 'en' }}
21+
>
22+
{children}
23+
</TranslationProvider>
24+
);
25+
826
type QueryMembersMock = jest.Mock<
927
Promise<{ members: ChannelMemberResponse[] }>,
1028
[unknown, unknown, unknown]
@@ -37,14 +55,11 @@ const buildMembers = (count: number, prefix = 'u') =>
3755
);
3856

3957
describe('useChannelAllMembers', () => {
40-
let warnSpy: jest.SpyInstance;
58+
let addNotification: jest.Mock;
4159

4260
beforeEach(() => {
43-
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
44-
});
45-
46-
afterEach(() => {
47-
warnSpy.mockRestore();
61+
addNotification = jest.fn();
62+
(useNotificationApi as jest.Mock).mockReturnValue({ addNotification });
4863
});
4964

5065
describe('local mode', () => {
@@ -192,21 +207,27 @@ describe('useChannelAllMembers', () => {
192207
await waitFor(() => expect(result.current.loading).toBe(false));
193208
});
194209

195-
it('recovers from a queryMembers rejection', async () => {
210+
it('recovers from a queryMembers rejection and notifies the user', async () => {
196211
const queryMembers: QueryMembersMock = jest.fn().mockRejectedValue(new Error('boom'));
197212
const channel = buildChannel({
198213
memberCount: 200,
199214
members: buildMembers(25, 'loaded'),
200215
queryMembers,
201216
});
202217

203-
const { result } = renderHook(() => useChannelAllMembers({ channel }));
218+
const { result } = renderHook(() => useChannelAllMembers({ channel }), {
219+
wrapper: translationWrapper,
220+
});
204221

205222
await waitFor(() => expect(result.current.loading).toBe(false));
206223
expect(result.current.results).toEqual([]);
207-
expect(warnSpy).toHaveBeenCalledWith(
208-
'[useChannelAllMembers] queryMembers failed',
209-
expect.any(Error),
224+
expect(addNotification).toHaveBeenCalledWith(
225+
expect.objectContaining({
226+
options: expect.objectContaining({
227+
severity: 'error',
228+
type: 'api:channel:query-members:failed',
229+
}),
230+
}),
210231
);
211232
});
212233
});

package/src/components/ChannelDetailsScreen/components/members/ChannelAllMembersModal.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { useTranslationContext } from '../../../../contexts/translationContext/T
66
import { useChannelMemberCount } from '../../../../hooks';
77
import { useChannelOwnCapabilities } from '../../../../hooks/useChannelOwnCapabilities';
88
import { UserAdd } from '../../../../icons/user-add';
9+
import { NotificationList } from '../../../Notifications/NotificationList';
10+
import { NotificationTargetProvider } from '../../../Notifications/NotificationTargetContext';
911
import { Button } from '../../../ui/Button/Button';
1012
import { ChannelDetailsModal } from '../modal/Modal';
1113
import { ModalHeader } from '../modal/ModalHeader';
@@ -50,6 +52,7 @@ const ChannelAllMembersModalContent = ({
5052
title={t('{{count}} members', { count: total })}
5153
/>
5254
<ChannelMemberList />
55+
<NotificationList />
5356
</>
5457
);
5558
};
@@ -61,11 +64,20 @@ export const ChannelAllMembersModal = ({
6164
onAddMembersPress,
6265
onClose,
6366
visible,
64-
}: ChannelAllMembersModalProps) => (
67+
}: ChannelAllMembersModalProps) => {
68+
const { channel } = useChannelDetailsContext();
69+
const notificationHostId = channel?.cid ? `channel-member-list:${channel.cid}` : undefined;
70+
6571
// The content lives in a child component so its hooks (member-state subscription,
6672
// member preview sort) only run while the modal is open. React Native's `Modal`
6773
// renders `null` when `visible` is false, so the child never mounts until then.
68-
<ChannelDetailsModal onClose={onClose} visible={visible}>
69-
<ChannelAllMembersModalContent onAddMembersPress={onAddMembersPress} onClose={onClose} />
70-
</ChannelDetailsModal>
71-
);
74+
return (
75+
<ChannelDetailsModal onClose={onClose} visible={visible}>
76+
{notificationHostId ? (
77+
<NotificationTargetProvider hostId={notificationHostId} panel='channel-details'>
78+
<ChannelAllMembersModalContent onAddMembersPress={onAddMembersPress} onClose={onClose} />
79+
</NotificationTargetProvider>
80+
) : null}
81+
</ChannelDetailsModal>
82+
);
83+
};

package/src/components/ChannelDetailsScreen/hooks/members/useChannelAllMembers.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
22

33
import type { Channel, ChannelMemberResponse, MemberFilters, MemberSort } from 'stream-chat';
44

5+
import { useTranslationContext } from '../../../../contexts';
6+
import { getNotificationErrorOptions } from '../../../../hooks/actions/useChannelActions';
57
import { useChannelMembersState } from '../../../ChannelList/hooks/useChannelMembersState';
8+
import { useNotificationApi } from '../../../Notifications/hooks/useNotificationApi';
69

710
const PAGE_SIZE = 25;
811

@@ -23,6 +26,8 @@ export const useChannelAllMembers = ({
2326
}: {
2427
channel: Channel;
2528
}): UseChannelAllMembersResult => {
29+
const { addNotification } = useNotificationApi();
30+
const { t } = useTranslationContext();
2631
const localMembers = useChannelMembersState(channel);
2732

2833
// Mode is decided once on mount (per channel). If member_count is unknown or matches
@@ -75,15 +80,23 @@ export const useChannelAllMembers = ({
7580
}
7681
} catch (err) {
7782
if (requestId !== requestIdRef.current) return;
78-
console.warn('[useChannelAllMembers] queryMembers failed', err);
83+
addNotification({
84+
message: t('Failed to load members'),
85+
options: {
86+
...getNotificationErrorOptions(err),
87+
severity: 'error',
88+
type: 'api:channel:query-members:failed',
89+
},
90+
origin: { context: { channel }, emitter: 'ChannelAllMembers' },
91+
});
7992
} finally {
8093
if (requestId === requestIdRef.current) {
8194
inFlightRef.current = false;
8295
setLoading(false);
8396
}
8497
}
8598
},
86-
[channel],
99+
[addNotification, channel, t],
87100
);
88101

89102
const fetchPageRef = useRef(fetchPage);

package/src/i18n/ar.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,5 +409,6 @@
409409
"a11y/Double tap and hold to activate contextual menu": "انقر نقرًا مزدوجًا مع الاستمرار لتفعيل قائمة السياق",
410410
"a11y/Swipe right to go through different actions": "اسحب لليمين للتنقل بين الإجراءات المختلفة",
411411
"a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.",
412-
"Muted": "مكتوم"
412+
"Muted": "مكتوم",
413+
"Failed to load members": "فشل تحميل الأعضاء"
413414
}

package/src/i18n/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@
379379
"Failed to block user": "Failed to block user",
380380
"Failed to delete channel": "Failed to delete channel",
381381
"Failed to leave channel": "Failed to leave channel",
382+
"Failed to load members": "Failed to load members",
382383
"Failed to load users": "Failed to load users",
383384
"Failed to play the recording": "Failed to play the recording",
384385
"Failed to update channel archive status": "Failed to update channel archive status",

package/src/i18n/es.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,5 +409,6 @@
409409
"a11y/Double tap and hold to activate contextual menu": "Toca dos veces y mantén pulsado para activar el menú contextual",
410410
"a11y/Swipe right to go through different actions": "Desliza a la derecha para recorrer las diferentes acciones",
411411
"a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.",
412-
"Muted": "Silenciado"
412+
"Muted": "Silenciado",
413+
"Failed to load members": "No se pudieron cargar los miembros"
413414
}

package/src/i18n/fr.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,5 +409,6 @@
409409
"a11y/Double tap and hold to activate contextual menu": "Appuyez deux fois et maintenez pour activer le menu contextuel",
410410
"a11y/Swipe right to go through different actions": "Glissez vers la droite pour parcourir les différentes actions",
411411
"a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.",
412-
"Muted": "En sourdine"
412+
"Muted": "En sourdine",
413+
"Failed to load members": "Échec du chargement des membres"
413414
}

package/src/i18n/he.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,5 +409,6 @@
409409
"a11y/Double tap and hold to activate contextual menu": "הקש פעמיים והחזק כדי להפעיל את התפריט ההקשרי",
410410
"a11y/Swipe right to go through different actions": "החלק ימינה כדי לעבור בין הפעולות השונות",
411411
"a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.",
412-
"Muted": "מושתק"
412+
"Muted": "מושתק",
413+
"Failed to load members": "טעינת החברים נכשלה"
413414
}

package/src/i18n/hi.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,5 +409,6 @@
409409
"a11y/Double tap and hold to activate contextual menu": "संदर्भ मेनू सक्रिय करने के लिए दो बार टैप करें और होल्ड करें",
410410
"a11y/Swipe right to go through different actions": "विभिन्न क्रियाओं के बीच जाने के लिए दाएं स्वाइप करें",
411411
"a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.",
412-
"Muted": "म्यूट किया गया"
412+
"Muted": "म्यूट किया गया",
413+
"Failed to load members": "सदस्यों को लोड करने में विफल"
413414
}

package/src/i18n/it.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,5 +409,6 @@
409409
"a11y/Double tap and hold to activate contextual menu": "Tocca due volte e tieni premuto per attivare il menu contestuale",
410410
"a11y/Swipe right to go through different actions": "Scorri a destra per passare in rassegna le diverse azioni",
411411
"a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.",
412-
"Muted": "Silenziato"
412+
"Muted": "Silenziato",
413+
"Failed to load members": "Impossibile caricare i membri"
413414
}

0 commit comments

Comments
 (0)