Skip to content

Commit 7a7f927

Browse files
authored
feat: bottom sheet dynamic resizing (#3544)
## 🎯 Goal This PR refines dynamic bottom sheet sizing and cleans up a few related UI issues in the SDK and `SampleApp`. - Adds content driven initial sizing to `BottomSheetModal` while keeping the top snap available (this is toggleable by a flag and will assume `height` as the maximum height the sheet can grow to through dynamic sizing) - Moves dynamic sizing calculations to shared values - Adds test coverage for the new bottom sheet snap point utilities - Fixes channel preview spacing for very long channel names - Hides quoted replies for attachment action previews such as ephemeral `Giphys` - Preserves the correct standalone message shell styling for ephemeral `Giphy` previews - Moves `hasAttachmentActions` into `MessageContext` so it is computed once and reused downstream On the `SampleApp` side, it fixes a few visual inconsistencies: - network/loading and DM button backgrounds now match the header - grouped `ChannelAvatar` now respects the requested size - group channel details now actually request `2xl` - `GoBack` and `GoForward` chevrons now mirror internally in `RTL` as an SVG PoC ## 🛠 Implementation details <!-- Provide a description of the implementation --> ## 🎨 UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## 🧪 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 66ee8e4 commit 7a7f927

35 files changed

+710
-248
lines changed

examples/ExpoMessaging/yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6043,10 +6043,10 @@ stream-chat-react-native-core@8.1.0:
60436043
version "0.0.0"
60446044
uid ""
60456045

6046-
stream-chat@^9.36.1:
6047-
version "9.36.2"
6048-
resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.36.2.tgz#cd2cfac1f8d7b045c34dce51e2de1cb66bf288f5"
6049-
integrity sha512-sSCxTXJOf0BLDMZ2/cqvFged/LLbiWoIhs7v3UsRj0EM0T8tTam7zpU77TSccNDlK5j1C1/llSUVyMLc7aCDsA==
6046+
stream-chat@^9.40.0:
6047+
version "9.41.0"
6048+
resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.41.0.tgz#ad88d7919aaf1d3c35b4a431a8cd464cb640f146"
6049+
integrity sha512-Rgp3vULGKYxHZ/aCeundly6ngdBGttTPz+YknmWhbqvNlEhPB/RM61CpQPHgPyfkSm+osJT3tEV9fKd+I/S77g==
60506050
dependencies:
60516051
"@types/jsonwebtoken" "^9.0.8"
60526052
"@types/ws" "^8.5.14"

examples/SampleApp/src/components/NetworkDownIndicator.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,19 @@ const styles = StyleSheet.create({
1616
fontSize: 16,
1717
fontWeight: '700',
1818
},
19-
spinner: {
20-
backgroundColor: 'white',
21-
},
2219
});
2320

2421
export const NetworkDownIndicator: React.FC<{ titleSize: 'small' | 'large' }> = ({
2522
titleSize = 'small',
2623
}) => {
27-
useTheme();
24+
const {
25+
theme: { semantics },
26+
} = useTheme();
2827
const { black } = useLegacyColors();
2928

3029
return (
3130
<View style={styles.networkDownContainer} testID='network-down-indicator'>
32-
<Spinner height={12} style={styles.spinner} width={12} />
31+
<Spinner height={12} style={{ backgroundColor: semantics.backgroundCoreElevation1 }} width={12} />
3332
<Text
3433
style={[
3534
styles.networkDownText,
Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
import React from 'react';
2-
import Svg, { Path } from 'react-native-svg';
2+
import { I18nManager } from 'react-native';
3+
import Svg, { G, Path } from 'react-native-svg';
34

45
import { IconProps } from '../utils/base';
56
import { useLegacyColors } from '../theme/useLegacyColors';
67

78
export const GoBack: React.FC<IconProps> = ({ height = 24, width = 24 }) => {
89
const { black } = useLegacyColors();
10+
911
return (
10-
<Svg fill='none' height={height} viewBox={`0 0 ${height} ${width}`} width={width}>
11-
<Path
12-
clipRule='evenodd'
13-
d='M15.694 18.6943C16.102 18.2867 16.102 17.6259 15.694 17.2184L10.4699 12L15.694 6.78165C16.102 6.37408 16.102 5.71326 15.694 5.30568C15.2859 4.89811 14.6244 4.8981 14.2164 5.30568L8.30602 11.2096C8.08861 11.4267 7.98704 11.7158 8.00132 12.0002C7.98713 12.2844 8.0887 12.5733 8.30603 12.7904L14.2164 18.6943C14.6244 19.1019 15.2859 19.1019 15.694 18.6943Z'
14-
fill={black}
15-
fillRule='evenodd'
16-
/>
12+
<Svg fill='none' height={height} viewBox='0 0 24 24' width={width}>
13+
<G transform={I18nManager.isRTL ? 'matrix(-1 0 0 1 24 0)' : undefined}>
14+
<Path
15+
clipRule='evenodd'
16+
d='M15.694 18.6943C16.102 18.2867 16.102 17.6259 15.694 17.2184L10.4699 12L15.694 6.78165C16.102 6.37408 16.102 5.71326 15.694 5.30568C15.2859 4.89811 14.6244 4.8981 14.2164 5.30568L8.30602 11.2096C8.08861 11.4267 7.98704 11.7158 8.00132 12.0002C7.98713 12.2844 8.0887 12.5733 8.30603 12.7904L14.2164 18.6943C14.6244 19.1019 15.2859 19.1019 15.694 18.6943Z'
17+
fill={black}
18+
fillRule='evenodd'
19+
/>
20+
</G>
1721
</Svg>
1822
);
1923
};

examples/SampleApp/src/icons/GoForward.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import React from 'react';
2+
import { I18nManager } from 'react-native';
23
import Svg, { G, Path } from 'react-native-svg';
34

45
import { IconProps } from '../utils/base';
56

67
export const GoForward: React.FC<IconProps> = ({ height = 20, width = 20, ...rest }) => {
78
return (
8-
<Svg fill='none' height={height} viewBox={`0 0 ${height} ${width}`} width={width}>
9-
<G>
9+
<Svg fill='none' height={height} viewBox='0 0 20 20' width={width}>
10+
<G transform={I18nManager.isRTL ? 'matrix(-1 0 0 1 20 0)' : undefined}>
1011
<Path
1112
d='M7.91675 15.2096L13.1251 10.0013L7.91675 4.79297'
1213
strokeWidth={1.5}

examples/SampleApp/src/screens/ChannelScreen.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { useCreateDraftFocusEffect } from '../utils/useCreateDraftFocusEffect.ts
3131
import { channelMessageActions } from '../utils/messageActions.tsx';
3232
import { MessageLocation } from '../components/LocationSharing/MessageLocation.tsx';
3333
import { useStreamChatContext } from '../context/StreamChatContext.tsx';
34-
import { CustomAttachmentPickerSelectionBar } from '../components/AttachmentPickerSelectionBar.tsx';
34+
// import { CustomAttachmentPickerSelectionBar } from '../components/AttachmentPickerSelectionBar.tsx';
3535
import { MessageInfoBottomSheet } from '../components/MessageInfoBottomSheet.tsx';
3636
import { CustomAttachmentPickerContent } from '../components/AttachmentPickerContent.tsx';
3737
import { ThreadType } from 'stream-chat-react-native-core';
@@ -103,7 +103,7 @@ const ChannelHeader: React.FC<ChannelHeaderProps> = ({ channel }) => {
103103
opacity: pressed ? 0.5 : 1,
104104
})}
105105
>
106-
<ChannelAvatar channel={channel} size='lg' />
106+
<ChannelAvatar channel={channel} size='xl' />
107107
</Pressable>
108108
)}
109109
showUnreadCountBadge
@@ -267,7 +267,7 @@ export const ChannelScreen: React.FC<ChannelScreenProps> = ({ navigation, route
267267
<View style={[styles.flex, { backgroundColor: 'transparent' }]}>
268268
<Channel
269269
audioRecordingEnabled={true}
270-
AttachmentPickerSelectionBar={CustomAttachmentPickerSelectionBar}
270+
// AttachmentPickerSelectionBar={CustomAttachmentPickerSelectionBar}
271271
AttachmentPickerContent={CustomAttachmentPickerContent}
272272
channel={channel}
273273
messageInputFloating={messageInputFloating}

examples/SampleApp/src/screens/NewGroupChannelAddMemberScreen.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback } from 'react';
1+
import React, { useCallback, useRef } from 'react';
22
import { FlatList, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
33
import { Search, useTheme } from 'stream-chat-react-native';
44

@@ -41,6 +41,12 @@ const styles = StyleSheet.create({
4141
navigationButton: {
4242
paddingRight: 8,
4343
},
44+
searchButton: {
45+
alignItems: 'center',
46+
justifyContent: 'center',
47+
paddingLeft: 8,
48+
paddingRight: 4,
49+
},
4450
userGridItemContainer: { marginHorizontal: 8, width: 64 },
4551
});
4652

@@ -70,6 +76,7 @@ type Props = {
7076

7177
export const NewGroupChannelAddMemberScreen: React.FC<Props> = ({ navigation }) => {
7278
const { chatClient } = useAppContext();
79+
const searchInputRef = useRef<TextInput>(null);
7380

7481
const {
7582
theme: { semantics },
@@ -97,6 +104,10 @@ export const NewGroupChannelAddMemberScreen: React.FC<Props> = ({ navigation })
97104
navigation.navigate('NewGroupChannelAssignNameScreen');
98105
};
99106

107+
const focusSearchInput = useCallback(() => {
108+
searchInputRef.current?.focus();
109+
}, []);
110+
100111
if (!chatClient) {
101112
return null;
102113
}
@@ -122,12 +133,12 @@ export const NewGroupChannelAddMemberScreen: React.FC<Props> = ({ navigation })
122133
},
123134
]}
124135
>
125-
<Search pathFill={black} />
126136
<TextInput
127137
onChangeText={onChangeSearchText}
128138
onFocus={onFocusInput}
129139
placeholder='Search'
130140
placeholderTextColor={grey}
141+
ref={searchInputRef}
131142
style={[
132143
styles.inputBox,
133144
{
@@ -136,6 +147,9 @@ export const NewGroupChannelAddMemberScreen: React.FC<Props> = ({ navigation })
136147
]}
137148
value={searchText}
138149
/>
150+
<TouchableOpacity hitSlop={8} onPress={focusSearchInput} style={styles.searchButton}>
151+
<Search height={20} pathFill={black} width={20} />
152+
</TouchableOpacity>
139153
</View>
140154
<FlatList
141155
data={selectedUsers}

examples/SampleApp/src/screens/ThreadScreen.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { useCreateDraftFocusEffect } from '../utils/useCreateDraftFocusEffect.ts
2525
import { channelMessageActions } from '../utils/messageActions.tsx';
2626
import { useStreamChatContext } from '../context/StreamChatContext.tsx';
2727
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
28-
import { CustomAttachmentPickerSelectionBar } from '../components/AttachmentPickerSelectionBar.tsx';
28+
// import { CustomAttachmentPickerSelectionBar } from '../components/AttachmentPickerSelectionBar.tsx';
2929
import { MessageLocation } from '../components/LocationSharing/MessageLocation.tsx';
3030
import { useAppContext } from '../context/AppContext.ts';
3131
import { useLegacyColors } from '../theme/useLegacyColors';
@@ -150,7 +150,7 @@ export const ThreadScreen: React.FC<ThreadScreenProps> = ({
150150
<View style={[styles.container, { backgroundColor: white }]}>
151151
<Channel
152152
audioRecordingEnabled={true}
153-
AttachmentPickerSelectionBar={CustomAttachmentPickerSelectionBar}
153+
// AttachmentPickerSelectionBar={CustomAttachmentPickerSelectionBar}
154154
channel={channel}
155155
enforceUniqueReaction
156156
keyboardVerticalOffset={0}

examples/SampleApp/src/theme/useLegacyColors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const useLegacyColors = () => {
1818
grey: semantics.textSecondary,
1919
grey_gainsboro: semantics.borderCoreDefault,
2020
grey_whisper: semantics.backgroundCoreSurfaceDefault,
21-
icon_background: semantics.backgroundCoreApp,
21+
icon_background: semantics.backgroundCoreElevation1,
2222
overlay: semantics.badgeBgOverlay,
2323
white: semantics.backgroundCoreApp,
2424
white_smoke: semantics.backgroundCoreSurfaceSubtle,

examples/SampleApp/src/utils/messageActions.tsx

Lines changed: 57 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
import React from 'react';
2-
import { Alert } from 'react-native';
1+
// import React from 'react';
2+
// import { Alert } from 'react-native';
33
import { LocalMessage, StreamChat } from 'stream-chat';
44
import {
55
Colors,
66
messageActions,
77
MessageActionsParams,
88
TranslationContextValue,
9-
Bell,
9+
// Bell,
1010
} from 'stream-chat-react-native';
1111
import { Theme } from 'stream-chat-react-native';
1212

1313
export function channelMessageActions({
1414
params,
15-
chatClient,
16-
t,
17-
// handleMessageInfo,
18-
semantics,
15+
// chatClient,
16+
// t,
17+
// // handleMessageInfo,
18+
// semantics,
1919
}: {
2020
params: MessageActionsParams;
2121
chatClient: StreamChat;
@@ -24,11 +24,11 @@ export function channelMessageActions({
2424
handleMessageInfo: (message: LocalMessage) => void;
2525
semantics: Theme['semantics'];
2626
}) {
27-
const { dismissOverlay, error /*deleteForMeMessage*/ } = params;
27+
// const { dismissOverlay, error /*deleteForMeMessage*/ } = params;
2828
const actions = messageActions(params);
2929

3030
// We cannot use the useMessageReminder hook here because it is a hook.
31-
const reminder = chatClient.reminders.getFromState(params.message.id);
31+
// const reminder = chatClient.reminders.getFromState(params.message.id);
3232

3333
// actions.push({
3434
// action: async () => {
@@ -48,54 +48,54 @@ export function channelMessageActions({
4848
// icon: <Time width={20} height={20} stroke={semantics.textSecondary} />,
4949
// type: 'standard',
5050
// });
51-
if (!error) {
52-
actions.push({
53-
action: () => {
54-
if (reminder) {
55-
Alert.alert('Remove Reminder', 'Are you sure you want to remove this reminder?', [
56-
{
57-
text: 'Cancel',
58-
style: 'cancel',
59-
},
60-
{
61-
text: 'Remove',
62-
onPress: () => {
63-
chatClient.reminders.deleteReminder(reminder.id).catch((err) => {
64-
console.error('Error deleting reminder:', err);
65-
});
66-
},
67-
style: 'destructive',
68-
},
69-
]);
70-
} else {
71-
Alert.alert(
72-
'Select Reminder Time',
73-
'When would you like to be reminded?',
74-
chatClient.reminders.scheduledOffsetsMs.map((offsetMs) => ({
75-
text: t('duration/Remind Me', { milliseconds: offsetMs }),
76-
onPress: () => {
77-
chatClient.reminders
78-
.upsertReminder({
79-
messageId: params.message.id,
80-
remind_at: new Date(new Date().getTime() + offsetMs).toISOString(),
81-
})
82-
.catch((_error) => {
83-
console.error('Error creating reminder:', _error);
84-
});
85-
},
86-
style: 'default',
87-
})),
88-
);
89-
}
90-
91-
dismissOverlay();
92-
},
93-
actionType: reminder ? 'remove-reminder' : 'remind-me',
94-
title: reminder ? 'Remove Reminder' : 'Remind Me',
95-
icon: <Bell height={20} width={20} stroke={semantics.textSecondary} />,
96-
type: 'standard',
97-
});
98-
}
51+
// if (!error) {
52+
// actions.push({
53+
// action: () => {
54+
// if (reminder) {
55+
// Alert.alert('Remove Reminder', 'Are you sure you want to remove this reminder?', [
56+
// {
57+
// text: 'Cancel',
58+
// style: 'cancel',
59+
// },
60+
// {
61+
// text: 'Remove',
62+
// onPress: () => {
63+
// chatClient.reminders.deleteReminder(reminder.id).catch((err) => {
64+
// console.error('Error deleting reminder:', err);
65+
// });
66+
// },
67+
// style: 'destructive',
68+
// },
69+
// ]);
70+
// } else {
71+
// Alert.alert(
72+
// 'Select Reminder Time',
73+
// 'When would you like to be reminded?',
74+
// chatClient.reminders.scheduledOffsetsMs.map((offsetMs) => ({
75+
// text: t('duration/Remind Me', { milliseconds: offsetMs }),
76+
// onPress: () => {
77+
// chatClient.reminders
78+
// .upsertReminder({
79+
// messageId: params.message.id,
80+
// remind_at: new Date(new Date().getTime() + offsetMs).toISOString(),
81+
// })
82+
// .catch((_error) => {
83+
// console.error('Error creating reminder:', _error);
84+
// });
85+
// },
86+
// style: 'default',
87+
// })),
88+
// );
89+
// }
90+
//
91+
// dismissOverlay();
92+
// },
93+
// actionType: reminder ? 'remove-reminder' : 'remind-me',
94+
// title: reminder ? 'Remove Reminder' : 'Remind Me',
95+
// icon: <Bell height={20} width={20} stroke={semantics.textSecondary} />,
96+
// type: 'standard',
97+
// });
98+
// }
9999
// actions.push({
100100
// action: async () => {
101101
// Alert.alert('Delete for me', 'Are you sure you want to delete this message for me?', [

package/src/components/Channel/Channel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
575575
asyncMessagesLockDistance = 50,
576576
asyncMessagesMinimumPressDuration = 500,
577577
asyncMessagesSlideToCancelDistance = 75,
578-
audioRecordingSendOnComplete = true,
578+
audioRecordingSendOnComplete = false,
579579
AttachButton = AttachButtonDefault,
580580
Attachment = AttachmentDefault,
581581
attachmentPickerBottomSheetHeight = disableAttachmentPicker ? 72 : 333,

0 commit comments

Comments
 (0)