Skip to content
This repository was archived by the owner on Feb 8, 2025. It is now read-only.

Commit 3eae6fe

Browse files
authored
Merge pull request #274 from zallo-labs/Z-328-panes-nav
feat(app): Panes navigator
2 parents c8a1603 + cd79425 commit 3eae6fe

File tree

12 files changed

+163
-97
lines changed

12 files changed

+163
-97
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
import { Panes } from '#/layout/Panes';
2-
import { Slot } from 'expo-router';
3-
import { HomePane } from './index';
1+
import { createPanesNavigator } from '#/layout/PanesNavigator';
2+
3+
const Panes = createPanesNavigator();
44

55
export default function HomeLayout() {
6-
return (
7-
<Panes>
8-
<HomePane />
9-
<Slot />
10-
</Panes>
11-
);
6+
return <Panes />;
127
}
138

149
export { ErrorBoundary } from '#/ErrorBoundary';

app/src/app/(nav)/[account]/(home)/activity/_layout.tsx

-11
This file was deleted.

app/src/app/(nav)/[account]/(home)/activity/index.tsx

+2-6
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ const Query = graphql`
6060
}
6161
`;
6262

63-
function ActivityPane_() {
63+
function ActivityPane() {
6464
const { styles } = useStyles(stylesheet);
6565
const { account } = useLocalParams(AccountParams);
6666

@@ -210,10 +210,6 @@ function withFirstAndLast<T>(items: T[]) {
210210
}));
211211
}
212212

213-
export const ActivityPane = withSuspense(ActivityPane_, <PaneSkeleton />);
214-
215-
export default function ActivityScreen() {
216-
return null;
217-
}
213+
export default withSuspense(ActivityPane, <PaneSkeleton />);
218214

219215
export { ErrorBoundary } from '#/ErrorBoundary';

app/src/app/(nav)/[account]/(home)/index.tsx

+3-6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { graphql } from 'relay-runtime';
1818
import { HomePaneQuery } from '~/api/__generated__/HomePaneQuery.graphql';
1919
import { useLazyQuery } from '~/api/useLazyQuery';
2020
import { Pane } from '#/layout/Pane';
21+
import { PaneOptions } from '#/Appbar/PaneOptions';
2122

2223
const Query = graphql`
2324
query HomePaneQuery($account: UAddress!, $chain: Chain!) {
@@ -45,7 +46,7 @@ const Query = graphql`
4546
}
4647
`;
4748

48-
function HomePane_() {
49+
function HomePane() {
4950
const { styles } = useStyles(stylesheet);
5051
const address = useLocalParams(AccountParams).account;
5152
const chain = asChain(address);
@@ -122,10 +123,6 @@ const stylesheet = createStyles(({ colors, padding }) => ({
122123
},
123124
}));
124125

125-
export const HomePane = withSuspense(HomePane_, <PaneSkeleton />);
126-
127-
export default function HomeScreen() {
128-
return null;
129-
}
126+
export default withSuspense(HomePane, <PaneSkeleton />);
130127

131128
export { ErrorBoundary } from '#/ErrorBoundary/ErrorBoundary';
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
1-
import { Panes } from '#/layout/Panes';
2-
import { Slot } from 'expo-router';
3-
import _ from 'lodash';
4-
import { AccountSettingsPane } from './index';
1+
import { createPanesNavigator } from '#/layout/PanesNavigator';
2+
3+
const Panes = createPanesNavigator();
54

65
export default function AccountSettingsLayout() {
7-
return (
8-
<Panes>
9-
<AccountSettingsPane />
10-
<Slot />
11-
</Panes>
12-
);
6+
return <Panes />;
137
}

app/src/app/(nav)/[account]/settings/index.tsx

+4-7
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ const Query = graphql`
6060

6161
export const AccountSettingsParams = AccountParams;
6262

63-
function AccountSettingsPane_() {
63+
function AccountSettings() {
6464
const { styles } = useStyles(stylesheet);
6565
const { account } = useLocalParams(AccountSettingsParams);
6666
const router = useRouter();
@@ -78,8 +78,9 @@ function AccountSettingsPane_() {
7878
.sort((a, b) => a.key - b.key);
7979
const upgradePolicy = a.policies.find((p) => p.key === PolicyPresetKey.upgrade);
8080

81+
// TODO: Pane fixed
8182
return (
82-
<Pane fixed>
83+
<Pane flex>
8384
<ScrollView contentContainerStyle={styles.pane} showsVerticalScrollIndicator={false}>
8485
<Searchbar
8586
leading={MenuOrSearchIcon}
@@ -208,8 +209,4 @@ const stylesheet = createStyles(({ colors }) => ({
208209
},
209210
}));
210211

211-
export const AccountSettingsPane = withSuspense(AccountSettingsPane_, <PaneSkeleton />);
212-
213-
export default function AccountSettings() {
214-
return null;
215-
}
212+
export default withSuspense(AccountSettings, <PaneSkeleton />);

app/src/app/(nav)/contacts/[address].tsx

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ function ContactScreen_(props: ContactScreenProps) {
9999
<Appbar
100100
mode="small"
101101
headline="Contact"
102+
noPadding
102103
{...(remove && {
103104
trailing: [
104105
(props) => <FormResetIcon control={control} reset={reset} {...props} />,
+4-13
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
1-
import { Panes } from '#/layout/Panes';
2-
import { Slot } from 'expo-router';
3-
import { ContactsPane } from './index';
4-
import { Pane } from '#/layout/Pane';
1+
import { createPanesNavigator } from '#/layout/PanesNavigator';
52

6-
export default function ContactsLayout() {
7-
return (
8-
<Panes>
9-
<Pane fixed>
10-
<ContactsPane />
11-
</Pane>
3+
const Panes = createPanesNavigator();
124

13-
<Slot />
14-
</Panes>
15-
);
5+
export default function ContactsLayout() {
6+
return <Panes />;
167
}

app/src/app/(nav)/contacts/index.tsx

+5-8
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { MenuOrSearchIcon } from '#/Appbar/MenuOrSearchIcon';
1818
import { graphql } from 'relay-runtime';
1919
import { useLazyQuery } from '~/api';
2020
import { contacts_ContactsPaneQuery } from '~/api/__generated__/contacts_ContactsPaneQuery.graphql';
21+
import { Pane } from '#/layout/Pane';
2122

2223
const Query = graphql`
2324
query contacts_ContactsPaneQuery($query: String) {
@@ -29,7 +30,7 @@ const Query = graphql`
2930
}
3031
`;
3132

32-
function ContactsPane_() {
33+
function ContactsPane() {
3334
const { styles } = useStyles(stylesheet);
3435
const router = useRouter();
3536
const contactSelected = usePath().includes('/(nav)/contacts/[address]');
@@ -43,7 +44,7 @@ function ContactsPane_() {
4344
});
4445

4546
return (
46-
<>
47+
<Pane flex>
4748
<Searchbar
4849
leading={MenuOrSearchIcon}
4950
placeholder="Search contacts"
@@ -92,7 +93,7 @@ function ContactsPane_() {
9293
)}
9394

9495
<Fab icon={AddIcon} label="Add contact" onPress={async () => router.push(`/contacts/add`)} />
95-
</>
96+
</Pane>
9697
);
9798
}
9899

@@ -117,10 +118,6 @@ const stylesheet = createStyles(({ colors }) => ({
117118
},
118119
}));
119120

120-
export const ContactsPane = withSuspense(ContactsPane_, <PaneSkeleton />);
121-
122-
export default function ContactsScreen() {
123-
return null;
124-
}
121+
export default withSuspense(ContactsPane, <PaneSkeleton />);
125122

126123
export { ErrorBoundary } from '#/ErrorBoundary';
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { PaneProps } from '#/layout/Pane';
2+
import { PanesNavigationOptions } from '#/layout/PanesNavigator';
3+
import { useNavigation } from 'expo-router';
4+
import { useLayoutEffect } from 'react';
5+
6+
export type PaneOptionsProps = PaneProps;
7+
8+
export function PaneOptions(options: PaneOptionsProps) {
9+
const navigation = useNavigation();
10+
11+
useLayoutEffect(() => {
12+
navigation.setOptions({ pane: options } satisfies Partial<PanesNavigationOptions>);
13+
}, [navigation, options]);
14+
15+
return null;
16+
}

app/src/components/layout/Pane.tsx

+19-27
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,36 @@
11
import { createStyles, useStyles } from '@theme/styles';
22
import { useAtomValue, useSetAtom } from 'jotai';
33
import { useLayoutEffect, useRef } from 'react';
4-
import { ViewProps } from 'react-native';
4+
import { View, ViewProps } from 'react-native';
55
import { PANES_MOUNTED } from './Panes';
6-
import { BREAKPOINTS } from '@theme/styles';
7-
import Animated, { FadeIn } from 'react-native-reanimated';
8-
9-
const MAX_PANES: Record<keyof typeof BREAKPOINTS, number> = {
10-
compact: 1,
11-
medium: 1,
12-
expanded: 2,
13-
large: 2,
14-
extraLarge: 3,
15-
// Injected by unistyles; never actually used
16-
landscape: 1,
17-
portrait: 1,
18-
};
6+
import { useNavigationState, useRoute } from '@react-navigation/native';
197

208
export type PaneProps = ViewProps & {
219
padding?: boolean;
2210
} & ({ fixed: true; flex?: never } | { fixed?: never; flex: true });
2311

2412
export function Pane({ padding = true, flex, fixed: _, ...props }: PaneProps) {
25-
const { styles, breakpoint } = useStyles(stylesheet);
26-
const order = usePaneMounted();
27-
const maxPanes = MAX_PANES[breakpoint];
28-
const count = useAtomValue(PANES_MOUNTED);
13+
const { styles } = useStyles(stylesheet);
14+
// const order = usePaneMounted();
15+
// const maxPanes = MAX_PANES[breakpoint];
16+
// const count = useAtomValue(PANES_MOUNTED);
17+
18+
// const withinWindow = (order.current ?? 0) < count - maxPanes;
19+
// const hidden = count > maxPanes && withinWindow;
20+
// if (count > maxPanes && withinWindow)
21+
// return <View style={{ width: 20, backgroundColor: 'red' }} />;
22+
23+
const route = useRoute();
24+
const isSelected = useNavigationState((state) => state.routes[state.index].key === route.key);
2925

30-
const withinWindow = (order.current ?? 0) < count - maxPanes;
31-
if (count > maxPanes && withinWindow) return null;
26+
console.warn(route.name, { isSelected });
3227

3328
// Flex a fixed pane when it is the only one visible
34-
const fixed = !flex && count !== 1;
29+
const fixed = !flex && !isSelected;
3530

3631
return (
37-
<Animated.View
32+
<View
3833
{...props}
39-
entering={FadeIn.duration(200)}
4034
// exiting depends on how the pane was removed
4135
style={[styles.flex, fixed && styles.fixed, padding && styles.margins, props.style]}
4236
/>
@@ -68,11 +62,9 @@ function usePaneMounted() {
6862
useLayoutEffect(() => {
6963
if (order.current === null) order.current = count;
7064

71-
setCount((count) => {
72-
return count + 1;
73-
});
65+
setCount((count) => count + 1);
7466

75-
return () => setCount((c) => c - 1);
67+
return () => setCount((count) => count - 1);
7668
}, [count, setCount]);
7769

7870
return order;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// PaneNavigator.tsx
2+
import {
3+
createNavigatorFactory,
4+
DefaultNavigatorOptions,
5+
ParamListBase,
6+
TabNavigationState,
7+
useNavigationBuilder,
8+
StackRouter,
9+
} from '@react-navigation/native';
10+
import { Screen } from '@react-navigation/elements';
11+
import { Panes } from './Panes';
12+
import { PaneProps } from './Pane';
13+
import { withLayoutContext } from 'expo-router';
14+
import { BREAKPOINTS, createStyles, useStyles } from '@theme/styles';
15+
16+
const MAX_PANES: Record<keyof typeof BREAKPOINTS, number> = {
17+
compact: 1,
18+
medium: 1,
19+
expanded: 2,
20+
large: 2,
21+
extraLarge: 3,
22+
// Injected by unistyles; never actually used
23+
landscape: 1,
24+
portrait: 1,
25+
};
26+
27+
// eslint-disable-next-line @typescript-eslint/ban-types
28+
export type PanesNavigationEventMap = {};
29+
30+
export interface PanesNavigationOptions {
31+
pane?: PaneProps;
32+
maxPanes?: number;
33+
}
34+
35+
export type PanesNavigatorProps = DefaultNavigatorOptions<
36+
ParamListBase,
37+
TabNavigationState<ParamListBase>,
38+
PanesNavigationOptions,
39+
PanesNavigationEventMap
40+
>;
41+
42+
function PanesNavigator({ initialRouteName, screenOptions, children }: PanesNavigatorProps) {
43+
const { state, descriptors, navigation, NavigationContent } = useNavigationBuilder(StackRouter, {
44+
children,
45+
screenOptions,
46+
initialRouteName,
47+
});
48+
const { breakpoint } = useStyles();
49+
50+
const maxPanes = MAX_PANES[breakpoint];
51+
const routes = state.routes
52+
.map((route) => ({
53+
...route,
54+
position: state.routeNames.indexOf(route.name),
55+
}))
56+
.sort((a, b) => a.position - b.position);
57+
58+
return (
59+
<NavigationContent>
60+
<Panes>
61+
{routes.map((route, i) => {
62+
const { navigation, render, options } = descriptors[route.key];
63+
64+
const focussed = state.index === i;
65+
const shown = routes.length - maxPanes <= i;
66+
67+
return (
68+
<Screen
69+
key={route.key}
70+
focused={focussed}
71+
route={route}
72+
navigation={navigation}
73+
header={null}
74+
headerShown={false}
75+
style={shown ? styles.visible : styles.hidden}
76+
>
77+
{render()}
78+
</Screen>
79+
);
80+
})}
81+
</Panes>
82+
</NavigationContent>
83+
);
84+
}
85+
86+
export const createPanesNavigator = () =>
87+
withLayoutContext(
88+
createNavigatorFactory<
89+
TabNavigationState<ParamListBase>,
90+
PanesNavigationOptions,
91+
PanesNavigationEventMap,
92+
typeof PanesNavigator
93+
>(PanesNavigator)().Navigator,
94+
);
95+
96+
const styles = createStyles({
97+
visible: {},
98+
hidden: {
99+
display: 'none',
100+
},
101+
});

0 commit comments

Comments
 (0)