react-native-gifted-chat 3.2.3 → 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.
@@ -1,9 +1,13 @@
1
- import React, { useCallback } from 'react'
2
- import { View } from 'react-native'
1
+ import React, { useCallback, useMemo, useRef } from 'react'
2
+ import { View, StyleSheet } from 'react-native'
3
+ import ReanimatedSwipeable, { SwipeableMethods } from 'react-native-gesture-handler/ReanimatedSwipeable'
4
+ import Animated, { SharedValue, useAnimatedStyle } from 'react-native-reanimated'
3
5
 
4
6
  import { Avatar } from '../Avatar'
5
7
  import { Bubble } from '../Bubble'
8
+ import { Color } from '../Color'
6
9
  import { IMessage } from '../Models'
10
+ import { SwipeToReplyProps } from '../Reply'
7
11
  import { getStyleWithPosition } from '../styles'
8
12
  import { SystemMessage } from '../SystemMessage'
9
13
  import { isSameUser, renderComponentOrElement } from '../utils'
@@ -12,6 +16,44 @@ import { MessageProps } from './types'
12
16
 
13
17
  export * from './types'
14
18
 
19
+ interface ReplyIconProps {
20
+ progress: SharedValue<number>
21
+ translation: SharedValue<number>
22
+ direction: 'left' | 'right'
23
+ position: 'left' | 'right'
24
+ style?: SwipeToReplyProps<IMessage>['actionContainerStyle']
25
+ }
26
+
27
+ const ReplyIcon = ({ progress, direction, position, style }: ReplyIconProps) => {
28
+ const animatedStyle = useAnimatedStyle(() => {
29
+ 'worklet'
30
+
31
+ const scale = Math.min(progress.value, 1)
32
+ // When swiping left (icon on right), icon should move left (negative)
33
+ // When swiping right (icon on left), icon should move right (positive)
34
+ const translateX = direction === 'left'
35
+ ? Math.min(progress.value * -12, -12)
36
+ : Math.max(progress.value * 12, 12)
37
+
38
+ return {
39
+ transform: [{ scale }, { translateX }],
40
+ marginLeft: position === 'left' ? 0 : 16,
41
+ marginRight: position === 'right' ? 0 : 16,
42
+ }
43
+ })
44
+
45
+ return (
46
+ <Animated.View style={[localStyles.swipeActionContainer, animatedStyle, style]}>
47
+ <View style={localStyles.replyIconContainer}>
48
+ <View style={localStyles.replyIcon}>
49
+ <View style={localStyles.replyIconArrow} />
50
+ <View style={localStyles.replyIconLine} />
51
+ </View>
52
+ </View>
53
+ </Animated.View>
54
+ )
55
+ }
56
+
15
57
  export const Message = <TMessage extends IMessage = IMessage>(props: MessageProps<TMessage>) => {
16
58
  const {
17
59
  currentMessage,
@@ -23,13 +65,24 @@ export const Message = <TMessage extends IMessage = IMessage>(props: MessageProp
23
65
  containerStyle,
24
66
  user,
25
67
  isUserAvatarVisible,
68
+ swipeToReply,
26
69
  } = props
27
70
 
71
+ // Extract swipe props
72
+ const isSwipeToReplyEnabled = swipeToReply?.isEnabled ?? false
73
+ const swipeToReplyDirection = swipeToReply?.direction ?? 'left'
74
+ const onSwipeToReply = swipeToReply?.onSwipe
75
+ const renderSwipeToReplyActionProp = swipeToReply?.renderAction
76
+ const swipeToReplyActionContainerStyle = swipeToReply?.actionContainerStyle
77
+
78
+ const swipeableRef = useRef<SwipeableMethods>(null)
79
+
28
80
  const renderBubble = useCallback(() => {
29
81
  const {
30
82
  /* eslint-disable @typescript-eslint/no-unused-vars */
31
83
  containerStyle,
32
84
  onMessageLayout,
85
+ swipeToReply,
33
86
  /* eslint-enable @typescript-eslint/no-unused-vars */
34
87
  ...rest
35
88
  } = props
@@ -45,6 +98,7 @@ export const Message = <TMessage extends IMessage = IMessage>(props: MessageProp
45
98
  /* eslint-disable @typescript-eslint/no-unused-vars */
46
99
  containerStyle,
47
100
  onMessageLayout,
101
+ swipeToReply,
48
102
  /* eslint-enable @typescript-eslint/no-unused-vars */
49
103
  ...rest
50
104
  } = props
@@ -71,6 +125,7 @@ export const Message = <TMessage extends IMessage = IMessage>(props: MessageProp
71
125
  /* eslint-disable @typescript-eslint/no-unused-vars */
72
126
  containerStyle,
73
127
  onMessageLayout,
128
+ swipeToReply,
74
129
  /* eslint-enable @typescript-eslint/no-unused-vars */
75
130
  ...rest
76
131
  } = props
@@ -83,31 +138,136 @@ export const Message = <TMessage extends IMessage = IMessage>(props: MessageProp
83
138
  isUserAvatarVisible,
84
139
  ])
85
140
 
141
+ const renderSwipeAction = useCallback((
142
+ progress: SharedValue<number>,
143
+ translation: SharedValue<number>
144
+ ) => {
145
+ if (renderSwipeToReplyActionProp)
146
+ return renderSwipeToReplyActionProp(progress, translation, position)
147
+
148
+ return (
149
+ <ReplyIcon
150
+ progress={progress}
151
+ translation={translation}
152
+ direction={swipeToReplyDirection}
153
+ position={position}
154
+ style={swipeToReplyActionContainerStyle}
155
+ />
156
+ )
157
+ }, [position, renderSwipeToReplyActionProp, swipeToReplyDirection, swipeToReplyActionContainerStyle])
158
+
159
+ const handleSwipeableOpen = useCallback(() => {
160
+ swipeableRef.current?.close()
161
+ }, [])
162
+
163
+ const handleSwipeableWillOpen = useCallback(() => {
164
+ if (onSwipeToReply && currentMessage)
165
+ onSwipeToReply(currentMessage)
166
+ }, [onSwipeToReply, currentMessage])
167
+
168
+ const sameUser = useMemo(() =>
169
+ isSameUser(currentMessage, nextMessage!)
170
+ , [currentMessage, nextMessage])
171
+
172
+ const messageContent = useMemo(() => {
173
+ if (currentMessage?.system)
174
+ return renderSystemMessage()
175
+
176
+ return (
177
+ <View
178
+ style={[
179
+ getStyleWithPosition(styles, 'container', position),
180
+ { marginBottom: sameUser ? 2 : 10 },
181
+ !props.isInverted && { marginBottom: 2 },
182
+ containerStyle?.[position],
183
+ ]}
184
+ >
185
+ {position === 'left' && renderAvatar()}
186
+ {renderBubble()}
187
+ {position === 'right' && renderAvatar()}
188
+ </View>
189
+ )
190
+ }, [
191
+ currentMessage?.system,
192
+ renderSystemMessage,
193
+ position,
194
+ sameUser,
195
+ props.isInverted,
196
+ containerStyle,
197
+ renderAvatar,
198
+ renderBubble,
199
+ ])
200
+
86
201
  if (!currentMessage)
87
202
  return null
88
203
 
89
- const sameUser = isSameUser(currentMessage, nextMessage!)
204
+ // Don't wrap system messages in Swipeable
205
+ if (currentMessage.system || !isSwipeToReplyEnabled)
206
+ return (
207
+ <View onLayout={onMessageLayout}>
208
+ {messageContent}
209
+ </View>
210
+ )
90
211
 
91
212
  return (
92
213
  <View onLayout={onMessageLayout}>
93
- {currentMessage.system
94
- ? (
95
- renderSystemMessage()
96
- )
97
- : (
98
- <View
99
- style={[
100
- getStyleWithPosition(styles, 'container', position),
101
- { marginBottom: sameUser ? 2 : 10 },
102
- !props.isInverted && { marginBottom: 2 },
103
- containerStyle?.[position],
104
- ]}
105
- >
106
- {position === 'left' && renderAvatar()}
107
- {renderBubble()}
108
- {position === 'right' && renderAvatar()}
109
- </View>
110
- )}
214
+ <ReanimatedSwipeable
215
+ ref={swipeableRef}
216
+ friction={2}
217
+ overshootFriction={8}
218
+ renderRightActions={swipeToReplyDirection === 'left' ? renderSwipeAction : undefined}
219
+ renderLeftActions={swipeToReplyDirection === 'right' ? renderSwipeAction : undefined}
220
+ onSwipeableOpen={handleSwipeableOpen}
221
+ onSwipeableWillOpen={handleSwipeableWillOpen}
222
+ >
223
+ {messageContent}
224
+ </ReanimatedSwipeable>
111
225
  </View>
112
226
  )
113
227
  }
228
+
229
+ const localStyles = StyleSheet.create({
230
+ swipeActionContainer: {
231
+ width: 40,
232
+ justifyContent: 'center',
233
+ alignItems: 'center',
234
+ },
235
+ replyIconContainer: {
236
+ width: 28,
237
+ height: 28,
238
+ borderRadius: 14,
239
+ backgroundColor: Color.defaultBlue,
240
+ justifyContent: 'center',
241
+ alignItems: 'center',
242
+ },
243
+ replyIcon: {
244
+ width: 14,
245
+ height: 10,
246
+ transform: [{ scaleX: -1 }],
247
+ },
248
+ replyIconArrow: {
249
+ position: 'absolute',
250
+ top: 0,
251
+ left: 0,
252
+ width: 0,
253
+ height: 0,
254
+ borderTopWidth: 5,
255
+ borderTopColor: 'transparent',
256
+ borderBottomWidth: 5,
257
+ borderBottomColor: 'transparent',
258
+ borderRightWidth: 6,
259
+ borderRightColor: Color.white,
260
+ },
261
+ replyIconLine: {
262
+ position: 'absolute',
263
+ top: 3,
264
+ left: 5,
265
+ width: 9,
266
+ height: 4,
267
+ borderTopWidth: 2,
268
+ borderRightWidth: 2,
269
+ borderTopColor: Color.white,
270
+ borderRightColor: Color.white,
271
+ borderTopRightRadius: 4,
272
+ },
273
+ })
@@ -1,8 +1,10 @@
1
1
  import { ViewStyle, LayoutChangeEvent } from 'react-native'
2
+
2
3
  import { AvatarProps } from '../Avatar'
3
4
  import { BubbleProps } from '../Bubble'
4
5
  import { DayProps } from '../Day'
5
6
  import { IMessage, User, LeftRightStyle } from '../Models'
7
+ import { SwipeToReplyProps } from '../Reply'
6
8
  import { SystemMessageProps } from '../SystemMessage'
7
9
 
8
10
  export interface MessageProps<TMessage extends IMessage> {
@@ -19,4 +21,6 @@ export interface MessageProps<TMessage extends IMessage> {
19
21
  renderSystemMessage?: (props: SystemMessageProps<TMessage>) => React.ReactNode
20
22
  renderAvatar?: (props: AvatarProps<TMessage>) => React.ReactNode
21
23
  onMessageLayout?: (event: LayoutChangeEvent) => void
24
+ /** Swipe to reply configuration */
25
+ swipeToReply?: SwipeToReplyProps<TMessage>
22
26
  }
@@ -0,0 +1,160 @@
1
+ import React, { useMemo, useCallback } from 'react'
2
+ import {
3
+ StyleSheet,
4
+ ViewStyle,
5
+ View,
6
+ Pressable,
7
+ Image,
8
+ TextStyle,
9
+ StyleProp,
10
+ ImageStyle,
11
+ } from 'react-native'
12
+
13
+ import { Text } from 'react-native-gesture-handler'
14
+ import { Color } from './Color'
15
+ import { LeftRightStyle, IMessage, ReplyMessage } from './Models'
16
+ import { getStyleWithPosition } from './styles'
17
+
18
+ export interface MessageReplyProps<TMessage extends IMessage> {
19
+ position?: 'left' | 'right'
20
+ currentMessage: TMessage
21
+ containerStyle?: LeftRightStyle<ViewStyle>
22
+ contentContainerStyle?: LeftRightStyle<ViewStyle>
23
+ imageStyle?: StyleProp<ImageStyle>
24
+ usernameStyle?: StyleProp<TextStyle>
25
+ textStyle?: StyleProp<TextStyle>
26
+ onPress?: (replyMessage: ReplyMessage) => void
27
+ }
28
+
29
+ export function MessageReply<TMessage extends IMessage = IMessage> ({
30
+ currentMessage,
31
+ position = 'left',
32
+ containerStyle,
33
+ contentContainerStyle,
34
+ imageStyle,
35
+ usernameStyle,
36
+ textStyle,
37
+ onPress: onPressProp,
38
+ }: MessageReplyProps<TMessage>) {
39
+ const handlePress = useCallback(() => {
40
+ if (!onPressProp || !currentMessage.replyMessage)
41
+ return
42
+
43
+ onPressProp(currentMessage.replyMessage)
44
+ }, [onPressProp, currentMessage.replyMessage])
45
+
46
+ const containerStyleMemo = useMemo(() => [
47
+ styles.container,
48
+ getStyleWithPosition(styles, 'container', position),
49
+ containerStyle?.[position],
50
+ ], [position, containerStyle])
51
+
52
+ const contentContainerStyleMemo = useMemo(() => [
53
+ styles.contentContainer,
54
+ contentContainerStyle?.[position],
55
+ ], [position, contentContainerStyle])
56
+
57
+ const imageStyleMemo = useMemo(() => [
58
+ styles.image,
59
+ imageStyle,
60
+ ], [imageStyle])
61
+
62
+ const usernameStyleMemo = useMemo(() => [
63
+ styles.username,
64
+ getStyleWithPosition(styles, 'username', position),
65
+ usernameStyle,
66
+ ], [position, usernameStyle])
67
+
68
+ const textStyleMemo = useMemo(() => [
69
+ styles.text,
70
+ getStyleWithPosition(styles, 'text', position),
71
+ textStyle,
72
+ ], [position, textStyle])
73
+
74
+ if (!currentMessage.replyMessage)
75
+ return null
76
+
77
+ const { replyMessage } = currentMessage
78
+
79
+ return (
80
+ <Pressable
81
+ onPress={handlePress}
82
+ style={containerStyleMemo}
83
+ >
84
+ <View style={contentContainerStyleMemo}>
85
+ {replyMessage.image && (
86
+ <Image
87
+ source={{ uri: replyMessage.image }}
88
+ style={imageStyleMemo}
89
+ />
90
+ )}
91
+ <View style={styles.textContainer}>
92
+ <Text
93
+ style={usernameStyleMemo}
94
+ numberOfLines={1}
95
+ >
96
+ {replyMessage.user?.name || 'User'}
97
+ </Text>
98
+ <Text
99
+ numberOfLines={1}
100
+ style={textStyleMemo}
101
+ >
102
+ {replyMessage.text || (replyMessage.image ? 'Photo' : (replyMessage.audio ? 'Audio' : 'Message'))}
103
+ </Text>
104
+ </View>
105
+ </View>
106
+ </Pressable>
107
+ )
108
+ }
109
+
110
+ const styles = StyleSheet.create({
111
+ container: {
112
+ marginHorizontal: 10,
113
+ marginTop: 5,
114
+ marginBottom: 2,
115
+ padding: 8,
116
+ borderRadius: 8,
117
+ borderLeftWidth: 3,
118
+ borderLeftColor: Color.defaultBlue,
119
+ minWidth: 150,
120
+ },
121
+ container_left: {
122
+ backgroundColor: 'rgba(0, 0, 0, 0.05)',
123
+ },
124
+ container_right: {
125
+ backgroundColor: 'rgba(255, 255, 255, 0.15)',
126
+ },
127
+ contentContainer: {
128
+ flexDirection: 'row',
129
+ alignItems: 'center',
130
+ },
131
+ image: {
132
+ width: 36,
133
+ height: 36,
134
+ borderRadius: 4,
135
+ marginRight: 8,
136
+ },
137
+ textContainer: {
138
+ flex: 1,
139
+ },
140
+ username: {
141
+ fontSize: 13,
142
+ fontWeight: '600',
143
+ marginBottom: 2,
144
+ },
145
+ username_left: {
146
+ color: Color.defaultBlue,
147
+ },
148
+ username_right: {
149
+ color: Color.white,
150
+ },
151
+ text: {
152
+ fontSize: 13,
153
+ },
154
+ text_left: {
155
+ color: Color.black,
156
+ },
157
+ text_right: {
158
+ color: 'rgba(255, 255, 255, 0.8)',
159
+ },
160
+ })
@@ -159,6 +159,7 @@ export const Item = <TMessage extends IMessage>(props: ItemProps<TMessage>) => {
159
159
  const {
160
160
  renderMessage: renderMessageProp,
161
161
  isDayAnimationEnabled,
162
+ reply,
162
163
  /* eslint-disable @typescript-eslint/no-unused-vars */
163
164
  scrolledY: _scrolledY,
164
165
  daysPositions: _daysPositions,
@@ -167,16 +168,29 @@ export const Item = <TMessage extends IMessage>(props: ItemProps<TMessage>) => {
167
168
  ...rest
168
169
  } = props
169
170
 
171
+ // Transform reply props for Message and Bubble
172
+ const messageProps = useMemo(() => ({
173
+ ...rest,
174
+ // Swipe to reply for Message component
175
+ swipeToReply: reply?.swipe,
176
+ // Message reply styling for Bubble component
177
+ messageReply: reply ? {
178
+ renderMessageReply: reply.renderMessageReply,
179
+ onPress: reply.onPress,
180
+ ...reply.messageStyle,
181
+ } : undefined,
182
+ }), [rest, reply])
183
+
170
184
  return (
171
185
  // do not remove key. it helps to get correct position of the day container
172
186
  <View key={props.currentMessage._id.toString()}>
173
187
  {isDayAnimationEnabled
174
188
  ? <AnimatedDayWrapper<TMessage> {...props} />
175
- : <DayWrapper<TMessage> {...rest as MessageProps<TMessage>} />}
189
+ : <DayWrapper<TMessage> {...messageProps as MessageProps<TMessage>} />}
176
190
  {
177
191
  renderMessageProp
178
- ? renderMessageProp(rest as MessageProps<TMessage>)
179
- : <Message<TMessage> {...rest as MessageProps<TMessage>} />
192
+ ? renderMessageProp(messageProps as MessageProps<TMessage>)
193
+ : <Message<TMessage> {...messageProps as MessageProps<TMessage>} />
180
194
  }
181
195
  </View>
182
196
  )
@@ -18,7 +18,7 @@ import { DayAnimated } from './components/DayAnimated'
18
18
  import { Item } from './components/Item'
19
19
  import { ItemProps } from './components/Item/types'
20
20
  import styles from './styles'
21
- import { MessagesContainerProps, DaysPositions } from './types'
21
+ import { MessagesContainerProps, DaysPositions, AnimatedFlatList } from './types'
22
22
 
23
23
  export * from './types'
24
24
 
@@ -34,6 +34,7 @@ export const MessagesContainer = <TMessage extends IMessage>(props: MessagesCont
34
34
  scrollToBottomOffset = 200,
35
35
  isAlignedTop = false,
36
36
  scrollToBottomStyle,
37
+ scrollToBottomContentStyle,
37
38
  loadEarlierMessagesProps,
38
39
  renderTypingIndicator: renderTypingIndicatorProp,
39
40
  renderFooter: renderFooterProp,
@@ -162,18 +163,18 @@ export const MessagesContainer = <TMessage extends IMessage>(props: MessagesCont
162
163
  messageItem.user = { _id: 0 }
163
164
  }
164
165
 
165
- if (messages && user) {
166
+ if (messages) {
166
167
  const previousMessage =
167
168
  (isInverted ? messages[index + 1] : messages[index - 1]) || {}
168
169
  const nextMessage =
169
170
  (isInverted ? messages[index - 1] : messages[index + 1]) || {}
170
171
 
171
172
  const messageProps: ItemProps<TMessage> = {
173
+ position: user?._id != null && messageItem.user?._id === user._id ? 'right' : 'left',
172
174
  ...restProps,
173
175
  currentMessage: messageItem,
174
176
  previousMessage,
175
177
  nextMessage,
176
- position: messageItem.user._id === user._id ? 'right' : 'left',
177
178
  scrolledY,
178
179
  daysPositions,
179
180
  listHeight,
@@ -238,14 +239,14 @@ export const MessagesContainer = <TMessage extends IMessage>(props: MessagesCont
238
239
  style={[
239
240
  stylesCommon.centerItems,
240
241
  styles.scrollToBottomContent,
241
- scrollToBottomStyle,
242
+ scrollToBottomContentStyle,
242
243
  scrollToBottomStyleAnim,
243
244
  ]}
244
245
  >
245
246
  {renderScrollBottomComponent()}
246
247
  </Animated.View>
247
248
  )
248
- }, [scrollToBottomStyle, scrollToBottomStyleAnim, renderScrollBottomComponent])
249
+ }, [scrollToBottomStyleAnim, scrollToBottomContentStyle, renderScrollBottomComponent])
249
250
 
250
251
  const ScrollToBottomWrapper = useCallback(() => {
251
252
  if (!isScrollToBottomEnabled)
@@ -256,13 +257,13 @@ export const MessagesContainer = <TMessage extends IMessage>(props: MessagesCont
256
257
 
257
258
  return (
258
259
  <Pressable
259
- style={styles.scrollToBottom}
260
+ style={[styles.scrollToBottom, scrollToBottomStyle]}
260
261
  onPress={handleScrollToBottomPress}
261
262
  >
262
263
  {scrollToBottomContent}
263
264
  </Pressable>
264
265
  )
265
- }, [isScrollToBottomEnabled, isScrollToBottomVisible, handleScrollToBottomPress, scrollToBottomContent])
266
+ }, [isScrollToBottomEnabled, isScrollToBottomVisible, handleScrollToBottomPress, scrollToBottomContent, scrollToBottomStyle])
266
267
 
267
268
  const onLayoutList = useCallback((event: LayoutChangeEvent) => {
268
269
  listHeight.value = event.nativeEvent.layout.height
@@ -276,7 +277,9 @@ export const MessagesContainer = <TMessage extends IMessage>(props: MessagesCont
276
277
  doScrollToBottom(false)
277
278
  }, 500)
278
279
 
279
- 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)
280
283
  }, [isInverted, messages, doScrollToBottom, listHeight, listProps, isScrollToBottomEnabled])
281
284
 
282
285
  const onEndReached = useCallback(() => {
@@ -387,7 +390,7 @@ export const MessagesContainer = <TMessage extends IMessage>(props: MessagesCont
387
390
  isAlignedTop ? styles.containerAlignTop : stylesCommon.fill,
388
391
  ]}
389
392
  >
390
- <Animated.FlatList
393
+ <AnimatedFlatList
391
394
  ref={forwardRef}
392
395
  keyExtractor={keyExtractor}
393
396
  data={messages}
@@ -398,10 +401,10 @@ export const MessagesContainer = <TMessage extends IMessage>(props: MessagesCont
398
401
  contentContainerStyle={styles.messagesContainer}
399
402
  ListEmptyComponent={renderChatEmpty}
400
403
  ListFooterComponent={
401
- isInverted ? ListHeaderComponent : ListFooterComponent
404
+ isInverted ? ListHeaderComponent : <>{ListFooterComponent}</>
402
405
  }
403
406
  ListHeaderComponent={
404
- isInverted ? ListFooterComponent : ListHeaderComponent
407
+ isInverted ? <>{ListFooterComponent}</> : ListHeaderComponent
405
408
  }
406
409
  scrollEventThrottle={1}
407
410
  onEndReached={onEndReached}
@@ -422,6 +425,8 @@ export const MessagesContainer = <TMessage extends IMessage>(props: MessagesCont
422
425
  renderDay={renderDayProp}
423
426
  messages={messages}
424
427
  isLoading={loadEarlierMessagesProps?.isLoading ?? false}
428
+ dateFormat={props.dateFormat}
429
+ dateFormatCalendar={props.dateFormatCalendar}
425
430
  />
426
431
  )}
427
432
  </View>
@@ -4,21 +4,36 @@ import {
4
4
  StyleProp,
5
5
  ViewStyle,
6
6
  } from 'react-native'
7
- import Animated, { AnimatedProps, ScrollEvent } from 'react-native-reanimated'
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
 
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
+
15
30
  export type AnimatedListProps<TMessage extends IMessage = IMessage> = Partial<
16
- Omit<AnimatedProps<FlatListProps<TMessage>>, 'onScroll'> & {
31
+ Omit<FlatListProps<TMessage>, 'onScroll'> & {
17
32
  onScroll?: (event: ScrollEvent) => void
18
33
  }
19
34
  >
20
35
 
21
- export type AnimatedList<TMessage> = Animated.FlatList<TMessage>
36
+ export type AnimatedList<TMessage> = FlatList<TMessage>
22
37
 
23
38
  export interface MessagesContainerProps<TMessage extends IMessage = IMessage>
24
39
  extends Omit<TypingIndicatorProps, 'style'> {
@@ -26,6 +41,10 @@ export interface MessagesContainerProps<TMessage extends IMessage = IMessage>
26
41
  forwardRef?: RefObject<AnimatedList<TMessage>>
27
42
  /** Messages to display */
28
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
29
48
  /** User sending the messages: { _id, name, avatar } */
30
49
  user?: User
31
50
  /** Additional props for FlatList */
@@ -38,6 +57,8 @@ export interface MessagesContainerProps<TMessage extends IMessage = IMessage>
38
57
  isScrollToBottomEnabled?: boolean
39
58
  /** Scroll to bottom wrapper style */
40
59
  scrollToBottomStyle?: StyleProp<ViewStyle>
60
+ /** Scroll to bottom content style */
61
+ scrollToBottomContentStyle?: StyleProp<ViewStyle>
41
62
  /** Distance from bottom before showing scroll to bottom button */
42
63
  scrollToBottomOffset?: number
43
64
  /** Custom component to render when messages are empty */
@@ -62,6 +83,8 @@ export interface MessagesContainerProps<TMessage extends IMessage = IMessage>
62
83
  typingIndicatorStyle?: StyleProp<ViewStyle>
63
84
  /** Enable animated day label that appears on scroll; default is true */
64
85
  isDayAnimationEnabled?: boolean
86
+ /** Reply functionality configuration */
87
+ reply?: ReplyProps<TMessage>
65
88
  }
66
89
 
67
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'