react-native-gifted-chat 2.8.1 → 2.8.2-alpha.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 +10 -5
- package/lib/Bubble/index.d.ts +2 -2
- package/lib/Bubble/index.js +8 -23
- package/lib/Bubble/index.js.map +1 -1
- package/lib/Bubble/types.d.ts +3 -3
- package/lib/GiftedChat/index.js +19 -4
- package/lib/GiftedChat/index.js.map +1 -1
- package/lib/GiftedChat/types.d.ts +2 -7
- package/lib/MessageContainer/index.js +50 -39
- package/lib/MessageContainer/index.js.map +1 -1
- package/lib/MessageContainer/styles.d.ts +3 -2
- package/lib/MessageContainer/styles.js +3 -2
- package/lib/MessageContainer/styles.js.map +1 -1
- package/lib/MessageContainer/types.d.ts +6 -9
- package/lib/MessageText.d.ts +11 -7
- package/lib/MessageText.js +57 -96
- package/lib/MessageText.js.map +1 -1
- package/lib/SystemMessage.d.ts +2 -1
- package/lib/SystemMessage.js +3 -2
- package/lib/SystemMessage.js.map +1 -1
- package/lib/utils.d.ts +2 -0
- package/lib/utils.js +66 -0
- package/lib/utils.js.map +1 -1
- package/package.json +33 -26
- package/src/Bubble/index.tsx +10 -35
- package/src/Bubble/types.ts +3 -3
- package/src/GiftedChat/index.tsx +23 -3
- package/src/GiftedChat/types.ts +3 -1
- package/src/MessageContainer/index.tsx +61 -40
- package/src/MessageContainer/styles.ts +3 -2
- package/src/MessageContainer/types.ts +6 -9
- package/src/MessageText.tsx +86 -121
- package/src/SystemMessage.tsx +4 -1
- package/src/__tests__/DayAnimated.test.tsx +54 -0
- package/src/__tests__/GiftedChat.test.tsx +25 -0
- package/src/__tests__/__snapshots__/DayAnimated.test.tsx.snap +5 -0
- package/src/__tests__/__snapshots__/GiftedChat.test.tsx.snap +25 -0
- package/src/__tests__/__snapshots__/MessageContainer.test.tsx.snap +10 -3
- package/src/utils.ts +77 -1
package/src/GiftedChat/index.tsx
CHANGED
|
@@ -89,6 +89,7 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
|
|
|
89
89
|
minComposerHeight = MIN_COMPOSER_HEIGHT,
|
|
90
90
|
maxComposerHeight = MAX_COMPOSER_HEIGHT,
|
|
91
91
|
isKeyboardInternallyHandled = true,
|
|
92
|
+
disableKeyboardController = false,
|
|
92
93
|
} = props
|
|
93
94
|
|
|
94
95
|
const actionSheetRef = useRef<ActionSheetProviderRef>(null)
|
|
@@ -112,7 +113,16 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
|
|
|
112
113
|
const [text, setText] = useState<string | undefined>(() => props.text || '')
|
|
113
114
|
const [isTypingDisabled, setIsTypingDisabled] = useState<boolean>(false)
|
|
114
115
|
|
|
115
|
-
|
|
116
|
+
// Always call the hook, but conditionally use its data
|
|
117
|
+
const keyboardControllerData = useReanimatedKeyboardAnimation()
|
|
118
|
+
|
|
119
|
+
// Create a mock keyboard object when disabled
|
|
120
|
+
const keyboard = useMemo(() => {
|
|
121
|
+
if (disableKeyboardController)
|
|
122
|
+
return { height: { value: 0 } }
|
|
123
|
+
return keyboardControllerData
|
|
124
|
+
}, [disableKeyboardController, keyboardControllerData])
|
|
125
|
+
|
|
116
126
|
const trackingKeyboardMovement = useSharedValue(false)
|
|
117
127
|
const debounceEnableTypingTimeoutId = useRef<ReturnType<typeof setTimeout>>(undefined)
|
|
118
128
|
const keyboardOffsetBottom = useSharedValue(0)
|
|
@@ -380,9 +390,14 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
|
|
|
380
390
|
setText(props.text)
|
|
381
391
|
}, [props.text])
|
|
382
392
|
|
|
393
|
+
// Only set up keyboard animation when keyboard controller is enabled
|
|
383
394
|
useAnimatedReaction(
|
|
384
|
-
() => -keyboard.height.value,
|
|
395
|
+
() => disableKeyboardController ? 0 : -keyboard.height.value,
|
|
385
396
|
(value, prevValue) => {
|
|
397
|
+
// Skip keyboard handling when disabled
|
|
398
|
+
if (disableKeyboardController)
|
|
399
|
+
return
|
|
400
|
+
|
|
386
401
|
if (prevValue !== null && value !== prevValue) {
|
|
387
402
|
const isKeyboardMovingUp = value > prevValue
|
|
388
403
|
if (isKeyboardMovingUp !== trackingKeyboardMovement.value) {
|
|
@@ -420,6 +435,7 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
|
|
|
420
435
|
disableTyping,
|
|
421
436
|
debounceEnableTyping,
|
|
422
437
|
bottomOffset,
|
|
438
|
+
disableKeyboardController,
|
|
423
439
|
]
|
|
424
440
|
)
|
|
425
441
|
|
|
@@ -433,7 +449,7 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
|
|
|
433
449
|
>
|
|
434
450
|
{isInitialized
|
|
435
451
|
? (
|
|
436
|
-
<Animated.View style={[stylesCommon.fill, isKeyboardInternallyHandled && contentStyleAnim]}>
|
|
452
|
+
<Animated.View style={[stylesCommon.fill, (isKeyboardInternallyHandled && !disableKeyboardController) && contentStyleAnim]}>
|
|
437
453
|
{renderMessages}
|
|
438
454
|
{inputToolbarFragment}
|
|
439
455
|
</Animated.View>
|
|
@@ -448,6 +464,10 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
|
|
|
448
464
|
}
|
|
449
465
|
|
|
450
466
|
function GiftedChatWrapper<TMessage extends IMessage = IMessage> (props: GiftedChatProps<TMessage>) {
|
|
467
|
+
// Don't use KeyboardProvider when keyboard controller is disabled
|
|
468
|
+
if (props.disableKeyboardController)
|
|
469
|
+
return <GiftedChat<TMessage> {...props} />
|
|
470
|
+
|
|
451
471
|
return (
|
|
452
472
|
<KeyboardProvider>
|
|
453
473
|
<GiftedChat<TMessage> {...props} />
|
package/src/GiftedChat/types.ts
CHANGED
|
@@ -75,6 +75,8 @@ export interface GiftedChatProps<TMessage extends IMessage> extends Partial<Mess
|
|
|
75
75
|
isLoadingEarlier?: boolean
|
|
76
76
|
/* Determine whether to handle keyboard awareness inside the plugin. If you have your own keyboard handling outside the plugin set this to false; default is `true` */
|
|
77
77
|
isKeyboardInternallyHandled?: boolean
|
|
78
|
+
/* Completely disable react-native-keyboard-controller. Useful when using react-native-navigation or other conflicting keyboard libraries; default is `false` */
|
|
79
|
+
disableKeyboardController?: boolean
|
|
78
80
|
/* Whether to render an avatar for the current user; default is false, only show avatars for other users */
|
|
79
81
|
showUserAvatar?: boolean
|
|
80
82
|
/* When false, avatars will only be displayed when a consecutive message is from the same user on the same day; default is false */
|
|
@@ -191,7 +193,7 @@ export interface GiftedChatProps<TMessage extends IMessage> extends Partial<Mess
|
|
|
191
193
|
/* Callback when the input text changes */
|
|
192
194
|
onInputTextChanged?(text: string): void
|
|
193
195
|
/* Custom parse patterns for react-native-parsed-text used to linking message content (like URLs and phone numbers) */
|
|
194
|
-
|
|
196
|
+
matchers?: MessageTextProps<TMessage>['matchers']
|
|
195
197
|
onQuickReply?(replies: Reply[]): void
|
|
196
198
|
renderQuickReplies?(
|
|
197
199
|
quickReplies: QuickRepliesProps<TMessage>,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
|
-
|
|
4
|
+
Pressable,
|
|
5
5
|
Text,
|
|
6
6
|
Platform,
|
|
7
7
|
LayoutChangeEvent,
|
|
@@ -23,11 +23,12 @@ import { ItemProps } from './components/Item/types'
|
|
|
23
23
|
import { warning } from '../logging'
|
|
24
24
|
import stylesCommon from '../styles'
|
|
25
25
|
import styles from './styles'
|
|
26
|
-
import { isSameDay } from '../utils'
|
|
26
|
+
import { isSameDay, useCallbackThrottled } from '../utils'
|
|
27
27
|
|
|
28
28
|
export * from './types'
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
+
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList) as React.ComponentType<any>
|
|
31
32
|
|
|
32
33
|
function MessageContainer<TMessage extends IMessage = IMessage> (props: MessageContainerProps<TMessage>) {
|
|
33
34
|
const {
|
|
@@ -53,9 +54,12 @@ function MessageContainer<TMessage extends IMessage = IMessage> (props: MessageC
|
|
|
53
54
|
forwardRef,
|
|
54
55
|
handleOnScroll: handleOnScrollProp,
|
|
55
56
|
scrollToBottomComponent: scrollToBottomComponentProp,
|
|
57
|
+
renderDay: renderDayProp,
|
|
56
58
|
} = props
|
|
57
59
|
|
|
58
60
|
const scrollToBottomOpacity = useSharedValue(0)
|
|
61
|
+
const isScrollingDown = useSharedValue(false)
|
|
62
|
+
const lastScrolledY = useSharedValue(0)
|
|
59
63
|
const [isScrollToBottomVisible, setIsScrollToBottomVisible] = useState(false)
|
|
60
64
|
const scrollToBottomStyleAnim = useAnimatedStyle(() => ({
|
|
61
65
|
opacity: scrollToBottomOpacity.value,
|
|
@@ -74,9 +78,9 @@ function MessageContainer<TMessage extends IMessage = IMessage> (props: MessageC
|
|
|
74
78
|
|
|
75
79
|
const ListFooterComponent = useMemo(() => {
|
|
76
80
|
if (renderFooterProp)
|
|
77
|
-
return
|
|
81
|
+
return renderFooterProp(props)
|
|
78
82
|
|
|
79
|
-
return
|
|
83
|
+
return renderTypingIndicator()
|
|
80
84
|
}, [renderFooterProp, renderTypingIndicator, props])
|
|
81
85
|
|
|
82
86
|
const renderLoadEarlier = useCallback(() => {
|
|
@@ -90,17 +94,33 @@ function MessageContainer<TMessage extends IMessage = IMessage> (props: MessageC
|
|
|
90
94
|
return null
|
|
91
95
|
}, [loadEarlier, renderLoadEarlierProp, props])
|
|
92
96
|
|
|
97
|
+
const changeScrollToBottomVisibility: (isVisible: boolean) => void = useCallbackThrottled((isVisible: boolean) => {
|
|
98
|
+
if (isScrollingDown.value && isVisible)
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
if (isVisible)
|
|
102
|
+
setIsScrollToBottomVisible(true)
|
|
103
|
+
|
|
104
|
+
scrollToBottomOpacity.value = withTiming(isVisible ? 1 : 0, { duration: 250 }, isFinished => {
|
|
105
|
+
if (isFinished && !isVisible)
|
|
106
|
+
runOnJS(setIsScrollToBottomVisible)(false)
|
|
107
|
+
})
|
|
108
|
+
}, [scrollToBottomOpacity, isScrollingDown], 50)
|
|
109
|
+
|
|
93
110
|
const scrollTo = useCallback((options: { animated?: boolean, offset: number }) => {
|
|
94
|
-
if (
|
|
95
|
-
forwardRef
|
|
111
|
+
if (options)
|
|
112
|
+
forwardRef?.current?.scrollToOffset(options)
|
|
96
113
|
}, [forwardRef])
|
|
97
114
|
|
|
98
115
|
const doScrollToBottom = useCallback((animated: boolean = true) => {
|
|
116
|
+
isScrollingDown.value = true
|
|
117
|
+
changeScrollToBottomVisibility(false)
|
|
118
|
+
|
|
99
119
|
if (inverted)
|
|
100
120
|
scrollTo({ offset: 0, animated })
|
|
101
121
|
else if (forwardRef?.current)
|
|
102
122
|
forwardRef.current.scrollToEnd({ animated })
|
|
103
|
-
}, [forwardRef, inverted, scrollTo])
|
|
123
|
+
}, [forwardRef, inverted, scrollTo, isScrollingDown, changeScrollToBottomVisibility])
|
|
104
124
|
|
|
105
125
|
const handleOnScroll = useCallback((event: ReanimatedScrollEvent) => {
|
|
106
126
|
handleOnScrollProp?.(event)
|
|
@@ -111,33 +131,25 @@ function MessageContainer<TMessage extends IMessage = IMessage> (props: MessageC
|
|
|
111
131
|
layoutMeasurement: { height: layoutMeasurementHeight },
|
|
112
132
|
} = event
|
|
113
133
|
|
|
114
|
-
|
|
134
|
+
isScrollingDown.value =
|
|
135
|
+
(inverted && lastScrolledY.value > contentOffsetY) ||
|
|
136
|
+
(!inverted && lastScrolledY.value < contentOffsetY)
|
|
115
137
|
|
|
116
|
-
|
|
117
|
-
setIsScrollToBottomVisible(true)
|
|
118
|
-
scrollToBottomOpacity.value = withTiming(1, { duration })
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const makeScrollToBottomHidden = () => {
|
|
122
|
-
scrollToBottomOpacity.value = withTiming(0, { duration }, isFinished => {
|
|
123
|
-
if (isFinished)
|
|
124
|
-
runOnJS(setIsScrollToBottomVisible)(false)
|
|
125
|
-
})
|
|
126
|
-
}
|
|
138
|
+
lastScrolledY.value = contentOffsetY
|
|
127
139
|
|
|
128
140
|
if (inverted)
|
|
129
141
|
if (contentOffsetY > scrollToBottomOffset!)
|
|
130
|
-
|
|
142
|
+
changeScrollToBottomVisibility(true)
|
|
131
143
|
else
|
|
132
|
-
|
|
144
|
+
changeScrollToBottomVisibility(false)
|
|
133
145
|
else if (
|
|
134
146
|
contentOffsetY < scrollToBottomOffset! &&
|
|
135
147
|
contentSizeHeight - layoutMeasurementHeight > scrollToBottomOffset!
|
|
136
148
|
)
|
|
137
|
-
|
|
149
|
+
changeScrollToBottomVisibility(false)
|
|
138
150
|
else
|
|
139
|
-
|
|
140
|
-
}, [handleOnScrollProp, inverted, scrollToBottomOffset,
|
|
151
|
+
changeScrollToBottomVisibility(false)
|
|
152
|
+
}, [handleOnScrollProp, inverted, scrollToBottomOffset, changeScrollToBottomVisibility, isScrollingDown, lastScrolledY])
|
|
141
153
|
|
|
142
154
|
const renderItem = useCallback(({ item, index }: ListRenderItemInfo<unknown>): React.ReactElement | null => {
|
|
143
155
|
const messageItem = item as TMessage
|
|
@@ -215,39 +227,46 @@ function MessageContainer<TMessage extends IMessage = IMessage> (props: MessageC
|
|
|
215
227
|
return <Text>{'V'}</Text>
|
|
216
228
|
}, [scrollToBottomComponentProp])
|
|
217
229
|
|
|
218
|
-
const
|
|
230
|
+
const ScrollToBottomWrapper = useCallback(() => {
|
|
231
|
+
if (!isScrollToBottomEnabled)
|
|
232
|
+
return null
|
|
233
|
+
|
|
219
234
|
if (!isScrollToBottomVisible)
|
|
220
235
|
return null
|
|
221
236
|
|
|
222
237
|
return (
|
|
223
|
-
<
|
|
238
|
+
<Pressable
|
|
239
|
+
style={styles.scrollToBottom}
|
|
240
|
+
onPress={() => doScrollToBottom()}
|
|
241
|
+
>
|
|
224
242
|
<Animated.View
|
|
225
243
|
style={[
|
|
226
244
|
stylesCommon.centerItems,
|
|
227
|
-
styles.
|
|
245
|
+
styles.scrollToBottomContent,
|
|
228
246
|
scrollToBottomStyle,
|
|
229
247
|
scrollToBottomStyleAnim,
|
|
230
248
|
]}
|
|
231
249
|
>
|
|
232
250
|
{renderScrollBottomComponent()}
|
|
233
251
|
</Animated.View>
|
|
234
|
-
</
|
|
252
|
+
</Pressable>
|
|
235
253
|
)
|
|
236
|
-
}, [scrollToBottomStyle, renderScrollBottomComponent, doScrollToBottom, scrollToBottomStyleAnim, isScrollToBottomVisible])
|
|
254
|
+
}, [scrollToBottomStyle, renderScrollBottomComponent, doScrollToBottom, scrollToBottomStyleAnim, isScrollToBottomEnabled, isScrollToBottomVisible])
|
|
237
255
|
|
|
238
256
|
const onLayoutList = useCallback((event: LayoutChangeEvent) => {
|
|
239
257
|
listHeight.value = event.nativeEvent.layout.height
|
|
240
258
|
|
|
241
259
|
if (
|
|
242
260
|
!inverted &&
|
|
243
|
-
messages?.length
|
|
261
|
+
messages?.length &&
|
|
262
|
+
isScrollToBottomEnabled
|
|
244
263
|
)
|
|
245
264
|
setTimeout(() => {
|
|
246
265
|
doScrollToBottom(false)
|
|
247
266
|
}, 500)
|
|
248
267
|
|
|
249
268
|
listViewProps?.onLayout?.(event)
|
|
250
|
-
}, [inverted, messages, doScrollToBottom, listHeight, listViewProps])
|
|
269
|
+
}, [inverted, messages, doScrollToBottom, listHeight, listViewProps, isScrollToBottomEnabled])
|
|
251
270
|
|
|
252
271
|
const onEndReached = useCallback(() => {
|
|
253
272
|
if (
|
|
@@ -263,15 +282,18 @@ function MessageContainer<TMessage extends IMessage = IMessage> (props: MessageC
|
|
|
263
282
|
const keyExtractor = useCallback((item: unknown) => (item as TMessage)._id.toString(), [])
|
|
264
283
|
|
|
265
284
|
const renderCell = useCallback((props: CellRendererProps<unknown>) => {
|
|
285
|
+
const { item, onLayout: onLayoutProp, children } = props
|
|
286
|
+
const id = (item as IMessage)._id.toString()
|
|
287
|
+
|
|
266
288
|
const handleOnLayout = (event: LayoutChangeEvent) => {
|
|
267
|
-
|
|
289
|
+
onLayoutProp?.(event)
|
|
268
290
|
|
|
269
291
|
const { y, height } = event.nativeEvent.layout
|
|
270
292
|
|
|
271
293
|
const newValue = {
|
|
272
294
|
y,
|
|
273
295
|
height,
|
|
274
|
-
createdAt: new Date((
|
|
296
|
+
createdAt: new Date((item as IMessage).createdAt).getTime(),
|
|
275
297
|
}
|
|
276
298
|
|
|
277
299
|
daysPositions.modify(value => {
|
|
@@ -295,7 +317,7 @@ function MessageContainer<TMessage extends IMessage = IMessage> (props: MessageC
|
|
|
295
317
|
}
|
|
296
318
|
|
|
297
319
|
// @ts-expect-error: https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue#remarks
|
|
298
|
-
value[
|
|
320
|
+
value[id] = newValue
|
|
299
321
|
return value
|
|
300
322
|
})
|
|
301
323
|
}
|
|
@@ -305,7 +327,7 @@ function MessageContainer<TMessage extends IMessage = IMessage> (props: MessageC
|
|
|
305
327
|
{...props}
|
|
306
328
|
onLayout={handleOnLayout}
|
|
307
329
|
>
|
|
308
|
-
{
|
|
330
|
+
{children}
|
|
309
331
|
</View>
|
|
310
332
|
)
|
|
311
333
|
}, [daysPositions, inverted])
|
|
@@ -349,7 +371,7 @@ function MessageContainer<TMessage extends IMessage = IMessage> (props: MessageC
|
|
|
349
371
|
>
|
|
350
372
|
<AnimatedFlatList
|
|
351
373
|
extraData={extraData}
|
|
352
|
-
ref={forwardRef
|
|
374
|
+
ref={forwardRef}
|
|
353
375
|
keyExtractor={keyExtractor}
|
|
354
376
|
data={messages}
|
|
355
377
|
renderItem={renderItem}
|
|
@@ -372,13 +394,12 @@ function MessageContainer<TMessage extends IMessage = IMessage> (props: MessageC
|
|
|
372
394
|
onLayout={onLayoutList}
|
|
373
395
|
CellRendererComponent={renderCell}
|
|
374
396
|
/>
|
|
375
|
-
|
|
376
|
-
? renderScrollToBottomWrapper()
|
|
377
|
-
: null}
|
|
397
|
+
<ScrollToBottomWrapper />
|
|
378
398
|
<DayAnimated
|
|
379
399
|
scrolledY={scrolledY}
|
|
380
400
|
daysPositions={daysPositions}
|
|
381
401
|
listHeight={listHeight}
|
|
402
|
+
renderDay={renderDayProp}
|
|
382
403
|
messages={messages}
|
|
383
404
|
isLoadingEarlier={isLoadingEarlier}
|
|
384
405
|
/>
|
|
@@ -13,12 +13,13 @@ export default StyleSheet.create({
|
|
|
13
13
|
emptyChatContainer: {
|
|
14
14
|
transform: [{ scaleY: -1 }],
|
|
15
15
|
},
|
|
16
|
-
|
|
17
|
-
opacity: 0.8,
|
|
16
|
+
scrollToBottom: {
|
|
18
17
|
position: 'absolute',
|
|
19
18
|
right: 10,
|
|
20
19
|
bottom: 30,
|
|
21
20
|
zIndex: 999,
|
|
21
|
+
},
|
|
22
|
+
scrollToBottomContent: {
|
|
22
23
|
height: 40,
|
|
23
24
|
width: 40,
|
|
24
25
|
borderRadius: 20,
|
|
@@ -1,23 +1,19 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { RefObject } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
FlatListProps,
|
|
4
|
-
LayoutChangeEvent,
|
|
5
4
|
StyleProp,
|
|
6
5
|
ViewStyle,
|
|
6
|
+
FlatList,
|
|
7
7
|
} from 'react-native'
|
|
8
8
|
|
|
9
9
|
import { LoadEarlierProps } from '../LoadEarlier'
|
|
10
10
|
import { MessageProps } from '../Message'
|
|
11
|
-
import { User, IMessage, Reply } from '../types'
|
|
11
|
+
import { User, IMessage, Reply, DayProps } from '../types'
|
|
12
12
|
import { ReanimatedScrollEvent } from 'react-native-reanimated/lib/typescript/hook/commonTypes'
|
|
13
|
-
import { FlatList } from 'react-native-reanimated/lib/typescript/Animated'
|
|
14
|
-
import { AnimateProps } from 'react-native-reanimated'
|
|
15
13
|
|
|
16
|
-
export type ListViewProps =
|
|
17
|
-
onLayout?: (event: LayoutChangeEvent) => void
|
|
18
|
-
} & object
|
|
14
|
+
export type ListViewProps<TMessage extends IMessage = IMessage> = Partial<FlatListProps<TMessage>>
|
|
19
15
|
|
|
20
|
-
export type AnimatedList<TMessage> =
|
|
16
|
+
export type AnimatedList<TMessage> = FlatList<TMessage>
|
|
21
17
|
|
|
22
18
|
export interface MessageContainerProps<TMessage extends IMessage = IMessage> {
|
|
23
19
|
forwardRef?: RefObject<AnimatedList<TMessage>>
|
|
@@ -36,6 +32,7 @@ export interface MessageContainerProps<TMessage extends IMessage = IMessage> {
|
|
|
36
32
|
renderChatEmpty?(): React.ReactNode
|
|
37
33
|
renderFooter?(props: MessageContainerProps<TMessage>): React.ReactNode
|
|
38
34
|
renderMessage?(props: MessageProps<TMessage>): React.ReactElement
|
|
35
|
+
renderDay?(props: DayProps): React.ReactNode
|
|
39
36
|
renderLoadEarlier?(props: LoadEarlierProps): React.ReactNode
|
|
40
37
|
renderTypingIndicator?(): React.ReactNode
|
|
41
38
|
scrollToBottomComponent?(): React.ReactNode
|
package/src/MessageText.tsx
CHANGED
|
@@ -1,84 +1,46 @@
|
|
|
1
|
-
import React from 'react'
|
|
1
|
+
import React, { useMemo, useCallback } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
Linking,
|
|
4
4
|
StyleSheet,
|
|
5
|
-
View,
|
|
6
|
-
TextProps,
|
|
7
5
|
StyleProp,
|
|
8
6
|
ViewStyle,
|
|
9
7
|
TextStyle,
|
|
10
8
|
} from 'react-native'
|
|
11
9
|
|
|
12
|
-
import
|
|
10
|
+
import Autolink, { AutolinkProps } from 'react-native-autolink'
|
|
11
|
+
import { Match } from 'autolinker/dist/es2015'
|
|
13
12
|
import { LeftRightStyle, IMessage } from './types'
|
|
14
|
-
import { useChatContext } from './GiftedChatContext'
|
|
15
13
|
import { error } from './logging'
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
textStyle: {
|
|
21
|
-
fontSize: 16,
|
|
22
|
-
lineHeight: 20,
|
|
23
|
-
marginTop: 5,
|
|
24
|
-
marginBottom: 5,
|
|
25
|
-
marginLeft: 10,
|
|
26
|
-
marginRight: 10,
|
|
27
|
-
},
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
const styles = {
|
|
31
|
-
left: StyleSheet.create({
|
|
32
|
-
container: {},
|
|
33
|
-
text: {
|
|
34
|
-
color: 'black',
|
|
35
|
-
...textStyle,
|
|
36
|
-
},
|
|
37
|
-
link: {
|
|
38
|
-
color: 'black',
|
|
39
|
-
textDecorationLine: 'underline',
|
|
40
|
-
},
|
|
41
|
-
}),
|
|
42
|
-
right: StyleSheet.create({
|
|
43
|
-
container: {},
|
|
44
|
-
text: {
|
|
45
|
-
color: 'white',
|
|
46
|
-
...textStyle,
|
|
47
|
-
},
|
|
48
|
-
link: {
|
|
49
|
-
color: 'white',
|
|
50
|
-
textDecorationLine: 'underline',
|
|
51
|
-
},
|
|
52
|
-
}),
|
|
15
|
+
export interface MessageOption {
|
|
16
|
+
title: string
|
|
17
|
+
action: (phone: string) => void
|
|
53
18
|
}
|
|
54
19
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
export interface MessageTextProps<TMessage extends IMessage> {
|
|
20
|
+
export type MessageTextProps<TMessage extends IMessage> = {
|
|
58
21
|
position?: 'left' | 'right'
|
|
59
|
-
optionTitles?: string[]
|
|
60
22
|
currentMessage: TMessage
|
|
61
23
|
containerStyle?: LeftRightStyle<ViewStyle>
|
|
62
24
|
textStyle?: LeftRightStyle<TextStyle>
|
|
63
25
|
linkStyle?: LeftRightStyle<TextStyle>
|
|
64
|
-
textProps?: TextProps
|
|
65
26
|
customTextStyle?: StyleProp<TextStyle>
|
|
66
|
-
|
|
67
|
-
|
|
27
|
+
onPress?: (
|
|
28
|
+
message: TMessage,
|
|
29
|
+
url: string,
|
|
30
|
+
match: Match
|
|
31
|
+
) => void
|
|
32
|
+
} & Omit<AutolinkProps, 'text' | 'onPress'>
|
|
68
33
|
|
|
69
|
-
export
|
|
70
|
-
currentMessage = {} as
|
|
71
|
-
optionTitles = DEFAULT_OPTION_TITLES,
|
|
34
|
+
export const MessageText: React.FC<MessageTextProps<IMessage>> = ({
|
|
35
|
+
currentMessage = {} as IMessage,
|
|
72
36
|
position = 'left',
|
|
73
37
|
containerStyle,
|
|
74
38
|
textStyle,
|
|
75
39
|
linkStyle: linkStyleProp,
|
|
76
40
|
customTextStyle,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
const { actionSheet } = useChatContext()
|
|
81
|
-
|
|
41
|
+
onPress: onPressProp,
|
|
42
|
+
...rest
|
|
43
|
+
}) => {
|
|
82
44
|
// TODO: React.memo
|
|
83
45
|
// const shouldComponentUpdate = (nextProps: MessageTextProps<TMessage>) => {
|
|
84
46
|
// return (
|
|
@@ -88,77 +50,80 @@ export function MessageText<TMessage extends IMessage = IMessage> ({
|
|
|
88
50
|
// )
|
|
89
51
|
// }
|
|
90
52
|
|
|
91
|
-
const onUrlPress = (url: string) => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
})
|
|
100
|
-
}
|
|
53
|
+
const onUrlPress = useCallback((url: string) => {
|
|
54
|
+
if (/^www\./i.test(url))
|
|
55
|
+
url = `https://${url}`
|
|
56
|
+
|
|
57
|
+
Linking.openURL(url).catch(e => {
|
|
58
|
+
error(e, 'No handler for URL:', url)
|
|
59
|
+
})
|
|
60
|
+
}, [])
|
|
101
61
|
|
|
102
|
-
const onPhonePress = (phone: string) => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const cancelButtonIndex = options.length - 1
|
|
108
|
-
actionSheet().showActionSheetWithOptions(
|
|
109
|
-
{
|
|
110
|
-
options,
|
|
111
|
-
cancelButtonIndex,
|
|
112
|
-
},
|
|
113
|
-
(buttonIndex?: number) => {
|
|
114
|
-
switch (buttonIndex) {
|
|
115
|
-
case 0:
|
|
116
|
-
Linking.openURL(`tel:${phone}`).catch(e => {
|
|
117
|
-
error(e, 'No handler for telephone')
|
|
118
|
-
})
|
|
119
|
-
break
|
|
120
|
-
case 1:
|
|
121
|
-
Linking.openURL(`sms:${phone}`).catch(e => {
|
|
122
|
-
error(e, 'No handler for text')
|
|
123
|
-
})
|
|
124
|
-
break
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
)
|
|
128
|
-
}
|
|
62
|
+
const onPhonePress = useCallback((phone: string) => {
|
|
63
|
+
Linking.openURL(`tel:${phone}`).catch(e => {
|
|
64
|
+
error(e, 'No handler for telephone')
|
|
65
|
+
})
|
|
66
|
+
}, [])
|
|
129
67
|
|
|
130
|
-
const onEmailPress = (email: string) =>
|
|
68
|
+
const onEmailPress = useCallback((email: string) =>
|
|
131
69
|
Linking.openURL(`mailto:${email}`).catch(e =>
|
|
132
70
|
error(e, 'No handler for mailto')
|
|
133
|
-
)
|
|
71
|
+
), [])
|
|
134
72
|
|
|
135
|
-
const linkStyle = [
|
|
136
|
-
styles
|
|
73
|
+
const linkStyle = useMemo(() => StyleSheet.flatten([
|
|
74
|
+
styles.link,
|
|
137
75
|
linkStyleProp?.[position],
|
|
138
|
-
]
|
|
76
|
+
]), [position, linkStyleProp])
|
|
77
|
+
|
|
78
|
+
const handlePress = useCallback((url: string, match: Match) => {
|
|
79
|
+
const type = match.getType()
|
|
80
|
+
|
|
81
|
+
if (onPressProp)
|
|
82
|
+
onPressProp(currentMessage, url, match)
|
|
83
|
+
else if (type === 'url')
|
|
84
|
+
onUrlPress(url)
|
|
85
|
+
else if (type === 'phone')
|
|
86
|
+
onPhonePress(url)
|
|
87
|
+
else if (type === 'email')
|
|
88
|
+
onEmailPress(url)
|
|
89
|
+
}, [onUrlPress, onPhonePress, onEmailPress, onPressProp, currentMessage])
|
|
90
|
+
|
|
91
|
+
const style = useMemo(() => [
|
|
92
|
+
containerStyle?.[position],
|
|
93
|
+
styles[`text_${position}`],
|
|
94
|
+
textStyle?.[position],
|
|
95
|
+
customTextStyle,
|
|
96
|
+
], [containerStyle, position, textStyle, customTextStyle])
|
|
97
|
+
|
|
139
98
|
return (
|
|
140
|
-
<
|
|
141
|
-
style={
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
textStyle?.[position],
|
|
150
|
-
customTextStyle,
|
|
151
|
-
]}
|
|
152
|
-
parse={[
|
|
153
|
-
...(parsePatterns ? parsePatterns(linkStyle as unknown as TextStyle) : []),
|
|
154
|
-
{ type: 'url', style: linkStyle, onPress: onUrlPress },
|
|
155
|
-
{ type: 'phone', style: linkStyle, onPress: onPhonePress },
|
|
156
|
-
{ type: 'email', style: linkStyle, onPress: onEmailPress },
|
|
157
|
-
]}
|
|
158
|
-
childrenProps={{ ...textProps }}
|
|
159
|
-
>
|
|
160
|
-
{currentMessage!.text}
|
|
161
|
-
</ParsedText>
|
|
162
|
-
</View>
|
|
99
|
+
<Autolink
|
|
100
|
+
style={style}
|
|
101
|
+
{...rest}
|
|
102
|
+
text={currentMessage!.text}
|
|
103
|
+
email
|
|
104
|
+
link
|
|
105
|
+
linkStyle={linkStyle}
|
|
106
|
+
onPress={handlePress}
|
|
107
|
+
/>
|
|
163
108
|
)
|
|
164
109
|
}
|
|
110
|
+
|
|
111
|
+
const styles = StyleSheet.create({
|
|
112
|
+
text: {
|
|
113
|
+
fontSize: 16,
|
|
114
|
+
lineHeight: 20,
|
|
115
|
+
marginTop: 5,
|
|
116
|
+
marginBottom: 5,
|
|
117
|
+
marginLeft: 10,
|
|
118
|
+
marginRight: 10,
|
|
119
|
+
},
|
|
120
|
+
text_left: {
|
|
121
|
+
color: 'black',
|
|
122
|
+
},
|
|
123
|
+
text_right: {
|
|
124
|
+
color: 'white',
|
|
125
|
+
},
|
|
126
|
+
link: {
|
|
127
|
+
textDecorationLine: 'underline',
|
|
128
|
+
},
|
|
129
|
+
})
|
package/src/SystemMessage.tsx
CHANGED
|
@@ -29,6 +29,7 @@ export interface SystemMessageProps<TMessage extends IMessage> {
|
|
|
29
29
|
containerStyle?: StyleProp<ViewStyle>
|
|
30
30
|
wrapperStyle?: StyleProp<ViewStyle>
|
|
31
31
|
textStyle?: StyleProp<TextStyle>
|
|
32
|
+
children?: React.ReactNode
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export function SystemMessage<TMessage extends IMessage = IMessage> ({
|
|
@@ -36,6 +37,7 @@ export function SystemMessage<TMessage extends IMessage = IMessage> ({
|
|
|
36
37
|
containerStyle,
|
|
37
38
|
wrapperStyle,
|
|
38
39
|
textStyle,
|
|
40
|
+
children,
|
|
39
41
|
}: SystemMessageProps<TMessage>) {
|
|
40
42
|
if (currentMessage == null || currentMessage.system === false)
|
|
41
43
|
return null
|
|
@@ -43,7 +45,8 @@ export function SystemMessage<TMessage extends IMessage = IMessage> ({
|
|
|
43
45
|
return (
|
|
44
46
|
<View style={[stylesCommon.fill, stylesCommon.centerItems, styles.container, containerStyle]}>
|
|
45
47
|
<View style={wrapperStyle}>
|
|
46
|
-
<Text style={[styles.text, textStyle]}>{currentMessage.text}</Text>
|
|
48
|
+
{!!currentMessage.text && <Text style={[styles.text, textStyle]}>{currentMessage.text}</Text>}
|
|
49
|
+
{children}
|
|
47
50
|
</View>
|
|
48
51
|
</View>
|
|
49
52
|
)
|