react-native-gifted-chat 3.1.5 → 3.2.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/CHANGELOG.md CHANGED
@@ -199,7 +199,6 @@ These props moved from `GiftedChatProps` to `MessagesContainerProps` but are sti
199
199
  - `@types/lodash.isequal`: ^4.5.8
200
200
  - `dayjs`: ^1.11.19
201
201
  - `lodash.isequal`: ^4.5.0
202
- - `react-native-autolink`: ^4.2.0
203
202
  - `react-native-zoom-reanimated`: ^1.4.10
204
203
 
205
204
  #### Peer Dependencies (now required)
package/README.md CHANGED
@@ -45,7 +45,7 @@
45
45
  - Composer actions (to attach photos, etc.)
46
46
  - Load earlier messages
47
47
  - Copy messages to clipboard
48
- - Touchable links using [react-native-autolink](https://github.com/joshswan/react-native-autolink)
48
+ - Touchable links with customizable parsing (URLs, emails, phone numbers, hashtags, mentions)
49
49
  - Avatar as user's initials
50
50
  - Localized dates
51
51
  - Multi-line TextInput
@@ -269,12 +269,18 @@ Messages, system messages, quick replies etc.: [data structure](src/Models.ts)
269
269
  - **`imageProps`** _(Object)_ - Extra props to be passed to the [`<Image>`](https://reactnative.dev/docs/image) component created by the default `renderMessageImage`
270
270
  - **`imageStyle`** _(Object)_ - Custom style for message images
271
271
  - **`videoProps`** _(Object)_ - Extra props to be passed to the video component created by the required `renderMessageVideo`
272
- - **`messageTextProps`** _(Object)_ - Extra props to be passed to the MessageText component. Useful for customizing link parsing behavior, text styles, and matchers. Supports all [react-native-autolink](https://github.com/joshswan/react-native-autolink) props including:
272
+ - **`messageTextProps`** _(Object)_ - Extra props to be passed to the MessageText component. Useful for customizing link parsing behavior, text styles, and matchers. Supports the following props:
273
273
  - `matchers` - Custom matchers for linking message content (like URLs, phone numbers, hashtags, mentions)
274
274
  - `linkStyle` - Custom style for links
275
275
  - `email` - Enable/disable email parsing (default: true)
276
276
  - `phone` - Enable/disable phone number parsing (default: true)
277
277
  - `url` - Enable/disable URL parsing (default: true)
278
+ - `hashtag` - Enable/disable hashtag parsing (default: false)
279
+ - `mention` - Enable/disable mention parsing (default: false)
280
+ - `hashtagUrl` - Base URL for hashtags (e.g., 'https://twitter.com/hashtag')
281
+ - `mentionUrl` - Base URL for mentions (e.g., 'https://twitter.com')
282
+ - `stripPrefix` - Strip 'http://' or 'https://' from URL display (default: false)
283
+ - `TextComponent` - Custom Text component to use (e.g., from react-native-gesture-handler)
278
284
 
279
285
  Example:
280
286
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-gifted-chat",
3
- "version": "3.1.5",
3
+ "version": "3.2.0",
4
4
  "description": "The most complete chat UI for React Native",
5
5
  "keywords": [
6
6
  "android",
@@ -52,7 +52,6 @@
52
52
  "@types/lodash.isequal": "^4.5.8",
53
53
  "dayjs": "^1.11.19",
54
54
  "lodash.isequal": "^4.5.0",
55
- "react-native-autolink": "^4.2.0",
56
55
  "react-native-zoom-reanimated": "^1.4.10"
57
56
  },
58
57
  "devDependencies": {
@@ -1,7 +1,7 @@
1
1
  import React, { useCallback, useMemo } from 'react'
2
2
  import {
3
3
  View,
4
- Pressable, // don't use Pressable from gesture handler to issues with react-native-autolink (onPress doesn't work)
4
+ Pressable,
5
5
  } from 'react-native'
6
6
 
7
7
  import { Text } from 'react-native-gesture-handler'
@@ -4,7 +4,7 @@ export default StyleSheet.create({
4
4
  container: {
5
5
  flexDirection: 'row',
6
6
  alignItems: 'flex-end',
7
- maxWidth: '80%',
7
+ maxWidth: '70%',
8
8
  },
9
9
  container_left: {
10
10
  justifyContent: 'flex-start',
@@ -7,8 +7,8 @@ import {
7
7
  View,
8
8
  } from 'react-native'
9
9
 
10
- import { Match } from 'autolinker/dist/es2015'
11
- import Autolink, { AutolinkProps } from 'react-native-autolink'
10
+ import { Text } from 'react-native-gesture-handler'
11
+ import { LinkParser, LinkMatcher, LinkType } from './linkParser'
12
12
  import { LeftRightStyle, IMessage } from './Models'
13
13
 
14
14
  export type MessageTextProps<TMessage extends IMessage> = {
@@ -21,9 +21,19 @@ export type MessageTextProps<TMessage extends IMessage> = {
21
21
  onPress?: (
22
22
  message: TMessage,
23
23
  url: string,
24
- match: Match
24
+ type: LinkType
25
25
  ) => void
26
- } & Omit<AutolinkProps, 'text' | 'onPress'>
26
+ // Link parser options
27
+ matchers?: LinkMatcher[]
28
+ email?: boolean
29
+ phone?: boolean
30
+ url?: boolean
31
+ hashtag?: boolean
32
+ mention?: boolean
33
+ hashtagUrl?: string
34
+ mentionUrl?: string
35
+ stripPrefix?: boolean
36
+ }
27
37
 
28
38
  export const MessageText: React.FC<MessageTextProps<IMessage>> = ({
29
39
  currentMessage,
@@ -33,7 +43,15 @@ export const MessageText: React.FC<MessageTextProps<IMessage>> = ({
33
43
  linkStyle: linkStyleProp,
34
44
  customTextStyle,
35
45
  onPress: onPressProp,
36
- ...rest
46
+ matchers,
47
+ email = true,
48
+ phone = true,
49
+ url = true,
50
+ hashtag = false,
51
+ mention = false,
52
+ hashtagUrl,
53
+ mentionUrl,
54
+ stripPrefix = false,
37
55
  }) => {
38
56
  const linkStyle = useMemo(() => StyleSheet.flatten([
39
57
  styles.link,
@@ -46,22 +64,27 @@ export const MessageText: React.FC<MessageTextProps<IMessage>> = ({
46
64
  customTextStyle,
47
65
  ], [position, textStyle, customTextStyle])
48
66
 
49
- const handlePress = useCallback((url: string, match: Match) => {
50
- onPressProp?.(currentMessage, url, match)
67
+ const handlePress = useCallback((url: string, type: LinkType) => {
68
+ onPressProp?.(currentMessage, url, type)
51
69
  }, [onPressProp, currentMessage])
52
70
 
53
71
  return (
54
72
  <View style={[styles.container, containerStyle?.[position]]}>
55
- <Autolink
56
- email
57
- phone
58
- url
59
- stripPrefix={false}
60
- {...rest}
61
- onPress={onPressProp ? handlePress : undefined}
62
- linkStyle={linkStyle}
63
- style={style}
73
+ <LinkParser
64
74
  text={currentMessage!.text}
75
+ matchers={matchers}
76
+ email={email}
77
+ phone={phone}
78
+ url={url}
79
+ hashtag={hashtag}
80
+ mention={mention}
81
+ hashtagUrl={hashtagUrl}
82
+ mentionUrl={mentionUrl}
83
+ stripPrefix={stripPrefix}
84
+ linkStyle={linkStyle}
85
+ textStyle={style}
86
+ onPress={onPressProp ? handlePress : undefined}
87
+ TextComponent={Text}
65
88
  />
66
89
  </View>
67
90
  )
@@ -9,6 +9,7 @@ export default StyleSheet.create({
9
9
  contentContainerStyle: {
10
10
  flexGrow: 1,
11
11
  justifyContent: 'flex-start',
12
+ paddingBottom: 10,
12
13
  },
13
14
  emptyChatContainer: {
14
15
  transform: [{ scaleY: -1 }],
@@ -69,15 +69,8 @@ exports[`should render <Bubble /> and compare with snapshot 1`] = `
69
69
  undefined,
70
70
  ]
71
71
  }
72
- user={
73
- {
74
- "_id": 1,
75
- }
76
- }
77
72
  >
78
- <Text>
79
- test
80
- </Text>
73
+ test
81
74
  </Text>
82
75
  </View>
83
76
  <View
@@ -12,7 +12,7 @@ exports[`Message component should render <Message /> and compare with snapshot 1
12
12
  "flexDirection": "row",
13
13
  "justifyContent": "flex-start",
14
14
  "marginLeft": 8,
15
- "maxWidth": "80%",
15
+ "maxWidth": "70%",
16
16
  },
17
17
  {
18
18
  "marginBottom": 10,
@@ -92,15 +92,8 @@ exports[`Message component should render <Message /> and compare with snapshot 1
92
92
  undefined,
93
93
  ]
94
94
  }
95
- user={
96
- {
97
- "_id": 1,
98
- }
99
- }
100
95
  >
101
- <Text>
102
- test
103
- </Text>
96
+ test
104
97
  </Text>
105
98
  </View>
106
99
  <View
@@ -162,7 +155,7 @@ exports[`Message component should render <Message /> with Avatar 1`] = `
162
155
  "flexDirection": "row",
163
156
  "justifyContent": "flex-start",
164
157
  "marginLeft": 8,
165
- "maxWidth": "80%",
158
+ "maxWidth": "70%",
166
159
  },
167
160
  {
168
161
  "marginBottom": 10,
@@ -272,7 +265,6 @@ exports[`Message component should render <Message /> with Avatar 1`] = `
272
265
  }
273
266
  >
274
267
  <Text
275
- isUserAvatarVisible={true}
276
268
  style={
277
269
  [
278
270
  {
@@ -282,15 +274,8 @@ exports[`Message component should render <Message /> with Avatar 1`] = `
282
274
  undefined,
283
275
  ]
284
276
  }
285
- user={
286
- {
287
- "_id": 1,
288
- }
289
- }
290
277
  >
291
- <Text>
292
- test
293
- </Text>
278
+ test
294
279
  </Text>
295
280
  </View>
296
281
  <View
@@ -352,7 +337,7 @@ exports[`Message component should render null if user has no Avatar 1`] = `
352
337
  "flexDirection": "row",
353
338
  "justifyContent": "flex-start",
354
339
  "marginLeft": 8,
355
- "maxWidth": "80%",
340
+ "maxWidth": "70%",
356
341
  },
357
342
  {
358
343
  "marginBottom": 10,
@@ -462,7 +447,6 @@ exports[`Message component should render null if user has no Avatar 1`] = `
462
447
  }
463
448
  >
464
449
  <Text
465
- isUserAvatarVisible={true}
466
450
  style={
467
451
  [
468
452
  {
@@ -472,15 +456,8 @@ exports[`Message component should render null if user has no Avatar 1`] = `
472
456
  undefined,
473
457
  ]
474
458
  }
475
- user={
476
- {
477
- "_id": 1,
478
- }
479
- }
480
459
  >
481
- <Text>
482
- test
483
- </Text>
460
+ test
484
461
  </Text>
485
462
  </View>
486
463
  <View
@@ -23,9 +23,7 @@ exports[`should render <MessageText /> and compare with snapshot 1`] = `
23
23
  ]
24
24
  }
25
25
  >
26
- <Text>
27
- test
28
- </Text>
26
+ test
29
27
  </Text>
30
28
  </View>
31
29
  `;
@@ -59,9 +59,7 @@ exports[`SystemMessage should render <SystemMessage /> and compare with snapshot
59
59
  ]
60
60
  }
61
61
  >
62
- <Text>
63
- test
64
- </Text>
62
+ test
65
63
  </Text>
66
64
  </View>
67
65
  </View>
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export * from './Constant'
3
3
  export * as utils from './utils'
4
4
  export * from './GiftedChatContext'
5
5
  export * from './types'
6
+ export * from './linkParser'
6
7
  export { Actions } from './Actions'
7
8
  export { Avatar } from './Avatar'
8
9
  export { Bubble } from './Bubble'
@@ -0,0 +1,255 @@
1
+ import React from 'react'
2
+ import { Text, TextStyle, StyleProp, Linking } from 'react-native'
3
+
4
+ export type LinkType = 'url' | 'email' | 'phone' | 'mention' | 'hashtag'
5
+
6
+ export interface ParsedLink {
7
+ type: LinkType
8
+ text: string
9
+ url: string
10
+ index: number
11
+ length: number
12
+ }
13
+
14
+ export interface LinkMatcher {
15
+ type: LinkType
16
+ pattern: RegExp
17
+ getLinkUrl?: (text: string) => string
18
+ getLinkText?: (text: string) => string
19
+ baseUrl?: string
20
+ style?: StyleProp<TextStyle>
21
+ renderLink?: (text: string, url: string, index: number, type: LinkType) => React.ReactNode
22
+ onPress?: (url: string, type: LinkType) => void
23
+ }
24
+
25
+ interface LinkParserProps {
26
+ text: string
27
+ matchers?: LinkMatcher[]
28
+ email?: boolean
29
+ phone?: boolean
30
+ url?: boolean
31
+ hashtag?: boolean
32
+ mention?: boolean
33
+ hashtagUrl?: string
34
+ mentionUrl?: string
35
+ linkStyle?: StyleProp<TextStyle>
36
+ onPress?: (url: string, type: LinkType) => void
37
+ stripPrefix?: boolean
38
+ textStyle?: StyleProp<TextStyle>
39
+ TextComponent?: React.ComponentType<any>
40
+ }
41
+
42
+ const DEFAULT_MATCHERS: LinkMatcher[] = [
43
+ {
44
+ type: 'url',
45
+ pattern: /(?:(?:https?:\/\/)|(?:www\.))[^\s]+|[a-zA-Z0-9][a-zA-Z0-9-]+\.[a-zA-Z]{2,}(?:\/[^\s]*)?/gi,
46
+ getLinkUrl: (text: string) => {
47
+ if (!/^https?:\/\//i.test(text))
48
+ return `http://${text}`
49
+
50
+ return text
51
+ },
52
+ },
53
+ {
54
+ type: 'email',
55
+ pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/gi,
56
+ getLinkUrl: (text: string) => `mailto:${text}`,
57
+ },
58
+ {
59
+ type: 'phone',
60
+ pattern: /(?:\+?\d{1,3}[\s.\-]?)?\(?\d{1,4}\)?[\s.\-]?\d{1,4}[\s.\-]?\d{1,9}/g,
61
+ getLinkUrl: (text: string) => {
62
+ const cleaned = text.replace(/[\s.()\-]/g, '')
63
+ return `tel:${cleaned}`
64
+ },
65
+ },
66
+ {
67
+ type: 'hashtag',
68
+ pattern: /#[\w]+/g,
69
+ getLinkUrl: (text: string) => text,
70
+ baseUrl: undefined,
71
+ },
72
+ {
73
+ type: 'mention',
74
+ pattern: /@[\w-]+/g,
75
+ getLinkUrl: (text: string) => text,
76
+ baseUrl: undefined,
77
+ },
78
+ ]
79
+
80
+ function parseLinks(text: string, matchers: LinkMatcher[]): ParsedLink[] {
81
+ const links: ParsedLink[] = []
82
+
83
+ matchers.forEach(matcher => {
84
+ const matches = text.matchAll(matcher.pattern)
85
+ for (const match of matches)
86
+ if (match.index !== undefined) {
87
+ const matchText = match[0]
88
+ const url = matcher.getLinkUrl
89
+ ? matcher.getLinkUrl(matchText)
90
+ : matchText
91
+ const linkText = matcher.getLinkText
92
+ ? matcher.getLinkText(matchText)
93
+ : matchText
94
+
95
+ links.push({
96
+ type: matcher.type,
97
+ text: linkText,
98
+ url,
99
+ index: match.index,
100
+ length: matchText.length,
101
+ })
102
+ }
103
+
104
+ })
105
+
106
+ // Sort by index to maintain order
107
+ return links.sort((a, b) => a.index - b.index)
108
+ }
109
+
110
+ function removeOverlaps(links: ParsedLink[]): ParsedLink[] {
111
+ const filtered: ParsedLink[] = []
112
+
113
+ for (const link of links) {
114
+ const hasOverlap = filtered.some(existing => {
115
+ const existingEnd = existing.index + existing.length
116
+ const linkEnd = link.index + link.length
117
+
118
+ return (
119
+ (link.index >= existing.index && link.index < existingEnd) ||
120
+ (linkEnd > existing.index && linkEnd <= existingEnd) ||
121
+ (link.index <= existing.index && linkEnd >= existingEnd)
122
+ )
123
+ })
124
+
125
+ if (!hasOverlap)
126
+ filtered.push(link)
127
+
128
+ }
129
+
130
+ return filtered
131
+ }
132
+
133
+ export function LinkParser({
134
+ text,
135
+ matchers: customMatchers,
136
+ email = true,
137
+ phone = true,
138
+ url = true,
139
+ hashtag = false,
140
+ mention = false,
141
+ hashtagUrl,
142
+ mentionUrl,
143
+ linkStyle,
144
+ onPress,
145
+ stripPrefix = true,
146
+ textStyle,
147
+ TextComponent = Text,
148
+ }: LinkParserProps): React.ReactElement {
149
+ const activeMatchers: LinkMatcher[] = []
150
+
151
+ // Add custom matchers first (they take precedence)
152
+ if (customMatchers)
153
+ activeMatchers.push(...customMatchers)
154
+
155
+
156
+ // Add default matchers based on flags
157
+ if (url && !customMatchers?.some(m => m.type === 'url'))
158
+ activeMatchers.push(DEFAULT_MATCHERS.find(m => m.type === 'url')!)
159
+
160
+ if (email && !customMatchers?.some(m => m.type === 'email'))
161
+ activeMatchers.push(DEFAULT_MATCHERS.find(m => m.type === 'email')!)
162
+
163
+ if (phone && !customMatchers?.some(m => m.type === 'phone'))
164
+ activeMatchers.push(DEFAULT_MATCHERS.find(m => m.type === 'phone')!)
165
+
166
+ if (hashtag && !customMatchers?.some(m => m.type === 'hashtag')) {
167
+ const hashtagMatcher = { ...DEFAULT_MATCHERS.find(m => m.type === 'hashtag')! }
168
+ if (hashtagUrl) {
169
+ hashtagMatcher.baseUrl = hashtagUrl
170
+ const baseUrl = hashtagUrl.endsWith('/') ? hashtagUrl : `${hashtagUrl}/`
171
+ hashtagMatcher.getLinkUrl = (text: string) => `${baseUrl}${text.substring(1)}`
172
+ }
173
+ activeMatchers.push(hashtagMatcher)
174
+ }
175
+
176
+ if (mention && !customMatchers?.some(m => m.type === 'mention')) {
177
+ const mentionMatcher = { ...DEFAULT_MATCHERS.find(m => m.type === 'mention')! }
178
+ if (mentionUrl) {
179
+ mentionMatcher.baseUrl = mentionUrl
180
+ const baseUrl = mentionUrl.endsWith('/') ? mentionUrl : `${mentionUrl}/`
181
+ mentionMatcher.getLinkUrl = (text: string) => `${baseUrl}${text.substring(1)}`
182
+ }
183
+ activeMatchers.push(mentionMatcher)
184
+ }
185
+
186
+
187
+ const links = removeOverlaps(parseLinks(text, activeMatchers))
188
+
189
+ if (links.length === 0)
190
+ return <TextComponent style={textStyle}>{text}</TextComponent>
191
+
192
+
193
+ const elements: React.ReactNode[] = []
194
+ let lastIndex = 0
195
+
196
+ links.forEach((link, index) => {
197
+ // Add text before link
198
+ if (link.index > lastIndex)
199
+ elements.push(
200
+ <TextComponent key={`text-${index}`} style={textStyle}>
201
+ {text.substring(lastIndex, link.index)}
202
+ </TextComponent>
203
+ )
204
+
205
+
206
+ // Find the matcher for this link
207
+ const matcher = activeMatchers.find(m => m.type === link.type)
208
+
209
+ // Handle link rendering
210
+ if (matcher?.renderLink) {
211
+ elements.push(matcher.renderLink(link.text, link.url, index, link.type))
212
+ } else {
213
+ const handlePress = () => {
214
+ if (matcher?.onPress)
215
+ matcher.onPress(link.url, link.type)
216
+ else if (onPress)
217
+ onPress(link.url, link.type)
218
+ else
219
+ // Default behavior
220
+ Linking.openURL(link.url).catch(err => {
221
+ console.warn('Failed to open URL:', err)
222
+ })
223
+
224
+ }
225
+
226
+ let displayText = link.text
227
+ if (stripPrefix && link.type === 'url')
228
+ displayText = displayText.replace(/^https?:\/\//i, '')
229
+
230
+
231
+ elements.push(
232
+ <TextComponent
233
+ key={`link-${index}`}
234
+ style={[linkStyle, matcher?.style]}
235
+ onPress={handlePress}
236
+ >
237
+ {displayText}
238
+ </TextComponent>
239
+ )
240
+ }
241
+
242
+ lastIndex = link.index + link.length
243
+ })
244
+
245
+ // Add remaining text
246
+ if (lastIndex < text.length)
247
+ elements.push(
248
+ <TextComponent key='text-end' style={textStyle}>
249
+ {text.substring(lastIndex)}
250
+ </TextComponent>
251
+ )
252
+
253
+
254
+ return <TextComponent style={textStyle}>{elements}</TextComponent>
255
+ }