Skip to content

Commit 7835c8a

Browse files
authored
feat: channel details and info (#3443)
This pull request refactors the overlay and channel preview UI in the sample app for improved maintainability and user experience. The main changes include replacing custom animated overlays with a reusable bottom sheet modal, simplifying the channel preview component, and updating icon usage and styling. Additionally, the channel list now supports custom action items, including a new "View Info" action. **Overlay and Modal Refactor:** * Replaced the custom animated overlay and gesture handling in `ChannelInfoOverlay.tsx` with the `BottomSheetModal` component, removing complex animation and gesture code for a simpler, more maintainable approach. [[1]](diffhunk://#diff-0bc001353733c815976c37051de07adc91ef1675f613531fa0e381496ece18cdL1-R26) [[2]](diffhunk://#diff-0bc001353733c815976c37051de07adc91ef1675f613531fa0e381496ece18cdL92-L215) [[3]](diffhunk://#diff-0bc001353733c815976c37051de07adc91ef1675f613531fa0e381496ece18cdR145-R150) [[4]](diffhunk://#diff-0bc001353733c815976c37051de07adc91ef1675f613531fa0e381496ece18cdL419-R292) **Channel Preview Simplification:** * Refactored `ChannelPreview.tsx` to remove swipeable actions and context dependencies, now directly using `ChannelPreviewMessenger` with a custom status component. This streamlines the UI and logic. [[1]](diffhunk://#diff-9e2e80dc4460facf62011e8ca9ca44884af293a18a042f50fbeaf9656319ef3eL3-L27) [[2]](diffhunk://#diff-9e2e80dc4460facf62011e8ca9ca44884af293a18a042f50fbeaf9656319ef3eL52-R40) [[3]](diffhunk://#diff-9e2e80dc4460facf62011e8ca9ca44884af293a18a042f50fbeaf9656319ef3eL78-R58) **Icon and Styling Updates:** * Added a new `ChannelInfo` icon and updated the `MenuPointHorizontal` icon to use a local SVG implementation, ensuring consistent iconography across the app. [[1]](diffhunk://#diff-8cfa99972971991abf2fece4e59e149b79531bb0d31f4ceb1c06330159d3579eR1-R26) [[2]](diffhunk://#diff-0f2d18cad1c6e4d05605f3292e8e3dd813cf2e5146f92000849faca738046e8aL3-R57) * Adjusted styling and props for several icons and components for improved consistency and appearance, such as removing unused color props and updating pin icon usage. [[1]](diffhunk://#diff-0bc001353733c815976c37051de07adc91ef1675f613531fa0e381496ece18cdL346-R223) [[2]](diffhunk://#diff-0bc001353733c815976c37051de07adc91ef1675f613531fa0e381496ece18cdL306-R187) [[3]](diffhunk://#diff-0bc001353733c815976c37051de07adc91ef1675f613531fa0e381496ece18cdL373-R250) [[4]](diffhunk://#diff-0bc001353733c815976c37051de07adc91ef1675f613531fa0e381496ece18cdL395-R270) **Channel List Enhancements:** * Introduced custom channel action items in `ChannelListScreen.tsx`, including a new "View Info" action that navigates to channel detail screens based on chat type. Also updated item layout sizing for improved appearance. [[1]](diffhunk://#diff-666b3f1604c90c9d52f5cb8fb6cf70c7005aa23730739ac3e29d43b203570549L12-R12) [[2]](diffhunk://#diff-666b3f1604c90c9d52f5cb8fb6cf70c7005aa23730739ac3e29d43b203570549L23-R24) [[3]](diffhunk://#diff-666b3f1604c90c9d52f5cb8fb6cf70c7005aa23730739ac3e29d43b203570549L122-R124) [[4]](diffhunk://#diff-666b3f1604c90c9d52f5cb8fb6cf70c7005aa23730739ac3e29d43b203570549R148-R176) These changes collectively modernize the UI, reduce complexity, and make the codebase easier to extend and maintain.
1 parent efc9512 commit 7835c8a

Some content is hidden

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

50 files changed

+2620
-429
lines changed

examples/SampleApp/src/components/ChannelInfoOverlay.tsx

Lines changed: 165 additions & 297 deletions
Large diffs are not rendered by default.
Lines changed: 4 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,15 @@
11
import React from 'react';
22
import { StyleSheet, View } from 'react-native';
3-
import { useNavigation } from '@react-navigation/native';
4-
import { RectButton } from 'react-native-gesture-handler';
5-
import Swipeable from 'react-native-gesture-handler/Swipeable';
63
import {
74
ChannelPreviewMessenger,
85
ChannelPreviewMessengerProps,
96
ChannelPreviewStatus,
107
ChannelPreviewStatusProps,
11-
Delete,
128
Pin,
139
useChannelMembershipState,
14-
useChatContext,
1510
useTheme,
1611
} from 'stream-chat-react-native';
1712

18-
import { useAppOverlayContext } from '../context/AppOverlayContext';
19-
import { useBottomSheetOverlayContext } from '../context/BottomSheetOverlayContext';
20-
import { useChannelInfoOverlayContext } from '../context/ChannelInfoOverlayContext';
21-
22-
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
23-
24-
import type { StackNavigatorParamList } from '../types';
25-
import { ChannelState } from 'stream-chat';
26-
import { MenuPointHorizontal } from '../icons/MenuPointHorizontal';
27-
2813
const styles = StyleSheet.create({
2914
leftSwipeableButton: {
3015
paddingLeft: 16,
@@ -49,15 +34,10 @@ const styles = StyleSheet.create({
4934
},
5035
});
5136

52-
type ChannelListScreenNavigationProp = NativeStackNavigationProp<
53-
StackNavigatorParamList,
54-
'ChannelListScreen'
55-
>;
37+
const CustomChannelPreviewStatus = (props: ChannelPreviewStatusProps) => {
38+
const { channel } = props;
5639

57-
const CustomChannelPreviewStatus = (
58-
props: ChannelPreviewStatusProps & { membership: ChannelState['membership'] },
59-
) => {
60-
const { membership } = props;
40+
const membership = useChannelMembershipState(channel);
6141
const {
6242
theme: { semantics },
6343
} = useTheme();
@@ -75,74 +55,5 @@ const CustomChannelPreviewStatus = (
7555
};
7656

7757
export const ChannelPreview: React.FC<ChannelPreviewMessengerProps> = (props) => {
78-
const { channel } = props;
79-
80-
const { setOverlay } = useAppOverlayContext();
81-
82-
const { setData: setDataBottomSheet } = useBottomSheetOverlayContext();
83-
84-
const { data, setData } = useChannelInfoOverlayContext();
85-
86-
const { client } = useChatContext();
87-
88-
const navigation = useNavigation<ChannelListScreenNavigationProp>();
89-
90-
const membership = useChannelMembershipState(channel);
91-
92-
const {
93-
theme: {
94-
colors: { accent_red, white_smoke },
95-
},
96-
} = useTheme();
97-
98-
const otherMembers = channel
99-
? Object.values(channel.state.members).filter((member) => member.user?.id !== data?.clientId)
100-
: [];
101-
102-
return (
103-
<Swipeable
104-
overshootLeft={false}
105-
overshootRight={false}
106-
renderRightActions={() => (
107-
<View style={[styles.swipeableContainer, { backgroundColor: white_smoke }]}>
108-
<RectButton
109-
onPress={() => {
110-
setData({ channel, clientId: client.userID, membership, navigation });
111-
setOverlay('channelInfo');
112-
}}
113-
style={[styles.leftSwipeableButton]}
114-
>
115-
<MenuPointHorizontal />
116-
</RectButton>
117-
<RectButton
118-
onPress={() => {
119-
setDataBottomSheet({
120-
confirmText: 'DELETE',
121-
onConfirm: () => {
122-
channel.delete();
123-
setOverlay('none');
124-
},
125-
subtext: `Are you sure you want to delete this ${
126-
otherMembers.length === 1 ? 'conversation' : 'group'
127-
}?`,
128-
title: `Delete ${otherMembers.length === 1 ? 'Conversation' : 'Group'}`,
129-
});
130-
setOverlay('confirmation');
131-
}}
132-
style={[styles.rightSwipeableButton]}
133-
>
134-
<Delete height={20} width={20} stroke={accent_red} />
135-
</RectButton>
136-
</View>
137-
)}
138-
>
139-
<ChannelPreviewMessenger
140-
{...props}
141-
// eslint-disable-next-line react/no-unstable-nested-components
142-
PreviewStatus={(statusProps) => (
143-
<CustomChannelPreviewStatus {...statusProps} membership={membership} />
144-
)}
145-
/>
146-
</Swipeable>
147-
);
58+
return <ChannelPreviewMessenger {...props} PreviewStatus={CustomChannelPreviewStatus} />;
14859
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from 'react';
2+
import Svg, { Path } from 'react-native-svg';
3+
import { useTheme, IconProps } from 'stream-chat-react-native';
4+
5+
export const ChannelInfo = (props: IconProps) => {
6+
const {
7+
theme: {
8+
semantics,
9+
},
10+
} = useTheme();
11+
12+
return (
13+
<Svg
14+
width={20}
15+
height={20}
16+
viewBox='0 0 20 20'
17+
fill='none'
18+
{...props}
19+
>
20+
<Path
21+
d='M8.95841 8.41663C8.5442 8.41663 8.20841 8.75241 8.20841 9.16663C8.20841 9.58084 8.5442 9.91663 8.95841 9.91663V9.16663V8.41663ZM10.0001 9.16663H10.7501C10.7501 8.75241 10.4143 8.41663 10.0001 8.41663V9.16663ZM9.25008 13.5416C9.25008 13.9558 9.58587 14.2916 10.0001 14.2916C10.4143 14.2916 10.7501 13.9558 10.7501 13.5416H10.0001H9.25008ZM8.95841 9.16663V9.91663H10.0001V9.16663V8.41663H8.95841V9.16663ZM10.0001 9.16663H9.25008V13.5416H10.0001H10.7501V9.16663H10.0001ZM17.7084 9.99996H16.9584C16.9584 13.8429 13.843 16.9583 10.0001 16.9583V17.7083V18.4583C14.6715 18.4583 18.4584 14.6713 18.4584 9.99996H17.7084ZM10.0001 17.7083V16.9583C6.1571 16.9583 3.04175 13.8429 3.04175 9.99996H2.29175H1.54175C1.54175 14.6713 5.32867 18.4583 10.0001 18.4583V17.7083ZM2.29175 9.99996H3.04175C3.04175 6.15698 6.1571 3.04163 10.0001 3.04163V2.29163V1.54163C5.32868 1.54163 1.54175 5.32855 1.54175 9.99996H2.29175ZM10.0001 2.29163V3.04163C13.843 3.04163 16.9584 6.15698 16.9584 9.99996H17.7084H18.4584C18.4584 5.32855 14.6715 1.54163 10.0001 1.54163V2.29163ZM9.89592 6.1458V5.3958C9.19402 5.3958 8.62508 5.96478 8.62508 6.66663H9.37508H10.1251C10.1251 6.79318 10.0225 6.8958 9.89592 6.8958V6.1458ZM9.37508 6.66663H8.62508C8.62508 7.36848 9.19402 7.93747 9.89592 7.93747V7.18747V6.43747C10.0225 6.43747 10.1251 6.54008 10.1251 6.66663H9.37508ZM9.89592 7.18747V7.93747C10.5978 7.93747 11.1667 7.36848 11.1667 6.66663H10.4167H9.66675C9.66675 6.54008 9.76935 6.43747 9.89592 6.43747V7.18747ZM10.4167 6.66663H11.1667C11.1667 5.96478 10.5978 5.3958 9.89592 5.3958V6.1458V6.8958C9.76935 6.8958 9.66675 6.79318 9.66675 6.66663H10.4167Z'
22+
fill={semantics.textTertiary}
23+
/>
24+
</Svg>
25+
);
26+
};
Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,58 @@
11
import React from 'react';
22

3-
import { IconProps, RootPath, RootSvg } from 'stream-chat-react-native';
3+
import Svg, { Path } from 'react-native-svg';
4+
5+
import { IconProps } from './utils/base';
46

57
export const MenuPointHorizontal = (props: IconProps) => (
6-
<RootSvg {...props}>
7-
<RootPath
8-
d='M8 12a2 2 0 11-4 0 2 2 0 014 0zM14 12a2 2 0 11-4 0 2 2 0 014 0zM18 14a2 2 0 100-4 2 2 0 000 4z'
8+
<Svg viewBox='0 0 17 4' fill='none' {...props} width={20} height={20}>
9+
<Path
10+
d='M8.45833 2.41667C8.91858 2.41667 9.29167 2.04358 9.29167 1.58333C9.29167 1.12308 8.91858 0.75 8.45833 0.75C7.99808 0.75 7.625 1.12308 7.625 1.58333C7.625 2.04358 7.99808 2.41667 8.45833 2.41667Z'
11+
fill='black'
12+
/>
13+
<Path
14+
d='M15.3333 2.41667C15.7936 2.41667 16.1667 2.04358 16.1667 1.58333C16.1667 1.12308 15.7936 0.75 15.3333 0.75C14.8731 0.75 14.5 1.12308 14.5 1.58333C14.5 2.04358 14.8731 2.41667 15.3333 2.41667Z'
15+
fill='black'
16+
/>
17+
<Path
18+
d='M1.58333 2.41667C2.04357 2.41667 2.41667 2.04358 2.41667 1.58333C2.41667 1.12308 2.04357 0.75 1.58333 0.75C1.1231 0.75 0.75 1.12308 0.75 1.58333C0.75 2.04358 1.1231 2.41667 1.58333 2.41667Z'
19+
fill='black'
20+
/>
21+
<Path
22+
d='M8.45833 2.41667C8.91858 2.41667 9.29167 2.04358 9.29167 1.58333C9.29167 1.12308 8.91858 0.75 8.45833 0.75C7.99808 0.75 7.625 1.12308 7.625 1.58333C7.625 2.04358 7.99808 2.41667 8.45833 2.41667Z'
23+
strokeWidth={1.5}
24+
strokeLinecap='round'
25+
{...props}
26+
/>
27+
<Path
28+
d='M15.3333 2.41667C15.7936 2.41667 16.1667 2.04358 16.1667 1.58333C16.1667 1.12308 15.7936 0.75 15.3333 0.75C14.8731 0.75 14.5 1.12308 14.5 1.58333C14.5 2.04358 14.8731 2.41667 15.3333 2.41667Z'
29+
strokeWidth={1.5}
30+
strokeLinecap='round'
31+
{...props}
32+
/>
33+
<Path
34+
d='M1.58333 2.41667C2.04357 2.41667 2.41667 2.04358 2.41667 1.58333C2.41667 1.12308 2.04357 0.75 1.58333 0.75C1.1231 0.75 0.75 1.12308 0.75 1.58333C0.75 2.04358 1.1231 2.41667 1.58333 2.41667Z'
35+
strokeWidth={1.5}
36+
strokeLinecap='round'
37+
{...props}
38+
/>
39+
<Path
40+
d='M8.45833 2.41667C8.91858 2.41667 9.29167 2.04358 9.29167 1.58333C9.29167 1.12308 8.91858 0.75 8.45833 0.75C7.99808 0.75 7.625 1.12308 7.625 1.58333C7.625 2.04358 7.99808 2.41667 8.45833 2.41667Z'
41+
strokeWidth={1.5}
42+
strokeLinecap='round'
43+
{...props}
44+
/>
45+
<Path
46+
d='M15.3333 2.41667C15.7936 2.41667 16.1667 2.04358 16.1667 1.58333C16.1667 1.12308 15.7936 0.75 15.3333 0.75C14.8731 0.75 14.5 1.12308 14.5 1.58333C14.5 2.04358 14.8731 2.41667 15.3333 2.41667Z'
47+
strokeWidth={1.5}
48+
strokeLinecap='round'
49+
{...props}
50+
/>
51+
<Path
52+
d='M1.58333 2.41667C2.04357 2.41667 2.41667 2.04358 2.41667 1.58333C2.41667 1.12308 2.04357 0.75 1.58333 0.75C1.1231 0.75 0.75 1.12308 0.75 1.58333C0.75 2.04358 1.1231 2.41667 1.58333 2.41667Z'
53+
strokeWidth={1.5}
54+
strokeLinecap='round'
955
{...props}
1056
/>
11-
</RootSvg>
57+
</Svg>
1258
);

examples/SampleApp/src/screens/ChannelListScreen.tsx

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
View,
1010
} from 'react-native';
1111
import { useNavigation, useScrollToTop } from '@react-navigation/native';
12-
import { ChannelList, useTheme } from 'stream-chat-react-native';
12+
import { ChannelList, useTheme, useStableCallback, ChannelActionItem } from 'stream-chat-react-native';
1313
import { Channel } from 'stream-chat';
1414
import { ChannelPreview } from '../components/ChannelPreview';
1515
import { ChatScreenHeader } from '../components/ChatScreenHeader';
@@ -20,7 +20,8 @@ import { usePaginatedSearchedMessages } from '../hooks/usePaginatedSearchedMessa
2020
import type { ChannelSort } from 'stream-chat';
2121
import { useStreamChatContext } from '../context/StreamChatContext';
2222
import { Search } from '../icons/Search';
23-
import { CircleClose } from '../icons/CircleClose';
23+
import { ChannelInfo } from '../icons/ChannelInfo.tsx';
24+
import { CircleClose } from '../icons/CircleClose.tsx';
2425

2526
const styles = StyleSheet.create({
2627
channelListContainer: {
@@ -119,8 +120,8 @@ export const ChannelListScreen: React.FC = () => {
119120
() => ({
120121
getItemLayout: (_: unknown, index: number) => ({
121122
index,
122-
length: 65,
123-
offset: 65 * index,
123+
length: 80,
124+
offset: 80 * index,
124125
}),
125126
keyboardDismissMode: 'on-drag',
126127
}),
@@ -144,6 +145,35 @@ export const ChannelListScreen: React.FC = () => {
144145
[],
145146
);
146147

148+
const getChannelActionItems = useStableCallback(({ context: { isDirectChat, channel }, defaultItems }) => {
149+
const viewInfo = () => {
150+
if (!channel) {
151+
return;
152+
}
153+
if (navigation) {
154+
if (isDirectChat) {
155+
navigation.navigate('OneOnOneChannelDetailScreen', {
156+
channel,
157+
});
158+
} else {
159+
navigation.navigate('GroupChannelDetailsScreen', {
160+
channel,
161+
});
162+
}
163+
}
164+
};
165+
166+
const viewInfoItem: ChannelActionItem = {
167+
action: viewInfo,
168+
Icon: ChannelInfo,
169+
id: 'info',
170+
label: 'View Info',
171+
placement: 'sheet',
172+
type: 'standard',
173+
}
174+
return [viewInfoItem, ...defaultItems]
175+
})
176+
147177
if (!chatClient) {
148178
return null;
149179
}
@@ -226,6 +256,7 @@ export const ChannelListScreen: React.FC = () => {
226256
options={options}
227257
Preview={ChannelPreview}
228258
setFlatListRef={setScrollRef}
259+
getChannelActionItems={getChannelActionItems}
229260
sort={sort}
230261
/>
231262
</View>

package/src/components/ChannelList/ChannelList.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
ChannelsProvider,
2727
} from '../../contexts/channelsContext/ChannelsContext';
2828
import { useChatContext } from '../../contexts/chatContext/ChatContext';
29+
import { SwipeRegistryProvider } from '../../contexts/swipeableContext/SwipeRegistryContext';
2930
import type { ChannelListEventListenerOptions } from '../../types/types';
3031
import { ChannelPreview } from '../ChannelPreview/ChannelPreview';
3132
import { EmptyStateIndicator as EmptyStateIndicatorDefault } from '../Indicators/EmptyStateIndicator';
@@ -51,6 +52,9 @@ export type ChannelListProps = Partial<
5152
| 'PreviewStatus'
5253
| 'PreviewTitle'
5354
| 'PreviewUnreadCount'
55+
| 'ChannelDetailsBottomSheet'
56+
| 'getChannelActionItems'
57+
| 'swipeActionsEnabled'
5458
| 'loadMoreThreshold'
5559
| 'Skeleton'
5660
| 'maxUnreadCount'
@@ -276,17 +280,20 @@ export const ChannelList = (props: ChannelListProps) => {
276280
onSelect,
277281
options = DEFAULT_OPTIONS,
278282
Preview = ChannelPreview,
283+
getChannelActionItems,
279284
PreviewAvatar,
280285
PreviewMessage,
281286
PreviewMutedStatus,
282287
PreviewStatus,
283288
PreviewTitle,
284289
PreviewUnreadCount,
290+
ChannelDetailsBottomSheet,
285291
setFlatListRef,
286292
Skeleton = SkeletonDefault,
287293
sort = DEFAULT_SORT,
288294
queryChannelsOverride,
289295
mutedStatusPosition = 'inlineTitle',
296+
swipeActionsEnabled = true,
290297
} = props;
291298

292299
const [forceUpdate, setForceUpdate] = useState(0);
@@ -403,12 +410,15 @@ export const ChannelList = (props: ChannelListProps) => {
403410
numberOfSkeletons,
404411
onSelect,
405412
Preview,
413+
getChannelActionItems,
406414
PreviewAvatar,
407415
PreviewMessage,
408416
PreviewMutedStatus,
409417
PreviewStatus,
410418
PreviewTitle,
411419
PreviewUnreadCount,
420+
ChannelDetailsBottomSheet,
421+
swipeActionsEnabled,
412422
refreshing,
413423
refreshList,
414424
reloadList,
@@ -423,7 +433,9 @@ export const ChannelList = (props: ChannelListProps) => {
423433

424434
return (
425435
<ChannelsProvider value={channelsContext}>
426-
<List />
436+
<SwipeRegistryProvider>
437+
<List />
438+
</SwipeRegistryProvider>
427439
</ChannelsProvider>
428440
);
429441
};

package/src/components/ChannelList/ChannelListMessenger.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { useChatContext } from '../../contexts/chatContext/ChatContext';
1313
import { useDebugContext } from '../../contexts/debugContext/DebugContext';
1414
import { useTheme } from '../../contexts/themeContext/ThemeContext';
1515

16+
import { useStableCallback } from '../../hooks';
1617
import { ChannelPreview } from '../ChannelPreview/ChannelPreview';
1718

1819
const styles = StyleSheet.create({
@@ -127,6 +128,13 @@ const ChannelListMessengerWithContext = (props: ChannelListMessengerPropsWithCon
127128
}
128129
}
129130

131+
const onEndReached = useStableCallback(() => {
132+
if (!onEndReachedCalledDuringCurrentScrollRef.current && hasNextPage) {
133+
loadNextPage();
134+
onEndReachedCalledDuringCurrentScrollRef.current = true;
135+
}
136+
});
137+
130138
if (error && !refreshing && !loadingChannels && (channels === null || !channelListInitialized)) {
131139
return (
132140
<LoadingErrorIndicator
@@ -138,13 +146,6 @@ const ChannelListMessengerWithContext = (props: ChannelListMessengerPropsWithCon
138146
);
139147
}
140148

141-
const onEndReached = () => {
142-
if (!onEndReachedCalledDuringCurrentScrollRef.current && hasNextPage) {
143-
loadNextPage();
144-
onEndReachedCalledDuringCurrentScrollRef.current = true;
145-
}
146-
};
147-
148149
return (
149150
<>
150151
<FlatList

0 commit comments

Comments
 (0)