Compare commits

...

6 Commits

Author SHA1 Message Date
Eric Bailey ac15d69151 Revert settings component changes 2024-12-02 16:32:12 -06:00
Eric Bailey a00672a7c6 Swap out on all settings screens 2024-12-02 16:31:19 -06:00
Eric Bailey 703d13e3f7 Good on all platforms 2024-12-02 16:30:53 -06:00
Eric Bailey 55a7b2ec67 Clean up new header 2024-12-02 14:05:53 -06:00
Eric Bailey 40eb45baad Build out header, WIP 2024-12-01 17:13:52 -06:00
Eric Bailey 8cd6763811 Centered layout 2024-12-01 16:22:56 -06:00
20 changed files with 498 additions and 168 deletions
+1
View File
@@ -20,6 +20,7 @@ export * from '#/alf/types'
export * from '#/alf/util/flatten'
export * from '#/alf/util/platform'
export * from '#/alf/util/themeSelector'
export * from '#/alf/util/useGutterStyles'
export type Alf = {
themeName: ThemeName
+21
View File
@@ -0,0 +1,21 @@
import React from 'react'
import {atoms as a, useBreakpoints, ViewStyleProp} from '#/alf'
export function useGutterStyles({
top,
bottom,
}: {
top?: boolean
bottom?: boolean
} = {}) {
const {gtMobile} = useBreakpoints()
return React.useMemo<ViewStyleProp['style']>(() => {
return [
a.px_lg,
top && a.pt_md,
bottom && a.pb_md,
gtMobile && [a.px_xl, top && a.pt_lg, bottom && a.pb_lg],
]
}, [gtMobile, top, bottom])
}
-100
View File
@@ -1,100 +0,0 @@
import React, {useContext, useMemo} from 'react'
import {View, ViewStyle} from 'react-native'
import {StyleProp} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {ScrollView} from '#/view/com/util/Views'
import {CenteredView} from '#/view/com/util/Views'
import {atoms as a} from '#/alf'
// Every screen should have a Layout component wrapping it.
// This component provides a default padding for the top of the screen.
// This allows certain screens to avoid the top padding if they want to.
const LayoutContext = React.createContext({
withinScreen: false,
topPaddingDisabled: false,
withinScrollView: false,
})
/**
* Every screen should have a Layout.Screen component wrapping it.
* This component provides a default padding for the top of the screen
* and height/minHeight
*/
let Screen = ({
disableTopPadding = false,
style,
...props
}: React.ComponentProps<typeof View> & {
disableTopPadding?: boolean
style?: StyleProp<ViewStyle>
}): React.ReactNode => {
const {top} = useSafeAreaInsets()
const context = useMemo(
() => ({
withinScreen: true,
topPaddingDisabled: disableTopPadding,
withinScrollView: false,
}),
[disableTopPadding],
)
return (
<LayoutContext.Provider value={context}>
<View
style={[
{paddingTop: disableTopPadding ? 0 : top},
a.util_screen_outer,
style,
]}
{...props}
/>
</LayoutContext.Provider>
)
}
Screen = React.memo(Screen)
export {Screen}
let Header = (
props: React.ComponentProps<typeof ViewHeader>,
): React.ReactNode => {
const {withinScrollView} = useContext(LayoutContext)
if (!withinScrollView) {
return (
<CenteredView topBorder={false} sideBorders>
<ViewHeader showOnDesktop showBorder {...props} />
</CenteredView>
)
} else {
return <ViewHeader showOnDesktop showBorder {...props} />
}
}
Header = React.memo(Header)
export {Header}
let Content = ({
style,
contentContainerStyle,
...props
}: React.ComponentProps<typeof ScrollView> & {
style?: StyleProp<ViewStyle>
contentContainerStyle?: StyleProp<ViewStyle>
}): React.ReactNode => {
const context = useContext(LayoutContext)
const newContext = useMemo(
() => ({...context, withinScrollView: true}),
[context],
)
return (
<LayoutContext.Provider value={newContext}>
<ScrollView
style={[a.flex_1, style]}
contentContainerStyle={[{paddingBottom: 100}, contentContainerStyle]}
{...props}
/>
</LayoutContext.Provider>
)
}
Content = React.memo(Content)
export {Content}
+160
View File
@@ -0,0 +1,160 @@
import React from 'react'
import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from '#/lib/routes/types'
import {isIOS} from '#/platform/detection'
import {useSetDrawerOpen} from '#/state/shell'
import {
atoms as a,
useBreakpoints,
useGutterStyles,
useTheme,
ViewStyleProp,
} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
import {Text} from '#/components/Typography'
const BUTTON_VISUAL_ALIGNMENT_OFFSET = 3
const BUTTON_SIZE = 34 // small button
export function Outer({children}: {children: React.ReactNode}) {
const t = useTheme()
const gutter = useGutterStyles()
return (
<View
style={[
a.w_full,
a.border_b,
a.flex_row,
a.align_center,
a.gap_sm,
gutter,
a.py_sm,
t.atoms.border_contrast_low,
]}>
<View style={[a.flex_1]}>{children}</View>
</View>
)
}
export function Content({children}: {children: React.ReactNode}) {
return (
<View
style={[
a.flex_1,
a.justify_center,
a.py_xs,
isIOS && a.align_center,
{
paddingHorizontal: BUTTON_SIZE + a.gap_sm.gap,
minHeight: BUTTON_SIZE,
},
]}>
{children}
</View>
)
}
export function Slot({
children,
style,
}: {children?: React.ReactNode} & ViewStyleProp) {
return (
<View
style={[
a.absolute,
a.inset_0,
a.z_50,
{
width: BUTTON_SIZE,
right: 'auto',
},
style,
]}>
{children}
</View>
)
}
export function BackButton() {
const {_} = useLingui()
const navigation = useNavigation<NavigationProp>()
const onPressBack = React.useCallback(() => {
if (navigation.canGoBack()) {
navigation.goBack()
} else {
navigation.navigate('Home')
}
}, [navigation])
return (
<Slot>
<Button
label={_(msg`Go back`)}
size="small"
variant="ghost"
color="secondary"
shape="round"
onPress={onPressBack}
style={[{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}]}>
<ButtonIcon icon={ArrowLeft} size="lg" />
</Button>
</Slot>
)
}
export function MenuButton() {
const {_} = useLingui()
const setDrawerOpen = useSetDrawerOpen()
const {gtMobile} = useBreakpoints()
const onPress = React.useCallback(() => {
setDrawerOpen(true)
}, [setDrawerOpen])
return gtMobile ? null : (
<Slot>
<Button
label={_(msg`Open drawer menu`)}
size="small"
variant="ghost"
color="secondary"
shape="round"
onPress={onPress}
style={[{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}]}>
<ButtonIcon icon={Menu} size="lg" />
</Button>
</Slot>
)
}
export function TitleText({children}: {children: React.ReactNode}) {
const {gtMobile} = useBreakpoints()
return (
<Text
style={[
a.text_lg,
a.font_heavy,
a.leading_snug,
gtMobile && [a.text_xl],
]}>
{children}
</Text>
)
}
export function SubtitleText({children}: {children: React.ReactNode}) {
const t = useTheme()
return (
<Text style={[a.text_sm, a.leading_tight, t.atoms.text_contrast_medium]}>
{children}
</Text>
)
}
+194
View File
@@ -0,0 +1,194 @@
import React, {useContext, useMemo} from 'react'
import {StyleSheet, View, ViewProps, ViewStyle} from 'react-native'
import {StyleProp} from 'react-native'
import {
KeyboardAwareScrollView,
KeyboardAwareScrollViewProps,
} from 'react-native-keyboard-controller'
import Animated, {AnimatedScrollViewProps} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {isWeb} from '#/platform/detection'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
export * as Header from '#/components/Layout/Header'
// Every screen should have a Layout component wrapping it.
// This component provides a default padding for the top of the screen.
// This allows certain screens to avoid the top padding if they want to.
const LayoutContext = React.createContext({
withinScreen: false,
topPaddingDisabled: false,
withinScrollView: false,
})
export type ScreenProps = React.ComponentProps<typeof View> & {
disableTopPadding?: boolean
style?: StyleProp<ViewStyle>
}
/**
* Every screen should have a Layout.Screen component wrapping it.
* This component provides a default padding for the top of the screen
* and height/minHeight
*/
export const Screen = React.memo(function Screen({
disableTopPadding = false,
style,
...props
}: ScreenProps) {
const {top} = useSafeAreaInsets()
const context = useMemo(
() => ({
withinScreen: true,
topPaddingDisabled: disableTopPadding,
withinScrollView: false,
}),
[disableTopPadding],
)
return (
<LayoutContext.Provider value={context}>
<View
style={[
{paddingTop: disableTopPadding ? 0 : top},
a.util_screen_outer,
style,
]}
{...props}
/>
</LayoutContext.Provider>
)
})
export type ContentProps = AnimatedScrollViewProps & {
style?: StyleProp<ViewStyle>
contentContainerStyle?: StyleProp<ViewStyle>
}
export const Content = React.memo(function Content({
children,
style,
contentContainerStyle,
...props
}: ContentProps) {
const context = useContext(LayoutContext)
const newContext = useMemo(
() => ({...context, withinScrollView: true}),
[context],
)
return (
<LayoutContext.Provider value={newContext}>
<Animated.ScrollView
id="content"
style={[
styles.scrollViewCommonStyles,
style,
styles.scrollViewOverrideStyles,
]}
contentContainerStyle={[
styles.scrollViewContentContainer,
contentContainerStyle,
]}
{...props}>
{isWeb ? (
// @ts-ignore web only -esb
<Center>{children}</Center>
) : (
children
)}
</Animated.ScrollView>
</LayoutContext.Provider>
)
})
export type KeyboardAwareContentProps = KeyboardAwareScrollViewProps & {
children: React.ReactNode
contentContainerStyle?: StyleProp<ViewStyle>
}
export const KeyboardAwareContent = React.forwardRef(function LayoutScrollView(
{children, style, contentContainerStyle, ...props}: KeyboardAwareContentProps,
_ref: React.Ref<typeof KeyboardAwareScrollView>,
) {
return (
<KeyboardAwareScrollView
style={[
styles.scrollViewCommonStyles,
style,
styles.scrollViewOverrideStyles,
]}
contentContainerStyle={[
styles.scrollViewContentContainer,
contentContainerStyle,
]}
keyboardShouldPersistTaps="handled"
{...props}>
{isWeb ? <Center>{children}</Center> : children}
</KeyboardAwareScrollView>
)
})
export const Center = React.forwardRef(function LayoutContent(
{children, style, ...props}: ViewProps,
ref: React.Ref<View>,
) {
const t = useTheme()
const {gtMobile} = useBreakpoints()
return (
<>
{gtMobile && (
<View
style={[
a.fixed,
a.inset_0,
a.border_l,
a.border_r,
t.atoms.border_contrast_low,
{
width: 602,
left: '50%',
transform: [
{
translateX: '-50%',
},
],
},
]}
/>
)}
<View
ref={ref}
style={[
a.util_screen_outer,
a.w_full,
a.mx_auto,
gtMobile && {
maxWidth: 600,
},
style,
]}
{...props}>
{children}
</View>
</>
)
})
const styles = StyleSheet.create({
scrollViewCommonStyles: {
width: '100%',
},
scrollViewContentContainer: {
paddingBottom: 100,
},
/**
* Applied last to ensure they override any locally-defined styles. Use sparingly.
*/
scrollViewOverrideStyles: {
/*
* Ensures fixed items within `ScrollView` are fixed relative to window.
*/
transform: 'unset',
},
})
+8 -1
View File
@@ -21,8 +21,15 @@ export function AboutSettingsScreen({}: Props) {
return (
<Layout.Screen>
<Layout.Header title={_(msg`About`)} />
<Layout.Content>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>About</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
</Layout.Header.Outer>
<SettingsList.Container>
<SettingsList.LinkItem
to="https://bsky.social/about/support/tos"
@@ -39,8 +39,15 @@ export function AccessibilitySettingsScreen({}: Props) {
return (
<Layout.Screen>
<Layout.Header title={_(msg`Accessibility`)} />
<Layout.Content>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Accessibility</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
</Layout.Header.Outer>
<SettingsList.Container>
<SettingsList.Group contentContainerStyle={[a.gap_sm]}>
<SettingsList.ItemIcon icon={AccessibilityIcon} />
+9 -1
View File
@@ -38,8 +38,16 @@ export function AccountSettingsScreen({}: Props) {
return (
<Layout.Screen>
<Layout.Header title={_(msg`Account`)} />
<Layout.Content>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Account</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
</Layout.Header.Outer>
<SettingsList.Container>
<SettingsList.Item>
<SettingsList.ItemIcon icon={EnvelopeIcon} />
+9 -2
View File
@@ -1,7 +1,7 @@
import React from 'react'
import {Alert, View} from 'react-native'
import {Image} from 'expo-image'
import {msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as AppIcon from '@mozzius/expo-dynamic-app-icon'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
@@ -20,9 +20,16 @@ export function AppIconSettingsScreen({}: Props) {
return (
<Layout.Screen>
<Layout.Header title={_('App Icon')} />
<Layout.Content
contentContainerStyle={[a.py_2xl, a.px_xl, {paddingBottom: 100}]}>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>App Icon</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
</Layout.Header.Outer>
<Text style={[a.text_lg, a.font_heavy]}>Defaults</Text>
<View style={[a.flex_row, a.flex_wrap]}>
{sets.defaults.map(icon => (
+8 -1
View File
@@ -44,8 +44,15 @@ export function AppPasswordsScreen({}: Props) {
return (
<Layout.Screen testID="AppPasswordsScreen">
<Layout.Header title={_(msg`App Passwords`)} />
<Layout.Content>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>App Passwords</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
</Layout.Header.Outer>
{error ? (
<ErrorScreen
title={_(msg`Oops!`)}
+8 -1
View File
@@ -79,8 +79,15 @@ export function AppearanceSettingsScreen({}: Props) {
return (
<LayoutAnimationConfig skipExiting skipEntering>
<Layout.Screen testID="preferencesThreadsScreen">
<Layout.Header title={_(msg`Appearance`)} />
<Layout.Content>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Appearance</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
</Layout.Header.Outer>
<SettingsList.Container>
<AppearanceToggleButtonGroup
title={_(msg`Color mode`)}
@@ -32,8 +32,15 @@ export function ContentAndMediaSettingsScreen({}: Props) {
return (
<Layout.Screen>
<Layout.Header title={_(msg`Content and Media`)} />
<Layout.Content>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Content & Media</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
</Layout.Header.Outer>
<SettingsList.Container>
<SettingsList.LinkItem
to="/settings/saved-feeds"
@@ -1,7 +1,6 @@
import {Fragment} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/macro'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {
@@ -23,11 +22,17 @@ type Props = NativeStackScreenProps<
'PreferencesExternalEmbeds'
>
export function ExternalMediaPreferencesScreen({}: Props) {
const {_} = useLingui()
return (
<Layout.Screen testID="externalMediaPreferencesScreen">
<Layout.Header title={_(msg`External Media Preferences`)} />
<Layout.Content>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>External Media Preferences</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
</Layout.Header.Outer>
<SettingsList.Container>
<SettingsList.Item>
<Admonition type="info" style={[a.flex_1]}>
@@ -46,8 +46,15 @@ export function FollowingFeedPreferencesScreen({}: Props) {
return (
<Layout.Screen testID="followingFeedPreferencesScreen">
<Layout.Header title={_(msg`Following Feed Preferences`)} />
<Layout.Content>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Following Feed Preferences</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
</Layout.Header.Outer>
<SettingsList.Container>
<SettingsList.Item>
<Admonition type="tip" style={[a.flex_1]}>
+8 -1
View File
@@ -64,8 +64,15 @@ export function LanguageSettingsScreen({}: Props) {
return (
<Layout.Screen testID="PreferencesLanguagesScreen">
<Layout.Header title={_(msg`Languages`)} />
<Layout.Content>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Languages</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
</Layout.Header.Outer>
<SettingsList.Container>
<SettingsList.Group iconInset={false}>
<SettingsList.ItemText>
@@ -33,8 +33,15 @@ export function NotificationSettingsScreen({}: Props) {
return (
<Layout.Screen>
<Layout.Header title={_(msg`Notification Settings`)} />
<Layout.Content>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Notification Settings</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
</Layout.Header.Outer>
{isQueryError ? (
<Error
title={_(msg`Oops!`)}
@@ -29,8 +29,15 @@ export function PrivacyAndSecuritySettingsScreen({}: Props) {
return (
<Layout.Screen>
<Layout.Header title={_(msg`Privacy and Security`)} />
<Layout.Content>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Privacy and Security</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
</Layout.Header.Outer>
<SettingsList.Container>
<SettingsList.Item>
<SettingsList.ItemIcon
+9 -1
View File
@@ -73,8 +73,16 @@ export function SettingsScreen({}: Props) {
return (
<Layout.Screen>
<Layout.Header title={_(msg`Settings`)} />
<Layout.Content>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Settings</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
</Layout.Header.Outer>
<SettingsList.Container>
<View
style={[
+10 -1
View File
@@ -38,8 +38,17 @@ export function ThreadPreferencesScreen({}: Props) {
return (
<Layout.Screen testID="threadPreferencesScreen">
<Layout.Header title={_(msg`Thread Preferences`)} />
<Layout.Content>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Thread Preferences</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<SettingsList.Container>
<SettingsList.Group>
<SettingsList.ItemIcon icon={BubblesIcon} />
+4 -50
View File
@@ -1,9 +1,6 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {StyleSheet, View} from 'react-native'
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
@@ -14,9 +11,9 @@ import {
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {getCurrentRoute, isStateAtTabRoot, isTab} from '#/lib/routes/helpers'
import {getCurrentRoute, isTab} from '#/lib/routes/helpers'
import {makeProfileLink} from '#/lib/routes/links'
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {isInvalidHandle} from '#/lib/strings/handles'
import {emitSoftReset} from '#/state/events'
import {useFetchHandle} from '#/state/queries/handle'
@@ -101,47 +98,6 @@ function ProfileCard() {
)
}
const HIDDEN_BACK_BNT_ROUTES = ['StarterPackWizard', 'StarterPackEdit']
function BackBtn() {
const {isTablet} = useWebMediaQueries()
const pal = usePalette('default')
const navigation = useNavigation<NavigationProp>()
const {_} = useLingui()
const shouldShow = useNavigationState(
state =>
!isStateAtTabRoot(state) &&
!HIDDEN_BACK_BNT_ROUTES.includes(getCurrentRoute(state).name),
)
const onPressBack = React.useCallback(() => {
if (navigation.canGoBack()) {
navigation.goBack()
} else {
navigation.navigate('Home')
}
}, [navigation])
if (!shouldShow || isTablet) {
return <></>
}
return (
<TouchableOpacity
testID="viewHeaderBackOrMenuBtn"
onPress={onPressBack}
style={styles.backBtn}
accessibilityRole="button"
accessibilityLabel={_(msg`Go back`)}
accessibilityHint="">
<FontAwesomeIcon
size={24}
icon="angle-left"
style={pal.text as FontAwesomeIconStyle}
/>
</TouchableOpacity>
)
}
interface NavItemProps {
count?: string
href: string
@@ -381,8 +337,6 @@ export function DesktopLeftNav() {
{hasSession && (
<>
<BackBtn />
<NavItem
href="/"
icon={