Skip to content

Commit 7b5f9d6

Browse files
authored
feat: redesign of the Message Input component (#3342)
# πŸ“ Changelog β€” Message Composer & Input Refactor ## ✨ Design & UX Improvements - Introduced a **new unified icon set** across the Message Composer. - Added **smoother and more contextual animations** throughout the component. - Message Composer now internally handles **bottom spacing**, removing the need for extra padding in the sample app’s `ChannelScreen`. - Added a new **Edit button** with a tick icon for clearer editing actions. - Reply UI received **theming updates** due to internal component refactor. --- ## ⬇️ Scroll to Bottom Enhancements - Refactored **Scroll to Bottom button** with: - Dedicated wrapper - Configurable `chevronColor` - Touchable support - Button visibility logic improved: - Now appears when scroll distance **exceeds the composer height** - Replaces the previous hardcoded `150px` threshold --- ## 🧱 Architectural & State Improvements - Message Composer height is now stored in a **detached internal state**. - Introduced `messageInputFloating` **config prop at the Channel level**. - Introduced **OutputButtons** as a new dedicated component to manage: - Send - Cooldown - Edit - Audio recording buttons --- ## 🎨 Theme Updates ### MessageList **New theme properties:** - `scrollToBottomButtonContainer` - `stickyHeaderContainer` - `unreadMessagesNotificationContainer` --- ### Removed / Deprecated Themes - `audioRecordingButton` theme is no longer relevant. - **SendButton** theme props removed: - `sendUpIcon` - `sendRightIcon` - `searchIcon` - `CooldownTimer.container` theme removed. - **InputButtons** - `MoreOptionsButton` - `CommandsButton` are no longer used. --- ## πŸ“Ž Attachment & Preview Changes ### ImageAttachmentUploadPreview - `itemContainer` theme removed. - New unified `container` theme introduced. ### AttachmentUploadListPreview - Removed: - `imagesFlatList` - `filesFlatList` - `wrapper` - Now uses: - Single unified `flatList` - `itemSeparator` as the only theme prop. ### FileAttachmentUploadPreview - `wrapper` theme removed. - `flatListWidth` prop removed. - Title rendering logic simplified and made more consistent. --- ## ⌨️ AutoComplete & Cooldown Updates - **AutoCompleteInput** - `coolDownActive` β†’ `cooldownRemainingSeconds` - **CooldownTimer** - `container` theme removed. --- ## βœ‚οΈ Removed Components The following components are no longer part of the Message Input flow: - `InputEditingStateHeader` - `InputReplyStateHeader` - `CommandButton` - `MoreOptionsButton` --- ## 🧩 MessageInput β€” Breaking Changes ### ❌ Removed Props - `InputEditingStateHeader` - `InputReplyStateHeader` - `StopMessageStreamingButton` - `SendButton` - `CooldownTimer` - `channel` ### βž• Added Props - `isKeyboardVisible` - `hasAttachments` --- ## 🎨 MessageInput Theme Changes ### Removed Theme Keys - `editingBoxContainer` - `editingBoxHeader` - `editingBoxHeaderTitle` - `editingStateHeader.editingBoxHeader` - `editingStateHeader.editingBoxHeaderTitle` - `imageUploadPreview.flatList` - `moreOptionsButton` - `autoCompleteInputContainer` - `optionsContainer` - `composerContainer` - `inputBox` ### Added Theme Keys - `wrapper` - `contentContainer` - `inputBoxWrapper` - `inputButtonsContainer` - `inputContainer` - `inputFloatingContainer` - `floatingWrapper` - `editButton` - `cooldownButtonContainer` - `outputButtonsContainer` --- ## ⚠️ Migration Notes - Custom themes targeting removed keys will need updates. - Remove manual bottom padding from `ChannelScreen`. - Update `AutoCompleteInput` usage to `cooldownRemainingSeconds`. - Consumers using removed MessageInput props must migrate to the new API. ---
1 parent 2218f1e commit 7b5f9d6

71 files changed

Lines changed: 2830 additions & 2460 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/SampleApp/App.tsxβ€Ž

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import { Toast } from './src/components/ToastComponent/Toast';
6060
import { useClientNotificationsToastHandler } from './src/hooks/useClientNotificationsToastHandler';
6161
import AsyncStore from './src/utils/AsyncStore.ts';
6262
import {
63+
MessageInputFloatingConfigItem,
6364
MessageListImplementationConfigItem,
6465
MessageListModeConfigItem,
6566
MessageListPruningConfigItem,
@@ -106,6 +107,9 @@ const App = () => {
106107
const [messageListPruning, setMessageListPruning] = useState<
107108
MessageListPruningConfigItem['value'] | undefined
108109
>(undefined);
110+
const [messageInputFloating, setMessageInputFloating] = useState<
111+
MessageInputFloatingConfigItem['value'] | undefined
112+
>(undefined);
109113
const colorScheme = useColorScheme();
110114
const streamChatTheme = useStreamChatTheme();
111115
const streami18n = new Streami18n();
@@ -161,13 +165,20 @@ const App = () => {
161165
'@stream-rn-sampleapp-messagelist-pruning',
162166
{ value: undefined },
163167
);
168+
const messageInputFloatingStoredValue = await AsyncStore.getItem(
169+
'@stream-rn-sampleapp-messageinput-floating',
170+
{ value: false },
171+
);
164172
setMessageListImplementation(
165173
messageListImplementationStoredValue?.id as MessageListImplementationConfigItem['id'],
166174
);
167175
setMessageListMode(messageListModeStoredValue?.mode as MessageListModeConfigItem['mode']);
168176
setMessageListPruning(
169177
messageListPruningStoredValue?.value as MessageListPruningConfigItem['value'],
170178
);
179+
setMessageInputFloating(
180+
messageInputFloatingStoredValue?.value as MessageInputFloatingConfigItem['value'],
181+
);
171182
};
172183
getMessageListConfig();
173184
return () => {
@@ -232,6 +243,7 @@ const App = () => {
232243
logout,
233244
switchUser,
234245
messageListImplementation,
246+
messageInputFloating: messageInputFloating ?? false,
235247
messageListMode,
236248
messageListPruning,
237249
}}

β€Žexamples/SampleApp/src/components/SecretMenu.tsxβ€Ž

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export type NotificationConfigItem = { label: string; name: string; id: string }
2626
export type MessageListImplementationConfigItem = { label: string; id: 'flatlist' | 'flashlist' };
2727
export type MessageListModeConfigItem = { label: string; mode: 'default' | 'livestream' };
2828
export type MessageListPruningConfigItem = { label: string; value: 100 | 500 | 1000 | undefined };
29+
export type MessageInputFloatingConfigItem = { label: string; value: boolean };
2930

3031
const messageListImplementationConfigItems: MessageListImplementationConfigItem[] = [
3132
{ label: 'FlatList', id: 'flatlist' },
@@ -44,6 +45,11 @@ const messageListPruningConfigItems: MessageListPruningConfigItem[] = [
4445
{ label: '1000 Messages', value: 1000 },
4546
];
4647

48+
const messageInputFloatingConfigItems: MessageInputFloatingConfigItem[] = [
49+
{ label: 'Normal', value: false },
50+
{ label: 'Floating', value: true },
51+
];
52+
4753
export const SlideInView = ({
4854
visible,
4955
children,
@@ -161,6 +167,23 @@ const SecretMenuMessageListImplementationConfigItem = ({
161167
</TouchableOpacity>
162168
);
163169

170+
const SecretMenuMessageInputFloatingConfigItem = ({
171+
messageInputFloatingConfigItem,
172+
storeMessageInputFloating,
173+
isSelected,
174+
}: {
175+
messageInputFloatingConfigItem: MessageInputFloatingConfigItem;
176+
storeMessageInputFloating: (item: MessageInputFloatingConfigItem) => void;
177+
isSelected: boolean;
178+
}) => (
179+
<TouchableOpacity
180+
style={[styles.notificationItemContainer, { borderColor: isSelected ? 'green' : 'gray' }]}
181+
onPress={() => storeMessageInputFloating(messageInputFloatingConfigItem)}
182+
>
183+
<Text style={styles.notificationItem}>{messageInputFloatingConfigItem.label}</Text>
184+
</TouchableOpacity>
185+
);
186+
164187
const SecretMenuMessageListModeConfigItem = ({
165188
messageListModeConfigItem,
166189
storeMessageListMode,
@@ -218,6 +241,8 @@ export const SecretMenu = ({
218241
const [selectedMessageListPruning, setSelectedMessageListPruning] = useState<
219242
MessageListPruningConfigItem['value'] | null
220243
>(null);
244+
const [selectedMessageInputFloating, setSelectedMessageInputFloating] =
245+
useState<MessageInputFloatingConfigItem['value']>(false);
221246
const {
222247
theme: {
223248
colors: { black, grey },
@@ -250,12 +275,19 @@ export const SecretMenu = ({
250275
'@stream-rn-sampleapp-messagelist-pruning',
251276
messageListPruningConfigItems[0],
252277
);
278+
const messageInputFloating = await AsyncStore.getItem(
279+
'@stream-rn-sampleapp-messageinput-floating',
280+
messageInputFloatingConfigItems[0],
281+
);
253282
setSelectedProvider(notificationProvider?.id ?? notificationConfigItems[0].id);
254283
setSelectedMessageListImplementation(
255284
messageListImplementation?.id ?? messageListImplementationConfigItems[0].id,
256285
);
257286
setSelectedMessageListMode(messageListMode?.mode ?? messageListModeConfigItems[0].mode);
258287
setSelectedMessageListPruning(messageListPruning?.value);
288+
setSelectedMessageInputFloating(
289+
messageInputFloating?.value ?? messageInputFloatingConfigItems[0].value,
290+
);
259291
};
260292
getSelectedConfig();
261293
}, [notificationConfigItems]);
@@ -283,6 +315,11 @@ export const SecretMenu = ({
283315
setSelectedMessageListPruning(item.value);
284316
}, []);
285317

318+
const storeMessageInputFloating = useCallback(async (item: MessageInputFloatingConfigItem) => {
319+
await AsyncStore.setItem('@stream-rn-sampleapp-messageinput-floating', item);
320+
setSelectedMessageInputFloating(item.value);
321+
}, []);
322+
286323
const removeAllDevices = useCallback(async () => {
287324
const { devices } = await chatClient.getDevices(chatClient.userID);
288325
for (const device of devices ?? []) {
@@ -335,6 +372,22 @@ export const SecretMenu = ({
335372
</View>
336373
</View>
337374
</View>
375+
<View style={[menuDrawerStyles.menuItem, { alignItems: 'flex-start' }]}>
376+
<Folder height={20} pathFill={grey} width={20} />
377+
<View>
378+
<Text style={[menuDrawerStyles.menuTitle]}>Message Input Floating</Text>
379+
<View style={{ marginLeft: 16 }}>
380+
{messageInputFloatingConfigItems.map((item) => (
381+
<SecretMenuMessageInputFloatingConfigItem
382+
key={item.value.toString()}
383+
messageInputFloatingConfigItem={item}
384+
storeMessageInputFloating={storeMessageInputFloating}
385+
isSelected={item.value === selectedMessageInputFloating}
386+
/>
387+
))}
388+
</View>
389+
</View>
390+
</View>
338391
<View style={[menuDrawerStyles.menuItem, { alignItems: 'flex-start' }]}>
339392
<Edit height={20} pathFill={grey} width={20} />
340393
<View>

β€Žexamples/SampleApp/src/context/AppContext.tsβ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { StreamChat } from 'stream-chat';
44

55
import type { LoginConfig } from '../types';
66
import {
7+
MessageInputFloatingConfigItem,
78
MessageListImplementationConfigItem,
89
MessageListModeConfigItem,
910
MessageListPruningConfigItem,
@@ -15,6 +16,7 @@ type AppContextType = {
1516
logout: () => void;
1617
switchUser: (userId?: string) => void;
1718
messageListImplementation: MessageListImplementationConfigItem['id'];
19+
messageInputFloating: MessageInputFloatingConfigItem['value'];
1820
messageListMode: MessageListModeConfigItem['mode'];
1921
messageListPruning: MessageListPruningConfigItem['value'];
2022
};

β€Žexamples/SampleApp/src/screens/ChannelListScreen.tsxβ€Ž

Lines changed: 2 additions & 1 deletion
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, CircleClose, Search, useTheme } from 'stream-chat-react-native';
12+
import { ChannelList, CircleClose, useTheme } from 'stream-chat-react-native';
1313
import { Channel } from 'stream-chat';
1414
import { ChannelPreview } from '../components/ChannelPreview';
1515
import { ChatScreenHeader } from '../components/ChatScreenHeader';
@@ -19,6 +19,7 @@ import { usePaginatedSearchedMessages } from '../hooks/usePaginatedSearchedMessa
1919

2020
import type { ChannelSort } from 'stream-chat';
2121
import { useStreamChatContext } from '../context/StreamChatContext';
22+
import { Search } from '../icons/Search';
2223

2324
const styles = StyleSheet.create({
2425
channelListContainer: {

β€Žexamples/SampleApp/src/screens/ChannelScreen.tsxβ€Ž

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
} from 'stream-chat-react-native';
2121
import { Pressable, StyleSheet, View } from 'react-native';
2222
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
23-
import { useSafeAreaInsets } from 'react-native-safe-area-context';
2423

2524
import { useAppContext } from '../context/AppContext';
2625
import { ScreenHeader } from '../components/ScreenHeader';
@@ -122,9 +121,13 @@ export const ChannelScreen: React.FC<ChannelScreenProps> = ({
122121
params: { channel: channelFromProp, channelId, messageId },
123122
},
124123
}) => {
125-
const { chatClient, messageListImplementation, messageListMode, messageListPruning } =
126-
useAppContext();
127-
const { bottom } = useSafeAreaInsets();
124+
const {
125+
chatClient,
126+
messageListImplementation,
127+
messageListMode,
128+
messageListPruning,
129+
messageInputFloating,
130+
} = useAppContext();
128131
const {
129132
theme: { colors },
130133
} = useTheme();
@@ -218,11 +221,12 @@ export const ChannelScreen: React.FC<ChannelScreenProps> = ({
218221
}
219222

220223
return (
221-
<View style={[styles.flex, { backgroundColor: colors.white_snow, paddingBottom: bottom }]}>
224+
<View style={[styles.flex, { backgroundColor: 'transparent' }]}>
222225
<Channel
223-
audioRecordingEnabled={true}
226+
audioRecordingEnabled={false}
224227
AttachmentPickerSelectionBar={CustomAttachmentPickerSelectionBar}
225228
channel={channel}
229+
messageInputFloating={messageInputFloating}
226230
onPressMessage={onPressMessage}
227231
disableTypingIndicator
228232
enforceUniqueReaction

β€Žexamples/SampleApp/src/screens/ThreadScreen.tsxβ€Ž

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useCallback } from 'react';
2-
import { Platform, StyleSheet } from 'react-native';
3-
import { SafeAreaView } from 'react-native-safe-area-context';
2+
import { StyleSheet, View } from 'react-native';
3+
import { useHeaderHeight } from '@react-navigation/elements';
4+
45
import {
56
Channel,
67
MessageActionsParams,
@@ -26,6 +27,7 @@ import { useStreamChatContext } from '../context/StreamChatContext.tsx';
2627
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
2728
import { CustomAttachmentPickerSelectionBar } from '../components/AttachmentPickerSelectionBar.tsx';
2829
import { MessageLocation } from '../components/LocationSharing/MessageLocation.tsx';
30+
import { useAppContext } from '../context/AppContext.ts';
2931

3032
const selector = (nextValue: ThreadState) => ({ parentMessage: nextValue.parentMessage }) as const;
3133

@@ -84,6 +86,8 @@ export const ThreadScreen: React.FC<ThreadScreenProps> = ({
8486
const { client: chatClient } = useChatContext();
8587
const { t } = useTranslationContext();
8688
const { setThread } = useStreamChatContext();
89+
const { messageInputFloating } = useAppContext();
90+
const headerHeight = useHeaderHeight();
8791

8892
const onPressMessage: NonNullable<React.ComponentProps<typeof Channel>['onPressMessage']> = (
8993
payload,
@@ -115,14 +119,15 @@ export const ThreadScreen: React.FC<ThreadScreenProps> = ({
115119
}, [setThread]);
116120

117121
return (
118-
<SafeAreaView edges={['bottom']} style={[styles.container, { backgroundColor: white }]}>
122+
<View style={[styles.container, { backgroundColor: white }]}>
119123
<Channel
120-
audioRecordingEnabled={true}
124+
audioRecordingEnabled={false}
121125
AttachmentPickerSelectionBar={CustomAttachmentPickerSelectionBar}
122126
channel={channel}
123127
enforceUniqueReaction
124-
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : -300}
128+
keyboardVerticalOffset={headerHeight}
125129
messageActions={messageActions}
130+
messageInputFloating={messageInputFloating}
126131
MessageHeader={MessageReminderHeader}
127132
MessageLocation={MessageLocation}
128133
onPressMessage={onPressMessage}
@@ -132,6 +137,6 @@ export const ThreadScreen: React.FC<ThreadScreenProps> = ({
132137
<ThreadHeader thread={thread} />
133138
<Thread onThreadDismount={onThreadDismount} />
134139
</Channel>
135-
</SafeAreaView>
140+
</View>
136141
);
137142
};

0 commit comments

Comments
Β (0)