Skip to content

Commit 631ac82

Browse files
authored
refactor: channel details cleanup (#3669)
## 🎯 Goal A cleanup for channel details, the main focus is to make it easier for integrators to reuse the specific building blocks, the implementation details describes all changes in detail. This PR also implements the new role label UI from Figma. Docs: GetStream/docs-content#1389 ## 🛠 Implementation details UI changes - New member-role UI (feat: implement new member role UI) — Replaced the old single text role label on member items with a dedicated, reusable RoleList / RoleItem component pair (components/roles/). Roles now render as styled chips/badges with distinct owner vs. regular-role colors. - New theme tokens added under messageMenu: roleList.container and roleItem (container, text, ownerBackgroundColor, ownerColor, roleBackgroundColor, roleColor). The old memberItem.role token was removed. - Logic moved from useMemberRoleLabel (single label string) → useMemberRoles (returns a list of roles). - Padding / spacing fixes — Several visual fixes: missing modal padding on Android in edge-to-edge mode, general padding fixes, and edit-button sizing in the Expo sample app. Component / architecture restructures - Add-members flow split into composable parts — ChannelAddMembersModal (deleted) → ChannelAddMembersButton, ChannelAddMembersForm, ChannelAddMembersFormContent, ChannelAddMembersFormHeader. The add-members button is now its own component, and a proper HOC was created for it. - Edit-details flow split the same way — ChannelEditDetailsModal (deleted) → ChannelEditDetailsForm, ChannelEditDetailsFormContent, ChannelEditDetailsFormHeader. Form submission was simplified. - ChannelDetails context split up — The monolithic ChannelDetails context was broken into focused contexts: channelDetailsContext, ChannelAddMembersContext, ChannelEditDetailsContext, plus per-list contexts (ChannelMemberListContext, ChannelMediaListContext, ChannelFileAttachmentListContext, ChannelPinnedMessageListContext). channel was moved into the context, the value wrapper was removed, and channel was dropped from contexts used under channel details. ChannelDetails.tsx itself shrank from ~171 lines of orchestration to a thin shell. - Hooks promoted to the SDK's shared src/hooks/ — useCanEdit/useCanAddMembers were moved, renamed, and split into useCanEditChannel and useCanAddMembersToChannel (with tests); useIsDirectChat updated. - New signal store (state-store/signal-store.ts) — A small signal store to imperatively close any open channel-details modal; related store hooks exported. The edit-channel-details-store was also reworked. - Public API changes — useSelectedChannelState is now exported from the SDK; ChannelAllMembersModal is no longer exposed (component deleted); custom searchSource can be passed to channel-details components. - Sample apps updated — Both SampleApp and ExpoMessaging (details/index.tsx, screens) were migrated to the new component/context structure; Expo sample app fixes. - Removed the experimental warnings ## 🎨 UI Changes No UI changes ## 🧪 Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## ☑️ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android
1 parent 949415d commit 631ac82

115 files changed

Lines changed: 2071 additions & 1580 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/ExpoMessaging/app/channel/[cid]/details/files.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default function ChannelFilesScreen() {
1919
return (
2020
<>
2121
<Stack.Screen options={{ contentStyle: { backgroundColor: semantics.backgroundCoreApp } }} />
22-
<ChannelDetailsContextProvider value={{ channel }}>
22+
<ChannelDetailsContextProvider channel={channel}>
2323
<FileAttachmentList />
2424
</ChannelDetailsContextProvider>
2525
</>

examples/ExpoMessaging/app/channel/[cid]/details/images.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default function ChannelImagesScreen() {
1919
return (
2020
<>
2121
<Stack.Screen options={{ contentStyle: { backgroundColor: semantics.backgroundCoreApp } }} />
22-
<ChannelDetailsContextProvider value={{ channel }}>
22+
<ChannelDetailsContextProvider channel={channel}>
2323
<MediaList />
2424
</ChannelDetailsContextProvider>
2525
</>

examples/ExpoMessaging/app/channel/[cid]/details/index.tsx

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
import React, { useCallback, useContext, useState } from 'react';
1+
import React, { useCallback, useContext } from 'react';
22

33
import { Stack, useRouter } from 'expo-router';
44

55
import {
6-
ChannelAddMembersModal,
7-
ChannelAllMembersModal,
86
ChannelDetails,
7+
ChannelDetailsActionsSection,
98
ChannelDetailsContextProvider,
9+
ChannelDetailsNavigationSection,
1010
ChannelDetailsNavigationSectionType,
1111
GetChannelDetailsNavigationItems,
12-
GetChannelMemberActionItems,
12+
ChannelDetailsEditButton,
13+
useCanEditChannel,
14+
useIsDirectChat,
1315
WithComponents,
1416
} from 'stream-chat-expo';
1517

@@ -23,13 +25,16 @@ const navigationItems: {
2325
files: 'files',
2426
};
2527

26-
const Header = () => {
28+
const EmptyHeader = () => {
2729
return null;
2830
};
2931

3032
export default function ChannelDetailsScreen() {
3133
const router = useRouter();
3234
const { channel } = useContext(AppContext);
35+
const canEdit = useCanEditChannel(channel);
36+
const isDirect = useIsDirectChat(channel);
37+
const isEditButtonVisible = canEdit && !isDirect;
3338

3439
const getNavigationItems = useCallback<GetChannelDetailsNavigationItems>(
3540
({ defaultItems }) =>
@@ -48,20 +53,24 @@ export default function ChannelDetailsScreen() {
4853

4954
const popToRoot = useCallback(() => router.replace('/'), [router]);
5055

51-
const [isAddMembersVisible, setAddMembersVisible] = useState(false);
52-
const handleAddMembersClose = useCallback(() => setAddMembersVisible(false), []);
53-
const handleAddMembersPress = useCallback(() => {
54-
setAllMembersVisible(false);
55-
setAddMembersVisible(true);
56-
}, []);
56+
const NavigationSection = useCallback(
57+
() => <ChannelDetailsNavigationSection getNavigationItems={getNavigationItems} />,
58+
[getNavigationItems],
59+
);
5760

58-
const [isAllMembersVisible, setAllMembersVisible] = useState(false);
59-
const handleAllMembersClose = useCallback(() => setAllMembersVisible(false), []);
60-
const handleAllMembersPress = useCallback(() => setAllMembersVisible(true), []);
61+
const renderHeaderRight = useCallback(
62+
() =>
63+
channel ? (
64+
<ChannelDetailsContextProvider channel={channel}>
65+
<ChannelDetailsEditButton style={{ flexShrink: 0, width: 'auto' }} />
66+
</ChannelDetailsContextProvider>
67+
) : null,
68+
[channel],
69+
);
6170

62-
const getChannelMemberActionItems = useCallback<GetChannelMemberActionItems>(
63-
({ defaultItems }) => defaultItems,
64-
[],
71+
const ActionsSection = useCallback(
72+
() => <ChannelDetailsActionsSection onChannelDismiss={popToRoot} />,
73+
[popToRoot],
6574
);
6675

6776
if (!channel) {
@@ -73,24 +82,19 @@ export default function ChannelDetailsScreen() {
7382
<Stack.Screen
7483
options={{
7584
title: 'Channel details',
85+
headerRight: isEditButtonVisible ? renderHeaderRight : undefined,
7686
}}
7787
/>
78-
<WithComponents overrides={{ ChannelDetailsNavHeader: Header }}>
79-
<ChannelDetails
80-
channel={channel}
81-
getChannelMemberActionItems={getChannelMemberActionItems}
82-
getNavigationItems={getNavigationItems}
83-
onChannelDismiss={popToRoot}
84-
onViewAllMembersPress={handleAllMembersPress}
85-
/>
86-
</WithComponents>
87-
<ChannelDetailsContextProvider value={{ channel, getChannelMemberActionItems }}>
88-
<ChannelAllMembersModal
89-
onClose={handleAllMembersClose}
90-
visible={isAllMembersVisible}
91-
onAddMembersPress={handleAddMembersPress}
92-
/>
93-
<ChannelAddMembersModal onClose={handleAddMembersClose} visible={isAddMembersVisible} />
88+
<ChannelDetailsContextProvider channel={channel}>
89+
<WithComponents
90+
overrides={{
91+
ChannelDetailsActionsSection: ActionsSection,
92+
ChannelDetailsNavHeader: EmptyHeader,
93+
ChannelDetailsNavigationSection: NavigationSection,
94+
}}
95+
>
96+
<ChannelDetails />
97+
</WithComponents>
9498
</ChannelDetailsContextProvider>
9599
</>
96100
);

examples/ExpoMessaging/app/channel/[cid]/details/pinned.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export default function ChannelPinnedMessagesScreen() {
4848
return (
4949
<>
5050
<Stack.Screen options={{ contentStyle: { backgroundColor: semantics.backgroundCoreApp } }} />
51-
<ChannelDetailsContextProvider value={{ channel }}>
51+
<ChannelDetailsContextProvider channel={channel}>
5252
<WithComponents overrides={{ PinnedMessageItem: PinnedMessage }}>
5353
<PinnedMessageList />
5454
</WithComponents>

examples/SampleApp/src/screens/ChannelDetailsScreen.tsx

Lines changed: 58 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1-
import React, { useCallback, useState } from 'react';
1+
import React, { useCallback } from 'react';
22

3-
import type { RouteProp } from '@react-navigation/native';
3+
import { useNavigation, type RouteProp } from '@react-navigation/native';
44
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
55

66
import {
77
ChannelDetails,
8+
ChannelDetailsActionsSection,
9+
ChannelDetailsNavigationSection,
810
GetChannelDetailsNavigationItems,
911
GetChannelMemberActionItems,
10-
ChannelAddMembersModal,
11-
ChannelAllMembersModal,
1212
ChannelDetailsContextProvider,
1313
ChannelDetailsNavigationSectionType,
14+
ChannelMemberActionsSheet,
15+
WithComponents,
16+
ChannelDetailsActionsSectionProps,
17+
useChannelDetailsContext,
1418
} from 'stream-chat-react-native';
1519

1620
import { SendDirectMessage } from '../icons/SendDirectMessage';
@@ -40,12 +44,10 @@ const navigationItems: {
4044
files: 'ChannelFilesScreen',
4145
};
4246

43-
export const ChannelDetailsScreen: React.FC<Props> = ({
44-
navigation,
45-
route: {
46-
params: { channel },
47-
},
48-
}) => {
47+
const ChannelDetailsScreenInner = () => {
48+
const navigation = useNavigation<ChannelDetailsScreenNavigationProp>();
49+
const { channel, closeModals } = useChannelDetailsContext();
50+
4951
const onBack = useCallback(() => navigation.goBack(), [navigation]);
5052
const getNavigationItems = useCallback<GetChannelDetailsNavigationItems>(
5153
({ defaultItems }) =>
@@ -65,15 +67,13 @@ export const ChannelDetailsScreen: React.FC<Props> = ({
6567
}),
6668
[navigation],
6769
);
68-
const [isAddMembersVisible, setAddMembersVisible] = useState(false);
69-
const handleAddMembersClose = useCallback(() => setAddMembersVisible(false), []);
70-
const handleAddMembersPress = useCallback(() => {
71-
setAllMembersVisible(false);
72-
setAddMembersVisible(true);
73-
}, []);
74-
const [isAllMembersVisible, setAllMembersVisible] = useState(false);
75-
const handleAllMembersClose = useCallback(() => setAllMembersVisible(false), []);
76-
const handleAllMembersPress = useCallback(() => setAllMembersVisible(true), []);
70+
71+
const ActionsSection = useCallback(
72+
(props: ChannelDetailsActionsSectionProps) => (
73+
<ChannelDetailsActionsSection {...props} onChannelDismiss={popToRoot} />
74+
),
75+
[popToRoot],
76+
);
7777

7878
const getChannelMemberActionItems = useCallback<GetChannelMemberActionItems>(
7979
({ context, defaultItems }) => {
@@ -85,7 +85,7 @@ export const ChannelDetailsScreen: React.FC<Props> = ({
8585
return [
8686
{
8787
action: () => {
88-
setAllMembersVisible(false);
88+
closeModals();
8989
navigation.navigate('NewDirectMessagingScreen', { initialUser: user });
9090
return Promise.resolve();
9191
},
@@ -97,28 +97,47 @@ export const ChannelDetailsScreen: React.FC<Props> = ({
9797
...defaultItems,
9898
];
9999
},
100-
[navigation],
100+
[navigation, closeModals],
101101
);
102102

103-
return (
104-
<>
105-
<ChannelDetails
106-
channel={channel}
103+
const NavigationSection = useCallback(
104+
(props: Parameters<typeof ChannelDetailsNavigationSection>[0]) => (
105+
<ChannelDetailsNavigationSection {...props} getNavigationItems={getNavigationItems} />
106+
),
107+
[getNavigationItems],
108+
);
109+
110+
const MemberActionsSheet = useCallback(
111+
(props: Parameters<typeof ChannelMemberActionsSheet>[0]) => (
112+
<ChannelMemberActionsSheet
113+
{...props}
107114
getChannelMemberActionItems={getChannelMemberActionItems}
108-
getNavigationItems={getNavigationItems}
109-
onBack={onBack}
110-
onChannelDismiss={popToRoot}
111-
// Handler view all members modal so we can close it after navigation is triggered by our custom action
112-
onViewAllMembersPress={handleAllMembersPress}
113115
/>
114-
<ChannelDetailsContextProvider value={{ channel, getChannelMemberActionItems }}>
115-
<ChannelAllMembersModal
116-
onClose={handleAllMembersClose}
117-
visible={isAllMembersVisible}
118-
onAddMembersPress={handleAddMembersPress}
119-
/>
120-
<ChannelAddMembersModal onClose={handleAddMembersClose} visible={isAddMembersVisible} />
121-
</ChannelDetailsContextProvider>
122-
</>
116+
),
117+
[getChannelMemberActionItems],
118+
);
119+
120+
return (
121+
<WithComponents
122+
overrides={{
123+
ChannelDetailsActionsSection: ActionsSection,
124+
ChannelDetailsNavigationSection: NavigationSection,
125+
ChannelMemberActionsSheet: MemberActionsSheet,
126+
}}
127+
>
128+
<ChannelDetails onBack={onBack} />
129+
</WithComponents>
130+
);
131+
};
132+
133+
export const ChannelDetailsScreen: React.FC<Props> = ({
134+
route: {
135+
params: { channel },
136+
},
137+
}) => {
138+
return (
139+
<ChannelDetailsContextProvider channel={channel}>
140+
<ChannelDetailsScreenInner />
141+
</ChannelDetailsContextProvider>
123142
);
124143
};

examples/SampleApp/src/screens/ChannelFilesScreen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const ChannelFilesScreen: React.FC<ChannelFilesScreenProps> = ({
3434
return (
3535
<View style={[styles.flex]}>
3636
<ScreenHeader titleText='Files' />
37-
<ChannelDetailsContextProvider value={{ channel }}>
37+
<ChannelDetailsContextProvider channel={channel}>
3838
<FileAttachmentList />
3939
</ChannelDetailsContextProvider>
4040
</View>

examples/SampleApp/src/screens/ChannelImagesScreen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const ChannelImagesScreen: React.FC<ChannelImagesScreenProps> = ({
2626
return (
2727
<View style={[styles.flex]}>
2828
<ScreenHeader titleText='Photos and Videos' />
29-
<ChannelDetailsContextProvider value={{ channel }}>
29+
<ChannelDetailsContextProvider channel={channel}>
3030
<MediaList />
3131
</ChannelDetailsContextProvider>
3232
</View>

examples/SampleApp/src/screens/ChannelPinnedMessagesScreen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export const ChannelPinnedMessagesScreen: React.FC<ChannelPinnedMessagesScreenPr
106106
return (
107107
<View style={[styles.flex]}>
108108
<ScreenHeader titleText='Pinned Messages' />
109-
<ChannelDetailsContextProvider value={{ channel }}>
109+
<ChannelDetailsContextProvider channel={channel}>
110110
<WithComponents overrides={{ PinnedMessageItem: PinnedMessage }}>
111111
<PinnedMessageList />
112112
</WithComponents>

0 commit comments

Comments
 (0)