react-native-gifted-chat 2.8.0 → 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 +18 -5
- package/lib/Bubble/index.d.ts +3 -27
- package/lib/Bubble/index.js +138 -125
- 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 +5 -10
- package/lib/Message/index.js +7 -12
- package/lib/Message/index.js.map +1 -1
- package/lib/Message/types.d.ts +2 -2
- package/lib/MessageContainer/components/Item/index.js +3 -1
- package/lib/MessageContainer/components/Item/index.js.map +1 -1
- package/lib/MessageContainer/index.js +69 -46
- 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 +37 -30
- package/src/Bubble/index.tsx +171 -172
- package/src/Bubble/types.ts +3 -3
- package/src/GiftedChat/index.tsx +23 -3
- package/src/GiftedChat/types.ts +6 -4
- package/src/Message/index.tsx +10 -16
- package/src/Message/types.ts +2 -2
- package/src/MessageContainer/components/Item/index.tsx +1 -0
- package/src/MessageContainer/index.tsx +93 -58
- 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 +11 -9
- package/src/utils.ts +77 -1
package/src/Message/types.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { AvatarProps } from '../Avatar'
|
|
|
3
3
|
import { SystemMessageProps } from '../SystemMessage'
|
|
4
4
|
import { DayProps } from '../Day'
|
|
5
5
|
import { IMessage, User, LeftRightStyle } from '../types'
|
|
6
|
-
import
|
|
6
|
+
import { BubbleProps } from '../Bubble'
|
|
7
7
|
|
|
8
8
|
export interface MessageProps<TMessage extends IMessage> {
|
|
9
9
|
showUserAvatar?: boolean
|
|
@@ -14,7 +14,7 @@ export interface MessageProps<TMessage extends IMessage> {
|
|
|
14
14
|
user: User
|
|
15
15
|
inverted?: boolean
|
|
16
16
|
containerStyle?: LeftRightStyle<ViewStyle>
|
|
17
|
-
renderBubble?(props:
|
|
17
|
+
renderBubble?(props: BubbleProps<TMessage>): React.ReactNode
|
|
18
18
|
renderDay?(props: DayProps): React.ReactNode
|
|
19
19
|
renderSystemMessage?(props: SystemMessageProps<TMessage>): React.ReactNode
|
|
20
20
|
renderAvatar?(props: AvatarProps<TMessage>): React.ReactNode
|
|
@@ -134,6 +134,7 @@ const Item = <TMessage extends IMessage>(props: ItemProps<TMessage>) => {
|
|
|
134
134
|
}), [relativeScrolledPositionToBottomOfDay, dayContainerHeight, dayTopOffset])
|
|
135
135
|
|
|
136
136
|
return (
|
|
137
|
+
// do not remove key. it helps to get correct position of the day container
|
|
137
138
|
<View key={props.currentMessage._id.toString()}>
|
|
138
139
|
<Animated.View
|
|
139
140
|
style={style}
|
|
@@ -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,42 +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
|
-
<
|
|
224
|
-
style={
|
|
225
|
-
|
|
226
|
-
styles.scrollToBottomStyle,
|
|
227
|
-
scrollToBottomStyle,
|
|
228
|
-
scrollToBottomStyleAnim,
|
|
229
|
-
]}
|
|
238
|
+
<Pressable
|
|
239
|
+
style={styles.scrollToBottom}
|
|
240
|
+
onPress={() => doScrollToBottom()}
|
|
230
241
|
>
|
|
231
|
-
<
|
|
232
|
-
|
|
233
|
-
|
|
242
|
+
<Animated.View
|
|
243
|
+
style={[
|
|
244
|
+
stylesCommon.centerItems,
|
|
245
|
+
styles.scrollToBottomContent,
|
|
246
|
+
scrollToBottomStyle,
|
|
247
|
+
scrollToBottomStyleAnim,
|
|
248
|
+
]}
|
|
234
249
|
>
|
|
235
250
|
{renderScrollBottomComponent()}
|
|
236
|
-
</
|
|
237
|
-
</
|
|
251
|
+
</Animated.View>
|
|
252
|
+
</Pressable>
|
|
238
253
|
)
|
|
239
|
-
}, [scrollToBottomStyle, renderScrollBottomComponent, doScrollToBottom, scrollToBottomStyleAnim, isScrollToBottomVisible])
|
|
254
|
+
}, [scrollToBottomStyle, renderScrollBottomComponent, doScrollToBottom, scrollToBottomStyleAnim, isScrollToBottomEnabled, isScrollToBottomVisible])
|
|
240
255
|
|
|
241
256
|
const onLayoutList = useCallback((event: LayoutChangeEvent) => {
|
|
242
257
|
listHeight.value = event.nativeEvent.layout.height
|
|
243
258
|
|
|
244
259
|
if (
|
|
245
260
|
!inverted &&
|
|
246
|
-
messages?.length
|
|
261
|
+
messages?.length &&
|
|
262
|
+
isScrollToBottomEnabled
|
|
247
263
|
)
|
|
248
264
|
setTimeout(() => {
|
|
249
265
|
doScrollToBottom(false)
|
|
250
266
|
}, 500)
|
|
251
267
|
|
|
252
268
|
listViewProps?.onLayout?.(event)
|
|
253
|
-
}, [inverted, messages, doScrollToBottom, listHeight, listViewProps])
|
|
269
|
+
}, [inverted, messages, doScrollToBottom, listHeight, listViewProps, isScrollToBottomEnabled])
|
|
254
270
|
|
|
255
271
|
const onEndReached = useCallback(() => {
|
|
256
272
|
if (
|
|
@@ -266,35 +282,55 @@ function MessageContainer<TMessage extends IMessage = IMessage> (props: MessageC
|
|
|
266
282
|
const keyExtractor = useCallback((item: unknown) => (item as TMessage)._id.toString(), [])
|
|
267
283
|
|
|
268
284
|
const renderCell = useCallback((props: CellRendererProps<unknown>) => {
|
|
285
|
+
const { item, onLayout: onLayoutProp, children } = props
|
|
286
|
+
const id = (item as IMessage)._id.toString()
|
|
287
|
+
|
|
269
288
|
const handleOnLayout = (event: LayoutChangeEvent) => {
|
|
270
|
-
|
|
271
|
-
if (prevMessage && isSameDay(props.item as IMessage, prevMessage))
|
|
272
|
-
return
|
|
289
|
+
onLayoutProp?.(event)
|
|
273
290
|
|
|
274
291
|
const { y, height } = event.nativeEvent.layout
|
|
275
292
|
|
|
276
293
|
const newValue = {
|
|
277
294
|
y,
|
|
278
295
|
height,
|
|
279
|
-
createdAt: new Date((
|
|
296
|
+
createdAt: new Date((item as IMessage).createdAt).getTime(),
|
|
280
297
|
}
|
|
281
298
|
|
|
282
299
|
daysPositions.modify(value => {
|
|
283
300
|
'worklet'
|
|
284
301
|
|
|
302
|
+
const isSameDay = (date1: number, date2: number) => {
|
|
303
|
+
const d1 = new Date(date1)
|
|
304
|
+
const d2 = new Date(date2)
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
d1.getDate() === d2.getDate() &&
|
|
308
|
+
d1.getMonth() === d2.getMonth() &&
|
|
309
|
+
d1.getFullYear() === d2.getFullYear()
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const [key, item] of Object.entries(value))
|
|
314
|
+
if (isSameDay(newValue.createdAt, item.createdAt) && (inverted ? item.y <= newValue.y : item.y >= newValue.y)) {
|
|
315
|
+
delete value[key]
|
|
316
|
+
break
|
|
317
|
+
}
|
|
318
|
+
|
|
285
319
|
// @ts-expect-error: https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue#remarks
|
|
286
|
-
value[
|
|
320
|
+
value[id] = newValue
|
|
287
321
|
return value
|
|
288
322
|
})
|
|
289
323
|
}
|
|
290
324
|
|
|
291
325
|
return (
|
|
292
|
-
<
|
|
326
|
+
<View
|
|
293
327
|
{...props}
|
|
294
328
|
onLayout={handleOnLayout}
|
|
295
|
-
|
|
329
|
+
>
|
|
330
|
+
{children}
|
|
331
|
+
</View>
|
|
296
332
|
)
|
|
297
|
-
}, [daysPositions,
|
|
333
|
+
}, [daysPositions, inverted])
|
|
298
334
|
|
|
299
335
|
const scrollHandler = useAnimatedScrollHandler({
|
|
300
336
|
onScroll: event => {
|
|
@@ -307,7 +343,7 @@ function MessageContainer<TMessage extends IMessage = IMessage> (props: MessageC
|
|
|
307
343
|
// removes unrendered days positions when messages are added/removed
|
|
308
344
|
useEffect(() => {
|
|
309
345
|
Object.keys(daysPositions.value).forEach(key => {
|
|
310
|
-
const messageIndex = messages.findIndex(m => m._id === key)
|
|
346
|
+
const messageIndex = messages.findIndex(m => m._id.toString() === key)
|
|
311
347
|
let shouldRemove = messageIndex === -1
|
|
312
348
|
|
|
313
349
|
if (!shouldRemove) {
|
|
@@ -334,14 +370,14 @@ function MessageContainer<TMessage extends IMessage = IMessage> (props: MessageC
|
|
|
334
370
|
]}
|
|
335
371
|
>
|
|
336
372
|
<AnimatedFlatList
|
|
337
|
-
|
|
338
|
-
|
|
373
|
+
extraData={extraData}
|
|
374
|
+
ref={forwardRef}
|
|
339
375
|
keyExtractor={keyExtractor}
|
|
340
|
-
automaticallyAdjustContentInsets={false}
|
|
341
|
-
inverted={inverted}
|
|
342
376
|
data={messages}
|
|
343
|
-
style={stylesCommon.fill}
|
|
344
377
|
renderItem={renderItem}
|
|
378
|
+
inverted={inverted}
|
|
379
|
+
automaticallyAdjustContentInsets={false}
|
|
380
|
+
style={stylesCommon.fill}
|
|
345
381
|
{...invertibleScrollViewProps}
|
|
346
382
|
ListEmptyComponent={renderChatEmpty}
|
|
347
383
|
ListFooterComponent={
|
|
@@ -358,13 +394,12 @@ function MessageContainer<TMessage extends IMessage = IMessage> (props: MessageC
|
|
|
358
394
|
onLayout={onLayoutList}
|
|
359
395
|
CellRendererComponent={renderCell}
|
|
360
396
|
/>
|
|
361
|
-
|
|
362
|
-
? renderScrollToBottomWrapper()
|
|
363
|
-
: null}
|
|
397
|
+
<ScrollToBottomWrapper />
|
|
364
398
|
<DayAnimated
|
|
365
399
|
scrolledY={scrolledY}
|
|
366
400
|
daysPositions={daysPositions}
|
|
367
401
|
listHeight={listHeight}
|
|
402
|
+
renderDay={renderDayProp}
|
|
368
403
|
messages={messages}
|
|
369
404
|
isLoadingEarlier={isLoadingEarlier}
|
|
370
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 && 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
|
)
|