Skip to content

Commit 2e3da00

Browse files
committed
Add notification for channel delete, add onSucess for channel actions
1 parent 30ee37c commit 2e3da00

19 files changed

Lines changed: 377 additions & 104 deletions

package/src/components/ChannelDetailsScreen/ChannelDetailsScreen.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { useComponentsContext } from '../../contexts/componentsContext/Component
1212
import { useTheme } from '../../contexts/themeContext/ThemeContext';
1313
import { useIsDirectChat } from '../../hooks/useIsDirectChat';
1414
import { primitives } from '../../theme';
15+
import { NotificationList } from '../Notifications/NotificationList';
16+
import { NotificationTargetProvider } from '../Notifications/NotificationTargetContext';
1517

1618
export type ChannelDetailsScreenProps = {
1719
channel: Channel;
@@ -64,6 +66,7 @@ export const ChannelDetailsScreenContent = () => {
6466
{isDirect ? null : <ChannelDetailsMemberSection />}
6567
<ChannelDetailsActionsSection />
6668
</ScrollView>
69+
<NotificationList />
6770
</View>
6871
);
6972
};
@@ -82,10 +85,17 @@ export const ChannelDetailsScreen = ({
8285
[channel, onAddMembersPress, onBack, onChannelDismiss, onViewAllMembersPress],
8386
);
8487
const Content = ChannelDetailsScreenContentOverride ?? ChannelDetailsScreenContent;
88+
const notificationHostId = channel?.cid ? `channel-details:${channel.cid}` : undefined;
8589

8690
return (
8791
<ChannelDetailsContextProvider value={value}>
88-
<Content />
92+
{notificationHostId ? (
93+
<NotificationTargetProvider hostId={notificationHostId} panel='channel-details'>
94+
<Content />
95+
</NotificationTargetProvider>
96+
) : (
97+
<Content />
98+
)}
8999
</ChannelDetailsContextProvider>
90100
);
91101
};

package/src/components/ChannelDetailsScreen/__tests__/ChannelDetailsMemberSection.test.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import { Pressable, Text } from 'react-native';
33

44
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react-native';
5+
import { NotificationManager } from 'stream-chat';
56
import type { Channel, ChannelMemberResponse, UserResponse } from 'stream-chat';
67

78
import { AccessibilityProvider } from '../../../contexts/accessibilityContext/AccessibilityContext';
@@ -95,7 +96,9 @@ const renderSection = ({
9596
userLanguage: 'en',
9697
}}
9798
>
98-
<ChatContext.Provider value={{ client: { userID: 'me' } } as never}>
99+
<ChatContext.Provider
100+
value={{ client: { notifications: new NotificationManager(), userID: 'me' } } as never}
101+
>
99102
<ChannelDetailsContextProvider
100103
value={{
101104
channel: applyCapabilities(channel, capabilities),

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

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,39 @@
1-
import React from 'react';
1+
import React, { PropsWithChildren } from 'react';
22
import { Text } from 'react-native';
33

44
import { render, screen } from '@testing-library/react-native';
5+
import { NotificationManager } from 'stream-chat';
56
import type { Channel } from 'stream-chat';
67

78
import { useChannelDetailsContext } from '../../../contexts/channelDetailsContext/channelDetailsContext';
9+
import { ChatContext } from '../../../contexts/chatContext/ChatContext';
810
import { WithComponents } from '../../../contexts/componentsContext/ComponentsContext';
911
import type { OwnCapabilitiesContextValue } from '../../../contexts/ownCapabilitiesContext/OwnCapabilitiesContext';
1012
import { useOwnCapabilitiesContext } from '../../../contexts/ownCapabilitiesContext/OwnCapabilitiesContext';
1113
import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext';
1214
import { defaultTheme } from '../../../contexts/themeContext/utils/theme';
15+
import { TranslationProvider } from '../../../contexts/translationContext/TranslationContext';
1316
import * as useIsDirectChatModule from '../../../hooks/useIsDirectChat';
1417
import { ChannelDetailsScreen } from '../ChannelDetailsScreen';
1518

19+
const Providers = ({ children }: PropsWithChildren) => (
20+
<ThemeProvider theme={defaultTheme}>
21+
<TranslationProvider
22+
value={{
23+
t: ((key: string) => key) as never,
24+
tDateTimeParser: ((input: unknown) => input) as never,
25+
userLanguage: 'en',
26+
}}
27+
>
28+
<ChatContext.Provider
29+
value={{ client: { notifications: new NotificationManager(), userID: 'me' } } as never}
30+
>
31+
{children}
32+
</ChatContext.Provider>
33+
</TranslationProvider>
34+
</ThemeProvider>
35+
);
36+
1637
const HeaderProbe = () => <Text testID='probe-header'>HEADER</Text>;
1738
const ProfileProbe = () => <Text testID='probe-profile'>PROFILE</Text>;
1839
const NavigationProbe = () => <Text testID='probe-navigation'>NAVIGATION</Text>;
@@ -35,11 +56,11 @@ const channel = {
3556

3657
const renderContent = () =>
3758
render(
38-
<ThemeProvider theme={defaultTheme}>
59+
<Providers>
3960
<WithComponents overrides={SECTION_OVERRIDES}>
4061
<ChannelDetailsScreen channel={channel} />
4162
</WithComponents>
42-
</ThemeProvider>,
63+
</Providers>,
4364
);
4465

4566
describe('ChannelDetailsScreenContent', () => {
@@ -94,7 +115,7 @@ describe('ChannelDetailsScreen', () => {
94115
};
95116

96117
render(
97-
<ThemeProvider theme={defaultTheme}>
118+
<Providers>
98119
<WithComponents
99120
overrides={{
100121
...SECTION_OVERRIDES,
@@ -107,7 +128,7 @@ describe('ChannelDetailsScreen', () => {
107128
onChannelDismiss={onChannelDismiss}
108129
/>
109130
</WithComponents>
110-
</ThemeProvider>,
131+
</Providers>,
111132
);
112133

113134
expect(captured).toBeDefined();
@@ -132,7 +153,7 @@ describe('ChannelDetailsScreen', () => {
132153
};
133154

134155
render(
135-
<ThemeProvider theme={defaultTheme}>
156+
<Providers>
136157
<WithComponents
137158
overrides={{
138159
...SECTION_OVERRIDES,
@@ -141,7 +162,7 @@ describe('ChannelDetailsScreen', () => {
141162
>
142163
<ChannelDetailsScreen channel={channelWithCapabilities} />
143164
</WithComponents>
144-
</ThemeProvider>,
165+
</Providers>,
145166
);
146167

147168
expect(captured).toBeDefined();
@@ -159,7 +180,7 @@ describe('ChannelDetailsScreen', () => {
159180
it('renders the override instead of the default content', () => {
160181
const Override = () => <Text testID='custom-content'>CUSTOM</Text>;
161182
render(
162-
<ThemeProvider theme={defaultTheme}>
183+
<Providers>
163184
<WithComponents
164185
overrides={{
165186
...SECTION_OVERRIDES,
@@ -168,7 +189,7 @@ describe('ChannelDetailsScreen', () => {
168189
>
169190
<ChannelDetailsScreen channel={channel} />
170191
</WithComponents>
171-
</ThemeProvider>,
192+
</Providers>,
172193
);
173194

174195
expect(screen.getByTestId('custom-content')).toBeTruthy();
@@ -184,11 +205,11 @@ describe('ChannelDetailsScreen', () => {
184205
// Note: re-export the default Content via the override map so we can prove it
185206
// wasn't swapped out — the section probes from SECTION_OVERRIDES should appear.
186207
render(
187-
<ThemeProvider theme={defaultTheme}>
208+
<Providers>
188209
<WithComponents overrides={SECTION_OVERRIDES}>
189210
<ChannelDetailsScreen channel={channel} />
190211
</WithComponents>
191-
</ThemeProvider>,
212+
</Providers>,
192213
);
193214
expect(screen.getByTestId('probe-header')).toBeTruthy();
194215
expect(screen.getByTestId('probe-actions')).toBeTruthy();

package/src/components/ChannelDetailsScreen/components/ChannelDetailsMemberSection.tsx

Lines changed: 87 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ import { useComponentsContext } from '../../../contexts/componentsContext/Compon
99
import { useOwnCapabilitiesContext } from '../../../contexts/ownCapabilitiesContext/OwnCapabilitiesContext';
1010
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
1111
import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
12+
import { getNotificationErrorOptions } from '../../../hooks/useChannelActions';
1213
import { useStableCallback } from '../../../hooks/useStableCallback';
1314
import { Checkmark } from '../../../icons/checkmark-1';
1415
import { UserAdd } from '../../../icons/user-add';
1516
import { NewClose } from '../../../icons/xmark';
1617
import { primitives } from '../../../theme';
18+
import { useNotificationApi } from '../../Notifications/hooks';
19+
import { NotificationList } from '../../Notifications/NotificationList';
20+
import { NotificationTargetProvider } from '../../Notifications/NotificationTargetContext';
1721
import { Button } from '../../ui/Button/Button';
1822
import { BottomSheetModal } from '../../UIComponents/BottomSheetModal';
1923
import { useChannelDetailsMembersPreview } from '../hooks/useChannelDetailsMembersPreview';
@@ -22,8 +26,12 @@ export const ChannelDetailsMemberSection = () => {
2226
const { channel, onAddMembersPress, onViewAllMembersPress } = useChannelDetailsContext();
2327
const { client } = useChatContext();
2428
const { t } = useTranslationContext();
29+
const { addNotification } = useNotificationApi();
2530
const { updateChannelMembers } = useOwnCapabilitiesContext();
2631
const { height: windowHeight } = useWindowDimensions();
32+
const addMembersNotificationHostId = channel?.cid
33+
? `channel-add-members:${channel.cid}`
34+
: undefined;
2735
const {
2836
theme: {
2937
channelDetailsScreen: {
@@ -72,12 +80,26 @@ export const ChannelDetailsMemberSection = () => {
7280
const handleAddMembersConfirm = useStableCallback(async () => {
7381
if (!addMembersSelection.length || addingMembers) return;
7482
setAddingMembers(true);
83+
const addedCount = addMembersSelection.length;
7584
try {
7685
await channel.addMembers(addMembersSelection.map((u) => u.id));
86+
addNotification({
87+
message: t('{{count}} members added', { count: addedCount }),
88+
options: { severity: 'success', type: 'api:channel:add-members:success' },
89+
origin: { context: { channel }, emitter: 'ChannelDetailsMemberSection' },
90+
});
7791
setAddMembersVisible(false);
7892
setAddMembersSelection([]);
79-
} catch (err) {
80-
console.warn('[ChannelDetailsMemberSection] failed to add members', err);
93+
} catch (error) {
94+
addNotification({
95+
message: t('Failed to add members'),
96+
options: {
97+
...getNotificationErrorOptions(error),
98+
severity: 'error',
99+
type: 'api:channel:add-members:failed',
100+
},
101+
origin: { context: { channel }, emitter: 'ChannelDetailsMemberSection' },
102+
});
81103
} finally {
82104
setAddingMembers(false);
83105
}
@@ -205,59 +227,66 @@ export const ChannelDetailsMemberSection = () => {
205227
onClose={handleAddMembersClose}
206228
visible={isAddMembersVisible}
207229
>
208-
<View style={[styles.modalHeader, modalHeaderOverride]}>
209-
<View style={styles.modalHeaderSide}>
210-
<Button
211-
accessibilityLabelKey='a11y/Close'
212-
iconOnly
213-
LeadingIcon={NewClose}
214-
onPress={handleAddMembersClose}
215-
size='md'
216-
type='outline'
217-
variant='secondary'
218-
/>
219-
</View>
220-
<View style={styles.modalHeaderCenter}>
221-
<Text
222-
accessibilityRole='header'
223-
numberOfLines={1}
224-
style={[
225-
styles.modalHeaderTitle,
226-
{ color: semantics.textPrimary },
227-
modalHeaderTitleOverride,
228-
]}
229-
>
230-
{t('Add Members')}
231-
</Text>
232-
</View>
233-
<View style={[styles.modalHeaderSide, styles.modalHeaderSideRight]}>
234-
<Pressable
235-
accessibilityLabel={t('a11y/Confirm add members')}
236-
accessibilityRole='button'
237-
accessibilityState={{ disabled: !confirmEnabled }}
238-
disabled={!confirmEnabled}
239-
onPress={handleAddMembersConfirm}
240-
style={[
241-
styles.confirmButton,
242-
confirmEnabled
243-
? { backgroundColor: semantics.accentPrimary }
244-
: {
245-
borderColor: semantics.borderCoreDefault,
246-
borderWidth: 1,
247-
},
248-
confirmButtonOverride,
249-
]}
250-
testID='channel-details-add-members-confirm-button'
251-
>
252-
<Checkmark
253-
height={20}
254-
pathFill={confirmEnabled ? semantics.textOnInverse : semantics.textSecondary}
255-
width={20}
256-
/>
257-
</Pressable>
258-
</View>
259-
</View>
260-
<ChannelAddMembers onSelectionChange={handleAddMembersSelectionChange} />
230+
{addMembersNotificationHostId ? (
231+
<NotificationTargetProvider hostId={addMembersNotificationHostId} panel='channel-details'>
232+
<View style={styles.modalBody}>
233+
<View style={[styles.modalHeader, modalHeaderOverride]}>
234+
<View style={styles.modalHeaderSide}>
235+
<Button
236+
accessibilityLabelKey='a11y/Close'
237+
iconOnly
238+
LeadingIcon={NewClose}
239+
onPress={handleAddMembersClose}
240+
size='md'
241+
type='outline'
242+
variant='secondary'
243+
/>
244+
</View>
245+
<View style={styles.modalHeaderCenter}>
246+
<Text
247+
accessibilityRole='header'
248+
numberOfLines={1}
249+
style={[
250+
styles.modalHeaderTitle,
251+
{ color: semantics.textPrimary },
252+
modalHeaderTitleOverride,
253+
]}
254+
>
255+
{t('Add Members')}
256+
</Text>
257+
</View>
258+
<View style={[styles.modalHeaderSide, styles.modalHeaderSideRight]}>
259+
<Pressable
260+
accessibilityLabel={t('a11y/Confirm add members')}
261+
accessibilityRole='button'
262+
accessibilityState={{ disabled: !confirmEnabled }}
263+
disabled={!confirmEnabled}
264+
onPress={handleAddMembersConfirm}
265+
style={[
266+
styles.confirmButton,
267+
confirmEnabled
268+
? { backgroundColor: semantics.accentPrimary }
269+
: {
270+
borderColor: semantics.borderCoreDefault,
271+
borderWidth: 1,
272+
},
273+
confirmButtonOverride,
274+
]}
275+
testID='channel-details-add-members-confirm-button'
276+
>
277+
<Checkmark
278+
height={20}
279+
pathFill={confirmEnabled ? semantics.textOnInverse : semantics.textSecondary}
280+
width={20}
281+
/>
282+
</Pressable>
283+
</View>
284+
</View>
285+
<ChannelAddMembers onSelectionChange={handleAddMembersSelectionChange} />
286+
<NotificationList />
287+
</View>
288+
</NotificationTargetProvider>
289+
) : null}
261290
</BottomSheetModal>
262291
</View>
263292
);
@@ -296,6 +325,9 @@ const useStyles = () => {
296325
list: {
297326
paddingBottom: primitives.spacingSm,
298327
},
328+
modalBody: {
329+
flex: 1,
330+
},
299331
modalHeader: {
300332
alignItems: 'center',
301333
flexDirection: 'row',

package/src/components/ChannelDetailsScreen/hooks/useChannelDetailsActionItems.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@ export const useChannelDetailsActionItems = (): ChannelActionItem[] => {
1616
if (item.id === 'leave' || item.id === 'deleteChannel') {
1717
return {
1818
...item,
19-
action: async () => {
20-
await item.action();
21-
onChannelDismiss?.();
22-
},
19+
action: (options) => item.action({ ...options, onSuccess: onChannelDismiss }),
2320
};
2421
}
2522
return item;

package/src/components/Notifications/notificationTarget.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import type { Notification } from 'stream-chat';
22

3-
const NOTIFICATION_TARGET_PANELS = ['channel', 'thread', 'channel-list', 'thread-list'] as const;
3+
const NOTIFICATION_TARGET_PANELS = [
4+
'channel',
5+
'thread',
6+
'channel-list',
7+
'thread-list',
8+
'channel-details',
9+
] as const;
410
const NOTIFICATION_TARGET_TAG_PREFIX = 'target:' as const;
511

612
/** Built-in SDK surfaces that can host snackbar notifications. */

package/src/contexts/themeContext/utils/theme.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,6 +1233,7 @@ export const defaultTheme: Theme = {
12331233
selectionCircleSelected: {},
12341234
userName: {},
12351235
userRow: {},
1236+
clearSearch: {},
12361237
},
12371238
},
12381239
channelListSkeleton: {

0 commit comments

Comments
 (0)