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 +0 -1
- package/README.md +8 -2
- package/package.json +1 -2
- package/src/Bubble/index.tsx +1 -1
- package/src/Message/styles.ts +1 -1
- package/src/MessageText.tsx +39 -16
- package/src/MessagesContainer/styles.ts +1 -0
- package/src/__tests__/__snapshots__/Bubble.test.tsx.snap +1 -8
- package/src/__tests__/__snapshots__/Message.test.tsx.snap +6 -29
- package/src/__tests__/__snapshots__/MessageText.test.tsx.snap +1 -3
- package/src/__tests__/__snapshots__/SystemMessage.test.tsx.snap +1 -3
- package/src/index.ts +1 -0
- package/src/linkParser.tsx +255 -0
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
|
|
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
|
|
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.
|
|
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": {
|
package/src/Bubble/index.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useCallback, useMemo } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
|
-
Pressable,
|
|
4
|
+
Pressable,
|
|
5
5
|
} from 'react-native'
|
|
6
6
|
|
|
7
7
|
import { Text } from 'react-native-gesture-handler'
|
package/src/Message/styles.ts
CHANGED
package/src/MessageText.tsx
CHANGED
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
View,
|
|
8
8
|
} from 'react-native'
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
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
|
-
|
|
24
|
+
type: LinkType
|
|
25
25
|
) => void
|
|
26
|
-
|
|
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
|
-
|
|
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,
|
|
50
|
-
onPressProp?.(currentMessage, url,
|
|
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
|
-
<
|
|
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
|
)
|
|
@@ -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": "
|
|
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
|
-
|
|
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": "
|
|
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
|
-
|
|
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": "
|
|
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
|
-
|
|
482
|
-
test
|
|
483
|
-
</Text>
|
|
460
|
+
test
|
|
484
461
|
</Text>
|
|
485
462
|
</View>
|
|
486
463
|
<View
|
package/src/index.ts
CHANGED
|
@@ -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
|
+
}
|