react-native-gifted-chat 3.2.2 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +450 -156
  2. package/package.json +9 -8
  3. package/src/Bubble/index.tsx +34 -2
  4. package/src/Bubble/types.ts +17 -4
  5. package/src/Composer.tsx +1 -2
  6. package/src/Day/index.tsx +2 -2
  7. package/src/Day/types.ts +3 -2
  8. package/src/GiftedAvatar.tsx +1 -1
  9. package/src/GiftedChat/index.tsx +109 -23
  10. package/src/GiftedChat/types.ts +9 -3
  11. package/src/InputToolbar.tsx +62 -8
  12. package/src/Message/index.tsx +181 -21
  13. package/src/Message/types.ts +4 -0
  14. package/src/MessageReply.tsx +160 -0
  15. package/src/MessageText.tsx +2 -2
  16. package/src/MessagesContainer/components/DayAnimated/index.tsx +5 -1
  17. package/src/MessagesContainer/components/Item/index.tsx +82 -47
  18. package/src/MessagesContainer/index.tsx +30 -19
  19. package/src/MessagesContainer/styles.ts +2 -0
  20. package/src/MessagesContainer/types.ts +30 -3
  21. package/src/Models.ts +3 -0
  22. package/src/Reply/index.ts +1 -0
  23. package/src/Reply/types.ts +80 -0
  24. package/src/ReplyPreview.tsx +132 -0
  25. package/src/Send.tsx +8 -3
  26. package/src/SystemMessage.tsx +22 -16
  27. package/src/__tests__/MessageReply.test.tsx +54 -0
  28. package/src/__tests__/ReplyPreview.test.tsx +41 -0
  29. package/src/__tests__/__snapshots__/GiftedChat.test.tsx.snap +69 -42
  30. package/src/__tests__/__snapshots__/InputToolbar.test.tsx.snap +11 -15
  31. package/src/__tests__/__snapshots__/MessageImage.test.tsx.snap +24 -18
  32. package/src/__tests__/__snapshots__/MessageReply.test.tsx.snap +181 -0
  33. package/src/__tests__/__snapshots__/ReplyPreview.test.tsx.snap +403 -0
  34. package/src/__tests__/__snapshots__/Send.test.tsx.snap +3 -0
  35. package/src/__tests__/__snapshots__/SystemMessage.test.tsx.snap +36 -25
  36. package/src/components/MessageReply.tsx +156 -0
  37. package/src/components/ReplyPreview.tsx +230 -0
  38. package/src/index.ts +6 -1
  39. package/src/types.ts +17 -16
  40. package/src/utils.ts +11 -3
  41. package/CHANGELOG.md +0 -364
  42. package/src/reanimatedCompat.ts +0 -27
@@ -5,12 +5,11 @@ import {
5
5
  ListRenderItemInfo,
6
6
  CellRendererProps,
7
7
  } from 'react-native'
8
- import { FlatList, Pressable, Text } from 'react-native-gesture-handler'
9
- import Animated, { runOnJS, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'
8
+ import { Pressable, Text } from 'react-native-gesture-handler'
9
+ import Animated, { runOnJS, ScrollEvent, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'
10
10
  import { LoadEarlierMessages } from '../LoadEarlierMessages'
11
11
  import { warning } from '../logging'
12
12
  import { IMessage } from '../Models'
13
- import { ReanimatedScrollEvent } from '../reanimatedCompat'
14
13
  import stylesCommon from '../styles'
15
14
  import { TypingIndicator } from '../TypingIndicator'
16
15
  import { isSameDay, useCallbackThrottled } from '../utils'
@@ -19,12 +18,10 @@ import { DayAnimated } from './components/DayAnimated'
19
18
  import { Item } from './components/Item'
20
19
  import { ItemProps } from './components/Item/types'
21
20
  import styles from './styles'
22
- import { MessagesContainerProps, DaysPositions } from './types'
21
+ import { MessagesContainerProps, DaysPositions, AnimatedFlatList } from './types'
23
22
 
24
23
  export * from './types'
25
24
 
26
- const AnimatedFlatList = Animated.createAnimatedComponent(FlatList) as React.ComponentType<any>
27
-
28
25
  export const MessagesContainer = <TMessage extends IMessage>(props: MessagesContainerProps<TMessage>) => {
29
26
  const {
30
27
  messages = [],
@@ -37,6 +34,7 @@ export const MessagesContainer = <TMessage extends IMessage>(props: MessagesCont
37
34
  scrollToBottomOffset = 200,
38
35
  isAlignedTop = false,
39
36
  scrollToBottomStyle,
37
+ scrollToBottomContentStyle,
40
38
  loadEarlierMessagesProps,
41
39
  renderTypingIndicator: renderTypingIndicatorProp,
42
40
  renderFooter: renderFooterProp,
@@ -114,8 +112,8 @@ export const MessagesContainer = <TMessage extends IMessage>(props: MessagesCont
114
112
  forwardRef.current.scrollToEnd({ animated })
115
113
  }, [forwardRef, isInverted, scrollTo, isScrollingDown, changeScrollToBottomVisibility])
116
114
 
117
- const handleOnScroll = useCallback((event: ReanimatedScrollEvent) => {
118
- listPropsOnScrollProp?.(event as any)
115
+ const handleOnScroll = useCallback((event: ScrollEvent) => {
116
+ listPropsOnScrollProp?.(event)
119
117
 
120
118
  const {
121
119
  contentOffset: { y: contentOffsetY },
@@ -165,18 +163,18 @@ export const MessagesContainer = <TMessage extends IMessage>(props: MessagesCont
165
163
  messageItem.user = { _id: 0 }
166
164
  }
167
165
 
168
- if (messages && user) {
166
+ if (messages) {
169
167
  const previousMessage =
170
168
  (isInverted ? messages[index + 1] : messages[index - 1]) || {}
171
169
  const nextMessage =
172
170
  (isInverted ? messages[index - 1] : messages[index + 1]) || {}
173
171
 
174
172
  const messageProps: ItemProps<TMessage> = {
173
+ position: user?._id != null && messageItem.user?._id === user._id ? 'right' : 'left',
175
174
  ...restProps,
176
175
  currentMessage: messageItem,
177
176
  previousMessage,
178
177
  nextMessage,
179
- position: messageItem.user._id === user._id ? 'right' : 'left',
180
178
  scrolledY,
181
179
  daysPositions,
182
180
  listHeight,
@@ -241,14 +239,14 @@ export const MessagesContainer = <TMessage extends IMessage>(props: MessagesCont
241
239
  style={[
242
240
  stylesCommon.centerItems,
243
241
  styles.scrollToBottomContent,
244
- scrollToBottomStyle,
242
+ scrollToBottomContentStyle,
245
243
  scrollToBottomStyleAnim,
246
244
  ]}
247
245
  >
248
246
  {renderScrollBottomComponent()}
249
247
  </Animated.View>
250
248
  )
251
- }, [scrollToBottomStyle, scrollToBottomStyleAnim, renderScrollBottomComponent])
249
+ }, [scrollToBottomStyleAnim, scrollToBottomContentStyle, renderScrollBottomComponent])
252
250
 
253
251
  const ScrollToBottomWrapper = useCallback(() => {
254
252
  if (!isScrollToBottomEnabled)
@@ -259,13 +257,13 @@ export const MessagesContainer = <TMessage extends IMessage>(props: MessagesCont
259
257
 
260
258
  return (
261
259
  <Pressable
262
- style={styles.scrollToBottom}
260
+ style={[styles.scrollToBottom, scrollToBottomStyle]}
263
261
  onPress={handleScrollToBottomPress}
264
262
  >
265
263
  {scrollToBottomContent}
266
264
  </Pressable>
267
265
  )
268
- }, [isScrollToBottomEnabled, isScrollToBottomVisible, handleScrollToBottomPress, scrollToBottomContent])
266
+ }, [isScrollToBottomEnabled, isScrollToBottomVisible, handleScrollToBottomPress, scrollToBottomContent, scrollToBottomStyle])
269
267
 
270
268
  const onLayoutList = useCallback((event: LayoutChangeEvent) => {
271
269
  listHeight.value = event.nativeEvent.layout.height
@@ -279,7 +277,9 @@ export const MessagesContainer = <TMessage extends IMessage>(props: MessagesCont
279
277
  doScrollToBottom(false)
280
278
  }, 500)
281
279
 
282
- listProps?.onLayout?.(event)
280
+ // listProps.onLayout may be a SharedValue in Reanimated types, but we only accept functions
281
+ const onLayoutProp = listProps?.onLayout as ((event: LayoutChangeEvent) => void) | undefined
282
+ onLayoutProp?.(event)
283
283
  }, [isInverted, messages, doScrollToBottom, listHeight, listProps, isScrollToBottomEnabled])
284
284
 
285
285
  const onEndReached = useCallback(() => {
@@ -301,6 +301,10 @@ export const MessagesContainer = <TMessage extends IMessage>(props: MessagesCont
301
301
  const handleOnLayout = (event: LayoutChangeEvent) => {
302
302
  onLayoutProp?.(event)
303
303
 
304
+ // Only track positions when day animation is enabled
305
+ if (!isDayAnimationEnabled)
306
+ return
307
+
304
308
  const { y, height } = event.nativeEvent.layout
305
309
 
306
310
  const newValue = {
@@ -343,7 +347,7 @@ export const MessagesContainer = <TMessage extends IMessage>(props: MessagesCont
343
347
  {children}
344
348
  </View>
345
349
  )
346
- }, [daysPositions, isInverted])
350
+ }, [daysPositions, isInverted, isDayAnimationEnabled])
347
351
 
348
352
  const scrollHandler = useAnimatedScrollHandler({
349
353
  onScroll: event => {
@@ -355,6 +359,10 @@ export const MessagesContainer = <TMessage extends IMessage>(props: MessagesCont
355
359
 
356
360
  // removes unrendered days positions when messages are added/removed
357
361
  useEffect(() => {
362
+ // Skip cleanup when day animation is disabled
363
+ if (!isDayAnimationEnabled)
364
+ return
365
+
358
366
  Object.keys(daysPositions.value).forEach(key => {
359
367
  const messageIndex = messages.findIndex(m => m._id.toString() === key)
360
368
  let shouldRemove = messageIndex === -1
@@ -373,7 +381,7 @@ export const MessagesContainer = <TMessage extends IMessage>(props: MessagesCont
373
381
  return value
374
382
  })
375
383
  })
376
- }, [messages, daysPositions, isInverted])
384
+ }, [messages, daysPositions, isInverted, isDayAnimationEnabled])
377
385
 
378
386
  return (
379
387
  <View
@@ -390,12 +398,13 @@ export const MessagesContainer = <TMessage extends IMessage>(props: MessagesCont
390
398
  inverted={isInverted}
391
399
  automaticallyAdjustContentInsets={false}
392
400
  style={stylesCommon.fill}
401
+ contentContainerStyle={styles.messagesContainer}
393
402
  ListEmptyComponent={renderChatEmpty}
394
403
  ListFooterComponent={
395
- isInverted ? ListHeaderComponent : ListFooterComponent
404
+ isInverted ? ListHeaderComponent : <>{ListFooterComponent}</>
396
405
  }
397
406
  ListHeaderComponent={
398
- isInverted ? ListFooterComponent : ListHeaderComponent
407
+ isInverted ? <>{ListFooterComponent}</> : ListHeaderComponent
399
408
  }
400
409
  scrollEventThrottle={1}
401
410
  onEndReached={onEndReached}
@@ -416,6 +425,8 @@ export const MessagesContainer = <TMessage extends IMessage>(props: MessagesCont
416
425
  renderDay={renderDayProp}
417
426
  messages={messages}
418
427
  isLoading={loadEarlierMessagesProps?.isLoading ?? false}
428
+ dateFormat={props.dateFormat}
429
+ dateFormatCalendar={props.dateFormatCalendar}
419
430
  />
420
431
  )}
421
432
  </View>
@@ -9,6 +9,8 @@ export default StyleSheet.create({
9
9
  contentContainerStyle: {
10
10
  flexGrow: 1,
11
11
  justifyContent: 'flex-start',
12
+ },
13
+ messagesContainer: {
12
14
  paddingBottom: 10,
13
15
  },
14
16
  emptyChatContainer: {
@@ -1,18 +1,37 @@
1
- import React, { RefObject } from 'react'
1
+ import { RefObject } from 'react'
2
2
  import {
3
3
  FlatListProps,
4
4
  StyleProp,
5
5
  ViewStyle,
6
6
  } from 'react-native'
7
7
  import { FlatList } from 'react-native-gesture-handler'
8
+ import Animated, { ScrollEvent } from 'react-native-reanimated'
8
9
 
9
10
  import { DayProps } from '../Day'
10
11
  import { LoadEarlierMessagesProps } from '../LoadEarlierMessages'
11
12
  import { MessageProps } from '../Message'
12
13
  import { User, IMessage, Reply } from '../Models'
14
+ import { ReplyProps } from '../Reply'
13
15
  import { TypingIndicatorProps } from '../TypingIndicator/types'
14
16
 
15
- export type ListProps<TMessage extends IMessage = IMessage> = Partial<FlatListProps<TMessage>>
17
+ /** Animated FlatList created from react-native-gesture-handler's FlatList */
18
+ const RNGHAnimatedFlatList = Animated.createAnimatedComponent(FlatList)
19
+
20
+ /**
21
+ * Typed AnimatedFlatList component that preserves generic type parameter.
22
+ * Uses react-native-gesture-handler's FlatList which respects keyboardShouldPersistTaps.
23
+ */
24
+ export const AnimatedFlatList = RNGHAnimatedFlatList as <TMessage>(
25
+ props: FlatListProps<TMessage> & {
26
+ ref?: RefObject<FlatList<TMessage>>
27
+ }
28
+ ) => React.ReactElement
29
+
30
+ export type AnimatedListProps<TMessage extends IMessage = IMessage> = Partial<
31
+ Omit<FlatListProps<TMessage>, 'onScroll'> & {
32
+ onScroll?: (event: ScrollEvent) => void
33
+ }
34
+ >
16
35
 
17
36
  export type AnimatedList<TMessage> = FlatList<TMessage>
18
37
 
@@ -22,10 +41,14 @@ export interface MessagesContainerProps<TMessage extends IMessage = IMessage>
22
41
  forwardRef?: RefObject<AnimatedList<TMessage>>
23
42
  /** Messages to display */
24
43
  messages?: TMessage[]
44
+ /** Format to use for rendering dates; default is 'll' */
45
+ dateFormat?: string
46
+ /** Format to use for rendering relative times */
47
+ dateFormatCalendar?: object
25
48
  /** User sending the messages: { _id, name, avatar } */
26
49
  user?: User
27
50
  /** Additional props for FlatList */
28
- listProps?: ListProps<TMessage>
51
+ listProps?: AnimatedListProps<TMessage>
29
52
  /** Reverses display order of messages; default is true */
30
53
  isInverted?: boolean
31
54
  /** Controls whether or not the message bubbles appear at the top of the chat */
@@ -34,6 +57,8 @@ export interface MessagesContainerProps<TMessage extends IMessage = IMessage>
34
57
  isScrollToBottomEnabled?: boolean
35
58
  /** Scroll to bottom wrapper style */
36
59
  scrollToBottomStyle?: StyleProp<ViewStyle>
60
+ /** Scroll to bottom content style */
61
+ scrollToBottomContentStyle?: StyleProp<ViewStyle>
37
62
  /** Distance from bottom before showing scroll to bottom button */
38
63
  scrollToBottomOffset?: number
39
64
  /** Custom component to render when messages are empty */
@@ -58,6 +83,8 @@ export interface MessagesContainerProps<TMessage extends IMessage = IMessage>
58
83
  typingIndicatorStyle?: StyleProp<ViewStyle>
59
84
  /** Enable animated day label that appears on scroll; default is true */
60
85
  isDayAnimationEnabled?: boolean
86
+ /** Reply functionality configuration */
87
+ reply?: ReplyProps<TMessage>
61
88
  }
62
89
 
63
90
  export interface State {
package/src/Models.ts CHANGED
@@ -27,6 +27,8 @@ export interface QuickReplies {
27
27
  keepIt?: boolean
28
28
  }
29
29
 
30
+ export interface ReplyMessage extends Pick<IMessage, '_id' | 'text' | 'user' | 'audio' | 'image'> {}
31
+
30
32
  export interface IMessage {
31
33
  _id: string | number
32
34
  text: string
@@ -40,6 +42,7 @@ export interface IMessage {
40
42
  received?: boolean
41
43
  pending?: boolean
42
44
  quickReplies?: QuickReplies
45
+ replyMessage?: ReplyMessage
43
46
  location?: {
44
47
  latitude: number
45
48
  longitude: number
@@ -0,0 +1 @@
1
+ export * from './types'
@@ -0,0 +1,80 @@
1
+ import { ImageStyle, StyleProp, TextStyle, ViewStyle } from 'react-native'
2
+ import { SharedValue } from 'react-native-reanimated'
3
+
4
+ import { MessageReplyProps } from '../components/MessageReply'
5
+ import { ReplyPreviewProps } from '../components/ReplyPreview'
6
+ import { IMessage, ReplyMessage } from '../Models'
7
+
8
+ /**
9
+ * Props for swipe-to-reply gesture behavior
10
+ */
11
+ export interface SwipeToReplyProps<TMessage extends IMessage> {
12
+ /** Enable swipe to reply on messages; default is false */
13
+ isEnabled?: boolean
14
+ /** Direction to swipe for reply; default is 'left' (swipe left, icon appears on right) */
15
+ direction?: 'left' | 'right'
16
+ /** Callback when swipe to reply is triggered */
17
+ onSwipe?: (message: TMessage) => void
18
+ /** Custom render for swipe action indicator */
19
+ renderAction?: (
20
+ progress: SharedValue<number>,
21
+ translation: SharedValue<number>,
22
+ position: 'left' | 'right'
23
+ ) => React.ReactNode
24
+ /** Style for the swipe action container */
25
+ actionContainerStyle?: StyleProp<ViewStyle>
26
+ }
27
+
28
+ /**
29
+ * Props for reply preview shown above input toolbar
30
+ */
31
+ export interface ReplyPreviewStyleProps {
32
+ /** Style for reply preview container */
33
+ containerStyle?: StyleProp<ViewStyle>
34
+ /** Style for reply preview text */
35
+ textStyle?: StyleProp<TextStyle>
36
+ /** Style for reply preview image */
37
+ imageStyle?: StyleProp<ImageStyle>
38
+ }
39
+
40
+ /**
41
+ * Props for message reply display inside bubble
42
+ */
43
+ export interface MessageReplyStyleProps {
44
+ /** Style for message reply container */
45
+ containerStyle?: StyleProp<ViewStyle>
46
+ /** Style for message reply container on left side */
47
+ containerStyleLeft?: StyleProp<ViewStyle>
48
+ /** Style for message reply container on right side */
49
+ containerStyleRight?: StyleProp<ViewStyle>
50
+ /** Style for message reply image */
51
+ imageStyle?: StyleProp<ImageStyle>
52
+ /** Style for message reply text */
53
+ textStyle?: StyleProp<TextStyle>
54
+ /** Style for message reply text on left side */
55
+ textStyleLeft?: StyleProp<TextStyle>
56
+ /** Style for message reply text on right side */
57
+ textStyleRight?: StyleProp<TextStyle>
58
+ }
59
+
60
+ /**
61
+ * Grouped props for reply functionality
62
+ */
63
+ export interface ReplyProps<TMessage extends IMessage> {
64
+ /** Reply message to show in input toolbar preview */
65
+ message?: ReplyMessage | null
66
+ /** Callback when reply is cleared */
67
+ onClear?: () => void
68
+ /** Callback when message reply is pressed inside bubble */
69
+ onPress?: (replyMessage: ReplyMessage) => void
70
+ /** Custom render for reply preview in input toolbar */
71
+ renderPreview?: (props: ReplyPreviewProps) => React.ReactNode
72
+ /** Custom render for message reply inside bubble */
73
+ renderMessageReply?: (props: MessageReplyProps<TMessage>) => React.ReactNode
74
+ /** Swipe-to-reply configuration */
75
+ swipe?: SwipeToReplyProps<TMessage>
76
+ /** Reply preview styling */
77
+ previewStyle?: ReplyPreviewStyleProps
78
+ /** Message reply styling */
79
+ messageStyle?: MessageReplyStyleProps
80
+ }
@@ -0,0 +1,132 @@
1
+ import React, { useMemo } from 'react'
2
+ import { StyleSheet, View, StyleProp, ViewStyle, TextStyle, Pressable } from 'react-native'
3
+
4
+ import { Text } from 'react-native-gesture-handler'
5
+ import { Color } from './Color'
6
+ import { useColorScheme } from './hooks/useColorScheme'
7
+ import { ReplyMessage } from './Models'
8
+
9
+ export interface ReplyPreviewProps {
10
+ replyMessage: ReplyMessage
11
+ onClearReply: () => void
12
+ containerStyle?: StyleProp<ViewStyle>
13
+ usernameStyle?: StyleProp<TextStyle>
14
+ textStyle?: StyleProp<TextStyle>
15
+ clearButtonStyle?: StyleProp<ViewStyle>
16
+ clearButtonTextStyle?: StyleProp<TextStyle>
17
+ }
18
+
19
+ export function ReplyPreview ({
20
+ replyMessage,
21
+ onClearReply,
22
+ containerStyle,
23
+ usernameStyle,
24
+ textStyle,
25
+ clearButtonStyle,
26
+ clearButtonTextStyle,
27
+ }: ReplyPreviewProps) {
28
+ const colorScheme = useColorScheme()
29
+
30
+ const containerStyles = useMemo(() => [
31
+ styles.container,
32
+ colorScheme === 'dark' && styles.container_dark,
33
+ containerStyle,
34
+ ], [colorScheme, containerStyle])
35
+
36
+ const usernameStyles = useMemo(() => [
37
+ styles.username,
38
+ colorScheme === 'dark' && styles.username_dark,
39
+ usernameStyle,
40
+ ], [colorScheme, usernameStyle])
41
+
42
+ const textStyles = useMemo(() => [
43
+ styles.text,
44
+ colorScheme === 'dark' && styles.text_dark,
45
+ textStyle,
46
+ ], [colorScheme, textStyle])
47
+
48
+ return (
49
+ <View style={containerStyles}>
50
+ <View style={styles.border} />
51
+ <View style={styles.content}>
52
+ <Text
53
+ style={usernameStyles}
54
+ numberOfLines={1}
55
+ >
56
+ {replyMessage.user?.name || 'User'}
57
+ </Text>
58
+ <Text
59
+ style={textStyles}
60
+ numberOfLines={1}
61
+ >
62
+ {replyMessage.text || (replyMessage.image ? 'Photo' : (replyMessage.audio ? 'Audio' : 'Message'))}
63
+ </Text>
64
+ </View>
65
+ <Pressable
66
+ onPress={onClearReply}
67
+ style={[styles.clearButton, clearButtonStyle]}
68
+ hitSlop={8}
69
+ >
70
+ <Text style={[styles.clearButtonText, clearButtonTextStyle]}>
71
+ {'✕'}
72
+ </Text>
73
+ </Pressable>
74
+ </View>
75
+ )
76
+ }
77
+
78
+ const styles = StyleSheet.create({
79
+ container: {
80
+ flexDirection: 'row',
81
+ alignItems: 'center',
82
+ paddingHorizontal: 10,
83
+ paddingVertical: 8,
84
+ backgroundColor: '#f5f5f5',
85
+ borderBottomWidth: StyleSheet.hairlineWidth,
86
+ borderBottomColor: Color.defaultColor,
87
+ },
88
+ container_dark: {
89
+ backgroundColor: '#2a2a2a',
90
+ borderBottomColor: '#444',
91
+ },
92
+ border: {
93
+ width: 3,
94
+ height: '100%',
95
+ backgroundColor: Color.defaultBlue,
96
+ borderRadius: 1.5,
97
+ marginRight: 10,
98
+ },
99
+ content: {
100
+ flex: 1,
101
+ },
102
+ username: {
103
+ fontSize: 13,
104
+ fontWeight: '600',
105
+ color: Color.defaultBlue,
106
+ marginBottom: 2,
107
+ },
108
+ username_dark: {
109
+ color: '#6eb5ff',
110
+ },
111
+ text: {
112
+ fontSize: 13,
113
+ color: '#666',
114
+ },
115
+ text_dark: {
116
+ color: '#999',
117
+ },
118
+ clearButton: {
119
+ width: 24,
120
+ height: 24,
121
+ borderRadius: 12,
122
+ backgroundColor: Color.defaultColor,
123
+ justifyContent: 'center',
124
+ alignItems: 'center',
125
+ marginLeft: 10,
126
+ },
127
+ clearButtonText: {
128
+ fontSize: 12,
129
+ fontWeight: '600',
130
+ color: '#666',
131
+ },
132
+ })
package/src/Send.tsx CHANGED
@@ -21,7 +21,10 @@ export interface SendProps<TMessage extends IMessage> {
21
21
  containerStyle?: StyleProp<ViewStyle>
22
22
  textStyle?: StyleProp<TextStyle>
23
23
  children?: React.ReactNode
24
+ /** Always show send button, even when text is empty */
24
25
  isSendButtonAlwaysVisible?: boolean
26
+ /** Text is optional, allow sending empty messages (useful for media-only messages) */
27
+ isTextOptional?: boolean
25
28
  sendButtonProps?: Partial<TouchableOpacityProps>
26
29
  onSend?(
27
30
  messages: Partial<TMessage> | Partial<TMessage>[],
@@ -36,6 +39,7 @@ export const Send = <TMessage extends IMessage = IMessage>({
36
39
  textStyle,
37
40
  label = 'Send',
38
41
  isSendButtonAlwaysVisible = false,
42
+ isTextOptional = false,
39
43
  sendButtonProps,
40
44
  onSend,
41
45
  }: SendProps<TMessage>) => {
@@ -43,11 +47,12 @@ export const Send = <TMessage extends IMessage = IMessage>({
43
47
  const opacity = useSharedValue(0)
44
48
 
45
49
  const handleOnPress = useCallback(() => {
46
- const message = { text: text?.trim() } as Partial<TMessage>
50
+ const trimmedText = text?.trim() ?? ''
51
+ const message = { text: trimmedText } as Partial<TMessage>
47
52
 
48
- if (onSend && message.text?.length)
53
+ if (onSend && (trimmedText.length || isTextOptional))
49
54
  onSend(message, true)
50
- }, [text, onSend])
55
+ }, [text, onSend, isTextOptional])
51
56
 
52
57
  const isVisible = useMemo(
53
58
  () => isSendButtonAlwaysVisible || !!text?.trim().length,
@@ -32,34 +32,39 @@ export function SystemMessage<TMessage extends IMessage> ({
32
32
  return null
33
33
 
34
34
  return (
35
- <View style={[stylesCommon.fill, styles.container, containerStyle]}>
36
- {
37
- !!currentMessage.text && (
38
- <MessageText
39
- currentMessage={currentMessage}
40
- customTextStyle={[styles.text, textStyle]}
41
- position='left'
42
- containerStyle={{ left: [styles.messageContainer, messageContainerStyle] }}
43
- {...messageTextProps}
44
- />
45
- )
46
- }
47
- {children}
35
+ <View style={[stylesCommon.fill, styles.wrapper]}>
36
+ <View style={[styles.container, containerStyle]}>
37
+ {
38
+ !!currentMessage.text && (
39
+ <MessageText
40
+ currentMessage={currentMessage}
41
+ customTextStyle={[styles.text, textStyle]}
42
+ position='left'
43
+ containerStyle={{ left: [styles.messageContainer, messageContainerStyle] }}
44
+ {...messageTextProps}
45
+ />
46
+ )
47
+ }
48
+ {children}
49
+ </View>
48
50
  </View>
49
51
  )
50
52
  }
51
53
 
52
54
  const styles = StyleSheet.create({
55
+ wrapper: {
56
+ alignItems: 'center',
57
+ marginVertical: 5,
58
+ marginHorizontal: 10,
59
+ },
53
60
  container: {
61
+ maxWidth: '70%',
54
62
  borderRadius: 20,
55
63
  borderWidth: 1,
56
64
  borderColor: 'rgba(0,0,0,0.1)',
57
65
  paddingHorizontal: 10,
58
66
  paddingVertical: 10,
59
67
  backgroundColor: 'rgba(0,0,0,0.05)',
60
- marginVertical: 5,
61
- marginHorizontal: 10,
62
- alignItems: 'flex-end',
63
68
  },
64
69
  messageContainer: {
65
70
  marginVertical: 0,
@@ -69,5 +74,6 @@ const styles = StyleSheet.create({
69
74
  backgroundColor: Color.backgroundTransparent,
70
75
  fontSize: 12,
71
76
  fontWeight: '300',
77
+ textAlign: 'center',
72
78
  },
73
79
  })
@@ -0,0 +1,54 @@
1
+ import React from 'react'
2
+ import { render } from '@testing-library/react-native'
3
+
4
+ import { MessageReply } from '../components/MessageReply'
5
+ import { IMessage, ReplyMessage } from '../Models'
6
+
7
+ const replyMessage: ReplyMessage = {
8
+ _id: 'reply-1',
9
+ text: 'Original message text',
10
+ user: {
11
+ _id: 2,
12
+ name: 'John Doe',
13
+ },
14
+ }
15
+
16
+ const currentMessage: IMessage = {
17
+ _id: 'msg-1',
18
+ text: 'Reply text',
19
+ createdAt: new Date(),
20
+ user: {
21
+ _id: 1,
22
+ name: 'Jane Doe',
23
+ },
24
+ replyMessage,
25
+ }
26
+
27
+ it('should render <MessageReply /> and compare with snapshot', () => {
28
+ const { toJSON } = render(
29
+ <MessageReply
30
+ replyMessage={replyMessage}
31
+ currentMessage={currentMessage}
32
+ position='left'
33
+ />
34
+ )
35
+
36
+ expect(toJSON()).toMatchSnapshot()
37
+ })
38
+
39
+ it('should render <MessageReply /> on right position and compare with snapshot', () => {
40
+ const currentMessageFromCurrentUser: IMessage = {
41
+ ...currentMessage,
42
+ user: replyMessage.user,
43
+ }
44
+
45
+ const { toJSON } = render(
46
+ <MessageReply
47
+ replyMessage={replyMessage}
48
+ currentMessage={currentMessageFromCurrentUser}
49
+ position='right'
50
+ />
51
+ )
52
+
53
+ expect(toJSON()).toMatchSnapshot()
54
+ })
@@ -0,0 +1,41 @@
1
+ import React from 'react'
2
+ import { render } from '@testing-library/react-native'
3
+
4
+ import { ReplyPreview } from '../components/ReplyPreview'
5
+ import { ReplyMessage } from '../Models'
6
+
7
+ const replyMessage: ReplyMessage = {
8
+ _id: 'reply-1',
9
+ text: 'Original message to reply to',
10
+ user: {
11
+ _id: 2,
12
+ name: 'John Doe',
13
+ },
14
+ }
15
+
16
+ it('should render <ReplyPreview /> and compare with snapshot', () => {
17
+ const { toJSON } = render(
18
+ <ReplyPreview
19
+ replyMessage={replyMessage}
20
+ onClearReply={() => {}}
21
+ />
22
+ )
23
+
24
+ expect(toJSON()).toMatchSnapshot()
25
+ })
26
+
27
+ it('should render <ReplyPreview /> with image and compare with snapshot', () => {
28
+ const replyWithImage: ReplyMessage = {
29
+ ...replyMessage,
30
+ image: 'https://example.com/image.jpg',
31
+ }
32
+
33
+ const { toJSON } = render(
34
+ <ReplyPreview
35
+ replyMessage={replyWithImage}
36
+ onClearReply={() => {}}
37
+ />
38
+ )
39
+
40
+ expect(toJSON()).toMatchSnapshot()
41
+ })