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.
Files changed (39) hide show
  1. package/README.md +10 -5
  2. package/lib/Bubble/index.d.ts +2 -2
  3. package/lib/Bubble/index.js +8 -23
  4. package/lib/Bubble/index.js.map +1 -1
  5. package/lib/Bubble/types.d.ts +3 -3
  6. package/lib/GiftedChat/index.js +19 -4
  7. package/lib/GiftedChat/index.js.map +1 -1
  8. package/lib/GiftedChat/types.d.ts +2 -7
  9. package/lib/MessageContainer/index.js +50 -39
  10. package/lib/MessageContainer/index.js.map +1 -1
  11. package/lib/MessageContainer/styles.d.ts +3 -2
  12. package/lib/MessageContainer/styles.js +3 -2
  13. package/lib/MessageContainer/styles.js.map +1 -1
  14. package/lib/MessageContainer/types.d.ts +6 -9
  15. package/lib/MessageText.d.ts +11 -7
  16. package/lib/MessageText.js +57 -96
  17. package/lib/MessageText.js.map +1 -1
  18. package/lib/SystemMessage.d.ts +2 -1
  19. package/lib/SystemMessage.js +3 -2
  20. package/lib/SystemMessage.js.map +1 -1
  21. package/lib/utils.d.ts +2 -0
  22. package/lib/utils.js +66 -0
  23. package/lib/utils.js.map +1 -1
  24. package/package.json +33 -26
  25. package/src/Bubble/index.tsx +10 -35
  26. package/src/Bubble/types.ts +3 -3
  27. package/src/GiftedChat/index.tsx +23 -3
  28. package/src/GiftedChat/types.ts +3 -1
  29. package/src/MessageContainer/index.tsx +61 -40
  30. package/src/MessageContainer/styles.ts +3 -2
  31. package/src/MessageContainer/types.ts +6 -9
  32. package/src/MessageText.tsx +86 -121
  33. package/src/SystemMessage.tsx +4 -1
  34. package/src/__tests__/DayAnimated.test.tsx +54 -0
  35. package/src/__tests__/GiftedChat.test.tsx +25 -0
  36. package/src/__tests__/__snapshots__/DayAnimated.test.tsx.snap +5 -0
  37. package/src/__tests__/__snapshots__/GiftedChat.test.tsx.snap +25 -0
  38. package/src/__tests__/__snapshots__/MessageContainer.test.tsx.snap +10 -3
  39. package/src/utils.ts +77 -1
@@ -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
- const keyboard = useReanimatedKeyboardAnimation()
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} />
@@ -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
- parsePatterns?: (linkStyle?: TextStyle) => { type?: string, pattern?: RegExp, style?: StyleProp<TextStyle> | object, onPress?: unknown, renderText?: unknown }[]
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
- TouchableOpacity,
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
- const AnimatedFlatList = Animated.createAnimatedComponent(FlatList)
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 <>{renderFooterProp(props)}</>
81
+ return renderFooterProp(props)
78
82
 
79
- return <>{renderTypingIndicator()}</>
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 (forwardRef?.current && options)
95
- forwardRef.current.scrollToOffset(options)
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
- const duration = 250
134
+ isScrollingDown.value =
135
+ (inverted && lastScrolledY.value > contentOffsetY) ||
136
+ (!inverted && lastScrolledY.value < contentOffsetY)
115
137
 
116
- const makeScrollToBottomVisible = () => {
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
- makeScrollToBottomVisible()
142
+ changeScrollToBottomVisibility(true)
131
143
  else
132
- makeScrollToBottomHidden()
144
+ changeScrollToBottomVisibility(false)
133
145
  else if (
134
146
  contentOffsetY < scrollToBottomOffset! &&
135
147
  contentSizeHeight - layoutMeasurementHeight > scrollToBottomOffset!
136
148
  )
137
- makeScrollToBottomVisible()
149
+ changeScrollToBottomVisibility(false)
138
150
  else
139
- makeScrollToBottomHidden()
140
- }, [handleOnScrollProp, inverted, scrollToBottomOffset, scrollToBottomOpacity])
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 renderScrollToBottomWrapper = useCallback(() => {
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
- <TouchableOpacity onPress={() => doScrollToBottom()}>
238
+ <Pressable
239
+ style={styles.scrollToBottom}
240
+ onPress={() => doScrollToBottom()}
241
+ >
224
242
  <Animated.View
225
243
  style={[
226
244
  stylesCommon.centerItems,
227
- styles.scrollToBottomStyle,
245
+ styles.scrollToBottomContent,
228
246
  scrollToBottomStyle,
229
247
  scrollToBottomStyleAnim,
230
248
  ]}
231
249
  >
232
250
  {renderScrollBottomComponent()}
233
251
  </Animated.View>
234
- </TouchableOpacity>
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
- props.onLayout?.(event)
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((props.item as IMessage).createdAt).getTime(),
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[props.item._id] = newValue
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
- {props.children}
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 as React.Ref<FlatList<unknown>>}
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
- {isScrollToBottomEnabled
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
- scrollToBottomStyle: {
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, { Component, RefObject } from '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> = Component<AnimateProps<FlatListProps<TMessage>>, unknown, unknown> & FlatList<FlatListProps<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
@@ -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 ParsedText from 'react-native-parsed-text'
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
- const WWW_URL_PATTERN = /^www\./i
18
-
19
- const { textStyle } = StyleSheet.create({
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
- const DEFAULT_OPTION_TITLES = ['Call', 'Text', 'Cancel']
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
- parsePatterns?: (linkStyle: TextStyle) => []
67
- }
27
+ onPress?: (
28
+ message: TMessage,
29
+ url: string,
30
+ match: Match
31
+ ) => void
32
+ } & Omit<AutolinkProps, 'text' | 'onPress'>
68
33
 
69
- export function MessageText<TMessage extends IMessage = IMessage> ({
70
- currentMessage = {} as TMessage,
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
- parsePatterns,
78
- textProps,
79
- }: MessageTextProps<TMessage>) {
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
- // When someone sends a message that includes a website address beginning with "www." (omitting the scheme),
93
- // react-native-parsed-text recognizes it as a valid url, but Linking fails to open due to the missing scheme.
94
- if (WWW_URL_PATTERN.test(url))
95
- onUrlPress(`https://${url}`)
96
- else
97
- Linking.openURL(url).catch(e => {
98
- error(e, 'No handler for URL:', url)
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
- const options =
104
- optionTitles && optionTitles.length > 0
105
- ? optionTitles.slice(0, 3)
106
- : DEFAULT_OPTION_TITLES
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[position].link,
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
- <View
141
- style={[
142
- styles[position].container,
143
- containerStyle?.[position],
144
- ]}
145
- >
146
- <ParsedText
147
- style={[
148
- styles[position].text,
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
+ })
@@ -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
  )