Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions mobile_app/app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import Reanimated, {
import { Feather } from '@expo/vector-icons';
import { fontFamily, fontSize, radii, spacing, useTheme } from '@/theme';
import { subscribeDrawer } from '@/hooks/drawerState';
import { closeThreadRef } from '@/hooks/closeThread';
import { messagesFocusedRef } from '@/hooks/messagesFocused';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { appMotion } from '@/src/design-system/motion';
import * as haptics from '@/src/design-system/haptics';
Expand Down Expand Up @@ -341,6 +343,13 @@ export default function TabLayout() {
useEffect(() => {
if (Platform.OS !== 'android') return;
const sub = BackHandler.addEventListener('hardwareBackPress', () => {
// An open chat thread is MessagesScreen state, not a route — back must
// close it first. Gate on focus so a thread left open on the (frozen)
// Messages tab doesn't swallow back presses made from other tabs.
if (messagesFocusedRef.current && closeThreadRef.current) {
closeThreadRef.current();
return true;
}
if (exitWindowRef.current) {
BackHandler.exitApp();
return true;
Expand Down
10 changes: 9 additions & 1 deletion mobile_app/app/onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Alert, Animated, Image, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { type Href, useRouter } from 'expo-router';
import { useIsFocused } from '@react-navigation/native';
import { useWallet } from '@/context/WalletContext';
import { useLxmfContext } from '@/context/LxmfContext';
import { fontFamily, fontSize, spacing } from '@/theme';
Expand All @@ -18,6 +19,7 @@ const TUTORIAL_ROUTE = '/tutorial' as Href;

export default function OnboardingScreen() {
const router = useRouter();
const isFocused = useIsFocused();
const { createWallet, connectMWA, isSolanaMobile, isLoading, isConnected, isInitialized, publicKey, walletMode } = useWallet();
const { displayName: nickname } = useLxmfContext();
const insets = useSafeAreaInsets();
Expand Down Expand Up @@ -73,6 +75,12 @@ const overlayOpacity = useRef(new Animated.Value(0)).current;
}, [router]);

useEffect(() => {
// Only auto-proceed while onboarding is the focused route. unstable_settings
// anchor mounts onboarding BENEATH a deep-linked route (anonmesh://tutorial),
// and router.replace targets the focused route — without this gate, wallet
// hydration replaced the just-opened tutorial with /(tabs), so the deep link
// appeared to do nothing on devices that already have a wallet.
if (!isFocused) return;
if (!isConnected || !publicKey) return;
// Freshly created local wallet → offer the recovery-key backup before the
// user reaches the app. One honest line: the key is device-local, here's the
Expand All @@ -95,7 +103,7 @@ const overlayOpacity = useRef(new Animated.Value(0)).current;
cancelled = true;
clearTimeout(t);
};
}, [isConnected, publicKey, walletMode, proceed]);
}, [isFocused, isConnected, publicKey, walletMode, proceed]);

const handleCreate = useCallback(async () => {
if (isLoading) return;
Expand Down
10 changes: 8 additions & 2 deletions mobile_app/components/messages/PeersDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ function SwipeableGroupRow({ groupName, onLeave, children }: { readonly groupNam
});

const rowAnim = useAnimatedStyle(() => ({ transform: [{ translateX: tx.value }] }));
// The row over the action strip is transparent at rest (activeBg falls back
// to 'transparent'), so the strip can't rely on being occluded — it must
// hide itself. Fade the whole strip in from the first pixels of swipe.
const revealAnim = useAnimatedStyle(() => ({
opacity: interpolate(tx.value, [-REVEAL * 0.15, 0], [1, 0], Extrapolation.CLAMP),
}));
const actionAnim = useAnimatedStyle(() => ({
transform: [{
scale: interpolate(tx.value, [-REVEAL, -REVEAL * 0.4, 0], [1, 0.82, 0.64], Extrapolation.CLAMP),
Expand All @@ -84,7 +90,7 @@ function SwipeableGroupRow({ groupName, onLeave, children }: { readonly groupNam

return (
<View style={S.swipeWrap}>
<View style={S.leaveAction}>
<Reanimated.View style={[S.leaveAction, revealAnim]}>
<Reanimated.View style={actionAnim}>
<Pressable
onPress={confirmLeave}
Expand All @@ -97,7 +103,7 @@ function SwipeableGroupRow({ groupName, onLeave, children }: { readonly groupNam
<Text style={S.leaveText}>LEAVE</Text>
</Pressable>
</Reanimated.View>
</View>
</Reanimated.View>
<GestureDetector gesture={pan}>
<Reanimated.View style={rowAnim}>
{children}
Expand Down
5 changes: 5 additions & 0 deletions mobile_app/hooks/closeThread.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** Module-level ref — MessagesScreen points this at its animated thread-close
* (goBack) while a thread is open, null otherwise. An open thread is screen
* state (activePeerHex), not a route, so the (tabs) layout back handler can't
* pop it — it invokes this instead, before the exit-app double-press flow. */
export const closeThreadRef: { current: (() => void) | null } = { current: null };
9 changes: 9 additions & 0 deletions mobile_app/screens/MessagesScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import type { NetworkMode } from '@/src/infrastructure/network/types';
import { activeConversationRef } from '@/hooks/activeConversation';
import { pendingConversationRef } from '@/hooks/pendingConversation';
import { messagesFocusedRef } from '@/hooks/messagesFocused';
import { closeThreadRef } from '@/hooks/closeThread';
import { useConversationSummaries } from '@/hooks/useConversationSummaries';
import { formatAgo } from '@/utils/time';
import { requestBLEPermissions } from '@/src/utils/blePermissions';
Expand Down Expand Up @@ -640,6 +641,14 @@ export default function MessagesScreen() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activePeerHex]);

// Hardware back must close an open thread before the (tabs) layout's
// exit-toast handler gets a say. The thread is component state, not a route,
// so back can't pop it — expose the animated close while a thread is open.
useEffect(() => {
closeThreadRef.current = activePeerHex !== null ? goBack : null;
return () => { closeThreadRef.current = null; };
}, [activePeerHex, goBack]);

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

const backPan = useMemo(() => Gesture.Pan()
Expand Down
Loading