diff --git a/examples/ExpoMessaging/app/channel/[cid]/index.tsx b/examples/ExpoMessaging/app/channel/[cid]/index.tsx index c52c8ef231..0410bec2f6 100644 --- a/examples/ExpoMessaging/app/channel/[cid]/index.tsx +++ b/examples/ExpoMessaging/app/channel/[cid]/index.tsx @@ -6,14 +6,11 @@ import { useChatContext, ThreadContextValue, MessageList, - WithComponents, } from 'stream-chat-expo'; import { Stack, useLocalSearchParams, useRouter } from 'expo-router'; import { AuthProgressLoader } from '../../../components/AuthProgressLoader'; import { AppContext } from '../../../context/AppContext'; import { useHeaderHeight } from '@react-navigation/elements'; -import InputButtons from '../../../components/InputButtons'; -import { MessageLocation } from '../../../components/LocationSharing/MessageLocation'; import { StyleSheet, View } from 'react-native'; export default function ChannelScreen() { @@ -71,23 +68,21 @@ export default function ChannelScreen() { - - - { - setThread(thread); - router.push(`/channel/${channel.cid}/thread/${thread?.cid ?? ''}`); - }} - /> - - - + + { + setThread(thread); + router.push(`/channel/${channel.cid}/thread/${thread?.cid ?? ''}`); + }} + /> + + ); } diff --git a/examples/ExpoMessaging/components/ChatWrapper.tsx b/examples/ExpoMessaging/components/ChatWrapper.tsx index d0bf6eff10..416eadc24d 100644 --- a/examples/ExpoMessaging/components/ChatWrapper.tsx +++ b/examples/ExpoMessaging/components/ChatWrapper.tsx @@ -6,6 +6,7 @@ import { SqliteClient, Streami18n, useCreateChatClient, + WithComponents, } from 'stream-chat-expo'; import { UserResponse } from 'stream-chat'; import { AuthProgressLoader } from './AuthProgressLoader'; @@ -13,6 +14,7 @@ import { useStreamChatTheme } from '../hooks/useStreamChatTheme'; import { useUserContext } from '@/context/UserContext'; import { STREAM_API_KEY, USER_TOKENS } from '@/constants/ChatUsers'; import { usePushNotifications } from '@/hooks/usePushNotifications'; +import { useExpoMessagingComponentOverrides } from './ExpoMessagingComponentOverrides'; import '../utils/backgroundMessageHandler'; const streami18n = new Streami18n({ @@ -39,16 +41,19 @@ export const ChatWrapper = ({ children }: PropsWithChildren<{}>) => { }); const theme = useStreamChatTheme(); + const componentOverrides = useExpoMessagingComponentOverrides(); if (!chatClient) { return ; } return ( - - - {children} - - + + + + {children} + + + ); }; diff --git a/examples/ExpoMessaging/components/ExpoMessagingComponentOverrides.tsx b/examples/ExpoMessaging/components/ExpoMessagingComponentOverrides.tsx new file mode 100644 index 0000000000..1f18c3703d --- /dev/null +++ b/examples/ExpoMessaging/components/ExpoMessagingComponentOverrides.tsx @@ -0,0 +1,12 @@ +import { useMemo } from 'react'; +import type { ComponentOverrides } from 'stream-chat-expo'; + +import InputButtons from './InputButtons'; +import { MessageLocation } from './LocationSharing/MessageLocation'; + +export const useExpoMessagingComponentOverrides = () => { + return useMemo( + () => ({ InputButtons, MessageLocation }), + [], + ); +}; diff --git a/examples/SampleApp/App.tsx b/examples/SampleApp/App.tsx index 474491377d..046b44170c 100644 --- a/examples/SampleApp/App.tsx +++ b/examples/SampleApp/App.tsx @@ -4,15 +4,12 @@ import { I18nManager, LogBox, Platform, - StyleSheet, useColorScheme, - View, } from 'react-native'; import { createDrawerNavigator } from '@react-navigation/drawer'; import { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { SafeAreaProvider } from 'react-native-safe-area-context'; -import { BlurView } from '@react-native-community/blur'; import { Chat, createTextComposerEmojiMiddleware, @@ -23,7 +20,7 @@ import { Streami18n, ThemeProvider, useOverlayContext, - useTheme, + WithComponents, } from 'stream-chat-react-native'; import { getMessaging } from '@react-native-firebase/messaging'; @@ -55,10 +52,10 @@ import Geolocation from '@react-native-community/geolocation'; import type { StackNavigatorParamList, UserSelectorParamList } from './src/types'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { navigateToChannel, RootNavigationRef } from './src/utils/RootNavigation'; -import FastImage from 'react-native-fast-image'; import { StreamChatProvider } from './src/context/StreamChatContext'; import { MapScreen } from './src/screens/MapScreen'; import { watchLocation } from './src/utils/watchLocation'; +import { useSampleAppComponentOverrides } from './src/components/SampleAppComponentOverrides'; Geolocation.setRNConfiguration({ skipPermissionRequests: false, @@ -107,30 +104,6 @@ const Stack = createNativeStackNavigator(); const UserSelectorStack = createNativeStackNavigator(); const RTL_STORAGE_KEY = '@stream-rn-sampleapp-rtl-enabled'; -const MessageOverlayBlurBackground = () => { - const { - theme: { semantics }, - } = useTheme(); - const scheme = useColorScheme(); - const isDark = scheme === 'dark'; - const isIOS = Platform.OS === 'ios'; - - return ( - <> - - - - ); -}; - const App = () => { const { chatClient, isConnecting, loginUser, logout, switchUser } = useChatClient(); const [rtlEnabled, setRtlEnabled] = useState(undefined); @@ -152,6 +125,7 @@ const App = () => { const colorScheme = useColorScheme(); const streamChatTheme = useStreamChatTheme(); const streami18n = new Streami18n(); + const componentOverrides = useSampleAppComponentOverrides(messageOverlayBackdrop); const setRTLEnabled = React.useCallback(async (enabled: boolean) => { await AsyncStore.setItem(RTL_STORAGE_KEY, enabled); @@ -295,50 +269,46 @@ const App = () => { }} > - - - - + + + - {isConnecting && !chatClient ? ( - - ) : chatClient ? ( - - ) : ( - - )} - - - - + + {isConnecting && !chatClient ? ( + + ) : chatClient ? ( + + ) : ( + + )} + + + + + ); @@ -369,8 +339,6 @@ const DrawerNavigatorWrapper: React.FC<{ diff --git a/examples/SampleApp/src/components/ChannelPreview.tsx b/examples/SampleApp/src/components/ChannelPreview.tsx index 2146d9c11d..81ff28f77a 100644 --- a/examples/SampleApp/src/components/ChannelPreview.tsx +++ b/examples/SampleApp/src/components/ChannelPreview.tsx @@ -1,8 +1,6 @@ import React from 'react'; import { StyleSheet, View } from 'react-native'; import { - ChannelPreviewView, - ChannelPreviewViewProps, ChannelPreviewStatus, ChannelPreviewStatusProps, Pin, @@ -34,7 +32,7 @@ const styles = StyleSheet.create({ }, }); -const CustomChannelPreviewStatus = (props: ChannelPreviewStatusProps) => { +export const CustomChannelPreviewStatus = (props: ChannelPreviewStatusProps) => { const { channel } = props; const membership = useChannelMembershipState(channel); @@ -53,7 +51,3 @@ const CustomChannelPreviewStatus = (props: ChannelPreviewStatusProps) => { ); }; - -export const ChannelPreview: React.FC = (props) => { - return ; -}; diff --git a/examples/SampleApp/src/components/SampleAppComponentOverrides.tsx b/examples/SampleApp/src/components/SampleAppComponentOverrides.tsx new file mode 100644 index 0000000000..d5ec67d778 --- /dev/null +++ b/examples/SampleApp/src/components/SampleAppComponentOverrides.tsx @@ -0,0 +1,60 @@ +import React, { useMemo } from 'react'; +import { Platform, StyleSheet, useColorScheme, View } from 'react-native'; +import type { ComponentOverrides } from 'stream-chat-react-native'; +import { BlurView } from '@react-native-community/blur'; +import FastImage from 'react-native-fast-image'; +import { + useTheme, +} from 'stream-chat-react-native'; + +import { CustomAttachmentPickerContent } from './AttachmentPickerContent'; +import { CustomChannelPreviewStatus } from './ChannelPreview'; +import { MessageLocation } from './LocationSharing/MessageLocation'; +import type { MessageOverlayBackdropConfigItem } from './SecretMenu'; + +const MessageOverlayBlurBackground: NonNullable = + () => { + const { + theme: { semantics }, + } = useTheme(); + const scheme = useColorScheme(); + const isDark = scheme === 'dark'; + const isIOS = Platform.OS === 'ios'; + + return ( + <> + + + + ); + }; + +const RenderNull = () => null; + +export const useSampleAppComponentOverrides = ( + messageOverlayBackdrop?: MessageOverlayBackdropConfigItem['value'], +) => + useMemo( + () => ({ + AttachmentPickerContent: CustomAttachmentPickerContent, + ChannelListHeaderNetworkDownIndicator: RenderNull, + ImageComponent: FastImage, + MessageLocation, + NetworkDownIndicator: RenderNull, + ChannelPreviewStatus: CustomChannelPreviewStatus, + ...(messageOverlayBackdrop === 'blurview' + ? { MessageOverlayBackground: MessageOverlayBlurBackground } + : {}), + }), + [messageOverlayBackdrop], + ); diff --git a/examples/SampleApp/src/screens/ChannelListScreen.tsx b/examples/SampleApp/src/screens/ChannelListScreen.tsx index 5408e6438a..8053615deb 100644 --- a/examples/SampleApp/src/screens/ChannelListScreen.tsx +++ b/examples/SampleApp/src/screens/ChannelListScreen.tsx @@ -10,9 +10,8 @@ import { View, } from 'react-native'; import { useNavigation, useScrollToTop } from '@react-navigation/native'; -import { ChannelList, useTheme, useStableCallback, ChannelActionItem, WithComponents } from 'stream-chat-react-native'; +import { ChannelList, useTheme, useStableCallback, ChannelActionItem } from 'stream-chat-react-native'; import { Channel } from 'stream-chat'; -import { ChannelPreview } from '../components/ChannelPreview'; import { ChatScreenHeader } from '../components/ChatScreenHeader'; import { MessageSearchList } from '../components/MessageSearch/MessageSearchList'; import { useAppContext } from '../context/AppContext'; @@ -75,8 +74,6 @@ const options = { message_limit: 25, }; -const HeaderNetworkDownIndicator = () => null; - export const ChannelListScreen: React.FC = () => { const { chatClient } = useAppContext(); const navigation = useNavigation(); @@ -249,23 +246,16 @@ export const ChannelListScreen: React.FC = () => { )} - - - + diff --git a/examples/SampleApp/src/screens/ChannelScreen.tsx b/examples/SampleApp/src/screens/ChannelScreen.tsx index 025f31038e..f423215596 100644 --- a/examples/SampleApp/src/screens/ChannelScreen.tsx +++ b/examples/SampleApp/src/screens/ChannelScreen.tsx @@ -17,7 +17,6 @@ import { MessageActionsParams, ChannelAvatar, PortalWhileClosingView, - WithComponents, } from 'stream-chat-react-native'; import { Pressable, StyleSheet, View } from 'react-native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; @@ -30,11 +29,9 @@ import type { StackNavigatorParamList } from '../types'; import { NetworkDownIndicator } from '../components/NetworkDownIndicator'; import { useCreateDraftFocusEffect } from '../utils/useCreateDraftFocusEffect.tsx'; import { channelMessageActions } from '../utils/messageActions.tsx'; -import { MessageLocation } from '../components/LocationSharing/MessageLocation.tsx'; import { useStreamChatContext } from '../context/StreamChatContext.tsx'; // import { CustomAttachmentPickerSelectionBar } from '../components/AttachmentPickerSelectionBar.tsx'; import { MessageInfoBottomSheet } from '../components/MessageInfoBottomSheet.tsx'; -import { CustomAttachmentPickerContent } from '../components/AttachmentPickerContent.tsx'; import { ThreadType } from 'stream-chat-react-native-core'; export type ChannelScreenNavigationProp = NativeStackNavigationProp< @@ -266,14 +263,6 @@ export const ChannelScreen: React.FC = ({ navigation, route return ( - null, - }} - > = ({ navigation, route /> )} - ); }; diff --git a/examples/SampleApp/src/screens/NewDirectMessagingScreen.tsx b/examples/SampleApp/src/screens/NewDirectMessagingScreen.tsx index cb83e70aa0..192f094e92 100644 --- a/examples/SampleApp/src/screens/NewDirectMessagingScreen.tsx +++ b/examples/SampleApp/src/screens/NewDirectMessagingScreen.tsx @@ -8,7 +8,6 @@ import { MessageList, UserAdd, useTheme, - WithComponents, } from 'stream-chat-react-native'; import { User } from '../icons/User'; @@ -23,7 +22,6 @@ import { useLegacyColors } from '../theme/useLegacyColors'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { Channel as StreamChatChannel } from 'stream-chat'; -import { NewDirectMessagingSendButton } from '../components/NewDirectMessagingSendButton'; import type { StackNavigatorParamList } from '../types'; import { Group } from '../icons/Group'; @@ -85,7 +83,7 @@ const styles = StyleSheet.create({ }, }); -const EmptyMessagesIndicator = () => { +export const EmptyMessagesIndicator = () => { const { grey } = useLegacyColors(); return ( @@ -339,37 +337,30 @@ export const NewDirectMessagingScreen: React.FC = }, ]} > - { + setFocusOnMessageInput(true); + setFocusOnSearchInput(false); + if (messageInputRef.current) { + messageInputRef.current.focus(); + } + }, }} + audioRecordingEnabled={true} + channel={currentChannel.current} + enforceUniqueReaction + keyboardVerticalOffset={0} + onChangeText={setMessageInputText} + overrideOwnCapabilities={{ sendMessage: true }} + setInputRef={(ref) => (messageInputRef.current = ref)} > - { - setFocusOnMessageInput(true); - setFocusOnSearchInput(false); - if (messageInputRef.current) { - messageInputRef.current.focus(); - } - }, - }} - audioRecordingEnabled={true} - channel={currentChannel.current} - enforceUniqueReaction - keyboardVerticalOffset={0} - onChangeText={setMessageInputText} - overrideOwnCapabilities={{ sendMessage: true }} - setInputRef={(ref) => (messageInputRef.current = ref)} - > - {renderUserSearch({ inSafeArea: true })} - {results && results.length >= 0 && !focusOnSearchInput && focusOnMessageInput && ( - - )} - - - + {renderUserSearch({ inSafeArea: true })} + {results && results.length >= 0 && !focusOnSearchInput && focusOnMessageInput && ( + + )} + + ); }; diff --git a/examples/SampleApp/src/screens/SharedGroupsScreen.tsx b/examples/SampleApp/src/screens/SharedGroupsScreen.tsx index c092f1e97d..263fa22430 100644 --- a/examples/SampleApp/src/screens/SharedGroupsScreen.tsx +++ b/examples/SampleApp/src/screens/SharedGroupsScreen.tsx @@ -11,7 +11,6 @@ import { useTheme, Avatar, getInitialsFromName, - WithComponents, } from 'stream-chat-react-native'; import { ScreenHeader } from '../components/ScreenHeader'; @@ -57,7 +56,7 @@ const styles = StyleSheet.create({ type CustomPreviewProps = ChannelPreviewViewProps; -const CustomPreview: React.FC = ({ channel }) => { +export const SharedGroupsPreview: React.FC = ({ channel }) => { const { chatClient } = useAppContext(); const name = useChannelPreviewDisplayName(channel, 30); const navigation = useNavigation>(); @@ -145,7 +144,7 @@ const EmptyListComponent = () => { }; // Custom empty state that also shows when there's only the 1:1 direct channel -const SharedGroupsEmptyState = () => { +export const SharedGroupsEmptyState = () => { const { channels, loadingChannels, refreshing } = useChannelsContext(); if (loadingChannels || refreshing) { @@ -179,24 +178,17 @@ export const SharedGroupsScreen: React.FC = ({ return ( - - - + options={{ + watch: false, + }} + sort={{ + last_updated: -1, + }} + /> ); }; diff --git a/examples/SampleApp/src/screens/ThreadScreen.tsx b/examples/SampleApp/src/screens/ThreadScreen.tsx index 8884bf9bc0..cbb5241e4d 100644 --- a/examples/SampleApp/src/screens/ThreadScreen.tsx +++ b/examples/SampleApp/src/screens/ThreadScreen.tsx @@ -12,7 +12,6 @@ import { useTranslationContext, useTypingString, PortalWhileClosingView, - WithComponents, } from 'stream-chat-react-native'; import { useStateStore } from 'stream-chat-react-native'; @@ -27,7 +26,6 @@ import { channelMessageActions } from '../utils/messageActions.tsx'; import { useStreamChatContext } from '../context/StreamChatContext.tsx'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; // import { CustomAttachmentPickerSelectionBar } from '../components/AttachmentPickerSelectionBar.tsx'; -import { MessageLocation } from '../components/LocationSharing/MessageLocation.tsx'; import { useAppContext } from '../context/AppContext.ts'; import { useLegacyColors } from '../theme/useLegacyColors'; @@ -149,12 +147,6 @@ export const ThreadScreen: React.FC = ({ return ( - = ({ shouldUseFlashList={messageListImplementation === 'flashlist'} /> - ); }; diff --git a/package/src/__tests__/offline-support/offline-feature.js b/package/src/__tests__/offline-support/offline-feature.js index bf6b72e38e..970338ee02 100644 --- a/package/src/__tests__/offline-support/offline-feature.js +++ b/package/src/__tests__/offline-support/offline-feature.js @@ -49,7 +49,7 @@ import { BetterSqlite } from '../../test-utils/BetterSqlite'; * to debug. */ /** - * Custom Preview component used via WithComponents. + * Custom ChannelPreview component used via WithComponents. * Receives { channel, muted, unread, lastMessage } from ChannelPreview. */ const ChannelPreviewComponent = ({ channel }) => ( @@ -207,7 +207,7 @@ export const Generic = () => { const renderComponent = () => render( - + , diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 0cde399ac9..2a2893b67a 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -235,7 +235,6 @@ export type ChannelPropsWithContext = Pick & MessagesContextValue, | 'additionalPressableProps' | 'customMessageSwipeAction' - | 'deletedMessagesVisibilityType' | 'disableTypingIndicator' | 'dismissKeyboardOnMessageTouch' | 'enableSwipeToReply' @@ -405,7 +404,6 @@ const ChannelWithContext = (props: PropsWithChildren) = compressImageQuality, createPollOptionGap, customMessageSwipeAction, - deletedMessagesVisibilityType = 'always', disableKeyboardCompatibleView = false, disableTypingIndicator, dismissKeyboardOnMessageTouch = true, @@ -1635,7 +1633,6 @@ const ChannelWithContext = (props: PropsWithChildren) = additionalPressableProps, channelId, customMessageSwipeAction, - deletedMessagesVisibilityType, deleteMessage, deleteReaction, disableTypingIndicator, diff --git a/package/src/components/Channel/hooks/useCreateMessagesContext.ts b/package/src/components/Channel/hooks/useCreateMessagesContext.ts index 62c375cec6..e90b13ef3b 100644 --- a/package/src/components/Channel/hooks/useCreateMessagesContext.ts +++ b/package/src/components/Channel/hooks/useCreateMessagesContext.ts @@ -6,7 +6,6 @@ export const useCreateMessagesContext = ({ additionalPressableProps, channelId, customMessageSwipeAction, - deletedMessagesVisibilityType, deleteMessage, deleteReaction, disableTypingIndicator, @@ -70,7 +69,6 @@ export const useCreateMessagesContext = ({ () => ({ additionalPressableProps, customMessageSwipeAction, - deletedMessagesVisibilityType, deleteMessage, deleteReaction, disableTypingIndicator, diff --git a/package/src/components/ChannelList/ChannelListView.tsx b/package/src/components/ChannelList/ChannelListView.tsx index 23814ae7a1..3731d7f746 100644 --- a/package/src/components/ChannelList/ChannelListView.tsx +++ b/package/src/components/ChannelList/ChannelListView.tsx @@ -26,7 +26,8 @@ const StatusIndicator = () => { const { isOnline } = useChatContext(); const styles = useStyles(); const { error, loadingChannels, refreshList } = useChannelsContext(); - const { HeaderErrorIndicator, HeaderNetworkDownIndicator } = useComponentsContext(); + const { ChannelListHeaderErrorIndicator, ChannelListHeaderNetworkDownIndicator } = + useComponentsContext(); if (loadingChannels) { return null; @@ -35,13 +36,13 @@ const StatusIndicator = () => { if (!isOnline) { return ( - + ); } else if (error) { return ( - + ); } @@ -72,7 +73,7 @@ const ChannelListViewWithContext = (props: ChannelListViewPropsWithContext) => { } = props; const { EmptyStateIndicator, - FooterLoadingIndicator, + ChannelListFooterLoadingIndicator, ListHeaderComponent, LoadingErrorIndicator, ChannelListLoadingIndicator: LoadingIndicator, @@ -138,7 +139,7 @@ const ChannelListViewWithContext = (props: ChannelListViewPropsWithContext) => { ListEmptyComponent={ loading ? : } - ListFooterComponent={loadingNextPage ? : undefined} + ListFooterComponent={loadingNextPage ? : undefined} ListHeaderComponent={ListHeaderComponent} onEndReached={onEndReached} onEndReachedThreshold={loadMoreThreshold} diff --git a/package/src/components/ChannelList/__tests__/ChannelList.test.js b/package/src/components/ChannelList/__tests__/ChannelList.test.js index 5700d93027..3fdadd4b15 100644 --- a/package/src/components/ChannelList/__tests__/ChannelList.test.js +++ b/package/src/components/ChannelList/__tests__/ChannelList.test.js @@ -46,7 +46,7 @@ jest.mock('../../ChannelPreview/ChannelSwipableWrapper', () => ({ })); /** - * Custom Preview component used via WithComponents to verify channel rendering. + * Custom ChannelPreview component used via WithComponents to verify channel rendering. * Receives { channel, muted, unread, lastMessage } from ChannelPreview. */ const ChannelPreviewComponent = ({ channel }) => ( @@ -58,7 +58,7 @@ const ChannelPreviewComponent = ({ channel }) => ( /** * Probe that reads swipeActionsEnabled from ChannelsContext. - * Used as a Preview override to inspect context values. + * Used as a ChannelPreview override to inspect context values. */ const SwipeActionsProbe = () => { const { swipeActionsEnabled } = useChannelsContext(); @@ -119,7 +119,7 @@ describe('ChannelList', () => { const { getByTestId } = render( - + , @@ -133,7 +133,7 @@ describe('ChannelList', () => { const { getByTestId } = render( - + , @@ -147,7 +147,7 @@ describe('ChannelList', () => { render( - + , @@ -162,7 +162,7 @@ describe('ChannelList', () => { screen.rerender( - + , @@ -204,7 +204,7 @@ describe('ChannelList', () => { const { rerender, queryByTestId } = render( - + , @@ -224,7 +224,7 @@ describe('ChannelList', () => { rerender( - + , @@ -254,7 +254,7 @@ describe('ChannelList', () => { render( - + , @@ -276,7 +276,7 @@ describe('ChannelList', () => { const { getByTestId } = render( - + , @@ -291,7 +291,7 @@ describe('ChannelList', () => { const { getByTestId } = render( - + , @@ -306,7 +306,7 @@ describe('ChannelList', () => { const { getByTestId, queryByTestId } = render( - + , @@ -323,7 +323,7 @@ describe('ChannelList', () => { const { getByTestId } = render( - + , @@ -344,7 +344,7 @@ describe('ChannelList', () => { @@ -366,7 +366,7 @@ describe('ChannelList', () => { @@ -395,7 +395,7 @@ describe('ChannelList', () => { it('should move channel to top of the list by default', async () => { render( - + , @@ -419,7 +419,7 @@ describe('ChannelList', () => { it('should add channel to top if channel is hidden from the list', async () => { render( - + , @@ -449,7 +449,7 @@ describe('ChannelList', () => { it('should not alter order if `lockChannelOrder` prop is true', async () => { render( - + , @@ -475,7 +475,7 @@ describe('ChannelList', () => { const onNewMessage = jest.fn(); render( - + , @@ -504,7 +504,7 @@ describe('ChannelList', () => { it('should move a channel to top of the list by default', async () => { render( - + , @@ -528,7 +528,7 @@ describe('ChannelList', () => { const onNewMessage = jest.fn(); render( - + , @@ -549,7 +549,7 @@ describe('ChannelList', () => { const onNewMessageNotification = jest.fn(); render( - + , @@ -578,7 +578,7 @@ describe('ChannelList', () => { it('should move a channel to top of the list by default', async () => { render( - + , @@ -605,7 +605,7 @@ describe('ChannelList', () => { const onAddedToChannel = jest.fn(); render( - + , @@ -631,7 +631,7 @@ describe('ChannelList', () => { it('should remove the channel from list by default', async () => { render( - + , @@ -658,7 +658,7 @@ describe('ChannelList', () => { const onRemovedFromChannel = jest.fn(); render( - + , @@ -684,7 +684,7 @@ describe('ChannelList', () => { it('should update a channel in the list by default', async () => { render( - + , @@ -710,7 +710,7 @@ describe('ChannelList', () => { const onChannelUpdated = jest.fn(); render( - + , @@ -741,7 +741,7 @@ describe('ChannelList', () => { it('should remove a channel from the list by default', async () => { render( - + , @@ -768,7 +768,7 @@ describe('ChannelList', () => { const onChannelDeleted = jest.fn(); render( - + , @@ -794,7 +794,7 @@ describe('ChannelList', () => { it('should hide a channel from the list by default', async () => { render( - + , @@ -821,7 +821,7 @@ describe('ChannelList', () => { const onChannelHidden = jest.fn(); render( - + , @@ -846,7 +846,7 @@ describe('ChannelList', () => { render( - + , @@ -874,7 +874,7 @@ describe('ChannelList', () => { render( - + , @@ -909,7 +909,7 @@ describe('ChannelList', () => { const onChannelTruncated = jest.fn(); render( - + , diff --git a/package/src/components/ChannelPreview/ChannelPreview.tsx b/package/src/components/ChannelPreview/ChannelPreview.tsx index 81b386b95b..79c0839ffc 100644 --- a/package/src/components/ChannelPreview/ChannelPreview.tsx +++ b/package/src/components/ChannelPreview/ChannelPreview.tsx @@ -26,7 +26,7 @@ export const ChannelPreview = (props: ChannelPreviewProps) => { const { client: contextClient } = useChatContext(); const { getChannelActionItems, swipeActionsEnabled } = useChannelsContext(); - const { Preview } = useComponentsContext(); + const { ChannelPreview } = useComponentsContext(); const client = propClient || contextClient; @@ -37,12 +37,12 @@ export const ChannelPreview = (props: ChannelPreviewProps) => { const message = translatedLastMessage ? translatedLastMessage : lastMessage; if (!swipeActionsEnabled) { - return ; + return ; } return ( - + ); }; diff --git a/package/src/components/ChannelPreview/ChannelPreviewMessage.tsx b/package/src/components/ChannelPreview/ChannelPreviewMessage.tsx index 2dbed3fd35..b086fe553e 100644 --- a/package/src/components/ChannelPreview/ChannelPreviewMessage.tsx +++ b/package/src/components/ChannelPreview/ChannelPreviewMessage.tsx @@ -26,8 +26,11 @@ export type ChannelPreviewMessageProps = Pick & export const ChannelPreviewMessage = (props: ChannelPreviewMessageProps) => { const { channel, lastMessage } = props; - const { PreviewTypingIndicator, PreviewMessageDeliveryStatus, PreviewLastMessage } = - useComponentsContext(); + const { + ChannelPreviewTypingIndicator, + ChannelPreviewMessageDeliveryStatus, + ChannelPreviewLastMessage, + } = useComponentsContext(); const { theme: { semantics }, } = useTheme(); @@ -53,14 +56,14 @@ export const ChannelPreviewMessage = (props: ChannelPreviewMessageProps) => { const showMessageDeliveryStatus = !isMessageDeleted; if (usersTyping.length > 0) { - return ; + return ; } if (draftMessage) { return ( {t('Draft')}: - + ); } @@ -98,18 +101,18 @@ export const ChannelPreviewMessage = (props: ChannelPreviewMessageProps) => { return ( {showMessageDeliveryStatus ? ( - + ) : null} - + ); } else { return ( {showMessageDeliveryStatus ? ( - + ) : null} - + ); } diff --git a/package/src/components/ChannelPreview/ChannelPreviewView.tsx b/package/src/components/ChannelPreview/ChannelPreviewView.tsx index 4bb0f224e8..f57c2e066b 100644 --- a/package/src/components/ChannelPreview/ChannelPreviewView.tsx +++ b/package/src/components/ChannelPreview/ChannelPreviewView.tsx @@ -46,12 +46,12 @@ const ChannelPreviewViewWithContext = (props: ChannelPreviewViewPropsWithContext lastMessage, } = props; const { - PreviewAvatar, - PreviewMessage, - PreviewMutedStatus, - PreviewStatus, - PreviewTitle, - PreviewUnreadCount, + ChannelPreviewAvatar, + ChannelPreviewMessage, + ChannelPreviewMutedStatus, + ChannelPreviewStatus, + ChannelPreviewTitle, + ChannelPreviewUnreadCount, } = useComponentsContext(); const { @@ -97,24 +97,26 @@ const ChannelPreviewViewWithContext = (props: ChannelPreviewViewPropsWithContext ]} testID='channel-preview-button' > - + - - {muted && mutedStatusPosition === 'inlineTitle' ? : null} + + {muted && mutedStatusPosition === 'inlineTitle' ? ( + + ) : null} - - - - {muted && mutedStatusPosition === 'trailingBottom' ? : null} + + {muted && mutedStatusPosition === 'trailingBottom' ? ( + + ) : null} diff --git a/package/src/components/ChannelPreview/__tests__/ChannelPreview.test.tsx b/package/src/components/ChannelPreview/__tests__/ChannelPreview.test.tsx index 42cb1ed3e2..fbf44b83ac 100644 --- a/package/src/components/ChannelPreview/__tests__/ChannelPreview.test.tsx +++ b/package/src/components/ChannelPreview/__tests__/ChannelPreview.test.tsx @@ -84,7 +84,7 @@ describe('ChannelPreview', () => { return ( - + @@ -437,7 +437,7 @@ describe('ChannelPreview', () => { { }); }); - it('should render deleted message in the list when `deleteMessagesVisibilityType` is set to default(always)', async () => { + it('should render deleted message in the list', async () => { const user1 = generateUser(); const mockedChannel = generateChannelResponse({ members: [generateMember({ user: user1 })], @@ -124,135 +124,6 @@ describe('MessageList', () => { }); }); - it('should render deleted message in the list when `deleteMessagesVisibilityType` is set to sender', async () => { - const user1 = generateUser(); - const user2 = generateUser({ id: 'testID' }); - const mockedChannel = generateChannelResponse({ - members: [generateMember({ user: user1 })], - messages: [ - generateMessage({ type: 'deleted', user: user2 }), - generateMessage({ type: 'system', user: undefined }), - generateMessage({ user: user1 }), - ], - }); - - const chatClient = await getTestClientWithUser({ id: 'testID' }); - useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]); - const channel = chatClient.channel('messaging', mockedChannel.id); - await channel.watch(); - - const { queryByTestId } = render( - - - - - - - , - ); - - await waitFor(() => { - expect(queryByTestId('message-deleted')).toBeTruthy(); - }); - }); - - it('should render deleted message in the list when `deleteMessagesVisibilityType` is set to receiver', async () => { - const user1 = generateUser(); - const user2 = generateUser({ id: 'testID' }); - const mockedChannel = generateChannelResponse({ - members: [generateMember({ user: user1 })], - messages: [ - generateMessage({ user: user2 }), - generateMessage({ type: 'system', user: undefined }), - generateMessage({ type: 'deleted', user: user1 }), - ], - }); - - const chatClient = await getTestClientWithUser({ id: 'testID' }); - useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]); - const channel = chatClient.channel('messaging', mockedChannel.id); - await channel.watch(); - - const { getByTestId, queryByTestId } = render( - - - - - - - , - ); - - await waitFor(() => { - expect(getByTestId('message-deleted')).toBeTruthy(); - expect(queryByTestId('only-visible-to-you')).toBeNull(); - }); - }); - - it('should render deleted message in the list when `deleteMessagesVisibilityType` is set to never', async () => { - const user1 = generateUser(); - const user2 = generateUser({ id: 'testID' }); - const mockedChannel = generateChannelResponse({ - members: [generateMember({ user: user1 })], - messages: [ - generateMessage({ user: user2 }), - generateMessage({ type: 'system', user: undefined }), - generateMessage({ type: 'deleted', user: user1 }), - ], - }); - - const chatClient = await getTestClientWithUser({ id: 'testID' }); - useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]); - const channel = chatClient.channel('messaging', mockedChannel.id); - await channel.watch(); - - const { queryByTestId } = render( - - - - - - - , - ); - - await waitFor(() => { - expect(queryByTestId('message-deleted')).toBeNull(); - expect(queryByTestId('only-visible-to-you')).toBeNull(); - }); - }); - - it('should render deleted message in the list', async () => { - const user1 = generateUser(); - const mockedChannel = generateChannelResponse({ - members: [generateMember({ user: user1 })], - messages: [ - generateMessage({ type: 'deleted', user: user1 }), - generateMessage({ type: 'system', user: undefined }), - generateMessage({ user: user1 }), - ], - }); - - const chatClient = await getTestClientWithUser({ id: 'testID' }); - useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]); - const channel = chatClient.channel('messaging', mockedChannel.id); - await channel.watch(); - - const { getByTestId } = render( - - - - - - - , - ); - - await waitFor(() => { - expect(getByTestId('message-deleted')).toBeTruthy(); - }); - }); - it('should render the typing indicator when typing object is non empty', async () => { const user1 = generateUser(); const mockedChannel = generateChannelResponse({ diff --git a/package/src/components/MessageList/hooks/useMessageList.ts b/package/src/components/MessageList/hooks/useMessageList.ts index 61d129238c..73dac61e24 100644 --- a/package/src/components/MessageList/hooks/useMessageList.ts +++ b/package/src/components/MessageList/hooks/useMessageList.ts @@ -2,11 +2,6 @@ import { useMemo } from 'react'; import type { LocalMessage } from 'stream-chat'; -import { useChatContext } from '../../../contexts/chatContext/ChatContext'; -import { - DeletedMessagesVisibilityType, - useMessagesContext, -} from '../../../contexts/messagesContext/MessagesContext'; import { usePaginatedMessageListContext } from '../../../contexts/paginatedMessageListContext/PaginatedMessageListContext'; import { useThreadContext } from '../../../contexts/threadContext/ThreadContext'; @@ -27,35 +22,8 @@ export type MessageGroupStyles = { [key: string]: string[]; }; -export const shouldIncludeMessageInList = ( - message: LocalMessage, - options: { deletedMessagesVisibilityType?: DeletedMessagesVisibilityType; userId?: string }, -) => { - const { deletedMessagesVisibilityType, userId } = options; - const isMessageTypeDeleted = message.type === 'deleted'; - const isSender = message.user?.id === userId; - - if (!isMessageTypeDeleted) { - return true; - } - - switch (deletedMessagesVisibilityType) { - case 'always': - return true; - case 'sender': - return isSender; - case 'receiver': - return !isSender; - case 'never': - default: - return false; - } -}; - export const useMessageList = (params: UseMessageListParams) => { const { threadList, isLiveStreaming, isFlashList = false } = params; - const { client } = useChatContext(); - const { deletedMessagesVisibilityType } = useMessagesContext(); const { messages, viewabilityChangedCallback } = usePaginatedMessageListContext(); const { threadMessages } = useThreadContext(); const messageList = threadList ? threadMessages : messages; @@ -63,14 +31,6 @@ export const useMessageList = (params: UseMessageListParams) => { const processedMessageList = useMemo(() => { const newMessageList = []; for (const message of messageList) { - if ( - !shouldIncludeMessageInList(message, { - deletedMessagesVisibilityType, - userId: client.userID, - }) - ) { - continue; - } if (isFlashList) { newMessageList.push(message); } else { @@ -78,7 +38,7 @@ export const useMessageList = (params: UseMessageListParams) => { } } return newMessageList; - }, [messageList, deletedMessagesVisibilityType, client.userID, isFlashList]); + }, [messageList, isFlashList]); const data = useRAFCoalescedValue(processedMessageList, isLiveStreaming); diff --git a/package/src/contexts/channelsContext/ChannelsContext.tsx b/package/src/contexts/channelsContext/ChannelsContext.tsx index b375a7997c..0dd3b2738e 100644 --- a/package/src/contexts/channelsContext/ChannelsContext.tsx +++ b/package/src/contexts/channelsContext/ChannelsContext.tsx @@ -51,7 +51,8 @@ export type ChannelsContextValue = { */ loadingChannels: boolean; /** - * Whether or not additional channels are being loaded, triggers the FooterLoadingIndicator + * Whether or not additional channels are being loaded, triggers the + * ChannelListFooterLoadingIndicator */ loadingNextPage: boolean; /** diff --git a/package/src/contexts/componentsContext/PLAN.md b/package/src/contexts/componentsContext/PLAN.md index d19280d2b5..bee0d5e58c 100644 --- a/package/src/contexts/componentsContext/PLAN.md +++ b/package/src/contexts/componentsContext/PLAN.md @@ -69,26 +69,26 @@ const getDefaults = () => { Some component keys differ from their default component names to avoid collisions: -| Override Key | Default Component | Why renamed | -| ----------------------------- | --------------------------------------- | ---------------------------------------------------------- | -| `FileAttachmentIcon` | `FileIcon` | Clarity | -| `ChannelListLoadingIndicator` | `ChannelListLoadingIndicator` | Split from shared `LoadingIndicator` — renders skeleton UI | -| `MessageListLoadingIndicator` | `LoadingIndicator` | Split from shared `LoadingIndicator` — renders text | -| `ChatLoadingIndicator` | `undefined` | Optional, no default | -| `ThreadMessageComposer` | `MessageComposer` | Avoid collision with `MessageComposer` component name | -| `ThreadListComponent` | `DefaultThreadListComponent` | Avoid collision with exported `ThreadList` | -| `StartAudioRecordingButton` | `AudioRecordingButton` | Historical naming | -| `Preview` | `ChannelPreviewView` | ChannelList preview item | -| `PreviewAvatar` | `ChannelAvatar` | ChannelList preview avatar | -| `FooterLoadingIndicator` | `ChannelListFooterLoadingIndicator` | ChannelList footer | -| `HeaderErrorIndicator` | `ChannelListHeaderErrorIndicator` | ChannelList header | -| `HeaderNetworkDownIndicator` | `ChannelListHeaderNetworkDownIndicator` | ChannelList header | +| Override Key | Default Component | Why renamed | +| --------------------------------------- | --------------------------------------- | ---------------------------------------------------------- | +| `FileAttachmentIcon` | `FileIcon` | Clarity | +| `ChannelListLoadingIndicator` | `ChannelListLoadingIndicator` | Split from shared `LoadingIndicator` — renders skeleton UI | +| `MessageListLoadingIndicator` | `LoadingIndicator` | Split from shared `LoadingIndicator` — renders text | +| `ChatLoadingIndicator` | `undefined` | Optional, no default | +| `ThreadMessageComposer` | `MessageComposer` | Avoid collision with `MessageComposer` component name | +| `ThreadListComponent` | `DefaultThreadListComponent` | Avoid collision with exported `ThreadList` | +| `StartAudioRecordingButton` | `AudioRecordingButton` | Historical naming | +| `ChannelPreview` | `ChannelPreviewView` | ChannelList preview item | +| `ChannelPreviewAvatar` | `ChannelAvatar` | ChannelList preview avatar | +| `ChannelListFooterLoadingIndicator` | `ChannelListFooterLoadingIndicator` | ChannelList footer | +| `ChannelListHeaderErrorIndicator` | `ChannelListHeaderErrorIndicator` | ChannelList header | +| `ChannelListHeaderNetworkDownIndicator` | `ChannelListHeaderNetworkDownIndicator` | ChannelList header | ### Optional Components (no default) These exist in `DEFAULT_COMPONENTS` as `undefined` with `React.ComponentType | undefined` type assertions: -`AttachmentPickerIOSSelectMorePhotos`, `ChatLoadingIndicator`, `CreatePollContent`, `ImageComponent`, `Input`, `ListHeaderComponent`, `MessageContentBottomView`, `MessageContentLeadingView`, `MessageContentTopView`, `MessageContentTrailingView`, `MessageLocation`, `MessageSpacer`, `MessageText`, `PollContent` +`AttachmentPickerIOSSelectMorePhotos`, `ChatLoadingIndicator`, `CreatePollContent`, `ImageComponent`, `Input`, `ListHeaderComponent`, `MessageActions`, `MessageContentBottomView`, `MessageContentLeadingView`, `MessageContentTopView`, `MessageContentTrailingView`, `MessageLocation`, `MessageSpacer`, `MessageText`, `PollContent` ### Shared Component Keys (audited) diff --git a/package/src/contexts/componentsContext/__tests__/defaultComponents.test.ts b/package/src/contexts/componentsContext/__tests__/defaultComponents.test.ts index 4742193501..10a5326532 100644 --- a/package/src/contexts/componentsContext/__tests__/defaultComponents.test.ts +++ b/package/src/contexts/componentsContext/__tests__/defaultComponents.test.ts @@ -5,6 +5,7 @@ const OPTIONAL_KEYS = new Set([ 'AttachmentPickerIOSSelectMorePhotos', 'ChatLoadingIndicator', 'CreatePollContent', + 'MessageActions', 'Input', 'ListHeaderComponent', 'MessageContentBottomView', diff --git a/package/src/contexts/componentsContext/defaultComponents.ts b/package/src/contexts/componentsContext/defaultComponents.ts index 12af4049ea..bfa5abc961 100644 --- a/package/src/contexts/componentsContext/defaultComponents.ts +++ b/package/src/contexts/componentsContext/defaultComponents.ts @@ -144,6 +144,7 @@ import { ThreadListUnreadBanner } from '../../components/ThreadList/ThreadListUn import { ThreadMessagePreviewDeliveryStatus } from '../../components/ThreadList/ThreadMessagePreviewDeliveryStatus'; import { ChannelAvatar } from '../../components/ui/Avatar/ChannelAvatar'; import { DefaultMessageOverlayBackground } from '../../contexts/overlayContext/MessageOverlayHostLayer'; +import type { MessageActionsProps } from '../../contexts/overlayContext/MessageOverlayHostLayer'; /** * All default component implementations used across the SDK. @@ -177,11 +178,11 @@ export const DEFAULT_COMPONENTS = { FileUploadNotSupportedIndicator, FileUploadRetryIndicator, FilePreview, - FooterLoadingIndicator: ChannelListFooterLoadingIndicator, + ChannelListFooterLoadingIndicator, Gallery, Giphy, - HeaderErrorIndicator: ChannelListHeaderErrorIndicator, - HeaderNetworkDownIndicator: ChannelListHeaderNetworkDownIndicator, + ChannelListHeaderErrorIndicator, + ChannelListHeaderNetworkDownIndicator, ImageAttachmentUploadPreview, ImageLoadingFailedIndicator, ImageLoadingIndicator, @@ -231,16 +232,16 @@ export const DEFAULT_COMPONENTS = { MessageUserReactionsAvatar, MessageUserReactionsItem, NetworkDownIndicator, - Preview: ChannelPreviewView, - PreviewAvatar: ChannelAvatar, - PreviewLastMessage: ChannelLastMessagePreview, - PreviewMessage: ChannelPreviewMessage, - PreviewMessageDeliveryStatus: ChannelMessagePreviewDeliveryStatus, - PreviewMutedStatus: ChannelPreviewMutedStatus, - PreviewStatus: ChannelPreviewStatus, - PreviewTitle: ChannelPreviewTitle, - PreviewTypingIndicator: ChannelPreviewTypingIndicator, - PreviewUnreadCount: ChannelPreviewUnreadCount, + ChannelPreview: ChannelPreviewView, + ChannelPreviewAvatar: ChannelAvatar, + ChannelPreviewLastMessage: ChannelLastMessagePreview, + ChannelPreviewMessage, + ChannelPreviewMessageDeliveryStatus: ChannelMessagePreviewDeliveryStatus, + ChannelPreviewMutedStatus, + ChannelPreviewStatus, + ChannelPreviewTitle, + ChannelPreviewTypingIndicator, + ChannelPreviewUnreadCount, ReactionListBottom, ReactionListClustered, ReactionListCountItem, @@ -309,6 +310,7 @@ export const DEFAULT_COMPONENTS = { ChatLoadingIndicator: undefined as React.ComponentType | null | undefined, // eslint-disable-next-line @typescript-eslint/no-explicit-any CreatePollContent: undefined as React.ComponentType | undefined, + MessageActions: undefined as React.ComponentType | undefined, // eslint-disable-next-line @typescript-eslint/no-explicit-any Input: undefined as React.ComponentType | undefined, // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/package/src/contexts/messagesContext/MessagesContext.tsx b/package/src/contexts/messagesContext/MessagesContext.tsx index 210ed7e24e..badfc58d94 100644 --- a/package/src/contexts/messagesContext/MessagesContext.tsx +++ b/package/src/contexts/messagesContext/MessagesContext.tsx @@ -39,7 +39,6 @@ export type MessageContentType = | 'ai_text' | 'text' | 'location'; -export type DeletedMessagesVisibilityType = 'always' | 'never' | 'receiver' | 'sender'; export type MessageLocationProps = { message: LocalMessage; @@ -112,14 +111,6 @@ export type MessagesContextValue = Pick void; - /** - * Full override of the delete message button in the Message Actions - * - * Please check [cookbook](https://github.com/GetStream/stream-chat-react-native/wiki/Cookbook-v3.0#override-or-intercept-message-actions-edit-delete-reaction-reply-etc) for details. - */ - /** Control if the deleted message is visible to both the send and reciever, either of them or none */ - deletedMessagesVisibilityType?: DeletedMessagesVisibilityType; - disableTypingIndicator?: boolean; /** * Enable swipe to reply on messages. diff --git a/package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx b/package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx index e4844914c1..520a0b55e9 100644 --- a/package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx +++ b/package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx @@ -4,8 +4,10 @@ import { Platform, Pressable, StyleSheet, + StyleProp, useWindowDimensions, View, + ViewStyle, } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated, { @@ -53,8 +55,15 @@ export const DefaultMessageOverlayBackground = () => { ); }; +export type MessageActionsProps = { + bottomItemStyle: StyleProp; + hostStyle: StyleProp; + portalHostStyle: StyleProp; + topItemStyle: StyleProp; +}; + export const MessageOverlayHostLayer = () => { - const { MessageOverlayBackground } = useComponentsContext(); + const { MessageActions, MessageOverlayBackground } = useComponentsContext(); const { id, closing } = useOverlayController(); const insets = useSafeAreaInsets(); const { height: screenH } = useWindowDimensions(); @@ -259,21 +268,32 @@ export const MessageOverlayHostLayer = () => { /> ) : null} - - - - - - - - - - - + {MessageActions ? ( + + ) : ( + <> + + + + + + + + + + + + + )} diff --git a/package/src/contexts/overlayContext/__tests__/MessageOverlayHostLayer.test.tsx b/package/src/contexts/overlayContext/__tests__/MessageOverlayHostLayer.test.tsx index 7facffe86d..bf826461d9 100644 --- a/package/src/contexts/overlayContext/__tests__/MessageOverlayHostLayer.test.tsx +++ b/package/src/contexts/overlayContext/__tests__/MessageOverlayHostLayer.test.tsx @@ -14,7 +14,7 @@ import { setOverlayTopH, } from '../../../state-store'; import { WithComponents } from '../../componentsContext/ComponentsContext'; -import { MessageOverlayHostLayer } from '../MessageOverlayHostLayer'; +import { MessageActionsProps, MessageOverlayHostLayer } from '../MessageOverlayHostLayer'; jest.mock('react-native', () => { const actual = jest.requireActual('react-native'); @@ -93,6 +93,18 @@ const TOP_RECT = { h: 20, w: 90, x: 5, y: 0 }; const MESSAGE_RECT = { h: 50, w: 180, x: 10, y: 0 }; const BOTTOM_RECT = { h: 30, w: 140, x: 20, y: 100 }; const NoopBackground = () => null; +const CustomMessageActions = ({ + bottomItemStyle, + hostStyle, + topItemStyle, +}: MessageActionsProps) => ( + <> + Custom + + + + +); const flushAnimationFrameQueue = () => { act(() => { @@ -260,4 +272,50 @@ describe('MessageOverlayHostLayer', () => { height: 0, }); }); + + it('renders MessageActions override instead of the default host wrappers when provided', () => { + const renderTree = () => ( + + + + ); + const { rerender } = render(renderTree()); + + act(() => { + setOverlayTopH(TOP_RECT); + setOverlayMessageH(MESSAGE_RECT); + setOverlayBottomH(BOTTOM_RECT); + openOverlay('message-1'); + }); + + rerender(renderTree()); + + expect(screen.getByTestId('custom-message-actions')).toBeTruthy(); + expect(screen.queryByTestId('message-overlay-top')).toBeNull(); + expect(screen.queryByTestId('message-overlay-message')).toBeNull(); + expect(screen.queryByTestId('message-overlay-bottom')).toBeNull(); + expect( + StyleSheet.flatten(screen.getByTestId('custom-message-actions-top').props.style), + ).toMatchObject({ + height: TOP_RECT.h, + width: TOP_RECT.w, + }); + expect( + StyleSheet.flatten(screen.getByTestId('custom-message-actions-message').props.style), + ).toMatchObject({ + height: MESSAGE_RECT.h, + width: MESSAGE_RECT.w, + }); + expect( + StyleSheet.flatten(screen.getByTestId('custom-message-actions-bottom').props.style), + ).toMatchObject({ + height: BOTTOM_RECT.h, + width: BOTTOM_RECT.w, + }); + }); });