diff --git a/App.tsx b/App.tsx index 92176989..3b7dc402 100644 --- a/App.tsx +++ b/App.tsx @@ -46,7 +46,7 @@ import LoginScreen from './src/screens/LoginScreen'; import MailTab from './src/screens/MailTab'; import FilesScreen from './src/screens/FilesScreen'; import CalendarScreen from './src/screens/CalendarScreen'; -import ContactsScreen from './src/screens/ContactsScreen'; +import ContactsTab from './src/screens/ContactsTab'; import ContactDetailScreen from './src/screens/ContactDetailScreen'; import ContactFormScreen from './src/screens/ContactFormScreen'; import GroupDetailScreen from './src/screens/GroupDetailScreen'; @@ -325,7 +325,7 @@ function MainTabsNavigator({ navigation }: NativeStackScreenProps , diff --git a/src/components/contacts/ContactListRow.tsx b/src/components/contacts/ContactListRow.tsx index b1e7853e..72e878d9 100644 --- a/src/components/contacts/ContactListRow.tsx +++ b/src/components/contacts/ContactListRow.tsx @@ -19,6 +19,8 @@ interface ContactListRowProps { onPress?: () => void; onLongPress?: () => void; selected?: boolean; + /** The contact open in the detail pane of a split view. */ + active?: boolean; selectionMode?: boolean; secondaryText?: string; } @@ -28,6 +30,7 @@ export default function ContactListRow({ onPress, onLongPress, selected, + active = false, selectionMode, secondaryText, }: ContactListRowProps) { @@ -52,6 +55,7 @@ export default function ContactListRow({ style={({ pressed }) => [ styles.row, pressed && styles.rowPressed, + active && styles.rowActive, selected && styles.rowSelected, ]} > @@ -95,6 +99,13 @@ function makeStyles(c: ThemePalette) { }, rowPressed: { backgroundColor: c.surfaceHover }, rowSelected: { backgroundColor: c.primaryBg }, + // Same as the open message in the mail list (webmail bg-selection + bar). + rowActive: { + backgroundColor: c.selection, + borderLeftWidth: 2, + borderLeftColor: c.primary, + paddingLeft: spacing.lg - 2, + }, checkbox: { width: 22, height: 22, diff --git a/src/navigation/pane.tsx b/src/navigation/pane.tsx new file mode 100644 index 00000000..5ff21676 --- /dev/null +++ b/src/navigation/pane.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { NavigationProvider, type NavigationProp, type ParamListBase } from '@react-navigation/native'; + +/** True inside the detail pane of a split view. */ +const PaneContext = React.createContext(false); + +/** A screen in a split view's detail pane has no back button. */ +export function useInPane(): boolean { + return React.useContext(PaneContext); +} + +/** Routes a pane shows itself instead of pushing them on the stack. */ +export type PaneTarget = { name: string; params?: object }; + +/** + * Runs a stack screen inside a split view's detail pane. The screen's own + * `useNavigation()` / `useRoute()` get the pane: `goBack` closes it, the + * routes `onOpen` accepts open in it, everything else goes to the stack. + */ +export function PaneScreen({ + navigation, + name, + params, + onOpen, + onClose, + children, +}: { + navigation: NavigationProp; + name: string; + params: object; + /** Return true when the pane shows this route itself. */ + onOpen: (target: PaneTarget) => boolean; + onClose: () => void; + children: React.ReactNode; +}) { + const route = React.useMemo(() => ({ key: `pane-${name}`, name, params }), [name, params]); + const paneNavigation = React.useMemo(() => { + const open = (method: 'navigate' | 'push' | 'replace') => (...args: unknown[]) => { + const [target, targetParams] = args as [string, object | undefined]; + if (typeof target === 'string' && onOpen({ name: target, params: targetParams })) return; + // `push` and `replace` only exist on a stack; from a tab they become a + // navigate, which reaches the root stack all the same. + const real = navigation as unknown as Record void) | undefined>; + (real[method] ?? real.navigate)!(...args); + }; + return { + ...navigation, + goBack: onClose, + navigate: open('navigate'), + push: open('push'), + replace: open('replace'), + canGoBack: () => true, + } as unknown as NavigationProp; + }, [navigation, onOpen, onClose]); + return ( + + + {children} + + + ); +} diff --git a/src/screens/ContactDetailScreen.tsx b/src/screens/ContactDetailScreen.tsx index 6d9f7ce0..f72230e1 100644 --- a/src/screens/ContactDetailScreen.tsx +++ b/src/screens/ContactDetailScreen.tsx @@ -6,6 +6,7 @@ import { import * as FileSystem from 'expo-file-system/legacy'; import * as Sharing from 'expo-sharing'; import { SafeAreaView } from 'react-native-safe-area-context'; +import { useInPane } from '../navigation/pane'; import { useNavigation, useRoute, type RouteProp } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { @@ -132,6 +133,7 @@ export default function ContactDetailScreen() { const navigation = useNavigation