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.
- package/README.md +450 -156
- package/package.json +9 -8
- package/src/Bubble/index.tsx +34 -2
- package/src/Bubble/types.ts +17 -4
- package/src/Day/index.tsx +2 -2
- package/src/Day/types.ts +3 -2
- package/src/GiftedChat/index.tsx +109 -23
- package/src/GiftedChat/types.ts +9 -3
- package/src/InputToolbar.tsx +62 -8
- package/src/Message/index.tsx +181 -21
- package/src/Message/types.ts +4 -0
- package/src/MessageReply.tsx +160 -0
- package/src/MessagesContainer/components/Item/index.tsx +17 -3
- package/src/MessagesContainer/index.tsx +16 -11
- package/src/MessagesContainer/types.ts +26 -3
- package/src/Models.ts +3 -0
- package/src/Reply/index.ts +1 -0
- package/src/Reply/types.ts +80 -0
- package/src/ReplyPreview.tsx +132 -0
- package/src/Send.tsx +8 -3
- package/src/__tests__/MessageReply.test.tsx +54 -0
- package/src/__tests__/ReplyPreview.test.tsx +41 -0
- package/src/__tests__/__snapshots__/GiftedChat.test.tsx.snap +69 -42
- package/src/__tests__/__snapshots__/InputToolbar.test.tsx.snap +11 -15
- package/src/__tests__/__snapshots__/MessageImage.test.tsx.snap +24 -18
- package/src/__tests__/__snapshots__/MessageReply.test.tsx.snap +181 -0
- package/src/__tests__/__snapshots__/ReplyPreview.test.tsx.snap +403 -0
- package/src/__tests__/__snapshots__/Send.test.tsx.snap +3 -0
- package/src/components/MessageReply.tsx +156 -0
- package/src/components/ReplyPreview.tsx +230 -0
- package/src/index.ts +6 -1
- package/src/types.ts +17 -16
- package/src/utils.ts +11 -3
package/src/Message/index.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
})
|
package/src/Message/types.ts
CHANGED
|
@@ -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> {...
|
|
189
|
+
: <DayWrapper<TMessage> {...messageProps as MessageProps<TMessage>} />}
|
|
176
190
|
{
|
|
177
191
|
renderMessageProp
|
|
178
|
-
? renderMessageProp(
|
|
179
|
-
: <Message<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
|
|
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
|
-
|
|
242
|
+
scrollToBottomContentStyle,
|
|
242
243
|
scrollToBottomStyleAnim,
|
|
243
244
|
]}
|
|
244
245
|
>
|
|
245
246
|
{renderScrollBottomComponent()}
|
|
246
247
|
</Animated.View>
|
|
247
248
|
)
|
|
248
|
-
}, [
|
|
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
|
|
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
|
-
<
|
|
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
|
|
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<
|
|
31
|
+
Omit<FlatListProps<TMessage>, 'onScroll'> & {
|
|
17
32
|
onScroll?: (event: ScrollEvent) => void
|
|
18
33
|
}
|
|
19
34
|
>
|
|
20
35
|
|
|
21
|
-
export type AnimatedList<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'
|