react-native-gifted-chat 3.0.0-alpha.1 → 3.0.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 +295 -0
- package/README.md +12 -30
- package/package.json +1 -1
- package/src/Actions.tsx +27 -18
- package/src/Composer.tsx +60 -80
- package/src/Constant.ts +0 -9
- package/src/GiftedChat/index.tsx +13 -38
- package/src/GiftedChat/types.ts +8 -6
- package/src/InputToolbar.tsx +6 -11
- package/src/MessageImage.tsx +94 -57
- package/src/{MessageContainer → MessagesContainer}/components/Item/index.tsx +21 -17
- package/src/{MessageContainer → MessagesContainer}/components/Item/types.ts +3 -2
- package/src/{MessageContainer → MessagesContainer}/index.tsx +16 -14
- package/src/{MessageContainer → MessagesContainer}/types.ts +4 -2
- package/src/Send.tsx +40 -22
- package/src/__tests__/DayAnimated.test.tsx +1 -1
- package/src/__tests__/{MessageContainer.test.tsx → MessagesContainer.test.tsx} +7 -7
- package/src/__tests__/__snapshots__/Actions.test.tsx.snap +31 -23
- package/src/__tests__/__snapshots__/Composer.test.tsx.snap +39 -30
- package/src/__tests__/__snapshots__/Constant.test.tsx.snap +0 -2
- package/src/__tests__/__snapshots__/InputToolbar.test.tsx.snap +112 -31
- package/src/__tests__/__snapshots__/MessageImage.test.tsx.snap +251 -0
- package/src/__tests__/__snapshots__/Send.test.tsx.snap +189 -49
- package/src/index.ts +1 -1
- package/src/styles.ts +5 -0
- package/src/types.ts +1 -1
- package/CHANGELOG_2.8.1_to_2.8.2-alpha.5.md +0 -374
- /package/src/{MessageContainer → MessagesContainer}/components/DayAnimated/index.tsx +0 -0
- /package/src/{MessageContainer → MessagesContainer}/components/DayAnimated/styles.ts +0 -0
- /package/src/{MessageContainer → MessagesContainer}/components/DayAnimated/types.ts +0 -0
- /package/src/{MessageContainer → MessagesContainer}/styles.ts +0 -0
package/src/Composer.tsx
CHANGED
|
@@ -1,120 +1,100 @@
|
|
|
1
|
-
import React, { useCallback,
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
Platform,
|
|
4
4
|
StyleSheet,
|
|
5
5
|
TextInput,
|
|
6
|
+
TextInputChangeEvent,
|
|
7
|
+
TextInputContentSizeChangeEvent,
|
|
6
8
|
TextInputProps,
|
|
7
|
-
NativeSyntheticEvent,
|
|
8
|
-
TextInputContentSizeChangeEventData,
|
|
9
9
|
useColorScheme,
|
|
10
|
+
View,
|
|
10
11
|
} from 'react-native'
|
|
11
12
|
import { Color } from './Color'
|
|
12
|
-
import {
|
|
13
|
-
import stylesCommon from './styles'
|
|
13
|
+
import stylesCommon, { getColorSchemeStyle } from './styles'
|
|
14
14
|
|
|
15
15
|
export interface ComposerProps {
|
|
16
16
|
composerHeight?: number
|
|
17
17
|
text?: string
|
|
18
18
|
textInputProps?: Partial<TextInputProps>
|
|
19
|
-
onInputSizeChanged?(layout: { width: number, height: number }): void
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
export function Composer ({
|
|
23
|
-
composerHeight = MIN_COMPOSER_HEIGHT,
|
|
24
|
-
onInputSizeChanged,
|
|
25
22
|
text = '',
|
|
26
23
|
textInputProps,
|
|
27
24
|
}: ComposerProps): React.ReactElement {
|
|
28
|
-
const dimensionsRef = useRef<{ width: number, height: number }>(null)
|
|
29
25
|
const colorScheme = useColorScheme()
|
|
30
26
|
const isDark = colorScheme === 'dark'
|
|
31
27
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
const placeholder = textInputProps?.placeholder ?? 'Type a message...'
|
|
29
|
+
|
|
30
|
+
const minHeight = useMemo(() =>
|
|
31
|
+
Platform.select({
|
|
32
|
+
web: styles.textInput.lineHeight + styles.textInput.paddingTop + styles.textInput.paddingBottom,
|
|
33
|
+
default: undefined,
|
|
34
|
+
})
|
|
35
|
+
, [])
|
|
36
|
+
|
|
37
|
+
const [height, setHeight] = useState<number | undefined>(minHeight)
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
) {
|
|
44
|
-
dimensionsRef.current = dimensions
|
|
45
|
-
onInputSizeChanged?.(dimensions)
|
|
39
|
+
const handleContentSizeChange = useMemo(() => {
|
|
40
|
+
if (Platform.OS === 'web')
|
|
41
|
+
return (e: TextInputContentSizeChangeEvent) => {
|
|
42
|
+
const contentHeight = e.nativeEvent.contentSize.height
|
|
43
|
+
setHeight(Math.max(minHeight ?? 0, contentHeight))
|
|
46
44
|
}
|
|
47
|
-
},
|
|
48
|
-
[onInputSizeChanged]
|
|
49
|
-
)
|
|
50
45
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
nativeEvent: { contentSize },
|
|
54
|
-
}: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) =>
|
|
55
|
-
determineInputSizeChange(contentSize),
|
|
56
|
-
[determineInputSizeChange]
|
|
57
|
-
)
|
|
46
|
+
return undefined
|
|
47
|
+
}, [minHeight])
|
|
58
48
|
|
|
59
|
-
const
|
|
49
|
+
const handleChange = useCallback((event: TextInputChangeEvent) => {
|
|
50
|
+
if (Platform.OS === 'web')
|
|
51
|
+
// Reset height to 0 to get the correct scrollHeight
|
|
52
|
+
// @ts-expect-error - web-specific code
|
|
53
|
+
window.requestAnimationFrame(() => {
|
|
54
|
+
// @ts-expect-error - web-specific code
|
|
55
|
+
event.nativeEvent.target.style.height = '0px'
|
|
56
|
+
// @ts-expect-error - web-specific code
|
|
57
|
+
event.nativeEvent.target.style.height = `${event.nativeEvent.target.scrollHeight}px`
|
|
58
|
+
})
|
|
59
|
+
}, [])
|
|
60
60
|
|
|
61
61
|
return (
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
styles.textInput,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
{
|
|
81
|
-
height: composerHeight,
|
|
82
|
-
...Platform.select({
|
|
83
|
-
web: {
|
|
84
|
-
outlineWidth: 0,
|
|
85
|
-
outlineColor: 'transparent',
|
|
86
|
-
outlineOffset: 0,
|
|
87
|
-
},
|
|
88
|
-
}),
|
|
89
|
-
},
|
|
90
|
-
]}
|
|
91
|
-
/>
|
|
62
|
+
<View style={stylesCommon.fill}>
|
|
63
|
+
<TextInput
|
|
64
|
+
testID={placeholder}
|
|
65
|
+
accessible
|
|
66
|
+
accessibilityLabel={placeholder}
|
|
67
|
+
placeholderTextColor={textInputProps?.placeholderTextColor ?? (isDark ? '#888' : Color.defaultColor)}
|
|
68
|
+
value={text}
|
|
69
|
+
enablesReturnKeyAutomatically
|
|
70
|
+
underlineColorAndroid='transparent'
|
|
71
|
+
keyboardAppearance={isDark ? 'dark' : 'default'}
|
|
72
|
+
multiline
|
|
73
|
+
placeholder={placeholder}
|
|
74
|
+
onContentSizeChange={handleContentSizeChange}
|
|
75
|
+
onChange={handleChange}
|
|
76
|
+
{...textInputProps}
|
|
77
|
+
style={[getColorSchemeStyle(styles, 'textInput', colorScheme), stylesWeb.textInput, { height }, textInputProps?.style]}
|
|
78
|
+
/>
|
|
79
|
+
</View>
|
|
92
80
|
)
|
|
93
81
|
}
|
|
94
82
|
|
|
95
83
|
const styles = StyleSheet.create({
|
|
96
84
|
textInput: {
|
|
97
|
-
marginLeft: 10,
|
|
98
85
|
fontSize: 16,
|
|
99
86
|
lineHeight: 22,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
paddingTop: 6,
|
|
103
|
-
paddingLeft: 4,
|
|
104
|
-
},
|
|
105
|
-
}),
|
|
106
|
-
marginTop: Platform.select({
|
|
107
|
-
ios: 6,
|
|
108
|
-
android: 0,
|
|
109
|
-
web: 6,
|
|
110
|
-
}),
|
|
111
|
-
marginBottom: Platform.select({
|
|
112
|
-
ios: 5,
|
|
113
|
-
android: 3,
|
|
114
|
-
web: 4,
|
|
115
|
-
}),
|
|
87
|
+
paddingTop: 8,
|
|
88
|
+
paddingBottom: 10,
|
|
116
89
|
},
|
|
117
90
|
textInput_dark: {
|
|
118
91
|
color: '#fff',
|
|
119
92
|
},
|
|
120
93
|
})
|
|
94
|
+
|
|
95
|
+
const stylesWeb = StyleSheet.create({
|
|
96
|
+
textInput: {
|
|
97
|
+
/* @ts-expect-error - web-specific styles */
|
|
98
|
+
outlineStyle: 'none',
|
|
99
|
+
},
|
|
100
|
+
})
|
package/src/Constant.ts
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
import { Platform } from 'react-native'
|
|
2
|
-
|
|
3
|
-
export const MIN_COMPOSER_HEIGHT = Platform.select({
|
|
4
|
-
ios: 33,
|
|
5
|
-
android: 41,
|
|
6
|
-
web: 34,
|
|
7
|
-
windows: 34,
|
|
8
|
-
})
|
|
9
|
-
export const MAX_COMPOSER_HEIGHT = 200
|
|
10
1
|
export const DATE_FORMAT = 'D MMMM'
|
|
11
2
|
export const TIME_FORMAT = 'LT'
|
|
12
3
|
|
package/src/GiftedChat/index.tsx
CHANGED
|
@@ -21,10 +21,10 @@ import localizedFormat from 'dayjs/plugin/localizedFormat'
|
|
|
21
21
|
import { GestureHandlerRootView } from 'react-native-gesture-handler'
|
|
22
22
|
import { KeyboardAvoidingView, KeyboardProvider } from 'react-native-keyboard-controller'
|
|
23
23
|
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
|
24
|
-
import {
|
|
24
|
+
import { TEST_ID } from '../Constant'
|
|
25
25
|
import { GiftedChatContext } from '../GiftedChatContext'
|
|
26
26
|
import { InputToolbar } from '../InputToolbar'
|
|
27
|
-
import {
|
|
27
|
+
import { MessagesContainer, AnimatedList } from '../MessagesContainer'
|
|
28
28
|
import { IMessage } from '../Models'
|
|
29
29
|
import stylesCommon from '../styles'
|
|
30
30
|
import { renderComponentOrElement } from '../utils'
|
|
@@ -55,15 +55,13 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
|
|
|
55
55
|
renderChatFooter,
|
|
56
56
|
renderInputToolbar,
|
|
57
57
|
isInverted = true,
|
|
58
|
-
minComposerHeight = MIN_COMPOSER_HEIGHT,
|
|
59
|
-
maxComposerHeight = MAX_COMPOSER_HEIGHT,
|
|
60
58
|
} = props
|
|
61
59
|
|
|
62
60
|
const actionSheetRef = useRef<ActionSheetProviderRef>(null)
|
|
63
61
|
|
|
64
|
-
const
|
|
65
|
-
() => props.
|
|
66
|
-
[props.
|
|
62
|
+
const messagesContainerRef = useMemo(
|
|
63
|
+
() => props.messagesContainerRef || createRef<AnimatedList<TMessage>>(),
|
|
64
|
+
[props.messagesContainerRef]
|
|
67
65
|
) as RefObject<AnimatedList<TMessage>>
|
|
68
66
|
|
|
69
67
|
const textInputRef = useMemo(
|
|
@@ -72,9 +70,6 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
|
|
|
72
70
|
)
|
|
73
71
|
|
|
74
72
|
const [isInitialized, setIsInitialized] = useState<boolean>(false)
|
|
75
|
-
const [composerHeight, setComposerHeight] = useState<number>(
|
|
76
|
-
minComposerHeight!
|
|
77
|
-
)
|
|
78
73
|
const [text, setText] = useState<string | undefined>(() => props.text || '')
|
|
79
74
|
|
|
80
75
|
const getTextFromProp = useCallback(
|
|
@@ -89,20 +84,20 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
|
|
|
89
84
|
|
|
90
85
|
const scrollToBottom = useCallback(
|
|
91
86
|
(isAnimated = true) => {
|
|
92
|
-
if (!
|
|
87
|
+
if (!messagesContainerRef?.current)
|
|
93
88
|
return
|
|
94
89
|
|
|
95
90
|
if (isInverted) {
|
|
96
|
-
|
|
91
|
+
messagesContainerRef.current.scrollToOffset({
|
|
97
92
|
offset: 0,
|
|
98
93
|
animated: isAnimated,
|
|
99
94
|
})
|
|
100
95
|
return
|
|
101
96
|
}
|
|
102
97
|
|
|
103
|
-
|
|
98
|
+
messagesContainerRef.current.scrollToEnd({ animated: isAnimated })
|
|
104
99
|
},
|
|
105
|
-
[isInverted,
|
|
100
|
+
[isInverted, messagesContainerRef]
|
|
106
101
|
)
|
|
107
102
|
|
|
108
103
|
const renderMessages = useMemo(() => {
|
|
@@ -113,11 +108,11 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
|
|
|
113
108
|
|
|
114
109
|
return (
|
|
115
110
|
<View style={[stylesCommon.fill, messagesContainerStyle]}>
|
|
116
|
-
<
|
|
111
|
+
<MessagesContainer<TMessage>
|
|
117
112
|
{...messagesContainerProps}
|
|
118
113
|
isInverted={isInverted}
|
|
119
114
|
messages={messages}
|
|
120
|
-
forwardRef={
|
|
115
|
+
forwardRef={messagesContainerRef}
|
|
121
116
|
isTyping={isTyping}
|
|
122
117
|
/>
|
|
123
118
|
{renderComponentOrElement(renderChatFooter, {})}
|
|
@@ -129,7 +124,7 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
|
|
|
129
124
|
messages,
|
|
130
125
|
props,
|
|
131
126
|
isInverted,
|
|
132
|
-
|
|
127
|
+
messagesContainerRef,
|
|
133
128
|
renderChatFooter,
|
|
134
129
|
])
|
|
135
130
|
|
|
@@ -142,10 +137,8 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
|
|
|
142
137
|
|
|
143
138
|
notifyInputTextReset()
|
|
144
139
|
|
|
145
|
-
setComposerHeight(minComposerHeight!)
|
|
146
140
|
setText(getTextFromProp(''))
|
|
147
141
|
}, [
|
|
148
|
-
minComposerHeight,
|
|
149
142
|
getTextFromProp,
|
|
150
143
|
textInputRef,
|
|
151
144
|
notifyInputTextReset,
|
|
@@ -175,18 +168,6 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
|
|
|
175
168
|
[messageIdGenerator, onSend, user, resetInputToolbar, scrollToBottom]
|
|
176
169
|
)
|
|
177
170
|
|
|
178
|
-
const onInputSizeChanged = useCallback(
|
|
179
|
-
(size: { height: number }) => {
|
|
180
|
-
const newComposerHeight = Math.max(
|
|
181
|
-
minComposerHeight!,
|
|
182
|
-
Math.min(maxComposerHeight!, size.height)
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
setComposerHeight(newComposerHeight)
|
|
186
|
-
},
|
|
187
|
-
[maxComposerHeight, minComposerHeight]
|
|
188
|
-
)
|
|
189
|
-
|
|
190
171
|
const _onChangeText = useCallback(
|
|
191
172
|
(text: string) => {
|
|
192
173
|
props.textInputProps?.onChangeText?.(text)
|
|
@@ -211,10 +192,9 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
|
|
|
211
192
|
notifyInputTextReset()
|
|
212
193
|
|
|
213
194
|
setIsInitialized(true)
|
|
214
|
-
setComposerHeight(minComposerHeight!)
|
|
215
195
|
setText(getTextFromProp(initialText))
|
|
216
196
|
},
|
|
217
|
-
[isInitialized, initialText,
|
|
197
|
+
[isInitialized, initialText, notifyInputTextReset, getTextFromProp]
|
|
218
198
|
)
|
|
219
199
|
|
|
220
200
|
const inputToolbarFragment = useMemo(() => {
|
|
@@ -224,9 +204,7 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
|
|
|
224
204
|
const inputToolbarProps = {
|
|
225
205
|
...props,
|
|
226
206
|
text: getTextFromProp(text!),
|
|
227
|
-
composerHeight: Math.max(minComposerHeight!, composerHeight),
|
|
228
207
|
onSend: _onSend,
|
|
229
|
-
onInputSizeChanged,
|
|
230
208
|
textInputProps: {
|
|
231
209
|
...textInputProps,
|
|
232
210
|
onChangeText: _onChangeText,
|
|
@@ -242,12 +220,9 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
|
|
|
242
220
|
isInitialized,
|
|
243
221
|
_onSend,
|
|
244
222
|
getTextFromProp,
|
|
245
|
-
minComposerHeight,
|
|
246
|
-
onInputSizeChanged,
|
|
247
223
|
props,
|
|
248
224
|
text,
|
|
249
225
|
renderInputToolbar,
|
|
250
|
-
composerHeight,
|
|
251
226
|
textInputRef,
|
|
252
227
|
textInputProps,
|
|
253
228
|
_onChangeText,
|
package/src/GiftedChat/types.ts
CHANGED
|
@@ -14,8 +14,8 @@ import { AvatarProps } from '../Avatar'
|
|
|
14
14
|
import { BubbleProps } from '../Bubble'
|
|
15
15
|
import { ComposerProps } from '../Composer'
|
|
16
16
|
import { InputToolbarProps } from '../InputToolbar'
|
|
17
|
-
import { AnimatedList, MessageContainerProps } from '../MessageContainer'
|
|
18
17
|
import { MessageImageProps } from '../MessageImage'
|
|
18
|
+
import { AnimatedList, MessagesContainerProps } from '../MessagesContainer'
|
|
19
19
|
import { MessageTextProps } from '../MessageText'
|
|
20
20
|
import {
|
|
21
21
|
IMessage,
|
|
@@ -29,9 +29,9 @@ import { SendProps } from '../Send'
|
|
|
29
29
|
import { SystemMessageProps } from '../SystemMessage'
|
|
30
30
|
import { TimeProps } from '../Time'
|
|
31
31
|
|
|
32
|
-
export interface GiftedChatProps<TMessage extends IMessage> extends Partial<
|
|
33
|
-
/*
|
|
34
|
-
|
|
32
|
+
export interface GiftedChatProps<TMessage extends IMessage> extends Partial<MessagesContainerProps<TMessage>> {
|
|
33
|
+
/* Messages container ref */
|
|
34
|
+
messagesContainerRef?: RefObject<AnimatedList<TMessage>>
|
|
35
35
|
/* text input ref */
|
|
36
36
|
textInputRef?: RefObject<TextInput>
|
|
37
37
|
/* Controls whether or not to show user.name property in the message bubble */
|
|
@@ -115,13 +115,13 @@ export interface GiftedChatProps<TMessage extends IMessage> extends Partial<Mess
|
|
|
115
115
|
renderMessageImage?: (props: MessageImageProps<TMessage>) => React.ReactNode
|
|
116
116
|
/* Custom message video */
|
|
117
117
|
renderMessageVideo?: (props: MessageVideoProps<TMessage>) => React.ReactNode
|
|
118
|
-
/* Custom message
|
|
118
|
+
/* Custom message audio */
|
|
119
119
|
renderMessageAudio?: (props: MessageAudioProps<TMessage>) => React.ReactNode
|
|
120
120
|
/* Custom view inside the bubble */
|
|
121
121
|
renderCustomView?: (props: BubbleProps<TMessage>) => React.ReactNode
|
|
122
122
|
/* Custom time inside a message */
|
|
123
123
|
renderTime?: (props: TimeProps<TMessage>) => React.ReactNode
|
|
124
|
-
/* Custom component to render below the
|
|
124
|
+
/* Custom component to render below the MessagesContainer */
|
|
125
125
|
renderChatFooter?: () => React.ReactNode
|
|
126
126
|
/* Custom message composer container. Can be a component, element, render function, or null */
|
|
127
127
|
renderInputToolbar?: React.ComponentType<InputToolbarProps<TMessage>> | React.ReactElement | ((props: InputToolbarProps<TMessage>) => React.ReactNode) | null
|
|
@@ -143,4 +143,6 @@ export interface GiftedChatProps<TMessage extends IMessage> extends Partial<Mess
|
|
|
143
143
|
renderQuickReplySend?: () => React.ReactNode
|
|
144
144
|
keyboardProviderProps?: React.ComponentProps<typeof KeyboardProvider>
|
|
145
145
|
keyboardAvoidingViewProps?: KeyboardAvoidingViewProps
|
|
146
|
+
/** Enable animated day label that appears on scroll; default is true */
|
|
147
|
+
isDayAnimationEnabled?: boolean
|
|
146
148
|
}
|
package/src/InputToolbar.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { Color } from './Color'
|
|
|
6
6
|
import { Composer, ComposerProps } from './Composer'
|
|
7
7
|
import { IMessage } from './Models'
|
|
8
8
|
import { Send, SendProps } from './Send'
|
|
9
|
+
import { getColorSchemeStyle } from './styles'
|
|
9
10
|
import { renderComponentOrElement } from './utils'
|
|
10
11
|
|
|
11
12
|
export interface InputToolbarProps<TMessage extends IMessage> {
|
|
@@ -13,7 +14,6 @@ export interface InputToolbarProps<TMessage extends IMessage> {
|
|
|
13
14
|
actionSheetOptionTintColor?: string
|
|
14
15
|
containerStyle?: StyleProp<ViewStyle>
|
|
15
16
|
primaryStyle?: StyleProp<ViewStyle>
|
|
16
|
-
accessoryStyle?: StyleProp<ViewStyle>
|
|
17
17
|
renderAccessory?: (props: InputToolbarProps<TMessage>) => React.ReactNode
|
|
18
18
|
renderActions?: (props: ActionsProps) => React.ReactNode
|
|
19
19
|
renderSend?: (props: SendProps<TMessage>) => React.ReactNode
|
|
@@ -88,16 +88,14 @@ export function InputToolbar<TMessage extends IMessage = IMessage> (
|
|
|
88
88
|
if (!renderAccessory)
|
|
89
89
|
return null
|
|
90
90
|
|
|
91
|
-
return (
|
|
92
|
-
<View style={[styles.accessory, props.accessoryStyle]}>
|
|
93
|
-
{renderComponentOrElement(renderAccessory, props)}
|
|
94
|
-
</View>
|
|
95
|
-
)
|
|
91
|
+
return renderComponentOrElement(renderAccessory, props)
|
|
96
92
|
}, [renderAccessory, props])
|
|
97
93
|
|
|
98
94
|
return (
|
|
99
|
-
<View
|
|
100
|
-
|
|
95
|
+
<View
|
|
96
|
+
style={[getColorSchemeStyle(styles, 'container', colorScheme), containerStyle]}
|
|
97
|
+
>
|
|
98
|
+
<View style={[getColorSchemeStyle(styles, 'primary', colorScheme), props.primaryStyle]}>
|
|
101
99
|
{actionsFragment}
|
|
102
100
|
{composerFragment}
|
|
103
101
|
{sendFragment}
|
|
@@ -121,7 +119,4 @@ const styles = StyleSheet.create({
|
|
|
121
119
|
flexDirection: 'row',
|
|
122
120
|
alignItems: 'flex-end',
|
|
123
121
|
},
|
|
124
|
-
accessory: {
|
|
125
|
-
height: 44,
|
|
126
|
-
},
|
|
127
122
|
})
|
package/src/MessageImage.tsx
CHANGED
|
@@ -19,14 +19,91 @@ import Animated, {
|
|
|
19
19
|
useAnimatedStyle,
|
|
20
20
|
useSharedValue,
|
|
21
21
|
withTiming,
|
|
22
|
-
runOnJS,
|
|
23
22
|
Easing,
|
|
23
|
+
runOnJS,
|
|
24
24
|
} from 'react-native-reanimated'
|
|
25
|
-
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
25
|
+
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
26
26
|
import Zoom from 'react-native-zoom-reanimated'
|
|
27
27
|
import { IMessage } from './Models'
|
|
28
28
|
import commonStyles from './styles'
|
|
29
29
|
|
|
30
|
+
interface ModalContentProps {
|
|
31
|
+
isVisible: boolean
|
|
32
|
+
imageSource: ImageURISource
|
|
33
|
+
modalImageDimensions: { width: number, height: number } | undefined
|
|
34
|
+
imageProps?: Partial<ImageProps>
|
|
35
|
+
onClose: () => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function ModalContent({ isVisible, imageSource, modalImageDimensions, imageProps, onClose }: ModalContentProps) {
|
|
39
|
+
const insets = useSafeAreaInsets()
|
|
40
|
+
|
|
41
|
+
// Animation values
|
|
42
|
+
const modalOpacity = useSharedValue(0)
|
|
43
|
+
const modalScale = useSharedValue(0.9)
|
|
44
|
+
const modalBorderRadius = useSharedValue(40)
|
|
45
|
+
|
|
46
|
+
const handleModalClose = useCallback(() => {
|
|
47
|
+
modalOpacity.value = withTiming(0, { duration: 200, easing: Easing.in(Easing.ease) })
|
|
48
|
+
modalScale.value = withTiming(0.9, { duration: 200, easing: Easing.in(Easing.ease) }, () => {
|
|
49
|
+
runOnJS(onClose)()
|
|
50
|
+
})
|
|
51
|
+
modalBorderRadius.value = withTiming(40, { duration: 200, easing: Easing.in(Easing.ease) })
|
|
52
|
+
}, [onClose, modalOpacity, modalScale, modalBorderRadius])
|
|
53
|
+
|
|
54
|
+
// Animate on visibility change
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (isVisible) {
|
|
57
|
+
modalOpacity.value = withTiming(1, { duration: 300, easing: Easing.out(Easing.ease) })
|
|
58
|
+
modalScale.value = withTiming(1, { duration: 300, easing: Easing.out(Easing.ease) })
|
|
59
|
+
modalBorderRadius.value = withTiming(0, { duration: 300, easing: Easing.out(Easing.ease) })
|
|
60
|
+
}
|
|
61
|
+
}, [isVisible, modalOpacity, modalScale, modalBorderRadius])
|
|
62
|
+
|
|
63
|
+
const modalAnimatedStyle = useAnimatedStyle(() => ({
|
|
64
|
+
opacity: modalOpacity.value,
|
|
65
|
+
transform: [{ scale: modalScale.value }],
|
|
66
|
+
}), [modalOpacity, modalScale])
|
|
67
|
+
|
|
68
|
+
const modalBorderRadiusStyle = useAnimatedStyle(() => ({
|
|
69
|
+
borderRadius: modalBorderRadius.value,
|
|
70
|
+
}), [modalBorderRadius])
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<>
|
|
74
|
+
<StatusBar animated barStyle='dark-content' />
|
|
75
|
+
<Animated.View style={[styles.modalOverlay, modalAnimatedStyle, modalBorderRadiusStyle]}>
|
|
76
|
+
<GestureHandlerRootView style={commonStyles.fill}>
|
|
77
|
+
<Animated.View style={[commonStyles.fill, styles.modalContent, modalBorderRadiusStyle, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
|
|
78
|
+
|
|
79
|
+
{/* close button */}
|
|
80
|
+
<View style={styles.closeButtonContainer}>
|
|
81
|
+
<BaseButton onPress={handleModalClose}>
|
|
82
|
+
<View style={styles.closeButtonContent}>
|
|
83
|
+
<Text style={styles.closeButtonIcon}>
|
|
84
|
+
{'X'}
|
|
85
|
+
</Text>
|
|
86
|
+
</View>
|
|
87
|
+
</BaseButton>
|
|
88
|
+
</View>
|
|
89
|
+
|
|
90
|
+
<View style={[commonStyles.fill, commonStyles.centerItems]}>
|
|
91
|
+
<Zoom>
|
|
92
|
+
<Image
|
|
93
|
+
style={modalImageDimensions}
|
|
94
|
+
source={imageSource}
|
|
95
|
+
resizeMode='contain'
|
|
96
|
+
{...imageProps}
|
|
97
|
+
/>
|
|
98
|
+
</Zoom>
|
|
99
|
+
</View>
|
|
100
|
+
</Animated.View>
|
|
101
|
+
</GestureHandlerRootView>
|
|
102
|
+
</Animated.View>
|
|
103
|
+
</>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
30
107
|
export interface MessageImageProps<TMessage extends IMessage> {
|
|
31
108
|
currentMessage: TMessage
|
|
32
109
|
containerStyle?: StyleProp<ViewStyle>
|
|
@@ -46,12 +123,6 @@ export function MessageImage<TMessage extends IMessage = IMessage> ({
|
|
|
46
123
|
const [imageDimensions, setImageDimensions] = useState<{ width: number, height: number }>()
|
|
47
124
|
const windowDimensions = useWindowDimensions()
|
|
48
125
|
|
|
49
|
-
const insets = useSafeAreaInsets()
|
|
50
|
-
|
|
51
|
-
// Animation values
|
|
52
|
-
const modalOpacity = useSharedValue(0)
|
|
53
|
-
const modalScale = useSharedValue(0.9)
|
|
54
|
-
|
|
55
126
|
const imageSource = useMemo(() => ({
|
|
56
127
|
...imageSourceProps,
|
|
57
128
|
uri: currentMessage?.image,
|
|
@@ -70,23 +141,15 @@ export function MessageImage<TMessage extends IMessage = IMessage> ({
|
|
|
70
141
|
|
|
71
142
|
setIsModalVisible(true)
|
|
72
143
|
|
|
73
|
-
// Animate modal entrance
|
|
74
|
-
modalOpacity.value = withTiming(1, { duration: 300, easing: Easing.out(Easing.ease) })
|
|
75
|
-
modalScale.value = withTiming(1, { duration: 300, easing: Easing.out(Easing.ease) })
|
|
76
|
-
|
|
77
144
|
if (isImageSourceChanged.current || !imageDimensions)
|
|
78
145
|
Image.getSize(imageSource.uri, (width, height) => {
|
|
79
146
|
setImageDimensions({ width, height })
|
|
80
147
|
})
|
|
81
|
-
}, [imageSource.uri, imageDimensions
|
|
148
|
+
}, [imageSource.uri, imageDimensions])
|
|
82
149
|
|
|
83
150
|
const handleModalClose = useCallback(() => {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
modalScale.value = withTiming(0.9, { duration: 200, easing: Easing.in(Easing.ease) }, () => {
|
|
87
|
-
runOnJS(setIsModalVisible)(false)
|
|
88
|
-
})
|
|
89
|
-
}, [modalOpacity, modalScale])
|
|
151
|
+
setIsModalVisible(false)
|
|
152
|
+
}, [])
|
|
90
153
|
|
|
91
154
|
const handleImageLayout = useCallback((e: LayoutChangeEvent) => {
|
|
92
155
|
setImageDimensions({
|
|
@@ -115,11 +178,6 @@ export function MessageImage<TMessage extends IMessage = IMessage> ({
|
|
|
115
178
|
}
|
|
116
179
|
}, [imageDimensions, windowDimensions.height, windowDimensions.width])
|
|
117
180
|
|
|
118
|
-
const modalAnimatedStyle = useAnimatedStyle(() => ({
|
|
119
|
-
opacity: modalOpacity.value,
|
|
120
|
-
transform: [{ scale: modalScale.value }],
|
|
121
|
-
}), [modalOpacity, modalScale])
|
|
122
|
-
|
|
123
181
|
useEffect(() => {
|
|
124
182
|
isImageSourceChanged.current = true
|
|
125
183
|
}, [imageSource.uri])
|
|
@@ -139,39 +197,17 @@ export function MessageImage<TMessage extends IMessage = IMessage> ({
|
|
|
139
197
|
/>
|
|
140
198
|
</TouchableOpacity>
|
|
141
199
|
|
|
142
|
-
{isModalVisible
|
|
143
|
-
<
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
<Text style={styles.closeButtonIcon}>
|
|
154
|
-
{'X'}
|
|
155
|
-
</Text>
|
|
156
|
-
</View>
|
|
157
|
-
</BaseButton>
|
|
158
|
-
</View>
|
|
159
|
-
|
|
160
|
-
<View style={[commonStyles.fill, commonStyles.centerItems]}>
|
|
161
|
-
<Zoom>
|
|
162
|
-
<Image
|
|
163
|
-
style={modalImageDimensions}
|
|
164
|
-
source={imageSource}
|
|
165
|
-
resizeMode='contain'
|
|
166
|
-
{...imageProps}
|
|
167
|
-
/>
|
|
168
|
-
</Zoom>
|
|
169
|
-
</View>
|
|
170
|
-
</View>
|
|
171
|
-
</GestureHandlerRootView>
|
|
172
|
-
</Animated.View>
|
|
173
|
-
</OverKeyboardView>
|
|
174
|
-
)}
|
|
200
|
+
<OverKeyboardView visible={isModalVisible}>
|
|
201
|
+
<SafeAreaProvider>
|
|
202
|
+
<ModalContent
|
|
203
|
+
isVisible={isModalVisible}
|
|
204
|
+
imageSource={imageSource}
|
|
205
|
+
modalImageDimensions={modalImageDimensions}
|
|
206
|
+
imageProps={imageProps}
|
|
207
|
+
onClose={handleModalClose}
|
|
208
|
+
/>
|
|
209
|
+
</SafeAreaProvider>
|
|
210
|
+
</OverKeyboardView>
|
|
175
211
|
</View>
|
|
176
212
|
)
|
|
177
213
|
}
|
|
@@ -193,6 +229,7 @@ const styles = StyleSheet.create({
|
|
|
193
229
|
},
|
|
194
230
|
modalContent: {
|
|
195
231
|
backgroundColor: '#000',
|
|
232
|
+
overflow: 'hidden',
|
|
196
233
|
},
|
|
197
234
|
modalImageContainer: {
|
|
198
235
|
width: '100%',
|