1- import React , { useRef , useState } from 'react' ;
1+ import React , { useState } from 'react' ;
22import { LayoutChangeEvent , StyleSheet , View } from 'react-native' ;
33
4- import Swipeable , { SwipeableMethods } from 'react-native-gesture-handler/ReanimatedSwipeable' ;
4+ import { Gesture , GestureDetector } from 'react-native-gesture-handler' ;
5+
6+ import Animated , {
7+ Extrapolation ,
8+ interpolate ,
9+ runOnJS ,
10+ useAnimatedStyle ,
11+ useSharedValue ,
12+ withSpring ,
13+ } from 'react-native-reanimated' ;
514
615import {
716 MessageContextValue ,
@@ -24,10 +33,9 @@ const styles = StyleSheet.create({
2433 } ,
2534 contentContainer : { } ,
2635 contentWrapper : {
36+ alignItems : 'center' ,
2737 flexDirection : 'row' ,
28- overflow : 'visible' ,
2938 } ,
30-
3139 lastMessageContainer : {
3240 marginBottom : 12 ,
3341 } ,
@@ -41,6 +49,9 @@ const styles = StyleSheet.create({
4149 rightAlignItems : {
4250 alignItems : 'flex-end' ,
4351 } ,
52+ swipeContentContainer : {
53+ position : 'absolute' ,
54+ } ,
4455} ) ;
4556
4657export type MessageSimplePropsWithContext <
@@ -86,7 +97,6 @@ const MessageSimpleWithContext = <
8697 props : MessageSimplePropsWithContext < StreamChatGenerics > ,
8798) => {
8899 const [ messageContentWidth , setMessageContentWidth ] = useState ( 0 ) ;
89- const swipeableRef = useRef < SwipeableMethods | null > ( null ) ;
90100
91101 const {
92102 alignment,
@@ -134,6 +144,7 @@ const MessageSimpleWithContext = <
134144 messageGroupedSingleOrBottomContainer,
135145 messageGroupedTopContainer,
136146 reactionListTop : { position : reactionPosition } ,
147+ swipeContentContainer,
137148 } ,
138149 } ,
139150 } = useTheme ( ) ;
@@ -190,8 +201,78 @@ const MessageSimpleWithContext = <
190201
191202 const repliesCurveColor = isMessageReceivedOrErrorType ? grey_gainsboro : backgroundColor ;
192203
204+ const translateX = useSharedValue ( 0 ) ;
205+ const touchStart = useSharedValue < { x : number ; y : number } | null > ( null ) ;
206+
207+ const onSwipeToReply = ( ) => {
208+ clearQuotedMessageState ( ) ;
209+ setQuotedMessageState ( message ) ;
210+ } ;
211+
212+ const THRESHOLD = 25 ;
213+
214+ const swipeGesture = Gesture . Pan ( )
215+ . manualActivation ( true )
216+ . onBegin ( ( event ) => {
217+ touchStart . value = { x : event . x , y : event . y } ;
218+ } )
219+ . onTouchesMove ( ( event , state ) => {
220+ if ( ! touchStart . value || ! event . changedTouches . length ) {
221+ state . fail ( ) ;
222+ return ;
223+ }
224+
225+ const xDiff = Math . abs ( event . changedTouches [ 0 ] . x - touchStart . value . x ) ;
226+ const yDiff = Math . abs ( event . changedTouches [ 0 ] . y - touchStart . value . y ) ;
227+ const isHorizontalPanning = xDiff > yDiff ;
228+
229+ if ( isHorizontalPanning ) {
230+ state . activate ( ) ;
231+ } else {
232+ state . fail ( ) ;
233+ }
234+ } )
235+ . onStart ( ( ) => {
236+ translateX . value = 0 ;
237+ } )
238+ . onChange ( ( { translationX } ) => {
239+ if ( translationX > 0 ) {
240+ translateX . value = translationX ;
241+ }
242+ } )
243+ . onEnd ( ( ) => {
244+ if ( translateX . value >= THRESHOLD ) {
245+ runOnJS ( onSwipeToReply ) ( ) ;
246+ runOnJS ( triggerHaptic ) ( 'impactMedium' ) ;
247+ }
248+ translateX . value = withSpring ( 0 , {
249+ dampingRatio : 1 ,
250+ duration : 500 ,
251+ overshootClamping : true ,
252+ stiffness : 1 ,
253+ } ) ;
254+ } ) ;
255+
256+ const messageBubbleAnimatedStyle = useAnimatedStyle ( ( ) => ( {
257+ transform : [ { translateX : translateX . value } ] ,
258+ } ) ) ;
259+
260+ const swipeContentAnimatedStyle = useAnimatedStyle ( ( ) => ( {
261+ opacity : interpolate ( translateX . value , [ 0 , THRESHOLD ] , [ 0 , 1 ] ) ,
262+ transform : [
263+ {
264+ translateX : interpolate (
265+ translateX . value ,
266+ [ 0 , THRESHOLD ] ,
267+ [ - THRESHOLD , 0 ] ,
268+ Extrapolation . CLAMP ,
269+ ) ,
270+ } ,
271+ ] ,
272+ } ) ) ;
273+
193274 const renderMessageBubble = (
194- < >
275+ < View style = { [ styles . contentWrapper , contentWrapper ] } >
195276 < MessageContent
196277 backgroundColor = { backgroundColor }
197278 noBorder = { noBorder }
@@ -200,18 +281,21 @@ const MessageSimpleWithContext = <
200281 { reactionListPosition === 'top' && ReactionListTop ? (
201282 < ReactionListTop messageContentWidth = { messageContentWidth } />
202283 ) : null }
203- </ >
284+ </ View >
204285 ) ;
205286
206- const leftAlignmentProps = {
207- leftThreshold : 100 ,
208- renderLeftActions : ( ) => ( MessageSwipeContent ? < MessageSwipeContent /> : null ) ,
209- } ;
210-
211- const rightAlignmentProps = {
212- renderRightActions : ( ) => ( MessageSwipeContent ? < MessageSwipeContent /> : null ) ,
213- rightThreshold : 100 ,
214- } ;
287+ const renderAnimatedMessageBubble = (
288+ < GestureDetector gesture = { swipeGesture } >
289+ < View style = { [ styles . contentWrapper , contentWrapper ] } >
290+ < Animated . View
291+ style = { [ styles . swipeContentContainer , swipeContentAnimatedStyle , swipeContentContainer ] }
292+ >
293+ { MessageSwipeContent ? < MessageSwipeContent /> : null }
294+ </ Animated . View >
295+ < Animated . View style = { messageBubbleAnimatedStyle } > { renderMessageBubble } </ Animated . View >
296+ </ View >
297+ </ GestureDetector >
298+ ) ;
215299
216300 return (
217301 < View
@@ -271,27 +355,7 @@ const MessageSimpleWithContext = <
271355 ) }
272356 { message . pinned ? < MessagePinnedHeader /> : null }
273357 </ View >
274-
275- { enableSwipeToReply ? (
276- < Swipeable
277- containerStyle = { [ styles . contentWrapper , contentWrapper ] }
278- friction = { 2 }
279- onSwipeableWillOpen = { ( ) => {
280- if ( ! swipeableRef . current ) return ;
281- clearQuotedMessageState ( ) ;
282- setQuotedMessageState ( message ) ;
283- triggerHaptic ( 'impactLight' ) ;
284- swipeableRef . current . close ( ) ;
285- } }
286- ref = { swipeableRef }
287- { ...( alignment === 'left' ? leftAlignmentProps : rightAlignmentProps ) }
288- >
289- { renderMessageBubble }
290- </ Swipeable >
291- ) : (
292- renderMessageBubble
293- ) }
294-
358+ { enableSwipeToReply ? renderAnimatedMessageBubble : renderMessageBubble }
295359 { reactionListPosition === 'bottom' && ReactionListBottom ? < ReactionListBottom /> : null }
296360 < MessageReplies noBorder = { noBorder } repliesCurveColor = { repliesCurveColor } />
297361 < MessageFooter date = { message . created_at } isDeleted = { ! ! isMessageTypeDeleted } />
0 commit comments