Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"@react-native/eslint-config": "0.81.5",
"@react-native/metro-config": "0.81.5",
"@react-native/typescript-config": "0.81.5",
"@stylistic/eslint-plugin": "^3.1.0",
"@stylistic/eslint-plugin": "^5.6.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.0",
"@testing-library/react-native": "^13.3.3",
Expand Down
34 changes: 34 additions & 0 deletions src/Bubble/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Text } from 'react-native-gesture-handler'
import { useChatContext } from '../GiftedChatContext'
import { MessageAudio } from '../MessageAudio'
import { MessageImage } from '../MessageImage'
import { MessageReply } from '../MessageReply'
import { MessageText } from '../MessageText'
import { MessageVideo } from '../MessageVideo'
import { IMessage } from '../Models'
Expand Down Expand Up @@ -319,10 +320,42 @@ export const Bubble = <TMessage extends IMessage = IMessage>(props: BubbleProps<
return null
}, [props])

const renderMessageReply = useCallback(() => {
if (!currentMessage?.replyMessage)
return null

const messageReplyProps = {
currentMessage,
position,
onPress: props.onPressMessageReply,
containerStyle: props.messageReplyContainerStyle,
contentContainerStyle: props.messageReplyContentContainerStyle,
imageStyle: props.messageReplyImageStyle,
usernameStyle: props.messageReplyUsernameStyle,
textStyle: props.messageReplyTextStyle,
}

if (props.renderMessageReply)
return renderComponentOrElement(props.renderMessageReply, messageReplyProps)

return <MessageReply {...messageReplyProps} />
}, [
props.renderMessageReply,
props.onPressMessageReply,
props.messageReplyContainerStyle,
props.messageReplyContentContainerStyle,
props.messageReplyImageStyle,
props.messageReplyUsernameStyle,
props.messageReplyTextStyle,
currentMessage,
position,
])

const renderBubbleContent = useCallback(() => {
return (
<>
{!props.isCustomViewBottom && renderCustomView()}
{renderMessageReply()}
{renderMessageImage()}
{renderMessageVideo()}
{renderMessageAudio()}
Expand All @@ -331,6 +364,7 @@ export const Bubble = <TMessage extends IMessage = IMessage>(props: BubbleProps<
</>
)
}, [
renderMessageReply,
renderCustomView,
renderMessageImage,
renderMessageVideo,
Expand Down
27 changes: 23 additions & 4 deletions src/Bubble/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ import {
StyleProp,
ViewStyle,
TextStyle,
ImageStyle,
Pressable,
} from 'react-native'
import { MessageImageProps } from '../MessageImage'
import { MessageReplyProps } from '../MessageReply'
import { MessageTextProps } from '../MessageText'
import {
User,
IMessage,
LeftRightStyle,
Reply,
ReplyMessage,
Omit,
MessageVideoProps,
MessageAudioProps,
Expand Down Expand Up @@ -71,13 +74,13 @@ export interface BubbleProps<TMessage extends IMessage> {
onLongPressMessage?: (context?: unknown, message?: unknown) => void
onQuickReply?: (replies: Reply[]) => void
renderMessageImage?: (
props: RenderMessageImageProps<TMessage>,
props: RenderMessageImageProps<TMessage>
) => React.ReactNode
renderMessageVideo?: (
props: RenderMessageVideoProps<TMessage>,
props: RenderMessageVideoProps<TMessage>
) => React.ReactNode
renderMessageAudio?: (
props: RenderMessageAudioProps<TMessage>,
props: RenderMessageAudioProps<TMessage>
) => React.ReactNode
renderMessageText?: (props: RenderMessageTextProps<TMessage>) => React.ReactNode
renderCustomView?: (bubbleProps: BubbleProps<TMessage>) => React.ReactNode
Expand All @@ -86,6 +89,22 @@ export interface BubbleProps<TMessage extends IMessage> {
renderUsername?: (user?: TMessage['user']) => React.ReactNode
renderQuickReplySend?: () => React.ReactNode
renderQuickReplies?: (
quickReplies: QuickRepliesProps<TMessage>,
quickReplies: QuickRepliesProps<TMessage>
) => React.ReactNode
/** Custom render for message reply; rendered on top of message content */
renderMessageReply?: (
props: MessageReplyProps<TMessage>
) => React.ReactNode
/** Callback when message reply is pressed */
onPressMessageReply?: (replyMessage: ReplyMessage) => void
/** Style for message reply container */
messageReplyContainerStyle?: LeftRightStyle<ViewStyle>
/** Style for message reply content container */
messageReplyContentContainerStyle?: LeftRightStyle<ViewStyle>
/** Style for message reply image */
messageReplyImageStyle?: StyleProp<ImageStyle>
/** Style for message reply username */
messageReplyUsernameStyle?: StyleProp<TextStyle>
/** Style for message reply text */
messageReplyTextStyle?: StyleProp<TextStyle>
}
89 changes: 87 additions & 2 deletions src/GiftedChat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { TEST_ID } from '../Constant'
import { GiftedChatContext } from '../GiftedChatContext'
import { InputToolbar } from '../InputToolbar'
import { MessagesContainer, AnimatedList } from '../MessagesContainer'
import { IMessage } from '../Models'
import { IMessage, ReplyMessage } from '../Models'
import stylesCommon from '../styles'
import { renderComponentOrElement } from '../utils'
import styles from './styles'
Expand Down Expand Up @@ -56,6 +56,19 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
renderChatFooter,
renderInputToolbar,
isInverted = true,
// Reply props
isSwipeToReplyEnabled = false,
swipeToReplyDirection = 'right',
onSwipeToReply: onSwipeToReplyProp,
renderSwipeToReplyAction,
swipeToReplyActionContainerStyle,
replyMessage: replyMessageProp,
onClearReply: onClearReplyProp,
renderReplyPreview,
replyPreviewContainerStyle,
replyPreviewUsernameStyle,
replyPreviewTextStyle,
renderMessageReply,
} = props

const systemColorScheme = useColorScheme()
Expand All @@ -75,6 +88,10 @@ function GiftedChat<TMessage extends IMessage = IMessage> (

const [isInitialized, setIsInitialized] = useState<boolean>(false)
const [text, setText] = useState<string | undefined>(() => props.text || '')
const [internalReplyMessage, setInternalReplyMessage] = useState<ReplyMessage | null>(null)

// Use prop if provided, otherwise use internal state
const replyMessage = replyMessageProp !== undefined ? replyMessageProp : internalReplyMessage

const getTextFromProp = useCallback(
(fallback: string) => {
Expand Down Expand Up @@ -104,6 +121,37 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
[isInverted, messagesContainerRef]
)

const clearReply = useCallback(() => {
if (replyMessageProp !== undefined)
// Controlled mode - call external callback
onClearReplyProp?.()
else
// Uncontrolled mode - manage internally
setInternalReplyMessage(null)

}, [replyMessageProp, onClearReplyProp])

const handleSwipeToReply = useCallback((message: TMessage) => {
// Focus the text input when reply is triggered
textInputRef.current?.focus()

if (replyMessageProp !== undefined) {
// Controlled mode - call external callback
onSwipeToReplyProp?.(message)
} else {
// Uncontrolled mode - manage internally
const reply: ReplyMessage = {
_id: message._id,
text: message.text,
user: message.user,
image: message.image,
audio: message.audio,
}
setInternalReplyMessage(reply)
onSwipeToReplyProp?.(message)
}
}, [replyMessageProp, onSwipeToReplyProp, textInputRef])

const renderMessages = useMemo(() => {
if (!isInitialized)
return null
Expand All @@ -118,6 +166,12 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
messages={messages}
forwardRef={messagesContainerRef}
isTyping={isTyping}
isSwipeToReplyEnabled={isSwipeToReplyEnabled}
swipeToReplyDirection={swipeToReplyDirection}
onSwipeToReply={handleSwipeToReply}
renderSwipeToReplyAction={renderSwipeToReplyAction}
swipeToReplyActionContainerStyle={swipeToReplyActionContainerStyle}
renderMessageReply={renderMessageReply}
/>
{renderComponentOrElement(renderChatFooter, {})}
</View>
Expand All @@ -130,6 +184,12 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
isInverted,
messagesContainerRef,
renderChatFooter,
isSwipeToReplyEnabled,
swipeToReplyDirection,
handleSwipeToReply,
renderSwipeToReplyAction,
swipeToReplyActionContainerStyle,
renderMessageReply,
])

const notifyInputTextReset = useCallback(() => {
Expand Down Expand Up @@ -159,17 +219,23 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
user: user!,
createdAt: new Date(),
_id: messageIdGenerator?.(),
// Attach reply message if present
replyMessage: replyMessage || undefined,
}
})

if (shouldResetInputToolbar === true)
resetInputToolbar()

// Clear reply after sending
if (replyMessage)
clearReply()

onSend?.(newMessages)

setTimeout(() => scrollToBottom(), 10)
},
[messageIdGenerator, onSend, user, resetInputToolbar, scrollToBottom]
[messageIdGenerator, onSend, user, resetInputToolbar, scrollToBottom, replyMessage, clearReply]
)

const _onChangeText = useCallback(
Expand Down Expand Up @@ -214,6 +280,13 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
onChangeText: _onChangeText,
ref: textInputRef,
},
// Reply props
replyMessage,
onClearReply: clearReply,
renderReplyPreview,
replyPreviewContainerStyle,
replyPreviewUsernameStyle,
replyPreviewTextStyle,
}

if (renderInputToolbar)
Expand All @@ -230,6 +303,12 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
textInputRef,
textInputProps,
_onChangeText,
replyMessage,
clearReply,
renderReplyPreview,
replyPreviewContainerStyle,
replyPreviewUsernameStyle,
replyPreviewTextStyle,
])

const contextValues = useMemo(
Expand All @@ -251,6 +330,12 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
setText(props.text)
}, [props.text])

// Sync controlled reply message prop
useEffect(() => {
if (replyMessageProp !== undefined)
setInternalReplyMessage(replyMessageProp)
}, [replyMessageProp])

return (
<GiftedChatContext.Provider value={contextValues}>
<ActionSheetProvider ref={actionSheetRef}>
Expand Down
48 changes: 46 additions & 2 deletions src/GiftedChat/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
StyleProp,
TextStyle,
ViewStyle,
ImageStyle,
} from 'react-native'
import {
ActionSheetOptions,
Expand All @@ -15,16 +16,19 @@ import { BubbleProps } from '../Bubble'
import { ComposerProps } from '../Composer'
import { InputToolbarProps } from '../InputToolbar'
import { MessageImageProps } from '../MessageImage'
import { MessageReplyProps } from '../MessageReply'
import { AnimatedList, MessagesContainerProps } from '../MessagesContainer'
import { MessageTextProps } from '../MessageText'
import {
IMessage,
LeftRightStyle,
MessageAudioProps,
MessageVideoProps,
ReplyMessage,
User,
} from '../Models'
import { QuickRepliesProps } from '../QuickReplies'
import { ReplyPreviewProps } from '../ReplyPreview'
import { SendProps } from '../Send'
import { SystemMessageProps } from '../SystemMessage'
import { TimeProps } from '../Time'
Expand Down Expand Up @@ -85,7 +89,7 @@ export interface GiftedChatProps<TMessage extends IMessage> extends Partial<Mess
actionSheet?: () => {
showActionSheetWithOptions: (
options: ActionSheetOptions,
callback: (buttonIndex: number) => void | Promise<void>,
callback: (buttonIndex: number) => void | Promise<void>
) => void
}
/* Callback when a message avatar is tapped */
Expand Down Expand Up @@ -140,11 +144,51 @@ export interface GiftedChatProps<TMessage extends IMessage> extends Partial<Mess
/* Extra props to be passed to the MessageText component */
messageTextProps?: Partial<MessageTextProps<TMessage>>
renderQuickReplies?: (
quickReplies: QuickRepliesProps<TMessage>,
quickReplies: QuickRepliesProps<TMessage>
) => React.ReactNode
renderQuickReplySend?: () => React.ReactNode
keyboardProviderProps?: React.ComponentProps<typeof KeyboardProvider>
keyboardAvoidingViewProps?: KeyboardAvoidingViewProps
/** Enable animated day label that appears on scroll; default is true */
isDayAnimationEnabled?: boolean
/** Enable swipe to reply on messages; default is false */
isSwipeToReplyEnabled?: boolean
/** Swipe direction for reply; default is 'right' (swipe left to reveal) */
swipeToReplyDirection?: 'left' | 'right'
/** Callback when swipe to reply is triggered */
onSwipeToReply?: (message: TMessage) => void
/** Custom render for swipe action indicator */
renderSwipeToReplyAction?: (
progress: any,
dragX: any,
position: 'left' | 'right'
) => React.ReactNode
/** Style for the swipe action container */
swipeToReplyActionContainerStyle?: StyleProp<ViewStyle>
/** Reply message to show in input toolbar preview; default is undefined (controlled externally) or managed internally when using onSwipeToReply without replyMessage prop */
replyMessage?: ReplyMessage | null
/** Callback when reply is cleared */
onClearReply?: () => void
/** Custom render for reply preview in input toolbar */
renderReplyPreview?: (props: ReplyPreviewProps) => React.ReactNode
/** Style for reply preview container */
replyPreviewContainerStyle?: StyleProp<ViewStyle>
/** Style for reply preview username */
replyPreviewUsernameStyle?: StyleProp<TextStyle>
/** Style for reply preview text */
replyPreviewTextStyle?: StyleProp<TextStyle>
/** Custom render for message reply inside bubble */
renderMessageReply?: (props: MessageReplyProps<TMessage>) => React.ReactNode
/** Callback when message reply is pressed */
onPressMessageReply?: (replyMessage: ReplyMessage) => void
/** Style for message reply container */
messageReplyContainerStyle?: LeftRightStyle<ViewStyle>
/** Style for message reply content container */
messageReplyContentContainerStyle?: LeftRightStyle<ViewStyle>
/** Style for message reply image */
messageReplyImageStyle?: StyleProp<ImageStyle>
/** Style for message reply username */
messageReplyUsernameStyle?: StyleProp<TextStyle>
/** Style for message reply text */
messageReplyTextStyle?: StyleProp<TextStyle>
}
Loading
Loading