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.
@@ -0,0 +1,156 @@
1
+ import React, { useMemo } from 'react'
2
+ import {
3
+ Image,
4
+ ImageStyle,
5
+ Pressable,
6
+ StyleProp,
7
+ StyleSheet,
8
+ Text,
9
+ TextStyle,
10
+ View,
11
+ ViewStyle,
12
+ } from 'react-native'
13
+
14
+ import { IMessage, ReplyMessage } from '../Models'
15
+ import { isSameUser } from '../utils'
16
+
17
+ export interface MessageReplyProps<TMessage extends IMessage = IMessage> {
18
+ /** The reply message to display */
19
+ replyMessage: ReplyMessage
20
+ /** The current message containing the reply */
21
+ currentMessage: TMessage
22
+ /** Position of the bubble (left or right) */
23
+ position: 'left' | 'right'
24
+ /** Container style for the reply */
25
+ containerStyle?: StyleProp<ViewStyle>
26
+ /** Container style for left position */
27
+ containerStyleLeft?: StyleProp<ViewStyle>
28
+ /** Container style for right position */
29
+ containerStyleRight?: StyleProp<ViewStyle>
30
+ /** Text style for the reply */
31
+ textStyle?: StyleProp<TextStyle>
32
+ /** Text style for left position */
33
+ textStyleLeft?: StyleProp<TextStyle>
34
+ /** Text style for right position */
35
+ textStyleRight?: StyleProp<TextStyle>
36
+ /** Image style for the reply */
37
+ imageStyle?: StyleProp<ImageStyle>
38
+ /** Callback when reply is pressed */
39
+ onPress?: (replyMessage: ReplyMessage) => void
40
+ }
41
+
42
+ const styles = StyleSheet.create({
43
+ container: {
44
+ borderRadius: 8,
45
+ marginBottom: 4,
46
+ paddingHorizontal: 10,
47
+ paddingVertical: 6,
48
+ },
49
+ containerLeft: {
50
+ backgroundColor: 'rgba(0, 0, 0, 0.06)',
51
+ borderLeftColor: '#0084ff',
52
+ borderLeftWidth: 3,
53
+ },
54
+ containerRight: {
55
+ backgroundColor: 'rgba(255, 255, 255, 0.15)',
56
+ borderLeftColor: 'rgba(255, 255, 255, 0.6)',
57
+ borderLeftWidth: 3,
58
+ },
59
+ image: {
60
+ borderRadius: 4,
61
+ height: 40,
62
+ marginTop: 4,
63
+ width: 40,
64
+ },
65
+ text: {
66
+ fontSize: 13,
67
+ },
68
+ textLeft: {
69
+ color: '#333',
70
+ },
71
+ textRight: {
72
+ color: 'rgba(255, 255, 255, 0.9)',
73
+ },
74
+ username: {
75
+ fontWeight: '600',
76
+ marginBottom: 2,
77
+ },
78
+ usernameLeft: {
79
+ color: '#0084ff',
80
+ },
81
+ usernameRight: {
82
+ color: 'rgba(255, 255, 255, 0.9)',
83
+ },
84
+ })
85
+
86
+ export function MessageReply<TMessage extends IMessage = IMessage> ({
87
+ replyMessage,
88
+ currentMessage,
89
+ position,
90
+ containerStyle,
91
+ containerStyleLeft,
92
+ containerStyleRight,
93
+ textStyle,
94
+ textStyleLeft,
95
+ textStyleRight,
96
+ imageStyle,
97
+ onPress,
98
+ }: MessageReplyProps<TMessage>) {
99
+ const isCurrentUser = useMemo(
100
+ () => isSameUser(currentMessage, { user: replyMessage.user } as TMessage),
101
+ [currentMessage, replyMessage.user]
102
+ )
103
+
104
+ const displayName = useMemo(() => {
105
+ if (isCurrentUser)
106
+ return 'You'
107
+
108
+ return replyMessage.user?.name || 'Unknown'
109
+ }, [isCurrentUser, replyMessage.user?.name])
110
+
111
+ const handlePress = () => {
112
+ onPress?.(replyMessage)
113
+ }
114
+
115
+ const containerStyles = [
116
+ styles.container,
117
+ position === 'left' ? styles.containerLeft : styles.containerRight,
118
+ containerStyle,
119
+ position === 'left' ? containerStyleLeft : containerStyleRight,
120
+ ]
121
+
122
+ const usernameStyles = [
123
+ styles.username,
124
+ position === 'left' ? styles.usernameLeft : styles.usernameRight,
125
+ textStyle,
126
+ position === 'left' ? textStyleLeft : textStyleRight,
127
+ ]
128
+
129
+ const textStyles = [
130
+ styles.text,
131
+ position === 'left' ? styles.textLeft : styles.textRight,
132
+ textStyle,
133
+ position === 'left' ? textStyleLeft : textStyleRight,
134
+ ]
135
+
136
+ return (
137
+ <Pressable onPress={handlePress}>
138
+ <View style={containerStyles}>
139
+ <Text style={usernameStyles} numberOfLines={1}>
140
+ {displayName}
141
+ </Text>
142
+ {replyMessage.text && (
143
+ <Text style={textStyles} numberOfLines={2}>
144
+ {replyMessage.text}
145
+ </Text>
146
+ )}
147
+ {replyMessage.image && (
148
+ <Image
149
+ source={{ uri: replyMessage.image }}
150
+ style={[styles.image, imageStyle]}
151
+ />
152
+ )}
153
+ </View>
154
+ </Pressable>
155
+ )
156
+ }
@@ -0,0 +1,230 @@
1
+ import React, { useEffect } from 'react'
2
+ import {
3
+ Image,
4
+ ImageStyle,
5
+ Pressable,
6
+ StyleProp,
7
+ StyleSheet,
8
+ Text,
9
+ TextStyle,
10
+ View,
11
+ ViewStyle,
12
+ } from 'react-native'
13
+ import Animated, {
14
+ useAnimatedStyle,
15
+ useSharedValue,
16
+ withTiming,
17
+ Easing,
18
+ interpolate,
19
+ runOnJS,
20
+ } from 'react-native-reanimated'
21
+
22
+ import { useColorScheme } from '../hooks/useColorScheme'
23
+ import { ReplyMessage } from '../Models'
24
+
25
+ const ANIMATION_DURATION = 200
26
+ const ANIMATION_EASING = Easing.bezier(0.25, 0.1, 0.25, 1)
27
+ const DEFAULT_HEIGHT = 68
28
+
29
+ export interface ReplyPreviewProps {
30
+ /** The reply message to preview */
31
+ replyMessage: ReplyMessage
32
+ /** Callback to clear the reply */
33
+ onClearReply?: () => void
34
+ /** Container style */
35
+ containerStyle?: StyleProp<ViewStyle>
36
+ /** Text style */
37
+ textStyle?: StyleProp<TextStyle>
38
+ /** Image style */
39
+ imageStyle?: StyleProp<ImageStyle>
40
+ }
41
+
42
+ const styles = StyleSheet.create({
43
+ borderIndicator: {
44
+ backgroundColor: '#0084ff',
45
+ borderTopLeftRadius: 4,
46
+ height: '100%',
47
+ width: 4,
48
+ },
49
+ clearButton: {
50
+ alignItems: 'center',
51
+ borderRadius: 12,
52
+ height: 24,
53
+ justifyContent: 'center',
54
+ width: 24,
55
+ },
56
+ clearButtonText: {
57
+ fontSize: 18,
58
+ fontWeight: '600',
59
+ },
60
+ container: {
61
+ borderRadius: 8,
62
+ flexDirection: 'row',
63
+ marginBottom: 8,
64
+ marginHorizontal: 10,
65
+ overflow: 'hidden',
66
+ },
67
+ containerDark: {
68
+ backgroundColor: '#2c2c2e',
69
+ },
70
+ containerLight: {
71
+ backgroundColor: '#e9e9eb',
72
+ },
73
+ content: {
74
+ flex: 1,
75
+ paddingHorizontal: 10,
76
+ paddingVertical: 8,
77
+ },
78
+ image: {
79
+ borderRadius: 4,
80
+ height: 40,
81
+ marginRight: 8,
82
+ width: 40,
83
+ },
84
+ row: {
85
+ alignItems: 'center',
86
+ flexDirection: 'row',
87
+ },
88
+ text: {
89
+ fontSize: 14,
90
+ },
91
+ textDark: {
92
+ color: '#fff',
93
+ },
94
+ textLight: {
95
+ color: '#333',
96
+ },
97
+ username: {
98
+ color: '#0084ff',
99
+ fontSize: 13,
100
+ fontWeight: '600',
101
+ marginBottom: 2,
102
+ },
103
+ wrapper: {
104
+ overflow: 'hidden',
105
+ },
106
+ })
107
+
108
+ export function ReplyPreview ({
109
+ replyMessage,
110
+ onClearReply,
111
+ containerStyle,
112
+ textStyle,
113
+ imageStyle,
114
+ }: ReplyPreviewProps) {
115
+ const colorScheme = useColorScheme()
116
+ const isDark = colorScheme === 'dark'
117
+
118
+ const animationProgress = useSharedValue(0)
119
+ const contentHeight = useSharedValue(DEFAULT_HEIGHT)
120
+
121
+ // Animate in on mount
122
+ useEffect(() => {
123
+ animationProgress.value = withTiming(1, {
124
+ duration: ANIMATION_DURATION,
125
+ easing: ANIMATION_EASING,
126
+ })
127
+ }, [animationProgress])
128
+
129
+ const handleClear = () => {
130
+ 'worklet'
131
+ animationProgress.value = withTiming(0, {
132
+ duration: ANIMATION_DURATION,
133
+ easing: ANIMATION_EASING,
134
+ }, finished => {
135
+ if (finished && onClearReply)
136
+ runOnJS(onClearReply)()
137
+ })
138
+ }
139
+
140
+ const wrapperAnimatedStyle = useAnimatedStyle(() => {
141
+ const height = interpolate(
142
+ animationProgress.value,
143
+ [0, 1],
144
+ [0, contentHeight.value]
145
+ )
146
+
147
+ const opacity = interpolate(
148
+ animationProgress.value,
149
+ [0, 0.5, 1],
150
+ [0, 0.5, 1]
151
+ )
152
+
153
+ const translateY = interpolate(
154
+ animationProgress.value,
155
+ [0, 1],
156
+ [10, 0]
157
+ )
158
+
159
+ return {
160
+ height,
161
+ opacity,
162
+ transform: [{ translateY }],
163
+ }
164
+ })
165
+
166
+ const displayName = replyMessage.user?.name || 'Unknown'
167
+
168
+ return (
169
+ <Animated.View style={[styles.wrapper, wrapperAnimatedStyle]}>
170
+ <View
171
+ style={[
172
+ styles.container,
173
+ isDark ? styles.containerDark : styles.containerLight,
174
+ containerStyle,
175
+ ]}
176
+ onLayout={e => {
177
+ const newHeight = e.nativeEvent.layout.height + 8
178
+ // Animate height change smoothly when content changes
179
+ contentHeight.value = withTiming(newHeight, {
180
+ duration: ANIMATION_DURATION,
181
+ easing: ANIMATION_EASING,
182
+ })
183
+ }}
184
+ >
185
+ <View style={styles.borderIndicator} />
186
+ <View style={styles.content}>
187
+ <View style={styles.row}>
188
+ {replyMessage.image && (
189
+ <Image
190
+ source={{ uri: replyMessage.image }}
191
+ style={[styles.image, imageStyle]}
192
+ />
193
+ )}
194
+ <View style={{ flex: 1 }}>
195
+ <Text style={styles.username} numberOfLines={1}>
196
+ Replying to {displayName}
197
+ </Text>
198
+ {replyMessage.text && (
199
+ <Text
200
+ style={[
201
+ styles.text,
202
+ isDark ? styles.textDark : styles.textLight,
203
+ textStyle,
204
+ ]}
205
+ numberOfLines={2}
206
+ >
207
+ {replyMessage.text}
208
+ </Text>
209
+ )}
210
+ </View>
211
+ </View>
212
+ </View>
213
+ <Pressable
214
+ style={styles.clearButton}
215
+ onPress={handleClear}
216
+ hitSlop={8}
217
+ >
218
+ <Text
219
+ style={[
220
+ styles.clearButtonText,
221
+ isDark ? styles.textDark : styles.textLight,
222
+ ]}
223
+ >
224
+ ×
225
+ </Text>
226
+ </Pressable>
227
+ </View>
228
+ </Animated.View>
229
+ )
230
+ }
package/src/index.ts CHANGED
@@ -1,9 +1,12 @@
1
+ import * as utils from './utils'
2
+
1
3
  export * from './GiftedChat'
2
4
  export * from './Constant'
3
- export * as utils from './utils'
5
+ export { utils }
4
6
  export * from './GiftedChatContext'
5
7
  export * from './types'
6
8
  export * from './linkParser'
9
+ export * from './Reply'
7
10
  export { Actions } from './Actions'
8
11
  export { Avatar } from './Avatar'
9
12
  export { Bubble } from './Bubble'
@@ -21,4 +24,6 @@ export { Time } from './Time'
21
24
  export { GiftedAvatar } from './GiftedAvatar'
22
25
  export { MessageAudio } from './MessageAudio'
23
26
  export { MessageVideo } from './MessageVideo'
27
+ export { MessageReply } from './components/MessageReply'
28
+ export { ReplyPreview } from './components/ReplyPreview'
24
29
  export { useColorScheme } from './hooks/useColorScheme'
package/src/types.ts CHANGED
@@ -1,24 +1,25 @@
1
1
  export * from './Models'
2
2
 
3
- export { ActionsProps } from './Actions'
4
- export { AvatarProps } from './Avatar'
5
- export {
3
+ export type { ActionsProps } from './Actions'
4
+ export type { AvatarProps } from './Avatar'
5
+ export type {
6
6
  BubbleProps,
7
7
  RenderMessageImageProps,
8
8
  RenderMessageVideoProps,
9
9
  RenderMessageAudioProps,
10
10
  RenderMessageTextProps
11
11
  } from './Bubble'
12
- export { ComposerProps } from './Composer'
13
- export { DayProps } from './Day'
14
- export { GiftedAvatarProps } from './GiftedAvatar'
15
- export { InputToolbarProps } from './InputToolbar'
16
- export { LoadEarlierMessagesProps } from './LoadEarlierMessages'
17
- export { MessageProps } from './Message'
18
- export { MessagesContainerProps } from './MessagesContainer'
19
- export { MessageImageProps } from './MessageImage'
20
- export { MessageTextProps } from './MessageText'
21
- export { QuickRepliesProps } from './QuickReplies'
22
- export { SendProps } from './Send'
23
- export { SystemMessageProps } from './SystemMessage'
24
- export { TimeProps } from './Time'
12
+ export type { ComposerProps } from './Composer'
13
+ export type { DayProps } from './Day'
14
+ export type { GiftedAvatarProps } from './GiftedAvatar'
15
+ export type { InputToolbarProps, ReplyPreviewProps } from './InputToolbar'
16
+ export type { LoadEarlierMessagesProps } from './LoadEarlierMessages'
17
+ export type { MessageProps } from './Message'
18
+ export type { MessagesContainerProps } from './MessagesContainer'
19
+ export type { MessageImageProps } from './MessageImage'
20
+ export type { MessageTextProps } from './MessageText'
21
+ export type { MessageReplyProps } from './components/MessageReply'
22
+ export type { QuickRepliesProps } from './QuickReplies'
23
+ export type { SendProps } from './Send'
24
+ export type { SystemMessageProps } from './SystemMessage'
25
+ export type { TimeProps } from './Time'
package/src/utils.ts CHANGED
@@ -14,9 +14,17 @@ export function renderComponentOrElement<TProps extends Record<string, any>>(
14
14
  return React.cloneElement(component, props as any)
15
15
 
16
16
  if (typeof component === 'function') {
17
- // If it's a component or render function
18
- const Component = component as React.ComponentType<TProps>
19
- return React.createElement(Component, props as any)
17
+ // Check if it's a class component (has prototype.isReactComponent)
18
+ // Class components must use React.createElement
19
+ const isClassComponent = component.prototype && component.prototype.isReactComponent
20
+
21
+ if (isClassComponent)
22
+ return React.createElement(component as React.ComponentType<TProps>, props as any)
23
+
24
+ // For function components and render functions, call directly
25
+ // Using createElement with inline arrow functions causes unmount/remount
26
+ // when function reference changes, this matches v2.x behavior
27
+ return (component as (props: TProps) => React.ReactNode)(props)
20
28
  }
21
29
 
22
30
  // If it's neither, return it as-is