react-native-gifted-chat 3.1.5 โ 3.2.1
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 +70 -1
- package/README.md +9 -3
- 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
|
@@ -1,5 +1,75 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.2.0] - 2025-11-25
|
|
4
|
+
|
|
5
|
+
### โจ Features
|
|
6
|
+
- **Custom Link Parser**: Replaced `react-native-autolink` dependency with custom link parser implementation for better control and performance
|
|
7
|
+
- Removed external dependency on `react-native-autolink`
|
|
8
|
+
- Improved link parsing with custom implementation in `linkParser.tsx`
|
|
9
|
+
- Updated `MessageText` component to use new parser
|
|
10
|
+
- Enhanced links example in example app
|
|
11
|
+
|
|
12
|
+
### ๐ Bug Fixes
|
|
13
|
+
- Adjusted message bubble styles for better rendering
|
|
14
|
+
- Updated test snapshots to reflect parser changes
|
|
15
|
+
|
|
16
|
+
## [3.1.5] - 2025-11-25
|
|
17
|
+
|
|
18
|
+
### โจ Features
|
|
19
|
+
- **Color Scheme Support**: Added `colorScheme` prop to `GiftedChat` component
|
|
20
|
+
- New `useColorScheme` hook for consistent color scheme handling
|
|
21
|
+
- Automatically adapts UI elements (Composer, InputToolbar, Send) based on color scheme
|
|
22
|
+
- Added comprehensive tests for color scheme functionality
|
|
23
|
+
|
|
24
|
+
### ๐ Documentation
|
|
25
|
+
- Updated README with `colorScheme` prop documentation
|
|
26
|
+
|
|
27
|
+
## [3.1.4] - 2025-11-25
|
|
28
|
+
|
|
29
|
+
### ๐ Bug Fixes
|
|
30
|
+
- Added left padding to `TextInput` when no accessory is present for better visual alignment
|
|
31
|
+
- Adjusted input toolbar styles for improved layout
|
|
32
|
+
|
|
33
|
+
## [3.1.3] - 2025-11-25
|
|
34
|
+
|
|
35
|
+
### ๐ง Improvements
|
|
36
|
+
- Removed unused imports for cleaner codebase
|
|
37
|
+
|
|
38
|
+
## [3.1.2] - 2025-11-24
|
|
39
|
+
|
|
40
|
+
### ๐ Bug Fixes
|
|
41
|
+
- Fixed message bubble styles for small messages
|
|
42
|
+
- Improved rendering of compact message content
|
|
43
|
+
|
|
44
|
+
### ๐งช Testing
|
|
45
|
+
- Updated test snapshots
|
|
46
|
+
|
|
47
|
+
## [3.1.1] - 2025-11-24
|
|
48
|
+
|
|
49
|
+
### ๐ Bug Fixes
|
|
50
|
+
- Fixed Bubble component styles for better message rendering
|
|
51
|
+
- Corrected style inconsistencies in message bubbles
|
|
52
|
+
|
|
53
|
+
### ๐งช Testing
|
|
54
|
+
- Updated test snapshots to reflect style fixes
|
|
55
|
+
|
|
56
|
+
## [3.1.0] - 2025-11-24
|
|
57
|
+
|
|
58
|
+
### ๐ง Improvements
|
|
59
|
+
- Refactored component styles for better maintainability
|
|
60
|
+
- Updated Expo Snack example with latest changes
|
|
61
|
+
|
|
62
|
+
### ๐งช Testing
|
|
63
|
+
- Updated test snapshots
|
|
64
|
+
|
|
65
|
+
## [3.0.1] - 2025-11-24
|
|
66
|
+
|
|
67
|
+
### ๐ Bug Fixes
|
|
68
|
+
- Fixed Composer auto-resize height behavior on web platform
|
|
69
|
+
|
|
70
|
+
### ๐งช Testing
|
|
71
|
+
- Updated test snapshots
|
|
72
|
+
|
|
3
73
|
## [3.0.0] - 2025-11-23
|
|
4
74
|
|
|
5
75
|
This is a major release with significant breaking changes, new features, and improvements. The library has been completely rewritten in TypeScript with improved type safety, better keyboard handling, and enhanced customization options.
|
|
@@ -199,7 +269,6 @@ These props moved from `GiftedChatProps` to `MessagesContainerProps` but are sti
|
|
|
199
269
|
- `@types/lodash.isequal`: ^4.5.8
|
|
200
270
|
- `dayjs`: ^1.11.19
|
|
201
271
|
- `lodash.isequal`: ^4.5.0
|
|
202
|
-
- `react-native-autolink`: ^4.2.0
|
|
203
272
|
- `react-native-zoom-reanimated`: ^1.4.10
|
|
204
273
|
|
|
205
274
|
#### 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://x.com/hashtag')
|
|
281
|
+
- `mentionUrl` - Base URL for mentions (e.g., 'https://x.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
|
|
|
@@ -501,7 +507,7 @@ Looking for a React Native freelance expert with more than 14 years of experienc
|
|
|
501
507
|
|
|
502
508
|
## Author
|
|
503
509
|
|
|
504
|
-
Feel free to ask me questions on Twitter [@FaridSafi](https://www.
|
|
510
|
+
Feel free to ask me questions on Twitter [@FaridSafi](https://www.x.com/FaridSafi) or [@xcapetir](https://www.x.com/xcapetir)
|
|
505
511
|
|
|
506
512
|
## Maintainer
|
|
507
513
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-gifted-chat",
|
|
3
|
-
"version": "3.1
|
|
3
|
+
"version": "3.2.1",
|
|
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\.)?|www\.)[^\s]+|(?<![A-Za-z0-9_.@])(?![A-Za-z0-9._%+-]*@)[a-zA-Z0-9][a-zA-Z0-9-]*\.(?!@)[a-zA-Z]{2,}(?![A-Za-z0-9._%+-]*@)(?:\/[^\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-Z0-9._%+-]*@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/gi,
|
|
56
|
+
getLinkUrl: (text: string) => `mailto:${text}`,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: 'phone',
|
|
60
|
+
pattern: /(?<![A-Za-z0-9_])(?:\+?\d{1,3}[\s.\-]?)?\(?\d{1,4}\)?[\s.\-]?\d{1,4}[\s.\-]?\d{1,9}(?![A-Za-z0-9_]|\.[a-z]{2,4})/gi,
|
|
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: /(?<![a-zA-Z0-9._%+-])@[\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
|
+
}
|