Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
4bf4b21
chore: replace react-native-restart with expo reloadAppAsync
diegolmello Sep 14, 2026
b36586c
chore: replace react-native-background-timer with setInterval
diegolmello Sep 14, 2026
4b1f58b
chore: bump react-native-localize to 3.7.0
diegolmello Sep 14, 2026
22c1182
chore: replace react-native-file-viewer with expo-quick-look
diegolmello Sep 14, 2026
3230b78
chore: migrate from expo-av to expo-audio and expo-video (#6719)
diegolmello Sep 15, 2026
6978e70
chore: bump react-native-mmkv and react-native-nitro-modules
OtavioStasiak Sep 15, 2026
f96be07
chore: drop dead react-native-mmkv patch and tighten its jest mock
OtavioStasiak Sep 15, 2026
559b2f7
chore: RN 0.86 / Expo 57 dependencies, toolchain and patches (#7693)
diegolmello Sep 15, 2026
5219014
fix: adapt app code to React Native 0.86 and Expo 57 (#7694)
diegolmello Sep 15, 2026
998957d
fix(ios): support React Native 0.86 and Expo 57 (#7696)
diegolmello Sep 16, 2026
0e8a91b
fix: update Android build for React Native 0.86 (#7695)
diegolmello Sep 16, 2026
7498ec7
Merge develop into React Native 0.86 upgrade
diegolmello Sep 16, 2026
5070263
ci: make TypeScript check blocking again
diegolmello Sep 16, 2026
a12e23b
fix: restore default submit behavior on login email field
diegolmello Sep 16, 2026
d0fcbad
fix: keep test identifiers on iOS accessibility elements
diegolmello Sep 16, 2026
f65faa6
fix: expose test identifiers on accessible elements
diegolmello Sep 16, 2026
df6a67a
fix: keep a single test identifier per element
diegolmello Sep 16, 2026
a2d8ba2
fix: remove OrSeparator from accessibility focus chain
diegolmello Sep 16, 2026
6c6e7a2
chore(ios): re-enable prebuilt React-Core and Hermes V1 (#7698)
diegolmello Sep 17, 2026
c770155
fix(e2e): anchor keyboard navigation login on focus (#7703)
diegolmello Sep 17, 2026
e5b26d4
chore(deps): bump reanimated to 4.6.0 and worklets to 0.12.2 (#7706)
diegolmello Sep 18, 2026
18d5f9d
chore(deps): bump react-native-screens to 4.28.0 (#7709)
diegolmello Sep 18, 2026
da56123
chore(deps): bump @react-navigation family (#7710)
diegolmello Sep 18, 2026
29c926f
chore(deps): bump react-native-safe-area-context to 5.10.0 (#7711)
diegolmello Sep 18, 2026
d8be466
Merge origin/develop into React Native 0.86 upgrade
diegolmello Sep 21, 2026
5c0c03a
fix(ios): adopt UIScene lifecycle for iOS 27
diegolmello Sep 22, 2026
2f86dc4
fix(android): stop gesture-handler buttons hijacking hardware Enter/D…
diegolmello Sep 22, 2026
49c7994
fix(room-list): close other swiped room items when a new one opens
diegolmello Sep 23, 2026
6c6bdf5
test(room-list): drop tautological swipe test, keep mock reverted
diegolmello Sep 23, 2026
4322d52
fix(room-list): close open row on press-in of another row
diegolmello Sep 23, 2026
aba4a85
wip: gesture manual restructuring attempt (bisect checkpoint)
diegolmello Sep 23, 2026
f0c0c80
fix(room-list): close open swipe row via plain JS registry
diegolmello Sep 23, 2026
cb0d380
fix(room-list): swallow tap that only closes another open row
diegolmello Sep 23, 2026
4a6edf1
fix(room-list): close other row's swipe animation from the UI thread
diegolmello Sep 23, 2026
c449cf5
feat(room-list): WhatsApp-style swipe actions and release spring
diegolmello Sep 23, 2026
7ee5794
fix(room-list): match left action width and tighten swipe release rules
diegolmello Sep 23, 2026
59b5c84
fix(room-list): refresh row when its record changes
diegolmello Sep 23, 2026
a4ee602
refactor(room-list): write gesture flag with shared value set
diegolmello Sep 23, 2026
f1f0180
fix(a11y): close drawer when navigating to the focused sidebar route …
diegolmello Sep 23, 2026
e316723
fix(room-list): show selected room tint only in master-detail
diegolmello Sep 24, 2026
7fae648
fix(room-list): re-render row on every record change
diegolmello Sep 24, 2026
c3268bc
fix(room-list): read swipe offset from shared value
diegolmello Sep 24, 2026
4718502
test(room-list): assert close resets translation
diegolmello Sep 24, 2026
26af173
refactor(room-list): observe row record without opting out of the com…
diegolmello Sep 24, 2026
ca7b341
fix(room-list): reset closed-row tap guard on every touch
diegolmello Sep 24, 2026
f3ac0f7
fix(room-list): close previously open row when another row opens
diegolmello Sep 24, 2026
ebc314c
refactor(room-list): address review feedback
diegolmello Sep 24, 2026
e7a9d5c
ci(ios): restore Xcode version pin
diegolmello Sep 25, 2026
1f12a34
Merge remote-tracking branch 'origin/diegolmello/native-1561-rn-upgra…
diegolmello Sep 25, 2026
e96f669
ci(ios): pin Xcode 26.6
diegolmello Sep 25, 2026
5430efd
perf(room-list): mount swipe actions only while the row is moved
diegolmello Sep 28, 2026
989de54
chore: tidy RN 0.86 upgrade leftovers
diegolmello Sep 28, 2026
681d520
chore(deps): upgrade react-native-gesture-handler to 3.3.0 (#7726)
diegolmello Sep 29, 2026
1c6bd41
Merge branch 'develop' into diegolmello/native-1561-rn-upgrade
diegolmello Sep 29, 2026
e383ca4
test: refresh audio button snapshots after gesture handler upgrade
diegolmello Sep 29, 2026
e1fe84c
fix: restore React Native Gradle plugin composite build
diegolmello Sep 29, 2026
4c741f4
Merge remote-tracking branch 'origin/diegolmello/native-1561-rn-upgra…
diegolmello Sep 29, 2026
c8cc2ee
refactor(RoomItem): settle swipe on the UI thread and mirror it in RTL
diegolmello Sep 29, 2026
d9b9501
fix(RoomItem): close the open row when a swipe-disabled row is tapped
diegolmello Sep 29, 2026
ca4dbdc
Merge remote-tracking branch 'origin/develop' into diegolmello/global…
diegolmello Sep 30, 2026
54c5b57
refactor(RoomItem): move swipe utils and room snapshot hook into subf…
diegolmello Sep 30, 2026
5b3c08c
Merge branch 'develop' into diegolmello/global-room-item-swipe
diegolmello Sep 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 69 additions & 95 deletions app/containers/RoomItem/Actions.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,53 @@
import { memo } from 'react';
import { View } from 'react-native';
import Animated, {
type SharedValue,
useAnimatedStyle,
interpolate,
withSpring,
useAnimatedReaction,
useSharedValue
useDerivedValue,
useSharedValue,
withTiming
} from 'react-native-reanimated';
import * as Haptics from 'expo-haptics';
import { scheduleOnRN } from 'react-native-worklets';

import { RectButton } from '~/containers/GestureButtons';
import { CustomIcon } from '../CustomIcon';
import { DisplayMode } from '~/lib/constants/constantDisplayMode';
import styles, { ACTION_WIDTH, LONG_SWIPE } from './styles';
import styles from './styles';
import { getActionWidth, getFullSwipeThreshold } from './utils/swipeRelease';
import { type ILeftActionsProps, type IRightActionsProps } from './interfaces';
import { useTheme } from '~/theme';
import I18n from '~/i18n';
import { useResponsiveLayout } from '~/lib/hooks/useResponsiveLayout/useResponsiveLayout';

const CONDENSED_ICON_SIZE = 24;
const EXPANDED_ICON_SIZE = 28;
const EXPAND_DURATION = 300;

const useHideExpandProgress = (transX: SharedValue<number>, width: number) => {
const fullSwipeThreshold = getFullSwipeThreshold(width);
const expandProgress = useSharedValue(0);

useAnimatedReaction(
() => -transX.value >= fullSwipeThreshold,
(isCrossed, wasCrossed) => {
if (isCrossed !== wasCrossed) {
expandProgress.value = withTiming(isCrossed ? 1 : 0, { duration: EXPAND_DURATION });
}
}
);

return expandProgress;
};

export const LeftActions = memo(({ transX, isRead, width, onToggleReadPress, displayMode }: ILeftActionsProps) => {
const { colors } = useTheme();

const { rowHeight, rowHeightCondensed } = useResponsiveLayout();

const animatedStyles = useAnimatedStyle(() => ({
transform: [{ translateX: transX.value }]
const actionWidth = getActionWidth(width);

const animatedButtonStyles = useAnimatedStyle(() => ({
width: Math.max(transX.value, 0)
}));

const isCondensed = displayMode === DisplayMode.Condensed;
Expand All @@ -43,23 +62,23 @@ export const LeftActions = memo(({ transX, isRead, width, onToggleReadPress, dis
<Animated.View
style={[
styles.actionLeftButtonContainer,
{ width: width * 2, backgroundColor: colors.badgeBackgroundLevel2, right: '100%' },
{ backgroundColor: colors.badgeBackgroundLevel2 },
viewHeight,
animatedStyles
animatedButtonStyles
]}>
<View style={[styles.actionLeftButtonContainer, viewHeight]}>
<RectButton
accessible={false}
accessibilityLabel={I18n.t(isRead ? 'Mark_unread' : 'Mark_read')}
style={styles.actionButton}
onPress={onToggleReadPress}>
<RectButton
accessible={false}
accessibilityLabel={I18n.t(isRead ? 'Mark_unread' : 'Mark_read')}
style={[styles.actionButton, styles.actionButtonContentEnd]}
onPress={onToggleReadPress}>
<View style={[styles.actionIconSlot, { width: actionWidth }]}>
<CustomIcon
size={isCondensed ? CONDENSED_ICON_SIZE : EXPANDED_ICON_SIZE}
name={isRead ? 'flag' : 'check'}
color={colors.fontWhite}
/>
</RectButton>
</View>
</View>
</RectButton>
</Animated.View>
</View>
);
Expand All @@ -70,55 +89,23 @@ export const RightActions = memo(({ transX, favorite, width, toggleFav, onHidePr

const { rowHeight, rowHeightCondensed } = useResponsiveLayout();

const animatedFavStyles = useAnimatedStyle(() => ({ transform: [{ translateX: transX.value }] }));

const translateXHide = useSharedValue(0);
const actionWidth = getActionWidth(width);
const expandProgress = useHideExpandProgress(transX, width);

const triggerHideAnimation = (toValue: number) => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
translateXHide.value = withSpring(toValue, { overshootClamping: true, mass: 0.7 });
};
const buttonWidths = useDerivedValue(() => {
const reveal = Math.max(-transX.value, 0);
const favorite = (reveal / 2) * (1 - expandProgress.value);
return { favorite, hide: reveal - favorite };
});

useAnimatedReaction(
() => transX.value,
(currentTransX, previousTransX) => {
// Triggers the animation and hapticFeedback if swipe reaches/unreaches the threshold.
if (I18n.isRTL) {
if (previousTransX && currentTransX > LONG_SWIPE && previousTransX <= LONG_SWIPE) {
scheduleOnRN(triggerHideAnimation, ACTION_WIDTH);
} else if (previousTransX && currentTransX <= LONG_SWIPE && previousTransX > LONG_SWIPE) {
scheduleOnRN(triggerHideAnimation, 0);
}
} else if (previousTransX && currentTransX < -LONG_SWIPE && previousTransX >= -LONG_SWIPE) {
scheduleOnRN(triggerHideAnimation, -ACTION_WIDTH);
} else if (previousTransX && currentTransX >= -LONG_SWIPE && previousTransX < -LONG_SWIPE) {
scheduleOnRN(triggerHideAnimation, 0);
}
}
);
const animatedFavStyles = useAnimatedStyle(() => ({
width: buttonWidths.value.favorite,
right: buttonWidths.value.hide
}));

const animatedHideStyles = useAnimatedStyle(() => {
if (I18n.isRTL) {
if (transX.value < LONG_SWIPE && transX.value >= 2 * ACTION_WIDTH) {
const parallaxSwipe = interpolate(
transX.value,
[2 * ACTION_WIDTH, LONG_SWIPE],
[ACTION_WIDTH, ACTION_WIDTH + 0.1 * transX.value]
);
return { transform: [{ translateX: parallaxSwipe + translateXHide.value }] };
}
return { transform: [{ translateX: transX.value - ACTION_WIDTH + translateXHide.value }] };
}
if (transX.value > -LONG_SWIPE && transX.value <= -2 * ACTION_WIDTH) {
const parallaxSwipe = interpolate(
transX.value,
[-2 * ACTION_WIDTH, -LONG_SWIPE],
[-ACTION_WIDTH, -ACTION_WIDTH + 0.1 * transX.value]
);
return { transform: [{ translateX: parallaxSwipe + translateXHide.value }] };
}
return { transform: [{ translateX: transX.value + ACTION_WIDTH + translateXHide.value }] };
});
const animatedHideStyles = useAnimatedStyle(() => ({
width: buttonWidths.value.hide
}));

const isCondensed = displayMode === DisplayMode.Condensed;
const viewHeight = { height: isCondensed ? rowHeightCondensed : rowHeight };
Expand All @@ -130,49 +117,36 @@ export const RightActions = memo(({ transX, favorite, width, toggleFav, onHidePr
accessibilityElementsHidden
importantForAccessibility='no'>
<Animated.View
style={[
styles.actionRightButtonContainer,
{
width,
backgroundColor: colors.statusFontWarning,
left: '100%'
},
viewHeight,
animatedFavStyles
]}>
style={[styles.actionRightButtonContainer, { backgroundColor: colors.statusFontWarning }, viewHeight, animatedFavStyles]}>
<RectButton
accessible={false}
accessibilityLabel={I18n.t(favorite ? 'Unfavorite' : 'Favorite')}
style={[styles.actionButton, { backgroundColor: colors.statusFontWarning }]}
style={styles.actionButton}
onPress={toggleFav}>
<CustomIcon
size={isCondensed ? CONDENSED_ICON_SIZE : EXPANDED_ICON_SIZE}
name={favorite ? 'star-filled' : 'star'}
color={colors.fontWhite}
/>
<View style={[styles.actionIconSlot, { width: actionWidth }]}>
<CustomIcon
size={isCondensed ? CONDENSED_ICON_SIZE : EXPANDED_ICON_SIZE}
name={favorite ? 'star-filled' : 'star'}
color={colors.fontWhite}
/>
</View>
</RectButton>
</Animated.View>
<Animated.View
style={[
styles.actionRightButtonContainer,
{
width: width * 2,
backgroundColor: colors.buttonBackgroundSecondaryPress,
left: '100%'
},
{ right: 0, backgroundColor: colors.buttonBackgroundSecondaryPress },
viewHeight,
animatedHideStyles
]}>
<RectButton
accessible={false}
accessibilityLabel={I18n.t('Hide')}
style={[styles.actionButton, { backgroundColor: colors.buttonBackgroundSecondaryPress }]}
onPress={onHidePress}>
<CustomIcon
size={isCondensed ? CONDENSED_ICON_SIZE : EXPANDED_ICON_SIZE}
name='unread-on-top-disabled'
color={colors.fontWhite}
/>
<RectButton accessible={false} accessibilityLabel={I18n.t('Hide')} style={styles.actionButton} onPress={onHidePress}>
<View style={[styles.actionIconSlot, { width: actionWidth }]}>
<CustomIcon
size={isCondensed ? CONDENSED_ICON_SIZE : EXPANDED_ICON_SIZE}
name='unread-on-top-disabled'
color={colors.fontWhite}
/>
</View>
</RectButton>
</Animated.View>
</View>
Expand Down
Loading
Loading