react-native-gifted-chat 3.1.4 → 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 +9 -2
- package/package.json +1 -2
- package/src/Bubble/index.tsx +1 -1
- package/src/Composer.tsx +1 -1
- package/src/GiftedChat/index.tsx +7 -1
- package/src/GiftedChat/types.ts +2 -0
- package/src/GiftedChatContext.ts +2 -0
- package/src/InputToolbar.tsx +2 -1
- package/src/Message/styles.ts +1 -1
- package/src/MessageText.tsx +39 -16
- package/src/MessagesContainer/styles.ts +1 -0
- package/src/Send.tsx +1 -1
- package/src/__tests__/GiftedChat.test.tsx +30 -0
- package/src/__tests__/__snapshots__/Bubble.test.tsx.snap +1 -8
- package/src/__tests__/__snapshots__/GiftedChat.test.tsx.snap +94 -0
- 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/hooks/useColorScheme.ts +18 -0
- package/src/index.ts +2 -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
|
|
@@ -210,6 +210,7 @@ Messages, system messages, quick replies etc.: [data structure](src/Models.ts)
|
|
|
210
210
|
- **`onSend`** _(Function)_ - Callback when sending a message
|
|
211
211
|
- **`messageIdGenerator`** _(Function)_ - Generate an id for new messages. Defaults to a simple random string generator.
|
|
212
212
|
- **`locale`** _(String)_ - Locale to localize the dates. You need first to import the locale you need (ie. `require('dayjs/locale/de')` or `import 'dayjs/locale/fr'`)
|
|
213
|
+
- **`colorScheme`** _('light' | 'dark')_ - Force color scheme (light/dark mode). When set to `'light'` or `'dark'`, it overrides the system color scheme. When `undefined`, it uses the system color scheme. Default is `undefined`.
|
|
213
214
|
|
|
214
215
|
### Refs
|
|
215
216
|
|
|
@@ -268,12 +269,18 @@ Messages, system messages, quick replies etc.: [data structure](src/Models.ts)
|
|
|
268
269
|
- **`imageProps`** _(Object)_ - Extra props to be passed to the [`<Image>`](https://reactnative.dev/docs/image) component created by the default `renderMessageImage`
|
|
269
270
|
- **`imageStyle`** _(Object)_ - Custom style for message images
|
|
270
271
|
- **`videoProps`** _(Object)_ - Extra props to be passed to the video component created by the required `renderMessageVideo`
|
|
271
|
-
- **`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:
|
|
272
273
|
- `matchers` - Custom matchers for linking message content (like URLs, phone numbers, hashtags, mentions)
|
|
273
274
|
- `linkStyle` - Custom style for links
|
|
274
275
|
- `email` - Enable/disable email parsing (default: true)
|
|
275
276
|
- `phone` - Enable/disable phone number parsing (default: true)
|
|
276
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)
|
|
277
284
|
|
|
278
285
|
Example:
|
|
279
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/Composer.tsx
CHANGED
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
TextInputChangeEvent,
|
|
6
6
|
TextInputContentSizeChangeEvent,
|
|
7
7
|
TextInputProps,
|
|
8
|
-
useColorScheme,
|
|
9
8
|
View,
|
|
10
9
|
} from 'react-native'
|
|
11
10
|
import { TextInput } from 'react-native-gesture-handler'
|
|
12
11
|
import { Color } from './Color'
|
|
12
|
+
import { useColorScheme } from './hooks/useColorScheme'
|
|
13
13
|
import stylesCommon, { getColorSchemeStyle } from './styles'
|
|
14
14
|
|
|
15
15
|
export interface ComposerProps {
|
package/src/GiftedChat/index.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import React, {
|
|
|
10
10
|
import {
|
|
11
11
|
View,
|
|
12
12
|
LayoutChangeEvent,
|
|
13
|
+
useColorScheme,
|
|
13
14
|
} from 'react-native'
|
|
14
15
|
import {
|
|
15
16
|
ActionSheetProvider,
|
|
@@ -48,6 +49,7 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
|
|
|
48
49
|
user = {},
|
|
49
50
|
onSend,
|
|
50
51
|
locale = 'en',
|
|
52
|
+
colorScheme: colorSchemeProp,
|
|
51
53
|
renderLoading,
|
|
52
54
|
actionSheet,
|
|
53
55
|
textInputProps,
|
|
@@ -56,6 +58,9 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
|
|
|
56
58
|
isInverted = true,
|
|
57
59
|
} = props
|
|
58
60
|
|
|
61
|
+
const systemColorScheme = useColorScheme()
|
|
62
|
+
const colorScheme = colorSchemeProp !== undefined ? colorSchemeProp : systemColorScheme
|
|
63
|
+
|
|
59
64
|
const actionSheetRef = useRef<ActionSheetProviderRef>(null)
|
|
60
65
|
|
|
61
66
|
const messagesContainerRef = useMemo(
|
|
@@ -236,8 +241,9 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
|
|
|
236
241
|
actionSheetRef.current!.showActionSheetWithOptions,
|
|
237
242
|
})),
|
|
238
243
|
getLocale: () => locale,
|
|
244
|
+
getColorScheme: () => colorScheme,
|
|
239
245
|
}),
|
|
240
|
-
[actionSheet, locale]
|
|
246
|
+
[actionSheet, locale, colorScheme]
|
|
241
247
|
)
|
|
242
248
|
|
|
243
249
|
useEffect(() => {
|
package/src/GiftedChat/types.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface GiftedChatProps<TMessage extends IMessage> extends Partial<Mess
|
|
|
45
45
|
user?: User
|
|
46
46
|
/* Locale to localize the dates */
|
|
47
47
|
locale?: string
|
|
48
|
+
/* Force color scheme (light/dark); default is undefined (uses system color scheme) */
|
|
49
|
+
colorScheme?: 'light' | 'dark'
|
|
48
50
|
/* Format to use for rendering times; default is 'LT' */
|
|
49
51
|
timeFormat?: string
|
|
50
52
|
/* Format to use for rendering dates; default is 'll' */
|
package/src/GiftedChatContext.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface IGiftedChatContext {
|
|
|
11
11
|
) => void
|
|
12
12
|
}
|
|
13
13
|
getLocale(): string
|
|
14
|
+
getColorScheme(): 'light' | 'dark' | null | undefined
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export const GiftedChatContext = createContext<IGiftedChatContext>({
|
|
@@ -18,6 +19,7 @@ export const GiftedChatContext = createContext<IGiftedChatContext>({
|
|
|
18
19
|
actionSheet: () => ({
|
|
19
20
|
showActionSheetWithOptions: () => {},
|
|
20
21
|
}),
|
|
22
|
+
getColorScheme: () => undefined,
|
|
21
23
|
})
|
|
22
24
|
|
|
23
25
|
export const useChatContext = () => useContext(GiftedChatContext)
|
package/src/InputToolbar.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React, { useMemo } from 'react'
|
|
2
|
-
import { StyleSheet, View, StyleProp, ViewStyle
|
|
2
|
+
import { StyleSheet, View, StyleProp, ViewStyle } from 'react-native'
|
|
3
3
|
|
|
4
4
|
import { Actions, ActionsProps } from './Actions'
|
|
5
5
|
import { Color } from './Color'
|
|
6
6
|
import { Composer, ComposerProps } from './Composer'
|
|
7
|
+
import { useColorScheme } from './hooks/useColorScheme'
|
|
7
8
|
import { IMessage } from './Models'
|
|
8
9
|
import { Send, SendProps } from './Send'
|
|
9
10
|
import { getColorSchemeStyle } from './styles'
|
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
|
)
|
package/src/Send.tsx
CHANGED
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
StyleProp,
|
|
5
5
|
ViewStyle,
|
|
6
6
|
TextStyle,
|
|
7
|
-
useColorScheme,
|
|
8
7
|
} from 'react-native'
|
|
9
8
|
import { Text } from 'react-native-gesture-handler'
|
|
10
9
|
import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated'
|
|
@@ -12,6 +11,7 @@ import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-na
|
|
|
12
11
|
import { Color } from './Color'
|
|
13
12
|
import { TouchableOpacity, TouchableOpacityProps } from './components/TouchableOpacity'
|
|
14
13
|
import { TEST_ID } from './Constant'
|
|
14
|
+
import { useColorScheme } from './hooks/useColorScheme'
|
|
15
15
|
import { IMessage } from './Models'
|
|
16
16
|
import { getColorSchemeStyle } from './styles'
|
|
17
17
|
|
|
@@ -28,3 +28,33 @@ it('should render <GiftedChat/> and compare with snapshot', () => {
|
|
|
28
28
|
|
|
29
29
|
expect(toJSON()).toMatchSnapshot()
|
|
30
30
|
})
|
|
31
|
+
|
|
32
|
+
it('should render <GiftedChat/> with light colorScheme and compare with snapshot', () => {
|
|
33
|
+
const { toJSON } = render(
|
|
34
|
+
<GiftedChat
|
|
35
|
+
messages={messages}
|
|
36
|
+
onSend={() => {}}
|
|
37
|
+
user={{
|
|
38
|
+
_id: 1,
|
|
39
|
+
}}
|
|
40
|
+
colorScheme='light'
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
expect(toJSON()).toMatchSnapshot()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should render <GiftedChat/> with dark colorScheme and compare with snapshot', () => {
|
|
48
|
+
const { toJSON } = render(
|
|
49
|
+
<GiftedChat
|
|
50
|
+
messages={messages}
|
|
51
|
+
onSend={() => {}}
|
|
52
|
+
user={{
|
|
53
|
+
_id: 1,
|
|
54
|
+
}}
|
|
55
|
+
colorScheme='dark'
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
expect(toJSON()).toMatchSnapshot()
|
|
60
|
+
})
|
|
@@ -46,3 +46,97 @@ exports[`should render <GiftedChat/> and compare with snapshot 1`] = `
|
|
|
46
46
|
</KeyboardProvider>
|
|
47
47
|
</View>
|
|
48
48
|
`;
|
|
49
|
+
|
|
50
|
+
exports[`should render <GiftedChat/> with dark colorScheme and compare with snapshot 1`] = `
|
|
51
|
+
<View
|
|
52
|
+
style={
|
|
53
|
+
{
|
|
54
|
+
"flex": 1,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
>
|
|
58
|
+
<KeyboardProvider
|
|
59
|
+
navigationBarTranslucent={true}
|
|
60
|
+
statusBarTranslucent={true}
|
|
61
|
+
>
|
|
62
|
+
<View
|
|
63
|
+
style={
|
|
64
|
+
{
|
|
65
|
+
"flex": 1,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
>
|
|
69
|
+
<View
|
|
70
|
+
behavior="padding"
|
|
71
|
+
style={
|
|
72
|
+
{
|
|
73
|
+
"flex": 1,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
>
|
|
77
|
+
<View
|
|
78
|
+
onLayout={[Function]}
|
|
79
|
+
style={
|
|
80
|
+
[
|
|
81
|
+
{
|
|
82
|
+
"flex": 1,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"overflow": "hidden",
|
|
86
|
+
},
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
testID="GC_WRAPPER"
|
|
90
|
+
/>
|
|
91
|
+
</View>
|
|
92
|
+
</View>
|
|
93
|
+
</KeyboardProvider>
|
|
94
|
+
</View>
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
exports[`should render <GiftedChat/> with light colorScheme and compare with snapshot 1`] = `
|
|
98
|
+
<View
|
|
99
|
+
style={
|
|
100
|
+
{
|
|
101
|
+
"flex": 1,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
>
|
|
105
|
+
<KeyboardProvider
|
|
106
|
+
navigationBarTranslucent={true}
|
|
107
|
+
statusBarTranslucent={true}
|
|
108
|
+
>
|
|
109
|
+
<View
|
|
110
|
+
style={
|
|
111
|
+
{
|
|
112
|
+
"flex": 1,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
>
|
|
116
|
+
<View
|
|
117
|
+
behavior="padding"
|
|
118
|
+
style={
|
|
119
|
+
{
|
|
120
|
+
"flex": 1,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
>
|
|
124
|
+
<View
|
|
125
|
+
onLayout={[Function]}
|
|
126
|
+
style={
|
|
127
|
+
[
|
|
128
|
+
{
|
|
129
|
+
"flex": 1,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"overflow": "hidden",
|
|
133
|
+
},
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
testID="GC_WRAPPER"
|
|
137
|
+
/>
|
|
138
|
+
</View>
|
|
139
|
+
</View>
|
|
140
|
+
</KeyboardProvider>
|
|
141
|
+
</View>
|
|
142
|
+
`;
|
|
@@ -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
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useColorScheme as useRNColorScheme } from 'react-native'
|
|
2
|
+
import { useChatContext } from '../GiftedChatContext'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Custom hook that returns the color scheme from GiftedChat context if provided,
|
|
6
|
+
* otherwise falls back to the system color scheme from React Native.
|
|
7
|
+
*
|
|
8
|
+
* @returns The current color scheme ('light', 'dark', null, or undefined)
|
|
9
|
+
*/
|
|
10
|
+
export function useColorScheme() {
|
|
11
|
+
const { getColorScheme } = useChatContext()
|
|
12
|
+
const contextColorScheme = getColorScheme()
|
|
13
|
+
const systemColorScheme = useRNColorScheme()
|
|
14
|
+
|
|
15
|
+
return contextColorScheme !== undefined && contextColorScheme !== null
|
|
16
|
+
? contextColorScheme
|
|
17
|
+
: systemColorScheme
|
|
18
|
+
}
|
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'
|
|
@@ -20,3 +21,4 @@ export { Time } from './Time'
|
|
|
20
21
|
export { GiftedAvatar } from './GiftedAvatar'
|
|
21
22
|
export { MessageAudio } from './MessageAudio'
|
|
22
23
|
export { MessageVideo } from './MessageVideo'
|
|
24
|
+
export { useColorScheme } from './hooks/useColorScheme'
|
|
@@ -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
|
+
}
|