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.
- 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/Composer.tsx +1 -2
- package/src/Day/index.tsx +2 -2
- package/src/Day/types.ts +3 -2
- package/src/GiftedAvatar.tsx +1 -1
- 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/MessageText.tsx +2 -2
- package/src/MessagesContainer/components/DayAnimated/index.tsx +5 -1
- package/src/MessagesContainer/components/Item/index.tsx +82 -47
- package/src/MessagesContainer/index.tsx +30 -19
- package/src/MessagesContainer/styles.ts +2 -0
- package/src/MessagesContainer/types.ts +30 -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/SystemMessage.tsx +22 -16
- 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/__tests__/__snapshots__/SystemMessage.test.tsx.snap +36 -25
- 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/CHANGELOG.md +0 -364
- package/src/reanimatedCompat.ts +0 -27
|
@@ -5,12 +5,11 @@ import {
|
|
|
5
5
|
ListRenderItemInfo,
|
|
6
6
|
CellRendererProps,
|
|
7
7
|
} from 'react-native'
|
|
8
|
-
import {
|
|
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:
|
|
118
|
-
listPropsOnScrollProp?.(event
|
|
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
|
|
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
|
-
|
|
242
|
+
scrollToBottomContentStyle,
|
|
245
243
|
scrollToBottomStyleAnim,
|
|
246
244
|
]}
|
|
247
245
|
>
|
|
248
246
|
{renderScrollBottomComponent()}
|
|
249
247
|
</Animated.View>
|
|
250
248
|
)
|
|
251
|
-
}, [
|
|
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
|
|
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>
|
|
@@ -1,18 +1,37 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
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?:
|
|
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
|
|
50
|
+
const trimmedText = text?.trim() ?? ''
|
|
51
|
+
const message = { text: trimmedText } as Partial<TMessage>
|
|
47
52
|
|
|
48
|
-
if (onSend &&
|
|
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,
|
package/src/SystemMessage.tsx
CHANGED
|
@@ -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.
|
|
36
|
-
{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
})
|