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.
Files changed (31) hide show
  1. package/CHANGELOG.md +295 -0
  2. package/README.md +12 -30
  3. package/package.json +1 -1
  4. package/src/Actions.tsx +27 -18
  5. package/src/Composer.tsx +60 -80
  6. package/src/Constant.ts +0 -9
  7. package/src/GiftedChat/index.tsx +13 -38
  8. package/src/GiftedChat/types.ts +8 -6
  9. package/src/InputToolbar.tsx +6 -11
  10. package/src/MessageImage.tsx +94 -57
  11. package/src/{MessageContainer → MessagesContainer}/components/Item/index.tsx +21 -17
  12. package/src/{MessageContainer → MessagesContainer}/components/Item/types.ts +3 -2
  13. package/src/{MessageContainer → MessagesContainer}/index.tsx +16 -14
  14. package/src/{MessageContainer → MessagesContainer}/types.ts +4 -2
  15. package/src/Send.tsx +40 -22
  16. package/src/__tests__/DayAnimated.test.tsx +1 -1
  17. package/src/__tests__/{MessageContainer.test.tsx → MessagesContainer.test.tsx} +7 -7
  18. package/src/__tests__/__snapshots__/Actions.test.tsx.snap +31 -23
  19. package/src/__tests__/__snapshots__/Composer.test.tsx.snap +39 -30
  20. package/src/__tests__/__snapshots__/Constant.test.tsx.snap +0 -2
  21. package/src/__tests__/__snapshots__/InputToolbar.test.tsx.snap +112 -31
  22. package/src/__tests__/__snapshots__/MessageImage.test.tsx.snap +251 -0
  23. package/src/__tests__/__snapshots__/Send.test.tsx.snap +189 -49
  24. package/src/index.ts +1 -1
  25. package/src/styles.ts +5 -0
  26. package/src/types.ts +1 -1
  27. package/CHANGELOG_2.8.1_to_2.8.2-alpha.5.md +0 -374
  28. /package/src/{MessageContainer → MessagesContainer}/components/DayAnimated/index.tsx +0 -0
  29. /package/src/{MessageContainer → MessagesContainer}/components/DayAnimated/styles.ts +0 -0
  30. /package/src/{MessageContainer → MessagesContainer}/components/DayAnimated/types.ts +0 -0
  31. /package/src/{MessageContainer → MessagesContainer}/styles.ts +0 -0
package/src/Composer.tsx CHANGED
@@ -1,120 +1,100 @@
1
- import React, { useCallback, useRef } from 'react'
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 { MIN_COMPOSER_HEIGHT } from './Constant'
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 determineInputSizeChange = useCallback(
33
- (dimensions: { width: number, height: number }) => {
34
- // Support earlier versions of React Native on Android.
35
- if (!dimensions)
36
- return
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
- if (
39
- !dimensionsRef.current ||
40
- (dimensionsRef.current &&
41
- (dimensionsRef.current.width !== dimensions.width ||
42
- dimensionsRef.current.height !== dimensions.height))
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
- const handleContentSizeChange = useCallback(
52
- ({
53
- nativeEvent: { contentSize },
54
- }: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) =>
55
- determineInputSizeChange(contentSize),
56
- [determineInputSizeChange]
57
- )
46
+ return undefined
47
+ }, [minHeight])
58
48
 
59
- const placeholder = textInputProps?.placeholder ?? 'Type a message...'
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
- <TextInput
63
- testID={placeholder}
64
- accessible
65
- accessibilityLabel={placeholder}
66
- placeholderTextColor={textInputProps?.placeholderTextColor ?? (isDark ? '#888' : Color.defaultColor)}
67
- onContentSizeChange={handleContentSizeChange}
68
- value={text}
69
- enablesReturnKeyAutomatically
70
- underlineColorAndroid='transparent'
71
- keyboardAppearance={isDark ? 'dark' : 'default'}
72
- multiline
73
- placeholder={placeholder}
74
- {...textInputProps}
75
- style={[
76
- stylesCommon.fill,
77
- styles.textInput,
78
- styles[`textInput_${colorScheme}`],
79
- textInputProps?.style,
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
- ...Platform.select({
101
- web: {
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
 
@@ -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 { MAX_COMPOSER_HEIGHT, MIN_COMPOSER_HEIGHT, TEST_ID } from '../Constant'
24
+ import { TEST_ID } from '../Constant'
25
25
  import { GiftedChatContext } from '../GiftedChatContext'
26
26
  import { InputToolbar } from '../InputToolbar'
27
- import { MessageContainer, AnimatedList } from '../MessageContainer'
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 messageContainerRef = useMemo(
65
- () => props.messageContainerRef || createRef<AnimatedList<TMessage>>(),
66
- [props.messageContainerRef]
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 (!messageContainerRef?.current)
87
+ if (!messagesContainerRef?.current)
93
88
  return
94
89
 
95
90
  if (isInverted) {
96
- messageContainerRef.current.scrollToOffset({
91
+ messagesContainerRef.current.scrollToOffset({
97
92
  offset: 0,
98
93
  animated: isAnimated,
99
94
  })
100
95
  return
101
96
  }
102
97
 
103
- messageContainerRef.current.scrollToEnd({ animated: isAnimated })
98
+ messagesContainerRef.current.scrollToEnd({ animated: isAnimated })
104
99
  },
105
- [isInverted, messageContainerRef]
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
- <MessageContainer<TMessage>
111
+ <MessagesContainer<TMessage>
117
112
  {...messagesContainerProps}
118
113
  isInverted={isInverted}
119
114
  messages={messages}
120
- forwardRef={messageContainerRef}
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
- messageContainerRef,
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, minComposerHeight, notifyInputTextReset, getTextFromProp]
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,
@@ -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<MessageContainerProps<TMessage>> {
33
- /* Message container ref */
34
- messageContainerRef?: RefObject<AnimatedList<TMessage>>
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 video */
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 MessageContainer (separate from the ListView) */
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
  }
@@ -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 style={[styles.container, colorScheme === 'dark' && styles.container_dark, containerStyle]}>
100
- <View style={[styles.primary, props.primaryStyle]}>
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
  })
@@ -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, modalOpacity, modalScale])
148
+ }, [imageSource.uri, imageDimensions])
82
149
 
83
150
  const handleModalClose = useCallback(() => {
84
- // Animate modal exit
85
- modalOpacity.value = withTiming(0, { duration: 200, easing: Easing.in(Easing.ease) })
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
- <OverKeyboardView visible={isModalVisible}>
144
- <StatusBar animated barStyle='dark-content' />
145
- <Animated.View style={[styles.modalOverlay, modalAnimatedStyle]}>
146
- <GestureHandlerRootView style={commonStyles.fill}>
147
- <View style={[commonStyles.fill, styles.modalContent, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
148
-
149
- {/* close button */}
150
- <View style={styles.closeButtonContainer}>
151
- <BaseButton onPress={handleModalClose}>
152
- <View style={styles.closeButtonContent}>
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%',