mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-04 16:06:41 +00:00
feat: show settings sections and the open page side by side on tablets
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { View, Text, StyleSheet, ScrollView, Pressable, TextInput, Keyboard } from 'react-native';
|
import { View, Text, StyleSheet, ScrollView, Pressable, TextInput, Keyboard } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { useLayout } from '../lib/use-layout';
|
||||||
import {
|
import {
|
||||||
ArrowLeft, LogOut, Settings, ChevronRight, Search, X,
|
ArrowLeft, LogOut, Settings, ChevronRight, Search, X,
|
||||||
Palette, User, Shield, UserPen, Palmtree, Calendar,
|
Palette, User, Shield, UserPen, Palmtree, Calendar,
|
||||||
@@ -173,6 +174,7 @@ export default function SettingsScreen({ onLogout, onBack, onTabSelect }: Settin
|
|||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||||
const [selectedTab, setSelectedTab] = useState<Tab | null>(null);
|
const [selectedTab, setSelectedTab] = useState<Tab | null>(null);
|
||||||
|
const { isSplit } = useLayout();
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
// A tapped search result: its pane scrolls to the setting and flashes it.
|
// A tapped search result: its pane scrolls to the setting and flashes it.
|
||||||
const [pendingHighlight, setPendingHighlight] = useState<{ tab: Tab; label: string } | null>(null);
|
const [pendingHighlight, setPendingHighlight] = useState<{ tab: Tab; label: string } | null>(null);
|
||||||
@@ -269,11 +271,11 @@ export default function SettingsScreen({ onLogout, onBack, onTabSelect }: Settin
|
|||||||
// Settings is a tab and stays mounted behind the other tabs, so hardware
|
// Settings is a tab and stays mounted behind the other tabs, so hardware
|
||||||
// back is only claimed while it is the focused one.
|
// back is only claimed while it is the focused one.
|
||||||
const handleBack = React.useCallback(() => {
|
const handleBack = React.useCallback(() => {
|
||||||
if (selectedTab) closePane();
|
if (selectedTab && !isSplit) closePane();
|
||||||
else if (searchQuery) setSearchQuery('');
|
else if (searchQuery) setSearchQuery('');
|
||||||
else leaveScope();
|
else leaveScope();
|
||||||
}, [selectedTab, searchQuery, closePane, leaveScope]);
|
}, [selectedTab, isSplit, searchQuery, closePane, leaveScope]);
|
||||||
useBackWhileFocused(!!selectedTab || !!searchQuery || !!managedAccount, handleBack);
|
useBackWhileFocused((!!selectedTab && !isSplit) || !!searchQuery || !!managedAccount, handleBack);
|
||||||
|
|
||||||
// The first row of the open pane whose label matches the tapped result
|
// The first row of the open pane whose label matches the tapped result
|
||||||
// calls this once it is laid out.
|
// calls this once it is laid out.
|
||||||
@@ -321,23 +323,30 @@ export default function SettingsScreen({ onLogout, onBack, onTabSelect }: Settin
|
|||||||
setPendingHighlight({ tab: tab.id, label: sub.label });
|
setPendingHighlight({ tab: tab.id, label: sub.label });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (selectedTab) {
|
// On a wide window the section list and the open page sit side by side;
|
||||||
|
// the first page is open from the start, so the right side is never empty.
|
||||||
|
const firstTab = groupedTabs.flatMap((g) => g.items).find((tab) => tab.implemented && !unavailableTabs.has(tab.id))?.id ?? null;
|
||||||
|
const shownTab = selectedTab ?? (isSplit ? firstTab : null);
|
||||||
|
|
||||||
|
const renderPage = (selectedTab: Tab) => {
|
||||||
const tabDef = TABS.find((tab) => tab.id === selectedTab)!;
|
const tabDef = TABS.find((tab) => tab.id === selectedTab)!;
|
||||||
const Component = TAB_COMPONENTS[selectedTab];
|
const Component = TAB_COMPONENTS[selectedTab];
|
||||||
const TabIcon = tabDef.icon;
|
const TabIcon = tabDef.icon;
|
||||||
const tabLabel = t(`settings.tabs.${tabDef.id}`, tabDef.label);
|
const tabLabel = t(`settings.tabs.${tabDef.id}`, tabDef.label);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container} edges={['top']}>
|
<SafeAreaView style={styles.container} edges={isSplit ? ['top', 'right'] : ['top', 'left', 'right']}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Pressable
|
{!isSplit && (
|
||||||
onPress={closePane}
|
<Pressable
|
||||||
accessibilityRole="button"
|
onPress={closePane}
|
||||||
accessibilityLabel={t('common.back', 'Back')}
|
accessibilityRole="button"
|
||||||
style={({ pressed }) => [styles.headerBackBtn, pressed && styles.headerBackBtnPressed]}
|
accessibilityLabel={t('common.back', 'Back')}
|
||||||
>
|
style={({ pressed }) => [styles.headerBackBtn, pressed && styles.headerBackBtnPressed]}
|
||||||
<ArrowLeft size={20} color={c.text} />
|
>
|
||||||
</Pressable>
|
<ArrowLeft size={20} color={c.text} />
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
<TabIcon size={20} color={c.mutedForeground} />
|
<TabIcon size={20} color={c.mutedForeground} />
|
||||||
<Text style={styles.headerTitle}>{tabLabel}</Text>
|
<Text style={styles.headerTitle}>{tabLabel}</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -356,10 +365,10 @@ export default function SettingsScreen({ onLogout, onBack, onTabSelect }: Settin
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
const list = (
|
||||||
<SafeAreaView style={styles.container} edges={['top']}>
|
<SafeAreaView style={styles.container} edges={isSplit ? ['top', 'left'] : ['top', 'left', 'right']}>
|
||||||
{/* Header - matches webmail mobile: h-14, border-b, back button + icon + title.
|
{/* Header - matches webmail mobile: h-14, border-b, back button + icon + title.
|
||||||
Settings is a tab root, so the back button is only rendered when a
|
Settings is a tab root, so the back button is only rendered when a
|
||||||
parent passes onBack (it'd otherwise lead nowhere). */}
|
parent passes onBack (it'd otherwise lead nowhere). */}
|
||||||
@@ -437,10 +446,13 @@ export default function SettingsScreen({ onLogout, onBack, onTabSelect }: Settin
|
|||||||
key={tab.id}
|
key={tab.id}
|
||||||
onPress={() => handleTabPress(tab)}
|
onPress={() => handleTabPress(tab)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityState={{ disabled, selected: isSplit && tab.id === shownTab }}
|
||||||
style={({ pressed }) => [
|
style={({ pressed }) => [
|
||||||
styles.tabItem,
|
styles.tabItem,
|
||||||
disabled && styles.tabItemDisabled,
|
disabled && styles.tabItemDisabled,
|
||||||
!disabled && pressed && styles.tabItemPressed,
|
!disabled && pressed && styles.tabItemPressed,
|
||||||
|
isSplit && tab.id === shownTab && styles.tabItemActive,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<View style={styles.tabItemLeft}>
|
<View style={styles.tabItemLeft}>
|
||||||
@@ -504,11 +516,25 @@ export default function SettingsScreen({ onLogout, onBack, onTabSelect }: Settin
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!isSplit) return shownTab ? renderPage(shownTab) : list;
|
||||||
|
return (
|
||||||
|
<View style={styles.splitRow}>
|
||||||
|
<View style={[styles.splitList, { width: SETTINGS_LIST_WIDTH }]}>{list}</View>
|
||||||
|
<View style={styles.splitPage}>{shownTab ? renderPage(shownTab) : null}</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The section list beside an open page on a wide window. */
|
||||||
|
const SETTINGS_LIST_WIDTH = 320;
|
||||||
|
|
||||||
function makeStyles(c: ThemePalette) {
|
function makeStyles(c: ThemePalette) {
|
||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: c.background },
|
container: { flex: 1, backgroundColor: c.background },
|
||||||
|
splitRow: { flex: 1, flexDirection: 'row', backgroundColor: c.background },
|
||||||
|
splitList: { borderRightWidth: 1, borderRightColor: c.border },
|
||||||
|
splitPage: { flex: 1 },
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -529,7 +555,9 @@ function makeStyles(c: ThemePalette) {
|
|||||||
|
|
||||||
scrollArea: { flex: 1 },
|
scrollArea: { flex: 1 },
|
||||||
scrollContent: { paddingBottom: 40 },
|
scrollContent: { paddingBottom: 40 },
|
||||||
detailContent: { padding: spacing.lg, paddingBottom: 40 },
|
// A page keeps form width on a wide pane: labels and their controls
|
||||||
|
// 700 dp apart are hard to read across.
|
||||||
|
detailContent: { padding: spacing.lg, paddingBottom: 40, width: '100%', maxWidth: 720, alignSelf: 'center' },
|
||||||
|
|
||||||
tabList: { paddingVertical: spacing.sm },
|
tabList: { paddingVertical: spacing.sm },
|
||||||
|
|
||||||
@@ -594,6 +622,8 @@ function makeStyles(c: ThemePalette) {
|
|||||||
paddingHorizontal: 20, paddingVertical: 14,
|
paddingHorizontal: 20, paddingVertical: 14,
|
||||||
},
|
},
|
||||||
tabItemPressed: { backgroundColor: c.muted },
|
tabItemPressed: { backgroundColor: c.muted },
|
||||||
|
// The page open beside the list (webmail sidebar: accent background).
|
||||||
|
tabItemActive: { backgroundColor: c.selection },
|
||||||
tabItemLeft: {
|
tabItemLeft: {
|
||||||
flexDirection: 'row', alignItems: 'center',
|
flexDirection: 'row', alignItems: 'center',
|
||||||
gap: spacing.md, flex: 1,
|
gap: spacing.md, flex: 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user