Close swipeable on recycle

This commit is contained in:
Diego Mello 2022-12-13 15:53:04 -03:00
parent 2b06b01bd6
commit 7e849943fe
4 changed files with 220 additions and 198 deletions

View File

@ -51,9 +51,11 @@ const RoomItem = ({
showAvatar, showAvatar,
displayMode, displayMode,
sourceType, sourceType,
hideMentionStatus hideMentionStatus,
touchableRef
}: IRoomItemProps) => ( }: IRoomItemProps) => (
<Touchable <Touchable
ref={touchableRef}
onPress={onPress} onPress={onPress}
onLongPress={onLongPress} onLongPress={onLongPress}
favorite={favorite} favorite={favorite}

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { forwardRef, useImperativeHandle } from 'react';
import Animated, { import Animated, {
useAnimatedGestureHandler, useAnimatedGestureHandler,
useSharedValue, useSharedValue,
@ -18,227 +18,237 @@ import { useWindowDimensions } from 'react-native';
import Touch from '../Touch'; import Touch from '../Touch';
import { ACTION_WIDTH, LONG_SWIPE, SMALL_SWIPE } from './styles'; import { ACTION_WIDTH, LONG_SWIPE, SMALL_SWIPE } from './styles';
import { LeftActions, RightActions } from './Actions'; import { LeftActions, RightActions } from './Actions';
import { ITouchableProps } from './interfaces'; import { ITouchableProps, ITouchableRef } from './interfaces';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { MAX_SIDEBAR_WIDTH } from '../../lib/constants'; import { MAX_SIDEBAR_WIDTH } from '../../lib/constants';
import { useAppSelector } from '../../lib/hooks'; import { useAppSelector } from '../../lib/hooks';
const Touchable = ({ const Touchable = forwardRef<ITouchableRef, ITouchableProps>(
children, (
type, {
onPress, children,
onLongPress, type,
testID, onPress,
favorite, onLongPress,
isRead, testID,
rid, favorite,
toggleFav, isRead,
toggleRead, rid,
hideChannel, toggleFav,
isFocused, toggleRead,
swipeEnabled, hideChannel,
displayMode isFocused,
}: ITouchableProps): React.ReactElement => { swipeEnabled,
const { colors } = useTheme(); displayMode
const { width: deviceWidth } = useWindowDimensions(); },
const isMasterDetail = useAppSelector(state => state.app.isMasterDetail); ref
const width = isMasterDetail ? MAX_SIDEBAR_WIDTH : deviceWidth; ): React.ReactElement => {
const { colors } = useTheme();
const { width: deviceWidth } = useWindowDimensions();
const isMasterDetail = useAppSelector(state => state.app.isMasterDetail);
const width = isMasterDetail ? MAX_SIDEBAR_WIDTH : deviceWidth;
const rowOffSet = useSharedValue(0); const rowOffSet = useSharedValue(0);
const transX = useSharedValue(0); const transX = useSharedValue(0);
const rowState = useSharedValue(0); // 0: closed, 1: right opened, -1: left opened const rowState = useSharedValue(0); // 0: closed, 1: right opened, -1: left opened
let _value = 0; let _value = 0;
const close = () => { const close = () => {
rowState.value = 0; console.log(`${rid} close`);
transX.value = withSpring(0, { overshootClamping: true }); rowState.value = 0;
rowOffSet.value = 0; transX.value = withSpring(0, { overshootClamping: true });
}; rowOffSet.value = 0;
};
const handleToggleFav = () => { useImperativeHandle(ref, () => ({
if (toggleFav) { close
toggleFav(rid, favorite); }));
}
close();
};
const handleToggleRead = () => { const handleToggleFav = () => {
if (toggleRead) { if (toggleFav) {
toggleRead(rid, isRead); toggleFav(rid, favorite);
} }
};
const handleHideChannel = () => {
if (hideChannel) {
hideChannel(rid, type);
}
};
const onToggleReadPress = () => {
handleToggleRead();
close();
};
const onHidePress = () => {
handleHideChannel();
close();
};
const handlePress = () => {
if (rowState.value !== 0) {
close(); close();
return; };
}
if (onPress) {
onPress();
}
};
const handleLongPress = () => { const handleToggleRead = () => {
if (rowState.value !== 0) { if (toggleRead) {
toggleRead(rid, isRead);
}
};
const handleHideChannel = () => {
if (hideChannel) {
hideChannel(rid, type);
}
};
const onToggleReadPress = () => {
handleToggleRead();
close(); close();
return; };
}
if (onLongPress) { const onHidePress = () => {
onLongPress(); handleHideChannel();
} close();
}; };
const onLongPressHandlerStateChange = ({ nativeEvent }: { nativeEvent: HandlerStateChangeEventPayload }) => { const handlePress = () => {
if (nativeEvent.state === State.ACTIVE) { if (rowState.value !== 0) {
handleLongPress(); close();
} return;
}; }
if (onPress) {
onPress();
}
};
const handleRelease = (event: PanGestureHandlerEventPayload) => { const handleLongPress = () => {
const { translationX } = event; if (rowState.value !== 0) {
_value += translationX; close();
let toValue = 0; return;
if (rowState.value === 0) { }
// if no option is opened
if (translationX > 0 && translationX < LONG_SWIPE) { if (onLongPress) {
if (I18n.isRTL) { onLongPress();
}
};
const onLongPressHandlerStateChange = ({ nativeEvent }: { nativeEvent: HandlerStateChangeEventPayload }) => {
if (nativeEvent.state === State.ACTIVE) {
handleLongPress();
}
};
const handleRelease = (event: PanGestureHandlerEventPayload) => {
const { translationX } = event;
_value += translationX;
let toValue = 0;
if (rowState.value === 0) {
// if no option is opened
if (translationX > 0 && translationX < LONG_SWIPE) {
if (I18n.isRTL) {
toValue = 2 * ACTION_WIDTH;
} else {
toValue = ACTION_WIDTH;
}
rowState.value = -1;
} else if (translationX >= LONG_SWIPE) {
toValue = 0;
if (I18n.isRTL) {
handleHideChannel();
} else {
handleToggleRead();
}
} else if (translationX < 0 && translationX > -LONG_SWIPE) {
// open trailing option if he swipe left
if (I18n.isRTL) {
toValue = -ACTION_WIDTH;
} else {
toValue = -2 * ACTION_WIDTH;
}
rowState.value = 1;
} else if (translationX <= -LONG_SWIPE) {
toValue = 0;
rowState.value = 1;
if (I18n.isRTL) {
handleToggleRead();
} else {
handleHideChannel();
}
} else {
toValue = 0;
}
} else if (rowState.value === -1) {
// if left option is opened
if (_value < SMALL_SWIPE) {
toValue = 0;
rowState.value = 0;
} else if (_value > LONG_SWIPE) {
toValue = 0;
rowState.value = 0;
if (I18n.isRTL) {
handleHideChannel();
} else {
handleToggleRead();
}
} else if (I18n.isRTL) {
toValue = 2 * ACTION_WIDTH; toValue = 2 * ACTION_WIDTH;
} else { } else {
toValue = ACTION_WIDTH; toValue = ACTION_WIDTH;
} }
rowState.value = -1; } else if (rowState.value === 1) {
} else if (translationX >= LONG_SWIPE) { // if right option is opened
toValue = 0; if (_value > -2 * SMALL_SWIPE) {
if (I18n.isRTL) { toValue = 0;
handleHideChannel(); rowState.value = 0;
} else { } else if (_value < -LONG_SWIPE) {
handleToggleRead(); if (I18n.isRTL) {
} handleToggleRead();
} else if (translationX < 0 && translationX > -LONG_SWIPE) { } else {
// open trailing option if he swipe left handleHideChannel();
if (I18n.isRTL) { }
} else if (I18n.isRTL) {
toValue = -ACTION_WIDTH; toValue = -ACTION_WIDTH;
} else { } else {
toValue = -2 * ACTION_WIDTH; toValue = -2 * ACTION_WIDTH;
} }
rowState.value = 1;
} else if (translationX <= -LONG_SWIPE) {
toValue = 0;
rowState.value = 1;
if (I18n.isRTL) {
handleToggleRead();
} else {
handleHideChannel();
}
} else {
toValue = 0;
} }
} else if (rowState.value === -1) { transX.value = withSpring(toValue, { overshootClamping: true });
// if left option is opened rowOffSet.value = toValue;
if (_value < SMALL_SWIPE) { _value = toValue;
toValue = 0; };
rowState.value = 0;
} else if (_value > LONG_SWIPE) { const onGestureEvent = useAnimatedGestureHandler({
toValue = 0; onActive: event => {
rowState.value = 0; transX.value = event.translationX + rowOffSet.value;
if (I18n.isRTL) { if (transX.value > 2 * width) transX.value = 2 * width;
handleHideChannel(); },
} else { onEnd: event => {
handleToggleRead(); runOnJS(handleRelease)(event);
}
} else if (I18n.isRTL) {
toValue = 2 * ACTION_WIDTH;
} else {
toValue = ACTION_WIDTH;
} }
} else if (rowState.value === 1) { });
// if right option is opened
if (_value > -2 * SMALL_SWIPE) {
toValue = 0;
rowState.value = 0;
} else if (_value < -LONG_SWIPE) {
if (I18n.isRTL) {
handleToggleRead();
} else {
handleHideChannel();
}
} else if (I18n.isRTL) {
toValue = -ACTION_WIDTH;
} else {
toValue = -2 * ACTION_WIDTH;
}
}
transX.value = withSpring(toValue, { overshootClamping: true });
rowOffSet.value = toValue;
_value = toValue;
};
const onGestureEvent = useAnimatedGestureHandler({ const animatedStyles = useAnimatedStyle(() => ({ transform: [{ translateX: transX.value }] }));
onActive: event => {
transX.value = event.translationX + rowOffSet.value;
if (transX.value > 2 * width) transX.value = 2 * width;
},
onEnd: event => {
runOnJS(handleRelease)(event);
}
});
const animatedStyles = useAnimatedStyle(() => ({ transform: [{ translateX: transX.value }] })); return (
<LongPressGestureHandler onHandlerStateChange={onLongPressHandlerStateChange}>
return ( <Animated.View>
<LongPressGestureHandler onHandlerStateChange={onLongPressHandlerStateChange}> <PanGestureHandler activeOffsetX={[-20, 20]} onGestureEvent={onGestureEvent} enabled={swipeEnabled}>
<Animated.View> <Animated.View>
<PanGestureHandler activeOffsetX={[-20, 20]} onGestureEvent={onGestureEvent} enabled={swipeEnabled}> <LeftActions
<Animated.View> transX={transX}
<LeftActions isRead={isRead}
transX={transX} width={width}
isRead={isRead} onToggleReadPress={onToggleReadPress}
width={width} displayMode={displayMode}
onToggleReadPress={onToggleReadPress} />
displayMode={displayMode} <RightActions
/> transX={transX}
<RightActions favorite={favorite}
transX={transX} width={width}
favorite={favorite} toggleFav={handleToggleFav}
width={width} onHidePress={onHidePress}
toggleFav={handleToggleFav} displayMode={displayMode}
onHidePress={onHidePress} />
displayMode={displayMode} <Animated.View style={animatedStyles}>
/> <Touch
<Animated.View style={animatedStyles}> onPress={handlePress}
<Touch testID={testID}
onPress={handlePress} style={{
testID={testID} backgroundColor: isFocused ? colors.chatComponentBackground : colors.backgroundColor
style={{ }}
backgroundColor: isFocused ? colors.chatComponentBackground : colors.backgroundColor >
}} {children}
> </Touch>
{children} </Animated.View>
</Touch>
</Animated.View> </Animated.View>
</Animated.View> </PanGestureHandler>
</PanGestureHandler> </Animated.View>
</Animated.View> </LongPressGestureHandler>
</LongPressGestureHandler> );
); }
}; );
export default Touchable; export default Touchable;

View File

@ -1,11 +1,11 @@
import React, { useEffect } from 'react'; import React, { useEffect, useRef } from 'react';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { useAppSelector } from '../../lib/hooks'; import { useAppSelector } from '../../lib/hooks';
import { getUserPresence } from '../../lib/methods'; import { getUserPresence } from '../../lib/methods';
import { isGroupChat } from '../../lib/methods/helpers'; import { isGroupChat } from '../../lib/methods/helpers';
import { formatDate } from '../../lib/methods/helpers/room'; import { formatDate } from '../../lib/methods/helpers/room';
import { IRoomItemContainerProps } from './interfaces'; import { IRoomItemContainerProps, ITouchableRef } from './interfaces';
import RoomItem from './RoomItem'; import RoomItem from './RoomItem';
import { ROW_HEIGHT, ROW_HEIGHT_CONDENSED } from './styles'; import { ROW_HEIGHT, ROW_HEIGHT_CONDENSED } from './styles';
@ -40,6 +40,7 @@ const RoomItemContainer = ({
const connected = useAppSelector(state => state.meteor.connected); const connected = useAppSelector(state => state.meteor.connected);
const userStatus = useAppSelector(state => state.activeUsers[id || '']?.status); const userStatus = useAppSelector(state => state.activeUsers[id || '']?.status);
const isDirect = !!(item.t === 'd' && id && !isGroupChat(item)); const isDirect = !!(item.t === 'd' && id && !isGroupChat(item));
const touchableRef = useRef<ITouchableRef>(null);
// When app reconnects, we need to fetch the rendered user's presence // When app reconnects, we need to fetch the rendered user's presence
useEffect(() => { useEffect(() => {
@ -56,6 +57,9 @@ const RoomItemContainer = ({
if (!userStatus && isDirect) { if (!userStatus && isDirect) {
getUserPresence(id); getUserPresence(id);
} }
// TODO: Remove this when we have a better way to close the swipeable
touchableRef?.current?.close();
}, [item.rid]); }, [item.rid]);
const handleOnPress = () => onPress(item); const handleOnPress = () => onPress(item);
@ -79,6 +83,7 @@ const RoomItemContainer = ({
return ( return (
<RoomItem <RoomItem
touchableRef={touchableRef}
name={name} name={name}
avatar={avatar} avatar={avatar}
isGroupChat={isGroupChat(item)} isGroupChat={isGroupChat(item)}

View File

@ -115,6 +115,7 @@ export interface IRoomItemProps extends IBaseRoomItem {
size?: number; size?: number;
sourceType: IOmnichannelSource; sourceType: IOmnichannelSource;
hideMentionStatus?: boolean; hideMentionStatus?: boolean;
touchableRef: React.RefObject<ITouchableRef>;
} }
export interface ILastMessageProps { export interface ILastMessageProps {
@ -126,6 +127,10 @@ export interface ILastMessageProps {
alert: boolean; alert: boolean;
} }
export interface ITouchableRef {
close: () => void;
}
export interface ITouchableProps extends IRoomItemTouchables { export interface ITouchableProps extends IRoomItemTouchables {
children: JSX.Element; children: JSX.Element;
type: SubscriptionType; type: SubscriptionType;