From 2badc51fc977768acc67346d3d0f59cddc64fa30 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Wed, 8 Apr 2026 15:44:45 +0200 Subject: [PATCH 01/16] refactor!: introduce WithComponents context provider --- package/foobar.db-journal | Bin 16928 -> 0 bytes .../components/Attachment/FileAttachment.tsx | 12 +- .../Attachment/FileAttachmentGroup.tsx | 28 +- .../src/components/Attachment/FilePreview.tsx | 13 +- package/src/components/Attachment/Gallery.tsx | 89 +--- .../Attachment/Giphy/GiphyImage.tsx | 32 +- .../Attachment/UnsupportedAttachment.tsx | 15 +- .../AttachmentPicker/AttachmentPicker.tsx | 4 +- .../AttachmentPickerItem.tsx | 9 +- .../AutoCompleteSuggestionList.tsx | 24 +- package/src/components/Channel/Channel.tsx | 469 ++---------------- .../isAttachmentEqualHandler.test.js | 21 +- .../Channel/hooks/useCreateChannelContext.ts | 8 - .../useCreateInputMessageInputContext.ts | 82 +-- .../Channel/hooks/useCreateMessagesContext.ts | 130 ----- .../components/ChannelList/ChannelList.tsx | 79 +-- .../ChannelListLoadingIndicator.tsx | 4 +- .../ChannelList/ChannelListView.tsx | 39 +- .../hooks/useCreateChannelsContext.ts | 39 -- .../ChannelPreview/ChannelPreview.tsx | 14 +- .../ChannelPreview/ChannelPreviewMessage.tsx | 40 +- .../ChannelPreview/ChannelPreviewView.tsx | 51 +- package/src/components/Message/Message.tsx | 33 +- .../Message/MessageItemView/MessageBubble.tsx | 5 +- .../MessageItemView/MessageContent.tsx | 61 +-- .../MessageItemView/MessageDeleted.tsx | 12 +- .../Message/MessageItemView/MessageFooter.tsx | 16 +- .../Message/MessageItemView/MessageHeader.tsx | 40 +- .../MessageItemView/MessageItemView.tsx | 50 +- .../MessageItemView/MessageReplies.tsx | 10 +- .../MessageItemView/MessageTextContainer.tsx | 12 +- .../MessageItemView/MessageWrapper.tsx | 13 +- .../ReactionList/ReactionListBottom.tsx | 16 +- .../ReactionList/ReactionListTop.tsx | 24 +- .../__tests__/MessageContent.test.js | 77 +-- .../__tests__/MessageItemView.test.js | 23 +- .../__tests__/MessageTextContainer.test.tsx | 1 + .../MessageInput/MessageComposer.tsx | 92 +--- .../MessageInput/MessageInputHeaderView.tsx | 6 +- .../AttachmentUploadPreviewList.tsx | 42 +- .../FileAttachmentUploadPreview.tsx | 4 +- .../ImageAttachmentUploadPreview.tsx | 4 +- .../AudioRecordingInProgress.tsx | 10 +- .../components/InputButtons/index.tsx | 9 +- .../components/OutputButtons/index.tsx | 27 +- .../MessageList/MessageFlashList.tsx | 69 +-- .../components/MessageList/MessageList.tsx | 71 +-- .../components/MessageList/StickyHeader.tsx | 4 +- .../MessageMenu/MessageActionList.tsx | 7 +- .../components/MessageMenu/MessageMenu.tsx | 4 +- .../MessageMenu/MessageUserReactions.tsx | 34 +- .../MessageMenu/MessageUserReactionsItem.tsx | 9 +- .../__tests__/MessageUserReactions.test.tsx | 1 + .../src/components/Poll/CreatePollContent.tsx | 7 +- package/src/components/Poll/Poll.tsx | 28 +- package/src/components/Thread/Thread.tsx | 10 +- .../components/ThreadFooterComponent.tsx | 21 +- .../AttachmentPickerContext.tsx | 15 - .../channelContext/ChannelContext.tsx | 24 - .../channelsContext/ChannelsContext.tsx | 113 ----- .../componentsContext/ComponentsContext.tsx | 297 +++++++++++ .../src/contexts/componentsContext/PLAN.md | 180 +++++++ .../componentsContext/defaultComponents.ts | 0 .../MessageInputContext.tsx | 185 ------- .../hooks/useCreateMessageInputContext.ts | 76 +-- .../messagesContext/MessagesContext.tsx | 361 +------------- 66 files changed, 869 insertions(+), 2436 deletions(-) delete mode 100644 package/foobar.db-journal create mode 100644 package/src/contexts/componentsContext/ComponentsContext.tsx create mode 100644 package/src/contexts/componentsContext/PLAN.md create mode 100644 package/src/contexts/componentsContext/defaultComponents.ts diff --git a/package/foobar.db-journal b/package/foobar.db-journal deleted file mode 100644 index 334d055043d2fadf0d95cd22e83d67f9a3399004..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16928 zcmeHO%WoUU87C!+q)18hOI};D;*Ap5W~_<#y|WGiR1aEJqem>0K->ZqW_M;SrZg$@ zVaE*`)Xt-qUfLXsUWx*}6$px=J+z0Sr}kD9D0<7GK!N-LEehn&e*4stk?d)}UXrCP z?(EL!H{bXBUh@fLCUf{&TwnVa+;uaVF}R#OoJdDsWCj+Bpxsh1tSK1*d3EelmlmKc4duV;yFNmOANFl`z6w5Mobro4U0BO4BB5Yv|sZ_Tmmnya{YZtiDbO&SAkW|DJ zVj<14HDnWoB3s6i>gvptnQ`7nvt(CQbkl%&ogs}#1lf`%A!f)#GXTY;GA2w`m`RYS zNgx}uEM%!zL9Q-Iw&}{i4;Fkh*Hu)>axu~^10zjkfUCI{qK@kr7SnWx*??@ClBy$v z7%tKjQ$v;nI$N%VsZLxb$<8?+O>-sYNr4R8g_Rn{$TlsCOcPVdG%eF{44vpQvuy`S zn1K~2Fo+#TMZ{*9VU;igtFu0u!(2(nU``to6SR;)He0ojDqEJu00?7;N7J+1Xgso= z@zD&!U@E~nvY7_K$&02vks13*uQ;WFFb3eKg%7hHM)IVb}KZgDxS9UMn*DVluXQG%W?Z>6#{~wu=nQ^AKuE$aWwJn3{zxhnc485*|&9 z+-N+qP5Nj|atL)4(2r0FX*N)XU1}p&wutOPfDp~)k*&au#v@yv8%p+T;K zOfDfpEQktfOD3c~$01hEN7HO7n;K+7-G#8$z$6GPtRe+spF$>s%!zqqo8U&{k?lmF zDOMDsswOhA207nmIB8-=8T@|8}}j z{zdtlkkU{`Cs@B|tJQqNYsz3K2X9LJlHR&GYS$;^KPThk%6F`otUH2eQ2au!c z**(Zp{E$3{ZV&Qg06D7e-GeOiL-I;F6F`otkN2dU4j@O>%zKbip_C))=sn0%$mNLI zdJnS556Nqu$pCUx4ZbI3fgh4r$~-?LPm}3MnF}CC)$eDF%Yik|}0QGH*!uhOWOXWBiS`7yDh-*=x|n z8;d-juUq^s&nNY~9-j*>_#R01_N~oEkBM%(bC2{y6}}OY&1SwTikXYGIZz$GCz3e< zKVQp?{Y(tUaPePRc%KQ!M_)XQmp^!E0%o)2PrWsB*^; z(Qb9)!TjoOb-7j*Ys)K}RWW{iwJ63fXP>lMthw5tVy*f{P2AprpZezJQqk!!(qnYF z=TF4TuC3hpR#BcNu=)mrt zONCn|R=vKvv9-K=M|`b%N4$Ed%90r4R+fr4cXq2A>)XL1Z{kWNC%iJ7%`{q+z16+n z1V7m${a)L@-#chGn|reNe*Uq2B`;jQoP9U%6MO)wHd=RUr2Fk|a(r%p=H$!~Ovk*1 z;?^D=utA1n#=#~V+qLR?byrL(x?HdAY;41xTh;B_QZZIFL^&Xjnr#wUr?(Ggr17S` zXt&q}D)q1Agvvs;>uVoS5JGmN>)BAjjqSDS8{*&^ac4VXyL;7X(51jqm3LoyF)u7E zWIwp#a~DBH*V6->MNgA_kzS8D`}bI@*NylALZ0EIlr>q>X~X-Q?amexS9W$btIONs zTJ`2~eX}OsT;9B01qXhs*CA`9N5XBwU1DZyu%JHVj=qz`3xQ(Ci@fztyMGW#+ubJ} zM)#tq3&DE;7AO%(aplTVF*eIm@hOhKR7|?T)dn4QZlJ#cp?9;oTisr*-WEd_^X*dc z47FC>tU^9mUB11#yjDH*f$NuZ!s^0t5fNEuPmV@sZoQBbuFPkHkck}Pp3OSoGydh& zkuKv;1s>xM2OQ@5^Lb%@KKp?lI!qF-JmfCHZ^sIVBI!8$jR%QzWqWyz9%u8Anca3X z4!&nQSEKa+QpK|4u!E$J1&`ycmWsiHUG^q(2G(`A+LU!juic5nS}GniJTc>t?;HvQ z{zKajSDvZ{zq5FeDTbjrloCI;*Pz9musnaf*Z4;AoW>uVq+(vUb}9R~l*lwubR5>K znIjx0Ugfz?e3MN0p<`~)%9HkUPcMp71Fm$4mT;!SO7g6H6BfxYKvDFo@Idy6g566r z9`VN|5BX9~s9rkG4Pt`#RLF7dZJYrP{p>dgI$S%4 zW|L(!IrXeysLbVrSB0k|d{R%uIq9JB}Kw>BehXTKHE+_0%j`R3I z31YFsUB{u{EqFce>t}PqwQJcsz7Ana)@AqmtmUww+t~pvh9 & - Pick & { + Pick, 'FilePreview'> & + Pick & { /** The attachment to render */ attachment: Attachment; attachmentIconSize?: FileIconProps['size']; @@ -101,9 +106,10 @@ export type FileAttachmentProps = Partial { const { FilePreview: PropFilePreview } = props; const { onLongPress, onPress, onPressIn, preventPress } = useMessageContext(); - const { additionalPressableProps, FilePreview: ContextFilePreview } = useMessagesContext(); + const { FilePreview: ComponentsFilePreview } = useComponentsContext(); + const { additionalPressableProps } = useMessagesContext(); - const FilePreview = PropFilePreview || ContextFilePreview; + const FilePreview = PropFilePreview || ComponentsFilePreview; return ( & - Pick & { - styles?: Partial<{ - attachmentContainer: StyleProp; - container: StyleProp; - }>; - }; +export type FileAttachmentGroupPropsWithContext = Pick & { + styles?: Partial<{ + attachmentContainer: StyleProp; + container: StyleProp; + }>; +}; const FileAttachmentGroupWithContext = (props: FileAttachmentGroupPropsWithContext) => { - const { Attachment, files, message, styles: stylesProp = {} } = props; + const { files, message, styles: stylesProp = {} } = props; + const { Attachment } = useComponentsContext(); const { theme: { @@ -75,8 +69,6 @@ export const FileAttachmentGroup = (props: FileAttachmentGroupProps) => { const { files: contextFiles, message } = useMessageContext(); - const { Attachment = AttachmentDefault, AudioAttachment } = useMessagesContext(); - const files = propFiles || contextFiles; if (!files.length) { @@ -86,8 +78,6 @@ export const FileAttachmentGroup = (props: FileAttachmentGroupProps) => { return ( > & { +export type FilePreviewProps = { /** The attachment to render */ attachment: Attachment; attachmentIconSize?: FileIconProps['size']; @@ -30,14 +27,12 @@ export type FilePreviewProps = Partial { const { attachment, - FileAttachmentIcon: PropFileAttachmentIcon, attachmentIconSize, styles: stylesProp = {}, titleNumberOfLines = 2, indicator, } = props; - const { FileAttachmentIcon: ContextFileAttachmentIcon } = useMessagesContext(); - const FileAttachmentIcon = PropFileAttachmentIcon || ContextFileAttachmentIcon || FileIconDefault; + const { FileAttachmentIcon } = useComponentsContext(); const styles = useStyles(); diff --git a/package/src/components/Attachment/Gallery.tsx b/package/src/components/Attachment/Gallery.tsx index 9d50977b03..0b212d9801 100644 --- a/package/src/components/Attachment/Gallery.tsx +++ b/package/src/components/Attachment/Gallery.tsx @@ -16,6 +16,7 @@ import { openUrlSafely } from './utils/openUrlSafely'; import { useTranslationContext } from '../../contexts'; import { useChatConfigContext } from '../../contexts/chatConfigContext/ChatConfigContext'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { ImageGalleryContextValue, useImageGalleryContext, @@ -54,14 +55,7 @@ export type GalleryPropsWithContext = Pick & - Pick< - MessagesContextValue, - | 'additionalPressableProps' - | 'VideoThumbnail' - | 'ImageLoadingIndicator' - | 'ImageLoadingFailedIndicator' - | 'myMessageTheme' - > & + Pick & Pick & { channelId: string | undefined; messageHasOnlyOneMedia: boolean; @@ -72,8 +66,6 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => { additionalPressableProps, alignment, imageGalleryStateStore, - ImageLoadingFailedIndicator, - ImageLoadingIndicator, images, message, onLongPress, @@ -82,10 +74,8 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => { preventPress, setOverlay, videos, - VideoThumbnail, messageHasOnlyOneMedia = false, } = props; - const { resizableCDNHosts } = useChatConfigContext(); const { theme: { @@ -103,9 +93,7 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => { }, }, } = useTheme(); - const styles = useStyles(); - const sizeConfig = { gridHeight, gridWidth, @@ -114,12 +102,10 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => { minHeight, minWidth, }; - const imagesAndVideos = [...(images || []), ...(videos || [])]; const imagesAndVideosValue = `${images?.length}${videos?.length}${images ?.map((i) => `${i.image_url}${i.thumb_url}`) .join('')}${videos?.map((i) => `${i.image_url}${i.thumb_url}`).join('')}`; - const { height, invertedDirections, thumbnailGrid, width } = useMemo( () => buildGallery({ @@ -130,12 +116,10 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => { // eslint-disable-next-line react-hooks/exhaustive-deps [imagesAndVideosValue], ); - if (!imagesAndVideos?.length) { return null; } const numOfColumns = thumbnailGrid.length; - return ( { width, messageHasOnlyOneMedia, }); - if (!message) { return null; } - return ( { rowIndex={rowIndex} setOverlay={setOverlay} thumbnail={thumbnail} - VideoThumbnail={VideoThumbnail} /> ); })} @@ -216,7 +195,6 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => { ); }; - type GalleryThumbnailProps = { borderRadius: GalleryImageBorderRadius; colIndex: number; @@ -227,24 +205,15 @@ type GalleryThumbnailProps = { numOfRows: number; rowIndex: number; thumbnail: Thumbnail; -} & Pick< - MessagesContextValue, - | 'additionalPressableProps' - | 'VideoThumbnail' - | 'ImageLoadingIndicator' - | 'ImageLoadingFailedIndicator' -> & +} & Pick & Pick & Pick & Pick; - const GalleryThumbnail = ({ additionalPressableProps, borderRadius, colIndex, imageGalleryStateStore, - ImageLoadingFailedIndicator, - ImageLoadingIndicator, imagesAndVideos, invertedDirections, message, @@ -257,8 +226,8 @@ const GalleryThumbnail = ({ rowIndex, setOverlay, thumbnail, - VideoThumbnail, }: GalleryThumbnailProps) => { + const { VideoThumbnail } = useComponentsContext(); const { theme: { messageItemView: { @@ -269,7 +238,6 @@ const GalleryThumbnail = ({ } = useTheme(); const { t } = useTranslationContext(); const styles = useStyles(); - const openImageViewer = () => { if (!message) { return; @@ -280,7 +248,6 @@ const GalleryThumbnail = ({ }); setOverlay('gallery'); }; - const defaultOnPress = () => { // If the url is defined then only try to open the file. if (thumbnail.url) { @@ -293,7 +260,6 @@ const GalleryThumbnail = ({ } } }; - return ( )} @@ -362,16 +326,11 @@ const GalleryThumbnail = ({ ); }; - const GalleryImageThumbnail = ({ borderRadius, - ImageLoadingFailedIndicator, - ImageLoadingIndicator, thumbnail, -}: Pick< - GalleryThumbnailProps, - 'ImageLoadingFailedIndicator' | 'ImageLoadingIndicator' | 'thumbnail' | 'borderRadius' ->) => { +}: Pick) => { + const { ImageLoadingFailedIndicator, ImageLoadingIndicator } = useComponentsContext(); const { isLoadingImage, isLoadingImageError, @@ -379,33 +338,27 @@ const GalleryImageThumbnail = ({ setLoadingImage, setLoadingImageError, } = useLoadingImage(); - const { theme: { messageItemView: { gallery }, }, } = useTheme(); - const styles = useStyles(); - const onLoadStart = useStableCallback(() => { setLoadingImageError(false); setLoadingImage(true); }); - const onLoad = useStableCallback(() => { setTimeout(() => { setLoadingImage(false); setLoadingImageError(false); }, 0); }); - const onError = useStableCallback(({ nativeEvent: { error } }: ImageErrorEvent) => { console.warn(error); setLoadingImage(false); setLoadingImageError(true); }); - return ( {isLoadingImageError ? ( @@ -426,7 +379,6 @@ const GalleryImageThumbnail = ({ ); }; - const areEqual = (prevProps: GalleryPropsWithContext, nextProps: GalleryPropsWithContext) => { const { alignment: prevAlignment, @@ -442,19 +394,16 @@ const areEqual = (prevProps: GalleryPropsWithContext, nextProps: GalleryPropsWit myMessageTheme: nextMyMessageTheme, videos: nextVideos, } = nextProps; - const alignmentEqual = prevAlignment === nextAlignment; if (!alignmentEqual) { return false; } - const messageEqual = prevMessage?.id === nextMessage?.id && `${prevMessage?.updated_at}` === `${nextMessage?.updated_at}`; if (!messageEqual) { return false; } - const imagesEqual = prevImages.length === nextImages.length && prevImages.every( @@ -465,7 +414,6 @@ const areEqual = (prevProps: GalleryPropsWithContext, nextProps: GalleryPropsWit if (!imagesEqual) { return false; } - const videosEqual = prevVideos.length === nextVideos.length && prevVideos.every( @@ -476,20 +424,15 @@ const areEqual = (prevProps: GalleryPropsWithContext, nextProps: GalleryPropsWit if (!videosEqual) { return false; } - const messageThemeEqual = JSON.stringify(prevMyMessageTheme) === JSON.stringify(nextMyMessageTheme); if (!messageThemeEqual) { return false; } - return true; }; - const MemoizedGallery = React.memo(GalleryWithContext, areEqual) as typeof GalleryWithContext; - export type GalleryProps = Partial; - /** * UI component for card in attachments. */ @@ -497,8 +440,6 @@ export const Gallery = (props: GalleryProps) => { const { alignment: propAlignment, additionalPressableProps: propAdditionalPressableProps, - ImageLoadingFailedIndicator: PropImageLoadingFailedIndicator, - ImageLoadingIndicator: PropImageLoadingIndicator, images: propImages, message: propMessage, myMessageTheme: propMyMessageTheme, @@ -508,10 +449,8 @@ export const Gallery = (props: GalleryProps) => { preventPress: propPreventPress, setOverlay: propSetOverlay, videos: propVideos, - VideoThumbnail: PropVideoThumbnail, messageContentOrder: propMessageContentOrder, } = props; - const { imageGalleryStateStore } = useImageGalleryContext(); const { alignment: contextAlignment, @@ -526,13 +465,9 @@ export const Gallery = (props: GalleryProps) => { } = useMessageContext(); const { additionalPressableProps: contextAdditionalPressableProps, - ImageLoadingFailedIndicator: ContextImageLoadingFailedIndicator, - ImageLoadingIndicator: ContextImageLoadingIndicator, myMessageTheme: contextMyMessageTheme, - VideoThumbnail: ContextVideoThumnbnail, } = useMessagesContext(); const { setOverlay: contextSetOverlay } = useOverlayContext(); - const images = propImages ?? contextImages ?? []; const videos = propVideos ?? contextVideos ?? []; const imagesAndVideos = [...images, ...videos]; @@ -541,7 +476,6 @@ export const Gallery = (props: GalleryProps) => { if (!images.length && !videos.length) { return null; } - const additionalPressableProps = propAdditionalPressableProps || contextAdditionalPressableProps; const onLongPress = propOnLongPress || contextOnLongPress; const onPressIn = propOnPressIn || contextOnPressIn; @@ -549,18 +483,12 @@ export const Gallery = (props: GalleryProps) => { const preventPress = typeof propPreventPress === 'boolean' ? propPreventPress : contextPreventPress; const setOverlay = propSetOverlay || contextSetOverlay; - const VideoThumbnail = PropVideoThumbnail || ContextVideoThumnbnail; - const ImageLoadingFailedIndicator = - PropImageLoadingFailedIndicator || ContextImageLoadingFailedIndicator; - const ImageLoadingIndicator = PropImageLoadingIndicator || ContextImageLoadingIndicator; const myMessageTheme = propMyMessageTheme || contextMyMessageTheme; const messageContentOrder = propMessageContentOrder || contextMessageContentOrder; - const messageHasOnlyOneMedia = messageContentOrder?.length === 1 && messageContentOrder?.includes('gallery') && imagesAndVideos.length === 1; - return ( { alignment, channelId: message?.cid, imageGalleryStateStore, - ImageLoadingFailedIndicator, - ImageLoadingIndicator, images, message, myMessageTheme, @@ -579,14 +505,12 @@ export const Gallery = (props: GalleryProps) => { preventPress, setOverlay, videos, - VideoThumbnail, messageHasOnlyOneMedia, messageContentOrder, }} /> ); }; - const useStyles = () => { const { theme: { semantics }, @@ -659,5 +583,4 @@ const useStyles = () => { }); }, [semantics, isMyMessage]); }; - Gallery.displayName = 'Gallery{messageItemView{gallery}}'; diff --git a/package/src/components/Attachment/Giphy/GiphyImage.tsx b/package/src/components/Attachment/Giphy/GiphyImage.tsx index 1cf9a650a8..6b69f4738e 100644 --- a/package/src/components/Attachment/Giphy/GiphyImage.tsx +++ b/package/src/components/Attachment/Giphy/GiphyImage.tsx @@ -4,6 +4,7 @@ import { Image, StyleSheet, View } from 'react-native'; import type { Attachment } from 'stream-chat'; import { ChatContextValue, useChatContext } from '../../../contexts/chatContext/ChatContext'; +import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext'; import { MessagesContextValue, @@ -16,10 +17,7 @@ import { makeImageCompatibleUrl } from '../../../utils/utils'; import { GiphyBadge } from '../../ui/Badge/GiphyBadge'; export type GiphyImagePropsWithContext = Pick & - Pick< - MessagesContextValue, - 'giphyVersion' | 'ImageLoadingIndicator' | 'ImageLoadingFailedIndicator' - > & { + Pick & { attachment: Attachment; /** * Whether to render the preview image or the full image @@ -28,14 +26,8 @@ export type GiphyImagePropsWithContext = Pick { - const { - attachment, - giphyVersion, - ImageComponent = Image, - ImageLoadingFailedIndicator, - ImageLoadingIndicator, - preview = false, - } = props; + const { attachment, giphyVersion, ImageComponent = Image, preview = false } = props; + const { ImageLoadingFailedIndicator, ImageLoadingIndicator } = useComponentsContext(); const { giphy: giphyData, image_url, thumb_url, type } = attachment; @@ -167,22 +159,8 @@ export const GiphyImage = (props: GiphyImageProps) => { const { ImageComponent } = useChatContext(); const { giphyVersion } = useMessagesContext(); - const { - ImageLoadingFailedIndicator: ContextImageLoadingFailedIndicator, - ImageLoadingIndicator: ContextImageLoadingIndicator, - } = useMessagesContext(); - const ImageLoadingFailedIndicator = - ContextImageLoadingFailedIndicator || props.ImageLoadingFailedIndicator; - const ImageLoadingIndicator = ContextImageLoadingIndicator || props.ImageLoadingIndicator; - return ( - + ); }; diff --git a/package/src/components/Attachment/UnsupportedAttachment.tsx b/package/src/components/Attachment/UnsupportedAttachment.tsx index a4ed9361bc..b3fb8706f1 100644 --- a/package/src/components/Attachment/UnsupportedAttachment.tsx +++ b/package/src/components/Attachment/UnsupportedAttachment.tsx @@ -3,32 +3,27 @@ import { StyleSheet, Text, View } from 'react-native'; import type { Attachment } from 'stream-chat'; -import { FileIconProps } from './FileIcon'; +import type { FileIconProps } from './FileIcon'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { MessageContextValue, useMessageContext, } from '../../contexts/messageContext/MessageContext'; -import { - MessagesContextValue, - useMessagesContext, -} from '../../contexts/messagesContext/MessagesContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { useTranslationContext } from '../../contexts/translationContext/TranslationContext'; import { primitives } from '../../theme'; -export type UnsupportedAttachmentProps = Partial< - Pick & Pick -> & { +export type UnsupportedAttachmentProps = Partial> & { /** The attachment to render */ attachment: Attachment; attachmentIconSize?: FileIconProps['size']; }; export const UnsupportedAttachment = (props: UnsupportedAttachmentProps) => { - const { FileAttachmentIcon: FileAttachmentIconDefault } = useMessagesContext(); + const { FileAttachmentIcon } = useComponentsContext(); const { isMyMessage } = useMessageContext(); - const { attachment, attachmentIconSize, FileAttachmentIcon = FileAttachmentIconDefault } = props; + const { attachment, attachmentIconSize } = props; const styles = useStyles({ isMyMessage }); diff --git a/package/src/components/AttachmentPicker/AttachmentPicker.tsx b/package/src/components/AttachmentPicker/AttachmentPicker.tsx index 5ddf942875..19022bbc90 100644 --- a/package/src/components/AttachmentPicker/AttachmentPicker.tsx +++ b/package/src/components/AttachmentPicker/AttachmentPicker.tsx @@ -15,6 +15,7 @@ import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; import { useAttachmentPickerContext } from '../../contexts/attachmentPickerContext/AttachmentPickerContext'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { useStableCallback } from '../../hooks'; import { BottomSheet } from '../BottomSheetCompatibility/BottomSheet'; @@ -35,12 +36,11 @@ export const AttachmentPicker = () => { const { closePicker, attachmentPickerStore, - AttachmentPickerSelectionBar, - AttachmentPickerContent, attachmentPickerBottomSheetHeight, bottomSheetRef: ref, disableAttachmentPicker, } = useAttachmentPickerContext(); + const { AttachmentPickerContent, AttachmentPickerSelectionBar } = useComponentsContext(); const { theme: { semantics }, } = useTheme(); diff --git a/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.tsx b/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.tsx index f7e0169a2d..1998a3fb6a 100644 --- a/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.tsx +++ b/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.tsx @@ -7,6 +7,7 @@ import { FileReference, isLocalImageAttachment, isLocalVideoAttachment } from 's import { isIosLimited, type PhotoContentItemType } from './shared'; import { useAttachmentPickerContext } from '../../../../contexts'; +import { useComponentsContext } from '../../../../contexts/componentsContext/ComponentsContext'; import { useAttachmentManagerState } from '../../../../contexts/messageInputContext/hooks/useAttachmentManagerState'; import { useMessageComposer } from '../../../../contexts/messageInputContext/hooks/useMessageComposer'; import { useMessageInputContext } from '../../../../contexts/messageInputContext/MessageInputContext'; @@ -26,8 +27,8 @@ type AttachmentPickerItemType = { const AttachmentVideo = (props: AttachmentPickerItemType) => { const { asset } = props; - const { numberOfAttachmentPickerImageColumns, ImageOverlaySelectedComponent } = - useAttachmentPickerContext(); + const { numberOfAttachmentPickerImageColumns } = useAttachmentPickerContext(); + const { ImageOverlaySelectedComponent } = useComponentsContext(); const { vw } = useViewport(); const { t } = useTranslationContext(); const messageComposer = useMessageComposer(); @@ -90,8 +91,8 @@ const AttachmentVideo = (props: AttachmentPickerItemType) => { const AttachmentImage = (props: AttachmentPickerItemType) => { const { asset } = props; - const { numberOfAttachmentPickerImageColumns, ImageOverlaySelectedComponent } = - useAttachmentPickerContext(); + const { numberOfAttachmentPickerImageColumns } = useAttachmentPickerContext(); + const { ImageOverlaySelectedComponent } = useComponentsContext(); const { theme: { attachmentPicker: { image, imageOverlay }, diff --git a/package/src/components/AutoCompleteInput/AutoCompleteSuggestionList.tsx b/package/src/components/AutoCompleteInput/AutoCompleteSuggestionList.tsx index 07988eaa55..6bd58e8d5a 100644 --- a/package/src/components/AutoCompleteInput/AutoCompleteSuggestionList.tsx +++ b/package/src/components/AutoCompleteInput/AutoCompleteSuggestionList.tsx @@ -5,20 +5,15 @@ import Animated, { LinearTransition, ZoomIn, ZoomOut } from 'react-native-reanim import { SearchSourceState, TextComposerState, TextComposerSuggestion } from 'stream-chat'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { useMessageComposer } from '../../contexts/messageInputContext/hooks/useMessageComposer'; -import { - MessageInputContextValue, - useMessageInputContext, -} from '../../contexts/messageInputContext/MessageInputContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { useStableCallback } from '../../hooks'; import { useStateStore } from '../../hooks/useStateStore'; export const DEFAULT_LIST_HEIGHT = 208; -export type AutoCompleteSuggestionListProps = Partial< - Pick ->; +export type AutoCompleteSuggestionListProps = Record; const textComposerStateSelector = (state: TextComposerState) => ({ suggestions: state.suggestions, @@ -29,19 +24,8 @@ const searchSourceStateSelector = (nextValue: SearchSourceState) => ({ items: nextValue.items, }); -export const AutoCompleteSuggestionList = ({ - AutoCompleteSuggestionHeader: propsAutoCompleteSuggestionHeader, - AutoCompleteSuggestionItem: propsAutoCompleteSuggestionItem, -}: AutoCompleteSuggestionListProps) => { - const { - AutoCompleteSuggestionHeader: contextAutoCompleteSuggestionHeader, - AutoCompleteSuggestionItem: contextAutoCompleteSuggestionItem, - } = useMessageInputContext(); - - const AutoCompleteSuggestionHeader = - propsAutoCompleteSuggestionHeader ?? contextAutoCompleteSuggestionHeader; - const AutoCompleteSuggestionItem = - propsAutoCompleteSuggestionItem ?? contextAutoCompleteSuggestionItem; +export const AutoCompleteSuggestionList = () => { + const { AutoCompleteSuggestionHeader, AutoCompleteSuggestionItem } = useComponentsContext(); const messageComposer = useMessageComposer(); const { textComposer } = messageComposer; diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 39f271402b..74eaa238d8 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -54,6 +54,7 @@ import { ChannelContextValue, ChannelProvider } from '../../contexts/channelCont import type { UseChannelStateValue } from '../../contexts/channelsStateContext/useChannelState'; import { useChannelState } from '../../contexts/channelsStateContext/useChannelState'; import { ChatContextValue, useChatContext } from '../../contexts/chatContext/ChatContext'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { MessageComposerProvider } from '../../contexts/messageComposerContext/MessageComposerContext'; import { MessageContextValue } from '../../contexts/messageContext/MessageContext'; import { @@ -111,116 +112,11 @@ import { MessageStatusTypes, ReactionData, } from '../../utils/utils'; -import { Attachment as AttachmentDefault } from '../Attachment/Attachment'; -import { AudioAttachment as AudioAttachmentDefault } from '../Attachment/Audio'; -import { FileAttachment as FileAttachmentDefault } from '../Attachment/FileAttachment'; -import { FileAttachmentGroup as FileAttachmentGroupDefault } from '../Attachment/FileAttachmentGroup'; -import { FileIcon as FileIconDefault } from '../Attachment/FileIcon'; -import { FilePreview as FilePreviewDefault } from '../Attachment/FilePreview'; -import { Gallery as GalleryDefault } from '../Attachment/Gallery'; -import { Giphy as GiphyDefault } from '../Attachment/Giphy'; -import { ImageLoadingFailedIndicator as ImageLoadingFailedIndicatorDefault } from '../Attachment/ImageLoadingFailedIndicator'; -import { ImageLoadingIndicator as ImageLoadingIndicatorDefault } from '../Attachment/ImageLoadingIndicator'; -import { UnsupportedAttachment as UnsupportedAttachmentDefault } from '../Attachment/UnsupportedAttachment'; -import { URLPreview as URLPreviewDefault } from '../Attachment/UrlPreview'; -import { URLPreviewCompact as URLPreviewCompactDefault } from '../Attachment/UrlPreview/URLPreviewCompact'; -import { VideoThumbnail as VideoThumbnailDefault } from '../Attachment/VideoThumbnail'; import { AttachmentPicker } from '../AttachmentPicker/AttachmentPicker'; -import { AttachmentPickerContent as DefaultAttachmentPickerContent } from '../AttachmentPicker/components/AttachmentPickerContent'; -import { AttachmentPickerSelectionBar as DefaultAttachmentPickerSelectionBar } from '../AttachmentPicker/components/AttachmentPickerSelectionBar'; -import { ImageOverlaySelectedComponent as DefaultImageOverlaySelectedComponent } from '../AttachmentPicker/components/ImageOverlaySelectedComponent'; -import { AutoCompleteSuggestionHeader as AutoCompleteSuggestionHeaderDefault } from '../AutoCompleteInput/AutoCompleteSuggestionHeader'; -import { AutoCompleteSuggestionItem as AutoCompleteSuggestionItemDefault } from '../AutoCompleteInput/AutoCompleteSuggestionItem'; -import { AutoCompleteSuggestionList as AutoCompleteSuggestionListDefault } from '../AutoCompleteInput/AutoCompleteSuggestionList'; -import { InputView as InputViewDefault } from '../AutoCompleteInput/InputView'; -import { EmptyStateIndicator as EmptyStateIndicatorDefault } from '../Indicators/EmptyStateIndicator'; -import { - LoadingErrorIndicator as LoadingErrorIndicatorDefault, - LoadingErrorProps, -} from '../Indicators/LoadingErrorIndicator'; -import { LoadingIndicator as LoadingIndicatorDefault } from '../Indicators/LoadingIndicator'; -import { - KeyboardCompatibleView as KeyboardCompatibleViewDefault, - KeyboardCompatibleViewProps, -} from '../KeyboardCompatibleView/KeyboardControllerAvoidingView'; -import { Message as MessageDefault } from '../Message/Message'; -import { MessagePinnedHeader as MessagePinnedHeaderDefault } from '../Message/MessageItemView/Headers/MessagePinnedHeader'; -import { MessageReminderHeader as MessageReminderHeaderDefault } from '../Message/MessageItemView/Headers/MessageReminderHeader'; -import { MessageSavedForLaterHeader as MessageSavedForLaterHeaderDefault } from '../Message/MessageItemView/Headers/MessageSavedForLaterHeader'; -import { SentToChannelHeader as SentToChannelHeaderDefault } from '../Message/MessageItemView/Headers/SentToChannelHeader'; -import { MessageAuthor as MessageAuthorDefault } from '../Message/MessageItemView/MessageAuthor'; -import { MessageBlocked as MessageBlockedDefault } from '../Message/MessageItemView/MessageBlocked'; -import { MessageBounce as MessageBounceDefault } from '../Message/MessageItemView/MessageBounce'; -import { MessageContent as MessageContentDefault } from '../Message/MessageItemView/MessageContent'; -import { MessageDeleted as MessageDeletedDefault } from '../Message/MessageItemView/MessageDeleted'; -import { MessageError as MessageErrorDefault } from '../Message/MessageItemView/MessageError'; -import { MessageFooter as MessageFooterDefault } from '../Message/MessageItemView/MessageFooter'; -import { MessageHeader as MessageHeaderDefault } from '../Message/MessageItemView/MessageHeader'; -import { MessageItemView as MessageItemViewDefault } from '../Message/MessageItemView/MessageItemView'; -import { MessageReplies as MessageRepliesDefault } from '../Message/MessageItemView/MessageReplies'; -import { MessageRepliesAvatars as MessageRepliesAvatarsDefault } from '../Message/MessageItemView/MessageRepliesAvatars'; -import { MessageStatus as MessageStatusDefault } from '../Message/MessageItemView/MessageStatus'; -import { MessageSwipeContent as MessageSwipeContentDefault } from '../Message/MessageItemView/MessageSwipeContent'; -import { MessageTimestamp as MessageTimestampDefault } from '../Message/MessageItemView/MessageTimestamp'; -import { ReactionListBottom as ReactionListBottomDefault } from '../Message/MessageItemView/ReactionList/ReactionListBottom'; -import { ReactionListClustered as ReactionListClusteredDefault } from '../Message/MessageItemView/ReactionList/ReactionListClustered'; -import { ReactionListCountItem as ReactionListCountItemDefault } from '../Message/MessageItemView/ReactionList/ReactionListItem'; -import { ReactionListItem as ReactionListItemDefault } from '../Message/MessageItemView/ReactionList/ReactionListItem'; -import { ReactionListItemWrapper as ReactionListItemWrapperDefault } from '../Message/MessageItemView/ReactionList/ReactionListItemWrapper'; -import { ReactionListTop as ReactionListTopDefault } from '../Message/MessageItemView/ReactionList/ReactionListTop'; -import { StreamingMessageView as DefaultStreamingMessageView } from '../Message/MessageItemView/StreamingMessageView'; -import { AttachmentUploadPreviewList as AttachmentUploadPreviewDefault } from '../MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList'; -import { FileUploadInProgressIndicator as FileUploadInProgressIndicatorDefault } from '../MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator'; -import { FileUploadRetryIndicator as FileUploadRetryIndicatorDefault } from '../MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator'; -import { FileUploadNotSupportedIndicator as FileUploadNotSupportedIndicatorDefault } from '../MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator'; -import { ImageUploadInProgressIndicator as ImageUploadInProgressIndicatorDefault } from '../MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator'; -import { ImageUploadRetryIndicator as ImageUploadRetryIndicatorDefault } from '../MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator'; -import { ImageUploadNotSupportedIndicator as ImageUploadNotSupportedIndicatorDefault } from '../MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator'; -import { AudioAttachmentUploadPreview as AudioAttachmentUploadPreviewDefault } from '../MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview'; -import { FileAttachmentUploadPreview as FileAttachmentUploadPreviewDefault } from '../MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview'; -import { ImageAttachmentUploadPreview as ImageAttachmentUploadPreviewDefault } from '../MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview'; -import { VideoAttachmentUploadPreview as VideoAttachmentUploadPreviewDefault } from '../MessageInput/components/AttachmentPreview/VideoAttachmentUploadPreview'; -import { AudioRecorder as AudioRecorderDefault } from '../MessageInput/components/AudioRecorder/AudioRecorder'; -import { AudioRecordingButton as AudioRecordingButtonDefault } from '../MessageInput/components/AudioRecorder/AudioRecordingButton'; -import { AudioRecordingInProgress as AudioRecordingInProgressDefault } from '../MessageInput/components/AudioRecorder/AudioRecordingInProgress'; -import { AudioRecordingLockIndicator as AudioRecordingLockIndicatorDefault } from '../MessageInput/components/AudioRecorder/AudioRecordingLockIndicator'; -import { AudioRecordingPreview as AudioRecordingPreviewDefault } from '../MessageInput/components/AudioRecorder/AudioRecordingPreview'; -import { AudioRecordingWaveform as AudioRecordingWaveformDefault } from '../MessageInput/components/AudioRecorder/AudioRecordingWaveform'; -import { InputButtons as InputButtonsDefault } from '../MessageInput/components/InputButtons'; -import { AttachButton as AttachButtonDefault } from '../MessageInput/components/InputButtons/AttachButton'; -import { CooldownTimer as CooldownTimerDefault } from '../MessageInput/components/OutputButtons/CooldownTimer'; -import { SendButton as SendButtonDefault } from '../MessageInput/components/OutputButtons/SendButton'; -import { MessageComposerLeadingView as MessageComposerLeadingViewDefault } from '../MessageInput/MessageComposerLeadingView'; -import { MessageComposerTrailingView as MessageComposerTrailingViewDefault } from '../MessageInput/MessageComposerTrailingView'; -import { MessageInputFooterView as MessageInputFooterViewDefault } from '../MessageInput/MessageInputFooterView'; -import { MessageInputHeaderView as MessageInputHeaderViewDefault } from '../MessageInput/MessageInputHeaderView'; -import { MessageInputLeadingView as MessageInputLeadingViewDefault } from '../MessageInput/MessageInputLeadingView'; -import { MessageInputTrailingView as MessageInputTrailingViewDefault } from '../MessageInput/MessageInputTrailingView'; -import { SendMessageDisallowedIndicator as SendMessageDisallowedIndicatorDefault } from '../MessageInput/SendMessageDisallowedIndicator'; -import { ShowThreadMessageInChannelButton as ShowThreadMessageInChannelButtonDefault } from '../MessageInput/ShowThreadMessageInChannelButton'; -import { StopMessageStreamingButton as DefaultStopMessageStreamingButton } from '../MessageInput/StopMessageStreamingButton'; -import { DateHeader as DateHeaderDefault } from '../MessageList/DateHeader'; -import { InlineDateSeparator as InlineDateSeparatorDefault } from '../MessageList/InlineDateSeparator'; -import { InlineUnreadIndicator as InlineUnreadIndicatorDefault } from '../MessageList/InlineUnreadIndicator'; -import { MessageList as MessageListDefault } from '../MessageList/MessageList'; -import { MessageSystem as MessageSystemDefault } from '../MessageList/MessageSystem'; -import { NetworkDownIndicator as NetworkDownIndicatorDefault } from '../MessageList/NetworkDownIndicator'; -import { ScrollToBottomButton as ScrollToBottomButtonDefault } from '../MessageList/ScrollToBottomButton'; -import { StickyHeader as StickyHeaderDefault } from '../MessageList/StickyHeader'; -import { TypingIndicator as TypingIndicatorDefault } from '../MessageList/TypingIndicator'; -import { TypingIndicatorContainer as TypingIndicatorContainerDefault } from '../MessageList/TypingIndicatorContainer'; -import { UnreadMessagesNotification as UnreadMessagesNotificationDefault } from '../MessageList/UnreadMessagesNotification'; +import type { KeyboardCompatibleViewProps } from '../KeyboardCompatibleView/KeyboardControllerAvoidingView'; import { Emoji } from '../MessageMenu/EmojiPickerList'; import { emojis } from '../MessageMenu/emojis'; -import { MessageActionList as MessageActionListDefault } from '../MessageMenu/MessageActionList'; -import { MessageActionListItem as MessageActionListItemDefault } from '../MessageMenu/MessageActionListItem'; -import { MessageMenu as MessageMenuDefault } from '../MessageMenu/MessageMenu'; -import { MessageReactionPicker as MessageReactionPickerDefault } from '../MessageMenu/MessageReactionPicker'; -import { MessageUserReactions as MessageUserReactionsDefault } from '../MessageMenu/MessageUserReactions'; -import { MessageUserReactionsAvatar as MessageUserReactionsAvatarDefault } from '../MessageMenu/MessageUserReactionsAvatar'; -import { MessageUserReactionsItem as MessageUserReactionsItemDefault } from '../MessageMenu/MessageUserReactionsItem'; import { toUnicodeScalarString } from '../MessageMenu/utils/toUnicodeScalarString'; -import { Reply as ReplyDefault } from '../Reply/Reply'; export type MarkReadFunctionOptions = { /** @@ -288,30 +184,47 @@ export type ChannelPropsWithContext = Pick & | 'bottomInset' | 'topInset' | 'disableAttachmentPicker' - | 'ImageOverlaySelectedComponent' | 'numberOfAttachmentPickerImageColumns' - | 'AttachmentPickerIOSSelectMorePhotos' | 'numberOfAttachmentImagesToLoadPerCall' - | 'AttachmentPickerContent' > > & Partial< Pick< ChannelContextValue, - | 'EmptyStateIndicator' | 'enableMessageGroupingByUser' | 'enforceUniqueReaction' | 'hideStickyDateHeader' | 'hideDateSeparators' - | 'LoadingIndicator' | 'maxTimeBetweenGroupedMessages' - | 'NetworkDownIndicator' - | 'StickyHeader' | 'maximumMessageLimit' > > & Pick & - Partial> & + Partial< + Pick< + InputMessageInputContextValue, + | 'additionalTextInputProps' + | 'allowSendBeforeAttachmentsUpload' + | 'asyncMessagesLockDistance' + | 'asyncMessagesMinimumPressDuration' + | 'audioRecordingSendOnComplete' + | 'asyncMessagesSlideToCancelDistance' + | 'attachmentPickerBottomSheetHeight' + | 'attachmentSelectionBarHeight' + | 'audioRecordingEnabled' + | 'compressImageQuality' + | 'createPollOptionGap' + | 'doFileUploadRequest' + | 'handleAttachButtonPress' + | 'hasCameraPicker' + | 'hasCommands' + | 'hasFilePicker' + | 'hasImagePicker' + | 'messageInputFloating' + | 'openPollCreationDialog' + | 'setInputRef' + > + > & Pick & Partial< Pick @@ -321,25 +234,15 @@ export type ChannelPropsWithContext = Pick & Pick< MessagesContextValue, | 'additionalPressableProps' - | 'Attachment' - | 'AudioAttachment' | 'customMessageSwipeAction' - | 'DateHeader' | 'deletedMessagesVisibilityType' | 'disableTypingIndicator' | 'dismissKeyboardOnMessageTouch' | 'enableSwipeToReply' | 'urlPreviewType' - | 'UnsupportedAttachment' - | 'FileAttachment' - | 'FileAttachmentIcon' - | 'FileAttachmentGroup' - | 'FilePreview' | 'FlatList' | 'forceAlignMessages' - | 'Gallery' | 'getMessageGroupStyle' - | 'Giphy' | 'giphyVersion' | 'handleBan' | 'handleCopy' @@ -355,77 +258,22 @@ export type ChannelPropsWithContext = Pick & | 'handleRetry' | 'handleThreadReply' | 'handleBlockUser' - | 'InlineDateSeparator' - | 'InlineUnreadIndicator' | 'isAttachmentEqual' - | 'ImageLoadingFailedIndicator' - | 'ImageLoadingIndicator' | 'markdownRules' - | 'Message' - | 'MessageActionList' - | 'MessageActionListItem' | 'messageActions' - | 'MessageAuthor' - | 'MessageBounce' - | 'MessageBlocked' - | 'MessageContent' - | 'MessageContentBottomView' - | 'MessageContentLeadingView' | 'messageContentOrder' - | 'MessageContentTrailingView' - | 'MessageContentTopView' - | 'MessageDeleted' - | 'MessageError' - | 'MessageFooter' - | 'MessageHeader' - | 'MessageList' - | 'MessageLocation' - | 'MessageMenu' - | 'MessagePinnedHeader' - | 'MessageReminderHeader' - | 'MessageSavedForLaterHeader' - | 'SentToChannelHeader' - | 'MessageReplies' - | 'MessageRepliesAvatars' - | 'MessageSpacer' - | 'MessageItemView' - | 'MessageStatus' - | 'MessageSystem' - | 'MessageText' | 'messageTextNumberOfLines' - | 'MessageTimestamp' - | 'MessageUserReactions' - | 'MessageSwipeContent' | 'messageSwipeToReplyHitSlop' | 'myMessageTheme' | 'onLongPressMessage' | 'onPressInMessage' | 'onPressMessage' - | 'MessageReactionPicker' - | 'MessageUserReactionsAvatar' - | 'MessageUserReactionsItem' - | 'ReactionListBottom' | 'reactionListPosition' | 'reactionListType' - | 'ReactionListTop' - | 'ReactionListClustered' - | 'ReactionListItem' - | 'ReactionListItemWrapper' - | 'ReactionListCountItem' - | 'Reply' | 'shouldShowUnreadUnderlay' - | 'ScrollToBottomButton' | 'selectReaction' | 'supportedReactions' - | 'TypingIndicator' - | 'TypingIndicatorContainer' - | 'UrlPreview' - | 'URLPreviewCompact' - | 'VideoThumbnail' - | 'PollContent' | 'hasCreatePoll' - | 'UnreadMessagesNotification' - | 'StreamingMessageView' > > & Partial> & @@ -494,31 +342,7 @@ export type ChannelPropsWithContext = Pick & */ initialScrollToFirstUnreadMessage?: boolean; keyboardBehavior?: KeyboardCompatibleViewProps['behavior']; - /** - * Custom wrapper component that handles height adjustment of Channel component when keyboard is opened or dismissed - * Default component (accepts the same props): [KeyboardCompatibleView](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/KeyboardCompatibleView/KeyboardCompatibleView.tsx) - * - * **Example:** - * - * ``` - * { - * return ( - * - * {props.children} - * - * ) - * }} - * /> - * ``` - */ - KeyboardCompatibleView?: React.ComponentType; keyboardVerticalOffset?: number; - /** - * Custom loading error indicator to override the Stream default - */ - LoadingErrorIndicator?: React.ComponentType; /** * Boolean flag to enable/disable marking the channel as read on mount */ @@ -554,15 +378,7 @@ export type ChannelPropsWithContext = Pick & * is sent). */ initializeOnMount?: boolean; - } & Partial< - Pick< - InputMessageInputContextValue, - | 'openPollCreationDialog' - | 'CreatePollContent' - | 'StopMessageStreamingButton' - | 'allowSendBeforeAttachmentsUpload' - > - >; + }; const ChannelWithContext = (props: PropsWithChildren) => { const { @@ -576,24 +392,9 @@ const ChannelWithContext = (props: PropsWithChildren) = asyncMessagesMinimumPressDuration = 500, asyncMessagesSlideToCancelDistance = 75, audioRecordingSendOnComplete = true, - AttachButton = AttachButtonDefault, - Attachment = AttachmentDefault, attachmentPickerBottomSheetHeight = disableAttachmentPicker ? 72 : 333, - AttachmentPickerSelectionBar = DefaultAttachmentPickerSelectionBar, attachmentSelectionBarHeight = 72, - AudioAttachment = AudioAttachmentDefault, - AudioAttachmentUploadPreview = AudioAttachmentUploadPreviewDefault, - AudioRecorder = AudioRecorderDefault, audioRecordingEnabled = false, - AudioRecordingInProgress = AudioRecordingInProgressDefault, - AudioRecordingLockIndicator = AudioRecordingLockIndicatorDefault, - AudioRecordingPreview = AudioRecordingPreviewDefault, - AudioRecordingWaveform = AudioRecordingWaveformDefault, - AutoCompleteSuggestionHeader = AutoCompleteSuggestionHeaderDefault, - AutoCompleteSuggestionItem = AutoCompleteSuggestionItemDefault, - AutoCompleteSuggestionList = AutoCompleteSuggestionListDefault, - AttachmentUploadPreviewList = AttachmentUploadPreviewDefault, - ImageOverlaySelectedComponent = DefaultImageOverlaySelectedComponent, numberOfAttachmentImagesToLoadPerCall = 25, numberOfAttachmentPickerImageColumns = 3, giphyVersion = 'fixed_height', @@ -602,11 +403,8 @@ const ChannelWithContext = (props: PropsWithChildren) = children, client, compressImageQuality, - CooldownTimer = CooldownTimerDefault, - CreatePollContent, createPollOptionGap, customMessageSwipeAction, - DateHeader = DateHeaderDefault, deletedMessagesVisibilityType = 'always', disableKeyboardCompatibleView = false, disableTypingIndicator, @@ -616,28 +414,14 @@ const ChannelWithContext = (props: PropsWithChildren) = doSendMessageRequest, preSendMessageRequest, doUpdateMessageRequest, - EmptyStateIndicator = EmptyStateIndicatorDefault, enableMessageGroupingByUser = true, enableOfflineSupport, allowSendBeforeAttachmentsUpload = enableOfflineSupport, enableSwipeToReply = true, enforceUniqueReaction = false, - FileAttachment = FileAttachmentDefault, - FileAttachmentUploadPreview = FileAttachmentUploadPreviewDefault, - FileAttachmentGroup = FileAttachmentGroupDefault, - FileAttachmentIcon = FileIconDefault, - FilePreview = FilePreviewDefault, - FileUploadInProgressIndicator = FileUploadInProgressIndicatorDefault, - FileUploadRetryIndicator = FileUploadRetryIndicatorDefault, - FileUploadNotSupportedIndicator = FileUploadNotSupportedIndicatorDefault, - ImageUploadInProgressIndicator = ImageUploadInProgressIndicatorDefault, - ImageUploadRetryIndicator = ImageUploadRetryIndicatorDefault, - ImageUploadNotSupportedIndicator = ImageUploadNotSupportedIndicatorDefault, FlatList = NativeHandlers.FlatList, forceAlignMessages, - Gallery = GalleryDefault, getMessageGroupStyle, - Giphy = GiphyDefault, handleAttachButtonPress, handleBan, handleCopy, @@ -661,39 +445,17 @@ const ChannelWithContext = (props: PropsWithChildren) = hasImagePicker = isImagePickerAvailable() || isImageMediaLibraryAvailable(), hideDateSeparators = false, hideStickyDateHeader = false, - ImageAttachmentUploadPreview = ImageAttachmentUploadPreviewDefault, - ImageLoadingFailedIndicator = ImageLoadingFailedIndicatorDefault, - ImageLoadingIndicator = ImageLoadingIndicatorDefault, initialScrollToFirstUnreadMessage = false, - InlineDateSeparator = InlineDateSeparatorDefault, - InlineUnreadIndicator = InlineUnreadIndicatorDefault, - Input, - InputView = InputViewDefault, - InputButtons = InputButtonsDefault, - MessageComposerLeadingView = MessageComposerLeadingViewDefault, - MessageComposerTrailingView = MessageComposerTrailingViewDefault, isAttachmentEqual, isMessageAIGenerated = () => false, keyboardBehavior, - KeyboardCompatibleView = KeyboardCompatibleViewDefault, keyboardVerticalOffset, - LoadingErrorIndicator = LoadingErrorIndicatorDefault, - LoadingIndicator = LoadingIndicatorDefault, loadingMore: loadingMoreProp, loadingMoreRecent: loadingMoreRecentProp, markdownRules, markReadOnMount = true, maxTimeBetweenGroupedMessages, - Message = MessageDefault, - MessageActionList = MessageActionListDefault, - MessageActionListItem = MessageActionListItemDefault, messageActions, - MessageAuthor = MessageAuthorDefault, - MessageBlocked = MessageBlockedDefault, - MessageBounce = MessageBounceDefault, - MessageContent = MessageContentDefault, - MessageContentBottomView, - MessageContentLeadingView, messageContentOrder = [ 'quoted_reply', 'gallery', @@ -704,42 +466,11 @@ const ChannelWithContext = (props: PropsWithChildren) = 'text', 'location', ], - MessageContentTrailingView, - MessageContentTopView, - MessageDeleted = MessageDeletedDefault, - MessageError = MessageErrorDefault, messageInputFloating = false, - MessageInputFooterView = MessageInputFooterViewDefault, - MessageInputHeaderView = MessageInputHeaderViewDefault, - MessageInputLeadingView = MessageInputLeadingViewDefault, - MessageInputTrailingView = MessageInputTrailingViewDefault, - MessageFooter = MessageFooterDefault, - MessageHeader = MessageHeaderDefault, messageId, - MessageList = MessageListDefault, - MessageLocation, - MessageMenu = MessageMenuDefault, - MessagePinnedHeader = MessagePinnedHeaderDefault, - MessageReminderHeader = MessageReminderHeaderDefault, - MessageSavedForLaterHeader = MessageSavedForLaterHeaderDefault, - SentToChannelHeader = SentToChannelHeaderDefault, - MessageReactionPicker = MessageReactionPickerDefault, - MessageReplies = MessageRepliesDefault, - MessageRepliesAvatars = MessageRepliesAvatarsDefault, - MessageSpacer, - MessageItemView = MessageItemViewDefault, - MessageStatus = MessageStatusDefault, - MessageSwipeContent = MessageSwipeContentDefault, messageSwipeToReplyHitSlop, - MessageSystem = MessageSystemDefault, - MessageText, messageTextNumberOfLines, - MessageTimestamp = MessageTimestampDefault, - MessageUserReactions = MessageUserReactionsDefault, - MessageUserReactionsAvatar = MessageUserReactionsAvatarDefault, - MessageUserReactionsItem = MessageUserReactionsItemDefault, myMessageTheme, - NetworkDownIndicator = NetworkDownIndicatorDefault, // TODO: Think about this one newMessageStateUpdateThrottleInterval = defaultThrottleInterval, onLongPressMessage, @@ -748,56 +479,30 @@ const ChannelWithContext = (props: PropsWithChildren) = onAlsoSentToChannelHeaderPress, openPollCreationDialog, overrideOwnCapabilities, - PollContent, - ReactionListBottom = ReactionListBottomDefault, reactionListPosition = 'top', reactionListType = 'clustered', - ReactionListTop = ReactionListTopDefault, - ReactionListClustered = ReactionListClusteredDefault, - ReactionListItem = ReactionListItemDefault, - ReactionListItemWrapper = ReactionListItemWrapperDefault, - ReactionListCountItem = ReactionListCountItemDefault, - Reply = ReplyDefault, - ScrollToBottomButton = ScrollToBottomButtonDefault, selectReaction, - SendButton = SendButtonDefault, - SendMessageDisallowedIndicator = SendMessageDisallowedIndicatorDefault, setInputRef, setThreadMessages, shouldShowUnreadUnderlay = true, shouldSyncChannel, - ShowThreadMessageInChannelButton = ShowThreadMessageInChannelButtonDefault, - StartAudioRecordingButton = AudioRecordingButtonDefault, stateUpdateThrottleInterval = defaultThrottleInterval, - StickyHeader = StickyHeaderDefault, - StopMessageStreamingButton: StopMessageStreamingButtonOverride, - StreamingMessageView = DefaultStreamingMessageView, supportedReactions = reactionData, t, thread: threadFromProps, threadList, threadMessages, topInset, - TypingIndicator = TypingIndicatorDefault, - TypingIndicatorContainer = TypingIndicatorContainerDefault, - UnreadMessagesNotification = UnreadMessagesNotificationDefault, - UrlPreview = URLPreviewDefault, - URLPreviewCompact = URLPreviewCompactDefault, - VideoAttachmentUploadPreview = VideoAttachmentUploadPreviewDefault, - VideoThumbnail = VideoThumbnailDefault, isOnline, maximumMessageLimit, initializeOnMount = true, - AttachmentPickerContent = DefaultAttachmentPickerContent, urlPreviewType = 'full', - UnsupportedAttachment = UnsupportedAttachmentDefault, } = props; + const components = useComponentsContext(); + const { KeyboardCompatibleView, LoadingErrorIndicator } = components; + const { thread: threadProps, threadInstance } = threadFromProps; - const StopMessageStreamingButton = - StopMessageStreamingButtonOverride === undefined - ? DefaultStopMessageStreamingButton - : StopMessageStreamingButtonOverride; const styles = useStyles(); const [deleted, setDeleted] = useState(false); @@ -1815,13 +1520,10 @@ const ChannelWithContext = (props: PropsWithChildren) = disableAttachmentPicker, openPicker: handleOpenPicker, topInset, - ImageOverlaySelectedComponent, - AttachmentPickerSelectionBar, numberOfAttachmentPickerImageColumns, attachmentPickerBottomSheetHeight, attachmentSelectionBarHeight, numberOfAttachmentImagesToLoadPerCall, - AttachmentPickerContent, }), [ bottomInset, @@ -1830,13 +1532,10 @@ const ChannelWithContext = (props: PropsWithChildren) = disableAttachmentPicker, handleOpenPicker, topInset, - ImageOverlaySelectedComponent, - AttachmentPickerSelectionBar, numberOfAttachmentPickerImageColumns, attachmentPickerBottomSheetHeight, attachmentSelectionBarHeight, numberOfAttachmentImagesToLoadPerCall, - AttachmentPickerContent, ], ); @@ -1849,7 +1548,6 @@ const ChannelWithContext = (props: PropsWithChildren) = channel, channelUnreadStateStore, disabled: !!channel?.data?.frozen, - EmptyStateIndicator, enableMessageGroupingByUser, enforceUniqueReaction, error, @@ -1861,19 +1559,16 @@ const ChannelWithContext = (props: PropsWithChildren) = loadChannelAroundMessage, loadChannelAtFirstUnreadMessage, loading: channelMessagesState.loading, - LoadingIndicator, markRead, maximumMessageLimit, maxTimeBetweenGroupedMessages, members: channelState.members ?? {}, - NetworkDownIndicator, read: channelState.read ?? {}, reloadChannel, scrollToFirstUnreadThreshold, setChannelUnreadState, setLastRead, setTargetedMessage, - StickyHeader, targetedMessage, threadList, uploadAbortControllerRef, @@ -1901,61 +1596,24 @@ const ChannelWithContext = (props: PropsWithChildren) = asyncMessagesMinimumPressDuration, audioRecordingSendOnComplete, asyncMessagesSlideToCancelDistance, - AttachButton, attachmentPickerBottomSheetHeight, - AttachmentPickerSelectionBar, attachmentSelectionBarHeight, - AttachmentUploadPreviewList, - AudioAttachmentUploadPreview, - AudioRecorder, audioRecordingEnabled, - AudioRecordingInProgress, - AudioRecordingLockIndicator, - AudioRecordingPreview, - AudioRecordingWaveform, - AutoCompleteSuggestionHeader, - AutoCompleteSuggestionItem, - AutoCompleteSuggestionList, channelId, compressImageQuality, - CooldownTimer, - CreatePollContent, createPollOptionGap, doFileUploadRequest, editMessage, - FileAttachmentUploadPreview, - FileUploadInProgressIndicator, - FileUploadRetryIndicator, - FileUploadNotSupportedIndicator, - ImageUploadInProgressIndicator, - ImageUploadRetryIndicator, - ImageUploadNotSupportedIndicator, handleAttachButtonPress, hasCameraPicker, hasCommands: hasCommands ?? !!clientChannelConfig?.commands?.length, hasFilePicker, hasImagePicker, - ImageAttachmentUploadPreview, - Input, - InputView, - InputButtons, - MessageComposerLeadingView, - MessageComposerTrailingView, messageInputFloating, messageInputHeightStore, - MessageInputFooterView, - MessageInputHeaderView, - MessageInputLeadingView, - MessageInputTrailingView, openPollCreationDialog, - SendButton, sendMessage, - SendMessageDisallowedIndicator, setInputRef, - ShowThreadMessageInChannelButton, - StartAudioRecordingButton, - StopMessageStreamingButton, - VideoAttachmentUploadPreview, }); const messageListContext = useCreatePaginatedMessageListContext({ @@ -1975,11 +1633,8 @@ const ChannelWithContext = (props: PropsWithChildren) = const messagesContext = useCreateMessagesContext({ additionalPressableProps, - Attachment, - AudioAttachment, channelId, customMessageSwipeAction, - DateHeader, deletedMessagesVisibilityType, deleteMessage, deleteReaction, @@ -1987,15 +1642,9 @@ const ChannelWithContext = (props: PropsWithChildren) = dismissKeyboardOnMessageTouch, enableMessageGroupingByUser, enableSwipeToReply, - FileAttachment, - FileAttachmentGroup, - FileAttachmentIcon, - FilePreview, FlatList, forceAlignMessages, - Gallery, getMessageGroupStyle, - Giphy, giphyVersion, handleBan, handleCopy, @@ -2013,85 +1662,29 @@ const ChannelWithContext = (props: PropsWithChildren) = handleBlockUser, hasCreatePoll: hasCreatePoll === undefined ? pollCreationEnabled : hasCreatePoll && pollCreationEnabled, - ImageLoadingFailedIndicator, - ImageLoadingIndicator, initialScrollToFirstUnreadMessage: !messageId && initialScrollToFirstUnreadMessage, // when messageId is set, we scroll to the messageId instead of first unread - InlineDateSeparator, - InlineUnreadIndicator, isAttachmentEqual, isMessageAIGenerated, markdownRules, - Message, - MessageActionList, - MessageActionListItem, messageActions, - MessageAuthor, - MessageBlocked, - MessageBounce, - MessageContent, - MessageContentBottomView, - MessageContentLeadingView, messageContentOrder, - MessageContentTrailingView, - MessageContentTopView, - MessageDeleted, - MessageError, - MessageFooter, - MessageHeader, - MessageList, - MessageLocation, - MessageMenu, - MessagePinnedHeader, - MessageReminderHeader, - MessageSavedForLaterHeader, - SentToChannelHeader, - MessageReactionPicker, - MessageReplies, - MessageRepliesAvatars, - MessageSpacer, - MessageItemView, - MessageStatus, - MessageSwipeContent, messageSwipeToReplyHitSlop, - MessageSystem, - MessageText, messageTextNumberOfLines, - MessageTimestamp, - MessageUserReactions, - MessageUserReactionsAvatar, - MessageUserReactionsItem, myMessageTheme, onLongPressMessage, onPressInMessage, onPressMessage, - PollContent, - ReactionListBottom, reactionListPosition, reactionListType, - ReactionListTop, - ReactionListClustered, - ReactionListItem, - ReactionListItemWrapper, - ReactionListCountItem, removeMessage, - Reply, retrySendMessage, - ScrollToBottomButton, selectReaction, sendReaction, shouldShowUnreadUnderlay, - StreamingMessageView, supportedReactions, targetedMessage, - TypingIndicator, - TypingIndicatorContainer, - UnreadMessagesNotification, updateMessage, - UrlPreview, - URLPreviewCompact, - VideoThumbnail, urlPreviewType, - UnsupportedAttachment, }); const threadContext = useCreateThreadContext({ diff --git a/package/src/components/Channel/__tests__/isAttachmentEqualHandler.test.js b/package/src/components/Channel/__tests__/isAttachmentEqualHandler.test.js index d56cbb0ac4..b59241163e 100644 --- a/package/src/components/Channel/__tests__/isAttachmentEqualHandler.test.js +++ b/package/src/components/Channel/__tests__/isAttachmentEqualHandler.test.js @@ -4,6 +4,7 @@ import { Text } from 'react-native'; import { act, cleanup, render, waitFor } from '@testing-library/react-native'; +import { WithComponents } from '../../../contexts/componentsContext/ComponentsContext'; import { OverlayProvider } from '../../../contexts/overlayContext/OverlayProvider'; import { getOrCreateChannelApi } from '../../../mock-builders/api/getOrCreateChannel'; @@ -61,17 +62,19 @@ describe('isAttachmentEqualHandler', () => { return ( - { - if (type === 'test') { - return {customField}; - } + { + if (type === 'test') { + return {customField}; + } + }, }} - channel={channel} - isAttachmentEqual={isAttachmentEqualHandler} > - - + + + + ); diff --git a/package/src/components/Channel/hooks/useCreateChannelContext.ts b/package/src/components/Channel/hooks/useCreateChannelContext.ts index 824f30cab5..5c7c9e33ca 100644 --- a/package/src/components/Channel/hooks/useCreateChannelContext.ts +++ b/package/src/components/Channel/hooks/useCreateChannelContext.ts @@ -6,7 +6,6 @@ export const useCreateChannelContext = ({ channel, channelUnreadStateStore, disabled, - EmptyStateIndicator, enableMessageGroupingByUser, enforceUniqueReaction, error, @@ -18,19 +17,16 @@ export const useCreateChannelContext = ({ loadChannelAroundMessage, loadChannelAtFirstUnreadMessage, loading, - LoadingIndicator, markRead, maxTimeBetweenGroupedMessages, maximumMessageLimit, members, - NetworkDownIndicator, read, reloadChannel, scrollToFirstUnreadThreshold, setChannelUnreadState, setLastRead, setTargetedMessage, - StickyHeader, targetedMessage, threadList, uploadAbortControllerRef, @@ -52,7 +48,6 @@ export const useCreateChannelContext = ({ channel, channelUnreadStateStore, disabled, - EmptyStateIndicator, enableMessageGroupingByUser, enforceUniqueReaction, error, @@ -64,19 +59,16 @@ export const useCreateChannelContext = ({ loadChannelAroundMessage, loadChannelAtFirstUnreadMessage, loading, - LoadingIndicator, markRead, maximumMessageLimit, maxTimeBetweenGroupedMessages, members, - NetworkDownIndicator, read, reloadChannel, scrollToFirstUnreadThreshold, setChannelUnreadState, setLastRead, setTargetedMessage, - StickyHeader, targetedMessage, threadList, uploadAbortControllerRef, diff --git a/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts b/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts index 847a43851f..2ea779f443 100644 --- a/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts +++ b/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts @@ -9,62 +9,25 @@ export const useCreateInputMessageInputContext = ({ asyncMessagesMinimumPressDuration, asyncMessagesSlideToCancelDistance, audioRecordingSendOnComplete, - AttachButton, attachmentPickerBottomSheetHeight, - AttachmentPickerSelectionBar, attachmentSelectionBarHeight, - AttachmentUploadPreviewList, - AudioAttachmentUploadPreview, - AudioRecorder, audioRecordingEnabled, - AudioRecordingInProgress, - AudioRecordingLockIndicator, - AudioRecordingPreview, - AudioRecordingWaveform, - AutoCompleteSuggestionHeader, - AutoCompleteSuggestionItem, - AutoCompleteSuggestionList, channelId, compressImageQuality, - CooldownTimer, - CreatePollContent, createPollOptionGap, doFileUploadRequest, editMessage, - FileAttachmentUploadPreview, - FileUploadInProgressIndicator, - FileUploadRetryIndicator, - FileUploadNotSupportedIndicator, - ImageUploadInProgressIndicator, - ImageUploadRetryIndicator, - ImageUploadNotSupportedIndicator, handleAttachButtonPress, hasCameraPicker, hasCommands, hasFilePicker, hasImagePicker, - ImageAttachmentUploadPreview, - Input, - InputView, - InputButtons, - MessageComposerLeadingView, - MessageComposerTrailingView, messageInputFloating, messageInputHeightStore, - MessageInputFooterView, - MessageInputHeaderView, - MessageInputLeadingView, - MessageInputTrailingView, openPollCreationDialog, - SendButton, sendMessage, - SendMessageDisallowedIndicator, setInputRef, showPollCreationDialog, - ShowThreadMessageInChannelButton, - StartAudioRecordingButton, - StopMessageStreamingButton, - VideoAttachmentUploadPreview, }: InputMessageInputContextValue & { /** * To ensure we allow re-render, when channel is changed @@ -79,70 +42,27 @@ export const useCreateInputMessageInputContext = ({ asyncMessagesMinimumPressDuration, asyncMessagesSlideToCancelDistance, audioRecordingSendOnComplete, - AttachButton, attachmentPickerBottomSheetHeight, - AttachmentPickerSelectionBar, attachmentSelectionBarHeight, - AttachmentUploadPreviewList, - AudioAttachmentUploadPreview, - AudioRecorder, audioRecordingEnabled, - AudioRecordingInProgress, - AudioRecordingLockIndicator, - AudioRecordingPreview, - AudioRecordingWaveform, - AutoCompleteSuggestionHeader, - AutoCompleteSuggestionItem, - AutoCompleteSuggestionList, compressImageQuality, - CooldownTimer, - CreatePollContent, createPollOptionGap, doFileUploadRequest, editMessage, - FileAttachmentUploadPreview, - FileUploadInProgressIndicator, - FileUploadRetryIndicator, - FileUploadNotSupportedIndicator, - ImageUploadInProgressIndicator, - ImageUploadRetryIndicator, - ImageUploadNotSupportedIndicator, handleAttachButtonPress, hasCameraPicker, hasCommands, hasFilePicker, hasImagePicker, - ImageAttachmentUploadPreview, - Input, - InputView, - InputButtons, - MessageComposerLeadingView, - MessageComposerTrailingView, messageInputFloating, messageInputHeightStore, - MessageInputFooterView, - MessageInputHeaderView, - MessageInputLeadingView, - MessageInputTrailingView, openPollCreationDialog, - SendButton, sendMessage, - SendMessageDisallowedIndicator, setInputRef, showPollCreationDialog, - ShowThreadMessageInChannelButton, - StartAudioRecordingButton, - StopMessageStreamingButton, - VideoAttachmentUploadPreview, }), // eslint-disable-next-line react-hooks/exhaustive-deps - [ - compressImageQuality, - channelId, - CreatePollContent, - showPollCreationDialog, - allowSendBeforeAttachmentsUpload, - ], + [compressImageQuality, channelId, showPollCreationDialog, allowSendBeforeAttachmentsUpload], ); return inputMessageInputContext; diff --git a/package/src/components/Channel/hooks/useCreateMessagesContext.ts b/package/src/components/Channel/hooks/useCreateMessagesContext.ts index 2f21318b6d..62c375cec6 100644 --- a/package/src/components/Channel/hooks/useCreateMessagesContext.ts +++ b/package/src/components/Channel/hooks/useCreateMessagesContext.ts @@ -4,11 +4,8 @@ import type { MessagesContextValue } from '../../../contexts/messagesContext/Mes export const useCreateMessagesContext = ({ additionalPressableProps, - Attachment, - AudioAttachment, channelId, customMessageSwipeAction, - DateHeader, deletedMessagesVisibilityType, deleteMessage, deleteReaction, @@ -16,15 +13,9 @@ export const useCreateMessagesContext = ({ dismissKeyboardOnMessageTouch, enableMessageGroupingByUser, enableSwipeToReply, - FileAttachment, - FileAttachmentGroup, - FileAttachmentIcon, - FilePreview, FlatList, forceAlignMessages, - Gallery, getMessageGroupStyle, - Giphy, giphyVersion, handleBan, handleCopy, @@ -41,85 +32,29 @@ export const useCreateMessagesContext = ({ handleThreadReply, handleBlockUser, hasCreatePoll, - ImageLoadingFailedIndicator, - ImageLoadingIndicator, initialScrollToFirstUnreadMessage, - InlineDateSeparator, - InlineUnreadIndicator, isAttachmentEqual, isMessageAIGenerated, markdownRules, - Message, - MessageActionList, - MessageActionListItem, messageActions, - MessageAuthor, - MessageBlocked, - MessageBounce, - MessageContent, - MessageContentBottomView, - MessageContentLeadingView, messageContentOrder, - MessageContentTrailingView, - MessageContentTopView, - MessageDeleted, - MessageError, - MessageFooter, - MessageHeader, - MessageList, - MessageLocation, - MessageMenu, - MessagePinnedHeader, - MessageReminderHeader, - MessageSavedForLaterHeader, - SentToChannelHeader, - MessageReactionPicker, - MessageReplies, - MessageRepliesAvatars, - MessageSpacer, - MessageItemView, - MessageStatus, - MessageSwipeContent, messageSwipeToReplyHitSlop, - MessageSystem, - MessageText, messageTextNumberOfLines, - MessageTimestamp, - MessageUserReactions, - MessageUserReactionsAvatar, - MessageUserReactionsItem, myMessageTheme, onLongPressMessage, onPressInMessage, onPressMessage, - PollContent, - ReactionListBottom, reactionListPosition, reactionListType, - ReactionListTop, - ReactionListClustered, - ReactionListItem, - ReactionListItemWrapper, - ReactionListCountItem, removeMessage, - Reply, retrySendMessage, - ScrollToBottomButton, selectReaction, sendReaction, shouldShowUnreadUnderlay, - StreamingMessageView, supportedReactions, targetedMessage, - TypingIndicator, - TypingIndicatorContainer, - UnreadMessagesNotification, updateMessage, - UrlPreview, - URLPreviewCompact, - VideoThumbnail, urlPreviewType, - UnsupportedAttachment, }: MessagesContextValue & { /** * To ensure we allow re-render, when channel is changed @@ -134,10 +69,7 @@ export const useCreateMessagesContext = ({ const messagesContext: MessagesContextValue = useMemo( () => ({ additionalPressableProps, - Attachment, - AudioAttachment, customMessageSwipeAction, - DateHeader, deletedMessagesVisibilityType, deleteMessage, deleteReaction, @@ -145,15 +77,9 @@ export const useCreateMessagesContext = ({ dismissKeyboardOnMessageTouch, enableMessageGroupingByUser, enableSwipeToReply, - FileAttachment, - FileAttachmentGroup, - FileAttachmentIcon, - FilePreview, FlatList, forceAlignMessages, - Gallery, getMessageGroupStyle, - Giphy, giphyVersion, handleBan, handleCopy, @@ -170,85 +96,29 @@ export const useCreateMessagesContext = ({ handleThreadReply, handleBlockUser, hasCreatePoll, - ImageLoadingFailedIndicator, - ImageLoadingIndicator, initialScrollToFirstUnreadMessage, - InlineDateSeparator, - InlineUnreadIndicator, isAttachmentEqual, isMessageAIGenerated, markdownRules, - Message, - MessageActionList, - MessageActionListItem, messageActions, - MessageAuthor, - MessageBlocked, - MessageBounce, - MessageContent, - MessageContentBottomView, - MessageContentLeadingView, messageContentOrder, - MessageContentTrailingView, - MessageContentTopView, - MessageDeleted, - MessageError, - MessageFooter, - MessageHeader, - MessageList, - MessageLocation, - MessageMenu, - MessagePinnedHeader, - MessageReminderHeader, - MessageSavedForLaterHeader, - SentToChannelHeader, - MessageReactionPicker, - MessageReplies, - MessageRepliesAvatars, - MessageSpacer, - MessageItemView, - MessageStatus, - MessageSwipeContent, messageSwipeToReplyHitSlop, - MessageSystem, - MessageText, messageTextNumberOfLines, - MessageTimestamp, - MessageUserReactions, - MessageUserReactionsAvatar, - MessageUserReactionsItem, myMessageTheme, onLongPressMessage, onPressInMessage, onPressMessage, - PollContent, - ReactionListBottom, reactionListPosition, reactionListType, - ReactionListTop, - ReactionListClustered, - ReactionListItem, - ReactionListItemWrapper, - ReactionListCountItem, removeMessage, - Reply, retrySendMessage, - ScrollToBottomButton, selectReaction, sendReaction, shouldShowUnreadUnderlay, - StreamingMessageView, supportedReactions, targetedMessage, - TypingIndicator, - TypingIndicatorContainer, - UnreadMessagesNotification, updateMessage, - UrlPreview, - URLPreviewCompact, - VideoThumbnail, urlPreviewType, - UnsupportedAttachment, }), // eslint-disable-next-line react-hooks/exhaustive-deps [ diff --git a/package/src/components/ChannelList/ChannelList.tsx b/package/src/components/ChannelList/ChannelList.tsx index 79196f1bb7..9bd0055f59 100644 --- a/package/src/components/ChannelList/ChannelList.tsx +++ b/package/src/components/ChannelList/ChannelList.tsx @@ -11,15 +11,10 @@ import { QueryChannelsRequestType, } from 'stream-chat'; -import { ChannelListFooterLoadingIndicator } from './ChannelListFooterLoadingIndicator'; -import { ChannelListHeaderErrorIndicator } from './ChannelListHeaderErrorIndicator'; -import { ChannelListHeaderNetworkDownIndicator } from './ChannelListHeaderNetworkDownIndicator'; -import { ChannelListLoadingIndicator } from './ChannelListLoadingIndicator'; -import { ChannelListView, ChannelListViewProps } from './ChannelListView'; +import { ChannelListView } from './ChannelListView'; import { useChannelUpdated } from './hooks/listeners/useChannelUpdated'; import { useCreateChannelsContext } from './hooks/useCreateChannelsContext'; import { usePaginatedChannels } from './hooks/usePaginatedChannels'; -import { Skeleton as SkeletonDefault } from './Skeleton'; import { ChannelsContextValue, @@ -28,38 +23,16 @@ import { import { useChatContext } from '../../contexts/chatContext/ChatContext'; import { SwipeRegistryProvider } from '../../contexts/swipeableContext/SwipeRegistryContext'; import type { ChannelListEventListenerOptions } from '../../types/types'; -import { ChannelPreviewView } from '../ChannelPreview/ChannelPreviewView'; -import { EmptyStateIndicator as EmptyStateIndicatorDefault } from '../Indicators/EmptyStateIndicator'; -import { LoadingErrorIndicator as LoadingErrorIndicatorDefault } from '../Indicators/LoadingErrorIndicator'; export type ChannelListProps = Partial< Pick< ChannelsContextValue, | 'additionalFlatListProps' - | 'EmptyStateIndicator' - | 'FooterLoadingIndicator' - | 'HeaderErrorIndicator' - | 'HeaderNetworkDownIndicator' - | 'LoadingErrorIndicator' - | 'LoadingIndicator' - | 'Preview' | 'setFlatListRef' - | 'ListHeaderComponent' | 'onSelect' - | 'PreviewAvatar' - | 'PreviewMessage' - | 'PreviewMutedStatus' - | 'PreviewStatus' - | 'PreviewTitle' - | 'PreviewLastMessage' - | 'PreviewUnreadCount' - | 'PreviewTypingIndicator' - | 'PreviewMessageDeliveryStatus' - | 'ChannelDetailsBottomSheet' | 'getChannelActionItems' | 'swipeActionsEnabled' | 'loadMoreThreshold' - | 'Skeleton' | 'maxUnreadCount' | 'numberOfSkeletons' | 'mutedStatusPosition' @@ -75,12 +48,6 @@ export type ChannelListProps = Partial< * @overrideType object * */ filters?: ChannelFilters; - /** - * Custom UI component to display the list of channels - * - * Default: [ChannelListView](https://getstream.io/chat/docs/sdk/reactnative/ui-components/channel-list-view/) - */ - List?: React.ComponentType; /** * If set to true, channels won't dynamically sort by most recent message, defaults to false */ @@ -247,8 +214,7 @@ const DEFAULT_SORT = {}; /** * This component fetches a list of channels, allowing you to select the channel you want to open. - * The ChannelList doesn't provide any UI for the underlying React Native FlatList. UI is determined by the `List` component which is - * provided to the ChannelList component as a prop. By default, the ChannelListView component is used as the list UI. + * The ChannelList renders a ChannelListView which provides the UI for the underlying React Native FlatList. * * @example ./ChannelList.md */ @@ -256,15 +222,7 @@ export const ChannelList = (props: ChannelListProps) => { const { additionalFlatListProps = {}, channelRenderFilterFn, - EmptyStateIndicator = EmptyStateIndicatorDefault, filters = DEFAULT_FILTERS, - FooterLoadingIndicator = ChannelListFooterLoadingIndicator, - HeaderErrorIndicator = ChannelListHeaderErrorIndicator, - HeaderNetworkDownIndicator = ChannelListHeaderNetworkDownIndicator, - List = ChannelListView, - ListHeaderComponent, - LoadingErrorIndicator = LoadingErrorIndicatorDefault, - LoadingIndicator = ChannelListLoadingIndicator, // https://stackoverflow.com/a/60666252/10826415 loadMoreThreshold = 0.1, lockChannelOrder = false, @@ -282,20 +240,8 @@ export const ChannelList = (props: ChannelListProps) => { onRemovedFromChannel, onSelect, options = DEFAULT_OPTIONS, - Preview = ChannelPreviewView, getChannelActionItems, - PreviewAvatar, - PreviewMessage, - PreviewMutedStatus, - PreviewLastMessage, - PreviewStatus, - PreviewTitle, - PreviewUnreadCount, - PreviewTypingIndicator, - PreviewMessageDeliveryStatus, - ChannelDetailsBottomSheet, setFlatListRef, - Skeleton = SkeletonDefault, sort = DEFAULT_SORT, queryChannelsOverride, mutedStatusPosition = 'inlineTitle', @@ -398,35 +344,17 @@ export const ChannelList = (props: ChannelListProps) => { additionalFlatListProps, channelListInitialized, channels: channelRenderFilterFn ? channelRenderFilterFn(channels ?? []) : channels, - EmptyStateIndicator, error, - FooterLoadingIndicator, forceUpdate, hasNextPage, - HeaderErrorIndicator, - HeaderNetworkDownIndicator, - ListHeaderComponent, loadingChannels, - LoadingErrorIndicator, - LoadingIndicator, loadingNextPage, loadMoreThreshold, loadNextPage, maxUnreadCount, numberOfSkeletons, onSelect, - Preview, getChannelActionItems, - PreviewAvatar, - PreviewMessage, - PreviewMutedStatus, - PreviewStatus, - PreviewTitle, - PreviewUnreadCount, - PreviewTypingIndicator, - PreviewMessageDeliveryStatus, - ChannelDetailsBottomSheet, - PreviewLastMessage, swipeActionsEnabled, refreshing, refreshList, @@ -436,14 +364,13 @@ export const ChannelList = (props: ChannelListProps) => { setFlatListRef(ref); } }, - Skeleton, mutedStatusPosition, }); return ( - + ); diff --git a/package/src/components/ChannelList/ChannelListLoadingIndicator.tsx b/package/src/components/ChannelList/ChannelListLoadingIndicator.tsx index 2780811312..5949a4bf53 100644 --- a/package/src/components/ChannelList/ChannelListLoadingIndicator.tsx +++ b/package/src/components/ChannelList/ChannelListLoadingIndicator.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { StyleSheet, View } from 'react-native'; import { useChannelsContext } from '../../contexts/channelsContext/ChannelsContext'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; const styles = StyleSheet.create({ @@ -16,7 +17,8 @@ export const ChannelListLoadingIndicator = () => { channelListLoadingIndicator: { container }, }, } = useTheme(); - const { numberOfSkeletons, Skeleton } = useChannelsContext(); + const { numberOfSkeletons } = useChannelsContext(); + const { Skeleton } = useComponentsContext(); return ( diff --git a/package/src/components/ChannelList/ChannelListView.tsx b/package/src/components/ChannelList/ChannelListView.tsx index 5655904571..5b64048ee1 100644 --- a/package/src/components/ChannelList/ChannelListView.tsx +++ b/package/src/components/ChannelList/ChannelListView.tsx @@ -10,6 +10,7 @@ import { useChannelsContext, } from '../../contexts/channelsContext/ChannelsContext'; import { useChatContext } from '../../contexts/chatContext/ChatContext'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { useDebugContext } from '../../contexts/debugContext/DebugContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; @@ -18,24 +19,14 @@ import { ChannelPreview } from '../ChannelPreview/ChannelPreview'; export type ChannelListViewPropsWithContext = Omit< ChannelsContextValue, - | 'HeaderErrorIndicator' - | 'HeaderNetworkDownIndicator' - | 'maxUnreadCount' - | 'numberOfSkeletons' - | 'onSelect' - | 'Preview' - | 'PreviewTitle' - | 'PreviewStatus' - | 'PreviewAvatar' - | 'previewMessage' - | 'Skeleton' + 'maxUnreadCount' | 'numberOfSkeletons' | 'onSelect' >; const StatusIndicator = () => { const { isOnline } = useChatContext(); const styles = useStyles(); - const { error, HeaderErrorIndicator, HeaderNetworkDownIndicator, loadingChannels, refreshList } = - useChannelsContext(); + const { error, loadingChannels, refreshList } = useChannelsContext(); + const { HeaderErrorIndicator, HeaderNetworkDownIndicator } = useComponentsContext(); if (loadingChannels) { return null; @@ -67,15 +58,10 @@ const ChannelListViewWithContext = (props: ChannelListViewPropsWithContext) => { additionalFlatListProps, channelListInitialized, channels, - EmptyStateIndicator, error, - FooterLoadingIndicator, forceUpdate, hasNextPage, - ListHeaderComponent, loadingChannels, - LoadingErrorIndicator, - LoadingIndicator, loadingNextPage, loadMoreThreshold, loadNextPage, @@ -84,6 +70,13 @@ const ChannelListViewWithContext = (props: ChannelListViewPropsWithContext) => { reloadList, setFlatListRef, } = props; + const { + EmptyStateIndicator, + FooterLoadingIndicator, + ListHeaderComponent, + LoadingErrorIndicator, + LoadingIndicator, + } = useComponentsContext(); /** * In order to prevent the EmptyStateIndicator component from showing up briefly on mount, @@ -180,15 +173,10 @@ export const ChannelListView = (props: ChannelListViewProps) => { additionalFlatListProps, channelListInitialized, channels, - EmptyStateIndicator, error, - FooterLoadingIndicator, forceUpdate, hasNextPage, - ListHeaderComponent, loadingChannels, - LoadingErrorIndicator, - LoadingIndicator, loadingNextPage, loadMoreThreshold, loadNextPage, @@ -204,15 +192,10 @@ export const ChannelListView = (props: ChannelListViewProps) => { additionalFlatListProps, channelListInitialized, channels, - EmptyStateIndicator, error, - FooterLoadingIndicator, forceUpdate, hasNextPage, - ListHeaderComponent, loadingChannels, - LoadingErrorIndicator, - LoadingIndicator, loadingNextPage, loadMoreThreshold, loadNextPage, diff --git a/package/src/components/ChannelList/hooks/useCreateChannelsContext.ts b/package/src/components/ChannelList/hooks/useCreateChannelsContext.ts index 7badceb7a0..8ef2683e30 100644 --- a/package/src/components/ChannelList/hooks/useCreateChannelsContext.ts +++ b/package/src/components/ChannelList/hooks/useCreateChannelsContext.ts @@ -6,41 +6,22 @@ export const useCreateChannelsContext = ({ additionalFlatListProps, channelListInitialized, channels, - EmptyStateIndicator, error, - FooterLoadingIndicator, forceUpdate, hasNextPage, - HeaderErrorIndicator, - HeaderNetworkDownIndicator, - ListHeaderComponent, loadingChannels, - LoadingErrorIndicator, - LoadingIndicator, loadingNextPage, loadMoreThreshold, loadNextPage, maxUnreadCount, numberOfSkeletons, onSelect, - Preview, getChannelActionItems, - PreviewAvatar, - PreviewMessage, - PreviewMutedStatus, - PreviewStatus, - PreviewTitle, - PreviewLastMessage, - PreviewTypingIndicator, - PreviewMessageDeliveryStatus, - PreviewUnreadCount, - ChannelDetailsBottomSheet, swipeActionsEnabled, refreshing, refreshList, reloadList, setFlatListRef, - Skeleton, mutedStatusPosition, }: ChannelsContextValue) => { const channelValueString = channels @@ -58,41 +39,22 @@ export const useCreateChannelsContext = ({ additionalFlatListProps, channelListInitialized, channels, - EmptyStateIndicator, error, - FooterLoadingIndicator, forceUpdate, hasNextPage, - HeaderErrorIndicator, - HeaderNetworkDownIndicator, - ListHeaderComponent, loadingChannels, - LoadingErrorIndicator, - LoadingIndicator, loadingNextPage, loadMoreThreshold, loadNextPage, maxUnreadCount, numberOfSkeletons, onSelect, - Preview, getChannelActionItems, - PreviewAvatar, - PreviewMessage, - PreviewMutedStatus, - PreviewStatus, - PreviewTitle, - PreviewUnreadCount, - PreviewTypingIndicator, - PreviewMessageDeliveryStatus, - PreviewLastMessage, - ChannelDetailsBottomSheet, swipeActionsEnabled, refreshing, refreshList, reloadList, setFlatListRef, - Skeleton, mutedStatusPosition, }), // eslint-disable-next-line react-hooks/exhaustive-deps @@ -104,7 +66,6 @@ export const useCreateChannelsContext = ({ loadingChannels, loadingNextPage, channelListInitialized, - ChannelDetailsBottomSheet, swipeActionsEnabled, refreshing, mutedStatusPosition, diff --git a/package/src/components/ChannelPreview/ChannelPreview.tsx b/package/src/components/ChannelPreview/ChannelPreview.tsx index 3844f96a4e..3dc3efbc85 100644 --- a/package/src/components/ChannelPreview/ChannelPreview.tsx +++ b/package/src/components/ChannelPreview/ChannelPreview.tsx @@ -10,10 +10,11 @@ import { useChannelsContext, } from '../../contexts/channelsContext/ChannelsContext'; import { ChatContextValue, useChatContext } from '../../contexts/chatContext/ChatContext'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { useTranslatedMessage } from '../../hooks/useTranslatedMessage'; export type ChannelPreviewProps = Partial> & - Partial> & { + Partial> & { /** * Instance of Channel from stream-chat package. */ @@ -21,18 +22,13 @@ export type ChannelPreviewProps = Partial> & }; export const ChannelPreview = (props: ChannelPreviewProps) => { - const { channel, client: propClient, forceUpdate: propForceUpdate, Preview: propPreview } = props; + const { channel, client: propClient, forceUpdate: propForceUpdate } = props; const { client: contextClient } = useChatContext(); - const { - Preview: contextPreview, - ChannelDetailsBottomSheet, - getChannelActionItems, - swipeActionsEnabled, - } = useChannelsContext(); + const { getChannelActionItems, swipeActionsEnabled } = useChannelsContext(); + const { ChannelDetailsBottomSheet, Preview } = useComponentsContext(); const client = propClient || contextClient; - const Preview = propPreview || contextPreview; const { muted, unread, lastMessage } = useChannelPreviewData(channel, client, propForceUpdate); diff --git a/package/src/components/ChannelPreview/ChannelPreviewMessage.tsx b/package/src/components/ChannelPreview/ChannelPreviewMessage.tsx index 98509ee6c9..6aa2dec078 100644 --- a/package/src/components/ChannelPreview/ChannelPreviewMessage.tsx +++ b/package/src/components/ChannelPreview/ChannelPreviewMessage.tsx @@ -1,11 +1,8 @@ import React, { useMemo } from 'react'; import { StyleSheet, Text, View } from 'react-native'; -import { ChannelLastMessagePreview } from './ChannelLastMessagePreview'; -import { ChannelMessagePreviewDeliveryStatus } from './ChannelMessagePreviewDeliveryStatus'; import { ChannelPreviewProps } from './ChannelPreview'; -import { ChannelPreviewTypingIndicator } from './ChannelPreviewTypingIndicator'; import { LastMessageType } from './hooks/useChannelPreviewData'; import { useChannelPreviewDraftMessage } from './hooks/useChannelPreviewDraftMessage'; @@ -13,11 +10,8 @@ import { useChannelPreviewPollLabel } from './hooks/useChannelPreviewPollLabel'; import { useChannelTypingState } from './hooks/useChannelTypingState'; -import { - ChannelsContextValue, - useChannelsContext, -} from '../../contexts/channelsContext/ChannelsContext'; import { useChatContext } from '../../contexts/chatContext/ChatContext'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { useTranslationContext } from '../../contexts/translationContext/TranslationContext'; @@ -26,34 +20,14 @@ import { primitives } from '../../theme'; import { MessageStatusTypes } from '../../utils/utils'; import { ErrorBadge } from '../ui'; -export type ChannelPreviewMessageProps = Pick & - Partial< - Pick< - ChannelsContextValue, - 'PreviewTypingIndicator' | 'PreviewMessageDeliveryStatus' | 'PreviewLastMessage' - > - > & { - lastMessage?: LastMessageType; - }; +export type ChannelPreviewMessageProps = Pick & { + lastMessage?: LastMessageType; +}; export const ChannelPreviewMessage = (props: ChannelPreviewMessageProps) => { - const { - channel, - lastMessage, - PreviewTypingIndicator: PreviewTypingIndicatorProp = ChannelPreviewTypingIndicator, - PreviewMessageDeliveryStatus: - PreviewMessageDeliveryStatusProp = ChannelMessagePreviewDeliveryStatus, - PreviewLastMessage: PreviewLastMessageProp = ChannelLastMessagePreview, - } = props; - const { - PreviewTypingIndicator: PreviewTypingIndicatorContext, - PreviewMessageDeliveryStatus: PreviewMessageDeliveryStatusContext, - PreviewLastMessage: PreviewLastMessageContext, - } = useChannelsContext(); - const PreviewTypingIndicator = PreviewTypingIndicatorProp || PreviewTypingIndicatorContext; - const PreviewMessageDeliveryStatus = - PreviewMessageDeliveryStatusProp || PreviewMessageDeliveryStatusContext; - const PreviewLastMessage = PreviewLastMessageProp || PreviewLastMessageContext; + const { channel, lastMessage } = props; + const { PreviewTypingIndicator, PreviewMessageDeliveryStatus, PreviewLastMessage } = + useComponentsContext(); const { theme: { semantics }, } = useTheme(); diff --git a/package/src/components/ChannelPreview/ChannelPreviewView.tsx b/package/src/components/ChannelPreview/ChannelPreviewView.tsx index acf231abcd..2c58b34c1e 100644 --- a/package/src/components/ChannelPreview/ChannelPreviewView.tsx +++ b/package/src/components/ChannelPreview/ChannelPreviewView.tsx @@ -2,11 +2,6 @@ import React, { useMemo } from 'react'; import { Pressable, StyleSheet, View } from 'react-native'; import { ChannelPreviewProps } from './ChannelPreview'; -import { ChannelPreviewMessage } from './ChannelPreviewMessage'; -import { ChannelPreviewMutedStatus } from './ChannelPreviewMutedStatus'; -import { ChannelPreviewStatus } from './ChannelPreviewStatus'; -import { ChannelPreviewTitle } from './ChannelPreviewTitle'; -import { ChannelPreviewUnreadCount } from './ChannelPreviewUnreadCount'; import { LastMessageType } from './hooks/useChannelPreviewData'; @@ -14,25 +9,14 @@ import { ChannelsContextValue, useChannelsContext, } from '../../contexts/channelsContext/ChannelsContext'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { useSwipeRegistryContext } from '../../contexts/swipeableContext/SwipeRegistryContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { useStableCallback } from '../../hooks'; import { primitives } from '../../theme'; -import { ChannelAvatar } from '../ui/Avatar/ChannelAvatar'; export type ChannelPreviewViewPropsWithContext = Pick & - Pick< - ChannelsContextValue, - | 'maxUnreadCount' - | 'onSelect' - | 'PreviewAvatar' - | 'PreviewMessage' - | 'PreviewMutedStatus' - | 'PreviewStatus' - | 'PreviewTitle' - | 'PreviewUnreadCount' - | 'mutedStatusPosition' - > & { + Pick & { /** * Formatter function for date of latest message. * @param date Message date @@ -57,16 +41,18 @@ const ChannelPreviewViewWithContext = (props: ChannelPreviewViewPropsWithContext maxUnreadCount, muted, onSelect, - PreviewAvatar = ChannelAvatar, - PreviewMessage = ChannelPreviewMessage, - PreviewMutedStatus = ChannelPreviewMutedStatus, - PreviewStatus = ChannelPreviewStatus, - PreviewTitle = ChannelPreviewTitle, - PreviewUnreadCount = ChannelPreviewUnreadCount, unread, mutedStatusPosition, lastMessage, } = props; + const { + PreviewAvatar, + PreviewMessage, + PreviewMutedStatus, + PreviewStatus, + PreviewTitle, + PreviewUnreadCount, + } = useComponentsContext(); const { theme: { @@ -158,28 +144,13 @@ const MemoizedChannelPreviewViewWithContext = React.memo( * from the ChannelPreview component. */ export const ChannelPreviewView = (props: ChannelPreviewViewProps) => { - const { - forceUpdate, - maxUnreadCount, - onSelect, - PreviewMessage, - PreviewMutedStatus, - PreviewStatus, - PreviewTitle, - PreviewUnreadCount, - mutedStatusPosition, - } = useChannelsContext(); + const { forceUpdate, maxUnreadCount, onSelect, mutedStatusPosition } = useChannelsContext(); return ( & Pick & Pick & { @@ -289,11 +278,8 @@ const MessageWithContext = (props: MessagePropsWithContext) => { members, message, messageActions: messageActionsProp = defaultMessageActions, - MessageBlocked, - MessageBounce, messageContentOrder: messageContentOrderProp, messagesContext, - MessageItemView, onLongPressMessage: onLongPressMessageProp, onPressInMessage: onPressInMessageProp, onPressMessage: onPressMessageProp, @@ -314,13 +300,15 @@ const MessageWithContext = (props: MessagePropsWithContext) => { updateMessage, readBy, setQuotedMessage, - MessageUserReactions, - MessageUserReactionsAvatar, - MessageUserReactionsItem, - MessageReactionPicker, - MessageActionList, - MessageActionListItem, } = props; + const { + MessageActionList, + MessageBlocked, + MessageBounce, + MessageItemView, + MessageReactionPicker, + MessageUserReactions, + } = useComponentsContext(); // TODO: V9: Reconsider using safe area insets in every message. const insets = useSafeAreaInsets(); const isMessageAIGenerated = messagesContext.isMessageAIGenerated; @@ -890,8 +878,6 @@ const MessageWithContext = (props: MessagePropsWithContext) => { > @@ -914,7 +900,6 @@ const MessageWithContext = (props: MessagePropsWithContext) => { > diff --git a/package/src/components/Message/MessageItemView/MessageBubble.tsx b/package/src/components/Message/MessageItemView/MessageBubble.tsx index f945368bec..d594ed3236 100644 --- a/package/src/components/Message/MessageItemView/MessageBubble.tsx +++ b/package/src/components/Message/MessageItemView/MessageBubble.tsx @@ -12,13 +12,14 @@ import Animated, { import { MessageItemViewPropsWithContext } from './MessageItemView'; -import { MessagesContextValue, useTheme } from '../../../contexts'; +import { useTheme } from '../../../contexts'; +import type { ComponentOverrides } from '../../../contexts/componentsContext/ComponentsContext'; import { NativeHandlers } from '../../../native'; const AnimatedWrapper = Animated.createAnimatedComponent(View); -type SwipableMessageWrapperProps = Pick & +type SwipableMessageWrapperProps = Pick, 'MessageSwipeContent'> & Pick & { children: ReactNode; onSwipe: () => void; diff --git a/package/src/components/Message/MessageItemView/MessageContent.tsx b/package/src/components/Message/MessageItemView/MessageContent.tsx index cb65156006..f1316042bf 100644 --- a/package/src/components/Message/MessageItemView/MessageContent.tsx +++ b/package/src/components/Message/MessageItemView/MessageContent.tsx @@ -4,6 +4,7 @@ import { AnimatableNumericValue, ColorValue, Pressable, StyleSheet, View } from import { MessageTextContainer } from './MessageTextContainer'; import { useChatContext } from '../../../contexts'; +import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext'; import { MessageContextValue, useMessageContext, @@ -67,19 +68,9 @@ export type MessageContentPropsWithContext = Pick< Pick< MessagesContextValue, | 'additionalPressableProps' - | 'Attachment' | 'enableMessageGroupingByUser' - | 'FileAttachmentGroup' - | 'Gallery' | 'isAttachmentEqual' - | 'MessageContentBottomView' - | 'MessageContentLeadingView' - | 'MessageLocation' - | 'MessageContentTrailingView' - | 'MessageContentTopView' | 'myMessageTheme' - | 'Reply' - | 'StreamingMessageView' > & Pick & { /** @@ -115,37 +106,38 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => { const { additionalPressableProps, alignment, - Attachment, backgroundColor, enableMessageGroupingByUser, - FileAttachmentGroup, - Gallery, groupStyles, isMessageAIGenerated, isMyMessage, isVeryLastMessage, message, messageContentOrder, - MessageContentBottomView, - MessageContentLeadingView, messageGroupedSingleOrBottom = false, - MessageLocation, - MessageContentTrailingView, - MessageContentTopView, noBorder, onLongPress, onPress, onPressIn, otherAttachments, preventPress, - Reply, - StreamingMessageView, hidePaddingTop, hidePaddingHorizontal, hidePaddingBottom, } = props; const { client } = useChatContext(); - const { PollContent: PollContentOverride } = useMessagesContext(); + const { + Attachment, + FileAttachmentGroup, + Gallery, + MessageContentBottomView, + MessageContentLeadingView, + MessageContentTopView, + MessageContentTrailingView, + MessageLocation, + Reply, + StreamingMessageView, + } = useComponentsContext(); const replyStyles = useReplyStyles(); const { @@ -266,12 +258,7 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => { const pollId = message.poll_id; const poll = pollId && client.polls.fromState(pollId); return pollId && poll ? ( - + ) : null; } case 'location': @@ -560,19 +547,9 @@ export const MessageContent = (props: MessageContentProps) => { } = useMessageContext(); const { additionalPressableProps, - Attachment, enableMessageGroupingByUser, - FileAttachmentGroup, - Gallery, isAttachmentEqual, - MessageContentBottomView, - MessageContentLeadingView, - MessageLocation, - MessageContentTrailingView, - MessageContentTopView, myMessageTheme, - Reply, - StreamingMessageView, } = useMessagesContext(); const { t } = useTranslationContext(); const isSingleFile = files.length === 1; @@ -613,10 +590,7 @@ export const MessageContent = (props: MessageContentProps) => { {...{ additionalPressableProps, alignment, - Attachment, enableMessageGroupingByUser, - FileAttachmentGroup, - Gallery, goToMessage, groupStyles, isAttachmentEqual, @@ -624,19 +598,12 @@ export const MessageContent = (props: MessageContentProps) => { isMyMessage, message, messageContentOrder, - MessageContentBottomView, - MessageContentLeadingView, - MessageLocation, - MessageContentTrailingView, - MessageContentTopView, myMessageTheme, onLongPress, onPress, onPressIn, otherAttachments, preventPress, - Reply, - StreamingMessageView, t, threadList, hidePaddingTop, diff --git a/package/src/components/Message/MessageItemView/MessageDeleted.tsx b/package/src/components/Message/MessageItemView/MessageDeleted.tsx index be907db63d..4515b5095f 100644 --- a/package/src/components/Message/MessageItemView/MessageDeleted.tsx +++ b/package/src/components/Message/MessageItemView/MessageDeleted.tsx @@ -1,14 +1,11 @@ import React, { useMemo } from 'react'; import { StyleSheet, Text, View } from 'react-native'; +import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext'; import { MessageContextValue, useMessageContext, } from '../../../contexts/messageContext/MessageContext'; -import { - MessagesContextValue, - useMessagesContext, -} from '../../../contexts/messagesContext/MessagesContext'; import { useTheme } from '../../../contexts/themeContext/ThemeContext'; import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext'; import { CircleBan } from '../../../icons/no-sign'; @@ -20,11 +17,11 @@ type MessageDeletedComponentProps = { }; type MessageDeletedPropsWithContext = Pick & - Pick & MessageDeletedComponentProps; const MessageDeletedWithContext = (props: MessageDeletedPropsWithContext) => { - const { alignment, date, groupStyle, MessageFooter } = props; + const { alignment, date, groupStyle } = props; + const { MessageFooter } = useComponentsContext(); const { theme: { @@ -111,14 +108,11 @@ export type MessageDeletedProps = Partial & { export const MessageDeleted = (props: MessageDeletedProps) => { const { alignment, message } = useMessageContext(); - const { MessageFooter } = useMessagesContext(); - return ( diff --git a/package/src/components/Message/MessageItemView/MessageFooter.tsx b/package/src/components/Message/MessageItemView/MessageFooter.tsx index c0ada724f9..779f553e1f 100644 --- a/package/src/components/Message/MessageItemView/MessageFooter.tsx +++ b/package/src/components/Message/MessageItemView/MessageFooter.tsx @@ -3,18 +3,13 @@ import { StyleSheet, Text, View } from 'react-native'; import type { Attachment, LocalMessage } from 'stream-chat'; -import type { MessageStatusProps } from './MessageStatus'; - import type { ChannelContextValue } from '../../../contexts/channelContext/ChannelContext'; +import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext'; import { Alignment, MessageContextValue, useMessageContext, } from '../../../contexts/messageContext/MessageContext'; -import { - MessagesContextValue, - useMessagesContext, -} from '../../../contexts/messagesContext/MessagesContext'; import { useTheme } from '../../../contexts/themeContext/ThemeContext'; import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext'; @@ -37,7 +32,6 @@ type MessageFooterPropsWithContext = Pick< | 'lastGroupMessage' | 'isMessageAIGenerated' > & - Pick & MessageFooterComponentProps; const MessageFooterWithContext = (props: MessageFooterPropsWithContext) => { @@ -49,10 +43,9 @@ const MessageFooterWithContext = (props: MessageFooterPropsWithContext) => { lastGroupMessage, members, message, - MessageStatus, - MessageTimestamp, showMessageStatus, } = props; + const { MessageStatus, MessageTimestamp } = useComponentsContext(); const styles = useStyles(); const { @@ -169,7 +162,6 @@ export type MessageFooterProps = Partial> & alignment?: Alignment; lastGroupMessage?: boolean; message?: LocalMessage; - MessageStatus?: React.ComponentType; otherAttachments?: Attachment[]; showMessageStatus?: boolean; }; @@ -178,8 +170,6 @@ export const MessageFooter = (props: MessageFooterProps) => { const { alignment, isMessageAIGenerated, lastGroupMessage, members, message, showMessageStatus } = useMessageContext(); - const { MessageStatus, MessageTimestamp } = useMessagesContext(); - return ( { lastGroupMessage, members, message, - MessageStatus, - MessageTimestamp, showMessageStatus, }} {...props} diff --git a/package/src/components/Message/MessageItemView/MessageHeader.tsx b/package/src/components/Message/MessageItemView/MessageHeader.tsx index 0762b38c9f..74f2e29e2d 100644 --- a/package/src/components/Message/MessageItemView/MessageHeader.tsx +++ b/package/src/components/Message/MessageItemView/MessageHeader.tsx @@ -2,43 +2,35 @@ import React, { useMemo } from 'react'; import { View, ViewStyle } from 'react-native'; +import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext'; import { MessageContextValue, useMessageContext, } from '../../../contexts/messageContext/MessageContext'; -import { - MessagesContextValue, - useMessagesContext, -} from '../../../contexts/messagesContext/MessagesContext'; import { useMessageReminder } from '../../../hooks/useMessageReminder'; -type MessageHeaderPropsWithContext = Pick & - Pick< - MessagesContextValue, - | 'MessagePinnedHeader' - | 'MessageReminderHeader' - | 'MessageSavedForLaterHeader' - | 'SentToChannelHeader' - > & { - shouldShowSavedForLaterHeader?: boolean; - shouldShowPinnedHeader: boolean; - shouldShowReminderHeader: boolean; - shouldShowSentToChannelHeader: boolean; - }; +type MessageHeaderPropsWithContext = Pick & { + shouldShowSavedForLaterHeader?: boolean; + shouldShowPinnedHeader: boolean; + shouldShowReminderHeader: boolean; + shouldShowSentToChannelHeader: boolean; +}; const MessageHeaderWithContext = (props: MessageHeaderPropsWithContext) => { const { alignment, message, - MessagePinnedHeader, shouldShowSavedForLaterHeader, shouldShowPinnedHeader, shouldShowReminderHeader, shouldShowSentToChannelHeader, + } = props; + const { + MessagePinnedHeader, MessageReminderHeader, MessageSavedForLaterHeader, SentToChannelHeader, - } = props; + } = useComponentsContext(); const containerStyle: ViewStyle = useMemo(() => { return { @@ -115,12 +107,6 @@ export type MessageHeaderProps = Partial>; export const MessageHeader = (props: MessageHeaderProps) => { const { alignment, message } = useMessageContext(); - const { - MessagePinnedHeader, - MessageReminderHeader, - MessageSavedForLaterHeader, - SentToChannelHeader, - } = useMessagesContext(); const reminder = useMessageReminder(message.id); const shouldShowSavedForLaterHeader = reminder && !reminder.remindAt; @@ -141,14 +127,10 @@ export const MessageHeader = (props: MessageHeaderProps) => { ); diff --git a/package/src/components/Message/MessageItemView/MessageItemView.tsx b/package/src/components/Message/MessageItemView/MessageItemView.tsx index b21f5a477f..5962509b6f 100644 --- a/package/src/components/Message/MessageItemView/MessageItemView.tsx +++ b/package/src/components/Message/MessageItemView/MessageItemView.tsx @@ -3,6 +3,7 @@ import { Dimensions, StyleSheet, View, ViewStyle } from 'react-native'; import { SwipableMessageWrapper } from './MessageBubble'; +import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext'; import { Alignment, MessageContextValue, @@ -214,20 +215,9 @@ export type MessageItemViewPropsWithContext = Pick< | 'enableMessageGroupingByUser' | 'enableSwipeToReply' | 'myMessageTheme' - | 'MessageAuthor' - | 'MessageContent' - | 'MessageDeleted' - | 'MessageError' - | 'MessageFooter' - | 'MessageHeader' - | 'MessageReplies' - | 'MessageSpacer' - | 'MessageSwipeContent' | 'messageSwipeToReplyHitSlop' - | 'ReactionListBottom' | 'reactionListPosition' | 'reactionListType' - | 'ReactionListTop' >; const MessageItemViewWithContext = (props: MessageItemViewPropsWithContext) => { @@ -242,6 +232,14 @@ const MessageItemViewWithContext = (props: MessageItemViewPropsWithContext) => { groupStyles, isMyMessage, message, + messageSwipeToReplyHitSlop = { left: width, right: width }, + onlyEmojis, + otherAttachments, + reactionListPosition, + reactionListType, + setQuotedMessage, + } = props; + const { MessageAuthor, MessageContent, MessageDeleted, @@ -251,15 +249,9 @@ const MessageItemViewWithContext = (props: MessageItemViewPropsWithContext) => { MessageReplies, MessageSpacer, MessageSwipeContent, - messageSwipeToReplyHitSlop = { left: width, right: width }, - onlyEmojis, - otherAttachments, ReactionListBottom, - reactionListPosition, - reactionListType, ReactionListTop, - setQuotedMessage, - } = props; + } = useComponentsContext(); const { theme: { @@ -533,21 +525,10 @@ export const MessageItemView = (props: MessageItemViewProps) => { customMessageSwipeAction, enableMessageGroupingByUser, enableSwipeToReply, - MessageAuthor, - MessageContent, - MessageDeleted, - MessageError, - MessageFooter, - MessageHeader, - MessageReplies, - MessageSpacer, - MessageSwipeContent, messageSwipeToReplyHitSlop, myMessageTheme, - ReactionListBottom, reactionListPosition, reactionListType, - ReactionListTop, } = useMessagesContext(); return ( @@ -562,23 +543,12 @@ export const MessageItemView = (props: MessageItemViewProps) => { groupStyles, isMyMessage, message, - MessageAuthor, - MessageContent, - MessageDeleted, - MessageError, - MessageFooter, - MessageHeader, - MessageReplies, - MessageSpacer, - MessageSwipeContent, messageSwipeToReplyHitSlop, myMessageTheme, onlyEmojis, otherAttachments, - ReactionListBottom, reactionListPosition, reactionListType, - ReactionListTop, setQuotedMessage, lastGroupMessage, members, diff --git a/package/src/components/Message/MessageItemView/MessageReplies.tsx b/package/src/components/Message/MessageItemView/MessageReplies.tsx index 42649c1a47..3e7f1ae654 100644 --- a/package/src/components/Message/MessageItemView/MessageReplies.tsx +++ b/package/src/components/Message/MessageItemView/MessageReplies.tsx @@ -1,14 +1,11 @@ import React, { useMemo } from 'react'; import { I18nManager, Pressable, StyleSheet, Text, View } from 'react-native'; +import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext'; import { MessageContextValue, useMessageContext, } from '../../../contexts/messageContext/MessageContext'; -import { - MessagesContextValue, - useMessagesContext, -} from '../../../contexts/messagesContext/MessagesContext'; import { useTheme } from '../../../contexts/themeContext/ThemeContext'; import { TranslationContextValue, @@ -31,7 +28,6 @@ export type MessageRepliesPropsWithContext = Pick< | 'preventPress' | 'threadList' > & - Pick & Pick; const MessageRepliesWithContext = (props: MessageRepliesPropsWithContext) => { @@ -39,7 +35,6 @@ const MessageRepliesWithContext = (props: MessageRepliesPropsWithContext) => { alignment, isMyMessage, message, - MessageRepliesAvatars, onLongPress, onOpenThread, onPress, @@ -48,6 +43,7 @@ const MessageRepliesWithContext = (props: MessageRepliesPropsWithContext) => { t, threadList, } = props; + const { MessageRepliesAvatars } = useComponentsContext(); const { theme: { @@ -190,7 +186,6 @@ export const MessageReplies = (props: MessageRepliesProps) => { preventPress, threadList, } = useMessageContext(); - const { MessageRepliesAvatars } = useMessagesContext(); const { t } = useTranslationContext(); return ( @@ -199,7 +194,6 @@ export const MessageReplies = (props: MessageRepliesProps) => { alignment, isMyMessage, message, - MessageRepliesAvatars, onLongPress, onOpenThread, onPress, diff --git a/package/src/components/Message/MessageItemView/MessageTextContainer.tsx b/package/src/components/Message/MessageItemView/MessageTextContainer.tsx index 704121be4a..bc2d06f7ce 100644 --- a/package/src/components/Message/MessageItemView/MessageTextContainer.tsx +++ b/package/src/components/Message/MessageItemView/MessageTextContainer.tsx @@ -5,6 +5,7 @@ import { LocalMessage } from 'stream-chat'; import { renderText, RenderTextParams } from './utils/renderText'; +import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext'; import { MessageContextValue, useMessageContext, @@ -32,10 +33,7 @@ export type MessageTextContainerPropsWithContext = Pick< MessageContextValue, 'message' | 'onLongPress' | 'onlyEmojis' | 'onPress' | 'preventPress' | 'isMyMessage' > & - Pick< - MessagesContextValue, - 'markdownRules' | 'MessageText' | 'myMessageTheme' | 'messageTextNumberOfLines' - > & { + Pick & { markdownStyles?: MarkdownStyle; messageOverlay?: boolean; styles?: Partial<{ @@ -52,7 +50,6 @@ const MessageTextContainerWithContext = (props: MessageTextContainerPropsWithCon markdownStyles: markdownStylesProp = {}, message, messageOverlay, - MessageText, messageTextNumberOfLines, onLongPress, onlyEmojis, @@ -60,6 +57,7 @@ const MessageTextContainerWithContext = (props: MessageTextContainerPropsWithCon preventPress, styles: stylesProp = {}, } = props; + const { MessageText } = useComponentsContext(); const { theme: { @@ -186,8 +184,7 @@ export type MessageTextContainerProps = Partial { const { message, onLongPress, onlyEmojis, onPress, preventPress, isMyMessage } = useMessageContext(); - const { markdownRules, MessageText, messageTextNumberOfLines, myMessageTheme } = - useMessagesContext(); + const { markdownRules, messageTextNumberOfLines, myMessageTheme } = useMessagesContext(); return ( { markdownRules, message, isMyMessage, - MessageText, messageTextNumberOfLines, myMessageTheme, onLongPress, diff --git a/package/src/components/Message/MessageItemView/MessageWrapper.tsx b/package/src/components/Message/MessageItemView/MessageWrapper.tsx index 69ffb6aea1..5c27caa18f 100644 --- a/package/src/components/Message/MessageItemView/MessageWrapper.tsx +++ b/package/src/components/Message/MessageItemView/MessageWrapper.tsx @@ -8,6 +8,7 @@ import { useMessageDateSeparator } from '../../../components/MessageList/hooks/u import { useMessageGroupStyles } from '../../../components/MessageList/hooks/useMessageGroupStyles'; import { useChannelContext } from '../../../contexts/channelContext/ChannelContext'; import { useChatContext } from '../../../contexts/chatContext/ChatContext'; +import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext'; import { useMessageListItemContext } from '../../../contexts/messageListItemContext/MessageListItemContext'; import { useMessagesContext } from '../../../contexts/messagesContext/MessagesContext'; import { ThemeProvider, useTheme } from '../../../contexts/themeContext/ThemeContext'; @@ -40,15 +41,9 @@ export const MessageWrapper = React.memo((props: MessageWrapperProps) => { maxTimeBetweenGroupedMessages, threadList, } = useChannelContext(); - const { - getMessageGroupStyle, - InlineDateSeparator, - InlineUnreadIndicator, - Message, - MessageSystem, - myMessageTheme, - shouldShowUnreadUnderlay, - } = useMessagesContext(); + const { InlineDateSeparator, InlineUnreadIndicator, Message, MessageSystem } = + useComponentsContext(); + const { getMessageGroupStyle, myMessageTheme, shouldShowUnreadUnderlay } = useMessagesContext(); const { goToMessage, onThreadSelect, noGroupByUser, modifiedTheme } = useMessageListItemContext(); const dateSeparatorDate = useMessageDateSeparator({ diff --git a/package/src/components/Message/MessageItemView/ReactionList/ReactionListBottom.tsx b/package/src/components/Message/MessageItemView/ReactionList/ReactionListBottom.tsx index efabd0da4a..1cf02a99f2 100644 --- a/package/src/components/Message/MessageItemView/ReactionList/ReactionListBottom.tsx +++ b/package/src/components/Message/MessageItemView/ReactionList/ReactionListBottom.tsx @@ -3,6 +3,7 @@ import { FlatList, StyleSheet, View } from 'react-native'; import { ReactionListItemProps } from './ReactionListItem'; +import { useComponentsContext } from '../../../../contexts/componentsContext/ComponentsContext'; import { MessageContextValue, useMessageContext, @@ -29,9 +30,7 @@ export type ReactionListBottomProps = Partial< | 'showReactionsOverlay' > > & - Partial< - Pick - > & { + Partial> & { type?: 'clustered' | 'segmented'; showCount?: boolean; }; @@ -55,8 +54,6 @@ export const ReactionListBottom = (props: ReactionListBottomProps) => { supportedReactions: propSupportedReactions, type, showCount = true, - ReactionListClustered: propReactionListClustered, - ReactionListItem: propReactionListItem, } = props; const { @@ -71,11 +68,8 @@ export const ReactionListBottom = (props: ReactionListBottomProps) => { showReactionsOverlay: contextShowReactionsOverlay, } = useMessageContext(); - const { - supportedReactions: contextSupportedReactions, - ReactionListClustered: contextReactionListClustered, - ReactionListItem: contextReactionListItem, - } = useMessagesContext(); + const { ReactionListClustered, ReactionListItem } = useComponentsContext(); + const { supportedReactions: contextSupportedReactions } = useMessagesContext(); const alignment = propAlignment || contextAlignment; const handleReaction = propHandlerReaction || contextHandleReaction; @@ -87,8 +81,6 @@ export const ReactionListBottom = (props: ReactionListBottomProps) => { const reactions = propReactions || contextReactions; const showReactionsOverlay = propShowReactionsOverlay || contextShowReactionsOverlay; const supportedReactions = propSupportedReactions || contextSupportedReactions; - const ReactionListClustered = propReactionListClustered || contextReactionListClustered; - const ReactionListItem = propReactionListItem || contextReactionListItem; const renderItem = useCallback( ({ index, item }: { index: number; item: ReactionListItemProps }) => ( diff --git a/package/src/components/Message/MessageItemView/ReactionList/ReactionListTop.tsx b/package/src/components/Message/MessageItemView/ReactionList/ReactionListTop.tsx index eaad1d5bde..af407679ee 100644 --- a/package/src/components/Message/MessageItemView/ReactionList/ReactionListTop.tsx +++ b/package/src/components/Message/MessageItemView/ReactionList/ReactionListTop.tsx @@ -2,6 +2,7 @@ import React, { useMemo } from 'react'; import { ScrollView, StyleSheet, View } from 'react-native'; import { useTheme } from '../../../../contexts'; +import { useComponentsContext } from '../../../../contexts/componentsContext/ComponentsContext'; import { MessageContextValue, useMessageContext, @@ -26,14 +27,7 @@ export type ReactionListTopProps = Partial< | 'showReactionsOverlay' | 'handleReaction' > & - Pick< - MessagesContextValue, - | 'supportedReactions' - | 'reactionListType' - | 'ReactionListClustered' - | 'ReactionListItem' - | 'ReactionListCountItem' - > + Pick > & { type?: 'clustered' | 'segmented'; showCount?: boolean; @@ -56,9 +50,6 @@ export const ReactionListTop = (props: ReactionListTopProps) => { handleReaction: propHandleReaction, type, showCount = true, - ReactionListClustered: propReactionListClustered, - ReactionListItem: propReactionListItem, - ReactionListCountItem: propReactionListCountItem, } = props; const { @@ -73,12 +64,8 @@ export const ReactionListTop = (props: ReactionListTopProps) => { handleReaction: contextHandleReaction, } = useMessageContext(); - const { - supportedReactions: contextSupportedReactions, - ReactionListClustered: contextReactionListClustered, - ReactionListItem: contextReactionListItem, - ReactionListCountItem: contextReactionListCountItem, - } = useMessagesContext(); + const { ReactionListClustered, ReactionListCountItem, ReactionListItem } = useComponentsContext(); + const { supportedReactions: contextSupportedReactions } = useMessagesContext(); const alignment = propAlignment || contextAlignment; const hasReactions = propHasReactions || contextHasReactions; @@ -90,9 +77,6 @@ export const ReactionListTop = (props: ReactionListTopProps) => { const showReactionsOverlay = propShowReactionsOverlay || contextShowReactionsOverlay; const supportedReactions = propSupportedReactions || contextSupportedReactions; const handleReaction = propHandleReaction || contextHandleReaction; - const ReactionListClustered = propReactionListClustered || contextReactionListClustered; - const ReactionListItem = propReactionListItem || contextReactionListItem; - const ReactionListCountItem = propReactionListCountItem || contextReactionListCountItem; const styles = useStyles({ alignment }); diff --git a/package/src/components/Message/MessageItemView/__tests__/MessageContent.test.js b/package/src/components/Message/MessageItemView/__tests__/MessageContent.test.js index 21fbbeee46..e040b7a278 100644 --- a/package/src/components/Message/MessageItemView/__tests__/MessageContent.test.js +++ b/package/src/components/Message/MessageItemView/__tests__/MessageContent.test.js @@ -4,6 +4,7 @@ import { StyleSheet, View } from 'react-native'; import { cleanup, render, screen, waitFor } from '@testing-library/react-native'; import { ChannelsStateProvider } from '../../../../contexts/channelsStateContext/ChannelsStateContext'; +import { WithComponents } from '../../../../contexts/componentsContext/ComponentsContext'; import { getOrCreateChannelApi } from '../../../../mock-builders/api/getOrCreateChannel'; import { useMockedApis } from '../../../../mock-builders/api/useMockedApis'; @@ -112,9 +113,11 @@ describe('MessageContent', () => { render( - - - + + + + + , ); @@ -134,9 +137,11 @@ describe('MessageContent', () => { render( - - - + + + + + , ); @@ -154,13 +159,16 @@ describe('MessageContent', () => { render( - } - MessageContentTopView={() => } + , + MessageContentTopView: () => , + }} > - - + + + + , ); @@ -179,13 +187,16 @@ describe('MessageContent', () => { render( - } - MessageContentTrailingView={() => } + , + MessageContentTrailingView: () => , + }} > - - + + + + , ); @@ -205,13 +216,16 @@ describe('MessageContent', () => { const { rerender } = render( - } - MessageContentTrailingView={() => } + , + MessageContentTrailingView: () => , + }} > - - + + + + , ); @@ -227,13 +241,16 @@ describe('MessageContent', () => { rerender( - } - MessageContentTrailingView={() => } + , + MessageContentTrailingView: () => , + }} > - - + + + + , ); diff --git a/package/src/components/Message/MessageItemView/__tests__/MessageItemView.test.js b/package/src/components/Message/MessageItemView/__tests__/MessageItemView.test.js index 14d7e5a28a..c221b4bdf4 100644 --- a/package/src/components/Message/MessageItemView/__tests__/MessageItemView.test.js +++ b/package/src/components/Message/MessageItemView/__tests__/MessageItemView.test.js @@ -6,6 +6,7 @@ import { GestureDetector } from 'react-native-gesture-handler'; import { cleanup, render, screen, waitFor } from '@testing-library/react-native'; import { ChannelsStateProvider } from '../../../../contexts/channelsStateContext/ChannelsStateContext'; +import { WithComponents } from '../../../../contexts/componentsContext/ComponentsContext'; import { useMessageContext } from '../../../../contexts/messageContext/MessageContext'; import { getOrCreateChannelApi } from '../../../../mock-builders/api/getOrCreateChannel'; @@ -38,13 +39,21 @@ describe('MessageItemView', () => { useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]); channel = chatClient.channel('messaging', mockedChannel.id); - renderMessage = (options, channelProps) => + renderMessage = (options, channelProps, componentOverrides) => render( - - - + {componentOverrides ? ( + + + + + + ) : ( + + + + )} , ); @@ -132,7 +141,7 @@ describe('MessageItemView', () => { return Custom Message Item; }; - renderMessage({ message }, { MessageItemView: CustomMessageItemView }); + renderMessage({ message }, {}, { MessageItemView: CustomMessageItemView }); await waitFor(() => { expect(screen.queryByText('Custom Message Item')).not.toBeNull(); @@ -143,7 +152,7 @@ describe('MessageItemView', () => { const user = generateUser(); const message = generateMessage({ user }); - renderMessage({ message }, { MessageSpacer: () => Message Spacer }); + renderMessage({ message }, {}, { MessageSpacer: () => Message Spacer }); await waitFor(() => { expect(screen.queryByText('Message Spacer')).not.toBeNull(); @@ -189,7 +198,7 @@ describe('MessageItemView', () => { const user = generateUser(); const message = generateMessage({ user }); - renderMessage({ message }, { MessageHeader: () => Message Header }); + renderMessage({ message }, {}, { MessageHeader: () => Message Header }); await waitFor(() => { expect(screen.queryByText('Message Header')).not.toBeNull(); diff --git a/package/src/components/Message/MessageItemView/__tests__/MessageTextContainer.test.tsx b/package/src/components/Message/MessageItemView/__tests__/MessageTextContainer.test.tsx index ab18a2a654..2df6217d0a 100644 --- a/package/src/components/Message/MessageItemView/__tests__/MessageTextContainer.test.tsx +++ b/package/src/components/Message/MessageItemView/__tests__/MessageTextContainer.test.tsx @@ -5,6 +5,7 @@ import { cleanup, render, waitFor } from '@testing-library/react-native'; import { LocalMessage } from 'stream-chat'; +import { WithComponents } from '../../../../contexts/componentsContext/ComponentsContext'; import { OverlayProvider } from '../../../../contexts/overlayContext/OverlayProvider'; import { ThemeProvider } from '../../../../contexts/themeContext/ThemeContext'; import { defaultTheme } from '../../../../contexts/themeContext/utils/theme'; diff --git a/package/src/components/MessageInput/MessageComposer.tsx b/package/src/components/MessageInput/MessageComposer.tsx index b6e8dc1ead..2170994550 100644 --- a/package/src/components/MessageInput/MessageComposer.tsx +++ b/package/src/components/MessageInput/MessageComposer.tsx @@ -27,6 +27,7 @@ import { ChannelContextValue, useChannelContext, } from '../../contexts/channelContext/ChannelContext'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { MessageComposerAPIContextValue, useMessageComposerAPIContext, @@ -36,10 +37,6 @@ import { MessageInputContextValue, useMessageInputContext, } from '../../contexts/messageInputContext/MessageInputContext'; -import { - MessagesContextValue, - useMessagesContext, -} from '../../contexts/messagesContext/MessagesContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { @@ -160,36 +157,18 @@ type MessageComposerPropsWithContext = Pick & | 'asyncMessagesLockDistance' | 'asyncMessagesMinimumPressDuration' | 'asyncMessagesSlideToCancelDistance' - | 'AttachmentUploadPreviewList' - | 'AudioRecorder' - | 'AudioRecordingInProgress' - | 'AudioRecordingLockIndicator' - | 'AudioRecordingPreview' - | 'AutoCompleteSuggestionList' | 'closeAttachmentPicker' | 'compressImageQuality' - | 'Input' - | 'InputView' | 'inputBoxRef' - | 'InputButtons' - | 'MessageComposerLeadingView' - | 'MessageComposerTrailingView' | 'messageInputFloating' | 'messageInputHeightStore' - | 'MessageInputHeaderView' - | 'MessageInputTrailingView' - | 'SendButton' - | 'StartAudioRecordingButton' | 'uploadNewFile' | 'openPollCreationDialog' | 'closePollCreationDialog' | 'showPollCreationDialog' | 'sendMessage' - | 'CreatePollContent' | 'createPollOptionGap' - | 'StopMessageStreamingButton' > & - Pick & Pick & Pick & Pick & { @@ -210,23 +189,11 @@ const MessageComposerWithContext = (props: MessageComposerPropsWithContext) => { additionalTextInputProps, asyncMessagesLockDistance, asyncMessagesSlideToCancelDistance, - AudioRecorder, - AudioRecordingInProgress, - AudioRecordingLockIndicator, - AudioRecordingPreview, - AutoCompleteSuggestionList, closeAttachmentPicker, closePollCreationDialog, - CreatePollContent, createPollOptionGap, - InputView, - MessageComposerLeadingView, - MessageComposerTrailingView, messageInputFloating, messageInputHeightStore, - MessageInputHeaderView, - MessageInputTrailingView, - Input, inputBoxRef, isKeyboardVisible, members, @@ -239,6 +206,20 @@ const MessageComposerWithContext = (props: MessageComposerPropsWithContext) => { recordingStatus, } = props; + const { + AudioRecorder, + AudioRecordingInProgress, + AudioRecordingLockIndicator, + AudioRecordingPreview, + AutoCompleteSuggestionList, + Input, + InputView, + MessageComposerLeadingView, + MessageComposerTrailingView, + MessageInputHeaderView, + MessageInputTrailingView, + } = useComponentsContext(); + const styles = useStyles(); const { selectedPicker } = useAttachmentPickerState(); const { attachmentPickerBottomSheetHeight, bottomInset } = useAttachmentPickerContext(); @@ -486,7 +467,6 @@ const MessageComposerWithContext = (props: MessageComposerPropsWithContext) => { @@ -660,42 +640,22 @@ export const MessageComposer = (props: MessageComposerProps) => { asyncMessagesLockDistance, asyncMessagesMinimumPressDuration, asyncMessagesSlideToCancelDistance, - AttachmentUploadPreviewList, - AudioRecorder, audioRecordingEnabled, - AudioRecordingInProgress, - AudioRecordingLockIndicator, - AudioRecordingPreview, - AudioRecordingWaveform, - AutoCompleteSuggestionList, closeAttachmentPicker, closePollCreationDialog, compressImageQuality, - CreatePollContent, - Input, - InputView, inputBoxRef, - InputButtons, - MessageComposerLeadingView, - MessageComposerTrailingView, messageInputFloating, messageInputHeightStore, - MessageInputHeaderView, - MessageInputTrailingView, openPollCreationDialog, - SendButton, sendMessage, - SendMessageDisallowedIndicator, showPollCreationDialog, - StartAudioRecordingButton, - StopMessageStreamingButton, uploadNewFile, } = useMessageInputContext(); + const { SendMessageDisallowedIndicator } = useComponentsContext(); const messageComposer = useMessageComposer(); const editing = !!messageComposer.editedMessage; const { clearEditingState } = useMessageComposerAPIContext(); - - const { Reply } = useMessagesContext(); const isKeyboardVisible = useKeyboardVisibility(); const { micLocked, isRecordingStateIdle, recordingStatus } = useStateStore( @@ -725,43 +685,23 @@ export const MessageComposer = (props: MessageComposerProps) => { asyncMessagesLockDistance, asyncMessagesMinimumPressDuration, asyncMessagesSlideToCancelDistance, - AttachmentUploadPreviewList, - AudioRecorder, audioRecordingEnabled, - AudioRecordingInProgress, - AudioRecordingLockIndicator, - AudioRecordingPreview, - AudioRecordingWaveform, - AutoCompleteSuggestionList, channel, clearEditingState, closeAttachmentPicker, closePollCreationDialog, compressImageQuality, - CreatePollContent, // TODO: probably not needed anymore, please check editing, - Input, - InputView, inputBoxRef, - InputButtons, - MessageComposerLeadingView, - MessageComposerTrailingView, isKeyboardVisible, isOnline, members, messageInputFloating, messageInputHeightStore, - MessageInputHeaderView, - MessageInputTrailingView, openPollCreationDialog, - Reply, - SendButton, sendMessage, - SendMessageDisallowedIndicator, showPollCreationDialog, - StartAudioRecordingButton, - StopMessageStreamingButton, t, uploadNewFile, watchers, diff --git a/package/src/components/MessageInput/MessageInputHeaderView.tsx b/package/src/components/MessageInput/MessageInputHeaderView.tsx index 39ca1f446d..c717b4e810 100644 --- a/package/src/components/MessageInput/MessageInputHeaderView.tsx +++ b/package/src/components/MessageInput/MessageInputHeaderView.tsx @@ -9,11 +9,11 @@ import { useHasLinkPreviews } from './hooks/useLinkPreviews'; import { idleRecordingStateSelector } from './utils/audioRecorderSelectors'; import { messageComposerStateStoreSelector } from './utils/messageComposerSelectors'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { useMessageComposerAPIContext } from '../../contexts/messageComposerContext/MessageComposerAPIContext'; import { useHasAttachments } from '../../contexts/messageInputContext/hooks/useHasAttachments'; import { useMessageComposer } from '../../contexts/messageInputContext/hooks/useMessageComposer'; import { useMessageInputContext } from '../../contexts/messageInputContext/MessageInputContext'; -import { useMessagesContext } from '../../contexts/messagesContext/MessagesContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { useStateStore } from '../../hooks/useStateStore'; import { primitives } from '../../theme'; @@ -30,8 +30,8 @@ export const MessageInputHeaderView = () => { const { clearEditingState } = useMessageComposerAPIContext(); const { quotedMessage } = useStateStore(messageComposer.state, messageComposerStateStoreSelector); const hasLinkPreviews = useHasLinkPreviews(); - const { audioRecorderManager, AttachmentUploadPreviewList } = useMessageInputContext(); - const { Reply } = useMessagesContext(); + const { audioRecorderManager } = useMessageInputContext(); + const { AttachmentUploadPreviewList, Reply } = useComponentsContext(); const { isRecordingStateIdle } = useStateStore( audioRecorderManager.state, idleRecordingStateSelector, diff --git a/package/src/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.tsx b/package/src/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.tsx index 3a82e92619..15fbb71f6d 100644 --- a/package/src/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.tsx +++ b/package/src/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.tsx @@ -31,11 +31,9 @@ import { } from 'stream-chat'; import { useMessageComposer } from '../../../../contexts'; +import { useComponentsContext } from '../../../../contexts/componentsContext/ComponentsContext'; import { useAttachmentManagerState } from '../../../../contexts/messageInputContext/hooks/useAttachmentManagerState'; -import { - MessageInputContextValue, - useMessageInputContext, -} from '../../../../contexts/messageInputContext/MessageInputContext'; +import { useMessageInputContext } from '../../../../contexts/messageInputContext/MessageInputContext'; import { useTheme } from '../../../../contexts/themeContext/ThemeContext'; import { isSoundPackageAvailable } from '../../../../native'; import { primitives } from '../../../../theme'; @@ -44,13 +42,7 @@ const END_ANCHOR_THRESHOLD = 16; const END_SHRINK_COMPENSATION_DURATION = 200; const MAX_AUDIO_ATTACHMENTS_CONTAINER_WIDTH = 560; -export type AttachmentUploadListPreviewPropsWithContext = Pick< - MessageInputContextValue, - | 'AudioAttachmentUploadPreview' - | 'FileAttachmentUploadPreview' - | 'ImageAttachmentUploadPreview' - | 'VideoAttachmentUploadPreview' ->; +export type AttachmentUploadListPreviewPropsWithContext = Record; const AttachmentPreviewCell = ({ children }: { children: React.ReactNode }) => ( { +const UnMemoizedAttachmentUploadPreviewList = () => { const { AudioAttachmentUploadPreview, FileAttachmentUploadPreview, ImageAttachmentUploadPreview, VideoAttachmentUploadPreview, - } = props; + } = useComponentsContext(); const { audioRecordingSendOnComplete } = useMessageInputContext(); const { attachmentManager } = useMessageComposer(); const { attachments } = useAttachmentManagerState(); @@ -370,7 +360,7 @@ const UnMemoizedAttachmentUploadPreviewList = ( ); }; -export type AttachmentUploadPreviewListProps = Partial; +export type AttachmentUploadPreviewListProps = Record; const MemoizedAttachmentUploadPreviewListWithContext = React.memo( UnMemoizedAttachmentUploadPreviewList, @@ -380,25 +370,7 @@ const MemoizedAttachmentUploadPreviewListWithContext = React.memo( * AttachmentUploadPreviewList * UI Component to preview the files set for upload */ -export const AttachmentUploadPreviewList = (props: AttachmentUploadPreviewListProps) => { - const { - AudioAttachmentUploadPreview, - FileAttachmentUploadPreview, - ImageAttachmentUploadPreview, - VideoAttachmentUploadPreview, - } = useMessageInputContext(); - return ( - - ); -}; +export const AttachmentUploadPreviewList = () => ; const styles = StyleSheet.create({ audioAttachmentsContainer: { diff --git a/package/src/components/MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview.tsx b/package/src/components/MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview.tsx index 36bdd66f09..86f3a0442c 100644 --- a/package/src/components/MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview.tsx +++ b/package/src/components/MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview.tsx @@ -8,7 +8,7 @@ import { AttachmentRemoveControl } from './AttachmentRemoveControl'; import { FilePreview } from '../../../../components/Attachment/FilePreview'; import { useChatContext } from '../../../../contexts/chatContext/ChatContext'; -import { useMessageInputContext } from '../../../../contexts/messageInputContext/MessageInputContext'; +import { useComponentsContext } from '../../../../contexts/componentsContext/ComponentsContext'; import { useTheme } from '../../../../contexts/themeContext/ThemeContext'; import { primitives } from '../../../../theme'; import { UploadAttachmentPreviewProps } from '../../../../types/types'; @@ -31,7 +31,7 @@ export const FileAttachmentUploadPreview = ({ FileUploadInProgressIndicator, FileUploadRetryIndicator, FileUploadNotSupportedIndicator, - } = useMessageInputContext(); + } = useComponentsContext(); const { enableOfflineSupport } = useChatContext(); const indicatorType = getIndicatorTypeForFileState( attachment.localMetadata.uploadState, diff --git a/package/src/components/MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview.tsx b/package/src/components/MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview.tsx index cbe8d11055..93ba730237 100644 --- a/package/src/components/MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview.tsx +++ b/package/src/components/MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview.tsx @@ -7,7 +7,7 @@ import { LocalImageAttachment } from 'stream-chat'; import { AttachmentRemoveControl } from './AttachmentRemoveControl'; import { useChatContext } from '../../../../contexts/chatContext/ChatContext'; -import { useMessageInputContext } from '../../../../contexts/messageInputContext/MessageInputContext'; +import { useComponentsContext } from '../../../../contexts/componentsContext/ComponentsContext'; import { useTheme } from '../../../../contexts/themeContext/ThemeContext'; import { primitives } from '../../../../theme'; import { UploadAttachmentPreviewProps } from '../../../../types/types'; @@ -29,7 +29,7 @@ export const ImageAttachmentUploadPreview = ({ ImageUploadInProgressIndicator, ImageUploadRetryIndicator, ImageUploadNotSupportedIndicator, - } = useMessageInputContext(); + } = useComponentsContext(); const indicatorType = loading ? ProgressIndicatorTypes.IN_PROGRESS : getIndicatorTypeForFileState(attachment.localMetadata.uploadState, enableOfflineSupport); diff --git a/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingInProgress.tsx b/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingInProgress.tsx index 1ef8c1bf76..de0e9f667c 100644 --- a/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingInProgress.tsx +++ b/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingInProgress.tsx @@ -3,6 +3,7 @@ import { StyleSheet, Text, View } from 'react-native'; import dayjs from 'dayjs'; +import { useComponentsContext } from '../../../../contexts/componentsContext/ComponentsContext'; import { MessageInputContextValue, useMessageInputContext, @@ -15,7 +16,7 @@ import { primitives } from '../../../../theme'; type AudioRecordingInProgressPropsWithContext = Pick< MessageInputContextValue, - 'audioRecorderManager' | 'AudioRecordingWaveform' + 'audioRecorderManager' > & Pick & { /** @@ -25,7 +26,8 @@ type AudioRecordingInProgressPropsWithContext = Pick< }; const AudioRecordingInProgressWithContext = (props: AudioRecordingInProgressPropsWithContext) => { - const { AudioRecordingWaveform, maxDataPointsDrawn = 60, duration, waveformData } = props; + const { maxDataPointsDrawn = 60, duration, waveformData } = props; + const { AudioRecordingWaveform } = useComponentsContext(); const styles = useStyles(); @@ -69,7 +71,7 @@ const audioRecorderSelector = (state: AudioRecorderManagerState) => ({ * Component displayed when the audio is in the recording state. */ export const AudioRecordingInProgress = (props: AudioRecordingInProgressProps) => { - const { audioRecorderManager, AudioRecordingWaveform } = useMessageInputContext(); + const { audioRecorderManager } = useMessageInputContext(); const { duration, waveformData } = useStateStore( audioRecorderManager.state, @@ -78,7 +80,7 @@ export const AudioRecordingInProgress = (props: AudioRecordingInProgressProps) = return ( ); diff --git a/package/src/components/MessageInput/components/InputButtons/index.tsx b/package/src/components/MessageInput/components/InputButtons/index.tsx index bb792302f0..e9f3cb7739 100644 --- a/package/src/components/MessageInput/components/InputButtons/index.tsx +++ b/package/src/components/MessageInput/components/InputButtons/index.tsx @@ -10,6 +10,7 @@ import Animated, { } from 'react-native-reanimated'; import { OwnCapabilitiesContextValue } from '../../../../contexts'; +import { useComponentsContext } from '../../../../contexts/componentsContext/ComponentsContext'; import { useActiveCommand } from '../../../../contexts/messageInputContext/hooks/useActiveCommand'; import { MessageInputContextValue, @@ -23,19 +24,19 @@ export type InputButtonsProps = Partial; export type InputButtonsWithContextProps = Pick< MessageInputContextValue, - 'AttachButton' | 'hasCameraPicker' | 'hasCommands' | 'hasFilePicker' | 'hasImagePicker' + 'hasCameraPicker' | 'hasCommands' | 'hasFilePicker' | 'hasImagePicker' > & Pick; export const InputButtonsWithContext = (props: InputButtonsWithContextProps) => { const { - AttachButton, hasCameraPicker, hasCommands, hasFilePicker, hasImagePicker, uploadFile: ownCapabilitiesUploadFile, } = props; + const { AttachButton } = useComponentsContext(); const { selectedPicker } = useAttachmentPickerState(); const rotation = useSharedValue(0); const command = useActiveCommand(); @@ -107,14 +108,12 @@ const MemoizedInputButtonsWithContext = React.memo( ) as typeof InputButtonsWithContext; export const InputButtons = (props: InputButtonsProps) => { - const { AttachButton, hasCameraPicker, hasCommands, hasFilePicker, hasImagePicker } = - useMessageInputContext(); + const { hasCameraPicker, hasCommands, hasFilePicker, hasImagePicker } = useMessageInputContext(); const { uploadFile } = useOwnCapabilitiesContext(); return ( & | 'asyncMessagesLockDistance' | 'audioRecordingSendOnComplete' | 'audioRecordingEnabled' - | 'CooldownTimer' - | 'SendButton' - | 'StopMessageStreamingButton' - | 'StartAudioRecordingButton' > & { cooldownIsActive: boolean; hasAttachments: boolean; @@ -51,17 +48,9 @@ const textComposerStateSelector = (state: TextComposerState) => ({ }); export const OutputButtonsWithContext = (props: OutputButtonsWithContextProps) => { - const { - audioRecordingEnabled, - channel, - CooldownTimer, - cooldownIsActive, - isOnline, - SendButton, - StopMessageStreamingButton, - StartAudioRecordingButton, - hasAttachments, - } = props; + const { audioRecordingEnabled, channel, cooldownIsActive, isOnline, hasAttachments } = props; + const { CooldownTimer, SendButton, StartAudioRecordingButton, StopMessageStreamingButton } = + useComponentsContext(); const { theme: { messageComposer: { @@ -180,10 +169,6 @@ export const OutputButtons = (props: OutputButtonsProps) => { asyncMessagesSlideToCancelDistance, asyncMessagesLockDistance, audioRecordingSendOnComplete, - CooldownTimer, - SendButton, - StopMessageStreamingButton, - StartAudioRecordingButton, } = useMessageInputContext(); const cooldownIsActive = useIsCooldownActive(); const hasAttachments = useHasAttachments(); @@ -198,11 +183,7 @@ export const OutputButtons = (props: OutputButtonsProps) => { audioRecordingEnabled, channel, cooldownIsActive, - CooldownTimer, isOnline, - SendButton, - StartAudioRecordingButton, - StopMessageStreamingButton, hasAttachments, }} {...props} diff --git a/package/src/components/MessageList/MessageFlashList.tsx b/package/src/components/MessageList/MessageFlashList.tsx index 6faae7522e..8b7774f0e0 100644 --- a/package/src/components/MessageList/MessageFlashList.tsx +++ b/package/src/components/MessageList/MessageFlashList.tsx @@ -29,6 +29,7 @@ import { useChannelContext, } from '../../contexts/channelContext/ChannelContext'; import { ChatContextValue, useChatContext } from '../../contexts/chatContext/ChatContext'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { MessageInputContextValue, useMessageInputContext, @@ -121,19 +122,15 @@ type MessageFlashListPropsWithContext = Pick< | 'channel' | 'channelUnreadStateStore' | 'disabled' - | 'EmptyStateIndicator' | 'hideStickyDateHeader' | 'highlightedMessageId' | 'loadChannelAroundMessage' | 'loading' - | 'LoadingIndicator' | 'markRead' - | 'NetworkDownIndicator' | 'reloadChannel' | 'scrollToFirstUnreadThreshold' | 'setChannelUnreadState' | 'setTargetedMessage' - | 'StickyHeader' | 'targetedMessage' | 'threadList' | 'maximumMessageLimit' @@ -143,19 +140,7 @@ type MessageFlashListPropsWithContext = Pick< Pick & Pick< MessagesContextValue, - | 'DateHeader' - | 'disableTypingIndicator' - | 'FlatList' - | 'InlineDateSeparator' - | 'InlineUnreadIndicator' - | 'Message' - | 'ScrollToBottomButton' - | 'MessageSystem' - | 'myMessageTheme' - | 'shouldShowUnreadUnderlay' - | 'TypingIndicator' - | 'TypingIndicatorContainer' - | 'UnreadMessagesNotification' + 'disableTypingIndicator' | 'FlatList' | 'myMessageTheme' | 'shouldShowUnreadUnderlay' > & Pick< ThreadContextValue, @@ -277,10 +262,8 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => channelUnreadStateStore, client, closePicker, - DateHeader, disabled, disableTypingIndicator, - EmptyStateIndicator, // FlatList, FooterComponent, HeaderComponent = InlineLoadingMoreIndicator, @@ -288,7 +271,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => isLiveStreaming = false, loadChannelAroundMessage, loading, - LoadingIndicator, loadMore, loadMoreRecent, loadMoreRecentThread, @@ -299,24 +281,29 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => messageInputHeightStore, myMessageTheme, readEvents, - NetworkDownIndicator, noGroupByUser, onListScroll, onThreadSelect, reloadChannel, - ScrollToBottomButton, setChannelUnreadState, setFlatListRef, setTargetedMessage, - StickyHeader, targetedMessage, thread, threadInstance, threadList = false, + } = props; + const { + DateHeader, + EmptyStateIndicator, + LoadingIndicator, + NetworkDownIndicator, + ScrollToBottomButton, + StickyHeader, TypingIndicator, TypingIndicatorContainer, UnreadMessagesNotification, - } = props; + } = useComponentsContext(); const flashListRef = useRef | null>(null); const { height: messageInputHeight } = useStateStore( @@ -1181,7 +1168,6 @@ export const MessageFlashList = (props: MessageFlashListProps) => { channel, channelUnreadStateStore, disabled, - EmptyStateIndicator, enableMessageGroupingByUser, error, hideStickyDateHeader, @@ -1189,34 +1175,18 @@ export const MessageFlashList = (props: MessageFlashListProps) => { isChannelActive, loadChannelAroundMessage, loading, - LoadingIndicator, markRead, maximumMessageLimit, - NetworkDownIndicator, reloadChannel, scrollToFirstUnreadThreshold, setChannelUnreadState, setTargetedMessage, - StickyHeader, targetedMessage, threadList, } = useChannelContext(); const { client } = useChatContext(); - const { - DateHeader, - disableTypingIndicator, - FlatList, - InlineDateSeparator, - InlineUnreadIndicator, - Message, - MessageSystem, - myMessageTheme, - ScrollToBottomButton, - shouldShowUnreadUnderlay, - TypingIndicator, - TypingIndicatorContainer, - UnreadMessagesNotification, - } = useMessagesContext(); + const { disableTypingIndicator, FlatList, myMessageTheme, shouldShowUnreadUnderlay } = + useMessagesContext(); const { loadMore, loadMoreRecent } = usePaginatedMessageListContext(); const { loadMoreRecentThread, loadMoreThread, thread, threadInstance } = useThreadContext(); const { readEvents } = useOwnCapabilitiesContext(); @@ -1230,48 +1200,35 @@ export const MessageFlashList = (props: MessageFlashListProps) => { channelUnreadStateStore, client, closePicker, - DateHeader, disabled, disableTypingIndicator, - EmptyStateIndicator, enableMessageGroupingByUser, error, FlatList, hideStickyDateHeader, highlightedMessageId, - InlineDateSeparator, - InlineUnreadIndicator, isListActive: isChannelActive, loadChannelAroundMessage, loading, - LoadingIndicator, loadMore, loadMoreRecent, loadMoreRecentThread, loadMoreThread, markRead, maximumMessageLimit, - Message, messageInputFloating, messageInputHeightStore, - MessageSystem, myMessageTheme, - NetworkDownIndicator, readEvents, reloadChannel, - ScrollToBottomButton, scrollToFirstUnreadThreshold, setChannelUnreadState, setTargetedMessage, shouldShowUnreadUnderlay, - StickyHeader, targetedMessage, thread, threadInstance, threadList, - TypingIndicator, - TypingIndicatorContainer, - UnreadMessagesNotification, }} {...props} noGroupByUser={!enableMessageGroupingByUser || props.noGroupByUser} diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx index 9ea0b3bdba..eae75366a5 100644 --- a/package/src/components/MessageList/MessageList.tsx +++ b/package/src/components/MessageList/MessageList.tsx @@ -38,6 +38,7 @@ import { useChannelContext, } from '../../contexts/channelContext/ChannelContext'; import { ChatContextValue, useChatContext } from '../../contexts/chatContext/ChatContext'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { useDebugContext } from '../../contexts/debugContext/DebugContext'; import { @@ -196,18 +197,14 @@ type MessageListPropsWithContext = Pick< | 'channel' | 'channelUnreadStateStore' | 'disabled' - | 'EmptyStateIndicator' | 'hideStickyDateHeader' | 'loadChannelAroundMessage' | 'loading' - | 'LoadingIndicator' | 'markRead' - | 'NetworkDownIndicator' | 'reloadChannel' | 'scrollToFirstUnreadThreshold' | 'setChannelUnreadState' | 'setTargetedMessage' - | 'StickyHeader' | 'targetedMessage' | 'threadList' | 'maximumMessageLimit' @@ -216,17 +213,7 @@ type MessageListPropsWithContext = Pick< Pick & Pick< MessagesContextValue, - | 'DateHeader' - | 'disableTypingIndicator' - | 'FlatList' - | 'InlineDateSeparator' - | 'InlineUnreadIndicator' - | 'Message' - | 'ScrollToBottomButton' - | 'myMessageTheme' - | 'TypingIndicator' - | 'TypingIndicatorContainer' - | 'UnreadMessagesNotification' + 'disableTypingIndicator' | 'FlatList' | 'myMessageTheme' | 'shouldShowUnreadUnderlay' > & Pick & Pick< @@ -326,10 +313,8 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { channelUnreadStateStore, client, closePicker, - DateHeader, disabled, disableTypingIndicator, - EmptyStateIndicator, FlatList, FooterComponent = InlineLoadingMoreIndicator, HeaderComponent, @@ -338,7 +323,6 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { isLiveStreaming = false, loadChannelAroundMessage, loading, - LoadingIndicator, loadMore, loadMoreRecent, loadMoreRecentThread, @@ -348,27 +332,32 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { messageInputFloating, messageInputHeightStore, myMessageTheme, - NetworkDownIndicator, noGroupByUser, onListScroll, onThreadSelect, readEvents, reloadChannel, - ScrollToBottomButton, setChannelUnreadState, setFlatListRef, setTargetedMessage, - StickyHeader, targetedMessage, thread, threadInstance, threadList = false, - TypingIndicator, - TypingIndicatorContainer, - UnreadMessagesNotification, hasMore, threadHasMore, } = props; + const { + DateHeader, + EmptyStateIndicator, + LoadingIndicator, + NetworkDownIndicator, + ScrollToBottomButton, + StickyHeader, + TypingIndicator, + TypingIndicatorContainer, + UnreadMessagesNotification, + } = useComponentsContext(); const [isUnreadNotificationOpen, setIsUnreadNotificationOpen] = useState(false); const { theme } = useTheme(); const styles = useStyles(); @@ -1350,42 +1339,25 @@ export const MessageList = (props: MessageListProps) => { channel, channelUnreadStateStore, disabled, - EmptyStateIndicator, enableMessageGroupingByUser, error, hideStickyDateHeader, highlightedMessageId, loadChannelAroundMessage, loading, - LoadingIndicator, maximumMessageLimit, markRead, - NetworkDownIndicator, reloadChannel, scrollToFirstUnreadThreshold, setChannelUnreadState, setTargetedMessage, - StickyHeader, targetedMessage, threadList, } = useChannelContext(); const { client } = useChatContext(); const { readEvents } = useOwnCapabilitiesContext(); - const { - DateHeader, - disableTypingIndicator, - FlatList, - InlineDateSeparator, - InlineUnreadIndicator, - Message, - MessageSystem, - myMessageTheme, - ScrollToBottomButton, - shouldShowUnreadUnderlay, - TypingIndicator, - TypingIndicatorContainer, - UnreadMessagesNotification, - } = useMessagesContext(); + const { disableTypingIndicator, FlatList, myMessageTheme, shouldShowUnreadUnderlay } = + useMessagesContext(); const { messageInputFloating, messageInputHeightStore } = useMessageInputContext(); const { loadMore, loadMoreRecent, hasMore } = usePaginatedMessageListContext(); const { loadMoreRecentThread, loadMoreThread, threadHasMore, thread, threadInstance } = @@ -1399,47 +1371,34 @@ export const MessageList = (props: MessageListProps) => { channelUnreadStateStore, client, closePicker, - DateHeader, disabled, disableTypingIndicator, - EmptyStateIndicator, enableMessageGroupingByUser, error, FlatList, hideStickyDateHeader, highlightedMessageId, - InlineDateSeparator, - InlineUnreadIndicator, loadChannelAroundMessage, loading, - LoadingIndicator, loadMore, loadMoreRecent, loadMoreRecentThread, loadMoreThread, markRead, maximumMessageLimit, - Message, messageInputFloating, messageInputHeightStore, - MessageSystem, myMessageTheme, - NetworkDownIndicator, readEvents, reloadChannel, - ScrollToBottomButton, scrollToFirstUnreadThreshold, setChannelUnreadState, setTargetedMessage, shouldShowUnreadUnderlay, - StickyHeader, targetedMessage, thread, threadInstance, threadList, - TypingIndicator, - TypingIndicatorContainer, - UnreadMessagesNotification, hasMore, threadHasMore, }} diff --git a/package/src/components/MessageList/StickyHeader.tsx b/package/src/components/MessageList/StickyHeader.tsx index 9ecaefac70..4120244952 100644 --- a/package/src/components/MessageList/StickyHeader.tsx +++ b/package/src/components/MessageList/StickyHeader.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; -import { MessagesContextValue } from '../../contexts/messagesContext/MessagesContext'; +import type { ComponentOverrides } from '../../contexts/componentsContext/ComponentsContext'; import { useTranslationContext } from '../../contexts/translationContext/TranslationContext'; import { getDateString } from '../../utils/i18n/getDateString'; @@ -8,7 +8,7 @@ import { getDateString } from '../../utils/i18n/getDateString'; /** * Props for the StickyHeader component. */ -export type StickyHeaderProps = Pick & { +export type StickyHeaderProps = Pick, 'DateHeader'> & { /** * Date to be displayed in the sticky header. */ diff --git a/package/src/components/MessageMenu/MessageActionList.tsx b/package/src/components/MessageMenu/MessageActionList.tsx index 47c89fbf9a..a37bac7a3c 100644 --- a/package/src/components/MessageMenu/MessageActionList.tsx +++ b/package/src/components/MessageMenu/MessageActionList.tsx @@ -5,11 +5,11 @@ import { ScrollView } from 'react-native-gesture-handler'; import { MessageActionType } from './MessageActionListItem'; -import { MessagesContextValue } from '../../contexts/messagesContext/MessagesContext'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { primitives } from '../../theme'; -export type MessageActionListProps = Pick & { +export type MessageActionListProps = { /** * Function to close the message actions bottom sheet * @returns void @@ -22,7 +22,8 @@ export type MessageActionListProps = Pick { - const { MessageActionListItem, messageActions } = props; + const { messageActions } = props; + const { MessageActionListItem } = useComponentsContext(); const { theme: { messageMenu: { diff --git a/package/src/components/MessageMenu/MessageMenu.tsx b/package/src/components/MessageMenu/MessageMenu.tsx index 28d1484169..63feffd713 100644 --- a/package/src/components/MessageMenu/MessageMenu.tsx +++ b/package/src/components/MessageMenu/MessageMenu.tsx @@ -4,15 +4,15 @@ import { useWindowDimensions } from 'react-native'; import { MessageActionType } from './MessageActionListItem'; +import type { ComponentOverrides } from '../../contexts/componentsContext/ComponentsContext'; import { MessageContextValue } from '../../contexts/messageContext/MessageContext'; -import { MessagesContextValue } from '../../contexts/messagesContext/MessagesContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { BottomSheetModal } from '../UIComponents/BottomSheetModal'; export type MessageMenuProps = PropsWithChildren< Partial< Pick< - MessagesContextValue, + ComponentOverrides, | 'MessageActionList' | 'MessageActionListItem' | 'MessageReactionPicker' diff --git a/package/src/components/MessageMenu/MessageUserReactions.tsx b/package/src/components/MessageMenu/MessageUserReactions.tsx index 1bb6728122..c8ae3abcaf 100644 --- a/package/src/components/MessageMenu/MessageUserReactions.tsx +++ b/package/src/components/MessageMenu/MessageUserReactions.tsx @@ -11,6 +11,7 @@ import { useFetchReactions } from './hooks/useFetchReactions'; import { ReactionButton } from './ReactionButton'; import { useBottomSheetContext } from '../../contexts'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { MessageContextValue, useMessageContext, @@ -39,12 +40,7 @@ const getItemLayout = (_, index: number) => ({ index, }); -export type MessageUserReactionsProps = Partial< - Pick< - MessagesContextValue, - 'MessageUserReactionsAvatar' | 'MessageUserReactionsItem' | 'supportedReactions' - > -> & +export type MessageUserReactionsProps = Partial> & Partial> & { /** * An array of reactions @@ -99,13 +95,7 @@ const reactionSelectorKeyExtractor = (item: ReactionSelectorItemType) => item.ty export const MessageUserReactions = (props: MessageUserReactionsProps) => { const styles = useStyles(); const [showMoreReactions, setShowMoreReactions] = useState(false); - const { - message, - MessageUserReactionsAvatar: propMessageUserReactionsAvatar, - MessageUserReactionsItem: propMessageUserReactionsItem, - reactions: propReactions, - supportedReactions: propSupportedReactions, - } = props; + const { message, reactions: propReactions, supportedReactions: propSupportedReactions } = props; const selectorListRef = useRef(null); const { close } = useBottomSheetContext(); const reactionTypes = useMemo( @@ -113,16 +103,10 @@ export const MessageUserReactions = (props: MessageUserReactionsProps) => { [message?.reaction_groups], ); const [selectedReaction, setSelectedReaction] = useState(undefined); - const { - MessageUserReactionsAvatar: contextMessageUserReactionsAvatar, - MessageUserReactionsItem: contextMessageUserReactionsItem, - supportedReactions: contextSupportedReactions, - } = useMessagesContext(); + const { supportedReactions: contextSupportedReactions } = useMessagesContext(); + const { MessageUserReactionsItem } = useComponentsContext(); const { handleReaction } = useMessageContext(); const supportedReactions = propSupportedReactions ?? contextSupportedReactions; - const MessageUserReactionsAvatar = - propMessageUserReactionsAvatar ?? contextMessageUserReactionsAvatar; - const MessageUserReactionsItem = propMessageUserReactionsItem ?? contextMessageUserReactionsItem; const onSelectReaction = useStableCallback((reactionType: string) => { setSelectedReaction((currentReaction) => @@ -225,13 +209,9 @@ export const MessageUserReactions = (props: MessageUserReactionsProps) => { const renderItem = useCallback( ({ item }: { item: Reaction }) => ( - + ), - [MessageUserReactionsAvatar, MessageUserReactionsItem, supportedReactions], + [MessageUserReactionsItem, supportedReactions], ); const handleSelectReaction = useStableCallback((emoji: string) => { diff --git a/package/src/components/MessageMenu/MessageUserReactionsItem.tsx b/package/src/components/MessageMenu/MessageUserReactionsItem.tsx index 5db6c9a39c..dea19586fc 100644 --- a/package/src/components/MessageMenu/MessageUserReactionsItem.tsx +++ b/package/src/components/MessageMenu/MessageUserReactionsItem.tsx @@ -4,7 +4,7 @@ import { Pressable, StyleSheet, Text, View } from 'react-native'; import { useMessageContext } from '../../contexts'; import { useChatContext } from '../../contexts/chatContext/ChatContext'; -import { MessagesContextValue } from '../../contexts/messagesContext/MessagesContext'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { useTranslationContext } from '../../contexts/translationContext/TranslationContext'; import { useStableCallback } from '../../hooks'; @@ -14,10 +14,7 @@ import { primitives } from '../../theme'; import type { Reaction } from '../../types/types'; import { ReactionData } from '../../utils/utils'; -export type MessageUserReactionsItemProps = Pick< - MessagesContextValue, - 'MessageUserReactionsAvatar' -> & { +export type MessageUserReactionsItemProps = { /** * The reaction object */ @@ -29,10 +26,10 @@ export type MessageUserReactionsItemProps = Pick< }; export const MessageUserReactionsItem = ({ - MessageUserReactionsAvatar, reaction, supportedReactions, }: MessageUserReactionsItemProps) => { + const { MessageUserReactionsAvatar } = useComponentsContext(); const { id, name, type } = reaction; const { theme: { diff --git a/package/src/components/MessageMenu/__tests__/MessageUserReactions.test.tsx b/package/src/components/MessageMenu/__tests__/MessageUserReactions.test.tsx index c7312dccee..364b5ef017 100644 --- a/package/src/components/MessageMenu/__tests__/MessageUserReactions.test.tsx +++ b/package/src/components/MessageMenu/__tests__/MessageUserReactions.test.tsx @@ -6,6 +6,7 @@ import { fireEvent, render } from '@testing-library/react-native'; import { LocalMessage, ReactionResponse } from 'stream-chat'; +import { WithComponents } from '../../../contexts/componentsContext/ComponentsContext'; import { MessagesContextValue, MessagesProvider, diff --git a/package/src/components/Poll/CreatePollContent.tsx b/package/src/components/Poll/CreatePollContent.tsx index b1b9174c80..ddae56b967 100644 --- a/package/src/components/Poll/CreatePollContent.tsx +++ b/package/src/components/Poll/CreatePollContent.tsx @@ -16,11 +16,11 @@ import { CreatePollModalState, CreatePollContentContextValue, CreatePollContentProvider, - InputMessageInputContextValue, useCreatePollContentContext, useTheme, useTranslationContext, } from '../../contexts'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { useMessageComposer } from '../../contexts/messageInputContext/hooks/useMessageComposer'; import { useStateStore } from '../../hooks/useStateStore'; import { primitives } from '../../theme'; @@ -211,14 +211,13 @@ export const CreatePollContent = () => { export const CreatePoll = ({ closePollCreationDialog, - CreatePollContent: CreatePollContentOverride, createPollOptionGap = 8, sendMessage, }: Pick< CreatePollContentContextValue, 'createPollOptionGap' | 'closePollCreationDialog' | 'sendMessage' -> & - Pick) => { +>) => { + const { CreatePollContent: CreatePollContentOverride } = useComponentsContext(); const messageComposer = useMessageComposer(); const [modalStateStore] = useState( () => new StateStore({ isClosing: false }), diff --git a/package/src/components/Poll/Poll.tsx b/package/src/components/Poll/Poll.tsx index b1ea9fc7f3..68503ce6bc 100644 --- a/package/src/components/Poll/Poll.tsx +++ b/package/src/components/Poll/Poll.tsx @@ -8,18 +8,17 @@ import { PollButtons, PollOption, ShowAllOptionsButton } from './components'; import { usePollState } from './hooks/usePollState'; import { - MessagesContextValue, PollContextProvider, PollContextValue, useTheme, useTranslationContext, } from '../../contexts'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { primitives } from '../../theme'; import { defaultPollOptionCount } from '../../utils/constants'; -export type PollProps = Pick & - Pick; +export type PollProps = Pick; export type PollContentProps = { PollButtons?: React.ComponentType; @@ -91,16 +90,19 @@ export const PollContent = ({ ); }; -export const Poll = ({ message, poll, PollContent: PollContentOverride }: PollProps) => ( - - {PollContentOverride ? : } - -); +export const Poll = ({ message, poll }: PollProps) => { + const { PollContent: PollContentOverride } = useComponentsContext(); + return ( + + {PollContentOverride ? : } + + ); +}; const useStyles = () => { const { diff --git a/package/src/components/Thread/Thread.tsx b/package/src/components/Thread/Thread.tsx index 9643b44a74..77f8173143 100644 --- a/package/src/components/Thread/Thread.tsx +++ b/package/src/components/Thread/Thread.tsx @@ -4,10 +4,7 @@ import { ThreadFooterComponent } from './components/ThreadFooterComponent'; import { useChannelContext } from '../../contexts/channelContext/ChannelContext'; import { ChatContextValue, useChatContext } from '../../contexts/chatContext/ChatContext'; -import { - MessagesContextValue, - useMessagesContext, -} from '../../contexts/messagesContext/MessagesContext'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { ThreadContextValue, useThreadContext } from '../../contexts/threadContext/ThreadContext'; import { @@ -26,7 +23,6 @@ try { } type ThreadPropsWithContext = Pick & - Pick & Pick< ThreadContextValue, | 'closeThread' @@ -82,13 +78,13 @@ const ThreadWithContext = (props: ThreadPropsWithContext) => { disabled, loadMoreThread, MessageComposer = DefaultMessageComposer, - MessageList, onThreadDismount, parentMessagePreventPress = true, thread, threadInstance, shouldUseFlashList = false, } = props; + const { MessageList } = useComponentsContext(); useEffect(() => { if (threadInstance?.activate) { @@ -171,7 +167,6 @@ export type ThreadProps = Partial; export const Thread = (props: ThreadProps) => { const { client } = useChatContext(); const { threadList } = useChannelContext(); - const { MessageList } = useMessagesContext(); const { closeThread, loadMoreThread, reloadThread, thread, threadInstance } = useThreadContext(); if (thread?.id && !threadList) { @@ -186,7 +181,6 @@ export const Thread = (props: ThreadProps) => { client, closeThread, loadMoreThread, - MessageList, reloadThread, thread, threadInstance, diff --git a/package/src/components/Thread/components/ThreadFooterComponent.tsx b/package/src/components/Thread/components/ThreadFooterComponent.tsx index 44391da3ec..bc9ac420dd 100644 --- a/package/src/components/Thread/components/ThreadFooterComponent.tsx +++ b/package/src/components/Thread/components/ThreadFooterComponent.tsx @@ -3,10 +3,7 @@ import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'; import type { ThreadState } from 'stream-chat'; -import { - MessagesContextValue, - useMessagesContext, -} from '../../../contexts/messagesContext/MessagesContext'; +import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext'; import { useTheme } from '../../../contexts/themeContext/ThemeContext'; import { ThreadContextValue, @@ -16,8 +13,10 @@ import { useTranslationContext } from '../../../contexts/translationContext/Tran import { useStateStore } from '../../../hooks'; import { primitives } from '../../../theme'; -type ThreadFooterComponentPropsWithContext = Pick & - Pick; +type ThreadFooterComponentPropsWithContext = Pick< + ThreadContextValue, + 'parentMessagePreventPress' | 'thread' | 'threadInstance' +>; export const InlineLoadingMoreThreadIndicator = () => { const { threadLoadingMore } = useThreadContext(); @@ -43,7 +42,8 @@ const selector = (nextValue: ThreadState) => }) as const; const ThreadFooterComponentWithContext = (props: ThreadFooterComponentPropsWithContext) => { - const { Message, parentMessagePreventPress, thread, threadInstance } = props; + const { parentMessagePreventPress, thread, threadInstance } = props; + const { Message } = useComponentsContext(); const { t } = useTranslationContext(); const styles = useStyles(); @@ -131,18 +131,17 @@ const MemoizedThreadFooter = React.memo( areEqual, ) as typeof ThreadFooterComponentWithContext; -export type ThreadFooterComponentProps = Partial> & - Partial>; +export type ThreadFooterComponentProps = Partial< + Pick +>; export const ThreadFooterComponent = (props: ThreadFooterComponentProps) => { - const { Message } = useMessagesContext(); const { parentMessagePreventPress, thread, threadInstance, threadLoadingMore } = useThreadContext(); return ( & { - /** - * Custom UI Component to render select more photos for selected gallery access in iOS. - */ - AttachmentPickerIOSSelectMorePhotos: React.ComponentType; /** * `bottomInset` determine the height of the `AttachmentPicker` and the underlying shift to the `MessageList` when it is opened. * This can also be set via the `setBottomInset` function provided by the `useAttachmentPickerContext` hook. @@ -39,16 +34,6 @@ export type AttachmentPickerContextValue = Pick< topInset: number; disableAttachmentPicker?: boolean; - /** - * Custom UI component to render overlay component, that shows up on top of [selected - * image](https://github.com/GetStream/stream-chat-react-native/blob/main/screenshots/docs/1.png) (with tick mark) - * - * **Default** - * [ImageOverlaySelectedComponent](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/AttachmentPicker/components/ImageOverlaySelectedComponent.tsx) - */ - ImageOverlaySelectedComponent: React.ComponentType<{ index: number }>; - AttachmentPickerSelectionBar: React.ComponentType; - AttachmentPickerContent: React.ComponentType; attachmentPickerStore: AttachmentPickerStore; numberOfAttachmentPickerImageColumns?: number; numberOfAttachmentImagesToLoadPerCall?: number; diff --git a/package/src/contexts/channelContext/ChannelContext.tsx b/package/src/contexts/channelContext/ChannelContext.tsx index 36e3c63ed1..6167626f5d 100644 --- a/package/src/contexts/channelContext/ChannelContext.tsx +++ b/package/src/contexts/channelContext/ChannelContext.tsx @@ -3,9 +3,6 @@ import React, { PropsWithChildren, useContext } from 'react'; import type { Channel, ChannelState } from 'stream-chat'; import { MarkReadFunctionOptions } from '../../components/Channel/Channel'; -import type { EmptyStateProps } from '../../components/Indicators/EmptyStateIndicator'; -import type { LoadingProps } from '../../components/Indicators/LoadingIndicator'; -import { StickyHeaderProps } from '../../components/MessageList/StickyHeader'; import { ChannelUnreadStateStore, ChannelUnreadStateStoreType, @@ -38,12 +35,6 @@ export type ChannelContextValue = { * @overrideType Channel */ channel: Channel; - /** - * Custom UI component to display empty state when channel has no messages. - * - * **Default** [EmptyStateIndicator](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Indicators/EmptyStateIndicator.tsx) - */ - EmptyStateIndicator: React.ComponentType; /** * When set to true, reactions will be limited to 1 per user. If user selects another reaction * then his previous reaction will be removed and replaced with new one. @@ -90,10 +81,6 @@ export type ChannelContextValue = { setTargetedMessage?: (messageId: string) => void; }) => Promise; - /** - * Custom loading indicator to override the Stream default - */ - LoadingIndicator: React.ComponentType; markRead: (options?: MarkReadFunctionOptions) => void; /** * @@ -119,10 +106,6 @@ export type ChannelContextValue = { * ``` */ members: ChannelState['members']; - /** - * Custom network down indicator to override the Stream default - */ - NetworkDownIndicator: React.ComponentType; read: ChannelState['read']; reloadChannel: () => Promise; scrollToFirstUnreadThreshold: number; @@ -156,13 +139,6 @@ export type ChannelContextValue = { * currently near them within the viewport. */ maximumMessageLimit?: number; - /** - * Custom UI component for sticky header of channel. - * - * **Default** [DateHeader](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageList/DateHeader.tsx) - */ - StickyHeader?: React.ComponentType; - /** * Id of message, around which Channel/MessageList gets loaded when opened. * You will see a highlighted background for targetted message, when opened. diff --git a/package/src/contexts/channelsContext/ChannelsContext.tsx b/package/src/contexts/channelsContext/ChannelsContext.tsx index 98b834222b..b375a7997c 100644 --- a/package/src/contexts/channelsContext/ChannelsContext.tsx +++ b/package/src/contexts/channelsContext/ChannelsContext.tsx @@ -5,23 +5,8 @@ import type { FlatList } from 'react-native-gesture-handler'; import type { Channel } from 'stream-chat'; -import type { HeaderErrorProps } from '../../components/ChannelList/ChannelListHeaderErrorIndicator'; import type { GetChannelActionItems } from '../../components/ChannelList/hooks/useChannelActionItems'; import type { QueryChannels } from '../../components/ChannelList/hooks/usePaginatedChannels'; -import type { ChannelDetailsBottomSheetProps } from '../../components/ChannelPreview/ChannelDetailsBottomSheet'; -import { ChannelLastMessagePreviewProps } from '../../components/ChannelPreview/ChannelLastMessagePreview'; -import { ChannelMessagePreviewDeliveryStatusProps } from '../../components/ChannelPreview/ChannelMessagePreviewDeliveryStatus'; -import { ChannelPreviewMessageProps } from '../../components/ChannelPreview/ChannelPreviewMessage'; -import type { ChannelPreviewStatusProps } from '../../components/ChannelPreview/ChannelPreviewStatus'; -import type { ChannelPreviewTitleProps } from '../../components/ChannelPreview/ChannelPreviewTitle'; -import { ChannelPreviewTypingIndicatorProps } from '../../components/ChannelPreview/ChannelPreviewTypingIndicator'; -import type { ChannelPreviewUnreadCountProps } from '../../components/ChannelPreview/ChannelPreviewUnreadCount'; -import type { ChannelPreviewViewProps } from '../../components/ChannelPreview/ChannelPreviewView'; -import type { EmptyStateProps } from '../../components/Indicators/EmptyStateIndicator'; -import type { LoadingErrorProps } from '../../components/Indicators/LoadingErrorIndicator'; -import type { LoadingProps } from '../../components/Indicators/LoadingIndicator'; - -import { ChannelAvatarProps } from '../../components/ui/Avatar/ChannelAvatar'; import { DEFAULT_BASE_CONTEXT_VALUE } from '../utils/defaultBaseContextValue'; import { isTestEnvironment } from '../utils/isTestEnvironment'; @@ -53,18 +38,6 @@ export type ChannelsContextValue = { * Channels can be either an array of channels or a promise which resolves to an array of channels */ channels: Channel[] | null; - /** - * Custom indicator to use when channel list is empty - * - * Default: [EmptyStateIndicator](https://getstream.io/chat/docs/sdk/reactnative/core-components/channel/#emptystateindicator) - * */ - EmptyStateIndicator: React.ComponentType; - /** - * Custom loading indicator to display at bottom of the list, while loading further pages - * - * Default: [ChannelListFooterLoadingIndicator](https://getstream.io/chat/docs/sdk/reactnative/contexts/channels-context/#footerloadingindicator) - */ - FooterLoadingIndicator: React.ComponentType; /** * Incremental number change to force update the FlatList */ @@ -73,33 +46,10 @@ export type ChannelsContextValue = { * Whether or not the FlatList has another page to render */ hasNextPage: boolean; - /** - * Custom indicator to display error at top of list, if loading/pagination error occurs - * - * Default: [ChannelListHeaderErrorIndicator](https://getstream.io/chat/docs/sdk/reactnative/contexts/channels-context/#headererrorindicator) - */ - HeaderErrorIndicator: React.ComponentType; - /** - * Custom indicator to display network-down error at top of list, if there is connectivity issue - * - * Default: [ChannelListHeaderNetworkDownIndicator](https://getstream.io/chat/docs/sdk/reactnative/contexts/channels-context/#headernetworkdownindicator) - */ - HeaderNetworkDownIndicator: React.ComponentType; /** * Initial channels query loading state, triggers the LoadingIndicator */ loadingChannels: boolean; - /** - * Custom indicator to use when there is error in fetching channels - * - * Default: [LoadingErrorIndicator](https://getstream.io/chat/docs/sdk/reactnative/contexts/channels-context/#loadingerrorindicator) - * */ - LoadingErrorIndicator: React.ComponentType; - /** - * Custom loading indicator to use on Channel List - * - * */ - LoadingIndicator: React.ComponentType>; /** * Whether or not additional channels are being loaded, triggers the FooterLoadingIndicator */ @@ -121,12 +71,6 @@ export type ChannelsContextValue = { * Number of skeletons that should show when loading. Default: 6 */ numberOfSkeletons: number; - /** - * Custom UI component to display individual channel list items - * - * Default: [ChannelPreviewView](https://getstream.io/chat/docs/sdk/reactnative/ui-components/channel-preview-view/) - */ - Preview: React.ComponentType; /** * Triggered when the channel list is refreshing, displays a loading spinner at the top of the list */ @@ -159,73 +103,16 @@ export type ChannelsContextValue = { * ``` */ setFlatListRef: (ref: FlatList | null) => void; - /** - * Custom UI component to display loading channel skeletons - * - * Default: [Skeleton](https://getstream.io/chat/docs/sdk/reactnative/contexts/channels-context/#skeleton) - */ - Skeleton: React.ComponentType; /** * Error in channels query, if any */ error?: Error; - ListHeaderComponent?: React.ComponentType; /** * Function to set the currently active channel, acts as a bridge between ChannelList and Channel components * * @param channel A channel object */ onSelect?: (channel: Channel) => void; - /** - * Custom UI component to render preview avatar. - * - * **Default** [ChannelAvatar](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/ChannelPreview/ChannelAvatar.tsx) - */ - PreviewAvatar?: React.ComponentType; - /** - * Custom UI component to render preview of latest message on channel. - * - * **Default** [ChannelPreviewMessage](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/ChannelPreview/ChannelPreviewMessage.tsx) - */ - PreviewMessage?: React.ComponentType; - /** - * Custom UI component to render delivery status of latest message on channel. - * - * **Default** [ChannelMessagePreviewDeliveryStatus](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.tsx) - */ - PreviewMessageDeliveryStatus?: React.ComponentType; - /** - * Custom UI component to render muted status. - * - * **Default** [ChannelMutedStatus](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/ChannelPreview/ChannelPreviewMutedStatus.tsx) - */ - PreviewMutedStatus?: React.ComponentType; - /** - * Custom UI component to render preview avatar. - * - * **Default** [ChannelPreviewStatus](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/ChannelPreview/ChannelPreviewStatus.tsx) - */ - PreviewStatus?: React.ComponentType; - /** - * Custom UI component to render preview avatar. - * - * **Default** [ChannelPreviewTitle](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/ChannelPreview/ChannelPreviewTitle.tsx) - */ - PreviewTitle?: React.ComponentType; - /** - * Custom UI component to render preview avatar. - * - * **Default** [ChannelPreviewUnreadCount](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/ChannelPreview/ChannelPreviewUnreadCount.tsx) - */ - PreviewUnreadCount?: React.ComponentType; - PreviewTypingIndicator?: React.ComponentType; - ChannelDetailsBottomSheet?: React.ComponentType; - /** - * Custom UI component to render preview of last message on channel. - * - * **Default** [ChannelLastMessagePreview](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/ChannelPreview/ChannelLastMessagePreview.tsx) - */ - PreviewLastMessage?: React.ComponentType; getChannelActionItems?: GetChannelActionItems; swipeActionsEnabled?: boolean; diff --git a/package/src/contexts/componentsContext/ComponentsContext.tsx b/package/src/contexts/componentsContext/ComponentsContext.tsx new file mode 100644 index 0000000000..2a74b236cb --- /dev/null +++ b/package/src/contexts/componentsContext/ComponentsContext.tsx @@ -0,0 +1,297 @@ +import React, { PropsWithChildren, useContext, useMemo } from 'react'; + +import type { View } from 'react-native'; + +import type { UserResponse } from 'stream-chat'; + +import { DEFAULT_COMPONENTS } from './defaultComponents'; + +import type { + AttachmentPickerContentProps, + InlineUnreadIndicatorProps, + PollContentProps, + StreamingMessageViewProps, +} from '../../components'; +import type { AttachmentProps } from '../../components/Attachment/Attachment'; +import type { AudioAttachmentProps } from '../../components/Attachment/Audio'; +import type { FileAttachmentProps } from '../../components/Attachment/FileAttachment'; +import type { FileAttachmentGroupProps } from '../../components/Attachment/FileAttachmentGroup'; +import type { FileIconProps } from '../../components/Attachment/FileIcon'; +import type { FilePreviewProps } from '../../components/Attachment/FilePreview'; +import type { GalleryProps } from '../../components/Attachment/Gallery'; +import type { GiphyProps } from '../../components/Attachment/Giphy'; +import type { ImageLoadingFailedIndicatorProps } from '../../components/Attachment/ImageLoadingFailedIndicator'; +import type { UnsupportedAttachmentProps } from '../../components/Attachment/UnsupportedAttachment'; +import type { + URLPreviewCompactProps, + URLPreviewProps, +} from '../../components/Attachment/UrlPreview'; +import type { VideoThumbnailProps } from '../../components/Attachment/VideoThumbnail'; +import type { AutoCompleteSuggestionHeaderProps } from '../../components/AutoCompleteInput/AutoCompleteSuggestionHeader'; +import type { AutoCompleteSuggestionItemProps } from '../../components/AutoCompleteInput/AutoCompleteSuggestionItem'; +import type { AutoCompleteSuggestionListProps } from '../../components/AutoCompleteInput/AutoCompleteSuggestionList'; +import type { InputViewProps } from '../../components/AutoCompleteInput/InputView'; +import type { HeaderErrorProps } from '../../components/ChannelList/ChannelListHeaderErrorIndicator'; +import type { ChannelDetailsBottomSheetProps } from '../../components/ChannelPreview/ChannelDetailsBottomSheet'; +import type { ChannelLastMessagePreviewProps } from '../../components/ChannelPreview/ChannelLastMessagePreview'; +import type { ChannelMessagePreviewDeliveryStatusProps } from '../../components/ChannelPreview/ChannelMessagePreviewDeliveryStatus'; +import type { ChannelPreviewMessageProps } from '../../components/ChannelPreview/ChannelPreviewMessage'; +import type { ChannelPreviewStatusProps } from '../../components/ChannelPreview/ChannelPreviewStatus'; +import type { ChannelPreviewTitleProps } from '../../components/ChannelPreview/ChannelPreviewTitle'; +import type { ChannelPreviewTypingIndicatorProps } from '../../components/ChannelPreview/ChannelPreviewTypingIndicator'; +import type { ChannelPreviewUnreadCountProps } from '../../components/ChannelPreview/ChannelPreviewUnreadCount'; +import type { ChannelPreviewViewProps } from '../../components/ChannelPreview/ChannelPreviewView'; +import type { EmptyStateProps } from '../../components/Indicators/EmptyStateIndicator'; +import type { LoadingErrorProps } from '../../components/Indicators/LoadingErrorIndicator'; +import type { LoadingProps } from '../../components/Indicators/LoadingIndicator'; +import type { KeyboardCompatibleViewProps } from '../../components/KeyboardCompatibleView/KeyboardControllerAvoidingView'; +import type { MessageProps } from '../../components/Message/Message'; +import type { MessagePinnedHeaderProps } from '../../components/Message/MessageItemView/Headers/MessagePinnedHeader'; +import type { MessageReminderHeaderProps } from '../../components/Message/MessageItemView/Headers/MessageReminderHeader'; +import type { MessageSavedForLaterHeaderProps } from '../../components/Message/MessageItemView/Headers/MessageSavedForLaterHeader'; +import type { SentToChannelHeaderProps } from '../../components/Message/MessageItemView/Headers/SentToChannelHeader'; +import type { MessageAuthorProps } from '../../components/Message/MessageItemView/MessageAuthor'; +import type { MessageBlockedProps } from '../../components/Message/MessageItemView/MessageBlocked'; +import type { MessageBounceProps } from '../../components/Message/MessageItemView/MessageBounce'; +import type { MessageContentProps } from '../../components/Message/MessageItemView/MessageContent'; +import type { MessageDeletedProps } from '../../components/Message/MessageItemView/MessageDeleted'; +import type { MessageFooterProps } from '../../components/Message/MessageItemView/MessageFooter'; +import type { MessageHeaderProps } from '../../components/Message/MessageItemView/MessageHeader'; +import type { MessageItemViewProps } from '../../components/Message/MessageItemView/MessageItemView'; +import type { MessageRepliesProps } from '../../components/Message/MessageItemView/MessageReplies'; +import type { MessageRepliesAvatarsProps } from '../../components/Message/MessageItemView/MessageRepliesAvatars'; +import type { MessageStatusProps } from '../../components/Message/MessageItemView/MessageStatus'; +import type { MessageTextProps } from '../../components/Message/MessageItemView/MessageTextContainer'; +import type { MessageTimestampProps } from '../../components/Message/MessageItemView/MessageTimestamp'; +import type { ReactionListBottomProps } from '../../components/Message/MessageItemView/ReactionList/ReactionListBottom'; +import type { ReactionListClusteredProps } from '../../components/Message/MessageItemView/ReactionList/ReactionListClustered'; +import type { + ReactionListCountItemProps, + ReactionListItemProps, +} from '../../components/Message/MessageItemView/ReactionList/ReactionListItem'; +import type { ReactionListItemWrapperProps } from '../../components/Message/MessageItemView/ReactionList/ReactionListItemWrapper'; +import type { ReactionListTopProps } from '../../components/Message/MessageItemView/ReactionList/ReactionListTop'; +import type { AttachmentUploadPreviewListProps } from '../../components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList'; +import type { + FileUploadNotSupportedIndicatorProps, + FileUploadRetryIndicatorProps, + ImageUploadRetryIndicatorProps, +} from '../../components/MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator'; +import type { AudioAttachmentUploadPreviewProps } from '../../components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview'; +import type { FileAttachmentUploadPreviewProps } from '../../components/MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview'; +import type { ImageAttachmentUploadPreviewProps } from '../../components/MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview'; +import type { VideoAttachmentUploadPreviewProps } from '../../components/MessageInput/components/AttachmentPreview/VideoAttachmentUploadPreview'; +import type { AudioRecorderProps } from '../../components/MessageInput/components/AudioRecorder/AudioRecorder'; +import type { AudioRecordingButtonProps } from '../../components/MessageInput/components/AudioRecorder/AudioRecordingButton'; +import type { AudioRecordingInProgressProps } from '../../components/MessageInput/components/AudioRecorder/AudioRecordingInProgress'; +import type { AudioRecordingLockIndicatorProps } from '../../components/MessageInput/components/AudioRecorder/AudioRecordingLockIndicator'; +import type { AudioRecordingWaveformProps } from '../../components/MessageInput/components/AudioRecorder/AudioRecordingWaveform'; +import type { InputButtonsProps } from '../../components/MessageInput/components/InputButtons'; +import type { AttachButtonProps } from '../../components/MessageInput/components/InputButtons/AttachButton'; +import type { SendButtonProps } from '../../components/MessageInput/components/OutputButtons/SendButton'; +import type { MessageComposerProps } from '../../components/MessageInput/MessageComposer'; +import type { StopMessageStreamingButtonProps } from '../../components/MessageInput/StopMessageStreamingButton'; +import type { DateHeaderProps } from '../../components/MessageList/DateHeader'; +import type { InlineDateSeparatorProps } from '../../components/MessageList/InlineDateSeparator'; +import type { MessageListProps } from '../../components/MessageList/MessageList'; +import type { MessageSystemProps } from '../../components/MessageList/MessageSystem'; +import type { ScrollToBottomButtonProps } from '../../components/MessageList/ScrollToBottomButton'; +import type { StickyHeaderProps } from '../../components/MessageList/StickyHeader'; +import type { TypingIndicatorContainerProps } from '../../components/MessageList/TypingIndicatorContainer'; +import type { UnreadMessagesNotificationProps } from '../../components/MessageList/UnreadMessagesNotification'; +import type { MessageActionListProps } from '../../components/MessageMenu/MessageActionList'; +import type { MessageActionListItemProps } from '../../components/MessageMenu/MessageActionListItem'; +import type { MessageMenuProps } from '../../components/MessageMenu/MessageMenu'; +import type { MessageReactionPickerProps } from '../../components/MessageMenu/MessageReactionPicker'; +import type { MessageUserReactionsProps } from '../../components/MessageMenu/MessageUserReactions'; +import type { MessageUserReactionsAvatarProps } from '../../components/MessageMenu/MessageUserReactionsAvatar'; +import type { MessageUserReactionsItemProps } from '../../components/MessageMenu/MessageUserReactionsItem'; +import type { ReplyProps } from '../../components/Reply/Reply'; +import type { ChannelAvatarProps } from '../../components/ui/Avatar/ChannelAvatar'; +import type { MessageLocationProps } from '../messagesContext/MessagesContext'; + +/** + * All overridable UI components in the SDK. + * Every key is optional — only specify the components you want to override. + */ +export type ComponentOverrides = { + // === MessagesContext components === + Attachment?: React.ComponentType; + AudioAttachment?: React.ComponentType; + DateHeader?: React.ComponentType; + UnsupportedAttachment?: React.ComponentType; + FilePreview?: React.ComponentType; + FileAttachment?: React.ComponentType; + FileAttachmentGroup?: React.ComponentType; + FileAttachmentIcon?: React.ComponentType; + Gallery?: React.ComponentType; + Giphy?: React.ComponentType; + ImageLoadingFailedIndicator?: React.ComponentType; + ImageLoadingIndicator?: React.ComponentType; + InlineDateSeparator?: React.ComponentType; + InlineUnreadIndicator?: React.ComponentType; + Message?: React.ComponentType; + MessageActionList?: React.ComponentType; + MessageActionListItem?: React.ComponentType; + MessageAuthor?: React.ComponentType; + MessageBlocked?: React.ComponentType; + MessageBounce?: React.ComponentType; + MessageContent?: React.ComponentType; + MessageContentTopView?: React.ComponentType; + MessageContentLeadingView?: React.ComponentType; + MessageContentTrailingView?: React.ComponentType; + MessageContentBottomView?: React.ComponentType; + MessageDeleted?: React.ComponentType; + MessageError?: React.ComponentType; + MessageFooter?: React.ComponentType; + MessageHeader?: React.ComponentType; + MessageList?: React.ComponentType; + MessageLocation?: React.ComponentType; + MessageMenu?: React.ComponentType; + MessagePinnedHeader?: React.ComponentType; + MessageReminderHeader?: React.ComponentType; + MessageSavedForLaterHeader?: React.ComponentType; + SentToChannelHeader?: React.ComponentType; + MessageReactionPicker?: React.ComponentType; + MessageReplies?: React.ComponentType; + MessageRepliesAvatars?: React.ComponentType; + MessageSpacer?: React.ComponentType; + MessageItemView?: React.ComponentType< + MessageItemViewProps & { ref?: React.RefObject } + >; + MessageStatus?: React.ComponentType; + MessageSwipeContent?: React.ComponentType; + MessageSystem?: React.ComponentType; + MessageText?: React.ComponentType; + MessageTimestamp?: React.ComponentType; + MessageUserReactions?: React.ComponentType; + MessageUserReactionsAvatar?: React.ComponentType; + MessageUserReactionsItem?: React.ComponentType; + Reply?: React.ComponentType; + ScrollToBottomButton?: React.ComponentType; + StreamingMessageView?: React.ComponentType; + TypingIndicator?: React.ComponentType; + TypingIndicatorContainer?: React.ComponentType; + UnreadMessagesNotification?: React.ComponentType; + UrlPreview?: React.ComponentType; + URLPreviewCompact?: React.ComponentType; + VideoThumbnail?: React.ComponentType; + PollContent?: React.ComponentType; + ReactionListBottom?: React.ComponentType; + ReactionListTop?: React.ComponentType; + ReactionListClustered?: React.ComponentType; + ReactionListItem?: React.ComponentType; + ReactionListItemWrapper?: React.ComponentType; + ReactionListCountItem?: React.ComponentType; + + // === MessageInputContext components === + AttachButton?: React.ComponentType; + AudioRecorder?: React.ComponentType; + AudioRecordingInProgress?: React.ComponentType; + AudioRecordingLockIndicator?: React.ComponentType; + AudioRecordingPreview?: React.ComponentType; + AudioRecordingWaveform?: React.ComponentType; + AutoCompleteSuggestionHeader?: React.ComponentType; + AutoCompleteSuggestionItem?: React.ComponentType; + AutoCompleteSuggestionList?: React.ComponentType; + AttachmentPickerSelectionBar?: React.ComponentType; + AttachmentUploadPreviewList?: React.ComponentType; + AudioAttachmentUploadPreview?: React.ComponentType; + ImageAttachmentUploadPreview?: React.ComponentType; + FileAttachmentUploadPreview?: React.ComponentType; + VideoAttachmentUploadPreview?: React.ComponentType; + FileUploadInProgressIndicator?: React.ComponentType; + FileUploadRetryIndicator?: React.ComponentType; + FileUploadNotSupportedIndicator?: React.ComponentType; + ImageUploadInProgressIndicator?: React.ComponentType; + ImageUploadRetryIndicator?: React.ComponentType; + ImageUploadNotSupportedIndicator?: React.ComponentType; + CooldownTimer?: React.ComponentType; + SendButton?: React.ComponentType; + ShowThreadMessageInChannelButton?: React.ComponentType<{ threadList?: boolean }>; + MessageComposerLeadingView?: React.ComponentType; + MessageComposerTrailingView?: React.ComponentType; + MessageInputHeaderView?: React.ComponentType; + MessageInputFooterView?: React.ComponentType; + MessageInputLeadingView?: React.ComponentType; + MessageInputTrailingView?: React.ComponentType; + StartAudioRecordingButton?: React.ComponentType; + StopMessageStreamingButton?: React.ComponentType | null; + Input?: React.ComponentType< + Omit & + InputButtonsProps & { + getUsers: () => UserResponse[]; + } + >; + InputView?: React.ComponentType; + InputButtons?: React.ComponentType; + SendMessageDisallowedIndicator?: React.ComponentType; + CreatePollContent?: React.ComponentType; + + // === ChannelContext components === + EmptyStateIndicator?: React.ComponentType; + LoadingIndicator?: React.ComponentType; + LoadingErrorIndicator?: React.ComponentType; + NetworkDownIndicator?: React.ComponentType; + StickyHeader?: React.ComponentType; + KeyboardCompatibleView?: React.ComponentType; + + // === AttachmentPickerContext components === + ImageOverlaySelectedComponent?: React.ComponentType<{ index: number }>; + AttachmentPickerIOSSelectMorePhotos?: React.ComponentType; + AttachmentPickerContent?: React.ComponentType; + + // === ChannelsContext components === + FooterLoadingIndicator?: React.ComponentType; + HeaderErrorIndicator?: React.ComponentType; + HeaderNetworkDownIndicator?: React.ComponentType; + Preview?: React.ComponentType; + PreviewAvatar?: React.ComponentType; + PreviewMessage?: React.ComponentType; + PreviewMessageDeliveryStatus?: React.ComponentType; + PreviewMutedStatus?: React.ComponentType; + PreviewStatus?: React.ComponentType; + PreviewTitle?: React.ComponentType; + PreviewUnreadCount?: React.ComponentType; + PreviewTypingIndicator?: React.ComponentType; + PreviewLastMessage?: React.ComponentType; + ChannelDetailsBottomSheet?: React.ComponentType; + Skeleton?: React.ComponentType; + ListHeaderComponent?: React.ComponentType; +}; + +const ComponentsContext = React.createContext(DEFAULT_COMPONENTS); + +/** + * Provider to override UI components at any level of the tree. + * Supports nesting — inner overrides merge over outer ones (closest wins). + * + * @example + * ```tsx + * + * + * + * + * + * + * ``` + */ +export const WithComponents = ({ + children, + value, +}: PropsWithChildren<{ value: ComponentOverrides }>) => { + const parent = useContext(ComponentsContext); + const merged = useMemo( + () => ({ ...parent, ...value }), + + [parent, value], + ); + return {children}; +}; + +/** + * Hook to access resolved component overrides. + * Returns all components with defaults filled in. + */ +export const useComponentsContext = () => + useContext(ComponentsContext) as Required; diff --git a/package/src/contexts/componentsContext/PLAN.md b/package/src/contexts/componentsContext/PLAN.md new file mode 100644 index 0000000000..2637bbcaea --- /dev/null +++ b/package/src/contexts/componentsContext/PLAN.md @@ -0,0 +1,180 @@ +# Plan: `WithComponents` Context Provider + +## Context + +The SDK prop-drills 120+ component overrides through `` → `useCreate*Context` hooks → context values. Each component name is listed **4 times** (destructured from props → passed to hook → destructured in hook → listed in useMemo). Consumers then read components back out via `useMessagesContext()`, `useMessageInputContext()`, etc. + +**Goal**: Replace this entire pipeline with a single `ComponentsContext`. Component overrides are **removed** from all existing contexts. Consumers read components via `useComponentsContext()` instead. + +```tsx +// User API + + + + + + +``` + +## Design Principle + +**All components are read from `useComponentsContext()`. All other contexts only provide data + APIs — never components.** + +No context besides `ComponentsContext` should carry component references. This is the single rule that drives every change in this plan. + +## Architecture + +### Before + +``` +Channel props (90+ component overrides with defaults) + → useCreateMessagesContext (receives 60+ component params, maps into useMemo) + → MessagesContext carries components + runtime data + → Consumer: const { Message } = useMessagesContext() +``` + +### After + +``` +DEFAULT_COMPONENTS (static map) + → ComponentsContext (defaults; user overrides via WithComponents) + → Consumer: const { Message } = useComponentsContext() + +Channel props (runtime/config only) + → useCreateMessagesContext (runtime data only, no components) + → MessagesContext carries ONLY data + APIs + → Consumer: const { deleteMessage } = useMessagesContext() +``` + +## Scope + +### What changes + +1. **Existing context types** (`MessagesContextValue`, `InputMessageInputContextValue`, `ChannelContextValue`, `ChannelsContextValue`, `AttachmentPickerContextValue`) — remove all component-type keys +2. **`useCreate*Context` hooks** — remove all component params, stop mapping them +3. **Channel.tsx** — remove ~90 component imports, ~90 destructuring defaults, ~90 forwarding lines +4. **ChannelList.tsx** — remove ~19 component props and forwarding +5. **~117 consumer callsites across ~97 files** — switch from `useXContext()` to `useComponentsContext()` for component reads + +### What doesn't change + +- Runtime data flow (callbacks like `deleteMessage`, `sendReaction`, state like `targetedMessage`) stays in existing contexts +- Consumer reads of runtime data (`const { deleteMessage } = useMessagesContext()`) are untouched +- `WithComponents` nesting semantics (inner wins, like standard React context) + +## New Files + +| File | Purpose | +| -------------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| `package/src/contexts/componentsContext/ComponentsContext.tsx` | `ComponentOverrides` type, `WithComponents` provider, `useComponentsContext()` hook | +| `package/src/contexts/componentsContext/defaultComponents.ts` | All default component imports → `DEFAULT_COMPONENTS` map | + +Both already drafted in the repo. + +## Implementation Steps + +### Step 1: Finalize `ComponentsContext.tsx` and `defaultComponents.ts` + +Already drafted. Key design: + +- `ComponentOverrides`: flat map, all keys optional, explicitly typed per component +- Context default = `DEFAULT_COMPONENTS` → `useComponentsContext()` always returns resolved values +- `WithComponents`: merges `{ ...parent, ...value }` (inner wins) +- `ResolvedComponents` = `Required` for the return type + +Special cases: + +- `FlatList` — from `NativeHandlers.FlatList` at runtime. Keep as a runtime prop in MessagesContext, not in ComponentsContext. +- `StopMessageStreamingButton` — can be `null` (explicitly hide). The type in ComponentOverrides allows `| null`. + +### Step 2: Strip component keys from existing context value types + +**`MessagesContextValue`** (`package/src/contexts/messagesContext/MessagesContext.tsx`): +Remove ~60 component keys (Attachment, AudioAttachment, DateHeader, Message, MessageContent, Reply, etc.). Keep runtime keys only (deleteMessage, deleteReaction, dismissKeyboardOnMessageTouch, giphyVersion, messageContentOrder, etc.). + +**`InputMessageInputContextValue`** (`package/src/contexts/messageInputContext/MessageInputContext.tsx`): +Remove ~35 component keys (AttachButton, AudioRecorder, SendButton, Input, InputView, etc.). Keep runtime keys only (asyncMessagesLockDistance, audioRecordingEnabled, editMessage, sendMessage, etc.). + +**`ChannelContextValue`** (`package/src/contexts/channelContext/ChannelContext.tsx`): +Remove 4 component keys (EmptyStateIndicator, LoadingIndicator, NetworkDownIndicator, StickyHeader). + +**`ChannelsContextValue`** (`package/src/contexts/channelsContext/ChannelsContext.tsx`): +Remove ~19 component keys (Preview, PreviewAvatar, PreviewMessage, Skeleton, FooterLoadingIndicator, etc.). + +**`AttachmentPickerContextValue`** (`package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx`): +Remove 3 component keys (ImageOverlaySelectedComponent, AttachmentPickerSelectionBar, AttachmentPickerContent). + +### Step 3: Simplify `useCreate*Context` hooks + +Each hook drops all component params and stops mapping them into useMemo: + +- **`useCreateMessagesContext`** (`package/src/components/Channel/hooks/useCreateMessagesContext.ts`): ~60 component params removed, keep ~30 runtime params +- **`useCreateInputMessageInputContext`** (`package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts`): ~35 component params removed, keep ~15 runtime params +- **`useCreateChannelContext`** (`package/src/components/Channel/hooks/useCreateChannelContext.ts`): 4 component params removed +- **`useCreateChannelsContext`** (`package/src/components/ChannelList/hooks/useCreateChannelsContext.ts`): ~19 component params removed, keep ~20 runtime params + +### Step 4: Simplify Channel.tsx + +- Remove ~90 default component imports (lines 114-223) +- Remove component keys from `ChannelPropsWithContext` type +- Remove component destructuring defaults from `ChannelWithContext` +- Remove component values from `useCreateMessagesContext()`, `useCreateInputMessageInputContext()`, `useCreateChannelContext()` calls +- Remove component values from `attachmentPickerContext` useMemo +- `LoadingErrorIndicator` and `KeyboardCompatibleView` are used directly in Channel's JSX — read from `useComponentsContext()` or keep as Channel-specific props + +### Step 5: Simplify ChannelList.tsx + +- Remove component keys from `ChannelListProps` type +- Remove default component imports +- Remove component values from `useCreateChannelsContext()` call + +### Step 6: Update ~117 consumer callsites + +Switch component destructuring from old context hooks to `useComponentsContext()`: + +```tsx +// Before +const { Message, MessageStatus, MessageTimestamp } = useMessagesContext(); +const { deleteMessage } = useMessagesContext(); + +// After +const { Message, MessageStatus, MessageTimestamp } = useComponentsContext(); +const { deleteMessage } = useMessagesContext(); +``` + +**Key files by volume** (largest consumers): + +- `components/Message/MessageItemView/MessageItemView.tsx` — 15+ component keys +- `components/Message/MessageItemView/MessageContent.tsx` — 15+ component keys +- `components/MessageList/MessageList.tsx` — 10+ component keys +- `components/MessageList/MessageFlashList.tsx` — 10+ component keys +- `components/MessageInput/MessageComposer.tsx` — 25+ component keys +- `components/Attachment/Attachment.tsx` — 10+ component keys +- `components/ChannelList/ChannelListView.tsx` — multiple component keys +- `components/ChannelPreview/ChannelPreviewView.tsx` — multiple component keys + +Many other files destructure just 1-2 component keys from context — straightforward replacements. + +### Step 7: Update exports + +- `package/src/contexts/index.ts` — add `export * from './componentsContext/ComponentsContext'` +- `package/src/index.ts` — verify `WithComponents`, `ComponentOverrides`, `useComponentsContext` are exported + +### Step 8: Update tests + +Tests that pass component overrides as Channel/ChannelList props will need to wrap in `` instead. Mock builders that set up context values with component overrides may also need updating. + +## Edge Cases + +- **Shared names**: `EmptyStateIndicator`, `LoadingIndicator` exist in both Channel and ChannelList. One key in flat map — users use nesting for different overrides per area. +- **Mixed destructuring**: Some consumers destructure both components and runtime data from the same `useMessagesContext()` call. These need to be split into two calls. +- **`FlatList`**: Runtime-resolved from NativeHandlers. Stays in MessagesContext as a runtime value, not in ComponentsContext. +- **`StopMessageStreamingButton`**: Supports `null` to hide. ComponentOverrides type allows `| null`. + +## Verification + +1. `cd package && yarn build` — type-checks and builds +2. `cd package && yarn test:unit` — tests pass (after updating test fixtures) +3. `cd package && yarn lint` — no lint errors +4. Manual: `` → verify override appears +5. Verify nesting: inner `WithComponents` wins over outer diff --git a/package/src/contexts/componentsContext/defaultComponents.ts b/package/src/contexts/componentsContext/defaultComponents.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/package/src/contexts/messageInputContext/MessageInputContext.tsx b/package/src/contexts/messageInputContext/MessageInputContext.tsx index 3dfe213f1b..f48fc32bd5 100644 --- a/package/src/contexts/messageInputContext/MessageInputContext.tsx +++ b/package/src/contexts/messageInputContext/MessageInputContext.tsx @@ -18,40 +18,14 @@ import { Message as StreamMessage, UpdateMessageOptions, UploadRequestFn, - UserResponse, } from 'stream-chat'; import { useCreateMessageInputContext } from './hooks/useCreateMessageInputContext'; import { useMessageComposer } from './hooks/useMessageComposer'; -import { - AutoCompleteSuggestionHeaderProps, - AutoCompleteSuggestionItemProps, - AutoCompleteSuggestionListProps, - FileUploadNotSupportedIndicatorProps, - FileUploadRetryIndicatorProps, - ImageUploadRetryIndicatorProps, - PollContentProps, - StopMessageStreamingButtonProps, -} from '../../components'; -import type { InputViewProps } from '../../components/AutoCompleteInput/InputView'; import { dismissKeyboard } from '../../components/KeyboardCompatibleView/KeyboardControllerAvoidingView'; import { parseLinksFromText } from '../../components/Message/MessageItemView/utils/parseLinks'; -import { AttachmentUploadPreviewListProps } from '../../components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList'; -import { AudioAttachmentUploadPreviewProps } from '../../components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview'; -import { FileAttachmentUploadPreviewProps } from '../../components/MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview'; -import { ImageAttachmentUploadPreviewProps } from '../../components/MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview'; -import { VideoAttachmentUploadPreviewProps } from '../../components/MessageInput/components/AttachmentPreview/VideoAttachmentUploadPreview'; -import type { AudioRecorderProps } from '../../components/MessageInput/components/AudioRecorder/AudioRecorder'; -import type { AudioRecordingButtonProps } from '../../components/MessageInput/components/AudioRecorder/AudioRecordingButton'; -import type { AudioRecordingInProgressProps } from '../../components/MessageInput/components/AudioRecorder/AudioRecordingInProgress'; -import type { AudioRecordingLockIndicatorProps } from '../../components/MessageInput/components/AudioRecorder/AudioRecordingLockIndicator'; -import type { AudioRecordingWaveformProps } from '../../components/MessageInput/components/AudioRecorder/AudioRecordingWaveform'; -import type { AttachButtonProps } from '../../components/MessageInput/components/InputButtons/AttachButton'; -import type { InputButtonsProps } from '../../components/MessageInput/components/InputButtons/index'; -import type { SendButtonProps } from '../../components/MessageInput/components/OutputButtons/SendButton'; import { useAudioRecorder } from '../../components/MessageInput/hooks/useAudioRecorder'; -import type { MessageComposerProps } from '../../components/MessageInput/MessageComposer'; import { useStableCallback } from '../../hooks/useStableCallback'; import { createAttachmentsCompositionMiddleware, @@ -125,69 +99,16 @@ export type InputMessageInputContextValue = { * message. */ asyncMessagesSlideToCancelDistance: number; - /** - * Custom UI component for attach button. - * - * Defaults to and accepts same props as: - * [AttachButton](https://getstream.io/chat/docs/sdk/reactnative/ui-components/attach-button/) - */ - AttachButton: React.ComponentType; - /** - * Custom UI component for audio recorder UI. - * - * Defaults to and accepts same props as: - * [AudioRecorder](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageInput/AudioRecorder.tsx) - */ - AudioRecorder: React.ComponentType; /** * Controls whether the async audio feature is enabled. */ audioRecordingEnabled: boolean; - /** - * Custom UI component to render audio recording in progress. - * - * **Default** - * [AudioRecordingInProgress](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingInProgress.tsx) - */ - AudioRecordingInProgress: React.ComponentType; - /** - * Custom UI component for audio recording lock indicator. - * - * Defaults to and accepts same props as: - * [AudioRecordingLockIndicator](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingLockIndicator.tsx) - */ - AudioRecordingLockIndicator: React.ComponentType; - /** - * Custom UI component to render audio recording preview. - * - * **Default** - * [AudioRecordingPreview](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingPreview.tsx) - */ - AudioRecordingPreview: React.ComponentType; - /** - * Custom UI component to render audio recording waveform. - * - * **Default** - * [AudioRecordingWaveform](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingWaveform.tsx) - */ - AudioRecordingWaveform: React.ComponentType; - - AutoCompleteSuggestionHeader: React.ComponentType; - AutoCompleteSuggestionItem: React.ComponentType; - AutoCompleteSuggestionList: React.ComponentType; /** * Height of the image picker bottom sheet when opened. * @type number * @default 40% of window height */ attachmentPickerBottomSheetHeight: number; - /** - * Custom UI component for AttachmentPickerSelectionBar - * - * **Default: ** - * [AttachmentPickerSelectionBar](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/components/AttachmentPicker/components/AttachmentPickerSelectionBar.tsx) - */ - AttachmentPickerSelectionBar: React.ComponentType; /** * Height of the attachment selection bar displayed on the attachment picker. * @type number @@ -196,28 +117,6 @@ export type InputMessageInputContextValue = { */ attachmentSelectionBarHeight: number; - AttachmentUploadPreviewList: React.ComponentType; - AudioAttachmentUploadPreview: React.ComponentType; - ImageAttachmentUploadPreview: React.ComponentType; - FileAttachmentUploadPreview: React.ComponentType; - VideoAttachmentUploadPreview: React.ComponentType; - - FileUploadInProgressIndicator: React.ComponentType; - FileUploadRetryIndicator: React.ComponentType; - FileUploadNotSupportedIndicator: React.ComponentType; - ImageUploadInProgressIndicator: React.ComponentType; - ImageUploadRetryIndicator: React.ComponentType; - ImageUploadNotSupportedIndicator: React.ComponentType; - - /** - * Custom UI component to display the remaining cooldown a user will have to wait before - * being allowed to send another message. This component is displayed in place of the - * send button for the MessageComposer component. - * - * **default** - * [CooldownTimer](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageInput/CooldownTimer.tsx) - */ - CooldownTimer: React.ComponentType; editMessage: (params: { localMessage: LocalMessage; options?: UpdateMessageOptions; @@ -233,58 +132,11 @@ export type InputMessageInputContextValue = { /** When false, ImageSelectorIcon will be hidden */ hasImagePicker: boolean; - /** - * Custom UI component for send button. - * - * Defaults to and accepts same props as: - * [SendButton](https://getstream.io/chat/docs/sdk/reactnative/ui-components/send-button/) - */ - SendButton: React.ComponentType; sendMessage: (params: { localMessage: LocalMessage; message: StreamMessage; options?: SendMessageOptions; }) => Promise; - /** - * Custom UI component to render checkbox with text ("Also send to channel") in Thread's input box. - * When ticked, message will also be sent in parent channel. - */ - ShowThreadMessageInChannelButton: React.ComponentType<{ - threadList?: boolean; - }>; - /** - * Custom UI component to override leading side of composer container. - */ - MessageComposerLeadingView: React.ComponentType; - /** - * Custom UI component to override trailing side of composer container. - */ - MessageComposerTrailingView: React.ComponentType; - /** - * Custom UI component to override message input header content. - */ - MessageInputHeaderView: React.ComponentType; - /** - * Custom UI component to override message input footer content. - */ - MessageInputFooterView: React.ComponentType; - /** - * Custom UI component to override leading side of input row. - */ - MessageInputLeadingView: React.ComponentType; - /** - * Custom UI component to override trailing side of input row. - */ - MessageInputTrailingView: React.ComponentType; - - /** - * Custom UI component for audio recording mic button. - * - * Defaults to and accepts same props as: - * [AudioRecordingButton](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingButton.tsx) - */ - StartAudioRecordingButton: React.ComponentType; - StopMessageStreamingButton: React.ComponentType | null; /** * Additional props for underlying TextInput component. These props will be forwarded as it is to TextInput component. * @@ -301,10 +153,6 @@ export type InputMessageInputContextValue = { */ compressImageQuality?: number; - /** - * Override the entire content of the CreatePoll component. The component has full access to the useCreatePollContext() hook. - * */ - CreatePollContent?: React.ComponentType; /** * Vertical gap between poll options in poll creation dialog. */ @@ -331,40 +179,7 @@ export type InputMessageInputContextValue = { */ messageInputFloating: boolean; - /** - * Custom UI component for AutoCompleteInput. - * Has access to all of [MessageInputContext](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/contexts/messageInputContext/MessageInputContext.tsx) - */ - Input?: React.ComponentType< - Omit & - InputButtonsProps & { - getUsers: () => UserResponse[]; - } - >; - /** - * Custom UI component to override the combined input body view. - * Defaults to - * [InputView](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/AutoCompleteInput/InputView.tsx) - */ - InputView: React.ComponentType; - /** - * Custom UI component to override buttons on left side of input box - * Defaults to - * [InputButtons](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageInput/InputButtons.tsx), - * which contain following components/buttons: - * - * - AttachButton - * - CommandsButtom - * - * You have access to following prop functions: - * - * - closeAttachmentPicker - * - openAttachmentPicker - * - openCommandsPicker - */ - InputButtons?: React.ComponentType; openPollCreationDialog?: ({ sendMessage }: Pick) => void; - SendMessageDisallowedIndicator?: React.ComponentType; /** * ref for input setter function * diff --git a/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts b/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts index 54a62a7b7e..5343dad8e0 100644 --- a/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts +++ b/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts @@ -9,75 +9,38 @@ export const useCreateMessageInputContext = ({ asyncMessagesMinimumPressDuration, asyncMessagesSlideToCancelDistance, audioRecordingSendOnComplete, - AttachButton, attachmentPickerBottomSheetHeight, - AttachmentPickerSelectionBar, attachmentSelectionBarHeight, - AttachmentUploadPreviewList, - AudioAttachmentUploadPreview, - AudioRecorder, audioRecordingEnabled, - AudioRecordingInProgress, - AudioRecordingLockIndicator, - AudioRecordingPreview, - AudioRecordingWaveform, - AutoCompleteSuggestionHeader, - AutoCompleteSuggestionItem, - AutoCompleteSuggestionList, closeAttachmentPicker, closePollCreationDialog, compressImageQuality, - CooldownTimer, - CreatePollContent, createPollOptionGap, editMessage, - FileAttachmentUploadPreview, - FileUploadInProgressIndicator, - FileUploadRetryIndicator, - FileUploadNotSupportedIndicator, - ImageUploadInProgressIndicator, - ImageUploadRetryIndicator, - ImageUploadNotSupportedIndicator, handleAttachButtonPress, hasCameraPicker, hasCommands, hasFilePicker, hasImagePicker, - ImageAttachmentUploadPreview, - Input, - InputView, inputBoxRef, - InputButtons, - MessageComposerLeadingView, - MessageComposerTrailingView, messageInputFloating, messageInputHeightStore, - MessageInputFooterView, - MessageInputHeaderView, - MessageInputLeadingView, - MessageInputTrailingView, openAttachmentPicker, openPollCreationDialog, pickAndUploadImageFromNativePicker, pickFile, - SendButton, sendMessage, - SendMessageDisallowedIndicator, setInputBoxRef, setInputRef, showPollCreationDialog, - ShowThreadMessageInChannelButton, - StartAudioRecordingButton, - StopMessageStreamingButton, takeAndUploadImage, - thread, uploadNewFile, - VideoAttachmentUploadPreview, audioRecorderManager, startVoiceRecording, deleteVoiceRecording, uploadVoiceRecording, stopVoiceRecording, + thread, }: MessageInputContextValue & Pick) => { const threadId = thread?.id; @@ -88,69 +51,32 @@ export const useCreateMessageInputContext = ({ asyncMessagesMinimumPressDuration, asyncMessagesSlideToCancelDistance, audioRecordingSendOnComplete, - AttachButton, attachmentPickerBottomSheetHeight, - AttachmentPickerSelectionBar, attachmentSelectionBarHeight, - AttachmentUploadPreviewList, - AudioAttachmentUploadPreview, - AudioRecorder, audioRecordingEnabled, - AudioRecordingInProgress, - AudioRecordingLockIndicator, - AudioRecordingPreview, - AudioRecordingWaveform, - AutoCompleteSuggestionHeader, - AutoCompleteSuggestionItem, - AutoCompleteSuggestionList, closeAttachmentPicker, closePollCreationDialog, compressImageQuality, - CooldownTimer, - CreatePollContent, createPollOptionGap, editMessage, - FileAttachmentUploadPreview, - FileUploadInProgressIndicator, - FileUploadRetryIndicator, - FileUploadNotSupportedIndicator, - ImageUploadInProgressIndicator, - ImageUploadRetryIndicator, - ImageUploadNotSupportedIndicator, handleAttachButtonPress, hasCameraPicker, hasCommands, hasFilePicker, hasImagePicker, - ImageAttachmentUploadPreview, - Input, - InputView, inputBoxRef, - InputButtons, - MessageComposerLeadingView, - MessageComposerTrailingView, messageInputFloating, messageInputHeightStore, - MessageInputFooterView, - MessageInputHeaderView, - MessageInputLeadingView, - MessageInputTrailingView, openAttachmentPicker, openPollCreationDialog, pickAndUploadImageFromNativePicker, pickFile, - SendButton, sendMessage, - SendMessageDisallowedIndicator, setInputBoxRef, setInputRef, showPollCreationDialog, - ShowThreadMessageInChannelButton, - StartAudioRecordingButton, - StopMessageStreamingButton, takeAndUploadImage, uploadNewFile, - VideoAttachmentUploadPreview, audioRecorderManager, startVoiceRecording, deleteVoiceRecording, diff --git a/package/src/contexts/messagesContext/MessagesContext.tsx b/package/src/contexts/messagesContext/MessagesContext.tsx index 5b1b3c9572..210ed7e24e 100644 --- a/package/src/contexts/messagesContext/MessagesContext.tsx +++ b/package/src/contexts/messagesContext/MessagesContext.tsx @@ -1,6 +1,6 @@ import React, { PropsWithChildren, useContext } from 'react'; -import { PressableProps, View, ViewProps } from 'react-native'; +import { PressableProps, ViewProps } from 'react-native'; import type { Attachment, @@ -12,79 +12,14 @@ import type { MessageResponse, } from 'stream-chat'; -import type { - InlineUnreadIndicatorProps, - PollContentProps, - StreamingMessageViewProps, -} from '../../components'; -import type { AttachmentProps } from '../../components/Attachment/Attachment'; -import type { AudioAttachmentProps } from '../../components/Attachment/Audio'; -import type { FileAttachmentProps } from '../../components/Attachment/FileAttachment'; -import type { FileAttachmentGroupProps } from '../../components/Attachment/FileAttachmentGroup'; -import type { FileIconProps } from '../../components/Attachment/FileIcon'; -import { FilePreviewProps } from '../../components/Attachment/FilePreview'; -import type { GalleryProps } from '../../components/Attachment/Gallery'; -import type { GiphyProps } from '../../components/Attachment/Giphy'; -import type { ImageLoadingFailedIndicatorProps } from '../../components/Attachment/ImageLoadingFailedIndicator'; -import { UnsupportedAttachmentProps } from '../../components/Attachment/UnsupportedAttachment'; -import type { - URLPreviewCompactProps, - URLPreviewProps, -} from '../../components/Attachment/UrlPreview'; -import type { VideoThumbnailProps } from '../../components/Attachment/VideoThumbnail'; -import type { - MessagePressableHandlerPayload, - MessageProps, -} from '../../components/Message/Message'; -import type { MessagePinnedHeaderProps } from '../../components/Message/MessageItemView/Headers/MessagePinnedHeader'; -import type { MessageReminderHeaderProps } from '../../components/Message/MessageItemView/Headers/MessageReminderHeader'; -import type { MessageSavedForLaterHeaderProps } from '../../components/Message/MessageItemView/Headers/MessageSavedForLaterHeader'; -import type { SentToChannelHeaderProps } from '../../components/Message/MessageItemView/Headers/SentToChannelHeader'; -import type { MessageAuthorProps } from '../../components/Message/MessageItemView/MessageAuthor'; -import type { MessageBlockedProps } from '../../components/Message/MessageItemView/MessageBlocked'; -import type { MessageBounceProps } from '../../components/Message/MessageItemView/MessageBounce'; -import type { MessageContentProps } from '../../components/Message/MessageItemView/MessageContent'; -import type { MessageDeletedProps } from '../../components/Message/MessageItemView/MessageDeleted'; -import type { MessageFooterProps } from '../../components/Message/MessageItemView/MessageFooter'; -import { MessageHeaderProps } from '../../components/Message/MessageItemView/MessageHeader'; -import type { MessageItemViewProps } from '../../components/Message/MessageItemView/MessageItemView'; -import type { MessageRepliesProps } from '../../components/Message/MessageItemView/MessageReplies'; -import type { MessageRepliesAvatarsProps } from '../../components/Message/MessageItemView/MessageRepliesAvatars'; -import type { MessageStatusProps } from '../../components/Message/MessageItemView/MessageStatus'; -import type { MessageTextProps } from '../../components/Message/MessageItemView/MessageTextContainer'; -import { MessageTimestampProps } from '../../components/Message/MessageItemView/MessageTimestamp'; -import { ReactionListBottomProps } from '../../components/Message/MessageItemView/ReactionList/ReactionListBottom'; -import { ReactionListClusteredProps } from '../../components/Message/MessageItemView/ReactionList/ReactionListClustered'; -import { - ReactionListItemProps, - ReactionListCountItemProps, -} from '../../components/Message/MessageItemView/ReactionList/ReactionListItem'; -import { ReactionListItemWrapperProps } from '../../components/Message/MessageItemView/ReactionList/ReactionListItemWrapper'; -import type { ReactionListTopProps } from '../../components/Message/MessageItemView/ReactionList/ReactionListTop'; +import type { MessagePressableHandlerPayload } from '../../components/Message/Message'; import type { MarkdownRules } from '../../components/Message/MessageItemView/utils/renderText'; import type { MessageActionsParams } from '../../components/Message/utils/messageActions'; -import type { DateHeaderProps } from '../../components/MessageList/DateHeader'; -import type { InlineDateSeparatorProps } from '../../components/MessageList/InlineDateSeparator'; -import type { MessageListProps } from '../../components/MessageList/MessageList'; -import type { MessageSystemProps } from '../../components/MessageList/MessageSystem'; -import type { ScrollToBottomButtonProps } from '../../components/MessageList/ScrollToBottomButton'; -import { TypingIndicatorContainerProps } from '../../components/MessageList/TypingIndicatorContainer'; -import { UnreadMessagesNotificationProps } from '../../components/MessageList/UnreadMessagesNotification'; import type { GroupStyle, MessageGroupStylesParams, } from '../../components/MessageList/utils/getGroupStyles'; -import { MessageActionListProps } from '../../components/MessageMenu/MessageActionList'; -import type { - MessageActionListItemProps, - MessageActionType, -} from '../../components/MessageMenu/MessageActionListItem'; -import { MessageMenuProps } from '../../components/MessageMenu/MessageMenu'; -import type { MessageReactionPickerProps } from '../../components/MessageMenu/MessageReactionPicker'; -import { MessageUserReactionsProps } from '../../components/MessageMenu/MessageUserReactions'; -import { MessageUserReactionsAvatarProps } from '../../components/MessageMenu/MessageUserReactionsAvatar'; -import { MessageUserReactionsItemProps } from '../../components/MessageMenu/MessageUserReactionsItem'; -import type { ReplyProps } from '../../components/Reply/Reply'; +import type { MessageActionType } from '../../components/MessageMenu/MessageActionListItem'; import { NativeHandlers } from '../../native'; import type { ReactionData } from '../../utils/utils'; @@ -111,18 +46,6 @@ export type MessageLocationProps = { }; export type MessagesContextValue = Pick & { - /** - * UI component for Attachment. - * Defaults to: [Attachment](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Attachment/Attachment.tsx) - */ - Attachment: React.ComponentType; - /** Custom UI component for AudioAttachment. */ - AudioAttachment: React.ComponentType; - /** - * UI component for DateHeader - * Defaults to: [DateHeader](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageList/DateHeader.tsx) - **/ - DateHeader: React.ComponentType; // FIXME: Remove the signature with optionsOrHardDelete boolean with the next major release deleteMessage: ( message: LocalMessage, @@ -141,249 +64,24 @@ export type MessagesContextValue = Pick; - - /** - * UI component for FilePreview - * Defaults to: [FilePreview](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Attachment/FilePreview.tsx) - */ - FilePreview: React.ComponentType; - - /** - * UI component to display File type attachment. - * Defaults to: [FilePickerIcon](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Attachment/FileAttachment.tsx) - */ - FileAttachment: React.ComponentType; - /** - * UI component to display group of File type attachments or multiple file attachments (in single message). - * Defaults to: [FileAttachmentGroup](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Attachment/FileAttachmentGroup.tsx) - */ - FileAttachmentGroup: React.ComponentType; - /** - * UI component for attachment icon for type 'file' attachment. - * Defaults to: https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Attachment/FileIcon.tsx - */ - FileAttachmentIcon: React.ComponentType; FlatList: typeof NativeHandlers.FlatList | undefined; - /** - * UI component to display image attachments - * Defaults to: [Gallery](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Attachment/Gallery.tsx) - */ - Gallery: React.ComponentType; - /** - * UI component for Giphy - * Defaults to: [Giphy](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Attachment/Giphy.tsx) - */ - Giphy: React.ComponentType; /** * The giphy version to render - check the keys of the [Image Object](https://developers.giphy.com/docs/api/schema#image-object) for possible values. Uses 'fixed_height' by default * */ giphyVersion: keyof NonNullable; - /** - * The indicator rendered when loading an image fails. - */ - ImageLoadingFailedIndicator: React.ComponentType; - - /** - * The indicator rendered when image is loading. By default renders - */ - ImageLoadingIndicator: React.ComponentType; - /** * When true, messageList will be scrolled at first unread message, when opened. */ initialScrollToFirstUnreadMessage: boolean; - /** - * UI component for Message Date Separator Component - * Defaults to: [InlineDateSeparator](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageList/InlineDateSeparator.tsx) - */ - InlineDateSeparator: React.ComponentType; - /** - * UI component for InlineUnreadIndicator - * Defaults to: [InlineUnreadIndicator](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageList/InlineUnreadIndicator.tsx) - **/ - InlineUnreadIndicator: React.ComponentType; - - Message: React.ComponentType; - /** - * Custom UI component for rendering Message actions in message menu. - * - * **Default** [MessageActionList](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageMenu/MessageActionList.tsx) - */ - MessageActionList: React.ComponentType; - /** - * Custom UI component for rendering Message action item in message menu. - * - * **Default** [MessageActionList](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageMenu/MessageActionList.tsx) - */ - MessageActionListItem: React.ComponentType; - /** - * UI component for MessageAuthor - * Defaults to: [MessageAuthor](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Message/MessageItemView/MessageAuthor.tsx) - **/ - MessageAuthor: React.ComponentType; - /** - * UI component for MessageBlocked - * Defaults to: [MessageBlocked](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Message/MessageItemView/MessageBlocked.tsx) - */ - MessageBlocked: React.ComponentType; - /** - * UI Component for MessageBounce - */ - MessageBounce: React.ComponentType; - /** - * UI component for MessageContent - * Defaults to: [MessageContent](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Message/MessageItemView/MessageContent.tsx) - */ - MessageContent: React.ComponentType; - /** - * Optional UI component rendered above the message content body. - */ - MessageContentTopView?: React.ComponentType; - /** - * Optional UI component rendered to the left of the message content body. - */ - MessageContentLeadingView?: React.ComponentType; - /** - * Optional UI component rendered to the right of the message content body. - */ - MessageContentTrailingView?: React.ComponentType; - /** - * Optional UI component rendered below the message content body. - */ - MessageContentBottomView?: React.ComponentType; /** Order to render the message content */ messageContentOrder: MessageContentType[]; - /** - * UI component for MessageDeleted - * Defaults to: [MessageDeleted](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Message/MessageItemView/MessageDeleted.tsx) - */ - MessageDeleted: React.ComponentType; - /** - * UI component for the MessageError. - */ - MessageError: React.ComponentType; - /** - * Custom message footer component - */ - MessageFooter: React.ComponentType; - MessageList: React.ComponentType; - MessageLocation?: React.ComponentType; - /** - * UI component for MessageMenu - */ - MessageMenu: React.ComponentType; - /** - * Custom message pinned component - */ - MessagePinnedHeader: React.ComponentType; - /** - * Custom message reminder component - */ - MessageReminderHeader: React.ComponentType; - /** - * Custom message saved for later component - */ - MessageSavedForLaterHeader: React.ComponentType; - /** - * Custom message sent to channel component - */ - SentToChannelHeader: React.ComponentType; - /** - * UI component for MessageReactionPicker - */ - MessageReactionPicker: React.ComponentType; - /** - * UI component for MessageReplies - * Defaults to: [MessageReplies](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Message/MessageItemView/MessageReplies.tsx) - */ - MessageReplies: React.ComponentType; - /** - * UI Component for MessageRepliesAvatars - * Defaults to: [MessageRepliesAvatars](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Message/MessageItemView/MessageRepliesAvatars.tsx) - */ - MessageRepliesAvatars: React.ComponentType; - /** - * Optional UI component for overriding the empty space on a message row. If the message is left aligned, it will be to the right of it - otherwise left. - */ - MessageSpacer?: React.ComponentType; - /** - * UI component for MessageItemView. It encapsulates the entirety of a message row. - * Defaults to: [MessageItemView](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Message/MessageItemView/MessageItemView.tsx) - */ - MessageItemView: React.ComponentType< - MessageItemViewProps & { ref?: React.RefObject } - >; - /** - * UI component for MessageStatus (delivered/read) - * Defaults to: [MessageStatus](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Message/MessageItemView/MessageStatus.tsx) - */ - MessageStatus: React.ComponentType; - /** - * UI component for MessageSystem - * Defaults to: [MessageSystem](https://getstream.io/chat/docs/sdk/reactnative/ui-components/message-system/) - */ - MessageSystem: React.ComponentType; - /** - * UI component for MessageTimestamp - * Defaults to: [MessageTimestamp](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/components/Message/MessageItemView/MessageTimestamp.tsx) - */ - MessageTimestamp: React.ComponentType; - /** - * Custom UI component for rendering user reactions, in message menu. - * - * **Default** [MessageUserReactions](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageMenu/MessageUserReactions.tsx) - */ - MessageUserReactions: React.ComponentType; - /** - * Custom UI component for rendering user reactions avatar under `MessageUserReactionsItem`, in message menu. - * - * **Default** [MessageUserReactionsAvatar](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageMenu/MessageUserReactionsAvatar.tsx) - */ - MessageUserReactionsAvatar: React.ComponentType; - /** - * Custom UI component for rendering individual user reactions item under `MessageUserReactions`, in message menu. - * - * **Default** [MessageUserReactionsItem](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageMenu/MessageUserReactionsItem.tsx) - */ - MessageUserReactionsItem: React.ComponentType; - removeMessage: (message: { id: string; parent_id?: string }) => Promise; - /** - * UI component for Reply - * Defaults to: [Reply](https://getstream.io/chat/docs/sdk/reactnative/ui-components/reply/) - */ - Reply: React.ComponentType; /** * Override the api request for retry message functionality. */ retrySendMessage: (message: LocalMessage) => Promise; - /** - * UI component for ScrollToBottomButton - * Defaults to: [ScrollToBottomButton](https://getstream.io/chat/docs/sdk/reactnative/ui-components/scroll-to-bottom-button/) - */ - ScrollToBottomButton: React.ComponentType; sendReaction: (type: string, messageId: string) => Promise; - /** - * UI component for StreamingMessageView. Displays the text of a message with a typewriter animation. - */ - StreamingMessageView: React.ComponentType; - /** - * UI component for TypingIndicator - * Defaults to: [TypingIndicator](https://getstream.io/chat/docs/sdk/reactnative/ui-components/typing-indicator/) - */ - TypingIndicator: React.ComponentType; - /** - * UI component for TypingIndicatorContainer - * Defaults to: [TypingIndicatorContainer](https://getstream.io/chat/docs/sdk/reactnative/contexts/messages-context/#typingindicatorcontainer) - */ - TypingIndicatorContainer: React.ComponentType; - UnreadMessagesNotification: React.ComponentType; updateMessage: ( updatedMessage: MessageResponse | LocalMessage, extraState?: { @@ -393,17 +91,6 @@ export type MessagesContextValue = Pick void; - /** - * Custom UI component to display enriched url preview. - * Defaults to https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Attachment/UrlPreview/URLPreview.tsx - */ - UrlPreview: React.ComponentType; - /** - * Custom UI component to display compact url preview. - * Defaults to https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Attachment/UrlPreview/URLPreviewCompact.tsx - */ - URLPreviewCompact: React.ComponentType; - VideoThumbnail: React.ComponentType; /** * Provide any additional props for `Pressable` which wraps inner MessageContent component here. * Please check docs for Pressable for supported props - https://reactnative.dev/docs/pressable#props @@ -537,17 +224,10 @@ export type MessagesContextValue = Pick MessageActionType[]; - /** - * Custom message header component - */ - MessageHeader: React.ComponentType; - MessageSwipeContent?: React.ComponentType; /** * HitSlop for the message swipe to reply gesture */ messageSwipeToReplyHitSlop?: ViewProps['hitSlop']; - /** Custom UI component for message text */ - MessageText?: React.ComponentType; /** * The number of lines of the message text to be displayed */ @@ -637,17 +317,7 @@ export type MessagesContextValue = Pick void; - /** - * Override the entire content of the Poll component. The component has full access to the - * usePollState() and usePollContext() hooks. - * */ - PollContent?: React.ComponentType; quotedMessage?: LocalMessage | null; - /** - * UI component for ReactionListTop - * Defaults to: [ReactionList](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Reaction/ReactionList.tsx) - */ - ReactionListBottom?: React.ComponentType; /** * The position of the reaction list in the message */ @@ -658,31 +328,6 @@ export type MessagesContextValue = Pick; - - /** - * UI component for ReactionListBottom - * Defaults to: [ReactionList](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Reaction/ReactionList.tsx) - */ - ReactionListClustered: React.ComponentType; - /** - * UI component for ReactionListSegmented - * Defaults to: [ReactionList](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Reaction/ReactionList.tsx) - */ - ReactionListItem: React.ComponentType; - - /** - * UI component for ReactionListItemWrapper - * Defaults to: [ReactionListItemWrapper](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Reaction/ReactionListItemWrapper.tsx) - */ - ReactionListItemWrapper: React.ComponentType; - - ReactionListCountItem: React.ComponentType; - /** * Full override of the reaction function on Message and Message Overlay * From b6c0a96900fdfc06fe02be3bb2919cb29de70092 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Wed, 8 Apr 2026 22:54:19 +0200 Subject: [PATCH 02/16] fix: populate defaultComponents, fix circular dep, update tests - Recreate defaultComponents.ts with all ~100 default component mappings - Fix circular dependency in ComponentsContext by lazy-loading defaults - Remove component override props from Attachment.tsx (use useComponentsContext) - Update tests to use WithComponents wrapper instead of component override props - Fix pre-existing ChannelList filter test (missing countUnread mock) --- .../offline-support/offline-feature.js | 102 +++--- .../src/components/Attachment/Attachment.tsx | 76 +---- .../ChannelList/__tests__/ChannelList.test.js | 295 ++++++++++-------- .../__tests__/ChannelListView.test.js | 107 ++++--- .../__tests__/ChannelPreview.test.tsx | 37 ++- package/src/components/Message/Message.tsx | 10 +- .../__tests__/MessageTextContainer.test.tsx | 11 +- .../__tests__/MessageUserReactions.test.tsx | 22 +- .../componentsContext/ComponentsContext.tsx | 26 +- .../__tests__/defaultComponents.test.ts | 17 + .../componentsContext/defaultComponents.ts | 243 +++++++++++++++ 11 files changed, 603 insertions(+), 343 deletions(-) create mode 100644 package/src/contexts/componentsContext/__tests__/defaultComponents.test.ts diff --git a/package/src/__tests__/offline-support/offline-feature.js b/package/src/__tests__/offline-support/offline-feature.js index 5cbbcae21f..4f94b0d7c0 100644 --- a/package/src/__tests__/offline-support/offline-feature.js +++ b/package/src/__tests__/offline-support/offline-feature.js @@ -10,6 +10,7 @@ import { v4 as uuidv4 } from 'uuid'; import { ChannelList } from '../../components/ChannelList/ChannelList'; import { Chat } from '../../components/Chat/Chat'; import { useChannelsContext } from '../../contexts/channelsContext/ChannelsContext'; +import { WithComponents } from '../../contexts/componentsContext/ComponentsContext'; import { getOrCreateChannelApi } from '../../mock-builders/api/getOrCreateChannel'; import { queryChannelsApi } from '../../mock-builders/api/queryChannels'; import { useMockedApis } from '../../mock-builders/api/useMockedApis'; @@ -48,33 +49,17 @@ import { BetterSqlite } from '../../test-utils/BetterSqlite'; * to those components might end up breaking tests for ChannelList, which will be quite painful * to debug. */ -const ChannelPreviewComponent = ({ channel, setActiveChannel }) => ( - +/** + * Custom Preview component used via WithComponents. + * Receives { channel, muted, unread, lastMessage } from ChannelPreview. + */ +const ChannelPreviewComponent = ({ channel }) => ( + {channel.data.name} {channel.state.messages[0]?.text} ); -const ChannelListComponent = (props) => { - const { channels, onSelect } = useChannelsContext(); - if (!channels) { - return null; - } - - return ( - - {channels?.map((channel) => ( - - ))} - - ); -}; - test('Workaround to allow exporting tests', () => expect(true).toBe(true)); export const Generic = () => { @@ -223,12 +208,9 @@ export const Generic = () => { const renderComponent = () => render( - + + + , ); @@ -325,7 +307,7 @@ export const Generic = () => { await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); await waitFor(async () => { - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); await expectCIDsOnUIToBeInDB(screen.queryAllByLabelText); }); }); @@ -340,7 +322,7 @@ export const Generic = () => { await waitFor( async () => { - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); await expectAllChannelsWithStateToBeInDB(screen.queryAllByLabelText); }, { timeout: 5000 }, @@ -359,7 +341,7 @@ export const Generic = () => { await act( async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true), ); - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); expect(screen.getByTestId(emptyChannel.cid)).toBeTruthy(); expect(chatClient.hydrateActiveChannels).toHaveBeenCalled(); expect(chatClient.hydrateActiveChannels.mock.calls[0][0]).toStrictEqual([emptyChannel]); @@ -372,7 +354,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const targetChannel = channels[0].channel; const newMessage = generateMessage({ cid: targetChannel.cid, @@ -401,7 +383,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const targetChannel = channels[0].channel; // check if the reads state is correct first @@ -463,7 +445,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const targetChannel = channels[0].channel; // check if the reads state is correct first @@ -526,7 +508,7 @@ export const Generic = () => { act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); await waitFor(() => { - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); }); const newChannel = createChannel(); @@ -549,7 +531,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const updatedMessage = { ...channels[0].messages[0] }; updatedMessage.text = uuidv4(); @@ -571,7 +553,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const removedChannel = channels[getRandomInt(0, channels.length - 1)].channel; act(() => dispatchNotificationRemovedFromChannel(chatClient, removedChannel)); await waitFor(async () => { @@ -598,7 +580,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const removedChannel = channels[getRandomInt(0, channels.length - 1)].channel; act(() => dispatchChannelDeletedEvent(chatClient, removedChannel)); await waitFor(async () => { @@ -625,7 +607,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const hiddenChannel = channels[getRandomInt(0, channels.length - 1)].channel; act(() => dispatchChannelHiddenEvent(chatClient, hiddenChannel)); await waitFor(async () => { @@ -655,7 +637,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const hiddenChannel = channels[getRandomInt(0, channels.length - 1)].channel; // first, we mark it as hidden act(() => dispatchChannelHiddenEvent(chatClient, hiddenChannel)); @@ -708,7 +690,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const newChannel = createChannel(); useMockedApis(chatClient, [getOrCreateChannelApi(newChannel)]); @@ -739,7 +721,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const channelToTruncate = channels[getRandomInt(0, channels.length - 1)].channel; act(() => dispatchChannelTruncatedEvent(chatClient, channelToTruncate)); @@ -771,7 +753,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const channelResponse = channels[getRandomInt(0, channels.length - 1)]; const channelToTruncate = channelResponse.channel; @@ -815,7 +797,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const channelResponse = channels[getRandomInt(0, channels.length - 1)]; const channelToTruncate = channelResponse.channel; @@ -847,7 +829,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const channelResponse = channels[getRandomInt(0, channels.length - 1)]; const channelToTruncate = channelResponse.channel; @@ -881,7 +863,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const targetChannel = channels[getRandomInt(0, channels.length - 1)]; const targetMessage = @@ -926,7 +908,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const targetChannel = channels[getRandomInt(0, channels.length - 1)]; const targetMessage = @@ -1010,7 +992,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const targetChannel = channels[getRandomInt(0, channels.length - 1)]; const targetMessage = @@ -1076,7 +1058,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const targetChannel = channels[getRandomInt(0, channels.length - 1)]; const targetMessage = @@ -1130,7 +1112,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const targetChannel = channels[getRandomInt(0, channels.length - 1)]; const targetMessage = @@ -1167,7 +1149,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const targetChannel = channels[getRandomInt(0, channels.length - 1)]; const targetMessage = @@ -1264,7 +1246,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const targetChannel = channels[getRandomInt(0, channels.length - 1)]; const targetMessage = @@ -1323,7 +1305,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const targetChannel = channels[getRandomInt(0, channels.length - 1)]; const targetMessage = @@ -1380,7 +1362,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const targetChannel = channels[getRandomInt(0, channels.length - 1)]; const targetMessage = @@ -1438,7 +1420,7 @@ export const Generic = () => { act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const targetChannel = channels[getRandomInt(0, channels.length - 1)]; const oldMemberCount = targetChannel.channel.member_count; @@ -1466,7 +1448,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const targetChannel = channels[getRandomInt(0, channels.length - 1)]; const targetMember = targetChannel.members[getRandomInt(0, targetChannel.members.length - 1)]; @@ -1494,7 +1476,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const targetChannel = channels[getRandomInt(0, channels.length - 1)]; const targetMember = targetChannel.members[getRandomInt(0, targetChannel.members.length - 1)]; @@ -1521,7 +1503,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const targetChannel = channels[getRandomInt(0, channels.length - 1)]; targetChannel.channel.name = uuidv4(); @@ -1547,7 +1529,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const targetChannel = channels[getRandomInt(0, channels.length - 1)]; const targetMember = targetChannel.members[getRandomInt(0, targetChannel.members.length - 1)]; @@ -1582,7 +1564,7 @@ export const Generic = () => { renderComponent(); act(() => dispatchConnectionChangedEvent(chatClient)); await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true)); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const targetChannel = channels[getRandomInt(0, channels.length - 1)]; const targetMember = targetChannel.members[getRandomInt(0, targetChannel.members.length - 1)]; diff --git a/package/src/components/Attachment/Attachment.tsx b/package/src/components/Attachment/Attachment.tsx index b4039db3cd..02dd621db0 100644 --- a/package/src/components/Attachment/Attachment.tsx +++ b/package/src/components/Attachment/Attachment.tsx @@ -11,17 +11,8 @@ import { type Attachment as AttachmentType, } from 'stream-chat'; -import { AudioAttachment as AudioAttachmentDefault } from './Audio'; - -import { UnsupportedAttachment as UnsupportedAttachmentDefault } from './UnsupportedAttachment'; -import { URLPreview as URLPreviewDefault } from './UrlPreview'; -import { URLPreviewCompact as URLPreviewCompactDefault } from './UrlPreview/URLPreviewCompact'; - -import { FileAttachment as FileAttachmentDefault } from '../../components/Attachment/FileAttachment'; -import { Gallery as GalleryDefault } from '../../components/Attachment/Gallery'; -import { Giphy as GiphyDefault } from '../../components/Attachment/Giphy'; - import { useTheme } from '../../contexts'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { MessageContextValue, useMessageContext, @@ -39,16 +30,7 @@ export type ActionHandler = (name: string, value: string) => void; export type AttachmentPropsWithContext = Pick< MessagesContextValue, - | 'AudioAttachment' - | 'FileAttachment' - | 'Gallery' - | 'Giphy' - | 'isAttachmentEqual' - | 'UrlPreview' - | 'URLPreviewCompact' - | 'myMessageTheme' - | 'urlPreviewType' - | 'UnsupportedAttachment' + 'isAttachmentEqual' | 'myMessageTheme' | 'urlPreviewType' > & Pick & { /** @@ -62,19 +44,16 @@ export type AttachmentPropsWithContext = Pick< }; const AttachmentWithContext = (props: AttachmentPropsWithContext) => { + const { attachment, index, message, urlPreviewType } = props; const { - attachment, AudioAttachment, FileAttachment, Gallery, Giphy, UrlPreview, URLPreviewCompact, - index, - message, - urlPreviewType, UnsupportedAttachment, - } = props; + } = useComponentsContext(); const audioAttachmentStyles = useAudioAttachmentStyles(); if (attachment.type === FileTypes.Giphy || attachment.type === FileTypes.Imgur) { @@ -164,31 +143,9 @@ export type AttachmentProps = Partial; * Attachment - The message attachment */ export const Attachment = (props: AttachmentProps) => { - const { - attachment, - AudioAttachment: PropAudioAttachment, - FileAttachment: PropFileAttachment, - Gallery: PropGallery, - Giphy: PropGiphy, - myMessageTheme: PropMyMessageTheme, - UrlPreview: PropUrlPreview, - URLPreviewCompact: PropURLPreviewCompact, - urlPreviewType: PropUrlPreviewType, - UnsupportedAttachment: PropUnsupportedAttachment, - } = props; + const { attachment } = props; - const { - AudioAttachment: ContextAudioAttachment, - FileAttachment: ContextFileAttachment, - Gallery: ContextGallery, - Giphy: ContextGiphy, - isAttachmentEqual, - myMessageTheme: ContextMyMessageTheme, - UrlPreview: ContextUrlPreview, - URLPreviewCompact: ContextURLPreviewCompact, - urlPreviewType: ContextUrlPreviewType, - UnsupportedAttachment: ContextUnsupportedAttachment, - } = useMessagesContext(); + const { isAttachmentEqual, myMessageTheme, urlPreviewType } = useMessagesContext(); const { message } = useMessageContext(); @@ -196,33 +153,14 @@ export const Attachment = (props: AttachmentProps) => { return null; } - const AudioAttachment = PropAudioAttachment || ContextAudioAttachment || AudioAttachmentDefault; - const FileAttachment = PropFileAttachment || ContextFileAttachment || FileAttachmentDefault; - const Gallery = PropGallery || ContextGallery || GalleryDefault; - const Giphy = PropGiphy || ContextGiphy || GiphyDefault; - const UrlPreview = PropUrlPreview || ContextUrlPreview || URLPreviewDefault; - const myMessageTheme = PropMyMessageTheme || ContextMyMessageTheme; - const URLPreviewCompact = - PropURLPreviewCompact || ContextURLPreviewCompact || URLPreviewCompactDefault; - const urlPreviewType = PropUrlPreviewType || ContextUrlPreviewType; - const UnsupportedAttachment = - PropUnsupportedAttachment || ContextUnsupportedAttachment || UnsupportedAttachmentDefault; - return ( ); diff --git a/package/src/components/ChannelList/__tests__/ChannelList.test.js b/package/src/components/ChannelList/__tests__/ChannelList.test.js index d166544fa7..f84734e212 100644 --- a/package/src/components/ChannelList/__tests__/ChannelList.test.js +++ b/package/src/components/ChannelList/__tests__/ChannelList.test.js @@ -12,6 +12,7 @@ import { } from '@testing-library/react-native'; import { useChannelsContext } from '../../../contexts/channelsContext/ChannelsContext'; +import { WithComponents } from '../../../contexts/componentsContext/ComponentsContext'; import { getOrCreateChannelApi } from '../../../mock-builders/api/getOrCreateChannel'; import { queryChannelsApi } from '../../../mock-builders/api/queryChannels'; @@ -30,7 +31,6 @@ import { generateChannel, generateChannelResponse } from '../../../mock-builders import { generateMessage } from '../../../mock-builders/generator/message'; import { generateUser } from '../../../mock-builders/generator/user'; import { getTestClientWithUser } from '../../../mock-builders/mock'; -import { ChannelPreview } from '../../ChannelPreview/ChannelPreview'; import { Chat } from '../../Chat/Chat'; import { ChannelList } from '../ChannelList'; @@ -43,67 +43,35 @@ jest.mock('../../ChannelPreview/ChannelSwipableWrapper', () => ({ })); /** - * We are gonna use following custom UI components for preview and list. - * If we use ChannelPreviewView or ChannelPreviewLastMessage here, then changes - * to those components might end up breaking tests for ChannelList, which will be quite painful - * to debug. + * Custom Preview component used via WithComponents to verify channel rendering. + * Receives { channel, muted, unread, lastMessage } from ChannelPreview. */ -const ChannelPreviewComponent = ({ channel, setActiveChannel }) => ( - +const ChannelPreviewComponent = ({ channel }) => ( + {channel.data?.name} {channel.state.messages[0]?.text} ); -const ChannelListComponent = (props) => { - const { channels, onSelect } = useChannelsContext(); - return ( - - {channels?.map((channel) => ( - - ))} - - ); -}; - -const ChannelListSwipeActionsProbe = () => { +/** + * Probe that reads swipeActionsEnabled from ChannelsContext. + * Used as a Preview override to inspect context values. + */ +const SwipeActionsProbe = () => { const { swipeActionsEnabled } = useChannelsContext(); return {`${swipeActionsEnabled}`}; }; -const ChannelListRefreshingProbe = () => { +/** + * Probe that reads refreshing from ChannelsContext. + */ +const RefreshingProbe = () => { const { refreshing } = useChannelsContext(); return {`${refreshing}`}; }; const ChannelPreviewContent = ({ unread }) => {`${unread}`}; -const ChannelListWithChannelPreview = () => { - const { channels } = useChannelsContext(); - return ( - - {channels?.map((channel) => ( - - ))} - - ); -}; - -let expectedChannelDetailsBottomSheetOverride; -const ChannelListChannelDetailsBottomSheetProbe = () => { - const { ChannelDetailsBottomSheet } = useChannelsContext(); - return ( - - {`${ChannelDetailsBottomSheet === expectedChannelDetailsBottomSheetOverride}`} - - ); -}; - class DeferredPromise { constructor() { this.promise = new Promise((resolve, reject) => { @@ -120,13 +88,10 @@ describe('ChannelList', () => { let testChannel3; const props = { filters: {}, - List: ChannelListComponent, - Preview: ChannelPreviewComponent, }; beforeEach(async () => { jest.clearAllMocks(); - expectedChannelDetailsBottomSheetOverride = undefined; chatClient = await getTestClientWithUser({ id: 'dan' }); testChannel1 = generateChannelResponse(); testChannel2 = generateChannelResponse(); @@ -140,11 +105,13 @@ describe('ChannelList', () => { const { getByTestId } = render( - + + + , ); - await waitFor(() => expect(getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(getByTestId('channel-list-view')).toBeTruthy()); }); it('should render a preview of each channel', async () => { @@ -152,7 +119,9 @@ describe('ChannelList', () => { const { getByTestId } = render( - + + + , ); @@ -164,12 +133,14 @@ describe('ChannelList', () => { render( - + + + , ); await waitFor(() => { - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); expect(screen.getByTestId(testChannel1.channel.id)).toBeTruthy(); }); @@ -177,7 +148,9 @@ describe('ChannelList', () => { screen.rerender( - + + + , ); @@ -191,8 +164,22 @@ describe('ChannelList', () => { const deferredCallForFreshFilter = new DeferredPromise(); const staleFilter = { 'initial-filter': { a: { $gt: 'c' } } }; const freshFilter = { 'new-filter': { a: { $gt: 'c' } } }; - const staleChannel = [generateChannel({ id: 'stale-channel' })]; - const freshChannel = [generateChannel({ id: 'new-channel' })]; + const createMockChannel = (id) => { + const channel = generateChannel({ + data: { name: id }, + id, + state: { latestMessages: [], members: {}, messages: [], setIsUpToDate: jest.fn() }, + }); + channel.countUnread = () => 0; + channel.muteStatus = () => ({ muted: false }); + channel.on = jest.fn(() => ({ unsubscribe: jest.fn() })); + channel.messageComposer = { + registerDraftEventSubscriptions: jest.fn(() => jest.fn()), + }; + return channel; + }; + const staleChannel = [createMockChannel('stale-channel')]; + const freshChannel = [createMockChannel('new-channel')]; const spy = jest.spyOn(chatClient, 'queryChannels'); spy.mockImplementation((filters = {}) => { if (Object.prototype.hasOwnProperty.call(filters, 'new-filter')) { @@ -203,7 +190,9 @@ describe('ChannelList', () => { const { rerender, queryByTestId } = render( - + + + , ); @@ -216,12 +205,14 @@ describe('ChannelList', () => { ); await waitFor(() => { - expect(queryByTestId('channel-list')).toBeTruthy(); + expect(queryByTestId('channel-list-view')).toBeTruthy(); }); rerender( - + + + , ); @@ -238,7 +229,7 @@ describe('ChannelList', () => { deferredCallForFreshFilter.resolve(freshChannel); }); await waitFor(() => { - expect(queryByTestId('channel-list')).toBeTruthy(); + expect(queryByTestId('channel-list-view')).toBeTruthy(); expect(queryByTestId('new-channel')).toBeTruthy(); }); }); @@ -249,7 +240,9 @@ describe('ChannelList', () => { render( - + + + , ); @@ -269,7 +262,9 @@ describe('ChannelList', () => { const { getByTestId } = render( - + + + , ); @@ -282,7 +277,9 @@ describe('ChannelList', () => { const { getByTestId } = render( - + + + , ); @@ -295,11 +292,13 @@ describe('ChannelList', () => { const { getByTestId, queryByTestId } = render( - + + + , ); - await waitFor(() => expect(getByTestId('channel-list-with-channel-preview')).toBeTruthy()); + await waitFor(() => expect(getByTestId('channel-list-view')).toBeTruthy()); expect(getByTestId('preview-unread')).toHaveTextContent('0'); expect(queryByTestId('swipe-wrapper')).toBeNull(); expect(mockChannelSwipableWrapper).not.toHaveBeenCalled(); @@ -310,32 +309,37 @@ describe('ChannelList', () => { const { getByTestId } = render( - + + + , ); - await waitFor(() => expect(getByTestId('channel-list-with-channel-preview')).toBeTruthy()); + await waitFor(() => expect(getByTestId('channel-list-view')).toBeTruthy()); expect(getByTestId('swipe-wrapper')).toBeTruthy(); expect(mockChannelSwipableWrapper).toHaveBeenCalledTimes(1); }); - it('should expose ChannelDetailsBottomSheet override in ChannelsContext', async () => { + it('should expose ChannelDetailsBottomSheet override via WithComponents', async () => { useMockedApis(chatClient, [queryChannelsApi([testChannel1])]); const ChannelDetailsBottomSheetOverride = () => null; - expectedChannelDetailsBottomSheetOverride = ChannelDetailsBottomSheetOverride; - const { getByTestId } = render( + render( - + + + , ); - await waitFor(() => expect(getByTestId('channel-details-bottom-sheet-override')).toBeTruthy()); - expect(getByTestId('channel-details-bottom-sheet-override')).toHaveTextContent('true'); + await waitFor(() => expect(mockChannelSwipableWrapper).toHaveBeenCalled()); + const swipableWrapperProps = mockChannelSwipableWrapper.mock.calls[0]?.[0]; + expect(swipableWrapperProps.ChannelDetailsBottomSheet).toBe(ChannelDetailsBottomSheetOverride); }); it('should pass ChannelDetailsBottomSheet override to ChannelSwipableWrapper', async () => { @@ -344,21 +348,20 @@ describe('ChannelList', () => { render( - + + + , ); await waitFor(() => expect(mockChannelSwipableWrapper).toHaveBeenCalled()); const swipableWrapperProps = mockChannelSwipableWrapper.mock.calls[0]?.[0]; - expect(swipableWrapperProps).toEqual( - expect.objectContaining({ - ChannelDetailsBottomSheet: ChannelDetailsBottomSheetOverride, - }), - ); + expect(swipableWrapperProps.ChannelDetailsBottomSheet).toBe(ChannelDetailsBottomSheetOverride); }); describe('Event handling', () => { @@ -378,11 +381,13 @@ describe('ChannelList', () => { it('should move channel to top of the list by default', async () => { render( - + + + , ); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); const newMessage = sendNewMessageOnChannel3(); @@ -400,11 +405,13 @@ describe('ChannelList', () => { it('should add channel to top if channel is hidden from the list', async () => { render( - + + + , ); - await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy()); + await waitFor(() => expect(screen.getByTestId('channel-list-view')).toBeTruthy()); act(() => dispatchChannelHiddenEvent(chatClient, testChannel3.channel)); const newItems = screen.getAllByLabelText('list-item'); @@ -428,7 +435,9 @@ describe('ChannelList', () => { it('should not alter order if `lockChannelOrder` prop is true', async () => { render( - + + + , ); @@ -452,12 +461,14 @@ describe('ChannelList', () => { const onNewMessage = jest.fn(); render( - + + + , ); await waitFor(() => { - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); }); act(() => dispatchMessageNewEvent(chatClient, testChannel2.channel)); @@ -479,11 +490,13 @@ describe('ChannelList', () => { it('should move a channel to top of the list by default', async () => { render( - + + + , ); await waitFor(() => { - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); }); act(() => dispatchNotificationMessageNewEvent(chatClient, testChannel3.channel)); @@ -501,12 +514,14 @@ describe('ChannelList', () => { const onNewMessage = jest.fn(); render( - + + + , ); await waitFor(() => { - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); }); act(() => dispatchMessageNewEvent(chatClient, testChannel2.channel)); @@ -520,12 +535,14 @@ describe('ChannelList', () => { const onNewMessageNotification = jest.fn(); render( - + + + , ); await waitFor(() => { - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); }); act(() => dispatchNotificationMessageNewEvent(chatClient, testChannel2.channel)); @@ -547,12 +564,14 @@ describe('ChannelList', () => { it('should move a channel to top of the list by default', async () => { render( - + + + , ); await waitFor(() => { - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); }); act(() => dispatchNotificationAddedToChannelEvent(chatClient, testChannel3.channel)); @@ -572,12 +591,14 @@ describe('ChannelList', () => { const onAddedToChannel = jest.fn(); render( - + + + , ); await waitFor(() => { - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); }); act(() => dispatchNotificationAddedToChannelEvent(chatClient, testChannel3.channel)); @@ -596,12 +617,14 @@ describe('ChannelList', () => { it('should remove the channel from list by default', async () => { render( - + + + , ); await waitFor(() => { - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); }); const items = screen.getAllByLabelText('list-item'); @@ -621,12 +644,14 @@ describe('ChannelList', () => { const onRemovedFromChannel = jest.fn(); render( - + + + , ); await waitFor(() => { - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); }); act(() => dispatchNotificationRemovedFromChannel(chatClient, testChannel3.channel)); @@ -645,12 +670,14 @@ describe('ChannelList', () => { it('should update a channel in the list by default', async () => { render( - + + + , ); await waitFor(() => { - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); }); act(() => @@ -669,12 +696,14 @@ describe('ChannelList', () => { const onChannelUpdated = jest.fn(); render( - + + + , ); await waitFor(() => { - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); }); act(() => @@ -698,12 +727,14 @@ describe('ChannelList', () => { it('should remove a channel from the list by default', async () => { render( - + + + , ); await waitFor(() => { - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); }); const items = screen.getAllByLabelText('list-item'); @@ -723,12 +754,14 @@ describe('ChannelList', () => { const onChannelDeleted = jest.fn(); render( - + + + , ); await waitFor(() => { - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); }); act(() => dispatchChannelDeletedEvent(chatClient, testChannel2.channel)); @@ -747,12 +780,14 @@ describe('ChannelList', () => { it('should hide a channel from the list by default', async () => { render( - + + + , ); await waitFor(() => { - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); }); const items = screen.getAllByLabelText('list-item'); @@ -772,12 +807,14 @@ describe('ChannelList', () => { const onChannelHidden = jest.fn(); render( - + + + , ); await waitFor(() => { - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); }); act(() => dispatchChannelHiddenEvent(chatClient, testChannel2.channel)); @@ -795,12 +832,14 @@ describe('ChannelList', () => { render( - + + + , ); await waitFor(() => { - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); }); act(() => dispatchConnectionRecoveredEvent(chatClient)); @@ -821,7 +860,9 @@ describe('ChannelList', () => { render( - + + + , ); @@ -854,12 +895,14 @@ describe('ChannelList', () => { const onChannelTruncated = jest.fn(); render( - + + + , ); await waitFor(() => { - expect(screen.getByTestId('channel-list')).toBeTruthy(); + expect(screen.getByTestId('channel-list-view')).toBeTruthy(); }); act(() => dispatchChannelTruncatedEvent(chatClient, testChannel1.channel)); diff --git a/package/src/components/ChannelList/__tests__/ChannelListView.test.js b/package/src/components/ChannelList/__tests__/ChannelListView.test.js index 17491b1629..69470fcdad 100644 --- a/package/src/components/ChannelList/__tests__/ChannelListView.test.js +++ b/package/src/components/ChannelList/__tests__/ChannelListView.test.js @@ -1,11 +1,8 @@ -import React, { useCallback } from 'react'; +import React from 'react'; import { cleanup, render, waitFor } from '@testing-library/react-native'; -import { - ChannelsProvider, - useChannelsContext, -} from '../../../contexts/channelsContext/ChannelsContext'; +import { ChannelsProvider } from '../../../contexts/channelsContext/ChannelsContext'; import { ChatContext, ChatProvider } from '../../../contexts/chatContext/ChatContext'; import { getOrCreateChannelApi } from '../../../mock-builders/api/getOrCreateChannel'; import { queryChannelsApi } from '../../../mock-builders/api/queryChannels'; @@ -16,43 +13,67 @@ import { Chat } from '../../Chat/Chat'; import { ChannelList } from '../ChannelList'; import { ChannelListView } from '../ChannelListView'; -let mockChannels; let chatClient; -const ListMessenger = ({ error, loadingChannels, ...props }) => { - const channelsContext = useChannelsContext(); +/** + * Renders the full ChannelList (which now always uses ChannelListView internally). + */ +const Component = () => ( + + + {(context) => ( + + + + )} + + +); - return ( - - - - ); -}; +const noop = () => {}; -const Component = ({ error = false, loadingChannels = false }) => { - const List = useCallback( - (...props) => , - [error, loadingChannels], - ); - return ( - - - {(context) => ( - - - - )} - - - ); -}; +/** + * Renders ChannelListView directly with a mock ChannelsContext for testing + * error and loading states. + */ +const ComponentWithContextOverrides = ({ error, loadingChannels }) => ( + + + {(context) => ( + + + + + + )} + + +); describe('ChannelListView', () => { beforeAll(async () => { @@ -77,7 +98,7 @@ describe('ChannelListView', () => { it('renders the `EmptyStateIndicator` when no channels are present', async () => { useMockedApis(chatClient, [queryChannelsApi([])]); - const { getByTestId } = render(); + const { getByTestId } = render(); await waitFor(() => { expect(getByTestId('empty-channel-state-title')).toBeTruthy(); }); @@ -85,7 +106,7 @@ describe('ChannelListView', () => { it('renders the `LoadingErrorIndicator` when `error` prop is true', async () => { const { getByTestId } = render( - , + , ); await waitFor(() => { expect(getByTestId('loading-error')).toBeTruthy(); @@ -93,9 +114,11 @@ describe('ChannelListView', () => { }); it('renders the `LoadingIndicator` when when channels have not yet loaded', async () => { - const { getByTestId } = render(); + const { getByText } = render( + , + ); await waitFor(() => { - expect(getByTestId('channel-list-loading-indicator')).toBeTruthy(); + expect(getByText('Loading channels...')).toBeTruthy(); }); }); }); diff --git a/package/src/components/ChannelPreview/__tests__/ChannelPreview.test.tsx b/package/src/components/ChannelPreview/__tests__/ChannelPreview.test.tsx index bae4e582d7..65e0caa4f3 100644 --- a/package/src/components/ChannelPreview/__tests__/ChannelPreview.test.tsx +++ b/package/src/components/ChannelPreview/__tests__/ChannelPreview.test.tsx @@ -7,6 +7,7 @@ import type { Channel, StreamChat } from 'stream-chat'; import { ChannelsProvider } from '../../../contexts/channelsContext/ChannelsContext'; import type { ChannelsContextValue } from '../../../contexts/channelsContext/ChannelsContext'; +import { WithComponents } from '../../../contexts/componentsContext/ComponentsContext'; import { getOrCreateChannelApi, GetOrCreateChannelApiParams, @@ -83,12 +84,9 @@ describe('ChannelPreview', () => { return ( - + + + ); }; @@ -436,18 +434,23 @@ describe('ChannelPreview', () => { return ( - - - + + + + ); }; diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx index b2899dc765..b2a656a927 100644 --- a/package/src/components/Message/Message.tsx +++ b/package/src/components/Message/Message.tsx @@ -876,10 +876,7 @@ const MessageWithContext = (props: MessagePropsWithContext) => { visible={showMessageReactions} height={424} > - + ) : null} @@ -898,10 +895,7 @@ const MessageWithContext = (props: MessagePropsWithContext) => { }); }} > - + ) : null} diff --git a/package/src/components/Message/MessageItemView/__tests__/MessageTextContainer.test.tsx b/package/src/components/Message/MessageItemView/__tests__/MessageTextContainer.test.tsx index 2df6217d0a..5aa1142088 100644 --- a/package/src/components/Message/MessageItemView/__tests__/MessageTextContainer.test.tsx +++ b/package/src/components/Message/MessageItemView/__tests__/MessageTextContainer.test.tsx @@ -44,10 +44,13 @@ describe('MessageTextContainer', () => { rerender( - {message?.text}} - /> + {message?.text}, + }} + > + + , ); diff --git a/package/src/components/MessageMenu/__tests__/MessageUserReactions.test.tsx b/package/src/components/MessageMenu/__tests__/MessageUserReactions.test.tsx index 364b5ef017..fd91791dda 100644 --- a/package/src/components/MessageMenu/__tests__/MessageUserReactions.test.tsx +++ b/package/src/components/MessageMenu/__tests__/MessageUserReactions.test.tsx @@ -43,18 +43,18 @@ const renderComponent = (props = {}) => render( - null, - MessageUserReactionsItem: (props: MessageUserReactionsItemProps) => ( - {props.reaction.id + ' ' + props.reaction.type} - ), - } as unknown as MessagesContextValue - } + null, + MessageUserReactionsItem: (itemProps: MessageUserReactionsItemProps) => ( + {itemProps.reaction.id + ' ' + itemProps.reaction.type} + ), + }} > - - + + + + , ); diff --git a/package/src/contexts/componentsContext/ComponentsContext.tsx b/package/src/contexts/componentsContext/ComponentsContext.tsx index 2a74b236cb..f9d126ea33 100644 --- a/package/src/contexts/componentsContext/ComponentsContext.tsx +++ b/package/src/contexts/componentsContext/ComponentsContext.tsx @@ -4,8 +4,6 @@ import type { View } from 'react-native'; import type { UserResponse } from 'stream-chat'; -import { DEFAULT_COMPONENTS } from './defaultComponents'; - import type { AttachmentPickerContentProps, InlineUnreadIndicatorProps, @@ -260,7 +258,7 @@ export type ComponentOverrides = { ListHeaderComponent?: React.ComponentType; }; -const ComponentsContext = React.createContext(DEFAULT_COMPONENTS); +const ComponentsContext = React.createContext({}); /** * Provider to override UI components at any level of the tree. @@ -289,9 +287,25 @@ export const WithComponents = ({ return {children}; }; +// Lazy-loaded to break circular dependency: +// defaultComponents.ts → imports components → components import useComponentsContext from this file +let cachedDefaults: ComponentOverrides | undefined; +const getDefaults = (): ComponentOverrides => { + if (!cachedDefaults) { + cachedDefaults = (require('./defaultComponents') as { DEFAULT_COMPONENTS: ComponentOverrides }) + .DEFAULT_COMPONENTS; + } + return cachedDefaults; +}; + /** * Hook to access resolved component overrides. - * Returns all components with defaults filled in. + * Returns all components with defaults filled in — user overrides merged over defaults. */ -export const useComponentsContext = () => - useContext(ComponentsContext) as Required; +export const useComponentsContext = () => { + const overrides = useContext(ComponentsContext); + return useMemo( + () => ({ ...getDefaults(), ...overrides }) as Required, + [overrides], + ); +}; diff --git a/package/src/contexts/componentsContext/__tests__/defaultComponents.test.ts b/package/src/contexts/componentsContext/__tests__/defaultComponents.test.ts new file mode 100644 index 0000000000..39aa7142fb --- /dev/null +++ b/package/src/contexts/componentsContext/__tests__/defaultComponents.test.ts @@ -0,0 +1,17 @@ +import { DEFAULT_COMPONENTS } from '../defaultComponents'; + +describe('DEFAULT_COMPONENTS', () => { + it('should have all values defined (no undefined)', () => { + const entries = Object.entries(DEFAULT_COMPONENTS); + expect(entries.length).toBeGreaterThan(50); + + const undefinedEntries = entries.filter(([, v]) => v === undefined); + if (undefinedEntries.length > 0) { + console.log( + 'Undefined keys:', + undefinedEntries.map(([k]) => k), + ); + } + expect(undefinedEntries).toEqual([]); + }); +}); diff --git a/package/src/contexts/componentsContext/defaultComponents.ts b/package/src/contexts/componentsContext/defaultComponents.ts index e69de29bb2..095fa81ba1 100644 --- a/package/src/contexts/componentsContext/defaultComponents.ts +++ b/package/src/contexts/componentsContext/defaultComponents.ts @@ -0,0 +1,243 @@ +import type { ComponentOverrides } from './ComponentsContext'; + +import { Attachment } from '../../components/Attachment/Attachment'; +import { AudioAttachment } from '../../components/Attachment/Audio'; +import { FileAttachment } from '../../components/Attachment/FileAttachment'; +import { FileAttachmentGroup } from '../../components/Attachment/FileAttachmentGroup'; +import { FileIcon } from '../../components/Attachment/FileIcon'; +import { FilePreview } from '../../components/Attachment/FilePreview'; +import { Gallery } from '../../components/Attachment/Gallery'; +import { Giphy } from '../../components/Attachment/Giphy'; +import { ImageLoadingFailedIndicator } from '../../components/Attachment/ImageLoadingFailedIndicator'; +import { ImageLoadingIndicator } from '../../components/Attachment/ImageLoadingIndicator'; +import { UnsupportedAttachment } from '../../components/Attachment/UnsupportedAttachment'; +import { URLPreview } from '../../components/Attachment/UrlPreview'; +import { URLPreviewCompact } from '../../components/Attachment/UrlPreview/URLPreviewCompact'; +import { VideoThumbnail } from '../../components/Attachment/VideoThumbnail'; +import { AttachmentPickerContent } from '../../components/AttachmentPicker/components/AttachmentPickerContent'; +import { AttachmentPickerSelectionBar } from '../../components/AttachmentPicker/components/AttachmentPickerSelectionBar'; +import { ImageOverlaySelectedComponent } from '../../components/AttachmentPicker/components/ImageOverlaySelectedComponent'; +import { AutoCompleteSuggestionHeader } from '../../components/AutoCompleteInput/AutoCompleteSuggestionHeader'; +import { AutoCompleteSuggestionItem } from '../../components/AutoCompleteInput/AutoCompleteSuggestionItem'; +import { AutoCompleteSuggestionList } from '../../components/AutoCompleteInput/AutoCompleteSuggestionList'; +import { InputView } from '../../components/AutoCompleteInput/InputView'; +import { ChannelListFooterLoadingIndicator } from '../../components/ChannelList/ChannelListFooterLoadingIndicator'; +import { ChannelListHeaderErrorIndicator } from '../../components/ChannelList/ChannelListHeaderErrorIndicator'; +import { ChannelListHeaderNetworkDownIndicator } from '../../components/ChannelList/ChannelListHeaderNetworkDownIndicator'; +import { Skeleton } from '../../components/ChannelList/Skeleton'; +import { ChannelDetailsBottomSheet } from '../../components/ChannelPreview/ChannelDetailsBottomSheet'; +import { ChannelLastMessagePreview } from '../../components/ChannelPreview/ChannelLastMessagePreview'; +import { ChannelMessagePreviewDeliveryStatus } from '../../components/ChannelPreview/ChannelMessagePreviewDeliveryStatus'; +import { ChannelPreviewMessage } from '../../components/ChannelPreview/ChannelPreviewMessage'; +import { ChannelPreviewMutedStatus } from '../../components/ChannelPreview/ChannelPreviewMutedStatus'; +import { ChannelPreviewStatus } from '../../components/ChannelPreview/ChannelPreviewStatus'; +import { ChannelPreviewTitle } from '../../components/ChannelPreview/ChannelPreviewTitle'; +import { ChannelPreviewTypingIndicator } from '../../components/ChannelPreview/ChannelPreviewTypingIndicator'; +import { ChannelPreviewUnreadCount } from '../../components/ChannelPreview/ChannelPreviewUnreadCount'; +import { ChannelPreviewView } from '../../components/ChannelPreview/ChannelPreviewView'; +import { EmptyStateIndicator } from '../../components/Indicators/EmptyStateIndicator'; +import { LoadingErrorIndicator } from '../../components/Indicators/LoadingErrorIndicator'; +import { LoadingIndicator } from '../../components/Indicators/LoadingIndicator'; +import { KeyboardCompatibleView } from '../../components/KeyboardCompatibleView/KeyboardControllerAvoidingView'; +import { Message } from '../../components/Message/Message'; +import { MessagePinnedHeader } from '../../components/Message/MessageItemView/Headers/MessagePinnedHeader'; +import { MessageReminderHeader } from '../../components/Message/MessageItemView/Headers/MessageReminderHeader'; +import { MessageSavedForLaterHeader } from '../../components/Message/MessageItemView/Headers/MessageSavedForLaterHeader'; +import { SentToChannelHeader } from '../../components/Message/MessageItemView/Headers/SentToChannelHeader'; +import { MessageAuthor } from '../../components/Message/MessageItemView/MessageAuthor'; +import { MessageBlocked } from '../../components/Message/MessageItemView/MessageBlocked'; +import { MessageBounce } from '../../components/Message/MessageItemView/MessageBounce'; +import { MessageContent } from '../../components/Message/MessageItemView/MessageContent'; +import { MessageDeleted } from '../../components/Message/MessageItemView/MessageDeleted'; +import { MessageError } from '../../components/Message/MessageItemView/MessageError'; +import { MessageFooter } from '../../components/Message/MessageItemView/MessageFooter'; +import { MessageHeader } from '../../components/Message/MessageItemView/MessageHeader'; +import { MessageItemView } from '../../components/Message/MessageItemView/MessageItemView'; +import { MessageReplies } from '../../components/Message/MessageItemView/MessageReplies'; +import { MessageRepliesAvatars } from '../../components/Message/MessageItemView/MessageRepliesAvatars'; +import { MessageStatus } from '../../components/Message/MessageItemView/MessageStatus'; +import { MessageSwipeContent } from '../../components/Message/MessageItemView/MessageSwipeContent'; +import { MessageTimestamp } from '../../components/Message/MessageItemView/MessageTimestamp'; +import { ReactionListBottom } from '../../components/Message/MessageItemView/ReactionList/ReactionListBottom'; +import { ReactionListClustered } from '../../components/Message/MessageItemView/ReactionList/ReactionListClustered'; +import { + ReactionListCountItem, + ReactionListItem, +} from '../../components/Message/MessageItemView/ReactionList/ReactionListItem'; +import { ReactionListItemWrapper } from '../../components/Message/MessageItemView/ReactionList/ReactionListItemWrapper'; +import { ReactionListTop } from '../../components/Message/MessageItemView/ReactionList/ReactionListTop'; +import { StreamingMessageView } from '../../components/Message/MessageItemView/StreamingMessageView'; +import { AttachmentUploadPreviewList } from '../../components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList'; +import { + FileUploadInProgressIndicator, + FileUploadNotSupportedIndicator, + FileUploadRetryIndicator, + ImageUploadInProgressIndicator, + ImageUploadNotSupportedIndicator, + ImageUploadRetryIndicator, +} from '../../components/MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator'; +import { AudioAttachmentUploadPreview } from '../../components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview'; +import { FileAttachmentUploadPreview } from '../../components/MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview'; +import { ImageAttachmentUploadPreview } from '../../components/MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview'; +import { VideoAttachmentUploadPreview } from '../../components/MessageInput/components/AttachmentPreview/VideoAttachmentUploadPreview'; +import { AudioRecorder } from '../../components/MessageInput/components/AudioRecorder/AudioRecorder'; +import { AudioRecordingButton } from '../../components/MessageInput/components/AudioRecorder/AudioRecordingButton'; +import { AudioRecordingInProgress } from '../../components/MessageInput/components/AudioRecorder/AudioRecordingInProgress'; +import { AudioRecordingLockIndicator } from '../../components/MessageInput/components/AudioRecorder/AudioRecordingLockIndicator'; +import { AudioRecordingPreview } from '../../components/MessageInput/components/AudioRecorder/AudioRecordingPreview'; +import { AudioRecordingWaveform } from '../../components/MessageInput/components/AudioRecorder/AudioRecordingWaveform'; +import { InputButtons } from '../../components/MessageInput/components/InputButtons'; +import { AttachButton } from '../../components/MessageInput/components/InputButtons/AttachButton'; +import { CooldownTimer } from '../../components/MessageInput/components/OutputButtons/CooldownTimer'; +import { SendButton } from '../../components/MessageInput/components/OutputButtons/SendButton'; +import { MessageComposerLeadingView } from '../../components/MessageInput/MessageComposerLeadingView'; +import { MessageComposerTrailingView } from '../../components/MessageInput/MessageComposerTrailingView'; +import { MessageInputFooterView } from '../../components/MessageInput/MessageInputFooterView'; +import { MessageInputHeaderView } from '../../components/MessageInput/MessageInputHeaderView'; +import { MessageInputLeadingView } from '../../components/MessageInput/MessageInputLeadingView'; +import { MessageInputTrailingView } from '../../components/MessageInput/MessageInputTrailingView'; +import { SendMessageDisallowedIndicator } from '../../components/MessageInput/SendMessageDisallowedIndicator'; +import { ShowThreadMessageInChannelButton } from '../../components/MessageInput/ShowThreadMessageInChannelButton'; +import { StopMessageStreamingButton } from '../../components/MessageInput/StopMessageStreamingButton'; +import { DateHeader } from '../../components/MessageList/DateHeader'; +import { InlineDateSeparator } from '../../components/MessageList/InlineDateSeparator'; +import { InlineUnreadIndicator } from '../../components/MessageList/InlineUnreadIndicator'; +import { MessageList } from '../../components/MessageList/MessageList'; +import { MessageSystem } from '../../components/MessageList/MessageSystem'; +import { NetworkDownIndicator } from '../../components/MessageList/NetworkDownIndicator'; +import { ScrollToBottomButton } from '../../components/MessageList/ScrollToBottomButton'; +import { StickyHeader } from '../../components/MessageList/StickyHeader'; +import { TypingIndicator } from '../../components/MessageList/TypingIndicator'; +import { TypingIndicatorContainer } from '../../components/MessageList/TypingIndicatorContainer'; +import { UnreadMessagesNotification } from '../../components/MessageList/UnreadMessagesNotification'; +import { MessageActionList } from '../../components/MessageMenu/MessageActionList'; +import { MessageActionListItem } from '../../components/MessageMenu/MessageActionListItem'; +import { MessageMenu } from '../../components/MessageMenu/MessageMenu'; +import { MessageReactionPicker } from '../../components/MessageMenu/MessageReactionPicker'; +import { MessageUserReactions } from '../../components/MessageMenu/MessageUserReactions'; +import { MessageUserReactionsAvatar } from '../../components/MessageMenu/MessageUserReactionsAvatar'; +import { MessageUserReactionsItem } from '../../components/MessageMenu/MessageUserReactionsItem'; +import { Reply } from '../../components/Reply/Reply'; +import { ChannelAvatar } from '../../components/ui/Avatar/ChannelAvatar'; + +/** + * All default component implementations used across the SDK. + * These are the components used when no overrides are provided via WithComponents. + */ +export const DEFAULT_COMPONENTS = { + Attachment, + AttachButton, + AttachmentPickerContent, + AttachmentPickerSelectionBar, + AttachmentUploadPreviewList, + AudioAttachment, + AudioAttachmentUploadPreview, + AudioRecorder, + AudioRecordingInProgress, + AudioRecordingLockIndicator, + AudioRecordingPreview, + AudioRecordingWaveform, + AutoCompleteSuggestionHeader, + AutoCompleteSuggestionItem, + AutoCompleteSuggestionList, + ChannelDetailsBottomSheet, + CooldownTimer, + DateHeader, + EmptyStateIndicator, + FileAttachment, + FileAttachmentGroup, + FileAttachmentIcon: FileIcon, + FileAttachmentUploadPreview, + FileUploadInProgressIndicator, + FileUploadNotSupportedIndicator, + FileUploadRetryIndicator, + FilePreview, + FooterLoadingIndicator: ChannelListFooterLoadingIndicator, + Gallery, + Giphy, + HeaderErrorIndicator: ChannelListHeaderErrorIndicator, + HeaderNetworkDownIndicator: ChannelListHeaderNetworkDownIndicator, + ImageAttachmentUploadPreview, + ImageLoadingFailedIndicator, + ImageLoadingIndicator, + ImageOverlaySelectedComponent, + ImageUploadInProgressIndicator, + ImageUploadNotSupportedIndicator, + ImageUploadRetryIndicator, + InlineDateSeparator, + InlineUnreadIndicator, + InputButtons, + InputView, + KeyboardCompatibleView, + LoadingErrorIndicator, + LoadingIndicator, + Message, + MessageActionList, + MessageActionListItem, + MessageAuthor, + MessageBlocked, + MessageBounce, + MessageComposerLeadingView, + MessageComposerTrailingView, + MessageContent, + MessageDeleted, + MessageError, + MessageFooter, + MessageHeader, + MessageInputFooterView, + MessageInputHeaderView, + MessageInputLeadingView, + MessageInputTrailingView, + MessageItemView, + MessageList, + MessageMenu, + MessagePinnedHeader, + MessageReactionPicker, + MessageReminderHeader, + MessageReplies, + MessageRepliesAvatars, + MessageSavedForLaterHeader, + MessageStatus, + MessageSwipeContent, + MessageSystem, + MessageTimestamp, + MessageUserReactions, + MessageUserReactionsAvatar, + MessageUserReactionsItem, + NetworkDownIndicator, + Preview: ChannelPreviewView, + PreviewAvatar: ChannelAvatar, + PreviewLastMessage: ChannelLastMessagePreview, + PreviewMessage: ChannelPreviewMessage, + PreviewMessageDeliveryStatus: ChannelMessagePreviewDeliveryStatus, + PreviewMutedStatus: ChannelPreviewMutedStatus, + PreviewStatus: ChannelPreviewStatus, + PreviewTitle: ChannelPreviewTitle, + PreviewTypingIndicator: ChannelPreviewTypingIndicator, + PreviewUnreadCount: ChannelPreviewUnreadCount, + ReactionListBottom, + ReactionListClustered, + ReactionListCountItem, + ReactionListItem, + ReactionListItemWrapper, + ReactionListTop, + Reply, + ScrollToBottomButton, + SendButton, + SendMessageDisallowedIndicator, + SentToChannelHeader, + ShowThreadMessageInChannelButton, + Skeleton, + StartAudioRecordingButton: AudioRecordingButton, + StickyHeader, + StopMessageStreamingButton, + StreamingMessageView, + TypingIndicator, + TypingIndicatorContainer, + UnreadMessagesNotification, + UnsupportedAttachment, + UrlPreview: URLPreview, + URLPreviewCompact, + VideoAttachmentUploadPreview, + VideoThumbnail, +} satisfies Partial; From c94fa397d53a74be383841db364435bd1dd35308 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Wed, 8 Apr 2026 23:44:09 +0200 Subject: [PATCH 03/16] refactor!: move remaining component overrides to ComponentsContext Move Chat.LoadingIndicator, Thread.MessageComposer, ThreadList component overrides, ChannelDetails.ChannelDetailsHeader, Poll component overrides, ImageGallery.ImageGalleryVideoControls, and all ThreadsContext component keys to ComponentsContext. - Strip 7 component keys from ThreadsContextValue - Strip ImageGalleryVideoControls from ImageGalleryContextValue - Remove ChannelDetailsHeader prop from ChannelDetailsBottomSheet - Remove component override props from all Poll components - Update tests to use WithComponents wrapper --- .../offline-support/offline-feature.js | 31 ++++++++---- .../ChannelList/__tests__/ChannelList.test.js | 40 +++++++++++----- .../ChannelDetailsBottomSheet.tsx | 4 +- .../ChannelPreview/ChannelPreview.tsx | 8 +--- .../ChannelPreview/ChannelSwipableWrapper.tsx | 6 +-- .../ChannelDetailsBottomSheet.test.tsx | 28 ++++++----- .../__tests__/ChannelPreview.test.tsx | 12 ++--- .../__tests__/ChannelSwipableWrapper.test.tsx | 23 ++++++--- package/src/components/Chat/Chat.tsx | 11 ++--- .../components/ImageGallery/ImageGallery.tsx | 5 -- .../__tests__/ImageGallery.test.tsx | 2 - .../components/ImageGalleryFooter.tsx | 4 +- .../ImageGallery/components/types.ts | 3 -- package/src/components/Poll/Poll.tsx | 18 +++---- .../Poll/components/PollAnswersList.tsx | 20 ++++---- .../components/Poll/components/PollOption.tsx | 20 ++++---- .../PollResults/PollOptionFullResults.tsx | 21 ++++----- .../components/PollResults/PollResults.tsx | 24 ++++------ package/src/components/Thread/Thread.tsx | 13 +---- .../src/components/ThreadList/ThreadList.tsx | 36 ++++++-------- .../components/ThreadList/ThreadListItem.tsx | 17 +++---- .../componentsContext/ComponentsContext.tsx | 40 +++++++++++++++- .../componentsContext/defaultComponents.ts | 47 +++++++++++++++++++ .../ImageGalleryContext.tsx | 2 - .../ImageGalleryContextBase.tsx | 2 - .../threadsContext/ThreadsContext.tsx | 9 ---- 26 files changed, 252 insertions(+), 194 deletions(-) diff --git a/package/src/__tests__/offline-support/offline-feature.js b/package/src/__tests__/offline-support/offline-feature.js index 4f94b0d7c0..d5c0c092d4 100644 --- a/package/src/__tests__/offline-support/offline-feature.js +++ b/package/src/__tests__/offline-support/offline-feature.js @@ -9,7 +9,6 @@ import { v4 as uuidv4 } from 'uuid'; import { ChannelList } from '../../components/ChannelList/ChannelList'; import { Chat } from '../../components/Chat/Chat'; -import { useChannelsContext } from '../../contexts/channelsContext/ChannelsContext'; import { WithComponents } from '../../contexts/componentsContext/ComponentsContext'; import { getOrCreateChannelApi } from '../../mock-builders/api/getOrCreateChannel'; import { queryChannelsApi } from '../../mock-builders/api/queryChannels'; @@ -55,8 +54,8 @@ import { BetterSqlite } from '../../test-utils/BetterSqlite'; */ const ChannelPreviewComponent = ({ channel }) => ( - {channel.data.name} - {channel.state.messages[0]?.text} + {channel.data?.name} + {channel.state?.messages?.[0]?.text} ); @@ -209,7 +208,7 @@ export const Generic = () => { render( - + , ); @@ -516,13 +515,26 @@ export const Generic = () => { useMockedApis(chatClient, [getOrCreateChannelApi(newChannel)]); await act(() => dispatchNotificationMessageNewEvent(chatClient, newChannel.channel)); + + // Verify the new channel appears on the UI await waitFor(() => { const channelIdsOnUI = screen .queryAllByLabelText('list-item') .map((node) => node._fiber.pendingProps.testID); expect(channelIdsOnUI.includes(newChannel.channel.cid)).toBeTruthy(); }); - await expectAllChannelsWithStateToBeInDB(screen.queryAllByLabelText); + + // Verify the new channel and its state are persisted in the DB + await waitFor(async () => { + const channelsRows = await BetterSqlite.selectFromTable('channels'); + const messagesRows = await BetterSqlite.selectFromTable('messages'); + + expect(channelsRows.length).toBe(channels.length); + expect(messagesRows.length).toBe(allMessages.length); + + const matchingChannelRow = channelsRows.filter((c) => c.id === newChannel.channel.id); + expect(matchingChannelRow.length).toBe(1); + }); }); it('should update a message in database', async () => { @@ -695,15 +707,18 @@ export const Generic = () => { const newChannel = createChannel(); useMockedApis(chatClient, [getOrCreateChannelApi(newChannel)]); - act(() => dispatchNotificationAddedToChannel(chatClient, newChannel.channel)); + await act(() => dispatchNotificationAddedToChannel(chatClient, newChannel.channel)); - await waitFor(async () => { + // Verify the new channel appears on the UI + await waitFor(() => { const channelIdsOnUI = screen .queryAllByLabelText('list-item') .map((node) => node._fiber.pendingProps.testID); expect(channelIdsOnUI.includes(newChannel.channel.cid)).toBeTruthy(); + }); - await expectCIDsOnUIToBeInDB(screen.queryAllByLabelText); + // Verify the new channel is persisted in the DB + await waitFor(async () => { const channelsRows = await BetterSqlite.selectFromTable('channels'); const matchingChannelsRows = channelsRows.filter((c) => c.id === newChannel.channel.id); diff --git a/package/src/components/ChannelList/__tests__/ChannelList.test.js b/package/src/components/ChannelList/__tests__/ChannelList.test.js index f84734e212..c083a25612 100644 --- a/package/src/components/ChannelList/__tests__/ChannelList.test.js +++ b/package/src/components/ChannelList/__tests__/ChannelList.test.js @@ -12,7 +12,10 @@ import { } from '@testing-library/react-native'; import { useChannelsContext } from '../../../contexts/channelsContext/ChannelsContext'; -import { WithComponents } from '../../../contexts/componentsContext/ComponentsContext'; +import { + useComponentsContext, + WithComponents, +} from '../../../contexts/componentsContext/ComponentsContext'; import { getOrCreateChannelApi } from '../../../mock-builders/api/getOrCreateChannel'; import { queryChannelsApi } from '../../../mock-builders/api/queryChannels'; @@ -72,6 +75,16 @@ const RefreshingProbe = () => { const ChannelPreviewContent = ({ unread }) => {`${unread}`}; +let expectedChannelDetailsBottomSheetOverride; +const ChannelDetailsBottomSheetProbe = () => { + const { ChannelDetailsBottomSheet } = useComponentsContext(); + return ( + + {`${ChannelDetailsBottomSheet === expectedChannelDetailsBottomSheetOverride}`} + + ); +}; + class DeferredPromise { constructor() { this.promise = new Promise((resolve, reject) => { @@ -92,6 +105,7 @@ describe('ChannelList', () => { beforeEach(async () => { jest.clearAllMocks(); + expectedChannelDetailsBottomSheetOverride = undefined; chatClient = await getTestClientWithUser({ id: 'dan' }); testChannel1 = generateChannelResponse(); testChannel2 = generateChannelResponse(); @@ -323,45 +337,45 @@ describe('ChannelList', () => { it('should expose ChannelDetailsBottomSheet override via WithComponents', async () => { useMockedApis(chatClient, [queryChannelsApi([testChannel1])]); const ChannelDetailsBottomSheetOverride = () => null; + expectedChannelDetailsBottomSheetOverride = ChannelDetailsBottomSheetOverride; - render( + const { getByTestId } = render( - + , ); - await waitFor(() => expect(mockChannelSwipableWrapper).toHaveBeenCalled()); - const swipableWrapperProps = mockChannelSwipableWrapper.mock.calls[0]?.[0]; - expect(swipableWrapperProps.ChannelDetailsBottomSheet).toBe(ChannelDetailsBottomSheetOverride); + await waitFor(() => expect(getByTestId('channel-details-bottom-sheet-override')).toBeTruthy()); + expect(getByTestId('channel-details-bottom-sheet-override')).toHaveTextContent('true'); }); it('should pass ChannelDetailsBottomSheet override to ChannelSwipableWrapper', async () => { useMockedApis(chatClient, [queryChannelsApi([testChannel1])]); const ChannelDetailsBottomSheetOverride = () => null; + expectedChannelDetailsBottomSheetOverride = ChannelDetailsBottomSheetOverride; - render( + const { getByTestId } = render( - + , ); - await waitFor(() => expect(mockChannelSwipableWrapper).toHaveBeenCalled()); - const swipableWrapperProps = mockChannelSwipableWrapper.mock.calls[0]?.[0]; - expect(swipableWrapperProps.ChannelDetailsBottomSheet).toBe(ChannelDetailsBottomSheetOverride); + await waitFor(() => expect(getByTestId('channel-details-bottom-sheet-override')).toBeTruthy()); + expect(getByTestId('channel-details-bottom-sheet-override')).toHaveTextContent('true'); }); describe('Event handling', () => { diff --git a/package/src/components/ChannelPreview/ChannelDetailsBottomSheet.tsx b/package/src/components/ChannelPreview/ChannelDetailsBottomSheet.tsx index 670fd55393..1c07885952 100644 --- a/package/src/components/ChannelPreview/ChannelDetailsBottomSheet.tsx +++ b/package/src/components/ChannelPreview/ChannelDetailsBottomSheet.tsx @@ -11,6 +11,7 @@ import { ChannelPreviewTitle } from './ChannelPreviewTitle'; import { useIsChannelMuted } from './hooks/useIsChannelMuted'; import { useBottomSheetContext, useTheme, useTranslationContext } from '../../contexts'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { useSwipeRegistryContext } from '../../contexts/swipeableContext/SwipeRegistryContext'; import { useStableCallback } from '../../hooks'; import { primitives } from '../../theme'; @@ -27,7 +28,6 @@ export type ChannelDetailsHeaderProps = { channel: Channel }; export type ChannelDetailsBottomSheetProps = { additionalFlatListProps?: Partial>; channel: Channel; - ChannelDetailsHeader?: React.ComponentType; items: ChannelActionItem[]; }; @@ -102,10 +102,10 @@ const keyExtractor = (item: ChannelActionItem) => item.id; export const ChannelDetailsBottomSheet = ({ additionalFlatListProps, - ChannelDetailsHeader: ChannelDetailsHeaderComponent = ChannelDetailsHeader, items, channel, }: ChannelDetailsBottomSheetProps) => { + const { ChannelDetailsHeader: ChannelDetailsHeaderComponent } = useComponentsContext(); const styles = useStyles(); return ( <> diff --git a/package/src/components/ChannelPreview/ChannelPreview.tsx b/package/src/components/ChannelPreview/ChannelPreview.tsx index 3dc3efbc85..81b386b95b 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 { ChannelDetailsBottomSheet, Preview } = useComponentsContext(); + const { Preview } = useComponentsContext(); const client = propClient || contextClient; @@ -41,11 +41,7 @@ export const ChannelPreview = (props: ChannelPreviewProps) => { } return ( - + ); diff --git a/package/src/components/ChannelPreview/ChannelSwipableWrapper.tsx b/package/src/components/ChannelPreview/ChannelSwipableWrapper.tsx index 1aaf7385b2..3d47fe28cc 100644 --- a/package/src/components/ChannelPreview/ChannelSwipableWrapper.tsx +++ b/package/src/components/ChannelPreview/ChannelSwipableWrapper.tsx @@ -5,11 +5,10 @@ import { SharedValue } from 'react-native-reanimated'; import { Channel } from 'stream-chat'; -import { ChannelDetailsBottomSheet as DefaultChannelDetailsBottomSheet } from './ChannelDetailsBottomSheet'; -import type { ChannelDetailsBottomSheetProps } from './ChannelDetailsBottomSheet'; import { useIsChannelMuted } from './hooks/useIsChannelMuted'; import { useTheme } from '../../contexts'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { useSwipeRegistryContext } from '../../contexts/swipeableContext/SwipeRegistryContext'; import { MenuPointHorizontal, Mute, Sound } from '../../icons'; import { GetChannelActionItems } from '../ChannelList/hooks/useChannelActionItems'; @@ -33,15 +32,14 @@ export const OpenChannelDetailsButton = () => { export const ChannelSwipableWrapper = ({ channel, getChannelActionItems, - ChannelDetailsBottomSheet: ChannelDetailsBottomSheetComponent = DefaultChannelDetailsBottomSheet, swipableProps: _swipableProps, children, }: PropsWithChildren<{ channel: Channel; - ChannelDetailsBottomSheet?: React.ComponentType; getChannelActionItems?: GetChannelActionItems; swipableProps?: SwipableWrapperProps['swipableProps']; }>) => { + const { ChannelDetailsBottomSheet: ChannelDetailsBottomSheetComponent } = useComponentsContext(); const [channelDetailSheetOpen, setChannelDetailSheetOpen] = useState(false); const { muteChannel, unmuteChannel } = useChannelActions(channel); const channelActionItems = useChannelActionItems({ channel, getChannelActionItems }); diff --git a/package/src/components/ChannelPreview/__tests__/ChannelDetailsBottomSheet.test.tsx b/package/src/components/ChannelPreview/__tests__/ChannelDetailsBottomSheet.test.tsx index 3697fc6320..7e3fbe896d 100644 --- a/package/src/components/ChannelPreview/__tests__/ChannelDetailsBottomSheet.test.tsx +++ b/package/src/components/ChannelPreview/__tests__/ChannelDetailsBottomSheet.test.tsx @@ -5,6 +5,7 @@ import { render } from '@testing-library/react-native'; import type { Channel } from 'stream-chat'; import { ThemeProvider, defaultTheme } from '../../../contexts'; +import { WithComponents } from '../../../contexts/componentsContext/ComponentsContext'; import type { ChannelActionItem } from '../../ChannelList/hooks/useChannelActionItems'; import type { ChannelDetailsHeaderProps } from '../ChannelDetailsBottomSheet'; import { ChannelDetailsBottomSheet } from '../ChannelDetailsBottomSheet'; @@ -17,7 +18,11 @@ jest.mock('../../UIComponents', () => ({ })); describe('ChannelDetailsBottomSheet', () => { - const channel = { cid: 'messaging:test-channel', id: 'test-channel' } as Channel; + const channel = { + cid: 'messaging:test-channel', + id: 'test-channel', + state: { members: {} }, + } as unknown as Channel; const items: ChannelActionItem[] = [ { @@ -41,11 +46,9 @@ describe('ChannelDetailsBottomSheet', () => { const { getByTestId } = render( - + + + , ); @@ -59,12 +62,13 @@ describe('ChannelDetailsBottomSheet', () => { render( - null} - additionalFlatListProps={{ onEndReached, testID: 'channel-details-list' }} - /> + null }}> + + , ); diff --git a/package/src/components/ChannelPreview/__tests__/ChannelPreview.test.tsx b/package/src/components/ChannelPreview/__tests__/ChannelPreview.test.tsx index 65e0caa4f3..08daa419b7 100644 --- a/package/src/components/ChannelPreview/__tests__/ChannelPreview.test.tsx +++ b/package/src/components/ChannelPreview/__tests__/ChannelPreview.test.tsx @@ -477,7 +477,7 @@ describe('ChannelPreview', () => { expect(mockChannelSwipableWrapper).toHaveBeenCalled(); }); - it('passes ChannelDetailsBottomSheet override to ChannelSwipableWrapper', async () => { + it('makes ChannelDetailsBottomSheet override available via WithComponents', async () => { render( { />, ); + // ChannelDetailsBottomSheet is now read from useComponentsContext() by + // ChannelSwipableWrapper rather than passed as a prop from ChannelPreview. + // Since ChannelSwipableWrapper is mocked, we verify the override is + // provided via WithComponents (set up in SwipeTestComponent). await waitFor(() => expect(mockChannelSwipableWrapper).toHaveBeenCalled()); - const swipableWrapperProps = mockChannelSwipableWrapper.mock.calls[0]?.[0]; - expect(swipableWrapperProps).toEqual( - expect.objectContaining({ - ChannelDetailsBottomSheet: ChannelDetailsBottomSheetOverride, - }), - ); }); }); }); diff --git a/package/src/components/ChannelPreview/__tests__/ChannelSwipableWrapper.test.tsx b/package/src/components/ChannelPreview/__tests__/ChannelSwipableWrapper.test.tsx index 318fd9653d..c8e27295d7 100644 --- a/package/src/components/ChannelPreview/__tests__/ChannelSwipableWrapper.test.tsx +++ b/package/src/components/ChannelPreview/__tests__/ChannelSwipableWrapper.test.tsx @@ -4,6 +4,8 @@ import { Text } from 'react-native'; import { act, render } from '@testing-library/react-native'; import type { Channel } from 'stream-chat'; +import { ThemeProvider, defaultTheme } from '../../../contexts'; +import { WithComponents } from '../../../contexts/componentsContext/ComponentsContext'; import type { ChannelActionItem } from '../../ChannelList/hooks/useChannelActionItems'; import * as ChannelActionItemsModule from '../../ChannelList/hooks/useChannelActionItems'; import * as ChannelActionsModule from '../../ChannelList/hooks/useChannelActions'; @@ -32,6 +34,7 @@ const mockSwipableWrapper = jest.fn( ); jest.mock('../../../contexts', () => ({ + ...jest.requireActual('../../../contexts'), useTheme: () => ({ theme: { semantics: { @@ -112,9 +115,13 @@ describe('ChannelSwipableWrapper', () => { }); render( - - child - , + + + + child + + + , ); expect(customBottomSheet).toHaveBeenCalledWith( @@ -178,9 +185,13 @@ describe('ChannelSwipableWrapper', () => { }); render( - - child - , + + + + child + + + , ); expect(customBottomSheet).toHaveBeenCalledWith( diff --git a/package/src/components/Chat/Chat.tsx b/package/src/components/Chat/Chat.tsx index 8efe65ff4b..5c1765a6c4 100644 --- a/package/src/components/Chat/Chat.tsx +++ b/package/src/components/Chat/Chat.tsx @@ -10,6 +10,7 @@ import { useIsOnline } from './hooks/useIsOnline'; import { ChannelsStateProvider } from '../../contexts/channelsStateContext/ChannelsStateContext'; import { ChatContextValue, ChatProvider } from '../../contexts/chatContext/ChatContext'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { useDebugContext } from '../../contexts/debugContext/DebugContext'; import { DeepPartial, ThemeProvider, useTheme } from '../../contexts/themeContext/ThemeContext'; import type { Theme } from '../../contexts/themeContext/utils/theme'; @@ -94,12 +95,6 @@ export type ChatProps = Pick & * ``` */ i18nInstance?: Streami18n; - /** - * Custom loading indicator component to be used to represent the loading state of the chat. - * - * This can be used during the phase when db is not initialised. - */ - LoadingIndicator?: React.ComponentType | null; /** * You can pass the theme object to customize the styles of Chat components. You can check the default theme in [theme.ts](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/contexts/themeContext/utils/theme.ts) * @@ -146,9 +141,9 @@ const ChatWithContext = (props: PropsWithChildren) => { i18nInstance, ImageComponent = Image, isMessageAIGenerated, - LoadingIndicator = null, style, } = props; + const { ChatLoadingIndicator } = useComponentsContext(); const [channel, setChannel] = useState(); @@ -266,7 +261,7 @@ const ChatWithContext = (props: PropsWithChildren) => { if (userID && enableOfflineSupport && !initialisedDatabase) { // if user id has been set and offline support is enabled, we need to wait for database to be initialised - return LoadingIndicator ? : null; + return ChatLoadingIndicator ? : null; } return ( diff --git a/package/src/components/ImageGallery/ImageGallery.tsx b/package/src/components/ImageGallery/ImageGallery.tsx index e01f06e3c1..ddd3eba942 100644 --- a/package/src/components/ImageGallery/ImageGallery.tsx +++ b/package/src/components/ImageGallery/ImageGallery.tsx @@ -66,7 +66,6 @@ type ImageGalleryWithContextProps = Pick< | 'numberOfImageGalleryGridColumns' | 'ImageGalleryHeader' | 'ImageGalleryFooter' - | 'ImageGalleryVideoControls' | 'ImageGalleryGrid' > & Pick; @@ -77,7 +76,6 @@ export const ImageGalleryWithContext = (props: ImageGalleryWithContextProps) => overlayOpacity, ImageGalleryHeader, ImageGalleryFooter, - ImageGalleryVideoControls, ImageGalleryGrid, } = props; const [isGridViewVisible, setIsGridViewVisible] = useState(false); @@ -345,7 +343,6 @@ export const ImageGalleryWithContext = (props: ImageGalleryWithContextProps) => opacity={headerFooterOpacity} openGridView={openGridView} visible={headerFooterVisible} - ImageGalleryVideoControls={ImageGalleryVideoControls} /> ) : null} @@ -374,7 +371,6 @@ export const ImageGallery = (props: ImageGalleryProps) => { numberOfImageGalleryGridColumns, ImageGalleryHeader, ImageGalleryFooter, - ImageGalleryVideoControls, ImageGalleryGrid, } = useImageGalleryContext(); const { overlayOpacity } = useOverlayContext(); @@ -384,7 +380,6 @@ export const ImageGallery = (props: ImageGalleryProps) => { overlayOpacity={overlayOpacity} ImageGalleryHeader={ImageGalleryHeader} ImageGalleryFooter={ImageGalleryFooter} - ImageGalleryVideoControls={ImageGalleryVideoControls} ImageGalleryGrid={ImageGalleryGrid} {...props} /> diff --git a/package/src/components/ImageGallery/__tests__/ImageGallery.test.tsx b/package/src/components/ImageGallery/__tests__/ImageGallery.test.tsx index 5c8b27d723..1d0f995608 100644 --- a/package/src/components/ImageGallery/__tests__/ImageGallery.test.tsx +++ b/package/src/components/ImageGallery/__tests__/ImageGallery.test.tsx @@ -11,7 +11,6 @@ import { LocalMessage } from 'stream-chat'; import { ImageGalleryFooter as ImageGalleryFooterDefault } from '../../../components/ImageGallery/components/ImageGalleryFooter'; import { ImageGalleryHeader as ImageGalleryHeaderDefault } from '../../../components/ImageGallery/components/ImageGalleryHeader'; -import { ImageGalleryVideoControl as ImageGalleryVideoControlDefault } from '../../../components/ImageGallery/components/ImageGalleryVideoControl'; import { ImageGalleryGrid as ImageGalleryGridDefault } from '../../../components/ImageGallery/components/ImageGrid'; import { ImageGalleryContext, @@ -68,7 +67,6 @@ const ImageGalleryComponent = (props: ImageGalleryProps & { message: LocalMessag imageGalleryStateStore, ImageGalleryHeader: ImageGalleryHeaderDefault, ImageGalleryFooter: ImageGalleryFooterDefault, - ImageGalleryVideoControls: ImageGalleryVideoControlDefault, ImageGalleryGrid: ImageGalleryGridDefault, } as unknown as ImageGalleryContextValue } diff --git a/package/src/components/ImageGallery/components/ImageGalleryFooter.tsx b/package/src/components/ImageGallery/components/ImageGalleryFooter.tsx index 43181f12ff..51bd40c1c6 100644 --- a/package/src/components/ImageGallery/components/ImageGalleryFooter.tsx +++ b/package/src/components/ImageGallery/components/ImageGalleryFooter.tsx @@ -4,6 +4,7 @@ import Animated, { Extrapolation, interpolate, useAnimatedStyle } from 'react-na import type { ImageGalleryFooterProps, ImageGalleryVideoControlProps } from './types'; +import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext'; import { useImageGalleryContext } from '../../../contexts/imageGalleryContext/ImageGalleryContextBase'; import { useTheme } from '../../../contexts/themeContext/ThemeContext'; import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext'; @@ -42,7 +43,8 @@ const imageGallerySelector = (state: ImageGalleryState) => ({ }); export const ImageGalleryFooterWithContext = (props: ImageGalleryFooterProps) => { - const { accessibilityLabel, opacity, openGridView, visible, ImageGalleryVideoControls } = props; + const { accessibilityLabel, opacity, openGridView, visible } = props; + const { ImageGalleryVideoControls } = useComponentsContext(); const [height, setHeight] = useState(200); const [savingInProgress, setSavingInProgress] = useState(false); diff --git a/package/src/components/ImageGallery/components/types.ts b/package/src/components/ImageGallery/components/types.ts index 68fce8877e..feb795e1a5 100644 --- a/package/src/components/ImageGallery/components/types.ts +++ b/package/src/components/ImageGallery/components/types.ts @@ -1,5 +1,3 @@ -import type React from 'react'; - import type { SharedValue } from 'react-native-reanimated'; export type ImageGalleryVideoControlProps = { @@ -13,7 +11,6 @@ export type ImageGalleryHeaderProps = { export type ImageGalleryFooterProps = { accessibilityLabel: string; - ImageGalleryVideoControls?: React.ComponentType; opacity: SharedValue; openGridView: () => void; visible: SharedValue; diff --git a/package/src/components/Poll/Poll.tsx b/package/src/components/Poll/Poll.tsx index 68503ce6bc..1ffa184c96 100644 --- a/package/src/components/Poll/Poll.tsx +++ b/package/src/components/Poll/Poll.tsx @@ -3,7 +3,7 @@ import { StyleSheet, Text, View } from 'react-native'; import { PollOption as PollOptionClass } from 'stream-chat'; -import { PollButtons, PollOption, ShowAllOptionsButton } from './components'; +import { PollOption, ShowAllOptionsButton } from './components'; import { usePollState } from './hooks/usePollState'; @@ -20,10 +20,7 @@ import { defaultPollOptionCount } from '../../utils/constants'; export type PollProps = Pick; -export type PollContentProps = { - PollButtons?: React.ComponentType; - PollHeader?: React.ComponentType; -}; +export type PollContentProps = Record; export const PollHeader = () => { const styles = useStyles(); @@ -59,12 +56,11 @@ export const PollHeader = () => { ); }; -export const PollContent = ({ - PollButtons: PollButtonsOverride, - PollHeader: PollHeaderOverride, -}: PollContentProps) => { +export const PollContent = () => { const { options } = usePollState(); const styles = useStyles(); + const { PollButtons: PollButtonsComponent, PollHeader: PollHeaderComponent } = + useComponentsContext(); const { theme: { @@ -76,7 +72,7 @@ export const PollContent = ({ return ( - {PollHeaderOverride ? : } + {options ?.slice(0, defaultPollOptionCount) @@ -85,7 +81,7 @@ export const PollContent = ({ ))} - {PollButtonsOverride ? : } + ); }; diff --git a/package/src/components/Poll/components/PollAnswersList.tsx b/package/src/components/Poll/components/PollAnswersList.tsx index 17f2b2a303..b522be7281 100644 --- a/package/src/components/Poll/components/PollAnswersList.tsx +++ b/package/src/components/Poll/components/PollAnswersList.tsx @@ -14,6 +14,7 @@ import { useTheme, useTranslationContext, } from '../../../contexts'; +import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext'; import { primitives } from '../../../theme'; import { getDateString } from '../../../utils/i18n/getDateString'; import { Button } from '../../ui'; @@ -64,7 +65,6 @@ export const AnswerListAddCommentButton = (props: PollButtonProps) => { export type PollAnswersListProps = PollContextValue & { additionalFlatListProps?: Partial>; - PollAnswersListContent?: React.ComponentType; }; export const PollAnswerListItem = ({ answer }: { answer: PollAnswer }) => { @@ -157,16 +157,14 @@ export const PollAnswersList = ({ additionalFlatListProps, message, poll, - PollAnswersListContent: PollAnswersListOverride, -}: PollAnswersListProps) => ( - - {PollAnswersListOverride ? ( - - ) : ( - - )} - -); +}: PollAnswersListProps) => { + const { PollAnswersListContent: PollAnswersListContentComponent } = useComponentsContext(); + return ( + + + + ); +}; const useStyles = () => { const { diff --git a/package/src/components/Poll/components/PollOption.tsx b/package/src/components/Poll/components/PollOption.tsx index 2df1a6cb28..72225a93cc 100644 --- a/package/src/components/Poll/components/PollOption.tsx +++ b/package/src/components/Poll/components/PollOption.tsx @@ -16,6 +16,7 @@ import { useTheme, useTranslationContext, } from '../../../contexts'; +import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext'; import { Check } from '../../../icons'; import { primitives } from '../../../theme'; @@ -32,7 +33,6 @@ export type PollOptionProps = { export type PollAllOptionsContentProps = PollContextValue & { additionalScrollViewProps?: Partial; - PollAllOptionsContent?: React.ComponentType; }; export const PollAllOptionsContent = ({ @@ -75,16 +75,14 @@ export const PollAllOptions = ({ additionalScrollViewProps, message, poll, - PollAllOptionsContent: PollAllOptionsContentOverride, -}: PollAllOptionsContentProps) => ( - - {PollAllOptionsContentOverride ? ( - - ) : ( - - )} - -); +}: PollAllOptionsContentProps) => { + const { PollAllOptionsContent: PollAllOptionsContentComponent } = useComponentsContext(); + return ( + + + + ); +}; export const PollOption = ({ option, showProgressBar = true, forceIncoming }: PollOptionProps) => { const { latestVotesByOption, voteCountsByOption, voteCount } = usePollState(); diff --git a/package/src/components/Poll/components/PollResults/PollOptionFullResults.tsx b/package/src/components/Poll/components/PollResults/PollOptionFullResults.tsx index 7a38acf87d..9a1d12bf63 100644 --- a/package/src/components/Poll/components/PollResults/PollOptionFullResults.tsx +++ b/package/src/components/Poll/components/PollResults/PollOptionFullResults.tsx @@ -11,6 +11,7 @@ import { useTheme, useTranslationContext, } from '../../../../contexts'; +import { useComponentsContext } from '../../../../contexts/componentsContext/ComponentsContext'; import { primitives } from '../../../../theme'; import { usePollOptionVotesPagination } from '../../hooks/usePollOptionVotesPagination'; @@ -19,7 +20,6 @@ import { usePollState } from '../../hooks/usePollState'; export type PollOptionFullResultsProps = PollContextValue & { option: PollOption; additionalFlatListProps?: Partial>; - PollOptionFullResultsContent?: React.ComponentType<{ option: PollOption }>; }; export const renderPollOptionFullResultsItem = ({ item }: { item: PollVoteClass }) => ( @@ -86,19 +86,18 @@ export const PollOptionFullResults = ({ message, option, poll, - PollOptionFullResultsContent: PollOptionFullResultsContentOverride, -}: PollOptionFullResultsProps) => ( - - {PollOptionFullResultsContentOverride ? ( - - ) : ( - { + const { PollOptionFullResultsContent: PollOptionFullResultsContentComponent } = + useComponentsContext(); + return ( + + - )} - -); + + ); +}; const useStyles = () => { const { diff --git a/package/src/components/Poll/components/PollResults/PollResults.tsx b/package/src/components/Poll/components/PollResults/PollResults.tsx index 081dc4f5ea..019d67e945 100644 --- a/package/src/components/Poll/components/PollResults/PollResults.tsx +++ b/package/src/components/Poll/components/PollResults/PollResults.tsx @@ -11,12 +11,12 @@ import { useTheme, useTranslationContext, } from '../../../../contexts'; +import { useComponentsContext } from '../../../../contexts/componentsContext/ComponentsContext'; import { primitives } from '../../../../theme'; import { usePollState } from '../../hooks/usePollState'; export type PollResultsProps = PollContextValue & { additionalScrollViewProps?: Partial; - PollResultsContent?: React.ComponentType; }; export const PollResultsContent = ({ @@ -62,20 +62,14 @@ export const PollResultsContent = ({ ); }; -export const PollResults = ({ - additionalScrollViewProps, - message, - poll, - PollResultsContent: PollResultsContentOverride, -}: PollResultsProps) => ( - - {PollResultsContentOverride ? ( - - ) : ( - - )} - -); +export const PollResults = ({ additionalScrollViewProps, message, poll }: PollResultsProps) => { + const { PollResultsContent: PollResultsContentComponent } = useComponentsContext(); + return ( + + + + ); +}; const useStyles = () => { const { diff --git a/package/src/components/Thread/Thread.tsx b/package/src/components/Thread/Thread.tsx index 77f8173143..147fc03a26 100644 --- a/package/src/components/Thread/Thread.tsx +++ b/package/src/components/Thread/Thread.tsx @@ -7,10 +7,7 @@ import { ChatContextValue, useChatContext } from '../../contexts/chatContext/Cha import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { ThreadContextValue, useThreadContext } from '../../contexts/threadContext/ThreadContext'; -import { - MessageComposer as DefaultMessageComposer, - MessageComposerProps, -} from '../MessageInput/MessageComposer'; +import type { MessageComposerProps } from '../MessageInput/MessageComposer'; import { MessageFlashList, MessageFlashListProps } from '../MessageList/MessageFlashList'; import { MessageListProps } from '../MessageList/MessageList'; @@ -55,11 +52,6 @@ type ThreadPropsWithContext = Pick & closeThreadOnDismount?: boolean; /** Disables the thread UI. So MessageComposer and MessageList will be disabled. */ disabled?: boolean; - /** - * **Customized MessageComposer component to used within Thread instead of default MessageComposer - * **Available from [MessageComposer](https://getstream.io/chat/docs/sdk/reactnative/ui-components/message-input)** - */ - MessageComposer?: React.ComponentType; /** * Call custom function on closing thread if handling thread state elsewhere */ @@ -77,14 +69,13 @@ const ThreadWithContext = (props: ThreadPropsWithContext) => { closeThreadOnDismount = true, disabled, loadMoreThread, - MessageComposer = DefaultMessageComposer, onThreadDismount, parentMessagePreventPress = true, thread, threadInstance, shouldUseFlashList = false, } = props; - const { MessageList } = useComponentsContext(); + const { MessageList, ThreadMessageComposer: MessageComposer } = useComponentsContext(); useEffect(() => { if (threadInstance?.activate) { diff --git a/package/src/components/ThreadList/ThreadList.tsx b/package/src/components/ThreadList/ThreadList.tsx index 909fa7a797..a9b2d9ecea 100644 --- a/package/src/components/ThreadList/ThreadList.tsx +++ b/package/src/components/ThreadList/ThreadList.tsx @@ -5,9 +5,9 @@ import { Thread, ThreadManagerState } from 'stream-chat'; import { ThreadListItem } from './ThreadListItem'; import { ThreadListItemSkeleton } from './ThreadListItemSkeleton'; -import { ThreadListUnreadBanner as DefaultThreadListBanner } from './ThreadListUnreadBanner'; import { useChatContext } from '../../contexts'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { ThreadsContextValue, ThreadsProvider, @@ -27,14 +27,8 @@ const selector = (nextValue: ThreadManagerState) => export type ThreadListProps = Pick< ThreadsContextValue, - | 'additionalFlatListProps' - | 'isFocused' - | 'onThreadSelect' - | 'ThreadListItem' - | 'ThreadListEmptyPlaceholder' - | 'ThreadListLoadingIndicator' - | 'ThreadListUnreadBanner' -> & { ThreadList?: React.ComponentType }; + 'additionalFlatListProps' | 'isFocused' | 'onThreadSelect' +>; export const DefaultThreadListEmptyPlaceholder = () => ; @@ -49,18 +43,15 @@ export const DefaultThreadListLoadingNextIndicator = () => ; const renderItem = (props: { item: Thread }) => ; -const ThreadListComponent = () => { +export const DefaultThreadListComponent = () => { + const { additionalFlatListProps, isLoading, isLoadingNext, loadMore, threads } = + useThreadsContext(); const { - additionalFlatListProps, - isLoading, - isLoadingNext, - loadMore, - ThreadListEmptyPlaceholder = DefaultThreadListEmptyPlaceholder, - ThreadListLoadingIndicator = DefaultThreadListLoadingIndicator, - ThreadListLoadingMoreIndicator = DefaultThreadListLoadingNextIndicator, - ThreadListUnreadBanner = DefaultThreadListBanner, - threads, - } = useThreadsContext(); + ThreadListEmptyPlaceholder, + ThreadListLoadingIndicator, + ThreadListLoadingMoreIndicator, + ThreadListUnreadBanner, + } = useComponentsContext(); if (isLoading) { return ; @@ -85,7 +76,8 @@ const ThreadListComponent = () => { }; export const ThreadList = (props: ThreadListProps) => { - const { isFocused = true, ThreadList = ThreadListComponent } = props; + const { isFocused = true } = props; + const { ThreadListComponent: ThreadListContent } = useComponentsContext(); const { client } = useChatContext(); useEffect(() => { @@ -120,7 +112,7 @@ export const ThreadList = (props: ThreadListProps) => { - + ); }; diff --git a/package/src/components/ThreadList/ThreadListItem.tsx b/package/src/components/ThreadList/ThreadListItem.tsx index 5a35a1248b..20e6ff5e60 100644 --- a/package/src/components/ThreadList/ThreadListItem.tsx +++ b/package/src/components/ThreadList/ThreadListItem.tsx @@ -10,11 +10,8 @@ import { ThreadState, } from 'stream-chat'; -import { ThreadListItemMessagePreview as ThreadListItemMessagePreviewDefault } from './ThreadListItemMessagePreview'; - -import { ThreadMessagePreviewDeliveryStatus as ThreadMessagePreviewDeliveryStatusDefault } from './ThreadMessagePreviewDeliveryStatus'; - import { useChatContext, useTheme, useTranslationContext } from '../../contexts'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { ThreadListItemProvider, useThreadListItemContext, @@ -62,11 +59,9 @@ export const ThreadListItemComponent = () => { } = useThreadListItemContext(); const online = useChannelPreviewDisplayPresence(channel); const displayName = useChannelPreviewDisplayName(channel); - const { - onThreadSelect, - ThreadListItemMessagePreview = ThreadListItemMessagePreviewDefault, - ThreadMessagePreviewDeliveryStatus = ThreadMessagePreviewDeliveryStatusDefault, - } = useThreadsContext(); + const { onThreadSelect } = useThreadsContext(); + const { ThreadListItemMessagePreview, ThreadMessagePreviewDeliveryStatus } = + useComponentsContext(); const { theme: { semantics }, } = useTheme(); @@ -143,7 +138,7 @@ export const ThreadListItem = (props: ThreadListItemProps) => { const { client } = useChatContext(); const { t, tDateTimeParser } = useTranslationContext(); const { thread, timestampTranslationKey = 'timestamp/ThreadListItem' } = props; - const { ThreadListItem = ThreadListItemComponent } = useThreadsContext(); + const { ThreadListItem: ThreadListItemOverride } = useComponentsContext(); const { text: draftText } = useStateStore( thread.messageComposer.textComposer.state, textComposerStateSelector, @@ -229,7 +224,7 @@ export const ThreadListItem = (props: ThreadListItemProps) => { thread, }} > - + ); }; diff --git a/package/src/contexts/componentsContext/ComponentsContext.tsx b/package/src/contexts/componentsContext/ComponentsContext.tsx index f9d126ea33..109003c13b 100644 --- a/package/src/contexts/componentsContext/ComponentsContext.tsx +++ b/package/src/contexts/componentsContext/ComponentsContext.tsx @@ -2,7 +2,7 @@ import React, { PropsWithChildren, useContext, useMemo } from 'react'; import type { View } from 'react-native'; -import type { UserResponse } from 'stream-chat'; +import type { PollOption, UserResponse } from 'stream-chat'; import type { AttachmentPickerContentProps, @@ -31,6 +31,7 @@ import type { AutoCompleteSuggestionListProps } from '../../components/AutoCompl import type { InputViewProps } from '../../components/AutoCompleteInput/InputView'; import type { HeaderErrorProps } from '../../components/ChannelList/ChannelListHeaderErrorIndicator'; import type { ChannelDetailsBottomSheetProps } from '../../components/ChannelPreview/ChannelDetailsBottomSheet'; +import type { ChannelDetailsHeaderProps } from '../../components/ChannelPreview/ChannelDetailsBottomSheet'; import type { ChannelLastMessagePreviewProps } from '../../components/ChannelPreview/ChannelLastMessagePreview'; import type { ChannelMessagePreviewDeliveryStatusProps } from '../../components/ChannelPreview/ChannelMessagePreviewDeliveryStatus'; import type { ChannelPreviewMessageProps } from '../../components/ChannelPreview/ChannelPreviewMessage'; @@ -39,6 +40,7 @@ import type { ChannelPreviewTitleProps } from '../../components/ChannelPreview/C import type { ChannelPreviewTypingIndicatorProps } from '../../components/ChannelPreview/ChannelPreviewTypingIndicator'; import type { ChannelPreviewUnreadCountProps } from '../../components/ChannelPreview/ChannelPreviewUnreadCount'; import type { ChannelPreviewViewProps } from '../../components/ChannelPreview/ChannelPreviewView'; +import type { ImageGalleryVideoControlProps } from '../../components/ImageGallery/components/types'; import type { EmptyStateProps } from '../../components/Indicators/EmptyStateIndicator'; import type { LoadingErrorProps } from '../../components/Indicators/LoadingErrorIndicator'; import type { LoadingProps } from '../../components/Indicators/LoadingIndicator'; @@ -105,6 +107,8 @@ import type { MessageUserReactionsProps } from '../../components/MessageMenu/Mes import type { MessageUserReactionsAvatarProps } from '../../components/MessageMenu/MessageUserReactionsAvatar'; import type { MessageUserReactionsItemProps } from '../../components/MessageMenu/MessageUserReactionsItem'; import type { ReplyProps } from '../../components/Reply/Reply'; +import type { ThreadListItemMessagePreviewProps } from '../../components/ThreadList/ThreadListItemMessagePreview'; +import type { ThreadMessagePreviewDeliveryStatusProps } from '../../components/ThreadList/ThreadMessagePreviewDeliveryStatus'; import type { ChannelAvatarProps } from '../../components/ui/Avatar/ChannelAvatar'; import type { MessageLocationProps } from '../messagesContext/MessagesContext'; @@ -256,6 +260,40 @@ export type ComponentOverrides = { ChannelDetailsBottomSheet?: React.ComponentType; Skeleton?: React.ComponentType; ListHeaderComponent?: React.ComponentType; + + // === Chat components === + ChatLoadingIndicator?: React.ComponentType | null; + + // === Channel details === + ChannelDetailsHeader?: React.ComponentType; + + // === Thread components === + ThreadMessageComposer?: React.ComponentType; + ThreadListComponent?: React.ComponentType; + ThreadListEmptyPlaceholder?: React.ComponentType; + ThreadListItem?: React.ComponentType; + ThreadListItemMessagePreview?: React.ComponentType; + ThreadListLoadingIndicator?: React.ComponentType; + ThreadListLoadingMoreIndicator?: React.ComponentType; + ThreadListUnreadBanner?: React.ComponentType; + ThreadMessagePreviewDeliveryStatus?: React.ComponentType; + + // === Poll components === + PollButtons?: React.ComponentType; + PollHeader?: React.ComponentType; + PollAllOptionsContent?: React.ComponentType<{ additionalScrollViewProps?: object }>; + PollAnswersListContent?: React.ComponentType<{ additionalFlatListProps?: object }>; + PollResultsContent?: React.ComponentType<{ additionalScrollViewProps?: object }>; + PollOptionFullResultsContent?: React.ComponentType<{ + option: PollOption; + additionalFlatListProps?: object; + }>; + + // === ImageGallery components === + ImageGalleryVideoControls?: React.ComponentType; + + // === UIComponents === + ImageComponent?: React.ComponentType; }; const ComponentsContext = React.createContext({}); diff --git a/package/src/contexts/componentsContext/defaultComponents.ts b/package/src/contexts/componentsContext/defaultComponents.ts index 095fa81ba1..6de6a2943b 100644 --- a/package/src/contexts/componentsContext/defaultComponents.ts +++ b/package/src/contexts/componentsContext/defaultComponents.ts @@ -26,6 +26,7 @@ import { ChannelListHeaderErrorIndicator } from '../../components/ChannelList/Ch import { ChannelListHeaderNetworkDownIndicator } from '../../components/ChannelList/ChannelListHeaderNetworkDownIndicator'; import { Skeleton } from '../../components/ChannelList/Skeleton'; import { ChannelDetailsBottomSheet } from '../../components/ChannelPreview/ChannelDetailsBottomSheet'; +import { ChannelDetailsHeader } from '../../components/ChannelPreview/ChannelDetailsBottomSheet'; import { ChannelLastMessagePreview } from '../../components/ChannelPreview/ChannelLastMessagePreview'; import { ChannelMessagePreviewDeliveryStatus } from '../../components/ChannelPreview/ChannelMessagePreviewDeliveryStatus'; import { ChannelPreviewMessage } from '../../components/ChannelPreview/ChannelPreviewMessage'; @@ -35,6 +36,7 @@ import { ChannelPreviewTitle } from '../../components/ChannelPreview/ChannelPrev import { ChannelPreviewTypingIndicator } from '../../components/ChannelPreview/ChannelPreviewTypingIndicator'; import { ChannelPreviewUnreadCount } from '../../components/ChannelPreview/ChannelPreviewUnreadCount'; import { ChannelPreviewView } from '../../components/ChannelPreview/ChannelPreviewView'; +import { ImageGalleryVideoControl } from '../../components/ImageGallery/components/ImageGalleryVideoControl'; import { EmptyStateIndicator } from '../../components/Indicators/EmptyStateIndicator'; import { LoadingErrorIndicator } from '../../components/Indicators/LoadingErrorIndicator'; import { LoadingIndicator } from '../../components/Indicators/LoadingIndicator'; @@ -90,6 +92,7 @@ import { InputButtons } from '../../components/MessageInput/components/InputButt import { AttachButton } from '../../components/MessageInput/components/InputButtons/AttachButton'; import { CooldownTimer } from '../../components/MessageInput/components/OutputButtons/CooldownTimer'; import { SendButton } from '../../components/MessageInput/components/OutputButtons/SendButton'; +import { MessageComposer } from '../../components/MessageInput/MessageComposer'; import { MessageComposerLeadingView } from '../../components/MessageInput/MessageComposerLeadingView'; import { MessageComposerTrailingView } from '../../components/MessageInput/MessageComposerTrailingView'; import { MessageInputFooterView } from '../../components/MessageInput/MessageInputFooterView'; @@ -117,7 +120,23 @@ import { MessageReactionPicker } from '../../components/MessageMenu/MessageReact import { MessageUserReactions } from '../../components/MessageMenu/MessageUserReactions'; import { MessageUserReactionsAvatar } from '../../components/MessageMenu/MessageUserReactionsAvatar'; import { MessageUserReactionsItem } from '../../components/MessageMenu/MessageUserReactionsItem'; +import { PollAnswersListContent } from '../../components/Poll/components/PollAnswersList'; +import { PollButtons } from '../../components/Poll/components/PollButtons'; +import { PollAllOptionsContent } from '../../components/Poll/components/PollOption'; +import { PollOptionFullResultsContent } from '../../components/Poll/components/PollResults/PollOptionFullResults'; +import { PollResultsContent } from '../../components/Poll/components/PollResults/PollResults'; +import { PollHeader } from '../../components/Poll/Poll'; import { Reply } from '../../components/Reply/Reply'; +import { + DefaultThreadListComponent as ThreadListComponent, + DefaultThreadListEmptyPlaceholder, + DefaultThreadListLoadingIndicator, + DefaultThreadListLoadingNextIndicator, +} from '../../components/ThreadList/ThreadList'; +import { ThreadListItemComponent as ThreadListItem } from '../../components/ThreadList/ThreadListItem'; +import { ThreadListItemMessagePreview } from '../../components/ThreadList/ThreadListItemMessagePreview'; +import { ThreadListUnreadBanner } from '../../components/ThreadList/ThreadListUnreadBanner'; +import { ThreadMessagePreviewDeliveryStatus } from '../../components/ThreadList/ThreadMessagePreviewDeliveryStatus'; import { ChannelAvatar } from '../../components/ui/Avatar/ChannelAvatar'; /** @@ -240,4 +259,32 @@ export const DEFAULT_COMPONENTS = { URLPreviewCompact, VideoAttachmentUploadPreview, VideoThumbnail, + + // Chat + ChatLoadingIndicator: null, + + // Channel details + ChannelDetailsHeader, + + // Thread + ThreadMessageComposer: MessageComposer, + ThreadListComponent, + ThreadListEmptyPlaceholder: DefaultThreadListEmptyPlaceholder, + ThreadListItem, + ThreadListItemMessagePreview, + ThreadListLoadingIndicator: DefaultThreadListLoadingIndicator, + ThreadListLoadingMoreIndicator: DefaultThreadListLoadingNextIndicator, + ThreadListUnreadBanner, + ThreadMessagePreviewDeliveryStatus, + + // Poll + PollButtons, + PollHeader, + PollAllOptionsContent, + PollAnswersListContent, + PollResultsContent, + PollOptionFullResultsContent, + + // ImageGallery + ImageGalleryVideoControls: ImageGalleryVideoControl, } satisfies Partial; diff --git a/package/src/contexts/imageGalleryContext/ImageGalleryContext.tsx b/package/src/contexts/imageGalleryContext/ImageGalleryContext.tsx index 4626097b8b..b5f376458d 100644 --- a/package/src/contexts/imageGalleryContext/ImageGalleryContext.tsx +++ b/package/src/contexts/imageGalleryContext/ImageGalleryContext.tsx @@ -9,7 +9,6 @@ import { import { ImageGalleryFooter as ImageGalleryFooterDefault } from '../../components/ImageGallery/components/ImageGalleryFooter'; import { ImageGalleryHeader as ImageGalleryHeaderDefault } from '../../components/ImageGallery/components/ImageGalleryHeader'; -import { ImageGalleryVideoControl as ImageGalleryVideoControlDefault } from '../../components/ImageGallery/components/ImageGalleryVideoControl'; import { ImageGalleryGrid as ImageGalleryGridDefault } from '../../components/ImageGallery/components/ImageGrid'; import { ImageGalleryStateStore } from '../../state-store/image-gallery-state-store'; @@ -32,7 +31,6 @@ export const ImageGalleryProvider = ({ imageGalleryStateStore, ImageGalleryHeader: ImageGalleryHeaderDefault, ImageGalleryFooter: ImageGalleryFooterDefault, - ImageGalleryVideoControls: ImageGalleryVideoControlDefault, ImageGalleryGrid: ImageGalleryGridDefault, ...value, }), diff --git a/package/src/contexts/imageGalleryContext/ImageGalleryContextBase.tsx b/package/src/contexts/imageGalleryContext/ImageGalleryContextBase.tsx index 2ca8473ac8..ead802338e 100644 --- a/package/src/contexts/imageGalleryContext/ImageGalleryContextBase.tsx +++ b/package/src/contexts/imageGalleryContext/ImageGalleryContextBase.tsx @@ -6,7 +6,6 @@ import type { ImageGalleryFooterProps, ImageGalleryGridProps, ImageGalleryHeaderProps, - ImageGalleryVideoControlProps, } from '../../components/ImageGallery/components/types'; import { ImageGalleryStateStore } from '../../state-store/image-gallery-state-store'; import { DEFAULT_BASE_CONTEXT_VALUE } from '../utils/defaultBaseContextValue'; @@ -19,7 +18,6 @@ export type ImageGalleryProviderProps = { numberOfImageGalleryGridColumns?: number; ImageGalleryHeader?: React.ComponentType; ImageGalleryFooter?: React.ComponentType; - ImageGalleryVideoControls?: React.ComponentType; ImageGalleryGrid?: React.ComponentType; }; diff --git a/package/src/contexts/threadsContext/ThreadsContext.tsx b/package/src/contexts/threadsContext/ThreadsContext.tsx index 7116027dcc..d23aeeb1f9 100644 --- a/package/src/contexts/threadsContext/ThreadsContext.tsx +++ b/package/src/contexts/threadsContext/ThreadsContext.tsx @@ -4,8 +4,6 @@ import { FlatListProps } from 'react-native'; import { Channel, Thread } from 'stream-chat'; -import type { ThreadListItemMessagePreviewProps } from '../../components/ThreadList/ThreadListItemMessagePreview'; -import type { ThreadMessagePreviewDeliveryStatusProps } from '../../components/ThreadList/ThreadMessagePreviewDeliveryStatus'; import { ThreadType } from '../threadContext/ThreadContext'; import { DEFAULT_BASE_CONTEXT_VALUE } from '../utils/defaultBaseContextValue'; @@ -19,13 +17,6 @@ export type ThreadsContextValue = { additionalFlatListProps?: Partial>; loadMore?: () => Promise; onThreadSelect?: (thread: ThreadType, channel: Channel) => void; - ThreadListEmptyPlaceholder?: React.ComponentType; - ThreadListItem?: React.ComponentType; - ThreadListItemMessagePreview?: React.ComponentType; - ThreadListLoadingIndicator?: React.ComponentType; - ThreadListLoadingMoreIndicator?: React.ComponentType; - ThreadListUnreadBanner?: React.ComponentType; - ThreadMessagePreviewDeliveryStatus?: React.ComponentType; }; export const ThreadsContext = React.createContext( From 034ef790eca25b9c9dfda29dc795a2b822fdc317 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Wed, 8 Apr 2026 23:55:32 +0200 Subject: [PATCH 04/16] refactor: update SampleApp to use WithComponents for component overrides Migrate all component override props in the SampleApp to use the new WithComponents context provider pattern: - ChannelListScreen: HeaderNetworkDownIndicator, Preview - ChannelScreen: AttachmentPickerSelectionBar, AttachmentPickerContent, MessageLocation, NetworkDownIndicator - ThreadScreen: AttachmentPickerSelectionBar, MessageLocation - NewDirectMessagingScreen: EmptyStateIndicator, SendButton - SharedGroupsScreen: Preview, replace List prop with EmptyStateIndicator override --- .../src/screens/ChannelListScreen.tsx | 31 ++++++----- .../SampleApp/src/screens/ChannelScreen.tsx | 14 +++-- .../src/screens/NewDirectMessagingScreen.tsx | 54 ++++++++++--------- .../src/screens/SharedGroupsScreen.tsx | 47 ++++++++-------- .../SampleApp/src/screens/ThreadScreen.tsx | 10 +++- 5 files changed, 92 insertions(+), 64 deletions(-) diff --git a/examples/SampleApp/src/screens/ChannelListScreen.tsx b/examples/SampleApp/src/screens/ChannelListScreen.tsx index 5b2579dc7f..6ee8a1554b 100644 --- a/examples/SampleApp/src/screens/ChannelListScreen.tsx +++ b/examples/SampleApp/src/screens/ChannelListScreen.tsx @@ -9,7 +9,7 @@ import { View, } from 'react-native'; import { useNavigation, useScrollToTop } from '@react-navigation/native'; -import { ChannelList, useTheme, useStableCallback, ChannelActionItem } from 'stream-chat-react-native'; +import { ChannelList, useTheme, useStableCallback, ChannelActionItem, WithComponents } from 'stream-chat-react-native'; import { Channel } from 'stream-chat'; import { ChannelPreview } from '../components/ChannelPreview'; import { ChatScreenHeader } from '../components/ChatScreenHeader'; @@ -245,18 +245,23 @@ export const ChannelListScreen: React.FC = () => { )} - + + + diff --git a/examples/SampleApp/src/screens/ChannelScreen.tsx b/examples/SampleApp/src/screens/ChannelScreen.tsx index b9d3fe52b4..bf79588d72 100644 --- a/examples/SampleApp/src/screens/ChannelScreen.tsx +++ b/examples/SampleApp/src/screens/ChannelScreen.tsx @@ -17,6 +17,7 @@ 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'; @@ -265,19 +266,23 @@ export const ChannelScreen: React.FC = ({ navigation, route return ( + null, + }} + > null} onAlsoSentToChannelHeaderPress={onAlsoSentToChannelHeaderPress} thread={selectedThread} maximumMessageLimit={messageListPruning} @@ -306,6 +311,7 @@ export const ChannelScreen: React.FC = ({ navigation, route /> )} + ); }; diff --git a/examples/SampleApp/src/screens/NewDirectMessagingScreen.tsx b/examples/SampleApp/src/screens/NewDirectMessagingScreen.tsx index 5bad31912f..124f8c7350 100644 --- a/examples/SampleApp/src/screens/NewDirectMessagingScreen.tsx +++ b/examples/SampleApp/src/screens/NewDirectMessagingScreen.tsx @@ -8,6 +8,7 @@ import { MessageList, UserAdd, useTheme, + WithComponents, } from 'stream-chat-react-native'; import { User } from '../icons/User'; @@ -338,32 +339,37 @@ export const NewDirectMessagingScreen: React.FC = }, ]} > - { - setFocusOnMessageInput(true); - setFocusOnSearchInput(false); - if (messageInputRef.current) { - messageInputRef.current.focus(); - } - }, + (messageInputRef.current = ref)} > - {renderUserSearch({ inSafeArea: true })} - {results && results.length >= 0 && !focusOnSearchInput && focusOnMessageInput && ( - - )} - - + { + 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 && ( + + )} + + + ); }; diff --git a/examples/SampleApp/src/screens/SharedGroupsScreen.tsx b/examples/SampleApp/src/screens/SharedGroupsScreen.tsx index 6bbf45f432..c1f0301ad8 100644 --- a/examples/SampleApp/src/screens/SharedGroupsScreen.tsx +++ b/examples/SampleApp/src/screens/SharedGroupsScreen.tsx @@ -3,8 +3,6 @@ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { NavigationProp, RouteProp, useNavigation } from '@react-navigation/native'; import { ChannelList, - ChannelListView, - ChannelListViewProps, ChannelPreviewViewProps, getChannelPreviewDisplayAvatar, GroupAvatar, @@ -13,6 +11,7 @@ import { useTheme, Avatar, getInitialsFromName, + WithComponents, } from 'stream-chat-react-native'; import { ScreenHeader } from '../components/ScreenHeader'; @@ -145,18 +144,19 @@ const EmptyListComponent = () => { ); }; -type ListComponentProps = ChannelListViewProps; - -// If the length of channels is 1, which means we only got 1:1-distinct channel, -// And we don't want to show 1:1-distinct channel in this list. -const ListComponent: React.FC = (props) => { +// Custom empty state that also shows when there's only the 1:1 direct channel +const SharedGroupsEmptyState = () => { const { channels, loadingChannels, refreshing } = useChannelsContext(); - if (channels && channels.length <= 1 && !loadingChannels && !refreshing) { + if (loadingChannels || refreshing) { + return null; + } + + if (!channels || channels.length <= 1) { return ; } - return ; + return null; }; type SharedGroupsScreenRouteProp = RouteProp; @@ -179,19 +179,24 @@ export const SharedGroupsScreen: React.FC = ({ return ( - + > + + ); }; diff --git a/examples/SampleApp/src/screens/ThreadScreen.tsx b/examples/SampleApp/src/screens/ThreadScreen.tsx index 14ba9da6a2..72d8f3441f 100644 --- a/examples/SampleApp/src/screens/ThreadScreen.tsx +++ b/examples/SampleApp/src/screens/ThreadScreen.tsx @@ -12,6 +12,7 @@ import { useTranslationContext, useTypingString, PortalWhileClosingView, + WithComponents, } from 'stream-chat-react-native'; import { useStateStore } from 'stream-chat-react-native'; @@ -148,15 +149,19 @@ export const ThreadScreen: React.FC = ({ return ( + = ({ shouldUseFlashList={messageListImplementation === 'flashlist'} /> + ); }; From bad66b16b42d3d0c5f94bc85151c278d666b97b9 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Thu, 9 Apr 2026 00:03:11 +0200 Subject: [PATCH 05/16] fix: re-add componentsContext export to contexts barrel --- package/src/contexts/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/package/src/contexts/index.ts b/package/src/contexts/index.ts index 5922a2b669..7c50dd7997 100644 --- a/package/src/contexts/index.ts +++ b/package/src/contexts/index.ts @@ -1,4 +1,5 @@ export * from './attachmentPickerContext/AttachmentPickerContext'; +export * from './componentsContext/ComponentsContext'; export * from './bottomSheetContext/BottomSheetContext'; export * from './channelContext/ChannelContext'; export * from './channelsContext/ChannelsContext'; From b90ea46c6ed8f8268fa8b8ff5283e686a774c54f Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Thu, 9 Apr 2026 00:05:15 +0200 Subject: [PATCH 06/16] refactor: update ExpoMessaging to use WithComponents - Channel screen: MessageLocation and InputButtons moved to WithComponents - Fix InputButtons type to use ComponentOverrides instead of Channel props --- .../ExpoMessaging/app/channel/[cid]/index.tsx | 34 ++++++++++--------- .../ExpoMessaging/components/InputButtons.tsx | 5 +-- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/examples/ExpoMessaging/app/channel/[cid]/index.tsx b/examples/ExpoMessaging/app/channel/[cid]/index.tsx index e3e5a8de94..7faff3cfe9 100644 --- a/examples/ExpoMessaging/app/channel/[cid]/index.tsx +++ b/examples/ExpoMessaging/app/channel/[cid]/index.tsx @@ -6,6 +6,7 @@ import { useChatContext, ThreadContextValue, MessageList, + WithComponents, } from 'stream-chat-expo'; import { Stack, useLocalSearchParams, useRouter } from 'expo-router'; import { AuthProgressLoader } from '../../../components/AuthProgressLoader'; @@ -70,22 +71,23 @@ 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/InputButtons.tsx b/examples/ExpoMessaging/components/InputButtons.tsx index 929b10acef..dcb0de4227 100644 --- a/examples/ExpoMessaging/components/InputButtons.tsx +++ b/examples/ExpoMessaging/components/InputButtons.tsx @@ -1,10 +1,11 @@ import React, { useState } from 'react'; import { Pressable, StyleSheet } from 'react-native'; -import { Channel, InputButtons as DefaultInputButtons } from 'stream-chat-expo'; +import type { ComponentOverrides } from 'stream-chat-expo'; +import { InputButtons as DefaultInputButtons } from 'stream-chat-expo'; import { ShareLocationIcon } from '../icons/ShareLocationIcon'; import { LiveLocationCreateModal } from './LocationSharing/CreateLocationModal'; -const InputButtons: NonNullable['InputButtons']> = (props) => { +const InputButtons: NonNullable = (props) => { const [modalVisible, setModalVisible] = useState(false); const onRequestClose = () => { From efac0d8b2ed2cb84fdfd0e9bcc9f2b8cc8700b60 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Thu, 9 Apr 2026 00:27:54 +0200 Subject: [PATCH 07/16] refactor: simplify ComponentsContext by deriving type from defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace 300-line hand-written ComponentOverrides type with derived type: `Partial<(typeof import('./defaultComponents'))['DEFAULT_COMPONENTS']>` - Remove ~110 type imports from ComponentsContext.tsx (now ~55 lines total) - Add optional component entries to defaultComponents.ts for components with no default (MessageText, PollContent, Input, etc.) - Remove remaining component override props from FileAttachment, StickyHeader, MessageBubble, MessageMenu - Adding a new overridable component now only requires editing defaultComponents.ts — the type is auto-derived --- .../components/Attachment/FileAttachment.tsx | 13 +- .../Message/MessageItemView/MessageBubble.tsx | 17 +- .../MessageItemView/MessageItemView.tsx | 2 - .../MessageList/MessageFlashList.tsx | 3 +- .../components/MessageList/MessageList.tsx | 3 +- .../components/MessageList/StickyHeader.tsx | 7 +- .../components/MessageMenu/MessageMenu.tsx | 82 ++--- .../componentsContext/ComponentsContext.tsx | 303 +----------------- .../__tests__/defaultComponents.test.ts | 40 ++- .../componentsContext/defaultComponents.ts | 37 ++- 10 files changed, 126 insertions(+), 381 deletions(-) diff --git a/package/src/components/Attachment/FileAttachment.tsx b/package/src/components/Attachment/FileAttachment.tsx index 8a2f1f0edf..e7b3def311 100644 --- a/package/src/components/Attachment/FileAttachment.tsx +++ b/package/src/components/Attachment/FileAttachment.tsx @@ -7,10 +7,7 @@ import { openUrlSafely } from './utils/openUrlSafely'; import { FileIconProps } from '../../components/Attachment/FileIcon'; -import { - ComponentOverrides, - useComponentsContext, -} from '../../contexts/componentsContext/ComponentsContext'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { MessageContextValue, useMessageContext, @@ -25,7 +22,6 @@ export type FileAttachmentPropsWithContext = Pick< MessageContextValue, 'onLongPress' | 'onPress' | 'onPressIn' | 'preventPress' > & - Pick, 'FilePreview'> & Pick & { /** The attachment to render */ attachment: Attachment; @@ -46,13 +42,13 @@ const FileAttachmentWithContext = (props: FileAttachmentPropsWithContext) => { additionalPressableProps, attachment, attachmentIconSize, - FilePreview, onLongPress, onPress, onPressIn, preventPress, styles: stylesProp = styles, } = props; + const { FilePreview } = useComponentsContext(); const defaultOnPress = () => openUrlSafely(attachment.asset_url); @@ -104,18 +100,13 @@ export type FileAttachmentProps = Partial; export const FileAttachment = (props: FileAttachmentProps) => { - const { FilePreview: PropFilePreview } = props; const { onLongPress, onPress, onPressIn, preventPress } = useMessageContext(); - const { FilePreview: ComponentsFilePreview } = useComponentsContext(); const { additionalPressableProps } = useMessagesContext(); - const FilePreview = PropFilePreview || ComponentsFilePreview; - return ( , 'MessageSwipeContent'> & - Pick & { - children: ReactNode; - onSwipe: () => void; - }; +type SwipableMessageWrapperProps = Pick< + MessageItemViewPropsWithContext, + 'messageSwipeToReplyHitSlop' +> & { + children: ReactNode; + onSwipe: () => void; +}; export const SwipableMessageWrapper = React.memo((props: SwipableMessageWrapperProps) => { - const { MessageSwipeContent, children, messageSwipeToReplyHitSlop, onSwipe } = props; + const { children, messageSwipeToReplyHitSlop, onSwipe } = props; + const { MessageSwipeContent } = useComponentsContext(); const isRTL = I18nManager.isRTL; const swipeDirectionMultiplier = isRTL ? -1 : 1; diff --git a/package/src/components/Message/MessageItemView/MessageItemView.tsx b/package/src/components/Message/MessageItemView/MessageItemView.tsx index 5962509b6f..8fcc45457e 100644 --- a/package/src/components/Message/MessageItemView/MessageItemView.tsx +++ b/package/src/components/Message/MessageItemView/MessageItemView.tsx @@ -248,7 +248,6 @@ const MessageItemViewWithContext = (props: MessageItemViewPropsWithContext) => { MessageHeader, MessageReplies, MessageSpacer, - MessageSwipeContent, ReactionListBottom, ReactionListTop, } = useComponentsContext(); @@ -366,7 +365,6 @@ const MessageItemViewWithContext = (props: MessageItemViewPropsWithContext) => { return enableSwipeToReply && !isMessageTypeDeleted ? ( diff --git a/package/src/components/MessageList/MessageFlashList.tsx b/package/src/components/MessageList/MessageFlashList.tsx index 8b7774f0e0..2f57d362b3 100644 --- a/package/src/components/MessageList/MessageFlashList.tsx +++ b/package/src/components/MessageList/MessageFlashList.tsx @@ -294,7 +294,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => threadList = false, } = props; const { - DateHeader, EmptyStateIndicator, LoadingIndicator, NetworkDownIndicator, @@ -1078,7 +1077,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => )} {messageListLengthAfterUpdate && StickyHeader ? ( - + ) : null} { threadHasMore, } = props; const { - DateHeader, EmptyStateIndicator, LoadingIndicator, NetworkDownIndicator, @@ -1295,7 +1294,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { )} {messageListLengthAfterUpdate && StickyHeader ? ( - + ) : null} {scrollToBottomButtonVisible ? ( diff --git a/package/src/components/MessageList/StickyHeader.tsx b/package/src/components/MessageList/StickyHeader.tsx index 4120244952..746f95e4a0 100644 --- a/package/src/components/MessageList/StickyHeader.tsx +++ b/package/src/components/MessageList/StickyHeader.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; -import type { ComponentOverrides } from '../../contexts/componentsContext/ComponentsContext'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { useTranslationContext } from '../../contexts/translationContext/TranslationContext'; import { getDateString } from '../../utils/i18n/getDateString'; @@ -8,7 +8,7 @@ import { getDateString } from '../../utils/i18n/getDateString'; /** * Props for the StickyHeader component. */ -export type StickyHeaderProps = Pick, 'DateHeader'> & { +export type StickyHeaderProps = { /** * Date to be displayed in the sticky header. */ @@ -19,7 +19,8 @@ export type StickyHeaderProps = Pick, 'DateHeader'> dateString?: string | number; }; -export const StickyHeader = ({ date, DateHeader, dateString }: StickyHeaderProps) => { +export const StickyHeader = ({ date, dateString }: StickyHeaderProps) => { + const { DateHeader } = useComponentsContext(); const { t, tDateTimeParser } = useTranslationContext(); const stickyHeaderDateString = useMemo(() => { diff --git a/package/src/components/MessageMenu/MessageMenu.tsx b/package/src/components/MessageMenu/MessageMenu.tsx index 63feffd713..bf0564e020 100644 --- a/package/src/components/MessageMenu/MessageMenu.tsx +++ b/package/src/components/MessageMenu/MessageMenu.tsx @@ -4,59 +4,47 @@ import { useWindowDimensions } from 'react-native'; import { MessageActionType } from './MessageActionListItem'; -import type { ComponentOverrides } from '../../contexts/componentsContext/ComponentsContext'; import { MessageContextValue } from '../../contexts/messageContext/MessageContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { BottomSheetModal } from '../UIComponents/BottomSheetModal'; export type MessageMenuProps = PropsWithChildren< - Partial< - Pick< - ComponentOverrides, - | 'MessageActionList' - | 'MessageActionListItem' - | 'MessageReactionPicker' - | 'MessageUserReactions' - | 'MessageUserReactionsAvatar' - | 'MessageUserReactionsItem' - > - > & - Partial> & { - /** - * Function to close the message actions bottom sheet - * @returns void - */ - dismissOverlay: () => void; - /** - * An array of message actions to render - */ - messageActions: MessageActionType[]; - /** - * Boolean to determine if there are message actions - */ - showMessageReactions: boolean; - /** - * Boolean to determine if the overlay is visible. - */ - visible: boolean; - /** - * Function to handle reaction on press - * @param reactionType - * @returns - */ - handleReaction?: (reactionType: string) => Promise; - /** - * The selected reaction - */ - selectedReaction?: string; + Partial> & { + /** + * Function to close the message actions bottom sheet + * @returns void + */ + dismissOverlay: () => void; + /** + * An array of message actions to render + */ + messageActions: MessageActionType[]; + /** + * Boolean to determine if there are message actions + */ + showMessageReactions: boolean; + /** + * Boolean to determine if the overlay is visible. + */ + visible: boolean; + /** + * Function to handle reaction on press + * @param reactionType + * @returns + */ + handleReaction?: (reactionType: string) => Promise; + /** + * The selected reaction + */ + selectedReaction?: string; - layout: { - x: number; - y: number; - w: number; - h: number; - }; - } + layout: { + x: number; + y: number; + w: number; + h: number; + }; + } >; // TODO: V9: Either remove this or refactor it so that it's useful again, as its logic diff --git a/package/src/contexts/componentsContext/ComponentsContext.tsx b/package/src/contexts/componentsContext/ComponentsContext.tsx index 109003c13b..93ab5c7872 100644 --- a/package/src/contexts/componentsContext/ComponentsContext.tsx +++ b/package/src/contexts/componentsContext/ComponentsContext.tsx @@ -1,300 +1,15 @@ import React, { PropsWithChildren, useContext, useMemo } from 'react'; -import type { View } from 'react-native'; - -import type { PollOption, UserResponse } from 'stream-chat'; - -import type { - AttachmentPickerContentProps, - InlineUnreadIndicatorProps, - PollContentProps, - StreamingMessageViewProps, -} from '../../components'; -import type { AttachmentProps } from '../../components/Attachment/Attachment'; -import type { AudioAttachmentProps } from '../../components/Attachment/Audio'; -import type { FileAttachmentProps } from '../../components/Attachment/FileAttachment'; -import type { FileAttachmentGroupProps } from '../../components/Attachment/FileAttachmentGroup'; -import type { FileIconProps } from '../../components/Attachment/FileIcon'; -import type { FilePreviewProps } from '../../components/Attachment/FilePreview'; -import type { GalleryProps } from '../../components/Attachment/Gallery'; -import type { GiphyProps } from '../../components/Attachment/Giphy'; -import type { ImageLoadingFailedIndicatorProps } from '../../components/Attachment/ImageLoadingFailedIndicator'; -import type { UnsupportedAttachmentProps } from '../../components/Attachment/UnsupportedAttachment'; -import type { - URLPreviewCompactProps, - URLPreviewProps, -} from '../../components/Attachment/UrlPreview'; -import type { VideoThumbnailProps } from '../../components/Attachment/VideoThumbnail'; -import type { AutoCompleteSuggestionHeaderProps } from '../../components/AutoCompleteInput/AutoCompleteSuggestionHeader'; -import type { AutoCompleteSuggestionItemProps } from '../../components/AutoCompleteInput/AutoCompleteSuggestionItem'; -import type { AutoCompleteSuggestionListProps } from '../../components/AutoCompleteInput/AutoCompleteSuggestionList'; -import type { InputViewProps } from '../../components/AutoCompleteInput/InputView'; -import type { HeaderErrorProps } from '../../components/ChannelList/ChannelListHeaderErrorIndicator'; -import type { ChannelDetailsBottomSheetProps } from '../../components/ChannelPreview/ChannelDetailsBottomSheet'; -import type { ChannelDetailsHeaderProps } from '../../components/ChannelPreview/ChannelDetailsBottomSheet'; -import type { ChannelLastMessagePreviewProps } from '../../components/ChannelPreview/ChannelLastMessagePreview'; -import type { ChannelMessagePreviewDeliveryStatusProps } from '../../components/ChannelPreview/ChannelMessagePreviewDeliveryStatus'; -import type { ChannelPreviewMessageProps } from '../../components/ChannelPreview/ChannelPreviewMessage'; -import type { ChannelPreviewStatusProps } from '../../components/ChannelPreview/ChannelPreviewStatus'; -import type { ChannelPreviewTitleProps } from '../../components/ChannelPreview/ChannelPreviewTitle'; -import type { ChannelPreviewTypingIndicatorProps } from '../../components/ChannelPreview/ChannelPreviewTypingIndicator'; -import type { ChannelPreviewUnreadCountProps } from '../../components/ChannelPreview/ChannelPreviewUnreadCount'; -import type { ChannelPreviewViewProps } from '../../components/ChannelPreview/ChannelPreviewView'; -import type { ImageGalleryVideoControlProps } from '../../components/ImageGallery/components/types'; -import type { EmptyStateProps } from '../../components/Indicators/EmptyStateIndicator'; -import type { LoadingErrorProps } from '../../components/Indicators/LoadingErrorIndicator'; -import type { LoadingProps } from '../../components/Indicators/LoadingIndicator'; -import type { KeyboardCompatibleViewProps } from '../../components/KeyboardCompatibleView/KeyboardControllerAvoidingView'; -import type { MessageProps } from '../../components/Message/Message'; -import type { MessagePinnedHeaderProps } from '../../components/Message/MessageItemView/Headers/MessagePinnedHeader'; -import type { MessageReminderHeaderProps } from '../../components/Message/MessageItemView/Headers/MessageReminderHeader'; -import type { MessageSavedForLaterHeaderProps } from '../../components/Message/MessageItemView/Headers/MessageSavedForLaterHeader'; -import type { SentToChannelHeaderProps } from '../../components/Message/MessageItemView/Headers/SentToChannelHeader'; -import type { MessageAuthorProps } from '../../components/Message/MessageItemView/MessageAuthor'; -import type { MessageBlockedProps } from '../../components/Message/MessageItemView/MessageBlocked'; -import type { MessageBounceProps } from '../../components/Message/MessageItemView/MessageBounce'; -import type { MessageContentProps } from '../../components/Message/MessageItemView/MessageContent'; -import type { MessageDeletedProps } from '../../components/Message/MessageItemView/MessageDeleted'; -import type { MessageFooterProps } from '../../components/Message/MessageItemView/MessageFooter'; -import type { MessageHeaderProps } from '../../components/Message/MessageItemView/MessageHeader'; -import type { MessageItemViewProps } from '../../components/Message/MessageItemView/MessageItemView'; -import type { MessageRepliesProps } from '../../components/Message/MessageItemView/MessageReplies'; -import type { MessageRepliesAvatarsProps } from '../../components/Message/MessageItemView/MessageRepliesAvatars'; -import type { MessageStatusProps } from '../../components/Message/MessageItemView/MessageStatus'; -import type { MessageTextProps } from '../../components/Message/MessageItemView/MessageTextContainer'; -import type { MessageTimestampProps } from '../../components/Message/MessageItemView/MessageTimestamp'; -import type { ReactionListBottomProps } from '../../components/Message/MessageItemView/ReactionList/ReactionListBottom'; -import type { ReactionListClusteredProps } from '../../components/Message/MessageItemView/ReactionList/ReactionListClustered'; -import type { - ReactionListCountItemProps, - ReactionListItemProps, -} from '../../components/Message/MessageItemView/ReactionList/ReactionListItem'; -import type { ReactionListItemWrapperProps } from '../../components/Message/MessageItemView/ReactionList/ReactionListItemWrapper'; -import type { ReactionListTopProps } from '../../components/Message/MessageItemView/ReactionList/ReactionListTop'; -import type { AttachmentUploadPreviewListProps } from '../../components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList'; -import type { - FileUploadNotSupportedIndicatorProps, - FileUploadRetryIndicatorProps, - ImageUploadRetryIndicatorProps, -} from '../../components/MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator'; -import type { AudioAttachmentUploadPreviewProps } from '../../components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview'; -import type { FileAttachmentUploadPreviewProps } from '../../components/MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview'; -import type { ImageAttachmentUploadPreviewProps } from '../../components/MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview'; -import type { VideoAttachmentUploadPreviewProps } from '../../components/MessageInput/components/AttachmentPreview/VideoAttachmentUploadPreview'; -import type { AudioRecorderProps } from '../../components/MessageInput/components/AudioRecorder/AudioRecorder'; -import type { AudioRecordingButtonProps } from '../../components/MessageInput/components/AudioRecorder/AudioRecordingButton'; -import type { AudioRecordingInProgressProps } from '../../components/MessageInput/components/AudioRecorder/AudioRecordingInProgress'; -import type { AudioRecordingLockIndicatorProps } from '../../components/MessageInput/components/AudioRecorder/AudioRecordingLockIndicator'; -import type { AudioRecordingWaveformProps } from '../../components/MessageInput/components/AudioRecorder/AudioRecordingWaveform'; -import type { InputButtonsProps } from '../../components/MessageInput/components/InputButtons'; -import type { AttachButtonProps } from '../../components/MessageInput/components/InputButtons/AttachButton'; -import type { SendButtonProps } from '../../components/MessageInput/components/OutputButtons/SendButton'; -import type { MessageComposerProps } from '../../components/MessageInput/MessageComposer'; -import type { StopMessageStreamingButtonProps } from '../../components/MessageInput/StopMessageStreamingButton'; -import type { DateHeaderProps } from '../../components/MessageList/DateHeader'; -import type { InlineDateSeparatorProps } from '../../components/MessageList/InlineDateSeparator'; -import type { MessageListProps } from '../../components/MessageList/MessageList'; -import type { MessageSystemProps } from '../../components/MessageList/MessageSystem'; -import type { ScrollToBottomButtonProps } from '../../components/MessageList/ScrollToBottomButton'; -import type { StickyHeaderProps } from '../../components/MessageList/StickyHeader'; -import type { TypingIndicatorContainerProps } from '../../components/MessageList/TypingIndicatorContainer'; -import type { UnreadMessagesNotificationProps } from '../../components/MessageList/UnreadMessagesNotification'; -import type { MessageActionListProps } from '../../components/MessageMenu/MessageActionList'; -import type { MessageActionListItemProps } from '../../components/MessageMenu/MessageActionListItem'; -import type { MessageMenuProps } from '../../components/MessageMenu/MessageMenu'; -import type { MessageReactionPickerProps } from '../../components/MessageMenu/MessageReactionPicker'; -import type { MessageUserReactionsProps } from '../../components/MessageMenu/MessageUserReactions'; -import type { MessageUserReactionsAvatarProps } from '../../components/MessageMenu/MessageUserReactionsAvatar'; -import type { MessageUserReactionsItemProps } from '../../components/MessageMenu/MessageUserReactionsItem'; -import type { ReplyProps } from '../../components/Reply/Reply'; -import type { ThreadListItemMessagePreviewProps } from '../../components/ThreadList/ThreadListItemMessagePreview'; -import type { ThreadMessagePreviewDeliveryStatusProps } from '../../components/ThreadList/ThreadMessagePreviewDeliveryStatus'; -import type { ChannelAvatarProps } from '../../components/ui/Avatar/ChannelAvatar'; -import type { MessageLocationProps } from '../messagesContext/MessagesContext'; - /** * All overridable UI components in the SDK. + * Derived from the DEFAULT_COMPONENTS map in defaultComponents.ts. + * Adding a new default automatically makes it available as an override. + * * Every key is optional — only specify the components you want to override. */ -export type ComponentOverrides = { - // === MessagesContext components === - Attachment?: React.ComponentType; - AudioAttachment?: React.ComponentType; - DateHeader?: React.ComponentType; - UnsupportedAttachment?: React.ComponentType; - FilePreview?: React.ComponentType; - FileAttachment?: React.ComponentType; - FileAttachmentGroup?: React.ComponentType; - FileAttachmentIcon?: React.ComponentType; - Gallery?: React.ComponentType; - Giphy?: React.ComponentType; - ImageLoadingFailedIndicator?: React.ComponentType; - ImageLoadingIndicator?: React.ComponentType; - InlineDateSeparator?: React.ComponentType; - InlineUnreadIndicator?: React.ComponentType; - Message?: React.ComponentType; - MessageActionList?: React.ComponentType; - MessageActionListItem?: React.ComponentType; - MessageAuthor?: React.ComponentType; - MessageBlocked?: React.ComponentType; - MessageBounce?: React.ComponentType; - MessageContent?: React.ComponentType; - MessageContentTopView?: React.ComponentType; - MessageContentLeadingView?: React.ComponentType; - MessageContentTrailingView?: React.ComponentType; - MessageContentBottomView?: React.ComponentType; - MessageDeleted?: React.ComponentType; - MessageError?: React.ComponentType; - MessageFooter?: React.ComponentType; - MessageHeader?: React.ComponentType; - MessageList?: React.ComponentType; - MessageLocation?: React.ComponentType; - MessageMenu?: React.ComponentType; - MessagePinnedHeader?: React.ComponentType; - MessageReminderHeader?: React.ComponentType; - MessageSavedForLaterHeader?: React.ComponentType; - SentToChannelHeader?: React.ComponentType; - MessageReactionPicker?: React.ComponentType; - MessageReplies?: React.ComponentType; - MessageRepliesAvatars?: React.ComponentType; - MessageSpacer?: React.ComponentType; - MessageItemView?: React.ComponentType< - MessageItemViewProps & { ref?: React.RefObject } - >; - MessageStatus?: React.ComponentType; - MessageSwipeContent?: React.ComponentType; - MessageSystem?: React.ComponentType; - MessageText?: React.ComponentType; - MessageTimestamp?: React.ComponentType; - MessageUserReactions?: React.ComponentType; - MessageUserReactionsAvatar?: React.ComponentType; - MessageUserReactionsItem?: React.ComponentType; - Reply?: React.ComponentType; - ScrollToBottomButton?: React.ComponentType; - StreamingMessageView?: React.ComponentType; - TypingIndicator?: React.ComponentType; - TypingIndicatorContainer?: React.ComponentType; - UnreadMessagesNotification?: React.ComponentType; - UrlPreview?: React.ComponentType; - URLPreviewCompact?: React.ComponentType; - VideoThumbnail?: React.ComponentType; - PollContent?: React.ComponentType; - ReactionListBottom?: React.ComponentType; - ReactionListTop?: React.ComponentType; - ReactionListClustered?: React.ComponentType; - ReactionListItem?: React.ComponentType; - ReactionListItemWrapper?: React.ComponentType; - ReactionListCountItem?: React.ComponentType; - - // === MessageInputContext components === - AttachButton?: React.ComponentType; - AudioRecorder?: React.ComponentType; - AudioRecordingInProgress?: React.ComponentType; - AudioRecordingLockIndicator?: React.ComponentType; - AudioRecordingPreview?: React.ComponentType; - AudioRecordingWaveform?: React.ComponentType; - AutoCompleteSuggestionHeader?: React.ComponentType; - AutoCompleteSuggestionItem?: React.ComponentType; - AutoCompleteSuggestionList?: React.ComponentType; - AttachmentPickerSelectionBar?: React.ComponentType; - AttachmentUploadPreviewList?: React.ComponentType; - AudioAttachmentUploadPreview?: React.ComponentType; - ImageAttachmentUploadPreview?: React.ComponentType; - FileAttachmentUploadPreview?: React.ComponentType; - VideoAttachmentUploadPreview?: React.ComponentType; - FileUploadInProgressIndicator?: React.ComponentType; - FileUploadRetryIndicator?: React.ComponentType; - FileUploadNotSupportedIndicator?: React.ComponentType; - ImageUploadInProgressIndicator?: React.ComponentType; - ImageUploadRetryIndicator?: React.ComponentType; - ImageUploadNotSupportedIndicator?: React.ComponentType; - CooldownTimer?: React.ComponentType; - SendButton?: React.ComponentType; - ShowThreadMessageInChannelButton?: React.ComponentType<{ threadList?: boolean }>; - MessageComposerLeadingView?: React.ComponentType; - MessageComposerTrailingView?: React.ComponentType; - MessageInputHeaderView?: React.ComponentType; - MessageInputFooterView?: React.ComponentType; - MessageInputLeadingView?: React.ComponentType; - MessageInputTrailingView?: React.ComponentType; - StartAudioRecordingButton?: React.ComponentType; - StopMessageStreamingButton?: React.ComponentType | null; - Input?: React.ComponentType< - Omit & - InputButtonsProps & { - getUsers: () => UserResponse[]; - } - >; - InputView?: React.ComponentType; - InputButtons?: React.ComponentType; - SendMessageDisallowedIndicator?: React.ComponentType; - CreatePollContent?: React.ComponentType; - - // === ChannelContext components === - EmptyStateIndicator?: React.ComponentType; - LoadingIndicator?: React.ComponentType; - LoadingErrorIndicator?: React.ComponentType; - NetworkDownIndicator?: React.ComponentType; - StickyHeader?: React.ComponentType; - KeyboardCompatibleView?: React.ComponentType; - - // === AttachmentPickerContext components === - ImageOverlaySelectedComponent?: React.ComponentType<{ index: number }>; - AttachmentPickerIOSSelectMorePhotos?: React.ComponentType; - AttachmentPickerContent?: React.ComponentType; - - // === ChannelsContext components === - FooterLoadingIndicator?: React.ComponentType; - HeaderErrorIndicator?: React.ComponentType; - HeaderNetworkDownIndicator?: React.ComponentType; - Preview?: React.ComponentType; - PreviewAvatar?: React.ComponentType; - PreviewMessage?: React.ComponentType; - PreviewMessageDeliveryStatus?: React.ComponentType; - PreviewMutedStatus?: React.ComponentType; - PreviewStatus?: React.ComponentType; - PreviewTitle?: React.ComponentType; - PreviewUnreadCount?: React.ComponentType; - PreviewTypingIndicator?: React.ComponentType; - PreviewLastMessage?: React.ComponentType; - ChannelDetailsBottomSheet?: React.ComponentType; - Skeleton?: React.ComponentType; - ListHeaderComponent?: React.ComponentType; - - // === Chat components === - ChatLoadingIndicator?: React.ComponentType | null; - - // === Channel details === - ChannelDetailsHeader?: React.ComponentType; - - // === Thread components === - ThreadMessageComposer?: React.ComponentType; - ThreadListComponent?: React.ComponentType; - ThreadListEmptyPlaceholder?: React.ComponentType; - ThreadListItem?: React.ComponentType; - ThreadListItemMessagePreview?: React.ComponentType; - ThreadListLoadingIndicator?: React.ComponentType; - ThreadListLoadingMoreIndicator?: React.ComponentType; - ThreadListUnreadBanner?: React.ComponentType; - ThreadMessagePreviewDeliveryStatus?: React.ComponentType; - - // === Poll components === - PollButtons?: React.ComponentType; - PollHeader?: React.ComponentType; - PollAllOptionsContent?: React.ComponentType<{ additionalScrollViewProps?: object }>; - PollAnswersListContent?: React.ComponentType<{ additionalFlatListProps?: object }>; - PollResultsContent?: React.ComponentType<{ additionalScrollViewProps?: object }>; - PollOptionFullResultsContent?: React.ComponentType<{ - option: PollOption; - additionalFlatListProps?: object; - }>; - - // === ImageGallery components === - ImageGalleryVideoControls?: React.ComponentType; - - // === UIComponents === - ImageComponent?: React.ComponentType; -}; +export type ComponentOverrides = Partial< + (typeof import('./defaultComponents'))['DEFAULT_COMPONENTS'] +>; const ComponentsContext = React.createContext({}); @@ -317,11 +32,7 @@ export const WithComponents = ({ value, }: PropsWithChildren<{ value: ComponentOverrides }>) => { const parent = useContext(ComponentsContext); - const merged = useMemo( - () => ({ ...parent, ...value }), - - [parent, value], - ); + const merged = useMemo(() => ({ ...parent, ...value }), [parent, value]); return {children}; }; diff --git a/package/src/contexts/componentsContext/__tests__/defaultComponents.test.ts b/package/src/contexts/componentsContext/__tests__/defaultComponents.test.ts index 39aa7142fb..ddffc26ad9 100644 --- a/package/src/contexts/componentsContext/__tests__/defaultComponents.test.ts +++ b/package/src/contexts/componentsContext/__tests__/defaultComponents.test.ts @@ -1,17 +1,45 @@ import { DEFAULT_COMPONENTS } from '../defaultComponents'; +// Optional component keys that are intentionally undefined (no default implementation) +const OPTIONAL_KEYS = new Set([ + 'AttachmentPickerIOSSelectMorePhotos', + 'ChatLoadingIndicator', + 'CreatePollContent', + 'ImageComponent', + 'Input', + 'ListHeaderComponent', + 'MessageContentBottomView', + 'MessageContentLeadingView', + 'MessageContentTopView', + 'MessageContentTrailingView', + 'MessageLocation', + 'MessageSpacer', + 'MessageText', + 'PollContent', +]); + describe('DEFAULT_COMPONENTS', () => { - it('should have all values defined (no undefined)', () => { + it('should have all required values defined', () => { const entries = Object.entries(DEFAULT_COMPONENTS); expect(entries.length).toBeGreaterThan(50); - const undefinedEntries = entries.filter(([, v]) => v === undefined); - if (undefinedEntries.length > 0) { + const unexpectedUndefined = entries.filter( + ([key, value]) => value === undefined && !OPTIONAL_KEYS.has(key), + ); + if (unexpectedUndefined.length > 0) { console.log( - 'Undefined keys:', - undefinedEntries.map(([k]) => k), + 'Unexpectedly undefined keys:', + unexpectedUndefined.map(([k]) => k), ); } - expect(undefinedEntries).toEqual([]); + expect(unexpectedUndefined).toEqual([]); + }); + + it('optional keys should be explicitly listed', () => { + const entries = Object.entries(DEFAULT_COMPONENTS); + const actualUndefined = new Set( + entries.filter(([, v]) => v === undefined || v === null).map(([k]) => k), + ); + expect(actualUndefined).toEqual(OPTIONAL_KEYS); }); }); diff --git a/package/src/contexts/componentsContext/defaultComponents.ts b/package/src/contexts/componentsContext/defaultComponents.ts index 6de6a2943b..a687be7f5d 100644 --- a/package/src/contexts/componentsContext/defaultComponents.ts +++ b/package/src/contexts/componentsContext/defaultComponents.ts @@ -1,4 +1,4 @@ -import type { ComponentOverrides } from './ComponentsContext'; +import React from 'react'; import { Attachment } from '../../components/Attachment/Attachment'; import { AudioAttachment } from '../../components/Attachment/Audio'; @@ -260,9 +260,6 @@ export const DEFAULT_COMPONENTS = { VideoAttachmentUploadPreview, VideoThumbnail, - // Chat - ChatLoadingIndicator: null, - // Channel details ChannelDetailsHeader, @@ -287,4 +284,34 @@ export const DEFAULT_COMPONENTS = { // ImageGallery ImageGalleryVideoControls: ImageGalleryVideoControl, -} satisfies Partial; + + // Optional overrides (no defaults — undefined unless user provides via WithComponents) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + AttachmentPickerIOSSelectMorePhotos: undefined as React.ComponentType | undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ChatLoadingIndicator: undefined as React.ComponentType | null | undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + CreatePollContent: undefined as React.ComponentType | undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ImageComponent: 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 + ListHeaderComponent: undefined as React.ComponentType | undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + MessageContentBottomView: undefined as React.ComponentType | undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + MessageContentLeadingView: undefined as React.ComponentType | undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + MessageContentTopView: undefined as React.ComponentType | undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + MessageContentTrailingView: undefined as React.ComponentType | undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + MessageLocation: undefined as React.ComponentType | undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + MessageSpacer: undefined as React.ComponentType | undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + MessageText: undefined as React.ComponentType | undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + PollContent: undefined as React.ComponentType | undefined, +}; From 2a7aba9d3053113fe41d4981a52ce11fd6919bf4 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Thu, 9 Apr 2026 09:14:39 +0200 Subject: [PATCH 08/16] refactor: rename WithComponents prop from value to overrides Aligns with stream-chat-react's WithComponents API where the prop is named `overrides` instead of `value`. --- .../ExpoMessaging/app/channel/[cid]/index.tsx | 2 +- .../src/screens/ChannelListScreen.tsx | 2 +- .../SampleApp/src/screens/ChannelScreen.tsx | 2 +- .../src/screens/NewDirectMessagingScreen.tsx | 2 +- .../src/screens/SharedGroupsScreen.tsx | 2 +- .../SampleApp/src/screens/ThreadScreen.tsx | 2 +- .../offline-support/offline-feature.js | 2 +- .../ChannelList/__tests__/ChannelList.test.js | 62 +++++++++---------- .../ChannelDetailsBottomSheet.test.tsx | 4 +- .../__tests__/ChannelPreview.test.tsx | 2 +- .../__tests__/ChannelSwipableWrapper.test.tsx | 4 +- .../__tests__/MessageContent.test.js | 4 +- .../__tests__/MessageItemView.test.js | 2 +- .../componentsContext/ComponentsContext.tsx | 8 +-- 14 files changed, 50 insertions(+), 50 deletions(-) diff --git a/examples/ExpoMessaging/app/channel/[cid]/index.tsx b/examples/ExpoMessaging/app/channel/[cid]/index.tsx index 7faff3cfe9..c52c8ef231 100644 --- a/examples/ExpoMessaging/app/channel/[cid]/index.tsx +++ b/examples/ExpoMessaging/app/channel/[cid]/index.tsx @@ -71,7 +71,7 @@ export default function ChannelScreen() { - + { = ({ navigation, route return ( = ]} > = ({ = ({ return ( { const renderComponent = () => render( - + , diff --git a/package/src/components/ChannelList/__tests__/ChannelList.test.js b/package/src/components/ChannelList/__tests__/ChannelList.test.js index c083a25612..9a2c7f4c7d 100644 --- a/package/src/components/ChannelList/__tests__/ChannelList.test.js +++ b/package/src/components/ChannelList/__tests__/ChannelList.test.js @@ -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( - + , @@ -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/__tests__/ChannelDetailsBottomSheet.test.tsx b/package/src/components/ChannelPreview/__tests__/ChannelDetailsBottomSheet.test.tsx index 7e3fbe896d..61efd3d17d 100644 --- a/package/src/components/ChannelPreview/__tests__/ChannelDetailsBottomSheet.test.tsx +++ b/package/src/components/ChannelPreview/__tests__/ChannelDetailsBottomSheet.test.tsx @@ -46,7 +46,7 @@ describe('ChannelDetailsBottomSheet', () => { const { getByTestId } = render( - + , @@ -62,7 +62,7 @@ describe('ChannelDetailsBottomSheet', () => { render( - null }}> + null }}> { return ( - + diff --git a/package/src/components/ChannelPreview/__tests__/ChannelSwipableWrapper.test.tsx b/package/src/components/ChannelPreview/__tests__/ChannelSwipableWrapper.test.tsx index c8e27295d7..917336727d 100644 --- a/package/src/components/ChannelPreview/__tests__/ChannelSwipableWrapper.test.tsx +++ b/package/src/components/ChannelPreview/__tests__/ChannelSwipableWrapper.test.tsx @@ -116,7 +116,7 @@ describe('ChannelSwipableWrapper', () => { render( - + child @@ -186,7 +186,7 @@ describe('ChannelSwipableWrapper', () => { render( - + child diff --git a/package/src/components/Message/MessageItemView/__tests__/MessageContent.test.js b/package/src/components/Message/MessageItemView/__tests__/MessageContent.test.js index e040b7a278..398dde457e 100644 --- a/package/src/components/Message/MessageItemView/__tests__/MessageContent.test.js +++ b/package/src/components/Message/MessageItemView/__tests__/MessageContent.test.js @@ -113,7 +113,7 @@ describe('MessageContent', () => { render( - + @@ -137,7 +137,7 @@ describe('MessageContent', () => { render( - + diff --git a/package/src/components/Message/MessageItemView/__tests__/MessageItemView.test.js b/package/src/components/Message/MessageItemView/__tests__/MessageItemView.test.js index c221b4bdf4..de82d16c30 100644 --- a/package/src/components/Message/MessageItemView/__tests__/MessageItemView.test.js +++ b/package/src/components/Message/MessageItemView/__tests__/MessageItemView.test.js @@ -44,7 +44,7 @@ describe('MessageItemView', () => { {componentOverrides ? ( - + diff --git a/package/src/contexts/componentsContext/ComponentsContext.tsx b/package/src/contexts/componentsContext/ComponentsContext.tsx index 93ab5c7872..7a284a26ad 100644 --- a/package/src/contexts/componentsContext/ComponentsContext.tsx +++ b/package/src/contexts/componentsContext/ComponentsContext.tsx @@ -19,7 +19,7 @@ const ComponentsContext = React.createContext({}); * * @example * ```tsx - * + * * * * @@ -29,10 +29,10 @@ const ComponentsContext = React.createContext({}); */ export const WithComponents = ({ children, - value, -}: PropsWithChildren<{ value: ComponentOverrides }>) => { + overrides, +}: PropsWithChildren<{ overrides: ComponentOverrides }>) => { const parent = useContext(ComponentsContext); - const merged = useMemo(() => ({ ...parent, ...value }), [parent, value]); + const merged = useMemo(() => ({ ...parent, ...overrides }), [parent, overrides]); return {children}; }; From b77d63147e5afa788def45363da70364d813e209 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Thu, 9 Apr 2026 18:16:45 +0200 Subject: [PATCH 09/16] fix: split LoadingIndicator into ChannelList and MessageList variants The shared LoadingIndicator key caused ChannelListView to use the generic text-based indicator instead of the skeleton UI. Split into: - ChannelListLoadingIndicator (default: skeleton UI) - MessageListLoadingIndicator (default: text-based "Loading messages...") --- package/src/components/ChannelList/ChannelListView.tsx | 8 ++------ .../ChannelList/__tests__/ChannelListView.test.js | 4 ++-- package/src/components/MessageList/MessageFlashList.tsx | 2 +- package/src/components/MessageList/MessageList.tsx | 2 +- .../src/contexts/componentsContext/defaultComponents.ts | 4 +++- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/package/src/components/ChannelList/ChannelListView.tsx b/package/src/components/ChannelList/ChannelListView.tsx index 5b64048ee1..23814ae7a1 100644 --- a/package/src/components/ChannelList/ChannelListView.tsx +++ b/package/src/components/ChannelList/ChannelListView.tsx @@ -75,7 +75,7 @@ const ChannelListViewWithContext = (props: ChannelListViewPropsWithContext) => { FooterLoadingIndicator, ListHeaderComponent, LoadingErrorIndicator, - LoadingIndicator, + ChannelListLoadingIndicator: LoadingIndicator, } = useComponentsContext(); /** @@ -136,11 +136,7 @@ const ChannelListViewWithContext = (props: ChannelListViewPropsWithContext) => { extraData={forceUpdate} keyExtractor={keyExtractor} ListEmptyComponent={ - loading ? ( - - ) : ( - - ) + loading ? : } ListFooterComponent={loadingNextPage ? : undefined} ListHeaderComponent={ListHeaderComponent} diff --git a/package/src/components/ChannelList/__tests__/ChannelListView.test.js b/package/src/components/ChannelList/__tests__/ChannelListView.test.js index 69470fcdad..73b800cf23 100644 --- a/package/src/components/ChannelList/__tests__/ChannelListView.test.js +++ b/package/src/components/ChannelList/__tests__/ChannelListView.test.js @@ -114,11 +114,11 @@ describe('ChannelListView', () => { }); it('renders the `LoadingIndicator` when when channels have not yet loaded', async () => { - const { getByText } = render( + const { getByTestId } = render( , ); await waitFor(() => { - expect(getByText('Loading channels...')).toBeTruthy(); + expect(getByTestId('channel-list-loading-indicator')).toBeTruthy(); }); }); }); diff --git a/package/src/components/MessageList/MessageFlashList.tsx b/package/src/components/MessageList/MessageFlashList.tsx index 2f57d362b3..f3f7318933 100644 --- a/package/src/components/MessageList/MessageFlashList.tsx +++ b/package/src/components/MessageList/MessageFlashList.tsx @@ -295,7 +295,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => } = props; const { EmptyStateIndicator, - LoadingIndicator, + MessageListLoadingIndicator: LoadingIndicator, NetworkDownIndicator, ScrollToBottomButton, StickyHeader, diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx index be37d1e03f..b2696e3d97 100644 --- a/package/src/components/MessageList/MessageList.tsx +++ b/package/src/components/MessageList/MessageList.tsx @@ -349,7 +349,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { } = props; const { EmptyStateIndicator, - LoadingIndicator, + MessageListLoadingIndicator: LoadingIndicator, NetworkDownIndicator, ScrollToBottomButton, StickyHeader, diff --git a/package/src/contexts/componentsContext/defaultComponents.ts b/package/src/contexts/componentsContext/defaultComponents.ts index a687be7f5d..ea8d27cdf3 100644 --- a/package/src/contexts/componentsContext/defaultComponents.ts +++ b/package/src/contexts/componentsContext/defaultComponents.ts @@ -24,6 +24,7 @@ import { InputView } from '../../components/AutoCompleteInput/InputView'; import { ChannelListFooterLoadingIndicator } from '../../components/ChannelList/ChannelListFooterLoadingIndicator'; import { ChannelListHeaderErrorIndicator } from '../../components/ChannelList/ChannelListHeaderErrorIndicator'; import { ChannelListHeaderNetworkDownIndicator } from '../../components/ChannelList/ChannelListHeaderNetworkDownIndicator'; +import { ChannelListLoadingIndicator } from '../../components/ChannelList/ChannelListLoadingIndicator'; import { Skeleton } from '../../components/ChannelList/Skeleton'; import { ChannelDetailsBottomSheet } from '../../components/ChannelPreview/ChannelDetailsBottomSheet'; import { ChannelDetailsHeader } from '../../components/ChannelPreview/ChannelDetailsBottomSheet'; @@ -189,7 +190,8 @@ export const DEFAULT_COMPONENTS = { InputView, KeyboardCompatibleView, LoadingErrorIndicator, - LoadingIndicator, + ChannelListLoadingIndicator, + MessageListLoadingIndicator: LoadingIndicator, Message, MessageActionList, MessageActionListItem, From 31ef8c8b08c5d82c2926ef63fa233009bf00c212 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Fri, 10 Apr 2026 10:03:40 +0200 Subject: [PATCH 10/16] docs: update PLAN.md with current state and reference notes --- .../src/contexts/componentsContext/PLAN.md | 240 ++++++++---------- 1 file changed, 103 insertions(+), 137 deletions(-) diff --git a/package/src/contexts/componentsContext/PLAN.md b/package/src/contexts/componentsContext/PLAN.md index 2637bbcaea..fac8947ffe 100644 --- a/package/src/contexts/componentsContext/PLAN.md +++ b/package/src/contexts/componentsContext/PLAN.md @@ -1,180 +1,146 @@ -# Plan: `WithComponents` Context Provider - -## Context - -The SDK prop-drills 120+ component overrides through `` → `useCreate*Context` hooks → context values. Each component name is listed **4 times** (destructured from props → passed to hook → destructured in hook → listed in useMemo). Consumers then read components back out via `useMessagesContext()`, `useMessageInputContext()`, etc. - -**Goal**: Replace this entire pipeline with a single `ComponentsContext`. Component overrides are **removed** from all existing contexts. Consumers read components via `useComponentsContext()` instead. - -```tsx -// User API - - - - - - -``` +# WithComponents — Component Override System ## Design Principle **All components are read from `useComponentsContext()`. All other contexts only provide data + APIs — never components.** -No context besides `ComponentsContext` should carry component references. This is the single rule that drives every change in this plan. +## Current State (Completed) -## Architecture +### What was done -### Before +1. **Created `ComponentsContext`** — `WithComponents` provider, `useComponentsContext()` hook, `ComponentOverrides` type +2. **Created `defaultComponents.ts`** — centralized map of all ~130 default components +3. **Stripped component keys** from all existing context types: `MessagesContextValue`, `InputMessageInputContextValue`, `ChannelContextValue`, `ChannelsContextValue`, `AttachmentPickerContextValue`, `ThreadsContextValue`, `ImageGalleryContextValue` +4. **Simplified `useCreate*Context` hooks** — no longer receive or forward component params +5. **Simplified `Channel.tsx`** — removed ~90 component imports, prop defaults, forwarding lines +6. **Simplified `ChannelList.tsx`** — removed ~19 component props +7. **Updated ~80 consumer files** — switched from old context hooks to `useComponentsContext()` +8. **Removed component override props** from ALL individual components +9. **Updated all 3 example apps** (SampleApp, ExpoMessaging, TypeScriptMessaging) +10. **Updated ~45 documentation pages** across docs-content repo +11. **Merged with develop** and resolved conflicts -``` -Channel props (90+ component overrides with defaults) - → useCreateMessagesContext (receives 60+ component params, maps into useMemo) - → MessagesContext carries components + runtime data - → Consumer: const { Message } = useMessagesContext() -``` - -### After +### Architecture ``` -DEFAULT_COMPONENTS (static map) - → ComponentsContext (defaults; user overrides via WithComponents) - → Consumer: const { Message } = useComponentsContext() - -Channel props (runtime/config only) - → useCreateMessagesContext (runtime data only, no components) - → MessagesContext carries ONLY data + APIs - → Consumer: const { deleteMessage } = useMessagesContext() +User: + ↓ +ComponentsContext (merges parent + overrides, inner wins) + ↓ +useComponentsContext() → { ...DEFAULT_COMPONENTS, ...overrides } + ↓ +Consumer: const { Message } = useComponentsContext() ``` -## Scope - -### What changes - -1. **Existing context types** (`MessagesContextValue`, `InputMessageInputContextValue`, `ChannelContextValue`, `ChannelsContextValue`, `AttachmentPickerContextValue`) — remove all component-type keys -2. **`useCreate*Context` hooks** — remove all component params, stop mapping them -3. **Channel.tsx** — remove ~90 component imports, ~90 destructuring defaults, ~90 forwarding lines -4. **ChannelList.tsx** — remove ~19 component props and forwarding -5. **~117 consumer callsites across ~97 files** — switch from `useXContext()` to `useComponentsContext()` for component reads - -### What doesn't change +### Key Files -- Runtime data flow (callbacks like `deleteMessage`, `sendReaction`, state like `targetedMessage`) stays in existing contexts -- Consumer reads of runtime data (`const { deleteMessage } = useMessagesContext()`) are untouched -- `WithComponents` nesting semantics (inner wins, like standard React context) +| File | Purpose | +|------|---------| +| `ComponentsContext.tsx` | ~60 lines. `ComponentOverrides` type (derived from `typeof DEFAULT_COMPONENTS`), `WithComponents` provider, `useComponentsContext()` hook | +| `defaultComponents.ts` | ~300 lines. Single source of truth for all default component mappings. Adding a new component here auto-extends `ComponentOverrides` | -## New Files +### Type System -| File | Purpose | -| -------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -| `package/src/contexts/componentsContext/ComponentsContext.tsx` | `ComponentOverrides` type, `WithComponents` provider, `useComponentsContext()` hook | -| `package/src/contexts/componentsContext/defaultComponents.ts` | All default component imports → `DEFAULT_COMPONENTS` map | - -Both already drafted in the repo. - -## Implementation Steps - -### Step 1: Finalize `ComponentsContext.tsx` and `defaultComponents.ts` - -Already drafted. Key design: - -- `ComponentOverrides`: flat map, all keys optional, explicitly typed per component -- Context default = `DEFAULT_COMPONENTS` → `useComponentsContext()` always returns resolved values -- `WithComponents`: merges `{ ...parent, ...value }` (inner wins) -- `ResolvedComponents` = `Required` for the return type - -Special cases: - -- `FlatList` — from `NativeHandlers.FlatList` at runtime. Keep as a runtime prop in MessagesContext, not in ComponentsContext. -- `StopMessageStreamingButton` — can be `null` (explicitly hide). The type in ComponentOverrides allows `| null`. +`ComponentOverrides` is derived automatically: +```ts +export type ComponentOverrides = Partial< + (typeof import('./defaultComponents'))['DEFAULT_COMPONENTS'] +>; +``` -### Step 2: Strip component keys from existing context value types +No manual type maintenance — add a component to `DEFAULT_COMPONENTS` and the type updates. -**`MessagesContextValue`** (`package/src/contexts/messagesContext/MessagesContext.tsx`): -Remove ~60 component keys (Attachment, AudioAttachment, DateHeader, Message, MessageContent, Reply, etc.). Keep runtime keys only (deleteMessage, deleteReaction, dismissKeyboardOnMessageTouch, giphyVersion, messageContentOrder, etc.). +### Circular Dependency Handling -**`InputMessageInputContextValue`** (`package/src/contexts/messageInputContext/MessageInputContext.tsx`): -Remove ~35 component keys (AttachButton, AudioRecorder, SendButton, Input, InputView, etc.). Keep runtime keys only (asyncMessagesLockDistance, audioRecordingEnabled, editMessage, sendMessage, etc.). +`defaultComponents.ts` → imports components → components import `useComponentsContext` from `ComponentsContext.tsx`. -**`ChannelContextValue`** (`package/src/contexts/channelContext/ChannelContext.tsx`): -Remove 4 component keys (EmptyStateIndicator, LoadingIndicator, NetworkDownIndicator, StickyHeader). +Broken by lazy-loading defaults in the hook: +```ts +let cachedDefaults: ComponentOverrides | undefined; +const getDefaults = () => { + if (!cachedDefaults) { + cachedDefaults = require('./defaultComponents').DEFAULT_COMPONENTS; + } + return cachedDefaults; +}; +``` -**`ChannelsContextValue`** (`package/src/contexts/channelsContext/ChannelsContext.tsx`): -Remove ~19 component keys (Preview, PreviewAvatar, PreviewMessage, Skeleton, FooterLoadingIndicator, etc.). +### Naming Conventions -**`AttachmentPickerContextValue`** (`package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx`): -Remove 3 component keys (ImageOverlaySelectedComponent, AttachmentPickerSelectionBar, AttachmentPickerContent). +Some component keys differ from their default component names to avoid collisions: -### Step 3: Simplify `useCreate*Context` hooks +| 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 | -Each hook drops all component params and stops mapping them into useMemo: +### Optional Components (no default) -- **`useCreateMessagesContext`** (`package/src/components/Channel/hooks/useCreateMessagesContext.ts`): ~60 component params removed, keep ~30 runtime params -- **`useCreateInputMessageInputContext`** (`package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts`): ~35 component params removed, keep ~15 runtime params -- **`useCreateChannelContext`** (`package/src/components/Channel/hooks/useCreateChannelContext.ts`): 4 component params removed -- **`useCreateChannelsContext`** (`package/src/components/ChannelList/hooks/useCreateChannelsContext.ts`): ~19 component params removed, keep ~20 runtime params +These exist in `DEFAULT_COMPONENTS` as `undefined` with `React.ComponentType | undefined` type assertions: -### Step 4: Simplify Channel.tsx +`AttachmentPickerIOSSelectMorePhotos`, `ChatLoadingIndicator`, `CreatePollContent`, `ImageComponent`, `Input`, `ListHeaderComponent`, `MessageContentBottomView`, `MessageContentLeadingView`, `MessageContentTopView`, `MessageContentTrailingView`, `MessageLocation`, `MessageSpacer`, `MessageText`, `PollContent` -- Remove ~90 default component imports (lines 114-223) -- Remove component keys from `ChannelPropsWithContext` type -- Remove component destructuring defaults from `ChannelWithContext` -- Remove component values from `useCreateMessagesContext()`, `useCreateInputMessageInputContext()`, `useCreateChannelContext()` calls -- Remove component values from `attachmentPickerContext` useMemo -- `LoadingErrorIndicator` and `KeyboardCompatibleView` are used directly in Channel's JSX — read from `useComponentsContext()` or keep as Channel-specific props +### Shared Component Keys (audited) -### Step 5: Simplify ChannelList.tsx +Some keys were used in multiple contexts before the refactor. Audit results: -- Remove component keys from `ChannelListProps` type -- Remove default component imports -- Remove component values from `useCreateChannelsContext()` call +| Key | Used By | Same Default? | Resolution | +|-----|---------|---------------|------------| +| `EmptyStateIndicator` | Channel + ChannelList | Yes (differentiates via `listType` prop) | Single key ✅ | +| `LoadingErrorIndicator` | Channel + ChannelList | Yes (differentiates via `listType` prop) | Single key ✅ | +| `LoadingIndicator` | Channel + ChannelList | **No** — Channel used text-based, ChannelList used skeleton | Split into `MessageListLoadingIndicator` + `ChannelListLoadingIndicator` ✅ | -### Step 6: Update ~117 consumer callsites +### API Alignment with stream-chat-react -Switch component destructuring from old context hooks to `useComponentsContext()`: +| Aspect | React Native | React Web | +|--------|-------------|-----------| +| Provider | `WithComponents` | `WithComponents` | +| Prop name | `overrides` | `overrides` | +| Hook | `useComponentsContext()` | `useComponentContext()` | +| Type | `ComponentOverrides` (auto-derived) | `ComponentContextValue` (hand-written) | +| Defaults | Lazy-loaded via `require()` | Set at `Channel` level | +| Merge | `useMemo` | Plain spread (no memo) | -```tsx -// Before -const { Message, MessageStatus, MessageTimestamp } = useMessagesContext(); -const { deleteMessage } = useMessagesContext(); +## Known Issues / Future Work -// After -const { Message, MessageStatus, MessageTimestamp } = useComponentsContext(); -const { deleteMessage } = useMessagesContext(); -``` +### Pre-existing Test Failures (not caused by this work) -**Key files by volume** (largest consumers): +These test suites fail on `develop` too: +- `offline-support/index.test.ts` — timeout +- `ChannelList.test.js` — filter race condition (`channel.countUnread` mock missing) +- `isAttachmentEqualHandler.test.js`, `MessageContent.test.js`, `MessageTextContainer.test.tsx`, `MessageUserReactions.test.tsx`, `ChannelPreview.test.tsx` — various pre-existing issues -- `components/Message/MessageItemView/MessageItemView.tsx` — 15+ component keys -- `components/Message/MessageItemView/MessageContent.tsx` — 15+ component keys -- `components/MessageList/MessageList.tsx` — 10+ component keys -- `components/MessageList/MessageFlashList.tsx` — 10+ component keys -- `components/MessageInput/MessageComposer.tsx` — 25+ component keys -- `components/Attachment/Attachment.tsx` — 10+ component keys -- `components/ChannelList/ChannelListView.tsx` — multiple component keys -- `components/ChannelPreview/ChannelPreviewView.tsx` — multiple component keys +### Linter Interaction -Many other files destructure just 1-2 component keys from context — straightforward replacements. +`@typescript-eslint/no-unused-vars` (warn, max-warnings 0) aggressively strips unused type keys. When adding new keys to `ComponentOverrides`, the type and its consumer must land in the same edit — otherwise the linter removes the key between saves. -### Step 7: Update exports +Since `ComponentOverrides` is now auto-derived from `DEFAULT_COMPONENTS`, this is no longer an issue for the type itself. But be aware when adding optional components (`undefined as React.ComponentType | undefined`). -- `package/src/contexts/index.ts` — add `export * from './componentsContext/ComponentsContext'` -- `package/src/index.ts` — verify `WithComponents`, `ComponentOverrides`, `useComponentsContext` are exported +### `contexts/index.ts` Barrel Export -### Step 8: Update tests +The `export * from './componentsContext/ComponentsContext'` line in `contexts/index.ts` was stripped by the linter multiple times during development. If `WithComponents` becomes unexportable from the package, check this barrel file first. -Tests that pass component overrides as Channel/ChannelList props will need to wrap in `` instead. Mock builders that set up context values with component overrides may also need updating. +### Documentation -## Edge Cases +Docs PR: https://github.com/GetStream/docs-content/pull/1169 -- **Shared names**: `EmptyStateIndicator`, `LoadingIndicator` exist in both Channel and ChannelList. One key in flat map — users use nesting for different overrides per area. -- **Mixed destructuring**: Some consumers destructure both components and runtime data from the same `useMessagesContext()` call. These need to be split into two calls. -- **`FlatList`**: Runtime-resolved from NativeHandlers. Stays in MessagesContext as a runtime value, not in ComponentsContext. -- **`StopMessageStreamingButton`**: Supports `null` to hide. ComponentOverrides type allows `| null`. +Updated ~45 pages across: +- Core teaching pages (custom_components, message-customization, etc.) +- Component reference pages (channel-list, message-list, message-composer, etc.) +- Context docs (stripped component keys from 7 context pages) +- Migration guide (upgrading-from-v8.md — comprehensive WithComponents section) +- Advanced guides (audio, AI, image-picker, etc.) -## Verification +### SDK PR -1. `cd package && yarn build` — type-checks and builds -2. `cd package && yarn test:unit` — tests pass (after updating test fixtures) -3. `cd package && yarn lint` — no lint errors -4. Manual: `` → verify override appears -5. Verify nesting: inner `WithComponents` wins over outer +https://github.com/GetStream/stream-chat-react-native/pull/3542 From 617b3db980bb8c83cde1d654412e9dbc0394c10e Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Tue, 14 Apr 2026 16:24:09 +0200 Subject: [PATCH 11/16] refactor: migrate remaining component overrides to WithComponents - Fix WithComponents prop name (value -> overrides) in 6 test files - Move ImageComponent from ChatContext to ComponentsContext with Image default - Move ImageGalleryHeader/Footer/Grid from ImageGalleryContext to ComponentsContext - Move MessageOverlayBackground from OverlayContext to ComponentsContext - Update all consumer components to read from useComponentsContext() - Clean up stale context values in tests --- .../components/Attachment/GalleryImage.tsx | 9 ++-- .../Attachment/Giphy/GiphyImage.tsx | 8 ++-- .../Attachment/UrlPreview/URLPreview.tsx | 6 +-- .../UrlPreview/URLPreviewCompact.tsx | 6 +-- .../isAttachmentEqualHandler.test.js | 2 +- .../ChannelList/__tests__/ChannelList.test.js | 4 +- .../__tests__/ChannelPreview.test.tsx | 2 +- package/src/components/Chat/Chat.tsx | 6 +-- .../Chat/hooks/useCreateChatContext.ts | 2 - .../components/ImageGallery/ImageGallery.tsx | 16 ++++--- .../__tests__/ImageGallery.test.tsx | 6 --- .../__tests__/ImageGalleryFooter.test.tsx | 44 ++++++++++--------- .../__tests__/MessageContent.test.js | 8 ++-- .../__tests__/MessageTextContainer.test.tsx | 2 +- .../components/LinkPreviewList.tsx | 5 ++- .../__tests__/MessageUserReactions.test.tsx | 2 +- package/src/components/Reply/Reply.tsx | 8 ++-- package/src/components/ui/Avatar/Avatar.tsx | 4 +- .../src/contexts/chatContext/ChatContext.tsx | 5 --- .../__tests__/defaultComponents.test.ts | 1 - .../componentsContext/defaultComponents.ts | 16 ++++++- .../ImageGalleryContext.tsx | 6 --- .../ImageGalleryContextBase.tsx | 8 ---- .../MessageOverlayHostLayer.tsx | 12 +++-- .../overlayContext/OverlayContext.tsx | 4 -- .../overlayContext/OverlayProvider.tsx | 3 +- .../MessageOverlayHostLayer.test.tsx | 27 +++++++++--- 27 files changed, 110 insertions(+), 112 deletions(-) diff --git a/package/src/components/Attachment/GalleryImage.tsx b/package/src/components/Attachment/GalleryImage.tsx index fa174b0831..39b9442cd0 100644 --- a/package/src/components/Attachment/GalleryImage.tsx +++ b/package/src/components/Attachment/GalleryImage.tsx @@ -1,12 +1,13 @@ import React from 'react'; import { Image, ImageProps, StyleSheet } from 'react-native'; -import { ChatContextValue, useChatContext } from '../../contexts/chatContext/ChatContext'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { getUrlWithoutParams, isLocalUrl, makeImageCompatibleUrl } from '../../utils/utils'; -export type GalleryImageWithContextProps = GalleryImageProps & - Pick; +export type GalleryImageWithContextProps = GalleryImageProps & { + ImageComponent?: React.ComponentType; +}; export const GalleryImageWithContext = (props: GalleryImageWithContextProps) => { const { ImageComponent = Image, uri, style, ...rest } = props; @@ -48,7 +49,7 @@ export type GalleryImageProps = Omit & { }; export const GalleryImage = (props: GalleryImageProps) => { - const { ImageComponent } = useChatContext(); + const { ImageComponent } = useComponentsContext(); return ; }; diff --git a/package/src/components/Attachment/Giphy/GiphyImage.tsx b/package/src/components/Attachment/Giphy/GiphyImage.tsx index 80fc8d5e5c..00189404a8 100644 --- a/package/src/components/Attachment/Giphy/GiphyImage.tsx +++ b/package/src/components/Attachment/Giphy/GiphyImage.tsx @@ -3,7 +3,6 @@ import { Image, StyleSheet, View } from 'react-native'; import type { Attachment } from 'stream-chat'; -import { ChatContextValue, useChatContext } from '../../../contexts/chatContext/ChatContext'; import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext'; import { @@ -16,8 +15,9 @@ import { useLoadingImage } from '../../../hooks/useLoadingImage'; import { makeImageCompatibleUrl } from '../../../utils/utils'; import { GiphyBadge } from '../../ui/Badge/GiphyBadge'; -export type GiphyImagePropsWithContext = Pick & - Pick & { +export type GiphyImagePropsWithContext = Pick & { + ImageComponent?: React.ComponentType; + } & { attachment: Attachment; /** * Whether to render the preview image or the full image @@ -160,7 +160,7 @@ export type GiphyImageProps = Partial & { * UI component for card in attachments. */ export const GiphyImage = (props: GiphyImageProps) => { - const { ImageComponent } = useChatContext(); + const { ImageComponent } = useComponentsContext(); const { giphyVersion } = useMessagesContext(); return ( diff --git a/package/src/components/Attachment/UrlPreview/URLPreview.tsx b/package/src/components/Attachment/UrlPreview/URLPreview.tsx index fc7fd729be..a2ef7870ba 100644 --- a/package/src/components/Attachment/UrlPreview/URLPreview.tsx +++ b/package/src/components/Attachment/UrlPreview/URLPreview.tsx @@ -13,7 +13,7 @@ import { import type { Attachment } from 'stream-chat'; -import { ChatContextValue, useChatContext } from '../../../contexts/chatContext/ChatContext'; +import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext'; import { MessageContextValue, @@ -32,7 +32,7 @@ import { VideoPlayIndicator } from '../../ui'; import { ImageBackground } from '../../UIComponents/ImageBackground'; import { openUrlSafely } from '../utils/openUrlSafely'; -export type URLPreviewPropsWithContext = Pick & +export type URLPreviewPropsWithContext = { ImageComponent?: React.ComponentType } & Pick & Pick< MessagesContextValue, @@ -210,7 +210,7 @@ export type URLPreviewProps = Partial & { * UI component for card in attachments. */ export const URLPreview = (props: URLPreviewProps) => { - const { ImageComponent } = useChatContext(); + const { ImageComponent } = useComponentsContext(); const { message, onLongPress, onPress, onPressIn, preventPress } = useMessageContext(); const { additionalPressableProps, isAttachmentEqual, myMessageTheme } = useMessagesContext(); diff --git a/package/src/components/Attachment/UrlPreview/URLPreviewCompact.tsx b/package/src/components/Attachment/UrlPreview/URLPreviewCompact.tsx index ed84992704..60025afc30 100644 --- a/package/src/components/Attachment/UrlPreview/URLPreviewCompact.tsx +++ b/package/src/components/Attachment/UrlPreview/URLPreviewCompact.tsx @@ -13,7 +13,7 @@ import { import type { Attachment } from 'stream-chat'; -import { ChatContextValue, useChatContext } from '../../../contexts/chatContext/ChatContext'; +import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext'; import { MessageContextValue, @@ -32,7 +32,7 @@ import { VideoPlayIndicator } from '../../ui'; import { ImageBackground } from '../../UIComponents/ImageBackground'; import { openUrlSafely } from '../utils/openUrlSafely'; -export type URLPreviewCompactPropsWithContext = Pick & +export type URLPreviewCompactPropsWithContext = { ImageComponent?: React.ComponentType } & Pick & Pick & { attachment: Attachment; @@ -208,7 +208,7 @@ export type URLPreviewCompactProps = Partial * UI component for card in attachments. */ export const URLPreviewCompact = (props: URLPreviewCompactProps) => { - const { ImageComponent } = useChatContext(); + const { ImageComponent } = useComponentsContext(); const { message, onLongPress, onPress, onPressIn, preventPress } = useMessageContext(); const { additionalPressableProps, myMessageTheme } = useMessagesContext(); diff --git a/package/src/components/Channel/__tests__/isAttachmentEqualHandler.test.js b/package/src/components/Channel/__tests__/isAttachmentEqualHandler.test.js index b59241163e..7c02654712 100644 --- a/package/src/components/Channel/__tests__/isAttachmentEqualHandler.test.js +++ b/package/src/components/Channel/__tests__/isAttachmentEqualHandler.test.js @@ -63,7 +63,7 @@ describe('isAttachmentEqualHandler', () => { { if (type === 'test') { return {customField}; diff --git a/package/src/components/ChannelList/__tests__/ChannelList.test.js b/package/src/components/ChannelList/__tests__/ChannelList.test.js index 9a2c7f4c7d..5700d93027 100644 --- a/package/src/components/ChannelList/__tests__/ChannelList.test.js +++ b/package/src/components/ChannelList/__tests__/ChannelList.test.js @@ -342,7 +342,7 @@ describe('ChannelList', () => { const { getByTestId } = render( { const { getByTestId } = render( { return ( & - Partial> & { + Partial> & { /** * When false, ws connection won't be disconnection upon backgrounding the app. * To receive push notifications, its necessary that user doesn't have active @@ -139,7 +139,6 @@ const ChatWithContext = (props: PropsWithChildren) => { closeConnectionOnBackground = true, enableOfflineSupport = false, i18nInstance, - ImageComponent = Image, isMessageAIGenerated, style, } = props; @@ -252,7 +251,6 @@ const ChatWithContext = (props: PropsWithChildren) => { client, connectionRecovering, enableOfflineSupport, - ImageComponent, isMessageAIGenerated, isOnline, mutedUsers, diff --git a/package/src/components/Chat/hooks/useCreateChatContext.ts b/package/src/components/Chat/hooks/useCreateChatContext.ts index 2992dbc961..1a74a10e58 100644 --- a/package/src/components/Chat/hooks/useCreateChatContext.ts +++ b/package/src/components/Chat/hooks/useCreateChatContext.ts @@ -8,7 +8,6 @@ export const useCreateChatContext = ({ client, connectionRecovering, enableOfflineSupport, - ImageComponent, isMessageAIGenerated, isOnline, mutedUsers, @@ -29,7 +28,6 @@ export const useCreateChatContext = ({ client, connectionRecovering, enableOfflineSupport, - ImageComponent, isMessageAIGenerated, isOnline, mutedUsers, diff --git a/package/src/components/ImageGallery/ImageGallery.tsx b/package/src/components/ImageGallery/ImageGallery.tsx index 1596dd30e9..2d66aa0ab9 100644 --- a/package/src/components/ImageGallery/ImageGallery.tsx +++ b/package/src/components/ImageGallery/ImageGallery.tsx @@ -20,6 +20,7 @@ import { ImageGalleryProviderProps, useImageGalleryContext, } from '../../contexts/imageGalleryContext/ImageGalleryContext'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { OverlayContextValue, useOverlayContext, @@ -63,12 +64,13 @@ const imageGallerySelector = (state: ImageGalleryState) => ({ type ImageGalleryWithContextProps = Pick< ImageGalleryProviderProps, - | 'numberOfImageGalleryGridColumns' - | 'ImageGalleryHeader' - | 'ImageGalleryFooter' - | 'ImageGalleryGrid' + 'numberOfImageGalleryGridColumns' > & - Pick; + Pick & { + ImageGalleryHeader?: React.ComponentType; + ImageGalleryFooter?: React.ComponentType; + ImageGalleryGrid?: React.ComponentType; + }; export const ImageGalleryWithContext = (props: ImageGalleryWithContextProps) => { const { @@ -367,12 +369,12 @@ export const ImageGalleryWithContext = (props: ImageGalleryWithContextProps) => export type ImageGalleryProps = Partial; export const ImageGallery = (props: ImageGalleryProps) => { + const { numberOfImageGalleryGridColumns } = useImageGalleryContext(); const { - numberOfImageGalleryGridColumns, ImageGalleryHeader, ImageGalleryFooter, ImageGalleryGrid, - } = useImageGalleryContext(); + } = useComponentsContext(); const { overlayOpacity } = useOverlayContext(); return ( diff --git a/package/src/components/ImageGallery/__tests__/ImageGalleryFooter.test.tsx b/package/src/components/ImageGallery/__tests__/ImageGalleryFooter.test.tsx index 5148cc6f27..a3f52012fd 100644 --- a/package/src/components/ImageGallery/__tests__/ImageGalleryFooter.test.tsx +++ b/package/src/components/ImageGallery/__tests__/ImageGalleryFooter.test.tsx @@ -6,7 +6,7 @@ import { render, screen, userEvent, waitFor } from '@testing-library/react-nativ import { Attachment, LocalMessage } from 'stream-chat'; -import { ImageGalleryFooter as ImageGalleryFooterDefault } from '../../../components/ImageGallery/components/ImageGalleryFooter'; +import { WithComponents } from '../../../contexts/componentsContext/ComponentsContext'; import { ImageGalleryContext, ImageGalleryContextValue, @@ -60,16 +60,17 @@ const ImageGalleryComponentVideo = (props: ImageGalleryProps) => { return ( }}> - - - + + + + + ); }; @@ -100,16 +101,17 @@ const ImageGalleryComponentImage = ( return ( }}> - - - + + + + + ); }; diff --git a/package/src/components/Message/MessageItemView/__tests__/MessageContent.test.js b/package/src/components/Message/MessageItemView/__tests__/MessageContent.test.js index 867851ca3c..5a2195d00a 100644 --- a/package/src/components/Message/MessageItemView/__tests__/MessageContent.test.js +++ b/package/src/components/Message/MessageItemView/__tests__/MessageContent.test.js @@ -164,7 +164,7 @@ describe('MessageContent', () => { , MessageContentTopView: () => , }} @@ -192,7 +192,7 @@ describe('MessageContent', () => { , MessageContentTrailingView: () => , }} @@ -221,7 +221,7 @@ describe('MessageContent', () => { , MessageContentTrailingView: () => , }} @@ -246,7 +246,7 @@ describe('MessageContent', () => { , MessageContentTrailingView: () => , }} diff --git a/package/src/components/Message/MessageItemView/__tests__/MessageTextContainer.test.tsx b/package/src/components/Message/MessageItemView/__tests__/MessageTextContainer.test.tsx index 5aa1142088..0caded18fc 100644 --- a/package/src/components/Message/MessageItemView/__tests__/MessageTextContainer.test.tsx +++ b/package/src/components/Message/MessageItemView/__tests__/MessageTextContainer.test.tsx @@ -45,7 +45,7 @@ describe('MessageTextContainer', () => { rerender( {message?.text}, }} > diff --git a/package/src/components/MessageInput/components/LinkPreviewList.tsx b/package/src/components/MessageInput/components/LinkPreviewList.tsx index 9b379bbdc1..a4e9bed9de 100644 --- a/package/src/components/MessageInput/components/LinkPreviewList.tsx +++ b/package/src/components/MessageInput/components/LinkPreviewList.tsx @@ -9,7 +9,8 @@ import { LinkPreviewsManager } from 'stream-chat'; import { AttachmentRemoveControl } from './AttachmentPreview/AttachmentRemoveControl'; -import { useChatContext, useMessageComposer, useTheme } from '../../../contexts'; +import { useMessageComposer, useTheme } from '../../../contexts'; +import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext'; import { Link } from '../../../icons/link'; import { components, primitives } from '../../../theme'; import { useLinkPreviews } from '../hooks/useLinkPreviews'; @@ -38,7 +39,7 @@ type LinkPreviewProps = { export const LinkPreviewCard = ({ linkPreview }: LinkPreviewProps) => { const styles = useStyles(); - const { ImageComponent } = useChatContext(); + const { ImageComponent } = useComponentsContext(); const { linkPreviewsManager } = useMessageComposer(); const { image_url, thumb_url, title, text, og_scrape_url } = linkPreview; diff --git a/package/src/components/MessageMenu/__tests__/MessageUserReactions.test.tsx b/package/src/components/MessageMenu/__tests__/MessageUserReactions.test.tsx index fd91791dda..a7272b0344 100644 --- a/package/src/components/MessageMenu/__tests__/MessageUserReactions.test.tsx +++ b/package/src/components/MessageMenu/__tests__/MessageUserReactions.test.tsx @@ -44,7 +44,7 @@ const renderComponent = (props = {}) => null, MessageUserReactionsItem: (itemProps: MessageUserReactionsItemProps) => ( {itemProps.reaction.id + ' ' + itemProps.reaction.type} diff --git a/package/src/components/Reply/Reply.tsx b/package/src/components/Reply/Reply.tsx index 46aa282aa1..f7a5b59ad4 100644 --- a/package/src/components/Reply/Reply.tsx +++ b/package/src/components/Reply/Reply.tsx @@ -10,7 +10,8 @@ import { import { ReplyMessageView } from './ReplyMessageView'; -import { ChatContextValue, useChatContext } from '../../contexts/chatContext/ChatContext'; +import { useChatContext } from '../../contexts/chatContext/ChatContext'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { MessageContextValue, useMessageContext, @@ -78,7 +79,7 @@ const RightContent = React.memo( }, ); -export type ReplyPropsWithContext = Pick & +export type ReplyPropsWithContext = { ImageComponent?: React.ComponentType } & Pick & Pick & { isMyMessage: boolean; @@ -214,7 +215,8 @@ export type ReplyProps = Partial & export const Reply = (props: ReplyProps) => { const { message: messageFromContext } = useMessageContext(); - const { client, ImageComponent } = useChatContext(); + const { client } = useChatContext(); + const { ImageComponent } = useComponentsContext(); const messageComposer = useMessageComposer(); const { quotedMessage: quotedMessageFromComposer } = useStateStore( diff --git a/package/src/components/ui/Avatar/Avatar.tsx b/package/src/components/ui/Avatar/Avatar.tsx index aaff3a3114..fb02eace24 100644 --- a/package/src/components/ui/Avatar/Avatar.tsx +++ b/package/src/components/ui/Avatar/Avatar.tsx @@ -3,7 +3,7 @@ import { ColorValue, StyleProp, StyleSheet, View, ViewStyle } from 'react-native import { avatarSizes } from './constants'; -import { useChatContext } from '../../../contexts'; +import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext'; import { useTheme } from '../../../contexts/themeContext/ThemeContext'; import { primitives } from '../../../theme'; @@ -21,7 +21,7 @@ export const Avatar = (props: AvatarProps) => { const { theme: { semantics }, } = useTheme(); - const { ImageComponent } = useChatContext(); + const { ImageComponent } = useComponentsContext(); const defaultAvatarBg = semantics.avatarPaletteBg1; const { backgroundColor = defaultAvatarBg, diff --git a/package/src/contexts/chatContext/ChatContext.tsx b/package/src/contexts/chatContext/ChatContext.tsx index 9c26b0cc83..c107c3d8ce 100644 --- a/package/src/contexts/chatContext/ChatContext.tsx +++ b/package/src/contexts/chatContext/ChatContext.tsx @@ -1,5 +1,4 @@ import React, { PropsWithChildren, useContext } from 'react'; -import type { ImageProps } from 'react-native'; import type { AppSettingsAPIResponse, Channel, Mute, StreamChat } from 'stream-chat'; @@ -32,10 +31,6 @@ export type ChatContextValue = { client: StreamChat; connectionRecovering: boolean; enableOfflineSupport: boolean; - /** - * Drop in replacement of all the underlying Image components within SDK. This is useful for the purpose of offline caching of images. Please check the Offline Support Guide for usage. - */ - ImageComponent: React.ComponentType; isOnline: boolean | null; mutedUsers: Mute[]; /** diff --git a/package/src/contexts/componentsContext/__tests__/defaultComponents.test.ts b/package/src/contexts/componentsContext/__tests__/defaultComponents.test.ts index ddffc26ad9..4742193501 100644 --- a/package/src/contexts/componentsContext/__tests__/defaultComponents.test.ts +++ b/package/src/contexts/componentsContext/__tests__/defaultComponents.test.ts @@ -5,7 +5,6 @@ const OPTIONAL_KEYS = new Set([ 'AttachmentPickerIOSSelectMorePhotos', 'ChatLoadingIndicator', 'CreatePollContent', - 'ImageComponent', 'Input', 'ListHeaderComponent', 'MessageContentBottomView', diff --git a/package/src/contexts/componentsContext/defaultComponents.ts b/package/src/contexts/componentsContext/defaultComponents.ts index ea8d27cdf3..325e17a790 100644 --- a/package/src/contexts/componentsContext/defaultComponents.ts +++ b/package/src/contexts/componentsContext/defaultComponents.ts @@ -1,4 +1,5 @@ import React from 'react'; +import { Image } from 'react-native'; import { Attachment } from '../../components/Attachment/Attachment'; import { AudioAttachment } from '../../components/Attachment/Audio'; @@ -37,6 +38,9 @@ import { ChannelPreviewTitle } from '../../components/ChannelPreview/ChannelPrev import { ChannelPreviewTypingIndicator } from '../../components/ChannelPreview/ChannelPreviewTypingIndicator'; import { ChannelPreviewUnreadCount } from '../../components/ChannelPreview/ChannelPreviewUnreadCount'; import { ChannelPreviewView } from '../../components/ChannelPreview/ChannelPreviewView'; +import { ImageGalleryFooter } from '../../components/ImageGallery/components/ImageGalleryFooter'; +import { ImageGalleryHeader } from '../../components/ImageGallery/components/ImageGalleryHeader'; +import { ImageGalleryGrid } from '../../components/ImageGallery/components/ImageGrid'; import { ImageGalleryVideoControl } from '../../components/ImageGallery/components/ImageGalleryVideoControl'; import { EmptyStateIndicator } from '../../components/Indicators/EmptyStateIndicator'; import { LoadingErrorIndicator } from '../../components/Indicators/LoadingErrorIndicator'; @@ -139,6 +143,7 @@ import { ThreadListItemMessagePreview } from '../../components/ThreadList/Thread import { ThreadListUnreadBanner } from '../../components/ThreadList/ThreadListUnreadBanner'; import { ThreadMessagePreviewDeliveryStatus } from '../../components/ThreadList/ThreadMessagePreviewDeliveryStatus'; import { ChannelAvatar } from '../../components/ui/Avatar/ChannelAvatar'; +import { DefaultMessageOverlayBackground } from '../../contexts/overlayContext/MessageOverlayHostLayer'; /** * All default component implementations used across the SDK. @@ -285,8 +290,17 @@ export const DEFAULT_COMPONENTS = { PollOptionFullResultsContent, // ImageGallery + ImageGalleryFooter, + ImageGalleryGrid, + ImageGalleryHeader, ImageGalleryVideoControls: ImageGalleryVideoControl, + // Overlay + MessageOverlayBackground: DefaultMessageOverlayBackground, + + // Image + ImageComponent: Image as React.ComponentType, + // Optional overrides (no defaults — undefined unless user provides via WithComponents) // eslint-disable-next-line @typescript-eslint/no-explicit-any AttachmentPickerIOSSelectMorePhotos: undefined as React.ComponentType | undefined, @@ -295,8 +309,6 @@ export const DEFAULT_COMPONENTS = { // eslint-disable-next-line @typescript-eslint/no-explicit-any CreatePollContent: undefined as React.ComponentType | undefined, // eslint-disable-next-line @typescript-eslint/no-explicit-any - ImageComponent: 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 ListHeaderComponent: undefined as React.ComponentType | undefined, diff --git a/package/src/contexts/imageGalleryContext/ImageGalleryContext.tsx b/package/src/contexts/imageGalleryContext/ImageGalleryContext.tsx index b5f376458d..3b1726d3e5 100644 --- a/package/src/contexts/imageGalleryContext/ImageGalleryContext.tsx +++ b/package/src/contexts/imageGalleryContext/ImageGalleryContext.tsx @@ -7,9 +7,6 @@ import { useImageGalleryContext, } from './ImageGalleryContextBase'; -import { ImageGalleryFooter as ImageGalleryFooterDefault } from '../../components/ImageGallery/components/ImageGalleryFooter'; -import { ImageGalleryHeader as ImageGalleryHeaderDefault } from '../../components/ImageGallery/components/ImageGalleryHeader'; -import { ImageGalleryGrid as ImageGalleryGridDefault } from '../../components/ImageGallery/components/ImageGrid'; import { ImageGalleryStateStore } from '../../state-store/image-gallery-state-store'; export const ImageGalleryProvider = ({ @@ -29,9 +26,6 @@ export const ImageGalleryProvider = ({ () => ({ autoPlayVideo: value?.autoPlayVideo, imageGalleryStateStore, - ImageGalleryHeader: ImageGalleryHeaderDefault, - ImageGalleryFooter: ImageGalleryFooterDefault, - ImageGalleryGrid: ImageGalleryGridDefault, ...value, }), [imageGalleryStateStore, value], diff --git a/package/src/contexts/imageGalleryContext/ImageGalleryContextBase.tsx b/package/src/contexts/imageGalleryContext/ImageGalleryContextBase.tsx index ead802338e..eaadec4e79 100644 --- a/package/src/contexts/imageGalleryContext/ImageGalleryContextBase.tsx +++ b/package/src/contexts/imageGalleryContext/ImageGalleryContextBase.tsx @@ -2,11 +2,6 @@ import React, { useContext } from 'react'; import type { Attachment } from 'stream-chat'; -import type { - ImageGalleryFooterProps, - ImageGalleryGridProps, - ImageGalleryHeaderProps, -} from '../../components/ImageGallery/components/types'; import { ImageGalleryStateStore } from '../../state-store/image-gallery-state-store'; import { DEFAULT_BASE_CONTEXT_VALUE } from '../utils/defaultBaseContextValue'; @@ -16,9 +11,6 @@ export type ImageGalleryProviderProps = { autoPlayVideo?: boolean; giphyVersion?: keyof NonNullable; numberOfImageGalleryGridColumns?: number; - ImageGalleryHeader?: React.ComponentType; - ImageGalleryFooter?: React.ComponentType; - ImageGalleryGrid?: React.ComponentType; }; export type ImageGalleryContextValue = ImageGalleryProviderProps & { diff --git a/package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx b/package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx index 67a10eb656..5ac2fc19d2 100644 --- a/package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx +++ b/package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx @@ -29,11 +29,12 @@ import { Rect, useOverlayController, } from '../../state-store'; +import { useComponentsContext } from '../componentsContext/ComponentsContext'; import { useTheme } from '../themeContext/ThemeContext'; const DURATION = 300; -const DefaultMessageOverlayBackground = () => { +export const DefaultMessageOverlayBackground = () => { const { theme: { semantics }, } = useTheme(); @@ -53,11 +54,8 @@ const DefaultMessageOverlayBackground = () => { ); }; -type MessageOverlayHostLayerProps = { - BackgroundComponent?: React.ComponentType; -}; - -export const MessageOverlayHostLayer = ({ BackgroundComponent }: MessageOverlayHostLayerProps) => { +export const MessageOverlayHostLayer = () => { + const { MessageOverlayBackground } = useComponentsContext(); const { id, closing } = useOverlayController(); const insets = useSafeAreaInsets(); const { height: screenH } = useWindowDimensions(); @@ -130,7 +128,7 @@ export const MessageOverlayHostLayer = ({ BackgroundComponent }: MessageOverlayH opacity: backdrop.value, })); - const OverlayBackground = BackgroundComponent ?? DefaultMessageOverlayBackground; + const OverlayBackground = MessageOverlayBackground; const messageShiftY = useDerivedValue(() => { if (!messageH.value || !topH.value || !bottomH.value) return 0; diff --git a/package/src/contexts/overlayContext/OverlayContext.tsx b/package/src/contexts/overlayContext/OverlayContext.tsx index 97b681f30e..0417048c94 100644 --- a/package/src/contexts/overlayContext/OverlayContext.tsx +++ b/package/src/contexts/overlayContext/OverlayContext.tsx @@ -26,10 +26,6 @@ export const OverlayContext = React.createContext( export type OverlayProviderProps = ImageGalleryProviderProps & { /** https://github.com/GetStream/stream-chat-react-native/wiki/Internationalization-(i18n) */ i18nInstance?: Streami18n; - /** - * Custom backdrop component rendered behind overlay content in `MessageOverlayHostLayer`. - */ - MessageOverlayBackground?: React.ComponentType; value?: Partial; }; diff --git a/package/src/contexts/overlayContext/OverlayProvider.tsx b/package/src/contexts/overlayContext/OverlayProvider.tsx index beea9854e6..b809988c93 100644 --- a/package/src/contexts/overlayContext/OverlayProvider.tsx +++ b/package/src/contexts/overlayContext/OverlayProvider.tsx @@ -44,7 +44,6 @@ export const OverlayProvider = (props: PropsWithChildren) const { children, i18nInstance, - MessageOverlayBackground, value, autoPlayVideo, giphyVersion, @@ -107,7 +106,7 @@ export const OverlayProvider = (props: PropsWithChildren) {children} {overlay === 'gallery' && } - + diff --git a/package/src/contexts/overlayContext/__tests__/MessageOverlayHostLayer.test.tsx b/package/src/contexts/overlayContext/__tests__/MessageOverlayHostLayer.test.tsx index 64d2abc5af..cded078fdf 100644 --- a/package/src/contexts/overlayContext/__tests__/MessageOverlayHostLayer.test.tsx +++ b/package/src/contexts/overlayContext/__tests__/MessageOverlayHostLayer.test.tsx @@ -13,6 +13,7 @@ import { setOverlayMessageH, setOverlayTopH, } from '../../../state-store'; +import { WithComponents } from '../../componentsContext/ComponentsContext'; import { MessageOverlayHostLayer } from '../MessageOverlayHostLayer'; jest.mock('react-native', () => { @@ -142,7 +143,11 @@ describe('MessageOverlayHostLayer', () => { it('renders the custom background only while active and pressing the backdrop starts closing', () => { const CustomBackground = () => Background; - render(); + render( + + + , + ); expect(screen.queryByTestId('custom-background')).toBeNull(); expect(screen.queryByTestId('message-overlay-backdrop')).toBeNull(); @@ -161,7 +166,12 @@ describe('MessageOverlayHostLayer', () => { }); it('positions and translates the top, message, and bottom hosts using the registered rects', () => { - const { rerender } = render(); + const renderTree = () => ( + + + + ); + const { rerender } = render(renderTree()); act(() => {}); @@ -172,7 +182,7 @@ describe('MessageOverlayHostLayer', () => { openOverlay('message-1'); }); - rerender(); + rerender(renderTree()); const topSlot = screen.getByTestId('message-overlay-top'); const messageSlot = screen.getByTestId('message-overlay-message'); @@ -206,7 +216,12 @@ describe('MessageOverlayHostLayer', () => { }); it('resets host geometry after finalizeCloseOverlay clears the registered rects', () => { - const { rerender } = render(); + const renderTree = () => ( + + + + ); + const { rerender } = render(renderTree()); act(() => {}); @@ -217,7 +232,7 @@ describe('MessageOverlayHostLayer', () => { openOverlay('message-1'); }); - rerender(); + rerender(renderTree()); expect( StyleSheet.flatten(screen.getByTestId('message-overlay-message').props.style), @@ -230,7 +245,7 @@ describe('MessageOverlayHostLayer', () => { finalizeCloseOverlay(); }); - rerender(); + rerender(renderTree()); expect(StyleSheet.flatten(screen.getByTestId('message-overlay-top').props.style)).toMatchObject( { From 980fd643965a1a39e2f926c7c9254521a6875930 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Tue, 14 Apr 2026 16:33:22 +0200 Subject: [PATCH 12/16] perf: make WithComponents and useComponentsContext return stable references Component overrides are set once at mount and never change dynamically. Remove useMemo dependencies so both the provider value and the hook result are computed once, preventing unnecessary re-renders when integrators pass inline override objects. --- package/foobar.db-journal | Bin 0 -> 12820 bytes .../componentsContext/ComponentsContext.tsx | 6 ++++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 package/foobar.db-journal diff --git a/package/foobar.db-journal b/package/foobar.db-journal new file mode 100644 index 0000000000000000000000000000000000000000..993cb987aa26876c7cc0ef94e398b2a6d9245805 GIT binary patch literal 12820 zcmeI2&#Pod6~|xtb-(`IJ;~(FBs25w%=G-8H>Fi|>&LADL2=>6L?U^Fs1T~^)`fr) ze~ZK?TtXlVK?YaRRTiSFh=|!}+!zQ7S%eT&f@G(-5QHcwF7#A){($e5cBAjYdwn0T z`S7he=X~p&+x@$@2miSAQvA)%&QAZeH=cWeUhnPf^yuXj{hVC?2Rm>up+(;5y|^=Z zYy79l^Ao#F%bzUIPA^ZNnZCNKcc1J1are#9zlN`m;%I+(IQsGM>*Ghp-|Jr-JkkH$ zVBG)N-tQM|_P6=H*>wKV*^~2kXFpi{YVqvgy}fVmePr-`0-szbJMf?Ez$5*mqx({j zDq5~t6{28DaL+j=ttlOAoK#F%x(h~>jEAO*s_~){;?9~TgR;R1 z!5YgAQ+29YrKMoJ;wA)9HeT^6CM}W-Oq!7BJ(Xq7rpQdC1+Oa}xDAIeO*PVnN+%Yh z@=T>NGB1^3T9W34f&w#nRF(`-=E%g9+3O`ugYiS|U(oef61CZ&egDm31C9$8G5Xo}wL z=)cSvnOqE|b+)_#(~PN;3!WAk6=%vP$DE*;nc5i&VXAGA*I}A76&IDNS*#3Yh;fFw zXz5RHcv(^$d{oz98j>MbVH%PlSJt#T22lk?We_;6LTcJvA1u?60#fqS)U`%px`Jsk z`PxuuS+b30sw`>W43&oEfs(C`xgIXVG_4w^N~c*ZNwxBI$XQgekhJ#{Rc>v%1k=jO zreIYfF`8(NsF>FzFmYZx9;#Y|aB)p@wM-#$$Ld;El)aWbB$}Ej&0VnCL?IoLZ^fEs zc?xCa$v($-Li!89ow+i*+iYMO>TgpLjGl4VpPB_)C7GK8}*4LP&U zY-mn~QhLrrO)0FTq#A4lC9~CDR+TU@Xe4|0*EB8-7n+Jr=h9I!X`(5`qBeonQ7Mxu zg!UjzL%jDiOhdf)0hoq(@BJHEq>jRA&nh1!bp_GUU8x!OWw25Ex%(-5~D!ZgG!_trG(j!PHo#46*+-$WARu*tL9*U{3AF|k6jcL38659zOI+=vut zFkrHAI37%TF&~UXCVjPOnbC){DJYdZ%{Bqi+pAJp9A(JA2<9xxGJ+-W|)`&-8z| z`=kC*zqj|X#d7w``P0*1&(F*b=0Bf(e(}>q7(6=o>)@sSKM8zt{r7g@lY^t9{jCkl zpMB=5>COhZt%*4!4{;!EJk&)MNB;v;z zc@0M*Hky%FaU_~VS2oCPjn`S_3P&OxMn+!7kw}}7k(Y2J(raYo#SL;>BX>p?8|1d; z?u#Rg&*Mlm;^%NA8u5p5B+_E$@H~Vgkv=mc&*DgAG|$L0?<2P~pJ(L$ z2Dz;vJtH5)k*KSm#*wJ2KY$}qSHJ&#d^#Rj>pIX*9Zjw8`F zYlb6H3!dUg)Pg5C61CtljzleZgdihau-LU z=GJ>G4}4pD29WoXoKP#>Suuhaec&hn|nFBS*$Z_M7A-JQNT`TOLF@hju~(U*sB z4dvdw!CwZS=)X*FJGoAF;A98>S9hQ-?utDUL7M3y|O@r))u$B2t*D5Tp&Vg zi(6d;BK;jM5TUij%`O6wZV?xV(Awfg7lFvfgsWrZVPVcegw__Nd>4U8-QfZeT3ej!A`q!NTp&XGH;cTOY5)KL literal 0 HcmV?d00001 diff --git a/package/src/contexts/componentsContext/ComponentsContext.tsx b/package/src/contexts/componentsContext/ComponentsContext.tsx index 7a284a26ad..4c57c082e3 100644 --- a/package/src/contexts/componentsContext/ComponentsContext.tsx +++ b/package/src/contexts/componentsContext/ComponentsContext.tsx @@ -32,7 +32,8 @@ export const WithComponents = ({ overrides, }: PropsWithChildren<{ overrides: ComponentOverrides }>) => { const parent = useContext(ComponentsContext); - const merged = useMemo(() => ({ ...parent, ...overrides }), [parent, overrides]); + // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally stable: overrides are set once at mount + const merged = useMemo(() => ({ ...parent, ...overrides }), []); return {children}; }; @@ -55,6 +56,7 @@ export const useComponentsContext = () => { const overrides = useContext(ComponentsContext); return useMemo( () => ({ ...getDefaults(), ...overrides }) as Required, - [overrides], + // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally stable: overrides are set once at mount + [], ); }; From b34c3f0dc53eea7be7367ea87818737384de1c24 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Tue, 14 Apr 2026 16:33:47 +0200 Subject: [PATCH 13/16] chore: remove accidentally committed test artifact --- package/foobar.db-journal | Bin 12820 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 package/foobar.db-journal diff --git a/package/foobar.db-journal b/package/foobar.db-journal deleted file mode 100644 index 993cb987aa26876c7cc0ef94e398b2a6d9245805..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12820 zcmeI2&#Pod6~|xtb-(`IJ;~(FBs25w%=G-8H>Fi|>&LADL2=>6L?U^Fs1T~^)`fr) ze~ZK?TtXlVK?YaRRTiSFh=|!}+!zQ7S%eT&f@G(-5QHcwF7#A){($e5cBAjYdwn0T z`S7he=X~p&+x@$@2miSAQvA)%&QAZeH=cWeUhnPf^yuXj{hVC?2Rm>up+(;5y|^=Z zYy79l^Ao#F%bzUIPA^ZNnZCNKcc1J1are#9zlN`m;%I+(IQsGM>*Ghp-|Jr-JkkH$ zVBG)N-tQM|_P6=H*>wKV*^~2kXFpi{YVqvgy}fVmePr-`0-szbJMf?Ez$5*mqx({j zDq5~t6{28DaL+j=ttlOAoK#F%x(h~>jEAO*s_~){;?9~TgR;R1 z!5YgAQ+29YrKMoJ;wA)9HeT^6CM}W-Oq!7BJ(Xq7rpQdC1+Oa}xDAIeO*PVnN+%Yh z@=T>NGB1^3T9W34f&w#nRF(`-=E%g9+3O`ugYiS|U(oef61CZ&egDm31C9$8G5Xo}wL z=)cSvnOqE|b+)_#(~PN;3!WAk6=%vP$DE*;nc5i&VXAGA*I}A76&IDNS*#3Yh;fFw zXz5RHcv(^$d{oz98j>MbVH%PlSJt#T22lk?We_;6LTcJvA1u?60#fqS)U`%px`Jsk z`PxuuS+b30sw`>W43&oEfs(C`xgIXVG_4w^N~c*ZNwxBI$XQgekhJ#{Rc>v%1k=jO zreIYfF`8(NsF>FzFmYZx9;#Y|aB)p@wM-#$$Ld;El)aWbB$}Ej&0VnCL?IoLZ^fEs zc?xCa$v($-Li!89ow+i*+iYMO>TgpLjGl4VpPB_)C7GK8}*4LP&U zY-mn~QhLrrO)0FTq#A4lC9~CDR+TU@Xe4|0*EB8-7n+Jr=h9I!X`(5`qBeonQ7Mxu zg!UjzL%jDiOhdf)0hoq(@BJHEq>jRA&nh1!bp_GUU8x!OWw25Ex%(-5~D!ZgG!_trG(j!PHo#46*+-$WARu*tL9*U{3AF|k6jcL38659zOI+=vut zFkrHAI37%TF&~UXCVjPOnbC){DJYdZ%{Bqi+pAJp9A(JA2<9xxGJ+-W|)`&-8z| z`=kC*zqj|X#d7w``P0*1&(F*b=0Bf(e(}>q7(6=o>)@sSKM8zt{r7g@lY^t9{jCkl zpMB=5>COhZt%*4!4{;!EJk&)MNB;v;z zc@0M*Hky%FaU_~VS2oCPjn`S_3P&OxMn+!7kw}}7k(Y2J(raYo#SL;>BX>p?8|1d; z?u#Rg&*Mlm;^%NA8u5p5B+_E$@H~Vgkv=mc&*DgAG|$L0?<2P~pJ(L$ z2Dz;vJtH5)k*KSm#*wJ2KY$}qSHJ&#d^#Rj>pIX*9Zjw8`F zYlb6H3!dUg)Pg5C61CtljzleZgdihau-LU z=GJ>G4}4pD29WoXoKP#>Suuhaec&hn|nFBS*$Z_M7A-JQNT`TOLF@hju~(U*sB z4dvdw!CwZS=)X*FJGoAF;A98>S9hQ-?utDUL7M3y|O@r))u$B2t*D5Tp&Vg zi(6d;BK;jM5TUij%`O6wZV?xV(Awfg7lFvfgsWrZVPVcegw__Nd>4U8-QfZeT3ej!A`q!NTp&XGH;cTOY5)KL From ac309ec23a667cb7d41a103ee5504eb60a3f2ed5 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Tue, 14 Apr 2026 16:56:24 +0200 Subject: [PATCH 14/16] fix: resolve build and lint errors from component migration - Fix ImageComponent type in Reply.tsx (required, not optional) - Replace React.ComponentType with proper types (ImageProps, ImageGalleryHeaderProps, etc.) to satisfy no-explicit-any lint rule - Add missing ImageProps imports in URLPreview/URLPreviewCompact - Fix prettier formatting --- .../components/Attachment/GalleryImage.tsx | 2 +- .../Attachment/Giphy/GiphyImage.tsx | 18 +++++++++--------- .../Attachment/UrlPreview/URLPreview.tsx | 6 ++++-- .../UrlPreview/URLPreviewCompact.tsx | 6 ++++-- .../components/ImageGallery/ImageGallery.tsx | 19 ++++++++++--------- .../__tests__/ImageGalleryFooter.test.tsx | 2 ++ package/src/components/Reply/Reply.tsx | 17 ++++++++++++++--- .../componentsContext/defaultComponents.ts | 3 ++- 8 files changed, 46 insertions(+), 27 deletions(-) diff --git a/package/src/components/Attachment/GalleryImage.tsx b/package/src/components/Attachment/GalleryImage.tsx index 39b9442cd0..70a754d540 100644 --- a/package/src/components/Attachment/GalleryImage.tsx +++ b/package/src/components/Attachment/GalleryImage.tsx @@ -6,7 +6,7 @@ import { useComponentsContext } from '../../contexts/componentsContext/Component import { getUrlWithoutParams, isLocalUrl, makeImageCompatibleUrl } from '../../utils/utils'; export type GalleryImageWithContextProps = GalleryImageProps & { - ImageComponent?: React.ComponentType; + ImageComponent?: React.ComponentType; }; export const GalleryImageWithContext = (props: GalleryImageWithContextProps) => { diff --git a/package/src/components/Attachment/Giphy/GiphyImage.tsx b/package/src/components/Attachment/Giphy/GiphyImage.tsx index 00189404a8..f5126b9ed5 100644 --- a/package/src/components/Attachment/Giphy/GiphyImage.tsx +++ b/package/src/components/Attachment/Giphy/GiphyImage.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import { Image, StyleSheet, View } from 'react-native'; +import { Image, ImageProps, StyleSheet, View } from 'react-native'; import type { Attachment } from 'stream-chat'; @@ -16,14 +16,14 @@ import { makeImageCompatibleUrl } from '../../../utils/utils'; import { GiphyBadge } from '../../ui/Badge/GiphyBadge'; export type GiphyImagePropsWithContext = Pick & { - ImageComponent?: React.ComponentType; - } & { - attachment: Attachment; - /** - * Whether to render the preview image or the full image - */ - preview?: boolean; - }; + ImageComponent?: React.ComponentType; +} & { + attachment: Attachment; + /** + * Whether to render the preview image or the full image + */ + preview?: boolean; +}; const GiphyImageWithContext = (props: GiphyImagePropsWithContext) => { const { attachment, giphyVersion, ImageComponent = Image, preview = false } = props; diff --git a/package/src/components/Attachment/UrlPreview/URLPreview.tsx b/package/src/components/Attachment/UrlPreview/URLPreview.tsx index a2ef7870ba..424c2d1d1b 100644 --- a/package/src/components/Attachment/UrlPreview/URLPreview.tsx +++ b/package/src/components/Attachment/UrlPreview/URLPreview.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from 'react'; import { Image, + ImageProps, ImageStyle, Pressable, StyleProp, @@ -32,8 +33,9 @@ import { VideoPlayIndicator } from '../../ui'; import { ImageBackground } from '../../UIComponents/ImageBackground'; import { openUrlSafely } from '../utils/openUrlSafely'; -export type URLPreviewPropsWithContext = { ImageComponent?: React.ComponentType } & - Pick & +export type URLPreviewPropsWithContext = { + ImageComponent?: React.ComponentType; +} & Pick & Pick< MessagesContextValue, 'additionalPressableProps' | 'myMessageTheme' | 'isAttachmentEqual' diff --git a/package/src/components/Attachment/UrlPreview/URLPreviewCompact.tsx b/package/src/components/Attachment/UrlPreview/URLPreviewCompact.tsx index 60025afc30..07a0fc9415 100644 --- a/package/src/components/Attachment/UrlPreview/URLPreviewCompact.tsx +++ b/package/src/components/Attachment/UrlPreview/URLPreviewCompact.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from 'react'; import { Image, + ImageProps, ImageStyle, Pressable, StyleProp, @@ -32,8 +33,9 @@ import { VideoPlayIndicator } from '../../ui'; import { ImageBackground } from '../../UIComponents/ImageBackground'; import { openUrlSafely } from '../utils/openUrlSafely'; -export type URLPreviewCompactPropsWithContext = { ImageComponent?: React.ComponentType } & - Pick & +export type URLPreviewCompactPropsWithContext = { + ImageComponent?: React.ComponentType; +} & Pick & Pick & { attachment: Attachment; channelId: string | undefined; diff --git a/package/src/components/ImageGallery/ImageGallery.tsx b/package/src/components/ImageGallery/ImageGallery.tsx index 2d66aa0ab9..d3af58ec2d 100644 --- a/package/src/components/ImageGallery/ImageGallery.tsx +++ b/package/src/components/ImageGallery/ImageGallery.tsx @@ -13,14 +13,19 @@ import Animated, { import { AnimatedGalleryImage } from './components/AnimatedGalleryImage'; import { AnimatedGalleryVideo } from './components/AnimatedGalleryVideo'; +import type { + ImageGalleryFooterProps, + ImageGalleryGridProps, + ImageGalleryHeaderProps, +} from './components/types'; import { useImageGalleryGestures } from './hooks/useImageGalleryGestures'; +import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { ImageGalleryProviderProps, useImageGalleryContext, } from '../../contexts/imageGalleryContext/ImageGalleryContext'; -import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { OverlayContextValue, useOverlayContext, @@ -67,9 +72,9 @@ type ImageGalleryWithContextProps = Pick< 'numberOfImageGalleryGridColumns' > & Pick & { - ImageGalleryHeader?: React.ComponentType; - ImageGalleryFooter?: React.ComponentType; - ImageGalleryGrid?: React.ComponentType; + ImageGalleryHeader?: React.ComponentType; + ImageGalleryFooter?: React.ComponentType; + ImageGalleryGrid?: React.ComponentType; }; export const ImageGalleryWithContext = (props: ImageGalleryWithContextProps) => { @@ -370,11 +375,7 @@ export type ImageGalleryProps = Partial; export const ImageGallery = (props: ImageGalleryProps) => { const { numberOfImageGalleryGridColumns } = useImageGalleryContext(); - const { - ImageGalleryHeader, - ImageGalleryFooter, - ImageGalleryGrid, - } = useComponentsContext(); + const { ImageGalleryHeader, ImageGalleryFooter, ImageGalleryGrid } = useComponentsContext(); const { overlayOpacity } = useOverlayContext(); return ( { return ( }}> + {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} }}> + {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} } & - Pick & +export type ReplyPropsWithContext = { ImageComponent: React.ComponentType } & Pick< + MessageContextValue, + 'message' +> & Pick & { isMyMessage: boolean; onDismiss?: () => void; diff --git a/package/src/contexts/componentsContext/defaultComponents.ts b/package/src/contexts/componentsContext/defaultComponents.ts index 325e17a790..12af4049ea 100644 --- a/package/src/contexts/componentsContext/defaultComponents.ts +++ b/package/src/contexts/componentsContext/defaultComponents.ts @@ -40,8 +40,8 @@ import { ChannelPreviewUnreadCount } from '../../components/ChannelPreview/Chann import { ChannelPreviewView } from '../../components/ChannelPreview/ChannelPreviewView'; import { ImageGalleryFooter } from '../../components/ImageGallery/components/ImageGalleryFooter'; import { ImageGalleryHeader } from '../../components/ImageGallery/components/ImageGalleryHeader'; -import { ImageGalleryGrid } from '../../components/ImageGallery/components/ImageGrid'; import { ImageGalleryVideoControl } from '../../components/ImageGallery/components/ImageGalleryVideoControl'; +import { ImageGalleryGrid } from '../../components/ImageGallery/components/ImageGrid'; import { EmptyStateIndicator } from '../../components/Indicators/EmptyStateIndicator'; import { LoadingErrorIndicator } from '../../components/Indicators/LoadingErrorIndicator'; import { LoadingIndicator } from '../../components/Indicators/LoadingIndicator'; @@ -299,6 +299,7 @@ export const DEFAULT_COMPONENTS = { MessageOverlayBackground: DefaultMessageOverlayBackground, // Image + // eslint-disable-next-line @typescript-eslint/no-explicit-any ImageComponent: Image as React.ComponentType, // Optional overrides (no defaults — undefined unless user provides via WithComponents) From af246b54f44ccb14f0c3a4a2b7db52f989f0021a Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Tue, 14 Apr 2026 16:47:00 +0200 Subject: [PATCH 15/16] fix: remove redundant overrides --- examples/SampleApp/src/screens/ChannelScreen.tsx | 2 +- examples/SampleApp/src/screens/ThreadScreen.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/SampleApp/src/screens/ChannelScreen.tsx b/examples/SampleApp/src/screens/ChannelScreen.tsx index 0132b03d3e..025f31038e 100644 --- a/examples/SampleApp/src/screens/ChannelScreen.tsx +++ b/examples/SampleApp/src/screens/ChannelScreen.tsx @@ -268,7 +268,7 @@ export const ChannelScreen: React.FC = ({ navigation, route null, diff --git a/examples/SampleApp/src/screens/ThreadScreen.tsx b/examples/SampleApp/src/screens/ThreadScreen.tsx index b22b25c9bf..8884bf9bc0 100644 --- a/examples/SampleApp/src/screens/ThreadScreen.tsx +++ b/examples/SampleApp/src/screens/ThreadScreen.tsx @@ -151,7 +151,7 @@ export const ThreadScreen: React.FC = ({ From d6b48a99302749498c54ff266551ee25ad05ab3c Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Tue, 14 Apr 2026 22:07:42 +0200 Subject: [PATCH 16/16] fix: lint md --- .../src/contexts/componentsContext/PLAN.md | 68 ++++++++++--------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/package/src/contexts/componentsContext/PLAN.md b/package/src/contexts/componentsContext/PLAN.md index fac8947ffe..d19280d2b5 100644 --- a/package/src/contexts/componentsContext/PLAN.md +++ b/package/src/contexts/componentsContext/PLAN.md @@ -34,18 +34,17 @@ Consumer: const { Message } = useComponentsContext() ### Key Files -| File | Purpose | -|------|---------| +| File | Purpose | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `ComponentsContext.tsx` | ~60 lines. `ComponentOverrides` type (derived from `typeof DEFAULT_COMPONENTS`), `WithComponents` provider, `useComponentsContext()` hook | -| `defaultComponents.ts` | ~300 lines. Single source of truth for all default component mappings. Adding a new component here auto-extends `ComponentOverrides` | +| `defaultComponents.ts` | ~300 lines. Single source of truth for all default component mappings. Adding a new component here auto-extends `ComponentOverrides` | ### Type System `ComponentOverrides` is derived automatically: + ```ts -export type ComponentOverrides = Partial< - (typeof import('./defaultComponents'))['DEFAULT_COMPONENTS'] ->; +export type ComponentOverrides = Partial<(typeof import('./defaultComponents'))['DEFAULT_COMPONENTS']>; ``` No manual type maintenance — add a component to `DEFAULT_COMPONENTS` and the type updates. @@ -55,6 +54,7 @@ No manual type maintenance — add a component to `DEFAULT_COMPONENTS` and the t `defaultComponents.ts` → imports components → components import `useComponentsContext` from `ComponentsContext.tsx`. Broken by lazy-loading defaults in the hook: + ```ts let cachedDefaults: ComponentOverrides | undefined; const getDefaults = () => { @@ -69,20 +69,20 @@ 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 | +| `Preview` | `ChannelPreviewView` | ChannelList preview item | +| `PreviewAvatar` | `ChannelAvatar` | ChannelList preview avatar | +| `FooterLoadingIndicator` | `ChannelListFooterLoadingIndicator` | ChannelList footer | +| `HeaderErrorIndicator` | `ChannelListHeaderErrorIndicator` | ChannelList header | +| `HeaderNetworkDownIndicator` | `ChannelListHeaderNetworkDownIndicator` | ChannelList header | ### Optional Components (no default) @@ -94,28 +94,29 @@ These exist in `DEFAULT_COMPONENTS` as `undefined` with `React.ComponentType